@exodus/ethereum-api 8.75.0 → 8.75.1

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.75.1](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.75.0...@exodus/ethereum-api@8.75.1) (2026-05-19)
7
+
8
+
9
+ ### Bug Fixes
10
+
11
+
12
+ * fix: use correct fromAddress for gasLimit multiplication (#8078)
13
+
14
+
15
+
6
16
  ## [8.75.0](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.74.1...@exodus/ethereum-api@8.75.0) (2026-05-13)
7
17
 
8
18
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/ethereum-api",
3
- "version": "8.75.0",
3
+ "version": "8.75.1",
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",
@@ -67,5 +67,5 @@
67
67
  "type": "git",
68
68
  "url": "git+https://github.com/ExodusMovement/assets.git"
69
69
  },
70
- "gitHead": "8cf5e6a5fc7d235c2c57ea51219bb1e3469329c5"
70
+ "gitHead": "ee5bf2d01055d8822c6421fa0ad4da84d4a33402"
71
71
  }
@@ -120,11 +120,9 @@ export const buildLegacyApproveTx = async ({
120
120
  const baseAsset = asset.baseAsset
121
121
 
122
122
  return {
123
- asset: baseAsset.name,
124
- receiver: {
125
- address: unsignedTx.txData.to,
126
- amount: buffer2currency({ asset: baseAsset, value: unsignedTx.txData.value }),
127
- },
123
+ asset: baseAsset,
124
+ address: unsignedTx.txData.to,
125
+ amount: buffer2currency({ asset: baseAsset, value: unsignedTx.txData.value }),
128
126
  txInput: unsignedTx.txData.data,
129
127
  gasPrice: buffer2currency({ asset: baseAsset, value: unsignedTx.txData.gasPrice }),
130
128
  tipGasPrice: unsignedTx.txData.tipGasPrice
@@ -132,13 +130,12 @@ export const buildLegacyApproveTx = async ({
132
130
  : undefined,
133
131
  gasLimit: bufferToInt(unsignedTx.txData.gasLimit),
134
132
  fromAddress: unsignedTx.txMeta.fromAddress,
135
- silent: true,
136
133
  }
137
134
  }
138
135
 
139
136
  // @deprecated use buildApproveTx instead
140
137
  export const createApprove =
141
- ({ assetClientInterface, sendTx }) =>
138
+ ({ assetClientInterface } = Object.create(null)) =>
142
139
  async ({
143
140
  spenderAddress,
144
141
  asset,
@@ -164,7 +161,16 @@ export const createApprove =
164
161
  walletAccount,
165
162
  })
166
163
 
167
- const txData = await sendTx({ walletAccount, ...approveTx, ...extras })
164
+ const { unsignedTx } = await asset.baseAsset.api.createTx({
165
+ walletAccount,
166
+ ...approveTx,
167
+ ...extras,
168
+ })
169
+ const txData = await asset.baseAsset.api.sendTx({
170
+ asset: asset.baseAsset,
171
+ walletAccount,
172
+ unsignedTx,
173
+ })
168
174
 
169
175
  if (!txData || !txData.txId) {
170
176
  throw new Error(`Failed to approve ${asset.displayTicker} - ${spenderAddress}`)
@@ -185,25 +191,20 @@ export const createApprove =
185
191
  walletAccount,
186
192
  })
187
193
 
188
- const { txId, rawTx } = await assetClientInterface.signTransaction({
189
- assetName: asset.baseAsset.name,
190
- unsignedTx,
191
- walletAccount,
192
- })
194
+ const { txId } = await asset.baseAsset.api.sendTx({ asset, walletAccount, unsignedTx })
193
195
 
194
- await asset.baseAsset.api.broadcastTx(rawTx.toString('hex'))
195
196
  return { txId }
196
197
  }
197
198
 
198
199
  const createSendApprovalAndWatchConfirmation =
199
- ({ sendTx, watchTxConfirmation }) =>
200
+ ({ watchTxConfirmation }) =>
200
201
  async (data) => {
201
202
  // To change the approve amount you first have to reduce the addresses`
202
203
  // allowance to zero by calling `approve(_spender, 0)` if it is not
203
204
  // already 0 to mitigate the race condition described here:
204
205
  // see: https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
205
206
  // see: https://etherscan.io/address/0xdac17f958d2ee523a2206206994597c13d831ec7#code
206
- const approve = createApprove({ sendTx })
207
+ const approve = createApprove()
207
208
  const { txId } = await approve(data)
208
209
  await watchTxConfirmation(
209
210
  { asset: data.asset.baseAsset.name, walletAccount: data.walletAccount },
@@ -215,11 +216,10 @@ const createSendApprovalAndWatchConfirmation =
215
216
  }
216
217
 
217
218
  export const createApproveSpendingTokens =
218
- ({ sendTx, watchTxConfirmation }) =>
219
+ ({ watchTxConfirmation }) =>
219
220
  async (data) => {
220
221
  const txIds = []
221
222
  const sendApprovalAndWatchConfirmation = createSendApprovalAndWatchConfirmation({
222
- sendTx,
223
223
  watchTxConfirmation,
224
224
  })
225
225
  if (ZERO_ALLOWANCE_ASSETS.includes(data.asset.name) && !data.approveAmount.isZero) {
@@ -235,7 +235,7 @@ export const createApproveSpendingTokens =
235
235
  data.gasLimit = Math.max(data.gasLimit || 0, APPROVAL_GAS_LIMIT)
236
236
  }
237
237
 
238
- const tokenApprovalTxId = await sendApprovalAndWatchConfirmation({ ...data, sendTx })
238
+ const tokenApprovalTxId = await sendApprovalAndWatchConfirmation(data)
239
239
  txIds.push(tokenApprovalTxId)
240
240
 
241
241
  return txIds
@@ -12,7 +12,9 @@ import { ClarityMonitorV2 } from './tx-log/clarity-monitor-v2.js'
12
12
  import { ClarityTruncatedHistoryMonitor } from './tx-log/clarity-truncated-history-monitor.js'
13
13
  import { EthereumMonitor } from './tx-log/ethereum-monitor.js'
14
14
  import { EthereumNoHistoryMonitor } from './tx-log/ethereum-no-history-monitor.js'
15
+ import { getOptimisticTxLogEffects } from './tx-log/get-optimistic-txlog-effects.js'
15
16
  import { BLOCK_TAG_LATEST, BLOCK_TAG_PENDING, resolveNonce } from './tx-send/nonce-utils.js'
17
+ import { signTx } from './tx-sign/index.js'
16
18
 
17
19
  // Determines the appropriate `monitorType`, `serverUrl` and `monitorInterval`
18
20
  // to use for a given config.
@@ -121,7 +123,126 @@ const broadcastPrivateBundleFactory =
121
123
  return null
122
124
  }
123
125
 
124
- export const createTransactionPrivacyFactory = ({ assetName, privacyRpcUrl }) => {
126
+ const assertSendPrivateTxProps = ({ asset, unsignedTx, walletAccount }) => {
127
+ assert(asset, 'expected asset')
128
+ assert(unsignedTx, 'expected unsignedTx')
129
+ assert(walletAccount, 'expected walletAccount')
130
+ }
131
+
132
+ export const createTransactionPrivacyResult = ({
133
+ assetClientInterface,
134
+ broadcastPrivateBundle,
135
+ broadcastPrivateTx,
136
+ privacyServer,
137
+ }) => {
138
+ assert(assetClientInterface, 'expected assetClientInterface')
139
+ assert(typeof broadcastPrivateBundle === 'function')
140
+ assert(typeof broadcastPrivateTx === 'function')
141
+ assert(privacyServer, 'expected privacyServer')
142
+
143
+ const sendPrivateTx = async ({ asset, unsignedTx, walletAccount }) => {
144
+ assertSendPrivateTxProps({ asset, unsignedTx, walletAccount })
145
+
146
+ const { rawTx, txId } = await signTx({
147
+ asset,
148
+ assetClientInterface,
149
+ unsignedTx,
150
+ walletAccount,
151
+ })
152
+
153
+ await broadcastPrivateTx(rawTx.toString('hex'))
154
+
155
+ const fromAddress = unsignedTx.txMeta?.fromAddress
156
+ assert(typeof fromAddress === 'string', 'expected fromAddress')
157
+
158
+ const { nonce, optimisticTxLogEffects } = await getOptimisticTxLogEffects({
159
+ asset,
160
+ assetClientInterface,
161
+ fromAddress,
162
+ txId,
163
+ unsignedTx,
164
+ walletAccount,
165
+ })
166
+
167
+ for (const optimisticTxLogEffect of optimisticTxLogEffects) {
168
+ await assetClientInterface.updateTxLogAndNotify(optimisticTxLogEffect)
169
+ }
170
+
171
+ return { nonce, txId }
172
+ }
173
+
174
+ const sendPrivateBundle = async ({ unsignedTxBundle }) => {
175
+ assert(
176
+ Array.isArray(unsignedTxBundle) && unsignedTxBundle.length > 0,
177
+ 'expected non-empty unsignedTxBundle'
178
+ )
179
+
180
+ unsignedTxBundle.forEach(assertSendPrivateTxProps)
181
+
182
+ const signTxResults = await Promise.all(
183
+ unsignedTxBundle.map(({ asset, unsignedTx, walletAccount }) =>
184
+ signTx({ assetClientInterface, asset, unsignedTx, walletAccount })
185
+ )
186
+ )
187
+
188
+ const maybeBundleResult = await broadcastPrivateBundle({
189
+ txs: signTxResults.map(({ rawTx }) => rawTx.toString('hex')),
190
+ })
191
+
192
+ const bundleHash = maybeBundleResult?.bundleHash || undefined
193
+
194
+ const getOptimisticTxLogEffectsResults = []
195
+
196
+ for (const [i, { txId }] of signTxResults.entries()) {
197
+ const { asset, unsignedTx, walletAccount } = unsignedTxBundle[i]
198
+
199
+ const fromAddress = unsignedTx.txMeta?.fromAddress
200
+ assert(typeof fromAddress === 'string', 'expected fromAddress')
201
+
202
+ void getOptimisticTxLogEffectsResults.push(
203
+ await getOptimisticTxLogEffects({
204
+ asset,
205
+ assetClientInterface,
206
+ fromAddress,
207
+ txId,
208
+ unsignedTx,
209
+ walletAccount,
210
+ // TODO: use consistent naming
211
+ bundleId: bundleHash,
212
+ })
213
+ )
214
+ }
215
+
216
+ const optimisticTxLogEffects = getOptimisticTxLogEffectsResults.flatMap(
217
+ ({ optimisticTxLogEffects }) => optimisticTxLogEffects
218
+ )
219
+
220
+ for (const optimisticTxLogEffect of optimisticTxLogEffects) {
221
+ await assetClientInterface.updateTxLogAndNotify(optimisticTxLogEffect)
222
+ }
223
+
224
+ return {
225
+ bundleHash,
226
+ txIds: signTxResults.map(({ txId }) => txId),
227
+ }
228
+ }
229
+
230
+ return {
231
+ broadcastPrivateBundle,
232
+ broadcastPrivateTx,
233
+ privacyServer,
234
+ sendPrivateTx,
235
+ sendPrivateBundle,
236
+ }
237
+ }
238
+
239
+ export const createTransactionPrivacyFactory = ({
240
+ assetClientInterface,
241
+ assetName,
242
+ privacyRpcUrl,
243
+ }) => {
244
+ assert(assetClientInterface, 'expected assetClientInterface')
245
+
125
246
  if (!privacyRpcUrl) return Object.create(null)
126
247
 
127
248
  const privacyServer = createEvmServer({
@@ -130,11 +251,16 @@ export const createTransactionPrivacyFactory = ({ assetName, privacyRpcUrl }) =>
130
251
  monitorType: 'no-history',
131
252
  })
132
253
 
133
- return {
134
- broadcastPrivateBundle: broadcastPrivateBundleFactory({ privacyServer }),
135
- broadcastPrivateTx: (...args) => privacyServer.sendRawTransaction(...args),
254
+ const broadcastPrivateBundle = broadcastPrivateBundleFactory({ privacyServer })
255
+
256
+ const broadcastPrivateTx = (...args) => privacyServer.sendRawTransaction(...args)
257
+
258
+ return createTransactionPrivacyResult({
259
+ assetClientInterface,
260
+ broadcastPrivateBundle,
261
+ broadcastPrivateTx,
136
262
  privacyServer,
137
- }
263
+ })
138
264
  }
139
265
 
140
266
  export const createHistoryMonitorFactory = ({
@@ -195,11 +195,17 @@ export const createAssetFactory = ({
195
195
  server,
196
196
  })
197
197
 
198
- const { broadcastPrivateBundle, broadcastPrivateTx, privacyServer } =
199
- createTransactionPrivacyFactory({
200
- assetName: asset.name,
201
- privacyRpcUrl,
202
- })
198
+ const {
199
+ broadcastPrivateBundle,
200
+ broadcastPrivateTx,
201
+ privacyServer,
202
+ sendPrivateTx,
203
+ sendPrivateBundle,
204
+ } = createTransactionPrivacyFactory({
205
+ assetClientInterface,
206
+ assetName: asset.name,
207
+ privacyRpcUrl,
208
+ })
203
209
 
204
210
  const features = {
205
211
  accountState: true,
@@ -354,16 +360,18 @@ export const createAssetFactory = ({
354
360
  chainId,
355
361
  monitorType,
356
362
  estimateL1DataFee,
357
- broadcastPrivateBundle,
358
- broadcastPrivateTx,
359
363
  forceGasLimitEstimation,
360
364
  eip7702Supported,
361
365
  getEIP7702Delegation: (addr) => getEIP7702Delegation({ address: addr, server }),
362
366
  getNonce,
363
- privacyServer,
364
367
  server,
365
368
  ...(erc20FuelBuffer && { erc20FuelBuffer }),
366
369
  ...(fuelThreshold && { fuelThreshold: asset.currency.defaultUnit(fuelThreshold) }),
370
+ broadcastPrivateBundle,
371
+ broadcastPrivateTx,
372
+ privacyServer,
373
+ sendPrivateTx,
374
+ sendPrivateBundle,
367
375
  }
368
376
  return overrideCallback({
369
377
  asset: fullAsset,
@@ -157,7 +157,7 @@ export async function fetchGasLimit({
157
157
  const gasLimitMultiplier = await resolveGasLimitMultiplier({
158
158
  asset,
159
159
  feeData,
160
- fromAddress: providedFromAddress,
160
+ fromAddress,
161
161
  toAddress: txToAddress,
162
162
  })
163
163
 
@@ -66,7 +66,6 @@ import NumberUnit from '@exodus/currency'
66
66
  import assert from 'minimalistic-assert'
67
67
 
68
68
  import { estimateGasLimit, scaleGasLimitEstimate } from '../../gas-estimation.js'
69
- import { getOptimisticTxLogEffects } from '../../tx-log/get-optimistic-txlog-effects.js'
70
69
  import { createWatchTx as defaultCreateWatch } from '../../watch-tx.js'
71
70
  import { stakingProviderClientFactory } from '../staking-provider-client.js'
72
71
  import { amountToCurrency, DISABLE_BALANCE_CHECKS, resolveFeeData } from '../utils/index.js'
@@ -281,8 +280,6 @@ export function createEthereumStakingService({
281
280
  asset,
282
281
  fromAddress: delegatorAddress,
283
282
  walletAccount,
284
- tag: 'latest',
285
- forceFromNode: true,
286
283
  })
287
284
 
288
285
  return {
@@ -369,14 +366,12 @@ export function createEthereumStakingService({
369
366
  plan: null,
370
367
  gasLimit: null,
371
368
  unsignedTx: null,
372
- signedTx: null,
373
369
  txId: null,
374
370
  },
375
371
  undelegate: {
376
372
  plan: null,
377
373
  gasLimit: null,
378
374
  unsignedTx: null,
379
- signedTx: null,
380
375
  txId: null,
381
376
  },
382
377
  }
@@ -465,34 +460,16 @@ export function createEthereumStakingService({
465
460
  revertOnSimulationError,
466
461
  })
467
462
 
468
- // 4. Sign transactions
469
- const signedTxEntries = await Promise.all(
470
- Object.entries(txSteps)
471
- .filter(([, txStep]) => txStep.unsignedTx)
472
- .map(async ([key, txStep]) => {
473
- const signedTx = await assetClientInterface.signTransaction({
474
- assetName: asset.baseAsset.name,
475
- unsignedTx: txStep.unsignedTx,
476
- walletAccount,
477
- })
478
- const txId = `0x${signedTx.txId.toString('hex')}`
479
- return [key, { signedTx, txId }]
480
- })
481
- )
482
-
483
- for (const [key, { signedTx, txId }] of signedTxEntries) {
484
- txSteps[key].signedTx = signedTx
485
- txSteps[key].txId = txId
486
- }
463
+ const unsignedTxBundle = createdTxEntries.map(([_, unsignedTx]) => ({
464
+ asset,
465
+ unsignedTx,
466
+ walletAccount,
467
+ }))
487
468
 
469
+ // 4. Sign transactions
470
+ //
488
471
  // 5. Broadcast transactions via bundle and get bundle hash
489
- const bundleResponse = await asset.broadcastPrivateBundle({
490
- txs: Object.values(txSteps)
491
- .filter((txStep) => txStep.signedTx)
492
- .map(({ signedTx }) => signedTx.rawTx),
493
- })
494
- const bundleHash = bundleResponse?.bundleHash
495
-
472
+ //
496
473
  // 6. Optimistic tx-log effects: reflect tx presence and fee consumption only.
497
474
  // The ETH returned to the user via the contract's internal transfer is not
498
475
  // captured here — it will be picked up once the tx is confirmed on-chain.
@@ -518,26 +495,14 @@ export function createEthereumStakingService({
518
495
  // That is incompatible with the current tx-log side-effect hook, which only
519
496
  // receives tx-local data (method id, calldata, nonce, gas, etc.) and not
520
497
  // staking snapshots or precomputed ETH-return amounts.
521
- const optimisticEffects = await Promise.all(
522
- Object.values(txSteps)
523
- .filter((txStep) => txStep.txId)
524
- .map((txStep) =>
525
- getOptimisticTxLogEffects({
526
- asset: asset.baseAsset,
527
- assetClientInterface,
528
- fromAddress: delegatorAddress,
529
- txId: txStep.txId,
530
- unsignedTx: txStep.unsignedTx,
531
- walletAccount,
532
- bundleId: bundleHash ?? undefined,
533
- })
534
- )
535
- )
498
+ const {
499
+ bundleHash,
500
+ txIds: [...txIds],
501
+ } = await asset.baseAsset.sendPrivateBundle({ unsignedTxBundle })
536
502
 
537
- for (const { optimisticTxLogEffects } of optimisticEffects) {
538
- for (const effect of optimisticTxLogEffects) {
539
- await assetClientInterface.updateTxLogAndNotify(effect)
540
- }
503
+ for (const txStep of Object.values(txSteps)) {
504
+ if (!txStep.unsignedTx) continue
505
+ assert((txStep.txId = txIds.shift()), 'expected txId')
541
506
  }
542
507
 
543
508
  // Testnet assets do not support delegations tracking
@@ -770,7 +735,7 @@ export function createEthereumStakingService({
770
735
  waitForConfirmation = false,
771
736
  feeData,
772
737
  }) {
773
- const sendTxArgs = {
738
+ const { unsignedTx } = await asset.baseAsset.api.createTx({
774
739
  asset,
775
740
  walletAccount,
776
741
  address: to,
@@ -781,9 +746,9 @@ export function createEthereumStakingService({
781
746
  // HACK: Override the `tipGasPrice` to use a custom `maxPriorityFeePerGas`.
782
747
  tipGasPrice,
783
748
  feeData,
784
- }
749
+ })
785
750
 
786
- const { txId } = await asset.baseAsset.api.sendTx(sendTxArgs)
751
+ const { txId } = await asset.baseAsset.api.sendTx({ asset, walletAccount, unsignedTx })
787
752
 
788
753
  const baseAsset = asset.baseAsset
789
754
  if (waitForConfirmation) {
@@ -1,8 +1,9 @@
1
- import { normalizeTxId, parseUnsignedTx, updateNonce } from '@exodus/ethereum-lib'
1
+ import { parseUnsignedTx, updateNonce } from '@exodus/ethereum-lib'
2
2
  import { safeString } from '@exodus/safe-string'
3
3
  import assert from 'minimalistic-assert'
4
4
 
5
5
  import { getOptimisticTxLogEffects } from '../tx-log/index.js'
6
+ import { signTx } from '../tx-sign/index.js'
6
7
  import { ARBITRARY_ADDRESS } from '../tx-type/index.js'
7
8
  import { handleBroadcastError } from './broadcast-error-handler.js'
8
9
 
@@ -10,16 +11,6 @@ const txSendFactory = ({ assetClientInterface, createTx }) => {
10
11
  assert(assetClientInterface, 'assetClientInterface is required')
11
12
  assert(createTx, 'createTx is required')
12
13
 
13
- async function signTx({ asset, unsignedTx, walletAccount }) {
14
- const { rawTx, txId } = await assetClientInterface.signTransaction({
15
- assetName: asset.baseAsset.name,
16
- unsignedTx,
17
- walletAccount,
18
- })
19
-
20
- return { rawTx, txId: normalizeTxId(txId) }
21
- }
22
-
23
14
  return async ({ asset, walletAccount, unsignedTx: providedUnsignedTx, ...legacyParams }) => {
24
15
  const baseAsset = asset.baseAsset
25
16
 
@@ -41,6 +32,7 @@ const txSendFactory = ({ assetClientInterface, createTx }) => {
41
32
  // Are there any signin-level errors that should be caught here?
42
33
  let { txId, rawTx } = await signTx({
43
34
  asset,
35
+ assetClientInterface,
44
36
  unsignedTx,
45
37
  walletAccount,
46
38
  })
@@ -92,7 +84,12 @@ const txSendFactory = ({ assetClientInterface, createTx }) => {
92
84
  unsignedTx.txData.nonce = newNonce
93
85
  }
94
86
 
95
- ;({ txId, rawTx } = await signTx({ asset, unsignedTx, walletAccount }))
87
+ ;({ txId, rawTx } = await signTx({
88
+ asset,
89
+ assetClientInterface,
90
+ unsignedTx,
91
+ walletAccount,
92
+ }))
96
93
 
97
94
  try {
98
95
  await baseAsset.api.broadcastTx(rawTx.toString('hex'))
@@ -0,0 +1,17 @@
1
+ import { normalizeTxId } from '@exodus/ethereum-lib'
2
+ import assert from 'minimalistic-assert'
3
+
4
+ export const signTx = async ({ asset, assetClientInterface, unsignedTx, walletAccount }) => {
5
+ assert(asset, 'expected asset')
6
+ assert(assetClientInterface, 'expected assetClientInterface')
7
+ assert(unsignedTx, 'expected unsignedTx')
8
+ assert(walletAccount, 'expected walletAccount')
9
+
10
+ const { rawTx, txId } = await assetClientInterface.signTransaction({
11
+ assetName: asset.baseAsset.name,
12
+ unsignedTx,
13
+ walletAccount,
14
+ })
15
+
16
+ return { rawTx, txId: normalizeTxId(txId) }
17
+ }