@exodus/ethereum-api 9.0.0-alpha.2 → 9.0.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.
Files changed (143) hide show
  1. package/CHANGELOG.md +2121 -0
  2. package/LICENSE +7 -0
  3. package/README.md +124 -0
  4. package/package.json +34 -33
  5. package/src/address-has-history.js +6 -2
  6. package/src/allowance/constants.js +4 -5
  7. package/src/allowance/index.js +159 -40
  8. package/src/check-tx/create-assess-transaction.js +23 -0
  9. package/src/check-tx/create-check-tx.js +104 -0
  10. package/src/check-tx/index.js +1 -0
  11. package/src/create-asset-plugin-factory.js +115 -0
  12. package/src/create-asset-utils.js +520 -0
  13. package/src/create-asset.js +272 -97
  14. package/src/create-token-factory.js +47 -17
  15. package/src/custom-fees.js +57 -0
  16. package/src/eip7702-whitelist.js +62 -0
  17. package/src/ens/index.js +12 -17
  18. package/src/error-wrapper.js +383 -0
  19. package/src/eth-like-util.js +172 -33
  20. package/src/exodus-eth-server/api-coin-nodes.js +27 -196
  21. package/src/exodus-eth-server/api.js +23 -14
  22. package/src/exodus-eth-server/clarity-v2.js +255 -0
  23. package/src/exodus-eth-server/clarity.js +67 -265
  24. package/src/exodus-eth-server/errors.js +40 -0
  25. package/src/exodus-eth-server/eth-like-server.js +385 -0
  26. package/src/exodus-eth-server/fetch-json.js +48 -0
  27. package/src/exodus-eth-server/index.js +77 -0
  28. package/src/exodus-eth-server/utils.js +31 -0
  29. package/src/exodus-eth-server/ws-gateway.js +306 -0
  30. package/src/exodus-eth-server/ws.js +1 -2
  31. package/src/fee-data/EthLikeFeeData.js +81 -0
  32. package/src/fee-data/index.js +54 -0
  33. package/src/fee-utils.js +229 -0
  34. package/src/gas-estimation.js +183 -66
  35. package/src/get-balance-for-address.js +1 -1
  36. package/src/get-balances.js +285 -27
  37. package/src/get-fee.js +92 -56
  38. package/src/get-historical-balance.js +58 -0
  39. package/src/hooks/index.js +1 -1
  40. package/src/hooks/monitor.js +182 -43
  41. package/src/hooks/staking-freshness.js +58 -0
  42. package/src/index.js +118 -12
  43. package/src/move-funds.js +153 -0
  44. package/src/multicall3/index.js +212 -0
  45. package/src/multisig-address.js +146 -0
  46. package/src/nft-utils.js +60 -0
  47. package/src/number-utils.js +34 -2
  48. package/src/optimism-gas/index.js +13 -5
  49. package/src/send-validations.js +58 -0
  50. package/src/server-based-fee-monitor.js +12 -19
  51. package/src/simulate-tx/fetch-tx-preview.js +0 -2
  52. package/src/simulate-tx/index.js +2 -2
  53. package/src/simulate-tx/simulate-eth-tx.js +24 -11
  54. package/src/simulation/common.js +34 -0
  55. package/src/simulation/create-simulate-message.js +49 -0
  56. package/src/simulation/create-simulate-transactions.js +108 -0
  57. package/src/simulation/estimate-fee.js +14 -0
  58. package/src/simulation/estimate-simple-transfer.js +15 -0
  59. package/src/simulation/get-message-type.js +18 -0
  60. package/src/simulation/simulate-message-api.js +68 -0
  61. package/src/simulation/simulate-transactions-api.js +266 -0
  62. package/src/simulation/simulate-transactions.js +16 -0
  63. package/src/simulation/transactions.js +52 -0
  64. package/src/simulation/try-estimating-changes-locally.js +26 -0
  65. package/src/staking/api/index.js +43 -0
  66. package/src/staking/cached-read.js +33 -0
  67. package/src/staking/ethereum/api.js +166 -34
  68. package/src/staking/ethereum/deps.js +37 -0
  69. package/src/staking/ethereum/everstake.js +113 -0
  70. package/src/staking/ethereum/index.js +4 -3
  71. package/src/staking/ethereum/service.js +682 -181
  72. package/src/staking/ethereum/staking-utils.js +275 -4
  73. package/src/staking/fantom-staking.js +9 -9
  74. package/src/staking/index.js +4 -4
  75. package/src/staking/matic/addresses.js +2 -0
  76. package/src/staking/matic/api.js +69 -49
  77. package/src/staking/matic/index.js +3 -3
  78. package/src/staking/matic/matic-staking-utils.js +243 -5
  79. package/src/staking/matic/matic-staking.md +53 -0
  80. package/src/staking/staking-provider-client.js +1 -1
  81. package/src/staking/utils/index.js +39 -0
  82. package/src/tx-create.js +666 -0
  83. package/src/tx-log/clarity-monitor.js +273 -103
  84. package/src/tx-log/clarity-truncated-history-monitor.js +109 -0
  85. package/src/tx-log/clarity-truncated-history-monitor.md +219 -0
  86. package/src/tx-log/clarity-utils/absolute.js +96 -0
  87. package/src/tx-log/clarity-utils/get-log-items-from-server-tx.js +94 -31
  88. package/src/tx-log/clarity-utils/get-names-of-tokens-transferred-by-server-tx.js +3 -2
  89. package/src/tx-log/clarity-utils/index.js +9 -3
  90. package/src/tx-log/clarity-utils/is-spam-pending-tx.js +13 -0
  91. package/src/tx-log/clarity-utils/normalize-transactions-response.js +65 -0
  92. package/src/tx-log/ethereum-monitor.js +93 -47
  93. package/src/tx-log/ethereum-no-history-monitor.js +52 -41
  94. package/src/tx-log/get-optimistic-txlog-effects.js +379 -0
  95. package/src/tx-log/index.js +5 -3
  96. package/src/tx-log/monitor-utils/check-pending-transactions.js +14 -5
  97. package/src/tx-log/monitor-utils/exclude-unchanged-token-balances.js +1 -1
  98. package/src/tx-log/monitor-utils/extract-balance-from-clarity-account-info.js +108 -0
  99. package/src/tx-log/monitor-utils/get-balance-updates.js +44 -0
  100. package/src/tx-log/monitor-utils/get-batched-rpc-balances.js +47 -0
  101. package/src/tx-log/monitor-utils/get-current-blacklist-status.js +38 -0
  102. package/src/tx-log/monitor-utils/get-current-eip7702-delegation.js +79 -0
  103. package/src/tx-log/monitor-utils/get-derive-transactions-to-check.js +10 -5
  104. package/src/tx-log/monitor-utils/get-fee-amount.js +1 -1
  105. package/src/tx-log/monitor-utils/get-history-from-server.js +1 -1
  106. package/src/tx-log/monitor-utils/get-log-items-from-server-tx.js +75 -33
  107. package/src/tx-log/monitor-utils/get-names-of-tokens-transferred-by-server-tx.js +3 -2
  108. package/src/tx-log/monitor-utils/get-raw-transfer-amount.js +29 -0
  109. package/src/tx-log/monitor-utils/get-transfers-by-token-name.js +3 -2
  110. package/src/tx-log/monitor-utils/get-value-of-transfers.js +10 -12
  111. package/src/tx-log/monitor-utils/index.js +12 -7
  112. package/src/tx-log/monitor-utils/is-unverifiable-pending-tx-expired.js +19 -0
  113. package/src/tx-log/monitor-utils/verify-pending-tx-status-rpc.js +90 -0
  114. package/src/tx-log-staking-processor/asset-staking-tx-data.js +82 -24
  115. package/src/tx-log-staking-processor/index.js +28 -14
  116. package/src/tx-log-staking-processor/utils.js +21 -20
  117. package/src/tx-send/broadcast-error-handler.js +63 -0
  118. package/src/tx-send/index.js +1 -2
  119. package/src/tx-send/nonce-utils.js +97 -0
  120. package/src/tx-send/tx-send.js +94 -288
  121. package/src/tx-sign/index.js +17 -0
  122. package/src/tx-type/index.js +280 -0
  123. package/src/watch-tx.js +36 -0
  124. package/src/web3/createSimulateMessage.js +3 -2
  125. package/src/web3/createSimulateTransactions.js +11 -0
  126. package/src/web3/index.js +3 -1
  127. package/src/etherscan/account.js +0 -49
  128. package/src/etherscan/index.js +0 -28
  129. package/src/etherscan/logs.js +0 -16
  130. package/src/etherscan/proxy.js +0 -47
  131. package/src/etherscan/request.js +0 -26
  132. package/src/etherscan/ws.js +0 -88
  133. package/src/exodus-eth-server/server-factory.js +0 -28
  134. package/src/exodus-eth-server/server-map.js +0 -21
  135. package/src/get-fee-async.js +0 -97
  136. package/src/staking/matic/service.js +0 -437
  137. package/src/staking-api.js +0 -5
  138. package/src/tx-log/clarity-utils/get-derive-data-needed-for-tick.js +0 -12
  139. package/src/tx-log-staking-processor/get-asset-tx-amount.js +0 -26
  140. package/src/tx-send/get-fee-info.js +0 -41
  141. package/src/websocket/index.android.js +0 -2
  142. package/src/websocket/index.ios.js +0 -2
  143. package/src/websocket/index.js +0 -21
@@ -0,0 +1,666 @@
1
+ import {
2
+ assertValidTxValue,
3
+ currency2buffer,
4
+ getHighestIncentiveTxByNonce,
5
+ isEthereumLikeToken,
6
+ } from '@exodus/ethereum-lib'
7
+ import createEthereumJsTx from '@exodus/ethereum-lib/src/unsigned-tx/create-ethereumjs-tx.js'
8
+ import assert from 'minimalistic-assert'
9
+
10
+ import { ensureSaneEip1559GasPriceForTipGasPrice } from './fee-utils.js'
11
+ import { fetchGasLimit } from './gas-estimation.js'
12
+ import { getAggregateTransactionPricing, getExtraFeeData } from './get-fee.js'
13
+ import { getNftArguments } from './nft-utils.js'
14
+ import {
15
+ assertTxAttributes,
16
+ isValidTxType,
17
+ resolveTxAttributesByTxType,
18
+ resolveTxFromAddress,
19
+ resolveTxNonce,
20
+ TX_TYPE_TRANSFER,
21
+ } from './tx-type/index.js'
22
+
23
+ // Thrown by `resolveAmountIncludesFeeTxAttributes` when the fee subtraction
24
+ // implied by `amountIncludesFee` would drive the resulting tx value below
25
+ // zero (i.e. the caller's balance can't cover the computed fee). Exported so
26
+ // callers like MoveFunds can `instanceof`-check and translate it into their
27
+ // own user-facing error type without string-matching the message.
28
+ export class AmountIncludesFeeUnderflowError extends Error {
29
+ constructor() {
30
+ super('transaction gas cost exceeds fee-inclusive amount')
31
+ this.name = 'AmountIncludesFeeUnderflowError'
32
+ }
33
+ }
34
+
35
+ async function calculateFee({
36
+ asset,
37
+ chainId,
38
+ eip1559Enabled,
39
+ gasLimit,
40
+ gasPrice,
41
+ tipGasPrice,
42
+ nonce,
43
+ txInput,
44
+ txToAddress,
45
+ txValue,
46
+ }) {
47
+ const ethjsTx = createEthereumJsTx({
48
+ txData: {
49
+ nonce,
50
+ gasPrice: currency2buffer(gasPrice),
51
+ tipGasPrice: tipGasPrice ? currency2buffer(tipGasPrice) : undefined,
52
+ gasLimit,
53
+ to: txToAddress,
54
+ value: currency2buffer(txValue),
55
+ data: txInput,
56
+ chainId,
57
+ },
58
+ txMeta: {
59
+ eip1559Enabled,
60
+ },
61
+ })
62
+ const transactionBuffer = ethjsTx.serialize()
63
+
64
+ const baseFee = gasPrice.mul(gasLimit)
65
+ const optimismL1DataFee = asset.baseAsset.estimateL1DataFee
66
+ ? await asset.baseAsset.estimateL1DataFee({
67
+ unsignedTx: { txData: { transactionBuffer, chainId } },
68
+ })
69
+ : undefined
70
+
71
+ const l1DataFee = optimismL1DataFee
72
+ ? asset.baseAsset.currency.baseUnit(optimismL1DataFee)
73
+ : asset.baseAsset.currency.ZERO
74
+
75
+ const fee = baseFee.add(l1DataFee)
76
+ return { fee, transactionBuffer }
77
+ }
78
+
79
+ async function createUnsignedTxWithFees({
80
+ asset,
81
+ chainId,
82
+ gasLimit,
83
+ eip1559Enabled,
84
+ gasPrice, // eip 1559: `maxFeePerGas`
85
+ tipGasPrice, // eip 1559: `maxPriorityPerGas`
86
+ bumpTxId,
87
+ /* txAttributes */
88
+ amount,
89
+ toAddress,
90
+ txInput,
91
+ txToAddress,
92
+ txType,
93
+ txValue,
94
+ fromAddress,
95
+ nonce,
96
+ isContractTxToAddress,
97
+ }) {
98
+ assert(asset, 'asset is required')
99
+ assert(typeof chainId === 'number', 'chainId is required')
100
+ assert(gasLimit, 'gasLimit is required')
101
+ assert(gasPrice, 'gasPrice is required')
102
+ assert(typeof eip1559Enabled === 'boolean', 'eip1559Enabled is required')
103
+
104
+ assertTxAttributes({
105
+ amount,
106
+ toAddress,
107
+ txInput,
108
+ txToAddress,
109
+ txType,
110
+ txValue,
111
+ fromAddress,
112
+ nonce,
113
+ isContractTxToAddress,
114
+ })
115
+
116
+ const { fee, transactionBuffer } = await calculateFee({
117
+ asset,
118
+ chainId,
119
+ eip1559Enabled,
120
+ gasLimit,
121
+ gasPrice,
122
+ tipGasPrice,
123
+ nonce,
124
+ txInput,
125
+ txToAddress,
126
+ txValue,
127
+ })
128
+
129
+ const extraFeeData = getExtraFeeData({ asset, amount, txValue })
130
+ const unsignedTx = {
131
+ txData: { transactionBuffer, chainId },
132
+ txMeta: {
133
+ assetName: asset.name,
134
+ bumpTxId,
135
+ eip1559Enabled,
136
+ fromAddress,
137
+ toAddress,
138
+ amount: amount.toDefaultString({ unit: true }),
139
+ fee: fee.toDefaultString({ unit: true }),
140
+ },
141
+ }
142
+ return {
143
+ unsignedTx,
144
+ fee,
145
+ amount,
146
+ extraFeeData,
147
+ // exhcange compatibility until the use usignedTx, remove me!
148
+ gasPrice,
149
+ tipGasPrice,
150
+ gasLimit,
151
+ }
152
+ }
153
+
154
+ // TODO: Move this into tx attribute resolution once gasLimit is resolved there.
155
+ // `amountIncludesFee` changes the tx value, so the fee subtraction should happen
156
+ // in the same flow that finalizes amount, txValue, txInput, txToAddress, and
157
+ // gasLimit.
158
+ async function resolveAmountIncludesFeeTxAttributes({
159
+ amountIncludesFee,
160
+ asset,
161
+ chainId,
162
+ eip1559Enabled,
163
+ gasLimit,
164
+ gasPrice,
165
+ tipGasPrice,
166
+ txAttributes,
167
+ }) {
168
+ const { amount, nonce, txInput, txToAddress, txValue } = txAttributes
169
+
170
+ // `amountIncludesFee` is a hint: subtract any fee that is paid in the same
171
+ // currency as `amount`. On EVM today, gas is paid in the native asset, so
172
+ // for ERC-20 sends and zero-value calldata (i.e approve()) flows there is nothing in the
173
+ // amount's currency to subtract. Pass through silently in those cases, so
174
+ // consumers can set this flag based on intent without needing to know
175
+ // whether the asset is a token or whether the call carries zero native
176
+ // value.
177
+ if (!amountIncludesFee || isEthereumLikeToken(asset) || amount.isZero) {
178
+ return { txAttributes }
179
+ }
180
+
181
+ const { fee } = await calculateFee({
182
+ asset,
183
+ chainId,
184
+ eip1559Enabled,
185
+ gasLimit,
186
+ gasPrice,
187
+ tipGasPrice,
188
+ nonce,
189
+ txInput,
190
+ txToAddress,
191
+ txValue,
192
+ })
193
+ const reducedAmount = amount.sub(fee)
194
+
195
+ if (!reducedAmount.gte(asset.currency.ZERO)) {
196
+ throw new AmountIncludesFeeUnderflowError()
197
+ }
198
+
199
+ return {
200
+ txAttributes: {
201
+ ...txAttributes,
202
+ amount: reducedAmount,
203
+ txValue: reducedAmount,
204
+ },
205
+ }
206
+ }
207
+
208
+ const resolveTxFactoryGasPrices = async ({
209
+ assetClientInterface,
210
+ baseAsset,
211
+ customFee: providedCustomFee,
212
+ feeData,
213
+ gasPrice: providedGasPrice,
214
+ tipGasPrice: providedTipGasPrice,
215
+ txToReplace,
216
+ nonce,
217
+ }) => {
218
+ assert(assetClientInterface, 'expected assetClientInterface')
219
+ assert(baseAsset, 'expected baseAsset')
220
+ assert(feeData, 'expected feeData')
221
+ assert(Number.isInteger(nonce), 'expected integer nonce')
222
+
223
+ const {
224
+ gasPrice: maybeGasPrice,
225
+ feeData: { tipGasPrice: maybeTipGasPrice, eip1559Enabled },
226
+ } = getAggregateTransactionPricing({
227
+ baseAsset,
228
+ customFee: providedCustomFee,
229
+ feeData,
230
+ txToReplace,
231
+ })
232
+
233
+ // When we determine the `gasPrice` for a transaction, we must be
234
+ // cognizant of any transactions which are pending for the current
235
+ // `nonce`, as this may be higher compared to the current `gasPrice`
236
+ // that we are recommending from the network.
237
+ //
238
+ // In this instance, the `gasPrice` we recommend should be
239
+ // sufficient to avoid `replacement transaction underpriced` errors.
240
+ const resolvedGasPrice = providedGasPrice ?? maybeGasPrice
241
+
242
+ if (!eip1559Enabled) return { eip1559Enabled, resolvedGasPrice }
243
+
244
+ const resolvedTipGasPrice = providedTipGasPrice ?? maybeTipGasPrice
245
+
246
+ return {
247
+ eip1559Enabled,
248
+ resolvedGasPrice: ensureSaneEip1559GasPriceForTipGasPrice({
249
+ gasPrice: resolvedGasPrice,
250
+ tipGasPrice: resolvedTipGasPrice,
251
+ }),
252
+ resolvedTipGasPrice,
253
+ }
254
+ }
255
+
256
+ // Pre-broadcast safety check for RBF replacements.
257
+ //
258
+ // Clarity can lag behind the node: it may still report a tx as pending after
259
+ // the node has evicted it, or show a queued (gapped) tx as acceleratable.
260
+ // Broadcasting an RBF for a gapped tx — one whose predecessor slot is empty in
261
+ // the node's mempool — produces ErrFutureReplacePending.
262
+ //
263
+ // This check is best-effort: if either RPC call fails (network error, unsupported
264
+ // endpoint, etc.) we fall through and let the broadcast surface the error.
265
+ async function verifyBumpTxCanReplace({ baseAsset, bumpTxId, fromAddress, nonce }) {
266
+ let rpcTx, pendingNonceHex
267
+
268
+ try {
269
+ ;[rpcTx, pendingNonceHex] = await Promise.all([
270
+ baseAsset.server.getTransactionByHash(bumpTxId),
271
+ baseAsset.server.getTransactionCount(fromAddress, 'pending'),
272
+ ])
273
+ } catch (err) {
274
+ console.warn(
275
+ `verifyBumpTxCanReplace: pre-broadcast RPC check failed for ${bumpTxId}, falling through`,
276
+ err.message
277
+ )
278
+ return
279
+ }
280
+
281
+ if (!rpcTx) {
282
+ // The tx was dropped from the node's mempool — Clarity hasn't caught up yet.
283
+ // TODO: eagerly mark this tx as dropped in the tx log so the UI updates
284
+ // immediately instead of waiting for Clarity's next polling cycle.
285
+ throw new Error(
286
+ `ERR_BUMP_TX_DROPPED: Cannot bump transaction ${bumpTxId}: transaction was dropped from the network`
287
+ )
288
+ }
289
+
290
+ // pendingNonce = 4
291
+ // [0][1][2][3]{4}{5}{6}...
292
+ // ^--- next empty slot (pendingNonce)
293
+ //
294
+ // Nonce to replace must be < pendingNonce (i.e. slot is already accounted for).
295
+ // If nonce >= pendingNonce, the slot is empty or gapped — not safe to replace.
296
+ const pendingNonce = parseInt(pendingNonceHex, 16)
297
+ if (pendingNonce <= nonce) {
298
+ // A predecessor tx was dropped by the node, leaving a gap below this tx.
299
+ // The tx itself is still in the node's queued pool but can't execute until
300
+ // the missing nonce slot is filled.
301
+ // TODO: identify the dropped predecessor (scan nonces from pendingNonce to
302
+ // nonce - 1), mark it as dropped in the tx log, and surface a clear message
303
+ // to the user: "a previous transaction was dropped — send a new transaction
304
+ // first, then you'll be able to accelerate this one."
305
+ throw new Error(
306
+ `ERR_BUMP_TX_NONCE_GAP: Cannot bump transaction ${bumpTxId}: nonce gap detected — ` +
307
+ `node's pending nonce (${pendingNonce}) ≤ tx nonce (${nonce}), ` +
308
+ `broadcasting would produce ErrFutureReplacePending`
309
+ )
310
+ }
311
+ }
312
+
313
+ const createBumpUnsignedTx = async ({
314
+ fromAddress,
315
+ chainId,
316
+ asset,
317
+ bumpTxId,
318
+ baseAssetTxLog,
319
+ assetClientInterface,
320
+ walletAccount,
321
+ feeData,
322
+ nonce: maybeProvidedNonce,
323
+ txType,
324
+ }) => {
325
+ assert(isValidTxType(txType), 'invalid txType')
326
+
327
+ const baseAsset = asset.baseAsset
328
+ const replacedTx = baseAssetTxLog.get(bumpTxId)
329
+ const assets = await assetClientInterface.getAssetsForNetwork({ baseAssetName: baseAsset.name })
330
+ if (!replacedTx || !replacedTx.pending) {
331
+ throw new Error(`Cannot bump transaction ${bumpTxId}: not found or confirmed`)
332
+ }
333
+
334
+ let replacedTokenTx
335
+ if (replacedTx.tokens.length > 0) {
336
+ const [tokenAssetName] = replacedTx.tokens
337
+ const tokenTxSet = await assetClientInterface.getTxLog({
338
+ assetName: tokenAssetName,
339
+ walletAccount,
340
+ })
341
+ replacedTokenTx = tokenTxSet.get(bumpTxId)
342
+
343
+ if (replacedTokenTx) {
344
+ // Attempt to overwrite the asset to reflect the fact that
345
+ // we're performing a token transaction.
346
+ asset = assets[tokenAssetName]
347
+ if (!asset) {
348
+ throw new Error(
349
+ `unable to find ${tokenAssetName} during token bump transaction: asset was not available in assetsForNetwork`
350
+ )
351
+ }
352
+ }
353
+ }
354
+
355
+ const sourceTx = replacedTokenTx || replacedTx
356
+ const toAddress = sourceTx.to
357
+
358
+ // For self-sends `coinAmount` is `0` (correct for balance accounting, the
359
+ // wallet's net delta really is zero), so we can't reconstruct the on-chain
360
+ // value from it. Recover from `data.rawCoinAmount`, which the tx-log
361
+ // writers persist alongside `coinAmount` for exactly this purpose.
362
+ if (sourceTx.selfSend && !sourceTx.data?.rawCoinAmount) {
363
+ throw new Error(
364
+ `Cannot bump self-send transaction ${bumpTxId}: original transfer amount is unrecoverable from the tx log (data.rawCoinAmount missing). Re-broadcast the send with a higher fee instead.`
365
+ )
366
+ }
367
+
368
+ const amount = sourceTx.data?.rawCoinAmount
369
+ ? asset.currency.baseUnit(sourceTx.data.rawCoinAmount)
370
+ : sourceTx.coinAmount.negate()
371
+
372
+ const txValue = assertValidTxValue({
373
+ asset,
374
+ amount,
375
+ txValue: replacedTx.data?.rawCoinAmount
376
+ ? baseAsset.currency.baseUnit(replacedTx.data.rawCoinAmount)
377
+ : replacedTx.coinAmount.negate(),
378
+ })
379
+
380
+ const isDuplex = Boolean(replacedTokenTx && txValue.gt(baseAsset.currency.ZERO))
381
+
382
+ const txInput = replacedTx.data.data || '0x'
383
+
384
+ const isTokenTx =
385
+ !isDuplex /* nonpayable */ &&
386
+ replacedTokenTx &&
387
+ Object.keys(asset.contract.methodIds).includes(txInput.slice(0, 10))
388
+
389
+ const txToAddress = isTokenTx ? asset.contract.address : undefined
390
+
391
+ const replacedTxNonce = replacedTx.data.nonce
392
+
393
+ assert(
394
+ Number.isInteger(replacedTxNonce),
395
+ `Cannot bump transaction ${bumpTxId}: data object seems to be corrupted`
396
+ )
397
+
398
+ // If we have evaluated a bump transaction and the `providedNonce` differs
399
+ // from the `bumpNonce`, we've encountered a conflict and cannot respect
400
+ // the caller's request.
401
+ if (maybeProvidedNonce && maybeProvidedNonce !== replacedTxNonce) {
402
+ throw new Error('incorrect nonce for replacement transaction')
403
+ }
404
+
405
+ const nonce = maybeProvidedNonce ?? replacedTxNonce
406
+
407
+ await verifyBumpTxCanReplace({ baseAsset, bumpTxId, fromAddress, nonce })
408
+
409
+ const resolvedTxAttributes = await resolveTxAttributesByTxType({
410
+ asset,
411
+ assetClientInterface,
412
+ fromAddress,
413
+ amount,
414
+ nonce,
415
+ txInput,
416
+ toAddress,
417
+ txToAddress,
418
+ txType,
419
+ txValue,
420
+ walletAccount,
421
+ })
422
+
423
+ const gasLimit = replacedTx.data.gasLimit
424
+
425
+ const maybeTxToReplace = await getHighestIncentiveTxByNonce({
426
+ assetClientInterface,
427
+ asset,
428
+ nonce: resolvedTxAttributes.nonce,
429
+ walletAccount,
430
+ })
431
+
432
+ assert(
433
+ maybeTxToReplace,
434
+ `unable to resolve pending transaction for nonce ${resolvedTxAttributes.nonce}`
435
+ )
436
+
437
+ const {
438
+ eip1559Enabled,
439
+ resolvedGasPrice: gasPrice,
440
+ resolvedTipGasPrice: tipGasPrice,
441
+ } = await resolveTxFactoryGasPrices({
442
+ assetClientInterface,
443
+ baseAsset,
444
+ feeData,
445
+ nonce,
446
+ txToReplace: maybeTxToReplace,
447
+ })
448
+
449
+ return createUnsignedTxWithFees({
450
+ ...resolvedTxAttributes,
451
+ asset,
452
+ chainId,
453
+ gasLimit,
454
+ gasPrice,
455
+ tipGasPrice,
456
+ bumpTxId,
457
+ eip1559Enabled,
458
+ })
459
+ }
460
+
461
+ export const createTxFactory = ({ chainId, assetClientInterface, useAbsoluteNonce }) => {
462
+ assert(assetClientInterface, 'assetClientInterface is required')
463
+ assert(typeof chainId === 'number', 'chainId is required')
464
+ return async ({
465
+ asset,
466
+ walletAccount,
467
+ nft, // when sending nfts
468
+ fromAddress: providedFromAddress, // wallet from address
469
+ toAddress, // user's to address, not the token or the dex contract
470
+ address, // same as toAddress
471
+ txInput: providedTxInput, // Provided when swapping via a DEX contract
472
+ txType = TX_TYPE_TRANSFER, // Defines what kind of transaction is being performed.
473
+ txValue: providedTxValue, // Override the native ETH value (e.g. payable contract calls with txInput when specifying a token asset)
474
+ contractAddress: providedTxToAddress, // Controls the final target address of the transaction.
475
+ gasLimit: providedGasLimit, // Provided by exchange when known
476
+ amount: providedAmount, // The NU amount to be sent, to be included in the tx value or tx input
477
+ nonce: providedNonce,
478
+ tipGasPrice: providedTipGasPrice,
479
+ gasPrice: providedGasPrice,
480
+ bip70,
481
+ customFee: providedCustomFee,
482
+ // Intent flag: the user is trying to empty the wallet. Used for send-all-only
483
+ // dust behavior such as the EIP-1559 tip override.
484
+ isSendAll,
485
+ // Amount semantics flag: the provided amount includes the fee budget, so the
486
+ // final tx value should be reduced by the computed fee.
487
+ amountIncludesFee = false,
488
+ bumpTxId,
489
+ }) => {
490
+ assert(asset, 'asset is required')
491
+ assert(walletAccount, 'walletAccount is required')
492
+ assert(isValidTxType(txType), 'invalid txType')
493
+
494
+ const providedToAddress = toAddress ?? address
495
+
496
+ const baseAsset = asset.baseAsset
497
+ const feeData = await assetClientInterface.getFeeConfig({ assetName: baseAsset.name })
498
+
499
+ const baseAssetTxLog = await assetClientInterface.getTxLog({
500
+ assetName: baseAsset.name,
501
+ walletAccount,
502
+ })
503
+
504
+ const fromAddress = await resolveTxFromAddress({
505
+ asset,
506
+ assetClientInterface,
507
+ fromAddress: providedFromAddress,
508
+ walletAccount,
509
+ })
510
+
511
+ if (bumpTxId) {
512
+ assert(!providedTxValue && !providedTxToAddress)
513
+
514
+ return createBumpUnsignedTx({
515
+ chainId,
516
+ asset,
517
+ fromAddress,
518
+ bumpTxId,
519
+ baseAssetTxLog,
520
+ assetClientInterface,
521
+ walletAccount,
522
+ feeData,
523
+ nonce: providedNonce,
524
+ txType,
525
+ })
526
+ }
527
+
528
+ const nonce = await resolveTxNonce({
529
+ asset,
530
+ fromAddress,
531
+ nonce: providedNonce,
532
+ walletAccount,
533
+ })
534
+
535
+ const maybeTxToReplace = await getHighestIncentiveTxByNonce({
536
+ assetClientInterface,
537
+ asset: baseAsset,
538
+ nonce,
539
+ walletAccount,
540
+ })
541
+
542
+ const {
543
+ eip1559Enabled,
544
+ resolvedGasPrice: gasPrice,
545
+ resolvedTipGasPrice: tipGasPrice,
546
+ } = await resolveTxFactoryGasPrices({
547
+ assetClientInterface,
548
+ baseAsset,
549
+ customFee: providedCustomFee,
550
+ feeData,
551
+ gasPrice: providedGasPrice,
552
+ nonce,
553
+ tipGasPrice: providedTipGasPrice,
554
+ txToReplace: maybeTxToReplace,
555
+ })
556
+
557
+ if (nft) {
558
+ assert(!providedTxValue && !providedTxToAddress)
559
+
560
+ const {
561
+ contractAddress: txToAddress,
562
+ gasLimit,
563
+ txInput,
564
+ } = await getNftArguments({
565
+ asset,
566
+ nft,
567
+ fromAddress,
568
+ toAddress: providedToAddress,
569
+ })
570
+
571
+ const resolvedTxAttributes = await resolveTxAttributesByTxType({
572
+ asset: baseAsset,
573
+ assetClientInterface,
574
+ fromAddress,
575
+ nonce,
576
+ txInput,
577
+ toAddress: providedToAddress,
578
+ txToAddress,
579
+ txType,
580
+ walletAccount,
581
+ })
582
+
583
+ return createUnsignedTxWithFees({
584
+ ...resolvedTxAttributes,
585
+ chainId,
586
+ asset,
587
+ gasLimit,
588
+ gasPrice,
589
+ tipGasPrice,
590
+ eip1559Enabled,
591
+ })
592
+ }
593
+
594
+ const resolvedTxAttributes = await resolveTxAttributesByTxType({
595
+ asset,
596
+ assetClientInterface,
597
+ amount: providedAmount,
598
+ fromAddress,
599
+ nonce,
600
+ txInput: providedTxInput,
601
+ toAddress: providedToAddress,
602
+ txToAddress: providedTxToAddress,
603
+ txType,
604
+ txValue: providedTxValue,
605
+ walletAccount,
606
+ })
607
+
608
+ // TODO: `gasLimit` should become `txGasLimit` and returned
609
+ // by `resolveTxAttributesByTxType`.
610
+ const gasLimit =
611
+ providedGasLimit ??
612
+ (await fetchGasLimit({
613
+ asset,
614
+ feeData,
615
+ fromAddress,
616
+ toAddress: resolvedTxAttributes.toAddress,
617
+ txInput: resolvedTxAttributes.txInput,
618
+ txValue: resolvedTxAttributes.txValue,
619
+ contractAddress: resolvedTxAttributes.txToAddress,
620
+ bip70,
621
+ amount: resolvedTxAttributes.amount,
622
+ txType,
623
+ isSendAll,
624
+ }))
625
+
626
+ // HACK: We cannot ensure the no dust invariant for `isSendAll`
627
+ // transactions to contract addresses, since we may be
628
+ // performing a raw token transaction and the parameter
629
+ // applies to the token and not the native amount.
630
+ //
631
+ // Contracts have nondeterministic gas most of the time
632
+ // versus estimations, anyway.
633
+ const isSendAllBaseAsset =
634
+ isSendAll && !isEthereumLikeToken(asset) && !resolvedTxAttributes.isContractTxToAddress
635
+
636
+ const finalTipGasPrice = isSendAllBaseAsset && eip1559Enabled ? gasPrice : tipGasPrice
637
+
638
+ const { txAttributes: finalTxAttributes } = await resolveAmountIncludesFeeTxAttributes({
639
+ amountIncludesFee,
640
+ asset,
641
+ chainId,
642
+ eip1559Enabled,
643
+ gasLimit,
644
+ gasPrice,
645
+ tipGasPrice: finalTipGasPrice,
646
+ txAttributes: resolvedTxAttributes,
647
+ })
648
+
649
+ return createUnsignedTxWithFees({
650
+ ...finalTxAttributes,
651
+ asset,
652
+ chainId,
653
+ gasLimit,
654
+ gasPrice,
655
+ // For native send all transactions, we have to make sure that
656
+ // the `tipGasPrice` is equal to the `gasPrice`, since this is
657
+ // effectively like saying that the `maxFeePerGas` is equal
658
+ // to the `maxPriorityFeePerGas`. We do this so that for a
659
+ // fixed gas cost transaction, no dust balance should remain,
660
+ // since any deviation in the underlying `baseFeePerGas` will
661
+ // result only affect the tip for the miner - no dust remains.
662
+ tipGasPrice: finalTipGasPrice,
663
+ eip1559Enabled,
664
+ })
665
+ }
666
+ }