@exodus/bitcoin-api 4.9.3 → 4.9.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 +28 -0
- package/package.json +2 -2
- package/src/fee/can-bump-tx.js +15 -5
- package/src/fee/utxo-selector.js +76 -17
- package/src/tx-create/create-tx.js +18 -10
- package/src/tx-log/bitcoin-monitor-scanner.js +28 -136
- package/src/tx-log/bitcoin-monitor.js +2 -4
- package/src/tx-send/batch-tx.js +1 -1
- package/src/tx-send/index.js +1 -0
- package/src/tx-send/update-state.js +141 -3
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,34 @@
|
|
|
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.9.5](https://github.com/ExodusMovement/assets/compare/@exodus/bitcoin-api@4.9.4...@exodus/bitcoin-api@4.9.5) (2026-02-14)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
* fix: Remove ordinals paths from bitcoin monitor scanner (#7430)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
## [4.9.4](https://github.com/ExodusMovement/assets/compare/@exodus/bitcoin-api@4.9.3...@exodus/bitcoin-api@4.9.4) (2026-02-10)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
### Bug Fixes
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
* fix(bitcoin-api): Fix empty UTXO selection in BTC sends (#7370)
|
|
23
|
+
|
|
24
|
+
* fix(bitcoin-api): Gate send-all RBF eligibility (#7373)
|
|
25
|
+
|
|
26
|
+
* fix(bitcoin-api): Refine bump UTXO selection controls (#7372)
|
|
27
|
+
|
|
28
|
+
* fix(bitcoin-api): Replace changeAddress with internalOutputs (#7378)
|
|
29
|
+
|
|
30
|
+
* fix(bitcoin-api): Tighten RBF send-all replacement eligibility (#7371)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|
|
6
34
|
## [4.9.3](https://github.com/ExodusMovement/assets/compare/@exodus/bitcoin-api@4.9.2...@exodus/bitcoin-api@4.9.3) (2026-01-30)
|
|
7
35
|
|
|
8
36
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/bitcoin-api",
|
|
3
|
-
"version": "4.9.
|
|
3
|
+
"version": "4.9.5",
|
|
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",
|
|
@@ -61,5 +61,5 @@
|
|
|
61
61
|
"type": "git",
|
|
62
62
|
"url": "git+https://github.com/ExodusMovement/assets.git"
|
|
63
63
|
},
|
|
64
|
-
"gitHead": "
|
|
64
|
+
"gitHead": "8c4fcde522beb978df6a5099e0fa9e34bb4a7e04"
|
|
65
65
|
}
|
package/src/fee/can-bump-tx.js
CHANGED
|
@@ -80,8 +80,13 @@ const _canBumpTx = ({
|
|
|
80
80
|
return { errorMessage: 'already confirmed' }
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
// Can't bump
|
|
84
|
-
|
|
83
|
+
// Can't RBF-bump if any internal output is already spent.
|
|
84
|
+
// Replacing would conflict with the child tx that spent the original output.
|
|
85
|
+
if (
|
|
86
|
+
bumpTx &&
|
|
87
|
+
((bumpTx.data.internalOutputs && bumpTx.data.internalOutputs.length > changeUtxos.size) ||
|
|
88
|
+
(bumpTx.data.changeAddress && changeUtxos.size === 0))
|
|
89
|
+
) {
|
|
85
90
|
return {
|
|
86
91
|
errorMessage: 'already spent',
|
|
87
92
|
}
|
|
@@ -100,25 +105,30 @@ const _canBumpTx = ({
|
|
|
100
105
|
utxosDescendingOrder,
|
|
101
106
|
taprootInputWitnessSize,
|
|
102
107
|
changeAddressType,
|
|
108
|
+
isBumpTx: true,
|
|
103
109
|
})
|
|
104
110
|
if (replaceTx) return { bumpType: BumpType.RBF, bumpFee: fee.sub(replaceTx.feeAmount) }
|
|
105
111
|
}
|
|
106
112
|
|
|
107
|
-
const { fee } = selectUtxos({
|
|
113
|
+
const { selectedUtxos, fee } = selectUtxos({
|
|
108
114
|
asset,
|
|
109
115
|
usableUtxos,
|
|
110
116
|
feeRate,
|
|
111
117
|
receiveAddress: changeAddressType,
|
|
112
118
|
getFeeEstimator,
|
|
113
|
-
|
|
119
|
+
spendUtxos: changeUtxos,
|
|
120
|
+
forceSpendUtxos: true,
|
|
114
121
|
allowUnconfirmedRbfEnabledUtxos,
|
|
115
122
|
unconfirmedTxAncestor,
|
|
116
123
|
utxosDescendingOrder,
|
|
117
124
|
taprootInputWitnessSize,
|
|
118
125
|
changeAddressType,
|
|
126
|
+
isBumpTx: true,
|
|
119
127
|
})
|
|
120
128
|
|
|
121
|
-
return
|
|
129
|
+
return selectedUtxos?.size
|
|
130
|
+
? { bumpType: BumpType.CPFP, bumpFee: fee }
|
|
131
|
+
: { errorMessage: 'insufficient funds' }
|
|
122
132
|
}
|
|
123
133
|
|
|
124
134
|
const validateIsNumber = (number, name) => {
|
package/src/fee/utxo-selector.js
CHANGED
|
@@ -18,6 +18,19 @@ const getBestReceiveAddresses = ({ asset, receiveAddress }) => {
|
|
|
18
18
|
return ['bitcoin', 'bitcoinregtest', 'bitcointestnet'].includes(asset.name) ? 'P2WSH' : 'P2PKH'
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
// Send-all replacement is limited to a single replaceable tx in this implementation.
|
|
22
|
+
// Multi-tx RBF is possible, but we don't implement it here due to complexity.
|
|
23
|
+
// We also require all unconfirmed UTXOs to belong to that tx to avoid leftover
|
|
24
|
+
// unconfirmed balance.
|
|
25
|
+
const canReplaceSendAll = ({ isSendAll, usableUtxos, confirmedUtxosArray, replaceableTxs }) => {
|
|
26
|
+
if (!isSendAll) return true
|
|
27
|
+
if (!replaceableTxs || replaceableTxs.length !== 1) return false
|
|
28
|
+
|
|
29
|
+
const unconfirmedUtxoCount = usableUtxos.size - confirmedUtxosArray.length
|
|
30
|
+
const replaceableTxUtxoCount = usableUtxos.getTxIdUtxos(replaceableTxs[0].txId).size
|
|
31
|
+
return unconfirmedUtxoCount === replaceableTxUtxoCount
|
|
32
|
+
}
|
|
33
|
+
|
|
21
34
|
export const selectUtxos = ({
|
|
22
35
|
asset,
|
|
23
36
|
utxosDescendingOrder,
|
|
@@ -30,7 +43,9 @@ export const selectUtxos = ({
|
|
|
30
43
|
isSendAll,
|
|
31
44
|
getFeeEstimator,
|
|
32
45
|
disableReplacement = false,
|
|
33
|
-
|
|
46
|
+
spendUtxos,
|
|
47
|
+
forceSpendUtxos = false,
|
|
48
|
+
isBumpTx = false,
|
|
34
49
|
allowUnconfirmedRbfEnabledUtxos,
|
|
35
50
|
unconfirmedTxAncestor,
|
|
36
51
|
taprootInputWitnessSize,
|
|
@@ -53,20 +68,53 @@ export const selectUtxos = ({
|
|
|
53
68
|
const { currency } = asset
|
|
54
69
|
if (!amount) amount = currency.ZERO
|
|
55
70
|
|
|
56
|
-
// We can only replace for a sendAll if only 1 replaceable tx and no unconfirmed utxos
|
|
57
71
|
const confirmedUtxosArray = getConfirmedUtxos({ asset, utxos: usableUtxos }).toArray()
|
|
58
72
|
const canReplace =
|
|
59
|
-
!
|
|
73
|
+
!forceSpendUtxos &&
|
|
60
74
|
!disableReplacement &&
|
|
61
75
|
replaceableTxs &&
|
|
62
|
-
(
|
|
63
|
-
(replaceableTxs.length === 1 && confirmedUtxosArray.length === usableUtxos.size - 1))
|
|
76
|
+
canReplaceSendAll({ isSendAll, usableUtxos, confirmedUtxosArray, replaceableTxs })
|
|
64
77
|
|
|
65
78
|
if (canReplace) {
|
|
66
79
|
for (const tx of replaceableTxs) {
|
|
80
|
+
// We do not RBF‑replace transactions with no external (sent) outputs (self‑sends).
|
|
81
|
+
//
|
|
82
|
+
// Reason: if the original replaceable tx was created outside this wallet,
|
|
83
|
+
// we do not have the original intent metadata that tells us which internal
|
|
84
|
+
// output was the user’s destination and which output was wallet‑generated
|
|
85
|
+
// change.
|
|
86
|
+
//
|
|
87
|
+
// This ambiguity is real:
|
|
88
|
+
// - A user can intentionally self‑send to a change address, and the wallet
|
|
89
|
+
// can still add another change output, resulting in multiple change outputs.
|
|
90
|
+
// - In single‑address mode, receive and change resolve to the same address,
|
|
91
|
+
// so outputs are indistinguishable.
|
|
92
|
+
//
|
|
93
|
+
// If we keep all internal outputs (change and self send outputs) during replacement, the tx grows, fees
|
|
94
|
+
// increase, and the user sees multiple internal outputs (confusing UX).
|
|
95
|
+
// If we treat any internal output as change via heuristics, we can violate user intent.
|
|
96
|
+
//
|
|
97
|
+
// Our solution:
|
|
98
|
+
// To avoid incorrect behavior and confusing UX, we do not RBF‑replace
|
|
99
|
+
// self‑send transactions. We instead attempt to fee‑bump them via chaining
|
|
100
|
+
// (CPFP) when possible.
|
|
101
|
+
//
|
|
102
|
+
// During replacement, internal outputs are removed and their output values are treated
|
|
103
|
+
// as inputs of the new transaction;
|
|
104
|
+
if (tx.data.sent.length === 0) continue
|
|
105
|
+
|
|
67
106
|
const changeUtxos = usableUtxos.getTxIdUtxos(tx.txId)
|
|
68
|
-
|
|
69
|
-
|
|
107
|
+
|
|
108
|
+
// Do not replace a tx if any of its UTXOs were already spent by another tx.
|
|
109
|
+
// Replacing would conflict with the child tx that spent the original output.
|
|
110
|
+
// Use changeAddress as a legacy fallback for txs created before internalOutputs were introduced.
|
|
111
|
+
if (
|
|
112
|
+
(tx.data.internalOutputs && tx.data.internalOutputs.length > changeUtxos.size) ||
|
|
113
|
+
(tx.data.changeAddress && changeUtxos.size === 0)
|
|
114
|
+
) {
|
|
115
|
+
continue
|
|
116
|
+
}
|
|
117
|
+
|
|
70
118
|
const feePerKB =
|
|
71
119
|
tx.data.feePerKB + MIN_RELAY_FEE > feeRate.toBaseNumber()
|
|
72
120
|
? currency.baseUnit(tx.data.feePerKB + MIN_RELAY_FEE)
|
|
@@ -125,7 +173,7 @@ export const selectUtxos = ({
|
|
|
125
173
|
}
|
|
126
174
|
}
|
|
127
175
|
|
|
128
|
-
if (
|
|
176
|
+
if (replaceTxAmount.add(additionalUtxos.value).gte(amount.add(fee))) {
|
|
129
177
|
const chainOutputs = isSendAll ? receiveAddresses : [...receiveAddresses, changeAddressType]
|
|
130
178
|
const chainFee = feeEstimator({
|
|
131
179
|
inputs: changeUtxos.union(additionalUtxos),
|
|
@@ -133,8 +181,11 @@ export const selectUtxos = ({
|
|
|
133
181
|
taprootInputWitnessSize,
|
|
134
182
|
})
|
|
135
183
|
// If batching is same or more expensive than chaining, don't replace
|
|
136
|
-
// Unless we are accelerating a
|
|
137
|
-
if (
|
|
184
|
+
// Unless we are accelerating a tx with no internal outputs, then we must replace because it can't be chained
|
|
185
|
+
if (
|
|
186
|
+
(!isBumpTx || tx.data.internalOutputs?.length || tx.data.changeAddress) &&
|
|
187
|
+
fee.sub(tx.feeAmount).gte(chainFee)
|
|
188
|
+
) {
|
|
138
189
|
continue
|
|
139
190
|
}
|
|
140
191
|
|
|
@@ -143,13 +194,19 @@ export const selectUtxos = ({
|
|
|
143
194
|
}
|
|
144
195
|
}
|
|
145
196
|
|
|
146
|
-
if
|
|
197
|
+
// For bumping: if RBF is not possible, the only valid path is CPFP,
|
|
198
|
+
// and only if spendUtxos are available.
|
|
199
|
+
if (isBumpTx && !spendUtxos?.size) {
|
|
200
|
+
return { selectedUtxos: undefined, fee: undefined, replaceTx: undefined }
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (!spendUtxos) spendUtxos = UtxoCollection.createEmpty({ currency })
|
|
147
204
|
|
|
148
205
|
// We can still spend our rbf utxos, but put them last
|
|
149
206
|
let ourRbfUtxos = UtxoCollection.createEmpty({ currency })
|
|
150
207
|
if (replaceableTxs) {
|
|
151
208
|
for (const tx of replaceableTxs) {
|
|
152
|
-
if (!tx.data.changeAddress) continue
|
|
209
|
+
if (!tx.data.internalOutputs?.length && !tx.data.changeAddress) continue
|
|
153
210
|
const changeUtxos = usableUtxos.getTxIdUtxos(tx.txId)
|
|
154
211
|
ourRbfUtxos = ourRbfUtxos.union(changeUtxos)
|
|
155
212
|
}
|
|
@@ -181,14 +238,14 @@ export const selectUtxos = ({
|
|
|
181
238
|
}
|
|
182
239
|
|
|
183
240
|
// quickly add utxos to get to amount before starting to figure out fees, the minimum place to start is as much as the amount
|
|
184
|
-
const selectedUtxosArray =
|
|
241
|
+
const selectedUtxosArray = spendUtxos.toArray()
|
|
185
242
|
let selectedUtxosValue = selectedUtxosArray.reduce(
|
|
186
243
|
(total, utxo) => total.add(utxo.value),
|
|
187
244
|
currency.ZERO
|
|
188
245
|
)
|
|
189
246
|
const remainingUtxosArray = _toPriorityOrderedArray({
|
|
190
247
|
utxosDescendingOrder,
|
|
191
|
-
utxos: spendableUtxos.union(ourRbfUtxos).difference(
|
|
248
|
+
utxos: spendableUtxos.union(ourRbfUtxos).difference(spendUtxos),
|
|
192
249
|
})
|
|
193
250
|
|
|
194
251
|
while (selectedUtxosValue.lte(amount) && remainingUtxosArray.length > 0) {
|
|
@@ -238,7 +295,8 @@ export const getUtxosData = ({
|
|
|
238
295
|
isSendAll,
|
|
239
296
|
getFeeEstimator,
|
|
240
297
|
disableReplacement,
|
|
241
|
-
|
|
298
|
+
spendUtxos,
|
|
299
|
+
forceSpendUtxos,
|
|
242
300
|
allowUnconfirmedRbfEnabledUtxos,
|
|
243
301
|
unconfirmedTxAncestor,
|
|
244
302
|
utxosDescendingOrder,
|
|
@@ -259,7 +317,8 @@ export const getUtxosData = ({
|
|
|
259
317
|
isSendAll,
|
|
260
318
|
getFeeEstimator,
|
|
261
319
|
disableReplacement,
|
|
262
|
-
|
|
320
|
+
spendUtxos,
|
|
321
|
+
forceSpendUtxos,
|
|
263
322
|
allowUnconfirmedRbfEnabledUtxos,
|
|
264
323
|
unconfirmedTxAncestor,
|
|
265
324
|
utxosDescendingOrder,
|
|
@@ -280,7 +339,7 @@ export const getUtxosData = ({
|
|
|
280
339
|
|
|
281
340
|
const spendableBalance = spendableUtxos.value
|
|
282
341
|
|
|
283
|
-
const extraFeeCpfp = selectedUtxos
|
|
342
|
+
const extraFeeCpfp = selectedUtxos?.size
|
|
284
343
|
? asset.currency.baseUnit(
|
|
285
344
|
getExtraFee({
|
|
286
345
|
asset,
|
|
@@ -90,24 +90,26 @@ function processAddress({ asset, toAddress }) {
|
|
|
90
90
|
// Determine strategy for transaction bumping (RBF vs CPFP)
|
|
91
91
|
function determineBumpStrategy({ bumpTxId, replaceableTxs, usableUtxos }) {
|
|
92
92
|
if (!bumpTxId) {
|
|
93
|
-
return { replaceableTxs, utxosToBump: undefined }
|
|
93
|
+
return { replaceableTxs, utxosToBump: undefined, forceUtxosToBump: false }
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
const utxosToBump = usableUtxos.getTxIdUtxos(bumpTxId)
|
|
97
|
+
|
|
96
98
|
// Check if we can use RBF (Replace-By-Fee)
|
|
97
99
|
const bumpTx = replaceableTxs.find(({ txId }) => txId === bumpTxId)
|
|
98
100
|
|
|
99
101
|
if (bumpTx) {
|
|
100
|
-
// Use RBF: replace the transaction directly
|
|
101
|
-
|
|
102
|
+
// Use RBF: replace the transaction directly.
|
|
103
|
+
// If CPFP is cheaper, then we will use that instead of RBF.
|
|
104
|
+
return { replaceableTxs: [bumpTx], utxosToBump, forceUtxosToBump: false }
|
|
102
105
|
}
|
|
103
106
|
|
|
104
|
-
// Otherwise try CPFP (Child-Pays-For-Parent) by spending the transaction's outputs
|
|
105
|
-
const utxosToBump = usableUtxos.getTxIdUtxos(bumpTxId)
|
|
107
|
+
// Otherwise only option is to try CPFP (Child-Pays-For-Parent) by spending the transaction's outputs
|
|
106
108
|
if (utxosToBump.size === 0) {
|
|
107
109
|
throw new Error(`Cannot bump transaction ${bumpTxId}`)
|
|
108
110
|
}
|
|
109
111
|
|
|
110
|
-
return { replaceableTxs: [], utxosToBump }
|
|
112
|
+
return { replaceableTxs: [], utxosToBump, forceUtxosToBump: true }
|
|
111
113
|
}
|
|
112
114
|
|
|
113
115
|
// Prepare parameters for UTXO selection
|
|
@@ -387,7 +389,11 @@ const transferHandler = {
|
|
|
387
389
|
let replaceableTxs = findUnconfirmedSentRbfTxs(context.txSet)
|
|
388
390
|
|
|
389
391
|
// Determine bumping strategy (RBF or CPFP)
|
|
390
|
-
const {
|
|
392
|
+
const {
|
|
393
|
+
replaceableTxs: updatedReplaceableTxs,
|
|
394
|
+
utxosToBump,
|
|
395
|
+
forceUtxosToBump,
|
|
396
|
+
} = determineBumpStrategy({
|
|
391
397
|
bumpTxId,
|
|
392
398
|
replaceableTxs,
|
|
393
399
|
usableUtxos: context.usableUtxos,
|
|
@@ -418,7 +424,9 @@ const transferHandler = {
|
|
|
418
424
|
isSendAll: utxoParams.resolvedIsSendAll,
|
|
419
425
|
getFeeEstimator: (asset, { feePerKB, ...options }) =>
|
|
420
426
|
getFeeEstimator(asset, feePerKB, options),
|
|
421
|
-
|
|
427
|
+
spendUtxos: utxosToBump,
|
|
428
|
+
forceSpendUtxos: forceUtxosToBump,
|
|
429
|
+
isBumpTx: !!bumpTxId,
|
|
422
430
|
allowUnconfirmedRbfEnabledUtxos,
|
|
423
431
|
unconfirmedTxAncestor: context.unconfirmedTxAncestor,
|
|
424
432
|
utxosDescendingOrder,
|
|
@@ -426,7 +434,7 @@ const transferHandler = {
|
|
|
426
434
|
changeAddressType,
|
|
427
435
|
})
|
|
428
436
|
|
|
429
|
-
if (!selectedUtxos && !replaceTx) {
|
|
437
|
+
if (!selectedUtxos?.size && !replaceTx) {
|
|
430
438
|
throw new Error('Not enough funds.')
|
|
431
439
|
}
|
|
432
440
|
|
|
@@ -435,7 +443,7 @@ const transferHandler = {
|
|
|
435
443
|
// then something is wrong because we can't actually bump the tx.
|
|
436
444
|
// This shouldn't happen but might due to either the tx confirming before accelerate was
|
|
437
445
|
// pressed, or if the change was already spent from another wallet.
|
|
438
|
-
if (bumpTxId && (!selectedUtxos || (replaceTx && replaceTx.txId !== bumpTxId))) {
|
|
446
|
+
if (bumpTxId && (!selectedUtxos?.size || (replaceTx && replaceTx.txId !== bumpTxId))) {
|
|
439
447
|
throw new Error(`Unable to bump ${bumpTxId}`)
|
|
440
448
|
}
|
|
441
449
|
|
|
@@ -5,9 +5,7 @@ import ms from 'ms'
|
|
|
5
5
|
|
|
6
6
|
import { isChangeAddress, isReceiveAddress } from '../address-utils.js'
|
|
7
7
|
import { orderTxs } from '../insight-api-client/util.js'
|
|
8
|
-
import {
|
|
9
|
-
import { getOrdinalsUtxos, getUtxos, partitionUtxos } from '../utxos-utils.js'
|
|
10
|
-
import { indexOrdinalUnconfirmedTx } from './ordinals-indexer-utils.js'
|
|
8
|
+
import { getUtxos } from '../utxos-utils.js'
|
|
11
9
|
|
|
12
10
|
const { compact, isEqual, uniq } = lodash
|
|
13
11
|
|
|
@@ -23,8 +21,6 @@ export class BitcoinMonitorScanner {
|
|
|
23
21
|
#txFetchLimitResolver
|
|
24
22
|
#shouldExcludeVoutUtxo
|
|
25
23
|
#yieldToUI
|
|
26
|
-
#ordinalsEnabled
|
|
27
|
-
#ordinalChainIndex
|
|
28
24
|
#extraChainIndexEnabled
|
|
29
25
|
#extraChainIndex
|
|
30
26
|
#gapLimit
|
|
@@ -36,8 +32,6 @@ export class BitcoinMonitorScanner {
|
|
|
36
32
|
yieldToUI = () => {},
|
|
37
33
|
shouldExcludeVoutUtxo = () => false,
|
|
38
34
|
txFetchLimitResolver = ({ refresh }) => (refresh ? 50 : 10),
|
|
39
|
-
ordinalsEnabled,
|
|
40
|
-
ordinalChainIndex,
|
|
41
35
|
extraChainIndexEnabled,
|
|
42
36
|
extraChainIndex,
|
|
43
37
|
gapLimit = 10,
|
|
@@ -57,8 +51,6 @@ export class BitcoinMonitorScanner {
|
|
|
57
51
|
this.#assetClientInterface = assetClientInterface
|
|
58
52
|
this.#txFetchLimitResolver = txFetchLimitResolver
|
|
59
53
|
this.#shouldExcludeVoutUtxo = shouldExcludeVoutUtxo
|
|
60
|
-
this.#ordinalsEnabled = ordinalsEnabled
|
|
61
|
-
this.#ordinalChainIndex = ordinalChainIndex
|
|
62
54
|
this.#extraChainIndexEnabled = extraChainIndexEnabled
|
|
63
55
|
this.#extraChainIndex = extraChainIndex
|
|
64
56
|
this.#gapLimit = gapLimit
|
|
@@ -92,9 +84,6 @@ export class BitcoinMonitorScanner {
|
|
|
92
84
|
const multiAddressMode = assetConfig.multiAddressMode ?? true
|
|
93
85
|
|
|
94
86
|
const storedUtxos = getUtxos({ asset, accountState })
|
|
95
|
-
const storedOrdinalUtxos = getOrdinalsUtxos({ asset, accountState })
|
|
96
|
-
|
|
97
|
-
const currentStoredUtxos = storedUtxos.union(storedOrdinalUtxos)
|
|
98
87
|
|
|
99
88
|
const currentTime = Date.now()
|
|
100
89
|
const unconfirmedTxsToCheck = [...currentTxs].reduce((txs, tx) => {
|
|
@@ -116,9 +105,7 @@ export class BitcoinMonitorScanner {
|
|
|
116
105
|
: (txs) =>
|
|
117
106
|
txs.filter((tx) => {
|
|
118
107
|
const txItem = currentTxs.get(tx.txid)
|
|
119
|
-
|
|
120
|
-
const inscriptionsIndexed = txItem?.data?.inscriptionsIndexed
|
|
121
|
-
return confirmed && (!this.#ordinalsEnabled || inscriptionsIndexed)
|
|
108
|
+
return txItem && txItem.confirmed
|
|
122
109
|
}).length >= txs.length
|
|
123
110
|
|
|
124
111
|
const unusedAddressIndexes = await assetClientInterface.getUnusedAddressIndexes({
|
|
@@ -254,28 +241,7 @@ export class BitcoinMonitorScanner {
|
|
|
254
241
|
}
|
|
255
242
|
|
|
256
243
|
const listOfTxs = await Promise.all(promises)
|
|
257
|
-
|
|
258
|
-
if (!this.#ordinalsEnabled) {
|
|
259
|
-
return insightTxs
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
return Promise.all(
|
|
263
|
-
insightTxs.map((tx) => {
|
|
264
|
-
try {
|
|
265
|
-
return indexOrdinalUnconfirmedTx({
|
|
266
|
-
tx,
|
|
267
|
-
currency: this.#asset.currency,
|
|
268
|
-
insightClient,
|
|
269
|
-
})
|
|
270
|
-
} catch (e) {
|
|
271
|
-
console.warn(
|
|
272
|
-
`Could not index ${asset.name} ordinal tx ${tx.txid} for wallet account ${walletAccount}. message: ${e.message}`,
|
|
273
|
-
e
|
|
274
|
-
)
|
|
275
|
-
return tx
|
|
276
|
-
}
|
|
277
|
-
})
|
|
278
|
-
)
|
|
244
|
+
return listOfTxs.flat()
|
|
279
245
|
}
|
|
280
246
|
|
|
281
247
|
const gapSearchParameters = newChains.map(({ purpose, chain }) => {
|
|
@@ -305,21 +271,6 @@ export class BitcoinMonitorScanner {
|
|
|
305
271
|
}
|
|
306
272
|
)
|
|
307
273
|
|
|
308
|
-
if (
|
|
309
|
-
fetchCount === 0 &&
|
|
310
|
-
this.#ordinalsEnabled &&
|
|
311
|
-
this.#ordinalChainIndex > 1 &&
|
|
312
|
-
purposes.includes(86)
|
|
313
|
-
) {
|
|
314
|
-
// this is the ordinal address
|
|
315
|
-
chainObjects.push({
|
|
316
|
-
purpose: 86,
|
|
317
|
-
chainIndex: this.#ordinalChainIndex,
|
|
318
|
-
startAddressIndex: 0,
|
|
319
|
-
endAddressIndex: 1,
|
|
320
|
-
})
|
|
321
|
-
}
|
|
322
|
-
|
|
323
274
|
if (
|
|
324
275
|
fetchCount === 0 &&
|
|
325
276
|
this.#extraChainIndexEnabled &&
|
|
@@ -373,10 +324,7 @@ export class BitcoinMonitorScanner {
|
|
|
373
324
|
const addressString = String(address)
|
|
374
325
|
const purposeToUpdate = purposeMap[addressString]
|
|
375
326
|
|
|
376
|
-
if (
|
|
377
|
-
metaAddressIndex === undefined ||
|
|
378
|
-
(metaChainIndex === this.#ordinalChainIndex && this.#ordinalChainIndex > 1)
|
|
379
|
-
) {
|
|
327
|
+
if (metaAddressIndex === undefined) {
|
|
380
328
|
return
|
|
381
329
|
}
|
|
382
330
|
|
|
@@ -455,20 +403,12 @@ export class BitcoinMonitorScanner {
|
|
|
455
403
|
feePerKB: txItem.fees ? (txItem.fees / txItem.vsize) * 1000 * 1e8 : null,
|
|
456
404
|
rbfEnabled: txItem.rbf,
|
|
457
405
|
blocksSeen: 0,
|
|
406
|
+
internalOutputs: [],
|
|
458
407
|
...(isCoinbase ? { isCoinbase: true } : undefined),
|
|
459
408
|
},
|
|
460
409
|
currencies: { [assetName]: currency },
|
|
461
410
|
}
|
|
462
411
|
|
|
463
|
-
if (this.#ordinalsEnabled) {
|
|
464
|
-
txLogItem.data = {
|
|
465
|
-
...txLogItem.data,
|
|
466
|
-
inscriptionsIndexed: txItem.inscriptionsIndexed,
|
|
467
|
-
sentInscriptions: [],
|
|
468
|
-
receivedInscriptions: [],
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
|
|
472
412
|
let from = []
|
|
473
413
|
|
|
474
414
|
// if txItem.vin has an address that matches ours, means we've spent this tx
|
|
@@ -494,14 +434,6 @@ export class BitcoinMonitorScanner {
|
|
|
494
434
|
txLogItem.coinAmount = txLogItem.coinAmount.sub(currency.defaultUnit(vin.value))
|
|
495
435
|
isSent = true
|
|
496
436
|
txLogItem.data.sent = []
|
|
497
|
-
if (this.#ordinalsEnabled && vin.inscriptions) {
|
|
498
|
-
txLogItem.data.sentInscriptions.push(
|
|
499
|
-
...vin.inscriptions.map((i) => ({
|
|
500
|
-
...i,
|
|
501
|
-
value: currency.defaultUnit(vin.value).toBaseNumber(),
|
|
502
|
-
}))
|
|
503
|
-
)
|
|
504
|
-
}
|
|
505
437
|
|
|
506
438
|
// this is only used to exclude the utxos in the reducer which is why we don't care about the other fields
|
|
507
439
|
utxosToRemove.push({
|
|
@@ -565,18 +497,23 @@ export class BitcoinMonitorScanner {
|
|
|
565
497
|
txLogItem.addresses.push(address)
|
|
566
498
|
}
|
|
567
499
|
|
|
500
|
+
// keeping this for backwards compatibility to not risk breaking any unknown code that depends on the change address being present
|
|
568
501
|
if (isSent && isChangeAddress(address)) {
|
|
569
502
|
txLogItem.data.changeAddress = address
|
|
570
503
|
}
|
|
571
504
|
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
505
|
+
// Internal outputs are any outputs that pay into this wallet.
|
|
506
|
+
// For sent txs, that includes change and self‑send outputs.
|
|
507
|
+
// For incoming txs, it includes normal receive outputs.
|
|
508
|
+
//
|
|
509
|
+
// We do not try to infer which internal output is the user’s intent
|
|
510
|
+
// vs wallet change, because it is ambiguous:
|
|
511
|
+
// - Users can self‑send to a change address and still get an extra change output.
|
|
512
|
+
// - In single‑address mode, receive and change outputs have same address.
|
|
513
|
+
txLogItem.data.internalOutputs.push({
|
|
514
|
+
address,
|
|
515
|
+
amount: currency.defaultUnit(vout.value).toBaseString(),
|
|
516
|
+
})
|
|
580
517
|
|
|
581
518
|
// it was sent to us...
|
|
582
519
|
const val = currency.defaultUnit(vout.value)
|
|
@@ -594,11 +531,6 @@ export class BitcoinMonitorScanner {
|
|
|
594
531
|
...(isCoinbase ? { isCoinbase: true } : undefined),
|
|
595
532
|
}
|
|
596
533
|
|
|
597
|
-
if (this.#ordinalsEnabled) {
|
|
598
|
-
output.inscriptionsIndexed = txItem.inscriptionsIndexed
|
|
599
|
-
output.inscriptions = vout.inscriptions || []
|
|
600
|
-
}
|
|
601
|
-
|
|
602
534
|
if (this.#shouldExcludeVoutUtxo({ asset, output, txItem, vout })) {
|
|
603
535
|
utxosToRemove.push({
|
|
604
536
|
address,
|
|
@@ -667,7 +599,7 @@ export class BitcoinMonitorScanner {
|
|
|
667
599
|
|
|
668
600
|
const utxosToRemoveCol = UtxoCollection.fromArray(utxosToRemove, { currency })
|
|
669
601
|
// Keep new utxos when they intersect with the stored utxos.
|
|
670
|
-
utxoCol = utxoCol.union(
|
|
602
|
+
utxoCol = utxoCol.union(storedUtxos).difference(utxosToRemoveCol)
|
|
671
603
|
|
|
672
604
|
const pendingDropCandidates = Object.values(unconfirmedTxsToCheck)
|
|
673
605
|
const verificationResults = await Promise.all(
|
|
@@ -712,7 +644,7 @@ export class BitcoinMonitorScanner {
|
|
|
712
644
|
}
|
|
713
645
|
|
|
714
646
|
// no changes, ignore
|
|
715
|
-
if (utxoCol.equals(
|
|
647
|
+
if (utxoCol.equals(storedUtxos)) {
|
|
716
648
|
utxoCol = null
|
|
717
649
|
}
|
|
718
650
|
|
|
@@ -722,28 +654,10 @@ export class BitcoinMonitorScanner {
|
|
|
722
654
|
return !isEqual(chain, originalChain.chain)
|
|
723
655
|
})
|
|
724
656
|
|
|
725
|
-
const ordinalAddress = await this.getOrdinalAddress({ walletAccount })
|
|
726
|
-
|
|
727
|
-
// latest account state, in case knownBalanceUtxoIds or mustAvoidUtxoIds gets updated in on another promise
|
|
728
|
-
const latestAccountState = await assetClientInterface.getAccountState({
|
|
729
|
-
assetName,
|
|
730
|
-
walletAccount,
|
|
731
|
-
})
|
|
732
|
-
const utxosData = utxoCol
|
|
733
|
-
? partitionUtxos({
|
|
734
|
-
allUtxos: utxoCol,
|
|
735
|
-
ordinalsEnabled: this.#ordinalsEnabled,
|
|
736
|
-
ordinalAddress,
|
|
737
|
-
knownBalanceUtxoIds: latestAccountState.knownBalanceUtxoIds,
|
|
738
|
-
mustAvoidUtxoIds: latestAccountState.mustAvoidUtxoIds,
|
|
739
|
-
additionalInscriptions: latestAccountState.additionalInscriptions,
|
|
740
|
-
})
|
|
741
|
-
: {}
|
|
742
|
-
|
|
743
657
|
return {
|
|
744
658
|
txsToUpdate: existingTxs,
|
|
745
659
|
txsToAdd: newTxs,
|
|
746
|
-
...
|
|
660
|
+
...(utxoCol ? { utxos: utxoCol } : {}),
|
|
747
661
|
changedUnusedAddressIndexes,
|
|
748
662
|
}
|
|
749
663
|
}
|
|
@@ -756,8 +670,6 @@ export class BitcoinMonitorScanner {
|
|
|
756
670
|
const accountState = await aci.getAccountState({ assetName, walletAccount })
|
|
757
671
|
|
|
758
672
|
const storedUtxos = getUtxos({ accountState, asset })
|
|
759
|
-
const storedOrdinalsUtxos = getOrdinalsUtxos({ accountState, asset })
|
|
760
|
-
const allStoredUtxos = storedUtxos.union(storedOrdinalsUtxos)
|
|
761
673
|
|
|
762
674
|
const currentTxs = [...(await aci.getTxLog({ assetName, walletAccount }))]
|
|
763
675
|
|
|
@@ -804,38 +716,18 @@ export class BitcoinMonitorScanner {
|
|
|
804
716
|
})
|
|
805
717
|
.filter((tx) => Object.keys(tx).length > 1)
|
|
806
718
|
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
const
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
const latestAccountState = await aci.getAccountState({
|
|
813
|
-
assetName,
|
|
814
|
-
walletAccount,
|
|
719
|
+
// Check before updateConfirmations since it mutates the shared utxo object references,
|
|
720
|
+
// which would cause storedUtxos.toJSON() to also reflect the updated confirmations.
|
|
721
|
+
const utxosChanged = storedUtxos.toArray().some((utxo) => {
|
|
722
|
+
const update = confirmationsList.find(({ txId }) => utxo.txId === txId)
|
|
723
|
+
return update && utxo.confirmations !== update.confirmations
|
|
815
724
|
})
|
|
816
725
|
|
|
817
|
-
const
|
|
818
|
-
allUtxos: txConfirmedUtxos,
|
|
819
|
-
ordinalsEnabled: this.#ordinalsEnabled,
|
|
820
|
-
ordinalAddress,
|
|
821
|
-
knownBalanceUtxoIds: latestAccountState.knownBalanceUtxoIds,
|
|
822
|
-
mustAvoidUtxoIds: latestAccountState.mustAvoidUtxoIds,
|
|
823
|
-
additionalInscriptions: latestAccountState.additionalInscriptions,
|
|
824
|
-
})
|
|
726
|
+
const txConfirmedUtxos = storedUtxos.updateConfirmations(confirmationsList)
|
|
825
727
|
|
|
826
728
|
return {
|
|
827
|
-
utxos:
|
|
828
|
-
ordinalsUtxos: ordinalsUtxos.equals(storedOrdinalsUtxos) ? null : ordinalsUtxos,
|
|
729
|
+
utxos: utxosChanged ? txConfirmedUtxos : null,
|
|
829
730
|
txsToUpdate: updatedPropertiesTxs,
|
|
830
731
|
}
|
|
831
732
|
}
|
|
832
|
-
|
|
833
|
-
getOrdinalAddress({ walletAccount }) {
|
|
834
|
-
return getOrdinalAddress({
|
|
835
|
-
asset: this.#asset,
|
|
836
|
-
assetClientInterface: this.#assetClientInterface,
|
|
837
|
-
walletAccount,
|
|
838
|
-
ordinalChainIndex: this.#ordinalChainIndex,
|
|
839
|
-
})
|
|
840
|
-
}
|
|
841
733
|
}
|
|
@@ -166,13 +166,12 @@ export class Monitor extends BaseMonitor {
|
|
|
166
166
|
const assetName = asset.name
|
|
167
167
|
const walletAccounts = await aci.getWalletAccounts({ assetName })
|
|
168
168
|
for (const walletAccount of walletAccounts) {
|
|
169
|
-
const { txsToUpdate, utxos
|
|
169
|
+
const { txsToUpdate, utxos } = await this.#scanner.rescanOnNewBlock({
|
|
170
170
|
walletAccount,
|
|
171
171
|
})
|
|
172
172
|
|
|
173
173
|
const newData = {}
|
|
174
174
|
if (utxos) newData.utxos = utxos
|
|
175
|
-
if (ordinalsUtxos) newData.ordinalsUtxos = ordinalsUtxos
|
|
176
175
|
|
|
177
176
|
if (txsToUpdate.length > 0) {
|
|
178
177
|
await this.updateTxLog({ assetName, walletAccount, logItems: txsToUpdate })
|
|
@@ -237,7 +236,7 @@ export class Monitor extends BaseMonitor {
|
|
|
237
236
|
// wait for all wallet accounts to load
|
|
238
237
|
await aci.getWalletAccounts({ assetName })
|
|
239
238
|
|
|
240
|
-
const { txsToAdd, txsToUpdate, utxos,
|
|
239
|
+
const { txsToAdd, txsToUpdate, utxos, changedUnusedAddressIndexes } =
|
|
241
240
|
await this.#scanner.rescanBlockchainInsight({
|
|
242
241
|
walletAccount,
|
|
243
242
|
refresh,
|
|
@@ -245,7 +244,6 @@ export class Monitor extends BaseMonitor {
|
|
|
245
244
|
|
|
246
245
|
const newData = {}
|
|
247
246
|
if (utxos) newData.utxos = utxos
|
|
248
|
-
if (ordinalsUtxos) newData.ordinalsUtxos = ordinalsUtxos
|
|
249
247
|
|
|
250
248
|
if (!isEmpty(changedUnusedAddressIndexes)) {
|
|
251
249
|
// Only for mobile atm, browser and hydra calculates from the latest txLogs
|
package/src/tx-send/batch-tx.js
CHANGED
|
@@ -58,7 +58,7 @@ export const getCreateBatchTransaction = ({
|
|
|
58
58
|
allowUnconfirmedRbfEnabledUtxos,
|
|
59
59
|
})
|
|
60
60
|
|
|
61
|
-
if (!selectedUtxos) throw new Error('Not enough funds.')
|
|
61
|
+
if (!selectedUtxos?.size) throw new Error('Not enough funds.')
|
|
62
62
|
|
|
63
63
|
const addressPathsMap = selectedUtxos.getAddressPathsMap()
|
|
64
64
|
|
package/src/tx-send/index.js
CHANGED
|
@@ -2,6 +2,109 @@ import { Address } from '@exodus/models'
|
|
|
2
2
|
|
|
3
3
|
import { serializeCurrency } from '../fee/fee-utils.js'
|
|
4
4
|
|
|
5
|
+
const normalizeAddressObject = (asset, value) => {
|
|
6
|
+
const address =
|
|
7
|
+
value instanceof Address
|
|
8
|
+
? value
|
|
9
|
+
: Address.create(value.address ?? value, value.meta ?? Object.create(null))
|
|
10
|
+
|
|
11
|
+
return asset.address.toLegacyAddress
|
|
12
|
+
? Address.create(asset.address.toLegacyAddress(String(address)), address.meta)
|
|
13
|
+
: address
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// internalOutputs are deterministically derivable while scanning while change address is not
|
|
17
|
+
// internalOutput.address is an Address object, so platform can re-derive complete Address object from the tx log
|
|
18
|
+
const buildInternalOutputs = ({
|
|
19
|
+
asset,
|
|
20
|
+
walletAddressObjects,
|
|
21
|
+
internalAddressSet,
|
|
22
|
+
amountToSerialize,
|
|
23
|
+
toAddress,
|
|
24
|
+
changeOutput,
|
|
25
|
+
ourAddress,
|
|
26
|
+
changeAmount,
|
|
27
|
+
}) => {
|
|
28
|
+
const internalOutputs = []
|
|
29
|
+
if (amountToSerialize && toAddress) {
|
|
30
|
+
const normalizedToAddress = normalizeAddressObject(asset, toAddress)
|
|
31
|
+
const normalizedToAddressString = String(normalizedToAddress)
|
|
32
|
+
if (internalAddressSet.has(normalizedToAddressString)) {
|
|
33
|
+
const internalAddress =
|
|
34
|
+
walletAddressObjects.find((address) => String(address) === normalizedToAddressString) ??
|
|
35
|
+
normalizedToAddress
|
|
36
|
+
internalOutputs.push({
|
|
37
|
+
address: internalAddress,
|
|
38
|
+
amount: amountToSerialize.toBaseString(),
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (changeOutput && ourAddress && changeAmount) {
|
|
44
|
+
const changeAddressObject = normalizeAddressObject(asset, ourAddress)
|
|
45
|
+
internalOutputs.push({
|
|
46
|
+
address: changeAddressObject,
|
|
47
|
+
amount: changeAmount.toBaseString(),
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return internalOutputs
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const getWalletAddressObjects = async ({
|
|
55
|
+
asset,
|
|
56
|
+
assetClientInterface,
|
|
57
|
+
assetName,
|
|
58
|
+
walletAccount,
|
|
59
|
+
chainIndex,
|
|
60
|
+
multisigDataLength,
|
|
61
|
+
}) => {
|
|
62
|
+
const purposes = await assetClientInterface.getSupportedPurposes({
|
|
63
|
+
assetName,
|
|
64
|
+
walletAccount,
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
const unusedAddressIndexes = await assetClientInterface.getUnusedAddressIndexes({
|
|
68
|
+
assetName,
|
|
69
|
+
walletAccount,
|
|
70
|
+
highestUnusedIndexes: true,
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
const addresses = []
|
|
74
|
+
const addressSet = new Set()
|
|
75
|
+
|
|
76
|
+
for (const purpose of purposes) {
|
|
77
|
+
const chainIndexes = unusedAddressIndexes.find((entry) => entry.purpose === purpose)?.chain || [
|
|
78
|
+
0, 0,
|
|
79
|
+
]
|
|
80
|
+
// users can switch between multi address mode and single address mode, so we need to use the highest unused address index irrespective of current mode
|
|
81
|
+
const endAddressIndex = chainIndexes[chainIndex] ?? 0
|
|
82
|
+
const dataLength = multisigDataLength ?? 1
|
|
83
|
+
|
|
84
|
+
for (let addressIndex = 0; addressIndex <= endAddressIndex; addressIndex++) {
|
|
85
|
+
for (let multisigDataIndex = 0; multisigDataIndex < dataLength; multisigDataIndex++) {
|
|
86
|
+
const address = await assetClientInterface.getAddress({
|
|
87
|
+
assetName,
|
|
88
|
+
walletAccount,
|
|
89
|
+
purpose,
|
|
90
|
+
chainIndex,
|
|
91
|
+
addressIndex,
|
|
92
|
+
...(multisigDataLength ? { multisigDataIndex } : Object.create(null)),
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
const addressObject = normalizeAddressObject(asset, address)
|
|
96
|
+
const addressKey = String(addressObject)
|
|
97
|
+
if (addressSet.has(addressKey)) continue
|
|
98
|
+
|
|
99
|
+
addressSet.add(addressKey)
|
|
100
|
+
addresses.push(addressObject)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return addresses
|
|
106
|
+
}
|
|
107
|
+
|
|
5
108
|
export async function updateAccountState({
|
|
6
109
|
assetClientInterface,
|
|
7
110
|
assetName,
|
|
@@ -71,6 +174,7 @@ export async function updateTransactionLog({
|
|
|
71
174
|
rbfEnabled,
|
|
72
175
|
toAddress,
|
|
73
176
|
sendAmount,
|
|
177
|
+
changeAmount,
|
|
74
178
|
}) {
|
|
75
179
|
const assetName = asset.name
|
|
76
180
|
|
|
@@ -80,22 +184,54 @@ export async function updateTransactionLog({
|
|
|
80
184
|
walletAccount,
|
|
81
185
|
})
|
|
82
186
|
|
|
83
|
-
const
|
|
187
|
+
const multisigDataLength = config?.multisigDataLength
|
|
188
|
+
|
|
189
|
+
const walletReceiveAddressObjects = await getWalletAddressObjects({
|
|
190
|
+
asset,
|
|
191
|
+
assetClientInterface,
|
|
192
|
+
assetName,
|
|
84
193
|
walletAccount,
|
|
194
|
+
chainIndex: 0,
|
|
195
|
+
multisigDataLength,
|
|
196
|
+
})
|
|
197
|
+
const walletChangeAddressObjects = await getWalletAddressObjects({
|
|
198
|
+
asset,
|
|
199
|
+
assetClientInterface,
|
|
85
200
|
assetName,
|
|
86
|
-
|
|
201
|
+
walletAccount,
|
|
202
|
+
chainIndex: 1,
|
|
203
|
+
multisigDataLength,
|
|
87
204
|
})
|
|
88
205
|
|
|
206
|
+
const walletAddressObjects = [...walletReceiveAddressObjects, ...walletChangeAddressObjects]
|
|
207
|
+
|
|
208
|
+
const internalAddressSet = new Set(walletAddressObjects.map(String))
|
|
209
|
+
const normalizedToAddressString = toAddress
|
|
210
|
+
? String(normalizeAddressObject(asset, toAddress))
|
|
211
|
+
: undefined
|
|
212
|
+
|
|
89
213
|
// There are two cases of bumping, replacing or chaining a self-send.
|
|
90
214
|
// If we have a bumpTxId, but we aren't replacing, then it is a self-send.
|
|
215
|
+
// If the user sent to a receive or change address of this wallet, it is a self-send.
|
|
91
216
|
const selfSend = bumpTxId
|
|
92
217
|
? !replaceTx
|
|
93
|
-
:
|
|
218
|
+
: Boolean(normalizedToAddressString && internalAddressSet.has(normalizedToAddressString))
|
|
94
219
|
|
|
95
220
|
const displayReceiveAddress = asset.address.displayAddress?.(toAddress) || toAddress
|
|
96
221
|
|
|
97
222
|
const amountToSerialize = sendAmount.isZero ? undefined : sendAmount
|
|
98
223
|
|
|
224
|
+
const internalOutputs = buildInternalOutputs({
|
|
225
|
+
asset,
|
|
226
|
+
walletAddressObjects,
|
|
227
|
+
internalAddressSet,
|
|
228
|
+
amountToSerialize,
|
|
229
|
+
toAddress,
|
|
230
|
+
changeOutput,
|
|
231
|
+
ourAddress,
|
|
232
|
+
changeAmount,
|
|
233
|
+
})
|
|
234
|
+
|
|
99
235
|
// Build receivers list
|
|
100
236
|
const receivers = bumpTxId
|
|
101
237
|
? replaceTx
|
|
@@ -134,8 +270,10 @@ export async function updateTransactionLog({
|
|
|
134
270
|
selfSend,
|
|
135
271
|
data: {
|
|
136
272
|
sent: selfSend ? [] : receivers,
|
|
273
|
+
internalOutputs,
|
|
137
274
|
rbfEnabled,
|
|
138
275
|
feePerKB: size ? fee.div(size / 1000).toBaseNumber() : undefined,
|
|
276
|
+
// keeping this for backwards compatibility to not risk breaking any unknown code that depends on the change address being present
|
|
139
277
|
changeAddress: changeOutput ? ourAddress : undefined,
|
|
140
278
|
blockHeight,
|
|
141
279
|
blocksSeen: 0,
|