@exodus/ethereum-api 8.45.6 → 8.46.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,26 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ## [8.46.1](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.46.0...@exodus/ethereum-api@8.46.1) (2025-09-03)
7
+
8
+
9
+ ### Bug Fixes
10
+
11
+
12
+ * fix: tx-create simplification (#6310)
13
+
14
+
15
+
16
+ ## [8.46.0](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.45.6...@exodus/ethereum-api@8.46.0) (2025-08-21)
17
+
18
+
19
+ ### Features
20
+
21
+
22
+ * feat: implement clarity websocket gateway client (#5623)
23
+
24
+
25
+
6
26
  ## [8.45.6](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.45.5...@exodus/ethereum-api@8.45.6) (2025-08-19)
7
27
 
8
28
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/ethereum-api",
3
- "version": "8.45.6",
3
+ "version": "8.46.1",
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",
@@ -39,6 +39,7 @@
39
39
  "@exodus/web3-ethereum-utils": "^4.2.1",
40
40
  "bn.js": "^5.2.1",
41
41
  "delay": "^4.0.1",
42
+ "eventemitter3": "^4.0.7",
42
43
  "events": "^1.1.1",
43
44
  "idna-uts46-hx": "^2.3.1",
44
45
  "lodash": "^4.17.15",
@@ -63,5 +64,5 @@
63
64
  "type": "git",
64
65
  "url": "git+https://github.com/ExodusMovement/assets.git"
65
66
  },
66
- "gitHead": "41fe883d14291bfb6b73b37b617ce2f7fa914edb"
67
+ "gitHead": "e84949a7310f03feb4893668abd44fe537bf250c"
67
68
  }
@@ -4,6 +4,7 @@ import ms from 'ms'
4
4
  import { createEvmServer, ValidMonitorTypes } from './exodus-eth-server/index.js'
5
5
  import { createEthereumHooks } from './hooks/index.js'
6
6
  import { ClarityMonitor } from './tx-log/clarity-monitor.js'
7
+ import { ClarityMonitorV2 } from './tx-log/clarity-monitor-v2.js'
7
8
  import { EthereumMonitor } from './tx-log/ethereum-monitor.js'
8
9
  import { EthereumNoHistoryMonitor } from './tx-log/ethereum-no-history-monitor.js'
9
10
  import { resolveNonce } from './tx-send/nonce-utils.js'
@@ -128,6 +129,15 @@ export const createHistoryMonitorFactory = ({
128
129
  ...args,
129
130
  })
130
131
  break
132
+ case 'clarity-v3':
133
+ monitor = new ClarityMonitorV2({
134
+ assetClientInterface,
135
+ interval: ms(monitorInterval || '5m'),
136
+ server,
137
+ rpcBalanceAssetNames,
138
+ ...args,
139
+ })
140
+ break
131
141
  case 'no-history':
132
142
  monitor = new EthereumNoHistoryMonitor({
133
143
  assetClientInterface,
@@ -33,7 +33,6 @@ import { createFeeData } from './fee-data-factory.js'
33
33
  import { createGetBalanceForAddress } from './get-balance-for-address.js'
34
34
  import { getBalancesFactory } from './get-balances.js'
35
35
  import { getFeeFactory } from './get-fee.js'
36
- import getFeeAsyncFactory from './get-fee-async.js'
37
36
  import { estimateL1DataFeeFactory, getL1GetFeeFactory } from './optimism-gas/index.js'
38
37
  import { serverBasedFeeMonitorFactoryFactory } from './server-based-fee-monitor.js'
39
38
  import { createStakingApi } from './staking-api.js'
@@ -258,7 +257,7 @@ export const createAssetFactory = ({
258
257
  getBalanceForAddress: createGetBalanceForAddress({ asset, server }),
259
258
  getConfirmationsNumber: () => confirmationsNumber,
260
259
  getDefaultAddressPath: () => defaultAddressPath,
261
- getFeeAsync: getFeeAsyncFactory({ assetClientInterface, createTx }),
260
+ getFeeAsync: createTx, // createTx alias, remove me when possible
262
261
  getFee,
263
262
  getFeeData: () => feeData,
264
263
  getKeyIdentifier: createGetKeyIdentifier({
@@ -13,7 +13,7 @@ import ApiCoinNodesServer from './api-coin-nodes.js'
13
13
  import ClarityServer from './clarity.js'
14
14
  import ClarityServerV2 from './clarity-v2.js'
15
15
 
16
- export const ValidMonitorTypes = ['no-history', 'clarity', 'clarity-v2', 'magnifier']
16
+ export const ValidMonitorTypes = ['no-history', 'clarity', 'clarity-v2', 'clarity-v3', 'magnifier']
17
17
 
18
18
  export function createEvmServer({ assetName, serverUrl, monitorType }) {
19
19
  assert(assetName, 'assetName is required')
@@ -25,6 +25,7 @@ export function createEvmServer({ assetName, serverUrl, monitorType }) {
25
25
  case 'clarity':
26
26
  return new ClarityServer({ baseAssetName: assetName, uri: serverUrl })
27
27
  case 'clarity-v2':
28
+ case 'clarity-v3':
28
29
  return new ClarityServerV2({ baseAssetName: assetName, uri: serverUrl })
29
30
  case 'magnifier':
30
31
  return create(serverUrl, assetName)
@@ -0,0 +1,267 @@
1
+ import { createConsoleLogger } from '@exodus/asset-lib'
2
+ import WebSocket from '@exodus/fetch/websocket'
3
+ import EventEmitter from 'eventemitter3'
4
+ import assert from 'minimalistic-assert'
5
+
6
+ const createSubscriptionName = (network) => {
7
+ return `v1/transactions/${network}`
8
+ }
9
+
10
+ const createSubscriptionKey = ({ subscriptionName, address }) => {
11
+ return `${subscriptionName}|${address}`
12
+ }
13
+
14
+ const parseSubscriptionKey = (subscriptionKey) => {
15
+ const [subscriptionName, address] = subscriptionKey.split('|')
16
+
17
+ return {
18
+ subscriptionName,
19
+ subscriptionAddress: address,
20
+ }
21
+ }
22
+
23
+ /**
24
+ * @typedef subscribeWalletAddresses
25
+ * @property {string} network
26
+ * @property {string} address
27
+ */
28
+
29
+ class WsGateway extends EventEmitter {
30
+ #socket = null
31
+ // Dedup subscriptions to reduce workload on server and resubscribe after closing
32
+ #subscriptions = new Set()
33
+ #reconnectionTimeoutId = null
34
+ #defaultUri = 'wss://ws-gateway-clarity.a.exodus.io'
35
+ #uri = null
36
+ #logger = createConsoleLogger(`@exodus/ws-gateway`)
37
+
38
+ _handlers = {
39
+ message: ({ target, data }) => {
40
+ if (target !== this.#socket) return
41
+ const msg = JSON.parse(data)
42
+
43
+ if (Array.isArray(msg)) {
44
+ for (const item of msg) {
45
+ this.#handleMessage(item)
46
+ }
47
+
48
+ return
49
+ }
50
+
51
+ this.#handleMessage(msg)
52
+ },
53
+ close: ({ target, code, reason }) => {
54
+ if (target !== this.#socket) return
55
+ this.#reconnect({ code, reason })
56
+ },
57
+ open: ({ target }) => {
58
+ if (target !== this.#socket) return
59
+ this.emit('opened')
60
+ },
61
+ error: ({ target }) => {
62
+ if (target !== this.#socket) return
63
+ this.#reconnect({ code: 0, reason: 'Error' })
64
+ },
65
+ }
66
+
67
+ getSocket() {
68
+ return this.#socket
69
+ }
70
+
71
+ getSubscriptions() {
72
+ return this.#subscriptions
73
+ }
74
+
75
+ setServer(uri) {
76
+ this.#uri = uri || this.#uri || this.#defaultUri
77
+ }
78
+
79
+ start() {
80
+ if (this.#socket) {
81
+ return
82
+ }
83
+
84
+ this.#socket = new WebSocket(this.#uri)
85
+
86
+ this.#socket.addEventListener('message', this._handlers.message)
87
+ this.#socket.addEventListener('close', this._handlers.close)
88
+ this.#socket.addEventListener('open', this._handlers.open)
89
+ this.#socket.addEventListener('error', this._handlers.error)
90
+ }
91
+
92
+ #reconnect({ code, reason } = {}) {
93
+ this.#logger.warn(`Reconnect. Code ${code}, ${reason || 'Empty reason'}`)
94
+
95
+ this.#clearSocketListeners()
96
+
97
+ if (this.#socket.readyState === WebSocket.CLOSED) this.#socket = null
98
+ clearTimeout(this.#reconnectionTimeoutId)
99
+ this.#subscriptions.clear()
100
+
101
+ this.#reconnectionTimeoutId = setTimeout(() => {
102
+ this.start()
103
+ clearTimeout(this.#reconnectionTimeoutId)
104
+ }, 2500)
105
+ }
106
+
107
+ unsubscribeWalletAddresses({ network, addresses = [] } = {}) {
108
+ assert(network, '"network" is required')
109
+
110
+ const unsubscribeName = createSubscriptionName(network)
111
+
112
+ const payload = []
113
+
114
+ const addressesSet = new Set(addresses)
115
+
116
+ for (const subscriptionKey of this.#subscriptions) {
117
+ const { subscriptionName, subscriptionAddress } = parseSubscriptionKey(subscriptionKey)
118
+ if (subscriptionName !== unsubscribeName) continue
119
+
120
+ // Unsubscribe whole network
121
+ if (addressesSet.size === 0) {
122
+ if (!payload.some((item) => item.subscription === subscriptionName)) {
123
+ payload.push({
124
+ subscription: subscriptionName,
125
+ })
126
+ }
127
+
128
+ this.#subscriptions.delete(subscriptionKey)
129
+ continue
130
+ }
131
+
132
+ if (addressesSet.has(subscriptionAddress)) {
133
+ payload.push({
134
+ subscription: subscriptionName,
135
+ entityId: subscriptionAddress,
136
+ })
137
+ this.#subscriptions.delete(subscriptionKey)
138
+ }
139
+ }
140
+
141
+ if (payload.length === 0) {
142
+ return
143
+ }
144
+
145
+ this.#sendMessage({
146
+ eventName: 'unsubscribe',
147
+ payload,
148
+ })
149
+ }
150
+
151
+ /**
152
+ * @param {Array<subscribeWalletAddresses>} subscriptions
153
+ */
154
+ subscribeWalletAddresses({ network, addresses }) {
155
+ const payload = []
156
+
157
+ for (const address of addresses) {
158
+ const subscriptionName = createSubscriptionName(network)
159
+ const subscriptionKey = createSubscriptionKey({ subscriptionName, address })
160
+
161
+ if (this.#subscriptions.has(subscriptionKey)) {
162
+ continue
163
+ }
164
+
165
+ payload.push({
166
+ subscription: subscriptionName,
167
+ entityId: address,
168
+ })
169
+ }
170
+
171
+ if (payload.length === 0) {
172
+ return
173
+ }
174
+
175
+ this.#sendMessage({
176
+ eventName: 'subscribe',
177
+ payload,
178
+ })
179
+ }
180
+
181
+ #sendMessage(message) {
182
+ if (this.#socket.readyState !== WebSocket.OPEN) {
183
+ return
184
+ }
185
+
186
+ this.#socket.send(JSON.stringify(message))
187
+ }
188
+
189
+ #handleMessage(message) {
190
+ switch (message.type) {
191
+ case 'new_transactions': {
192
+ const { subscription, entityId, payload } = message
193
+ const network = subscription.split('/')[2]
194
+
195
+ for (const transaction of payload.transactions) {
196
+ this.emit(`${network}:new_transaction`, {
197
+ address: entityId,
198
+ transaction,
199
+ cursor: payload.cursor,
200
+ })
201
+ }
202
+
203
+ break
204
+ }
205
+
206
+ case 'connection_ack': {
207
+ this.emit('connected', message)
208
+ break
209
+ }
210
+
211
+ case 'subscribe_ack': {
212
+ const { subscription, entityId } = message
213
+
214
+ const subscriptionKey = createSubscriptionKey({
215
+ subscriptionName: subscription,
216
+ address: entityId,
217
+ })
218
+ this.#subscriptions.add(subscriptionKey)
219
+
220
+ this.emit('subscribed', message)
221
+ break
222
+ }
223
+
224
+ case 'error': {
225
+ this.#logger.error('Error from server', message)
226
+ this.emit('error', message)
227
+ break
228
+ }
229
+
230
+ default: {
231
+ break
232
+ }
233
+ }
234
+ }
235
+
236
+ #clearSocketListeners() {
237
+ if (!this.#socket) return
238
+
239
+ this.#socket.removeEventListener('message', this._handlers.message)
240
+ this.#socket.removeEventListener('close', this._handlers.close)
241
+ this.#socket.removeEventListener('open', this._handlers.open)
242
+ this.#socket.removeEventListener('error', this._handlers.error)
243
+ }
244
+
245
+ dispose(network, addresses) {
246
+ this.unsubscribeWalletAddresses({ network, addresses })
247
+
248
+ if (this.#subscriptions.size > 0) {
249
+ return
250
+ }
251
+
252
+ this.#clearSocketListeners()
253
+
254
+ clearTimeout(this.#reconnectionTimeoutId)
255
+
256
+ if (this.#socket && [WebSocket.OPEN, WebSocket.CONNECTING].includes(this.#socket.readyState)) {
257
+ this.#socket.close()
258
+ }
259
+
260
+ this.#socket = null
261
+ }
262
+ }
263
+
264
+ const wsGateway = new WsGateway()
265
+ const createWsGateway = () => wsGateway
266
+
267
+ export { createWsGateway, WsGateway }
package/src/tx-create.js CHANGED
@@ -6,7 +6,7 @@ import * as ErrorWrapper from './error-wrapper.js'
6
6
  import { isContractAddressCached } from './eth-like-util.js'
7
7
  import { ensureSaneEip1559GasPriceForTipGasPrice } from './fee-utils.js'
8
8
  import { ARBITRARY_ADDRESS, fetchGasLimit, resolveDefaultTxInput } from './gas-estimation.js'
9
- import { getFeeFactoryGasPrices } from './get-fee.js'
9
+ import { getExtraFeeData, getFeeFactoryGasPrices } from './get-fee.js'
10
10
  import { getNftArguments } from './nft-utils.js'
11
11
 
12
12
  async function createUnsignedTxWithFees({
@@ -66,7 +66,7 @@ async function createUnsignedTxWithFees({
66
66
  : asset.baseAsset.currency.ZERO
67
67
 
68
68
  const fee = baseFee.add(l1DataFee)
69
-
69
+ const extraFeeData = getExtraFeeData({ asset, amount: coinAmount })
70
70
  const unsignedTx = {
71
71
  txData: { transactionBuffer, chainId },
72
72
  txMeta: {
@@ -81,6 +81,8 @@ async function createUnsignedTxWithFees({
81
81
  }
82
82
  return {
83
83
  unsignedTx,
84
+ fee,
85
+ extraFeeData,
84
86
  // exhcange compatibility until the use usignedTx, remove me!
85
87
  gasPrice,
86
88
  tipGasPrice,
@@ -195,7 +197,6 @@ export const createTxFactory = ({ chainId, assetClientInterface, useAbsoluteNonc
195
197
  return async ({
196
198
  asset,
197
199
  walletAccount,
198
- feeData,
199
200
  nft, // when sending nfts
200
201
  fromAddress: providedFromAddress, // wallet from address
201
202
  toAddress: providedToAddress, // user's to address, not the token or the dex contract
@@ -213,8 +214,16 @@ export const createTxFactory = ({ chainId, assetClientInterface, useAbsoluteNonc
213
214
  keepTxInput, // @deprecated this flag is used by swaps when swapping a token via DEX. The asset is token but the tx TO address is not the token address. Update swap to use `contractAddress`
214
215
  }) => {
215
216
  assert(asset, 'asset is required')
216
- assert(feeData, 'feeData is required')
217
- const fromAddress = providedFromAddress || ARBITRARY_ADDRESS
217
+ assert(walletAccount, 'walletAccount is required')
218
+
219
+ const feeData = await assetClientInterface.getFeeConfig({ assetName: asset.baseAsset.name })
220
+
221
+ const fromAddress =
222
+ providedFromAddress ??
223
+ (await assetClientInterface.getReceiveAddress({
224
+ assetName: asset.baseAsset.name,
225
+ walletAccount,
226
+ }))
218
227
 
219
228
  const baseAssetTxLog = await assetClientInterface.getTxLog({
220
229
  assetName: asset.baseAsset.name,
@@ -320,7 +329,7 @@ export const createTxFactory = ({ chainId, assetClientInterface, useAbsoluteNonc
320
329
  (await fetchGasLimit({
321
330
  asset,
322
331
  feeData,
323
- fromAddress: providedFromAddress,
332
+ fromAddress,
324
333
  toAddress: providedToAddress,
325
334
  txInput: providedTxInput,
326
335
  contractAddress: txToAddress,
@@ -0,0 +1,467 @@
1
+ import { BaseMonitor } from '@exodus/asset-lib'
2
+ import { getAssetAddresses } from '@exodus/ethereum-lib'
3
+ import lodash from 'lodash'
4
+
5
+ import { createWsGateway } from '../exodus-eth-server/ws-gateway.js'
6
+ import { executeEthLikeFeeMonitorUpdate } from '../fee-utils.js'
7
+ import { fromHexToString } from '../number-utils.js'
8
+ import { filterEffects, getLogItemsFromServerTx } from './clarity-utils/index.js'
9
+ import {
10
+ checkPendingTransactions,
11
+ excludeUnchangedTokenBalances,
12
+ getAllLogItemsByAsset,
13
+ getDeriveDataNeededForTick,
14
+ getDeriveTransactionsToCheck,
15
+ } from './monitor-utils/index.js'
16
+
17
+ const { isEmpty } = lodash
18
+
19
+ export class ClarityMonitorV2 extends BaseMonitor {
20
+ #wsClient = null
21
+ #walletAccountByAddress = new Map()
22
+ #walletAccountInfo = new Map()
23
+ #rpcBalanceAssetNames = []
24
+ constructor({
25
+ server,
26
+ wsGatewayClient = createWsGateway(),
27
+ rpcBalanceAssetNames,
28
+ config,
29
+ ...args
30
+ } = {}) {
31
+ super(args)
32
+ this.config = { GAS_PRICE_FROM_WEBSOCKET: true, ...config }
33
+ this.server = server
34
+ this.#wsClient = wsGatewayClient
35
+ this.#rpcBalanceAssetNames = rpcBalanceAssetNames
36
+ this.getAllLogItemsByAsset = getAllLogItemsByAsset
37
+ this.deriveDataNeededForTick = getDeriveDataNeededForTick(this.aci)
38
+ this.deriveTransactionsToCheck = getDeriveTransactionsToCheck({
39
+ getTxLog: (...args) => this.aci.getTxLog(...args),
40
+ })
41
+
42
+ this.addHook('before-start', (...args) => this.beforeStart(...args))
43
+ this.addHook('after-stop', (...args) => this.afterStop(...args))
44
+ }
45
+
46
+ setServer(config) {
47
+ const uri = config?.server || this.server.defaultUri
48
+
49
+ this.#wsClient.setServer(config.wsGatewayUrl?.v1)
50
+
51
+ this.#wsClient.on('connected', () => this.subscribeAllWalletAccounts())
52
+ this.#wsClient.start()
53
+
54
+ if (uri === this.server.uri) {
55
+ return
56
+ }
57
+
58
+ this.server.setURI(uri)
59
+ if (this.config.GAS_PRICE_FROM_WEBSOCKET) {
60
+ this.server.connectFee()
61
+ }
62
+ }
63
+
64
+ async deriveData({ assetName, walletAccount, tokens }) {
65
+ const { ourWalletAddress, currentAccountState } = await this.deriveDataNeededForTick({
66
+ assetName,
67
+ walletAccount,
68
+ })
69
+ const transactionsToCheck = await this.deriveTransactionsToCheck({
70
+ assetName,
71
+ walletAccount,
72
+ tokens,
73
+ ourWalletAddress,
74
+ })
75
+
76
+ return {
77
+ ourWalletAddress,
78
+ currentAccountState,
79
+ ...transactionsToCheck,
80
+ }
81
+ }
82
+
83
+ // eslint-disable-next-line no-undef
84
+ async checkPendingTransactions(params) {
85
+ const { pendingTransactionsToCheck, pendingTransactionsGroupedByAddressAndNonce } =
86
+ checkPendingTransactions(params)
87
+ const txsToRemove = []
88
+ const { walletAccount } = params
89
+
90
+ const updateTx = (tx, asset, { error, remove }) => {
91
+ if (remove) {
92
+ txsToRemove.push({ tx, assetSource: { asset, walletAccount } })
93
+ } else {
94
+ params.logItemsByAsset[asset].push({
95
+ ...tx,
96
+ dropped: true,
97
+ error,
98
+ })
99
+ }
100
+
101
+ // in case this is an ETH fee tx that has associated ERC20 send txs
102
+ const promises = tx.tokens.map(async (assetName) => {
103
+ const tokenTxSet = await this.aci.getTxLog({ assetName, walletAccount })
104
+ if (remove) {
105
+ txsToRemove.push({
106
+ tx: tokenTxSet.get(tx.txId),
107
+ assetSource: { asset: assetName, walletAccount },
108
+ })
109
+ } else if (tokenTxSet && tokenTxSet.has(tx.txId)) {
110
+ params.logItemsByAsset[assetName].push({
111
+ ...tokenTxSet.get(tx.txId),
112
+ error,
113
+ dropped: true,
114
+ })
115
+ }
116
+ })
117
+ return Promise.all(promises)
118
+ }
119
+
120
+ for (const { tx, assetName, replaced = false } of Object.values(
121
+ pendingTransactionsGroupedByAddressAndNonce
122
+ )) {
123
+ if (replaced) {
124
+ await updateTx(tx, assetName, { remove: true })
125
+ delete pendingTransactionsToCheck[tx.txId]
126
+ }
127
+ }
128
+
129
+ for (const { tx, assetName } of Object.values(pendingTransactionsToCheck)) {
130
+ if (params.refresh) await updateTx(tx, assetName, { remove: true })
131
+ else await updateTx(tx, assetName, { error: 'Dropped' })
132
+ }
133
+
134
+ return { txsToRemove }
135
+ }
136
+
137
+ async tick({ walletAccount, refresh }) {
138
+ await this.subscribeWalletAddresses(walletAccount)
139
+
140
+ const walletAccountInfo = this.#walletAccountInfo.get(walletAccount)
141
+
142
+ if (!walletAccountInfo) {
143
+ return this.logger.warn('walletAccountInfo is empty', { walletAccount })
144
+ }
145
+
146
+ const { derivedData, tokensByAddress, assets, tokens, assetName } = walletAccountInfo
147
+
148
+ const response = await this.getHistoryFromServer({ walletAccount, derivedData, refresh })
149
+ const allTxs = [...response.transactions.pending, ...response.transactions.confirmed]
150
+ const cursor = response.cursor
151
+
152
+ await this.processAndFillTransactionsToState({
153
+ allTxs,
154
+ derivedData,
155
+ tokensByAddress,
156
+ assets,
157
+ tokens,
158
+ assetName,
159
+ walletAccount,
160
+ refresh,
161
+ cursor,
162
+ })
163
+ }
164
+
165
+ async processAndFillTransactionsToState({
166
+ allTxs,
167
+ derivedData,
168
+ tokensByAddress,
169
+ assets,
170
+ tokens,
171
+ assetName,
172
+ walletAccount,
173
+ refresh,
174
+ cursor,
175
+ }) {
176
+ const hasNewTxs = allTxs.length > 0
177
+
178
+ const logItemsByAsset = this.getAllLogItemsByAsset({
179
+ getLogItemsFromServerTx,
180
+ ourWalletAddress: derivedData.ourWalletAddress,
181
+ allTransactionsFromServer: allTxs,
182
+ asset: this.asset,
183
+ tokensByAddress,
184
+ assets,
185
+ })
186
+
187
+ const { txsToRemove } = await this.checkPendingTransactions({
188
+ txlist: allTxs,
189
+ walletAccount,
190
+ refresh,
191
+ logItemsByAsset,
192
+ asset: this.asset,
193
+ ...derivedData,
194
+ })
195
+
196
+ const accountState = await this.getNewAccountState({
197
+ tokens,
198
+ currentTokenBalances: derivedData.currentAccountState?.tokenBalances,
199
+ ourWalletAddress: derivedData.ourWalletAddress,
200
+ })
201
+
202
+ const batch = this.aci.createOperationsBatch()
203
+
204
+ this.aci.removeTxLogBatch({
205
+ assetName,
206
+ walletAccount,
207
+ txs: txsToRemove,
208
+ batch,
209
+ })
210
+
211
+ for (const [assetName, txs] of Object.entries(logItemsByAsset)) {
212
+ this.aci.updateTxLogAndNotifyBatch({
213
+ assetName,
214
+ walletAccount,
215
+ txs,
216
+ refresh,
217
+ batch,
218
+ })
219
+ }
220
+
221
+ const newData = { ...accountState }
222
+
223
+ if (cursor) {
224
+ newData.clarityCursor = cursor
225
+ }
226
+
227
+ this.aci.updateAccountStateBatch({
228
+ assetName,
229
+ walletAccount,
230
+ accountState,
231
+ newData,
232
+ batch,
233
+ })
234
+
235
+ await this.aci.executeOperationsBatch(batch)
236
+
237
+ if (refresh || hasNewTxs) {
238
+ const unknownTokenAddresses = this.getUnknownTokenAddresses({
239
+ transactions: allTxs,
240
+ tokensByAddress,
241
+ })
242
+ if (unknownTokenAddresses.length > 0) {
243
+ this.emit('unknown-tokens', unknownTokenAddresses)
244
+ }
245
+ }
246
+ }
247
+
248
+ async addSingleTx({ tx, address, cursor }) {
249
+ const walletAccounts = this.#walletAccountByAddress.get(address)
250
+
251
+ if (!walletAccounts || walletAccounts.length === 0) {
252
+ return
253
+ }
254
+
255
+ for (const walletAccount of walletAccounts) {
256
+ const walletAccountInfo = this.#walletAccountInfo.get(walletAccount)
257
+
258
+ if (!walletAccountInfo) {
259
+ continue
260
+ }
261
+
262
+ const { derivedData, tokensByAddress, assets, tokens, assetName } = walletAccountInfo
263
+
264
+ await this.processAndFillTransactionsToState({
265
+ allTxs: [tx],
266
+ derivedData,
267
+ tokensByAddress,
268
+ assets,
269
+ tokens,
270
+ assetName,
271
+ walletAccount,
272
+ refresh: false,
273
+ cursor,
274
+ })
275
+ }
276
+ }
277
+
278
+ async getNewAccountState({ tokens, currentTokenBalances, ourWalletAddress }) {
279
+ const asset = this.asset
280
+ const newAccountState = Object.create(null)
281
+ const balances = await this.getBalances({ tokens, ourWalletAddress })
282
+ if (this.#rpcBalanceAssetNames.includes(asset.name)) {
283
+ const balance = balances[asset.name]
284
+ newAccountState.balance = asset.currency.baseUnit(balance)
285
+ }
286
+
287
+ const tokenBalancePairs = Object.entries(balances).filter((entry) => entry[0] !== asset.name)
288
+ const tokenBalanceEntries = tokenBalancePairs
289
+ .map((pair) => {
290
+ const token = tokens.find((token) => token.name === pair[0])
291
+ const value = token.currency.baseUnit(pair[1] || 0)
292
+ return [token.name, value]
293
+ })
294
+ .filter(Boolean)
295
+
296
+ const tokenBalances = excludeUnchangedTokenBalances(currentTokenBalances, tokenBalanceEntries)
297
+ if (!isEmpty(tokenBalances)) newAccountState.tokenBalances = tokenBalances
298
+ return newAccountState
299
+ }
300
+
301
+ async getReceiveAddressesByWalletAccount() {
302
+ const walletAccounts = await this.aci.getWalletAccounts({ assetName: this.asset.name })
303
+ const addressesByAccount = Object.create(null)
304
+ for (const walletAccount of walletAccounts) {
305
+ addressesByAccount[walletAccount] = await this.aci.getReceiveAddresses({
306
+ assetName: this.asset.name,
307
+ walletAccount,
308
+ useCache: true,
309
+ })
310
+ }
311
+
312
+ return addressesByAccount
313
+ }
314
+
315
+ async fillAssetsTokensAndData({ walletAccount }) {
316
+ const assetName = this.asset.name
317
+ const assets = await this.aci.getAssetsForNetwork({ baseAssetName: assetName })
318
+ const tokens = Object.values(assets).filter((asset) => assetName !== asset.name)
319
+
320
+ const tokensByAddress = tokens.reduce((map, token) => {
321
+ const addresses = getAssetAddresses(token)
322
+ for (const address of addresses) map.set(address.toLowerCase(), token)
323
+ return map
324
+ }, new Map())
325
+
326
+ const derivedData = await this.deriveData({ assetName, walletAccount, tokens })
327
+
328
+ this.#walletAccountInfo.set(walletAccount, {
329
+ assets,
330
+ tokens,
331
+ tokensByAddress,
332
+ derivedData,
333
+ assetName,
334
+ })
335
+ }
336
+
337
+ async subscribeAllWalletAccounts() {
338
+ const addressesByWalletAccount = await this.getReceiveAddressesByWalletAccount()
339
+ const entriesAddressesByWalletAccount = Object.entries(addressesByWalletAccount)
340
+
341
+ for (const [walletAccount] of entriesAddressesByWalletAccount) {
342
+ await this.subscribeWalletAddresses(walletAccount)
343
+ }
344
+ }
345
+
346
+ async subscribeWalletAddresses(walletAccount) {
347
+ const addressesByWalletAccount = await this.aci.getReceiveAddresses({
348
+ assetName: this.asset.name,
349
+ walletAccount,
350
+ useCache: true,
351
+ })
352
+
353
+ const address = addressesByWalletAccount[0].toLowerCase() // Only check m/0/0
354
+ await this.fillAssetsTokensAndData({ walletAccount })
355
+
356
+ if (!this.#walletAccountByAddress.has(address)) {
357
+ this.#walletAccountByAddress.set(address, [])
358
+ }
359
+
360
+ const walletAccounts = this.#walletAccountByAddress.get(address)
361
+
362
+ if (!walletAccounts.includes(walletAccount)) {
363
+ walletAccounts.push(walletAccount)
364
+ this.#walletAccountByAddress.set(address, walletAccounts)
365
+ }
366
+
367
+ this.server.connectTransactions({ walletAccount, address })
368
+
369
+ this.#wsClient.subscribeWalletAddresses({
370
+ network: this.asset.name,
371
+ addresses: [address],
372
+ })
373
+ }
374
+
375
+ async getBalances({ tokens, ourWalletAddress }) {
376
+ const batch = Object.create(null)
377
+ if (this.#rpcBalanceAssetNames.includes(this.asset.name)) {
378
+ const request = this.server.getBalanceRequest(ourWalletAddress)
379
+ batch[this.asset.name] = request
380
+ }
381
+
382
+ for (const token of tokens) {
383
+ if (this.#rpcBalanceAssetNames.includes(token.name) && token.contract.address) {
384
+ const request = this.server.balanceOfRequest(ourWalletAddress, token.contract.address)
385
+ batch[token.name] = request
386
+ }
387
+ }
388
+
389
+ const pairs = Object.entries(batch)
390
+ if (pairs.length === 0) {
391
+ return {}
392
+ }
393
+
394
+ const requests = pairs.map((pair) => pair[1])
395
+ const responses = await this.server.sendBatchRequest(requests)
396
+ const entries = pairs.map((pair, idx) => {
397
+ const balanceHex = responses[idx]
398
+ const name = pair[0]
399
+ const balance = fromHexToString(balanceHex)
400
+ return [name, balance]
401
+ })
402
+ return Object.fromEntries(entries)
403
+ }
404
+
405
+ getUnknownTokenAddresses({ transactions, tokensByAddress }) {
406
+ const set = transactions.reduce((acc, txn) => {
407
+ const transfers = filterEffects(txn.effects, 'erc20') || []
408
+ transfers.forEach((transfer) => {
409
+ const addr = transfer.address.toLowerCase()
410
+ if (!tokensByAddress.has(addr)) {
411
+ acc.add(addr)
412
+ }
413
+ })
414
+ return acc
415
+ }, new Set())
416
+ return [...set]
417
+ }
418
+
419
+ // NOTE: Here, fetchedGasPrices is the result of a call to `ClarityMonitor.getFee()`.
420
+ async updateGasPrice(fetchedGasPrices) {
421
+ try {
422
+ await executeEthLikeFeeMonitorUpdate({
423
+ assetClientInterface: this.aci,
424
+ feeAsset: this.asset,
425
+ fetchedGasPrices,
426
+ })
427
+ } catch (e) {
428
+ this.logger.warn('error updating gasPrice', e)
429
+ }
430
+ }
431
+
432
+ async onFeeUpdated(fee) {
433
+ return this.updateGasPrice(fee)
434
+ }
435
+
436
+ async beforeStart() {
437
+ this.listenToServerEvents()
438
+ if (this.config.GAS_PRICE_FROM_WEBSOCKET) {
439
+ this.server.connectFee()
440
+ }
441
+ }
442
+
443
+ async afterStop() {
444
+ this.server.dispose()
445
+ this.#wsClient.dispose(this.asset.name)
446
+ }
447
+
448
+ async getHistoryFromServer({ walletAccount, derivedData, refresh }) {
449
+ const address = derivedData.ourWalletAddress
450
+ const currentCursor = derivedData.currentAccountState?.clarityCursor
451
+ const cursor = currentCursor && !refresh ? currentCursor : null
452
+ return this.server.getAllTransactions({ walletAccount, address, cursor })
453
+ }
454
+
455
+ listenToServerEvents() {
456
+ this.server.on('feeUpdated', (...args) => this.onFeeUpdated(...args))
457
+ this.#wsClient.on(
458
+ `${this.asset.name}:new_transaction`,
459
+ async ({ transaction, address, cursor }) =>
460
+ this.addSingleTx({
461
+ tx: transaction,
462
+ address,
463
+ cursor,
464
+ })
465
+ )
466
+ }
467
+ }
@@ -33,24 +33,9 @@ const txSendFactory = ({ assetClientInterface, createTx }) => {
33
33
  return { unsignedTx: providedUnsignedTx }
34
34
  }
35
35
 
36
- const feeData =
37
- legacyParams.feeData ??
38
- (await assetClientInterface.getFeeData({
39
- assetName: baseAsset.name,
40
- }))
41
-
42
- const fromAddress =
43
- legacyParams.fromAddress ??
44
- (await assetClientInterface.getReceiveAddress({
45
- assetName: baseAsset.name,
46
- walletAccount,
47
- }))
48
-
49
36
  return createTx({
50
37
  asset,
51
38
  walletAccount,
52
- feeData,
53
- fromAddress,
54
39
  toAddress: legacyParams.address,
55
40
  ...legacyParams,
56
41
  ...legacyParams.options,
@@ -1,29 +0,0 @@
1
- import assert from 'minimalistic-assert'
2
-
3
- import { getExtraFeeData } from './get-fee.js'
4
-
5
- const getFeeAsyncFactory = ({ assetClientInterface, createTx }) => {
6
- assert(assetClientInterface, 'assetClientInterface is required')
7
- assert(createTx, 'createTx is required')
8
-
9
- return async (params) => {
10
- const { asset } = params
11
- const { unsignedTx, gasPrice, tipGasPrice, gasLimit } = params.unsignedTx
12
- ? params
13
- : await createTx(params)
14
- const fee = asset.feeAsset.currency.parse(unsignedTx.txMeta.fee)
15
- const coinAmount = asset.currency.parse(unsignedTx.txMeta.amount)
16
- const extraFeeData = getExtraFeeData({ asset, amount: coinAmount })
17
- return {
18
- fee,
19
- extraFeeData,
20
- unsignedTx,
21
- // deprecated, exchanges uses these params to recreate the tx during send. It should just use unsignedTx...
22
- gasPrice,
23
- tipGasPrice,
24
- gasLimit,
25
- }
26
- }
27
- }
28
-
29
- export default getFeeAsyncFactory