@exodus/ethereum-api 8.73.6 → 8.74.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/package.json +2 -2
- package/src/create-asset.js +10 -0
- package/src/exodus-eth-server/ws-gateway.js +0 -1
- package/src/exodus-eth-server/ws.js +0 -1
- package/src/index.js +2 -0
- package/src/move-funds.js +120 -0
- package/src/tx-send/nonce-utils.js +19 -4
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,16 @@
|
|
|
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.74.0](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.73.6...@exodus/ethereum-api@8.74.0) (2026-05-12)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
* feat: add MoveFunds API for EVM assets (#7868)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
6
16
|
## [8.73.6](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.73.5...@exodus/ethereum-api@8.73.6) (2026-05-11)
|
|
7
17
|
|
|
8
18
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/ethereum-api",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.74.0",
|
|
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",
|
|
@@ -67,5 +67,5 @@
|
|
|
67
67
|
"type": "git",
|
|
68
68
|
"url": "git+https://github.com/ExodusMovement/assets.git"
|
|
69
69
|
},
|
|
70
|
-
"gitHead": "
|
|
70
|
+
"gitHead": "f841266053c82af6c3aecb3196b9ae20f4d5340c"
|
|
71
71
|
}
|
package/src/create-asset.js
CHANGED
|
@@ -37,6 +37,7 @@ import { createFeeData } from './fee-data/index.js'
|
|
|
37
37
|
import { createGetBalanceForAddress } from './get-balance-for-address.js'
|
|
38
38
|
import { getBalancesFactory } from './get-balances.js'
|
|
39
39
|
import { getFeeFactory } from './get-fee.js'
|
|
40
|
+
import { moveFundsFactory } from './move-funds.js'
|
|
40
41
|
import { estimateL1DataFeeFactory, getL1GetFeeFactory } from './optimism-gas/index.js'
|
|
41
42
|
import { serverBasedFeeMonitorFactoryFactory } from './server-based-fee-monitor.js'
|
|
42
43
|
import { stakingApiFactory } from './staking/api/index.js'
|
|
@@ -208,6 +209,7 @@ export const createAssetFactory = ({
|
|
|
208
209
|
feesApi: true,
|
|
209
210
|
isMaxFeeAsset,
|
|
210
211
|
isTestnet,
|
|
212
|
+
moveFunds: true,
|
|
211
213
|
nfts,
|
|
212
214
|
noHistory: monitorType === 'no-history',
|
|
213
215
|
signWithSigner: true,
|
|
@@ -281,6 +283,13 @@ export const createAssetFactory = ({
|
|
|
281
283
|
|
|
282
284
|
const securityChecks = createSecurityChecks({ eip7702Supported })
|
|
283
285
|
|
|
286
|
+
const moveFunds = moveFundsFactory({
|
|
287
|
+
baseAssetName: asset.name,
|
|
288
|
+
assetClientInterface,
|
|
289
|
+
createTx,
|
|
290
|
+
server,
|
|
291
|
+
})
|
|
292
|
+
|
|
284
293
|
const api = {
|
|
285
294
|
addressHasHistory,
|
|
286
295
|
broadcastTx: (...args) => server.sendRawTransaction(...args),
|
|
@@ -311,6 +320,7 @@ export const createAssetFactory = ({
|
|
|
311
320
|
getSupportedPurposes: () => [44],
|
|
312
321
|
getTokens,
|
|
313
322
|
hasFeature: (feature) => !!features[feature], // @deprecated use api.features instead
|
|
323
|
+
moveFunds,
|
|
314
324
|
privateKeyEncodingDefinition: { encoding: 'hex' },
|
|
315
325
|
sendTx,
|
|
316
326
|
signTx: ({ unsignedTx, privateKey, signer }) =>
|
package/src/index.js
CHANGED
|
@@ -99,6 +99,8 @@ export { txSendFactory } from './tx-send/index.js'
|
|
|
99
99
|
|
|
100
100
|
export { createAssetFactory } from './create-asset.js'
|
|
101
101
|
|
|
102
|
+
export { moveFundsFactory } from './move-funds.js'
|
|
103
|
+
|
|
102
104
|
export { createContractBlackListCheck } from './create-asset-utils.js'
|
|
103
105
|
|
|
104
106
|
export {
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { signUnsignedTx } from '@exodus/ethereum-lib'
|
|
2
|
+
import { addHexPrefix, isValidPrivate, privateToAddress, toBuffer } from '@exodus/ethereumjs/util'
|
|
3
|
+
import assert from 'minimalistic-assert'
|
|
4
|
+
|
|
5
|
+
import { getNonce, getTokenBalanceFromNode } from './eth-like-util.js'
|
|
6
|
+
import { fetchGasLimit } from './gas-estimation.js'
|
|
7
|
+
|
|
8
|
+
export const moveFundsFactory = ({ baseAssetName, assetClientInterface, createTx, server }) => {
|
|
9
|
+
assert(baseAssetName, 'baseAssetName is required')
|
|
10
|
+
assert(assetClientInterface, 'assetClientInterface is required')
|
|
11
|
+
assert(createTx, 'createTx is required')
|
|
12
|
+
assert(server, 'server is required')
|
|
13
|
+
|
|
14
|
+
async function prepareSendFundsTx({
|
|
15
|
+
assetName,
|
|
16
|
+
input,
|
|
17
|
+
toAddress,
|
|
18
|
+
walletAccount,
|
|
19
|
+
MoveFundsError,
|
|
20
|
+
}) {
|
|
21
|
+
assert(assetName, 'assetName is required')
|
|
22
|
+
assert(input, 'input is required')
|
|
23
|
+
|
|
24
|
+
assert(walletAccount, 'walletAccount is required')
|
|
25
|
+
assert(MoveFundsError, 'MoveFundsError is required')
|
|
26
|
+
|
|
27
|
+
const assets = await assetClientInterface.getAssetsForNetwork({ baseAssetName })
|
|
28
|
+
const asset = assets[assetName]
|
|
29
|
+
assert(asset, 'asset is required')
|
|
30
|
+
|
|
31
|
+
assert(toAddress && asset.baseAsset.address.validate(toAddress), 'valid toAddress is required')
|
|
32
|
+
|
|
33
|
+
let privateKey
|
|
34
|
+
try {
|
|
35
|
+
const hexKey = addHexPrefix(input.trim())
|
|
36
|
+
if (typeof hexKey !== 'string' || hexKey.length !== 66) throw new Error('invalid length')
|
|
37
|
+
privateKey = toBuffer(hexKey)
|
|
38
|
+
if (!isValidPrivate(privateKey)) throw new Error('invalid key')
|
|
39
|
+
} catch {
|
|
40
|
+
throw new MoveFundsError('private-key-invalid')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const fromAddress = `0x${privateToAddress(privateKey).toString('hex')}`
|
|
44
|
+
|
|
45
|
+
if (fromAddress === toAddress.toLowerCase()) {
|
|
46
|
+
throw new MoveFundsError('private-key-own-key', { fromAddress })
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const isToken = asset.name !== baseAssetName
|
|
50
|
+
|
|
51
|
+
const [nonce, rawEthBalance] = await Promise.all([
|
|
52
|
+
getNonce({ asset: asset.baseAsset, address: fromAddress }),
|
|
53
|
+
server.getBalanceProxied(fromAddress),
|
|
54
|
+
])
|
|
55
|
+
|
|
56
|
+
const ethBalance = asset.baseAsset.currency.baseUnit(rawEthBalance)
|
|
57
|
+
|
|
58
|
+
let amount = ethBalance
|
|
59
|
+
if (isToken) {
|
|
60
|
+
const rawTokenBalance = await getTokenBalanceFromNode({ asset, address: fromAddress })
|
|
61
|
+
amount = asset.currency.baseUnit(rawTokenBalance)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (amount.isZero) {
|
|
65
|
+
throw new MoveFundsError('balance-zero', { fromAddress })
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const feeData = await assetClientInterface.getFeeData({ assetName })
|
|
69
|
+
|
|
70
|
+
const gasLimit = await fetchGasLimit({
|
|
71
|
+
asset,
|
|
72
|
+
feeData,
|
|
73
|
+
fromAddress,
|
|
74
|
+
toAddress,
|
|
75
|
+
amount,
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
const fee = feeData.gasPrice.mul(gasLimit)
|
|
79
|
+
|
|
80
|
+
const afterFee = ethBalance.sub(fee)
|
|
81
|
+
if (isToken) {
|
|
82
|
+
if (afterFee.isNegative) {
|
|
83
|
+
throw new MoveFundsError('token-fee-insufficient', { fromAddress })
|
|
84
|
+
}
|
|
85
|
+
} else {
|
|
86
|
+
if (!afterFee.isPositive) {
|
|
87
|
+
throw new MoveFundsError('balance-negative', { fromAddress })
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
amount = amount.sub(fee)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const { unsignedTx } = await createTx({
|
|
94
|
+
asset,
|
|
95
|
+
walletAccount,
|
|
96
|
+
fromAddress,
|
|
97
|
+
address: toAddress,
|
|
98
|
+
amount,
|
|
99
|
+
nonce,
|
|
100
|
+
gasLimit,
|
|
101
|
+
gasPrice: feeData.gasPrice,
|
|
102
|
+
customFee: feeData.gasPrice,
|
|
103
|
+
isSendAll: true,
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
return { fromAddress, toAddress, amount, fee, privateKey, unsignedTx }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const sendFunds = async ({ privateKey, unsignedTx }) => {
|
|
110
|
+
assert(privateKey, 'privateKey is required')
|
|
111
|
+
assert(unsignedTx, 'unsignedTx is required')
|
|
112
|
+
|
|
113
|
+
const { rawTx } = await signUnsignedTx(unsignedTx, privateKey)
|
|
114
|
+
const txId = await server.sendRawTransaction(rawTx.toString('hex'))
|
|
115
|
+
|
|
116
|
+
return { txId }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return { prepareSendFundsTx, sendFunds }
|
|
120
|
+
}
|
|
@@ -15,7 +15,7 @@ export const resolveNonce = async ({
|
|
|
15
15
|
asset,
|
|
16
16
|
forceFromNode,
|
|
17
17
|
fromAddress,
|
|
18
|
-
txLog
|
|
18
|
+
txLog, // TxSet expected. Array only for testing purposes.
|
|
19
19
|
accountState,
|
|
20
20
|
tag = BLOCK_TAG_LATEST,
|
|
21
21
|
useAbsoluteNonce,
|
|
@@ -51,13 +51,27 @@ const getLatestTxWithNonceChange = ({ reversedTxLog }) => {
|
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Computes the next nonce from the given `txLog`.
|
|
56
|
+
*
|
|
57
|
+
* @param {Object} options
|
|
58
|
+
* @param {import('@exodus/models').TxSet} options.txLog - A `TxSet` from
|
|
59
|
+
* `@exodus/models`, NOT a plain `Array`.
|
|
60
|
+
* @param {boolean} [options.useAbsoluteNonce]
|
|
61
|
+
* @param {string} [options.tag]
|
|
62
|
+
* @returns {number}
|
|
63
|
+
*/
|
|
54
64
|
const getNonceFromTxLog = ({ txLog, useAbsoluteNonce, tag }) => {
|
|
65
|
+
if (!txLog || txLog.size === 0) return 0
|
|
66
|
+
|
|
55
67
|
let absoluteNonce = 0
|
|
56
68
|
|
|
57
69
|
if (useAbsoluteNonce) {
|
|
58
|
-
// NOTE:
|
|
59
|
-
//
|
|
60
|
-
|
|
70
|
+
// NOTE: `TxSet#reverse()` returns a one-shot iterable yielding txs
|
|
71
|
+
// in reverse order — it is NOT an in-place reversal like
|
|
72
|
+
// `Array.prototype.reverse()`, so the caller's `txLog` is
|
|
73
|
+
// left untouched.
|
|
74
|
+
const reversedTxLog = txLog.reverse()
|
|
61
75
|
const maybeLatestTxWithNonceChange = getLatestTxWithNonceChange({ reversedTxLog })
|
|
62
76
|
|
|
63
77
|
if (maybeLatestTxWithNonceChange) {
|
|
@@ -70,6 +84,7 @@ const getNonceFromTxLog = ({ txLog, useAbsoluteNonce, tag }) => {
|
|
|
70
84
|
}
|
|
71
85
|
}
|
|
72
86
|
|
|
87
|
+
// TODO: can't we iterate in reverse up to the first (few) nonce(s)?
|
|
73
88
|
const nonceFromLog = [...txLog]
|
|
74
89
|
.filter((tx) => tx.sent && !tx.dropped && tx.data.nonce != null)
|
|
75
90
|
// NOTE: If we're only considering the `'latest'` block `tag`,
|