Skip to content
Open
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
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.0]

### 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,20 @@ 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('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,10 +119,12 @@ export class TransactionPayController extends BaseController<
isPostQuote: transactionData.isPostQuote,
isHyperliquidSource: transactionData.isHyperliquidSource,
refundTo: transactionData.refundTo,
accountOverride: transactionData.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;
Expand Down Expand Up @@ -215,6 +217,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 +239,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
14 changes: 14 additions & 0 deletions packages/transaction-pay-controller/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,13 @@ 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, used as the delegator for the transaction, it provides the funds and pays for gas.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Slightly ambigious as the delegation is still done on behalf of the original from and not accountOverride. Plus this also still applies when isPostQuote is true.

*/
accountOverride?: Hex;
};

/** Callback to update transaction config. */
Expand Down Expand Up @@ -183,6 +190,13 @@ 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, used as the delegator for the transaction, it provides the funds and pays for gas.
*/
accountOverride?: Hex;

/**
* Token selected for the transaction.
* - For standard flows (isPostQuote=false): This is the SOURCE/payment token
Expand Down
45 changes: 45 additions & 0 deletions packages/transaction-pay-controller/src/utils/quotes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -931,6 +931,51 @@ describe('Quotes Utils', () => {
);
});

it('uses accountOverride as from for post-quote flow', async () => {
const accountOverride =
'0xrecipient0000000000000000000000000000001' as Hex;

await run({
transactionData: {
...POST_QUOTE_TRANSACTION_DATA,
accountOverride,
},
});

expect(getQuotesMock).toHaveBeenCalledWith(
expect.objectContaining({
requests: [
expect.objectContaining({
isPostQuote: true,
from: accountOverride,
}),
],
}),
);
});

it('uses accountOverride as from for non-post-quote flow', async () => {
const accountOverride =
'0xdelegator0000000000000000000000000000001' as Hex;

await run({
transactionData: {
...TRANSACTION_DATA_MOCK,
accountOverride,
},
});

expect(getQuotesMock).toHaveBeenCalledWith(
expect.objectContaining({
requests: [
expect.objectContaining({
from: accountOverride,
}),
],
}),
);
});

it('passes isHyperliquidSource through to post-quote request', async () => {
await run({
transactionData: {
Expand Down
3 changes: 2 additions & 1 deletion packages/transaction-pay-controller/src/utils/quotes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ export async function updateQuotes(
log('Updating quotes', { transactionId });

const {
accountOverride,
isMaxAmount,
isPostQuote,
isHyperliquidSource,
Expand All @@ -77,7 +78,7 @@ export async function updateQuotes(
tokens,
} = transactionData;

const from = transaction.txParams.from as Hex;
const from = accountOverride ?? (transaction.txParams.from as Hex);

updateTransactionData(transactionId, (data) => {
data.isLoading = true;
Expand Down
Loading