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/client-utils/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 6 additions & 6 deletions packages/client-utils/src/mappers/ramps-order-mapper.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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'])(
Expand Down
10 changes: 7 additions & 3 deletions packages/client-utils/src/mappers/ramps-order-mapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -184,8 +190,6 @@ export function mapRampsOrder(order: RampsOrderLike): ActivityItem | null {
},
];

const chainId = resolveRampsOrderChainId(order);

return {
type: isBuy ? 'rampBuy' : 'rampSell',
chainId,
Expand Down
46 changes: 19 additions & 27 deletions packages/client-utils/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 & {
Expand Down
4 changes: 4 additions & 0 deletions packages/ramps-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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`;
Expand Down
20 changes: 20 additions & 0 deletions packages/ramps-controller/src/RampsController.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: '' });
});
});

Expand All @@ -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: '',
});
});
});

Expand All @@ -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);
Expand Down
9 changes: 6 additions & 3 deletions packages/ramps-controller/src/RampsController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2618,20 +2618,23 @@ 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;

const orderCode = getInternalOrderCode(orderId);
if (!orderCode?.trim()) {
return;
}
if (!chainId.trim()) {
return;
}
const stubOrder: RampsOrder = {
providerOrderId: orderCode,
provider: {
Expand All @@ -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,
Expand Down