From 5ee6a6eb5c755faad33b9581c518e68af6edf52f Mon Sep 17 00:00:00 2001 From: Albina Nikiforova Date: Tue, 11 Feb 2025 11:44:32 +0100 Subject: [PATCH] chore(suite): wip --- .../blockchain-link-types/src/responses.ts | 21 ++++++++ packages/connect/src/api/bitcoin/Fees.ts | 50 ++++++++++++++----- packages/connect/src/types/fees.ts | 15 ++++++ .../suite/src/hooks/wallet/form/useCompose.ts | 2 +- .../src/hooks/wallet/useSendFormCompose.ts | 23 ++++++++- packages/suite/src/support/messages.ts | 34 +++++++++++++ submodules/trezor-common | 2 +- suite-common/wallet-config/src/types.ts | 8 ++- .../src/blockchain/blockchainThunks.ts | 9 ++-- .../src/send/sendFormEthereumThunks.ts | 5 +- .../wallet-core/src/send/sendFormThunks.ts | 5 +- .../wallet-core/src/send/sendFormTypes.ts | 1 + suite-common/wallet-types/src/transaction.ts | 4 +- .../wallet-utils/src/sendFormUtils.ts | 48 ++++++++++++++---- suite-native/module-send/src/types.ts | 2 +- 15 files changed, 191 insertions(+), 38 deletions(-) diff --git a/packages/blockchain-link-types/src/responses.ts b/packages/blockchain-link-types/src/responses.ts index c440fdd60c7..5260f3c8dee 100644 --- a/packages/blockchain-link-types/src/responses.ts +++ b/packages/blockchain-link-types/src/responses.ts @@ -92,12 +92,33 @@ export interface GetFiatRatesTickersList { }; } +export interface PriorityFeeEstimationDetails { + maxFeePerGas: string; + maxPriorityFeePerGas: string; + minWaitTimeEstimate: number; + maxWaitTimeEstimate: number; +} + +export type FeeTrend = 'up' | 'down'; + export interface EstimateFee { type: typeof RESPONSES.ESTIMATE_FEE; payload: { feePerUnit: string; feePerTx?: string; feeLimit?: string; + eip1559?: { + baseFeePerGas: string; + low: PriorityFeeEstimationDetails; + medium: PriorityFeeEstimationDetails; + high: PriorityFeeEstimationDetails; + networkCongestion: number; + latestPriorityFeeRange: string[]; + historicalPriorityFeeRange: string[]; + historicalBaseFeeRange: string[]; + priorityFeeTrend: FeeTrend; + baseFeeTrend: FeeTrend; + }; }[]; } diff --git a/packages/connect/src/api/bitcoin/Fees.ts b/packages/connect/src/api/bitcoin/Fees.ts index 1f41d03f33d..660cc3a357f 100644 --- a/packages/connect/src/api/bitcoin/Fees.ts +++ b/packages/connect/src/api/bitcoin/Fees.ts @@ -82,19 +82,43 @@ export class FeeLevels { async loadMisc(blockchain: Blockchain) { try { const [response] = await blockchain.estimateFee({ blocks: [1] }); - // misc coins should have only one FeeLevel (normal) - this.levels[0] = { - ...this.levels[0], - ...response, - // validate `feePerUnit` from the backend - // should be lower than `coinInfo.maxFee` and higher than `coinInfo.minFee` - // xrp sends values from 1 to very high number occasionally - // see: https://github.com/trezor/trezor-suite/blob/develop/packages/blockchain-link/src/workers/ripple/index.ts#L316 - feePerUnit: Math.min( - this.coinInfo.maxFee, - Math.max(this.coinInfo.minFee, parseInt(response.feePerUnit, 10)), - ).toString(), - }; + if (response.eip1559) { + const eip1559LevelKeys = ['low', 'medium', 'high'] as const; + + const { eip1559 } = response; + const eip1559Levels = eip1559LevelKeys.map(levelKey => { + const level = eip1559[levelKey]; + + return { + label: levelKey, + baseFeePerGas: eip1559.baseFeePerGas, + maxFeePerGas: level.maxFeePerGas, + maxPriorityFeePerGas: level.maxPriorityFeePerGas, + minWaitTimeEstimate: level.minWaitTimeEstimate / 1000, // Infura provides wait time in miliseconds + maxWaitTimeEstimate: level.maxWaitTimeEstimate / 1000, + feePerUnit: '0', + feeLimit: undefined, + ethFeeType: 'eip1559' as const, + blocks: -1, + }; + }); + + this.levels = [...eip1559Levels]; + } else { + //misc coins should have only one FeeLevel (normal), for ethereum depends on availability of eip1559 + this.levels[0] = { + ...this.levels[0], + ...response, + // validate `feePerUnit` from the backend + // should be lower than `coinInfo.maxFee` and higher than `coinInfo.minFee` + // xrp sends values from 1 to very high number occasionally + // see: https://github.com/trezor/trezor-suite/blob/develop/packages/blockchain-link/src/workers/ripple/index.ts#L316 + feePerUnit: Math.min( + this.coinInfo.maxFee, + Math.max(this.coinInfo.minFee, parseInt(response.feePerUnit, 10)), + ).toString(), + }; + } } catch { // silent } diff --git a/packages/connect/src/types/fees.ts b/packages/connect/src/types/fees.ts index 33d0d213845..96e9ec768d1 100644 --- a/packages/connect/src/types/fees.ts +++ b/packages/connect/src/types/fees.ts @@ -8,6 +8,14 @@ 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.Number(), + minWaitTimeEstimate: Type.Number(), +}); + export type FeeLevel = Static; export const FeeLevel = Type.Object({ label: Type.Union([ @@ -16,11 +24,18 @@ export const FeeLevel = Type.Object({ Type.Literal('economy'), Type.Literal('low'), Type.Literal('custom'), + Type.Literal('medium'), ]), + ethFeeType: Type.Optional(Type.Union([Type.Literal('legacy'), Type.Literal('eip1559')])), feePerUnit: Type.String(), blocks: Type.Number(), feeLimit: Type.Optional(Type.String()), // eth gas limit feePerTx: Type.Optional(Type.String()), // fee for BlockchainEstimateFeeParams.request.specific + baseFeePerGas: Type.Optional(Type.String()), + maxFeePerGas: Type.Optional(Type.String()), + maxPriorityFeePerGas: Type.Optional(Type.String()), + maxWaitTimeEstimate: Type.Optional(Type.Number()), + minWaitTimeEstimate: Type.Optional(Type.Number()), }); export type SelectFeeLevel = Static; diff --git a/packages/suite/src/hooks/wallet/form/useCompose.ts b/packages/suite/src/hooks/wallet/form/useCompose.ts index 2084d610185..f6812f922f0 100644 --- a/packages/suite/src/hooks/wallet/form/useCompose.ts +++ b/packages/suite/src/hooks/wallet/form/useCompose.ts @@ -186,7 +186,7 @@ export const useCompose = ({ const switchToNearestFee = useCallback( (composedLevels: NonNullable) => { const { selectedFee, setMaxOutputId } = getValues(); - let composed = composedLevels[selectedFee || 'normal']; + let composed = composedLevels[selectedFee || 'normal'] ?? composedLevels['medium']; // selectedFee was not set yet (no interaction with Fees) and default (normal) fee tx is not valid // OR setMax option was used diff --git a/packages/suite/src/hooks/wallet/useSendFormCompose.ts b/packages/suite/src/hooks/wallet/useSendFormCompose.ts index c307f71ba56..a780b13521d 100644 --- a/packages/suite/src/hooks/wallet/useSendFormCompose.ts +++ b/packages/suite/src/hooks/wallet/useSendFormCompose.ts @@ -56,6 +56,8 @@ export const useSendFormCompose = ({ const [composeField, setComposeField] = useState | undefined>(undefined); const [draftSaveRequest, setDraftSaveRequest] = useState(false); + const shouldUsePriorityFees = state.network.features.includes('eip1559'); + const dispatch = useDispatch(); const { translationString } = useTranslation(); @@ -79,6 +81,7 @@ export const useSendFormCompose = ({ feeInfo: state.feeInfo, excludedUtxos, prison, + shouldUsePriorityFees, }, }), ); @@ -90,7 +93,16 @@ export const useSendFormCompose = ({ setLoading(false); } }, - [account, dispatch, prison, excludedUtxos, setLoading, state.network, state.feeInfo], + [ + account, + dispatch, + prison, + excludedUtxos, + setLoading, + state.network, + state.feeInfo, + shouldUsePriorityFees, + ], ); // Create a compose request @@ -130,6 +142,7 @@ export const useSendFormCompose = ({ feeInfo: state.feeInfo, excludedUtxos, prison, + shouldUsePriorityFees, }, }), ); @@ -162,6 +175,7 @@ export const useSendFormCompose = ({ state.feeInfo, excludedUtxos, prison, + shouldUsePriorityFees, ], ); @@ -244,7 +258,12 @@ export const useSendFormCompose = ({ const values = getValues(); const { selectedFee, setMaxOutputId } = values; - let composed = composedLevels[selectedFee || 'normal']; + + let composed = + selectedFee && composedLevels[selectedFee] + ? composedLevels[selectedFee] + : composedLevels.medium || composedLevels.normal; + console.log('composed', composed, composedLevels, selectedFee); // selectedFee was not set yet (no interaction with Fees) and default (normal) fee tx is not valid // OR setMax option was used diff --git a/packages/suite/src/support/messages.ts b/packages/suite/src/support/messages.ts index 528f37221dc..3f00e64c061 100644 --- a/packages/suite/src/support/messages.ts +++ b/packages/suite/src/support/messages.ts @@ -3433,6 +3433,14 @@ export default defineMessages({ id: 'TR_GAS_PRICE', defaultMessage: 'Gas price', }, + TR_AVERAGE_FEE: { + id: 'TR_AVERAGE_FEE', + defaultMessage: 'Average fee', + }, + TR_AVERAGE_GAS_PRICE: { + id: 'TR_AVERAGE_GAS_PRICE', + defaultMessage: 'Average gas price', + }, TR_GAS_LIMIT: { id: 'TR_GAS_LIMIT', defaultMessage: 'Gas limit', @@ -5585,6 +5593,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', @@ -5606,6 +5618,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: 'Max priority fee per gas', + id: 'TR_MAX_PRIORITY_FEE_PER_GAS', + }, + TR_MAX_FEE_PER_GAS: { + defaultMessage: 'Max fee per gas', + 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.', @@ -8813,6 +8837,16 @@ export default defineMessages({ defaultMessage: 'Maximum fee is the network transaction fee that you’re willing to pay on the network to ensure your transaction gets processed.', }, + TR_EVM_MAX_FEE_DESC: { + id: 'TR_EVM_MAX_FEE_DESC', + defaultMessage: + 'Set how much you’re willing to pay to process your transaction on the blockchain. A higher maximum fee may speed up your transaction confirmation.', + }, + TR_TRANSACTION_FEE_DESC: { + id: 'TR_TRANSACTION_FEE_DESC', + defaultMessage: + 'The highest transaction fee you’re willing to pay to ensure your transaction gets processed on the blockchain. A higher fee can speed up confirmation times.', + }, TR_STAKE_MAX: { id: 'TR_STAKE_MAX', defaultMessage: 'Max', diff --git a/submodules/trezor-common b/submodules/trezor-common index e8792ecc265..739207fa93b 160000 --- a/submodules/trezor-common +++ b/submodules/trezor-common @@ -1 +1 @@ -Subproject commit e8792ecc2659d756798f38c85482238c8a9da942 +Subproject commit 739207fa93b09597c1cb815cd0669c46de2bbb52 diff --git a/suite-common/wallet-config/src/types.ts b/suite-common/wallet-config/src/types.ts index 3038ce36355..d1339cc7202 100644 --- a/suite-common/wallet-config/src/types.ts +++ b/suite-common/wallet-config/src/types.ts @@ -8,7 +8,12 @@ export type NetworkSymbol = | 'etc' | 'xrp' | 'bch' + | 'btg' + | 'dash' + | 'dgb' | 'doge' + | 'nmc' + | 'vtc' | 'zec' | 'ada' | 'sol' @@ -57,7 +62,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 810f7f41a27..708f18d78a4 100644 --- a/suite-common/wallet-core/src/blockchain/blockchainThunks.ts +++ b/suite-common/wallet-core/src/blockchain/blockchainThunks.ts @@ -58,7 +58,7 @@ const getAccountSyncInterval = (symbol: NetworkSymbol) => // sort FeeLevels in reversed order (Low > High) // TODO: consider to use same order in @trezor/connect to avoid double sorting -const order: FeeLevel['label'][] = ['low', 'economy', 'normal', 'high']; +const order: FeeLevel['label'][] = ['low', 'economy', 'medium', 'normal', 'high']; const sortLevels = (levels: FeeLevel[]) => levels.sort((levelA, levelB) => order.indexOf(levelA.label) - order.indexOf(levelB.label)); @@ -124,26 +124,25 @@ 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 result = await TrezorConnect.blockchainEstimateFee({ coin: network.symbol, request: { blocks: [2], + feeLevels: 'smart', specific: { from: '0x0000000000000000000000000000000000000000', to: '0x0000000000000000000000000000000000000000', }, }, }); + if (result.success) { newFeeInfo = { ...result.payload, 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 0209a619072..0c1a52e11a4 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) diff --git a/suite-common/wallet-core/src/send/sendFormThunks.ts b/suite-common/wallet-core/src/send/sendFormThunks.ts index 46d7584fd15..6d2723c2339 100644 --- a/suite-common/wallet-core/src/send/sendFormThunks.ts +++ b/suite-common/wallet-core/src/send/sendFormThunks.ts @@ -156,7 +156,10 @@ export const composeSendFormTransactionFeeLevelsThunk = createThunk< ); } else if (networkType === 'ethereum') { response = await dispatch( - composeEthereumTransactionFeeLevelsThunk({ formState, composeContext }), + composeEthereumTransactionFeeLevelsThunk({ + formState, + composeContext, + }), ); } else if (networkType === 'ripple') { response = await dispatch( diff --git a/suite-common/wallet-core/src/send/sendFormTypes.ts b/suite-common/wallet-core/src/send/sendFormTypes.ts index 5a8a856c36b..4e42764197e 100644 --- a/suite-common/wallet-core/src/send/sendFormTypes.ts +++ b/suite-common/wallet-core/src/send/sendFormTypes.ts @@ -19,6 +19,7 @@ export interface ComposeActionContext { feeInfo: FeeInfo; excludedUtxos?: ExcludedUtxos; prison?: Record; + shouldUsePriorityFees: boolean; } export type EthTransactionData = { diff --git a/suite-common/wallet-types/src/transaction.ts b/suite-common/wallet-types/src/transaction.ts index e1ae2bd78ca..44186ae45f7 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; diff --git a/suite-common/wallet-utils/src/sendFormUtils.ts b/suite-common/wallet-utils/src/sendFormUtils.ts index 397dad48701..516359f37d5 100644 --- a/suite-common/wallet-utils/src/sendFormUtils.ts +++ b/suite-common/wallet-utils/src/sendFormUtils.ts @@ -32,7 +32,13 @@ import type { 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'; @@ -139,15 +145,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 | undefined => { + 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), + }; + } 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, + }; + } else { + throw new Error('No gas price or maxFeePerGas and maxPriorityFeePerGas provided'); + } if (!txInfo.token && txInfo.data) { result.data = sanitizeHex(txInfo.data); @@ -181,7 +207,7 @@ const getFeeLevels = ({ feeInfo, networkType }: GetFeeInfoProps) => { 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) diff --git a/suite-native/module-send/src/types.ts b/suite-native/module-send/src/types.ts index b54ea0045a3..3be21509cb2 100644 --- a/suite-native/module-send/src/types.ts +++ b/suite-native/module-send/src/types.ts @@ -13,7 +13,7 @@ import { export type StatefulReviewOutput = ReviewOutput & { state: ReviewOutputState }; -export type NativeSupportedFeeLevel = Exclude; +export type NativeSupportedFeeLevel = Exclude; export type SendAmountInputProps = { recipientIndex: number;