@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.
@@ -1,4 +1,10 @@
1
- import { EthereumStaking } from './api.js'
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 { delegate: txAmount, txAmount }
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
- undelegatePending,
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 { undelegate, txAmount }
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 { claimUndelegate: txAmount, txAmount }
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
- ['delegate', 'undelegate', 'claimUndelegate'].some((stakeTx) => tx.data?.[stakeTx]) &&
61
- tx.coinAmount.isZero
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 { delegate, txAmount, ...(rewards ? { rewards } : {}) }
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 { undelegate, txAmount, rewards }
94
+ return {
95
+ coinAmount: currency.ZERO,
96
+ data: { undelegate, txAmount, rewards },
97
+ }
80
98
  }
81
99
 
82
100
  if (isPolygonClaimUndelegate(tx)) {
83
- return { claimUndelegate: txAmount, txAmount }
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 assetStakingTxData = {
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 { assetStakingTxData } from './asset-staking-tx-data.js'
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 stakingData = getTxStakingData({ assetName, currency, tx })
31
- if (stakingData) {
32
- newTxs.push({
33
- ...tx,
34
- coinAmount: currency.ZERO,
35
- data: { ...tx.data, ...stakingData },
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
- const reversedTxLog = txLog.reverse()
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) {
@@ -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
- }
@@ -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'
@@ -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
- }
@@ -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
- }
@@ -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
- )