@exodus/ethereum-api 8.76.2 → 8.76.4
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 +18 -0
- package/package.json +2 -2
- package/src/create-asset-utils.js +4 -14
- package/src/create-asset.js +1 -1
- package/src/gas-estimation.js +10 -1
- package/src/move-funds.js +56 -23
- package/src/tx-create.js +143 -29
- package/src/tx-log/clarity-monitor-v2.js +0 -637
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,24 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
## [8.76.4](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.76.3...@exodus/ethereum-api@8.76.4) (2026-06-02)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
* fix(ethereum-api): eliminate send-all dust on HYPE and other forceGasLimitEstimation networks (#7747)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
## [8.76.3](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.76.2...@exodus/ethereum-api@8.76.3) (2026-06-01)
|
|
17
|
+
|
|
18
|
+
**Note:** Version bump only for package @exodus/ethereum-api
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
6
24
|
## [8.76.2](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.76.1...@exodus/ethereum-api@8.76.2) (2026-05-27)
|
|
7
25
|
|
|
8
26
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/ethereum-api",
|
|
3
|
-
"version": "8.76.
|
|
3
|
+
"version": "8.76.4",
|
|
4
4
|
"description": "Transaction monitors, fee monitors, RPC with the blockchain node, and other networking code for Ethereum and EVM-based blockchains",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -70,5 +70,5 @@
|
|
|
70
70
|
"type": "git",
|
|
71
71
|
"url": "git+https://github.com/ExodusMovement/assets.git"
|
|
72
72
|
},
|
|
73
|
-
"gitHead": "
|
|
73
|
+
"gitHead": "f0c007138096bf627e21fc9e757637394fe2a5b0"
|
|
74
74
|
}
|
|
@@ -5,10 +5,9 @@ import assert from 'minimalistic-assert'
|
|
|
5
5
|
import ms from 'ms'
|
|
6
6
|
|
|
7
7
|
import { EVM_ERROR_REASONS, withErrorReason } from './error-wrapper.js'
|
|
8
|
-
import { createEvmServer,
|
|
8
|
+
import { createEvmServer, ValidMonitorTypes } from './exodus-eth-server/index.js'
|
|
9
9
|
import { createEthereumHooks } from './hooks/index.js'
|
|
10
10
|
import { ClarityMonitor } from './tx-log/clarity-monitor.js'
|
|
11
|
-
import { ClarityMonitorV2 } from './tx-log/clarity-monitor-v2.js'
|
|
12
11
|
import { ClarityTruncatedHistoryMonitor } from './tx-log/clarity-truncated-history-monitor.js'
|
|
13
12
|
import { EthereumMonitor } from './tx-log/ethereum-monitor.js'
|
|
14
13
|
import { EthereumNoHistoryMonitor } from './tx-log/ethereum-no-history-monitor.js'
|
|
@@ -284,6 +283,9 @@ export const createHistoryMonitorFactory = ({
|
|
|
284
283
|
return (args) => {
|
|
285
284
|
let monitor
|
|
286
285
|
switch (monitorType) {
|
|
286
|
+
case 'clarity-v3':
|
|
287
|
+
console.log('clarity-v3 is no longer supported, falling back to clarity-v2')
|
|
288
|
+
// eslint-disable-next-line no-fallthrough
|
|
287
289
|
case 'clarity':
|
|
288
290
|
case 'clarity-v2':
|
|
289
291
|
monitor = new ClarityMonitor({
|
|
@@ -306,18 +308,6 @@ export const createHistoryMonitorFactory = ({
|
|
|
306
308
|
...args,
|
|
307
309
|
})
|
|
308
310
|
break
|
|
309
|
-
case 'clarity-v3':
|
|
310
|
-
monitor = new ClarityMonitorV2({
|
|
311
|
-
assetClientInterface,
|
|
312
|
-
interval: ms(monitorInterval || '5m'),
|
|
313
|
-
server,
|
|
314
|
-
rpcBalanceAssetNames,
|
|
315
|
-
wsGatewayClient: createWsGateway({ uri: wsGatewayUri }),
|
|
316
|
-
eip7702Supported,
|
|
317
|
-
getBlackListStatus,
|
|
318
|
-
...args,
|
|
319
|
-
})
|
|
320
|
-
break
|
|
321
311
|
case 'no-history':
|
|
322
312
|
monitor = new EthereumNoHistoryMonitor({
|
|
323
313
|
assetClientInterface,
|
package/src/create-asset.js
CHANGED
package/src/gas-estimation.js
CHANGED
|
@@ -119,6 +119,7 @@ export async function fetchGasLimit({
|
|
|
119
119
|
bip70,
|
|
120
120
|
txType = TX_TYPE_TRANSFER,
|
|
121
121
|
throwOnError = true,
|
|
122
|
+
isSendAll = false,
|
|
122
123
|
}) {
|
|
123
124
|
if (bip70?.bitpay?.data && bip70?.bitpay?.gasPrice) {
|
|
124
125
|
// from on chain stats https://dune.xyz/queries/189123
|
|
@@ -178,7 +179,15 @@ export async function fetchGasLimit({
|
|
|
178
179
|
})
|
|
179
180
|
|
|
180
181
|
const scaledGasLimitEstimate = scaleGasLimitEstimate({ estimatedGasLimit, gasLimitMultiplier })
|
|
181
|
-
if (!isToken)
|
|
182
|
+
if (!isToken) {
|
|
183
|
+
// For native send-all on EOA targets, return the raw `eth_estimateGas`
|
|
184
|
+
// result (no 2x safety pad). Combined with the EIP-1559 tip override
|
|
185
|
+
// applied in tx-create.js (also gated on `isSendAll && !isContract`),
|
|
186
|
+
// this is the no-dust path: gasLimit ≈ gasUsed, so nothing comes back
|
|
187
|
+
// as refund. For contracts we keep the safety pad because contract
|
|
188
|
+
// gas can be nondeterministic against estimates.
|
|
189
|
+
return isSendAll && !isContractTxToAddress ? estimatedGasLimit : scaledGasLimitEstimate
|
|
190
|
+
}
|
|
182
191
|
|
|
183
192
|
// NOTE: If we've enabled `fixGasLimit`s for a token,
|
|
184
193
|
// we need to make sure that transaction we're
|
package/src/move-funds.js
CHANGED
|
@@ -4,6 +4,7 @@ import assert from 'minimalistic-assert'
|
|
|
4
4
|
|
|
5
5
|
import { getNonce, getTokenBalanceFromNode } from './eth-like-util.js'
|
|
6
6
|
import { fetchGasLimit } from './gas-estimation.js'
|
|
7
|
+
import { AmountIncludesFeeUnderflowError } from './tx-create.js'
|
|
7
8
|
|
|
8
9
|
export const moveFundsFactory = ({ baseAssetName, assetClientInterface, createTx, server }) => {
|
|
9
10
|
assert(baseAssetName, 'baseAssetName is required')
|
|
@@ -73,37 +74,69 @@ export const moveFundsFactory = ({ baseAssetName, assetClientInterface, createTx
|
|
|
73
74
|
fromAddress,
|
|
74
75
|
toAddress,
|
|
75
76
|
amount,
|
|
77
|
+
// `isSendAll` and `amountIncludesFee` are no-ops on the token branch
|
|
78
|
+
// (both gated on `!isToken` / `!isEthereumLikeToken(asset)` downstream),
|
|
79
|
+
// so don't pass them. Keeps the API surface honest about which flags
|
|
80
|
+
// actually take effect for each branch.
|
|
81
|
+
...(isToken ? null : { isSendAll: true }),
|
|
76
82
|
})
|
|
77
83
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
84
|
+
let createTxResult
|
|
85
|
+
try {
|
|
86
|
+
createTxResult = await createTx({
|
|
87
|
+
asset,
|
|
88
|
+
walletAccount,
|
|
89
|
+
fromAddress,
|
|
90
|
+
// Native sweep: pass the full balance and let createTx subtract the
|
|
91
|
+
// accurate fee. Token sweep: pass the full token balance unchanged
|
|
92
|
+
// (the native gas fee is paid separately from native balance).
|
|
93
|
+
address: toAddress,
|
|
94
|
+
amount,
|
|
95
|
+
nonce,
|
|
96
|
+
gasLimit,
|
|
97
|
+
gasPrice: feeData.gasPrice,
|
|
98
|
+
// TODO: drop this. Move funds has no fee picker, so it's always
|
|
99
|
+
// `feeData.gasPrice` (same as `gasPrice` above) and a no-op anyway.
|
|
100
|
+
// If we ever need a custom fee, thread it through prepareSendFundsTx.
|
|
101
|
+
customFee: feeData.gasPrice,
|
|
102
|
+
// Both flags are no-ops for tokens (see fetchGasLimit call above).
|
|
103
|
+
...(isToken ? null : { isSendAll: true, amountIncludesFee: true }),
|
|
104
|
+
})
|
|
105
|
+
} catch (err) {
|
|
106
|
+
// createTx throws `AmountIncludesFeeUnderflowError` for the native
|
|
107
|
+
// sweep when the accurate fee (including OP-stack L1 data fee) exceeds
|
|
108
|
+
// the balance we passed in. Translate to the typed MoveFundsError so
|
|
109
|
+
// consumers (mobile/desktop MoveFunds screens) render the right copy.
|
|
110
|
+
if (!isToken && err instanceof AmountIncludesFeeUnderflowError) {
|
|
87
111
|
throw new MoveFundsError('balance-negative', { fromAddress })
|
|
88
112
|
}
|
|
89
113
|
|
|
90
|
-
|
|
114
|
+
throw err
|
|
91
115
|
}
|
|
92
116
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
gasPrice: feeData.gasPrice,
|
|
102
|
-
customFee: feeData.gasPrice,
|
|
103
|
-
isSendAll: true,
|
|
104
|
-
})
|
|
117
|
+
// Token sweep equivalent of the native underflow gate: createTx doesn't
|
|
118
|
+
// validate ETH balance vs gas for token transfers, so check it here using
|
|
119
|
+
// the authoritative fee returned from `calculateFee`. This catches the
|
|
120
|
+
// OP-stack edge case where balance covers `gasPrice * gasLimit` but not
|
|
121
|
+
// the additional L1 data fee.
|
|
122
|
+
if (isToken && ethBalance.sub(createTxResult.fee).isNegative) {
|
|
123
|
+
throw new MoveFundsError('token-fee-insufficient', { fromAddress })
|
|
124
|
+
}
|
|
105
125
|
|
|
106
|
-
|
|
126
|
+
// Trust createTx for both amount and fee. For native sweep `amount` is
|
|
127
|
+
// the post-subtraction value (input balance minus accurate fee). For
|
|
128
|
+
// token sweep `amount` is the input token balance unchanged, since we
|
|
129
|
+
// don't pass `amountIncludesFee`. In both cases `fee` includes the
|
|
130
|
+
// OP-stack L1 data fee from `calculateFee`, which matters more for
|
|
131
|
+
// tokens than for native because ERC-20 calldata is larger.
|
|
132
|
+
return {
|
|
133
|
+
fromAddress,
|
|
134
|
+
toAddress,
|
|
135
|
+
amount: createTxResult.amount,
|
|
136
|
+
fee: createTxResult.fee,
|
|
137
|
+
privateKey,
|
|
138
|
+
unsignedTx: createTxResult.unsignedTx,
|
|
139
|
+
}
|
|
107
140
|
}
|
|
108
141
|
|
|
109
142
|
const sendFunds = async ({ privateKey, unsignedTx }) => {
|
package/src/tx-create.js
CHANGED
|
@@ -20,6 +20,62 @@ import {
|
|
|
20
20
|
TX_TYPE_TRANSFER,
|
|
21
21
|
} from './tx-type/index.js'
|
|
22
22
|
|
|
23
|
+
// Thrown by `resolveAmountIncludesFeeTxAttributes` when the fee subtraction
|
|
24
|
+
// implied by `amountIncludesFee` would drive the resulting tx value below
|
|
25
|
+
// zero (i.e. the caller's balance can't cover the computed fee). Exported so
|
|
26
|
+
// callers like MoveFunds can `instanceof`-check and translate it into their
|
|
27
|
+
// own user-facing error type without string-matching the message.
|
|
28
|
+
export class AmountIncludesFeeUnderflowError extends Error {
|
|
29
|
+
constructor() {
|
|
30
|
+
super('transaction gas cost exceeds fee-inclusive amount')
|
|
31
|
+
this.name = 'AmountIncludesFeeUnderflowError'
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function calculateFee({
|
|
36
|
+
asset,
|
|
37
|
+
chainId,
|
|
38
|
+
eip1559Enabled,
|
|
39
|
+
gasLimit,
|
|
40
|
+
gasPrice,
|
|
41
|
+
tipGasPrice,
|
|
42
|
+
nonce,
|
|
43
|
+
txInput,
|
|
44
|
+
txToAddress,
|
|
45
|
+
txValue,
|
|
46
|
+
}) {
|
|
47
|
+
const ethjsTx = createEthereumJsTx({
|
|
48
|
+
txData: {
|
|
49
|
+
nonce,
|
|
50
|
+
gasPrice: currency2buffer(gasPrice),
|
|
51
|
+
tipGasPrice: tipGasPrice ? currency2buffer(tipGasPrice) : undefined,
|
|
52
|
+
gasLimit,
|
|
53
|
+
to: txToAddress,
|
|
54
|
+
value: currency2buffer(txValue),
|
|
55
|
+
data: txInput,
|
|
56
|
+
chainId,
|
|
57
|
+
},
|
|
58
|
+
txMeta: {
|
|
59
|
+
eip1559Enabled,
|
|
60
|
+
},
|
|
61
|
+
})
|
|
62
|
+
const transactionBuffer = ethjsTx.serialize()
|
|
63
|
+
|
|
64
|
+
const baseFee = gasPrice.mul(gasLimit)
|
|
65
|
+
const optimismL1DataFee = asset.baseAsset.estimateL1DataFee
|
|
66
|
+
? await asset.baseAsset.estimateL1DataFee({
|
|
67
|
+
unsignedTx: { txData: { transactionBuffer, chainId } },
|
|
68
|
+
})
|
|
69
|
+
: undefined
|
|
70
|
+
|
|
71
|
+
const l1DataFee = optimismL1DataFee
|
|
72
|
+
? asset.baseAsset.currency.baseUnit(optimismL1DataFee)
|
|
73
|
+
: asset.baseAsset.currency.ZERO
|
|
74
|
+
|
|
75
|
+
const fee = baseFee.add(l1DataFee)
|
|
76
|
+
return { fee, transactionBuffer }
|
|
77
|
+
}
|
|
78
|
+
|
|
23
79
|
async function createUnsignedTxWithFees({
|
|
24
80
|
asset,
|
|
25
81
|
chainId,
|
|
@@ -57,35 +113,19 @@ async function createUnsignedTxWithFees({
|
|
|
57
113
|
isContractTxToAddress,
|
|
58
114
|
})
|
|
59
115
|
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
txMeta: {
|
|
72
|
-
eip1559Enabled,
|
|
73
|
-
},
|
|
116
|
+
const { fee, transactionBuffer } = await calculateFee({
|
|
117
|
+
asset,
|
|
118
|
+
chainId,
|
|
119
|
+
eip1559Enabled,
|
|
120
|
+
gasLimit,
|
|
121
|
+
gasPrice,
|
|
122
|
+
tipGasPrice,
|
|
123
|
+
nonce,
|
|
124
|
+
txInput,
|
|
125
|
+
txToAddress,
|
|
126
|
+
txValue,
|
|
74
127
|
})
|
|
75
|
-
const transactionBuffer = ethjsTx.serialize()
|
|
76
128
|
|
|
77
|
-
const baseFee = gasPrice.mul(gasLimit)
|
|
78
|
-
const optimismL1DataFee = asset.baseAsset.estimateL1DataFee
|
|
79
|
-
? await asset.baseAsset.estimateL1DataFee({
|
|
80
|
-
unsignedTx: { txData: { transactionBuffer, chainId } },
|
|
81
|
-
})
|
|
82
|
-
: undefined
|
|
83
|
-
|
|
84
|
-
const l1DataFee = optimismL1DataFee
|
|
85
|
-
? asset.baseAsset.currency.baseUnit(optimismL1DataFee)
|
|
86
|
-
: asset.baseAsset.currency.ZERO
|
|
87
|
-
|
|
88
|
-
const fee = baseFee.add(l1DataFee)
|
|
89
129
|
const extraFeeData = getExtraFeeData({ asset, amount, txValue })
|
|
90
130
|
const unsignedTx = {
|
|
91
131
|
txData: { transactionBuffer, chainId },
|
|
@@ -102,6 +142,7 @@ async function createUnsignedTxWithFees({
|
|
|
102
142
|
return {
|
|
103
143
|
unsignedTx,
|
|
104
144
|
fee,
|
|
145
|
+
amount,
|
|
105
146
|
extraFeeData,
|
|
106
147
|
// exhcange compatibility until the use usignedTx, remove me!
|
|
107
148
|
gasPrice,
|
|
@@ -110,6 +151,60 @@ async function createUnsignedTxWithFees({
|
|
|
110
151
|
}
|
|
111
152
|
}
|
|
112
153
|
|
|
154
|
+
// TODO: Move this into tx attribute resolution once gasLimit is resolved there.
|
|
155
|
+
// `amountIncludesFee` changes the tx value, so the fee subtraction should happen
|
|
156
|
+
// in the same flow that finalizes amount, txValue, txInput, txToAddress, and
|
|
157
|
+
// gasLimit.
|
|
158
|
+
async function resolveAmountIncludesFeeTxAttributes({
|
|
159
|
+
amountIncludesFee,
|
|
160
|
+
asset,
|
|
161
|
+
chainId,
|
|
162
|
+
eip1559Enabled,
|
|
163
|
+
gasLimit,
|
|
164
|
+
gasPrice,
|
|
165
|
+
tipGasPrice,
|
|
166
|
+
txAttributes,
|
|
167
|
+
}) {
|
|
168
|
+
const { amount, nonce, txInput, txToAddress, txValue } = txAttributes
|
|
169
|
+
|
|
170
|
+
// `amountIncludesFee` is a hint: subtract any fee that is paid in the same
|
|
171
|
+
// currency as `amount`. On EVM today, gas is paid in the native asset, so
|
|
172
|
+
// for ERC-20 sends and zero-value calldata (i.e approve()) flows there is nothing in the
|
|
173
|
+
// amount's currency to subtract. Pass through silently in those cases, so
|
|
174
|
+
// consumers can set this flag based on intent without needing to know
|
|
175
|
+
// whether the asset is a token or whether the call carries zero native
|
|
176
|
+
// value.
|
|
177
|
+
if (!amountIncludesFee || isEthereumLikeToken(asset) || amount.isZero) {
|
|
178
|
+
return { txAttributes }
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const { fee } = await calculateFee({
|
|
182
|
+
asset,
|
|
183
|
+
chainId,
|
|
184
|
+
eip1559Enabled,
|
|
185
|
+
gasLimit,
|
|
186
|
+
gasPrice,
|
|
187
|
+
tipGasPrice,
|
|
188
|
+
nonce,
|
|
189
|
+
txInput,
|
|
190
|
+
txToAddress,
|
|
191
|
+
txValue,
|
|
192
|
+
})
|
|
193
|
+
const reducedAmount = amount.sub(fee)
|
|
194
|
+
|
|
195
|
+
if (!reducedAmount.gte(asset.currency.ZERO)) {
|
|
196
|
+
throw new AmountIncludesFeeUnderflowError()
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
txAttributes: {
|
|
201
|
+
...txAttributes,
|
|
202
|
+
amount: reducedAmount,
|
|
203
|
+
txValue: reducedAmount,
|
|
204
|
+
},
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
113
208
|
const resolveTxFactoryGasPrices = async ({
|
|
114
209
|
assetClientInterface,
|
|
115
210
|
baseAsset,
|
|
@@ -384,7 +479,12 @@ export const createTxFactory = ({ chainId, assetClientInterface, useAbsoluteNonc
|
|
|
384
479
|
gasPrice: providedGasPrice,
|
|
385
480
|
bip70,
|
|
386
481
|
customFee: providedCustomFee,
|
|
482
|
+
// Intent flag: the user is trying to empty the wallet. Used for send-all-only
|
|
483
|
+
// dust behavior such as the EIP-1559 tip override.
|
|
387
484
|
isSendAll,
|
|
485
|
+
// Amount semantics flag: the provided amount includes the fee budget, so the
|
|
486
|
+
// final tx value should be reduced by the computed fee.
|
|
487
|
+
amountIncludesFee = false,
|
|
388
488
|
bumpTxId,
|
|
389
489
|
}) => {
|
|
390
490
|
assert(asset, 'asset is required')
|
|
@@ -520,6 +620,7 @@ export const createTxFactory = ({ chainId, assetClientInterface, useAbsoluteNonc
|
|
|
520
620
|
bip70,
|
|
521
621
|
amount: resolvedTxAttributes.amount,
|
|
522
622
|
txType,
|
|
623
|
+
isSendAll,
|
|
523
624
|
}))
|
|
524
625
|
|
|
525
626
|
// HACK: We cannot ensure the no dust invariant for `isSendAll`
|
|
@@ -532,8 +633,21 @@ export const createTxFactory = ({ chainId, assetClientInterface, useAbsoluteNonc
|
|
|
532
633
|
const isSendAllBaseAsset =
|
|
533
634
|
isSendAll && !isEthereumLikeToken(asset) && !resolvedTxAttributes.isContractTxToAddress
|
|
534
635
|
|
|
636
|
+
const finalTipGasPrice = isSendAllBaseAsset && eip1559Enabled ? gasPrice : tipGasPrice
|
|
637
|
+
|
|
638
|
+
const { txAttributes: finalTxAttributes } = await resolveAmountIncludesFeeTxAttributes({
|
|
639
|
+
amountIncludesFee,
|
|
640
|
+
asset,
|
|
641
|
+
chainId,
|
|
642
|
+
eip1559Enabled,
|
|
643
|
+
gasLimit,
|
|
644
|
+
gasPrice,
|
|
645
|
+
tipGasPrice: finalTipGasPrice,
|
|
646
|
+
txAttributes: resolvedTxAttributes,
|
|
647
|
+
})
|
|
648
|
+
|
|
535
649
|
return createUnsignedTxWithFees({
|
|
536
|
-
...
|
|
650
|
+
...finalTxAttributes,
|
|
537
651
|
asset,
|
|
538
652
|
chainId,
|
|
539
653
|
gasLimit,
|
|
@@ -545,7 +659,7 @@ export const createTxFactory = ({ chainId, assetClientInterface, useAbsoluteNonc
|
|
|
545
659
|
// fixed gas cost transaction, no dust balance should remain,
|
|
546
660
|
// since any deviation in the underlying `baseFeePerGas` will
|
|
547
661
|
// result only affect the tip for the miner - no dust remains.
|
|
548
|
-
tipGasPrice:
|
|
662
|
+
tipGasPrice: finalTipGasPrice,
|
|
549
663
|
eip1559Enabled,
|
|
550
664
|
})
|
|
551
665
|
}
|
|
@@ -1,637 +0,0 @@
|
|
|
1
|
-
import { BaseMonitor } from '@exodus/asset-lib'
|
|
2
|
-
import { getAssetAddresses } from '@exodus/ethereum-lib'
|
|
3
|
-
import lodash from 'lodash'
|
|
4
|
-
import assert from 'minimalistic-assert'
|
|
5
|
-
|
|
6
|
-
import WsGateway from '../exodus-eth-server/ws-gateway.js'
|
|
7
|
-
import { executeEthLikeFeeMonitorUpdate } from '../fee-utils.js'
|
|
8
|
-
import { fromHexToString } from '../number-utils.js'
|
|
9
|
-
import {
|
|
10
|
-
filterEffects,
|
|
11
|
-
getLogItemsFromServerTx,
|
|
12
|
-
normalizeTransactionsResponse,
|
|
13
|
-
} from './clarity-utils/index.js'
|
|
14
|
-
import {
|
|
15
|
-
checkPendingTransactions,
|
|
16
|
-
excludeUnchangedTokenBalances,
|
|
17
|
-
getAllLogItemsByAsset,
|
|
18
|
-
getCurrentBlackListStatus,
|
|
19
|
-
getCurrentEIP7702Delegation,
|
|
20
|
-
getDeriveDataNeededForTick,
|
|
21
|
-
getDeriveTransactionsToCheck,
|
|
22
|
-
verifyRpcPendingTxStatusBatch,
|
|
23
|
-
} from './monitor-utils/index.js'
|
|
24
|
-
|
|
25
|
-
const { isEmpty } = lodash
|
|
26
|
-
|
|
27
|
-
export class ClarityMonitorV2 extends BaseMonitor {
|
|
28
|
-
#wsClient = null
|
|
29
|
-
#walletAccountByAddress = new Map()
|
|
30
|
-
#walletAccountInfo = new Map()
|
|
31
|
-
#rpcBalanceAssetNames = []
|
|
32
|
-
constructor({
|
|
33
|
-
server,
|
|
34
|
-
wsGatewayClient,
|
|
35
|
-
rpcBalanceAssetNames,
|
|
36
|
-
eip7702Supported,
|
|
37
|
-
getBlackListStatus,
|
|
38
|
-
config,
|
|
39
|
-
...args
|
|
40
|
-
} = {}) {
|
|
41
|
-
super(args)
|
|
42
|
-
assert(wsGatewayClient instanceof WsGateway, 'expected WsGateway wsGatewayClient')
|
|
43
|
-
|
|
44
|
-
this.config = { GAS_PRICE_FROM_WEBSOCKET: true, ...config }
|
|
45
|
-
this.server = server
|
|
46
|
-
this.#wsClient = wsGatewayClient
|
|
47
|
-
this.#rpcBalanceAssetNames = rpcBalanceAssetNames
|
|
48
|
-
this.eip7702Supported = eip7702Supported
|
|
49
|
-
this.getBlackListStatus = getBlackListStatus
|
|
50
|
-
this.getAllLogItemsByAsset = getAllLogItemsByAsset
|
|
51
|
-
this.deriveDataNeededForTick = getDeriveDataNeededForTick(this.aci)
|
|
52
|
-
this.deriveTransactionsToCheck = getDeriveTransactionsToCheck({
|
|
53
|
-
getTxLog: (...args) => this.aci.getTxLog(...args),
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
this.addHook('before-start', (...args) => this.beforeStart(...args))
|
|
57
|
-
this.addHook('after-stop', (...args) => this.afterStop(...args))
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
setServer(config) {
|
|
61
|
-
const uri = config?.server || this.server.defaultUri
|
|
62
|
-
|
|
63
|
-
this.#wsClient.on('connected', () => this.subscribeAllWalletAccounts())
|
|
64
|
-
this.#wsClient.start()
|
|
65
|
-
|
|
66
|
-
if (uri === this.server.uri) {
|
|
67
|
-
return
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
this.server.setURI(uri)
|
|
71
|
-
if (this.config.GAS_PRICE_FROM_WEBSOCKET) {
|
|
72
|
-
this.server.connectFee()
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
async deriveData({ assetName, walletAccount, tokens }) {
|
|
77
|
-
const { ourWalletAddress, currentAccountState } = await this.deriveDataNeededForTick({
|
|
78
|
-
assetName,
|
|
79
|
-
walletAccount,
|
|
80
|
-
})
|
|
81
|
-
const transactionsToCheck = await this.deriveTransactionsToCheck({
|
|
82
|
-
assetName,
|
|
83
|
-
walletAccount,
|
|
84
|
-
tokens,
|
|
85
|
-
ourWalletAddress,
|
|
86
|
-
})
|
|
87
|
-
|
|
88
|
-
return {
|
|
89
|
-
ourWalletAddress,
|
|
90
|
-
currentAccountState,
|
|
91
|
-
...transactionsToCheck,
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// eslint-disable-next-line no-undef
|
|
96
|
-
async checkPendingTransactions(params) {
|
|
97
|
-
const { pendingTransactionsToCheck, pendingTransactionsGroupedByAddressAndNonce } =
|
|
98
|
-
checkPendingTransactions(params)
|
|
99
|
-
const txsToRemove = []
|
|
100
|
-
const { walletAccount } = params
|
|
101
|
-
|
|
102
|
-
const updateTx = (tx, asset, { error, remove }) => {
|
|
103
|
-
if (remove) {
|
|
104
|
-
txsToRemove.push({ tx, assetSource: { asset, walletAccount } })
|
|
105
|
-
} else {
|
|
106
|
-
params.logItemsByAsset[asset].push({
|
|
107
|
-
...tx,
|
|
108
|
-
dropped: true,
|
|
109
|
-
error,
|
|
110
|
-
})
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// in case this is an ETH fee tx that has associated ERC20 send txs
|
|
114
|
-
const promises = tx.tokens.map(async (assetName) => {
|
|
115
|
-
const tokenTxSet = await this.aci.getTxLog({ assetName, walletAccount })
|
|
116
|
-
if (remove) {
|
|
117
|
-
txsToRemove.push({
|
|
118
|
-
tx: tokenTxSet.get(tx.txId),
|
|
119
|
-
assetSource: { asset: assetName, walletAccount },
|
|
120
|
-
})
|
|
121
|
-
} else if (tokenTxSet && tokenTxSet.has(tx.txId)) {
|
|
122
|
-
params.logItemsByAsset[assetName].push({
|
|
123
|
-
...tokenTxSet.get(tx.txId),
|
|
124
|
-
error,
|
|
125
|
-
dropped: true,
|
|
126
|
-
})
|
|
127
|
-
}
|
|
128
|
-
})
|
|
129
|
-
return Promise.all(promises)
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
for (const { tx, assetName, replaced = false } of Object.values(
|
|
133
|
-
pendingTransactionsGroupedByAddressAndNonce
|
|
134
|
-
)) {
|
|
135
|
-
if (replaced) {
|
|
136
|
-
await updateTx(tx, assetName, { remove: true })
|
|
137
|
-
delete pendingTransactionsToCheck[tx.txId]
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// Batch verify all pending txs with a single RPC call (skip if refresh)
|
|
142
|
-
const txIds = Object.keys(pendingTransactionsToCheck)
|
|
143
|
-
const statuses = params.refresh
|
|
144
|
-
? {}
|
|
145
|
-
: await verifyRpcPendingTxStatusBatch({
|
|
146
|
-
server: this.server,
|
|
147
|
-
logger: this.logger,
|
|
148
|
-
txIds,
|
|
149
|
-
})
|
|
150
|
-
|
|
151
|
-
for (const { tx, assetName } of Object.values(pendingTransactionsToCheck)) {
|
|
152
|
-
if (params.refresh) {
|
|
153
|
-
await updateTx(tx, assetName, { remove: true })
|
|
154
|
-
} else {
|
|
155
|
-
const txStatus = statuses[tx.txId]
|
|
156
|
-
if (txStatus?.status === 'dropped') {
|
|
157
|
-
await updateTx(tx, assetName, { error: 'Dropped' })
|
|
158
|
-
}
|
|
159
|
-
// status === 'confirmed' or 'pending' - tx is fine, wait for Clarity to confirm
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
return { txsToRemove }
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
async persistSecurityState({ walletAccount, accountState, isBlacklisted, eip7702Delegation }) {
|
|
167
|
-
const securityStatePatch = {
|
|
168
|
-
...(isBlacklisted !== undefined && { isBlacklisted }),
|
|
169
|
-
...(eip7702Delegation !== undefined && { eip7702Delegation }),
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
if (isEmpty(securityStatePatch)) {
|
|
173
|
-
return
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
await this.updateAccountState({
|
|
177
|
-
walletAccount,
|
|
178
|
-
accountState,
|
|
179
|
-
newData: securityStatePatch,
|
|
180
|
-
})
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
async tick({ walletAccount, refresh }) {
|
|
184
|
-
await this.subscribeWalletAddresses(walletAccount)
|
|
185
|
-
|
|
186
|
-
const walletAccountInfo = this.#walletAccountInfo.get(walletAccount)
|
|
187
|
-
|
|
188
|
-
if (!walletAccountInfo) {
|
|
189
|
-
return this.logger.warn('walletAccountInfo is empty', { walletAccount })
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
const { derivedData, tokensByAddress, assets, tokens, assetName } = walletAccountInfo
|
|
193
|
-
const { eip7702Delegation, isBlacklisted } = await this.getSecurityAccountState({
|
|
194
|
-
derivedData,
|
|
195
|
-
walletAccount,
|
|
196
|
-
})
|
|
197
|
-
|
|
198
|
-
await this.persistSecurityState({
|
|
199
|
-
walletAccount,
|
|
200
|
-
accountState: derivedData.currentAccountState,
|
|
201
|
-
isBlacklisted,
|
|
202
|
-
eip7702Delegation,
|
|
203
|
-
})
|
|
204
|
-
|
|
205
|
-
const accountState = await this.getNewAccountState({
|
|
206
|
-
tokens,
|
|
207
|
-
currentTokenBalances: derivedData.currentAccountState?.tokenBalances,
|
|
208
|
-
ourWalletAddress: derivedData.ourWalletAddress,
|
|
209
|
-
})
|
|
210
|
-
|
|
211
|
-
const batch = this.aci.createOperationsBatch()
|
|
212
|
-
const newData = { ...accountState }
|
|
213
|
-
let allTxs = []
|
|
214
|
-
let hasNewTxs = false
|
|
215
|
-
let historyError
|
|
216
|
-
|
|
217
|
-
try {
|
|
218
|
-
const response = await this.getHistoryFromServer({ walletAccount, derivedData, refresh })
|
|
219
|
-
|
|
220
|
-
;({ allTxs } = await normalizeTransactionsResponse({
|
|
221
|
-
asset: this.asset,
|
|
222
|
-
fromAddress: derivedData.ourWalletAddress,
|
|
223
|
-
response,
|
|
224
|
-
walletAccount,
|
|
225
|
-
}))
|
|
226
|
-
|
|
227
|
-
hasNewTxs = allTxs.length > 0
|
|
228
|
-
|
|
229
|
-
const logItemsByAsset = this.getAllLogItemsByAsset({
|
|
230
|
-
getLogItemsFromServerTx,
|
|
231
|
-
ourWalletAddress: derivedData.ourWalletAddress,
|
|
232
|
-
allTransactionsFromServer: allTxs,
|
|
233
|
-
asset: this.asset,
|
|
234
|
-
tokensByAddress,
|
|
235
|
-
assets,
|
|
236
|
-
})
|
|
237
|
-
|
|
238
|
-
const { txsToRemove } = await this.checkPendingTransactions({
|
|
239
|
-
txlist: allTxs,
|
|
240
|
-
walletAccount,
|
|
241
|
-
refresh,
|
|
242
|
-
logItemsByAsset,
|
|
243
|
-
asset: this.asset,
|
|
244
|
-
...derivedData,
|
|
245
|
-
})
|
|
246
|
-
|
|
247
|
-
this.aci.removeTxLogBatch({
|
|
248
|
-
assetName,
|
|
249
|
-
walletAccount,
|
|
250
|
-
txs: txsToRemove,
|
|
251
|
-
batch,
|
|
252
|
-
})
|
|
253
|
-
|
|
254
|
-
for (const [assetName, txs] of Object.entries(logItemsByAsset)) {
|
|
255
|
-
this.aci.updateTxLogAndNotifyBatch({
|
|
256
|
-
assetName,
|
|
257
|
-
walletAccount,
|
|
258
|
-
txs,
|
|
259
|
-
refresh,
|
|
260
|
-
batch,
|
|
261
|
-
})
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
if (response.cursor) {
|
|
265
|
-
newData.clarityCursor = response.cursor
|
|
266
|
-
}
|
|
267
|
-
} catch (error) {
|
|
268
|
-
historyError = error
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
try {
|
|
272
|
-
this.aci.updateAccountStateBatch({
|
|
273
|
-
assetName,
|
|
274
|
-
walletAccount,
|
|
275
|
-
accountState,
|
|
276
|
-
newData,
|
|
277
|
-
batch,
|
|
278
|
-
})
|
|
279
|
-
|
|
280
|
-
await this.aci.executeOperationsBatch(batch)
|
|
281
|
-
} catch (batchError) {
|
|
282
|
-
if (!historyError) throw batchError
|
|
283
|
-
this.logger.warn('error persisting account state after history failure', batchError)
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
if (historyError) {
|
|
287
|
-
throw historyError
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
if (refresh || hasNewTxs) {
|
|
291
|
-
const unknownTokenAddresses = this.getUnknownTokenAddresses({
|
|
292
|
-
transactions: allTxs,
|
|
293
|
-
tokensByAddress,
|
|
294
|
-
})
|
|
295
|
-
if (unknownTokenAddresses.length > 0) {
|
|
296
|
-
this.emit('unknown-tokens', unknownTokenAddresses)
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
async processAndFillTransactionsToState({
|
|
302
|
-
allTxs,
|
|
303
|
-
derivedData,
|
|
304
|
-
tokensByAddress,
|
|
305
|
-
assets,
|
|
306
|
-
tokens,
|
|
307
|
-
assetName,
|
|
308
|
-
walletAccount,
|
|
309
|
-
refresh,
|
|
310
|
-
cursor,
|
|
311
|
-
}) {
|
|
312
|
-
const hasNewTxs = allTxs.length > 0
|
|
313
|
-
|
|
314
|
-
const { eip7702Delegation, isBlacklisted } = await this.getSecurityAccountState({
|
|
315
|
-
derivedData,
|
|
316
|
-
walletAccount,
|
|
317
|
-
})
|
|
318
|
-
|
|
319
|
-
await this.persistSecurityState({
|
|
320
|
-
walletAccount,
|
|
321
|
-
accountState: derivedData.currentAccountState,
|
|
322
|
-
isBlacklisted,
|
|
323
|
-
eip7702Delegation,
|
|
324
|
-
})
|
|
325
|
-
|
|
326
|
-
const accountState = await this.getNewAccountState({
|
|
327
|
-
tokens,
|
|
328
|
-
currentTokenBalances: derivedData.currentAccountState?.tokenBalances,
|
|
329
|
-
ourWalletAddress: derivedData.ourWalletAddress,
|
|
330
|
-
})
|
|
331
|
-
|
|
332
|
-
const logItemsByAsset = this.getAllLogItemsByAsset({
|
|
333
|
-
getLogItemsFromServerTx,
|
|
334
|
-
ourWalletAddress: derivedData.ourWalletAddress,
|
|
335
|
-
allTransactionsFromServer: allTxs,
|
|
336
|
-
asset: this.asset,
|
|
337
|
-
tokensByAddress,
|
|
338
|
-
assets,
|
|
339
|
-
})
|
|
340
|
-
|
|
341
|
-
const { txsToRemove } = await this.checkPendingTransactions({
|
|
342
|
-
txlist: allTxs,
|
|
343
|
-
walletAccount,
|
|
344
|
-
refresh,
|
|
345
|
-
logItemsByAsset,
|
|
346
|
-
asset: this.asset,
|
|
347
|
-
...derivedData,
|
|
348
|
-
})
|
|
349
|
-
|
|
350
|
-
const batch = this.aci.createOperationsBatch()
|
|
351
|
-
|
|
352
|
-
this.aci.removeTxLogBatch({
|
|
353
|
-
assetName,
|
|
354
|
-
walletAccount,
|
|
355
|
-
txs: txsToRemove,
|
|
356
|
-
batch,
|
|
357
|
-
})
|
|
358
|
-
|
|
359
|
-
for (const [assetName, txs] of Object.entries(logItemsByAsset)) {
|
|
360
|
-
this.aci.updateTxLogAndNotifyBatch({
|
|
361
|
-
assetName,
|
|
362
|
-
walletAccount,
|
|
363
|
-
txs,
|
|
364
|
-
refresh,
|
|
365
|
-
batch,
|
|
366
|
-
})
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
// All updates must go through newData (accountState param is only used for mem merging)
|
|
370
|
-
const newData = { ...accountState }
|
|
371
|
-
|
|
372
|
-
if (cursor) {
|
|
373
|
-
newData.clarityCursor = cursor
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
this.aci.updateAccountStateBatch({
|
|
377
|
-
assetName,
|
|
378
|
-
walletAccount,
|
|
379
|
-
accountState,
|
|
380
|
-
newData,
|
|
381
|
-
batch,
|
|
382
|
-
})
|
|
383
|
-
|
|
384
|
-
await this.aci.executeOperationsBatch(batch)
|
|
385
|
-
|
|
386
|
-
if (refresh || hasNewTxs) {
|
|
387
|
-
const unknownTokenAddresses = this.getUnknownTokenAddresses({
|
|
388
|
-
transactions: allTxs,
|
|
389
|
-
tokensByAddress,
|
|
390
|
-
})
|
|
391
|
-
if (unknownTokenAddresses.length > 0) {
|
|
392
|
-
this.emit('unknown-tokens', unknownTokenAddresses)
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
async getSecurityAccountState({ derivedData, walletAccount }) {
|
|
398
|
-
const shouldCheckBlacklist = this.tickCount[walletAccount] === 0
|
|
399
|
-
const eip7702Delegation = await getCurrentEIP7702Delegation({
|
|
400
|
-
server: this.server,
|
|
401
|
-
address: derivedData.ourWalletAddress,
|
|
402
|
-
eip7702Supported: this.eip7702Supported,
|
|
403
|
-
currentDelegation: derivedData.currentAccountState?.eip7702Delegation,
|
|
404
|
-
logger: this.logger,
|
|
405
|
-
})
|
|
406
|
-
const isBlacklisted = shouldCheckBlacklist
|
|
407
|
-
? await getCurrentBlackListStatus({
|
|
408
|
-
getBlackListStatus: this.getBlackListStatus,
|
|
409
|
-
address: derivedData.ourWalletAddress,
|
|
410
|
-
currentIsBlacklisted: derivedData.currentAccountState?.isBlacklisted,
|
|
411
|
-
logger: this.logger,
|
|
412
|
-
})
|
|
413
|
-
: undefined
|
|
414
|
-
|
|
415
|
-
return { eip7702Delegation, isBlacklisted }
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
async addSingleTx({ tx, address, cursor }) {
|
|
419
|
-
const walletAccounts = this.#walletAccountByAddress.get(address)
|
|
420
|
-
|
|
421
|
-
if (!walletAccounts || walletAccounts.length === 0) {
|
|
422
|
-
return
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
for (const walletAccount of walletAccounts) {
|
|
426
|
-
const walletAccountInfo = this.#walletAccountInfo.get(walletAccount)
|
|
427
|
-
|
|
428
|
-
if (!walletAccountInfo) {
|
|
429
|
-
continue
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
const { derivedData, tokensByAddress, assets, tokens, assetName } = walletAccountInfo
|
|
433
|
-
|
|
434
|
-
await this.processAndFillTransactionsToState({
|
|
435
|
-
allTxs: [tx],
|
|
436
|
-
derivedData,
|
|
437
|
-
tokensByAddress,
|
|
438
|
-
assets,
|
|
439
|
-
tokens,
|
|
440
|
-
assetName,
|
|
441
|
-
walletAccount,
|
|
442
|
-
refresh: false,
|
|
443
|
-
cursor,
|
|
444
|
-
})
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
async getNewAccountState({ tokens, currentTokenBalances, ourWalletAddress }) {
|
|
449
|
-
const asset = this.asset
|
|
450
|
-
const newAccountState = Object.create(null)
|
|
451
|
-
const balances = await this.getBalances({ tokens, ourWalletAddress })
|
|
452
|
-
if (this.#rpcBalanceAssetNames.includes(asset.name)) {
|
|
453
|
-
const balance = balances[asset.name]
|
|
454
|
-
newAccountState.balance = asset.currency.baseUnit(balance)
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
const tokenBalancePairs = Object.entries(balances).filter((entry) => entry[0] !== asset.name)
|
|
458
|
-
const tokenBalanceEntries = tokenBalancePairs
|
|
459
|
-
.map((pair) => {
|
|
460
|
-
const token = tokens.find((token) => token.name === pair[0])
|
|
461
|
-
const value = token.currency.baseUnit(pair[1] || 0)
|
|
462
|
-
return [token.name, value]
|
|
463
|
-
})
|
|
464
|
-
.filter(Boolean)
|
|
465
|
-
|
|
466
|
-
const tokenBalances = excludeUnchangedTokenBalances(currentTokenBalances, tokenBalanceEntries)
|
|
467
|
-
if (!isEmpty(tokenBalances)) newAccountState.tokenBalances = tokenBalances
|
|
468
|
-
return newAccountState
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
async getReceiveAddressesByWalletAccount() {
|
|
472
|
-
const walletAccounts = await this.aci.getWalletAccounts({ assetName: this.asset.name })
|
|
473
|
-
const addressesByAccount = Object.create(null)
|
|
474
|
-
for (const walletAccount of walletAccounts) {
|
|
475
|
-
addressesByAccount[walletAccount] = await this.aci.getReceiveAddresses({
|
|
476
|
-
assetName: this.asset.name,
|
|
477
|
-
walletAccount,
|
|
478
|
-
useCache: true,
|
|
479
|
-
})
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
return addressesByAccount
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
async fillAssetsTokensAndData({ walletAccount }) {
|
|
486
|
-
const assetName = this.asset.name
|
|
487
|
-
const assets = await this.aci.getAssetsForNetwork({ baseAssetName: assetName })
|
|
488
|
-
const tokens = Object.values(assets).filter((asset) => assetName !== asset.name)
|
|
489
|
-
|
|
490
|
-
const tokensByAddress = tokens.reduce((map, token) => {
|
|
491
|
-
const addresses = getAssetAddresses(token)
|
|
492
|
-
for (const address of addresses) map.set(address.toLowerCase(), token)
|
|
493
|
-
return map
|
|
494
|
-
}, new Map())
|
|
495
|
-
|
|
496
|
-
const derivedData = await this.deriveData({ assetName, walletAccount, tokens })
|
|
497
|
-
|
|
498
|
-
this.#walletAccountInfo.set(walletAccount, {
|
|
499
|
-
assets,
|
|
500
|
-
tokens,
|
|
501
|
-
tokensByAddress,
|
|
502
|
-
derivedData,
|
|
503
|
-
assetName,
|
|
504
|
-
})
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
async subscribeAllWalletAccounts() {
|
|
508
|
-
const addressesByWalletAccount = await this.getReceiveAddressesByWalletAccount()
|
|
509
|
-
const entriesAddressesByWalletAccount = Object.entries(addressesByWalletAccount)
|
|
510
|
-
|
|
511
|
-
for (const [walletAccount] of entriesAddressesByWalletAccount) {
|
|
512
|
-
await this.subscribeWalletAddresses(walletAccount)
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
async subscribeWalletAddresses(walletAccount) {
|
|
517
|
-
const addressesByWalletAccount = await this.aci.getReceiveAddresses({
|
|
518
|
-
assetName: this.asset.name,
|
|
519
|
-
walletAccount,
|
|
520
|
-
useCache: true,
|
|
521
|
-
})
|
|
522
|
-
|
|
523
|
-
const address = addressesByWalletAccount[0].toLowerCase() // Only check m/0/0
|
|
524
|
-
await this.fillAssetsTokensAndData({ walletAccount })
|
|
525
|
-
|
|
526
|
-
if (!this.#walletAccountByAddress.has(address)) {
|
|
527
|
-
this.#walletAccountByAddress.set(address, [])
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
const walletAccounts = this.#walletAccountByAddress.get(address)
|
|
531
|
-
|
|
532
|
-
if (!walletAccounts.includes(walletAccount)) {
|
|
533
|
-
walletAccounts.push(walletAccount)
|
|
534
|
-
this.#walletAccountByAddress.set(address, walletAccounts)
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
this.server.connectTransactions({ walletAccount, address })
|
|
538
|
-
|
|
539
|
-
this.#wsClient.subscribeWalletAddresses({
|
|
540
|
-
network: this.asset.name,
|
|
541
|
-
addresses: [address],
|
|
542
|
-
})
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
async getBalances({ tokens, ourWalletAddress }) {
|
|
546
|
-
const batch = Object.create(null)
|
|
547
|
-
if (this.#rpcBalanceAssetNames.includes(this.asset.name)) {
|
|
548
|
-
const request = this.server.getBalanceRequest(ourWalletAddress)
|
|
549
|
-
batch[this.asset.name] = request
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
for (const token of tokens) {
|
|
553
|
-
if (this.#rpcBalanceAssetNames.includes(token.name) && token.contract.address) {
|
|
554
|
-
const request = this.server.balanceOfRequest(ourWalletAddress, token.contract.address)
|
|
555
|
-
batch[token.name] = request
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
const pairs = Object.entries(batch)
|
|
560
|
-
if (pairs.length === 0) {
|
|
561
|
-
return {}
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
const requests = pairs.map((pair) => pair[1])
|
|
565
|
-
const responses = await this.server.sendBatchRequest(requests)
|
|
566
|
-
const entries = pairs.map((pair, idx) => {
|
|
567
|
-
const balanceHex = responses[idx]
|
|
568
|
-
const name = pair[0]
|
|
569
|
-
const balance = fromHexToString(balanceHex)
|
|
570
|
-
return [name, balance]
|
|
571
|
-
})
|
|
572
|
-
return Object.fromEntries(entries)
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
getUnknownTokenAddresses({ transactions, tokensByAddress }) {
|
|
576
|
-
const set = transactions.reduce((acc, txn) => {
|
|
577
|
-
const transfers = filterEffects(txn.effects, 'erc20') || []
|
|
578
|
-
transfers.forEach((transfer) => {
|
|
579
|
-
const addr = transfer.address.toLowerCase()
|
|
580
|
-
if (!tokensByAddress.has(addr)) {
|
|
581
|
-
acc.add(addr)
|
|
582
|
-
}
|
|
583
|
-
})
|
|
584
|
-
return acc
|
|
585
|
-
}, new Set())
|
|
586
|
-
return [...set]
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
// NOTE: Here, fetchedGasPrices is the result of a call to `ClarityMonitor.getFee()`.
|
|
590
|
-
async updateGasPrice(fetchedGasPrices) {
|
|
591
|
-
try {
|
|
592
|
-
await executeEthLikeFeeMonitorUpdate({
|
|
593
|
-
assetClientInterface: this.aci,
|
|
594
|
-
feeAsset: this.asset,
|
|
595
|
-
fetchedGasPrices,
|
|
596
|
-
})
|
|
597
|
-
} catch (e) {
|
|
598
|
-
this.logger.warn('error updating gasPrice', e)
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
async onFeeUpdated(fee) {
|
|
603
|
-
return this.updateGasPrice(fee)
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
async beforeStart() {
|
|
607
|
-
this.listenToServerEvents()
|
|
608
|
-
if (this.config.GAS_PRICE_FROM_WEBSOCKET) {
|
|
609
|
-
this.server.connectFee()
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
async afterStop() {
|
|
614
|
-
this.server.dispose()
|
|
615
|
-
this.#wsClient.dispose(this.asset.name)
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
async getHistoryFromServer({ walletAccount, derivedData, refresh }) {
|
|
619
|
-
const address = derivedData.ourWalletAddress
|
|
620
|
-
const currentCursor = derivedData.currentAccountState?.clarityCursor
|
|
621
|
-
const cursor = currentCursor && !refresh ? currentCursor : null
|
|
622
|
-
return this.server.getAllTransactions({ walletAccount, address, cursor })
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
listenToServerEvents() {
|
|
626
|
-
this.server.on('feeUpdated', (...args) => this.onFeeUpdated(...args))
|
|
627
|
-
this.#wsClient.on(
|
|
628
|
-
`${this.asset.name}:new_transaction`,
|
|
629
|
-
async ({ transaction, address, cursor }) =>
|
|
630
|
-
this.addSingleTx({
|
|
631
|
-
tx: transaction,
|
|
632
|
-
address,
|
|
633
|
-
cursor,
|
|
634
|
-
})
|
|
635
|
-
)
|
|
636
|
-
}
|
|
637
|
-
}
|