@exodus/ethereum-api 8.76.1 → 8.76.3

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 CHANGED
@@ -3,6 +3,24 @@
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.76.3](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.76.2...@exodus/ethereum-api@8.76.3) (2026-06-01)
7
+
8
+ **Note:** Version bump only for package @exodus/ethereum-api
9
+
10
+
11
+
12
+
13
+
14
+ ## [8.76.2](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.76.1...@exodus/ethereum-api@8.76.2) (2026-05-27)
15
+
16
+
17
+ ### Bug Fixes
18
+
19
+
20
+ * fix(ethereum-api): persist rawCoinAmount in tx-log monitor readers (#8131)
21
+
22
+
23
+
6
24
  ## [8.76.1](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.76.0...@exodus/ethereum-api@8.76.1) (2026-05-26)
7
25
 
8
26
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/ethereum-api",
3
- "version": "8.76.1",
3
+ "version": "8.76.3",
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",
@@ -70,5 +70,5 @@
70
70
  "type": "git",
71
71
  "url": "git+https://github.com/ExodusMovement/assets.git"
72
72
  },
73
- "gitHead": "50c5568a3fb1663b61344b18a044ebc69bfd45f0"
73
+ "gitHead": "aac7087b928f14da94a1a071b5abaf0983136c2a"
74
74
  }
@@ -5,10 +5,9 @@ import assert from 'minimalistic-assert'
5
5
  import ms from 'ms'
6
6
 
7
7
  import { EVM_ERROR_REASONS, withErrorReason } from './error-wrapper.js'
8
- import { createEvmServer, createWsGateway, ValidMonitorTypes } from './exodus-eth-server/index.js'
8
+ import { createEvmServer, ValidMonitorTypes } from './exodus-eth-server/index.js'
9
9
  import { createEthereumHooks } from './hooks/index.js'
10
10
  import { ClarityMonitor } from './tx-log/clarity-monitor.js'
11
- import { ClarityMonitorV2 } from './tx-log/clarity-monitor-v2.js'
12
11
  import { ClarityTruncatedHistoryMonitor } from './tx-log/clarity-truncated-history-monitor.js'
13
12
  import { EthereumMonitor } from './tx-log/ethereum-monitor.js'
14
13
  import { EthereumNoHistoryMonitor } from './tx-log/ethereum-no-history-monitor.js'
@@ -284,6 +283,9 @@ export const createHistoryMonitorFactory = ({
284
283
  return (args) => {
285
284
  let monitor
286
285
  switch (monitorType) {
286
+ case 'clarity-v3':
287
+ console.log('clarity-v3 is no longer supported, falling back to clarity-v2')
288
+ // eslint-disable-next-line no-fallthrough
287
289
  case 'clarity':
288
290
  case 'clarity-v2':
289
291
  monitor = new ClarityMonitor({
@@ -306,18 +308,6 @@ export const createHistoryMonitorFactory = ({
306
308
  ...args,
307
309
  })
308
310
  break
309
- case 'clarity-v3':
310
- monitor = new ClarityMonitorV2({
311
- assetClientInterface,
312
- interval: ms(monitorInterval || '5m'),
313
- server,
314
- rpcBalanceAssetNames,
315
- wsGatewayClient: createWsGateway({ uri: wsGatewayUri }),
316
- eip7702Supported,
317
- getBlackListStatus,
318
- ...args,
319
- })
320
- break
321
311
  case 'no-history':
322
312
  monitor = new EthereumNoHistoryMonitor({
323
313
  assetClientInterface,
package/src/tx-create.js CHANGED
@@ -257,14 +257,29 @@ const createBumpUnsignedTx = async ({
257
257
  }
258
258
  }
259
259
 
260
- const toAddress = (replacedTokenTx || replacedTx).to
260
+ const sourceTx = replacedTokenTx || replacedTx
261
+ const toAddress = sourceTx.to
262
+
263
+ // For self-sends `coinAmount` is `0` (correct for balance accounting, the
264
+ // wallet's net delta really is zero), so we can't reconstruct the on-chain
265
+ // value from it. Recover from `data.rawCoinAmount`, which the tx-log
266
+ // writers persist alongside `coinAmount` for exactly this purpose.
267
+ if (sourceTx.selfSend && !sourceTx.data?.rawCoinAmount) {
268
+ throw new Error(
269
+ `Cannot bump self-send transaction ${bumpTxId}: original transfer amount is unrecoverable from the tx log (data.rawCoinAmount missing). Re-broadcast the send with a higher fee instead.`
270
+ )
271
+ }
261
272
 
262
- const amount = (replacedTokenTx || replacedTx).coinAmount.negate()
273
+ const amount = sourceTx.data?.rawCoinAmount
274
+ ? asset.currency.baseUnit(sourceTx.data.rawCoinAmount)
275
+ : sourceTx.coinAmount.negate()
263
276
 
264
277
  const txValue = assertValidTxValue({
265
278
  asset,
266
279
  amount,
267
- txValue: replacedTx.coinAmount.negate(),
280
+ txValue: replacedTx.data?.rawCoinAmount
281
+ ? baseAsset.currency.baseUnit(replacedTx.data.rawCoinAmount)
282
+ : replacedTx.coinAmount.negate(),
268
283
  })
269
284
 
270
285
  const isDuplex = Boolean(replacedTokenTx && txValue.gt(baseAsset.currency.ZERO))
@@ -2,6 +2,7 @@ import lodash from 'lodash'
2
2
 
3
3
  import { getWalletUpdates } from '../monitor-utils/get-balance-updates.js'
4
4
  import getFeeAmount from '../monitor-utils/get-fee-amount.js'
5
+ import getRawTransferAmount from '../monitor-utils/get-raw-transfer-amount.js'
5
6
  import getTransfersByTokenName from '../monitor-utils/get-transfers-by-token-name.js'
6
7
  import getValueOfTransfers from '../monitor-utils/get-value-of-transfers.js'
7
8
  import isConfirmedServerTx from '../monitor-utils/is-confirmed-server-tx.js'
@@ -62,6 +63,18 @@ export default function getLogItemsFromServerTx({
62
63
 
63
64
  if (shouldAttachTx) {
64
65
  const coinAmount = getValueOfTransfers(ourWalletAddress, asset, ethereumTransfers)
66
+ // `rawCoinAmount` mirrors the field written by `getOptimisticTxLogEffects`
67
+ // and is consumed by the bump path (`tx-create.js`) to recover the
68
+ // user-signed on-chain `value` when accelerating a self-send (where
69
+ // `coinAmount` is zero). For the base-asset entry the raw amount is the
70
+ // top-level `value` field of the tx itself, not the sum of internal
71
+ // transfers — that's what the user signed and what we must restore on
72
+ // bump.
73
+ const rawCoinAmount = getRawTransferAmount({
74
+ ourWalletAddress,
75
+ asset,
76
+ transfers: [serverTx],
77
+ }).toBaseString()
65
78
  const selfSend = isSelfSendTx({
66
79
  coinAmount,
67
80
  ourWalletWasSender,
@@ -90,6 +103,7 @@ export default function getLogItemsFromServerTx({
90
103
  data,
91
104
  nonce,
92
105
  gasLimit,
106
+ rawCoinAmount,
93
107
  balanceChange: baseBalanceUpdate,
94
108
  nonceChange: nonceUpdate,
95
109
  ...methodId,
@@ -130,6 +144,14 @@ export default function getLogItemsFromServerTx({
130
144
 
131
145
  const tokenTransferToAddress = tryFindExternalRecipient(tokenTransfers, ourWalletAddress)
132
146
  const coinAmount = getValueOfTransfers(ourWalletAddress, token, tokenTransfers)
147
+ // See base-entry comment above; for token entries the raw amount is the
148
+ // absolute total tokens our wallet moved in this tx (sum of outgoing, or
149
+ // incoming for receive-only entries).
150
+ const rawCoinAmount = getRawTransferAmount({
151
+ ourWalletAddress,
152
+ asset: token,
153
+ transfers: tokenTransfers,
154
+ }).toBaseString()
133
155
 
134
156
  const tokenFromAddresses = lodash.uniq(
135
157
  tokenTransfers.filter(({ to }) => to === ourWalletAddress).map(({ from }) => from)
@@ -158,6 +180,7 @@ export default function getLogItemsFromServerTx({
158
180
  data,
159
181
  nonce,
160
182
  gasLimit,
183
+ rawCoinAmount,
161
184
  balanceChange,
162
185
  ...methodId,
163
186
  ...(isFiniteInteger(blockNumber) ? { blockNumber } : null),
@@ -158,6 +158,7 @@ export const getOptimisticTxLogEffects = async ({
158
158
  txs: [
159
159
  {
160
160
  ...sharedProps,
161
+ data: { ...sharedProps.data, rawCoinAmount: amount.abs().toBaseString() },
161
162
  coinAmount: selfSend ? asset.currency.ZERO : amount.abs().negate(),
162
163
  coinName: asset.name,
163
164
  currencies: {
@@ -173,6 +174,7 @@ export const getOptimisticTxLogEffects = async ({
173
174
  txs: [
174
175
  {
175
176
  ...sharedProps,
177
+ data: { ...sharedProps.data, rawCoinAmount: txValue.abs().toBaseString() },
176
178
  coinAmount: selfSend ? baseAsset.currency.ZERO : txValue.abs().negate(),
177
179
  coinName: baseAsset.name,
178
180
  currencies: {
@@ -2,6 +2,7 @@ import lodash from 'lodash'
2
2
 
3
3
  import getFeeAmount from './get-fee-amount.js'
4
4
  import getNamesOfTokensTransferredByServerTx from './get-names-of-tokens-transferred-by-server-tx.js'
5
+ import getRawTransferAmount from './get-raw-transfer-amount.js'
5
6
  import getTransfersByTokenName from './get-transfers-by-token-name.js'
6
7
  import getValueOfTransfers from './get-value-of-transfers.js'
7
8
  import isConfirmedServerTx from './is-confirmed-server-tx.js'
@@ -50,6 +51,18 @@ export default function getLogItemsFromServerTx({
50
51
 
51
52
  if (sendingTransferPresent || receivingTransferPresent || nftTransferPresent) {
52
53
  const coinAmount = getValueOfTransfers(ourWalletAddress, asset, ethereumTransfers)
54
+ // `rawCoinAmount` mirrors the field written by `getOptimisticTxLogEffects`
55
+ // and is consumed by the bump path (`tx-create.js`) to recover the
56
+ // user-signed on-chain `value` when accelerating a self-send (where
57
+ // `coinAmount` is zero). For the base-asset entry the raw amount is the
58
+ // top-level `value` field of the tx itself, not the sum of internal
59
+ // transfers — that's what the user signed and what we must restore on
60
+ // bump.
61
+ const rawCoinAmount = getRawTransferAmount({
62
+ ourWalletAddress,
63
+ asset,
64
+ transfers: [serverTx],
65
+ }).toBaseString()
53
66
  const selfSend = isSelfSendTx({
54
67
  coinAmount,
55
68
  ourWalletWasSender,
@@ -78,6 +91,7 @@ export default function getLogItemsFromServerTx({
78
91
  data,
79
92
  nonce,
80
93
  gasLimit,
94
+ rawCoinAmount,
81
95
  ...methodId,
82
96
  ...(sent?.length > 0 ? { sent } : undefined),
83
97
  },
@@ -108,11 +122,25 @@ export default function getLogItemsFromServerTx({
108
122
 
109
123
  const token = assets[tokenName]
110
124
  const tokenTransferToAddress = tryFindExternalRecipient(tokenTransfers, ourWalletAddress)
111
- const coinAmount = getValueOfTransfers(
125
+ // Transfers from this reader's server source are tagged with
126
+ // `events: true` once the tx is included in a block (event-log derived)
127
+ // and `events: false` while still pending (tx-data derived). Only one
128
+ // flavor is real for a given lifecycle state; filtering avoids
129
+ // double-counting if both ever coexist for the same transfer. Both
130
+ // `coinAmount` and `rawCoinAmount` must see the same set so they stay
131
+ // consistent.
132
+ const filteredTokenTransfers = lodash.filter(tokenTransfers, {
133
+ events: confirmations > 0,
134
+ })
135
+ const coinAmount = getValueOfTransfers(ourWalletAddress, token, filteredTokenTransfers)
136
+ // See base-entry comment above; for token entries the raw amount is the
137
+ // absolute total tokens our wallet moved in this tx (sum of outgoing, or
138
+ // incoming for receive-only entries).
139
+ const rawCoinAmount = getRawTransferAmount({
112
140
  ourWalletAddress,
113
- token,
114
- lodash.filter(tokenTransfers, { events: confirmations > 0 })
115
- )
141
+ asset: token,
142
+ transfers: filteredTokenTransfers,
143
+ }).toBaseString()
116
144
  const tokenFromAddresses = lodash.uniq(
117
145
  tokenTransfers.filter(({ to }) => to === ourWalletAddress).map(({ from }) => from)
118
146
  )
@@ -136,7 +164,7 @@ export default function getLogItemsFromServerTx({
136
164
  ...logItemCommonProperties,
137
165
  coinAmount,
138
166
  coinName: tokenName,
139
- data: { data, nonce, gasLimit, ...methodId },
167
+ data: { data, nonce, gasLimit, rawCoinAmount, ...methodId },
140
168
  ...(isConsideredSent
141
169
  ? { from: [], to: tokenTransferToAddress, feeAmount, feeCoinName: asset.feeAsset.name }
142
170
  : { from: tokenFromAddresses }),
@@ -0,0 +1,29 @@
1
+ // Returns the absolute raw transferred amount for `ourWalletAddress`, in
2
+ // `asset` base units, as a NumberUnit. Unlike `getValueOfTransfers` (which
3
+ // returns the signed net delta), this preserves the raw transfer value even
4
+ // when sender and receiver net to zero — e.g. self-sends. Used by tx-log
5
+ // writers to persist `data.rawCoinAmount` so the bump path can reconstruct
6
+ // the original on-chain value when accelerating a self-send.
7
+ //
8
+ // Each transfer is expected to have the shape `{ from, to, value }`. For the
9
+ // base-asset entry, pass `[serverTx]` (the tx itself has the same shape at
10
+ // its top level); for token entries, pass the array of token transfers for
11
+ // that token.
12
+ //
13
+ // Outgoing transfers (from === ourWalletAddress) take precedence: when our
14
+ // wallet sent anything, the raw amount is the sum of what we sent. We fall
15
+ // back to incoming so receiver-only entries still report a non-zero raw
16
+ // amount for consistency, even though the bump path only consults it on the
17
+ // sender side.
18
+
19
+ export default function getRawTransferAmount({ ourWalletAddress, asset, transfers }) {
20
+ const outgoing = transfers
21
+ .filter(({ from }) => from === ourWalletAddress)
22
+ .reduce((sum, { value }) => sum.add(asset.currency.baseUnit(value)), asset.currency.ZERO)
23
+
24
+ if (!outgoing.isZero) return outgoing
25
+
26
+ return transfers
27
+ .filter(({ to }) => to === ourWalletAddress)
28
+ .reduce((sum, { value }) => sum.add(asset.currency.baseUnit(value)), asset.currency.ZERO)
29
+ }
@@ -1,637 +0,0 @@
1
- import { BaseMonitor } from '@exodus/asset-lib'
2
- import { getAssetAddresses } from '@exodus/ethereum-lib'
3
- import lodash from 'lodash'
4
- import assert from 'minimalistic-assert'
5
-
6
- import WsGateway from '../exodus-eth-server/ws-gateway.js'
7
- import { executeEthLikeFeeMonitorUpdate } from '../fee-utils.js'
8
- import { fromHexToString } from '../number-utils.js'
9
- import {
10
- filterEffects,
11
- getLogItemsFromServerTx,
12
- normalizeTransactionsResponse,
13
- } from './clarity-utils/index.js'
14
- import {
15
- checkPendingTransactions,
16
- excludeUnchangedTokenBalances,
17
- getAllLogItemsByAsset,
18
- getCurrentBlackListStatus,
19
- getCurrentEIP7702Delegation,
20
- getDeriveDataNeededForTick,
21
- getDeriveTransactionsToCheck,
22
- verifyRpcPendingTxStatusBatch,
23
- } from './monitor-utils/index.js'
24
-
25
- const { isEmpty } = lodash
26
-
27
- export class ClarityMonitorV2 extends BaseMonitor {
28
- #wsClient = null
29
- #walletAccountByAddress = new Map()
30
- #walletAccountInfo = new Map()
31
- #rpcBalanceAssetNames = []
32
- constructor({
33
- server,
34
- wsGatewayClient,
35
- rpcBalanceAssetNames,
36
- eip7702Supported,
37
- getBlackListStatus,
38
- config,
39
- ...args
40
- } = {}) {
41
- super(args)
42
- assert(wsGatewayClient instanceof WsGateway, 'expected WsGateway wsGatewayClient')
43
-
44
- this.config = { GAS_PRICE_FROM_WEBSOCKET: true, ...config }
45
- this.server = server
46
- this.#wsClient = wsGatewayClient
47
- this.#rpcBalanceAssetNames = rpcBalanceAssetNames
48
- this.eip7702Supported = eip7702Supported
49
- this.getBlackListStatus = getBlackListStatus
50
- this.getAllLogItemsByAsset = getAllLogItemsByAsset
51
- this.deriveDataNeededForTick = getDeriveDataNeededForTick(this.aci)
52
- this.deriveTransactionsToCheck = getDeriveTransactionsToCheck({
53
- getTxLog: (...args) => this.aci.getTxLog(...args),
54
- })
55
-
56
- this.addHook('before-start', (...args) => this.beforeStart(...args))
57
- this.addHook('after-stop', (...args) => this.afterStop(...args))
58
- }
59
-
60
- setServer(config) {
61
- const uri = config?.server || this.server.defaultUri
62
-
63
- this.#wsClient.on('connected', () => this.subscribeAllWalletAccounts())
64
- this.#wsClient.start()
65
-
66
- if (uri === this.server.uri) {
67
- return
68
- }
69
-
70
- this.server.setURI(uri)
71
- if (this.config.GAS_PRICE_FROM_WEBSOCKET) {
72
- this.server.connectFee()
73
- }
74
- }
75
-
76
- async deriveData({ assetName, walletAccount, tokens }) {
77
- const { ourWalletAddress, currentAccountState } = await this.deriveDataNeededForTick({
78
- assetName,
79
- walletAccount,
80
- })
81
- const transactionsToCheck = await this.deriveTransactionsToCheck({
82
- assetName,
83
- walletAccount,
84
- tokens,
85
- ourWalletAddress,
86
- })
87
-
88
- return {
89
- ourWalletAddress,
90
- currentAccountState,
91
- ...transactionsToCheck,
92
- }
93
- }
94
-
95
- // eslint-disable-next-line no-undef
96
- async checkPendingTransactions(params) {
97
- const { pendingTransactionsToCheck, pendingTransactionsGroupedByAddressAndNonce } =
98
- checkPendingTransactions(params)
99
- const txsToRemove = []
100
- const { walletAccount } = params
101
-
102
- const updateTx = (tx, asset, { error, remove }) => {
103
- if (remove) {
104
- txsToRemove.push({ tx, assetSource: { asset, walletAccount } })
105
- } else {
106
- params.logItemsByAsset[asset].push({
107
- ...tx,
108
- dropped: true,
109
- error,
110
- })
111
- }
112
-
113
- // in case this is an ETH fee tx that has associated ERC20 send txs
114
- const promises = tx.tokens.map(async (assetName) => {
115
- const tokenTxSet = await this.aci.getTxLog({ assetName, walletAccount })
116
- if (remove) {
117
- txsToRemove.push({
118
- tx: tokenTxSet.get(tx.txId),
119
- assetSource: { asset: assetName, walletAccount },
120
- })
121
- } else if (tokenTxSet && tokenTxSet.has(tx.txId)) {
122
- params.logItemsByAsset[assetName].push({
123
- ...tokenTxSet.get(tx.txId),
124
- error,
125
- dropped: true,
126
- })
127
- }
128
- })
129
- return Promise.all(promises)
130
- }
131
-
132
- for (const { tx, assetName, replaced = false } of Object.values(
133
- pendingTransactionsGroupedByAddressAndNonce
134
- )) {
135
- if (replaced) {
136
- await updateTx(tx, assetName, { remove: true })
137
- delete pendingTransactionsToCheck[tx.txId]
138
- }
139
- }
140
-
141
- // Batch verify all pending txs with a single RPC call (skip if refresh)
142
- const txIds = Object.keys(pendingTransactionsToCheck)
143
- const statuses = params.refresh
144
- ? {}
145
- : await verifyRpcPendingTxStatusBatch({
146
- server: this.server,
147
- logger: this.logger,
148
- txIds,
149
- })
150
-
151
- for (const { tx, assetName } of Object.values(pendingTransactionsToCheck)) {
152
- if (params.refresh) {
153
- await updateTx(tx, assetName, { remove: true })
154
- } else {
155
- const txStatus = statuses[tx.txId]
156
- if (txStatus?.status === 'dropped') {
157
- await updateTx(tx, assetName, { error: 'Dropped' })
158
- }
159
- // status === 'confirmed' or 'pending' - tx is fine, wait for Clarity to confirm
160
- }
161
- }
162
-
163
- return { txsToRemove }
164
- }
165
-
166
- async persistSecurityState({ walletAccount, accountState, isBlacklisted, eip7702Delegation }) {
167
- const securityStatePatch = {
168
- ...(isBlacklisted !== undefined && { isBlacklisted }),
169
- ...(eip7702Delegation !== undefined && { eip7702Delegation }),
170
- }
171
-
172
- if (isEmpty(securityStatePatch)) {
173
- return
174
- }
175
-
176
- await this.updateAccountState({
177
- walletAccount,
178
- accountState,
179
- newData: securityStatePatch,
180
- })
181
- }
182
-
183
- async tick({ walletAccount, refresh }) {
184
- await this.subscribeWalletAddresses(walletAccount)
185
-
186
- const walletAccountInfo = this.#walletAccountInfo.get(walletAccount)
187
-
188
- if (!walletAccountInfo) {
189
- return this.logger.warn('walletAccountInfo is empty', { walletAccount })
190
- }
191
-
192
- const { derivedData, tokensByAddress, assets, tokens, assetName } = walletAccountInfo
193
- const { eip7702Delegation, isBlacklisted } = await this.getSecurityAccountState({
194
- derivedData,
195
- walletAccount,
196
- })
197
-
198
- await this.persistSecurityState({
199
- walletAccount,
200
- accountState: derivedData.currentAccountState,
201
- isBlacklisted,
202
- eip7702Delegation,
203
- })
204
-
205
- const accountState = await this.getNewAccountState({
206
- tokens,
207
- currentTokenBalances: derivedData.currentAccountState?.tokenBalances,
208
- ourWalletAddress: derivedData.ourWalletAddress,
209
- })
210
-
211
- const batch = this.aci.createOperationsBatch()
212
- const newData = { ...accountState }
213
- let allTxs = []
214
- let hasNewTxs = false
215
- let historyError
216
-
217
- try {
218
- const response = await this.getHistoryFromServer({ walletAccount, derivedData, refresh })
219
-
220
- ;({ allTxs } = await normalizeTransactionsResponse({
221
- asset: this.asset,
222
- fromAddress: derivedData.ourWalletAddress,
223
- response,
224
- walletAccount,
225
- }))
226
-
227
- hasNewTxs = allTxs.length > 0
228
-
229
- const logItemsByAsset = this.getAllLogItemsByAsset({
230
- getLogItemsFromServerTx,
231
- ourWalletAddress: derivedData.ourWalletAddress,
232
- allTransactionsFromServer: allTxs,
233
- asset: this.asset,
234
- tokensByAddress,
235
- assets,
236
- })
237
-
238
- const { txsToRemove } = await this.checkPendingTransactions({
239
- txlist: allTxs,
240
- walletAccount,
241
- refresh,
242
- logItemsByAsset,
243
- asset: this.asset,
244
- ...derivedData,
245
- })
246
-
247
- this.aci.removeTxLogBatch({
248
- assetName,
249
- walletAccount,
250
- txs: txsToRemove,
251
- batch,
252
- })
253
-
254
- for (const [assetName, txs] of Object.entries(logItemsByAsset)) {
255
- this.aci.updateTxLogAndNotifyBatch({
256
- assetName,
257
- walletAccount,
258
- txs,
259
- refresh,
260
- batch,
261
- })
262
- }
263
-
264
- if (response.cursor) {
265
- newData.clarityCursor = response.cursor
266
- }
267
- } catch (error) {
268
- historyError = error
269
- }
270
-
271
- try {
272
- this.aci.updateAccountStateBatch({
273
- assetName,
274
- walletAccount,
275
- accountState,
276
- newData,
277
- batch,
278
- })
279
-
280
- await this.aci.executeOperationsBatch(batch)
281
- } catch (batchError) {
282
- if (!historyError) throw batchError
283
- this.logger.warn('error persisting account state after history failure', batchError)
284
- }
285
-
286
- if (historyError) {
287
- throw historyError
288
- }
289
-
290
- if (refresh || hasNewTxs) {
291
- const unknownTokenAddresses = this.getUnknownTokenAddresses({
292
- transactions: allTxs,
293
- tokensByAddress,
294
- })
295
- if (unknownTokenAddresses.length > 0) {
296
- this.emit('unknown-tokens', unknownTokenAddresses)
297
- }
298
- }
299
- }
300
-
301
- async processAndFillTransactionsToState({
302
- allTxs,
303
- derivedData,
304
- tokensByAddress,
305
- assets,
306
- tokens,
307
- assetName,
308
- walletAccount,
309
- refresh,
310
- cursor,
311
- }) {
312
- const hasNewTxs = allTxs.length > 0
313
-
314
- const { eip7702Delegation, isBlacklisted } = await this.getSecurityAccountState({
315
- derivedData,
316
- walletAccount,
317
- })
318
-
319
- await this.persistSecurityState({
320
- walletAccount,
321
- accountState: derivedData.currentAccountState,
322
- isBlacklisted,
323
- eip7702Delegation,
324
- })
325
-
326
- const accountState = await this.getNewAccountState({
327
- tokens,
328
- currentTokenBalances: derivedData.currentAccountState?.tokenBalances,
329
- ourWalletAddress: derivedData.ourWalletAddress,
330
- })
331
-
332
- const logItemsByAsset = this.getAllLogItemsByAsset({
333
- getLogItemsFromServerTx,
334
- ourWalletAddress: derivedData.ourWalletAddress,
335
- allTransactionsFromServer: allTxs,
336
- asset: this.asset,
337
- tokensByAddress,
338
- assets,
339
- })
340
-
341
- const { txsToRemove } = await this.checkPendingTransactions({
342
- txlist: allTxs,
343
- walletAccount,
344
- refresh,
345
- logItemsByAsset,
346
- asset: this.asset,
347
- ...derivedData,
348
- })
349
-
350
- const batch = this.aci.createOperationsBatch()
351
-
352
- this.aci.removeTxLogBatch({
353
- assetName,
354
- walletAccount,
355
- txs: txsToRemove,
356
- batch,
357
- })
358
-
359
- for (const [assetName, txs] of Object.entries(logItemsByAsset)) {
360
- this.aci.updateTxLogAndNotifyBatch({
361
- assetName,
362
- walletAccount,
363
- txs,
364
- refresh,
365
- batch,
366
- })
367
- }
368
-
369
- // All updates must go through newData (accountState param is only used for mem merging)
370
- const newData = { ...accountState }
371
-
372
- if (cursor) {
373
- newData.clarityCursor = cursor
374
- }
375
-
376
- this.aci.updateAccountStateBatch({
377
- assetName,
378
- walletAccount,
379
- accountState,
380
- newData,
381
- batch,
382
- })
383
-
384
- await this.aci.executeOperationsBatch(batch)
385
-
386
- if (refresh || hasNewTxs) {
387
- const unknownTokenAddresses = this.getUnknownTokenAddresses({
388
- transactions: allTxs,
389
- tokensByAddress,
390
- })
391
- if (unknownTokenAddresses.length > 0) {
392
- this.emit('unknown-tokens', unknownTokenAddresses)
393
- }
394
- }
395
- }
396
-
397
- async getSecurityAccountState({ derivedData, walletAccount }) {
398
- const shouldCheckBlacklist = this.tickCount[walletAccount] === 0
399
- const eip7702Delegation = await getCurrentEIP7702Delegation({
400
- server: this.server,
401
- address: derivedData.ourWalletAddress,
402
- eip7702Supported: this.eip7702Supported,
403
- currentDelegation: derivedData.currentAccountState?.eip7702Delegation,
404
- logger: this.logger,
405
- })
406
- const isBlacklisted = shouldCheckBlacklist
407
- ? await getCurrentBlackListStatus({
408
- getBlackListStatus: this.getBlackListStatus,
409
- address: derivedData.ourWalletAddress,
410
- currentIsBlacklisted: derivedData.currentAccountState?.isBlacklisted,
411
- logger: this.logger,
412
- })
413
- : undefined
414
-
415
- return { eip7702Delegation, isBlacklisted }
416
- }
417
-
418
- async addSingleTx({ tx, address, cursor }) {
419
- const walletAccounts = this.#walletAccountByAddress.get(address)
420
-
421
- if (!walletAccounts || walletAccounts.length === 0) {
422
- return
423
- }
424
-
425
- for (const walletAccount of walletAccounts) {
426
- const walletAccountInfo = this.#walletAccountInfo.get(walletAccount)
427
-
428
- if (!walletAccountInfo) {
429
- continue
430
- }
431
-
432
- const { derivedData, tokensByAddress, assets, tokens, assetName } = walletAccountInfo
433
-
434
- await this.processAndFillTransactionsToState({
435
- allTxs: [tx],
436
- derivedData,
437
- tokensByAddress,
438
- assets,
439
- tokens,
440
- assetName,
441
- walletAccount,
442
- refresh: false,
443
- cursor,
444
- })
445
- }
446
- }
447
-
448
- async getNewAccountState({ tokens, currentTokenBalances, ourWalletAddress }) {
449
- const asset = this.asset
450
- const newAccountState = Object.create(null)
451
- const balances = await this.getBalances({ tokens, ourWalletAddress })
452
- if (this.#rpcBalanceAssetNames.includes(asset.name)) {
453
- const balance = balances[asset.name]
454
- newAccountState.balance = asset.currency.baseUnit(balance)
455
- }
456
-
457
- const tokenBalancePairs = Object.entries(balances).filter((entry) => entry[0] !== asset.name)
458
- const tokenBalanceEntries = tokenBalancePairs
459
- .map((pair) => {
460
- const token = tokens.find((token) => token.name === pair[0])
461
- const value = token.currency.baseUnit(pair[1] || 0)
462
- return [token.name, value]
463
- })
464
- .filter(Boolean)
465
-
466
- const tokenBalances = excludeUnchangedTokenBalances(currentTokenBalances, tokenBalanceEntries)
467
- if (!isEmpty(tokenBalances)) newAccountState.tokenBalances = tokenBalances
468
- return newAccountState
469
- }
470
-
471
- async getReceiveAddressesByWalletAccount() {
472
- const walletAccounts = await this.aci.getWalletAccounts({ assetName: this.asset.name })
473
- const addressesByAccount = Object.create(null)
474
- for (const walletAccount of walletAccounts) {
475
- addressesByAccount[walletAccount] = await this.aci.getReceiveAddresses({
476
- assetName: this.asset.name,
477
- walletAccount,
478
- useCache: true,
479
- })
480
- }
481
-
482
- return addressesByAccount
483
- }
484
-
485
- async fillAssetsTokensAndData({ walletAccount }) {
486
- const assetName = this.asset.name
487
- const assets = await this.aci.getAssetsForNetwork({ baseAssetName: assetName })
488
- const tokens = Object.values(assets).filter((asset) => assetName !== asset.name)
489
-
490
- const tokensByAddress = tokens.reduce((map, token) => {
491
- const addresses = getAssetAddresses(token)
492
- for (const address of addresses) map.set(address.toLowerCase(), token)
493
- return map
494
- }, new Map())
495
-
496
- const derivedData = await this.deriveData({ assetName, walletAccount, tokens })
497
-
498
- this.#walletAccountInfo.set(walletAccount, {
499
- assets,
500
- tokens,
501
- tokensByAddress,
502
- derivedData,
503
- assetName,
504
- })
505
- }
506
-
507
- async subscribeAllWalletAccounts() {
508
- const addressesByWalletAccount = await this.getReceiveAddressesByWalletAccount()
509
- const entriesAddressesByWalletAccount = Object.entries(addressesByWalletAccount)
510
-
511
- for (const [walletAccount] of entriesAddressesByWalletAccount) {
512
- await this.subscribeWalletAddresses(walletAccount)
513
- }
514
- }
515
-
516
- async subscribeWalletAddresses(walletAccount) {
517
- const addressesByWalletAccount = await this.aci.getReceiveAddresses({
518
- assetName: this.asset.name,
519
- walletAccount,
520
- useCache: true,
521
- })
522
-
523
- const address = addressesByWalletAccount[0].toLowerCase() // Only check m/0/0
524
- await this.fillAssetsTokensAndData({ walletAccount })
525
-
526
- if (!this.#walletAccountByAddress.has(address)) {
527
- this.#walletAccountByAddress.set(address, [])
528
- }
529
-
530
- const walletAccounts = this.#walletAccountByAddress.get(address)
531
-
532
- if (!walletAccounts.includes(walletAccount)) {
533
- walletAccounts.push(walletAccount)
534
- this.#walletAccountByAddress.set(address, walletAccounts)
535
- }
536
-
537
- this.server.connectTransactions({ walletAccount, address })
538
-
539
- this.#wsClient.subscribeWalletAddresses({
540
- network: this.asset.name,
541
- addresses: [address],
542
- })
543
- }
544
-
545
- async getBalances({ tokens, ourWalletAddress }) {
546
- const batch = Object.create(null)
547
- if (this.#rpcBalanceAssetNames.includes(this.asset.name)) {
548
- const request = this.server.getBalanceRequest(ourWalletAddress)
549
- batch[this.asset.name] = request
550
- }
551
-
552
- for (const token of tokens) {
553
- if (this.#rpcBalanceAssetNames.includes(token.name) && token.contract.address) {
554
- const request = this.server.balanceOfRequest(ourWalletAddress, token.contract.address)
555
- batch[token.name] = request
556
- }
557
- }
558
-
559
- const pairs = Object.entries(batch)
560
- if (pairs.length === 0) {
561
- return {}
562
- }
563
-
564
- const requests = pairs.map((pair) => pair[1])
565
- const responses = await this.server.sendBatchRequest(requests)
566
- const entries = pairs.map((pair, idx) => {
567
- const balanceHex = responses[idx]
568
- const name = pair[0]
569
- const balance = fromHexToString(balanceHex)
570
- return [name, balance]
571
- })
572
- return Object.fromEntries(entries)
573
- }
574
-
575
- getUnknownTokenAddresses({ transactions, tokensByAddress }) {
576
- const set = transactions.reduce((acc, txn) => {
577
- const transfers = filterEffects(txn.effects, 'erc20') || []
578
- transfers.forEach((transfer) => {
579
- const addr = transfer.address.toLowerCase()
580
- if (!tokensByAddress.has(addr)) {
581
- acc.add(addr)
582
- }
583
- })
584
- return acc
585
- }, new Set())
586
- return [...set]
587
- }
588
-
589
- // NOTE: Here, fetchedGasPrices is the result of a call to `ClarityMonitor.getFee()`.
590
- async updateGasPrice(fetchedGasPrices) {
591
- try {
592
- await executeEthLikeFeeMonitorUpdate({
593
- assetClientInterface: this.aci,
594
- feeAsset: this.asset,
595
- fetchedGasPrices,
596
- })
597
- } catch (e) {
598
- this.logger.warn('error updating gasPrice', e)
599
- }
600
- }
601
-
602
- async onFeeUpdated(fee) {
603
- return this.updateGasPrice(fee)
604
- }
605
-
606
- async beforeStart() {
607
- this.listenToServerEvents()
608
- if (this.config.GAS_PRICE_FROM_WEBSOCKET) {
609
- this.server.connectFee()
610
- }
611
- }
612
-
613
- async afterStop() {
614
- this.server.dispose()
615
- this.#wsClient.dispose(this.asset.name)
616
- }
617
-
618
- async getHistoryFromServer({ walletAccount, derivedData, refresh }) {
619
- const address = derivedData.ourWalletAddress
620
- const currentCursor = derivedData.currentAccountState?.clarityCursor
621
- const cursor = currentCursor && !refresh ? currentCursor : null
622
- return this.server.getAllTransactions({ walletAccount, address, cursor })
623
- }
624
-
625
- listenToServerEvents() {
626
- this.server.on('feeUpdated', (...args) => this.onFeeUpdated(...args))
627
- this.#wsClient.on(
628
- `${this.asset.name}:new_transaction`,
629
- async ({ transaction, address, cursor }) =>
630
- this.addSingleTx({
631
- tx: transaction,
632
- address,
633
- cursor,
634
- })
635
- )
636
- }
637
- }