@exodus/ethereum-api 8.38.0 → 8.39.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.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,26 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ## [8.39.0](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.38.1...@exodus/ethereum-api@8.39.0) (2025-06-20)
7
+
8
+
9
+ ### Features
10
+
11
+
12
+ * feat: add `isPrivate` flag to `txSend` and export `transactionPrivacy` feature (#5906)
13
+
14
+
15
+
16
+ ## [8.38.1](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.38.0...@exodus/ethereum-api@8.38.1) (2025-06-19)
17
+
18
+
19
+ ### Bug Fixes
20
+
21
+
22
+ * fix: include l1 in fee (#5895)
23
+
24
+
25
+
6
26
  ## [8.38.0](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.37.0...@exodus/ethereum-api@8.38.0) (2025-06-17)
7
27
 
8
28
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/ethereum-api",
3
- "version": "8.38.0",
3
+ "version": "8.39.0",
4
4
  "description": "Transaction monitors, fee monitors, RPC with the blockchain node, and other networking code for Ethereum and EVM-based blockchains",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -53,6 +53,7 @@
53
53
  "@exodus/bsc-meta": "^2.1.2",
54
54
  "@exodus/ethereumarbone-meta": "^2.0.3",
55
55
  "@exodus/fantommainnet-meta": "^2.0.2",
56
+ "@exodus/matic-meta": "^2.2.6",
56
57
  "@exodus/rootstock-meta": "^2.0.3"
57
58
  },
58
59
  "bugs": {
@@ -62,5 +63,5 @@
62
63
  "type": "git",
63
64
  "url": "git+https://github.com/ExodusMovement/assets.git"
64
65
  },
65
- "gitHead": "7150173679bc6f43d40c151728ae5f910bc52d88"
66
+ "gitHead": "dab2f29bb10eb8486ce39445f451439215d450cd"
66
67
  }
@@ -1,6 +1,11 @@
1
1
  import assert from 'minimalistic-assert'
2
+ import ms from 'ms'
2
3
 
3
4
  import { createEvmServer, ValidMonitorTypes } from './exodus-eth-server/index.js'
5
+ import { createEthereumHooks } from './hooks/index.js'
6
+ import { ClarityMonitor } from './tx-log/clarity-monitor.js'
7
+ import { EthereumMonitor } from './tx-log/ethereum-monitor.js'
8
+ import { EthereumNoHistoryMonitor } from './tx-log/ethereum-no-history-monitor.js'
4
9
 
5
10
  // Determines the appropriate `monitorType`, `serverUrl` and `monitorInterval`
6
11
  // to use for a given config.
@@ -74,3 +79,63 @@ export const createBroadcastTxFactory = ({ assetName, server, privacyRpcUrl }) =
74
79
  broadcastPrivateTx: (...args) => privacyServer.sendRawTransaction(...args),
75
80
  }
76
81
  }
82
+
83
+ export const createHistoryMonitorFactory = ({
84
+ assetName,
85
+ assetClientInterface,
86
+ monitorInterval,
87
+ monitorType,
88
+ server,
89
+ stakingAssetNames,
90
+ }) => {
91
+ assert(assetName, 'expected assetName')
92
+ assert(assetClientInterface, 'expected assetClientInterface')
93
+ assert(monitorType, 'expected monitorType')
94
+ assert(server, 'expected server')
95
+ assert(Array.isArray(stakingAssetNames), 'expected array stakingAssetNames')
96
+
97
+ return (args) => {
98
+ let monitor
99
+ switch (monitorType) {
100
+ case 'clarity':
101
+ case 'clarity-v2':
102
+ monitor = new ClarityMonitor({
103
+ assetClientInterface,
104
+ interval: ms(monitorInterval || '5m'),
105
+ server,
106
+ ...args,
107
+ })
108
+ break
109
+ case 'no-history':
110
+ monitor = new EthereumNoHistoryMonitor({
111
+ assetClientInterface,
112
+ interval: ms(monitorInterval || '15s'),
113
+ server,
114
+ ...args,
115
+ })
116
+ break
117
+ case 'magnifier':
118
+ monitor = new EthereumMonitor({
119
+ assetClientInterface,
120
+ interval: ms(monitorInterval || '15s'),
121
+ server,
122
+ ...args,
123
+ })
124
+ break
125
+ default:
126
+ throw new Error(`Monitor type ${monitorType} of evm asset ${assetName} is unknown`)
127
+ }
128
+
129
+ if (stakingAssetNames.length > 0) {
130
+ const afterTickHook = createEthereumHooks({
131
+ assetClientInterface,
132
+ assetName,
133
+ stakingAssetNames,
134
+ server,
135
+ })['after-tick']
136
+ monitor.addHook('after-tick', afterTickHook)
137
+ }
138
+
139
+ return monitor
140
+ }
141
+ }
@@ -19,10 +19,13 @@ import {
19
19
  } from '@exodus/ethereum-lib'
20
20
  import lodash from 'lodash'
21
21
  import assert from 'minimalistic-assert'
22
- import ms from 'ms'
23
22
 
24
23
  import { addressHasHistoryFactory } from './address-has-history.js'
25
- import { createBroadcastTxFactory, resolveMonitorSettings } from './create-asset-utils.js'
24
+ import {
25
+ createBroadcastTxFactory,
26
+ createHistoryMonitorFactory,
27
+ resolveMonitorSettings,
28
+ } from './create-asset-utils.js'
26
29
  import { createTokenFactory } from './create-token-factory.js'
27
30
  import { createCustomFeesApi } from './custom-fees.js'
28
31
  import { createEvmServer } from './exodus-eth-server/index.js'
@@ -31,13 +34,9 @@ import { createGetBalanceForAddress } from './get-balance-for-address.js'
31
34
  import { getBalancesFactory } from './get-balances.js'
32
35
  import { getFeeFactory } from './get-fee.js'
33
36
  import getFeeAsyncFactory from './get-fee-async.js'
34
- import { createEthereumHooks } from './hooks/index.js'
35
37
  import { estimateL1DataFeeFactory, getL1GetFeeFactory } from './optimism-gas/index.js'
36
38
  import { serverBasedFeeMonitorFactoryFactory } from './server-based-fee-monitor.js'
37
39
  import { createStakingApi } from './staking-api.js'
38
- import { ClarityMonitor } from './tx-log/clarity-monitor.js'
39
- import { EthereumMonitor } from './tx-log/ethereum-monitor.js'
40
- import { EthereumNoHistoryMonitor } from './tx-log/ethereum-no-history-monitor.js'
41
40
  import { txSendFactory } from './tx-send/index.js'
42
41
  import { createWeb3API } from './web3/index.js'
43
42
 
@@ -171,6 +170,12 @@ export const createAssetFactory = ({
171
170
  server,
172
171
  })
173
172
 
173
+ const { broadcastTx, broadcastPrivateTx } = createBroadcastTxFactory({
174
+ assetName: asset.name,
175
+ server,
176
+ privacyRpcUrl,
177
+ })
178
+
174
179
  const features = {
175
180
  accountState: true,
176
181
  customTokens,
@@ -180,6 +185,7 @@ export const createAssetFactory = ({
180
185
  isTestnet,
181
186
  nfts,
182
187
  noHistory: monitorType === 'no-history',
188
+ transactionPrivacy: typeof broadcastPrivateTx === 'function',
183
189
  signWithSigner: true,
184
190
  signMessageWithSigner: true,
185
191
  supportsCustomFees,
@@ -201,50 +207,14 @@ export const createAssetFactory = ({
201
207
  const accountStateClass =
202
208
  CustomAccountState || createEthereumLikeAccountState({ asset: base, assets, extraData })
203
209
 
204
- const createHistoryMonitor = (args) => {
205
- let monitor
206
- switch (monitorType) {
207
- case 'clarity':
208
- case 'clarity-v2':
209
- monitor = new ClarityMonitor({
210
- assetClientInterface,
211
- interval: ms(monitorInterval || '5m'),
212
- server,
213
- ...args,
214
- })
215
- break
216
- case 'no-history':
217
- monitor = new EthereumNoHistoryMonitor({
218
- assetClientInterface,
219
- interval: ms(monitorInterval || '15s'),
220
- server,
221
- ...args,
222
- })
223
- break
224
- case 'magnifier':
225
- monitor = new EthereumMonitor({
226
- assetClientInterface,
227
- interval: ms(monitorInterval || '15s'),
228
- server,
229
- ...args,
230
- })
231
- break
232
- default:
233
- throw new Error(`Monitor type ${monitorType} of evm asset ${asset.name} is unknown`)
234
- }
235
-
236
- if (stakingAssetNames.length > 0) {
237
- const afterTickHook = createEthereumHooks({
238
- assetClientInterface,
239
- assetName: asset.name,
240
- stakingAssetNames,
241
- server,
242
- })['after-tick']
243
- monitor.addHook('after-tick', afterTickHook)
244
- }
245
-
246
- return monitor
247
- }
210
+ const createHistoryMonitor = createHistoryMonitorFactory({
211
+ assetName: asset.name,
212
+ assetClientInterface,
213
+ monitorInterval,
214
+ monitorType,
215
+ server,
216
+ stakingAssetNames,
217
+ })
248
218
 
249
219
  const defaultAddressPath = 'm/0/0'
250
220
 
@@ -266,12 +236,6 @@ export const createAssetFactory = ({
266
236
  ? getL1GetFeeFactory({ asset, originalGetFee })
267
237
  : originalGetFee
268
238
 
269
- const { broadcastTx, broadcastPrivateTx } = createBroadcastTxFactory({
270
- assetName: asset.name,
271
- server,
272
- privacyRpcUrl,
273
- })
274
-
275
239
  const api = {
276
240
  addressHasHistory,
277
241
  broadcastTx,
@@ -78,6 +78,48 @@ export default class ClarityServerV2 extends ClarityServer {
78
78
  return fetchJsonRetry(url)
79
79
  }
80
80
 
81
+ // Clarity exposes a new way to determine fee data for EIP-1559
82
+ // networks. In addition to the `nextBlockBaseFeePerGas`, we also
83
+ // receive a `rewardPercentiles` mapping, which defines the prevailing
84
+ // 25th, 50th and 75th percentiles of `maxPriorityFeePerGas` for
85
+ // an EVM-based network.
86
+ async getGasPriceEstimation() {
87
+ return fetchJson(`${this.baseApiPath}/gas-price-estimation`)
88
+ }
89
+
90
+ async getFee() {
91
+ // HACK: When we call `getFee`, we should still
92
+ // subscribe for period fee updates.
93
+ await this.connectFee()
94
+
95
+ try {
96
+ // NOTE: Depending on whether the network is `eip1559Enabled`
97
+ // **on the backend**, this can omit certain
98
+ // properties.
99
+ //
100
+ // By default, we should receive:
101
+ // + blockNumber
102
+ // + gasPrice
103
+ // + baseGasPrice (deprecated)
104
+ //
105
+ // In addition, when `eip1559Enabled` on Clarity:
106
+ // + baseFeePerGas
107
+ // + nextBaseFeePerGas
108
+ // + rewardPercentiles,
109
+ //
110
+ // See: https://github.com/ExodusMovement/clarity/blob/d3c2a7f501a4391da630592bca3bf57c3ddd5e89/src/modules/ethereum-like/gas-price/index.js#L192C5-L219C6
111
+ return await this.getGasPriceEstimation()
112
+ } catch {
113
+ console.log(
114
+ `failed to query ${this.baseAssetName} gas-price-estimation endpoint, falling back to websocket`
115
+ )
116
+ // HACK: The `getGasPriceEstimation` endpoint is not guaranteed
117
+ // to exist for all assets. In this case, we'll fallback
118
+ // to legacy behaviour, which is to query via the WebSocket.
119
+ return this.getFeeFromWebSocket()
120
+ }
121
+ }
122
+
81
123
  async getAllTransactions({ address, cursor }) {
82
124
  let { blockNumber } = decodeCursor(cursor)
83
125
  blockNumber = blockNumber.toString()
@@ -137,7 +137,7 @@ export default class ClarityServer extends EventEmitter {
137
137
  .finally(() => socket.off('transactionsChunk', listener))
138
138
  }
139
139
 
140
- async getFee() {
140
+ getFeeFromWebSocket() {
141
141
  const socket = this.connectFee()
142
142
  return new Promise((resolve, reject) => {
143
143
  const timeout = setTimeout(() => reject(new Error('Fee Timeout')), 30_000)
@@ -153,6 +153,10 @@ export default class ClarityServer extends EventEmitter {
153
153
  })
154
154
  }
155
155
 
156
+ async getFee() {
157
+ return this.getFeeFromWebSocket()
158
+ }
159
+
156
160
  // for fee monitors
157
161
  async getGasPrice() {
158
162
  const fee = await this.getFee()
package/src/fee-utils.js CHANGED
@@ -1,10 +1,20 @@
1
1
  import assert from 'minimalistic-assert'
2
2
 
3
- const applyMultiplierToPriceInWei = ({ feeAsset, feeData, priceInWei }) => {
4
- assert(typeof priceInWei === 'string', 'gasPriceInWei should be a string')
3
+ export const shouldFetchEthLikeFallbackGasPrices = async ({ eip1559Enabled, server }) => {
4
+ const [gasPrice, baseFeePerGas] = await Promise.all([
5
+ server.getGasPrice(),
6
+ eip1559Enabled ? server.getBaseFeePerGas() : undefined,
7
+ ])
5
8
 
6
- const gasPriceMultiplier = feeData.gasPriceMultiplier || 1
7
- return feeAsset.currency.parse(priceInWei).mul(gasPriceMultiplier).toBaseString({ unit: true })
9
+ return { gasPrice, baseFeePerGas }
10
+ }
11
+
12
+ export const applyMultiplierToPrice = ({ feeAsset, gasPriceMultiplier, price }) => {
13
+ assert(typeof price === 'string', 'price should be a string')
14
+ return feeAsset.currency
15
+ .parse(price)
16
+ .mul(gasPriceMultiplier || 1)
17
+ .toBaseString({ unit: true })
8
18
  }
9
19
 
10
20
  /**
@@ -15,18 +25,18 @@ const applyMultiplierToPriceInWei = ({ feeAsset, feeData, priceInWei }) => {
15
25
  * TODO: We shouldn't actually do this, lol. The closer we are to
16
26
  * the node, the better!
17
27
  */
18
- export const rewriteFeeConfig = ({ feeAsset, feeConfig, feeData }) => {
28
+ export const rewriteFeeConfig = ({ feeAsset, feeConfig, gasPriceMultiplier }) => {
19
29
  try {
20
30
  const { gasPrice, baseFeePerGas, ...extras } = feeConfig
21
31
  return {
22
32
  ...extras,
23
- gasPrice: applyMultiplierToPriceInWei({
33
+ gasPrice: applyMultiplierToPrice({
24
34
  feeAsset,
25
- feeData,
26
- priceInWei: gasPrice,
35
+ gasPriceMultiplier,
36
+ price: gasPrice,
27
37
  }) /* required */,
28
38
  baseFeePerGas: baseFeePerGas
29
- ? applyMultiplierToPriceInWei({ feeAsset, feeData, priceInWei: baseFeePerGas })
39
+ ? applyMultiplierToPrice({ feeAsset, gasPriceMultiplier, price: baseFeePerGas })
30
40
  : undefined,
31
41
  }
32
42
  } catch (e) {
@@ -38,6 +48,154 @@ export const rewriteFeeConfig = ({ feeAsset, feeConfig, feeData }) => {
38
48
  }
39
49
  }
40
50
 
51
+ const canNormalizeToBigInt = (val) =>
52
+ typeof val === 'string' || typeof val === 'number' || typeof val === 'bigint'
53
+
54
+ // Returns a `bigint`ified version of the supplied value.
55
+ export const ensureBigInt = (val) => {
56
+ assert(canNormalizeToBigInt(val), `unable to normalize ${String(val)}`)
57
+ return BigInt(val)
58
+ }
59
+
60
+ export const getOptionalBigInt = (fetchedGasPrices, prop) => {
61
+ const val = fetchedGasPrices?.[prop]
62
+ return canNormalizeToBigInt(val) ? BigInt(val) : null
63
+ }
64
+
65
+ const calculateEthLikeFeeMonitorUpdateNonEip1559 = async ({ gasPrice }) => ({
66
+ gasPrice: `${gasPrice.toString()} wei`,
67
+ })
68
+
69
+ export const getBaseFeePerGasFromServerOrNull = async ({ feeAsset }) => {
70
+ try {
71
+ const { server } = feeAsset
72
+ return ensureBigInt(await server.getBaseFeePerGas())
73
+ } catch {
74
+ return null
75
+ }
76
+ }
77
+
78
+ // HACK: If the `baseFeePerGas` has not been defined for a network,
79
+ // or due to issues with the `baseFeePerGas` being returned
80
+ // as `0` from WebSockets, we'll make a fresh attempt to query
81
+ // . the RPC here. If that still doesn't work we'll return `null`.
82
+ const getBaseFeePerGasOrNull = async ({ feeAsset, fetchedGasPrices }) => {
83
+ const baseFeePerGas = getOptionalBigInt(fetchedGasPrices, 'baseFeePerGas')
84
+
85
+ if (typeof baseFeePerGas === 'bigint' && baseFeePerGas > BigInt('0')) return baseFeePerGas
86
+
87
+ return getBaseFeePerGasFromServerOrNull({ feeAsset })
88
+ }
89
+
90
+ export const calculateEthLikeFeeMonitorUpdateEip1559 = async ({
91
+ feeAsset,
92
+ fetchedGasPrices,
93
+ gasPrice: defaultGasPrice,
94
+ }) => {
95
+ // Determine the `baseFeePerGas`, if possible.
96
+ let baseFeePerGas = await getBaseFeePerGasOrNull({ feeAsset, fetchedGasPrices })
97
+
98
+ // Some backends will return a `nextBaseFeePerGas`, which
99
+ // provides us more information about pricing in an upcoming
100
+ // block.
101
+ //
102
+ // As a precaution, rather than choose this value unconditionally,
103
+ // we'll aim to use the larger between the two (where defined).
104
+ const nextBaseFeePerGas = getOptionalBigInt(fetchedGasPrices, 'nextBaseFeePerGas')
105
+ if (typeof nextBaseFeePerGas === 'bigint' && nextBaseFeePerGas > BigInt(baseFeePerGas || 0)) {
106
+ baseFeePerGas = nextBaseFeePerGas
107
+ }
108
+
109
+ // Knowing the `baseFeePerGas` is required for EIP-1559 networks,
110
+ // so we if we haven't found one, we cannot continue.
111
+ if (typeof baseFeePerGas !== 'bigint') throw new Error('expected baseFeePerGas')
112
+
113
+ // HACK: Enforce our expectations that the `gasPrice` must be
114
+ // at least the `baseFeePerGas`.
115
+ const gasPrice = baseFeePerGas > defaultGasPrice ? baseFeePerGas : defaultGasPrice
116
+
117
+ const defaultFeeConfig = {
118
+ gasPrice: `${gasPrice.toString()} wei`,
119
+ baseFeePerGas: `${baseFeePerGas.toString()} wei`,
120
+ }
121
+
122
+ // Determine if there are any `rewardPercentiles` specified in the
123
+ // `fetchedGasPrices`. If there's nothing, we can continue as normal.
124
+ if (!fetchedGasPrices.hasOwnProperty('rewardPercentiles')) return defaultFeeConfig
125
+
126
+ // If `rewardPercentiles` have been defined, we aim to consume the
127
+ // `50`th reward percentile, and use this as the `maxPriorityFeePerGas`.
128
+ const maxPriorityFeePerGas50Percentile = getOptionalBigInt(
129
+ fetchedGasPrices.rewardPercentiles,
130
+ '50'
131
+ )
132
+
133
+ // NOTE: We do not use a `maxPriorityFeePerGas50Percentile === 0n` as
134
+ // a condition for early termination, since this is a valid
135
+ // (although unlikely) condition.
136
+ if (typeof maxPriorityFeePerGas50Percentile !== 'bigint') return defaultFeeConfig
137
+
138
+ return {
139
+ ...defaultFeeConfig,
140
+ tipGasPrice: `${maxPriorityFeePerGas50Percentile.toString()} wei`,
141
+ }
142
+ }
143
+
144
+ /**
145
+ * The core logic for normalizing and interpreting cross-server
146
+ * fee information.
147
+ */
148
+ export const calculateEthLikeFeeMonitorUpdate = async ({
149
+ assetClientInterface,
150
+ feeAsset,
151
+ fetchedGasPrices,
152
+ }) => {
153
+ assert(assetClientInterface, 'expected assetClientInterface')
154
+ assert(feeAsset, 'expected feeAsset')
155
+ assert(fetchedGasPrices, 'expected fetchedGasPrices')
156
+
157
+ // Depending on whether `eip1559Enabled` on the client, we
158
+ // can choose to return different properties.
159
+ const { eip1559Enabled, gasPriceMultiplier } = await assetClientInterface.getFeeConfig({
160
+ assetName: feeAsset.name,
161
+ })
162
+
163
+ // NOTE: The `gasPrice` is a required value for all EVM networks.
164
+ const gasPrice = ensureBigInt(fetchedGasPrices.gasPrice)
165
+
166
+ const feeConfig = await (eip1559Enabled
167
+ ? calculateEthLikeFeeMonitorUpdateEip1559({ gasPrice, feeAsset, fetchedGasPrices })
168
+ : calculateEthLikeFeeMonitorUpdateNonEip1559({ gasPrice }))
169
+
170
+ // HACK: We use `rewriteFeeConfig` to apply some additional padding
171
+ // to our `gasPrice` and `baseFeePerGas` values. Once we're
172
+ // feeling confident, we should be safe to skip doing this for
173
+ // `eip1559Enabled` chains, since we should be able to guarantee
174
+ // next block inclusion with accuracy.
175
+ return rewriteFeeConfig({ feeAsset, feeConfig, gasPriceMultiplier })
176
+ }
177
+
178
+ /**
179
+ * A common handler for normalizing the handling of `FeeMonitor` updates.
180
+ */
181
+ export const executeEthLikeFeeMonitorUpdate = async ({
182
+ assetClientInterface,
183
+ feeAsset,
184
+ fetchedGasPrices,
185
+ }) => {
186
+ const nextFeeConfig = await calculateEthLikeFeeMonitorUpdate({
187
+ assetClientInterface,
188
+ feeAsset,
189
+ fetchedGasPrices,
190
+ })
191
+
192
+ // Update the `assetClientInterface` so our updated fee
193
+ // settings propagate through the client.
194
+ await assetClientInterface.updateFeeConfig({ assetName: feeAsset.name, feeConfig: nextFeeConfig })
195
+
196
+ return nextFeeConfig
197
+ }
198
+
41
199
  export const resolveGasPrice = ({ feeData }) => {
42
200
  assert(feeData, 'feeData is required')
43
201
 
@@ -1,8 +1,10 @@
1
1
  import { FeeMonitor } from '@exodus/asset-lib'
2
2
  import assert from 'minimalistic-assert'
3
3
 
4
- import { rewriteFeeConfig } from './fee-utils.js'
5
- import { fromHexToString } from './number-utils.js'
4
+ import {
5
+ calculateEthLikeFeeMonitorUpdate,
6
+ shouldFetchEthLikeFallbackGasPrices,
7
+ } from './fee-utils.js'
6
8
 
7
9
  /**
8
10
  * Generic eth server based fee monitor.
@@ -23,6 +25,11 @@ export const serverBasedFeeMonitorFactoryFactory = ({ asset, interval, server, a
23
25
  assert(server, 'server is required')
24
26
  assert(aci, 'aci is required')
25
27
 
28
+ const shouldFetchGasPrices = async () => {
29
+ const { eip1559Enabled } = await aci.getFeeConfig({ assetName: asset.name })
30
+ return shouldFetchEthLikeFallbackGasPrices({ eip1559Enabled, server })
31
+ }
32
+
26
33
  const FeeMonitorClass = class ServerBaseEthereumFeeMonitor extends FeeMonitor {
27
34
  constructor({ updateFee }) {
28
35
  assert(updateFee, 'updateFee is required')
@@ -34,15 +41,11 @@ export const serverBasedFeeMonitorFactoryFactory = ({ asset, interval, server, a
34
41
  }
35
42
 
36
43
  async fetchFee() {
37
- const feeData = await aci.getFeeConfig({ assetName: asset.name })
38
- const eip1559Enabled = feeData.eip1559Enabled
39
- const [gasPrice, baseFeePerGas] = await Promise.all([
40
- server.getGasPrice().then((wei) => `${fromHexToString(wei)} wei`),
41
- eip1559Enabled ? server.getBaseFeePerGas().then((wei) => `${wei} wei`) : undefined,
42
- ])
43
-
44
- const feeConfig = { gasPrice, baseFeePerGas }
45
- return rewriteFeeConfig({ feeAsset: asset, feeConfig, feeData })
44
+ return calculateEthLikeFeeMonitorUpdate({
45
+ assetClientInterface: aci,
46
+ feeAsset: asset,
47
+ fetchedGasPrices: await shouldFetchGasPrices(),
48
+ })
46
49
  }
47
50
  }
48
51
  return (...args) => new FeeMonitorClass(...args)
@@ -2,7 +2,7 @@ import { BaseMonitor } from '@exodus/asset-lib'
2
2
  import { getAssetAddresses, isRpcBalanceAsset } from '@exodus/ethereum-lib'
3
3
  import lodash from 'lodash'
4
4
 
5
- import { rewriteFeeConfig } from '../fee-utils.js'
5
+ import { executeEthLikeFeeMonitorUpdate } from '../fee-utils.js'
6
6
  import { fromHexToString } from '../number-utils.js'
7
7
  import { filterEffects, getLogItemsFromServerTx } from './clarity-utils/index.js'
8
8
  import {
@@ -286,17 +286,13 @@ export class ClarityMonitor extends BaseMonitor {
286
286
  return [...set]
287
287
  }
288
288
 
289
- async updateGasPrice({ gasPrice, baseGasPrice: baseFeePerGas }) {
289
+ // NOTE: Here, fetchedGasPrices is the result of a call to `ClarityMonitor.getFee()`.
290
+ async updateGasPrice(fetchedGasPrices) {
290
291
  try {
291
- const feeData = await this.aci.getFeeConfig({ assetName: this.asset.name })
292
-
293
- const feeConfig = {
294
- gasPrice: `${fromHexToString(gasPrice)} wei`,
295
- baseFeePerGas: baseFeePerGas ? `${fromHexToString(baseFeePerGas)} wei` : undefined,
296
- }
297
- await this.aci.updateFeeConfig({
298
- assetName: this.asset.name,
299
- feeConfig: rewriteFeeConfig({ feeAsset: this.asset, feeData, feeConfig }),
292
+ await executeEthLikeFeeMonitorUpdate({
293
+ assetClientInterface: this.aci,
294
+ feeAsset: this.asset,
295
+ fetchedGasPrices,
300
296
  })
301
297
  } catch (e) {
302
298
  this.logger.warn('error updating gasPrice', e)
@@ -2,7 +2,7 @@ import { BaseMonitor } from '@exodus/asset-lib'
2
2
  import { getAssetAddresses, isRpcBalanceAsset } from '@exodus/ethereum-lib'
3
3
  import lodash from 'lodash'
4
4
 
5
- import { rewriteFeeConfig } from '../fee-utils.js'
5
+ import { executeEthLikeFeeMonitorUpdate } from '../fee-utils.js'
6
6
  import { fromHexToString } from '../number-utils.js'
7
7
  import {
8
8
  checkPendingTransactions,
@@ -199,30 +199,12 @@ export class EthereumMonitor extends BaseMonitor {
199
199
  }
200
200
  }
201
201
 
202
- async updateGasPrice({ gasPrice, baseFeePerGas }) {
202
+ async updateGasPrice(fetchedGasPrices) {
203
203
  try {
204
- const feeData = await this.aci.getFeeConfig({ assetName: this.asset.name })
205
-
206
- const resolveBaseFeePerGas = () => {
207
- if (!feeData.eip1559Enabled) {
208
- return
209
- }
210
-
211
- if (baseFeePerGas === 0) {
212
- // In some cases, web socker sends 0 baseFeePerGas!!
213
- return this.server.getBaseFeePerGas().then((wei) => `${wei} wei`)
214
- }
215
-
216
- return `${fromHexToString(baseFeePerGas)} wei`
217
- }
218
-
219
- const feeConfig = {
220
- gasPrice: `${fromHexToString(gasPrice)} wei`,
221
- baseFeePerGas: await resolveBaseFeePerGas(),
222
- }
223
- await this.aci.updateFeeConfig({
224
- assetName: this.asset.name,
225
- feeConfig: rewriteFeeConfig({ feeAsset: this.asset, feeConfig, feeData }),
204
+ await executeEthLikeFeeMonitorUpdate({
205
+ assetClientInterface: this.aci,
206
+ feeAsset: this.asset,
207
+ fetchedGasPrices,
226
208
  })
227
209
  } catch (e) {
228
210
  this.logger.warn('error updating gasPrice', e)
@@ -79,8 +79,9 @@ const txSendFactory = ({ assetClientInterface, createUnsignedTx, useAbsoluteBala
79
79
  keepTxInput,
80
80
  isSendAll,
81
81
  isHardware,
82
+ isPrivate,
82
83
  } = options
83
- let { txInput } = options // avoid let!
84
+ let { txInput, feeAmount: providedFeeAmount } = options // avoid let!
84
85
 
85
86
  const feeOpts = {
86
87
  gasPrice: options.gasPrice,
@@ -225,14 +226,20 @@ const txSendFactory = ({ assetClientInterface, createUnsignedTx, useAbsoluteBala
225
226
  isSendAll,
226
227
  createUnsignedTx,
227
228
  feeData,
229
+ providedFeeAmount,
228
230
  }
229
231
 
230
- let { txId, rawTx, nonce, gasLimit, tipGasPrice, gasPrice } = await createTx(createTxParams)
232
+ let { txId, rawTx, nonce, gasLimit, tipGasPrice, feeAmount } = await createTx(createTxParams)
231
233
 
232
- const feeAmount = gasPrice.mul(gasLimit)
234
+ if (isPrivate && !baseAsset.api.hasFeature('transactionPrivacy'))
235
+ throw new Error(
236
+ `unable to send private transaction - transactionPrivacy is not enabled for ${baseAsset.name}`
237
+ )
238
+
239
+ const broadcastTx = isPrivate ? baseAsset.broadcastPrivateTx : baseAsset.api.broadcastTx
233
240
 
234
241
  try {
235
- await baseAsset.api.broadcastTx(rawTx.toString('hex'))
242
+ await broadcastTx(rawTx.toString('hex'))
236
243
  } catch (err) {
237
244
  const nonceTooLowErr = err.message.match(/nonce (is |)too low/i)
238
245
  const insufficientFundsErr = err.message.match(/insufficient funds/i)
@@ -378,6 +385,7 @@ const createTx = async ({
378
385
  feeOpts,
379
386
  createUnsignedTx,
380
387
  feeData,
388
+ providedFeeAmount,
381
389
  }) => {
382
390
  assert(
383
391
  nonce !== undefined && typeof nonce === 'number',
@@ -470,6 +478,23 @@ const createTx = async ({
470
478
 
471
479
  unsignedTx.txMeta.eip1559Enabled = eip1559Enabled
472
480
 
481
+ const resolveFee = async () => {
482
+ if (providedFeeAmount) {
483
+ return providedFeeAmount
484
+ }
485
+
486
+ const optimismL1DataFee = asset.baseAsset.estimateL1DataFee
487
+ ? await asset.baseAsset.estimateL1DataFee({
488
+ unsignedTx,
489
+ })
490
+ : undefined
491
+
492
+ const l1DataFee = optimismL1DataFee
493
+ ? asset.baseAsset.currency.baseUnit(optimismL1DataFee)
494
+ : asset.baseAsset.currency.ZERO
495
+ return gasPrice.mul(gasLimit).add(l1DataFee)
496
+ }
497
+
473
498
  const { txId, rawTx } = await assetClientInterface.signTransaction({
474
499
  assetName: asset.baseAsset.name,
475
500
  unsignedTx,
@@ -483,6 +508,7 @@ const createTx = async ({
483
508
  gasLimit,
484
509
  gasPrice,
485
510
  tipGasPrice,
511
+ feeAmount: await resolveFee(),
486
512
  }
487
513
  }
488
514