@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 +20 -0
- package/package.json +3 -2
- package/src/create-asset-utils.js +65 -0
- package/src/create-asset.js +20 -56
- package/src/exodus-eth-server/clarity-v2.js +42 -0
- package/src/exodus-eth-server/clarity.js +5 -1
- package/src/fee-utils.js +167 -9
- package/src/server-based-fee-monitor.js +14 -11
- package/src/tx-log/clarity-monitor.js +7 -11
- package/src/tx-log/ethereum-monitor.js +6 -24
- package/src/tx-send/tx-send.js +30 -4
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.
|
|
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": "
|
|
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
|
+
}
|
package/src/create-asset.js
CHANGED
|
@@ -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 {
|
|
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 = (
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
-
|
|
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
|
|
4
|
-
|
|
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
|
-
|
|
7
|
-
|
|
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,
|
|
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:
|
|
33
|
+
gasPrice: applyMultiplierToPrice({
|
|
24
34
|
feeAsset,
|
|
25
|
-
|
|
26
|
-
|
|
35
|
+
gasPriceMultiplier,
|
|
36
|
+
price: gasPrice,
|
|
27
37
|
}) /* required */,
|
|
28
38
|
baseFeePerGas: baseFeePerGas
|
|
29
|
-
?
|
|
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 {
|
|
5
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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 {
|
|
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
|
-
|
|
289
|
+
// NOTE: Here, fetchedGasPrices is the result of a call to `ClarityMonitor.getFee()`.
|
|
290
|
+
async updateGasPrice(fetchedGasPrices) {
|
|
290
291
|
try {
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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 {
|
|
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(
|
|
202
|
+
async updateGasPrice(fetchedGasPrices) {
|
|
203
203
|
try {
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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)
|
package/src/tx-send/tx-send.js
CHANGED
|
@@ -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,
|
|
232
|
+
let { txId, rawTx, nonce, gasLimit, tipGasPrice, feeAmount } = await createTx(createTxParams)
|
|
231
233
|
|
|
232
|
-
|
|
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
|
|
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
|
|