@exodus/ethereum-api 9.0.0-alpha.3 → 9.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 +2121 -0
- package/LICENSE +7 -0
- package/README.md +124 -0
- package/package.json +34 -33
- package/src/address-has-history.js +6 -2
- package/src/allowance/constants.js +4 -5
- package/src/allowance/index.js +159 -40
- package/src/check-tx/create-assess-transaction.js +23 -0
- package/src/check-tx/create-check-tx.js +104 -0
- package/src/check-tx/index.js +1 -0
- package/src/create-asset-plugin-factory.js +115 -0
- package/src/create-asset-utils.js +520 -0
- package/src/create-asset.js +272 -97
- package/src/create-token-factory.js +47 -17
- package/src/custom-fees.js +57 -0
- package/src/eip7702-whitelist.js +62 -0
- package/src/ens/index.js +12 -17
- package/src/error-wrapper.js +383 -0
- package/src/eth-like-util.js +172 -33
- package/src/exodus-eth-server/api-coin-nodes.js +27 -196
- package/src/exodus-eth-server/api.js +23 -14
- package/src/exodus-eth-server/clarity-v2.js +255 -0
- package/src/exodus-eth-server/clarity.js +67 -265
- package/src/exodus-eth-server/errors.js +40 -0
- package/src/exodus-eth-server/eth-like-server.js +385 -0
- package/src/exodus-eth-server/fetch-json.js +48 -0
- package/src/exodus-eth-server/index.js +77 -0
- package/src/exodus-eth-server/utils.js +31 -0
- package/src/exodus-eth-server/ws-gateway.js +306 -0
- package/src/exodus-eth-server/ws.js +1 -2
- package/src/fee-data/EthLikeFeeData.js +81 -0
- package/src/fee-data/index.js +54 -0
- package/src/fee-utils.js +229 -0
- package/src/gas-estimation.js +183 -66
- package/src/get-balance-for-address.js +1 -1
- package/src/get-balances.js +285 -27
- package/src/get-fee.js +92 -56
- package/src/get-historical-balance.js +58 -0
- package/src/hooks/index.js +1 -1
- package/src/hooks/monitor.js +182 -43
- package/src/hooks/staking-freshness.js +58 -0
- package/src/index.js +118 -12
- package/src/move-funds.js +153 -0
- package/src/multicall3/index.js +212 -0
- package/src/multisig-address.js +146 -0
- package/src/nft-utils.js +60 -0
- package/src/number-utils.js +34 -2
- package/src/optimism-gas/index.js +13 -5
- package/src/send-validations.js +58 -0
- package/src/server-based-fee-monitor.js +12 -19
- package/src/simulate-tx/fetch-tx-preview.js +0 -2
- package/src/simulate-tx/index.js +2 -2
- package/src/simulate-tx/simulate-eth-tx.js +24 -11
- package/src/simulation/common.js +34 -0
- package/src/simulation/create-simulate-message.js +49 -0
- package/src/simulation/create-simulate-transactions.js +108 -0
- package/src/simulation/estimate-fee.js +14 -0
- package/src/simulation/estimate-simple-transfer.js +15 -0
- package/src/simulation/get-message-type.js +18 -0
- package/src/simulation/simulate-message-api.js +68 -0
- package/src/simulation/simulate-transactions-api.js +266 -0
- package/src/simulation/simulate-transactions.js +16 -0
- package/src/simulation/transactions.js +52 -0
- package/src/simulation/try-estimating-changes-locally.js +26 -0
- package/src/staking/api/index.js +43 -0
- package/src/staking/cached-read.js +33 -0
- package/src/staking/ethereum/api.js +166 -34
- package/src/staking/ethereum/deps.js +37 -0
- package/src/staking/ethereum/everstake.js +113 -0
- package/src/staking/ethereum/index.js +4 -3
- package/src/staking/ethereum/service.js +682 -181
- package/src/staking/ethereum/staking-utils.js +275 -4
- package/src/staking/fantom-staking.js +9 -9
- package/src/staking/index.js +4 -4
- package/src/staking/matic/addresses.js +2 -0
- package/src/staking/matic/api.js +69 -49
- package/src/staking/matic/index.js +3 -3
- package/src/staking/matic/matic-staking-utils.js +243 -5
- package/src/staking/matic/matic-staking.md +53 -0
- package/src/staking/staking-provider-client.js +1 -1
- package/src/staking/utils/index.js +39 -0
- package/src/tx-create.js +666 -0
- package/src/tx-log/clarity-monitor.js +273 -103
- package/src/tx-log/clarity-truncated-history-monitor.js +109 -0
- package/src/tx-log/clarity-truncated-history-monitor.md +219 -0
- package/src/tx-log/clarity-utils/absolute.js +96 -0
- package/src/tx-log/clarity-utils/get-log-items-from-server-tx.js +94 -31
- package/src/tx-log/clarity-utils/get-names-of-tokens-transferred-by-server-tx.js +3 -2
- package/src/tx-log/clarity-utils/index.js +9 -3
- package/src/tx-log/clarity-utils/is-spam-pending-tx.js +13 -0
- package/src/tx-log/clarity-utils/normalize-transactions-response.js +65 -0
- package/src/tx-log/ethereum-monitor.js +93 -47
- package/src/tx-log/ethereum-no-history-monitor.js +52 -41
- package/src/tx-log/get-optimistic-txlog-effects.js +379 -0
- package/src/tx-log/index.js +5 -3
- package/src/tx-log/monitor-utils/check-pending-transactions.js +14 -5
- package/src/tx-log/monitor-utils/exclude-unchanged-token-balances.js +1 -1
- package/src/tx-log/monitor-utils/extract-balance-from-clarity-account-info.js +108 -0
- package/src/tx-log/monitor-utils/get-balance-updates.js +44 -0
- package/src/tx-log/monitor-utils/get-batched-rpc-balances.js +47 -0
- package/src/tx-log/monitor-utils/get-current-blacklist-status.js +38 -0
- package/src/tx-log/monitor-utils/get-current-eip7702-delegation.js +79 -0
- package/src/tx-log/monitor-utils/get-derive-transactions-to-check.js +10 -5
- package/src/tx-log/monitor-utils/get-fee-amount.js +1 -1
- package/src/tx-log/monitor-utils/get-history-from-server.js +1 -1
- package/src/tx-log/monitor-utils/get-log-items-from-server-tx.js +75 -33
- package/src/tx-log/monitor-utils/get-names-of-tokens-transferred-by-server-tx.js +3 -2
- package/src/tx-log/monitor-utils/get-raw-transfer-amount.js +29 -0
- package/src/tx-log/monitor-utils/get-transfers-by-token-name.js +3 -2
- package/src/tx-log/monitor-utils/get-value-of-transfers.js +10 -12
- package/src/tx-log/monitor-utils/index.js +12 -7
- package/src/tx-log/monitor-utils/is-unverifiable-pending-tx-expired.js +19 -0
- package/src/tx-log/monitor-utils/verify-pending-tx-status-rpc.js +90 -0
- package/src/tx-log-staking-processor/asset-staking-tx-data.js +82 -24
- package/src/tx-log-staking-processor/index.js +28 -14
- package/src/tx-log-staking-processor/utils.js +21 -20
- package/src/tx-send/broadcast-error-handler.js +63 -0
- package/src/tx-send/index.js +1 -2
- package/src/tx-send/nonce-utils.js +97 -0
- package/src/tx-send/tx-send.js +94 -288
- package/src/tx-sign/index.js +17 -0
- package/src/tx-type/index.js +280 -0
- package/src/watch-tx.js +36 -0
- package/src/web3/createSimulateMessage.js +3 -2
- package/src/web3/createSimulateTransactions.js +11 -0
- package/src/web3/index.js +3 -1
- package/src/etherscan/account.js +0 -49
- package/src/etherscan/index.js +0 -28
- package/src/etherscan/logs.js +0 -16
- package/src/etherscan/proxy.js +0 -47
- package/src/etherscan/request.js +0 -26
- package/src/etherscan/ws.js +0 -88
- package/src/exodus-eth-server/server-factory.js +0 -28
- package/src/exodus-eth-server/server-map.js +0 -21
- package/src/get-fee-async.js +0 -97
- package/src/staking/matic/service.js +0 -437
- package/src/staking-api.js +0 -5
- package/src/tx-log/clarity-utils/get-derive-data-needed-for-tick.js +0 -12
- package/src/tx-log-staking-processor/get-asset-tx-amount.js +0 -26
- package/src/tx-send/get-fee-info.js +0 -41
- package/src/websocket/index.android.js +0 -2
- package/src/websocket/index.ios.js +0 -2
- package/src/websocket/index.js +0 -21
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import assert from 'minimalistic-assert'
|
|
2
|
+
|
|
3
|
+
import ClarityServer, { RPC_REQUEST_TIMEOUT } from './clarity.js'
|
|
4
|
+
import { fetchJsonRetry } from './fetch-json.js'
|
|
5
|
+
|
|
6
|
+
const ASSETS_GATEWAY_URL = 'https://assets-gateway-clarity-api.a.exodus.io/assets'
|
|
7
|
+
|
|
8
|
+
export const encodeCursor = (blockNumberBigInt, isLegacy = false) => {
|
|
9
|
+
if (typeof blockNumberBigInt !== 'bigint') throw new Error('expected bigint')
|
|
10
|
+
|
|
11
|
+
if (isLegacy) {
|
|
12
|
+
const cursor = Buffer.alloc(26)
|
|
13
|
+
cursor.writeBigInt64LE(blockNumberBigInt, 10)
|
|
14
|
+
return cursor
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const cursor = Buffer.alloc(8)
|
|
18
|
+
cursor.writeBigUInt64LE(blockNumberBigInt, 0)
|
|
19
|
+
return cursor
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const decodeCursor = (cursor) => {
|
|
23
|
+
if (Buffer.isBuffer(cursor)) {
|
|
24
|
+
// New format: buffer containing only the block number
|
|
25
|
+
if (cursor.length === 8) return { blockNumber: cursor.readBigUInt64LE(0) }
|
|
26
|
+
|
|
27
|
+
// Old format: buffer with length 26
|
|
28
|
+
if (cursor.length === 26) return { blockNumber: cursor.readBigInt64LE(10) }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Doesn't match any known format, return default
|
|
32
|
+
return { blockNumber: BigInt(0) }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const fetchHttpRequest = ({ baseApiPath, path, method, body, search }) => {
|
|
36
|
+
assert(typeof baseApiPath === 'string', 'expected string baseApiPath')
|
|
37
|
+
|
|
38
|
+
const url = new URL(`${baseApiPath}${path}`)
|
|
39
|
+
if (search) url.search = search
|
|
40
|
+
|
|
41
|
+
const fetchOptions = {
|
|
42
|
+
method,
|
|
43
|
+
headers: { 'Content-Type': 'application/json' },
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (body) fetchOptions.body = JSON.stringify(body)
|
|
47
|
+
return fetchJsonRetry(url, fetchOptions)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export default class ClarityServerV2 extends ClarityServer {
|
|
51
|
+
constructor({ baseAssetName, uri }) {
|
|
52
|
+
super({ baseAssetName, uri })
|
|
53
|
+
this.updateBaseApiPath() // default to assets-gateway
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
updateBaseApiPath(uri) {
|
|
57
|
+
const base = uri || ASSETS_GATEWAY_URL
|
|
58
|
+
this.baseApiPath = new URL(`${base}/api/v2/${this.baseAssetName}`).toString()
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
setURI(uri) {
|
|
62
|
+
super.setURI(uri)
|
|
63
|
+
this.updateBaseApiPath(uri) // pass in the uri from remote config to override assets-gateway
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
getTransactionsFromBlockNumber = async ({ address, blockNumber, withInput, truncated }) => {
|
|
67
|
+
const url = new URL(`${this.baseApiPath}/addresses/${encodeURIComponent(address)}/transactions`)
|
|
68
|
+
url.searchParams.set('cursor', blockNumber || '0')
|
|
69
|
+
|
|
70
|
+
if (withInput) {
|
|
71
|
+
url.searchParams.set('withInput', 'true')
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (truncated) {
|
|
75
|
+
url.searchParams.set('truncated', 'true')
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return fetchJsonRetry(url)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Clarity exposes a new way to determine fee data for EIP-1559
|
|
82
|
+
// networks. In addition to the `nextBlockBaseFeePerGas`, we also
|
|
83
|
+
// receive a `rewardPercentiles` mapping, which defines the prevailing
|
|
84
|
+
// 25th, 50th and 75th percentiles of `maxPriorityFeePerGas` for
|
|
85
|
+
// an EVM-based network.
|
|
86
|
+
async getGasPriceEstimation() {
|
|
87
|
+
return fetchJsonRetry(`${this.baseApiPath}/gas-price-estimation`)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async getFee() {
|
|
91
|
+
// HACK: When we call `getFee`, we should still
|
|
92
|
+
// subscribe for period fee updates.
|
|
93
|
+
await this.connectFee()
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
// NOTE: Depending on whether the network is `eip1559Enabled`
|
|
97
|
+
// **on the backend**, this can omit certain
|
|
98
|
+
// properties.
|
|
99
|
+
//
|
|
100
|
+
// By default, we should receive:
|
|
101
|
+
// + blockNumber
|
|
102
|
+
// + gasPrice
|
|
103
|
+
// + baseGasPrice (deprecated)
|
|
104
|
+
//
|
|
105
|
+
// In addition, when `eip1559Enabled` on Clarity:
|
|
106
|
+
// + baseFeePerGas
|
|
107
|
+
// + nextBaseFeePerGas
|
|
108
|
+
// + rewardPercentiles
|
|
109
|
+
//
|
|
110
|
+
// See: https://github.com/ExodusMovement/clarity/blob/d3c2a7f501a4391da630592bca3bf57c3ddd5e89/src/modules/ethereum-like/gas-price/index.js#L192C5-L219C6
|
|
111
|
+
return await this.getGasPriceEstimation()
|
|
112
|
+
} catch {
|
|
113
|
+
// HACK: The `getGasPriceEstimation` endpoint is not guaranteed
|
|
114
|
+
// to exist for all assets. In this case, we'll fallback
|
|
115
|
+
// to legacy behaviour, which is to query via the WebSocket.
|
|
116
|
+
return this.getFeeFromWebSocket()
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Paginates GET .../addresses/:addr/transactions. Pass `truncated: true` to opt
|
|
122
|
+
* into the backend's truncation mode + `accountInfo` pre-cursor snapshots (used by
|
|
123
|
+
* `ClarityTruncatedHistoryMonitor`). For `clarity`/`clarity-v2`/`clarity-v3` flows,
|
|
124
|
+
* leave the default so Clarity returns full history.
|
|
125
|
+
*/
|
|
126
|
+
async getAllTransactions({ address, cursor, withInput = true, truncated = false }) {
|
|
127
|
+
let { blockNumber } = decodeCursor(cursor)
|
|
128
|
+
|
|
129
|
+
let transactions = []
|
|
130
|
+
|
|
131
|
+
// accountInfo rows carry balance/nonce snapshots for blocks before the
|
|
132
|
+
// initial cursor — used to derive correct state with truncated history.
|
|
133
|
+
// Only collected (and returned) when the caller opted into `truncated`.
|
|
134
|
+
const accountInfo = truncated ? [] : null
|
|
135
|
+
|
|
136
|
+
while (true) {
|
|
137
|
+
const result = await this.getTransactionsFromBlockNumber({
|
|
138
|
+
address,
|
|
139
|
+
blockNumber: blockNumber.toString(),
|
|
140
|
+
withInput,
|
|
141
|
+
truncated,
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
const txs = result.transactions || []
|
|
145
|
+
|
|
146
|
+
if (accountInfo && Array.isArray(result.accountInfo)) {
|
|
147
|
+
accountInfo.push(...result.accountInfo)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const nextBlockNumber = BigInt(result.cursor)
|
|
151
|
+
|
|
152
|
+
if (txs.length === 0) {
|
|
153
|
+
// fetch until no more new transactions
|
|
154
|
+
blockNumber = nextBlockNumber
|
|
155
|
+
break
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (nextBlockNumber <= blockNumber) {
|
|
159
|
+
console.warn(
|
|
160
|
+
`Invalid cursor: next block number is not greater than current block number at cursor ${blockNumber}`
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
// self healing, reset cursor to the smaller block number and remove the transactions greater than the block number
|
|
164
|
+
transactions.push(...txs)
|
|
165
|
+
transactions = transactions.filter((tx) => tx.blockNumber <= nextBlockNumber)
|
|
166
|
+
blockNumber = nextBlockNumber
|
|
167
|
+
break
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
transactions.push(...txs)
|
|
171
|
+
blockNumber = nextBlockNumber
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const newCursor = Buffer.alloc(8)
|
|
175
|
+
newCursor.writeBigUInt64LE(BigInt(blockNumber), 0)
|
|
176
|
+
|
|
177
|
+
// Separate confirmed (has blockNumber) from pending (blockNumber is null)
|
|
178
|
+
const confirmed = transactions.filter((tx) => tx.blockNumber != null)
|
|
179
|
+
const pending = transactions.filter((tx) => tx.blockNumber == null)
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
transactions: { confirmed, pending },
|
|
183
|
+
cursor: newCursor,
|
|
184
|
+
...(accountInfo && { accountInfo }),
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
fetchRpcHttpRequest = ({ baseApiPath, path = '/rpc', body }) => {
|
|
189
|
+
return fetchHttpRequest({ baseApiPath, path, method: 'POST', body })
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async sendRpcRequest(rpcRequest) {
|
|
193
|
+
try {
|
|
194
|
+
return await super.sendRpcRequest(rpcRequest)
|
|
195
|
+
} catch (err) {
|
|
196
|
+
// If we encounter an error which isn't associated
|
|
197
|
+
// with transport timeouts, then bubble up.
|
|
198
|
+
if (err.message !== RPC_REQUEST_TIMEOUT) throw err
|
|
199
|
+
|
|
200
|
+
const { baseApiPath } = this
|
|
201
|
+
return this.fetchRpcHttpRequest({ baseApiPath, body: rpcRequest })
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Maybe some functions from clarity should be overriden to have handleJsonRPCResponse as well
|
|
206
|
+
|
|
207
|
+
async sendRawTransaction(...params) {
|
|
208
|
+
const { baseApiPath } = this
|
|
209
|
+
const request = this.sendRawTransactionRequest(...params)
|
|
210
|
+
return this.handleJsonRPCResponse(
|
|
211
|
+
await this.fetchRpcHttpRequest({ baseApiPath, body: request })
|
|
212
|
+
)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async getTransactionCount(...params) {
|
|
216
|
+
// nonce is called during tx send, use it in rest api
|
|
217
|
+
const { baseApiPath } = this
|
|
218
|
+
const request = this.getTransactionCountRequest(...params)
|
|
219
|
+
return this.handleJsonRPCResponse(
|
|
220
|
+
await this.fetchRpcHttpRequest({ baseApiPath, body: request })
|
|
221
|
+
)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async estimateGas(...params) {
|
|
225
|
+
const { baseApiPath } = this
|
|
226
|
+
const request = this.estimateGasRequest(...params)
|
|
227
|
+
return this.handleJsonRPCResponse(
|
|
228
|
+
await this.fetchRpcHttpRequest({ baseApiPath, body: request })
|
|
229
|
+
)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
getServoTransactionsByBundleHash({ bundleHash }) {
|
|
233
|
+
assert(bundleHash, 'expected bundleHash')
|
|
234
|
+
|
|
235
|
+
const { baseApiPath } = this
|
|
236
|
+
|
|
237
|
+
return fetchHttpRequest({
|
|
238
|
+
baseApiPath,
|
|
239
|
+
path: '/proxy/servo/transactions',
|
|
240
|
+
method: 'GET',
|
|
241
|
+
search: new URLSearchParams({ bundleHash }).toString(),
|
|
242
|
+
})
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async sendBundle(...params) {
|
|
246
|
+
const { baseApiPath } = this
|
|
247
|
+
return this.handleJsonRPCResponse(
|
|
248
|
+
await this.fetchRpcHttpRequest({
|
|
249
|
+
baseApiPath,
|
|
250
|
+
path: '/proxy/servo',
|
|
251
|
+
body: this.sendBundleRequest(...params),
|
|
252
|
+
})
|
|
253
|
+
)
|
|
254
|
+
}
|
|
255
|
+
}
|
|
@@ -1,21 +1,30 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
import EventEmitter from 'events'
|
|
1
|
+
import { safeString } from '@exodus/safe-string'
|
|
2
|
+
import { TraceId } from '@exodus/traceparent'
|
|
4
3
|
import io from 'socket.io-client'
|
|
5
|
-
import { fromHexToString } from '../number-utils'
|
|
6
4
|
|
|
7
|
-
|
|
5
|
+
import { errorMessageToSafeHint } from './errors.js'
|
|
6
|
+
import { EthLikeServer } from './eth-like-server.js'
|
|
7
|
+
|
|
8
|
+
export const RPC_REQUEST_TIMEOUT = 'RPC_REQUEST_TIMEOUT'
|
|
9
|
+
|
|
10
|
+
export default class ClarityServer extends EthLikeServer {
|
|
8
11
|
constructor({ baseAssetName, uri }) {
|
|
9
12
|
super()
|
|
10
13
|
this.baseAssetName = baseAssetName
|
|
11
14
|
this.uri = uri
|
|
15
|
+
this.wsUri = uri
|
|
12
16
|
this.defaultUri = uri
|
|
13
17
|
this.baseNamespace = `/v1/${this.baseAssetName}`
|
|
14
18
|
this.sockets = Object.create(null)
|
|
15
|
-
this.id = 0
|
|
16
19
|
}
|
|
17
20
|
|
|
18
21
|
setURI(uri) {
|
|
22
|
+
if (!uri.includes('assets-gateway')) {
|
|
23
|
+
// assets-gateway endpoint doesn't support legacy ws
|
|
24
|
+
// guard this against remote config override
|
|
25
|
+
this.wsUri = uri
|
|
26
|
+
}
|
|
27
|
+
|
|
19
28
|
this.dispose()
|
|
20
29
|
this.uri = uri
|
|
21
30
|
}
|
|
@@ -52,9 +61,10 @@ export default class ClarityServer extends EventEmitter {
|
|
|
52
61
|
}
|
|
53
62
|
|
|
54
63
|
createSocket(namespace) {
|
|
55
|
-
return io(`${this.
|
|
64
|
+
return io(`${this.wsUri}${namespace}`, {
|
|
56
65
|
transports: ['websocket', 'polling'],
|
|
57
66
|
extraHeaders: { 'User-Agent': 'exodus' },
|
|
67
|
+
reconnection: true,
|
|
58
68
|
})
|
|
59
69
|
}
|
|
60
70
|
|
|
@@ -64,11 +74,11 @@ export default class ClarityServer extends EventEmitter {
|
|
|
64
74
|
}
|
|
65
75
|
|
|
66
76
|
disconnectRpc() {
|
|
67
|
-
this.disconnectSocket(this.
|
|
77
|
+
this.disconnectSocket(this.formatRpcNamespace())
|
|
68
78
|
}
|
|
69
79
|
|
|
70
80
|
disconnectFee() {
|
|
71
|
-
this.disconnectSocket(this.
|
|
81
|
+
this.disconnectSocket(this.formatFeeNamespace())
|
|
72
82
|
}
|
|
73
83
|
|
|
74
84
|
disconnectSocket(namespace) {
|
|
@@ -83,8 +93,34 @@ export default class ClarityServer extends EventEmitter {
|
|
|
83
93
|
for (const namespace of namespaces) {
|
|
84
94
|
this.disconnectSocket(namespace)
|
|
85
95
|
}
|
|
96
|
+
|
|
97
|
+
this.removeAllListeners('transaction')
|
|
98
|
+
this.removeAllListeners('feeUpdated')
|
|
86
99
|
}
|
|
87
100
|
|
|
101
|
+
// See https://www.jsonrpc.org/specification#response_object for details.
|
|
102
|
+
handleJsonRPCResponse(response) {
|
|
103
|
+
const result = response?.result
|
|
104
|
+
const error = response?.error
|
|
105
|
+
if (error || result === undefined) {
|
|
106
|
+
const message = error?.message || error?.code || safeString`no result`
|
|
107
|
+
|
|
108
|
+
const revisedError = new Error(`Bad rpc response: ${message}`)
|
|
109
|
+
revisedError.hint = safeString`Bad rpc response: ${errorMessageToSafeHint(message)}`
|
|
110
|
+
|
|
111
|
+
// Preserve trace ID: from HTTP (__traceId) or WebSocket (traceparent in JSON body)
|
|
112
|
+
const traceId = response?.__traceId || response?.traceparent
|
|
113
|
+
if (traceId) {
|
|
114
|
+
revisedError.traceId = traceId
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
throw revisedError
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return result
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Transport: WS only in ClarityServer, HTTP only in ClarityServerV2 (overridden)
|
|
88
124
|
async getAllTransactions(params) {
|
|
89
125
|
const transactions = { pending: [], confirmed: [] }
|
|
90
126
|
const cursor = await this.getTransactions({
|
|
@@ -104,6 +140,7 @@ export default class ClarityServer extends EventEmitter {
|
|
|
104
140
|
return { cursor, transactions }
|
|
105
141
|
}
|
|
106
142
|
|
|
143
|
+
// Transport: WS only (uses transactions socket, not RPC socket)
|
|
107
144
|
async getTransactions({ walletAccount, address, cursor, onChunk }) {
|
|
108
145
|
const socket = this.connectTransactions({ walletAccount, address })
|
|
109
146
|
const listener = (isPending, chunk, callback) => {
|
|
@@ -124,10 +161,11 @@ export default class ClarityServer extends EventEmitter {
|
|
|
124
161
|
.finally(() => socket.off('transactionsChunk', listener))
|
|
125
162
|
}
|
|
126
163
|
|
|
127
|
-
|
|
164
|
+
// Transport: WS only (uses fee socket)
|
|
165
|
+
getFeeFromWebSocket() {
|
|
128
166
|
const socket = this.connectFee()
|
|
129
167
|
return new Promise((resolve, reject) => {
|
|
130
|
-
const timeout = setTimeout(() => reject(new Error('Fee Timeout')),
|
|
168
|
+
const timeout = setTimeout(() => reject(new Error('Fee Timeout')), 3000)
|
|
131
169
|
socket.emit('getFee', (fee) => {
|
|
132
170
|
clearTimeout(timeout)
|
|
133
171
|
if (!fee) {
|
|
@@ -140,6 +178,12 @@ export default class ClarityServer extends EventEmitter {
|
|
|
140
178
|
})
|
|
141
179
|
}
|
|
142
180
|
|
|
181
|
+
// Transport: WS only in ClarityServer, HTTP first → WS fallback in ClarityServerV2 (overridden)
|
|
182
|
+
async getFee() {
|
|
183
|
+
return this.getFeeFromWebSocket()
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Transport: Depends on getFee() - WS only in ClarityServer, HTTP first → WS fallback in ClarityServerV2
|
|
143
187
|
// for fee monitors
|
|
144
188
|
async getGasPrice() {
|
|
145
189
|
const fee = await this.getFee()
|
|
@@ -149,273 +193,31 @@ export default class ClarityServer extends EventEmitter {
|
|
|
149
193
|
async sendRpcRequest(rpcRequest) {
|
|
150
194
|
const rpcSocket = this.connectRpc()
|
|
151
195
|
return new Promise((resolve, reject) => {
|
|
152
|
-
const timeout = setTimeout(() => reject(new Error(
|
|
196
|
+
const timeout = setTimeout(() => reject(new Error(RPC_REQUEST_TIMEOUT)), 3000)
|
|
153
197
|
rpcSocket.emit('request', rpcRequest, (response) => {
|
|
154
198
|
clearTimeout(timeout)
|
|
199
|
+
const rawTraceparent = response?.traceparent || response?.[0]?.traceparent
|
|
200
|
+
if (rawTraceparent) {
|
|
201
|
+
// eslint-disable-next-line @exodus/hydra/no-param-reassign-prop-only
|
|
202
|
+
response.traceparent = TraceId.fromRawTraceparent(rawTraceparent)
|
|
203
|
+
}
|
|
204
|
+
|
|
155
205
|
resolve(response)
|
|
156
206
|
})
|
|
157
207
|
})
|
|
158
208
|
}
|
|
159
209
|
|
|
160
|
-
|
|
210
|
+
// Transport: WS only in ClarityServer, WS first → HTTP fallback in ClarityServerV2
|
|
211
|
+
async sendBatchRequest(batch, { tolerant } = {}) {
|
|
161
212
|
const responses = await this.sendRpcRequest(batch)
|
|
162
|
-
|
|
163
|
-
const isValid = responses.every((response) => {
|
|
164
|
-
return !isNaN(response?.id) && response?.result
|
|
165
|
-
})
|
|
166
|
-
if (responses.length !== batch.length || !isValid) {
|
|
167
|
-
throw new Error('Bad Response')
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
const keyed = responses.reduce((acc, response) => {
|
|
171
|
-
return { ...acc, [`${response.id}`]: response.result }
|
|
172
|
-
}, Object.create(null))
|
|
173
|
-
return batch.map((request) => keyed[`${request.id}`])
|
|
213
|
+
return this.parseBatchResponse({ batch, responses, errorMessage: 'Bad Response', tolerant })
|
|
174
214
|
}
|
|
175
215
|
|
|
216
|
+
// Transport: Uses sendRpcRequest - WS only in ClarityServer, WS first → HTTP fallback in ClarityServerV2
|
|
176
217
|
async sendRequest(request) {
|
|
177
218
|
const response = await this.sendRpcRequest(request)
|
|
178
|
-
const result = response?.result
|
|
179
|
-
const error = response?.error
|
|
180
|
-
if (error || result === undefined) {
|
|
181
|
-
const message = error?.message || error?.code || 'no result'
|
|
182
|
-
throw new Error(`Bad rpc response: ${message}`)
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
return result
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
async isContract(address) {
|
|
189
|
-
const code = await this.getCode(address)
|
|
190
|
-
return code.length > 2
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
buildRequest({ method, params = [] }) {
|
|
194
|
-
return { jsonrpc: '2.0', id: this.id++, method, params }
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
balanceOfRequest(address, tokenAddress, tag = 'latest') {
|
|
198
|
-
const contract = SolidityContract.simpleErc20(tokenAddress)
|
|
199
|
-
const callData = contract.balanceOf.build(address)
|
|
200
|
-
const data = {
|
|
201
|
-
data: bufferToHex(callData),
|
|
202
|
-
to: tokenAddress,
|
|
203
|
-
}
|
|
204
|
-
return this.ethCallRequest(data, tag)
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
getBalanceRequest(address, tag = 'latest') {
|
|
208
|
-
return this.buildRequest({ method: 'eth_getBalance', params: [address, tag] })
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
gasPriceRequest() {
|
|
212
|
-
return this.buildRequest({ method: 'eth_gasPrice' })
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
estimateGasRequest(data, tag = 'latest') {
|
|
216
|
-
return this.buildRequest({ method: 'eth_estimateGas', params: [data, tag] })
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
sendRawTransactionRequest(data) {
|
|
220
|
-
const hex = data.startsWith('0x') ? data : '0x' + data
|
|
221
|
-
return this.buildRequest({ method: 'eth_sendRawTransaction', params: [hex] })
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
coinbaseRequest() {
|
|
225
|
-
return this.buildRequest({ method: 'eth_coinbase' })
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
getCodeRequest(address, tag = 'latest') {
|
|
229
|
-
return this.buildRequest({ method: 'eth_getCode', params: [address, tag] })
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
getStorageAtRequest(address, position, tag = 'latest') {
|
|
233
|
-
return this.buildRequest({ method: 'eth_getStorageAt', params: [address, position, tag] })
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
getTransactionCountRequest(address, tag = 'latest') {
|
|
237
|
-
return this.buildRequest({ method: 'eth_getTransactionCount', params: [address, tag] })
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
getTransactionByHashRequest(hash) {
|
|
241
|
-
return this.buildRequest({ method: 'eth_getTransactionByHash', params: [hash] })
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
getTransactionReceiptRequest(txhash) {
|
|
245
|
-
return this.buildRequest({ method: 'eth_getTransactionReceipt', params: [txhash] })
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
ethCallRequest(data, tag = 'latest') {
|
|
249
|
-
return this.buildRequest({ method: 'eth_call', params: [data, tag] })
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
blockNumberRequest() {
|
|
253
|
-
return this.buildRequest({ method: 'eth_blockNumber' })
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
getLogsRequest(object) {
|
|
257
|
-
return this.buildRequest({ method: 'eth_getLogs', params: [object] })
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
getBlockByNumberRequest(numberHex, isFullTxs = false) {
|
|
261
|
-
return this.buildRequest({ method: 'eth_getBlockByNumber', params: [numberHex, isFullTxs] })
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
getBlockByHashRequest(blockHash, isFullTxs = false) {
|
|
265
|
-
return this.buildRequest({ method: 'eth_getBlockByHash', params: [blockHash, isFullTxs] })
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
getBlockTransactionCountByHashRequest(blockHash) {
|
|
269
|
-
return this.buildRequest({ method: 'eth_getBlockTransactionCountByHash', params: [blockHash] })
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
getBlockTransactionCountByNumberRequest(quantityOrTag) {
|
|
273
|
-
return this.buildRequest({
|
|
274
|
-
method: 'eth_getBlockTransactionCountByNumber',
|
|
275
|
-
params: [quantityOrTag],
|
|
276
|
-
})
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
getCompilersRequest() {
|
|
280
|
-
return this.buildRequest({ method: 'eth_getCompilers' })
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
getNetVersionRequest() {
|
|
284
|
-
return this.buildRequest({ method: 'net_version' })
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
simulateRawTransactionRequest(rawTx, applyPending = true) {
|
|
288
|
-
const replaced = rawTx.replace('0x', '')
|
|
289
|
-
return this.buildRequest({
|
|
290
|
-
method: 'debug_simulateRawTransaction',
|
|
291
|
-
params: [replaced, applyPending],
|
|
292
|
-
})
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
async proxyToCoinNode(params) {
|
|
296
|
-
const request = this.buildRequest(params)
|
|
297
|
-
return this.sendRequest(request)
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
async balanceOf(address, tokenAddress, tag = 'latest') {
|
|
301
|
-
const request = this.balanceOfRequest(address, tokenAddress, tag)
|
|
302
|
-
const result = await this.sendRequest(request)
|
|
303
|
-
const balance = fromHexToString(result)
|
|
304
|
-
return {
|
|
305
|
-
confirmed: {
|
|
306
|
-
[tokenAddress]: balance,
|
|
307
|
-
},
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
async getBalance(...params) {
|
|
312
|
-
const request = this.getBalanceRequest(...params)
|
|
313
|
-
return this.sendRequest(request)
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
async getBalanceProxied(...params) {
|
|
317
|
-
return this.getBalance(...params)
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
async gasPrice(...params) {
|
|
321
|
-
const request = this.gasPriceRequest(...params)
|
|
322
|
-
return this.sendRequest(request)
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
async estimateGas(...params) {
|
|
326
|
-
const request = this.estimateGasRequest(...params)
|
|
327
|
-
return this.sendRequest(request)
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
async sendRawTransaction(...params) {
|
|
331
|
-
const request = this.sendRawTransactionRequest(...params)
|
|
332
|
-
return this.sendRequest(request)
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
async getCode(...params) {
|
|
336
|
-
const request = this.getCodeRequest(...params)
|
|
337
|
-
return this.sendRequest(request)
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
async getStorageAt(...params) {
|
|
341
|
-
const request = this.getStorageAtRequest(...params)
|
|
342
|
-
return this.sendRequest(request)
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
async getTransactionCount(...params) {
|
|
346
|
-
const request = this.getTransactionCountRequest(...params)
|
|
347
|
-
return this.sendRequest(request)
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
async getTransactionByHash(...params) {
|
|
351
|
-
const request = this.getTransactionByHashRequest(...params)
|
|
352
|
-
return this.sendRequest(request)
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
async getTransactionReceipt(...params) {
|
|
356
|
-
const request = this.getTransactionReceiptRequest(...params)
|
|
357
|
-
return this.sendRequest(request)
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
async ethCall(...params) {
|
|
361
|
-
const request = this.ethCallRequest(...params)
|
|
362
|
-
return this.sendRequest(request)
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
async blockNumber(...params) {
|
|
366
|
-
const request = this.blockNumberRequest(...params)
|
|
367
|
-
return this.sendRequest(request)
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
async getLatestBlock() {
|
|
371
|
-
return this.getBlockByNumber('latest')
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
async getBaseFeePerGas() {
|
|
375
|
-
const response = await this.getLatestBlock()
|
|
376
|
-
if (response.baseFeePerGas) {
|
|
377
|
-
return fromHexToString(response.baseFeePerGas)
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
async getBlockByHash(...params) {
|
|
382
|
-
const request = this.getBlockByHashRequest(...params)
|
|
383
|
-
return this.sendRequest(request)
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
async getBlockTransactionCountByNumber(...params) {
|
|
387
|
-
const request = this.getBlockTransactionCountByNumberRequest(...params)
|
|
388
|
-
return this.sendRequest(request)
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
async getBlockByNumber(...params) {
|
|
392
|
-
const request = this.getBlockByNumberRequest(...params)
|
|
393
|
-
return this.sendRequest(request)
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
async simulateRawTransaction(...params) {
|
|
397
|
-
const request = this.simulateRawTransactionRequest(...params)
|
|
398
|
-
return this.sendRequest(request)
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
async getCoinbase() {
|
|
402
|
-
const request = this.coinbaseRequest()
|
|
403
|
-
return this.sendRequest(request)
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
async getCompilers() {
|
|
407
|
-
const request = this.getCompilersRequest()
|
|
408
|
-
return this.sendRequest(request)
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
async getNetVersion() {
|
|
412
|
-
const request = this.getNetVersion()
|
|
413
|
-
return this.sendRequest(request)
|
|
414
|
-
}
|
|
415
219
|
|
|
416
|
-
|
|
417
|
-
const request = this.getLogsRequest(...params)
|
|
418
|
-
return this.sendRequest(request)
|
|
220
|
+
return this.handleJsonRPCResponse(response)
|
|
419
221
|
}
|
|
420
222
|
|
|
421
223
|
formatTransactionsNamespace(address) {
|