diff --git a/packages/transaction-controller/CHANGELOG.md b/packages/transaction-controller/CHANGELOG.md index 18ca9c6c0c7..01d77e04570 100644 --- a/packages/transaction-controller/CHANGELOG.md +++ b/packages/transaction-controller/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Add optional `targetFiat` property to `MetamaskPayMetadata` type ([#7562](https://github.com/MetaMask/core/pull/7562)) - Add optional `isStateOnly` property to `TransactionMeta` and `AddTransactionOptions` ([#7591](https://github.com/MetaMask/core/pull/7591)) - Transactions with `isStateOnly` set to `true` have no lifecycle and are not signed or published. - Transactions are also excluded from confirmation polling. diff --git a/packages/transaction-controller/src/types.ts b/packages/transaction-controller/src/types.ts index 9d42d444f95..2d6aaf3d565 100644 --- a/packages/transaction-controller/src/types.ts +++ b/packages/transaction-controller/src/types.ts @@ -2071,6 +2071,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; diff --git a/packages/transaction-pay-controller/CHANGELOG.md b/packages/transaction-pay-controller/CHANGELOG.md index 6db47aa063a..fe01d1da3c6 100644 --- a/packages/transaction-pay-controller/CHANGELOG.md +++ b/packages/transaction-pay-controller/CHANGELOG.md @@ -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/controller-utils` from `^11.17.0` to `^11.18.0` ([#7583](https://github.com/MetaMask/core/pull/7583)) diff --git a/packages/transaction-pay-controller/src/TransactionPayController.test.ts b/packages/transaction-pay-controller/src/TransactionPayController.test.ts index 80fece81294..82232be1a13 100644 --- a/packages/transaction-pay-controller/src/TransactionPayController.test.ts +++ b/packages/transaction-pay-controller/src/TransactionPayController.test.ts @@ -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(); diff --git a/packages/transaction-pay-controller/src/TransactionPayController.ts b/packages/transaction-pay-controller/src/TransactionPayController.ts index e1602ba8abf..9bba57d98d8 100644 --- a/packages/transaction-pay-controller/src/TransactionPayController.ts +++ b/packages/transaction-pay-controller/src/TransactionPayController.ts @@ -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, @@ -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] = { @@ -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; @@ -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), diff --git a/packages/transaction-pay-controller/src/index.ts b/packages/transaction-pay-controller/src/index.ts index 04b8fe4d86b..d3cb83c35f3 100644 --- a/packages/transaction-pay-controller/src/index.ts +++ b/packages/transaction-pay-controller/src/index.ts @@ -6,6 +6,7 @@ export type { TransactionPayControllerGetStrategyAction, TransactionPayControllerMessenger, TransactionPayControllerOptions, + TransactionPayControllerSetIsMaxAmountAction, TransactionPayControllerState, TransactionPayControllerStateChangeEvent, TransactionPayControllerUpdatePaymentTokenAction, diff --git a/packages/transaction-pay-controller/src/strategy/bridge/bridge-quotes.test.ts b/packages/transaction-pay-controller/src/strategy/bridge/bridge-quotes.test.ts index 920e5a57627..0eb0b433b47 100644 --- a/packages/transaction-pay-controller/src/strategy/bridge/bridge-quotes.test.ts +++ b/packages/transaction-pay-controller/src/strategy/bridge/bridge-quotes.test.ts @@ -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', diff --git a/packages/transaction-pay-controller/src/strategy/bridge/bridge-quotes.ts b/packages/transaction-pay-controller/src/strategy/bridge/bridge-quotes.ts index d30aa73e98a..a82e6e2bbe1 100644 --- a/packages/transaction-pay-controller/src/strategy/bridge/bridge-quotes.ts +++ b/packages/transaction-pay-controller/src/strategy/bridge/bridge-quotes.ts @@ -554,6 +554,7 @@ function normalizeQuote( original: quote, request, sourceAmount, + targetAmount, strategy: TransactionPayStrategy.Bridge, }; } diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts index 9a95aa1e286..33b9f63b6b9 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts @@ -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: { @@ -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, @@ -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, }); }); @@ -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')); diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts index 3ddb0478788..4a75d1f1fb9 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts @@ -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); @@ -141,12 +152,13 @@ async function processTransactions( messenger: TransactionPayControllerMessenger, ): Promise { 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)); @@ -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 }, @@ -263,7 +280,7 @@ async function normalizeQuote( ): Promise> { const { messenger } = fullRequest; const { details } = quote; - const { currencyIn } = details; + const { currencyIn, currencyOut } = details; const { usdToFiatRate } = getFiatRates(messenger, request); @@ -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, }; @@ -313,6 +336,7 @@ async function normalizeQuote( }, request, sourceAmount, + targetAmount, strategy: TransactionPayStrategy.Relay, }; } diff --git a/packages/transaction-pay-controller/src/strategy/relay/types.ts b/packages/transaction-pay-controller/src/strategy/relay/types.ts index fbab58bc223..4883e2837cf 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/types.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/types.ts @@ -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; @@ -38,6 +38,7 @@ export type RelayQuote = { }; }; currencyOut: { + amount: string; amountFormatted: string; amountUsd: string; currency: { diff --git a/packages/transaction-pay-controller/src/strategy/test/TestStrategy.test.ts b/packages/transaction-pay-controller/src/strategy/test/TestStrategy.test.ts index 3207baf5899..a6ed67f3c1a 100644 --- a/packages/transaction-pay-controller/src/strategy/test/TestStrategy.test.ts +++ b/packages/transaction-pay-controller/src/strategy/test/TestStrategy.test.ts @@ -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), + }, }, ]); }); diff --git a/packages/transaction-pay-controller/src/strategy/test/TestStrategy.ts b/packages/transaction-pay-controller/src/strategy/test/TestStrategy.ts index b0ca2cc7359..06b470bcb8e 100644 --- a/packages/transaction-pay-controller/src/strategy/test/TestStrategy.ts +++ b/packages/transaction-pay-controller/src/strategy/test/TestStrategy.ts @@ -54,6 +54,12 @@ export class TestStrategy implements PayStrategy { raw: '456000', usd: '4.56', }, + targetAmount: { + human: '5.67', + fiat: '5.67', + raw: '567000', + usd: '5.67', + }, strategy: TransactionPayStrategy.Test, }, ]; diff --git a/packages/transaction-pay-controller/src/types.ts b/packages/transaction-pay-controller/src/types.ts index 1a2595f6d3a..8539a6e15ff 100644 --- a/packages/transaction-pay-controller/src/types.ts +++ b/packages/transaction-pay-controller/src/types.ts @@ -85,6 +85,12 @@ export type TransactionPayControllerUpdatePaymentTokenAction = { handler: (request: UpdatePaymentTokenRequest) => void; }; +/** Action to set the max amount flag for a transaction. */ +export type TransactionPayControllerSetIsMaxAmountAction = { + type: `${typeof CONTROLLER_NAME}:setIsMaxAmount`; + handler: (transactionId: string, isMaxAmount: boolean) => void; +}; + export type TransactionPayControllerStateChangeEvent = ControllerStateChangeEvent< typeof CONTROLLER_NAME, @@ -95,6 +101,7 @@ export type TransactionPayControllerActions = | TransactionPayControllerGetDelegationTransactionAction | TransactionPayControllerGetStateAction | TransactionPayControllerGetStrategyAction + | TransactionPayControllerSetIsMaxAmountAction | TransactionPayControllerUpdatePaymentTokenAction; export type TransactionPayControllerEvents = @@ -132,6 +139,9 @@ export type TransactionData = { /** Whether quotes are currently being retrieved. */ isLoading: boolean; + /** Whether the user has selected the maximum amount. */ + isMaxAmount?: boolean; + /** Source token selected for the transaction. */ paymentToken?: TransactionPaymentToken; @@ -257,6 +267,9 @@ export type QuoteRequest = { /** Address of the user's account. */ from: Hex; + /** Whether the transaction is a maximum amount transaction. */ + isMaxAmount?: boolean; + /** Balance of the source token in atomic format without factoring token decimals. */ sourceBalanceRaw: string; @@ -322,6 +335,9 @@ export type TransactionPayQuote = { /** Name of the strategy used to retrieve the quote. */ strategy: TransactionPayStrategy; + + /** Amount of target token provided. */ + targetAmount: Amount; }; /** Request to get quotes for a transaction. */ @@ -415,6 +431,9 @@ export type TransactionPayTotals = { /** Total amount of source token required. */ sourceAmount: Amount; + /** Total amount of target token provided. */ + targetAmount: Amount; + /** Overall total cost for the target transaction and all quotes. */ total: FiatValue; }; diff --git a/packages/transaction-pay-controller/src/utils/quotes.test.ts b/packages/transaction-pay-controller/src/utils/quotes.test.ts index 4f5fb338744..a247cef45d1 100644 --- a/packages/transaction-pay-controller/src/utils/quotes.test.ts +++ b/packages/transaction-pay-controller/src/utils/quotes.test.ts @@ -73,6 +73,12 @@ const TOTALS_MOCK = { }, }, }, + targetAmount: { + fiat: '5.67', + human: '5.67', + raw: '567000', + usd: '6.78', + }, total: { fiat: '1.23', usd: '4.56', @@ -222,6 +228,7 @@ describe('Quotes Utils', () => { messenger, requests: [ { + isMaxAmount: false, from: TRANSACTION_META_MOCK.txParams.from, sourceBalanceRaw: TRANSACTION_DATA_MOCK.paymentToken?.balanceRaw, sourceTokenAmount: @@ -300,6 +307,7 @@ describe('Quotes Utils', () => { bridgeFeeFiat: TOTALS_MOCK.fees.provider.usd, chainId: TRANSACTION_DATA_MOCK.paymentToken?.chainId, networkFeeFiat: TOTALS_MOCK.fees.sourceNetwork.estimate.usd, + targetFiat: TOTALS_MOCK.targetAmount.usd, tokenAddress: TRANSACTION_DATA_MOCK.paymentToken?.address, totalFiat: TOTALS_MOCK.total.usd, }, diff --git a/packages/transaction-pay-controller/src/utils/quotes.ts b/packages/transaction-pay-controller/src/utils/quotes.ts index 1610965248e..a242607fbf7 100644 --- a/packages/transaction-pay-controller/src/utils/quotes.ts +++ b/packages/transaction-pay-controller/src/utils/quotes.ts @@ -55,10 +55,11 @@ export async function updateQuotes( log('Updating quotes', { transactionId }); - const { paymentToken, sourceAmounts, tokens } = transactionData; + const { isMaxAmount, paymentToken, sourceAmounts, tokens } = transactionData; const requests = buildQuoteRequests({ from: transaction.txParams.from as Hex, + isMaxAmount: isMaxAmount ?? false, paymentToken, sourceAmounts, tokens, @@ -77,8 +78,9 @@ export async function updateQuotes( ); const totals = calculateTotals({ - quotes: quotes as TransactionPayQuote[], + isMaxAmount, messenger, + quotes: quotes as TransactionPayQuote[], tokens, transaction, }); @@ -148,6 +150,7 @@ function syncTransaction({ bridgeFeeFiat: totals.fees.provider.usd, chainId: paymentToken.chainId, networkFeeFiat: totals.fees.sourceNetwork.estimate.usd, + targetFiat: totals.targetAmount.usd, tokenAddress: paymentToken.address, totalFiat: totals.total.usd, }; @@ -209,6 +212,7 @@ export async function refreshQuotes( * * @param request - Request parameters. * @param request.from - Address from which the transaction is sent. + * @param request.isMaxAmount - Whether the transaction is a maximum amount transaction. * @param request.paymentToken - Payment token used for the transaction. * @param request.sourceAmounts - Source amounts for the transaction. * @param request.tokens - Required tokens for the transaction. @@ -217,12 +221,14 @@ export async function refreshQuotes( */ function buildQuoteRequests({ from, + isMaxAmount, paymentToken, sourceAmounts, tokens, transactionId, }: { from: Hex; + isMaxAmount: boolean; paymentToken: TransactionPaymentToken | undefined; sourceAmounts: TransactionPaySourceAmount[] | undefined; tokens: TransactionPayRequiredToken[]; @@ -239,6 +245,7 @@ function buildQuoteRequests({ return { from, + isMaxAmount, sourceBalanceRaw: paymentToken.balanceRaw, sourceTokenAmount: sourceAmount.sourceAmountRaw, sourceChainId: paymentToken.chainId, diff --git a/packages/transaction-pay-controller/src/utils/source-amounts.test.ts b/packages/transaction-pay-controller/src/utils/source-amounts.test.ts index f8af32058c3..9612a79e032 100644 --- a/packages/transaction-pay-controller/src/utils/source-amounts.test.ts +++ b/packages/transaction-pay-controller/src/utils/source-amounts.test.ts @@ -163,6 +163,25 @@ describe('Source Amounts Utils', () => { expect(transactionData.sourceAmounts).toStrictEqual([]); }); + it('uses payment token balance if isMaxAmount is true', () => { + const transactionData: TransactionData = { + isLoading: false, + isMaxAmount: true, + paymentToken: PAYMENT_TOKEN_MOCK, + tokens: [TRANSACTION_TOKEN_MOCK], + }; + + updateSourceAmounts(TRANSACTION_ID_MOCK, transactionData, messenger); + + expect(transactionData.sourceAmounts).toStrictEqual([ + { + sourceAmountHuman: PAYMENT_TOKEN_MOCK.balanceHuman, + sourceAmountRaw: PAYMENT_TOKEN_MOCK.balanceRaw, + targetTokenAddress: TRANSACTION_TOKEN_MOCK.address, + }, + ]); + }); + it('does nothing if no payment token', () => { const transactionData: TransactionData = { isLoading: false, diff --git a/packages/transaction-pay-controller/src/utils/source-amounts.ts b/packages/transaction-pay-controller/src/utils/source-amounts.ts index 2677c4861bb..f98cd422c37 100644 --- a/packages/transaction-pay-controller/src/utils/source-amounts.ts +++ b/packages/transaction-pay-controller/src/utils/source-amounts.ts @@ -35,7 +35,7 @@ export function updateSourceAmounts( return; } - const { paymentToken, tokens } = transactionData; + const { isMaxAmount, paymentToken, tokens } = transactionData; if (!tokens.length || !paymentToken) { return; @@ -48,6 +48,7 @@ export function updateSourceAmounts( singleToken, messenger, transactionId, + isMaxAmount ?? false, ), ) .filter(Boolean) as TransactionPaySourceAmount[]; @@ -64,6 +65,7 @@ export function updateSourceAmounts( * @param token - Target token to cover. * @param messenger - Controller messenger. * @param transactionId - ID of the transaction. + * @param isMaxAmount - Whether the transaction is a maximum amount transaction. * @returns The source amount or undefined if calculation failed. */ function calculateSourceAmount( @@ -71,6 +73,7 @@ function calculateSourceAmount( token: TransactionPayRequiredToken, messenger: TransactionPayControllerMessenger, transactionId: string, + isMaxAmount: boolean, ): TransactionPaySourceAmount | undefined { const paymentTokenFiatRate = getTokenFiatRate( messenger, @@ -119,6 +122,14 @@ function calculateSourceAmount( return undefined; } + if (isMaxAmount) { + return { + sourceAmountHuman: paymentToken.balanceHuman, + sourceAmountRaw: paymentToken.balanceRaw, + targetTokenAddress: token.address, + }; + } + return { sourceAmountHuman, sourceAmountRaw, diff --git a/packages/transaction-pay-controller/src/utils/totals.test.ts b/packages/transaction-pay-controller/src/utils/totals.test.ts index 98c5891acde..59c1beb6a59 100644 --- a/packages/transaction-pay-controller/src/utils/totals.test.ts +++ b/packages/transaction-pay-controller/src/utils/totals.test.ts @@ -53,6 +53,12 @@ const QUOTE_1_MOCK: TransactionPayQuote = { usd: '8.88', }, strategy: TransactionPayStrategy.Test, + targetAmount: { + human: '9.99', + fiat: '9.99', + raw: '999000000000000', + usd: '10.10', + }, }; const TOKEN_1_MOCK = { @@ -105,6 +111,12 @@ const QUOTE_2_MOCK: TransactionPayQuote = { usd: '14.14', }, strategy: TransactionPayStrategy.Test, + targetAmount: { + human: '15.15', + fiat: '15.15', + raw: '1515000000000000', + usd: '16.16', + }, }; const TRANSACTION_META_MOCK = {} as TransactionMeta; @@ -150,6 +162,19 @@ describe('Totals Utils', () => { expect(result.total.usd).toBe('51.08'); }); + it('returns adjusted total when isMaxAmount is true', () => { + const result = calculateTotals({ + isMaxAmount: true, + quotes: [QUOTE_1_MOCK, QUOTE_2_MOCK], + tokens: [TOKEN_1_MOCK, TOKEN_2_MOCK], + messenger: MESSENGER_MOCK, + transaction: TRANSACTION_META_MOCK, + }); + + expect(result.total.fiat).toBe('64'); + expect(result.total.usd).toBe('70.68'); + }); + it('returns total excluding token amount not in quote', () => { const result = calculateTotals({ quotes: [QUOTE_1_MOCK, QUOTE_2_MOCK], diff --git a/packages/transaction-pay-controller/src/utils/totals.ts b/packages/transaction-pay-controller/src/utils/totals.ts index d3ece1efaae..7588857a9cb 100644 --- a/packages/transaction-pay-controller/src/utils/totals.ts +++ b/packages/transaction-pay-controller/src/utils/totals.ts @@ -15,6 +15,7 @@ import type { * Calculate totals for a list of quotes and tokens. * * @param request - Request parameters. + * @param request.isMaxAmount - Whether the transaction is a maximum amount transaction. * @param request.quotes - List of bridge quotes. * @param request.messenger - Controller messenger. * @param request.tokens - List of required tokens. @@ -22,11 +23,13 @@ import type { * @returns The calculated totals in USD and fiat currency. */ export function calculateTotals({ + isMaxAmount, quotes, messenger, tokens, transaction, }: { + isMaxAmount?: boolean; quotes: TransactionPayQuote[]; messenger: TransactionPayControllerMessenger; tokens: TransactionPayRequiredToken[]; @@ -55,6 +58,7 @@ export function calculateTotals({ : transactionNetworkFee; const sourceAmount = sumAmounts(quotes.map((quote) => quote.sourceAmount)); + const targetAmount = sumAmounts(quotes.map((quote) => quote.targetAmount)); const quoteTokens = tokens.filter( (singleToken) => !singleToken.skipIfBalance, @@ -62,17 +66,18 @@ export function calculateTotals({ const amountFiat = sumProperty(quoteTokens, (token) => token.amountFiat); const amountUsd = sumProperty(quoteTokens, (token) => token.amountUsd); + const hasQuotes = quotes.length > 0; const totalFiat = new BigNumber(providerFee.fiat) .plus(sourceNetworkFeeEstimate.fiat) .plus(targetNetworkFee.fiat) - .plus(amountFiat) + .plus(isMaxAmount && hasQuotes ? targetAmount.fiat : amountFiat) .toString(10); const totalUsd = new BigNumber(providerFee.usd) .plus(sourceNetworkFeeEstimate.usd) .plus(targetNetworkFee.usd) - .plus(amountUsd) + .plus(isMaxAmount && hasQuotes ? targetAmount.usd : amountUsd) .toString(10); const estimatedDuration = Number( @@ -100,6 +105,7 @@ export function calculateTotals({ targetNetwork: targetNetworkFee, }, sourceAmount, + targetAmount, total: { fiat: totalFiat, usd: totalUsd,