Skip to content

Commit 8d570c5

Browse files
authored
fix(ramps-controller): remove nested widgetUrl field from Quote type (#7881)
## Explanation During development of the quotes functionality in PR #7747, the `widgetUrl` field was mistakenly added to the nested `quote.quote` object when the API format was still being finalized. The API has since been corrected to use a top-level `url` field instead. This PR removes the incorrect nested structure before the quotes feature is released. ## Changes - Remove `widgetUrl` from the nested `quote.quote` object in the `Quote` type - Simplify `getWidgetUrl()` method to only check the top-level `quote.url` field - Update all test mocks to use the top-level `url` field instead of nested `widgetUrl` - Keep all provider metadata types (`ProviderType`, `ProviderFeatures`, etc.) that were correctly added ## References Related to PR #7747 tests: https://github.com/user-attachments/assets/07e4ea98-6436-4613-b118-1fbcb92af185 https://github.com/user-attachments/assets/6bab6f39-e526-4e71-a9ef-450394eacd06 <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Introduces a breaking async API and adds a network call in the widget-launch path; failures or malformed responses now affect redirect-provider flows and must be handled by consumers. > > **Overview** > **BREAKING:** Changes `RampsController.getWidgetUrl()` from a synchronous `quote.quote.widgetUrl` lookup to an async call that fetches widget details from the `quote.quote.buyURL` endpoint via a new `RampsService:getBuyWidgetUrl` messenger action. > > Extends `RampsService` with `getBuyWidgetUrl()` (with retry/policy handling and response validation), updates the `Quote` type to replace `widgetUrl` with `buyURL`, exports new `BuyWidget`/browser types and action types, and updates/extends unit tests to cover success and failure cases. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 3a70d9e. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent adafd9c commit 8d570c5

7 files changed

Lines changed: 268 additions & 26 deletions

File tree

packages/ramps-controller/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Changed
11+
12+
- **BREAKING:** Make `getWidgetUrl()` async to fetch the actual provider widget URL from the `buyURL` endpoint ([#7881](https://github.com/MetaMask/core/pull/7881))
13+
1014
## [7.1.0]
1115

1216
### Fixed

packages/ramps-controller/src/RampsController.test.ts

Lines changed: 78 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import type {
3535
RampsServiceGetProvidersAction,
3636
RampsServiceGetPaymentMethodsAction,
3737
RampsServiceGetQuotesAction,
38+
RampsServiceGetBuyWidgetUrlAction,
3839
} from './RampsService-method-action-types';
3940
import { RequestStatus } from './RequestCache';
4041

@@ -3686,7 +3687,6 @@ describe('RampsController', () => {
36863687
amountOut: '0.05',
36873688
paymentMethod: '/payments/debit-credit-card',
36883689
amountOutInFiat: 98,
3689-
widgetUrl: 'https://buy.moonpay.com/widget?txId=123',
36903690
},
36913691
metadata: {
36923692
reliability: 95,
@@ -5651,26 +5651,36 @@ describe('RampsController', () => {
56515651
});
56525652

56535653
describe('getWidgetUrl', () => {
5654-
it('returns widget URL when present in quote', async () => {
5655-
await withController(({ controller }) => {
5654+
it('fetches and returns widget URL via RampsService messenger', async () => {
5655+
await withController(async ({ controller, rootMessenger }) => {
56565656
const quote: Quote = {
5657-
provider: '/providers/moonpay',
5657+
provider: '/providers/transak-staging',
56585658
quote: {
56595659
amountIn: 100,
56605660
amountOut: '0.05',
56615661
paymentMethod: '/payments/debit-credit-card',
5662-
widgetUrl: 'https://buy.moonpay.com/widget?txId=123',
5662+
buyURL:
5663+
'https://on-ramp.uat-api.cx.metamask.io/providers/transak-staging/buy-widget',
56635664
},
56645665
};
56655666

5666-
const widgetUrl = controller.getWidgetUrl(quote);
5667+
rootMessenger.registerActionHandler(
5668+
'RampsService:getBuyWidgetUrl',
5669+
async () => ({
5670+
url: 'https://global.transak.com/?apiKey=test',
5671+
browser: 'APP_BROWSER' as const,
5672+
orderId: null,
5673+
}),
5674+
);
5675+
5676+
const widgetUrl = await controller.getWidgetUrl(quote);
56675677

5668-
expect(widgetUrl).toBe('https://buy.moonpay.com/widget?txId=123');
5678+
expect(widgetUrl).toBe('https://global.transak.com/?apiKey=test');
56695679
});
56705680
});
56715681

5672-
it('returns null when widget URL is not present', async () => {
5673-
await withController(({ controller }) => {
5682+
it('returns null when buyURL is not present', async () => {
5683+
await withController(async ({ controller }) => {
56745684
const quote: Quote = {
56755685
provider: '/providers/transak',
56765686
quote: {
@@ -5680,19 +5690,73 @@ describe('RampsController', () => {
56805690
},
56815691
};
56825692

5683-
const widgetUrl = controller.getWidgetUrl(quote);
5693+
const widgetUrl = await controller.getWidgetUrl(quote);
56845694

56855695
expect(widgetUrl).toBeNull();
56865696
});
56875697
});
56885698

56895699
it('returns null when quote object is malformed', async () => {
5690-
await withController(({ controller }) => {
5700+
await withController(async ({ controller }) => {
56915701
const quote = {
56925702
provider: '/providers/moonpay',
56935703
} as unknown as Quote;
56945704

5695-
const widgetUrl = controller.getWidgetUrl(quote);
5705+
const widgetUrl = await controller.getWidgetUrl(quote);
5706+
5707+
expect(widgetUrl).toBeNull();
5708+
});
5709+
});
5710+
5711+
it('returns null when service call throws an error', async () => {
5712+
await withController(async ({ controller, rootMessenger }) => {
5713+
const quote: Quote = {
5714+
provider: '/providers/transak-staging',
5715+
quote: {
5716+
amountIn: 100,
5717+
amountOut: '0.05',
5718+
paymentMethod: '/payments/debit-credit-card',
5719+
buyURL:
5720+
'https://on-ramp.uat-api.cx.metamask.io/providers/transak-staging/buy-widget',
5721+
},
5722+
};
5723+
5724+
rootMessenger.registerActionHandler(
5725+
'RampsService:getBuyWidgetUrl',
5726+
async () => {
5727+
throw new Error('Network error');
5728+
},
5729+
);
5730+
5731+
const widgetUrl = await controller.getWidgetUrl(quote);
5732+
5733+
expect(widgetUrl).toBeNull();
5734+
});
5735+
});
5736+
5737+
it('returns null when service returns BuyWidget with null url', async () => {
5738+
await withController(async ({ controller, rootMessenger }) => {
5739+
const quote: Quote = {
5740+
provider: '/providers/transak-staging',
5741+
quote: {
5742+
amountIn: 100,
5743+
amountOut: '0.05',
5744+
paymentMethod: '/payments/debit-credit-card',
5745+
buyURL:
5746+
'https://on-ramp.uat-api.cx.metamask.io/providers/transak-staging/buy-widget',
5747+
},
5748+
};
5749+
5750+
rootMessenger.registerActionHandler(
5751+
'RampsService:getBuyWidgetUrl',
5752+
async () => ({
5753+
url: null as unknown as string,
5754+
browser: 'APP_BROWSER' as const,
5755+
orderId: null,
5756+
}),
5757+
);
5758+
5759+
const widgetUrl = await controller.getWidgetUrl(quote);
56965760

56975761
expect(widgetUrl).toBeNull();
56985762
});
@@ -5820,7 +5884,8 @@ type RootMessenger = Messenger<
58205884
| RampsServiceGetTokensAction
58215885
| RampsServiceGetProvidersAction
58225886
| RampsServiceGetPaymentMethodsAction
5823-
| RampsServiceGetQuotesAction,
5887+
| RampsServiceGetQuotesAction
5888+
| RampsServiceGetBuyWidgetUrlAction,
58245889
MessengerEvents<RampsControllerMessenger>
58255890
>;
58265891

packages/ramps-controller/src/RampsController.ts

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import type {
2828
RampsServiceGetProvidersAction,
2929
RampsServiceGetPaymentMethodsAction,
3030
RampsServiceGetQuotesAction,
31+
RampsServiceGetBuyWidgetUrlAction,
3132
} from './RampsService-method-action-types';
3233
import type {
3334
RequestCache as RequestCacheType,
@@ -69,6 +70,7 @@ export const RAMPS_CONTROLLER_REQUIRED_SERVICE_ACTIONS: readonly RampsServiceAct
6970
'RampsService:getProviders',
7071
'RampsService:getPaymentMethods',
7172
'RampsService:getQuotes',
73+
'RampsService:getBuyWidgetUrl',
7274
];
7375

7476
/**
@@ -322,7 +324,8 @@ type AllowedActions =
322324
| RampsServiceGetTokensAction
323325
| RampsServiceGetProvidersAction
324326
| RampsServiceGetPaymentMethodsAction
325-
| RampsServiceGetQuotesAction;
327+
| RampsServiceGetQuotesAction
328+
| RampsServiceGetBuyWidgetUrlAction;
326329

327330
/**
328331
* Published when the state of {@link RampsController} changes.
@@ -1614,13 +1617,28 @@ export class RampsController extends BaseController<
16141617
}
16151618

16161619
/**
1617-
* Extracts the widget URL from a quote for redirect providers.
1618-
* Returns the widget URL if available, or null if the quote doesn't have one.
1620+
* Fetches the widget URL from a quote for redirect providers.
1621+
* Makes a request to the buyURL endpoint via the RampsService to get the
1622+
* actual provider widget URL, using the injected fetch and retry policy.
16191623
*
1620-
* @param quote - The quote to extract the widget URL from.
1621-
* @returns The widget URL string, or null if not available.
1624+
* @param quote - The quote to fetch the widget URL from.
1625+
* @returns Promise resolving to the widget URL string, or null if not available.
16221626
*/
1623-
getWidgetUrl(quote: Quote): string | null {
1624-
return quote.quote?.widgetUrl ?? null;
1627+
async getWidgetUrl(quote: Quote): Promise<string | null> {
1628+
const buyUrl = quote.quote?.buyURL;
1629+
if (!buyUrl) {
1630+
return null;
1631+
}
1632+
1633+
try {
1634+
const buyWidget = await this.messenger.call(
1635+
'RampsService:getBuyWidgetUrl',
1636+
buyUrl,
1637+
);
1638+
return buyWidget.url ?? null;
1639+
} catch (error) {
1640+
console.error('Error fetching widget URL:', error);
1641+
return null;
1642+
}
16251643
}
16261644
}

packages/ramps-controller/src/RampsService-method-action-types.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,19 @@ export type RampsServiceGetQuotesAction = {
9696
handler: RampsService['getQuotes'];
9797
};
9898

99+
/**
100+
* Fetches the buy widget data from a buy URL endpoint.
101+
* Makes a request to the buyURL (as provided in a quote) to get the actual
102+
* provider widget URL, browser type, and order ID.
103+
*
104+
* @param buyUrl - The full buy URL endpoint to fetch from.
105+
* @returns The buy widget data containing the provider widget URL.
106+
*/
107+
export type RampsServiceGetBuyWidgetUrlAction = {
108+
type: `RampsService:getBuyWidgetUrl`;
109+
handler: RampsService['getBuyWidgetUrl'];
110+
};
111+
99112
/**
100113
* Union of all RampsService action types.
101114
*/
@@ -105,4 +118,5 @@ export type RampsServiceMethodActions =
105118
| RampsServiceGetTokensAction
106119
| RampsServiceGetProvidersAction
107120
| RampsServiceGetPaymentMethodsAction
108-
| RampsServiceGetQuotesAction;
121+
| RampsServiceGetQuotesAction
122+
| RampsServiceGetBuyWidgetUrlAction;

packages/ramps-controller/src/RampsService.test.ts

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1710,7 +1710,6 @@ describe('RampsService', () => {
17101710
amountOut: '0.05',
17111711
paymentMethod: '/payments/debit-credit-card',
17121712
amountOutInFiat: 98,
1713-
widgetUrl: 'https://buy.moonpay.com/widget?txId=123',
17141713
},
17151714
metadata: {
17161715
reliability: 95,
@@ -2250,6 +2249,90 @@ describe('RampsService', () => {
22502249
expect(quotesResponse.success).toHaveLength(2);
22512250
});
22522251
});
2252+
2253+
describe('RampsService:getBuyWidgetUrl', () => {
2254+
it('returns buy widget data from the buy URL endpoint', async () => {
2255+
nock('https://on-ramp.uat-api.cx.metamask.io')
2256+
.get('/providers/transak-staging/buy-widget')
2257+
.query({
2258+
sdk: '2.1.6',
2259+
controller: CONTROLLER_VERSION,
2260+
context: 'mobile-ios',
2261+
})
2262+
.reply(200, {
2263+
url: 'https://global.transak.com/?apiKey=test',
2264+
browser: 'APP_BROWSER',
2265+
orderId: null,
2266+
});
2267+
const { rootMessenger } = getService();
2268+
2269+
const buyWidgetPromise = rootMessenger.call(
2270+
'RampsService:getBuyWidgetUrl',
2271+
'https://on-ramp.uat-api.cx.metamask.io/providers/transak-staging/buy-widget',
2272+
);
2273+
await clock.runAllAsync();
2274+
await flushPromises();
2275+
const buyWidget = await buyWidgetPromise;
2276+
2277+
expect(buyWidget).toStrictEqual({
2278+
url: 'https://global.transak.com/?apiKey=test',
2279+
browser: 'APP_BROWSER',
2280+
orderId: null,
2281+
});
2282+
});
2283+
2284+
it('throws when the response is not ok', async () => {
2285+
nock('https://on-ramp.uat-api.cx.metamask.io')
2286+
.get('/providers/transak-staging/buy-widget')
2287+
.query({
2288+
sdk: '2.1.6',
2289+
controller: CONTROLLER_VERSION,
2290+
context: 'mobile-ios',
2291+
})
2292+
.times(4)
2293+
.reply(500, 'Internal Server Error');
2294+
const { service } = getService();
2295+
service.onRetry(() => {
2296+
clock.nextAsync().catch(() => undefined);
2297+
});
2298+
2299+
const buyWidgetPromise = service.getBuyWidgetUrl(
2300+
'https://on-ramp.uat-api.cx.metamask.io/providers/transak-staging/buy-widget',
2301+
);
2302+
await clock.runAllAsync();
2303+
await flushPromises();
2304+
2305+
await expect(buyWidgetPromise).rejects.toThrow(
2306+
`Fetching 'https://on-ramp.uat-api.cx.metamask.io/providers/transak-staging/buy-widget?sdk=2.1.6&controller=${CONTROLLER_VERSION}&context=mobile-ios' failed with status '500'`,
2307+
);
2308+
});
2309+
2310+
it('throws when the response does not contain url field', async () => {
2311+
nock('https://on-ramp.uat-api.cx.metamask.io')
2312+
.get('/providers/transak-staging/buy-widget')
2313+
.query({
2314+
sdk: '2.1.6',
2315+
controller: CONTROLLER_VERSION,
2316+
context: 'mobile-ios',
2317+
})
2318+
.reply(200, {
2319+
browser: 'APP_BROWSER',
2320+
orderId: null,
2321+
});
2322+
const { rootMessenger } = getService();
2323+
2324+
const buyWidgetPromise = rootMessenger.call(
2325+
'RampsService:getBuyWidgetUrl',
2326+
'https://on-ramp.uat-api.cx.metamask.io/providers/transak-staging/buy-widget',
2327+
);
2328+
await clock.runAllAsync();
2329+
await flushPromises();
2330+
2331+
await expect(buyWidgetPromise).rejects.toThrow(
2332+
'Malformed response received from buy widget URL API',
2333+
);
2334+
});
2335+
});
22532336
});
22542337

22552338
/**

0 commit comments

Comments
 (0)