Skip to content

Commit 9dfb6d1

Browse files
committed
feat(suite): fees in send form
1 parent 1d2ba76 commit 9dfb6d1

File tree

11 files changed

+647
-132
lines changed

11 files changed

+647
-132
lines changed

packages/suite/src/hooks/wallet/form/useCompose.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -204,9 +204,13 @@ export const useCompose = <TFieldValues extends FormState>({
204204
setValue('selectedFee', nearest);
205205
if (nearest === 'custom') {
206206
// @ts-expect-error: type = error already filtered above
207-
const { feePerByte, feeLimit } = composed;
207+
const { feePerByte, feeLimit, effectiveGasPrice, maxPriorityFeePerGas } =
208+
composed;
208209
setValue('feePerUnit', feePerByte);
209210
setValue('feeLimit', feeLimit || '');
211+
setValue('effectiveGasPrice', effectiveGasPrice || '');
212+
setValue('maxPriorityFeePerGas', maxPriorityFeePerGas || '');
213+
setValue('customMaxPriorityFeePerGas', maxPriorityFeePerGas || '');
210214
}
211215
}
212216
// or do nothing, use default composed tx

packages/suite/src/hooks/wallet/form/useFees.ts

+64-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { useEffect, useRef } from 'react';
22
import { FieldPath, UseFormReturn } from 'react-hook-form';
33

4+
import { calculateEffectiveGasPrice } from '@suite-common/wallet-core/src/send/sendFormEthereumUtils';
45
import {
56
FeeInfo,
67
FormState,
@@ -41,8 +42,11 @@ export const useFees = <TFieldValues extends FormState>({
4142
const selectedFeeRef = useRef(defaultValue);
4243
const feePerUnitRef = useRef<string | undefined>('');
4344
const feeLimitRef = useRef<string | undefined>('');
45+
const customMaxPriorityFeePerGasRef = useRef<string | undefined>('');
46+
const customMaxBaseFeePerGasRef = useRef<string | undefined>('');
4447
const estimatedFeeLimitRef = useRef<string | undefined>('');
4548
const saveLastUsedFeeRef = useRef(saveLastUsedFee);
49+
const effectiveGasPriceRef = useRef<string | undefined>('');
4650

4751
// Type assertion allowing to make the component reusable, see https://stackoverflow.com/a/73624072.
4852
const { clearErrors, getValues, register, setValue, watch } =
@@ -52,22 +56,35 @@ export const useFees = <TFieldValues extends FormState>({
5256
useEffect(() => {
5357
register('selectedFee', { shouldUnregister: true });
5458
register('estimatedFeeLimit', { shouldUnregister: true });
59+
register('effectiveGasPrice', { shouldUnregister: true });
5560
}, [register]);
5661

5762
// watch selectedFee change and update local references
5863
const selectedFee = watch('selectedFee');
5964
useEffect(() => {
6065
if (selectedFeeRef.current === selectedFee) return;
6166
selectedFeeRef.current = selectedFee;
62-
const { feePerUnit, feeLimit } = getValues();
67+
const {
68+
feePerUnit,
69+
feeLimit,
70+
customMaxPriorityFeePerGas,
71+
customMaxBaseFeePerGas,
72+
effectiveGasPrice, // is used here only to update it in the last used fee level
73+
} = getValues();
6374
feePerUnitRef.current = feePerUnit;
6475
feeLimitRef.current = feeLimit;
76+
customMaxPriorityFeePerGasRef.current = customMaxPriorityFeePerGas;
77+
customMaxBaseFeePerGasRef.current = customMaxBaseFeePerGas;
78+
effectiveGasPriceRef.current = effectiveGasPrice;
6579
}, [selectedFee, getValues]);
6680

6781
// watch custom feePerUnit/feeLimit inputs change
6882
const feePerUnit = watch('feePerUnit');
6983
const feeLimit = watch('feeLimit');
7084
const baseFee = watch('baseFee');
85+
const customMaxPriorityFeePerGas = watch('customMaxPriorityFeePerGas');
86+
const customMaxBaseFeePerGas = watch('customMaxBaseFeePerGas');
87+
const effectiveGasPrice = watch('effectiveGasPrice');
7188
useEffect(() => {
7289
if (selectedFeeRef.current !== 'custom') return;
7390

@@ -82,7 +99,32 @@ export const useFees = <TFieldValues extends FormState>({
8299
updateField = 'feeLimit';
83100
}
84101

85-
// compose
102+
if (
103+
customMaxBaseFeePerGas &&
104+
customMaxPriorityFeePerGas &&
105+
(customMaxPriorityFeePerGasRef.current !== customMaxPriorityFeePerGas ||
106+
customMaxBaseFeePerGasRef.current !== customMaxBaseFeePerGas)
107+
) {
108+
const effectiveGasPriceNew = calculateEffectiveGasPrice(
109+
customMaxPriorityFeePerGas,
110+
customMaxBaseFeePerGas,
111+
);
112+
setValue('effectiveGasPrice', effectiveGasPriceNew, { shouldValidate: true });
113+
effectiveGasPriceRef.current = effectiveGasPriceNew;
114+
updateField = 'effectiveGasPrice';
115+
}
116+
117+
if (customMaxBaseFeePerGasRef.current !== customMaxBaseFeePerGas) {
118+
customMaxBaseFeePerGasRef.current = customMaxBaseFeePerGas;
119+
updateField = 'customMaxBaseFeePerGas';
120+
}
121+
122+
if (customMaxPriorityFeePerGasRef.current !== customMaxPriorityFeePerGas) {
123+
customMaxPriorityFeePerGasRef.current = customMaxPriorityFeePerGas;
124+
updateField = 'customMaxPriorityFeePerGas';
125+
}
126+
127+
//compose
86128
if (updateField) {
87129
if (composeRequest) {
88130
composeRequest(updateField);
@@ -95,11 +137,29 @@ export const useFees = <TFieldValues extends FormState>({
95137
!errors.feeLimit
96138
) {
97139
dispatch(
98-
setLastUsedFeeLevel({ label: 'custom', feePerUnit, feeLimit, blocks: -1 }),
140+
setLastUsedFeeLevel({
141+
label: 'custom',
142+
feePerUnit,
143+
feeLimit,
144+
blocks: -1,
145+
customMaxPriorityFeePerGas,
146+
effectiveGasPrice,
147+
}),
99148
);
100149
}
101150
}
102-
}, [dispatch, feePerUnit, feeLimit, errors.feePerUnit, errors.feeLimit, composeRequest]);
151+
}, [
152+
dispatch,
153+
feePerUnit,
154+
effectiveGasPrice,
155+
feeLimit,
156+
customMaxPriorityFeePerGas,
157+
customMaxBaseFeePerGas,
158+
errors.feePerUnit,
159+
errors.feeLimit,
160+
composeRequest,
161+
setValue,
162+
]);
103163

104164
// watch estimatedFee change
105165
const estimatedFeeLimit = watch('estimatedFeeLimit');

packages/suite/src/hooks/wallet/useSendForm.ts

+4
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,10 @@ export const useSendForm = (props: UseSendFormProps): SendContextValues => {
126126
if (lastUsedFee.label === 'custom') {
127127
feeEnhancement.feePerUnit = lastUsedFee.feePerUnit;
128128
feeEnhancement.feeLimit = lastUsedFee.feeLimit;
129+
feeEnhancement.customMaxPriorityFeePerGas =
130+
lastUsedFee.customMaxPriorityFeePerGas;
131+
feeEnhancement.customMaxBaseFeePerGas = lastUsedFee.customMaxBaseFeePerGas;
132+
feeEnhancement.maxFeePerGas = lastUsedFee.maxFeePerGas;
129133
}
130134
}
131135
}

packages/suite/src/hooks/wallet/useSendFormCompose.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -262,9 +262,12 @@ export const useSendFormCompose = ({
262262
setValue('selectedFee', nearest);
263263
if (nearest === 'custom') {
264264
// @ts-expect-error: type = error already filtered above
265-
const { feePerByte, feeLimit } = composed;
265+
const { feePerByte, feeLimit, maxPriorityFeePerGas, maxBaseFeePerGas } =
266+
composed;
266267
setValue('feePerUnit', feePerByte);
267268
setValue('feeLimit', feeLimit || '');
269+
setValue('customMaxPriorityFeePerGas', maxPriorityFeePerGas || '');
270+
setValue('customMaxBaseFeePerGas', maxBaseFeePerGas || '');
268271
}
269272
setDraftSaveRequest(true);
270273
}

suite-common/wallet-core/src/send/sendFormEthereumThunks.ts

+22-101
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { fromWei, toWei } from 'web3-utils';
1+
import { toWei } from 'web3-utils';
22

33
import { createThunk } from '@suite-common/redux-utils';
44
import { notificationsActions } from '@suite-common/toast-notifications';
@@ -11,16 +11,10 @@ import {
1111
import {
1212
Account,
1313
AddressDisplayOptions,
14-
ExternalOutput,
1514
PrecomposedLevels,
16-
PrecomposedTransaction,
1715
RbfTransactionParams,
1816
} from '@suite-common/wallet-types';
1917
import {
20-
amountToSmallestUnit,
21-
calculateEthFee,
22-
calculateMax,
23-
calculateTotal,
2418
formatAmount,
2519
getAccountIdentity,
2620
getEthereumEstimateFeeParams,
@@ -30,106 +24,18 @@ import {
3024
isPending,
3125
prepareEthereumTransaction,
3226
} from '@suite-common/wallet-utils';
33-
import TrezorConnect, { FeeLevel, TokenInfo } from '@trezor/connect';
27+
import TrezorConnect, { FeeLevel } from '@trezor/connect';
3428
import { BigNumber } from '@trezor/utils/src/bigNumber';
3529

3630
import { SEND_MODULE_PREFIX } from './sendFormConstants';
31+
import { calculateEvmTxWithFees } from './sendFormEthereumUtils';
3732
import {
3833
ComposeFeeLevelsError,
3934
ComposeTransactionThunkArguments,
4035
SignTransactionError,
4136
SignTransactionThunkArguments,
4237
} from './sendFormTypes';
4338
import { selectTransactions } from '../transactions/transactionsReducer';
44-
45-
const calculate = (
46-
availableBalance: string,
47-
output: ExternalOutput,
48-
feeLevel: FeeLevel,
49-
token?: TokenInfo,
50-
): PrecomposedTransaction => {
51-
let amount: string;
52-
let max: string | undefined;
53-
const feeInGwei = calculateEthFee(toWei(feeLevel.feePerUnit, 'gwei'), feeLevel.feeLimit || '0');
54-
55-
const availableTokenBalance = token
56-
? amountToSmallestUnit(token.balance!, token.decimals)
57-
: undefined;
58-
if (output.type === 'send-max' || output.type === 'send-max-noaddress') {
59-
max = availableTokenBalance || calculateMax(availableBalance, feeInGwei);
60-
amount = max;
61-
} else {
62-
amount = output.amount;
63-
}
64-
65-
// total ETH spent (amount + fee), in ERC20 only fee
66-
const totalSpent = new BigNumber(calculateTotal(token ? '0' : amount, feeInGwei));
67-
68-
if (totalSpent.isGreaterThan(availableBalance)) {
69-
if (token) {
70-
return {
71-
type: 'error',
72-
error: 'AMOUNT_NOT_ENOUGH_CURRENCY_FEE_WITH_ETH_AMOUNT',
73-
errorMessage: {
74-
id: 'AMOUNT_NOT_ENOUGH_CURRENCY_FEE_WITH_ETH_AMOUNT',
75-
values: {
76-
feeAmount: fromWei(feeInGwei, 'ether').toString(),
77-
},
78-
},
79-
} as const;
80-
}
81-
82-
return {
83-
type: 'error',
84-
error: 'AMOUNT_IS_NOT_ENOUGH',
85-
errorMessage: { id: 'AMOUNT_IS_NOT_ENOUGH' },
86-
} as const;
87-
}
88-
89-
// validate if token balance is not 0 or lower than amount
90-
if (
91-
availableTokenBalance &&
92-
(availableTokenBalance === '0' || new BigNumber(amount).gt(availableTokenBalance))
93-
) {
94-
return {
95-
type: 'error',
96-
error: 'AMOUNT_IS_NOT_ENOUGH',
97-
errorMessage: { id: 'AMOUNT_IS_NOT_ENOUGH' },
98-
} as const;
99-
}
100-
101-
const payloadData = {
102-
type: 'nonfinal' as const,
103-
totalSpent: token ? amount : totalSpent.toString(),
104-
max,
105-
fee: feeInGwei,
106-
feePerByte: feeLevel.feePerUnit,
107-
feeLimit: feeLevel.feeLimit,
108-
token,
109-
bytes: 0, // TODO: calculate
110-
inputs: [],
111-
};
112-
113-
if (output.type === 'send-max' || output.type === 'payment') {
114-
return {
115-
...payloadData,
116-
type: 'final',
117-
// compatibility with BTC PrecomposedTransaction from @trezor/connect
118-
inputs: [],
119-
outputsPermutation: [0],
120-
outputs: [
121-
{
122-
address: output.address,
123-
amount,
124-
script_type: 'PAYTOADDRESS',
125-
},
126-
],
127-
};
128-
}
129-
130-
return payloadData;
131-
};
132-
13339
export const composeEthereumTransactionFeeLevelsThunk = createThunk<
13440
PrecomposedLevels,
13541
ComposeTransactionThunkArguments,
@@ -164,6 +70,7 @@ export const composeEthereumTransactionFeeLevelsThunk = createThunk<
16470
coin: account.symbol,
16571
identity: getAccountIdentity(account),
16672
request: {
73+
feeLevels: 'smart',
16774
blocks: [2],
16875
specific: {
16976
from: account.descriptor,
@@ -212,18 +119,27 @@ export const composeEthereumTransactionFeeLevelsThunk = createThunk<
212119
}
213120
// in case when selectedFee is set to 'custom' construct this FeeLevel from values
214121
if (formState.selectedFee === 'custom') {
122+
const { customMaxPriorityFeePerGas, effectiveGasPrice, feePerUnit, feeLimit } =
123+
formState;
124+
125+
const customMaxPriorityFeePerGasWei = customMaxPriorityFeePerGas
126+
? BigNumber(toWei(Number(customMaxPriorityFeePerGas), 'gwei'))
127+
: undefined;
128+
215129
predefinedLevels.push({
216130
label: 'custom',
217-
feePerUnit: formState.feePerUnit,
218-
feeLimit: formState.feeLimit,
131+
feePerUnit,
132+
feeLimit,
133+
effectiveGasPrice,
134+
maxPriorityFeePerGas: customMaxPriorityFeePerGasWei?.toString(),
219135
blocks: -1,
220136
});
221137
}
222138

223139
// wrap response into PrecomposedLevels object where key is a FeeLevel label
224140
const resultLevels: PrecomposedLevels = {};
225141
const response = predefinedLevels.map(level =>
226-
calculate(availableBalance, output, level, tokenInfo),
142+
calculateEvmTxWithFees({ availableBalance, output, feeLevel: level, token: tokenInfo }),
227143
);
228144
response.forEach((tx, index) => {
229145
const feeLabel = predefinedLevels[index].label as FeeLevel['label'];
@@ -331,7 +247,12 @@ export const signEthereumSendFormTransactionThunk = createThunk<
331247
amount: formState.outputs[0].amount,
332248
data: formState.ethereumDataHex,
333249
gasLimit: precomposedTransaction.feeLimit || '',
334-
gasPrice: precomposedTransaction.feePerByte,
250+
maxFeePerGas: precomposedTransaction.maxFeePerGas ?? undefined,
251+
maxPriorityFeePerGas: precomposedTransaction.maxPriorityFeePerGas ?? undefined,
252+
gasPrice:
253+
!precomposedTransaction.maxFeePerGas && precomposedTransaction.feePerByte
254+
? precomposedTransaction.feePerByte
255+
: undefined,
335256
nonce,
336257
});
337258

0 commit comments

Comments
 (0)