From e7f9a43e9c69c7675c6b51fd9ea24b4dbde806ec Mon Sep 17 00:00:00 2001 From: Pedro Figueiredo Date: Mon, 14 Sep 2026 13:08:13 +0100 Subject: [PATCH 1/2] feat(transaction-pay-controller): persist chain-agnostic pay intents --- packages/transaction-controller/CHANGELOG.md | 4 + packages/transaction-controller/src/index.ts | 1 + packages/transaction-controller/src/types.ts | 37 ++++- .../ARCHITECTURE.md | 4 +- .../transaction-pay-controller/CHANGELOG.md | 4 + ...actionPayController-method-action-types.ts | 17 +++ .../src/TransactionPayController.test.ts | 144 +++++++++++++++++- .../src/TransactionPayController.ts | 39 +++++ .../transaction-pay-controller/src/index.ts | 9 +- .../transaction-pay-controller/src/types.ts | 18 ++- .../src/utils/quotes.test.ts | 52 +++++-- .../src/utils/quotes.ts | 3 + 12 files changed, 313 insertions(+), 19 deletions(-) diff --git a/packages/transaction-controller/CHANGELOG.md b/packages/transaction-controller/CHANGELOG.md index 56fa6fb9d65..6d91799f546 100644 --- a/packages/transaction-controller/CHANGELOG.md +++ b/packages/transaction-controller/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add versioned, chain-agnostic Pay intent metadata for persisting CAIP source identity and execution correlation without changing legacy EVM Pay fields. + ### Changed - Bump `uuid` from `^8.3.2` to `^9.0.1` ([#10117](https://github.com/MetaMask/core/pull/10117)) diff --git a/packages/transaction-controller/src/index.ts b/packages/transaction-controller/src/index.ts index b7b17038a97..2bbf040c2e4 100644 --- a/packages/transaction-controller/src/index.ts +++ b/packages/transaction-controller/src/index.ts @@ -88,6 +88,7 @@ export type { IsAtomicBatchSupportedResultEntry, LegacyGasFeeEstimates, Log, + MetamaskPayIntent, MetamaskPayMetadata, NestedTransactionMetadata, NestedTransactionUpdate, diff --git a/packages/transaction-controller/src/types.ts b/packages/transaction-controller/src/types.ts index 83b19561560..8d54cdcb8f5 100644 --- a/packages/transaction-controller/src/types.ts +++ b/packages/transaction-controller/src/types.ts @@ -4,7 +4,13 @@ import type { AccessList } from '@ethereumjs/tx'; import type { AccountsController } from '@metamask/accounts-controller'; import type { GasFeeState } from '@metamask/gas-fee-controller'; import type { NetworkClientId } from '@metamask/network-controller'; -import type { Hex, Json } from '@metamask/utils'; +import type { + CaipAccountId, + CaipAssetType, + CaipChainId, + Hex, + Json, +} from '@metamask/utils'; import type { Operation } from 'fast-json-patch'; import type { TransactionControllerMessenger } from './TransactionController.js'; @@ -2158,6 +2164,32 @@ export type AssetsFiatValues = { sending?: string; }; +/** + * Durable MetaMask Pay intent for a chain-agnostic payment source. + * + * The target transaction is the TransactionController record containing this + * intent, or the key of this intent in TransactionPayController state. + */ +export type MetamaskPayIntent = { + /** Schema version of the persisted intent. */ + version: 1; + + /** Canonical CAIP-10 identity of the source account. */ + sourceAccountId: CaipAccountId; + + /** Canonical CAIP-19 identity of the source asset. */ + sourceAssetId: CaipAssetType; + + /** Explicit CAIP-2 identity of the source chain. */ + sourceChainId: CaipChainId; + + /** Provider request ID used to reconcile execution after restart. */ + requestId?: string; + + /** Chain-native source transaction identifier, such as a Solana signature. */ + sourceTransactionId?: string; +}; + /** Metadata specific to the MetaMask Pay feature. */ export type MetamaskPayMetadata = { /** Total fee from any bridge transactions, in fiat currency. */ @@ -2180,6 +2212,9 @@ export type MetamaskPayMetadata = { */ isPostQuote?: boolean; + /** Durable chain-agnostic payment intent. */ + intent?: MetamaskPayIntent; + /** Total network fee in fiat currency, including the original and bridge transactions. */ networkFeeFiat?: string; diff --git a/packages/transaction-pay-controller/ARCHITECTURE.md b/packages/transaction-pay-controller/ARCHITECTURE.md index 1b7cc23602e..5ee3020f97e 100644 --- a/packages/transaction-pay-controller/ARCHITECTURE.md +++ b/packages/transaction-pay-controller/ARCHITECTURE.md @@ -68,6 +68,6 @@ The high level interaction with the `TransactionPayController` is as follows: ## State -State is grouped according to the associated transaction ID in the `transactionData` property. +Transient state is grouped according to the associated transaction ID in the `transactionData` property. It includes required tokens, the selected payment token, retrieved quotes, and calculated totals, and is not persisted across restarts. -This transaction specific data includes any required tokens, selected payment token, retrieved quotes, and calculated totals. +Durable chain-agnostic source identity and execution correlation are stored as versioned intents in `payIntents`, keyed by the target transaction ID. Each intent is also mirrored to the target transaction's `metamaskPay.intent` metadata. This lets recovery correlate the persisted Pay state with the persisted transaction record without storing non-EVM identifiers in legacy EVM-only fields. diff --git a/packages/transaction-pay-controller/CHANGELOG.md b/packages/transaction-pay-controller/CHANGELOG.md index d0587708279..f2541775bd2 100644 --- a/packages/transaction-pay-controller/CHANGELOG.md +++ b/packages/transaction-pay-controller/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add a persisted, versioned Pay intent model with canonical CAIP source identity and execution correlation, mirrored to transaction metadata for restart recovery. + ### Changed - Bump `@metamask/utils` from `^11.12.0` to `^12.0.0` ([#10192](https://github.com/MetaMask/core/pull/10192)) diff --git a/packages/transaction-pay-controller/src/TransactionPayController-method-action-types.ts b/packages/transaction-pay-controller/src/TransactionPayController-method-action-types.ts index 14a91436fa2..21d9cc72aaf 100644 --- a/packages/transaction-pay-controller/src/TransactionPayController-method-action-types.ts +++ b/packages/transaction-pay-controller/src/TransactionPayController-method-action-types.ts @@ -5,6 +5,22 @@ import type { TransactionPayController } from './TransactionPayController.js'; +/** + * Persists a versioned Pay intent and projects it onto the target + * transaction record for restart recovery. + * + * The chain-agnostic source identity remains in the additive intent model; + * legacy EVM-only Pay metadata is preserved unchanged. + * + * @param request - Pay intent and target transaction ID. + * @param request.intent - Durable Pay intent. + * @param request.transactionId - ID of the target transaction. + */ +export type TransactionPayControllerSetPayIntentAction = { + type: `TransactionPayController:setPayIntent`; + handler: TransactionPayController['setPayIntent']; +}; + /** * Sets the transaction configuration. * @@ -141,6 +157,7 @@ export type TransactionPayControllerPolymarketSubmitDepositWalletBatchAction = { * Union of all TransactionPayController action types. */ export type TransactionPayControllerMethodActions = + | TransactionPayControllerSetPayIntentAction | TransactionPayControllerSetTransactionConfigAction | TransactionPayControllerUpdatePaymentTokenAction | TransactionPayControllerUpdateFiatPaymentAction diff --git a/packages/transaction-pay-controller/src/TransactionPayController.test.ts b/packages/transaction-pay-controller/src/TransactionPayController.test.ts index e0650bfe7a6..12c3c15f1d6 100644 --- a/packages/transaction-pay-controller/src/TransactionPayController.test.ts +++ b/packages/transaction-pay-controller/src/TransactionPayController.test.ts @@ -1,7 +1,13 @@ /* eslint-disable no-new */ +import { deriveStateFromMetadata } from '@metamask/base-controller'; import type { TransactionMeta } from '@metamask/transaction-controller'; -import type { Hex } from '@metamask/utils'; +import type { + CaipAccountId, + CaipAssetType, + CaipChainId, + Hex, +} from '@metamask/utils'; import { updateFiatPayment } from './actions/update-fiat-payment.js'; import { updatePaymentToken } from './actions/update-payment-token.js'; @@ -12,6 +18,7 @@ import { getMessengerMock } from './tests/messenger-mock.js'; import type { TransactionPayControllerMessenger, TransactionPayControllerOptions, + TransactionPayIntent, TransactionPaySourceAmount, UpdateTransactionDataCallback, } from './types.js'; @@ -22,6 +29,7 @@ import { getTransaction, subscribeAssetChanges, subscribeTransactionChanges, + updateTransaction, } from './utils/transaction.js'; jest.mock('./actions/update-fiat-payment'); @@ -36,6 +44,17 @@ const TRANSACTION_ID_MOCK = '123-456'; const TRANSACTION_META_MOCK = { id: TRANSACTION_ID_MOCK } as TransactionMeta; const TOKEN_ADDRESS_MOCK = '0xabc' as Hex; const CHAIN_ID_MOCK = '0x1' as Hex; +const SOLANA_PAY_INTENT_MOCK: TransactionPayIntent = { + version: 1, + sourceAccountId: + 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp:7Ec4QeG8wF3RnTjHDrTuYP8hVV7WYuPFyM4hZUodkG6Z' as CaipAccountId, + sourceAssetId: + 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/token:EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v' as CaipAssetType, + sourceChainId: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp' as CaipChainId, + requestId: 'relay-request-123', + sourceTransactionId: + '5KtPn3tE7mWcMgvBvXhXxF9xQmP9xNj7wT6oYxj3BbJpfK2p5yhx2S1QnW8gQwJm7U3F5v9hQmXk8b2mJ7dP4c', +}; describe('TransactionPayController', () => { const updateFiatPaymentMock = jest.mocked(updateFiatPayment); const updatePaymentTokenMock = jest.mocked(updatePaymentToken); @@ -43,6 +62,7 @@ describe('TransactionPayController', () => { deriveFiatAssetForFiatPayment, ); const getTransactionMock = jest.mocked(getTransaction); + const updateTransactionMock = jest.mocked(updateTransaction); const updateSourceAmountsMock = jest.mocked(updateSourceAmounts); const updateQuotesMock = jest.mocked(updateQuotes); const subscribeTransactionChangesMock = jest.mocked( @@ -104,6 +124,128 @@ describe('TransactionPayController', () => { const getControllerState = subscribeAssetChangesMock.mock.calls[0][1]; expect(getControllerState()).toBe(controller.state); }); + + it('defaults persisted pay intents for legacy state', () => { + const controller = createController({ + state: { transactionData: {} }, + }); + + expect(controller.state.payIntents).toStrictEqual({}); + }); + + it('rehydrates persisted pay intents', () => { + const controller = createController({ + state: { + payIntents: { + [TRANSACTION_ID_MOCK]: SOLANA_PAY_INTENT_MOCK, + }, + }, + }); + + expect(controller.state.payIntents).toStrictEqual({ + [TRANSACTION_ID_MOCK]: SOLANA_PAY_INTENT_MOCK, + }); + }); + + it('persists pay intents without persisting transient transaction data', () => { + const controller = createController({ + state: { + payIntents: { + [TRANSACTION_ID_MOCK]: SOLANA_PAY_INTENT_MOCK, + }, + transactionData: { + [TRANSACTION_ID_MOCK]: { + isLoading: true, + tokens: [], + }, + }, + }, + }); + + expect( + deriveStateFromMetadata( + controller.state, + controller.metadata, + 'persist', + ), + ).toStrictEqual({ + payIntents: { + [TRANSACTION_ID_MOCK]: SOLANA_PAY_INTENT_MOCK, + }, + }); + }); + }); + + describe('setPayIntent', () => { + it('persists a pay intent and projects it onto the transaction record', () => { + const controller = createController(); + + controller.setPayIntent({ + transactionId: TRANSACTION_ID_MOCK, + intent: SOLANA_PAY_INTENT_MOCK, + }); + + expect(controller.state.payIntents[TRANSACTION_ID_MOCK]).toStrictEqual( + SOLANA_PAY_INTENT_MOCK, + ); + expect(updateTransactionMock).toHaveBeenCalledWith( + { + transactionId: TRANSACTION_ID_MOCK, + messenger, + note: 'Set transaction pay intent', + }, + expect.any(Function), + ); + + const updateTransactionCallback = updateTransactionMock.mock.calls[0][1]; + const transaction = { + metamaskPay: { + chainId: CHAIN_ID_MOCK, + sourceHash: '0xabc' as Hex, + tokenAddress: TOKEN_ADDRESS_MOCK, + }, + } as TransactionMeta; + + updateTransactionCallback(transaction); + + expect(transaction.metamaskPay).toStrictEqual({ + chainId: CHAIN_ID_MOCK, + intent: SOLANA_PAY_INTENT_MOCK, + sourceHash: '0xabc', + tokenAddress: TOKEN_ADDRESS_MOCK, + }); + }); + + it('does not project Solana identifiers into legacy EVM-only fields', () => { + const controller = createController(); + + controller.setPayIntent({ + transactionId: TRANSACTION_ID_MOCK, + intent: SOLANA_PAY_INTENT_MOCK, + }); + + const updateTransactionCallback = updateTransactionMock.mock.calls[0][1]; + const transaction = {} as TransactionMeta; + + updateTransactionCallback(transaction); + + expect(transaction.metamaskPay).toStrictEqual({ + intent: SOLANA_PAY_INTENT_MOCK, + }); + }); + + it('is callable via messenger action handler', () => { + const controller = createController(); + + messenger.call('TransactionPayController:setPayIntent', { + transactionId: TRANSACTION_ID_MOCK, + intent: SOLANA_PAY_INTENT_MOCK, + }); + + expect(controller.state.payIntents[TRANSACTION_ID_MOCK]).toStrictEqual( + SOLANA_PAY_INTENT_MOCK, + ); + }); }); describe('updatePaymentToken', () => { diff --git a/packages/transaction-pay-controller/src/TransactionPayController.ts b/packages/transaction-pay-controller/src/TransactionPayController.ts index 60e47e74567..0e5083164ba 100644 --- a/packages/transaction-pay-controller/src/TransactionPayController.ts +++ b/packages/transaction-pay-controller/src/TransactionPayController.ts @@ -18,6 +18,7 @@ import type { GetDelegationTransactionCallback, GetPaymentOverrideDataCallback, PolymarketCallbacks, + SetPayIntentRequest, TransactionConfig, TransactionConfigCallback, TransactionData, @@ -34,6 +35,7 @@ import { updateSourceAmounts } from './utils/source-amounts.js'; import { subscribeAssetChanges, subscribeTransactionChanges, + updateTransaction, } from './utils/transaction.js'; const MESSENGER_EXPOSED_METHODS = [ @@ -44,12 +46,19 @@ const MESSENGER_EXPOSED_METHODS = [ 'getStrategy', 'polymarketGetDepositWalletAddress', 'polymarketSubmitDepositWalletBatch', + 'setPayIntent', 'setTransactionConfig', 'updateFiatPayment', 'updatePaymentToken', ] as const; const stateMetadata: StateMetadata = { + payIntents: { + includeInDebugSnapshot: false, + includeInStateLogs: false, + persist: true, + usedInUi: true, + }, transactionData: { includeInDebugSnapshot: false, includeInStateLogs: true, @@ -59,6 +68,7 @@ const stateMetadata: StateMetadata = { }; const getDefaultState = (): TransactionPayControllerState => ({ + payIntents: {}, transactionData: {}, }); @@ -140,6 +150,35 @@ export class TransactionPayController extends BaseController< }); } + /** + * Persists a versioned Pay intent and projects it onto the target + * transaction record for restart recovery. + * + * The chain-agnostic source identity remains in the additive intent model; + * legacy EVM-only Pay metadata is preserved unchanged. + * + * @param request - Pay intent and target transaction ID. + * @param request.intent - Durable Pay intent. + * @param request.transactionId - ID of the target transaction. + */ + setPayIntent({ transactionId, intent }: SetPayIntentRequest): void { + updateTransaction( + { + transactionId, + messenger: this.messenger, + note: 'Set transaction pay intent', + }, + (transaction) => { + transaction.metamaskPay ??= {}; + transaction.metamaskPay.intent = { ...intent }; + }, + ); + + this.update((state) => { + state.payIntents[transactionId] = { ...intent }; + }); + } + /** * Sets the transaction configuration. * diff --git a/packages/transaction-pay-controller/src/index.ts b/packages/transaction-pay-controller/src/index.ts index f89d3983ca6..f5b956e5bfc 100644 --- a/packages/transaction-pay-controller/src/index.ts +++ b/packages/transaction-pay-controller/src/index.ts @@ -7,6 +7,10 @@ export type { GetBalanceResponse, GetPaymentOverrideDataRequest, GetPaymentOverrideDataResponse, + PolymarketCallbacks, + QuoteErrorInfo, + QuoteErrorReason, + SetPayIntentRequest, TransactionConfig, TransactionConfigCallback, TransactionData, @@ -19,10 +23,8 @@ export type { TransactionPayControllerMessenger, TransactionPayControllerOptions, TransactionPayControllerState, - PolymarketCallbacks, - QuoteErrorInfo, - QuoteErrorReason, TransactionPayControllerStateChangeEvent, + TransactionPayIntent, TransactionPaymentToken, TransactionPayQuote, TransactionPayRequiredToken, @@ -38,6 +40,7 @@ export type { TransactionPayControllerGetStrategyAction, TransactionPayControllerPolymarketGetDepositWalletAddressAction, TransactionPayControllerPolymarketSubmitDepositWalletBatchAction, + TransactionPayControllerSetPayIntentAction, TransactionPayControllerSetTransactionConfigAction, TransactionPayControllerUpdatePaymentTokenAction, TransactionPayControllerUpdateFiatPaymentAction, diff --git a/packages/transaction-pay-controller/src/types.ts b/packages/transaction-pay-controller/src/types.ts index d029e1e85b7..3ddff55a5e6 100644 --- a/packages/transaction-pay-controller/src/types.ts +++ b/packages/transaction-pay-controller/src/types.ts @@ -44,6 +44,7 @@ import type { import type { BatchTransaction, BatchTransactionParams, + MetamaskPayIntent, TransactionControllerAddTransactionAction, TransactionControllerGetGasFeeTokensAction, TransactionControllerGetStateAction, @@ -312,9 +313,24 @@ export type TransactionPayControllerOptions = { state?: Partial; }; +/** Durable, versioned Pay intent stored for restart recovery. */ +export type TransactionPayIntent = MetamaskPayIntent; + +/** Request to persist a Pay intent for a target transaction. */ +export type SetPayIntentRequest = { + /** Durable Pay intent. */ + intent: TransactionPayIntent; + + /** ID of the target TransactionController transaction. */ + transactionId: string; +}; + /** State of the TransactionPayController. */ export type TransactionPayControllerState = { - /** State relating to each transaction, keyed by transaction ID. */ + /** Durable Pay intents keyed by target transaction ID. */ + payIntents: Record; + + /** Transient state relating to each transaction, keyed by transaction ID. */ transactionData: Record; }; diff --git a/packages/transaction-pay-controller/src/utils/quotes.test.ts b/packages/transaction-pay-controller/src/utils/quotes.test.ts index 71c582f8fe8..6d4bfb7a1e9 100644 --- a/packages/transaction-pay-controller/src/utils/quotes.test.ts +++ b/packages/transaction-pay-controller/src/utils/quotes.test.ts @@ -1,7 +1,13 @@ import { TransactionStatus } from '@metamask/transaction-controller'; import type { TransactionMeta } from '@metamask/transaction-controller'; import type { BatchTransaction } from '@metamask/transaction-controller'; -import type { Hex, Json } from '@metamask/utils'; +import type { + CaipAccountId, + CaipAssetType, + CaipChainId, + Hex, + Json, +} from '@metamask/utils'; import { cloneDeep } from 'lodash-es'; import { TransactionPayStrategy } from '../constants.js'; @@ -967,16 +973,40 @@ describe('Quotes Utils', () => { const transactionMetaMock = {} as TransactionMeta; updateTransactionMock.mock.calls[0][1](transactionMetaMock); - expect(transactionMetaMock).toMatchObject({ - metamaskPay: { - bridgeFeeFiat: TOTALS_MOCK.fees.provider.usd, - chainId: TRANSACTION_DATA_MOCK.paymentToken?.chainId, - networkFeeFiat: TOTALS_MOCK.fees.sourceNetwork.estimate.usd, - strategy: TransactionPayStrategy.Across, - targetFiat: TOTALS_MOCK.targetAmount.usd, - tokenAddress: TRANSACTION_DATA_MOCK.paymentToken?.address, - totalFiat: TOTALS_MOCK.total.usd, - }, + expect(transactionMetaMock.metamaskPay).toStrictEqual({ + bridgeFeeFiat: TOTALS_MOCK.fees.provider.usd, + chainId: TRANSACTION_DATA_MOCK.paymentToken?.chainId, + isPostQuote: undefined, + networkFeeFiat: TOTALS_MOCK.fees.sourceNetwork.estimate.usd, + strategy: TransactionPayStrategy.Across, + targetFiat: TOTALS_MOCK.targetAmount.usd, + tokenAddress: TRANSACTION_DATA_MOCK.paymentToken?.address, + totalFiat: TOTALS_MOCK.total.usd, + }); + }); + + it('preserves a persisted chain-agnostic Pay intent when updating EVM compatibility metadata', async () => { + await run(); + + const intent = { + version: 1, + sourceAccountId: + 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp:7Ec4QeG8wF3RnTjHDrTuYP8hVV7WYuPFyM4hZUodkG6Z' as CaipAccountId, + sourceAssetId: + 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/slip44:501' as CaipAssetType, + sourceChainId: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp' as CaipChainId, + requestId: 'relay-request-123', + } as const; + const transactionMetaMock = { + metamaskPay: { intent }, + } as TransactionMeta; + + updateTransactionMock.mock.calls[0][1](transactionMetaMock); + + expect(transactionMetaMock.metamaskPay).toMatchObject({ + chainId: TRANSACTION_DATA_MOCK.paymentToken?.chainId, + intent, + tokenAddress: TRANSACTION_DATA_MOCK.paymentToken?.address, }); }); diff --git a/packages/transaction-pay-controller/src/utils/quotes.ts b/packages/transaction-pay-controller/src/utils/quotes.ts index b12e45816ca..a024758800b 100644 --- a/packages/transaction-pay-controller/src/utils/quotes.ts +++ b/packages/transaction-pay-controller/src/utils/quotes.ts @@ -276,10 +276,13 @@ function syncTransaction({ tx.isExternalSign = hasQuotes; } + const intent = tx.metamaskPay?.intent; + tx.metamaskPay = { bridgeFeeFiat: totals.fees.provider.usd, chainId: paymentToken?.chainId, isPostQuote, + ...(intent && { intent }), networkFeeFiat: totals.fees.sourceNetwork.estimate.usd, strategy, targetFiat: totals.targetAmount.usd, From ee17d5a13b1665b4c0c50eae919bd2d5dd3ed372 Mon Sep 17 00:00:00 2001 From: Pedro Figueiredo Date: Thu, 17 Sep 2026 13:23:56 +0100 Subject: [PATCH 2/2] refactor(transaction-pay-controller): narrow persisted pay source metadata --- packages/transaction-controller/CHANGELOG.md | 2 +- packages/transaction-controller/src/index.ts | 2 +- packages/transaction-controller/src/types.ts | 33 +--- .../ARCHITECTURE.md | 2 +- .../transaction-pay-controller/CHANGELOG.md | 2 +- ...actionPayController-method-action-types.ts | 19 ++- .../src/TransactionPayController.test.ts | 143 ++++++++++-------- .../src/TransactionPayController.ts | 40 +++-- .../transaction-pay-controller/src/index.ts | 6 +- .../transaction-pay-controller/src/types.ts | 17 +-- .../src/utils/quotes.test.ts | 21 +-- .../src/utils/quotes.ts | 4 +- 12 files changed, 135 insertions(+), 156 deletions(-) diff --git a/packages/transaction-controller/CHANGELOG.md b/packages/transaction-controller/CHANGELOG.md index 6d91799f546..f1d9847d095 100644 --- a/packages/transaction-controller/CHANGELOG.md +++ b/packages/transaction-controller/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Add versioned, chain-agnostic Pay intent metadata for persisting CAIP source identity and execution correlation without changing legacy EVM Pay fields. +- Add chain-agnostic Pay source metadata using CAIP-10 account and CAIP-19 asset identifiers without changing legacy EVM Pay fields ([#10272](https://github.com/MetaMask/core/pull/10272)) ### Changed diff --git a/packages/transaction-controller/src/index.ts b/packages/transaction-controller/src/index.ts index 2bbf040c2e4..f0dd613ead1 100644 --- a/packages/transaction-controller/src/index.ts +++ b/packages/transaction-controller/src/index.ts @@ -88,8 +88,8 @@ export type { IsAtomicBatchSupportedResultEntry, LegacyGasFeeEstimates, Log, - MetamaskPayIntent, MetamaskPayMetadata, + MetamaskPaySource, NestedTransactionMetadata, NestedTransactionUpdate, PublishBatchHook, diff --git a/packages/transaction-controller/src/types.ts b/packages/transaction-controller/src/types.ts index 8d54cdcb8f5..180900f3d67 100644 --- a/packages/transaction-controller/src/types.ts +++ b/packages/transaction-controller/src/types.ts @@ -4,13 +4,7 @@ import type { AccessList } from '@ethereumjs/tx'; import type { AccountsController } from '@metamask/accounts-controller'; import type { GasFeeState } from '@metamask/gas-fee-controller'; import type { NetworkClientId } from '@metamask/network-controller'; -import type { - CaipAccountId, - CaipAssetType, - CaipChainId, - Hex, - Json, -} from '@metamask/utils'; +import type { CaipAccountId, CaipAssetType, Hex, Json } from '@metamask/utils'; import type { Operation } from 'fast-json-patch'; import type { TransactionControllerMessenger } from './TransactionController.js'; @@ -2164,30 +2158,13 @@ export type AssetsFiatValues = { sending?: string; }; -/** - * Durable MetaMask Pay intent for a chain-agnostic payment source. - * - * The target transaction is the TransactionController record containing this - * intent, or the key of this intent in TransactionPayController state. - */ -export type MetamaskPayIntent = { - /** Schema version of the persisted intent. */ - version: 1; - +/** Chain-agnostic source metadata for a MetaMask Pay transaction. */ +export type MetamaskPaySource = { /** Canonical CAIP-10 identity of the source account. */ sourceAccountId: CaipAccountId; /** Canonical CAIP-19 identity of the source asset. */ sourceAssetId: CaipAssetType; - - /** Explicit CAIP-2 identity of the source chain. */ - sourceChainId: CaipChainId; - - /** Provider request ID used to reconcile execution after restart. */ - requestId?: string; - - /** Chain-native source transaction identifier, such as a Solana signature. */ - sourceTransactionId?: string; }; /** Metadata specific to the MetaMask Pay feature. */ @@ -2212,8 +2189,8 @@ export type MetamaskPayMetadata = { */ isPostQuote?: boolean; - /** Durable chain-agnostic payment intent. */ - intent?: MetamaskPayIntent; + /** Chain-agnostic payment source metadata. */ + source?: MetamaskPaySource; /** Total network fee in fiat currency, including the original and bridge transactions. */ networkFeeFiat?: string; diff --git a/packages/transaction-pay-controller/ARCHITECTURE.md b/packages/transaction-pay-controller/ARCHITECTURE.md index 5ee3020f97e..a3a66e013f5 100644 --- a/packages/transaction-pay-controller/ARCHITECTURE.md +++ b/packages/transaction-pay-controller/ARCHITECTURE.md @@ -70,4 +70,4 @@ The high level interaction with the `TransactionPayController` is as follows: Transient state is grouped according to the associated transaction ID in the `transactionData` property. It includes required tokens, the selected payment token, retrieved quotes, and calculated totals, and is not persisted across restarts. -Durable chain-agnostic source identity and execution correlation are stored as versioned intents in `payIntents`, keyed by the target transaction ID. Each intent is also mirrored to the target transaction's `metamaskPay.intent` metadata. This lets recovery correlate the persisted Pay state with the persisted transaction record without storing non-EVM identifiers in legacy EVM-only fields. +Chain-agnostic source account and asset metadata is stored only on the persisted target transaction in `metamaskPay.source`. The CAIP-10 account and CAIP-19 asset identify their source chain without placing non-EVM identifiers in legacy EVM-only fields. Execution correlation and recovery checkpoints are not part of ordinary Pay selection state. diff --git a/packages/transaction-pay-controller/CHANGELOG.md b/packages/transaction-pay-controller/CHANGELOG.md index f2541775bd2..0870802e091 100644 --- a/packages/transaction-pay-controller/CHANGELOG.md +++ b/packages/transaction-pay-controller/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Add a persisted, versioned Pay intent model with canonical CAIP source identity and execution correlation, mirrored to transaction metadata for restart recovery. +- Add validated chain-agnostic Pay source metadata to persisted transaction records while keeping ordinary Pay selection state transient ([#10272](https://github.com/MetaMask/core/pull/10272)) ### Changed diff --git a/packages/transaction-pay-controller/src/TransactionPayController-method-action-types.ts b/packages/transaction-pay-controller/src/TransactionPayController-method-action-types.ts index 21d9cc72aaf..2495a408164 100644 --- a/packages/transaction-pay-controller/src/TransactionPayController-method-action-types.ts +++ b/packages/transaction-pay-controller/src/TransactionPayController-method-action-types.ts @@ -6,19 +6,18 @@ import type { TransactionPayController } from './TransactionPayController.js'; /** - * Persists a versioned Pay intent and projects it onto the target - * transaction record for restart recovery. + * Persists chain-agnostic Pay source metadata on the target transaction. * - * The chain-agnostic source identity remains in the additive intent model; - * legacy EVM-only Pay metadata is preserved unchanged. + * The CAIP-10 account and CAIP-19 asset must identify the same chain. + * Legacy EVM-only Pay metadata is preserved unchanged. * - * @param request - Pay intent and target transaction ID. - * @param request.intent - Durable Pay intent. + * @param request - Pay source metadata and target transaction ID. + * @param request.source - Chain-agnostic payment source metadata. * @param request.transactionId - ID of the target transaction. */ -export type TransactionPayControllerSetPayIntentAction = { - type: `TransactionPayController:setPayIntent`; - handler: TransactionPayController['setPayIntent']; +export type TransactionPayControllerSetPaySourceAction = { + type: `TransactionPayController:setPaySource`; + handler: TransactionPayController['setPaySource']; }; /** @@ -157,7 +156,7 @@ export type TransactionPayControllerPolymarketSubmitDepositWalletBatchAction = { * Union of all TransactionPayController action types. */ export type TransactionPayControllerMethodActions = - | TransactionPayControllerSetPayIntentAction + | TransactionPayControllerSetPaySourceAction | TransactionPayControllerSetTransactionConfigAction | TransactionPayControllerUpdatePaymentTokenAction | TransactionPayControllerUpdateFiatPaymentAction diff --git a/packages/transaction-pay-controller/src/TransactionPayController.test.ts b/packages/transaction-pay-controller/src/TransactionPayController.test.ts index 12c3c15f1d6..0d1a03852ab 100644 --- a/packages/transaction-pay-controller/src/TransactionPayController.test.ts +++ b/packages/transaction-pay-controller/src/TransactionPayController.test.ts @@ -2,12 +2,7 @@ import { deriveStateFromMetadata } from '@metamask/base-controller'; import type { TransactionMeta } from '@metamask/transaction-controller'; -import type { - CaipAccountId, - CaipAssetType, - CaipChainId, - Hex, -} from '@metamask/utils'; +import type { CaipAccountId, CaipAssetType, Hex } from '@metamask/utils'; import { updateFiatPayment } from './actions/update-fiat-payment.js'; import { updatePaymentToken } from './actions/update-payment-token.js'; @@ -18,7 +13,7 @@ import { getMessengerMock } from './tests/messenger-mock.js'; import type { TransactionPayControllerMessenger, TransactionPayControllerOptions, - TransactionPayIntent, + TransactionPaySource, TransactionPaySourceAmount, UpdateTransactionDataCallback, } from './types.js'; @@ -44,16 +39,11 @@ const TRANSACTION_ID_MOCK = '123-456'; const TRANSACTION_META_MOCK = { id: TRANSACTION_ID_MOCK } as TransactionMeta; const TOKEN_ADDRESS_MOCK = '0xabc' as Hex; const CHAIN_ID_MOCK = '0x1' as Hex; -const SOLANA_PAY_INTENT_MOCK: TransactionPayIntent = { - version: 1, +const SOLANA_PAY_SOURCE_MOCK: TransactionPaySource = { sourceAccountId: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp:7Ec4QeG8wF3RnTjHDrTuYP8hVV7WYuPFyM4hZUodkG6Z' as CaipAccountId, sourceAssetId: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/token:EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v' as CaipAssetType, - sourceChainId: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp' as CaipChainId, - requestId: 'relay-request-123', - sourceTransactionId: - '5KtPn3tE7mWcMgvBvXhXxF9xQmP9xNj7wT6oYxj3BbJpfK2p5yhx2S1QnW8gQwJm7U3F5v9hQmXk8b2mJ7dP4c', }; describe('TransactionPayController', () => { const updateFiatPaymentMock = jest.mocked(updateFiatPayment); @@ -125,34 +115,9 @@ describe('TransactionPayController', () => { expect(getControllerState()).toBe(controller.state); }); - it('defaults persisted pay intents for legacy state', () => { - const controller = createController({ - state: { transactionData: {} }, - }); - - expect(controller.state.payIntents).toStrictEqual({}); - }); - - it('rehydrates persisted pay intents', () => { - const controller = createController({ - state: { - payIntents: { - [TRANSACTION_ID_MOCK]: SOLANA_PAY_INTENT_MOCK, - }, - }, - }); - - expect(controller.state.payIntents).toStrictEqual({ - [TRANSACTION_ID_MOCK]: SOLANA_PAY_INTENT_MOCK, - }); - }); - - it('persists pay intents without persisting transient transaction data', () => { + it('keeps ordinary Pay selection state transient', () => { const controller = createController({ state: { - payIntents: { - [TRANSACTION_ID_MOCK]: SOLANA_PAY_INTENT_MOCK, - }, transactionData: { [TRANSACTION_ID_MOCK]: { isLoading: true, @@ -168,31 +133,25 @@ describe('TransactionPayController', () => { controller.metadata, 'persist', ), - ).toStrictEqual({ - payIntents: { - [TRANSACTION_ID_MOCK]: SOLANA_PAY_INTENT_MOCK, - }, - }); + ).toStrictEqual({}); }); }); - describe('setPayIntent', () => { - it('persists a pay intent and projects it onto the transaction record', () => { + describe('setPaySource', () => { + it('persists the source only on the target transaction record', () => { const controller = createController(); - controller.setPayIntent({ + controller.setPaySource({ transactionId: TRANSACTION_ID_MOCK, - intent: SOLANA_PAY_INTENT_MOCK, + source: SOLANA_PAY_SOURCE_MOCK, }); - expect(controller.state.payIntents[TRANSACTION_ID_MOCK]).toStrictEqual( - SOLANA_PAY_INTENT_MOCK, - ); + expect(controller.state).toStrictEqual({ transactionData: {} }); expect(updateTransactionMock).toHaveBeenCalledWith( { transactionId: TRANSACTION_ID_MOCK, messenger, - note: 'Set transaction pay intent', + note: 'Set transaction pay source', }, expect.any(Function), ); @@ -210,7 +169,7 @@ describe('TransactionPayController', () => { expect(transaction.metamaskPay).toStrictEqual({ chainId: CHAIN_ID_MOCK, - intent: SOLANA_PAY_INTENT_MOCK, + source: SOLANA_PAY_SOURCE_MOCK, sourceHash: '0xabc', tokenAddress: TOKEN_ADDRESS_MOCK, }); @@ -219,9 +178,9 @@ describe('TransactionPayController', () => { it('does not project Solana identifiers into legacy EVM-only fields', () => { const controller = createController(); - controller.setPayIntent({ + controller.setPaySource({ transactionId: TRANSACTION_ID_MOCK, - intent: SOLANA_PAY_INTENT_MOCK, + source: SOLANA_PAY_SOURCE_MOCK, }); const updateTransactionCallback = updateTransactionMock.mock.calls[0][1]; @@ -230,21 +189,51 @@ describe('TransactionPayController', () => { updateTransactionCallback(transaction); expect(transaction.metamaskPay).toStrictEqual({ - intent: SOLANA_PAY_INTENT_MOCK, + source: SOLANA_PAY_SOURCE_MOCK, }); }); - it('is callable via messenger action handler', () => { + it('rejects source account and asset identifiers from different chains', () => { + const controller = createController(); + + expect(() => + controller.setPaySource({ + transactionId: TRANSACTION_ID_MOCK, + source: { + ...SOLANA_PAY_SOURCE_MOCK, + sourceAssetId: 'eip155:1/slip44:60' as CaipAssetType, + }, + }), + ).toThrow('Pay source account and asset must use the same chain'); + + expect(updateTransactionMock).not.toHaveBeenCalled(); + }); + + it('validates source identifiers before updating the transaction', () => { const controller = createController(); - messenger.call('TransactionPayController:setPayIntent', { + expect(() => + controller.setPaySource({ + transactionId: TRANSACTION_ID_MOCK, + source: { + ...SOLANA_PAY_SOURCE_MOCK, + sourceAccountId: 'invalid' as CaipAccountId, + }, + }), + ).toThrow('Invalid CAIP account ID'); + + expect(updateTransactionMock).not.toHaveBeenCalled(); + }); + + it('is callable via messenger action handler', () => { + createController(); + + messenger.call('TransactionPayController:setPaySource', { transactionId: TRANSACTION_ID_MOCK, - intent: SOLANA_PAY_INTENT_MOCK, + source: SOLANA_PAY_SOURCE_MOCK, }); - expect(controller.state.payIntents[TRANSACTION_ID_MOCK]).toStrictEqual( - SOLANA_PAY_INTENT_MOCK, - ); + expect(updateTransactionMock).toHaveBeenCalledTimes(1); }); }); @@ -1136,6 +1125,36 @@ describe('TransactionPayController', () => { controller.state.transactionData[TRANSACTION_ID_MOCK], ).toBeUndefined(); }); + + it('removes only controller-owned transient data', () => { + const controller = createController(); + controller.setPaySource({ + transactionId: TRANSACTION_ID_MOCK, + source: SOLANA_PAY_SOURCE_MOCK, + }); + + const transaction = {} as TransactionMeta; + updateTransactionMock.mock.calls[0][1](transaction); + + controller.updatePaymentToken({ + transactionId: TRANSACTION_ID_MOCK, + tokenAddress: TOKEN_ADDRESS_MOCK, + chainId: CHAIN_ID_MOCK, + }); + const { updateTransactionData } = updatePaymentTokenMock.mock.calls[0][1]; + updateTransactionData(TRANSACTION_ID_MOCK, () => undefined); + + const removeTransactionDataCallback = + subscribeTransactionChangesMock.mock.calls[0][2]; + removeTransactionDataCallback(TRANSACTION_ID_MOCK); + + expect( + controller.state.transactionData[TRANSACTION_ID_MOCK], + ).toBeUndefined(); + expect(transaction.metamaskPay?.source).toStrictEqual( + SOLANA_PAY_SOURCE_MOCK, + ); + }); }); describe('fiat token selection', () => { diff --git a/packages/transaction-pay-controller/src/TransactionPayController.ts b/packages/transaction-pay-controller/src/TransactionPayController.ts index 0e5083164ba..c86f7553d1e 100644 --- a/packages/transaction-pay-controller/src/TransactionPayController.ts +++ b/packages/transaction-pay-controller/src/TransactionPayController.ts @@ -1,6 +1,7 @@ import type { StateMetadata } from '@metamask/base-controller'; import { BaseController } from '@metamask/base-controller'; import type { TransactionMeta } from '@metamask/transaction-controller'; +import { parseCaipAccountId, parseCaipAssetType } from '@metamask/utils'; import type { Draft } from 'immer'; import { noop } from 'lodash-es'; @@ -18,7 +19,7 @@ import type { GetDelegationTransactionCallback, GetPaymentOverrideDataCallback, PolymarketCallbacks, - SetPayIntentRequest, + SetPaySourceRequest, TransactionConfig, TransactionConfigCallback, TransactionData, @@ -46,19 +47,13 @@ const MESSENGER_EXPOSED_METHODS = [ 'getStrategy', 'polymarketGetDepositWalletAddress', 'polymarketSubmitDepositWalletBatch', - 'setPayIntent', + 'setPaySource', 'setTransactionConfig', 'updateFiatPayment', 'updatePaymentToken', ] as const; const stateMetadata: StateMetadata = { - payIntents: { - includeInDebugSnapshot: false, - includeInStateLogs: false, - persist: true, - usedInUi: true, - }, transactionData: { includeInDebugSnapshot: false, includeInStateLogs: true, @@ -68,7 +63,6 @@ const stateMetadata: StateMetadata = { }; const getDefaultState = (): TransactionPayControllerState => ({ - payIntents: {}, transactionData: {}, }); @@ -151,32 +145,34 @@ export class TransactionPayController extends BaseController< } /** - * Persists a versioned Pay intent and projects it onto the target - * transaction record for restart recovery. + * Persists chain-agnostic Pay source metadata on the target transaction. * - * The chain-agnostic source identity remains in the additive intent model; - * legacy EVM-only Pay metadata is preserved unchanged. + * The CAIP-10 account and CAIP-19 asset must identify the same chain. + * Legacy EVM-only Pay metadata is preserved unchanged. * - * @param request - Pay intent and target transaction ID. - * @param request.intent - Durable Pay intent. + * @param request - Pay source metadata and target transaction ID. + * @param request.source - Chain-agnostic payment source metadata. * @param request.transactionId - ID of the target transaction. */ - setPayIntent({ transactionId, intent }: SetPayIntentRequest): void { + setPaySource({ transactionId, source }: SetPaySourceRequest): void { + const accountChainId = parseCaipAccountId(source.sourceAccountId).chainId; + const assetChainId = parseCaipAssetType(source.sourceAssetId).chainId; + + if (accountChainId !== assetChainId) { + throw new Error('Pay source account and asset must use the same chain'); + } + updateTransaction( { transactionId, messenger: this.messenger, - note: 'Set transaction pay intent', + note: 'Set transaction pay source', }, (transaction) => { transaction.metamaskPay ??= {}; - transaction.metamaskPay.intent = { ...intent }; + transaction.metamaskPay.source = { ...source }; }, ); - - this.update((state) => { - state.payIntents[transactionId] = { ...intent }; - }); } /** diff --git a/packages/transaction-pay-controller/src/index.ts b/packages/transaction-pay-controller/src/index.ts index f5b956e5bfc..552d61d843d 100644 --- a/packages/transaction-pay-controller/src/index.ts +++ b/packages/transaction-pay-controller/src/index.ts @@ -10,7 +10,7 @@ export type { PolymarketCallbacks, QuoteErrorInfo, QuoteErrorReason, - SetPayIntentRequest, + SetPaySourceRequest, TransactionConfig, TransactionConfigCallback, TransactionData, @@ -24,10 +24,10 @@ export type { TransactionPayControllerOptions, TransactionPayControllerState, TransactionPayControllerStateChangeEvent, - TransactionPayIntent, TransactionPaymentToken, TransactionPayQuote, TransactionPayRequiredToken, + TransactionPaySource, TransactionPaySourceAmount, TransactionPayTotals, UpdateFiatPaymentRequest, @@ -40,7 +40,7 @@ export type { TransactionPayControllerGetStrategyAction, TransactionPayControllerPolymarketGetDepositWalletAddressAction, TransactionPayControllerPolymarketSubmitDepositWalletBatchAction, - TransactionPayControllerSetPayIntentAction, + TransactionPayControllerSetPaySourceAction, TransactionPayControllerSetTransactionConfigAction, TransactionPayControllerUpdatePaymentTokenAction, TransactionPayControllerUpdateFiatPaymentAction, diff --git a/packages/transaction-pay-controller/src/types.ts b/packages/transaction-pay-controller/src/types.ts index 3ddff55a5e6..9cf4d48e84d 100644 --- a/packages/transaction-pay-controller/src/types.ts +++ b/packages/transaction-pay-controller/src/types.ts @@ -44,7 +44,7 @@ import type { import type { BatchTransaction, BatchTransactionParams, - MetamaskPayIntent, + MetamaskPaySource, TransactionControllerAddTransactionAction, TransactionControllerGetGasFeeTokensAction, TransactionControllerGetStateAction, @@ -313,13 +313,13 @@ export type TransactionPayControllerOptions = { state?: Partial; }; -/** Durable, versioned Pay intent stored for restart recovery. */ -export type TransactionPayIntent = MetamaskPayIntent; +/** Chain-agnostic source metadata for a MetaMask Pay transaction. */ +export type TransactionPaySource = MetamaskPaySource; -/** Request to persist a Pay intent for a target transaction. */ -export type SetPayIntentRequest = { - /** Durable Pay intent. */ - intent: TransactionPayIntent; +/** Request to persist Pay source metadata on a target transaction. */ +export type SetPaySourceRequest = { + /** Chain-agnostic payment source metadata. */ + source: TransactionPaySource; /** ID of the target TransactionController transaction. */ transactionId: string; @@ -327,9 +327,6 @@ export type SetPayIntentRequest = { /** State of the TransactionPayController. */ export type TransactionPayControllerState = { - /** Durable Pay intents keyed by target transaction ID. */ - payIntents: Record; - /** Transient state relating to each transaction, keyed by transaction ID. */ transactionData: Record; }; diff --git a/packages/transaction-pay-controller/src/utils/quotes.test.ts b/packages/transaction-pay-controller/src/utils/quotes.test.ts index 6d4bfb7a1e9..67480f402fa 100644 --- a/packages/transaction-pay-controller/src/utils/quotes.test.ts +++ b/packages/transaction-pay-controller/src/utils/quotes.test.ts @@ -1,13 +1,7 @@ import { TransactionStatus } from '@metamask/transaction-controller'; import type { TransactionMeta } from '@metamask/transaction-controller'; import type { BatchTransaction } from '@metamask/transaction-controller'; -import type { - CaipAccountId, - CaipAssetType, - CaipChainId, - Hex, - Json, -} from '@metamask/utils'; +import type { CaipAccountId, CaipAssetType, Hex, Json } from '@metamask/utils'; import { cloneDeep } from 'lodash-es'; import { TransactionPayStrategy } from '../constants.js'; @@ -985,27 +979,24 @@ describe('Quotes Utils', () => { }); }); - it('preserves a persisted chain-agnostic Pay intent when updating EVM compatibility metadata', async () => { + it('preserves chain-agnostic source metadata when updating EVM compatibility metadata', async () => { await run(); - const intent = { - version: 1, + const source = { sourceAccountId: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp:7Ec4QeG8wF3RnTjHDrTuYP8hVV7WYuPFyM4hZUodkG6Z' as CaipAccountId, sourceAssetId: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/slip44:501' as CaipAssetType, - sourceChainId: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp' as CaipChainId, - requestId: 'relay-request-123', - } as const; + }; const transactionMetaMock = { - metamaskPay: { intent }, + metamaskPay: { source }, } as TransactionMeta; updateTransactionMock.mock.calls[0][1](transactionMetaMock); expect(transactionMetaMock.metamaskPay).toMatchObject({ chainId: TRANSACTION_DATA_MOCK.paymentToken?.chainId, - intent, + source, tokenAddress: TRANSACTION_DATA_MOCK.paymentToken?.address, }); }); diff --git a/packages/transaction-pay-controller/src/utils/quotes.ts b/packages/transaction-pay-controller/src/utils/quotes.ts index a024758800b..5426b892b2d 100644 --- a/packages/transaction-pay-controller/src/utils/quotes.ts +++ b/packages/transaction-pay-controller/src/utils/quotes.ts @@ -276,13 +276,13 @@ function syncTransaction({ tx.isExternalSign = hasQuotes; } - const intent = tx.metamaskPay?.intent; + const source = tx.metamaskPay?.source; tx.metamaskPay = { bridgeFeeFiat: totals.fees.provider.usd, chainId: paymentToken?.chainId, isPostQuote, - ...(intent && { intent }), + ...(source && { source }), networkFeeFiat: totals.fees.sourceNetwork.estimate.usd, strategy, targetFiat: totals.targetAmount.usd,