@exodus/ethereum-api 8.78.2 → 8.79.1
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 +75 -0
- package/package.json +3 -3
- package/src/allowance/index.js +1 -3
- package/src/create-asset-utils.js +5 -5
- package/src/create-asset.js +8 -7
- package/src/create-token-factory.js +2 -0
- package/src/ens/index.js +0 -2
- package/src/exodus-eth-server/api-coin-nodes.js +7 -2
- package/src/exodus-eth-server/api.js +3 -3
- package/src/exodus-eth-server/clarity.js +3 -3
- package/src/exodus-eth-server/eth-like-server.js +17 -9
- package/src/exodus-eth-server/index.js +0 -6
- package/src/fee-data/EthLikeFeeData.js +0 -2
- package/src/hooks/monitor.js +161 -33
- package/src/hooks/staking-freshness.js +65 -0
- package/src/index.js +2 -0
- package/src/simulation/simulate-message-api.js +1 -1
- package/src/simulation/simulate-transactions-api.js +1 -1
- package/src/staking/api/index.js +9 -1
- package/src/staking/cached-read.js +33 -0
- package/src/staking/ethereum/api.js +52 -1
- package/src/staking/ethereum/deps.js +5 -1
- package/src/staking/ethereum/everstake.js +7 -0
- package/src/staking/ethereum/service.js +31 -23
- package/src/staking/utils/index.js +0 -2
- package/src/tx-log/clarity-monitor.js +59 -23
- package/src/tx-log/ethereum-monitor.js +54 -23
- package/src/tx-log/get-optimistic-txlog-effects.js +1 -1
- package/src/tx-log/monitor-utils/check-pending-transactions.js +12 -3
- package/src/tx-log/monitor-utils/get-derive-transactions-to-check.js +7 -3
- package/src/tx-log/monitor-utils/get-transfers-by-token-name.js +3 -2
- package/src/tx-log/monitor-utils/index.js +1 -0
- package/src/tx-log/monitor-utils/is-unverifiable-pending-tx-expired.js +19 -0
- package/src/tx-log/monitor-utils/verify-pending-tx-status-rpc.js +34 -11
- package/src/tx-log-staking-processor/utils.js +1 -1
- package/src/tx-send/broadcast-error-handler.js +2 -2
- package/src/tx-send/tx-send.js +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,81 @@
|
|
|
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.79.1](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.77.1...@exodus/ethereum-api@8.79.1) (2026-09-09)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* source eip7702 delegation whitelist from remote config ([#8356](https://github.com/ExodusMovement/assets/issues/8356)) ([1a0966b](https://github.com/ExodusMovement/assets/commit/1a0966b08befb6e9310993f323c21d003dea2754))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
### Bug Fixes
|
|
15
|
+
|
|
16
|
+
* disable @exodus/hydra/no-param-reassign-prop-only warnings ([#8531](https://github.com/ExodusMovement/assets/issues/8531)) ([ddae556](https://github.com/ExodusMovement/assets/commit/ddae556361285919fe82cd10a81cfeae79ccc6b5))
|
|
17
|
+
* **ethereum-api:** detect legacy delegate transactions ([#8357](https://github.com/ExodusMovement/assets/issues/8357)) ([070e529](https://github.com/ExodusMovement/assets/commit/070e5295f21f12695e97e721ff8c0601c553e447))
|
|
18
|
+
* **ethereum-api:** preserve live pending txs across refresh to prevent nonce reuse ([#8430](https://github.com/ExodusMovement/assets/issues/8430)) ([a24ad82](https://github.com/ExodusMovement/assets/commit/a24ad82d303ce50606143bc5d1d917fbbad45ef1))
|
|
19
|
+
* **ethereum-api:** preserve precision for wei values in tx simulation ([#7920](https://github.com/ExodusMovement/assets/issues/7920)) ([5c99258](https://github.com/ExodusMovement/assets/commit/5c9925871d450167fcf23e33ba62e100a2d71a9b))
|
|
20
|
+
* **ethereum-api:** reference defined ethereum asset in simulate-tx tests ([#8525](https://github.com/ExodusMovement/assets/issues/8525)) ([27a8918](https://github.com/ExodusMovement/assets/commit/27a8918f5ba3112f1830ac0111a43d30921aa3b3))
|
|
21
|
+
* **ethereum:** prevent silently dropped zero-tip private bundles ([#8493](https://github.com/ExodusMovement/assets/issues/8493)) ([67dd1ff](https://github.com/ExodusMovement/assets/commit/67dd1ffd7e1c22e7119b1781968c4be321ab1822))
|
|
22
|
+
* **lint:** add explicit process imports and enforce no-implicit-process ([#8530](https://github.com/ExodusMovement/assets/issues/8530)) ([885c29d](https://github.com/ExodusMovement/assets/commit/885c29decb1db363673da5be91b2bc6d06da8a6c))
|
|
23
|
+
* **lint:** convert console.log/info to console.debug and enforce no-console ([#8529](https://github.com/ExodusMovement/assets/issues/8529)) ([81cf5e9](https://github.com/ExodusMovement/assets/commit/81cf5e90b8157b084cf4cb311c02803c29e622e4))
|
|
24
|
+
* **lint:** fix import/named warnings and upgrade to error ([#8496](https://github.com/ExodusMovement/assets/issues/8496)) ([8d290c9](https://github.com/ExodusMovement/assets/commit/8d290c948a0d3c2c1897287cb133f9d26cad73b0))
|
|
25
|
+
* **lint:** fix JSDoc warnings across multiple packages ([#8519](https://github.com/ExodusMovement/assets/issues/8519)) ([5fefcd5](https://github.com/ExodusMovement/assets/commit/5fefcd5af996efbe9587cecba540efc7f469ceb2))
|
|
26
|
+
* **lint:** fix no-throw-literal warnings and upgrade to error ([#8498](https://github.com/ExodusMovement/assets/issues/8498)) ([8fc68eb](https://github.com/ExodusMovement/assets/commit/8fc68eb65682e0b5a7da9415410ee1dedf309696))
|
|
27
|
+
* **lint:** fix oxlint warnings and convert rules to error ([#8489](https://github.com/ExodusMovement/assets/issues/8489)) ([19d35be](https://github.com/ExodusMovement/assets/commit/19d35be718391303d331978926884f2f376b356f))
|
|
28
|
+
* **lint:** migrate lodash to @exodus/basic-utils ([#8514](https://github.com/ExodusMovement/assets/issues/8514)) ([12c574f](https://github.com/ExodusMovement/assets/commit/12c574f1ac8ab0592700df574b2643141237090a))
|
|
29
|
+
* **simulation:** route recipient changes to dedicated field, guard asset-less entries ([#8370](https://github.com/ExodusMovement/assets/issues/8370)) ([c0a082b](https://github.com/ExodusMovement/assets/commit/c0a082b6c80710408337ff9285a19fb657c656be))
|
|
30
|
+
* upgrade low-complexity lint rules to error ([#8510](https://github.com/ExodusMovement/assets/issues/8510)) ([25016bd](https://github.com/ExodusMovement/assets/commit/25016bdf43526c6bc9efe2771f92f1ae1ebf7cd7))
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
### Performance Improvements
|
|
34
|
+
|
|
35
|
+
* **ethereum:** reduce background staking request volume ([#8617](https://github.com/ExodusMovement/assets/issues/8617)) ([a9746d9](https://github.com/ExodusMovement/assets/commit/a9746d98ee0a2a04f5fd0131d3f1228cbc0aacf2))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
## [8.79.0](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.77.1...@exodus/ethereum-api@8.79.0) (2026-08-18)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
### Features
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
* feat: source eip7702 delegation whitelist from remote config (#8356)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
### Bug Fixes
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
* fix: disable @exodus/hydra/no-param-reassign-prop-only warnings (#8531)
|
|
52
|
+
|
|
53
|
+
* fix(ethereum-api): detect legacy delegate transactions (#8357)
|
|
54
|
+
|
|
55
|
+
* fix(ethereum-api): preserve live pending txs across refresh to prevent nonce reuse (#8430)
|
|
56
|
+
|
|
57
|
+
* fix(ethereum-api): preserve precision for wei values in tx simulation (#7920)
|
|
58
|
+
|
|
59
|
+
* fix(ethereum-api): reference defined ethereum asset in simulate-tx tests (#8525)
|
|
60
|
+
|
|
61
|
+
* fix(ethereum): prevent silently dropped zero-tip private bundles (#8493)
|
|
62
|
+
|
|
63
|
+
* fix(lint): add explicit process imports and enforce no-implicit-process (#8530)
|
|
64
|
+
|
|
65
|
+
* fix(lint): convert console.log/info to console.debug and enforce no-console (#8529)
|
|
66
|
+
|
|
67
|
+
* fix(lint): fix import/named warnings and upgrade to error (#8496)
|
|
68
|
+
|
|
69
|
+
* fix(lint): fix JSDoc warnings across multiple packages (#8519)
|
|
70
|
+
|
|
71
|
+
* fix(lint): fix no-throw-literal warnings and upgrade to error (#8498)
|
|
72
|
+
|
|
73
|
+
* fix(lint): fix oxlint warnings and convert rules to error (#8489)
|
|
74
|
+
|
|
75
|
+
* fix(simulation): route recipient changes to dedicated field, guard asset-less entries (#8370)
|
|
76
|
+
|
|
77
|
+
* fix: upgrade low-complexity lint rules to error (#8510)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
|
|
6
81
|
## [8.78.2](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.77.1...@exodus/ethereum-api@8.78.2) (2026-08-14)
|
|
7
82
|
|
|
8
83
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/ethereum-api",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.79.1",
|
|
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",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"provenance": false
|
|
18
18
|
},
|
|
19
19
|
"scripts": {
|
|
20
|
-
"test": "
|
|
20
|
+
"test": "exodus-test --jest"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"@exodus/asset": "^2.0.4",
|
|
@@ -67,5 +67,5 @@
|
|
|
67
67
|
"type": "git",
|
|
68
68
|
"url": "git+https://github.com/ExodusMovement/assets.git"
|
|
69
69
|
},
|
|
70
|
-
"gitHead": "
|
|
70
|
+
"gitHead": "53bdb90b8e90413468dc13cf8960ef2aa64dac4b"
|
|
71
71
|
}
|
package/src/allowance/index.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
// eslint-disable-next-line @exodus/import/no-deprecated
|
|
2
1
|
import { isNumberUnit } from '@exodus/currency'
|
|
3
2
|
import { buffer2currency, createUnsignedTxFactory } from '@exodus/ethereum-lib'
|
|
4
3
|
import { bufferToInt } from '@exodus/ethereumjs/util'
|
|
@@ -46,7 +45,6 @@ export const buildApproveTx = async ({
|
|
|
46
45
|
}) => {
|
|
47
46
|
assert(asset, 'expected asset')
|
|
48
47
|
assert(feeData, 'expected feeData')
|
|
49
|
-
// eslint-disable-next-line @exodus/import/no-deprecated
|
|
50
48
|
assert(isNumberUnit(approveAmount), 'expected approveAmount')
|
|
51
49
|
assert(typeof spenderAddress === 'string', 'expected spenderAddress')
|
|
52
50
|
assert(walletAccount, 'expected walletAccount')
|
|
@@ -231,7 +229,7 @@ export const createApproveSpendingTokens =
|
|
|
231
229
|
|
|
232
230
|
txIds.push(revokeTokenApprovalTxId)
|
|
233
231
|
// Hardcoded min gas limit. Otherwise `fetchGasLimit` will fail since `approve(_spender, 0)` is not yet confirmed
|
|
234
|
-
// eslint-disable-next-line @exodus/
|
|
232
|
+
// eslint-disable-next-line @exodus/hydra/no-param-reassign-prop-only
|
|
235
233
|
data.gasLimit = Math.max(data.gasLimit || 0, APPROVAL_GAS_LIMIT)
|
|
236
234
|
}
|
|
237
235
|
|
|
@@ -62,7 +62,7 @@ export const resolveMonitorSettings = (
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
const fallbackWithWarning = (msg) => {
|
|
65
|
-
console.
|
|
65
|
+
console.debug([msg, `Falling back to ${defaultMonitorType}<${defaultServerUrl}>.`].join(' '))
|
|
66
66
|
return defaultResolution
|
|
67
67
|
}
|
|
68
68
|
|
|
@@ -317,7 +317,7 @@ export const createHistoryMonitorFactory = ({
|
|
|
317
317
|
let monitor
|
|
318
318
|
switch (monitorType) {
|
|
319
319
|
case 'clarity-v3':
|
|
320
|
-
console.
|
|
320
|
+
console.debug('clarity-v3 is no longer supported, falling back to clarity-v2')
|
|
321
321
|
// eslint-disable-next-line no-fallthrough
|
|
322
322
|
case 'clarity':
|
|
323
323
|
case 'clarity-v2':
|
|
@@ -365,13 +365,13 @@ export const createHistoryMonitorFactory = ({
|
|
|
365
365
|
}
|
|
366
366
|
|
|
367
367
|
if (stakingAssetNames.length > 0) {
|
|
368
|
-
const
|
|
368
|
+
const hooks = createEthereumHooks({
|
|
369
369
|
assetClientInterface,
|
|
370
370
|
assetName,
|
|
371
371
|
stakingAssetNames,
|
|
372
372
|
server,
|
|
373
|
-
})
|
|
374
|
-
monitor.addHook(
|
|
373
|
+
})
|
|
374
|
+
for (const [name, hook] of Object.entries(hooks)) monitor.addHook(name, hook)
|
|
375
375
|
}
|
|
376
376
|
|
|
377
377
|
return monitor
|
package/src/create-asset.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { mapValues } from '@exodus/basic-utils'
|
|
1
2
|
import { createConsoleLogger } from '@exodus/asset-lib'
|
|
2
3
|
import { ASSET_FAMILY, connectAssetsList } from '@exodus/assets'
|
|
3
4
|
import bip44Constants from '@exodus/bip44-constants/by-ticker.js'
|
|
@@ -18,7 +19,6 @@ import {
|
|
|
18
19
|
validateFactory,
|
|
19
20
|
} from '@exodus/ethereum-lib'
|
|
20
21
|
import { toChecksumAddress } from '@exodus/ethereumjs/util'
|
|
21
|
-
import lodash from 'lodash'
|
|
22
22
|
import assert from 'minimalistic-assert'
|
|
23
23
|
|
|
24
24
|
import { addressHasHistoryFactory } from './address-has-history.js'
|
|
@@ -53,8 +53,6 @@ import { createTxFactory } from './tx-create.js'
|
|
|
53
53
|
import { txSendFactory } from './tx-send/index.js'
|
|
54
54
|
import { createWeb3API } from './web3/index.js'
|
|
55
55
|
|
|
56
|
-
const { mapValues } = lodash
|
|
57
|
-
|
|
58
56
|
export const createAssetFactory = ({
|
|
59
57
|
AccountState: CustomAccountState,
|
|
60
58
|
assetsList,
|
|
@@ -192,6 +190,10 @@ export const createAssetFactory = ({
|
|
|
192
190
|
getBalances,
|
|
193
191
|
})
|
|
194
192
|
|
|
193
|
+
const createRpcRequestAccumulator = createRpcRequestAccumulatorFactory({
|
|
194
|
+
multicall3Address,
|
|
195
|
+
})
|
|
196
|
+
|
|
195
197
|
const { createToken, getTokens } = createTokenFactory(
|
|
196
198
|
{
|
|
197
199
|
address,
|
|
@@ -204,6 +206,7 @@ export const createAssetFactory = ({
|
|
|
204
206
|
server,
|
|
205
207
|
stakingConfiguration,
|
|
206
208
|
stakingDependencies,
|
|
209
|
+
createRpcRequestAccumulator,
|
|
207
210
|
},
|
|
208
211
|
assets
|
|
209
212
|
)
|
|
@@ -232,10 +235,6 @@ export const createAssetFactory = ({
|
|
|
232
235
|
supportsTransactionPrivacy,
|
|
233
236
|
})
|
|
234
237
|
|
|
235
|
-
const createRpcRequestAccumulator = createRpcRequestAccumulatorFactory({
|
|
236
|
-
multicall3Address,
|
|
237
|
-
})
|
|
238
|
-
|
|
239
238
|
const features = {
|
|
240
239
|
accountState: true,
|
|
241
240
|
customTokens,
|
|
@@ -262,6 +261,8 @@ export const createAssetFactory = ({
|
|
|
262
261
|
const extraData =
|
|
263
262
|
stakingAssetNames.length > 0
|
|
264
263
|
? {
|
|
264
|
+
// Persist ownership independently of host annotations and balances.
|
|
265
|
+
stakingSnapshotOwners: {},
|
|
265
266
|
staking: mapValues(
|
|
266
267
|
stakingConfiguration,
|
|
267
268
|
(assetStakinConfiguration) => assetStakinConfiguration.accountStateExtraData
|
|
@@ -43,6 +43,7 @@ const getCreateBaseToken =
|
|
|
43
43
|
server,
|
|
44
44
|
stakingConfiguration = Object.create(null),
|
|
45
45
|
stakingDependencies = Object.create(null),
|
|
46
|
+
createRpcRequestAccumulator,
|
|
46
47
|
...props
|
|
47
48
|
}) =>
|
|
48
49
|
({ name, contract, features, ...tokenDef }) => {
|
|
@@ -78,6 +79,7 @@ const getCreateBaseToken =
|
|
|
78
79
|
server,
|
|
79
80
|
stakingConfiguration: stakingConfiguration[name],
|
|
80
81
|
stakingDependencies: stakingDependencies[name],
|
|
82
|
+
createRpcRequestAccumulator,
|
|
81
83
|
})
|
|
82
84
|
}
|
|
83
85
|
|
package/src/ens/index.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
// eslint-disable-next-line @exodus/import/no-deprecated
|
|
2
1
|
import { hashSync } from '@exodus/crypto/hash'
|
|
3
2
|
import { ABI } from '@exodus/ethereum-lib'
|
|
4
3
|
import SolidityContract from '@exodus/solidity-contract'
|
|
@@ -25,7 +24,6 @@ function namehash(inputName) {
|
|
|
25
24
|
const name = normalize(inputName)
|
|
26
25
|
if (name) {
|
|
27
26
|
for (const label of name.split('.').reverse()) {
|
|
28
|
-
// eslint-disable-next-line @exodus/import/no-deprecated
|
|
29
27
|
node = hashSync('keccak256', [node, hashSync('keccak256', label, 'uint8')], 'buffer')
|
|
30
28
|
}
|
|
31
29
|
}
|
|
@@ -29,10 +29,15 @@ export default class ApiCoinNodesServer extends EthLikeServer {
|
|
|
29
29
|
return fetchJsonRetry(this.uri, options)
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
async sendBatchRequest(batch) {
|
|
32
|
+
async sendBatchRequest(batch, { tolerant } = {}) {
|
|
33
33
|
if (isEmpty(batch)) return batch
|
|
34
34
|
const responses = await this.request(batch)
|
|
35
|
-
return this.parseBatchResponse({
|
|
35
|
+
return this.parseBatchResponse({
|
|
36
|
+
batch,
|
|
37
|
+
responses,
|
|
38
|
+
errorMessage: 'Bad rpc batch response',
|
|
39
|
+
tolerant,
|
|
40
|
+
})
|
|
36
41
|
}
|
|
37
42
|
|
|
38
43
|
async sendRequest(request) {
|
|
@@ -263,11 +263,11 @@ export function create(defaultURL, ensAssetName) {
|
|
|
263
263
|
},
|
|
264
264
|
|
|
265
265
|
async proxyToCoinNode(requestData) {
|
|
266
|
-
// eslint-disable-next-line @exodus/
|
|
266
|
+
// eslint-disable-next-line @exodus/hydra/no-param-reassign-prop-only
|
|
267
267
|
if (!requestData.jsonrpc) requestData.jsonrpc = '2.0'
|
|
268
|
-
// eslint-disable-next-line @exodus/
|
|
268
|
+
// eslint-disable-next-line @exodus/hydra/no-param-reassign-prop-only
|
|
269
269
|
if (!requestData.id) requestData.id = randomUUID()
|
|
270
|
-
// eslint-disable-next-line @exodus/
|
|
270
|
+
// eslint-disable-next-line @exodus/hydra/no-param-reassign-prop-only
|
|
271
271
|
if (!requestData.params) requestData.params = []
|
|
272
272
|
|
|
273
273
|
return request('proxy', requestData, { version: 'v2', method: 'post' })
|
|
@@ -198,7 +198,7 @@ export default class ClarityServer extends EthLikeServer {
|
|
|
198
198
|
clearTimeout(timeout)
|
|
199
199
|
const rawTraceparent = response?.traceparent || response?.[0]?.traceparent
|
|
200
200
|
if (rawTraceparent) {
|
|
201
|
-
// eslint-disable-next-line @exodus/
|
|
201
|
+
// eslint-disable-next-line @exodus/hydra/no-param-reassign-prop-only
|
|
202
202
|
response.traceparent = TraceId.fromRawTraceparent(rawTraceparent)
|
|
203
203
|
}
|
|
204
204
|
|
|
@@ -208,9 +208,9 @@ export default class ClarityServer extends EthLikeServer {
|
|
|
208
208
|
}
|
|
209
209
|
|
|
210
210
|
// Transport: WS only in ClarityServer, WS first → HTTP fallback in ClarityServerV2
|
|
211
|
-
async sendBatchRequest(batch) {
|
|
211
|
+
async sendBatchRequest(batch, { tolerant } = {}) {
|
|
212
212
|
const responses = await this.sendRpcRequest(batch)
|
|
213
|
-
return this.parseBatchResponse({ batch, responses, errorMessage: 'Bad Response' })
|
|
213
|
+
return this.parseBatchResponse({ batch, responses, errorMessage: 'Bad Response', tolerant })
|
|
214
214
|
}
|
|
215
215
|
|
|
216
216
|
// Transport: Uses sendRpcRequest - WS only in ClarityServer, WS first → HTTP fallback in ClarityServerV2
|
|
@@ -165,10 +165,11 @@ export class EthLikeServer extends EthLikeServerBase {
|
|
|
165
165
|
* @param {Array<{ id: number|string }>} args.batch - Requests sent, in the order results are expected.
|
|
166
166
|
* @param {unknown} args.responses - Raw transport payload; expected `[{ id, result }]` but treated as untrusted (gateways may return `{ message }` on error).
|
|
167
167
|
* @param {string} args.errorMessage - Thrown-error prefix per transport (e.g. `'Bad rpc batch response'`, `'Bad Response'`).
|
|
168
|
-
* @
|
|
169
|
-
* @
|
|
168
|
+
* @param {boolean} [args.tolerant] - When true, a request without a valid entry (JSON-RPC error entry, non-numeric `id`, duplicated or out-of-batch `id`, missing from a short response) maps to `undefined` instead of failing the whole batch.
|
|
169
|
+
* @returns {unknown[]} Each request's `result`, ordered to match `batch`. Entries with a duplicated or out-of-batch `id` are discarded in both modes, so an affected request maps to `undefined` rather than another request's result.
|
|
170
|
+
* @throws {Error} When `responses` isn't an array; in strict mode also when length mismatches `batch` or any entry lacks a numeric `id` / defined `result`.
|
|
170
171
|
*/
|
|
171
|
-
parseBatchResponse({ batch, responses, errorMessage }) {
|
|
172
|
+
parseBatchResponse({ batch, responses, errorMessage, tolerant = false }) {
|
|
172
173
|
// Gateways can respond with a JSON object (e.g. `{ message }`) instead of a batch array.
|
|
173
174
|
if (!Array.isArray(responses)) {
|
|
174
175
|
// Untrusted server text: keep only a short string preview, developer-facing (no `hint`).
|
|
@@ -179,16 +180,23 @@ export class EthLikeServer extends EthLikeServerBase {
|
|
|
179
180
|
throw new Error(`${errorMessage}${preview}`)
|
|
180
181
|
}
|
|
181
182
|
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
)
|
|
185
|
-
if (responses.length !== batch.length || !isValid) {
|
|
183
|
+
const isValidEntry = (response) => !isNaN(response?.id) && response?.result !== undefined
|
|
184
|
+
|
|
185
|
+
if (!tolerant && (responses.length !== batch.length || !responses.every(isValidEntry))) {
|
|
186
186
|
throw new Error(errorMessage)
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
-
// Safe against prototype keys:
|
|
189
|
+
// Safe against prototype keys: only entries with a validated numeric `id` are kept.
|
|
190
190
|
const keyed = Object.fromEntries(
|
|
191
|
-
responses
|
|
191
|
+
responses
|
|
192
|
+
.filter(isValidEntry)
|
|
193
|
+
.filter(({ id }, _i, all) => {
|
|
194
|
+
const key = `${id}`
|
|
195
|
+
const inBatch = batch.some((request) => `${request.id}` === key)
|
|
196
|
+
const unique = all.filter(({ id: other }) => `${other}` === key).length === 1
|
|
197
|
+
return inBatch && unique
|
|
198
|
+
})
|
|
199
|
+
.map((response) => [`${response.id}`, response.result])
|
|
192
200
|
)
|
|
193
201
|
return batch.map((request) => keyed[`${request.id}`])
|
|
194
202
|
}
|
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
|
-
// eslint-disable-next-line @exodus/import/no-deprecated
|
|
3
2
|
DEFAULT_SERVER_URLS,
|
|
4
|
-
// eslint-disable-next-line @exodus/import/no-deprecated
|
|
5
3
|
ETHEREUM_LIKE_ASSETS,
|
|
6
|
-
// eslint-disable-next-line @exodus/import/no-deprecated
|
|
7
4
|
ETHEREUM_LIKE_MONITOR_TYPES,
|
|
8
5
|
} from '@exodus/ethereum-lib'
|
|
9
6
|
import assert from 'minimalistic-assert'
|
|
@@ -80,11 +77,8 @@ export const createWsGateway = ({ uri } = Object.create(null)) => {
|
|
|
80
77
|
|
|
81
78
|
// @Deprecated
|
|
82
79
|
const serverMap = Object.fromEntries(
|
|
83
|
-
// eslint-disable-next-line @exodus/import/no-deprecated
|
|
84
80
|
ETHEREUM_LIKE_ASSETS.map((assetName) => {
|
|
85
|
-
// eslint-disable-next-line @exodus/import/no-deprecated
|
|
86
81
|
const monitorType = ETHEREUM_LIKE_MONITOR_TYPES[assetName]
|
|
87
|
-
// eslint-disable-next-line @exodus/import/no-deprecated
|
|
88
82
|
const serverUrl = DEFAULT_SERVER_URLS[assetName]
|
|
89
83
|
return [assetName, createEvmServer({ assetName, serverUrl, monitorType })]
|
|
90
84
|
})
|
|
@@ -3,7 +3,6 @@ import {
|
|
|
3
3
|
mapCurrency as defaultMapCurrency,
|
|
4
4
|
modelToJSON,
|
|
5
5
|
} from '@exodus/asset-lib/src/utils/index.js'
|
|
6
|
-
// eslint-disable-next-line @exodus/import/no-deprecated
|
|
7
6
|
import { isNumberUnit } from '@exodus/currency'
|
|
8
7
|
import lodash from 'lodash'
|
|
9
8
|
|
|
@@ -13,7 +12,6 @@ const PRE_EIP1559_UNIT_KEYS_GUARD = ['gasPrice']
|
|
|
13
12
|
const EIP1559_UNIT_KEYS_GUARD = ['gasPrice', 'baseFeePerGas']
|
|
14
13
|
|
|
15
14
|
export const bound = ({ val, multiplier, max, min }) => {
|
|
16
|
-
// eslint-disable-next-line @exodus/import/no-deprecated
|
|
17
15
|
if (!isNumberUnit(val)) return val
|
|
18
16
|
|
|
19
17
|
if (multiplier) {
|
package/src/hooks/monitor.js
CHANGED
|
@@ -1,67 +1,195 @@
|
|
|
1
1
|
import assert from 'minimalistic-assert'
|
|
2
2
|
|
|
3
3
|
import processTxLog from '../tx-log-staking-processor/index.js'
|
|
4
|
+
import { getStakingActivity, stakingStateInterval } from './staking-freshness.js'
|
|
5
|
+
|
|
6
|
+
// Service-owned fields must never leak between delegator snapshots. Keep host
|
|
7
|
+
// annotations outside this set; new service fields must be added here as well.
|
|
8
|
+
const STAKING_SNAPSHOT_FIELDS = new Set([
|
|
9
|
+
'rewardsBalance',
|
|
10
|
+
'isDelegating',
|
|
11
|
+
'activeStakedBalance',
|
|
12
|
+
'delegatedBalance',
|
|
13
|
+
'pendingBalance',
|
|
14
|
+
'pendingDepositedBalance',
|
|
15
|
+
'minDelegateAmount',
|
|
16
|
+
'unclaimedUndelegatedBalance',
|
|
17
|
+
'canClaimUndelegatedBalance',
|
|
18
|
+
'isUndelegateInProgress',
|
|
19
|
+
'validatorActivationTime',
|
|
20
|
+
'validatorExitTime',
|
|
21
|
+
'validatorWithdrawTime',
|
|
22
|
+
'liquidRewards',
|
|
23
|
+
'withdrawable',
|
|
24
|
+
'unbondNonce',
|
|
25
|
+
'minRewardsToWithdraw',
|
|
26
|
+
])
|
|
4
27
|
|
|
5
28
|
export const createEthereumHooks = ({
|
|
6
29
|
assetClientInterface,
|
|
7
30
|
assetName,
|
|
8
31
|
stakingAssetNames,
|
|
9
32
|
server,
|
|
33
|
+
now = () => performance.now(),
|
|
10
34
|
}) => {
|
|
11
35
|
assert(assetClientInterface, 'assetClientInterface is required')
|
|
12
36
|
assert(server, 'server is required')
|
|
13
37
|
assert(assetName, 'assetName is required')
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
//
|
|
21
|
-
const
|
|
22
|
-
|
|
38
|
+
const freshness = new Map()
|
|
39
|
+
const addresses = new Map()
|
|
40
|
+
const generations = new Map()
|
|
41
|
+
const commits = new Map()
|
|
42
|
+
const afterTickHook = async ({ walletAccount, refresh = false }) => {
|
|
43
|
+
// Order requests at entry, not after address discovery: discovery itself can
|
|
44
|
+
// be delayed while a newer explicit refresh is already running.
|
|
45
|
+
const accountKey = String(walletAccount)
|
|
46
|
+
const generation = {}
|
|
47
|
+
generations.set(accountKey, generation)
|
|
48
|
+
const assets = await assetClientInterface.getAssetsForNetwork({ baseAssetName: assetName })
|
|
49
|
+
const stakingAssets = stakingAssetNames.map((name) => assets[name]).filter(Boolean)
|
|
23
50
|
const userAddress = await assetClientInterface.getReceiveAddress({
|
|
24
51
|
assetName,
|
|
25
52
|
walletAccount,
|
|
26
53
|
useCache: true,
|
|
27
54
|
})
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
55
|
+
const address = userAddress.toString().toLowerCase()
|
|
56
|
+
if (generations.get(accountKey) !== generation) return
|
|
57
|
+
const addressChanged = addresses.get(accountKey) !== address
|
|
58
|
+
addresses.set(accountKey, address)
|
|
59
|
+
const baseTxLog = await assetClientInterface.getTxLog({ assetName, walletAccount })
|
|
60
|
+
const patches = Object.create(null)
|
|
61
|
+
const completed = []
|
|
32
62
|
|
|
33
63
|
for (const asset of stakingAssets) {
|
|
34
64
|
const stakingApi = asset.api?.staking
|
|
35
65
|
if (!stakingApi?.getStakingInfo) continue
|
|
66
|
+
const key = JSON.stringify([String(walletAccount), assetName, asset.name, address])
|
|
67
|
+
const previous = freshness.get(key)
|
|
68
|
+
try {
|
|
69
|
+
const txLog =
|
|
70
|
+
asset.name === assetName
|
|
71
|
+
? baseTxLog
|
|
72
|
+
: await assetClientInterface.getTxLog({ assetName: asset.name, walletAccount })
|
|
73
|
+
const { pending, fingerprint } = getStakingActivity({
|
|
74
|
+
assetName: asset.name,
|
|
75
|
+
txLogs: [baseTxLog, txLog],
|
|
76
|
+
})
|
|
77
|
+
const force =
|
|
78
|
+
refresh || addressChanged || pending || !previous || previous.fingerprint !== fingerprint
|
|
79
|
+
if (!force && now() < previous.deadline) continue
|
|
36
80
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
81
|
+
patches[asset.name] = await stakingApi.getStakingInfo({
|
|
82
|
+
address,
|
|
83
|
+
background: true,
|
|
84
|
+
useCache: !force,
|
|
85
|
+
// Initial per-address discovery is full, but the queue is global.
|
|
86
|
+
useQueueCache:
|
|
87
|
+
!refresh && !pending && (!previous || previous.fingerprint === fingerprint),
|
|
88
|
+
})
|
|
89
|
+
completed.push([
|
|
90
|
+
key,
|
|
91
|
+
{ fingerprint, deadline: now() + stakingStateInterval(patches[asset.name]) },
|
|
92
|
+
])
|
|
93
|
+
} catch (error) {
|
|
94
|
+
// Keep the last successful account state and retry on the next normal
|
|
95
|
+
// tick. Never interpret transport/contract/provider failures as zero.
|
|
96
|
+
if (generations.get(accountKey) === generation) freshness.delete(key)
|
|
97
|
+
console.warn(`Failed to refresh ${asset.name} staking state`, error)
|
|
98
|
+
}
|
|
41
99
|
}
|
|
42
100
|
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
batch
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
101
|
+
const publish = async () => {
|
|
102
|
+
if (generations.get(accountKey) !== generation) return
|
|
103
|
+
const batch = assetClientInterface.createOperationsBatch()
|
|
104
|
+
// All asynchronous preparation is inside the per-account commit queue.
|
|
105
|
+
// Newer reads can run concurrently, but cannot commit ahead of an already
|
|
106
|
+
// executing batch. A superseded preparation is discarded before execute.
|
|
107
|
+
const current = await assetClientInterface.getAccountState({ assetName, walletAccount })
|
|
108
|
+
await Promise.all(
|
|
109
|
+
stakingAssets.map((asset) =>
|
|
110
|
+
processTxLog({
|
|
111
|
+
asset,
|
|
112
|
+
assetClientInterface,
|
|
113
|
+
walletAccount,
|
|
114
|
+
batch,
|
|
115
|
+
})
|
|
116
|
+
)
|
|
117
|
+
)
|
|
118
|
+
const latestAddress = await assetClientInterface.getReceiveAddress({
|
|
119
|
+
assetName,
|
|
55
120
|
walletAccount,
|
|
56
|
-
|
|
121
|
+
useCache: true,
|
|
57
122
|
})
|
|
58
|
-
|
|
123
|
+
if (
|
|
124
|
+
latestAddress.toString().toLowerCase() !== address ||
|
|
125
|
+
generations.get(accountKey) !== generation
|
|
126
|
+
) {
|
|
127
|
+
return
|
|
128
|
+
}
|
|
59
129
|
|
|
60
|
-
|
|
61
|
-
|
|
130
|
+
const staking = { ...current.staking }
|
|
131
|
+
const owners = { ...current.stakingSnapshotOwners }
|
|
132
|
+
let changed = false
|
|
133
|
+
// Ownership is persisted with the snapshot, not inferred from in-memory
|
|
134
|
+
// deadlines. Unknown legacy owners cannot supply failed fields on restore.
|
|
135
|
+
// Preserve only non-service host metadata when changing snapshot owners.
|
|
136
|
+
for (const name of new Set([...stakingAssetNames, ...Object.keys(owners)])) {
|
|
137
|
+
if (owners[name]?.address !== address || owners[name]?.network !== assetName) {
|
|
138
|
+
staking[name] = Object.fromEntries(
|
|
139
|
+
Object.entries(staking[name] || {}).filter(([key]) => !STAKING_SNAPSHOT_FIELDS.has(key))
|
|
140
|
+
)
|
|
141
|
+
delete owners[name]
|
|
142
|
+
changed = true
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
for (const [name, patch] of Object.entries(patches)) {
|
|
146
|
+
staking[name] = { ...staking[name], ...patch }
|
|
147
|
+
owners[name] = { address, network: assetName }
|
|
148
|
+
changed = true
|
|
149
|
+
}
|
|
150
|
+
if (changed) {
|
|
151
|
+
assetClientInterface.updateAccountStateBatch({
|
|
152
|
+
assetName,
|
|
153
|
+
walletAccount,
|
|
154
|
+
newData: { staking, stakingSnapshotOwners: owners },
|
|
155
|
+
batch,
|
|
156
|
+
})
|
|
157
|
+
}
|
|
158
|
+
// No await between the final generation/address check and batch execution.
|
|
159
|
+
await assetClientInterface.executeOperationsBatch(batch)
|
|
160
|
+
// An in-flight storage commit cannot be cancelled through ACI. It is
|
|
161
|
+
// serialized before newer commits; never let it publish a stale deadline.
|
|
162
|
+
const committedAddress = await assetClientInterface.getReceiveAddress({
|
|
163
|
+
assetName,
|
|
164
|
+
walletAccount,
|
|
165
|
+
useCache: true,
|
|
166
|
+
})
|
|
167
|
+
if (
|
|
168
|
+
generations.get(accountKey) === generation &&
|
|
169
|
+
committedAddress.toString().toLowerCase() === address
|
|
170
|
+
) {
|
|
171
|
+
for (const [key, entry] of completed) freshness.set(key, entry)
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
const previousCommit = commits.get(accountKey) || Promise.resolve()
|
|
175
|
+
const commit = previousCommit.catch(() => {}).then(publish)
|
|
176
|
+
commits.set(accountKey, commit)
|
|
177
|
+
try {
|
|
178
|
+
await commit
|
|
179
|
+
} finally {
|
|
180
|
+
if (commits.get(accountKey) === commit) commits.delete(accountKey)
|
|
181
|
+
}
|
|
62
182
|
}
|
|
63
183
|
|
|
64
184
|
return {
|
|
185
|
+
// A restart/resume exposed through monitor.start must rediscover restored
|
|
186
|
+
// accounts. No host screen lifecycle is assumed; update({refresh:true}) and
|
|
187
|
+
// direct getStakingInfo({address}) are explicit uncached refresh paths.
|
|
188
|
+
'before-start': () => {
|
|
189
|
+
freshness.clear()
|
|
190
|
+
addresses.clear()
|
|
191
|
+
generations.clear()
|
|
192
|
+
},
|
|
65
193
|
'after-tick': afterTickHook,
|
|
66
194
|
}
|
|
67
195
|
}
|