From f559f67c75cea36ed8557a0657adf5d20627cb27 Mon Sep 17 00:00:00 2001 From: Jyoti Puri Date: Tue, 28 Apr 2026 17:02:19 +0530 Subject: [PATCH 1/6] fix: transaction execution when accountOverride is present for postquote transaction --- .../src/strategy/relay/relay-submit.test.ts | 72 ++++++++++++++++ .../src/strategy/relay/relay-submit.ts | 85 ++++++++++++++++--- 2 files changed, 144 insertions(+), 13 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 afc32821ff..36e060ecaf 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 @@ -989,6 +989,78 @@ describe('Relay Submit Utils', () => { ); }); + describe('with accountOverride', () => { + const ACCOUNT_OVERRIDE_MOCK = '0xaccountOverride' as Hex; + const DELEGATION_TO_MOCK = '0xdelegationManager' as Hex; + const DELEGATION_DATA_MOCK = '0xdelegationdata' as Hex; + const DELEGATION_VALUE_MOCK = '0x0' as Hex; + + beforeEach(() => { + request.quotes[0].request.from = ACCOUNT_OVERRIDE_MOCK; + getDelegationTransactionMock.mockResolvedValue({ + data: DELEGATION_DATA_MOCK, + to: DELEGATION_TO_MOCK, + value: DELEGATION_VALUE_MOCK, + }); + }); + + it('builds the prepended tx via getDelegationTransaction with the original call as the only nested tx', async () => { + await submitRelayQuotes(request); + + expect(getDelegationTransactionMock).toHaveBeenCalledTimes(1); + expect(getDelegationTransactionMock).toHaveBeenCalledWith({ + transaction: expect.objectContaining({ + chainId: CHAIN_ID_MOCK, + networkClientId: NETWORK_CLIENT_ID_MOCK, + nestedTransactions: [ + { + data: '0xorigdata', + to: '0xrecipient', + value: '0x100', + }, + ], + txParams: expect.objectContaining({ + from: ACCOUNT_OVERRIDE_MOCK, + }), + }), + }); + }); + + it('uses the delegation result as the first batch tx', async () => { + await submitRelayQuotes(request); + + expect(addTransactionBatchMock).toHaveBeenCalledTimes(1); + expect(addTransactionBatchMock).toHaveBeenCalledWith( + expect.objectContaining({ + from: ACCOUNT_OVERRIDE_MOCK, + transactions: [ + expect.objectContaining({ + params: expect.objectContaining({ + data: DELEGATION_DATA_MOCK, + to: DELEGATION_TO_MOCK, + value: DELEGATION_VALUE_MOCK, + }), + type: TransactionType.simpleSend, + }), + expect.objectContaining({ + params: expect.objectContaining({ + data: '0x1234', + to: '0xfedcb', + }), + type: TransactionType.relayDeposit, + }), + ], + }), + ); + }); + }); + + it('does not call getDelegationTransaction when accountOverride is not set', async () => { + await submitRelayQuotes(request); + + expect(getDelegationTransactionMock).not.toHaveBeenCalled(); + }); + it('activates 7702 mode with single combined post-quote gas limit', async () => { request.quotes[0].original.metamask.gasLimits = [203093]; request.quotes[0].original.metamask.is7702 = 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 9694ce83a9..df261f8c1e 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts @@ -343,20 +343,29 @@ async function submitTransactions( // For post-quote flows, prepend the original transaction so it gets // included in the batch alongside the relay deposit(s). // This always results in multiple params, so it takes the batch path. + // When an accountOverride is set (detected by `from` divergence between the + // quote and the original tx), the override account does not directly hold + // the funds for the original call, so the prepended tx is replaced with a + // delegation tx that redeems the original call on its behalf. const { isPostQuote } = quote.request; - - const allParams = - isPostQuote && transaction.txParams.to - ? [ - { - data: transaction.txParams.data as Hex | undefined, - from: transaction.txParams.from, - to: transaction.txParams.to, - value: transaction.txParams.value as Hex | undefined, - } as TransactionParams, - ...normalizedParams, - ] - : normalizedParams; + const hasAccountOverride = + quote.request.from.toLowerCase() !== + (transaction.txParams.from as Hex).toLowerCase(); + + let allParams = normalizedParams; + + if (isPostQuote && transaction.txParams.to) { + const prependedParams = hasAccountOverride + ? await buildDelegatedOriginalParams(quote, transaction, messenger) + : ({ + data: transaction.txParams.data as Hex | undefined, + from: transaction.txParams.from, + to: transaction.txParams.to, + value: transaction.txParams.value as Hex | undefined, + } as TransactionParams); + + allParams = [prependedParams, ...normalizedParams]; + } if (quote.original.metamask.isExecute) { return await submitViaRelayExecute( @@ -376,6 +385,56 @@ async function submitTransactions( ); } +/** + * Build TransactionParams for a delegation that redeems the original + * post-quote transaction on behalf of the override account. Used when the + * override account cannot execute the original call directly. + * + * @param quote - Relay quote (used for source chain + override `from`). + * @param transaction - Original transaction meta to be redeemed. + * @param messenger - Controller messenger. + * @returns Transaction params for the delegation tx. + */ +async function buildDelegatedOriginalParams( + quote: TransactionPayQuote, + transaction: TransactionMeta, + messenger: TransactionPayControllerMessenger, +): Promise { + const { from, sourceChainId } = quote.request; + + const networkClientId = messenger.call( + 'NetworkController:findNetworkClientIdByChainId', + sourceChainId, + ); + + const sourceCallTransaction = { + ...transaction, + chainId: sourceChainId, + networkClientId, + nestedTransactions: [ + { + data: (transaction.txParams.data ?? '0x') as Hex, + to: transaction.txParams.to as Hex, + value: (transaction.txParams.value ?? '0x0') as Hex, + }, + ], + } as TransactionMeta; + + const delegation = await messenger.call( + 'TransactionPayController:getDelegationTransaction', + { transaction: sourceCallTransaction }, + ); + + log('Delegation result for prepended original tx', delegation); + + return { + data: delegation.data, + from, + to: delegation.to, + value: delegation.value, + }; +} + /** * Submit source transactions via Relay's /execute endpoint. * From 872b555ff10c2cbbe4c8fd71aa8717673f82671f Mon Sep 17 00:00:00 2001 From: Jyoti Puri Date: Tue, 28 Apr 2026 17:15:14 +0530 Subject: [PATCH 2/6] update --- .../src/strategy/relay/relay-submit.test.ts | 17 ++++------- .../src/strategy/relay/relay-submit.ts | 28 +++++-------------- 2 files changed, 13 insertions(+), 32 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 36e060ecaf..3dbf4611e6 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 @@ -1004,23 +1004,18 @@ describe('Relay Submit Utils', () => { }); }); - it('builds the prepended tx via getDelegationTransaction with the original call as the only nested tx', async () => { + it('passes the original transaction through to getDelegationTransaction', async () => { await submitRelayQuotes(request); expect(getDelegationTransactionMock).toHaveBeenCalledTimes(1); expect(getDelegationTransactionMock).toHaveBeenCalledWith({ transaction: expect.objectContaining({ - chainId: CHAIN_ID_MOCK, - networkClientId: NETWORK_CLIENT_ID_MOCK, - nestedTransactions: [ - { - data: '0xorigdata', - to: '0xrecipient', - value: '0x100', - }, - ], + id: ORIGINAL_TRANSACTION_ID_MOCK, txParams: expect.objectContaining({ - from: ACCOUNT_OVERRIDE_MOCK, + from: FROM_MOCK, + to: '0xrecipient', + data: '0xorigdata', + value: '0x100', }), }), }); 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 df261f8c1e..8fcfb7682f 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts @@ -390,7 +390,11 @@ async function submitTransactions( * post-quote transaction on behalf of the override account. Used when the * override account cannot execute the original call directly. * - * @param quote - Relay quote (used for source chain + override `from`). + * The original tx is already on the correct chain and from the money + * account, so it can be passed through to `getDelegationTransaction` + * unchanged. + * + * @param quote - Relay quote (used for the override `from`). * @param transaction - Original transaction meta to be redeemed. * @param messenger - Controller messenger. * @returns Transaction params for the delegation tx. @@ -400,29 +404,11 @@ async function buildDelegatedOriginalParams( transaction: TransactionMeta, messenger: TransactionPayControllerMessenger, ): Promise { - const { from, sourceChainId } = quote.request; - - const networkClientId = messenger.call( - 'NetworkController:findNetworkClientIdByChainId', - sourceChainId, - ); - - const sourceCallTransaction = { - ...transaction, - chainId: sourceChainId, - networkClientId, - nestedTransactions: [ - { - data: (transaction.txParams.data ?? '0x') as Hex, - to: transaction.txParams.to as Hex, - value: (transaction.txParams.value ?? '0x0') as Hex, - }, - ], - } as TransactionMeta; + const { from } = quote.request; const delegation = await messenger.call( 'TransactionPayController:getDelegationTransaction', - { transaction: sourceCallTransaction }, + { transaction }, ); log('Delegation result for prepended original tx', delegation); From 70cdcadfede7c637da835e5d324ede74d0de8aa6 Mon Sep 17 00:00:00 2001 From: Jyoti Puri Date: Tue, 28 Apr 2026 17:21:06 +0530 Subject: [PATCH 3/6] update --- .../src/strategy/relay/relay-submit.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 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 8fcfb7682f..2bba4de006 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts @@ -356,7 +356,7 @@ async function submitTransactions( if (isPostQuote && transaction.txParams.to) { const prependedParams = hasAccountOverride - ? await buildDelegatedOriginalParams(quote, transaction, messenger) + ? await buildDelegatedOriginalParams(transaction, messenger) : ({ data: transaction.txParams.data as Hex | undefined, from: transaction.txParams.from, @@ -394,18 +394,14 @@ async function submitTransactions( * account, so it can be passed through to `getDelegationTransaction` * unchanged. * - * @param quote - Relay quote (used for the override `from`). * @param transaction - Original transaction meta to be redeemed. * @param messenger - Controller messenger. * @returns Transaction params for the delegation tx. */ async function buildDelegatedOriginalParams( - quote: TransactionPayQuote, transaction: TransactionMeta, messenger: TransactionPayControllerMessenger, ): Promise { - const { from } = quote.request; - const delegation = await messenger.call( 'TransactionPayController:getDelegationTransaction', { transaction }, @@ -415,7 +411,7 @@ async function buildDelegatedOriginalParams( return { data: delegation.data, - from, + from: transaction.txParams.from as Hex, to: delegation.to, value: delegation.value, }; From 561cc9c65ce5ce461952a5329d37d7478077ac5a Mon Sep 17 00:00:00 2001 From: Jyoti Puri Date: Tue, 28 Apr 2026 17:32:28 +0530 Subject: [PATCH 4/6] update --- packages/transaction-pay-controller/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/transaction-pay-controller/CHANGELOG.md b/packages/transaction-pay-controller/CHANGELOG.md index aa8d790388..3bb739feda 100644 --- a/packages/transaction-pay-controller/CHANGELOG.md +++ b/packages/transaction-pay-controller/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Fall back from Across to later pay strategies when Across quotes would require a first-time EIP-7702 authorization list ([#8577](https://github.com/MetaMask/core/pull/8577)) +- Fix post-quote relay submission when `accountOverride` is set by replacing the prepended original transaction with a delegation transaction so the override account can submit it ([#8615](https://github.com/MetaMask/core/pull/8615)) - **BREAKING:** Fix mUSD conversion for hardware wallets on EIP-7702 chains by gating relay and Across 7702 paths on the account keyring type via `KeyringController:getState` ([#8388](https://github.com/MetaMask/core/pull/8388)) - The `TransactionPayControllerMessenger` now requires `KeyringController:getState` permission. From 3366f1b541dbe260298c2d1d5877f43f571ca441 Mon Sep 17 00:00:00 2001 From: Jyoti Puri Date: Tue, 28 Apr 2026 17:55:36 +0530 Subject: [PATCH 5/6] update --- packages/transaction-pay-controller/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/transaction-pay-controller/CHANGELOG.md b/packages/transaction-pay-controller/CHANGELOG.md index e9cdba09c1..697d0b5c8b 100644 --- a/packages/transaction-pay-controller/CHANGELOG.md +++ b/packages/transaction-pay-controller/CHANGELOG.md @@ -18,9 +18,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Fall back from Across to later pay strategies when Across quotes would require a first-time EIP-7702 authorization list ([#8577](https://github.com/MetaMask/core/pull/8577)) -- Fix post-quote relay submission when `accountOverride` is set by replacing the prepended original transaction with a delegation transaction so the override account can submit it ([#8615](https://github.com/MetaMask/core/pull/8615)) - **BREAKING:** Fix mUSD conversion for hardware wallets on EIP-7702 chains by gating relay and Across 7702 paths on the account keyring type via `KeyringController:getState` ([#8388](https://github.com/MetaMask/core/pull/8388)) - The `TransactionPayControllerMessenger` now requires `KeyringController:getState` permission. +- Fix post-quote relay submission when `accountOverride` is set by replacing the prepended original transaction with a delegation transaction so the override account can submit it ([#8615](https://github.com/MetaMask/core/pull/8615)) ## [19.3.0] From 90cfbe66b1eff4234ce15a5dd7a8c3b9cb780f19 Mon Sep 17 00:00:00 2001 From: Jyoti Puri Date: Tue, 28 Apr 2026 19:02:19 +0530 Subject: [PATCH 6/6] update --- packages/transaction-pay-controller/CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/transaction-pay-controller/CHANGELOG.md b/packages/transaction-pay-controller/CHANGELOG.md index 697d0b5c8b..47237976e8 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] +### Fixed + +- Fix post-quote relay submission when `accountOverride` is set by replacing the prepended original transaction with a delegation transaction so the override account can submit it ([#8615](https://github.com/MetaMask/core/pull/8615)) + ## [20.0.0] ### Changed @@ -20,7 +24,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fall back from Across to later pay strategies when Across quotes would require a first-time EIP-7702 authorization list ([#8577](https://github.com/MetaMask/core/pull/8577)) - **BREAKING:** Fix mUSD conversion for hardware wallets on EIP-7702 chains by gating relay and Across 7702 paths on the account keyring type via `KeyringController:getState` ([#8388](https://github.com/MetaMask/core/pull/8388)) - The `TransactionPayControllerMessenger` now requires `KeyringController:getState` permission. -- Fix post-quote relay submission when `accountOverride` is set by replacing the prepended original transaction with a delegation transaction so the override account can submit it ([#8615](https://github.com/MetaMask/core/pull/8615)) ## [19.3.0]