@exodus/ethereum-api 8.40.0 → 8.41.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,34 @@
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.41.0](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.40.1...@exodus/ethereum-api@8.41.0) (2025-07-01)
7
+
8
+
9
+ ### Features
10
+
11
+
12
+ * feat: transaction bundles and private server gas estimation (#5953)
13
+
14
+
15
+ ### Bug Fixes
16
+
17
+
18
+ * fix: include l1 fee in tx log (#5986)
19
+
20
+ * fix: remove unused gradientCoords from meta and validation (#5937)
21
+
22
+
23
+
24
+ ## [8.40.1](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.40.0...@exodus/ethereum-api@8.40.1) (2025-06-26)
25
+
26
+
27
+ ### Bug Fixes
28
+
29
+
30
+ * fix: activate `currentTipGasPrice` for bumped transactions in `tx-send` (#5950)
31
+
32
+
33
+
6
34
  ## [8.40.0](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.39.0...@exodus/ethereum-api@8.40.0) (2025-06-26)
7
35
 
8
36
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/ethereum-api",
3
- "version": "8.40.0",
3
+ "version": "8.41.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",
@@ -28,7 +28,7 @@
28
28
  "@exodus/bip44-constants": "^195.0.0",
29
29
  "@exodus/crypto": "^1.0.0-rc.13",
30
30
  "@exodus/currency": "^6.0.1",
31
- "@exodus/ethereum-lib": "^5.12.0",
31
+ "@exodus/ethereum-lib": "^5.15.1",
32
32
  "@exodus/ethereum-meta": "^2.5.0",
33
33
  "@exodus/ethereumholesky-meta": "^2.0.2",
34
34
  "@exodus/ethereumjs": "^1.0.0",
@@ -63,5 +63,5 @@
63
63
  "type": "git",
64
64
  "url": "git+https://github.com/ExodusMovement/assets.git"
65
65
  },
66
- "gitHead": "8c81c64076e7092e8a1a0d7c9a51f410a94aeec9"
66
+ "gitHead": "1077a752b60e0b25961613fd007596ab3368188e"
67
67
  }
@@ -99,12 +99,6 @@ export const fromAddEthereumChainParameterToFactoryParams = (params) => {
99
99
  decimals: params.nativeCurrency.decimals,
100
100
  primaryColor: color,
101
101
  gradientColors: [color, color],
102
- gradientCoords: {
103
- x1: '0%',
104
- y1: '0%',
105
- x2: '100%',
106
- y2: '100%',
107
- },
108
102
  },
109
103
  plugin: {
110
104
  serverUrl: params.rpcUrls?.[0],
@@ -59,14 +59,22 @@ export const resolveMonitorSettings = (
59
59
  return { ...defaultResolution, monitorType: overrideMonitorType, serverUrl: overrideServerUrl }
60
60
  }
61
61
 
62
- export const createBroadcastTxFactory = ({ assetName, server, privacyRpcUrl }) => {
63
- assert(server, 'expected server')
64
-
65
- const defaultResult = {
66
- broadcastTx: (...args) => server.sendRawTransaction(...args),
62
+ const broadcastPrivateBundleFactory =
63
+ ({ privacyServer }) =>
64
+ async ({ txs }) => {
65
+ assert(Array.isArray(txs), 'txs must be an array')
66
+ if (txs.length === 0) return
67
+
68
+ await privacyServer.sendRequest(
69
+ privacyServer.buildRequest({
70
+ method: 'eth_sendBundle',
71
+ params: [{ txs }],
72
+ })
73
+ )
67
74
  }
68
75
 
69
- if (!privacyRpcUrl) return defaultResult
76
+ export const createTransactionPrivacyFactory = ({ assetName, privacyRpcUrl }) => {
77
+ if (!privacyRpcUrl) return Object.create(null)
70
78
 
71
79
  const privacyServer = createEvmServer({
72
80
  assetName,
@@ -75,8 +83,9 @@ export const createBroadcastTxFactory = ({ assetName, server, privacyRpcUrl }) =
75
83
  })
76
84
 
77
85
  return {
78
- ...defaultResult,
86
+ broadcastPrivateBundle: broadcastPrivateBundleFactory({ privacyServer }),
79
87
  broadcastPrivateTx: (...args) => privacyServer.sendRawTransaction(...args),
88
+ privacyServer,
80
89
  }
81
90
  }
82
91
 
@@ -87,6 +96,7 @@ export const createHistoryMonitorFactory = ({
87
96
  monitorType,
88
97
  server,
89
98
  stakingAssetNames,
99
+ rpcBalanceAssetNames,
90
100
  }) => {
91
101
  assert(assetName, 'expected assetName')
92
102
  assert(assetClientInterface, 'expected assetClientInterface')
@@ -103,6 +113,7 @@ export const createHistoryMonitorFactory = ({
103
113
  assetClientInterface,
104
114
  interval: ms(monitorInterval || '5m'),
105
115
  server,
116
+ rpcBalanceAssetNames,
106
117
  ...args,
107
118
  })
108
119
  break
@@ -119,6 +130,7 @@ export const createHistoryMonitorFactory = ({
119
130
  assetClientInterface,
120
131
  interval: ms(monitorInterval || '15s'),
121
132
  server,
133
+ rpcBalanceAssetNames,
122
134
  ...args,
123
135
  })
124
136
  break
@@ -22,8 +22,8 @@ import assert from 'minimalistic-assert'
22
22
 
23
23
  import { addressHasHistoryFactory } from './address-has-history.js'
24
24
  import {
25
- createBroadcastTxFactory,
26
25
  createHistoryMonitorFactory,
26
+ createTransactionPrivacyFactory,
27
27
  resolveMonitorSettings,
28
28
  } from './create-asset-utils.js'
29
29
  import { createTokenFactory } from './create-token-factory.js'
@@ -64,6 +64,7 @@ export const createAssetFactory = ({
64
64
  stakingConfiguration = {},
65
65
  useEip1191ChainIdChecksum = false,
66
66
  forceGasLimitEstimation = false,
67
+ rpcBalanceAssetNames = [],
67
68
  supportsCustomFees: defaultSupportsCustomFees = false,
68
69
  useAbsoluteBalanceAndNonce = false,
69
70
  }) => {
@@ -145,8 +146,8 @@ export const createAssetFactory = ({
145
146
 
146
147
  const getBalances = getBalancesFactory({
147
148
  monitorType,
148
- config,
149
149
  useAbsoluteBalance: useAbsoluteBalanceAndNonce,
150
+ rpcBalanceAssetNames,
150
151
  })
151
152
 
152
153
  const { createToken, getTokens } = createTokenFactory(
@@ -170,11 +171,11 @@ export const createAssetFactory = ({
170
171
  server,
171
172
  })
172
173
 
173
- const { broadcastTx, broadcastPrivateTx } = createBroadcastTxFactory({
174
- assetName: asset.name,
175
- server,
176
- privacyRpcUrl,
177
- })
174
+ const { broadcastPrivateBundle, broadcastPrivateTx, privacyServer } =
175
+ createTransactionPrivacyFactory({
176
+ assetName: asset.name,
177
+ privacyRpcUrl,
178
+ })
178
179
 
179
180
  const features = {
180
181
  accountState: true,
@@ -185,7 +186,6 @@ export const createAssetFactory = ({
185
186
  isTestnet,
186
187
  nfts,
187
188
  noHistory: monitorType === 'no-history',
188
- transactionPrivacy: typeof broadcastPrivateTx === 'function',
189
189
  signWithSigner: true,
190
190
  signMessageWithSigner: true,
191
191
  supportsCustomFees,
@@ -214,6 +214,7 @@ export const createAssetFactory = ({
214
214
  monitorType,
215
215
  server,
216
216
  stakingAssetNames,
217
+ rpcBalanceAssetNames,
217
218
  })
218
219
 
219
220
  const defaultAddressPath = 'm/0/0'
@@ -238,7 +239,7 @@ export const createAssetFactory = ({
238
239
 
239
240
  const api = {
240
241
  addressHasHistory,
241
- broadcastTx,
242
+ broadcastTx: (...args) => server.sendRawTransaction(...args),
242
243
  createAccountState: () => accountStateClass,
243
244
  createFeeMonitor,
244
245
  createHistoryMonitor,
@@ -289,8 +290,10 @@ export const createAssetFactory = ({
289
290
  chainId,
290
291
  monitorType,
291
292
  estimateL1DataFee,
293
+ broadcastPrivateBundle,
292
294
  broadcastPrivateTx,
293
295
  forceGasLimitEstimation,
296
+ privacyServer,
294
297
  server,
295
298
  ...(erc20FuelBuffer && { erc20FuelBuffer }),
296
299
  ...(fuelThreshold && { fuelThreshold: asset.currency.defaultUnit(fuelThreshold) }),
@@ -50,14 +50,14 @@ export const scaleGasLimitEstimate = ({
50
50
  // be calculated with account's balance divided by gasPrice. If user's balance is too low,
51
51
  // the gasEstimation will fail. If gasPrice is set to '0x0', the account's balance is not
52
52
  // used to estimate gas.
53
- export async function estimateGasLimit(
53
+ export async function estimateGasLimit({
54
54
  asset,
55
55
  fromAddress,
56
56
  toAddress,
57
- amount,
57
+ amount = asset.currency.ZERO,
58
58
  data,
59
- gasPrice = '0x'
60
- ) {
59
+ gasPrice = '0x',
60
+ }) {
61
61
  const opts = {
62
62
  from: fromAddress,
63
63
  to: toAddress,
@@ -150,13 +150,13 @@ export async function fetchGasLimit({
150
150
  const txAmount = isToken ? asset.baseAsset.currency.ZERO : amount
151
151
 
152
152
  try {
153
- const estimatedGasLimit = await estimateGasLimit(
153
+ const estimatedGasLimit = await estimateGasLimit({
154
154
  asset,
155
155
  fromAddress,
156
- txToAddress,
157
- txAmount,
158
- txInput
159
- )
156
+ toAddress: txToAddress,
157
+ amount: txAmount,
158
+ data: txInput,
159
+ })
160
160
 
161
161
  return scaleGasLimitEstimate({ estimatedGasLimit, gasLimitMultiplier })
162
162
  } catch (err) {
@@ -4,7 +4,6 @@ import {
4
4
  getUnconfirmedReceivedBalance,
5
5
  getUnconfirmedSentBalance,
6
6
  } from '@exodus/asset-lib'
7
- import { isRpcBalanceAsset } from '@exodus/ethereum-lib'
8
7
  import assert from 'minimalistic-assert'
9
8
 
10
9
  export const getAbsoluteBalance = ({ asset, txLog }) => {
@@ -113,13 +112,7 @@ const getSpendable = ({ asset, balance, txLog, unconfirmedReceived }) => {
113
112
  * @param accountState the account state when the balance is loaded from RPC
114
113
  * @returns {{balance}|null} an object with the balance or null if the balance is unknown
115
114
  */
116
- export const getBalancesFactory = ({
117
- monitorType,
118
- config = Object.create(null),
119
- useAbsoluteBalance,
120
- }) => {
121
- const { useAccountStateBalanceOnly } = config
122
-
115
+ export const getBalancesFactory = ({ monitorType, useAbsoluteBalance, rpcBalanceAssetNames }) => {
123
116
  assert(monitorType, 'monitorType is required')
124
117
  return ({ asset, txLog, accountState }) => {
125
118
  const unconfirmedReceived = getUnconfirmedReceivedBalance({ asset, txLog })
@@ -139,7 +132,7 @@ export const getBalancesFactory = ({
139
132
  let spendable
140
133
 
141
134
  // Balance from accountState is considered total b/c is fetched from rpc
142
- if (useAccountStateBalanceOnly || isRpcBalanceAsset(asset) || monitorType === 'no-history') {
135
+ if (rpcBalanceAssetNames.includes(asset.name) || monitorType === 'no-history') {
143
136
  total = getBalanceFromAccountState({ asset, accountState }).sub(unconfirmedSent)
144
137
  spendable = total.sub(staked).sub(staking).sub(unstaking).sub(unstaked)
145
138
  } else {
@@ -359,14 +359,14 @@ export function createEthereumStakingService({
359
359
  amount = amount || asset.currency.ZERO
360
360
  from = from.toLowerCase()
361
361
 
362
- const estimatedGasLimit = await estimateGasLimit(
362
+ const estimatedGasLimit = await estimateGasLimit({
363
363
  asset,
364
- from,
365
- to.toLowerCase(),
364
+ fromAddress: from,
365
+ toAddress: to.toLowerCase(),
366
366
  amount, // staking contracts does not always require ETH amount to interact with
367
- txInput,
368
- DISABLE_BALANCE_CHECKS
369
- )
367
+ data: txInput,
368
+ gasPrice: DISABLE_BALANCE_CHECKS,
369
+ })
370
370
 
371
371
  const scaledGasLimit = scaleGasLimitEstimate({ estimatedGasLimit })
372
372
 
@@ -337,14 +337,14 @@ export function createPolygonStakingService({
337
337
 
338
338
  const amount = ethereum.currency.ZERO
339
339
 
340
- const gasLimit = await estimateGasLimit(
341
- ethereum,
342
- from,
343
- to,
340
+ const gasLimit = await estimateGasLimit({
341
+ asset: ethereum,
342
+ fromAddress: from,
343
+ toAddress: to,
344
344
  amount, // staking contracts does not require ETH amount to interact with
345
- txInput,
346
- DISABLE_BALANCE_CHECKS
347
- )
345
+ data: txInput,
346
+ gasPrice: DISABLE_BALANCE_CHECKS,
347
+ })
348
348
 
349
349
  return ethereum.api.getFee({ asset: ethereum, feeData, gasLimit, amount })
350
350
  }
@@ -1,5 +1,5 @@
1
1
  import { BaseMonitor } from '@exodus/asset-lib'
2
- import { getAssetAddresses, isRpcBalanceAsset } from '@exodus/ethereum-lib'
2
+ import { getAssetAddresses } from '@exodus/ethereum-lib'
3
3
  import lodash from 'lodash'
4
4
 
5
5
  import { executeEthLikeFeeMonitorUpdate } from '../fee-utils.js'
@@ -16,10 +16,11 @@ import {
16
16
  const { isEmpty } = lodash
17
17
 
18
18
  export class ClarityMonitor extends BaseMonitor {
19
- constructor({ server, config, ...args }) {
19
+ constructor({ server, config, rpcBalanceAssetNames, ...args }) {
20
20
  super(args)
21
21
  this.config = { GAS_PRICE_FROM_WEBSOCKET: true, ...config }
22
22
  this.server = server
23
+ this.rpcBalanceAssetNames = rpcBalanceAssetNames
23
24
  this.getAllLogItemsByAsset = getAllLogItemsByAsset
24
25
  this.deriveDataNeededForTick = getDeriveDataNeededForTick(this.aci)
25
26
  this.deriveTransactionsToCheck = getDeriveTransactionsToCheck({
@@ -201,7 +202,7 @@ export class ClarityMonitor extends BaseMonitor {
201
202
  const asset = this.asset
202
203
  const newAccountState = Object.create(null)
203
204
  const balances = await this.getBalances({ tokens, ourWalletAddress })
204
- if (isRpcBalanceAsset(asset)) {
205
+ if (this.rpcBalanceAssetNames.includes(asset.name)) {
205
206
  const balance = balances[asset.name]
206
207
  newAccountState.balance = asset.currency.baseUnit(balance)
207
208
  }
@@ -244,13 +245,13 @@ export class ClarityMonitor extends BaseMonitor {
244
245
 
245
246
  async getBalances({ tokens, ourWalletAddress }) {
246
247
  const batch = Object.create(null)
247
- if (isRpcBalanceAsset(this.asset)) {
248
+ if (this.rpcBalanceAssetNames.includes(this.asset.name)) {
248
249
  const request = this.server.getBalanceRequest(ourWalletAddress)
249
250
  batch[this.asset.name] = request
250
251
  }
251
252
 
252
253
  for (const token of tokens) {
253
- if (isRpcBalanceAsset(token) && token.contract.address) {
254
+ if (this.rpcBalanceAssetNames.includes(token.name) && token.contract.address) {
254
255
  const request = this.server.balanceOfRequest(ourWalletAddress, token.contract.address)
255
256
  batch[token.name] = request
256
257
  }
@@ -1,5 +1,5 @@
1
1
  import { BaseMonitor } from '@exodus/asset-lib'
2
- import { getAssetAddresses, isRpcBalanceAsset } from '@exodus/ethereum-lib'
2
+ import { getAssetAddresses } from '@exodus/ethereum-lib'
3
3
  import lodash from 'lodash'
4
4
 
5
5
  import { executeEthLikeFeeMonitorUpdate } from '../fee-utils.js'
@@ -25,7 +25,7 @@ const { isEmpty } = lodash
25
25
  // formatting, and populating-to-state all ETH/ETC/ERC20 transactions.
26
26
 
27
27
  export class EthereumMonitor extends BaseMonitor {
28
- constructor({ server, config, webSocketEnabled = true, ...args }) {
28
+ constructor({ server, config, rpcBalanceAssetNames, webSocketEnabled = true, ...args }) {
29
29
  super(args)
30
30
  this.server = server
31
31
  this.config = { GAS_PRICE_FROM_WEBSOCKET: true, ...config }
@@ -38,6 +38,7 @@ export class EthereumMonitor extends BaseMonitor {
38
38
  this.addHook('after-stop', (...args) => this.afterStop(...args))
39
39
  this.subscribedToGasPriceMap = new Map()
40
40
  this._webSocketEnabled = webSocketEnabled
41
+ this.rpcBalanceAssetNames = rpcBalanceAssetNames
41
42
  }
42
43
 
43
44
  setServer(config = {}) {
@@ -215,7 +216,7 @@ export class EthereumMonitor extends BaseMonitor {
215
216
  const asset = this.asset
216
217
  const newAccountState = Object.create(null)
217
218
  const server = this.server
218
- if (isRpcBalanceAsset(asset)) {
219
+ if (this.rpcBalanceAssetNames.includes(asset.name)) {
219
220
  const result = await server.getBalanceProxied(ourWalletAddress)
220
221
  const balance = fromHexToString(result)
221
222
  newAccountState.balance = asset.currency.baseUnit(balance)
@@ -223,7 +224,7 @@ export class EthereumMonitor extends BaseMonitor {
223
224
 
224
225
  const tokenBalancePairs = await Promise.all(
225
226
  tokens
226
- .filter((token) => isRpcBalanceAsset(token) && token.contract.address)
227
+ .filter((token) => this.rpcBalanceAssetNames.includes(token.name) && token.contract.address)
227
228
  .map(async (token) => {
228
229
  const { confirmed } = await server.balanceOf(ourWalletAddress, token.contract.address)
229
230
  const value = token.currency.baseUnit(confirmed[token.contract.address] || 0)
@@ -7,5 +7,8 @@ export default function getFeeAmount(asset, serverTx) {
7
7
  // genesis, coinbase, uncles
8
8
  if (!gasPrice) return asset.currency.ZERO
9
9
 
10
- return asset.currency.baseUnit(gasUsed || gasLimit).mul(gasPrice)
10
+ return asset.currency
11
+ .baseUnit(gasUsed || gasLimit)
12
+ .mul(gasPrice)
13
+ .add(asset.currency.baseUnit(serverTx.extraData?.l1Fee || 0))
11
14
  }
@@ -158,12 +158,17 @@ const txSendFactory = ({ assetClientInterface, createUnsignedTx, useAbsoluteBala
158
158
  amount = (replacedTokenTx || replacedTx).coinAmount.negate()
159
159
  feeOpts.gasLimit = replacedTx.data.gasLimit
160
160
 
161
- const { gasPrice: currentGasPrice, baseFeePerGas: currentBaseFee } = feeData
161
+ const {
162
+ gasPrice: currentGasPrice,
163
+ baseFeePerGas: currentBaseFee,
164
+ tipGasPrice: currentTipGasPrice,
165
+ } = feeData
162
166
  const { bumpedGasPrice, bumpedTipGasPrice } = calculateBumpedGasPrice({
163
167
  baseAsset,
164
168
  tx: replacedTx,
165
169
  currentGasPrice,
166
170
  currentBaseFee,
171
+ currentTipGasPrice,
167
172
  eip1559Enabled,
168
173
  })
169
174
  feeOpts.gasPrice = bumpedGasPrice
@@ -231,9 +236,9 @@ const txSendFactory = ({ assetClientInterface, createUnsignedTx, useAbsoluteBala
231
236
 
232
237
  let { txId, rawTx, nonce, gasLimit, tipGasPrice, feeAmount } = await createTx(createTxParams)
233
238
 
234
- if (isPrivate && !baseAsset.api.features.transactionPrivacy)
239
+ if (isPrivate && typeof baseAsset.broadcastPrivateTx !== 'function')
235
240
  throw new Error(
236
- `unable to send private transaction - transactionPrivacy is not enabled for ${baseAsset.name}`
241
+ `unable to send private transaction - private mempools are not enabled for ${baseAsset.name}`
237
242
  )
238
243
 
239
244
  const broadcastTx = isPrivate ? baseAsset.broadcastPrivateTx : baseAsset.api.broadcastTx