@exodus/ethereum-api 6.3.7 → 6.3.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/ethereum-api",
3
- "version": "6.3.7",
3
+ "version": "6.3.9",
4
4
  "description": "Ethereum Api",
5
5
  "main": "src/index.js",
6
6
  "files": [
@@ -16,9 +16,9 @@
16
16
  "dependencies": {
17
17
  "@exodus/asset-lib": "^3.7.1",
18
18
  "@exodus/crypto": "^1.0.0-rc.0",
19
- "@exodus/ethereum-lib": "^3.3.26",
19
+ "@exodus/ethereum-lib": "^3.3.28",
20
20
  "@exodus/ethereumjs-util": "^7.1.0-exodus.6",
21
- "@exodus/fetch": "^1.3.0-beta.3",
21
+ "@exodus/fetch": "^1.3.0-beta.4",
22
22
  "@exodus/simple-retry": "^0.0.6",
23
23
  "@exodus/solidity-contract": "^1.1.3",
24
24
  "bn.js": "^5.2.1",
@@ -34,5 +34,5 @@
34
34
  "devDependencies": {
35
35
  "@exodus/models": "^8.10.4"
36
36
  },
37
- "gitHead": "23add284852287667fdf24d1c891915f4caa58bc"
37
+ "gitHead": "c5e6d4fb8566d79ecb82f249381e198a8d8bfb96"
38
38
  }
@@ -1,6 +1,6 @@
1
1
  import ms from 'ms'
2
2
  import makeConcurrent from 'make-concurrent'
3
- import { fetchival } from '@exodus/fetch'
3
+ import fetchival from '@exodus/fetch/experimental/fetchival'
4
4
 
5
5
  const ETHERSCAN_API_URL = 'https://api.etherscan.io/api'
6
6
  const DEFAULT_ETHERSCAN_API_KEY = 'XM3VGRSNW1TMSIR14I9MVFP15X74GNHTRI'
@@ -13,8 +13,12 @@ export function setEtherscanApiKey(apiKey) {
13
13
 
14
14
  export default makeConcurrent(
15
15
  async function(isValidResponseCheck, module, action, params = {}) {
16
- const queryParams = { ...params, module, action, apiKey: etherscanApiKey }
17
- const data = await fetchival(ETHERSCAN_API_URL, { timeout: ms('15s') }).get(queryParams)
16
+ const data = await fetchival(new URL(ETHERSCAN_API_URL), { timeout: ms('15s') }).get({
17
+ ...params,
18
+ module,
19
+ action,
20
+ apiKey: etherscanApiKey,
21
+ })
18
22
  if (!isValidResponseCheck(data)) throw new Error(`Invalid response: ${JSON.stringify(data)}`)
19
23
  return data.result
20
24
  },
@@ -23,7 +23,10 @@ export function create(defaultURL, ensAssetName) {
23
23
 
24
24
  async function request(module, params = {}, { version = 'v1', method = 'get' } = {}) {
25
25
  try {
26
- return await fetchival(baseUrl(version), { timeout: ms('15s') })(module)[method](params)
26
+ return await fetchival(baseUrl(version), { timeout: ms('15s') })(module).method(
27
+ method,
28
+ params
29
+ )
27
30
  } catch (err) {
28
31
  let nerr = err
29
32
  if (err.response && err.response.status === 500) {
@@ -0,0 +1,25 @@
1
+ import { EthereumStaking } from './ethereum-staking'
2
+
3
+ // stake(uint256 source)
4
+ const DELEGATE = '0x3a29dbae'
5
+ // unstakePending(uint256 amount)
6
+ const UNSTAKE_PENDING = '0xed0723d4'
7
+
8
+ // unstake(uint256 amount)
9
+ const UNSTAKE = '0x2e17de78'
10
+
11
+ const STAKING_MANAGER_CONTRACT = EthereumStaking.addresses.ethereum.EVERSTAKE_ADDRESS_CONTRACT_POOL
12
+
13
+ export const isEthereumTx = ({ coinName }) => coinName === 'ethereum'
14
+ export const isEthereumDelegate = (tx) =>
15
+ isEthereumTx(tx) && tx.to === STAKING_MANAGER_CONTRACT && tx.data?.methodId === DELEGATE
16
+ export const isEthereumUndelegate = (tx) =>
17
+ isEthereumTx(tx) &&
18
+ tx.from[0] === STAKING_MANAGER_CONTRACT &&
19
+ tx.data &&
20
+ tx.data.methodId === UNSTAKE
21
+ export const isEthereumClaimUndelegate = (tx) =>
22
+ isEthereumTx(tx) &&
23
+ tx.from[0] === STAKING_MANAGER_CONTRACT &&
24
+ tx.data &&
25
+ tx.data.methodId === UNSTAKE_PENDING
@@ -0,0 +1,192 @@
1
+ import { createContract } from '@exodus/ethereum-lib'
2
+ import { getServerByName } from '../exodus-eth-server'
3
+ import { retry } from '@exodus/simple-retry'
4
+ import { bufferToHex } from '@exodus/ethereumjs-util'
5
+
6
+ const MIN_AMOUNT = '0.1'
7
+ const RETRY_DELAYS = ['10s']
8
+
9
+ export class EthereumStaking {
10
+ static addresses = {
11
+ ethereum: {
12
+ EVERSTAKE_ADDRESS_CONTRACT_ACCOUNTING: '...', // TODO
13
+ EVERSTAKE_ADDRESS_CONTRACT_POOL: '...', // TODO
14
+ },
15
+ ethereumgoerli: {
16
+ EVERSTAKE_ADDRESS_CONTRACT_ACCOUNTING: '0x6e95818C2Dde3d87406F5C5d0A759d9372053a94',
17
+ EVERSTAKE_ADDRESS_CONTRACT_POOL: '0x1F28aD3B3e26192a09c1248D4092c692c32f8Ec0',
18
+ },
19
+ }
20
+
21
+ constructor(
22
+ asset, // ethereum or ethereumgoerli for testnet
23
+ minAmount = MIN_AMOUNT
24
+ ) {
25
+ this.asset = asset
26
+ const accountingAddress =
27
+ EthereumStaking.addresses[asset.name].EVERSTAKE_ADDRESS_CONTRACT_ACCOUNTING
28
+ const poolAddress = EthereumStaking.addresses[asset.name].EVERSTAKE_ADDRESS_CONTRACT_POOL
29
+
30
+ this.contractAccounting = createContract(accountingAddress, 'ethStakingAccounting')
31
+ this.contractPool = createContract(poolAddress, 'ethStakingPool')
32
+ this.accountingAddress = accountingAddress
33
+ this.poolAddress = poolAddress
34
+ this.minAmount = asset.currency.defaultUnit(minAmount)
35
+ }
36
+
37
+ buildTxData = (contract, method, ...args) => {
38
+ const txData = contract[method].build(...args)
39
+ return txData
40
+ }
41
+
42
+ callReadFunctionContract = (contract, method, ...args) => {
43
+ const callData = this.buildTxData(contract, method, ...args)
44
+ const data = {
45
+ data: bufferToHex(callData),
46
+ to: contract.address,
47
+ tag: 'latest',
48
+ }
49
+
50
+ const eth = getServerByName(this.asset.name)
51
+ return retry(eth.ethCall, { delayTimesMs: RETRY_DELAYS })(data)
52
+ }
53
+
54
+ // === ACCOUNTING ===
55
+
56
+ /** Return total deposited and activated pool balance */
57
+ async poolsBalance() {
58
+ const totalPoolBalance = await this.callReadFunctionContract(this.contractAccounting, 'balance')
59
+ return this.asset.currency.baseUnit(totalPoolBalance)
60
+ }
61
+
62
+ /** Return pool pending balance. Always < 32 ETH */
63
+ async poolPendingBalance() {
64
+ const pendingBalance = await this.callReadFunctionContract(
65
+ this.contractAccounting,
66
+ 'pendingBalance'
67
+ )
68
+ return this.asset.currency.baseUnit(pendingBalance)
69
+ }
70
+
71
+ /** Return user pending balance (waiting to be added from the Pool contract to the ETH2 validator) */
72
+ async pendingBalanceOf(address) {
73
+ let pendingBalance = await this.callReadFunctionContract(
74
+ this.contractAccounting,
75
+ 'pendingBalanceOf',
76
+ address
77
+ )
78
+ pendingBalance = this.contractAccounting.pendingBalanceOf.parse(pendingBalance)
79
+ return this.asset.currency.baseUnit(pendingBalance[0])
80
+ }
81
+
82
+ /** Return total user autocompound balance. Part of this balance could be in pending state after rewards autocompound */
83
+ async autocompoundBalanceOf(address) {
84
+ const userBalance = await this.callReadFunctionContract(
85
+ this.contractAccounting,
86
+ 'autocompoundBalanceOf',
87
+ address
88
+ )
89
+ return this.asset.currency.baseUnit(userBalance)
90
+ }
91
+
92
+ /** Return user active origin deposited balance */
93
+ async depositedBalanceOf(address) {
94
+ const depositedBalance = await this.callReadFunctionContract(
95
+ this.contractAccounting,
96
+ 'depositedBalanceOf',
97
+ address
98
+ )
99
+ return this.asset.currency.baseUnit(depositedBalance)
100
+ }
101
+
102
+ /* Get earned Rewards */
103
+ async getLiquidRewards(address) {
104
+ const [compoundBalance, depositedBalance] = await Promise.all([
105
+ this.autocompoundBalanceOf(address),
106
+ this.depositedBalanceOf(address),
107
+ ])
108
+ return compoundBalance.sub(depositedBalance)
109
+ }
110
+
111
+ /** Return withdrawable unstaked amount */
112
+ async withdrawRequest(address) {
113
+ let amounts = await this.callReadFunctionContract(
114
+ this.contractAccounting,
115
+ 'withdrawRequest',
116
+ address
117
+ )
118
+ amounts = this.contractAccounting.withdrawRequest.parse(amounts)
119
+ return {
120
+ requested: this.asset.currency.baseUnit(amounts[0]),
121
+ readyForClaim: this.asset.currency.baseUnit(amounts[1]),
122
+ canSendClaimRequest: amounts[0] !== '0' && amounts[0] === amounts[1],
123
+ }
124
+ }
125
+
126
+ // === POOL ===
127
+
128
+ async stake({ amount }) {
129
+ if (amount.gte(this.minAmount)) {
130
+ return {
131
+ to: this.poolAddress,
132
+ amount,
133
+ data: bufferToHex(this.buildTxData(this.contractPool, 'stake', '0')), // tx data field
134
+ }
135
+ } else {
136
+ throw new Error(`Min Amount ${this.minAmount}`)
137
+ }
138
+ }
139
+
140
+ async unstake({ address, amount }) {
141
+ const amountWei = amount.toBaseString()
142
+ const balance = await this.autocompoundBalanceOf(address)
143
+
144
+ if (balance.gte(amount)) {
145
+ return {
146
+ to: this.poolAddress,
147
+ amount: this.asset.currency.ZERO,
148
+ data: bufferToHex(this.buildTxData(this.contractPool, 'unstake', amountWei)),
149
+ }
150
+ } else {
151
+ throw new Error(`Max Amount for unstake ${balance}`)
152
+ }
153
+ }
154
+
155
+ /** Claim funds requested by unstake (in withdraw state) */
156
+ async claimWithdrawRequest({ address }) {
157
+ try {
158
+ const rewards = await this.withdrawRequest(address)
159
+ if (rewards.canSendClaimRequest) {
160
+ return {
161
+ to: this.accountingAddress,
162
+ amount: this.asset.currency.ZERO,
163
+ data: bufferToHex(this.buildTxData(this.contractAccounting, 'claimWithdrawRequest')),
164
+ }
165
+ } else {
166
+ return null // no amount to withdraw
167
+ }
168
+ } catch (error) {
169
+ throw new Error(error)
170
+ }
171
+ }
172
+
173
+ /** Unstake pending funds, that are still not staked into a Validator */
174
+ async unstakePending({ address, amount }) {
175
+ const pendingBalance = await this.pendingBalanceOf(address)
176
+
177
+ if (amount.lte(pendingBalance)) {
178
+ try {
179
+ const amountWei = amount.toBaseString()
180
+ return {
181
+ to: this.poolAddress,
182
+ amount: this.asset.currency.ZERO,
183
+ data: bufferToHex(this.buildTxData(this.contractPool, 'unstakePending', amountWei)),
184
+ }
185
+ } catch (err) {
186
+ throw new Error(err)
187
+ }
188
+ } else {
189
+ throw new Error(`Min Amount ${this.minAmount}`)
190
+ }
191
+ }
192
+ }
@@ -0,0 +1,51 @@
1
+ ## Staking in Ethereum (ETH)
2
+
3
+ ## How it works?
4
+
5
+ Ethereum Staking happens in the **Ethereum mainnet** network, using **Everstake** contracts.
6
+
7
+ The staking contracts are deployed also to the **Goerli** testnet.
8
+
9
+ ## Contracts
10
+
11
+ ETH Staking is handled by 2 main contracts. The Accounting Contract and the Pool Contract.
12
+
13
+ #### Staking
14
+
15
+ To stake we call the `stake` method with the `isAutocompound` flag set to `true`. By default Exodus users will stake with autocompound enabled.
16
+
17
+ Ethereum Entry Queue is the numer of validators waiting to be operative. We can see the number of validators Entering/Exiting the chain using dashboard like https://www.validatorqueue.com/ (Currently ~43 days to get operative).
18
+
19
+ #### Unstaking
20
+
21
+ Unstake value from common or autocompound balance. Unstaked immediately if _value_ <= _pool pending balance_, or partially (with funds available in the pool), otherwise a withdraw request is needed (meaning the funds are staked in the Validator and will need some time to move back and be available for withdraw. Ethereum caps the number of validator withdrawals per day at 1,800, so based on the exit queue the time can vary, check the dashboard above to see how crowded is the queue and the estimated time to exit).
22
+
23
+ When funds are withdrawable (`withdrawRequest(...).readyForClaim`) a `claimWithdrawRequest` tx can be broadcasted.
24
+
25
+ #### Withdrawing
26
+
27
+ If you stake 0.1 ETH and these funds are still pending (not in a validator but in the pool).
28
+ You won't be able to just `unstake`. That operation is required when funds are staked already.
29
+
30
+ You'll have to broadcast an `unstakePending` tx.
31
+
32
+ #### Rewards
33
+
34
+ Rewards are autocompounded by default.
35
+
36
+ Staked balanced can be queried using `autocompoundBalanceOf`, part of this balance could be in pending state if there's not enough ETH (32 ETH) to start a new Validator.
37
+
38
+ To check pending balance we can use `pendingBalanceOf`.
39
+
40
+ ### Staking Bussines Rules
41
+
42
+ All of these rules can be queried from the smart contracts, except `minimum amount to stake`, this is defined by Everstake/Exodus.
43
+
44
+ - **Minimum rewards to withdraw**: Cannot claim rewards, by default they are autocompounded
45
+ - **Minimum amount to stake**: 0.1 ETH (contract rule)
46
+ - **Withdrawal delay**: Cannot be predicted precisely. It depends when funds move from the Validator to the Pool (or new users funds enter the pool)
47
+ - **Unstaking period**: immediately, but staked ETH are not available to be withdrawn before withdrawal delay.
48
+
49
+ Useful resources:
50
+
51
+ [Everstake wallet SDK](https://github.com/everstake/wallet-sdk/blob/main/ethereum.js)
@@ -1,4 +1,6 @@
1
+ export { EthereumStaking } from './ethereum-staking'
1
2
  export { MaticStaking } from './matic-staking'
2
3
  export { FantomStaking } from './fantom-staking'
3
4
  export { stakingProviderClientFactory } from './staking-provider-client'
4
5
  export * from './matic-staking-utils'
6
+ export * from './ethereum-staking-utils'
@@ -13,7 +13,7 @@ const STAKING_MANAGER_CONTRACT = new MaticStaking().stakingManagerContract.addre
13
13
 
14
14
  export const isPolygonTx = ({ coinName }) => coinName === 'polygon'
15
15
  export const isPolygonDelegate = (tx) =>
16
- isPolygonTx(tx) && tx.to === STAKING_MANAGER_CONTRACT && tx.data && tx.data.methodId === DELEGATE
16
+ isPolygonTx(tx) && tx.to === STAKING_MANAGER_CONTRACT && tx.data?.methodId === DELEGATE
17
17
  export const isPolygonUndelegate = (tx) =>
18
18
  isPolygonTx(tx) &&
19
19
  tx.from[0] === STAKING_MANAGER_CONTRACT &&
@@ -1,5 +1,5 @@
1
1
  import assert from 'minimalistic-assert'
2
- import { fetchival } from '@exodus/fetch'
2
+ import fetchival from '@exodus/fetch/experimental/fetchival'
3
3
  import ms from 'ms'
4
4
 
5
5
  const DEFAULT_STAKING_URL = 'https://staking.a.exodus.io'
@@ -8,20 +8,20 @@ const HTTP_POST_TIMEOUT = ms('30s')
8
8
  export const stakingProviderClientFactory = (defaultStakingUrl = DEFAULT_STAKING_URL) => {
9
9
  assert(defaultStakingUrl, '"defaultStakingUrl" must be provided')
10
10
 
11
- let stakingUrl = defaultStakingUrl
11
+ let stakingUrl = new URL(defaultStakingUrl) // always should be an URL instance
12
12
 
13
13
  const assetValidators = {
14
14
  polygon: '0x3f4ce357b9d61d3b904492b8b5abc69c6c693720',
15
15
  }
16
16
 
17
17
  const setStakingUrl = (newStakingUrl) => {
18
- stakingUrl = newStakingUrl || defaultStakingUrl
18
+ stakingUrl = new URL(newStakingUrl || defaultStakingUrl)
19
19
  }
20
20
 
21
21
  const stakingRequest = ({ asset, data }) => {
22
- return fetchival(`${stakingUrl}/${asset}/stake`, {
22
+ return fetchival(stakingUrl, {
23
23
  timeout: HTTP_POST_TIMEOUT,
24
- }).post(data)
24
+ })(asset)('stake').post(data)
25
25
  }
26
26
 
27
27
  const notifyActionFactory = ({ type }) => {