@exodus/ethereum-api 8.76.4 → 8.76.6
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 +18 -0
- package/package.json +3 -3
- package/src/address-has-history.js +6 -2
- package/src/check-tx/create-assess-transaction.js +23 -0
- package/src/check-tx/create-check-tx.js +12 -24
- package/src/create-asset.js +18 -11
- package/src/eth-like-util.js +53 -0
- package/src/send-validations.js +1 -2
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,24 @@
|
|
|
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.6](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.76.5...@exodus/ethereum-api@8.76.6) (2026-06-11)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
* fix: Ledger clear-signing for EVM token approvals (#8207)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
## [8.76.5](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.76.4...@exodus/ethereum-api@8.76.5) (2026-06-03)
|
|
17
|
+
|
|
18
|
+
**Note:** Version bump only for package @exodus/ethereum-api
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
6
24
|
## [8.76.4](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.76.3...@exodus/ethereum-api@8.76.4) (2026-06-02)
|
|
7
25
|
|
|
8
26
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/ethereum-api",
|
|
3
|
-
"version": "8.76.
|
|
3
|
+
"version": "8.76.6",
|
|
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",
|
|
@@ -70,5 +70,5 @@
|
|
|
70
70
|
"type": "git",
|
|
71
71
|
"url": "git+https://github.com/ExodusMovement/assets.git"
|
|
72
72
|
},
|
|
73
|
-
"gitHead": "
|
|
73
|
+
"gitHead": "f9bb7fcf58132b8daa6db82edc0bc53f380029cd"
|
|
74
74
|
}
|
|
@@ -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
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { fetchival } from '@exodus/fetch'
|
|
2
|
+
import assert from 'minimalistic-assert'
|
|
3
|
+
|
|
4
|
+
export const DEFAULT_ASSESS_TRANSACTION_API_URL =
|
|
5
|
+
'https://simulation.a.exodus.io/simulation/transaction/assessment'
|
|
6
|
+
|
|
7
|
+
export const createAssessTransaction = (
|
|
8
|
+
{ apiUrl = DEFAULT_ASSESS_TRANSACTION_API_URL, headers, request = fetchival } = Object.create(
|
|
9
|
+
null
|
|
10
|
+
)
|
|
11
|
+
) => {
|
|
12
|
+
assert(typeof apiUrl === 'string' && apiUrl.length > 0, 'apiUrl must be a non-empty string')
|
|
13
|
+
|
|
14
|
+
return async function assessTransaction({ payload }) {
|
|
15
|
+
return request(apiUrl, {
|
|
16
|
+
method: 'POST',
|
|
17
|
+
headers: {
|
|
18
|
+
...headers,
|
|
19
|
+
'Content-Type': 'application/json',
|
|
20
|
+
},
|
|
21
|
+
}).post(payload)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { memoizeLruCache } from '@exodus/asset-lib'
|
|
2
|
-
import { makeSimulationAPICall } from '@exodus/web3-utils'
|
|
3
2
|
import assert from 'minimalistic-assert'
|
|
4
3
|
import ms from 'ms'
|
|
5
4
|
|
|
@@ -37,25 +36,25 @@ const isRecipientFinding = ({ finding, recipientAddress }) => {
|
|
|
37
36
|
})
|
|
38
37
|
}
|
|
39
38
|
|
|
40
|
-
const
|
|
41
|
-
if (!Array.isArray(findings) || !recipientAddress) return
|
|
42
|
-
|
|
43
|
-
isRecipientFinding({ finding: candidate, recipientAddress })
|
|
44
|
-
)
|
|
45
|
-
return finding?.title || GENERIC_RISK_REASON
|
|
39
|
+
const findRecipientFinding = ({ findings, recipientAddress }) => {
|
|
40
|
+
if (!Array.isArray(findings) || !recipientAddress) return
|
|
41
|
+
return findings.find((candidate) => isRecipientFinding({ finding: candidate, recipientAddress }))
|
|
46
42
|
}
|
|
47
43
|
|
|
48
44
|
const parseAssessment = ({ body, recipientAddress }) => {
|
|
49
45
|
if (!body?.success) return { action: 'NONE' }
|
|
50
46
|
if (body.data?.recommendation !== 'deny') return { action: 'NONE' }
|
|
47
|
+
const finding = findRecipientFinding({ findings: body.data?.findings, recipientAddress })
|
|
48
|
+
if (!finding) return { action: 'NONE' }
|
|
49
|
+
|
|
51
50
|
return {
|
|
52
51
|
action: 'WARN',
|
|
53
|
-
reason:
|
|
52
|
+
reason: finding.title || GENERIC_RISK_REASON,
|
|
54
53
|
}
|
|
55
54
|
}
|
|
56
55
|
|
|
57
|
-
// Rejects after `timeoutMs`. The underlying request keeps running because
|
|
58
|
-
//
|
|
56
|
+
// Rejects after `timeoutMs`. The underlying request keeps running because the
|
|
57
|
+
// API helper doesn't accept an AbortSignal.
|
|
59
58
|
const withTimeout = async (promise, timeoutMs) => {
|
|
60
59
|
let timeoutId
|
|
61
60
|
const timeoutPromise = new Promise((_resolve, reject) => {
|
|
@@ -71,14 +70,9 @@ const withTimeout = async (promise, timeoutMs) => {
|
|
|
71
70
|
const noopLogger = { warn: () => {} }
|
|
72
71
|
|
|
73
72
|
export const createCheckTx = (
|
|
74
|
-
{
|
|
75
|
-
apiUrl,
|
|
76
|
-
timeout = DEFAULT_TIMEOUT_MS,
|
|
77
|
-
makeApiCall = makeSimulationAPICall,
|
|
78
|
-
logger = noopLogger,
|
|
79
|
-
} = Object.create(null)
|
|
73
|
+
{ timeout = DEFAULT_TIMEOUT_MS, makeApiCall, logger = noopLogger } = Object.create(null)
|
|
80
74
|
) => {
|
|
81
|
-
assert(
|
|
75
|
+
assert(typeof makeApiCall === 'function', 'makeApiCall is required')
|
|
82
76
|
|
|
83
77
|
const warn = typeof logger?.warn === 'function' ? logger.warn.bind(logger) : noopLogger.warn
|
|
84
78
|
|
|
@@ -90,13 +84,7 @@ export const createCheckTx = (
|
|
|
90
84
|
if (input !== undefined) transaction.input = input
|
|
91
85
|
if (hash !== undefined) transaction.hash = hash
|
|
92
86
|
|
|
93
|
-
const body = await withTimeout(
|
|
94
|
-
makeApiCall({
|
|
95
|
-
url: apiUrl,
|
|
96
|
-
payload: { serviceProvider: 'hypernative', transaction },
|
|
97
|
-
}),
|
|
98
|
-
timeout
|
|
99
|
-
)
|
|
87
|
+
const body = await withTimeout(makeApiCall({ payload: { transaction } }), timeout)
|
|
100
88
|
|
|
101
89
|
if (!body) throw new Error('checkTx: empty response')
|
|
102
90
|
return parseAssessment({ body, recipientAddress: toAddress })
|
package/src/create-asset.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createConsoleLogger } from '@exodus/asset-lib'
|
|
1
2
|
import { ASSET_FAMILY, connectAssetsList } from '@exodus/assets'
|
|
2
3
|
import bip44Constants from '@exodus/bip44-constants/by-ticker.js'
|
|
3
4
|
import {
|
|
@@ -21,6 +22,7 @@ import lodash from 'lodash'
|
|
|
21
22
|
import assert from 'minimalistic-assert'
|
|
22
23
|
|
|
23
24
|
import { addressHasHistoryFactory } from './address-has-history.js'
|
|
25
|
+
import { createAssessTransaction } from './check-tx/create-assess-transaction.js'
|
|
24
26
|
import { createCheckTx } from './check-tx/index.js'
|
|
25
27
|
import {
|
|
26
28
|
createGetBlackListStatus,
|
|
@@ -32,7 +34,7 @@ import {
|
|
|
32
34
|
} from './create-asset-utils.js'
|
|
33
35
|
import { createTokenFactory } from './create-token-factory.js'
|
|
34
36
|
import { createCustomFeesApi } from './custom-fees.js'
|
|
35
|
-
import { getEIP7702Delegation } from './eth-like-util.js'
|
|
37
|
+
import { getEIP7702Delegation, getIsNftContract } from './eth-like-util.js'
|
|
36
38
|
import { createEvmServer } from './exodus-eth-server/index.js'
|
|
37
39
|
import { createFeeData } from './fee-data/index.js'
|
|
38
40
|
import { createGetBalanceForAddress } from './get-balance-for-address.js'
|
|
@@ -78,9 +80,9 @@ export const createAssetFactory = ({
|
|
|
78
80
|
useAbsoluteBalanceAndNonce = false,
|
|
79
81
|
delisted = false,
|
|
80
82
|
privacyRpcUrl: defaultPrivacyRpcUrl,
|
|
81
|
-
riskAssessment: defaultRiskAssessment,
|
|
82
83
|
wsGatewayUri: defaultWsGatewayUri,
|
|
83
84
|
eip7702Supported,
|
|
85
|
+
transactionAssessment: defaultTransactionAssessment,
|
|
84
86
|
}) => {
|
|
85
87
|
assert(assetsList, 'assetsList is required')
|
|
86
88
|
assert(providedFeeData || feeDataConfig, 'feeData or feeDataConfig is required')
|
|
@@ -104,7 +106,7 @@ export const createAssetFactory = ({
|
|
|
104
106
|
nfts: defaultNfts,
|
|
105
107
|
customTokens: defaultCustomTokens,
|
|
106
108
|
privacyRpcUrl: defaultPrivacyRpcUrl,
|
|
107
|
-
|
|
109
|
+
transactionAssessment: defaultTransactionAssessment,
|
|
108
110
|
}
|
|
109
111
|
return (
|
|
110
112
|
{
|
|
@@ -125,7 +127,7 @@ export const createAssetFactory = ({
|
|
|
125
127
|
supportsCustomFees,
|
|
126
128
|
useAbsoluteBalanceAndNonce: overrideUseAbsoluteBalanceAndNonce,
|
|
127
129
|
privacyRpcUrl,
|
|
128
|
-
|
|
130
|
+
transactionAssessment,
|
|
129
131
|
} = configWithOverrides
|
|
130
132
|
|
|
131
133
|
const asset = assets[base.name]
|
|
@@ -294,16 +296,18 @@ export const createAssetFactory = ({
|
|
|
294
296
|
|
|
295
297
|
const securityChecks = createSecurityChecks({ eip7702Supported })
|
|
296
298
|
|
|
299
|
+
const web3 = createWeb3API({ asset })
|
|
300
|
+
|
|
297
301
|
let checkTx
|
|
298
|
-
|
|
302
|
+
let sendValidations = []
|
|
303
|
+
if (transactionAssessment?.enabled) {
|
|
299
304
|
checkTx = createCheckTx({
|
|
300
|
-
apiUrl:
|
|
301
|
-
logger:
|
|
305
|
+
makeApiCall: createAssessTransaction({ apiUrl: transactionAssessment.apiUrl }),
|
|
306
|
+
logger: createConsoleLogger('@exodus/ethereum-api:check-tx'),
|
|
302
307
|
})
|
|
308
|
+
sendValidations = createSendValidations({ assetClientInterface, checkTx })
|
|
303
309
|
}
|
|
304
310
|
|
|
305
|
-
const sendValidations = checkTx ? createSendValidations({ assetClientInterface, checkTx }) : []
|
|
306
|
-
|
|
307
311
|
const moveFunds = moveFundsFactory({
|
|
308
312
|
baseAssetName: asset.name,
|
|
309
313
|
assetClientInterface,
|
|
@@ -349,7 +353,10 @@ export const createAssetFactory = ({
|
|
|
349
353
|
signer
|
|
350
354
|
? signUnsignedTxWithSigner(unsignedTx, signer)
|
|
351
355
|
: signUnsignedTx(unsignedTx, privateKey),
|
|
352
|
-
signHardware: signHardwareFactory({
|
|
356
|
+
signHardware: signHardwareFactory({
|
|
357
|
+
baseAssetName: asset.name,
|
|
358
|
+
getIsNft: ({ address }) => getIsNftContract({ server, address }),
|
|
359
|
+
}),
|
|
353
360
|
signMessage: ({ message, privateKey, signer }) =>
|
|
354
361
|
signer ? signMessageWithSigner({ message, signer }) : signMessage({ privateKey, message }),
|
|
355
362
|
...(checkTx && { checkTx }),
|
|
@@ -365,7 +372,7 @@ export const createAssetFactory = ({
|
|
|
365
372
|
}),
|
|
366
373
|
}),
|
|
367
374
|
validateAssetId: address.validate,
|
|
368
|
-
web3
|
|
375
|
+
web3,
|
|
369
376
|
}
|
|
370
377
|
|
|
371
378
|
const fullAsset = {
|
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),
|
package/src/send-validations.js
CHANGED
|
@@ -39,14 +39,13 @@ export const createSendValidations = ({ assetClientInterface, checkTx }) => {
|
|
|
39
39
|
})
|
|
40
40
|
if (!fromAddress) return
|
|
41
41
|
|
|
42
|
-
//
|
|
42
|
+
// Native send: empty calldata, no signed tx hash to pass yet.
|
|
43
43
|
const result = await checkTx({
|
|
44
44
|
chain: asset.chainId,
|
|
45
45
|
fromAddress,
|
|
46
46
|
toAddress: destinationAddress,
|
|
47
47
|
value: sendAmount.toBaseString({ unit: false }),
|
|
48
48
|
input: '0x',
|
|
49
|
-
hash: '0x1',
|
|
50
49
|
})
|
|
51
50
|
|
|
52
51
|
if (result?.action !== 'WARN') return
|