diff --git a/packages/transaction-pay-controller/CHANGELOG.md b/packages/transaction-pay-controller/CHANGELOG.md index f1e60db0bd..dc46544f20 100644 --- a/packages/transaction-pay-controller/CHANGELOG.md +++ b/packages/transaction-pay-controller/CHANGELOG.md @@ -45,6 +45,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Abort in-flight quote requests when a newer request is made for the same transaction, preventing stale responses from overwriting newer ones ([#8612](https://github.com/MetaMask/core/pull/8612)) - Bump `@metamask/messenger` from `^1.1.1` to `^1.2.0` ([#8632](https://github.com/MetaMask/core/pull/8632)) - Bump `@metamask/network-controller` from `^30.0.1` to `^30.1.0` ([#8636](https://github.com/MetaMask/core/pull/8636)) +- 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.1] 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 f93a005c20..eb47e5a6a0 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 @@ -1013,6 +1013,73 @@ 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('passes the original transaction through to getDelegationTransaction', async () => { + await submitRelayQuotes(request); + + expect(getDelegationTransactionMock).toHaveBeenCalledTimes(1); + expect(getDelegationTransactionMock).toHaveBeenCalledWith({ + transaction: expect.objectContaining({ + id: ORIGINAL_TRANSACTION_ID_MOCK, + txParams: expect.objectContaining({ + from: FROM_MOCK, + to: '0xrecipient', + data: '0xorigdata', + value: '0x100', + }), + }), + }); + }); + + 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 0082b6af66..53165cb4af 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(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,38 @@ 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. + * + * The original tx is already on the correct chain and from the money + * account, so it can be passed through to `getDelegationTransaction` + * unchanged. + * + * @param transaction - Original transaction meta to be redeemed. + * @param messenger - Controller messenger. + * @returns Transaction params for the delegation tx. + */ +async function buildDelegatedOriginalParams( + transaction: TransactionMeta, + messenger: TransactionPayControllerMessenger, +): Promise { + const delegation = await messenger.call( + 'TransactionPayController:getDelegationTransaction', + { transaction }, + ); + + log('Delegation result for prepended original tx', delegation); + + return { + data: delegation.data, + from: transaction.txParams.from as Hex, + to: delegation.to, + value: delegation.value, + }; +} + /** * Submit source transactions via Relay's /execute endpoint. *