@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,11 +1,24 @@
|
|
|
1
1
|
import { createContract } from '@exodus/ethereum-lib'
|
|
2
|
+
import { bufferToHex } from '@exodus/ethereumjs/util'
|
|
3
|
+
import { safeString } from '@exodus/safe-string'
|
|
2
4
|
import { retry } from '@exodus/simple-retry'
|
|
3
|
-
import { bufferToHex } from '@exodus/ethereumjs-util'
|
|
4
5
|
import assert from 'minimalistic-assert'
|
|
5
6
|
|
|
7
|
+
import { EVM_ERROR_REASONS, withErrorReason } from '../../error-wrapper.js'
|
|
8
|
+
import { EthLikeRpcRequestAccumulator } from '../../multicall3/index.js'
|
|
9
|
+
import { createCachedStakingRead } from '../cached-read.js'
|
|
10
|
+
|
|
11
|
+
// TODO: Shouldn't this be a function of `ethereumStakingState.minDelegateAmount`?
|
|
6
12
|
const MIN_AMOUNT = '0.1'
|
|
7
13
|
const RETRY_DELAYS = ['10s']
|
|
8
14
|
|
|
15
|
+
const EVERSTAKE_API_URL = 'https://eth-clarity.a.exodus.io/everstake-rewards'
|
|
16
|
+
|
|
17
|
+
export const UNSTAKE_DEFAULTS = {
|
|
18
|
+
allowedInterchangeNum: 0,
|
|
19
|
+
source: '2',
|
|
20
|
+
}
|
|
21
|
+
|
|
9
22
|
export class EthereumStaking {
|
|
10
23
|
static addresses = {
|
|
11
24
|
ethereum: {
|
|
@@ -16,10 +29,6 @@ export class EthereumStaking {
|
|
|
16
29
|
EVERSTAKE_ADDRESS_CONTRACT_ACCOUNTING: '0x6e95818C2Dde3d87406F5C5d0A759d9372053a94',
|
|
17
30
|
EVERSTAKE_ADDRESS_CONTRACT_POOL: '0x1F28aD3B3e26192a09c1248D4092c692c32f8Ec0',
|
|
18
31
|
},
|
|
19
|
-
ethereumholesky: {
|
|
20
|
-
EVERSTAKE_ADDRESS_CONTRACT_ACCOUNTING: '0x624087DD1904ab122A32878Ce9e933C7071F53B9',
|
|
21
|
-
EVERSTAKE_ADDRESS_CONTRACT_POOL: '0xAFA848357154a6a624686b348303EF9a13F63264',
|
|
22
|
-
},
|
|
23
32
|
}
|
|
24
33
|
|
|
25
34
|
static METHODS_IDS = {
|
|
@@ -27,54 +36,111 @@ export class EthereumStaking {
|
|
|
27
36
|
UNSTAKE: '0x76ec871c', // unstake(uint256 amount)
|
|
28
37
|
UNSTAKE_PENDING: '0xed0723d4', // unstakePending(uint256 amount)
|
|
29
38
|
CLAIM_UNSTAKE: '0x33986ffa', // claimWithdrawRequest(uint256 amount)
|
|
39
|
+
DEPOSIT: '0x47e7ef24', // deposit(address staker, uint256 amount)
|
|
30
40
|
}
|
|
31
41
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
42
|
+
static isDelegationTransactonCalldata = (txInput) => {
|
|
43
|
+
const txInputHex = (Buffer.isBuffer(txInput) ? bufferToHex(txInput) : txInput) || '0x'
|
|
44
|
+
if (typeof txInputHex !== 'string') throw new Error('expected string')
|
|
45
|
+
|
|
46
|
+
return txInputHex.toLowerCase().startsWith(EthereumStaking.METHODS_IDS.DELEGATE)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
constructor(assetName, currency, minAmount = MIN_AMOUNT, server) {
|
|
50
|
+
this.asset = { name: assetName, currency }
|
|
39
51
|
const accountingAddress =
|
|
40
|
-
EthereumStaking.addresses[
|
|
41
|
-
const poolAddress = EthereumStaking.addresses[
|
|
52
|
+
EthereumStaking.addresses[assetName].EVERSTAKE_ADDRESS_CONTRACT_ACCOUNTING
|
|
53
|
+
const poolAddress = EthereumStaking.addresses[assetName].EVERSTAKE_ADDRESS_CONTRACT_POOL
|
|
42
54
|
|
|
43
55
|
this.contractAccounting = createContract(accountingAddress, 'ethStakingAccounting')
|
|
44
56
|
this.contractPool = createContract(poolAddress, 'ethStakingPool')
|
|
45
57
|
this.accountingAddress = accountingAddress
|
|
46
58
|
this.poolAddress = poolAddress
|
|
47
|
-
this.minAmount =
|
|
59
|
+
this.minAmount = currency.defaultUnit(minAmount)
|
|
48
60
|
this.server = server
|
|
49
61
|
}
|
|
50
62
|
|
|
51
|
-
buildTxData = (contract, method, ...args) => {
|
|
63
|
+
#buildTxData = (contract, method, ...args) => {
|
|
52
64
|
return contract[method].build(...args)
|
|
53
65
|
}
|
|
54
66
|
|
|
55
|
-
callReadFunctionContract = (contract, method, ...args) =>
|
|
56
|
-
|
|
67
|
+
#callReadFunctionContract = (contract, method, ...args) =>
|
|
68
|
+
this.#callReadFunctionContractFrom(contract, method, undefined, ...args)
|
|
69
|
+
|
|
70
|
+
#callReadFunctionContractFrom = (contract, method, from, ...args) => {
|
|
71
|
+
const callData = this.#buildTxData(contract, method, ...args)
|
|
57
72
|
const data = {
|
|
58
73
|
data: bufferToHex(callData),
|
|
59
74
|
to: contract.address,
|
|
60
75
|
tag: 'latest',
|
|
61
76
|
}
|
|
62
77
|
|
|
78
|
+
if (typeof from === 'string' && from.length > 0) data.from = from
|
|
79
|
+
|
|
63
80
|
const eth = this.server
|
|
64
|
-
|
|
81
|
+
assert(eth, 'server is required')
|
|
82
|
+
return withErrorReason({
|
|
83
|
+
promise: retry((...args) => eth.ethCall(...args), { delayTimesMs: RETRY_DELAYS })(data),
|
|
84
|
+
errorReasonInfo: EVM_ERROR_REASONS.ethCallFailed,
|
|
85
|
+
hint: safeString`stakingEthReadFunctionContract`,
|
|
86
|
+
baseAssetName: eth.baseAssetName,
|
|
87
|
+
})
|
|
65
88
|
}
|
|
66
89
|
|
|
90
|
+
// Do not use Multicall for these reads without verifying the deployed Accounting
|
|
91
|
+
// implementation (currently unverified). JSON-RPC batching preserves msg.sender
|
|
92
|
+
// and isolated eth_call execution, including autocompound simulation semantics.
|
|
93
|
+
async getStakingState(address) {
|
|
94
|
+
const read = () => {
|
|
95
|
+
const requests = new EthLikeRpcRequestAccumulator()
|
|
96
|
+
for (const method of [
|
|
97
|
+
'autocompoundBalanceOf',
|
|
98
|
+
'pendingBalanceOf',
|
|
99
|
+
'pendingDepositedBalanceOf',
|
|
100
|
+
'withdrawRequest',
|
|
101
|
+
]) {
|
|
102
|
+
requests.contractStaticCallRequest(this.contractAccounting, method, address)
|
|
103
|
+
}
|
|
104
|
+
assert(this.server, 'server is required')
|
|
105
|
+
return requests.flush(this.server)
|
|
106
|
+
}
|
|
107
|
+
const [[active], [pending], [deposited], [requested, readyForClaim]] = await withErrorReason({
|
|
108
|
+
promise: retry(read, { delayTimesMs: RETRY_DELAYS })(),
|
|
109
|
+
errorReasonInfo: EVM_ERROR_REASONS.ethCallFailed,
|
|
110
|
+
hint: safeString`stakingEthReadFunctionContract`,
|
|
111
|
+
baseAssetName: this.asset.name,
|
|
112
|
+
})
|
|
113
|
+
const { currency } = this.asset
|
|
114
|
+
return {
|
|
115
|
+
activeStakedBalance: currency.baseUnit(active),
|
|
116
|
+
pendingBalance: currency.baseUnit(pending),
|
|
117
|
+
pendingDepositedBalance: currency.baseUnit(deposited),
|
|
118
|
+
withdrawRequest: {
|
|
119
|
+
requested: currency.baseUnit(requested),
|
|
120
|
+
readyForClaim: currency.baseUnit(readyForClaim),
|
|
121
|
+
canSendClaimRequest: requested !== '0' && requested === readyForClaim,
|
|
122
|
+
},
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
getCachedTotalRewards = createCachedStakingRead({
|
|
127
|
+
read: (address) => this.getTotalRewards(address, { throwOnError: true }),
|
|
128
|
+
})
|
|
129
|
+
|
|
67
130
|
// === ACCOUNTING ===
|
|
68
131
|
|
|
69
132
|
/** Return total deposited and activated pool balance */
|
|
70
133
|
async poolsBalance() {
|
|
71
|
-
const totalPoolBalance = await this
|
|
134
|
+
const totalPoolBalance = await this.#callReadFunctionContract(
|
|
135
|
+
this.contractAccounting,
|
|
136
|
+
'balance'
|
|
137
|
+
)
|
|
72
138
|
return this.asset.currency.baseUnit(totalPoolBalance)
|
|
73
139
|
}
|
|
74
140
|
|
|
75
141
|
/** Return pool pending balance. Always < 32 ETH */
|
|
76
142
|
async poolPendingBalance() {
|
|
77
|
-
const pendingBalance = await this
|
|
143
|
+
const pendingBalance = await this.#callReadFunctionContract(
|
|
78
144
|
this.contractAccounting,
|
|
79
145
|
'pendingBalance'
|
|
80
146
|
)
|
|
@@ -83,7 +149,7 @@ export class EthereumStaking {
|
|
|
83
149
|
|
|
84
150
|
/** Return user pending balance (waiting to be added from the Pool contract to the ETH2 validator) */
|
|
85
151
|
async pendingBalanceOf(address) {
|
|
86
|
-
let pendingBalance = await this
|
|
152
|
+
let pendingBalance = await this.#callReadFunctionContract(
|
|
87
153
|
this.contractAccounting,
|
|
88
154
|
'pendingBalanceOf',
|
|
89
155
|
address
|
|
@@ -94,7 +160,7 @@ export class EthereumStaking {
|
|
|
94
160
|
|
|
95
161
|
/** Return total user autocompound balance. Part of this balance could be in pending state after rewards autocompound */
|
|
96
162
|
async autocompoundBalanceOf(address) {
|
|
97
|
-
const userBalance = await this
|
|
163
|
+
const userBalance = await this.#callReadFunctionContract(
|
|
98
164
|
this.contractAccounting,
|
|
99
165
|
'autocompoundBalanceOf',
|
|
100
166
|
address
|
|
@@ -104,7 +170,7 @@ export class EthereumStaking {
|
|
|
104
170
|
|
|
105
171
|
/** Return user pending deposited balance. Balance which deposited into validator but not active yet. Pending deposited balance can't be unstake till validator activation */
|
|
106
172
|
async pendingDepositedBalanceOf(address) {
|
|
107
|
-
const userBalance = await this
|
|
173
|
+
const userBalance = await this.#callReadFunctionContract(
|
|
108
174
|
this.contractAccounting,
|
|
109
175
|
'pendingDepositedBalanceOf',
|
|
110
176
|
address
|
|
@@ -114,7 +180,7 @@ export class EthereumStaking {
|
|
|
114
180
|
|
|
115
181
|
/** Return user active origin deposited balance */
|
|
116
182
|
async depositedBalanceOf(address) {
|
|
117
|
-
const depositedBalance = await this
|
|
183
|
+
const depositedBalance = await this.#callReadFunctionContract(
|
|
118
184
|
this.contractAccounting,
|
|
119
185
|
'depositedBalanceOf',
|
|
120
186
|
address
|
|
@@ -122,8 +188,8 @@ export class EthereumStaking {
|
|
|
122
188
|
return this.asset.currency.baseUnit(depositedBalance)
|
|
123
189
|
}
|
|
124
190
|
|
|
125
|
-
/* Get earned Rewards */
|
|
126
191
|
async getLiquidRewards(address) {
|
|
192
|
+
// latest accrued rewards
|
|
127
193
|
const [compoundBalance, depositedBalance] = await Promise.all([
|
|
128
194
|
this.autocompoundBalanceOf(address),
|
|
129
195
|
this.depositedBalanceOf(address),
|
|
@@ -131,9 +197,42 @@ export class EthereumStaking {
|
|
|
131
197
|
return compoundBalance.sub(depositedBalance)
|
|
132
198
|
}
|
|
133
199
|
|
|
200
|
+
async getTotalRewards(address, { throwOnError = false } = {}) {
|
|
201
|
+
// use external everstake API to get total rewards
|
|
202
|
+
const url = new URL(EVERSTAKE_API_URL)
|
|
203
|
+
|
|
204
|
+
const options = {
|
|
205
|
+
method: 'POST',
|
|
206
|
+
headers: { 'Content-Type': 'application/json' },
|
|
207
|
+
body: JSON.stringify({
|
|
208
|
+
address,
|
|
209
|
+
}),
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
try {
|
|
213
|
+
const response = await fetch(url.toString(), options)
|
|
214
|
+
if (!response.ok) throw new Error('Failed to fetch ETH total rewards')
|
|
215
|
+
const rewards = await response.json()
|
|
216
|
+
if (rewards === 'address') return this.asset.currency.ZERO // address not found
|
|
217
|
+
if (
|
|
218
|
+
!(
|
|
219
|
+
(typeof rewards === 'string' && /^\d+$/.test(rewards)) ||
|
|
220
|
+
(Number.isSafeInteger(rewards) && rewards >= 0)
|
|
221
|
+
)
|
|
222
|
+
) {
|
|
223
|
+
throw new Error('Malformed ETH total rewards')
|
|
224
|
+
}
|
|
225
|
+
return this.asset.currency.baseUnit(rewards)
|
|
226
|
+
} catch (error) {
|
|
227
|
+
console.warn('Error fetching ETH total rewards:', error)
|
|
228
|
+
if (throwOnError) throw error
|
|
229
|
+
return this.asset.currency.ZERO
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
134
233
|
/** Return withdrawable unstaked amount */
|
|
135
234
|
async withdrawRequest(address) {
|
|
136
|
-
let amounts = await this
|
|
235
|
+
let amounts = await this.#callReadFunctionContract(
|
|
137
236
|
this.contractAccounting,
|
|
138
237
|
'withdrawRequest',
|
|
139
238
|
address
|
|
@@ -148,20 +247,53 @@ export class EthereumStaking {
|
|
|
148
247
|
|
|
149
248
|
// === POOL ===
|
|
150
249
|
|
|
250
|
+
/** Determine the effects of a deposit operation by mocking a transaction from the Everstake Pool contract, which can be used to estimate slippage. */
|
|
251
|
+
async getDepositEffects(depositorAddress, amount) {
|
|
252
|
+
const { address: from } = this.contractPool
|
|
253
|
+
const prankDepositReturnData = await this.#callReadFunctionContractFrom(
|
|
254
|
+
this.contractAccounting,
|
|
255
|
+
'deposit',
|
|
256
|
+
from,
|
|
257
|
+
depositorAddress,
|
|
258
|
+
amount.toBaseString()
|
|
259
|
+
)
|
|
260
|
+
const [interchangedAmount, activatedSlots] =
|
|
261
|
+
this.contractAccounting.deposit.parse(prankDepositReturnData)
|
|
262
|
+
|
|
263
|
+
return {
|
|
264
|
+
interchangedAmount: this.asset.currency.baseUnit(interchangedAmount),
|
|
265
|
+
activatedSlots: BigInt(activatedSlots),
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
151
269
|
async stake({ amount, source = '2' }) {
|
|
152
270
|
if (amount.gte(this.minAmount)) {
|
|
153
271
|
return {
|
|
154
272
|
to: this.poolAddress,
|
|
155
273
|
amount,
|
|
156
|
-
data: bufferToHex(this
|
|
274
|
+
data: bufferToHex(this.#buildTxData(this.contractPool, 'stake', source)), // tx data field
|
|
157
275
|
}
|
|
158
276
|
}
|
|
159
277
|
|
|
160
278
|
throw new Error(`Min Amount ${this.minAmount}`)
|
|
161
279
|
}
|
|
162
280
|
|
|
163
|
-
|
|
164
|
-
|
|
281
|
+
/**
|
|
282
|
+
* Unstake funds from the validator. The unstaked amount enters a withdrawal
|
|
283
|
+
* queue and must be claimed via `claimWithdrawRequest` after the validator exits.
|
|
284
|
+
*
|
|
285
|
+
* @param {number} allowedInterchangeNum - Max number of incoming stake requests
|
|
286
|
+
* the contract may use to immediately return ETH to the unstaker (interchange).
|
|
287
|
+
* When 0 (default), no interchange occurs and the full amount is queued.
|
|
288
|
+
* When > 0, the contract may match up to N new stakers' deposits against this
|
|
289
|
+
* unstake, returning ETH immediately without waiting for validator exit.
|
|
290
|
+
*/
|
|
291
|
+
async unstake({
|
|
292
|
+
address,
|
|
293
|
+
amount,
|
|
294
|
+
allowedInterchangeNum = UNSTAKE_DEFAULTS.allowedInterchangeNum,
|
|
295
|
+
source = UNSTAKE_DEFAULTS.source,
|
|
296
|
+
}) {
|
|
165
297
|
const amountWei = amount.toBaseString()
|
|
166
298
|
const balance = await this.autocompoundBalanceOf(address) // amount staked into the validator (active balance)
|
|
167
299
|
|
|
@@ -170,7 +302,7 @@ export class EthereumStaking {
|
|
|
170
302
|
to: this.poolAddress,
|
|
171
303
|
amount: this.asset.currency.ZERO,
|
|
172
304
|
data: bufferToHex(
|
|
173
|
-
this
|
|
305
|
+
this.#buildTxData(this.contractPool, 'unstake', amountWei, allowedInterchangeNum, source)
|
|
174
306
|
),
|
|
175
307
|
}
|
|
176
308
|
}
|
|
@@ -186,7 +318,7 @@ export class EthereumStaking {
|
|
|
186
318
|
return {
|
|
187
319
|
to: this.accountingAddress,
|
|
188
320
|
amount: this.asset.currency.ZERO,
|
|
189
|
-
data: bufferToHex(this
|
|
321
|
+
data: bufferToHex(this.#buildTxData(this.contractAccounting, 'claimWithdrawRequest')),
|
|
190
322
|
}
|
|
191
323
|
}
|
|
192
324
|
|
|
@@ -206,13 +338,13 @@ export class EthereumStaking {
|
|
|
206
338
|
return {
|
|
207
339
|
to: this.poolAddress,
|
|
208
340
|
amount: this.asset.currency.ZERO,
|
|
209
|
-
data: bufferToHex(this
|
|
341
|
+
data: bufferToHex(this.#buildTxData(this.contractPool, 'unstakePending', amountWei)),
|
|
210
342
|
}
|
|
211
343
|
} catch (err) {
|
|
212
344
|
throw new Error(err)
|
|
213
345
|
}
|
|
214
346
|
} else {
|
|
215
|
-
throw new Error(`
|
|
347
|
+
throw new Error(`Max Amount for unstakePending ${pendingBalance}`)
|
|
216
348
|
}
|
|
217
349
|
}
|
|
218
350
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { EthereumStaking } from './api.js'
|
|
2
|
+
import { createEthereumStakingService, getEthereumStakingInfo } from './service.js'
|
|
3
|
+
|
|
4
|
+
/** Staking server factory for ethereum (used by stakingApiFactory). */
|
|
5
|
+
export const stakingServerFactory = ({ assetName, currency, server, minAmount }) =>
|
|
6
|
+
new EthereumStaking(assetName, currency, minAmount, server)
|
|
7
|
+
|
|
8
|
+
/** Staking service factory for ethereum (used by stakingApiFactory). */
|
|
9
|
+
export const stakingServiceFactory = ({ assetClientInterface, server, stakingServer }) => {
|
|
10
|
+
const service = createEthereumStakingService({
|
|
11
|
+
assetName: stakingServer.asset.name,
|
|
12
|
+
assetClientInterface,
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
...service,
|
|
17
|
+
getStakingInfo: ({ address, useCache = false, useQueueCache = useCache, background = false }) =>
|
|
18
|
+
getEthereumStakingInfo({
|
|
19
|
+
address,
|
|
20
|
+
asset: stakingServer.asset,
|
|
21
|
+
server,
|
|
22
|
+
staking: stakingServer,
|
|
23
|
+
useCache,
|
|
24
|
+
useQueueCache,
|
|
25
|
+
background,
|
|
26
|
+
}),
|
|
27
|
+
stake: service.delegate,
|
|
28
|
+
unstake: service.undelegate,
|
|
29
|
+
claimUnstaked: service.claimUndelegatedBalance,
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Staking deps for ethereum base asset (plugin can pass as stakingDependencies.ethereum). */
|
|
34
|
+
export const ethereumStakingDeps = {
|
|
35
|
+
stakingServerFactory,
|
|
36
|
+
stakingServiceFactory,
|
|
37
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { fetch as exodusFetch } from '@exodus/fetch'
|
|
2
|
+
import { TraceId } from '@exodus/traceparent'
|
|
3
|
+
import assert from 'minimalistic-assert'
|
|
4
|
+
|
|
5
|
+
import { createCachedStakingRead } from '../cached-read.js'
|
|
6
|
+
import { UNSTAKE_DEFAULTS } from './api.js'
|
|
7
|
+
|
|
8
|
+
const BASE_URL = 'https://eth-clarity.a.exodus.io/api/v2/ethereum/proxy/everstake/'
|
|
9
|
+
const WALLET_SDK_BASE_URL = 'https://wallet-sdk-api.everstake.one/'
|
|
10
|
+
|
|
11
|
+
const fetch = async (path, config = Object.create(null)) => {
|
|
12
|
+
const url = new URL(path, BASE_URL).toString()
|
|
13
|
+
|
|
14
|
+
const response = await exodusFetch(url, config)
|
|
15
|
+
|
|
16
|
+
const newErrorWithTrace = (msg) => {
|
|
17
|
+
const error = new Error(msg)
|
|
18
|
+
error.traceId = TraceId.fromResponse(response)
|
|
19
|
+
return error
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (!response.ok) throw newErrorWithTrace(`failed to fetch ${path}`)
|
|
23
|
+
|
|
24
|
+
const data = await response.json()
|
|
25
|
+
|
|
26
|
+
if (!data || typeof data !== 'object') throw newErrorWithTrace('malformed response')
|
|
27
|
+
|
|
28
|
+
return data
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const isFiniteInteger = (e) => Number.isInteger(e) && Number.isFinite(e)
|
|
32
|
+
|
|
33
|
+
// Simulate an unstake to predict how much ETH the interchange pool returns instantly.
|
|
34
|
+
// https://wallet-sdk-api.everstake.one/swagger/#/Ethereum/post_ethereum_pool_simulate_unstake
|
|
35
|
+
//
|
|
36
|
+
// Inputs:
|
|
37
|
+
// address: staker address (0x...)
|
|
38
|
+
// amount: unstake amount in ETH (default units), e.g. "1.5" — NOT wei
|
|
39
|
+
// allowedInterchangeNum: interchange pool parameter (default from UNSTAKE_DEFAULTS)
|
|
40
|
+
// source: source identifier (default from UNSTAKE_DEFAULTS)
|
|
41
|
+
//
|
|
42
|
+
// Success (200): { result: <number> } — ETH returned instantly (capped by pool liquidity)
|
|
43
|
+
// Error (500): { "Internal Server Error": "Error: Max Amount For Unstake <max_eth>" }
|
|
44
|
+
// when amount exceeds the staker's active balance
|
|
45
|
+
export const simulateEverstakeUnstake = async ({
|
|
46
|
+
address,
|
|
47
|
+
amount,
|
|
48
|
+
allowedInterchangeNum = UNSTAKE_DEFAULTS.allowedInterchangeNum,
|
|
49
|
+
source = UNSTAKE_DEFAULTS.source,
|
|
50
|
+
}) => {
|
|
51
|
+
try {
|
|
52
|
+
assert(typeof address === 'string' && address.startsWith('0x'), 'expected valid address')
|
|
53
|
+
assert(typeof amount === 'string' && amount.length > 0, 'expected amount in ETH as string')
|
|
54
|
+
|
|
55
|
+
const url = new URL('ethereum/pool/simulate_unstake', WALLET_SDK_BASE_URL).toString()
|
|
56
|
+
|
|
57
|
+
const response = await exodusFetch(url, {
|
|
58
|
+
method: 'POST',
|
|
59
|
+
headers: { 'Content-Type': 'application/json' },
|
|
60
|
+
body: JSON.stringify({ address, amount, allowedInterchangeNum, source }),
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
if (!response.ok) {
|
|
64
|
+
const body = await response.json().catch(() => null)
|
|
65
|
+
const serverMsg = body?.['Internal Server Error'] || response.statusText
|
|
66
|
+
console.warn(`simulate_unstake failed: ${serverMsg}`)
|
|
67
|
+
return null
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const data = await response.json()
|
|
71
|
+
if (!data || typeof data.result !== 'number') {
|
|
72
|
+
console.warn('simulate_unstake: malformed response', data)
|
|
73
|
+
return null
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return { instantReturnEth: data.result }
|
|
77
|
+
} catch (e) {
|
|
78
|
+
console.warn('simulate_unstake error:', e.message)
|
|
79
|
+
return null
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// The queue is global, not delegator-specific. Share successful background reads
|
|
84
|
+
// across wallets; explicit refreshes bypass both its deadline and in-flight work.
|
|
85
|
+
export const getCachedEverstakeValidatorsQueue = createCachedStakingRead({
|
|
86
|
+
read: () => getEverstakeValidatorsQueue(),
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
export const getEverstakeValidatorsQueue = async () => {
|
|
90
|
+
// https://swagger.eth-api-b2c.everstake.one/#/Staking/validatorsQueue
|
|
91
|
+
const result = await fetch('v1/validators/queue')
|
|
92
|
+
|
|
93
|
+
const {
|
|
94
|
+
// Estimated time in seconds of delay before validator become active
|
|
95
|
+
validator_activation_time: validatorActivationTime,
|
|
96
|
+
// Estimated time in seconds of delay before validator become exited
|
|
97
|
+
validator_exit_time: validatorExitTime,
|
|
98
|
+
// Withdraw period in seconds from Beacon chain
|
|
99
|
+
validator_withdraw_time: validatorWithdrawTime,
|
|
100
|
+
} = result
|
|
101
|
+
|
|
102
|
+
assert(isFiniteInteger(validatorActivationTime), 'expected integer validatorActivationTime')
|
|
103
|
+
assert(isFiniteInteger(validatorExitTime), 'expected integer validatorExitTime')
|
|
104
|
+
assert(isFiniteInteger(validatorWithdrawTime), 'expected integer validatorWithdrawTime')
|
|
105
|
+
|
|
106
|
+
// NOTE: We convert the values into milliseconds for
|
|
107
|
+
// normalized handling on clients.
|
|
108
|
+
return {
|
|
109
|
+
validatorActivationTime: validatorActivationTime * 1000,
|
|
110
|
+
validatorExitTime: validatorExitTime * 1000,
|
|
111
|
+
validatorWithdrawTime: validatorWithdrawTime * 1000,
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
export * from './staking-utils'
|
|
2
|
-
export * from './api'
|
|
3
|
-
export * from './service'
|
|
1
|
+
export * from './staking-utils.js'
|
|
2
|
+
export * from './api.js'
|
|
3
|
+
export * from './service.js'
|
|
4
|
+
export { ethereumStakingDeps } from './deps.js'
|