@exodus/ethereum-api 8.40.1 → 8.41.0-alpha.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-alpha.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",
@@ -49,7 +49,7 @@
49
49
  "ws": "^6.1.0"
50
50
  },
51
51
  "devDependencies": {
52
- "@exodus/assets-testing": "^1.0.0",
52
+ "@exodus/assets-testing": "workspace:^",
53
53
  "@exodus/bsc-meta": "^2.1.2",
54
54
  "@exodus/ethereumarbone-meta": "^2.0.3",
55
55
  "@exodus/fantommainnet-meta": "^2.0.2",
@@ -62,6 +62,5 @@
62
62
  "repository": {
63
63
  "type": "git",
64
64
  "url": "git+https://github.com/ExodusMovement/assets.git"
65
- },
66
- "gitHead": "48a7c711842748ab188eb6bbbb575d1a13b8a6ab"
65
+ }
67
66
  }
@@ -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'
@@ -37,6 +37,7 @@ import getFeeAsyncFactory from './get-fee-async.js'
37
37
  import { estimateL1DataFeeFactory, getL1GetFeeFactory } from './optimism-gas/index.js'
38
38
  import { serverBasedFeeMonitorFactoryFactory } from './server-based-fee-monitor.js'
39
39
  import { createStakingApi } from './staking-api.js'
40
+ import { createTxFactory } from './tx-create.js'
40
41
  import { txSendFactory } from './tx-send/index.js'
41
42
  import { createWeb3API } from './web3/index.js'
42
43
 
@@ -64,6 +65,7 @@ export const createAssetFactory = ({
64
65
  stakingConfiguration = {},
65
66
  useEip1191ChainIdChecksum = false,
66
67
  forceGasLimitEstimation = false,
68
+ rpcBalanceAssetNames = [],
67
69
  supportsCustomFees: defaultSupportsCustomFees = false,
68
70
  useAbsoluteBalanceAndNonce = false,
69
71
  }) => {
@@ -145,8 +147,8 @@ export const createAssetFactory = ({
145
147
 
146
148
  const getBalances = getBalancesFactory({
147
149
  monitorType,
148
- config,
149
150
  useAbsoluteBalance: useAbsoluteBalanceAndNonce,
151
+ rpcBalanceAssetNames,
150
152
  })
151
153
 
152
154
  const { createToken, getTokens } = createTokenFactory(
@@ -170,11 +172,11 @@ export const createAssetFactory = ({
170
172
  server,
171
173
  })
172
174
 
173
- const { broadcastTx, broadcastPrivateTx } = createBroadcastTxFactory({
174
- assetName: asset.name,
175
- server,
176
- privacyRpcUrl,
177
- })
175
+ const { broadcastPrivateBundle, broadcastPrivateTx, privacyServer } =
176
+ createTransactionPrivacyFactory({
177
+ assetName: asset.name,
178
+ privacyRpcUrl,
179
+ })
178
180
 
179
181
  const features = {
180
182
  accountState: true,
@@ -185,7 +187,6 @@ export const createAssetFactory = ({
185
187
  isTestnet,
186
188
  nfts,
187
189
  noHistory: monitorType === 'no-history',
188
- transactionPrivacy: typeof broadcastPrivateTx === 'function',
189
190
  signWithSigner: true,
190
191
  signMessageWithSigner: true,
191
192
  supportsCustomFees,
@@ -214,16 +215,22 @@ export const createAssetFactory = ({
214
215
  monitorType,
215
216
  server,
216
217
  stakingAssetNames,
218
+ rpcBalanceAssetNames,
217
219
  })
218
220
 
219
221
  const defaultAddressPath = 'm/0/0'
220
222
 
221
223
  const createUnsignedTx = createUnsignedTxFactory({ chainId })
222
224
 
225
+ const createTx = createTxFactory({
226
+ chainId,
227
+ assetClientInterface,
228
+ useAbsoluteNonce: useAbsoluteBalanceAndNonce,
229
+ })
230
+
223
231
  const sendTx = txSendFactory({
224
232
  assetClientInterface,
225
- createUnsignedTx,
226
- useAbsoluteBalanceAndNonce,
233
+ createTx,
227
234
  })
228
235
 
229
236
  const estimateL1DataFee = l1GasOracleAddress
@@ -238,11 +245,12 @@ export const createAssetFactory = ({
238
245
 
239
246
  const api = {
240
247
  addressHasHistory,
241
- broadcastTx,
248
+ broadcastTx: (...args) => server.sendRawTransaction(...args),
242
249
  createAccountState: () => accountStateClass,
243
250
  createFeeMonitor,
244
251
  createHistoryMonitor,
245
252
  createToken,
253
+ createTx,
246
254
  createUnsignedTx,
247
255
  customFees: createCustomFeesApi({ baseAsset: asset }),
248
256
  defaultAddressPath,
@@ -252,7 +260,7 @@ export const createAssetFactory = ({
252
260
  getBalanceForAddress: createGetBalanceForAddress({ asset, server }),
253
261
  getConfirmationsNumber: () => confirmationsNumber,
254
262
  getDefaultAddressPath: () => defaultAddressPath,
255
- getFeeAsync: getFeeAsyncFactory({ assetClientInterface, gasLimit, createUnsignedTx }),
263
+ getFeeAsync: getFeeAsyncFactory({ assetClientInterface, gasLimit, createTx }),
256
264
  getFee,
257
265
  getFeeData: () => feeData,
258
266
  getKeyIdentifier: createGetKeyIdentifier({
@@ -289,8 +297,10 @@ export const createAssetFactory = ({
289
297
  chainId,
290
298
  monitorType,
291
299
  estimateL1DataFee,
300
+ broadcastPrivateBundle,
292
301
  broadcastPrivateTx,
293
302
  forceGasLimitEstimation,
303
+ privacyServer,
294
304
  server,
295
305
  ...(erc20FuelBuffer && { erc20FuelBuffer }),
296
306
  ...(fuelThreshold && { fuelThreshold: asset.currency.defaultUnit(fuelThreshold) }),
@@ -114,6 +114,7 @@ export async function fetchGasLimit({
114
114
  toAddress: providedToAddress,
115
115
  txInput: providedTxInput,
116
116
  amount: providedAmount,
117
+ contractAddress,
117
118
  bip70,
118
119
  throwOnError = true,
119
120
  }) {
@@ -146,7 +147,7 @@ export async function fetchGasLimit({
146
147
  toAddress: providedToAddress,
147
148
  })
148
149
 
149
- const txToAddress = isToken ? asset.contract.address : toAddress
150
+ const txToAddress = contractAddress ?? (isToken ? asset.contract.address : toAddress)
150
151
  const txAmount = isToken ? asset.baseAsset.currency.ZERO : amount
151
152
 
152
153
  try {
@@ -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,89 +1,21 @@
1
1
  import assert from 'minimalistic-assert'
2
2
 
3
- import { ARBITRARY_ADDRESS, fetchGasLimit, resolveDefaultTxInput } from './gas-estimation.js'
4
- import { getFeeFactory } from './get-fee.js'
5
- import { getNftArguments } from './nft-utils.js'
3
+ import { getExtraFeeData } from './get-fee.js'
6
4
 
7
- const getFeeAsyncFactory = ({
8
- assetClientInterface,
9
- gasLimit: defaultGasLimit,
10
- createUnsignedTx,
11
- }) => {
5
+ const getFeeAsyncFactory = ({ assetClientInterface, createTx }) => {
12
6
  assert(assetClientInterface, 'assetClientInterface is required')
13
- assert(createUnsignedTx, 'createUnsignedTx is required')
14
- const getFee = getFeeFactory({ gasLimit: defaultGasLimit })
15
- return async ({
16
- nft,
17
- asset,
18
- // provided are values from the UI or other services, they could be undefined
19
- fromAddress: providedFromAddress,
20
- toAddress: providedToAddress,
21
- amount: providedAmount,
22
- txInput: providedTxInput,
23
- gasLimit: providedGasLimit,
24
- bip70,
25
- customFee,
26
- feeData,
27
- }) => {
28
- const fromAddress = providedFromAddress || ARBITRARY_ADDRESS // sending from a random address
29
- const toAddress = providedToAddress || ARBITRARY_ADDRESS // sending to a random address,
30
- const amount = providedAmount ?? asset.currency.ZERO
31
- const resolveGasLimit = async () => {
32
- if (nft) {
33
- return getNftArguments({ asset, nft, fromAddress, toAddress })
34
- }
35
-
36
- const txInput = providedTxInput || resolveDefaultTxInput({ asset, toAddress, amount })
37
- if (providedGasLimit) return { gasLimit: providedGasLimit, txInput }
38
-
39
- const gasLimit = await fetchGasLimit({
40
- asset,
41
- fromAddress: providedFromAddress,
42
- toAddress: providedToAddress,
43
- txInput,
44
- amount,
45
- bip70,
46
- feeData,
47
- })
48
- return { gasLimit, txInput }
49
- }
50
-
51
- const { txInput, gasLimit, contractAddress } = await resolveGasLimit()
52
-
53
- const { fee, gasPrice, ...rest } = getFee({
54
- asset,
55
- feeData,
56
- gasLimit,
57
- amount,
58
- customFee,
59
- })
60
-
61
- const optimismL1DataFee = asset.baseAsset.estimateL1DataFee
62
- ? await asset.baseAsset.estimateL1DataFee({
63
- unsignedTx: createUnsignedTx({
64
- asset,
65
- address: contractAddress || toAddress,
66
- fromAddress,
67
- amount,
68
- nonce: 0,
69
- txInput,
70
- gasPrice,
71
- gasLimit,
72
- }),
73
- })
74
- : undefined
75
-
76
- const l1DataFee = optimismL1DataFee
77
- ? asset.baseAsset.currency.baseUnit(optimismL1DataFee)
78
- : asset.baseAsset.currency.ZERO
79
-
7
+ assert(createTx, 'createTx is required')
8
+
9
+ return async (params) => {
10
+ const { asset } = params
11
+ const { unsignedTx } = params.unsignedTx ? params : await createTx(params)
12
+ const fee = asset.feeAsset.currency.parse(unsignedTx.txMeta.fee)
13
+ const coinAmount = asset.currency.parse(unsignedTx.txMeta.amount)
14
+ const extraFeeData = getExtraFeeData({ asset, amount: coinAmount })
80
15
  return {
81
- fee: fee.add(l1DataFee),
82
- // TODO: Should this be `l1DataFee`?
83
- optimismL1DataFee,
84
- gasLimit,
85
- gasPrice,
86
- ...rest,
16
+ fee,
17
+ extraFeeData,
18
+ unsignedTx,
87
19
  }
88
20
  }
89
21
  }
package/src/get-fee.js CHANGED
@@ -11,7 +11,7 @@ const taxes = {
11
11
  paxgold: 0.0002,
12
12
  }
13
13
 
14
- const getExtraFeeData = ({ asset, amount }) => {
14
+ export const getExtraFeeData = ({ asset, amount }) => {
15
15
  const tax = taxes[asset.name]
16
16
  if (!amount || !tax || amount.isZero) {
17
17
  return {}
package/src/index.js CHANGED
@@ -81,9 +81,7 @@ export {
81
81
 
82
82
  export { reasons as errorReasons, withErrorReason, EthLikeError } from './error-wrapper.js'
83
83
 
84
- // TODO: `getFeeInfo` is not consumed by third parties and should
85
- // be considered an internal API.
86
- export { txSendFactory, getFeeInfo } from './tx-send/index.js'
84
+ export { txSendFactory } from './tx-send/index.js'
87
85
 
88
86
  export { createAssetFactory } from './create-asset.js'
89
87