@exodus/ethereum-api 8.35.0 → 8.36.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.36.0](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.35.1...@exodus/ethereum-api@8.36.0) (2025-06-11)
7
+
8
+
9
+ ### Features
10
+
11
+
12
+ * feat: ethereum api use balances and nonces data (#5727)
13
+
14
+
15
+ ### Bug Fixes
16
+
17
+
18
+ * fix: ethereum api cleaning up (#5832)
19
+
20
+ * fix: improve evm gas estimation when using arbitrary addresses (#5842)
21
+
22
+
23
+
24
+ ## [8.35.1](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.35.0...@exodus/ethereum-api@8.35.1) (2025-06-06)
25
+
26
+
27
+ ### Bug Fixes
28
+
29
+
30
+ * fix: hardcode gasLimit in client (#5815)
31
+
32
+
33
+
6
34
  ## [8.35.0](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.34.4...@exodus/ethereum-api@8.35.0) (2025-05-28)
7
35
 
8
36
 
@@ -399,7 +427,7 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline
399
427
  ### Bug Fixes
400
428
 
401
429
 
402
- * fix: use gasPriceMultipler when setting up min/max/recommended (#4700)
430
+ * fix: use gasPriceMultiplier when setting up min/max/recommended (#4700)
403
431
 
404
432
 
405
433
 
package/README.md CHANGED
@@ -13,7 +13,7 @@ Excluding custom fees, `feeData.gasPrice` is used to resolve the transaction's g
13
13
  The `gasPrice` value is, in preference order, loaded from:
14
14
 
15
15
  1. Remote config
16
- 2. The node via the fee data monitor and web socket. `gasPrice = baseFeePerGas * gasPriceMultipler + tipGasPrice`
16
+ 2. The node via the fee data monitor and web socket. `gasPrice = baseFeePerGas * gasPriceMultiplier + tipGasPrice`
17
17
  3. The default configuration.
18
18
 
19
19
  Using remote config is never recommended, as manually updating this value won't keep up with network activity.
@@ -42,7 +42,7 @@ The base gas price (or base fee) is the minimum amount of gas required to be pai
42
42
 
43
43
  It can be found in `feeData.baseFeePerGas`. The fee monitor keeps it up to date by monitoring the latest block's information. It could be tuned by remote config, but this is not recommended.
44
44
 
45
- The `feeData.baseFeePerGas` is used when showing an "estimated fee" in the UI and to calculate the max gas price. `gasPrice = baseFeePerGas * gasPriceMultipler + tipGasPrice`
45
+ The `feeData.baseFeePerGas` is used when showing an "estimated fee" in the UI and to calculate the max gas price. `gasPrice = baseFeePerGas * gasPriceMultiplier + tipGasPrice`
46
46
 
47
47
  ### Gas Limit
48
48
 
@@ -69,7 +69,7 @@ The L2 extra fee needs to be considered when showing the fee to the user, resolv
69
69
 
70
70
  Custom fees allow users to set a custom `gasPrice`. The wallet allows a range, as follows:
71
71
 
72
- Note: `gasPrice = baseFeePerGas * gasPriceMultipler + tipGasPrice`
72
+ Note: `gasPrice = baseFeePerGas * gasPriceMultiplier + tipGasPrice`
73
73
 
74
74
  - `recommended: gasPrice`
75
75
  - `min: gasPrice * gasPriceMinimumRate`
@@ -88,7 +88,7 @@ Min and max values can be tuned by changing `gasPriceMinimumRate` and `gasPriceM
88
88
 
89
89
  ### TX formulas
90
90
 
91
- Note: `gasPrice = baseFeePerGas * gasPriceMultipler + tipGasPrice`
91
+ Note: `gasPrice = baseFeePerGas * gasPriceMultiplier + tipGasPrice`
92
92
 
93
93
  Table shows how the txs are create for both legacy and 1559 logic for major use case:
94
94
 
@@ -108,7 +108,7 @@ This is the default fee data configuration for Ethereum. Other EVMs have differe
108
108
  All these values can be tuned via remote config, although it may not be recommended as they are also tuned by the fee monitors:
109
109
 
110
110
  - **`baseFeePerGas`:** `50 Gwei` – Reference value for the base gas price. It is quickly updated by the fee monitor.
111
- - **`gasPrice`:** `77 Gwei` – `baseFeePerGas * gasPriceMultipler + tipGasPrice`
111
+ - **`gasPrice`:** `77 Gwei` – `baseFeePerGas * gasPriceMultiplier + tipGasPrice`
112
112
  - **`severGasPrice`:** `75 Gwei` – Reference value for the network gas price. It is quickly updated by the fee monitor.
113
113
  - **`tipGasPrice`:** `2 Gwei` – Controls the `maxPriorityFeePerGas` for all transactions when EIP-1559 is enabled.
114
114
  - **`eip1559Enabled`:** `true` – Enables or disables EIP-1559. A value of `false` means legacy fees and transactions.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/ethereum-api",
3
- "version": "8.35.0",
3
+ "version": "8.36.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,12 +28,12 @@
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.10.2",
31
+ "@exodus/ethereum-lib": "^5.12.0",
32
32
  "@exodus/ethereum-meta": "^2.5.0",
33
33
  "@exodus/ethereumholesky-meta": "^2.0.2",
34
34
  "@exodus/ethereumjs": "^1.0.0",
35
35
  "@exodus/fetch": "^1.3.0",
36
- "@exodus/models": "^12.0.1",
36
+ "@exodus/models": "^12.13.0",
37
37
  "@exodus/simple-retry": "^0.0.6",
38
38
  "@exodus/solidity-contract": "^1.1.3",
39
39
  "@exodus/web3-ethereum-utils": "^4.2.1",
@@ -64,5 +64,5 @@
64
64
  "type": "git",
65
65
  "url": "git+https://github.com/ExodusMovement/assets.git"
66
66
  },
67
- "gitHead": "1dd642f94e801034ff66828f519579911a2dca18"
67
+ "gitHead": "2172464a34a03cb2c2401f80db89d4609cb0528f"
68
68
  }
@@ -65,6 +65,7 @@ export const createAssetFactory = ({
65
65
  useEip1191ChainIdChecksum = false,
66
66
  forceGasLimitEstimation = false,
67
67
  supportsCustomFees: defaultSupportsCustomFees = false,
68
+ useAbsoluteBalanceAndNonce = false,
68
69
  }) => {
69
70
  assert(assetsList, 'assetsList is required')
70
71
  assert(providedFeeData || feeDataConfig, 'feeData or feeDataConfig is required')
@@ -106,6 +107,7 @@ export const createAssetFactory = ({
106
107
  monitorType: overrideMonitorType,
107
108
  serverUrl: overrideServerUrl,
108
109
  monitorInterval: overrideMonitorInterval,
110
+ useAbsoluteBalanceAndNonce: overrideUseAbsoluteBalanceAndNonce,
109
111
  } = {
110
112
  ...defaultConfig,
111
113
  ...config,
@@ -126,6 +128,10 @@ export const createAssetFactory = ({
126
128
  }
127
129
  }
128
130
 
131
+ if (overrideUseAbsoluteBalanceAndNonce !== undefined) {
132
+ useAbsoluteBalanceAndNonce = overrideUseAbsoluteBalanceAndNonce
133
+ }
134
+
129
135
  monitorType = overrideMonitorType || monitorType
130
136
  const server = createEvmServer({ assetName: asset.name, serverUrl, monitorType })
131
137
 
@@ -149,6 +155,7 @@ export const createAssetFactory = ({
149
155
  const getBalances = getBalancesFactory({
150
156
  monitorType,
151
157
  config,
158
+ useAbsoluteBalance: useAbsoluteBalanceAndNonce,
152
159
  })
153
160
 
154
161
  const { createToken, getTokens } = createTokenFactory(
@@ -254,6 +261,7 @@ export const createAssetFactory = ({
254
261
  const sendTx = txSendFactory({
255
262
  assetClientInterface,
256
263
  createUnsignedTx,
264
+ useAbsoluteBalanceAndNonce,
257
265
  })
258
266
 
259
267
  const estimateL1DataFee = l1GasOracleAddress
@@ -130,4 +130,14 @@ export default class ClarityServerV2 extends ClarityServer {
130
130
  body: request,
131
131
  })
132
132
  }
133
+
134
+ async getTransactionCount(...params) {
135
+ // nonce is called during tx send, use it in rest api
136
+ const request = this.getTransactionCountRequest(...params)
137
+ return this.sendHttpRequest({
138
+ path: '/rpc',
139
+ method: 'POST',
140
+ body: request,
141
+ })
142
+ }
133
143
  }
package/src/fee-utils.js CHANGED
@@ -3,8 +3,8 @@ import assert from 'minimalistic-assert'
3
3
  const applyMultiplierToPriceInWei = ({ feeAsset, feeData, priceInWei }) => {
4
4
  assert(typeof priceInWei === 'string', 'gasPriceInWei should be a string')
5
5
 
6
- const multipler = feeData.gasPriceMultiplier || 1
7
- return feeAsset.currency.parse(priceInWei).mul(multipler).toBaseString({ unit: true })
6
+ const gasPriceMultiplier = feeData.gasPriceMultiplier || 1
7
+ return feeAsset.currency.parse(priceInWei).mul(gasPriceMultiplier).toBaseString({ unit: true })
8
8
  }
9
9
 
10
10
  /**
@@ -15,6 +15,15 @@ export const DEFAULT_CONTRACT_GAS_LIMIT = 1e6
15
15
  export const scaleGasLimitEstimate = ({ estimatedGasLimit, extraPercentage = EXTRA_PERCENTAGE }) =>
16
16
  Number((BigInt(estimatedGasLimit) * BigInt(100 + extraPercentage)) / BigInt(100))
17
17
 
18
+ const hardcodedGasLimits = new Map([
19
+ ['amp', 151_000],
20
+ ['tetherusd', 70_000],
21
+ ['usdcoin', 70_000],
22
+ ['snx', 220_000],
23
+ ['geminidollar', 75_000],
24
+ ['aave', 250_000],
25
+ ])
26
+
18
27
  // Starting with geth v1.9.14, if gasPrice is set for eth_estimateGas call, the call allowance will
19
28
  // be calculated with account's balance divided by gasPrice. If user's balance is too low,
20
29
  // the gasEstimation will fail. If gasPrice is set to '0x0', the account's balance is not
@@ -28,6 +37,11 @@ export async function estimateGasLimit(
28
37
  gasPrice = '0x',
29
38
  extraPercentage
30
39
  ) {
40
+ // move this to remote config
41
+ if (hardcodedGasLimits.has(asset.name)) {
42
+ return hardcodedGasLimits.get(asset.name)
43
+ }
44
+
31
45
  const opts = {
32
46
  from: fromAddress,
33
47
  to: toAddress,
@@ -74,18 +88,7 @@ export async function fetchGasLimit({
74
88
  if (isEthereumLikeToken(asset)) {
75
89
  amount = asset.baseAsset.currency.ZERO
76
90
  toAddress = asset.contract.address
77
- } else if (
78
- !isContract &&
79
- !asset.forceGasLimitEstimation &&
80
- ![
81
- // back compatiblity until all plugins are updated. Then remove this array
82
- 'ethereumarbone',
83
- 'ethereumarbonesepolia',
84
- 'ethereumarbnova',
85
- 'mantle',
86
- 'mantlesepolia',
87
- ].includes(asset.name)
88
- ) {
91
+ } else if (!isContract && !asset.forceGasLimitEstimation) {
89
92
  return defaultGasLimit()
90
93
  }
91
94
 
@@ -7,6 +7,44 @@ import {
7
7
  import { isRpcBalanceAsset } from '@exodus/ethereum-lib'
8
8
  import assert from 'minimalistic-assert'
9
9
 
10
+ export const getAbsoluteBalance = ({ asset, txLog }) => {
11
+ assert(asset, 'asset is required')
12
+ assert(txLog, 'txLog is required')
13
+
14
+ if (txLog.size === 0) {
15
+ return asset.currency.ZERO
16
+ }
17
+
18
+ let balance = asset.currency.ZERO
19
+ let hasAbsoluteBalance = false
20
+
21
+ for (const tx of txLog.reverse()) {
22
+ if (tx.data.balanceChange) {
23
+ hasAbsoluteBalance = true
24
+ balance = balance.add(asset.currency.baseUnit(tx.data.balanceChange.to))
25
+
26
+ break
27
+ }
28
+
29
+ if (!(tx.dropped || tx.data.replacedBy)) {
30
+ if (!tx.error) {
31
+ balance = balance.add(tx.coinAmount)
32
+ }
33
+
34
+ if (tx.feeAmount?.unitType.equals(tx.coinAmount.unitType)) {
35
+ balance = balance.sub(tx.feeAmount)
36
+ }
37
+ }
38
+ }
39
+
40
+ if (!hasAbsoluteBalance) {
41
+ console.warn('No absolute balance found', { assetName: asset.name, txLogSize: txLog.size })
42
+ return
43
+ }
44
+
45
+ return balance
46
+ }
47
+
10
48
  const getStaked = ({ accountState, asset }) => {
11
49
  return accountState?.staking?.[asset.name]?.activeStakedBalance || asset.currency.ZERO
12
50
  }
@@ -73,7 +111,11 @@ const getSpendable = ({ asset, balance, txLog, unconfirmedReceived }) => {
73
111
  * @param accountState the account state when the balance is loaded from RPC
74
112
  * @returns {{balance}|null} an object with the balance or null if the balance is unknown
75
113
  */
76
- export const getBalancesFactory = ({ monitorType, config = Object.create(null) }) => {
114
+ export const getBalancesFactory = ({
115
+ monitorType,
116
+ config = Object.create(null),
117
+ useAbsoluteBalance,
118
+ }) => {
77
119
  const { useAccountStateBalanceOnly } = config
78
120
 
79
121
  assert(monitorType, 'monitorType is required')
@@ -101,12 +143,19 @@ export const getBalancesFactory = ({ monitorType, config = Object.create(null) }
101
143
  } else {
102
144
  // Balance from txLog does not include staking rewards
103
145
  // spendable and total are calculated differently based on staking txs
104
- spendable = getSpendable({
105
- asset,
106
- balance: getBalanceFromTxLog({ txLog, asset }),
107
- txLog,
108
- unconfirmedReceived,
109
- })
146
+ const absoluteBalance = useAbsoluteBalance && getAbsoluteBalance({ asset, txLog })
147
+
148
+ if (absoluteBalance) {
149
+ spendable = absoluteBalance
150
+ } else {
151
+ spendable = getSpendable({
152
+ asset,
153
+ balance: getBalanceFromTxLog({ txLog, asset }),
154
+ txLog,
155
+ unconfirmedReceived,
156
+ })
157
+ }
158
+
110
159
  total = spendable.add(staked).add(staking).add(unstaking).add(unstaked)
111
160
  }
112
161
 
@@ -31,7 +31,22 @@ export async function resolveExtraPercentage({ asset, fromAddress, toAddress })
31
31
  return EXTRA_PERCENTAGE
32
32
  }
33
33
 
34
- const ARBITRARY_ADDRESS = '0xffffffffffffffffffffffffffffffffffffffff'
34
+ // HACK: If a recipient address is not defined, we usually fall back to
35
+ // default address so gas estimation can still complete successfully
36
+ // without knowledge of which accounts are involved.
37
+ //
38
+ // However, we must be careful to select addresses which are unlikely
39
+ // to have existing obligations, such as popular dead addresses or the
40
+ // reserved addresses of precompiles, since these can influence gas
41
+ // estimation.
42
+ //
43
+ // Here, we use an address which is mostly all `1`s to make sure we can
44
+ // exaggerate the worst-case calldata cost (which is priced per high bit)
45
+ // whilst being unlikely to have any token balances.
46
+ //
47
+ // Unfortunately, we can't use `0xffffffffffffffffffffffffffffffffffffffff`,
48
+ // since this address is a whale.
49
+ const ARBITRARY_ADDRESS = '0xfffFfFfFfFfFFFFFfeFfFFFffFffFFFFfFFFFFFF'.toLowerCase()
35
50
 
36
51
  const getFeeAsyncFactory = ({
37
52
  assetClientInterface,
@@ -51,8 +66,8 @@ const getFeeAsyncFactory = ({
51
66
  txInput: txInputPram,
52
67
  isExchange,
53
68
  customFee,
54
- calculateEffectiveFee,
55
69
  gasLimit: providedGasLimit,
70
+ feeData,
56
71
  }) => {
57
72
  const fromAddress = providedFromAddress || ARBITRARY_ADDRESS // sending from a random address
58
73
  const toAddress = providedToAddress || ARBITRARY_ADDRESS // sending to a random address,
@@ -86,15 +101,12 @@ const getFeeAsyncFactory = ({
86
101
 
87
102
  const { txInput, gasLimit, contractAddress } = await resolveGasLimit()
88
103
 
89
- const feeData = await assetClientInterface.getFeeConfig({ assetName: asset.baseAsset.name })
90
-
91
104
  const { fee, gasPrice, ...rest } = getFee({
92
105
  asset,
93
106
  feeData,
94
107
  gasLimit,
95
108
  isExchange,
96
109
  amount,
97
- calculateEffectiveFee,
98
110
  customFee,
99
111
  })
100
112
 
package/src/get-fee.js CHANGED
@@ -41,17 +41,9 @@ export const getFeeFactoryGasPrices = ({ customFee, feeData }) => {
41
41
 
42
42
  export const getFeeFactory =
43
43
  ({ gasLimit: defaultGasLimit }) =>
44
- ({
45
- asset,
46
- feeData,
47
- customFee,
48
- gasLimit: providedGasLimit,
49
- isExchange,
50
- amount,
51
- calculateEffectiveFee,
52
- }) => {
44
+ ({ asset, feeData, customFee, gasLimit: providedGasLimit, isExchange, amount }) => {
53
45
  const {
54
- feeData: { baseFeePerGas, tipGasPrice, eip1559Enabled },
46
+ feeData: { tipGasPrice, eip1559Enabled },
55
47
  gasPrice,
56
48
  } = getFeeFactoryGasPrices({
57
49
  customFee,
@@ -65,19 +57,6 @@ export const getFeeFactory =
65
57
  const maybeReturnTipGasPrice = eip1559Enabled ? { tipGasPrice } : null
66
58
 
67
59
  const extraFeeData = getExtraFeeData({ asset, amount })
68
- if (calculateEffectiveFee && eip1559Enabled) {
69
- const maxFeePerGas = gasPrice
70
- // effective_gas_price = min(base_fee_per_gas + tip_gas_price, max_fee_per_gas)
71
- const feePerGas = baseFeePerGas.add(tipGasPrice)
72
- const effectiveGasPrice = feePerGas.lt(maxFeePerGas) ? feePerGas : maxFeePerGas
73
-
74
- return {
75
- ...maybeReturnTipGasPrice,
76
- fee: effectiveGasPrice.mul(gasLimit),
77
- gasPrice,
78
- extraFeeData,
79
- }
80
- }
81
60
 
82
61
  const fee = gasPrice.mul(gasLimit)
83
62
  return { ...maybeReturnTipGasPrice, fee, gasPrice, extraFeeData }
@@ -1,5 +1,6 @@
1
1
  import lodash from 'lodash'
2
2
 
3
+ import { getWalletUpdates } from '../monitor-utils/get-balance-updates.js'
3
4
  import getFeeAmount from '../monitor-utils/get-fee-amount.js'
4
5
  import getTransfersByTokenName from '../monitor-utils/get-transfers-by-token-name.js'
5
6
  import getValueOfTransfers from '../monitor-utils/get-value-of-transfers.js'
@@ -36,6 +37,8 @@ export default function getLogItemsFromServerTx({
36
37
  methodId: serverTx.input.slice(0, Math.max(0, METHOD_ID_LENGTH)),
37
38
  }
38
39
  const data = serverTx.input || '0x'
40
+ const walletUpdates = getWalletUpdates(ourWalletAddress, serverTx.walletChanges || [])
41
+ const { baseBalanceUpdate, nonceUpdate, tokenBalancesUpdate } = walletUpdates
39
42
 
40
43
  const logItemCommonProperties = {
41
44
  confirmations,
@@ -51,7 +54,10 @@ export default function getLogItemsFromServerTx({
51
54
  const sendingTransferPresent = ethereumTransfers.some(({ from }) => from === ourWalletAddress)
52
55
  const receivingTransferPresent = ethereumTransfers.some(({ to }) => to === ourWalletAddress)
53
56
 
54
- if (sendingTransferPresent || receivingTransferPresent) {
57
+ const shouldAttachTx =
58
+ sendingTransferPresent || receivingTransferPresent || baseBalanceUpdate || nonceUpdate // need to also include if there's a base balance change
59
+
60
+ if (shouldAttachTx) {
55
61
  const coinAmount = getValueOfTransfers(ourWalletAddress, asset, ethereumTransfers)
56
62
  const selfSend = isSelfSendTx({
57
63
  coinAmount,
@@ -81,6 +87,8 @@ export default function getLogItemsFromServerTx({
81
87
  data,
82
88
  nonce,
83
89
  gasLimit,
90
+ balanceChange: baseBalanceUpdate,
91
+ nonceChange: nonceUpdate,
84
92
  ...methodId,
85
93
  ...(sent?.length > 0 ? { sent } : undefined),
86
94
  },
@@ -109,12 +117,15 @@ export default function getLogItemsFromServerTx({
109
117
  Object.entries(tokenTransfersByTokenName).forEach(([tokenName, tokenTransfers]) => {
110
118
  const sendingTransferPresent = tokenTransfers.some(({ from }) => from === ourWalletAddress)
111
119
  const receivingTransferPresent = tokenTransfers.some(({ to }) => to === ourWalletAddress)
112
- if (!sendingTransferPresent && !receivingTransferPresent) return
113
-
114
120
  const token = assets[tokenName]
115
- const tokenTransferToAddress = tryFindExternalRecipient(tokenTransfers, ourWalletAddress)
121
+ const tokenAddress = token.addresses.current.toLowerCase()
122
+ const balanceChange = tokenBalancesUpdate[tokenAddress]
116
123
 
124
+ if (!sendingTransferPresent && !receivingTransferPresent && !balanceChange) return
125
+
126
+ const tokenTransferToAddress = tryFindExternalRecipient(tokenTransfers, ourWalletAddress)
117
127
  const coinAmount = getValueOfTransfers(ourWalletAddress, token, tokenTransfers)
128
+
118
129
  const tokenFromAddresses = lodash.uniq(
119
130
  tokenTransfers.filter(({ to }) => to === ourWalletAddress).map(({ from }) => from)
120
131
  )
@@ -138,7 +149,7 @@ export default function getLogItemsFromServerTx({
138
149
  ...logItemCommonProperties,
139
150
  coinAmount,
140
151
  coinName: tokenName,
141
- data: { data, nonce, gasLimit, ...methodId },
152
+ data: { data, nonce, gasLimit, balanceChange, ...methodId },
142
153
  ...(isConsideredSent
143
154
  ? { from: [], to: tokenTransferToAddress, feeAmount, feeCoinName: token.feeAsset.name }
144
155
  : { from: tokenFromAddresses }),
@@ -0,0 +1,44 @@
1
+ export const getWalletUpdates = (ourWalletAddress, walletChanges) => {
2
+ return walletChanges.reduce(
3
+ (acc, walletChange) => {
4
+ const { wallet: walletAddress, type, from, to, contract: contractAddress } = walletChange
5
+
6
+ if (walletAddress !== ourWalletAddress) {
7
+ return acc
8
+ }
9
+
10
+ switch (type) {
11
+ case 'balance':
12
+ acc.baseBalanceUpdate = {
13
+ from,
14
+ to,
15
+ }
16
+ break
17
+ case 'nonce':
18
+ acc.nonceUpdate = {
19
+ from,
20
+ to,
21
+ }
22
+ break
23
+ case 'token':
24
+ acc.tokenBalancesUpdate = {
25
+ ...acc.tokenBalancesUpdate,
26
+ [contractAddress]: {
27
+ from,
28
+ to,
29
+ },
30
+ }
31
+ break
32
+ default:
33
+ break
34
+ }
35
+
36
+ return acc
37
+ },
38
+ {
39
+ baseBalanceUpdate: undefined,
40
+ nonceUpdate: undefined,
41
+ tokenBalancesUpdate: Object.create(null),
42
+ }
43
+ )
44
+ }
@@ -8,15 +8,14 @@ export const resolveNonce = async ({
8
8
  txLog,
9
9
  triedNonce,
10
10
  tag = 'latest', // use 'pending' for unconfirmed txs
11
+ useAbsoluteNonce,
11
12
  }) => {
12
13
  const nonceFromNode =
13
14
  asset.baseAsset?.api?.features?.noHistory || forceFromNode
14
15
  ? await getNonce({ asset: asset.baseAsset, address: fromAddress, tag })
15
16
  : 0
16
17
 
17
- const nonceFromLog = [...txLog]
18
- .filter((tx) => tx.sent && !tx.dropped && tx.data.nonce != null)
19
- .reduce((nonce, tx) => Math.max(tx.data.nonce + 1, nonce), 0)
18
+ const nonceFromLog = getNonceFromTxLog({ txLog, useAbsoluteNonce })
20
19
 
21
20
  return Math.max(
22
21
  nonceFromNode,
@@ -25,3 +24,20 @@ export const resolveNonce = async ({
25
24
  triedNonce === undefined ? 0 : triedNonce + 1
26
25
  )
27
26
  }
27
+
28
+ export const getNonceFromTxLog = ({ txLog, useAbsoluteNonce }) => {
29
+ let absoluteNonce = 0
30
+ if (useAbsoluteNonce) {
31
+ for (const tx of txLog.reverse()) {
32
+ if (tx.data.nonceChange) {
33
+ absoluteNonce = parseInt(tx.data.nonceChange.to, 10)
34
+ }
35
+ }
36
+ }
37
+
38
+ const nonceFromLog = [...txLog]
39
+ .filter((tx) => tx.sent && !tx.dropped && tx.data.nonce != null)
40
+ .reduce((nonce, tx) => Math.max(tx.data.nonce + 1, nonce), 0)
41
+
42
+ return Math.max(absoluteNonce, nonceFromLog)
43
+ }
@@ -67,7 +67,7 @@ export const HACK_maybeRefineSendAllAmount = async ({
67
67
  }
68
68
  }
69
69
 
70
- const txSendFactory = ({ assetClientInterface, createUnsignedTx }) => {
70
+ const txSendFactory = ({ assetClientInterface, createUnsignedTx, useAbsoluteBalanceAndNonce }) => {
71
71
  assert(assetClientInterface, 'assetClientInterface is required')
72
72
  assert(createUnsignedTx, 'createUnsignedTx is required')
73
73
  return async ({ asset, walletAccount, address, amount, feeData: maybeFeeData, options = {} }) => {
@@ -207,6 +207,7 @@ const txSendFactory = ({ assetClientInterface, createUnsignedTx }) => {
207
207
  // we'll fall back to the `TxLog` since this also has a knowledge
208
208
  // of which transactions are currently in pending.
209
209
  tag: 'pending',
210
+ useAbsoluteNonce: useAbsoluteBalanceAndNonce,
210
211
  }))
211
212
 
212
213
  const createTxParams = {