From 4fddaecb311ffa3e46189c7eeeb59bc23fb973f6 Mon Sep 17 00:00:00 2001 From: Jyoti Puri Date: Wed, 20 May 2026 17:22:29 +0530 Subject: [PATCH 01/35] feat: add transaction data property useMoneyAccount --- .../src/TransactionPayController.test.ts | 12 ++++++++++++ .../src/TransactionPayController.ts | 2 ++ packages/transaction-pay-controller/src/types.ts | 6 ++++++ 3 files changed, 20 insertions(+) diff --git a/packages/transaction-pay-controller/src/TransactionPayController.test.ts b/packages/transaction-pay-controller/src/TransactionPayController.test.ts index 76d3ecd6d6..734e3ebeae 100644 --- a/packages/transaction-pay-controller/src/TransactionPayController.test.ts +++ b/packages/transaction-pay-controller/src/TransactionPayController.test.ts @@ -206,6 +206,18 @@ describe('TransactionPayController', () => { ).toBe(true); }); + it('updates useMoneyAccount in state', () => { + const controller = createController(); + + controller.setTransactionConfig(TRANSACTION_ID_MOCK, (config) => { + config.useMoneyAccount = true; + }); + + expect( + controller.state.transactionData[TRANSACTION_ID_MOCK].useMoneyAccount, + ).toBe(true); + }); + it('triggers source amounts and quotes update when only isPostQuote changes', () => { const controller = createController(); diff --git a/packages/transaction-pay-controller/src/TransactionPayController.ts b/packages/transaction-pay-controller/src/TransactionPayController.ts index 70b4b3f8d6..f851b686d9 100644 --- a/packages/transaction-pay-controller/src/TransactionPayController.ts +++ b/packages/transaction-pay-controller/src/TransactionPayController.ts @@ -140,6 +140,7 @@ export class TransactionPayController extends BaseController< isPolymarketDepositWallet: transactionData.isPolymarketDepositWallet, refundTo: transactionData.refundTo, accountOverride: transactionData.accountOverride, + useMoneyAccount: transactionData.useMoneyAccount, }; const previousAccountOverride = config.accountOverride; @@ -153,6 +154,7 @@ export class TransactionPayController extends BaseController< transactionData.isPolymarketDepositWallet = config.isPolymarketDepositWallet; transactionData.refundTo = config.refundTo; + transactionData.useMoneyAccount = config.useMoneyAccount; if ( !config.isPostQuote && diff --git a/packages/transaction-pay-controller/src/types.ts b/packages/transaction-pay-controller/src/types.ts index 8844400512..b46019fc57 100644 --- a/packages/transaction-pay-controller/src/types.ts +++ b/packages/transaction-pay-controller/src/types.ts @@ -140,6 +140,9 @@ export type TransactionConfig = { * When `isPostQuote` is false, it provides the funds and pays for gas. */ accountOverride?: Hex; + + /** Whether to use the Money Account (Money Keyring) as the payment source. */ + useMoneyAccount?: boolean; }; /** Callback to update transaction config. */ @@ -246,6 +249,9 @@ export type TransactionData = { */ accountOverride?: Hex; + /** Whether to use the Money Account (Money Keyring) as the payment source. */ + useMoneyAccount?: boolean; + /** * Token selected for the transaction. * - For standard flows (isPostQuote=false): This is the SOURCE/payment token From a1258e6fdc606fe2335f899eea6277514cdd166b Mon Sep 17 00:00:00 2001 From: Jyoti Puri Date: Wed, 20 May 2026 17:25:38 +0530 Subject: [PATCH 02/35] update changelog --- packages/transaction-pay-controller/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/transaction-pay-controller/CHANGELOG.md b/packages/transaction-pay-controller/CHANGELOG.md index fc2ba23cda..92fc201ea4 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 `useMoneyAccount` property to `TransactionConfig` and `TransactionData`, settable via `setTransactionConfig` ([#8858](https://github.com/MetaMask/core/pull/8858)) + ## [22.6.1] ### Changed From 3ecc08618163227361ff966c41beaffd5ff2b15b Mon Sep 17 00:00:00 2001 From: Jyoti Puri Date: Thu, 21 May 2026 11:05:46 +0530 Subject: [PATCH 03/35] feat: support for money account source for MM pay deposit transactions --- .../transaction-pay-controller/CHANGELOG.md | 1 + ...actionPayController-method-action-types.ts | 17 +++ .../src/TransactionPayController.test.ts | 39 +++++++ .../src/TransactionPayController.ts | 23 ++++ .../src/strategy/relay/relay-quotes.test.ts | 107 ++++++++++++++++++ .../src/strategy/relay/relay-quotes.ts | 90 ++++++++++++++- .../src/tests/messenger-mock.ts | 11 ++ .../transaction-pay-controller/src/types.ts | 19 ++++ .../src/utils/quotes.ts | 9 ++ 9 files changed, 314 insertions(+), 2 deletions(-) diff --git a/packages/transaction-pay-controller/CHANGELOG.md b/packages/transaction-pay-controller/CHANGELOG.md index 92fc201ea4..e4f29a8f8d 100644 --- a/packages/transaction-pay-controller/CHANGELOG.md +++ b/packages/transaction-pay-controller/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Add `useMoneyAccount` property to `TransactionConfig` and `TransactionData`, settable via `setTransactionConfig` ([#8858](https://github.com/MetaMask/core/pull/8858)) +- Add `getMoneyAccountTransactions` callback to `TransactionPayControllerOptions`; when `useMoneyAccount` is true on a transaction, the publish hook calls this callback and passes the resulting `additionalTransactions` to the strategy's `execute` request, ordered before or after the quote batch based on `isPostQuote` ([#8858](https://github.com/MetaMask/core/pull/8858)) ## [22.6.1] 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 8afd1b0559..55a173aff4 100644 --- a/packages/transaction-pay-controller/src/TransactionPayController-method-action-types.ts +++ b/packages/transaction-pay-controller/src/TransactionPayController-method-action-types.ts @@ -49,6 +49,22 @@ export type TransactionPayControllerUpdateFiatPaymentAction = { handler: TransactionPayController['updateFiatPayment']; }; +/** + * Returns additional transactions for the money account flow. + * + * Delegates to the client-supplied callback. Called during quote execution + * when `useMoneyAccount` is true. Returns an empty array when no callback + * is configured. + * + * @param args - The arguments forwarded to the {@link GetMoneyAccountTransactionsCallback}, + * containing the transaction ID. + * @returns A promise resolving to the additional transactions array. + */ +export type TransactionPayControllerGetMoneyAccountTransactionsAction = { + type: `TransactionPayController:getMoneyAccountTransactions`; + handler: TransactionPayController['getMoneyAccountTransactions']; +}; + /** * Gets the delegation transaction for a given transaction. * @@ -113,6 +129,7 @@ export type TransactionPayControllerMethodActions = | TransactionPayControllerUpdatePaymentTokenAction | TransactionPayControllerUpdateFiatPaymentAction | TransactionPayControllerGetDelegationTransactionAction + | TransactionPayControllerGetMoneyAccountTransactionsAction | TransactionPayControllerGetStrategyAction | TransactionPayControllerPolymarketGetDepositWalletAddressAction | TransactionPayControllerPolymarketSubmitDepositWalletBatchAction; diff --git a/packages/transaction-pay-controller/src/TransactionPayController.test.ts b/packages/transaction-pay-controller/src/TransactionPayController.test.ts index 734e3ebeae..4bec5185d3 100644 --- a/packages/transaction-pay-controller/src/TransactionPayController.test.ts +++ b/packages/transaction-pay-controller/src/TransactionPayController.test.ts @@ -469,6 +469,45 @@ describe('TransactionPayController', () => { }); }); + describe('getMoneyAccountTransactions', () => { + it('delegates to the callback', async () => { + const txMock = { from: '0xabc', to: '0xdef' }; + const getMoneyAccountTransactionsMock = jest + .fn() + .mockResolvedValue([txMock]); + + new TransactionPayController({ + getDelegationTransaction: jest.fn(), + getMoneyAccountTransactions: getMoneyAccountTransactionsMock, + messenger, + }); + + const result = await messenger.call( + 'TransactionPayController:getMoneyAccountTransactions', + TRANSACTION_ID_MOCK, + ); + + expect(getMoneyAccountTransactionsMock).toHaveBeenCalledWith( + TRANSACTION_ID_MOCK, + ); + expect(result).toStrictEqual([txMock]); + }); + + it('returns empty array when no callback is configured', async () => { + new TransactionPayController({ + getDelegationTransaction: jest.fn(), + messenger, + }); + + const result = await messenger.call( + 'TransactionPayController:getMoneyAccountTransactions', + TRANSACTION_ID_MOCK, + ); + + expect(result).toStrictEqual([]); + }); + }); + describe('polymarket callbacks', () => { const EOA_MOCK = '0x1111111111111111111111111111111111111111' as Hex; const DEPOSIT_WALLET_MOCK = diff --git a/packages/transaction-pay-controller/src/TransactionPayController.ts b/packages/transaction-pay-controller/src/TransactionPayController.ts index f851b686d9..85920508a3 100644 --- a/packages/transaction-pay-controller/src/TransactionPayController.ts +++ b/packages/transaction-pay-controller/src/TransactionPayController.ts @@ -15,6 +15,7 @@ import { QuoteRefresher } from './helpers/QuoteRefresher'; import { deriveFiatAssetForFiatPayment } from './strategy/fiat/utils'; import type { GetDelegationTransactionCallback, + GetMoneyAccountTransactionsCallback, PolymarketCallbacks, TransactionConfigCallback, TransactionData, @@ -36,6 +37,7 @@ import { const MESSENGER_EXPOSED_METHODS = [ 'getDelegationTransaction', + 'getMoneyAccountTransactions', 'getStrategy', 'polymarketGetDepositWalletAddress', 'polymarketSubmitDepositWalletBatch', @@ -64,6 +66,8 @@ export class TransactionPayController extends BaseController< > { readonly #getDelegationTransaction: GetDelegationTransactionCallback; + readonly #getMoneyAccountTransactions?: GetMoneyAccountTransactionsCallback; + readonly #getStrategy?: ( transaction: TransactionMeta, ) => TransactionPayStrategy; @@ -76,6 +80,7 @@ export class TransactionPayController extends BaseController< constructor({ getDelegationTransaction, + getMoneyAccountTransactions, getStrategy, getStrategies, messenger, @@ -90,6 +95,7 @@ export class TransactionPayController extends BaseController< }); this.#getDelegationTransaction = getDelegationTransaction; + this.#getMoneyAccountTransactions = getMoneyAccountTransactions; this.#getStrategy = getStrategy; this.#getStrategies = getStrategies; this.#polymarket = polymarket; @@ -217,6 +223,23 @@ export class TransactionPayController extends BaseController< return this.#getDelegationTransaction(...args); } + /** + * Returns additional transactions for the money account flow. + * + * Delegates to the client-supplied {@link GetMoneyAccountTransactionsCallback}. + * Called during quote execution when `useMoneyAccount` is true on the transaction. + * Returns an empty array when no callback is configured. + * + * @param args - The arguments forwarded to the {@link GetMoneyAccountTransactionsCallback}, + * containing the transaction ID. + * @returns A promise resolving to the additional transactions array. + */ + getMoneyAccountTransactions( + ...args: Parameters + ): ReturnType { + return this.#getMoneyAccountTransactions?.(...args) ?? Promise.resolve([]); + } + /** * Gets the preferred strategy for a transaction. * diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts index 9439faf4cc..4a37ed6112 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts @@ -183,6 +183,7 @@ describe('Relay Quotes Utils', () => { getDelegationTransactionMock, getGasFeeTokensMock, getKeyringControllerStateMock, + getMoneyAccountTransactionsMock, getRemoteFeatureFlagControllerStateMock, polymarketGetDepositWalletAddressMock, } = getMessengerMock(); @@ -3375,6 +3376,112 @@ describe('Relay Quotes Utils', () => { }); }); + describe('money account deposit step injection (useMoneyAccount)', () => { + const DEPOSIT_TX_MOCK = { + from: FROM_MOCK, + to: '0xmoneyaccount' as Hex, + data: '0xdeposit' as Hex, + value: '0x0', + gas: '30000', + maxFeePerGas: '1000000000', + maxPriorityFeePerGas: '2000000000', + }; + + beforeEach(() => { + successfulFetchMock.mockResolvedValue({ + ok: true, + json: async () => QUOTE_MOCK, + } as never); + + estimateGasBatchMock.mockResolvedValue({ + gasLimits: [30000, 21000], + totalGasLimit: 51000, + }); + }); + + it('prepends deposit step before relay steps for standard (non-post-quote) flow', async () => { + getMoneyAccountTransactionsMock.mockResolvedValue([DEPOSIT_TX_MOCK]); + + const [quote] = await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [{ ...QUOTE_REQUEST_MOCK, useMoneyAccount: true }], + transaction: TRANSACTION_META_MOCK, + }); + + const txSteps = quote.original.steps.filter( + (s): s is RelayTransactionStep => s.kind === 'transaction', + ); + expect(txSteps[0].id).toBe('money-account-deposit'); + expect(txSteps[0].items[0].data.to).toBe(DEPOSIT_TX_MOCK.to); + expect(txSteps[1].id).toBe(STEP_MOCK.id); + }); + + it('appends deposit step after relay steps for post-quote flow', async () => { + getMoneyAccountTransactionsMock.mockResolvedValue([DEPOSIT_TX_MOCK]); + + const [quote] = await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [ + { ...QUOTE_REQUEST_MOCK, useMoneyAccount: true, isPostQuote: true }, + ], + transaction: TRANSACTION_META_MOCK, + }); + + const txSteps = quote.original.steps.filter( + (s): s is RelayTransactionStep => s.kind === 'transaction', + ); + expect(txSteps[0].id).toBe(STEP_MOCK.id); + expect(txSteps[1].id).toBe('money-account-deposit'); + }); + + it('does not inject when useMoneyAccount is false', async () => { + const [quote] = await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }); + + expect( + quote.original.steps.every((s) => s.id !== 'money-account-deposit'), + ).toBe(true); + expect(getMoneyAccountTransactionsMock).not.toHaveBeenCalled(); + }); + + it('does not inject when callback returns empty array', async () => { + getMoneyAccountTransactionsMock.mockResolvedValue([]); + + const [quote] = await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [{ ...QUOTE_REQUEST_MOCK, useMoneyAccount: true }], + transaction: TRANSACTION_META_MOCK, + }); + + expect(quote.original.steps).toHaveLength(1); + expect(quote.original.steps[0].id).toBe(STEP_MOCK.id); + }); + + it('uses sourceChainId to set chainId on deposit step items', async () => { + getMoneyAccountTransactionsMock.mockResolvedValue([DEPOSIT_TX_MOCK]); + + const [quote] = await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [{ ...QUOTE_REQUEST_MOCK, useMoneyAccount: true }], + transaction: TRANSACTION_META_MOCK, + }); + + const depositStep = quote.original.steps.find( + (s) => s.id === 'money-account-deposit', + ) as RelayTransactionStep; + // QUOTE_REQUEST_MOCK.sourceChainId is '0x1' → chainId 1 + expect(depositStep.items[0].data.chainId).toBe(1); + }); + }); + describe('gas buffer support', () => { it('applies buffer to single transaction gas estimate', async () => { const quoteMock = cloneDeep(QUOTE_MOCK); diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts index 08077cd823..6961798028 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts @@ -2,7 +2,10 @@ import { Interface } from '@ethersproject/abi'; import { toHex } from '@metamask/controller-utils'; -import type { TransactionMeta } from '@metamask/transaction-controller'; +import type { + TransactionMeta, + TransactionParams, +} from '@metamask/transaction-controller'; import type { Hex } from '@metamask/utils'; import { createModuleLogger } from '@metamask/utils'; import { BigNumber } from 'bignumber.js'; @@ -275,7 +278,11 @@ async function getSingleQuote( log('Fetched relay quote', quote); - return await normalizeQuote(quote, request, fullRequest); + const quoteWithDeposits = request.useMoneyAccount + ? await injectMoneyAccountDepositSteps(quote, request, fullRequest) + : quote; + + return await normalizeQuote(quoteWithDeposits, request, fullRequest); } catch (error) { log('Error fetching relay quote', error); throw error; @@ -1015,6 +1022,85 @@ function getSubsidizedFeeAmountUsd(quote: RelayQuote): BigNumber { return isSubsidizedStablecoin ? amountFormatted : amountUsd; } +/** + * Fetches deposit transactions from the money account callback and injects + * them into the relay quote's steps so they are submitted alongside the relay + * transactions and included in gas estimation. + * + * For standard flows (`isPostQuote` false) the deposit step is prepended so it + * executes before the relay bridge transaction. For post-quote flows it is + * appended so it executes after. + * + * @param quote - Relay quote to mutate in place. + * @param request - Quote request, used to determine ordering and chain ID. + * @param fullRequest - Full quotes request, provides messenger and transaction ID. + */ +async function injectMoneyAccountDepositSteps( + quote: RelayQuote, + request: QuoteRequest, + fullRequest: PayStrategyGetQuotesRequest, +): Promise { + const { messenger, transaction } = fullRequest; + + const depositTxs = await messenger.call( + 'TransactionPayController:getMoneyAccountTransactions', + transaction.id, + ); + + if (!depositTxs.length) { + return quote; + } + + const depositStep = buildDepositStep(depositTxs, request.sourceChainId); + + const steps = request.isPostQuote + ? [...quote.steps, depositStep] + : [depositStep, ...quote.steps]; + + log('Injected money account deposit step', { + transactionId: transaction.id, + isPostQuote: request.isPostQuote, + depositTxCount: depositTxs.length, + }); + + return { ...quote, steps }; +} + +/** + * Converts an array of TransactionParams into a single RelayTransactionStep + * so they can be injected into a relay quote's steps array. + * + * @param txParams - Deposit transactions from the money account callback. + * @param sourceChainId - Hex chain ID of the source network. + * @returns A relay transaction step wrapping the deposit transactions. + */ +function buildDepositStep( + txParams: TransactionParams[], + sourceChainId: Hex, +): RelayTransactionStep { + const chainId = parseInt(sourceChainId, 16); + + return { + id: 'money-account-deposit', + kind: 'transaction', + requestId: 'money-account-deposit', + items: txParams.map((params) => ({ + check: { endpoint: '', method: 'GET' as const }, + status: 'incomplete' as const, + data: { + chainId, + data: (params.data as Hex) ?? '0x', + from: params.from as Hex, + gas: params.gas as string | undefined, + maxFeePerGas: (params.maxFeePerGas as string) ?? '0x0', + maxPriorityFeePerGas: (params.maxPriorityFeePerGas as string) ?? '0x0', + to: params.to as Hex, + value: params.value as string | undefined, + }, + })), + }; +} + function isStablecoin(chainId: string, tokenAddress: string): boolean { return Boolean( STABLECOINS[chainId as Hex]?.includes(tokenAddress.toLowerCase() as Hex), diff --git a/packages/transaction-pay-controller/src/tests/messenger-mock.ts b/packages/transaction-pay-controller/src/tests/messenger-mock.ts index 1931aa202a..ca049ce39c 100644 --- a/packages/transaction-pay-controller/src/tests/messenger-mock.ts +++ b/packages/transaction-pay-controller/src/tests/messenger-mock.ts @@ -29,6 +29,7 @@ import type { TransactionControllerUpdateTransactionAction } from '@metamask/tra import type { TransactionPayControllerMessenger } from '..'; import type { TransactionPayControllerGetDelegationTransactionAction, + TransactionPayControllerGetMoneyAccountTransactionsAction, TransactionPayControllerGetStrategyAction, TransactionPayControllerPolymarketGetDepositWalletAddressAction, TransactionPayControllerPolymarketSubmitDepositWalletBatchAction, @@ -120,6 +121,10 @@ export function getMessengerMock({ TransactionPayControllerGetDelegationTransactionAction['handler'] > = jest.fn(); + const getMoneyAccountTransactionsMock: jest.MockedFn< + TransactionPayControllerGetMoneyAccountTransactionsAction['handler'] + > = jest.fn().mockResolvedValue([]); + const polymarketGetDepositWalletAddressMock: jest.MockedFn< TransactionPayControllerPolymarketGetDepositWalletAddressAction['handler'] > = jest.fn(); @@ -255,6 +260,11 @@ export function getMessengerMock({ getDelegationTransactionMock, ); + messenger.registerActionHandler( + 'TransactionPayController:getMoneyAccountTransactions', + getMoneyAccountTransactionsMock, + ); + messenger.registerActionHandler( 'TransactionPayController:polymarketGetDepositWalletAddress', polymarketGetDepositWalletAddressMock, @@ -306,6 +316,7 @@ export function getMessengerMock({ getControllerStateMock, getCurrencyRateControllerStateMock, getDelegationTransactionMock, + getMoneyAccountTransactionsMock, getGasFeeControllerStateMock, getGasFeeTokensMock, getKeyringControllerStateMock, diff --git a/packages/transaction-pay-controller/src/types.ts b/packages/transaction-pay-controller/src/types.ts index b46019fc57..04927b98b3 100644 --- a/packages/transaction-pay-controller/src/types.ts +++ b/packages/transaction-pay-controller/src/types.ts @@ -54,6 +54,7 @@ import type { TransactionControllerStateChangeEvent, TransactionControllerUpdateTransactionAction, TransactionMeta, + TransactionParams, } from '@metamask/transaction-controller'; import type { Hex, Json } from '@metamask/utils'; import type { Draft } from 'immer'; @@ -148,6 +149,15 @@ export type TransactionConfig = { /** Callback to update transaction config. */ export type TransactionConfigCallback = (config: TransactionConfig) => void; +/** + * Callback invoked during quote execution when `useMoneyAccount` is true. + * Returns additional transactions to be submitted alongside the quote, + * ordered before or after the quote batch depending on `isPostQuote`. + */ +export type GetMoneyAccountTransactionsCallback = ( + transactionId: string, +) => Promise; + /** Callback to update fiat payment state. */ export type TransactionFiatPaymentCallback = ( fiatPayment: TransactionFiatPayment, @@ -199,6 +209,12 @@ export type TransactionPayControllerOptions = { /** Callbacks for the Polymarket relayer; required only for the Polymarket deposit-wallet flow. */ polymarket?: PolymarketCallbacks; + /** + * Optional callback invoked during quote execution when `useMoneyAccount` is true. + * Returns additional transactions to be submitted alongside the quote batch. + */ + getMoneyAccountTransactions?: GetMoneyAccountTransactionsCallback; + /** Initial state of the controller. */ state?: Partial; }; @@ -420,6 +436,9 @@ export type QuoteRequest = { /** Whether the source of funds is a Polymarket deposit wallet. */ isPolymarketDepositWallet?: boolean; + /** Whether the money account is the source of funds for this quote. */ + useMoneyAccount?: boolean; + /** * Optional address to receive refunds if the quote provider transaction fails. * When set, overrides the default refund recipient (EOA) in the quote diff --git a/packages/transaction-pay-controller/src/utils/quotes.ts b/packages/transaction-pay-controller/src/utils/quotes.ts index ba2ec2afd7..0ae0ccc825 100644 --- a/packages/transaction-pay-controller/src/utils/quotes.ts +++ b/packages/transaction-pay-controller/src/utils/quotes.ts @@ -86,6 +86,7 @@ export async function updateQuotes( isPostQuote, isHyperliquidSource, isPolymarketDepositWallet, + useMoneyAccount, paymentToken: originalPaymentToken, refundTo, sourceAmounts, @@ -122,6 +123,7 @@ export async function updateQuotes( isPostQuote, isHyperliquidSource, isPolymarketDepositWallet, + useMoneyAccount, paymentToken, refundTo, sourceAmounts, @@ -339,6 +341,7 @@ function buildQuoteRequests({ isPostQuote, isHyperliquidSource, isPolymarketDepositWallet, + useMoneyAccount, paymentToken, refundTo, sourceAmounts, @@ -350,6 +353,7 @@ function buildQuoteRequests({ isPostQuote?: boolean; isHyperliquidSource?: boolean; isPolymarketDepositWallet?: boolean; + useMoneyAccount?: boolean; paymentToken: TransactionPaymentToken | undefined; refundTo?: Hex; sourceAmounts: TransactionPaySourceAmount[] | undefined; @@ -366,6 +370,7 @@ function buildQuoteRequests({ isMaxAmount, isHyperliquidSource, isPolymarketDepositWallet, + useMoneyAccount, destinationToken: paymentToken, refundTo, sourceAmounts, @@ -382,6 +387,7 @@ function buildQuoteRequests({ return { from, isMaxAmount, + useMoneyAccount, sourceBalanceRaw: paymentToken.balanceRaw, sourceTokenAmount: sourceAmount.sourceAmountRaw, sourceChainId: paymentToken.chainId, @@ -420,6 +426,7 @@ function buildPostQuoteRequests({ isMaxAmount, isHyperliquidSource, isPolymarketDepositWallet, + useMoneyAccount, destinationToken, refundTo, sourceAmounts, @@ -429,6 +436,7 @@ function buildPostQuoteRequests({ isMaxAmount: boolean; isHyperliquidSource?: boolean; isPolymarketDepositWallet?: boolean; + useMoneyAccount?: boolean; destinationToken: TransactionPaymentToken; refundTo?: Hex; sourceAmounts: TransactionPaySourceAmount[] | undefined; @@ -459,6 +467,7 @@ function buildPostQuoteRequests({ isPostQuote: true, isHyperliquidSource, isPolymarketDepositWallet, + useMoneyAccount, refundTo, sourceBalanceRaw: sourceAmount.sourceBalanceRaw, sourceTokenAmount: sourceAmount.sourceAmountRaw, From 886efa9e41c767065eb4ae49f4c80d87129bd09b Mon Sep 17 00:00:00 2001 From: Jyoti Puri Date: Fri, 22 May 2026 19:28:24 +0530 Subject: [PATCH 04/35] update --- packages/transaction-controller/src/types.ts | 4 +-- ...actionPayController-method-action-types.ts | 14 +++++----- .../src/TransactionPayController.test.ts | 12 ++++----- .../src/TransactionPayController.ts | 26 +++++++++---------- .../src/strategy/relay/relay-quotes.test.ts | 22 ++++++++-------- .../src/strategy/relay/relay-quotes.ts | 19 +++++++------- .../src/tests/messenger-mock.ts | 12 ++++----- .../transaction-pay-controller/src/types.ts | 8 +++--- .../src/utils/quotes.ts | 2 ++ 9 files changed, 61 insertions(+), 58 deletions(-) diff --git a/packages/transaction-controller/src/types.ts b/packages/transaction-controller/src/types.ts index 6694b67dba..162cc7ab5a 100644 --- a/packages/transaction-controller/src/types.ts +++ b/packages/transaction-controller/src/types.ts @@ -791,12 +791,12 @@ export enum TransactionType { lendingWithdraw = 'lendingWithdraw', /** - * A transaction that deposits funds into a money account. + * A transaction that deposits funds when paymentOverride is set. */ moneyAccountDeposit = 'moneyAccountDeposit', /** - * A transaction that withdraws funds from a money account. + * A transaction that withdraws funds when paymentOverride is set. */ moneyAccountWithdraw = 'moneyAccountWithdraw', 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 41855f805f..45671cd85a 100644 --- a/packages/transaction-pay-controller/src/TransactionPayController-method-action-types.ts +++ b/packages/transaction-pay-controller/src/TransactionPayController-method-action-types.ts @@ -50,19 +50,19 @@ export type TransactionPayControllerUpdateFiatPaymentAction = { }; /** - * Returns additional transactions for the money account flow. + * Returns additional transactions for the paymentOverride flow. * * Delegates to the client-supplied callback. Called during quote execution - * when `paymentOverride === PaymentOverride.MoneyAccount` on the transaction. Returns an empty array when no callback + * when paymentOverride is true. Returns an empty array when no callback * is configured. * - * @param args - The arguments forwarded to the {@link GetMoneyAccountTransactionsCallback}, + * @param args - The arguments forwarded to the {@link GetPaymentOverrideDataCallback}, * containing the transaction ID. * @returns A promise resolving to the additional transactions array. */ -export type TransactionPayControllerGetMoneyAccountTransactionsAction = { - type: `TransactionPayController:getMoneyAccountTransactions`; - handler: TransactionPayController['getMoneyAccountTransactions']; +export type TransactionPayControllerGetPaymentOverrideDataAction = { + type: `TransactionPayController:getPaymentOverrideData`; + handler: TransactionPayController['getPaymentOverrideData']; }; /** @@ -129,7 +129,7 @@ export type TransactionPayControllerMethodActions = | TransactionPayControllerUpdatePaymentTokenAction | TransactionPayControllerUpdateFiatPaymentAction | TransactionPayControllerGetDelegationTransactionAction - | TransactionPayControllerGetMoneyAccountTransactionsAction + | TransactionPayControllerGetPaymentOverrideDataAction | TransactionPayControllerGetStrategyAction | TransactionPayControllerPolymarketGetDepositWalletAddressAction | TransactionPayControllerPolymarketSubmitDepositWalletBatchAction; diff --git a/packages/transaction-pay-controller/src/TransactionPayController.test.ts b/packages/transaction-pay-controller/src/TransactionPayController.test.ts index f28696444a..e6a572dac7 100644 --- a/packages/transaction-pay-controller/src/TransactionPayController.test.ts +++ b/packages/transaction-pay-controller/src/TransactionPayController.test.ts @@ -469,25 +469,25 @@ describe('TransactionPayController', () => { }); }); - describe('getMoneyAccountTransactions', () => { + describe('getPaymentOverrideData', () => { it('delegates to the callback', async () => { const txMock = { from: '0xabc', to: '0xdef' }; - const getMoneyAccountTransactionsMock = jest + const getPaymentOverrideDataMock = jest .fn() .mockResolvedValue([txMock]); new TransactionPayController({ getDelegationTransaction: jest.fn(), - getMoneyAccountTransactions: getMoneyAccountTransactionsMock, + getPaymentOverrideData: getPaymentOverrideDataMock, messenger, }); const result = await messenger.call( - 'TransactionPayController:getMoneyAccountTransactions', + 'TransactionPayController:getPaymentOverrideData', TRANSACTION_ID_MOCK, ); - expect(getMoneyAccountTransactionsMock).toHaveBeenCalledWith( + expect(getPaymentOverrideDataMock).toHaveBeenCalledWith( TRANSACTION_ID_MOCK, ); expect(result).toStrictEqual([txMock]); @@ -500,7 +500,7 @@ describe('TransactionPayController', () => { }); const result = await messenger.call( - 'TransactionPayController:getMoneyAccountTransactions', + 'TransactionPayController:getPaymentOverrideData', TRANSACTION_ID_MOCK, ); diff --git a/packages/transaction-pay-controller/src/TransactionPayController.ts b/packages/transaction-pay-controller/src/TransactionPayController.ts index 4c9ae07314..b0d0eafb20 100644 --- a/packages/transaction-pay-controller/src/TransactionPayController.ts +++ b/packages/transaction-pay-controller/src/TransactionPayController.ts @@ -15,7 +15,7 @@ import { QuoteRefresher } from './helpers/QuoteRefresher'; import { deriveFiatAssetForFiatPayment } from './strategy/fiat/utils'; import type { GetDelegationTransactionCallback, - GetMoneyAccountTransactionsCallback, + GetPaymentOverrideDataCallback, PolymarketCallbacks, TransactionConfigCallback, TransactionData, @@ -37,7 +37,7 @@ import { const MESSENGER_EXPOSED_METHODS = [ 'getDelegationTransaction', - 'getMoneyAccountTransactions', + 'getPaymentOverrideData', 'getStrategy', 'polymarketGetDepositWalletAddress', 'polymarketSubmitDepositWalletBatch', @@ -66,7 +66,7 @@ export class TransactionPayController extends BaseController< > { readonly #getDelegationTransaction: GetDelegationTransactionCallback; - readonly #getMoneyAccountTransactions?: GetMoneyAccountTransactionsCallback; + readonly #getPaymentOverrideData?: GetPaymentOverrideDataCallback; readonly #getStrategy?: ( transaction: TransactionMeta, @@ -80,7 +80,7 @@ export class TransactionPayController extends BaseController< constructor({ getDelegationTransaction, - getMoneyAccountTransactions, + getPaymentOverrideData, getStrategy, getStrategies, messenger, @@ -95,7 +95,7 @@ export class TransactionPayController extends BaseController< }); this.#getDelegationTransaction = getDelegationTransaction; - this.#getMoneyAccountTransactions = getMoneyAccountTransactions; + this.#getPaymentOverrideData = getPaymentOverrideData; this.#getStrategy = getStrategy; this.#getStrategies = getStrategies; this.#polymarket = polymarket; @@ -224,20 +224,20 @@ export class TransactionPayController extends BaseController< } /** - * Returns additional transactions for the money account flow. + * Returns additional transactions for the paymentOverride flow. * - * Delegates to the client-supplied {@link GetMoneyAccountTransactionsCallback}. - * Called during quote execution when `paymentOverride === PaymentOverride.MoneyAccount` on the transaction. + * Delegates to the client-supplied {@link GetPaymentOverrideDataCallback}. + * Called during quote execution when `paymentOverride` is defined on the transaction. * Returns an empty array when no callback is configured. * - * @param args - The arguments forwarded to the {@link GetMoneyAccountTransactionsCallback}, + * @param args - The arguments forwarded to the {@link GetPaymentOverrideDataCallback}, * containing the transaction ID. * @returns A promise resolving to the additional transactions array. */ - getMoneyAccountTransactions( - ...args: Parameters - ): ReturnType { - return this.#getMoneyAccountTransactions?.(...args) ?? Promise.resolve([]); + getPaymentOverrideData( + ...args: Parameters + ): ReturnType { + return this.#getPaymentOverrideData?.(...args) ?? Promise.resolve([]); } /** diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts index c2a2f1049f..f2da52c47a 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts @@ -184,7 +184,7 @@ describe('Relay Quotes Utils', () => { getDelegationTransactionMock, getGasFeeTokensMock, getKeyringControllerStateMock, - getMoneyAccountTransactionsMock, + getPaymentOverrideDataMock, getRemoteFeatureFlagControllerStateMock, polymarketGetDepositWalletAddressMock, } = getMessengerMock(); @@ -3377,7 +3377,7 @@ describe('Relay Quotes Utils', () => { }); }); - describe('money account deposit step injection (paymentOverride === PaymentOverride.MoneyAccount)', () => { + describe('paymentOverride deposit step injection (paymentOverride defined)', () => { const DEPOSIT_TX_MOCK = { from: FROM_MOCK, to: '0xmoneyaccount' as Hex, @@ -3401,7 +3401,7 @@ describe('Relay Quotes Utils', () => { }); it('prepends deposit step before relay steps for standard (non-post-quote) flow', async () => { - getMoneyAccountTransactionsMock.mockResolvedValue([DEPOSIT_TX_MOCK]); + getPaymentOverrideDataMock.mockResolvedValue([DEPOSIT_TX_MOCK]); const [quote] = await getRelayQuotes({ accountSupports7702: true, @@ -3411,7 +3411,7 @@ describe('Relay Quotes Utils', () => { }); const txSteps = quote.original.steps.filter( - (s): s is RelayTransactionStep => s.kind === 'transaction', + (step): step is RelayTransactionStep => step.kind === 'transaction', ); expect(txSteps[0].id).toBe('money-account-deposit'); expect(txSteps[0].items[0].data.to).toBe(DEPOSIT_TX_MOCK.to); @@ -3419,7 +3419,7 @@ describe('Relay Quotes Utils', () => { }); it('appends deposit step after relay steps for post-quote flow', async () => { - getMoneyAccountTransactionsMock.mockResolvedValue([DEPOSIT_TX_MOCK]); + getPaymentOverrideDataMock.mockResolvedValue([DEPOSIT_TX_MOCK]); const [quote] = await getRelayQuotes({ accountSupports7702: true, @@ -3431,7 +3431,7 @@ describe('Relay Quotes Utils', () => { }); const txSteps = quote.original.steps.filter( - (s): s is RelayTransactionStep => s.kind === 'transaction', + (step): step is RelayTransactionStep => step.kind === 'transaction', ); expect(txSteps[0].id).toBe(STEP_MOCK.id); expect(txSteps[1].id).toBe('money-account-deposit'); @@ -3446,13 +3446,13 @@ describe('Relay Quotes Utils', () => { }); expect( - quote.original.steps.every((s) => s.id !== 'money-account-deposit'), + quote.original.steps.every((step) => step.id !== 'money-account-deposit'), ).toBe(true); - expect(getMoneyAccountTransactionsMock).not.toHaveBeenCalled(); + expect(getPaymentOverrideDataMock).not.toHaveBeenCalled(); }); it('does not inject when callback returns empty array', async () => { - getMoneyAccountTransactionsMock.mockResolvedValue([]); + getPaymentOverrideDataMock.mockResolvedValue([]); const [quote] = await getRelayQuotes({ accountSupports7702: true, @@ -3466,7 +3466,7 @@ describe('Relay Quotes Utils', () => { }); it('uses sourceChainId to set chainId on deposit step items', async () => { - getMoneyAccountTransactionsMock.mockResolvedValue([DEPOSIT_TX_MOCK]); + getPaymentOverrideDataMock.mockResolvedValue([DEPOSIT_TX_MOCK]); const [quote] = await getRelayQuotes({ accountSupports7702: true, @@ -3476,7 +3476,7 @@ describe('Relay Quotes Utils', () => { }); const depositStep = quote.original.steps.find( - (s) => s.id === 'money-account-deposit', + (step) => step.id === 'money-account-deposit', ) as RelayTransactionStep; // QUOTE_REQUEST_MOCK.sourceChainId is '0x1' → chainId 1 expect(depositStep.items[0].data.chainId).toBe(1); diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts index b8a5a87b04..72cd41cd70 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts @@ -278,8 +278,8 @@ async function getSingleQuote( log('Fetched relay quote', quote); - const quoteWithDeposits = request.paymentOverride === PaymentOverride.MoneyAccount - ? await injectMoneyAccountDepositSteps(quote, request, fullRequest) + const quoteWithDeposits = request.paymentOverride + ? await injectPaymentOverrideDepositSteps(quote, request, fullRequest) : quote; return await normalizeQuote(quoteWithDeposits, request, fullRequest); @@ -1023,7 +1023,7 @@ function getSubsidizedFeeAmountUsd(quote: RelayQuote): BigNumber { } /** - * Fetches deposit transactions from the money account callback and injects + * Fetches deposit transactions from the paymentOverride callback and injects * them into the relay quote's steps so they are submitted alongside the relay * transactions and included in gas estimation. * @@ -1034,8 +1034,9 @@ function getSubsidizedFeeAmountUsd(quote: RelayQuote): BigNumber { * @param quote - Relay quote to mutate in place. * @param request - Quote request, used to determine ordering and chain ID. * @param fullRequest - Full quotes request, provides messenger and transaction ID. + * @returns The relay quote with deposit steps injected, or the original quote if no deposits are returned. */ -async function injectMoneyAccountDepositSteps( +async function injectPaymentOverrideDepositSteps( quote: RelayQuote, request: QuoteRequest, fullRequest: PayStrategyGetQuotesRequest, @@ -1043,7 +1044,7 @@ async function injectMoneyAccountDepositSteps( const { messenger, transaction } = fullRequest; const depositTxs = await messenger.call( - 'TransactionPayController:getMoneyAccountTransactions', + 'TransactionPayController:getPaymentOverrideData', transaction.id, ); @@ -1057,7 +1058,7 @@ async function injectMoneyAccountDepositSteps( ? [...quote.steps, depositStep] : [depositStep, ...quote.steps]; - log('Injected money account deposit step', { + log('Injected paymentOverride deposit step', { transactionId: transaction.id, isPostQuote: request.isPostQuote, depositTxCount: depositTxs.length, @@ -1070,7 +1071,7 @@ async function injectMoneyAccountDepositSteps( * Converts an array of TransactionParams into a single RelayTransactionStep * so they can be injected into a relay quote's steps array. * - * @param txParams - Deposit transactions from the money account callback. + * @param txParams - Deposit transactions from the paymentOverride callback. * @param sourceChainId - Hex chain ID of the source network. * @returns A relay transaction step wrapping the deposit transactions. */ @@ -1091,11 +1092,11 @@ function buildDepositStep( chainId, data: (params.data as Hex) ?? '0x', from: params.from as Hex, - gas: params.gas as string | undefined, + gas: params.gas, maxFeePerGas: (params.maxFeePerGas as string) ?? '0x0', maxPriorityFeePerGas: (params.maxPriorityFeePerGas as string) ?? '0x0', to: params.to as Hex, - value: params.value as string | undefined, + value: params.value, }, })), }; diff --git a/packages/transaction-pay-controller/src/tests/messenger-mock.ts b/packages/transaction-pay-controller/src/tests/messenger-mock.ts index ca049ce39c..0a0892b0c1 100644 --- a/packages/transaction-pay-controller/src/tests/messenger-mock.ts +++ b/packages/transaction-pay-controller/src/tests/messenger-mock.ts @@ -29,7 +29,7 @@ import type { TransactionControllerUpdateTransactionAction } from '@metamask/tra import type { TransactionPayControllerMessenger } from '..'; import type { TransactionPayControllerGetDelegationTransactionAction, - TransactionPayControllerGetMoneyAccountTransactionsAction, + TransactionPayControllerGetPaymentOverrideDataAction, TransactionPayControllerGetStrategyAction, TransactionPayControllerPolymarketGetDepositWalletAddressAction, TransactionPayControllerPolymarketSubmitDepositWalletBatchAction, @@ -121,8 +121,8 @@ export function getMessengerMock({ TransactionPayControllerGetDelegationTransactionAction['handler'] > = jest.fn(); - const getMoneyAccountTransactionsMock: jest.MockedFn< - TransactionPayControllerGetMoneyAccountTransactionsAction['handler'] + const getPaymentOverrideDataMock: jest.MockedFn< + TransactionPayControllerGetPaymentOverrideDataAction['handler'] > = jest.fn().mockResolvedValue([]); const polymarketGetDepositWalletAddressMock: jest.MockedFn< @@ -261,8 +261,8 @@ export function getMessengerMock({ ); messenger.registerActionHandler( - 'TransactionPayController:getMoneyAccountTransactions', - getMoneyAccountTransactionsMock, + 'TransactionPayController:getPaymentOverrideData', + getPaymentOverrideDataMock, ); messenger.registerActionHandler( @@ -316,7 +316,7 @@ export function getMessengerMock({ getControllerStateMock, getCurrencyRateControllerStateMock, getDelegationTransactionMock, - getMoneyAccountTransactionsMock, + getPaymentOverrideDataMock, getGasFeeControllerStateMock, getGasFeeTokensMock, getKeyringControllerStateMock, diff --git a/packages/transaction-pay-controller/src/types.ts b/packages/transaction-pay-controller/src/types.ts index 0c140ec4f1..119a005a80 100644 --- a/packages/transaction-pay-controller/src/types.ts +++ b/packages/transaction-pay-controller/src/types.ts @@ -154,11 +154,11 @@ export type TransactionConfig = { export type TransactionConfigCallback = (config: TransactionConfig) => void; /** - * Callback invoked during quote execution when `paymentOverride === PaymentOverride.MoneyAccount`. + * Callback invoked during quote execution when `paymentOverride` is defined. * Returns additional transactions to be submitted alongside the quote, * ordered before or after the quote batch depending on `isPostQuote`. */ -export type GetMoneyAccountTransactionsCallback = ( +export type GetPaymentOverrideDataCallback = ( transactionId: string, ) => Promise; @@ -214,10 +214,10 @@ export type TransactionPayControllerOptions = { polymarket?: PolymarketCallbacks; /** - * Optional callback invoked during quote execution when `paymentOverride === PaymentOverride.MoneyAccount`. + * Optional callback invoked during quote execution when `paymentOverride` is defined. * Returns additional transactions to be submitted alongside the quote batch. */ - getMoneyAccountTransactions?: GetMoneyAccountTransactionsCallback; + getPaymentOverrideData?: GetPaymentOverrideDataCallback; /** Initial state of the controller. */ state?: Partial; diff --git a/packages/transaction-pay-controller/src/utils/quotes.ts b/packages/transaction-pay-controller/src/utils/quotes.ts index 867d55cee6..e332b63285 100644 --- a/packages/transaction-pay-controller/src/utils/quotes.ts +++ b/packages/transaction-pay-controller/src/utils/quotes.ts @@ -328,6 +328,7 @@ function clearControllerIfCurrent( * @param request.isHyperliquidSource - Whether the source of funds is HyperLiquid. * @param request.isPolymarketDepositWallet - Whether the source of funds is a Polymarket deposit wallet. * @param request.isPostQuote - Whether this is a post-quote flow. + * @param request.paymentOverride - Optional payment override type for the transaction. * @param request.paymentToken - Payment token (source for standard flows, destination for post-quote). * @param request.refundTo - Optional address to receive refunds if the Relay transaction fails. * @param request.sourceAmounts - Source amounts for the transaction. @@ -415,6 +416,7 @@ function buildQuoteRequests({ * @param request.isMaxAmount - Whether the transaction is a maximum amount transaction. * @param request.isHyperliquidSource - Whether the source of funds is HyperLiquid. * @param request.isPolymarketDepositWallet - Whether the source of funds is a Polymarket deposit wallet. + * @param request.paymentOverride - Optional payment override type for the transaction. * @param request.destinationToken - Destination token (paymentToken in post-quote mode). * @param request.refundTo - Optional address to receive refunds if the Relay transaction fails. * @param request.sourceAmounts - Source amounts for the transaction (includes source token info). From c6f336a2498653e637294b5c495de13cb43d9a61 Mon Sep 17 00:00:00 2001 From: Jyoti Puri Date: Fri, 22 May 2026 20:23:51 +0530 Subject: [PATCH 05/35] update --- packages/transaction-controller/src/types.ts | 4 ++-- .../src/strategy/relay/relay-quotes.test.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/transaction-controller/src/types.ts b/packages/transaction-controller/src/types.ts index 162cc7ab5a..6694b67dba 100644 --- a/packages/transaction-controller/src/types.ts +++ b/packages/transaction-controller/src/types.ts @@ -791,12 +791,12 @@ export enum TransactionType { lendingWithdraw = 'lendingWithdraw', /** - * A transaction that deposits funds when paymentOverride is set. + * A transaction that deposits funds into a money account. */ moneyAccountDeposit = 'moneyAccountDeposit', /** - * A transaction that withdraws funds when paymentOverride is set. + * A transaction that withdraws funds from a money account. */ moneyAccountWithdraw = 'moneyAccountWithdraw', diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts index f2da52c47a..ca45c8bd79 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts @@ -3437,7 +3437,7 @@ describe('Relay Quotes Utils', () => { expect(txSteps[1].id).toBe('money-account-deposit'); }); - it('does not inject when paymentOverride is not PaymentOverride.MoneyAccount', async () => { + it('does not inject when paymentOverride is not defined', async () => { const [quote] = await getRelayQuotes({ accountSupports7702: true, messenger, From 3c024f34ebb12aa762ea88392ff727e0be1d09b3 Mon Sep 17 00:00:00 2001 From: Jyoti Puri Date: Fri, 22 May 2026 20:29:08 +0530 Subject: [PATCH 06/35] update --- packages/transaction-pay-controller/CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/transaction-pay-controller/CHANGELOG.md b/packages/transaction-pay-controller/CHANGELOG.md index a8c6916269..3298a0779b 100644 --- a/packages/transaction-pay-controller/CHANGELOG.md +++ b/packages/transaction-pay-controller/CHANGELOG.md @@ -9,7 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Add `getMoneyAccountTransactions` callback to `TransactionPayControllerOptions`; when `paymentOverride` is true on a transaction, the publish hook calls this callback and passes the resulting `additionalTransactions` to the strategy's `execute` request, ordered before or after the quote batch based on `isPostQuote` ([#8858](https://github.com/MetaMask/core/pull/8858)) +- Add `getPaymentOverrideData` callback (type `GetPaymentOverrideDataCallback`) to `TransactionPayControllerOptions`; when `paymentOverride` is defined on a transaction, this callback is invoked during quote execution and the resulting transactions are injected into the relay quote steps, ordered before or after the quote batch based on `isPostQuote` ([#8858](https://github.com/MetaMask/core/pull/8858)) + - Exposed as the `TransactionPayController:getPaymentOverrideData` messenger action (type `TransactionPayControllerGetPaymentOverrideDataAction`) ## [22.7.0] From b270c54dd98967ddb84f41b16f5119c1ddb5c1dc Mon Sep 17 00:00:00 2001 From: Jyoti Puri Date: Sat, 23 May 2026 02:08:11 +0530 Subject: [PATCH 07/35] update --- .../src/strategy/relay/relay-quotes.test.ts | 37 +++++++++---------- .../src/strategy/relay/relay-quotes.ts | 37 +++++++++---------- .../src/strategy/relay/relay-submit.ts | 10 +---- .../src/strategy/relay/types.ts | 4 +- 4 files changed, 37 insertions(+), 51 deletions(-) diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts index ca45c8bd79..a631b67ab9 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts @@ -3377,15 +3377,12 @@ describe('Relay Quotes Utils', () => { }); }); - describe('paymentOverride deposit step injection (paymentOverride defined)', () => { - const DEPOSIT_TX_MOCK = { + describe('paymentOverride step injection (paymentOverride defined)', () => { + const PAYMENT_OVERRIDE_TX_MOCK = { from: FROM_MOCK, - to: '0xmoneyaccount' as Hex, - data: '0xdeposit' as Hex, + to: '0xpaymentoverride' as Hex, + data: '0xpaymentoverride' as Hex, value: '0x0', - gas: '30000', - maxFeePerGas: '1000000000', - maxPriorityFeePerGas: '2000000000', }; beforeEach(() => { @@ -3400,8 +3397,8 @@ describe('Relay Quotes Utils', () => { }); }); - it('prepends deposit step before relay steps for standard (non-post-quote) flow', async () => { - getPaymentOverrideDataMock.mockResolvedValue([DEPOSIT_TX_MOCK]); + it('prepends paymentOverride step before relay steps for standard (non-post-quote) flow', async () => { + getPaymentOverrideDataMock.mockResolvedValue([PAYMENT_OVERRIDE_TX_MOCK]); const [quote] = await getRelayQuotes({ accountSupports7702: true, @@ -3413,13 +3410,13 @@ describe('Relay Quotes Utils', () => { const txSteps = quote.original.steps.filter( (step): step is RelayTransactionStep => step.kind === 'transaction', ); - expect(txSteps[0].id).toBe('money-account-deposit'); - expect(txSteps[0].items[0].data.to).toBe(DEPOSIT_TX_MOCK.to); + expect(txSteps[0].id).toBe('payment-override'); + expect(txSteps[0].items[0].data.to).toBe(PAYMENT_OVERRIDE_TX_MOCK.to); expect(txSteps[1].id).toBe(STEP_MOCK.id); }); - it('appends deposit step after relay steps for post-quote flow', async () => { - getPaymentOverrideDataMock.mockResolvedValue([DEPOSIT_TX_MOCK]); + it('appends paymentOverride step after relay steps for post-quote flow', async () => { + getPaymentOverrideDataMock.mockResolvedValue([PAYMENT_OVERRIDE_TX_MOCK]); const [quote] = await getRelayQuotes({ accountSupports7702: true, @@ -3434,7 +3431,7 @@ describe('Relay Quotes Utils', () => { (step): step is RelayTransactionStep => step.kind === 'transaction', ); expect(txSteps[0].id).toBe(STEP_MOCK.id); - expect(txSteps[1].id).toBe('money-account-deposit'); + expect(txSteps[1].id).toBe('payment-override'); }); it('does not inject when paymentOverride is not defined', async () => { @@ -3446,7 +3443,7 @@ describe('Relay Quotes Utils', () => { }); expect( - quote.original.steps.every((step) => step.id !== 'money-account-deposit'), + quote.original.steps.every((step) => step.id !== 'payment-override'), ).toBe(true); expect(getPaymentOverrideDataMock).not.toHaveBeenCalled(); }); @@ -3465,8 +3462,8 @@ describe('Relay Quotes Utils', () => { expect(quote.original.steps[0].id).toBe(STEP_MOCK.id); }); - it('uses sourceChainId to set chainId on deposit step items', async () => { - getPaymentOverrideDataMock.mockResolvedValue([DEPOSIT_TX_MOCK]); + it('uses sourceChainId to set chainId on paymentOverride step items', async () => { + getPaymentOverrideDataMock.mockResolvedValue([PAYMENT_OVERRIDE_TX_MOCK]); const [quote] = await getRelayQuotes({ accountSupports7702: true, @@ -3475,11 +3472,11 @@ describe('Relay Quotes Utils', () => { transaction: TRANSACTION_META_MOCK, }); - const depositStep = quote.original.steps.find( - (step) => step.id === 'money-account-deposit', + const overrideStep = quote.original.steps.find( + (step) => step.id === 'payment-override', ) as RelayTransactionStep; // QUOTE_REQUEST_MOCK.sourceChainId is '0x1' → chainId 1 - expect(depositStep.items[0].data.chainId).toBe(1); + expect(overrideStep.items[0].data.chainId).toBe(1); }); }); diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts index 72cd41cd70..f158e5da9a 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts @@ -279,7 +279,7 @@ async function getSingleQuote( log('Fetched relay quote', quote); const quoteWithDeposits = request.paymentOverride - ? await injectPaymentOverrideDepositSteps(quote, request, fullRequest) + ? await injectPaymentOverrideSteps(quote, request, fullRequest) : quote; return await normalizeQuote(quoteWithDeposits, request, fullRequest); @@ -1023,45 +1023,45 @@ function getSubsidizedFeeAmountUsd(quote: RelayQuote): BigNumber { } /** - * Fetches deposit transactions from the paymentOverride callback and injects + * Fetches transactions from the paymentOverride callback and injects * them into the relay quote's steps so they are submitted alongside the relay * transactions and included in gas estimation. * - * For standard flows (`isPostQuote` false) the deposit step is prepended so it + * For standard flows (`isPostQuote` false) the step is prepended so it * executes before the relay bridge transaction. For post-quote flows it is * appended so it executes after. * * @param quote - Relay quote to mutate in place. * @param request - Quote request, used to determine ordering and chain ID. * @param fullRequest - Full quotes request, provides messenger and transaction ID. - * @returns The relay quote with deposit steps injected, or the original quote if no deposits are returned. + * @returns The relay quote with paymentOverride steps injected, or the original quote if the callback returns no transactions. */ -async function injectPaymentOverrideDepositSteps( +async function injectPaymentOverrideSteps( quote: RelayQuote, request: QuoteRequest, fullRequest: PayStrategyGetQuotesRequest, ): Promise { const { messenger, transaction } = fullRequest; - const depositTxs = await messenger.call( + const overrideTxs = await messenger.call( 'TransactionPayController:getPaymentOverrideData', transaction.id, ); - if (!depositTxs.length) { + if (!overrideTxs.length) { return quote; } - const depositStep = buildDepositStep(depositTxs, request.sourceChainId); + const overrideStep = buildPaymentOverrideStep(overrideTxs, request.sourceChainId); const steps = request.isPostQuote - ? [...quote.steps, depositStep] - : [depositStep, ...quote.steps]; + ? [...quote.steps, overrideStep] + : [overrideStep, ...quote.steps]; - log('Injected paymentOverride deposit step', { + log('Injected paymentOverride step', { transactionId: transaction.id, isPostQuote: request.isPostQuote, - depositTxCount: depositTxs.length, + txCount: overrideTxs.length, }); return { ...quote, steps }; @@ -1071,20 +1071,20 @@ async function injectPaymentOverrideDepositSteps( * Converts an array of TransactionParams into a single RelayTransactionStep * so they can be injected into a relay quote's steps array. * - * @param txParams - Deposit transactions from the paymentOverride callback. + * @param txParams - Transactions from the paymentOverride callback. * @param sourceChainId - Hex chain ID of the source network. - * @returns A relay transaction step wrapping the deposit transactions. + * @returns A relay transaction step wrapping the paymentOverride transactions. */ -function buildDepositStep( +function buildPaymentOverrideStep( txParams: TransactionParams[], sourceChainId: Hex, ): RelayTransactionStep { const chainId = parseInt(sourceChainId, 16); return { - id: 'money-account-deposit', + id: 'payment-override', kind: 'transaction', - requestId: 'money-account-deposit', + requestId: 'payment-override', items: txParams.map((params) => ({ check: { endpoint: '', method: 'GET' as const }, status: 'incomplete' as const, @@ -1092,9 +1092,6 @@ function buildDepositStep( chainId, data: (params.data as Hex) ?? '0x', from: params.from as Hex, - gas: params.gas, - maxFeePerGas: (params.maxFeePerGas as string) ?? '0x0', - maxPriorityFeePerGas: (params.maxPriorityFeePerGas as string) ?? '0x0', to: params.to as Hex, value: params.value, }, diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts index b6f0fc5431..346f96dab2 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts @@ -16,7 +16,6 @@ import type { TransactionPayQuote, } from '../../types'; import { - getFeatureFlags, getRelayPollingInterval, getRelayPollingTimeout, } from '../../utils/feature-flags'; @@ -266,21 +265,14 @@ async function waitForRelayCompletion( * Normalize the parameters from a relay quote step to match TransactionParams. * * @param params - Parameters from a relay quote step. - * @param messenger - Controller messenger. * @returns Normalized transaction parameters. */ function normalizeParams( params: RelayTransactionStep['items'][0]['data'], - messenger: TransactionPayControllerMessenger, ): TransactionParams { - const featureFlags = getFeatureFlags(messenger); - return { data: params.data, from: params.from, - gas: toHex(params.gas ?? featureFlags.relayFallbackGas.max), - maxFeePerGas: toHex(params.maxFeePerGas), - maxPriorityFeePerGas: toHex(params.maxPriorityFeePerGas), to: params.to, value: toHex(params.value ?? '0'), }; @@ -385,7 +377,7 @@ async function submitTransactions( } const normalizedParams = params.map((singleParams) => - normalizeParams(singleParams, messenger), + normalizeParams(singleParams), ); // For post-quote flows, prepend the original transaction so it gets diff --git a/packages/transaction-pay-controller/src/strategy/relay/types.ts b/packages/transaction-pay-controller/src/strategy/relay/types.ts index 442097d2e0..9b5d089e55 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/types.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/types.ts @@ -96,8 +96,8 @@ export type RelayTransactionStep = { data: Hex; from: Hex; gas?: string; - maxFeePerGas: string; - maxPriorityFeePerGas: string; + maxFeePerGas?: string; + maxPriorityFeePerGas?: string; to: Hex; value?: string; }; From 0abfc0041a738128aadd51ba8e61508449bf1d66 Mon Sep 17 00:00:00 2001 From: Jyoti Puri Date: Sat, 23 May 2026 02:11:36 +0530 Subject: [PATCH 08/35] update --- .../src/strategy/relay/relay-quotes.test.ts | 2 ++ .../src/strategy/relay/relay-quotes.ts | 2 ++ .../src/strategy/relay/relay-submit.ts | 10 +++++++++- .../src/strategy/relay/types.ts | 4 ++-- 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts index a631b67ab9..ab8ae47cf2 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts @@ -3383,6 +3383,8 @@ describe('Relay Quotes Utils', () => { to: '0xpaymentoverride' as Hex, data: '0xpaymentoverride' as Hex, value: '0x0', + maxFeePerGas: '1000000000', + maxPriorityFeePerGas: '2000000000', }; beforeEach(() => { diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts index f158e5da9a..5a72b981b8 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts @@ -1092,6 +1092,8 @@ function buildPaymentOverrideStep( chainId, data: (params.data as Hex) ?? '0x', from: params.from as Hex, + maxFeePerGas: params.maxFeePerGas as string, + maxPriorityFeePerGas: params.maxPriorityFeePerGas as string, to: params.to as Hex, value: params.value, }, diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts index 346f96dab2..b6f0fc5431 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts @@ -16,6 +16,7 @@ import type { TransactionPayQuote, } from '../../types'; import { + getFeatureFlags, getRelayPollingInterval, getRelayPollingTimeout, } from '../../utils/feature-flags'; @@ -265,14 +266,21 @@ async function waitForRelayCompletion( * Normalize the parameters from a relay quote step to match TransactionParams. * * @param params - Parameters from a relay quote step. + * @param messenger - Controller messenger. * @returns Normalized transaction parameters. */ function normalizeParams( params: RelayTransactionStep['items'][0]['data'], + messenger: TransactionPayControllerMessenger, ): TransactionParams { + const featureFlags = getFeatureFlags(messenger); + return { data: params.data, from: params.from, + gas: toHex(params.gas ?? featureFlags.relayFallbackGas.max), + maxFeePerGas: toHex(params.maxFeePerGas), + maxPriorityFeePerGas: toHex(params.maxPriorityFeePerGas), to: params.to, value: toHex(params.value ?? '0'), }; @@ -377,7 +385,7 @@ async function submitTransactions( } const normalizedParams = params.map((singleParams) => - normalizeParams(singleParams), + normalizeParams(singleParams, messenger), ); // For post-quote flows, prepend the original transaction so it gets diff --git a/packages/transaction-pay-controller/src/strategy/relay/types.ts b/packages/transaction-pay-controller/src/strategy/relay/types.ts index 9b5d089e55..442097d2e0 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/types.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/types.ts @@ -96,8 +96,8 @@ export type RelayTransactionStep = { data: Hex; from: Hex; gas?: string; - maxFeePerGas?: string; - maxPriorityFeePerGas?: string; + maxFeePerGas: string; + maxPriorityFeePerGas: string; to: Hex; value?: string; }; From 4a19e414c97b27de79a362c464fad5c3461b7467 Mon Sep 17 00:00:00 2001 From: Jyoti Puri Date: Sat, 23 May 2026 02:16:29 +0530 Subject: [PATCH 09/35] update --- .../src/strategy/relay/relay-quotes.test.ts | 20 ++++++++++++++++++- .../src/strategy/relay/relay-quotes.ts | 9 ++++++--- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts index ab8ae47cf2..381751bc6e 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts @@ -3464,7 +3464,25 @@ describe('Relay Quotes Utils', () => { expect(quote.original.steps[0].id).toBe(STEP_MOCK.id); }); - it('uses sourceChainId to set chainId on paymentOverride step items', async () => { + it('uses the transaction chainId when present', async () => { + getPaymentOverrideDataMock.mockResolvedValue([ + { ...PAYMENT_OVERRIDE_TX_MOCK, chainId: '0xa' }, + ]); + + const [quote] = await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [{ ...QUOTE_REQUEST_MOCK, paymentOverride: PaymentOverride.MoneyAccount }], + transaction: TRANSACTION_META_MOCK, + }); + + const overrideStep = quote.original.steps.find( + (step) => step.id === 'payment-override', + ) as RelayTransactionStep; + expect(overrideStep.items[0].data.chainId).toBe(10); + }); + + it('falls back to sourceChainId when transaction chainId is absent', async () => { getPaymentOverrideDataMock.mockResolvedValue([PAYMENT_OVERRIDE_TX_MOCK]); const [quote] = await getRelayQuotes({ diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts index 5a72b981b8..ad6ff6fd3e 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts @@ -1071,15 +1071,18 @@ async function injectPaymentOverrideSteps( * Converts an array of TransactionParams into a single RelayTransactionStep * so they can be injected into a relay quote's steps array. * + * Each transaction's `chainId` is used when present; `sourceChainId` is the + * fallback for transactions that do not specify one. + * * @param txParams - Transactions from the paymentOverride callback. - * @param sourceChainId - Hex chain ID of the source network. + * @param sourceChainId - Fallback hex chain ID used when a transaction omits its own. * @returns A relay transaction step wrapping the paymentOverride transactions. */ function buildPaymentOverrideStep( txParams: TransactionParams[], sourceChainId: Hex, ): RelayTransactionStep { - const chainId = parseInt(sourceChainId, 16); + const fallbackChainId = parseInt(sourceChainId, 16); return { id: 'payment-override', @@ -1089,7 +1092,7 @@ function buildPaymentOverrideStep( check: { endpoint: '', method: 'GET' as const }, status: 'incomplete' as const, data: { - chainId, + chainId: params.chainId ? parseInt(params.chainId, 16) : fallbackChainId, data: (params.data as Hex) ?? '0x', from: params.from as Hex, maxFeePerGas: params.maxFeePerGas as string, From a0501e5c28f9b34e048fc3c967267bef173de113 Mon Sep 17 00:00:00 2001 From: Jyoti Puri Date: Sat, 23 May 2026 02:17:03 +0530 Subject: [PATCH 10/35] update --- .../src/strategy/relay/relay-quotes.test.ts | 20 +------------------ .../src/strategy/relay/relay-quotes.ts | 9 +++------ 2 files changed, 4 insertions(+), 25 deletions(-) diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts index 381751bc6e..ab8ae47cf2 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts @@ -3464,25 +3464,7 @@ describe('Relay Quotes Utils', () => { expect(quote.original.steps[0].id).toBe(STEP_MOCK.id); }); - it('uses the transaction chainId when present', async () => { - getPaymentOverrideDataMock.mockResolvedValue([ - { ...PAYMENT_OVERRIDE_TX_MOCK, chainId: '0xa' }, - ]); - - const [quote] = await getRelayQuotes({ - accountSupports7702: true, - messenger, - requests: [{ ...QUOTE_REQUEST_MOCK, paymentOverride: PaymentOverride.MoneyAccount }], - transaction: TRANSACTION_META_MOCK, - }); - - const overrideStep = quote.original.steps.find( - (step) => step.id === 'payment-override', - ) as RelayTransactionStep; - expect(overrideStep.items[0].data.chainId).toBe(10); - }); - - it('falls back to sourceChainId when transaction chainId is absent', async () => { + it('uses sourceChainId to set chainId on paymentOverride step items', async () => { getPaymentOverrideDataMock.mockResolvedValue([PAYMENT_OVERRIDE_TX_MOCK]); const [quote] = await getRelayQuotes({ diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts index ad6ff6fd3e..5a72b981b8 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts @@ -1071,18 +1071,15 @@ async function injectPaymentOverrideSteps( * Converts an array of TransactionParams into a single RelayTransactionStep * so they can be injected into a relay quote's steps array. * - * Each transaction's `chainId` is used when present; `sourceChainId` is the - * fallback for transactions that do not specify one. - * * @param txParams - Transactions from the paymentOverride callback. - * @param sourceChainId - Fallback hex chain ID used when a transaction omits its own. + * @param sourceChainId - Hex chain ID of the source network. * @returns A relay transaction step wrapping the paymentOverride transactions. */ function buildPaymentOverrideStep( txParams: TransactionParams[], sourceChainId: Hex, ): RelayTransactionStep { - const fallbackChainId = parseInt(sourceChainId, 16); + const chainId = parseInt(sourceChainId, 16); return { id: 'payment-override', @@ -1092,7 +1089,7 @@ function buildPaymentOverrideStep( check: { endpoint: '', method: 'GET' as const }, status: 'incomplete' as const, data: { - chainId: params.chainId ? parseInt(params.chainId, 16) : fallbackChainId, + chainId, data: (params.data as Hex) ?? '0x', from: params.from as Hex, maxFeePerGas: params.maxFeePerGas as string, From edec73a92e797fc708679c6d2e0f17c7d5c271ac Mon Sep 17 00:00:00 2001 From: Jyoti Puri Date: Sat, 23 May 2026 02:23:47 +0530 Subject: [PATCH 11/35] update --- packages/transaction-pay-controller/CHANGELOG.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/transaction-pay-controller/CHANGELOG.md b/packages/transaction-pay-controller/CHANGELOG.md index 3298a0779b..eee671e161 100644 --- a/packages/transaction-pay-controller/CHANGELOG.md +++ b/packages/transaction-pay-controller/CHANGELOG.md @@ -9,8 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Add `getPaymentOverrideData` callback (type `GetPaymentOverrideDataCallback`) to `TransactionPayControllerOptions`; when `paymentOverride` is defined on a transaction, this callback is invoked during quote execution and the resulting transactions are injected into the relay quote steps, ordered before or after the quote batch based on `isPostQuote` ([#8858](https://github.com/MetaMask/core/pull/8858)) - - Exposed as the `TransactionPayController:getPaymentOverrideData` messenger action (type `TransactionPayControllerGetPaymentOverrideDataAction`) +- Add `getPaymentOverrideData` callback to `TransactionPayControllerOptions`, when `paymentOverride` is defined on a transaction, this callback is invoked the resulting transactions are injected into the relay quote steps ([#8858](https://github.com/MetaMask/core/pull/8858)) ## [22.7.0] From 0b42cf3fc9a39133f05c9f94b254a2c77a68f71c Mon Sep 17 00:00:00 2001 From: Jyoti Puri Date: Sat, 23 May 2026 02:37:30 +0530 Subject: [PATCH 12/35] update --- .../src/strategy/relay/relay-quotes.test.ts | 20 ++++++++++++++++++- .../src/strategy/relay/relay-quotes.ts | 9 ++++++--- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts index ab8ae47cf2..381751bc6e 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts @@ -3464,7 +3464,25 @@ describe('Relay Quotes Utils', () => { expect(quote.original.steps[0].id).toBe(STEP_MOCK.id); }); - it('uses sourceChainId to set chainId on paymentOverride step items', async () => { + it('uses the transaction chainId when present', async () => { + getPaymentOverrideDataMock.mockResolvedValue([ + { ...PAYMENT_OVERRIDE_TX_MOCK, chainId: '0xa' }, + ]); + + const [quote] = await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [{ ...QUOTE_REQUEST_MOCK, paymentOverride: PaymentOverride.MoneyAccount }], + transaction: TRANSACTION_META_MOCK, + }); + + const overrideStep = quote.original.steps.find( + (step) => step.id === 'payment-override', + ) as RelayTransactionStep; + expect(overrideStep.items[0].data.chainId).toBe(10); + }); + + it('falls back to sourceChainId when transaction chainId is absent', async () => { getPaymentOverrideDataMock.mockResolvedValue([PAYMENT_OVERRIDE_TX_MOCK]); const [quote] = await getRelayQuotes({ diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts index 5a72b981b8..ad6ff6fd3e 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts @@ -1071,15 +1071,18 @@ async function injectPaymentOverrideSteps( * Converts an array of TransactionParams into a single RelayTransactionStep * so they can be injected into a relay quote's steps array. * + * Each transaction's `chainId` is used when present; `sourceChainId` is the + * fallback for transactions that do not specify one. + * * @param txParams - Transactions from the paymentOverride callback. - * @param sourceChainId - Hex chain ID of the source network. + * @param sourceChainId - Fallback hex chain ID used when a transaction omits its own. * @returns A relay transaction step wrapping the paymentOverride transactions. */ function buildPaymentOverrideStep( txParams: TransactionParams[], sourceChainId: Hex, ): RelayTransactionStep { - const chainId = parseInt(sourceChainId, 16); + const fallbackChainId = parseInt(sourceChainId, 16); return { id: 'payment-override', @@ -1089,7 +1092,7 @@ function buildPaymentOverrideStep( check: { endpoint: '', method: 'GET' as const }, status: 'incomplete' as const, data: { - chainId, + chainId: params.chainId ? parseInt(params.chainId, 16) : fallbackChainId, data: (params.data as Hex) ?? '0x', from: params.from as Hex, maxFeePerGas: params.maxFeePerGas as string, From d32a3f4dfcdc7b0cfcb098f51fb6c7931e4fe3d7 Mon Sep 17 00:00:00 2001 From: Jyoti Puri Date: Sat, 23 May 2026 02:43:07 +0530 Subject: [PATCH 13/35] update --- .../src/TransactionPayController.test.ts | 4 +- .../src/strategy/relay/relay-quotes.test.ts | 46 +++++++++++++++---- .../src/strategy/relay/relay-quotes.ts | 9 +++- 3 files changed, 46 insertions(+), 13 deletions(-) diff --git a/packages/transaction-pay-controller/src/TransactionPayController.test.ts b/packages/transaction-pay-controller/src/TransactionPayController.test.ts index e6a572dac7..26d8d43039 100644 --- a/packages/transaction-pay-controller/src/TransactionPayController.test.ts +++ b/packages/transaction-pay-controller/src/TransactionPayController.test.ts @@ -472,9 +472,7 @@ describe('TransactionPayController', () => { describe('getPaymentOverrideData', () => { it('delegates to the callback', async () => { const txMock = { from: '0xabc', to: '0xdef' }; - const getPaymentOverrideDataMock = jest - .fn() - .mockResolvedValue([txMock]); + const getPaymentOverrideDataMock = jest.fn().mockResolvedValue([txMock]); new TransactionPayController({ getDelegationTransaction: jest.fn(), diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts index 381751bc6e..448d04fd30 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts @@ -3400,12 +3400,19 @@ describe('Relay Quotes Utils', () => { }); it('prepends paymentOverride step before relay steps for standard (non-post-quote) flow', async () => { - getPaymentOverrideDataMock.mockResolvedValue([PAYMENT_OVERRIDE_TX_MOCK]); + getPaymentOverrideDataMock.mockResolvedValue([ + PAYMENT_OVERRIDE_TX_MOCK, + ]); const [quote] = await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [{ ...QUOTE_REQUEST_MOCK, paymentOverride: PaymentOverride.MoneyAccount }], + requests: [ + { + ...QUOTE_REQUEST_MOCK, + paymentOverride: PaymentOverride.MoneyAccount, + }, + ], transaction: TRANSACTION_META_MOCK, }); @@ -3418,13 +3425,19 @@ describe('Relay Quotes Utils', () => { }); it('appends paymentOverride step after relay steps for post-quote flow', async () => { - getPaymentOverrideDataMock.mockResolvedValue([PAYMENT_OVERRIDE_TX_MOCK]); + getPaymentOverrideDataMock.mockResolvedValue([ + PAYMENT_OVERRIDE_TX_MOCK, + ]); const [quote] = await getRelayQuotes({ accountSupports7702: true, messenger, requests: [ - { ...QUOTE_REQUEST_MOCK, paymentOverride: PaymentOverride.MoneyAccount, isPostQuote: true }, + { + ...QUOTE_REQUEST_MOCK, + paymentOverride: PaymentOverride.MoneyAccount, + isPostQuote: true, + }, ], transaction: TRANSACTION_META_MOCK, }); @@ -3456,7 +3469,12 @@ describe('Relay Quotes Utils', () => { const [quote] = await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [{ ...QUOTE_REQUEST_MOCK, paymentOverride: PaymentOverride.MoneyAccount }], + requests: [ + { + ...QUOTE_REQUEST_MOCK, + paymentOverride: PaymentOverride.MoneyAccount, + }, + ], transaction: TRANSACTION_META_MOCK, }); @@ -3472,7 +3490,12 @@ describe('Relay Quotes Utils', () => { const [quote] = await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [{ ...QUOTE_REQUEST_MOCK, paymentOverride: PaymentOverride.MoneyAccount }], + requests: [ + { + ...QUOTE_REQUEST_MOCK, + paymentOverride: PaymentOverride.MoneyAccount, + }, + ], transaction: TRANSACTION_META_MOCK, }); @@ -3483,12 +3506,19 @@ describe('Relay Quotes Utils', () => { }); it('falls back to sourceChainId when transaction chainId is absent', async () => { - getPaymentOverrideDataMock.mockResolvedValue([PAYMENT_OVERRIDE_TX_MOCK]); + getPaymentOverrideDataMock.mockResolvedValue([ + PAYMENT_OVERRIDE_TX_MOCK, + ]); const [quote] = await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [{ ...QUOTE_REQUEST_MOCK, paymentOverride: PaymentOverride.MoneyAccount }], + requests: [ + { + ...QUOTE_REQUEST_MOCK, + paymentOverride: PaymentOverride.MoneyAccount, + }, + ], transaction: TRANSACTION_META_MOCK, }); diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts index ad6ff6fd3e..2c5c9c33d8 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts @@ -1052,7 +1052,10 @@ async function injectPaymentOverrideSteps( return quote; } - const overrideStep = buildPaymentOverrideStep(overrideTxs, request.sourceChainId); + const overrideStep = buildPaymentOverrideStep( + overrideTxs, + request.sourceChainId, + ); const steps = request.isPostQuote ? [...quote.steps, overrideStep] @@ -1092,7 +1095,9 @@ function buildPaymentOverrideStep( check: { endpoint: '', method: 'GET' as const }, status: 'incomplete' as const, data: { - chainId: params.chainId ? parseInt(params.chainId, 16) : fallbackChainId, + chainId: params.chainId + ? parseInt(params.chainId, 16) + : fallbackChainId, data: (params.data as Hex) ?? '0x', from: params.from as Hex, maxFeePerGas: params.maxFeePerGas as string, From 7801d590cb86d82433d8e7c67ccb95f52969d12e Mon Sep 17 00:00:00 2001 From: Jyoti Puri Date: Mon, 25 May 2026 15:45:20 +0530 Subject: [PATCH 14/35] update --- .../transaction-pay-controller/CHANGELOG.md | 2 +- ...actionPayController-method-action-types.ts | 32 +++++++++---------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/packages/transaction-pay-controller/CHANGELOG.md b/packages/transaction-pay-controller/CHANGELOG.md index eee671e161..d428657f70 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 `getPaymentOverrideData` callback to `TransactionPayControllerOptions`, when `paymentOverride` is defined on a transaction, this callback is invoked the resulting transactions are injected into the relay quote steps ([#8858](https://github.com/MetaMask/core/pull/8858)) +- Add `getPaymentOverrideData` callback to `TransactionPayControllerOptions`, when `paymentOverride` is defined on a transaction, this callback is invoked the resulting transactions are injected into the relay quote steps ([#8870](https://github.com/MetaMask/core/pull/8870)) ## [22.7.0] 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 45671cd85a..501df919c7 100644 --- a/packages/transaction-pay-controller/src/TransactionPayController-method-action-types.ts +++ b/packages/transaction-pay-controller/src/TransactionPayController-method-action-types.ts @@ -49,22 +49,6 @@ export type TransactionPayControllerUpdateFiatPaymentAction = { handler: TransactionPayController['updateFiatPayment']; }; -/** - * Returns additional transactions for the paymentOverride flow. - * - * Delegates to the client-supplied callback. Called during quote execution - * when paymentOverride is true. Returns an empty array when no callback - * is configured. - * - * @param args - The arguments forwarded to the {@link GetPaymentOverrideDataCallback}, - * containing the transaction ID. - * @returns A promise resolving to the additional transactions array. - */ -export type TransactionPayControllerGetPaymentOverrideDataAction = { - type: `TransactionPayController:getPaymentOverrideData`; - handler: TransactionPayController['getPaymentOverrideData']; -}; - /** * Gets the delegation transaction for a given transaction. * @@ -82,6 +66,22 @@ export type TransactionPayControllerGetDelegationTransactionAction = { handler: TransactionPayController['getDelegationTransaction']; }; +/** + * Returns additional transactions for the paymentOverride flow. + * + * Delegates to the client-supplied {@link GetPaymentOverrideDataCallback}. + * Called during quote execution when `paymentOverride` is defined on the transaction. + * Returns an empty array when no callback is configured. + * + * @param args - The arguments forwarded to the {@link GetPaymentOverrideDataCallback}, + * containing the transaction ID. + * @returns A promise resolving to the additional transactions array. + */ +export type TransactionPayControllerGetPaymentOverrideDataAction = { + type: `TransactionPayController:getPaymentOverrideData`; + handler: TransactionPayController['getPaymentOverrideData']; +}; + /** * Gets the preferred strategy for a transaction. * From fb4ff924d1817f83316507e9a2e1155acaf0c63b Mon Sep 17 00:00:00 2001 From: Jyoti Puri Date: Mon, 25 May 2026 15:57:19 +0530 Subject: [PATCH 15/35] update --- .../src/strategy/relay/relay-quotes.test.ts | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts index 448d04fd30..d4fe82e56b 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts @@ -3505,6 +3505,28 @@ describe('Relay Quotes Utils', () => { expect(overrideStep.items[0].data.chainId).toBe(10); }); + it('defaults data to 0x when transaction data is absent', async () => { + const { data: _data, ...txWithoutData } = PAYMENT_OVERRIDE_TX_MOCK; + getPaymentOverrideDataMock.mockResolvedValue([txWithoutData]); + + const [quote] = await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [ + { + ...QUOTE_REQUEST_MOCK, + paymentOverride: PaymentOverride.MoneyAccount, + }, + ], + transaction: TRANSACTION_META_MOCK, + }); + + const overrideStep = quote.original.steps.find( + (step) => step.id === 'payment-override', + ) as RelayTransactionStep; + expect(overrideStep.items[0].data.data).toBe('0x'); + }); + it('falls back to sourceChainId when transaction chainId is absent', async () => { getPaymentOverrideDataMock.mockResolvedValue([ PAYMENT_OVERRIDE_TX_MOCK, From 8d683c8548c5ba79501ece17815598109e74bc31 Mon Sep 17 00:00:00 2001 From: Jyoti Puri Date: Tue, 26 May 2026 07:51:00 +0530 Subject: [PATCH 16/35] update --- .../src/strategy/relay/relay-quotes.test.ts | 110 +++++----------- .../src/strategy/relay/relay-quotes.ts | 122 ++++-------------- 2 files changed, 55 insertions(+), 177 deletions(-) diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts index d4fe82e56b..97efef8efe 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts @@ -3377,14 +3377,11 @@ describe('Relay Quotes Utils', () => { }); }); - describe('paymentOverride step injection (paymentOverride defined)', () => { + describe('paymentOverride via body.txs (paymentOverride defined)', () => { const PAYMENT_OVERRIDE_TX_MOCK = { - from: FROM_MOCK, to: '0xpaymentoverride' as Hex, data: '0xpaymentoverride' as Hex, value: '0x0', - maxFeePerGas: '1000000000', - maxPriorityFeePerGas: '2000000000', }; beforeEach(() => { @@ -3399,12 +3396,12 @@ describe('Relay Quotes Utils', () => { }); }); - it('prepends paymentOverride step before relay steps for standard (non-post-quote) flow', async () => { + it('includes override txs in the relay request body.txs', async () => { getPaymentOverrideDataMock.mockResolvedValue([ PAYMENT_OVERRIDE_TX_MOCK, ]); - const [quote] = await getRelayQuotes({ + await getRelayQuotes({ accountSupports7702: true, messenger, requests: [ @@ -3416,57 +3413,34 @@ describe('Relay Quotes Utils', () => { transaction: TRANSACTION_META_MOCK, }); - const txSteps = quote.original.steps.filter( - (step): step is RelayTransactionStep => step.kind === 'transaction', + const body = JSON.parse( + successfulFetchMock.mock.calls[0][1]?.body as string, ); - expect(txSteps[0].id).toBe('payment-override'); - expect(txSteps[0].items[0].data.to).toBe(PAYMENT_OVERRIDE_TX_MOCK.to); - expect(txSteps[1].id).toBe(STEP_MOCK.id); - }); - it('appends paymentOverride step after relay steps for post-quote flow', async () => { - getPaymentOverrideDataMock.mockResolvedValue([ - PAYMENT_OVERRIDE_TX_MOCK, + expect(body.txs).toStrictEqual([ + { + to: PAYMENT_OVERRIDE_TX_MOCK.to, + data: PAYMENT_OVERRIDE_TX_MOCK.data, + value: PAYMENT_OVERRIDE_TX_MOCK.value, + }, ]); - - const [quote] = await getRelayQuotes({ - accountSupports7702: true, - messenger, - requests: [ - { - ...QUOTE_REQUEST_MOCK, - paymentOverride: PaymentOverride.MoneyAccount, - isPostQuote: true, - }, - ], - transaction: TRANSACTION_META_MOCK, - }); - - const txSteps = quote.original.steps.filter( - (step): step is RelayTransactionStep => step.kind === 'transaction', - ); - expect(txSteps[0].id).toBe(STEP_MOCK.id); - expect(txSteps[1].id).toBe('payment-override'); }); - it('does not inject when paymentOverride is not defined', async () => { - const [quote] = await getRelayQuotes({ + it('does not call getPaymentOverrideData when paymentOverride is not defined', async () => { + await getRelayQuotes({ accountSupports7702: true, messenger, requests: [QUOTE_REQUEST_MOCK], transaction: TRANSACTION_META_MOCK, }); - expect( - quote.original.steps.every((step) => step.id !== 'payment-override'), - ).toBe(true); expect(getPaymentOverrideDataMock).not.toHaveBeenCalled(); }); - it('does not inject when callback returns empty array', async () => { + it('does not set body.txs when callback returns empty array', async () => { getPaymentOverrideDataMock.mockResolvedValue([]); - const [quote] = await getRelayQuotes({ + await getRelayQuotes({ accountSupports7702: true, messenger, requests: [ @@ -3478,38 +3452,18 @@ describe('Relay Quotes Utils', () => { transaction: TRANSACTION_META_MOCK, }); - expect(quote.original.steps).toHaveLength(1); - expect(quote.original.steps[0].id).toBe(STEP_MOCK.id); - }); - - it('uses the transaction chainId when present', async () => { - getPaymentOverrideDataMock.mockResolvedValue([ - { ...PAYMENT_OVERRIDE_TX_MOCK, chainId: '0xa' }, - ]); - - const [quote] = await getRelayQuotes({ - accountSupports7702: true, - messenger, - requests: [ - { - ...QUOTE_REQUEST_MOCK, - paymentOverride: PaymentOverride.MoneyAccount, - }, - ], - transaction: TRANSACTION_META_MOCK, - }); + const body = JSON.parse( + successfulFetchMock.mock.calls[0][1]?.body as string, + ); - const overrideStep = quote.original.steps.find( - (step) => step.id === 'payment-override', - ) as RelayTransactionStep; - expect(overrideStep.items[0].data.chainId).toBe(10); + expect(body.txs).toBeUndefined(); }); it('defaults data to 0x when transaction data is absent', async () => { const { data: _data, ...txWithoutData } = PAYMENT_OVERRIDE_TX_MOCK; getPaymentOverrideDataMock.mockResolvedValue([txWithoutData]); - const [quote] = await getRelayQuotes({ + await getRelayQuotes({ accountSupports7702: true, messenger, requests: [ @@ -3521,18 +3475,19 @@ describe('Relay Quotes Utils', () => { transaction: TRANSACTION_META_MOCK, }); - const overrideStep = quote.original.steps.find( - (step) => step.id === 'payment-override', - ) as RelayTransactionStep; - expect(overrideStep.items[0].data.data).toBe('0x'); + const body = JSON.parse( + successfulFetchMock.mock.calls[0][1]?.body as string, + ); + + expect(body.txs[0].data).toBe('0x'); }); - it('falls back to sourceChainId when transaction chainId is absent', async () => { + it('skips processTransactions when paymentOverride is defined', async () => { getPaymentOverrideDataMock.mockResolvedValue([ PAYMENT_OVERRIDE_TX_MOCK, ]); - const [quote] = await getRelayQuotes({ + await getRelayQuotes({ accountSupports7702: true, messenger, requests: [ @@ -3541,14 +3496,13 @@ describe('Relay Quotes Utils', () => { paymentOverride: PaymentOverride.MoneyAccount, }, ], - transaction: TRANSACTION_META_MOCK, + transaction: { + ...TRANSACTION_META_MOCK, + txParams: { data: '0xabc' as Hex }, + } as TransactionMeta, }); - const overrideStep = quote.original.steps.find( - (step) => step.id === 'payment-override', - ) as RelayTransactionStep; - // QUOTE_REQUEST_MOCK.sourceChainId is '0x1' → chainId 1 - expect(overrideStep.items[0].data.chainId).toBe(1); + expect(getDelegationTransactionMock).not.toHaveBeenCalled(); }); }); diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts index 2c5c9c33d8..818e6dc750 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts @@ -2,10 +2,7 @@ import { Interface } from '@ethersproject/abi'; import { toHex } from '@metamask/controller-utils'; -import type { - TransactionMeta, - TransactionParams, -} from '@metamask/transaction-controller'; +import type { TransactionMeta } from '@metamask/transaction-controller'; import type { Hex } from '@metamask/utils'; import { createModuleLogger } from '@metamask/utils'; import { BigNumber } from 'bignumber.js'; @@ -259,11 +256,27 @@ async function getSingleQuote( await applyPolymarketDepositWalletOverrides(body, request, messenger); } - // Skip transaction processing for post-quote flows - the original transaction - // will be included in the batch separately, not as part of the quote. - // Skip for Polymarket deposit wallet flows - the source is already a - // bridged token transfer, not a contract call to embed. - if (!request.isPostQuote && !request.isPolymarketDepositWallet) { + // When paymentOverride is defined, fetch the override transactions and + // include them in the Relay request body so Relay executes them + // atomically alongside the bridge. + if (request.paymentOverride) { + const overrideTxs = await messenger.call( + 'TransactionPayController:getPaymentOverrideData', + transaction.id, + ); + + if (overrideTxs.length) { + body.txs = overrideTxs.map((tx) => ({ + to: (tx.to as string) ?? '', + data: (tx.data as string) ?? '0x', + value: (tx.value as string) ?? '0x0', + })); + } + } else if (!request.isPostQuote && !request.isPolymarketDepositWallet) { + // Skip transaction processing for post-quote flows - the original transaction + // will be included in the batch separately, not as part of the quote. + // Skip for Polymarket deposit wallet flows - the source is already a + // bridged token transfer, not a contract call to embed. await processTransactions(transaction, request, body, messenger); } else if (request.refundTo) { // For post-quote flows, honour the caller-specified refund address so that @@ -278,11 +291,7 @@ async function getSingleQuote( log('Fetched relay quote', quote); - const quoteWithDeposits = request.paymentOverride - ? await injectPaymentOverrideSteps(quote, request, fullRequest) - : quote; - - return await normalizeQuote(quoteWithDeposits, request, fullRequest); + return await normalizeQuote(quote, request, fullRequest); } catch (error) { log('Error fetching relay quote', error); throw error; @@ -1022,92 +1031,7 @@ function getSubsidizedFeeAmountUsd(quote: RelayQuote): BigNumber { return isSubsidizedStablecoin ? amountFormatted : amountUsd; } -/** - * Fetches transactions from the paymentOverride callback and injects - * them into the relay quote's steps so they are submitted alongside the relay - * transactions and included in gas estimation. - * - * For standard flows (`isPostQuote` false) the step is prepended so it - * executes before the relay bridge transaction. For post-quote flows it is - * appended so it executes after. - * - * @param quote - Relay quote to mutate in place. - * @param request - Quote request, used to determine ordering and chain ID. - * @param fullRequest - Full quotes request, provides messenger and transaction ID. - * @returns The relay quote with paymentOverride steps injected, or the original quote if the callback returns no transactions. - */ -async function injectPaymentOverrideSteps( - quote: RelayQuote, - request: QuoteRequest, - fullRequest: PayStrategyGetQuotesRequest, -): Promise { - const { messenger, transaction } = fullRequest; - - const overrideTxs = await messenger.call( - 'TransactionPayController:getPaymentOverrideData', - transaction.id, - ); - - if (!overrideTxs.length) { - return quote; - } - const overrideStep = buildPaymentOverrideStep( - overrideTxs, - request.sourceChainId, - ); - - const steps = request.isPostQuote - ? [...quote.steps, overrideStep] - : [overrideStep, ...quote.steps]; - - log('Injected paymentOverride step', { - transactionId: transaction.id, - isPostQuote: request.isPostQuote, - txCount: overrideTxs.length, - }); - - return { ...quote, steps }; -} - -/** - * Converts an array of TransactionParams into a single RelayTransactionStep - * so they can be injected into a relay quote's steps array. - * - * Each transaction's `chainId` is used when present; `sourceChainId` is the - * fallback for transactions that do not specify one. - * - * @param txParams - Transactions from the paymentOverride callback. - * @param sourceChainId - Fallback hex chain ID used when a transaction omits its own. - * @returns A relay transaction step wrapping the paymentOverride transactions. - */ -function buildPaymentOverrideStep( - txParams: TransactionParams[], - sourceChainId: Hex, -): RelayTransactionStep { - const fallbackChainId = parseInt(sourceChainId, 16); - - return { - id: 'payment-override', - kind: 'transaction', - requestId: 'payment-override', - items: txParams.map((params) => ({ - check: { endpoint: '', method: 'GET' as const }, - status: 'incomplete' as const, - data: { - chainId: params.chainId - ? parseInt(params.chainId, 16) - : fallbackChainId, - data: (params.data as Hex) ?? '0x', - from: params.from as Hex, - maxFeePerGas: params.maxFeePerGas as string, - maxPriorityFeePerGas: params.maxPriorityFeePerGas as string, - to: params.to as Hex, - value: params.value, - }, - })), - }; -} function isStablecoin(chainId: string, tokenAddress: string): boolean { return Boolean( From e3897dd86010fed7fed47f9c515496834319dfb1 Mon Sep 17 00:00:00 2001 From: Jyoti Puri Date: Tue, 26 May 2026 20:25:59 +0530 Subject: [PATCH 17/35] update --- .../src/strategy/relay/relay-quotes.test.ts | 99 +------------------ .../src/strategy/relay/relay-quotes.ts | 33 ++----- .../src/strategy/relay/relay-submit.test.ts | 70 +++++++++++++ .../src/strategy/relay/relay-submit.ts | 18 +++- 4 files changed, 98 insertions(+), 122 deletions(-) diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts index 97efef8efe..6ad7a487c3 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts @@ -184,7 +184,6 @@ describe('Relay Quotes Utils', () => { getDelegationTransactionMock, getGasFeeTokensMock, getKeyringControllerStateMock, - getPaymentOverrideDataMock, getRemoteFeatureFlagControllerStateMock, polymarketGetDepositWalletAddressMock, } = getMessengerMock(); @@ -3377,13 +3376,7 @@ describe('Relay Quotes Utils', () => { }); }); - describe('paymentOverride via body.txs (paymentOverride defined)', () => { - const PAYMENT_OVERRIDE_TX_MOCK = { - to: '0xpaymentoverride' as Hex, - data: '0xpaymentoverride' as Hex, - value: '0x0', - }; - + describe('paymentOverride (paymentOverride defined)', () => { beforeEach(() => { successfulFetchMock.mockResolvedValue({ ok: true, @@ -3396,97 +3389,7 @@ describe('Relay Quotes Utils', () => { }); }); - it('includes override txs in the relay request body.txs', async () => { - getPaymentOverrideDataMock.mockResolvedValue([ - PAYMENT_OVERRIDE_TX_MOCK, - ]); - - await getRelayQuotes({ - accountSupports7702: true, - messenger, - requests: [ - { - ...QUOTE_REQUEST_MOCK, - paymentOverride: PaymentOverride.MoneyAccount, - }, - ], - transaction: TRANSACTION_META_MOCK, - }); - - const body = JSON.parse( - successfulFetchMock.mock.calls[0][1]?.body as string, - ); - - expect(body.txs).toStrictEqual([ - { - to: PAYMENT_OVERRIDE_TX_MOCK.to, - data: PAYMENT_OVERRIDE_TX_MOCK.data, - value: PAYMENT_OVERRIDE_TX_MOCK.value, - }, - ]); - }); - - it('does not call getPaymentOverrideData when paymentOverride is not defined', async () => { - await getRelayQuotes({ - accountSupports7702: true, - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, - }); - - expect(getPaymentOverrideDataMock).not.toHaveBeenCalled(); - }); - - it('does not set body.txs when callback returns empty array', async () => { - getPaymentOverrideDataMock.mockResolvedValue([]); - - await getRelayQuotes({ - accountSupports7702: true, - messenger, - requests: [ - { - ...QUOTE_REQUEST_MOCK, - paymentOverride: PaymentOverride.MoneyAccount, - }, - ], - transaction: TRANSACTION_META_MOCK, - }); - - const body = JSON.parse( - successfulFetchMock.mock.calls[0][1]?.body as string, - ); - - expect(body.txs).toBeUndefined(); - }); - - it('defaults data to 0x when transaction data is absent', async () => { - const { data: _data, ...txWithoutData } = PAYMENT_OVERRIDE_TX_MOCK; - getPaymentOverrideDataMock.mockResolvedValue([txWithoutData]); - - await getRelayQuotes({ - accountSupports7702: true, - messenger, - requests: [ - { - ...QUOTE_REQUEST_MOCK, - paymentOverride: PaymentOverride.MoneyAccount, - }, - ], - transaction: TRANSACTION_META_MOCK, - }); - - const body = JSON.parse( - successfulFetchMock.mock.calls[0][1]?.body as string, - ); - - expect(body.txs[0].data).toBe('0x'); - }); - it('skips processTransactions when paymentOverride is defined', async () => { - getPaymentOverrideDataMock.mockResolvedValue([ - PAYMENT_OVERRIDE_TX_MOCK, - ]); - await getRelayQuotes({ accountSupports7702: true, messenger, diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts index 818e6dc750..36c5c15a42 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts @@ -256,27 +256,16 @@ async function getSingleQuote( await applyPolymarketDepositWalletOverrides(body, request, messenger); } - // When paymentOverride is defined, fetch the override transactions and - // include them in the Relay request body so Relay executes them - // atomically alongside the bridge. - if (request.paymentOverride) { - const overrideTxs = await messenger.call( - 'TransactionPayController:getPaymentOverrideData', - transaction.id, - ); - - if (overrideTxs.length) { - body.txs = overrideTxs.map((tx) => ({ - to: (tx.to as string) ?? '', - data: (tx.data as string) ?? '0x', - value: (tx.value as string) ?? '0x0', - })); - } - } else if (!request.isPostQuote && !request.isPolymarketDepositWallet) { - // Skip transaction processing for post-quote flows - the original transaction - // will be included in the batch separately, not as part of the quote. - // Skip for Polymarket deposit wallet flows - the source is already a - // bridged token transfer, not a contract call to embed. + // Skip transaction processing for post-quote flows - the original transaction + // will be included in the batch separately, not as part of the quote. + // Skip for Polymarket deposit wallet flows - the source is already a + // bridged token transfer, not a contract call to embed. + // Skip for paymentOverride flows - override txs are prepended during submit. + if ( + !request.paymentOverride && + !request.isPostQuote && + !request.isPolymarketDepositWallet + ) { await processTransactions(transaction, request, body, messenger); } else if (request.refundTo) { // For post-quote flows, honour the caller-specified refund address so that @@ -1031,8 +1020,6 @@ function getSubsidizedFeeAmountUsd(quote: RelayQuote): BigNumber { return isSubsidizedStablecoin ? amountFormatted : amountUsd; } - - function isStablecoin(chainId: string, tokenAddress: string): boolean { return Boolean( STABLECOINS[chainId as Hex]?.includes(tokenAddress.toLowerCase() as Hex), diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts index ae08364ae8..7ae3727538 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts @@ -4,6 +4,7 @@ import type { TransactionMeta } from '@metamask/transaction-controller'; import type { Hex } from '@metamask/utils'; import { cloneDeep } from 'lodash'; +import { PaymentOverride } from '../../constants'; import { getMessengerMock } from '../../tests/messenger-mock'; import type { PayStrategyExecuteRequest, @@ -142,6 +143,7 @@ describe('Relay Submit Utils', () => { addTransactionBatchMock, getDelegationTransactionMock, findNetworkClientIdByChainIdMock, + getPaymentOverrideDataMock, messenger, } = getMessengerMock(); @@ -857,6 +859,74 @@ describe('Relay Submit Utils', () => { ]); }); + describe('paymentOverride flow', () => { + const PAYMENT_OVERRIDE_TX_MOCK = { + to: '0xpaymentoverride' as Hex, + data: '0xpaymentoverride' as Hex, + value: '0x0', + }; + + it('prepends override txs to submit params', async () => { + request.quotes[0].request.paymentOverride = + PaymentOverride.MoneyAccount; + getPaymentOverrideDataMock.mockResolvedValue([ + PAYMENT_OVERRIDE_TX_MOCK, + ]); + + await submitRelayQuotes(request); + + const batchCall = addTransactionBatchMock.mock.calls[0][0]; + expect(batchCall.transactions[0].params).toStrictEqual( + expect.objectContaining({ + to: PAYMENT_OVERRIDE_TX_MOCK.to, + data: PAYMENT_OVERRIDE_TX_MOCK.data, + value: PAYMENT_OVERRIDE_TX_MOCK.value, + }), + ); + }); + + it('does not call getPaymentOverrideData when paymentOverride is not defined', async () => { + await submitRelayQuotes(request); + + expect(getPaymentOverrideDataMock).not.toHaveBeenCalled(); + }); + + it('does not prepend when callback returns empty array', async () => { + request.quotes[0].request.paymentOverride = + PaymentOverride.MoneyAccount; + getPaymentOverrideDataMock.mockResolvedValue([]); + + await submitRelayQuotes(request); + + expect(addTransactionBatchMock).not.toHaveBeenCalled(); + expect(addTransactionMock).toHaveBeenCalledTimes(1); + }); + + it('defaults data to 0x when transaction data is absent', async () => { + request.quotes[0].request.paymentOverride = + PaymentOverride.MoneyAccount; + const { data: _data, ...txWithoutData } = PAYMENT_OVERRIDE_TX_MOCK; + getPaymentOverrideDataMock.mockResolvedValue([txWithoutData]); + + await submitRelayQuotes(request); + + const batchCall = addTransactionBatchMock.mock.calls[0][0]; + expect(batchCall.transactions[0].params.data).toBe('0x'); + }); + + it('defaults value to 0x0 when transaction value is absent', async () => { + request.quotes[0].request.paymentOverride = + PaymentOverride.MoneyAccount; + const { value: _value, ...txWithoutValue } = PAYMENT_OVERRIDE_TX_MOCK; + getPaymentOverrideDataMock.mockResolvedValue([txWithoutValue]); + + await submitRelayQuotes(request); + + const batchCall = addTransactionBatchMock.mock.calls[0][0]; + expect(batchCall.transactions[0].params.value).toBe('0x0'); + }); + }); + describe('post-quote flow', () => { beforeEach(() => { request.quotes[0].request.isPostQuote = true; diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts index b6f0fc5431..a00237b2fa 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts @@ -402,7 +402,23 @@ async function submitTransactions( let allParams = normalizedParams; - if (isPostQuote && transaction.txParams.to) { + if (quote.request.paymentOverride) { + const overrideTxs = await messenger.call( + 'TransactionPayController:getPaymentOverrideData', + transaction.id, + ); + + if (overrideTxs.length) { + const overrideParams: TransactionParams[] = overrideTxs.map((tx) => ({ + data: (tx.data as Hex) ?? ('0x' as Hex), + from: tx.from, + to: tx.to, + value: (tx.value as Hex) ?? ('0x0' as Hex), + })); + + allParams = [...overrideParams, ...normalizedParams]; + } + } else if (isPostQuote && transaction.txParams.to) { const prependedParams = hasAccountOverride ? await buildDelegatedOriginalParams(transaction, messenger) : ({ From f4a6fea3a34d1da045845d713bfbb74898503d76 Mon Sep 17 00:00:00 2001 From: Jyoti Puri Date: Tue, 26 May 2026 20:29:35 +0530 Subject: [PATCH 18/35] update --- .../src/strategy/relay/relay-quotes.test.ts | 34 ------------------- .../src/strategy/relay/relay-quotes.ts | 7 +--- 2 files changed, 1 insertion(+), 40 deletions(-) diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts index 6ad7a487c3..9439faf4cc 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts @@ -14,7 +14,6 @@ import { CHAIN_ID_HYPERCORE, CHAIN_ID_POLYGON, NATIVE_TOKEN_ADDRESS, - PaymentOverride, POLYGON_USDCE_ADDRESS, } from '../../constants'; import { getMessengerMock } from '../../tests/messenger-mock'; @@ -3376,39 +3375,6 @@ describe('Relay Quotes Utils', () => { }); }); - describe('paymentOverride (paymentOverride defined)', () => { - beforeEach(() => { - successfulFetchMock.mockResolvedValue({ - ok: true, - json: async () => QUOTE_MOCK, - } as never); - - estimateGasBatchMock.mockResolvedValue({ - gasLimits: [30000, 21000], - totalGasLimit: 51000, - }); - }); - - it('skips processTransactions when paymentOverride is defined', async () => { - await getRelayQuotes({ - accountSupports7702: true, - messenger, - requests: [ - { - ...QUOTE_REQUEST_MOCK, - paymentOverride: PaymentOverride.MoneyAccount, - }, - ], - transaction: { - ...TRANSACTION_META_MOCK, - txParams: { data: '0xabc' as Hex }, - } as TransactionMeta, - }); - - expect(getDelegationTransactionMock).not.toHaveBeenCalled(); - }); - }); - describe('gas buffer support', () => { it('applies buffer to single transaction gas estimate', async () => { const quoteMock = cloneDeep(QUOTE_MOCK); diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts index 36c5c15a42..08077cd823 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts @@ -260,12 +260,7 @@ async function getSingleQuote( // will be included in the batch separately, not as part of the quote. // Skip for Polymarket deposit wallet flows - the source is already a // bridged token transfer, not a contract call to embed. - // Skip for paymentOverride flows - override txs are prepended during submit. - if ( - !request.paymentOverride && - !request.isPostQuote && - !request.isPolymarketDepositWallet - ) { + if (!request.isPostQuote && !request.isPolymarketDepositWallet) { await processTransactions(transaction, request, body, messenger); } else if (request.refundTo) { // For post-quote flows, honour the caller-specified refund address so that From c392af5801bfa1b0c2b9dc742e6fbf7e38451732 Mon Sep 17 00:00:00 2001 From: Jyoti Puri Date: Wed, 27 May 2026 08:37:09 +0530 Subject: [PATCH 19/35] update --- .../src/strategy/relay/relay-submit.test.ts | 56 +++++++++++++++---- .../src/strategy/relay/relay-submit.ts | 32 ++++++++--- 2 files changed, 69 insertions(+), 19 deletions(-) diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts index 7ae3727538..adc718d425 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts @@ -866,7 +866,19 @@ describe('Relay Submit Utils', () => { value: '0x0', }; - it('prepends override txs to submit params', async () => { + const DELEGATION_TO_MOCK = '0xdelegationto' as Hex; + const DELEGATION_DATA_MOCK = '0xdelegationdata' as Hex; + const DELEGATION_VALUE_MOCK = '0x0' as Hex; + + beforeEach(() => { + getDelegationTransactionMock.mockResolvedValue({ + data: DELEGATION_DATA_MOCK, + to: DELEGATION_TO_MOCK, + value: DELEGATION_VALUE_MOCK, + }); + }); + + it('builds delegation for override txs and prepends to submit params', async () => { request.quotes[0].request.paymentOverride = PaymentOverride.MoneyAccount; getPaymentOverrideDataMock.mockResolvedValue([ @@ -875,12 +887,25 @@ describe('Relay Submit Utils', () => { await submitRelayQuotes(request); + expect(getDelegationTransactionMock).toHaveBeenCalledWith({ + transaction: expect.objectContaining({ + nestedTransactions: [ + { + data: PAYMENT_OVERRIDE_TX_MOCK.data, + to: PAYMENT_OVERRIDE_TX_MOCK.to, + value: PAYMENT_OVERRIDE_TX_MOCK.value, + }, + ], + }), + }); + const batchCall = addTransactionBatchMock.mock.calls[0][0]; + expect(batchCall.from).toBe(FROM_MOCK); expect(batchCall.transactions[0].params).toStrictEqual( expect.objectContaining({ - to: PAYMENT_OVERRIDE_TX_MOCK.to, - data: PAYMENT_OVERRIDE_TX_MOCK.data, - value: PAYMENT_OVERRIDE_TX_MOCK.value, + data: DELEGATION_DATA_MOCK, + to: DELEGATION_TO_MOCK, + value: DELEGATION_VALUE_MOCK, }), ); }); @@ -898,11 +923,12 @@ describe('Relay Submit Utils', () => { await submitRelayQuotes(request); + expect(getDelegationTransactionMock).not.toHaveBeenCalled(); expect(addTransactionBatchMock).not.toHaveBeenCalled(); expect(addTransactionMock).toHaveBeenCalledTimes(1); }); - it('defaults data to 0x when transaction data is absent', async () => { + it('defaults data to 0x when override tx data is absent', async () => { request.quotes[0].request.paymentOverride = PaymentOverride.MoneyAccount; const { data: _data, ...txWithoutData } = PAYMENT_OVERRIDE_TX_MOCK; @@ -910,11 +936,16 @@ describe('Relay Submit Utils', () => { await submitRelayQuotes(request); - const batchCall = addTransactionBatchMock.mock.calls[0][0]; - expect(batchCall.transactions[0].params.data).toBe('0x'); + expect(getDelegationTransactionMock).toHaveBeenCalledWith({ + transaction: expect.objectContaining({ + nestedTransactions: [ + expect.objectContaining({ data: '0x' }), + ], + }), + }); }); - it('defaults value to 0x0 when transaction value is absent', async () => { + it('defaults value to 0x0 when override tx value is absent', async () => { request.quotes[0].request.paymentOverride = PaymentOverride.MoneyAccount; const { value: _value, ...txWithoutValue } = PAYMENT_OVERRIDE_TX_MOCK; @@ -922,8 +953,13 @@ describe('Relay Submit Utils', () => { await submitRelayQuotes(request); - const batchCall = addTransactionBatchMock.mock.calls[0][0]; - expect(batchCall.transactions[0].params.value).toBe('0x0'); + expect(getDelegationTransactionMock).toHaveBeenCalledWith({ + transaction: expect.objectContaining({ + nestedTransactions: [ + expect.objectContaining({ value: '0x0' }), + ], + }), + }); }); }); diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts index a00237b2fa..39c62ae958 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts @@ -380,7 +380,7 @@ async function submitTransactions( // In post-quote flows (e.g. Predict withdraw), the source tokens are held in // the Safe — not the EOA — and only become available after the original tx // executes as part of the batch. Skip the EOA balance check here. - if (!quote.request.isPostQuote) { + if (!quote.request.isPostQuote && !quote.request.paymentOverride) { await validateSourceBalance(quote, messenger); } @@ -409,14 +409,28 @@ async function submitTransactions( ); if (overrideTxs.length) { - const overrideParams: TransactionParams[] = overrideTxs.map((tx) => ({ - data: (tx.data as Hex) ?? ('0x' as Hex), - from: tx.from, - to: tx.to, - value: (tx.value as Hex) ?? ('0x0' as Hex), - })); - - allParams = [...overrideParams, ...normalizedParams]; + const delegationTransaction = { + ...transaction, + nestedTransactions: overrideTxs.map((tx) => ({ + data: (tx.data ?? '0x') as Hex, + to: tx.to as Hex, + value: (tx.value ?? '0x0') as Hex, + })), + } as TransactionMeta; + + const delegation = await messenger.call( + 'TransactionPayController:getDelegationTransaction', + { transaction: delegationTransaction }, + ); + + const delegatedParams: TransactionParams = { + data: delegation.data, + from: quote.request.from, + to: delegation.to, + value: delegation.value, + }; + + allParams = [delegatedParams, ...normalizedParams]; } } else if (isPostQuote && transaction.txParams.to) { const prependedParams = hasAccountOverride From 26439e58dd2277b50d4f194fc9dbee62b7f532f0 Mon Sep 17 00:00:00 2001 From: Jyoti Puri Date: Wed, 27 May 2026 08:42:24 +0530 Subject: [PATCH 20/35] update --- .../src/TransactionPayController.test.ts | 8 +- .../src/TransactionPayController.ts | 2 +- .../src/strategy/relay/relay-submit.test.ts | 80 ++++--------------- .../src/strategy/relay/relay-submit.ts | 27 +------ .../src/tests/messenger-mock.ts | 2 +- .../transaction-pay-controller/src/types.ts | 7 +- 6 files changed, 28 insertions(+), 98 deletions(-) diff --git a/packages/transaction-pay-controller/src/TransactionPayController.test.ts b/packages/transaction-pay-controller/src/TransactionPayController.test.ts index 26d8d43039..566f3fff98 100644 --- a/packages/transaction-pay-controller/src/TransactionPayController.test.ts +++ b/packages/transaction-pay-controller/src/TransactionPayController.test.ts @@ -472,7 +472,7 @@ describe('TransactionPayController', () => { describe('getPaymentOverrideData', () => { it('delegates to the callback', async () => { const txMock = { from: '0xabc', to: '0xdef' }; - const getPaymentOverrideDataMock = jest.fn().mockResolvedValue([txMock]); + const getPaymentOverrideDataMock = jest.fn().mockResolvedValue(txMock); new TransactionPayController({ getDelegationTransaction: jest.fn(), @@ -488,10 +488,10 @@ describe('TransactionPayController', () => { expect(getPaymentOverrideDataMock).toHaveBeenCalledWith( TRANSACTION_ID_MOCK, ); - expect(result).toStrictEqual([txMock]); + expect(result).toStrictEqual(txMock); }); - it('returns empty array when no callback is configured', async () => { + it('returns undefined when no callback is configured', async () => { new TransactionPayController({ getDelegationTransaction: jest.fn(), messenger, @@ -502,7 +502,7 @@ describe('TransactionPayController', () => { TRANSACTION_ID_MOCK, ); - expect(result).toStrictEqual([]); + expect(result).toBeUndefined(); }); }); diff --git a/packages/transaction-pay-controller/src/TransactionPayController.ts b/packages/transaction-pay-controller/src/TransactionPayController.ts index b0d0eafb20..8361911e2f 100644 --- a/packages/transaction-pay-controller/src/TransactionPayController.ts +++ b/packages/transaction-pay-controller/src/TransactionPayController.ts @@ -237,7 +237,7 @@ export class TransactionPayController extends BaseController< getPaymentOverrideData( ...args: Parameters ): ReturnType { - return this.#getPaymentOverrideData?.(...args) ?? Promise.resolve([]); + return this.#getPaymentOverrideData?.(...args) ?? Promise.resolve(undefined); } /** diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts index adc718d425..7110db4973 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts @@ -1,6 +1,9 @@ import { ORIGIN_METAMASK } from '@metamask/controller-utils'; import { TransactionType } from '@metamask/transaction-controller'; -import type { TransactionMeta } from '@metamask/transaction-controller'; +import type { + TransactionMeta, + TransactionParams, +} from '@metamask/transaction-controller'; import type { Hex } from '@metamask/utils'; import { cloneDeep } from 'lodash'; @@ -864,48 +867,21 @@ describe('Relay Submit Utils', () => { to: '0xpaymentoverride' as Hex, data: '0xpaymentoverride' as Hex, value: '0x0', - }; - - const DELEGATION_TO_MOCK = '0xdelegationto' as Hex; - const DELEGATION_DATA_MOCK = '0xdelegationdata' as Hex; - const DELEGATION_VALUE_MOCK = '0x0' as Hex; + } as TransactionParams; - beforeEach(() => { - getDelegationTransactionMock.mockResolvedValue({ - data: DELEGATION_DATA_MOCK, - to: DELEGATION_TO_MOCK, - value: DELEGATION_VALUE_MOCK, - }); - }); - - it('builds delegation for override txs and prepends to submit params', async () => { + it('prepends override tx to submit params', async () => { request.quotes[0].request.paymentOverride = PaymentOverride.MoneyAccount; - getPaymentOverrideDataMock.mockResolvedValue([ - PAYMENT_OVERRIDE_TX_MOCK, - ]); + getPaymentOverrideDataMock.mockResolvedValue(PAYMENT_OVERRIDE_TX_MOCK); await submitRelayQuotes(request); - expect(getDelegationTransactionMock).toHaveBeenCalledWith({ - transaction: expect.objectContaining({ - nestedTransactions: [ - { - data: PAYMENT_OVERRIDE_TX_MOCK.data, - to: PAYMENT_OVERRIDE_TX_MOCK.to, - value: PAYMENT_OVERRIDE_TX_MOCK.value, - }, - ], - }), - }); - const batchCall = addTransactionBatchMock.mock.calls[0][0]; - expect(batchCall.from).toBe(FROM_MOCK); expect(batchCall.transactions[0].params).toStrictEqual( expect.objectContaining({ - data: DELEGATION_DATA_MOCK, - to: DELEGATION_TO_MOCK, - value: DELEGATION_VALUE_MOCK, + data: PAYMENT_OVERRIDE_TX_MOCK.data, + to: PAYMENT_OVERRIDE_TX_MOCK.to, + value: PAYMENT_OVERRIDE_TX_MOCK.value, }), ); }); @@ -916,50 +892,26 @@ describe('Relay Submit Utils', () => { expect(getPaymentOverrideDataMock).not.toHaveBeenCalled(); }); - it('does not prepend when callback returns empty array', async () => { + it('does not prepend when callback returns undefined', async () => { request.quotes[0].request.paymentOverride = PaymentOverride.MoneyAccount; - getPaymentOverrideDataMock.mockResolvedValue([]); + getPaymentOverrideDataMock.mockResolvedValue(undefined); await submitRelayQuotes(request); - expect(getDelegationTransactionMock).not.toHaveBeenCalled(); expect(addTransactionBatchMock).not.toHaveBeenCalled(); expect(addTransactionMock).toHaveBeenCalledTimes(1); }); - it('defaults data to 0x when override tx data is absent', async () => { - request.quotes[0].request.paymentOverride = - PaymentOverride.MoneyAccount; - const { data: _data, ...txWithoutData } = PAYMENT_OVERRIDE_TX_MOCK; - getPaymentOverrideDataMock.mockResolvedValue([txWithoutData]); - - await submitRelayQuotes(request); - - expect(getDelegationTransactionMock).toHaveBeenCalledWith({ - transaction: expect.objectContaining({ - nestedTransactions: [ - expect.objectContaining({ data: '0x' }), - ], - }), - }); - }); - - it('defaults value to 0x0 when override tx value is absent', async () => { + it('skips source balance validation', async () => { request.quotes[0].request.paymentOverride = PaymentOverride.MoneyAccount; - const { value: _value, ...txWithoutValue } = PAYMENT_OVERRIDE_TX_MOCK; - getPaymentOverrideDataMock.mockResolvedValue([txWithoutValue]); + getPaymentOverrideDataMock.mockResolvedValue(PAYMENT_OVERRIDE_TX_MOCK); + getLiveTokenBalanceMock.mockResolvedValue('0'); await submitRelayQuotes(request); - expect(getDelegationTransactionMock).toHaveBeenCalledWith({ - transaction: expect.objectContaining({ - nestedTransactions: [ - expect.objectContaining({ value: '0x0' }), - ], - }), - }); + expect(getLiveTokenBalanceMock).not.toHaveBeenCalled(); }); }); diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts index 39c62ae958..694246d36f 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts @@ -403,34 +403,13 @@ async function submitTransactions( let allParams = normalizedParams; if (quote.request.paymentOverride) { - const overrideTxs = await messenger.call( + const overrideTx = await messenger.call( 'TransactionPayController:getPaymentOverrideData', transaction.id, ); - if (overrideTxs.length) { - const delegationTransaction = { - ...transaction, - nestedTransactions: overrideTxs.map((tx) => ({ - data: (tx.data ?? '0x') as Hex, - to: tx.to as Hex, - value: (tx.value ?? '0x0') as Hex, - })), - } as TransactionMeta; - - const delegation = await messenger.call( - 'TransactionPayController:getDelegationTransaction', - { transaction: delegationTransaction }, - ); - - const delegatedParams: TransactionParams = { - data: delegation.data, - from: quote.request.from, - to: delegation.to, - value: delegation.value, - }; - - allParams = [delegatedParams, ...normalizedParams]; + if (overrideTx) { + allParams = [overrideTx, ...normalizedParams]; } } else if (isPostQuote && transaction.txParams.to) { const prependedParams = hasAccountOverride diff --git a/packages/transaction-pay-controller/src/tests/messenger-mock.ts b/packages/transaction-pay-controller/src/tests/messenger-mock.ts index 0a0892b0c1..66ce380e41 100644 --- a/packages/transaction-pay-controller/src/tests/messenger-mock.ts +++ b/packages/transaction-pay-controller/src/tests/messenger-mock.ts @@ -123,7 +123,7 @@ export function getMessengerMock({ const getPaymentOverrideDataMock: jest.MockedFn< TransactionPayControllerGetPaymentOverrideDataAction['handler'] - > = jest.fn().mockResolvedValue([]); + > = jest.fn().mockResolvedValue(undefined); const polymarketGetDepositWalletAddressMock: jest.MockedFn< TransactionPayControllerPolymarketGetDepositWalletAddressAction['handler'] diff --git a/packages/transaction-pay-controller/src/types.ts b/packages/transaction-pay-controller/src/types.ts index 119a005a80..3697708a03 100644 --- a/packages/transaction-pay-controller/src/types.ts +++ b/packages/transaction-pay-controller/src/types.ts @@ -154,13 +154,12 @@ export type TransactionConfig = { export type TransactionConfigCallback = (config: TransactionConfig) => void; /** - * Callback invoked during quote execution when `paymentOverride` is defined. - * Returns additional transactions to be submitted alongside the quote, - * ordered before or after the quote batch depending on `isPostQuote`. + * Callback invoked during submit when `paymentOverride` is defined. + * Returns a single delegation transaction to prepend to the submit batch. */ export type GetPaymentOverrideDataCallback = ( transactionId: string, -) => Promise; +) => Promise; /** Callback to update fiat payment state. */ export type TransactionFiatPaymentCallback = ( From 287808b784043aeb7dbdd921142e75d81326db04 Mon Sep 17 00:00:00 2001 From: Jyoti Puri Date: Wed, 27 May 2026 09:02:44 +0530 Subject: [PATCH 21/35] update --- .../src/TransactionPayController.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/transaction-pay-controller/src/TransactionPayController.ts b/packages/transaction-pay-controller/src/TransactionPayController.ts index 8361911e2f..7e1f637780 100644 --- a/packages/transaction-pay-controller/src/TransactionPayController.ts +++ b/packages/transaction-pay-controller/src/TransactionPayController.ts @@ -237,7 +237,9 @@ export class TransactionPayController extends BaseController< getPaymentOverrideData( ...args: Parameters ): ReturnType { - return this.#getPaymentOverrideData?.(...args) ?? Promise.resolve(undefined); + return ( + this.#getPaymentOverrideData?.(...args) ?? Promise.resolve(undefined) + ); } /** From 02c06951a242f2a2dcff054fbb5beddd5e5739ea Mon Sep 17 00:00:00 2001 From: Jyoti Puri Date: Wed, 27 May 2026 09:12:06 +0530 Subject: [PATCH 22/35] update --- .../src/TransactionPayController.test.ts | 2 +- .../src/strategy/relay/relay-submit.test.ts | 37 +++++++++++++------ .../src/strategy/relay/relay-submit.ts | 15 +++++++- .../transaction-pay-controller/src/types.ts | 6 +-- 4 files changed, 43 insertions(+), 17 deletions(-) diff --git a/packages/transaction-pay-controller/src/TransactionPayController.test.ts b/packages/transaction-pay-controller/src/TransactionPayController.test.ts index 566f3fff98..91df2e03bb 100644 --- a/packages/transaction-pay-controller/src/TransactionPayController.test.ts +++ b/packages/transaction-pay-controller/src/TransactionPayController.test.ts @@ -471,7 +471,7 @@ describe('TransactionPayController', () => { describe('getPaymentOverrideData', () => { it('delegates to the callback', async () => { - const txMock = { from: '0xabc', to: '0xdef' }; + const txMock = { id: 'override-id', txParams: { from: '0xabc', to: '0xdef' } }; const getPaymentOverrideDataMock = jest.fn().mockResolvedValue(txMock); new TransactionPayController({ diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts index 7110db4973..db2432d676 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts @@ -1,9 +1,6 @@ import { ORIGIN_METAMASK } from '@metamask/controller-utils'; import { TransactionType } from '@metamask/transaction-controller'; -import type { - TransactionMeta, - TransactionParams, -} from '@metamask/transaction-controller'; +import type { TransactionMeta } from '@metamask/transaction-controller'; import type { Hex } from '@metamask/utils'; import { cloneDeep } from 'lodash'; @@ -864,24 +861,39 @@ describe('Relay Submit Utils', () => { describe('paymentOverride flow', () => { const PAYMENT_OVERRIDE_TX_MOCK = { - to: '0xpaymentoverride' as Hex, - data: '0xpaymentoverride' as Hex, - value: '0x0', - } as TransactionParams; + id: 'override-tx-id', + txParams: { + from: FROM_MOCK, + to: '0xpaymentoverride' as Hex, + data: '0xpaymentoverride' as Hex, + value: '0x0', + }, + } as TransactionMeta; + + const DELEGATION_RESULT_MOCK = { + data: '0xdelegationdata' as Hex, + to: '0xdelegationto' as Hex, + value: '0xdelegationvalue' as Hex, + }; - it('prepends override tx to submit params', async () => { + it('builds delegation and prepends to submit params', async () => { request.quotes[0].request.paymentOverride = PaymentOverride.MoneyAccount; getPaymentOverrideDataMock.mockResolvedValue(PAYMENT_OVERRIDE_TX_MOCK); + getDelegationTransactionMock.mockResolvedValue(DELEGATION_RESULT_MOCK); await submitRelayQuotes(request); + expect(getDelegationTransactionMock).toHaveBeenCalledWith({ + transaction: PAYMENT_OVERRIDE_TX_MOCK, + }); + const batchCall = addTransactionBatchMock.mock.calls[0][0]; expect(batchCall.transactions[0].params).toStrictEqual( expect.objectContaining({ - data: PAYMENT_OVERRIDE_TX_MOCK.data, - to: PAYMENT_OVERRIDE_TX_MOCK.to, - value: PAYMENT_OVERRIDE_TX_MOCK.value, + data: DELEGATION_RESULT_MOCK.data, + to: DELEGATION_RESULT_MOCK.to, + value: DELEGATION_RESULT_MOCK.value, }), ); }); @@ -907,6 +919,7 @@ describe('Relay Submit Utils', () => { request.quotes[0].request.paymentOverride = PaymentOverride.MoneyAccount; getPaymentOverrideDataMock.mockResolvedValue(PAYMENT_OVERRIDE_TX_MOCK); + getDelegationTransactionMock.mockResolvedValue(DELEGATION_RESULT_MOCK); getLiveTokenBalanceMock.mockResolvedValue('0'); await submitRelayQuotes(request); diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts index 694246d36f..a401197026 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts @@ -409,7 +409,20 @@ async function submitTransactions( ); if (overrideTx) { - allParams = [overrideTx, ...normalizedParams]; + const delegation = await messenger.call( + 'TransactionPayController:getDelegationTransaction', + { transaction: overrideTx }, + ); + + allParams = [ + { + data: delegation.data, + from: overrideTx.txParams.from as Hex, + to: delegation.to, + value: delegation.value, + }, + ...normalizedParams, + ]; } } else if (isPostQuote && transaction.txParams.to) { const prependedParams = hasAccountOverride diff --git a/packages/transaction-pay-controller/src/types.ts b/packages/transaction-pay-controller/src/types.ts index 3697708a03..112432d5a8 100644 --- a/packages/transaction-pay-controller/src/types.ts +++ b/packages/transaction-pay-controller/src/types.ts @@ -54,7 +54,6 @@ import type { TransactionControllerStateChangeEvent, TransactionControllerUpdateTransactionAction, TransactionMeta, - TransactionParams, } from '@metamask/transaction-controller'; import type { Hex, Json } from '@metamask/utils'; import type { Draft } from 'immer'; @@ -155,11 +154,12 @@ export type TransactionConfigCallback = (config: TransactionConfig) => void; /** * Callback invoked during submit when `paymentOverride` is defined. - * Returns a single delegation transaction to prepend to the submit batch. + * Returns the override transaction metadata used to build a delegation + * transaction that is prepended to the submit batch. */ export type GetPaymentOverrideDataCallback = ( transactionId: string, -) => Promise; +) => Promise; /** Callback to update fiat payment state. */ export type TransactionFiatPaymentCallback = ( From d86d98a8fc4dc06bb9df7d1165a32545710408e3 Mon Sep 17 00:00:00 2001 From: Jyoti Puri Date: Wed, 27 May 2026 09:14:44 +0530 Subject: [PATCH 23/35] update --- .../src/TransactionPayController.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/transaction-pay-controller/src/TransactionPayController.test.ts b/packages/transaction-pay-controller/src/TransactionPayController.test.ts index 91df2e03bb..638d76f7c2 100644 --- a/packages/transaction-pay-controller/src/TransactionPayController.test.ts +++ b/packages/transaction-pay-controller/src/TransactionPayController.test.ts @@ -471,7 +471,10 @@ describe('TransactionPayController', () => { describe('getPaymentOverrideData', () => { it('delegates to the callback', async () => { - const txMock = { id: 'override-id', txParams: { from: '0xabc', to: '0xdef' } }; + const txMock = { + id: 'override-id', + txParams: { from: '0xabc', to: '0xdef' }, + }; const getPaymentOverrideDataMock = jest.fn().mockResolvedValue(txMock); new TransactionPayController({ From 3209a5d6c567db4a9cdcfa9a63f07d8d2889b7ee Mon Sep 17 00:00:00 2001 From: Jyoti Puri Date: Wed, 27 May 2026 16:00:57 +0530 Subject: [PATCH 24/35] update --- .../src/TransactionPayController.test.ts | 5 +-- .../src/strategy/relay/relay-submit.test.ts | 37 ++++++------------- .../src/strategy/relay/relay-submit.ts | 15 +------- .../transaction-pay-controller/src/types.ts | 3 +- 4 files changed, 16 insertions(+), 44 deletions(-) diff --git a/packages/transaction-pay-controller/src/TransactionPayController.test.ts b/packages/transaction-pay-controller/src/TransactionPayController.test.ts index 638d76f7c2..566f3fff98 100644 --- a/packages/transaction-pay-controller/src/TransactionPayController.test.ts +++ b/packages/transaction-pay-controller/src/TransactionPayController.test.ts @@ -471,10 +471,7 @@ describe('TransactionPayController', () => { describe('getPaymentOverrideData', () => { it('delegates to the callback', async () => { - const txMock = { - id: 'override-id', - txParams: { from: '0xabc', to: '0xdef' }, - }; + const txMock = { from: '0xabc', to: '0xdef' }; const getPaymentOverrideDataMock = jest.fn().mockResolvedValue(txMock); new TransactionPayController({ diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts index db2432d676..cbc4c5c1bd 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts @@ -1,6 +1,9 @@ import { ORIGIN_METAMASK } from '@metamask/controller-utils'; import { TransactionType } from '@metamask/transaction-controller'; -import type { TransactionMeta } from '@metamask/transaction-controller'; +import type { + TransactionMeta, + TransactionParams, +} from '@metamask/transaction-controller'; import type { Hex } from '@metamask/utils'; import { cloneDeep } from 'lodash'; @@ -861,39 +864,24 @@ describe('Relay Submit Utils', () => { describe('paymentOverride flow', () => { const PAYMENT_OVERRIDE_TX_MOCK = { - id: 'override-tx-id', - txParams: { - from: FROM_MOCK, - to: '0xpaymentoverride' as Hex, - data: '0xpaymentoverride' as Hex, - value: '0x0', - }, - } as TransactionMeta; - - const DELEGATION_RESULT_MOCK = { - data: '0xdelegationdata' as Hex, - to: '0xdelegationto' as Hex, - value: '0xdelegationvalue' as Hex, - }; + to: '0xpaymentoverride' as Hex, + data: '0xpaymentoverride' as Hex, + value: '0x0', + } as TransactionParams; - it('builds delegation and prepends to submit params', async () => { + it('prepends override tx params to submit batch', async () => { request.quotes[0].request.paymentOverride = PaymentOverride.MoneyAccount; getPaymentOverrideDataMock.mockResolvedValue(PAYMENT_OVERRIDE_TX_MOCK); - getDelegationTransactionMock.mockResolvedValue(DELEGATION_RESULT_MOCK); await submitRelayQuotes(request); - expect(getDelegationTransactionMock).toHaveBeenCalledWith({ - transaction: PAYMENT_OVERRIDE_TX_MOCK, - }); - const batchCall = addTransactionBatchMock.mock.calls[0][0]; expect(batchCall.transactions[0].params).toStrictEqual( expect.objectContaining({ - data: DELEGATION_RESULT_MOCK.data, - to: DELEGATION_RESULT_MOCK.to, - value: DELEGATION_RESULT_MOCK.value, + data: PAYMENT_OVERRIDE_TX_MOCK.data, + to: PAYMENT_OVERRIDE_TX_MOCK.to, + value: PAYMENT_OVERRIDE_TX_MOCK.value, }), ); }); @@ -919,7 +907,6 @@ describe('Relay Submit Utils', () => { request.quotes[0].request.paymentOverride = PaymentOverride.MoneyAccount; getPaymentOverrideDataMock.mockResolvedValue(PAYMENT_OVERRIDE_TX_MOCK); - getDelegationTransactionMock.mockResolvedValue(DELEGATION_RESULT_MOCK); getLiveTokenBalanceMock.mockResolvedValue('0'); await submitRelayQuotes(request); diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts index a401197026..694246d36f 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts @@ -409,20 +409,7 @@ async function submitTransactions( ); if (overrideTx) { - const delegation = await messenger.call( - 'TransactionPayController:getDelegationTransaction', - { transaction: overrideTx }, - ); - - allParams = [ - { - data: delegation.data, - from: overrideTx.txParams.from as Hex, - to: delegation.to, - value: delegation.value, - }, - ...normalizedParams, - ]; + allParams = [overrideTx, ...normalizedParams]; } } else if (isPostQuote && transaction.txParams.to) { const prependedParams = hasAccountOverride diff --git a/packages/transaction-pay-controller/src/types.ts b/packages/transaction-pay-controller/src/types.ts index 112432d5a8..2c94f0a290 100644 --- a/packages/transaction-pay-controller/src/types.ts +++ b/packages/transaction-pay-controller/src/types.ts @@ -54,6 +54,7 @@ import type { TransactionControllerStateChangeEvent, TransactionControllerUpdateTransactionAction, TransactionMeta, + TransactionParams, } from '@metamask/transaction-controller'; import type { Hex, Json } from '@metamask/utils'; import type { Draft } from 'immer'; @@ -159,7 +160,7 @@ export type TransactionConfigCallback = (config: TransactionConfig) => void; */ export type GetPaymentOverrideDataCallback = ( transactionId: string, -) => Promise; +) => Promise; /** Callback to update fiat payment state. */ export type TransactionFiatPaymentCallback = ( From aa8c59ebc9b31e0c2308393584769b33df5cbbc9 Mon Sep 17 00:00:00 2001 From: Jyoti Puri Date: Wed, 27 May 2026 19:44:29 +0530 Subject: [PATCH 25/35] update --- .../src/TransactionPayController.test.ts | 3 +++ .../src/strategy/relay/relay-submit.test.ts | 5 +++++ .../src/strategy/relay/relay-submit.ts | 1 + packages/transaction-pay-controller/src/types.ts | 1 + 4 files changed, 10 insertions(+) diff --git a/packages/transaction-pay-controller/src/TransactionPayController.test.ts b/packages/transaction-pay-controller/src/TransactionPayController.test.ts index 566f3fff98..e677c9fd58 100644 --- a/packages/transaction-pay-controller/src/TransactionPayController.test.ts +++ b/packages/transaction-pay-controller/src/TransactionPayController.test.ts @@ -483,10 +483,12 @@ describe('TransactionPayController', () => { const result = await messenger.call( 'TransactionPayController:getPaymentOverrideData', TRANSACTION_ID_MOCK, + '1.5', ); expect(getPaymentOverrideDataMock).toHaveBeenCalledWith( TRANSACTION_ID_MOCK, + '1.5', ); expect(result).toStrictEqual(txMock); }); @@ -500,6 +502,7 @@ describe('TransactionPayController', () => { const result = await messenger.call( 'TransactionPayController:getPaymentOverrideData', TRANSACTION_ID_MOCK, + '1.5', ); expect(result).toBeUndefined(); diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts index cbc4c5c1bd..a5e1630b83 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts @@ -876,6 +876,11 @@ describe('Relay Submit Utils', () => { await submitRelayQuotes(request); + expect(getPaymentOverrideDataMock).toHaveBeenCalledWith( + request.transaction.id, + request.quotes[0].sourceAmount.human, + ); + const batchCall = addTransactionBatchMock.mock.calls[0][0]; expect(batchCall.transactions[0].params).toStrictEqual( expect.objectContaining({ diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts index 694246d36f..77f41edcb7 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts @@ -406,6 +406,7 @@ async function submitTransactions( const overrideTx = await messenger.call( 'TransactionPayController:getPaymentOverrideData', transaction.id, + quote.sourceAmount.human, ); if (overrideTx) { diff --git a/packages/transaction-pay-controller/src/types.ts b/packages/transaction-pay-controller/src/types.ts index 2c94f0a290..bf591a786a 100644 --- a/packages/transaction-pay-controller/src/types.ts +++ b/packages/transaction-pay-controller/src/types.ts @@ -160,6 +160,7 @@ export type TransactionConfigCallback = (config: TransactionConfig) => void; */ export type GetPaymentOverrideDataCallback = ( transactionId: string, + amount: string, ) => Promise; /** Callback to update fiat payment state. */ From be8c99a275d2082bc456bdd83518b19fea5a45f3 Mon Sep 17 00:00:00 2001 From: Jyoti Puri Date: Wed, 27 May 2026 20:14:13 +0530 Subject: [PATCH 26/35] update --- .../src/strategy/relay/relay-submit.test.ts | 72 +++++++++++++++++++ .../src/strategy/relay/relay-submit.ts | 19 +++-- 2 files changed, 86 insertions(+), 5 deletions(-) diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts index a5e1630b83..f955e9e6a1 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts @@ -918,6 +918,78 @@ describe('Relay Submit Utils', () => { expect(getLiveTokenBalanceMock).not.toHaveBeenCalled(); }); + + it('assigns correct gas limits offset by override tx', async () => { + request.quotes[0].request.paymentOverride = + PaymentOverride.MoneyAccount; + request.quotes[0].original.metamask.gasLimits = [30000, 50000]; + + request.quotes[0].original.steps[0].items.push({ + ...request.quotes[0].original.steps[0].items[0], + data: { + ...request.quotes[0].original.steps[0].items[0].data, + data: '0xapprove' as Hex, + to: '0xapproveTarget' as Hex, + }, + }); + + getPaymentOverrideDataMock.mockResolvedValue(PAYMENT_OVERRIDE_TX_MOCK); + + await submitRelayQuotes(request); + + const { transactions } = addTransactionBatchMock.mock + .calls[0][0] as unknown as Record< + string, + { params: { gas?: string } }[] + >; + + expect(transactions).toHaveLength(3); + expect(transactions[0].params.gas).toBeUndefined(); + expect(transactions[1].params.gas).toBe('0x7530'); + expect(transactions[2].params.gas).toBe('0xc350'); + }); + + it('assigns correct transaction types with multi-step relay (approve + deposit)', async () => { + request.quotes[0].request.paymentOverride = + PaymentOverride.MoneyAccount; + request.transaction = { + ...request.transaction, + type: TransactionType.simpleSend, + } as TransactionMeta; + + request.quotes[0].original.steps[0].items.push({ + ...request.quotes[0].original.steps[0].items[0], + data: { + ...request.quotes[0].original.steps[0].items[0].data, + data: '0xapprove' as Hex, + to: '0xapproveTarget' as Hex, + }, + }); + + getPaymentOverrideDataMock.mockResolvedValue(PAYMENT_OVERRIDE_TX_MOCK); + + await submitRelayQuotes(request); + + const { transactions } = addTransactionBatchMock.mock + .calls[0][0] as unknown as Record; + + expect(transactions).toHaveLength(3); + expect(transactions[0]).toStrictEqual( + expect.objectContaining({ + type: TransactionType.simpleSend, + }), + ); + expect(transactions[1]).toStrictEqual( + expect.objectContaining({ + type: TransactionType.tokenMethodApprove, + }), + ); + expect(transactions[2]).toStrictEqual( + expect.objectContaining({ + type: TransactionType.relayDeposit, + }), + ); + }); }); describe('post-quote flow', () => { diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts index 77f41edcb7..eb89eb18d7 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts @@ -674,8 +674,13 @@ async function submitViaTransactionController( ? toHex(metamask.gasLimits[0]) : undefined; + const hasPaymentOverride = + Boolean(quote.request.paymentOverride) && + allParams.length > normalizedParams.length; + const transactions = allParams.map((singleParams, index) => { - const gasLimit = gasLimits[index]; + const relayIndex = hasPaymentOverride ? index - 1 : index; + const gasLimit = relayIndex >= 0 ? gasLimits[relayIndex] : undefined; const gas = gasLimit === undefined || gasLimit7702 ? undefined : toHex(gasLimit); @@ -690,6 +695,7 @@ async function submitViaTransactionController( }, type: getTransactionType( isPostQuote, + hasPaymentOverride, index, getEffectiveTransactionType(transaction), normalizedParams.length, @@ -744,17 +750,20 @@ async function submitViaTransactionController( */ function getTransactionType( isPostQuote: boolean | undefined, + hasPaymentOverride: boolean, index: number, originalType: TransactionMeta['type'], relayParamCount: number, ): TransactionMeta['type'] { - // Post-quote index 0 is the original transaction - if (isPostQuote && index === 0) { + const hasPrependedTx = isPostQuote || hasPaymentOverride; + + // Index 0 is the prepended transaction (original or override) + if (hasPrependedTx && index === 0) { return originalType; } - // Adjust index for post-quote flows where original tx is prepended - const relayIndex = isPostQuote ? index - 1 : index; + // Adjust index for flows where a transaction is prepended + const relayIndex = hasPrependedTx ? index - 1 : index; const depositType = getRelayDepositType(originalType); From 833255678744a40ad744d8a89408b228eba5f0a2 Mon Sep 17 00:00:00 2001 From: Jyoti Puri Date: Wed, 27 May 2026 20:22:48 +0530 Subject: [PATCH 27/35] update --- .../src/strategy/relay/relay-submit.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts index eb89eb18d7..83da5e88bd 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts @@ -743,9 +743,10 @@ async function submitViaTransactionController( * Determine the transaction type for a given index in the batch. * * @param isPostQuote - Whether this is a post-quote flow. + * @param hasPaymentOverride - Whether a payment override tx was prepended. * @param index - Index of the transaction in the batch. - * @param originalType - Type of the original transaction (used for post-quote index 0). - * @param relayParamCount - Number of relay-only params (excludes prepended original tx). + * @param originalType - Type of the original transaction (used for prepended index 0). + * @param relayParamCount - Number of relay-only params (excludes prepended tx). * @returns The transaction type. */ function getTransactionType( @@ -755,7 +756,7 @@ function getTransactionType( originalType: TransactionMeta['type'], relayParamCount: number, ): TransactionMeta['type'] { - const hasPrependedTx = isPostQuote || hasPaymentOverride; + const hasPrependedTx = isPostQuote ?? hasPaymentOverride; // Index 0 is the prepended transaction (original or override) if (hasPrependedTx && index === 0) { From 2fb76df46a991b71dd563561faadfb938872532a Mon Sep 17 00:00:00 2001 From: Jyoti Puri Date: Wed, 27 May 2026 22:00:19 +0530 Subject: [PATCH 28/35] update --- .../src/TransactionPayController.ts | 2 +- .../src/strategy/relay/relay-submit.test.ts | 24 +++--- .../src/strategy/relay/relay-submit.ts | 46 +++++++----- .../transaction-pay-controller/src/types.ts | 74 +++++++++++-------- 4 files changed, 85 insertions(+), 61 deletions(-) diff --git a/packages/transaction-pay-controller/src/TransactionPayController.ts b/packages/transaction-pay-controller/src/TransactionPayController.ts index 7e1f637780..5139a30501 100644 --- a/packages/transaction-pay-controller/src/TransactionPayController.ts +++ b/packages/transaction-pay-controller/src/TransactionPayController.ts @@ -238,7 +238,7 @@ export class TransactionPayController extends BaseController< ...args: Parameters ): ReturnType { return ( - this.#getPaymentOverrideData?.(...args) ?? Promise.resolve(undefined) + this.#getPaymentOverrideData?.(...args) ?? Promise.resolve([]) ); } diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts index f955e9e6a1..81d38927ab 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts @@ -1,8 +1,8 @@ import { ORIGIN_METAMASK } from '@metamask/controller-utils'; import { TransactionType } from '@metamask/transaction-controller'; import type { + BatchTransactionParams, TransactionMeta, - TransactionParams, } from '@metamask/transaction-controller'; import type { Hex } from '@metamask/utils'; import { cloneDeep } from 'lodash'; @@ -863,16 +863,18 @@ describe('Relay Submit Utils', () => { }); describe('paymentOverride flow', () => { - const PAYMENT_OVERRIDE_TX_MOCK = { + const PAYMENT_OVERRIDE_TX_MOCK: BatchTransactionParams = { to: '0xpaymentoverride' as Hex, data: '0xpaymentoverride' as Hex, - value: '0x0', - } as TransactionParams; + value: '0x0' as Hex, + }; it('prepends override tx params to submit batch', async () => { request.quotes[0].request.paymentOverride = PaymentOverride.MoneyAccount; - getPaymentOverrideDataMock.mockResolvedValue(PAYMENT_OVERRIDE_TX_MOCK); + getPaymentOverrideDataMock.mockResolvedValue([ + PAYMENT_OVERRIDE_TX_MOCK, + ]); await submitRelayQuotes(request); @@ -897,10 +899,10 @@ describe('Relay Submit Utils', () => { expect(getPaymentOverrideDataMock).not.toHaveBeenCalled(); }); - it('does not prepend when callback returns undefined', async () => { + it('does not prepend when callback returns empty array', async () => { request.quotes[0].request.paymentOverride = PaymentOverride.MoneyAccount; - getPaymentOverrideDataMock.mockResolvedValue(undefined); + getPaymentOverrideDataMock.mockResolvedValue([]); await submitRelayQuotes(request); @@ -911,7 +913,9 @@ describe('Relay Submit Utils', () => { it('skips source balance validation', async () => { request.quotes[0].request.paymentOverride = PaymentOverride.MoneyAccount; - getPaymentOverrideDataMock.mockResolvedValue(PAYMENT_OVERRIDE_TX_MOCK); + getPaymentOverrideDataMock.mockResolvedValue([ + PAYMENT_OVERRIDE_TX_MOCK, + ]); getLiveTokenBalanceMock.mockResolvedValue('0'); await submitRelayQuotes(request); @@ -933,7 +937,9 @@ describe('Relay Submit Utils', () => { }, }); - getPaymentOverrideDataMock.mockResolvedValue(PAYMENT_OVERRIDE_TX_MOCK); + getPaymentOverrideDataMock.mockResolvedValue([ + PAYMENT_OVERRIDE_TX_MOCK, + ]); await submitRelayQuotes(request); diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts index 83da5e88bd..44ad627cb5 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts @@ -403,14 +403,24 @@ async function submitTransactions( let allParams = normalizedParams; if (quote.request.paymentOverride) { - const overrideTx = await messenger.call( + const { transactionData } = messenger.call( + 'TransactionPayController:getState', + ); + + const overrideTxs = await messenger.call( 'TransactionPayController:getPaymentOverrideData', - transaction.id, - quote.sourceAmount.human, + { + amount: quote.sourceAmount.human, + transaction, + transactionData: transactionData[transaction.id], + }, ); - if (overrideTx) { - allParams = [overrideTx, ...normalizedParams]; + if (overrideTxs.length > 0) { + allParams = [ + ...(overrideTxs as TransactionParams[]), + ...normalizedParams, + ]; } } else if (isPostQuote && transaction.txParams.to) { const prependedParams = hasAccountOverride @@ -674,12 +684,12 @@ async function submitViaTransactionController( ? toHex(metamask.gasLimits[0]) : undefined; - const hasPaymentOverride = - Boolean(quote.request.paymentOverride) && - allParams.length > normalizedParams.length; + const overrideCount = quote.request.paymentOverride + ? allParams.length - normalizedParams.length + : 0; const transactions = allParams.map((singleParams, index) => { - const relayIndex = hasPaymentOverride ? index - 1 : index; + const relayIndex = overrideCount > 0 ? index - overrideCount : index; const gasLimit = relayIndex >= 0 ? gasLimits[relayIndex] : undefined; const gas = gasLimit === undefined || gasLimit7702 ? undefined : toHex(gasLimit); @@ -695,7 +705,7 @@ async function submitViaTransactionController( }, type: getTransactionType( isPostQuote, - hasPaymentOverride, + overrideCount, index, getEffectiveTransactionType(transaction), normalizedParams.length, @@ -743,28 +753,26 @@ async function submitViaTransactionController( * Determine the transaction type for a given index in the batch. * * @param isPostQuote - Whether this is a post-quote flow. - * @param hasPaymentOverride - Whether a payment override tx was prepended. + * @param overrideCount - Number of payment override txs prepended. * @param index - Index of the transaction in the batch. - * @param originalType - Type of the original transaction (used for prepended index 0). - * @param relayParamCount - Number of relay-only params (excludes prepended tx). + * @param originalType - Type of the original transaction (used for prepended indices). + * @param relayParamCount - Number of relay-only params (excludes prepended txs). * @returns The transaction type. */ function getTransactionType( isPostQuote: boolean | undefined, - hasPaymentOverride: boolean, + overrideCount: number, index: number, originalType: TransactionMeta['type'], relayParamCount: number, ): TransactionMeta['type'] { - const hasPrependedTx = isPostQuote ?? hasPaymentOverride; + const prependCount = isPostQuote ? 1 : overrideCount; - // Index 0 is the prepended transaction (original or override) - if (hasPrependedTx && index === 0) { + if (prependCount > 0 && index < prependCount) { return originalType; } - // Adjust index for flows where a transaction is prepended - const relayIndex = hasPrependedTx ? index - 1 : index; + const relayIndex = index - prependCount; const depositType = getRelayDepositType(originalType); diff --git a/packages/transaction-pay-controller/src/types.ts b/packages/transaction-pay-controller/src/types.ts index bf591a786a..b5b7aaabb2 100644 --- a/packages/transaction-pay-controller/src/types.ts +++ b/packages/transaction-pay-controller/src/types.ts @@ -48,13 +48,13 @@ import type { } from '@metamask/transaction-controller'; import type { BatchTransaction, + BatchTransactionParams, TransactionControllerAddTransactionAction, TransactionControllerGetGasFeeTokensAction, TransactionControllerGetStateAction, TransactionControllerStateChangeEvent, TransactionControllerUpdateTransactionAction, TransactionMeta, - TransactionParams, } from '@metamask/transaction-controller'; import type { Hex, Json } from '@metamask/utils'; import type { Draft } from 'immer'; @@ -110,6 +110,13 @@ export type TransactionPayControllerGetStateAction = ControllerGetStateAction< /** Configurable properties of a transaction. */ export type TransactionConfig = { + /** + * Optional address to override the default account used by the transaction. + * When `isPostQuote` is true, used as the recipient of the MM Pay transfer. + * When `isPostQuote` is false, it provides the funds and pays for gas. + */ + accountOverride?: Hex; + /** * Whether the source of funds is HyperLiquid (HyperCore). * When true, the Relay strategy uses the HyperLiquid 2-step withdrawal @@ -130,6 +137,9 @@ export type TransactionConfig = { */ isPostQuote?: boolean; + /** Overrides the payment source for the transaction. */ + paymentOverride?: PaymentOverride; + /** * Optional address to receive refunds if the quote provider transaction fails. * When set, overrides the default refund recipient (EOA) in the quote @@ -138,30 +148,30 @@ export type TransactionConfig = { * go back to that account rather than the EOA. */ refundTo?: Hex; - - /** - * Optional address to override the default account used by the transaction. - * When `isPostQuote` is true, used as the recipient of the MM Pay transfer. - * When `isPostQuote` is false, it provides the funds and pays for gas. - */ - accountOverride?: Hex; - - /** Overrides the payment source for the transaction. */ - paymentOverride?: PaymentOverride; }; /** Callback to update transaction config. */ export type TransactionConfigCallback = (config: TransactionConfig) => void; +/** Request passed to {@link GetPaymentOverrideDataCallback}. */ +export type GetPaymentOverrideDataRequest = { + /** Amount of the source token in human-readable format. */ + amount: string; + + /** Metadata of the original transaction. */ + transaction: TransactionMeta; + + /** Pay-controller state for the transaction. */ + transactionData: TransactionData; +}; + /** * Callback invoked during submit when `paymentOverride` is defined. - * Returns the override transaction metadata used to build a delegation - * transaction that is prepended to the submit batch. + * Returns batch transaction params to prepend to the submit batch. */ export type GetPaymentOverrideDataCallback = ( - transactionId: string, - amount: string, -) => Promise; + request: GetPaymentOverrideDataRequest, +) => Promise; /** Callback to update fiat payment state. */ export type TransactionFiatPaymentCallback = ( @@ -202,6 +212,12 @@ export type TransactionPayControllerOptions = { /** Callback to convert a transaction into a redeem delegation. */ getDelegationTransaction: GetDelegationTransactionCallback; + /** + * Optional callback invoked during quote execution when `paymentOverride` is defined. + * Returns additional transactions to be submitted alongside the quote batch. + */ + getPaymentOverrideData?: GetPaymentOverrideDataCallback; + /** Callback to select the PayStrategy for a transaction. */ getStrategy?: (transaction: TransactionMeta) => TransactionPayStrategy; @@ -214,12 +230,6 @@ export type TransactionPayControllerOptions = { /** Callbacks for the Polymarket relayer; required only for the Polymarket deposit-wallet flow. */ polymarket?: PolymarketCallbacks; - /** - * Optional callback invoked during quote execution when `paymentOverride` is defined. - * Returns additional transactions to be submitted alongside the quote batch. - */ - getPaymentOverrideData?: GetPaymentOverrideDataCallback; - /** Initial state of the controller. */ state?: Partial; }; @@ -232,6 +242,13 @@ export type TransactionPayControllerState = { /** State relating to a single transaction. */ export type TransactionData = { + /** + * Optional address to override the default account used by the transaction. + * When `isPostQuote` is true, used as the recipient of the MM Pay transfer. + * When `isPostQuote` is false, it provides the funds and pays for gas. + */ + accountOverride?: Hex; + /** Fiat payment method state. */ fiatPayment?: TransactionFiatPayment; @@ -256,6 +273,9 @@ export type TransactionData = { /** Whether the source of funds is a Polymarket deposit wallet. */ isPolymarketDepositWallet?: boolean; + /** Overrides the payment source for the transaction. */ + paymentOverride?: PaymentOverride; + /** * Optional address to receive refunds if the quote provider transaction fails. * When set, overrides the default refund recipient (EOA) in the quote @@ -263,16 +283,6 @@ export type TransactionData = { */ refundTo?: Hex; - /** - * Optional address to override the default account used by the transaction. - * When `isPostQuote` is true, used as the recipient of the MM Pay transfer. - * When `isPostQuote` is false, it provides the funds and pays for gas. - */ - accountOverride?: Hex; - - /** Overrides the payment source for the transaction. */ - paymentOverride?: PaymentOverride; - /** * Token selected for the transaction. * - For standard flows (isPostQuote=false): This is the SOURCE/payment token From c2afcb478cae367e1a88a2b634898097526b8912 Mon Sep 17 00:00:00 2001 From: Jyoti Puri Date: Wed, 27 May 2026 22:08:13 +0530 Subject: [PATCH 29/35] update --- .../src/TransactionPayController.test.ts | 33 +++++++----- .../src/TransactionPayController.ts | 3 +- .../src/strategy/relay/relay-quotes.ts | 53 +++++++++++++------ .../src/strategy/relay/relay-submit.test.ts | 27 ++++++++-- .../src/tests/messenger-mock.ts | 2 +- 5 files changed, 82 insertions(+), 36 deletions(-) diff --git a/packages/transaction-pay-controller/src/TransactionPayController.test.ts b/packages/transaction-pay-controller/src/TransactionPayController.test.ts index e677c9fd58..e0e7e7fd6e 100644 --- a/packages/transaction-pay-controller/src/TransactionPayController.test.ts +++ b/packages/transaction-pay-controller/src/TransactionPayController.test.ts @@ -471,8 +471,10 @@ describe('TransactionPayController', () => { describe('getPaymentOverrideData', () => { it('delegates to the callback', async () => { - const txMock = { from: '0xabc', to: '0xdef' }; - const getPaymentOverrideDataMock = jest.fn().mockResolvedValue(txMock); + const resultMock = [{ to: '0xdef' as const, data: '0xabc' as const }]; + const getPaymentOverrideDataMock = jest + .fn() + .mockResolvedValue(resultMock); new TransactionPayController({ getDelegationTransaction: jest.fn(), @@ -480,20 +482,22 @@ describe('TransactionPayController', () => { messenger, }); + const requestMock = { + amount: '1.5', + transaction: TRANSACTION_META_MOCK, + transactionData: { isLoading: false, tokens: [] }, + }; + const result = await messenger.call( 'TransactionPayController:getPaymentOverrideData', - TRANSACTION_ID_MOCK, - '1.5', + requestMock, ); - expect(getPaymentOverrideDataMock).toHaveBeenCalledWith( - TRANSACTION_ID_MOCK, - '1.5', - ); - expect(result).toStrictEqual(txMock); + expect(getPaymentOverrideDataMock).toHaveBeenCalledWith(requestMock); + expect(result).toStrictEqual(resultMock); }); - it('returns undefined when no callback is configured', async () => { + it('returns empty array when no callback is configured', async () => { new TransactionPayController({ getDelegationTransaction: jest.fn(), messenger, @@ -501,11 +505,14 @@ describe('TransactionPayController', () => { const result = await messenger.call( 'TransactionPayController:getPaymentOverrideData', - TRANSACTION_ID_MOCK, - '1.5', + { + amount: '1.5', + transaction: TRANSACTION_META_MOCK, + transactionData: { isLoading: false, tokens: [] }, + }, ); - expect(result).toBeUndefined(); + expect(result).toStrictEqual([]); }); }); diff --git a/packages/transaction-pay-controller/src/TransactionPayController.ts b/packages/transaction-pay-controller/src/TransactionPayController.ts index 5139a30501..aa2d2e65b1 100644 --- a/packages/transaction-pay-controller/src/TransactionPayController.ts +++ b/packages/transaction-pay-controller/src/TransactionPayController.ts @@ -230,8 +230,7 @@ export class TransactionPayController extends BaseController< * Called during quote execution when `paymentOverride` is defined on the transaction. * Returns an empty array when no callback is configured. * - * @param args - The arguments forwarded to the {@link GetPaymentOverrideDataCallback}, - * containing the transaction ID. + * @param args - The arguments forwarded to the {@link GetPaymentOverrideDataCallback}. * @returns A promise resolving to the additional transactions array. */ getPaymentOverrideData( diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts index 08077cd823..bf31e4d2e6 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts @@ -697,9 +697,7 @@ async function calculateSourceNetworkCost( ); const { gasLimits, is7702, totalGasEstimate, totalGasLimit } = - request.isPostQuote - ? combinePostQuoteGas(relayOnlyGas, transaction) - : relayOnlyGas; + combinePrependedGas(relayOnlyGas, request, transaction); log('Gas limit', { is7702, @@ -897,6 +895,25 @@ function toRelayQuoteGasTransaction( }; } +type RelayGasResult = { + totalGasEstimate: number; + totalGasLimit: number; + gasLimits: number[]; + is7702: boolean; +}; + +function combinePrependedGas( + relayOnlyGas: RelayGasResult, + request: QuoteRequest, + transaction: TransactionMeta, +): RelayGasResult { + const gas = request.isPostQuote + ? combinePostQuoteGas(relayOnlyGas, transaction) + : relayOnlyGas; + + return request.paymentOverride ? addPaymentOverrideGas(gas) : gas; +} + /** * Combine the original transaction's gas with relay gas for post-quote flows. * @@ -913,19 +930,9 @@ function toRelayQuoteGasTransaction( * @returns Combined gas estimates including the original transaction. */ function combinePostQuoteGas( - relayGas: { - totalGasEstimate: number; - totalGasLimit: number; - gasLimits: number[]; - is7702: boolean; - }, + relayGas: RelayGasResult, transaction: TransactionMeta, -): { - totalGasEstimate: number; - totalGasLimit: number; - gasLimits: number[]; - is7702: boolean; -} { +): RelayGasResult { const nestedGas = transaction.nestedTransactions?.find((tx) => tx.gas)?.gas; const rawGas = nestedGas ?? transaction.txParams.gas; const originalTxGas = rawGas ? new BigNumber(rawGas).toNumber() : undefined; @@ -964,6 +971,22 @@ function combinePostQuoteGas( }; } +// Hardcoded gas allowance for the prepended payment override transaction(s). +const PAYMENT_OVERRIDE_GAS = 75_000; + +function addPaymentOverrideGas(relayGas: RelayGasResult): RelayGasResult { + const gasLimits = relayGas.is7702 + ? [relayGas.gasLimits[0] + PAYMENT_OVERRIDE_GAS] + : [PAYMENT_OVERRIDE_GAS, ...relayGas.gasLimits]; + + return { + totalGasEstimate: relayGas.totalGasEstimate + PAYMENT_OVERRIDE_GAS, + totalGasLimit: relayGas.totalGasLimit + PAYMENT_OVERRIDE_GAS, + gasLimits, + is7702: relayGas.is7702, + }; +} + /** * Calculate the provider fee for a Relay quote. * diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts index 81d38927ab..4cc3e97c50 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts @@ -144,6 +144,7 @@ describe('Relay Submit Utils', () => { const { addTransactionMock, addTransactionBatchMock, + getControllerStateMock, getDelegationTransactionMock, findNetworkClientIdByChainIdMock, getPaymentOverrideDataMock, @@ -863,12 +864,25 @@ describe('Relay Submit Utils', () => { }); describe('paymentOverride flow', () => { + const TRANSACTION_DATA_MOCK = { + isLoading: false, + tokens: [], + }; + const PAYMENT_OVERRIDE_TX_MOCK: BatchTransactionParams = { to: '0xpaymentoverride' as Hex, data: '0xpaymentoverride' as Hex, value: '0x0' as Hex, }; + beforeEach(() => { + getControllerStateMock.mockReturnValue({ + transactionData: { + [ORIGINAL_TRANSACTION_ID_MOCK]: TRANSACTION_DATA_MOCK, + }, + }); + }); + it('prepends override tx params to submit batch', async () => { request.quotes[0].request.paymentOverride = PaymentOverride.MoneyAccount; @@ -878,10 +892,11 @@ describe('Relay Submit Utils', () => { await submitRelayQuotes(request); - expect(getPaymentOverrideDataMock).toHaveBeenCalledWith( - request.transaction.id, - request.quotes[0].sourceAmount.human, - ); + expect(getPaymentOverrideDataMock).toHaveBeenCalledWith({ + amount: request.quotes[0].sourceAmount.human, + transaction: request.transaction, + transactionData: TRANSACTION_DATA_MOCK, + }); const batchCall = addTransactionBatchMock.mock.calls[0][0]; expect(batchCall.transactions[0].params).toStrictEqual( @@ -972,7 +987,9 @@ describe('Relay Submit Utils', () => { }, }); - getPaymentOverrideDataMock.mockResolvedValue(PAYMENT_OVERRIDE_TX_MOCK); + getPaymentOverrideDataMock.mockResolvedValue([ + PAYMENT_OVERRIDE_TX_MOCK, + ]); await submitRelayQuotes(request); diff --git a/packages/transaction-pay-controller/src/tests/messenger-mock.ts b/packages/transaction-pay-controller/src/tests/messenger-mock.ts index 66ce380e41..0a0892b0c1 100644 --- a/packages/transaction-pay-controller/src/tests/messenger-mock.ts +++ b/packages/transaction-pay-controller/src/tests/messenger-mock.ts @@ -123,7 +123,7 @@ export function getMessengerMock({ const getPaymentOverrideDataMock: jest.MockedFn< TransactionPayControllerGetPaymentOverrideDataAction['handler'] - > = jest.fn().mockResolvedValue(undefined); + > = jest.fn().mockResolvedValue([]); const polymarketGetDepositWalletAddressMock: jest.MockedFn< TransactionPayControllerPolymarketGetDepositWalletAddressAction['handler'] From 9f70f714d5c5a287da15d4b7967609f657907e3e Mon Sep 17 00:00:00 2001 From: Jyoti Puri Date: Wed, 27 May 2026 22:13:03 +0530 Subject: [PATCH 30/35] update --- .../src/TransactionPayController.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/transaction-pay-controller/src/TransactionPayController.ts b/packages/transaction-pay-controller/src/TransactionPayController.ts index aa2d2e65b1..0e4b189622 100644 --- a/packages/transaction-pay-controller/src/TransactionPayController.ts +++ b/packages/transaction-pay-controller/src/TransactionPayController.ts @@ -236,9 +236,7 @@ export class TransactionPayController extends BaseController< getPaymentOverrideData( ...args: Parameters ): ReturnType { - return ( - this.#getPaymentOverrideData?.(...args) ?? Promise.resolve([]) - ); + return this.#getPaymentOverrideData?.(...args) ?? Promise.resolve([]); } /** From d54d178931b4d18c0b586ef063d0fc13b86b3890 Mon Sep 17 00:00:00 2001 From: Jyoti Puri Date: Wed, 27 May 2026 22:16:24 +0530 Subject: [PATCH 31/35] update --- .../src/strategy/relay/relay-quotes.test.ts | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts index 9439faf4cc..c20aa399dc 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts @@ -14,6 +14,7 @@ import { CHAIN_ID_HYPERCORE, CHAIN_ID_POLYGON, NATIVE_TOKEN_ADDRESS, + PaymentOverride, POLYGON_USDCE_ADDRESS, } from '../../constants'; import { getMessengerMock } from '../../tests/messenger-mock'; @@ -2648,6 +2649,78 @@ describe('Relay Quotes Utils', () => { expect(result[0].original.metamask.gasLimits).toStrictEqual([]); }); + + it('adds extra gas when paymentOverride is set', async () => { + successfulFetchMock.mockResolvedValue({ + ok: true, + json: async () => QUOTE_MOCK, + } as never); + + const result = await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [ + { + ...QUOTE_REQUEST_MOCK, + paymentOverride: PaymentOverride.MoneyAccount, + }, + ], + transaction: TRANSACTION_META_MOCK, + }); + + expect(calculateGasCostMock).toHaveBeenCalledWith( + expect.objectContaining({ gas: 21000 + 75000 }), + ); + + expect(result[0].original.metamask.gasLimits).toStrictEqual([ + 75000, 21000, + ]); + }); + + it('adds extra gas to combined 7702 limit when paymentOverride is set', async () => { + const multiStepQuote = { + ...QUOTE_MOCK, + steps: [ + { + ...STEP_MOCK, + items: [ + STEP_MOCK.items[0], + { + ...STEP_MOCK.items[0], + data: { ...STEP_MOCK.items[0].data, gas: '30000' }, + }, + ], + }, + ], + }; + + successfulFetchMock.mockResolvedValue({ + ok: true, + json: async () => multiStepQuote, + } as never); + + estimateGasBatchMock.mockResolvedValue({ + totalGasLimit: 51000, + gasLimits: [51000], + }); + + const result = await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [ + { + ...QUOTE_REQUEST_MOCK, + paymentOverride: PaymentOverride.MoneyAccount, + }, + ], + transaction: TRANSACTION_META_MOCK, + }); + + expect(result[0].original.metamask.gasLimits).toStrictEqual([ + 51000 + 75000, + ]); + expect(result[0].original.metamask.is7702).toBe(true); + }); }); describe('HyperLiquid source (isHyperliquidSource)', () => { From 6444b39b5e74d59b709e2cfd2fcb174d4cd9aabf Mon Sep 17 00:00:00 2001 From: Jyoti Puri Date: Wed, 27 May 2026 22:21:12 +0530 Subject: [PATCH 32/35] update --- .../src/TransactionPayController-method-action-types.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 501df919c7..3eb0b69758 100644 --- a/packages/transaction-pay-controller/src/TransactionPayController-method-action-types.ts +++ b/packages/transaction-pay-controller/src/TransactionPayController-method-action-types.ts @@ -73,8 +73,7 @@ export type TransactionPayControllerGetDelegationTransactionAction = { * Called during quote execution when `paymentOverride` is defined on the transaction. * Returns an empty array when no callback is configured. * - * @param args - The arguments forwarded to the {@link GetPaymentOverrideDataCallback}, - * containing the transaction ID. + * @param args - The arguments forwarded to the {@link GetPaymentOverrideDataCallback}. * @returns A promise resolving to the additional transactions array. */ export type TransactionPayControllerGetPaymentOverrideDataAction = { From 12757b5e8af89798cb847d5edb282e19bd8b5c23 Mon Sep 17 00:00:00 2001 From: Jyoti Puri Date: Thu, 28 May 2026 14:56:44 +0530 Subject: [PATCH 33/35] update --- .../src/TransactionPayController.test.ts | 6 ++-- .../src/TransactionPayController.ts | 5 +++- .../transaction-pay-controller/src/index.ts | 2 ++ .../src/strategy/relay/relay-quotes.ts | 6 ++-- .../src/strategy/relay/relay-submit.test.ts | 30 +++++++++---------- .../src/strategy/relay/relay-submit.ts | 5 ++-- .../src/tests/messenger-mock.ts | 2 +- .../transaction-pay-controller/src/types.ts | 8 ++++- 8 files changed, 38 insertions(+), 26 deletions(-) diff --git a/packages/transaction-pay-controller/src/TransactionPayController.test.ts b/packages/transaction-pay-controller/src/TransactionPayController.test.ts index e0e7e7fd6e..b7d2fc2b9c 100644 --- a/packages/transaction-pay-controller/src/TransactionPayController.test.ts +++ b/packages/transaction-pay-controller/src/TransactionPayController.test.ts @@ -471,7 +471,9 @@ describe('TransactionPayController', () => { describe('getPaymentOverrideData', () => { it('delegates to the callback', async () => { - const resultMock = [{ to: '0xdef' as const, data: '0xabc' as const }]; + const resultMock = { + calls: [{ to: '0xdef' as const, data: '0xabc' as const }], + }; const getPaymentOverrideDataMock = jest .fn() .mockResolvedValue(resultMock); @@ -512,7 +514,7 @@ describe('TransactionPayController', () => { }, ); - expect(result).toStrictEqual([]); + expect(result).toStrictEqual({ calls: [] }); }); }); diff --git a/packages/transaction-pay-controller/src/TransactionPayController.ts b/packages/transaction-pay-controller/src/TransactionPayController.ts index 0e4b189622..e55b24efa8 100644 --- a/packages/transaction-pay-controller/src/TransactionPayController.ts +++ b/packages/transaction-pay-controller/src/TransactionPayController.ts @@ -236,7 +236,10 @@ export class TransactionPayController extends BaseController< getPaymentOverrideData( ...args: Parameters ): ReturnType { - return this.#getPaymentOverrideData?.(...args) ?? Promise.resolve([]); + return ( + this.#getPaymentOverrideData?.(...args) ?? + Promise.resolve({ calls: [] }) + ); } /** diff --git a/packages/transaction-pay-controller/src/index.ts b/packages/transaction-pay-controller/src/index.ts index 44cd88e82a..9b0b113627 100644 --- a/packages/transaction-pay-controller/src/index.ts +++ b/packages/transaction-pay-controller/src/index.ts @@ -1,4 +1,6 @@ export type { + GetPaymentOverrideDataRequest, + GetPaymentOverrideDataResponse, TransactionConfig, TransactionConfigCallback, TransactionData, diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts index bf31e4d2e6..4d1bb5fca2 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts @@ -65,6 +65,9 @@ import type { const log = createModuleLogger(projectLogger, 'relay-strategy'); +// Hardcoded gas allowance for the prepended payment override transaction(s). +const PAYMENT_OVERRIDE_GAS = 75_000; + /** * Fetches Relay quotes. * @@ -971,9 +974,6 @@ function combinePostQuoteGas( }; } -// Hardcoded gas allowance for the prepended payment override transaction(s). -const PAYMENT_OVERRIDE_GAS = 75_000; - function addPaymentOverrideGas(relayGas: RelayGasResult): RelayGasResult { const gasLimits = relayGas.is7702 ? [relayGas.gasLimits[0] + PAYMENT_OVERRIDE_GAS] diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts index 4cc3e97c50..37d9a71818 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts @@ -886,9 +886,9 @@ describe('Relay Submit Utils', () => { it('prepends override tx params to submit batch', async () => { request.quotes[0].request.paymentOverride = PaymentOverride.MoneyAccount; - getPaymentOverrideDataMock.mockResolvedValue([ - PAYMENT_OVERRIDE_TX_MOCK, - ]); + getPaymentOverrideDataMock.mockResolvedValue({ + calls: [PAYMENT_OVERRIDE_TX_MOCK], + }); await submitRelayQuotes(request); @@ -917,7 +917,7 @@ describe('Relay Submit Utils', () => { it('does not prepend when callback returns empty array', async () => { request.quotes[0].request.paymentOverride = PaymentOverride.MoneyAccount; - getPaymentOverrideDataMock.mockResolvedValue([]); + getPaymentOverrideDataMock.mockResolvedValue({ calls: [] }); await submitRelayQuotes(request); @@ -928,9 +928,9 @@ describe('Relay Submit Utils', () => { it('skips source balance validation', async () => { request.quotes[0].request.paymentOverride = PaymentOverride.MoneyAccount; - getPaymentOverrideDataMock.mockResolvedValue([ - PAYMENT_OVERRIDE_TX_MOCK, - ]); + getPaymentOverrideDataMock.mockResolvedValue({ + calls: [PAYMENT_OVERRIDE_TX_MOCK], + }); getLiveTokenBalanceMock.mockResolvedValue('0'); await submitRelayQuotes(request); @@ -938,10 +938,10 @@ describe('Relay Submit Utils', () => { expect(getLiveTokenBalanceMock).not.toHaveBeenCalled(); }); - it('assigns correct gas limits offset by override tx', async () => { + it('assigns correct gas limits with override tx', async () => { request.quotes[0].request.paymentOverride = PaymentOverride.MoneyAccount; - request.quotes[0].original.metamask.gasLimits = [30000, 50000]; + request.quotes[0].original.metamask.gasLimits = [10000, 30000, 50000]; request.quotes[0].original.steps[0].items.push({ ...request.quotes[0].original.steps[0].items[0], @@ -952,9 +952,9 @@ describe('Relay Submit Utils', () => { }, }); - getPaymentOverrideDataMock.mockResolvedValue([ - PAYMENT_OVERRIDE_TX_MOCK, - ]); + getPaymentOverrideDataMock.mockResolvedValue({ + calls: [PAYMENT_OVERRIDE_TX_MOCK], + }); await submitRelayQuotes(request); @@ -965,9 +965,9 @@ describe('Relay Submit Utils', () => { >; expect(transactions).toHaveLength(3); - expect(transactions[0].params.gas).toBeUndefined(); - expect(transactions[1].params.gas).toBe('0x7530'); - expect(transactions[2].params.gas).toBe('0xc350'); + expect(transactions[0].params.gas).toBe('0x7530'); + expect(transactions[1].params.gas).toBe('0xc350'); + expect(transactions[2].params.gas).toBeUndefined(); }); it('assigns correct transaction types with multi-step relay (approve + deposit)', async () => { diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts index 44ad627cb5..2ec4986994 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts @@ -407,7 +407,7 @@ async function submitTransactions( 'TransactionPayController:getState', ); - const overrideTxs = await messenger.call( + const { calls: overrideTxs } = await messenger.call( 'TransactionPayController:getPaymentOverrideData', { amount: quote.sourceAmount.human, @@ -689,8 +689,7 @@ async function submitViaTransactionController( : 0; const transactions = allParams.map((singleParams, index) => { - const relayIndex = overrideCount > 0 ? index - overrideCount : index; - const gasLimit = relayIndex >= 0 ? gasLimits[relayIndex] : undefined; + const gasLimit = gasLimits[index]; const gas = gasLimit === undefined || gasLimit7702 ? undefined : toHex(gasLimit); diff --git a/packages/transaction-pay-controller/src/tests/messenger-mock.ts b/packages/transaction-pay-controller/src/tests/messenger-mock.ts index 0a0892b0c1..294cb1bc2b 100644 --- a/packages/transaction-pay-controller/src/tests/messenger-mock.ts +++ b/packages/transaction-pay-controller/src/tests/messenger-mock.ts @@ -123,7 +123,7 @@ export function getMessengerMock({ const getPaymentOverrideDataMock: jest.MockedFn< TransactionPayControllerGetPaymentOverrideDataAction['handler'] - > = jest.fn().mockResolvedValue([]); + > = jest.fn().mockResolvedValue({ calls: [] }); const polymarketGetDepositWalletAddressMock: jest.MockedFn< TransactionPayControllerPolymarketGetDepositWalletAddressAction['handler'] diff --git a/packages/transaction-pay-controller/src/types.ts b/packages/transaction-pay-controller/src/types.ts index b5b7aaabb2..8dad3a1a4b 100644 --- a/packages/transaction-pay-controller/src/types.ts +++ b/packages/transaction-pay-controller/src/types.ts @@ -165,13 +165,19 @@ export type GetPaymentOverrideDataRequest = { transactionData: TransactionData; }; +/** Response returned by {@link GetPaymentOverrideDataCallback}. */ +export type GetPaymentOverrideDataResponse = { + /** Batch transaction params to prepend to the submit batch. */ + calls: BatchTransactionParams[]; +}; + /** * Callback invoked during submit when `paymentOverride` is defined. * Returns batch transaction params to prepend to the submit batch. */ export type GetPaymentOverrideDataCallback = ( request: GetPaymentOverrideDataRequest, -) => Promise; +) => Promise; /** Callback to update fiat payment state. */ export type TransactionFiatPaymentCallback = ( From 7c064a994a41091cd33c06bfbf812f19de8e7601 Mon Sep 17 00:00:00 2001 From: Jyoti Puri Date: Thu, 28 May 2026 15:01:40 +0530 Subject: [PATCH 34/35] update --- .../src/TransactionPayController.ts | 3 +-- .../src/strategy/relay/relay-submit.test.ts | 12 ++++++------ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/packages/transaction-pay-controller/src/TransactionPayController.ts b/packages/transaction-pay-controller/src/TransactionPayController.ts index e55b24efa8..cc6e9f7a3f 100644 --- a/packages/transaction-pay-controller/src/TransactionPayController.ts +++ b/packages/transaction-pay-controller/src/TransactionPayController.ts @@ -237,8 +237,7 @@ export class TransactionPayController extends BaseController< ...args: Parameters ): ReturnType { return ( - this.#getPaymentOverrideData?.(...args) ?? - Promise.resolve({ calls: [] }) + this.#getPaymentOverrideData?.(...args) ?? Promise.resolve({ calls: [] }) ); } diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts index 37d9a71818..c43dfb22e6 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts @@ -965,9 +965,9 @@ describe('Relay Submit Utils', () => { >; expect(transactions).toHaveLength(3); - expect(transactions[0].params.gas).toBe('0x7530'); - expect(transactions[1].params.gas).toBe('0xc350'); - expect(transactions[2].params.gas).toBeUndefined(); + expect(transactions[0].params.gas).toBe('0x2710'); + expect(transactions[1].params.gas).toBe('0x7530'); + expect(transactions[2].params.gas).toBe('0xc350'); }); it('assigns correct transaction types with multi-step relay (approve + deposit)', async () => { @@ -987,9 +987,9 @@ describe('Relay Submit Utils', () => { }, }); - getPaymentOverrideDataMock.mockResolvedValue([ - PAYMENT_OVERRIDE_TX_MOCK, - ]); + getPaymentOverrideDataMock.mockResolvedValue({ + calls: [PAYMENT_OVERRIDE_TX_MOCK], + }); await submitRelayQuotes(request); From 31a81521c28545fe2270dd4ec39b80a9ff0f2c99 Mon Sep 17 00:00:00 2001 From: Jyoti Puri Date: Thu, 28 May 2026 15:09:03 +0530 Subject: [PATCH 35/35] update --- .../src/strategy/relay/relay-submit.ts | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts index 2ec4986994..dfece5307f 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts @@ -684,9 +684,7 @@ async function submitViaTransactionController( ? toHex(metamask.gasLimits[0]) : undefined; - const overrideCount = quote.request.paymentOverride - ? allParams.length - normalizedParams.length - : 0; + const prependCount = allParams.length - normalizedParams.length; const transactions = allParams.map((singleParams, index) => { const gasLimit = gasLimits[index]; @@ -703,8 +701,7 @@ async function submitViaTransactionController( value: singleParams.value as Hex, }, type: getTransactionType( - isPostQuote, - overrideCount, + prependCount, index, getEffectiveTransactionType(transaction), normalizedParams.length, @@ -751,22 +748,18 @@ async function submitViaTransactionController( /** * Determine the transaction type for a given index in the batch. * - * @param isPostQuote - Whether this is a post-quote flow. - * @param overrideCount - Number of payment override txs prepended. + * @param prependCount - Number of non-relay txs prepended to the batch. * @param index - Index of the transaction in the batch. * @param originalType - Type of the original transaction (used for prepended indices). * @param relayParamCount - Number of relay-only params (excludes prepended txs). * @returns The transaction type. */ function getTransactionType( - isPostQuote: boolean | undefined, - overrideCount: number, + prependCount: number, index: number, originalType: TransactionMeta['type'], relayParamCount: number, ): TransactionMeta['type'] { - const prependCount = isPostQuote ? 1 : overrideCount; - if (prependCount > 0 && index < prependCount) { return originalType; }