@exodus/ethereum-api 9.0.0-alpha.3 → 9.0.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.
Files changed (143) hide show
  1. package/CHANGELOG.md +2139 -0
  2. package/LICENSE +7 -0
  3. package/README.md +124 -0
  4. package/package.json +34 -33
  5. package/src/address-has-history.js +6 -2
  6. package/src/allowance/constants.js +4 -5
  7. package/src/allowance/index.js +159 -40
  8. package/src/check-tx/create-assess-transaction.js +23 -0
  9. package/src/check-tx/create-check-tx.js +104 -0
  10. package/src/check-tx/index.js +1 -0
  11. package/src/create-asset-plugin-factory.js +115 -0
  12. package/src/create-asset-utils.js +520 -0
  13. package/src/create-asset.js +273 -97
  14. package/src/create-token-factory.js +47 -17
  15. package/src/custom-fees.js +57 -0
  16. package/src/eip7702-whitelist.js +62 -0
  17. package/src/ens/index.js +12 -17
  18. package/src/error-wrapper.js +383 -0
  19. package/src/eth-like-util.js +172 -33
  20. package/src/exodus-eth-server/api-coin-nodes.js +27 -196
  21. package/src/exodus-eth-server/api.js +23 -14
  22. package/src/exodus-eth-server/clarity-v2.js +255 -0
  23. package/src/exodus-eth-server/clarity.js +67 -265
  24. package/src/exodus-eth-server/errors.js +40 -0
  25. package/src/exodus-eth-server/eth-like-server.js +385 -0
  26. package/src/exodus-eth-server/fetch-json.js +48 -0
  27. package/src/exodus-eth-server/index.js +77 -0
  28. package/src/exodus-eth-server/utils.js +31 -0
  29. package/src/exodus-eth-server/ws-gateway.js +306 -0
  30. package/src/exodus-eth-server/ws.js +1 -2
  31. package/src/fee-data/EthLikeFeeData.js +81 -0
  32. package/src/fee-data/index.js +54 -0
  33. package/src/fee-utils.js +229 -0
  34. package/src/gas-estimation.js +183 -66
  35. package/src/get-balance-for-address.js +1 -1
  36. package/src/get-balances.js +285 -27
  37. package/src/get-fee.js +92 -56
  38. package/src/get-historical-balance.js +58 -0
  39. package/src/hooks/index.js +1 -1
  40. package/src/hooks/monitor.js +182 -43
  41. package/src/hooks/staking-freshness.js +58 -0
  42. package/src/index.js +118 -12
  43. package/src/move-funds.js +153 -0
  44. package/src/multicall3/index.js +212 -0
  45. package/src/multisig-address.js +146 -0
  46. package/src/nft-utils.js +60 -0
  47. package/src/number-utils.js +34 -2
  48. package/src/optimism-gas/index.js +13 -5
  49. package/src/send-validations.js +58 -0
  50. package/src/server-based-fee-monitor.js +12 -19
  51. package/src/simulate-tx/fetch-tx-preview.js +0 -2
  52. package/src/simulate-tx/index.js +2 -2
  53. package/src/simulate-tx/simulate-eth-tx.js +24 -11
  54. package/src/simulation/common.js +34 -0
  55. package/src/simulation/create-simulate-message.js +49 -0
  56. package/src/simulation/create-simulate-transactions.js +108 -0
  57. package/src/simulation/estimate-fee.js +14 -0
  58. package/src/simulation/estimate-simple-transfer.js +15 -0
  59. package/src/simulation/get-message-type.js +18 -0
  60. package/src/simulation/simulate-message-api.js +68 -0
  61. package/src/simulation/simulate-transactions-api.js +266 -0
  62. package/src/simulation/simulate-transactions.js +16 -0
  63. package/src/simulation/transactions.js +52 -0
  64. package/src/simulation/try-estimating-changes-locally.js +26 -0
  65. package/src/staking/api/index.js +43 -0
  66. package/src/staking/cached-read.js +33 -0
  67. package/src/staking/ethereum/api.js +166 -34
  68. package/src/staking/ethereum/deps.js +37 -0
  69. package/src/staking/ethereum/everstake.js +113 -0
  70. package/src/staking/ethereum/index.js +4 -3
  71. package/src/staking/ethereum/service.js +682 -181
  72. package/src/staking/ethereum/staking-utils.js +275 -4
  73. package/src/staking/fantom-staking.js +9 -9
  74. package/src/staking/index.js +4 -4
  75. package/src/staking/matic/addresses.js +2 -0
  76. package/src/staking/matic/api.js +69 -49
  77. package/src/staking/matic/index.js +3 -3
  78. package/src/staking/matic/matic-staking-utils.js +243 -5
  79. package/src/staking/matic/matic-staking.md +53 -0
  80. package/src/staking/staking-provider-client.js +1 -1
  81. package/src/staking/utils/index.js +39 -0
  82. package/src/tx-create.js +666 -0
  83. package/src/tx-log/clarity-monitor.js +276 -103
  84. package/src/tx-log/clarity-truncated-history-monitor.js +130 -0
  85. package/src/tx-log/clarity-truncated-history-monitor.md +219 -0
  86. package/src/tx-log/clarity-utils/absolute.js +96 -0
  87. package/src/tx-log/clarity-utils/get-log-items-from-server-tx.js +94 -31
  88. package/src/tx-log/clarity-utils/get-names-of-tokens-transferred-by-server-tx.js +3 -2
  89. package/src/tx-log/clarity-utils/index.js +9 -3
  90. package/src/tx-log/clarity-utils/is-spam-pending-tx.js +13 -0
  91. package/src/tx-log/clarity-utils/normalize-transactions-response.js +65 -0
  92. package/src/tx-log/ethereum-monitor.js +93 -47
  93. package/src/tx-log/ethereum-no-history-monitor.js +52 -41
  94. package/src/tx-log/get-optimistic-txlog-effects.js +379 -0
  95. package/src/tx-log/index.js +5 -3
  96. package/src/tx-log/monitor-utils/check-pending-transactions.js +14 -5
  97. package/src/tx-log/monitor-utils/exclude-unchanged-token-balances.js +1 -1
  98. package/src/tx-log/monitor-utils/extract-balance-from-clarity-account-info.js +108 -0
  99. package/src/tx-log/monitor-utils/get-balance-updates.js +44 -0
  100. package/src/tx-log/monitor-utils/get-batched-rpc-balances.js +47 -0
  101. package/src/tx-log/monitor-utils/get-current-blacklist-status.js +38 -0
  102. package/src/tx-log/monitor-utils/get-current-eip7702-delegation.js +79 -0
  103. package/src/tx-log/monitor-utils/get-derive-transactions-to-check.js +10 -5
  104. package/src/tx-log/monitor-utils/get-fee-amount.js +1 -1
  105. package/src/tx-log/monitor-utils/get-history-from-server.js +1 -1
  106. package/src/tx-log/monitor-utils/get-log-items-from-server-tx.js +75 -33
  107. package/src/tx-log/monitor-utils/get-names-of-tokens-transferred-by-server-tx.js +3 -2
  108. package/src/tx-log/monitor-utils/get-raw-transfer-amount.js +29 -0
  109. package/src/tx-log/monitor-utils/get-transfers-by-token-name.js +3 -2
  110. package/src/tx-log/monitor-utils/get-value-of-transfers.js +10 -12
  111. package/src/tx-log/monitor-utils/index.js +12 -7
  112. package/src/tx-log/monitor-utils/is-unverifiable-pending-tx-expired.js +19 -0
  113. package/src/tx-log/monitor-utils/verify-pending-tx-status-rpc.js +90 -0
  114. package/src/tx-log-staking-processor/asset-staking-tx-data.js +82 -24
  115. package/src/tx-log-staking-processor/index.js +28 -14
  116. package/src/tx-log-staking-processor/utils.js +21 -20
  117. package/src/tx-send/broadcast-error-handler.js +63 -0
  118. package/src/tx-send/index.js +1 -2
  119. package/src/tx-send/nonce-utils.js +97 -0
  120. package/src/tx-send/tx-send.js +94 -288
  121. package/src/tx-sign/index.js +17 -0
  122. package/src/tx-type/index.js +280 -0
  123. package/src/watch-tx.js +36 -0
  124. package/src/web3/createSimulateMessage.js +3 -2
  125. package/src/web3/createSimulateTransactions.js +11 -0
  126. package/src/web3/index.js +3 -1
  127. package/src/etherscan/account.js +0 -49
  128. package/src/etherscan/index.js +0 -28
  129. package/src/etherscan/logs.js +0 -16
  130. package/src/etherscan/proxy.js +0 -47
  131. package/src/etherscan/request.js +0 -26
  132. package/src/etherscan/ws.js +0 -88
  133. package/src/exodus-eth-server/server-factory.js +0 -28
  134. package/src/exodus-eth-server/server-map.js +0 -21
  135. package/src/get-fee-async.js +0 -97
  136. package/src/staking/matic/service.js +0 -437
  137. package/src/staking-api.js +0 -5
  138. package/src/tx-log/clarity-utils/get-derive-data-needed-for-tick.js +0 -12
  139. package/src/tx-log-staking-processor/get-asset-tx-amount.js +0 -26
  140. package/src/tx-send/get-fee-info.js +0 -41
  141. package/src/websocket/index.android.js +0 -2
  142. package/src/websocket/index.ios.js +0 -2
  143. package/src/websocket/index.js +0 -21
@@ -1,21 +1,46 @@
1
1
  import { BaseMonitor } from '@exodus/asset-lib'
2
- import { getAssetAddresses, isRpcBalanceAsset } from '@exodus/ethereum-lib'
3
- import { isEmpty } from 'lodash'
2
+ import { getAssetAddresses } from '@exodus/ethereum-lib'
3
+ import lodash from 'lodash'
4
+ import assert from 'minimalistic-assert'
4
5
 
6
+ import { applyRemoteConfigEip7702Whitelist, createEip7702Whitelist } from '../eip7702-whitelist.js'
7
+ import { executeEthLikeFeeMonitorUpdate } from '../fee-utils.js'
8
+ import {
9
+ filterEffects,
10
+ getLogItemsFromServerTx,
11
+ normalizeTransactionsResponse,
12
+ } from './clarity-utils/index.js'
13
+ import { getBatchedRpcBalances } from './monitor-utils/get-batched-rpc-balances.js'
5
14
  import {
6
- getAllLogItemsByAsset,
7
15
  checkPendingTransactions,
8
- getDeriveTransactionsToCheck,
9
16
  excludeUnchangedTokenBalances,
10
- } from './monitor-utils'
11
- import { getLogItemsFromServerTx, getDeriveDataNeededForTick, filterEffects } from './clarity-utils'
12
- import { fromHexToString } from '../number-utils'
17
+ getAllLogItemsByAsset,
18
+ getCurrentBlackListStatus,
19
+ getCurrentEIP7702Delegation,
20
+ getDeriveDataNeededForTick,
21
+ getDeriveTransactionsToCheck,
22
+ isUnverifiablePendingTxExpired,
23
+ verifyRpcPendingTxStatusBatch,
24
+ } from './monitor-utils/index.js'
25
+
26
+ const { isEmpty } = lodash
13
27
 
14
28
  export class ClarityMonitor extends BaseMonitor {
15
- constructor({ server, config, ...args }) {
29
+ constructor({
30
+ server,
31
+ config,
32
+ rpcBalanceAssetNames,
33
+ eip7702Supported,
34
+ eip7702Whitelist,
35
+ getBlackListStatus,
36
+ ...args
37
+ }) {
16
38
  super(args)
17
39
  this.config = { GAS_PRICE_FROM_WEBSOCKET: true, ...config }
18
40
  this.server = server
41
+ this.rpcBalanceAssetNames = rpcBalanceAssetNames
42
+ this.eip7702Whitelist = eip7702Whitelist ?? createEip7702Whitelist(eip7702Supported)
43
+ this.getBlackListStatus = getBlackListStatus
19
44
  this.getAllLogItemsByAsset = getAllLogItemsByAsset
20
45
  this.deriveDataNeededForTick = getDeriveDataNeededForTick(this.aci)
21
46
  this.deriveTransactionsToCheck = getDeriveTransactionsToCheck({
@@ -26,6 +51,12 @@ export class ClarityMonitor extends BaseMonitor {
26
51
  }
27
52
 
28
53
  setServer(config) {
54
+ applyRemoteConfigEip7702Whitelist({
55
+ eip7702Whitelist: this.eip7702Whitelist,
56
+ remoteConfig: config,
57
+ logger: this.logger,
58
+ })
59
+
29
60
  const uri = config?.server || this.server.defaultUri
30
61
 
31
62
  if (uri === this.server.uri) {
@@ -60,19 +91,23 @@ export class ClarityMonitor extends BaseMonitor {
60
91
 
61
92
  // eslint-disable-next-line no-undef
62
93
  async checkPendingTransactions(params) {
63
- const { pendingTransactionsToCheck, pendingTransactionsGroupedByAddressAndNonce } =
64
- checkPendingTransactions(params)
94
+ const {
95
+ pendingTransactionsToCheck,
96
+ pendingTransactionsGroupedByAddressAndNonce,
97
+ recentPendingTransactions,
98
+ } = checkPendingTransactions(params)
65
99
  const txsToRemove = []
66
100
  const { walletAccount } = params
67
101
 
68
- const updateTx = (tx, asset, { error, remove }) => {
102
+ const updateTx = (tx, asset, { error, remove, preserve, isTxConfirmed }) => {
69
103
  if (remove) {
70
104
  txsToRemove.push({ tx, assetSource: { asset, walletAccount } })
105
+ } else if (preserve) {
106
+ params.logItemsByAsset[asset].push(tx)
71
107
  } else {
72
108
  params.logItemsByAsset[asset].push({
73
109
  ...tx,
74
- dropped: true,
75
- error,
110
+ ...(isTxConfirmed ? { confirmations: 1 } : { dropped: true, error }),
76
111
  })
77
112
  }
78
113
 
@@ -85,11 +120,15 @@ export class ClarityMonitor extends BaseMonitor {
85
120
  assetSource: { asset: assetName, walletAccount },
86
121
  })
87
122
  } else if (tokenTxSet && tokenTxSet.has(tx.txId)) {
88
- params.logItemsByAsset[assetName].push({
89
- ...tokenTxSet.get(tx.txId),
90
- error,
91
- dropped: true,
92
- })
123
+ // Optional: the stored tx may name a token since removed from the registry
124
+ params.logItemsByAsset[assetName]?.push(
125
+ preserve
126
+ ? tokenTxSet.get(tx.txId)
127
+ : {
128
+ ...tokenTxSet.get(tx.txId),
129
+ ...(isTxConfirmed ? { confirmations: 1 } : { dropped: true, error }),
130
+ }
131
+ )
93
132
  }
94
133
  })
95
134
  return Promise.all(promises)
@@ -101,19 +140,82 @@ export class ClarityMonitor extends BaseMonitor {
101
140
  if (replaced) {
102
141
  await updateTx(tx, assetName, { remove: true })
103
142
  delete pendingTransactionsToCheck[tx.txId]
143
+ delete recentPendingTransactions[tx.txId]
104
144
  }
105
145
  }
106
146
 
147
+ // Batch verify all pending txs with a single RPC call
148
+ const txIds = Object.keys(pendingTransactionsToCheck)
149
+ const statuses = await verifyRpcPendingTxStatusBatch({
150
+ server: this.server,
151
+ logger: this.logger,
152
+ txIds,
153
+ })
154
+
107
155
  for (const { tx, assetName } of Object.values(pendingTransactionsToCheck)) {
108
- if (params.refresh) await updateTx(tx, assetName, { remove: true })
109
- else await updateTx(tx, assetName, { error: 'Dropped' })
156
+ const txStatus = statuses[tx.txId]
157
+ if (params.refresh) {
158
+ if (txStatus?.status === 'dropped') {
159
+ await updateTx(tx, assetName, { remove: true })
160
+ } else if (txStatus?.status === 'confirmed') {
161
+ // Confirmed on-chain but missing from the indexer response: mark it
162
+ // confirmed so it stops being a pending entry that would otherwise be
163
+ // preserved (exempt from the age ceiling) on every refresh forever.
164
+ await updateTx(tx, assetName, { isTxConfirmed: true })
165
+ } else if (
166
+ // No txStatus at all means the whole verification batch failed - evidence
167
+ // about nothing, so preserve unconditionally; the age ceiling only applies
168
+ // to a per-entry 'unknown' verdict (evidence about that one hash).
169
+ !txStatus ||
170
+ txStatus.status === 'pending' ||
171
+ !isUnverifiablePendingTxExpired(tx)
172
+ ) {
173
+ // Still live on-chain (or not verifiable yet) but missing from the indexer
174
+ // response: re-add it so the refresh overwrite cannot wipe it and break
175
+ // tx-log-based nonce resolution.
176
+ await updateTx(tx, assetName, { preserve: true })
177
+ } else {
178
+ // Unverifiable past the age ceiling: purge so a hash the node keeps
179
+ // erroring on cannot pin nonce derivation forever.
180
+ await updateTx(tx, assetName, { remove: true })
181
+ }
182
+ } else if (txStatus?.status === 'dropped') {
183
+ await updateTx(tx, assetName, { error: 'Dropped' })
184
+ }
185
+ // non-refresh: 'confirmed', 'pending' or unknown - tx is fine, wait for Clarity to confirm
186
+ }
187
+
188
+ if (params.refresh) {
189
+ // Too recent for a reliable on-chain verdict - always survive the refresh overwrite
190
+ for (const { tx, assetName } of Object.values(recentPendingTransactions)) {
191
+ await updateTx(tx, assetName, { preserve: true })
192
+ }
110
193
  }
111
194
 
112
195
  return { txsToRemove }
113
196
  }
114
197
 
198
+ async persistSecurityState({ walletAccount, accountState, isBlacklisted, eip7702Delegation }) {
199
+ const securityStatePatch = {
200
+ ...(isBlacklisted !== undefined && { isBlacklisted }),
201
+ ...(eip7702Delegation !== undefined && { eip7702Delegation }),
202
+ }
203
+
204
+ if (isEmpty(securityStatePatch)) {
205
+ return
206
+ }
207
+
208
+ await this.updateAccountState({
209
+ walletAccount,
210
+ accountState,
211
+ newData: securityStatePatch,
212
+ })
213
+ }
214
+
115
215
  async tick({ walletAccount, refresh }) {
116
216
  await this.subscribeWalletAddresses()
217
+ const tickCount = this.tickCount[walletAccount]
218
+ const shouldCheckBlacklist = tickCount === 0
117
219
 
118
220
  const assets = await this.aci.getAssetsForNetwork({ baseAssetName: this.asset.name })
119
221
  const tokens = Object.values(assets).filter((asset) => asset.baseAsset.name !== asset.name)
@@ -123,44 +225,116 @@ export class ClarityMonitor extends BaseMonitor {
123
225
  return map
124
226
  }, new Map())
125
227
  const assetName = this.asset.name
126
-
127
228
  const derivedData = await this.deriveData({ assetName, walletAccount, tokens })
128
- const response = await this.getHistoryFromServer({ walletAccount, derivedData, refresh })
129
- const allTxs = [...response.transactions.pending, ...response.transactions.confirmed]
130
- const hasNewTxs = allTxs.length > 0
131
-
132
- const logItemsByAsset = this.getAllLogItemsByAsset({
133
- getLogItemsFromServerTx,
134
- ourWalletAddress: derivedData.ourWalletAddress,
135
- allTransactionsFromServer: allTxs,
136
- asset: this.asset,
137
- tokensByAddress,
138
- assets,
229
+ const { eip7702Delegation, isBlacklisted } = await this.getSecurityAccountState({
230
+ derivedData,
231
+ shouldCheckBlacklist,
139
232
  })
140
233
 
141
- const { txsToRemove } = await this.checkPendingTransactions({
142
- txlist: allTxs,
234
+ await this.persistSecurityState({
143
235
  walletAccount,
144
- refresh,
145
- logItemsByAsset,
146
- asset: this.asset,
147
- ...derivedData,
236
+ accountState: derivedData.currentAccountState,
237
+ isBlacklisted,
238
+ eip7702Delegation,
148
239
  })
149
240
 
150
- const accountState = await this.getNewAccountState({
151
- tokens,
152
- currentTokenBalances: derivedData.currentAccountState?.tokenBalances,
153
- ourWalletAddress: derivedData.ourWalletAddress,
154
- })
155
- await this.updateAccountState({
156
- walletAccount,
157
- newData: { clarityCursor: response.cursor, ...accountState },
158
- })
241
+ const batch = this.aci.createOperationsBatch()
242
+ let allTxs = []
243
+ let hasNewTxs = false
244
+ let historyResponse
245
+ let historyError
246
+ let processedLogItemsByAsset
247
+
248
+ try {
249
+ const response = await this.getHistoryFromServer({ walletAccount, derivedData, refresh })
250
+
251
+ ;({ allTxs } = await normalizeTransactionsResponse({
252
+ asset: this.asset,
253
+ fromAddress: derivedData.ourWalletAddress,
254
+ response,
255
+ walletAccount,
256
+ }))
257
+
258
+ hasNewTxs = allTxs.length > 0
259
+
260
+ const logItemsByAsset = this.getAllLogItemsByAsset({
261
+ getLogItemsFromServerTx,
262
+ ourWalletAddress: derivedData.ourWalletAddress,
263
+ allTransactionsFromServer: allTxs,
264
+ asset: this.asset,
265
+ tokensByAddress,
266
+ assets,
267
+ })
268
+
269
+ const { txsToRemove } = await this.checkPendingTransactions({
270
+ txlist: allTxs,
271
+ walletAccount,
272
+ refresh,
273
+ logItemsByAsset,
274
+ asset: this.asset,
275
+ ...derivedData,
276
+ })
277
+
278
+ this.aci.removeTxLogBatch({
279
+ assetName,
280
+ walletAccount,
281
+ txs: txsToRemove,
282
+ batch,
283
+ })
284
+
285
+ for (const [assetName, txs] of Object.entries(logItemsByAsset)) {
286
+ this.aci.updateTxLogAndNotifyBatch({
287
+ assetName,
288
+ walletAccount,
289
+ txs,
290
+ refresh,
291
+ batch,
292
+ })
293
+ }
294
+
295
+ historyResponse = response
296
+ processedLogItemsByAsset = logItemsByAsset
297
+ } catch (error) {
298
+ historyError = error
299
+ }
300
+
301
+ const newData = {
302
+ ...(await this.resolveAccountState({
303
+ derivedData,
304
+ tokens,
305
+ tokensByAddress,
306
+ historyResponse,
307
+ logItemsByAsset: processedLogItemsByAsset,
308
+ refresh,
309
+ })),
310
+ ...(historyResponse && { clarityCursor: historyResponse.cursor }),
311
+ }
312
+
313
+ try {
314
+ // Persist balance and account metadata even when the history path fails
315
+ // during startup bursts or other temporary backend pressure.
316
+ this.aci.updateAccountStateBatch({
317
+ assetName,
318
+ walletAccount,
319
+ accountState: derivedData.currentAccountState,
320
+ newData,
321
+ batch,
322
+ })
323
+
324
+ await this.aci.executeOperationsBatch(batch)
325
+ } catch (batchError) {
326
+ if (!historyError) throw batchError
327
+ this.logger.warn('error persisting account state after history failure', batchError)
328
+ }
329
+
330
+ if (historyError) {
331
+ throw historyError instanceof Error
332
+ ? historyError
333
+ : new Error(String(historyError), { cause: historyError })
334
+ }
159
335
 
160
- await this.removeFromTxLog(txsToRemove)
161
- await this.updateTxLogByAsset({ logItemsByAsset, walletAccount, refresh })
162
336
  if (refresh || hasNewTxs) {
163
- const unknownTokenAddresses = this._getUnknownTokenAddresses({
337
+ const unknownTokenAddresses = this.getUnknownTokenAddresses({
164
338
  transactions: allTxs,
165
339
  tokensByAddress,
166
340
  })
@@ -170,24 +344,40 @@ export class ClarityMonitor extends BaseMonitor {
170
344
  }
171
345
  }
172
346
 
173
- async deriveDataNeededForTick({ assetName, walletAccount }) {
174
- const receiveAddress = await this.aci.getReceiveAddress({
175
- assetName,
176
- walletAccount,
177
- useCache: true,
347
+ async resolveAccountState({ derivedData, tokens }) {
348
+ return this.getNewAccountState({
349
+ tokens,
350
+ currentTokenBalances: derivedData.currentAccountState?.tokenBalances,
351
+ ourWalletAddress: derivedData.ourWalletAddress,
178
352
  })
179
- const currentAccountState = await this.aci.getAccountState({ assetName, walletAccount })
180
- return {
181
- ourWalletAddress: receiveAddress.toLowerCase(),
182
- currentAccountState,
183
- }
353
+ }
354
+
355
+ async getSecurityAccountState({ derivedData, shouldCheckBlacklist }) {
356
+ const eip7702Delegation = await getCurrentEIP7702Delegation({
357
+ server: this.server,
358
+ address: derivedData.ourWalletAddress,
359
+ eip7702Supported: this.eip7702Whitelist.get(),
360
+ currentDelegation: derivedData.currentAccountState?.eip7702Delegation,
361
+ logger: this.logger,
362
+ })
363
+
364
+ const isBlacklisted = shouldCheckBlacklist
365
+ ? await getCurrentBlackListStatus({
366
+ getBlackListStatus: this.getBlackListStatus,
367
+ address: derivedData.ourWalletAddress,
368
+ currentIsBlacklisted: derivedData.currentAccountState?.isBlacklisted,
369
+ logger: this.logger,
370
+ })
371
+ : undefined
372
+
373
+ return { eip7702Delegation, isBlacklisted }
184
374
  }
185
375
 
186
376
  async getNewAccountState({ tokens, currentTokenBalances, ourWalletAddress }) {
187
377
  const asset = this.asset
188
- const newAccountState = {}
378
+ const newAccountState = Object.create(null)
189
379
  const balances = await this.getBalances({ tokens, ourWalletAddress })
190
- if (isRpcBalanceAsset(asset)) {
380
+ if (this.rpcBalanceAssetNames.includes(asset.name)) {
191
381
  const balance = balances[asset.name]
192
382
  newAccountState.balance = asset.currency.baseUnit(balance)
193
383
  }
@@ -208,7 +398,7 @@ export class ClarityMonitor extends BaseMonitor {
208
398
 
209
399
  async getReceiveAddressesByWalletAccount() {
210
400
  const walletAccounts = await this.aci.getWalletAccounts({ assetName: this.asset.name })
211
- const addressesByAccount = {}
401
+ const addressesByAccount = Object.create(null)
212
402
  for (const walletAccount of walletAccounts) {
213
403
  addressesByAccount[walletAccount] = await this.aci.getReceiveAddresses({
214
404
  assetName: this.asset.name,
@@ -223,42 +413,28 @@ export class ClarityMonitor extends BaseMonitor {
223
413
  async subscribeWalletAddresses() {
224
414
  const addressesByWalletAccount = await this.getReceiveAddressesByWalletAccount()
225
415
  Object.entries(addressesByWalletAccount).forEach(([walletAccount, addresses]) => {
226
- const address = String([...addresses][0]).toLowerCase() // Only check m/0/0
227
- this.server.connectTransactions({ walletAccount, address })
416
+ try {
417
+ const [rawAddress] = addresses
418
+ assert(rawAddress, 'rawAddress should be defined')
419
+
420
+ const address = String(rawAddress).toLowerCase() // Only check m/0/0
421
+ this.server.connectTransactions({ walletAccount, address })
422
+ } catch (e) {
423
+ this.logger.error(e)
424
+ }
228
425
  })
229
426
  }
230
427
 
231
428
  async getBalances({ tokens, ourWalletAddress }) {
232
- const batch = {}
233
- if (isRpcBalanceAsset(this.asset)) {
234
- const request = this.server.getBalanceRequest(ourWalletAddress)
235
- batch[this.asset.name] = request
236
- }
237
-
238
- for (const token of tokens) {
239
- if (isRpcBalanceAsset(token) && token.contract.address) {
240
- const request = this.server.balanceOfRequest(ourWalletAddress, token.contract.address)
241
- batch[token.name] = request
242
- }
243
- }
244
-
245
- const pairs = Object.entries(batch)
246
- if (pairs.length === 0) {
247
- return {}
248
- }
249
-
250
- const requests = pairs.map((pair) => pair[1])
251
- const responses = await this.server.sendBatchRequest(requests)
252
- const entries = pairs.map((pair, idx) => {
253
- const balanceHex = responses[idx]
254
- const name = pair[0]
255
- const balance = fromHexToString(balanceHex)
256
- return [name, balance]
429
+ return getBatchedRpcBalances({
430
+ baseAsset: this.asset,
431
+ ourWalletAddress,
432
+ tokens,
433
+ rpcBalanceAssetNames: this.rpcBalanceAssetNames,
257
434
  })
258
- return Object.fromEntries(entries)
259
435
  }
260
436
 
261
- _getUnknownTokenAddresses({ transactions, tokensByAddress }) {
437
+ getUnknownTokenAddresses({ transactions, tokensByAddress }) {
262
438
  const set = transactions.reduce((acc, txn) => {
263
439
  const transfers = filterEffects(txn.effects, 'erc20') || []
264
440
  transfers.forEach((transfer) => {
@@ -272,24 +448,21 @@ export class ClarityMonitor extends BaseMonitor {
272
448
  return [...set]
273
449
  }
274
450
 
275
- async updateGasPrice({ gasPrice, baseFeePerGas }) {
451
+ // NOTE: Here, fetchedGasPrices is the result of a call to `ClarityMonitor.getFee()`.
452
+ async updateGasPrice(fetchedGasPrices) {
276
453
  try {
277
- const feeConfig = { gasPrice: `${fromHexToString(gasPrice).toString()} wei` }
278
- if (baseFeePerGas) {
279
- feeConfig.baseFeePerGas = `${fromHexToString(baseFeePerGas)} wei`
280
- }
281
-
282
- this.logger.debug(
283
- `Update ${this.asset.name} gas price: ${feeConfig.gasPrice}, baseFeePerGas: ${feeConfig.baseFeePerGas}`
284
- )
285
- await this.aci.updateFeeConfig({ assetName: this.asset.name, feeConfig })
454
+ await executeEthLikeFeeMonitorUpdate({
455
+ assetClientInterface: this.aci,
456
+ feeAsset: this.asset,
457
+ fetchedGasPrices,
458
+ })
286
459
  } catch (e) {
287
460
  this.logger.warn('error updating gasPrice', e)
288
461
  }
289
462
  }
290
463
 
291
464
  async onTransaction({ walletAccount }) {
292
- return this.tick({ walletAccount })
465
+ return this.tickWalletAccounts({ walletAccount })
293
466
  }
294
467
 
295
468
  async onFeeUpdated(fee) {
@@ -0,0 +1,130 @@
1
+ import { ClarityMonitor } from './clarity-monitor.js'
2
+ import { extractBalanceFromClarityAccountInfo } from './monitor-utils/index.js'
3
+
4
+ const omitTokensWithConfirmedBalanceChange = ({ tokenBalances, logItemsByAsset }) => {
5
+ if (!tokenBalances || !logItemsByAsset) return tokenBalances
6
+ const result = { ...tokenBalances }
7
+ for (const [assetName, txs] of Object.entries(logItemsByAsset)) {
8
+ if (txs.some((tx) => tx.confirmations > 0 && tx.data?.balanceChange)) delete result[assetName]
9
+ }
10
+
11
+ return result
12
+ }
13
+
14
+ /**
15
+ * Clarity v2 + HTTP `getAllTransactions` with `truncated: true` so the backend can attach
16
+ * `accountInfo` (pre-cursor balance/nonce/token snapshots). Merges that data in
17
+ * `resolveAccountState` before optional RPC follow-up; does not use `rpcBalanceAssetNames`
18
+ * (forced empty).
19
+ */
20
+ export class ClarityTruncatedHistoryMonitor extends ClarityMonitor {
21
+ constructor({ rpcBalanceAssetNames, ...args } = {}) {
22
+ super({ ...args, rpcBalanceAssetNames: [] })
23
+ }
24
+
25
+ async getStateUpdate(_args) {
26
+ return { accountState: Object.create(null) }
27
+ }
28
+
29
+ async resolveAccountState({
30
+ derivedData,
31
+ tokens,
32
+ tokensByAddress,
33
+ historyResponse,
34
+ logItemsByAsset,
35
+ refresh,
36
+ }) {
37
+ let currentAccountState = derivedData.currentAccountState
38
+ let resetBalances
39
+
40
+ // When refreshing, we need to clear out old asset
41
+ // balances in `accountState` since these can surface
42
+ // values populated from alternative monitors i.e.
43
+ // `no-history`, which can be written at arbitrary
44
+ // points in time.
45
+ //
46
+ // Since the `truncated-history-monitor` does not
47
+ // guarantee an `accountState` write for an asset,
48
+ // (these are only provided when the history is
49
+ // truncated), we must ensure older checkpoints cannot
50
+ // be inadvertently relied upon.
51
+ //
52
+ if (refresh) {
53
+ const defaultAccountState = this.asset.api.createAccountState().create()
54
+ resetBalances = {
55
+ balance: defaultAccountState.balance,
56
+ tokenBalances: defaultAccountState.tokenBalances,
57
+ }
58
+ currentAccountState = (currentAccountState ?? defaultAccountState).merge(resetBalances)
59
+ }
60
+
61
+ const balanceFromAccountInfo = extractBalanceFromClarityAccountInfo({
62
+ accountInfo: historyResponse?.accountInfo,
63
+ asset: this.asset,
64
+ tokensByAddress,
65
+ })
66
+
67
+ const prevTokenBalances = omitTokensWithConfirmedBalanceChange({
68
+ tokenBalances: currentAccountState?.tokenBalances,
69
+ logItemsByAsset,
70
+ })
71
+
72
+ const { accountState } = await this.getStateUpdate({
73
+ derivedData,
74
+ tokens,
75
+ currentTokenBalances: {
76
+ ...prevTokenBalances,
77
+ ...balanceFromAccountInfo.tokenBalances,
78
+ },
79
+ })
80
+
81
+ // Clarity `accountInfo`, then RPC `accountState` (empty here) overlays.
82
+ const prevTruncatedAccountState = currentAccountState?.truncatedAccountState
83
+ const newData = {
84
+ ...resetBalances,
85
+ ...balanceFromAccountInfo,
86
+ ...accountState,
87
+ }
88
+
89
+ if (prevTokenBalances || balanceFromAccountInfo.tokenBalances || accountState.tokenBalances) {
90
+ newData.tokenBalances = {
91
+ ...prevTokenBalances,
92
+ ...balanceFromAccountInfo.tokenBalances,
93
+ ...accountState.tokenBalances,
94
+ }
95
+ }
96
+
97
+ if (
98
+ prevTruncatedAccountState ||
99
+ balanceFromAccountInfo.truncatedAccountState ||
100
+ accountState.truncatedAccountState
101
+ ) {
102
+ newData.truncatedAccountState = {
103
+ ...prevTruncatedAccountState,
104
+ ...balanceFromAccountInfo.truncatedAccountState,
105
+ ...accountState.truncatedAccountState,
106
+ tokenBalances: {
107
+ ...prevTruncatedAccountState?.tokenBalances,
108
+ ...balanceFromAccountInfo.truncatedAccountState?.tokenBalances,
109
+ ...accountState.truncatedAccountState?.tokenBalances,
110
+ },
111
+ }
112
+ }
113
+
114
+ return newData
115
+ }
116
+
117
+ async getHistoryFromServer({ walletAccount, derivedData, refresh }) {
118
+ const address = derivedData.ourWalletAddress
119
+ const currentCursor = derivedData.currentAccountState?.clarityCursor
120
+ const cursor = currentCursor && !refresh ? currentCursor : null
121
+ return this.server.getAllTransactions({
122
+ walletAccount,
123
+ address,
124
+ cursor,
125
+ refresh,
126
+ withInput: true,
127
+ truncated: true,
128
+ })
129
+ }
130
+ }