Skip to content
4 changes: 4 additions & 0 deletions packages/transaction-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 optional `targetFiat` property to `MetamaskPayMetadata` type ([#7562](https://github.com/MetaMask/core/pull/7562))

### Changed

- Bump `@metamask/remote-feature-flag-controller` from `^3.1.0` to `^4.0.0` ([#7546](https://github.com/MetaMask/core/pull/7546))
Expand Down
3 changes: 3 additions & 0 deletions packages/transaction-controller/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2066,6 +2066,9 @@ export type MetamaskPayMetadata = {
/** Total network fee in fiat currency, including the original and bridge transactions. */
networkFeeFiat?: string;

/** Total amount of target token provided in fiat currency. */
targetFiat?: string;

/** Address of the payment token that the transaction funds were sourced from. */
tokenAddress?: Hex;

Expand Down
9 changes: 9 additions & 0 deletions packages/transaction-pay-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- **BREAKING:** Support max amount quotes ([#7562](https://github.com/MetaMask/core/pull/7562))
- Add `TransactionPayController:setIsMaxAmount` messenger action.
- Add `isMaxAmount` property to `TransactionData` type.
- Add `targetAmount` property to `TransactionPayQuote` and `TransactionPayTotals`.
- Update Relay quote requests to use `EXACT_INPUT` trade type when max amount is selected.
- Update totals calculation to account for max amount selection.

### Changed

- Bump `@metamask/remote-feature-flag-controller` from `^3.1.0` to `^4.0.0` ([#7546](https://github.com/MetaMask/core/pull/7546))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,18 @@ describe('TransactionPayController', () => {
});
});

describe('setIsMaxAmount', () => {
it('updates state', () => {
const controller = createController();

controller.setIsMaxAmount(TRANSACTION_ID_MOCK, true);

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

describe('getStrategy Action', () => {
it('returns relay if no callback', async () => {
createController();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,12 @@ export class TransactionPayController extends BaseController<
});
}

setIsMaxAmount(transactionId: string, isMaxAmount: boolean): void {
this.#updateTransactionData(transactionId, (transactionData) => {
transactionData.isMaxAmount = isMaxAmount;
});
}

updatePaymentToken(request: UpdatePaymentTokenRequest): void {
updatePaymentToken(request, {
messenger: this.messenger,
Expand All @@ -98,6 +104,7 @@ export class TransactionPayController extends BaseController<
let current = transactionData[transactionId];
const originalPaymentToken = current?.paymentToken;
const originalTokens = current?.tokens;
const originalIsMaxAmount = current?.isMaxAmount;

if (!current) {
transactionData[transactionId] = {
Expand All @@ -114,8 +121,9 @@ export class TransactionPayController extends BaseController<
current.paymentToken !== originalPaymentToken;

const isTokensUpdated = current.tokens !== originalTokens;
const isIsMaxUpdated = current.isMaxAmount !== originalIsMaxAmount;

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

shouldUpdateQuotes = true;
Expand Down Expand Up @@ -144,6 +152,11 @@ export class TransactionPayController extends BaseController<
((): TransactionPayStrategy => TransactionPayStrategy.Relay),
);

this.messenger.registerActionHandler(
'TransactionPayController:setIsMaxAmount',
this.setIsMaxAmount.bind(this),
);

this.messenger.registerActionHandler(
'TransactionPayController:updatePaymentToken',
this.updatePaymentToken.bind(this),
Expand Down
1 change: 1 addition & 0 deletions packages/transaction-pay-controller/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export type {
TransactionPayControllerGetStrategyAction,
TransactionPayControllerMessenger,
TransactionPayControllerOptions,
TransactionPayControllerSetIsMaxAmountAction,
TransactionPayControllerState,
TransactionPayControllerStateChangeEvent,
TransactionPayControllerUpdatePaymentTokenAction,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -716,6 +716,20 @@ describe('Bridge Quotes Utils', () => {
});
});

it('returns target amount in quote', async () => {
const quotes = await getBridgeQuotes({
...request,
requests: [QUOTE_REQUEST_1_MOCK],
});

expect(quotes[0].targetAmount).toStrictEqual({
fiat: '24.6',
human: '12.3',
raw: QUOTE_REQUEST_1_MOCK.targetAmountMinimum,
usd: '36.9',
});
});

it('returns target network fee in quote', async () => {
calculateTransactionGasCostMock.mockReturnValue({
fiat: '1.23',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -554,6 +554,7 @@ function normalizeQuote(
original: quote,
request,
sourceAmount,
targetAmount,
strategy: TransactionPayStrategy.Bridge,
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,12 @@ const QUOTE_REQUEST_MOCK: QuoteRequest = {
const QUOTE_MOCK = {
details: {
currencyIn: {
amount: '1240000000000000000',
amountFormatted: '1.24',
amountUsd: '1.24',
},
currencyOut: {
amount: '100',
amountFormatted: '1.0',
amountUsd: '1.23',
currency: {
Expand Down Expand Up @@ -249,12 +252,52 @@ describe('Relay Quotes Utils', () => {
originChainId: 1,
originCurrency: QUOTE_REQUEST_MOCK.sourceTokenAddress,
recipient: QUOTE_REQUEST_MOCK.from,
tradeType: 'EXACT_OUTPUT',
tradeType: 'EXPECTED_OUTPUT',
user: QUOTE_REQUEST_MOCK.from,
}),
);
});

it('sends request with EXACT_INPUT trade type when isMaxAmount is true', async () => {
successfulFetchMock.mockResolvedValue({
json: async () => QUOTE_MOCK,
} as never);

await getRelayQuotes({
messenger,
requests: [{ ...QUOTE_REQUEST_MOCK, isMaxAmount: true }],
transaction: TRANSACTION_META_MOCK,
});

const body = JSON.parse(
successfulFetchMock.mock.calls[0][1]?.body as string,
);

expect(body).toStrictEqual(
expect.objectContaining({
amount: QUOTE_REQUEST_MOCK.sourceTokenAmount,
tradeType: 'EXACT_INPUT',
}),
);
});

it('throws if isMaxAmount is true and transaction includes data', async () => {
await expect(
getRelayQuotes({
messenger,
requests: [{ ...QUOTE_REQUEST_MOCK, isMaxAmount: true }],
transaction: {
...TRANSACTION_META_MOCK,
txParams: {
data: '0xabc' as Hex,
},
} as TransactionMeta,
}),
).rejects.toThrow(
'Max amount quotes do not support included transactions',
);
});

it('includes transactions in request', async () => {
successfulFetchMock.mockResolvedValue({
json: async () => QUOTE_MOCK,
Expand Down Expand Up @@ -314,15 +357,13 @@ describe('Relay Quotes Utils', () => {

expect(result[0].original.request).toStrictEqual({
amount: QUOTE_REQUEST_MOCK.targetAmountMinimum,
authorizationList: expect.any(Array),
destinationChainId: 2,
destinationCurrency: QUOTE_REQUEST_MOCK.targetTokenAddress,
originChainId: 1,
originCurrency: QUOTE_REQUEST_MOCK.sourceTokenAddress,
recipient: QUOTE_REQUEST_MOCK.from,
slippageTolerance: '50',
tradeType: 'EXACT_OUTPUT',
txs: expect.any(Array),
tradeType: 'EXPECTED_OUTPUT',
user: QUOTE_REQUEST_MOCK.from,
});
});
Expand Down Expand Up @@ -974,6 +1015,25 @@ describe('Relay Quotes Utils', () => {
});
});

it('includes target amount in quote', async () => {
successfulFetchMock.mockResolvedValue({
json: async () => QUOTE_MOCK,
} as never);

const result = await getRelayQuotes({
messenger,
requests: [QUOTE_REQUEST_MOCK],
transaction: TRANSACTION_META_MOCK,
});

expect(result[0].targetAmount).toStrictEqual({
human: QUOTE_MOCK.details.currencyOut.amountFormatted,
raw: QUOTE_MOCK.details.currencyOut.amount,
usd: '1.23',
fiat: '2.46',
});
});

it('throws if fetching quote fails', async () => {
successfulFetchMock.mockRejectedValue(new Error('Fetch error'));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,17 +89,28 @@ async function getSingleQuote(
0,
);

const {
from,
isMaxAmount,
sourceChainId,
sourceTokenAddress,
sourceTokenAmount,
targetAmountMinimum,
targetChainId,
targetTokenAddress,
} = request;

try {
const body: RelayQuoteRequest = {
amount: request.targetAmountMinimum,
destinationChainId: Number(request.targetChainId),
destinationCurrency: request.targetTokenAddress,
originChainId: Number(request.sourceChainId),
originCurrency: request.sourceTokenAddress,
recipient: request.from,
amount: isMaxAmount ? sourceTokenAmount : targetAmountMinimum,
destinationChainId: Number(targetChainId),
destinationCurrency: targetTokenAddress,
originChainId: Number(sourceChainId),
originCurrency: sourceTokenAddress,
recipient: from,
slippageTolerance,
tradeType: 'EXPECTED_OUTPUT',
user: request.from,
tradeType: isMaxAmount ? 'EXACT_INPUT' : 'EXPECTED_OUTPUT',
user: from,
};

await processTransactions(transaction, request, body, messenger);
Expand Down Expand Up @@ -141,12 +152,13 @@ async function processTransactions(
messenger: TransactionPayControllerMessenger,
): Promise<void> {
const { nestedTransactions, txParams } = transaction;
const { isMaxAmount, targetChainId } = request;
const data = txParams?.data as Hex | undefined;

const singleData =
nestedTransactions?.length === 1 ? nestedTransactions[0].data : data;

const isHypercore = request.targetChainId === CHAIN_ID_HYPERCORE;
const isHypercore = targetChainId === CHAIN_ID_HYPERCORE;

const isTokenTransfer =
!isHypercore && Boolean(singleData?.startsWith(TOKEN_TRANSFER_FOUR_BYTE));
Expand All @@ -157,13 +169,18 @@ async function processTransactions(
log('Updating recipient as token transfer', requestBody.recipient);
}

const skipDelegation = isTokenTransfer || isHypercore;
const hasNoData = singleData === undefined || singleData === '0x';
const skipDelegation = hasNoData || isTokenTransfer || isHypercore;

if (skipDelegation) {
log('Skipping delegation as token transfer or Hypercore deposit');
return;
}

if (isMaxAmount) {
throw new Error('Max amount quotes do not support included transactions');
}

const delegation = await messenger.call(
'TransactionPayController:getDelegationTransaction',
{ transaction },
Expand Down Expand Up @@ -263,7 +280,7 @@ async function normalizeQuote(
): Promise<TransactionPayQuote<RelayQuote>> {
const { messenger } = fullRequest;
const { details } = quote;
const { currencyIn } = details;
const { currencyIn, currencyOut } = details;

const { usdToFiatRate } = getFiatRates(messenger, request);

Expand Down Expand Up @@ -294,6 +311,12 @@ async function normalizeQuote(
...getFiatValueFromUsd(new BigNumber(currencyIn.amountUsd), usdToFiatRate),
};

const targetAmount: Amount = {
human: currencyOut.amountFormatted,
raw: currencyOut.amount,
...getFiatValueFromUsd(new BigNumber(currencyOut.amountUsd), usdToFiatRate),
};

const metamask = {
gasLimits,
};
Expand All @@ -313,6 +336,7 @@ async function normalizeQuote(
},
request,
sourceAmount,
targetAmount,
strategy: TransactionPayStrategy.Relay,
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export type RelayQuoteRequest = {
recipient: Hex;
refundTo?: Hex;
slippageTolerance?: string;
tradeType: 'EXPECTED_OUTPUT' | 'EXACT_OUTPUT';
tradeType: 'EXACT_INPUT' | 'EXACT_OUTPUT' | 'EXPECTED_OUTPUT';
txs?: {
to: Hex;
data: Hex;
Expand All @@ -38,6 +38,7 @@ export type RelayQuote = {
};
};
currencyOut: {
amount: string;
amountFormatted: string;
amountUsd: string;
currency: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,12 @@ describe('TestStrategy', () => {
usd: expect.any(String),
},
strategy: TransactionPayStrategy.Test,
targetAmount: {
fiat: expect.any(String),
human: expect.any(String),
raw: expect.any(String),
usd: expect.any(String),
},
},
]);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ export class TestStrategy implements PayStrategy<void> {
raw: '456000',
usd: '4.56',
},
targetAmount: {
human: '5.67',
fiat: '5.67',
raw: '567000',
usd: '5.67',
},
strategy: TransactionPayStrategy.Test,
},
];
Expand Down
Loading
Loading