@bsv/wallet-toolbox 2.4.20 → 2.4.22
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 +13 -0
- package/README.md +32 -0
- package/out/src/Wallet.d.ts.map +1 -1
- package/out/src/Wallet.js +14 -1
- package/out/src/Wallet.js.map +1 -1
- package/out/src/storage/StorageIdb.d.ts +12 -1
- package/out/src/storage/StorageIdb.d.ts.map +1 -1
- package/out/src/storage/StorageIdb.js +79 -14
- package/out/src/storage/StorageIdb.js.map +1 -1
- package/out/src/storage/StorageKnex.d.ts +5 -2
- package/out/src/storage/StorageKnex.d.ts.map +1 -1
- package/out/src/storage/StorageKnex.js +58 -2
- package/out/src/storage/StorageKnex.js.map +1 -1
- package/out/src/storage/StorageProvider.d.ts +7 -1
- package/out/src/storage/StorageProvider.d.ts.map +1 -1
- package/out/src/storage/StorageProvider.js +30 -1
- package/out/src/storage/StorageProvider.js.map +1 -1
- package/out/src/storage/idbHelpers.d.ts.map +1 -1
- package/out/src/storage/idbHelpers.js +2 -0
- package/out/src/storage/idbHelpers.js.map +1 -1
- package/out/src/storage/methods/actionBatch.d.ts.map +1 -1
- package/out/src/storage/methods/actionBatch.js +2 -14
- package/out/src/storage/methods/actionBatch.js.map +1 -1
- package/out/src/storage/methods/availableManagedChange.d.ts +10 -0
- package/out/src/storage/methods/availableManagedChange.d.ts.map +1 -0
- package/out/src/storage/methods/availableManagedChange.js +25 -0
- package/out/src/storage/methods/availableManagedChange.js.map +1 -0
- package/out/src/storage/methods/createAction.d.ts.map +1 -1
- package/out/src/storage/methods/createAction.js +374 -120
- package/out/src/storage/methods/createAction.js.map +1 -1
- package/out/src/storage/methods/getBeefForTransaction.d.ts.map +1 -1
- package/out/src/storage/methods/getBeefForTransaction.js +23 -4
- package/out/src/storage/methods/getBeefForTransaction.js.map +1 -1
- package/out/src/storage/remoting/StorageClient.d.ts.map +1 -1
- package/out/src/storage/remoting/StorageClient.js +2 -6
- package/out/src/storage/remoting/StorageClient.js.map +1 -1
- package/out/src/storage/remoting/StorageMobile.d.ts.map +1 -1
- package/out/src/storage/remoting/StorageMobile.js +2 -6
- package/out/src/storage/remoting/StorageMobile.js.map +1 -1
- package/out/src/storage/schema/KnexMigrations.d.ts +1 -0
- package/out/src/storage/schema/KnexMigrations.d.ts.map +1 -1
- package/out/src/storage/schema/KnexMigrations.js +14 -1
- package/out/src/storage/schema/KnexMigrations.js.map +1 -1
- package/out/src/storage/schema/StorageIdbSchema.d.ts +2 -0
- package/out/src/storage/schema/StorageIdbSchema.d.ts.map +1 -1
- package/package.json +5 -4
|
@@ -11,7 +11,6 @@ const utilityHelpers_noBuffer_1 = require("../../utility/utilityHelpers.noBuffer
|
|
|
11
11
|
const EntityProvenTx_1 = require("../schema/entities/EntityProvenTx");
|
|
12
12
|
const Wallet_1 = require("../../Wallet");
|
|
13
13
|
const offsetKey_1 = require("./offsetKey");
|
|
14
|
-
const utils_1 = require("./utils");
|
|
15
14
|
const sdk_2 = require("../../sdk");
|
|
16
15
|
const managedChange_1 = require("./managedChange");
|
|
17
16
|
const actionPlanning_1 = require("./actionPlanning");
|
|
@@ -20,6 +19,31 @@ function setDisableDoubleSpendCheckForTest(v) {
|
|
|
20
19
|
disableDoubleSpendCheckForTest = v;
|
|
21
20
|
}
|
|
22
21
|
async function createAction(storage, auth, vargs, _originator) {
|
|
22
|
+
if (!storage.telemetry.enabled)
|
|
23
|
+
return await createActionCore(storage, auth, vargs);
|
|
24
|
+
return await storage.telemetry.withSpan('wallet.storage.create_action', {
|
|
25
|
+
component: 'wallet-storage',
|
|
26
|
+
carrier: vargs,
|
|
27
|
+
attributes: {
|
|
28
|
+
'action.fixed_input_count': vargs.inputs.length,
|
|
29
|
+
'action.fixed_output_count': vargs.outputs.length,
|
|
30
|
+
'action.known_txid_count': vargs.options.knownTxids?.length ?? 0,
|
|
31
|
+
'action.is_delayed': vargs.isDelayed,
|
|
32
|
+
'action.is_no_send': vargs.isNoSend
|
|
33
|
+
}
|
|
34
|
+
}, async (span) => {
|
|
35
|
+
const result = await createActionCore(storage, auth, vargs, span);
|
|
36
|
+
span.end({
|
|
37
|
+
attributes: {
|
|
38
|
+
'action.result_input_count': result.inputs.length,
|
|
39
|
+
'action.result_output_count': result.outputs.length,
|
|
40
|
+
'action.input_beef_bytes': result.inputBeef?.length ?? 0
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
return result;
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
async function createActionCore(storage, auth, vargs, parent) {
|
|
23
47
|
const logger = vargs.logger;
|
|
24
48
|
logger?.group('storage createAction');
|
|
25
49
|
// stampLog(vargs, `start storage createTransactionSdk`)
|
|
@@ -44,39 +68,59 @@ async function createAction(storage, auth, vargs, _originator) {
|
|
|
44
68
|
* - Create result inputs with source locking scripts
|
|
45
69
|
* - Create result outputs with new locking scripts.
|
|
46
70
|
* - Create and return result.
|
|
47
|
-
|
|
71
|
+
*/
|
|
48
72
|
const userId = auth.userId;
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
73
|
+
const validated = await traceStorageStep(storage, 'wallet.storage.create_action.validate', parent, {
|
|
74
|
+
'action.fixed_input_count': vargs.inputs.length,
|
|
75
|
+
'action.fixed_output_count': vargs.outputs.length
|
|
76
|
+
}, async (span) => {
|
|
77
|
+
const requiredInputs = await validateRequiredInputs(storage, userId, vargs);
|
|
78
|
+
logger?.log('validated required inputs');
|
|
79
|
+
const xoutputs = validateRequiredOutputs(storage, userId, vargs);
|
|
80
|
+
logger?.log('validated required outputs');
|
|
81
|
+
const changeBasketName = 'default';
|
|
82
|
+
const changeBasket = (0, utilityHelpers_1.verifyOne)(await storage.findOutputBaskets({
|
|
83
|
+
partial: { userId, name: changeBasketName }
|
|
84
|
+
}), `Invalid outputGeneration basket "${changeBasketName}"`);
|
|
85
|
+
logger?.log('found change basket');
|
|
86
|
+
const noSendChangeIn = await validateNoSendChange(storage, userId, vargs, changeBasket);
|
|
87
|
+
logger?.log('validated noSendChange');
|
|
88
|
+
span?.end({
|
|
89
|
+
attributes: {
|
|
90
|
+
'action.validated_input_count': requiredInputs.xinputs.length,
|
|
91
|
+
'action.validated_output_count': xoutputs.length,
|
|
92
|
+
'action.no_send_change_input_count': noSendChangeIn.length,
|
|
93
|
+
'action.validated_beef_tx_count': requiredInputs.beef.txs.length
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
return { ...requiredInputs, xoutputs, changeBasket, noSendChangeIn };
|
|
97
|
+
});
|
|
98
|
+
const { storageBeef, beef, xinputs, xoutputs, changeBasket, noSendChangeIn } = validated;
|
|
62
99
|
const feeModel = (0, StorageProvider_1.validateStorageFeeModel)(storage.feeModel);
|
|
63
100
|
logger?.log(`validated fee model ${JSON.stringify(feeModel)}`);
|
|
64
|
-
await
|
|
65
|
-
logger?.log(
|
|
101
|
+
const initialFundingPlan = await prepareFundingPlan(storage, { userId, vargs, xinputs, xoutputs, changeBasket, noSendChangeIn, feeModel, parent });
|
|
102
|
+
logger?.log(`planned funding from ${initialFundingPlan.availableChangeCount} change inputs`);
|
|
66
103
|
let newTx;
|
|
67
104
|
try {
|
|
68
|
-
|
|
105
|
+
const storageBeefBytes = storageBeef.toBinary();
|
|
106
|
+
newTx = await traceStorageStep(storage, 'wallet.storage.create_action.create_record', parent, {
|
|
107
|
+
'action.label_count': vargs.labels.length,
|
|
108
|
+
'action.storage_beef_bytes': storageBeefBytes.length
|
|
109
|
+
}, async (span) => {
|
|
110
|
+
const transaction = await createNewTxRecord(storage, userId, vargs, storageBeefBytes);
|
|
111
|
+
span?.end({ attributes: { 'action.transaction_record_created': true } });
|
|
112
|
+
return transaction;
|
|
113
|
+
});
|
|
69
114
|
logger?.log('created new transaction record');
|
|
70
115
|
const ctx = {
|
|
71
116
|
xinputs,
|
|
72
117
|
xoutputs,
|
|
73
118
|
changeBasket,
|
|
74
119
|
noSendChangeIn,
|
|
75
|
-
availableChangeCount,
|
|
76
120
|
feeModel,
|
|
77
121
|
transactionId: newTx.transactionId
|
|
78
122
|
};
|
|
79
|
-
const { allocatedChange, changeOutputs, derivationPrefix, maxPossibleSatoshisAdjustment } = await fundNewTransactionSdk(storage, userId, vargs, ctx);
|
|
123
|
+
const { allocatedChange, changeOutputs, derivationPrefix, maxPossibleSatoshisAdjustment } = await fundNewTransactionSdk(storage, userId, vargs, ctx, initialFundingPlan, parent);
|
|
80
124
|
logger?.log('funded new transaction');
|
|
81
125
|
if (maxPossibleSatoshisAdjustment != null) {
|
|
82
126
|
const a = maxPossibleSatoshisAdjustment;
|
|
@@ -87,12 +131,27 @@ async function createAction(storage, auth, vargs, _originator) {
|
|
|
87
131
|
}
|
|
88
132
|
// The satoshis of the transaction is the satoshis we get back in change minus the satoshis we spend.
|
|
89
133
|
const satoshis = changeOutputs.reduce((a, e) => a + e.satoshis, 0) - allocatedChange.reduce((a, e) => a + e.satoshis, 0);
|
|
90
|
-
await storage.
|
|
91
|
-
|
|
134
|
+
const { outputs, changeVouts } = await traceStorageStep(storage, 'wallet.storage.create_action.persist_outputs', parent, {
|
|
135
|
+
'action.fixed_output_count': ctx.xoutputs.length,
|
|
136
|
+
'action.change_output_count': changeOutputs.length
|
|
137
|
+
}, async (span) => {
|
|
138
|
+
await storage.updateTransaction(newTx.transactionId, { satoshis });
|
|
139
|
+
const persisted = await createNewOutputs(storage, userId, vargs, ctx, changeOutputs);
|
|
140
|
+
span?.end({ attributes: { 'action.persisted_output_count': persisted.outputs.length } });
|
|
141
|
+
return persisted;
|
|
142
|
+
});
|
|
92
143
|
logger?.log('created new output records');
|
|
93
|
-
const inputBeef = await mergeAllocatedChangeBeefs(storage,
|
|
144
|
+
const inputBeef = await mergeAllocatedChangeBeefs(storage, vargs, allocatedChange, beef, parent);
|
|
94
145
|
logger?.log('merged allocated change beefs');
|
|
95
|
-
const inputs = await
|
|
146
|
+
const inputs = await traceStorageStep(storage, 'wallet.storage.create_action.assemble_inputs', parent, {
|
|
147
|
+
'action.fixed_input_count': ctx.xinputs.length,
|
|
148
|
+
'action.funding_input_count': allocatedChange.length,
|
|
149
|
+
'action.include_source_transactions': vargs.includeAllSourceTransactions
|
|
150
|
+
}, async (span) => {
|
|
151
|
+
const assembled = await createNewInputs(storage, userId, vargs, ctx, allocatedChange);
|
|
152
|
+
span?.end({ attributes: { 'action.result_input_count': assembled.length } });
|
|
153
|
+
return assembled;
|
|
154
|
+
});
|
|
96
155
|
logger?.log('created new inputs');
|
|
97
156
|
const r = {
|
|
98
157
|
reference: newTx.reference,
|
|
@@ -362,14 +421,16 @@ async function createNewTxRecord(storage, userId, vargs, storageBeef) {
|
|
|
362
421
|
satoshis: 0, // updated after fundingTransaction
|
|
363
422
|
userId,
|
|
364
423
|
isOutgoing: true,
|
|
365
|
-
inputBEEF: storageBeef
|
|
424
|
+
inputBEEF: storageBeef,
|
|
366
425
|
description: vargs.description,
|
|
367
426
|
txid: undefined,
|
|
368
427
|
rawTx: undefined
|
|
369
428
|
};
|
|
370
429
|
newTx.transactionId = await storage.insertTransaction(newTx);
|
|
371
|
-
|
|
372
|
-
|
|
430
|
+
const labelNames = [...new Set(vargs.labels)];
|
|
431
|
+
const labels = await storage.findOrInsertTxLabelsBulk(userId, labelNames);
|
|
432
|
+
for (const label of labelNames) {
|
|
433
|
+
const txLabel = labels[label];
|
|
373
434
|
await storage.findOrInsertTxLabelMap((0, utilityHelpers_1.verifyId)(newTx.transactionId), (0, utilityHelpers_1.verifyId)(txLabel.txLabelId));
|
|
374
435
|
}
|
|
375
436
|
return newTx;
|
|
@@ -574,10 +635,9 @@ async function validateNoSendChange(storage, userId, vargs, changeBasket) {
|
|
|
574
635
|
return [];
|
|
575
636
|
const noSendChange = vargs.options.noSendChange;
|
|
576
637
|
if (noSendChange && noSendChange.length > 0) {
|
|
638
|
+
const byOutpoint = await storage.findOutputsByOutpoints(userId, noSendChange);
|
|
577
639
|
for (const op of noSendChange) {
|
|
578
|
-
const output =
|
|
579
|
-
partial: { userId, txid: op.txid, vout: op.vout }
|
|
580
|
-
}));
|
|
640
|
+
const output = byOutpoint[`${op.txid}.${op.vout}`];
|
|
581
641
|
// noSendChange is signed through the same BRC-29 path as allocated change.
|
|
582
642
|
// It must satisfy the full managed-change policy, not merely share the
|
|
583
643
|
// default basket or have a P2PKH-shaped locking script.
|
|
@@ -599,87 +659,232 @@ async function validateNoSendChange(storage, userId, vargs, changeBasket) {
|
|
|
599
659
|
}
|
|
600
660
|
return r;
|
|
601
661
|
}
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
const spending = xoutputs.reduce((a, e) => a + e.satoshis, 0);
|
|
608
|
-
const minSize = (0, utils_1.transactionSize)(xinputs.map(i => i.unlockingScriptLength || 0), xoutputs.map(o => Math.floor(o.lockingScript.length / 2)));
|
|
609
|
-
const minFee = Math.ceil((minSize / 1000) * feeModel.value);
|
|
610
|
-
const minRequired = spending + minFee;
|
|
611
|
-
const fixedAvailable = fixedInputSatoshis + noSendSatoshis;
|
|
612
|
-
if (fixedAvailable >= minRequired)
|
|
613
|
-
return;
|
|
614
|
-
// Keep common successful path cheap:
|
|
615
|
-
// - If there are zero change candidates, failure is certain.
|
|
616
|
-
// Otherwise, defer to the main funding allocator.
|
|
617
|
-
const deficit = minRequired - fixedAvailable;
|
|
618
|
-
if (availableChangeCount <= 0) {
|
|
619
|
-
throw new WERR_errors_1.WERR_INSUFFICIENT_FUNDS(minRequired, deficit);
|
|
662
|
+
class FundingClaimConflict extends Error {
|
|
663
|
+
conflict;
|
|
664
|
+
constructor(conflict) {
|
|
665
|
+
super('createAction funding claim changed concurrently');
|
|
666
|
+
this.conflict = conflict;
|
|
620
667
|
}
|
|
621
668
|
}
|
|
622
|
-
async function
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
669
|
+
async function traceStorageStep(storage, name, parent, attributes, callback) {
|
|
670
|
+
if (!storage.telemetry.enabled)
|
|
671
|
+
return await callback();
|
|
672
|
+
return await storage.telemetry.withSpan(name, { component: 'wallet-storage', parent: parent?.context, attributes }, async (span) => await callback(span));
|
|
673
|
+
}
|
|
674
|
+
function makeFundingParams(vargs, xinputs, xoutputs, changeBasket, feeModel, availableChangeCount) {
|
|
675
|
+
return {
|
|
676
|
+
fixedInputs: xinputs.map(input => ({
|
|
677
|
+
satoshis: input.satoshis,
|
|
678
|
+
unlockingScriptLength: input.unlockingScriptLength
|
|
627
679
|
})),
|
|
628
|
-
fixedOutputs:
|
|
629
|
-
satoshis:
|
|
630
|
-
lockingScriptLength:
|
|
680
|
+
fixedOutputs: xoutputs.map(output => ({
|
|
681
|
+
satoshis: output.satoshis,
|
|
682
|
+
lockingScriptLength: output.lockingScript.length / 2
|
|
631
683
|
})),
|
|
632
|
-
feeModel
|
|
633
|
-
changeInitialSatoshis: Math.max(1,
|
|
634
|
-
changeFirstSatoshis: Math.max(1, Math.round(
|
|
684
|
+
feeModel,
|
|
685
|
+
changeInitialSatoshis: Math.max(1, changeBasket.minimumDesiredUTXOValue),
|
|
686
|
+
changeFirstSatoshis: Math.max(1, Math.round(changeBasket.minimumDesiredUTXOValue / 4)),
|
|
635
687
|
changeLockingScriptLength: 25,
|
|
636
688
|
changeUnlockingScriptLength: 107,
|
|
637
|
-
targetNetCount:
|
|
689
|
+
targetNetCount: changeBasket.numberOfDesiredUTXOs - availableChangeCount,
|
|
638
690
|
randomVals: vargs.randomVals
|
|
639
691
|
};
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
const
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
const
|
|
666
|
-
|
|
667
|
-
|
|
692
|
+
}
|
|
693
|
+
async function prepareFundingPlan(storage, context) {
|
|
694
|
+
const { userId, vargs, xinputs, xoutputs, changeBasket, noSendChangeIn, feeModel, parent } = context;
|
|
695
|
+
const excludeSending = !vargs.isDelayed;
|
|
696
|
+
const candidates = await traceStorageStep(storage, 'wallet.storage.create_action.funding_candidates', parent, { 'funding.exclude_sending': excludeSending }, async (span) => {
|
|
697
|
+
const outputs = await storage.findAvailableManagedChangeInputs(userId, changeBasket.basketId, excludeSending);
|
|
698
|
+
span?.end({
|
|
699
|
+
attributes: {
|
|
700
|
+
'funding.candidate_count': outputs.length,
|
|
701
|
+
'funding.candidate_satoshis': outputs.reduce((sum, output) => sum + output.satoshis, 0)
|
|
702
|
+
}
|
|
703
|
+
});
|
|
704
|
+
return outputs;
|
|
705
|
+
});
|
|
706
|
+
const noSendIds = new Set(noSendChangeIn.map(output => output.outputId));
|
|
707
|
+
const available = candidates.filter(output => !noSendIds.has(output.outputId));
|
|
708
|
+
// Preserve the legacy target-net-count input: noSendChange was included in
|
|
709
|
+
// countChangeInputs before it was consumed by the allocator.
|
|
710
|
+
const params = makeFundingParams(vargs, xinputs, xoutputs, changeBasket, feeModel, candidates.length);
|
|
711
|
+
return await traceStorageStep(storage, 'wallet.storage.create_action.funding_plan', parent, {
|
|
712
|
+
'funding.candidate_count': available.length,
|
|
713
|
+
'funding.no_send_change_count': noSendChangeIn.length
|
|
714
|
+
}, async (span) => {
|
|
715
|
+
const allocated = new Map();
|
|
716
|
+
const noSend = [...noSendChangeIn];
|
|
717
|
+
const allocate = async (targetSatoshis, exactSatoshis) => {
|
|
718
|
+
let output = noSend.pop();
|
|
719
|
+
output ??= (0, actionPlanning_1.selectCanonicalChange)(available.filter(candidate => !allocated.has(candidate.outputId)), targetSatoshis, exactSatoshis);
|
|
720
|
+
if (output == null)
|
|
721
|
+
return undefined;
|
|
722
|
+
allocated.set(output.outputId, output);
|
|
723
|
+
return { outputId: output.outputId, satoshis: output.satoshis };
|
|
668
724
|
};
|
|
669
|
-
|
|
725
|
+
const release = async (outputId) => {
|
|
726
|
+
const output = allocated.get(outputId);
|
|
727
|
+
if (output == null)
|
|
728
|
+
return;
|
|
729
|
+
allocated.delete(outputId);
|
|
730
|
+
if (noSendIds.has(outputId))
|
|
731
|
+
noSend.push(output);
|
|
732
|
+
};
|
|
733
|
+
const result = await (0, generateChange_1.generateChangeSdk)(params, allocate, release, vargs.logger, storage.telemetry);
|
|
734
|
+
const selected = result.allocatedChangeInputs.map(input => (0, utilityHelpers_1.verifyTruthy)(allocated.get(input.outputId)));
|
|
735
|
+
span?.end({
|
|
736
|
+
attributes: {
|
|
737
|
+
'funding.allocated_input_count': selected.length,
|
|
738
|
+
'funding.change_output_count': result.changeOutputs.length,
|
|
739
|
+
'funding.fee_satoshis': result.fee,
|
|
740
|
+
'funding.transaction_size_bytes': result.size
|
|
741
|
+
}
|
|
742
|
+
});
|
|
743
|
+
return {
|
|
744
|
+
params,
|
|
745
|
+
result,
|
|
746
|
+
selected,
|
|
747
|
+
availableChangeCount: candidates.length
|
|
748
|
+
};
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
async function claimFundingPlan(storage, userId, basketId, excludeSending, transactionId, noSendChangeIn, plan) {
|
|
752
|
+
if (plan.selected.length === 0) {
|
|
753
|
+
return { outputs: [], sourceTransactionCount: 0, hydratedScriptCount: 0, scriptSourceTransactionCount: 0 };
|
|
754
|
+
}
|
|
755
|
+
const noSendIds = new Set(noSendChangeIn.map(output => output.outputId));
|
|
756
|
+
const statuses = ['completed', 'unproven'];
|
|
757
|
+
if (!excludeSending)
|
|
758
|
+
statuses.push('sending');
|
|
759
|
+
const claim = await storage.transaction(async (trx) => {
|
|
760
|
+
const outpoints = plan.selected.map(output => {
|
|
761
|
+
if (output.txid == null)
|
|
762
|
+
throw new WERR_errors_1.WERR_INTERNAL('planned change input is missing txid');
|
|
763
|
+
return { txid: output.txid, vout: output.vout };
|
|
764
|
+
});
|
|
765
|
+
const currentByOutpoint = await storage.findOutputsByOutpointsForUpdate(userId, outpoints, trx, true);
|
|
766
|
+
const reserved = new Set(await storage.findReservedActionBatchOutputIds(plan.selected.map(output => output.outputId), trx));
|
|
767
|
+
const transactionIds = [...new Set(Object.values(currentByOutpoint).map(output => output.transactionId))];
|
|
768
|
+
const transactionStatuses = await storage.findTransactionStatusesByIds(userId, transactionIds, trx);
|
|
769
|
+
const claimed = [];
|
|
770
|
+
for (const planned of plan.selected) {
|
|
771
|
+
const key = `${String(planned.txid)}.${planned.vout}`;
|
|
772
|
+
const current = currentByOutpoint[key];
|
|
773
|
+
const currentStatus = current == null ? undefined : transactionStatuses.get(current.transactionId);
|
|
774
|
+
const validTransaction = currentStatus != null && statuses.includes(currentStatus);
|
|
775
|
+
if (current?.outputId !== planned.outputId ||
|
|
776
|
+
current?.satoshis !== planned.satoshis ||
|
|
777
|
+
current?.basketId !== basketId ||
|
|
778
|
+
!(0, managedChange_1.isAutoSpendableChangeOutput)(current) ||
|
|
779
|
+
reserved.has(current?.outputId ?? -1) ||
|
|
780
|
+
validTransaction !== true) {
|
|
781
|
+
return { conflict: noSendIds.has(planned.outputId) ? 'noSendChange' : 'candidate' };
|
|
782
|
+
}
|
|
783
|
+
claimed.push(current);
|
|
784
|
+
}
|
|
785
|
+
const updated = await storage.markChangeInputsSpent(claimed.map(output => output.outputId), transactionId, trx);
|
|
786
|
+
if (updated !== claimed.length) {
|
|
787
|
+
throw new FundingClaimConflict(claimed.some(output => noSendIds.has(output.outputId)) ? 'noSendChange' : 'candidate');
|
|
788
|
+
}
|
|
789
|
+
for (const output of claimed) {
|
|
790
|
+
output.spendable = false;
|
|
791
|
+
output.spentBy = transactionId;
|
|
792
|
+
}
|
|
793
|
+
return { outputs: claimed, sourceTransactionCount: transactionIds.length };
|
|
794
|
+
}).catch(error => {
|
|
795
|
+
if (error instanceof FundingClaimConflict)
|
|
796
|
+
return { conflict: error.conflict };
|
|
797
|
+
throw error;
|
|
798
|
+
});
|
|
799
|
+
if (claim.outputs == null)
|
|
800
|
+
return claim;
|
|
801
|
+
const hydration = await hydrateFundingInputScripts(storage, claim.outputs);
|
|
802
|
+
return {
|
|
803
|
+
outputs: claim.outputs,
|
|
804
|
+
sourceTransactionCount: claim.sourceTransactionCount,
|
|
805
|
+
...hydration
|
|
670
806
|
};
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
807
|
+
}
|
|
808
|
+
async function hydrateFundingInputScripts(storage, outputs) {
|
|
809
|
+
const missing = outputs.filter(output => output.lockingScript?.length !== output.scriptLength &&
|
|
810
|
+
output.scriptLength != null && output.scriptLength > 0 &&
|
|
811
|
+
output.scriptOffset != null && output.scriptOffset > 0 &&
|
|
812
|
+
output.txid != null && output.txid !== '');
|
|
813
|
+
if (missing.length === 0)
|
|
814
|
+
return { hydratedScriptCount: 0, scriptSourceTransactionCount: 0 };
|
|
815
|
+
const byTxid = new Map();
|
|
816
|
+
for (const output of missing) {
|
|
817
|
+
const txid = (0, utilityHelpers_1.verifyTruthy)(output.txid);
|
|
818
|
+
const group = byTxid.get(txid) ?? [];
|
|
819
|
+
group.push(output);
|
|
820
|
+
byTxid.set(txid, group);
|
|
821
|
+
}
|
|
822
|
+
const groups = [...byTxid.entries()];
|
|
823
|
+
let cursor = 0;
|
|
824
|
+
await Promise.all(Array.from({ length: Math.min(8, groups.length) }, async () => {
|
|
825
|
+
while (cursor < groups.length) {
|
|
826
|
+
const [txid, group] = groups[cursor++];
|
|
827
|
+
if (group.length === 1) {
|
|
828
|
+
await storage.validateOutputScript(group[0]);
|
|
829
|
+
continue;
|
|
830
|
+
}
|
|
831
|
+
const rawTx = await storage.getRawTxOfKnownValidTransaction(txid);
|
|
832
|
+
if (rawTx != null) {
|
|
833
|
+
for (const output of group) {
|
|
834
|
+
output.lockingScript = rawTx.slice(output.scriptOffset, output.scriptOffset + output.scriptLength);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
else {
|
|
838
|
+
for (const output of group)
|
|
839
|
+
await storage.validateOutputScript(output);
|
|
840
|
+
}
|
|
676
841
|
}
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
842
|
+
}));
|
|
843
|
+
return {
|
|
844
|
+
hydratedScriptCount: missing.filter(output => output.lockingScript?.length === output.scriptLength).length,
|
|
845
|
+
scriptSourceTransactionCount: groups.length
|
|
681
846
|
};
|
|
682
|
-
|
|
847
|
+
}
|
|
848
|
+
async function fundNewTransactionSdk(storage, userId, vargs, ctx, initialPlan, parent) {
|
|
849
|
+
let plan = initialPlan;
|
|
850
|
+
let allocatedChange;
|
|
851
|
+
let retryCount = 0;
|
|
852
|
+
await traceStorageStep(storage, 'wallet.storage.create_action.funding_claim', parent, { 'funding.planned_input_count': initialPlan.selected.length }, async (span) => {
|
|
853
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
854
|
+
const claim = await claimFundingPlan(storage, userId, ctx.changeBasket.basketId, !vargs.isDelayed, ctx.transactionId, ctx.noSendChangeIn, plan);
|
|
855
|
+
if (claim.outputs != null) {
|
|
856
|
+
allocatedChange = claim.outputs;
|
|
857
|
+
span?.end({
|
|
858
|
+
attributes: {
|
|
859
|
+
'funding.claim_retry_count': retryCount,
|
|
860
|
+
'funding.source_transaction_count': claim.sourceTransactionCount,
|
|
861
|
+
'funding.hydrated_script_count': claim.hydratedScriptCount,
|
|
862
|
+
'funding.script_source_transaction_count': claim.scriptSourceTransactionCount
|
|
863
|
+
}
|
|
864
|
+
});
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
867
|
+
if (claim.conflict === 'noSendChange') {
|
|
868
|
+
throw new WERR_errors_1.WERR_INVALID_PARAMETER('noSendChange', 'outputs that remain spendable during action planning');
|
|
869
|
+
}
|
|
870
|
+
retryCount++;
|
|
871
|
+
plan = await prepareFundingPlan(storage, {
|
|
872
|
+
userId,
|
|
873
|
+
vargs,
|
|
874
|
+
xinputs: ctx.xinputs,
|
|
875
|
+
xoutputs: ctx.xoutputs,
|
|
876
|
+
changeBasket: ctx.changeBasket,
|
|
877
|
+
noSendChangeIn: ctx.noSendChangeIn,
|
|
878
|
+
feeModel: ctx.feeModel,
|
|
879
|
+
parent
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
throw new WERR_errors_1.WERR_INVALID_OPERATION('wallet funding changed repeatedly during action planning; retry createAction');
|
|
883
|
+
});
|
|
884
|
+
if (allocatedChange == null)
|
|
885
|
+
throw new WERR_errors_1.WERR_INTERNAL('funding plan was not claimed');
|
|
886
|
+
const params = plan.params;
|
|
887
|
+
const gcr = plan.result;
|
|
683
888
|
const nextRandomVal = () => {
|
|
684
889
|
let val = 0;
|
|
685
890
|
if (vargs.randomVals == null || vargs.randomVals.length === 0) {
|
|
@@ -715,7 +920,7 @@ async function fundNewTransactionSdk(storage, userId, vargs, ctx) {
|
|
|
715
920
|
const derivationPrefix = randomDerivation(16);
|
|
716
921
|
const r = {
|
|
717
922
|
maxPossibleSatoshisAdjustment: gcr.maxPossibleSatoshisAdjustment,
|
|
718
|
-
allocatedChange
|
|
923
|
+
allocatedChange,
|
|
719
924
|
changeOutputs: gcr.changeOutputs.map((o, i) => ({
|
|
720
925
|
// what we knnow now and can insert into the database for this new transaction's change output
|
|
721
926
|
created_at: new Date(),
|
|
@@ -755,15 +960,31 @@ async function fundNewTransactionSdk(storage, userId, vargs, ctx) {
|
|
|
755
960
|
function trimInputBeef(beef, vargs) {
|
|
756
961
|
if (vargs.options.returnTXIDOnly)
|
|
757
962
|
return undefined;
|
|
758
|
-
const knownTxids =
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
963
|
+
const knownTxids = vargs.options.knownTxids ?? [];
|
|
964
|
+
const hasKnownTxid = makeKnownTxidLookup(knownTxids);
|
|
965
|
+
// The returned BEEF normally contains only a handful of transactions. A
|
|
966
|
+
// direct scan avoids materializing a second full index of a potentially
|
|
967
|
+
// very large wallet history on every successful action.
|
|
968
|
+
for (const btx of beef.txs)
|
|
969
|
+
if (hasKnownTxid(btx.txid))
|
|
970
|
+
beef.makeTxidOnly(btx.txid);
|
|
764
971
|
return beef.toUint8Array();
|
|
765
972
|
}
|
|
766
|
-
|
|
973
|
+
function makeKnownTxidLookup(knownTxids) {
|
|
974
|
+
let lookups = 0;
|
|
975
|
+
let indexed;
|
|
976
|
+
return txid => {
|
|
977
|
+
lookups++;
|
|
978
|
+
if (indexed != null)
|
|
979
|
+
return indexed.has(txid);
|
|
980
|
+
if (knownTxids.length > 64 && lookups > 4) {
|
|
981
|
+
indexed = new Set(knownTxids);
|
|
982
|
+
return indexed.has(txid);
|
|
983
|
+
}
|
|
984
|
+
return knownTxids.includes(txid);
|
|
985
|
+
};
|
|
986
|
+
}
|
|
987
|
+
async function mergeAllocatedChangeBeefs(storage, vargs, allocatedChange, beef, parent) {
|
|
767
988
|
const options = {
|
|
768
989
|
trustSelf: undefined,
|
|
769
990
|
knownTxids: vargs.options.knownTxids,
|
|
@@ -775,22 +996,55 @@ async function mergeAllocatedChangeBeefs(storage, userId, vargs, allocatedChange
|
|
|
775
996
|
};
|
|
776
997
|
if (vargs.options.returnTXIDOnly)
|
|
777
998
|
return undefined;
|
|
778
|
-
const
|
|
779
|
-
const
|
|
999
|
+
const knownTxids = vargs.options.knownTxids ?? [];
|
|
1000
|
+
const hasKnownTxid = makeKnownTxidLookup(knownTxids);
|
|
1001
|
+
const missing = Array.from(new Set(allocatedChange
|
|
1002
|
+
.map(output => (0, utilityHelpers_1.verifyTruthy)(output.txid))
|
|
1003
|
+
.filter(txid => beef.findTxid(txid) == null && !hasKnownTxid(txid))));
|
|
780
1004
|
const fetched = Array.from({ length: missing.length });
|
|
781
1005
|
const concurrency = Math.min(8, Math.max(1, missing.length));
|
|
782
1006
|
let cursor = 0;
|
|
783
|
-
await
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
1007
|
+
await traceStorageStep(storage, 'wallet.storage.create_action.beef_fetch', parent, {
|
|
1008
|
+
'beef.allocated_change_count': allocatedChange.length,
|
|
1009
|
+
'beef.distinct_source_count': new Set(allocatedChange.map(output => output.txid)).size,
|
|
1010
|
+
'beef.known_txid_count': knownTxids.length,
|
|
1011
|
+
'beef.missing_source_count': missing.length,
|
|
1012
|
+
'beef.fetch_concurrency': concurrency
|
|
1013
|
+
}, async (span) => {
|
|
1014
|
+
await Promise.all(Array.from({ length: concurrency }, async () => {
|
|
1015
|
+
while (cursor < missing.length) {
|
|
1016
|
+
const index = cursor++;
|
|
1017
|
+
fetched[index] = await storage.getBeefForTransaction(missing[index], { ...options, mergeToBeef: undefined });
|
|
1018
|
+
}
|
|
1019
|
+
}));
|
|
1020
|
+
span?.end({
|
|
1021
|
+
attributes: {
|
|
1022
|
+
'beef.fetched_tx_count': fetched.reduce((sum, item) => sum + (item?.txs.length ?? 0), 0),
|
|
1023
|
+
'beef.fetched_bump_count': fetched.reduce((sum, item) => sum + (item?.bumps.length ?? 0), 0)
|
|
1024
|
+
}
|
|
1025
|
+
});
|
|
1026
|
+
});
|
|
1027
|
+
await traceStorageStep(storage, 'wallet.storage.create_action.beef_merge', parent, { 'beef.fragment_count': fetched.length }, async (span) => {
|
|
1028
|
+
for (const fetchedBeef of fetched) {
|
|
1029
|
+
if (fetchedBeef == null)
|
|
1030
|
+
continue;
|
|
1031
|
+
beef.mergeBeef(fetchedBeef);
|
|
787
1032
|
}
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
1033
|
+
span?.end({
|
|
1034
|
+
attributes: {
|
|
1035
|
+
'beef.merged_tx_count': beef.txs.length,
|
|
1036
|
+
'beef.merged_bump_count': beef.bumps.length
|
|
1037
|
+
}
|
|
1038
|
+
});
|
|
1039
|
+
});
|
|
1040
|
+
return await traceStorageStep(storage, 'wallet.storage.create_action.beef_trim_serialize', parent, {
|
|
1041
|
+
'beef.tx_count': beef.txs.length,
|
|
1042
|
+
'beef.bump_count': beef.bumps.length,
|
|
1043
|
+
'beef.known_txid_count': knownTxids.length
|
|
1044
|
+
}, async (span) => {
|
|
1045
|
+
const result = trimInputBeef(beef, vargs);
|
|
1046
|
+
span?.end({ attributes: { 'beef.result_bytes': result?.length ?? 0 } });
|
|
1047
|
+
return result;
|
|
1048
|
+
});
|
|
795
1049
|
}
|
|
796
1050
|
//# sourceMappingURL=createAction.js.map
|