diff --git a/packages/connect/src/api/ethereum/EthereumFees.ts b/packages/connect/src/api/ethereum/EthereumFees.ts index d44f9163bcb..9a4120697d5 100644 --- a/packages/connect/src/api/ethereum/EthereumFees.ts +++ b/packages/connect/src/api/ethereum/EthereumFees.ts @@ -2,7 +2,7 @@ import { BigNumber } from '@trezor/utils/src/bigNumber'; import { Blockchain } from '../../backend/BlockchainLink'; import type { EthereumNetworkInfo, FeeLevel } from '../../types'; -import { Blocks, MiscFeeLevels, findBlocksForFee } from '../common/MiscFees'; +import { Blocks, MiscFeeLevels } from '../common/MiscFees'; type EipResponse1559Level = 'low' | 'medium' | 'high'; type Eip1559Level = 'low' | 'normal' | 'high'; @@ -72,22 +72,4 @@ export class EthereumFeeLevels extends MiscFeeLevels { return this.levels; } - - updateEthereumCustomFee( - feePerUnit: string, - effectiveGasPrice?: string, - maxPriorityFeePerGas?: string, - ) { - // remove "custom" level from list - this.levels = this.levels.filter(l => l.label !== 'custom'); - // recreate "custom" level - const blocks = findBlocksForFee(feePerUnit, this.blocks); - this.levels.push({ - label: 'custom', - feePerUnit, - blocks, - maxPriorityFeePerGas, - effectiveGasPrice, - }); - } } diff --git a/packages/connect/src/types/fees.ts b/packages/connect/src/types/fees.ts index 821d10c2002..a991af0e18d 100644 --- a/packages/connect/src/types/fees.ts +++ b/packages/connect/src/types/fees.ts @@ -8,14 +8,6 @@ export const FeeInfo = Type.Object({ dustLimit: Type.Number(), }); -export type PriorityFeeEstimationDetails = Static; -export const PriorityFeeEstimationDetails = Type.Object({ - maxFeePerGas: Type.String(), - maxPriorityFeePerGas: Type.String(), - maxWaitTimeEstimate: Type.Optional(Type.Number()), - minWaitTimeEstimate: Type.Optional(Type.Number()), -}); - export type FeeLevel = Static; export const FeeLevel = Type.Object({ label: Type.Union([ @@ -32,6 +24,8 @@ export const FeeLevel = Type.Object({ baseFeePerGas: Type.Optional(Type.String()), maxFeePerGas: Type.Optional(Type.String()), effectiveGasPrice: Type.Optional(Type.String()), + customMaxBaseFeePerGas: Type.Optional(Type.String()), + customMaxPriorityFeePerGas: Type.Optional(Type.String()), maxPriorityFeePerGas: Type.Optional(Type.String()), maxWaitTimeEstimate: Type.Optional(Type.Number()), minWaitTimeEstimate: Type.Optional(Type.Number()), diff --git a/packages/suite/src/actions/wallet/stake/stakeFormActions.ts b/packages/suite/src/actions/wallet/stake/stakeFormActions.ts index b060a882c0e..b3df663835f 100644 --- a/packages/suite/src/actions/wallet/stake/stakeFormActions.ts +++ b/packages/suite/src/actions/wallet/stake/stakeFormActions.ts @@ -81,6 +81,8 @@ export const calculate = ( max, fee: feeInBaseUnits, feePerByte: feeLevel.feePerUnit, + maxFeePerGas: feeLevel.effectiveGasPrice || undefined, + maxPriorityFeePerGas: feeLevel.maxPriorityFeePerGas || undefined, feeLimit: feeLevel.feeLimit, bytes: 0, inputs: [], @@ -123,6 +125,7 @@ export const composeStakingTransaction = ( ) => { const { account, network } = formState; const composeOutputs = getExternalComposeOutput(formValues, account, network); + console.log('composeOutputs', formValues, composeOutputs); if (!composeOutputs) return; // no valid Output const { output, decimals } = composeOutputs; @@ -131,6 +134,8 @@ export const composeStakingTransaction = ( // wrap response into PrecomposedLevels object where key is a FeeLevel label const wrappedResponse: PrecomposedLevels = {}; const compareWithAmount = formValues.stakeType === 'stake'; + + console.log('predefinedLevels', predefinedLevels); const response = predefinedLevels.map(level => calculateTransaction( availableBalance, diff --git a/packages/suite/src/actions/wallet/stake/stakeFormEthereumActions.ts b/packages/suite/src/actions/wallet/stake/stakeFormEthereumActions.ts index c0370abe129..f6d214e0f84 100644 --- a/packages/suite/src/actions/wallet/stake/stakeFormEthereumActions.ts +++ b/packages/suite/src/actions/wallet/stake/stakeFormEthereumActions.ts @@ -9,6 +9,7 @@ import { UNSTAKE_INTERCHANGES, } from '@suite-common/wallet-constants'; import { ComposeActionContext, selectSelectedDevice } from '@suite-common/wallet-core'; +import { calculateEffectiveGasPrice } from '@suite-common/wallet-core/src/send/sendFormEthereumUtils'; import { AddressDisplayOptions, ExternalOutput, @@ -16,7 +17,7 @@ import { PrecomposedTransactionFinal, StakeFormState, } from '@suite-common/wallet-types'; -import { calculateEthFee, getAccountIdentity, isPending } from '@suite-common/wallet-utils'; +import { calculateMaxEthFee, getAccountIdentity, isPending } from '@suite-common/wallet-utils'; import TrezorConnect, { FeeLevel } from '@trezor/connect'; import { BigNumber } from '@trezor/utils/src/bigNumber'; @@ -31,14 +32,18 @@ import { import { calculate, composeStakingTransaction } from './stakeFormActions'; -const calculateTransaction = ( +const calculateStakingTransaction = ( availableBalance: string, output: ExternalOutput, feeLevel: FeeLevel, compareWithAmount = true, symbol: NetworkSymbol, ): PrecomposedTransaction => { - const feeInWei = calculateEthFee(toWei(feeLevel.feePerUnit, 'gwei'), feeLevel.feeLimit || '0'); + const isEip1559 = feeLevel.maxPriorityFeePerGas !== undefined; + + const feeInWei = isEip1559 + ? calculateMaxEthFee(feeLevel.effectiveGasPrice, feeLevel.feeLimit) + : calculateMaxEthFee(toWei(feeLevel.feePerUnit, 'gwei'), feeLevel.feeLimit); const stakingParams = { feeInBaseUnits: feeInWei, @@ -47,6 +52,8 @@ const calculateTransaction = ( minAmountForWithdrawalInBaseUnits: toWei(MIN_ETH_FOR_WITHDRAWALS.toString(), 'ether'), }; + console.log('feeLevel', feeLevel); + return calculate(availableBalance, output, feeLevel, compareWithAmount, symbol, stakingParams); }; @@ -81,11 +88,20 @@ export const composeTransaction = predefinedLevels.forEach(l => (l.feeLimit = customFeeLimit)); } // in case when selectedFee is set to 'custom' construct this FeeLevel from values + //TODO: calculate effective gas price here? if (formValues.selectedFee === 'custom') { + const calculatedEffectiveGasPrice = calculateEffectiveGasPrice( + formValues.customMaxPriorityFeePerGas, + formValues.customMaxBaseFeePerGas, + ); predefinedLevels.push({ label: 'custom', feePerUnit: formValues.feePerUnit, feeLimit: formValues.feeLimit, + customMaxBaseFeePerGas: formValues.customMaxBaseFeePerGas, + customMaxPriorityFeePerGas: formValues.customMaxPriorityFeePerGas, + effectiveGasPrice: calculatedEffectiveGasPrice, + maxPriorityFeePerGas: toWei(Number(formValues.customMaxPriorityFeePerGas), 'gwei'), blocks: -1, }); } @@ -94,7 +110,7 @@ export const composeTransaction = formValues, formState, predefinedLevels, - calculateTransaction, + calculateStakingTransaction, undefined, customFeeLimit, ); @@ -150,6 +166,8 @@ export const signTransaction = identity, amount: formValues.outputs[0].amount, gasPrice: transactionInfo.feePerByte, + maxFeePerGas: transactionInfo.maxFeePerGas, + maxPriorityFeePerGas: transactionInfo.maxPriorityFeePerGas, nonce, chainId: network.chainId, }); @@ -161,6 +179,8 @@ export const signTransaction = identity, amount: formValues.outputs[0].amount, gasPrice: transactionInfo.feePerByte, + maxFeePerGas: transactionInfo.maxFeePerGas, + maxPriorityFeePerGas: transactionInfo.maxPriorityFeePerGas, nonce, chainId: network.chainId, interchanges: UNSTAKE_INTERCHANGES, @@ -172,6 +192,8 @@ export const signTransaction = from: account.descriptor, identity, gasPrice: transactionInfo.feePerByte, + maxFeePerGas: transactionInfo.maxFeePerGas, + maxPriorityFeePerGas: transactionInfo.maxPriorityFeePerGas, nonce, chainId: network.chainId, }); diff --git a/packages/suite/src/components/suite/modals/ReduxModal/TransactionReviewModal/TransactionReviewSummary.tsx b/packages/suite/src/components/suite/modals/ReduxModal/TransactionReviewModal/TransactionReviewSummary.tsx index eecc9cc1e12..3a33f640543 100644 --- a/packages/suite/src/components/suite/modals/ReduxModal/TransactionReviewModal/TransactionReviewSummary.tsx +++ b/packages/suite/src/components/suite/modals/ReduxModal/TransactionReviewModal/TransactionReviewSummary.tsx @@ -1,5 +1,5 @@ import { formatDurationStrict } from '@suite-common/suite-utils'; -import { NetworkType, networks } from '@suite-common/wallet-config'; +import { NetworkType, getNetworkFeatures, networks } from '@suite-common/wallet-config'; import { FeeInfo, GeneralPrecomposedTransactionFinal, StakeType } from '@suite-common/wallet-types'; import { getFee } from '@suite-common/wallet-utils'; import { Box, IconButton, Note, Row, Text } from '@trezor/components'; @@ -47,14 +47,22 @@ export const TransactionReviewSummary = ({ const fees = useSelector(state => state.wallet.fees); const locale = useLocales(); const { symbol, accountType, index, networkType } = account; + const network = networks[symbol]; - const fee = getFee(networkType, tx); + + const baseFee = fees[symbol].levels[0].baseFeePerGas; + const hasEip1559Feature = getNetworkFeatures(symbol).includes('eip1559'); + const shouldUsePriorityFees = !!tx.fee && hasEip1559Feature && !!baseFee; + const fee = getFee({ account, tx, shouldUsePriorityFees }); + const estimateTime = getEstimatedTime(networkType, fees[account.symbol], tx); const formFeeRate = drafts[currentAccountKey]?.feePerUnit; const isFeeCustom = drafts[currentAccountKey]?.selectedFee === 'custom'; const isComposedFeeRateDifferent = isFeeCustom && formFeeRate !== fee; + const isEthereumNetworkType = networkType === 'ethereum'; + return ( @@ -74,7 +82,7 @@ export const TransactionReviewSummary = ({ )} - {!!tx.feeLimit && network.networkType !== 'solana' && ( + {!!tx.feeLimit && network.networkType !== 'solana' && !hasEip1559Feature && ( {': '} @@ -82,17 +90,15 @@ export const TransactionReviewSummary = ({ )} - {networkType === 'ethereum' ? ( - - - {': '} - - - ) : ( - - - - )} + + {isEthereumNetworkType && ( + <> + + {': '} + + )} + + {isComposedFeeRateDifferent && network.networkType === 'bitcoin' && ( diff --git a/packages/suite/src/components/wallet/Fees/CustomFee/CurrentFee.tsx b/packages/suite/src/components/wallet/Fees/CustomFee/CurrentFee.tsx index 9013f7de8eb..8c39800760c 100644 --- a/packages/suite/src/components/wallet/Fees/CustomFee/CurrentFee.tsx +++ b/packages/suite/src/components/wallet/Fees/CustomFee/CurrentFee.tsx @@ -10,12 +10,20 @@ type CurrentFeeProps = { feeIconName: IconName; currentFee: string; symbol: NetworkSymbol; + isEip1559?: boolean; }; -export const CurrentFee = ({ networkType, feeIconName, currentFee, symbol }: CurrentFeeProps) => ( +// For priority fees it should show current base fee +export const CurrentFee = ({ + networkType, + feeIconName, + currentFee, + symbol, + isEip1559 = false, +}: CurrentFeeProps) => ( - + diff --git a/packages/suite/src/components/wallet/Fees/CustomFee/CustomFee.tsx b/packages/suite/src/components/wallet/Fees/CustomFee/CustomFee.tsx index 3d00bfa4dd6..3a1baa8635c 100644 --- a/packages/suite/src/components/wallet/Fees/CustomFee/CustomFee.tsx +++ b/packages/suite/src/components/wallet/Fees/CustomFee/CustomFee.tsx @@ -6,6 +6,8 @@ import { UseFormSetValue, } from 'react-hook-form'; +import { fromWei } from 'web3-utils'; + import { NetworkSymbol, NetworkType } from '@suite-common/wallet-config'; import { FeeInfo, FormState } from '@suite-common/wallet-types'; import { getFeeUnits, isInteger } from '@suite-common/wallet-utils'; @@ -75,7 +77,17 @@ export const CustomFee = ({ const locale = useSelector(selectLanguage); + const normalLevel = feeInfo.levels.filter(level => level.label === 'normal')[0]; + + const isEip1559 = normalLevel.maxPriorityFeePerGas !== undefined; + + const currentBaseFee = fromWei(Number(normalLevel.baseFeePerGas), 'Gwei'); + const getCurrentFee = () => { + if (isEip1559) { + return `${currentBaseFee}`; + } + const { levels } = feeInfo; const middleIndex = Math.floor((levels.length - 1) / 2); @@ -92,11 +104,14 @@ export const CustomFee = ({ feeIconName={feeIconName} currentFee={getCurrentFee()} symbol={symbol} + isEip1559={isEip1559} /> {networkType === 'ethereum' ? ( ({ networkType, feeInfo, @@ -20,8 +23,13 @@ export const CustomFeeEthereum = ({ translationString, feeUnits, sharedRules, + isEip1559, + currentBaseFee, ...props -}: CustomFeeBasicProps) => { +}: CustomFeeBasicProps & { + isEip1559: boolean; + currentBaseFee: string; +}) => { // 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; @@ -32,6 +40,9 @@ export const CustomFeeEthereum = ({ const feePerUnitError = errors.feePerUnit; const feeLimitError = errors.feeLimit; + const customMaxBaseFeePerGasError = errors.customMaxBaseFeePerGas; + const customMaxPriorityFeePerGasError = errors.customMaxPriorityFeePerGas; + const feeLimitRules = { required: translationString('GAS_LIMIT_IS_NOT_SET'), validate: { @@ -66,6 +77,27 @@ export const CustomFeeEthereum = ({ }, }; + const customMaxBaseFeePerGasRules = { + ...sharedRules, + validate: { + ...sharedRules.validate, + ethereumDecimalsLimit: feeRules.validate.ethereumDecimalsLimit, + // Base fee can't be lower than the current network base fee. + customMaxBaseFeePerGas: (value: string) => { + const baseFee = new BigNumber(value); + if (baseFee.isLessThan(currentBaseFee)) { + return translationString('TR_CUSTOM_FEE_BASE_FEE_BELOW_CURRENT'); + } + }, + }, + }; + + const customMaxPriorityFeePerGasRules = { + validate: { + ethereumDecimalsLimit: feeRules.validate.ethereumDecimalsLimit, + }, + }; + const feeLimitValidationProps = { onClick: () => estimatedFeeLimit && @@ -78,6 +110,20 @@ export const CustomFeeEthereum = ({ const feeLimitValidationButtonProps = feeLimitError?.type === 'feeLimit' ? feeLimitValidationProps : undefined; + const customMaxBaseFeeValidationProps = { + onClick: () => + estimatedFeeLimit && + setValue(CUSTOM_MAX_BASE_FEE_PER_GAS, currentBaseFee, { + shouldValidate: true, + }), + text: translationString('TR_CUSTOM_MAX_BASE_FEE_USE_NETWORK_BASE_FEE'), + }; + + const customMaxBaseFeeValidationButtonProps = + customMaxBaseFeePerGasError?.type === 'customMaxBaseFeePerGas' + ? customMaxBaseFeeValidationProps + : undefined; + const gasLimitInput = ( } @@ -98,6 +144,49 @@ export const CustomFeeEthereum = ({ /> ); + const eip1559InputFields = ( + <> + {/* Base fee per gas can't be lower than the current network base fee */} + } + locale={locale} + control={control} + inputState={getInputState(customMaxBaseFeePerGasError)} + innerAddon={ + + {feeUnits} + + } + name={CUSTOM_MAX_BASE_FEE_PER_GAS} + data-testid={CUSTOM_MAX_BASE_FEE_PER_GAS} + rules={customMaxBaseFeePerGasRules} + bottomText={ + customMaxBaseFeePerGasError?.message ? ( + + ) : null + } + /> + } + locale={locale} + control={control} + inputState={getInputState(customMaxPriorityFeePerGasError)} + innerAddon={ + + {feeUnits} + + } + name={CUSTOM_MAX_PRIORITY_FEE_PER_GAS} + data-testid={CUSTOM_MAX_PRIORITY_FEE_PER_GAS} + rules={customMaxPriorityFeePerGasRules} + bottomText={customMaxPriorityFeePerGasError?.message || null} + /> + + ); + const legacyEvmInputFields = ( } @@ -119,7 +208,7 @@ export const CustomFeeEthereum = ({ return ( <> {gasLimitInput} - {legacyEvmInputFields} + {isEip1559 ? eip1559InputFields : legacyEvmInputFields} ); }; diff --git a/packages/suite/src/components/wallet/Fees/Fees.tsx b/packages/suite/src/components/wallet/Fees/Fees.tsx index 49bbd80abe2..2c3c2ddab10 100644 --- a/packages/suite/src/components/wallet/Fees/Fees.tsx +++ b/packages/suite/src/components/wallet/Fees/Fees.tsx @@ -44,6 +44,8 @@ export type FeeOptionType = { feePerUnit?: string; networkAmount?: string | null; feePerTx?: string; // Solana specific + maxWaitTime?: number; // Ethereum specific + effectiveGasPrice?: string; // Ethereum specific }; const SelectBarWrapper = styled.div` @@ -109,8 +111,15 @@ const buildFeeOptions = ( }; }); case 'ethereum': - // legacy fee format - return filteredLevels.map(level => buildBasicFeeOptions(level)); + return filteredLevels.map(level => { + const basicFeeOption = buildBasicFeeOptions(level); + + return { + ...basicFeeOption, + maxWaitTime: level.maxWaitTimeEstimate, + effectiveGasPrice: level.effectiveGasPrice, + }; + }); case 'bitcoin': return filteredLevels.map(level => { const basicFeeOption = buildBasicFeeOptions(level); @@ -143,11 +152,15 @@ export const Fees = ({ const errors = props.errors as unknown as FieldErrors; const error = errors.selectedFee; - const selectedLevel = feeInfo.levels.find(level => level.label === selectedOption); + const selectedLevel = + feeInfo.levels.find(level => level.label === selectedOption) || + feeInfo.levels.find(level => level.label === 'normal')!; const transactionInfo = composedLevels?.[selectedOption]; const feeOptions = buildFeeOptions(feeInfo.levels, networkType, symbol, composedLevels); + const isEip1559 = feeOptions.some(option => option.effectiveGasPrice); + const hasTransactionInfo = transactionInfo !== undefined && transactionInfo.type !== 'error'; const networkAmount = hasTransactionInfo ? formatNetworkAmount(transactionInfo.fee, symbol) @@ -208,7 +221,7 @@ export const Fees = ({ )} - {!isCustomFee && selectedLevel && ( + {!isCustomFee && ( ({ - : + : {networkAmount && ( diff --git a/packages/suite/src/components/wallet/Fees/StandardFee/EthereumFeeCards.tsx b/packages/suite/src/components/wallet/Fees/StandardFee/EthereumFeeCards.tsx index 2c04ad0d0fe..6882941b3fb 100644 --- a/packages/suite/src/components/wallet/Fees/StandardFee/EthereumFeeCards.tsx +++ b/packages/suite/src/components/wallet/Fees/StandardFee/EthereumFeeCards.tsx @@ -1,7 +1,12 @@ +import { fromWei } from 'web3-utils'; + +import { formatDurationStrict } from '@suite-common/suite-utils'; import { FeeRate } from '@trezor/product-components'; import { FiatValue } from 'src/components/suite'; +import { useLocales } from 'src/hooks/suite'; +import { FeeOptionType } from '../Fees'; import { FeeCard } from './FeeCard'; import { StandardFeeProps } from './StandardFee'; @@ -13,29 +18,52 @@ export const EthereumFeeCards = ({ symbol, networkType, }: StandardFeeProps) => { + const locale = useLocales(); if (!showFee || !feeOptions.length) { return null; } - return feeOptions.map((fee, index) => ( - {fee.label}} - topRightChild="" - bottomLeftChild={ - { + if (isEip1559) { + return `~${formatDurationStrict(fee.maxWaitTime || 0, locale)}`; + } + + return ''; + }; + + return ( + <> + {feeOptions?.map((fee, index) => ( + {fee.label}} + topRightChild={getTimeEstimate(fee)} + bottomLeftChild={ + + } + bottomRightChild={ + + } /> - } - bottomRightChild={ - - } - /> - )); + ))} + + ); }; diff --git a/packages/suite/src/hooks/wallet/form/useCompose.ts b/packages/suite/src/hooks/wallet/form/useCompose.ts index 2084d610185..04b57d49761 100644 --- a/packages/suite/src/hooks/wallet/form/useCompose.ts +++ b/packages/suite/src/hooks/wallet/form/useCompose.ts @@ -204,9 +204,13 @@ export const useCompose = ({ setValue('selectedFee', nearest); if (nearest === 'custom') { // @ts-expect-error: type = error already filtered above - const { feePerByte, feeLimit } = composed; + const { feePerByte, feeLimit, effectiveGasPrice, maxPriorityFeePerGas } = + composed; setValue('feePerUnit', feePerByte); setValue('feeLimit', feeLimit || ''); + setValue('effectiveGasPrice', effectiveGasPrice || ''); + setValue('maxPriorityFeePerGas', maxPriorityFeePerGas || ''); + setValue('customMaxPriorityFeePerGas', maxPriorityFeePerGas || ''); } } // or do nothing, use default composed tx diff --git a/packages/suite/src/hooks/wallet/form/useFees.ts b/packages/suite/src/hooks/wallet/form/useFees.ts index a024311fcf3..2b3ec9d9e1a 100644 --- a/packages/suite/src/hooks/wallet/form/useFees.ts +++ b/packages/suite/src/hooks/wallet/form/useFees.ts @@ -1,6 +1,7 @@ import { useEffect, useRef } from 'react'; import { FieldPath, UseFormReturn } from 'react-hook-form'; +import { calculateEffectiveGasPrice } from '@suite-common/wallet-core/src/send/sendFormEthereumUtils'; import { FeeInfo, FormState, @@ -41,8 +42,11 @@ export const useFees = ({ const selectedFeeRef = useRef(defaultValue); const feePerUnitRef = useRef(''); const feeLimitRef = useRef(''); + const customMaxPriorityFeePerGasRef = useRef(''); + const customMaxBaseFeePerGasRef = useRef(''); const estimatedFeeLimitRef = useRef(''); const saveLastUsedFeeRef = useRef(saveLastUsedFee); + const effectiveGasPriceRef = useRef(''); // Type assertion allowing to make the component reusable, see https://stackoverflow.com/a/73624072. const { clearErrors, getValues, register, setValue, watch } = @@ -52,6 +56,7 @@ export const useFees = ({ useEffect(() => { register('selectedFee', { shouldUnregister: true }); register('estimatedFeeLimit', { shouldUnregister: true }); + register('effectiveGasPrice', { shouldUnregister: true }); }, [register]); // watch selectedFee change and update local references @@ -59,15 +64,27 @@ export const useFees = ({ useEffect(() => { if (selectedFeeRef.current === selectedFee) return; selectedFeeRef.current = selectedFee; - const { feePerUnit, feeLimit } = getValues(); + const { + feePerUnit, + feeLimit, + customMaxPriorityFeePerGas, + customMaxBaseFeePerGas, + effectiveGasPrice, // is used here only to update it in the last used fee level + } = getValues(); feePerUnitRef.current = feePerUnit; feeLimitRef.current = feeLimit; + customMaxPriorityFeePerGasRef.current = customMaxPriorityFeePerGas; + customMaxBaseFeePerGasRef.current = customMaxBaseFeePerGas; + effectiveGasPriceRef.current = effectiveGasPrice; }, [selectedFee, getValues]); // watch custom feePerUnit/feeLimit inputs change const feePerUnit = watch('feePerUnit'); const feeLimit = watch('feeLimit'); const baseFee = watch('baseFee'); + const customMaxPriorityFeePerGas = watch('customMaxPriorityFeePerGas'); + const customMaxBaseFeePerGas = watch('customMaxBaseFeePerGas'); + const effectiveGasPrice = watch('effectiveGasPrice'); useEffect(() => { if (selectedFeeRef.current !== 'custom') return; @@ -82,7 +99,32 @@ export const useFees = ({ updateField = 'feeLimit'; } - // compose + if ( + customMaxBaseFeePerGas && + customMaxPriorityFeePerGas && + (customMaxPriorityFeePerGasRef.current !== customMaxPriorityFeePerGas || + customMaxBaseFeePerGasRef.current !== customMaxBaseFeePerGas) + ) { + const effectiveGasPriceNew = calculateEffectiveGasPrice( + customMaxPriorityFeePerGas, + customMaxBaseFeePerGas, + ); + setValue('effectiveGasPrice', effectiveGasPriceNew, { shouldValidate: true }); + effectiveGasPriceRef.current = effectiveGasPriceNew; + updateField = 'effectiveGasPrice'; + } + + if (customMaxBaseFeePerGasRef.current !== customMaxBaseFeePerGas) { + customMaxBaseFeePerGasRef.current = customMaxBaseFeePerGas; + updateField = 'customMaxBaseFeePerGas'; + } + + if (customMaxPriorityFeePerGasRef.current !== customMaxPriorityFeePerGas) { + customMaxPriorityFeePerGasRef.current = customMaxPriorityFeePerGas; + updateField = 'customMaxPriorityFeePerGas'; + } + + //compose if (updateField) { if (composeRequest) { composeRequest(updateField); @@ -95,11 +137,29 @@ export const useFees = ({ !errors.feeLimit ) { dispatch( - setLastUsedFeeLevel({ label: 'custom', feePerUnit, feeLimit, blocks: -1 }), + setLastUsedFeeLevel({ + label: 'custom', + feePerUnit, + feeLimit, + blocks: -1, + customMaxPriorityFeePerGas, + effectiveGasPrice, + }), ); } } - }, [dispatch, feePerUnit, feeLimit, errors.feePerUnit, errors.feeLimit, composeRequest]); + }, [ + dispatch, + feePerUnit, + effectiveGasPrice, + feeLimit, + customMaxPriorityFeePerGas, + customMaxBaseFeePerGas, + errors.feePerUnit, + errors.feeLimit, + composeRequest, + setValue, + ]); // watch estimatedFee change const estimatedFeeLimit = watch('estimatedFeeLimit'); diff --git a/packages/suite/src/hooks/wallet/form/useStakeCompose.ts b/packages/suite/src/hooks/wallet/form/useStakeCompose.ts index 250e3d70772..1ce24f4336a 100644 --- a/packages/suite/src/hooks/wallet/form/useStakeCompose.ts +++ b/packages/suite/src/hooks/wallet/form/useStakeCompose.ts @@ -184,9 +184,12 @@ export const useStakeCompose = ({ setValue('selectedFee', nearest); if (nearest === 'custom') { // @ts-expect-error: type = error already filtered above - const { feePerByte, feeLimit } = composed; + const { feePerByte, feeLimit, effectiveGasPrice, maxPriorityFeePerGas } = + composed; setValue('feePerUnit', feePerByte); setValue('feeLimit', feeLimit || ''); + setValue('customMaxPriorityFeePerGas', maxPriorityFeePerGas || ''); + setValue('customMaxBaseFeePerGas', effectiveGasPrice || ''); } } // or do nothing, use default composed tx diff --git a/packages/suite/src/hooks/wallet/useSendForm.ts b/packages/suite/src/hooks/wallet/useSendForm.ts index c8d32d7b949..3452607f31b 100644 --- a/packages/suite/src/hooks/wallet/useSendForm.ts +++ b/packages/suite/src/hooks/wallet/useSendForm.ts @@ -126,6 +126,10 @@ export const useSendForm = (props: UseSendFormProps): SendContextValues => { if (lastUsedFee.label === 'custom') { feeEnhancement.feePerUnit = lastUsedFee.feePerUnit; feeEnhancement.feeLimit = lastUsedFee.feeLimit; + feeEnhancement.customMaxPriorityFeePerGas = + lastUsedFee.customMaxPriorityFeePerGas; + feeEnhancement.customMaxBaseFeePerGas = lastUsedFee.customMaxBaseFeePerGas; + feeEnhancement.maxFeePerGas = lastUsedFee.maxFeePerGas; } } } diff --git a/packages/suite/src/hooks/wallet/useSendFormCompose.ts b/packages/suite/src/hooks/wallet/useSendFormCompose.ts index c307f71ba56..90aca3e9fc9 100644 --- a/packages/suite/src/hooks/wallet/useSendFormCompose.ts +++ b/packages/suite/src/hooks/wallet/useSendFormCompose.ts @@ -262,9 +262,12 @@ export const useSendFormCompose = ({ setValue('selectedFee', nearest); if (nearest === 'custom') { // @ts-expect-error: type = error already filtered above - const { feePerByte, feeLimit } = composed; + const { feePerByte, feeLimit, maxPriorityFeePerGas, maxBaseFeePerGas } = + composed; setValue('feePerUnit', feePerByte); setValue('feeLimit', feeLimit || ''); + setValue('customMaxPriorityFeePerGas', maxPriorityFeePerGas || ''); + setValue('customMaxBaseFeePerGas', maxBaseFeePerGas || ''); } setDraftSaveRequest(true); } diff --git a/packages/suite/src/hooks/wallet/useTradingRecomposeAndSign.ts b/packages/suite/src/hooks/wallet/useTradingRecomposeAndSign.ts index a81566c6eea..3a5e7aaf03a 100644 --- a/packages/suite/src/hooks/wallet/useTradingRecomposeAndSign.ts +++ b/packages/suite/src/hooks/wallet/useTradingRecomposeAndSign.ts @@ -70,6 +70,10 @@ export const useTradingRecomposeAndSign = () => { setMaxOutputId: !composed.token?.contract ? setMaxOutputId : undefined, selectedFee, feePerUnit: composed.feePerByte, + maxFeePerGas: composed.maxFeePerGas, + maxPriorityFeePerGas: composed.maxPriorityFeePerGas, + customMaxBaseFeePerGas: composed.maxFeePerGas, + customMaxPriorityFeePerGas: composed.maxPriorityFeePerGas, feeLimit: composed.feeLimit || '', estimatedFeeLimit: composed.estimatedFeeLimit, options, @@ -85,6 +89,8 @@ export const useTradingRecomposeAndSign = () => { networkType: account.networkType, feeInfo: fees[account.symbol], }); + + console.log('feeInfo', feeInfo, formState); const composeContext = { account, network, feeInfo }; // recalcCustomLimit is used in case of custom fee level, when we want to keep the feePerUnit defined by the user @@ -124,6 +130,7 @@ export const useTradingRecomposeAndSign = () => { return; } + //TODO priority formState.feeLimit = normalLevels.normal.feeLimit; } diff --git a/packages/suite/src/reducers/wallet/tradingReducer.ts b/packages/suite/src/reducers/wallet/tradingReducer.ts index 2a9f943d102..26e4a36085c 100644 --- a/packages/suite/src/reducers/wallet/tradingReducer.ts +++ b/packages/suite/src/reducers/wallet/tradingReducer.ts @@ -36,7 +36,14 @@ import { Action } from 'src/types/suite'; export interface ComposedTransactionInfo { composed?: Pick< PrecomposedTransactionFinal, - 'feePerByte' | 'estimatedFeeLimit' | 'feeLimit' | 'token' | 'fee' + | 'feePerByte' + | 'estimatedFeeLimit' + | 'feeLimit' + | 'token' + | 'fee' + | 'maxFeePerGas' + | 'maxPriorityFeePerGas' + | 'effectiveGasPrice' >; selectedFee?: FeeLevel['label']; } diff --git a/packages/suite/src/support/messages.ts b/packages/suite/src/support/messages.ts index 6fafc0b0d44..5892cf3818e 100644 --- a/packages/suite/src/support/messages.ts +++ b/packages/suite/src/support/messages.ts @@ -3440,6 +3440,18 @@ export default defineMessages({ id: 'TR_CURRENT_FEE_CUSTOM_FEES', defaultMessage: 'Current network fee:', }, + TR_CURRENT_BASE_FEE: { + id: 'TR_CURRENT_BASE_FEE', + defaultMessage: 'Current network base fee:', + }, + TR_MAX_BASE_FEE_PER_GAS: { + id: 'TR_MAX_BASE_FEE_PER_GAS', + defaultMessage: 'Max base fee', + }, + TR_CUSTOM_MAX_BASE_FEE_USE_NETWORK_BASE_FEE: { + id: 'TR_CUSTOM_MAX_BASE_FEE_USE_NETWORK_BASE_FEE', + defaultMessage: 'Use network base fee', + }, GAS_LIMIT_IS_NOT_SET: { id: 'GAS_LIMIT_IS_NOT_SET', defaultMessage: 'Set gas limit for this transaction', @@ -5585,6 +5597,10 @@ export default defineMessages({ description: 'Label in Send form for Ethereum network type', id: 'MAX_FEE', }, + WHY_FEES: { + defaultMessage: 'Why fees?', + id: 'WHY_FEES', + }, EXPECTED_FEE: { defaultMessage: 'Expected fee', description: 'Label in Send form for Solana network type', @@ -5610,6 +5626,18 @@ export default defineMessages({ defaultMessage: 'Low', id: 'FEE_LEVEL_LOW', }, + FEE_LEVEL_MEDIUM: { + defaultMessage: 'Medium', + id: 'FEE_LEVEL_MEDIUM', + }, + TR_MAX_PRIORITY_FEE_PER_GAS: { + defaultMessage: 'Priority fee', + id: 'TR_MAX_PRIORITY_FEE_PER_GAS', + }, + TR_MAX_FEE_PER_GAS: { + defaultMessage: 'Max fee', + id: 'TR_MAX_FEE_PER_GAS', + }, CUSTOM_FEE_IS_NOT_SET: { defaultMessage: 'Enter the fee rate you want to spend in order to complete this transaction.', @@ -5623,6 +5651,10 @@ export default defineMessages({ defaultMessage: 'Enter a fee between {minFee} and {maxFee}', id: 'CUSTOM_FEE_NOT_IN_RANGE', }, + TR_CUSTOM_FEE_BASE_FEE_BELOW_CURRENT: { + defaultMessage: 'Custom base fee can not be below current network base fee.', + id: 'TR_CUSTOM_FEE_BASE_FEE_BELOW_CURRENT', + }, CUSTOM_FEE_LIMIT_BELOW_RECOMMENDED: { defaultMessage: 'Gas limit too low', id: 'CUSTOM_FEE_LIMIT_BELOW_RECOMMENDED', diff --git a/packages/suite/src/utils/suite/__tests__/ethereumStaking.test.ts b/packages/suite/src/utils/suite/__tests__/ethereumStaking.test.ts index 39f9c78eaf7..3954bfdbbd4 100644 --- a/packages/suite/src/utils/suite/__tests__/ethereumStaking.test.ts +++ b/packages/suite/src/utils/suite/__tests__/ethereumStaking.test.ts @@ -56,7 +56,12 @@ import { describe('transformTx', () => { transformTxFixtures.forEach(test => { it(test.description, () => { - const result = transformTx(test.tx, test.gasPrice, test.nonce, test.chainId); + const result = transformTx({ + tx: test.tx, + gasPrice: test.gasPrice, + nonce: test.nonce, + chainId: test.chainId, + }); expect(result).toEqual(test.result); expect(result).not.toHaveProperty('from'); }); diff --git a/packages/suite/src/utils/suite/ethereumStaking.ts b/packages/suite/src/utils/suite/ethereumStaking.ts index 3c64a595231..10be1f414da 100644 --- a/packages/suite/src/utils/suite/ethereumStaking.ts +++ b/packages/suite/src/utils/suite/ethereumStaking.ts @@ -23,7 +23,12 @@ import { isSupportedEthStakingNetworkSymbol, sanitizeHex, } from '@suite-common/wallet-utils'; -import TrezorConnect, { EthereumTransaction, InternalTransfer, Success } from '@trezor/connect'; +import TrezorConnect, { + EthereumTransaction, + EthereumTransactionEIP1559, + InternalTransfer, + Success, +} from '@trezor/connect'; import { BlockchainEstimatedFee } from '@trezor/connect/src/types/api/blockchainEstimateFee'; import { PartialRecord } from '@trezor/type-utils'; import { BigNumber } from '@trezor/utils/src/bigNumber'; @@ -297,22 +302,48 @@ export const getStakeFormsDefaultValues = ({ selectedUtxos: [], }); -export const transformTx = ( - tx: any, - gasPrice: string, - nonce: string, - chainId: number, -): EthereumTransaction => { - const transformedTx = { - ...tx, - gasLimit: numberToHex(tx.gasLimit), - gasPrice: numberToHex(toWei(gasPrice, 'gwei')), - nonce: numberToHex(nonce), - chainId, - data: sanitizeHex(tx.data), - // in send form, the amount is in ether, here in wei because it is converted earlier in stake, unstake, claimToWithdraw methods - value: numberToHex(tx.value), - }; +type TransformTxParams = { + tx: any; + gasPrice: string | undefined; + nonce: string; + chainId: number; + maxFeePerGas?: string; + maxPriorityFeePerGas?: string; +}; +export const transformTx = ({ + tx, + gasPrice, + nonce, + chainId, + maxFeePerGas, + maxPriorityFeePerGas, +}: TransformTxParams): EthereumTransaction | EthereumTransactionEIP1559 => { + let transformedTx; + + if (maxFeePerGas && maxPriorityFeePerGas) { + transformedTx = { + ...tx, + gasLimit: numberToHex(tx.gasLimit), + gasPrice: undefined, + nonce: numberToHex(nonce), + chainId, + data: sanitizeHex(tx.data), + maxFeePerGas: numberToHex(maxFeePerGas), + maxPriorityFeePerGas: numberToHex(maxPriorityFeePerGas), + value: numberToHex(tx.value), + }; + } else { + transformedTx = { + ...tx, + gasLimit: numberToHex(tx.gasLimit), + gasPrice: numberToHex(toWei(gasPrice || '0', 'gwei')), + nonce: numberToHex(nonce), + chainId, + data: sanitizeHex(tx.data), + // in send form, the amount is in ether, here in wei because it is converted earlier in stake, unstake, claimToWithdraw methods + value: numberToHex(tx.value), + }; + } delete transformedTx.from; return transformedTx; @@ -323,14 +354,16 @@ interface PrepareStakeEthTxParams { identity?: string; from: string; amount: string; - gasPrice: string; + gasPrice: string | undefined; nonce: string; chainId: number; + maxFeePerGas?: string; + maxPriorityFeePerGas?: string; } export type PrepareStakeEthTxResponse = | { success: true; - tx: EthereumTransaction; + tx: EthereumTransaction | EthereumTransactionEIP1559; } | { success: false; @@ -344,6 +377,8 @@ export const prepareStakeEthTx = async ({ gasPrice, nonce, chainId, + maxFeePerGas, + maxPriorityFeePerGas, identity, }: PrepareStakeEthTxParams): Promise => { try { @@ -353,7 +388,14 @@ export const prepareStakeEthTx = async ({ symbol, identity, }); - const transformedTx = transformTx(tx, gasPrice, nonce, chainId); + const transformedTx = transformTx({ + tx, + gasPrice, + nonce, + chainId, + maxFeePerGas, + maxPriorityFeePerGas, + }); return { success: true, @@ -382,6 +424,8 @@ export const prepareUnstakeEthTx = async ({ chainId, identity, interchanges = UNSTAKE_INTERCHANGES, + maxFeePerGas, + maxPriorityFeePerGas, }: PrepareUnstakeEthTxParams): Promise => { try { const tx = await unstake({ @@ -391,7 +435,14 @@ export const prepareUnstakeEthTx = async ({ interchanges, symbol, }); - const transformedTx = transformTx(tx, gasPrice, nonce, chainId); + const transformedTx = transformTx({ + tx, + gasPrice, + nonce, + chainId, + maxFeePerGas, + maxPriorityFeePerGas, + }); return { success: true, @@ -416,10 +467,19 @@ export const prepareClaimEthTx = async ({ gasPrice, nonce, chainId, + maxFeePerGas, + maxPriorityFeePerGas, }: PrepareClaimEthTxParams): Promise => { try { const tx = await claimWithdrawRequest({ from, symbol, identity }); - const transformedTx = transformTx(tx, gasPrice, nonce, chainId); + const transformedTx = transformTx({ + tx, + gasPrice, + nonce, + chainId, + maxFeePerGas, + maxPriorityFeePerGas, + }); return { success: true, diff --git a/suite-common/wallet-config/src/networksConfig.ts b/suite-common/wallet-config/src/networksConfig.ts index a6aacf8b1c5..406514a10ad 100644 --- a/suite-common/wallet-config/src/networksConfig.ts +++ b/suite-common/wallet-config/src/networksConfig.ts @@ -57,6 +57,7 @@ export const networks = { 'coin-definitions', 'nft-definitions', 'staking', + 'eip1559', ], backendTypes: ['blockbook'], accountTypes: { @@ -87,7 +88,15 @@ export const networks = { decimals: 18, testnet: false, explorer: getExplorerUrls('https://pol1.trezor.io', 'ethereum'), - features: ['rbf', 'sign-verify', 'tokens', 'nfts', 'coin-definitions', 'nft-definitions'], + features: [ + 'rbf', + 'sign-verify', + 'tokens', + 'nfts', + 'coin-definitions', + 'nft-definitions', + 'eip1559', + ], backendTypes: ['blockbook'], accountTypes: { ledger: { @@ -136,7 +145,15 @@ export const networks = { decimals: 18, testnet: false, explorer: getExplorerUrls('https://arbiscan.io', 'ethereum'), - features: ['rbf', 'sign-verify', 'tokens', 'nfts', 'coin-definitions', 'nft-definitions'], + features: [ + 'rbf', + 'sign-verify', + 'tokens', + 'nfts', + 'coin-definitions', + 'nft-definitions', + 'eip1559', + ], backendTypes: ['blockbook'], accountTypes: { ledger: { @@ -161,7 +178,15 @@ export const networks = { decimals: 18, testnet: false, explorer: getExplorerUrls('https://basescan.org', 'ethereum'), - features: ['rbf', 'sign-verify', 'tokens', 'nfts', 'coin-definitions', 'nft-definitions'], + features: [ + 'rbf', + 'sign-verify', + 'tokens', + 'nfts', + 'coin-definitions', + 'nft-definitions', + 'eip1559', + ], backendTypes: ['blockbook'], accountTypes: { ledger: { @@ -186,7 +211,15 @@ export const networks = { decimals: 18, testnet: false, explorer: getExplorerUrls('https://optimistic.etherscan.io', 'ethereum'), - features: ['rbf', 'sign-verify', 'tokens', 'nfts', 'coin-definitions', 'nft-definitions'], + features: [ + 'rbf', + 'sign-verify', + 'tokens', + 'nfts', + 'coin-definitions', + 'nft-definitions', + 'eip1559', + ], backendTypes: ['blockbook'], accountTypes: { ledger: { @@ -446,7 +479,7 @@ export const networks = { decimals: 18, testnet: true, explorer: getExplorerUrls('https://sepolia1.trezor.io', 'ethereum'), - features: ['rbf', 'sign-verify', 'tokens', 'nfts', 'nft-definitions'], + features: ['rbf', 'sign-verify', 'tokens', 'nfts', 'nft-definitions', 'eip1559'], backendTypes: ['blockbook'], accountTypes: {}, coingeckoId: undefined, @@ -462,7 +495,7 @@ export const networks = { decimals: 18, testnet: true, explorer: getExplorerUrls('https://holesky1.trezor.io', 'ethereum'), - features: ['rbf', 'sign-verify', 'tokens', 'staking', 'nfts', 'nft-definitions'], + features: ['rbf', 'sign-verify', 'tokens', 'staking', 'nfts', 'nft-definitions', 'eip1559'], backendTypes: ['blockbook'], accountTypes: {}, coingeckoId: undefined, diff --git a/suite-common/wallet-config/src/types.ts b/suite-common/wallet-config/src/types.ts index e513149ecfd..73e044a34da 100644 --- a/suite-common/wallet-config/src/types.ts +++ b/suite-common/wallet-config/src/types.ts @@ -57,7 +57,8 @@ export type NetworkFeature = | 'tokens' | 'staking' | 'coin-definitions' - | 'nft-definitions'; + | 'nft-definitions' + | 'eip1559'; type Level = `/${number}'`; type MaybeApostrophe = `'` | ''; diff --git a/suite-common/wallet-core/src/blockchain/blockchainThunks.ts b/suite-common/wallet-core/src/blockchain/blockchainThunks.ts index 49efb5b6968..c3caf5b4c74 100644 --- a/suite-common/wallet-core/src/blockchain/blockchainThunks.ts +++ b/suite-common/wallet-core/src/blockchain/blockchainThunks.ts @@ -3,6 +3,7 @@ import { notificationsActions } from '@suite-common/toast-notifications'; import { NetworkSymbol, externalBackendTypeNetworks, + getNetworkFeatures, getNetworkOptional, isNetworkSymbol, isTrezorInfraBasedNetwork, @@ -126,14 +127,18 @@ export const updateFeeInfoThunk = createThunk( let newFeeInfo; - if (network.networkType === 'ethereum') { - // NOTE: ethereum smart fees are not implemented properly in @trezor/connect Issue: https://github.com/trezor/trezor-suite/issues/5340 - // create raw call to @trezor/blockchain-link, receive data and create FeeLevel.normal from it + const feeLevels: 'preloaded' | 'smart' = getNetworkFeatures(network.symbol).includes( + 'eip1559', + ) + ? 'smart' + : 'preloaded'; + if (network.networkType === 'ethereum') { const result = await TrezorConnect.blockchainEstimateFee({ coin: network.symbol, request: { blocks: [2], + feeLevels, specific: { from: '0x0000000000000000000000000000000000000000', to: '0x0000000000000000000000000000000000000000', @@ -146,7 +151,7 @@ export const updateFeeInfoThunk = createThunk( levels: result.payload.levels.map(l => ({ ...l, blocks: -1, // NOTE: @trezor/connect returns -1 for ethereum default - label: 'normal' as const, + label: l.label || ('normal' as const), })), }; } diff --git a/suite-common/wallet-core/src/send/sendFormEthereumThunks.ts b/suite-common/wallet-core/src/send/sendFormEthereumThunks.ts index 6acc6b9aa7d..dca5aeebb5d 100644 --- a/suite-common/wallet-core/src/send/sendFormEthereumThunks.ts +++ b/suite-common/wallet-core/src/send/sendFormEthereumThunks.ts @@ -1,4 +1,4 @@ -import { fromWei, toWei } from 'web3-utils'; +import { toWei } from 'web3-utils'; import { createThunk } from '@suite-common/redux-utils'; import { notificationsActions } from '@suite-common/toast-notifications'; @@ -11,16 +11,10 @@ import { import { Account, AddressDisplayOptions, - ExternalOutput, PrecomposedLevels, - PrecomposedTransaction, RbfTransactionParams, } from '@suite-common/wallet-types'; import { - amountToSmallestUnit, - calculateEthFee, - calculateMax, - calculateTotal, formatAmount, getAccountIdentity, getEthereumEstimateFeeParams, @@ -30,10 +24,11 @@ import { isPending, prepareEthereumTransaction, } from '@suite-common/wallet-utils'; -import TrezorConnect, { FeeLevel, TokenInfo } from '@trezor/connect'; +import TrezorConnect, { FeeLevel } from '@trezor/connect'; import { BigNumber } from '@trezor/utils/src/bigNumber'; import { SEND_MODULE_PREFIX } from './sendFormConstants'; +import { calculateEvmTxWithFees } from './sendFormEthereumUtils'; import { ComposeFeeLevelsError, ComposeTransactionThunkArguments, @@ -41,95 +36,6 @@ import { SignTransactionThunkArguments, } from './sendFormTypes'; import { selectTransactions } from '../transactions/transactionsReducer'; - -const calculate = ( - availableBalance: string, - output: ExternalOutput, - feeLevel: FeeLevel, - token?: TokenInfo, -): PrecomposedTransaction => { - let amount: string; - let max: string | undefined; - const feeInGwei = calculateEthFee(toWei(feeLevel.feePerUnit, 'gwei'), feeLevel.feeLimit || '0'); - - const availableTokenBalance = token - ? amountToSmallestUnit(token.balance!, token.decimals) - : undefined; - if (output.type === 'send-max' || output.type === 'send-max-noaddress') { - max = availableTokenBalance || calculateMax(availableBalance, feeInGwei); - amount = max; - } else { - amount = output.amount; - } - - // total ETH spent (amount + fee), in ERC20 only fee - const totalSpent = new BigNumber(calculateTotal(token ? '0' : amount, feeInGwei)); - - if (totalSpent.isGreaterThan(availableBalance)) { - if (token) { - return { - type: 'error', - error: 'AMOUNT_NOT_ENOUGH_CURRENCY_FEE_WITH_ETH_AMOUNT', - errorMessage: { - id: 'AMOUNT_NOT_ENOUGH_CURRENCY_FEE_WITH_ETH_AMOUNT', - values: { - feeAmount: fromWei(feeInGwei, 'ether').toString(), - }, - }, - } as const; - } - - return { - type: 'error', - error: 'AMOUNT_IS_NOT_ENOUGH', - errorMessage: { id: 'AMOUNT_IS_NOT_ENOUGH' }, - } as const; - } - - // validate if token balance is not 0 or lower than amount - if ( - availableTokenBalance && - (availableTokenBalance === '0' || new BigNumber(amount).gt(availableTokenBalance)) - ) { - return { - type: 'error', - error: 'AMOUNT_IS_NOT_ENOUGH', - errorMessage: { id: 'AMOUNT_IS_NOT_ENOUGH' }, - } as const; - } - - const payloadData = { - type: 'nonfinal' as const, - totalSpent: token ? amount : totalSpent.toString(), - max, - fee: feeInGwei, - feePerByte: feeLevel.feePerUnit, - feeLimit: feeLevel.feeLimit, - token, - bytes: 0, // TODO: calculate - inputs: [], - }; - - if (output.type === 'send-max' || output.type === 'payment') { - return { - ...payloadData, - type: 'final', - // compatibility with BTC PrecomposedTransaction from @trezor/connect - inputs: [], - outputsPermutation: [0], - outputs: [ - { - address: output.address, - amount, - script_type: 'PAYTOADDRESS', - }, - ], - }; - } - - return payloadData; -}; - export const composeEthereumTransactionFeeLevelsThunk = createThunk< PrecomposedLevels, ComposeTransactionThunkArguments, @@ -164,6 +70,7 @@ export const composeEthereumTransactionFeeLevelsThunk = createThunk< coin: account.symbol, identity: getAccountIdentity(account), request: { + feeLevels: 'smart', blocks: [2], specific: { from: account.descriptor, @@ -212,10 +119,19 @@ export const composeEthereumTransactionFeeLevelsThunk = createThunk< } // in case when selectedFee is set to 'custom' construct this FeeLevel from values if (formState.selectedFee === 'custom') { + const { customMaxPriorityFeePerGas, effectiveGasPrice, feePerUnit, feeLimit } = + formState; + + const customMaxPriorityFeePerGasWei = customMaxPriorityFeePerGas + ? BigNumber(toWei(Number(customMaxPriorityFeePerGas), 'gwei')) + : undefined; + predefinedLevels.push({ label: 'custom', - feePerUnit: formState.feePerUnit, - feeLimit: formState.feeLimit, + feePerUnit, + feeLimit, + effectiveGasPrice, + maxPriorityFeePerGas: customMaxPriorityFeePerGasWei?.toString(), blocks: -1, }); } @@ -223,7 +139,7 @@ export const composeEthereumTransactionFeeLevelsThunk = createThunk< // wrap response into PrecomposedLevels object where key is a FeeLevel label const resultLevels: PrecomposedLevels = {}; const response = predefinedLevels.map(level => - calculate(availableBalance, output, level, tokenInfo), + calculateEvmTxWithFees({ availableBalance, output, feeLevel: level, token: tokenInfo }), ); response.forEach((tx, index) => { const feeLabel = predefinedLevels[index].label as FeeLevel['label']; @@ -331,7 +247,12 @@ export const signEthereumSendFormTransactionThunk = createThunk< amount: formState.outputs[0].amount, data: formState.ethereumDataHex, gasLimit: precomposedTransaction.feeLimit || '', - gasPrice: precomposedTransaction.feePerByte, + maxFeePerGas: precomposedTransaction.maxFeePerGas ?? undefined, + maxPriorityFeePerGas: precomposedTransaction.maxPriorityFeePerGas ?? undefined, + gasPrice: + !precomposedTransaction.maxFeePerGas && precomposedTransaction.feePerByte + ? precomposedTransaction.feePerByte + : undefined, nonce, }); diff --git a/suite-common/wallet-core/src/send/sendFormEthereumUtils.ts b/suite-common/wallet-core/src/send/sendFormEthereumUtils.ts new file mode 100644 index 00000000000..45641301dbe --- /dev/null +++ b/suite-common/wallet-core/src/send/sendFormEthereumUtils.ts @@ -0,0 +1,139 @@ +import { fromWei, toWei } from 'web3-utils'; + +import { ExternalOutput, PrecomposedTransaction } from '@suite-common/wallet-types'; +import { + amountToSmallestUnit, + calculateMax, + calculateMaxEthFee, + calculateTotal, +} from '@suite-common/wallet-utils'; +import { FeeLevel, TokenInfo } from '@trezor/connect'; +import { BigNumber } from '@trezor/utils'; + +export type CalculateEvmTxWithFeesProps = { + availableBalance: string; + output: ExternalOutput; + feeLevel: FeeLevel; + token?: TokenInfo; +}; + +/** + * Calculate the effective gas price from maxPriorityFeePerGas and maxFeePerGas + * @param maxPriorityFeePerGasGwei - maxPriorityFeePerGas in gwei + * @param maxFeePerGasGwei - maxFeePerGas in gwei + * @returns effective gas price in wei + */ +export const calculateEffectiveGasPrice = ( + maxPriorityFeePerGasGwei?: string, + maxFeePerGasGwei?: string, +) => { + if (!maxPriorityFeePerGasGwei || !maxFeePerGasGwei) { + return undefined; + } + const baseFee = BigNumber(toWei(maxFeePerGasGwei, 'gwei')); + const priorityFee = BigNumber(toWei(maxPriorityFeePerGasGwei, 'gwei')); + + return baseFee.plus(priorityFee).toString(); +}; + +export const calculateEvmTxWithFees = ({ + availableBalance, + output, + feeLevel, + token, +}: CalculateEvmTxWithFeesProps): PrecomposedTransaction => { + let amount: string; + let max: string | undefined; + + const eip1559 = feeLevel.maxPriorityFeePerGas + ? { + maxPriorityFeePerGas: feeLevel.maxPriorityFeePerGas, + maxFeePerGas: feeLevel.maxFeePerGas, + effectiveGasPrice: feeLevel.effectiveGasPrice, + } + : undefined; + + const feeInWei = eip1559 + ? calculateMaxEthFee(eip1559.effectiveGasPrice, feeLevel.feeLimit) + : calculateMaxEthFee(toWei(feeLevel.feePerUnit || '0', 'gwei'), feeLevel.feeLimit); + + const availableTokenBalance = token + ? amountToSmallestUnit(token.balance!, token.decimals) + : undefined; + + if (output.type === 'send-max' || output.type === 'send-max-noaddress') { + max = availableTokenBalance || calculateMax(availableBalance, feeInWei); + amount = max; + } else { + amount = output.amount; + } + + // total ETH spent (amount + fee), in ERC20 only fee + const totalSpent = new BigNumber(calculateTotal(token ? '0' : amount, feeInWei)); + + if (totalSpent.isGreaterThan(availableBalance)) { + if (token) { + return { + type: 'error', + error: 'AMOUNT_NOT_ENOUGH_CURRENCY_FEE_WITH_ETH_AMOUNT', + errorMessage: { + id: 'AMOUNT_NOT_ENOUGH_CURRENCY_FEE_WITH_ETH_AMOUNT', + values: { + feeAmount: fromWei(feeInWei, 'ether').toString(), + }, + }, + } as const; + } + + return { + type: 'error', + error: 'AMOUNT_IS_NOT_ENOUGH', + errorMessage: { id: 'AMOUNT_IS_NOT_ENOUGH' }, + } as const; + } + + // validate if token balance is not 0 or lower than amount + if ( + availableTokenBalance && + (availableTokenBalance === '0' || new BigNumber(amount).gt(availableTokenBalance)) + ) { + return { + type: 'error', + error: 'AMOUNT_IS_NOT_ENOUGH', + errorMessage: { id: 'AMOUNT_IS_NOT_ENOUGH' }, + } as const; + } + + const payloadData = { + type: 'nonfinal' as const, + totalSpent: token ? amount : totalSpent.toString(), + max, + fee: feeInWei, + maxFeePerGas: feeLevel.effectiveGasPrice, + maxPriorityFeePerGas: feeLevel.maxPriorityFeePerGas, + feePerByte: feeLevel.feePerUnit, + feeLimit: feeLevel.feeLimit, + token, + bytes: 0, // TODO: calculate + inputs: [], + }; + + if (output.type === 'send-max' || output.type === 'payment') { + return { + ...payloadData, + type: 'final', + // compatibility with BTC PrecomposedTransaction from @trezor/connect + inputs: [], + outputsPermutation: [0], + outputs: [ + { + address: output.address, + amount, + script_type: 'PAYTOADDRESS', + }, + ], + }; + } + + return payloadData; +}; diff --git a/suite-common/wallet-core/tests/send/sendFormEthereumUtils.fixtures.ts b/suite-common/wallet-core/tests/send/sendFormEthereumUtils.fixtures.ts new file mode 100644 index 00000000000..2ae35d375f3 --- /dev/null +++ b/suite-common/wallet-core/tests/send/sendFormEthereumUtils.fixtures.ts @@ -0,0 +1,267 @@ +export const calculateEvmTxWithFees = [ + { + description: 'Legacy fee, no token, tx should pass', + input: { + availableBalance: '1000000000000000000', + output: { type: 'payment' as const, address: 'A', amount: '1' }, + feeLevel: { label: 'normal' as const, feePerUnit: '10', feeLimit: '21000', blocks: -1 }, + token: undefined, + }, + result: { + max: undefined, + bytes: 0, + fee: '210000000000000', + feeLimit: '21000', + feePerByte: '10', + inputs: [], + maxFeePerGas: undefined, + maxPriorityFeePerGas: undefined, + outputs: [ + { + address: 'A', + amount: '1', + script_type: 'PAYTOADDRESS', + }, + ], + outputsPermutation: [0], + token: undefined, + totalSpent: '210000000000001', + type: 'final', + }, + }, + + { + description: 'Legacy fee, no token, balance is not enough', + input: { + availableBalance: '1000', + output: { type: 'payment' as const, address: 'A', amount: '1' }, + feeLevel: { label: 'normal' as const, feePerUnit: '10', feeLimit: '21000', blocks: -1 }, + token: undefined, + }, + result: { + type: 'error', + error: 'AMOUNT_IS_NOT_ENOUGH', + errorMessage: { + id: 'AMOUNT_IS_NOT_ENOUGH', + }, + }, + }, + { + description: 'Legacy fee, token transfer, tx should pass', + input: { + availableBalance: '1000000000000000000', + output: { type: 'payment' as const, address: 'A', amount: '1' }, + feeLevel: { label: 'normal' as const, feePerUnit: '10', feeLimit: '21000', blocks: -1 }, + token: { + type: 'ERC20', + standard: 'ERC20', + contract: '0x123', + symbol: 'test', + decimals: 18, + balance: '100', + }, + }, + result: { + max: undefined, + bytes: 0, + fee: '210000000000000', + feeLimit: '21000', + feePerByte: '10', + inputs: [], + maxFeePerGas: undefined, + maxPriorityFeePerGas: undefined, + outputs: [ + { + address: 'A', + amount: '1', + script_type: 'PAYTOADDRESS', + }, + ], + outputsPermutation: [0], + token: { + type: 'ERC20', + standard: 'ERC20', + contract: '0x123', + symbol: 'test', + decimals: 18, + balance: '100', + }, + totalSpent: '1', + type: 'final', + }, + }, + + { + description: 'Legacy fee, token transfer, balance is not enough', + input: { + availableBalance: '100000', + output: { type: 'payment' as const, address: 'A', amount: '101' }, + feeLevel: { label: 'normal' as const, feePerUnit: '10', feeLimit: '21000', blocks: -1 }, + token: { + type: 'ERC20', + standard: 'ERC20', + contract: '0x123', + symbol: 'test', + decimals: 18, + balance: '1', + }, + }, + result: { + type: 'error', + error: 'AMOUNT_NOT_ENOUGH_CURRENCY_FEE_WITH_ETH_AMOUNT', + errorMessage: { + id: 'AMOUNT_NOT_ENOUGH_CURRENCY_FEE_WITH_ETH_AMOUNT', + values: { feeAmount: '0.00021' }, + }, + }, + }, + { + description: 'Eip1559 fee, no token, tx should pass', + input: { + availableBalance: '1000000000000000000', + output: { type: 'payment' as const, address: 'A', amount: '1' }, + feeLevel: { + label: 'low' as const, + feeLimit: '21000', + feePerUnit: '0', + maxFeePerGas: '1191416838', + effectiveGasPrice: '1191416838', + maxPriorityFeePerGas: '7946902', + blocks: -1, + }, + token: undefined, + }, + result: { + max: undefined, + bytes: 0, + fee: '25019753598000', + feeLimit: '21000', + feePerByte: '0', + inputs: [], + maxFeePerGas: '1191416838', + maxPriorityFeePerGas: '7946902', + outputs: [ + { + address: 'A', + amount: '1', + script_type: 'PAYTOADDRESS', + }, + ], + outputsPermutation: [0], + token: undefined, + totalSpent: '25019753598001', + type: 'final', + }, + }, + + { + description: 'Eip1559 fee, no token, balance is not enough', + input: { + availableBalance: '1000', + output: { type: 'payment' as const, address: 'A', amount: '1' }, + feeLevel: { + label: 'low' as const, + feeLimit: '21000', + feePerUnit: '0', + maxFeePerGas: '1191416838', + effectiveGasPrice: '1191416838', + maxPriorityFeePerGas: '7946902', + blocks: -1, + }, + token: undefined, + }, + result: { + type: 'error', + error: 'AMOUNT_IS_NOT_ENOUGH', + errorMessage: { + id: 'AMOUNT_IS_NOT_ENOUGH', + }, + }, + }, + + { + description: 'Eip1559 fee, token transfer, should pass', + input: { + availableBalance: '1000000000000000000', + output: { type: 'payment' as const, address: 'A', amount: '1' }, + feeLevel: { + label: 'low' as const, + feeLimit: '21000', + feePerUnit: '0', + maxFeePerGas: '1191416838', + effectiveGasPrice: '1191416838', + maxPriorityFeePerGas: '7946902', + blocks: -1, + }, + token: { + type: 'ERC20', + standard: 'ERC20', + contract: '0x123', + symbol: 'test', + decimals: 18, + balance: '100', + }, + }, + result: { + max: undefined, + bytes: 0, + fee: '25019753598000', + feeLimit: '21000', + feePerByte: '0', + inputs: [], + maxFeePerGas: '1191416838', + maxPriorityFeePerGas: '7946902', + outputs: [ + { + address: 'A', + amount: '1', + script_type: 'PAYTOADDRESS', + }, + ], + outputsPermutation: [0], + token: { + type: 'ERC20', + standard: 'ERC20', + contract: '0x123', + symbol: 'test', + decimals: 18, + balance: '100', + }, + totalSpent: '1', + type: 'final', + }, + }, + + { + description: 'Eip1559 fee, token transfer, balance is not enough', + input: { + availableBalance: '1000', + output: { type: 'payment' as const, address: 'A', amount: '1' }, + feeLevel: { + label: 'low' as const, + feeLimit: '21000', + feePerUnit: '0', + maxFeePerGas: '1191416838', + effectiveGasPrice: '1191416838', + maxPriorityFeePerGas: '7946902', + blocks: -1, + }, + token: { + type: 'ERC20', + standard: 'ERC20', + contract: '0x123', + symbol: 'test', + decimals: 18, + balance: '100', + }, + }, + result: { + error: 'AMOUNT_NOT_ENOUGH_CURRENCY_FEE_WITH_ETH_AMOUNT', + errorMessage: { + id: 'AMOUNT_NOT_ENOUGH_CURRENCY_FEE_WITH_ETH_AMOUNT', + values: { feeAmount: '0.000025019753598' }, + }, + type: 'error', + }, + }, +]; diff --git a/suite-common/wallet-core/tests/send/sendFormEthereumUtils.test.ts b/suite-common/wallet-core/tests/send/sendFormEthereumUtils.test.ts new file mode 100644 index 00000000000..8abf46e8e3f --- /dev/null +++ b/suite-common/wallet-core/tests/send/sendFormEthereumUtils.test.ts @@ -0,0 +1,12 @@ +import * as fixtures from './sendFormEthereumUtils.fixtures'; +import { calculateEvmTxWithFees } from '../../src/send/sendFormEthereumUtils'; + +describe(calculateEvmTxWithFees.name, () => { + fixtures.calculateEvmTxWithFees.forEach(f => { + it(`${f.description}`, () => { + const result = calculateEvmTxWithFees(f.input); + + expect(result).toEqual(f.result); + }); + }); +}); diff --git a/suite-common/wallet-types/src/sendForm.ts b/suite-common/wallet-types/src/sendForm.ts index c351911eeb4..e790a9dd9f2 100644 --- a/suite-common/wallet-types/src/sendForm.ts +++ b/suite-common/wallet-types/src/sendForm.ts @@ -17,6 +17,9 @@ export interface FormState { setMaxOutputId?: number; selectedFee?: FeeLevel['label']; feePerUnit: string; // bitcoin/ethereum/ripple custom fee field (satB/gasPrice/drops) + maxPriorityFeePerGas?: string; // ethereum eip1559 only + maxFeePerGas?: string; // ethereum eip1559 only + baseFeePerGas?: string; // ethereum eip1559 only feeLimit: string; // ethereum only (gasLimit) estimatedFeeLimit?: string; // ethereum only (gasLimit) @@ -40,6 +43,9 @@ export interface FormState { isCoinControlEnabled: boolean; hasCoinControlBeenOpened: boolean; anonymityWarningChecked?: boolean; + customMaxBaseFeePerGas?: string; // ethereum eip1559 only custom + customMaxPriorityFeePerGas?: string; // ethereum eip1559 only custom + effectiveGasPrice?: string; // ethereum eip1559 only selectedUtxos: AccountUtxo[]; utxoSorting?: UtxoSorting; isTrading?: boolean; diff --git a/suite-common/wallet-types/src/transaction.ts b/suite-common/wallet-types/src/transaction.ts index 315f85c9796..fafd1f03519 100644 --- a/suite-common/wallet-types/src/transaction.ts +++ b/suite-common/wallet-types/src/transaction.ts @@ -77,8 +77,10 @@ export type EthTransactionData = { amount: string; data?: string; gasLimit: string; - gasPrice: string; nonce: string; + gasPrice?: string; // this field is not used for EIP1559 transactions (See EthereumTransaction and EthereumTransactionEIP1559 types in connect) + maxFeePerGas?: string; + maxPriorityFeePerGas?: string; }; export type ExternalOutput = Exclude; @@ -107,6 +109,9 @@ type PrecomposedTransactionBase = PrecomposedTransactionConnectResponseFinal & { max?: string; feeLimit?: string; estimatedFeeLimit?: string; + maxFeePerGas?: string; + maxPriorityFeePerGas?: string; + effectiveGasPrice?: string; token?: TokenInfo; isTokenKnown?: boolean; createdTimestamp?: number; diff --git a/suite-common/wallet-utils/src/__fixtures__/sendFormUtils.ts b/suite-common/wallet-utils/src/__fixtures__/sendFormUtils.ts index 492936e887f..96bd906aaa9 100644 --- a/suite-common/wallet-utils/src/__fixtures__/sendFormUtils.ts +++ b/suite-common/wallet-utils/src/__fixtures__/sendFormUtils.ts @@ -50,6 +50,66 @@ export const prepareEthereumTransaction = [ data: '0xa9059cbb000000000000000000000000A6ABB480640d6D27D2FB314196D94463ceDcB31e0000000000000000000000000000000000000000000000000011c37937e08000', }, }, + + { + description: 'regular with eip1559 fees', + txInfo: { + to: '0x1f815D67006163E502b8eD4947C91ad0A62De24e', + amount: '1', + chainId: 1, + nonce: '2', + gasLimit: '21000', + gasPrice: '1', + data: 'deadbeef', + maxFeePerGas: '1', + maxPriorityFeePerGas: '1', + }, + result: { + to: '0x1f815D67006163E502b8eD4947C91ad0A62De24e', + value: '0xde0b6b3a7640000', + chainId: 1, + nonce: '0x2', + gasLimit: '0x5208', + gasPrice: undefined, + data: '0xdeadbeef', + maxFeePerGas: '0x1', + maxPriorityFeePerGas: '0x1', + }, + }, + + { + description: 'erc20 with eip1559 fees', + txInfo: { + token: { + type: 'ERC20', + standard: 'ERC20', + symbol: 'gnt', + decimals: 18, + contract: '0xa74476443119A942dE498590Fe1f2454d7D4aC0d', + name: 'GNT', + }, + to: '0xA6ABB480640d6D27D2FB314196D94463ceDcB31e', + amount: '0.005', + chainId: 1, + nonce: '11', + gasLimit: '200000', + gasPrice: '5', + data: 'deadbeef-not-used', + maxFeePerGas: '1', + maxPriorityFeePerGas: '1', + }, + result: { + to: '0xa74476443119A942dE498590Fe1f2454d7D4aC0d', + value: '0x00', + chainId: 1, + nonce: '0xb', + gasLimit: '0x30d40', + gasPrice: undefined, + data: '0xa9059cbb000000000000000000000000A6ABB480640d6D27D2FB314196D94463ceDcB31e0000000000000000000000000000000000000000000000000011c37937e08000', + maxFeePerGas: '0x1', + maxPriorityFeePerGas: '0x1', + }, + }, ]; export const restoreOrigOutputsOrder = [ diff --git a/suite-common/wallet-utils/src/__tests__/accountUtils.test.ts b/suite-common/wallet-utils/src/__tests__/accountUtils.test.ts index 487a444a6e4..4fa770cbc5b 100644 --- a/suite-common/wallet-utils/src/__tests__/accountUtils.test.ts +++ b/suite-common/wallet-utils/src/__tests__/accountUtils.test.ts @@ -236,6 +236,7 @@ describe('account utils', () => { 'coin-definitions', 'nft-definitions', 'staking', + 'eip1559', ]); expect(getNetworkAccountFeatures(coinjoinAcc)).toEqual(['rbf', 'amount-unit']); // when account does not have features defined, take them from root network object diff --git a/suite-common/wallet-utils/src/__tests__/sendFormUtils.test.ts b/suite-common/wallet-utils/src/__tests__/sendFormUtils.test.ts index 40088babd61..52b460630e1 100644 --- a/suite-common/wallet-utils/src/__tests__/sendFormUtils.test.ts +++ b/suite-common/wallet-utils/src/__tests__/sendFormUtils.test.ts @@ -4,8 +4,8 @@ import { networks } from '@suite-common/wallet-config'; import * as fixtures from '../__fixtures__/sendFormUtils'; import { getUtxoOutpoint } from '../accountUtils'; import { - calculateEthFee, calculateMax, + calculateMaxEthFee, calculateTotal, findComposeErrors, getBitcoinComposeOutputs, @@ -358,18 +358,18 @@ describe('sendForm utils', () => { }); }); - it('calculateEthFee', () => { - expect(calculateEthFee()).toEqual('0'); - expect(calculateEthFee('', '')).toEqual('0'); - expect(calculateEthFee('1', '')).toEqual('0'); - expect(calculateEthFee('0', '1')).toEqual('0'); + it('calculateMaxEthFee', () => { + expect(calculateMaxEthFee()).toEqual('0'); + expect(calculateMaxEthFee('', '')).toEqual('0'); + expect(calculateMaxEthFee('1', '')).toEqual('0'); + expect(calculateMaxEthFee('0', '1')).toEqual('0'); // @ts-expect-error invalid params - expect(calculateEthFee({}, {})).toEqual('0'); + expect(calculateMaxEthFee({}, {})).toEqual('0'); // @ts-expect-error invalid params - expect(calculateEthFee(() => {}, {})).toEqual('0'); + expect(calculateMaxEthFee(() => {}, {})).toEqual('0'); // @ts-expect-error invalid params - expect(calculateEthFee(null, true)).toEqual('0'); - expect(calculateEthFee('1', '2')).toEqual('2'); + expect(calculateMaxEthFee(null, true)).toEqual('0'); + expect(calculateMaxEthFee('1', '2')).toEqual('2'); }); it('getExcludedUtxos', () => { diff --git a/suite-common/wallet-utils/src/sendFormUtils.ts b/suite-common/wallet-utils/src/sendFormUtils.ts index 37f451bc95e..bc1328d6f7d 100644 --- a/suite-common/wallet-utils/src/sendFormUtils.ts +++ b/suite-common/wallet-utils/src/sendFormUtils.ts @@ -28,11 +28,18 @@ import type { FormState, GeneralPrecomposedTransactionFinal, Output, + PrecomposedTransactionFinal, RbfTransactionParams, SendFormDraftKey, TokenAddress, } from '@suite-common/wallet-types'; -import { ComposeOutput, EthereumTransaction, PROTO, TokenInfo } from '@trezor/connect'; +import { + ComposeOutput, + EthereumTransaction, + EthereumTransactionEIP1559, + PROTO, + TokenInfo, +} from '@trezor/connect'; import { BigNumber } from '@trezor/utils/src/bigNumber'; import { amountToSmallestUnit, getUtxoOutpoint, networkAmountToSmallestUnit } from './accountUtils'; @@ -81,7 +88,7 @@ export const calculateMax = (availableBalance: string, fee: string): string => { * @param {string} [gasLimit] - The gas limit. * @returns {string} The calculated fee in wei, or '0' if inputs are invalid. */ -export const calculateEthFee = (gasPrice?: string, gasLimit?: string): string => { +export const calculateMaxEthFee = (gasPrice?: string, gasLimit?: string): string => { if (!gasPrice || !gasLimit) { return '0'; } @@ -139,15 +146,35 @@ export const getEthereumEstimateFeeParams = ( }; }; -export const prepareEthereumTransaction = (txInfo: EthTransactionData) => { - const result: EthereumTransaction = { - to: txInfo.to, - value: getSerializedAmount(txInfo.amount), - chainId: txInfo.chainId, - nonce: numberToHex(txInfo.nonce), - gasLimit: numberToHex(txInfo.gasLimit), - gasPrice: numberToHex(toWei(txInfo.gasPrice, 'gwei')), - }; +export const prepareEthereumTransaction = ( + txInfo: EthTransactionData, +): EthereumTransaction | EthereumTransactionEIP1559 => { + let result: EthereumTransaction | EthereumTransactionEIP1559; + if (txInfo.maxFeePerGas && txInfo.maxPriorityFeePerGas) { + result = { + to: txInfo.to, + value: getSerializedAmount(txInfo.amount), + chainId: txInfo.chainId, + nonce: numberToHex(txInfo.nonce), + gasLimit: numberToHex(txInfo.gasLimit), + gasPrice: undefined, //Not sure if this is a good way to handle this + maxFeePerGas: numberToHex(txInfo.maxFeePerGas), + maxPriorityFeePerGas: numberToHex(txInfo.maxPriorityFeePerGas), + } as EthereumTransactionEIP1559; + } else if (txInfo.gasPrice) { + result = { + to: txInfo.to, + value: getSerializedAmount(txInfo.amount), + chainId: txInfo.chainId, + nonce: numberToHex(txInfo.nonce), + gasLimit: numberToHex(txInfo.gasLimit), + gasPrice: numberToHex(toWei(txInfo.gasPrice, 'gwei')), //Not sure if this is a good way to handle this + maxFeePerGas: undefined, + maxPriorityFeePerGas: undefined, + } as EthereumTransaction; + } else { + throw new Error('No gas price or maxFeePerGas and maxPriorityFeePerGas provided'); + } if (!txInfo.token && txInfo.data) { result.data = sanitizeHex(txInfo.data); @@ -177,11 +204,24 @@ const getFeeLevels = ({ feeInfo, networkType }: GetFeeInfoProps) => { feePerUnit: '0', blocks: -1, }); + const isEthereum = networkType === 'ethereum'; + const isEip1559 = feeInfo.levels.some(level => level.maxPriorityFeePerGas); + + if (isEthereum) { + if (isEip1559) { + return levels.map(level => ({ + ...level, + feePerUnit: level.feePerUnit, + maxFeePerGas: level.maxFeePerGas, + maxPriorityFeePerGas: level.maxPriorityFeePerGas, + effectiveGasPrice: level.effectiveGasPrice, + feeLimit: level.feeLimit, + })); + } - if (networkType === 'ethereum') { // convert wei to gwei return levels.map(level => { - const gwei = new BigNumber(fromWei(level.feePerUnit, 'gwei')); + const gwei = new BigNumber(fromWei(level.feePerUnit || '0', 'gwei')); // blockbook/geth may return 0 in feePerUnit. if this happens set at least minFee const feePerUnit = level.label !== 'custom' && gwei.lt(feeInfo.minFee) @@ -228,8 +268,19 @@ const mapNetworkTypeToFeeUnits: Record = { export const getFeeUnits = (networkType: NetworkType) => mapNetworkTypeToFeeUnits[networkType]; -export const getFee = (networkType: NetworkType, tx: GeneralPrecomposedTransactionFinal) => { - if (networkType === 'solana') return tx.fee; +type GetFeeProps = { + account: Account; + tx: GeneralPrecomposedTransactionFinal; + baseFee?: string; + shouldUsePriorityFees?: boolean; +}; + +export const getFee = ({ account, tx, shouldUsePriorityFees }: GetFeeProps) => { + if (account.networkType === 'solana') return tx.fee; + + if (account.networkType === 'ethereum' && shouldUsePriorityFees) { + return fromWei((tx as PrecomposedTransactionFinal).maxFeePerGas || '0', 'gwei'); + } return tx.feePerByte; };