@exodus/ethereum-api 8.34.2 → 8.34.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,16 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ## [8.34.3](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.34.2...@exodus/ethereum-api@8.34.3) (2025-04-29)
7
+
8
+
9
+ ### Bug Fixes
10
+
11
+
12
+ * fix: prevent insufficient evm custom fees on eip1559Enabled networks (#5519)
13
+
14
+
15
+
6
16
  ## [8.34.2](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.34.1...@exodus/ethereum-api@8.34.2) (2025-04-18)
7
17
 
8
18
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/ethereum-api",
3
- "version": "8.34.2",
3
+ "version": "8.34.3",
4
4
  "description": "Transaction monitors, fee monitors, RPC with the blockchain node, and other networking code for Ethereum and EVM-based blockchains",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -64,5 +64,5 @@
64
64
  "type": "git",
65
65
  "url": "git+https://github.com/ExodusMovement/assets.git"
66
66
  },
67
- "gitHead": "e7d49daa108ffa7200e33f6cfe2d1c175cd675ea"
67
+ "gitHead": "5513269ef920bfdd91701a8108fe73336d105866"
68
68
  }
@@ -28,7 +28,7 @@ import { createEvmServer, ValidMonitorTypes } from './exodus-eth-server/index.js
28
28
  import { createFeeData } from './fee-data-factory.js'
29
29
  import { createGetBalanceForAddress } from './get-balance-for-address.js'
30
30
  import { getBalancesFactory } from './get-balances.js'
31
- import { getEffectiveGasPrice, getFeeFactory } from './get-fee.js'
31
+ import { getFeeFactory } from './get-fee.js'
32
32
  import getFeeAsyncFactory from './get-fee-async.js'
33
33
  import { createEthereumHooks } from './hooks/index.js'
34
34
  import { estimateL1DataFeeFactory, getL1GetFeeFactory } from './optimism-gas/index.js'
@@ -323,7 +323,6 @@ export const createAssetFactory = ({
323
323
  server,
324
324
  ...(erc20FuelBuffer && { erc20FuelBuffer }),
325
325
  ...(fuelThreshold && { fuelThreshold: asset.currency.defaultUnit(fuelThreshold) }),
326
- getEffectiveGasPrice,
327
326
  }
328
327
  return overrideCallback({
329
328
  asset: fullAsset,
@@ -2,21 +2,51 @@ import NumberUnit from '@exodus/currency'
2
2
 
3
3
  import { resolveGasPrice } from './fee-utils.js'
4
4
 
5
+ const calculateMinCustomFee = ({ feeData }) => {
6
+ const { eip1559Enabled, baseFeePerGas, gasPriceMinimumRate } = feeData
7
+
8
+ // On `eip1559Enabled` networks, the `baseFeePerGas` is
9
+ // the minimum a transaction should be sent using, else
10
+ // get rejected by the RPC.
11
+ if (eip1559Enabled && baseFeePerGas) return baseFeePerGas.mul(Math.max(gasPriceMinimumRate, 1))
12
+
13
+ // On legacy networks, senders are permitted to send
14
+ // transactions below the `baseFeePerGas` (since these
15
+ // would not be rejected by the RPC - in fact, it was
16
+ // even possible to mine transactions with a `gasPrice`
17
+ // of `0`!).
18
+ return resolveGasPrice({ feeData }).mul(gasPriceMinimumRate)
19
+ }
20
+
21
+ const calculateRecommendedCustomFee = ({ min, feeData }) => {
22
+ const recommended = resolveGasPrice({ feeData })
23
+ // The `recommended` fee must be at least the `min`.
24
+ return recommended.gt(min) ? recommended : min
25
+ }
26
+
27
+ const calculateMaxCustomFee = ({ feeData, recommended }) => {
28
+ const { gasPriceMinimumRate, gasPriceMaximumRate } = feeData
29
+
30
+ const maxGasPrice = resolveGasPrice({ feeData }).mul(
31
+ Math.max(gasPriceMaximumRate, gasPriceMinimumRate)
32
+ )
33
+
34
+ // When calculating the `max` custom fee, the returned fee
35
+ // must be at least the `recommended` price.
36
+ return recommended.gt(maxGasPrice) ? recommended : maxGasPrice
37
+ }
38
+
5
39
  export const createCustomFeesApi = ({ baseAsset }) => {
6
40
  const Gwei = baseAsset.currency.units.Gwei
7
41
  return {
8
42
  getRecommendedMinMaxFeeUnitPrices: ({ feeData }) => {
9
- const { gasPriceMinimumRate, gasPriceMaximumRate } = feeData
10
-
11
- const gasPrice = resolveGasPrice({ feeData })
12
-
13
- const calculateFeeUnitPrice = (feeUnitPrice, multiplier = 1) =>
14
- feeUnitPrice.mul(multiplier).toNumber(Gwei)
15
-
43
+ const min = calculateMinCustomFee({ feeData })
44
+ const recommended = calculateRecommendedCustomFee({ feeData, min })
45
+ const max = calculateMaxCustomFee({ feeData, recommended })
16
46
  return {
17
- recommended: calculateFeeUnitPrice(gasPrice),
18
- min: calculateFeeUnitPrice(gasPrice, gasPriceMinimumRate),
19
- max: calculateFeeUnitPrice(gasPrice, gasPriceMaximumRate),
47
+ recommended: recommended.toNumber(Gwei),
48
+ min: min.toNumber(Gwei),
49
+ max: max.toNumber(Gwei),
20
50
  }
21
51
  },
22
52
  unit: 'gwei/gas',
package/src/fee-utils.js CHANGED
@@ -41,9 +41,7 @@ export const rewriteFeeConfig = ({ feeAsset, feeConfig, feeData }) => {
41
41
  export const resolveGasPrice = ({ feeData }) => {
42
42
  assert(feeData, 'feeData is required')
43
43
 
44
- const eip1559Enabled = feeData.eip1559Enabled
45
-
46
- if (eip1559Enabled && feeData.useBaseGasPrice) {
44
+ if (feeData.eip1559Enabled) {
47
45
  return feeData.tipGasPrice
48
46
  ? feeData.baseFeePerGas.add(feeData.tipGasPrice)
49
47
  : feeData.baseFeePerGas
@@ -51,3 +49,8 @@ export const resolveGasPrice = ({ feeData }) => {
51
49
 
52
50
  return feeData.gasPrice
53
51
  }
52
+
53
+ export const ensureSaneEip1559GasPriceForTipGasPrice = ({ gasPrice, tipGasPrice }) => {
54
+ if (!tipGasPrice || tipGasPrice.lt(gasPrice)) return gasPrice
55
+ return tipGasPrice
56
+ }
package/src/get-fee.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { calculateBumpedGasPrice, calculateExtraEth } from '@exodus/ethereum-lib'
2
2
 
3
- import { resolveGasPrice } from './fee-utils.js'
3
+ import { ensureSaneEip1559GasPriceForTipGasPrice, resolveGasPrice } from './fee-utils.js'
4
4
 
5
5
  // Move to meta?
6
6
  const taxes = {
@@ -44,20 +44,20 @@ export const getFeeFactory =
44
44
  feeData = { ...feeData, tipGasPrice: customFee }
45
45
  }
46
46
 
47
- const { baseFeePerGas, tipGasPrice, useBaseGasPrice } = feeData
47
+ const { baseFeePerGas, tipGasPrice } = feeData
48
48
 
49
49
  let gasPrice = customFee || resolveGasPrice({ feeData })
50
50
 
51
51
  // The `gasPrice` must be at least the `tipGasPrice`.
52
- if (eip1559Enabled && tipGasPrice && gasPrice.lt(tipGasPrice)) {
53
- gasPrice = tipGasPrice
52
+ if (eip1559Enabled) {
53
+ gasPrice = ensureSaneEip1559GasPriceForTipGasPrice({ gasPrice, tipGasPrice })
54
54
  }
55
55
 
56
56
  const gasLimit = providedGasLimit || asset.gasLimit || defaultGasLimit
57
57
 
58
58
  // When explicitly opting into EIP-1559 transactions,
59
59
  // lock in the `tipGasPrice` we used to compute the fees.
60
- const maybeReturnTipGasPrice = eip1559Enabled && useBaseGasPrice ? { tipGasPrice } : null
60
+ const maybeReturnTipGasPrice = eip1559Enabled ? { tipGasPrice } : null
61
61
 
62
62
  const extraFeeData = getExtraFeeData({ asset, amount })
63
63
  if (calculateEffectiveFee && eip1559Enabled) {
@@ -78,26 +78,15 @@ export const getFeeFactory =
78
78
  return { ...maybeReturnTipGasPrice, fee, gasPrice, extraFeeData }
79
79
  }
80
80
 
81
- // Used in BE
82
- export const getEffectiveGasPrice = ({ feeData }) => {
83
- const { baseFeePerGas, tipGasPrice, gasPrice: maxFeePerGas, eip1559Enabled } = feeData
84
-
85
- if (!eip1559Enabled) {
86
- return maxFeePerGas
87
- }
88
-
89
- const gasPrice = baseFeePerGas.add(tipGasPrice)
90
- return gasPrice.lt(maxFeePerGas) ? gasPrice : maxFeePerGas
91
- }
92
-
93
81
  // Used in Mobile
94
82
  export const getExtraFeeForBump = ({ tx, feeData, balance, unconfirmedBalance }) => {
95
83
  if (!balance || !unconfirmedBalance) return null
96
- const { gasPrice: currentGasPrice, eip1559Enabled } = feeData
84
+ const { gasPrice: currentGasPrice, eip1559Enabled, baseFeePerGas: currentBaseFee } = feeData
97
85
  const { bumpedGasPrice } = calculateBumpedGasPrice({
98
86
  baseAsset: 'ethereum',
99
87
  tx,
100
88
  currentGasPrice,
89
+ currentBaseFee,
101
90
  eip1559Enabled,
102
91
  })
103
92
  return calculateExtraEth({
@@ -18,15 +18,6 @@ const SLOT_ACTIVATION_PROXIMITY_ETH = '1.0'
18
18
  // ratio commonly fall under this limit.
19
19
  const MINIMUM_DELEGATION_GAS_LIMIT = 180_000
20
20
 
21
- // Use a custom `maxPriorityFeePerGas` so we can
22
- // respect the default EIP-1559 relationship:
23
- // maxFeePerGas = baseFeePerGas + maxPriorityFeePerGas.
24
- //
25
- // Currently, the backend will return an excessive
26
- // `tipGasPrice` which hinders the configuration ofrational
27
- // inclusion incentives when defined using remote config.
28
- const MAX_PRIORITY_FEE_PER_GAS = '0.06 Gwei' // semi-urgent
29
-
30
21
  const EXTRA_GAS_LIMIT = 20_000 // extra gas Limit to prevent tx failing if something change on pool state (till tx is in mempool)
31
22
 
32
23
  const DISABLE_BALANCE_CHECKS = '0x0'
@@ -35,9 +26,9 @@ export function createEthereumStakingService({
35
26
  asset,
36
27
  assetClientInterface,
37
28
  createWatchTx = defaultCreateWatch,
29
+ stakingProvider = stakingProviderClientFactory(),
38
30
  }) {
39
- const staking = new EthereumStaking(asset)
40
- const stakingProvider = stakingProviderClientFactory()
31
+ const staking = new EthereumStaking(asset, undefined, asset.server)
41
32
  const minAmount = staking.minAmount
42
33
 
43
34
  async function delegate({ walletAccount, amount } = Object.create(null)) {
@@ -360,7 +351,6 @@ export function createEthereumStakingService({
360
351
  amount,
361
352
  asset,
362
353
  assetClientInterface,
363
- maxPriorityFeePerGas: MAX_PRIORITY_FEE_PER_GAS,
364
354
  gasLimit,
365
355
  })
366
356
 
@@ -8,7 +8,6 @@ import { amountToCurrency, getEvmStakingServiceFee } from '../utils/index.js'
8
8
  import { MaticStakingApi } from './api.js'
9
9
 
10
10
  const DISABLE_BALANCE_CHECKS = '0x0'
11
- const MAX_PRIORITY_FEE_PER_GAS = '0.06 Gwei' // semi-urgent
12
11
 
13
12
  export function createPolygonStakingService({
14
13
  assetClientInterface,
@@ -258,7 +257,6 @@ export function createPolygonStakingService({
258
257
  amount,
259
258
  asset: ethereum,
260
259
  assetClientInterface,
261
- maxPriorityFeePerGas: MAX_PRIORITY_FEE_PER_GAS,
262
260
  gasLimit,
263
261
  })
264
262
  }
@@ -29,45 +29,15 @@ function maybeNormalizeFeeData({ asset, feeData }) {
29
29
  * to specify a custom `maxPriorityFeePerGas` whilst the current
30
30
  * `tipGasPrice` is incompatible with EIP-1559 pricing.
31
31
  */
32
- export async function getEvmStakingServiceFee({
33
- amount,
34
- asset,
35
- assetClientInterface,
36
- maxPriorityFeePerGas: maybeMaxPriorityFeePerGas,
37
- gasLimit,
38
- }) {
39
- const defaultFeeData = await assetClientInterface.getFeeData({
40
- assetName: asset.name,
41
- })
42
-
43
- // HACK: We wish to explicitly enable `useBaseGasPrice`
44
- // since we're using a custom `tipGasPrice`. This
45
- // will be compatible during the transition from
46
- // Magnifier to Clarity, and can be safely removed
47
- // once `useBaseGasPrice` is the default behaviour
48
- // (alongside the custom tip).
49
- const { eip1559Enabled } = defaultFeeData
50
- const useBaseGasPrice = Boolean(eip1559Enabled) && Boolean(maybeMaxPriorityFeePerGas)
51
-
52
- const normalizedFeeData = maybeNormalizeFeeData({
32
+ export const getEvmStakingServiceFee = async ({ amount, asset, assetClientInterface, gasLimit }) =>
33
+ asset.api.getFee({
53
34
  asset,
54
- feeData: {
55
- ...defaultFeeData,
56
- // HACK: The backend currently exports a very large `tipGasPrice` that is
57
- // compatible with Magnifier's legacy `gasPrice`, but it would
58
- // be incompatible with EIP-1559, since this would evaluate into
59
- // a very large `maxPriorityFeePerGas`.
60
- ...(useBaseGasPrice ? { tipGasPrice: maybeMaxPriorityFeePerGas } : null),
61
- useBaseGasPrice,
62
- },
63
- })
64
-
65
- // Returns `fee`, `gasPrice`, `extraFeeData`, and `tipGasPrice`
66
- // if the config defines we should `useBaseGasPrice`.
67
- return asset.api.getFee({
68
- asset,
69
- feeData: normalizedFeeData,
35
+ feeData: maybeNormalizeFeeData({
36
+ asset,
37
+ feeData: await assetClientInterface.getFeeData({
38
+ assetName: asset.name,
39
+ }),
40
+ }),
70
41
  gasLimit,
71
42
  amount,
72
43
  })
73
- }
@@ -1,3 +1,4 @@
1
+ import { ensureSaneEip1559GasPriceForTipGasPrice } from '../fee-utils.js'
1
2
  import { fetchGasLimit } from '../gas-estimation.js'
2
3
 
3
4
  const getFeeInfo = async function getFeeInfo({
@@ -9,7 +10,11 @@ const getFeeInfo = async function getFeeInfo({
9
10
  txInput,
10
11
  feeOpts = {},
11
12
  }) {
12
- const { gasPrice: gasPrice_, tipGasPrice: tipGasPrice_ } = await assetClientInterface.getFeeData({
13
+ const {
14
+ gasPrice: gasPrice_,
15
+ tipGasPrice: tipGasPrice_,
16
+ eip1559Enabled,
17
+ } = await assetClientInterface.getFeeData({
13
18
  assetName: asset.name,
14
19
  })
15
20
 
@@ -26,6 +31,13 @@ const getFeeInfo = async function getFeeInfo({
26
31
  })
27
32
  }
28
33
 
34
+ // HACK: If we've received an invalid combination of `tipGasPrice`
35
+ // (maxPriorityFeePerGas) and `gasPrice` (maxFeePerGas), then
36
+ // we must normalize these before returning.
37
+ if (eip1559Enabled) {
38
+ gasPrice = ensureSaneEip1559GasPriceForTipGasPrice({ gasPrice, tipGasPrice })
39
+ }
40
+
29
41
  return { gasPrice, gasLimit, tipGasPrice }
30
42
  }
31
43
 
@@ -117,31 +117,43 @@ const txSendFactory = ({ assetClientInterface, createUnsignedTx }) => {
117
117
  let replacedTx, replacedTokenTx
118
118
  if (bumpTxId) {
119
119
  replacedTx = baseAssetTxLog.get(bumpTxId)
120
+
120
121
  if (!replacedTx || !replacedTx.pending) {
121
122
  throw new Error(`Cannot bump transaction ${bumpTxId}: not found or confirmed`)
122
123
  }
123
124
 
124
125
  if (replacedTx.tokens.length > 0) {
126
+ const [tokenAssetName] = replacedTx.tokens
125
127
  const tokenTxSet = await assetClientInterface.getTxLog({
126
- assetName: replacedTx.tokens[0],
128
+ assetName: tokenAssetName,
127
129
  walletAccount,
128
130
  })
129
131
  replacedTokenTx = tokenTxSet.get(bumpTxId)
130
132
 
131
133
  if (replacedTokenTx) {
132
- asset = assets[replacedTx.tokens[0]]
134
+ // Attempt to overwrite the asset to reflect the fact that
135
+ // we're performing a token transaction.
136
+ asset = assets[tokenAssetName]
137
+ if (!asset) {
138
+ console.warn(
139
+ `unable to find ${tokenAssetName} during token bump transaction: asset was not available in assetsForNetwork`
140
+ )
141
+ }
133
142
  }
143
+
144
+ // TODO: Should we `throw` if we can't find the asset?
134
145
  }
135
146
 
136
147
  address = (replacedTokenTx || replacedTx).to
137
148
  amount = (replacedTokenTx || replacedTx).coinAmount.negate()
138
149
  feeOpts.gasLimit = replacedTx.data.gasLimit
139
150
 
140
- const { gasPrice: currentGasPrice } = feeData
151
+ const { gasPrice: currentGasPrice, baseFeePerGas: currentBaseFee } = feeData
141
152
  const { bumpedGasPrice, bumpedTipGasPrice } = calculateBumpedGasPrice({
142
153
  baseAsset,
143
154
  tx: replacedTx,
144
155
  currentGasPrice,
156
+ currentBaseFee,
145
157
  eip1559Enabled,
146
158
  })
147
159
  feeOpts.gasPrice = bumpedGasPrice
@@ -174,6 +186,7 @@ const txSendFactory = ({ assetClientInterface, createUnsignedTx }) => {
174
186
  isSendAll,
175
187
  createUnsignedTx,
176
188
  }
189
+
177
190
  let { txId, rawTx, nonce, gasLimit, tipGasPrice, gasPrice } = await createTx(createTxParams)
178
191
 
179
192
  const feeAmount = gasPrice.mul(gasLimit)