Skip to content

Commit 39a6cd0

Browse files
authored
feat: Deduct a fixed amount for gas token on Max button click (#3943)
* feat: Deduct a fixed amount for gas token on Max button click Signed-off-by: Emre Bogazliyanlioglu <emre@wormholelabs.xyz> * test: Add tests and refactor Signed-off-by: Emre Bogazliyanlioglu <emre@wormholelabs.xyz> * feat: Change button behavior after product feedback Signed-off-by: Emre Bogazliyanlioglu <emre@wormholelabs.xyz> * chore: Get gas token decimal in the utility function instead of passing down Signed-off-by: Emre Bogazliyanlioglu <emre@wormholelabs.xyz> * feat: add Monad Signed-off-by: Emre Bogazliyanlioglu <emre@wormholelabs.xyz> * test: remove off-setting related tests Signed-off-by: Emre Bogazliyanlioglu <emre@wormholelabs.xyz> * feat: Keep gasReserve in chain config instead of separate const Signed-off-by: Emre Bogazliyanlioglu <emre@wormholelabs.xyz> * feat: add gasReserve to more chains Signed-off-by: Emre Bogazliyanlioglu <emre@wormholelabs.xyz> * fix: Make sure gas reserve deduction persists when user changes destination asset Signed-off-by: Emre Bogazliyanlioglu <emre@wormholelabs.xyz> * fix: Bumping Moonbeam gas reserve to 0.03 as wallets complain about insufficient gas for 0.01 Signed-off-by: Emre Bogazliyanlioglu <emre@wormholelabs.xyz> * fix: Clear previous amount when source wallet address changes Signed-off-by: Emre Bogazliyanlioglu <emre@wormholelabs.xyz> * test: Fix unit test Signed-off-by: Emre Bogazliyanlioglu <emre@wormholelabs.xyz> --------- Signed-off-by: Emre Bogazliyanlioglu <emre@wormholelabs.xyz>
1 parent 160aaa1 commit 39a6cd0

7 files changed

Lines changed: 990 additions & 88 deletions

File tree

src/config/mainnet/chains.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export const MAINNET_CHAINS: ChainsConfig = {
88
explorerName: 'Etherscan',
99
icon: 'Ethereum',
1010
symbol: 'ETH',
11+
gasReserve: '0.01',
1112
},
1213
Bsc: {
1314
displayName: 'BNB',
@@ -16,6 +17,7 @@ export const MAINNET_CHAINS: ChainsConfig = {
1617
explorerName: 'BscScan',
1718
icon: 'Bsc',
1819
symbol: 'BSC',
20+
gasReserve: '0.01',
1921
},
2022
Polygon: {
2123
displayName: 'Polygon',
@@ -24,6 +26,7 @@ export const MAINNET_CHAINS: ChainsConfig = {
2426
explorerName: 'PolygonScan',
2527
icon: 'Polygon',
2628
symbol: 'POL',
29+
gasReserve: '0.01',
2730
},
2831
Avalanche: {
2932
displayName: 'Avalanche',
@@ -32,6 +35,7 @@ export const MAINNET_CHAINS: ChainsConfig = {
3235
explorerName: 'Snowtrace',
3336
icon: 'Avalanche',
3437
symbol: 'AVAX',
38+
gasReserve: '0.01',
3539
},
3640
Fantom: {
3741
displayName: 'Fantom',
@@ -48,6 +52,7 @@ export const MAINNET_CHAINS: ChainsConfig = {
4852
explorerName: 'Celo Explorer',
4953
icon: 'Celo',
5054
symbol: 'CELO',
55+
gasReserve: '0.01',
5156
},
5257
Moonbeam: {
5358
displayName: 'Moonbeam',
@@ -56,6 +61,7 @@ export const MAINNET_CHAINS: ChainsConfig = {
5661
explorerName: 'Moonscan',
5762
icon: 'Moonbeam',
5863
symbol: 'GLMR',
64+
gasReserve: '0.03',
5965
},
6066
Solana: {
6167
displayName: 'Solana',
@@ -64,6 +70,7 @@ export const MAINNET_CHAINS: ChainsConfig = {
6470
explorerName: 'Solana Explorer',
6571
icon: 'Solana',
6672
symbol: 'SOL',
73+
gasReserve: '0.01',
6774
},
6875
Sui: {
6976
displayName: 'Sui',
@@ -72,6 +79,7 @@ export const MAINNET_CHAINS: ChainsConfig = {
7279
explorerName: 'Suiscan',
7380
icon: 'Sui',
7481
symbol: 'SUI',
82+
gasReserve: '0.01',
7583
},
7684
Aptos: {
7785
displayName: 'Aptos',
@@ -80,6 +88,7 @@ export const MAINNET_CHAINS: ChainsConfig = {
8088
explorerName: 'Aptos Explorer',
8189
icon: 'Aptos',
8290
symbol: 'APT',
91+
gasReserve: '0.01',
8392
},
8493
Base: {
8594
displayName: 'Base',
@@ -88,6 +97,7 @@ export const MAINNET_CHAINS: ChainsConfig = {
8897
explorerName: 'BaseScan',
8998
icon: 'Base',
9099
symbol: 'BASE',
100+
gasReserve: '0.001',
91101
},
92102
Arbitrum: {
93103
displayName: 'Arbitrum',
@@ -96,6 +106,7 @@ export const MAINNET_CHAINS: ChainsConfig = {
96106
explorerName: 'Arbitrum Explorer',
97107
icon: 'Arbitrum',
98108
symbol: 'ARB',
109+
gasReserve: '0.001',
99110
},
100111
Optimism: {
101112
displayName: 'Optimism',
@@ -104,6 +115,7 @@ export const MAINNET_CHAINS: ChainsConfig = {
104115
explorerName: 'Optimistic Etherscan',
105116
icon: 'Optimism',
106117
symbol: 'OP',
118+
gasReserve: '0.001',
107119
},
108120
Klaytn: {
109121
displayName: 'Kaia',
@@ -112,6 +124,7 @@ export const MAINNET_CHAINS: ChainsConfig = {
112124
explorerName: 'KaiaScan',
113125
icon: 'Klaytn',
114126
symbol: 'KAIA',
127+
gasReserve: '0.01',
115128
},
116129
Scroll: {
117130
displayName: 'Scroll',
@@ -120,6 +133,7 @@ export const MAINNET_CHAINS: ChainsConfig = {
120133
explorerName: 'Scrollscan',
121134
icon: 'Scroll',
122135
symbol: 'SCR',
136+
gasReserve: '0.001',
123137
},
124138
Xlayer: {
125139
displayName: 'X Layer',
@@ -128,6 +142,7 @@ export const MAINNET_CHAINS: ChainsConfig = {
128142
explorerName: 'OKX Explorer',
129143
icon: 'Xlayer',
130144
symbol: 'OKX',
145+
gasReserve: '0.001',
131146
},
132147
Mantle: {
133148
displayName: 'Mantle',
@@ -136,6 +151,7 @@ export const MAINNET_CHAINS: ChainsConfig = {
136151
explorerName: 'Mantle Explorer',
137152
icon: 'Mantle',
138153
symbol: 'MNT',
154+
gasReserve: '0.001',
139155
},
140156
Worldchain: {
141157
displayName: 'World Chain',
@@ -232,6 +248,7 @@ export const MAINNET_CHAINS: ChainsConfig = {
232248
explorerName: 'XRPL EVM Explorer',
233249
icon: 'XRPLEVM',
234250
symbol: 'XRP',
251+
gasReserve: '0.01',
235252
},
236253
CreditCoin: {
237254
displayName: 'Creditcoin',
@@ -248,6 +265,7 @@ export const MAINNET_CHAINS: ChainsConfig = {
248265
explorerName: 'Fogo Explorer',
249266
icon: 'Fogo',
250267
symbol: 'FOGO',
268+
gasReserve: '0.01',
251269
},
252270
Monad: {
253271
displayName: 'Monad',
@@ -256,5 +274,6 @@ export const MAINNET_CHAINS: ChainsConfig = {
256274
explorerName: 'MonadVision',
257275
icon: 'Monad',
258276
symbol: 'MON',
277+
gasReserve: '0.01',
259278
},
260279
};

src/config/types.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,18 @@ export interface ChainConfig {
233233
explorerName: string;
234234
icon: Chain;
235235
symbol?: string;
236+
/**
237+
* Gas reserve amount to keep for the chain's native token.
238+
* This ensures users have enough gas to complete transactions.
239+
* Format: decimal string (e.g., '0.01' for 0.01 of the native token)
240+
*
241+
* Typical values:
242+
* - Ethereum: '0.01' ($30 notional)
243+
* - L2s (Base, Op, Arb): '0.001' ($3 notional)
244+
* - Other EVM L1s: '0.01'
245+
* - Solana: '0.01' ($1.3 notional)
246+
*/
247+
gasReserve?: string;
236248
}
237249

238250
export type ChainsConfig = {

src/utils/gasReserve.test.ts

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { amount as sdkAmount } from '@wormhole-foundation/sdk';
3+
import { getGasReserve } from './gasReserve';
4+
5+
describe('getGasReserve', () => {
6+
describe('Ethereum mainnet', () => {
7+
it('returns 0.01 ETH for Ethereum', () => {
8+
const reserve = getGasReserve('Ethereum');
9+
10+
expect(reserve).toBeDefined();
11+
expect(sdkAmount.display(reserve!)).toBe('0.01');
12+
expect(sdkAmount.units(reserve!)).toBe(10000000000000000n); // 0.01 * 10^18
13+
});
14+
});
15+
16+
describe('L2 chains', () => {
17+
it('returns 0.001 ETH for Base', () => {
18+
const reserve = getGasReserve('Base');
19+
20+
expect(reserve).toBeDefined();
21+
expect(sdkAmount.display(reserve!)).toBe('0.001');
22+
expect(sdkAmount.units(reserve!)).toBe(1000000000000000n); // 0.001 * 10^18
23+
});
24+
25+
it('returns 0.001 ETH for Optimism', () => {
26+
const reserve = getGasReserve('Optimism');
27+
28+
expect(reserve).toBeDefined();
29+
expect(sdkAmount.display(reserve!)).toBe('0.001');
30+
expect(sdkAmount.units(reserve!)).toBe(1000000000000000n);
31+
});
32+
33+
it('returns 0.001 ETH for Arbitrum', () => {
34+
const reserve = getGasReserve('Arbitrum');
35+
36+
expect(reserve).toBeDefined();
37+
expect(sdkAmount.display(reserve!)).toBe('0.001');
38+
expect(sdkAmount.units(reserve!)).toBe(1000000000000000n);
39+
});
40+
41+
it('returns 0.001 for Scroll', () => {
42+
const reserve = getGasReserve('Scroll');
43+
44+
expect(reserve).toBeDefined();
45+
expect(sdkAmount.display(reserve!)).toBe('0.001');
46+
expect(sdkAmount.units(reserve!)).toBe(1000000000000000n);
47+
});
48+
49+
it('returns 0.001 for Xlayer', () => {
50+
const reserve = getGasReserve('Xlayer');
51+
52+
expect(reserve).toBeDefined();
53+
expect(sdkAmount.display(reserve!)).toBe('0.001');
54+
expect(sdkAmount.units(reserve!)).toBe(1000000000000000n);
55+
});
56+
57+
it('returns 0.001 for Mantle', () => {
58+
const reserve = getGasReserve('Mantle');
59+
60+
expect(reserve).toBeDefined();
61+
expect(sdkAmount.display(reserve!)).toBe('0.001');
62+
expect(sdkAmount.units(reserve!)).toBe(1000000000000000n);
63+
});
64+
});
65+
66+
describe('Solana', () => {
67+
it('returns 0.01 SOL for Solana', () => {
68+
const reserve = getGasReserve('Solana');
69+
70+
expect(reserve).toBeDefined();
71+
expect(sdkAmount.display(reserve!)).toBe('0.01');
72+
expect(sdkAmount.units(reserve!)).toBe(10000000n); // 0.01 * 10^9
73+
});
74+
});
75+
76+
describe('Other EVM L1 chains', () => {
77+
it('returns 0.01 for Bsc', () => {
78+
const reserve = getGasReserve('Bsc');
79+
80+
expect(reserve).toBeDefined();
81+
expect(sdkAmount.display(reserve!)).toBe('0.01');
82+
expect(sdkAmount.units(reserve!)).toBe(10000000000000000n);
83+
});
84+
85+
it('returns 0.01 for Avalanche', () => {
86+
const reserve = getGasReserve('Avalanche');
87+
88+
expect(reserve).toBeDefined();
89+
expect(sdkAmount.display(reserve!)).toBe('0.01');
90+
expect(sdkAmount.units(reserve!)).toBe(10000000000000000n);
91+
});
92+
93+
it('returns 0.01 for Polygon', () => {
94+
const reserve = getGasReserve('Polygon');
95+
96+
expect(reserve).toBeDefined();
97+
expect(sdkAmount.display(reserve!)).toBe('0.01');
98+
expect(sdkAmount.units(reserve!)).toBe(10000000000000000n);
99+
});
100+
101+
it('returns undefined for Fantom (gas token not configured)', () => {
102+
const reserve = getGasReserve('Fantom');
103+
104+
// Fantom has a reserve configured but no gas token in config, so should return undefined
105+
expect(reserve).toBeUndefined();
106+
});
107+
108+
it('returns 0.01 for Celo', () => {
109+
const reserve = getGasReserve('Celo');
110+
111+
expect(reserve).toBeDefined();
112+
expect(sdkAmount.display(reserve!)).toBe('0.01');
113+
expect(sdkAmount.units(reserve!)).toBe(10000000000000000n);
114+
});
115+
116+
it('returns 0.03 for Moonbeam', () => {
117+
const reserve = getGasReserve('Moonbeam');
118+
119+
expect(reserve).toBeDefined();
120+
expect(sdkAmount.display(reserve!)).toBe('0.03');
121+
expect(sdkAmount.units(reserve!)).toBe(30000000000000000n);
122+
});
123+
124+
it('returns 0.01 for Monad', () => {
125+
const reserve = getGasReserve('Monad');
126+
127+
expect(reserve).toBeDefined();
128+
expect(sdkAmount.display(reserve!)).toBe('0.01');
129+
expect(sdkAmount.units(reserve!)).toBe(10000000000000000n);
130+
});
131+
132+
it('returns 0.01 for Klaytn', () => {
133+
const reserve = getGasReserve('Klaytn');
134+
135+
expect(reserve).toBeDefined();
136+
expect(sdkAmount.display(reserve!)).toBe('0.01');
137+
expect(sdkAmount.units(reserve!)).toBe(10000000000000000n);
138+
});
139+
140+
it('returns 0.01 for Fogo', () => {
141+
const reserve = getGasReserve('Fogo');
142+
143+
expect(reserve).toBeDefined();
144+
expect(sdkAmount.display(reserve!)).toBe('0.01');
145+
expect(sdkAmount.units(reserve!)).toBe(10000000n); // 0.01 * 10^9
146+
});
147+
148+
it('returns 0.01 for XRPLEVM', () => {
149+
const reserve = getGasReserve('XRPLEVM');
150+
151+
expect(reserve).toBeDefined();
152+
expect(sdkAmount.display(reserve!)).toBe('0.01');
153+
expect(sdkAmount.units(reserve!)).toBe(10000000000000000n);
154+
});
155+
});
156+
157+
describe('Move chains', () => {
158+
it('returns 0.01 for Sui', () => {
159+
const reserve = getGasReserve('Sui');
160+
161+
expect(reserve).toBeDefined();
162+
expect(sdkAmount.display(reserve!)).toBe('0.01');
163+
expect(sdkAmount.units(reserve!)).toBe(10000000n); // 0.01 * 10^9
164+
});
165+
166+
it('returns 0.01 for Aptos', () => {
167+
const reserve = getGasReserve('Aptos');
168+
169+
expect(reserve).toBeDefined();
170+
expect(sdkAmount.display(reserve!)).toBe('0.01');
171+
expect(sdkAmount.units(reserve!)).toBe(1000000n); // 0.01 * 10^8
172+
});
173+
});
174+
175+
describe('Chains without configured reserves', () => {
176+
it('returns undefined for Cosmoshub', () => {
177+
const reserve = getGasReserve('Cosmoshub');
178+
179+
expect(reserve).toBeUndefined();
180+
});
181+
182+
it('returns undefined for Wormchain', () => {
183+
const reserve = getGasReserve('Wormchain');
184+
185+
expect(reserve).toBeUndefined();
186+
});
187+
188+
it('returns undefined for Osmosis', () => {
189+
const reserve = getGasReserve('Osmosis');
190+
191+
expect(reserve).toBeUndefined();
192+
});
193+
});
194+
});

src/utils/gasReserve.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import type { Chain } from '@wormhole-foundation/sdk';
2+
import { amount as sdkAmount } from '@wormhole-foundation/sdk';
3+
4+
import config from 'config';
5+
import { getGasToken } from 'utils';
6+
7+
/**
8+
* Get the gas reserve amount for a given chain.
9+
* Returns undefined if no reserve is configured for the chain.
10+
*
11+
* @param chain - The source chain
12+
* @returns The amount to reserve, or undefined if no reserve configured
13+
*/
14+
export function getGasReserve(chain: Chain): sdkAmount.Amount | undefined {
15+
const chainConfig = config.chains[chain];
16+
const reserve = chainConfig?.gasReserve;
17+
18+
if (!reserve) {
19+
return undefined;
20+
}
21+
22+
try {
23+
const gasToken = getGasToken(chain);
24+
return sdkAmount.parse(reserve, gasToken.decimals);
25+
} catch {
26+
// If gas token is not configured for this chain, return undefined
27+
return undefined;
28+
}
29+
}

0 commit comments

Comments
 (0)