@exodus/bitcoin-api 4.15.8 → 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 +10 -0
- package/package.json +2 -2
- package/src/fee/utxo-selector.js +43 -16
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,16 @@
|
|
|
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
|
+
|
|
6
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)
|
|
7
17
|
|
|
8
18
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/bitcoin-api",
|
|
3
|
-
"version": "4.15.
|
|
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": "
|
|
66
|
+
"gitHead": "35f528942e03aa95179f6f8d6b42f1f2317cec5e"
|
|
67
67
|
}
|
package/src/fee/utxo-selector.js
CHANGED
|
@@ -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
|
|
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 >
|
|
113
|
-
(tx.data.changeAddress &&
|
|
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 =
|
|
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:
|
|
192
|
+
inputs: chainUtxos,
|
|
180
193
|
outputs: chainOutputs,
|
|
181
194
|
taprootInputWitnessSize,
|
|
182
195
|
})
|
|
183
|
-
|
|
184
|
-
|
|
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
|
|
211
|
-
ourRbfUtxos = ourRbfUtxos.union(
|
|
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(
|
|
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
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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(
|
|
308
|
+
if (selectedUtxos.value.lt(totalOutputAmount.add(fee))) {
|
|
282
309
|
return { fee }
|
|
283
310
|
}
|
|
284
311
|
|