@exodus/ethereum-api 9.0.0-alpha.2 → 9.0.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 +2121 -0
- package/LICENSE +7 -0
- package/README.md +124 -0
- package/package.json +34 -33
- package/src/address-has-history.js +6 -2
- package/src/allowance/constants.js +4 -5
- package/src/allowance/index.js +159 -40
- package/src/check-tx/create-assess-transaction.js +23 -0
- package/src/check-tx/create-check-tx.js +104 -0
- package/src/check-tx/index.js +1 -0
- package/src/create-asset-plugin-factory.js +115 -0
- package/src/create-asset-utils.js +520 -0
- package/src/create-asset.js +272 -97
- package/src/create-token-factory.js +47 -17
- package/src/custom-fees.js +57 -0
- package/src/eip7702-whitelist.js +62 -0
- package/src/ens/index.js +12 -17
- package/src/error-wrapper.js +383 -0
- package/src/eth-like-util.js +172 -33
- package/src/exodus-eth-server/api-coin-nodes.js +27 -196
- package/src/exodus-eth-server/api.js +23 -14
- package/src/exodus-eth-server/clarity-v2.js +255 -0
- package/src/exodus-eth-server/clarity.js +67 -265
- package/src/exodus-eth-server/errors.js +40 -0
- package/src/exodus-eth-server/eth-like-server.js +385 -0
- package/src/exodus-eth-server/fetch-json.js +48 -0
- package/src/exodus-eth-server/index.js +77 -0
- package/src/exodus-eth-server/utils.js +31 -0
- package/src/exodus-eth-server/ws-gateway.js +306 -0
- package/src/exodus-eth-server/ws.js +1 -2
- package/src/fee-data/EthLikeFeeData.js +81 -0
- package/src/fee-data/index.js +54 -0
- package/src/fee-utils.js +229 -0
- package/src/gas-estimation.js +183 -66
- package/src/get-balance-for-address.js +1 -1
- package/src/get-balances.js +285 -27
- package/src/get-fee.js +92 -56
- package/src/get-historical-balance.js +58 -0
- package/src/hooks/index.js +1 -1
- package/src/hooks/monitor.js +182 -43
- package/src/hooks/staking-freshness.js +58 -0
- package/src/index.js +118 -12
- package/src/move-funds.js +153 -0
- package/src/multicall3/index.js +212 -0
- package/src/multisig-address.js +146 -0
- package/src/nft-utils.js +60 -0
- package/src/number-utils.js +34 -2
- package/src/optimism-gas/index.js +13 -5
- package/src/send-validations.js +58 -0
- package/src/server-based-fee-monitor.js +12 -19
- package/src/simulate-tx/fetch-tx-preview.js +0 -2
- package/src/simulate-tx/index.js +2 -2
- package/src/simulate-tx/simulate-eth-tx.js +24 -11
- package/src/simulation/common.js +34 -0
- package/src/simulation/create-simulate-message.js +49 -0
- package/src/simulation/create-simulate-transactions.js +108 -0
- package/src/simulation/estimate-fee.js +14 -0
- package/src/simulation/estimate-simple-transfer.js +15 -0
- package/src/simulation/get-message-type.js +18 -0
- package/src/simulation/simulate-message-api.js +68 -0
- package/src/simulation/simulate-transactions-api.js +266 -0
- package/src/simulation/simulate-transactions.js +16 -0
- package/src/simulation/transactions.js +52 -0
- package/src/simulation/try-estimating-changes-locally.js +26 -0
- package/src/staking/api/index.js +43 -0
- package/src/staking/cached-read.js +33 -0
- package/src/staking/ethereum/api.js +166 -34
- package/src/staking/ethereum/deps.js +37 -0
- package/src/staking/ethereum/everstake.js +113 -0
- package/src/staking/ethereum/index.js +4 -3
- package/src/staking/ethereum/service.js +682 -181
- package/src/staking/ethereum/staking-utils.js +275 -4
- package/src/staking/fantom-staking.js +9 -9
- package/src/staking/index.js +4 -4
- package/src/staking/matic/addresses.js +2 -0
- package/src/staking/matic/api.js +69 -49
- package/src/staking/matic/index.js +3 -3
- package/src/staking/matic/matic-staking-utils.js +243 -5
- package/src/staking/matic/matic-staking.md +53 -0
- package/src/staking/staking-provider-client.js +1 -1
- package/src/staking/utils/index.js +39 -0
- package/src/tx-create.js +666 -0
- package/src/tx-log/clarity-monitor.js +273 -103
- package/src/tx-log/clarity-truncated-history-monitor.js +109 -0
- package/src/tx-log/clarity-truncated-history-monitor.md +219 -0
- package/src/tx-log/clarity-utils/absolute.js +96 -0
- package/src/tx-log/clarity-utils/get-log-items-from-server-tx.js +94 -31
- package/src/tx-log/clarity-utils/get-names-of-tokens-transferred-by-server-tx.js +3 -2
- package/src/tx-log/clarity-utils/index.js +9 -3
- package/src/tx-log/clarity-utils/is-spam-pending-tx.js +13 -0
- package/src/tx-log/clarity-utils/normalize-transactions-response.js +65 -0
- package/src/tx-log/ethereum-monitor.js +93 -47
- package/src/tx-log/ethereum-no-history-monitor.js +52 -41
- package/src/tx-log/get-optimistic-txlog-effects.js +379 -0
- package/src/tx-log/index.js +5 -3
- package/src/tx-log/monitor-utils/check-pending-transactions.js +14 -5
- package/src/tx-log/monitor-utils/exclude-unchanged-token-balances.js +1 -1
- package/src/tx-log/monitor-utils/extract-balance-from-clarity-account-info.js +108 -0
- package/src/tx-log/monitor-utils/get-balance-updates.js +44 -0
- package/src/tx-log/monitor-utils/get-batched-rpc-balances.js +47 -0
- package/src/tx-log/monitor-utils/get-current-blacklist-status.js +38 -0
- package/src/tx-log/monitor-utils/get-current-eip7702-delegation.js +79 -0
- package/src/tx-log/monitor-utils/get-derive-transactions-to-check.js +10 -5
- package/src/tx-log/monitor-utils/get-fee-amount.js +1 -1
- package/src/tx-log/monitor-utils/get-history-from-server.js +1 -1
- package/src/tx-log/monitor-utils/get-log-items-from-server-tx.js +75 -33
- package/src/tx-log/monitor-utils/get-names-of-tokens-transferred-by-server-tx.js +3 -2
- package/src/tx-log/monitor-utils/get-raw-transfer-amount.js +29 -0
- package/src/tx-log/monitor-utils/get-transfers-by-token-name.js +3 -2
- package/src/tx-log/monitor-utils/get-value-of-transfers.js +10 -12
- package/src/tx-log/monitor-utils/index.js +12 -7
- package/src/tx-log/monitor-utils/is-unverifiable-pending-tx-expired.js +19 -0
- package/src/tx-log/monitor-utils/verify-pending-tx-status-rpc.js +90 -0
- package/src/tx-log-staking-processor/asset-staking-tx-data.js +82 -24
- package/src/tx-log-staking-processor/index.js +28 -14
- package/src/tx-log-staking-processor/utils.js +21 -20
- package/src/tx-send/broadcast-error-handler.js +63 -0
- package/src/tx-send/index.js +1 -2
- package/src/tx-send/nonce-utils.js +97 -0
- package/src/tx-send/tx-send.js +94 -288
- package/src/tx-sign/index.js +17 -0
- package/src/tx-type/index.js +280 -0
- package/src/watch-tx.js +36 -0
- package/src/web3/createSimulateMessage.js +3 -2
- package/src/web3/createSimulateTransactions.js +11 -0
- package/src/web3/index.js +3 -1
- package/src/etherscan/account.js +0 -49
- package/src/etherscan/index.js +0 -28
- package/src/etherscan/logs.js +0 -16
- package/src/etherscan/proxy.js +0 -47
- package/src/etherscan/request.js +0 -26
- package/src/etherscan/ws.js +0 -88
- package/src/exodus-eth-server/server-factory.js +0 -28
- package/src/exodus-eth-server/server-map.js +0 -21
- package/src/get-fee-async.js +0 -97
- package/src/staking/matic/service.js +0 -437
- package/src/staking-api.js +0 -5
- package/src/tx-log/clarity-utils/get-derive-data-needed-for-tick.js +0 -12
- package/src/tx-log-staking-processor/get-asset-tx-amount.js +0 -26
- package/src/tx-send/get-fee-info.js +0 -41
- package/src/websocket/index.android.js +0 -2
- package/src/websocket/index.ios.js +0 -2
- package/src/websocket/index.js +0 -21
|
@@ -1,43 +1,240 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
+
|
|
64
|
+
import { memoize } from '@exodus/basic-utils'
|
|
65
|
+
import NumberUnit from '@exodus/currency'
|
|
66
|
+
import assert from 'minimalistic-assert'
|
|
67
|
+
|
|
68
|
+
import { estimateGasLimit, scaleGasLimitEstimate } from '../../gas-estimation.js'
|
|
69
|
+
import { createWatchTx as defaultCreateWatch } from '../../watch-tx.js'
|
|
70
|
+
import { stakingProviderClientFactory } from '../staking-provider-client.js'
|
|
71
|
+
import { amountToCurrency, DISABLE_BALANCE_CHECKS, resolveFeeData } from '../utils/index.js'
|
|
72
|
+
import { EthereumStaking } from './api.js'
|
|
73
|
+
import { getCachedEverstakeValidatorsQueue } from './everstake.js'
|
|
74
|
+
import { simulateUndelegateTransactions } from './staking-utils.js'
|
|
75
|
+
|
|
76
|
+
const WETH9_ADDRESS = '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'
|
|
77
|
+
|
|
78
|
+
// Transactions will usually stake values up to `1 ether`, so
|
|
79
|
+
// we'll assume this amount as a slippage protection mechanism,
|
|
80
|
+
// the idea being that this is a value of ether that we risk
|
|
81
|
+
// being mined whilst our transaction is in-flight.
|
|
82
|
+
const SLOT_ACTIVATION_PROXIMITY_ETH = '1.0'
|
|
83
|
+
|
|
84
|
+
// We've observed that most transactions which offer
|
|
85
|
+
// a healthy ratio between `gasUsed` had a `gasLimit`
|
|
86
|
+
// over `170_000`. Transactions which have an unhealthy
|
|
87
|
+
// ratio commonly fall under this limit.
|
|
88
|
+
const MINIMUM_DELEGATION_GAS_LIMIT = 180_000
|
|
89
|
+
|
|
90
|
+
const EXTRA_GAS_LIMIT = 20_000 // extra gas Limit to prevent tx failing if something change on pool state (till tx is in mempool)
|
|
91
|
+
|
|
92
|
+
// Estimates based on real life transaction averages.
|
|
93
|
+
// Overestimated averages by 4x.
|
|
94
|
+
const STAKING_OPERATION_FALLBACK_FEE_ESTIMATES = {
|
|
95
|
+
delegate: '0.00081512',
|
|
96
|
+
undelegate: '0.000554',
|
|
97
|
+
claimUndelegatedBalance: '0.0001800912',
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const getStakingApi = memoize(
|
|
101
|
+
(asset) => new EthereumStaking(asset.name, asset.currency, undefined, asset.server),
|
|
102
|
+
(asset) => asset
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
export async function getUndelegatePendingData({
|
|
106
|
+
staking,
|
|
107
|
+
delegatorAddress,
|
|
108
|
+
requestedAmount,
|
|
109
|
+
pendingAmount,
|
|
110
|
+
minAmount,
|
|
111
|
+
resolvedFeeData,
|
|
112
|
+
estimateTxFee,
|
|
113
|
+
}) {
|
|
114
|
+
if (!pendingAmount.isPositive) return null
|
|
115
|
+
|
|
116
|
+
const leftOver = pendingAmount.sub(requestedAmount)
|
|
117
|
+
|
|
118
|
+
if (leftOver.isPositive && leftOver.lt(minAmount)) return null
|
|
119
|
+
|
|
120
|
+
// Essentially the min of the 2 amounts
|
|
121
|
+
const inactiveAmountToUnstake = pendingAmount.lte(requestedAmount)
|
|
122
|
+
? pendingAmount
|
|
123
|
+
: requestedAmount
|
|
6
124
|
|
|
7
|
-
const
|
|
125
|
+
const { to, data } = await staking.unstakePending({
|
|
126
|
+
address: delegatorAddress,
|
|
127
|
+
amount: inactiveAmountToUnstake,
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
const feeInfo = estimateTxFee
|
|
131
|
+
? await estimateTxFee({
|
|
132
|
+
from: delegatorAddress,
|
|
133
|
+
to,
|
|
134
|
+
amount: null,
|
|
135
|
+
txInput: data,
|
|
136
|
+
feeData: resolvedFeeData,
|
|
137
|
+
})
|
|
138
|
+
: null
|
|
139
|
+
|
|
140
|
+
return { plan: { to, txData: data, amount: inactiveAmountToUnstake }, feeInfo }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export async function getUndelegateData({
|
|
144
|
+
staking,
|
|
145
|
+
delegatorAddress,
|
|
146
|
+
requestedAmount,
|
|
147
|
+
pendingAmount,
|
|
148
|
+
resolvedFeeData,
|
|
149
|
+
estimateTxFee,
|
|
150
|
+
}) {
|
|
151
|
+
const activeAmountToUnstake = requestedAmount.sub(pendingAmount)
|
|
152
|
+
|
|
153
|
+
if (!activeAmountToUnstake.isPositive) return null
|
|
154
|
+
|
|
155
|
+
const { to, data } = await staking.unstake({
|
|
156
|
+
address: delegatorAddress,
|
|
157
|
+
amount: activeAmountToUnstake,
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
const feeInfo = estimateTxFee
|
|
161
|
+
? await estimateTxFee({
|
|
162
|
+
from: delegatorAddress,
|
|
163
|
+
to,
|
|
164
|
+
amount: null,
|
|
165
|
+
txInput: data,
|
|
166
|
+
feeData: resolvedFeeData,
|
|
167
|
+
})
|
|
168
|
+
: null
|
|
169
|
+
|
|
170
|
+
return { plan: { to, txData: data, amount: activeAmountToUnstake }, feeInfo }
|
|
171
|
+
}
|
|
8
172
|
|
|
9
173
|
export function createEthereumStakingService({
|
|
10
|
-
asset,
|
|
174
|
+
asset: deprectedArg, // @deprecated use `assetName` instead
|
|
175
|
+
assetName,
|
|
11
176
|
assetClientInterface,
|
|
12
|
-
|
|
13
|
-
|
|
177
|
+
createWatchTx = defaultCreateWatch,
|
|
178
|
+
stakingProvider = stakingProviderClientFactory(),
|
|
14
179
|
}) {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
function amountToCurrency({ asset, amount }) {
|
|
19
|
-
return isNumberUnit(amount) ? amount : asset.currency.parse(amount)
|
|
180
|
+
if (deprectedArg && !assetName) {
|
|
181
|
+
console.debug('using deprecated ARG `asset` instead of `assetName`')
|
|
182
|
+
assetName = deprectedArg.name
|
|
20
183
|
}
|
|
21
184
|
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
185
|
+
const getAsset = memoize(async (baseAssetName) => {
|
|
186
|
+
const { [assetName]: asset } = await assetClientInterface.getAssetsForNetwork({ baseAssetName })
|
|
187
|
+
return asset
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
const getTransactionProps = async ({ feeData, walletAccount }) => {
|
|
191
|
+
const asset = await getAsset(assetName)
|
|
192
|
+
let address
|
|
193
|
+
;[address, feeData] = await Promise.all([
|
|
194
|
+
assetClientInterface.getReceiveAddress({
|
|
195
|
+
assetName,
|
|
196
|
+
walletAccount,
|
|
197
|
+
}),
|
|
198
|
+
resolveFeeData({ asset, assetClientInterface, feeData }),
|
|
199
|
+
])
|
|
200
|
+
|
|
27
201
|
const delegatorAddress = address.toLowerCase()
|
|
202
|
+
return { delegatorAddress, feeData }
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async function delegate({ walletAccount, amount, feeData, isDelegateAll }) {
|
|
206
|
+
const asset = await getAsset(assetName)
|
|
207
|
+
const staking = getStakingApi(asset)
|
|
28
208
|
amount = amountToCurrency({ asset, amount })
|
|
29
209
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
})
|
|
210
|
+
let delegatorAddress
|
|
211
|
+
;({ feeData, delegatorAddress } = await getTransactionProps({ feeData, walletAccount }))
|
|
33
212
|
|
|
34
|
-
const {
|
|
213
|
+
const { to, data } = await staking.stake({ amount })
|
|
35
214
|
|
|
36
|
-
console.
|
|
215
|
+
console.debug(
|
|
37
216
|
`delegator address ${delegatorAddress} staking ${amount.toDefaultString({
|
|
38
217
|
unit: true,
|
|
39
218
|
})}`
|
|
40
219
|
)
|
|
220
|
+
|
|
221
|
+
const { gasPrice, gasLimit, tipGasPrice, fee } = await estimateTxFee({
|
|
222
|
+
from: delegatorAddress,
|
|
223
|
+
to,
|
|
224
|
+
amount,
|
|
225
|
+
txInput: data,
|
|
226
|
+
feeData,
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
// If we are delegating all, we need to subtract the estimated ETH cost from the amount
|
|
230
|
+
// 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
|
+
}
|
|
237
|
+
|
|
41
238
|
const txId = await prepareAndSendTx({
|
|
42
239
|
asset,
|
|
43
240
|
walletAccount,
|
|
@@ -46,204 +243,484 @@ export function createEthereumStakingService({
|
|
|
46
243
|
txData: data,
|
|
47
244
|
gasPrice,
|
|
48
245
|
gasLimit,
|
|
49
|
-
|
|
246
|
+
tipGasPrice,
|
|
247
|
+
feeData,
|
|
50
248
|
})
|
|
51
249
|
|
|
52
250
|
// Goerli is not supported
|
|
53
|
-
if (
|
|
251
|
+
if (assetName === 'ethereum') {
|
|
54
252
|
await stakingProvider.notifyStaking({
|
|
55
253
|
txId,
|
|
56
|
-
asset:
|
|
254
|
+
asset: assetName,
|
|
57
255
|
delegator: delegatorAddress,
|
|
58
256
|
amount: amount.toBaseString(),
|
|
59
257
|
})
|
|
258
|
+
}
|
|
60
259
|
|
|
61
260
|
return txId
|
|
62
261
|
}
|
|
63
262
|
|
|
64
|
-
async function
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
amount = amountToCurrency({ asset, amount })
|
|
263
|
+
async function prepareUndelegate({ walletAccount, amount, feeData }) {
|
|
264
|
+
assert(amount instanceof NumberUnit, 'expected amount to be a NumberUnit')
|
|
265
|
+
const asset = await getAsset(assetName)
|
|
266
|
+
const staking = getStakingApi(asset)
|
|
267
|
+
const minAmount = getMinAmount(asset)
|
|
268
|
+
const requestedAmount = amount
|
|
71
269
|
|
|
72
|
-
const address = await
|
|
73
|
-
|
|
270
|
+
const [address, resolvedFeeData] = await Promise.all([
|
|
271
|
+
assetClientInterface.getReceiveAddress({
|
|
272
|
+
assetName,
|
|
273
|
+
walletAccount,
|
|
274
|
+
}),
|
|
275
|
+
resolveFeeData({ asset, assetClientInterface, feeData }),
|
|
276
|
+
])
|
|
277
|
+
const delegatorAddress = address.toLowerCase()
|
|
278
|
+
const pendingAmount = await staking.pendingBalanceOf(delegatorAddress)
|
|
279
|
+
const baseNonce = await asset.baseAsset.getNonce({
|
|
280
|
+
asset,
|
|
281
|
+
fromAddress: delegatorAddress,
|
|
74
282
|
walletAccount,
|
|
75
283
|
})
|
|
76
|
-
|
|
284
|
+
|
|
285
|
+
return {
|
|
286
|
+
asset,
|
|
287
|
+
staking,
|
|
288
|
+
minAmount,
|
|
289
|
+
requestedAmount,
|
|
290
|
+
feeData: resolvedFeeData,
|
|
291
|
+
delegatorAddress,
|
|
292
|
+
pendingAmount,
|
|
293
|
+
baseNonce,
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Unstaking may involve 2 txs; unstaking from pending and unstaking from validator.
|
|
299
|
+
* Fee estimation depends on the executed txs. Can be both.
|
|
300
|
+
* @returns total undelegete fee
|
|
301
|
+
*/
|
|
302
|
+
async function estimateUndelegate({ walletAccount, amount: requestedAmount, feeData }) {
|
|
303
|
+
assert(requestedAmount instanceof NumberUnit, 'expected amount to be a NumberUnit')
|
|
304
|
+
const asset = await getAsset(assetName)
|
|
305
|
+
const staking = getStakingApi(asset)
|
|
306
|
+
const minAmount = getMinAmount(asset)
|
|
307
|
+
|
|
308
|
+
let delegatorAddress
|
|
309
|
+
;({ feeData, delegatorAddress } = await getTransactionProps({ feeData, walletAccount }))
|
|
77
310
|
|
|
78
311
|
const pendingAmount = await staking.pendingBalanceOf(delegatorAddress)
|
|
79
312
|
|
|
80
|
-
|
|
81
|
-
|
|
313
|
+
let pendingResult = null
|
|
314
|
+
try {
|
|
315
|
+
pendingResult = await getUndelegatePendingData({
|
|
316
|
+
staking,
|
|
317
|
+
delegatorAddress,
|
|
318
|
+
requestedAmount,
|
|
319
|
+
pendingAmount,
|
|
320
|
+
minAmount,
|
|
321
|
+
resolvedFeeData: feeData,
|
|
322
|
+
estimateTxFee,
|
|
323
|
+
})
|
|
324
|
+
} catch (err) {
|
|
325
|
+
console.warn('ETH unstake pending estimation failed, continuing with unstake', err)
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const undelegateResult = await getUndelegateData({
|
|
329
|
+
staking,
|
|
330
|
+
delegatorAddress,
|
|
331
|
+
requestedAmount,
|
|
332
|
+
pendingAmount,
|
|
333
|
+
resolvedFeeData: feeData,
|
|
334
|
+
estimateTxFee,
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
const pendingFee = pendingResult?.feeInfo?.fee ?? asset.currency.ZERO
|
|
338
|
+
const undelegateFee = undelegateResult?.feeInfo?.fee ?? asset.currency.ZERO
|
|
339
|
+
|
|
340
|
+
return pendingFee.add(undelegateFee)
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// NOTE: Like the legacy `undelegate()` flow, this planner currently splits
|
|
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 }) {
|
|
349
|
+
const {
|
|
350
|
+
asset,
|
|
351
|
+
staking,
|
|
352
|
+
minAmount,
|
|
353
|
+
requestedAmount,
|
|
354
|
+
feeData: resolvedFeeData,
|
|
355
|
+
delegatorAddress,
|
|
356
|
+
pendingAmount,
|
|
357
|
+
baseNonce,
|
|
358
|
+
} = await prepareUndelegate({ walletAccount, amount, feeData })
|
|
359
|
+
|
|
360
|
+
if (!requestedAmount.isPositive) {
|
|
361
|
+
throw new Error('Undelegate amount must be positive')
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const txSteps = {
|
|
365
|
+
undelegatePending: {
|
|
366
|
+
plan: null,
|
|
367
|
+
gasLimit: null,
|
|
368
|
+
unsignedTx: null,
|
|
369
|
+
txId: null,
|
|
370
|
+
},
|
|
371
|
+
undelegate: {
|
|
372
|
+
plan: null,
|
|
373
|
+
gasLimit: null,
|
|
374
|
+
unsignedTx: null,
|
|
375
|
+
txId: null,
|
|
376
|
+
},
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
console.debug(
|
|
380
|
+
`delegator address ${delegatorAddress} unstaking ${requestedAmount.toDefaultString({
|
|
82
381
|
unit: true,
|
|
83
382
|
})} - pending amount: ${pendingAmount.toDefaultString({ unit: true })}`
|
|
84
383
|
)
|
|
85
384
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
385
|
+
// 1. Plan transactions and estimate fees
|
|
386
|
+
const pendingResult = await getUndelegatePendingData({
|
|
387
|
+
staking,
|
|
388
|
+
delegatorAddress,
|
|
389
|
+
requestedAmount,
|
|
390
|
+
pendingAmount,
|
|
391
|
+
minAmount,
|
|
392
|
+
resolvedFeeData,
|
|
393
|
+
estimateTxFee,
|
|
394
|
+
})
|
|
96
395
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
396
|
+
const undelegateResult = await getUndelegateData({
|
|
397
|
+
staking,
|
|
398
|
+
delegatorAddress,
|
|
399
|
+
requestedAmount,
|
|
400
|
+
pendingAmount,
|
|
401
|
+
resolvedFeeData,
|
|
402
|
+
estimateTxFee,
|
|
403
|
+
})
|
|
101
404
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
to,
|
|
105
|
-
null,
|
|
106
|
-
data
|
|
107
|
-
)
|
|
108
|
-
txId = await prepareAndSendTx({
|
|
109
|
-
asset,
|
|
110
|
-
walletAccount,
|
|
111
|
-
to,
|
|
112
|
-
txData: data,
|
|
113
|
-
gasPrice,
|
|
114
|
-
gasLimit,
|
|
115
|
-
fee,
|
|
116
|
-
})
|
|
405
|
+
if (!pendingResult && !undelegateResult) {
|
|
406
|
+
throw new Error('Requested undelegation produced no transactions')
|
|
117
407
|
}
|
|
118
408
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const activeAmountToUnstake = amount.sub(pendingAmount)
|
|
122
|
-
const { to, data } = await staking.unstake({
|
|
123
|
-
address: delegatorAddress,
|
|
124
|
-
amount: activeAmountToUnstake,
|
|
125
|
-
})
|
|
409
|
+
let gasPrice
|
|
410
|
+
let tipGasPrice
|
|
126
411
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
txData: data,
|
|
133
|
-
gasPrice,
|
|
134
|
-
gasLimit,
|
|
135
|
-
fee,
|
|
136
|
-
})
|
|
412
|
+
if (pendingResult) {
|
|
413
|
+
txSteps.undelegatePending.plan = pendingResult.plan
|
|
414
|
+
txSteps.undelegatePending.gasLimit = pendingResult.feeInfo.gasLimit
|
|
415
|
+
gasPrice = pendingResult.feeInfo.gasPrice
|
|
416
|
+
tipGasPrice = pendingResult.feeInfo.tipGasPrice
|
|
137
417
|
}
|
|
138
418
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
amount: amount.toBaseString(),
|
|
146
|
-
})
|
|
147
|
-
|
|
148
|
-
return txId
|
|
149
|
-
}
|
|
419
|
+
if (undelegateResult) {
|
|
420
|
+
txSteps.undelegate.plan = undelegateResult.plan
|
|
421
|
+
txSteps.undelegate.gasLimit = undelegateResult.feeInfo.gasLimit
|
|
422
|
+
gasPrice = gasPrice || undelegateResult.feeInfo.gasPrice
|
|
423
|
+
tipGasPrice = tipGasPrice || undelegateResult.feeInfo.tipGasPrice
|
|
424
|
+
}
|
|
150
425
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
assetName: asset.name,
|
|
426
|
+
// 3. Create unsigned txs
|
|
427
|
+
const createTxBaseArgs = {
|
|
428
|
+
asset,
|
|
155
429
|
walletAccount,
|
|
430
|
+
fromAddress: delegatorAddress,
|
|
431
|
+
amount: asset.currency.ZERO,
|
|
432
|
+
tipGasPrice,
|
|
433
|
+
gasPrice,
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const createdTxEntries = await Promise.all(
|
|
437
|
+
Object.entries(txSteps)
|
|
438
|
+
.filter(([, txStep]) => txStep.plan)
|
|
439
|
+
.map(async ([key, txStep], index) => {
|
|
440
|
+
const result = await asset.baseAsset.api.createTx({
|
|
441
|
+
...createTxBaseArgs,
|
|
442
|
+
toAddress: txStep.plan.to,
|
|
443
|
+
txInput: txStep.plan.txData,
|
|
444
|
+
gasLimit: txStep.gasLimit,
|
|
445
|
+
nonce: baseNonce + index,
|
|
446
|
+
})
|
|
447
|
+
return [key, result.unsignedTx]
|
|
448
|
+
})
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
for (const [key, unsignedTx] of createdTxEntries) {
|
|
452
|
+
txSteps[key].unsignedTx = unsignedTx
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
await simulateUndelegateTransactions({
|
|
456
|
+
asset,
|
|
457
|
+
txSteps,
|
|
458
|
+
senderAddress: delegatorAddress,
|
|
459
|
+
gasPrice,
|
|
460
|
+
revertOnSimulationError,
|
|
156
461
|
})
|
|
157
|
-
|
|
462
|
+
|
|
463
|
+
const unsignedTxBundle = createdTxEntries.map(([_, unsignedTx]) => ({
|
|
464
|
+
asset,
|
|
465
|
+
unsignedTx,
|
|
466
|
+
walletAccount,
|
|
467
|
+
}))
|
|
468
|
+
|
|
469
|
+
// 4. Sign transactions
|
|
470
|
+
//
|
|
471
|
+
// 5. Broadcast transactions via bundle and get bundle hash
|
|
472
|
+
//
|
|
473
|
+
// 6. Optimistic tx-log effects: reflect tx presence and fee consumption only.
|
|
474
|
+
// The ETH returned to the user via the contract's internal transfer is not
|
|
475
|
+
// captured here — it will be picked up once the tx is confirmed on-chain.
|
|
476
|
+
// It would also be incorrect to optimistically log the full requested ETH
|
|
477
|
+
// as immediately received: some portion may instead be routed into the
|
|
478
|
+
// undelegation queue / withdraw-request system and only become claimable
|
|
479
|
+
// much later after validator exit.
|
|
480
|
+
//
|
|
481
|
+
// We now have a more deterministic model for these incoming ETH amounts,
|
|
482
|
+
// but it requires staking-operation context that getOptimisticTxLogEffects()
|
|
483
|
+
// does not currently accept:
|
|
484
|
+
//
|
|
485
|
+
// - unstakePending: the ETH returned is exactly the amount passed to
|
|
486
|
+
// unstakePending(). In this flow that amount is chosen from the pre-send
|
|
487
|
+
// pending balance and the requested undelegation amount:
|
|
488
|
+
// amountPassedToUnstakePending = min(totalRequestedAmount, pendingBalanceBefore)
|
|
489
|
+
// - unstake: the ETH returned can be derived only from staking state before
|
|
490
|
+
// and after the operation:
|
|
491
|
+
// amountPassedToUnstake = totalRequestedAmount - pendingBalanceBefore
|
|
492
|
+
// queuedFromUnstake = unclaimedUndelegatedBalanceAfter - unclaimedUndelegatedBalanceBefore
|
|
493
|
+
// returnedImmediatelyFromUnstake = amountPassedToUnstake - queuedFromUnstake
|
|
494
|
+
//
|
|
495
|
+
// That is incompatible with the current tx-log side-effect hook, which only
|
|
496
|
+
// receives tx-local data (method id, calldata, nonce, gas, etc.) and not
|
|
497
|
+
// staking snapshots or precomputed ETH-return amounts.
|
|
498
|
+
const {
|
|
499
|
+
bundleHash,
|
|
500
|
+
txIds: [...txIds],
|
|
501
|
+
} = await asset.baseAsset.sendPrivateBundle({ unsignedTxBundle })
|
|
502
|
+
|
|
503
|
+
for (const txStep of Object.values(txSteps)) {
|
|
504
|
+
if (!txStep.unsignedTx) continue
|
|
505
|
+
assert((txStep.txId = txIds.shift()), 'expected txId')
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Testnet assets do not support delegations tracking
|
|
509
|
+
if (assetName === 'ethereum') {
|
|
510
|
+
const notify = (txStep) =>
|
|
511
|
+
stakingProvider.notifyUnstaking({
|
|
512
|
+
txId: txStep.txId,
|
|
513
|
+
asset: assetName,
|
|
514
|
+
delegator: delegatorAddress,
|
|
515
|
+
amount: txStep.plan.amount.toBaseString(),
|
|
516
|
+
})
|
|
517
|
+
|
|
518
|
+
await Promise.all([
|
|
519
|
+
txSteps.undelegatePending.txId && notify(txSteps.undelegatePending),
|
|
520
|
+
txSteps.undelegate.txId && notify(txSteps.undelegate),
|
|
521
|
+
])
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// This is the most correct response, but it doesn't matter, it is not consumed by the apps
|
|
525
|
+
return bundleHash
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
async function claimUndelegatedBalance({ walletAccount, feeData }) {
|
|
529
|
+
const asset = await getAsset(assetName)
|
|
530
|
+
const staking = getStakingApi(asset)
|
|
531
|
+
|
|
532
|
+
let delegatorAddress
|
|
533
|
+
;({ feeData, delegatorAddress } = await getTransactionProps({ feeData, walletAccount }))
|
|
158
534
|
|
|
159
535
|
const withdrawRequest = await staking.claimWithdrawRequest({
|
|
160
536
|
address: delegatorAddress,
|
|
161
537
|
})
|
|
538
|
+
|
|
162
539
|
if (withdrawRequest) {
|
|
163
540
|
const { to, data } = withdrawRequest
|
|
164
|
-
|
|
541
|
+
|
|
542
|
+
const { gasLimit, gasPrice, tipGasPrice } = await estimateTxFee({
|
|
543
|
+
from: delegatorAddress,
|
|
544
|
+
to,
|
|
545
|
+
amount: null,
|
|
546
|
+
txInput: data,
|
|
547
|
+
feeData,
|
|
548
|
+
})
|
|
165
549
|
return prepareAndSendTx({
|
|
166
550
|
asset,
|
|
167
551
|
walletAccount,
|
|
168
552
|
to,
|
|
169
553
|
txData: data,
|
|
170
|
-
gasPrice,
|
|
171
554
|
gasLimit,
|
|
172
|
-
|
|
555
|
+
gasPrice,
|
|
556
|
+
tipGasPrice,
|
|
557
|
+
feeData,
|
|
173
558
|
})
|
|
174
559
|
}
|
|
175
|
-
|
|
176
|
-
return null // -> no withdrawable balance
|
|
177
560
|
}
|
|
178
561
|
|
|
179
|
-
async function estimateDelegateOperation({ walletAccount, operation, args }) {
|
|
180
|
-
const
|
|
181
|
-
assetName: asset.name,
|
|
182
|
-
walletAccount,
|
|
183
|
-
})
|
|
184
|
-
const delegatorAddress = address.toLowerCase()
|
|
562
|
+
async function estimateDelegateOperation({ walletAccount, operation, args, feeData }) {
|
|
563
|
+
const asset = await getAsset(assetName)
|
|
185
564
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
undelegate: 'unstake',
|
|
189
|
-
claimUndelegatedBalance: 'claimWithdrawRequest',
|
|
190
|
-
}
|
|
191
|
-
const delegateOperation = staking[NAMING_MAP[operation]].bind(staking)
|
|
565
|
+
try {
|
|
566
|
+
const staking = getStakingApi(asset)
|
|
192
567
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
568
|
+
const requestedAmount = args.amount
|
|
569
|
+
? amountToCurrency({ asset, amount: args.amount })
|
|
570
|
+
: asset.currency.ZERO
|
|
196
571
|
|
|
197
|
-
|
|
198
|
-
|
|
572
|
+
let delegatorAddress
|
|
573
|
+
;({ delegatorAddress, feeData } = await getTransactionProps({ feeData, walletAccount }))
|
|
199
574
|
|
|
200
|
-
let amount, data, fee
|
|
201
|
-
try {
|
|
202
|
-
;({ amount, data } = await delegateOperation(args))
|
|
203
|
-
const address =
|
|
204
|
-
operation === 'claimUndelegatedBalance' ? staking.accountingAddress : staking.poolAddress
|
|
205
|
-
;({ fee } = await estimateTxFee(delegatorAddress, address, amount, data))
|
|
206
|
-
} catch (e) {
|
|
207
|
-
// If the operation is to unstake and failed retry with unstakePending
|
|
208
575
|
if (operation === 'undelegate') {
|
|
209
|
-
|
|
210
|
-
;({ fee } = await estimateTxFee(delegatorAddress, staking.poolAddress, amount, data))
|
|
211
|
-
} else {
|
|
212
|
-
throw e
|
|
576
|
+
return estimateUndelegate({ walletAccount, amount: requestedAmount, feeData })
|
|
213
577
|
}
|
|
214
|
-
}
|
|
215
578
|
|
|
216
|
-
|
|
579
|
+
const NAMING_MAP = {
|
|
580
|
+
delegate: 'stake',
|
|
581
|
+
undelegate: 'unstake',
|
|
582
|
+
claimUndelegatedBalance: 'claimWithdrawRequest',
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
const delegateOperation = staking[NAMING_MAP[operation]].bind(staking)
|
|
586
|
+
|
|
587
|
+
if (!delegateOperation) throw new Error('Invalid staking operation')
|
|
588
|
+
|
|
589
|
+
const { amount, data } = await delegateOperation({ ...args, amount: requestedAmount })
|
|
590
|
+
|
|
591
|
+
const { fee } = await (operation === 'claimUndelegatedBalance'
|
|
592
|
+
? estimateTxFee({
|
|
593
|
+
from: delegatorAddress,
|
|
594
|
+
to: staking.accountingAddress,
|
|
595
|
+
amount,
|
|
596
|
+
txInput: data,
|
|
597
|
+
feeData,
|
|
598
|
+
})
|
|
599
|
+
: // The `gasUsed` of a delegation transaction can vary
|
|
600
|
+
// significantly depending upon whether it will result
|
|
601
|
+
// in the activation of new slots (i.e. validator creation).
|
|
602
|
+
//
|
|
603
|
+
// This can result in transaction `revert` due to slippage
|
|
604
|
+
// from incompatible transaction ordering.
|
|
605
|
+
//
|
|
606
|
+
// To mitigate this, we:
|
|
607
|
+
// 1. Simulate the delegation at an amplified deposit amount
|
|
608
|
+
// to show enhanced fees close to a proximity buffer.
|
|
609
|
+
// 2. Originate the transaction from the WETH contract,
|
|
610
|
+
// which guarantees deep native ether liquidity which
|
|
611
|
+
// exceeds any rational user deposit.
|
|
612
|
+
estimateTxFee({
|
|
613
|
+
from: WETH9_ADDRESS,
|
|
614
|
+
to: staking.poolAddress,
|
|
615
|
+
amount: amount.add(asset.currency.defaultUnit(SLOT_ACTIVATION_PROXIMITY_ETH)),
|
|
616
|
+
txInput: data,
|
|
617
|
+
feeData,
|
|
618
|
+
}))
|
|
619
|
+
|
|
620
|
+
return fee
|
|
621
|
+
} catch (error) {
|
|
622
|
+
const fallbackFee = STAKING_OPERATION_FALLBACK_FEE_ESTIMATES[operation]
|
|
623
|
+
if (fallbackFee) {
|
|
624
|
+
console.warn(
|
|
625
|
+
`ETH staking fee estimation failed for '${operation}', using fallback:`,
|
|
626
|
+
error.message
|
|
627
|
+
)
|
|
628
|
+
return asset.currency.defaultUnit(fallbackFee)
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
error.message = `StakingFeeEstimationError: Failed to estimate fee for '${operation}' (and no fallback configured). Cause: ${error.message}`
|
|
632
|
+
throw error
|
|
633
|
+
}
|
|
217
634
|
}
|
|
218
635
|
|
|
219
|
-
async function estimateTxFee(from, to, amount, txInput,
|
|
636
|
+
async function estimateTxFee({ from, to, amount, txInput, feeData }) {
|
|
637
|
+
const asset = await getAsset(assetName)
|
|
638
|
+
|
|
220
639
|
amount = amount || asset.currency.ZERO
|
|
640
|
+
from = from.toLowerCase()
|
|
221
641
|
|
|
222
|
-
const
|
|
642
|
+
const estimatedGasLimit = await estimateGasLimit({
|
|
223
643
|
asset,
|
|
224
|
-
from
|
|
225
|
-
to.toLowerCase(),
|
|
644
|
+
fromAddress: from,
|
|
645
|
+
toAddress: to.toLowerCase(),
|
|
226
646
|
amount, // staking contracts does not always require ETH amount to interact with
|
|
227
|
-
txInput,
|
|
228
|
-
gasPrice
|
|
229
|
-
)
|
|
647
|
+
data: txInput,
|
|
648
|
+
gasPrice: DISABLE_BALANCE_CHECKS,
|
|
649
|
+
})
|
|
230
650
|
|
|
231
|
-
|
|
232
|
-
gasPrice = await server.gasPrice()
|
|
233
|
-
}
|
|
651
|
+
const scaledGasLimit = scaleGasLimitEstimate({ estimatedGasLimit })
|
|
234
652
|
|
|
235
|
-
|
|
236
|
-
|
|
653
|
+
const gasLimit = Math.max(
|
|
654
|
+
scaledGasLimit + EXTRA_GAS_LIMIT,
|
|
237
655
|
|
|
238
|
-
|
|
656
|
+
// For delgation transactions, we enforce an empirical
|
|
657
|
+
// `MINIMUM_DELEGATE_GAS_LIMIT`, since the majority of
|
|
658
|
+
// transactions which possess a healthy ratio of
|
|
659
|
+
// `gasUsed` to `gasLimit` operate at this boundary.
|
|
660
|
+
EthereumStaking.isDelegationTransactonCalldata(txInput) ? MINIMUM_DELEGATION_GAS_LIMIT : 0
|
|
661
|
+
)
|
|
662
|
+
|
|
663
|
+
const getFeeResult = await asset.api.getFee({
|
|
664
|
+
asset,
|
|
665
|
+
feeData,
|
|
239
666
|
gasLimit,
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
667
|
+
amount,
|
|
668
|
+
})
|
|
669
|
+
|
|
670
|
+
return { ...getFeeResult, gasLimit }
|
|
243
671
|
}
|
|
244
672
|
|
|
245
|
-
|
|
246
|
-
|
|
673
|
+
/** Returns the minimum possible amount that can be staked. */
|
|
674
|
+
function getMinAmount(asset) {
|
|
675
|
+
assert(asset, 'getMinAmount: `asset` is required')
|
|
676
|
+
return getStakingApi(asset).minAmount
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/** Determine the maximum possible stake for a given spendable amount. */
|
|
680
|
+
async function getDelegateSelectAllAmount({ walletAccount, spendableForStaking, feeData }) {
|
|
681
|
+
const asset = await getAsset(assetName)
|
|
682
|
+
const minAmount = getMinAmount(asset)
|
|
683
|
+
|
|
684
|
+
if (!spendableForStaking) spendableForStaking = asset.currency.ZERO
|
|
685
|
+
;({ feeData } = await getTransactionProps({ feeData, walletAccount }))
|
|
686
|
+
|
|
687
|
+
// If the caller hasn't specified a value of `spendableForStaking`
|
|
688
|
+
// which satisfies the minimum amount, then we'll coerce this value
|
|
689
|
+
// up to the minimum amount so we can at least provide a rational
|
|
690
|
+
// estimate for the `calculatedFee`, so they know how much ether
|
|
691
|
+
// to purchase.
|
|
692
|
+
spendableForStaking = spendableForStaking.lt(minAmount) ? minAmount : spendableForStaking
|
|
693
|
+
|
|
694
|
+
// Compute the cost of delegation. Even though the `walletAccount`'s
|
|
695
|
+
// balance is potentially insufficient, we can still provide
|
|
696
|
+
// rational fee estimates because the estimation process will mock
|
|
697
|
+
// the transaction from an account with deep liquidity.
|
|
698
|
+
const calculatedFee = await estimateDelegateOperation({
|
|
699
|
+
walletAccount,
|
|
700
|
+
operation: 'delegate',
|
|
701
|
+
args: {
|
|
702
|
+
amount: spendableForStaking,
|
|
703
|
+
},
|
|
704
|
+
feeData,
|
|
705
|
+
})
|
|
706
|
+
|
|
707
|
+
// If the `spendableForStaking` is insufficient to cover both the
|
|
708
|
+
// transaction fee and the minimum stake, then there is no reasonable
|
|
709
|
+
// value we can recommend.
|
|
710
|
+
if (calculatedFee.add(minAmount).gt(spendableForStaking)) {
|
|
711
|
+
return {
|
|
712
|
+
calculatedFee,
|
|
713
|
+
selectAllAmount: asset.currency.ZERO,
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// At this stage, we've confirmed that the remaining `spendableForStaking`
|
|
718
|
+
// after transactions is sufficient to cover the minimum stake, so any
|
|
719
|
+
// excess amounts over this can be.
|
|
720
|
+
return {
|
|
721
|
+
calculatedFee,
|
|
722
|
+
selectAllAmount: spendableForStaking.sub(calculatedFee),
|
|
723
|
+
}
|
|
247
724
|
}
|
|
248
725
|
|
|
249
726
|
async function prepareAndSendTx({
|
|
@@ -254,25 +731,35 @@ export function createEthereumStakingService({
|
|
|
254
731
|
txData: txInput,
|
|
255
732
|
gasPrice,
|
|
256
733
|
gasLimit,
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
734
|
+
tipGasPrice,
|
|
735
|
+
waitForConfirmation = false,
|
|
736
|
+
feeData,
|
|
737
|
+
}) {
|
|
738
|
+
const { unsignedTx } = await asset.baseAsset.api.createTx({
|
|
260
739
|
asset,
|
|
261
740
|
walletAccount,
|
|
262
741
|
address: to,
|
|
263
742
|
amount: amount || asset.currency.ZERO,
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
}
|
|
743
|
+
txInput,
|
|
744
|
+
gasPrice,
|
|
745
|
+
gasLimit,
|
|
746
|
+
// HACK: Override the `tipGasPrice` to use a custom `maxPriorityFeePerGas`.
|
|
747
|
+
tipGasPrice,
|
|
748
|
+
feeData,
|
|
749
|
+
})
|
|
272
750
|
|
|
273
|
-
|
|
751
|
+
const { txId } = await asset.baseAsset.api.sendTx({ asset, walletAccount, unsignedTx })
|
|
274
752
|
|
|
275
|
-
const
|
|
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
|
+
}
|
|
276
763
|
|
|
277
764
|
return txId
|
|
278
765
|
}
|
|
@@ -284,26 +771,33 @@ export function createEthereumStakingService({
|
|
|
284
771
|
getEthereumStakingInfo,
|
|
285
772
|
estimateDelegateOperation,
|
|
286
773
|
getMinAmount,
|
|
774
|
+
getDelegateSelectAllAmount,
|
|
287
775
|
}
|
|
288
776
|
}
|
|
289
777
|
|
|
290
|
-
export async function getEthereumStakingInfo({
|
|
778
|
+
export async function getEthereumStakingInfo({
|
|
779
|
+
address,
|
|
780
|
+
asset,
|
|
781
|
+
server,
|
|
782
|
+
staking = new EthereumStaking(asset.name, asset.currency, undefined, server),
|
|
783
|
+
useCache = false,
|
|
784
|
+
useQueueCache = useCache,
|
|
785
|
+
background = false,
|
|
786
|
+
}) {
|
|
291
787
|
const { currency } = asset
|
|
292
788
|
const delegator = address.toLowerCase()
|
|
293
|
-
const staking = new EthereumStaking({ asset, server })
|
|
294
|
-
|
|
295
789
|
const [
|
|
296
|
-
activeStakedBalance,
|
|
297
|
-
pendingBalance,
|
|
298
|
-
pendingDepositedBalance,
|
|
299
|
-
withdrawRequest,
|
|
790
|
+
{ activeStakedBalance, pendingBalance, pendingDepositedBalance, withdrawRequest },
|
|
300
791
|
rewardsBalance,
|
|
792
|
+
everstakeValidatorsQueue,
|
|
301
793
|
] = await Promise.all([
|
|
302
|
-
staking.
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
staking
|
|
306
|
-
|
|
794
|
+
staking.getStakingState(delegator),
|
|
795
|
+
// Provider availability must not prevent restoring on-chain stake. Omit
|
|
796
|
+
// failed metadata so the monitor preserves the last successful rewards.
|
|
797
|
+
staking
|
|
798
|
+
.getCachedTotalRewards(delegator, { useCache })
|
|
799
|
+
.catch(() => (background ? null : currency.ZERO)),
|
|
800
|
+
getCachedEverstakeValidatorsQueue('ethereum', { useCache: useQueueCache }).catch(() => null),
|
|
307
801
|
])
|
|
308
802
|
|
|
309
803
|
const delegatedBalance = activeStakedBalance.add(pendingBalance).add(pendingDepositedBalance)
|
|
@@ -323,7 +817,7 @@ export async function getEthereumStakingInfo({ address, asset, server }) {
|
|
|
323
817
|
const unclaimedUndelegatedBalance = requested
|
|
324
818
|
|
|
325
819
|
return {
|
|
326
|
-
rewardsBalance,
|
|
820
|
+
...(rewardsBalance !== null && { rewardsBalance }),
|
|
327
821
|
isDelegating,
|
|
328
822
|
activeStakedBalance, // balance staked into the validator
|
|
329
823
|
delegatedBalance, // balance staked into the validator + pending
|
|
@@ -333,5 +827,12 @@ export async function getEthereumStakingInfo({ address, asset, server }) {
|
|
|
333
827
|
unclaimedUndelegatedBalance,
|
|
334
828
|
canClaimUndelegatedBalance,
|
|
335
829
|
isUndelegateInProgress,
|
|
830
|
+
// Do not replace previous queue estimates with undefined on a transient failure.
|
|
831
|
+
...(!background && {
|
|
832
|
+
validatorActivationTime: undefined,
|
|
833
|
+
validatorExitTime: undefined,
|
|
834
|
+
validatorWithdrawTime: undefined,
|
|
835
|
+
}),
|
|
836
|
+
...everstakeValidatorsQueue,
|
|
336
837
|
}
|
|
337
838
|
}
|