From fe50da2fea92620ec5a7b8c48a9ce2a948a51223 Mon Sep 17 00:00:00 2001 From: Charlie McKenzie Date: Fri, 10 May 2024 15:19:25 +1000 Subject: [PATCH] feat: [CM-521] Process secondary fees for swap widget fee drawer (#1744) --- .../widgets-lib/src/components/Fees/Fees.tsx | 12 +- .../FeesBreakdown/FeesBreakdown.cy.tsx | 11 +- .../FeesBreakdown/FeesBreakdown.tsx | 15 +- .../FeesBreakdown/FeesBreakdownStyles.tsx | 4 +- .../checkout/widgets-lib/src/locales/en.json | 30 +-- .../checkout/widgets-lib/src/locales/ja.json | 19 +- .../checkout/widgets-lib/src/locales/ko.json | 19 +- .../checkout/widgets-lib/src/locales/zh.json | 21 +- .../bridge/components/BridgeReviewSummary.tsx | 2 +- .../widgets/bridge/functions/BridgeFees.ts | 24 +- .../widgets/swap/components/SwapForm.cy.tsx | 8 +- .../src/widgets/swap/components/SwapForm.tsx | 188 +++++++--------- .../swap/functions/processQuoteToken.ts | 37 ++++ .../functions/processSecondaryFees.test.ts | 207 ++++++++++++++++++ .../swap/functions/processSecondaryFees.ts | 29 +++ .../swap/functions/swapConversionRate.test.ts | 136 ++++++++++++ .../swap/functions/swapConversionRate.ts | 59 +++++ .../widgets/swap/functions/swapFees.test.ts | 110 ++++++++++ .../src/widgets/swap/functions/swapFees.ts | 67 ++++++ 19 files changed, 823 insertions(+), 175 deletions(-) create mode 100644 packages/checkout/widgets-lib/src/widgets/swap/functions/processQuoteToken.ts create mode 100644 packages/checkout/widgets-lib/src/widgets/swap/functions/processSecondaryFees.test.ts create mode 100644 packages/checkout/widgets-lib/src/widgets/swap/functions/processSecondaryFees.ts create mode 100644 packages/checkout/widgets-lib/src/widgets/swap/functions/swapConversionRate.test.ts create mode 100644 packages/checkout/widgets-lib/src/widgets/swap/functions/swapConversionRate.ts create mode 100644 packages/checkout/widgets-lib/src/widgets/swap/functions/swapFees.test.ts create mode 100644 packages/checkout/widgets-lib/src/widgets/swap/functions/swapFees.ts diff --git a/packages/checkout/widgets-lib/src/components/Fees/Fees.tsx b/packages/checkout/widgets-lib/src/components/Fees/Fees.tsx index f2a083989e..e90fc3fb06 100644 --- a/packages/checkout/widgets-lib/src/components/Fees/Fees.tsx +++ b/packages/checkout/widgets-lib/src/components/Fees/Fees.tsx @@ -10,17 +10,13 @@ import { useState } from 'react'; import { formatZeroAmount, tokenValueFormat } from '../../lib/utils'; import { FeesBreakdown } from '../FeesBreakdown/FeesBreakdown'; import { gasAmountAccordionStyles, gasAmountHeadingStyles } from './FeeStyles'; +import { FormattedFee } from '../../widgets/swap/functions/swapFees'; interface FeesProps { gasFeeValue: string; gasFeeToken?: TokenInfo; gasFeeFiatValue: string; - fees: { - fiatAmount: string; - amount: string; - label: string; - prefix?: string; - }[]; + fees: FormattedFee[]; onFeesClick?: () => void; loading?: boolean; sx?: any; @@ -82,8 +78,8 @@ export function Fees({ {!loading && ( )} diff --git a/packages/checkout/widgets-lib/src/components/FeesBreakdown/FeesBreakdown.cy.tsx b/packages/checkout/widgets-lib/src/components/FeesBreakdown/FeesBreakdown.cy.tsx index 40714bc501..f4da472a4e 100644 --- a/packages/checkout/widgets-lib/src/components/FeesBreakdown/FeesBreakdown.cy.tsx +++ b/packages/checkout/widgets-lib/src/components/FeesBreakdown/FeesBreakdown.cy.tsx @@ -5,6 +5,7 @@ import { ViewContextTestComponent } from 'context/view-context/test-components/V import { cySmartGet } from '../../lib/testUtils'; import { SimpleLayout } from '../SimpleLayout/SimpleLayout'; import { FeesBreakdown } from './FeesBreakdown'; +import { FormattedFee } from '../../widgets/swap/functions/swapFees'; describe('FeesBreakdown', () => { beforeEach(() => { @@ -29,7 +30,7 @@ describe('FeesBreakdown', () => { cySmartGet('fees-breakdown-content').should('be.visible'); cySmartGet('fee-item-total-fees').should('be.visible'); cySmartGet('total-fees__price').should('have.text', 'IMX 1'); - cySmartGet('total-fees__fiatAmount').should('have.text', '~ USD $0.70'); + cySmartGet('total-fees__fiatAmount').should('have.text', '≈ USD $0.70'); }); it('should not include totals if only fees are provided', () => { @@ -38,12 +39,12 @@ describe('FeesBreakdown', () => { label: 'Gas fee', fiatAmount: 'Approx USD $1234.0', amount: '0.12345', - }, + } as FormattedFee, { label: 'Maker fee', fiatAmount: 'Approx USD $5544.0', amount: '1234.444', - }, + } as FormattedFee, ]; mount( @@ -76,12 +77,12 @@ describe('FeesBreakdown', () => { label: 'Gas fee', fiatAmount: 'Approx USD $1234.0', amount: '0.12345', - }, + } as FormattedFee, { label: 'Maker fee', fiatAmount: 'Approx USD $5544.0', amount: '1234.444', - }, + } as FormattedFee, ]; mount( diff --git a/packages/checkout/widgets-lib/src/components/FeesBreakdown/FeesBreakdown.tsx b/packages/checkout/widgets-lib/src/components/FeesBreakdown/FeesBreakdown.tsx index fca8735a75..73c95bd1f7 100644 --- a/packages/checkout/widgets-lib/src/components/FeesBreakdown/FeesBreakdown.tsx +++ b/packages/checkout/widgets-lib/src/components/FeesBreakdown/FeesBreakdown.tsx @@ -6,17 +6,11 @@ import { useTranslation } from 'react-i18next'; import { feeItemContainerStyles, feeItemLoadingStyles, feesBreakdownContentStyles } from './FeesBreakdownStyles'; import { FeeItem } from './FeeItem'; import { FooterLogo } from '../Footer/FooterLogo'; - -type Fee = { - label: string; - amount: string; - fiatAmount: string; - prefix?: string; -}; +import { FormattedFee } from '../../widgets/swap/functions/swapFees'; type FeesBreakdownProps = { onCloseDrawer?: () => void; - fees: Fee[]; + fees: FormattedFee[]; children?: any; visible?: boolean; totalFiatAmount?: string; @@ -62,13 +56,14 @@ export function FeesBreakdown({ amount, fiatAmount, prefix, + token, }) => ( )) @@ -81,7 +76,7 @@ export function FeesBreakdown({ label={t('drawers.feesBreakdown.total')} amount={tokenValueFormat(totalAmount)} fiatAmount={totalFiatAmount - ? `~ ${t('drawers.feesBreakdown.fees.fiatPricePrefix')}${totalFiatAmount}` + ? `≈ ${t('drawers.feesBreakdown.fees.fiatPricePrefix')}${totalFiatAmount}` : formatZeroAmount('0')} tokenSymbol={tokenSymbol} boldLabel diff --git a/packages/checkout/widgets-lib/src/components/FeesBreakdown/FeesBreakdownStyles.tsx b/packages/checkout/widgets-lib/src/components/FeesBreakdown/FeesBreakdownStyles.tsx index dc81833cac..d8d1c0be54 100644 --- a/packages/checkout/widgets-lib/src/components/FeesBreakdown/FeesBreakdownStyles.tsx +++ b/packages/checkout/widgets-lib/src/components/FeesBreakdown/FeesBreakdownStyles.tsx @@ -15,12 +15,12 @@ export const feeItemContainerStyles = { export const feeItemStyles = { display: 'flex', width: '100%' }; export const feeItemLabelStyles = (boldLabel?: boolean) => ({ - width: '50%', + width: '65%', color: boldLabel ? 'base.color.text.body.primary' : 'base.color.text.body.secondary', }); export const feeItemPriceDisplayStyles = { - width: '50%', + width: '35%', }; export const feeItemLoadingStyles = { diff --git a/packages/checkout/widgets-lib/src/locales/en.json b/packages/checkout/widgets-lib/src/locales/en.json index fd52d4d2b0..d690e97ef4 100644 --- a/packages/checkout/widgets-lib/src/locales/en.json +++ b/packages/checkout/widgets-lib/src/locales/en.json @@ -115,7 +115,7 @@ }, "content": { "title": "What would you like to swap?", - "fiatPricePrefix": "~ USD", + "fiatPricePrefix": "≈ USD", "availableBalancePrefix": "Available" }, "swapForm": { @@ -129,7 +129,8 @@ "inputPlaceholder": "0", "selectorTitle": "What would you like to swap to?" }, - "buttonText": "Swap" + "buttonText": "Swap", + "conversionRate": "1 {{fromSymbol}} ≈ {{rate}} {{toSymbol}}, inclusive of {{fee}}% fee" }, "fees": { "title": "Fees total" @@ -454,11 +455,11 @@ }, "fees": { "title": "Gas Fee", - "fiatPricePrefix": "~ USD $" + "fiatPricePrefix": "≈ USD $" }, "content": { "title": "How much would you like to move?", - "fiatPricePrefix": "~ USD", + "fiatPricePrefix": "≈ USD", "availableBalancePrefix": "Available" }, "bridgeForm": { @@ -490,7 +491,7 @@ "submitButton": { "buttonText": "Confirm move" }, - "fiatPricePrefix": "~ USD $" + "fiatPricePrefix": "≈ USD $" }, "BRIDGE_FAILURE": { "bridgeFailureText": { @@ -548,7 +549,7 @@ "changeWallet": { "buttonText": "Change wallet" }, - "fiatPricePrefix": "~ USD $", + "fiatPricePrefix": "≈ USD $", "support": { "body1": "Need help?", "body2": " Contact ", @@ -618,14 +619,17 @@ "total": "Total fees", "fees": { "fiatPricePrefix": "USD $", - "gasFeeSwap": { - "label": "Gas fee swap" - }, "gasFeeMove": { - "label": "Gas fee move" + "label": "Gas fee" + }, + "approvalFee": { + "label": "Approval fee" + }, + "swapGasFee": { + "label": "Gas fee" }, - "gasFeeApproval": { - "label": "Gas fee approval" + "swapSecondaryFee": { + "label": "{{amount}} Swap fee (factored into quote)" }, "serviceFee": { "label": "Service fee" @@ -646,7 +650,7 @@ }, "coinSelector": { "option": { - "fiatPricePrefix": "~ USD $" + "fiatPricePrefix": "≈ USD $" }, "noCoins": "You have no available coins to select in your wallet." }, diff --git a/packages/checkout/widgets-lib/src/locales/ja.json b/packages/checkout/widgets-lib/src/locales/ja.json index 3abd653112..f88cafcc5f 100644 --- a/packages/checkout/widgets-lib/src/locales/ja.json +++ b/packages/checkout/widgets-lib/src/locales/ja.json @@ -461,7 +461,7 @@ }, "fees": { "title": "ガス手数料", - "fiatPricePrefix": "~ USD $" + "fiatPricePrefix": "≈ USD $" }, "content": { "title": "いくら移動しますか?", @@ -497,7 +497,7 @@ "submitButton": { "buttonText": "移動を確認" }, - "fiatPricePrefix": "~ USD $" + "fiatPricePrefix": "≈ USD $" }, "BRIDGE_FAILURE": { "bridgeFailureText": { @@ -624,8 +624,21 @@ "heading": "手数料の内訳", "total": "総手数料", "fees": { - "gas": { + "fiatPricePrefix": "USD $", + "gasFeeMove": { "label": "ガス手数料" + }, + "approvalFee": { + "label": "承認手数料" + }, + "swapGasFee": { + "label": "ガス手数料" + }, + "swapSecondaryFee": { + "label": "{{amount}} スワップ手数料(見積もりに含まれる)" + }, + "serviceFee": { + "label": "サービス手数料" } } }, diff --git a/packages/checkout/widgets-lib/src/locales/ko.json b/packages/checkout/widgets-lib/src/locales/ko.json index 86d3c6835d..ecd70f333f 100644 --- a/packages/checkout/widgets-lib/src/locales/ko.json +++ b/packages/checkout/widgets-lib/src/locales/ko.json @@ -454,7 +454,7 @@ }, "fees": { "title": "가스 요금", - "fiatPricePrefix": "~ USD $" + "fiatPricePrefix": "≈ USD $" }, "content": { "title": "얼마나 많이 이동하시겠습니까?", @@ -490,7 +490,7 @@ "submitButton": { "buttonText": "이동 확인" }, - "fiatPricePrefix": "~ USD $" + "fiatPricePrefix": "≈ USD $" }, "BRIDGE_FAILURE": { "bridgeFailureText": { @@ -617,8 +617,21 @@ "heading": "수수료 내역", "total": "수수료 총액", "fees": { - "gas": { + "fiatPricePrefix": "USD $", + "gasFeeMove": { "label": "가스 수수료" + }, + "approvalFee": { + "label": "승인 수수료" + }, + "swapGasFee": { + "label": "가스 수수료" + }, + "swapSecondaryFee": { + "label": "{{amount}} 스왑 수수료 (견적에 포함)" + }, + "serviceFee": { + "label": "서비스 수수료" } } }, diff --git a/packages/checkout/widgets-lib/src/locales/zh.json b/packages/checkout/widgets-lib/src/locales/zh.json index 15a8e05a5f..b16ddc9f78 100644 --- a/packages/checkout/widgets-lib/src/locales/zh.json +++ b/packages/checkout/widgets-lib/src/locales/zh.json @@ -454,7 +454,7 @@ }, "fees": { "title": "燃气费", - "fiatPricePrefix": "~ 美元 $" + "fiatPricePrefix": "≈ 美元 $" }, "content": { "title": "您想要转移多少金额?", @@ -490,7 +490,7 @@ "submitButton": { "buttonText": "确认转移" }, - "fiatPricePrefix": "~ 美元 $" + "fiatPricePrefix": "≈ 美元 $" }, "BRIDGE_FAILURE": { "bridgeFailureText": { @@ -617,8 +617,21 @@ "heading": "费用分解", "total": "费用总计", "fees": { - "gas": { - "label": "燃气费" + "fiatPricePrefix": "USD $", + "gasFeeMove": { + "label": "气体费用" + }, + "approvalFee": { + "label": "批准费" + }, + "swapGasFee": { + "label": "气体费用" + }, + "swapSecondaryFee": { + "label": "{{amount}} 交换费(已计入报价)" + }, + "serviceFee": { + "label": "服务费" } } }, diff --git a/packages/checkout/widgets-lib/src/widgets/bridge/components/BridgeReviewSummary.tsx b/packages/checkout/widgets-lib/src/widgets/bridge/components/BridgeReviewSummary.tsx index ffe7af9050..f0e24412f4 100644 --- a/packages/checkout/widgets-lib/src/widgets/bridge/components/BridgeReviewSummary.tsx +++ b/packages/checkout/widgets-lib/src/widgets/bridge/components/BridgeReviewSummary.tsx @@ -211,7 +211,7 @@ export function BridgeReviewSummary() { useInterval(() => fetchGasEstimate(), DEFAULT_QUOTE_REFRESH_INTERVAL); const formatFeeBreakdown = useCallback( - (): any => formatBridgeFees(estimates, cryptoFiatState, t), + () => formatBridgeFees(estimates, cryptoFiatState, t), [estimates], ); diff --git a/packages/checkout/widgets-lib/src/widgets/bridge/functions/BridgeFees.ts b/packages/checkout/widgets-lib/src/widgets/bridge/functions/BridgeFees.ts index 338915e2f2..9ab51f2300 100644 --- a/packages/checkout/widgets-lib/src/widgets/bridge/functions/BridgeFees.ts +++ b/packages/checkout/widgets-lib/src/widgets/bridge/functions/BridgeFees.ts @@ -1,9 +1,10 @@ import { BigNumber, utils } from 'ethers'; import { GasEstimateBridgeToL2Result } from '@imtbl/checkout-sdk'; import { calculateCryptoToFiat, tokenValueFormat } from '../../../lib/utils'; +import { FormattedFee } from '../../swap/functions/swapFees'; export const formatBridgeFees = (estimates: GasEstimateBridgeToL2Result | undefined, cryptoFiatState, t): any[] => { - const fees: any[] = []; + const fees: FormattedFee[] = []; if (!estimates?.fees || !estimates.token) return fees; let serviceFee = BigNumber.from(0); @@ -12,39 +13,42 @@ export const formatBridgeFees = (estimates: GasEstimateBridgeToL2Result | undefi if (serviceFee.gt(0)) { fees.push({ label: t('drawers.feesBreakdown.fees.serviceFee.label'), - fiatAmount: `~ ${t('drawers.feesBreakdown.fees.fiatPricePrefix')}${calculateCryptoToFiat( + fiatAmount: `≈ ${t('drawers.feesBreakdown.fees.fiatPricePrefix')}${calculateCryptoToFiat( utils.formatUnits(serviceFee, estimates.token.decimals), estimates.token.symbol, cryptoFiatState.conversions, )}`, amount: tokenValueFormat(utils.formatUnits(serviceFee, estimates.token.decimals)), - }); + token: estimates.token, + } as FormattedFee); } if (estimates.fees.sourceChainGas?.gt(0)) { const formattedGas = utils.formatUnits(estimates.fees.sourceChainGas, estimates.token.decimals); fees.push({ label: t('drawers.feesBreakdown.fees.gasFeeMove.label'), - fiatAmount: `~ ${t('drawers.feesBreakdown.fees.fiatPricePrefix')}${calculateCryptoToFiat( + fiatAmount: `≈ ${t('drawers.feesBreakdown.fees.fiatPricePrefix')}${calculateCryptoToFiat( formattedGas, estimates.token.symbol, cryptoFiatState.conversions, )}`, amount: `${tokenValueFormat(formattedGas)}`, - prefix: '~ ', - }); + prefix: '≈ ', + token: estimates.token, + } as FormattedFee); } if (estimates.fees.approvalFee?.gt(0)) { const formattedApprovalGas = utils.formatUnits(estimates.fees.approvalFee, estimates.token.decimals); fees.push({ - label: t('drawers.feesBreakdown.fees.gasFeeApproval.label'), - fiatAmount: `~ ${t('drawers.feesBreakdown.fees.fiatPricePrefix')}${calculateCryptoToFiat( + label: t('drawers.feesBreakdown.fees.approvalFee.label'), + fiatAmount: `≈ ${t('drawers.feesBreakdown.fees.fiatPricePrefix')}${calculateCryptoToFiat( formattedApprovalGas, estimates.token.symbol, cryptoFiatState.conversions, )}`, amount: `${tokenValueFormat(formattedApprovalGas)}`, - prefix: '~ ', - }); + prefix: '≈ ', + token: estimates.token, + } as FormattedFee); } return fees; diff --git a/packages/checkout/widgets-lib/src/widgets/swap/components/SwapForm.cy.tsx b/packages/checkout/widgets-lib/src/widgets/swap/components/SwapForm.cy.tsx index 75d0edd77b..415a5646c4 100644 --- a/packages/checkout/widgets-lib/src/widgets/swap/components/SwapForm.cy.tsx +++ b/packages/checkout/widgets-lib/src/widgets/swap/components/SwapForm.cy.tsx @@ -607,8 +607,8 @@ describe('SwapForm', () => { }, ]; cySmartGet('@fromAmountInStub').should('have.been.calledWith', ...params); - cySmartGet('fees-gas-fee__priceDisplay__price').should('have.text', '~ IMX 0.112300'); - cySmartGet('fees-gas-fee__priceDisplay__fiatAmount').should('have.text', '~ USD $0.08'); + cySmartGet('fees-gas-fee__priceDisplay__price').should('have.text', '≈ IMX 0.112300'); + cySmartGet('fees-gas-fee__priceDisplay__fiatAmount').should('have.text', '≈ USD $0.08'); }); it('should fetch a quote after from amount max button is clicked', () => { @@ -834,8 +834,8 @@ describe('SwapForm', () => { }, ]; cySmartGet('@fromAmountInStub').should('have.been.calledWith', ...params); - cySmartGet('fees-gas-fee__priceDisplay__price').should('have.text', '~ IMX 0.224600'); - cySmartGet('fees-gas-fee__priceDisplay__fiatAmount').should('have.text', '~ USD $0.17'); + cySmartGet('fees-gas-fee__priceDisplay__price').should('have.text', '≈ IMX 0.224600'); + cySmartGet('fees-gas-fee__priceDisplay__fiatAmount').should('have.text', '≈ USD $0.17'); }); }); diff --git a/packages/checkout/widgets-lib/src/widgets/swap/components/SwapForm.tsx b/packages/checkout/widgets-lib/src/widgets/swap/components/SwapForm.tsx index d8f7b78d6e..35b8b16bdf 100644 --- a/packages/checkout/widgets-lib/src/widgets/swap/components/SwapForm.tsx +++ b/packages/checkout/widgets-lib/src/widgets/swap/components/SwapForm.tsx @@ -3,7 +3,7 @@ import { useContext, useEffect, useMemo, useState, } from 'react'; import { - Body, Box, Heading, OptionKey, + Box, Heading, Icon, OptionKey, Tooltip, } from '@biom3/react'; import { BigNumber, utils } from 'ethers'; import { TokenInfo, WidgetTheme } from '@imtbl/checkout-sdk'; @@ -46,47 +46,17 @@ import { ConnectLoaderContext } from '../../../context/connect-loader-context/Co import useDebounce from '../../../lib/hooks/useDebounce'; import { CancellablePromise } from '../../../lib/async/cancellablePromise'; import { isPassportProvider } from '../../../lib/provider'; +import { formatSwapFees } from '../functions/swapFees'; +import { processGasFree } from '../functions/processGasFree'; +import { processSecondaryFees } from '../functions/processSecondaryFees'; +import { processQuoteToken } from '../functions/processQuoteToken'; +import { formatQuoteConversionRate } from '../functions/swapConversionRate'; enum SwapDirection { FROM = 'FROM', TO = 'TO', } -const swapValuesToText = ({ - swapFromToken, - swapToToken, - swapFromAmount, - swapToAmount, - conversion, -}: { - swapFromToken?: TokenInfo; - swapFromAmount: string; - swapToToken?: TokenInfo; - swapToAmount: string; - conversion: BigNumber; -}): { - fromToConversion: string, - swapToAmount: string, -} => { - const resp = { - fromToConversion: '', - swapToAmount: '', - }; - - if (!swapToAmount) return resp; - resp.swapToAmount = tokenValueFormat(swapToAmount); - - if (swapFromAmount && swapFromToken && swapToToken) { - const formattedConversion = formatZeroAmount(tokenValueFormat( - utils.formatUnits(conversion, swapToToken.decimals), - ), true); - - resp.fromToConversion = `1 ${swapFromToken.symbol} ≈ ${formattedConversion} ${swapToToken.symbol}`; - } - - return resp; -}; - // Ensures that the to token address does not match the from token address const shouldSetToAddress = (toAddress: string | undefined, fromAddress: string | undefined): boolean => { if (toAddress === undefined) return false; @@ -125,9 +95,6 @@ export function SwapForm({ data, theme }: SwapFromProps) { const [direction, setDirection] = useState(SwapDirection.FROM); const [loading, setLoading] = useState(false); - const [conversion, setConversion] = useState(BigNumber.from(0)); - const [swapFromToConversionText, setSwapFromToConversionText] = useState(''); - const { track } = useAnalytics(); // Form State @@ -151,6 +118,25 @@ export function SwapForm({ data, theme }: SwapFromProps) { const [gasFeeToken, setGasFeeToken] = useState(undefined); const [gasFeeFiatValue, setGasFeeFiatValue] = useState(''); const [tokensOptionsFrom, setTokensOptionsForm] = useState([]); + const formattedFees = useMemo( + () => (quote ? formatSwapFees(quote, cryptoFiatState, t) : []), + [quote, cryptoFiatState, t], + ); + const [conversionToken, setConversionToken] = useState(null); + const [conversionAmount, setConversionAmount] = useState(''); + const swapConversionRateTooltip = useMemo( + () => { + if (!quote || !conversionAmount || !conversionToken) return ''; + return formatQuoteConversionRate( + conversionAmount, + conversionToken as TokenInfo, + quote, + 'views.SWAP.swapForm.conversionRate', + t, + ); + }, + [conversionAmount, conversionToken, quote, t], + ); // Drawers const [showNotEnoughImxDrawer, setShowNotEnoughImxDrawer] = useState(false); @@ -252,7 +238,8 @@ export function SwapForm({ data, theme }: SwapFromProps) { if (quoteRequest) { quoteRequest.cancel(); } - setSwapFromToConversionText(''); + setConversionAmount(''); + setConversionToken(null); setGasFeeFiatValue(''); setQuote(null); }; @@ -264,8 +251,6 @@ export function SwapForm({ data, theme }: SwapFromProps) { if (!toToken) return; try { - if (!silently) setSwapFromToConversionText(''); - const quoteResultPromise = quotesProcessor.fromAmountIn( exchange, provider, @@ -273,24 +258,17 @@ export function SwapForm({ data, theme }: SwapFromProps) { fromAmount, toToken, ); - const conversionResultPromise = quotesProcessor.fromAmountIn( - exchange, - provider, - fromToken, - '1', - toToken, - ); const currentQuoteRequest = CancellablePromise.all([ quoteResultPromise, - conversionResultPromise, ]); quoteRequest = currentQuoteRequest; const resolved = await currentQuoteRequest; - const quoteResult = resolved[0]; - const conversionResult = resolved[1]; - setConversion(conversionResult.quote.amount.value); + + let quoteResult = processGasFree(provider, resolved[0]); + quoteResult = processSecondaryFees(fromToken, quoteResult); + quoteResult = processQuoteToken(toToken, quoteResult); const estimate = quoteResult.swap.gasFeeEstimate; let gasFeeTotal = BigNumber.from(estimate?.value || 0); @@ -302,11 +280,12 @@ export function SwapForm({ data, theme }: SwapFromProps) { DEFAULT_TOKEN_DECIMALS, ); const estimateToken = estimate?.token; - const gasToken = allowedTokens.find( (token) => token.address?.toLocaleLowerCase() === estimateToken?.address?.toLocaleLowerCase(), ); + setConversionToken(fromToken); + setConversionAmount(fromAmount); setQuote(quoteResult); setGasFeeValue(gasFee); setGasFeeToken({ @@ -337,6 +316,9 @@ export function SwapForm({ data, theme }: SwapFromProps) { resetFormErrors(); } catch (error: any) { if (!error.cancelled) { + // eslint-disable-next-line no-console + console.error('Error fetching quote.', error); + resetQuote(); setShowNotEnoughImxDrawer(false); setShowUnableToSwapDrawer(true); @@ -355,8 +337,6 @@ export function SwapForm({ data, theme }: SwapFromProps) { if (!toToken) return; try { - if (!silently) setSwapFromToConversionText(''); - const quoteResultPromise = quotesProcessor.fromAmountOut( exchange, provider, @@ -364,24 +344,15 @@ export function SwapForm({ data, theme }: SwapFromProps) { toAmount, fromToken, ); - const conversionResultPromise = quotesProcessor.fromAmountIn( - exchange, - provider, - fromToken, - '1', - toToken, - ); const currentQuoteRequest = CancellablePromise.all([ quoteResultPromise, - conversionResultPromise, ]); quoteRequest = currentQuoteRequest; const resolved = await currentQuoteRequest; - const quoteResult = resolved[0]; - const conversionResult = resolved[1]; - setConversion(conversionResult.quote.amount.value); + let quoteResult = processGasFree(provider, resolved[0]); + quoteResult = processSecondaryFees(fromToken, quoteResult); const estimate = quoteResult.swap.gasFeeEstimate; let gasFeeTotal = BigNumber.from(estimate?.value || 0); @@ -393,8 +364,10 @@ export function SwapForm({ data, theme }: SwapFromProps) { DEFAULT_TOKEN_DECIMALS, ); const estimateToken = estimate?.token; - const gasToken = allowedTokens.find((token) => token.symbol === estimateToken?.symbol); + + setConversionToken(toToken); + setConversionAmount(toAmount); setQuote(quoteResult); setGasFeeValue(gasFee); setGasFeeToken({ @@ -682,17 +655,6 @@ export function SwapForm({ data, theme }: SwapFromProps) { return isSwapFormValid; }; - useEffect(() => { - if (!quote || !conversion) return; - setSwapFromToConversionText(swapValuesToText({ - swapFromToken: fromToken, - swapFromAmount: fromAmount, - swapToToken: toToken, - swapToAmount: toAmount, - conversion, - }).fromToConversion); - }, [quote, conversion]); - return ( <> {t('views.SWAP.swapForm.to.label')} - - {swapFromToConversionText} - + {swapConversionRateTooltip?.length > 0 && ( + + + + + + {swapConversionRateTooltip} + + + )} - { - track({ - userJourney: UserJourney.SWAP, - screen: 'SwapCoins', - control: 'ViewFees', - controlType: 'Button', - }); - }} - sx={{ - paddingBottom: '0', - }} - loading={loading} - /> + {!isPassportProvider(provider) && ( + { + track({ + userJourney: UserJourney.SWAP, + screen: 'SwapCoins', + control: 'ViewFees', + controlType: 'Button', + }); + }} + sx={{ + paddingBottom: '0', + }} + loading={loading} + /> + )} { + if (!currentQuote.quote.amount && !currentQuote.quote.amountWithMaxSlippage) return currentQuote; + + const adjustedAmount = { + ...currentQuote.quote.amount, + token: { + ...currentQuote.quote.amount.token, + symbol: (toToken.address === currentQuote.quote.amount.token.address) + ? toToken.symbol : currentQuote.quote.amount.token.symbol, + }, + }; + + const adjustedAmountWithMaxSlippage = { + ...currentQuote.quote.amountWithMaxSlippage, + token: { + ...currentQuote.quote.amountWithMaxSlippage.token, + symbol: (toToken.address === currentQuote.quote.amountWithMaxSlippage.token.address) + ? toToken.symbol : currentQuote.quote.amountWithMaxSlippage.token.symbol, + }, + }; + return { + ...currentQuote, + quote: { + ...currentQuote.quote, + amount: adjustedAmount, + amountWithMaxSlippage: adjustedAmountWithMaxSlippage, + }, + }; +}; diff --git a/packages/checkout/widgets-lib/src/widgets/swap/functions/processSecondaryFees.test.ts b/packages/checkout/widgets-lib/src/widgets/swap/functions/processSecondaryFees.test.ts new file mode 100644 index 0000000000..e1af30de29 --- /dev/null +++ b/packages/checkout/widgets-lib/src/widgets/swap/functions/processSecondaryFees.test.ts @@ -0,0 +1,207 @@ +import { + Amount, + Fee, + Quote, + TransactionDetails, + TransactionResponse, +} from '@imtbl/dex-sdk'; +import { BigNumber } from 'ethers'; +import { TokenInfo } from '@imtbl/checkout-sdk'; +import { describe } from '@jest/globals'; +import { processSecondaryFees } from './processSecondaryFees'; + +describe('processSecondaryFees', () => { + const mockQuote = { + quote: { + amount: {} as Amount, + amountWithMaxSlippage: {} as Amount, + slippage: 0, + fees: [{ + recipient: '0x123', + basisPoints: 100, + amount: { + value: BigNumber.from(100), + token: { + symbol: 'ETH', + address: '0x123', + chainId: 1, + decimals: 18, + }, + }, + } as Fee], + } as Quote, + approval: null, + swap: {} as TransactionDetails, + } as TransactionResponse; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should return the original quote if no fees are present', () => { + const fromToken = { symbol: 'ETH', address: '0x123' } as TokenInfo; + const quote = { + quote: {}, + } as TransactionResponse; + expect(processSecondaryFees(fromToken, quote)).toEqual(quote); + }); + + it('should not modify fees with correct symbols', () => { + const fromToken = { symbol: 'ETH', address: '0x123' } as TokenInfo; + const quote = mockQuote; + + expect(processSecondaryFees(fromToken, quote)).toEqual(quote); + }); + + it('should add symbol to token without symbol if address matches', () => { + const fromToken = { symbol: 'ETH', address: '0x123' } as TokenInfo; + const mockQuoteWithNoSymbol = { + quote: { + amount: {} as Amount, + amountWithMaxSlippage: {} as Amount, + slippage: 0, + fees: [{ + recipient: '0x123', + basisPoints: 100, + amount: { + value: BigNumber.from(100), + token: { + address: '0x123', + chainId: 1, + decimals: 18, + }, + }, + } as Fee], + } as Quote, + approval: null, + swap: {} as TransactionDetails, + } as TransactionResponse; + + expect(processSecondaryFees(fromToken, mockQuoteWithNoSymbol)).toEqual(mockQuote); + }); + + it('should not add a symbol if token address does not match fromToken address', () => { + const fromToken = { symbol: 'ETH', address: '0x123' } as TokenInfo; + const mockQuoteWithDifferentAddress = { + quote: { + amount: {} as Amount, + amountWithMaxSlippage: {} as Amount, + slippage: 0, + fees: [{ + recipient: '0x123', + basisPoints: 100, + amount: { + value: BigNumber.from(100), + token: { + address: '0x000', + chainId: 1, + decimals: 18, + }, + }, + } as Fee], + } as Quote, + approval: null, + swap: {} as TransactionDetails, + } as TransactionResponse; + + const mockQuoteWithBlankSymbol = { + quote: { + amount: {} as Amount, + amountWithMaxSlippage: {} as Amount, + slippage: 0, + fees: [{ + recipient: '0x123', + basisPoints: 100, + amount: { + value: BigNumber.from(100), + token: { + address: '0x000', + chainId: 1, + decimals: 18, + symbol: undefined, + }, + }, + } as Fee], + } as Quote, + approval: null, + swap: {} as TransactionDetails, + } as TransactionResponse; + + expect(processSecondaryFees(fromToken, mockQuoteWithDifferentAddress)).toEqual(mockQuoteWithBlankSymbol); + }); + + it('should handle multiple fees correctly', () => { + const fromToken = { symbol: 'ETH', address: '0x123' } as TokenInfo; + const mockQuoteWithMultipleFees = { + quote: { + amount: {} as Amount, + amountWithMaxSlippage: {} as Amount, + slippage: 0, + fees: [{ + recipient: '0x456', + basisPoints: 100, + amount: { + value: BigNumber.from(100), + token: { + address: '0x000', + chainId: 1, + decimals: 18, + }, + }, + } as Fee, + { + recipient: '0x456', + basisPoints: 100, + amount: { + value: BigNumber.from(100), + token: { + address: '0x123', + chainId: 1, + decimals: 18, + }, + }, + } as Fee], + } as Quote, + approval: null, + swap: {} as TransactionDetails, + } as TransactionResponse; + + const mockQuoteWithMultipleFeesResolved = { + quote: { + amount: {} as Amount, + amountWithMaxSlippage: {} as Amount, + slippage: 0, + fees: [{ + recipient: '0x456', + basisPoints: 100, + amount: { + value: BigNumber.from(100), + token: { + symbol: undefined, + address: '0x000', + chainId: 1, + decimals: 18, + }, + }, + } as Fee, + { + recipient: '0x456', + basisPoints: 100, + amount: { + value: BigNumber.from(100), + token: { + symbol: 'ETH', + address: '0x123', + chainId: 1, + decimals: 18, + }, + }, + } as Fee], + } as Quote, + approval: null, + swap: {} as TransactionDetails, + } as TransactionResponse; + + expect(processSecondaryFees(fromToken, mockQuoteWithMultipleFees)).toEqual(mockQuoteWithMultipleFeesResolved); + }); +}); diff --git a/packages/checkout/widgets-lib/src/widgets/swap/functions/processSecondaryFees.ts b/packages/checkout/widgets-lib/src/widgets/swap/functions/processSecondaryFees.ts new file mode 100644 index 0000000000..547d85d9c4 --- /dev/null +++ b/packages/checkout/widgets-lib/src/widgets/swap/functions/processSecondaryFees.ts @@ -0,0 +1,29 @@ +import { TokenInfo } from '@imtbl/checkout-sdk'; +import { TransactionResponse } from '@imtbl/dex-sdk'; + +/** + * Ensures that the fees token has the correct symbol. At the moment the dex quote doesn't return it. + * Assumes the fee token is the from token. If it's not, it will be incorrect. + * TODO: Fix this when the canonical tokens list comes into play so we can look up the symbol based on address + * @param fromToken Assumption is fees are delineated in this from token + * @param currentQuote + */ +export const processSecondaryFees = (fromToken: TokenInfo, currentQuote: TransactionResponse) => { + if (!currentQuote.quote.fees) return currentQuote; + + const adjustedFees = currentQuote.quote.fees.map((fee) => { + if (fee.amount.token.symbol) return fee; + + return { + ...fee, + amount: { + ...fee.amount, + token: { + ...fee.amount.token, + symbol: (fromToken.address === fee.amount.token.address) ? fromToken.symbol : fee.amount.token.symbol, + }, + }, + }; + }); + return { ...currentQuote, quote: { ...currentQuote.quote, fees: adjustedFees } }; +}; diff --git a/packages/checkout/widgets-lib/src/widgets/swap/functions/swapConversionRate.test.ts b/packages/checkout/widgets-lib/src/widgets/swap/functions/swapConversionRate.test.ts new file mode 100644 index 0000000000..f21297f926 --- /dev/null +++ b/packages/checkout/widgets-lib/src/widgets/swap/functions/swapConversionRate.test.ts @@ -0,0 +1,136 @@ +import { BigNumber } from 'ethers'; +import { + Amount, + Fee, + Quote, + TransactionResponse, +} from '@imtbl/dex-sdk'; +import { TokenInfo } from '@imtbl/checkout-sdk'; +import { TFunction } from 'i18next'; +import { formatQuoteConversionRate } from './swapConversionRate'; + +describe('formatQuoteConversionRate', () => { + const mockTranslate = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should calculate the correct conversion rate', () => { + const fromAmount = '1'; + const fromToken = { + name: 'ETH', + symbol: 'ETH', + address: '0x123', + chainId: 1, + decimals: 18, + } as TokenInfo; + const mockQuote = { + quote: { + amount: { + value: BigNumber.from('2000000000000000000'), + token: { + symbol: 'DAI', + address: '0x456', + chainId: 1, + decimals: 18, + }, + } as Amount, + amountWithMaxSlippage: {} as Amount, + slippage: 0, + fees: [{ + recipient: '0x000', + basisPoints: 100, + amount: { + value: BigNumber.from('100000000000000000'), + token: { + symbol: 'ETH', + address: '0x123', + chainId: 1, + decimals: 18, + }, + }, + } as Fee], + } as Quote, + swap: { + gasFeeEstimate: { + value: BigNumber.from(100), + }, + }, + approval: { + gasFeeEstimate: { + value: BigNumber.from(50), + }, + }, + } as TransactionResponse; + const labelKey = 'conversion.label'; + + formatQuoteConversionRate(fromAmount, fromToken, mockQuote, labelKey, mockTranslate as unknown as TFunction); + + expect(mockTranslate).toHaveBeenCalledWith(labelKey, { + fromSymbol: 'ETH', + toSymbol: 'DAI', + rate: '2', + fee: 1, + }); + }); + + it('should handle fromAmount with decimals', () => { + const fromAmount = '1.50'; + const fromToken = { + name: 'ETH', + symbol: 'ETH', + address: '0x123', + chainId: 1, + decimals: 18, + } as TokenInfo; + const mockQuote = { + quote: { + amount: { + value: BigNumber.from('2000000000000000000'), + token: { + symbol: 'DAI', + address: '0x456', + chainId: 1, + decimals: 18, + }, + } as Amount, + amountWithMaxSlippage: {} as Amount, + slippage: 0, + fees: [{ + recipient: '0x000', + basisPoints: 100, + amount: { + value: BigNumber.from('150000000000000000'), + token: { + symbol: 'ETH', + address: '0x123', + chainId: 1, + decimals: 18, + }, + }, + } as Fee], + } as Quote, + swap: { + gasFeeEstimate: { + value: BigNumber.from(100), + }, + }, + approval: { + gasFeeEstimate: { + value: BigNumber.from(50), + }, + }, + } as TransactionResponse; + const labelKey = 'conversion.label'; + + formatQuoteConversionRate(fromAmount, fromToken, mockQuote, labelKey, mockTranslate as unknown as TFunction); + + expect(mockTranslate).toHaveBeenCalledWith(labelKey, { + fromSymbol: 'ETH', + toSymbol: 'DAI', + rate: '1.33', + fee: 1, + }); + }); +}); diff --git a/packages/checkout/widgets-lib/src/widgets/swap/functions/swapConversionRate.ts b/packages/checkout/widgets-lib/src/widgets/swap/functions/swapConversionRate.ts new file mode 100644 index 0000000000..2e1df2a2af --- /dev/null +++ b/packages/checkout/widgets-lib/src/widgets/swap/functions/swapConversionRate.ts @@ -0,0 +1,59 @@ +import { TransactionResponse } from '@imtbl/dex-sdk'; +import { BigNumber, utils } from 'ethers'; +import { TFunction } from 'i18next'; +import { TokenInfo } from '@imtbl/checkout-sdk'; +import { formatZeroAmount, tokenValueFormat } from '../../../lib/utils'; + +export const formatQuoteConversionRate = ( + amount: string, + token: TokenInfo, + quote: TransactionResponse, + labelKey: string, + t: TFunction, +) => { + // Grab the token from the quote secondary fees + // NOTE: This has a dependency on the secondary fee and needs to change if we change that fee + const secondaryFee = quote.quote.fees[0]; + const fromToken = token; + const toToken = quote.quote.amount.token; + + // Parse the fromAmount input, multiply by 10^decimals to convert to integer units + const parsedFromAmount = parseFloat(amount); + const expandedFromAmount = parsedFromAmount * (10 ** fromToken.decimals); + const relativeFromAmount = BigNumber.from(expandedFromAmount.toFixed(0)); + const relativeToAmount = BigNumber.from(quote.quote.amount.value); + + // Determine the maximum decimal places to equalize to + const fromDecimals = fromToken.decimals; + const toDecimals = quote.quote.amount.token.decimals; + const maxDecimals = Math.max(fromDecimals, toDecimals); + + // Calculate scale factors based on maximum decimals + const fromScaleFactor = BigNumber.from('10').pow(maxDecimals - fromDecimals); + const toScaleFactor = BigNumber.from('10').pow(maxDecimals - toDecimals); + + // Adjust amounts to the same decimal scale + const adjustedFromAmount = relativeFromAmount.mul(fromScaleFactor); + const adjustedToAmount = relativeToAmount.mul(toScaleFactor); + + // Calculate conversion rate + const initialRate = adjustedToAmount.div(adjustedFromAmount); + + // Calculate the remainder and adjust it correctly + const conversionRemainder = adjustedToAmount.mod(adjustedFromAmount); + const remainderAdjustmentFactor = BigNumber.from('10').pow(maxDecimals); + const adjustedRemainder = conversionRemainder.mul(remainderAdjustmentFactor).div(adjustedFromAmount); + + // Compose the total conversion rate by adding the adjusted remainder + const accurateRate = initialRate.mul(remainderAdjustmentFactor).add(adjustedRemainder); + const formattedConversion = formatZeroAmount(tokenValueFormat( + utils.formatUnits(accurateRate, maxDecimals), + ), true); + + return t(labelKey, { + fromSymbol: fromToken.symbol, + toSymbol: toToken.symbol, + rate: formattedConversion, + fee: secondaryFee.basisPoints / 100, + }); +}; diff --git a/packages/checkout/widgets-lib/src/widgets/swap/functions/swapFees.test.ts b/packages/checkout/widgets-lib/src/widgets/swap/functions/swapFees.test.ts new file mode 100644 index 0000000000..0280c0e41c --- /dev/null +++ b/packages/checkout/widgets-lib/src/widgets/swap/functions/swapFees.test.ts @@ -0,0 +1,110 @@ +import { BigNumber } from 'ethers'; +import { + Amount, + Fee, + Quote, + TransactionResponse, +} from '@imtbl/dex-sdk'; +import { formatSwapFees } from './swapFees'; +import { CryptoFiatState } from '../../../context/crypto-fiat-context/CryptoFiatContext'; +import { calculateCryptoToFiat, tokenValueFormat } from '../../../lib/utils'; + +jest.mock('../../../lib/utils'); + +describe('formatSwapFees', () => { + const mockTranslate = ((labelKey) => labelKey) as any; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should format swap gas fee correctly', () => { + const mockGasFeeQuote = { + quote: { + amount: {} as Amount, + amountWithMaxSlippage: {} as Amount, + slippage: 0, + fees: [], + } as Quote, + swap: { + gasFeeEstimate: { + value: BigNumber.from(100), + token: { + decimals: 18, + symbol: 'ETH', + }, + }, + }, + approval: null, + } as TransactionResponse; + const cryptoFiatState = { + conversions: {}, + } as CryptoFiatState; + (calculateCryptoToFiat as jest.Mock).mockReturnValue('FiatValue:1'); + (tokenValueFormat as jest.Mock).mockReturnValue('Formatted:1'); + + const fees = formatSwapFees(mockGasFeeQuote, cryptoFiatState, mockTranslate); + expect(fees).toEqual([ + { + label: 'drawers.feesBreakdown.fees.swapGasFee.label', + fiatAmount: '≈ drawers.feesBreakdown.fees.fiatPricePrefixFiatValue:1', + amount: 'Formatted:1', + prefix: '≈ ', + token: { + decimals: 18, + symbol: 'ETH', + }, + }, + ]); + }); + + it('should format a secondary fee correctly', () => { + const mockSecondaryFeeQuote = { + quote: { + amount: {} as Amount, + amountWithMaxSlippage: {} as Amount, + slippage: 0, + fees: [{ + recipient: '0x123', + basisPoints: 100, + amount: { + value: BigNumber.from(100), + token: { + symbol: 'ETH', + address: '0x123', + chainId: 1, + decimals: 18, + }, + }, + } as Fee], + } as Quote, + swap: { + gasFeeEstimate: { + value: BigNumber.from(0), + }, + }, + approval: null, + } as TransactionResponse; + (calculateCryptoToFiat as jest.Mock).mockReturnValue('FiatValue:0.5'); + (tokenValueFormat as jest.Mock).mockReturnValue('Formatted:0.5'); + const cryptoFiatState = { + conversions: {}, + } as CryptoFiatState; + + const fees = formatSwapFees(mockSecondaryFeeQuote, cryptoFiatState, mockTranslate); + expect(fees).toEqual([ + { + label: 'drawers.feesBreakdown.fees.swapSecondaryFee.label', + fiatAmount: '≈ drawers.feesBreakdown.fees.fiatPricePrefixFiatValue:0.5', + amount: 'Formatted:0.5', + prefix: '', + token: { + decimals: 18, + symbol: 'ETH', + address: '0x123', + chainId: 1, + }, + }, + ]); + }); +}); diff --git a/packages/checkout/widgets-lib/src/widgets/swap/functions/swapFees.ts b/packages/checkout/widgets-lib/src/widgets/swap/functions/swapFees.ts new file mode 100644 index 0000000000..00a2f29ef1 --- /dev/null +++ b/packages/checkout/widgets-lib/src/widgets/swap/functions/swapFees.ts @@ -0,0 +1,67 @@ +import { Amount, Token, TransactionResponse } from '@imtbl/dex-sdk'; +import { TFunction } from 'i18next'; +import { BigNumber, utils } from 'ethers'; +import { CryptoFiatState } from '../../../context/crypto-fiat-context/CryptoFiatContext'; +import { calculateCryptoToFiat, tokenValueFormat } from '../../../lib/utils'; + +export type FormattedFee = { + label: string; + fiatAmount: string; + amount: string; + prefix?: string; + token: Token; +}; + +/** + * Formats a quote into a list of fees for the fee drawer + * @param swapQuote + * @param cryptoFiatState + * @param t + */ +export const formatSwapFees = ( + swapQuote: TransactionResponse, + cryptoFiatState: CryptoFiatState, + t: TFunction, +): FormattedFee[] => { + const fees: FormattedFee[] = []; + if (!swapQuote.swap) return fees; + + const addFee = (estimate: Amount | undefined, label: string, prefix: string = '≈ ') => { + const value = BigNumber.from(estimate?.value ?? 0); + if (estimate && value.gt(0)) { + const formattedFee = utils.formatUnits(value, estimate.token.decimals); + fees.push({ + label, + fiatAmount: `≈ ${t('drawers.feesBreakdown.fees.fiatPricePrefix')}${calculateCryptoToFiat( + formattedFee, + estimate.token.symbol || '', + cryptoFiatState.conversions, + )}`, + amount: `${tokenValueFormat(formattedFee)}`, + prefix, + token: estimate.token, + }); + } + }; + + // Format gas fee + if (swapQuote.swap && swapQuote.swap.gasFeeEstimate) { + addFee(swapQuote.swap.gasFeeEstimate, t('drawers.feesBreakdown.fees.swapGasFee.label')); + } + + // Format gas fee approval + if (swapQuote.approval && swapQuote.approval.gasFeeEstimate) { + addFee(swapQuote.approval.gasFeeEstimate, t('drawers.feesBreakdown.fees.approvalFee.label')); + } + + // Format the secondary fees + swapQuote.quote?.fees?.forEach((fee) => { + addFee( + fee.amount, + t('drawers.feesBreakdown.fees.swapSecondaryFee.label', { amount: `${(fee.basisPoints / 100)}%` }), + '', + ); + }); + + return fees; +};