@exodus/ethereum-api 9.0.3 → 10.0.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 +13 -0
- package/package.json +2 -2
- package/src/create-asset-utils.js +0 -10
- package/src/error-wrapper.js +9 -1
- package/src/eth-like-util.js +7 -2
- package/src/exodus-eth-server/index.js +0 -4
- package/src/get-balance-for-address.js +1 -1
- package/src/index.js +0 -1
- package/src/nft-utils.js +6 -2
- package/src/server-based-fee-monitor.js +1 -1
- package/src/tx-log/index.js +0 -1
- package/src/tx-log/monitor-utils/index.js +0 -2
- package/src/exodus-eth-server/api.js +0 -276
- package/src/exodus-eth-server/ws.js +0 -126
- package/src/tx-log/ethereum-monitor.js +0 -351
- package/src/tx-log/monitor-utils/get-history-from-server.js +0 -43
- package/src/tx-log/monitor-utils/get-log-items-from-server-tx.js +0 -203
- package/src/tx-log/monitor-utils/get-names-of-tokens-transferred-by-server-tx.js +0 -40
- package/src/tx-log/ws-updates.js +0 -71
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,19 @@
|
|
|
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
|
+
## [10.0.0](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@9.0.3...@exodus/ethereum-api@10.0.0) (2026-10-05)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### ⚠ BREAKING CHANGES
|
|
10
|
+
|
|
11
|
+
* **ethereum-api:** remove magnifier evm monitor (#8828)
|
|
12
|
+
|
|
13
|
+
### Code Refactoring
|
|
14
|
+
|
|
15
|
+
* **ethereum-api:** remove magnifier evm monitor ([#8828](https://github.com/ExodusMovement/assets/issues/8828)) ([d179ae1](https://github.com/ExodusMovement/assets/commit/d179ae145410f2a8033718a1889a7ea6676001e1))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
6
19
|
## [9.0.3](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@9.0.2...@exodus/ethereum-api@9.0.3) (2026-10-05)
|
|
7
20
|
|
|
8
21
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/ethereum-api",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "10.0.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",
|
|
@@ -66,5 +66,5 @@
|
|
|
66
66
|
"type": "git",
|
|
67
67
|
"url": "git+https://github.com/ExodusMovement/assets.git"
|
|
68
68
|
},
|
|
69
|
-
"gitHead": "
|
|
69
|
+
"gitHead": "83a0e678debb92fbfcefd92e7bd0453d1a92136b"
|
|
70
70
|
}
|
|
@@ -15,7 +15,6 @@ import {
|
|
|
15
15
|
} from './multicall3/index.js'
|
|
16
16
|
import { ClarityMonitor } from './tx-log/clarity-monitor.js'
|
|
17
17
|
import { ClarityTruncatedHistoryMonitor } from './tx-log/clarity-truncated-history-monitor.js'
|
|
18
|
-
import { EthereumMonitor } from './tx-log/ethereum-monitor.js'
|
|
19
18
|
import { EthereumNoHistoryMonitor } from './tx-log/ethereum-no-history-monitor.js'
|
|
20
19
|
import { getOptimisticTxLogEffects } from './tx-log/get-optimistic-txlog-effects.js'
|
|
21
20
|
import { BLOCK_TAG_LATEST, BLOCK_TAG_PENDING, resolveNonce } from './tx-send/nonce-utils.js'
|
|
@@ -355,15 +354,6 @@ export const createHistoryMonitorFactory = ({
|
|
|
355
354
|
...args,
|
|
356
355
|
})
|
|
357
356
|
break
|
|
358
|
-
case 'magnifier':
|
|
359
|
-
monitor = new EthereumMonitor({
|
|
360
|
-
assetClientInterface,
|
|
361
|
-
interval: ms(monitorInterval || '15s'),
|
|
362
|
-
server,
|
|
363
|
-
rpcBalanceAssetNames,
|
|
364
|
-
...args,
|
|
365
|
-
})
|
|
366
|
-
break
|
|
367
357
|
default:
|
|
368
358
|
throw new Error(`Monitor type ${monitorType} of evm asset ${assetName} is unknown`)
|
|
369
359
|
}
|
package/src/error-wrapper.js
CHANGED
|
@@ -281,6 +281,13 @@ export function getEvmErrorReason(errorMessage) {
|
|
|
281
281
|
return null
|
|
282
282
|
}
|
|
283
283
|
|
|
284
|
+
export function getFetchGasLimitErrorReason(errorMessage) {
|
|
285
|
+
const reason = getEvmErrorReason(errorMessage)?.reason
|
|
286
|
+
return reason && reason !== EVM_ERROR_REASONS.executionReverted.reason
|
|
287
|
+
? { reason, type: EVM_ERROR_TYPES.CONTRACT_CALL }
|
|
288
|
+
: EVM_ERROR_REASONS.fetchGasLimitFailed
|
|
289
|
+
}
|
|
290
|
+
|
|
284
291
|
const MAX_HINT_LENGTH = 100
|
|
285
292
|
// TODO: move this to be a generic error for all assets
|
|
286
293
|
export class EthLikeError extends Error {
|
|
@@ -374,7 +381,8 @@ export const withErrorReason = async ({ promise, errorReasonInfo, hint, baseAsse
|
|
|
374
381
|
|
|
375
382
|
throw new EthLikeError({
|
|
376
383
|
message: err.message,
|
|
377
|
-
errorReasonInfo
|
|
384
|
+
errorReasonInfo:
|
|
385
|
+
typeof errorReasonInfo === 'function' ? errorReasonInfo(err.message) : errorReasonInfo,
|
|
378
386
|
hint,
|
|
379
387
|
traceId: err.traceId,
|
|
380
388
|
baseAssetName,
|
package/src/eth-like-util.js
CHANGED
|
@@ -4,7 +4,12 @@ import { safeString } from '@exodus/safe-string'
|
|
|
4
4
|
import SolidityContract from '@exodus/solidity-contract'
|
|
5
5
|
import assert from 'minimalistic-assert'
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
EthLikeError,
|
|
9
|
+
EVM_ERROR_REASONS,
|
|
10
|
+
getFetchGasLimitErrorReason,
|
|
11
|
+
withErrorReason,
|
|
12
|
+
} from './error-wrapper.js'
|
|
8
13
|
import { getServer } from './exodus-eth-server/index.js'
|
|
9
14
|
import { fromHexToString } from './number-utils.js'
|
|
10
15
|
|
|
@@ -60,7 +65,7 @@ export async function getNonce({ asset, address, tag = 'latest' }) {
|
|
|
60
65
|
export async function estimateGas({ asset, ...args }) {
|
|
61
66
|
return withErrorReason({
|
|
62
67
|
promise: getServer(asset).estimateGas(args),
|
|
63
|
-
errorReasonInfo:
|
|
68
|
+
errorReasonInfo: getFetchGasLimitErrorReason,
|
|
64
69
|
hint: safeString`estimateGas`,
|
|
65
70
|
baseAssetName: asset.baseAsset.name,
|
|
66
71
|
})
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import assert from 'minimalistic-assert'
|
|
2
2
|
|
|
3
|
-
import { create } from './api.js'
|
|
4
3
|
import ApiCoinNodesServer from './api-coin-nodes.js'
|
|
5
4
|
import ClarityServer from './clarity.js'
|
|
6
5
|
import ClarityServerV2 from './clarity-v2.js'
|
|
@@ -12,7 +11,6 @@ export const ValidMonitorTypes = [
|
|
|
12
11
|
'clarity-v2',
|
|
13
12
|
'clarity-truncated-history',
|
|
14
13
|
'clarity-v3',
|
|
15
|
-
'magnifier',
|
|
16
14
|
]
|
|
17
15
|
|
|
18
16
|
export function createEvmServer({ assetName, serverUrl, monitorType }) {
|
|
@@ -30,8 +28,6 @@ export function createEvmServer({ assetName, serverUrl, monitorType }) {
|
|
|
30
28
|
case 'clarity-truncated-history':
|
|
31
29
|
case 'clarity-v3':
|
|
32
30
|
return new ClarityServerV2({ baseAssetName: assetName, uri: serverUrl })
|
|
33
|
-
case 'magnifier':
|
|
34
|
-
return create(serverUrl, assetName)
|
|
35
31
|
default:
|
|
36
32
|
throw new Error(`No server API found for ${assetName} and type ${monitorType}`)
|
|
37
33
|
}
|
|
@@ -11,7 +11,7 @@ export const createGetBalanceForAddress = ({ asset, server }) => {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
if (balance && typeof balance === 'object' && typeof balance.confirmed?.value === 'string') {
|
|
14
|
-
//
|
|
14
|
+
// Clarity response
|
|
15
15
|
return asset.currency.baseUnit(balance.confirmed.value)
|
|
16
16
|
}
|
|
17
17
|
|
package/src/index.js
CHANGED
package/src/nft-utils.js
CHANGED
|
@@ -2,7 +2,7 @@ import { safeString } from '@exodus/safe-string'
|
|
|
2
2
|
import SolidityContract from '@exodus/solidity-contract'
|
|
3
3
|
import assert from 'minimalistic-assert'
|
|
4
4
|
|
|
5
|
-
import { EthLikeError, EVM_ERROR_REASONS } from './error-wrapper.js'
|
|
5
|
+
import { EthLikeError, EVM_ERROR_REASONS, getFetchGasLimitErrorReason } from './error-wrapper.js'
|
|
6
6
|
import { fetchGasLimit } from './gas-estimation.js'
|
|
7
7
|
|
|
8
8
|
export const getNftArguments = async ({ asset, nft, fromAddress, toAddress }) => {
|
|
@@ -45,9 +45,13 @@ export const getNftArguments = async ({ asset, nft, fromAddress, toAddress }) =>
|
|
|
45
45
|
}
|
|
46
46
|
})
|
|
47
47
|
).catch((e) => {
|
|
48
|
+
const [first, ...rest] = e.errors.map((err) => getFetchGasLimitErrorReason(err.message))
|
|
48
49
|
throw new EthLikeError({
|
|
49
50
|
message: errors.join('\n'),
|
|
50
|
-
errorReasonInfo:
|
|
51
|
+
errorReasonInfo:
|
|
52
|
+
first && rest.every(({ reason }) => reason === first.reason)
|
|
53
|
+
? first
|
|
54
|
+
: EVM_ERROR_REASONS.fetchGasLimitFailed,
|
|
51
55
|
hint: safeString`getNftArguments`,
|
|
52
56
|
traceId: e.traceId,
|
|
53
57
|
})
|
|
@@ -6,7 +6,7 @@ import { calculateEthLikeFeeMonitorUpdate } from './fee-utils.js'
|
|
|
6
6
|
/**
|
|
7
7
|
* Generic eth server based fee monitor.
|
|
8
8
|
*
|
|
9
|
-
* Compatible with all servers, clarity
|
|
9
|
+
* Compatible with all servers, clarity and api-coin (they all implement the gasPrice method).
|
|
10
10
|
*
|
|
11
11
|
* Usage in the index files:
|
|
12
12
|
*
|
package/src/tx-log/index.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
export { EthereumMonitor } from './ethereum-monitor.js'
|
|
2
1
|
export { EthereumNoHistoryMonitor } from './ethereum-no-history-monitor.js'
|
|
3
2
|
export { ClarityMonitor } from './clarity-monitor.js'
|
|
4
3
|
export { ClarityTruncatedHistoryMonitor } from './clarity-truncated-history-monitor.js'
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
export { default as getDeriveDataNeededForTick } from './get-derive-data-needed-for-tick.js'
|
|
2
2
|
|
|
3
3
|
export { default as getAllLogItemsByAsset } from './get-all-log-items-by-asset.js'
|
|
4
|
-
export { default as getLogItemsFromServerTx } from './get-log-items-from-server-tx.js'
|
|
5
|
-
export { default as getHistoryFromServer } from './get-history-from-server.js'
|
|
6
4
|
export { default as checkPendingTransactions } from './check-pending-transactions.js'
|
|
7
5
|
export { default as getDeriveTransactionsToCheck } from './get-derive-transactions-to-check.js'
|
|
8
6
|
export { default as getCurrentEIP7702Delegation } from './get-current-eip7702-delegation.js'
|
|
@@ -1,276 +0,0 @@
|
|
|
1
|
-
import { randomUUID } from '@exodus/crypto/randomUUID'
|
|
2
|
-
import { bufferToHex } from '@exodus/ethereumjs/util'
|
|
3
|
-
import fetchival from '@exodus/fetch/experimental/fetchival'
|
|
4
|
-
import { retry } from '@exodus/simple-retry'
|
|
5
|
-
import SolidityContract from '@exodus/solidity-contract'
|
|
6
|
-
import ms from 'ms'
|
|
7
|
-
|
|
8
|
-
import { fromHexToString } from '../number-utils.js'
|
|
9
|
-
import { getFallbackGasPriceEstimation } from './utils.js'
|
|
10
|
-
import createWebSocket from './ws.js'
|
|
11
|
-
|
|
12
|
-
const RETRY_DELAYS = ['10s']
|
|
13
|
-
|
|
14
|
-
export function create(defaultURL, ensAssetName) {
|
|
15
|
-
let API_URL = defaultURL
|
|
16
|
-
const ws = createWebSocket(() => {
|
|
17
|
-
const obj = new URL(API_URL)
|
|
18
|
-
obj.protocol = 'wss:'
|
|
19
|
-
return `${obj}`
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
function baseUrl(version = 'v1') {
|
|
23
|
-
return new URL(version === 'v1' ? API_URL : API_URL.replace('v1', version))
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
async function request(module, params = {}, { version = 'v1', method = 'get' } = {}) {
|
|
27
|
-
try {
|
|
28
|
-
return await fetchival(baseUrl(version), { timeout: ms('15s') })(module).method(
|
|
29
|
-
method,
|
|
30
|
-
params
|
|
31
|
-
)
|
|
32
|
-
} catch (err) {
|
|
33
|
-
let nerr = err
|
|
34
|
-
if (err.response && (err.response.status === 500 || err.response.status === 400)) {
|
|
35
|
-
try {
|
|
36
|
-
const data = await err.response.json()
|
|
37
|
-
const msg = data.error.replace(/^RPC error \(code: -\d+\): /, '')
|
|
38
|
-
nerr = new Error(msg)
|
|
39
|
-
} catch {}
|
|
40
|
-
|
|
41
|
-
nerr.finalError = true
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
throw nerr
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// Default retry function
|
|
49
|
-
const requestWithRetry = retry(request, { delayTimesMs: RETRY_DELAYS })
|
|
50
|
-
|
|
51
|
-
return {
|
|
52
|
-
getURL() {
|
|
53
|
-
return API_URL
|
|
54
|
-
},
|
|
55
|
-
|
|
56
|
-
setURL(newURL) {
|
|
57
|
-
if (newURL === API_URL) return // prevents useless WS reconnections
|
|
58
|
-
|
|
59
|
-
API_URL = newURL || defaultURL
|
|
60
|
-
|
|
61
|
-
if (ws.isCreated()) {
|
|
62
|
-
ws.close()
|
|
63
|
-
ws.open()
|
|
64
|
-
}
|
|
65
|
-
},
|
|
66
|
-
|
|
67
|
-
stop() {
|
|
68
|
-
ws.close()
|
|
69
|
-
},
|
|
70
|
-
|
|
71
|
-
ws,
|
|
72
|
-
|
|
73
|
-
async getBalance(address, opts) {
|
|
74
|
-
opts = { startblock: 'earliest', endblock: 'pending', ...opts }
|
|
75
|
-
return requestWithRetry('balance', { address, from: opts.startblock, to: opts.endblock })
|
|
76
|
-
},
|
|
77
|
-
|
|
78
|
-
async getBalanceProxied(address, tag = 'latest') {
|
|
79
|
-
return requestWithRetry('proxy', { method: 'eth_getBalance', address, tag })
|
|
80
|
-
},
|
|
81
|
-
|
|
82
|
-
async balanceOf(address, tokenAddress, tag = 'latest') {
|
|
83
|
-
const contract = SolidityContract.simpleErc20(tokenAddress)
|
|
84
|
-
const callData = contract.balanceOf.build(address)
|
|
85
|
-
const data = {
|
|
86
|
-
data: bufferToHex(callData),
|
|
87
|
-
to: tokenAddress,
|
|
88
|
-
tag,
|
|
89
|
-
}
|
|
90
|
-
const result = await retry(this.ethCall, { delayTimesMs: RETRY_DELAYS })(data)
|
|
91
|
-
const balance = fromHexToString(result)
|
|
92
|
-
return {
|
|
93
|
-
confirmed: {
|
|
94
|
-
[tokenAddress]: balance,
|
|
95
|
-
},
|
|
96
|
-
}
|
|
97
|
-
},
|
|
98
|
-
|
|
99
|
-
async getHistory(address, opts) {
|
|
100
|
-
opts = { startblock: 'earliest', endblock: 'pending', limit: 1000, ...opts }
|
|
101
|
-
return requestWithRetry('history', {
|
|
102
|
-
address,
|
|
103
|
-
from: opts.startblock,
|
|
104
|
-
to: opts.endblock,
|
|
105
|
-
limit: opts.limit,
|
|
106
|
-
})
|
|
107
|
-
},
|
|
108
|
-
|
|
109
|
-
async getHistoryV2(address, opts = {}) {
|
|
110
|
-
return requestWithRetry(
|
|
111
|
-
'history',
|
|
112
|
-
{
|
|
113
|
-
address,
|
|
114
|
-
...opts,
|
|
115
|
-
},
|
|
116
|
-
{ version: 'v2' }
|
|
117
|
-
)
|
|
118
|
-
},
|
|
119
|
-
|
|
120
|
-
async gasPrice() {
|
|
121
|
-
return requestWithRetry('proxy', { method: 'eth_gasPrice' })
|
|
122
|
-
},
|
|
123
|
-
|
|
124
|
-
async getGasPriceEstimation() {
|
|
125
|
-
return getFallbackGasPriceEstimation({ server: this })
|
|
126
|
-
},
|
|
127
|
-
|
|
128
|
-
// for fee monitor
|
|
129
|
-
async getGasPrice() {
|
|
130
|
-
return requestWithRetry('proxy', { method: 'eth_gasPrice' })
|
|
131
|
-
},
|
|
132
|
-
|
|
133
|
-
async getLatestBlock() {
|
|
134
|
-
return this.getBlockByNumber('latest')
|
|
135
|
-
},
|
|
136
|
-
|
|
137
|
-
async getBaseFeePerGas() {
|
|
138
|
-
const response = await this.getLatestBlock()
|
|
139
|
-
if (response.baseFeePerGas) {
|
|
140
|
-
return fromHexToString(response.baseFeePerGas)
|
|
141
|
-
}
|
|
142
|
-
},
|
|
143
|
-
|
|
144
|
-
async getTransactionCount(address, tag = 'latest') {
|
|
145
|
-
return requestWithRetry('proxy', { method: 'eth_getTransactionCount', address, tag })
|
|
146
|
-
},
|
|
147
|
-
|
|
148
|
-
async getTransactionByHash(hash) {
|
|
149
|
-
return requestWithRetry('proxy', { method: 'eth_getTransactionByHash', hash })
|
|
150
|
-
},
|
|
151
|
-
|
|
152
|
-
async getTransactionReceipt(txhash) {
|
|
153
|
-
return requestWithRetry('proxy', { method: 'eth_getTransactionReceipt', txhash })
|
|
154
|
-
},
|
|
155
|
-
|
|
156
|
-
async getCode(address, tag = 'latest') {
|
|
157
|
-
return requestWithRetry('proxy', { method: 'eth_getCode', address, tag })
|
|
158
|
-
},
|
|
159
|
-
|
|
160
|
-
async getStorageAt(address, slot, tag = 'latest') {
|
|
161
|
-
return requestWithRetry('proxy', { method: 'eth_getStorageAt', address, slot, tag })
|
|
162
|
-
},
|
|
163
|
-
|
|
164
|
-
async isContract(address) {
|
|
165
|
-
const code = await requestWithRetry('proxy', { method: 'eth_getCode', address })
|
|
166
|
-
return code.length > 2
|
|
167
|
-
},
|
|
168
|
-
|
|
169
|
-
async sendRawTransaction(data) {
|
|
170
|
-
const _data = data instanceof Uint8Array ? Buffer.from(data).toString('hex') : data
|
|
171
|
-
const hex = _data.startsWith('0x') ? _data : '0x' + _data
|
|
172
|
-
return requestWithRetry(
|
|
173
|
-
'proxy',
|
|
174
|
-
{ method: 'eth_sendRawTransaction', params: [hex] },
|
|
175
|
-
{ version: 'v2', method: 'post' }
|
|
176
|
-
)
|
|
177
|
-
},
|
|
178
|
-
|
|
179
|
-
async estimateGas(data, tag = 'latest') {
|
|
180
|
-
return requestWithRetry('proxy', { method: 'eth_estimateGas', ...data, tag })
|
|
181
|
-
},
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* Hits a magnifier endpoint returning maximum gas limit that a specified
|
|
185
|
-
* contract along with method consumed in the past.
|
|
186
|
-
* @param opts : Object with parameters
|
|
187
|
-
* @returns {Promise<*>}
|
|
188
|
-
*/
|
|
189
|
-
async getGasEstimation({ address, data }) {
|
|
190
|
-
return requestWithRetry('gas-estimation', {
|
|
191
|
-
address,
|
|
192
|
-
data,
|
|
193
|
-
})
|
|
194
|
-
},
|
|
195
|
-
|
|
196
|
-
async ethCall(data, tag = 'latest') {
|
|
197
|
-
return request('proxy', { method: 'eth_call', ...data, tag })
|
|
198
|
-
},
|
|
199
|
-
|
|
200
|
-
async blockNumber() {
|
|
201
|
-
return requestWithRetry('proxy', { method: 'eth_blockNumber' })
|
|
202
|
-
},
|
|
203
|
-
|
|
204
|
-
async getBlockByNumber(number, isFullTransactions = false) {
|
|
205
|
-
return requestWithRetry('proxy', {
|
|
206
|
-
method: 'eth_getBlockByNumber',
|
|
207
|
-
number,
|
|
208
|
-
isFullTransactions,
|
|
209
|
-
})
|
|
210
|
-
},
|
|
211
|
-
|
|
212
|
-
async getLogs(params) {
|
|
213
|
-
return requestWithRetry('proxy', { method: 'eth_getLogs', ...params })
|
|
214
|
-
},
|
|
215
|
-
|
|
216
|
-
async simulateRawTransaction(rawTransaction, applyPending = true) {
|
|
217
|
-
rawTransaction = rawTransaction.replace('0x', '')
|
|
218
|
-
return request('proxy', {
|
|
219
|
-
method: 'debug_simulateRawTransaction',
|
|
220
|
-
rawTransaction,
|
|
221
|
-
applyPending,
|
|
222
|
-
})
|
|
223
|
-
},
|
|
224
|
-
|
|
225
|
-
async getCoinbase() {
|
|
226
|
-
return request('proxy', {
|
|
227
|
-
method: 'eth_coinbase',
|
|
228
|
-
})
|
|
229
|
-
},
|
|
230
|
-
|
|
231
|
-
async getBlockByHash(blockHash, fullTx = false) {
|
|
232
|
-
return request('proxy', {
|
|
233
|
-
method: 'eth_getBlockByHash',
|
|
234
|
-
blockHash,
|
|
235
|
-
fullTx,
|
|
236
|
-
})
|
|
237
|
-
},
|
|
238
|
-
|
|
239
|
-
async getBlockTransactionCountByHash(blockHash) {
|
|
240
|
-
return request('proxy', {
|
|
241
|
-
method: 'eth_getBlockTransactionCountByHash',
|
|
242
|
-
blockHash,
|
|
243
|
-
})
|
|
244
|
-
},
|
|
245
|
-
|
|
246
|
-
async getBlockTransactionCountByNumber(quantityOrTag) {
|
|
247
|
-
return request('proxy', {
|
|
248
|
-
method: 'eth_getBlockTransactionCountByNumber',
|
|
249
|
-
quantityOrTag,
|
|
250
|
-
})
|
|
251
|
-
},
|
|
252
|
-
|
|
253
|
-
async getCompilers() {
|
|
254
|
-
return request('proxy', {
|
|
255
|
-
method: 'eth_getCompilers',
|
|
256
|
-
})
|
|
257
|
-
},
|
|
258
|
-
|
|
259
|
-
async getNetVersion() {
|
|
260
|
-
return request('proxy', {
|
|
261
|
-
method: 'net_version',
|
|
262
|
-
})
|
|
263
|
-
},
|
|
264
|
-
|
|
265
|
-
async proxyToCoinNode(requestData) {
|
|
266
|
-
// eslint-disable-next-line @exodus/hydra/no-param-reassign-prop-only
|
|
267
|
-
if (!requestData.jsonrpc) requestData.jsonrpc = '2.0'
|
|
268
|
-
// eslint-disable-next-line @exodus/hydra/no-param-reassign-prop-only
|
|
269
|
-
if (!requestData.id) requestData.id = randomUUID()
|
|
270
|
-
// eslint-disable-next-line @exodus/hydra/no-param-reassign-prop-only
|
|
271
|
-
if (!requestData.params) requestData.params = []
|
|
272
|
-
|
|
273
|
-
return request('proxy', requestData, { version: 'v2', method: 'post' })
|
|
274
|
-
},
|
|
275
|
-
}
|
|
276
|
-
}
|
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
import EventEmitter from 'events/events.js' // forces it to use the module from node_modules
|
|
2
|
-
import ms from 'ms'
|
|
3
|
-
|
|
4
|
-
const RECONNECT_INTERVAL = ms('10s')
|
|
5
|
-
const PING_INTERVAL = ms('10s')
|
|
6
|
-
const MAX_RECONNECT_DELAY = ms('15s')
|
|
7
|
-
|
|
8
|
-
export default function createWebSocket(getURL) {
|
|
9
|
-
const addresses = new Set()
|
|
10
|
-
const events = new EventEmitter()
|
|
11
|
-
let ws
|
|
12
|
-
let pingIntervalId = null
|
|
13
|
-
let opened = false
|
|
14
|
-
let isWSOpened = false
|
|
15
|
-
let openTimeoutId
|
|
16
|
-
|
|
17
|
-
function subscribeAddress(address) {
|
|
18
|
-
const data = JSON.stringify({ type: 'subscribe-address', address })
|
|
19
|
-
ws.send(data)
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function subscribeGasPrice() {
|
|
23
|
-
const data = JSON.stringify({ type: 'subscribe-gasprice' })
|
|
24
|
-
ws.send(data)
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function clearPing() {
|
|
28
|
-
clearInterval(pingIntervalId)
|
|
29
|
-
pingIntervalId = null
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function onMessage(data) {
|
|
33
|
-
data = JSON.parse(data)
|
|
34
|
-
switch (data.type) {
|
|
35
|
-
case 'address':
|
|
36
|
-
events.emit(`address-${data.address}`)
|
|
37
|
-
break
|
|
38
|
-
case 'gasprice':
|
|
39
|
-
events.emit('gasprice', { gasPrice: data.gasprice, baseFeePerGas: data.baseGasPrice })
|
|
40
|
-
break
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function open() {
|
|
45
|
-
opened = true
|
|
46
|
-
clearTimeout(openTimeoutId)
|
|
47
|
-
if (ws) return
|
|
48
|
-
|
|
49
|
-
ws = new WebSocket(getURL())
|
|
50
|
-
// eslint-disable-next-line unicorn/prefer-add-event-listener
|
|
51
|
-
ws.onerror = (e) => {}
|
|
52
|
-
// eslint-disable-next-line unicorn/prefer-add-event-listener
|
|
53
|
-
ws.onmessage = (e) => {
|
|
54
|
-
try {
|
|
55
|
-
onMessage(e.data)
|
|
56
|
-
} catch {}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// eslint-disable-next-line unicorn/prefer-add-event-listener
|
|
60
|
-
ws.onopen = () => {
|
|
61
|
-
for (const address of addresses.values()) subscribeAddress(address)
|
|
62
|
-
subscribeGasPrice()
|
|
63
|
-
if (typeof ws.ping === 'function') {
|
|
64
|
-
pingIntervalId = setInterval(() => ws.ping(), PING_INTERVAL)
|
|
65
|
-
} else {
|
|
66
|
-
console.warn('Client side ping not available')
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
isWSOpened = true
|
|
70
|
-
events.emit('open')
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// eslint-disable-next-line unicorn/prefer-add-event-listener
|
|
74
|
-
ws.onclose = () => {
|
|
75
|
-
ws = null
|
|
76
|
-
isWSOpened = false
|
|
77
|
-
clearPing()
|
|
78
|
-
if (opened) {
|
|
79
|
-
openTimeoutId = setTimeout(open, reconnectDelay())
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
events.emit('close')
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function close() {
|
|
87
|
-
opened = false
|
|
88
|
-
isWSOpened = false
|
|
89
|
-
clearTimeout(openTimeoutId)
|
|
90
|
-
if (!ws) return
|
|
91
|
-
// eslint-disable-next-line unicorn/prefer-add-event-listener
|
|
92
|
-
ws.onerror = null
|
|
93
|
-
// eslint-disable-next-line unicorn/prefer-add-event-listener
|
|
94
|
-
ws.onmessage = null
|
|
95
|
-
// eslint-disable-next-line unicorn/prefer-add-event-listener
|
|
96
|
-
ws.onopen = null
|
|
97
|
-
// eslint-disable-next-line unicorn/prefer-add-event-listener
|
|
98
|
-
ws.onclose = null
|
|
99
|
-
try {
|
|
100
|
-
ws.close()
|
|
101
|
-
} catch (e) {
|
|
102
|
-
if (!/WebSocket was closed before the connection was established/.test(e.message)) {
|
|
103
|
-
throw e
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
ws = null
|
|
108
|
-
clearPing()
|
|
109
|
-
events.emit('close')
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function reconnectDelay() {
|
|
113
|
-
const min = RECONNECT_INTERVAL
|
|
114
|
-
const max = MAX_RECONNECT_DELAY
|
|
115
|
-
return Math.floor(Math.random() * (max - min + 1) + min)
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function watch(address) {
|
|
119
|
-
if (addresses.has(address)) return
|
|
120
|
-
addresses.add(address)
|
|
121
|
-
|
|
122
|
-
if (ws) subscribeAddress(address)
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
return { events, open, close, watch, isCreated: () => !!ws, isOpened: () => isWSOpened }
|
|
126
|
-
}
|
|
@@ -1,351 +0,0 @@
|
|
|
1
|
-
import { BaseMonitor } from '@exodus/asset-lib'
|
|
2
|
-
import { getAssetAddresses } from '@exodus/ethereum-lib'
|
|
3
|
-
import lodash from 'lodash'
|
|
4
|
-
|
|
5
|
-
import { executeEthLikeFeeMonitorUpdate } from '../fee-utils.js'
|
|
6
|
-
import { fromHexToString } from '../number-utils.js'
|
|
7
|
-
import {
|
|
8
|
-
checkPendingTransactions,
|
|
9
|
-
excludeUnchangedTokenBalances,
|
|
10
|
-
getAllLogItemsByAsset,
|
|
11
|
-
getDeriveDataNeededForTick,
|
|
12
|
-
getDeriveTransactionsToCheck,
|
|
13
|
-
getHistoryFromServer,
|
|
14
|
-
getLogItemsFromServerTx,
|
|
15
|
-
isUnverifiablePendingTxExpired,
|
|
16
|
-
verifyRpcPendingTxStatusBatch,
|
|
17
|
-
} from './monitor-utils/index.js'
|
|
18
|
-
import {
|
|
19
|
-
enableWSUpdates,
|
|
20
|
-
subscribeToGasPriceNotifications,
|
|
21
|
-
subscribeToWSNotifications,
|
|
22
|
-
} from './ws-updates.js'
|
|
23
|
-
|
|
24
|
-
const { isEmpty } = lodash
|
|
25
|
-
|
|
26
|
-
// The base ethereum monitor class handles listening for, fetching,
|
|
27
|
-
// formatting, and populating-to-state all ETH/ETC/ERC20 transactions.
|
|
28
|
-
|
|
29
|
-
export class EthereumMonitor extends BaseMonitor {
|
|
30
|
-
constructor({ server, config, rpcBalanceAssetNames, webSocketEnabled = true, ...args }) {
|
|
31
|
-
super(args)
|
|
32
|
-
this.server = server
|
|
33
|
-
this.config = { GAS_PRICE_FROM_WEBSOCKET: true, ...config }
|
|
34
|
-
this.deriveDataNeededForTick = getDeriveDataNeededForTick(this.aci)
|
|
35
|
-
this.deriveTransactionsToCheck = getDeriveTransactionsToCheck({
|
|
36
|
-
getTxLog: (...args) => this.aci.getTxLog(...args),
|
|
37
|
-
})
|
|
38
|
-
this.subscribedToWalletAccountNotificationsMap = new Map()
|
|
39
|
-
this.addHook('before-start', (...args) => this.beforeStart(...args))
|
|
40
|
-
this.addHook('after-stop', (...args) => this.afterStop(...args))
|
|
41
|
-
this.subscribedToGasPriceMap = new Map()
|
|
42
|
-
this._webSocketEnabled = webSocketEnabled
|
|
43
|
-
this.rpcBalanceAssetNames = rpcBalanceAssetNames
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
setServer(config = {}) {
|
|
47
|
-
const uri = config?.serverv1 || this.server.defaultUri
|
|
48
|
-
|
|
49
|
-
if (uri === this.server.getURL()) {
|
|
50
|
-
return
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
this.server.setURL(uri)
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
async deriveData({ assetSource, tokens }) {
|
|
57
|
-
const { asset: assetName, walletAccount } = assetSource
|
|
58
|
-
|
|
59
|
-
const { ourWalletAddress, currentAccountState, minimumConfirmations } =
|
|
60
|
-
await this.deriveDataNeededForTick({ assetName, walletAccount })
|
|
61
|
-
const transactionsToCheck = await this.deriveTransactionsToCheck({
|
|
62
|
-
assetName,
|
|
63
|
-
walletAccount,
|
|
64
|
-
tokens,
|
|
65
|
-
ourWalletAddress,
|
|
66
|
-
})
|
|
67
|
-
|
|
68
|
-
return {
|
|
69
|
-
ourWalletAddress,
|
|
70
|
-
currentAccountState,
|
|
71
|
-
minimumConfirmations,
|
|
72
|
-
...transactionsToCheck,
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// eslint-disable-next-line no-undef
|
|
77
|
-
async checkPendingTransactions(params) {
|
|
78
|
-
const {
|
|
79
|
-
pendingTransactionsToCheck,
|
|
80
|
-
pendingTransactionsGroupedByAddressAndNonce,
|
|
81
|
-
recentPendingTransactions,
|
|
82
|
-
} = checkPendingTransactions(params)
|
|
83
|
-
const txsToRemove = []
|
|
84
|
-
const {
|
|
85
|
-
assetSource: { walletAccount },
|
|
86
|
-
} = params
|
|
87
|
-
|
|
88
|
-
const updateTx = (tx, asset, { isTxConfirmed, remove, preserve }) => {
|
|
89
|
-
if (remove) {
|
|
90
|
-
txsToRemove.push({ tx, assetSource: { asset, walletAccount } })
|
|
91
|
-
} else if (preserve) {
|
|
92
|
-
params.logItemsByAsset[asset].push(tx)
|
|
93
|
-
} else {
|
|
94
|
-
params.logItemsByAsset[asset].push({
|
|
95
|
-
...tx,
|
|
96
|
-
...(isTxConfirmed ? { confirmations: 1 } : { dropped: true, error: 'Dropped' }),
|
|
97
|
-
})
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// in case this is an ETH fee tx that has associated ERC20 send txs
|
|
101
|
-
const promises = tx.tokens.map(async (assetName) => {
|
|
102
|
-
const tokenTxSet = await this.aci.getTxLog({ assetName, walletAccount })
|
|
103
|
-
if (remove) {
|
|
104
|
-
txsToRemove.push({
|
|
105
|
-
tx: tokenTxSet.get(tx.txId),
|
|
106
|
-
assetSource: { asset: assetName, walletAccount },
|
|
107
|
-
})
|
|
108
|
-
} else if (tokenTxSet && tokenTxSet.has(tx.txId)) {
|
|
109
|
-
// Optional: the stored tx may name a token since removed from the registry
|
|
110
|
-
params.logItemsByAsset[assetName]?.push(
|
|
111
|
-
preserve
|
|
112
|
-
? tokenTxSet.get(tx.txId)
|
|
113
|
-
: {
|
|
114
|
-
...tokenTxSet.get(tx.txId),
|
|
115
|
-
...(isTxConfirmed ? { confirmations: 1 } : { dropped: true, error: 'Dropped' }),
|
|
116
|
-
}
|
|
117
|
-
)
|
|
118
|
-
}
|
|
119
|
-
})
|
|
120
|
-
|
|
121
|
-
return Promise.all(promises)
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
for (const { tx, assetName, replaced = false } of Object.values(
|
|
125
|
-
pendingTransactionsGroupedByAddressAndNonce
|
|
126
|
-
)) {
|
|
127
|
-
if (replaced) {
|
|
128
|
-
await updateTx(tx, assetName, { remove: true })
|
|
129
|
-
delete pendingTransactionsToCheck[tx.txId]
|
|
130
|
-
delete recentPendingTransactions[tx.txId]
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// Batch verify all pending txs with a single RPC call
|
|
135
|
-
const txIds = Object.keys(pendingTransactionsToCheck)
|
|
136
|
-
const statuses = await verifyRpcPendingTxStatusBatch({
|
|
137
|
-
server: this.server,
|
|
138
|
-
logger: this.logger,
|
|
139
|
-
txIds,
|
|
140
|
-
})
|
|
141
|
-
|
|
142
|
-
for (const { tx, assetName } of Object.values(pendingTransactionsToCheck)) {
|
|
143
|
-
const txStatus = statuses[tx.txId]
|
|
144
|
-
if (txStatus?.status === 'confirmed') {
|
|
145
|
-
await updateTx(tx, assetName, { isTxConfirmed: true })
|
|
146
|
-
} else if (txStatus?.status === 'dropped') {
|
|
147
|
-
if (params.refresh) {
|
|
148
|
-
await updateTx(tx, assetName, { remove: true })
|
|
149
|
-
} else {
|
|
150
|
-
await updateTx(tx, assetName, { isTxConfirmed: false })
|
|
151
|
-
}
|
|
152
|
-
} else if (params.refresh) {
|
|
153
|
-
// No txStatus at all means the whole verification batch failed - evidence
|
|
154
|
-
// about nothing, so preserve unconditionally; the age ceiling only applies
|
|
155
|
-
// to a per-entry 'unknown' verdict (evidence about that one hash).
|
|
156
|
-
if (!txStatus || txStatus.status === 'pending' || !isUnverifiablePendingTxExpired(tx)) {
|
|
157
|
-
// Still in the mempool (or not verifiable yet) but missing from the history
|
|
158
|
-
// server response: re-add it so the refresh overwrite cannot wipe it and
|
|
159
|
-
// break tx-log-based nonce resolution.
|
|
160
|
-
await updateTx(tx, assetName, { preserve: true })
|
|
161
|
-
} else {
|
|
162
|
-
// Unverifiable past the age ceiling: purge so a hash the node keeps
|
|
163
|
-
// erroring on cannot pin nonce derivation forever.
|
|
164
|
-
await updateTx(tx, assetName, { remove: true })
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
// non-refresh: 'pending' or unknown - tx still in mempool, do nothing
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
if (params.refresh) {
|
|
171
|
-
// Too recent for a reliable on-chain verdict - always survive the refresh overwrite
|
|
172
|
-
for (const { tx, assetName } of Object.values(recentPendingTransactions)) {
|
|
173
|
-
await updateTx(tx, assetName, { preserve: true })
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
return {
|
|
178
|
-
txsToRemove,
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
async tick({ refresh, walletAccount }) {
|
|
183
|
-
await this._subscribeToWSNotifications() // checks if it's a new walletAccount
|
|
184
|
-
|
|
185
|
-
const assets = await this.aci.getAssetsForNetwork({ baseAssetName: this.asset.name })
|
|
186
|
-
const tokens = Object.values(assets).filter((asset) => asset.baseAsset.name !== asset.name)
|
|
187
|
-
const tokensByAddress = tokens.reduce((map, token) => {
|
|
188
|
-
const addresses = getAssetAddresses(token)
|
|
189
|
-
for (const address of addresses) map.set(address.toLowerCase(), token)
|
|
190
|
-
return map
|
|
191
|
-
}, new Map())
|
|
192
|
-
const assetSource = { asset: this.asset.name, walletAccount }
|
|
193
|
-
|
|
194
|
-
const derivedData = await this.deriveData({ assetSource, tokens })
|
|
195
|
-
const derivedIndex = derivedData.currentAccountState?.index
|
|
196
|
-
|
|
197
|
-
const { allTransactionsFromServer, index } = await getHistoryFromServer({
|
|
198
|
-
server: this.server,
|
|
199
|
-
ourWalletAddress: derivedData.ourWalletAddress,
|
|
200
|
-
minimumConfirmations: derivedData.minimumConfirmations,
|
|
201
|
-
index: refresh ? 0 : (derivedIndex ?? 0),
|
|
202
|
-
})
|
|
203
|
-
const hasNewIndex = !derivedIndex || index > derivedIndex
|
|
204
|
-
|
|
205
|
-
const logItemsByAsset = getAllLogItemsByAsset({
|
|
206
|
-
getLogItemsFromServerTx,
|
|
207
|
-
ourWalletAddress: derivedData.ourWalletAddress,
|
|
208
|
-
allTransactionsFromServer,
|
|
209
|
-
asset: this.asset,
|
|
210
|
-
tokensByAddress,
|
|
211
|
-
assets,
|
|
212
|
-
})
|
|
213
|
-
|
|
214
|
-
const { txsToRemove } = await this.checkPendingTransactions({
|
|
215
|
-
txlist: allTransactionsFromServer,
|
|
216
|
-
assetSource,
|
|
217
|
-
refresh,
|
|
218
|
-
logItemsByAsset,
|
|
219
|
-
asset: this.asset,
|
|
220
|
-
...derivedData,
|
|
221
|
-
})
|
|
222
|
-
|
|
223
|
-
const accountState = await this.getNewAccountState({
|
|
224
|
-
tokens,
|
|
225
|
-
currentTokenBalances: derivedData.currentAccountState?.tokenBalances,
|
|
226
|
-
ourWalletAddress: derivedData.ourWalletAddress,
|
|
227
|
-
})
|
|
228
|
-
|
|
229
|
-
const newData = { ...accountState, ...(hasNewIndex && { index }) }
|
|
230
|
-
await this.updateAccountState({ newData, walletAccount })
|
|
231
|
-
await this.removeFromTxLog(txsToRemove)
|
|
232
|
-
await this.updateTxLogByAsset({ logItemsByAsset, walletAccount, refresh })
|
|
233
|
-
|
|
234
|
-
if (refresh || hasNewIndex) {
|
|
235
|
-
const unknownTokenAddresses = this.getUnknownTokenAddresses({
|
|
236
|
-
transactions: allTransactionsFromServer,
|
|
237
|
-
tokensByAddress,
|
|
238
|
-
})
|
|
239
|
-
if (unknownTokenAddresses.length > 0) {
|
|
240
|
-
this.emit('unknown-tokens', unknownTokenAddresses)
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
async updateGasPrice(fetchedGasPrices) {
|
|
246
|
-
try {
|
|
247
|
-
await executeEthLikeFeeMonitorUpdate({
|
|
248
|
-
assetClientInterface: this.aci,
|
|
249
|
-
feeAsset: this.asset,
|
|
250
|
-
fetchedGasPrices,
|
|
251
|
-
})
|
|
252
|
-
} catch (e) {
|
|
253
|
-
this.logger.warn('error updating gasPrice', e)
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
async getNewAccountState({ tokens, currentTokenBalances, ourWalletAddress }) {
|
|
258
|
-
const asset = this.asset
|
|
259
|
-
const newAccountState = Object.create(null)
|
|
260
|
-
const server = this.server
|
|
261
|
-
if (this.rpcBalanceAssetNames.includes(asset.name)) {
|
|
262
|
-
const result = await server.getBalanceProxied(ourWalletAddress)
|
|
263
|
-
const balance = fromHexToString(result)
|
|
264
|
-
newAccountState.balance = asset.currency.baseUnit(balance)
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
const tokenBalancePairs = await Promise.all(
|
|
268
|
-
tokens
|
|
269
|
-
.filter((token) => this.rpcBalanceAssetNames.includes(token.name) && token.contract.address)
|
|
270
|
-
.map(async (token) => {
|
|
271
|
-
const { confirmed } = await server.balanceOf(ourWalletAddress, token.contract.address)
|
|
272
|
-
const value = token.currency.baseUnit(confirmed[token.contract.address] || 0)
|
|
273
|
-
return [token.name, value]
|
|
274
|
-
})
|
|
275
|
-
)
|
|
276
|
-
|
|
277
|
-
const tokenBalances = excludeUnchangedTokenBalances(currentTokenBalances, tokenBalancePairs)
|
|
278
|
-
if (!isEmpty(tokenBalances)) newAccountState.tokenBalances = tokenBalances
|
|
279
|
-
return newAccountState
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
async getReceiveAddressesByWalletAccount() {
|
|
283
|
-
const walletAccounts = await this.aci.getWalletAccounts({ assetName: this.asset.name })
|
|
284
|
-
const addressesByAccount = Object.create(null)
|
|
285
|
-
for (const walletAccount of walletAccounts) {
|
|
286
|
-
addressesByAccount[walletAccount] = await this.aci.getReceiveAddresses({
|
|
287
|
-
assetName: this.asset.name,
|
|
288
|
-
walletAccount,
|
|
289
|
-
useCache: true,
|
|
290
|
-
})
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
return addressesByAccount
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
async _subscribeToWSNotifications() {
|
|
297
|
-
if (this._webSocketEnabled === false) return
|
|
298
|
-
|
|
299
|
-
const addressesByWalletAccount = await this.getReceiveAddressesByWalletAccount()
|
|
300
|
-
subscribeToWSNotifications({
|
|
301
|
-
addressesByWalletAccount,
|
|
302
|
-
tick: (...args) => this.tick(...args),
|
|
303
|
-
server: this.server,
|
|
304
|
-
})
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
getUnknownTokenAddresses({ transactions, tokensByAddress }) {
|
|
308
|
-
const set = transactions.reduce((acc, txn) => {
|
|
309
|
-
const transfers = txn.erc20 || []
|
|
310
|
-
transfers.forEach((transfer) => {
|
|
311
|
-
const addr = transfer.address.toLowerCase()
|
|
312
|
-
if (!tokensByAddress.has(addr)) {
|
|
313
|
-
acc.add(addr)
|
|
314
|
-
}
|
|
315
|
-
})
|
|
316
|
-
return acc
|
|
317
|
-
}, new Set())
|
|
318
|
-
return [...set]
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
async beforeStart() {
|
|
322
|
-
await this._enableWSUpdates()
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
async _enableWSUpdates() {
|
|
326
|
-
if (this._webSocketEnabled === false) return
|
|
327
|
-
|
|
328
|
-
const addressesByWalletAccount = await this.getReceiveAddressesByWalletAccount()
|
|
329
|
-
enableWSUpdates({
|
|
330
|
-
interval: this.interval,
|
|
331
|
-
server: this.server,
|
|
332
|
-
timer: this.timer,
|
|
333
|
-
tick: (...args) => this.tick(...args),
|
|
334
|
-
tickWalletAccounts: () => this.tickWalletAccounts(),
|
|
335
|
-
addressesByWalletAccount,
|
|
336
|
-
beforeStart: true,
|
|
337
|
-
})
|
|
338
|
-
|
|
339
|
-
if (this.config.GAS_PRICE_FROM_WEBSOCKET) {
|
|
340
|
-
subscribeToGasPriceNotifications({
|
|
341
|
-
server: this.server,
|
|
342
|
-
updateGasPrice: (newGasPrice) => this.updateGasPrice(newGasPrice),
|
|
343
|
-
subscribedToGasPriceMap: this.subscribedToGasPriceMap,
|
|
344
|
-
})
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
async afterStop() {
|
|
349
|
-
await this.server.stop()
|
|
350
|
-
}
|
|
351
|
-
}
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import isConfirmedServerTx from './is-confirmed-server-tx.js'
|
|
2
|
-
|
|
3
|
-
// Fetches JSON transaction history from the given Ethereum server
|
|
4
|
-
// object (from @exodus/ethereum-api). Starts fetching from the given
|
|
5
|
-
// block cursor and returns the latest cursor and all transactions
|
|
6
|
-
// fetched in the process. minimumConfirmations determines how many
|
|
7
|
-
// confirmations a transaction must have for the cursor to be moved
|
|
8
|
-
// to that transaction's block.
|
|
9
|
-
|
|
10
|
-
export default async function getHistoryFromServer({
|
|
11
|
-
server,
|
|
12
|
-
ourWalletAddress,
|
|
13
|
-
index,
|
|
14
|
-
minimumConfirmations,
|
|
15
|
-
limit = 1000,
|
|
16
|
-
}) {
|
|
17
|
-
if (limit === 0) return []
|
|
18
|
-
const allTransactionsFromServer = []
|
|
19
|
-
|
|
20
|
-
let isEndOfAddressHistory = false
|
|
21
|
-
let newIndex = index
|
|
22
|
-
while (true) {
|
|
23
|
-
const transactions = await server.getHistoryV2(ourWalletAddress, {
|
|
24
|
-
index,
|
|
25
|
-
limit, // the server returns the number of TXs that's less or equal to the limit
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
if (transactions.length === 0) isEndOfAddressHistory = true
|
|
29
|
-
for (const transaction of transactions) {
|
|
30
|
-
allTransactionsFromServer.push(transaction)
|
|
31
|
-
index = transaction.addressIndex + 1 // this index is used to fetch a next page of TXs
|
|
32
|
-
if (transaction.confirmations >= minimumConfirmations) newIndex = transaction.addressIndex + 1 // this index is used to return from the function
|
|
33
|
-
if (!isConfirmedServerTx(transaction)) isEndOfAddressHistory = true
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
if (isEndOfAddressHistory) {
|
|
37
|
-
return {
|
|
38
|
-
allTransactionsFromServer,
|
|
39
|
-
index: newIndex,
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
}
|
|
@@ -1,203 +0,0 @@
|
|
|
1
|
-
import lodash from 'lodash'
|
|
2
|
-
|
|
3
|
-
import getFeeAmount from './get-fee-amount.js'
|
|
4
|
-
import getNamesOfTokensTransferredByServerTx from './get-names-of-tokens-transferred-by-server-tx.js'
|
|
5
|
-
import getRawTransferAmount from './get-raw-transfer-amount.js'
|
|
6
|
-
import getTransfersByTokenName from './get-transfers-by-token-name.js'
|
|
7
|
-
import getValueOfTransfers from './get-value-of-transfers.js'
|
|
8
|
-
import isConfirmedServerTx from './is-confirmed-server-tx.js'
|
|
9
|
-
import isConsideredSentTokenTx from './is-considered-sent-token-tx.js'
|
|
10
|
-
|
|
11
|
-
// This function takes a server transaction object fetched from magnifier,
|
|
12
|
-
// and transforms it into Tx models to update the exodus state.
|
|
13
|
-
|
|
14
|
-
const tryFindExternalRecipient = (transfers, ourWalletAddress) =>
|
|
15
|
-
transfers.find(({ to }) => to !== ourWalletAddress)?.to || ourWalletAddress
|
|
16
|
-
|
|
17
|
-
function isSelfSendTx({
|
|
18
|
-
coinAmount,
|
|
19
|
-
ourWalletWasSender,
|
|
20
|
-
sendingTransferPresent,
|
|
21
|
-
receivingTransferPresent,
|
|
22
|
-
}) {
|
|
23
|
-
return (
|
|
24
|
-
coinAmount.isZero && sendingTransferPresent && receivingTransferPresent && ourWalletWasSender
|
|
25
|
-
)
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function isNftTransfer({ serverTx, ourWalletAddress }) {
|
|
29
|
-
if (!Array.isArray(serverTx.erc721) || serverTx.erc721.length === 0) return false
|
|
30
|
-
|
|
31
|
-
return serverTx.erc721.some(
|
|
32
|
-
(transfer) => transfer.to === ourWalletAddress || transfer.from === ourWalletAddress
|
|
33
|
-
)
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export default function getLogItemsFromServerTx({
|
|
37
|
-
serverTx,
|
|
38
|
-
asset,
|
|
39
|
-
ourWalletAddress,
|
|
40
|
-
tokensByAddress,
|
|
41
|
-
assets,
|
|
42
|
-
}) {
|
|
43
|
-
const confirmations = isConfirmedServerTx(serverTx) ? 1 : 0
|
|
44
|
-
const date = new Date(parseInt(serverTx.timestamp, 16) * 1000) // included even for unconfirmed txs
|
|
45
|
-
const txId = serverTx.hash
|
|
46
|
-
const nonce = parseInt(serverTx.nonce, 16)
|
|
47
|
-
const gasLimit = parseInt(serverTx.gas, 16)
|
|
48
|
-
const error = serverTx.error || (serverTx.status === false ? 'Failed' : null)
|
|
49
|
-
const feeAmount = getFeeAmount(asset, serverTx)
|
|
50
|
-
const ethereumTransfers = [serverTx, ...(serverTx.internal || [])]
|
|
51
|
-
const tokenTransfersByTokenName = getTransfersByTokenName(serverTx.erc20 || [], tokensByAddress)
|
|
52
|
-
const toAddress = tryFindExternalRecipient(ethereumTransfers, ourWalletAddress)
|
|
53
|
-
const ourWalletWasSender = serverTx.from === ourWalletAddress
|
|
54
|
-
const METHOD_ID_LENGTH = 10
|
|
55
|
-
const methodId = serverTx.data && {
|
|
56
|
-
methodId: serverTx.data.slice(0, Math.max(0, METHOD_ID_LENGTH)),
|
|
57
|
-
}
|
|
58
|
-
const data = serverTx.data || '0x'
|
|
59
|
-
const logItemCommonProperties = {
|
|
60
|
-
confirmations,
|
|
61
|
-
date,
|
|
62
|
-
error,
|
|
63
|
-
txId,
|
|
64
|
-
dropped: false,
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const logItemsForServerTxEntries = []
|
|
68
|
-
|
|
69
|
-
{
|
|
70
|
-
const sendingTransferPresent = ethereumTransfers.some(({ from }) => from === ourWalletAddress)
|
|
71
|
-
const receivingTransferPresent = ethereumTransfers.some(({ to }) => to === ourWalletAddress)
|
|
72
|
-
const nftTransferPresent = isNftTransfer({ serverTx, ourWalletAddress })
|
|
73
|
-
|
|
74
|
-
if (sendingTransferPresent || receivingTransferPresent || nftTransferPresent) {
|
|
75
|
-
const coinAmount = getValueOfTransfers(ourWalletAddress, asset, ethereumTransfers)
|
|
76
|
-
// `rawCoinAmount` mirrors the field written by `getOptimisticTxLogEffects`
|
|
77
|
-
// and is consumed by the bump path (`tx-create.js`) to recover the
|
|
78
|
-
// user-signed on-chain `value` when accelerating a self-send (where
|
|
79
|
-
// `coinAmount` is zero). For the base-asset entry the raw amount is the
|
|
80
|
-
// top-level `value` field of the tx itself, not the sum of internal
|
|
81
|
-
// transfers — that's what the user signed and what we must restore on
|
|
82
|
-
// bump.
|
|
83
|
-
const rawCoinAmount = getRawTransferAmount({
|
|
84
|
-
ourWalletAddress,
|
|
85
|
-
asset,
|
|
86
|
-
transfers: [serverTx],
|
|
87
|
-
}).toBaseString()
|
|
88
|
-
const selfSend = isSelfSendTx({
|
|
89
|
-
coinAmount,
|
|
90
|
-
ourWalletWasSender,
|
|
91
|
-
sendingTransferPresent,
|
|
92
|
-
receivingTransferPresent,
|
|
93
|
-
})
|
|
94
|
-
let sent
|
|
95
|
-
if (sendingTransferPresent) {
|
|
96
|
-
sent = serverTx.internal
|
|
97
|
-
.map(({ from, to, value }) => {
|
|
98
|
-
if (from === ourWalletAddress) {
|
|
99
|
-
const amount = asset.currency.baseUnit(value).toDefaultString({ unit: true })
|
|
100
|
-
return { address: to, amount }
|
|
101
|
-
}
|
|
102
|
-
})
|
|
103
|
-
.filter(Boolean)
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
logItemsForServerTxEntries.push([
|
|
107
|
-
asset.name,
|
|
108
|
-
{
|
|
109
|
-
...logItemCommonProperties,
|
|
110
|
-
coinAmount,
|
|
111
|
-
coinName: asset.name,
|
|
112
|
-
data: {
|
|
113
|
-
data,
|
|
114
|
-
nonce,
|
|
115
|
-
gasLimit,
|
|
116
|
-
rawCoinAmount,
|
|
117
|
-
...methodId,
|
|
118
|
-
...(sent?.length > 0 ? { sent } : undefined),
|
|
119
|
-
},
|
|
120
|
-
...(ourWalletWasSender
|
|
121
|
-
? { from: [], to: toAddress, feeAmount, feeCoinName: asset.feeAsset.name }
|
|
122
|
-
: { from: [serverTx.from] }),
|
|
123
|
-
selfSend,
|
|
124
|
-
tokens: getNamesOfTokensTransferredByServerTx({
|
|
125
|
-
asset,
|
|
126
|
-
tokensByAddress,
|
|
127
|
-
serverTx,
|
|
128
|
-
ourWalletAddress,
|
|
129
|
-
}),
|
|
130
|
-
currencies: {
|
|
131
|
-
[asset.name]: asset.currency,
|
|
132
|
-
[asset.feeAsset.name]: asset.feeAsset.currency,
|
|
133
|
-
},
|
|
134
|
-
},
|
|
135
|
-
])
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// handle erc20
|
|
140
|
-
Object.entries(tokenTransfersByTokenName).forEach(([tokenName, tokenTransfers]) => {
|
|
141
|
-
const sendingTransferPresent = tokenTransfers.some(({ from }) => from === ourWalletAddress)
|
|
142
|
-
const receivingTransferPresent = tokenTransfers.some(({ to }) => to === ourWalletAddress)
|
|
143
|
-
if (!sendingTransferPresent && !receivingTransferPresent) return
|
|
144
|
-
|
|
145
|
-
const token = assets[tokenName]
|
|
146
|
-
const tokenTransferToAddress = tryFindExternalRecipient(tokenTransfers, ourWalletAddress)
|
|
147
|
-
// Transfers from this reader's server source are tagged with
|
|
148
|
-
// `events: true` once the tx is included in a block (event-log derived)
|
|
149
|
-
// and `events: false` while still pending (tx-data derived). Only one
|
|
150
|
-
// flavor is real for a given lifecycle state; filtering avoids
|
|
151
|
-
// double-counting if both ever coexist for the same transfer. Both
|
|
152
|
-
// `coinAmount` and `rawCoinAmount` must see the same set so they stay
|
|
153
|
-
// consistent.
|
|
154
|
-
const filteredTokenTransfers = lodash.filter(tokenTransfers, {
|
|
155
|
-
events: confirmations > 0,
|
|
156
|
-
})
|
|
157
|
-
const coinAmount = getValueOfTransfers(ourWalletAddress, token, filteredTokenTransfers)
|
|
158
|
-
// See base-entry comment above; for token entries the raw amount is the
|
|
159
|
-
// absolute total tokens our wallet moved in this tx (sum of outgoing, or
|
|
160
|
-
// incoming for receive-only entries).
|
|
161
|
-
const rawCoinAmount = getRawTransferAmount({
|
|
162
|
-
ourWalletAddress,
|
|
163
|
-
asset: token,
|
|
164
|
-
transfers: filteredTokenTransfers,
|
|
165
|
-
}).toBaseString()
|
|
166
|
-
const tokenFromAddresses = lodash.uniq(
|
|
167
|
-
tokenTransfers.filter(({ to }) => to === ourWalletAddress).map(({ from }) => from)
|
|
168
|
-
)
|
|
169
|
-
|
|
170
|
-
const isConsideredSent = isConsideredSentTokenTx({
|
|
171
|
-
coinAmount,
|
|
172
|
-
ourWalletWasSender,
|
|
173
|
-
sendingTransferPresent,
|
|
174
|
-
receivingTransferPresent,
|
|
175
|
-
})
|
|
176
|
-
const selfSend = isSelfSendTx({
|
|
177
|
-
coinAmount,
|
|
178
|
-
ourWalletWasSender,
|
|
179
|
-
sendingTransferPresent,
|
|
180
|
-
receivingTransferPresent,
|
|
181
|
-
})
|
|
182
|
-
|
|
183
|
-
logItemsForServerTxEntries.push([
|
|
184
|
-
tokenName,
|
|
185
|
-
{
|
|
186
|
-
...logItemCommonProperties,
|
|
187
|
-
coinAmount,
|
|
188
|
-
coinName: tokenName,
|
|
189
|
-
data: { data, nonce, gasLimit, rawCoinAmount, ...methodId },
|
|
190
|
-
...(isConsideredSent
|
|
191
|
-
? { from: [], to: tokenTransferToAddress, feeAmount, feeCoinName: asset.feeAsset.name }
|
|
192
|
-
: { from: tokenFromAddresses }),
|
|
193
|
-
selfSend,
|
|
194
|
-
currencies: {
|
|
195
|
-
[token.name]: token.currency,
|
|
196
|
-
[token.feeAsset.name]: token.feeAsset.currency,
|
|
197
|
-
},
|
|
198
|
-
},
|
|
199
|
-
])
|
|
200
|
-
})
|
|
201
|
-
|
|
202
|
-
return Object.fromEntries(logItemsForServerTxEntries)
|
|
203
|
-
}
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import { FEE_PAYMENT_PREFIX } from '@exodus/ethereum-lib'
|
|
2
|
-
import lodash from 'lodash'
|
|
3
|
-
|
|
4
|
-
import getValueOfTransfers from './get-value-of-transfers.js'
|
|
5
|
-
|
|
6
|
-
// For ETH transactions, we store an array of token names inside
|
|
7
|
-
// the TX model. This array is used to display text in the UI for
|
|
8
|
-
// transactions which pay fees for ERC20 token transfers. This
|
|
9
|
-
// function is used to determine that array, choosing which
|
|
10
|
-
// tokens a transaction ostensibly 'paid fees' for.
|
|
11
|
-
|
|
12
|
-
export default function getNamesOfTokensTransferredByServerTx({
|
|
13
|
-
asset,
|
|
14
|
-
tokensByAddress,
|
|
15
|
-
ourWalletAddress,
|
|
16
|
-
serverTx,
|
|
17
|
-
}) {
|
|
18
|
-
// Treat smart contract ETH transfer transactions as ETH transfers, not token transfers
|
|
19
|
-
if (!getValueOfTransfers(ourWalletAddress, asset, serverTx.internal || []).isZero) {
|
|
20
|
-
return []
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const tokenAddresses = (serverTx.erc20 || []).map((event) => event.address)
|
|
24
|
-
if (
|
|
25
|
-
tokenAddresses.length === 0 &&
|
|
26
|
-
serverTx.data &&
|
|
27
|
-
serverTx.data !== '0x' &&
|
|
28
|
-
!serverTx.data.startsWith(FEE_PAYMENT_PREFIX)
|
|
29
|
-
) {
|
|
30
|
-
// We may still be transacting with tokens even though we found no `tokenAddresses`. This includes
|
|
31
|
-
// failed token transactions as well as other transactions sent to a token contract. If we do not
|
|
32
|
-
// recognize the `to` address in the next step, then we will not treat it as a token contract address.
|
|
33
|
-
tokenAddresses.push(serverTx.to)
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
return lodash
|
|
37
|
-
.uniq(tokenAddresses)
|
|
38
|
-
.map((address) => tokensByAddress.get(address)?.name)
|
|
39
|
-
.filter(Boolean)
|
|
40
|
-
}
|
package/src/tx-log/ws-updates.js
DELETED
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import ms from 'ms'
|
|
2
|
-
|
|
3
|
-
const subscribedToAddressNotificationsMap = new Map()
|
|
4
|
-
|
|
5
|
-
const UPDATE_LOOP_INTERVAL = ms('5m')
|
|
6
|
-
|
|
7
|
-
export function subscribeToWSNotifications({
|
|
8
|
-
addressesByWalletAccount,
|
|
9
|
-
tick,
|
|
10
|
-
server,
|
|
11
|
-
beforeStart = false,
|
|
12
|
-
}) {
|
|
13
|
-
Object.entries(addressesByWalletAccount).forEach(([walletAccount, addresses]) => {
|
|
14
|
-
const address = String([...addresses][0]).toLowerCase() // Only check m/0/0
|
|
15
|
-
const mapKey = `${server.getURL()}:${address}` // different chains might have the same addresses
|
|
16
|
-
if (subscribedToAddressNotificationsMap.has(mapKey)) return
|
|
17
|
-
|
|
18
|
-
server.ws.watch(address)
|
|
19
|
-
server.ws.events.on(`address-${address}`, () => {
|
|
20
|
-
tick({ refresh: false, walletAccount })
|
|
21
|
-
})
|
|
22
|
-
subscribedToAddressNotificationsMap.set(mapKey, true)
|
|
23
|
-
if (!beforeStart) {
|
|
24
|
-
// tick for a new wallet account
|
|
25
|
-
tick({ refresh: false, walletAccount })
|
|
26
|
-
}
|
|
27
|
-
})
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function subscribeToGasPriceNotifications({
|
|
31
|
-
server,
|
|
32
|
-
updateGasPrice,
|
|
33
|
-
subscribedToGasPriceMap,
|
|
34
|
-
}) {
|
|
35
|
-
const mapKey = server.getURL()
|
|
36
|
-
if (subscribedToGasPriceMap.has(mapKey)) return
|
|
37
|
-
server.ws.events.on('gasprice', updateGasPrice)
|
|
38
|
-
subscribedToGasPriceMap.set(mapKey, true)
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export function enableWSUpdates({
|
|
42
|
-
interval,
|
|
43
|
-
server,
|
|
44
|
-
timer,
|
|
45
|
-
tickWalletAccounts,
|
|
46
|
-
tick,
|
|
47
|
-
addressesByWalletAccount,
|
|
48
|
-
beforeStart = false,
|
|
49
|
-
}) {
|
|
50
|
-
const defaultInterval = interval
|
|
51
|
-
|
|
52
|
-
subscribeToWSNotifications({
|
|
53
|
-
server,
|
|
54
|
-
beforeStart,
|
|
55
|
-
tick,
|
|
56
|
-
addressesByWalletAccount,
|
|
57
|
-
})
|
|
58
|
-
|
|
59
|
-
server.ws.events.on('open', () => {
|
|
60
|
-
if (timer.isRunning) {
|
|
61
|
-
timer.setNewInterval(UPDATE_LOOP_INTERVAL)
|
|
62
|
-
}
|
|
63
|
-
})
|
|
64
|
-
server.ws.events.on('close', async () => {
|
|
65
|
-
if (timer.isRunning) {
|
|
66
|
-
await timer.setNewInterval(defaultInterval)
|
|
67
|
-
return tickWalletAccounts()
|
|
68
|
-
}
|
|
69
|
-
})
|
|
70
|
-
server.ws.open()
|
|
71
|
-
}
|