@exodus/bitcoin-api 4.6.0 → 4.7.0
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/psbt-builder.js +5 -0
- package/src/psbt-parser.js +169 -34
- package/src/tx-create/create-tx.js +38 -45
- package/src/tx-send/index.js +129 -123
- package/src/tx-send/update-state.js +40 -63
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.7.0](https://github.com/ExodusMovement/assets/compare/@exodus/bitcoin-api@4.6.0...@exodus/bitcoin-api@4.7.0) (2025-11-17)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
* feat: remove giant metadata from createTx result (#6860)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
6
16
|
## [4.6.0](https://github.com/ExodusMovement/assets/compare/@exodus/bitcoin-api@4.2.1...@exodus/bitcoin-api@4.6.0) (2025-11-14)
|
|
7
17
|
|
|
8
18
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/bitcoin-api",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.7.0",
|
|
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": "e281f3eae247487330a0ae16eb57372fa7982ca2"
|
|
64
64
|
}
|
package/src/psbt-builder.js
CHANGED
|
@@ -245,6 +245,7 @@ export async function createPsbtWithMetadata({
|
|
|
245
245
|
addressPathsMap,
|
|
246
246
|
purposeXPubs,
|
|
247
247
|
allowedPurposes,
|
|
248
|
+
changeOutputIndex: metadata.changeOutputIndex,
|
|
248
249
|
})
|
|
249
250
|
psbt.addOutput(psbtOutput)
|
|
250
251
|
})
|
|
@@ -254,5 +255,9 @@ export async function createPsbtWithMetadata({
|
|
|
254
255
|
writePsbtOutputField(psbt, sendOutputIndex, SubType.OutputRole, 'primary')
|
|
255
256
|
}
|
|
256
257
|
|
|
258
|
+
if (metadata.changeOutputIndex !== undefined) {
|
|
259
|
+
writePsbtOutputField(psbt, metadata.changeOutputIndex, SubType.OutputRole, 'change')
|
|
260
|
+
}
|
|
261
|
+
|
|
257
262
|
return psbt
|
|
258
263
|
}
|
package/src/psbt-parser.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { publicKeyToX } from '@exodus/crypto/secp256k1'
|
|
2
|
-
import { UtxoCollection } from '@exodus/models'
|
|
2
|
+
import { Address, UtxoCollection } from '@exodus/models'
|
|
3
3
|
import BipPath from 'bip32-path'
|
|
4
4
|
import assert from 'minimalistic-assert'
|
|
5
5
|
|
|
@@ -70,7 +70,7 @@ function parseSingleInput({ txInput, psbtInput, index, asset, purposeXPubs, Tran
|
|
|
70
70
|
throw new Error(`Input ${index} has no derivation path`)
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
input.address = { address, meta: { path: derivation.path
|
|
73
|
+
input.address = { address, meta: { path: derivation.path } }
|
|
74
74
|
return input
|
|
75
75
|
}
|
|
76
76
|
|
|
@@ -222,32 +222,22 @@ function getSelectedUtxos({ parsedInputs, utxos, asset, txSet }) {
|
|
|
222
222
|
async function getChangeOutputData({ outputs, assetClientInterface, walletAccount, asset }) {
|
|
223
223
|
let changeOutputData = null
|
|
224
224
|
for (const [i, output] of outputs.entries()) {
|
|
225
|
-
|
|
226
|
-
// (common for self‑sends, consolidations, etc.), so skip them during change detection.
|
|
227
|
-
if (output.metadata?.outputRole === 'primary') {
|
|
225
|
+
if (output.metadata?.outputRole !== 'change') {
|
|
228
226
|
continue
|
|
229
227
|
}
|
|
230
228
|
|
|
231
|
-
// We only treat addresses we can re-derive from the wallet xpub as potential
|
|
232
|
-
// change outputs.
|
|
233
|
-
if (!output.address?.meta?.path) {
|
|
234
|
-
continue
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
// At this point we already know the output was derived by our wallet and it is
|
|
238
|
-
// not explicitly flagged as a primary/send output, so we tentatively treat it as
|
|
239
|
-
// change and confirm by re-deriving the on-chain address.
|
|
240
229
|
if (changeOutputData) {
|
|
241
|
-
// Multiple change outputs are a smell. This can happen if an external PSBT
|
|
242
|
-
// marks a send output of the same wallet with its derivation path, so we fail fast instead of
|
|
243
|
-
// silently mislabelling funds.
|
|
244
230
|
throw new Error('Multiple change outputs are not allowed')
|
|
245
231
|
}
|
|
246
232
|
|
|
233
|
+
if (!output.address?.meta?.path || !output.address?.meta?.purpose) {
|
|
234
|
+
throw new Error('Change output has no derivation path or purpose')
|
|
235
|
+
}
|
|
236
|
+
|
|
247
237
|
const { path, purpose } = output.address.meta
|
|
248
238
|
const [chainIndex, addressIndex] = BipPath.fromString(path).toPathArray()
|
|
249
239
|
|
|
250
|
-
|
|
240
|
+
let ourAddress = await assetClientInterface.getAddress({
|
|
251
241
|
assetName: asset.name,
|
|
252
242
|
walletAccount: walletAccount.toString(),
|
|
253
243
|
purpose,
|
|
@@ -255,10 +245,15 @@ async function getChangeOutputData({ outputs, assetClientInterface, walletAccoun
|
|
|
255
245
|
addressIndex,
|
|
256
246
|
})
|
|
257
247
|
|
|
258
|
-
if (
|
|
248
|
+
if (asset.address.toLegacyAddress) {
|
|
249
|
+
const legacyAddress = asset.address.toLegacyAddress(ourAddress.address)
|
|
250
|
+
ourAddress = Address.create(legacyAddress, ourAddress.meta)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (ourAddress.toString() === output.address.address) {
|
|
259
254
|
changeOutputData = {
|
|
260
|
-
address:
|
|
261
|
-
amount: output.amount,
|
|
255
|
+
address: ourAddress,
|
|
256
|
+
amount: toBigIntAmount(output.amount),
|
|
262
257
|
index: i,
|
|
263
258
|
}
|
|
264
259
|
} else {
|
|
@@ -316,6 +311,17 @@ function buildAddressPathsMap(selectedUtxos, changeOutputData) {
|
|
|
316
311
|
return addressPathsMap
|
|
317
312
|
}
|
|
318
313
|
|
|
314
|
+
function buildOutputAddressPurposesMap(outputs) {
|
|
315
|
+
const map = Object.create(null)
|
|
316
|
+
for (const output of outputs) {
|
|
317
|
+
if (output.address?.meta?.purpose) {
|
|
318
|
+
map[output.address.address] = output.address.meta.purpose
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return map
|
|
323
|
+
}
|
|
324
|
+
|
|
319
325
|
function extractRawTransactions(parsedInputs) {
|
|
320
326
|
const rawTxsData = parsedInputs
|
|
321
327
|
.filter((parsedInput) => parsedInput.prevTxId)
|
|
@@ -392,8 +398,127 @@ export async function parsePsbt({
|
|
|
392
398
|
}
|
|
393
399
|
}
|
|
394
400
|
|
|
401
|
+
function toBigIntAmount(value) {
|
|
402
|
+
return Buffer.isBuffer(value) ? value.readBigInt64LE(0) : BigInt(value)
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function parseUnsignedTx({ unsignedTx }) {
|
|
406
|
+
const { txData, txMeta } = unsignedTx
|
|
407
|
+
assert(txData, 'txData is required')
|
|
408
|
+
assert(txMeta, 'txMeta is required')
|
|
409
|
+
|
|
410
|
+
const {
|
|
411
|
+
addressPathsMap = Object.create(null),
|
|
412
|
+
outputAddressPurposesMap = Object.create(null),
|
|
413
|
+
changeOutputIndex,
|
|
414
|
+
rawTxs = [],
|
|
415
|
+
blockHeight,
|
|
416
|
+
sendOutputIndexes = [],
|
|
417
|
+
rbfEnabled = false,
|
|
418
|
+
bumpTxId,
|
|
419
|
+
txType,
|
|
420
|
+
} = txMeta
|
|
421
|
+
|
|
422
|
+
const rawTxMap = new Map(rawTxs.map((tx) => [tx.txId, tx.rawData]))
|
|
423
|
+
|
|
424
|
+
const inputs = (txData.inputs || []).map((input) =>
|
|
425
|
+
parseUnsignedInput({
|
|
426
|
+
input,
|
|
427
|
+
addressPathsMap,
|
|
428
|
+
rawTxMap,
|
|
429
|
+
})
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
const outputs = (txData.outputs || []).map((output, index) =>
|
|
433
|
+
parseUnsignedOutput({
|
|
434
|
+
index,
|
|
435
|
+
output,
|
|
436
|
+
addressPathsMap,
|
|
437
|
+
outputAddressPurposesMap,
|
|
438
|
+
sendOutputIndexes,
|
|
439
|
+
changeOutputIndex,
|
|
440
|
+
})
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
const fee = computeFee(inputs, outputs)
|
|
444
|
+
|
|
445
|
+
const globalMetadata = {
|
|
446
|
+
blockHeight,
|
|
447
|
+
rbfEnabled,
|
|
448
|
+
bumpTxId,
|
|
449
|
+
txType,
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
return { inputs, outputs, fee, globalMetadata }
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function computeFee(inputs, outputs) {
|
|
456
|
+
const inputSum = inputs.reduce((sum, input) => sum + toBigIntAmount(input.value), BigInt(0))
|
|
457
|
+
const outputSum = outputs.reduce((sum, output) => sum + toBigIntAmount(output.amount), BigInt(0))
|
|
458
|
+
return inputSum - outputSum
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function parseUnsignedInput({ input, addressPathsMap, rawTxMap }) {
|
|
462
|
+
const { txId, vout, sequence, value, script, address: addressString } = input
|
|
463
|
+
|
|
464
|
+
const path = addressPathsMap[addressString]
|
|
465
|
+
|
|
466
|
+
const parsedInput = {
|
|
467
|
+
txId,
|
|
468
|
+
vout,
|
|
469
|
+
sequence,
|
|
470
|
+
value,
|
|
471
|
+
script,
|
|
472
|
+
address: { address: addressString, meta: { path } },
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const rawTxHex = rawTxMap.get(txId)
|
|
476
|
+
if (rawTxHex) {
|
|
477
|
+
parsedInput.prevTxHex = rawTxHex
|
|
478
|
+
parsedInput.prevTxId = txId
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
return parsedInput
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function parseUnsignedOutput({
|
|
485
|
+
index,
|
|
486
|
+
output,
|
|
487
|
+
addressPathsMap,
|
|
488
|
+
outputAddressPurposesMap,
|
|
489
|
+
sendOutputIndexes,
|
|
490
|
+
changeOutputIndex,
|
|
491
|
+
}) {
|
|
492
|
+
const [addressString, amount] = output
|
|
493
|
+
|
|
494
|
+
const purpose = outputAddressPurposesMap[addressString]
|
|
495
|
+
const path = addressPathsMap[addressString]
|
|
496
|
+
|
|
497
|
+
const addressObject = { address: addressString }
|
|
498
|
+
if (purpose !== undefined) {
|
|
499
|
+
addressObject.meta = { purpose }
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if (path !== undefined) {
|
|
503
|
+
addressObject.meta = { ...addressObject.meta, path }
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const parsedOutput = {
|
|
507
|
+
amount,
|
|
508
|
+
address: addressObject,
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
if (sendOutputIndexes.includes(index)) {
|
|
512
|
+
parsedOutput.metadata = { outputRole: 'primary' }
|
|
513
|
+
} else if (changeOutputIndex === index) {
|
|
514
|
+
parsedOutput.metadata = { outputRole: 'change' }
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
return parsedOutput
|
|
518
|
+
}
|
|
519
|
+
|
|
395
520
|
export async function extractTransactionContext({
|
|
396
|
-
|
|
521
|
+
unsignedTx,
|
|
397
522
|
asset,
|
|
398
523
|
assetClientInterface,
|
|
399
524
|
walletAccount,
|
|
@@ -401,27 +526,31 @@ export async function extractTransactionContext({
|
|
|
401
526
|
Psbt,
|
|
402
527
|
Transaction,
|
|
403
528
|
}) {
|
|
404
|
-
assert(
|
|
529
|
+
assert(unsignedTx, 'unsignedTx is required')
|
|
405
530
|
assert(asset, 'asset is required')
|
|
406
531
|
assert(assetClientInterface, 'assetClientInterface is required')
|
|
407
532
|
assert(walletAccount, 'walletAccount is required')
|
|
408
|
-
|
|
409
|
-
|
|
533
|
+
|
|
534
|
+
const psbtBuffer = unsignedTx?.txData?.psbtBuffer
|
|
535
|
+
|
|
536
|
+
const parsed = psbtBuffer
|
|
537
|
+
? await parsePsbt({
|
|
538
|
+
psbtBuffer,
|
|
539
|
+
asset,
|
|
540
|
+
assetClientInterface,
|
|
541
|
+
walletAccount,
|
|
542
|
+
allowedPurposes,
|
|
543
|
+
Psbt,
|
|
544
|
+
Transaction,
|
|
545
|
+
})
|
|
546
|
+
: parseUnsignedTx({ unsignedTx })
|
|
410
547
|
|
|
411
548
|
const {
|
|
412
549
|
inputs: parsedInputs,
|
|
413
550
|
outputs: parsedOutputs,
|
|
414
551
|
fee: calculatedFee,
|
|
415
552
|
globalMetadata,
|
|
416
|
-
} =
|
|
417
|
-
psbtBuffer,
|
|
418
|
-
asset,
|
|
419
|
-
assetClientInterface,
|
|
420
|
-
walletAccount,
|
|
421
|
-
allowedPurposes,
|
|
422
|
-
Psbt,
|
|
423
|
-
Transaction,
|
|
424
|
-
})
|
|
553
|
+
} = parsed
|
|
425
554
|
|
|
426
555
|
const changeOutputData = await getChangeOutputData({
|
|
427
556
|
outputs: parsedOutputs,
|
|
@@ -482,6 +611,7 @@ export async function extractTransactionContext({
|
|
|
482
611
|
)
|
|
483
612
|
|
|
484
613
|
const addressPathsMap = buildAddressPathsMap(selectedUtxos, changeOutputData)
|
|
614
|
+
const outputAddressPurposesMap = buildOutputAddressPurposesMap(parsedOutputs)
|
|
485
615
|
const rawTxs = extractRawTransactions(parsedInputs)
|
|
486
616
|
|
|
487
617
|
return {
|
|
@@ -492,6 +622,7 @@ export async function extractTransactionContext({
|
|
|
492
622
|
fee: asset.currency.baseUnit(calculatedFee),
|
|
493
623
|
sendOutputIndexes: primaryOutputIndexes,
|
|
494
624
|
changeOutputIndex: changeOutputData?.index,
|
|
625
|
+
outputAddressPurposesMap,
|
|
495
626
|
sendAmounts,
|
|
496
627
|
changeAmount,
|
|
497
628
|
totalSendAmount,
|
|
@@ -505,5 +636,9 @@ export async function extractTransactionContext({
|
|
|
505
636
|
changeOutput: changeOutputData ? outputs[changeOutputData.index] : undefined,
|
|
506
637
|
rbfEnabled: globalMetadata.rbfEnabled,
|
|
507
638
|
blockHeight: globalMetadata.blockHeight,
|
|
639
|
+
bumpTxId: globalMetadata.bumpTxId,
|
|
640
|
+
txType: globalMetadata.txType,
|
|
641
|
+
useCashAddress: unsignedTx?.txMeta?.useCashAddress,
|
|
642
|
+
psbtBuffer,
|
|
508
643
|
}
|
|
509
644
|
}
|
|
@@ -174,6 +174,7 @@ function createTransactionOutputs({
|
|
|
174
174
|
const assetName = asset.name
|
|
175
175
|
let outputs = []
|
|
176
176
|
let sendOutput
|
|
177
|
+
let adjustedFee
|
|
177
178
|
|
|
178
179
|
// Add existing outputs from replacement transaction
|
|
179
180
|
if (replaceTx) {
|
|
@@ -196,8 +197,7 @@ function createTransactionOutputs({
|
|
|
196
197
|
)
|
|
197
198
|
: sendAmount
|
|
198
199
|
|
|
199
|
-
|
|
200
|
-
const { changeOutput, adjustedFee, changeAddressKeypath, ourAddress } = createChangeOutput({
|
|
200
|
+
const changeOutputData = createChangeOutput({
|
|
201
201
|
selectedUtxos,
|
|
202
202
|
totalAmount,
|
|
203
203
|
fee,
|
|
@@ -206,18 +206,23 @@ function createTransactionOutputs({
|
|
|
206
206
|
asset,
|
|
207
207
|
})
|
|
208
208
|
|
|
209
|
-
if (
|
|
210
|
-
outputs.push(changeOutput)
|
|
209
|
+
if (changeOutputData) {
|
|
210
|
+
outputs.push(changeOutputData.changeOutput)
|
|
211
|
+
adjustedFee = fee
|
|
212
|
+
} else {
|
|
213
|
+
// If we don't have enough for a change output, then all remaining dust is just added to fee
|
|
214
|
+
const changeAmount = selectedUtxos.value.sub(totalAmount).sub(fee)
|
|
215
|
+
adjustedFee = fee.add(changeAmount)
|
|
211
216
|
}
|
|
212
217
|
|
|
213
218
|
return {
|
|
214
219
|
outputs: replaceTx ? outputs : shuffle(outputs),
|
|
215
220
|
sendOutput,
|
|
216
|
-
|
|
217
|
-
|
|
221
|
+
ourAddress: changeOutputData?.ourAddress,
|
|
222
|
+
changeOutput: changeOutputData?.changeOutput,
|
|
223
|
+
changeAddressPath: changeOutputData?.changeAddressPath,
|
|
224
|
+
changeAddressPurpose: changeOutputData?.changeAddressPurpose,
|
|
218
225
|
adjustedFee,
|
|
219
|
-
changeAddressKeypath,
|
|
220
|
-
ourAddress,
|
|
221
226
|
}
|
|
222
227
|
}
|
|
223
228
|
|
|
@@ -233,7 +238,7 @@ function createChangeOutput({ selectedUtxos, totalAmount, fee, replaceTx, change
|
|
|
233
238
|
ourAddress = Address.create(legacyAddress, ourAddress.meta)
|
|
234
239
|
}
|
|
235
240
|
|
|
236
|
-
// Create change output if above dust threshold
|
|
241
|
+
// Create change output if above dust threshold.
|
|
237
242
|
if (change.gte(dust)) {
|
|
238
243
|
const changeOutput = createOutput(
|
|
239
244
|
asset.name,
|
|
@@ -241,22 +246,16 @@ function createChangeOutput({ selectedUtxos, totalAmount, fee, replaceTx, change
|
|
|
241
246
|
change
|
|
242
247
|
)
|
|
243
248
|
|
|
244
|
-
// Return the
|
|
249
|
+
// Return the path for hardware wallet change detection
|
|
245
250
|
return {
|
|
246
|
-
changeOutput,
|
|
247
|
-
adjustedFee: fee,
|
|
248
|
-
changeAddressKeypath: ourAddress.meta.path,
|
|
249
251
|
ourAddress,
|
|
252
|
+
changeOutput,
|
|
253
|
+
changeAddressPath: ourAddress.meta.path,
|
|
254
|
+
changeAddressPurpose: ourAddress.meta.purpose,
|
|
250
255
|
}
|
|
251
256
|
}
|
|
252
257
|
|
|
253
|
-
|
|
254
|
-
return {
|
|
255
|
-
changeOutput: undefined,
|
|
256
|
-
adjustedFee: fee.add(change),
|
|
257
|
-
changeAddressKeypath: undefined,
|
|
258
|
-
ourAddress,
|
|
259
|
-
}
|
|
258
|
+
return null
|
|
260
259
|
}
|
|
261
260
|
|
|
262
261
|
async function createUnsignedTx({
|
|
@@ -275,6 +274,7 @@ async function createUnsignedTx({
|
|
|
275
274
|
txType = 'transfer',
|
|
276
275
|
sendOutputIndex,
|
|
277
276
|
changeOutputIndex,
|
|
277
|
+
outputAddressPurposesMap,
|
|
278
278
|
allowedPurposes,
|
|
279
279
|
Psbt,
|
|
280
280
|
Transaction,
|
|
@@ -291,6 +291,13 @@ async function createUnsignedTx({
|
|
|
291
291
|
addressPathsMap,
|
|
292
292
|
blockHeight,
|
|
293
293
|
rawTxs: nonWitnessTxs,
|
|
294
|
+
sendOutputIndexes:
|
|
295
|
+
sendOutputIndex === undefined || sendOutputIndex === null ? [] : [sendOutputIndex],
|
|
296
|
+
changeOutputIndex,
|
|
297
|
+
outputAddressPurposesMap,
|
|
298
|
+
rbfEnabled,
|
|
299
|
+
bumpTxId,
|
|
300
|
+
txType,
|
|
294
301
|
},
|
|
295
302
|
}
|
|
296
303
|
|
|
@@ -450,10 +457,10 @@ const transferHandler = {
|
|
|
450
457
|
outputs,
|
|
451
458
|
sendOutput,
|
|
452
459
|
changeOutput,
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
changeAddressKeypath,
|
|
460
|
+
changeAddressPath,
|
|
461
|
+
changeAddressPurpose,
|
|
456
462
|
ourAddress,
|
|
463
|
+
adjustedFee,
|
|
457
464
|
} = createTransactionOutputs({
|
|
458
465
|
replaceTx: processedReplaceTx,
|
|
459
466
|
processedAddress,
|
|
@@ -464,11 +471,15 @@ const transferHandler = {
|
|
|
464
471
|
changeAddress: context.changeAddress,
|
|
465
472
|
})
|
|
466
473
|
|
|
474
|
+
// Create a map of wallet's own output addresses and their purposes.
|
|
475
|
+
const outputAddressPurposesMap = Object.create(null)
|
|
476
|
+
|
|
467
477
|
// Add the keypath of change address to support Trezor detect the change output.
|
|
468
478
|
// Output is change and does not need approval from user which shows the strange address that user never seen.
|
|
469
|
-
if (
|
|
479
|
+
if (changeAddressPath && changeAddressPurpose) {
|
|
470
480
|
const changeKey = ourAddress?.address ?? String(ourAddress)
|
|
471
|
-
addressPathsMap[changeKey] =
|
|
481
|
+
addressPathsMap[changeKey] = changeAddressPath
|
|
482
|
+
outputAddressPurposesMap[changeKey] = changeAddressPurpose
|
|
472
483
|
}
|
|
473
484
|
|
|
474
485
|
// Create unsigned transaction
|
|
@@ -477,6 +488,7 @@ const transferHandler = {
|
|
|
477
488
|
outputs,
|
|
478
489
|
useCashAddress,
|
|
479
490
|
addressPathsMap,
|
|
491
|
+
outputAddressPurposesMap,
|
|
480
492
|
blockHeight: context.blockHeight,
|
|
481
493
|
asset,
|
|
482
494
|
selectedUtxos,
|
|
@@ -493,26 +505,7 @@ const transferHandler = {
|
|
|
493
505
|
Transaction,
|
|
494
506
|
})
|
|
495
507
|
|
|
496
|
-
return {
|
|
497
|
-
unsignedTx,
|
|
498
|
-
metadata: {
|
|
499
|
-
fee: adjustedFee,
|
|
500
|
-
amount,
|
|
501
|
-
change: selectedUtxos.value.sub(totalAmount).sub(adjustedFee),
|
|
502
|
-
totalAmount,
|
|
503
|
-
address: processedAddress,
|
|
504
|
-
ourAddress,
|
|
505
|
-
receiveAddress: utxoParams.receiveAddress,
|
|
506
|
-
sendAmount: utxoParams.sendAmount,
|
|
507
|
-
usableUtxos: context.usableUtxos,
|
|
508
|
-
selectedUtxos,
|
|
509
|
-
replaceTx: processedReplaceTx,
|
|
510
|
-
sendOutput,
|
|
511
|
-
changeOutput,
|
|
512
|
-
blockHeight: context.blockHeight,
|
|
513
|
-
rbfEnabled: context.rbfEnabled,
|
|
514
|
-
},
|
|
515
|
-
}
|
|
508
|
+
return { unsignedTx, fee: adjustedFee }
|
|
516
509
|
},
|
|
517
510
|
}
|
|
518
511
|
|
package/src/tx-send/index.js
CHANGED
|
@@ -99,65 +99,21 @@ export async function signTransaction({
|
|
|
99
99
|
return { rawTx, txId, tx }
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
|
|
103
|
-
address,
|
|
104
|
-
allowUnconfirmedRbfEnabledUtxos,
|
|
105
|
-
amount,
|
|
106
|
-
asset,
|
|
107
|
-
assetClientInterface,
|
|
108
|
-
changeAddressType,
|
|
109
|
-
getFeeEstimator,
|
|
110
|
-
options,
|
|
111
|
-
utxosDescendingOrder,
|
|
112
|
-
walletAccount,
|
|
113
|
-
allowedPurposes,
|
|
114
|
-
Psbt,
|
|
115
|
-
Transaction,
|
|
116
|
-
}) => {
|
|
117
|
-
const createTx = createTxFactory({
|
|
118
|
-
getFeeEstimator,
|
|
119
|
-
allowUnconfirmedRbfEnabledUtxos,
|
|
120
|
-
utxosDescendingOrder,
|
|
121
|
-
assetClientInterface,
|
|
122
|
-
changeAddressType,
|
|
123
|
-
allowedPurposes,
|
|
124
|
-
Psbt,
|
|
125
|
-
Transaction,
|
|
126
|
-
})
|
|
127
|
-
|
|
128
|
-
// Set default values for options
|
|
129
|
-
const { isRbfAllowed = true, ...restOptions } = options || Object.create(null)
|
|
130
|
-
|
|
131
|
-
return createTx({
|
|
132
|
-
asset,
|
|
133
|
-
walletAccount,
|
|
134
|
-
type: 'transfer',
|
|
135
|
-
toAddress: address,
|
|
136
|
-
amount,
|
|
137
|
-
isRbfAllowed,
|
|
138
|
-
...restOptions,
|
|
139
|
-
})
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function toTransactionDescriptor({ txContext, psbtBuffer }) {
|
|
102
|
+
function getTransferUnsignedTx(txContext) {
|
|
143
103
|
const {
|
|
144
104
|
inputs,
|
|
145
105
|
outputs,
|
|
106
|
+
psbtBuffer,
|
|
107
|
+
useCashAddress,
|
|
146
108
|
addressPathsMap,
|
|
109
|
+
outputAddressPurposesMap,
|
|
147
110
|
blockHeight,
|
|
148
111
|
rawTxs,
|
|
149
|
-
|
|
150
|
-
totalSendAmount,
|
|
151
|
-
changeAmount,
|
|
152
|
-
totalAmount,
|
|
153
|
-
primaryAddresses,
|
|
154
|
-
ourAddress,
|
|
155
|
-
usableUtxos,
|
|
156
|
-
selectedUtxos,
|
|
157
|
-
replaceTx,
|
|
158
|
-
sendOutputs,
|
|
159
|
-
changeOutput,
|
|
112
|
+
txType,
|
|
160
113
|
rbfEnabled,
|
|
114
|
+
bumpTxId,
|
|
115
|
+
changeOutputIndex,
|
|
116
|
+
sendOutputIndexes,
|
|
161
117
|
} = txContext
|
|
162
118
|
|
|
163
119
|
return {
|
|
@@ -168,27 +124,47 @@ function toTransactionDescriptor({ txContext, psbtBuffer }) {
|
|
|
168
124
|
psbtBuffer,
|
|
169
125
|
},
|
|
170
126
|
txMeta: {
|
|
127
|
+
useCashAddress,
|
|
171
128
|
addressPathsMap,
|
|
129
|
+
outputAddressPurposesMap,
|
|
172
130
|
blockHeight,
|
|
173
131
|
rawTxs,
|
|
132
|
+
txType,
|
|
133
|
+
rbfEnabled,
|
|
134
|
+
bumpTxId,
|
|
135
|
+
changeOutputIndex,
|
|
136
|
+
sendOutputIndexes,
|
|
174
137
|
},
|
|
175
138
|
},
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Get additional transaction metadata that are not part of the unsignedTx.txMeta
|
|
143
|
+
function getExtendedTxMeta(txContext) {
|
|
144
|
+
const {
|
|
145
|
+
fee,
|
|
146
|
+
totalSendAmount,
|
|
147
|
+
changeAmount,
|
|
148
|
+
totalAmount,
|
|
149
|
+
primaryAddresses,
|
|
150
|
+
ourAddress,
|
|
151
|
+
usableUtxos,
|
|
152
|
+
selectedUtxos,
|
|
153
|
+
replaceTx,
|
|
154
|
+
changeOutput,
|
|
155
|
+
} = txContext
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
fee,
|
|
159
|
+
sendAmount: totalSendAmount,
|
|
160
|
+
changeAmount,
|
|
161
|
+
totalAmount,
|
|
162
|
+
toAddress: primaryAddresses[0]?.address,
|
|
163
|
+
ourAddress,
|
|
164
|
+
usableUtxos,
|
|
165
|
+
selectedUtxos,
|
|
166
|
+
replaceTx,
|
|
167
|
+
changeOutput,
|
|
192
168
|
}
|
|
193
169
|
}
|
|
194
170
|
|
|
@@ -206,53 +182,77 @@ export const createAndBroadcastTXFactory =
|
|
|
206
182
|
Psbt = DefaultPsbt,
|
|
207
183
|
Transaction = DefaultTransaction,
|
|
208
184
|
}) =>
|
|
209
|
-
async ({
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
185
|
+
async ({
|
|
186
|
+
asset,
|
|
187
|
+
walletAccount,
|
|
188
|
+
address,
|
|
189
|
+
amount,
|
|
190
|
+
multipleAddressesEnabled,
|
|
191
|
+
feePerKB,
|
|
192
|
+
customFee,
|
|
193
|
+
isSendAll,
|
|
194
|
+
bumpTxId: bumpTxIdProvided,
|
|
195
|
+
isExchange,
|
|
196
|
+
isRbfAllowed = true,
|
|
197
|
+
taprootInputWitnessSize,
|
|
198
|
+
}) => {
|
|
213
199
|
const assetName = asset.name
|
|
214
200
|
const accountState = await assetClientInterface.getAccountState({ assetName, walletAccount })
|
|
215
201
|
|
|
216
|
-
|
|
217
|
-
|
|
202
|
+
const createTx = createTxFactory({
|
|
203
|
+
getFeeEstimator,
|
|
218
204
|
allowUnconfirmedRbfEnabledUtxos,
|
|
205
|
+
utxosDescendingOrder,
|
|
206
|
+
assetClientInterface,
|
|
207
|
+
changeAddressType,
|
|
208
|
+
allowedPurposes,
|
|
209
|
+
Psbt,
|
|
210
|
+
Transaction,
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
const { unsignedTx: unsignedTxByCreateTx } = await createTx({
|
|
214
|
+
asset,
|
|
215
|
+
walletAccount,
|
|
216
|
+
type: 'transfer',
|
|
217
|
+
toAddress: address,
|
|
219
218
|
amount,
|
|
219
|
+
isRbfAllowed,
|
|
220
|
+
multipleAddressesEnabled,
|
|
221
|
+
feePerKB,
|
|
222
|
+
customFee,
|
|
223
|
+
isSendAll,
|
|
224
|
+
bumpTxId: bumpTxIdProvided,
|
|
225
|
+
isExchange,
|
|
226
|
+
taprootInputWitnessSize,
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
const txContext = await extractTransactionContext({
|
|
230
|
+
unsignedTx: unsignedTxByCreateTx,
|
|
220
231
|
asset,
|
|
221
232
|
assetClientInterface,
|
|
222
|
-
changeAddressType,
|
|
223
|
-
getFeeEstimator,
|
|
224
|
-
options,
|
|
225
|
-
utxosDescendingOrder,
|
|
226
233
|
walletAccount,
|
|
227
234
|
allowedPurposes,
|
|
228
235
|
Psbt,
|
|
229
236
|
Transaction,
|
|
230
237
|
})
|
|
231
238
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
transactionDescriptor = toTransactionDescriptor({ txContext, psbtBuffer })
|
|
247
|
-
unsignedTx = transactionDescriptor.unsignedTx
|
|
248
|
-
metadata = transactionDescriptor.metadata
|
|
249
|
-
} else {
|
|
250
|
-
// Legacy/non-PSBT flows stick with the original descriptor shape.
|
|
251
|
-
unsignedTx = transactionDescriptor.unsignedTx
|
|
252
|
-
metadata = transactionDescriptor.metadata
|
|
253
|
-
}
|
|
239
|
+
const { unsignedTx } = getTransferUnsignedTx(txContext)
|
|
240
|
+
|
|
241
|
+
const {
|
|
242
|
+
fee,
|
|
243
|
+
sendAmount,
|
|
244
|
+
changeAmount,
|
|
245
|
+
totalAmount,
|
|
246
|
+
toAddress,
|
|
247
|
+
ourAddress,
|
|
248
|
+
usableUtxos,
|
|
249
|
+
selectedUtxos,
|
|
250
|
+
replaceTx,
|
|
251
|
+
changeOutput,
|
|
252
|
+
} = getExtendedTxMeta(txContext)
|
|
254
253
|
|
|
255
|
-
const {
|
|
254
|
+
const { sendOutputIndexes, changeOutputIndex, bumpTxId, rbfEnabled, blockHeight } =
|
|
255
|
+
unsignedTx.txMeta
|
|
256
256
|
|
|
257
257
|
// Sign transaction
|
|
258
258
|
const { rawTx, txId, tx } = await signTransaction({
|
|
@@ -269,7 +269,7 @@ export const createAndBroadcastTXFactory =
|
|
|
269
269
|
if (/insight broadcast http error.*missing inputs/i.test(err.message)) {
|
|
270
270
|
err.txInfo = JSON.stringify({
|
|
271
271
|
amount: sendAmount.toDefaultString({ unit: true }),
|
|
272
|
-
fee: ((
|
|
272
|
+
fee: ((fee && fee.toDefaultString({ unit: true })) || 0).toString(),
|
|
273
273
|
allUtxos: usableUtxos.toJSON(),
|
|
274
274
|
})
|
|
275
275
|
}
|
|
@@ -277,34 +277,31 @@ export const createAndBroadcastTXFactory =
|
|
|
277
277
|
throw err
|
|
278
278
|
}
|
|
279
279
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
if (output) {
|
|
283
|
-
for (const [i, [address, amount]] of unsignedTx.txData.outputs.entries()) {
|
|
284
|
-
if (output[0] === address && output[1] === amount) {
|
|
285
|
-
utxoIndex = i
|
|
286
|
-
break
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
return utxoIndex
|
|
292
|
-
}
|
|
280
|
+
const changeUtxoIndex = changeOutputIndex ?? -1
|
|
281
|
+
const sendUtxoIndex = sendOutputIndexes?.[0] ?? -1
|
|
293
282
|
|
|
294
|
-
const
|
|
295
|
-
|
|
283
|
+
const { script, size } = getSizeAndChangeScript({
|
|
284
|
+
assetName,
|
|
285
|
+
tx,
|
|
286
|
+
rawTx,
|
|
287
|
+
changeUtxoIndex,
|
|
288
|
+
txId,
|
|
289
|
+
})
|
|
296
290
|
|
|
297
|
-
|
|
291
|
+
await updateAccountState({
|
|
298
292
|
assetClientInterface,
|
|
299
293
|
assetName,
|
|
300
294
|
walletAccount,
|
|
301
295
|
accountState,
|
|
302
296
|
txId,
|
|
303
|
-
metadata,
|
|
304
|
-
tx,
|
|
305
|
-
rawTx,
|
|
306
297
|
changeUtxoIndex,
|
|
307
|
-
|
|
298
|
+
script,
|
|
299
|
+
usableUtxos,
|
|
300
|
+
selectedUtxos,
|
|
301
|
+
replaceTx,
|
|
302
|
+
changeAmount,
|
|
303
|
+
ourAddress,
|
|
304
|
+
rbfEnabled,
|
|
308
305
|
})
|
|
309
306
|
|
|
310
307
|
await updateTransactionLog({
|
|
@@ -312,7 +309,16 @@ export const createAndBroadcastTXFactory =
|
|
|
312
309
|
assetClientInterface,
|
|
313
310
|
walletAccount,
|
|
314
311
|
txId,
|
|
315
|
-
|
|
312
|
+
totalAmount,
|
|
313
|
+
selectedUtxos,
|
|
314
|
+
replaceTx,
|
|
315
|
+
changeOutput,
|
|
316
|
+
ourAddress,
|
|
317
|
+
fee,
|
|
318
|
+
blockHeight,
|
|
319
|
+
rbfEnabled,
|
|
320
|
+
toAddress,
|
|
321
|
+
sendAmount,
|
|
316
322
|
bumpTxId,
|
|
317
323
|
size,
|
|
318
324
|
})
|
|
@@ -2,44 +2,21 @@ import { Address } from '@exodus/models'
|
|
|
2
2
|
|
|
3
3
|
import { serializeCurrency } from '../fee/fee-utils.js'
|
|
4
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
|
-
*/
|
|
20
5
|
export async function updateAccountState({
|
|
21
6
|
assetClientInterface,
|
|
22
7
|
assetName,
|
|
23
8
|
walletAccount,
|
|
24
9
|
accountState,
|
|
25
10
|
txId,
|
|
26
|
-
metadata,
|
|
27
|
-
tx,
|
|
28
|
-
rawTx,
|
|
29
11
|
changeUtxoIndex,
|
|
30
|
-
|
|
12
|
+
script,
|
|
13
|
+
usableUtxos,
|
|
14
|
+
selectedUtxos,
|
|
15
|
+
replaceTx,
|
|
16
|
+
changeAmount,
|
|
17
|
+
ourAddress,
|
|
18
|
+
rbfEnabled,
|
|
31
19
|
}) {
|
|
32
|
-
const { usableUtxos, selectedUtxos, replaceTx, change, ourAddress, rbfEnabled } = metadata
|
|
33
|
-
|
|
34
|
-
// Get change script and size
|
|
35
|
-
const { script, size } = getSizeAndChangeScript({
|
|
36
|
-
assetName,
|
|
37
|
-
tx,
|
|
38
|
-
rawTx,
|
|
39
|
-
changeUtxoIndex,
|
|
40
|
-
txId,
|
|
41
|
-
})
|
|
42
|
-
|
|
43
20
|
// Update remaining UTXOs
|
|
44
21
|
const knownBalanceUtxoIds = accountState.knownBalanceUtxoIds || []
|
|
45
22
|
let remainingUtxos = usableUtxos.difference(selectedUtxos)
|
|
@@ -52,7 +29,7 @@ export async function updateAccountState({
|
|
|
52
29
|
address,
|
|
53
30
|
vout: changeUtxoIndex,
|
|
54
31
|
script,
|
|
55
|
-
value:
|
|
32
|
+
value: changeAmount,
|
|
56
33
|
confirmations: 0,
|
|
57
34
|
rbfEnabled,
|
|
58
35
|
}
|
|
@@ -75,42 +52,26 @@ export async function updateAccountState({
|
|
|
75
52
|
knownBalanceUtxoIds,
|
|
76
53
|
},
|
|
77
54
|
})
|
|
78
|
-
|
|
79
|
-
return { size }
|
|
80
55
|
}
|
|
81
56
|
|
|
82
|
-
/**
|
|
83
|
-
* Update transaction log with new transaction
|
|
84
|
-
* @param {Object} params
|
|
85
|
-
* @param {Object} params.asset - Asset object
|
|
86
|
-
* @param {Object} params.assetClientInterface - Asset client interface
|
|
87
|
-
* @param {Object} params.walletAccount - Wallet account
|
|
88
|
-
* @param {string} params.txId - Transaction ID
|
|
89
|
-
* @param {Object} params.metadata - Transaction metadata
|
|
90
|
-
* @param {string} params.bumpTxId - ID of transaction being bumped (if applicable)
|
|
91
|
-
* @param {number} params.size - Transaction size
|
|
92
|
-
*/
|
|
93
57
|
export async function updateTransactionLog({
|
|
94
58
|
asset,
|
|
95
59
|
assetClientInterface,
|
|
96
60
|
walletAccount,
|
|
97
61
|
txId,
|
|
98
|
-
metadata,
|
|
99
62
|
bumpTxId,
|
|
100
63
|
size,
|
|
64
|
+
totalAmount,
|
|
65
|
+
selectedUtxos,
|
|
66
|
+
replaceTx,
|
|
67
|
+
changeOutput,
|
|
68
|
+
ourAddress,
|
|
69
|
+
fee,
|
|
70
|
+
blockHeight,
|
|
71
|
+
rbfEnabled,
|
|
72
|
+
toAddress,
|
|
73
|
+
sendAmount,
|
|
101
74
|
}) {
|
|
102
|
-
const {
|
|
103
|
-
totalAmount,
|
|
104
|
-
selectedUtxos,
|
|
105
|
-
replaceTx,
|
|
106
|
-
changeOutput,
|
|
107
|
-
ourAddress,
|
|
108
|
-
fee,
|
|
109
|
-
blockHeight,
|
|
110
|
-
rbfEnabled,
|
|
111
|
-
address,
|
|
112
|
-
amount,
|
|
113
|
-
} = metadata
|
|
114
75
|
const assetName = asset.name
|
|
115
76
|
|
|
116
77
|
// Check if this is a self-send
|
|
@@ -129,9 +90,11 @@ export async function updateTransactionLog({
|
|
|
129
90
|
// If we have a bumpTxId, but we aren't replacing, then it is a self-send.
|
|
130
91
|
const selfSend = bumpTxId
|
|
131
92
|
? !replaceTx
|
|
132
|
-
: walletAddressObjects.some((receiveAddress) => String(receiveAddress) === String(
|
|
93
|
+
: walletAddressObjects.some((receiveAddress) => String(receiveAddress) === String(toAddress))
|
|
133
94
|
|
|
134
|
-
const displayReceiveAddress = asset.address.displayAddress?.(
|
|
95
|
+
const displayReceiveAddress = asset.address.displayAddress?.(toAddress) || toAddress
|
|
96
|
+
|
|
97
|
+
const amountToSerialize = sendAmount.isZero ? undefined : sendAmount
|
|
135
98
|
|
|
136
99
|
// Build receivers list
|
|
137
100
|
const receivers = bumpTxId
|
|
@@ -141,9 +104,17 @@ export async function updateTransactionLog({
|
|
|
141
104
|
: replaceTx
|
|
142
105
|
? [
|
|
143
106
|
...replaceTx.data.sent,
|
|
144
|
-
{
|
|
107
|
+
{
|
|
108
|
+
address: displayReceiveAddress,
|
|
109
|
+
amount: serializeCurrency(amountToSerialize, asset.currency),
|
|
110
|
+
},
|
|
111
|
+
]
|
|
112
|
+
: [
|
|
113
|
+
{
|
|
114
|
+
address: displayReceiveAddress,
|
|
115
|
+
amount: serializeCurrency(amountToSerialize, asset.currency),
|
|
116
|
+
},
|
|
145
117
|
]
|
|
146
|
-
: [{ address: displayReceiveAddress, amount: serializeCurrency(amount, asset.currency) }]
|
|
147
118
|
|
|
148
119
|
// Calculate coin amount
|
|
149
120
|
const coinAmount = selfSend ? asset.currency.ZERO : totalAmount.abs().negate()
|
|
@@ -177,11 +148,17 @@ export async function updateTransactionLog({
|
|
|
177
148
|
|
|
178
149
|
// If replacing a transaction, update the old one
|
|
179
150
|
if (replaceTx) {
|
|
180
|
-
|
|
151
|
+
const updatedReplaceTx = {
|
|
152
|
+
...replaceTx,
|
|
153
|
+
data: {
|
|
154
|
+
...replaceTx.data,
|
|
155
|
+
replacedBy: txId,
|
|
156
|
+
},
|
|
157
|
+
}
|
|
181
158
|
await assetClientInterface.updateTxLogAndNotify({
|
|
182
159
|
assetName,
|
|
183
160
|
walletAccount,
|
|
184
|
-
txs: [
|
|
161
|
+
txs: [updatedReplaceTx],
|
|
185
162
|
})
|
|
186
163
|
}
|
|
187
164
|
}
|