@exodus/ethereum-api 8.28.0 → 8.29.0

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,30 @@
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.29.0](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.28.1...@exodus/ethereum-api@8.29.0) (2025-02-06)
7
+
8
+
9
+ ### Features
10
+
11
+
12
+ * feat: improve (max)gasPrice resolution using baseFeePerGas (#4984)
13
+
14
+
15
+
16
+ ## [8.28.1](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.28.0...@exodus/ethereum-api@8.28.1) (2025-02-03)
17
+
18
+
19
+ ### Bug Fixes
20
+
21
+
22
+ * fix: add ETH stakeable property (#4917)
23
+
24
+ * fix: add optional chaining to tx references (#4948)
25
+
26
+ * fix: ETH activity txs field (#4915)
27
+
28
+
29
+
6
30
  ## [8.28.0](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.27.0...@exodus/ethereum-api@8.28.0) (2025-01-23)
7
31
 
8
32
 
package/README.md CHANGED
@@ -8,12 +8,12 @@ This section explains how fees currently work in Exodus and provides guidance on
8
8
 
9
9
  ### Gas Price
10
10
 
11
- Excluding custom fees, the result `gasPrice * gasPriceMultiplier` is used to resolve the transaction's gas price (legacy) and max gas price (EIP-1559).
11
+ Excluding custom fees, `feeData.gasPrice` is used to resolve the transaction's gas price (legacy) and max gas price (EIP-1559).
12
12
 
13
13
  The `gasPrice` value is, in preference order, loaded from:
14
14
 
15
15
  1. Remote config
16
- 2. The node via the fee data monitor
16
+ 2. The node via the fee data monitor and web socket. `gasPrice = baseFeePerGas * gasPriceMultipler + tipGasPrice`
17
17
  3. The default configuration.
18
18
 
19
19
  Using remote config is never recommended, as manually updating this value won't keep up with network activity.
@@ -26,7 +26,7 @@ To tune the fee resolution, we can:
26
26
  - Set a `min` value via remote config. Gas price will never be lower than this value.
27
27
  - Set a `max` value via remote config. Gas price will never be higher than this value.
28
28
 
29
- If the `gasPrice` returned by the node is not accurate or lower than the network's, the transaction may get stuck in pending status or may not be broadcast to the blockchain.
29
+ If the resolved `gasPrice` is not accurate or lower than the network's, the transaction may get stuck in pending status or may not be broadcast to the blockchain.
30
30
 
31
31
  This is an undesirable situation:
32
32
 
@@ -42,9 +42,7 @@ The base gas price (or base fee) is the minimum amount of gas required to be pai
42
42
 
43
43
  It can be found in `feeData.baseFeePerGas`. The fee monitor keeps it up to date by monitoring the latest block's information. It could be tuned by remote config, but this is not recommended.
44
44
 
45
- The `feeData.baseFeePerGas` is only used when showing an "estimated fee" in the UI and not when sending a transaction.
46
-
47
- One possible improvement is to define the max gas price based on the base gas price rather than the network's current gas price.
45
+ The `feeData.baseFeePerGas` is used when showing an "estimated fee" in the UI and to calculate the max gas price. `gasPrice = baseFeePerGas * gasPriceMultipler + tipGasPrice`
48
46
 
49
47
  ### Gas Limit
50
48
 
@@ -71,16 +69,18 @@ The L2 extra fee needs to be considered when showing the fee to the user, resolv
71
69
 
72
70
  Custom fees allow users to set a custom `gasPrice`. The wallet allows a range, as follows:
73
71
 
74
- - `recommended: gasPrice * gasPriceMultiplier`
75
- - `min: gasPrice * gasPriceMultiplier * gasPriceMinimumRate`
76
- - `max: gasPrice * gasPriceMultiplier * gasPriceMaximumRate`
72
+ Note: `gasPrice = baseFeePerGas * gasPriceMultipler + tipGasPrice`
73
+
74
+ - `recommended: gasPrice`
75
+ - `min: gasPrice * gasPriceMinimumRate`
76
+ - `max: gasPrice * gasPriceMaximumRate`
77
77
 
78
78
  If the user picks the recommended value, the custom fee is not set, leaving the default fee behavior.
79
79
  Custom fees can be disabled by setting `rbfEnabled: false` via remote config. Currently, only Ethereum allows custom fees. Other EVMs could be included in the future.
80
80
 
81
81
  The wallet allows acceleration when custom fees are allowed (and vice versa).
82
82
 
83
- If the user picks a different custom fee in that range, the fee is calculated as `customFee * gasLimit`. `gasPriceMultiplier` is not applied to the custom gas price.
83
+ If the user picks a different custom fee in that range, the fee is calculated as `customFee * gasLimit`.
84
84
 
85
85
  A transaction created with a low custom fee may be stuck for a while, blocking future transactions, and it may create issues with the nonce, as explained above.
86
86
 
@@ -88,16 +88,18 @@ Min and max values can be tuned by changing `gasPriceMinimumRate` and `gasPriceM
88
88
 
89
89
  ### TX formulas
90
90
 
91
+ Note: `gasPrice = baseFeePerGas * gasPriceMultipler + tipGasPrice`
92
+
91
93
  Table shows how the txs are create for both legacy and 1559 logic for major use case:
92
94
 
93
- | | Send some main | Send all main | Send token/contract call | Send main with custom fee | Send token/contract with custom fee | Accelerate |
94
- | ------------------ | ----------------------------- | ----------------------------- | ----------------------------- | ------------------------- | ----------------------------------- | ------------------------------------------------------------ |
95
- | Legacy Gas Price | gasPrice \* gasPriceMultipler | gasPrice \* gasPriceMultipler | gasPrice \* gasPriceMultipler | customFee | customFee | max(current gasPrice,original tx gasPrice) \* BUMP_RATE 1.2 |
96
- | Legacy Gas Limit | 21000 | 21000 | estimated + 20% extra | 21000 | estimated + 20% extra | original tx gasLimit |
97
- | - | - | - | - | - | - | - |
98
- | 1559 Max gas Price | gasPrice \* gasPriceMultipler | gasPrice \* gasPriceMultipler | gasPrice \* gasPriceMultipler | customFee | customFee | max(current gasPrice, original tx gasPrice) \* BUMP_RATE 1.2 |
99
- | 1559 Tip gas Price | tipGasPrice | gasPrice \* gasPriceMultipler | tipGasPrice | customFee | tipGasPrice | orginal tx tipGasPrice \* BUMP_RATE 1.2 |
100
- | 1559 Gas Limit | 21000 | 21000 | estimated + 20% extra | 21000 | estimated + 20% extra | original tx gasLimit |
95
+ | | Send some main | Send all main | Send token/contract call | Send main with custom fee | Send token/contract with custom fee | Accelerate |
96
+ | ------------------ | -------------- | ------------- | ------------------------ | ------------------------- | ----------------------------------- | ------------------------------------------------------------ |
97
+ | Legacy Gas Price | gasPrice | gasPrice | gasPrice | customFee | customFee | max(current gasPrice,original tx gasPrice) \* BUMP_RATE 1.2 |
98
+ | Legacy Gas Limit | 21000 | 21000 | estimated + 20% extra | 21000 | estimated + 20% extra | original tx gasLimit |
99
+ | - | - | - | - | - | - | - |
100
+ | 1559 Max gas Price | gasPrice | gasPrice | gasPrice | customFee | customFee | max(current gasPrice, original tx gasPrice) \* BUMP_RATE 1.2 |
101
+ | 1559 Tip gas Price | tipGasPrice | gasPrice | tipGasPrice | customFee | tipGasPrice | orginal tx tipGasPrice \* BUMP_RATE 1.2 |
102
+ | 1559 Gas Limit | 21000 | 21000 | estimated + 20% extra | 21000 | estimated + 20% extra | original tx gasLimit |
101
103
 
102
104
  ### Fee Data Example
103
105
 
@@ -106,7 +108,8 @@ This is the default fee data configuration for Ethereum. Other EVMs have differe
106
108
  All these values can be tuned via remote config, although it may not be recommended as they are also tuned by the fee monitors:
107
109
 
108
110
  - **`baseFeePerGas`:** `50 Gwei` – Reference value for the base gas price. It is quickly updated by the fee monitor.
109
- - **`gasPrice`:** `75 Gwei` – Reference value for the network gas price. It is quickly updated by the fee monitor.
111
+ - **`gasPrice`:** `77 Gwei` – `baseFeePerGas * gasPriceMultipler + tipGasPrice`
112
+ - **`severGasPrice`:** `75 Gwei` – Reference value for the network gas price. It is quickly updated by the fee monitor.
110
113
  - **`tipGasPrice`:** `2 Gwei` – Controls the `maxPriorityFeePerGas` for all transactions when EIP-1559 is enabled.
111
114
  - **`eip1559Enabled`:** `true` – Enables or disables EIP-1559. A value of `false` means legacy fees and transactions.
112
115
  - **`rbfEnabled`:** `true` – Enables custom fees and acceleration. Currently, this is only available for Ethereum but can be expanded to other EVMs.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/ethereum-api",
3
- "version": "8.28.0",
3
+ "version": "8.29.0",
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": "1f116146a725da6f3c98abdf233261512a6333fc"
67
+ "gitHead": "45145f8d82dcee20192872aae962f9c1e1bfc487"
68
68
  }
@@ -143,9 +143,9 @@ export const createAssetFactory = ({
143
143
  (isWebSocket ? null : DEFAULT_FEE_MONITOR_INTERVAL) // null means do not start the timer
144
144
 
145
145
  const createFeeMonitor = serverBasedFeeMonitorFactoryFactory({
146
- assetName: base.name,
146
+ asset: base,
147
147
  interval: resolvedFeeMonitorInterval,
148
- eip1559Enabled: feeData.eip1559Enabled, // this is not updated via remote config. Should it be?
148
+ aci: assetClientInterface,
149
149
  server,
150
150
  })
151
151
 
@@ -4,18 +4,18 @@ export const createCustomFeesApi = ({ baseAsset }) => {
4
4
  const Gwei = baseAsset.currency.units.Gwei
5
5
  return {
6
6
  getRecommendedMinMaxFeeUnitPrices: ({ feeData }) => {
7
- const { gasPrice, gasPriceMultiplier, gasPriceMinimumRate, gasPriceMaximumRate } = feeData
7
+ const { gasPrice, gasPriceMinimumRate, gasPriceMaximumRate } = feeData
8
8
 
9
9
  const calculateFeeUnitPrice = (
10
10
  feeUnitPrice,
11
11
  multiplier = 1,
12
12
  toAdd = baseAsset.currency.ZERO
13
- ) => feeUnitPrice.to('Gwei').mul(multiplier).add(toAdd).toNumber(Gwei)
13
+ ) => feeUnitPrice.mul(multiplier).add(toAdd).toNumber(Gwei)
14
14
 
15
15
  return {
16
- recommended: calculateFeeUnitPrice(gasPrice, gasPriceMultiplier),
17
- min: calculateFeeUnitPrice(gasPrice, gasPriceMinimumRate * gasPriceMultiplier),
18
- max: calculateFeeUnitPrice(gasPrice, gasPriceMaximumRate * gasPriceMultiplier),
16
+ recommended: calculateFeeUnitPrice(gasPrice),
17
+ min: calculateFeeUnitPrice(gasPrice, gasPriceMinimumRate),
18
+ max: calculateFeeUnitPrice(gasPrice, gasPriceMaximumRate),
19
19
  }
20
20
  },
21
21
  unit: 'gwei/gas',
@@ -0,0 +1,38 @@
1
+ import assert from 'minimalistic-assert'
2
+
3
+ export const resolveGasPrices = ({
4
+ asset,
5
+ feeData,
6
+ gasPrice: gasPriceString,
7
+ baseFeePerGas: baseFeePerGasString,
8
+ }) => {
9
+ assert(asset, 'asset is required')
10
+ assert(feeData, 'feeData is required')
11
+ assert(gasPriceString, 'gasPrice is required')
12
+
13
+ const zero = asset.currency.ZERO
14
+ const eip1559Enabled = feeData.eip1559Enabled
15
+ const gasPrice = asset.currency.parse(gasPriceString)
16
+
17
+ if (eip1559Enabled) {
18
+ assert(baseFeePerGasString, 'baseFeePerGasString is required')
19
+ const baseFeePerGas = asset.currency.parse(baseFeePerGasString)
20
+
21
+ const tipGasPrice = feeData.tipGasPrice ?? zero
22
+
23
+ const resolvedGasPrice = baseFeePerGas.mul(feeData.gasPriceMultiplier || 1).add(tipGasPrice)
24
+
25
+ return {
26
+ serverGasPrice: gasPriceString, // for reference
27
+ gasPrice: resolvedGasPrice.toBaseString({ unit: true }),
28
+ baseFeePerGas: baseFeePerGasString,
29
+ tipGasPrice: tipGasPrice.toBaseString({ unit: true }),
30
+ }
31
+ }
32
+
33
+ const resolvedGasPrice = gasPrice.mul(feeData.gasPriceMultiplier || 1)
34
+ return {
35
+ serverGasPrice: gasPriceString, // for reference
36
+ gasPrice: resolvedGasPrice.toBaseString({ unit: true }),
37
+ }
38
+ }
@@ -110,10 +110,13 @@ export const getBalancesFactory = ({ monitorType, config = Object.create(null) }
110
110
  total = spendable.add(staked).add(staking).add(unstaking).add(unstaked)
111
111
  }
112
112
 
113
+ const stakeable = spendable
114
+
113
115
  return {
114
116
  // new
115
117
  spendable,
116
118
  total,
119
+ stakeable,
117
120
  staked,
118
121
  staking,
119
122
  unstaking,
package/src/get-fee.js CHANGED
@@ -34,9 +34,7 @@ export const getFeeFactory =
34
34
  }) => {
35
35
  const { gasPrice: feeDataGasPrice, eip1559Enabled, baseFeePerGas, tipGasPrice } = feeData
36
36
 
37
- const gasPriceMultiplier = feeData.gasPriceMultiplier || 1
38
-
39
- const gasPrice = customFee || feeDataGasPrice.mul(gasPriceMultiplier)
37
+ const gasPrice = customFee || feeDataGasPrice
40
38
 
41
39
  const gasLimit = providedGasLimit || asset.gasLimit || defaultGasLimit
42
40
 
@@ -1,6 +1,7 @@
1
1
  import { FeeMonitor } from '@exodus/asset-lib'
2
2
  import assert from 'minimalistic-assert'
3
3
 
4
+ import { resolveGasPrices } from './fee-monitor-utils.js'
4
5
  import { fromHexToString } from './number-utils.js'
5
6
 
6
7
  /**
@@ -12,39 +13,34 @@ import { fromHexToString } from './number-utils.js'
12
13
  *
13
14
  * const api = {
14
15
  * ...
15
- * createFeeMonitor: serverBasedFeeMonitorFactoryFactory({ interval: '50s', server, assetName: base.name }),
16
+ * createFeeMonitor: serverBasedFeeMonitorFactoryFactory({ interval: '50s', server, asset, aci }),
16
17
  * ...
17
18
  * }
18
19
  */
19
20
 
20
- export const serverBasedFeeMonitorFactoryFactory = ({
21
- assetName,
22
- interval,
23
- eip1559Enabled,
24
- server,
25
- }) => {
26
- assert(assetName, 'assetName is required')
21
+ export const serverBasedFeeMonitorFactoryFactory = ({ asset, interval, server, aci }) => {
22
+ assert(asset, 'asset is required')
27
23
  assert(server, 'server is required')
24
+ assert(aci, 'aci is required')
28
25
 
29
26
  const FeeMonitorClass = class ServerBaseEthereumFeeMonitor extends FeeMonitor {
30
27
  constructor({ updateFee }) {
31
28
  assert(updateFee, 'updateFee is required')
32
29
  super({
33
30
  updateFee,
34
- assetName,
31
+ assetName: asset.name,
35
32
  interval,
36
33
  })
37
34
  }
38
35
 
39
36
  async fetchFee() {
40
- const gasPrice = fromHexToString(await server.getGasPrice())
41
-
42
- const baseFeePerGas = eip1559Enabled ? `${await server.getBaseFeePerGas()} wei` : undefined
43
-
44
- return {
45
- gasPrice: `${gasPrice} wei`,
46
- baseFeePerGas,
47
- }
37
+ const feeData = await aci.getFeeConfig({ assetName: asset.name })
38
+ const eip1559Enabled = feeData.eip1559Enabled
39
+ const [gasPrice, baseFeePerGas] = await Promise.all([
40
+ server.getGasPrice().then((wei) => `${fromHexToString(wei)} wei`),
41
+ eip1559Enabled ? server.getBaseFeePerGas().then((wei) => `${wei} wei`) : undefined,
42
+ ])
43
+ return resolveGasPrices({ asset, feeData, gasPrice, baseFeePerGas })
48
44
  }
49
45
  }
50
46
  return (...args) => new FeeMonitorClass(...args)
@@ -15,10 +15,10 @@ export const isPolygonTx = ({ coinName }) => coinName === 'polygon'
15
15
  export const isPolygonDelegate = (tx) =>
16
16
  isPolygonTx(tx) && tx.to === STAKING_MANAGER_CONTRACT && tx.data?.methodId === DELEGATE
17
17
  export const isPolygonUndelegate = (tx) =>
18
- isPolygonTx(tx) && tx.from[0] === STAKING_MANAGER_CONTRACT && tx.data?.methodId === UNDELEGATE
18
+ isPolygonTx(tx) && tx.from?.[0] === STAKING_MANAGER_CONTRACT && tx.data?.methodId === UNDELEGATE
19
19
  export const isPolygonReward = (tx) =>
20
- isPolygonTx(tx) && tx.from[0] === STAKING_MANAGER_CONTRACT && tx.data?.methodId === CLAIM_REWARD
20
+ isPolygonTx(tx) && tx.from?.[0] === STAKING_MANAGER_CONTRACT && tx.data?.methodId === CLAIM_REWARD
21
21
  export const isPolygonClaimUndelegate = (tx) =>
22
22
  isPolygonTx(tx) &&
23
- tx.from[0] === STAKING_MANAGER_CONTRACT &&
23
+ tx.from?.[0] === STAKING_MANAGER_CONTRACT &&
24
24
  tx.data?.methodId === CLAIM_UNDELEGATE_BALANCE
@@ -2,6 +2,7 @@ import { BaseMonitor } from '@exodus/asset-lib'
2
2
  import { getAssetAddresses, isRpcBalanceAsset } from '@exodus/ethereum-lib'
3
3
  import lodash from 'lodash'
4
4
 
5
+ import { resolveGasPrices } from '../fee-monitor-utils.js'
5
6
  import { fromHexToString } from '../number-utils.js'
6
7
  import { filterEffects, getLogItemsFromServerTx } from './clarity-utils/index.js'
7
8
  import {
@@ -264,14 +265,13 @@ export class ClarityMonitor extends BaseMonitor {
264
265
 
265
266
  async updateGasPrice({ gasPrice, baseFeePerGas }) {
266
267
  try {
267
- const feeConfig = { gasPrice: `${fromHexToString(gasPrice).toString()} wei` }
268
- if (baseFeePerGas) {
269
- feeConfig.baseFeePerGas = `${fromHexToString(baseFeePerGas)} wei`
270
- }
271
-
272
- this.logger.debug(
273
- `Update ${this.asset.name} gas price: ${feeConfig.gasPrice}, baseFeePerGas: ${feeConfig.baseFeePerGas}`
274
- )
268
+ const feeData = await this.aci.getFeeConfig({ assetName: this.asset.name })
269
+ const feeConfig = resolveGasPrices({
270
+ asset: this.asset,
271
+ feeData,
272
+ gasPrice: `${fromHexToString(gasPrice)} wei`,
273
+ baseFeePerGas: baseFeePerGas ? `${fromHexToString(baseFeePerGas)} wei` : undefined,
274
+ })
275
275
  await this.aci.updateFeeConfig({ assetName: this.asset.name, feeConfig })
276
276
  } catch (e) {
277
277
  this.logger.warn('error updating gasPrice', e)
@@ -2,6 +2,7 @@ import { BaseMonitor } from '@exodus/asset-lib'
2
2
  import { getAssetAddresses, isRpcBalanceAsset } from '@exodus/ethereum-lib'
3
3
  import lodash from 'lodash'
4
4
 
5
+ import { resolveGasPrices } from '../fee-monitor-utils.js'
5
6
  import { fromHexToString } from '../number-utils.js'
6
7
  import {
7
8
  checkPendingTransactions,
@@ -200,16 +201,13 @@ export class EthereumMonitor extends BaseMonitor {
200
201
 
201
202
  async updateGasPrice({ gasPrice, baseFeePerGas }) {
202
203
  try {
203
- const feeConfig = {
204
- gasPrice: `${parseInt(gasPrice, 16)} wei`,
205
- }
206
- if (baseFeePerGas) {
207
- feeConfig.baseFeePerGas = `${parseInt(baseFeePerGas, 16)} wei`
208
- }
209
-
210
- this.logger.debug(
211
- `Update ${this.asset.name} gas price: ${feeConfig.gasPrice}, baseFeePerGas: ${feeConfig.baseFeePerGas}`
212
- )
204
+ const feeData = await this.aci.getFeeConfig({ assetName: this.asset.name })
205
+ const feeConfig = resolveGasPrices({
206
+ asset: this.asset,
207
+ feeData,
208
+ gasPrice: `${fromHexToString(gasPrice)} wei`,
209
+ baseFeePerGas: baseFeePerGas ? `${fromHexToString(baseFeePerGas)} wei` : undefined,
210
+ })
213
211
  await this.aci.updateFeeConfig({ assetName: this.asset.name, feeConfig })
214
212
  } catch (e) {
215
213
  this.logger.warn('error updating gasPrice', e)
@@ -21,16 +21,16 @@ export const decodePolygonStakingTxInputAmount = (tx) => {
21
21
 
22
22
  export const calculateRewardsFromStakeTx = ({ tx, currency }) => {
23
23
  const stakedAmount = currency.baseUnit(decodePolygonStakingTxInputAmount(tx))
24
- const { reward } = tx.data
24
+ const { rewards } = tx.data
25
25
  // stake tx might have rewards in it,
26
26
  // i.e: https://etherscan.io/tx/0x0a81d266109034a3a70c6f1b9601c105d8caebbd0de652a0619344f9559ae4fa
27
27
  // thus reward = txInputAmount(amount to stake) - tx.coinAmount
28
28
  // tx.coinAmount is already computed from monitor; incoming - outgoing ERC20 token txs
29
29
  const stakeTxContainsReward = !stakedAmount.equals(tx.coinAmount.abs())
30
30
 
31
- if (reward) {
31
+ if (rewards) {
32
32
  // cache rewards
33
- return currency.baseUnit(reward)
33
+ return currency.baseUnit(rewards)
34
34
  }
35
35
 
36
36
  if (stakeTxContainsReward) {