@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.
@@ -1,239 +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 { gasPrice: currentGasPrice, baseFeePerGas: currentBaseFee } = feeData
162
- const { bumpedGasPrice, bumpedTipGasPrice } = calculateBumpedGasPrice({
163
- baseAsset,
164
- tx: replacedTx,
165
- currentGasPrice,
166
- currentBaseFee,
167
- eip1559Enabled,
168
- })
169
- feeOpts.gasPrice = bumpedGasPrice
170
- feeOpts.tipGasPrice = bumpedTipGasPrice
171
- bumpNonce = replacedTx.data.nonce
172
- txInput = replacedTokenTx ? null : replacedTx.data.data || '0x'
173
- if (bumpNonce === undefined) {
174
- throw new Error(`Cannot bump transaction ${bumpTxId}: data object seems to be corrupted`)
175
- }
176
- }
74
+ // unknown data from buffer...
75
+ const selfSend = unsignedTx.txMeta.fromAddress === to
76
+ const replacedTxId = unsignedTx.txMeta.bumpTxId
177
77
 
178
- // If we have evaluated a bump transaction and the `providedNonce` differs
179
- // from the `bumpNonce`, we've encountered a conflict and cannot respect
180
- // the caller's request.
181
- if (
182
- typeof bumpNonce === 'number' &&
183
- typeof providedNonce === 'number' &&
184
- bumpNonce !== providedNonce
78
+ assert(
79
+ to.toLowerCase() !== ARBITRARY_ADDRESS,
80
+ `The receiving wallet address must not be ${ARBITRARY_ADDRESS}`
185
81
  )
186
- throw new ErrorWrapper.EthLikeError({
187
- message: new Error('incorrect nonce for replacement transaction'),
188
- reason: ErrorWrapper.reasons.bumpTxFailed,
189
- hint: 'providedNonce',
190
- })
191
-
192
- // Choose a nonce that compensates for transctions which are currently
193
- // in pending; for example, when we send transactions at low gas which
194
- // will be stored by `geth` for execution at a later point in time.
195
- //
196
- // When we are not intentionally bumping a transaction, users are
197
- // appending a new transaction to the chain - therefore we should
198
- // be mindful of nonces belonging to us which are currently pending.
199
- const resolvedNonce =
200
- providedNonce ??
201
- bumpNonce ??
202
- (await resolveNonce({
203
- asset,
204
- fromAddress,
205
- txLog: baseAssetTxLog,
206
- // For assets where we'll fall back to querying the coin node, we
207
- // search for pending transactions. For base assets with history,
208
- // we'll fall back to the `TxLog` since this also has a knowledge
209
- // of which transactions are currently in pending.
210
- tag: 'pending',
211
- useAbsoluteNonce: useAbsoluteBalanceAndNonce,
212
- }))
213
82
 
214
- const createTxParams = {
215
- assetClientInterface,
83
+ let { txId, rawTx } = await signTx({
216
84
  asset,
85
+ unsignedTx,
217
86
  walletAccount,
218
- toAddress: contractAddress || address,
219
- amount,
220
- nonce: resolvedNonce,
221
- fromAddress,
222
- customFee,
223
- feeOpts,
224
- txInput,
225
- keepTxInput,
226
- isSendAll,
227
- createUnsignedTx,
228
- feeData,
229
- providedFeeAmount,
230
- }
87
+ })
231
88
 
232
- let { txId, rawTx, nonce, gasLimit, tipGasPrice, feeAmount } = await createTx(createTxParams)
89
+ const isPrivate = Boolean(legacyParams?.options?.isPrivate)
233
90
 
234
- if (isPrivate && !baseAsset.api.features.transactionPrivacy)
91
+ if (isPrivate && typeof baseAsset.broadcastPrivateTx !== 'function')
235
92
  throw new Error(
236
- `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}`
237
94
  )
238
95
 
239
96
  const broadcastTx = isPrivate ? baseAsset.broadcastPrivateTx : baseAsset.api.broadcastTx
@@ -255,7 +112,7 @@ const txSendFactory = ({ assetClientInterface, createUnsignedTx, useAbsoluteBala
255
112
  reason: ErrorWrapper.reasons.insufficientFunds,
256
113
  hint: 'broadcastTx',
257
114
  })
258
- } else if (bumpTxId) {
115
+ } else if (unsignedTx.txMeta.bumpTxId) {
259
116
  throw new ErrorWrapper.EthLikeError({
260
117
  message: err.message,
261
118
  reason: ErrorWrapper.reasons.bumpTxFailed,
@@ -267,18 +124,16 @@ const txSendFactory = ({ assetClientInterface, createUnsignedTx, useAbsoluteBala
267
124
  reason: ErrorWrapper.reasons.broadcastTxFailed,
268
125
  hint: 'otherErr:broadcastTx',
269
126
  })
270
- } else if (nonceTooLowErr && !isHardware) {
127
+ } else if (nonceTooLowErr && !unsignedTx.txMeta.isHardware) {
271
128
  console.info('trying to send again...') // inject logger factory from platform
272
129
  // let's try to fix the nonce issue
273
- nonce = await resolveNonce({
130
+ const newNonce = await resolveNonce({
274
131
  asset,
275
- fromAddress,
276
- providedNonce,
277
- txLog: baseAssetTxLog,
278
132
  triedNonce: nonce,
279
133
  forceFromNode: true,
280
134
  })
281
- ;({ txId, rawTx } = await createTx({ ...createTxParams, nonce }))
135
+ nonce = newNonce
136
+ ;({ txId, rawTx } = await signTx({ asset, unsignedTx, walletAccount }))
282
137
 
283
138
  try {
284
139
  await baseAsset.api.broadcastTx(rawTx.toString('hex'))
@@ -301,7 +156,18 @@ const txSendFactory = ({ assetClientInterface, createUnsignedTx, useAbsoluteBala
301
156
  }
302
157
  }
303
158
 
304
- 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
+ }
305
171
 
306
172
  await assetClientInterface.updateTxLogAndNotify({
307
173
  assetName: asset.name,
@@ -315,21 +181,12 @@ const txSendFactory = ({ assetClientInterface, createUnsignedTx, useAbsoluteBala
315
181
  feeAmount,
316
182
  feeCoinName: asset.feeAsset.name,
317
183
  selfSend,
318
- to: address,
184
+ to,
319
185
  currencies: {
320
186
  [assetName]: asset.currency,
321
187
  [asset.feeAsset.name]: asset.feeAsset.currency,
322
188
  },
323
- data: eip1559Enabled
324
- ? {
325
- gasLimit,
326
- replacedTxId: bumpTxId,
327
- nonce,
328
- ...(tipGasPrice
329
- ? { tipGasPrice: tipGasPrice.toBaseString() }
330
- : Object.create(null)),
331
- }
332
- : { gasLimit, replacedTxId: bumpTxId, nonce },
189
+ data: txData,
333
190
  },
334
191
  ],
335
192
  })
@@ -347,20 +204,13 @@ const txSendFactory = ({ assetClientInterface, createUnsignedTx, useAbsoluteBala
347
204
  feeAmount,
348
205
  feeCoinName: baseAsset.name,
349
206
  selfSend,
350
- to: address,
207
+ to,
351
208
  token: asset.name,
352
209
  currencies: {
353
210
  [baseAsset.name]: baseAsset.currency,
354
211
  [asset.feeAsset.name]: asset.feeAsset.currency,
355
212
  },
356
- data: eip1559Enabled
357
- ? {
358
- gasLimit,
359
- replacedTxId: bumpTxId,
360
- nonce,
361
- ...(tipGasPrice ? { tipGasPrice: tipGasPrice.toBaseString() } : {}),
362
- }
363
- : { gasLimit, replacedTxId: bumpTxId, nonce },
213
+ data: txData,
364
214
  },
365
215
  ],
366
216
  })
@@ -370,146 +220,4 @@ const txSendFactory = ({ assetClientInterface, createUnsignedTx, useAbsoluteBala
370
220
  }
371
221
  }
372
222
 
373
- const createTx = async ({
374
- assetClientInterface,
375
- asset,
376
- walletAccount,
377
- toAddress,
378
- amount,
379
- nonce,
380
- txInput,
381
- keepTxInput = false,
382
- customFee,
383
- isSendAll,
384
- fromAddress,
385
- feeOpts,
386
- createUnsignedTx,
387
- feeData,
388
- providedFeeAmount,
389
- }) => {
390
- assert(
391
- nonce !== undefined && typeof nonce === 'number',
392
- 'Nonce must be provided when creating a tx'
393
- )
394
- const isToken = isEthereumLikeToken(asset)
395
-
396
- if (txInput && isToken && !keepTxInput)
397
- throw new Error(`Additional data for Ethereum Token (${asset.name}) is not allowed`)
398
-
399
- txInput =
400
- isToken && !keepTxInput
401
- ? asset.contract.transfer.build(toAddress.toLowerCase(), amount.toBaseString())
402
- : txInput
403
-
404
- let { gasLimit, gasPrice, tipGasPrice, eip1559Enabled } = await getFeeInfo({
405
- assetClientInterface,
406
- asset,
407
- fromAddress,
408
- toAddress,
409
- amount,
410
- txInput,
411
- feeOpts,
412
- feeData,
413
- customFee,
414
- })
415
-
416
- const isContractToAddress = await isContractAddressCached({ asset, address: toAddress })
417
-
418
- // HACK: We cannot ensure the no dust invariant for `isSendAll`
419
- // transactions to contract addresses, since we may be
420
- // performing a raw token transaction and the parameter
421
- // applies to the token and not the native amount.
422
- //
423
- // Contracts have nondeterministic gas most of the time
424
- // versus estimations, anyway.
425
- const isSendAllBaseAsset = isSendAll && !isToken && !isContractToAddress
426
-
427
- // For native send all transactions, we have to make sure that
428
- // the `tipGasPrice` is equal to the `gasPrice`, since this is
429
- // effectively like saying that the `maxFeePerGas` is equal
430
- // to the `maxPriorityFeePerGas`. We do this so that for a
431
- // fixed gas cost transaction, no dust balance should remain,
432
- // since any deviation in the underlying `baseFeePerGas` will
433
- // result only affect the tip for the miner - no dust remains.
434
- if (eip1559Enabled && isSendAllBaseAsset) {
435
- // force consuming all gas
436
- tipGasPrice = gasPrice
437
- }
438
-
439
- // HACK: If we are handling a send all transaction, we must ensure
440
- // the send all invariant is maintained before producing the
441
- // final transaction.
442
- const maybeOverrideSendAllAmount =
443
- isSendAllBaseAsset &&
444
- (await HACK_maybeRefineSendAllAmount({
445
- amount,
446
- asset,
447
- assetClientInterface,
448
- walletAccount,
449
- gasLimit,
450
- gasPrice,
451
- }))
452
-
453
- if (maybeOverrideSendAllAmount) {
454
- console.log(
455
- `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 })}.`
456
- )
457
- amount = maybeOverrideSendAllAmount
458
- }
459
-
460
- const unsignedTx = await createUnsignedTx({
461
- asset,
462
- walletAccount,
463
- address: toAddress,
464
- amount,
465
- nonce,
466
- txInput,
467
- gasLimit,
468
- gasPrice,
469
- tipGasPrice,
470
- fromAddress,
471
- eip1559Enabled,
472
- })
473
-
474
- // TODO: move into createUnsignedTx()
475
- if (keepTxInput && !isToken) {
476
- unsignedTx.txData.to = toAddress
477
- }
478
-
479
- unsignedTx.txMeta.eip1559Enabled = eip1559Enabled
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
-
498
- const { txId, rawTx } = await assetClientInterface.signTransaction({
499
- assetName: asset.baseAsset.name,
500
- unsignedTx,
501
- walletAccount,
502
- })
503
-
504
- return {
505
- txId: normalizeTxId(txId),
506
- rawTx,
507
- nonce,
508
- gasLimit,
509
- gasPrice,
510
- tipGasPrice,
511
- feeAmount: await resolveFee(),
512
- }
513
- }
514
-
515
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