diff --git a/packages/transaction-pay-controller/CHANGELOG.md b/packages/transaction-pay-controller/CHANGELOG.md index 1ea4f2b1789..3a2493d3050 100644 --- a/packages/transaction-pay-controller/CHANGELOG.md +++ b/packages/transaction-pay-controller/CHANGELOG.md @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- **BREAKING:** Add `sourceAmount` to `TransactionPayQuote` ([#7159](https://github.com/MetaMask/core/pull/7159)) + - Add `estimate` and `max` properties to `fee.sourceNetwork` in `TransactionPayQuote`. + - Add `isTargetGasFeeToken` to `fee` in `TransactionPayQuote`. + - Add matching properties to `TransactionPayTotals`. + - Use fixed fiat rate for Polygon USDCe and Arbitrum USDC. + ## [6.0.0] ### Fixed diff --git a/packages/transaction-pay-controller/src/constants.ts b/packages/transaction-pay-controller/src/constants.ts index 1a64eecba2f..0b6d75ff06e 100644 --- a/packages/transaction-pay-controller/src/constants.ts +++ b/packages/transaction-pay-controller/src/constants.ts @@ -1,10 +1,18 @@ import type { Hex } from '@metamask/utils'; export const CONTROLLER_NAME = 'TransactionPayController'; +export const CHAIN_ID_ARBITRUM = '0xa4b1' as Hex; +export const CHAIN_ID_POLYGON = '0x89' as Hex; export const NATIVE_TOKEN_ADDRESS = '0x0000000000000000000000000000000000000000' as Hex; +export const ARBITRUM_USDC_ADDRESS = + '0xaf88d065e77c8cC2239327C5EDb3A432268e5831' as Hex; + +export const POLYGON_USDCE_ADDRESS = + '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174' as Hex; + export enum TransactionPayStrategy { Bridge = 'bridge', Relay = 'relay', 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 98dfc1d17bb..873c7916c1b 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 @@ -135,6 +135,8 @@ describe('Bridge Quotes Utils', () => { calculateGasCostMock.mockReturnValue({ fiat: '0.1', + human: '0.051', + raw: '51000000000000', usd: '0.2', }); @@ -715,6 +717,8 @@ describe('Bridge Quotes Utils', () => { it('returns target network fee in quote', async () => { calculateTransactionGasCostMock.mockReturnValue({ fiat: '1.23', + human: '0.000123', + raw: '123000000000000', usd: '2.34', }); @@ -735,6 +739,8 @@ describe('Bridge Quotes Utils', () => { it('for trade only', async () => { calculateGasCostMock.mockReturnValue({ fiat: '1.23', + human: '0.000123', + raw: '123000000000000', usd: '2.34', }); @@ -756,8 +762,7 @@ describe('Bridge Quotes Utils', () => { expect(quotes[0].fees).toMatchObject({ sourceNetwork: { - fiat: '1.23', - usd: '2.34', + estimate: { fiat: '1.23', usd: '2.34' }, }, }); }); @@ -765,6 +770,8 @@ describe('Bridge Quotes Utils', () => { it('for trade and approval', async () => { calculateGasCostMock.mockReturnValue({ fiat: '1.23', + human: '0.000123', + raw: '123000000000000', usd: '2.34', }); @@ -790,8 +797,10 @@ describe('Bridge Quotes Utils', () => { expect(quotes[0].fees).toMatchObject({ sourceNetwork: { - fiat: '2.46', - usd: '4.68', + estimate: { + fiat: '2.46', + usd: '4.68', + }, }, }); }); 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 763c363fad5..3e6ee786581 100644 --- a/packages/transaction-pay-controller/src/strategy/bridge/bridge-quotes.ts +++ b/packages/transaction-pay-controller/src/strategy/bridge/bridge-quotes.ts @@ -21,7 +21,7 @@ import type { import { TransactionPayStrategy } from '../..'; import { projectLogger } from '../../logger'; import type { - FiatValue, + Amount, PayStrategyGetBatchRequest, PayStrategyGetQuotesRequest, PayStrategyGetRefreshIntervalRequest, @@ -497,21 +497,21 @@ function normalizeQuote( ); } - const targetAmountMinimumFiat = calculateFiatValue( + const targetAmountMinimumFiat = calculateAmount( quote.quote.minDestTokenAmount, quote.quote.destAsset.decimals, targetFiatRate.fiatRate, targetFiatRate.usdRate, ); - const sourceAmountFiat = calculateFiatValue( + const sourceAmount = calculateAmount( quote.quote.srcTokenAmount, quote.quote.srcAsset.decimals, sourceFiatRate.fiatRate, sourceFiatRate.usdRate, ); - const targetAmountGoal = calculateFiatValue( + const targetAmount = calculateAmount( request.targetAmountMinimum, quote.quote.destAsset.decimals, targetFiatRate.fiatRate, @@ -519,24 +519,28 @@ function normalizeQuote( ); const targetNetwork = calculateTransactionGasCost(transaction, messenger); - const sourceNetwork = calculateSourceNetworkFee(quote, messenger); + + const sourceNetwork = { + estimate: calculateSourceNetworkFee(quote, messenger), + max: calculateSourceNetworkFee(quote, messenger, { isMax: true }), + }; return { estimatedDuration: quote.estimatedProcessingTimeInSeconds, dust: { fiat: new BigNumber(targetAmountMinimumFiat.fiat) - .minus(targetAmountGoal.fiat) + .minus(targetAmount.fiat) .toString(10), usd: new BigNumber(targetAmountMinimumFiat.usd) - .minus(targetAmountGoal.usd) + .minus(targetAmount.usd) .toString(10), }, fees: { provider: { - fiat: new BigNumber(sourceAmountFiat.fiat) + fiat: new BigNumber(sourceAmount.fiat) .minus(targetAmountMinimumFiat.fiat) .toString(10), - usd: new BigNumber(sourceAmountFiat.usd) + usd: new BigNumber(sourceAmount.usd) .minus(targetAmountMinimumFiat.usd) .toString(10), }, @@ -545,30 +549,33 @@ function normalizeQuote( }, original: quote, request, + sourceAmount, strategy: TransactionPayStrategy.Bridge, }; } /** - * Calculate fiat value from amount and fiat rates. + * Calculate amount from raw value and fiat rates. * - * @param amount - Amount to convert. + * @param raw - Amount to convert. * @param decimals - Token decimals. * @param fiatRateFiat - Fiat rate. * @param fiatRateUsd - USD rate. - * @returns Fiat value. + * @returns Amount object. */ -function calculateFiatValue( - amount: string, +function calculateAmount( + raw: string, decimals: number, fiatRateFiat: string, fiatRateUsd: string, -): FiatValue { - const amountHuman = new BigNumber(amount).shiftedBy(-decimals); - const usd = amountHuman.multipliedBy(fiatRateUsd).toString(10); - const fiat = amountHuman.multipliedBy(fiatRateFiat).toString(10); +): Amount { + const humanValue = new BigNumber(raw).shiftedBy(-decimals); + const human = humanValue.toString(10); - return { fiat, usd }; + const usd = humanValue.multipliedBy(fiatRateUsd).toString(10); + const fiat = humanValue.multipliedBy(fiatRateFiat).toString(10); + + return { fiat, human, raw, usd }; } /** @@ -576,22 +583,29 @@ function calculateFiatValue( * * @param quote - Bridge quote response. * @param messenger - Controller messenger. + * @param options - Calculation options. + * @param options.isMax - Whether to calculate the maximum cost. * @returns Estimated gas cost for the source network. */ function calculateSourceNetworkFee( quote: TransactionPayBridgeQuote, messenger: TransactionPayControllerMessenger, -): FiatValue { + { isMax = false } = {}, +): Amount { const { approval, trade } = quote; const approvalCost = approval - ? calculateTransactionCost(approval as TxData, messenger) - : { fiat: '0', usd: '0' }; + ? calculateTransactionCost(approval as TxData, messenger, { isMax }) + : { fiat: '0', human: '0', raw: '0', usd: '0' }; - const tradeCost = calculateTransactionCost(trade as TxData, messenger); + const tradeCost = calculateTransactionCost(trade as TxData, messenger, { + isMax, + }); return { fiat: new BigNumber(approvalCost.fiat).plus(tradeCost.fiat).toString(10), + human: new BigNumber(approvalCost.human).plus(tradeCost.human).toString(10), + raw: new BigNumber(approvalCost.raw).plus(tradeCost.raw).toString(10), usd: new BigNumber(approvalCost.usd).plus(tradeCost.usd).toString(10), }; } @@ -601,17 +615,22 @@ function calculateSourceNetworkFee( * * @param transaction - Transaction parameters. * @param messenger - Controller messenger + * @param options - Calculation options. + * @param options.isMax - Whether to calculate the maximum cost. * @returns Estimated gas cost for a bridge transaction. */ function calculateTransactionCost( transaction: TxData, messenger: TransactionPayControllerMessenger, -): FiatValue { - const { effectiveGas, gasLimit } = transaction; + { isMax }: { isMax: boolean }, +): Amount { + const { effectiveGas: effectiveGasOriginal, gasLimit } = transaction; + const effectiveGas = isMax ? undefined : effectiveGasOriginal; return calculateGasCost({ ...transaction, gas: effectiveGas || gasLimit || '0x0', messenger, + isMax, }); } diff --git a/packages/transaction-pay-controller/src/strategy/relay/constants.ts b/packages/transaction-pay-controller/src/strategy/relay/constants.ts index 9d41f1836e0..b176d7b73f6 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/constants.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/constants.ts @@ -1,7 +1,3 @@ -export const ARBITRUM_USDC_ADDRESS = - '0xaf88d065e77c8cC2239327C5EDb3A432268e5831'; -export const CHAIN_ID_ARBITRUM = '0xa4b1'; -export const CHAIN_ID_POLYGON = '0x89'; export const CHAIN_ID_HYPERCORE = '0x539'; export const RELAY_URL_BASE = 'https://api.relay.link'; export const RELAY_URL_QUOTE = `${RELAY_URL_BASE}/quote`; 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 49a5e654658..b2ce1efce98 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 @@ -3,15 +3,15 @@ import type { TransactionMeta } from '@metamask/transaction-controller'; import type { Hex } from '@metamask/utils'; import { cloneDeep } from 'lodash'; +import { RELAY_URL_QUOTE } from './constants'; +import { getRelayQuotes } from './relay-quotes'; +import type { RelayQuote } from './types'; import { ARBITRUM_USDC_ADDRESS, CHAIN_ID_ARBITRUM, CHAIN_ID_POLYGON, - RELAY_URL_QUOTE, -} from './constants'; -import { getRelayQuotes } from './relay-quotes'; -import type { RelayQuote } from './types'; -import { NATIVE_TOKEN_ADDRESS } from '../../constants'; + NATIVE_TOKEN_ADDRESS, +} from '../../constants'; import { getMessengerMock } from '../../tests/messenger-mock'; import type { GetDelegationTransactionCallback, @@ -41,6 +41,9 @@ const QUOTE_REQUEST_MOCK: QuoteRequest = { const QUOTE_MOCK = { details: { + currencyIn: { + amountUsd: '1.24', + }, currencyOut: { amountFormatted: '1.0', amountUsd: '1.23', @@ -122,13 +125,17 @@ describe('Relay Quotes Utils', () => { }); calculateTransactionGasCostMock.mockReturnValue({ - usd: '1.23', fiat: '2.34', + human: '0.615', + raw: '6150000000000000', + usd: '1.23', }); calculateGasCostMock.mockReturnValue({ - usd: '3.45', fiat: '4.56', + human: '1.725', + raw: '1725000000000000', + usd: '3.45', }); getRemoteFeatureFlagControllerStateMock.mockReturnValue({ @@ -288,7 +295,7 @@ describe('Relay Quotes Utils', () => { expect(result[0].estimatedDuration).toBe(300); }); - it('includes provider fee in quote', async () => { + it('includes provider fee from relayer fee', async () => { successfulFetchMock.mockResolvedValue({ json: async () => QUOTE_MOCK, } as never); @@ -305,6 +312,26 @@ describe('Relay Quotes Utils', () => { }); }); + it('includes provider fee from usd change if greater', async () => { + const quote = cloneDeep(QUOTE_MOCK); + quote.details.currencyIn.amountUsd = '3.00'; + + successfulFetchMock.mockResolvedValue({ + json: async () => quote, + } as never); + + const result = await getRelayQuotes({ + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }); + + expect(result[0].fees.provider).toStrictEqual({ + usd: '1.77', + fiat: '3.54', + }); + }); + it('includes dust in quote', async () => { successfulFetchMock.mockResolvedValue({ json: async () => QUOTE_MOCK, @@ -334,8 +361,18 @@ describe('Relay Quotes Utils', () => { }); expect(result[0].fees.sourceNetwork).toStrictEqual({ - usd: '3.45', - fiat: '4.56', + estimate: { + fiat: '4.56', + human: '1.725', + raw: '1725000000000000', + usd: '3.45', + }, + max: { + fiat: '4.56', + human: '1.725', + raw: '1725000000000000', + usd: '3.45', + }, }); }); 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 0d723ec5795..e00eb5b1b5a 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts @@ -5,19 +5,22 @@ import { createModuleLogger } from '@metamask/utils'; import { BigNumber } from 'bignumber.js'; import { - ARBITRUM_USDC_ADDRESS, - CHAIN_ID_ARBITRUM, CHAIN_ID_HYPERCORE, - CHAIN_ID_POLYGON, RELAY_FALLBACK_GAS_LIMIT, RELAY_URL_QUOTE, } from './constants'; import type { RelayQuote } from './types'; import { TransactionPayStrategy } from '../..'; import type { TransactionMeta } from '../../../../transaction-controller/src'; -import { NATIVE_TOKEN_ADDRESS } from '../../constants'; +import { + ARBITRUM_USDC_ADDRESS, + CHAIN_ID_ARBITRUM, + CHAIN_ID_POLYGON, + NATIVE_TOKEN_ADDRESS, +} from '../../constants'; import { projectLogger } from '../../logger'; import type { + Amount, FiatValue, PayStrategyGetQuotesRequest, QuoteRequest, @@ -228,7 +231,8 @@ function normalizeQuote( fullRequest: PayStrategyGetQuotesRequest, ): TransactionPayQuote { const { messenger } = fullRequest; - const { details, fees } = quote; + const { details } = quote; + const { currencyIn } = details; const { usdToFiatRate } = getFiatRates(messenger, request); @@ -238,7 +242,7 @@ function normalizeQuote( ); const provider = getFiatValueFromUsd( - new BigNumber(fees.relayer.amountUsd), + calculateProviderFee(quote), usdToFiatRate, ); @@ -249,6 +253,12 @@ function normalizeQuote( fiat: '0', }; + const sourceAmount: Amount = { + human: currencyIn.amountFormatted, + raw: currencyIn.amount, + ...getFiatValueFromUsd(new BigNumber(currencyIn.amountUsd), usdToFiatRate), + }; + return { dust, estimatedDuration: details.timeEstimate, @@ -259,6 +269,7 @@ function normalizeQuote( }, original: quote, request, + sourceAmount, strategy: TransactionPayStrategy.Relay, }; } @@ -370,15 +381,25 @@ function getFeatureFlags(messenger: TransactionPayControllerMessenger) { function calculateSourceNetworkCost( quote: RelayQuote, messenger: TransactionPayControllerMessenger, -) { +): TransactionPayQuote['fees']['sourceNetwork'] { const allParams = quote.steps[0].items.map((i) => i.data); + const { chainId } = allParams[0]; const totalGasLimit = calculateSourceNetworkGasLimit(allParams); - return calculateGasCost({ - chainId: allParams[0].chainId, + const estimate = calculateGasCost({ + chainId, + gas: totalGasLimit, + messenger, + }); + + const max = calculateGasCost({ + chainId, gas: totalGasLimit, messenger, + isMax: true, }); + + return { estimate, max }; } /** @@ -408,3 +429,19 @@ function calculateSourceNetworkGasLimit( 0, ); } + +/** + * Calculate the provider fee for a Relay quote. + * + * @param quote - Relay quote. + * @returns - Provider fee in USD. + */ +function calculateProviderFee(quote: RelayQuote) { + const relayerFee = new BigNumber(quote.fees.relayer.amountUsd); + + const valueLoss = new BigNumber(quote.details.currencyIn.amountUsd).minus( + quote.details.currencyOut.amountUsd, + ); + + return relayerFee.gt(valueLoss) ? relayerFee : valueLoss; +} diff --git a/packages/transaction-pay-controller/src/strategy/relay/types.ts b/packages/transaction-pay-controller/src/strategy/relay/types.ts index 5d35ccc4e32..bc3ed2560e8 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/types.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/types.ts @@ -3,6 +3,7 @@ import type { Hex } from '@metamask/utils'; export type RelayQuote = { details: { currencyIn: { + 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 c38ad9b348c..3207baf5899 100644 --- a/packages/transaction-pay-controller/src/strategy/test/TestStrategy.test.ts +++ b/packages/transaction-pay-controller/src/strategy/test/TestStrategy.test.ts @@ -40,8 +40,18 @@ describe('TestStrategy', () => { usd: expect.any(String), }, sourceNetwork: { - fiat: expect.any(String), - usd: expect.any(String), + estimate: { + fiat: expect.any(String), + human: expect.any(String), + raw: expect.any(String), + usd: expect.any(String), + }, + max: { + fiat: expect.any(String), + human: expect.any(String), + raw: expect.any(String), + usd: expect.any(String), + }, }, targetNetwork: { fiat: expect.any(String), @@ -50,6 +60,12 @@ describe('TestStrategy', () => { }, original: undefined, request: REQUEST_MOCK, + sourceAmount: { + human: expect.any(String), + fiat: expect.any(String), + raw: expect.any(String), + usd: expect.any(String), + }, strategy: TransactionPayStrategy.Test, }, ]); diff --git a/packages/transaction-pay-controller/src/strategy/test/TestStrategy.ts b/packages/transaction-pay-controller/src/strategy/test/TestStrategy.ts index 35be85dedb9..e01e2936074 100644 --- a/packages/transaction-pay-controller/src/strategy/test/TestStrategy.ts +++ b/packages/transaction-pay-controller/src/strategy/test/TestStrategy.ts @@ -27,11 +27,33 @@ export class TestStrategy implements PayStrategy { estimatedDuration: 5, fees: { provider: { fiat: '1.23', usd: '1.23' }, - sourceNetwork: { fiat: '2.34', usd: '2.34' }, - targetNetwork: { fiat: '3.45', usd: '3.45' }, + sourceNetwork: { + estimate: { + human: '2.34', + fiat: '2.34', + usd: '2.34', + raw: '234000', + }, + max: { + human: '2.35', + fiat: '2.35', + usd: '2.35', + raw: '235000', + }, + }, + targetNetwork: { + fiat: '3.45', + usd: '3.45', + }, }, original: undefined, request: requests[0], + sourceAmount: { + human: '4.56', + fiat: '4.56', + raw: '456000', + usd: '4.56', + }, strategy: TransactionPayStrategy.Test, }, ]; diff --git a/packages/transaction-pay-controller/src/types.ts b/packages/transaction-pay-controller/src/types.ts index 530abe7d4a3..05c3fd1e1c0 100644 --- a/packages/transaction-pay-controller/src/types.ts +++ b/packages/transaction-pay-controller/src/types.ts @@ -273,11 +273,17 @@ export type QuoteRequest = { /** Fees associated with a transaction pay quote. */ export type TransactionPayFees = { + /** Whether a gas fee token is used to pay target network fees. */ + isTargetGasFeeToken?: boolean; + /** Fee charged by the quote provider. */ provider: FiatValue; /** Network fee for transactions on the source network. */ - sourceNetwork: FiatValue; + sourceNetwork: { + estimate: Amount; + max: Amount; + }; /** Network fee for transactions on the target network. */ targetNetwork: FiatValue; @@ -300,6 +306,9 @@ export type TransactionPayQuote = { /** Associated quote request. */ request: QuoteRequest; + /** Amount of source token required. */ + sourceAmount: Amount; + /** Name of the strategy used to retrieve the quote. */ strategy: TransactionPayStrategy; }; @@ -392,6 +401,9 @@ export type TransactionPayTotals = { /** Total fees for the target transaction and all quotes. */ fees: TransactionPayFees; + /** Total amount of source token required. */ + sourceAmount: Amount; + /** Overall total cost for the target transaction and all quotes. */ total: FiatValue; }; @@ -419,3 +431,12 @@ export type GetDelegationTransactionCallback = ({ to: Hex; value: Hex; }>; + +/** Single amount in alternate formats. */ +export type Amount = FiatValue & { + /** Amount in human-readable format factoring token decimals. */ + human: string; + + /** Amount in atomic format without factoring token decimals. */ + raw: string; +}; diff --git a/packages/transaction-pay-controller/src/utils/gas.test.ts b/packages/transaction-pay-controller/src/utils/gas.test.ts index 0671d203d91..afcba93be99 100644 --- a/packages/transaction-pay-controller/src/utils/gas.test.ts +++ b/packages/transaction-pay-controller/src/utils/gas.test.ts @@ -1,11 +1,14 @@ import { toHex } from '@metamask/controller-utils'; import type { Hex } from '@metamask/utils'; -import { cloneDeep } from 'lodash'; +import { clone, cloneDeep } from 'lodash'; import { calculateGasCost, calculateTransactionGasCost } from './gas'; -import { getTokenFiatRate } from './token'; +import { getTokenBalance, getTokenFiatRate, getTokenInfo } from './token'; import type { GasFeeEstimates } from '../../../gas-fee-controller/src'; -import type { TransactionMeta } from '../../../transaction-controller/src'; +import type { + GasFeeToken, + TransactionMeta, +} from '../../../transaction-controller/src'; import { getMessengerMock } from '../tests/messenger-mock'; jest.mock('./token'); @@ -14,13 +17,20 @@ const GAS_USED_MOCK = toHex(21000); const GAS_LIMIT_NO_BUFFER_MOCK = toHex(30000); const GAS_MOCK = toHex(40000); const MAX_PRIORITY_FEE_PER_GAS_MOCK = toHex(2500000000); -const MAX_FEE_PER_GAS_MOCK = toHex(5500000000); +const MAX_FEE_PER_GAS_MOCK = toHex(750000000); const CHAIN_ID_MOCK = '0x1' as Hex; +const TOKEN_ADDRESS_MOCK = '0x789' as Hex; + +const GAS_FEE_TOKEN_MOCK = { + amount: toHex(1230000), + tokenAddress: TOKEN_ADDRESS_MOCK, +} as GasFeeToken; const TRANSACTION_META_MOCK = { chainId: CHAIN_ID_MOCK as Hex, gasUsed: GAS_USED_MOCK, txParams: { + gas: GAS_MOCK, maxPriorityFeePerGas: MAX_PRIORITY_FEE_PER_GAS_MOCK, }, } as TransactionMeta; @@ -31,7 +41,7 @@ const GAS_FEE_CONTROLLER_STATE_MOCK = { gasFeeEstimates: { estimatedBaseFee: '4', medium: { - suggestedMaxFeePerGas: '5', + suggestedMaxFeePerGas: '7', suggestedMaxPriorityFeePerGas: '2', }, } as GasFeeEstimates, @@ -41,17 +51,25 @@ const GAS_FEE_CONTROLLER_STATE_MOCK = { describe('Gas Utils', () => { const getTokenFiatRateMock = jest.mocked(getTokenFiatRate); + const getTokenInfoMock = jest.mocked(getTokenInfo); + const getTokenBalanceMock = jest.mocked(getTokenBalance); const { messenger, getGasFeeControllerStateMock } = getMessengerMock(); beforeEach(() => { jest.resetAllMocks(); getGasFeeControllerStateMock.mockReturnValue(GAS_FEE_CONTROLLER_STATE_MOCK); + getTokenBalanceMock.mockReturnValue('147000000000000'); getTokenFiatRateMock.mockReturnValue({ usdRate: '4000', fiatRate: '2000', }); + + getTokenInfoMock.mockReturnValue({ + decimals: 6, + symbol: 'TST', + }); }); describe('calculateGasCost', () => { @@ -64,6 +82,8 @@ describe('Gas Utils', () => { expect(result).toStrictEqual({ fiat: '0.48', + human: '0.00024', + raw: '240000000000000', usd: '0.96', }); }); @@ -87,6 +107,8 @@ describe('Gas Utils', () => { expect(result).toStrictEqual({ fiat: '0.52', + human: '0.00026', + raw: '260000000000000', usd: '1.04', }); }); @@ -108,8 +130,10 @@ describe('Gas Utils', () => { }); expect(result).toStrictEqual({ - fiat: '0.4', - usd: '0.8', + fiat: '0.56', + human: '0.00028', + raw: '280000000000000', + usd: '1.12', }); }); @@ -124,8 +148,10 @@ describe('Gas Utils', () => { }); expect(result).toStrictEqual({ - fiat: '0.44', - usd: '0.88', + fiat: '0.06', + human: '0.00003', + raw: '30000000000000', + usd: '0.12', }); }); @@ -140,6 +166,8 @@ describe('Gas Utils', () => { expect(result).toStrictEqual({ fiat: '0', + human: '0', + raw: '0', usd: '0', }); }); @@ -155,6 +183,22 @@ describe('Gas Utils', () => { }), ).toThrow('Could not fetch fiat rate for native token'); }); + + it('returns gas cost using max fee if isMax', () => { + const result = calculateGasCost({ + chainId: CHAIN_ID_MOCK, + gas: GAS_MOCK, + isMax: true, + messenger, + }); + + expect(result).toStrictEqual({ + fiat: '0.56', + human: '0.00028', + raw: '280000000000000', + usd: '1.12', + }); + }); }); describe('calculateTransactionGasCost', () => { @@ -166,6 +210,8 @@ describe('Gas Utils', () => { expect(result).toStrictEqual({ fiat: '0.273', + human: '0.0001365', + raw: '136500000000000', usd: '0.546', }); }); @@ -181,6 +227,8 @@ describe('Gas Utils', () => { expect(result).toStrictEqual({ fiat: '0.39', + human: '0.000195', + raw: '195000000000000', usd: '0.78', }); }); @@ -199,6 +247,8 @@ describe('Gas Utils', () => { expect(result).toStrictEqual({ fiat: '0.52', + human: '0.00026', + raw: '260000000000000', usd: '1.04', }); }); @@ -217,8 +267,121 @@ describe('Gas Utils', () => { expect(result).toStrictEqual({ fiat: '0', + human: '0', + raw: '0', usd: '0', }); }); + + it('does not use gasUsed if isMax', () => { + const result = calculateTransactionGasCost( + TRANSACTION_META_MOCK, + messenger, + { isMax: true }, + ); + + expect(result).toStrictEqual({ + fiat: '0.56', + human: '0.00028', + raw: '280000000000000', + usd: '1.12', + }); + }); + + it('returns gas fee token cost if selected gas fee token', () => { + const transactionMeta = clone(TRANSACTION_META_MOCK); + + transactionMeta.gasFeeTokens = [GAS_FEE_TOKEN_MOCK]; + transactionMeta.selectedGasFeeToken = TOKEN_ADDRESS_MOCK; + + const result = calculateTransactionGasCost(transactionMeta, messenger); + + expect(result).toStrictEqual({ + isGasFeeToken: true, + fiat: '2460', + human: '1.23', + raw: '1230000', + usd: '4920', + }); + }); + + it('does not return gas fee token if sufficient native balance and isGasFeeTokenIgnoredIfBalance', () => { + const transactionMeta = clone(TRANSACTION_META_MOCK); + + transactionMeta.gasFeeTokens = [GAS_FEE_TOKEN_MOCK]; + transactionMeta.selectedGasFeeToken = TOKEN_ADDRESS_MOCK; + transactionMeta.isGasFeeTokenIgnoredIfBalance = true; + + const result = calculateTransactionGasCost(transactionMeta, messenger); + + expect(result).toStrictEqual({ + fiat: '0.273', + human: '0.0001365', + raw: '136500000000000', + usd: '0.546', + }); + }); + + it('does not return gas fee token if token info is unavailable', () => { + const transactionMeta = clone(TRANSACTION_META_MOCK); + + transactionMeta.gasFeeTokens = [GAS_FEE_TOKEN_MOCK]; + transactionMeta.selectedGasFeeToken = TOKEN_ADDRESS_MOCK; + + getTokenInfoMock.mockReturnValue(undefined); + + const result = calculateTransactionGasCost(transactionMeta, messenger); + + expect(result).toStrictEqual({ + fiat: '0.273', + human: '0.0001365', + raw: '136500000000000', + usd: '0.546', + }); + }); + + it('does not return gas fee token if fiat rate is unavailable', () => { + const transactionMeta = clone(TRANSACTION_META_MOCK); + + transactionMeta.gasFeeTokens = [GAS_FEE_TOKEN_MOCK]; + transactionMeta.selectedGasFeeToken = TOKEN_ADDRESS_MOCK; + + getTokenFiatRateMock.mockReset(); + getTokenFiatRateMock + .mockReturnValueOnce({ + usdRate: '4000', + fiatRate: '2000', + }) + .mockReturnValueOnce({ + usdRate: '4000', + fiatRate: '2000', + }) + .mockReturnValueOnce(undefined); + + const result = calculateTransactionGasCost(transactionMeta, messenger); + + expect(result).toStrictEqual({ + fiat: '0.273', + human: '0.0001365', + raw: '136500000000000', + usd: '0.546', + }); + }); + + it('does not return gas fee token if selected gas fee token not found', () => { + const transactionMeta = clone(TRANSACTION_META_MOCK); + + transactionMeta.gasFeeTokens = [GAS_FEE_TOKEN_MOCK]; + transactionMeta.selectedGasFeeToken = '0x0' as Hex; + + const result = calculateTransactionGasCost(transactionMeta, messenger); + + expect(result).toStrictEqual({ + fiat: '0.273', + human: '0.0001365', + raw: '136500000000000', + usd: '0.546', + }); + }); }); }); diff --git a/packages/transaction-pay-controller/src/utils/gas.ts b/packages/transaction-pay-controller/src/utils/gas.ts index d28c0f2135c..04a350de421 100644 --- a/packages/transaction-pay-controller/src/utils/gas.ts +++ b/packages/transaction-pay-controller/src/utils/gas.ts @@ -4,9 +4,17 @@ import type { TransactionMeta } from '@metamask/transaction-controller'; import type { Hex } from '@metamask/utils'; import { BigNumber } from 'bignumber.js'; -import { getNativeToken, getTokenFiatRate } from './token'; +import { + getNativeToken, + getTokenBalance, + getTokenFiatRate, + getTokenInfo, +} from './token'; import type { TransactionPayControllerMessenger } from '..'; -import type { FiatValue } from '../types'; +import { createModuleLogger, projectLogger } from '../logger'; +import type { Amount } from '../types'; + +const log = createModuleLogger(projectLogger, 'gas'); /** * @@ -14,23 +22,62 @@ import type { FiatValue } from '../types'; * * @param transaction - Transaction to calculate gas cost for * @param messenger - Controller messenger. + * @param options - Calculation options. + * @param options.isMax - Whether to calculate the maximum fee. * @returns Estimated gas cost for the transaction. */ export function calculateTransactionGasCost( transaction: TransactionMeta, messenger: TransactionPayControllerMessenger, -): FiatValue { - const { chainId, gasUsed, gasLimitNoBuffer, txParams } = transaction; - const { gas, maxFeePerGas, maxPriorityFeePerGas } = txParams; + { isMax }: { isMax?: boolean } = {}, +): Amount & { isGasFeeToken?: boolean } { + const { + chainId, + gasUsed: gasUsedOriginal, + gasLimitNoBuffer, + txParams, + } = transaction; + + const { from, gas, maxFeePerGas, maxPriorityFeePerGas } = txParams; + const gasUsed = isMax ? undefined : gasUsedOriginal; const finalGas = gasUsed || gasLimitNoBuffer || gas || '0x0'; - return calculateGasCost({ + const result = calculateGasCost({ chainId, gas: finalGas, + isMax, maxFeePerGas, maxPriorityFeePerGas, messenger, }); + + const max = calculateGasCost({ + chainId, + gas: finalGas, + isMax: true, + messenger, + }); + + const nativeBalance = getTokenBalance( + messenger, + from as Hex, + chainId, + getNativeToken(chainId), + ); + + const hasBalance = new BigNumber(nativeBalance).gte(max.raw); + + const gasFeeTokenCost = calculateGasFeeTokenCost({ + hasBalance, + messenger, + transaction, + }); + + if (gasFeeTokenCost) { + return gasFeeTokenCost; + } + + return result; } /** @@ -39,21 +86,25 @@ export function calculateTransactionGasCost( * @param request - Gas cost calculation parameters. * @param request.chainId - ID of the chain. * @param request.gas - Amount of gas the transaction will use. + * @param request.isMax - Whether to calculate the maximum fee. * @param request.maxFeePerGas - Max fee to pay per gas. * @param request.maxPriorityFeePerGas - Max priority fee to pay per gas. * @param request.messenger - Controller messenger. + * @returns Estimated gas cost for the transaction. */ export function calculateGasCost(request: { chainId: number | Hex; gas: BigNumber.Value; + isMax?: boolean; maxFeePerGas?: BigNumber.Value; maxPriorityFeePerGas?: BigNumber.Value; messenger: TransactionPayControllerMessenger; -}): FiatValue { +}): Amount { const { chainId: chainIdInput, gas, + isMax, maxFeePerGas: maxFeePerGasInput, maxPriorityFeePerGas: maxPriorityFeePerGasInput, messenger, @@ -73,13 +124,15 @@ export function calculateGasCost(request: { maxPriorityFeePerGasInput || maxPriorityFeePerGasEstimate; const feePerGas = - estimatedBaseFee && maxPriorityFeePerGas + estimatedBaseFee && maxPriorityFeePerGas && !isMax ? new BigNumber(estimatedBaseFee).plus(maxPriorityFeePerGas) : new BigNumber(maxFeePerGas || '0x0'); - const gasCostNative = new BigNumber(gas) - .multipliedBy(feePerGas) - .shiftedBy(-18); + const rawValue = new BigNumber(gas).multipliedBy(feePerGas); + const raw = rawValue.toString(10); + + const humanValue = rawValue.shiftedBy(-18); + const human = humanValue.toString(10); const fiatRate = getTokenFiatRate( messenger, @@ -91,12 +144,14 @@ export function calculateGasCost(request: { throw new Error('Could not fetch fiat rate for native token'); } - const usd = gasCostNative.multipliedBy(fiatRate.usdRate).toString(10); - const fiat = gasCostNative.multipliedBy(fiatRate.fiatRate).toString(10); + const usd = humanValue.multipliedBy(fiatRate.usdRate).toString(10); + const fiat = humanValue.multipliedBy(fiatRate.fiatRate).toString(10); return { - usd, fiat, + human, + raw, + usd, }; } @@ -132,3 +187,82 @@ function getGasFee(chainId: Hex, messenger: TransactionPayControllerMessenger) { return { estimatedBaseFee, maxFeePerGas, maxPriorityFeePerGas }; } + +/** + * Calculate the cost of a gas fee token on a transaction. + * + * @param request - Request parameters. + * @param request.hasBalance - Whether the user has enough balance to cover the gas fee. + * @param request.messenger - Controller messenger. + * @param request.transaction - Transaction to calculate gas fee token cost for. + * @returns Cost of the gas fee token. + */ +function calculateGasFeeTokenCost({ + hasBalance, + messenger, + transaction, +}: { + hasBalance: boolean; + messenger: TransactionPayControllerMessenger; + transaction: TransactionMeta; +}): (Amount & { isGasFeeToken?: boolean }) | undefined { + const { + chainId, + gasFeeTokens, + isGasFeeTokenIgnoredIfBalance, + selectedGasFeeToken, + } = transaction; + + if ( + !gasFeeTokens || + !selectedGasFeeToken || + (isGasFeeTokenIgnoredIfBalance && hasBalance) + ) { + return undefined; + } + + log('Calculating gas fee token cost', { selectedGasFeeToken, chainId }); + + const gasFeeToken = gasFeeTokens?.find( + (t) => t.tokenAddress.toLowerCase() === selectedGasFeeToken.toLowerCase(), + ); + + if (!gasFeeToken) { + log('Gas fee token not found', { + gasFeeTokens, + selectedGasFeeToken, + }); + + return undefined; + } + + const tokenInfo = getTokenInfo(messenger, selectedGasFeeToken, chainId); + + const tokenFiatRate = getTokenFiatRate( + messenger, + selectedGasFeeToken, + chainId, + ); + + if (!tokenFiatRate || !tokenInfo) { + log('Cannot get gas fee token info'); + return undefined; + } + + const rawValue = new BigNumber(gasFeeToken.amount); + const raw = rawValue.toString(10); + + const humanValue = rawValue.shiftedBy(-tokenInfo.decimals); + const human = humanValue.toString(10); + + const fiat = humanValue.multipliedBy(tokenFiatRate.fiatRate).toString(10); + const usd = humanValue.multipliedBy(tokenFiatRate.usdRate).toString(10); + + return { + isGasFeeToken: true, + fiat, + human, + raw, + usd, + }; +} diff --git a/packages/transaction-pay-controller/src/utils/quotes.test.ts b/packages/transaction-pay-controller/src/utils/quotes.test.ts index af60914f3d5..9de6050952a 100644 --- a/packages/transaction-pay-controller/src/utils/quotes.test.ts +++ b/packages/transaction-pay-controller/src/utils/quotes.test.ts @@ -69,8 +69,10 @@ const TOTALS_MOCK = { usd: '8.90', }, sourceNetwork: { - fiat: '9.01', - usd: '1.12', + estimate: { + fiat: '9.01', + usd: '1.12', + }, }, }, total: { @@ -299,7 +301,7 @@ describe('Quotes Utils', () => { metamaskPay: { bridgeFeeFiat: TOTALS_MOCK.fees.provider.usd, chainId: TRANSACTION_DATA_MOCK.paymentToken?.chainId, - networkFeeFiat: TOTALS_MOCK.fees.sourceNetwork.usd, + networkFeeFiat: TOTALS_MOCK.fees.sourceNetwork.estimate.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 d5d94581200..f396ba94b6d 100644 --- a/packages/transaction-pay-controller/src/utils/quotes.ts +++ b/packages/transaction-pay-controller/src/utils/quotes.ts @@ -149,7 +149,7 @@ function syncTransaction({ tx.metamaskPay = { bridgeFeeFiat: totals.fees.provider.usd, chainId: paymentToken.chainId, - networkFeeFiat: totals.fees.sourceNetwork.usd, + networkFeeFiat: totals.fees.sourceNetwork.estimate.usd, tokenAddress: paymentToken.address, totalFiat: totals.total.usd, }; 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 7a8bfc90921..184e51cb2d1 100644 --- a/packages/transaction-pay-controller/src/utils/source-amounts.test.ts +++ b/packages/transaction-pay-controller/src/utils/source-amounts.test.ts @@ -2,10 +2,7 @@ import { updateSourceAmounts } from './source-amounts'; import { getTokenFiatRate } from './token'; import { getTransaction } from './transaction'; import { TransactionPayStrategy, type TransactionPaymentToken } from '..'; -import { - ARBITRUM_USDC_ADDRESS, - CHAIN_ID_ARBITRUM, -} from '../strategy/relay/constants'; +import { ARBITRUM_USDC_ADDRESS, CHAIN_ID_ARBITRUM } from '../constants'; import { getMessengerMock } from '../tests/messenger-mock'; import type { TransactionData, TransactionPayRequiredToken } from '../types'; diff --git a/packages/transaction-pay-controller/src/utils/source-amounts.ts b/packages/transaction-pay-controller/src/utils/source-amounts.ts index 9907dba2dab..80723a554cf 100644 --- a/packages/transaction-pay-controller/src/utils/source-amounts.ts +++ b/packages/transaction-pay-controller/src/utils/source-amounts.ts @@ -9,11 +9,8 @@ import type { } from '..'; import { TransactionPayStrategy } from '..'; import type { TransactionMeta } from '../../../transaction-controller/src'; +import { ARBITRUM_USDC_ADDRESS, CHAIN_ID_ARBITRUM } from '../constants'; import { projectLogger } from '../logger'; -import { - ARBITRUM_USDC_ADDRESS, - CHAIN_ID_ARBITRUM, -} from '../strategy/relay/constants'; import type { TransactionPaySourceAmount, TransactionData, diff --git a/packages/transaction-pay-controller/src/utils/token.test.ts b/packages/transaction-pay-controller/src/utils/token.test.ts index 3e95bfe857f..ddc09cbecb3 100644 --- a/packages/transaction-pay-controller/src/utils/token.test.ts +++ b/packages/transaction-pay-controller/src/utils/token.test.ts @@ -10,7 +10,11 @@ import { getAllTokenBalances, getNativeToken, } from './token'; -import { NATIVE_TOKEN_ADDRESS } from '../constants'; +import { + CHAIN_ID_POLYGON, + NATIVE_TOKEN_ADDRESS, + POLYGON_USDCE_ADDRESS, +} from '../constants'; import { getMessengerMock } from '../tests/messenger-mock'; const TOKEN_ADDRESS_MOCK = '0x559B65722aD62AD6DAC4Fa5a1c6B23A2e8ce57Ec' as Hex; @@ -367,6 +371,44 @@ describe('Token Utils', () => { usdRate: '4', }); }); + + it('returns fixed usd rate for stablecoins', () => { + findNetworkClientIdByChainIdMock.mockReturnValue(NETWORK_CLIENT_ID_MOCK); + + getNetworkClientByIdMock.mockReturnValue({ + configuration: { ticker: TICKER_MOCK }, + } as never); + + getTokenRatesControllerStateMock.mockReturnValue({ + marketData: { + [CHAIN_ID_POLYGON]: { + [POLYGON_USDCE_ADDRESS]: { + price: 1.0, + }, + }, + }, + } as TokenRatesControllerState); + + getCurrencyRateControllerStateMock.mockReturnValue({ + currencyRates: { + [TICKER_MOCK]: { + conversionRate: 3.0, + usdConversionRate: 4.0, + }, + }, + }); + + const result = getTokenFiatRate( + messenger, + POLYGON_USDCE_ADDRESS, + CHAIN_ID_POLYGON, + ); + + expect(result).toStrictEqual({ + fiatRate: '3', + usdRate: '1', + }); + }); }); describe('getNativeToken', () => { diff --git a/packages/transaction-pay-controller/src/utils/token.ts b/packages/transaction-pay-controller/src/utils/token.ts index 6883ca5fd9c..6fcba9a4786 100644 --- a/packages/transaction-pay-controller/src/utils/token.ts +++ b/packages/transaction-pay-controller/src/utils/token.ts @@ -3,9 +3,20 @@ import type { Hex } from '@metamask/utils'; import { BigNumber } from 'bignumber.js'; import { uniq } from 'lodash'; -import { NATIVE_TOKEN_ADDRESS } from '../constants'; +import { + ARBITRUM_USDC_ADDRESS, + CHAIN_ID_ARBITRUM, + CHAIN_ID_POLYGON, + NATIVE_TOKEN_ADDRESS, + POLYGON_USDCE_ADDRESS, +} from '../constants'; import type { FiatRates, TransactionPayControllerMessenger } from '../types'; +const STABLECOINS: Record = { + [CHAIN_ID_ARBITRUM]: [ARBITRUM_USDC_ADDRESS.toLowerCase() as Hex], + [CHAIN_ID_POLYGON]: [POLYGON_USDCE_ADDRESS.toLowerCase() as Hex], +}; + /** * Get the token balance for a specific account and token. * @@ -13,7 +24,7 @@ import type { FiatRates, TransactionPayControllerMessenger } from '../types'; * @param account - Address of the account. * @param chainId - Id of the chain. * @param tokenAddress - Address of the token contract. - * @returns The token balance as a BigNumber. + * @returns Raw token balance as a decimal string. */ export function getTokenBalance( messenger: TransactionPayControllerMessenger, @@ -186,10 +197,15 @@ export function getTokenFiatRate( if (nativeToFiatRate === null || nativeToUsdRate === null) { return undefined; } + const isStablecoin = STABLECOINS[chainId]?.includes( + tokenAddress.toLowerCase() as Hex, + ); - const usdRate = new BigNumber(tokenToNativeRate ?? 1) - .multipliedBy(nativeToUsdRate) - .toString(10); + const usdRate = isStablecoin + ? '1' + : new BigNumber(tokenToNativeRate ?? 1) + .multipliedBy(nativeToUsdRate) + .toString(10); const fiatRate = new BigNumber(tokenToNativeRate ?? 1) .multipliedBy(nativeToFiatRate) diff --git a/packages/transaction-pay-controller/src/utils/totals.test.ts b/packages/transaction-pay-controller/src/utils/totals.test.ts index 734e0926e96..93111ab94b8 100644 --- a/packages/transaction-pay-controller/src/utils/totals.test.ts +++ b/packages/transaction-pay-controller/src/utils/totals.test.ts @@ -28,8 +28,18 @@ const QUOTE_1_MOCK: TransactionPayQuote = { usd: '2.22', }, sourceNetwork: { - fiat: '3.33', - usd: '4.44', + estimate: { + fiat: '3.33', + human: '3.33', + raw: '333000000000000', + usd: '4.44', + }, + max: { + fiat: '3.34', + human: '3.34', + raw: '334000000000000', + usd: '4.45', + }, }, targetNetwork: { fiat: '5.55', @@ -38,6 +48,12 @@ const QUOTE_1_MOCK: TransactionPayQuote = { }, original: undefined, request: {} as QuoteRequest, + sourceAmount: { + human: '7.77', + fiat: '7.77', + raw: '777000000000000', + usd: '8.88', + }, strategy: TransactionPayStrategy.Test, }; @@ -63,8 +79,18 @@ const QUOTE_2_MOCK: TransactionPayQuote = { usd: '8.88', }, sourceNetwork: { - fiat: '9.99', - usd: '10.10', + estimate: { + fiat: '9.99', + human: '9.99', + raw: '999000000000000', + usd: '10.10', + }, + max: { + fiat: '9.999', + human: '9.999', + raw: '999900000000000', + usd: '10.11', + }, }, targetNetwork: { fiat: '11.11', @@ -73,6 +99,12 @@ const QUOTE_2_MOCK: TransactionPayQuote = { }, original: undefined, request: {} as QuoteRequest, + sourceAmount: { + human: '13.13', + fiat: '13.13', + raw: '1313000000000000', + usd: '14.14', + }, strategy: TransactionPayStrategy.Test, }; @@ -87,7 +119,10 @@ describe('Totals Utils', () => { jest.resetAllMocks(); calculateTransactionGasCostMock.mockReturnValue({ + isGasFeeToken: true, fiat: '1.23', + human: '1.23', + raw: '1230000000000000', usd: '2.34', }); }); @@ -156,8 +191,10 @@ describe('Totals Utils', () => { transaction: TRANSACTION_META_MOCK, }); - expect(result.fees.sourceNetwork.fiat).toBe('13.32'); - expect(result.fees.sourceNetwork.usd).toBe('14.54'); + expect(result.fees.sourceNetwork.estimate.fiat).toBe('13.32'); + expect(result.fees.sourceNetwork.estimate.usd).toBe('14.54'); + expect(result.fees.sourceNetwork.max.fiat).toBe('13.339'); + expect(result.fees.sourceNetwork.max.usd).toBe('14.56'); }); it('returns target network fees', () => { @@ -182,6 +219,19 @@ describe('Totals Utils', () => { expect(result.fees.targetNetwork.fiat).toBe('1.23'); expect(result.fees.targetNetwork.usd).toBe('2.34'); + expect(result.fees.isTargetGasFeeToken).toBe(true); + }); + + it('returns source amount', () => { + const result = calculateTotals({ + quotes: [QUOTE_1_MOCK, QUOTE_2_MOCK], + tokens: [], + messenger: MESSENGER_MOCK, + transaction: TRANSACTION_META_MOCK, + }); + + expect(result.sourceAmount.fiat).toBe('20.9'); + expect(result.sourceAmount.usd).toBe('23.02'); }); }); }); diff --git a/packages/transaction-pay-controller/src/utils/totals.ts b/packages/transaction-pay-controller/src/utils/totals.ts index 9104e7e1706..021f67113f1 100644 --- a/packages/transaction-pay-controller/src/utils/totals.ts +++ b/packages/transaction-pay-controller/src/utils/totals.ts @@ -3,6 +3,8 @@ import { BigNumber } from 'bignumber.js'; import { calculateTransactionGasCost } from './gas'; import type { + Amount, + FiatValue, TransactionPayControllerMessenger, TransactionPayQuote, TransactionPayRequiredToken, @@ -30,24 +32,14 @@ export function calculateTotals({ tokens: TransactionPayRequiredToken[]; transaction: TransactionMeta; }): TransactionPayTotals { - const providerFeeFiat = sumProperty( - quotes, - (quote) => quote.fees.provider.fiat, - ); - - const providerFeeUsd = sumProperty( - quotes, - (quote) => quote.fees.provider.usd, - ); + const providerFee = sumFiat(quotes.map((quote) => quote.fees.provider)); - const sourceNetworkFeeFiat = sumProperty( - quotes, - (quote) => quote.fees.sourceNetwork.fiat, + const sourceNetworkFeeMax = sumAmounts( + quotes.map((quote) => quote.fees.sourceNetwork.max), ); - const sourceNetworkFeeUsd = sumProperty( - quotes, - (quote) => quote.fees.sourceNetwork.usd, + const sourceNetworkFeeEstimate = sumAmounts( + quotes.map((quote) => quote.fees.sourceNetwork.estimate), ); const transactionNetworkFee = calculateTransactionGasCost( @@ -55,27 +47,27 @@ export function calculateTotals({ messenger, ); - const targetNetworkFeeFiat = quotes?.length - ? sumProperty(quotes, (quote) => quote.fees.targetNetwork.fiat) - : transactionNetworkFee.fiat; - - const targetNetworkFeeUsd = quotes.length - ? sumProperty(quotes, (quote) => quote.fees.targetNetwork.usd) - : transactionNetworkFee.usd; + const targetNetworkFee = quotes?.length + ? { + ...sumFiat(quotes.map((quote) => quote.fees.targetNetwork)), + isGasFeeToken: false, + } + : transactionNetworkFee; + const sourceAmount = sumAmounts(quotes.map((quote) => quote.sourceAmount)); const quoteTokens = tokens.filter((t) => !t.skipIfBalance); const amountFiat = sumProperty(quoteTokens, (token) => token.amountFiat); const amountUsd = sumProperty(quoteTokens, (token) => token.amountUsd); - const totalFiat = new BigNumber(providerFeeFiat) - .plus(sourceNetworkFeeFiat) - .plus(targetNetworkFeeFiat) + const totalFiat = new BigNumber(providerFee.fiat) + .plus(sourceNetworkFeeEstimate.fiat) + .plus(targetNetworkFee.fiat) .plus(amountFiat) .toString(10); - const totalUsd = new BigNumber(providerFeeUsd) - .plus(sourceNetworkFeeUsd) - .plus(targetNetworkFeeUsd) + const totalUsd = new BigNumber(providerFee.usd) + .plus(sourceNetworkFeeEstimate.usd) + .plus(targetNetworkFee.usd) .plus(amountUsd) .toString(10); @@ -83,22 +75,22 @@ export function calculateTotals({ sumProperty(quotes, (quote) => quote.estimatedDuration), ); + const isTargetGasFeeToken = + targetNetworkFee.isGasFeeToken || + quotes.some((quote) => quote.fees.isTargetGasFeeToken); + return { estimatedDuration, fees: { - provider: { - fiat: providerFeeFiat, - usd: providerFeeUsd, - }, + isTargetGasFeeToken, + provider: providerFee, sourceNetwork: { - fiat: sourceNetworkFeeFiat, - usd: sourceNetworkFeeUsd, - }, - targetNetwork: { - fiat: targetNetworkFeeFiat, - usd: targetNetworkFeeUsd, + estimate: sourceNetworkFeeEstimate, + max: sourceNetworkFeeMax, }, + targetNetwork: targetNetworkFee, }, + sourceAmount, total: { fiat: totalFiat, usd: totalUsd, @@ -106,6 +98,36 @@ export function calculateTotals({ }; } +/** + * Sum a list of amounts. + * + * @param data - List of amounts. + * @returns Total amount. + */ +function sumAmounts(data: Amount[]): Amount { + const fiatValue = sumFiat(data); + const human = sumProperty(data, (item) => item.human); + const raw = sumProperty(data, (item) => item.raw); + + return { + ...fiatValue, + human, + raw, + }; +} + +/** + * Sum a list of fiat value. + * + * @param data - List of fiat values. + * @returns Total fiat value. + */ +function sumFiat(data: FiatValue[]): FiatValue { + const fiat = sumProperty(data, (item) => item.fiat); + const usd = sumProperty(data, (item) => item.usd); + return { fiat, usd }; +} + /** * Sum a specific property from a list of items. *