@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.
- package/CHANGELOG.md +2121 -0
- package/LICENSE +7 -0
- package/README.md +124 -0
- package/package.json +34 -33
- package/src/address-has-history.js +6 -2
- package/src/allowance/constants.js +4 -5
- package/src/allowance/index.js +159 -40
- package/src/check-tx/create-assess-transaction.js +23 -0
- package/src/check-tx/create-check-tx.js +104 -0
- package/src/check-tx/index.js +1 -0
- package/src/create-asset-plugin-factory.js +115 -0
- package/src/create-asset-utils.js +520 -0
- package/src/create-asset.js +272 -97
- package/src/create-token-factory.js +47 -17
- package/src/custom-fees.js +57 -0
- package/src/eip7702-whitelist.js +62 -0
- package/src/ens/index.js +12 -17
- package/src/error-wrapper.js +383 -0
- package/src/eth-like-util.js +172 -33
- package/src/exodus-eth-server/api-coin-nodes.js +27 -196
- package/src/exodus-eth-server/api.js +23 -14
- package/src/exodus-eth-server/clarity-v2.js +255 -0
- package/src/exodus-eth-server/clarity.js +67 -265
- package/src/exodus-eth-server/errors.js +40 -0
- package/src/exodus-eth-server/eth-like-server.js +385 -0
- package/src/exodus-eth-server/fetch-json.js +48 -0
- package/src/exodus-eth-server/index.js +77 -0
- package/src/exodus-eth-server/utils.js +31 -0
- package/src/exodus-eth-server/ws-gateway.js +306 -0
- package/src/exodus-eth-server/ws.js +1 -2
- package/src/fee-data/EthLikeFeeData.js +81 -0
- package/src/fee-data/index.js +54 -0
- package/src/fee-utils.js +229 -0
- package/src/gas-estimation.js +183 -66
- package/src/get-balance-for-address.js +1 -1
- package/src/get-balances.js +285 -27
- package/src/get-fee.js +92 -56
- package/src/get-historical-balance.js +58 -0
- package/src/hooks/index.js +1 -1
- package/src/hooks/monitor.js +182 -43
- package/src/hooks/staking-freshness.js +58 -0
- package/src/index.js +118 -12
- package/src/move-funds.js +153 -0
- package/src/multicall3/index.js +212 -0
- package/src/multisig-address.js +146 -0
- package/src/nft-utils.js +60 -0
- package/src/number-utils.js +34 -2
- package/src/optimism-gas/index.js +13 -5
- package/src/send-validations.js +58 -0
- package/src/server-based-fee-monitor.js +12 -19
- package/src/simulate-tx/fetch-tx-preview.js +0 -2
- package/src/simulate-tx/index.js +2 -2
- package/src/simulate-tx/simulate-eth-tx.js +24 -11
- package/src/simulation/common.js +34 -0
- package/src/simulation/create-simulate-message.js +49 -0
- package/src/simulation/create-simulate-transactions.js +108 -0
- package/src/simulation/estimate-fee.js +14 -0
- package/src/simulation/estimate-simple-transfer.js +15 -0
- package/src/simulation/get-message-type.js +18 -0
- package/src/simulation/simulate-message-api.js +68 -0
- package/src/simulation/simulate-transactions-api.js +266 -0
- package/src/simulation/simulate-transactions.js +16 -0
- package/src/simulation/transactions.js +52 -0
- package/src/simulation/try-estimating-changes-locally.js +26 -0
- package/src/staking/api/index.js +43 -0
- package/src/staking/cached-read.js +33 -0
- package/src/staking/ethereum/api.js +166 -34
- package/src/staking/ethereum/deps.js +37 -0
- package/src/staking/ethereum/everstake.js +113 -0
- package/src/staking/ethereum/index.js +4 -3
- package/src/staking/ethereum/service.js +682 -181
- package/src/staking/ethereum/staking-utils.js +275 -4
- package/src/staking/fantom-staking.js +9 -9
- package/src/staking/index.js +4 -4
- package/src/staking/matic/addresses.js +2 -0
- package/src/staking/matic/api.js +69 -49
- package/src/staking/matic/index.js +3 -3
- package/src/staking/matic/matic-staking-utils.js +243 -5
- package/src/staking/matic/matic-staking.md +53 -0
- package/src/staking/staking-provider-client.js +1 -1
- package/src/staking/utils/index.js +39 -0
- package/src/tx-create.js +666 -0
- package/src/tx-log/clarity-monitor.js +273 -103
- package/src/tx-log/clarity-truncated-history-monitor.js +109 -0
- package/src/tx-log/clarity-truncated-history-monitor.md +219 -0
- package/src/tx-log/clarity-utils/absolute.js +96 -0
- package/src/tx-log/clarity-utils/get-log-items-from-server-tx.js +94 -31
- package/src/tx-log/clarity-utils/get-names-of-tokens-transferred-by-server-tx.js +3 -2
- package/src/tx-log/clarity-utils/index.js +9 -3
- package/src/tx-log/clarity-utils/is-spam-pending-tx.js +13 -0
- package/src/tx-log/clarity-utils/normalize-transactions-response.js +65 -0
- package/src/tx-log/ethereum-monitor.js +93 -47
- package/src/tx-log/ethereum-no-history-monitor.js +52 -41
- package/src/tx-log/get-optimistic-txlog-effects.js +379 -0
- package/src/tx-log/index.js +5 -3
- package/src/tx-log/monitor-utils/check-pending-transactions.js +14 -5
- package/src/tx-log/monitor-utils/exclude-unchanged-token-balances.js +1 -1
- package/src/tx-log/monitor-utils/extract-balance-from-clarity-account-info.js +108 -0
- package/src/tx-log/monitor-utils/get-balance-updates.js +44 -0
- package/src/tx-log/monitor-utils/get-batched-rpc-balances.js +47 -0
- package/src/tx-log/monitor-utils/get-current-blacklist-status.js +38 -0
- package/src/tx-log/monitor-utils/get-current-eip7702-delegation.js +79 -0
- package/src/tx-log/monitor-utils/get-derive-transactions-to-check.js +10 -5
- package/src/tx-log/monitor-utils/get-fee-amount.js +1 -1
- package/src/tx-log/monitor-utils/get-history-from-server.js +1 -1
- package/src/tx-log/monitor-utils/get-log-items-from-server-tx.js +75 -33
- package/src/tx-log/monitor-utils/get-names-of-tokens-transferred-by-server-tx.js +3 -2
- package/src/tx-log/monitor-utils/get-raw-transfer-amount.js +29 -0
- package/src/tx-log/monitor-utils/get-transfers-by-token-name.js +3 -2
- package/src/tx-log/monitor-utils/get-value-of-transfers.js +10 -12
- package/src/tx-log/monitor-utils/index.js +12 -7
- package/src/tx-log/monitor-utils/is-unverifiable-pending-tx-expired.js +19 -0
- package/src/tx-log/monitor-utils/verify-pending-tx-status-rpc.js +90 -0
- package/src/tx-log-staking-processor/asset-staking-tx-data.js +82 -24
- package/src/tx-log-staking-processor/index.js +28 -14
- package/src/tx-log-staking-processor/utils.js +21 -20
- package/src/tx-send/broadcast-error-handler.js +63 -0
- package/src/tx-send/index.js +1 -2
- package/src/tx-send/nonce-utils.js +97 -0
- package/src/tx-send/tx-send.js +94 -288
- package/src/tx-sign/index.js +17 -0
- package/src/tx-type/index.js +280 -0
- package/src/watch-tx.js +36 -0
- package/src/web3/createSimulateMessage.js +3 -2
- package/src/web3/createSimulateTransactions.js +11 -0
- package/src/web3/index.js +3 -1
- package/src/etherscan/account.js +0 -49
- package/src/etherscan/index.js +0 -28
- package/src/etherscan/logs.js +0 -16
- package/src/etherscan/proxy.js +0 -47
- package/src/etherscan/request.js +0 -26
- package/src/etherscan/ws.js +0 -88
- package/src/exodus-eth-server/server-factory.js +0 -28
- package/src/exodus-eth-server/server-map.js +0 -21
- package/src/get-fee-async.js +0 -97
- package/src/staking/matic/service.js +0 -437
- package/src/staking-api.js +0 -5
- package/src/tx-log/clarity-utils/get-derive-data-needed-for-tick.js +0 -12
- package/src/tx-log-staking-processor/get-asset-tx-amount.js +0 -26
- package/src/tx-send/get-fee-info.js +0 -41
- package/src/websocket/index.android.js +0 -2
- package/src/websocket/index.ios.js +0 -2
- package/src/websocket/index.js +0 -21
|
@@ -1,20 +1,291 @@
|
|
|
1
|
-
import
|
|
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'
|
|
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) &&
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
91
|
+
return this.#buildTxData(this.sfcContract, 'unlockStake', validatorId, amount)
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
#validateValidatorId = (validatorId) => {
|
package/src/staking/index.js
CHANGED
|
@@ -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'
|
package/src/staking/matic/api.js
CHANGED
|
@@ -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
|
|
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 =
|
|
13
|
-
static STAKING_MANAGER_ADDR =
|
|
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
|
|
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
|
-
|
|
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
|
|
59
|
+
const withdrawalDelay = await this.#callReadFunctionContract(
|
|
48
60
|
this.stakingManagerContract,
|
|
49
61
|
'withdrawalDelay'
|
|
50
62
|
)
|
|
51
|
-
return
|
|
63
|
+
return fromHexToBN(withdrawalDelay)
|
|
52
64
|
}
|
|
53
65
|
|
|
54
66
|
getMinRewardsToWithdraw = async () => {
|
|
55
|
-
const minRewardsToWithdraw = await this
|
|
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
|
|
68
|
-
return
|
|
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
|
|
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:
|
|
92
|
-
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
|
-
* @
|
|
111
|
+
* @returns current unbonded nonce
|
|
100
112
|
*/
|
|
101
113
|
getCurrentUnbondNonce = async (address) => {
|
|
102
|
-
const unbondNonce = await this
|
|
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
|
-
|
|
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
|
|
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(
|
|
162
|
+
return polygonEthToken.currency.baseUnit(fromHexToString(amount))
|
|
127
163
|
}
|
|
128
164
|
|
|
129
165
|
getWithdrawExchangeRate = async () => {
|
|
130
|
-
const withdrawExchangeRate = await this
|
|
166
|
+
const withdrawExchangeRate = await this.#callReadFunctionContract(
|
|
131
167
|
this.validatorShareContract,
|
|
132
168
|
'withdrawExchangeRate'
|
|
133
169
|
)
|
|
134
170
|
|
|
135
|
-
return
|
|
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
|
|
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
|
|
193
|
+
return this.#buildTxData(this.validatorShareContract, 'restake')
|
|
158
194
|
}
|
|
159
195
|
|
|
160
196
|
withdrawRewards = () => {
|
|
161
|
-
return this
|
|
197
|
+
return this.#buildTxData(this.validatorShareContract, 'withdrawRewards')
|
|
162
198
|
}
|
|
163
199
|
|
|
164
200
|
delegate = ({ amount }) => {
|
|
165
|
-
return this
|
|
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
|
|
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
|
|
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
|
-
|
|
8
|
+
isPendingPolygonUndelegateTxInEthereumTxLog,
|
|
9
|
+
} from './matic-staking-utils.js'
|