@exodus/ethereum-api 8.41.0-alpha.0 → 8.42.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,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.42.0](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.41.0...@exodus/ethereum-api@8.42.0) (2025-07-10)
7
+
8
+
9
+ ### Features
10
+
11
+
12
+ * feat: rig `getNonce` to evm asset api (#6047)
13
+
14
+
15
+
6
16
  ## [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
17
 
8
18
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/ethereum-api",
3
- "version": "8.41.0-alpha.0",
3
+ "version": "8.42.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",
@@ -49,7 +49,7 @@
49
49
  "ws": "^6.1.0"
50
50
  },
51
51
  "devDependencies": {
52
- "@exodus/assets-testing": "workspace:^",
52
+ "@exodus/assets-testing": "^1.0.0",
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,5 +62,6 @@
62
62
  "repository": {
63
63
  "type": "git",
64
64
  "url": "git+https://github.com/ExodusMovement/assets.git"
65
- }
65
+ },
66
+ "gitHead": "6bd56e57bee720ffef602f57421eae67e42e190c"
66
67
  }
@@ -1,3 +1,6 @@
1
+ import { isNumberUnit } from '@exodus/currency'
2
+ import assert from 'minimalistic-assert'
3
+
1
4
  import { getServer } from '../exodus-eth-server/index.js'
2
5
  import { fetchGasLimit } from '../gas-estimation.js'
3
6
  import { ALLOWANCE_TX_TIMEOUT, APPROVAL_GAS_LIMIT, ZERO_ALLOWANCE_ASSETS } from './constants.js'
@@ -27,6 +30,52 @@ export async function isSpendingApprovalRequired({
27
30
  return tokenAmount.gt(allowance)
28
31
  }
29
32
 
33
+ export const buildApproveTx = async ({
34
+ spenderAddress,
35
+ asset,
36
+ feeData,
37
+ fromAddress,
38
+ approveAmount = asset.currency.ZERO,
39
+ gasLimit,
40
+ txInput,
41
+ }) => {
42
+ assert(asset, 'expected asset')
43
+ assert(feeData, 'expected feeData')
44
+ assert(isNumberUnit(approveAmount), 'expected approveAmount')
45
+ assert(typeof spenderAddress === 'string', 'expected spenderAddress')
46
+
47
+ const baseAsset = asset.baseAsset
48
+ const toAddress = asset.contract.address
49
+
50
+ if (!txInput) txInput = asset.contract.approve.build(spenderAddress, approveAmount.toBaseString())
51
+
52
+ if (!gasLimit)
53
+ gasLimit = await fetchGasLimit({
54
+ asset: baseAsset,
55
+ amount: baseAsset.currency.ZERO,
56
+ toAddress,
57
+ txInput,
58
+ feeData,
59
+ isContract: true,
60
+ fromAddress,
61
+ })
62
+
63
+ return {
64
+ asset: baseAsset.name,
65
+ receiver: {
66
+ address: toAddress,
67
+ amount: baseAsset.currency.ZERO,
68
+ },
69
+ txInput,
70
+ gasPrice: feeData.gasPrice,
71
+ tipGasPrice: feeData.tipGasPrice,
72
+ gasLimit,
73
+ silent: true,
74
+ fromAddress,
75
+ }
76
+ }
77
+
78
+ // @deprecated use buildApproveTx instead
30
79
  export const createApprove =
31
80
  ({ sendTx }) =>
32
81
  async ({
@@ -36,43 +85,23 @@ export const createApprove =
36
85
  fromAddress,
37
86
  approveAmount,
38
87
  gasLimit,
39
- txInput: preTxInput,
40
- ...rest
88
+ txInput,
89
+ ...extras
41
90
  }) => {
42
- const baseAsset = asset.baseAsset
43
- const txInput =
44
- preTxInput ?? asset.contract.approve.build(spenderAddress, approveAmount.toBaseString())
45
-
46
- const toAddress = asset.contract.address
47
- const gasLimitOptions = {
48
- asset: baseAsset,
49
- amount: baseAsset.currency.ZERO,
50
- toAddress,
51
- txInput,
91
+ const approveTx = await buildApproveTx({
92
+ spenderAddress,
93
+ asset,
52
94
  feeData,
53
- isContract: true,
54
95
  fromAddress,
55
- }
56
-
57
- gasLimit = gasLimit || (await fetchGasLimit(gasLimitOptions))
58
-
59
- const txData = await sendTx({
60
- asset: baseAsset.name,
61
- receiver: {
62
- address: toAddress,
63
- amount: baseAsset.currency.ZERO,
64
- },
65
- txInput,
66
- gasPrice: feeData.gasPrice,
96
+ approveAmount,
67
97
  gasLimit,
68
- silent: true,
69
- fromAddress,
70
- ...rest,
98
+ txInput,
71
99
  })
72
100
 
73
- if (!txData || !txData.txId) {
101
+ const txData = await sendTx({ ...extras, ...approveTx })
102
+
103
+ if (!txData || !txData.txId)
74
104
  throw new Error(`Failed to approve ${asset.displayTicker} - ${spenderAddress}`)
75
- }
76
105
 
77
106
  return txData
78
107
  }
@@ -6,6 +6,7 @@ import { createEthereumHooks } from './hooks/index.js'
6
6
  import { ClarityMonitor } from './tx-log/clarity-monitor.js'
7
7
  import { EthereumMonitor } from './tx-log/ethereum-monitor.js'
8
8
  import { EthereumNoHistoryMonitor } from './tx-log/ethereum-no-history-monitor.js'
9
+ import { resolveNonce } from './tx-send/nonce-utils.js'
9
10
 
10
11
  // Determines the appropriate `monitorType`, `serverUrl` and `monitorInterval`
11
12
  // to use for a given config.
@@ -151,3 +152,33 @@ export const createHistoryMonitorFactory = ({
151
152
  return monitor
152
153
  }
153
154
  }
155
+
156
+ export const getNonceFactory = ({ assetClientInterface, useAbsoluteBalanceAndNonce }) => {
157
+ assert(assetClientInterface, 'expected assetClientInterface')
158
+ assert(typeof useAbsoluteBalanceAndNonce === 'boolean', 'expected useAbsoluteBalanceAndNonce')
159
+
160
+ const getNonce = async ({ asset, fromAddress, walletAccount }) => {
161
+ assert(asset, 'expected asset')
162
+ assert(typeof fromAddress === 'string', 'expected fromAddress')
163
+ assert(walletAccount, 'expected walletAccount')
164
+
165
+ const txLog = await assetClientInterface.getTxLog({
166
+ assetName: asset.baseAsset.name,
167
+ walletAccount,
168
+ })
169
+
170
+ return resolveNonce({
171
+ asset,
172
+ fromAddress,
173
+ txLog,
174
+ // For assets where we'll fall back to querying the coin node, we
175
+ // search for pending transactions. For base assets with history,
176
+ // we'll fall back to the `TxLog` since this also has a knowledge
177
+ // of which transactions are currently in pending.
178
+ tag: 'pending',
179
+ useAbsoluteNonce: useAbsoluteBalanceAndNonce,
180
+ })
181
+ }
182
+
183
+ return { getNonce }
184
+ }
@@ -24,6 +24,7 @@ import { addressHasHistoryFactory } from './address-has-history.js'
24
24
  import {
25
25
  createHistoryMonitorFactory,
26
26
  createTransactionPrivacyFactory,
27
+ getNonceFactory,
27
28
  resolveMonitorSettings,
28
29
  } from './create-asset-utils.js'
29
30
  import { createTokenFactory } from './create-token-factory.js'
@@ -37,7 +38,6 @@ import getFeeAsyncFactory from './get-fee-async.js'
37
38
  import { estimateL1DataFeeFactory, getL1GetFeeFactory } from './optimism-gas/index.js'
38
39
  import { serverBasedFeeMonitorFactoryFactory } from './server-based-fee-monitor.js'
39
40
  import { createStakingApi } from './staking-api.js'
40
- import { createTxFactory } from './tx-create.js'
41
41
  import { txSendFactory } from './tx-send/index.js'
42
42
  import { createWeb3API } from './web3/index.js'
43
43
 
@@ -222,15 +222,10 @@ export const createAssetFactory = ({
222
222
 
223
223
  const createUnsignedTx = createUnsignedTxFactory({ chainId })
224
224
 
225
- const createTx = createTxFactory({
226
- chainId,
227
- assetClientInterface,
228
- useAbsoluteNonce: useAbsoluteBalanceAndNonce,
229
- })
230
-
231
225
  const sendTx = txSendFactory({
232
226
  assetClientInterface,
233
- createTx,
227
+ createUnsignedTx,
228
+ useAbsoluteBalanceAndNonce,
234
229
  })
235
230
 
236
231
  const estimateL1DataFee = l1GasOracleAddress
@@ -243,6 +238,8 @@ export const createAssetFactory = ({
243
238
  ? getL1GetFeeFactory({ asset, originalGetFee })
244
239
  : originalGetFee
245
240
 
241
+ const { getNonce } = getNonceFactory({ assetClientInterface, useAbsoluteBalanceAndNonce })
242
+
246
243
  const api = {
247
244
  addressHasHistory,
248
245
  broadcastTx: (...args) => server.sendRawTransaction(...args),
@@ -250,7 +247,6 @@ export const createAssetFactory = ({
250
247
  createFeeMonitor,
251
248
  createHistoryMonitor,
252
249
  createToken,
253
- createTx,
254
250
  createUnsignedTx,
255
251
  customFees: createCustomFeesApi({ baseAsset: asset }),
256
252
  defaultAddressPath,
@@ -260,7 +256,7 @@ export const createAssetFactory = ({
260
256
  getBalanceForAddress: createGetBalanceForAddress({ asset, server }),
261
257
  getConfirmationsNumber: () => confirmationsNumber,
262
258
  getDefaultAddressPath: () => defaultAddressPath,
263
- getFeeAsync: getFeeAsyncFactory({ assetClientInterface, gasLimit, createTx }),
259
+ getFeeAsync: getFeeAsyncFactory({ assetClientInterface, gasLimit, createUnsignedTx }),
264
260
  getFee,
265
261
  getFeeData: () => feeData,
266
262
  getKeyIdentifier: createGetKeyIdentifier({
@@ -300,6 +296,7 @@ export const createAssetFactory = ({
300
296
  broadcastPrivateBundle,
301
297
  broadcastPrivateTx,
302
298
  forceGasLimitEstimation,
299
+ getNonce,
303
300
  privacyServer,
304
301
  server,
305
302
  ...(erc20FuelBuffer && { erc20FuelBuffer }),
@@ -114,7 +114,6 @@ export async function fetchGasLimit({
114
114
  toAddress: providedToAddress,
115
115
  txInput: providedTxInput,
116
116
  amount: providedAmount,
117
- contractAddress,
118
117
  bip70,
119
118
  throwOnError = true,
120
119
  }) {
@@ -147,7 +146,7 @@ export async function fetchGasLimit({
147
146
  toAddress: providedToAddress,
148
147
  })
149
148
 
150
- const txToAddress = contractAddress ?? (isToken ? asset.contract.address : toAddress)
149
+ const txToAddress = isToken ? asset.contract.address : toAddress
151
150
  const txAmount = isToken ? asset.baseAsset.currency.ZERO : amount
152
151
 
153
152
  try {
@@ -1,21 +1,89 @@
1
1
  import assert from 'minimalistic-assert'
2
2
 
3
- import { getExtraFeeData } from './get-fee.js'
3
+ import { ARBITRARY_ADDRESS, fetchGasLimit, resolveDefaultTxInput } from './gas-estimation.js'
4
+ import { getFeeFactory } from './get-fee.js'
5
+ import { getNftArguments } from './nft-utils.js'
4
6
 
5
- const getFeeAsyncFactory = ({ assetClientInterface, createTx }) => {
7
+ const getFeeAsyncFactory = ({
8
+ assetClientInterface,
9
+ gasLimit: defaultGasLimit,
10
+ createUnsignedTx,
11
+ }) => {
6
12
  assert(assetClientInterface, 'assetClientInterface is required')
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 })
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
+
15
80
  return {
16
- fee,
17
- extraFeeData,
18
- unsignedTx,
81
+ fee: fee.add(l1DataFee),
82
+ // TODO: Should this be `l1DataFee`?
83
+ optimismL1DataFee,
84
+ gasLimit,
85
+ gasPrice,
86
+ ...rest,
19
87
  }
20
88
  }
21
89
  }
package/src/get-fee.js CHANGED
@@ -11,7 +11,7 @@ const taxes = {
11
11
  paxgold: 0.0002,
12
12
  }
13
13
 
14
- export const getExtraFeeData = ({ asset, amount }) => {
14
+ 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
@@ -61,6 +61,7 @@ export {
61
61
  isZeroAllowanceAsset,
62
62
  getSpendingAllowance,
63
63
  isSpendingApprovalRequired,
64
+ buildApproveTx,
64
65
  createApprove,
65
66
  createApproveSpendingTokens,
66
67
  APPROVAL_GAS_LIMIT,
@@ -81,7 +82,9 @@ export {
81
82
 
82
83
  export { reasons as errorReasons, withErrorReason, EthLikeError } from './error-wrapper.js'
83
84
 
84
- export { txSendFactory } from './tx-send/index.js'
85
+ // TODO: `getFeeInfo` is not consumed by third parties and should
86
+ // be considered an internal API.
87
+ export { txSendFactory, getFeeInfo } from './tx-send/index.js'
85
88
 
86
89
  export { createAssetFactory } from './create-asset.js'
87
90
 
@@ -0,0 +1,58 @@
1
+ import { ensureSaneEip1559GasPriceForTipGasPrice } from '../fee-utils.js'
2
+ import { fetchGasLimit } from '../gas-estimation.js'
3
+ import { getFeeFactoryGasPrices } from '../get-fee.js'
4
+
5
+ const getFeeInfo = async function getFeeInfo({
6
+ assetClientInterface,
7
+ asset,
8
+ fromAddress,
9
+ toAddress,
10
+ amount,
11
+ txInput,
12
+ feeOpts = {},
13
+ feeData,
14
+ customFee,
15
+ }) {
16
+ // HACK: Previously, calls `getFeeInfo` were not provided a reference
17
+ // to `feeData`. For backwards compatibility, we'll revert to
18
+ // legacy behaviour.
19
+ // NOTE: This shouldn't actually be used outside of the `assets` repo;
20
+ // this is done just for safety.
21
+ if (!feeData) {
22
+ console.warn('`getFeeInfo` was not explicitly passed a `feeData` object.')
23
+ const { name: assetName } = asset
24
+ feeData = await assetClientInterface.getFeeData({ assetName })
25
+ }
26
+
27
+ const {
28
+ gasPrice: gasPrice_,
29
+ feeData: { tipGasPrice: tipGasPrice_, eip1559Enabled },
30
+ } = getFeeFactoryGasPrices({ customFee, feeData })
31
+
32
+ const tipGasPrice = feeOpts.tipGasPrice || tipGasPrice_
33
+
34
+ const maybeGasPrice = feeOpts.gasPrice || gasPrice_
35
+
36
+ // HACK: If we've received an invalid combination of `tipGasPrice`
37
+ // (maxPriorityFeePerGas) and `gasPrice` (maxFeePerGas), then
38
+ // we must normalize these before returning.
39
+ const gasPrice = eip1559Enabled
40
+ ? ensureSaneEip1559GasPriceForTipGasPrice({ gasPrice: maybeGasPrice, tipGasPrice })
41
+ : maybeGasPrice
42
+
43
+ const gasLimit =
44
+ feeOpts.gasLimit ||
45
+ (await fetchGasLimit({
46
+ asset,
47
+ fromAddress,
48
+ toAddress,
49
+ amount,
50
+ txInput,
51
+ feeData,
52
+ throwOnError: false,
53
+ }))
54
+
55
+ return { gasPrice, gasLimit, tipGasPrice, eip1559Enabled }
56
+ }
57
+
58
+ export default getFeeInfo
@@ -1 +1,2 @@
1
1
  export { default as txSendFactory } from './tx-send.js'
2
+ export { default as getFeeInfo } from './get-fee-info.js'
@@ -5,7 +5,7 @@ export const resolveNonce = async ({
5
5
  forceFromNode,
6
6
  fromAddress,
7
7
  providedNonce,
8
- txLog = [],
8
+ txLog,
9
9
  triedNonce,
10
10
  tag = 'latest', // use 'pending' for unconfirmed txs
11
11
  useAbsoluteNonce,