@exodus/bitcoin-api 4.15.7 → 4.15.9

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,26 @@
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
+ ## [4.15.9](https://github.com/ExodusMovement/assets/compare/@exodus/bitcoin-api@4.15.8...@exodus/bitcoin-api@4.15.9) (2026-06-08)
7
+
8
+
9
+ ### Bug Fixes
10
+
11
+
12
+ * fix(bitcoin-api): prevent dust-only CPFP bump transactions (#8218)
13
+
14
+
15
+
16
+ ## [4.15.8](https://github.com/ExodusMovement/assets/compare/@exodus/bitcoin-api@4.15.7...@exodus/bitcoin-api@4.15.8) (2026-06-05)
17
+
18
+
19
+ ### Bug Fixes
20
+
21
+
22
+ * fix(bitcoin-api): increase insight address timeout and reduce retry delays for faster restore (#8216)
23
+
24
+
25
+
6
26
  ## [4.15.7](https://github.com/ExodusMovement/assets/compare/@exodus/bitcoin-api@4.15.6...@exodus/bitcoin-api@4.15.7) (2026-06-04)
7
27
 
8
28
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/bitcoin-api",
3
- "version": "4.15.7",
3
+ "version": "4.15.9",
4
4
  "description": "Bitcoin transaction and fee monitors, RPC with the blockchain node, other networking code.",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -63,5 +63,5 @@
63
63
  "type": "git",
64
64
  "url": "git+https://github.com/ExodusMovement/assets.git"
65
65
  },
66
- "gitHead": "046974709fffbbc24c47b833d8775d1f4decd839"
66
+ "gitHead": "35f528942e03aa95179f6f8d6b42f1f2317cec5e"
67
67
  }
@@ -31,6 +31,18 @@ const canReplaceSendAll = ({ isSendAll, usableUtxos, confirmedUtxosArray, replac
31
31
  return unconfirmedUtxoCount === replaceableTxUtxoCount
32
32
  }
33
33
 
34
+ const getCpfpOutputDust = ({ asset, amount, isBumpTx }) => {
35
+ if (!isBumpTx || !amount?.isZero) return asset.currency.ZERO
36
+ return getChangeDustValue(asset) ?? asset.currency.ZERO
37
+ }
38
+
39
+ const hasCpfpOutputAboveDust = ({ asset, selectedUtxos, amount, fee, isBumpTx }) => {
40
+ const cpfpOutputDust = getCpfpOutputDust({ asset, amount, isBumpTx })
41
+ if (cpfpOutputDust.isZero) return true
42
+
43
+ return selectedUtxos.value.sub(amount).sub(fee).gte(cpfpOutputDust)
44
+ }
45
+
34
46
  export const selectUtxos = ({
35
47
  asset,
36
48
  utxosDescendingOrder,
@@ -103,14 +115,14 @@ export const selectUtxos = ({
103
115
  // as inputs of the new transaction;
104
116
  if (tx.data.sent.length === 0) continue
105
117
 
106
- const changeUtxos = usableUtxos.getTxIdUtxos(tx.txId)
118
+ const internalUtxos = usableUtxos.getTxIdUtxos(tx.txId)
107
119
 
108
120
  // Do not replace a tx if any of its UTXOs were already spent by another tx.
109
121
  // Replacing would conflict with the child tx that spent the original output.
110
122
  // Use changeAddress as a legacy fallback for txs created before internalOutputs were introduced.
111
123
  if (
112
- (tx.data.internalOutputs && tx.data.internalOutputs.length > changeUtxos.size) ||
113
- (tx.data.changeAddress && changeUtxos.size === 0)
124
+ (tx.data.internalOutputs && tx.data.internalOutputs.length > internalUtxos.size) ||
125
+ (tx.data.changeAddress && internalUtxos.size === 0)
114
126
  ) {
115
127
  continue
116
128
  }
@@ -132,7 +144,7 @@ export const selectUtxos = ({
132
144
 
133
145
  let fee
134
146
  let additionalUtxos
135
- const replaceTxAmount = changeUtxos.value.add(tx.feeAmount)
147
+ const replaceTxAmount = internalUtxos.value.add(tx.feeAmount)
136
148
 
137
149
  if (isSendAll) {
138
150
  additionalUtxos = UtxoCollection.fromArray(confirmedUtxosArray, { currency })
@@ -175,14 +187,24 @@ export const selectUtxos = ({
175
187
 
176
188
  if (replaceTxAmount.add(additionalUtxos.value).gte(amount.add(fee))) {
177
189
  const chainOutputs = isSendAll ? receiveAddresses : [...receiveAddresses, changeAddressType]
190
+ const chainUtxos = internalUtxos.union(additionalUtxos)
178
191
  const chainFee = feeEstimator({
179
- inputs: changeUtxos.union(additionalUtxos),
192
+ inputs: chainUtxos,
180
193
  outputs: chainOutputs,
181
194
  taprootInputWitnessSize,
182
195
  })
183
- // If batching is same or more expensive than chaining, don't replace
184
- // Unless we are accelerating a tx with no internal outputs, then we must replace because it can't be chained
196
+ const hasValidCpfpChild = hasCpfpOutputAboveDust({
197
+ asset,
198
+ selectedUtxos: chainUtxos,
199
+ amount,
200
+ fee: chainFee,
201
+ isBumpTx,
202
+ })
203
+
204
+ // Only prefer CPFP when the child can keep a non-dust output and is cheaper than RBF/batching.
205
+ // If the original tx has no internal output, it cannot be chained, so RBF is required.
185
206
  if (
207
+ hasValidCpfpChild &&
186
208
  (!isBumpTx || tx.data.internalOutputs?.length || tx.data.changeAddress) &&
187
209
  fee.sub(tx.feeAmount).gte(chainFee)
188
210
  ) {
@@ -207,8 +229,8 @@ export const selectUtxos = ({
207
229
  if (replaceableTxs) {
208
230
  for (const tx of replaceableTxs) {
209
231
  if (!tx.data.internalOutputs?.length && !tx.data.changeAddress) continue
210
- const changeUtxos = usableUtxos.getTxIdUtxos(tx.txId)
211
- ourRbfUtxos = ourRbfUtxos.union(changeUtxos)
232
+ const internalUtxos = usableUtxos.getTxIdUtxos(tx.txId)
233
+ ourRbfUtxos = ourRbfUtxos.union(internalUtxos)
212
234
  }
213
235
  }
214
236
 
@@ -258,18 +280,23 @@ export const selectUtxos = ({
258
280
 
259
281
  // start figuring out fees
260
282
  const outputs = amount.isZero ? [changeAddressType] : [...receiveAddresses, changeAddressType]
283
+ const cpfpOutputDust = getCpfpOutputDust({ asset, amount, isBumpTx })
284
+ const totalOutputAmount = amount.add(cpfpOutputDust)
261
285
 
262
286
  let fee = feeEstimator({ inputs: selectedUtxos, outputs, taprootInputWitnessSize })
263
287
 
264
- while (selectedUtxos.value.lt(amount.add(fee))) {
288
+ while (selectedUtxos.value.lt(totalOutputAmount.add(fee))) {
265
289
  // We ran out of UTXOs, give up now
266
290
  if (remainingUtxosArray.length === 0) {
267
291
  // Try fee with no change
268
- fee = feeEstimator({
269
- inputs: selectedUtxos,
270
- outputs: receiveAddresses,
271
- taprootInputWitnessSize,
272
- })
292
+ if (cpfpOutputDust.isZero) {
293
+ fee = feeEstimator({
294
+ inputs: selectedUtxos,
295
+ outputs: receiveAddresses,
296
+ taprootInputWitnessSize,
297
+ })
298
+ }
299
+
273
300
  break
274
301
  }
275
302
 
@@ -278,7 +305,7 @@ export const selectUtxos = ({
278
305
  fee = feeEstimator({ inputs: selectedUtxos, outputs, taprootInputWitnessSize })
279
306
  }
280
307
 
281
- if (selectedUtxos.value.lt(amount.add(fee))) {
308
+ if (selectedUtxos.value.lt(totalOutputAmount.add(fee))) {
282
309
  return { fee }
283
310
  }
284
311
 
@@ -67,7 +67,7 @@ const fetchJson = async (
67
67
  }
68
68
 
69
69
  async function fetchJsonRetry(url, fetchOptions, httpErrorMessage) {
70
- const waitTimes = ['5s', '10s', '20s', '30s']
70
+ const waitTimes = ['2s', '5s', '10s', '20s']
71
71
  const fetchWithRetry = retry(fetchJson, { delayTimesMs: waitTimes })
72
72
  return fetchWithRetry(url, fetchOptions, false, httpErrorMessage)
73
73
  }
@@ -156,7 +156,7 @@ export default class InsightAPIClient {
156
156
  'Content-Type': 'application/json',
157
157
  },
158
158
  body: JSON.stringify({ addrs: addrs.join(',') }),
159
- timeout: 10_000,
159
+ timeout: 25_000,
160
160
  }
161
161
 
162
162
  return fetchJsonRetry(url, fetchOptions, INSIGHT_HTTP_ERROR_ADDRS_TXS_MESSAGE)