@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.
Files changed (143) hide show
  1. package/CHANGELOG.md +2121 -0
  2. package/LICENSE +7 -0
  3. package/README.md +124 -0
  4. package/package.json +34 -33
  5. package/src/address-has-history.js +6 -2
  6. package/src/allowance/constants.js +4 -5
  7. package/src/allowance/index.js +159 -40
  8. package/src/check-tx/create-assess-transaction.js +23 -0
  9. package/src/check-tx/create-check-tx.js +104 -0
  10. package/src/check-tx/index.js +1 -0
  11. package/src/create-asset-plugin-factory.js +115 -0
  12. package/src/create-asset-utils.js +520 -0
  13. package/src/create-asset.js +272 -97
  14. package/src/create-token-factory.js +47 -17
  15. package/src/custom-fees.js +57 -0
  16. package/src/eip7702-whitelist.js +62 -0
  17. package/src/ens/index.js +12 -17
  18. package/src/error-wrapper.js +383 -0
  19. package/src/eth-like-util.js +172 -33
  20. package/src/exodus-eth-server/api-coin-nodes.js +27 -196
  21. package/src/exodus-eth-server/api.js +23 -14
  22. package/src/exodus-eth-server/clarity-v2.js +255 -0
  23. package/src/exodus-eth-server/clarity.js +67 -265
  24. package/src/exodus-eth-server/errors.js +40 -0
  25. package/src/exodus-eth-server/eth-like-server.js +385 -0
  26. package/src/exodus-eth-server/fetch-json.js +48 -0
  27. package/src/exodus-eth-server/index.js +77 -0
  28. package/src/exodus-eth-server/utils.js +31 -0
  29. package/src/exodus-eth-server/ws-gateway.js +306 -0
  30. package/src/exodus-eth-server/ws.js +1 -2
  31. package/src/fee-data/EthLikeFeeData.js +81 -0
  32. package/src/fee-data/index.js +54 -0
  33. package/src/fee-utils.js +229 -0
  34. package/src/gas-estimation.js +183 -66
  35. package/src/get-balance-for-address.js +1 -1
  36. package/src/get-balances.js +285 -27
  37. package/src/get-fee.js +92 -56
  38. package/src/get-historical-balance.js +58 -0
  39. package/src/hooks/index.js +1 -1
  40. package/src/hooks/monitor.js +182 -43
  41. package/src/hooks/staking-freshness.js +58 -0
  42. package/src/index.js +118 -12
  43. package/src/move-funds.js +153 -0
  44. package/src/multicall3/index.js +212 -0
  45. package/src/multisig-address.js +146 -0
  46. package/src/nft-utils.js +60 -0
  47. package/src/number-utils.js +34 -2
  48. package/src/optimism-gas/index.js +13 -5
  49. package/src/send-validations.js +58 -0
  50. package/src/server-based-fee-monitor.js +12 -19
  51. package/src/simulate-tx/fetch-tx-preview.js +0 -2
  52. package/src/simulate-tx/index.js +2 -2
  53. package/src/simulate-tx/simulate-eth-tx.js +24 -11
  54. package/src/simulation/common.js +34 -0
  55. package/src/simulation/create-simulate-message.js +49 -0
  56. package/src/simulation/create-simulate-transactions.js +108 -0
  57. package/src/simulation/estimate-fee.js +14 -0
  58. package/src/simulation/estimate-simple-transfer.js +15 -0
  59. package/src/simulation/get-message-type.js +18 -0
  60. package/src/simulation/simulate-message-api.js +68 -0
  61. package/src/simulation/simulate-transactions-api.js +266 -0
  62. package/src/simulation/simulate-transactions.js +16 -0
  63. package/src/simulation/transactions.js +52 -0
  64. package/src/simulation/try-estimating-changes-locally.js +26 -0
  65. package/src/staking/api/index.js +43 -0
  66. package/src/staking/cached-read.js +33 -0
  67. package/src/staking/ethereum/api.js +166 -34
  68. package/src/staking/ethereum/deps.js +37 -0
  69. package/src/staking/ethereum/everstake.js +113 -0
  70. package/src/staking/ethereum/index.js +4 -3
  71. package/src/staking/ethereum/service.js +682 -181
  72. package/src/staking/ethereum/staking-utils.js +275 -4
  73. package/src/staking/fantom-staking.js +9 -9
  74. package/src/staking/index.js +4 -4
  75. package/src/staking/matic/addresses.js +2 -0
  76. package/src/staking/matic/api.js +69 -49
  77. package/src/staking/matic/index.js +3 -3
  78. package/src/staking/matic/matic-staking-utils.js +243 -5
  79. package/src/staking/matic/matic-staking.md +53 -0
  80. package/src/staking/staking-provider-client.js +1 -1
  81. package/src/staking/utils/index.js +39 -0
  82. package/src/tx-create.js +666 -0
  83. package/src/tx-log/clarity-monitor.js +273 -103
  84. package/src/tx-log/clarity-truncated-history-monitor.js +109 -0
  85. package/src/tx-log/clarity-truncated-history-monitor.md +219 -0
  86. package/src/tx-log/clarity-utils/absolute.js +96 -0
  87. package/src/tx-log/clarity-utils/get-log-items-from-server-tx.js +94 -31
  88. package/src/tx-log/clarity-utils/get-names-of-tokens-transferred-by-server-tx.js +3 -2
  89. package/src/tx-log/clarity-utils/index.js +9 -3
  90. package/src/tx-log/clarity-utils/is-spam-pending-tx.js +13 -0
  91. package/src/tx-log/clarity-utils/normalize-transactions-response.js +65 -0
  92. package/src/tx-log/ethereum-monitor.js +93 -47
  93. package/src/tx-log/ethereum-no-history-monitor.js +52 -41
  94. package/src/tx-log/get-optimistic-txlog-effects.js +379 -0
  95. package/src/tx-log/index.js +5 -3
  96. package/src/tx-log/monitor-utils/check-pending-transactions.js +14 -5
  97. package/src/tx-log/monitor-utils/exclude-unchanged-token-balances.js +1 -1
  98. package/src/tx-log/monitor-utils/extract-balance-from-clarity-account-info.js +108 -0
  99. package/src/tx-log/monitor-utils/get-balance-updates.js +44 -0
  100. package/src/tx-log/monitor-utils/get-batched-rpc-balances.js +47 -0
  101. package/src/tx-log/monitor-utils/get-current-blacklist-status.js +38 -0
  102. package/src/tx-log/monitor-utils/get-current-eip7702-delegation.js +79 -0
  103. package/src/tx-log/monitor-utils/get-derive-transactions-to-check.js +10 -5
  104. package/src/tx-log/monitor-utils/get-fee-amount.js +1 -1
  105. package/src/tx-log/monitor-utils/get-history-from-server.js +1 -1
  106. package/src/tx-log/monitor-utils/get-log-items-from-server-tx.js +75 -33
  107. package/src/tx-log/monitor-utils/get-names-of-tokens-transferred-by-server-tx.js +3 -2
  108. package/src/tx-log/monitor-utils/get-raw-transfer-amount.js +29 -0
  109. package/src/tx-log/monitor-utils/get-transfers-by-token-name.js +3 -2
  110. package/src/tx-log/monitor-utils/get-value-of-transfers.js +10 -12
  111. package/src/tx-log/monitor-utils/index.js +12 -7
  112. package/src/tx-log/monitor-utils/is-unverifiable-pending-tx-expired.js +19 -0
  113. package/src/tx-log/monitor-utils/verify-pending-tx-status-rpc.js +90 -0
  114. package/src/tx-log-staking-processor/asset-staking-tx-data.js +82 -24
  115. package/src/tx-log-staking-processor/index.js +28 -14
  116. package/src/tx-log-staking-processor/utils.js +21 -20
  117. package/src/tx-send/broadcast-error-handler.js +63 -0
  118. package/src/tx-send/index.js +1 -2
  119. package/src/tx-send/nonce-utils.js +97 -0
  120. package/src/tx-send/tx-send.js +94 -288
  121. package/src/tx-sign/index.js +17 -0
  122. package/src/tx-type/index.js +280 -0
  123. package/src/watch-tx.js +36 -0
  124. package/src/web3/createSimulateMessage.js +3 -2
  125. package/src/web3/createSimulateTransactions.js +11 -0
  126. package/src/web3/index.js +3 -1
  127. package/src/etherscan/account.js +0 -49
  128. package/src/etherscan/index.js +0 -28
  129. package/src/etherscan/logs.js +0 -16
  130. package/src/etherscan/proxy.js +0 -47
  131. package/src/etherscan/request.js +0 -26
  132. package/src/etherscan/ws.js +0 -88
  133. package/src/exodus-eth-server/server-factory.js +0 -28
  134. package/src/exodus-eth-server/server-map.js +0 -21
  135. package/src/get-fee-async.js +0 -97
  136. package/src/staking/matic/service.js +0 -437
  137. package/src/staking-api.js +0 -5
  138. package/src/tx-log/clarity-utils/get-derive-data-needed-for-tick.js +0 -12
  139. package/src/tx-log-staking-processor/get-asset-tx-amount.js +0 -26
  140. package/src/tx-send/get-fee-info.js +0 -41
  141. package/src/websocket/index.android.js +0 -2
  142. package/src/websocket/index.ios.js +0 -2
  143. 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 { bufferToHex } from '@exodus/ethereumjs-util'
2
- import SolidityContract from '@exodus/solidity-contract'
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
- export default class ClarityServer extends EventEmitter {
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.uri}${namespace}`, {
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.rpcNamespace)
77
+ this.disconnectSocket(this.formatRpcNamespace())
68
78
  }
69
79
 
70
80
  disconnectFee() {
71
- this.disconnectSocket(this.feeNamespace)
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
- async getFee() {
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')), 30_000)
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('Rpc Timeout')), 30_000)
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
- async sendBatchRequest(batch) {
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
- // FIXME: this falls apart if responses is not an array
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
- async getLogs(...params) {
417
- const request = this.getLogsRequest(...params)
418
- return this.sendRequest(request)
220
+ return this.handleJsonRPCResponse(response)
419
221
  }
420
222
 
421
223
  formatTransactionsNamespace(address) {