Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/transaction-pay-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,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]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -989,6 +989,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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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<TransactionParams> {
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.
*
Expand Down
Loading