@exodus/ethereum-api 9.0.0-alpha.3 → 9.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/CHANGELOG.md +2121 -0
  2. package/LICENSE +7 -0
  3. package/README.md +124 -0
  4. package/package.json +34 -33
  5. package/src/address-has-history.js +6 -2
  6. package/src/allowance/constants.js +4 -5
  7. package/src/allowance/index.js +159 -40
  8. package/src/check-tx/create-assess-transaction.js +23 -0
  9. package/src/check-tx/create-check-tx.js +104 -0
  10. package/src/check-tx/index.js +1 -0
  11. package/src/create-asset-plugin-factory.js +115 -0
  12. package/src/create-asset-utils.js +520 -0
  13. package/src/create-asset.js +272 -97
  14. package/src/create-token-factory.js +47 -17
  15. package/src/custom-fees.js +57 -0
  16. package/src/eip7702-whitelist.js +62 -0
  17. package/src/ens/index.js +12 -17
  18. package/src/error-wrapper.js +383 -0
  19. package/src/eth-like-util.js +172 -33
  20. package/src/exodus-eth-server/api-coin-nodes.js +27 -196
  21. package/src/exodus-eth-server/api.js +23 -14
  22. package/src/exodus-eth-server/clarity-v2.js +255 -0
  23. package/src/exodus-eth-server/clarity.js +67 -265
  24. package/src/exodus-eth-server/errors.js +40 -0
  25. package/src/exodus-eth-server/eth-like-server.js +385 -0
  26. package/src/exodus-eth-server/fetch-json.js +48 -0
  27. package/src/exodus-eth-server/index.js +77 -0
  28. package/src/exodus-eth-server/utils.js +31 -0
  29. package/src/exodus-eth-server/ws-gateway.js +306 -0
  30. package/src/exodus-eth-server/ws.js +1 -2
  31. package/src/fee-data/EthLikeFeeData.js +81 -0
  32. package/src/fee-data/index.js +54 -0
  33. package/src/fee-utils.js +229 -0
  34. package/src/gas-estimation.js +183 -66
  35. package/src/get-balance-for-address.js +1 -1
  36. package/src/get-balances.js +285 -27
  37. package/src/get-fee.js +92 -56
  38. package/src/get-historical-balance.js +58 -0
  39. package/src/hooks/index.js +1 -1
  40. package/src/hooks/monitor.js +182 -43
  41. package/src/hooks/staking-freshness.js +58 -0
  42. package/src/index.js +118 -12
  43. package/src/move-funds.js +153 -0
  44. package/src/multicall3/index.js +212 -0
  45. package/src/multisig-address.js +146 -0
  46. package/src/nft-utils.js +60 -0
  47. package/src/number-utils.js +34 -2
  48. package/src/optimism-gas/index.js +13 -5
  49. package/src/send-validations.js +58 -0
  50. package/src/server-based-fee-monitor.js +12 -19
  51. package/src/simulate-tx/fetch-tx-preview.js +0 -2
  52. package/src/simulate-tx/index.js +2 -2
  53. package/src/simulate-tx/simulate-eth-tx.js +24 -11
  54. package/src/simulation/common.js +34 -0
  55. package/src/simulation/create-simulate-message.js +49 -0
  56. package/src/simulation/create-simulate-transactions.js +108 -0
  57. package/src/simulation/estimate-fee.js +14 -0
  58. package/src/simulation/estimate-simple-transfer.js +15 -0
  59. package/src/simulation/get-message-type.js +18 -0
  60. package/src/simulation/simulate-message-api.js +68 -0
  61. package/src/simulation/simulate-transactions-api.js +266 -0
  62. package/src/simulation/simulate-transactions.js +16 -0
  63. package/src/simulation/transactions.js +52 -0
  64. package/src/simulation/try-estimating-changes-locally.js +26 -0
  65. package/src/staking/api/index.js +43 -0
  66. package/src/staking/cached-read.js +33 -0
  67. package/src/staking/ethereum/api.js +166 -34
  68. package/src/staking/ethereum/deps.js +37 -0
  69. package/src/staking/ethereum/everstake.js +113 -0
  70. package/src/staking/ethereum/index.js +4 -3
  71. package/src/staking/ethereum/service.js +682 -181
  72. package/src/staking/ethereum/staking-utils.js +275 -4
  73. package/src/staking/fantom-staking.js +9 -9
  74. package/src/staking/index.js +4 -4
  75. package/src/staking/matic/addresses.js +2 -0
  76. package/src/staking/matic/api.js +69 -49
  77. package/src/staking/matic/index.js +3 -3
  78. package/src/staking/matic/matic-staking-utils.js +243 -5
  79. package/src/staking/matic/matic-staking.md +53 -0
  80. package/src/staking/staking-provider-client.js +1 -1
  81. package/src/staking/utils/index.js +39 -0
  82. package/src/tx-create.js +666 -0
  83. package/src/tx-log/clarity-monitor.js +273 -103
  84. package/src/tx-log/clarity-truncated-history-monitor.js +109 -0
  85. package/src/tx-log/clarity-truncated-history-monitor.md +219 -0
  86. package/src/tx-log/clarity-utils/absolute.js +96 -0
  87. package/src/tx-log/clarity-utils/get-log-items-from-server-tx.js +94 -31
  88. package/src/tx-log/clarity-utils/get-names-of-tokens-transferred-by-server-tx.js +3 -2
  89. package/src/tx-log/clarity-utils/index.js +9 -3
  90. package/src/tx-log/clarity-utils/is-spam-pending-tx.js +13 -0
  91. package/src/tx-log/clarity-utils/normalize-transactions-response.js +65 -0
  92. package/src/tx-log/ethereum-monitor.js +93 -47
  93. package/src/tx-log/ethereum-no-history-monitor.js +52 -41
  94. package/src/tx-log/get-optimistic-txlog-effects.js +379 -0
  95. package/src/tx-log/index.js +5 -3
  96. package/src/tx-log/monitor-utils/check-pending-transactions.js +14 -5
  97. package/src/tx-log/monitor-utils/exclude-unchanged-token-balances.js +1 -1
  98. package/src/tx-log/monitor-utils/extract-balance-from-clarity-account-info.js +108 -0
  99. package/src/tx-log/monitor-utils/get-balance-updates.js +44 -0
  100. package/src/tx-log/monitor-utils/get-batched-rpc-balances.js +47 -0
  101. package/src/tx-log/monitor-utils/get-current-blacklist-status.js +38 -0
  102. package/src/tx-log/monitor-utils/get-current-eip7702-delegation.js +79 -0
  103. package/src/tx-log/monitor-utils/get-derive-transactions-to-check.js +10 -5
  104. package/src/tx-log/monitor-utils/get-fee-amount.js +1 -1
  105. package/src/tx-log/monitor-utils/get-history-from-server.js +1 -1
  106. package/src/tx-log/monitor-utils/get-log-items-from-server-tx.js +75 -33
  107. package/src/tx-log/monitor-utils/get-names-of-tokens-transferred-by-server-tx.js +3 -2
  108. package/src/tx-log/monitor-utils/get-raw-transfer-amount.js +29 -0
  109. package/src/tx-log/monitor-utils/get-transfers-by-token-name.js +3 -2
  110. package/src/tx-log/monitor-utils/get-value-of-transfers.js +10 -12
  111. package/src/tx-log/monitor-utils/index.js +12 -7
  112. package/src/tx-log/monitor-utils/is-unverifiable-pending-tx-expired.js +19 -0
  113. package/src/tx-log/monitor-utils/verify-pending-tx-status-rpc.js +90 -0
  114. package/src/tx-log-staking-processor/asset-staking-tx-data.js +82 -24
  115. package/src/tx-log-staking-processor/index.js +28 -14
  116. package/src/tx-log-staking-processor/utils.js +21 -20
  117. package/src/tx-send/broadcast-error-handler.js +63 -0
  118. package/src/tx-send/index.js +1 -2
  119. package/src/tx-send/nonce-utils.js +97 -0
  120. package/src/tx-send/tx-send.js +94 -288
  121. package/src/tx-sign/index.js +17 -0
  122. package/src/tx-type/index.js +280 -0
  123. package/src/watch-tx.js +36 -0
  124. package/src/web3/createSimulateMessage.js +3 -2
  125. package/src/web3/createSimulateTransactions.js +11 -0
  126. package/src/web3/index.js +3 -1
  127. package/src/etherscan/account.js +0 -49
  128. package/src/etherscan/index.js +0 -28
  129. package/src/etherscan/logs.js +0 -16
  130. package/src/etherscan/proxy.js +0 -47
  131. package/src/etherscan/request.js +0 -26
  132. package/src/etherscan/ws.js +0 -88
  133. package/src/exodus-eth-server/server-factory.js +0 -28
  134. package/src/exodus-eth-server/server-map.js +0 -21
  135. package/src/get-fee-async.js +0 -97
  136. package/src/staking/matic/service.js +0 -437
  137. package/src/staking-api.js +0 -5
  138. package/src/tx-log/clarity-utils/get-derive-data-needed-for-tick.js +0 -12
  139. package/src/tx-log-staking-processor/get-asset-tx-amount.js +0 -26
  140. package/src/tx-send/get-fee-info.js +0 -41
  141. package/src/websocket/index.android.js +0 -2
  142. package/src/websocket/index.ios.js +0 -2
  143. 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
+ }
@@ -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
+ }