@exodus/ethereum-api 6.3.35 → 7.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/ethereum-api",
3
- "version": "6.3.35",
3
+ "version": "7.0.0",
4
4
  "description": "Ethereum Api",
5
5
  "main": "src/index.js",
6
6
  "files": [
@@ -16,6 +16,7 @@
16
16
  "dependencies": {
17
17
  "@exodus/asset-lib": "^3.7.1",
18
18
  "@exodus/crypto": "^1.0.0-rc.0",
19
+ "@exodus/currency": "^2.1.3",
19
20
  "@exodus/ethereum-lib": "^4.0.3",
20
21
  "@exodus/ethereumjs-util": "^7.1.0-exodus.6",
21
22
  "@exodus/fetch": "^1.3.0-beta.4",
@@ -32,7 +33,10 @@
32
33
  "ws": "^6.1.0"
33
34
  },
34
35
  "devDependencies": {
35
- "@exodus/models": "^8.10.4"
36
+ "@exodus/assets": "9.1.0",
37
+ "@exodus/assets-base": "^8.1.14",
38
+ "@exodus/assets-testing": "file:../../../__testing__",
39
+ "@exodus/models": "^11.0.0"
36
40
  },
37
- "gitHead": "66d65ee9561df9952741317a358659a600dabe19"
41
+ "gitHead": "1ebfe4c6b2ba3cc49564290a498571e5f3921a5d"
38
42
  }
@@ -1,5 +1,4 @@
1
- export { MaticStaking } from './matic-staking'
2
1
  export { FantomStaking } from './fantom-staking'
3
2
  export { stakingProviderClientFactory } from './staking-provider-client'
4
- export * from './matic-staking-utils'
5
3
  export * from './ethereum'
4
+ export * from './matic'
@@ -1,21 +1,20 @@
1
1
  import BN from 'bn.js'
2
2
  import { createContract } from '@exodus/ethereum-lib'
3
3
  import ethAssets from '@exodus/ethereum-meta'
4
- import { getServerByName } from '../exodus-eth-server'
4
+ import { getServerByName } from '../../exodus-eth-server'
5
5
  import { retry } from '@exodus/simple-retry'
6
6
  import { bufferToHex } from '@exodus/ethereumjs-util'
7
7
 
8
- const EVERSTAKE_VALIDATOR_CONTRACT_ADDR = '0xf30cf4ed712d3734161fdaab5b1dbb49fd2d0e5c'
9
- const STAKING_MANAGER_ADDR = '0x5e3ef299fddf15eaa0432e6e66473ace8c13d908'
10
-
11
8
  const polygonEthToken = ethAssets.find(({ name: tokenName }) => tokenName === 'polygon')
12
9
 
13
10
  const RETRY_DELAYS = ['10s']
11
+ export class MaticStakingApi {
12
+ static EVERSTAKE_VALIDATOR_CONTRACT_ADDR = '0xf30cf4ed712d3734161fdaab5b1dbb49fd2d0e5c'
13
+ static STAKING_MANAGER_ADDR = '0x5e3ef299fddf15eaa0432e6e66473ace8c13d908'
14
14
 
15
- export class MaticStaking {
16
15
  constructor(
17
- validatorId = EVERSTAKE_VALIDATOR_CONTRACT_ADDR,
18
- stakeManagerAddr = STAKING_MANAGER_ADDR
16
+ validatorId = MaticStakingApi.EVERSTAKE_VALIDATOR_CONTRACT_ADDR,
17
+ stakeManagerAddr = MaticStakingApi.STAKING_MANAGER_ADDR
19
18
  ) {
20
19
  // harcoded exchange rate from the validtor share contract
21
20
  // in order to calculate claim unstake amount off-chain
@@ -0,0 +1,9 @@
1
+ export { MaticStakingApi } from './api'
2
+ export { createPolygonStakingService, getPolygonStakingInfo } from './service'
3
+ export {
4
+ isPolygonTx,
5
+ isPolygonDelegate,
6
+ isPolygonUndelegate,
7
+ isPolygonReward,
8
+ isPolygonClaimUndelegate,
9
+ } from './matic-staking-utils'
@@ -1,4 +1,4 @@
1
- import { MaticStaking } from './matic-staking'
1
+ import { MaticStakingApi } from './api'
2
2
 
3
3
  // function selector for buyVoucher(uint256 _amount, uint256 _minSharesToMint)
4
4
  const DELEGATE = '0x6ab15071'
@@ -9,23 +9,16 @@ const CLAIM_REWARD = '0xc7b8981c'
9
9
  // function selector for unstakeClaimTokens_new(uint256 unbondNonce)
10
10
  const CLAIM_UNDELEGATE_BALANCE = '0xe97fddc2'
11
11
 
12
- const STAKING_MANAGER_CONTRACT = new MaticStaking().stakingManagerContract.address
12
+ const STAKING_MANAGER_CONTRACT = MaticStakingApi.STAKING_MANAGER_ADDR
13
13
 
14
14
  export const isPolygonTx = ({ coinName }) => coinName === 'polygon'
15
15
  export const isPolygonDelegate = (tx) =>
16
16
  isPolygonTx(tx) && tx.to === STAKING_MANAGER_CONTRACT && tx.data?.methodId === DELEGATE
17
17
  export const isPolygonUndelegate = (tx) =>
18
- isPolygonTx(tx) &&
19
- tx.from[0] === STAKING_MANAGER_CONTRACT &&
20
- tx.data &&
21
- tx.data.methodId === UNDELEGATE
18
+ isPolygonTx(tx) && tx.from[0] === STAKING_MANAGER_CONTRACT && tx.data?.methodId === UNDELEGATE
22
19
  export const isPolygonReward = (tx) =>
23
- isPolygonTx(tx) &&
24
- tx.from[0] === STAKING_MANAGER_CONTRACT &&
25
- tx.data &&
26
- tx.data.methodId === CLAIM_REWARD
20
+ isPolygonTx(tx) && tx.from[0] === STAKING_MANAGER_CONTRACT && tx.data?.methodId === CLAIM_REWARD
27
21
  export const isPolygonClaimUndelegate = (tx) =>
28
22
  isPolygonTx(tx) &&
29
23
  tx.from[0] === STAKING_MANAGER_CONTRACT &&
30
- tx.data &&
31
- tx.data.methodId === CLAIM_UNDELEGATE_BALANCE
24
+ tx.data?.methodId === CLAIM_UNDELEGATE_BALANCE
@@ -0,0 +1,442 @@
1
+ import BN from 'bn.js'
2
+ import { getServer } from '../../exodus-eth-server'
3
+ import { estimateGasLimit } from '../../gas-estimation'
4
+ import { MaticStakingApi } from './api'
5
+ import { stakingProviderClientFactory } from '../staking-provider-client'
6
+ import { isNumberUnit } from '@exodus/currency'
7
+
8
+ export function createPolygonStakingService({ assetClientInterface, createAndBroadcastTX }) {
9
+ const stakingApi = new MaticStakingApi()
10
+ const assetName = 'ethereum'
11
+ const stakingProvider = stakingProviderClientFactory()
12
+
13
+ async function getStakeAssets() {
14
+ const { polygon: asset, ethereum: feeAsset } = await assetClientInterface.getAssetsForNetwork({
15
+ baseAssetName: assetName,
16
+ })
17
+ return { asset, feeAsset }
18
+ }
19
+
20
+ function amountToCurrency({ asset, amount }) {
21
+ return isNumberUnit(amount) ? amount : asset.currency.parse(amount)
22
+ }
23
+
24
+ async function delegate({ walletAccount, amount } = {}) {
25
+ const delegatorAddress = (
26
+ await assetClientInterface.getReceiveAddress({
27
+ assetName,
28
+ walletAccount,
29
+ })
30
+ ).toLowerCase()
31
+
32
+ const { asset } = await getStakeAssets()
33
+ amount = amountToCurrency({ asset, amount })
34
+
35
+ const txApproveData = await stakingApi.approveStakeManager(amount)
36
+ let { gasPrice, gasLimit, fee } = await estimateTxFee(
37
+ delegatorAddress,
38
+ stakingApi.polygonContract.address,
39
+ txApproveData
40
+ )
41
+ await prepareAndSendTx({
42
+ walletAccount,
43
+ waitForConfirmation: true,
44
+ to: stakingApi.polygonContract.address,
45
+ txData: txApproveData,
46
+ gasPrice,
47
+ gasLimit,
48
+ fee,
49
+ })
50
+
51
+ const txDelegateData = await stakingApi.delegate({ amount })
52
+ ;({ gasPrice, gasLimit, fee } = await estimateTxFee(
53
+ delegatorAddress,
54
+ stakingApi.validatorShareContract.address,
55
+ txDelegateData
56
+ ))
57
+
58
+ const txId = await prepareAndSendTx({
59
+ walletAccount,
60
+ to: stakingApi.validatorShareContract.address,
61
+ txData: txDelegateData,
62
+ gasPrice,
63
+ gasLimit,
64
+ fee,
65
+ })
66
+
67
+ await stakingProvider.notifyStaking({
68
+ txId,
69
+ asset: asset.name,
70
+ delegator: delegatorAddress,
71
+ amount: amount.toBaseString(),
72
+ })
73
+
74
+ return txId
75
+ }
76
+
77
+ async function undelegate({ walletAccount, amount } = {}) {
78
+ const delegatorAddress = (
79
+ await assetClientInterface.getReceiveAddress({
80
+ assetName,
81
+ walletAccount,
82
+ })
83
+ ).toLowerCase()
84
+
85
+ const { asset } = await getStakeAssets()
86
+ amount = amountToCurrency({ asset, amount })
87
+
88
+ const txUndelegateData = await stakingApi.undelegate({ amount })
89
+ let { gasPrice, gasLimit, fee } = await estimateTxFee(
90
+ delegatorAddress.toLowerCase(),
91
+ stakingApi.validatorShareContract.address,
92
+ txUndelegateData
93
+ )
94
+ const txId = await prepareAndSendTx({
95
+ walletAccount,
96
+ to: stakingApi.validatorShareContract.address,
97
+ txData: txUndelegateData,
98
+ gasPrice,
99
+ gasLimit,
100
+ fee,
101
+ })
102
+
103
+ return txId
104
+ }
105
+
106
+ async function claimRewards({ walletAccount } = {}) {
107
+ const delegatorAddress = (
108
+ await assetClientInterface.getReceiveAddress({
109
+ assetName,
110
+ walletAccount,
111
+ })
112
+ ).toLowerCase()
113
+
114
+ const txWithdrawRewardsData = await stakingApi.withdrawRewards()
115
+ let { gasPrice, gasLimit, fee } = await estimateTxFee(
116
+ delegatorAddress,
117
+ stakingApi.validatorShareContract.address,
118
+ txWithdrawRewardsData
119
+ )
120
+ const txId = await prepareAndSendTx({
121
+ walletAccount,
122
+ to: stakingApi.validatorShareContract.address,
123
+ txData: txWithdrawRewardsData,
124
+ gasPrice,
125
+ gasLimit,
126
+ fee,
127
+ })
128
+
129
+ return txId
130
+ }
131
+
132
+ async function claimUndelegatedBalance({ walletAccount, unbondNonce } = {}) {
133
+ const delegatorAddress = (
134
+ await assetClientInterface.getReceiveAddress({
135
+ assetName,
136
+ walletAccount,
137
+ })
138
+ ).toLowerCase()
139
+
140
+ const { asset } = await getStakeAssets()
141
+ const { currency } = asset
142
+ const unstakedClaimInfo = await fetchUnstakedClaimInfo({
143
+ stakingApi,
144
+ delegator: delegatorAddress,
145
+ })
146
+
147
+ const { unclaimedUndelegatedBalance } = await getUnstakedUnclaimedInfo({
148
+ stakingApi,
149
+ currency,
150
+ delegator: delegatorAddress,
151
+ ...unstakedClaimInfo,
152
+ })
153
+
154
+ const txClaimUndelegatedData = await stakingApi.claimUndelegatedBalance({ unbondNonce })
155
+ let { gasPrice, gasLimit, fee } = await estimateTxFee(
156
+ delegatorAddress,
157
+ stakingApi.validatorShareContract.address,
158
+ txClaimUndelegatedData
159
+ )
160
+ const txId = await prepareAndSendTx({
161
+ walletAccount,
162
+ to: stakingApi.validatorShareContract.address,
163
+ txData: txClaimUndelegatedData,
164
+ gasPrice,
165
+ gasLimit,
166
+ fee,
167
+ })
168
+
169
+ await stakingProvider.notifyUnstaking({
170
+ txId,
171
+ asset: asset.name,
172
+ delegator: delegatorAddress,
173
+ amount: unclaimedUndelegatedBalance.toBaseString(),
174
+ })
175
+
176
+ return txId
177
+ }
178
+
179
+ async function estimateDelegateOperation({ walletAccount, operation, args }) {
180
+ const delegateOperation = stakingApi[operation]
181
+
182
+ if (!delegateOperation) {
183
+ return
184
+ }
185
+ const delegatorAddress = (
186
+ await assetClientInterface.getReceiveAddress({
187
+ assetName,
188
+ walletAccount,
189
+ })
190
+ ).toLowerCase()
191
+
192
+ const { amount } = args
193
+ if (amount) {
194
+ const { asset } = await getStakeAssets()
195
+ args = { ...args, amount: amountToCurrency({ asset, amount }) }
196
+ }
197
+
198
+ const operationTxData = await delegateOperation({ ...args, walletAccount })
199
+ const { fee } = await estimateTxFee(
200
+ delegatorAddress,
201
+ stakingApi.validatorShareContract.address,
202
+ operationTxData
203
+ )
204
+
205
+ return fee
206
+ }
207
+
208
+ /**
209
+ * Estimating delegete tx using {estimateGasLimit} function does not work
210
+ * as the execution reverts (due to missing MATIC approval).
211
+ * Instead, a fixed gas limit is use, which is:
212
+ *
213
+ * delegateGasLimit = ERC20ApproveGas + delegateGas
214
+ *
215
+ * This is just for displaying purposes and it's just an aproximation of the delegate gas cost,
216
+ * NOT the real fee cost
217
+ */
218
+ async function estimateDelegateTxFee() {
219
+ // approx gas limits
220
+ const { ethereum } = await assetClientInterface.getAssetsForNetwork({
221
+ baseAssetName: 'ethereum',
222
+ })
223
+ const erc20ApproveGas = 4900
224
+ const delegateGas = 240000
225
+ const gasPrice = parseInt(await getServer(ethereum).gasPrice(), 16)
226
+ const extraPercentage = 20
227
+
228
+ const gasLimit = erc20ApproveGas + delegateGas
229
+ const gasLimitWithBuffer = new BN(gasLimit)
230
+ .imuln(100 + extraPercentage)
231
+ .idivn(100)
232
+ .toString()
233
+
234
+ const fee = new BN(gasLimitWithBuffer).mul(new BN(gasPrice))
235
+
236
+ return {
237
+ gasLimit: gasLimitWithBuffer,
238
+ gasPrice,
239
+ fee: ethereum.currency.baseUnit(fee.toString()),
240
+ }
241
+ }
242
+
243
+ async function estimateTxFee(from, to, txInput, gasPrice = '0x0') {
244
+ const { ethereum } = await assetClientInterface.getAssetsForNetwork({
245
+ baseAssetName: 'ethereum',
246
+ })
247
+ const amount = ethereum.currency.ZERO
248
+ const gasLimit = await estimateGasLimit(
249
+ ethereum,
250
+ from,
251
+ to,
252
+ amount, // staking contracts does not require ETH amount to interact with
253
+ txInput,
254
+ gasPrice
255
+ )
256
+
257
+ if (gasPrice === '0x0') {
258
+ gasPrice = await getServer(ethereum).gasPrice()
259
+ }
260
+
261
+ gasPrice = parseInt(gasPrice, 16)
262
+ const fee = new BN(gasPrice).mul(new BN(gasLimit))
263
+
264
+ return {
265
+ gasLimit,
266
+ gasPrice: ethereum.currency.baseUnit(gasPrice),
267
+ fee: ethereum.currency.baseUnit(fee.toString()),
268
+ }
269
+ }
270
+
271
+ async function prepareAndSendTx({
272
+ walletAccount,
273
+ to,
274
+ txData: txInput,
275
+ gasPrice,
276
+ gasLimit,
277
+ fee,
278
+ waitForConfirmation = false,
279
+ } = {}) {
280
+ const { ethereum } = await assetClientInterface.getAssetsForNetwork({
281
+ baseAssetName: 'ethereum',
282
+ })
283
+ const sendTxArgs = {
284
+ asset: ethereum,
285
+ walletAccount,
286
+ address: to,
287
+ amount: ethereum.currency.ZERO,
288
+ options: {
289
+ shouldLog: true,
290
+ txInput,
291
+ gasPrice,
292
+ gasLimit,
293
+ feeAmount: fee,
294
+ },
295
+ waitForConfirmation,
296
+ }
297
+
298
+ const { txId } = await createAndBroadcastTX(sendTxArgs)
299
+
300
+ return txId
301
+ }
302
+
303
+ return {
304
+ delegate,
305
+ undelegate,
306
+ claimRewards,
307
+ claimUndelegatedBalance,
308
+ getPolygonStakingInfo,
309
+ estimateDelegateTxFee,
310
+ estimateDelegateOperation,
311
+ }
312
+ }
313
+
314
+ async function fetchUnstakedClaimInfo({ stakingApi, delegator }) {
315
+ const [unbondNonce, withdrawalDelay, currentEpoch, withdrawExchangeRate] = await Promise.all([
316
+ stakingApi.getCurrentUnbondNonce(delegator),
317
+ stakingApi.getWithdrawalDelay(),
318
+ stakingApi.getCurrentCheckpoint(),
319
+ stakingApi.getWithdrawExchangeRate(),
320
+ ])
321
+
322
+ return {
323
+ unbondNonce,
324
+ withdrawalDelay,
325
+ currentEpoch,
326
+ withdrawExchangeRate,
327
+ }
328
+ }
329
+
330
+ function calculateUnclaimedTokens({
331
+ currency,
332
+ exchangeRatePrecision,
333
+ withdrawExchangeRate,
334
+ shares,
335
+ canClaimUndelegatedBalance,
336
+ isUndelegateInProgress,
337
+ }) {
338
+ // see contract implementation
339
+ // https://github.com/maticnetwork/contracts/blob/1eb6960e511a967c15d4936904570a890d134fa6/contracts/staking/validatorShare/ValidatorShare.sol#L304
340
+ if (canClaimUndelegatedBalance || isUndelegateInProgress) {
341
+ const unclaimedTokens = withdrawExchangeRate
342
+ .mul(shares) // shares === validator tokens
343
+ .div(exchangeRatePrecision)
344
+ .toString()
345
+
346
+ return currency.baseUnit(unclaimedTokens)
347
+ }
348
+
349
+ return currency.ZERO
350
+ }
351
+
352
+ function canClaimUndelegatedBalance({ shares, withdrawEpoch, isUndelegateInProgress }) {
353
+ const undelegateNotStarted = shares.isZero() && withdrawEpoch.isZero()
354
+ return !(isUndelegateInProgress || undelegateNotStarted)
355
+ }
356
+
357
+ async function getUnstakedUnclaimedInfo({
358
+ stakingApi,
359
+ currency,
360
+ delegator,
361
+ unbondNonce,
362
+ currentEpoch,
363
+ withdrawalDelay,
364
+ withdrawExchangeRate,
365
+ }) {
366
+ const { withdrawEpoch, shares } = await stakingApi.getUnboundInfo(delegator, unbondNonce)
367
+ const exchangeRatePrecision = stakingApi.EXCHANGE_RATE_PRECISION
368
+ const isUndelegateInProgress =
369
+ !withdrawEpoch.isZero() && withdrawEpoch.add(withdrawalDelay).gte(currentEpoch)
370
+ const isUndelegatedBalanceClaimable = canClaimUndelegatedBalance({
371
+ shares,
372
+ withdrawEpoch,
373
+ isUndelegateInProgress,
374
+ })
375
+ const unclaimedUndelegatedBalance = calculateUnclaimedTokens({
376
+ currency,
377
+ exchangeRatePrecision,
378
+ withdrawExchangeRate,
379
+ shares,
380
+ canClaimUndelegatedBalance: isUndelegatedBalanceClaimable,
381
+ isUndelegateInProgress,
382
+ })
383
+ return {
384
+ isUndelegateInProgress,
385
+ canClaimUndelegatedBalance: isUndelegatedBalanceClaimable,
386
+ unclaimedUndelegatedBalance,
387
+ }
388
+ }
389
+
390
+ async function fetchRewardsInfo({ stakingApi, delegator, currency }) {
391
+ const [minRewardsToWithdraw, rewardsBalance] = await Promise.all([
392
+ stakingApi.getMinRewardsToWithdraw(),
393
+ stakingApi.getLiquidRewards(delegator),
394
+ ])
395
+ const withdrawable = rewardsBalance.sub(minRewardsToWithdraw).gte(currency.ZERO)
396
+ ? rewardsBalance
397
+ : currency.ZERO
398
+
399
+ return {
400
+ rewardsBalance,
401
+ minRewardsToWithdraw,
402
+ withdrawable,
403
+ }
404
+ }
405
+
406
+ export async function getPolygonStakingInfo({ address, asset }) {
407
+ const { currency } = asset
408
+ const stakingApi = new MaticStakingApi()
409
+ const delegator = address.toLowerCase()
410
+ const [
411
+ delegatedBalance,
412
+ { rewardsBalance, minRewardsToWithdraw, withdrawable },
413
+ { unbondNonce, withdrawalDelay, currentEpoch, withdrawExchangeRate },
414
+ ] = await Promise.all([
415
+ stakingApi.getTotalStake(delegator),
416
+ fetchRewardsInfo({ stakingApi, delegator, currency }),
417
+ fetchUnstakedClaimInfo({ stakingApi, delegator }),
418
+ ])
419
+ const minDelegateAmount = currency.defaultUnit(1)
420
+ const isDelegating = !delegatedBalance.isZero
421
+
422
+ const unclaimedUndelegatedInfo = await getUnstakedUnclaimedInfo({
423
+ stakingApi,
424
+ currency,
425
+ delegator,
426
+ unbondNonce,
427
+ currentEpoch,
428
+ withdrawalDelay,
429
+ withdrawExchangeRate,
430
+ })
431
+
432
+ return {
433
+ rewardsBalance,
434
+ withdrawable,
435
+ unbondNonce,
436
+ isDelegating,
437
+ delegatedBalance,
438
+ minRewardsToWithdraw,
439
+ minDelegateAmount,
440
+ ...unclaimedUndelegatedInfo,
441
+ }
442
+ }
@@ -76,6 +76,10 @@ export default function getLogItemsFromServerTx({
76
76
  internalTransfers,
77
77
  erc20Transfers,
78
78
  }),
79
+ currencies: {
80
+ [asset.name]: asset.currency,
81
+ [asset.feeAsset.name]: asset.feeAsset.currency,
82
+ },
79
83
  },
80
84
  ])
81
85
  }
@@ -118,6 +122,10 @@ export default function getLogItemsFromServerTx({
118
122
  ? { from: [], to: tokenTransferToAddress, feeAmount, feeCoinName: token.feeAsset.name }
119
123
  : { from: tokenFromAddresses }),
120
124
  selfSend,
125
+ currencies: {
126
+ [token.name]: token.currency,
127
+ [token.feeAsset.name]: token.feeAsset.currency,
128
+ },
121
129
  },
122
130
  ])
123
131
  })
@@ -75,6 +75,10 @@ export default function getLogItemsFromServerTx({
75
75
  serverTx,
76
76
  ourWalletAddress,
77
77
  }),
78
+ currencies: {
79
+ [asset.name]: asset.currency,
80
+ [asset.feeAsset.name]: asset.feeAsset.currency,
81
+ },
78
82
  },
79
83
  ])
80
84
  }
@@ -120,6 +124,10 @@ export default function getLogItemsFromServerTx({
120
124
  ? { from: [], to: tokenTransferToAddress, feeAmount, feeCoinName: asset.feeAsset.name }
121
125
  : { from: tokenFromAddresses }),
122
126
  selfSend,
127
+ currencies: {
128
+ [token.name]: token.currency,
129
+ [token.feeAsset.name]: token.feeAsset.currency,
130
+ },
123
131
  },
124
132
  ])
125
133
  })