@exodus/ethereum-api 8.76.3 → 8.76.5

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 CHANGED
@@ -3,6 +3,24 @@
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.76.5](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.76.4...@exodus/ethereum-api@8.76.5) (2026-06-03)
7
+
8
+ **Note:** Version bump only for package @exodus/ethereum-api
9
+
10
+
11
+
12
+
13
+
14
+ ## [8.76.4](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.76.3...@exodus/ethereum-api@8.76.4) (2026-06-02)
15
+
16
+
17
+ ### Bug Fixes
18
+
19
+
20
+ * fix(ethereum-api): eliminate send-all dust on HYPE and other forceGasLimitEstimation networks (#7747)
21
+
22
+
23
+
6
24
  ## [8.76.3](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.76.2...@exodus/ethereum-api@8.76.3) (2026-06-01)
7
25
 
8
26
  **Note:** Version bump only for package @exodus/ethereum-api
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/ethereum-api",
3
- "version": "8.76.3",
3
+ "version": "8.76.5",
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",
@@ -70,5 +70,5 @@
70
70
  "type": "git",
71
71
  "url": "git+https://github.com/ExodusMovement/assets.git"
72
72
  },
73
- "gitHead": "aac7087b928f14da94a1a071b5abaf0983136c2a"
73
+ "gitHead": "651f8346570d40e724109e3df1cd2a3e10323a6f"
74
74
  }
@@ -0,0 +1,23 @@
1
+ import { fetchival } from '@exodus/fetch'
2
+ import assert from 'minimalistic-assert'
3
+
4
+ export const DEFAULT_ASSESS_TRANSACTION_API_URL =
5
+ 'https://simulation.a.exodus.io/simulation/transaction/assessment'
6
+
7
+ export const createAssessTransaction = (
8
+ { apiUrl = DEFAULT_ASSESS_TRANSACTION_API_URL, headers, request = fetchival } = Object.create(
9
+ null
10
+ )
11
+ ) => {
12
+ assert(typeof apiUrl === 'string' && apiUrl.length > 0, 'apiUrl must be a non-empty string')
13
+
14
+ return async function assessTransaction({ payload }) {
15
+ return request(apiUrl, {
16
+ method: 'POST',
17
+ headers: {
18
+ ...headers,
19
+ 'Content-Type': 'application/json',
20
+ },
21
+ }).post(payload)
22
+ }
23
+ }
@@ -1,5 +1,4 @@
1
1
  import { memoizeLruCache } from '@exodus/asset-lib'
2
- import { makeSimulationAPICall } from '@exodus/web3-utils'
3
2
  import assert from 'minimalistic-assert'
4
3
  import ms from 'ms'
5
4
 
@@ -37,25 +36,25 @@ const isRecipientFinding = ({ finding, recipientAddress }) => {
37
36
  })
38
37
  }
39
38
 
40
- const pickFindingForRecipient = ({ findings, recipientAddress }) => {
41
- if (!Array.isArray(findings) || !recipientAddress) return GENERIC_RISK_REASON
42
- const finding = findings.find((candidate) =>
43
- isRecipientFinding({ finding: candidate, recipientAddress })
44
- )
45
- return finding?.title || GENERIC_RISK_REASON
39
+ const findRecipientFinding = ({ findings, recipientAddress }) => {
40
+ if (!Array.isArray(findings) || !recipientAddress) return
41
+ return findings.find((candidate) => isRecipientFinding({ finding: candidate, recipientAddress }))
46
42
  }
47
43
 
48
44
  const parseAssessment = ({ body, recipientAddress }) => {
49
45
  if (!body?.success) return { action: 'NONE' }
50
46
  if (body.data?.recommendation !== 'deny') return { action: 'NONE' }
47
+ const finding = findRecipientFinding({ findings: body.data?.findings, recipientAddress })
48
+ if (!finding) return { action: 'NONE' }
49
+
51
50
  return {
52
51
  action: 'WARN',
53
- reason: pickFindingForRecipient({ findings: body.data?.findings, recipientAddress }),
52
+ reason: finding.title || GENERIC_RISK_REASON,
54
53
  }
55
54
  }
56
55
 
57
- // Rejects after `timeoutMs`. The underlying request keeps running because
58
- // `makeSimulationAPICall` doesn't accept an AbortSignal.
56
+ // Rejects after `timeoutMs`. The underlying request keeps running because the
57
+ // API helper doesn't accept an AbortSignal.
59
58
  const withTimeout = async (promise, timeoutMs) => {
60
59
  let timeoutId
61
60
  const timeoutPromise = new Promise((_resolve, reject) => {
@@ -71,14 +70,9 @@ const withTimeout = async (promise, timeoutMs) => {
71
70
  const noopLogger = { warn: () => {} }
72
71
 
73
72
  export const createCheckTx = (
74
- {
75
- apiUrl,
76
- timeout = DEFAULT_TIMEOUT_MS,
77
- makeApiCall = makeSimulationAPICall,
78
- logger = noopLogger,
79
- } = Object.create(null)
73
+ { timeout = DEFAULT_TIMEOUT_MS, makeApiCall, logger = noopLogger } = Object.create(null)
80
74
  ) => {
81
- assert(apiUrl, 'apiUrl is required')
75
+ assert(typeof makeApiCall === 'function', 'makeApiCall is required')
82
76
 
83
77
  const warn = typeof logger?.warn === 'function' ? logger.warn.bind(logger) : noopLogger.warn
84
78
 
@@ -90,13 +84,7 @@ export const createCheckTx = (
90
84
  if (input !== undefined) transaction.input = input
91
85
  if (hash !== undefined) transaction.hash = hash
92
86
 
93
- const body = await withTimeout(
94
- makeApiCall({
95
- url: apiUrl,
96
- payload: { serviceProvider: 'hypernative', transaction },
97
- }),
98
- timeout
99
- )
87
+ const body = await withTimeout(makeApiCall({ payload: { transaction } }), timeout)
100
88
 
101
89
  if (!body) throw new Error('checkTx: empty response')
102
90
  return parseAssessment({ body, recipientAddress: toAddress })
@@ -1,3 +1,4 @@
1
+ import { createConsoleLogger } from '@exodus/asset-lib'
1
2
  import { ASSET_FAMILY, connectAssetsList } from '@exodus/assets'
2
3
  import bip44Constants from '@exodus/bip44-constants/by-ticker.js'
3
4
  import {
@@ -21,6 +22,7 @@ import lodash from 'lodash'
21
22
  import assert from 'minimalistic-assert'
22
23
 
23
24
  import { addressHasHistoryFactory } from './address-has-history.js'
25
+ import { createAssessTransaction } from './check-tx/create-assess-transaction.js'
24
26
  import { createCheckTx } from './check-tx/index.js'
25
27
  import {
26
28
  createGetBlackListStatus,
@@ -78,9 +80,9 @@ export const createAssetFactory = ({
78
80
  useAbsoluteBalanceAndNonce = false,
79
81
  delisted = false,
80
82
  privacyRpcUrl: defaultPrivacyRpcUrl,
81
- riskAssessment: defaultRiskAssessment,
82
83
  wsGatewayUri: defaultWsGatewayUri,
83
84
  eip7702Supported,
85
+ transactionAssessment: defaultTransactionAssessment,
84
86
  }) => {
85
87
  assert(assetsList, 'assetsList is required')
86
88
  assert(providedFeeData || feeDataConfig, 'feeData or feeDataConfig is required')
@@ -104,7 +106,7 @@ export const createAssetFactory = ({
104
106
  nfts: defaultNfts,
105
107
  customTokens: defaultCustomTokens,
106
108
  privacyRpcUrl: defaultPrivacyRpcUrl,
107
- riskAssessment: defaultRiskAssessment,
109
+ transactionAssessment: defaultTransactionAssessment,
108
110
  }
109
111
  return (
110
112
  {
@@ -125,7 +127,7 @@ export const createAssetFactory = ({
125
127
  supportsCustomFees,
126
128
  useAbsoluteBalanceAndNonce: overrideUseAbsoluteBalanceAndNonce,
127
129
  privacyRpcUrl,
128
- riskAssessment,
130
+ transactionAssessment,
129
131
  } = configWithOverrides
130
132
 
131
133
  const asset = assets[base.name]
@@ -276,7 +278,7 @@ export const createAssetFactory = ({
276
278
  })
277
279
 
278
280
  const getFeeAsync = async (...args) => {
279
- const { unsignedTx, ...rest } = await createTx(...args)
281
+ const { unsignedTx, amount, ...rest } = await createTx(...args)
280
282
  return rest
281
283
  }
282
284
 
@@ -294,16 +296,18 @@ export const createAssetFactory = ({
294
296
 
295
297
  const securityChecks = createSecurityChecks({ eip7702Supported })
296
298
 
299
+ const web3 = createWeb3API({ asset })
300
+
297
301
  let checkTx
298
- if (riskAssessment?.apiUrl) {
302
+ let sendValidations = []
303
+ if (transactionAssessment?.enabled) {
299
304
  checkTx = createCheckTx({
300
- apiUrl: riskAssessment.apiUrl,
301
- logger: riskAssessment.logger,
305
+ makeApiCall: createAssessTransaction({ apiUrl: transactionAssessment.apiUrl }),
306
+ logger: createConsoleLogger('@exodus/ethereum-api:check-tx'),
302
307
  })
308
+ sendValidations = createSendValidations({ assetClientInterface, checkTx })
303
309
  }
304
310
 
305
- const sendValidations = checkTx ? createSendValidations({ assetClientInterface, checkTx }) : []
306
-
307
311
  const moveFunds = moveFundsFactory({
308
312
  baseAssetName: asset.name,
309
313
  assetClientInterface,
@@ -365,7 +369,7 @@ export const createAssetFactory = ({
365
369
  }),
366
370
  }),
367
371
  validateAssetId: address.validate,
368
- web3: createWeb3API({ asset }),
372
+ web3,
369
373
  }
370
374
 
371
375
  const fullAsset = {
@@ -119,6 +119,7 @@ export async function fetchGasLimit({
119
119
  bip70,
120
120
  txType = TX_TYPE_TRANSFER,
121
121
  throwOnError = true,
122
+ isSendAll = false,
122
123
  }) {
123
124
  if (bip70?.bitpay?.data && bip70?.bitpay?.gasPrice) {
124
125
  // from on chain stats https://dune.xyz/queries/189123
@@ -178,7 +179,15 @@ export async function fetchGasLimit({
178
179
  })
179
180
 
180
181
  const scaledGasLimitEstimate = scaleGasLimitEstimate({ estimatedGasLimit, gasLimitMultiplier })
181
- if (!isToken) return scaledGasLimitEstimate
182
+ if (!isToken) {
183
+ // For native send-all on EOA targets, return the raw `eth_estimateGas`
184
+ // result (no 2x safety pad). Combined with the EIP-1559 tip override
185
+ // applied in tx-create.js (also gated on `isSendAll && !isContract`),
186
+ // this is the no-dust path: gasLimit ≈ gasUsed, so nothing comes back
187
+ // as refund. For contracts we keep the safety pad because contract
188
+ // gas can be nondeterministic against estimates.
189
+ return isSendAll && !isContractTxToAddress ? estimatedGasLimit : scaledGasLimitEstimate
190
+ }
182
191
 
183
192
  // NOTE: If we've enabled `fixGasLimit`s for a token,
184
193
  // we need to make sure that transaction we're
package/src/move-funds.js CHANGED
@@ -4,6 +4,7 @@ import assert from 'minimalistic-assert'
4
4
 
5
5
  import { getNonce, getTokenBalanceFromNode } from './eth-like-util.js'
6
6
  import { fetchGasLimit } from './gas-estimation.js'
7
+ import { AmountIncludesFeeUnderflowError } from './tx-create.js'
7
8
 
8
9
  export const moveFundsFactory = ({ baseAssetName, assetClientInterface, createTx, server }) => {
9
10
  assert(baseAssetName, 'baseAssetName is required')
@@ -73,37 +74,69 @@ export const moveFundsFactory = ({ baseAssetName, assetClientInterface, createTx
73
74
  fromAddress,
74
75
  toAddress,
75
76
  amount,
77
+ // `isSendAll` and `amountIncludesFee` are no-ops on the token branch
78
+ // (both gated on `!isToken` / `!isEthereumLikeToken(asset)` downstream),
79
+ // so don't pass them. Keeps the API surface honest about which flags
80
+ // actually take effect for each branch.
81
+ ...(isToken ? null : { isSendAll: true }),
76
82
  })
77
83
 
78
- const fee = feeData.gasPrice.mul(gasLimit)
79
-
80
- const afterFee = ethBalance.sub(fee)
81
- if (isToken) {
82
- if (afterFee.isNegative) {
83
- throw new MoveFundsError('token-fee-insufficient', { fromAddress })
84
- }
85
- } else {
86
- if (!afterFee.isPositive) {
84
+ let createTxResult
85
+ try {
86
+ createTxResult = await createTx({
87
+ asset,
88
+ walletAccount,
89
+ fromAddress,
90
+ // Native sweep: pass the full balance and let createTx subtract the
91
+ // accurate fee. Token sweep: pass the full token balance unchanged
92
+ // (the native gas fee is paid separately from native balance).
93
+ address: toAddress,
94
+ amount,
95
+ nonce,
96
+ gasLimit,
97
+ gasPrice: feeData.gasPrice,
98
+ // TODO: drop this. Move funds has no fee picker, so it's always
99
+ // `feeData.gasPrice` (same as `gasPrice` above) and a no-op anyway.
100
+ // If we ever need a custom fee, thread it through prepareSendFundsTx.
101
+ customFee: feeData.gasPrice,
102
+ // Both flags are no-ops for tokens (see fetchGasLimit call above).
103
+ ...(isToken ? null : { isSendAll: true, amountIncludesFee: true }),
104
+ })
105
+ } catch (err) {
106
+ // createTx throws `AmountIncludesFeeUnderflowError` for the native
107
+ // sweep when the accurate fee (including OP-stack L1 data fee) exceeds
108
+ // the balance we passed in. Translate to the typed MoveFundsError so
109
+ // consumers (mobile/desktop MoveFunds screens) render the right copy.
110
+ if (!isToken && err instanceof AmountIncludesFeeUnderflowError) {
87
111
  throw new MoveFundsError('balance-negative', { fromAddress })
88
112
  }
89
113
 
90
- amount = amount.sub(fee)
114
+ throw err
91
115
  }
92
116
 
93
- const { unsignedTx } = await createTx({
94
- asset,
95
- walletAccount,
96
- fromAddress,
97
- address: toAddress,
98
- amount,
99
- nonce,
100
- gasLimit,
101
- gasPrice: feeData.gasPrice,
102
- customFee: feeData.gasPrice,
103
- isSendAll: true,
104
- })
117
+ // Token sweep equivalent of the native underflow gate: createTx doesn't
118
+ // validate ETH balance vs gas for token transfers, so check it here using
119
+ // the authoritative fee returned from `calculateFee`. This catches the
120
+ // OP-stack edge case where balance covers `gasPrice * gasLimit` but not
121
+ // the additional L1 data fee.
122
+ if (isToken && ethBalance.sub(createTxResult.fee).isNegative) {
123
+ throw new MoveFundsError('token-fee-insufficient', { fromAddress })
124
+ }
105
125
 
106
- return { fromAddress, toAddress, amount, fee, privateKey, unsignedTx }
126
+ // Trust createTx for both amount and fee. For native sweep `amount` is
127
+ // the post-subtraction value (input balance minus accurate fee). For
128
+ // token sweep `amount` is the input token balance unchanged, since we
129
+ // don't pass `amountIncludesFee`. In both cases `fee` includes the
130
+ // OP-stack L1 data fee from `calculateFee`, which matters more for
131
+ // tokens than for native because ERC-20 calldata is larger.
132
+ return {
133
+ fromAddress,
134
+ toAddress,
135
+ amount: createTxResult.amount,
136
+ fee: createTxResult.fee,
137
+ privateKey,
138
+ unsignedTx: createTxResult.unsignedTx,
139
+ }
107
140
  }
108
141
 
109
142
  const sendFunds = async ({ privateKey, unsignedTx }) => {
@@ -39,14 +39,13 @@ export const createSendValidations = ({ assetClientInterface, checkTx }) => {
39
39
  })
40
40
  if (!fromAddress) return
41
41
 
42
- // Match Hypernative's documented native-send payload shape.
42
+ // Native send: empty calldata, no signed tx hash to pass yet.
43
43
  const result = await checkTx({
44
44
  chain: asset.chainId,
45
45
  fromAddress,
46
46
  toAddress: destinationAddress,
47
47
  value: sendAmount.toBaseString({ unit: false }),
48
48
  input: '0x',
49
- hash: '0x1',
50
49
  })
51
50
 
52
51
  if (result?.action !== 'WARN') return
package/src/tx-create.js CHANGED
@@ -20,6 +20,62 @@ import {
20
20
  TX_TYPE_TRANSFER,
21
21
  } from './tx-type/index.js'
22
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
+
23
79
  async function createUnsignedTxWithFees({
24
80
  asset,
25
81
  chainId,
@@ -57,35 +113,19 @@ async function createUnsignedTxWithFees({
57
113
  isContractTxToAddress,
58
114
  })
59
115
 
60
- const ethjsTx = createEthereumJsTx({
61
- txData: {
62
- nonce,
63
- gasPrice: currency2buffer(gasPrice),
64
- tipGasPrice: tipGasPrice ? currency2buffer(tipGasPrice) : undefined,
65
- gasLimit,
66
- to: txToAddress,
67
- value: currency2buffer(txValue),
68
- data: txInput,
69
- chainId,
70
- },
71
- txMeta: {
72
- eip1559Enabled,
73
- },
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,
74
127
  })
75
- const transactionBuffer = ethjsTx.serialize()
76
128
 
77
- const baseFee = gasPrice.mul(gasLimit)
78
- const optimismL1DataFee = asset.baseAsset.estimateL1DataFee
79
- ? await asset.baseAsset.estimateL1DataFee({
80
- unsignedTx: { txData: { transactionBuffer, chainId } },
81
- })
82
- : undefined
83
-
84
- const l1DataFee = optimismL1DataFee
85
- ? asset.baseAsset.currency.baseUnit(optimismL1DataFee)
86
- : asset.baseAsset.currency.ZERO
87
-
88
- const fee = baseFee.add(l1DataFee)
89
129
  const extraFeeData = getExtraFeeData({ asset, amount, txValue })
90
130
  const unsignedTx = {
91
131
  txData: { transactionBuffer, chainId },
@@ -102,6 +142,7 @@ async function createUnsignedTxWithFees({
102
142
  return {
103
143
  unsignedTx,
104
144
  fee,
145
+ amount,
105
146
  extraFeeData,
106
147
  // exhcange compatibility until the use usignedTx, remove me!
107
148
  gasPrice,
@@ -110,6 +151,60 @@ async function createUnsignedTxWithFees({
110
151
  }
111
152
  }
112
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
+
113
208
  const resolveTxFactoryGasPrices = async ({
114
209
  assetClientInterface,
115
210
  baseAsset,
@@ -384,7 +479,12 @@ export const createTxFactory = ({ chainId, assetClientInterface, useAbsoluteNonc
384
479
  gasPrice: providedGasPrice,
385
480
  bip70,
386
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.
387
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,
388
488
  bumpTxId,
389
489
  }) => {
390
490
  assert(asset, 'asset is required')
@@ -520,6 +620,7 @@ export const createTxFactory = ({ chainId, assetClientInterface, useAbsoluteNonc
520
620
  bip70,
521
621
  amount: resolvedTxAttributes.amount,
522
622
  txType,
623
+ isSendAll,
523
624
  }))
524
625
 
525
626
  // HACK: We cannot ensure the no dust invariant for `isSendAll`
@@ -532,8 +633,21 @@ export const createTxFactory = ({ chainId, assetClientInterface, useAbsoluteNonc
532
633
  const isSendAllBaseAsset =
533
634
  isSendAll && !isEthereumLikeToken(asset) && !resolvedTxAttributes.isContractTxToAddress
534
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
+
535
649
  return createUnsignedTxWithFees({
536
- ...resolvedTxAttributes,
650
+ ...finalTxAttributes,
537
651
  asset,
538
652
  chainId,
539
653
  gasLimit,
@@ -545,7 +659,7 @@ export const createTxFactory = ({ chainId, assetClientInterface, useAbsoluteNonc
545
659
  // fixed gas cost transaction, no dust balance should remain,
546
660
  // since any deviation in the underlying `baseFeePerGas` will
547
661
  // result only affect the tip for the miner - no dust remains.
548
- tipGasPrice: isSendAllBaseAsset && eip1559Enabled ? gasPrice : tipGasPrice,
662
+ tipGasPrice: finalTipGasPrice,
549
663
  eip1559Enabled,
550
664
  })
551
665
  }