@exodus/ethereum-api 8.76.5 → 8.76.7

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 (37) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/package.json +3 -4
  3. package/src/address-has-history.js +6 -2
  4. package/src/create-asset-plugin-factory.js +1 -0
  5. package/src/create-asset-utils.js +39 -35
  6. package/src/create-asset.js +26 -16
  7. package/src/eth-like-util.js +53 -0
  8. package/src/exodus-eth-server/api-coin-nodes.js +11 -84
  9. package/src/exodus-eth-server/clarity-v2.js +30 -51
  10. package/src/exodus-eth-server/clarity.js +2 -115
  11. package/src/exodus-eth-server/errors.js +5 -1
  12. package/src/exodus-eth-server/eth-like-server-base.js +123 -0
  13. package/src/exodus-eth-server/fetch-json.js +48 -0
  14. package/src/gas-estimation.js +19 -4
  15. package/src/get-balances.js +14 -0
  16. package/src/index.js +1 -0
  17. package/src/multicall3/index.js +169 -0
  18. package/src/simulation/common.js +34 -0
  19. package/src/simulation/create-simulate-message.js +49 -0
  20. package/src/simulation/create-simulate-transactions.js +106 -0
  21. package/src/simulation/estimate-fee.js +14 -0
  22. package/src/simulation/estimate-simple-transfer.js +15 -0
  23. package/src/simulation/get-message-type.js +18 -0
  24. package/src/simulation/simulate-message-api.js +68 -0
  25. package/src/simulation/simulate-transactions-api.js +265 -0
  26. package/src/simulation/simulate-transactions.js +16 -0
  27. package/src/simulation/transactions.js +52 -0
  28. package/src/simulation/try-estimating-changes-locally.js +26 -0
  29. package/src/staking/ethereum/staking-utils.js +3 -1
  30. package/src/staking/matic/matic-staking-utils.js +3 -1
  31. package/src/tx-log/clarity-truncated-history-monitor.js +34 -0
  32. package/src/tx-log/ethereum-no-history-monitor.js +2 -23
  33. package/src/tx-log/monitor-utils/get-batched-rpc-balances.js +28 -0
  34. package/src/tx-send/broadcast-error-handler.js +7 -2
  35. package/src/tx-send/tx-send.js +1 -0
  36. package/src/web3/createSimulateMessage.js +2 -1
  37. package/src/web3/createSimulateTransactions.js +3 -9
package/CHANGELOG.md CHANGED
@@ -3,6 +3,36 @@
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.76.7](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.76.6...@exodus/ethereum-api@8.76.7) (2026-06-26)
7
+
8
+
9
+ ### Features
10
+
11
+ * clarity native transaction privacy ([#8265](https://github.com/ExodusMovement/assets/issues/8265)) ([c79975e](https://github.com/ExodusMovement/assets/commit/c79975ec0605ef05a4f6eb8c3d1d0fc4531a1f5b))
12
+ * enable multicall3 balance batches for `'no-history'` monitor ([#8276](https://github.com/ExodusMovement/assets/issues/8276)) ([acccb80](https://github.com/ExodusMovement/assets/commit/acccb8062ad5bc9f8c7f80bc14f53943b4421b2a))
13
+ * introduce multicall3 capability ([#8243](https://github.com/ExodusMovement/assets/issues/8243)) ([8fde0b7](https://github.com/ExodusMovement/assets/commit/8fde0b74322854d69d3ea962e5500287d111ec39))
14
+
15
+
16
+ ### Bug Fixes
17
+
18
+ * **ethereum-api:** retry and surface non-OK responses in ApiCoinNodesServer ([#8278](https://github.com/ExodusMovement/assets/issues/8278)) ([99cff43](https://github.com/ExodusMovement/assets/commit/99cff4317d5bb2b311972681625cd373c0b555af))
19
+ * **ethereum:** clamp negative spendable from in-flight sends ([#8246](https://github.com/ExodusMovement/assets/issues/8246)) ([25dbe72](https://github.com/ExodusMovement/assets/commit/25dbe7216bec02b4adec9362cf46509077ea7d1f))
20
+ * prevent incompatible account state from interfering with clarity… ([#8251](https://github.com/ExodusMovement/assets/issues/8251)) ([04ac9d0](https://github.com/ExodusMovement/assets/commit/04ac9d0658d57718c76831569455ed5c75566c55))
21
+ * prevent retry on nonce-too-low retry ([#8299](https://github.com/ExodusMovement/assets/issues/8299)) ([d7028b4](https://github.com/ExodusMovement/assets/commit/d7028b40add3fb33f3868f55de96939d9f452b34))
22
+ * use eip-7623 calldata pricing for defaultGasLimit ([#8288](https://github.com/ExodusMovement/assets/issues/8288)) ([227efda](https://github.com/ExodusMovement/assets/commit/227efda89a79970cd763805a17dd1b2ba07e0b23))
23
+
24
+
25
+
26
+ ## [8.76.6](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.76.5...@exodus/ethereum-api@8.76.6) (2026-06-11)
27
+
28
+
29
+ ### Bug Fixes
30
+
31
+
32
+ * fix: Ledger clear-signing for EVM token approvals (#8207)
33
+
34
+
35
+
6
36
  ## [8.76.5](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.76.4...@exodus/ethereum-api@8.76.5) (2026-06-03)
7
37
 
8
38
  **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.76.5",
3
+ "version": "8.76.7",
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",
@@ -29,7 +29,7 @@
29
29
  "@exodus/bip44-constants": "^195.0.0",
30
30
  "@exodus/crypto": "^1.0.0-rc.26",
31
31
  "@exodus/currency": "^6.0.1",
32
- "@exodus/ethereum-lib": "^5.24.2",
32
+ "@exodus/ethereum-lib": "^5.24.3",
33
33
  "@exodus/ethereum-meta": "^2.9.1",
34
34
  "@exodus/ethereumholesky-meta": "^2.0.5",
35
35
  "@exodus/ethereumjs": "^1.11.0",
@@ -40,7 +40,6 @@
40
40
  "@exodus/simple-retry": "^0.0.6",
41
41
  "@exodus/solidity-contract": "^1.3.0",
42
42
  "@exodus/traceparent": "^3.0.1",
43
- "@exodus/web3-ethereum-utils": "^4.7.4",
44
43
  "@exodus/web3-utils": "^1.51.2",
45
44
  "bn.js": "^5.2.1",
46
45
  "delay": "^4.0.1",
@@ -70,5 +69,5 @@
70
69
  "type": "git",
71
70
  "url": "git+https://github.com/ExodusMovement/assets.git"
72
71
  },
73
- "gitHead": "651f8346570d40e724109e3df1cd2a3e10323a6f"
72
+ "gitHead": "c704777d72888bf3a94f3a7ddc1b0e57cda3e6dc"
74
73
  }
@@ -1,6 +1,10 @@
1
1
  export const addressHasHistoryFactory =
2
2
  ({ server }) =>
3
3
  async (address) => {
4
- const history = await server.getHistoryV2(address, { index: 0, limit: 1 })
5
- return history.length > 0
4
+ const [nonceHex, balanceHex] = await Promise.all([
5
+ server.getTransactionCount(address, 'pending'),
6
+ server.getBalanceProxied(address, 'latest'),
7
+ ])
8
+
9
+ return Number.parseInt(nonceHex, 16) > 0 || Number.parseInt(balanceHex, 16) > 0
6
10
  }
@@ -60,6 +60,7 @@ export const createAssetPluginFactory = (config) => {
60
60
  ...config.feeData,
61
61
  }).filter(([_, value]) => value !== undefined)
62
62
  )
63
+
63
64
  const createAsset = createAssetFactory({
64
65
  assetsList,
65
66
  feeDataConfig,
@@ -5,8 +5,13 @@ import assert from 'minimalistic-assert'
5
5
  import ms from 'ms'
6
6
 
7
7
  import { EVM_ERROR_REASONS, withErrorReason } from './error-wrapper.js'
8
- import { createEvmServer, ValidMonitorTypes } from './exodus-eth-server/index.js'
8
+ import ClarityServerV2 from './exodus-eth-server/clarity-v2.js'
9
+ import { ValidMonitorTypes } from './exodus-eth-server/index.js'
9
10
  import { createEthereumHooks } from './hooks/index.js'
11
+ import {
12
+ EthLikeMulticall3RpcRequestAccumulator,
13
+ EthLikeRpcRequestAccumulator,
14
+ } from './multicall3/index.js'
10
15
  import { ClarityMonitor } from './tx-log/clarity-monitor.js'
11
16
  import { ClarityTruncatedHistoryMonitor } from './tx-log/clarity-truncated-history-monitor.js'
12
17
  import { EthereumMonitor } from './tx-log/ethereum-monitor.js'
@@ -101,27 +106,6 @@ export const stringifyPrivateTx = (tx) => {
101
106
  return `0x${tx}`
102
107
  }
103
108
 
104
- const broadcastPrivateBundleFactory =
105
- ({ privacyServer }) =>
106
- async ({ txs }) => {
107
- assert(Array.isArray(txs), 'txs must be an array')
108
- assert(txs.length > 0, 'txs must be an non-empty array')
109
-
110
- const sendBundleResult = await privacyServer.sendRequest(
111
- privacyServer.buildRequest({
112
- method: 'eth_sendBundle',
113
- params: [{ txs: txs.map((tx) => stringifyPrivateTx(tx)) }],
114
- })
115
- )
116
- if (typeof sendBundleResult === 'string') return { bundleHash: sendBundleResult }
117
-
118
- const bundleHash = sendBundleResult?.bundleHash
119
- if (typeof bundleHash === 'string') return { bundleHash }
120
-
121
- console.warn('unexpected sendBundleResult shape, cannot determine bundle hash')
122
- return null
123
- }
124
-
125
109
  const assertSendPrivateTxProps = ({ asset, unsignedTx, walletAccount }) => {
126
110
  assert(asset, 'expected asset')
127
111
  assert(unsignedTx, 'expected unsignedTx')
@@ -132,12 +116,10 @@ export const createTransactionPrivacyResult = ({
132
116
  assetClientInterface,
133
117
  broadcastPrivateBundle,
134
118
  broadcastPrivateTx,
135
- privacyServer,
136
119
  }) => {
137
120
  assert(assetClientInterface, 'expected assetClientInterface')
138
121
  assert(typeof broadcastPrivateBundle === 'function')
139
122
  assert(typeof broadcastPrivateTx === 'function')
140
- assert(privacyServer, 'expected privacyServer')
141
123
 
142
124
  const sendPrivateTx = async ({ asset, unsignedTx, walletAccount }) => {
143
125
  assertSendPrivateTxProps({ asset, unsignedTx, walletAccount })
@@ -229,7 +211,6 @@ export const createTransactionPrivacyResult = ({
229
211
  return {
230
212
  broadcastPrivateBundle,
231
213
  broadcastPrivateTx,
232
- privacyServer,
233
214
  sendPrivateTx,
234
215
  sendPrivateBundle,
235
216
  }
@@ -238,30 +219,53 @@ export const createTransactionPrivacyResult = ({
238
219
  export const createTransactionPrivacyFactory = ({
239
220
  assetClientInterface,
240
221
  assetName,
241
- privacyRpcUrl,
222
+ server,
223
+ supportsTransactionPrivacy,
242
224
  }) => {
243
225
  assert(assetClientInterface, 'expected assetClientInterface')
226
+ assert(server, 'expected server')
244
227
 
245
- if (!privacyRpcUrl) return Object.create(null)
228
+ if (!supportsTransactionPrivacy) return Object.create(null)
246
229
 
247
- const privacyServer = createEvmServer({
248
- assetName,
249
- serverUrl: privacyRpcUrl,
250
- monitorType: 'no-history',
251
- })
230
+ if (!(server instanceof ClarityServerV2)) {
231
+ console.warn(
232
+ `attempted to enable transactionPrivacy for ${assetName} but provided server is incompatible`
233
+ )
234
+ return Object.create(null)
235
+ }
236
+
237
+ const broadcastPrivateBundle = async ({ txs }) => {
238
+ assert(Array.isArray(txs), 'txs must be an array')
239
+ assert(txs.length > 0, 'txs must be a non-empty array')
240
+
241
+ const sendBundleResult = await server.sendBundle({
242
+ txs: txs.map((tx) => stringifyPrivateTx(tx)),
243
+ })
244
+ if (typeof sendBundleResult === 'string') return { bundleHash: sendBundleResult }
252
245
 
253
- const broadcastPrivateBundle = broadcastPrivateBundleFactory({ privacyServer })
246
+ const bundleHash = sendBundleResult?.bundleHash
247
+ if (typeof bundleHash === 'string') return { bundleHash }
254
248
 
255
- const broadcastPrivateTx = (...args) => privacyServer.sendRawTransaction(...args)
249
+ console.warn('unexpected sendBundleResult shape, cannot determine bundle hash')
250
+ return null
251
+ }
252
+
253
+ const broadcastPrivateTx = (tx) => broadcastPrivateBundle({ txs: [tx] })
256
254
 
257
255
  return createTransactionPrivacyResult({
258
256
  assetClientInterface,
259
257
  broadcastPrivateBundle,
260
258
  broadcastPrivateTx,
261
- privacyServer,
262
259
  })
263
260
  }
264
261
 
262
+ export const createRpcRequestAccumulatorFactory =
263
+ ({ multicall3Address }) =>
264
+ () =>
265
+ multicall3Address
266
+ ? new EthLikeMulticall3RpcRequestAccumulator({ multicall3Address })
267
+ : new EthLikeRpcRequestAccumulator()
268
+
265
269
  export const createHistoryMonitorFactory = ({
266
270
  assetName,
267
271
  assetClientInterface,
@@ -27,6 +27,7 @@ import { createCheckTx } from './check-tx/index.js'
27
27
  import {
28
28
  createGetBlackListStatus,
29
29
  createHistoryMonitorFactory,
30
+ createRpcRequestAccumulatorFactory,
30
31
  createSecurityChecks,
31
32
  createTransactionPrivacyFactory,
32
33
  getNonceFactory,
@@ -34,7 +35,7 @@ import {
34
35
  } from './create-asset-utils.js'
35
36
  import { createTokenFactory } from './create-token-factory.js'
36
37
  import { createCustomFeesApi } from './custom-fees.js'
37
- import { getEIP7702Delegation } from './eth-like-util.js'
38
+ import { getEIP7702Delegation, getIsNftContract } from './eth-like-util.js'
38
39
  import { createEvmServer } from './exodus-eth-server/index.js'
39
40
  import { createFeeData } from './fee-data/index.js'
40
41
  import { createGetBalanceForAddress } from './get-balance-for-address.js'
@@ -68,6 +69,7 @@ export const createAssetFactory = ({
68
69
  l1GasOracleAddress, // l1 extra fee for base and optostakingConfiguration = {},
69
70
  monitorInterval: defaultMonitorInterval,
70
71
  monitorType: defaultMonitorType = 'magnifier',
72
+ multicall3Address: defaultMulticall3Address,
71
73
  nfts: defaultNfts = false,
72
74
  serverUrl: defaultServerUrl,
73
75
  stakingConfiguration = Object.create(null),
@@ -79,8 +81,9 @@ export const createAssetFactory = ({
79
81
  supportsCustomFees: defaultSupportsCustomFees = false,
80
82
  useAbsoluteBalanceAndNonce = false,
81
83
  delisted = false,
82
- privacyRpcUrl: defaultPrivacyRpcUrl,
84
+ supportsTransactionPrivacy: defaultSupportsTransactionPrivacy,
83
85
  wsGatewayUri: defaultWsGatewayUri,
86
+ eip7623Supported = false,
84
87
  eip7702Supported,
85
88
  transactionAssessment: defaultTransactionAssessment,
86
89
  }) => {
@@ -105,8 +108,9 @@ export const createAssetFactory = ({
105
108
  supportsCustomFees: defaultSupportsCustomFees,
106
109
  nfts: defaultNfts,
107
110
  customTokens: defaultCustomTokens,
108
- privacyRpcUrl: defaultPrivacyRpcUrl,
111
+ supportsTransactionPrivacy: defaultSupportsTransactionPrivacy,
109
112
  transactionAssessment: defaultTransactionAssessment,
113
+ multicall3Address: defaultMulticall3Address,
110
114
  }
111
115
  return (
112
116
  {
@@ -126,8 +130,9 @@ export const createAssetFactory = ({
126
130
  customTokens,
127
131
  supportsCustomFees,
128
132
  useAbsoluteBalanceAndNonce: overrideUseAbsoluteBalanceAndNonce,
129
- privacyRpcUrl,
133
+ supportsTransactionPrivacy,
130
134
  transactionAssessment,
135
+ multicall3Address,
131
136
  } = configWithOverrides
132
137
 
133
138
  const asset = assets[base.name]
@@ -202,16 +207,16 @@ export const createAssetFactory = ({
202
207
  server,
203
208
  })
204
209
 
205
- const {
206
- broadcastPrivateBundle,
207
- broadcastPrivateTx,
208
- privacyServer,
209
- sendPrivateTx,
210
- sendPrivateBundle,
211
- } = createTransactionPrivacyFactory({
212
- assetClientInterface,
213
- assetName: asset.name,
214
- privacyRpcUrl,
210
+ const { broadcastPrivateBundle, broadcastPrivateTx, sendPrivateTx, sendPrivateBundle } =
211
+ createTransactionPrivacyFactory({
212
+ assetClientInterface,
213
+ assetName: asset.name,
214
+ server,
215
+ supportsTransactionPrivacy,
216
+ })
217
+
218
+ const createRpcRequestAccumulator = createRpcRequestAccumulatorFactory({
219
+ multicall3Address,
215
220
  })
216
221
 
217
222
  const features = {
@@ -228,6 +233,7 @@ export const createAssetFactory = ({
228
233
  signWithSigner: true,
229
234
  signMessageWithSigner: true,
230
235
  supportsCustomFees,
236
+ supportsTransactionPrivacy: Boolean(supportsTransactionPrivacy),
231
237
  ...(supportsStaking && { staking: {} }),
232
238
  ...(delisted && { delisted }),
233
239
  }
@@ -353,7 +359,10 @@ export const createAssetFactory = ({
353
359
  signer
354
360
  ? signUnsignedTxWithSigner(unsignedTx, signer)
355
361
  : signUnsignedTx(unsignedTx, privateKey),
356
- signHardware: signHardwareFactory({ baseAssetName: asset.name }),
362
+ signHardware: signHardwareFactory({
363
+ baseAssetName: asset.name,
364
+ getIsNft: ({ address }) => getIsNftContract({ server, address }),
365
+ }),
357
366
  signMessage: ({ message, privateKey, signer }) =>
358
367
  signer ? signMessageWithSigner({ message, signer }) : signMessage({ privateKey, message }),
359
368
  ...(checkTx && { checkTx }),
@@ -382,6 +391,7 @@ export const createAssetFactory = ({
382
391
  monitorType,
383
392
  estimateL1DataFee,
384
393
  forceGasLimitEstimation,
394
+ eip7623Supported,
385
395
  eip7702Supported,
386
396
  getEIP7702Delegation: (addr) => getEIP7702Delegation({ address: addr, server }),
387
397
  getNonce,
@@ -390,7 +400,7 @@ export const createAssetFactory = ({
390
400
  ...(fuelThreshold && { fuelThreshold: asset.currency.defaultUnit(fuelThreshold) }),
391
401
  broadcastPrivateBundle,
392
402
  broadcastPrivateTx,
393
- privacyServer,
403
+ createRpcRequestAccumulator,
394
404
  sendPrivateTx,
395
405
  sendPrivateBundle,
396
406
  }
@@ -159,6 +159,7 @@ export const getIsForwarderContract = memoizeLruCache(
159
159
 
160
160
  const ERC20 = new SolidityContract(ABI.erc20)
161
161
  const ERC20BytesParams = new SolidityContract(ABI.erc20BytesParams)
162
+ const ERC721 = new SolidityContract(ABI.erc721)
162
163
  const DEFAULT_PARAM_NAMES = ['decimals', 'name', 'symbol']
163
164
  const erc20ParamsCache = Object.create(null)
164
165
 
@@ -213,6 +214,58 @@ export const getERC20Params = async ({ asset, address, paramNames = DEFAULT_PARA
213
214
  return response
214
215
  }
215
216
 
217
+ // ERC-165 introspection used to drive Ledger's clear-signing for contract calls:
218
+ // it tells the device whether the target is an NFT (ERC-721/1155) or a fungible
219
+ // token. See `signHardwareFactory` in ethereum-lib for how this feeds the device's
220
+ // `isNft` hint. Display-only: never affects the signed transaction.
221
+ const buildSupportsInterfaceData = (interfaceId) => {
222
+ const erc165InterfaceId = `0x${interfaceId}`
223
+ return `0x${ERC721.supportsInterface.build(erc165InterfaceId).toString('hex')}`
224
+ }
225
+
226
+ const decodeBool = (result) => {
227
+ return ERC721.decodeOutput({ method: 'supportsInterface', data: result })[0]
228
+ }
229
+
230
+ // true = supported, false = not supported / reverted, undefined = transport failure
231
+ const probeSupportsInterface = async ({ server, address, interfaceId }) => {
232
+ try {
233
+ const result = await server.ethCall({
234
+ to: address,
235
+ data: buildSupportsInterfaceData(interfaceId),
236
+ })
237
+ return decodeBool(result)
238
+ } catch (err) {
239
+ // Empirically, a contract without supportsInterface comes through Clarity as
240
+ // "Bad rpc response: execution reverted". Unknown errors stay undefined so
241
+ // we don't force a wrong hint.
242
+ const EXECUTION_REVERT_MESSAGE = 'execution reverted'
243
+ const errorMessage = err?.message || String(err || '')
244
+ return errorMessage.toLowerCase().includes(EXECUTION_REVERT_MESSAGE) ? false : undefined
245
+ }
246
+ }
247
+
248
+ // Resolves whether `address` is an NFT contract (ERC-721 or ERC-1155) via ERC-165.
249
+ // Returns true (NFT), false (not an NFT), or undefined (couldn't determine).
250
+ export async function getIsNftContract({ server, address }) {
251
+ assert(server, 'getIsNftContract(): server required')
252
+ assert(address, 'getIsNftContract(): address required')
253
+
254
+ const ERC721_INTERFACE_ID = '80ac58cd'
255
+ const ERC1155_INTERFACE_ID = 'd9b67a26'
256
+
257
+ const [isErc721, isErc1155] = await Promise.all([
258
+ probeSupportsInterface({ server, address, interfaceId: ERC721_INTERFACE_ID }),
259
+ probeSupportsInterface({ server, address, interfaceId: ERC1155_INTERFACE_ID }),
260
+ ])
261
+
262
+ if (isErc721 === true || isErc1155 === true) {
263
+ return true
264
+ }
265
+
266
+ if (isErc721 === false && isErc1155 === false) return false
267
+ }
268
+
216
269
  export async function getEIP7702Delegation({ address, server }) {
217
270
  const code = await withErrorReason({
218
271
  promise: server.getCode(address),
@@ -1,22 +1,20 @@
1
- import { bufferToHex } from '@exodus/ethereumjs/util'
2
1
  import { safeString } from '@exodus/safe-string'
3
- import SolidityContract from '@exodus/solidity-contract'
4
- import EventEmitter from 'events/events.js'
5
2
  import lodash from 'lodash'
6
3
 
7
4
  import { fromHexToString } from '../number-utils.js'
8
5
  import { errorMessageToSafeHint } from './errors.js'
6
+ import EthLikeServerBase from './eth-like-server-base.js'
7
+ import { fetchJsonRetry } from './fetch-json.js'
9
8
  import { getFallbackGasPriceEstimation } from './utils.js'
10
9
 
11
10
  const { isEmpty } = lodash
12
11
 
13
- export default class ApiCoinNodesServer extends EventEmitter {
12
+ export default class ApiCoinNodesServer extends EthLikeServerBase {
14
13
  constructor({ baseAssetName, uri }) {
15
14
  super()
16
15
  this.baseAssetName = baseAssetName
17
16
  this.uri = uri
18
17
  this.defaultUri = uri
19
- this.id = 0
20
18
  }
21
19
 
22
20
  setURI(uri) {
@@ -30,8 +28,7 @@ export default class ApiCoinNodesServer extends EventEmitter {
30
28
  body: JSON.stringify(body),
31
29
  }
32
30
 
33
- const response = await fetch(this.uri, options)
34
- return response.json()
31
+ return fetchJsonRetry(this.uri, options)
35
32
  }
36
33
 
37
34
  async sendBatchRequest(batch) {
@@ -60,6 +57,12 @@ export default class ApiCoinNodesServer extends EventEmitter {
60
57
 
61
58
  const revisedError = new Error(`Bad rpc response: ${message}`)
62
59
  revisedError.hint = safeString`Bad rpc response: ${errorMessageToSafeHint(message)}`
60
+
61
+ const traceId = response?.__traceId
62
+ if (traceId) {
63
+ revisedError.traceId = traceId
64
+ }
65
+
63
66
  throw revisedError
64
67
  }
65
68
 
@@ -71,78 +74,6 @@ export default class ApiCoinNodesServer extends EventEmitter {
71
74
  return code.length > 2
72
75
  }
73
76
 
74
- buildRequest({ method, params = [] }) {
75
- return { jsonrpc: '2.0', id: this.id++, method, params }
76
- }
77
-
78
- balanceOfRequest(address, tokenAddress, tag = 'latest') {
79
- const contract = SolidityContract.simpleErc20(tokenAddress)
80
- const callData = contract.balanceOf.build(address)
81
- const data = {
82
- data: bufferToHex(callData),
83
- to: tokenAddress,
84
- }
85
- return this.ethCallRequest(data, tag)
86
- }
87
-
88
- getBalanceRequest(address, tag = 'latest') {
89
- return this.buildRequest({ method: 'eth_getBalance', params: [address, tag] })
90
- }
91
-
92
- gasPriceRequest() {
93
- return this.buildRequest({ method: 'eth_gasPrice' })
94
- }
95
-
96
- estimateGasRequest(data) {
97
- return this.buildRequest({ method: 'eth_estimateGas', params: [data] })
98
- }
99
-
100
- sendRawTransactionRequest(data) {
101
- const _data = data instanceof Uint8Array ? Buffer.from(data).toString('hex') : data
102
- const hex = _data.startsWith('0x') ? _data : '0x' + _data
103
- return this.buildRequest({ method: 'eth_sendRawTransaction', params: [hex] })
104
- }
105
-
106
- getCodeRequest(address, tag = 'latest') {
107
- return this.buildRequest({ method: 'eth_getCode', params: [address, tag] })
108
- }
109
-
110
- getStorageAtRequest(address, position, tag = 'latest') {
111
- return this.buildRequest({ method: 'eth_getStorageAt', params: [address, position, tag] })
112
- }
113
-
114
- getTransactionCountRequest(address, tag = 'latest') {
115
- return this.buildRequest({ method: 'eth_getTransactionCount', params: [address, tag] })
116
- }
117
-
118
- getTransactionByHashRequest(hash) {
119
- return this.buildRequest({ method: 'eth_getTransactionByHash', params: [hash] })
120
- }
121
-
122
- getTransactionReceiptRequest(txhash) {
123
- return this.buildRequest({ method: 'eth_getTransactionReceipt', params: [txhash] })
124
- }
125
-
126
- ethCallRequest(data, tag = 'latest') {
127
- return this.buildRequest({ method: 'eth_call', params: [data, tag] })
128
- }
129
-
130
- blockNumberRequest() {
131
- return this.buildRequest({ method: 'eth_blockNumber' })
132
- }
133
-
134
- getBlockByNumberRequest(numberHex, isFullTxs = false) {
135
- return this.buildRequest({ method: 'eth_getBlockByNumber', params: [numberHex, isFullTxs] })
136
- }
137
-
138
- simulateRawTransactionRequest(rawTx, applyPending = true) {
139
- const replaced = rawTx.replace('0x', '')
140
- return this.buildRequest({
141
- method: 'debug_simulateRawTransaction',
142
- params: [replaced, applyPending],
143
- })
144
- }
145
-
146
77
  async balanceOf(address, tokenAddress, tag = 'latest') {
147
78
  const request = this.balanceOfRequest(address, tokenAddress, tag)
148
79
  const result = await this.sendRequest(request)
@@ -176,11 +107,7 @@ export default class ApiCoinNodesServer extends EventEmitter {
176
107
  getGasPrice = this.gasPrice
177
108
 
178
109
  async getLatestBlock() {
179
- const request = this.buildRequest({
180
- method: 'eth_getBlockByNumber',
181
- params: ['latest', false],
182
- })
183
- return this.sendRequest(request)
110
+ return this.sendRequest(this.getBlockByNumberRequest('latest', false))
184
111
  }
185
112
 
186
113
  async getBaseFeePerGas() {
@@ -1,8 +1,7 @@
1
- import { retry } from '@exodus/simple-retry'
2
- import { TraceId } from '@exodus/traceparent'
3
1
  import assert from 'minimalistic-assert'
4
2
 
5
3
  import ClarityServer, { RPC_REQUEST_TIMEOUT } from './clarity.js'
4
+ import { fetchJsonRetry } from './fetch-json.js'
6
5
 
7
6
  const ASSETS_GATEWAY_URL = 'https://assets-gateway-clarity-api.a.exodus.io/assets'
8
7
 
@@ -33,56 +32,12 @@ export const decodeCursor = (cursor) => {
33
32
  return { blockNumber: BigInt(0) }
34
33
  }
35
34
 
36
- const getTextFromResponse = async (response) => {
37
- try {
38
- const responseBody = await response.text()
39
- return responseBody.slice(0, 100)
40
- } catch {
41
- return ''
42
- }
43
- }
44
-
45
- const fetchJson = async (url, fetchOptions) => {
46
- const response = await fetch(url, fetchOptions)
47
-
48
- if (!response.ok) {
49
- const traceId = TraceId.fromResponse(response)
50
- const error = new Error(
51
- `${url} returned ${response.status}: ${
52
- response.statusText || 'Unknown Status Text'
53
- }. Body: ${await getTextFromResponse(response)}`
54
- )
55
- if (traceId) {
56
- error.traceId = traceId
57
- }
58
-
59
- throw error
60
- }
61
-
62
- const json = await response.json()
63
-
64
- // Only capture trace ID if there's an RPC error in the response
65
- // (handleJsonRPCResponse will extract it when throwing the error)
66
- if (json.error) {
67
- const traceId = TraceId.fromResponse(response)
68
- if (traceId) {
69
- json.__traceId = traceId
70
- }
71
- }
72
-
73
- return json
74
- }
75
-
76
- async function fetchJsonRetry(url, fetchOptions) {
77
- const waitTimes = ['3s']
78
- const fetchWithRetry = retry(fetchJson, { delayTimesMs: waitTimes })
79
- return fetchWithRetry(url, fetchOptions)
80
- }
81
-
82
- const fetchHttpRequest = ({ baseApiPath, path, method, body }) => {
35
+ const fetchHttpRequest = ({ baseApiPath, path, method, body, search }) => {
83
36
  assert(typeof baseApiPath === 'string', 'expected string baseApiPath')
84
37
 
85
38
  const url = new URL(`${baseApiPath}${path}`)
39
+ if (search) url.search = search
40
+
86
41
  const fetchOptions = {
87
42
  method,
88
43
  headers: { 'Content-Type': 'application/json' },
@@ -230,8 +185,8 @@ export default class ClarityServerV2 extends ClarityServer {
230
185
  }
231
186
  }
232
187
 
233
- fetchRpcHttpRequest = ({ baseApiPath, body }) => {
234
- return fetchHttpRequest({ baseApiPath, path: '/rpc', method: 'POST', body })
188
+ fetchRpcHttpRequest = ({ baseApiPath, path = '/rpc', body }) => {
189
+ return fetchHttpRequest({ baseApiPath, path, method: 'POST', body })
235
190
  }
236
191
 
237
192
  async sendRpcRequest(rpcRequest) {
@@ -273,4 +228,28 @@ export default class ClarityServerV2 extends ClarityServer {
273
228
  await this.fetchRpcHttpRequest({ baseApiPath, body: request })
274
229
  )
275
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
+ }
276
255
  }