@exodus/ethereum-api 9.0.3 → 10.1.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 +22 -0
- package/package.json +2 -2
- package/src/create-asset-utils.js +6 -16
- package/src/create-asset.js +32 -10
- package/src/create-token-factory.js +18 -7
- 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 +15 -1
- package/src/nft-utils.js +6 -2
- package/src/server-based-fee-monitor.js +1 -1
- package/src/staking/api/index.js +2 -1
- package/src/staking/ethereum/api.js +2 -3
- package/src/staking/ethereum/deps.js +3 -0
- package/src/staking/ethereum/min-amount.js +1 -0
- package/src/staking/ethereum/service.js +148 -123
- package/src/staking/ethereum/staking-adapter.js +382 -0
- package/src/staking/evm/compose-staking-api.js +42 -0
- package/src/staking/evm/create-evm-staking-api.js +269 -0
- package/src/staking/evm/staking-errors.js +37 -0
- package/src/staking/evm/staking-txs.js +107 -0
- package/src/staking/matic/index.js +1 -0
- 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
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export const createRejectionError = ({ operation, check }) => {
|
|
2
|
+
const error = new Error(`Cannot ${operation}: ${check.reasonCode}`)
|
|
3
|
+
error.name = 'StakingRejectionError'
|
|
4
|
+
error.reasonCode = check.reasonCode
|
|
5
|
+
if (check.shortfall) {
|
|
6
|
+
error.shortfall = check.shortfall
|
|
7
|
+
error.shortfallAssetName = check.shortfallAssetName
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
return error
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const createSubmitError = ({ error, txIds, failedIndex }) => {
|
|
14
|
+
const submitError = new Error(error.message, { cause: error })
|
|
15
|
+
submitError.name = 'StakingSubmitError'
|
|
16
|
+
submitError.txIds = txIds
|
|
17
|
+
submitError.failedIndex = failedIndex
|
|
18
|
+
return submitError
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const fail = (reasonCode, shortfall, shortfallAssetName) => ({
|
|
22
|
+
ok: false,
|
|
23
|
+
reasonCode,
|
|
24
|
+
...(shortfall && { shortfall, shortfallAssetName }),
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
export const pass = (warnings = []) => (warnings.length > 0 ? { ok: true, warnings } : { ok: true })
|
|
28
|
+
|
|
29
|
+
export const sumOperationFees = ({ estimateOperationFee, operations }) => {
|
|
30
|
+
if (!estimateOperationFee) return
|
|
31
|
+
const fees = operations.map(estimateOperationFee)
|
|
32
|
+
if (fees.some((fee) => !fee)) return
|
|
33
|
+
return fees.reduce((total, fee) => total.add(fee))
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const warnIfBelow = ({ balance, required, warning }) =>
|
|
37
|
+
pass(balance && required && balance.lt(required) ? [warning] : [])
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import assert from 'minimalistic-assert'
|
|
2
|
+
|
|
3
|
+
export const STAKING_TX_MODE = Object.freeze({ single: 'single', bundle: 'bundle' })
|
|
4
|
+
|
|
5
|
+
const createSingleTx = async ({ asset, walletAccount, step, gasPrice, tipGasPrice, feeData }) => {
|
|
6
|
+
const createdTx = await asset.baseAsset.api.createTx({
|
|
7
|
+
asset,
|
|
8
|
+
walletAccount,
|
|
9
|
+
address: step.to,
|
|
10
|
+
amount: step.amount || asset.currency.ZERO,
|
|
11
|
+
txInput: step.txInput,
|
|
12
|
+
gasPrice,
|
|
13
|
+
gasLimit: step.gasLimit,
|
|
14
|
+
tipGasPrice,
|
|
15
|
+
feeData,
|
|
16
|
+
})
|
|
17
|
+
return [createdTx]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const createBundleTxs = async ({
|
|
21
|
+
asset,
|
|
22
|
+
walletAccount,
|
|
23
|
+
fromAddress,
|
|
24
|
+
steps,
|
|
25
|
+
gasPrice,
|
|
26
|
+
tipGasPrice,
|
|
27
|
+
}) => {
|
|
28
|
+
const baseNonce = await asset.baseAsset.getNonce({ asset, fromAddress, walletAccount })
|
|
29
|
+
|
|
30
|
+
return Promise.all(
|
|
31
|
+
steps.map((step, index) =>
|
|
32
|
+
asset.baseAsset.api.createTx({
|
|
33
|
+
asset,
|
|
34
|
+
walletAccount,
|
|
35
|
+
fromAddress,
|
|
36
|
+
amount: step.amount || asset.currency.ZERO,
|
|
37
|
+
tipGasPrice,
|
|
38
|
+
gasPrice,
|
|
39
|
+
toAddress: step.to,
|
|
40
|
+
txInput: step.txInput,
|
|
41
|
+
gasLimit: step.gasLimit,
|
|
42
|
+
nonce: baseNonce + index,
|
|
43
|
+
})
|
|
44
|
+
)
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function createStakingTxs({
|
|
49
|
+
asset,
|
|
50
|
+
walletAccount,
|
|
51
|
+
fromAddress,
|
|
52
|
+
steps,
|
|
53
|
+
mode,
|
|
54
|
+
gasPrice,
|
|
55
|
+
tipGasPrice,
|
|
56
|
+
feeData,
|
|
57
|
+
}) {
|
|
58
|
+
assert(steps.length > 0, 'staking txs require at least one step')
|
|
59
|
+
assert(
|
|
60
|
+
steps.every(({ amount }) => !amount || amount.unitType.equals(asset.currency)),
|
|
61
|
+
`staking tx amounts must be in ${asset.name}, the asset sending the txs`
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
switch (mode) {
|
|
65
|
+
case STAKING_TX_MODE.single:
|
|
66
|
+
assert(steps.length === 1, 'single staking tx mode requires exactly one step')
|
|
67
|
+
return createSingleTx({
|
|
68
|
+
asset,
|
|
69
|
+
walletAccount,
|
|
70
|
+
step: steps[0],
|
|
71
|
+
gasPrice,
|
|
72
|
+
tipGasPrice,
|
|
73
|
+
feeData,
|
|
74
|
+
})
|
|
75
|
+
case STAKING_TX_MODE.bundle:
|
|
76
|
+
return createBundleTxs({ asset, walletAccount, fromAddress, steps, gasPrice, tipGasPrice })
|
|
77
|
+
default:
|
|
78
|
+
throw new Error(`Unknown staking tx mode: ${mode}`)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function sendStakingTxs({ asset, walletAccount, unsignedTxs, mode, onProgress }) {
|
|
83
|
+
switch (mode) {
|
|
84
|
+
case STAKING_TX_MODE.single: {
|
|
85
|
+
assert(unsignedTxs.length === 1, 'single staking tx mode requires exactly one tx')
|
|
86
|
+
onProgress?.({ index: 0, total: 1 })
|
|
87
|
+
const { txId } = await asset.baseAsset.api.sendTx({
|
|
88
|
+
asset,
|
|
89
|
+
walletAccount,
|
|
90
|
+
unsignedTx: unsignedTxs[0],
|
|
91
|
+
})
|
|
92
|
+
return { txIds: [txId] }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
case STAKING_TX_MODE.bundle: {
|
|
96
|
+
const { bundleHash, txIds } = await asset.baseAsset.sendPrivateBundle({
|
|
97
|
+
unsignedTxBundle: unsignedTxs.map((unsignedTx) => ({ asset, unsignedTx, walletAccount })),
|
|
98
|
+
onBeforeSign: onProgress,
|
|
99
|
+
})
|
|
100
|
+
assert(txIds.length === unsignedTxs.length, 'expected a tx id for every bundled tx')
|
|
101
|
+
return { bundleHash, txIds: [...txIds] }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
default:
|
|
105
|
+
throw new Error(`Unknown staking tx mode: ${mode}`)
|
|
106
|
+
}
|
|
107
|
+
}
|
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
|
-
}
|