@exodus/ethereum-api 8.73.1 → 8.73.3
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 +24 -0
- package/package.json +4 -5
- package/src/error-wrapper.js +7 -4
- package/src/staking/ethereum/api.js +22 -3
- package/src/staking/ethereum/everstake.js +53 -0
- package/src/staking/ethereum/service.js +382 -141
- package/src/staking/ethereum/staking-utils.js +268 -1
- package/src/tx-log/get-optimistic-txlog-effects.js +49 -0
- package/src/tx-log-staking-processor/asset-staking-tx-data.js +40 -13
- package/src/tx-log-staking-processor/index.js +9 -13
- package/src/tx-send/nonce-utils.js +3 -1
- package/src/etherscan/account.js +0 -50
- package/src/etherscan/index.js +0 -28
- package/src/etherscan/logs.js +0 -17
- package/src/etherscan/proxy.js +0 -48
- package/src/etherscan/request.js +0 -26
- package/src/etherscan/ws.js +0 -88
|
@@ -1,12 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Everstake ETH Staking — Balance States
|
|
3
|
+
*
|
|
4
|
+
* When a user stakes ETH via Everstake, their funds move through several
|
|
5
|
+
* buckets tracked by the Accounting contract. Each bucket has different
|
|
6
|
+
* rules about what operations are allowed.
|
|
7
|
+
*
|
|
8
|
+
* User Wallet (ETH)
|
|
9
|
+
* |
|
|
10
|
+
* | stake()
|
|
11
|
+
* v
|
|
12
|
+
* pendingBalance
|
|
13
|
+
* Pool waiting room. ETH sits here until enough accumulates
|
|
14
|
+
* (32 ETH across all users) to fund a new validator.
|
|
15
|
+
* Can be withdrawn instantly via unstakePending().
|
|
16
|
+
* |
|
|
17
|
+
* | pool reaches 32 ETH, deposits to Beacon chain
|
|
18
|
+
* v
|
|
19
|
+
* pendingDepositedBalance
|
|
20
|
+
* Sent to Beacon chain but validator not yet activated.
|
|
21
|
+
* LOCKED — cannot be unstaked until validator activates.
|
|
22
|
+
* |
|
|
23
|
+
* | validator activates
|
|
24
|
+
* v
|
|
25
|
+
* activeStakedBalance (autocompoundBalanceOf)
|
|
26
|
+
* User's share of active validator ETH, including compounded
|
|
27
|
+
* rewards. Can be unstaked via unstake().
|
|
28
|
+
* |
|
|
29
|
+
* | unstake()
|
|
30
|
+
* |-----> ETH returned immediately (if pool has liquidity / interchange)
|
|
31
|
+
* | goes directly to wallet
|
|
32
|
+
* v
|
|
33
|
+
* unclaimedUndelegatedBalance
|
|
34
|
+
* Withdrawal queue. Waiting for validator exit.
|
|
35
|
+
* Global per-user — all unstake() calls accumulate into a single
|
|
36
|
+
* balance. Cannot claim until the entire balance is ready.
|
|
37
|
+
* |
|
|
38
|
+
* | validator(s) exit
|
|
39
|
+
* v
|
|
40
|
+
* readyForClaim (withdrawRequest.readyForClaim)
|
|
41
|
+
* When requested == readyForClaim, user can call
|
|
42
|
+
* claimWithdrawRequest() to get ETH back to wallet.
|
|
43
|
+
* |
|
|
44
|
+
* | claimWithdrawRequest()
|
|
45
|
+
* v
|
|
46
|
+
* User Wallet (ETH)
|
|
47
|
+
*
|
|
48
|
+
* Additional derived values:
|
|
49
|
+
* - delegatedBalance = activeStakedBalance + pendingBalance + pendingDepositedBalance
|
|
50
|
+
* - depositedBalanceOf = the user's original deposit amount tracked by the
|
|
51
|
+
* Accounting contract. Does NOT include compounded rewards — only what the
|
|
52
|
+
* user explicitly staked. Grows when user stakes, shrinks when user unstakes.
|
|
53
|
+
* - liquidRewards = activeStakedBalance - depositedBalanceOf
|
|
54
|
+
* The difference between the user's current share of validator ETH
|
|
55
|
+
* (which grows as validators earn rewards and autocompound) and what
|
|
56
|
+
* they originally deposited. This is the "unrealized" profit that
|
|
57
|
+
* would be forfeited if the user unstakes only their deposited amount.
|
|
58
|
+
* - rewardsBalance (getTotalRewards) = historical total rewards from the
|
|
59
|
+
* Everstake API, including already-claimed and compounded rewards.
|
|
60
|
+
* Unlike liquidRewards, this is NOT derived on-chain — it comes from
|
|
61
|
+
* an external Everstake endpoint and may lag behind real-time state.
|
|
62
|
+
*/
|
|
63
|
+
|
|
1
64
|
import { memoize } from '@exodus/basic-utils'
|
|
65
|
+
import NumberUnit from '@exodus/currency'
|
|
2
66
|
import assert from 'minimalistic-assert'
|
|
3
67
|
|
|
4
68
|
import { estimateGasLimit, scaleGasLimitEstimate } from '../../gas-estimation.js'
|
|
69
|
+
import { getOptimisticTxLogEffects } from '../../tx-log/get-optimistic-txlog-effects.js'
|
|
5
70
|
import { createWatchTx as defaultCreateWatch } from '../../watch-tx.js'
|
|
6
71
|
import { stakingProviderClientFactory } from '../staking-provider-client.js'
|
|
7
72
|
import { amountToCurrency, DISABLE_BALANCE_CHECKS, resolveFeeData } from '../utils/index.js'
|
|
8
73
|
import { EthereumStaking } from './api.js'
|
|
9
74
|
import { getEverstakeValidatorsQueue } from './everstake.js'
|
|
75
|
+
import { simulateUndelegateTransactions } from './staking-utils.js'
|
|
10
76
|
|
|
11
77
|
const WETH9_ADDRESS = '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'
|
|
12
78
|
|
|
@@ -37,6 +103,74 @@ const getStakingApi = memoize(
|
|
|
37
103
|
(asset) => asset.name
|
|
38
104
|
)
|
|
39
105
|
|
|
106
|
+
export async function getUndelegatePendingData({
|
|
107
|
+
staking,
|
|
108
|
+
delegatorAddress,
|
|
109
|
+
requestedAmount,
|
|
110
|
+
pendingAmount,
|
|
111
|
+
minAmount,
|
|
112
|
+
resolvedFeeData,
|
|
113
|
+
estimateTxFee,
|
|
114
|
+
}) {
|
|
115
|
+
if (!pendingAmount.isPositive) return null
|
|
116
|
+
|
|
117
|
+
const leftOver = pendingAmount.sub(requestedAmount)
|
|
118
|
+
|
|
119
|
+
if (leftOver.isPositive && leftOver.lt(minAmount)) return null
|
|
120
|
+
|
|
121
|
+
// Essentially the min of the 2 amounts
|
|
122
|
+
const inactiveAmountToUnstake = pendingAmount.lte(requestedAmount)
|
|
123
|
+
? pendingAmount
|
|
124
|
+
: requestedAmount
|
|
125
|
+
|
|
126
|
+
const { to, data } = await staking.unstakePending({
|
|
127
|
+
address: delegatorAddress,
|
|
128
|
+
amount: inactiveAmountToUnstake,
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
const feeInfo = estimateTxFee
|
|
132
|
+
? await estimateTxFee({
|
|
133
|
+
from: delegatorAddress,
|
|
134
|
+
to,
|
|
135
|
+
amount: null,
|
|
136
|
+
txInput: data,
|
|
137
|
+
feeData: resolvedFeeData,
|
|
138
|
+
})
|
|
139
|
+
: null
|
|
140
|
+
|
|
141
|
+
return { plan: { to, txData: data, amount: inactiveAmountToUnstake }, feeInfo }
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export async function getUndelegateData({
|
|
145
|
+
staking,
|
|
146
|
+
delegatorAddress,
|
|
147
|
+
requestedAmount,
|
|
148
|
+
pendingAmount,
|
|
149
|
+
resolvedFeeData,
|
|
150
|
+
estimateTxFee,
|
|
151
|
+
}) {
|
|
152
|
+
const activeAmountToUnstake = requestedAmount.sub(pendingAmount)
|
|
153
|
+
|
|
154
|
+
if (!activeAmountToUnstake.isPositive) return null
|
|
155
|
+
|
|
156
|
+
const { to, data } = await staking.unstake({
|
|
157
|
+
address: delegatorAddress,
|
|
158
|
+
amount: activeAmountToUnstake,
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
const feeInfo = estimateTxFee
|
|
162
|
+
? await estimateTxFee({
|
|
163
|
+
from: delegatorAddress,
|
|
164
|
+
to,
|
|
165
|
+
amount: null,
|
|
166
|
+
txInput: data,
|
|
167
|
+
feeData: resolvedFeeData,
|
|
168
|
+
})
|
|
169
|
+
: null
|
|
170
|
+
|
|
171
|
+
return { plan: { to, txData: data, amount: activeAmountToUnstake }, feeInfo }
|
|
172
|
+
}
|
|
173
|
+
|
|
40
174
|
export function createEthereumStakingService({
|
|
41
175
|
asset: deprectedArg, // @deprecated use `assetName` instead
|
|
42
176
|
assetName,
|
|
@@ -127,71 +261,40 @@ export function createEthereumStakingService({
|
|
|
127
261
|
return txId
|
|
128
262
|
}
|
|
129
263
|
|
|
130
|
-
async function
|
|
131
|
-
|
|
132
|
-
resquestedAmount,
|
|
133
|
-
pendingAmount,
|
|
134
|
-
minAmount,
|
|
135
|
-
feeData,
|
|
136
|
-
}) {
|
|
264
|
+
async function prepareUndelegate({ walletAccount, amount, feeData }) {
|
|
265
|
+
assert(amount instanceof NumberUnit, 'expected amount to be a NumberUnit')
|
|
137
266
|
const asset = await getAsset(assetName)
|
|
138
267
|
const staking = getStakingApi(asset)
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
if (leftOver.isPositive && leftOver.lt(minAmount)) {
|
|
142
|
-
throw new Error(`Pending balance less than min stake amount ${minAmount}`)
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
const inactiveAmountToUnstake = pendingAmount.lte(resquestedAmount)
|
|
146
|
-
? pendingAmount
|
|
147
|
-
: resquestedAmount
|
|
148
|
-
|
|
149
|
-
feeData = await resolveFeeData({ asset, assetClientInterface, feeData })
|
|
150
|
-
|
|
151
|
-
const { to, data } = await staking.unstakePending({
|
|
152
|
-
address: delegatorAddress,
|
|
153
|
-
amount: inactiveAmountToUnstake,
|
|
154
|
-
})
|
|
268
|
+
const minAmount = getMinAmount(asset)
|
|
269
|
+
const requestedAmount = amount
|
|
155
270
|
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
feeData,
|
|
271
|
+
const [address, resolvedFeeData] = await Promise.all([
|
|
272
|
+
assetClientInterface.getReceiveAddress({
|
|
273
|
+
assetName,
|
|
274
|
+
walletAccount,
|
|
275
|
+
}),
|
|
276
|
+
resolveFeeData({ asset, assetClientInterface, feeData }),
|
|
277
|
+
])
|
|
278
|
+
const delegatorAddress = address.toLowerCase()
|
|
279
|
+
const pendingAmount = await staking.pendingBalanceOf(delegatorAddress)
|
|
280
|
+
const baseNonce = await asset.baseAsset.getNonce({
|
|
281
|
+
asset,
|
|
282
|
+
fromAddress: delegatorAddress,
|
|
283
|
+
walletAccount,
|
|
284
|
+
tag: 'latest',
|
|
285
|
+
forceFromNode: true,
|
|
162
286
|
})
|
|
163
287
|
|
|
164
|
-
return {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
console.warn('UnstakePending covered requested unstake. Nothing to unstake from validator')
|
|
174
|
-
return Object.create(null)
|
|
288
|
+
return {
|
|
289
|
+
asset,
|
|
290
|
+
staking,
|
|
291
|
+
minAmount,
|
|
292
|
+
requestedAmount,
|
|
293
|
+
feeData: resolvedFeeData,
|
|
294
|
+
delegatorAddress,
|
|
295
|
+
pendingAmount,
|
|
296
|
+
baseNonce,
|
|
175
297
|
}
|
|
176
|
-
|
|
177
|
-
const activeAmountToUnstake = resquestedAmount.sub(pendingAmount)
|
|
178
|
-
|
|
179
|
-
feeData = await resolveFeeData({ asset, assetClientInterface, feeData })
|
|
180
|
-
|
|
181
|
-
const { to, data } = await staking.unstake({
|
|
182
|
-
address: delegatorAddress,
|
|
183
|
-
amount: activeAmountToUnstake,
|
|
184
|
-
})
|
|
185
|
-
|
|
186
|
-
const { fee, gasLimit, gasPrice, tipGasPrice } = await estimateTxFee({
|
|
187
|
-
from: delegatorAddress,
|
|
188
|
-
to,
|
|
189
|
-
amount: null,
|
|
190
|
-
txInput: data,
|
|
191
|
-
feeData,
|
|
192
|
-
})
|
|
193
|
-
|
|
194
|
-
return { to, txData: data, gasLimit, gasPrice, tipGasPrice, fee }
|
|
195
298
|
}
|
|
196
299
|
|
|
197
300
|
/**
|
|
@@ -199,7 +302,8 @@ export function createEthereumStakingService({
|
|
|
199
302
|
* Fee estimation depends on the executed txs. Can be both.
|
|
200
303
|
* @returns total undelegete fee
|
|
201
304
|
*/
|
|
202
|
-
async function estimateUndelegate({ walletAccount, amount:
|
|
305
|
+
async function estimateUndelegate({ walletAccount, amount: requestedAmount, feeData }) {
|
|
306
|
+
assert(requestedAmount instanceof NumberUnit, 'expected amount to be a NumberUnit')
|
|
203
307
|
const asset = await getAsset(assetName)
|
|
204
308
|
const staking = getStakingApi(asset)
|
|
205
309
|
const minAmount = getMinAmount(asset)
|
|
@@ -209,114 +313,251 @@ export function createEthereumStakingService({
|
|
|
209
313
|
|
|
210
314
|
const pendingAmount = await staking.pendingBalanceOf(delegatorAddress)
|
|
211
315
|
|
|
212
|
-
let
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
} catch (err) {
|
|
226
|
-
// useful to debug fee calculation
|
|
227
|
-
console.warn('ETH unstake pending estimation failed, continuing with unstake', err)
|
|
228
|
-
}
|
|
316
|
+
let pendingResult = null
|
|
317
|
+
try {
|
|
318
|
+
pendingResult = await getUndelegatePendingData({
|
|
319
|
+
staking,
|
|
320
|
+
delegatorAddress,
|
|
321
|
+
requestedAmount,
|
|
322
|
+
pendingAmount,
|
|
323
|
+
minAmount,
|
|
324
|
+
resolvedFeeData: feeData,
|
|
325
|
+
estimateTxFee,
|
|
326
|
+
})
|
|
327
|
+
} catch (err) {
|
|
328
|
+
console.warn('ETH unstake pending estimation failed, continuing with unstake', err)
|
|
229
329
|
}
|
|
230
330
|
|
|
231
|
-
const
|
|
331
|
+
const undelegateResult = await getUndelegateData({
|
|
332
|
+
staking,
|
|
232
333
|
delegatorAddress,
|
|
233
|
-
|
|
334
|
+
requestedAmount,
|
|
234
335
|
pendingAmount,
|
|
235
|
-
|
|
236
|
-
|
|
336
|
+
resolvedFeeData: feeData,
|
|
337
|
+
estimateTxFee,
|
|
237
338
|
})
|
|
238
339
|
|
|
239
|
-
|
|
240
|
-
|
|
340
|
+
const pendingFee = pendingResult?.feeInfo?.fee ?? asset.currency.ZERO
|
|
341
|
+
const undelegateFee = undelegateResult?.feeInfo?.fee ?? asset.currency.ZERO
|
|
241
342
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
const staking = getStakingApi(asset)
|
|
245
|
-
const minAmount = getMinAmount(asset)
|
|
343
|
+
return pendingFee.add(undelegateFee)
|
|
344
|
+
}
|
|
246
345
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
346
|
+
// NOTE: Like the legacy `undelegate()` flow, this planner currently splits
|
|
347
|
+
// the request using `pendingAmount` only. It does not pre-validate that the
|
|
348
|
+
// validator-side remainder is actually available in active staked balance,
|
|
349
|
+
// so an oversized request can still fail later when building/sending the
|
|
350
|
+
// validator unstake step.
|
|
351
|
+
async function undelegate({ walletAccount, amount, feeData, revertOnSimulationError = true }) {
|
|
352
|
+
const {
|
|
353
|
+
asset,
|
|
354
|
+
staking,
|
|
355
|
+
minAmount,
|
|
356
|
+
requestedAmount,
|
|
357
|
+
feeData: resolvedFeeData,
|
|
358
|
+
delegatorAddress,
|
|
359
|
+
pendingAmount,
|
|
360
|
+
baseNonce,
|
|
361
|
+
} = await prepareUndelegate({ walletAccount, amount, feeData })
|
|
253
362
|
|
|
254
|
-
|
|
255
|
-
|
|
363
|
+
if (!requestedAmount.isPositive) {
|
|
364
|
+
throw new Error('Undelegate amount must be positive')
|
|
365
|
+
}
|
|
256
366
|
|
|
257
|
-
const
|
|
367
|
+
const txSteps = {
|
|
368
|
+
undelegatePending: {
|
|
369
|
+
plan: null,
|
|
370
|
+
gasLimit: null,
|
|
371
|
+
unsignedTx: null,
|
|
372
|
+
signedTx: null,
|
|
373
|
+
txId: null,
|
|
374
|
+
},
|
|
375
|
+
undelegate: {
|
|
376
|
+
plan: null,
|
|
377
|
+
gasLimit: null,
|
|
378
|
+
unsignedTx: null,
|
|
379
|
+
signedTx: null,
|
|
380
|
+
txId: null,
|
|
381
|
+
},
|
|
382
|
+
}
|
|
258
383
|
|
|
259
384
|
console.log(
|
|
260
|
-
`delegator address ${delegatorAddress} unstaking ${
|
|
385
|
+
`delegator address ${delegatorAddress} unstaking ${requestedAmount.toDefaultString({
|
|
261
386
|
unit: true,
|
|
262
387
|
})} - pending amount: ${pendingAmount.toDefaultString({ unit: true })}`
|
|
263
388
|
)
|
|
264
389
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
390
|
+
// 1. Plan transactions and estimate fees
|
|
391
|
+
const pendingResult = await getUndelegatePendingData({
|
|
392
|
+
staking,
|
|
393
|
+
delegatorAddress,
|
|
394
|
+
requestedAmount,
|
|
395
|
+
pendingAmount,
|
|
396
|
+
minAmount,
|
|
397
|
+
resolvedFeeData,
|
|
398
|
+
estimateTxFee,
|
|
399
|
+
})
|
|
274
400
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
401
|
+
const undelegateResult = await getUndelegateData({
|
|
402
|
+
staking,
|
|
403
|
+
delegatorAddress,
|
|
404
|
+
requestedAmount,
|
|
405
|
+
pendingAmount,
|
|
406
|
+
resolvedFeeData,
|
|
407
|
+
estimateTxFee,
|
|
408
|
+
})
|
|
282
409
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
//
|
|
286
|
-
// NOTE: This will invalidate previous transaction fee estimates!
|
|
287
|
-
if (waitForConfirmation) {
|
|
288
|
-
feeData = await assetClientInterface.getFeeData({ assetName })
|
|
289
|
-
}
|
|
410
|
+
if (!pendingResult && !undelegateResult) {
|
|
411
|
+
throw new Error('Requested undelegation produced no transactions')
|
|
290
412
|
}
|
|
291
413
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
414
|
+
let gasPrice
|
|
415
|
+
let tipGasPrice
|
|
416
|
+
|
|
417
|
+
if (pendingResult) {
|
|
418
|
+
txSteps.undelegatePending.plan = pendingResult.plan
|
|
419
|
+
txSteps.undelegatePending.gasLimit = pendingResult.feeInfo.gasLimit
|
|
420
|
+
gasPrice = pendingResult.feeInfo.gasPrice
|
|
421
|
+
tipGasPrice = pendingResult.feeInfo.tipGasPrice
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if (undelegateResult) {
|
|
425
|
+
txSteps.undelegate.plan = undelegateResult.plan
|
|
426
|
+
txSteps.undelegate.gasLimit = undelegateResult.feeInfo.gasLimit
|
|
427
|
+
gasPrice = gasPrice || undelegateResult.feeInfo.gasPrice
|
|
428
|
+
tipGasPrice = tipGasPrice || undelegateResult.feeInfo.tipGasPrice
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// 3. Create unsigned txs
|
|
432
|
+
const createTxBaseArgs = {
|
|
433
|
+
asset,
|
|
434
|
+
walletAccount,
|
|
435
|
+
fromAddress: delegatorAddress,
|
|
436
|
+
amount: asset.currency.ZERO,
|
|
437
|
+
tipGasPrice,
|
|
438
|
+
gasPrice,
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const createdTxEntries = await Promise.all(
|
|
442
|
+
Object.entries(txSteps)
|
|
443
|
+
.filter(([, txStep]) => txStep.plan)
|
|
444
|
+
.map(async ([key, txStep], index) => {
|
|
445
|
+
const result = await asset.baseAsset.api.createTx({
|
|
446
|
+
...createTxBaseArgs,
|
|
447
|
+
toAddress: txStep.plan.to,
|
|
448
|
+
txInput: txStep.plan.txData,
|
|
449
|
+
gasLimit: txStep.gasLimit,
|
|
450
|
+
nonce: baseNonce + index,
|
|
451
|
+
})
|
|
452
|
+
return [key, result.unsignedTx]
|
|
453
|
+
})
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
for (const [key, unsignedTx] of createdTxEntries) {
|
|
457
|
+
txSteps[key].unsignedTx = unsignedTx
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
await simulateUndelegateTransactions({
|
|
461
|
+
asset,
|
|
462
|
+
txSteps,
|
|
463
|
+
senderAddress: delegatorAddress,
|
|
464
|
+
gasPrice,
|
|
465
|
+
revertOnSimulationError,
|
|
298
466
|
})
|
|
299
467
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
468
|
+
// 4. Sign transactions
|
|
469
|
+
const signedTxEntries = await Promise.all(
|
|
470
|
+
Object.entries(txSteps)
|
|
471
|
+
.filter(([, txStep]) => txStep.unsignedTx)
|
|
472
|
+
.map(async ([key, txStep]) => {
|
|
473
|
+
const signedTx = await assetClientInterface.signTransaction({
|
|
474
|
+
assetName: asset.baseAsset.name,
|
|
475
|
+
unsignedTx: txStep.unsignedTx,
|
|
476
|
+
walletAccount,
|
|
477
|
+
})
|
|
478
|
+
const txId = `0x${signedTx.txId.toString('hex')}`
|
|
479
|
+
return [key, { signedTx, txId }]
|
|
480
|
+
})
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
for (const [key, { signedTx, txId }] of signedTxEntries) {
|
|
484
|
+
txSteps[key].signedTx = signedTx
|
|
485
|
+
txSteps[key].txId = txId
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// 5. Broadcast transactions via bundle and get bundle hash
|
|
489
|
+
const bundleResponse = await asset.broadcastPrivateBundle({
|
|
490
|
+
txs: Object.values(txSteps)
|
|
491
|
+
.filter((txStep) => txStep.signedTx)
|
|
492
|
+
.map(({ signedTx }) => signedTx.rawTx),
|
|
493
|
+
})
|
|
494
|
+
const bundleHash = bundleResponse?.bundleHash
|
|
495
|
+
|
|
496
|
+
// 6. Optimistic tx-log effects: reflect tx presence and fee consumption only.
|
|
497
|
+
// The ETH returned to the user via the contract's internal transfer is not
|
|
498
|
+
// captured here — it will be picked up once the tx is confirmed on-chain.
|
|
499
|
+
// It would also be incorrect to optimistically log the full requested ETH
|
|
500
|
+
// as immediately received: some portion may instead be routed into the
|
|
501
|
+
// undelegation queue / withdraw-request system and only become claimable
|
|
502
|
+
// much later after validator exit.
|
|
503
|
+
//
|
|
504
|
+
// We now have a more deterministic model for these incoming ETH amounts,
|
|
505
|
+
// but it requires staking-operation context that getOptimisticTxLogEffects()
|
|
506
|
+
// does not currently accept:
|
|
507
|
+
//
|
|
508
|
+
// - unstakePending: the ETH returned is exactly the amount passed to
|
|
509
|
+
// unstakePending(). In this flow that amount is chosen from the pre-send
|
|
510
|
+
// pending balance and the requested undelegation amount:
|
|
511
|
+
// amountPassedToUnstakePending = min(totalRequestedAmount, pendingBalanceBefore)
|
|
512
|
+
// - unstake: the ETH returned can be derived only from staking state before
|
|
513
|
+
// and after the operation:
|
|
514
|
+
// amountPassedToUnstake = totalRequestedAmount - pendingBalanceBefore
|
|
515
|
+
// queuedFromUnstake = unclaimedUndelegatedBalanceAfter - unclaimedUndelegatedBalanceBefore
|
|
516
|
+
// returnedImmediatelyFromUnstake = amountPassedToUnstake - queuedFromUnstake
|
|
517
|
+
//
|
|
518
|
+
// That is incompatible with the current tx-log side-effect hook, which only
|
|
519
|
+
// receives tx-local data (method id, calldata, nonce, gas, etc.) and not
|
|
520
|
+
// staking snapshots or precomputed ETH-return amounts.
|
|
521
|
+
const optimisticEffects = await Promise.all(
|
|
522
|
+
Object.values(txSteps)
|
|
523
|
+
.filter((txStep) => txStep.txId)
|
|
524
|
+
.map((txStep) =>
|
|
525
|
+
getOptimisticTxLogEffects({
|
|
526
|
+
asset: asset.baseAsset,
|
|
527
|
+
assetClientInterface,
|
|
528
|
+
fromAddress: delegatorAddress,
|
|
529
|
+
txId: txStep.txId,
|
|
530
|
+
unsignedTx: txStep.unsignedTx,
|
|
531
|
+
walletAccount,
|
|
532
|
+
bundleId: bundleHash ?? undefined,
|
|
533
|
+
})
|
|
534
|
+
)
|
|
535
|
+
)
|
|
536
|
+
|
|
537
|
+
for (const { optimisticTxLogEffects } of optimisticEffects) {
|
|
538
|
+
for (const effect of optimisticTxLogEffects) {
|
|
539
|
+
await assetClientInterface.updateTxLogAndNotify(effect)
|
|
540
|
+
}
|
|
307
541
|
}
|
|
308
542
|
|
|
309
543
|
// Testnet assets do not support delegations tracking
|
|
310
|
-
if (
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
544
|
+
if (assetName === 'ethereum') {
|
|
545
|
+
const notify = (txStep) =>
|
|
546
|
+
stakingProvider.notifyUnstaking({
|
|
547
|
+
txId: txStep.txId,
|
|
548
|
+
asset: assetName,
|
|
549
|
+
delegator: delegatorAddress,
|
|
550
|
+
amount: txStep.plan.amount.toBaseString(),
|
|
551
|
+
})
|
|
552
|
+
|
|
553
|
+
await Promise.all([
|
|
554
|
+
txSteps.undelegatePending.txId && notify(txSteps.undelegatePending),
|
|
555
|
+
txSteps.undelegate.txId && notify(txSteps.undelegate),
|
|
556
|
+
])
|
|
317
557
|
}
|
|
318
558
|
|
|
319
|
-
|
|
559
|
+
// This is the most correct response, but it doesn't matter, it is not consumed by the apps
|
|
560
|
+
return bundleHash
|
|
320
561
|
}
|
|
321
562
|
|
|
322
563
|
async function claimUndelegatedBalance({ walletAccount, feeData }) {
|