@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.
Files changed (143) hide show
  1. package/CHANGELOG.md +2121 -0
  2. package/LICENSE +7 -0
  3. package/README.md +124 -0
  4. package/package.json +34 -33
  5. package/src/address-has-history.js +6 -2
  6. package/src/allowance/constants.js +4 -5
  7. package/src/allowance/index.js +159 -40
  8. package/src/check-tx/create-assess-transaction.js +23 -0
  9. package/src/check-tx/create-check-tx.js +104 -0
  10. package/src/check-tx/index.js +1 -0
  11. package/src/create-asset-plugin-factory.js +115 -0
  12. package/src/create-asset-utils.js +520 -0
  13. package/src/create-asset.js +272 -97
  14. package/src/create-token-factory.js +47 -17
  15. package/src/custom-fees.js +57 -0
  16. package/src/eip7702-whitelist.js +62 -0
  17. package/src/ens/index.js +12 -17
  18. package/src/error-wrapper.js +383 -0
  19. package/src/eth-like-util.js +172 -33
  20. package/src/exodus-eth-server/api-coin-nodes.js +27 -196
  21. package/src/exodus-eth-server/api.js +23 -14
  22. package/src/exodus-eth-server/clarity-v2.js +255 -0
  23. package/src/exodus-eth-server/clarity.js +67 -265
  24. package/src/exodus-eth-server/errors.js +40 -0
  25. package/src/exodus-eth-server/eth-like-server.js +385 -0
  26. package/src/exodus-eth-server/fetch-json.js +48 -0
  27. package/src/exodus-eth-server/index.js +77 -0
  28. package/src/exodus-eth-server/utils.js +31 -0
  29. package/src/exodus-eth-server/ws-gateway.js +306 -0
  30. package/src/exodus-eth-server/ws.js +1 -2
  31. package/src/fee-data/EthLikeFeeData.js +81 -0
  32. package/src/fee-data/index.js +54 -0
  33. package/src/fee-utils.js +229 -0
  34. package/src/gas-estimation.js +183 -66
  35. package/src/get-balance-for-address.js +1 -1
  36. package/src/get-balances.js +285 -27
  37. package/src/get-fee.js +92 -56
  38. package/src/get-historical-balance.js +58 -0
  39. package/src/hooks/index.js +1 -1
  40. package/src/hooks/monitor.js +182 -43
  41. package/src/hooks/staking-freshness.js +58 -0
  42. package/src/index.js +118 -12
  43. package/src/move-funds.js +153 -0
  44. package/src/multicall3/index.js +212 -0
  45. package/src/multisig-address.js +146 -0
  46. package/src/nft-utils.js +60 -0
  47. package/src/number-utils.js +34 -2
  48. package/src/optimism-gas/index.js +13 -5
  49. package/src/send-validations.js +58 -0
  50. package/src/server-based-fee-monitor.js +12 -19
  51. package/src/simulate-tx/fetch-tx-preview.js +0 -2
  52. package/src/simulate-tx/index.js +2 -2
  53. package/src/simulate-tx/simulate-eth-tx.js +24 -11
  54. package/src/simulation/common.js +34 -0
  55. package/src/simulation/create-simulate-message.js +49 -0
  56. package/src/simulation/create-simulate-transactions.js +108 -0
  57. package/src/simulation/estimate-fee.js +14 -0
  58. package/src/simulation/estimate-simple-transfer.js +15 -0
  59. package/src/simulation/get-message-type.js +18 -0
  60. package/src/simulation/simulate-message-api.js +68 -0
  61. package/src/simulation/simulate-transactions-api.js +266 -0
  62. package/src/simulation/simulate-transactions.js +16 -0
  63. package/src/simulation/transactions.js +52 -0
  64. package/src/simulation/try-estimating-changes-locally.js +26 -0
  65. package/src/staking/api/index.js +43 -0
  66. package/src/staking/cached-read.js +33 -0
  67. package/src/staking/ethereum/api.js +166 -34
  68. package/src/staking/ethereum/deps.js +37 -0
  69. package/src/staking/ethereum/everstake.js +113 -0
  70. package/src/staking/ethereum/index.js +4 -3
  71. package/src/staking/ethereum/service.js +682 -181
  72. package/src/staking/ethereum/staking-utils.js +275 -4
  73. package/src/staking/fantom-staking.js +9 -9
  74. package/src/staking/index.js +4 -4
  75. package/src/staking/matic/addresses.js +2 -0
  76. package/src/staking/matic/api.js +69 -49
  77. package/src/staking/matic/index.js +3 -3
  78. package/src/staking/matic/matic-staking-utils.js +243 -5
  79. package/src/staking/matic/matic-staking.md +53 -0
  80. package/src/staking/staking-provider-client.js +1 -1
  81. package/src/staking/utils/index.js +39 -0
  82. package/src/tx-create.js +666 -0
  83. package/src/tx-log/clarity-monitor.js +273 -103
  84. package/src/tx-log/clarity-truncated-history-monitor.js +109 -0
  85. package/src/tx-log/clarity-truncated-history-monitor.md +219 -0
  86. package/src/tx-log/clarity-utils/absolute.js +96 -0
  87. package/src/tx-log/clarity-utils/get-log-items-from-server-tx.js +94 -31
  88. package/src/tx-log/clarity-utils/get-names-of-tokens-transferred-by-server-tx.js +3 -2
  89. package/src/tx-log/clarity-utils/index.js +9 -3
  90. package/src/tx-log/clarity-utils/is-spam-pending-tx.js +13 -0
  91. package/src/tx-log/clarity-utils/normalize-transactions-response.js +65 -0
  92. package/src/tx-log/ethereum-monitor.js +93 -47
  93. package/src/tx-log/ethereum-no-history-monitor.js +52 -41
  94. package/src/tx-log/get-optimistic-txlog-effects.js +379 -0
  95. package/src/tx-log/index.js +5 -3
  96. package/src/tx-log/monitor-utils/check-pending-transactions.js +14 -5
  97. package/src/tx-log/monitor-utils/exclude-unchanged-token-balances.js +1 -1
  98. package/src/tx-log/monitor-utils/extract-balance-from-clarity-account-info.js +108 -0
  99. package/src/tx-log/monitor-utils/get-balance-updates.js +44 -0
  100. package/src/tx-log/monitor-utils/get-batched-rpc-balances.js +47 -0
  101. package/src/tx-log/monitor-utils/get-current-blacklist-status.js +38 -0
  102. package/src/tx-log/monitor-utils/get-current-eip7702-delegation.js +79 -0
  103. package/src/tx-log/monitor-utils/get-derive-transactions-to-check.js +10 -5
  104. package/src/tx-log/monitor-utils/get-fee-amount.js +1 -1
  105. package/src/tx-log/monitor-utils/get-history-from-server.js +1 -1
  106. package/src/tx-log/monitor-utils/get-log-items-from-server-tx.js +75 -33
  107. package/src/tx-log/monitor-utils/get-names-of-tokens-transferred-by-server-tx.js +3 -2
  108. package/src/tx-log/monitor-utils/get-raw-transfer-amount.js +29 -0
  109. package/src/tx-log/monitor-utils/get-transfers-by-token-name.js +3 -2
  110. package/src/tx-log/monitor-utils/get-value-of-transfers.js +10 -12
  111. package/src/tx-log/monitor-utils/index.js +12 -7
  112. package/src/tx-log/monitor-utils/is-unverifiable-pending-tx-expired.js +19 -0
  113. package/src/tx-log/monitor-utils/verify-pending-tx-status-rpc.js +90 -0
  114. package/src/tx-log-staking-processor/asset-staking-tx-data.js +82 -24
  115. package/src/tx-log-staking-processor/index.js +28 -14
  116. package/src/tx-log-staking-processor/utils.js +21 -20
  117. package/src/tx-send/broadcast-error-handler.js +63 -0
  118. package/src/tx-send/index.js +1 -2
  119. package/src/tx-send/nonce-utils.js +97 -0
  120. package/src/tx-send/tx-send.js +94 -288
  121. package/src/tx-sign/index.js +17 -0
  122. package/src/tx-type/index.js +280 -0
  123. package/src/watch-tx.js +36 -0
  124. package/src/web3/createSimulateMessage.js +3 -2
  125. package/src/web3/createSimulateTransactions.js +11 -0
  126. package/src/web3/index.js +3 -1
  127. package/src/etherscan/account.js +0 -49
  128. package/src/etherscan/index.js +0 -28
  129. package/src/etherscan/logs.js +0 -16
  130. package/src/etherscan/proxy.js +0 -47
  131. package/src/etherscan/request.js +0 -26
  132. package/src/etherscan/ws.js +0 -88
  133. package/src/exodus-eth-server/server-factory.js +0 -28
  134. package/src/exodus-eth-server/server-map.js +0 -21
  135. package/src/get-fee-async.js +0 -97
  136. package/src/staking/matic/service.js +0 -437
  137. package/src/staking-api.js +0 -5
  138. package/src/tx-log/clarity-utils/get-derive-data-needed-for-tick.js +0 -12
  139. package/src/tx-log-staking-processor/get-asset-tx-amount.js +0 -26
  140. package/src/tx-send/get-fee-info.js +0 -41
  141. package/src/websocket/index.android.js +0 -2
  142. package/src/websocket/index.ios.js +0 -2
  143. 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
- constructor({
33
- asset, // ethereum or ethereumholesky for testnet
34
- minAmount = MIN_AMOUNT,
35
- server,
36
- }) {
37
- assert(server, 'server is required')
38
- this.asset = asset
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[asset.name].EVERSTAKE_ADDRESS_CONTRACT_ACCOUNTING
41
- const poolAddress = EthereumStaking.addresses[asset.name].EVERSTAKE_ADDRESS_CONTRACT_POOL
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 = asset.currency.defaultUnit(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
- const callData = this.buildTxData(contract, method, ...args)
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
- return retry(eth.ethCall, { delayTimesMs: RETRY_DELAYS })(data)
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.callReadFunctionContract(this.contractAccounting, 'balance')
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.callReadFunctionContract(
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.callReadFunctionContract(
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.callReadFunctionContract(
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.callReadFunctionContract(
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.callReadFunctionContract(
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.callReadFunctionContract(
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.buildTxData(this.contractPool, 'stake', source)), // tx data field
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
- /* Unstake funds that are staked into the validator. Once unstaked they need to be claimed (once ready to withdraw) */
164
- async unstake({ address, amount, allowedInterchangeNum = 0, source = '2' }) {
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.buildTxData(this.contractPool, 'unstake', amountWei, allowedInterchangeNum, source)
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.buildTxData(this.contractAccounting, 'claimWithdrawRequest')),
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.buildTxData(this.contractPool, 'unstakePending', amountWei)),
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(`Min Amount ${this.minAmount}`)
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'