@exodus/ethereum-api 8.73.6 → 8.74.1
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 +19 -0
- package/package.json +3 -3
- package/src/create-asset.js +10 -0
- package/src/error-wrapper.js +1 -0
- package/src/exodus-eth-server/ws-gateway.js +0 -1
- package/src/exodus-eth-server/ws.js +0 -1
- package/src/index.js +3 -0
- package/src/move-funds.js +120 -0
- package/src/staking/matic/index.js +1 -0
- package/src/staking/matic/matic-staking-utils.js +12 -1
- package/src/tx-send/nonce-utils.js +19 -4
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,25 @@
|
|
|
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.1](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.74.0...@exodus/ethereum-api@8.74.1) (2026-05-12)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
|
|
11
|
+
* validate polygon unstake state before gas estimation ([#8002](https://github.com/ExodusMovement/assets/issues/8002)) ([f63a244](https://github.com/ExodusMovement/assets/commit/f63a2441868c6226fe14085c0f7d9587131e69e0))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
## [8.74.0](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.73.6...@exodus/ethereum-api@8.74.0) (2026-05-12)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Features
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
* feat: add MoveFunds API for EVM assets (#7868)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
6
25
|
## [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
26
|
|
|
8
27
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/ethereum-api",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.74.1",
|
|
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.1",
|
|
33
33
|
"@exodus/ethereum-meta": "^2.9.1",
|
|
34
34
|
"@exodus/ethereumholesky-meta": "^2.0.5",
|
|
35
35
|
"@exodus/ethereumjs": "^1.11.0",
|
|
@@ -67,5 +67,5 @@
|
|
|
67
67
|
"type": "git",
|
|
68
68
|
"url": "git+https://github.com/ExodusMovement/assets.git"
|
|
69
69
|
},
|
|
70
|
-
"gitHead": "
|
|
70
|
+
"gitHead": "7f184588091acba3b358e4af13783ee1e2bab4ba"
|
|
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/error-wrapper.js
CHANGED
|
@@ -5,6 +5,7 @@ export const EVM_ERROR_TYPES = {
|
|
|
5
5
|
NODE_STATE_READ: safeString`NODE_STATE_READ`, // RPC-level read operations
|
|
6
6
|
CONTRACT_CALL: safeString`CONTRACT_CALL`, // Smart contract calls (eth_call, estimateGas)
|
|
7
7
|
BROADCAST: safeString`BROADCAST`, // Transaction broadcast errors (includes reverts)
|
|
8
|
+
PREFLIGHT_VALIDATION: safeString`PREFLIGHT_VALIDATION`, // Logic/state validation before contract gasEstimation & broadcast
|
|
8
9
|
}
|
|
9
10
|
|
|
10
11
|
// Operation-specific reasons
|
package/src/index.js
CHANGED
|
@@ -57,6 +57,7 @@ export {
|
|
|
57
57
|
isPolygonUndelegate,
|
|
58
58
|
isPolygonReward,
|
|
59
59
|
isPolygonClaimUndelegate,
|
|
60
|
+
isPendingPolygonUndelegateTxInEthereumTxLog,
|
|
60
61
|
} from './staking/index.js'
|
|
61
62
|
|
|
62
63
|
export { fetchTxPreview, maybeRemoveDuplicates, retrieveSideEffects } from './simulate-tx/index.js'
|
|
@@ -99,6 +100,8 @@ export { txSendFactory } from './tx-send/index.js'
|
|
|
99
100
|
|
|
100
101
|
export { createAssetFactory } from './create-asset.js'
|
|
101
102
|
|
|
103
|
+
export { moveFundsFactory } from './move-funds.js'
|
|
104
|
+
|
|
102
105
|
export { createContractBlackListCheck } from './create-asset-utils.js'
|
|
103
106
|
|
|
104
107
|
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
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import NumberUnit from '@exodus/currency'
|
|
2
|
-
import { parseUnsignedTx } from '@exodus/ethereum-lib'
|
|
2
|
+
import { getMethodIdFromEthTx, isPendingTxInLog, parseUnsignedTx } from '@exodus/ethereum-lib'
|
|
3
3
|
import assetsList, { asset as ethereum } from '@exodus/ethereum-meta'
|
|
4
4
|
import { Tx } from '@exodus/models'
|
|
5
5
|
import assert from 'minimalistic-assert'
|
|
@@ -52,6 +52,17 @@ const isPolygonUndelegateTxInEthreumTxLog = (tx) =>
|
|
|
52
52
|
export const isPolygonUndelegateTxInEthereumTxLog = (tx) =>
|
|
53
53
|
isPolygonStakingContractTxInEthereumTxLog(tx) && isPolygonUndelegateTxInEthreumTxLog(tx)
|
|
54
54
|
|
|
55
|
+
// Pending variant intended for client-side selectors (e.g. "show Unstaking… as
|
|
56
|
+
// soon as the tx hits the log"). Composes the shared tx-log lifecycle and
|
|
57
|
+
// method-ID helpers so this and the asset-side preflight in
|
|
58
|
+
// @exodus/ethereum-plugin/staking/polygon/staking-utils agree on what "pending"
|
|
59
|
+
// means from a single definition.
|
|
60
|
+
export const isPendingPolygonUndelegateTxInEthereumTxLog = (tx) =>
|
|
61
|
+
!!tx &&
|
|
62
|
+
isPendingTxInLog(tx) &&
|
|
63
|
+
tx.to?.toLowerCase() === MaticStakingApi.EVERSTAKE_VALIDATOR_CONTRACT_ADDR &&
|
|
64
|
+
getMethodIdFromEthTx(tx, UNDELEGATE) === UNDELEGATE
|
|
65
|
+
|
|
55
66
|
export const createUndelegateTx = (tx) => {
|
|
56
67
|
return Tx.fromJSON({
|
|
57
68
|
...tx,
|
|
@@ -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`,
|