@exodus/ethereum-plugin 2.3.0 → 2.4.1
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 +17 -0
- package/package.json +15 -13
- package/src/index.js +3 -2
- package/src/staking/polygon/Staking.md +71 -0
- package/src/staking/polygon/account-state.js +12 -0
- package/src/staking/polygon/api.js +201 -0
- package/src/staking/polygon/contracts/index.js +12 -0
- package/src/staking/polygon/index.js +27 -0
- package/src/staking/polygon/service.js +467 -0
- package/src/staking/polygon/staking-utils.js +117 -0
- package/src/staking/polygon/tx-filters/index.js +34 -0
- package/src/staking.js +7 -20
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,23 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
## [2.4.1](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-plugin@2.4.0...@exodus/ethereum-plugin@2.4.1) (2024-09-12)
|
|
7
|
+
|
|
8
|
+
**Note:** Version bump only for package @exodus/ethereum-plugin
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
## [2.4.0](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-plugin@2.3.0...@exodus/ethereum-plugin@2.4.0) (2024-09-11)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Features
|
|
18
|
+
|
|
19
|
+
* switch ethereum to ESM ([#3374](https://github.com/ExodusMovement/assets/issues/3374)) ([d3a86c3](https://github.com/ExodusMovement/assets/commit/d3a86c3202754a0e6ab988d454d3e006ec11d9e4))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
6
23
|
## [2.3.0](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-plugin@2.2.3...@exodus/ethereum-plugin@2.3.0) (2024-08-24)
|
|
7
24
|
|
|
8
25
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/ethereum-plugin",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.1",
|
|
4
4
|
"description": "Exodus ethereum-plugin",
|
|
5
|
+
"type": "module",
|
|
5
6
|
"main": "src/index.js",
|
|
6
7
|
"files": [
|
|
7
8
|
"src",
|
|
@@ -14,24 +15,25 @@
|
|
|
14
15
|
"access": "restricted"
|
|
15
16
|
},
|
|
16
17
|
"scripts": {
|
|
17
|
-
"test": "run -T exodus-test --jest
|
|
18
|
-
"lint": "run -T
|
|
18
|
+
"test": "run -T exodus-test --jest",
|
|
19
|
+
"lint": "run -T eslintc .",
|
|
19
20
|
"lint:fix": "yarn lint --fix"
|
|
20
21
|
},
|
|
21
22
|
"dependencies": {
|
|
22
|
-
"@exodus/asset-lib": "^
|
|
23
|
-
"@exodus/
|
|
24
|
-
"@exodus/ethereum-
|
|
25
|
-
"@exodus/ethereum-
|
|
26
|
-
"@exodus/
|
|
23
|
+
"@exodus/asset-lib": "^5.0.0",
|
|
24
|
+
"@exodus/currency": "^5.0.2",
|
|
25
|
+
"@exodus/ethereum-api": "^8.18.1",
|
|
26
|
+
"@exodus/ethereum-lib": "^5.4.0",
|
|
27
|
+
"@exodus/ethereum-meta": "^2.0.0",
|
|
28
|
+
"@exodus/ethereumjs-util": "^7.1.0-exodus.7",
|
|
29
|
+
"@exodus/simple-retry": "^0.0.6",
|
|
30
|
+
"bn.js": "^5.2.1",
|
|
31
|
+
"minimalistic-assert": "^1.0.1"
|
|
27
32
|
},
|
|
28
33
|
"devDependencies": {
|
|
29
34
|
"@exodus/assets": "^11.0.0",
|
|
30
35
|
"@exodus/assets-testing": "^1.0.0",
|
|
31
|
-
"@exodus/
|
|
32
|
-
"@exodus/fetch": "^1.3.0",
|
|
33
|
-
"@exodus/models": "^11.0.0",
|
|
34
|
-
"lodash": "^4.17.21"
|
|
36
|
+
"@exodus/models": "^12.0.1"
|
|
35
37
|
},
|
|
36
38
|
"bugs": {
|
|
37
39
|
"url": "https://github.com/ExodusMovement/assets/issues?q=is%3Aissue+is%3Aopen+label%3Aethereum-plugin"
|
|
@@ -41,5 +43,5 @@
|
|
|
41
43
|
"type": "git",
|
|
42
44
|
"url": "git+https://github.com/ExodusMovement/assets.git"
|
|
43
45
|
},
|
|
44
|
-
"gitHead": "
|
|
46
|
+
"gitHead": "3885dfad0efad7a6f12053aab220f0b127039fcf"
|
|
45
47
|
}
|
package/src/index.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
import { createAssetFactory } from '@exodus/ethereum-api'
|
|
3
3
|
import assetsList from '@exodus/ethereum-meta'
|
|
4
4
|
|
|
5
|
-
import feeData from './fee-data'
|
|
6
|
-
import { stakingConfiguration } from './staking'
|
|
5
|
+
import feeData from './fee-data.js'
|
|
6
|
+
import { stakingConfiguration, stakingDependencies } from './staking.js'
|
|
7
7
|
|
|
8
8
|
const createAsset = createAssetFactory({
|
|
9
9
|
assetsList,
|
|
@@ -13,6 +13,7 @@ const createAsset = createAssetFactory({
|
|
|
13
13
|
erc20FuelBuffer: 1.1, // 10% more than the required fee
|
|
14
14
|
fuelThreshold: '0.005',
|
|
15
15
|
stakingConfiguration,
|
|
16
|
+
stakingDependencies,
|
|
16
17
|
serverUrl: 'https://geth.a.exodus.io/wallet/v1/',
|
|
17
18
|
confirmationsNumber: 30,
|
|
18
19
|
})
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
## Staking in Polygon (MATIC)
|
|
2
|
+
|
|
3
|
+
## How it works?
|
|
4
|
+
|
|
5
|
+
Polygon Staking happens in the **Ethereum network**, it is implemented by a set of smart contracts deployed on the ETH mainnet.
|
|
6
|
+
Staking is made by delegating the `ERC20 MATIC` token to those smart contracts which handle the `(un)staking/rewards` process and it shouldn't be confused with the Native asset in **Polygon network** (`MATIC NATIVE`).
|
|
7
|
+
|
|
8
|
+
Stakers are divided into `validators`, `delegators`, and watchers (for fraud reporting).
|
|
9
|
+
|
|
10
|
+
## Contracts
|
|
11
|
+
|
|
12
|
+
### StakeManager contract
|
|
13
|
+
|
|
14
|
+
`StakeManager` is the main contract for handling validator related activities like checkPoint signature verification, reward distribution, and stake management. Since the contract is using NFT ID as a source of ownership, change of ownership and signer won't affect anything in the system. [see](https://wiki.polygon.technology/docs/pos/contracts/stakingmanager).
|
|
15
|
+
|
|
16
|
+
### ValidatorShare contract
|
|
17
|
+
|
|
18
|
+
For delegation staking each validator has its own deployed **contract**, this contract has the logic to `stake/unstake` as delegators, but it also acts as an `ERC20`, this `ERC20` token is what we know as the shares token. Shares token are calculated based on the total amount staked in the contract, varying from time to time
|
|
19
|
+
When delegators stake matic, they call `buyVoucher()` , the contract receives the MATIC tokens to stake (approval is needed on Matic token contract) and it calculates and mints the number of token shares that correspond to that staked amount. [see](https://wiki.polygon.technology/docs/pos/contracts/delegation).
|
|
20
|
+
|
|
21
|
+
### Staking Anatomy
|
|
22
|
+
|
|
23
|
+
Example taken from the docs:
|
|
24
|
+
|
|
25
|
+
_Polygon supports delegation via validator shares. By using this design, it is easier to distribute rewards and slash with scale (thousands of delegators) on Ethereum contracts without much computation.
|
|
26
|
+
Delegators delegate by purchasing shares of a finite pool from validators. Each validator will have their own validator share token. Let's call these fungible tokens `VATIC` for a validator `A`. As soon as a user delegates to a validator `A`, they will be issued `VATIC` based on an exchange rate of `MATIC/VATIC` pair. As users accrue value the exchange rate indicates that they can now withdraw more `MATIC` for each `VATIC` and when users get slashed, users withdraw less `MATIC` for their `VATIC`.
|
|
27
|
+
Note that `MATIC` is a staking token. A delegator needs to have `MATIC` tokens to participate in the delegation.
|
|
28
|
+
Initially, a delegator `D` buys tokens from validator `A` specific pool when `1 MATIC per 1 VATIC`.
|
|
29
|
+
When a validator gets rewarded with more `MATIC` tokens, new tokens are added to the pool. Let's say with the current pool of `100 MATIC` tokens, `10 MATIC` rewards are added to the pool. But since the total supply of `VATIC` tokens didn't change due to rewards, the exchange rate becomes `1 MATIC per 0.9 VATIC`. Now, delegator `D` gets more `MATIC` for the same shares.
|
|
30
|
+
`VATIC`: Validator specific minted validator share tokens (ERC20 tokens)_
|
|
31
|
+
|
|
32
|
+
#### Rewards
|
|
33
|
+
|
|
34
|
+
Delegators can do with their rewards the following:
|
|
35
|
+
withdraw via `withdrawRewards()` or
|
|
36
|
+
`restake` (earned rewards are put as stake in the contract via `restake()`)
|
|
37
|
+
|
|
38
|
+
#### Unstaking
|
|
39
|
+
|
|
40
|
+
For delegators to unstake, there are two steps:
|
|
41
|
+
|
|
42
|
+
1. `sellVoucher_new()`
|
|
43
|
+
2. `unstakeClaimTokens()`
|
|
44
|
+
|
|
45
|
+
**Note**: there are some methods in the validator share contract with the suffix `_new`, for instance:
|
|
46
|
+
`sellVoucher()` and `sellVoucher_new()` this is because the recent changes on the smart contract to support the new exit API (**unstake** and **claim** tokens)\*
|
|
47
|
+
|
|
48
|
+
**sellVoucher** method calculates the token shares that correspond to the staked MATIC at that time, and burns those share tokens, it also transfers the rewards, and makes changes in the contract to update the total stake held in the contract .
|
|
49
|
+
Basically it prepares the contract to let delegators withdraw their staked MATIC in a second step once the **withdrawal delay** it's ben fulfilled.
|
|
50
|
+
|
|
51
|
+
#### Unstake Claim Tokens
|
|
52
|
+
|
|
53
|
+
**unstakeClaimTokens** makes the actual withdraw of the staked tokens, sellVoucher does not transfer the staked MATIC back to the delegator, we need to call this function after the withdrawal delay has been fulfilled, so that delegators can get their stake amount back to their wallet.
|
|
54
|
+
|
|
55
|
+
### Staking Bussines Rules
|
|
56
|
+
|
|
57
|
+
All of these rules can be queried from the smart contracts, except `minimum amount to stake`, this is defined by Exodus.
|
|
58
|
+
|
|
59
|
+
- **Minimum rewards to withdraw**: 1 MATIC (contract rule)
|
|
60
|
+
- **Minimum amount to stake**: 1 MATIC (exodus rule)
|
|
61
|
+
- **Withdrawal delay**: The amount of time delegators must wait before claiming their staked amount. Varies depending on the contract governance
|
|
62
|
+
- **Withdrawal exchange rate**: The exchange rate used to convert tokens to shares and vice-versa. Varies depending on the number of MATIC in the withdraw pool share (affected by the earned rewards).
|
|
63
|
+
- **Unstaking period**: immediately, but staked MATIC tokens are not available to be withdrawn before withdrawal delay
|
|
64
|
+
- **Claim unstaked tokens**: after 3-4 days has passed since unstaked was called, the unstaked tokens can be claimed by the delegator.
|
|
65
|
+
|
|
66
|
+
Useful resources:
|
|
67
|
+
|
|
68
|
+
[Polygon docs](https://wiki.polygon.technology/docs/pos/polygon-architecture/)
|
|
69
|
+
[Polygon Staking](https://wiki.polygon.technology/docs/pos/contracts/delegation)
|
|
70
|
+
[StakeManager contract](https://github.com/maticnetwork/contracts/blob/main/contracts/staking/stakeManager/StakeManager.sol)
|
|
71
|
+
[Validator share contract](https://github.com/maticnetwork/contracts/blob/main/contracts/staking/stakeManager/StakeManager.sol)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export const stakingAccountState = ({ currency }) => ({
|
|
2
|
+
isDelegating: false,
|
|
3
|
+
isUndelegateInProgress: false,
|
|
4
|
+
canClaimUndelegatedBalance: false,
|
|
5
|
+
minRewardsToWithdraw: currency.defaultUnit(1),
|
|
6
|
+
minDelegateAmount: currency.defaultUnit(1),
|
|
7
|
+
unclaimedUndelegatedBalance: currency.ZERO,
|
|
8
|
+
delegatedBalance: currency.ZERO,
|
|
9
|
+
rewardsBalance: currency.ZERO,
|
|
10
|
+
withdrawable: currency.ZERO,
|
|
11
|
+
unbondNonce: '0',
|
|
12
|
+
})
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { createContract } from '@exodus/ethereum-lib'
|
|
2
|
+
import { bufferToHex } from '@exodus/ethereumjs-util'
|
|
3
|
+
import { retry } from '@exodus/simple-retry'
|
|
4
|
+
import BN from 'bn.js'
|
|
5
|
+
|
|
6
|
+
import { mainnetContracts } from './contracts/index.js'
|
|
7
|
+
|
|
8
|
+
const RETRY_DELAYS = ['10s']
|
|
9
|
+
|
|
10
|
+
class StakingServer {
|
|
11
|
+
constructor({ asset, server, contracts = mainnetContracts }) {
|
|
12
|
+
this.asset = asset
|
|
13
|
+
// harcoded exchange rate from the validator share contract
|
|
14
|
+
// in order to calculate claim unstake amount off-chain
|
|
15
|
+
this.EXCHANGE_RATE_PRECISION = new BN(10).pow(new BN(29))
|
|
16
|
+
this.validatorShareContract = createContract(
|
|
17
|
+
contracts.VALIDATOR_SHARES_CONTRACT_ADDR,
|
|
18
|
+
'maticValidatorShare'
|
|
19
|
+
)
|
|
20
|
+
this.stakingManagerContract = createContract(
|
|
21
|
+
contracts.STAKING_MANAGER_ADDR,
|
|
22
|
+
'maticStakingManager'
|
|
23
|
+
)
|
|
24
|
+
this.polygonContract = createContract(contracts.TOKEN_CONTRACT, 'polygon')
|
|
25
|
+
this.server = server
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
buildTxData = (contract, method, ...args) => {
|
|
29
|
+
return contract[method].build(...args)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
callReadFunctionContract = (contract, method, ...args) => {
|
|
33
|
+
const callData = this.buildTxData(contract, method, ...args)
|
|
34
|
+
const data = {
|
|
35
|
+
data: bufferToHex(callData),
|
|
36
|
+
to: contract.address,
|
|
37
|
+
tag: 'latest',
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const eth = this.server
|
|
41
|
+
return retry(eth.ethCall, { delayTimesMs: RETRY_DELAYS })(data)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
getWithdrawalDelay = async () => {
|
|
45
|
+
const withdrawalDelay = await this.callReadFunctionContract(
|
|
46
|
+
this.stakingManagerContract,
|
|
47
|
+
'withdrawalDelay'
|
|
48
|
+
)
|
|
49
|
+
return toBN(withdrawalDelay)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
getMinRewardsToWithdraw = async () => {
|
|
53
|
+
const minRewardsToWithdraw = await this.callReadFunctionContract(
|
|
54
|
+
this.validatorShareContract,
|
|
55
|
+
'minAmount'
|
|
56
|
+
)
|
|
57
|
+
return this.asset.currency.baseUnit(minRewardsToWithdraw)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* A checkpoint is an epoch value that increments in the contract, every epoch is
|
|
62
|
+
* stored in a list and points the events that happened in that epoch. (timeline)
|
|
63
|
+
*/
|
|
64
|
+
getCurrentCheckpoint = async () => {
|
|
65
|
+
const currentEpoch = await this.callReadFunctionContract(this.stakingManagerContract, 'epoch')
|
|
66
|
+
return toBN(currentEpoch)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Unbond is a struct in the staking contract that holds the number of shares and
|
|
71
|
+
* the withdrawEpoch for a delegator (used when delegators unstaked their tokens)
|
|
72
|
+
* @returns {number}
|
|
73
|
+
*/
|
|
74
|
+
getUnboundInfo = async (address, nonce) => {
|
|
75
|
+
let _nonce = nonce
|
|
76
|
+
if (!Number.isInteger(_nonce)) {
|
|
77
|
+
_nonce = await this.getCurrentUnbondNonce(address)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const unboundInfo = await this.callReadFunctionContract(
|
|
81
|
+
this.validatorShareContract,
|
|
82
|
+
'unbonds_new',
|
|
83
|
+
address,
|
|
84
|
+
_nonce
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
const [shares, withdrawEpoch] = splitIn32BytesArray(unboundInfo)
|
|
88
|
+
return {
|
|
89
|
+
withdrawEpoch: toBN(withdrawEpoch),
|
|
90
|
+
shares: toBN(shares),
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* UnbondNonce is a counter stored in the contract that tracks each time a delegator unstakes
|
|
96
|
+
* @param address delegator address
|
|
97
|
+
* @returns current unbonded nonce
|
|
98
|
+
*/
|
|
99
|
+
getCurrentUnbondNonce = async (address) => {
|
|
100
|
+
const unbondNonce = await this.callReadFunctionContract(
|
|
101
|
+
this.validatorShareContract,
|
|
102
|
+
'unbondNonces',
|
|
103
|
+
address
|
|
104
|
+
)
|
|
105
|
+
return parseInt(unbondNonce, 16)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
getLiquidRewards = async (address) => {
|
|
109
|
+
const liquidRewards = await this.callReadFunctionContract(
|
|
110
|
+
this.validatorShareContract,
|
|
111
|
+
'getLiquidRewards',
|
|
112
|
+
address
|
|
113
|
+
)
|
|
114
|
+
return this.asset.currency.baseUnit(liquidRewards)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
getTotalStake = async (address) => {
|
|
118
|
+
const stakeInfo = await this.callReadFunctionContract(
|
|
119
|
+
this.validatorShareContract,
|
|
120
|
+
'getTotalStake',
|
|
121
|
+
address
|
|
122
|
+
)
|
|
123
|
+
const [amount] = splitIn32BytesArray(stakeInfo)
|
|
124
|
+
return this.asset.currency.baseUnit(toBN(amount).toString())
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
getWithdrawExchangeRate = async () => {
|
|
128
|
+
const withdrawExchangeRate = await this.callReadFunctionContract(
|
|
129
|
+
this.validatorShareContract,
|
|
130
|
+
'withdrawExchangeRate'
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
return toBN(withdrawExchangeRate)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Approves StakeManager contract for withdrawing {amount} in Matic tokens
|
|
138
|
+
* when users stakes.
|
|
139
|
+
* This function needs to be called before calling delegate function so that staking
|
|
140
|
+
* can properly work
|
|
141
|
+
* This also partially address the front running attack on ERC20 approve function:
|
|
142
|
+
* https://github.com/OpenZeppelin/openzeppelin-contracts/blob/f959d7e4e6ee0b022b41e5b644c79369869d8411/contracts/token/ERC20/ERC20.sol#L165-L206
|
|
143
|
+
* @param amount Matic tokens to be approved for StakeManager to withdraw (polygon currency)
|
|
144
|
+
*/
|
|
145
|
+
approveStakeManager = (amount) => {
|
|
146
|
+
return this.buildTxData(
|
|
147
|
+
this.polygonContract,
|
|
148
|
+
'increaseAllowance',
|
|
149
|
+
this.stakingManagerContract.address,
|
|
150
|
+
amount.toBaseString()
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
restakeReward = () => {
|
|
155
|
+
return this.buildTxData(this.validatorShareContract, 'restake')
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
withdrawRewards = () => {
|
|
159
|
+
return this.buildTxData(this.validatorShareContract, 'withdrawRewards')
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
delegate = ({ amount }) => {
|
|
163
|
+
return this.buildTxData(this.validatorShareContract, 'buyVoucher', amount.toBaseString(), '0')
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
undelegate = ({ amount, maximumSharesToBurn }) => {
|
|
167
|
+
const _maximumSharesToBurn = maximumSharesToBurn || amount
|
|
168
|
+
return this.buildTxData(
|
|
169
|
+
this.validatorShareContract,
|
|
170
|
+
'sellVoucher_new',
|
|
171
|
+
amount.toBaseString(),
|
|
172
|
+
_maximumSharesToBurn.toBaseString()
|
|
173
|
+
)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* @param {number} unbondNonce the unbond nonce from where delegator claim its staked tokens
|
|
178
|
+
*/
|
|
179
|
+
claimUndelegatedBalance = ({ unbondNonce }) => {
|
|
180
|
+
return this.buildTxData(this.validatorShareContract, 'unstakeClaimTokens_new', unbondNonce)
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// see arguments encoding standard (padded 32 bytes)
|
|
185
|
+
// https://docs.soliditylang.org/en/v0.8.15/abi-spec.html?highlight=abi.encode#function-selector-and-argument-encoding
|
|
186
|
+
const splitIn32BytesArray = (output) => removeHexPrefix(output).match(/[\da-f]{1,64}/gi) || []
|
|
187
|
+
|
|
188
|
+
const toBN = (str) => new BN(removeLeadingZeroes(removeHexPrefix(str)), 16)
|
|
189
|
+
|
|
190
|
+
const removeLeadingZeroes = (str) => str.replace(/^0+/, '')
|
|
191
|
+
|
|
192
|
+
const removeHexPrefix = (str) => {
|
|
193
|
+
if (typeof str !== 'string' || str === '') {
|
|
194
|
+
return str
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return str.startsWith('0x') ? str.slice(2) : str
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export const stakingServerFactory = ({ asset, contracts, server }) =>
|
|
201
|
+
new StakingServer({ asset, contracts, server })
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export const mainnetContracts = {
|
|
2
|
+
VALIDATOR_SHARES_CONTRACT_ADDR: '0xf30cf4ed712d3734161fdaab5b1dbb49fd2d0e5c',
|
|
3
|
+
STAKING_MANAGER_ADDR: '0x5e3ef299fddf15eaa0432e6e66473ace8c13d908',
|
|
4
|
+
TOKEN_CONTRACT: '0x7d1afa7b718fb893db30a3abc0cfc608aacfebb0',
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export const methodIds = {
|
|
8
|
+
DELEGATE: '0x6ab15071', // buyVoucher(uint256 _amount, uint256 _minSharesToMint)
|
|
9
|
+
UNDELEGATE: '0xc83ec04d', // sellVoucher_new(uint256 claimAmount, uint256 maximumSharesToBurn)
|
|
10
|
+
CLAIM_REWARD: '0xc7b8981c', // withdrawRewards()
|
|
11
|
+
CLAIM_UNDELEGATE: '0xe97fddc2', // unstakeClaimTokens_new(uint256 unbondNonce)
|
|
12
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { tokens } from '@exodus/ethereum-meta'
|
|
2
|
+
|
|
3
|
+
import { stakingAccountState } from './account-state.js'
|
|
4
|
+
import { stakingServerFactory } from './api.js'
|
|
5
|
+
import { mainnetContracts, methodIds } from './contracts/index.js'
|
|
6
|
+
import { stakingServiceFactory } from './service.js'
|
|
7
|
+
import { txUtilsFactory } from './staking-utils.js'
|
|
8
|
+
|
|
9
|
+
const polygon = tokens.find(({ name: tokenName }) => tokenName === 'polygon')
|
|
10
|
+
|
|
11
|
+
export const polygonStakingConfig = {
|
|
12
|
+
accountStateExtraData: stakingAccountState(polygon),
|
|
13
|
+
features: {
|
|
14
|
+
stake: true,
|
|
15
|
+
unstake: true,
|
|
16
|
+
claimUnstaked: true,
|
|
17
|
+
claimRewards: true,
|
|
18
|
+
},
|
|
19
|
+
contracts: mainnetContracts,
|
|
20
|
+
methodIds,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const polygonStakingDeps = {
|
|
24
|
+
txUtilsFactory,
|
|
25
|
+
stakingServiceFactory,
|
|
26
|
+
stakingServerFactory,
|
|
27
|
+
}
|
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
import { isNumberUnit } from '@exodus/currency'
|
|
2
|
+
import { estimateGasLimit, stakingProviderClientFactory } from '@exodus/ethereum-api'
|
|
3
|
+
import BN from 'bn.js'
|
|
4
|
+
|
|
5
|
+
export function stakingServiceFactory({ assetClientInterface, server, stakingServer }) {
|
|
6
|
+
const stakingProvider = stakingProviderClientFactory()
|
|
7
|
+
|
|
8
|
+
function amountToCurrency({ asset, amount }) {
|
|
9
|
+
return isNumberUnit(amount) ? amount : asset.currency.parse(amount)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async function getStakeAssets() {
|
|
13
|
+
const { polygon: asset, ethereum: feeAsset } = await assetClientInterface.getAssetsForNetwork({
|
|
14
|
+
baseAssetName: 'ethereum', // Polygon token lives in ETH network
|
|
15
|
+
})
|
|
16
|
+
return { asset, feeAsset }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function approveDelegateAmount({ walletAccount, amount } = {}) {
|
|
20
|
+
const { asset } = await getStakeAssets()
|
|
21
|
+
const address = await assetClientInterface.getReceiveAddress({
|
|
22
|
+
assetName: asset.name,
|
|
23
|
+
walletAccount,
|
|
24
|
+
})
|
|
25
|
+
const delegatorAddress = address.toLowerCase()
|
|
26
|
+
|
|
27
|
+
amount = amountToCurrency({ asset, amount })
|
|
28
|
+
|
|
29
|
+
const txApproveData = await stakingServer.approveStakeManager(amount)
|
|
30
|
+
const { gasPrice, gasLimit, fee } = await estimateTxFee(
|
|
31
|
+
delegatorAddress,
|
|
32
|
+
asset.contracts.TOKEN_CONTRACT,
|
|
33
|
+
txApproveData
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
return prepareAndSendTx({
|
|
37
|
+
walletAccount,
|
|
38
|
+
waitForConfirmation: true,
|
|
39
|
+
to: asset.contracts.TOKEN_CONTRACT,
|
|
40
|
+
txData: txApproveData,
|
|
41
|
+
gasPrice,
|
|
42
|
+
gasLimit,
|
|
43
|
+
fee,
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function delegate({ walletAccount, amount } = {}) {
|
|
48
|
+
const { asset } = await getStakeAssets()
|
|
49
|
+
const address = await assetClientInterface.getReceiveAddress({
|
|
50
|
+
assetName: asset.name,
|
|
51
|
+
walletAccount,
|
|
52
|
+
})
|
|
53
|
+
const delegatorAddress = address.toLowerCase()
|
|
54
|
+
|
|
55
|
+
amount = amountToCurrency({ asset, amount })
|
|
56
|
+
|
|
57
|
+
// const txApproveData = await stakingServer.approveStakeManager(amount)
|
|
58
|
+
// let { gasPrice, gasLimit, fee } = await estimateTxFee(
|
|
59
|
+
// delegatorAddress,
|
|
60
|
+
// contracts.TOKEN_CONTRACT,
|
|
61
|
+
// txApproveData
|
|
62
|
+
// )
|
|
63
|
+
// await prepareAndSendTx({
|
|
64
|
+
// walletAccount,
|
|
65
|
+
// waitForConfirmation: true,
|
|
66
|
+
// to: contracts.TOKEN_CONTRACT,
|
|
67
|
+
// txData: txApproveData,
|
|
68
|
+
// gasPrice,
|
|
69
|
+
// gasLimit,
|
|
70
|
+
// fee,
|
|
71
|
+
// })
|
|
72
|
+
|
|
73
|
+
const txDelegateData = await stakingServer.delegate({ amount })
|
|
74
|
+
const { gasPrice, gasLimit, fee } = await estimateTxFee(
|
|
75
|
+
delegatorAddress,
|
|
76
|
+
asset.contracts.EVERSTAKE_VALIDATOR_CONTRACT_ADDR,
|
|
77
|
+
txDelegateData
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
const txId = await prepareAndSendTx({
|
|
81
|
+
walletAccount,
|
|
82
|
+
to: asset.contracts.EVERSTAKE_VALIDATOR_CONTRACT_ADDR,
|
|
83
|
+
txData: txDelegateData,
|
|
84
|
+
gasPrice,
|
|
85
|
+
gasLimit,
|
|
86
|
+
fee,
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
await stakingProvider.notifyStaking({
|
|
90
|
+
txId,
|
|
91
|
+
asset: asset.name,
|
|
92
|
+
delegator: delegatorAddress,
|
|
93
|
+
amount: amount.toBaseString(),
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
return txId
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function undelegate({ walletAccount, amount } = {}) {
|
|
100
|
+
const { asset } = await getStakeAssets()
|
|
101
|
+
const address = await assetClientInterface.getReceiveAddress({
|
|
102
|
+
assetName: asset.name,
|
|
103
|
+
walletAccount,
|
|
104
|
+
})
|
|
105
|
+
const delegatorAddress = address.toLowerCase()
|
|
106
|
+
|
|
107
|
+
amount = amountToCurrency({ asset, amount })
|
|
108
|
+
|
|
109
|
+
const txUndelegateData = await stakingServer.undelegate({ amount })
|
|
110
|
+
const { gasPrice, gasLimit, fee } = await estimateTxFee(
|
|
111
|
+
delegatorAddress.toLowerCase(),
|
|
112
|
+
asset.contracts.EVERSTAKE_VALIDATOR_CONTRACT_ADDR,
|
|
113
|
+
txUndelegateData
|
|
114
|
+
)
|
|
115
|
+
return prepareAndSendTx({
|
|
116
|
+
walletAccount,
|
|
117
|
+
to: asset.contracts.EVERSTAKE_VALIDATOR_CONTRACT_ADDR,
|
|
118
|
+
txData: txUndelegateData,
|
|
119
|
+
gasPrice,
|
|
120
|
+
gasLimit,
|
|
121
|
+
fee,
|
|
122
|
+
})
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function claimRewards({ walletAccount } = {}) {
|
|
126
|
+
const { asset } = await getStakeAssets()
|
|
127
|
+
const address = await assetClientInterface.getReceiveAddress({
|
|
128
|
+
assetName: asset.name,
|
|
129
|
+
walletAccount,
|
|
130
|
+
})
|
|
131
|
+
const delegatorAddress = address.toLowerCase()
|
|
132
|
+
|
|
133
|
+
const txWithdrawRewardsData = await stakingServer.withdrawRewards()
|
|
134
|
+
const { gasPrice, gasLimit, fee } = await estimateTxFee(
|
|
135
|
+
delegatorAddress,
|
|
136
|
+
asset.contracts.EVERSTAKE_VALIDATOR_CONTRACT_ADDR,
|
|
137
|
+
txWithdrawRewardsData
|
|
138
|
+
)
|
|
139
|
+
return prepareAndSendTx({
|
|
140
|
+
walletAccount,
|
|
141
|
+
to: asset.contracts.EVERSTAKE_VALIDATOR_CONTRACT_ADDR,
|
|
142
|
+
txData: txWithdrawRewardsData,
|
|
143
|
+
gasPrice,
|
|
144
|
+
gasLimit,
|
|
145
|
+
fee,
|
|
146
|
+
})
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function claimUndelegatedBalance({ walletAccount, unbondNonce } = {}) {
|
|
150
|
+
const { asset } = await getStakeAssets()
|
|
151
|
+
const address = await assetClientInterface.getReceiveAddress({
|
|
152
|
+
assetName: asset.name,
|
|
153
|
+
walletAccount,
|
|
154
|
+
})
|
|
155
|
+
const delegatorAddress = address.toLowerCase()
|
|
156
|
+
|
|
157
|
+
const { currency } = asset
|
|
158
|
+
const unstakedClaimInfo = await fetchUnstakedClaimInfo({
|
|
159
|
+
stakingServer,
|
|
160
|
+
delegator: delegatorAddress,
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
const { unclaimedUndelegatedBalance } = await getUnstakedUnclaimedInfo({
|
|
164
|
+
stakingServer,
|
|
165
|
+
currency,
|
|
166
|
+
delegator: delegatorAddress,
|
|
167
|
+
...unstakedClaimInfo,
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
const txClaimUndelegatedData = await stakingServer.claimUndelegatedBalance({ unbondNonce })
|
|
171
|
+
const { gasPrice, gasLimit, fee } = await estimateTxFee(
|
|
172
|
+
delegatorAddress,
|
|
173
|
+
asset.contracts.EVERSTAKE_VALIDATOR_CONTRACT_ADDR,
|
|
174
|
+
txClaimUndelegatedData
|
|
175
|
+
)
|
|
176
|
+
const txId = await prepareAndSendTx({
|
|
177
|
+
walletAccount,
|
|
178
|
+
to: asset.contracts.EVERSTAKE_VALIDATOR_CONTRACT_ADDR,
|
|
179
|
+
txData: txClaimUndelegatedData,
|
|
180
|
+
gasPrice,
|
|
181
|
+
gasLimit,
|
|
182
|
+
fee,
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
await stakingProvider.notifyUnstaking({
|
|
186
|
+
txId,
|
|
187
|
+
asset: asset.name,
|
|
188
|
+
delegator: delegatorAddress,
|
|
189
|
+
amount: unclaimedUndelegatedBalance.toBaseString(),
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
return txId
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async function estimateDelegateOperation({ walletAccount, operation, args }) {
|
|
196
|
+
const delegateOperation = stakingServer[operation]
|
|
197
|
+
|
|
198
|
+
if (!delegateOperation) {
|
|
199
|
+
return
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const { asset } = await getStakeAssets()
|
|
203
|
+
const address = await assetClientInterface.getReceiveAddress({
|
|
204
|
+
assetName: asset.name,
|
|
205
|
+
walletAccount,
|
|
206
|
+
})
|
|
207
|
+
const delegatorAddress = address.toLowerCase()
|
|
208
|
+
|
|
209
|
+
const { amount } = args
|
|
210
|
+
if (amount) {
|
|
211
|
+
args = { ...args, amount: amountToCurrency({ asset, amount }) }
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const operationTxData = await delegateOperation({ ...args, walletAccount })
|
|
215
|
+
const { fee } = await estimateTxFee(
|
|
216
|
+
delegatorAddress,
|
|
217
|
+
asset.contracts.EVERSTAKE_VALIDATOR_CONTRACT_ADDR,
|
|
218
|
+
operationTxData
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
return fee
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Estimating delegete tx using {estimateGasLimit} function does not work
|
|
226
|
+
* as the execution reverts (due to missing MATIC approval).
|
|
227
|
+
* Instead, a fixed gas limit is use, which is:
|
|
228
|
+
*
|
|
229
|
+
* delegateGasLimit = ERC20ApproveGas + delegateGas
|
|
230
|
+
*
|
|
231
|
+
* This is just for displaying purposes and it's just an aproximation of the delegate gas cost,
|
|
232
|
+
* NOT the real fee cost
|
|
233
|
+
*/
|
|
234
|
+
async function estimateDelegateTxFee() {
|
|
235
|
+
// approx gas limits
|
|
236
|
+
const { feeAsset } = await getStakeAssets()
|
|
237
|
+
const erc20ApproveGas = 4900
|
|
238
|
+
const delegateGas = 240_000
|
|
239
|
+
const gasPrice = parseInt(await server.gasPrice(), 16)
|
|
240
|
+
const extraPercentage = 20
|
|
241
|
+
|
|
242
|
+
const gasLimit = erc20ApproveGas + delegateGas
|
|
243
|
+
const gasLimitWithBuffer = new BN(gasLimit)
|
|
244
|
+
.imuln(100 + extraPercentage)
|
|
245
|
+
.idivn(100)
|
|
246
|
+
.toString()
|
|
247
|
+
|
|
248
|
+
const fee = new BN(gasLimitWithBuffer).mul(new BN(gasPrice))
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
gasLimit: gasLimitWithBuffer,
|
|
252
|
+
gasPrice,
|
|
253
|
+
fee: feeAsset.currency.baseUnit(fee.toString()),
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async function estimateTxFee(from, to, txInput, gasPrice = '0x0') {
|
|
258
|
+
const { feeAsset } = await getStakeAssets()
|
|
259
|
+
|
|
260
|
+
const amount = feeAsset.currency.ZERO
|
|
261
|
+
const gasLimit = await estimateGasLimit(
|
|
262
|
+
feeAsset,
|
|
263
|
+
from,
|
|
264
|
+
to,
|
|
265
|
+
amount, // staking contracts does not require ETH amount to interact with
|
|
266
|
+
txInput,
|
|
267
|
+
gasPrice
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
if (gasPrice === '0x0') {
|
|
271
|
+
gasPrice = await server.gasPrice()
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
gasPrice = parseInt(gasPrice, 16)
|
|
275
|
+
const fee = new BN(gasPrice).mul(new BN(gasLimit))
|
|
276
|
+
|
|
277
|
+
return {
|
|
278
|
+
gasLimit,
|
|
279
|
+
gasPrice: feeAsset.currency.baseUnit(gasPrice),
|
|
280
|
+
fee: feeAsset.currency.baseUnit(fee.toString()),
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async function prepareAndSendTx({
|
|
285
|
+
walletAccount,
|
|
286
|
+
to,
|
|
287
|
+
txData: txInput,
|
|
288
|
+
gasPrice,
|
|
289
|
+
gasLimit,
|
|
290
|
+
fee,
|
|
291
|
+
waitForConfirmation = false,
|
|
292
|
+
} = {}) {
|
|
293
|
+
const { asset, feeAsset } = await getStakeAssets()
|
|
294
|
+
|
|
295
|
+
const sendTxArgs = {
|
|
296
|
+
asset: feeAsset,
|
|
297
|
+
walletAccount,
|
|
298
|
+
address: to,
|
|
299
|
+
amount: feeAsset.currency.ZERO,
|
|
300
|
+
// used in desktop,
|
|
301
|
+
// remove once, txSend is unified
|
|
302
|
+
receiver: {
|
|
303
|
+
address: to,
|
|
304
|
+
amount: feeAsset.currency.ZERO,
|
|
305
|
+
},
|
|
306
|
+
options: {
|
|
307
|
+
shouldLog: true,
|
|
308
|
+
txInput,
|
|
309
|
+
gasPrice,
|
|
310
|
+
gasLimit,
|
|
311
|
+
feeAmount: fee,
|
|
312
|
+
},
|
|
313
|
+
waitForConfirmation,
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const { txId } = await asset.api.sendTx(sendTxArgs)
|
|
317
|
+
|
|
318
|
+
return txId
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const getStakingInfo = getPolygonStakingInfo({ assetClientInterface, stakingServer })
|
|
322
|
+
|
|
323
|
+
return {
|
|
324
|
+
approveDelegateAmount,
|
|
325
|
+
delegate,
|
|
326
|
+
undelegate,
|
|
327
|
+
claimRewards,
|
|
328
|
+
claimUndelegatedBalance,
|
|
329
|
+
getStakingInfo,
|
|
330
|
+
estimateDelegateTxFee,
|
|
331
|
+
estimateDelegateOperation,
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
async function fetchUnstakedClaimInfo({ stakingServer, delegator }) {
|
|
336
|
+
const [unbondNonce, withdrawalDelay, currentEpoch, withdrawExchangeRate] = await Promise.all([
|
|
337
|
+
stakingServer.getCurrentUnbondNonce(delegator),
|
|
338
|
+
stakingServer.getWithdrawalDelay(),
|
|
339
|
+
stakingServer.getCurrentCheckpoint(),
|
|
340
|
+
stakingServer.getWithdrawExchangeRate(),
|
|
341
|
+
])
|
|
342
|
+
|
|
343
|
+
return {
|
|
344
|
+
unbondNonce,
|
|
345
|
+
withdrawalDelay,
|
|
346
|
+
currentEpoch,
|
|
347
|
+
withdrawExchangeRate,
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function calculateUnclaimedTokens({
|
|
352
|
+
currency,
|
|
353
|
+
exchangeRatePrecision,
|
|
354
|
+
withdrawExchangeRate,
|
|
355
|
+
shares,
|
|
356
|
+
canClaimUndelegatedBalance,
|
|
357
|
+
isUndelegateInProgress,
|
|
358
|
+
}) {
|
|
359
|
+
// see contract implementation
|
|
360
|
+
// https://github.com/maticnetwork/contracts/blob/1eb6960e511a967c15d4936904570a890d134fa6/contracts/staking/validatorShare/ValidatorShare.sol#L304
|
|
361
|
+
if (canClaimUndelegatedBalance || isUndelegateInProgress) {
|
|
362
|
+
const unclaimedTokens = withdrawExchangeRate
|
|
363
|
+
.mul(shares) // shares === validator tokens
|
|
364
|
+
.div(exchangeRatePrecision)
|
|
365
|
+
.toString()
|
|
366
|
+
|
|
367
|
+
return currency.baseUnit(unclaimedTokens)
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
return currency.ZERO
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function canClaimUndelegatedBalance({ shares, withdrawEpoch, isUndelegateInProgress }) {
|
|
374
|
+
const undelegateNotStarted = shares.isZero() && withdrawEpoch.isZero()
|
|
375
|
+
return !(isUndelegateInProgress || undelegateNotStarted)
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
async function getUnstakedUnclaimedInfo({
|
|
379
|
+
stakingServer,
|
|
380
|
+
currency,
|
|
381
|
+
delegator,
|
|
382
|
+
unbondNonce,
|
|
383
|
+
currentEpoch,
|
|
384
|
+
withdrawalDelay,
|
|
385
|
+
withdrawExchangeRate,
|
|
386
|
+
}) {
|
|
387
|
+
const { withdrawEpoch, shares } = await stakingServer.getUnboundInfo(delegator, unbondNonce)
|
|
388
|
+
const exchangeRatePrecision = stakingServer.EXCHANGE_RATE_PRECISION
|
|
389
|
+
const isUndelegateInProgress =
|
|
390
|
+
!withdrawEpoch.isZero() && withdrawEpoch.add(withdrawalDelay).gte(currentEpoch)
|
|
391
|
+
const isUndelegatedBalanceClaimable = canClaimUndelegatedBalance({
|
|
392
|
+
shares,
|
|
393
|
+
withdrawEpoch,
|
|
394
|
+
isUndelegateInProgress,
|
|
395
|
+
})
|
|
396
|
+
const unclaimedUndelegatedBalance = calculateUnclaimedTokens({
|
|
397
|
+
currency,
|
|
398
|
+
exchangeRatePrecision,
|
|
399
|
+
withdrawExchangeRate,
|
|
400
|
+
shares,
|
|
401
|
+
canClaimUndelegatedBalance: isUndelegatedBalanceClaimable,
|
|
402
|
+
isUndelegateInProgress,
|
|
403
|
+
})
|
|
404
|
+
return {
|
|
405
|
+
isUndelegateInProgress,
|
|
406
|
+
canClaimUndelegatedBalance: isUndelegatedBalanceClaimable,
|
|
407
|
+
unclaimedUndelegatedBalance,
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
async function fetchRewardsInfo({ stakingServer, delegator, currency }) {
|
|
412
|
+
const [minRewardsToWithdraw, rewardsBalance] = await Promise.all([
|
|
413
|
+
stakingServer.getMinRewardsToWithdraw(),
|
|
414
|
+
stakingServer.getLiquidRewards(delegator),
|
|
415
|
+
])
|
|
416
|
+
const withdrawable = rewardsBalance.sub(minRewardsToWithdraw).gte(currency.ZERO)
|
|
417
|
+
? rewardsBalance
|
|
418
|
+
: currency.ZERO
|
|
419
|
+
|
|
420
|
+
return {
|
|
421
|
+
rewardsBalance,
|
|
422
|
+
minRewardsToWithdraw,
|
|
423
|
+
withdrawable,
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
export function getPolygonStakingInfo({ assetClientInterface, stakingServer }) {
|
|
428
|
+
return async function ({ address }) {
|
|
429
|
+
const { polygon: asset } = await assetClientInterface.getAssetsForNetwork({
|
|
430
|
+
baseAssetName: 'ethereum', // Polygon token lives in ETH network
|
|
431
|
+
})
|
|
432
|
+
const { currency } = asset
|
|
433
|
+
const delegator = address.toLowerCase()
|
|
434
|
+
const [
|
|
435
|
+
delegatedBalance,
|
|
436
|
+
{ rewardsBalance, minRewardsToWithdraw, withdrawable },
|
|
437
|
+
{ unbondNonce, withdrawalDelay, currentEpoch, withdrawExchangeRate },
|
|
438
|
+
] = await Promise.all([
|
|
439
|
+
stakingServer.getTotalStake(delegator),
|
|
440
|
+
fetchRewardsInfo({ stakingServer, delegator, currency }),
|
|
441
|
+
fetchUnstakedClaimInfo({ stakingServer, delegator }),
|
|
442
|
+
])
|
|
443
|
+
const minDelegateAmount = currency.defaultUnit(1)
|
|
444
|
+
const isDelegating = !delegatedBalance.isZero
|
|
445
|
+
|
|
446
|
+
const unclaimedUndelegatedInfo = await getUnstakedUnclaimedInfo({
|
|
447
|
+
stakingServer,
|
|
448
|
+
currency,
|
|
449
|
+
delegator,
|
|
450
|
+
unbondNonce,
|
|
451
|
+
currentEpoch,
|
|
452
|
+
withdrawalDelay,
|
|
453
|
+
withdrawExchangeRate,
|
|
454
|
+
})
|
|
455
|
+
|
|
456
|
+
return {
|
|
457
|
+
rewardsBalance,
|
|
458
|
+
withdrawable,
|
|
459
|
+
unbondNonce,
|
|
460
|
+
isDelegating,
|
|
461
|
+
delegatedBalance,
|
|
462
|
+
minRewardsToWithdraw,
|
|
463
|
+
minDelegateAmount,
|
|
464
|
+
...unclaimedUndelegatedInfo,
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import assert from 'minimalistic-assert'
|
|
2
|
+
|
|
3
|
+
import { mainnetContracts, methodIds as methodIds_ } from './contracts/index.js'
|
|
4
|
+
import { txFiltersFactory } from './tx-filters/index.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Used in ethereum-hooks to extract the final tx amount and the staking type
|
|
8
|
+
*
|
|
9
|
+
* tx.amount is altered for staking txs
|
|
10
|
+
*
|
|
11
|
+
* - amount is set to ZERO
|
|
12
|
+
* - rewards are derived from the tx (only applicable to Polygon. Ethereum has more complicated rewards distribution)
|
|
13
|
+
* -
|
|
14
|
+
*/
|
|
15
|
+
function getStakingTxDetailsFactory({
|
|
16
|
+
isDelegateTx,
|
|
17
|
+
isUndelegateTx,
|
|
18
|
+
isClaimUndelegateTx,
|
|
19
|
+
decodeStakingTxAmount,
|
|
20
|
+
calculateRewardsFromStakeTx,
|
|
21
|
+
}) {
|
|
22
|
+
return function ({ tx, currency }) {
|
|
23
|
+
if (
|
|
24
|
+
['delegate', 'undelegate', 'claimUndelegate'].some((stakingType) => tx.data?.[stakingType]) &&
|
|
25
|
+
tx.coinAmount.isZero
|
|
26
|
+
) {
|
|
27
|
+
return
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const txAmount = tx.coinAmount.toDefaultString()
|
|
31
|
+
|
|
32
|
+
if (isDelegateTx(tx)) {
|
|
33
|
+
const delegate = currency.baseUnit(decodeStakingTxAmount(tx)).toDefaultString()
|
|
34
|
+
// MATIC returned in unstake tx is always reward
|
|
35
|
+
const rewards = calculateRewardsFromStakeTx({ tx, currency })
|
|
36
|
+
return { delegate, txAmount, ...(rewards ? { rewards } : {}) }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (isUndelegateTx(tx)) {
|
|
40
|
+
const undelegate = currency.baseUnit(decodeStakingTxAmount(tx)).toDefaultString()
|
|
41
|
+
// MATIC returned in unstake tx is always reward
|
|
42
|
+
const rewards = txAmount
|
|
43
|
+
return { undelegate, txAmount, rewards }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (isClaimUndelegateTx(tx)) {
|
|
47
|
+
return { claimUndelegate: txAmount, txAmount }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// not a staking tx
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const calculateRewardsFromStakeTxFactory =
|
|
55
|
+
({ decodeStakingTxAmount }) =>
|
|
56
|
+
({ tx, currency }) => {
|
|
57
|
+
const stakedAmount = currency.baseUnit(decodeStakingTxAmount(tx))
|
|
58
|
+
const { reward } = tx.data
|
|
59
|
+
// stake tx might have rewards in it,
|
|
60
|
+
// i.e: https://etherscan.io/tx/0x0a81d266109034a3a70c6f1b9601c105d8caebbd0de652a0619344f9559ae4fa
|
|
61
|
+
// thus reward = txInputAmount(amount to stake) - tx.coinAmount
|
|
62
|
+
// tx.coinAmount is already computed from monitor; incoming - outgoing ERC20 token txs
|
|
63
|
+
const stakeTxContainsReward = !stakedAmount.equals(tx.coinAmount.abs())
|
|
64
|
+
|
|
65
|
+
if (reward) {
|
|
66
|
+
// cache rewards
|
|
67
|
+
return currency.baseUnit(reward)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (stakeTxContainsReward) {
|
|
71
|
+
const txAmount = stakedAmount.sub(tx.coinAmount.abs()).abs()
|
|
72
|
+
// eslint-disable-next-line @exodus/mutable/no-param-reassign-prop-only -- TODO: Fix this the next time the file is edited.
|
|
73
|
+
return txAmount.toDefaultString()
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const decodeStakingTxAmountFactory =
|
|
78
|
+
({ validatorShareContract }) =>
|
|
79
|
+
(tx) => {
|
|
80
|
+
const {
|
|
81
|
+
data: { data: txInput },
|
|
82
|
+
} = tx
|
|
83
|
+
|
|
84
|
+
const {
|
|
85
|
+
values: [amount], // stake or unstake amount
|
|
86
|
+
} = validatorShareContract.decodeInput(txInput)
|
|
87
|
+
|
|
88
|
+
return amount
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export const txUtilsFactory = ({
|
|
92
|
+
stakingServer,
|
|
93
|
+
contracts = mainnetContracts,
|
|
94
|
+
methodIds = methodIds_,
|
|
95
|
+
}) => {
|
|
96
|
+
assert(stakingServer, 'stakingServer is required')
|
|
97
|
+
|
|
98
|
+
const { isDelegateTx, isUndelegateTx, isClaimUndelegateTx, getStakingTxLogFilter } =
|
|
99
|
+
txFiltersFactory({ contracts, methodIds })
|
|
100
|
+
|
|
101
|
+
const { validatorShareContract } = stakingServer
|
|
102
|
+
const decodeStakingTxAmount = decodeStakingTxAmountFactory({ validatorShareContract })
|
|
103
|
+
const calculateRewardsFromStakeTx = calculateRewardsFromStakeTxFactory({ decodeStakingTxAmount })
|
|
104
|
+
|
|
105
|
+
const getStakingTxDetails = getStakingTxDetailsFactory({
|
|
106
|
+
isDelegateTx,
|
|
107
|
+
isUndelegateTx,
|
|
108
|
+
isClaimUndelegateTx,
|
|
109
|
+
decodeStakingTxAmount,
|
|
110
|
+
calculateRewardsFromStakeTx,
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
getStakingTxLogFilter,
|
|
115
|
+
getStakingTxDetails,
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export const txFiltersFactory = ({ contracts, methodIds }) => {
|
|
2
|
+
const isAssetTx = ({ coinName }) => coinName === 'polygon'
|
|
3
|
+
const isDelegateTx = (tx) =>
|
|
4
|
+
isAssetTx(tx) &&
|
|
5
|
+
tx.to === contracts.STAKING_MANAGER_ADDR &&
|
|
6
|
+
tx.data?.methodId === methodIds.DELEGATE
|
|
7
|
+
const isUndelegateTx = (tx) =>
|
|
8
|
+
isAssetTx(tx) &&
|
|
9
|
+
tx.from[0] === contracts.STAKING_MANAGER_ADDR &&
|
|
10
|
+
tx.data?.methodId === methodIds.UNDELEGATE
|
|
11
|
+
const isRewardTx = (tx) =>
|
|
12
|
+
isAssetTx(tx) &&
|
|
13
|
+
tx.from[0] === contracts.STAKING_MANAGER_ADDR &&
|
|
14
|
+
tx.data?.methodId === methodIds.CLAIM_REWARD
|
|
15
|
+
const isClaimUndelegateTx = (tx) =>
|
|
16
|
+
isAssetTx(tx) &&
|
|
17
|
+
tx.from[0] === contracts.STAKING_MANAGER_ADDR &&
|
|
18
|
+
tx.data?.methodId === methodIds.CLAIM_UNDELEGATE
|
|
19
|
+
|
|
20
|
+
const isStakingTx = (tx) => tx.to === contracts.EVERSTAKE_VALIDATOR_CONTRACT_ADDR
|
|
21
|
+
|
|
22
|
+
const getStakingTxLogFilter = (tx) =>
|
|
23
|
+
isDelegateTx(tx) || isClaimUndelegateTx(tx) || isUndelegateTx(tx)
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
isAssetTx,
|
|
27
|
+
isStakingTx,
|
|
28
|
+
isDelegateTx,
|
|
29
|
+
isUndelegateTx,
|
|
30
|
+
isClaimUndelegateTx,
|
|
31
|
+
isRewardTx,
|
|
32
|
+
getStakingTxLogFilter,
|
|
33
|
+
}
|
|
34
|
+
}
|
package/src/staking.js
CHANGED
|
@@ -1,26 +1,13 @@
|
|
|
1
1
|
import { ethStakeAccountState } from '@exodus/ethereum-lib'
|
|
2
|
-
import { asset
|
|
2
|
+
import { asset } from '@exodus/ethereum-meta'
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
export const polygonStakeAccountState = ({ currency }) => ({
|
|
7
|
-
isDelegating: false,
|
|
8
|
-
// Unstaking takes ~3-4 days, after that, user needs to
|
|
9
|
-
// claim the unstaked amount (initial staked amount)
|
|
10
|
-
isUndelegateInProgress: false,
|
|
11
|
-
canClaimUndelegatedBalance: false,
|
|
12
|
-
minRewardsToWithdraw: currency.defaultUnit(1),
|
|
13
|
-
minDelegateAmount: currency.defaultUnit(1),
|
|
14
|
-
unclaimedUndelegatedBalance: currency.ZERO,
|
|
15
|
-
delegatedBalance: currency.ZERO,
|
|
16
|
-
rewardsBalance: currency.ZERO,
|
|
17
|
-
withdrawable: currency.ZERO,
|
|
18
|
-
unbondNonce: '0',
|
|
19
|
-
})
|
|
4
|
+
import { polygonStakingConfig, polygonStakingDeps } from './staking/polygon/index.js'
|
|
20
5
|
|
|
21
6
|
export const stakingConfiguration = {
|
|
22
7
|
ethereum: { accountStateExtraData: ethStakeAccountState(asset) },
|
|
23
|
-
polygon:
|
|
24
|
-
|
|
25
|
-
|
|
8
|
+
polygon: polygonStakingConfig,
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const stakingDependencies = {
|
|
12
|
+
polygon: polygonStakingDeps,
|
|
26
13
|
}
|