Skip to content
Closed
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 @@ -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` ([#TBD](https://github.com/MetaMask/core/pull/TBD))

## [14.2.0]

### Changed
Expand Down
Original file line number Diff line number Diff 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 number Diff line number Diff 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 Down Expand 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 Down Expand 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 Down Expand 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 Down Expand 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 Down Expand 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 number Diff line number Diff 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 Down Expand 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 Down Expand 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 Down Expand 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 number Diff line number Diff 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