@exodus/ethereum-api 9.0.0-alpha.3 → 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
@@ -0,0 +1,379 @@
1
+ import { SynchronizedTime } from '@exodus/basic-utils'
2
+ import NumberUnit from '@exodus/currency'
3
+ import {
4
+ getHighestIncentiveTxByNonce,
5
+ isEthereumLikeToken,
6
+ parseUnsignedTx,
7
+ } from '@exodus/ethereum-lib'
8
+ import { bufferToHex } from '@exodus/ethereumjs/util'
9
+ import assert from 'minimalistic-assert'
10
+
11
+ import { EthereumStaking } from '../staking/ethereum/api.js'
12
+ import {
13
+ ethereumUnstakeOptimisticSideEffectTxLogs,
14
+ ethereumUnstakePendingOptimisticSideEffectTxLogs,
15
+ isEthereumStakingPoolContract,
16
+ } from '../staking/ethereum/staking-utils.js'
17
+ import { MaticStakingApi } from '../staking/matic/api.js'
18
+ import {
19
+ DELEGATE,
20
+ maticDelegateOptimisticSideEffectTxLogs,
21
+ } from '../staking/matic/matic-staking-utils.js'
22
+
23
+ export const getOptimisticTxLogEffects = async ({
24
+ asset,
25
+ assetClientInterface,
26
+ bundleId,
27
+ confirmations = 0,
28
+ date = SynchronizedTime.now(),
29
+ fromAddress,
30
+ txId,
31
+ unsignedTx,
32
+ walletAccount,
33
+ }) => {
34
+ assert(asset, 'expected asset')
35
+ assert(assetClientInterface, 'expected assetClientInterface')
36
+ assert(Number.isInteger(confirmations), 'expected integer confirmations')
37
+ assert(typeof fromAddress === 'string', 'expected string fromAddress')
38
+ assert(txId, 'expected txId')
39
+ assert(unsignedTx, 'expected unsignedTx')
40
+ assert(walletAccount, 'expected walletAccount')
41
+
42
+ // this converts an transactionBuffer to values we can use when creating the tx logs
43
+ const parsedTx = parseUnsignedTx({ asset, unsignedTx })
44
+
45
+ const { nonce, to, txToAddress } = parsedTx
46
+ assert(Number.isInteger(nonce), 'expected integer nonce')
47
+
48
+ const amount = parsedTx.amount || asset.currency.ZERO
49
+ assert(amount instanceof NumberUnit, 'expected NumberUnit amount')
50
+
51
+ const feeAmount = parsedTx.fee
52
+ assert(feeAmount instanceof NumberUnit, 'expected feeAmount')
53
+
54
+ const txValue = parsedTx.value
55
+ assert(txValue instanceof NumberUnit, 'expected txValue')
56
+
57
+ const maybeTipGasPrice = parsedTx.tipGasPrice
58
+ if (maybeTipGasPrice) {
59
+ assert(maybeTipGasPrice instanceof NumberUnit, 'expected NumberUnit tipGasPrice')
60
+ }
61
+
62
+ const gasLimit = parsedTx.gasLimit
63
+ assert(Number.isInteger(gasLimit), 'expected integer gasLimit')
64
+
65
+ if (to) assert(typeof to === 'string', 'expected string toAddress')
66
+
67
+ const data = parsedTx.data
68
+ const methodId = data ? bufferToHex(data).slice(0, 10) : undefined
69
+
70
+ const selfSend = fromAddress.toLowerCase() === to?.toLowerCase()
71
+
72
+ const baseAsset = asset.baseAsset
73
+
74
+ const maybeTxToReplace = await getHighestIncentiveTxByNonce({
75
+ asset,
76
+ assetClientInterface,
77
+ nonce,
78
+ walletAccount,
79
+ })
80
+
81
+ const replacedTxId =
82
+ maybeTxToReplace && maybeTxToReplace.feeAmount.lt(feeAmount) ? maybeTxToReplace.txId : undefined
83
+
84
+ if (!replacedTxId && maybeTxToReplace) {
85
+ console.debug('Attempting to replace a transaction using an insufficient fee!')
86
+ }
87
+
88
+ // Contains effects for smart-contract initiated token movements.
89
+ const methodOptimisticSideEffects = await operationTxLogSideEffects({
90
+ txToAddress,
91
+ methodId,
92
+ asset,
93
+ walletAccount,
94
+ feeAmount,
95
+ txId,
96
+ nonce,
97
+ gasLimit,
98
+ tipGasPrice: maybeTipGasPrice,
99
+ data,
100
+ date,
101
+ bundleId,
102
+ fromAddress,
103
+ })
104
+
105
+ // Fallback to basic logic that only handles transfers + fee decoding
106
+ const sharedProps = {
107
+ confirmations,
108
+ date,
109
+ feeAmount,
110
+ feeCoinName: asset.feeAsset.name,
111
+ selfSend,
112
+ from: [],
113
+ to,
114
+ txId,
115
+ data: {
116
+ gasLimit,
117
+ replacedTxId,
118
+ nonce,
119
+ ...(maybeTipGasPrice ? { tipGasPrice: maybeTipGasPrice.toBaseString() } : null),
120
+ ...(methodId ? { methodId } : null),
121
+ ...(bundleId ? { bundleId } : null),
122
+ ...(data ? { data: bufferToHex(data) } : null),
123
+ },
124
+ }
125
+
126
+ const receiveWalletAccount = await findOtherPortfolioReceiveWalletAccount({
127
+ amount,
128
+ asset,
129
+ assetClientInterface,
130
+ methodId,
131
+ selfSend,
132
+ to,
133
+ txToAddress,
134
+ walletAccount,
135
+ })
136
+
137
+ const receiveSideEffects = receiveWalletAccount
138
+ ? getOtherPortfolioReceiveEffects({
139
+ amount,
140
+ asset,
141
+ confirmations,
142
+ date,
143
+ fromAddress,
144
+ gasLimit,
145
+ methodId,
146
+ maybeTipGasPrice,
147
+ nonce,
148
+ receiveWalletAccount,
149
+ txId,
150
+ data,
151
+ })
152
+ : []
153
+
154
+ const optimisticTxLogEffects = [
155
+ {
156
+ assetName: asset.name,
157
+ walletAccount,
158
+ txs: [
159
+ {
160
+ ...sharedProps,
161
+ data: { ...sharedProps.data, rawCoinAmount: amount.abs().toBaseString() },
162
+ coinAmount: selfSend ? asset.currency.ZERO : amount.abs().negate(),
163
+ coinName: asset.name,
164
+ currencies: {
165
+ [asset.name]: asset.currency,
166
+ [asset.feeAsset.name]: asset.feeAsset.currency,
167
+ },
168
+ },
169
+ ],
170
+ },
171
+ isEthereumLikeToken(asset) && {
172
+ assetName: baseAsset.name,
173
+ walletAccount,
174
+ txs: [
175
+ {
176
+ ...sharedProps,
177
+ data: { ...sharedProps.data, rawCoinAmount: txValue.abs().toBaseString() },
178
+ coinAmount: selfSend ? baseAsset.currency.ZERO : txValue.abs().negate(),
179
+ coinName: baseAsset.name,
180
+ currencies: {
181
+ [baseAsset.name]: baseAsset.currency,
182
+ [asset.feeAsset.name]: asset.feeAsset.currency,
183
+ },
184
+ tokens: [asset.name],
185
+ },
186
+ ],
187
+ },
188
+ ...methodOptimisticSideEffects,
189
+ ...receiveSideEffects,
190
+ ].filter(Boolean)
191
+
192
+ return { optimisticTxLogEffects, nonce }
193
+ }
194
+
195
+ const findOtherPortfolioReceiveWalletAccount = async ({
196
+ amount,
197
+ asset,
198
+ assetClientInterface,
199
+ methodId,
200
+ selfSend,
201
+ to,
202
+ txToAddress,
203
+ walletAccount,
204
+ }) => {
205
+ // Exclude cases where another-portfolio receive logs would be misleading:
206
+ // history-backed assets, self-sends, incomplete/zero-value transfers, and
207
+ // token approvals/swaps/router calls that are not plain ERC-20 transfers.
208
+ if (!asset.baseAsset.api.features.noHistory || selfSend || !to || amount.isZero) return
209
+
210
+ if (isEthereumLikeToken(asset)) {
211
+ const isPlainTransfer =
212
+ txToAddress?.toLowerCase() === asset.contract.address.toLowerCase() &&
213
+ methodId === asset.contract.transfer.methodId
214
+ if (!isPlainTransfer) return
215
+ }
216
+
217
+ try {
218
+ return await getWalletAccountForReceiveAddress({
219
+ assetClientInterface,
220
+ baseAsset: asset.baseAsset,
221
+ sendingWalletAccount: walletAccount,
222
+ to,
223
+ })
224
+ } catch {}
225
+ }
226
+
227
+ const getOtherPortfolioReceiveEffects = ({
228
+ amount,
229
+ asset,
230
+ confirmations,
231
+ date,
232
+ fromAddress,
233
+ gasLimit,
234
+ methodId,
235
+ maybeTipGasPrice,
236
+ nonce,
237
+ receiveWalletAccount,
238
+ txId,
239
+ data,
240
+ }) => {
241
+ const receiveProps = {
242
+ confirmations,
243
+ date,
244
+ selfSend: false,
245
+ from: [fromAddress],
246
+ txId,
247
+ data: {
248
+ gasLimit,
249
+ nonce,
250
+ ...(maybeTipGasPrice ? { tipGasPrice: maybeTipGasPrice.toBaseString() } : null),
251
+ ...(methodId ? { methodId } : null),
252
+ ...(data ? { data: bufferToHex(data) } : null),
253
+ },
254
+ }
255
+
256
+ return [
257
+ {
258
+ assetName: asset.name,
259
+ walletAccount: receiveWalletAccount,
260
+ txs: [
261
+ {
262
+ ...receiveProps,
263
+ coinAmount: amount.abs(),
264
+ coinName: asset.name,
265
+ currencies: {
266
+ [asset.name]: asset.currency,
267
+ [asset.feeAsset.name]: asset.feeAsset.currency,
268
+ },
269
+ },
270
+ ],
271
+ },
272
+ ]
273
+ }
274
+
275
+ const getWalletAccountForReceiveAddress = async ({
276
+ assetClientInterface,
277
+ baseAsset,
278
+ sendingWalletAccount,
279
+ to,
280
+ }) => {
281
+ const normalizedTo = to.toLowerCase()
282
+ const walletAccounts = await assetClientInterface.getWalletAccounts({ assetName: baseAsset.name })
283
+
284
+ for (const walletAccount of walletAccounts) {
285
+ if (walletAccount === sendingWalletAccount) continue
286
+
287
+ const receiveAddresses = await assetClientInterface.getReceiveAddresses({
288
+ assetName: baseAsset.name,
289
+ walletAccount,
290
+ useCache: true,
291
+ })
292
+
293
+ if (receiveAddresses.some((address) => String(address).toLowerCase() === normalizedTo)) {
294
+ return walletAccount
295
+ }
296
+ }
297
+ }
298
+
299
+ const operationTxLogSideEffects = async ({
300
+ txToAddress,
301
+ methodId,
302
+ asset,
303
+ walletAccount,
304
+ feeAmount,
305
+ txId,
306
+ nonce,
307
+ gasLimit,
308
+ tipGasPrice,
309
+ data,
310
+ date,
311
+ bundleId,
312
+ fromAddress,
313
+ }) => {
314
+ // Matic Delegate
315
+ if (
316
+ txToAddress?.toLowerCase() ===
317
+ MaticStakingApi.EVERSTAKE_VALIDATOR_CONTRACT_ADDR.toLowerCase() &&
318
+ methodId === DELEGATE
319
+ ) {
320
+ return maticDelegateOptimisticSideEffectTxLogs({
321
+ asset,
322
+ walletAccount,
323
+ feeAmount,
324
+ delegateTxId: txId,
325
+ nonce,
326
+ estimatedDelegateGasLimit: gasLimit,
327
+ tipGasPrice,
328
+ transactionData: data,
329
+ date,
330
+ bundleId,
331
+ })
332
+ }
333
+
334
+ // Ethereum Undelegate
335
+ if (
336
+ isEthereumStakingPoolContract(txToAddress) &&
337
+ methodId === EthereumStaking.METHODS_IDS.UNSTAKE_PENDING
338
+ ) {
339
+ return ethereumUnstakePendingOptimisticSideEffectTxLogs({
340
+ asset,
341
+ walletAccount,
342
+ feeAmount,
343
+ unstakePendingTxId: txId,
344
+ nonce,
345
+ estimatedUnstakePendingGasLimit: gasLimit,
346
+ tipGasPrice,
347
+ transactionData: data,
348
+ txToAddress,
349
+ date,
350
+ bundleId,
351
+ })
352
+ }
353
+
354
+ // Ethereum Undelegate (unstake from active validator)
355
+ if (
356
+ isEthereumStakingPoolContract(txToAddress) &&
357
+ methodId === EthereumStaking.METHODS_IDS.UNSTAKE
358
+ ) {
359
+ return ethereumUnstakeOptimisticSideEffectTxLogs({
360
+ asset,
361
+ walletAccount,
362
+ feeAmount,
363
+ unstakeTxId: txId,
364
+ nonce,
365
+ estimatedUnstakeGasLimit: gasLimit,
366
+ tipGasPrice,
367
+ transactionData: data,
368
+ txToAddress,
369
+ fromAddress,
370
+ date,
371
+ bundleId,
372
+ })
373
+ }
374
+
375
+ // Add other officially supported operations here
376
+ // Then can fallback to simulation results (TODO)
377
+ // Then finally to the default basic logic
378
+ return []
379
+ }
@@ -1,3 +1,5 @@
1
- export * from './ethereum-monitor'
2
- export * from './ethereum-no-history-monitor'
3
- export { ClarityMonitor } from './clarity-monitor'
1
+ export { EthereumMonitor } from './ethereum-monitor.js'
2
+ export { EthereumNoHistoryMonitor } from './ethereum-no-history-monitor.js'
3
+ export { ClarityMonitor } from './clarity-monitor.js'
4
+ export { ClarityTruncatedHistoryMonitor } from './clarity-truncated-history-monitor.js'
5
+ export { getOptimisticTxLogEffects } from './get-optimistic-txlog-effects.js'
@@ -1,16 +1,21 @@
1
- import getSenderNonceKey from './get-sender-nonce-key'
2
- import getFeeAmount from './get-fee-amount'
1
+ import getFeeAmount from './get-fee-amount.js'
2
+ import getSenderNonceKey from './get-sender-nonce-key.js'
3
3
 
4
4
  export default function checkPendingTransactions({
5
5
  pendingTransactionsGroupedByAddressAndNonce = {},
6
6
  pendingTransactionsToCheck = {},
7
+ recentPendingTransactions = {},
7
8
  txlist,
8
9
  ourWalletAddress,
9
10
  asset,
10
11
  }) {
11
12
  for (const tx of txlist) {
12
- // eslint-disable-next-line @exodus/mutable/no-param-reassign-prop-only
13
- delete pendingTransactionsToCheck[tx.txId || tx.hash] // Found this transaction, do not mark it as dropped
13
+ const txId = tx.txId || tx.hash
14
+ // Found this transaction on the server, do not mark it as dropped or re-add it
15
+ // eslint-disable-next-line @exodus/hydra/no-param-reassign-prop-only
16
+ delete pendingTransactionsToCheck[txId]
17
+ // eslint-disable-next-line @exodus/hydra/no-param-reassign-prop-only
18
+ delete recentPendingTransactions[txId]
14
19
 
15
20
  // check if this TX replaces another pending TX
16
21
  const replacedTx =
@@ -29,5 +34,9 @@ export default function checkPendingTransactions({
29
34
  }
30
35
  }
31
36
 
32
- return { pendingTransactionsGroupedByAddressAndNonce, pendingTransactionsToCheck }
37
+ return {
38
+ pendingTransactionsGroupedByAddressAndNonce,
39
+ pendingTransactionsToCheck,
40
+ recentPendingTransactions,
41
+ }
33
42
  }
@@ -8,5 +8,5 @@ export function excludeUnchangedTokenBalances(currentTokenBalances, newTokenBala
8
8
  }
9
9
 
10
10
  return tokenBalancesAcc
11
- }, {})
11
+ }, Object.create(null))
12
12
  }
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Maps Clarity `accountInfo` rows (GET .../transactions?truncated=true) into fields aligned
3
+ * with Ethereum-like account state: `balance`, `tokenBalances`, `nonce` (plus base units
4
+ * for amounts via `currency.baseUnit`).
5
+ * `account_balance` source rows are stored in `balance`/`tokenBalances`; truncated source rows
6
+ * are stored in `truncatedAccountState`.
7
+ *
8
+ * NOTE: All `row.value` payloads arrive over the wire as decimal strings (e.g. `"0"`,
9
+ * `"20191469428776270391810"`). They're parsed with `currency.baseUnit(...)` for amounts
10
+ * and `Number(...)` for nonce. `null`, `undefined`, and empty-string `""` all mean "row
11
+ * carries no value" and the row is skipped — `currency.baseUnit("")` throws and `Number("")`
12
+ * returns 0, both of which would silently corrupt accountState.
13
+ */
14
+ const ACCOUNT_BALANCE = 'account_balance'
15
+ const TRUNCATED = 'truncated'
16
+
17
+ const hasValue = (value) => value !== null && value !== undefined && value !== ''
18
+
19
+ const warnUnknownSource = (row) => {
20
+ console.warn('Unknown accountInfo source', row)
21
+ }
22
+
23
+ export const extractBalanceFromClarityAccountInfo = ({ accountInfo, asset, tokensByAddress }) => {
24
+ if (!accountInfo || accountInfo.length === 0) return {}
25
+
26
+ const sorted = [...accountInfo].sort((a, b) => (b.blockNumber || 0) - (a.blockNumber || 0))
27
+
28
+ const result = {}
29
+ const seenAssetIds = new Set()
30
+ const tokenBalances = Object.create(null)
31
+ const truncatedTokenBalances = Object.create(null)
32
+ let truncatedBalance
33
+
34
+ for (const row of sorted) {
35
+ switch (row.type) {
36
+ case 'balance':
37
+ if (hasValue(row.value)) {
38
+ const balance = asset.currency.baseUnit(row.value)
39
+ if (row.source === ACCOUNT_BALANCE) {
40
+ result.balance = balance
41
+ } else if (row.source === TRUNCATED) {
42
+ truncatedBalance = balance
43
+ } else {
44
+ warnUnknownSource(row)
45
+ }
46
+ }
47
+
48
+ break
49
+
50
+ case 'nonce': {
51
+ // Nonce collapses regardless of source — both `account_balance` and
52
+ // `truncated` rows represent the latest chain-known nonce; max wins
53
+ // for safety against out-of-order rows.
54
+ if (!hasValue(row.value)) break
55
+ const nonce = Number(row.value)
56
+ if (!Number.isFinite(nonce) || nonce < 0) break
57
+
58
+ result.nonce = Math.max(result.nonce ?? 0, nonce)
59
+
60
+ break
61
+ }
62
+
63
+ case 'token': {
64
+ const assetId = row.assetId?.toLowerCase()
65
+
66
+ if (!assetId || seenAssetIds.has(assetId) || !hasValue(row.value)) break
67
+ const token = tokensByAddress.get(assetId)
68
+ if (!token) break
69
+
70
+ const balance = token.currency.baseUnit(row.value)
71
+ if (row.source === ACCOUNT_BALANCE) {
72
+ tokenBalances[token.name] = balance
73
+ } else if (row.source === TRUNCATED) {
74
+ truncatedTokenBalances[token.name] = balance
75
+ } else {
76
+ warnUnknownSource(row)
77
+ }
78
+
79
+ seenAssetIds.add(assetId)
80
+ break
81
+ }
82
+
83
+ default:
84
+ break
85
+ }
86
+ }
87
+
88
+ if (Object.keys(tokenBalances).length > 0) {
89
+ result.tokenBalances = tokenBalances
90
+ }
91
+
92
+ const hasTruncatedTokenBalances = Object.keys(truncatedTokenBalances).length > 0
93
+ if (truncatedBalance || hasTruncatedTokenBalances) {
94
+ const truncatedAccountState = Object.create(null)
95
+
96
+ if (truncatedBalance) {
97
+ truncatedAccountState.balance = truncatedBalance
98
+ }
99
+
100
+ if (hasTruncatedTokenBalances) {
101
+ truncatedAccountState.tokenBalances = truncatedTokenBalances
102
+ }
103
+
104
+ result.truncatedAccountState = truncatedAccountState
105
+ }
106
+
107
+ return result
108
+ }
@@ -0,0 +1,44 @@
1
+ export const getWalletUpdates = (ourWalletAddress, walletChanges) => {
2
+ return walletChanges.reduce(
3
+ (acc, walletChange) => {
4
+ const { wallet: walletAddress, type, from, to, contract: contractAddress } = walletChange
5
+
6
+ if (walletAddress !== ourWalletAddress) {
7
+ return acc
8
+ }
9
+
10
+ switch (type) {
11
+ case 'balance':
12
+ acc.baseBalanceUpdate = {
13
+ from,
14
+ to,
15
+ }
16
+ break
17
+ case 'nonce':
18
+ acc.nonceUpdate = {
19
+ from,
20
+ to,
21
+ }
22
+ break
23
+ case 'token':
24
+ acc.tokenBalancesUpdate = {
25
+ ...acc.tokenBalancesUpdate,
26
+ [contractAddress]: {
27
+ from,
28
+ to,
29
+ },
30
+ }
31
+ break
32
+ default:
33
+ break
34
+ }
35
+
36
+ return acc
37
+ },
38
+ {
39
+ baseBalanceUpdate: undefined,
40
+ nonceUpdate: undefined,
41
+ tokenBalancesUpdate: Object.create(null),
42
+ }
43
+ )
44
+ }
@@ -0,0 +1,47 @@
1
+ import assert from 'minimalistic-assert'
2
+
3
+ import { fromHexToString } from '../../number-utils.js'
4
+
5
+ export const getBatchedRpcBalances = async ({
6
+ baseAsset,
7
+ tokens,
8
+ ourWalletAddress,
9
+ rpcBalanceAssetNames,
10
+ }) => {
11
+ assert(baseAsset, 'expected baseAsset')
12
+ assert(Array.isArray(tokens), 'expected array tokens')
13
+ assert(ourWalletAddress, 'expected ourWalletAddress')
14
+
15
+ const batch = (assetName) =>
16
+ !Array.isArray(rpcBalanceAssetNames) || rpcBalanceAssetNames.includes(assetName)
17
+
18
+ const isBaseAssetBatched = batch(baseAsset.name)
19
+
20
+ const rpcRequestAccumulator = tokens
21
+ .filter(({ name, contract }) => batch(name) && contract?.address)
22
+ .reduce(
23
+ (acc, { contract: { address: tokenAddress } }) =>
24
+ acc.balanceOfRequest(ourWalletAddress, tokenAddress),
25
+ isBaseAssetBatched
26
+ ? baseAsset.createRpcRequestAccumulator().getBalanceRequest(ourWalletAddress)
27
+ : baseAsset.createRpcRequestAccumulator()
28
+ )
29
+
30
+ if (rpcRequestAccumulator.length() === 0) return Object.create(null)
31
+
32
+ const results = await rpcRequestAccumulator.flush(baseAsset.server)
33
+
34
+ return {
35
+ ...(isBaseAssetBatched && { [baseAsset.name]: fromHexToString(results[0]) }),
36
+ ...Object.fromEntries(
37
+ tokens
38
+ .filter(({ name, contract }) => batch(name) && contract?.address)
39
+ .map(({ name, contract: { address: tokenAddress } }, i) => {
40
+ const tokenBalance = results[i + (isBaseAssetBatched ? 1 : 0)]?.confirmed?.[tokenAddress]
41
+
42
+ assert(tokenBalance != null, `missing token balance for ${name} (${tokenAddress})`)
43
+ return [name, tokenBalance]
44
+ })
45
+ ),
46
+ }
47
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Checks the current blacklist status for an address and returns the state to use.
3
+ * Returns the new state if changed, undefined if unchanged.
4
+ * On error, resets to null (unknown) to avoid showing a stale blacklist warning —
5
+ * preferring a false negative over a false positive.
6
+ *
7
+ * @param {Object} params
8
+ * @param {Function} [params.getBlackListStatus] - Asset blacklist status checker
9
+ * @param {string} params.address - Wallet address to check
10
+ * @param {boolean|null} [params.currentIsBlacklisted] - Current blacklist flag from accountState
11
+ * @param {Object} [params.logger] - Optional logger for warnings
12
+ * @returns {Promise<boolean|null|undefined>} The new blacklist flag, or undefined if unchanged
13
+ */
14
+ export async function getCurrentBlackListStatus({
15
+ getBlackListStatus,
16
+ address,
17
+ currentIsBlacklisted,
18
+ logger,
19
+ }) {
20
+ if (typeof getBlackListStatus !== 'function') return
21
+
22
+ try {
23
+ const { isBlacklisted } = await getBlackListStatus({ address })
24
+
25
+ if (currentIsBlacklisted !== isBlacklisted) {
26
+ return isBlacklisted
27
+ }
28
+ } catch (error) {
29
+ if (logger) {
30
+ logger.warn('Failed to check blacklist status:', error)
31
+ }
32
+
33
+ // Reset to null (unknown) on error — only a confirmed check should show the blacklist warning.
34
+ return null
35
+ }
36
+ }
37
+
38
+ export default getCurrentBlackListStatus