Skip to content
Merged
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 numberDiff line numberDiff line change
Expand Up@@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- Bump `@metamask/profile-sync-controller` from `^28.1.1` to `^28.2.0` ([#9119](https://github.com/MetaMask/core/pull/9119))

### Fixed

- Compare internal order codes (from canonical order `id`) instead of provider-native `providerOrderId` when merging orders in `RampsController.addOrder` and `RampsController.getOrder` ([#9159](https://github.com/MetaMask/core/pull/9159))

## [14.2.0]

### Changed
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -229,7 +229,7 @@ export type RampsControllerGetQuotesAction = {

/**
* Adds or updates a V2 order in controller state.
* If an order with the same providerOrderId already exists, the incoming
* If an order with the same internal order code already exists, the incoming
* fields are merged on top of the existing order so that fields not present
* in the update (e.g. paymentDetails from the Transak API) are preserved.
*
Expand Down
71 changes: 70 additions & 1 deletion packages/ramps-controller/src/RampsController.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -7100,12 +7100,39 @@ describe('RampsController', () => {
rootMessenger.call('RampsController:addOrder', mockOrder);
rootMessenger.call('RampsController:addOrder', {
...mockOrder,
id: '/providers/transak-staging/orders/def-456',
providerOrderId: 'def-456',
});

expect(controller.state.orders).toHaveLength(2);
});
});

it('merges orders using internal order id when providerOrderId differs', async () => {
await withController(({ controller, rootMessenger }) => {
const precreatedOrder = createMockOrder({
id: '/providers/paypal/orders/internal-order-123',
providerOrderId: 'internal-order-123',
status: RampsOrderStatus.Precreated,
});
rootMessenger.call('RampsController:addOrder', precreatedOrder);

const apiOrder = createMockOrder({
id: '/providers/paypal/orders/internal-order-123',
providerOrderId: 'provider-native-order-456',
status: RampsOrderStatus.Pending,
});
rootMessenger.call('RampsController:addOrder', apiOrder);

expect(controller.state.orders).toHaveLength(1);
expect(controller.state.orders[0]?.status).toBe(
RampsOrderStatus.Pending,
);
expect(controller.state.orders[0]?.providerOrderId).toBe(
'internal-order-123',
);
});
});
});

describe('removeOrder', () => {
Expand DownExpand Up@@ -7334,6 +7361,42 @@ describe('RampsController', () => {
});
});

it('merges precreated order when API returns a different providerOrderId', async () => {
await withController(async ({ controller, rootMessenger }) => {
const precreatedOrder = createMockOrder({
id: '/providers/paypal/orders/internal-order-123',
providerOrderId: 'internal-order-123',
status: RampsOrderStatus.Precreated,
});
rootMessenger.call('RampsController:addOrder', precreatedOrder);

const apiOrder = createMockOrder({
id: '/providers/paypal/orders/internal-order-123',
providerOrderId: 'provider-native-order-456',
status: RampsOrderStatus.Pending,
});
rootMessenger.registerActionHandler(
'RampsService:getOrder',
async () => apiOrder,
);

await rootMessenger.call(
'RampsController:getOrder',
'paypal',
'internal-order-123',
'0xabc',
);

expect(controller.state.orders).toHaveLength(1);
expect(controller.state.orders[0]?.status).toBe(
RampsOrderStatus.Pending,
);
expect(controller.state.orders[0]?.providerOrderId).toBe(
'internal-order-123',
);
});
});

it('uses wallet param when updating existing order and API omits walletAddress', async () => {
await withController(async ({ controller, rootMessenger }) => {
const existingOrder = createMockOrder({
Expand DownExpand Up@@ -7509,6 +7572,7 @@ describe('RampsController', () => {
it('publishes orderStatusChanged when order status transitions', async () => {
await withController(async ({ rootMessenger, messenger }) => {
const pendingOrder = createMockOrder({
id: '/providers/transak/orders/status-change-1',
providerOrderId: 'status-change-1',
status: RampsOrderStatus.Pending,
provider: createMockProvider({
Expand DownExpand Up@@ -7538,7 +7602,10 @@ describe('RampsController', () => {
await jest.advanceTimersByTimeAsync(0);

expect(statusChangedListener).toHaveBeenCalledWith({
order: updatedOrder,
order: {
...updatedOrder,
providerOrderId: 'status-change-1',
},
previousStatus: RampsOrderStatus.Pending,
});

Expand DownExpand Up@@ -7701,6 +7768,7 @@ describe('RampsController', () => {
it('skips orders without providerOrderId', async () => {
await withController(async ({ rootMessenger }) => {
const orderNoId = createMockOrder({
id: undefined,
providerOrderId: '',
status: RampsOrderStatus.Pending,
provider: createMockProvider({
Expand DownExpand Up@@ -7751,6 +7819,7 @@ describe('RampsController', () => {
it('passes provider id through to service without stripping prefix', async () => {
await withController(async ({ rootMessenger }) => {
const order = createMockOrder({
id: '/providers/transak/orders/strip-prefix-1',
providerOrderId: 'strip-prefix-1',
status: RampsOrderStatus.Pending,
provider: createMockProvider({
Expand Down
52 changes: 43 additions & 9 deletions packages/ramps-controller/src/RampsController.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -739,6 +739,31 @@ export function normalizeProviderCode(providerCode: string): string {
return providerCode.replace(/^\/providers\//u, '');
}

/**
* Returns the internal MetaMask order code used for state lookups and polling.
* Prefers the code embedded in the canonical order `id` path over `providerOrderId`,
* which may contain the provider's native order identifier.
*
* @param orderOrId - Order fields or a full order id / order code string.
* @returns The internal order code.
*/
export function getInternalOrderCode(
orderOrId: Pick<RampsOrder, 'id' | 'providerOrderId'> | string,
): string {
if (typeof orderOrId === 'string') {
return orderOrId.includes('/orders/')
? orderOrId.split('/orders/')[1]
: orderOrId;
}

const { id, providerOrderId } = orderOrId;
if (id?.includes('/orders/')) {
return id.split('/orders/')[1];
}

return providerOrderId;
}

// === ORDER POLLING CONSTANTS ===

const TERMINAL_ORDER_STATUSES = new Set<RampsOrderStatus>([
Expand DownExpand Up@@ -2088,23 +2113,29 @@ export class RampsController extends BaseController<

/**
* Adds or updates a V2 order in controller state.
* If an order with the same providerOrderId already exists, the incoming
* If an order with the same internal order code already exists, the incoming
* fields are merged on top of the existing order so that fields not present
* in the update (e.g. paymentDetails from the Transak API) are preserved.
*
* @param order - The RampsOrder to add or update.
*/
addOrder(order: RampsOrder): void {
const internalOrderCode = getInternalOrderCode(order);
const healedOrder = {
...order,
providerOrderId: internalOrderCode,
};

this.update((state) => {
const idx = state.orders.findIndex(
(existing) => existing.providerOrderId === order.providerOrderId,
(existing) => getInternalOrderCode(existing) === internalOrderCode,
);
if (idx === -1) {
state.orders.push(order as Draft<RampsOrder>);
state.orders.push(healedOrder as Draft<RampsOrder>);
} else {
state.orders[idx] = {
...state.orders[idx],
...order,
...healedOrder,
} as Draft<RampsOrder>;
}
});
Expand DownExpand Up@@ -2306,9 +2337,7 @@ export class RampsController extends BaseController<
}): void {
const { orderId, providerCode, walletAddress, chainId } = params;

const orderCode = orderId.includes('/orders/')
? orderId.split('/orders/')[1]
: orderId;
const orderCode = getInternalOrderCode(orderId);
if (!orderCode?.trim()) {
return;
}
Expand DownExpand Up@@ -2368,15 +2397,20 @@ export class RampsController extends BaseController<
);

const healedWalletAddress = order.walletAddress || wallet;
const internalOrderCode = getInternalOrderCode({
id: order.id,
providerOrderId: orderCode,
});
const healedOrder = {
...order,
walletAddress: healedWalletAddress,
providerOrderId: orderCode,
providerOrderId: internalOrderCode,
};

this.update((state) => {
const idx = state.orders.findIndex(
(existing: RampsOrder) => existing.providerOrderId === orderCode,
(existing: RampsOrder) =>
getInternalOrderCode(existing) === internalOrderCode,
);
if (idx === -1) {
state.orders.push(healedOrder as Draft<RampsOrder>);
Expand Down
1 change: 1 addition & 0 deletions packages/ramps-controller/src/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -64,6 +64,7 @@ export type {
export {
RampsController,
getDefaultRampsControllerState,
getInternalOrderCode,
normalizeProviderCode,
RAMPS_CONTROLLER_REQUIRED_SERVICE_ACTIONS,
} from './RampsController';
Expand Down
Loading