diff --git a/packages/client-utils/CHANGELOG.md b/packages/client-utils/CHANGELOG.md index 0d9765451c..e473a16d10 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` ([#9777](https://github.com/MetaMask/core/pull/9777)) + ## [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 4adf272924..75ec0765c3 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 13709953ef..8794d31d53 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 0aed7e8956..08c8c5c00c 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 2483f3bc0c..9ecedd1ecd 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-method-action-types.ts b/packages/ramps-controller/src/RampsController-method-action-types.ts index 714c11a3fc..3898e4fea1 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 cde0a1ca64..54dca251d3 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).toStrictEqual({ 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).toStrictEqual({ + 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 d454386b80..3f88e46e0c 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,