@exodus/ethereum-api 9.0.0-alpha.3 → 9.0.1
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 +2139 -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 +273 -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 +276 -103
- package/src/tx-log/clarity-truncated-history-monitor.js +130 -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
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import { createConsoleLogger } from '@exodus/asset-lib'
|
|
2
|
+
import EventEmitter from 'eventemitter3'
|
|
3
|
+
import assert from 'minimalistic-assert'
|
|
4
|
+
|
|
5
|
+
const createSubscriptionName = (network) => {
|
|
6
|
+
return `v1/transactions/${network}`
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const createSubscriptionKey = ({ subscriptionName, address }) => {
|
|
10
|
+
return `${subscriptionName}|${address}`
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const parseSubscriptionKey = (subscriptionKey) => {
|
|
14
|
+
const [subscriptionName, address] = subscriptionKey.split('|')
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
subscriptionName,
|
|
18
|
+
subscriptionAddress: address,
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @typedef subscribeWalletAddresses
|
|
24
|
+
* @property {string} network - The network identifier
|
|
25
|
+
* @property {string} address - The wallet address to subscribe
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
export default class WsGateway extends EventEmitter {
|
|
29
|
+
#socket = null
|
|
30
|
+
#pendingOutboundMessages = []
|
|
31
|
+
// Dedup subscriptions to reduce workload on server and resubscribe after closing
|
|
32
|
+
#subscriptions = new Set()
|
|
33
|
+
#reconnectionTimeoutId = null
|
|
34
|
+
#uri = null
|
|
35
|
+
#logger = createConsoleLogger(`@exodus/ws-gateway`)
|
|
36
|
+
|
|
37
|
+
constructor({ uri }) {
|
|
38
|
+
super()
|
|
39
|
+
assert(typeof uri === 'string', 'expected string uri')
|
|
40
|
+
|
|
41
|
+
this.#uri = uri
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
_handlers = {
|
|
45
|
+
message: ({ target, data }) => {
|
|
46
|
+
if (target !== this.#socket) return
|
|
47
|
+
try {
|
|
48
|
+
const msg = JSON.parse(data)
|
|
49
|
+
|
|
50
|
+
if (Array.isArray(msg)) {
|
|
51
|
+
for (const item of msg) {
|
|
52
|
+
this.#handleMessage(item)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
this.#handleMessage(msg)
|
|
59
|
+
} catch (error) {
|
|
60
|
+
this.#logger.error('WS gateway on message error', { error })
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
close: ({ target, code, reason }) => {
|
|
64
|
+
if (target !== this.#socket) return
|
|
65
|
+
this.#reconnect({ code, reason })
|
|
66
|
+
},
|
|
67
|
+
open: ({ target }) => {
|
|
68
|
+
if (target !== this.#socket) return
|
|
69
|
+
this.#flushPendingOutbound()
|
|
70
|
+
this.emit('opened')
|
|
71
|
+
},
|
|
72
|
+
error: ({ target }) => {
|
|
73
|
+
if (target !== this.#socket) return
|
|
74
|
+
this.#reconnect({ code: 0, reason: 'Error' })
|
|
75
|
+
},
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
getSocket() {
|
|
79
|
+
return this.#socket
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
getSubscriptions() {
|
|
83
|
+
return this.#subscriptions
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
setServer(uri) {
|
|
87
|
+
this.#uri = uri || this.#uri
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
start() {
|
|
91
|
+
if (this.#socket) {
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
this.#socket = new WebSocket(this.#uri)
|
|
96
|
+
|
|
97
|
+
this.#socket.addEventListener('message', this._handlers.message)
|
|
98
|
+
this.#socket.addEventListener('close', this._handlers.close)
|
|
99
|
+
this.#socket.addEventListener('open', this._handlers.open)
|
|
100
|
+
this.#socket.addEventListener('error', this._handlers.error)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
#reconnect({ code, reason } = {}) {
|
|
104
|
+
this.#logger.warn(`Reconnect. Code ${code}, ${reason || 'Empty reason'}`)
|
|
105
|
+
|
|
106
|
+
this.#clearSocketListeners()
|
|
107
|
+
|
|
108
|
+
if (this.#socket) {
|
|
109
|
+
if ([WebSocket.OPEN, WebSocket.CONNECTING].includes(this.#socket.readyState)) {
|
|
110
|
+
this.#socket.close()
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
this.#socket = null
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
clearTimeout(this.#reconnectionTimeoutId)
|
|
117
|
+
this.#subscriptions.clear()
|
|
118
|
+
this.#pendingOutboundMessages = []
|
|
119
|
+
|
|
120
|
+
this.#reconnectionTimeoutId = setTimeout(() => {
|
|
121
|
+
this.start()
|
|
122
|
+
clearTimeout(this.#reconnectionTimeoutId)
|
|
123
|
+
}, 2500)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
unsubscribeWalletAddresses({ network, addresses = [] } = {}) {
|
|
127
|
+
assert(network, '"network" is required')
|
|
128
|
+
|
|
129
|
+
const unsubscribeName = createSubscriptionName(network)
|
|
130
|
+
|
|
131
|
+
const payload = []
|
|
132
|
+
|
|
133
|
+
const addressesSet = new Set(addresses)
|
|
134
|
+
|
|
135
|
+
for (const subscriptionKey of this.#subscriptions) {
|
|
136
|
+
const { subscriptionName, subscriptionAddress } = parseSubscriptionKey(subscriptionKey)
|
|
137
|
+
if (subscriptionName !== unsubscribeName) continue
|
|
138
|
+
|
|
139
|
+
// Unsubscribe whole network
|
|
140
|
+
if (addressesSet.size === 0) {
|
|
141
|
+
if (!payload.some((item) => item.subscription === subscriptionName)) {
|
|
142
|
+
payload.push({
|
|
143
|
+
subscription: subscriptionName,
|
|
144
|
+
})
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
this.#subscriptions.delete(subscriptionKey)
|
|
148
|
+
continue
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (addressesSet.has(subscriptionAddress)) {
|
|
152
|
+
payload.push({
|
|
153
|
+
subscription: subscriptionName,
|
|
154
|
+
entityId: subscriptionAddress,
|
|
155
|
+
})
|
|
156
|
+
this.#subscriptions.delete(subscriptionKey)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (payload.length === 0) {
|
|
161
|
+
return
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
this.#sendMessage({
|
|
165
|
+
eventName: 'unsubscribe',
|
|
166
|
+
payload,
|
|
167
|
+
})
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* @param {Array<subscribeWalletAddresses>} subscriptions
|
|
172
|
+
*/
|
|
173
|
+
subscribeWalletAddresses({ network, addresses }) {
|
|
174
|
+
assert(network, '"network" is required')
|
|
175
|
+
const payload = []
|
|
176
|
+
|
|
177
|
+
for (const address of addresses) {
|
|
178
|
+
const subscriptionName = createSubscriptionName(network)
|
|
179
|
+
const subscriptionKey = createSubscriptionKey({ subscriptionName, address })
|
|
180
|
+
|
|
181
|
+
if (this.#subscriptions.has(subscriptionKey)) {
|
|
182
|
+
continue
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
payload.push({
|
|
186
|
+
subscription: subscriptionName,
|
|
187
|
+
entityId: address,
|
|
188
|
+
})
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (payload.length === 0) {
|
|
192
|
+
return
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
this.#sendMessage({
|
|
196
|
+
eventName: 'subscribe',
|
|
197
|
+
payload,
|
|
198
|
+
})
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
#sendMessage(message) {
|
|
202
|
+
if (!this.#socket) {
|
|
203
|
+
this.start()
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (!this.#socket) {
|
|
207
|
+
return
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (this.#socket.readyState === WebSocket.OPEN) {
|
|
211
|
+
this.#socket.send(JSON.stringify(message))
|
|
212
|
+
return
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
this.#pendingOutboundMessages.push(message)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
#flushPendingOutbound() {
|
|
219
|
+
if (!this.#socket || this.#socket.readyState !== WebSocket.OPEN) {
|
|
220
|
+
return
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const pending = this.#pendingOutboundMessages
|
|
224
|
+
this.#pendingOutboundMessages = []
|
|
225
|
+
|
|
226
|
+
for (const message of pending) {
|
|
227
|
+
this.#socket.send(JSON.stringify(message))
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
#handleMessage(message) {
|
|
232
|
+
switch (message.type) {
|
|
233
|
+
case 'new_transactions': {
|
|
234
|
+
const { subscription, entityId, payload } = message
|
|
235
|
+
const network = subscription.split('/')[2]
|
|
236
|
+
|
|
237
|
+
for (const transaction of payload.transactions) {
|
|
238
|
+
this.emit(`${network}:new_transaction`, {
|
|
239
|
+
address: entityId,
|
|
240
|
+
transaction,
|
|
241
|
+
cursor: payload.cursor,
|
|
242
|
+
})
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
break
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
case 'connection_ack': {
|
|
249
|
+
this.emit('connected', message)
|
|
250
|
+
break
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
case 'subscribe_ack': {
|
|
254
|
+
const { subscription, entityId } = message
|
|
255
|
+
|
|
256
|
+
const subscriptionKey = createSubscriptionKey({
|
|
257
|
+
subscriptionName: subscription,
|
|
258
|
+
address: entityId,
|
|
259
|
+
})
|
|
260
|
+
this.#subscriptions.add(subscriptionKey)
|
|
261
|
+
|
|
262
|
+
this.emit('subscribed', message)
|
|
263
|
+
break
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
case 'error': {
|
|
267
|
+
this.#logger.error('Error from server', message)
|
|
268
|
+
this.emit('error', message)
|
|
269
|
+
break
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
default: {
|
|
273
|
+
break
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
#clearSocketListeners() {
|
|
279
|
+
if (!this.#socket) return
|
|
280
|
+
|
|
281
|
+
this.#socket.removeEventListener('message', this._handlers.message)
|
|
282
|
+
this.#socket.removeEventListener('close', this._handlers.close)
|
|
283
|
+
this.#socket.removeEventListener('open', this._handlers.open)
|
|
284
|
+
this.#socket.removeEventListener('error', this._handlers.error)
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
dispose(network, addresses) {
|
|
288
|
+
this.unsubscribeWalletAddresses({ network, addresses })
|
|
289
|
+
this.removeAllListeners(`${network}:new_transaction`)
|
|
290
|
+
|
|
291
|
+
if (this.#subscriptions.size > 0) {
|
|
292
|
+
return
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
this.#flushPendingOutbound()
|
|
296
|
+
this.#clearSocketListeners()
|
|
297
|
+
|
|
298
|
+
clearTimeout(this.#reconnectionTimeoutId)
|
|
299
|
+
|
|
300
|
+
if (this.#socket && [WebSocket.OPEN, WebSocket.CONNECTING].includes(this.#socket.readyState)) {
|
|
301
|
+
this.#socket.close()
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
this.#socket = null
|
|
305
|
+
}
|
|
306
|
+
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
+
import EventEmitter from 'events/events.js' // forces it to use the module from node_modules
|
|
1
2
|
import ms from 'ms'
|
|
2
|
-
import EventEmitter from 'events/' // '/' forces it to use the module from node_modules
|
|
3
|
-
import WebSocket from '../websocket'
|
|
4
3
|
|
|
5
4
|
const RECONNECT_INTERVAL = ms('10s')
|
|
6
5
|
const PING_INTERVAL = ms('10s')
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { FeeData } from '@exodus/asset-lib'
|
|
2
|
+
import {
|
|
3
|
+
mapCurrency as defaultMapCurrency,
|
|
4
|
+
modelToJSON,
|
|
5
|
+
} from '@exodus/asset-lib/src/utils/index.js'
|
|
6
|
+
import { isNumberUnit } from '@exodus/currency'
|
|
7
|
+
import lodash from 'lodash'
|
|
8
|
+
|
|
9
|
+
const { isEmpty, isEqual } = lodash
|
|
10
|
+
|
|
11
|
+
const PRE_EIP1559_UNIT_KEYS_GUARD = ['gasPrice']
|
|
12
|
+
const EIP1559_UNIT_KEYS_GUARD = ['gasPrice', 'baseFeePerGas']
|
|
13
|
+
|
|
14
|
+
export const bound = ({ val, multiplier, max, min }) => {
|
|
15
|
+
if (!isNumberUnit(val)) return val
|
|
16
|
+
|
|
17
|
+
if (multiplier) {
|
|
18
|
+
val = val.mul(multiplier)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (max) {
|
|
22
|
+
val = val.lte(max) ? val : max
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (min) {
|
|
26
|
+
val = val.gte(min) ? val : min
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return val
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const mapCurrency = ({ currency, config }) => {
|
|
33
|
+
const { eip1559Enabled } = config
|
|
34
|
+
return defaultMapCurrency(
|
|
35
|
+
config,
|
|
36
|
+
[currency],
|
|
37
|
+
eip1559Enabled ? EIP1559_UNIT_KEYS_GUARD : PRE_EIP1559_UNIT_KEYS_GUARD
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const boundFactory =
|
|
42
|
+
({ config }) =>
|
|
43
|
+
(val) => {
|
|
44
|
+
const { multiplier, max, min } = config
|
|
45
|
+
return bound({ val, multiplier, max, min })
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export class EthLikeFeeData extends FeeData {
|
|
49
|
+
constructor({ currency, config }) {
|
|
50
|
+
super({ currency, config, mainKey: 'gasPrice' })
|
|
51
|
+
Object.assign(this, mapCurrency({ currency, config }))
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
update(config = {}) {
|
|
55
|
+
if (isEmpty(config)) return this
|
|
56
|
+
|
|
57
|
+
config = super.update(config)
|
|
58
|
+
|
|
59
|
+
if (config.eip1559Enabled) {
|
|
60
|
+
const bound = boundFactory({ config })
|
|
61
|
+
|
|
62
|
+
const { baseFeePerGas, tipGasPrice = this.currency.ZERO } = config
|
|
63
|
+
|
|
64
|
+
const enforcedEffectiveGasPrice = bound(baseFeePerGas.add(tipGasPrice))
|
|
65
|
+
|
|
66
|
+
// When the `effectiveGasPrice` is lower, we'll increase the
|
|
67
|
+
// `tipGasPrice` to ensure we meet the `enforcedEffectiveGasPrice`.
|
|
68
|
+
//
|
|
69
|
+
// Conversely, when the `effectiveGasPrice` is too high, we'll
|
|
70
|
+
// try to reduce the `tipGasPrice` where possible, but whilst
|
|
71
|
+
// maintaining the `baseFeePerGas`, since this is the minimum
|
|
72
|
+
// for the current network.
|
|
73
|
+
Object.assign(config, {
|
|
74
|
+
tipGasPrice: enforcedEffectiveGasPrice.sub(baseFeePerGas).clampLowerZero(),
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (isEqual(this.toJSON(), modelToJSON({ ...config }))) return this
|
|
79
|
+
return new this.constructor({ config, currency: this.currency })
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import assert from 'minimalistic-assert'
|
|
2
|
+
|
|
3
|
+
import { EthLikeFeeData } from './EthLikeFeeData.js'
|
|
4
|
+
|
|
5
|
+
export const createFeeDataConfigDefaults = ({ currency, feeDataConfig }) => {
|
|
6
|
+
const shared = {
|
|
7
|
+
tipGasPrice: '0 Gwei',
|
|
8
|
+
fuelThreshold: '0 Gwei',
|
|
9
|
+
gasPriceMinimumRate: 0.6,
|
|
10
|
+
gasPriceMaximumRate: 1.3,
|
|
11
|
+
gasPriceMultiplier: 1,
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (feeDataConfig.eip1559Enabled === true) {
|
|
15
|
+
assert(feeDataConfig.baseFeePerGas, 'baseFeePerGas is required')
|
|
16
|
+
const baseFeePerGas = currency.parse(feeDataConfig.baseFeePerGas)
|
|
17
|
+
const gasPrice = feeDataConfig.gasPrice
|
|
18
|
+
? currency.parse(feeDataConfig.gasPrice)
|
|
19
|
+
: baseFeePerGas.mul(1.5)
|
|
20
|
+
return {
|
|
21
|
+
gasPrice: gasPrice.toBaseString({ unit: true }),
|
|
22
|
+
baseFeePerGas: baseFeePerGas.toBaseString({ unit: true }),
|
|
23
|
+
eip1559Enabled: feeDataConfig.eip1559Enabled,
|
|
24
|
+
...shared,
|
|
25
|
+
...feeDataConfig,
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (feeDataConfig.eip1559Enabled === false) {
|
|
30
|
+
assert(feeDataConfig.gasPrice, 'gasPrice is required')
|
|
31
|
+
const gasPrice = currency.parse(feeDataConfig.gasPrice)
|
|
32
|
+
return {
|
|
33
|
+
gasPrice: gasPrice.toBaseString({ unit: true }),
|
|
34
|
+
max: gasPrice.mul(5).toBaseString({ unit: true }),
|
|
35
|
+
min: gasPrice.div(5).toBaseString({ unit: true }),
|
|
36
|
+
eip1559Enabled: feeDataConfig.eip1559Enabled,
|
|
37
|
+
...shared,
|
|
38
|
+
...feeDataConfig,
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
gasPrice: '0.02 Gwei',
|
|
44
|
+
baseFeePerGas: '0.02 Gwei',
|
|
45
|
+
eip1559Enabled: true,
|
|
46
|
+
...shared,
|
|
47
|
+
...feeDataConfig,
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const createFeeData = ({ currency, feeDataConfig }) => {
|
|
52
|
+
const config = createFeeDataConfigDefaults({ currency, feeDataConfig })
|
|
53
|
+
return new EthLikeFeeData({ config, currency })
|
|
54
|
+
}
|
package/src/fee-utils.js
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import assert from 'minimalistic-assert'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Returns augmented `feeConfig` values manipulated using the `feeData`,
|
|
5
|
+
* for example, like multiplying the `gasPrice`. This helps us to apply
|
|
6
|
+
* modifications to values before presenting them to the consumer.
|
|
7
|
+
*/
|
|
8
|
+
export const refineFeeConfig = ({ feeAsset, feeConfig, gasPriceBump, gasPriceMultiplier }) => {
|
|
9
|
+
const fromString = (str) => {
|
|
10
|
+
assert(typeof str === 'string', 'expected string')
|
|
11
|
+
return feeAsset.currency.parse(str)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
const { gasPrice, baseFeePerGas, tipGasPrice, ...extras } = feeConfig
|
|
16
|
+
|
|
17
|
+
gasPriceMultiplier ||= 1
|
|
18
|
+
gasPriceBump ||= feeAsset.currency.ZERO
|
|
19
|
+
|
|
20
|
+
if (!baseFeePerGas) {
|
|
21
|
+
const nextGasPrice = fromString(gasPrice).mul(gasPriceMultiplier).add(gasPriceBump)
|
|
22
|
+
return {
|
|
23
|
+
...extras,
|
|
24
|
+
gasPrice: nextGasPrice.toBaseString({ unit: true }),
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const nextBaseFeePerGas = fromString(baseFeePerGas).mul(gasPriceMultiplier)
|
|
29
|
+
const nextTipGasPrice = tipGasPrice
|
|
30
|
+
? fromString(tipGasPrice).mul(gasPriceMultiplier).add(gasPriceBump)
|
|
31
|
+
: gasPriceBump
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
...extras,
|
|
35
|
+
baseFeePerGas: nextBaseFeePerGas.toBaseString({ unit: true }),
|
|
36
|
+
tipGasPrice: nextTipGasPrice.toBaseString({ unit: true }),
|
|
37
|
+
gasPrice: nextBaseFeePerGas.add(nextTipGasPrice).toBaseString({ unit: true }),
|
|
38
|
+
}
|
|
39
|
+
} catch (e) {
|
|
40
|
+
console.error(
|
|
41
|
+
'Unable to rewrite feeConfig - gas prices will not be amplified using the `gasPriceMultiplier`.',
|
|
42
|
+
e
|
|
43
|
+
)
|
|
44
|
+
return feeConfig
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const canNormalizeToBigInt = (val) =>
|
|
49
|
+
typeof val === 'string' || typeof val === 'number' || typeof val === 'bigint'
|
|
50
|
+
|
|
51
|
+
// Returns a `bigint`ified version of the supplied value.
|
|
52
|
+
export const ensureBigInt = (val) => {
|
|
53
|
+
assert(canNormalizeToBigInt(val), `unable to normalize ${String(val)}`)
|
|
54
|
+
return BigInt(val)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export const getOptionalBigInt = (fetchedGasPrices, prop) => {
|
|
58
|
+
const val = fetchedGasPrices?.[prop]
|
|
59
|
+
return canNormalizeToBigInt(val) ? BigInt(val) : null
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const calculateEthLikeFeeMonitorUpdateNonEip1559 = async ({ gasPrice }) => ({
|
|
63
|
+
gasPrice: `${gasPrice.toString()} wei`,
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
export const getBaseFeePerGasFromServerOrNull = async ({ feeAsset }) => {
|
|
67
|
+
try {
|
|
68
|
+
const { server } = feeAsset
|
|
69
|
+
return ensureBigInt(await server.getBaseFeePerGas())
|
|
70
|
+
} catch {
|
|
71
|
+
return null
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// HACK: If the `baseFeePerGas` has not been defined for a network,
|
|
76
|
+
// or due to issues with the `baseFeePerGas` being returned
|
|
77
|
+
// as `0` from WebSockets, we'll make a fresh attempt to query
|
|
78
|
+
// . the RPC here. If that still doesn't work we'll return `null`.
|
|
79
|
+
const getBaseFeePerGasOrNull = async ({ feeAsset, fetchedGasPrices }) => {
|
|
80
|
+
const baseFeePerGas = getOptionalBigInt(fetchedGasPrices, 'baseFeePerGas')
|
|
81
|
+
|
|
82
|
+
if (typeof baseFeePerGas === 'bigint' && baseFeePerGas > BigInt('0')) return baseFeePerGas
|
|
83
|
+
|
|
84
|
+
return getBaseFeePerGasFromServerOrNull({ feeAsset })
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export const calculateEthLikeFeeMonitorUpdateEip1559 = async ({
|
|
88
|
+
feeAsset,
|
|
89
|
+
fetchedGasPrices,
|
|
90
|
+
gasPrice: defaultGasPrice,
|
|
91
|
+
}) => {
|
|
92
|
+
// Determine the `baseFeePerGas`, if possible.
|
|
93
|
+
let baseFeePerGas = await getBaseFeePerGasOrNull({ feeAsset, fetchedGasPrices })
|
|
94
|
+
|
|
95
|
+
// Some backends will return a `nextBaseFeePerGas`, which
|
|
96
|
+
// provides us more information about pricing in an upcoming
|
|
97
|
+
// block.
|
|
98
|
+
//
|
|
99
|
+
// As a precaution, rather than choose this value unconditionally,
|
|
100
|
+
// we'll aim to use the larger between the two (where defined).
|
|
101
|
+
const nextBaseFeePerGas = getOptionalBigInt(fetchedGasPrices, 'nextBaseFeePerGas')
|
|
102
|
+
if (typeof nextBaseFeePerGas === 'bigint' && nextBaseFeePerGas > BigInt(baseFeePerGas || 0)) {
|
|
103
|
+
baseFeePerGas = nextBaseFeePerGas
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Knowing the `baseFeePerGas` is required for EIP-1559 networks,
|
|
107
|
+
// so we if we haven't found one, we cannot continue.
|
|
108
|
+
if (typeof baseFeePerGas !== 'bigint') throw new Error('expected baseFeePerGas')
|
|
109
|
+
|
|
110
|
+
// HACK: Enforce our expectations that the `gasPrice` must be
|
|
111
|
+
// at least the `baseFeePerGas`.
|
|
112
|
+
const gasPrice = baseFeePerGas > defaultGasPrice ? baseFeePerGas : defaultGasPrice
|
|
113
|
+
|
|
114
|
+
// `eth_gasPrice` embeds the node's prevailing tip on top of the
|
|
115
|
+
// `baseFeePerGas`. Without an explicit `tipGasPrice`, `refineFeeConfig`
|
|
116
|
+
// collapses the effective gas price to the bare base fee, producing
|
|
117
|
+
// zero-tip transactions that private bundle relays drop silently.
|
|
118
|
+
const inferredTipGasPrice = gasPrice - baseFeePerGas
|
|
119
|
+
|
|
120
|
+
const defaultFeeConfig = {
|
|
121
|
+
gasPrice: `${gasPrice.toString()} wei`,
|
|
122
|
+
baseFeePerGas: `${baseFeePerGas.toString()} wei`,
|
|
123
|
+
...(inferredTipGasPrice > BigInt(0) && {
|
|
124
|
+
tipGasPrice: `${inferredTipGasPrice.toString()} wei`,
|
|
125
|
+
}),
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Determine if there are any `rewardPercentiles` specified in the
|
|
129
|
+
// `fetchedGasPrices`. If there's nothing, we can continue as normal.
|
|
130
|
+
if (!fetchedGasPrices.hasOwnProperty('rewardPercentiles')) return defaultFeeConfig
|
|
131
|
+
|
|
132
|
+
// If `rewardPercentiles` have been defined, we aim to consume the
|
|
133
|
+
// `50`th reward percentile, and use this as the `maxPriorityFeePerGas`.
|
|
134
|
+
const maxPriorityFeePerGas50Percentile = getOptionalBigInt(
|
|
135
|
+
fetchedGasPrices.rewardPercentiles,
|
|
136
|
+
'50'
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
// NOTE: We do not use a `maxPriorityFeePerGas50Percentile === 0n` as
|
|
140
|
+
// a condition for early termination, since this is a valid
|
|
141
|
+
// (although unlikely) condition.
|
|
142
|
+
if (typeof maxPriorityFeePerGas50Percentile !== 'bigint') return defaultFeeConfig
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
...defaultFeeConfig,
|
|
146
|
+
gasPrice: `${(baseFeePerGas + maxPriorityFeePerGas50Percentile).toString()} wei`,
|
|
147
|
+
tipGasPrice: `${maxPriorityFeePerGas50Percentile.toString()} wei`,
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* The core logic for normalizing and interpreting cross-server
|
|
153
|
+
* fee information.
|
|
154
|
+
*/
|
|
155
|
+
export const calculateEthLikeFeeMonitorUpdate = async ({
|
|
156
|
+
assetClientInterface,
|
|
157
|
+
feeAsset,
|
|
158
|
+
fetchedGasPrices,
|
|
159
|
+
}) => {
|
|
160
|
+
assert(assetClientInterface, 'expected assetClientInterface')
|
|
161
|
+
assert(feeAsset, 'expected feeAsset')
|
|
162
|
+
assert(fetchedGasPrices, 'expected fetchedGasPrices')
|
|
163
|
+
|
|
164
|
+
// Depending on whether `eip1559Enabled` on the client, we
|
|
165
|
+
// can choose to return different properties.
|
|
166
|
+
const { eip1559Enabled, gasPriceBump, gasPriceMultiplier } =
|
|
167
|
+
await assetClientInterface.getFeeConfig({
|
|
168
|
+
assetName: feeAsset.name,
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
// NOTE: The `gasPrice` is a required value for all EVM networks.
|
|
172
|
+
const gasPrice = ensureBigInt(fetchedGasPrices.gasPrice)
|
|
173
|
+
|
|
174
|
+
const feeConfig = await (eip1559Enabled
|
|
175
|
+
? calculateEthLikeFeeMonitorUpdateEip1559({ gasPrice, feeAsset, fetchedGasPrices })
|
|
176
|
+
: calculateEthLikeFeeMonitorUpdateNonEip1559({ gasPrice }))
|
|
177
|
+
|
|
178
|
+
return refineFeeConfig({ feeAsset, feeConfig, gasPriceBump, gasPriceMultiplier })
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* A common handler for normalizing the handling of `FeeMonitor` updates.
|
|
183
|
+
*/
|
|
184
|
+
export const executeEthLikeFeeMonitorUpdate = async ({
|
|
185
|
+
assetClientInterface,
|
|
186
|
+
feeAsset,
|
|
187
|
+
fetchedGasPrices,
|
|
188
|
+
}) => {
|
|
189
|
+
const nextFeeConfig = await calculateEthLikeFeeMonitorUpdate({
|
|
190
|
+
assetClientInterface,
|
|
191
|
+
feeAsset,
|
|
192
|
+
fetchedGasPrices,
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
// Update the `assetClientInterface` so our updated fee
|
|
196
|
+
// settings propagate through the client.
|
|
197
|
+
await assetClientInterface.updateFeeConfig({ assetName: feeAsset.name, feeConfig: nextFeeConfig })
|
|
198
|
+
|
|
199
|
+
return nextFeeConfig
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// TODO: rename to getEffectiveGasPriceForFeeData
|
|
203
|
+
export const resolveGasPrice = ({ feeData }) => {
|
|
204
|
+
assert(feeData, 'feeData is required')
|
|
205
|
+
|
|
206
|
+
if (feeData.eip1559Enabled) {
|
|
207
|
+
return feeData.tipGasPrice
|
|
208
|
+
? feeData.baseFeePerGas.add(feeData.tipGasPrice)
|
|
209
|
+
: feeData.baseFeePerGas
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return feeData.gasPrice
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export const ensureSaneEip1559GasPriceForTipGasPrice = ({ gasPrice, tipGasPrice }) => {
|
|
216
|
+
if (!tipGasPrice || tipGasPrice.lt(gasPrice)) return gasPrice
|
|
217
|
+
return tipGasPrice
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export const getNormalizedFeeDataForCustomFee = ({ customFee, feeData }) => {
|
|
221
|
+
const { eip1559Enabled } = feeData
|
|
222
|
+
// We must ensure maxPriorityFeePerGas <= maxFeePerGas or our transaction library throws an error
|
|
223
|
+
// It's a bit counterintuitive since maxPriorityFeePerGas should only contain the tip,
|
|
224
|
+
// so we should be subtracting the base gas price from the custom gas price to keep just the tip
|
|
225
|
+
// but the fee is also limited by our maxFeePerGas above, so that implicitly captures the max tip.
|
|
226
|
+
// Setting this tipGasPrice to undefined will result in a legacy transaction (not an EIP1559 anymore)
|
|
227
|
+
if (eip1559Enabled && customFee) return { ...feeData, tipGasPrice: customFee }
|
|
228
|
+
return feeData
|
|
229
|
+
}
|