Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
5bfa16c
feat: add accountOverride to override default recipient or delegator …
jpuri Apr 14, 2026
1d0bce3
update
jpuri Apr 15, 2026
7f9ce88
update
jpuri Apr 15, 2026
a969dd0
merge
jpuri Apr 15, 2026
f6a4071
Merge branch 'main' into acc_override_support
jpuri Apr 15, 2026
7a28240
update
jpuri Apr 15, 2026
dae645e
Merge branch 'acc_override_support' of https://github.com/MetaMask/co…
jpuri Apr 15, 2026
db25703
update
jpuri Apr 15, 2026
868b17d
update
jpuri Apr 15, 2026
68f6ac3
update
jpuri Apr 15, 2026
f83bf0f
update
jpuri Apr 15, 2026
bd9f3a0
update
jpuri Apr 15, 2026
1b7f538
update
jpuri Apr 15, 2026
45a11d7
update
jpuri Apr 15, 2026
7c350b3
Merge branch 'main' into acc_override_support
jpuri Apr 15, 2026
7e3d54f
Merge branch 'main' into acc_override_support
jpuri Apr 16, 2026
0b893bb
update
jpuri Apr 16, 2026
ef7089e
Merge branch 'acc_override_support' of https://github.com/MetaMask/co…
jpuri Apr 16, 2026
d6d0050
Merge branch 'main' into acc_override_support
jpuri Apr 16, 2026
350ff2c
update
jpuri Apr 16, 2026
2a0467b
Merge branch 'acc_override_support' of https://github.com/MetaMask/co…
jpuri Apr 16, 2026
ac28b25
update
jpuri Apr 17, 2026
9a55495
Merge branch 'main' into acc_override_support
jpuri Apr 17, 2026
14693e5
update
jpuri Apr 17, 2026
193cc90
Merge branch 'acc_override_support' of https://github.com/MetaMask/co…
jpuri Apr 17, 2026
722493b
merge
jpuri Apr 20, 2026
fe7577d
update
jpuri Apr 22, 2026
f526d8c
Merge branch 'main' into acc_override_support
jpuri Apr 22, 2026
d2a8ad4
update
jpuri Apr 22, 2026
d770b87
Merge branch 'acc_override_support' of https://github.com/MetaMask/co…
jpuri Apr 22, 2026
2383954
Merge branch 'main' into acc_override_support
jpuri Apr 22, 2026
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
4 changes: 4 additions & 0 deletions packages/transaction-pay-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Add support for transaction config parameter accountOverride ([#8454](https://github.com/MetaMask/core/pull/8454))

## [19.2.2]

### Changed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,26 @@ describe('TransactionPayController', () => {
expect(updateQuotesMock).toHaveBeenCalledTimes(1);
});

it('triggers source amounts and quotes update when accountOverride changes', () => {
const controller = createController();
const accountOverride =
'0xdeadbeef00000000000000000000000000000002' as Hex;

controller.setTransactionConfig(TRANSACTION_ID_MOCK, () => {
// no-op, just initializes
});

updateSourceAmountsMock.mockClear();
updateQuotesMock.mockClear();

controller.setTransactionConfig(TRANSACTION_ID_MOCK, (config) => {
config.accountOverride = accountOverride;
});

expect(updateSourceAmountsMock).toHaveBeenCalledTimes(1);
expect(updateQuotesMock).toHaveBeenCalledTimes(1);
});

it('updates refundTo in state', () => {
const controller = createController();
const refundTo = '0xdeadbeef00000000000000000000000000000001' as Hex;
Expand Down Expand Up @@ -212,6 +232,102 @@ describe('TransactionPayController', () => {
).toBeUndefined();
});

it('updates accountOverride in state', () => {
const controller = createController();
const accountOverride =
'0xdeadbeef00000000000000000000000000000002' as Hex;

controller.setTransactionConfig(TRANSACTION_ID_MOCK, (config) => {
config.accountOverride = accountOverride;
});

expect(
controller.state.transactionData[TRANSACTION_ID_MOCK].accountOverride,
).toBe(accountOverride);
});

it('clears paymentToken when accountOverride changes', () => {
const controller = createController();
const accountOverride =
'0xdeadbeef00000000000000000000000000000002' as Hex;

controller.updatePaymentToken({
transactionId: TRANSACTION_ID_MOCK,
tokenAddress: TOKEN_ADDRESS_MOCK,
chainId: CHAIN_ID_MOCK,
});

const { updateTransactionData } = updatePaymentTokenMock.mock.calls[0][1];

updateTransactionData(TRANSACTION_ID_MOCK, (data) => {
data.paymentToken = {
address: TOKEN_ADDRESS_MOCK,
balanceFiat: '1',
balanceHuman: '1',
balanceRaw: '1',
balanceUsd: '1',
chainId: CHAIN_ID_MOCK,
decimals: 6,
symbol: 'USDC',
};
});

expect(
controller.state.transactionData[TRANSACTION_ID_MOCK].paymentToken,
).toBeDefined();

controller.setTransactionConfig(TRANSACTION_ID_MOCK, (config) => {
config.accountOverride = accountOverride;
});

expect(
controller.state.transactionData[TRANSACTION_ID_MOCK].paymentToken,
).toBeUndefined();
});

it('does not clear paymentToken when accountOverride is unchanged', () => {
const controller = createController();
const accountOverride =
'0xdeadbeef00000000000000000000000000000002' as Hex;

controller.setTransactionConfig(TRANSACTION_ID_MOCK, (config) => {
config.accountOverride = accountOverride;
});

controller.updatePaymentToken({
transactionId: TRANSACTION_ID_MOCK,
tokenAddress: TOKEN_ADDRESS_MOCK,
chainId: CHAIN_ID_MOCK,
});

const { updateTransactionData } = updatePaymentTokenMock.mock.calls[0][1];

updateTransactionData(TRANSACTION_ID_MOCK, (data) => {
data.paymentToken = {
address: TOKEN_ADDRESS_MOCK,
balanceFiat: '1',
balanceHuman: '1',
balanceRaw: '1',
balanceUsd: '1',
chainId: CHAIN_ID_MOCK,
decimals: 6,
symbol: 'USDC',
};
});

expect(
controller.state.transactionData[TRANSACTION_ID_MOCK].paymentToken,
).toBeDefined();

controller.setTransactionConfig(TRANSACTION_ID_MOCK, (config) => {
config.accountOverride = accountOverride;
});

expect(
controller.state.transactionData[TRANSACTION_ID_MOCK].paymentToken,
).toBeDefined();
});

it('updates multiple config properties at once', () => {
const controller = createController();
const refundTo = '0xdeadbeef00000000000000000000000000000001' as Hex;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,14 +119,22 @@ export class TransactionPayController extends BaseController<
isPostQuote: transactionData.isPostQuote,
isHyperliquidSource: transactionData.isHyperliquidSource,
refundTo: transactionData.refundTo,
accountOverride: transactionData.accountOverride,
};

const previousAccountOverride = config.accountOverride;

callback(config);

transactionData.accountOverride = config.accountOverride;
Comment thread
cursor[bot] marked this conversation as resolved.
transactionData.isMaxAmount = config.isMaxAmount;
transactionData.isPostQuote = config.isPostQuote;
transactionData.isHyperliquidSource = config.isHyperliquidSource;
transactionData.refundTo = config.refundTo;

if (config.accountOverride !== previousAccountOverride) {
transactionData.paymentToken = undefined;
}
});
}

Expand Down Expand Up @@ -215,6 +223,7 @@ export class TransactionPayController extends BaseController<
const originalTokens = current?.tokens;
const originalIsMaxAmount = current?.isMaxAmount;
const originalIsPostQuote = current?.isPostQuote;
const originalAccountOverride = current?.accountOverride;

if (!current) {
transactionData[transactionId] = {
Expand All @@ -236,12 +245,15 @@ export class TransactionPayController extends BaseController<
const isTokensUpdated = current.tokens !== originalTokens;
const isIsMaxUpdated = current.isMaxAmount !== originalIsMaxAmount;
const isPostQuoteUpdated = current.isPostQuote !== originalIsPostQuote;
const isAccountOverrideUpdated =
current.accountOverride !== originalAccountOverride;

if (
isPaymentTokenUpdated ||
isIsMaxUpdated ||
isTokensUpdated ||
isPostQuoteUpdated
isPostQuoteUpdated ||
isAccountOverrideUpdated
) {
updateSourceAmounts(transactionId, current as never, this.messenger);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ const CHAIN_ID_MOCK = '0x1';
const FROM_MOCK = '0x456';
const TRANSACTION_ID_MOCK = '123-456';

const ACCOUNT_OVERRIDE_MOCK = '0x789';

const PAYMENT_TOKEN_MOCK: TransactionPaymentToken = {
address: TOKEN_ADDRESS_MOCK,
balanceFiat: '2.46',
Expand All @@ -36,6 +38,14 @@ const PAYMENT_TOKEN_MOCK: TransactionPaymentToken = {
symbol: 'TST',
};

function createMessengerMock(
transactionData: Record<string, unknown> = {},
): never {
return {
call: jest.fn().mockReturnValue({ transactionData }),
} as never;
}

describe('Update Payment Token Action', () => {
const getTokenInfoMock = jest.mocked(getTokenInfo);
const getTokenFiatRateMock = jest.mocked(getTokenFiatRate);
Expand Down Expand Up @@ -65,6 +75,7 @@ describe('Update Payment Token Action', () => {

it('updates payment token', () => {
const updateTransactionDataMock = jest.fn();
const messenger = createMessengerMock();

updatePaymentToken(
{
Expand All @@ -73,15 +84,16 @@ describe('Update Payment Token Action', () => {
transactionId: TRANSACTION_ID_MOCK,
},
{
messenger: {} as never,
messenger,
updateTransactionData: updateTransactionDataMock,
},
);

expect(getTokenInfoMock).toHaveBeenCalledWith(
{},
TOKEN_ADDRESS_MOCK,
expect(getTokenBalanceMock).toHaveBeenCalledWith(
messenger,
FROM_MOCK,
CHAIN_ID_MOCK,
TOKEN_ADDRESS_MOCK,
);

expect(updateTransactionDataMock).toHaveBeenCalledTimes(1);
Expand All @@ -103,6 +115,32 @@ describe('Update Payment Token Action', () => {
expect(transactionDataMock.fiatPayment).toStrictEqual({});
});

it('uses accountOverride for balance lookup when set', () => {
const updateTransactionDataMock = jest.fn();
const messenger = createMessengerMock({
[TRANSACTION_ID_MOCK]: { accountOverride: ACCOUNT_OVERRIDE_MOCK },
});

updatePaymentToken(
{
chainId: CHAIN_ID_MOCK,
tokenAddress: TOKEN_ADDRESS_MOCK,
transactionId: TRANSACTION_ID_MOCK,
},
{
messenger,
updateTransactionData: updateTransactionDataMock,
},
);

expect(getTokenBalanceMock).toHaveBeenCalledWith(
messenger,
ACCOUNT_OVERRIDE_MOCK,
CHAIN_ID_MOCK,
TOKEN_ADDRESS_MOCK,
);
});

it('throws if token info not found', () => {
getTokenInfoMock.mockReturnValue(undefined);

Expand All @@ -114,7 +152,7 @@ describe('Update Payment Token Action', () => {
transactionId: TRANSACTION_ID_MOCK,
},
{
messenger: {} as never,
messenger: createMessengerMock(),
updateTransactionData: noop,
},
),
Expand All @@ -132,7 +170,7 @@ describe('Update Payment Token Action', () => {
transactionId: TRANSACTION_ID_MOCK,
},
{
messenger: {} as never,
messenger: createMessengerMock(),
updateTransactionData: noop,
},
),
Expand All @@ -150,7 +188,7 @@ describe('Update Payment Token Action', () => {
transactionId: TRANSACTION_ID_MOCK,
},
{
messenger: {} as never,
messenger: createMessengerMock(),
updateTransactionData: noop,
},
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,12 @@ export function updatePaymentToken(
throw new Error('Transaction not found');
}

const state = messenger.call('TransactionPayController:getState');
const accountOverride = state.transactionData[transactionId]?.accountOverride;

const paymentToken = getPaymentToken({
chainId,
from: transaction?.txParams.from as Hex,
from: accountOverride ?? (transaction.txParams.from as Hex),
messenger,
tokenAddress,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1191,6 +1191,27 @@ describe('Relay Submit Utils', () => {
expect(addTransactionBatchMock).not.toHaveBeenCalled();
});

it('uses quote.request.from (accountOverride) for signing when accountOverride is set', async () => {
const { submitHyperliquidWithdraw: hlWithdrawMock } = jest.requireMock(
'./hyperliquid-withdraw',
);

const ACCOUNT_OVERRIDE_MOCK = '0xaccountOverride' as Hex;

request.quotes[0].request.isHyperliquidSource = true;
request.quotes[0].request.from = ACCOUNT_OVERRIDE_MOCK;
request.quotes[0].original.steps[0].kind = 'transaction';

await submitRelayQuotes(request);

expect(hlWithdrawMock).toHaveBeenCalledTimes(1);
expect(hlWithdrawMock).toHaveBeenCalledWith(
request.quotes[0],
ACCOUNT_OVERRIDE_MOCK,
messenger,
);
});

it('still polls relay status after HyperLiquid withdraw', async () => {
request.quotes[0].request.isHyperliquidSource = true;

Expand Down Expand Up @@ -1292,6 +1313,32 @@ describe('Relay Submit Utils', () => {
);
});

it('passes txParams with from overridden by quote request from', async () => {
const ACCOUNT_OVERRIDE_MOCK = '0xaccountOverride' as Hex;

request.quotes[0].request.from = ACCOUNT_OVERRIDE_MOCK;
request.transaction = {
...request.transaction,
txParams: {
from: FROM_MOCK,
data: '0xorigdata' as Hex,
value: '0x100' as Hex,
},
} as TransactionMeta;

await submitRelayQuotes(request);

expect(getDelegationTransactionMock).toHaveBeenCalledWith({
transaction: expect.objectContaining({
txParams: {
from: ACCOUNT_OVERRIDE_MOCK,
data: '0xorigdata',
value: '0x100',
},
}),
});
});

it('submits to /execute with delegation data', async () => {
await submitRelayQuotes(request);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,7 @@ async function executeSingleQuote(
);

if (quote.request.isHyperliquidSource) {
const from = transaction.txParams.from as Hex;
await submitHyperliquidWithdraw(quote, from, messenger);
await submitHyperliquidWithdraw(quote, quote.request.from, messenger);
} else {
await submitTransactions(quote, transaction, messenger);
}
Expand Down Expand Up @@ -414,6 +413,10 @@ async function submitViaRelayExecute(
to: params.to as Hex,
value: (params.value ?? '0x0') as Hex,
})),
txParams: {
...transaction.txParams,
from,
},
} as TransactionMeta;

const delegation = await messenger.call(
Expand Down
Loading
Loading