@exodus/ethereum-api 9.0.0-alpha.2 → 9.0.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.
Files changed (143) hide show
  1. package/CHANGELOG.md +2121 -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 +272 -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 +273 -103
  84. package/src/tx-log/clarity-truncated-history-monitor.js +109 -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,113 @@ 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
+
247
+ try {
248
+ const response = await this.getHistoryFromServer({ walletAccount, derivedData, refresh })
249
+
250
+ ;({ allTxs } = await normalizeTransactionsResponse({
251
+ asset: this.asset,
252
+ fromAddress: derivedData.ourWalletAddress,
253
+ response,
254
+ walletAccount,
255
+ }))
256
+
257
+ hasNewTxs = allTxs.length > 0
258
+
259
+ const logItemsByAsset = this.getAllLogItemsByAsset({
260
+ getLogItemsFromServerTx,
261
+ ourWalletAddress: derivedData.ourWalletAddress,
262
+ allTransactionsFromServer: allTxs,
263
+ asset: this.asset,
264
+ tokensByAddress,
265
+ assets,
266
+ })
267
+
268
+ const { txsToRemove } = await this.checkPendingTransactions({
269
+ txlist: allTxs,
270
+ walletAccount,
271
+ refresh,
272
+ logItemsByAsset,
273
+ asset: this.asset,
274
+ ...derivedData,
275
+ })
276
+
277
+ this.aci.removeTxLogBatch({
278
+ assetName,
279
+ walletAccount,
280
+ txs: txsToRemove,
281
+ batch,
282
+ })
283
+
284
+ for (const [assetName, txs] of Object.entries(logItemsByAsset)) {
285
+ this.aci.updateTxLogAndNotifyBatch({
286
+ assetName,
287
+ walletAccount,
288
+ txs,
289
+ refresh,
290
+ batch,
291
+ })
292
+ }
293
+
294
+ historyResponse = response
295
+ } catch (error) {
296
+ historyError = error
297
+ }
298
+
299
+ const newData = {
300
+ ...(await this.resolveAccountState({
301
+ derivedData,
302
+ tokens,
303
+ tokensByAddress,
304
+ historyResponse,
305
+ refresh,
306
+ })),
307
+ ...(historyResponse && { clarityCursor: historyResponse.cursor }),
308
+ }
309
+
310
+ try {
311
+ // Persist balance and account metadata even when the history path fails
312
+ // during startup bursts or other temporary backend pressure.
313
+ this.aci.updateAccountStateBatch({
314
+ assetName,
315
+ walletAccount,
316
+ accountState: derivedData.currentAccountState,
317
+ newData,
318
+ batch,
319
+ })
320
+
321
+ await this.aci.executeOperationsBatch(batch)
322
+ } catch (batchError) {
323
+ if (!historyError) throw batchError
324
+ this.logger.warn('error persisting account state after history failure', batchError)
325
+ }
326
+
327
+ if (historyError) {
328
+ throw historyError instanceof Error
329
+ ? historyError
330
+ : new Error(String(historyError), { cause: historyError })
331
+ }
159
332
 
160
- await this.removeFromTxLog(txsToRemove)
161
- await this.updateTxLogByAsset({ logItemsByAsset, walletAccount, refresh })
162
333
  if (refresh || hasNewTxs) {
163
- const unknownTokenAddresses = this._getUnknownTokenAddresses({
334
+ const unknownTokenAddresses = this.getUnknownTokenAddresses({
164
335
  transactions: allTxs,
165
336
  tokensByAddress,
166
337
  })
@@ -170,24 +341,40 @@ export class ClarityMonitor extends BaseMonitor {
170
341
  }
171
342
  }
172
343
 
173
- async deriveDataNeededForTick({ assetName, walletAccount }) {
174
- const receiveAddress = await this.aci.getReceiveAddress({
175
- assetName,
176
- walletAccount,
177
- useCache: true,
344
+ async resolveAccountState({ derivedData, tokens }) {
345
+ return this.getNewAccountState({
346
+ tokens,
347
+ currentTokenBalances: derivedData.currentAccountState?.tokenBalances,
348
+ ourWalletAddress: derivedData.ourWalletAddress,
178
349
  })
179
- const currentAccountState = await this.aci.getAccountState({ assetName, walletAccount })
180
- return {
181
- ourWalletAddress: receiveAddress.toLowerCase(),
182
- currentAccountState,
183
- }
350
+ }
351
+
352
+ async getSecurityAccountState({ derivedData, shouldCheckBlacklist }) {
353
+ const eip7702Delegation = await getCurrentEIP7702Delegation({
354
+ server: this.server,
355
+ address: derivedData.ourWalletAddress,
356
+ eip7702Supported: this.eip7702Whitelist.get(),
357
+ currentDelegation: derivedData.currentAccountState?.eip7702Delegation,
358
+ logger: this.logger,
359
+ })
360
+
361
+ const isBlacklisted = shouldCheckBlacklist
362
+ ? await getCurrentBlackListStatus({
363
+ getBlackListStatus: this.getBlackListStatus,
364
+ address: derivedData.ourWalletAddress,
365
+ currentIsBlacklisted: derivedData.currentAccountState?.isBlacklisted,
366
+ logger: this.logger,
367
+ })
368
+ : undefined
369
+
370
+ return { eip7702Delegation, isBlacklisted }
184
371
  }
185
372
 
186
373
  async getNewAccountState({ tokens, currentTokenBalances, ourWalletAddress }) {
187
374
  const asset = this.asset
188
- const newAccountState = {}
375
+ const newAccountState = Object.create(null)
189
376
  const balances = await this.getBalances({ tokens, ourWalletAddress })
190
- if (isRpcBalanceAsset(asset)) {
377
+ if (this.rpcBalanceAssetNames.includes(asset.name)) {
191
378
  const balance = balances[asset.name]
192
379
  newAccountState.balance = asset.currency.baseUnit(balance)
193
380
  }
@@ -208,7 +395,7 @@ export class ClarityMonitor extends BaseMonitor {
208
395
 
209
396
  async getReceiveAddressesByWalletAccount() {
210
397
  const walletAccounts = await this.aci.getWalletAccounts({ assetName: this.asset.name })
211
- const addressesByAccount = {}
398
+ const addressesByAccount = Object.create(null)
212
399
  for (const walletAccount of walletAccounts) {
213
400
  addressesByAccount[walletAccount] = await this.aci.getReceiveAddresses({
214
401
  assetName: this.asset.name,
@@ -223,42 +410,28 @@ export class ClarityMonitor extends BaseMonitor {
223
410
  async subscribeWalletAddresses() {
224
411
  const addressesByWalletAccount = await this.getReceiveAddressesByWalletAccount()
225
412
  Object.entries(addressesByWalletAccount).forEach(([walletAccount, addresses]) => {
226
- const address = String([...addresses][0]).toLowerCase() // Only check m/0/0
227
- this.server.connectTransactions({ walletAccount, address })
413
+ try {
414
+ const [rawAddress] = addresses
415
+ assert(rawAddress, 'rawAddress should be defined')
416
+
417
+ const address = String(rawAddress).toLowerCase() // Only check m/0/0
418
+ this.server.connectTransactions({ walletAccount, address })
419
+ } catch (e) {
420
+ this.logger.error(e)
421
+ }
228
422
  })
229
423
  }
230
424
 
231
425
  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]
426
+ return getBatchedRpcBalances({
427
+ baseAsset: this.asset,
428
+ ourWalletAddress,
429
+ tokens,
430
+ rpcBalanceAssetNames: this.rpcBalanceAssetNames,
257
431
  })
258
- return Object.fromEntries(entries)
259
432
  }
260
433
 
261
- _getUnknownTokenAddresses({ transactions, tokensByAddress }) {
434
+ getUnknownTokenAddresses({ transactions, tokensByAddress }) {
262
435
  const set = transactions.reduce((acc, txn) => {
263
436
  const transfers = filterEffects(txn.effects, 'erc20') || []
264
437
  transfers.forEach((transfer) => {
@@ -272,24 +445,21 @@ export class ClarityMonitor extends BaseMonitor {
272
445
  return [...set]
273
446
  }
274
447
 
275
- async updateGasPrice({ gasPrice, baseFeePerGas }) {
448
+ // NOTE: Here, fetchedGasPrices is the result of a call to `ClarityMonitor.getFee()`.
449
+ async updateGasPrice(fetchedGasPrices) {
276
450
  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 })
451
+ await executeEthLikeFeeMonitorUpdate({
452
+ assetClientInterface: this.aci,
453
+ feeAsset: this.asset,
454
+ fetchedGasPrices,
455
+ })
286
456
  } catch (e) {
287
457
  this.logger.warn('error updating gasPrice', e)
288
458
  }
289
459
  }
290
460
 
291
461
  async onTransaction({ walletAccount }) {
292
- return this.tick({ walletAccount })
462
+ return this.tickWalletAccounts({ walletAccount })
293
463
  }
294
464
 
295
465
  async onFeeUpdated(fee) {
@@ -0,0 +1,109 @@
1
+ import { ClarityMonitor } from './clarity-monitor.js'
2
+ import { extractBalanceFromClarityAccountInfo } from './monitor-utils/index.js'
3
+
4
+ /**
5
+ * Clarity v2 + HTTP `getAllTransactions` with `truncated: true` so the backend can attach
6
+ * `accountInfo` (pre-cursor balance/nonce/token snapshots). Merges that data in
7
+ * `resolveAccountState` before optional RPC follow-up; does not use `rpcBalanceAssetNames`
8
+ * (forced empty).
9
+ */
10
+ export class ClarityTruncatedHistoryMonitor extends ClarityMonitor {
11
+ constructor({ rpcBalanceAssetNames, ...args } = {}) {
12
+ super({ ...args, rpcBalanceAssetNames: [] })
13
+ }
14
+
15
+ async getStateUpdate(_args) {
16
+ return { accountState: Object.create(null) }
17
+ }
18
+
19
+ async resolveAccountState({ derivedData, tokens, tokensByAddress, historyResponse, refresh }) {
20
+ let currentAccountState = derivedData.currentAccountState
21
+ let resetBalances
22
+
23
+ // When refreshing, we need to clear out old asset
24
+ // balances in `accountState` since these can surface
25
+ // values populated from alternative monitors i.e.
26
+ // `no-history`, which can be written at arbitrary
27
+ // points in time.
28
+ //
29
+ // Since the `truncated-history-monitor` does not
30
+ // guarantee an `accountState` write for an asset,
31
+ // (these are only provided when the history is
32
+ // truncated), we must ensure older checkpoints cannot
33
+ // be inadvertently relied upon.
34
+ //
35
+ if (refresh) {
36
+ const defaultAccountState = this.asset.api.createAccountState().create()
37
+ resetBalances = {
38
+ balance: defaultAccountState.balance,
39
+ tokenBalances: defaultAccountState.tokenBalances,
40
+ }
41
+ currentAccountState = (currentAccountState ?? defaultAccountState).merge(resetBalances)
42
+ }
43
+
44
+ const balanceFromAccountInfo = extractBalanceFromClarityAccountInfo({
45
+ accountInfo: historyResponse?.accountInfo,
46
+ asset: this.asset,
47
+ tokensByAddress,
48
+ })
49
+
50
+ const { accountState } = await this.getStateUpdate({
51
+ derivedData,
52
+ tokens,
53
+ currentTokenBalances: {
54
+ ...currentAccountState?.tokenBalances,
55
+ ...balanceFromAccountInfo.tokenBalances,
56
+ },
57
+ })
58
+
59
+ // Clarity `accountInfo`, then RPC `accountState` (empty here) overlays.
60
+ const prevTokenBalances = currentAccountState?.tokenBalances
61
+ const prevTruncatedAccountState = currentAccountState?.truncatedAccountState
62
+ const newData = {
63
+ ...resetBalances,
64
+ ...balanceFromAccountInfo,
65
+ ...accountState,
66
+ }
67
+
68
+ if (prevTokenBalances || balanceFromAccountInfo.tokenBalances || accountState.tokenBalances) {
69
+ newData.tokenBalances = {
70
+ ...prevTokenBalances,
71
+ ...balanceFromAccountInfo.tokenBalances,
72
+ ...accountState.tokenBalances,
73
+ }
74
+ }
75
+
76
+ if (
77
+ prevTruncatedAccountState ||
78
+ balanceFromAccountInfo.truncatedAccountState ||
79
+ accountState.truncatedAccountState
80
+ ) {
81
+ newData.truncatedAccountState = {
82
+ ...prevTruncatedAccountState,
83
+ ...balanceFromAccountInfo.truncatedAccountState,
84
+ ...accountState.truncatedAccountState,
85
+ tokenBalances: {
86
+ ...prevTruncatedAccountState?.tokenBalances,
87
+ ...balanceFromAccountInfo.truncatedAccountState?.tokenBalances,
88
+ ...accountState.truncatedAccountState?.tokenBalances,
89
+ },
90
+ }
91
+ }
92
+
93
+ return newData
94
+ }
95
+
96
+ async getHistoryFromServer({ walletAccount, derivedData, refresh }) {
97
+ const address = derivedData.ourWalletAddress
98
+ const currentCursor = derivedData.currentAccountState?.clarityCursor
99
+ const cursor = currentCursor && !refresh ? currentCursor : null
100
+ return this.server.getAllTransactions({
101
+ walletAccount,
102
+ address,
103
+ cursor,
104
+ refresh,
105
+ withInput: true,
106
+ truncated: true,
107
+ })
108
+ }
109
+ }