@exodus/ethereum-api 8.40.1 → 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,24 @@
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
+
6
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)
7
25
 
8
26
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/ethereum-api",
3
- "version": "8.40.1",
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": "48a7c711842748ab188eb6bbbb575d1a13b8a6ab"
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) }),
@@ -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 {
@@ -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
  }
@@ -236,9 +236,9 @@ const txSendFactory = ({ assetClientInterface, createUnsignedTx, useAbsoluteBala
236
236
 
237
237
  let { txId, rawTx, nonce, gasLimit, tipGasPrice, feeAmount } = await createTx(createTxParams)
238
238
 
239
- if (isPrivate && !baseAsset.api.features.transactionPrivacy)
239
+ if (isPrivate && typeof baseAsset.broadcastPrivateTx !== 'function')
240
240
  throw new Error(
241
- `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}`
242
242
  )
243
243
 
244
244
  const broadcastTx = isPrivate ? baseAsset.broadcastPrivateTx : baseAsset.api.broadcastTx