@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 +4 -4
- package/src/etherscan/request.js +7 -3
- package/src/exodus-eth-server/api.js +4 -1
- package/src/staking/ethereum-staking-utils.js +25 -0
- package/src/staking/ethereum-staking.js +192 -0
- package/src/staking/ethereum-staking.md +51 -0
- package/src/staking/index.js +2 -0
- package/src/staking/matic-staking-utils.js +1 -1
- package/src/staking/staking-provider-client.js +5 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/ethereum-api",
|
|
3
|
-
"version": "6.3.
|
|
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.
|
|
19
|
+
"@exodus/ethereum-lib": "^3.3.28",
|
|
20
20
|
"@exodus/ethereumjs-util": "^7.1.0-exodus.6",
|
|
21
|
-
"@exodus/fetch": "^1.3.0-beta.
|
|
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": "
|
|
37
|
+
"gitHead": "c5e6d4fb8566d79ecb82f249381e198a8d8bfb96"
|
|
38
38
|
}
|
package/src/etherscan/request.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import ms from 'ms'
|
|
2
2
|
import makeConcurrent from 'make-concurrent'
|
|
3
|
-
import
|
|
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
|
|
17
|
-
|
|
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)
|
|
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)
|
package/src/staking/index.js
CHANGED
|
@@ -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
|
|
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
|
|
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(
|
|
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 }) => {
|