@exodus/ethereum-api 8.45.6 → 8.46.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.
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,16 @@
|
|
|
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.0](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.45.6...@exodus/ethereum-api@8.46.0) (2025-08-21)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
* feat: implement clarity websocket gateway client (#5623)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
6
16
|
## [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
17
|
|
|
8
18
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/ethereum-api",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.46.0",
|
|
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": "
|
|
67
|
+
"gitHead": "9d7bd1606fa42f6e05c68e0396d5aed7f7ea8852"
|
|
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,
|
|
@@ -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 }
|
|
@@ -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
|
+
}
|