Skip to content

Commit 731c90c

Browse files
Improve swap fee estimation (#4168)
* Improve swap fee estimation * Fix tests * Update copy * Fix color * Fix legacy gas price calculation * Add test
1 parent 18c742e commit 731c90c

File tree

21 files changed

+226
-64
lines changed

21 files changed

+226
-64
lines changed

src/components/GasSelector.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,10 +106,11 @@ export default function GasSelector({
106106

107107
try {
108108
const { network } = account;
109-
const [gas, fetchedNonce] = await Promise.all([
109+
const [gasEstimate, fetchedNonce] = await Promise.all([
110110
fetchUniversalGasPriceEstimate(network, account),
111111
getNonce(network, account.address)
112112
]);
113+
const { estimate: gas } = gasEstimate;
113114
setGasPrice({
114115
gasPrice: gas.gasPrice ?? '',
115116
maxFeePerGas: gas.maxFeePerGas ?? '',

src/components/TokenMigration/components/TokenMigrationForm.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ export const TokenMigrationFormUI = ({
182182
const handleSubmit = () => {
183183
if (isFormValid) {
184184
fetchUniversalGasPriceEstimate(values.network, values.account)
185-
.then((gas) => {
185+
.then(({ estimate: gas }) => {
186186
onComplete({ ...values, ...gas });
187187
})
188188
.catch(console.error);

src/components/TransactionFeeEIP1559.tsx

Lines changed: 11 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { GasLimitField, GasPriceField } from '@features/SendAssets/components';
77
import { COLORS } from '@theme';
88
import { translateRaw } from '@translations';
99
import { Asset, Fiat } from '@types';
10-
import { bigify, bigNumGasPriceToViewableGwei, gasStringsToMaxGasNumber } from '@utils';
10+
import { calculateMinMaxFee } from '@utils';
1111

1212
import Box from './Box';
1313
import { default as Currency } from './Currency';
@@ -63,26 +63,16 @@ export const TransactionFeeEIP1559 = ({
6363
const [editMode, setEditMode] = useState(false);
6464
const handleToggleEditMode = () => setEditMode(!editMode);
6565

66-
const viewableBaseFee = baseFee && bigify(bigNumGasPriceToViewableGwei(baseFee));
67-
68-
const maxFee = gasStringsToMaxGasNumber(maxFeePerGas, gasLimit);
69-
const maxFeeFiat = maxFee.multipliedBy(baseAssetRate);
70-
const hasFiatValue = maxFeeFiat.gt(0);
71-
72-
const minMaxFee =
73-
viewableBaseFee &&
74-
BigNumber.min(
75-
bigify(maxPriorityFeePerGas).gt(viewableBaseFee)
76-
? bigify(maxPriorityFeePerGas).plus(viewableBaseFee)
77-
: viewableBaseFee,
78-
maxFeePerGas
79-
);
80-
81-
const minFee = minMaxFee ? gasStringsToMaxGasNumber(minMaxFee.toString(), gasLimit) : maxFee;
82-
const minFeeFiat = minFee.multipliedBy(baseAssetRate);
83-
84-
const avgFee = minFee.plus(maxFee).dividedBy(2);
85-
const avgFeeFiat = avgFee.multipliedBy(baseAssetRate);
66+
const {
67+
viewableBaseFee,
68+
minFee,
69+
minFeeFiat,
70+
avgFee,
71+
avgFeeFiat,
72+
maxFee,
73+
maxFeeFiat,
74+
hasFiatValue
75+
} = calculateMinMaxFee({ baseFee, baseAssetRate, maxFeePerGas, maxPriorityFeePerGas, gasLimit });
8676

8777
return (
8878
<Box opacity={disabled ? '0.5' : undefined}>

src/components/TransactionFlow/helpers.spec.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
import { fetchUniversalGasPriceEstimate } from '@services/ApiService/Gas';
1515
import { translateRaw } from '@translations';
1616
import { ITxGasLimit, ITxNonce, ITxObject, ITxStatus, ITxType } from '@types';
17-
import { generateUUID, noOp } from '@utils';
17+
import { bigify, generateUUID, noOp } from '@utils';
1818

1919
import {
2020
calculateReplacementGasPrice,
@@ -24,7 +24,7 @@ import {
2424

2525
jest.mock('@services/ApiService/Gas', () => ({
2626
...jest.requireActual('@services/ApiService/Gas'),
27-
fetchUniversalGasPriceEstimate: jest.fn().mockResolvedValueOnce({ gasPrice: '500' })
27+
fetchUniversalGasPriceEstimate: jest.fn().mockResolvedValueOnce({ estimate: { gasPrice: '500' } })
2828
}));
2929

3030
describe('calculateReplacementGasPrice', () => {
@@ -39,7 +39,7 @@ describe('calculateReplacementGasPrice', () => {
3939
it('correctly determines tx gas price with too low fast gas price', () => {
4040
(fetchUniversalGasPriceEstimate as jest.MockedFunction<
4141
typeof fetchUniversalGasPriceEstimate
42-
>).mockResolvedValueOnce({ gasPrice: '1' });
42+
>).mockResolvedValueOnce({ estimate: { gasPrice: '1' } });
4343
return expect(
4444
calculateReplacementGasPrice(fTxConfig, { ...fNetworks[0], supportsEIP1559: false })
4545
).resolves.toStrictEqual({
@@ -50,7 +50,10 @@ describe('calculateReplacementGasPrice', () => {
5050
it('correctly determines tx gas price for eip 1559', () => {
5151
(fetchUniversalGasPriceEstimate as jest.MockedFunction<
5252
typeof fetchUniversalGasPriceEstimate
53-
>).mockResolvedValueOnce({ maxFeePerGas: '1', maxPriorityFeePerGas: '1' });
53+
>).mockResolvedValueOnce({
54+
baseFee: bigify(1000000000),
55+
estimate: { maxFeePerGas: '1', maxPriorityFeePerGas: '1' }
56+
});
5457
return expect(
5558
calculateReplacementGasPrice(fTxConfigEIP1559, { ...fNetworks[0], supportsEIP1559: true })
5659
).resolves.toStrictEqual({
@@ -62,7 +65,10 @@ describe('calculateReplacementGasPrice', () => {
6265
it('correctly determines tx gas price for eip 1559 when new price too high', () => {
6366
(fetchUniversalGasPriceEstimate as jest.MockedFunction<
6467
typeof fetchUniversalGasPriceEstimate
65-
>).mockResolvedValueOnce({ maxFeePerGas: '100', maxPriorityFeePerGas: '10' });
68+
>).mockResolvedValueOnce({
69+
baseFee: bigify(100000000000),
70+
estimate: { maxFeePerGas: '100', maxPriorityFeePerGas: '10' }
71+
});
6672
return expect(
6773
calculateReplacementGasPrice(fTxConfigEIP1559, { ...fNetworks[0], supportsEIP1559: true })
6874
).resolves.toStrictEqual({

src/components/TransactionFlow/helpers.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ export const constructSenderFromTxConfig = (
143143

144144
// replacement gas price must be at least 10% higher than the replaced tx's gas price
145145
export const calculateReplacementGasPrice = async (txConfig: ITxConfig, network: Network) => {
146-
const gas = await fetchUniversalGasPriceEstimate(network, txConfig.senderAccount);
146+
const { estimate: gas } = await fetchUniversalGasPriceEstimate(network, txConfig.senderAccount);
147147

148148
return isType2Tx(txConfig.rawTransaction)
149149
? {

src/features/PurchaseMembership/components/MembershipPurchaseForm.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -263,9 +263,11 @@ export const MembershipFormUI = ({
263263
loading={isSubmitting}
264264
onClick={() => {
265265
if (isValid) {
266-
fetchUniversalGasPriceEstimate(values.network, values.account).then((gas) => {
267-
onComplete({ ...values, ...gas });
268-
});
266+
fetchUniversalGasPriceEstimate(values.network, values.account).then(
267+
({ estimate: gas }) => {
268+
onComplete({ ...values, ...gas });
269+
}
270+
);
269271
}
270272
}}
271273
>

src/features/SwapAssets/SwapAssetsFlow.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import SwapAssetsFlow from './SwapAssetsFlow';
1111

1212
jest.mock('@services/ApiService/Gas', () => ({
1313
...jest.requireActual('@services/ApiService/Gas'),
14-
fetchUniversalGasPriceEstimate: () => Promise.resolve({ gasPrice: '20' }),
14+
fetchUniversalGasPriceEstimate: () => Promise.resolve({ estimate: { gasPrice: '20' } }),
1515
getGasEstimate: () => Promise.resolve(21000)
1616
}));
1717

src/features/SwapAssets/SwapAssetsFlow.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,8 @@ const SwapAssetsFlow = (props: RouteComponentProps) => {
6161
approvalTx,
6262
isEstimatingGas,
6363
tradeTx,
64-
selectedNetwork
64+
selectedNetwork,
65+
gas
6566
}: SwapFormState = formState;
6667

6768
const [assetPair, setAssetPair] = useState({});
@@ -110,7 +111,8 @@ const SwapAssetsFlow = (props: RouteComponentProps) => {
110111
approvalTx,
111112
isEstimatingGas,
112113
isSubmitting,
113-
selectedNetwork
114+
selectedNetwork,
115+
gas
114116
},
115117
actions: {
116118
handleFromAssetSelected,

src/features/SwapAssets/components/SwapAssets.tsx

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,9 @@ import {
3232
import { SPACING } from '@theme';
3333
import translate, { translateRaw } from '@translations';
3434
import { Asset, ISwapAsset, Network, NetworkId, StoreAccount } from '@types';
35-
import { bigify, getTimeDifference, sortByLabel, totalTxFeeToString, useInterval } from '@utils';
35+
import { getTimeDifference, sortByLabel, useInterval } from '@utils';
3636

37-
import { getAccountsWithAssetBalance, getUnselectedAssets } from '../helpers';
37+
import { getAccountsWithAssetBalance, getEstimatedGasFee, getUnselectedAssets } from '../helpers';
3838
import { SwapFormState } from '../types';
3939
import { SwapQuote } from './SwapQuote';
4040

@@ -96,7 +96,8 @@ const SwapAssets = (props: Props) => {
9696
gasPrice,
9797
isEstimatingGas,
9898
expiration,
99-
setNetwork
99+
setNetwork,
100+
gas
100101
} = props;
101102

102103
const settings = useSelector(getSettings);
@@ -147,13 +148,12 @@ const SwapAssets = (props: Props) => {
147148
calculateNewFromAmountDebounced(value);
148149
};
149150

150-
const estimatedGasFee =
151-
gasPrice &&
152-
tradeGasLimit &&
153-
totalTxFeeToString(
154-
gasPrice,
155-
bigify(tradeGasLimit).plus(approvalGasLimit ? approvalGasLimit : 0)
156-
);
151+
const estimatedGasFee = getEstimatedGasFee({
152+
tradeGasLimit,
153+
approvalGasLimit,
154+
baseAssetRate,
155+
gas
156+
});
157157

158158
// Accounts with a balance of the chosen asset and base asset
159159
const filteredAccounts = fromAsset

src/features/SwapAssets/components/SwapQuote.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ export const SwapQuote = ({
5353
</Heading>
5454
<LinkApp href="#" variant="opacityLink">
5555
<Box variant="rowAlign" onClick={() => handleRefreshQuote()}>
56-
<Icon type="refresh" width="16px" />
56+
<Icon type="refresh" width="16px" color="BLUE_BRIGHT" />
5757
<Text ml={SPACING.XS} mb={0}>
5858
{translateRaw('GET_NEW_QUOTE')}
5959
</Text>
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { ITxGasLimit } from '@types';
2+
import { bigify } from '@utils';
3+
4+
import { getEstimatedGasFee } from './helpers';
5+
6+
describe('getEstimatedGasFee', () => {
7+
it('supports legacy gas', () => {
8+
expect(
9+
getEstimatedGasFee({
10+
tradeGasLimit: '0x3f7b1' as ITxGasLimit,
11+
approvalGasLimit: '0x3f7b1' as ITxGasLimit,
12+
baseAssetRate: 0,
13+
gas: {
14+
estimate: { gasPrice: '90', maxFeePerGas: undefined, maxPriorityFeePerGas: undefined }
15+
}
16+
})
17+
).toStrictEqual('0.046803');
18+
});
19+
20+
it('supports eip 1559 gas', () => {
21+
expect(
22+
getEstimatedGasFee({
23+
tradeGasLimit: '0x3f7b1' as ITxGasLimit,
24+
approvalGasLimit: '0x3f7b1' as ITxGasLimit,
25+
baseAssetRate: 0,
26+
gas: {
27+
estimate: { maxFeePerGas: '200', maxPriorityFeePerGas: '5', gasPrice: undefined },
28+
baseFee: bigify('200000000000')
29+
}
30+
})
31+
).toStrictEqual('0.104007');
32+
});
33+
});

src/features/SwapAssets/helpers.ts

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,25 @@
1+
import BigNumber from 'bignumber.js';
2+
13
import { WALLET_STEPS } from '@components';
24
import { DEFAULT_ASSET_DECIMAL } from '@config';
3-
import { getAssetByTicker, getAssetByUUID } from '@services';
4-
import { ISwapAsset, ITxConfig, ITxObject, StoreAccount, StoreAsset, TUuid } from '@types';
5-
import { toTokenBase, weiToFloat } from '@utils';
5+
import { getAssetByTicker, getAssetByUUID, UniversalGasEstimationResult } from '@services';
6+
import {
7+
ISwapAsset,
8+
ITxConfig,
9+
ITxGasLimit,
10+
ITxObject,
11+
StoreAccount,
12+
StoreAsset,
13+
TUuid
14+
} from '@types';
15+
import {
16+
bigify,
17+
calculateMinMaxFee,
18+
inputGasPriceToHex,
19+
totalTxFeeToString,
20+
toTokenBase,
21+
weiToFloat
22+
} from '@utils';
623

724
export const makeSwapTxConfig = (assets: StoreAsset[]) => (
825
transaction: ITxObject,
@@ -28,6 +45,42 @@ export const makeSwapTxConfig = (assets: StoreAsset[]) => (
2845
return txConfig;
2946
};
3047

48+
export const getEstimatedGasFee = ({
49+
tradeGasLimit,
50+
approvalGasLimit,
51+
baseAssetRate,
52+
gas
53+
}: {
54+
tradeGasLimit?: ITxGasLimit;
55+
approvalGasLimit?: ITxGasLimit;
56+
baseAssetRate?: number;
57+
gas?: { estimate: UniversalGasEstimationResult; baseFee?: BigNumber };
58+
}) => {
59+
if (tradeGasLimit && gas?.estimate.maxFeePerGas) {
60+
const { avgFee } = calculateMinMaxFee({
61+
baseFee: gas.baseFee,
62+
...gas.estimate,
63+
gasLimit:
64+
tradeGasLimit &&
65+
bigify(tradeGasLimit)
66+
.plus(approvalGasLimit ? approvalGasLimit : 0)
67+
.toString(),
68+
baseAssetRate
69+
});
70+
71+
return avgFee.toFixed(6);
72+
}
73+
74+
return (
75+
gas?.estimate.gasPrice &&
76+
tradeGasLimit &&
77+
totalTxFeeToString(
78+
inputGasPriceToHex(gas.estimate.gasPrice),
79+
bigify(tradeGasLimit).plus(approvalGasLimit ? approvalGasLimit : 0)
80+
)
81+
);
82+
};
83+
3184
// filter accounts based on wallet type and sufficient balance
3285
export const getAccountsWithAssetBalance = (
3386
accounts: StoreAccount[],

src/features/SwapAssets/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { BigNumber } from 'bignumber.js';
22
import { DistributiveOmit } from 'react-redux';
33

4+
import { UniversalGasEstimationResult } from '@services';
45
import {
56
ISwapAsset,
67
ITxGasLimit,
@@ -62,6 +63,7 @@ export interface SwapFormState {
6263
txType: ITxType;
6364
metadata: ITxMetadata;
6465
};
66+
gas?: { estimate: UniversalGasEstimationResult; baseFee?: BigNumber };
6567
}
6668

6769
export interface IAssetPair {

src/helpers/transaction.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -443,7 +443,7 @@ export const appendGasPrice = (network: Network, account: StoreAccount) => async
443443
return tx as TxBeforeGasLimit;
444444
}
445445
const gas = await fetchUniversalGasPriceEstimate(network, account)
446-
.then((r) => mapObjIndexed((v) => v && inputGasPriceToHex(v), r))
446+
.then(({ estimate: r }) => mapObjIndexed((v) => v && inputGasPriceToHex(v), r))
447447
.catch((err) => {
448448
throw new Error(`getGasPriceEstimate: ${err}`);
449449
});

src/services/ApiService/Dex/Dex.spec.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,14 @@ import {
1313
TUuid,
1414
WalletId
1515
} from '@types';
16+
import { bigify } from '@utils';
1617

1718
import { DexService } from '.';
1819
import { formatTradeTx } from './Dex';
1920

2021
jest.mock('@services/ApiService/Gas', () => ({
2122
...jest.requireActual('@services/ApiService/Gas'),
22-
fetchUniversalGasPriceEstimate: jest.fn().mockResolvedValue({ gasPrice: '154' })
23+
fetchUniversalGasPriceEstimate: jest.fn().mockResolvedValue({ estimate: { gasPrice: '154' } })
2324
}));
2425

2526
describe('SwapFlow', () => {
@@ -64,7 +65,10 @@ describe('SwapFlow', () => {
6465
it('returns the expected two transactions for a multi tx swap using eip1559', async () => {
6566
(fetchUniversalGasPriceEstimate as jest.MockedFunction<
6667
typeof fetchUniversalGasPriceEstimate
67-
>).mockResolvedValueOnce({ maxFeePerGas: '100', maxPriorityFeePerGas: '10' });
68+
>).mockResolvedValueOnce({
69+
baseFee: bigify(100000000000),
70+
estimate: { maxFeePerGas: '100', maxPriorityFeePerGas: '10' }
71+
});
6872
const promise = DexService.instance.getOrderDetailsFrom(
6973
{ ...fNetwork, supportsEIP1559: true },
7074
{ ...fAccount, wallet: WalletId.LEDGER_NANO_S_NEW },

src/services/ApiService/Dex/Dex.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,8 @@ export default class DexService {
120120
})
121121
});
122122

123-
const gas = await fetchUniversalGasPriceEstimate(network, account);
123+
const gasResult = await fetchUniversalGasPriceEstimate(network, account);
124+
const { estimate: gas } = gasResult;
124125

125126
const gasPrice = gas.gasPrice ?? gas.maxFeePerGas;
126127

@@ -153,6 +154,7 @@ export default class DexService {
153154
expiration: Date.now() / 1000 + DEX_TRADE_EXPIRATION,
154155
approvalTx,
155156
tradeGasLimit,
157+
gas: gasResult,
156158
tradeTx: formatTradeTx({
157159
account: account!,
158160
to: data.to,

0 commit comments

Comments
 (0)