@exodus/ethereum-lib 5.20.0 → 5.20.2

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,26 @@
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
+ ## [5.20.2](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-lib@5.20.1...@exodus/ethereum-lib@5.20.2) (2025-12-17)
7
+
8
+
9
+ ### Bug Fixes
10
+
11
+
12
+ * fix: prevent acceleration of replaced evm transactions (#6964)
13
+
14
+
15
+
16
+ ## [5.20.1](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-lib@5.20.0...@exodus/ethereum-lib@5.20.1) (2025-12-09)
17
+
18
+
19
+ ### Bug Fixes
20
+
21
+
22
+ * fix: EIP1559 acceleration gas calculation in (#6997). Update getLinearIncentiveBumpForEip1559Transaction() to properly calculate bump tx logic under eip1559
23
+
24
+
25
+
6
26
  ## [5.20.0](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-lib@5.18.5...@exodus/ethereum-lib@5.20.0) (2025-11-19)
7
27
 
8
28
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/ethereum-lib",
3
- "version": "5.20.0",
3
+ "version": "5.20.2",
4
4
  "description": "Ethereum utils, such as for cryptography, address encoding/decoding, transaction building, etc.",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -51,5 +51,5 @@
51
51
  "type": "git",
52
52
  "url": "git+https://github.com/ExodusMovement/assets.git"
53
53
  },
54
- "gitHead": "a15d8239871e44fd99d48a2b1a66606e2d428735"
54
+ "gitHead": "8c3e94c96818b1ce8ce10cb75da29db2c9fe9ab9"
55
55
  }
package/src/constants.js CHANGED
@@ -136,3 +136,6 @@ export const CONFIRMATIONS_NUMBER = mapValues(
136
136
  export const ETHEREUM_LIKE_ASSETS = Object.keys(CHAIN_DATA)
137
137
 
138
138
  export const BUMP_RATE = 1.2
139
+ // Node bump requirement (10% as enforced by Geth/Erigon/Nethermind)
140
+ // Client always bumps by 20% (BUMP_RATE) to exceed this threshold
141
+ export const NODE_BUMP_REQUIREMENT = 1.1
@@ -12,6 +12,57 @@ const BumpType = {
12
12
  RBF: 2,
13
13
  }
14
14
 
15
+ // NOTE: If a transaction was successfully included,
16
+ // it is the de-facto highest incentive
17
+ // transaction - irrespective of other
18
+ // attempts for that nonce.
19
+ const getHighestIncentiveTxByNonceForTxLog = ({ nonce, txLog }) => {
20
+ assert(txLog, 'expected txLog')
21
+
22
+ if (txLog.size === 0 || !Number.isInteger(nonce)) return
23
+
24
+ const txLogSendsByFeeAmountDesc = [...txLog]
25
+ .filter((tx) => tx.data.nonce === nonce && tx.sent)
26
+ .sort((a, b) => (a.feeAmount.gt(b.feeAmount) ? -1 : b.feeAmount.gt(a.feeAmount) ? 1 : 0))
27
+
28
+ // If any of the transactions competing for this `nonce`
29
+ // were successful, then we can return this transaction
30
+ // as it effectively had the highest game-theoretical
31
+ // incentive regardless of other (potentially higher fee)
32
+ // transactions that were sent.
33
+ //
34
+ // https://github.com/ExodusMovement/exodus-hydra/blob/e59004097f15974a975d14e1823de5d7b1c28308/features/activity-txs/redux/utils/activity-formatters/format-tx-activity.js#L13
35
+ const maybeConfirmedTx = txLogSendsByFeeAmountDesc.find((tx) => !tx.failed && !tx.pending)
36
+ if (maybeConfirmedTx) return maybeConfirmedTx
37
+
38
+ // https://github.com/ExodusMovement/assets/blob/fbe3702861cba3b21885a65b15f038fcd8541891/shield/asset-lib/src/balances-utils.js#L26
39
+ const isUnconfirmed = (tx) => !tx.failed && tx.pending
40
+
41
+ // NOTE: When trying to find the highest incentive of a
42
+ // transaction, consider those which are still
43
+ // pending.
44
+ return txLogSendsByFeeAmountDesc.find(isUnconfirmed)
45
+ }
46
+
47
+ // Returns the most competitively priced pending
48
+ // transaction from the `TxLog` for a given `nonce`.
49
+ export const getHighestIncentiveTxByNonce = async ({
50
+ assetClientInterface,
51
+ asset,
52
+ nonce,
53
+ walletAccount,
54
+ }) => {
55
+ assert(assetClientInterface, 'expected assetClientInterface')
56
+ assert(asset, 'expected asset')
57
+ assert(Number.isInteger(nonce), 'expected integer nonce')
58
+ assert(walletAccount, 'expected walletAccount')
59
+
60
+ return getHighestIncentiveTxByNonceForTxLog({
61
+ txLog: await assetClientInterface.getTxLog({ assetName: asset.name, walletAccount }),
62
+ nonce,
63
+ })
64
+ }
65
+
15
66
  // Legacy compatibility with wallets
16
67
  export function getPendingNonExchangeTxs(activeWalletAccount, getTxLog, getIsExchangeTx) {
17
68
  console.log('Using deprecated function getPendingNonExchangeTxs()')
@@ -103,37 +154,63 @@ const calculateBumpedGasPriceNonEip1559 = ({ currentGasPrice, prevMaxFeePerGas }
103
154
  }
104
155
  }
105
156
 
106
- const getLinearIncentiveBumpForEip1559Transaction = ({
157
+ /*
158
+ Node Rules:
159
+ GETH:
160
+ The EIP1559 transaction bump logic requires that a replacement transaction must have BOTH:
161
+ 1. GasFeeCap >= oldGasFeeCap × (100 + priceBump) / 100
162
+ 2. GasTipCap >= oldGasTipCap × (100 + priceBump) / 100
163
+ ERIGON:
164
+ The Erigon EIP1559 transaction bump logic requires that a replacement transaction must have BOTH:
165
+ 1. `Tip >= oldTip × (100 + priceBump) / 100`
166
+ 2. `FeeCap >= oldFeeCap × (100 + priceBump) / 100`
167
+ NETHERMIND:
168
+ Nethermind's EIP1559 transaction bump logic requires that a replacement transaction must have BOTH:
169
+ ### Regular Transactions (10% bump)
170
+ 1. `newMaxFeePerGas > oldMaxFeePerGas × 1.1`
171
+ 2. `newMaxPriorityFeePerGas > oldMaxPriorityFeePerGas × 1.1`
172
+
173
+ In short, all these 3 nodes require that the bump is applied to both the tip and the cap.
174
+ This won't work for besu nodes.
175
+ */
176
+ export const getLinearIncentiveBumpForEip1559Transaction = ({
107
177
  currentBaseFeePerGas,
108
- prevMaxFeePerGas,
109
- prevMaxPriorityFeePerGas,
178
+ prevMaxFeePerGas, // Previous maxFeePerGas (includes the old tip)
179
+ prevMaxPriorityFeePerGas, // Previous tip (maxPriorityFeePerGas)
110
180
  }) => {
111
181
  assert(currentBaseFeePerGas, 'currentBaseFeePerGas is required')
112
182
  assert(prevMaxFeePerGas, 'prevMaxFeePerGas is required')
113
183
  assert(prevMaxPriorityFeePerGas, 'prevMaxPriorityFeePerGas is required')
114
184
 
115
- // First, let's determine if there's a bump required just to bring
116
- // the transaction up to the `baseFeePerGas`. If the current base
117
- // fee is larger, we'll need to see how much to adjust the overall
118
- // transaction by.
119
- const increaseToBaseFeePerGasTipGasPrice = currentBaseFeePerGas.gt(prevMaxFeePerGas)
120
- ? currentBaseFeePerGas.sub(prevMaxFeePerGas)
121
- : currentBaseFeePerGas.sub(currentBaseFeePerGas) // TODO: What is a nice way to get ZERO without an `asset`? Is it `currentBaseFeePerGas.ZERO`?
122
-
123
- // The new `bumpedTipGasPrice`.
124
- const bumpedTipGasPrice = prevMaxPriorityFeePerGas
125
- .mul(BUMP_RATE)
126
- .add(increaseToBaseFeePerGasTipGasPrice)
127
-
128
- // We'll make attempt a linear increase of the transaction pricing. By
129
- // increasing the `tipGasPrice` and the `gasPrice` by the same amount,
130
- // we ensuret he any additional amount we spend (over the basefee) goes
131
- // directly to the miner, thereby increasing transaction incentive.
185
+ //
186
+ // ---- STEP 1: Geth / Erigon / Nethermind "pricebump" policy ----
187
+ //
188
+ // These clients require that BOTH the tip AND the cap are bumped by
189
+ // the configured percentage (BUMP_RATE), compared to the previous tx:
190
+ //
191
+ // newTip >= prevTip * BUMP_RATE AND
192
+ // newMaxFee >= prevMaxFee * BUMP_RATE
193
+ //
194
+ // Both conditions must be satisfied independently for the replacement
195
+ // transaction to be accepted by the node's mempool.
196
+ //
197
+ const newTip = prevMaxPriorityFeePerGas.mul(BUMP_RATE)
198
+ let newMaxFee = prevMaxFeePerGas.mul(BUMP_RATE)
199
+
200
+ //
201
+ // ---- STEP 2: EIP-1559 validity "now" ----
202
+ //
203
+ // EIP-1559 requires maxFeePerGas >= baseFee + tip for the tx
204
+ // to be able to actually pay the full priority fee at the current baseFee.
205
+ //
206
+ const minCapRequiredNow = currentBaseFeePerGas.add(newTip)
207
+ if (newMaxFee.lt(minCapRequiredNow)) {
208
+ newMaxFee = minCapRequiredNow
209
+ }
210
+
132
211
  return {
133
- bumpedGasPrice: prevMaxFeePerGas
134
- .sub(prevMaxPriorityFeePerGas) // Removes the previous `tipGasPrice` before adding the next one (`bumpedTipGasPrice` is inclusive).
135
- .add(bumpedTipGasPrice),
136
- bumpedTipGasPrice,
212
+ bumpedGasPrice: newMaxFee,
213
+ bumpedTipGasPrice: newTip,
137
214
  }
138
215
  }
139
216
 
@@ -322,6 +399,20 @@ export const canAccelerateTx = ({
322
399
  return wrapResponseToObject({ errorMessage: 'there is a stuck TX with lower nonce' })
323
400
  }
324
401
 
402
+ // Check to see if this is the highest incentive transaction for this nonce.
403
+ const maybeHighestIncentiveTxByNonce = getHighestIncentiveTxByNonceForTxLog({
404
+ // TODO: is this the correct way to determine a nonce?
405
+ nonce: tx.data.nonce,
406
+ txLog: getTxLog(baseAssetName, activeWalletAccount),
407
+ })
408
+
409
+ // NOTE: We should only be able to accelerate the highest
410
+ // incentive transaction which is competing for a
411
+ // given `nonce`.
412
+ if (maybeHighestIncentiveTxByNonce && maybeHighestIncentiveTxByNonce.txId !== tx.txId) {
413
+ return wrapResponseToObject({ errorMessage: 'TX was replaced' })
414
+ }
415
+
325
416
  const {
326
417
  gasPrice: currentGasPrice,
327
418
  eip1559Enabled,
@@ -4,6 +4,7 @@ export {
4
4
  calculateBumpedGasPriceForFeeData,
5
5
  getPendingNonExchangeTxs,
6
6
  getAssetPendingNonExchangeTxs,
7
+ getHighestIncentiveTxByNonce,
7
8
  } from './get-can-accelerate-tx-factory.js'
8
9
 
9
10
  export { default as getIsEnoughBalanceToAccelerateSelectorFactory } from './get-is-enough-balance-to-accelerate-factory.js'