@exodus/ethereum-api 8.73.2 → 8.73.3
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 +16 -0
- package/package.json +3 -4
- package/src/error-wrapper.js +7 -4
- package/src/staking/ethereum/api.js +1 -1
- package/src/tx-log-staking-processor/asset-staking-tx-data.js +40 -13
- package/src/tx-log-staking-processor/index.js +9 -13
- package/src/tx-send/nonce-utils.js +3 -1
- package/src/etherscan/account.js +0 -50
- package/src/etherscan/index.js +0 -28
- package/src/etherscan/logs.js +0 -17
- package/src/etherscan/proxy.js +0 -48
- package/src/etherscan/request.js +0 -26
- package/src/etherscan/ws.js +0 -88
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,22 @@
|
|
|
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.73.3](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.73.2...@exodus/ethereum-api@8.73.3) (2026-05-06)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
* fix: correct misleading assertion and error messages (#7965)
|
|
13
|
+
|
|
14
|
+
* fix(ethereum-api): avoid mutating caller's txLog in resolveNonce (#7996)
|
|
15
|
+
|
|
16
|
+
* fix(ethereum-api): filter hints before pushing to stack in EthLikeError (#7991)
|
|
17
|
+
|
|
18
|
+
* fix: missing matic claim unstake transactions (#8006)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
6
22
|
## [8.73.2](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.73.1...@exodus/ethereum-api@8.73.2) (2026-05-05)
|
|
7
23
|
|
|
8
24
|
**Note:** Version bump only for package @exodus/ethereum-api
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/ethereum-api",
|
|
3
|
-
"version": "8.73.
|
|
3
|
+
"version": "8.73.3",
|
|
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",
|
|
@@ -49,8 +49,7 @@
|
|
|
49
49
|
"make-concurrent": "^4.0.0",
|
|
50
50
|
"minimalistic-assert": "^1.0.1",
|
|
51
51
|
"ms": "^2.1.1",
|
|
52
|
-
"socket.io-client": "^2.1.1"
|
|
53
|
-
"ws": "^6.1.0"
|
|
52
|
+
"socket.io-client": "^2.1.1"
|
|
54
53
|
},
|
|
55
54
|
"devDependencies": {
|
|
56
55
|
"@exodus/assets-testing": "^1.0.0",
|
|
@@ -68,5 +67,5 @@
|
|
|
68
67
|
"type": "git",
|
|
69
68
|
"url": "git+https://github.com/ExodusMovement/assets.git"
|
|
70
69
|
},
|
|
71
|
-
"gitHead": "
|
|
70
|
+
"gitHead": "fe7c27f31aa14bf777660af02300c92def4b4a8c"
|
|
72
71
|
}
|
package/src/error-wrapper.js
CHANGED
|
@@ -293,9 +293,12 @@ export class EthLikeError extends Error {
|
|
|
293
293
|
constructor({ message, errorReasonInfo, hint, traceId, baseAssetName }) {
|
|
294
294
|
super(message)
|
|
295
295
|
this.name = safeString`EthLikeError`
|
|
296
|
-
|
|
296
|
+
const filteredHint = this.#extractHint(hint)
|
|
297
|
+
// Only filtered hints are kept on the stack so that subsequent joins via
|
|
298
|
+
// `addHint` cannot leak sensitive info or untruncated content.
|
|
299
|
+
this.#hintStack = filteredHint ? [filteredHint] : []
|
|
297
300
|
this.reason = errorReasonInfo.reason
|
|
298
|
-
this.hint =
|
|
301
|
+
this.hint = filteredHint
|
|
299
302
|
this.type = errorReasonInfo.type
|
|
300
303
|
this.traceId = traceId
|
|
301
304
|
this.baseAssetName = baseAssetName
|
|
@@ -307,8 +310,8 @@ export class EthLikeError extends Error {
|
|
|
307
310
|
return this
|
|
308
311
|
}
|
|
309
312
|
|
|
310
|
-
this.#hintStack.push(
|
|
311
|
-
this.hint =
|
|
313
|
+
this.#hintStack.push(filteredHint)
|
|
314
|
+
this.hint = this.#hintStack.join(':')
|
|
312
315
|
return this
|
|
313
316
|
}
|
|
314
317
|
|
|
@@ -28,7 +28,10 @@ const getEthereumStakingTxData = ({ tx, currency }) => {
|
|
|
28
28
|
const txAmount = tx.coinAmount.toDefaultString()
|
|
29
29
|
|
|
30
30
|
if (isEthereumDelegate(tx)) {
|
|
31
|
-
return {
|
|
31
|
+
return {
|
|
32
|
+
coinAmount: currency.ZERO,
|
|
33
|
+
data: { delegate: txAmount, txAmount },
|
|
34
|
+
}
|
|
32
35
|
}
|
|
33
36
|
|
|
34
37
|
// undelegate must be taken in consideration, if unstaked ETH is still
|
|
@@ -38,28 +41,33 @@ const getEthereumStakingTxData = ({ tx, currency }) => {
|
|
|
38
41
|
.baseUnit(decodeEthLikeStakingTxInputAmount(tx))
|
|
39
42
|
.toDefaultString()
|
|
40
43
|
return {
|
|
41
|
-
|
|
42
|
-
txAmount,
|
|
44
|
+
coinAmount: currency.ZERO,
|
|
45
|
+
data: { undelegatePending, txAmount },
|
|
43
46
|
}
|
|
44
47
|
}
|
|
45
48
|
|
|
46
49
|
if (isEthereumUndelegate(tx)) {
|
|
47
50
|
const undelegate = currency.baseUnit(decodeEthLikeStakingTxInputAmount(tx)).toDefaultString()
|
|
48
|
-
return {
|
|
51
|
+
return {
|
|
52
|
+
coinAmount: currency.ZERO,
|
|
53
|
+
data: { undelegate, txAmount },
|
|
54
|
+
}
|
|
49
55
|
}
|
|
50
56
|
|
|
51
57
|
// In the case of the ETH being actually staked and earning,
|
|
52
58
|
// unstake has a withdraw period, after that, unstaked can be claimed.
|
|
53
59
|
if (isEthereumClaimUndelegate(tx)) {
|
|
54
|
-
return {
|
|
60
|
+
return {
|
|
61
|
+
coinAmount: currency.ZERO,
|
|
62
|
+
data: { claimUndelegate: txAmount, txAmount },
|
|
63
|
+
}
|
|
55
64
|
}
|
|
56
65
|
}
|
|
57
66
|
|
|
58
67
|
const getPolygonStakingTxData = ({ tx, currency }) => {
|
|
59
|
-
if (
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
) {
|
|
68
|
+
if (tx.data?.claimUndelegate) return
|
|
69
|
+
|
|
70
|
+
if (['delegate', 'undelegate'].some((stakeTx) => tx.data?.[stakeTx]) && tx.coinAmount.isZero) {
|
|
63
71
|
return
|
|
64
72
|
}
|
|
65
73
|
|
|
@@ -69,22 +77,41 @@ const getPolygonStakingTxData = ({ tx, currency }) => {
|
|
|
69
77
|
const delegate = currency.baseUnit(decodePolygonStakingTxInputAmount(tx)).toDefaultString()
|
|
70
78
|
// MATIC returned in unstake tx is always reward
|
|
71
79
|
const rewards = calculateRewardsFromStakeTx({ tx, currency })
|
|
72
|
-
return {
|
|
80
|
+
return {
|
|
81
|
+
coinAmount: currency.ZERO,
|
|
82
|
+
data: {
|
|
83
|
+
delegate,
|
|
84
|
+
txAmount,
|
|
85
|
+
...(rewards ? { rewards } : {}),
|
|
86
|
+
},
|
|
87
|
+
}
|
|
73
88
|
}
|
|
74
89
|
|
|
75
90
|
if (isPolygonUndelegate(tx)) {
|
|
76
91
|
const undelegate = currency.baseUnit(decodePolygonStakingTxInputAmount(tx)).toDefaultString()
|
|
77
92
|
// MATIC returned in unstake tx is always reward
|
|
78
93
|
const rewards = txAmount
|
|
79
|
-
return {
|
|
94
|
+
return {
|
|
95
|
+
coinAmount: currency.ZERO,
|
|
96
|
+
data: { undelegate, txAmount, rewards },
|
|
97
|
+
}
|
|
80
98
|
}
|
|
81
99
|
|
|
82
100
|
if (isPolygonClaimUndelegate(tx)) {
|
|
83
|
-
return {
|
|
101
|
+
return {
|
|
102
|
+
// NOTE: We intentionally omit for compatibility with `rx`:
|
|
103
|
+
// https://github.com/ExodusMovement/exodus-mobile/blob/229c9c0634af3e8e17eb1624019682c6fffe29bf/src/utils/getTxTag.js#L186
|
|
104
|
+
//
|
|
105
|
+
// coinAmount: currency.ZERO,
|
|
106
|
+
data: {
|
|
107
|
+
claimUndelegate: txAmount,
|
|
108
|
+
txAmount,
|
|
109
|
+
},
|
|
110
|
+
}
|
|
84
111
|
}
|
|
85
112
|
}
|
|
86
113
|
|
|
87
|
-
export const
|
|
114
|
+
export const assetStakingTxProps = {
|
|
88
115
|
polygon: getPolygonStakingTxData,
|
|
89
116
|
ethereum: getEthereumStakingTxData,
|
|
90
117
|
ethereumholesky: getEthereumStakingTxData,
|
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
import { getPolygonUndelegateTxInEthereumTxLog } from '../staking/matic/matic-staking-utils.js'
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
const getTxStakingData = ({ assetName, currency, tx }) => {
|
|
5
|
-
return assetStakingTxData[assetName]({ tx, currency })
|
|
6
|
-
}
|
|
2
|
+
import { assetStakingTxProps } from './asset-staking-tx-data.js'
|
|
7
3
|
|
|
8
4
|
const getAssetExpandedTxLog = async ({ assetName, aci, txs, walletAccount }) => {
|
|
9
5
|
const additionalTxs = []
|
|
@@ -27,14 +23,14 @@ const processTxLog = async ({ asset, assetClientInterface: aci, walletAccount, b
|
|
|
27
23
|
|
|
28
24
|
const newTxs = []
|
|
29
25
|
for (const tx of txs) {
|
|
30
|
-
const
|
|
31
|
-
if (
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
}
|
|
26
|
+
const stakingProps = assetStakingTxProps[assetName]?.({ tx, currency })
|
|
27
|
+
if (!stakingProps) continue
|
|
28
|
+
|
|
29
|
+
newTxs.push({
|
|
30
|
+
...tx,
|
|
31
|
+
...stakingProps,
|
|
32
|
+
data: { ...tx.data, ...stakingProps.data },
|
|
33
|
+
})
|
|
38
34
|
}
|
|
39
35
|
|
|
40
36
|
const expandedTxs = await getAssetExpandedTxLog({ assetName, aci, txs, walletAccount })
|
|
@@ -55,7 +55,9 @@ const getNonceFromTxLog = ({ txLog, useAbsoluteNonce, tag }) => {
|
|
|
55
55
|
let absoluteNonce = 0
|
|
56
56
|
|
|
57
57
|
if (useAbsoluteNonce) {
|
|
58
|
-
|
|
58
|
+
// NOTE: Use a copy to avoid mutating the caller's `txLog` array, since
|
|
59
|
+
// `Array.prototype.reverse()` reverses in-place.
|
|
60
|
+
const reversedTxLog = [...txLog].reverse()
|
|
59
61
|
const maybeLatestTxWithNonceChange = getLatestTxWithNonceChange({ reversedTxLog })
|
|
60
62
|
|
|
61
63
|
if (maybeLatestTxWithNonceChange) {
|
package/src/etherscan/account.js
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import assert from 'minimalistic-assert'
|
|
2
|
-
|
|
3
|
-
import request from './request.js'
|
|
4
|
-
|
|
5
|
-
const isValidResponseCheck = (x) =>
|
|
6
|
-
(x.status === '1' && x.message === 'OK') || x.message === 'No transactions found'
|
|
7
|
-
const _request = async (...args) => request(isValidResponseCheck, 'account', ...args)
|
|
8
|
-
|
|
9
|
-
export async function fetchBalance(address) {
|
|
10
|
-
const balance = await _request('balance', { address })
|
|
11
|
-
|
|
12
|
-
const isValid = /^\d+$/.test(balance)
|
|
13
|
-
if (!isValid) throw new RangeError(`Invalid balance: ${balance}`)
|
|
14
|
-
|
|
15
|
-
return balance
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export async function fetchTxlist(address, options) {
|
|
19
|
-
const params = { startblock: 0, endblock: 'latest', ...options, address }
|
|
20
|
-
const txlist = await _request('txlist', params)
|
|
21
|
-
|
|
22
|
-
// simple check
|
|
23
|
-
assert(Array.isArray(txlist), `Invalid transactions: ${txlist}`)
|
|
24
|
-
|
|
25
|
-
return txlist
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export async function fetchTxlistinternal(address, options) {
|
|
29
|
-
const params = { startblock: 0, endblock: 'latest', ...options, address }
|
|
30
|
-
const txlist = await _request('txlistinternal', params)
|
|
31
|
-
|
|
32
|
-
// simple check
|
|
33
|
-
assert(Array.isArray(txlist), `Invalid transactions: ${txlist}`)
|
|
34
|
-
|
|
35
|
-
return txlist
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export async function tokenBalance(token, address) {
|
|
39
|
-
const params = {
|
|
40
|
-
[token.length === 42 ? 'contractaddress' : 'tokenname']: token,
|
|
41
|
-
address,
|
|
42
|
-
tag: 'latest',
|
|
43
|
-
}
|
|
44
|
-
const balance = await _request('tokenbalance', params)
|
|
45
|
-
|
|
46
|
-
const isValid = /^\d+$/.test(balance)
|
|
47
|
-
if (!isValid) throw new RangeError(`Invalid balance: ${balance}`)
|
|
48
|
-
|
|
49
|
-
return balance
|
|
50
|
-
}
|
package/src/etherscan/index.js
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import createWebSocket from './ws.js'
|
|
2
|
-
|
|
3
|
-
export const ETHERSCAN_WS_URL = 'wss://socket.etherscan.io/wshandler'
|
|
4
|
-
|
|
5
|
-
export const ws = createWebSocket(ETHERSCAN_WS_URL)
|
|
6
|
-
|
|
7
|
-
export function filterTxsSent(addr, etherscanTxs) {
|
|
8
|
-
return etherscanTxs.filter((tx) => tx.from.toLowerCase() === addr.toLowerCase())
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export function filterTxsReceived(addr, etherscanTxs) {
|
|
12
|
-
return etherscanTxs.filter((tx) => tx.to.toLowerCase() === addr.toLowerCase())
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export { fetchBalance, fetchTxlistinternal, fetchTxlist, tokenBalance } from './account.js'
|
|
16
|
-
export {
|
|
17
|
-
sendRawTransaction,
|
|
18
|
-
getTransactionCount,
|
|
19
|
-
estimateGas,
|
|
20
|
-
getTransactionReceipt,
|
|
21
|
-
getCode,
|
|
22
|
-
ethCall,
|
|
23
|
-
gasPrice,
|
|
24
|
-
} from './proxy.js'
|
|
25
|
-
|
|
26
|
-
export { setEtherscanApiKey as setApiKey } from './request.js'
|
|
27
|
-
|
|
28
|
-
export { getLogs } from './logs.js'
|
package/src/etherscan/logs.js
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import assert from 'minimalistic-assert'
|
|
2
|
-
|
|
3
|
-
import request from './request.js'
|
|
4
|
-
|
|
5
|
-
const isValidResponseCheck = (x) =>
|
|
6
|
-
(x.status === '1' && x.message === 'OK') || x.message === 'No records found'
|
|
7
|
-
const _request = async (...args) => request(isValidResponseCheck, 'logs', ...args)
|
|
8
|
-
|
|
9
|
-
export async function getLogs(address, fromBlock, toBlock, options) {
|
|
10
|
-
const params = { ...options, address, fromBlock, toBlock }
|
|
11
|
-
const events = await _request('getLogs', params)
|
|
12
|
-
|
|
13
|
-
// simple check
|
|
14
|
-
assert(Array.isArray(events), `Invalid transactions: ${events}`)
|
|
15
|
-
|
|
16
|
-
return events
|
|
17
|
-
}
|
package/src/etherscan/proxy.js
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import request from './request.js'
|
|
2
|
-
|
|
3
|
-
const isValidResponseCheck = (x) => x.result !== undefined
|
|
4
|
-
const _request = async (...args) => request(isValidResponseCheck, 'proxy', ...args)
|
|
5
|
-
|
|
6
|
-
export async function sendRawTransaction(data) {
|
|
7
|
-
const _data = data instanceof Uint8Array ? Buffer.from(data).toString('hex') : data
|
|
8
|
-
const txhash = await _request('eth_sendRawTransaction', { hex: '0x' + _data })
|
|
9
|
-
|
|
10
|
-
const isValidTxHash = /^0x[\dA-Fa-f]{64}$/.test(txhash)
|
|
11
|
-
if (!isValidTxHash) throw new Error(`Invalid tx hash: ${txhash}`)
|
|
12
|
-
|
|
13
|
-
return txhash.slice(2)
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export async function getTransactionCount(address, tag = 'latest') {
|
|
17
|
-
return _request('eth_getTransactionCount', { address, tag })
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export async function getTransactionReceipt(txhash) {
|
|
21
|
-
return _request('eth_getTransactionReceipt', { txhash })
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export async function estimateGas(data) {
|
|
25
|
-
return _request('eth_estimateGas', data)
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export async function getCode(address) {
|
|
29
|
-
const code = await _request('eth_getCode', { address })
|
|
30
|
-
|
|
31
|
-
const isValidCode = /^0x[\dA-Fa-f]*$/.test(code) && code.length % 2 === 0
|
|
32
|
-
if (!isValidCode) throw new Error(`Invalid address code: ${code}`)
|
|
33
|
-
|
|
34
|
-
return code
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export async function gasPrice() {
|
|
38
|
-
const price = await _request('eth_gasPrice')
|
|
39
|
-
|
|
40
|
-
const isValidPrice = /^0x[\dA-Fa-f]+$/.test(price)
|
|
41
|
-
if (!isValidPrice) throw new Error(`Invalid price: ${price}`)
|
|
42
|
-
|
|
43
|
-
return price
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export async function ethCall(data) {
|
|
47
|
-
return _request('eth_call', data)
|
|
48
|
-
}
|
package/src/etherscan/request.js
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import fetchival from '@exodus/fetch/experimental/fetchival'
|
|
2
|
-
import makeConcurrent from 'make-concurrent'
|
|
3
|
-
import ms from 'ms'
|
|
4
|
-
|
|
5
|
-
const ETHERSCAN_API_URL = 'https://api.etherscan.io/api'
|
|
6
|
-
const DEFAULT_ETHERSCAN_API_KEY = 'XM3VGRSNW1TMSIR14I9MVFP15X74GNHTRI'
|
|
7
|
-
|
|
8
|
-
let etherscanApiKey = DEFAULT_ETHERSCAN_API_KEY
|
|
9
|
-
|
|
10
|
-
export function setEtherscanApiKey(apiKey) {
|
|
11
|
-
etherscanApiKey = apiKey || DEFAULT_ETHERSCAN_API_KEY
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export default makeConcurrent(
|
|
15
|
-
async function (isValidResponseCheck, module, action, params = {}) {
|
|
16
|
-
const data = await fetchival(new URL(ETHERSCAN_API_URL), { timeout: ms('15s') }).get({
|
|
17
|
-
...params,
|
|
18
|
-
module,
|
|
19
|
-
action,
|
|
20
|
-
apiKey: etherscanApiKey,
|
|
21
|
-
})
|
|
22
|
-
if (!isValidResponseCheck(data)) throw new Error(`Invalid response: ${JSON.stringify(data)}`)
|
|
23
|
-
return data.result
|
|
24
|
-
},
|
|
25
|
-
{ concurrency: 3 }
|
|
26
|
-
)
|
package/src/etherscan/ws.js
DELETED
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
import WebSocket from '@exodus/fetch/websocket'
|
|
2
|
-
import EventEmitter from 'events/events.js' // forces it to use the module from node_modules
|
|
3
|
-
import ms from 'ms'
|
|
4
|
-
|
|
5
|
-
const RECONNECT_INTERVAL = ms('10s')
|
|
6
|
-
const PING_INTERVAL = ms('20s')
|
|
7
|
-
|
|
8
|
-
export default function createWebSocket(url) {
|
|
9
|
-
const addresses = new Set()
|
|
10
|
-
const events = new EventEmitter().setMaxListeners(20)
|
|
11
|
-
const pingMessage = JSON.stringify({ event: 'ping' })
|
|
12
|
-
let ws
|
|
13
|
-
let wsOpened = false
|
|
14
|
-
let opened = false
|
|
15
|
-
let openTimeoutId
|
|
16
|
-
let pingIntervalId
|
|
17
|
-
|
|
18
|
-
function subscribeAddress(address) {
|
|
19
|
-
const data = JSON.stringify({ event: 'txlist', address })
|
|
20
|
-
ws.send(data)
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function onMessage(data) {
|
|
24
|
-
data = JSON.parse(data)
|
|
25
|
-
switch (data.event) {
|
|
26
|
-
case 'txlist':
|
|
27
|
-
for (const tx of data.result) events.emit(`address-${data.address}`, tx)
|
|
28
|
-
break
|
|
29
|
-
|
|
30
|
-
case 'subscribe-txlist':
|
|
31
|
-
const match = data.message.toLowerCase().match(/0x[\da-f]{40}/)
|
|
32
|
-
if (match && data.status === '1') events.emit(`address-${match[0]}-subscribed`)
|
|
33
|
-
else ws.close()
|
|
34
|
-
break
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function isOpened() {
|
|
39
|
-
return wsOpened
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function open() {
|
|
43
|
-
opened = true
|
|
44
|
-
clearTimeout(openTimeoutId)
|
|
45
|
-
if (ws) return
|
|
46
|
-
|
|
47
|
-
ws = new WebSocket(url)
|
|
48
|
-
|
|
49
|
-
ws.on('message', (data) => {
|
|
50
|
-
try {
|
|
51
|
-
onMessage(data)
|
|
52
|
-
} catch {}
|
|
53
|
-
})
|
|
54
|
-
ws.once('open', () => {
|
|
55
|
-
for (const address of addresses.values()) subscribeAddress(address)
|
|
56
|
-
pingIntervalId = setInterval(() => ws && ws.send(pingMessage), PING_INTERVAL)
|
|
57
|
-
wsOpened = true
|
|
58
|
-
events.emit('open')
|
|
59
|
-
})
|
|
60
|
-
ws.once('close', () => {
|
|
61
|
-
ws = null
|
|
62
|
-
clearInterval(pingIntervalId)
|
|
63
|
-
if (opened) openTimeoutId = setTimeout(open, RECONNECT_INTERVAL)
|
|
64
|
-
wsOpened = false
|
|
65
|
-
events.emit('close')
|
|
66
|
-
})
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function close() {
|
|
70
|
-
opened = false
|
|
71
|
-
clearTimeout(openTimeoutId)
|
|
72
|
-
if (!ws) return
|
|
73
|
-
|
|
74
|
-
ws.close()
|
|
75
|
-
ws = null
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function watch(address) {
|
|
79
|
-
address = address.toLowerCase()
|
|
80
|
-
|
|
81
|
-
if (addresses.has(address)) return
|
|
82
|
-
addresses.add(address)
|
|
83
|
-
|
|
84
|
-
if (wsOpened) subscribeAddress(address)
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
return { events, isOpened, open, close, watch }
|
|
88
|
-
}
|