From a02ee59ebee0c6a897bbb1b6d77803763299d497 Mon Sep 17 00:00:00 2001 From: Albina Nikiforova Date: Wed, 22 Jan 2025 17:30:06 +0100 Subject: [PATCH] feat(suite): wip frontend --- packages/connect/src/api/bitcoin/Fees.ts | 7 +- .../ClaimModal/ClaimModal.tsx | 2 + .../StakeModal/StakeEthForm/StakeEthForm.tsx | 2 + .../TxDetailModal/ChangeFee/RbfFees.tsx | 2 + .../UnstakeModal/UnstakeEthForm/Fees.tsx | 2 + .../src/components/wallet/Fees/CustomFee.tsx | 83 ++++-- .../src/components/wallet/Fees/FeeDetails.tsx | 248 +++++++++++++--- .../suite/src/components/wallet/Fees/Fees.tsx | 279 ++++++++++-------- .../suite/src/hooks/wallet/form/useFees.ts | 3 + packages/suite/src/support/messages.ts | 8 + .../CoinmarketForm/CoinmarketFormInputs.tsx | 65 ++-- .../suite/src/views/wallet/send/SendFees.tsx | 2 + .../src/send/sendFormEthereumThunks.ts | 5 +- 13 files changed, 491 insertions(+), 217 deletions(-) diff --git a/packages/connect/src/api/bitcoin/Fees.ts b/packages/connect/src/api/bitcoin/Fees.ts index d427170a539..76bac5b2aa1 100644 --- a/packages/connect/src/api/bitcoin/Fees.ts +++ b/packages/connect/src/api/bitcoin/Fees.ts @@ -86,7 +86,7 @@ export class FeeLevels { if (response.eip1559) { const eip1559LevelKeys = ['low', 'medium', 'high'] as const; - const { eip1559, ...baseLevel } = response; + const { eip1559 } = response; const eip1559Levels = eip1559LevelKeys.map(levelKey => { const level = eip1559[levelKey]; @@ -104,10 +104,7 @@ export class FeeLevels { }; }); - this.levels = [ - { ...baseLevel, label: 'normal' as const, blocks: -1, ethFeeType: 'legacy' }, - ...eip1559Levels, - ]; + this.levels = [...eip1559Levels]; } else { this.levels[0] = { ...this.levels[0], diff --git a/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/ClaimModal/ClaimModal.tsx b/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/ClaimModal/ClaimModal.tsx index 4641d1c2608..30455da2e0f 100644 --- a/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/ClaimModal/ClaimModal.tsx +++ b/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/ClaimModal/ClaimModal.tsx @@ -26,6 +26,7 @@ export const ClaimModal = ({ onCancel }: ClaimModalModalProps) => { const { account, + network, formState: { errors, isSubmitting }, register, control, @@ -113,6 +114,7 @@ export const ClaimModal = ({ onCancel }: ClaimModalModalProps) => { { const { account, + network, isConfirmModalOpen, closeConfirmModal, signTx, @@ -45,6 +46,7 @@ export const StakeEthForm = () => { { account, feeInfo, composedLevels, + network, } = useRbfContext(); return ( { changeFeeLevel, feeInfo, composedLevels, + network, } = useUnstakeEthFormContext(); return ( { networkType: NetworkType; feeInfo: FeeInfo; @@ -36,8 +39,8 @@ interface CustomFeeProps { control: Control; setValue: UseFormSetValue; getValues: UseFormGetValues; - changeFeeLimit?: (value: string) => void; composedFeePerByte: string; + shouldUsePriorityFees: boolean; } export const CustomFee = ({ @@ -45,8 +48,8 @@ export const CustomFee = ({ feeInfo, register, control, - changeFeeLimit, composedFeePerByte, + shouldUsePriorityFees = false, ...props }: CustomFeeProps) => { const { translationString } = useTranslation(); @@ -54,6 +57,8 @@ export const CustomFee = ({ const locale = useSelector(selectLanguage); + console.log('feeInfo', feeInfo); + // Type assertion allowing to make the component reusable, see https://stackoverflow.com/a/73624072. const { getValues, setValue } = props as unknown as UseFormReturn; const errors = props.errors as unknown as FieldErrors; @@ -64,6 +69,11 @@ export const CustomFee = ({ const feeUnits = getFeeUnits(networkType); const estimatedFeeLimit = getValues('estimatedFeeLimit'); + // const maxFeePerGas = shouldUsePriorityFees ? getValues(MAX_FEE_PER_GAS) : undefined; //only for priority fees on evms + // const maxPriorityFeePerGas = shouldUsePriorityFees + // ? getValues(MAX_PRIORITY_FEE_PER_GAS) + // : undefined; + const feePerUnitError = errors.feePerUnit; const feeLimitError = errors.feeLimit; @@ -169,7 +179,6 @@ export const CustomFee = ({ inputState={getInputState(feeLimitError)} name={FEE_LIMIT} data-testid={FEE_LIMIT} - onChange={changeFeeLimit} bottomText={ feeLimitError?.message ? ( ({ ) : ( )} /> )} - : undefined} - locale={locale} - control={control} - inputState={getInputState(feePerUnitError)} - innerAddon={ - - {feeUnits} - - } - name={FEE_PER_UNIT} - data-testid={FEE_PER_UNIT} - rules={feeRules} - bottomText={feePerUnitError?.message || null} - /> + + {shouldUsePriorityFees && feeInfo.levels[0].ethFeeType === 'eip1559' ? ( + <> + } + locale={locale} + control={control} + inputState={getInputState(feePerUnitError)} + innerAddon={ + + {feeUnits} + + } + name={MAX_PRIORITY_FEE_PER_GAS} + data-testid={MAX_PRIORITY_FEE_PER_GAS} + rules={feeRules} + bottomText={feePerUnitError?.message || null} + /> + } + locale={locale} + control={control} + inputState={getInputState(feePerUnitError)} + innerAddon={ + + {feeUnits} + + } + name={MAX_FEE_PER_GAS} + data-testid={MAX_FEE_PER_GAS} + rules={feeRules} + bottomText={feePerUnitError?.message || null} + /> + + ) : ( + : undefined} + locale={locale} + control={control} + inputState={getInputState(feePerUnitError)} + innerAddon={ + + {feeUnits} + + } + name={FEE_PER_UNIT} + data-testid={FEE_PER_UNIT} + rules={feeRules} + bottomText={feePerUnitError?.message || null} + /> + )} {feeDifferenceWarning && {feeDifferenceWarning}} diff --git a/packages/suite/src/components/wallet/Fees/FeeDetails.tsx b/packages/suite/src/components/wallet/Fees/FeeDetails.tsx index 0482a3645f4..96eb1b73da1 100644 --- a/packages/suite/src/components/wallet/Fees/FeeDetails.tsx +++ b/packages/suite/src/components/wallet/Fees/FeeDetails.tsx @@ -1,11 +1,13 @@ -import React, { useState, useEffect } from 'react'; +import React, { useEffect, useState } from 'react'; + +import styled, { css, useTheme } from 'styled-components'; import { spacings } from '@trezor/theme'; -import { Row, Text } from '@trezor/components'; +import { Card, Column, Grid, Row, Text, useMediaQuery, variables } from '@trezor/components'; import { FeeLevel } from '@trezor/connect'; -import { getFeeUnits } from '@suite-common/wallet-utils'; +import { formatNetworkAmount, getFeeUnits } from '@suite-common/wallet-utils'; import { formatDuration } from '@suite-common/suite-utils'; -import { NetworkType } from '@suite-common/wallet-config'; +import { NetworkType, NetworkSymbol } from '@suite-common/wallet-config'; import { PrecomposedTransaction, PrecomposedTransactionCardano, @@ -13,14 +15,20 @@ import { } from '@suite-common/wallet-types'; import { Translation } from 'src/components/suite/Translation'; +import { FiatValue } from 'src/components/suite/FiatValue'; + +import { FeeOption } from './Fees'; type DetailsProps = { networkType: NetworkType; + symbol: NetworkSymbol; selectedLevel: FeeLevel; // fields below are validated as false-positives, eslint claims that they are not used... - + feeOptions: FeeOption[]; feeInfo: FeeInfo; - + selectedLevelOption: string; + setSelectedLevelOption: (option: string) => void; + changeFeeLevel: (level: FeeLevel['label']) => void; transactionInfo?: PrecomposedTransaction | PrecomposedTransactionCardano; showFee: boolean; @@ -38,28 +46,134 @@ const Item = ({ label, children }: ItemProps) => ( ); +type FeeCardProps = { + value: FeeLevel['label']; + setSelectedLevelOption: (option: string) => void; + selected: boolean; + children: React.ReactNode; + changeFeeLevel: (level: FeeLevel['label']) => void; +}; + +const WrapperWithCheckmark = styled.div<{ selected: boolean }>` + position: relative; // Ensure the wrapper has a positioning context + border-radius: 16px; // Optional: Border radius for the wrapper + padding: 3px; + + // Add the checkmark icon using ::after pseudo-element + ${({ selected }) => + selected && + css` + &::after { + content: ''; // Unicode for a checkmark + position: absolute; + top: 1px; // Adjust to position outside the border + right: 1px; // Adjust to position outside the border + background-color: ${({ theme }) => + theme.borderSecondary}; // Green background for the checkmark + color: white; // White color for the checkmark + width: 16px; // Width of the checkmark circle + height: 16px; // Height of the checkmark circle + border-radius: 50%; // Make it circular + display: flex; // Center the checkmark + align-items: center; + justify-content: center; + font-size: 14px; // Adjust the size of the checkmark + font-weight: bold; // Make the checkmark bold + } + `} +`; + +const FeeCard = ({ + value, + setSelectedLevelOption, + selected, + children, + changeFeeLevel, +}: FeeCardProps) => { + const theme = useTheme(); + + return ( + + { + setSelectedLevelOption(value); + changeFeeLevel(value); + }} + minWidth={160} + width="100%" + paddingType="small" + flex="1" + border={selected ? `2px solid ${theme.borderSecondary} !important` : 'none'} + fillType="none" + > + {children} + + + ); +}; + const BitcoinDetails = ({ networkType, feeInfo, selectedLevel, transactionInfo, + feeOptions, showFee, + setSelectedLevelOption, + selectedLevelOption, + changeFeeLevel, + symbol, }: DetailsProps) => { const hasInfo = transactionInfo && transactionInfo.type !== 'error'; + const isBelowTablet = useMediaQuery(`(max-width: ${variables.SCREEN_SIZE.MD})`); + console.log('selectedLevel', selectedLevel); return ( showFee && ( - <> - }> - {formatDuration(feeInfo.blockTime * selectedLevel.blocks * 60)} - - - }> - {hasInfo ? transactionInfo.feePerByte : selectedLevel.feePerUnit}{' '} - {getFeeUnits(networkType)} - {hasInfo ? ` (${transactionInfo.bytes} B)` : ''} - - + + + {feeOptions && + feeOptions.map((fee, index) => ( + + + + + {fee.label} + + + ~ + {formatDuration( + feeInfo.blockTime * (fee?.blocks || 0) * 60, + )} + + + + + + + + {fee?.feePerUnit} {getFeeUnits(networkType)} + {hasInfo ? ` (${transactionInfo.bytes} B)` : ''} + + + + + ))} + + ) ); }; @@ -69,6 +183,10 @@ const EthereumDetails = ({ selectedLevel, transactionInfo, showFee, + feeOptions, + setSelectedLevelOption, + selectedLevelOption, + changeFeeLevel, }: DetailsProps) => { // States to remember the last known values of feeLimit and feePerByte when isComposedTx was true. const [lastKnownFeeLimit, setLastKnownFeeLimit] = useState(''); @@ -81,40 +199,102 @@ const EthereumDetails = ({ setLastKnownFeeLimit(transactionInfo.feeLimit); setLastKnownFeePerByte(transactionInfo.feePerByte); } - }, [isComposedTx, transactionInfo]); + if (!feeOptions[0].eip1559) { + setSelectedLevelOption(feeOptions[0].value); + } + }, [isComposedTx, transactionInfo, feeOptions, setSelectedLevelOption]); + + console.log('transactionInfo', transactionInfo, lastKnownFeeLimit, lastKnownFeePerByte); const gasLimit = isComposedTx - ? transactionInfo.feeLimit - : lastKnownFeeLimit || selectedLevel.feeLimit; + ? transactionInfo.feeLimit || 0 + : lastKnownFeeLimit || selectedLevel?.feeLimit || 0; const gasPrice = isComposedTx - ? transactionInfo.feePerByte - : lastKnownFeePerByte || selectedLevel.feePerUnit; + ? transactionInfo.feePerByte || 0 + : lastKnownFeePerByte || selectedLevel?.feePerUnit || 0; + + const isBelowTablet = useMediaQuery(`(max-width: ${variables.SCREEN_SIZE.MD})`); + + console.log(feeOptions, 'feeOptions'); return ( showFee && ( - <> - }>{gasLimit} - - }> - {gasPrice} {getFeeUnits(networkType)} - - + + + {feeOptions && + feeOptions.map((fee, index) => ( + + {fee.eip1559 ? ( + + + {fee.label} + + {fee.waitTimeEstimate} + + + + {fee.gasFeeFiat} + + {fee.gasFeeCrypto} + + + + ) : ( + {fee.label} + )} + + ))} + + + + }> + {gasPrice} {getFeeUnits(networkType)} + + + + }>{gasLimit} + + + ) ); }; -const RippleDetails = ({ networkType, selectedLevel, showFee }: DetailsProps) => - showFee && {getFeeUnits(networkType)}; +// Solana, Ripple, Cardano and other networks with only one option +const MiscDetails = ({ networkType, showFee, feeOptions }: DetailsProps) => + showFee && ( + {}} + selected={true} + changeFeeLevel={() => {}} + > + + {feeOptions[0].label} + + {feeOptions[0].feePerUnit} {getFeeUnits(networkType)} + + + + ); export const FeeDetails = (props: DetailsProps) => { const { networkType } = props; return ( - - + + {networkType === 'bitcoin' && } {networkType === 'ethereum' && } - {networkType === 'ripple' && } + {networkType !== 'bitcoin' && networkType !== 'ethereum' && ( + + )} ); diff --git a/packages/suite/src/components/wallet/Fees/Fees.tsx b/packages/suite/src/components/wallet/Fees/Fees.tsx index bee3531ee0d..eb51e193043 100644 --- a/packages/suite/src/components/wallet/Fees/Fees.tsx +++ b/packages/suite/src/components/wallet/Fees/Fees.tsx @@ -6,20 +6,21 @@ import { UseFormReturn, UseFormSetValue, } from 'react-hook-form'; +import { useState } from 'react'; import { AnimatePresence, motion } from 'framer-motion'; import { FeeLevel } from '@trezor/connect'; +import { Network, NetworkType } from '@suite-common/wallet-config'; import { Banner, SelectBar, Tooltip, Column, Note, - motionEasing, InfoItem, Row, - Text, + motionEasing, } from '@trezor/components'; import { FormState, @@ -29,10 +30,9 @@ import { PrecomposedTransactionFinal, } from '@suite-common/wallet-types'; import { spacings } from '@trezor/theme'; -import { formatNetworkAmount } from '@suite-common/wallet-utils'; import { TranslationKey } from '@suite-common/intl-types'; -import { FiatValue, FormattedCryptoAmount, Translation } from 'src/components/suite'; +import { Translation } from 'src/components/suite'; import { Account } from 'src/types/wallet'; import { CustomFee } from './CustomFee'; @@ -47,13 +47,56 @@ const FEE_LEVELS_TRANSLATIONS: Record = { medium: 'FEE_LEVEL_MEDIUM', } as const; -const buildFeeOptions = (levels: FeeLevel[]) => - levels - .filter(l => l.ethFeeType !== 'eip1559') //TODO: remove this when frontend for eip1559 is implemented - .map(({ label }) => ({ +const buildFeeOptions = (levels: FeeLevel[], networkType: NetworkType) => { + const filteredLevels = levels.filter(level => level.label !== 'custom'); + if (networkType === 'ethereum') { + if (filteredLevels[0].baseFeePerGas) { + return filteredLevels.map(level => ({ + label: , + value: level.label, + waitTimeEstimate: level.maxWaitTimeEstimate, + gasFeeFiat: level.feePerUnit, + gasFeeCrypto: level.feePerUnit, + eip1559: true, + })); + } + + return filteredLevels.map(({ label }) => ({ label: , value: label, })); + } + + if (networkType === 'bitcoin') { + return filteredLevels.map(level => ({ + label: , + value: level.label, + blocks: level.blocks, + feePerUnit: level.feePerUnit, + })); + } + + return filteredLevels.map(level => ({ + label: , + value: level.label, + feePerUnit: level.feePerUnit, + })); +}; + +export type FeeOption = { + label: React.ReactNode; + value: FeeLevel['label']; + blocks?: number; + feePerUnit?: string; + maxFeePerGas?: string; + maxPriorityFeePerGas?: string; + maxWaitTimeEstimate?: number; + minWaitTimeEstimate?: number; + eip1559?: boolean; + gasFeeFiat?: string; + gasFeeCrypto?: string; + waitTimeEstimate?: string; +}; export interface FeesProps { account: Account; @@ -64,11 +107,11 @@ export interface FeesProps { getValues: UseFormGetValues; errors: FieldErrors; changeFeeLevel: (level: FeeLevel['label']) => void; - changeFeeLimit?: (value: string) => void; composedLevels?: PrecomposedLevels | PrecomposedLevelsCardano; label?: TranslationKey; rbfForm?: boolean; helperText?: React.ReactNode; + network: Network; } export const Fees = ({ @@ -76,140 +119,136 @@ export const Fees = ({ feeInfo, control, changeFeeLevel, - changeFeeLimit, composedLevels, label, rbfForm, helperText, + network, ...props }: FeesProps) => { // Type assertion allowing to make the component reusable, see https://stackoverflow.com/a/73624072. + const shouldUsePriorityFees = network?.features?.includes('eip1559'); const { getValues, register, setValue } = props as unknown as UseFormReturn; + const [isCustomFee, setIsCustomFee] = useState(false); + const [selectedLevelOption, setSelectedLevelOption] = useState( + shouldUsePriorityFees ? 'medium' : 'normal', + ); const errors = props.errors as unknown as FieldErrors; - const selectedOption = getValues('selectedFee') || 'normal'; - const isCustomLevel = selectedOption === 'custom'; + const selectedOption = isCustomFee ? 'custom' : selectedLevelOption; + // const isCustomLevel = selectedOption === 'custom'; const error = errors.selectedFee; const selectedLevel = feeInfo.levels.find(level => level.label === selectedOption)!; + console.log('selectedLevel', selectedLevel); const transactionInfo = composedLevels?.[selectedOption]; // Solana has only `normal` fee level, so we do not display any feeOptions since there is nothing to choose from - const feeOptions = networkType === 'solana' ? [] : buildFeeOptions(feeInfo.levels); - - console.log('feeOptions', feeOptions); + const feeOptions = buildFeeOptions(feeInfo.levels, networkType); - const shouldAnimateNormalFee = !isCustomLevel; + // const shouldAnimateNormalFee = !isCustomFee; return ( - - } + + + {networkType === 'ethereum' && ( + } + > + + + } + /> + )} + { + changeFeeLevel(!isCustomFee ? 'custom' : 'normal'); + setIsCustomFee(!isCustomFee); + }} + isFullWidth={false} + /> + + + <> + + {!isCustomFee && ( + - - - ) : ( - - ) - } - > - {transactionInfo !== undefined && transactionInfo.type !== 'error' && ( - - - - - - - )} - - - {feeOptions.length > 0 && ( - <> - - - {shouldAnimateNormalFee && ( - - - - )} - - - - {isCustomLevel && ( - - - - )} - - - {error && ( - - {error.message} - + )} + + + + {isCustomFee && ( + + + + )} + + + {error && ( + + {error.message} + + )} - {helperText && {helperText}} - - )} + {helperText && {helperText}} + ); }; diff --git a/packages/suite/src/hooks/wallet/form/useFees.ts b/packages/suite/src/hooks/wallet/form/useFees.ts index 7d942fcb4eb..16319ccbf6a 100644 --- a/packages/suite/src/hooks/wallet/form/useFees.ts +++ b/packages/suite/src/hooks/wallet/form/useFees.ts @@ -118,10 +118,13 @@ export const useFees = ({ // called from UI on click const changeFeeLevel = (level: FeeLevel['label']) => { if (selectedFeeRef.current === level || !feeInfo) return; + console.log('changeFeeLevel', level, selectedFeeRef.current, feeInfo); let feePerUnit; let feeLimit; + console.log('level!', level); if (level === 'custom') { + console.log('custom level!'); // switching to custom FeeLevel for the first time const currentLevel = feeInfo.levels.find( l => l.label === (selectedFeeRef.current || 'normal'), diff --git a/packages/suite/src/support/messages.ts b/packages/suite/src/support/messages.ts index 82f08db2d75..214b186fd18 100644 --- a/packages/suite/src/support/messages.ts +++ b/packages/suite/src/support/messages.ts @@ -6135,6 +6135,14 @@ export default defineMessages({ id: 'TR_DATA', defaultMessage: 'Data', }, + TR_MAX_PRIORITY_FEE_PER_GAS: { + id: 'TR_MAX_PRIORITY_FEE_PER_GAS', + defaultMessage: 'Max priority fee per gas', + }, + TR_MAX_FEE_PER_GAS: { + id: 'TR_MAX_FEE_PER_GAS', + defaultMessage: 'Max fee per gas', + }, TR_AFFECTED_TXS: { id: 'TR_AFFECTED_TXS', defaultMessage: 'This operation will remove the following transactions from the mempool', diff --git a/packages/suite/src/views/wallet/coinmarket/common/CoinmarketForm/CoinmarketFormInputs.tsx b/packages/suite/src/views/wallet/coinmarket/common/CoinmarketForm/CoinmarketFormInputs.tsx index 4bba0c2152d..ce340d2d47d 100644 --- a/packages/suite/src/views/wallet/coinmarket/common/CoinmarketForm/CoinmarketFormInputs.tsx +++ b/packages/suite/src/views/wallet/coinmarket/common/CoinmarketForm/CoinmarketFormInputs.tsx @@ -1,12 +1,5 @@ import { spacings } from '@trezor/theme'; -import { - Row, - Column, - Card, - ElevationContext, - FractionButton, - FractionButtonProps, -} from '@trezor/components'; +import { Row, Column, FractionButton, FractionButtonProps } from '@trezor/components'; import { hasBitcoinOnlyFirmware } from '@trezor/device-utils/src/firmwareUtils'; import { TokenAddress } from '@suite-common/wallet-types'; import { formatAmount } from '@suite-common/wallet-utils'; @@ -78,6 +71,7 @@ export const CoinmarketFormInputs = () => { control, feeInfo, account, + network, composedLevels, formState: { errors }, form: { helpers }, @@ -131,21 +125,18 @@ export const CoinmarketFormInputs = () => { )} - - - - - + @@ -157,6 +148,7 @@ export const CoinmarketFormInputs = () => { control, feeInfo, account, + network, composedLevels, formState: { errors }, form: { helpers }, @@ -218,21 +210,18 @@ export const CoinmarketFormInputs = () => { supportedCryptoCurrencies={supportedCryptoCurrencies} methods={{ ...context }} /> - - - - - + ); diff --git a/packages/suite/src/views/wallet/send/SendFees.tsx b/packages/suite/src/views/wallet/send/SendFees.tsx index aa1a0d63d19..7931482cb17 100644 --- a/packages/suite/src/views/wallet/send/SendFees.tsx +++ b/packages/suite/src/views/wallet/send/SendFees.tsx @@ -14,6 +14,7 @@ export const SendFees = () => { account, feeInfo, composedLevels, + network, } = useSendFormContext(); return ( @@ -28,6 +29,7 @@ export const SendFees = () => { account={account} composedLevels={composedLevels} changeFeeLevel={changeFeeLevel} + network={network} /> ); diff --git a/suite-common/wallet-core/src/send/sendFormEthereumThunks.ts b/suite-common/wallet-core/src/send/sendFormEthereumThunks.ts index 1549a5f8224..104a98842fe 100644 --- a/suite-common/wallet-core/src/send/sendFormEthereumThunks.ts +++ b/suite-common/wallet-core/src/send/sendFormEthereumThunks.ts @@ -48,7 +48,10 @@ const calculate = ( ): PrecomposedTransaction => { let amount: string; let max: string | undefined; - const feeInGwei = calculateEthFee(toWei(feeLevel.feePerUnit, 'gwei'), feeLevel.feeLimit || '0'); + const feeInGwei = calculateEthFee( + toWei(feeLevel.feePerUnit || '0', 'gwei'), + feeLevel.feeLimit || '0', + ); const availableTokenBalance = token ? amountToSmallestUnit(token.balance!, token.decimals)