diff --git a/packages/bridge-controller/src/utils/validators.ts b/packages/bridge-controller/src/utils/validators.ts index 8f25ca4e973..adbdd5c30b5 100644 --- a/packages/bridge-controller/src/utils/validators.ts +++ b/packages/bridge-controller/src/utils/validators.ts @@ -192,9 +192,6 @@ export const StepSchema = type({ const RefuelDataSchema = StepSchema; -// Allow digit strings for amounts/validTo for flexibility across providers -const DigitStringOrNumberSchema = union([TruthyDigitStringSchema, number()]); - /** * Identifier of the intent protocol used for order creation and submission. * @@ -233,8 +230,7 @@ export const IntentOrderSchema = type({ * Can be provided as a UNIX timestamp in seconds, either as a number * or as a digit string, depending on provider requirements. */ - validTo: DigitStringOrNumberSchema, - + validTo: number(), /** * Arbitrary application-specific data attached to the order. */ @@ -283,6 +279,8 @@ export const IntentOrderSchema = type({ * Provided for convenience when building the EIP-712 domain and message. */ from: optional(HexAddressSchema), + sellTokenBalance: string(), + buyTokenBalance: string(), }); /** diff --git a/packages/bridge-status-controller/jest.config.js b/packages/bridge-status-controller/jest.config.js index 08c991c44f6..15a04af42e5 100644 --- a/packages/bridge-status-controller/jest.config.js +++ b/packages/bridge-status-controller/jest.config.js @@ -14,15 +14,13 @@ module.exports = merge(baseConfig, { // The display name when running multiple projects displayName, - coverageProvider: 'v8', - // An object that configures minimum threshold enforcement for coverage results coverageThreshold: { global: { - branches: 92.06, + branches: 94, functions: 100, - lines: 99.75, - statements: 99.75, + lines: 100, + statements: 100, }, }, }); diff --git a/packages/bridge-status-controller/src/__snapshots__/bridge-status-controller.test.ts.snap b/packages/bridge-status-controller/src/__snapshots__/bridge-status-controller.test.ts.snap index a3a47e29fa4..c912d0fe426 100644 --- a/packages/bridge-status-controller/src/__snapshots__/bridge-status-controller.test.ts.snap +++ b/packages/bridge-status-controller/src/__snapshots__/bridge-status-controller.test.ts.snap @@ -12,8 +12,8 @@ Object { "featureId": undefined, "hasApprovalTx": false, "initialDestAssetBalance": undefined, + "intentOrderId": undefined, "isStxEnabled": false, - "originalTransactionId": "bridgeTxMetaId1", "pricingData": Object { "amountSent": "1.234", "amountSentInUsd": undefined, @@ -214,8 +214,8 @@ Object { "featureId": undefined, "hasApprovalTx": false, "initialDestAssetBalance": undefined, + "intentOrderId": undefined, "isStxEnabled": false, - "originalTransactionId": "bridgeTxMetaId1", "pricingData": Object { "amountSent": "1.234", "amountSentInUsd": undefined, @@ -416,8 +416,8 @@ Object { "featureId": undefined, "hasApprovalTx": true, "initialDestAssetBalance": undefined, + "intentOrderId": undefined, "isStxEnabled": false, - "originalTransactionId": "test-tx-id", "pricingData": Object { "amountSent": "1.234", "amountSentInUsd": "1.01", @@ -652,8 +652,8 @@ Object { "featureId": undefined, "hasApprovalTx": true, "initialDestAssetBalance": undefined, + "intentOrderId": undefined, "isStxEnabled": false, - "originalTransactionId": "test-tx-id", "pricingData": Object { "amountSent": "1.234", "amountSentInUsd": "1.01", @@ -888,8 +888,8 @@ Object { "featureId": undefined, "hasApprovalTx": true, "initialDestAssetBalance": undefined, + "intentOrderId": undefined, "isStxEnabled": false, - "originalTransactionId": "test-tx-id", "pricingData": Object { "amountSent": "1.234", "amountSentInUsd": "1.01", @@ -1125,8 +1125,8 @@ Object { "featureId": undefined, "hasApprovalTx": false, "initialDestAssetBalance": undefined, + "intentOrderId": undefined, "isStxEnabled": true, - "originalTransactionId": "test-tx-id", "pricingData": Object { "amountSent": "1.234", "amountSentInUsd": "1.01", @@ -1388,8 +1388,8 @@ Object { "featureId": undefined, "hasApprovalTx": true, "initialDestAssetBalance": undefined, + "intentOrderId": undefined, "isStxEnabled": false, - "originalTransactionId": "test-tx-id", "pricingData": Object { "amountSent": "1.234", "amountSentInUsd": "1.01", @@ -1624,8 +1624,8 @@ Object { "featureId": undefined, "hasApprovalTx": true, "initialDestAssetBalance": undefined, + "intentOrderId": undefined, "isStxEnabled": false, - "originalTransactionId": "test-tx-id", "pricingData": Object { "amountSent": "1.234", "amountSentInUsd": "1.01", @@ -1860,8 +1860,8 @@ Object { "featureId": undefined, "hasApprovalTx": true, "initialDestAssetBalance": undefined, + "intentOrderId": undefined, "isStxEnabled": false, - "originalTransactionId": "test-tx-id", "pricingData": Object { "amountSent": "1.234", "amountSentInUsd": "1.01", @@ -2200,8 +2200,8 @@ Object { "featureId": undefined, "hasApprovalTx": true, "initialDestAssetBalance": undefined, + "intentOrderId": undefined, "isStxEnabled": false, - "originalTransactionId": "test-tx-id", "pricingData": Object { "amountSent": "1.234", "amountSentInUsd": "1.01", @@ -2456,8 +2456,8 @@ Object { "featureId": undefined, "hasApprovalTx": false, "initialDestAssetBalance": undefined, + "intentOrderId": undefined, "isStxEnabled": false, - "originalTransactionId": "test-tx-id", "pricingData": Object { "amountSent": "1.234", "amountSentInUsd": "1.01", @@ -2863,8 +2863,8 @@ Object { "featureId": undefined, "hasApprovalTx": true, "initialDestAssetBalance": undefined, + "intentOrderId": undefined, "isStxEnabled": true, - "originalTransactionId": "test-tx-id", "pricingData": Object { "amountSent": "1.234", "amountSentInUsd": "1.01", @@ -3216,8 +3216,8 @@ Object { "featureId": undefined, "hasApprovalTx": false, "initialDestAssetBalance": undefined, + "intentOrderId": undefined, "isStxEnabled": false, - "originalTransactionId": "test-tx-id", "pricingData": Object { "amountSent": "1.234", "amountSentInUsd": "1.01", @@ -3429,8 +3429,8 @@ Object { "featureId": undefined, "hasApprovalTx": false, "initialDestAssetBalance": undefined, + "intentOrderId": undefined, "isStxEnabled": false, - "originalTransactionId": "test-tx-id", "pricingData": Object { "amountSent": "1.234", "amountSentInUsd": "1.01", @@ -3728,8 +3728,8 @@ Object { "featureId": undefined, "hasApprovalTx": false, "initialDestAssetBalance": undefined, + "intentOrderId": undefined, "isStxEnabled": false, - "originalTransactionId": "signature", "pricingData": Object { "amountSent": "1", "amountSentInUsd": "100", @@ -4063,8 +4063,8 @@ Object { "featureId": undefined, "hasApprovalTx": false, "initialDestAssetBalance": undefined, + "intentOrderId": undefined, "isStxEnabled": false, - "originalTransactionId": "signature", "pricingData": Object { "amountSent": "1", "amountSentInUsd": "100", @@ -4439,8 +4439,8 @@ Object { "featureId": undefined, "hasApprovalTx": true, "initialDestAssetBalance": undefined, + "intentOrderId": undefined, "isStxEnabled": false, - "originalTransactionId": "bridge-signature", "pricingData": Object { "amountSent": "1", "amountSentInUsd": "1", @@ -4658,8 +4658,8 @@ Object { "featureId": undefined, "hasApprovalTx": true, "initialDestAssetBalance": undefined, + "intentOrderId": undefined, "isStxEnabled": false, - "originalTransactionId": "swap-signature", "pricingData": Object { "amountSent": "1", "amountSentInUsd": "1", diff --git a/packages/bridge-status-controller/src/bridge-status-controller.intent.test.ts b/packages/bridge-status-controller/src/bridge-status-controller.intent.test.ts index be6a8b6c2df..13c8dd1dd12 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.intent.test.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.intent.test.ts @@ -1,6 +1,9 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { + QuoteMetadata, + QuoteResponse, StatusTypes, + TxData, UnifiedSwapBridgeEventName, } from '@metamask/bridge-controller'; import type { TransactionMeta } from '@metamask/transaction-controller'; @@ -21,9 +24,9 @@ type Tx = Pick & { const seedIntentHistory = (controller: any): any => { controller.update((state: any) => { - state.txHistory['intent:1'] = { - txMetaId: 'intent:1', - originalTransactionId: 'tx1', + state.txHistory.tx1 = { + txMetaId: 'tx1', + intentOrderId: '1', quote: { srcChainId: 1, destChainId: 1, @@ -38,9 +41,14 @@ const seedIntentHistory = (controller: any): any => { }); }; -const minimalIntentQuoteResponse = (overrides?: Partial): any => { +const minimalIntentQuoteResponse = ( + overrides?: Partial & QuoteMetadata>, +): QuoteResponse & QuoteMetadata => { return { quote: { + bridgeId: 'across', + bridges: ['across'], + steps: [], requestId: 'req-1', srcChainId: 1, destChainId: 1, @@ -63,31 +71,88 @@ const minimalIntentQuoteResponse = (overrides?: Partial): any => { name: 'ETH', decimals: 18, }, - feeData: { txFee: { maxFeePerGas: '1', maxPriorityFeePerGas: '1' } }, + feeData: { + metabridge: { + amount: '1', + asset: { + symbol: 'ETH', + chainId: 1, + address: '0x0000000000000000000000000000000000000000', + assetId: 'eip155:1/slip44:60', + name: 'ETH', + decimals: 18, + }, + }, + txFee: { + maxFeePerGas: '1', + maxPriorityFeePerGas: '1', + amount: '1', + asset: { + symbol: 'ETH', + chainId: 1, + address: '0x0000000000000000000000000000000000000000', + assetId: 'eip155:1/slip44:60', + name: 'ETH', + decimals: 18, + }, + }, + }, intent: { protocol: 'cowswap', - order: { some: 'order' }, + order: { + sellToken: '0x0000000000000000000000000000000000000000', + buyToken: '0x0000000000000000000000000000000000000000', + validTo: 1715136000, + appData: '0x', + appDataHash: '0x', + feeAmount: '1', + kind: 'sell', + partiallyFillable: false, + receiver: '0x0000000000000000000000000000000000000000', + sellAmount: '1', + buyAmount: '1', + from: '0x0000000000000000000000000000000000000000', + sellTokenBalance: 'erc20', + buyTokenBalance: 'erc20', + }, settlementContract: '0x9008D19f58AAbd9eD0D60971565AA8510560ab41', }, }, - sentAmount: { amount: '1', usd: '1' }, - gasFee: { effective: { amount: '0', usd: '0' } }, - toTokenAmount: { usd: '1' }, + sentAmount: { amount: '1', usd: '1', valueInCurrency: '1' }, + gasFee: { + effective: { amount: '0', usd: '0', valueInCurrency: '0' }, + total: { amount: '0', usd: '0', valueInCurrency: '0' }, + max: { amount: '0', usd: '0', valueInCurrency: '0' }, + }, + toTokenAmount: { amount: '1', usd: '1', valueInCurrency: '1' }, + minToTokenAmount: { amount: '1', usd: '1', valueInCurrency: '1' }, + totalNetworkFee: { amount: '1', usd: '1', valueInCurrency: '1' }, + totalMaxNetworkFee: { amount: '1', usd: '1', valueInCurrency: '1' }, + adjustedReturn: { valueInCurrency: '1', usd: '1' }, + cost: { valueInCurrency: '1', usd: '1' }, + swapRate: '1', estimatedProcessingTimeInSeconds: 15, - featureId: undefined, - approval: undefined, - resetApproval: undefined, - trade: '0xdeadbeef', + trade: { + chainId: 1, + from: '0x0000000000000000000000000000000000000000', + to: '0x0000000000000000000000000000000000000001', + data: '0x', + value: '0x0', + gasLimit: 21000, + }, ...overrides, }; }; const minimalBridgeQuoteResponse = ( accountAddress: string, - overrides?: Partial, -): any => { + overrides?: Partial & QuoteMetadata>, +): QuoteResponse & QuoteMetadata => { return { quote: { + bridgeId: 'across', + bridges: ['across'], + steps: [], requestId: 'req-bridge-1', srcChainId: 1, destChainId: 10, @@ -110,11 +175,46 @@ const minimalBridgeQuoteResponse = ( name: 'ETH', decimals: 18, }, - feeData: { txFee: { maxFeePerGas: '1', maxPriorityFeePerGas: '1' } }, + feeData: { + metabridge: { + amount: '1', + asset: { + symbol: 'ETH', + chainId: 1, + address: '0x0000000000000000000000000000000000000000', + assetId: 'eip155:1/slip44:60', + name: 'ETH', + decimals: 18, + }, + }, + txFee: { + maxFeePerGas: '1', + maxPriorityFeePerGas: '1', + amount: '1', + asset: { + symbol: 'ETH', + chainId: 1, + address: '0x0000000000000000000000000000000000000000', + assetId: 'eip155:1/slip44:60', + name: 'ETH', + decimals: 18, + }, + }, + }, }, - sentAmount: { amount: '1', usd: '1' }, - gasFee: { effective: { amount: '0', usd: '0' } }, - toTokenAmount: { usd: '1' }, + sentAmount: { amount: '1', usd: '1', valueInCurrency: '1' }, + gasFee: { + effective: { amount: '0', usd: '0', valueInCurrency: '0' }, + total: { amount: '0', usd: '0', valueInCurrency: '0' }, + max: { amount: '0', usd: '0', valueInCurrency: '0' }, + }, + toTokenAmount: { amount: '1', usd: '1', valueInCurrency: '1' }, + minToTokenAmount: { amount: '1', usd: '1', valueInCurrency: '1' }, + totalNetworkFee: { amount: '1', usd: '1', valueInCurrency: '1' }, + totalMaxNetworkFee: { amount: '1', usd: '1', valueInCurrency: '1' }, + adjustedReturn: { valueInCurrency: '1', usd: '1' }, + cost: { valueInCurrency: '1', usd: '1' }, + swapRate: '1', estimatedProcessingTimeInSeconds: 15, featureId: undefined, approval: undefined, @@ -235,22 +335,6 @@ const loadControllerWithMocks = (): any => { }; }); - jest.doMock('./utils/metrics', () => ({ - getFinalizedTxProperties: jest.fn().mockReturnValue({}), - getPriceImpactFromQuote: jest.fn().mockReturnValue({}), - getRequestMetadataFromHistory: jest.fn().mockReturnValue({}), - getRequestParamFromHistory: jest.fn().mockReturnValue({ - chain_id_source: 'eip155:1', - chain_id_destination: 'eip155:10', - token_address_source: '0xsrc', - token_address_destination: '0xdest', - }), - getTradeDataFromHistory: jest.fn().mockReturnValue({}), - getEVMTxPropertiesFromTransactionMeta: jest.fn().mockReturnValue({}), - getTxStatusesFromHistory: jest.fn().mockReturnValue({}), - getPreConfirmationPropertiesFromQuote: jest.fn().mockReturnValue({}), - })); - /* eslint-disable @typescript-eslint/no-require-imports, n/global-require */ BridgeStatusController = require('./bridge-status-controller').BridgeStatusController; @@ -364,8 +448,13 @@ describe('BridgeStatusController (intent swaps)', () => { }); it('submitIntent: throws if approval confirmation fails (does not write history or start polling)', async () => { - const { controller, accountAddress, submitIntentMock, startPollingSpy } = - setup(); + const { + controller, + accountAddress, + submitIntentMock, + startPollingSpy, + messenger, + } = setup(); const orderUid = 'order-uid-1'; @@ -391,19 +480,87 @@ describe('BridgeStatusController (intent swaps)', () => { }, }); + const expectedHistory = controller.state.txHistory; await expect( controller.submitIntent({ quoteResponse, signature: '0xsig', accountAddress, + quotesReceivedContext: { + best_quote_provider: 'best-quote-provider', + can_submit: true, + gas_included: false, + gas_included_7702: false, + price_impact: 0.01, + warnings: [], + }, }), ).rejects.toThrow(/approval/iu); // Since we throw before intent order submission succeeds, we should not create the intent:* history item // (and therefore should not start polling). - const historyKey = `intent:${orderUid}`; - expect(controller.state.txHistory[historyKey]).toBeUndefined(); - + expect(controller.state.txHistory).toStrictEqual(expectedHistory); + expect(messenger.call.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "BridgeController:stopPollingForQuotes", + "Transaction submitted", + Object { + "best_quote_provider": "best-quote-provider", + "can_submit": true, + "gas_included": false, + "gas_included_7702": false, + "price_impact": 0.01, + "warnings": Array [], + }, + ], + Array [ + "AccountsController:getAccountByAddress", + "0xAccount1", + ], + Array [ + "AccountsController:getAccountByAddress", + "0xAccount1", + ], + Array [ + "NetworkController:findNetworkClientIdByChainId", + "0x1", + ], + Array [ + "GasFeeController:getState", + ], + Array [ + "TransactionController:getState", + ], + Array [ + "TransactionController:getState", + ], + Array [ + "BridgeController:trackUnifiedSwapBridgeEvent", + "Unified SwapBridge Failed", + Object { + "action_type": "swapbridge-v1", + "chain_id_destination": "eip155:1", + "chain_id_source": "eip155:1", + "custom_slippage": false, + "error_message": "Approval transaction did not confirm", + "gas_included": false, + "gas_included_7702": false, + "is_hardware_wallet": false, + "price_impact": 0, + "provider": "across_across", + "quoted_time_minutes": 0.25, + "stx_enabled": false, + "swap_type": "single_chain", + "token_symbol_destination": "ETH", + "token_symbol_source": "ETH", + "usd_amount_source": 1, + "usd_quoted_gas": 0, + "usd_quoted_return": 1, + }, + ], + ] + `); expect(startPollingSpy).not.toHaveBeenCalled(); // Optional: ensure we never called the intent API submit @@ -417,6 +574,7 @@ describe('BridgeStatusController (intent swaps)', () => { submitIntentMock, getOrderStatusMock, stopPollingSpy, + messenger, } = setup(); const orderUid = 'order-uid-2'; @@ -430,14 +588,20 @@ describe('BridgeStatusController (intent swaps)', () => { const quoteResponse = minimalIntentQuoteResponse(); - await controller.submitIntent({ + const { id: historyKey } = await controller.submitIntent({ quoteResponse, signature: '0xsig', accountAddress, + quotesReceivedContext: { + best_quote_provider: 'best-quote-provider', + can_submit: true, + gas_included: false, + gas_included_7702: false, + price_impact: 0.01, + warnings: [], + }, }); - const historyKey = `intent:${orderUid}`; - // Seed existing hashes via controller.update (state is frozen) controller.update((state: any) => { state.txHistory[historyKey].srcTxHashes = ['0xold1']; @@ -458,6 +622,76 @@ describe('BridgeStatusController (intent swaps)', () => { expect.arrayContaining(['0xold1', '0xnewhash']), ); + expect(messenger.call.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "BridgeController:stopPollingForQuotes", + "Transaction submitted", + Object { + "best_quote_provider": "best-quote-provider", + "can_submit": true, + "gas_included": false, + "gas_included_7702": false, + "price_impact": 0.01, + "warnings": Array [], + }, + ], + Array [ + "AccountsController:getAccountByAddress", + "0xAccount1", + ], + Array [ + "NetworkController:findNetworkClientIdByChainId", + "0x1", + ], + Array [ + "TransactionController:getState", + ], + Array [ + "AccountsController:getAccountByAddress", + "0xAccount1", + ], + Array [ + "TransactionController:getState", + ], + Array [ + "BridgeController:trackUnifiedSwapBridgeEvent", + "Unified SwapBridge Completed", + Object { + "action_type": "swapbridge-v1", + "actual_time_minutes": 0, + "allowance_reset_transaction": undefined, + "approval_transaction": undefined, + "chain_id_destination": "eip155:1", + "chain_id_source": "eip155:1", + "custom_slippage": true, + "destination_transaction": "PENDING", + "gas_included": false, + "gas_included_7702": false, + "is_hardware_wallet": false, + "price_impact": 0, + "provider": "across_across", + "quote_vs_execution_ratio": 0, + "quoted_time_minutes": 0.25, + "quoted_vs_used_gas_ratio": 0, + "security_warnings": Array [], + "slippage_limit": 0, + "source_transaction": "COMPLETE", + "stx_enabled": false, + "swap_type": "single_chain", + "token_address_destination": "eip155:1/slip44:60", + "token_address_source": "eip155:1/slip44:60", + "token_symbol_destination": "ETH", + "token_symbol_source": "ETH", + "usd_actual_gas": 0, + "usd_actual_return": 0, + "usd_amount_source": 1, + "usd_quoted_gas": 0, + "usd_quoted_return": 1, + }, + ], + ] + `); expect(stopPollingSpy).toHaveBeenCalledWith('poll-token-1'); }); @@ -469,6 +703,7 @@ describe('BridgeStatusController (intent swaps)', () => { getOrderStatusMock, transactions, stopPollingSpy, + messenger, } = setup(); const orderUid = 'order-uid-expired-1'; @@ -482,14 +717,12 @@ describe('BridgeStatusController (intent swaps)', () => { const quoteResponse = minimalIntentQuoteResponse(); - await controller.submitIntent({ + const { id: historyKey } = await controller.submitIntent({ quoteResponse, signature: '0xsig', accountAddress, }); - const historyKey = `intent:${orderUid}`; - // Remove TC tx so update branch logs "transaction not found" transactions.splice(0, transactions.length); @@ -508,6 +741,69 @@ describe('BridgeStatusController (intent swaps)', () => { expect.arrayContaining(['0xonlyhash']), ); + expect(messenger.call.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "BridgeController:stopPollingForQuotes", + "Transaction submitted", + undefined, + ], + Array [ + "AccountsController:getAccountByAddress", + "0xAccount1", + ], + Array [ + "NetworkController:findNetworkClientIdByChainId", + "0x1", + ], + Array [ + "TransactionController:getState", + ], + Array [ + "AccountsController:getAccountByAddress", + "0xAccount1", + ], + Array [ + "TransactionController:getState", + ], + Array [ + "BridgeController:trackUnifiedSwapBridgeEvent", + "Unified SwapBridge Failed", + Object { + "action_type": "swapbridge-v1", + "actual_time_minutes": 0.000033333333333333335, + "allowance_reset_transaction": undefined, + "approval_transaction": undefined, + "chain_id_destination": "eip155:1", + "chain_id_source": "eip155:1", + "custom_slippage": true, + "destination_transaction": "FAILED", + "gas_included": false, + "gas_included_7702": false, + "is_hardware_wallet": false, + "price_impact": 0, + "provider": "across_across", + "quote_vs_execution_ratio": 0, + "quoted_time_minutes": 0.25, + "quoted_vs_used_gas_ratio": 0, + "security_warnings": Array [], + "slippage_limit": 0, + "source_transaction": "COMPLETE", + "stx_enabled": false, + "swap_type": "single_chain", + "token_address_destination": "eip155:1/slip44:60", + "token_address_source": "eip155:1/slip44:60", + "token_symbol_destination": "ETH", + "token_symbol_source": "ETH", + "usd_actual_gas": 0, + "usd_actual_return": 0, + "usd_amount_source": 1, + "usd_quoted_gas": 0, + "usd_quoted_return": 1, + }, + ], + ] + `); expect(stopPollingSpy).toHaveBeenCalledWith('poll-token-1'); }); @@ -537,7 +833,11 @@ describe('BridgeStatusController (intent swaps)', () => { accountAddress, }); - const historyKey = `intent:${orderUid}`; + const { id: historyKey } = await controller.submitIntent({ + quoteResponse, + signature: '0xsig', + accountAddress, + }); // Prime attempts so next failure hits MAX_ATTEMPTS controller.update((state: any) => { @@ -570,11 +870,14 @@ describe('BridgeStatusController (subscriptions + bridge polling + wiping)', () controller.update((state: any) => { state.txHistory.bridgeTxMetaId1 = { txMetaId: 'bridgeTxMetaId1', - originalTransactionId: 'bridgeTxMetaId1', quote: { + bridges: ['across'], srcChainId: 1, destChainId: 10, - srcAsset: { assetId: 'eip155:1/slip44:60' }, + srcAsset: { + assetId: 'eip155:1/slip44:60', + address: '0x0000000000000000000000000000000000000000', + }, destAsset: { assetId: 'eip155:10/slip44:60' }, }, account: '0xAccount1', @@ -604,13 +907,54 @@ describe('BridgeStatusController (subscriptions + bridge polling + wiping)', () ); // ensure tracking was attempted - expect((messenger.call as jest.Mock).mock.calls).toStrictEqual( - expect.arrayContaining([ - expect.arrayContaining([ - 'BridgeController:trackUnifiedSwapBridgeEvent', - ]), - ]), - ); + expect(messenger.call.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "AccountsController:getAccountByAddress", + "0xAccount1", + ], + Array [ + "TransactionController:getState", + ], + Array [ + "BridgeController:trackUnifiedSwapBridgeEvent", + "Unified SwapBridge Failed", + Object { + "action_type": "swapbridge-v1", + "actual_time_minutes": 0, + "allowance_reset_transaction": undefined, + "approval_transaction": undefined, + "chain_id_destination": "eip155:10", + "chain_id_source": "eip155:1", + "custom_slippage": false, + "destination_transaction": "FAILED", + "error_message": "", + "gas_included": false, + "gas_included_7702": false, + "is_hardware_wallet": false, + "price_impact": 0, + "provider": "undefined_across", + "quote_vs_execution_ratio": 0, + "quoted_time_minutes": NaN, + "quoted_vs_used_gas_ratio": 0, + "security_warnings": Array [], + "slippage_limit": undefined, + "source_transaction": "COMPLETE", + "stx_enabled": false, + "swap_type": "crosschain", + "token_address_destination": "eip155:10/slip44:60", + "token_address_source": "eip155:1/slip44:60", + "token_symbol_destination": undefined, + "token_symbol_source": undefined, + "usd_actual_gas": 0, + "usd_actual_return": 0, + "usd_amount_source": 0, + "usd_quoted_gas": 0, + "usd_quoted_return": 0, + }, + ], + ] + `); }); it('transactionFailed subscription: maps approval tx id back to main history item', async () => { @@ -619,7 +963,6 @@ describe('BridgeStatusController (subscriptions + bridge polling + wiping)', () controller.update((state: any) => { state.txHistory.mainTx = { txMetaId: 'mainTx', - originalTransactionId: 'mainTx', approvalTxId: 'approvalTx', quote: { srcChainId: 1, @@ -660,7 +1003,6 @@ describe('BridgeStatusController (subscriptions + bridge polling + wiping)', () controller.update((state: any) => { state.txHistory.bridgeConfirmed1 = { txMetaId: 'bridgeConfirmed1', - originalTransactionId: 'bridgeConfirmed1', quote: { srcChainId: 1, destChainId: 10, @@ -719,7 +1061,6 @@ describe('BridgeStatusController (subscriptions + bridge polling + wiping)', () controller.update((state: any) => { state.txHistory.bridgeTx1 = { txMetaId: 'bridgeTx1', - originalTransactionId: 'bridgeTx1', quote: { srcChainId: 1, destChainId: 10, @@ -749,7 +1090,6 @@ describe('BridgeStatusController (subscriptions + bridge polling + wiping)', () controller.update((state: any) => { state.txHistory.swapTx1 = { txMetaId: 'swapTx1', - originalTransactionId: 'swapTx1', quote: { srcChainId: 1, destChainId: 1, @@ -822,7 +1162,6 @@ describe('BridgeStatusController (subscriptions + bridge polling + wiping)', () bridgeTxMeta: { id: 'bridgePoll1' }, statusRequest: { srcChainId: 1, - srcTxHash: '', // force TC lookup destChainId: 10, }, quoteResponse, @@ -869,6 +1208,71 @@ describe('BridgeStatusController (subscriptions + bridge polling + wiping)', () 'BridgeStatusController:destinationTransactionCompleted', quoteResponse.quote.destAsset.assetId, ); + expect(messenger.publish.mock.calls.map((call: any) => call[0])) + .toMatchInlineSnapshot(` + Array [ + "BridgeStatusController:stateChange", + "BridgeStatusController:stateChange", + "BridgeStatusController:stateChange", + "BridgeStatusController:destinationTransactionCompleted", + ] + `); + expect(messenger.call.mock.calls.map((call: any) => call.slice(0, 2))) + .toMatchInlineSnapshot(` + Array [ + Array [ + "TransactionController:getState", + ], + Array [ + "AccountsController:getAccountByAddress", + "0xAccount1", + ], + Array [ + "TransactionController:getState", + ], + Array [ + "BridgeController:trackUnifiedSwapBridgeEvent", + "Unified SwapBridge Completed", + ], + ] + `); + // eslint-disable-next-line @typescript-eslint/naming-convention, camelcase + const { actual_time_minutes, ...eventProperties } = + messenger.call.mock.calls.at(-1)?.at(-1) ?? {}; + expect(actual_time_minutes).toBeGreaterThan(0); + expect(eventProperties).toMatchInlineSnapshot(` + Object { + "action_type": "swapbridge-v1", + "allowance_reset_transaction": undefined, + "approval_transaction": undefined, + "chain_id_destination": "eip155:10", + "chain_id_source": "eip155:1", + "custom_slippage": true, + "destination_transaction": "COMPLETE", + "gas_included": false, + "gas_included_7702": false, + "is_hardware_wallet": false, + "price_impact": 0, + "provider": "across_across", + "quote_vs_execution_ratio": 0, + "quoted_time_minutes": 0.25, + "quoted_vs_used_gas_ratio": 0, + "security_warnings": Array [], + "slippage_limit": 0, + "source_transaction": "COMPLETE", + "stx_enabled": false, + "swap_type": "crosschain", + "token_address_destination": "eip155:10/slip44:60", + "token_address_source": "eip155:1/slip44:60", + "token_symbol_destination": "ETH", + "token_symbol_source": "ETH", + "usd_actual_gas": 0, + "usd_actual_return": 0, + "usd_amount_source": 1, + "usd_quoted_gas": 0, + "usd_quoted_return": 1, + } + `); }); it('eVM bridge polling: tracks StatusValidationFailed, increments attempts, and stops polling at MAX_ATTEMPTS', async () => { @@ -969,9 +1373,9 @@ describe('BridgeStatusController (target uncovered branches)', () => { controller.update((state: any) => { state.txHistory.tx1 = { txMetaId: 'tx1', - originalTransactionId: 'tx1', - quote: { srcChainId: 1, destChainId: 10 }, + quote: minimalIntentQuoteResponse().quote, account: '0xAccount1', + intentOrderId: '1', status: { status: StatusTypes.PENDING, srcChain: { chainId: 1, txHash: '0x' }, @@ -985,6 +1389,7 @@ describe('BridgeStatusController (target uncovered branches)', () => { failedCb({ transactionMeta: { + chainId: '0x1', id: 'tx1', type: TransactionType.bridge, status: TransactionStatus.failed, @@ -995,6 +1400,54 @@ describe('BridgeStatusController (target uncovered branches)', () => { expect(controller.state.txHistory.tx1.status.status).toBe( StatusTypes.FAILED, ); + expect(messenger.call.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "AccountsController:getAccountByAddress", + "0xAccount1", + ], + Array [ + "TransactionController:getState", + ], + Array [ + "BridgeController:trackUnifiedSwapBridgeEvent", + "Unified SwapBridge Failed", + Object { + "action_type": "swapbridge-v1", + "actual_time_minutes": 0, + "allowance_reset_transaction": undefined, + "approval_transaction": undefined, + "chain_id_destination": "eip155:1", + "chain_id_source": "eip155:1", + "custom_slippage": false, + "destination_transaction": "FAILED", + "error_message": "", + "gas_included": false, + "gas_included_7702": false, + "is_hardware_wallet": false, + "price_impact": 0, + "provider": "across_across", + "quote_vs_execution_ratio": 0, + "quoted_time_minutes": NaN, + "quoted_vs_used_gas_ratio": 0, + "security_warnings": Array [], + "slippage_limit": undefined, + "source_transaction": "COMPLETE", + "stx_enabled": false, + "swap_type": "single_chain", + "token_address_destination": "eip155:1/slip44:60", + "token_address_source": "eip155:1/slip44:60", + "token_symbol_destination": "ETH", + "token_symbol_source": "ETH", + "usd_actual_gas": 0, + "usd_actual_return": 0, + "usd_amount_source": 0, + "usd_quoted_gas": 0, + "usd_quoted_return": 0, + }, + ], + ] + `); }); it('constructor restartPolling: skips items when shouldSkipFetchDueToFetchFailures returns true', () => { @@ -1015,7 +1468,6 @@ describe('BridgeStatusController (target uncovered branches)', () => { txHistory: { init1: { txMetaId: 'init1', - originalTransactionId: 'init1', quote: { srcChainId: 1, destChainId: 10 }, account: accountAddress, status: { @@ -1168,15 +1620,17 @@ describe('BridgeStatusController (target uncovered branches)', () => { await controller._executePoll({ bridgeTxMetaId: 'failFinal1' }); - expect((messenger.call as jest.Mock).mock.calls).toStrictEqual( - expect.arrayContaining([ - expect.arrayContaining([ - 'BridgeController:trackUnifiedSwapBridgeEvent', - UnifiedSwapBridgeEventName.Failed, - expect.any(Object), - ]), - ]), - ); + expect(messenger.call.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "AccountsController:getAccountByAddress", + "0xAccount1", + ], + Array [ + "TransactionController:getState", + ], + ] + `); }); it('bridge polling: final COMPLETE with featureId set stops polling but skips tracking', async () => { @@ -1312,8 +1766,11 @@ describe('BridgeStatusController (target uncovered branches)', () => { controller.update((state: any) => { state.txHistory.feat1 = { txMetaId: 'feat1', - originalTransactionId: 'feat1', - quote: { srcChainId: 1, destChainId: 10 }, + quote: { + ...minimalBridgeQuoteResponse('0xAccount1').quote, + srcChainId: 1, + destChainId: 10, + }, account: '0xAccount1', featureId: 'perps', status: { @@ -1329,6 +1786,7 @@ describe('BridgeStatusController (target uncovered branches)', () => { failedCb({ transactionMeta: { + chainId: '0x1', id: 'feat1', type: TransactionType.bridge, status: TransactionStatus.failed, @@ -1369,9 +1827,9 @@ describe('BridgeStatusController (target uncovered branches)', () => { metadata: { txHashes: [] }, }); - await controller._executePoll({ bridgeTxMetaId: 'intent:1' }); + await controller._executePoll({ bridgeTxMetaId: 'tx1' }); - expect(controller.state.txHistory['intent:1'].status.status).toBe( + expect(controller.state.txHistory.tx1.status.status).toBe( StatusTypes.PENDING, ); }); @@ -1388,9 +1846,9 @@ describe('BridgeStatusController (target uncovered branches)', () => { metadata: { txHashes: [] }, }); - await controller._executePoll({ bridgeTxMetaId: 'intent:1' }); + await controller._executePoll({ bridgeTxMetaId: 'tx1' }); - expect(controller.state.txHistory['intent:1'].status.status).toBe( + expect(controller.state.txHistory.tx1.status.status).toBe( StatusTypes.SUBMITTED, ); }); @@ -1407,9 +1865,9 @@ describe('BridgeStatusController (target uncovered branches)', () => { metadata: { txHashes: [] }, }); - await controller._executePoll({ bridgeTxMetaId: 'intent:1' }); + await controller._executePoll({ bridgeTxMetaId: 'tx1' }); - expect(controller.state.txHistory['intent:1'].status.status).toBe( + expect(controller.state.txHistory.tx1.status.status).toBe( StatusTypes.UNKNOWN, ); }); diff --git a/packages/bridge-status-controller/src/bridge-status-controller.test.ts b/packages/bridge-status-controller/src/bridge-status-controller.test.ts index d9695b5dfe9..518e8e6d2d2 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.test.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.test.ts @@ -348,7 +348,6 @@ const MockTxHistory = { } = {}): Record => ({ [txMetaId]: { txMetaId, - originalTransactionId: txMetaId, quote: getMockQuote({ srcChainId, destChainId }), startTime: 1729964825189, estimatedProcessingTimeInSeconds: 15, @@ -372,7 +371,6 @@ const MockTxHistory = { } = {}): Record => ({ [txMetaId]: { txMetaId, - originalTransactionId: txMetaId, quote: getMockQuote({ srcChainId, destChainId }), startTime: 1729964825189, estimatedProcessingTimeInSeconds: 15, @@ -399,7 +397,7 @@ const MockTxHistory = { } = {}): Record => ({ [txMetaId]: { txMetaId, - originalTransactionId: txMetaId, + intentOrderId: undefined, batchId, quote: getMockQuote({ srcChainId, destChainId }), startTime: 1729964825189, @@ -436,7 +434,6 @@ const MockTxHistory = { } = {}): Record => ({ [txMetaId]: { txMetaId, - originalTransactionId: txMetaId, quote: getMockQuote({ srcChainId, destChainId }), startTime: 1729964825189, estimatedProcessingTimeInSeconds: 15, @@ -472,7 +469,6 @@ const MockTxHistory = { } = {}): Record => ({ [txMetaId]: { txMetaId, - originalTransactionId: txMetaId, quote: getMockQuote({ srcChainId, destChainId }), startTime: 1729964825189, estimatedProcessingTimeInSeconds: 15, @@ -507,7 +503,7 @@ const MockTxHistory = { } = {}): Record => ({ [txMetaId]: { txMetaId, - originalTransactionId: txMetaId, + intentOrderId: undefined, batchId, featureId: undefined, quote: getMockQuote({ srcChainId, destChainId }), diff --git a/packages/bridge-status-controller/src/bridge-status-controller.ts b/packages/bridge-status-controller/src/bridge-status-controller.ts index 555dde64118..04dad0f15cc 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.ts @@ -5,7 +5,6 @@ import type { RequiredEventContextFromClient, TxData, QuoteResponse, - Intent, Trade, } from '@metamask/bridge-controller'; import { @@ -84,7 +83,10 @@ import { handleNonEvmTxResponse, generateActionId, } from './utils/transaction'; -import { IntentOrder, IntentOrderStatus } from './utils/validators'; +import { + IntentOrderStatusResponse, + IntentOrderStatus, +} from './utils/validators'; const metadata: StateMetadata = { // We want to persist the bridge status state so that we can show the proper data for the Activity list @@ -448,18 +450,15 @@ export class BridgeStatusController extends StaticIntervalPollingController => { const { txHistory } = this.state; - // Intent-based items: poll intent provider instead of Bridge API - if (bridgeTxMetaId.startsWith('intent:')) { - await this.#fetchIntentOrderStatus({ bridgeTxMetaId }); - return; - } - if ( shouldSkipFetchDueToFetchFailures(txHistory[bridgeTxMetaId]?.attempts) ) { return; } + const historyItem = txHistory[bridgeTxMetaId]; try { + // Intent-based items: poll intent provider instead of Bridge API + if (historyItem?.intentOrderId) { + await this.#fetchIntentOrderStatus({ bridgeTxMetaId }); + return; + } // We try here because we receive 500 errors from Bridge API if we try to fetch immediately after submitting the source tx // Oddly mostly happens on Optimism, never on Arbitrum. By the 2nd fetch, the Bridge API responds properly. // Also srcTxHash may not be available immediately for STX, so we don't want to fetch in those cases - const historyItem = txHistory[bridgeTxMetaId]; const srcTxHash = this.#getSrcTxHash(bridgeTxMetaId); if (!srcTxHash) { return; @@ -695,7 +694,8 @@ export class BridgeStatusController extends StaticIntervalPollingController tx.id === originalTxId, + (tx: TransactionMeta) => tx.id === bridgeTxMetaId, ); if (existingTxMeta) { const updatedTxMeta: TransactionMeta = { @@ -851,19 +833,15 @@ export class BridgeStatusController extends StaticIntervalPollingController; - } - ).txReceipt, + ...(existingTxMeta.txReceipt ?? {}), transactionHash: txHash, - status: (isComplete ? '0x1' : '0x0') as unknown as string, + status: isComplete ? '0x1' : '0x0', }, - } as Partial) + } : {}), - } as TransactionMeta; + }; this.#updateTransactionFn( updatedTxMeta, @@ -872,7 +850,7 @@ export class BridgeStatusController extends StaticIntervalPollingController & QuoteMetadata; + quoteResponse: QuoteResponse & QuoteMetadata; signature: string; accountAddress: string; + quotesReceivedContext?: RequiredEventContextFromClient[UnifiedSwapBridgeEventName.QuotesReceived]; }): Promise> => { - const { quoteResponse, signature, accountAddress } = params; + const { quoteResponse, signature, accountAddress, quotesReceivedContext } = + params; this.messenger.call( 'BridgeController:stopPollingForQuotes', AbortReason.TransactionSubmitted, + quotesReceivedContext, ); // Build pre-confirmation properties for error tracking parity with submitTx @@ -1614,9 +1596,11 @@ export class BridgeStatusController extends StaticIntervalPollingController; export type RefuelStatusResponse = object & StatusResponse; export type BridgeHistoryItem = { - txMetaId: string; // Need this to handle STX that might not have a txHash immediately - originalTransactionId?: string; // Keep original transaction ID for intent transactions + /** + * This is the TransactionController tx id for the trade. This is used to + * find the transaction in state if the hash is not available. + * For intent orders, this is the meta id of the synthetic transaction created to display the order's status + */ + txMetaId: string; batchId?: string; quote: Quote; status: StatusResponse; @@ -113,6 +117,10 @@ export type BridgeHistoryItem = { * for backward compatibility with consumers expecting a single hash. */ srcTxHashes?: string[]; + /** + * This is returned by the intent provider after the intent order has been submitted. + */ + intentOrderId?: string; startTime?: number; // timestamp in ms estimatedProcessingTimeInSeconds: number; slippagePercentage: number; @@ -203,6 +211,7 @@ export type StartPollingForBridgeTxStatusArgs = { initialDestAssetBalance?: BridgeHistoryItem['initialDestAssetBalance']; targetContractAddress?: BridgeHistoryItem['targetContractAddress']; approvalTxId?: BridgeHistoryItem['approvalTxId']; + intentOrderId?: BridgeHistoryItem['intentOrderId']; isStxEnabled?: BridgeHistoryItem['isStxEnabled']; accountAddress: string; }; diff --git a/packages/bridge-status-controller/src/utils/intent-api.test.ts b/packages/bridge-status-controller/src/utils/intent-api.test.ts index 2f15fdfa214..316eaf06eac 100644 --- a/packages/bridge-status-controller/src/utils/intent-api.test.ts +++ b/packages/bridge-status-controller/src/utils/intent-api.test.ts @@ -1,6 +1,5 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { IntentApiImpl } from './intent-api'; -import type { IntentSubmissionParams } from './intent-api'; import { IntentOrderStatus } from './validators'; import type { FetchFunction } from '../types'; @@ -8,14 +7,14 @@ describe('IntentApiImpl', () => { const baseUrl = 'https://example.com/api'; const clientId = 'client-id'; - const makeParams = (): IntentSubmissionParams => ({ - srcChainId: '1', + const makeParams = { + srcChainId: 1, quoteId: 'quote-123', signature: '0xsig', - order: { some: 'payload' }, + order: { some: 'payload' } as never, userAddress: '0xabc', aggregatorId: 'agg-1', - }); + }; const makeFetchMock = (): any => jest.fn, Parameters>(); @@ -30,7 +29,7 @@ describe('IntentApiImpl', () => { const fetchFn = makeFetchMock().mockResolvedValue(validIntentOrderResponse); const api = new IntentApiImpl(baseUrl, fetchFn); - const params = makeParams(); + const params = makeParams; const result = await api.submitIntent(params, clientId); expect(result).toStrictEqual(validIntentOrderResponse); @@ -49,7 +48,7 @@ describe('IntentApiImpl', () => { const fetchFn = makeFetchMock().mockRejectedValue(new Error('boom')); const api = new IntentApiImpl(baseUrl, fetchFn); - await expect(api.submitIntent(makeParams(), clientId)).rejects.toThrow( + await expect(api.submitIntent(makeParams, clientId)).rejects.toThrow( 'Failed to submit intent: boom', ); }); @@ -58,7 +57,7 @@ describe('IntentApiImpl', () => { const fetchFn = makeFetchMock().mockRejectedValue('boom'); const api = new IntentApiImpl(baseUrl, fetchFn); - await expect(api.submitIntent(makeParams(), clientId)).rejects.toThrow( + await expect(api.submitIntent(makeParams, clientId)).rejects.toThrow( 'Failed to submit intent', ); }); @@ -116,11 +115,11 @@ describe('IntentApiImpl', () => { it('submitIntent throws when response fails validation', async () => { const fetchFn = makeFetchMock().mockResolvedValue({ foo: 'bar', // invalid IntentOrder shape - } as any); + }); const api = new IntentApiImpl(baseUrl, fetchFn); - await expect(api.submitIntent(makeParams(), clientId)).rejects.toThrow( + await expect(api.submitIntent(makeParams, clientId)).rejects.toThrow( 'Failed to submit intent: Invalid submitOrder response', ); }); @@ -128,7 +127,7 @@ describe('IntentApiImpl', () => { it('getOrderStatus throws when response fails validation', async () => { const fetchFn = makeFetchMock().mockResolvedValue({ foo: 'bar', // invalid IntentOrder shape - } as any); + }); const api = new IntentApiImpl(baseUrl, fetchFn); diff --git a/packages/bridge-status-controller/src/utils/intent-api.ts b/packages/bridge-status-controller/src/utils/intent-api.ts index 928f946c15a..de35f790e5a 100644 --- a/packages/bridge-status-controller/src/utils/intent-api.ts +++ b/packages/bridge-status-controller/src/utils/intent-api.ts @@ -1,17 +1,21 @@ +import type { + QuoteResponse, + IntentOrderLike, +} from '@metamask/bridge-controller'; import { TransactionStatus } from '@metamask/transaction-controller'; import { - IntentOrder, + IntentOrderStatusResponse, IntentOrderStatus, validateIntentOrderResponse, } from './validators'; import type { FetchFunction } from '../types'; -export type IntentSubmissionParams = { - srcChainId: string; - quoteId: string; +type IntentSubmissionParams = { + srcChainId: QuoteResponse['quote']['srcChainId']; + quoteId: QuoteResponse['quote']['requestId']; signature: string; - order: unknown; + order: IntentOrderLike; userAddress: string; aggregatorId: string; }; @@ -26,13 +30,13 @@ export type IntentApi = { submitIntent( params: IntentSubmissionParams, clientId: string, - ): Promise; + ): Promise; getOrderStatus( orderId: string, aggregatorId: string, srcChainId: string, clientId: string, - ): Promise; + ): Promise; }; export class IntentApiImpl implements IntentApi { @@ -48,7 +52,7 @@ export class IntentApiImpl implements IntentApi { async submitIntent( params: IntentSubmissionParams, clientId: string, - ): Promise { + ): Promise { const endpoint = `${this.#baseUrl}/submitOrder`; try { const response = await this.#fetchFn(endpoint, { @@ -59,10 +63,10 @@ export class IntentApiImpl implements IntentApi { }, body: JSON.stringify(params), }); - if (!validateIntentOrderResponse(response)) { - throw new Error('Invalid submitOrder response'); - } - return response; + return validateIntentOrderResponse( + response, + 'Invalid submitOrder response', + ); } catch (error: unknown) { if (error instanceof Error) { throw new Error(`Failed to submit intent: ${error.message}`); @@ -76,17 +80,17 @@ export class IntentApiImpl implements IntentApi { aggregatorId: string, srcChainId: string, clientId: string, - ): Promise { + ): Promise { const endpoint = `${this.#baseUrl}/getOrderStatus?orderId=${orderId}&aggregatorId=${encodeURIComponent(aggregatorId)}&srcChainId=${srcChainId}`; try { const response = await this.#fetchFn(endpoint, { method: 'GET', headers: getClientIdHeader(clientId), }); - if (!validateIntentOrderResponse(response)) { - throw new Error('Invalid getOrderStatus response'); - } - return response; + return validateIntentOrderResponse( + response, + 'Invalid getOrderStatus response', + ); } catch (error: unknown) { if (error instanceof Error) { throw new Error(`Failed to get order status: ${error.message}`); diff --git a/packages/bridge-status-controller/src/utils/validators.ts b/packages/bridge-status-controller/src/utils/validators.ts index e0054f3a0fc..d2fe9d3f65f 100644 --- a/packages/bridge-status-controller/src/utils/validators.ts +++ b/packages/bridge-status-controller/src/utils/validators.ts @@ -10,8 +10,8 @@ import { type, assert, array, - is, } from '@metamask/superstruct'; +import { StrictHexStruct } from '@metamask/utils'; const ChainIdSchema = number(); @@ -70,26 +70,23 @@ export enum IntentOrderStatus { EXPIRED = 'expired', } -export type IntentOrder = { - id: string; - status: IntentOrderStatus; - txHash?: string; - metadata: { - txHashes?: string[] | string; - }; -}; - -export const IntentOrderResponseSchema = type({ +const IntentOrderStatusResponseSchema = type({ id: string(), status: enums(Object.values(IntentOrderStatus)), - txHash: optional(string()), + txHash: optional(StrictHexStruct), metadata: type({ - txHashes: optional(union([array(string()), string()])), + txHashes: optional(union([array(StrictHexStruct), StrictHexStruct])), }), }); +export type IntentOrderStatusResponse = Infer< + typeof IntentOrderStatusResponseSchema +>; + export const validateIntentOrderResponse = ( data: unknown, -): data is Infer => { - return is(data, IntentOrderResponseSchema); + message: string, +): IntentOrderStatusResponse => { + assert(data, IntentOrderStatusResponseSchema, message); + return data; };