@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.
- package/CHANGELOG.md +30 -0
- package/package.json +3 -4
- package/src/address-has-history.js +6 -2
- package/src/create-asset-plugin-factory.js +1 -0
- package/src/create-asset-utils.js +39 -35
- package/src/create-asset.js +26 -16
- package/src/eth-like-util.js +53 -0
- package/src/exodus-eth-server/api-coin-nodes.js +11 -84
- package/src/exodus-eth-server/clarity-v2.js +30 -51
- package/src/exodus-eth-server/clarity.js +2 -115
- package/src/exodus-eth-server/errors.js +5 -1
- package/src/exodus-eth-server/eth-like-server-base.js +123 -0
- package/src/exodus-eth-server/fetch-json.js +48 -0
- package/src/gas-estimation.js +19 -4
- package/src/get-balances.js +14 -0
- package/src/index.js +1 -0
- package/src/multicall3/index.js +169 -0
- package/src/simulation/common.js +34 -0
- package/src/simulation/create-simulate-message.js +49 -0
- package/src/simulation/create-simulate-transactions.js +106 -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 +265 -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/ethereum/staking-utils.js +3 -1
- package/src/staking/matic/matic-staking-utils.js +3 -1
- package/src/tx-log/clarity-truncated-history-monitor.js +34 -0
- package/src/tx-log/ethereum-no-history-monitor.js +2 -23
- package/src/tx-log/monitor-utils/get-batched-rpc-balances.js +28 -0
- package/src/tx-send/broadcast-error-handler.js +7 -2
- package/src/tx-send/tx-send.js +1 -0
- package/src/web3/createSimulateMessage.js +2 -1
- 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.
|
|
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.
|
|
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": "
|
|
72
|
+
"gitHead": "c704777d72888bf3a94f3a7ddc1b0e57cda3e6dc"
|
|
74
73
|
}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
export const addressHasHistoryFactory =
|
|
2
2
|
({ server }) =>
|
|
3
3
|
async (address) => {
|
|
4
|
-
const
|
|
5
|
-
|
|
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
|
}
|
|
@@ -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
|
|
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
|
-
|
|
222
|
+
server,
|
|
223
|
+
supportsTransactionPrivacy,
|
|
242
224
|
}) => {
|
|
243
225
|
assert(assetClientInterface, 'expected assetClientInterface')
|
|
226
|
+
assert(server, 'expected server')
|
|
244
227
|
|
|
245
|
-
if (!
|
|
228
|
+
if (!supportsTransactionPrivacy) return Object.create(null)
|
|
246
229
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
246
|
+
const bundleHash = sendBundleResult?.bundleHash
|
|
247
|
+
if (typeof bundleHash === 'string') return { bundleHash }
|
|
254
248
|
|
|
255
|
-
|
|
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,
|
package/src/create-asset.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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({
|
|
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
|
-
|
|
403
|
+
createRpcRequestAccumulator,
|
|
394
404
|
sendPrivateTx,
|
|
395
405
|
sendPrivateBundle,
|
|
396
406
|
}
|
package/src/eth-like-util.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
}
|