@exodus/ethereum-api 8.40.1 → 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.
@@ -1,244 +1,96 @@
1
- /* eslint-disable @exodus/export-default/last */
2
-
3
- import { calculateBumpedGasPrice, isEthereumLikeToken, normalizeTxId } from '@exodus/ethereum-lib'
1
+ import { isEthereumLikeToken, normalizeTxId, parseUnsignedTx } from '@exodus/ethereum-lib'
4
2
  import assert from 'minimalistic-assert'
5
3
 
6
4
  import * as ErrorWrapper from '../error-wrapper.js'
7
- import { isContractAddressCached, transactionExists } from '../eth-like-util.js'
8
- import { getNftArguments } from '../nft-utils.js'
9
- import getFeeInfo from './get-fee-info.js'
5
+ import { transactionExists } from '../eth-like-util.js'
6
+ import { ARBITRARY_ADDRESS } from '../gas-estimation.js'
10
7
  import { resolveNonce } from './nonce-utils.js'
11
8
 
12
- // Exodus enforces a strict invariant that `sendAll` transactions
13
- // must not leave any dust in the sender's account. Currently, the
14
- // assets library has the expectation that the client frontend
15
- // should calculate the precise amount to send, but due to the
16
- // sheer amount of variables involved when resolving a `gasPrice`,
17
- // this is a significant undertaking.
18
- //
19
- // Therefore, although clients try their very best to calculate
20
- // the correct amount, in cases this fails we can fall back to
21
- // the implementation defined here with a warning.
22
- // eslint-disable-next-line camelcase
23
- export const HACK_maybeRefineSendAllAmount = async ({
24
- amount: providedAmount,
25
- asset,
26
- assetClientInterface,
27
- walletAccount,
28
- gasLimit,
29
- gasPrice,
30
- }) => {
31
- try {
32
- const { name: assetName, estimateL1DataFee } = asset
33
-
34
- // HACK: For the interim, we won't attempt to
35
- // reconcile transaction dust on L2s due
36
- // to the nondeterminism about the calldata
37
- // fee.
38
- if (typeof estimateL1DataFee === 'function') return null
39
-
40
- const [txLog, accountState] = await Promise.all([
41
- assetClientInterface.getTxLog({
42
- assetName,
43
- walletAccount,
44
- }),
45
- assetClientInterface.getAccountState({
46
- assetName,
47
- walletAccount,
48
- }),
49
- ])
50
-
51
- const { spendable } = await asset.api.getBalances({ asset, txLog, accountState })
52
- const maxGasCost = gasPrice.mul(gasLimit)
53
-
54
- if (maxGasCost.gt(spendable)) throw new Error('transaction gas cost exceeds spendable balance')
55
-
56
- const expectedSendAllAmount = spendable.sub(maxGasCost)
9
+ const txSendFactory = ({ assetClientInterface, createTx }) => {
10
+ assert(assetClientInterface, 'assetClientInterface is required')
11
+ assert(createTx, 'createTx is required')
57
12
 
58
- // If the client attempted to send the correct
59
- // amount, good job! You get a cookie!
60
- if (providedAmount.equals(expectedSendAllAmount)) return null
13
+ async function signTx({ asset, unsignedTx, walletAccount }) {
14
+ const { rawTx, txId } = await assetClientInterface.signTransaction({
15
+ assetName: asset.baseAsset.name,
16
+ unsignedTx,
17
+ walletAccount,
18
+ })
61
19
 
62
- // The client attempted to `sendAll` using the incorrect amount.
63
- return expectedSendAllAmount
64
- } catch (e) {
65
- console.error('failed to refine send all amount', e)
66
- return null
20
+ return { rawTx, txId: normalizeTxId(txId) }
67
21
  }
68
- }
69
-
70
- const txSendFactory = ({ assetClientInterface, createUnsignedTx, useAbsoluteBalanceAndNonce }) => {
71
- assert(assetClientInterface, 'assetClientInterface is required')
72
- assert(createUnsignedTx, 'createUnsignedTx is required')
73
- return async ({ asset, walletAccount, address, amount, feeData: maybeFeeData, options = {} }) => {
74
- const {
75
- nft,
76
- bumpTxId,
77
- nonce: providedNonce,
78
- customFee,
79
- keepTxInput,
80
- isSendAll,
81
- isHardware,
82
- isPrivate,
83
- } = options
84
- let { txInput, feeAmount: providedFeeAmount } = options // avoid let!
85
-
86
- const feeOpts = {
87
- gasPrice: options.gasPrice,
88
- tipGasPrice: options.tipGasPrice,
89
- gasLimit: options.gasLimit,
90
- }
91
22
 
23
+ return async ({ asset, walletAccount, unsignedTx: providedUnsignedTx, ...legacyParams }) => {
92
24
  const assetName = asset.name
93
25
  const baseAsset = asset.baseAsset
94
26
 
95
- const assets = await assetClientInterface.getAssetsForNetwork({ baseAssetName: baseAsset.name })
96
-
97
- const feeData =
98
- maybeFeeData ||
99
- (await assetClientInterface.getFeeData({
100
- assetName: baseAsset.name,
101
- }))
27
+ const resolveUnsignedTx = async () => {
28
+ if (providedUnsignedTx) {
29
+ return { unsignedTx: providedUnsignedTx }
30
+ }
102
31
 
103
- const { eip1559Enabled } = feeData
32
+ const feeData =
33
+ legacyParams.feeData ??
34
+ (await assetClientInterface.getFeeData({
35
+ assetName: baseAsset.name,
36
+ }))
104
37
 
105
- const fromAddress = await assetClientInterface.getReceiveAddress({
106
- assetName: baseAsset.name,
107
- walletAccount,
108
- })
38
+ const fromAddress =
39
+ legacyParams.fromAddress ??
40
+ (await assetClientInterface.getReceiveAddress({
41
+ assetName: baseAsset.name,
42
+ walletAccount,
43
+ }))
109
44
 
110
- let contractAddress
111
- if (nft) {
112
- const nftArguments = await getNftArguments({ asset, nft, fromAddress, toAddress: address })
113
- contractAddress = nftArguments.contractAddress
114
- feeOpts.gasLimit = nftArguments.gasLimit
115
- txInput = nftArguments.txInput
116
- amount = asset.baseAsset.currency.ZERO
45
+ return createTx({
46
+ asset,
47
+ walletAccount,
48
+ feeData,
49
+ fromAddress,
50
+ toAddress: legacyParams.address,
51
+ ...legacyParams,
52
+ ...legacyParams.options,
53
+ })
117
54
  }
118
55
 
119
- let bumpNonce
56
+ const { unsignedTx } = await resolveUnsignedTx()
120
57
 
121
- const baseAssetTxLog = await assetClientInterface.getTxLog({
122
- assetName: baseAsset.name,
123
- walletAccount,
124
- })
58
+ // this converts an transactionBuffer to values we can use when creating the tx logs
59
+ const parsedTx = parseUnsignedTx({ asset, unsignedTx })
125
60
 
126
- // `replacedTx` is always an ETH/ETC transaction (not a token)
127
- let replacedTx, replacedTokenTx
128
- if (bumpTxId) {
129
- replacedTx = baseAssetTxLog.get(bumpTxId)
61
+ // the txMeta.fee may include implicit l1 fees
62
+ const feeAmount = unsignedTx.txMeta.fee
63
+ ? asset.feeAsset.currency.parse(unsignedTx.txMeta.fee)
64
+ : parsedTx.fee
130
65
 
131
- if (!replacedTx || !replacedTx.pending) {
132
- throw new Error(`Cannot bump transaction ${bumpTxId}: not found or confirmed`)
133
- }
66
+ let nonce = parsedTx.nonce
134
67
 
135
- if (replacedTx.tokens.length > 0) {
136
- const [tokenAssetName] = replacedTx.tokens
137
- const tokenTxSet = await assetClientInterface.getTxLog({
138
- assetName: tokenAssetName,
139
- walletAccount,
140
- })
141
- replacedTokenTx = tokenTxSet.get(bumpTxId)
142
-
143
- if (replacedTokenTx) {
144
- // Attempt to overwrite the asset to reflect the fact that
145
- // we're performing a token transaction.
146
- asset = assets[tokenAssetName]
147
- if (!asset) {
148
- console.warn(
149
- `unable to find ${tokenAssetName} during token bump transaction: asset was not available in assetsForNetwork`
150
- )
151
- }
152
- }
68
+ const tipGasPrice = parsedTx.tipGasPrice
69
+ const gasLimit = parsedTx.gasLimit
70
+ const amount = parsedTx.amount
71
+ const to = parsedTx.to
72
+ const eip1559Enabled = parsedTx.eip1559Enabled
153
73
 
154
- // TODO: Should we `throw` if we can't find the asset?
155
- }
156
-
157
- address = (replacedTokenTx || replacedTx).to
158
- amount = (replacedTokenTx || replacedTx).coinAmount.negate()
159
- feeOpts.gasLimit = replacedTx.data.gasLimit
160
-
161
- const {
162
- gasPrice: currentGasPrice,
163
- baseFeePerGas: currentBaseFee,
164
- tipGasPrice: currentTipGasPrice,
165
- } = feeData
166
- const { bumpedGasPrice, bumpedTipGasPrice } = calculateBumpedGasPrice({
167
- baseAsset,
168
- tx: replacedTx,
169
- currentGasPrice,
170
- currentBaseFee,
171
- currentTipGasPrice,
172
- eip1559Enabled,
173
- })
174
- feeOpts.gasPrice = bumpedGasPrice
175
- feeOpts.tipGasPrice = bumpedTipGasPrice
176
- bumpNonce = replacedTx.data.nonce
177
- txInput = replacedTokenTx ? null : replacedTx.data.data || '0x'
178
- if (bumpNonce === undefined) {
179
- throw new Error(`Cannot bump transaction ${bumpTxId}: data object seems to be corrupted`)
180
- }
181
- }
74
+ // unknown data from buffer...
75
+ const selfSend = unsignedTx.txMeta.fromAddress === to
76
+ const replacedTxId = unsignedTx.txMeta.bumpTxId
182
77
 
183
- // If we have evaluated a bump transaction and the `providedNonce` differs
184
- // from the `bumpNonce`, we've encountered a conflict and cannot respect
185
- // the caller's request.
186
- if (
187
- typeof bumpNonce === 'number' &&
188
- typeof providedNonce === 'number' &&
189
- bumpNonce !== providedNonce
78
+ assert(
79
+ to.toLowerCase() !== ARBITRARY_ADDRESS,
80
+ `The receiving wallet address must not be ${ARBITRARY_ADDRESS}`
190
81
  )
191
- throw new ErrorWrapper.EthLikeError({
192
- message: new Error('incorrect nonce for replacement transaction'),
193
- reason: ErrorWrapper.reasons.bumpTxFailed,
194
- hint: 'providedNonce',
195
- })
196
-
197
- // Choose a nonce that compensates for transctions which are currently
198
- // in pending; for example, when we send transactions at low gas which
199
- // will be stored by `geth` for execution at a later point in time.
200
- //
201
- // When we are not intentionally bumping a transaction, users are
202
- // appending a new transaction to the chain - therefore we should
203
- // be mindful of nonces belonging to us which are currently pending.
204
- const resolvedNonce =
205
- providedNonce ??
206
- bumpNonce ??
207
- (await resolveNonce({
208
- asset,
209
- fromAddress,
210
- txLog: baseAssetTxLog,
211
- // For assets where we'll fall back to querying the coin node, we
212
- // search for pending transactions. For base assets with history,
213
- // we'll fall back to the `TxLog` since this also has a knowledge
214
- // of which transactions are currently in pending.
215
- tag: 'pending',
216
- useAbsoluteNonce: useAbsoluteBalanceAndNonce,
217
- }))
218
82
 
219
- const createTxParams = {
220
- assetClientInterface,
83
+ let { txId, rawTx } = await signTx({
221
84
  asset,
85
+ unsignedTx,
222
86
  walletAccount,
223
- toAddress: contractAddress || address,
224
- amount,
225
- nonce: resolvedNonce,
226
- fromAddress,
227
- customFee,
228
- feeOpts,
229
- txInput,
230
- keepTxInput,
231
- isSendAll,
232
- createUnsignedTx,
233
- feeData,
234
- providedFeeAmount,
235
- }
87
+ })
236
88
 
237
- let { txId, rawTx, nonce, gasLimit, tipGasPrice, feeAmount } = await createTx(createTxParams)
89
+ const isPrivate = Boolean(legacyParams?.options?.isPrivate)
238
90
 
239
- if (isPrivate && !baseAsset.api.features.transactionPrivacy)
91
+ if (isPrivate && typeof baseAsset.broadcastPrivateTx !== 'function')
240
92
  throw new Error(
241
- `unable to send private transaction - transactionPrivacy is not enabled for ${baseAsset.name}`
93
+ `unable to send private transaction - private mempools are not enabled for ${baseAsset.name}`
242
94
  )
243
95
 
244
96
  const broadcastTx = isPrivate ? baseAsset.broadcastPrivateTx : baseAsset.api.broadcastTx
@@ -260,7 +112,7 @@ const txSendFactory = ({ assetClientInterface, createUnsignedTx, useAbsoluteBala
260
112
  reason: ErrorWrapper.reasons.insufficientFunds,
261
113
  hint: 'broadcastTx',
262
114
  })
263
- } else if (bumpTxId) {
115
+ } else if (unsignedTx.txMeta.bumpTxId) {
264
116
  throw new ErrorWrapper.EthLikeError({
265
117
  message: err.message,
266
118
  reason: ErrorWrapper.reasons.bumpTxFailed,
@@ -272,18 +124,16 @@ const txSendFactory = ({ assetClientInterface, createUnsignedTx, useAbsoluteBala
272
124
  reason: ErrorWrapper.reasons.broadcastTxFailed,
273
125
  hint: 'otherErr:broadcastTx',
274
126
  })
275
- } else if (nonceTooLowErr && !isHardware) {
127
+ } else if (nonceTooLowErr && !unsignedTx.txMeta.isHardware) {
276
128
  console.info('trying to send again...') // inject logger factory from platform
277
129
  // let's try to fix the nonce issue
278
- nonce = await resolveNonce({
130
+ const newNonce = await resolveNonce({
279
131
  asset,
280
- fromAddress,
281
- providedNonce,
282
- txLog: baseAssetTxLog,
283
132
  triedNonce: nonce,
284
133
  forceFromNode: true,
285
134
  })
286
- ;({ txId, rawTx } = await createTx({ ...createTxParams, nonce }))
135
+ nonce = newNonce
136
+ ;({ txId, rawTx } = await signTx({ asset, unsignedTx, walletAccount }))
287
137
 
288
138
  try {
289
139
  await baseAsset.api.broadcastTx(rawTx.toString('hex'))
@@ -306,7 +156,18 @@ const txSendFactory = ({ assetClientInterface, createUnsignedTx, useAbsoluteBala
306
156
  }
307
157
  }
308
158
 
309
- const selfSend = fromAddress === address
159
+ const txData = eip1559Enabled
160
+ ? {
161
+ gasLimit,
162
+ replacedTxId,
163
+ nonce,
164
+ ...(tipGasPrice ? { tipGasPrice: tipGasPrice.toBaseString() } : Object.create(null)),
165
+ }
166
+ : {
167
+ gasLimit,
168
+ replacedTxId,
169
+ nonce,
170
+ }
310
171
 
311
172
  await assetClientInterface.updateTxLogAndNotify({
312
173
  assetName: asset.name,
@@ -320,21 +181,12 @@ const txSendFactory = ({ assetClientInterface, createUnsignedTx, useAbsoluteBala
320
181
  feeAmount,
321
182
  feeCoinName: asset.feeAsset.name,
322
183
  selfSend,
323
- to: address,
184
+ to,
324
185
  currencies: {
325
186
  [assetName]: asset.currency,
326
187
  [asset.feeAsset.name]: asset.feeAsset.currency,
327
188
  },
328
- data: eip1559Enabled
329
- ? {
330
- gasLimit,
331
- replacedTxId: bumpTxId,
332
- nonce,
333
- ...(tipGasPrice
334
- ? { tipGasPrice: tipGasPrice.toBaseString() }
335
- : Object.create(null)),
336
- }
337
- : { gasLimit, replacedTxId: bumpTxId, nonce },
189
+ data: txData,
338
190
  },
339
191
  ],
340
192
  })
@@ -352,20 +204,13 @@ const txSendFactory = ({ assetClientInterface, createUnsignedTx, useAbsoluteBala
352
204
  feeAmount,
353
205
  feeCoinName: baseAsset.name,
354
206
  selfSend,
355
- to: address,
207
+ to,
356
208
  token: asset.name,
357
209
  currencies: {
358
210
  [baseAsset.name]: baseAsset.currency,
359
211
  [asset.feeAsset.name]: asset.feeAsset.currency,
360
212
  },
361
- data: eip1559Enabled
362
- ? {
363
- gasLimit,
364
- replacedTxId: bumpTxId,
365
- nonce,
366
- ...(tipGasPrice ? { tipGasPrice: tipGasPrice.toBaseString() } : {}),
367
- }
368
- : { gasLimit, replacedTxId: bumpTxId, nonce },
213
+ data: txData,
369
214
  },
370
215
  ],
371
216
  })
@@ -375,146 +220,4 @@ const txSendFactory = ({ assetClientInterface, createUnsignedTx, useAbsoluteBala
375
220
  }
376
221
  }
377
222
 
378
- const createTx = async ({
379
- assetClientInterface,
380
- asset,
381
- walletAccount,
382
- toAddress,
383
- amount,
384
- nonce,
385
- txInput,
386
- keepTxInput = false,
387
- customFee,
388
- isSendAll,
389
- fromAddress,
390
- feeOpts,
391
- createUnsignedTx,
392
- feeData,
393
- providedFeeAmount,
394
- }) => {
395
- assert(
396
- nonce !== undefined && typeof nonce === 'number',
397
- 'Nonce must be provided when creating a tx'
398
- )
399
- const isToken = isEthereumLikeToken(asset)
400
-
401
- if (txInput && isToken && !keepTxInput)
402
- throw new Error(`Additional data for Ethereum Token (${asset.name}) is not allowed`)
403
-
404
- txInput =
405
- isToken && !keepTxInput
406
- ? asset.contract.transfer.build(toAddress.toLowerCase(), amount.toBaseString())
407
- : txInput
408
-
409
- let { gasLimit, gasPrice, tipGasPrice, eip1559Enabled } = await getFeeInfo({
410
- assetClientInterface,
411
- asset,
412
- fromAddress,
413
- toAddress,
414
- amount,
415
- txInput,
416
- feeOpts,
417
- feeData,
418
- customFee,
419
- })
420
-
421
- const isContractToAddress = await isContractAddressCached({ asset, address: toAddress })
422
-
423
- // HACK: We cannot ensure the no dust invariant for `isSendAll`
424
- // transactions to contract addresses, since we may be
425
- // performing a raw token transaction and the parameter
426
- // applies to the token and not the native amount.
427
- //
428
- // Contracts have nondeterministic gas most of the time
429
- // versus estimations, anyway.
430
- const isSendAllBaseAsset = isSendAll && !isToken && !isContractToAddress
431
-
432
- // For native send all transactions, we have to make sure that
433
- // the `tipGasPrice` is equal to the `gasPrice`, since this is
434
- // effectively like saying that the `maxFeePerGas` is equal
435
- // to the `maxPriorityFeePerGas`. We do this so that for a
436
- // fixed gas cost transaction, no dust balance should remain,
437
- // since any deviation in the underlying `baseFeePerGas` will
438
- // result only affect the tip for the miner - no dust remains.
439
- if (eip1559Enabled && isSendAllBaseAsset) {
440
- // force consuming all gas
441
- tipGasPrice = gasPrice
442
- }
443
-
444
- // HACK: If we are handling a send all transaction, we must ensure
445
- // the send all invariant is maintained before producing the
446
- // final transaction.
447
- const maybeOverrideSendAllAmount =
448
- isSendAllBaseAsset &&
449
- (await HACK_maybeRefineSendAllAmount({
450
- amount,
451
- asset,
452
- assetClientInterface,
453
- walletAccount,
454
- gasLimit,
455
- gasPrice,
456
- }))
457
-
458
- if (maybeOverrideSendAllAmount) {
459
- console.log(
460
- `Attempted to execute a sendAll transaction with an amount of ${amount.toDefaultString({ unit: true })}, but this would fail to maintain the no dust invariant! Overriding with ${maybeOverrideSendAllAmount.toDefaultString({ unit: true })}.`
461
- )
462
- amount = maybeOverrideSendAllAmount
463
- }
464
-
465
- const unsignedTx = await createUnsignedTx({
466
- asset,
467
- walletAccount,
468
- address: toAddress,
469
- amount,
470
- nonce,
471
- txInput,
472
- gasLimit,
473
- gasPrice,
474
- tipGasPrice,
475
- fromAddress,
476
- eip1559Enabled,
477
- })
478
-
479
- // TODO: move into createUnsignedTx()
480
- if (keepTxInput && !isToken) {
481
- unsignedTx.txData.to = toAddress
482
- }
483
-
484
- unsignedTx.txMeta.eip1559Enabled = eip1559Enabled
485
-
486
- const resolveFee = async () => {
487
- if (providedFeeAmount) {
488
- return providedFeeAmount
489
- }
490
-
491
- const optimismL1DataFee = asset.baseAsset.estimateL1DataFee
492
- ? await asset.baseAsset.estimateL1DataFee({
493
- unsignedTx,
494
- })
495
- : undefined
496
-
497
- const l1DataFee = optimismL1DataFee
498
- ? asset.baseAsset.currency.baseUnit(optimismL1DataFee)
499
- : asset.baseAsset.currency.ZERO
500
- return gasPrice.mul(gasLimit).add(l1DataFee)
501
- }
502
-
503
- const { txId, rawTx } = await assetClientInterface.signTransaction({
504
- assetName: asset.baseAsset.name,
505
- unsignedTx,
506
- walletAccount,
507
- })
508
-
509
- return {
510
- txId: normalizeTxId(txId),
511
- rawTx,
512
- nonce,
513
- gasLimit,
514
- gasPrice,
515
- tipGasPrice,
516
- feeAmount: await resolveFee(),
517
- }
518
- }
519
-
520
223
  export default txSendFactory
@@ -1,58 +0,0 @@
1
- import { ensureSaneEip1559GasPriceForTipGasPrice } from '../fee-utils.js'
2
- import { fetchGasLimit } from '../gas-estimation.js'
3
- import { getFeeFactoryGasPrices } from '../get-fee.js'
4
-
5
- const getFeeInfo = async function getFeeInfo({
6
- assetClientInterface,
7
- asset,
8
- fromAddress,
9
- toAddress,
10
- amount,
11
- txInput,
12
- feeOpts = {},
13
- feeData,
14
- customFee,
15
- }) {
16
- // HACK: Previously, calls `getFeeInfo` were not provided a reference
17
- // to `feeData`. For backwards compatibility, we'll revert to
18
- // legacy behaviour.
19
- // NOTE: This shouldn't actually be used outside of the `assets` repo;
20
- // this is done just for safety.
21
- if (!feeData) {
22
- console.warn('`getFeeInfo` was not explicitly passed a `feeData` object.')
23
- const { name: assetName } = asset
24
- feeData = await assetClientInterface.getFeeData({ assetName })
25
- }
26
-
27
- const {
28
- gasPrice: gasPrice_,
29
- feeData: { tipGasPrice: tipGasPrice_, eip1559Enabled },
30
- } = getFeeFactoryGasPrices({ customFee, feeData })
31
-
32
- const tipGasPrice = feeOpts.tipGasPrice || tipGasPrice_
33
-
34
- const maybeGasPrice = feeOpts.gasPrice || gasPrice_
35
-
36
- // HACK: If we've received an invalid combination of `tipGasPrice`
37
- // (maxPriorityFeePerGas) and `gasPrice` (maxFeePerGas), then
38
- // we must normalize these before returning.
39
- const gasPrice = eip1559Enabled
40
- ? ensureSaneEip1559GasPriceForTipGasPrice({ gasPrice: maybeGasPrice, tipGasPrice })
41
- : maybeGasPrice
42
-
43
- const gasLimit =
44
- feeOpts.gasLimit ||
45
- (await fetchGasLimit({
46
- asset,
47
- fromAddress,
48
- toAddress,
49
- amount,
50
- txInput,
51
- feeData,
52
- throwOnError: false,
53
- }))
54
-
55
- return { gasPrice, gasLimit, tipGasPrice, eip1559Enabled }
56
- }
57
-
58
- export default getFeeInfo