@exodus/ethereum-api 9.0.3 → 10.1.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.
@@ -0,0 +1,37 @@
1
+ export const createRejectionError = ({ operation, check }) => {
2
+ const error = new Error(`Cannot ${operation}: ${check.reasonCode}`)
3
+ error.name = 'StakingRejectionError'
4
+ error.reasonCode = check.reasonCode
5
+ if (check.shortfall) {
6
+ error.shortfall = check.shortfall
7
+ error.shortfallAssetName = check.shortfallAssetName
8
+ }
9
+
10
+ return error
11
+ }
12
+
13
+ export const createSubmitError = ({ error, txIds, failedIndex }) => {
14
+ const submitError = new Error(error.message, { cause: error })
15
+ submitError.name = 'StakingSubmitError'
16
+ submitError.txIds = txIds
17
+ submitError.failedIndex = failedIndex
18
+ return submitError
19
+ }
20
+
21
+ export const fail = (reasonCode, shortfall, shortfallAssetName) => ({
22
+ ok: false,
23
+ reasonCode,
24
+ ...(shortfall && { shortfall, shortfallAssetName }),
25
+ })
26
+
27
+ export const pass = (warnings = []) => (warnings.length > 0 ? { ok: true, warnings } : { ok: true })
28
+
29
+ export const sumOperationFees = ({ estimateOperationFee, operations }) => {
30
+ if (!estimateOperationFee) return
31
+ const fees = operations.map(estimateOperationFee)
32
+ if (fees.some((fee) => !fee)) return
33
+ return fees.reduce((total, fee) => total.add(fee))
34
+ }
35
+
36
+ export const warnIfBelow = ({ balance, required, warning }) =>
37
+ pass(balance && required && balance.lt(required) ? [warning] : [])
@@ -0,0 +1,107 @@
1
+ import assert from 'minimalistic-assert'
2
+
3
+ export const STAKING_TX_MODE = Object.freeze({ single: 'single', bundle: 'bundle' })
4
+
5
+ const createSingleTx = async ({ asset, walletAccount, step, gasPrice, tipGasPrice, feeData }) => {
6
+ const createdTx = await asset.baseAsset.api.createTx({
7
+ asset,
8
+ walletAccount,
9
+ address: step.to,
10
+ amount: step.amount || asset.currency.ZERO,
11
+ txInput: step.txInput,
12
+ gasPrice,
13
+ gasLimit: step.gasLimit,
14
+ tipGasPrice,
15
+ feeData,
16
+ })
17
+ return [createdTx]
18
+ }
19
+
20
+ const createBundleTxs = async ({
21
+ asset,
22
+ walletAccount,
23
+ fromAddress,
24
+ steps,
25
+ gasPrice,
26
+ tipGasPrice,
27
+ }) => {
28
+ const baseNonce = await asset.baseAsset.getNonce({ asset, fromAddress, walletAccount })
29
+
30
+ return Promise.all(
31
+ steps.map((step, index) =>
32
+ asset.baseAsset.api.createTx({
33
+ asset,
34
+ walletAccount,
35
+ fromAddress,
36
+ amount: step.amount || asset.currency.ZERO,
37
+ tipGasPrice,
38
+ gasPrice,
39
+ toAddress: step.to,
40
+ txInput: step.txInput,
41
+ gasLimit: step.gasLimit,
42
+ nonce: baseNonce + index,
43
+ })
44
+ )
45
+ )
46
+ }
47
+
48
+ export async function createStakingTxs({
49
+ asset,
50
+ walletAccount,
51
+ fromAddress,
52
+ steps,
53
+ mode,
54
+ gasPrice,
55
+ tipGasPrice,
56
+ feeData,
57
+ }) {
58
+ assert(steps.length > 0, 'staking txs require at least one step')
59
+ assert(
60
+ steps.every(({ amount }) => !amount || amount.unitType.equals(asset.currency)),
61
+ `staking tx amounts must be in ${asset.name}, the asset sending the txs`
62
+ )
63
+
64
+ switch (mode) {
65
+ case STAKING_TX_MODE.single:
66
+ assert(steps.length === 1, 'single staking tx mode requires exactly one step')
67
+ return createSingleTx({
68
+ asset,
69
+ walletAccount,
70
+ step: steps[0],
71
+ gasPrice,
72
+ tipGasPrice,
73
+ feeData,
74
+ })
75
+ case STAKING_TX_MODE.bundle:
76
+ return createBundleTxs({ asset, walletAccount, fromAddress, steps, gasPrice, tipGasPrice })
77
+ default:
78
+ throw new Error(`Unknown staking tx mode: ${mode}`)
79
+ }
80
+ }
81
+
82
+ export async function sendStakingTxs({ asset, walletAccount, unsignedTxs, mode, onProgress }) {
83
+ switch (mode) {
84
+ case STAKING_TX_MODE.single: {
85
+ assert(unsignedTxs.length === 1, 'single staking tx mode requires exactly one tx')
86
+ onProgress?.({ index: 0, total: 1 })
87
+ const { txId } = await asset.baseAsset.api.sendTx({
88
+ asset,
89
+ walletAccount,
90
+ unsignedTx: unsignedTxs[0],
91
+ })
92
+ return { txIds: [txId] }
93
+ }
94
+
95
+ case STAKING_TX_MODE.bundle: {
96
+ const { bundleHash, txIds } = await asset.baseAsset.sendPrivateBundle({
97
+ unsignedTxBundle: unsignedTxs.map((unsignedTx) => ({ asset, unsignedTx, walletAccount })),
98
+ onBeforeSign: onProgress,
99
+ })
100
+ assert(txIds.length === unsignedTxs.length, 'expected a tx id for every bundled tx')
101
+ return { bundleHash, txIds: [...txIds] }
102
+ }
103
+
104
+ default:
105
+ throw new Error(`Unknown staking tx mode: ${mode}`)
106
+ }
107
+ }
@@ -6,4 +6,5 @@ export {
6
6
  isPolygonReward,
7
7
  isPolygonClaimUndelegate,
8
8
  isPendingPolygonUndelegateTxInEthereumTxLog,
9
+ isPolygonUndelegateTxInEthereumTxLog,
9
10
  } from './matic-staking-utils.js'
@@ -1,4 +1,3 @@
1
- export { EthereumMonitor } from './ethereum-monitor.js'
2
1
  export { EthereumNoHistoryMonitor } from './ethereum-no-history-monitor.js'
3
2
  export { ClarityMonitor } from './clarity-monitor.js'
4
3
  export { ClarityTruncatedHistoryMonitor } from './clarity-truncated-history-monitor.js'
@@ -1,8 +1,6 @@
1
1
  export { default as getDeriveDataNeededForTick } from './get-derive-data-needed-for-tick.js'
2
2
 
3
3
  export { default as getAllLogItemsByAsset } from './get-all-log-items-by-asset.js'
4
- export { default as getLogItemsFromServerTx } from './get-log-items-from-server-tx.js'
5
- export { default as getHistoryFromServer } from './get-history-from-server.js'
6
4
  export { default as checkPendingTransactions } from './check-pending-transactions.js'
7
5
  export { default as getDeriveTransactionsToCheck } from './get-derive-transactions-to-check.js'
8
6
  export { default as getCurrentEIP7702Delegation } from './get-current-eip7702-delegation.js'
@@ -1,276 +0,0 @@
1
- import { randomUUID } from '@exodus/crypto/randomUUID'
2
- import { bufferToHex } from '@exodus/ethereumjs/util'
3
- import fetchival from '@exodus/fetch/experimental/fetchival'
4
- import { retry } from '@exodus/simple-retry'
5
- import SolidityContract from '@exodus/solidity-contract'
6
- import ms from 'ms'
7
-
8
- import { fromHexToString } from '../number-utils.js'
9
- import { getFallbackGasPriceEstimation } from './utils.js'
10
- import createWebSocket from './ws.js'
11
-
12
- const RETRY_DELAYS = ['10s']
13
-
14
- export function create(defaultURL, ensAssetName) {
15
- let API_URL = defaultURL
16
- const ws = createWebSocket(() => {
17
- const obj = new URL(API_URL)
18
- obj.protocol = 'wss:'
19
- return `${obj}`
20
- })
21
-
22
- function baseUrl(version = 'v1') {
23
- return new URL(version === 'v1' ? API_URL : API_URL.replace('v1', version))
24
- }
25
-
26
- async function request(module, params = {}, { version = 'v1', method = 'get' } = {}) {
27
- try {
28
- return await fetchival(baseUrl(version), { timeout: ms('15s') })(module).method(
29
- method,
30
- params
31
- )
32
- } catch (err) {
33
- let nerr = err
34
- if (err.response && (err.response.status === 500 || err.response.status === 400)) {
35
- try {
36
- const data = await err.response.json()
37
- const msg = data.error.replace(/^RPC error \(code: -\d+\): /, '')
38
- nerr = new Error(msg)
39
- } catch {}
40
-
41
- nerr.finalError = true
42
- }
43
-
44
- throw nerr
45
- }
46
- }
47
-
48
- // Default retry function
49
- const requestWithRetry = retry(request, { delayTimesMs: RETRY_DELAYS })
50
-
51
- return {
52
- getURL() {
53
- return API_URL
54
- },
55
-
56
- setURL(newURL) {
57
- if (newURL === API_URL) return // prevents useless WS reconnections
58
-
59
- API_URL = newURL || defaultURL
60
-
61
- if (ws.isCreated()) {
62
- ws.close()
63
- ws.open()
64
- }
65
- },
66
-
67
- stop() {
68
- ws.close()
69
- },
70
-
71
- ws,
72
-
73
- async getBalance(address, opts) {
74
- opts = { startblock: 'earliest', endblock: 'pending', ...opts }
75
- return requestWithRetry('balance', { address, from: opts.startblock, to: opts.endblock })
76
- },
77
-
78
- async getBalanceProxied(address, tag = 'latest') {
79
- return requestWithRetry('proxy', { method: 'eth_getBalance', address, tag })
80
- },
81
-
82
- async balanceOf(address, tokenAddress, tag = 'latest') {
83
- const contract = SolidityContract.simpleErc20(tokenAddress)
84
- const callData = contract.balanceOf.build(address)
85
- const data = {
86
- data: bufferToHex(callData),
87
- to: tokenAddress,
88
- tag,
89
- }
90
- const result = await retry(this.ethCall, { delayTimesMs: RETRY_DELAYS })(data)
91
- const balance = fromHexToString(result)
92
- return {
93
- confirmed: {
94
- [tokenAddress]: balance,
95
- },
96
- }
97
- },
98
-
99
- async getHistory(address, opts) {
100
- opts = { startblock: 'earliest', endblock: 'pending', limit: 1000, ...opts }
101
- return requestWithRetry('history', {
102
- address,
103
- from: opts.startblock,
104
- to: opts.endblock,
105
- limit: opts.limit,
106
- })
107
- },
108
-
109
- async getHistoryV2(address, opts = {}) {
110
- return requestWithRetry(
111
- 'history',
112
- {
113
- address,
114
- ...opts,
115
- },
116
- { version: 'v2' }
117
- )
118
- },
119
-
120
- async gasPrice() {
121
- return requestWithRetry('proxy', { method: 'eth_gasPrice' })
122
- },
123
-
124
- async getGasPriceEstimation() {
125
- return getFallbackGasPriceEstimation({ server: this })
126
- },
127
-
128
- // for fee monitor
129
- async getGasPrice() {
130
- return requestWithRetry('proxy', { method: 'eth_gasPrice' })
131
- },
132
-
133
- async getLatestBlock() {
134
- return this.getBlockByNumber('latest')
135
- },
136
-
137
- async getBaseFeePerGas() {
138
- const response = await this.getLatestBlock()
139
- if (response.baseFeePerGas) {
140
- return fromHexToString(response.baseFeePerGas)
141
- }
142
- },
143
-
144
- async getTransactionCount(address, tag = 'latest') {
145
- return requestWithRetry('proxy', { method: 'eth_getTransactionCount', address, tag })
146
- },
147
-
148
- async getTransactionByHash(hash) {
149
- return requestWithRetry('proxy', { method: 'eth_getTransactionByHash', hash })
150
- },
151
-
152
- async getTransactionReceipt(txhash) {
153
- return requestWithRetry('proxy', { method: 'eth_getTransactionReceipt', txhash })
154
- },
155
-
156
- async getCode(address, tag = 'latest') {
157
- return requestWithRetry('proxy', { method: 'eth_getCode', address, tag })
158
- },
159
-
160
- async getStorageAt(address, slot, tag = 'latest') {
161
- return requestWithRetry('proxy', { method: 'eth_getStorageAt', address, slot, tag })
162
- },
163
-
164
- async isContract(address) {
165
- const code = await requestWithRetry('proxy', { method: 'eth_getCode', address })
166
- return code.length > 2
167
- },
168
-
169
- async sendRawTransaction(data) {
170
- const _data = data instanceof Uint8Array ? Buffer.from(data).toString('hex') : data
171
- const hex = _data.startsWith('0x') ? _data : '0x' + _data
172
- return requestWithRetry(
173
- 'proxy',
174
- { method: 'eth_sendRawTransaction', params: [hex] },
175
- { version: 'v2', method: 'post' }
176
- )
177
- },
178
-
179
- async estimateGas(data, tag = 'latest') {
180
- return requestWithRetry('proxy', { method: 'eth_estimateGas', ...data, tag })
181
- },
182
-
183
- /**
184
- * Hits a magnifier endpoint returning maximum gas limit that a specified
185
- * contract along with method consumed in the past.
186
- * @param opts : Object with parameters
187
- * @returns {Promise<*>}
188
- */
189
- async getGasEstimation({ address, data }) {
190
- return requestWithRetry('gas-estimation', {
191
- address,
192
- data,
193
- })
194
- },
195
-
196
- async ethCall(data, tag = 'latest') {
197
- return request('proxy', { method: 'eth_call', ...data, tag })
198
- },
199
-
200
- async blockNumber() {
201
- return requestWithRetry('proxy', { method: 'eth_blockNumber' })
202
- },
203
-
204
- async getBlockByNumber(number, isFullTransactions = false) {
205
- return requestWithRetry('proxy', {
206
- method: 'eth_getBlockByNumber',
207
- number,
208
- isFullTransactions,
209
- })
210
- },
211
-
212
- async getLogs(params) {
213
- return requestWithRetry('proxy', { method: 'eth_getLogs', ...params })
214
- },
215
-
216
- async simulateRawTransaction(rawTransaction, applyPending = true) {
217
- rawTransaction = rawTransaction.replace('0x', '')
218
- return request('proxy', {
219
- method: 'debug_simulateRawTransaction',
220
- rawTransaction,
221
- applyPending,
222
- })
223
- },
224
-
225
- async getCoinbase() {
226
- return request('proxy', {
227
- method: 'eth_coinbase',
228
- })
229
- },
230
-
231
- async getBlockByHash(blockHash, fullTx = false) {
232
- return request('proxy', {
233
- method: 'eth_getBlockByHash',
234
- blockHash,
235
- fullTx,
236
- })
237
- },
238
-
239
- async getBlockTransactionCountByHash(blockHash) {
240
- return request('proxy', {
241
- method: 'eth_getBlockTransactionCountByHash',
242
- blockHash,
243
- })
244
- },
245
-
246
- async getBlockTransactionCountByNumber(quantityOrTag) {
247
- return request('proxy', {
248
- method: 'eth_getBlockTransactionCountByNumber',
249
- quantityOrTag,
250
- })
251
- },
252
-
253
- async getCompilers() {
254
- return request('proxy', {
255
- method: 'eth_getCompilers',
256
- })
257
- },
258
-
259
- async getNetVersion() {
260
- return request('proxy', {
261
- method: 'net_version',
262
- })
263
- },
264
-
265
- async proxyToCoinNode(requestData) {
266
- // eslint-disable-next-line @exodus/hydra/no-param-reassign-prop-only
267
- if (!requestData.jsonrpc) requestData.jsonrpc = '2.0'
268
- // eslint-disable-next-line @exodus/hydra/no-param-reassign-prop-only
269
- if (!requestData.id) requestData.id = randomUUID()
270
- // eslint-disable-next-line @exodus/hydra/no-param-reassign-prop-only
271
- if (!requestData.params) requestData.params = []
272
-
273
- return request('proxy', requestData, { version: 'v2', method: 'post' })
274
- },
275
- }
276
- }
@@ -1,126 +0,0 @@
1
- import EventEmitter from 'events/events.js' // forces it to use the module from node_modules
2
- import ms from 'ms'
3
-
4
- const RECONNECT_INTERVAL = ms('10s')
5
- const PING_INTERVAL = ms('10s')
6
- const MAX_RECONNECT_DELAY = ms('15s')
7
-
8
- export default function createWebSocket(getURL) {
9
- const addresses = new Set()
10
- const events = new EventEmitter()
11
- let ws
12
- let pingIntervalId = null
13
- let opened = false
14
- let isWSOpened = false
15
- let openTimeoutId
16
-
17
- function subscribeAddress(address) {
18
- const data = JSON.stringify({ type: 'subscribe-address', address })
19
- ws.send(data)
20
- }
21
-
22
- function subscribeGasPrice() {
23
- const data = JSON.stringify({ type: 'subscribe-gasprice' })
24
- ws.send(data)
25
- }
26
-
27
- function clearPing() {
28
- clearInterval(pingIntervalId)
29
- pingIntervalId = null
30
- }
31
-
32
- function onMessage(data) {
33
- data = JSON.parse(data)
34
- switch (data.type) {
35
- case 'address':
36
- events.emit(`address-${data.address}`)
37
- break
38
- case 'gasprice':
39
- events.emit('gasprice', { gasPrice: data.gasprice, baseFeePerGas: data.baseGasPrice })
40
- break
41
- }
42
- }
43
-
44
- function open() {
45
- opened = true
46
- clearTimeout(openTimeoutId)
47
- if (ws) return
48
-
49
- ws = new WebSocket(getURL())
50
- // eslint-disable-next-line unicorn/prefer-add-event-listener
51
- ws.onerror = (e) => {}
52
- // eslint-disable-next-line unicorn/prefer-add-event-listener
53
- ws.onmessage = (e) => {
54
- try {
55
- onMessage(e.data)
56
- } catch {}
57
- }
58
-
59
- // eslint-disable-next-line unicorn/prefer-add-event-listener
60
- ws.onopen = () => {
61
- for (const address of addresses.values()) subscribeAddress(address)
62
- subscribeGasPrice()
63
- if (typeof ws.ping === 'function') {
64
- pingIntervalId = setInterval(() => ws.ping(), PING_INTERVAL)
65
- } else {
66
- console.warn('Client side ping not available')
67
- }
68
-
69
- isWSOpened = true
70
- events.emit('open')
71
- }
72
-
73
- // eslint-disable-next-line unicorn/prefer-add-event-listener
74
- ws.onclose = () => {
75
- ws = null
76
- isWSOpened = false
77
- clearPing()
78
- if (opened) {
79
- openTimeoutId = setTimeout(open, reconnectDelay())
80
- }
81
-
82
- events.emit('close')
83
- }
84
- }
85
-
86
- function close() {
87
- opened = false
88
- isWSOpened = false
89
- clearTimeout(openTimeoutId)
90
- if (!ws) return
91
- // eslint-disable-next-line unicorn/prefer-add-event-listener
92
- ws.onerror = null
93
- // eslint-disable-next-line unicorn/prefer-add-event-listener
94
- ws.onmessage = null
95
- // eslint-disable-next-line unicorn/prefer-add-event-listener
96
- ws.onopen = null
97
- // eslint-disable-next-line unicorn/prefer-add-event-listener
98
- ws.onclose = null
99
- try {
100
- ws.close()
101
- } catch (e) {
102
- if (!/WebSocket was closed before the connection was established/.test(e.message)) {
103
- throw e
104
- }
105
- }
106
-
107
- ws = null
108
- clearPing()
109
- events.emit('close')
110
- }
111
-
112
- function reconnectDelay() {
113
- const min = RECONNECT_INTERVAL
114
- const max = MAX_RECONNECT_DELAY
115
- return Math.floor(Math.random() * (max - min + 1) + min)
116
- }
117
-
118
- function watch(address) {
119
- if (addresses.has(address)) return
120
- addresses.add(address)
121
-
122
- if (ws) subscribeAddress(address)
123
- }
124
-
125
- return { events, open, close, watch, isCreated: () => !!ws, isOpened: () => isWSOpened }
126
- }