Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/ramps-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Authenticate `RampsService.getPaymentMethods` and `RampsService.getQuotes` by sourcing a bearer token from `AuthenticationController:getBearerToken` and sending it as an `Authorization: Bearer <token>` header ([#8888](https://github.com/MetaMask/core/pull/8888))

## [14.0.0]

### Added
Expand Down
186 changes: 186 additions & 0 deletions packages/ramps-controller/src/RampsService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1879,6 +1879,93 @@ describe('RampsService', () => {
`Fetching 'https://on-ramp-cache.uat-api.cx.metamask.io/v2/regions/us-al/payments?sdk=2.1.6&controller=${CONTROLLER_VERSION}&context=mobile-ios&region=us-al&fiat=usd&crypto=eip155%3A1%2Fslip44%3A60&provider=%2Fproviders%2Fstripe' failed with status '500'`,
);
});

it('sends an Authorization header containing the bearer token', async () => {
const scope = nock('https://on-ramp-cache.uat-api.cx.metamask.io', {
reqheaders: {
Authorization: 'Bearer mock-bearer-token',
},
})
.get('/v2/regions/us-al/payments')
.query({
region: 'us-al',
fiat: 'usd',
crypto: 'eip155:1/slip44:60',
provider: '/providers/stripe',
sdk: '2.1.6',
controller: CONTROLLER_VERSION,
context: 'mobile-ios',
})
.reply(200, mockPaymentMethodsResponse);
const { service } = getService();

const paymentMethodsPromise = service.getPaymentMethods({
region: 'us-al',
fiat: 'usd',
assetId: 'eip155:1/slip44:60',
provider: '/providers/stripe',
});
await jest.runAllTimersAsync();
await flushPromises();
await paymentMethodsPromise;

expect(scope.isDone()).toBe(true);
});

it('requests a bearer token exactly once per call', async () => {
nock('https://on-ramp-cache.uat-api.cx.metamask.io', {
reqheaders: {
Authorization: 'Bearer mock-bearer-token',
},
})
.get('/v2/regions/us-al/payments')
.query({
region: 'us-al',
fiat: 'usd',
crypto: 'eip155:1/slip44:60',
provider: '/providers/stripe',
sdk: '2.1.6',
controller: CONTROLLER_VERSION,
context: 'mobile-ios',
})
.reply(200, mockPaymentMethodsResponse);
const { service, mockGetBearerToken } = getService();

const paymentMethodsPromise = service.getPaymentMethods({
region: 'us-al',
fiat: 'usd',
assetId: 'eip155:1/slip44:60',
provider: '/providers/stripe',
});
await jest.runAllTimersAsync();
await flushPromises();
await paymentMethodsPromise;

expect(mockGetBearerToken).toHaveBeenCalledTimes(1);
});

it('rejects without making an HTTP call when the bearer token cannot be retrieved', async () => {
const interceptor = nock('https://on-ramp-cache.uat-api.cx.metamask.io')
.get('/v2/regions/us-al/payments')
.query(true)
.reply(200, mockPaymentMethodsResponse);
const { service } = getService({
mockGetBearerToken: jest
.fn()
.mockRejectedValue(new Error('Wallet is locked')),
});

await expect(
service.getPaymentMethods({
region: 'us-al',
fiat: 'usd',
assetId: 'eip155:1/slip44:60',
provider: '/providers/stripe',
}),
).rejects.toThrow('Wallet is locked');
expect(interceptor.isDone()).toBe(false);
cleanAll();
});
});

describe('getQuotes', () => {
Expand Down Expand Up @@ -2464,6 +2551,105 @@ describe('RampsService', () => {

expect(quotesResponse.success).toHaveLength(2);
});

it('sends an Authorization header containing the bearer token', async () => {
const scope = nock('https://on-ramp.uat-api.cx.metamask.io', {
reqheaders: {
Authorization: 'Bearer mock-bearer-token',
},
})
.get('/v2/quotes')
.query({
action: 'buy',
sdk: '2.1.6',
controller: CONTROLLER_VERSION,
context: 'mobile-ios',
region: 'us',
fiat: 'usd',
crypto: 'eip155:1/slip44:60',
amount: '100',
walletAddress: '0x1234567890abcdef1234567890abcdef12345678',
payments: '/payments/debit-credit-card',
})
.reply(200, mockQuotesResponse);
const { service } = getService();

const quotesPromise = service.getQuotes({
region: 'us',
fiat: 'usd',
assetId: 'eip155:1/slip44:60',
amount: 100,
walletAddress: '0x1234567890abcdef1234567890abcdef12345678',
paymentMethods: ['/payments/debit-credit-card'],
});
await jest.runAllTimersAsync();
await flushPromises();
await quotesPromise;

expect(scope.isDone()).toBe(true);
});

it('requests a bearer token exactly once per call', async () => {
nock('https://on-ramp.uat-api.cx.metamask.io', {
reqheaders: {
Authorization: 'Bearer mock-bearer-token',
},
})
.get('/v2/quotes')
.query({
action: 'buy',
sdk: '2.1.6',
controller: CONTROLLER_VERSION,
context: 'mobile-ios',
region: 'us',
fiat: 'usd',
crypto: 'eip155:1/slip44:60',
amount: '100',
walletAddress: '0x1234567890abcdef1234567890abcdef12345678',
payments: '/payments/debit-credit-card',
})
.reply(200, mockQuotesResponse);
const { service, mockGetBearerToken } = getService();

const quotesPromise = service.getQuotes({
region: 'us',
fiat: 'usd',
assetId: 'eip155:1/slip44:60',
amount: 100,
walletAddress: '0x1234567890abcdef1234567890abcdef12345678',
paymentMethods: ['/payments/debit-credit-card'],
});
await jest.runAllTimersAsync();
await flushPromises();
await quotesPromise;

expect(mockGetBearerToken).toHaveBeenCalledTimes(1);
});

it('rejects without making an HTTP call when the bearer token cannot be retrieved', async () => {
const interceptor = nock('https://on-ramp.uat-api.cx.metamask.io')
.get('/v2/quotes')
.query(true)
.reply(200, mockQuotesResponse);
const { service } = getService({
mockGetBearerToken: jest
.fn()
.mockRejectedValue(new Error('Wallet is locked')),
});

await expect(
service.getQuotes({
region: 'us',
fiat: 'usd',
assetId: 'eip155:1/slip44:60',
amount: 100,
walletAddress: '0x1234567890abcdef1234567890abcdef12345678',
paymentMethods: ['/payments/debit-credit-card'],
}),
).rejects.toThrow('Wallet is locked');
expect(interceptor.isDone()).toBe(false);
cleanAll();
});
});

describe('RampsService:getBuyWidgetUrl', () => {
Expand Down
8 changes: 6 additions & 2 deletions packages/ramps-controller/src/RampsService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1234,8 +1234,10 @@ export class RampsService {
url.searchParams.set('crypto', options.assetId);
url.searchParams.set('provider', options.provider);

const headers = await this.#getRequestHeaders();

const response = await this.#policy.execute(async () => {
const fetchResponse = await this.#fetch(url);
const fetchResponse = await this.#fetch(url, { headers });
if (!fetchResponse.ok) {
throw new HttpError(
fetchResponse.status,
Expand Down Expand Up @@ -1290,6 +1292,8 @@ export class RampsService {
url.searchParams.set('amount', String(params.amount));
url.searchParams.set('walletAddress', params.walletAddress);

const headers = await this.#getRequestHeaders();

// Add payment methods as array parameters
params.paymentMethods.forEach((paymentMethod) => {
url.searchParams.append('payments', paymentMethod);
Expand All @@ -1306,7 +1310,7 @@ export class RampsService {
}

const response = await this.#policy.execute(async () => {
const fetchResponse = await this.#fetch(url);
const fetchResponse = await this.#fetch(url, { headers });
if (!fetchResponse.ok) {
throw new HttpError(
fetchResponse.status,
Expand Down
Loading