@exodus/ethereum-api 8.62.3 → 8.63.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 +20 -0
- package/package.json +2 -2
- package/src/create-asset-utils.js +12 -8
- package/src/custom-fees.js +1 -1
- package/src/get-balances.js +1 -59
- package/src/tx-log/clarity-utils/absolute.js +92 -0
- package/src/tx-log/clarity-utils/index.js +6 -0
- package/src/tx-send/nonce-utils.js +47 -16
- package/src/tx-send/tx-send.js +9 -2
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,26 @@
|
|
|
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.63.0](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.62.4...@exodus/ethereum-api@8.63.0) (2026-01-14)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
* feat: add 'latest' evm nonce support (#7267)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
## [8.62.4](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.62.3...@exodus/ethereum-api@8.62.4) (2026-01-14)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
### Bug Fixes
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
* fix: absolute canonical nonce (#7238)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
6
26
|
## [8.62.3](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.62.2...@exodus/ethereum-api@8.62.3) (2026-01-13)
|
|
7
27
|
|
|
8
28
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/ethereum-api",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.63.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",
|
|
@@ -67,5 +67,5 @@
|
|
|
67
67
|
"type": "git",
|
|
68
68
|
"url": "git+https://github.com/ExodusMovement/assets.git"
|
|
69
69
|
},
|
|
70
|
-
"gitHead": "
|
|
70
|
+
"gitHead": "db8bd0629b287cd2031a12ff2a3fed3fc765bbab"
|
|
71
71
|
}
|
|
@@ -7,7 +7,7 @@ import { ClarityMonitor } from './tx-log/clarity-monitor.js'
|
|
|
7
7
|
import { ClarityMonitorV2 } from './tx-log/clarity-monitor-v2.js'
|
|
8
8
|
import { EthereumMonitor } from './tx-log/ethereum-monitor.js'
|
|
9
9
|
import { EthereumNoHistoryMonitor } from './tx-log/ethereum-no-history-monitor.js'
|
|
10
|
-
import { resolveNonce } from './tx-send/nonce-utils.js'
|
|
10
|
+
import { BLOCK_TAG_PENDING, resolveNonce } from './tx-send/nonce-utils.js'
|
|
11
11
|
|
|
12
12
|
// Determines the appropriate `monitorType`, `serverUrl` and `monitorInterval`
|
|
13
13
|
// to use for a given config.
|
|
@@ -184,7 +184,16 @@ export const getNonceFactory = ({ assetClientInterface, useAbsoluteBalanceAndNon
|
|
|
184
184
|
assert(assetClientInterface, 'expected assetClientInterface')
|
|
185
185
|
assert(typeof useAbsoluteBalanceAndNonce === 'boolean', 'expected useAbsoluteBalanceAndNonce')
|
|
186
186
|
|
|
187
|
-
const getNonce = async ({
|
|
187
|
+
const getNonce = async ({
|
|
188
|
+
asset,
|
|
189
|
+
fromAddress,
|
|
190
|
+
walletAccount,
|
|
191
|
+
forceFromNode,
|
|
192
|
+
// NOTE: By default, Exodus assumes the client will by default want
|
|
193
|
+
// to send transactions on top of those which are currently
|
|
194
|
+
// pending at the public mempool.
|
|
195
|
+
tag = BLOCK_TAG_PENDING,
|
|
196
|
+
}) => {
|
|
188
197
|
assert(asset, 'expected asset')
|
|
189
198
|
assert(typeof fromAddress === 'string', 'expected fromAddress')
|
|
190
199
|
assert(walletAccount, 'expected walletAccount')
|
|
@@ -198,13 +207,8 @@ export const getNonceFactory = ({ assetClientInterface, useAbsoluteBalanceAndNon
|
|
|
198
207
|
asset,
|
|
199
208
|
fromAddress,
|
|
200
209
|
txLog,
|
|
201
|
-
triedNonce,
|
|
202
210
|
forceFromNode,
|
|
203
|
-
|
|
204
|
-
// search for pending transactions. For base assets with history,
|
|
205
|
-
// we'll fall back to the `TxLog` since this also has a knowledge
|
|
206
|
-
// of which transactions are currently in pending.
|
|
207
|
-
tag: 'pending',
|
|
211
|
+
tag,
|
|
208
212
|
useAbsoluteNonce: useAbsoluteBalanceAndNonce,
|
|
209
213
|
})
|
|
210
214
|
}
|
package/src/custom-fees.js
CHANGED
|
@@ -51,7 +51,7 @@ export const createCustomFeesApi = ({ baseAsset }) => {
|
|
|
51
51
|
},
|
|
52
52
|
unit: 'gwei/gas',
|
|
53
53
|
feeUnitPriceToNumber: (feeNumberUnit) => feeNumberUnit.toNumber(Gwei),
|
|
54
|
-
numberToFeeUnitPrice: (value) =>
|
|
54
|
+
numberToFeeUnitPrice: (value) => NumberUnit.create(value, Gwei),
|
|
55
55
|
isEnabled: ({ feeData }) => Boolean(feeData.rbfEnabled),
|
|
56
56
|
}
|
|
57
57
|
}
|
package/src/get-balances.js
CHANGED
|
@@ -5,66 +5,8 @@ import {
|
|
|
5
5
|
getUnconfirmedSentBalance,
|
|
6
6
|
} from '@exodus/asset-lib'
|
|
7
7
|
import assert from 'minimalistic-assert'
|
|
8
|
-
import ms from 'ms'
|
|
9
|
-
|
|
10
|
-
const DEFAULT_CANONICAL_ABSOLUTE_BALANCE_SEARCH_DEPTH = ms('5m')
|
|
11
|
-
|
|
12
|
-
// Defines whether a `Tx` is an absolute balance
|
|
13
|
-
// node that can be sorted in terms of transaction
|
|
14
|
-
// ordering.
|
|
15
|
-
//
|
|
16
|
-
export const isCanonicalAbsoluteBalanceTx = (tx) =>
|
|
17
|
-
Boolean(
|
|
18
|
-
tx?.data?.balanceChange &&
|
|
19
|
-
typeof tx.data.blockNumber === 'number' &&
|
|
20
|
-
typeof tx.data.transactionIndex === 'number'
|
|
21
|
-
)
|
|
22
|
-
|
|
23
|
-
// Attempts to find the most recent `absoluteBalance` Tx for
|
|
24
|
-
// a given `txLog`.
|
|
25
|
-
//
|
|
26
|
-
// NOTE: Returns `null` if nothing is found.
|
|
27
|
-
// NOTE: Uses `tx.data.blockNumber` and `tx.data.transactionIndex`,
|
|
28
|
-
// which are not guaranteed to exist in older transaction
|
|
29
|
-
// histories.
|
|
30
|
-
export const getLatestCanonicalAbsoluteBalanceTx = ({
|
|
31
|
-
// Due to nondeterministic sorting for records in close
|
|
32
|
-
// proximity to one-another on high TPS chains, the
|
|
33
|
-
// order of the `reversedTxLog` cannot be trusted to be
|
|
34
|
-
// exactly precise since two consecutive blocks may share
|
|
35
|
-
// the same timestamp. Therefore, when finding an absolute
|
|
36
|
-
// balance node, we use the`searchDepthMs` to take into
|
|
37
|
-
// account extra nodes chronologically deeper than what we
|
|
38
|
-
// presume to be the latest to make sure we resolve to the
|
|
39
|
-
// correct node.
|
|
40
|
-
searchDepthMs = DEFAULT_CANONICAL_ABSOLUTE_BALANCE_SEARCH_DEPTH,
|
|
41
|
-
reversedTxLog,
|
|
42
|
-
}) => {
|
|
43
|
-
assert(reversedTxLog, 'expected reversedTxLog')
|
|
44
|
-
|
|
45
|
-
let latest = null
|
|
46
|
-
|
|
47
|
-
for (const tx of reversedTxLog) {
|
|
48
|
-
if (latest) {
|
|
49
|
-
const diff = +latest.date - +tx.date
|
|
50
|
-
|
|
51
|
-
if (diff >= searchDepthMs) break
|
|
52
|
-
}
|
|
53
8
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
if (
|
|
57
|
-
!latest ||
|
|
58
|
-
tx.data.blockNumber > latest.data.blockNumber ||
|
|
59
|
-
(tx.data.blockNumber === latest.data.blockNumber &&
|
|
60
|
-
tx.data.transactionIndex > latest.data.transactionIndex)
|
|
61
|
-
) {
|
|
62
|
-
latest = tx
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
return latest
|
|
67
|
-
}
|
|
9
|
+
import { getLatestCanonicalAbsoluteBalanceTx } from './tx-log/clarity-utils/index.js'
|
|
68
10
|
|
|
69
11
|
/**
|
|
70
12
|
* TODO: Balance model gaps for EVM staking assets
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import assert from 'minimalistic-assert'
|
|
2
|
+
import ms from 'ms'
|
|
3
|
+
|
|
4
|
+
const DEFAULT_CANONICAL_ABSOLUTE_TX_SEARCH_DEPTH = ms('5m')
|
|
5
|
+
|
|
6
|
+
const ABSOLUTE_FIELD_NAME_BALANCE_CHANGE = 'balanceChange'
|
|
7
|
+
const ABSOLUTE_FIELD_NAME_NONCE_CHANGE = 'nonceChange'
|
|
8
|
+
|
|
9
|
+
const ABSOLUTE_BALANCE_FIELDS = new Set([
|
|
10
|
+
ABSOLUTE_FIELD_NAME_BALANCE_CHANGE,
|
|
11
|
+
ABSOLUTE_FIELD_NAME_NONCE_CHANGE,
|
|
12
|
+
])
|
|
13
|
+
|
|
14
|
+
// Defines whether a `Tx` is an absolute balance
|
|
15
|
+
// node that can be sorted in terms of transaction
|
|
16
|
+
// ordering.
|
|
17
|
+
//
|
|
18
|
+
const isCanonicalAbsoluteTx = (tx, fieldName) => {
|
|
19
|
+
assert(ABSOLUTE_BALANCE_FIELDS.has(fieldName), `unsupported fieldName, "${fieldName}"`)
|
|
20
|
+
|
|
21
|
+
return Boolean(
|
|
22
|
+
tx?.data?.[fieldName] &&
|
|
23
|
+
typeof tx.data.blockNumber === 'number' &&
|
|
24
|
+
typeof tx.data.transactionIndex === 'number'
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const isCanonicalAbsoluteBalanceTx = (tx) =>
|
|
29
|
+
isCanonicalAbsoluteTx(tx, ABSOLUTE_FIELD_NAME_BALANCE_CHANGE)
|
|
30
|
+
export const isCanonicalAbsoluteNonceTx = (tx) =>
|
|
31
|
+
isCanonicalAbsoluteTx(tx, ABSOLUTE_FIELD_NAME_NONCE_CHANGE)
|
|
32
|
+
|
|
33
|
+
// Attempts to find the most recent absolute Tx for a given
|
|
34
|
+
// `txLog`.
|
|
35
|
+
//
|
|
36
|
+
// NOTE: Returns `null` if nothing is found.
|
|
37
|
+
// NOTE: Uses `tx.data.blockNumber` and `tx.data.transactionIndex`,
|
|
38
|
+
// which are not guaranteed to exist in older transaction
|
|
39
|
+
// histories.
|
|
40
|
+
const getLatestCanonicalAbsoluteTx = ({
|
|
41
|
+
// Due to nondeterministic sorting for records in close
|
|
42
|
+
// proximity to one-another on high TPS chains, the
|
|
43
|
+
// order of the `reversedTxLog` cannot be trusted to be
|
|
44
|
+
// exactly precise since two consecutive blocks may share
|
|
45
|
+
// the same timestamp. Therefore, when finding an absolute
|
|
46
|
+
// balance node, we use the`searchDepthMs` to take into
|
|
47
|
+
// account extra nodes chronologically deeper than what we
|
|
48
|
+
// presume to be the latest to make sure we resolve to the
|
|
49
|
+
// correct node.
|
|
50
|
+
searchDepthMs = DEFAULT_CANONICAL_ABSOLUTE_TX_SEARCH_DEPTH,
|
|
51
|
+
reversedTxLog,
|
|
52
|
+
fieldName,
|
|
53
|
+
}) => {
|
|
54
|
+
assert(reversedTxLog, 'expected reversedTxLog')
|
|
55
|
+
|
|
56
|
+
let latest = null
|
|
57
|
+
|
|
58
|
+
for (const tx of reversedTxLog) {
|
|
59
|
+
if (latest) {
|
|
60
|
+
const diff = +latest.date - +tx.date
|
|
61
|
+
|
|
62
|
+
if (diff >= searchDepthMs) break
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (!isCanonicalAbsoluteTx(tx, fieldName)) continue
|
|
66
|
+
|
|
67
|
+
if (
|
|
68
|
+
!latest ||
|
|
69
|
+
tx.data.blockNumber > latest.data.blockNumber ||
|
|
70
|
+
(tx.data.blockNumber === latest.data.blockNumber &&
|
|
71
|
+
tx.data.transactionIndex > latest.data.transactionIndex)
|
|
72
|
+
) {
|
|
73
|
+
latest = tx
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return latest
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export const getLatestCanonicalAbsoluteBalanceTx = ({ searchDepthMs, reversedTxLog }) =>
|
|
81
|
+
getLatestCanonicalAbsoluteTx({
|
|
82
|
+
searchDepthMs,
|
|
83
|
+
reversedTxLog,
|
|
84
|
+
fieldName: ABSOLUTE_FIELD_NAME_BALANCE_CHANGE,
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
export const getLatestCanonicalAbsoluteNonceTx = ({ searchDepthMs, reversedTxLog }) =>
|
|
88
|
+
getLatestCanonicalAbsoluteTx({
|
|
89
|
+
searchDepthMs,
|
|
90
|
+
reversedTxLog,
|
|
91
|
+
fieldName: ABSOLUTE_FIELD_NAME_NONCE_CHANGE,
|
|
92
|
+
})
|
|
@@ -1,2 +1,8 @@
|
|
|
1
1
|
export { default as getLogItemsFromServerTx } from './get-log-items-from-server-tx.js'
|
|
2
2
|
export { default as filterEffects } from './filter-effects.js'
|
|
3
|
+
export {
|
|
4
|
+
isCanonicalAbsoluteBalanceTx,
|
|
5
|
+
isCanonicalAbsoluteNonceTx,
|
|
6
|
+
getLatestCanonicalAbsoluteBalanceTx,
|
|
7
|
+
getLatestCanonicalAbsoluteNonceTx,
|
|
8
|
+
} from './absolute.js'
|
|
@@ -1,44 +1,75 @@
|
|
|
1
|
+
import assert from 'minimalistic-assert'
|
|
2
|
+
|
|
1
3
|
import { getNonce } from '../eth-like-util.js'
|
|
4
|
+
import { getLatestCanonicalAbsoluteNonceTx } from '../tx-log/clarity-utils/absolute.js'
|
|
5
|
+
|
|
6
|
+
export const BLOCK_TAG_LATEST = 'latest'
|
|
7
|
+
export const BLOCK_TAG_PENDING = 'pending'
|
|
8
|
+
|
|
9
|
+
const VALID_BLOCK_TAGS = new Set([BLOCK_TAG_LATEST, BLOCK_TAG_PENDING])
|
|
10
|
+
|
|
11
|
+
// NOTE: We do not yet support tags at arbitrary block heights.
|
|
12
|
+
const assertValidBlockTag = (tag) => assert(VALID_BLOCK_TAGS.has(tag), `invalid tag "${tag}"`)
|
|
2
13
|
|
|
3
14
|
export const resolveNonce = async ({
|
|
4
15
|
asset,
|
|
5
16
|
forceFromNode,
|
|
6
17
|
fromAddress,
|
|
7
|
-
providedNonce,
|
|
8
18
|
txLog = [],
|
|
9
|
-
|
|
10
|
-
tag = 'latest', // use 'pending' for unconfirmed txs
|
|
19
|
+
tag = BLOCK_TAG_LATEST,
|
|
11
20
|
useAbsoluteNonce,
|
|
12
21
|
}) => {
|
|
22
|
+
assertValidBlockTag(tag)
|
|
23
|
+
|
|
13
24
|
const nonceFromNode =
|
|
14
25
|
asset.baseAsset?.api?.features?.noHistory || forceFromNode
|
|
15
26
|
? await getNonce({ asset: asset.baseAsset, address: fromAddress, tag })
|
|
16
27
|
: 0
|
|
17
28
|
|
|
18
|
-
const nonceFromLog = getNonceFromTxLog({ txLog, useAbsoluteNonce })
|
|
29
|
+
const nonceFromLog = getNonceFromTxLog({ txLog, useAbsoluteNonce, tag })
|
|
30
|
+
|
|
31
|
+
return Math.max(nonceFromNode, nonceFromLog)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const getLatestTxWithNonceChange = ({ reversedTxLog }) => {
|
|
35
|
+
assert(reversedTxLog, 'expected reversedTxLog')
|
|
36
|
+
|
|
37
|
+
const maybeLatestAbsoluteNonceTx = getLatestCanonicalAbsoluteNonceTx({
|
|
38
|
+
reversedTxLog,
|
|
39
|
+
})
|
|
19
40
|
|
|
20
|
-
return
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
41
|
+
if (maybeLatestAbsoluteNonceTx) return maybeLatestAbsoluteNonceTx
|
|
42
|
+
|
|
43
|
+
for (const tx of reversedTxLog) {
|
|
44
|
+
if (tx.data.nonceChange) {
|
|
45
|
+
return tx
|
|
46
|
+
}
|
|
47
|
+
}
|
|
26
48
|
}
|
|
27
49
|
|
|
28
|
-
|
|
50
|
+
const getNonceFromTxLog = ({ txLog, useAbsoluteNonce, tag }) => {
|
|
29
51
|
let absoluteNonce = 0
|
|
52
|
+
|
|
30
53
|
if (useAbsoluteNonce) {
|
|
31
54
|
const reversedTxLog = txLog.reverse()
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
55
|
+
const maybeLatestTxWithNonceChange = getLatestTxWithNonceChange({ reversedTxLog })
|
|
56
|
+
|
|
57
|
+
if (maybeLatestTxWithNonceChange) {
|
|
58
|
+
// NOTE: This is the latest nonce currently confirmed by
|
|
59
|
+
// Clarity, which lags behind `CLARITY_MIN_CONFIRMS`
|
|
60
|
+
// depth:
|
|
61
|
+
//
|
|
62
|
+
// https://github.com/ExodusMovement/clarity/blob/4a6ea30fce33246d1ef1440e61b0a84b876900d6/deployment/eth-clarity-indexer/values.yaml#L34
|
|
63
|
+
absoluteNonce = parseInt(maybeLatestTxWithNonceChange.data.nonceChange.to, 10)
|
|
37
64
|
}
|
|
38
65
|
}
|
|
39
66
|
|
|
40
67
|
const nonceFromLog = [...txLog]
|
|
41
68
|
.filter((tx) => tx.sent && !tx.dropped && tx.data.nonce != null)
|
|
69
|
+
// NOTE: If we're only considering the `'latest'` block `tag`,
|
|
70
|
+
// then we should not take into account unconfirmed
|
|
71
|
+
// transactions when computing the `nonce`.
|
|
72
|
+
.filter((tx) => tag !== BLOCK_TAG_LATEST || !tx.pending)
|
|
42
73
|
.reduce((nonce, tx) => Math.max(tx.data.nonce + 1, nonce), 0)
|
|
43
74
|
|
|
44
75
|
return Math.max(absoluteNonce, nonceFromLog)
|
package/src/tx-send/tx-send.js
CHANGED
|
@@ -83,15 +83,22 @@ const txSendFactory = ({ assetClientInterface, createTx }) => {
|
|
|
83
83
|
})
|
|
84
84
|
} else if (nonceTooLowErr && !unsignedTx.txMeta.isHardware) {
|
|
85
85
|
console.info('trying to send again...') // inject logger factory from platform
|
|
86
|
+
|
|
86
87
|
// let's try to fix the nonce issue
|
|
87
|
-
|
|
88
|
+
let newNonce = await baseAsset.getNonce({
|
|
88
89
|
asset,
|
|
89
90
|
fromAddress,
|
|
90
91
|
walletAccount,
|
|
91
|
-
triedNonce: parsedTx.nonce,
|
|
92
92
|
forceFromNode: true,
|
|
93
93
|
})
|
|
94
94
|
|
|
95
|
+
// TODO: We should only do this for non-replacement transactions.
|
|
96
|
+
const triedNonce = parsedTx.nonce
|
|
97
|
+
|
|
98
|
+
if (typeof triedNonce === 'number') {
|
|
99
|
+
newNonce = Math.max(newNonce, triedNonce + 1)
|
|
100
|
+
}
|
|
101
|
+
|
|
95
102
|
// NOTE: An `unsignedTx.txData.transactionBuffer` may be optional
|
|
96
103
|
// in `unsignedTx`, since a `providedUnsignedTx` may be
|
|
97
104
|
// truthy but composed only of `legacyParams`:
|