@exodus/ethereum-api 8.50.0 → 8.51.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.51.0](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.50.1...@exodus/ethereum-api@8.51.0) (2025-09-25)
7
+
8
+
9
+ ### Features
10
+
11
+
12
+ * feat(ethereum): check Eip1191ChainId in displayAddress function (#6504)
13
+
14
+
15
+ ### Bug Fixes
16
+
17
+
18
+ * fix: removed contractAddress, keepTxInput and and resolve token amount when sending to a different contract (#6508)
19
+
20
+
21
+
22
+ ## [8.50.1](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.50.0...@exodus/ethereum-api@8.50.1) (2025-09-22)
23
+
24
+ **Note:** Version bump only for package @exodus/ethereum-api
25
+
26
+
27
+
28
+
29
+
6
30
  ## [8.50.0](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.49.0...@exodus/ethereum-api@8.50.0) (2025-09-17)
7
31
 
8
32
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/ethereum-api",
3
- "version": "8.50.0",
3
+ "version": "8.51.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",
@@ -66,5 +66,5 @@
66
66
  "type": "git",
67
67
  "url": "git+https://github.com/ExodusMovement/assets.git"
68
68
  },
69
- "gitHead": "05a8eb8fc2b399e3497825462f429970f559b8cc"
69
+ "gitHead": "a5a5ca09ab04597193c9243306a884df16bdab8b"
70
70
  }
@@ -132,7 +132,8 @@ export const createAssetFactory = ({
132
132
  validate: validateFactory({ chainId, useEip1191ChainIdChecksum }),
133
133
  hasChecksum,
134
134
  isContract: server.isContract,
135
- displayAddress: (addr) => toChecksumAddress(addr),
135
+ displayAddress: (addr) =>
136
+ useEip1191ChainIdChecksum ? toChecksumAddress(addr, chainId) : toChecksumAddress(addr),
136
137
  }
137
138
 
138
139
  const bip44 = customBip44 || bip44Constants['ETH']
package/src/tx-create.js CHANGED
@@ -8,6 +8,7 @@ import { ensureSaneEip1559GasPriceForTipGasPrice } from './fee-utils.js'
8
8
  import { ARBITRARY_ADDRESS, fetchGasLimit, resolveDefaultTxInput } from './gas-estimation.js'
9
9
  import { getExtraFeeData, getFeeFactoryGasPrices } from './get-fee.js'
10
10
  import { getNftArguments } from './nft-utils.js'
11
+ import { getHighestIncentivePendingTxByNonce } from './tx-log/index.js'
11
12
 
12
13
  async function createUnsignedTxWithFees({
13
14
  asset,
@@ -134,6 +135,7 @@ const createBumpUnsignedTx = async ({
134
135
  const txToAddress = isToken ? asset.contract.address : toAddress
135
136
  const coinAmount = (replacedTokenTx || replacedTx).coinAmount.negate()
136
137
  const gasLimit = replacedTx.data.gasLimit
138
+ const nonce = replacedTx.data.nonce
137
139
 
138
140
  const value = isToken ? baseAsset.currency.ZERO : coinAmount
139
141
 
@@ -143,9 +145,27 @@ const createBumpUnsignedTx = async ({
143
145
  eip1559Enabled,
144
146
  tipGasPrice: currentTipGasPrice,
145
147
  } = feeData
148
+
149
+ const maybeHighestIncentivePendingTxForNonce = await getHighestIncentivePendingTxByNonce({
150
+ assetClientInterface,
151
+ asset,
152
+ nonce,
153
+ walletAccount,
154
+ })
155
+
156
+ assert(
157
+ maybeHighestIncentivePendingTxForNonce,
158
+ `unable to resolve pending transaction for nonce ${nonce}`
159
+ )
160
+
146
161
  const { bumpedGasPrice, bumpedTipGasPrice } = calculateBumpedGasPrice({
147
162
  baseAsset,
148
- tx: replacedTx,
163
+ // HACK: Although the `bumpTxId` defines the characteristics of
164
+ // of a transaction the user would like to accelerate, for
165
+ // acceleration to be successful, the transaction must be
166
+ // priced to exceed the miner incentive of whichever
167
+ // transaction is currently pending at the specified nonce.
168
+ tx: maybeHighestIncentivePendingTxForNonce,
149
169
  currentGasPrice,
150
170
  currentBaseFee,
151
171
  currentTipGasPrice,
@@ -153,7 +173,6 @@ const createBumpUnsignedTx = async ({
153
173
  })
154
174
  const gasPrice = bumpedGasPrice
155
175
  const tipGasPrice = bumpedTipGasPrice
156
- const nonce = replacedTx.data.nonce
157
176
  const data = replacedTokenTx
158
177
  ? asset.contract.transfer.build(toAddress.toLowerCase(), coinAmount.toBaseString())
159
178
  : replacedTx.data.data || '0x'
@@ -200,7 +219,6 @@ export const createTxFactory = ({ chainId, assetClientInterface, useAbsoluteNonc
200
219
  nft, // when sending nfts
201
220
  fromAddress: providedFromAddress, // wallet from address
202
221
  toAddress: providedToAddress, // user's to address, not the token or the dex contract
203
- contractAddress: providedContractAddress, // Provided when swapping a token via the DEX contract, not via the token's contract
204
222
  txInput: providedTxInput, // Provided when swapping via a DEX contract
205
223
  gasLimit: providedGasLimit, // Provided by exchange when known
206
224
  amount: providedAmount, // The NU amount to be sent, to be included in the tx value or tx input
@@ -211,7 +229,6 @@ export const createTxFactory = ({ chainId, assetClientInterface, useAbsoluteNonc
211
229
  customFee,
212
230
  isSendAll,
213
231
  bumpTxId,
214
- keepTxInput, // @deprecated this flag is used by swaps when swapping a token via DEX. The asset is token but the tx TO address is not the token address. Update swap to use `contractAddress`
215
232
  }) => {
216
233
  assert(asset, 'asset is required')
217
234
  assert(walletAccount, 'walletAccount is required')
@@ -254,8 +271,7 @@ export const createTxFactory = ({ chainId, assetClientInterface, useAbsoluteNonc
254
271
 
255
272
  const resolvedGasPrice = providedGasPrice ?? maybeGasPrice
256
273
 
257
- const txToAddress =
258
- providedContractAddress ?? (isToken && !keepTxInput ? asset.contract.address : toAddress)
274
+ const txToAddress = isToken && !providedTxInput ? asset.contract.address : toAddress
259
275
 
260
276
  const isContractToAddress = await isContractAddressCached({ asset, address: txToAddress })
261
277
 
@@ -0,0 +1,130 @@
1
+ import { isNumberUnit } from '@exodus/currency'
2
+ import { isEthereumLikeToken } from '@exodus/ethereum-lib'
3
+ import assert from 'minimalistic-assert'
4
+
5
+ // Returns the most competitively priced pending
6
+ // transaction from the `TxLog` for a given `nonce`.
7
+ export const getHighestIncentivePendingTxByNonce = async ({
8
+ assetClientInterface,
9
+ asset,
10
+ nonce,
11
+ walletAccount,
12
+ }) => {
13
+ assert(assetClientInterface, 'expected assetClientInterface')
14
+ assert(asset, 'expected asset')
15
+ assert(Number.isInteger(nonce), 'expected integer nonce')
16
+ assert(walletAccount, 'expected walletAccount')
17
+
18
+ // https://github.com/ExodusMovement/assets/blob/fbe3702861cba3b21885a65b15f038fcd8541891/shield/asset-lib/src/balances-utils.js#L26
19
+ const isUnconfirmed = (tx) => !tx.failed && tx.pending
20
+
21
+ const [maybeHighestIncentiveTx] = [
22
+ ...(await assetClientInterface.getTxLog({ assetName: asset.name, walletAccount })),
23
+ ]
24
+ .filter(isUnconfirmed)
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
+ return maybeHighestIncentiveTx
29
+ }
30
+
31
+ export const getOptimisticTxLogEffects = async ({
32
+ asset,
33
+ amount = asset.currency.ZERO,
34
+ assetClientInterface,
35
+ confirmations = 0,
36
+ feeAmount,
37
+ fromAddress,
38
+ gasLimit,
39
+ nonce,
40
+ txId,
41
+ toAddress,
42
+ tipGasPrice: maybeTipGasPrice,
43
+ walletAccount,
44
+ }) => {
45
+ assert(isNumberUnit(amount), 'expected NumberUnit amount')
46
+ assert(asset, 'expected asset')
47
+ assert(assetClientInterface, 'expected assetClientInterface')
48
+ assert(Number.isInteger(confirmations), 'expected integer confirmations')
49
+ assert(isNumberUnit(feeAmount), 'expected feeAmount')
50
+ assert(typeof fromAddress === 'string', 'expected string fromAddress')
51
+ assert(Number.isInteger(gasLimit), 'expected integer gasLimit')
52
+ assert(Number.isInteger(nonce), 'expected integer nonce')
53
+ assert(typeof toAddress === 'string', 'expected string toAddress')
54
+ assert(txId, 'expected txId')
55
+ assert(walletAccount, 'expected walletAccount')
56
+
57
+ if (maybeTipGasPrice) assert(isNumberUnit(maybeTipGasPrice), 'expected NumberUnit tipGasPrice')
58
+
59
+ const baseAsset = asset.baseAsset
60
+
61
+ // TODO: This is incorrect for token transfers.
62
+ const selfSend = fromAddress.toLowerCase() === toAddress.toLowerCase()
63
+
64
+ assert(asset.feeAsset.name === baseAsset.name, 'inconsistent feeAsset')
65
+
66
+ const maybeTxToReplace = await getHighestIncentivePendingTxByNonce({
67
+ asset,
68
+ assetClientInterface,
69
+ nonce,
70
+ walletAccount,
71
+ })
72
+
73
+ const replacedTxId =
74
+ maybeTxToReplace && maybeTxToReplace.feeAmount.lt(feeAmount) ? maybeTxToReplace.txId : undefined
75
+
76
+ if (!replacedTxId && maybeTxToReplace) {
77
+ console.log('Attempting to replace a transaction using an insufficient fee!')
78
+ }
79
+
80
+ const sharedProps = {
81
+ confirmations,
82
+ feeAmount,
83
+ feeCoinName: asset.feeAsset.name,
84
+ selfSend,
85
+ to: toAddress,
86
+ txId,
87
+ data: {
88
+ gasLimit,
89
+ replacedTxId,
90
+ nonce,
91
+ ...(maybeTipGasPrice ? { tipGasPrice: maybeTipGasPrice.toBaseString() } : null),
92
+ },
93
+ }
94
+
95
+ const optimisticTxLogEffects = [
96
+ {
97
+ assetName: asset.name,
98
+ walletAccount,
99
+ txs: [
100
+ {
101
+ ...sharedProps,
102
+ coinAmount: selfSend ? asset.currency.ZERO : amount.abs().negate(),
103
+ coinName: asset.name,
104
+ currencies: {
105
+ [asset.name]: asset.currency,
106
+ [asset.feeAsset.name]: asset.feeAsset.currency,
107
+ },
108
+ },
109
+ ],
110
+ },
111
+ isEthereumLikeToken(asset) && {
112
+ assetName: baseAsset.name,
113
+ walletAccount,
114
+ txs: [
115
+ {
116
+ ...sharedProps,
117
+ coinAmount: baseAsset.currency.ZERO,
118
+ coinName: baseAsset.name,
119
+ currencies: {
120
+ [baseAsset.name]: baseAsset.currency,
121
+ [asset.feeAsset.name]: asset.feeAsset.currency,
122
+ },
123
+ tokens: [asset.name],
124
+ },
125
+ ],
126
+ },
127
+ ].filter(Boolean)
128
+
129
+ return { optimisticTxLogEffects }
130
+ }
@@ -1,3 +1,7 @@
1
1
  export { EthereumMonitor } from './ethereum-monitor.js'
2
2
  export { EthereumNoHistoryMonitor } from './ethereum-no-history-monitor.js'
3
3
  export { ClarityMonitor } from './clarity-monitor.js'
4
+ export {
5
+ getOptimisticTxLogEffects,
6
+ getHighestIncentivePendingTxByNonce,
7
+ } from './get-optimistic-txlog-effects.js'
@@ -1,14 +1,10 @@
1
- import {
2
- isEthereumLikeToken,
3
- normalizeTxId,
4
- parseUnsignedTx,
5
- updateNonce,
6
- } from '@exodus/ethereum-lib'
1
+ import { normalizeTxId, parseUnsignedTx, updateNonce } from '@exodus/ethereum-lib'
7
2
  import assert from 'minimalistic-assert'
8
3
 
9
4
  import * as ErrorWrapper from '../error-wrapper.js'
10
5
  import { transactionExists } from '../eth-like-util.js'
11
6
  import { ARBITRARY_ADDRESS } from '../gas-estimation.js'
7
+ import { getOptimisticTxLogEffects } from '../tx-log/index.js'
12
8
 
13
9
  const txSendFactory = ({ assetClientInterface, createTx }) => {
14
10
  assert(assetClientInterface, 'assetClientInterface is required')
@@ -25,7 +21,6 @@ const txSendFactory = ({ assetClientInterface, createTx }) => {
25
21
  }
26
22
 
27
23
  return async ({ asset, walletAccount, unsignedTx: providedUnsignedTx, ...legacyParams }) => {
28
- const assetName = asset.name
29
24
  const baseAsset = asset.baseAsset
30
25
 
31
26
  const resolveUnsignedTx = async () => {
@@ -47,26 +42,19 @@ const txSendFactory = ({ assetClientInterface, createTx }) => {
47
42
  // this converts an transactionBuffer to values we can use when creating the tx logs
48
43
  const parsedTx = parseUnsignedTx({ asset, unsignedTx })
49
44
 
50
- // the txMeta.fee may include implicit l1 fees
51
- const feeAmount = unsignedTx.txMeta.fee
52
- ? asset.feeAsset.currency.parse(unsignedTx.txMeta.fee)
53
- : parsedTx.fee
54
-
55
45
  let nonce = parsedTx.nonce
56
46
 
57
47
  const tipGasPrice = parsedTx.tipGasPrice
48
+ const feeAmount = parsedTx.fee
58
49
  const gasLimit = parsedTx.gasLimit
59
50
  const amount = parsedTx.amount
60
- const to = parsedTx.to
61
- const eip1559Enabled = parsedTx.eip1559Enabled
51
+ const toAddress = parsedTx.to
62
52
 
63
53
  // unknown data from buffer...
64
54
  const fromAddress = unsignedTx.txMeta.fromAddress
65
- const selfSend = fromAddress === to
66
- const replacedTxId = unsignedTx.txMeta.bumpTxId
67
55
 
68
56
  assert(
69
- to.toLowerCase() !== ARBITRARY_ADDRESS,
57
+ toAddress.toLowerCase() !== ARBITRARY_ADDRESS,
70
58
  `The receiving wallet address must not be ${ARBITRARY_ADDRESS}`
71
59
  )
72
60
 
@@ -154,64 +142,23 @@ const txSendFactory = ({ assetClientInterface, createTx }) => {
154
142
  }
155
143
  }
156
144
 
157
- const txData = eip1559Enabled
158
- ? {
159
- gasLimit,
160
- replacedTxId,
161
- nonce,
162
- ...(tipGasPrice ? { tipGasPrice: tipGasPrice.toBaseString() } : Object.create(null)),
163
- }
164
- : {
165
- gasLimit,
166
- replacedTxId,
167
- nonce,
168
- }
169
-
170
- await assetClientInterface.updateTxLogAndNotify({
171
- assetName: asset.name,
145
+ const { optimisticTxLogEffects } = await getOptimisticTxLogEffects({
146
+ amount,
147
+ asset,
148
+ assetClientInterface,
149
+ feeAmount,
150
+ fromAddress,
151
+ gasLimit,
152
+ nonce,
153
+ txId,
154
+ toAddress,
155
+ tipGasPrice,
172
156
  walletAccount,
173
- txs: [
174
- {
175
- txId,
176
- confirmations: 0,
177
- coinAmount: selfSend ? asset.currency.ZERO : amount.abs().negate(),
178
- coinName: asset.name,
179
- feeAmount,
180
- feeCoinName: asset.feeAsset.name,
181
- selfSend,
182
- to,
183
- currencies: {
184
- [assetName]: asset.currency,
185
- [asset.feeAsset.name]: asset.feeAsset.currency,
186
- },
187
- data: txData,
188
- },
189
- ],
190
157
  })
191
158
 
192
- const isToken = isEthereumLikeToken(asset)
193
- if (isToken) {
194
- await assetClientInterface.updateTxLogAndNotify({
195
- assetName: baseAsset.name,
196
- walletAccount,
197
- txs: [
198
- {
199
- txId,
200
- coinAmount: baseAsset.currency.ZERO,
201
- coinName: baseAsset.name,
202
- feeAmount,
203
- feeCoinName: baseAsset.name,
204
- selfSend,
205
- to,
206
- token: asset.name,
207
- currencies: {
208
- [baseAsset.name]: baseAsset.currency,
209
- [asset.feeAsset.name]: asset.feeAsset.currency,
210
- },
211
- data: txData,
212
- },
213
- ],
214
- })
159
+ // NOTE: `optimisticTxLogEffects` **must** be written sequentially.
160
+ for (const optimisticTxLogEffect of optimisticTxLogEffects) {
161
+ await assetClientInterface.updateTxLogAndNotify(optimisticTxLogEffect)
215
162
  }
216
163
 
217
164
  return { txId, nonce }