@exodus/ethereum-api 8.34.3 → 8.34.5

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,16 +3,6 @@
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
-
16
6
  ## [8.34.2](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.34.1...@exodus/ethereum-api@8.34.2) (2025-04-18)
17
7
 
18
8
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/ethereum-api",
3
- "version": "8.34.3",
3
+ "version": "8.34.5",
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",
@@ -49,7 +49,7 @@
49
49
  "ws": "^6.1.0"
50
50
  },
51
51
  "devDependencies": {
52
- "@exodus/assets-testing": "^1.0.0",
52
+ "@exodus/assets-testing": "workspace:^",
53
53
  "@exodus/bsc-meta": "^2.1.2",
54
54
  "@exodus/ethereumarbone-meta": "^2.0.3",
55
55
  "@exodus/ethereumgoerli-meta": "^2.0.1",
@@ -63,6 +63,5 @@
63
63
  "repository": {
64
64
  "type": "git",
65
65
  "url": "git+https://github.com/ExodusMovement/assets.git"
66
- },
67
- "gitHead": "5513269ef920bfdd91701a8108fe73336d105866"
66
+ }
68
67
  }
@@ -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 { getFeeFactory } from './get-fee.js'
31
+ import { getEffectiveGasPrice, 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,6 +323,7 @@ export const createAssetFactory = ({
323
323
  server,
324
324
  ...(erc20FuelBuffer && { erc20FuelBuffer }),
325
325
  ...(fuelThreshold && { fuelThreshold: asset.currency.defaultUnit(fuelThreshold) }),
326
+ getEffectiveGasPrice,
326
327
  }
327
328
  return overrideCallback({
328
329
  asset: fullAsset,
@@ -2,51 +2,21 @@ 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
-
39
5
  export const createCustomFeesApi = ({ baseAsset }) => {
40
6
  const Gwei = baseAsset.currency.units.Gwei
41
7
  return {
42
8
  getRecommendedMinMaxFeeUnitPrices: ({ feeData }) => {
43
- const min = calculateMinCustomFee({ feeData })
44
- const recommended = calculateRecommendedCustomFee({ feeData, min })
45
- const max = calculateMaxCustomFee({ feeData, recommended })
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
+
46
16
  return {
47
- recommended: recommended.toNumber(Gwei),
48
- min: min.toNumber(Gwei),
49
- max: max.toNumber(Gwei),
17
+ recommended: calculateFeeUnitPrice(gasPrice),
18
+ min: calculateFeeUnitPrice(gasPrice, gasPriceMinimumRate),
19
+ max: calculateFeeUnitPrice(gasPrice, gasPriceMaximumRate),
50
20
  }
51
21
  },
52
22
  unit: 'gwei/gas',
package/src/fee-utils.js CHANGED
@@ -41,7 +41,9 @@ export const rewriteFeeConfig = ({ feeAsset, feeConfig, feeData }) => {
41
41
  export const resolveGasPrice = ({ feeData }) => {
42
42
  assert(feeData, 'feeData is required')
43
43
 
44
- if (feeData.eip1559Enabled) {
44
+ const eip1559Enabled = feeData.eip1559Enabled
45
+
46
+ if (eip1559Enabled && feeData.useBaseGasPrice) {
45
47
  return feeData.tipGasPrice
46
48
  ? feeData.baseFeePerGas.add(feeData.tipGasPrice)
47
49
  : feeData.baseFeePerGas
@@ -49,8 +51,3 @@ export const resolveGasPrice = ({ feeData }) => {
49
51
 
50
52
  return feeData.gasPrice
51
53
  }
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 { ensureSaneEip1559GasPriceForTipGasPrice, resolveGasPrice } from './fee-utils.js'
3
+ import { 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 } = feeData
47
+ const { baseFeePerGas, tipGasPrice, useBaseGasPrice } = 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) {
53
- gasPrice = ensureSaneEip1559GasPriceForTipGasPrice({ gasPrice, tipGasPrice })
52
+ if (eip1559Enabled && tipGasPrice && gasPrice.lt(tipGasPrice)) {
53
+ 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 ? { tipGasPrice } : null
60
+ const maybeReturnTipGasPrice = eip1559Enabled && useBaseGasPrice ? { tipGasPrice } : null
61
61
 
62
62
  const extraFeeData = getExtraFeeData({ asset, amount })
63
63
  if (calculateEffectiveFee && eip1559Enabled) {
@@ -78,15 +78,26 @@ 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
+
81
93
  // Used in Mobile
82
94
  export const getExtraFeeForBump = ({ tx, feeData, balance, unconfirmedBalance }) => {
83
95
  if (!balance || !unconfirmedBalance) return null
84
- const { gasPrice: currentGasPrice, eip1559Enabled, baseFeePerGas: currentBaseFee } = feeData
96
+ const { gasPrice: currentGasPrice, eip1559Enabled } = feeData
85
97
  const { bumpedGasPrice } = calculateBumpedGasPrice({
86
98
  baseAsset: 'ethereum',
87
99
  tx,
88
100
  currentGasPrice,
89
- currentBaseFee,
90
101
  eip1559Enabled,
91
102
  })
92
103
  return calculateExtraEth({
@@ -18,6 +18,15 @@ 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
+
21
30
  const EXTRA_GAS_LIMIT = 20_000 // extra gas Limit to prevent tx failing if something change on pool state (till tx is in mempool)
22
31
 
23
32
  const DISABLE_BALANCE_CHECKS = '0x0'
@@ -26,9 +35,9 @@ export function createEthereumStakingService({
26
35
  asset,
27
36
  assetClientInterface,
28
37
  createWatchTx = defaultCreateWatch,
29
- stakingProvider = stakingProviderClientFactory(),
30
38
  }) {
31
- const staking = new EthereumStaking(asset, undefined, asset.server)
39
+ const staking = new EthereumStaking(asset)
40
+ const stakingProvider = stakingProviderClientFactory()
32
41
  const minAmount = staking.minAmount
33
42
 
34
43
  async function delegate({ walletAccount, amount } = Object.create(null)) {
@@ -351,6 +360,7 @@ export function createEthereumStakingService({
351
360
  amount,
352
361
  asset,
353
362
  assetClientInterface,
363
+ maxPriorityFeePerGas: MAX_PRIORITY_FEE_PER_GAS,
354
364
  gasLimit,
355
365
  })
356
366
 
@@ -8,6 +8,7 @@ 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
11
12
 
12
13
  export function createPolygonStakingService({
13
14
  assetClientInterface,
@@ -257,6 +258,7 @@ export function createPolygonStakingService({
257
258
  amount,
258
259
  asset: ethereum,
259
260
  assetClientInterface,
261
+ maxPriorityFeePerGas: MAX_PRIORITY_FEE_PER_GAS,
260
262
  gasLimit,
261
263
  })
262
264
  }
@@ -29,15 +29,45 @@ 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 const getEvmStakingServiceFee = async ({ amount, asset, assetClientInterface, gasLimit }) =>
33
- asset.api.getFee({
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({
34
53
  asset,
35
- feeData: maybeNormalizeFeeData({
36
- asset,
37
- feeData: await assetClientInterface.getFeeData({
38
- assetName: asset.name,
39
- }),
40
- }),
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,
41
70
  gasLimit,
42
71
  amount,
43
72
  })
73
+ }
@@ -1,4 +1,3 @@
1
- import { ensureSaneEip1559GasPriceForTipGasPrice } from '../fee-utils.js'
2
1
  import { fetchGasLimit } from '../gas-estimation.js'
3
2
 
4
3
  const getFeeInfo = async function getFeeInfo({
@@ -10,11 +9,7 @@ const getFeeInfo = async function getFeeInfo({
10
9
  txInput,
11
10
  feeOpts = {},
12
11
  }) {
13
- const {
14
- gasPrice: gasPrice_,
15
- tipGasPrice: tipGasPrice_,
16
- eip1559Enabled,
17
- } = await assetClientInterface.getFeeData({
12
+ const { gasPrice: gasPrice_, tipGasPrice: tipGasPrice_ } = await assetClientInterface.getFeeData({
18
13
  assetName: asset.name,
19
14
  })
20
15
 
@@ -31,13 +26,6 @@ const getFeeInfo = async function getFeeInfo({
31
26
  })
32
27
  }
33
28
 
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
-
41
29
  return { gasPrice, gasLimit, tipGasPrice }
42
30
  }
43
31
 
@@ -4,7 +4,7 @@ import { calculateBumpedGasPrice, isEthereumLikeToken, normalizeTxId } from '@ex
4
4
  import assert from 'minimalistic-assert'
5
5
 
6
6
  import * as ErrorWrapper from '../error-wrapper.js'
7
- import { transactionExists } from '../eth-like-util.js'
7
+ import { isContractAddressCached, transactionExists } from '../eth-like-util.js'
8
8
  import { getNftArguments } from '../nft-utils.js'
9
9
  import getFeeInfo from './get-fee-info.js'
10
10
  import { resolveNonce } from './nonce-utils.js'
@@ -117,43 +117,31 @@ const txSendFactory = ({ assetClientInterface, createUnsignedTx }) => {
117
117
  let replacedTx, replacedTokenTx
118
118
  if (bumpTxId) {
119
119
  replacedTx = baseAssetTxLog.get(bumpTxId)
120
-
121
120
  if (!replacedTx || !replacedTx.pending) {
122
121
  throw new Error(`Cannot bump transaction ${bumpTxId}: not found or confirmed`)
123
122
  }
124
123
 
125
124
  if (replacedTx.tokens.length > 0) {
126
- const [tokenAssetName] = replacedTx.tokens
127
125
  const tokenTxSet = await assetClientInterface.getTxLog({
128
- assetName: tokenAssetName,
126
+ assetName: replacedTx.tokens[0],
129
127
  walletAccount,
130
128
  })
131
129
  replacedTokenTx = tokenTxSet.get(bumpTxId)
132
130
 
133
131
  if (replacedTokenTx) {
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
- }
132
+ asset = assets[replacedTx.tokens[0]]
142
133
  }
143
-
144
- // TODO: Should we `throw` if we can't find the asset?
145
134
  }
146
135
 
147
136
  address = (replacedTokenTx || replacedTx).to
148
137
  amount = (replacedTokenTx || replacedTx).coinAmount.negate()
149
138
  feeOpts.gasLimit = replacedTx.data.gasLimit
150
139
 
151
- const { gasPrice: currentGasPrice, baseFeePerGas: currentBaseFee } = feeData
140
+ const { gasPrice: currentGasPrice } = feeData
152
141
  const { bumpedGasPrice, bumpedTipGasPrice } = calculateBumpedGasPrice({
153
142
  baseAsset,
154
143
  tx: replacedTx,
155
144
  currentGasPrice,
156
- currentBaseFee,
157
145
  eip1559Enabled,
158
146
  })
159
147
  feeOpts.gasPrice = bumpedGasPrice
@@ -186,7 +174,6 @@ const txSendFactory = ({ assetClientInterface, createUnsignedTx }) => {
186
174
  isSendAll,
187
175
  createUnsignedTx,
188
176
  }
189
-
190
177
  let { txId, rawTx, nonce, gasLimit, tipGasPrice, gasPrice } = await createTx(createTxParams)
191
178
 
192
179
  const feeAmount = gasPrice.mul(gasLimit)
@@ -363,7 +350,16 @@ const createTx = async ({
363
350
  feeOpts,
364
351
  })
365
352
 
366
- const isSendAllBaseAsset = isSendAll && !isToken
353
+ const isContractToAddress = await isContractAddressCached({ asset, address: toAddress })
354
+
355
+ // HACK: We cannot ensure the no dust invariant for `isSendAll`
356
+ // transactions to contract addresses, since we may be
357
+ // performing a raw token transaction and the parameter
358
+ // applies to the token and not the native amount.
359
+ //
360
+ // Contracts have nondeterministic gas most of the time
361
+ // versus estimations, anyway.
362
+ const isSendAllBaseAsset = isSendAll && !isToken && !isContractToAddress
367
363
 
368
364
  if (eip1559Enabled) {
369
365
  if (customGasPrice) {