@exodus/ethereum-api 8.45.5 → 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 +20 -0
- package/package.json +11 -10
- package/src/create-asset-plugin-factory.js +0 -1
- package/src/create-asset-utils.js +10 -0
- package/src/create-asset.js +2 -8
- package/src/exodus-eth-server/index.js +2 -1
- package/src/exodus-eth-server/ws-gateway.js +267 -0
- package/src/gas-estimation.js +14 -6
- package/src/get-fee.js +4 -3
- package/src/tx-log/clarity-monitor-v2.js +467 -0
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.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
|
+
|
|
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)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
### Bug Fixes
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
* fix: remove meta boilerplate (#6305)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
6
26
|
## [8.45.5](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.45.4...@exodus/ethereum-api@8.45.5) (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.
|
|
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",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"lint:fix": "yarn lint --fix"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@exodus/asset": "^2.0.
|
|
24
|
+
"@exodus/asset": "^2.0.4",
|
|
25
25
|
"@exodus/asset-lib": "^5.3.0",
|
|
26
26
|
"@exodus/assets": "^11.4.0",
|
|
27
27
|
"@exodus/basic-utils": "^3.0.1",
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
"@exodus/crypto": "^1.0.0-rc.13",
|
|
30
30
|
"@exodus/currency": "^6.0.1",
|
|
31
31
|
"@exodus/ethereum-lib": "^5.17.0",
|
|
32
|
-
"@exodus/ethereum-meta": "^2.9.
|
|
33
|
-
"@exodus/ethereumholesky-meta": "^2.0.
|
|
32
|
+
"@exodus/ethereum-meta": "^2.9.1",
|
|
33
|
+
"@exodus/ethereumholesky-meta": "^2.0.5",
|
|
34
34
|
"@exodus/ethereumjs": "^1.0.0",
|
|
35
35
|
"@exodus/fetch": "^1.3.0",
|
|
36
36
|
"@exodus/models": "^12.13.0",
|
|
@@ -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",
|
|
@@ -50,11 +51,11 @@
|
|
|
50
51
|
},
|
|
51
52
|
"devDependencies": {
|
|
52
53
|
"@exodus/assets-testing": "^1.0.0",
|
|
53
|
-
"@exodus/bsc-meta": "^2.1
|
|
54
|
-
"@exodus/ethereumarbone-meta": "^2.
|
|
55
|
-
"@exodus/fantommainnet-meta": "^2.0.
|
|
56
|
-
"@exodus/matic-meta": "^2.2.
|
|
57
|
-
"@exodus/rootstock-meta": "^2.0.
|
|
54
|
+
"@exodus/bsc-meta": "^2.5.1",
|
|
55
|
+
"@exodus/ethereumarbone-meta": "^2.1.2",
|
|
56
|
+
"@exodus/fantommainnet-meta": "^2.0.5",
|
|
57
|
+
"@exodus/matic-meta": "^2.2.7",
|
|
58
|
+
"@exodus/rootstock-meta": "^2.0.5"
|
|
58
59
|
},
|
|
59
60
|
"bugs": {
|
|
60
61
|
"url": "https://github.com/ExodusMovement/assets/issues?q=is%3Aissue+is%3Aopen+label%3Aethereum-api"
|
|
@@ -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,
|
package/src/create-asset.js
CHANGED
|
@@ -128,10 +128,6 @@ export const createAssetFactory = ({
|
|
|
128
128
|
|
|
129
129
|
const server = createEvmServer({ assetName: asset.name, serverUrl, monitorType })
|
|
130
130
|
|
|
131
|
-
const gasLimit = 21e3 // 21 KGas, enough only for sending ether to normal address
|
|
132
|
-
|
|
133
|
-
const contractGasLimit = 1e6 // used when estimateGas fail
|
|
134
|
-
|
|
135
131
|
const address = {
|
|
136
132
|
validate: validateFactory({ chainId, useEip1191ChainIdChecksum }),
|
|
137
133
|
hasChecksum,
|
|
@@ -237,7 +233,7 @@ export const createAssetFactory = ({
|
|
|
237
233
|
? estimateL1DataFeeFactory({ l1GasOracleAddress, server })
|
|
238
234
|
: undefined
|
|
239
235
|
|
|
240
|
-
const originalGetFee = getFeeFactory(
|
|
236
|
+
const originalGetFee = getFeeFactory()
|
|
241
237
|
|
|
242
238
|
const getFee = l1GasOracleAddress
|
|
243
239
|
? getL1GetFeeFactory({ asset, originalGetFee })
|
|
@@ -262,7 +258,7 @@ export const createAssetFactory = ({
|
|
|
262
258
|
getBalanceForAddress: createGetBalanceForAddress({ asset, server }),
|
|
263
259
|
getConfirmationsNumber: () => confirmationsNumber,
|
|
264
260
|
getDefaultAddressPath: () => defaultAddressPath,
|
|
265
|
-
getFeeAsync: getFeeAsyncFactory({ assetClientInterface,
|
|
261
|
+
getFeeAsync: getFeeAsyncFactory({ assetClientInterface, createTx }),
|
|
266
262
|
getFee,
|
|
267
263
|
getFeeData: () => feeData,
|
|
268
264
|
getKeyIdentifier: createGetKeyIdentifier({
|
|
@@ -289,8 +285,6 @@ export const createAssetFactory = ({
|
|
|
289
285
|
|
|
290
286
|
const fullAsset = {
|
|
291
287
|
...asset,
|
|
292
|
-
gasLimit,
|
|
293
|
-
contractGasLimit,
|
|
294
288
|
bip44,
|
|
295
289
|
keys,
|
|
296
290
|
address,
|
|
@@ -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/gas-estimation.js
CHANGED
|
@@ -9,6 +9,8 @@ export const DEFAULT_GAS_LIMIT_MULTIPLIER = 1.29
|
|
|
9
9
|
const GAS_PER_NON_ZERO_BYTE = 16
|
|
10
10
|
|
|
11
11
|
export const DEFAULT_CONTRACT_GAS_LIMIT = 1e6
|
|
12
|
+
export const DEFAULT_TOKEN_GAS_LIMIT = 120e3
|
|
13
|
+
export const DEFAULT_GAS_LIMIT = 21_000
|
|
12
14
|
|
|
13
15
|
// HACK: If a recipient address is not defined, we usually fall back to
|
|
14
16
|
// default address so gas estimation can still complete successfully
|
|
@@ -107,6 +109,14 @@ export function resolveDefaultTxInput({ asset, toAddress, amount }) {
|
|
|
107
109
|
: '0x'
|
|
108
110
|
}
|
|
109
111
|
|
|
112
|
+
export const defaultGasLimit = ({ asset, txInput }) => {
|
|
113
|
+
const isToken = isEthereumLikeToken(asset)
|
|
114
|
+
return (
|
|
115
|
+
(isToken ? DEFAULT_TOKEN_GAS_LIMIT : DEFAULT_GAS_LIMIT) +
|
|
116
|
+
GAS_PER_NON_ZERO_BYTE * toBuffer(txInput).length
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
|
|
110
120
|
export async function fetchGasLimit({
|
|
111
121
|
asset,
|
|
112
122
|
feeData,
|
|
@@ -128,13 +138,12 @@ export async function fetchGasLimit({
|
|
|
128
138
|
const toAddress = providedToAddress ?? ARBITRARY_ADDRESS
|
|
129
139
|
const txInput = providedTxInput || resolveDefaultTxInput({ asset, toAddress, amount })
|
|
130
140
|
|
|
131
|
-
const
|
|
141
|
+
const isToken = isEthereumLikeToken(asset)
|
|
132
142
|
|
|
133
143
|
const isContract = await isContractAddressCached({ asset, address: toAddress })
|
|
134
144
|
|
|
135
|
-
const isToken = isEthereumLikeToken(asset)
|
|
136
145
|
if (!isToken && !isContract && !asset.forceGasLimitEstimation) {
|
|
137
|
-
return defaultGasLimit()
|
|
146
|
+
return defaultGasLimit({ asset, txInput })
|
|
138
147
|
}
|
|
139
148
|
|
|
140
149
|
const gasLimitMultiplier = await resolveGasLimitMultiplier({
|
|
@@ -173,10 +182,9 @@ export async function fetchGasLimit({
|
|
|
173
182
|
console.error('fetchGasLimit error', err)
|
|
174
183
|
|
|
175
184
|
// fallback value for contract case
|
|
176
|
-
if (isContract) return
|
|
185
|
+
if (isContract) return DEFAULT_CONTRACT_GAS_LIMIT
|
|
177
186
|
|
|
178
|
-
|
|
179
|
-
return defaultGasLimit()
|
|
187
|
+
return defaultGasLimit({ asset, txInput })
|
|
180
188
|
}
|
|
181
189
|
}
|
|
182
190
|
|
package/src/get-fee.js
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
getNormalizedFeeDataForCustomFee,
|
|
6
6
|
resolveGasPrice,
|
|
7
7
|
} from './fee-utils.js'
|
|
8
|
+
import { defaultGasLimit } from './gas-estimation.js'
|
|
8
9
|
|
|
9
10
|
// Move to meta?
|
|
10
11
|
const taxes = {
|
|
@@ -40,8 +41,8 @@ export const getFeeFactoryGasPrices = ({ customFee, feeData }) => {
|
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
export const getFeeFactory =
|
|
43
|
-
(
|
|
44
|
-
({ asset, feeData, customFee, gasLimit: providedGasLimit, amount }) => {
|
|
44
|
+
() =>
|
|
45
|
+
({ asset, feeData, customFee, txInput, gasLimit: providedGasLimit, amount }) => {
|
|
45
46
|
const {
|
|
46
47
|
feeData: { tipGasPrice, eip1559Enabled },
|
|
47
48
|
gasPrice,
|
|
@@ -50,7 +51,7 @@ export const getFeeFactory =
|
|
|
50
51
|
feeData,
|
|
51
52
|
})
|
|
52
53
|
|
|
53
|
-
const gasLimit = providedGasLimit
|
|
54
|
+
const gasLimit = providedGasLimit ?? defaultGasLimit({ asset, txInput })
|
|
54
55
|
|
|
55
56
|
// When explicitly opting into EIP-1559 transactions,
|
|
56
57
|
// lock in the `tipGasPrice` we used to compute the fees.
|
|
@@ -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
|
+
}
|