@exodus/ethereum-api 8.40.0 → 8.41.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,352 @@
1
+ import { calculateBumpedGasPrice, currency2buffer, isEthereumLikeToken } from '@exodus/ethereum-lib'
2
+ import createEthereumJsTx from '@exodus/ethereum-lib/src/unsigned-tx/create-ethereumjs-tx.js'
3
+ import assert from 'minimalistic-assert'
4
+
5
+ import * as ErrorWrapper from './error-wrapper.js'
6
+ import { isContractAddressCached } from './eth-like-util.js'
7
+ import { ensureSaneEip1559GasPriceForTipGasPrice } from './fee-utils.js'
8
+ import { ARBITRARY_ADDRESS, fetchGasLimit, resolveDefaultTxInput } from './gas-estimation.js'
9
+ import { getFeeFactoryGasPrices } from './get-fee.js'
10
+ import { getNftArguments } from './nft-utils.js'
11
+ import { resolveNonce } from './tx-send/nonce-utils.js'
12
+
13
+ async function createUnsignedTxWithFees({
14
+ asset,
15
+ chainId,
16
+ to, // the tx to address, it could be the reciver address for native sending, the token contract, the DEX contract, etc
17
+ value, // the value of the tx in NU, it can be the value in eth or 0 when calling contracts
18
+ data, // the data of the tx in hex string, it can be 0x for native sending or the params when sending to a contract
19
+ gasLimit,
20
+ eip1559Enabled,
21
+ gasPrice, // eip 1559: `maxFeePerGas`
22
+ tipGasPrice, // eip 1559: `maxPriorityPerGas`
23
+ nonce,
24
+ bumpTxId,
25
+ coinAmount, // coinAmount
26
+ fromAddress, // user's sending address
27
+ toAddress, // user's receiver address
28
+ }) {
29
+ assert(asset, 'asset is required')
30
+ assert(typeof chainId === 'number', 'chainId is required')
31
+ assert(to, 'to is required')
32
+ assert(value, 'value is required')
33
+ assert(data, 'data is required')
34
+ assert(gasLimit, 'gasLimit is required')
35
+ assert(gasPrice, 'gasPrice is required')
36
+ assert(coinAmount, 'coinAmount is required')
37
+ assert(fromAddress, 'fromAddress is required')
38
+ assert(toAddress, 'toAddress is required')
39
+ assert(typeof eip1559Enabled === 'boolean', 'eip1559Enabled is required')
40
+
41
+ const ethjsTx = createEthereumJsTx({
42
+ txData: {
43
+ nonce,
44
+ gasPrice: currency2buffer(gasPrice),
45
+ tipGasPrice: tipGasPrice ? currency2buffer(tipGasPrice) : undefined,
46
+ gasLimit,
47
+ to,
48
+ value: currency2buffer(value),
49
+ data,
50
+ chainId,
51
+ },
52
+ txMeta: {
53
+ eip1559Enabled,
54
+ },
55
+ })
56
+ const transactionBuffer = ethjsTx.serialize()
57
+
58
+ const baseFee = gasPrice.mul(gasLimit)
59
+ const optimismL1DataFee = asset.baseAsset.estimateL1DataFee
60
+ ? await asset.baseAsset.estimateL1DataFee({
61
+ unsignedTx: { txData: { transactionBuffer, chainId } },
62
+ })
63
+ : undefined
64
+
65
+ const l1DataFee = optimismL1DataFee
66
+ ? asset.baseAsset.currency.baseUnit(optimismL1DataFee)
67
+ : asset.baseAsset.currency.ZERO
68
+
69
+ const fee = baseFee.add(l1DataFee)
70
+
71
+ const unsignedTx = {
72
+ txData: { transactionBuffer, chainId },
73
+ txMeta: {
74
+ bumpTxId,
75
+ eip1559Enabled,
76
+ fromAddress,
77
+ toAddress,
78
+ amount: coinAmount.toDefaultString({ unit: true }),
79
+ fee: fee.toDefaultString({ unit: true }),
80
+ },
81
+ }
82
+ return { unsignedTx }
83
+ }
84
+
85
+ const createBumpUnsignedTx = async ({
86
+ fromAddress,
87
+ chainId,
88
+ asset,
89
+ bumpTxId,
90
+ baseAssetTxLog,
91
+ assetClientInterface,
92
+ walletAccount,
93
+ feeData,
94
+ nonce: providedNonce,
95
+ }) => {
96
+ const baseAsset = asset.baseAsset
97
+ const replacedTx = baseAssetTxLog.get(bumpTxId)
98
+ const assets = await assetClientInterface.getAssetsForNetwork({ baseAssetName: baseAsset.name })
99
+ if (!replacedTx || !replacedTx.pending) {
100
+ throw new Error(`Cannot bump transaction ${bumpTxId}: not found or confirmed`)
101
+ }
102
+
103
+ let replacedTokenTx
104
+ if (replacedTx.tokens.length > 0) {
105
+ const [tokenAssetName] = replacedTx.tokens
106
+ const tokenTxSet = await assetClientInterface.getTxLog({
107
+ assetName: tokenAssetName,
108
+ walletAccount,
109
+ })
110
+ replacedTokenTx = tokenTxSet.get(bumpTxId)
111
+
112
+ if (replacedTokenTx) {
113
+ // Attempt to overwrite the asset to reflect the fact that
114
+ // we're performing a token transaction.
115
+ asset = assets[tokenAssetName]
116
+ if (!asset) {
117
+ throw new Error(
118
+ `unable to find ${tokenAssetName} during token bump transaction: asset was not available in assetsForNetwork`
119
+ )
120
+ }
121
+ }
122
+ }
123
+
124
+ const toAddress = (replacedTokenTx || replacedTx).to
125
+ const isToken = isEthereumLikeToken(asset)
126
+ const txToAddress = isToken ? asset.contract.address : toAddress
127
+ const coinAmount = (replacedTokenTx || replacedTx).coinAmount.negate()
128
+ const gasLimit = replacedTx.data.gasLimit
129
+
130
+ const value = isToken ? baseAsset.currency.ZERO : coinAmount
131
+
132
+ const {
133
+ gasPrice: currentGasPrice,
134
+ baseFeePerGas: currentBaseFee,
135
+ eip1559Enabled,
136
+ tipGasPrice: currentTipGasPrice,
137
+ } = feeData
138
+ const { bumpedGasPrice, bumpedTipGasPrice } = calculateBumpedGasPrice({
139
+ baseAsset,
140
+ tx: replacedTx,
141
+ currentGasPrice,
142
+ currentBaseFee,
143
+ currentTipGasPrice,
144
+ eip1559Enabled,
145
+ })
146
+ const gasPrice = bumpedGasPrice
147
+ const tipGasPrice = bumpedTipGasPrice
148
+ const nonce = replacedTx.data.nonce
149
+ const data = replacedTokenTx
150
+ ? asset.contract.transfer.build(toAddress.toLowerCase(), coinAmount.toBaseString())
151
+ : replacedTx.data.data || '0x'
152
+
153
+ if (nonce === undefined) {
154
+ throw new Error(`Cannot bump transaction ${bumpTxId}: data object seems to be corrupted`)
155
+ }
156
+
157
+ // If we have evaluated a bump transaction and the `providedNonce` differs
158
+ // from the `bumpNonce`, we've encountered a conflict and cannot respect
159
+ // the caller's request.
160
+ if (typeof nonce === 'number' && typeof providedNonce === 'number' && nonce !== providedNonce)
161
+ throw new ErrorWrapper.EthLikeError({
162
+ message: new Error('incorrect nonce for replacement transaction'),
163
+ reason: ErrorWrapper.reasons.bumpTxFailed,
164
+ hint: 'providedNonce',
165
+ })
166
+
167
+ return createUnsignedTxWithFees({
168
+ asset,
169
+ chainId,
170
+ to: txToAddress,
171
+ value,
172
+ data,
173
+ gasLimit,
174
+ gasPrice,
175
+ tipGasPrice,
176
+ nonce,
177
+ bumpTxId,
178
+ coinAmount,
179
+ fromAddress,
180
+ toAddress,
181
+ eip1559Enabled,
182
+ })
183
+ }
184
+
185
+ export const createTxFactory = ({ chainId, assetClientInterface, useAbsoluteNonce }) => {
186
+ assert(assetClientInterface, 'assetClientInterface is required')
187
+ assert(typeof chainId === 'number', 'chainId is required')
188
+ return async ({
189
+ asset,
190
+ walletAccount,
191
+ feeData,
192
+ nft, // when sending nfts
193
+ fromAddress: providedFromAddress, // wallet from address
194
+ toAddress: providedToAddress, // user's to address, not the token or the dex contract
195
+ contractAddress: providedContractAddress, // Provided when swapping a token via the DEX contract, not via the token's contract
196
+ txInput: providedTxInput, // Provided when swapping via a DEX contract
197
+ gasLimit: providedGasLimit, // Provided by exchange when known
198
+ amount: providedAmount, // The NU amount to be sent, to be included in the tx value or tx input
199
+ nonce: providedNonce,
200
+ tipGasPrice: providedTipGasPrice,
201
+ gasPrice: providedGasPrice,
202
+ bip70,
203
+ isExchange,
204
+ customFee,
205
+ isSendAll,
206
+ bumpTxId,
207
+ keepTxInput, // @deprecated this flag is used by swaps when swapping a token via DEX. The asset is token but the tx TO address is not the token address. Update swap to use `contractAddress`
208
+ }) => {
209
+ assert(asset, 'asset is required')
210
+ assert(feeData, 'feeData is required')
211
+ const fromAddress = providedFromAddress || ARBITRARY_ADDRESS
212
+
213
+ const baseAssetTxLog = await assetClientInterface.getTxLog({
214
+ assetName: asset.baseAsset.name,
215
+ walletAccount,
216
+ })
217
+
218
+ if (bumpTxId) {
219
+ return createBumpUnsignedTx({
220
+ chainId,
221
+ asset,
222
+ fromAddress,
223
+ bumpTxId,
224
+ baseAssetTxLog,
225
+ assetClientInterface,
226
+ walletAccount,
227
+ feeData,
228
+ nonce: providedNonce,
229
+ })
230
+ }
231
+
232
+ const toAddress = providedToAddress || ARBITRARY_ADDRESS
233
+ const {
234
+ gasPrice: maybeGasPrice,
235
+ feeData: { tipGasPrice: maybeTipGasPrice, eip1559Enabled },
236
+ } = getFeeFactoryGasPrices({ customFee, feeData })
237
+
238
+ const isToken = isEthereumLikeToken(asset)
239
+
240
+ const resolvedGasPrice = providedGasPrice ?? maybeGasPrice
241
+
242
+ const txToAddress =
243
+ providedContractAddress ?? (isToken && !keepTxInput ? asset.contract.address : toAddress)
244
+
245
+ const isContractToAddress = await isContractAddressCached({ asset, address: txToAddress })
246
+
247
+ // HACK: We cannot ensure the no dust invariant for `isSendAll`
248
+ // transactions to contract addresses, since we may be
249
+ // performing a raw token transaction and the parameter
250
+ // applies to the token and not the native amount.
251
+ //
252
+ // Contracts have nondeterministic gas most of the time
253
+ // versus estimations, anyway.
254
+ const isSendAllBaseAsset = isSendAll && !isToken && !isContractToAddress
255
+
256
+ // For native send all transactions, we have to make sure that
257
+ // the `tipGasPrice` is equal to the `gasPrice`, since this is
258
+ // effectively like saying that the `maxFeePerGas` is equal
259
+ // to the `maxPriorityFeePerGas`. We do this so that for a
260
+ // fixed gas cost transaction, no dust balance should remain,
261
+ // since any deviation in the underlying `baseFeePerGas` will
262
+ // result only affect the tip for the miner - no dust remains.
263
+ const tipGasPrice =
264
+ providedTipGasPrice ??
265
+ (eip1559Enabled && isSendAllBaseAsset ? resolvedGasPrice : maybeTipGasPrice)
266
+
267
+ const gasPrice = eip1559Enabled
268
+ ? ensureSaneEip1559GasPriceForTipGasPrice({
269
+ gasPrice: resolvedGasPrice,
270
+ tipGasPrice,
271
+ })
272
+ : resolvedGasPrice
273
+
274
+ const nonce =
275
+ providedNonce ??
276
+ (await resolveNonce({
277
+ asset,
278
+ fromAddress,
279
+ txLog: baseAssetTxLog,
280
+ // For assets where we'll fall back to querying the coin node, we
281
+ // search for pending transactions. For base assets with history,
282
+ // we'll fall back to the `TxLog` since this also has a knowledge
283
+ // of which transactions are currently in pending.
284
+ tag: 'pending',
285
+ useAbsoluteNonce,
286
+ }))
287
+
288
+ if (nft) {
289
+ const {
290
+ contractAddress: txToAddress,
291
+ gasLimit,
292
+ txInput,
293
+ } = await getNftArguments({
294
+ asset,
295
+ nft,
296
+ fromAddress,
297
+ toAddress: providedToAddress,
298
+ })
299
+
300
+ const value = asset.baseAsset.currency.ZERO
301
+
302
+ return createUnsignedTxWithFees({
303
+ chainId,
304
+ asset,
305
+ to: txToAddress,
306
+ value,
307
+ data: txInput,
308
+ gasLimit,
309
+ gasPrice,
310
+ tipGasPrice,
311
+ nonce,
312
+ coinAmount: value,
313
+ fromAddress,
314
+ toAddress,
315
+ eip1559Enabled,
316
+ })
317
+ }
318
+
319
+ const amount = providedAmount ?? asset.currency.ZERO
320
+
321
+ const value = isToken ? asset.baseAsset.currency.ZERO : amount
322
+ const txInput = providedTxInput || resolveDefaultTxInput({ asset, toAddress, amount })
323
+ const gasLimit =
324
+ providedGasLimit ??
325
+ (await fetchGasLimit({
326
+ asset,
327
+ feeData,
328
+ fromAddress: providedFromAddress,
329
+ toAddress: providedToAddress,
330
+ txInput: providedTxInput,
331
+ contractAddress: txToAddress,
332
+ bip70,
333
+ amount,
334
+ }))
335
+
336
+ return createUnsignedTxWithFees({
337
+ asset,
338
+ chainId,
339
+ to: txToAddress,
340
+ value,
341
+ data: txInput,
342
+ gasLimit,
343
+ gasPrice,
344
+ tipGasPrice,
345
+ nonce,
346
+ coinAmount: amount,
347
+ fromAddress,
348
+ toAddress,
349
+ eip1559Enabled,
350
+ })
351
+ }
352
+ }
@@ -1,5 +1,5 @@
1
1
  import { BaseMonitor } from '@exodus/asset-lib'
2
- import { getAssetAddresses, isRpcBalanceAsset } from '@exodus/ethereum-lib'
2
+ import { getAssetAddresses } from '@exodus/ethereum-lib'
3
3
  import lodash from 'lodash'
4
4
 
5
5
  import { executeEthLikeFeeMonitorUpdate } from '../fee-utils.js'
@@ -16,10 +16,11 @@ import {
16
16
  const { isEmpty } = lodash
17
17
 
18
18
  export class ClarityMonitor extends BaseMonitor {
19
- constructor({ server, config, ...args }) {
19
+ constructor({ server, config, rpcBalanceAssetNames, ...args }) {
20
20
  super(args)
21
21
  this.config = { GAS_PRICE_FROM_WEBSOCKET: true, ...config }
22
22
  this.server = server
23
+ this.rpcBalanceAssetNames = rpcBalanceAssetNames
23
24
  this.getAllLogItemsByAsset = getAllLogItemsByAsset
24
25
  this.deriveDataNeededForTick = getDeriveDataNeededForTick(this.aci)
25
26
  this.deriveTransactionsToCheck = getDeriveTransactionsToCheck({
@@ -201,7 +202,7 @@ export class ClarityMonitor extends BaseMonitor {
201
202
  const asset = this.asset
202
203
  const newAccountState = Object.create(null)
203
204
  const balances = await this.getBalances({ tokens, ourWalletAddress })
204
- if (isRpcBalanceAsset(asset)) {
205
+ if (this.rpcBalanceAssetNames.includes(asset.name)) {
205
206
  const balance = balances[asset.name]
206
207
  newAccountState.balance = asset.currency.baseUnit(balance)
207
208
  }
@@ -244,13 +245,13 @@ export class ClarityMonitor extends BaseMonitor {
244
245
 
245
246
  async getBalances({ tokens, ourWalletAddress }) {
246
247
  const batch = Object.create(null)
247
- if (isRpcBalanceAsset(this.asset)) {
248
+ if (this.rpcBalanceAssetNames.includes(this.asset.name)) {
248
249
  const request = this.server.getBalanceRequest(ourWalletAddress)
249
250
  batch[this.asset.name] = request
250
251
  }
251
252
 
252
253
  for (const token of tokens) {
253
- if (isRpcBalanceAsset(token) && token.contract.address) {
254
+ if (this.rpcBalanceAssetNames.includes(token.name) && token.contract.address) {
254
255
  const request = this.server.balanceOfRequest(ourWalletAddress, token.contract.address)
255
256
  batch[token.name] = request
256
257
  }
@@ -1,5 +1,5 @@
1
1
  import { BaseMonitor } from '@exodus/asset-lib'
2
- import { getAssetAddresses, isRpcBalanceAsset } from '@exodus/ethereum-lib'
2
+ import { getAssetAddresses } from '@exodus/ethereum-lib'
3
3
  import lodash from 'lodash'
4
4
 
5
5
  import { executeEthLikeFeeMonitorUpdate } from '../fee-utils.js'
@@ -25,7 +25,7 @@ const { isEmpty } = lodash
25
25
  // formatting, and populating-to-state all ETH/ETC/ERC20 transactions.
26
26
 
27
27
  export class EthereumMonitor extends BaseMonitor {
28
- constructor({ server, config, webSocketEnabled = true, ...args }) {
28
+ constructor({ server, config, rpcBalanceAssetNames, webSocketEnabled = true, ...args }) {
29
29
  super(args)
30
30
  this.server = server
31
31
  this.config = { GAS_PRICE_FROM_WEBSOCKET: true, ...config }
@@ -38,6 +38,7 @@ export class EthereumMonitor extends BaseMonitor {
38
38
  this.addHook('after-stop', (...args) => this.afterStop(...args))
39
39
  this.subscribedToGasPriceMap = new Map()
40
40
  this._webSocketEnabled = webSocketEnabled
41
+ this.rpcBalanceAssetNames = rpcBalanceAssetNames
41
42
  }
42
43
 
43
44
  setServer(config = {}) {
@@ -215,7 +216,7 @@ export class EthereumMonitor extends BaseMonitor {
215
216
  const asset = this.asset
216
217
  const newAccountState = Object.create(null)
217
218
  const server = this.server
218
- if (isRpcBalanceAsset(asset)) {
219
+ if (this.rpcBalanceAssetNames.includes(asset.name)) {
219
220
  const result = await server.getBalanceProxied(ourWalletAddress)
220
221
  const balance = fromHexToString(result)
221
222
  newAccountState.balance = asset.currency.baseUnit(balance)
@@ -223,7 +224,7 @@ export class EthereumMonitor extends BaseMonitor {
223
224
 
224
225
  const tokenBalancePairs = await Promise.all(
225
226
  tokens
226
- .filter((token) => isRpcBalanceAsset(token) && token.contract.address)
227
+ .filter((token) => this.rpcBalanceAssetNames.includes(token.name) && token.contract.address)
227
228
  .map(async (token) => {
228
229
  const { confirmed } = await server.balanceOf(ourWalletAddress, token.contract.address)
229
230
  const value = token.currency.baseUnit(confirmed[token.contract.address] || 0)
@@ -7,5 +7,8 @@ export default function getFeeAmount(asset, serverTx) {
7
7
  // genesis, coinbase, uncles
8
8
  if (!gasPrice) return asset.currency.ZERO
9
9
 
10
- return asset.currency.baseUnit(gasUsed || gasLimit).mul(gasPrice)
10
+ return asset.currency
11
+ .baseUnit(gasUsed || gasLimit)
12
+ .mul(gasPrice)
13
+ .add(asset.currency.baseUnit(serverTx.extraData?.l1Fee || 0))
11
14
  }
@@ -1,2 +1 @@
1
1
  export { default as txSendFactory } from './tx-send.js'
2
- export { default as getFeeInfo } from './get-fee-info.js'
@@ -5,7 +5,7 @@ export const resolveNonce = async ({
5
5
  forceFromNode,
6
6
  fromAddress,
7
7
  providedNonce,
8
- txLog,
8
+ txLog = [],
9
9
  triedNonce,
10
10
  tag = 'latest', // use 'pending' for unconfirmed txs
11
11
  useAbsoluteNonce,