@exodus/ethereum-api 8.64.0 → 8.64.2
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 +34 -8
- package/src/create-asset.js +4 -1
- package/src/exodus-eth-server/clarity-v2.js +5 -4
- package/src/exodus-eth-server/index.js +34 -0
- package/src/exodus-eth-server/ws-gateway.js +9 -8
- package/src/tx-log/clarity-monitor-v2.js +25 -12
- package/src/tx-log/clarity-monitor.js +20 -2
- package/src/tx-log/ethereum-monitor.js +18 -7
- package/src/tx-log/ethereum-no-history-monitor.js +1 -1
- package/src/tx-log/monitor-utils/index.js +1 -0
- package/src/tx-log/monitor-utils/verify-pending-tx-status-rpc.js +69 -0
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.64.2](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.64.1...@exodus/ethereum-api@8.64.2) (2026-01-23)
|
|
7
|
+
|
|
8
|
+
**Note:** Version bump only for package @exodus/ethereum-api
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
## [8.64.1](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.64.0...@exodus/ethereum-api@8.64.1) (2026-01-21)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Bug Fixes
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
* fix: monitors should check rpc before dropping (#7277)
|
|
21
|
+
|
|
22
|
+
* fix: prevent transactions from being dropped from no history monitor whilst they are still served at the rpc (#7299)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
6
26
|
## [8.64.0](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.63.0...@exodus/ethereum-api@8.64.0) (2026-01-14)
|
|
7
27
|
|
|
8
28
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/ethereum-api",
|
|
3
|
-
"version": "8.64.
|
|
3
|
+
"version": "8.64.2",
|
|
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": "d6aa274a62e489f5dd27e11088c56b3a64903779"
|
|
71
71
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import assert from 'minimalistic-assert'
|
|
2
2
|
import ms from 'ms'
|
|
3
3
|
|
|
4
|
-
import { createEvmServer, ValidMonitorTypes } from './exodus-eth-server/index.js'
|
|
4
|
+
import { createEvmServer, createWsGateway, ValidMonitorTypes } from './exodus-eth-server/index.js'
|
|
5
5
|
import { createEthereumHooks } from './hooks/index.js'
|
|
6
6
|
import { ClarityMonitor } from './tx-log/clarity-monitor.js'
|
|
7
7
|
import { ClarityMonitorV2 } from './tx-log/clarity-monitor-v2.js'
|
|
@@ -18,6 +18,7 @@ export const resolveMonitorSettings = (
|
|
|
18
18
|
defaultMonitorInterval,
|
|
19
19
|
defaultMonitorType,
|
|
20
20
|
defaultServerUrl,
|
|
21
|
+
defaultWsGatewayUri,
|
|
21
22
|
} = Object.create(null)
|
|
22
23
|
) => {
|
|
23
24
|
assert(asset, 'expected asset')
|
|
@@ -27,9 +28,17 @@ export const resolveMonitorSettings = (
|
|
|
27
28
|
)
|
|
28
29
|
assert(defaultServerUrl, `expected default serverUrl for ${asset.name}`)
|
|
29
30
|
|
|
31
|
+
// NOTE: When trying to enable `clarity-v3` as an asset
|
|
32
|
+
// default config, there must exist a default
|
|
33
|
+
// gateway definition.
|
|
34
|
+
if (defaultMonitorType === 'clarity-v3') {
|
|
35
|
+
assert(defaultWsGatewayUri, 'expected defaultWsGatewayUri')
|
|
36
|
+
}
|
|
37
|
+
|
|
30
38
|
const overrideMonitorType = configWithOverrides?.monitorType
|
|
31
39
|
const overrideServerUrl = configWithOverrides?.serverUrl
|
|
32
40
|
const overrideMonitorInterval = configWithOverrides?.monitorInterval
|
|
41
|
+
const overrideWsGatewayUri = configWithOverrides?.wsGatewayUri
|
|
33
42
|
|
|
34
43
|
const defaultResolution = {
|
|
35
44
|
// NOTE: Regardless of the `monitorType`, the `monitorInterval`
|
|
@@ -37,13 +46,18 @@ export const resolveMonitorSettings = (
|
|
|
37
46
|
monitorInterval: overrideMonitorInterval ?? defaultMonitorInterval,
|
|
38
47
|
monitorType: defaultMonitorType,
|
|
39
48
|
serverUrl: defaultServerUrl,
|
|
49
|
+
wsGatewayUri: defaultWsGatewayUri,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const fallbackWithWarning = (msg) => {
|
|
53
|
+
console.log([msg, `Falling back to ${defaultMonitorType}<${defaultServerUrl}>.`].join(' '))
|
|
54
|
+
return defaultResolution
|
|
40
55
|
}
|
|
41
56
|
|
|
42
57
|
if (!overrideMonitorType && overrideServerUrl) {
|
|
43
|
-
|
|
44
|
-
`Received an \`overrideServerUrl\`, but not the \`monitorType\` for ${asset.name}
|
|
58
|
+
return fallbackWithWarning(
|
|
59
|
+
`Received an \`overrideServerUrl\`, but not the \`monitorType\` for ${asset.name}.`
|
|
45
60
|
)
|
|
46
|
-
return defaultResolution
|
|
47
61
|
}
|
|
48
62
|
|
|
49
63
|
// If we don't attempt to override the `monitorType`, we resort
|
|
@@ -51,14 +65,24 @@ export const resolveMonitorSettings = (
|
|
|
51
65
|
if (!overrideMonitorType) return defaultResolution
|
|
52
66
|
|
|
53
67
|
if (!ValidMonitorTypes.includes(overrideMonitorType)) {
|
|
54
|
-
|
|
55
|
-
|
|
68
|
+
return fallbackWithWarning(`"${overrideMonitorType}" is not a valid \`MonitorType\`.`)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const wsGatewayUri = overrideWsGatewayUri || defaultWsGatewayUri
|
|
72
|
+
|
|
73
|
+
if (overrideMonitorType === 'clarity-v3' && !wsGatewayUri) {
|
|
74
|
+
return fallbackWithWarning(
|
|
75
|
+
"Attempted to use 'clarity-v3' without a defining a supporting wsGatewayUri."
|
|
56
76
|
)
|
|
57
|
-
return defaultResolution
|
|
58
77
|
}
|
|
59
78
|
|
|
60
79
|
// Permit the `monitorType` and `serverUrl` to be overrided.
|
|
61
|
-
return {
|
|
80
|
+
return {
|
|
81
|
+
...defaultResolution,
|
|
82
|
+
monitorType: overrideMonitorType,
|
|
83
|
+
serverUrl: overrideServerUrl,
|
|
84
|
+
wsGatewayUri,
|
|
85
|
+
}
|
|
62
86
|
}
|
|
63
87
|
|
|
64
88
|
export const stringifyPrivateTx = (tx) => {
|
|
@@ -116,6 +140,7 @@ export const createHistoryMonitorFactory = ({
|
|
|
116
140
|
server,
|
|
117
141
|
stakingAssetNames,
|
|
118
142
|
rpcBalanceAssetNames,
|
|
143
|
+
wsGatewayUri,
|
|
119
144
|
}) => {
|
|
120
145
|
assert(assetName, 'expected assetName')
|
|
121
146
|
assert(assetClientInterface, 'expected assetClientInterface')
|
|
@@ -142,6 +167,7 @@ export const createHistoryMonitorFactory = ({
|
|
|
142
167
|
interval: ms(monitorInterval || '5m'),
|
|
143
168
|
server,
|
|
144
169
|
rpcBalanceAssetNames,
|
|
170
|
+
wsGatewayClient: createWsGateway({ uri: wsGatewayUri }),
|
|
145
171
|
...args,
|
|
146
172
|
})
|
|
147
173
|
break
|
package/src/create-asset.js
CHANGED
|
@@ -71,6 +71,7 @@ export const createAssetFactory = ({
|
|
|
71
71
|
useAbsoluteBalanceAndNonce = false,
|
|
72
72
|
delisted = false,
|
|
73
73
|
privacyRpcUrl: defaultPrivacyRpcUrl,
|
|
74
|
+
wsGatewayUri: defaultWsGatewayUri,
|
|
74
75
|
}) => {
|
|
75
76
|
assert(assetsList, 'assetsList is required')
|
|
76
77
|
assert(providedFeeData || feeDataConfig, 'feeData or feeDataConfig is required')
|
|
@@ -118,12 +119,13 @@ export const createAssetFactory = ({
|
|
|
118
119
|
|
|
119
120
|
const asset = assets[base.name]
|
|
120
121
|
|
|
121
|
-
const { monitorType, serverUrl, monitorInterval } = resolveMonitorSettings({
|
|
122
|
+
const { monitorType, serverUrl, monitorInterval, wsGatewayUri } = resolveMonitorSettings({
|
|
122
123
|
asset,
|
|
123
124
|
configWithOverrides,
|
|
124
125
|
defaultMonitorInterval,
|
|
125
126
|
defaultMonitorType,
|
|
126
127
|
defaultServerUrl,
|
|
128
|
+
defaultWsGatewayUri,
|
|
127
129
|
})
|
|
128
130
|
|
|
129
131
|
if (overrideUseAbsoluteBalanceAndNonce !== undefined) {
|
|
@@ -220,6 +222,7 @@ export const createAssetFactory = ({
|
|
|
220
222
|
server,
|
|
221
223
|
stakingAssetNames,
|
|
222
224
|
rpcBalanceAssetNames,
|
|
225
|
+
wsGatewayUri,
|
|
223
226
|
})
|
|
224
227
|
|
|
225
228
|
const defaultAddressPath = 'm/0/0'
|
|
@@ -175,11 +175,12 @@ export default class ClarityServerV2 extends ClarityServer {
|
|
|
175
175
|
const newCursor = Buffer.alloc(8)
|
|
176
176
|
newCursor.writeBigUInt64LE(BigInt(blockNumber), 0)
|
|
177
177
|
|
|
178
|
+
// Separate confirmed (has blockNumber) from pending (blockNumber is null)
|
|
179
|
+
const confirmed = transactions.filter((tx) => tx.blockNumber != null)
|
|
180
|
+
const pending = transactions.filter((tx) => tx.blockNumber == null)
|
|
181
|
+
|
|
178
182
|
return {
|
|
179
|
-
transactions: {
|
|
180
|
-
confirmed: transactions,
|
|
181
|
-
pending: [],
|
|
182
|
-
},
|
|
183
|
+
transactions: { confirmed, pending },
|
|
183
184
|
cursor: newCursor,
|
|
184
185
|
}
|
|
185
186
|
}
|
|
@@ -12,6 +12,7 @@ import { create } from './api.js'
|
|
|
12
12
|
import ApiCoinNodesServer from './api-coin-nodes.js'
|
|
13
13
|
import ClarityServer from './clarity.js'
|
|
14
14
|
import ClarityServerV2 from './clarity-v2.js'
|
|
15
|
+
import WsGateway from './ws-gateway.js'
|
|
15
16
|
|
|
16
17
|
export const ValidMonitorTypes = ['no-history', 'clarity', 'clarity-v2', 'clarity-v3', 'magnifier']
|
|
17
18
|
|
|
@@ -34,6 +35,39 @@ export function createEvmServer({ assetName, serverUrl, monitorType }) {
|
|
|
34
35
|
}
|
|
35
36
|
}
|
|
36
37
|
|
|
38
|
+
const gatewayUriToWsGateway = Object.create(null)
|
|
39
|
+
|
|
40
|
+
export const createWsGateway = ({ uri } = Object.create(null)) => {
|
|
41
|
+
assert(typeof uri === 'string', 'expected string uri')
|
|
42
|
+
|
|
43
|
+
uri = new URL(uri).toString()
|
|
44
|
+
|
|
45
|
+
const wsGateway = gatewayUriToWsGateway[uri]
|
|
46
|
+
if (wsGateway) return wsGateway
|
|
47
|
+
|
|
48
|
+
// NOTE: We disable the ability to `setServer` on the
|
|
49
|
+
// `WsGateway`, since we assume a mapping between
|
|
50
|
+
// `gatewayUri -> WsGateway`.
|
|
51
|
+
//
|
|
52
|
+
// This is a safe modification since a `WsGateway`
|
|
53
|
+
// monitor isn't rigged up to the same life cycle
|
|
54
|
+
// as other servers.
|
|
55
|
+
class FrozenWsGateway extends WsGateway {
|
|
56
|
+
constructor() {
|
|
57
|
+
super({ uri })
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
setServer() {
|
|
61
|
+
assert(false, 'setServer is disabled')
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const frozenWsGateway = new FrozenWsGateway()
|
|
66
|
+
gatewayUriToWsGateway[uri] = frozenWsGateway
|
|
67
|
+
|
|
68
|
+
return frozenWsGateway
|
|
69
|
+
}
|
|
70
|
+
|
|
37
71
|
// @Deprecated
|
|
38
72
|
const serverMap = Object.fromEntries(
|
|
39
73
|
// eslint-disable-next-line @exodus/import/no-deprecated
|
|
@@ -26,15 +26,21 @@ const parseSubscriptionKey = (subscriptionKey) => {
|
|
|
26
26
|
* @property {string} address
|
|
27
27
|
*/
|
|
28
28
|
|
|
29
|
-
class WsGateway extends EventEmitter {
|
|
29
|
+
export default class WsGateway extends EventEmitter {
|
|
30
30
|
#socket = null
|
|
31
31
|
// Dedup subscriptions to reduce workload on server and resubscribe after closing
|
|
32
32
|
#subscriptions = new Set()
|
|
33
33
|
#reconnectionTimeoutId = null
|
|
34
|
-
#defaultUri = 'wss://ws-gateway-clarity.a.exodus.io'
|
|
35
34
|
#uri = null
|
|
36
35
|
#logger = createConsoleLogger(`@exodus/ws-gateway`)
|
|
37
36
|
|
|
37
|
+
constructor({ uri }) {
|
|
38
|
+
super()
|
|
39
|
+
assert(typeof uri === 'string', 'expected string uri')
|
|
40
|
+
|
|
41
|
+
this.#uri = uri
|
|
42
|
+
}
|
|
43
|
+
|
|
38
44
|
_handlers = {
|
|
39
45
|
message: ({ target, data }) => {
|
|
40
46
|
if (target !== this.#socket) return
|
|
@@ -77,7 +83,7 @@ class WsGateway extends EventEmitter {
|
|
|
77
83
|
}
|
|
78
84
|
|
|
79
85
|
setServer(uri) {
|
|
80
|
-
this.#uri = uri || this.#uri
|
|
86
|
+
this.#uri = uri || this.#uri
|
|
81
87
|
}
|
|
82
88
|
|
|
83
89
|
start() {
|
|
@@ -266,8 +272,3 @@ class WsGateway extends EventEmitter {
|
|
|
266
272
|
this.#socket = null
|
|
267
273
|
}
|
|
268
274
|
}
|
|
269
|
-
|
|
270
|
-
const wsGateway = new WsGateway()
|
|
271
|
-
const createWsGateway = () => wsGateway
|
|
272
|
-
|
|
273
|
-
export { createWsGateway, WsGateway }
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { BaseMonitor } from '@exodus/asset-lib'
|
|
2
2
|
import { getAssetAddresses } from '@exodus/ethereum-lib'
|
|
3
3
|
import lodash from 'lodash'
|
|
4
|
+
import assert from 'minimalistic-assert'
|
|
4
5
|
|
|
5
|
-
import
|
|
6
|
+
import WsGateway from '../exodus-eth-server/ws-gateway.js'
|
|
6
7
|
import { executeEthLikeFeeMonitorUpdate } from '../fee-utils.js'
|
|
7
8
|
import { fromHexToString } from '../number-utils.js'
|
|
8
9
|
import { filterEffects, getLogItemsFromServerTx } from './clarity-utils/index.js'
|
|
@@ -13,6 +14,7 @@ import {
|
|
|
13
14
|
getCurrentEIP7702Delegation,
|
|
14
15
|
getDeriveDataNeededForTick,
|
|
15
16
|
getDeriveTransactionsToCheck,
|
|
17
|
+
verifyRpcPendingTxStatusBatch,
|
|
16
18
|
} from './monitor-utils/index.js'
|
|
17
19
|
|
|
18
20
|
const { isEmpty } = lodash
|
|
@@ -22,14 +24,10 @@ export class ClarityMonitorV2 extends BaseMonitor {
|
|
|
22
24
|
#walletAccountByAddress = new Map()
|
|
23
25
|
#walletAccountInfo = new Map()
|
|
24
26
|
#rpcBalanceAssetNames = []
|
|
25
|
-
constructor({
|
|
26
|
-
server,
|
|
27
|
-
wsGatewayClient = createWsGateway(),
|
|
28
|
-
rpcBalanceAssetNames,
|
|
29
|
-
config,
|
|
30
|
-
...args
|
|
31
|
-
} = {}) {
|
|
27
|
+
constructor({ server, wsGatewayClient, rpcBalanceAssetNames, config, ...args } = {}) {
|
|
32
28
|
super(args)
|
|
29
|
+
assert(wsGatewayClient instanceof WsGateway, 'expected WsGateway wsGatewayClient')
|
|
30
|
+
|
|
33
31
|
this.config = { GAS_PRICE_FROM_WEBSOCKET: true, ...config }
|
|
34
32
|
this.server = server
|
|
35
33
|
this.#wsClient = wsGatewayClient
|
|
@@ -47,8 +45,6 @@ export class ClarityMonitorV2 extends BaseMonitor {
|
|
|
47
45
|
setServer(config) {
|
|
48
46
|
const uri = config?.server || this.server.defaultUri
|
|
49
47
|
|
|
50
|
-
this.#wsClient.setServer(config.wsGatewayUrl?.v1)
|
|
51
|
-
|
|
52
48
|
this.#wsClient.on('connected', () => this.subscribeAllWalletAccounts())
|
|
53
49
|
this.#wsClient.start()
|
|
54
50
|
|
|
@@ -127,9 +123,26 @@ export class ClarityMonitorV2 extends BaseMonitor {
|
|
|
127
123
|
}
|
|
128
124
|
}
|
|
129
125
|
|
|
126
|
+
// Batch verify all pending txs with a single RPC call (skip if refresh)
|
|
127
|
+
const txIds = Object.keys(pendingTransactionsToCheck)
|
|
128
|
+
const statuses = params.refresh
|
|
129
|
+
? {}
|
|
130
|
+
: await verifyRpcPendingTxStatusBatch({
|
|
131
|
+
baseAsset: this.asset,
|
|
132
|
+
logger: this.logger,
|
|
133
|
+
txIds,
|
|
134
|
+
})
|
|
135
|
+
|
|
130
136
|
for (const { tx, assetName } of Object.values(pendingTransactionsToCheck)) {
|
|
131
|
-
if (params.refresh)
|
|
132
|
-
|
|
137
|
+
if (params.refresh) {
|
|
138
|
+
await updateTx(tx, assetName, { remove: true })
|
|
139
|
+
} else {
|
|
140
|
+
const txStatus = statuses[tx.txId]
|
|
141
|
+
if (txStatus?.status === 'dropped') {
|
|
142
|
+
await updateTx(tx, assetName, { error: 'Dropped' })
|
|
143
|
+
}
|
|
144
|
+
// status === 'confirmed' or 'pending' - tx is fine, wait for Clarity to confirm
|
|
145
|
+
}
|
|
133
146
|
}
|
|
134
147
|
|
|
135
148
|
return { txsToRemove }
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
getCurrentEIP7702Delegation,
|
|
14
14
|
getDeriveDataNeededForTick,
|
|
15
15
|
getDeriveTransactionsToCheck,
|
|
16
|
+
verifyRpcPendingTxStatusBatch,
|
|
16
17
|
} from './monitor-utils/index.js'
|
|
17
18
|
|
|
18
19
|
const { isEmpty } = lodash
|
|
@@ -111,9 +112,26 @@ export class ClarityMonitor extends BaseMonitor {
|
|
|
111
112
|
}
|
|
112
113
|
}
|
|
113
114
|
|
|
115
|
+
// Batch verify all pending txs with a single RPC call (skip if refresh)
|
|
116
|
+
const txIds = Object.keys(pendingTransactionsToCheck)
|
|
117
|
+
const statuses = params.refresh
|
|
118
|
+
? {}
|
|
119
|
+
: await verifyRpcPendingTxStatusBatch({
|
|
120
|
+
baseAsset: this.asset,
|
|
121
|
+
logger: this.logger,
|
|
122
|
+
txIds,
|
|
123
|
+
})
|
|
124
|
+
|
|
114
125
|
for (const { tx, assetName } of Object.values(pendingTransactionsToCheck)) {
|
|
115
|
-
if (params.refresh)
|
|
116
|
-
|
|
126
|
+
if (params.refresh) {
|
|
127
|
+
await updateTx(tx, assetName, { remove: true })
|
|
128
|
+
} else {
|
|
129
|
+
const txStatus = statuses[tx.txId]
|
|
130
|
+
if (txStatus?.status === 'dropped') {
|
|
131
|
+
await updateTx(tx, assetName, { error: 'Dropped' })
|
|
132
|
+
}
|
|
133
|
+
// status === 'confirmed' or 'pending' - tx is fine, wait for Clarity to confirm
|
|
134
|
+
}
|
|
117
135
|
}
|
|
118
136
|
|
|
119
137
|
return { txsToRemove }
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
getDeriveTransactionsToCheck,
|
|
13
13
|
getHistoryFromServer,
|
|
14
14
|
getLogItemsFromServerTx,
|
|
15
|
+
verifyRpcPendingTxStatusBatch,
|
|
15
16
|
} from './monitor-utils/index.js'
|
|
16
17
|
import {
|
|
17
18
|
enableWSUpdates,
|
|
@@ -118,17 +119,27 @@ export class EthereumMonitor extends BaseMonitor {
|
|
|
118
119
|
}
|
|
119
120
|
}
|
|
120
121
|
|
|
122
|
+
// Batch verify all pending txs with a single RPC call (skip if refresh)
|
|
123
|
+
const txIds = Object.keys(pendingTransactionsToCheck)
|
|
124
|
+
const statuses = params.refresh
|
|
125
|
+
? {}
|
|
126
|
+
: await verifyRpcPendingTxStatusBatch({
|
|
127
|
+
baseAsset: this.asset,
|
|
128
|
+
logger: this.logger,
|
|
129
|
+
txIds,
|
|
130
|
+
})
|
|
131
|
+
|
|
121
132
|
for (const { tx, assetName } of Object.values(pendingTransactionsToCheck)) {
|
|
122
133
|
if (params.refresh) {
|
|
123
134
|
await updateTx(tx, assetName, { remove: true })
|
|
124
135
|
} else {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
136
|
+
const txStatus = statuses[tx.txId]
|
|
137
|
+
if (txStatus?.status === 'confirmed') {
|
|
138
|
+
await updateTx(tx, assetName, { isTxConfirmed: true })
|
|
139
|
+
} else if (txStatus?.status === 'dropped') {
|
|
140
|
+
await updateTx(tx, assetName, { isTxConfirmed: false })
|
|
141
|
+
}
|
|
142
|
+
// status === 'pending' or missing - tx still in mempool, do nothing
|
|
132
143
|
}
|
|
133
144
|
}
|
|
134
145
|
|
|
@@ -135,7 +135,7 @@ export class EthereumNoHistoryMonitor extends BaseMonitor {
|
|
|
135
135
|
for (const { tx, assetName } of pendingTransactions) {
|
|
136
136
|
const txFromNode = pendingTxsFromNode[tx.txId]
|
|
137
137
|
const isConfirmed = Boolean(txFromNode?.blockHash)
|
|
138
|
-
if (now - tx.date.getTime() > UNCONFIRMED_TX_LIMIT && !
|
|
138
|
+
if (now - tx.date.getTime() > UNCONFIRMED_TX_LIMIT && !txFromNode) {
|
|
139
139
|
txsToRemove.push({
|
|
140
140
|
tx,
|
|
141
141
|
assetSource: { asset: assetName, walletAccount },
|
|
@@ -6,4 +6,5 @@ export { default as getHistoryFromServer } from './get-history-from-server.js'
|
|
|
6
6
|
export { default as checkPendingTransactions } from './check-pending-transactions.js'
|
|
7
7
|
export { default as getDeriveTransactionsToCheck } from './get-derive-transactions-to-check.js'
|
|
8
8
|
export { default as getCurrentEIP7702Delegation } from './get-current-eip7702-delegation.js'
|
|
9
|
+
export { default as verifyRpcPendingTxStatusBatch } from './verify-pending-tx-status-rpc.js'
|
|
9
10
|
export * from './exclude-unchanged-token-balances.js'
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Determines the status of an RPC transaction response.
|
|
3
|
+
*
|
|
4
|
+
* @param {Object|null} rpcTx - The RPC transaction response
|
|
5
|
+
* @returns {{status: 'confirmed' | 'pending' | 'dropped', blockNumber?: number}}
|
|
6
|
+
*/
|
|
7
|
+
function getTxStatus(rpcTx) {
|
|
8
|
+
if (rpcTx?.blockNumber) {
|
|
9
|
+
return { status: 'confirmed', blockNumber: parseInt(rpcTx.blockNumber, 16) }
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (rpcTx) {
|
|
13
|
+
return { status: 'pending' }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return { status: 'dropped' }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Batch verify pending transactions against the RPC node before marking them as dropped.
|
|
21
|
+
*
|
|
22
|
+
* Any lag in the Clarity indexer can cause confirmed transactions to be incorrectly
|
|
23
|
+
* flagged as dropped if Clarity hasn't indexed their block yet.
|
|
24
|
+
* By checking the RPC directly, we avoid this race condition.
|
|
25
|
+
*
|
|
26
|
+
* Uses a single batch RPC call for efficiency and graceful error handling.
|
|
27
|
+
*
|
|
28
|
+
* @param {Object} params
|
|
29
|
+
* @param {Object} params.baseAsset - The base asset to get the server from
|
|
30
|
+
* @param {Object} params.logger - Logger instance for info/debug output
|
|
31
|
+
* @param {string[]} params.txIds - Array of transaction IDs/hashes to verify
|
|
32
|
+
* @returns {Promise<Object<string, {status: 'confirmed' | 'pending' | 'dropped', blockNumber?: number}>>}
|
|
33
|
+
*/
|
|
34
|
+
export default async function verifyRpcPendingTxStatusBatch({ baseAsset, logger, txIds }) {
|
|
35
|
+
if (txIds.length === 0) return {}
|
|
36
|
+
|
|
37
|
+
const server = baseAsset.server
|
|
38
|
+
|
|
39
|
+
// Build batch request
|
|
40
|
+
const requests = txIds.map((txId) => server.getTransactionByHashRequest(txId))
|
|
41
|
+
|
|
42
|
+
let responses
|
|
43
|
+
try {
|
|
44
|
+
responses = await server.sendBatchRequest(requests)
|
|
45
|
+
} catch (error) {
|
|
46
|
+
// If batch request fails, skip all drop checks (safe default)
|
|
47
|
+
logger.warn('Batch RPC request failed, skipping drop checks', error)
|
|
48
|
+
return {}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Map responses to statuses
|
|
52
|
+
const results = {}
|
|
53
|
+
txIds.forEach((txId, idx) => {
|
|
54
|
+
const rpcTx = responses[idx]
|
|
55
|
+
const txStatus = getTxStatus(rpcTx)
|
|
56
|
+
results[txId] = txStatus
|
|
57
|
+
|
|
58
|
+
// Log each tx status
|
|
59
|
+
if (txStatus.status === 'confirmed') {
|
|
60
|
+
logger.info(`tx ${txId} is confirmed on-chain (block ${txStatus.blockNumber}), skipping drop`)
|
|
61
|
+
} else if (txStatus.status === 'pending') {
|
|
62
|
+
logger.info(`tx ${txId} is still in mempool, skipping drop`)
|
|
63
|
+
} else {
|
|
64
|
+
logger.info(`tx ${txId} not found on-chain, marking as dropped`)
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
return results
|
|
69
|
+
}
|