@exodus/ethereum-api 9.0.0-alpha.2 → 9.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/CHANGELOG.md +2121 -0
  2. package/LICENSE +7 -0
  3. package/README.md +124 -0
  4. package/package.json +34 -33
  5. package/src/address-has-history.js +6 -2
  6. package/src/allowance/constants.js +4 -5
  7. package/src/allowance/index.js +159 -40
  8. package/src/check-tx/create-assess-transaction.js +23 -0
  9. package/src/check-tx/create-check-tx.js +104 -0
  10. package/src/check-tx/index.js +1 -0
  11. package/src/create-asset-plugin-factory.js +115 -0
  12. package/src/create-asset-utils.js +520 -0
  13. package/src/create-asset.js +272 -97
  14. package/src/create-token-factory.js +47 -17
  15. package/src/custom-fees.js +57 -0
  16. package/src/eip7702-whitelist.js +62 -0
  17. package/src/ens/index.js +12 -17
  18. package/src/error-wrapper.js +383 -0
  19. package/src/eth-like-util.js +172 -33
  20. package/src/exodus-eth-server/api-coin-nodes.js +27 -196
  21. package/src/exodus-eth-server/api.js +23 -14
  22. package/src/exodus-eth-server/clarity-v2.js +255 -0
  23. package/src/exodus-eth-server/clarity.js +67 -265
  24. package/src/exodus-eth-server/errors.js +40 -0
  25. package/src/exodus-eth-server/eth-like-server.js +385 -0
  26. package/src/exodus-eth-server/fetch-json.js +48 -0
  27. package/src/exodus-eth-server/index.js +77 -0
  28. package/src/exodus-eth-server/utils.js +31 -0
  29. package/src/exodus-eth-server/ws-gateway.js +306 -0
  30. package/src/exodus-eth-server/ws.js +1 -2
  31. package/src/fee-data/EthLikeFeeData.js +81 -0
  32. package/src/fee-data/index.js +54 -0
  33. package/src/fee-utils.js +229 -0
  34. package/src/gas-estimation.js +183 -66
  35. package/src/get-balance-for-address.js +1 -1
  36. package/src/get-balances.js +285 -27
  37. package/src/get-fee.js +92 -56
  38. package/src/get-historical-balance.js +58 -0
  39. package/src/hooks/index.js +1 -1
  40. package/src/hooks/monitor.js +182 -43
  41. package/src/hooks/staking-freshness.js +58 -0
  42. package/src/index.js +118 -12
  43. package/src/move-funds.js +153 -0
  44. package/src/multicall3/index.js +212 -0
  45. package/src/multisig-address.js +146 -0
  46. package/src/nft-utils.js +60 -0
  47. package/src/number-utils.js +34 -2
  48. package/src/optimism-gas/index.js +13 -5
  49. package/src/send-validations.js +58 -0
  50. package/src/server-based-fee-monitor.js +12 -19
  51. package/src/simulate-tx/fetch-tx-preview.js +0 -2
  52. package/src/simulate-tx/index.js +2 -2
  53. package/src/simulate-tx/simulate-eth-tx.js +24 -11
  54. package/src/simulation/common.js +34 -0
  55. package/src/simulation/create-simulate-message.js +49 -0
  56. package/src/simulation/create-simulate-transactions.js +108 -0
  57. package/src/simulation/estimate-fee.js +14 -0
  58. package/src/simulation/estimate-simple-transfer.js +15 -0
  59. package/src/simulation/get-message-type.js +18 -0
  60. package/src/simulation/simulate-message-api.js +68 -0
  61. package/src/simulation/simulate-transactions-api.js +266 -0
  62. package/src/simulation/simulate-transactions.js +16 -0
  63. package/src/simulation/transactions.js +52 -0
  64. package/src/simulation/try-estimating-changes-locally.js +26 -0
  65. package/src/staking/api/index.js +43 -0
  66. package/src/staking/cached-read.js +33 -0
  67. package/src/staking/ethereum/api.js +166 -34
  68. package/src/staking/ethereum/deps.js +37 -0
  69. package/src/staking/ethereum/everstake.js +113 -0
  70. package/src/staking/ethereum/index.js +4 -3
  71. package/src/staking/ethereum/service.js +682 -181
  72. package/src/staking/ethereum/staking-utils.js +275 -4
  73. package/src/staking/fantom-staking.js +9 -9
  74. package/src/staking/index.js +4 -4
  75. package/src/staking/matic/addresses.js +2 -0
  76. package/src/staking/matic/api.js +69 -49
  77. package/src/staking/matic/index.js +3 -3
  78. package/src/staking/matic/matic-staking-utils.js +243 -5
  79. package/src/staking/matic/matic-staking.md +53 -0
  80. package/src/staking/staking-provider-client.js +1 -1
  81. package/src/staking/utils/index.js +39 -0
  82. package/src/tx-create.js +666 -0
  83. package/src/tx-log/clarity-monitor.js +273 -103
  84. package/src/tx-log/clarity-truncated-history-monitor.js +109 -0
  85. package/src/tx-log/clarity-truncated-history-monitor.md +219 -0
  86. package/src/tx-log/clarity-utils/absolute.js +96 -0
  87. package/src/tx-log/clarity-utils/get-log-items-from-server-tx.js +94 -31
  88. package/src/tx-log/clarity-utils/get-names-of-tokens-transferred-by-server-tx.js +3 -2
  89. package/src/tx-log/clarity-utils/index.js +9 -3
  90. package/src/tx-log/clarity-utils/is-spam-pending-tx.js +13 -0
  91. package/src/tx-log/clarity-utils/normalize-transactions-response.js +65 -0
  92. package/src/tx-log/ethereum-monitor.js +93 -47
  93. package/src/tx-log/ethereum-no-history-monitor.js +52 -41
  94. package/src/tx-log/get-optimistic-txlog-effects.js +379 -0
  95. package/src/tx-log/index.js +5 -3
  96. package/src/tx-log/monitor-utils/check-pending-transactions.js +14 -5
  97. package/src/tx-log/monitor-utils/exclude-unchanged-token-balances.js +1 -1
  98. package/src/tx-log/monitor-utils/extract-balance-from-clarity-account-info.js +108 -0
  99. package/src/tx-log/monitor-utils/get-balance-updates.js +44 -0
  100. package/src/tx-log/monitor-utils/get-batched-rpc-balances.js +47 -0
  101. package/src/tx-log/monitor-utils/get-current-blacklist-status.js +38 -0
  102. package/src/tx-log/monitor-utils/get-current-eip7702-delegation.js +79 -0
  103. package/src/tx-log/monitor-utils/get-derive-transactions-to-check.js +10 -5
  104. package/src/tx-log/monitor-utils/get-fee-amount.js +1 -1
  105. package/src/tx-log/monitor-utils/get-history-from-server.js +1 -1
  106. package/src/tx-log/monitor-utils/get-log-items-from-server-tx.js +75 -33
  107. package/src/tx-log/monitor-utils/get-names-of-tokens-transferred-by-server-tx.js +3 -2
  108. package/src/tx-log/monitor-utils/get-raw-transfer-amount.js +29 -0
  109. package/src/tx-log/monitor-utils/get-transfers-by-token-name.js +3 -2
  110. package/src/tx-log/monitor-utils/get-value-of-transfers.js +10 -12
  111. package/src/tx-log/monitor-utils/index.js +12 -7
  112. package/src/tx-log/monitor-utils/is-unverifiable-pending-tx-expired.js +19 -0
  113. package/src/tx-log/monitor-utils/verify-pending-tx-status-rpc.js +90 -0
  114. package/src/tx-log-staking-processor/asset-staking-tx-data.js +82 -24
  115. package/src/tx-log-staking-processor/index.js +28 -14
  116. package/src/tx-log-staking-processor/utils.js +21 -20
  117. package/src/tx-send/broadcast-error-handler.js +63 -0
  118. package/src/tx-send/index.js +1 -2
  119. package/src/tx-send/nonce-utils.js +97 -0
  120. package/src/tx-send/tx-send.js +94 -288
  121. package/src/tx-sign/index.js +17 -0
  122. package/src/tx-type/index.js +280 -0
  123. package/src/watch-tx.js +36 -0
  124. package/src/web3/createSimulateMessage.js +3 -2
  125. package/src/web3/createSimulateTransactions.js +11 -0
  126. package/src/web3/index.js +3 -1
  127. package/src/etherscan/account.js +0 -49
  128. package/src/etherscan/index.js +0 -28
  129. package/src/etherscan/logs.js +0 -16
  130. package/src/etherscan/proxy.js +0 -47
  131. package/src/etherscan/request.js +0 -26
  132. package/src/etherscan/ws.js +0 -88
  133. package/src/exodus-eth-server/server-factory.js +0 -28
  134. package/src/exodus-eth-server/server-map.js +0 -21
  135. package/src/get-fee-async.js +0 -97
  136. package/src/staking/matic/service.js +0 -437
  137. package/src/staking-api.js +0 -5
  138. package/src/tx-log/clarity-utils/get-derive-data-needed-for-tick.js +0 -12
  139. package/src/tx-log-staking-processor/get-asset-tx-amount.js +0 -26
  140. package/src/tx-send/get-fee-info.js +0 -41
  141. package/src/websocket/index.android.js +0 -2
  142. package/src/websocket/index.ios.js +0 -2
  143. package/src/websocket/index.js +0 -21
@@ -1,20 +1,291 @@
1
- import { EthereumStaking } from './api'
1
+ import NumberUnit from '@exodus/currency'
2
+ import { parseUnsignedTx } from '@exodus/ethereum-lib'
3
+ import { bufferToHex } from '@exodus/ethereumjs/util'
4
+ import assert from 'minimalistic-assert'
5
+
6
+ import { simulateTransactions as simulateEthereumTransactions } from '../../simulation/simulate-transactions.js'
7
+ import { EthereumStaking, UNSTAKE_DEFAULTS } from './api.js'
8
+ import { simulateEverstakeUnstake } from './everstake.js'
2
9
 
3
10
  const { DELEGATE, UNSTAKE, UNSTAKE_PENDING, CLAIM_UNSTAKE } = EthereumStaking.METHODS_IDS
4
11
 
5
12
  const STAKING_MANAGER_CONTRACTS = new Set([
6
13
  EthereumStaking.addresses.ethereum.EVERSTAKE_ADDRESS_CONTRACT_POOL.toLowerCase(),
7
14
  EthereumStaking.addresses.ethereumgoerli.EVERSTAKE_ADDRESS_CONTRACT_POOL.toLowerCase(),
8
- EthereumStaking.addresses.ethereumholesky.EVERSTAKE_ADDRESS_CONTRACT_POOL.toLowerCase(),
9
15
  ])
10
16
 
17
+ const METHOD_ID_CHAR_LENGTH = 10
18
+ const FIRST_UINT256_END = METHOD_ID_CHAR_LENGTH + 64
19
+
11
20
  export const isEthereumStakingTx = ({ coinName }) =>
12
- ['ethereum', 'ethereumgoerli', 'ethereumholesky'].includes(coinName)
21
+ ['ethereum', 'ethereumgoerli'].includes(coinName)
22
+ export const isEthereumStakingPoolContract = (address) =>
23
+ typeof address === 'string' && STAKING_MANAGER_CONTRACTS.has(address.toLowerCase())
13
24
  export const isEthereumDelegate = (tx) =>
14
- isEthereumStakingTx(tx) && STAKING_MANAGER_CONTRACTS.has(tx.to) && tx.data?.methodId === DELEGATE
25
+ isEthereumStakingTx(tx) &&
26
+ ((STAKING_MANAGER_CONTRACTS.has(tx.to) && tx.data?.methodId === DELEGATE) ||
27
+ tx.data?.delegate === true ||
28
+ typeof tx.data?.delegate === 'string')
15
29
  export const isEthereumUndelegatePending = (tx) =>
16
30
  isEthereumStakingTx(tx) && tx.data?.methodId === UNSTAKE_PENDING
17
31
  export const isEthereumUndelegate = (tx) =>
18
32
  (isEthereumStakingTx(tx) && tx.data?.methodId === UNSTAKE) || isEthereumUndelegatePending(tx)
19
33
  export const isEthereumClaimUndelegate = (tx) =>
20
34
  isEthereumStakingTx(tx) && tx.data?.methodId === CLAIM_UNSTAKE
35
+
36
+ export function decodeEthereumStakingFirstUintArg(transactionData) {
37
+ const txInputHex =
38
+ (Buffer.isBuffer(transactionData) ? bufferToHex(transactionData) : transactionData) || '0x'
39
+ assert(typeof txInputHex === 'string', 'expected string transactionData')
40
+ assert(
41
+ txInputHex.length >= FIRST_UINT256_END,
42
+ 'expected staking transaction calldata with first uint256 arg'
43
+ )
44
+
45
+ return BigInt(`0x${txInputHex.slice(METHOD_ID_CHAR_LENGTH, FIRST_UINT256_END)}`)
46
+ }
47
+
48
+ export async function ethereumUnstakePendingOptimisticSideEffectTxLogs({
49
+ asset: baseAsset,
50
+ walletAccount,
51
+ feeAmount,
52
+ unstakePendingTxId,
53
+ nonce,
54
+ estimatedUnstakePendingGasLimit,
55
+ tipGasPrice,
56
+ transactionData,
57
+ txToAddress,
58
+ date,
59
+ bundleId,
60
+ }) {
61
+ let amount
62
+ try {
63
+ const decodedAmount = decodeEthereumStakingFirstUintArg(transactionData)
64
+ amount = baseAsset.currency.baseUnit(decodedAmount.toString())
65
+ } catch (e) {
66
+ console.warn(
67
+ 'Could not decode ETH unstakePending transaction data:',
68
+ unstakePendingTxId,
69
+ e.message,
70
+ transactionData
71
+ )
72
+ return []
73
+ }
74
+
75
+ return [
76
+ {
77
+ assetName: baseAsset.name,
78
+ walletAccount,
79
+ txs: [
80
+ {
81
+ confirmations: 0,
82
+ feeAmount,
83
+ feeCoinName: baseAsset.feeAsset.name,
84
+ selfSend: false,
85
+ to: txToAddress,
86
+ txId: unstakePendingTxId,
87
+ data: {
88
+ gasLimit: estimatedUnstakePendingGasLimit,
89
+ nonce,
90
+ ...(tipGasPrice && { tipGasPrice: tipGasPrice.toBaseString() }),
91
+ methodId: UNSTAKE_PENDING,
92
+ ...(bundleId ? { bundleId } : {}),
93
+ },
94
+ coinAmount: amount,
95
+ coinName: baseAsset.name,
96
+ currencies: {
97
+ [baseAsset.name]: baseAsset.currency,
98
+ [baseAsset.feeAsset.name]: baseAsset.feeAsset.currency,
99
+ },
100
+ date,
101
+ },
102
+ ],
103
+ },
104
+ ]
105
+ }
106
+
107
+ export async function ethereumUnstakeOptimisticSideEffectTxLogs({
108
+ asset: baseAsset,
109
+ walletAccount,
110
+ feeAmount,
111
+ unstakeTxId,
112
+ nonce,
113
+ estimatedUnstakeGasLimit,
114
+ tipGasPrice,
115
+ transactionData,
116
+ txToAddress,
117
+ fromAddress,
118
+ allowedInterchangeNum = UNSTAKE_DEFAULTS.allowedInterchangeNum,
119
+ date,
120
+ bundleId,
121
+ }) {
122
+ let amountEth
123
+ try {
124
+ const decodedAmountWei = decodeEthereumStakingFirstUintArg(transactionData)
125
+ amountEth = baseAsset.currency.baseUnit(decodedAmountWei.toString()).toDefaultString()
126
+ } catch (e) {
127
+ console.warn(
128
+ 'Could not decode ETH unstake transaction data:',
129
+ unstakeTxId,
130
+ e.message,
131
+ transactionData
132
+ )
133
+ return []
134
+ }
135
+
136
+ const result = await simulateEverstakeUnstake({
137
+ address: fromAddress,
138
+ amount: amountEth,
139
+ allowedInterchangeNum,
140
+ })
141
+
142
+ if (!result || result.instantReturnEth <= 0) return []
143
+
144
+ const instantAmount = baseAsset.currency.defaultUnit(String(result.instantReturnEth))
145
+
146
+ return [
147
+ {
148
+ assetName: baseAsset.name,
149
+ walletAccount,
150
+ txs: [
151
+ {
152
+ confirmations: 0,
153
+ feeAmount,
154
+ feeCoinName: baseAsset.feeAsset.name,
155
+ selfSend: false,
156
+ to: txToAddress,
157
+ txId: unstakeTxId,
158
+ data: {
159
+ gasLimit: estimatedUnstakeGasLimit,
160
+ nonce,
161
+ ...(tipGasPrice && { tipGasPrice: tipGasPrice.toBaseString() }),
162
+ methodId: UNSTAKE,
163
+ ...(bundleId ? { bundleId } : {}),
164
+ },
165
+ coinAmount: instantAmount,
166
+ coinName: baseAsset.name,
167
+ currencies: {
168
+ [baseAsset.name]: baseAsset.currency,
169
+ [baseAsset.feeAsset.name]: baseAsset.feeAsset.currency,
170
+ },
171
+ date,
172
+ },
173
+ ],
174
+ },
175
+ ]
176
+ }
177
+
178
+ export function buildSimulationRequest({ asset, unsignedTx, gasPrice }) {
179
+ const parsed = parseUnsignedTx({ asset, unsignedTx })
180
+ const chainId = unsignedTx.txData?.chainId
181
+ const from = unsignedTx.txMeta?.fromAddress
182
+ const to = unsignedTx.txMeta?.toAddress
183
+
184
+ assert(gasPrice instanceof NumberUnit, 'expected NumberUnit gasPrice')
185
+ assert(Number.isInteger(chainId), 'expected integer chainId')
186
+ assert(typeof from === 'string' && from, 'expected string fromAddress')
187
+ assert(typeof to === 'string' && to, 'expected string toAddress')
188
+
189
+ return {
190
+ chainId,
191
+ from,
192
+ to,
193
+ gas: `0x${parsed.gasLimit.toString(16)}`,
194
+ gasPrice: `0x${BigInt(gasPrice.toBaseString()).toString(16)}`,
195
+ value: `0x${BigInt(parsed.value.toBaseString()).toString(16)}`,
196
+ data: `0x${parsed.data?.toString('hex')}`,
197
+ nonce: `0x${parsed.nonce.toString(16)}`,
198
+ }
199
+ }
200
+
201
+ export async function simulateUndelegateTransactions({
202
+ asset,
203
+ txSteps,
204
+ senderAddress,
205
+ gasPrice,
206
+ blockNumber,
207
+ revertOnSimulationError,
208
+ }) {
209
+ const transactions = Object.values(txSteps)
210
+ .filter((txStep) => txStep.unsignedTx)
211
+ .map((txStep) =>
212
+ buildSimulationRequest({ asset: asset.baseAsset, unsignedTx: txStep.unsignedTx, gasPrice })
213
+ )
214
+
215
+ if (transactions.length === 0) return
216
+
217
+ const simulationResponse = await simulateEthereumTransactions({
218
+ asset: asset.baseAsset,
219
+ baseAssetName: asset.baseAsset.name,
220
+ origin: 'exodus-staking',
221
+ senderAddress,
222
+ ...(blockNumber === undefined ? undefined : { blockNumber }),
223
+ transactions,
224
+ })
225
+
226
+ if (simulationResponse?.warnings?.length || simulationResponse?.metadata?.humanReadableError) {
227
+ if (revertOnSimulationError) {
228
+ const err = new Error('StakingEthUndelegateSimulationError')
229
+ err.message = simulationResponse?.metadata?.humanReadableError
230
+ err.reason = `warnings: ${JSON.stringify(simulationResponse?.warnings)}`
231
+ err.hint = 'undelegate-eth-simulation-error'
232
+ throw err
233
+ } else {
234
+ console.warn(
235
+ 'Simulation for ETH undelegate returned issues',
236
+ simulationResponse?.metadata?.humanReadableError
237
+ )
238
+ }
239
+ }
240
+
241
+ // Validate undelegatePending simulation results.
242
+ //
243
+ // Pass: every ETH entry in willReceive has a parseable balance and their
244
+ // sum is positive (i.e. we are actually receiving ETH back).
245
+ // Fail: any ETH entry has a malformed balance (shape error), or the
246
+ // summed ETH is zero/negative (no refund, or only non-ETH entries).
247
+ //
248
+ // Our on-chain analysis showed an apparent invariant: successful
249
+ // unstakePending() calls return exactly the input amount. We intentionally
250
+ // do not enforce that here because we were not able to prove it via
251
+ // simulation alone and there may be an off-chain component involved.
252
+ //
253
+ // When there's no pending balance (only unstake with
254
+ // allowedInterchangeNum=0), nothing may come back immediately — that's
255
+ // valid and we skip the check.
256
+ if (txSteps.undelegatePending.plan?.amount?.isPositive) {
257
+ const { balanceChanges = Object.create(null) } = simulationResponse || Object.create(null)
258
+
259
+ const asArray = (v) => (Array.isArray(v) ? v : [])
260
+ let invalidEthReceiveValue = false
261
+ let sumEthReceive = BigInt(0)
262
+
263
+ for (const receive of asArray(balanceChanges.willReceive)) {
264
+ const isEth = receive?.asset?.symbol === 'ETH' || receive?.asset?.name === 'Ether'
265
+ if (!isEth) continue
266
+
267
+ try {
268
+ sumEthReceive += BigInt(receive.balance.toBaseString())
269
+ } catch {
270
+ invalidEthReceiveValue = true
271
+ }
272
+ }
273
+
274
+ if (invalidEthReceiveValue || sumEthReceive <= BigInt(0)) {
275
+ if (revertOnSimulationError) {
276
+ const err = new Error('StakingEthUndelegateSimulationResultsError')
277
+ err.message = 'Simulation did not produce expected effects'
278
+ err.reason = invalidEthReceiveValue
279
+ ? 'invalid ETH balance.value in balanceChanges: willReceive'
280
+ : 'missing positive ETH in balanceChanges: willReceive (expected pending refund)'
281
+ err.hint = 'undelegate-eth-simulation-results-error'
282
+ throw err
283
+ } else {
284
+ console.warn(
285
+ 'Simulation for ETH undelegate returned issues',
286
+ simulationResponse?.metadata?.humanReadableError
287
+ )
288
+ }
289
+ }
290
+ }
291
+ }
@@ -7,7 +7,7 @@ export class FantomStaking {
7
7
  this.sfcContract = createContract(SFC_ADDRESS, 'fantomSfc')
8
8
  }
9
9
 
10
- buildTxData = (contract, method, ...args) => {
10
+ #buildTxData = (contract, method, ...args) => {
11
11
  return contract[method].build(...args)
12
12
  }
13
13
 
@@ -16,7 +16,7 @@ export class FantomStaking {
16
16
  */
17
17
  delegate = ({ validatorId }) => {
18
18
  this.#validateValidatorId(validatorId)
19
- return this.buildTxData(this.sfcContract, 'delegate', validatorId)
19
+ return this.#buildTxData(this.sfcContract, 'delegate', validatorId)
20
20
  }
21
21
 
22
22
  /**
@@ -31,7 +31,7 @@ export class FantomStaking {
31
31
  undelegate = ({ requestId, validatorId, amount }) => {
32
32
  this.#validateRequestId(requestId)
33
33
  this.#validateValidatorId(validatorId)
34
- return this.buildTxData(this.sfcContract, 'undelegate', validatorId, requestId, amount)
34
+ return this.#buildTxData(this.sfcContract, 'undelegate', validatorId, requestId, amount)
35
35
  }
36
36
 
37
37
  /**
@@ -44,7 +44,7 @@ export class FantomStaking {
44
44
  withdraw = ({ requestId, validatorId }) => {
45
45
  this.#validateRequestId(requestId)
46
46
  this.#validateValidatorId(validatorId)
47
- return this.buildTxData(this.sfcContract, 'withdraw', validatorId, requestId)
47
+ return this.#buildTxData(this.sfcContract, 'withdraw', validatorId, requestId)
48
48
  }
49
49
 
50
50
  /**
@@ -53,7 +53,7 @@ export class FantomStaking {
53
53
  */
54
54
  claimRewards = ({ validatorId }) => {
55
55
  this.#validateValidatorId(validatorId)
56
- return this.buildTxData(this.sfcContract, 'claimRewards', validatorId)
56
+ return this.#buildTxData(this.sfcContract, 'claimRewards', validatorId)
57
57
  }
58
58
 
59
59
  /**
@@ -62,7 +62,7 @@ export class FantomStaking {
62
62
  */
63
63
  restakeRewards = ({ validatorId }) => {
64
64
  this.#validateValidatorId(validatorId)
65
- return this.buildTxData(this.sfcContract, 'restakeRewards', validatorId)
65
+ return this.#buildTxData(this.sfcContract, 'restakeRewards', validatorId)
66
66
  }
67
67
 
68
68
  /**
@@ -71,7 +71,7 @@ export class FantomStaking {
71
71
  lockStake = ({ validatorId, amount, durationSeconds }) => {
72
72
  this.#validateValidatorId(validatorId)
73
73
  this.#validateDuration(durationSeconds)
74
- return this.buildTxData(this.sfcContract, 'lockStake', validatorId, durationSeconds, amount)
74
+ return this.#buildTxData(this.sfcContract, 'lockStake', validatorId, durationSeconds, amount)
75
75
  }
76
76
 
77
77
  /**
@@ -80,7 +80,7 @@ export class FantomStaking {
80
80
  relockStake = ({ validatorId, amount, durationSeconds }) => {
81
81
  this.#validateValidatorId(validatorId)
82
82
  this.#validateDuration(durationSeconds)
83
- return this.buildTxData(this.sfcContract, 'relockStake', validatorId, durationSeconds, amount)
83
+ return this.#buildTxData(this.sfcContract, 'relockStake', validatorId, durationSeconds, amount)
84
84
  }
85
85
 
86
86
  /**
@@ -88,7 +88,7 @@ export class FantomStaking {
88
88
  */
89
89
  unlockStake = ({ validatorId, amount }) => {
90
90
  this.#validateValidatorId(validatorId)
91
- return this.buildTxData(this.sfcContract, 'unlockStake', validatorId, amount)
91
+ return this.#buildTxData(this.sfcContract, 'unlockStake', validatorId, amount)
92
92
  }
93
93
 
94
94
  #validateValidatorId = (validatorId) => {
@@ -1,4 +1,4 @@
1
- export { FantomStaking } from './fantom-staking'
2
- export { stakingProviderClientFactory } from './staking-provider-client'
3
- export * from './ethereum'
4
- export * from './matic'
1
+ export { FantomStaking } from './fantom-staking.js'
2
+ export { stakingProviderClientFactory } from './staking-provider-client.js'
3
+ export * from './ethereum/index.js'
4
+ export * from './matic/index.js'
@@ -0,0 +1,2 @@
1
+ export const EVERSTAKE_VALIDATOR_CONTRACT_ADDR = '0xf30cf4ed712d3734161fdaab5b1dbb49fd2d0e5c'
2
+ export const STAKING_MANAGER_ADDR = '0x5e3ef299fddf15eaa0432e6e66473ace8c13d908'
@@ -1,23 +1,29 @@
1
- import BN from 'bn.js'
2
1
  import { createContract } from '@exodus/ethereum-lib'
3
2
  import ethAssets from '@exodus/ethereum-meta'
3
+ import { bufferToHex } from '@exodus/ethereumjs/util'
4
+ import { safeString } from '@exodus/safe-string'
4
5
  import { retry } from '@exodus/simple-retry'
5
- import { bufferToHex } from '@exodus/ethereumjs-util'
6
+ import BN from 'bn.js'
6
7
  import assert from 'minimalistic-assert'
7
8
 
9
+ import { EVM_ERROR_REASONS, withErrorReason } from '../../error-wrapper.js'
10
+ import { fromHexToBN, fromHexToString, splitIn32BytesArray } from '../../number-utils.js'
11
+ import { EVERSTAKE_VALIDATOR_CONTRACT_ADDR, STAKING_MANAGER_ADDR } from './addresses.js'
12
+
13
+ const EVERSTAKE_API_URL = 'https://polygon-clarity.a.exodus.io/everstake-rewards'
14
+
8
15
  const polygonEthToken = ethAssets.find(({ name: tokenName }) => tokenName === 'polygon')
9
16
 
10
17
  const RETRY_DELAYS = ['10s']
11
18
  export class MaticStakingApi {
12
- static EVERSTAKE_VALIDATOR_CONTRACT_ADDR = '0xf30cf4ed712d3734161fdaab5b1dbb49fd2d0e5c'
13
- static STAKING_MANAGER_ADDR = '0x5e3ef299fddf15eaa0432e6e66473ace8c13d908'
19
+ static EVERSTAKE_VALIDATOR_CONTRACT_ADDR = EVERSTAKE_VALIDATOR_CONTRACT_ADDR
20
+ static STAKING_MANAGER_ADDR = STAKING_MANAGER_ADDR
14
21
 
15
- constructor({
22
+ constructor(
16
23
  validatorId = MaticStakingApi.EVERSTAKE_VALIDATOR_CONTRACT_ADDR,
17
24
  stakeManagerAddr = MaticStakingApi.STAKING_MANAGER_ADDR,
18
- server,
19
- }) {
20
- assert(server, 'server is required')
25
+ server
26
+ ) {
21
27
  // harcoded exchange rate from the validtor share contract
22
28
  // in order to calculate claim unstake amount off-chain
23
29
  this.EXCHANGE_RATE_PRECISION = new BN(10).pow(new BN(29))
@@ -27,12 +33,12 @@ export class MaticStakingApi {
27
33
  this.server = server
28
34
  }
29
35
 
30
- buildTxData = (contract, method, ...args) => {
36
+ #buildTxData = (contract, method, ...args) => {
31
37
  return contract[method].build(...args)
32
38
  }
33
39
 
34
- callReadFunctionContract = (contract, method, ...args) => {
35
- const callData = this.buildTxData(contract, method, ...args)
40
+ #callReadFunctionContract = (contract, method, ...args) => {
41
+ const callData = this.#buildTxData(contract, method, ...args)
36
42
  const data = {
37
43
  data: bufferToHex(callData),
38
44
  to: contract.address,
@@ -40,19 +46,25 @@ export class MaticStakingApi {
40
46
  }
41
47
 
42
48
  const eth = this.server
43
- return retry(eth.ethCall, { delayTimesMs: RETRY_DELAYS })(data)
49
+ assert(eth, 'server is required')
50
+ return withErrorReason({
51
+ promise: retry((...args) => eth.ethCall(...args), { delayTimesMs: RETRY_DELAYS })(data),
52
+ errorReasonInfo: EVM_ERROR_REASONS.ethCallFailed,
53
+ hint: safeString`stakingMaticReadFunctionContract`,
54
+ baseAssetName: eth.baseAssetName,
55
+ })
44
56
  }
45
57
 
46
58
  getWithdrawalDelay = async () => {
47
- const withdrawalDelay = await this.callReadFunctionContract(
59
+ const withdrawalDelay = await this.#callReadFunctionContract(
48
60
  this.stakingManagerContract,
49
61
  'withdrawalDelay'
50
62
  )
51
- return toBN(withdrawalDelay)
63
+ return fromHexToBN(withdrawalDelay)
52
64
  }
53
65
 
54
66
  getMinRewardsToWithdraw = async () => {
55
- const minRewardsToWithdraw = await this.callReadFunctionContract(
67
+ const minRewardsToWithdraw = await this.#callReadFunctionContract(
56
68
  this.validatorShareContract,
57
69
  'minAmount'
58
70
  )
@@ -64,8 +76,8 @@ export class MaticStakingApi {
64
76
  * stored in a list and points the events that happened in that epoch. (timeline)
65
77
  */
66
78
  getCurrentCheckpoint = async () => {
67
- const currentEpoch = await this.callReadFunctionContract(this.stakingManagerContract, 'epoch')
68
- return toBN(currentEpoch)
79
+ const currentEpoch = await this.#callReadFunctionContract(this.stakingManagerContract, 'epoch')
80
+ return fromHexToBN(currentEpoch)
69
81
  }
70
82
 
71
83
  /**
@@ -79,7 +91,7 @@ export class MaticStakingApi {
79
91
  _nonce = await this.getCurrentUnbondNonce(address)
80
92
  }
81
93
 
82
- const unboundInfo = await this.callReadFunctionContract(
94
+ const unboundInfo = await this.#callReadFunctionContract(
83
95
  this.validatorShareContract,
84
96
  'unbonds_new',
85
97
  address,
@@ -88,18 +100,18 @@ export class MaticStakingApi {
88
100
 
89
101
  const [shares, withdrawEpoch] = splitIn32BytesArray(unboundInfo)
90
102
  return {
91
- withdrawEpoch: toBN(withdrawEpoch),
92
- shares: toBN(shares),
103
+ withdrawEpoch: fromHexToBN(withdrawEpoch),
104
+ shares: fromHexToBN(shares),
93
105
  }
94
106
  }
95
107
 
96
108
  /**
97
109
  * UnbondNonce is a counter stored in the contract that tracks each time a delegator unstakes
98
110
  * @param address delegator address
99
- * @return current unbonded nonce
111
+ * @returns current unbonded nonce
100
112
  */
101
113
  getCurrentUnbondNonce = async (address) => {
102
- const unbondNonce = await this.callReadFunctionContract(
114
+ const unbondNonce = await this.#callReadFunctionContract(
103
115
  this.validatorShareContract,
104
116
  'unbondNonces',
105
117
  address
@@ -108,7 +120,8 @@ export class MaticStakingApi {
108
120
  }
109
121
 
110
122
  getLiquidRewards = async (address) => {
111
- const liquidRewards = await this.callReadFunctionContract(
123
+ // latest accrued rewards
124
+ const liquidRewards = await this.#callReadFunctionContract(
112
125
  this.validatorShareContract,
113
126
  'getLiquidRewards',
114
127
  address
@@ -116,23 +129,46 @@ export class MaticStakingApi {
116
129
  return polygonEthToken.currency.baseUnit(liquidRewards)
117
130
  }
118
131
 
132
+ getTotalRewards = async (address) => {
133
+ // use external everstake API to get total rewards
134
+ const url = new URL(EVERSTAKE_API_URL)
135
+
136
+ const options = {
137
+ method: 'POST',
138
+ headers: { 'Content-Type': 'application/json' },
139
+ body: JSON.stringify({
140
+ address,
141
+ }),
142
+ }
143
+
144
+ try {
145
+ const response = await fetch(url.toString(), options)
146
+ const res = await response.json()
147
+ if (res === 'address' || !res?.rewards) return polygonEthToken.currency.ZERO // address not found
148
+ return polygonEthToken.currency.baseUnit(res.rewards)
149
+ } catch (error) {
150
+ console.warn('Error fetching Polygon total rewards:', error)
151
+ return polygonEthToken.currency.ZERO
152
+ }
153
+ }
154
+
119
155
  getTotalStake = async (address) => {
120
- const stakeInfo = await this.callReadFunctionContract(
156
+ const stakeInfo = await this.#callReadFunctionContract(
121
157
  this.validatorShareContract,
122
158
  'getTotalStake',
123
159
  address
124
160
  )
125
161
  const [amount] = splitIn32BytesArray(stakeInfo)
126
- return polygonEthToken.currency.baseUnit(toBN(amount).toString())
162
+ return polygonEthToken.currency.baseUnit(fromHexToString(amount))
127
163
  }
128
164
 
129
165
  getWithdrawExchangeRate = async () => {
130
- const withdrawExchangeRate = await this.callReadFunctionContract(
166
+ const withdrawExchangeRate = await this.#callReadFunctionContract(
131
167
  this.validatorShareContract,
132
168
  'withdrawExchangeRate'
133
169
  )
134
170
 
135
- return toBN(withdrawExchangeRate)
171
+ return fromHexToBN(withdrawExchangeRate)
136
172
  }
137
173
 
138
174
  /**
@@ -145,7 +181,7 @@ export class MaticStakingApi {
145
181
  * @param amount Matic tokens to be approved for StakeManager to withdraw (polygon currency)
146
182
  */
147
183
  approveStakeManager = (amount) => {
148
- return this.buildTxData(
184
+ return this.#buildTxData(
149
185
  this.polygonContract,
150
186
  'increaseAllowance',
151
187
  this.stakingManagerContract.address,
@@ -154,20 +190,20 @@ export class MaticStakingApi {
154
190
  }
155
191
 
156
192
  restakeReward = () => {
157
- return this.buildTxData(this.validatorShareContract, 'restake')
193
+ return this.#buildTxData(this.validatorShareContract, 'restake')
158
194
  }
159
195
 
160
196
  withdrawRewards = () => {
161
- return this.buildTxData(this.validatorShareContract, 'withdrawRewards')
197
+ return this.#buildTxData(this.validatorShareContract, 'withdrawRewards')
162
198
  }
163
199
 
164
200
  delegate = ({ amount }) => {
165
- return this.buildTxData(this.validatorShareContract, 'buyVoucher', amount.toBaseString(), '0')
201
+ return this.#buildTxData(this.validatorShareContract, 'buyVoucher', amount.toBaseString(), '0')
166
202
  }
167
203
 
168
204
  undelegate = ({ amount, maximumSharesToBurn }) => {
169
205
  const _maximumSharesToBurn = maximumSharesToBurn || amount
170
- return this.buildTxData(
206
+ return this.#buildTxData(
171
207
  this.validatorShareContract,
172
208
  'sellVoucher_new',
173
209
  amount.toBaseString(),
@@ -179,22 +215,6 @@ export class MaticStakingApi {
179
215
  * @param {number} unbondNonce the unbond nonce from where delegator claim its staked tokens
180
216
  */
181
217
  claimUndelegatedBalance = ({ unbondNonce }) => {
182
- return this.buildTxData(this.validatorShareContract, 'unstakeClaimTokens_new', unbondNonce)
183
- }
184
- }
185
-
186
- // see arguments encoding standard (padded 32 bytes)
187
- // https://docs.soliditylang.org/en/v0.8.15/abi-spec.html?highlight=abi.encode#function-selector-and-argument-encoding
188
- const splitIn32BytesArray = (output) => removeHexPrefix(output).match(/[\da-f]{1,64}/gi) || []
189
-
190
- const toBN = (str) => new BN(removeLeadingZeroes(removeHexPrefix(str)), 16)
191
-
192
- const removeLeadingZeroes = (str) => str.replace(/^0+/, '')
193
-
194
- const removeHexPrefix = (str) => {
195
- if (typeof str !== 'string' || str === '') {
196
- return str
218
+ return this.#buildTxData(this.validatorShareContract, 'unstakeClaimTokens_new', unbondNonce)
197
219
  }
198
-
199
- return str.startsWith('0x') ? str.slice(2) : str
200
220
  }
@@ -1,9 +1,9 @@
1
- export { MaticStakingApi } from './api'
2
- export { createPolygonStakingService, getPolygonStakingInfo } from './service'
1
+ export { MaticStakingApi } from './api.js'
3
2
  export {
4
3
  isPolygonTx,
5
4
  isPolygonDelegate,
6
5
  isPolygonUndelegate,
7
6
  isPolygonReward,
8
7
  isPolygonClaimUndelegate,
9
- } from './matic-staking-utils'
8
+ isPendingPolygonUndelegateTxInEthereumTxLog,
9
+ } from './matic-staking-utils.js'