@exodus/ethereum-api 10.0.0 → 10.1.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 +9 -0
- package/package.json +2 -2
- package/src/create-asset-utils.js +6 -6
- package/src/create-asset.js +32 -10
- package/src/create-token-factory.js +18 -7
- package/src/index.js +15 -0
- package/src/staking/api/index.js +2 -1
- package/src/staking/ethereum/api.js +2 -3
- package/src/staking/ethereum/deps.js +3 -0
- package/src/staking/ethereum/min-amount.js +1 -0
- package/src/staking/ethereum/service.js +148 -123
- package/src/staking/ethereum/staking-adapter.js +382 -0
- package/src/staking/evm/compose-staking-api.js +42 -0
- package/src/staking/evm/create-evm-staking-api.js +269 -0
- package/src/staking/evm/staking-errors.js +37 -0
- package/src/staking/evm/staking-txs.js +107 -0
- package/src/staking/matic/index.js +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,15 @@
|
|
|
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
|
+
## [10.1.0](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@10.0.0...@exodus/ethereum-api@10.1.0) (2026-10-05)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* **ethereum:** add evm staking api core with Everstake ETH and polygon adapters ([#8775](https://github.com/ExodusMovement/assets/issues/8775)) ([2d90106](https://github.com/ExodusMovement/assets/commit/2d901069989be82eb56aa253d5171e4f6af97356))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
6
15
|
## [10.0.0](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@9.0.3...@exodus/ethereum-api@10.0.0) (2026-10-05)
|
|
7
16
|
|
|
8
17
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/ethereum-api",
|
|
3
|
-
"version": "10.
|
|
3
|
+
"version": "10.1.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",
|
|
@@ -66,5 +66,5 @@
|
|
|
66
66
|
"type": "git",
|
|
67
67
|
"url": "git+https://github.com/ExodusMovement/assets.git"
|
|
68
68
|
},
|
|
69
|
-
"gitHead": "
|
|
69
|
+
"gitHead": "f5c81fc2962e5d2836794149870fb9aaf094ddc9"
|
|
70
70
|
}
|
|
@@ -176,7 +176,7 @@ export const createTransactionPrivacyResult = ({
|
|
|
176
176
|
return { nonce, txId }
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
-
const sendPrivateBundle = async ({ unsignedTxBundle }) => {
|
|
179
|
+
const sendPrivateBundle = async ({ unsignedTxBundle, onBeforeSign }) => {
|
|
180
180
|
assert(
|
|
181
181
|
Array.isArray(unsignedTxBundle) && unsignedTxBundle.length > 0,
|
|
182
182
|
'expected non-empty unsignedTxBundle'
|
|
@@ -188,11 +188,11 @@ export const createTransactionPrivacyResult = ({
|
|
|
188
188
|
await assertNonZeroTipGasPrice({ asset, assetClientInterface, unsignedTx })
|
|
189
189
|
}
|
|
190
190
|
|
|
191
|
-
const signTxResults =
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
)
|
|
195
|
-
|
|
191
|
+
const signTxResults = []
|
|
192
|
+
for (const [index, { asset, unsignedTx, walletAccount }] of unsignedTxBundle.entries()) {
|
|
193
|
+
onBeforeSign?.({ index, total: unsignedTxBundle.length })
|
|
194
|
+
signTxResults.push(await signTx({ assetClientInterface, asset, unsignedTx, walletAccount }))
|
|
195
|
+
}
|
|
196
196
|
|
|
197
197
|
const maybeBundleResult = await broadcastPrivateBundle({
|
|
198
198
|
txs: signTxResults.map(({ rawTx }) => rawTx.toString('hex')),
|
package/src/create-asset.js
CHANGED
|
@@ -49,6 +49,11 @@ import { estimateL1DataFeeFactory, getL1GetFeeFactory } from './optimism-gas/ind
|
|
|
49
49
|
import { createSendValidations } from './send-validations.js'
|
|
50
50
|
import { serverBasedFeeMonitorFactoryFactory } from './server-based-fee-monitor.js'
|
|
51
51
|
import { stakingApiFactory } from './staking/api/index.js'
|
|
52
|
+
import {
|
|
53
|
+
applyStakingApiOverrides,
|
|
54
|
+
composeStakingApi,
|
|
55
|
+
isStakingApiEnabled,
|
|
56
|
+
} from './staking/evm/compose-staking-api.js'
|
|
52
57
|
import { createTxFactory } from './tx-create.js'
|
|
53
58
|
import { txSendFactory } from './tx-send/index.js'
|
|
54
59
|
import { createWeb3API } from './web3/index.js'
|
|
@@ -135,8 +140,14 @@ export const createAssetFactory = ({
|
|
|
135
140
|
supportsTransactionPrivacy,
|
|
136
141
|
transactionAssessment,
|
|
137
142
|
multicall3Address,
|
|
143
|
+
stakingApiEnabled: stakingApiOverrides,
|
|
138
144
|
} = configWithOverrides
|
|
139
145
|
|
|
146
|
+
const assetStakingDependencies = applyStakingApiOverrides({
|
|
147
|
+
stakingDependencies,
|
|
148
|
+
overrides: stakingApiOverrides,
|
|
149
|
+
})
|
|
150
|
+
|
|
140
151
|
const asset = assets[base.name]
|
|
141
152
|
|
|
142
153
|
const { monitorType, serverUrl, monitorInterval } = resolveMonitorSettings({
|
|
@@ -206,7 +217,7 @@ export const createAssetFactory = ({
|
|
|
206
217
|
assetClientInterface,
|
|
207
218
|
server,
|
|
208
219
|
stakingConfiguration,
|
|
209
|
-
stakingDependencies,
|
|
220
|
+
stakingDependencies: assetStakingDependencies,
|
|
210
221
|
createRpcRequestAccumulator,
|
|
211
222
|
},
|
|
212
223
|
assets
|
|
@@ -236,6 +247,23 @@ export const createAssetFactory = ({
|
|
|
236
247
|
supportsTransactionPrivacy,
|
|
237
248
|
})
|
|
238
249
|
|
|
250
|
+
const stakingApiEnabled =
|
|
251
|
+
supportsStaking && isStakingApiEnabled(assetStakingDependencies[asset.name])
|
|
252
|
+
|
|
253
|
+
const createAssetStakingApi = () =>
|
|
254
|
+
composeStakingApi({
|
|
255
|
+
assetClientInterface,
|
|
256
|
+
stakingDependencies: assetStakingDependencies[asset.name],
|
|
257
|
+
legacyStakingApi: stakingApiFactory({
|
|
258
|
+
assetName: asset.name,
|
|
259
|
+
currency: asset.currency,
|
|
260
|
+
assetClientInterface,
|
|
261
|
+
server,
|
|
262
|
+
stakingConfiguration: stakingConfiguration[asset.name],
|
|
263
|
+
stakingDependencies: assetStakingDependencies[asset.name],
|
|
264
|
+
}),
|
|
265
|
+
})
|
|
266
|
+
|
|
239
267
|
const features = {
|
|
240
268
|
accountState: true,
|
|
241
269
|
customTokens,
|
|
@@ -254,6 +282,7 @@ export const createAssetFactory = ({
|
|
|
254
282
|
supportsCustomFees,
|
|
255
283
|
supportsTransactionPrivacy: Boolean(supportsTransactionPrivacy),
|
|
256
284
|
...(supportsStaking && { staking: {} }),
|
|
285
|
+
...(stakingApiEnabled && { stakingApi: true }),
|
|
257
286
|
...(delisted && { delisted }),
|
|
258
287
|
}
|
|
259
288
|
|
|
@@ -393,15 +422,8 @@ export const createAssetFactory = ({
|
|
|
393
422
|
signer ? signMessageWithSigner({ message, signer }) : signMessage({ privateKey, message }),
|
|
394
423
|
...(checkTx && { checkTx }),
|
|
395
424
|
...(supportsStaking &&
|
|
396
|
-
|
|
397
|
-
staking:
|
|
398
|
-
assetName: asset.name,
|
|
399
|
-
currency: asset.currency,
|
|
400
|
-
assetClientInterface,
|
|
401
|
-
server,
|
|
402
|
-
stakingConfiguration: stakingConfiguration[asset.name],
|
|
403
|
-
stakingDependencies: stakingDependencies[asset.name],
|
|
404
|
-
}),
|
|
425
|
+
assetStakingDependencies[asset.name] && {
|
|
426
|
+
staking: createAssetStakingApi(),
|
|
405
427
|
}),
|
|
406
428
|
validateAssetId: address.validate,
|
|
407
429
|
web3,
|
|
@@ -3,6 +3,7 @@ import { createContract } from '@exodus/ethereum-lib'
|
|
|
3
3
|
import assert from 'minimalistic-assert'
|
|
4
4
|
|
|
5
5
|
import { stakingApiFactory } from './staking/api/index.js'
|
|
6
|
+
import { composeStakingApi, isStakingApiEnabled } from './staking/evm/compose-staking-api.js'
|
|
6
7
|
import {
|
|
7
8
|
isPolygonClaimUndelegate,
|
|
8
9
|
isPolygonDelegate,
|
|
@@ -48,7 +49,13 @@ const getCreateBaseToken =
|
|
|
48
49
|
}) =>
|
|
49
50
|
({ name, contract, features, ...tokenDef }) => {
|
|
50
51
|
assert(getBalances, 'getBalances is required')
|
|
51
|
-
const tokenSpecificFeatures =
|
|
52
|
+
const tokenSpecificFeatures =
|
|
53
|
+
name === 'polygon'
|
|
54
|
+
? {
|
|
55
|
+
staking: {},
|
|
56
|
+
...(isStakingApiEnabled(stakingDependencies[name]) && { stakingApi: true }),
|
|
57
|
+
}
|
|
58
|
+
: {}
|
|
52
59
|
|
|
53
60
|
const tokenFeatures = { ...defaultTokenFeatures, ...features, ...tokenSpecificFeatures }
|
|
54
61
|
const baseApi = {
|
|
@@ -72,14 +79,18 @@ const getCreateBaseToken =
|
|
|
72
79
|
}
|
|
73
80
|
|
|
74
81
|
if (name === 'polygon' && stakingDependencies[name]) {
|
|
75
|
-
baseApi.staking =
|
|
76
|
-
assetName: tokenAsset.name,
|
|
77
|
-
currency: tokenAsset.currency,
|
|
82
|
+
baseApi.staking = composeStakingApi({
|
|
78
83
|
assetClientInterface,
|
|
79
|
-
server,
|
|
80
|
-
stakingConfiguration: stakingConfiguration[name],
|
|
81
84
|
stakingDependencies: stakingDependencies[name],
|
|
82
|
-
|
|
85
|
+
legacyStakingApi: stakingApiFactory({
|
|
86
|
+
assetName: tokenAsset.name,
|
|
87
|
+
currency: tokenAsset.currency,
|
|
88
|
+
assetClientInterface,
|
|
89
|
+
server,
|
|
90
|
+
stakingConfiguration: stakingConfiguration[name],
|
|
91
|
+
stakingDependencies: stakingDependencies[name],
|
|
92
|
+
createRpcRequestAccumulator,
|
|
93
|
+
}),
|
|
83
94
|
})
|
|
84
95
|
}
|
|
85
96
|
|
package/src/index.js
CHANGED
|
@@ -43,6 +43,20 @@ export { createEncodeMultisigContract } from './multisig-address.js'
|
|
|
43
43
|
export { FantomStaking, stakingProviderClientFactory } from './staking/index.js'
|
|
44
44
|
export { EthLikeRpcRequestAccumulator } from './multicall3/index.js'
|
|
45
45
|
export { createCachedStakingRead } from './staking/cached-read.js'
|
|
46
|
+
export { createStakingTxs, sendStakingTxs, STAKING_TX_MODE } from './staking/evm/staking-txs.js'
|
|
47
|
+
export { createEvmStakingApi } from './staking/evm/create-evm-staking-api.js'
|
|
48
|
+
export {
|
|
49
|
+
createRejectionError as createStakingRejectionError,
|
|
50
|
+
createSubmitError as createStakingSubmitError,
|
|
51
|
+
fail as failStakingCheck,
|
|
52
|
+
pass as passStakingCheck,
|
|
53
|
+
sumOperationFees as sumStakingOperationFees,
|
|
54
|
+
warnIfBelow as warnIfStakingBalanceBelow,
|
|
55
|
+
} from './staking/evm/staking-errors.js'
|
|
56
|
+
export {
|
|
57
|
+
createEverstakeStakingAdapter,
|
|
58
|
+
ETH_STAKING_GAS_LIMITS,
|
|
59
|
+
} from './staking/ethereum/staking-adapter.js'
|
|
46
60
|
export { ethereumStakingDeps } from './staking/ethereum/deps.js'
|
|
47
61
|
export { getEthereumStakingInfo, createEthereumStakingService } from './staking/ethereum/service.js'
|
|
48
62
|
export { EthereumStaking } from './staking/ethereum/api.js'
|
|
@@ -65,6 +79,7 @@ export {
|
|
|
65
79
|
isPolygonReward,
|
|
66
80
|
isPolygonClaimUndelegate,
|
|
67
81
|
isPendingPolygonUndelegateTxInEthereumTxLog,
|
|
82
|
+
isPolygonUndelegateTxInEthereumTxLog,
|
|
68
83
|
} from './staking/matic/matic-staking-utils.js'
|
|
69
84
|
|
|
70
85
|
export { fetchTxPreview } from './simulate-tx/fetch-tx-preview.js'
|
package/src/staking/api/index.js
CHANGED
|
@@ -36,8 +36,9 @@ export const stakingApiFactory = ({
|
|
|
36
36
|
|
|
37
37
|
return {
|
|
38
38
|
...stakingService,
|
|
39
|
+
// @deprecated legacy staking api, remove once evm staking unification is done
|
|
39
40
|
isStaking: async ({ isDelegating }) => isDelegating,
|
|
40
41
|
isUnstaking: async ({ isUndelegateInProgress }) => isUndelegateInProgress,
|
|
41
|
-
isUnstaked: async ({
|
|
42
|
+
isUnstaked: async ({ canClaimUndelegatedBalance }) => canClaimUndelegatedBalance,
|
|
42
43
|
}
|
|
43
44
|
}
|
|
@@ -7,9 +7,8 @@ import assert from 'minimalistic-assert'
|
|
|
7
7
|
import { EVM_ERROR_REASONS, withErrorReason } from '../../error-wrapper.js'
|
|
8
8
|
import { EthLikeRpcRequestAccumulator } from '../../multicall3/index.js'
|
|
9
9
|
import { createCachedStakingRead } from '../cached-read.js'
|
|
10
|
+
import { ETH_STAKING_MIN_AMOUNT } from './min-amount.js'
|
|
10
11
|
|
|
11
|
-
// TODO: Shouldn't this be a function of `ethereumStakingState.minDelegateAmount`?
|
|
12
|
-
const MIN_AMOUNT = '0.1'
|
|
13
12
|
const RETRY_DELAYS = ['10s']
|
|
14
13
|
|
|
15
14
|
const EVERSTAKE_API_URL = 'https://eth-clarity.a.exodus.io/everstake-rewards'
|
|
@@ -46,7 +45,7 @@ export class EthereumStaking {
|
|
|
46
45
|
return txInputHex.toLowerCase().startsWith(EthereumStaking.METHODS_IDS.DELEGATE)
|
|
47
46
|
}
|
|
48
47
|
|
|
49
|
-
constructor(assetName, currency, minAmount =
|
|
48
|
+
constructor(assetName, currency, minAmount = ETH_STAKING_MIN_AMOUNT, server) {
|
|
50
49
|
this.asset = { name: assetName, currency }
|
|
51
50
|
const accountingAddress =
|
|
52
51
|
EthereumStaking.addresses[assetName].EVERSTAKE_ADDRESS_CONTRACT_ACCOUNTING
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { EthereumStaking } from './api.js'
|
|
2
2
|
import { createEthereumStakingService, getEthereumStakingInfo } from './service.js'
|
|
3
|
+
import { createEverstakeStakingAdapter } from './staking-adapter.js'
|
|
3
4
|
|
|
4
5
|
/** Staking server factory for ethereum (used by stakingApiFactory). */
|
|
5
6
|
export const stakingServerFactory = ({ assetName, currency, server, minAmount }) =>
|
|
@@ -24,6 +25,7 @@ export const stakingServiceFactory = ({ assetClientInterface, server, stakingSer
|
|
|
24
25
|
useQueueCache,
|
|
25
26
|
background,
|
|
26
27
|
}),
|
|
28
|
+
// @deprecated legacy staking api, remove once evm staking unification is done
|
|
27
29
|
stake: service.delegate,
|
|
28
30
|
unstake: service.undelegate,
|
|
29
31
|
claimUnstaked: service.claimUndelegatedBalance,
|
|
@@ -34,4 +36,5 @@ export const stakingServiceFactory = ({ assetClientInterface, server, stakingSer
|
|
|
34
36
|
export const ethereumStakingDeps = {
|
|
35
37
|
stakingServerFactory,
|
|
36
38
|
stakingServiceFactory,
|
|
39
|
+
createStakingAdapter: ({ service }) => createEverstakeStakingAdapter({ service }),
|
|
37
40
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const ETH_STAKING_MIN_AMOUNT = '0.1'
|
|
@@ -66,11 +66,12 @@ import NumberUnit from '@exodus/currency'
|
|
|
66
66
|
import assert from 'minimalistic-assert'
|
|
67
67
|
|
|
68
68
|
import { estimateGasLimit, scaleGasLimitEstimate } from '../../gas-estimation.js'
|
|
69
|
-
import {
|
|
69
|
+
import { createStakingTxs, sendStakingTxs, STAKING_TX_MODE } from '../evm/staking-txs.js'
|
|
70
70
|
import { stakingProviderClientFactory } from '../staking-provider-client.js'
|
|
71
71
|
import { amountToCurrency, DISABLE_BALANCE_CHECKS, resolveFeeData } from '../utils/index.js'
|
|
72
72
|
import { EthereumStaking } from './api.js'
|
|
73
73
|
import { getCachedEverstakeValidatorsQueue } from './everstake.js'
|
|
74
|
+
import { ETH_STAKING_MIN_AMOUNT } from './min-amount.js'
|
|
74
75
|
import { simulateUndelegateTransactions } from './staking-utils.js'
|
|
75
76
|
|
|
76
77
|
const WETH9_ADDRESS = '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'
|
|
@@ -174,7 +175,6 @@ export function createEthereumStakingService({
|
|
|
174
175
|
asset: deprectedArg, // @deprecated use `assetName` instead
|
|
175
176
|
assetName,
|
|
176
177
|
assetClientInterface,
|
|
177
|
-
createWatchTx = defaultCreateWatch,
|
|
178
178
|
stakingProvider = stakingProviderClientFactory(),
|
|
179
179
|
}) {
|
|
180
180
|
if (deprectedArg && !assetName) {
|
|
@@ -202,7 +202,21 @@ export function createEthereumStakingService({
|
|
|
202
202
|
return { delegatorAddress, feeData }
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
-
async
|
|
205
|
+
const createPlanTxs = async ({ walletAccount, plan }) => {
|
|
206
|
+
const createdTxs = await createStakingTxs({
|
|
207
|
+
asset: plan.asset,
|
|
208
|
+
walletAccount,
|
|
209
|
+
fromAddress: plan.delegatorAddress,
|
|
210
|
+
steps: plan.steps,
|
|
211
|
+
mode: plan.mode,
|
|
212
|
+
gasPrice: plan.gasPrice,
|
|
213
|
+
tipGasPrice: plan.tipGasPrice,
|
|
214
|
+
feeData: plan.feeData,
|
|
215
|
+
})
|
|
216
|
+
return createdTxs.map(({ unsignedTx }) => unsignedTx)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async function planDelegate({ walletAccount, amount, feeData, isDelegateAll }) {
|
|
206
220
|
const asset = await getAsset(assetName)
|
|
207
221
|
const staking = getStakingApi(asset)
|
|
208
222
|
amount = amountToCurrency({ asset, amount })
|
|
@@ -228,24 +242,40 @@ export function createEthereumStakingService({
|
|
|
228
242
|
|
|
229
243
|
// If we are delegating all, we need to subtract the estimated ETH cost from the amount
|
|
230
244
|
// Better to do it here than from the mobile app where fees can change before the transaction is sent
|
|
231
|
-
if (isDelegateAll)
|
|
232
|
-
const minAmount = getMinAmount(asset)
|
|
233
|
-
amount = amount.sub(fee)
|
|
234
|
-
// Re-validate that the amount after fee subtraction is still above the minimum
|
|
235
|
-
assert(amount.gte(minAmount), `Min Amount ${minAmount}`)
|
|
236
|
-
}
|
|
245
|
+
if (isDelegateAll) amount = amount.sub(fee)
|
|
237
246
|
|
|
238
|
-
|
|
247
|
+
return {
|
|
239
248
|
asset,
|
|
240
|
-
|
|
241
|
-
to,
|
|
249
|
+
delegatorAddress,
|
|
242
250
|
amount,
|
|
243
|
-
|
|
251
|
+
fee,
|
|
252
|
+
feeData,
|
|
244
253
|
gasPrice,
|
|
245
|
-
gasLimit,
|
|
246
254
|
tipGasPrice,
|
|
247
|
-
|
|
248
|
-
|
|
255
|
+
steps: [{ to, txInput: data, amount, gasLimit }],
|
|
256
|
+
mode: STAKING_TX_MODE.single,
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async function buildDelegate(params) {
|
|
261
|
+
const plan = await planDelegate(params)
|
|
262
|
+
if (params.isDelegateAll) {
|
|
263
|
+
const minAmount = getMinAmount(plan.asset)
|
|
264
|
+
// Re-validate that the amount after fee subtraction is still above the minimum
|
|
265
|
+
assert(plan.amount.gte(minAmount), `Min Amount ${minAmount}`)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const unsignedTxs = await createPlanTxs({ walletAccount: params.walletAccount, plan })
|
|
269
|
+
return { ...plan, unsignedTxs }
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async function delegate({ walletAccount, amount, feeData, isDelegateAll }) {
|
|
273
|
+
const plan = await buildDelegate({ walletAccount, amount, feeData, isDelegateAll })
|
|
274
|
+
const { asset, delegatorAddress, unsignedTxs, mode } = plan
|
|
275
|
+
amount = plan.amount
|
|
276
|
+
const {
|
|
277
|
+
txIds: [txId],
|
|
278
|
+
} = await sendStakingTxs({ asset, walletAccount, unsignedTxs, mode })
|
|
249
279
|
|
|
250
280
|
// Goerli is not supported
|
|
251
281
|
if (assetName === 'ethereum') {
|
|
@@ -276,11 +306,6 @@ export function createEthereumStakingService({
|
|
|
276
306
|
])
|
|
277
307
|
const delegatorAddress = address.toLowerCase()
|
|
278
308
|
const pendingAmount = await staking.pendingBalanceOf(delegatorAddress)
|
|
279
|
-
const baseNonce = await asset.baseAsset.getNonce({
|
|
280
|
-
asset,
|
|
281
|
-
fromAddress: delegatorAddress,
|
|
282
|
-
walletAccount,
|
|
283
|
-
})
|
|
284
309
|
|
|
285
310
|
return {
|
|
286
311
|
asset,
|
|
@@ -290,7 +315,6 @@ export function createEthereumStakingService({
|
|
|
290
315
|
feeData: resolvedFeeData,
|
|
291
316
|
delegatorAddress,
|
|
292
317
|
pendingAmount,
|
|
293
|
-
baseNonce,
|
|
294
318
|
}
|
|
295
319
|
}
|
|
296
320
|
|
|
@@ -340,12 +364,7 @@ export function createEthereumStakingService({
|
|
|
340
364
|
return pendingFee.add(undelegateFee)
|
|
341
365
|
}
|
|
342
366
|
|
|
343
|
-
|
|
344
|
-
// the request using `pendingAmount` only. It does not pre-validate that the
|
|
345
|
-
// validator-side remainder is actually available in active staked balance,
|
|
346
|
-
// so an oversized request can still fail later when building/sending the
|
|
347
|
-
// validator unstake step.
|
|
348
|
-
async function undelegate({ walletAccount, amount, feeData, revertOnSimulationError = true }) {
|
|
367
|
+
async function planUndelegate({ walletAccount, amount, feeData }) {
|
|
349
368
|
const {
|
|
350
369
|
asset,
|
|
351
370
|
staking,
|
|
@@ -354,7 +373,6 @@ export function createEthereumStakingService({
|
|
|
354
373
|
feeData: resolvedFeeData,
|
|
355
374
|
delegatorAddress,
|
|
356
375
|
pendingAmount,
|
|
357
|
-
baseNonce,
|
|
358
376
|
} = await prepareUndelegate({ walletAccount, amount, feeData })
|
|
359
377
|
|
|
360
378
|
if (!requestedAmount.isPositive) {
|
|
@@ -423,48 +441,68 @@ export function createEthereumStakingService({
|
|
|
423
441
|
tipGasPrice = tipGasPrice || undelegateResult.feeInfo.tipGasPrice
|
|
424
442
|
}
|
|
425
443
|
|
|
426
|
-
|
|
427
|
-
const
|
|
444
|
+
const plannedSteps = Object.values(txSteps).filter((txStep) => txStep.plan)
|
|
445
|
+
const fee = [pendingResult, undelegateResult]
|
|
446
|
+
.filter(Boolean)
|
|
447
|
+
.reduce((total, { feeInfo }) => total.add(feeInfo.fee), asset.currency.ZERO)
|
|
448
|
+
|
|
449
|
+
return {
|
|
428
450
|
asset,
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
451
|
+
delegatorAddress,
|
|
452
|
+
amount: requestedAmount,
|
|
453
|
+
fee,
|
|
454
|
+
feeData: resolvedFeeData,
|
|
433
455
|
gasPrice,
|
|
456
|
+
tipGasPrice,
|
|
457
|
+
txSteps,
|
|
458
|
+
steps: plannedSteps.map((txStep) => ({
|
|
459
|
+
to: txStep.plan.to,
|
|
460
|
+
txInput: txStep.plan.txData,
|
|
461
|
+
gasLimit: txStep.gasLimit,
|
|
462
|
+
})),
|
|
463
|
+
mode: STAKING_TX_MODE.bundle,
|
|
434
464
|
}
|
|
465
|
+
}
|
|
435
466
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
txInput: txStep.plan.txData,
|
|
444
|
-
gasLimit: txStep.gasLimit,
|
|
445
|
-
nonce: baseNonce + index,
|
|
446
|
-
})
|
|
447
|
-
return [key, result.unsignedTx]
|
|
448
|
-
})
|
|
467
|
+
async function simulateUndelegate({ plan, unsignedTxs, revertOnSimulationError = true }) {
|
|
468
|
+
const plannedSteps = Object.values(plan.txSteps).filter((txStep) => txStep.plan)
|
|
469
|
+
const txSteps = Object.fromEntries(
|
|
470
|
+
Object.entries(plan.txSteps).map(([key, txStep]) => [
|
|
471
|
+
key,
|
|
472
|
+
{ ...txStep, unsignedTx: unsignedTxs[plannedSteps.indexOf(txStep)] ?? null },
|
|
473
|
+
])
|
|
449
474
|
)
|
|
450
475
|
|
|
451
|
-
for (const [key, unsignedTx] of createdTxEntries) {
|
|
452
|
-
txSteps[key].unsignedTx = unsignedTx
|
|
453
|
-
}
|
|
454
|
-
|
|
455
476
|
await simulateUndelegateTransactions({
|
|
456
|
-
asset,
|
|
477
|
+
asset: plan.asset,
|
|
457
478
|
txSteps,
|
|
458
|
-
senderAddress: delegatorAddress,
|
|
459
|
-
gasPrice,
|
|
479
|
+
senderAddress: plan.delegatorAddress,
|
|
480
|
+
gasPrice: plan.gasPrice,
|
|
460
481
|
revertOnSimulationError,
|
|
461
482
|
})
|
|
462
483
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
484
|
+
return txSteps
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
async function buildUndelegate({ walletAccount, amount, feeData, revertOnSimulationError }) {
|
|
488
|
+
const plan = await planUndelegate({ walletAccount, amount, feeData })
|
|
489
|
+
const unsignedTxs = await createPlanTxs({ walletAccount, plan })
|
|
490
|
+
const txSteps = await simulateUndelegate({ plan, unsignedTxs, revertOnSimulationError })
|
|
491
|
+
return { ...plan, txSteps, unsignedTxs }
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// NOTE: Like the legacy `undelegate()` flow, this planner currently splits
|
|
495
|
+
// the request using `pendingAmount` only. It does not pre-validate that the
|
|
496
|
+
// validator-side remainder is actually available in active staked balance,
|
|
497
|
+
// so an oversized request can still fail later when building/sending the
|
|
498
|
+
// validator unstake step.
|
|
499
|
+
async function undelegate({ walletAccount, amount, feeData, revertOnSimulationError = true }) {
|
|
500
|
+
const { asset, delegatorAddress, txSteps, unsignedTxs, mode } = await buildUndelegate({
|
|
466
501
|
walletAccount,
|
|
467
|
-
|
|
502
|
+
amount,
|
|
503
|
+
feeData,
|
|
504
|
+
revertOnSimulationError,
|
|
505
|
+
})
|
|
468
506
|
|
|
469
507
|
// 4. Sign transactions
|
|
470
508
|
//
|
|
@@ -498,7 +536,7 @@ export function createEthereumStakingService({
|
|
|
498
536
|
const {
|
|
499
537
|
bundleHash,
|
|
500
538
|
txIds: [...txIds],
|
|
501
|
-
} = await
|
|
539
|
+
} = await sendStakingTxs({ asset, walletAccount, unsignedTxs, mode })
|
|
502
540
|
|
|
503
541
|
for (const txStep of Object.values(txSteps)) {
|
|
504
542
|
if (!txStep.unsignedTx) continue
|
|
@@ -525,7 +563,7 @@ export function createEthereumStakingService({
|
|
|
525
563
|
return bundleHash
|
|
526
564
|
}
|
|
527
565
|
|
|
528
|
-
async function
|
|
566
|
+
async function planClaimUndelegatedBalance({ walletAccount, feeData }) {
|
|
529
567
|
const asset = await getAsset(assetName)
|
|
530
568
|
const staking = getStakingApi(asset)
|
|
531
569
|
|
|
@@ -536,29 +574,52 @@ export function createEthereumStakingService({
|
|
|
536
574
|
address: delegatorAddress,
|
|
537
575
|
})
|
|
538
576
|
|
|
539
|
-
if (withdrawRequest)
|
|
540
|
-
const { to, data } = withdrawRequest
|
|
577
|
+
if (!withdrawRequest) return null
|
|
541
578
|
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
}
|
|
579
|
+
const { to, data } = withdrawRequest
|
|
580
|
+
const { gasLimit, gasPrice, tipGasPrice, fee } = await estimateTxFee({
|
|
581
|
+
from: delegatorAddress,
|
|
582
|
+
to,
|
|
583
|
+
amount: null,
|
|
584
|
+
txInput: data,
|
|
585
|
+
feeData,
|
|
586
|
+
})
|
|
587
|
+
|
|
588
|
+
return {
|
|
589
|
+
asset,
|
|
590
|
+
delegatorAddress,
|
|
591
|
+
fee,
|
|
592
|
+
feeData,
|
|
593
|
+
gasPrice,
|
|
594
|
+
tipGasPrice,
|
|
595
|
+
steps: [{ to, txInput: data, gasLimit }],
|
|
596
|
+
mode: STAKING_TX_MODE.single,
|
|
559
597
|
}
|
|
560
598
|
}
|
|
561
599
|
|
|
600
|
+
async function buildClaimUndelegatedBalance(params) {
|
|
601
|
+
const plan = await planClaimUndelegatedBalance(params)
|
|
602
|
+
if (!plan) return null
|
|
603
|
+
|
|
604
|
+
const unsignedTxs = await createPlanTxs({ walletAccount: params.walletAccount, plan })
|
|
605
|
+
return { ...plan, unsignedTxs }
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
async function claimUndelegatedBalance({ walletAccount, feeData }) {
|
|
609
|
+
const plan = await buildClaimUndelegatedBalance({ walletAccount, feeData })
|
|
610
|
+
if (!plan) return
|
|
611
|
+
|
|
612
|
+
const {
|
|
613
|
+
txIds: [txId],
|
|
614
|
+
} = await sendStakingTxs({
|
|
615
|
+
asset: plan.asset,
|
|
616
|
+
walletAccount,
|
|
617
|
+
unsignedTxs: plan.unsignedTxs,
|
|
618
|
+
mode: plan.mode,
|
|
619
|
+
})
|
|
620
|
+
return txId
|
|
621
|
+
}
|
|
622
|
+
|
|
562
623
|
async function estimateDelegateOperation({ walletAccount, operation, args, feeData }) {
|
|
563
624
|
const asset = await getAsset(assetName)
|
|
564
625
|
|
|
@@ -723,52 +784,16 @@ export function createEthereumStakingService({
|
|
|
723
784
|
}
|
|
724
785
|
}
|
|
725
786
|
|
|
726
|
-
async function prepareAndSendTx({
|
|
727
|
-
asset,
|
|
728
|
-
walletAccount,
|
|
729
|
-
to,
|
|
730
|
-
amount,
|
|
731
|
-
txData: txInput,
|
|
732
|
-
gasPrice,
|
|
733
|
-
gasLimit,
|
|
734
|
-
tipGasPrice,
|
|
735
|
-
waitForConfirmation = false,
|
|
736
|
-
feeData,
|
|
737
|
-
}) {
|
|
738
|
-
const { unsignedTx } = await asset.baseAsset.api.createTx({
|
|
739
|
-
asset,
|
|
740
|
-
walletAccount,
|
|
741
|
-
address: to,
|
|
742
|
-
amount: amount || asset.currency.ZERO,
|
|
743
|
-
txInput,
|
|
744
|
-
gasPrice,
|
|
745
|
-
gasLimit,
|
|
746
|
-
// HACK: Override the `tipGasPrice` to use a custom `maxPriorityFeePerGas`.
|
|
747
|
-
tipGasPrice,
|
|
748
|
-
feeData,
|
|
749
|
-
})
|
|
750
|
-
|
|
751
|
-
const { txId } = await asset.baseAsset.api.sendTx({ asset, walletAccount, unsignedTx })
|
|
752
|
-
|
|
753
|
-
const baseAsset = asset.baseAsset
|
|
754
|
-
if (waitForConfirmation) {
|
|
755
|
-
const getTxLog = (...args) => assetClientInterface.getTxLog(...args)
|
|
756
|
-
const watchTx = createWatchTx({
|
|
757
|
-
walletAccount,
|
|
758
|
-
getTxLog,
|
|
759
|
-
baseAsset,
|
|
760
|
-
})
|
|
761
|
-
await watchTx(txId)
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
return txId
|
|
765
|
-
}
|
|
766
|
-
|
|
767
787
|
return {
|
|
788
|
+
planDelegate,
|
|
789
|
+
planUndelegate,
|
|
790
|
+
planClaimUndelegatedBalance,
|
|
791
|
+
simulateUndelegate,
|
|
792
|
+
getEthereumStakingInfo,
|
|
793
|
+
// @deprecated legacy staking api, remove once evm staking unification is done
|
|
768
794
|
delegate,
|
|
769
795
|
undelegate,
|
|
770
796
|
claimUndelegatedBalance,
|
|
771
|
-
getEthereumStakingInfo,
|
|
772
797
|
estimateDelegateOperation,
|
|
773
798
|
getMinAmount,
|
|
774
799
|
getDelegateSelectAllAmount,
|
|
@@ -801,7 +826,7 @@ export async function getEthereumStakingInfo({
|
|
|
801
826
|
])
|
|
802
827
|
|
|
803
828
|
const delegatedBalance = activeStakedBalance.add(pendingBalance).add(pendingDepositedBalance)
|
|
804
|
-
const minDelegateAmount = currency.defaultUnit(
|
|
829
|
+
const minDelegateAmount = currency.defaultUnit(ETH_STAKING_MIN_AMOUNT)
|
|
805
830
|
|
|
806
831
|
const {
|
|
807
832
|
requested,
|