@exodus/bitcoin-api 4.1.3 → 4.1.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 +16 -0
- package/package.json +2 -2
- package/src/tx-create/create-tx.js +342 -140
- package/src/tx-send/broadcast-tx.js +48 -0
- package/src/tx-send/index.js +42 -160
- package/src/tx-send/update-state.js +188 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,22 @@
|
|
|
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.1.5](https://github.com/ExodusMovement/assets/compare/@exodus/bitcoin-api@4.1.4...@exodus/bitcoin-api@4.1.5) (2025-10-16)
|
|
7
|
+
|
|
8
|
+
**Note:** Version bump only for package @exodus/bitcoin-api
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
## [4.1.4](https://github.com/ExodusMovement/assets/compare/@exodus/bitcoin-api@4.1.3...@exodus/bitcoin-api@4.1.4) (2025-10-15)
|
|
15
|
+
|
|
16
|
+
**Note:** Version bump only for package @exodus/bitcoin-api
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
6
22
|
## [4.1.3](https://github.com/ExodusMovement/assets/compare/@exodus/bitcoin-api@4.1.1...@exodus/bitcoin-api@4.1.3) (2025-10-14)
|
|
7
23
|
|
|
8
24
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/bitcoin-api",
|
|
3
|
-
"version": "4.1.
|
|
3
|
+
"version": "4.1.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",
|
|
@@ -60,5 +60,5 @@
|
|
|
60
60
|
"type": "git",
|
|
61
61
|
"url": "git+https://github.com/ExodusMovement/assets.git"
|
|
62
62
|
},
|
|
63
|
-
"gitHead": "
|
|
63
|
+
"gitHead": "d6f680d217f82d3b165372a7bc728899b719a398"
|
|
64
64
|
}
|
|
@@ -10,6 +10,255 @@ import { getUnconfirmedTxAncestorMap } from '../unconfirmed-ancestor-data.js'
|
|
|
10
10
|
import { getUsableUtxos, getUtxos } from '../utxos-utils.js'
|
|
11
11
|
import { createInputs, createOutput, getBlockHeight, getNonWitnessTxs } from './tx-create-utils.js'
|
|
12
12
|
|
|
13
|
+
// Helper to shuffle arrays for randomized input/output ordering
|
|
14
|
+
const shuffle = (list) => {
|
|
15
|
+
return lodash.shuffle(list)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function validateTransactionParams({ assetClientInterface, asset, toAddress, bumpTxId }) {
|
|
19
|
+
assert(assetClientInterface, `assetClientInterface must be supplied in sendTx for ${asset.name}`)
|
|
20
|
+
assert(
|
|
21
|
+
toAddress || bumpTxId,
|
|
22
|
+
'should not be called without either a receiving toAddress or to bump a tx'
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function prepareTransactionContext({
|
|
27
|
+
asset,
|
|
28
|
+
assetName,
|
|
29
|
+
walletAccount,
|
|
30
|
+
multipleAddressesEnabled,
|
|
31
|
+
assetClientInterface,
|
|
32
|
+
accountState,
|
|
33
|
+
insightClient,
|
|
34
|
+
feeData,
|
|
35
|
+
feePerKB,
|
|
36
|
+
isExchange,
|
|
37
|
+
isRbfAllowed,
|
|
38
|
+
}) {
|
|
39
|
+
const updatedFeeData = { ...feeData, feePerKB: feePerKB ?? feeData.feePerKB }
|
|
40
|
+
|
|
41
|
+
const blockHeight = await getBlockHeight({ assetName, insightClient })
|
|
42
|
+
|
|
43
|
+
const rbfEnabled = updatedFeeData.rbfEnabled && !isExchange && isRbfAllowed
|
|
44
|
+
|
|
45
|
+
const changeAddress = multipleAddressesEnabled
|
|
46
|
+
? await assetClientInterface.getNextChangeAddress({ assetName, walletAccount })
|
|
47
|
+
: await assetClientInterface.getReceiveAddressObject({ assetName, walletAccount })
|
|
48
|
+
|
|
49
|
+
const txSet = await assetClientInterface.getTxLog({ assetName, walletAccount })
|
|
50
|
+
|
|
51
|
+
const unconfirmedTxAncestor = getUnconfirmedTxAncestorMap({ accountState })
|
|
52
|
+
|
|
53
|
+
const usableUtxos = getUsableUtxos({
|
|
54
|
+
asset,
|
|
55
|
+
utxos: getUtxos({ accountState, asset }),
|
|
56
|
+
feeData: updatedFeeData,
|
|
57
|
+
txSet,
|
|
58
|
+
unconfirmedTxAncestor,
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
updatedFeeData,
|
|
63
|
+
blockHeight,
|
|
64
|
+
rbfEnabled,
|
|
65
|
+
changeAddress,
|
|
66
|
+
txSet,
|
|
67
|
+
unconfirmedTxAncestor,
|
|
68
|
+
usableUtxos,
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Process and normalize addresses
|
|
73
|
+
function processAddress({ asset, toAddress }) {
|
|
74
|
+
let processedAddress = toAddress
|
|
75
|
+
|
|
76
|
+
if (asset.address.toLegacyAddress) {
|
|
77
|
+
processedAddress = asset.address.toLegacyAddress(toAddress)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (asset.name === 'digibyte' && asset.address.isP2SH2(processedAddress)) {
|
|
81
|
+
processedAddress = asset.address.P2SH2ToP2SH(processedAddress)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const useCashAddress = asset.address.isCashAddress?.(toAddress)
|
|
85
|
+
|
|
86
|
+
return { processedAddress, useCashAddress }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Determine strategy for transaction bumping (RBF vs CPFP)
|
|
90
|
+
function determineBumpStrategy({ bumpTxId, replaceableTxs, usableUtxos }) {
|
|
91
|
+
if (!bumpTxId) {
|
|
92
|
+
return { replaceableTxs, utxosToBump: undefined }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Check if we can use RBF (Replace-By-Fee)
|
|
96
|
+
const bumpTx = replaceableTxs.find(({ txId }) => txId === bumpTxId)
|
|
97
|
+
|
|
98
|
+
if (bumpTx) {
|
|
99
|
+
// Use RBF: replace the transaction directly
|
|
100
|
+
return { replaceableTxs: [bumpTx], utxosToBump: undefined }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Otherwise try CPFP (Child-Pays-For-Parent) by spending the transaction's outputs
|
|
104
|
+
const utxosToBump = usableUtxos.getTxIdUtxos(bumpTxId)
|
|
105
|
+
if (utxosToBump.size === 0) {
|
|
106
|
+
throw new Error(`Cannot bump transaction ${bumpTxId}`)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return { replaceableTxs: [], utxosToBump }
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Prepare parameters for UTXO selection
|
|
113
|
+
function prepareUtxoSelectionParams({
|
|
114
|
+
bumpTxId,
|
|
115
|
+
asset,
|
|
116
|
+
amount,
|
|
117
|
+
replaceableTxs,
|
|
118
|
+
processedAddress,
|
|
119
|
+
changeAddressType,
|
|
120
|
+
updatedFeeData,
|
|
121
|
+
rbfEnabled,
|
|
122
|
+
feePerKB,
|
|
123
|
+
customFee,
|
|
124
|
+
isSendAll,
|
|
125
|
+
}) {
|
|
126
|
+
const sendAmount = bumpTxId ? asset.currency.ZERO : amount
|
|
127
|
+
|
|
128
|
+
const receiveAddress = bumpTxId
|
|
129
|
+
? replaceableTxs.length > 0
|
|
130
|
+
? null
|
|
131
|
+
: changeAddressType
|
|
132
|
+
: processedAddress
|
|
133
|
+
|
|
134
|
+
const feeRate = customFee || updatedFeeData.feePerKB
|
|
135
|
+
const resolvedIsSendAll = !rbfEnabled && feePerKB ? false : isSendAll
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
sendAmount,
|
|
139
|
+
receiveAddress,
|
|
140
|
+
feeRate,
|
|
141
|
+
resolvedIsSendAll,
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Process RBF replacement transaction
|
|
146
|
+
function processReplacementTransaction({ replaceTx, asset }) {
|
|
147
|
+
if (!replaceTx) return { replaceTx: undefined, replaceTxInputUtxos: undefined }
|
|
148
|
+
|
|
149
|
+
const clonedTx = replaceTx.clone()
|
|
150
|
+
const updatedTx = clonedTx.update({ data: { ...clonedTx.data } })
|
|
151
|
+
|
|
152
|
+
updatedTx.data.sent = updatedTx.data.sent.map((to) => ({
|
|
153
|
+
...to,
|
|
154
|
+
amount: serializeCurrency(to.amount, asset.currency),
|
|
155
|
+
}))
|
|
156
|
+
|
|
157
|
+
const replaceTxInputUtxos = UtxoCollection.fromJSON(updatedTx.data.inputs, {
|
|
158
|
+
currency: asset.currency,
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
return { replaceTx: updatedTx, replaceTxInputUtxos }
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Create transaction outputs
|
|
165
|
+
function createTransactionOutputs({
|
|
166
|
+
replaceTx,
|
|
167
|
+
processedAddress,
|
|
168
|
+
sendAmount,
|
|
169
|
+
asset,
|
|
170
|
+
selectedUtxos,
|
|
171
|
+
fee,
|
|
172
|
+
changeAddress,
|
|
173
|
+
}) {
|
|
174
|
+
const assetName = asset.name
|
|
175
|
+
let outputs = []
|
|
176
|
+
let sendOutput
|
|
177
|
+
|
|
178
|
+
// Add existing outputs from replacement transaction
|
|
179
|
+
if (replaceTx) {
|
|
180
|
+
outputs = replaceTx.data.sent.map(({ address, amount }) =>
|
|
181
|
+
createOutput(assetName, address, parseCurrency(amount, asset.currency))
|
|
182
|
+
)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Add send output if we have a destination address
|
|
186
|
+
if (processedAddress) {
|
|
187
|
+
sendOutput = createOutput(assetName, processedAddress, sendAmount)
|
|
188
|
+
outputs.push(sendOutput)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Calculate total amount
|
|
192
|
+
const totalAmount = replaceTx
|
|
193
|
+
? replaceTx.data.sent.reduce(
|
|
194
|
+
(total, { amount }) => total.add(parseCurrency(amount, asset.currency)),
|
|
195
|
+
sendAmount
|
|
196
|
+
)
|
|
197
|
+
: sendAmount
|
|
198
|
+
|
|
199
|
+
// Handle change output
|
|
200
|
+
const { changeOutput, adjustedFee, changeAddressKeypath, ourAddress } = createChangeOutput({
|
|
201
|
+
selectedUtxos,
|
|
202
|
+
totalAmount,
|
|
203
|
+
fee,
|
|
204
|
+
replaceTx,
|
|
205
|
+
changeAddress,
|
|
206
|
+
asset,
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
if (changeOutput) {
|
|
210
|
+
outputs.push(changeOutput)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
outputs: replaceTx ? outputs : shuffle(outputs),
|
|
215
|
+
sendOutput,
|
|
216
|
+
changeOutput,
|
|
217
|
+
totalAmount,
|
|
218
|
+
adjustedFee,
|
|
219
|
+
changeAddressKeypath,
|
|
220
|
+
ourAddress,
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Create change output if needed
|
|
225
|
+
function createChangeOutput({ selectedUtxos, totalAmount, fee, replaceTx, changeAddress, asset }) {
|
|
226
|
+
const change = selectedUtxos.value.sub(totalAmount).sub(fee)
|
|
227
|
+
const dust = getChangeDustValue(asset)
|
|
228
|
+
|
|
229
|
+
// Process change address
|
|
230
|
+
let ourAddress = replaceTx?.data?.changeAddress || changeAddress
|
|
231
|
+
if (asset.address.toLegacyAddress) {
|
|
232
|
+
const legacyAddress = asset.address.toLegacyAddress(ourAddress.address)
|
|
233
|
+
ourAddress = Address.create(legacyAddress, ourAddress.meta)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Create change output if above dust threshold
|
|
237
|
+
if (change.gte(dust)) {
|
|
238
|
+
const changeOutput = createOutput(
|
|
239
|
+
asset.name,
|
|
240
|
+
ourAddress.address ?? ourAddress.toString(),
|
|
241
|
+
change
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
// Return the keypath for hardware wallet change detection
|
|
245
|
+
return {
|
|
246
|
+
changeOutput,
|
|
247
|
+
adjustedFee: fee,
|
|
248
|
+
changeAddressKeypath: ourAddress.meta.path,
|
|
249
|
+
ourAddress,
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Add dust to fee if not enough for change output
|
|
254
|
+
return {
|
|
255
|
+
changeOutput: undefined,
|
|
256
|
+
adjustedFee: fee.add(change),
|
|
257
|
+
changeAddressKeypath: undefined,
|
|
258
|
+
ourAddress,
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
13
262
|
async function createUnsignedTx({
|
|
14
263
|
inputs,
|
|
15
264
|
outputs,
|
|
@@ -51,8 +300,6 @@ const transferHandler = {
|
|
|
51
300
|
walletAccount,
|
|
52
301
|
toAddress,
|
|
53
302
|
amount,
|
|
54
|
-
blockHeight: providedBlockHeight,
|
|
55
|
-
rbfEnabled: providedRbfEnabled,
|
|
56
303
|
multipleAddressesEnabled,
|
|
57
304
|
feePerKB,
|
|
58
305
|
customFee,
|
|
@@ -70,99 +317,72 @@ const transferHandler = {
|
|
|
70
317
|
changeAddressType,
|
|
71
318
|
}) => {
|
|
72
319
|
const assetName = asset.name
|
|
73
|
-
const updatedFeeData = { ...feeData, feePerKB: feePerKB ?? feeData.feePerKB }
|
|
74
320
|
const insightClient = asset.baseAsset.insightClient
|
|
75
321
|
|
|
76
|
-
|
|
322
|
+
validateTransactionParams({ assetClientInterface, asset, toAddress, bumpTxId })
|
|
77
323
|
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
return lodash.shuffle(list)
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
assert(
|
|
324
|
+
const context = await prepareTransactionContext({
|
|
325
|
+
asset,
|
|
326
|
+
assetName,
|
|
327
|
+
walletAccount,
|
|
328
|
+
multipleAddressesEnabled,
|
|
87
329
|
assetClientInterface,
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
330
|
+
accountState,
|
|
331
|
+
insightClient,
|
|
332
|
+
feeData,
|
|
333
|
+
feePerKB,
|
|
334
|
+
isExchange,
|
|
335
|
+
isRbfAllowed,
|
|
336
|
+
})
|
|
94
337
|
|
|
95
|
-
const useCashAddress = asset
|
|
338
|
+
const { processedAddress, useCashAddress } = processAddress({ asset, toAddress })
|
|
96
339
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
: await assetClientInterface.getReceiveAddressObject({ assetName, walletAccount })
|
|
340
|
+
// Get replaceable transactions
|
|
341
|
+
let replaceableTxs = findUnconfirmedSentRbfTxs(context.txSet)
|
|
100
342
|
|
|
101
|
-
|
|
343
|
+
// Determine bumping strategy (RBF or CPFP)
|
|
344
|
+
const { replaceableTxs: updatedReplaceableTxs, utxosToBump } = determineBumpStrategy({
|
|
345
|
+
bumpTxId,
|
|
346
|
+
replaceableTxs,
|
|
347
|
+
usableUtxos: context.usableUtxos,
|
|
348
|
+
})
|
|
349
|
+
replaceableTxs = updatedReplaceableTxs
|
|
102
350
|
|
|
103
|
-
const
|
|
104
|
-
|
|
351
|
+
const utxoParams = prepareUtxoSelectionParams({
|
|
352
|
+
bumpTxId,
|
|
105
353
|
asset,
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
354
|
+
amount,
|
|
355
|
+
replaceableTxs,
|
|
356
|
+
processedAddress,
|
|
357
|
+
changeAddressType,
|
|
358
|
+
updatedFeeData: context.updatedFeeData,
|
|
359
|
+
rbfEnabled: context.rbfEnabled,
|
|
360
|
+
feePerKB,
|
|
361
|
+
customFee,
|
|
362
|
+
isSendAll,
|
|
110
363
|
})
|
|
111
364
|
|
|
112
|
-
let replaceableTxs = findUnconfirmedSentRbfTxs(txSet)
|
|
113
|
-
|
|
114
|
-
let processedAddress = toAddress
|
|
115
|
-
if (asset.address.toLegacyAddress) {
|
|
116
|
-
processedAddress = asset.address.toLegacyAddress(toAddress)
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
if (assetName === 'digibyte' && asset.address.isP2SH2(processedAddress)) {
|
|
120
|
-
processedAddress = asset.address.P2SH2ToP2SH(processedAddress)
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
let utxosToBump
|
|
124
|
-
if (bumpTxId) {
|
|
125
|
-
const bumpTx = replaceableTxs.find(({ txId }) => txId === bumpTxId)
|
|
126
|
-
if (bumpTx) {
|
|
127
|
-
replaceableTxs = [bumpTx]
|
|
128
|
-
} else {
|
|
129
|
-
utxosToBump = usableUtxos.getTxIdUtxos(bumpTxId)
|
|
130
|
-
if (utxosToBump.size === 0) {
|
|
131
|
-
throw new Error(`Cannot bump transaction ${bumpTxId}`)
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
replaceableTxs = []
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
const sendAmount = bumpTxId ? asset.currency.ZERO : amount
|
|
139
|
-
const receiveAddress = bumpTxId
|
|
140
|
-
? replaceableTxs.length > 0
|
|
141
|
-
? null
|
|
142
|
-
: changeAddressType
|
|
143
|
-
: processedAddress
|
|
144
|
-
const feeRate = updatedFeeData.feePerKB
|
|
145
|
-
const resolvedIsSendAll = !rbfEnabled && feePerKB ? false : isSendAll
|
|
146
|
-
|
|
147
365
|
let { selectedUtxos, fee, replaceTx } = selectUtxos({
|
|
148
366
|
asset,
|
|
149
|
-
usableUtxos,
|
|
367
|
+
usableUtxos: context.usableUtxos,
|
|
150
368
|
replaceableTxs,
|
|
151
|
-
amount: sendAmount,
|
|
152
|
-
feeRate:
|
|
153
|
-
receiveAddress,
|
|
154
|
-
isSendAll: resolvedIsSendAll,
|
|
369
|
+
amount: utxoParams.sendAmount,
|
|
370
|
+
feeRate: utxoParams.feeRate,
|
|
371
|
+
receiveAddress: utxoParams.receiveAddress,
|
|
372
|
+
isSendAll: utxoParams.resolvedIsSendAll,
|
|
155
373
|
getFeeEstimator: (asset, { feePerKB, ...options }) =>
|
|
156
374
|
getFeeEstimator(asset, feePerKB, options),
|
|
157
375
|
mustSpendUtxos: utxosToBump,
|
|
158
376
|
allowUnconfirmedRbfEnabledUtxos,
|
|
159
|
-
unconfirmedTxAncestor,
|
|
377
|
+
unconfirmedTxAncestor: context.unconfirmedTxAncestor,
|
|
160
378
|
utxosDescendingOrder,
|
|
161
379
|
taprootInputWitnessSize,
|
|
162
380
|
changeAddressType,
|
|
163
381
|
})
|
|
164
382
|
|
|
165
|
-
if (!selectedUtxos && !replaceTx)
|
|
383
|
+
if (!selectedUtxos && !replaceTx) {
|
|
384
|
+
throw new Error('Not enough funds.')
|
|
385
|
+
}
|
|
166
386
|
|
|
167
387
|
// When bumping a tx, we can either replace the tx with RBF or spend its selected change.
|
|
168
388
|
// If there is no selected UTXO or the tx to replace is not the tx we want to bump,
|
|
@@ -173,90 +393,76 @@ const transferHandler = {
|
|
|
173
393
|
throw new Error(`Unable to bump ${bumpTxId}`)
|
|
174
394
|
}
|
|
175
395
|
|
|
176
|
-
|
|
177
|
-
replaceTx
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
selectedUtxos = selectedUtxos.union(
|
|
183
|
-
UtxoCollection.fromJSON(replaceTx.data.inputs, { currency: asset.currency })
|
|
184
|
-
)
|
|
396
|
+
const { replaceTx: processedReplaceTx, replaceTxInputUtxos } = processReplacementTransaction({
|
|
397
|
+
replaceTx,
|
|
398
|
+
asset,
|
|
399
|
+
})
|
|
400
|
+
|
|
401
|
+
if (replaceTxInputUtxos) {
|
|
402
|
+
selectedUtxos = selectedUtxos.union(replaceTxInputUtxos)
|
|
185
403
|
}
|
|
186
404
|
|
|
187
405
|
const addressPathsMap = selectedUtxos.getAddressPathsMap()
|
|
188
406
|
|
|
189
|
-
//
|
|
190
|
-
const inputs = shuffle(createInputs(assetName, selectedUtxos.toArray(), rbfEnabled))
|
|
191
|
-
let outputs = replaceTx
|
|
192
|
-
? replaceTx.data.sent.map(({ address, amount }) =>
|
|
193
|
-
createOutput(assetName, address, parseCurrency(amount, asset.currency))
|
|
194
|
-
)
|
|
195
|
-
: []
|
|
196
|
-
|
|
197
|
-
// Send output
|
|
198
|
-
let sendOutput
|
|
199
|
-
if (processedAddress) {
|
|
200
|
-
sendOutput = createOutput(assetName, processedAddress, sendAmount)
|
|
201
|
-
outputs.push(sendOutput)
|
|
202
|
-
}
|
|
407
|
+
// Create inputs
|
|
408
|
+
const inputs = shuffle(createInputs(assetName, selectedUtxos.toArray(), context.rbfEnabled))
|
|
203
409
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
410
|
+
// Create outputs
|
|
411
|
+
const {
|
|
412
|
+
outputs,
|
|
413
|
+
sendOutput,
|
|
414
|
+
changeOutput,
|
|
415
|
+
totalAmount,
|
|
416
|
+
adjustedFee,
|
|
417
|
+
changeAddressKeypath,
|
|
418
|
+
ourAddress,
|
|
419
|
+
} = createTransactionOutputs({
|
|
420
|
+
replaceTx: processedReplaceTx,
|
|
421
|
+
processedAddress,
|
|
422
|
+
sendAmount: utxoParams.sendAmount,
|
|
423
|
+
asset,
|
|
424
|
+
selectedUtxos,
|
|
425
|
+
fee,
|
|
426
|
+
changeAddress: context.changeAddress,
|
|
427
|
+
})
|
|
218
428
|
|
|
219
|
-
//
|
|
220
|
-
|
|
221
|
-
if (
|
|
222
|
-
|
|
223
|
-
// Add the keypath of change address to support Trezor detect the change output.
|
|
224
|
-
// Output is change and does not need approval from user which shows the strange address that user never seen.
|
|
225
|
-
addressPathsMap[changeAddress] = ourAddress.meta.path
|
|
226
|
-
outputs.push(changeOutput)
|
|
227
|
-
} else {
|
|
228
|
-
// If we don't have enough for a change output, then all remaining dust is just added to fee
|
|
229
|
-
fee = fee.add(change)
|
|
429
|
+
// Add the keypath of change address to support Trezor detect the change output.
|
|
430
|
+
// Output is change and does not need approval from user which shows the strange address that user never seen.
|
|
431
|
+
if (changeAddressKeypath) {
|
|
432
|
+
addressPathsMap[context.changeAddress] = changeAddressKeypath
|
|
230
433
|
}
|
|
231
434
|
|
|
232
|
-
|
|
233
|
-
|
|
435
|
+
// Create unsigned transaction
|
|
234
436
|
const unsignedTx = await createUnsignedTx({
|
|
235
437
|
inputs,
|
|
236
438
|
outputs,
|
|
237
439
|
useCashAddress,
|
|
238
440
|
addressPathsMap,
|
|
239
|
-
blockHeight,
|
|
441
|
+
blockHeight: context.blockHeight,
|
|
240
442
|
asset,
|
|
241
443
|
selectedUtxos,
|
|
242
444
|
insightClient,
|
|
243
445
|
})
|
|
244
446
|
|
|
245
447
|
return {
|
|
246
|
-
amount,
|
|
247
|
-
change,
|
|
248
|
-
totalAmount,
|
|
249
|
-
address: processedAddress,
|
|
250
|
-
ourAddress,
|
|
251
|
-
receiveAddress,
|
|
252
|
-
sendAmount,
|
|
253
|
-
fee,
|
|
254
|
-
usableUtxos,
|
|
255
|
-
selectedUtxos,
|
|
256
|
-
replaceTx,
|
|
257
|
-
sendOutput,
|
|
258
|
-
changeOutput,
|
|
259
448
|
unsignedTx,
|
|
449
|
+
fee: adjustedFee,
|
|
450
|
+
metadata: {
|
|
451
|
+
amount,
|
|
452
|
+
change: selectedUtxos.value.sub(totalAmount).sub(adjustedFee),
|
|
453
|
+
totalAmount,
|
|
454
|
+
address: processedAddress,
|
|
455
|
+
ourAddress,
|
|
456
|
+
receiveAddress: utxoParams.receiveAddress,
|
|
457
|
+
sendAmount: utxoParams.sendAmount,
|
|
458
|
+
usableUtxos: context.usableUtxos,
|
|
459
|
+
selectedUtxos,
|
|
460
|
+
replaceTx: processedReplaceTx,
|
|
461
|
+
sendOutput,
|
|
462
|
+
changeOutput,
|
|
463
|
+
blockHeight: context.blockHeight,
|
|
464
|
+
rbfEnabled: context.rbfEnabled,
|
|
465
|
+
},
|
|
260
466
|
}
|
|
261
467
|
},
|
|
262
468
|
}
|
|
@@ -275,8 +481,6 @@ export const createTxFactory =
|
|
|
275
481
|
type,
|
|
276
482
|
toAddress,
|
|
277
483
|
amount,
|
|
278
|
-
blockHeight,
|
|
279
|
-
rbfEnabled,
|
|
280
484
|
multipleAddressesEnabled,
|
|
281
485
|
feePerKB,
|
|
282
486
|
customFee,
|
|
@@ -300,8 +504,6 @@ export const createTxFactory =
|
|
|
300
504
|
walletAccount,
|
|
301
505
|
toAddress,
|
|
302
506
|
amount,
|
|
303
|
-
blockHeight,
|
|
304
|
-
rbfEnabled,
|
|
305
507
|
multipleAddressesEnabled,
|
|
306
508
|
feePerKB,
|
|
307
509
|
customFee,
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { retry } from '@exodus/simple-retry'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Broadcast a signed transaction to the Bitcoin network with retry logic
|
|
5
|
+
* @param {Object} params
|
|
6
|
+
* @param {Object} params.asset - The asset object
|
|
7
|
+
* @param {Buffer} params.rawTx - The raw signed transaction
|
|
8
|
+
* @returns {Promise<void>}
|
|
9
|
+
* @throws {Error} With finalError=true for non-retryable errors
|
|
10
|
+
*/
|
|
11
|
+
export async function broadcastTransaction({ asset, rawTx }) {
|
|
12
|
+
const broadcastTxWithRetry = retry(
|
|
13
|
+
async (rawTxHex) => {
|
|
14
|
+
try {
|
|
15
|
+
return await asset.api.broadcastTx(rawTxHex)
|
|
16
|
+
} catch (e) {
|
|
17
|
+
// Mark certain errors as final (non-retryable)
|
|
18
|
+
if (
|
|
19
|
+
/missing inputs/i.test(e.message) ||
|
|
20
|
+
/absurdly-high-fee/.test(e.message) ||
|
|
21
|
+
/too-long-mempool-chain/.test(e.message) ||
|
|
22
|
+
/txn-mempool-conflict/.test(e.message) ||
|
|
23
|
+
/tx-size/.test(e.message) ||
|
|
24
|
+
/txn-already-in-mempool/.test(e.message)
|
|
25
|
+
) {
|
|
26
|
+
e.finalError = true
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
throw e
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
{ delayTimesMs: ['10s'] }
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
const rawTxHex = rawTx.toString('hex')
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
await broadcastTxWithRetry(rawTxHex)
|
|
39
|
+
} catch (err) {
|
|
40
|
+
if (err.message.includes('txn-already-in-mempool')) {
|
|
41
|
+
// Not an error, transaction is already broadcast
|
|
42
|
+
console.log('Transaction is already in the mempool.')
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
throw err
|
|
47
|
+
}
|
|
48
|
+
}
|
package/src/tx-send/index.js
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import * as defaultBitcoinjsLib from '@exodus/bitcoinjs'
|
|
2
|
-
import { Address } from '@exodus/models'
|
|
3
|
-
import { retry } from '@exodus/simple-retry'
|
|
4
2
|
import assert from 'minimalistic-assert'
|
|
5
3
|
|
|
6
|
-
import { serializeCurrency } from '../fee/fee-utils.js'
|
|
7
4
|
import { createTxFactory } from '../tx-create/create-tx.js'
|
|
8
|
-
import {
|
|
5
|
+
import { broadcastTransaction } from './broadcast-tx.js'
|
|
6
|
+
import { updateAccountState, updateTransactionLog } from './update-state.js'
|
|
9
7
|
|
|
10
8
|
const getSize = (tx) => {
|
|
11
9
|
if (typeof tx.size === 'number') return tx.size
|
|
@@ -105,11 +103,9 @@ const getPrepareSendTransaction = async ({
|
|
|
105
103
|
amount,
|
|
106
104
|
asset,
|
|
107
105
|
assetClientInterface,
|
|
108
|
-
blockHeight,
|
|
109
106
|
changeAddressType,
|
|
110
107
|
getFeeEstimator,
|
|
111
108
|
options,
|
|
112
|
-
rbfEnabled,
|
|
113
109
|
utxosDescendingOrder,
|
|
114
110
|
walletAccount,
|
|
115
111
|
}) => {
|
|
@@ -121,15 +117,17 @@ const getPrepareSendTransaction = async ({
|
|
|
121
117
|
changeAddressType,
|
|
122
118
|
})
|
|
123
119
|
|
|
120
|
+
// Set default values for options
|
|
121
|
+
const { isRbfAllowed = true, ...restOptions } = options || {}
|
|
122
|
+
|
|
124
123
|
return createTx({
|
|
125
124
|
asset,
|
|
126
125
|
walletAccount,
|
|
127
126
|
type: 'transfer',
|
|
128
127
|
toAddress: address,
|
|
129
128
|
amount,
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
...options,
|
|
129
|
+
isRbfAllowed,
|
|
130
|
+
...restOptions,
|
|
133
131
|
})
|
|
134
132
|
}
|
|
135
133
|
|
|
@@ -146,17 +144,10 @@ export const createAndBroadcastTXFactory =
|
|
|
146
144
|
}) =>
|
|
147
145
|
async ({ asset, walletAccount, address, amount, options }) => {
|
|
148
146
|
// Prepare transaction
|
|
149
|
-
const { bumpTxId
|
|
147
|
+
const { bumpTxId } = options
|
|
150
148
|
|
|
151
149
|
const assetName = asset.name
|
|
152
|
-
const feeData = await assetClientInterface.getFeeConfig({ assetName })
|
|
153
150
|
const accountState = await assetClientInterface.getAccountState({ assetName, walletAccount })
|
|
154
|
-
const insightClient = asset.baseAsset.insightClient
|
|
155
|
-
|
|
156
|
-
const rbfEnabled = feeData.rbfEnabled && !isExchange && isRbfAllowed
|
|
157
|
-
|
|
158
|
-
// blockHeight
|
|
159
|
-
const blockHeight = await getBlockHeight({ assetName, insightClient })
|
|
160
151
|
|
|
161
152
|
const transactionDescriptor = await getPrepareSendTransaction({
|
|
162
153
|
address,
|
|
@@ -164,31 +155,27 @@ export const createAndBroadcastTXFactory =
|
|
|
164
155
|
amount,
|
|
165
156
|
asset,
|
|
166
157
|
assetClientInterface,
|
|
167
|
-
blockHeight,
|
|
168
158
|
changeAddressType,
|
|
169
159
|
getFeeEstimator,
|
|
170
160
|
options,
|
|
171
|
-
rbfEnabled,
|
|
172
161
|
utxosDescendingOrder,
|
|
173
162
|
walletAccount,
|
|
174
163
|
})
|
|
164
|
+
|
|
165
|
+
const { unsignedTx, fee, metadata } = transactionDescriptor
|
|
175
166
|
const {
|
|
176
|
-
change,
|
|
177
|
-
totalAmount,
|
|
178
|
-
ourAddress,
|
|
179
|
-
receiveAddress,
|
|
180
167
|
sendAmount,
|
|
181
|
-
fee,
|
|
182
168
|
usableUtxos,
|
|
183
|
-
selectedUtxos,
|
|
184
169
|
replaceTx,
|
|
185
170
|
sendOutput,
|
|
186
171
|
changeOutput,
|
|
187
|
-
|
|
188
|
-
|
|
172
|
+
blockHeight,
|
|
173
|
+
rbfEnabled,
|
|
174
|
+
} = metadata
|
|
175
|
+
|
|
189
176
|
const outputs = unsignedTx.txData.outputs
|
|
190
177
|
|
|
191
|
-
address =
|
|
178
|
+
address = metadata.address
|
|
192
179
|
|
|
193
180
|
// Sign transaction
|
|
194
181
|
const { rawTx, txId, tx } = await signTransaction({
|
|
@@ -199,44 +186,18 @@ export const createAndBroadcastTXFactory =
|
|
|
199
186
|
})
|
|
200
187
|
|
|
201
188
|
// Broadcast transaction
|
|
202
|
-
const broadcastTxWithRetry = retry(
|
|
203
|
-
async (rawTx) => {
|
|
204
|
-
try {
|
|
205
|
-
return await asset.api.broadcastTx(rawTx)
|
|
206
|
-
} catch (e) {
|
|
207
|
-
if (
|
|
208
|
-
/missing inputs/i.test(e.message) ||
|
|
209
|
-
/absurdly-high-fee/.test(e.message) ||
|
|
210
|
-
/too-long-mempool-chain/.test(e.message) ||
|
|
211
|
-
/txn-mempool-conflict/.test(e.message) ||
|
|
212
|
-
/tx-size/.test(e.message) ||
|
|
213
|
-
/txn-already-in-mempool/.test(e.message)
|
|
214
|
-
) {
|
|
215
|
-
e.finalError = true
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
throw e
|
|
219
|
-
}
|
|
220
|
-
},
|
|
221
|
-
{ delayTimesMs: ['10s'] }
|
|
222
|
-
)
|
|
223
|
-
|
|
224
189
|
try {
|
|
225
|
-
await
|
|
190
|
+
await broadcastTransaction({ asset, rawTx })
|
|
226
191
|
} catch (err) {
|
|
227
|
-
if (err.message
|
|
228
|
-
// It's not an error, we must ignore it.
|
|
229
|
-
console.log('Transaction is already in the mempool.')
|
|
230
|
-
} else if (/insight broadcast http error.*missing inputs/i.test(err.message)) {
|
|
192
|
+
if (/insight broadcast http error.*missing inputs/i.test(err.message)) {
|
|
231
193
|
err.txInfo = JSON.stringify({
|
|
232
194
|
amount: sendAmount.toDefaultString({ unit: true }),
|
|
233
|
-
fee: ((fee && fee.toDefaultString({ unit: true })) || 0).toString(),
|
|
195
|
+
fee: ((fee && fee.toDefaultString({ unit: true })) || 0).toString(),
|
|
234
196
|
allUtxos: usableUtxos.toJSON(),
|
|
235
197
|
})
|
|
236
|
-
throw err
|
|
237
|
-
} else {
|
|
238
|
-
throw err
|
|
239
198
|
}
|
|
199
|
+
|
|
200
|
+
throw err
|
|
240
201
|
}
|
|
241
202
|
|
|
242
203
|
function findUtxoIndex(output) {
|
|
@@ -256,114 +217,35 @@ export const createAndBroadcastTXFactory =
|
|
|
256
217
|
const changeUtxoIndex = findUtxoIndex(changeOutput)
|
|
257
218
|
const sendUtxoIndex = findUtxoIndex(sendOutput)
|
|
258
219
|
|
|
259
|
-
const {
|
|
260
|
-
|
|
261
|
-
const knownBalanceUtxoIds = accountState.knownBalanceUtxoIds || []
|
|
262
|
-
let remainingUtxos = usableUtxos.difference(selectedUtxos)
|
|
263
|
-
if (changeUtxoIndex !== -1) {
|
|
264
|
-
const address = Address.create(ourAddress.address, ourAddress.meta)
|
|
265
|
-
const changeUtxo = {
|
|
266
|
-
txId,
|
|
267
|
-
address,
|
|
268
|
-
vout: changeUtxoIndex,
|
|
269
|
-
script,
|
|
270
|
-
value: change,
|
|
271
|
-
confirmations: 0,
|
|
272
|
-
rbfEnabled,
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
knownBalanceUtxoIds.push(`${changeUtxo.txId}:${changeUtxo.vout}`.toLowerCase())
|
|
276
|
-
remainingUtxos = remainingUtxos.addUtxo(changeUtxo)
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
if (replaceTx) {
|
|
280
|
-
remainingUtxos = remainingUtxos.difference(remainingUtxos.getTxIdUtxos(replaceTx.txId))
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
await assetClientInterface.updateAccountState({
|
|
284
|
-
assetName,
|
|
285
|
-
walletAccount,
|
|
286
|
-
newData: {
|
|
287
|
-
utxos: remainingUtxos,
|
|
288
|
-
knownBalanceUtxoIds,
|
|
289
|
-
},
|
|
290
|
-
})
|
|
291
|
-
|
|
292
|
-
const config = await assetClientInterface.getAssetConfig?.({
|
|
220
|
+
const { size } = await updateAccountState({
|
|
221
|
+
assetClientInterface,
|
|
293
222
|
assetName,
|
|
294
223
|
walletAccount,
|
|
224
|
+
accountState,
|
|
225
|
+
txId,
|
|
226
|
+
metadata,
|
|
227
|
+
tx,
|
|
228
|
+
rawTx,
|
|
229
|
+
changeUtxoIndex,
|
|
230
|
+
getSizeAndChangeScript,
|
|
231
|
+
rbfEnabled,
|
|
295
232
|
})
|
|
296
|
-
const walletAddressObjects = await assetClientInterface.getReceiveAddresses({
|
|
297
|
-
walletAccount,
|
|
298
|
-
assetName,
|
|
299
|
-
multiAddressMode: config?.multiAddressMode ?? true,
|
|
300
|
-
})
|
|
301
|
-
// There are two cases of bumping, replacing or chaining a self-send.
|
|
302
|
-
// If we have a bumpTxId, but we aren't replacing, then it is a self-send.
|
|
303
|
-
const selfSend = bumpTxId
|
|
304
|
-
? !replaceTx
|
|
305
|
-
: walletAddressObjects.some((receiveAddress) => String(receiveAddress) === String(address))
|
|
306
|
-
|
|
307
|
-
const displayReceiveAddress = asset.address.displayAddress?.(receiveAddress) || receiveAddress
|
|
308
|
-
|
|
309
|
-
const receivers = bumpTxId
|
|
310
|
-
? replaceTx
|
|
311
|
-
? replaceTx.data.sent
|
|
312
|
-
: []
|
|
313
|
-
: replaceTx
|
|
314
|
-
? [
|
|
315
|
-
...replaceTx.data.sent,
|
|
316
|
-
{ address: displayReceiveAddress, amount: serializeCurrency(amount, asset.currency) },
|
|
317
|
-
]
|
|
318
|
-
: [{ address: displayReceiveAddress, amount: serializeCurrency(amount, asset.currency) }]
|
|
319
233
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
return totalAmount.abs().negate()
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
const coinAmount = calculateCoinAmount()
|
|
329
|
-
|
|
330
|
-
await assetClientInterface.updateTxLogAndNotify({
|
|
331
|
-
assetName: asset.name,
|
|
234
|
+
await updateTransactionLog({
|
|
235
|
+
asset,
|
|
236
|
+
assetClientInterface,
|
|
332
237
|
walletAccount,
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
data: {
|
|
343
|
-
sent: selfSend ? [] : receivers,
|
|
344
|
-
rbfEnabled,
|
|
345
|
-
feePerKB: size ? fee.div(size / 1000).toBaseNumber() : undefined,
|
|
346
|
-
changeAddress: changeOutput ? ourAddress : undefined,
|
|
347
|
-
blockHeight,
|
|
348
|
-
blocksSeen: 0,
|
|
349
|
-
inputs: selectedUtxos.toJSON(),
|
|
350
|
-
replacedTxId: replaceTx ? replaceTx.txId : undefined,
|
|
351
|
-
},
|
|
352
|
-
},
|
|
353
|
-
],
|
|
238
|
+
txId,
|
|
239
|
+
fee,
|
|
240
|
+
metadata,
|
|
241
|
+
address,
|
|
242
|
+
amount,
|
|
243
|
+
bumpTxId,
|
|
244
|
+
size,
|
|
245
|
+
blockHeight,
|
|
246
|
+
rbfEnabled,
|
|
354
247
|
})
|
|
355
248
|
|
|
356
|
-
// If we are replacing the tx, add the replacedBy info to the previous tx to update UI
|
|
357
|
-
// Also, clone the personal note and attach it to the new tx so it is not lost
|
|
358
|
-
if (replaceTx) {
|
|
359
|
-
replaceTx.data.replacedBy = txId
|
|
360
|
-
await assetClientInterface.updateTxLogAndNotify({
|
|
361
|
-
assetName,
|
|
362
|
-
walletAccount,
|
|
363
|
-
txs: [replaceTx],
|
|
364
|
-
})
|
|
365
|
-
}
|
|
366
|
-
|
|
367
249
|
return {
|
|
368
250
|
txId,
|
|
369
251
|
sendUtxoIndex,
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { Address } from '@exodus/models'
|
|
2
|
+
|
|
3
|
+
import { serializeCurrency } from '../fee/fee-utils.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Update account state after transaction is broadcast
|
|
7
|
+
* @param {Object} params
|
|
8
|
+
* @param {Object} params.assetClientInterface - Asset client interface
|
|
9
|
+
* @param {string} params.assetName - Name of the asset
|
|
10
|
+
* @param {Object} params.walletAccount - Wallet account
|
|
11
|
+
* @param {Object} params.accountState - Current account state
|
|
12
|
+
* @param {string} params.txId - Transaction ID
|
|
13
|
+
* @param {Object} params.metadata - Transaction metadata
|
|
14
|
+
* @param {Object} params.tx - Signed transaction object
|
|
15
|
+
* @param {Buffer} params.rawTx - Raw transaction
|
|
16
|
+
* @param {number} params.changeUtxoIndex - Index of change output
|
|
17
|
+
* @param {Object} params.changeOutput - Change output details
|
|
18
|
+
* @param {Object} params.getSizeAndChangeScript - Function to get size and script
|
|
19
|
+
* @param {boolean} params.rbfEnabled - Whether RBF is enabled
|
|
20
|
+
*/
|
|
21
|
+
export async function updateAccountState({
|
|
22
|
+
assetClientInterface,
|
|
23
|
+
assetName,
|
|
24
|
+
walletAccount,
|
|
25
|
+
accountState,
|
|
26
|
+
txId,
|
|
27
|
+
metadata,
|
|
28
|
+
tx,
|
|
29
|
+
rawTx,
|
|
30
|
+
changeUtxoIndex,
|
|
31
|
+
getSizeAndChangeScript,
|
|
32
|
+
rbfEnabled,
|
|
33
|
+
}) {
|
|
34
|
+
const { usableUtxos, selectedUtxos, replaceTx, change, ourAddress } = metadata
|
|
35
|
+
|
|
36
|
+
// Get change script and size
|
|
37
|
+
const { script, size } = getSizeAndChangeScript({
|
|
38
|
+
assetName,
|
|
39
|
+
tx,
|
|
40
|
+
rawTx,
|
|
41
|
+
changeUtxoIndex,
|
|
42
|
+
txId,
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
// Update remaining UTXOs
|
|
46
|
+
const knownBalanceUtxoIds = accountState.knownBalanceUtxoIds || []
|
|
47
|
+
let remainingUtxos = usableUtxos.difference(selectedUtxos)
|
|
48
|
+
|
|
49
|
+
// Add change UTXO if present
|
|
50
|
+
if (changeUtxoIndex !== -1 && ourAddress) {
|
|
51
|
+
const address = Address.create(ourAddress.address || ourAddress, ourAddress.meta || {})
|
|
52
|
+
const changeUtxo = {
|
|
53
|
+
txId,
|
|
54
|
+
address,
|
|
55
|
+
vout: changeUtxoIndex,
|
|
56
|
+
script,
|
|
57
|
+
value: change,
|
|
58
|
+
confirmations: 0,
|
|
59
|
+
rbfEnabled,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
knownBalanceUtxoIds.push(`${changeUtxo.txId}:${changeUtxo.vout}`.toLowerCase())
|
|
63
|
+
remainingUtxos = remainingUtxos.addUtxo(changeUtxo)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Remove replaced transaction UTXOs if present
|
|
67
|
+
if (replaceTx) {
|
|
68
|
+
remainingUtxos = remainingUtxos.difference(remainingUtxos.getTxIdUtxos(replaceTx.txId))
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Update account state
|
|
72
|
+
await assetClientInterface.updateAccountState({
|
|
73
|
+
assetName,
|
|
74
|
+
walletAccount,
|
|
75
|
+
newData: {
|
|
76
|
+
utxos: remainingUtxos,
|
|
77
|
+
knownBalanceUtxoIds,
|
|
78
|
+
},
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
return { size }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Update transaction log with new transaction
|
|
86
|
+
* @param {Object} params
|
|
87
|
+
* @param {Object} params.asset - Asset object
|
|
88
|
+
* @param {Object} params.assetClientInterface - Asset client interface
|
|
89
|
+
* @param {Object} params.walletAccount - Wallet account
|
|
90
|
+
* @param {string} params.txId - Transaction ID
|
|
91
|
+
* @param {Object} params.fee - Transaction fee
|
|
92
|
+
* @param {Object} params.metadata - Transaction metadata
|
|
93
|
+
* @param {string} params.address - Recipient address
|
|
94
|
+
* @param {Object} params.amount - Transaction amount (for regular sends)
|
|
95
|
+
* @param {string} params.bumpTxId - ID of transaction being bumped (if applicable)
|
|
96
|
+
* @param {number} params.size - Transaction size
|
|
97
|
+
* @param {number} params.blockHeight - Block height
|
|
98
|
+
* @param {boolean} params.rbfEnabled - Whether RBF is enabled
|
|
99
|
+
*/
|
|
100
|
+
export async function updateTransactionLog({
|
|
101
|
+
asset,
|
|
102
|
+
assetClientInterface,
|
|
103
|
+
walletAccount,
|
|
104
|
+
txId,
|
|
105
|
+
fee,
|
|
106
|
+
metadata,
|
|
107
|
+
address,
|
|
108
|
+
amount,
|
|
109
|
+
bumpTxId,
|
|
110
|
+
size,
|
|
111
|
+
blockHeight,
|
|
112
|
+
rbfEnabled,
|
|
113
|
+
}) {
|
|
114
|
+
const { totalAmount, selectedUtxos, replaceTx, changeOutput, ourAddress } = metadata
|
|
115
|
+
const assetName = asset.name
|
|
116
|
+
|
|
117
|
+
// Check if this is a self-send
|
|
118
|
+
const config = await assetClientInterface.getAssetConfig?.({
|
|
119
|
+
assetName,
|
|
120
|
+
walletAccount,
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
const walletAddressObjects = await assetClientInterface.getReceiveAddresses({
|
|
124
|
+
walletAccount,
|
|
125
|
+
assetName,
|
|
126
|
+
multiAddressMode: config?.multiAddressMode ?? true,
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
// There are two cases of bumping, replacing or chaining a self-send.
|
|
130
|
+
// If we have a bumpTxId, but we aren't replacing, then it is a self-send.
|
|
131
|
+
const selfSend = bumpTxId
|
|
132
|
+
? !replaceTx
|
|
133
|
+
: walletAddressObjects.some((receiveAddress) => String(receiveAddress) === String(address))
|
|
134
|
+
|
|
135
|
+
const displayReceiveAddress = asset.address.displayAddress?.(address) || address
|
|
136
|
+
|
|
137
|
+
// Build receivers list
|
|
138
|
+
const receivers = bumpTxId
|
|
139
|
+
? replaceTx
|
|
140
|
+
? replaceTx.data.sent
|
|
141
|
+
: []
|
|
142
|
+
: replaceTx
|
|
143
|
+
? [
|
|
144
|
+
...replaceTx.data.sent,
|
|
145
|
+
{ address: displayReceiveAddress, amount: serializeCurrency(amount, asset.currency) },
|
|
146
|
+
]
|
|
147
|
+
: [{ address: displayReceiveAddress, amount: serializeCurrency(amount, asset.currency) }]
|
|
148
|
+
|
|
149
|
+
// Calculate coin amount
|
|
150
|
+
const coinAmount = selfSend ? asset.currency.ZERO : totalAmount.abs().negate()
|
|
151
|
+
|
|
152
|
+
// Update transaction log
|
|
153
|
+
await assetClientInterface.updateTxLogAndNotify({
|
|
154
|
+
assetName,
|
|
155
|
+
walletAccount,
|
|
156
|
+
txs: [
|
|
157
|
+
{
|
|
158
|
+
txId,
|
|
159
|
+
confirmations: 0,
|
|
160
|
+
coinAmount,
|
|
161
|
+
coinName: asset.name,
|
|
162
|
+
feeAmount: fee,
|
|
163
|
+
feeCoinName: assetName,
|
|
164
|
+
selfSend,
|
|
165
|
+
data: {
|
|
166
|
+
sent: selfSend ? [] : receivers,
|
|
167
|
+
rbfEnabled,
|
|
168
|
+
feePerKB: size ? fee.div(size / 1000).toBaseNumber() : undefined,
|
|
169
|
+
changeAddress: changeOutput ? ourAddress : undefined,
|
|
170
|
+
blockHeight,
|
|
171
|
+
blocksSeen: 0,
|
|
172
|
+
inputs: selectedUtxos.toJSON(),
|
|
173
|
+
replacedTxId: replaceTx ? replaceTx.txId : undefined,
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
],
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
// If replacing a transaction, update the old one
|
|
180
|
+
if (replaceTx) {
|
|
181
|
+
replaceTx.data.replacedBy = txId
|
|
182
|
+
await assetClientInterface.updateTxLogAndNotify({
|
|
183
|
+
assetName,
|
|
184
|
+
walletAccount,
|
|
185
|
+
txs: [replaceTx],
|
|
186
|
+
})
|
|
187
|
+
}
|
|
188
|
+
}
|