@exodus/ethereum-api 8.73.1 → 8.73.3
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 +24 -0
- package/package.json +4 -5
- package/src/error-wrapper.js +7 -4
- package/src/staking/ethereum/api.js +22 -3
- package/src/staking/ethereum/everstake.js +53 -0
- package/src/staking/ethereum/service.js +382 -141
- package/src/staking/ethereum/staking-utils.js +268 -1
- package/src/tx-log/get-optimistic-txlog-effects.js +49 -0
- package/src/tx-log-staking-processor/asset-staking-tx-data.js +40 -13
- package/src/tx-log-staking-processor/index.js +9 -13
- package/src/tx-send/nonce-utils.js +3 -1
- package/src/etherscan/account.js +0 -50
- package/src/etherscan/index.js +0 -28
- package/src/etherscan/logs.js +0 -17
- package/src/etherscan/proxy.js +0 -48
- package/src/etherscan/request.js +0 -26
- package/src/etherscan/ws.js +0 -88
|
@@ -1,4 +1,10 @@
|
|
|
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 { EthereumStaking, UNSTAKE_DEFAULTS } from './api.js'
|
|
7
|
+
import { simulateEverstakeUnstake } from './everstake.js'
|
|
2
8
|
|
|
3
9
|
const { DELEGATE, UNSTAKE, UNSTAKE_PENDING, CLAIM_UNSTAKE } = EthereumStaking.METHODS_IDS
|
|
4
10
|
|
|
@@ -8,8 +14,13 @@ const STAKING_MANAGER_CONTRACTS = new Set([
|
|
|
8
14
|
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
21
|
['ethereum', 'ethereumgoerli', 'ethereumholesky'].includes(coinName)
|
|
22
|
+
export const isEthereumStakingPoolContract = (address) =>
|
|
23
|
+
typeof address === 'string' && STAKING_MANAGER_CONTRACTS.has(address.toLowerCase())
|
|
13
24
|
export const isEthereumDelegate = (tx) =>
|
|
14
25
|
isEthereumStakingTx(tx) && STAKING_MANAGER_CONTRACTS.has(tx.to) && tx.data?.methodId === DELEGATE
|
|
15
26
|
export const isEthereumUndelegatePending = (tx) =>
|
|
@@ -18,3 +29,259 @@ export const isEthereumUndelegate = (tx) =>
|
|
|
18
29
|
(isEthereumStakingTx(tx) && tx.data?.methodId === UNSTAKE) || isEthereumUndelegatePending(tx)
|
|
19
30
|
export const isEthereumClaimUndelegate = (tx) =>
|
|
20
31
|
isEthereumStakingTx(tx) && tx.data?.methodId === CLAIM_UNSTAKE
|
|
32
|
+
|
|
33
|
+
export function decodeEthereumStakingFirstUintArg(transactionData) {
|
|
34
|
+
const txInputHex =
|
|
35
|
+
(Buffer.isBuffer(transactionData) ? bufferToHex(transactionData) : transactionData) || '0x'
|
|
36
|
+
assert(typeof txInputHex === 'string', 'expected string transactionData')
|
|
37
|
+
assert(
|
|
38
|
+
txInputHex.length >= FIRST_UINT256_END,
|
|
39
|
+
'expected staking transaction calldata with first uint256 arg'
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
return BigInt(`0x${txInputHex.slice(METHOD_ID_CHAR_LENGTH, FIRST_UINT256_END)}`)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function ethereumUnstakePendingOptimisticSideEffectTxLogs({
|
|
46
|
+
asset: baseAsset,
|
|
47
|
+
walletAccount,
|
|
48
|
+
feeAmount,
|
|
49
|
+
unstakePendingTxId,
|
|
50
|
+
nonce,
|
|
51
|
+
estimatedUnstakePendingGasLimit,
|
|
52
|
+
tipGasPrice,
|
|
53
|
+
transactionData,
|
|
54
|
+
txToAddress,
|
|
55
|
+
date,
|
|
56
|
+
bundleId,
|
|
57
|
+
}) {
|
|
58
|
+
let amount
|
|
59
|
+
try {
|
|
60
|
+
const decodedAmount = decodeEthereumStakingFirstUintArg(transactionData)
|
|
61
|
+
amount = baseAsset.currency.baseUnit(decodedAmount.toString())
|
|
62
|
+
} catch (e) {
|
|
63
|
+
console.warn(
|
|
64
|
+
'Could not decode ETH unstakePending transaction data:',
|
|
65
|
+
unstakePendingTxId,
|
|
66
|
+
e.message,
|
|
67
|
+
transactionData
|
|
68
|
+
)
|
|
69
|
+
return []
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return [
|
|
73
|
+
{
|
|
74
|
+
assetName: baseAsset.name,
|
|
75
|
+
walletAccount,
|
|
76
|
+
txs: [
|
|
77
|
+
{
|
|
78
|
+
confirmations: 0,
|
|
79
|
+
feeAmount,
|
|
80
|
+
feeCoinName: baseAsset.feeAsset.name,
|
|
81
|
+
selfSend: false,
|
|
82
|
+
to: txToAddress,
|
|
83
|
+
txId: unstakePendingTxId,
|
|
84
|
+
data: {
|
|
85
|
+
gasLimit: estimatedUnstakePendingGasLimit,
|
|
86
|
+
nonce,
|
|
87
|
+
...(tipGasPrice && { tipGasPrice: tipGasPrice.toBaseString() }),
|
|
88
|
+
methodId: UNSTAKE_PENDING,
|
|
89
|
+
...(bundleId ? { bundleId } : {}),
|
|
90
|
+
},
|
|
91
|
+
coinAmount: amount,
|
|
92
|
+
coinName: baseAsset.name,
|
|
93
|
+
currencies: {
|
|
94
|
+
[baseAsset.name]: baseAsset.currency,
|
|
95
|
+
[baseAsset.feeAsset.name]: baseAsset.feeAsset.currency,
|
|
96
|
+
},
|
|
97
|
+
date,
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
},
|
|
101
|
+
]
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export async function ethereumUnstakeOptimisticSideEffectTxLogs({
|
|
105
|
+
asset: baseAsset,
|
|
106
|
+
walletAccount,
|
|
107
|
+
feeAmount,
|
|
108
|
+
unstakeTxId,
|
|
109
|
+
nonce,
|
|
110
|
+
estimatedUnstakeGasLimit,
|
|
111
|
+
tipGasPrice,
|
|
112
|
+
transactionData,
|
|
113
|
+
txToAddress,
|
|
114
|
+
fromAddress,
|
|
115
|
+
allowedInterchangeNum = UNSTAKE_DEFAULTS.allowedInterchangeNum,
|
|
116
|
+
date,
|
|
117
|
+
bundleId,
|
|
118
|
+
}) {
|
|
119
|
+
let amountEth
|
|
120
|
+
try {
|
|
121
|
+
const decodedAmountWei = decodeEthereumStakingFirstUintArg(transactionData)
|
|
122
|
+
amountEth = baseAsset.currency.baseUnit(decodedAmountWei.toString()).toDefaultString()
|
|
123
|
+
} catch (e) {
|
|
124
|
+
console.warn(
|
|
125
|
+
'Could not decode ETH unstake transaction data:',
|
|
126
|
+
unstakeTxId,
|
|
127
|
+
e.message,
|
|
128
|
+
transactionData
|
|
129
|
+
)
|
|
130
|
+
return []
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const result = await simulateEverstakeUnstake({
|
|
134
|
+
address: fromAddress,
|
|
135
|
+
amount: amountEth,
|
|
136
|
+
allowedInterchangeNum,
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
if (!result || result.instantReturnEth <= 0) return []
|
|
140
|
+
|
|
141
|
+
const instantAmount = baseAsset.currency.defaultUnit(String(result.instantReturnEth))
|
|
142
|
+
|
|
143
|
+
return [
|
|
144
|
+
{
|
|
145
|
+
assetName: baseAsset.name,
|
|
146
|
+
walletAccount,
|
|
147
|
+
txs: [
|
|
148
|
+
{
|
|
149
|
+
confirmations: 0,
|
|
150
|
+
feeAmount,
|
|
151
|
+
feeCoinName: baseAsset.feeAsset.name,
|
|
152
|
+
selfSend: false,
|
|
153
|
+
to: txToAddress,
|
|
154
|
+
txId: unstakeTxId,
|
|
155
|
+
data: {
|
|
156
|
+
gasLimit: estimatedUnstakeGasLimit,
|
|
157
|
+
nonce,
|
|
158
|
+
...(tipGasPrice && { tipGasPrice: tipGasPrice.toBaseString() }),
|
|
159
|
+
methodId: UNSTAKE,
|
|
160
|
+
...(bundleId ? { bundleId } : {}),
|
|
161
|
+
},
|
|
162
|
+
coinAmount: instantAmount,
|
|
163
|
+
coinName: baseAsset.name,
|
|
164
|
+
currencies: {
|
|
165
|
+
[baseAsset.name]: baseAsset.currency,
|
|
166
|
+
[baseAsset.feeAsset.name]: baseAsset.feeAsset.currency,
|
|
167
|
+
},
|
|
168
|
+
date,
|
|
169
|
+
},
|
|
170
|
+
],
|
|
171
|
+
},
|
|
172
|
+
]
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function buildSimulationRequest({ asset, unsignedTx, gasPrice }) {
|
|
176
|
+
const parsed = parseUnsignedTx({ asset, unsignedTx })
|
|
177
|
+
const chainId = unsignedTx.txData?.chainId
|
|
178
|
+
const from = unsignedTx.txMeta?.fromAddress
|
|
179
|
+
const to = unsignedTx.txMeta?.toAddress
|
|
180
|
+
|
|
181
|
+
assert(gasPrice instanceof NumberUnit, 'expected NumberUnit gasPrice')
|
|
182
|
+
assert(Number.isInteger(chainId), 'expected integer chainId')
|
|
183
|
+
assert(typeof from === 'string' && from, 'expected string fromAddress')
|
|
184
|
+
assert(typeof to === 'string' && to, 'expected string toAddress')
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
chainId,
|
|
188
|
+
from,
|
|
189
|
+
to,
|
|
190
|
+
gas: `0x${parsed.gasLimit.toString(16)}`,
|
|
191
|
+
gasPrice: `0x${BigInt(gasPrice.toBaseString()).toString(16)}`,
|
|
192
|
+
value: `0x${BigInt(parsed.value.toBaseString()).toString(16)}`,
|
|
193
|
+
data: `0x${parsed.data?.toString('hex')}`,
|
|
194
|
+
nonce: `0x${parsed.nonce.toString(16)}`,
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export async function simulateUndelegateTransactions({
|
|
199
|
+
asset,
|
|
200
|
+
txSteps,
|
|
201
|
+
senderAddress,
|
|
202
|
+
gasPrice,
|
|
203
|
+
blockNumber,
|
|
204
|
+
revertOnSimulationError,
|
|
205
|
+
}) {
|
|
206
|
+
const transactions = Object.values(txSteps)
|
|
207
|
+
.filter((txStep) => txStep.unsignedTx)
|
|
208
|
+
.map((txStep) =>
|
|
209
|
+
buildSimulationRequest({ asset: asset.baseAsset, unsignedTx: txStep.unsignedTx, gasPrice })
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
if (transactions.length === 0) return
|
|
213
|
+
|
|
214
|
+
const simulationResponse = await asset.baseAsset.api.web3.simulateTransactions({
|
|
215
|
+
baseAssetName: asset.baseAsset.name,
|
|
216
|
+
origin: 'exodus-staking',
|
|
217
|
+
senderAddress,
|
|
218
|
+
...(blockNumber === undefined ? undefined : { blockNumber }),
|
|
219
|
+
transactions,
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
if (simulationResponse?.warnings?.length || simulationResponse?.metadata?.humanReadableError) {
|
|
223
|
+
if (revertOnSimulationError) {
|
|
224
|
+
const err = new Error('StakingEthUndelegateSimulationError')
|
|
225
|
+
err.message = simulationResponse?.metadata?.humanReadableError
|
|
226
|
+
err.reason = `warnings: ${JSON.stringify(simulationResponse?.warnings)}`
|
|
227
|
+
err.hint = 'undelegate-eth-simulation-error'
|
|
228
|
+
throw err
|
|
229
|
+
} else {
|
|
230
|
+
console.warn(
|
|
231
|
+
'Simulation for ETH undelegate returned issues',
|
|
232
|
+
simulationResponse?.metadata?.humanReadableError
|
|
233
|
+
)
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Validate undelegatePending simulation results.
|
|
238
|
+
//
|
|
239
|
+
// Pass: every ETH entry in willReceive has a parseable balance and their
|
|
240
|
+
// sum is positive (i.e. we are actually receiving ETH back).
|
|
241
|
+
// Fail: any ETH entry has a malformed balance (shape error), or the
|
|
242
|
+
// summed ETH is zero/negative (no refund, or only non-ETH entries).
|
|
243
|
+
//
|
|
244
|
+
// Our on-chain analysis showed an apparent invariant: successful
|
|
245
|
+
// unstakePending() calls return exactly the input amount. We intentionally
|
|
246
|
+
// do not enforce that here because we were not able to prove it via
|
|
247
|
+
// simulation alone and there may be an off-chain component involved.
|
|
248
|
+
//
|
|
249
|
+
// When there's no pending balance (only unstake with
|
|
250
|
+
// allowedInterchangeNum=0), nothing may come back immediately — that's
|
|
251
|
+
// valid and we skip the check.
|
|
252
|
+
if (txSteps.undelegatePending.plan?.amount?.isPositive) {
|
|
253
|
+
const { balanceChanges = Object.create(null) } = simulationResponse || Object.create(null)
|
|
254
|
+
|
|
255
|
+
const asArray = (v) => (Array.isArray(v) ? v : [])
|
|
256
|
+
let invalidEthReceiveValue = false
|
|
257
|
+
let sumEthReceive = BigInt(0)
|
|
258
|
+
|
|
259
|
+
for (const receive of asArray(balanceChanges.willReceive)) {
|
|
260
|
+
const isEth = receive?.asset?.symbol === 'ETH' || receive?.asset?.name === 'Ether'
|
|
261
|
+
if (!isEth) continue
|
|
262
|
+
|
|
263
|
+
try {
|
|
264
|
+
sumEthReceive += BigInt(receive.balance.toBaseString())
|
|
265
|
+
} catch {
|
|
266
|
+
invalidEthReceiveValue = true
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (invalidEthReceiveValue || sumEthReceive <= BigInt(0)) {
|
|
271
|
+
if (revertOnSimulationError) {
|
|
272
|
+
const err = new Error('StakingEthUndelegateSimulationResultsError')
|
|
273
|
+
err.message = 'Simulation did not produce expected effects'
|
|
274
|
+
err.reason = invalidEthReceiveValue
|
|
275
|
+
? 'invalid ETH balance.value in balanceChanges: willReceive'
|
|
276
|
+
: 'missing positive ETH in balanceChanges: willReceive (expected pending refund)'
|
|
277
|
+
err.hint = 'undelegate-eth-simulation-results-error'
|
|
278
|
+
throw err
|
|
279
|
+
} else {
|
|
280
|
+
console.warn(
|
|
281
|
+
'Simulation for ETH undelegate returned issues',
|
|
282
|
+
simulationResponse?.metadata?.humanReadableError
|
|
283
|
+
)
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
@@ -8,6 +8,12 @@ import {
|
|
|
8
8
|
import { bufferToHex } from '@exodus/ethereumjs/util'
|
|
9
9
|
import assert from 'minimalistic-assert'
|
|
10
10
|
|
|
11
|
+
import { EthereumStaking } from '../staking/ethereum/api.js'
|
|
12
|
+
import {
|
|
13
|
+
ethereumUnstakeOptimisticSideEffectTxLogs,
|
|
14
|
+
ethereumUnstakePendingOptimisticSideEffectTxLogs,
|
|
15
|
+
isEthereumStakingPoolContract,
|
|
16
|
+
} from '../staking/ethereum/staking-utils.js'
|
|
11
17
|
import { MaticStakingApi } from '../staking/matic/api.js'
|
|
12
18
|
import {
|
|
13
19
|
DELEGATE,
|
|
@@ -93,6 +99,7 @@ export const getOptimisticTxLogEffects = async ({
|
|
|
93
99
|
data,
|
|
94
100
|
date,
|
|
95
101
|
bundleId,
|
|
102
|
+
fromAddress,
|
|
96
103
|
})
|
|
97
104
|
|
|
98
105
|
// Fallback to basic logic that only handles transfers + fee decoding
|
|
@@ -167,6 +174,7 @@ const operationTxLogSideEffects = async ({
|
|
|
167
174
|
data,
|
|
168
175
|
date,
|
|
169
176
|
bundleId,
|
|
177
|
+
fromAddress,
|
|
170
178
|
}) => {
|
|
171
179
|
// Matic Delegate
|
|
172
180
|
if (
|
|
@@ -188,6 +196,47 @@ const operationTxLogSideEffects = async ({
|
|
|
188
196
|
})
|
|
189
197
|
}
|
|
190
198
|
|
|
199
|
+
// Ethereum Undelegate
|
|
200
|
+
if (
|
|
201
|
+
isEthereumStakingPoolContract(txToAddress) &&
|
|
202
|
+
methodId === EthereumStaking.METHODS_IDS.UNSTAKE_PENDING
|
|
203
|
+
) {
|
|
204
|
+
return ethereumUnstakePendingOptimisticSideEffectTxLogs({
|
|
205
|
+
asset,
|
|
206
|
+
walletAccount,
|
|
207
|
+
feeAmount,
|
|
208
|
+
unstakePendingTxId: txId,
|
|
209
|
+
nonce,
|
|
210
|
+
estimatedUnstakePendingGasLimit: gasLimit,
|
|
211
|
+
tipGasPrice,
|
|
212
|
+
transactionData: data,
|
|
213
|
+
txToAddress,
|
|
214
|
+
date,
|
|
215
|
+
bundleId,
|
|
216
|
+
})
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Ethereum Undelegate (unstake from active validator)
|
|
220
|
+
if (
|
|
221
|
+
isEthereumStakingPoolContract(txToAddress) &&
|
|
222
|
+
methodId === EthereumStaking.METHODS_IDS.UNSTAKE
|
|
223
|
+
) {
|
|
224
|
+
return ethereumUnstakeOptimisticSideEffectTxLogs({
|
|
225
|
+
asset,
|
|
226
|
+
walletAccount,
|
|
227
|
+
feeAmount,
|
|
228
|
+
unstakeTxId: txId,
|
|
229
|
+
nonce,
|
|
230
|
+
estimatedUnstakeGasLimit: gasLimit,
|
|
231
|
+
tipGasPrice,
|
|
232
|
+
transactionData: data,
|
|
233
|
+
txToAddress,
|
|
234
|
+
fromAddress,
|
|
235
|
+
date,
|
|
236
|
+
bundleId,
|
|
237
|
+
})
|
|
238
|
+
}
|
|
239
|
+
|
|
191
240
|
// Add other officially supported operations here
|
|
192
241
|
// Then can fallback to simulation results (TODO)
|
|
193
242
|
// Then finally to the default basic logic
|
|
@@ -28,7 +28,10 @@ const getEthereumStakingTxData = ({ tx, currency }) => {
|
|
|
28
28
|
const txAmount = tx.coinAmount.toDefaultString()
|
|
29
29
|
|
|
30
30
|
if (isEthereumDelegate(tx)) {
|
|
31
|
-
return {
|
|
31
|
+
return {
|
|
32
|
+
coinAmount: currency.ZERO,
|
|
33
|
+
data: { delegate: txAmount, txAmount },
|
|
34
|
+
}
|
|
32
35
|
}
|
|
33
36
|
|
|
34
37
|
// undelegate must be taken in consideration, if unstaked ETH is still
|
|
@@ -38,28 +41,33 @@ const getEthereumStakingTxData = ({ tx, currency }) => {
|
|
|
38
41
|
.baseUnit(decodeEthLikeStakingTxInputAmount(tx))
|
|
39
42
|
.toDefaultString()
|
|
40
43
|
return {
|
|
41
|
-
|
|
42
|
-
txAmount,
|
|
44
|
+
coinAmount: currency.ZERO,
|
|
45
|
+
data: { undelegatePending, txAmount },
|
|
43
46
|
}
|
|
44
47
|
}
|
|
45
48
|
|
|
46
49
|
if (isEthereumUndelegate(tx)) {
|
|
47
50
|
const undelegate = currency.baseUnit(decodeEthLikeStakingTxInputAmount(tx)).toDefaultString()
|
|
48
|
-
return {
|
|
51
|
+
return {
|
|
52
|
+
coinAmount: currency.ZERO,
|
|
53
|
+
data: { undelegate, txAmount },
|
|
54
|
+
}
|
|
49
55
|
}
|
|
50
56
|
|
|
51
57
|
// In the case of the ETH being actually staked and earning,
|
|
52
58
|
// unstake has a withdraw period, after that, unstaked can be claimed.
|
|
53
59
|
if (isEthereumClaimUndelegate(tx)) {
|
|
54
|
-
return {
|
|
60
|
+
return {
|
|
61
|
+
coinAmount: currency.ZERO,
|
|
62
|
+
data: { claimUndelegate: txAmount, txAmount },
|
|
63
|
+
}
|
|
55
64
|
}
|
|
56
65
|
}
|
|
57
66
|
|
|
58
67
|
const getPolygonStakingTxData = ({ tx, currency }) => {
|
|
59
|
-
if (
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
) {
|
|
68
|
+
if (tx.data?.claimUndelegate) return
|
|
69
|
+
|
|
70
|
+
if (['delegate', 'undelegate'].some((stakeTx) => tx.data?.[stakeTx]) && tx.coinAmount.isZero) {
|
|
63
71
|
return
|
|
64
72
|
}
|
|
65
73
|
|
|
@@ -69,22 +77,41 @@ const getPolygonStakingTxData = ({ tx, currency }) => {
|
|
|
69
77
|
const delegate = currency.baseUnit(decodePolygonStakingTxInputAmount(tx)).toDefaultString()
|
|
70
78
|
// MATIC returned in unstake tx is always reward
|
|
71
79
|
const rewards = calculateRewardsFromStakeTx({ tx, currency })
|
|
72
|
-
return {
|
|
80
|
+
return {
|
|
81
|
+
coinAmount: currency.ZERO,
|
|
82
|
+
data: {
|
|
83
|
+
delegate,
|
|
84
|
+
txAmount,
|
|
85
|
+
...(rewards ? { rewards } : {}),
|
|
86
|
+
},
|
|
87
|
+
}
|
|
73
88
|
}
|
|
74
89
|
|
|
75
90
|
if (isPolygonUndelegate(tx)) {
|
|
76
91
|
const undelegate = currency.baseUnit(decodePolygonStakingTxInputAmount(tx)).toDefaultString()
|
|
77
92
|
// MATIC returned in unstake tx is always reward
|
|
78
93
|
const rewards = txAmount
|
|
79
|
-
return {
|
|
94
|
+
return {
|
|
95
|
+
coinAmount: currency.ZERO,
|
|
96
|
+
data: { undelegate, txAmount, rewards },
|
|
97
|
+
}
|
|
80
98
|
}
|
|
81
99
|
|
|
82
100
|
if (isPolygonClaimUndelegate(tx)) {
|
|
83
|
-
return {
|
|
101
|
+
return {
|
|
102
|
+
// NOTE: We intentionally omit for compatibility with `rx`:
|
|
103
|
+
// https://github.com/ExodusMovement/exodus-mobile/blob/229c9c0634af3e8e17eb1624019682c6fffe29bf/src/utils/getTxTag.js#L186
|
|
104
|
+
//
|
|
105
|
+
// coinAmount: currency.ZERO,
|
|
106
|
+
data: {
|
|
107
|
+
claimUndelegate: txAmount,
|
|
108
|
+
txAmount,
|
|
109
|
+
},
|
|
110
|
+
}
|
|
84
111
|
}
|
|
85
112
|
}
|
|
86
113
|
|
|
87
|
-
export const
|
|
114
|
+
export const assetStakingTxProps = {
|
|
88
115
|
polygon: getPolygonStakingTxData,
|
|
89
116
|
ethereum: getEthereumStakingTxData,
|
|
90
117
|
ethereumholesky: getEthereumStakingTxData,
|
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
import { getPolygonUndelegateTxInEthereumTxLog } from '../staking/matic/matic-staking-utils.js'
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
const getTxStakingData = ({ assetName, currency, tx }) => {
|
|
5
|
-
return assetStakingTxData[assetName]({ tx, currency })
|
|
6
|
-
}
|
|
2
|
+
import { assetStakingTxProps } from './asset-staking-tx-data.js'
|
|
7
3
|
|
|
8
4
|
const getAssetExpandedTxLog = async ({ assetName, aci, txs, walletAccount }) => {
|
|
9
5
|
const additionalTxs = []
|
|
@@ -27,14 +23,14 @@ const processTxLog = async ({ asset, assetClientInterface: aci, walletAccount, b
|
|
|
27
23
|
|
|
28
24
|
const newTxs = []
|
|
29
25
|
for (const tx of txs) {
|
|
30
|
-
const
|
|
31
|
-
if (
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
}
|
|
26
|
+
const stakingProps = assetStakingTxProps[assetName]?.({ tx, currency })
|
|
27
|
+
if (!stakingProps) continue
|
|
28
|
+
|
|
29
|
+
newTxs.push({
|
|
30
|
+
...tx,
|
|
31
|
+
...stakingProps,
|
|
32
|
+
data: { ...tx.data, ...stakingProps.data },
|
|
33
|
+
})
|
|
38
34
|
}
|
|
39
35
|
|
|
40
36
|
const expandedTxs = await getAssetExpandedTxLog({ assetName, aci, txs, walletAccount })
|
|
@@ -55,7 +55,9 @@ const getNonceFromTxLog = ({ txLog, useAbsoluteNonce, tag }) => {
|
|
|
55
55
|
let absoluteNonce = 0
|
|
56
56
|
|
|
57
57
|
if (useAbsoluteNonce) {
|
|
58
|
-
|
|
58
|
+
// NOTE: Use a copy to avoid mutating the caller's `txLog` array, since
|
|
59
|
+
// `Array.prototype.reverse()` reverses in-place.
|
|
60
|
+
const reversedTxLog = [...txLog].reverse()
|
|
59
61
|
const maybeLatestTxWithNonceChange = getLatestTxWithNonceChange({ reversedTxLog })
|
|
60
62
|
|
|
61
63
|
if (maybeLatestTxWithNonceChange) {
|
package/src/etherscan/account.js
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import assert from 'minimalistic-assert'
|
|
2
|
-
|
|
3
|
-
import request from './request.js'
|
|
4
|
-
|
|
5
|
-
const isValidResponseCheck = (x) =>
|
|
6
|
-
(x.status === '1' && x.message === 'OK') || x.message === 'No transactions found'
|
|
7
|
-
const _request = async (...args) => request(isValidResponseCheck, 'account', ...args)
|
|
8
|
-
|
|
9
|
-
export async function fetchBalance(address) {
|
|
10
|
-
const balance = await _request('balance', { address })
|
|
11
|
-
|
|
12
|
-
const isValid = /^\d+$/.test(balance)
|
|
13
|
-
if (!isValid) throw new RangeError(`Invalid balance: ${balance}`)
|
|
14
|
-
|
|
15
|
-
return balance
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export async function fetchTxlist(address, options) {
|
|
19
|
-
const params = { startblock: 0, endblock: 'latest', ...options, address }
|
|
20
|
-
const txlist = await _request('txlist', params)
|
|
21
|
-
|
|
22
|
-
// simple check
|
|
23
|
-
assert(Array.isArray(txlist), `Invalid transactions: ${txlist}`)
|
|
24
|
-
|
|
25
|
-
return txlist
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export async function fetchTxlistinternal(address, options) {
|
|
29
|
-
const params = { startblock: 0, endblock: 'latest', ...options, address }
|
|
30
|
-
const txlist = await _request('txlistinternal', params)
|
|
31
|
-
|
|
32
|
-
// simple check
|
|
33
|
-
assert(Array.isArray(txlist), `Invalid transactions: ${txlist}`)
|
|
34
|
-
|
|
35
|
-
return txlist
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export async function tokenBalance(token, address) {
|
|
39
|
-
const params = {
|
|
40
|
-
[token.length === 42 ? 'contractaddress' : 'tokenname']: token,
|
|
41
|
-
address,
|
|
42
|
-
tag: 'latest',
|
|
43
|
-
}
|
|
44
|
-
const balance = await _request('tokenbalance', params)
|
|
45
|
-
|
|
46
|
-
const isValid = /^\d+$/.test(balance)
|
|
47
|
-
if (!isValid) throw new RangeError(`Invalid balance: ${balance}`)
|
|
48
|
-
|
|
49
|
-
return balance
|
|
50
|
-
}
|
package/src/etherscan/index.js
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import createWebSocket from './ws.js'
|
|
2
|
-
|
|
3
|
-
export const ETHERSCAN_WS_URL = 'wss://socket.etherscan.io/wshandler'
|
|
4
|
-
|
|
5
|
-
export const ws = createWebSocket(ETHERSCAN_WS_URL)
|
|
6
|
-
|
|
7
|
-
export function filterTxsSent(addr, etherscanTxs) {
|
|
8
|
-
return etherscanTxs.filter((tx) => tx.from.toLowerCase() === addr.toLowerCase())
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export function filterTxsReceived(addr, etherscanTxs) {
|
|
12
|
-
return etherscanTxs.filter((tx) => tx.to.toLowerCase() === addr.toLowerCase())
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export { fetchBalance, fetchTxlistinternal, fetchTxlist, tokenBalance } from './account.js'
|
|
16
|
-
export {
|
|
17
|
-
sendRawTransaction,
|
|
18
|
-
getTransactionCount,
|
|
19
|
-
estimateGas,
|
|
20
|
-
getTransactionReceipt,
|
|
21
|
-
getCode,
|
|
22
|
-
ethCall,
|
|
23
|
-
gasPrice,
|
|
24
|
-
} from './proxy.js'
|
|
25
|
-
|
|
26
|
-
export { setEtherscanApiKey as setApiKey } from './request.js'
|
|
27
|
-
|
|
28
|
-
export { getLogs } from './logs.js'
|
package/src/etherscan/logs.js
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import assert from 'minimalistic-assert'
|
|
2
|
-
|
|
3
|
-
import request from './request.js'
|
|
4
|
-
|
|
5
|
-
const isValidResponseCheck = (x) =>
|
|
6
|
-
(x.status === '1' && x.message === 'OK') || x.message === 'No records found'
|
|
7
|
-
const _request = async (...args) => request(isValidResponseCheck, 'logs', ...args)
|
|
8
|
-
|
|
9
|
-
export async function getLogs(address, fromBlock, toBlock, options) {
|
|
10
|
-
const params = { ...options, address, fromBlock, toBlock }
|
|
11
|
-
const events = await _request('getLogs', params)
|
|
12
|
-
|
|
13
|
-
// simple check
|
|
14
|
-
assert(Array.isArray(events), `Invalid transactions: ${events}`)
|
|
15
|
-
|
|
16
|
-
return events
|
|
17
|
-
}
|
package/src/etherscan/proxy.js
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import request from './request.js'
|
|
2
|
-
|
|
3
|
-
const isValidResponseCheck = (x) => x.result !== undefined
|
|
4
|
-
const _request = async (...args) => request(isValidResponseCheck, 'proxy', ...args)
|
|
5
|
-
|
|
6
|
-
export async function sendRawTransaction(data) {
|
|
7
|
-
const _data = data instanceof Uint8Array ? Buffer.from(data).toString('hex') : data
|
|
8
|
-
const txhash = await _request('eth_sendRawTransaction', { hex: '0x' + _data })
|
|
9
|
-
|
|
10
|
-
const isValidTxHash = /^0x[\dA-Fa-f]{64}$/.test(txhash)
|
|
11
|
-
if (!isValidTxHash) throw new Error(`Invalid tx hash: ${txhash}`)
|
|
12
|
-
|
|
13
|
-
return txhash.slice(2)
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export async function getTransactionCount(address, tag = 'latest') {
|
|
17
|
-
return _request('eth_getTransactionCount', { address, tag })
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export async function getTransactionReceipt(txhash) {
|
|
21
|
-
return _request('eth_getTransactionReceipt', { txhash })
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export async function estimateGas(data) {
|
|
25
|
-
return _request('eth_estimateGas', data)
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export async function getCode(address) {
|
|
29
|
-
const code = await _request('eth_getCode', { address })
|
|
30
|
-
|
|
31
|
-
const isValidCode = /^0x[\dA-Fa-f]*$/.test(code) && code.length % 2 === 0
|
|
32
|
-
if (!isValidCode) throw new Error(`Invalid address code: ${code}`)
|
|
33
|
-
|
|
34
|
-
return code
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export async function gasPrice() {
|
|
38
|
-
const price = await _request('eth_gasPrice')
|
|
39
|
-
|
|
40
|
-
const isValidPrice = /^0x[\dA-Fa-f]+$/.test(price)
|
|
41
|
-
if (!isValidPrice) throw new Error(`Invalid price: ${price}`)
|
|
42
|
-
|
|
43
|
-
return price
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export async function ethCall(data) {
|
|
47
|
-
return _request('eth_call', data)
|
|
48
|
-
}
|
package/src/etherscan/request.js
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import fetchival from '@exodus/fetch/experimental/fetchival'
|
|
2
|
-
import makeConcurrent from 'make-concurrent'
|
|
3
|
-
import ms from 'ms'
|
|
4
|
-
|
|
5
|
-
const ETHERSCAN_API_URL = 'https://api.etherscan.io/api'
|
|
6
|
-
const DEFAULT_ETHERSCAN_API_KEY = 'XM3VGRSNW1TMSIR14I9MVFP15X74GNHTRI'
|
|
7
|
-
|
|
8
|
-
let etherscanApiKey = DEFAULT_ETHERSCAN_API_KEY
|
|
9
|
-
|
|
10
|
-
export function setEtherscanApiKey(apiKey) {
|
|
11
|
-
etherscanApiKey = apiKey || DEFAULT_ETHERSCAN_API_KEY
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export default makeConcurrent(
|
|
15
|
-
async function (isValidResponseCheck, module, action, params = {}) {
|
|
16
|
-
const data = await fetchival(new URL(ETHERSCAN_API_URL), { timeout: ms('15s') }).get({
|
|
17
|
-
...params,
|
|
18
|
-
module,
|
|
19
|
-
action,
|
|
20
|
-
apiKey: etherscanApiKey,
|
|
21
|
-
})
|
|
22
|
-
if (!isValidResponseCheck(data)) throw new Error(`Invalid response: ${JSON.stringify(data)}`)
|
|
23
|
-
return data.result
|
|
24
|
-
},
|
|
25
|
-
{ concurrency: 3 }
|
|
26
|
-
)
|