From 435faf5ade9419d3fc243ebec5777a53d20e0ddc Mon Sep 17 00:00:00 2001 From: George Weiler Date: Tue, 4 Aug 2026 03:30:23 -0600 Subject: [PATCH 1/5] fix(client-utils): require chainId on ramp activity items Make rampBuy/rampSell ActivityItem.chainId required like every other kind. mapRampsOrder returns null when no CAIP chain can be resolved, matching mobile's existing hide-if-unresolved behavior. Also require a non-empty chainId on RampsController.addPrecreatedOrder so checkout stubs always seed network before pending flips. Co-authored-by: Cursor --- packages/client-utils/CHANGELOG.md | 4 ++ .../src/mappers/ramps-order-mapper.test.ts | 12 ++--- .../src/mappers/ramps-order-mapper.ts | 10 ++-- packages/client-utils/src/types.ts | 46 ++++++++----------- packages/ramps-controller/CHANGELOG.md | 4 ++ .../src/RampsController.test.ts | 20 ++++++++ .../ramps-controller/src/RampsController.ts | 9 ++-- 7 files changed, 66 insertions(+), 39 deletions(-) diff --git a/packages/client-utils/CHANGELOG.md b/packages/client-utils/CHANGELOG.md index 0d9765451ce..b3405670237 100644 --- a/packages/client-utils/CHANGELOG.md +++ b/packages/client-utils/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- **BREAKING:** Make `chainId` required on `rampBuy` / `rampSell` `ActivityItem` variants, matching every other activity kind. `mapRampsOrder` now returns `null` when no CAIP chain id can be resolved from the order (empty or unparseable `network`, missing `cryptoCurrency.chainId` / `assetId`), instead of emitting an item with an undefined `chainId`. + ## [1.6.0] ### Added diff --git a/packages/client-utils/src/mappers/ramps-order-mapper.test.ts b/packages/client-utils/src/mappers/ramps-order-mapper.test.ts index 4adf2729244..75ec0765c3f 100644 --- a/packages/client-utils/src/mappers/ramps-order-mapper.test.ts +++ b/packages/client-utils/src/mappers/ramps-order-mapper.test.ts @@ -102,14 +102,14 @@ describe('mapRampsOrder', () => { expect(item?.status).toBe('pending'); }); - it('maps a precreated stub order with an empty chain id to an undefined chainId, not eip155:0', () => { + it('hides a precreated stub order with an empty chain id instead of mapping eip155:0', () => { const item = mapRampsOrder({ ...baseOrder, network: { chainId: '' }, cryptoCurrency: undefined, }); - expect(item?.chainId).toBeUndefined(); + expect(item).toBeNull(); }); it('falls through an unparseable network name to cryptoCurrency.chainId', () => { @@ -139,24 +139,24 @@ describe('mapRampsOrder', () => { expect(item?.chainId).toBe('eip155:1'); }); - it('returns an undefined chainId when cryptoCurrency.assetId has no valid chain segment', () => { + it('hides an order when cryptoCurrency.assetId has no valid chain segment', () => { const item = mapRampsOrder({ ...baseOrder, network: 'ethereum', cryptoCurrency: { assetId: 'not-an-asset-id', symbol: 'ETH' }, }); - expect(item?.chainId).toBeUndefined(); + expect(item).toBeNull(); }); - it('returns an undefined chainId when network is an unparseable name and crypto currency has no chain', () => { + it('hides an order when network is an unparseable name and crypto currency has no chain', () => { const item = mapRampsOrder({ ...baseOrder, network: 'ethereum', cryptoCurrency: undefined, }); - expect(item?.chainId).toBeUndefined(); + expect(item).toBeNull(); }); it.each(['0x', '0x0000'])( diff --git a/packages/client-utils/src/mappers/ramps-order-mapper.ts b/packages/client-utils/src/mappers/ramps-order-mapper.ts index 13709953eff..8794d31d53d 100644 --- a/packages/client-utils/src/mappers/ramps-order-mapper.ts +++ b/packages/client-utils/src/mappers/ramps-order-mapper.ts @@ -143,13 +143,19 @@ function isPlausibleRampTxHash(txHash: string): boolean { * * @param order - The ramps order to map. * @returns The normalized activity item, or `null` if the order's status - * should not be surfaced in the activity list. + * should not be surfaced in the activity list, or if no CAIP chain id can be + * resolved from the order. */ export function mapRampsOrder(order: RampsOrderLike): ActivityItem | null { if (HIDDEN_STATUSES.has(order.status) || order.excludeFromPurchases) { return null; } + const chainId = resolveRampsOrderChainId(order); + if (!chainId) { + return null; + } + // The V2 API returns `orderType` uppercased (e.g. `'BUY'`); normalize since // some call sites (e.g. locally-created stub orders) use lowercase. Transak // deposits (`'DEPOSIT'`) are a buy variant, not a sell. @@ -184,8 +190,6 @@ export function mapRampsOrder(order: RampsOrderLike): ActivityItem | null { }, ]; - const chainId = resolveRampsOrderChainId(order); - return { type: isBuy ? 'rampBuy' : 'rampSell', chainId, diff --git a/packages/client-utils/src/types.ts b/packages/client-utils/src/types.ts index 0aed7e89565..08c8c5c00c3 100644 --- a/packages/client-utils/src/types.ts +++ b/packages/client-utils/src/types.ts @@ -176,34 +176,26 @@ export type ActivityItem = transactionProtocol?: string; } > - | (Omit< - ActivityData< - 'rampBuy' | 'rampSell', - { - from?: string; - fiat?: FiatAmount; - token?: TokenAmount; - fees?: Fee[]; - provider?: { - id?: string; - name?: string; - orderLink?: string; - }; - statusDescription?: string; - paymentDetails?: RampOrderPaymentDetail[]; - // Stable identifier for orders that may not have a hash yet (e.g. a - // ramp order pending fiat settlement, where `hash` is empty until it - // settles on-chain). Lives in `data` as a ramp-specific property. + | ActivityData< + 'rampBuy' | 'rampSell', + { + from?: string; + fiat?: FiatAmount; + token?: TokenAmount; + fees?: Fee[]; + provider?: { id?: string; - } - >, - 'chainId' - > & { - // Precreated stub orders (see `RampsController.addPrecreatedOrder`) may - // not have an assigned network yet, so unlike every other activity - // kind, a ramp order's chain id isn't guaranteed. - chainId?: CaipChainId; - }); + name?: string; + orderLink?: string; + }; + statusDescription?: string; + paymentDetails?: RampOrderPaymentDetail[]; + // Stable identifier for orders that may not have a hash yet (e.g. a + // ramp order pending fiat settlement, where `hash` is empty until it + // settles on-chain). Lives in `data` as a ramp-specific property. + id?: string; + } + >; // Note: Update core-backend export type ValueTransfer = _ValueTransfer & { diff --git a/packages/ramps-controller/CHANGELOG.md b/packages/ramps-controller/CHANGELOG.md index 2483f3bc0c6..9ecedd1ecdf 100644 --- a/packages/ramps-controller/CHANGELOG.md +++ b/packages/ramps-controller/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- **BREAKING:** Require a non-empty `chainId` on `addPrecreatedOrder`. Callers must seed the selected token's chain so precreated stubs (and pending flips before API enrichment) always carry a network. Empty or whitespace `chainId` is a no-op, matching empty `orderId` handling. ([#9777](https://github.com/MetaMask/core/pull/9777)) + ## [19.0.0] ### Added diff --git a/packages/ramps-controller/src/RampsController.test.ts b/packages/ramps-controller/src/RampsController.test.ts index cde0a1ca64b..22ec358f4f9 100644 --- a/packages/ramps-controller/src/RampsController.test.ts +++ b/packages/ramps-controller/src/RampsController.test.ts @@ -8870,6 +8870,7 @@ describe('RampsController', () => { expect(stub?.provider?.id).toBe('paypal'); expect(stub?.walletAddress).toBe('0xabc'); expect(stub?.status).toBe(RampsOrderStatus.Precreated); + expect(stub?.network).toEqual({ chainId: '1', name: '' }); }); }); @@ -8879,11 +8880,16 @@ describe('RampsController', () => { orderId: 'plain-order-id', providerCode: 'transak', walletAddress: '0xdef', + chainId: 'eip155:1', }); expect(controller.state.orders[0]?.providerOrderId).toBe( 'plain-order-id', ); + expect(controller.state.orders[0]?.network).toEqual({ + chainId: 'eip155:1', + name: '', + }); }); }); @@ -8893,6 +8899,20 @@ describe('RampsController', () => { orderId: '/providers/paypal/orders/', providerCode: 'paypal', walletAddress: '0xabc', + chainId: '1', + }); + + expect(controller.state.orders).toHaveLength(0); + }); + }); + + it('skips addOrder when chainId is empty or whitespace', async () => { + await withController(({ controller, rootMessenger }) => { + rootMessenger.call('RampsController:addPrecreatedOrder', { + orderId: '/providers/paypal/orders/abc123', + providerCode: 'paypal', + walletAddress: '0xabc', + chainId: ' ', }); expect(controller.state.orders).toHaveLength(0); diff --git a/packages/ramps-controller/src/RampsController.ts b/packages/ramps-controller/src/RampsController.ts index d454386b809..3f88e46e0cc 100644 --- a/packages/ramps-controller/src/RampsController.ts +++ b/packages/ramps-controller/src/RampsController.ts @@ -2618,13 +2618,13 @@ export class RampsController extends BaseController< * @param params.orderId - Full order ID (e.g. "/providers/paypal/orders/abc123") or order code. * @param params.providerCode - Canonical provider code (e.g. "paypal", "transak"). * @param params.walletAddress - Wallet address for the order. - * @param params.chainId - Optional chain ID for the order. + * @param params.chainId - Chain ID for the order (decimal, hex, or CAIP-2). Must be non-empty. */ addPrecreatedOrder(params: { orderId: string; providerCode: string; walletAddress: string; - chainId?: string; + chainId: string; }): void { const { orderId, providerCode, walletAddress, chainId } = params; @@ -2632,6 +2632,9 @@ export class RampsController extends BaseController< if (!orderCode?.trim()) { return; } + if (!chainId.trim()) { + return; + } const stubOrder: RampsOrder = { providerOrderId: orderCode, provider: { @@ -2654,7 +2657,7 @@ export class RampsController extends BaseController< providerOrderLink: '', totalFeesFiat: 0, txHash: '', - network: chainId ? { chainId, name: '' } : { chainId: '', name: '' }, + network: { chainId, name: '' }, canBeUpdated: true, idHasExpired: false, excludeFromPurchases: false, From 4f732abf1c1e5b241e2264714a8a80db3b51599a Mon Sep 17 00:00:00 2001 From: George Weiler Date: Tue, 4 Aug 2026 03:35:39 -0600 Subject: [PATCH 2/5] chore(client-utils): bump version to 1.7.0 Co-authored-by: Cursor --- packages/client-utils/CHANGELOG.md | 4 +++- packages/client-utils/package.json | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/client-utils/CHANGELOG.md b/packages/client-utils/CHANGELOG.md index b3405670237..df8b70f0219 100644 --- a/packages/client-utils/CHANGELOG.md +++ b/packages/client-utils/CHANGELOG.md @@ -7,9 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.7.0] + ### Changed -- **BREAKING:** Make `chainId` required on `rampBuy` / `rampSell` `ActivityItem` variants, matching every other activity kind. `mapRampsOrder` now returns `null` when no CAIP chain id can be resolved from the order (empty or unparseable `network`, missing `cryptoCurrency.chainId` / `assetId`), instead of emitting an item with an undefined `chainId`. +- **BREAKING:** Make `chainId` required on `rampBuy` / `rampSell` `ActivityItem` variants, matching every other activity kind. `mapRampsOrder` now returns `null` when no CAIP chain id can be resolved from the order (empty or unparseable `network`, missing `cryptoCurrency.chainId` / `assetId`), instead of emitting an item with an undefined `chainId` ([#9777](https://github.com/MetaMask/core/pull/9777)) ## [1.6.0] diff --git a/packages/client-utils/package.json b/packages/client-utils/package.json index 2a34d05502f..5cb9b524421 100644 --- a/packages/client-utils/package.json +++ b/packages/client-utils/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/client-utils", - "version": "1.6.0", + "version": "1.7.0", "description": "Shared functions and utilities used across MetaMask clients (extension and mobile)", "keywords": [ "Ethereum", From 444bef589f06faeb088d46e833784e1ea75a7559 Mon Sep 17 00:00:00 2001 From: George Weiler Date: Tue, 4 Aug 2026 03:38:21 -0600 Subject: [PATCH 3/5] chore(client-utils): keep version at 1.6.0 until release PR Preview/release pipelines own the version bump; leave the breaking change under Unreleased. Co-authored-by: Cursor --- packages/client-utils/CHANGELOG.md | 2 -- packages/client-utils/package.json | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/client-utils/CHANGELOG.md b/packages/client-utils/CHANGELOG.md index df8b70f0219..e473a16d105 100644 --- a/packages/client-utils/CHANGELOG.md +++ b/packages/client-utils/CHANGELOG.md @@ -7,8 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -## [1.7.0] - ### Changed - **BREAKING:** Make `chainId` required on `rampBuy` / `rampSell` `ActivityItem` variants, matching every other activity kind. `mapRampsOrder` now returns `null` when no CAIP chain id can be resolved from the order (empty or unparseable `network`, missing `cryptoCurrency.chainId` / `assetId`), instead of emitting an item with an undefined `chainId` ([#9777](https://github.com/MetaMask/core/pull/9777)) diff --git a/packages/client-utils/package.json b/packages/client-utils/package.json index 5cb9b524421..2a34d05502f 100644 --- a/packages/client-utils/package.json +++ b/packages/client-utils/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/client-utils", - "version": "1.7.0", + "version": "1.6.0", "description": "Shared functions and utilities used across MetaMask clients (extension and mobile)", "keywords": [ "Ethereum", From 265e4e7e918052705e94895ab6690d5bd1b973b1 Mon Sep 17 00:00:00 2001 From: George Weiler Date: Tue, 4 Aug 2026 03:49:23 -0600 Subject: [PATCH 4/5] fix(ramps-controller): resolve CI lint failures for #9777 Use toStrictEqual in addPrecreatedOrder tests, merge Unreleased Changed changelog sections, and regenerate messenger action types after making chainId required. Co-authored-by: Cursor --- packages/ramps-controller/CHANGELOG.md | 1 + .../src/RampsController-method-action-types.ts | 2 +- packages/ramps-controller/src/RampsController.test.ts | 4 ++-- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/ramps-controller/CHANGELOG.md b/packages/ramps-controller/CHANGELOG.md index 9ecedd1ecdf..b3fb2ba53fb 100644 --- a/packages/ramps-controller/CHANGELOG.md +++ b/packages/ramps-controller/CHANGELOG.md @@ -24,6 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- **BREAKING:** Require a non-empty `chainId` on `addPrecreatedOrder`. Callers must seed the selected token's chain so precreated stubs (and pending flips before API enrichment) always carry a network. Empty or whitespace `chainId` is a no-op, matching empty `orderId` handling. ([#9777](https://github.com/MetaMask/core/pull/9777)) - **BREAKING:** `RampsController` now calls `RampsService:getDefaultRedirectCallbackUrl` on the widened quote path, so hosts must delegate that action to the controller's messenger. It is included in the exported `RAMPS_CONTROLLER_REQUIRED_SERVICE_ACTIONS` list; hosts that spell out their delegated action list instead of spreading that constant have to add it, or the entire `RampsController:getQuotes` call rejects with a messenger "handler has not been delegated" error (including MM Pay's fiat quote path, which omits `redirectUrl` and relies on widening). ([#9752](https://github.com/MetaMask/core/pull/9752)) - The action is only called when the `moneyHeadlessAllProviders` widening is in effect and the caller omitted `redirectUrl`. An explicit `redirectUrl` and the native-only path never reach the service. diff --git a/packages/ramps-controller/src/RampsController-method-action-types.ts b/packages/ramps-controller/src/RampsController-method-action-types.ts index 714c11a3fc5..3898e4fea14 100644 --- a/packages/ramps-controller/src/RampsController-method-action-types.ts +++ b/packages/ramps-controller/src/RampsController-method-action-types.ts @@ -321,7 +321,7 @@ export type RampsControllerGetBuyWidgetDataAction = { * @param params.orderId - Full order ID (e.g. "/providers/paypal/orders/abc123") or order code. * @param params.providerCode - Canonical provider code (e.g. "paypal", "transak"). * @param params.walletAddress - Wallet address for the order. - * @param params.chainId - Optional chain ID for the order. + * @param params.chainId - Chain ID for the order (decimal, hex, or CAIP-2). Must be non-empty. */ export type RampsControllerAddPrecreatedOrderAction = { type: `RampsController:addPrecreatedOrder`; diff --git a/packages/ramps-controller/src/RampsController.test.ts b/packages/ramps-controller/src/RampsController.test.ts index 22ec358f4f9..54dca251d3c 100644 --- a/packages/ramps-controller/src/RampsController.test.ts +++ b/packages/ramps-controller/src/RampsController.test.ts @@ -8870,7 +8870,7 @@ describe('RampsController', () => { expect(stub?.provider?.id).toBe('paypal'); expect(stub?.walletAddress).toBe('0xabc'); expect(stub?.status).toBe(RampsOrderStatus.Precreated); - expect(stub?.network).toEqual({ chainId: '1', name: '' }); + expect(stub?.network).toStrictEqual({ chainId: '1', name: '' }); }); }); @@ -8886,7 +8886,7 @@ describe('RampsController', () => { expect(controller.state.orders[0]?.providerOrderId).toBe( 'plain-order-id', ); - expect(controller.state.orders[0]?.network).toEqual({ + expect(controller.state.orders[0]?.network).toStrictEqual({ chainId: 'eip155:1', name: '', }); From a795eb7f5e61f70e3106bc718b5ce90583805cea Mon Sep 17 00:00:00 2001 From: George Weiler Date: Tue, 4 Aug 2026 07:10:10 -0600 Subject: [PATCH 5/5] fix(ramps-controller): keep #9777 changelog entry under Unreleased Rebase onto main after Release/1172.0.0 moved prior Unreleased entries into 19.0.0. Ensure the addPrecreatedOrder chainId BREAKING note stays in Unreleased so merge-queue changelog validation passes. Co-authored-by: Cursor --- packages/ramps-controller/CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/ramps-controller/CHANGELOG.md b/packages/ramps-controller/CHANGELOG.md index b3fb2ba53fb..9ecedd1ecdf 100644 --- a/packages/ramps-controller/CHANGELOG.md +++ b/packages/ramps-controller/CHANGELOG.md @@ -24,7 +24,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- **BREAKING:** Require a non-empty `chainId` on `addPrecreatedOrder`. Callers must seed the selected token's chain so precreated stubs (and pending flips before API enrichment) always carry a network. Empty or whitespace `chainId` is a no-op, matching empty `orderId` handling. ([#9777](https://github.com/MetaMask/core/pull/9777)) - **BREAKING:** `RampsController` now calls `RampsService:getDefaultRedirectCallbackUrl` on the widened quote path, so hosts must delegate that action to the controller's messenger. It is included in the exported `RAMPS_CONTROLLER_REQUIRED_SERVICE_ACTIONS` list; hosts that spell out their delegated action list instead of spreading that constant have to add it, or the entire `RampsController:getQuotes` call rejects with a messenger "handler has not been delegated" error (including MM Pay's fiat quote path, which omits `redirectUrl` and relies on widening). ([#9752](https://github.com/MetaMask/core/pull/9752)) - The action is only called when the `moneyHeadlessAllProviders` widening is in effect and the caller omitted `redirectUrl`. An explicit `redirectUrl` and the native-only path never reach the service.