@bsv/wallet-toolbox-client 2.4.21 → 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/out/index.client.cjs +517 -147
- package/out/index.client.cjs.map +1 -1
- package/out/index.client.d.cts +20 -1
- package/out/index.client.d.cts.map +1 -1
- package/out/index.client.d.mts +20 -1
- package/out/index.client.d.mts.map +1 -1
- package/out/index.client.mjs +517 -147
- package/out/index.client.mjs.map +1 -1
- package/package.json +2 -2
package/out/index.client.mjs
CHANGED
|
@@ -4888,6 +4888,12 @@ function upgradeOutputs(db) {
|
|
|
4888
4888
|
autoIncrement: true
|
|
4889
4889
|
});
|
|
4890
4890
|
store.createIndex("userId", "userId");
|
|
4891
|
+
store.createIndex("userId_basketId", ["userId", "basketId"]);
|
|
4892
|
+
store.createIndex("txid_vout_userId", [
|
|
4893
|
+
"txid",
|
|
4894
|
+
"vout",
|
|
4895
|
+
"userId"
|
|
4896
|
+
], { unique: true });
|
|
4891
4897
|
store.createIndex("transactionId", "transactionId");
|
|
4892
4898
|
store.createIndex("basketId", "basketId");
|
|
4893
4899
|
store.createIndex("spentBy", "spentBy");
|
|
@@ -5090,7 +5096,7 @@ async function getBeefForTransaction(storage, txid, options) {
|
|
|
5090
5096
|
if (options.mergeToBeef instanceof Beef) beef = options.mergeToBeef;
|
|
5091
5097
|
else if (options.mergeToBeef != null) beef = Beef.fromBinary(options.mergeToBeef);
|
|
5092
5098
|
else beef = new Beef();
|
|
5093
|
-
const
|
|
5099
|
+
const hasKnownTxid = makeKnownTxidLookup$1(options.knownTxids ?? []);
|
|
5094
5100
|
const scheduled = /* @__PURE__ */ new Set([txid]);
|
|
5095
5101
|
let frontier = [{
|
|
5096
5102
|
txid,
|
|
@@ -5100,7 +5106,7 @@ async function getBeefForTransaction(storage, txid, options) {
|
|
|
5100
5106
|
const concurrency = Number.isFinite(requestedConcurrency) ? Math.max(1, Math.min(32, Math.floor(requestedConcurrency))) : 8;
|
|
5101
5107
|
while (frontier.length > 0) {
|
|
5102
5108
|
const current = frontier.filter((item) => beef.findTxid(item.txid) == null);
|
|
5103
|
-
const resolved = await mapWithConcurrency(current, concurrency, async (item) => await resolveBeefForTransaction(storage, item.txid, options,
|
|
5109
|
+
const resolved = await mapWithConcurrency(current, concurrency, async (item) => await resolveBeefForTransaction(storage, item.txid, options, hasKnownTxid, item.depth));
|
|
5104
5110
|
const next = [];
|
|
5105
5111
|
for (let i = 0; i < resolved.length; i++) {
|
|
5106
5112
|
const result = resolved[i];
|
|
@@ -5117,6 +5123,19 @@ async function getBeefForTransaction(storage, txid, options) {
|
|
|
5117
5123
|
}
|
|
5118
5124
|
return beef;
|
|
5119
5125
|
}
|
|
5126
|
+
function makeKnownTxidLookup$1(knownTxids) {
|
|
5127
|
+
let lookups = 0;
|
|
5128
|
+
let indexed;
|
|
5129
|
+
return (txid) => {
|
|
5130
|
+
lookups++;
|
|
5131
|
+
if (indexed != null) return indexed.has(txid);
|
|
5132
|
+
if (knownTxids.length > 64 && lookups > 4) {
|
|
5133
|
+
indexed = new Set(knownTxids);
|
|
5134
|
+
return indexed.has(txid);
|
|
5135
|
+
}
|
|
5136
|
+
return knownTxids.includes(txid);
|
|
5137
|
+
};
|
|
5138
|
+
}
|
|
5120
5139
|
async function mapWithConcurrency(values, concurrency, mapper) {
|
|
5121
5140
|
const results = Array.from({ length: values.length }, () => void 0);
|
|
5122
5141
|
let cursor = 0;
|
|
@@ -5160,11 +5179,11 @@ async function mergeUsableProvenTransaction(beef, txid, result, options, recursi
|
|
|
5160
5179
|
beef.mergeBump(merklePath);
|
|
5161
5180
|
return beef;
|
|
5162
5181
|
}
|
|
5163
|
-
async function resolveBeefForTransaction(storage, txid, options,
|
|
5182
|
+
async function resolveBeefForTransaction(storage, txid, options, hasKnownTxid, recursionDepth) {
|
|
5164
5183
|
const maxDepth = storage.maxRecursionDepth;
|
|
5165
5184
|
if (maxDepth && maxDepth <= recursionDepth) throw new WERR_INVALID_OPERATION(`Maximum BEEF depth exceeded. Limit is ${storage.maxRecursionDepth}`);
|
|
5166
5185
|
const beef = new Beef();
|
|
5167
|
-
if (
|
|
5186
|
+
if (hasKnownTxid(txid)) {
|
|
5168
5187
|
beef.mergeTxidOnly(txid);
|
|
5169
5188
|
return {
|
|
5170
5189
|
beef,
|
|
@@ -6765,7 +6784,7 @@ function getResultBeef(result) {
|
|
|
6765
6784
|
//#endregion
|
|
6766
6785
|
//#region ../src/signer/methods/createAction.ts
|
|
6767
6786
|
async function createAction$1(wallet, auth, vargs) {
|
|
6768
|
-
if (!wallet.telemetry.enabled) return await createActionCore(wallet, auth, vargs);
|
|
6787
|
+
if (!wallet.telemetry.enabled) return await createActionCore$1(wallet, auth, vargs);
|
|
6769
6788
|
return await wallet.telemetry.withSpan("wallet.create_action", {
|
|
6770
6789
|
component: "wallet-toolbox",
|
|
6771
6790
|
carrier: vargs,
|
|
@@ -6776,7 +6795,7 @@ async function createAction$1(wallet, auth, vargs) {
|
|
|
6776
6795
|
"action.is_sign_action": vargs.isSignAction
|
|
6777
6796
|
}
|
|
6778
6797
|
}, async (span) => {
|
|
6779
|
-
const result = await createActionCore(wallet, auth, vargs, span);
|
|
6798
|
+
const result = await createActionCore$1(wallet, auth, vargs, span);
|
|
6780
6799
|
span.end({ attributes: {
|
|
6781
6800
|
"action.has_transaction": result.tx != null,
|
|
6782
6801
|
"action.has_signable_transaction": result.signableTransaction != null,
|
|
@@ -6785,7 +6804,7 @@ async function createAction$1(wallet, auth, vargs) {
|
|
|
6785
6804
|
return result;
|
|
6786
6805
|
});
|
|
6787
6806
|
}
|
|
6788
|
-
async function createActionCore(wallet, auth, vargs, parent) {
|
|
6807
|
+
async function createActionCore$1(wallet, auth, vargs, parent) {
|
|
6789
6808
|
const r = {};
|
|
6790
6809
|
const logger = vargs.logger;
|
|
6791
6810
|
let prior;
|
|
@@ -9069,7 +9088,16 @@ var Wallet = class {
|
|
|
9069
9088
|
Validation.validateOriginator(originator);
|
|
9070
9089
|
args.options ??= {};
|
|
9071
9090
|
args.options.trustSelf ||= this.trustSelf;
|
|
9072
|
-
if (this.autoKnownTxids && args.options.knownTxids == null) args.options.knownTxids = this.
|
|
9091
|
+
if (this.autoKnownTxids && args.options.knownTxids == null) if (this.telemetry.enabled) args.options.knownTxids = this.telemetry.withSpan("wallet.create_action.prepare_known_txids", {
|
|
9092
|
+
component: "wallet-toolbox",
|
|
9093
|
+
carrier: args,
|
|
9094
|
+
attributes: { "beef.tx_count": this.beef.txs.length }
|
|
9095
|
+
}, (span) => {
|
|
9096
|
+
const knownTxids = this.getKnownTxids(args.options?.knownTxids);
|
|
9097
|
+
span.end({ attributes: { "beef.known_txid_count": knownTxids.length } });
|
|
9098
|
+
return knownTxids;
|
|
9099
|
+
});
|
|
9100
|
+
else args.options.knownTxids = this.getKnownTxids(args.options.knownTxids);
|
|
9073
9101
|
const { auth, vargs } = this.validateAuthAndArgs(args, Validation.validateCreateActionArgs, logger);
|
|
9074
9102
|
logger?.log("validated args");
|
|
9075
9103
|
vargs.includeAllSourceTransactions = this.includeAllSourceTransactions;
|
|
@@ -9388,6 +9416,28 @@ function isAutoSpendableChangeOutput(output) {
|
|
|
9388
9416
|
return isManagedChangeOutput(output) && output.spendable && output.spentBy == null;
|
|
9389
9417
|
}
|
|
9390
9418
|
async function createAction(storage, auth, vargs, _originator) {
|
|
9419
|
+
if (!storage.telemetry.enabled) return await createActionCore(storage, auth, vargs);
|
|
9420
|
+
return await storage.telemetry.withSpan("wallet.storage.create_action", {
|
|
9421
|
+
component: "wallet-storage",
|
|
9422
|
+
carrier: vargs,
|
|
9423
|
+
attributes: {
|
|
9424
|
+
"action.fixed_input_count": vargs.inputs.length,
|
|
9425
|
+
"action.fixed_output_count": vargs.outputs.length,
|
|
9426
|
+
"action.known_txid_count": vargs.options.knownTxids?.length ?? 0,
|
|
9427
|
+
"action.is_delayed": vargs.isDelayed,
|
|
9428
|
+
"action.is_no_send": vargs.isNoSend
|
|
9429
|
+
}
|
|
9430
|
+
}, async (span) => {
|
|
9431
|
+
const result = await createActionCore(storage, auth, vargs, span);
|
|
9432
|
+
span.end({ attributes: {
|
|
9433
|
+
"action.result_input_count": result.inputs.length,
|
|
9434
|
+
"action.result_output_count": result.outputs.length,
|
|
9435
|
+
"action.input_beef_bytes": result.inputBeef?.length ?? 0
|
|
9436
|
+
} });
|
|
9437
|
+
return result;
|
|
9438
|
+
});
|
|
9439
|
+
}
|
|
9440
|
+
async function createActionCore(storage, auth, vargs, parent) {
|
|
9391
9441
|
const logger = vargs.logger;
|
|
9392
9442
|
logger?.group("storage createAction");
|
|
9393
9443
|
if (vargs.isTestWerrReviewActions) throwDummyReviewActions();
|
|
@@ -9407,38 +9457,69 @@ async function createAction(storage, auth, vargs, _originator) {
|
|
|
9407
9457
|
* - Create and return result.
|
|
9408
9458
|
*/
|
|
9409
9459
|
const userId = auth.userId;
|
|
9410
|
-
const { storageBeef, beef, xinputs } = await
|
|
9411
|
-
|
|
9412
|
-
|
|
9413
|
-
|
|
9414
|
-
|
|
9415
|
-
|
|
9416
|
-
userId,
|
|
9417
|
-
|
|
9418
|
-
|
|
9419
|
-
|
|
9420
|
-
|
|
9421
|
-
|
|
9422
|
-
|
|
9423
|
-
|
|
9460
|
+
const { storageBeef, beef, xinputs, xoutputs, changeBasket, noSendChangeIn } = await traceStorageStep(storage, "wallet.storage.create_action.validate", parent, {
|
|
9461
|
+
"action.fixed_input_count": vargs.inputs.length,
|
|
9462
|
+
"action.fixed_output_count": vargs.outputs.length
|
|
9463
|
+
}, async (span) => {
|
|
9464
|
+
const requiredInputs = await validateRequiredInputs(storage, userId, vargs);
|
|
9465
|
+
logger?.log("validated required inputs");
|
|
9466
|
+
const xoutputs = validateRequiredOutputs(storage, userId, vargs);
|
|
9467
|
+
logger?.log("validated required outputs");
|
|
9468
|
+
const changeBasketName = "default";
|
|
9469
|
+
const changeBasket = verifyOne(await storage.findOutputBaskets({ partial: {
|
|
9470
|
+
userId,
|
|
9471
|
+
name: changeBasketName
|
|
9472
|
+
} }), `Invalid outputGeneration basket "${changeBasketName}"`);
|
|
9473
|
+
logger?.log("found change basket");
|
|
9474
|
+
const noSendChangeIn = await validateNoSendChange(storage, userId, vargs, changeBasket);
|
|
9475
|
+
logger?.log("validated noSendChange");
|
|
9476
|
+
span?.end({ attributes: {
|
|
9477
|
+
"action.validated_input_count": requiredInputs.xinputs.length,
|
|
9478
|
+
"action.validated_output_count": xoutputs.length,
|
|
9479
|
+
"action.no_send_change_input_count": noSendChangeIn.length,
|
|
9480
|
+
"action.validated_beef_tx_count": requiredInputs.beef.txs.length
|
|
9481
|
+
} });
|
|
9482
|
+
return {
|
|
9483
|
+
...requiredInputs,
|
|
9484
|
+
xoutputs,
|
|
9485
|
+
changeBasket,
|
|
9486
|
+
noSendChangeIn
|
|
9487
|
+
};
|
|
9488
|
+
});
|
|
9424
9489
|
const feeModel = validateStorageFeeModel(storage.feeModel);
|
|
9425
9490
|
logger?.log(`validated fee model ${JSON.stringify(feeModel)}`);
|
|
9426
|
-
|
|
9427
|
-
|
|
9491
|
+
const initialFundingPlan = await prepareFundingPlan(storage, {
|
|
9492
|
+
userId,
|
|
9493
|
+
vargs,
|
|
9494
|
+
xinputs,
|
|
9495
|
+
xoutputs,
|
|
9496
|
+
changeBasket,
|
|
9497
|
+
noSendChangeIn,
|
|
9498
|
+
feeModel,
|
|
9499
|
+
parent
|
|
9500
|
+
});
|
|
9501
|
+
logger?.log(`planned funding from ${initialFundingPlan.availableChangeCount} change inputs`);
|
|
9428
9502
|
let newTx;
|
|
9429
9503
|
try {
|
|
9430
|
-
|
|
9504
|
+
const storageBeefBytes = storageBeef.toBinary();
|
|
9505
|
+
newTx = await traceStorageStep(storage, "wallet.storage.create_action.create_record", parent, {
|
|
9506
|
+
"action.label_count": vargs.labels.length,
|
|
9507
|
+
"action.storage_beef_bytes": storageBeefBytes.length
|
|
9508
|
+
}, async (span) => {
|
|
9509
|
+
const transaction = await createNewTxRecord(storage, userId, vargs, storageBeefBytes);
|
|
9510
|
+
span?.end({ attributes: { "action.transaction_record_created": true } });
|
|
9511
|
+
return transaction;
|
|
9512
|
+
});
|
|
9431
9513
|
logger?.log("created new transaction record");
|
|
9432
9514
|
const ctx = {
|
|
9433
9515
|
xinputs,
|
|
9434
9516
|
xoutputs,
|
|
9435
9517
|
changeBasket,
|
|
9436
9518
|
noSendChangeIn,
|
|
9437
|
-
availableChangeCount,
|
|
9438
9519
|
feeModel,
|
|
9439
9520
|
transactionId: newTx.transactionId
|
|
9440
9521
|
};
|
|
9441
|
-
const { allocatedChange, changeOutputs, derivationPrefix, maxPossibleSatoshisAdjustment } = await fundNewTransactionSdk(storage, userId, vargs, ctx);
|
|
9522
|
+
const { allocatedChange, changeOutputs, derivationPrefix, maxPossibleSatoshisAdjustment } = await fundNewTransactionSdk(storage, userId, vargs, ctx, initialFundingPlan, parent);
|
|
9442
9523
|
logger?.log("funded new transaction");
|
|
9443
9524
|
if (maxPossibleSatoshisAdjustment != null) {
|
|
9444
9525
|
const a = maxPossibleSatoshisAdjustment;
|
|
@@ -9447,12 +9528,27 @@ async function createAction(storage, auth, vargs, _originator) {
|
|
|
9447
9528
|
logger?.log("adjusted change outputs to max possible");
|
|
9448
9529
|
}
|
|
9449
9530
|
const satoshis = changeOutputs.reduce((a, e) => a + e.satoshis, 0) - allocatedChange.reduce((a, e) => a + e.satoshis, 0);
|
|
9450
|
-
await storage.
|
|
9451
|
-
|
|
9531
|
+
const { outputs, changeVouts } = await traceStorageStep(storage, "wallet.storage.create_action.persist_outputs", parent, {
|
|
9532
|
+
"action.fixed_output_count": ctx.xoutputs.length,
|
|
9533
|
+
"action.change_output_count": changeOutputs.length
|
|
9534
|
+
}, async (span) => {
|
|
9535
|
+
await storage.updateTransaction(newTx.transactionId, { satoshis });
|
|
9536
|
+
const persisted = await createNewOutputs(storage, userId, vargs, ctx, changeOutputs);
|
|
9537
|
+
span?.end({ attributes: { "action.persisted_output_count": persisted.outputs.length } });
|
|
9538
|
+
return persisted;
|
|
9539
|
+
});
|
|
9452
9540
|
logger?.log("created new output records");
|
|
9453
|
-
const inputBeef = await mergeAllocatedChangeBeefs(storage,
|
|
9541
|
+
const inputBeef = await mergeAllocatedChangeBeefs(storage, vargs, allocatedChange, beef, parent);
|
|
9454
9542
|
logger?.log("merged allocated change beefs");
|
|
9455
|
-
const inputs = await
|
|
9543
|
+
const inputs = await traceStorageStep(storage, "wallet.storage.create_action.assemble_inputs", parent, {
|
|
9544
|
+
"action.fixed_input_count": ctx.xinputs.length,
|
|
9545
|
+
"action.funding_input_count": allocatedChange.length,
|
|
9546
|
+
"action.include_source_transactions": vargs.includeAllSourceTransactions
|
|
9547
|
+
}, async (span) => {
|
|
9548
|
+
const assembled = await createNewInputs(storage, userId, vargs, ctx, allocatedChange);
|
|
9549
|
+
span?.end({ attributes: { "action.result_input_count": assembled.length } });
|
|
9550
|
+
return assembled;
|
|
9551
|
+
});
|
|
9456
9552
|
logger?.log("created new inputs");
|
|
9457
9553
|
const r = {
|
|
9458
9554
|
reference: newTx.reference,
|
|
@@ -9731,14 +9827,16 @@ async function createNewTxRecord(storage, userId, vargs, storageBeef) {
|
|
|
9731
9827
|
satoshis: 0,
|
|
9732
9828
|
userId,
|
|
9733
9829
|
isOutgoing: true,
|
|
9734
|
-
inputBEEF: storageBeef
|
|
9830
|
+
inputBEEF: storageBeef,
|
|
9735
9831
|
description: vargs.description,
|
|
9736
9832
|
txid: void 0,
|
|
9737
9833
|
rawTx: void 0
|
|
9738
9834
|
};
|
|
9739
9835
|
newTx.transactionId = await storage.insertTransaction(newTx);
|
|
9740
|
-
|
|
9741
|
-
|
|
9836
|
+
const labelNames = [...new Set(vargs.labels)];
|
|
9837
|
+
const labels = await storage.findOrInsertTxLabelsBulk(userId, labelNames);
|
|
9838
|
+
for (const label of labelNames) {
|
|
9839
|
+
const txLabel = labels[label];
|
|
9742
9840
|
await storage.findOrInsertTxLabelMap(verifyId(newTx.transactionId), verifyId(txLabel.txLabelId));
|
|
9743
9841
|
}
|
|
9744
9842
|
return newTx;
|
|
@@ -9923,87 +10021,222 @@ async function validateNoSendChange(storage, userId, vargs, changeBasket) {
|
|
|
9923
10021
|
const r = [];
|
|
9924
10022
|
if (!vargs.isNoSend) return [];
|
|
9925
10023
|
const noSendChange = vargs.options.noSendChange;
|
|
9926
|
-
if (noSendChange && noSendChange.length > 0)
|
|
9927
|
-
const
|
|
9928
|
-
|
|
9929
|
-
|
|
9930
|
-
|
|
9931
|
-
|
|
9932
|
-
|
|
9933
|
-
|
|
9934
|
-
r.push(output);
|
|
10024
|
+
if (noSendChange && noSendChange.length > 0) {
|
|
10025
|
+
const byOutpoint = await storage.findOutputsByOutpoints(userId, noSendChange);
|
|
10026
|
+
for (const op of noSendChange) {
|
|
10027
|
+
const output = byOutpoint[`${op.txid}.${op.vout}`];
|
|
10028
|
+
if (!isAutoSpendableChangeOutput(output) || !verifyNumber(output.satoshis) || output.basketId !== changeBasket.basketId) throw new WERR_INVALID_PARAMETER("noSendChange outpoint", "wallet-managed BRC-29 change");
|
|
10029
|
+
if (r.some((o) => o.outputId === output.outputId)) throw new WERR_INVALID_PARAMETER("noSendChange outpoint", "unique. Duplicates are not allowed.");
|
|
10030
|
+
r.push(output);
|
|
10031
|
+
}
|
|
9935
10032
|
}
|
|
9936
10033
|
if ((await storage.findReservedActionBatchOutputIds(r.map((output) => output.outputId))).length > 0) throw new WERR_INVALID_PARAMETER("noSendChange", "outputs not reserved by an active action batch");
|
|
9937
10034
|
return r;
|
|
9938
10035
|
}
|
|
9939
|
-
|
|
9940
|
-
|
|
9941
|
-
|
|
9942
|
-
|
|
9943
|
-
|
|
9944
|
-
|
|
9945
|
-
|
|
9946
|
-
|
|
9947
|
-
if (
|
|
9948
|
-
|
|
9949
|
-
|
|
9950
|
-
|
|
9951
|
-
|
|
9952
|
-
|
|
9953
|
-
|
|
9954
|
-
|
|
9955
|
-
|
|
10036
|
+
var FundingClaimConflict = class extends Error {
|
|
10037
|
+
conflict;
|
|
10038
|
+
constructor(conflict) {
|
|
10039
|
+
super("createAction funding claim changed concurrently");
|
|
10040
|
+
this.conflict = conflict;
|
|
10041
|
+
}
|
|
10042
|
+
};
|
|
10043
|
+
async function traceStorageStep(storage, name, parent, attributes, callback) {
|
|
10044
|
+
if (!storage.telemetry.enabled) return await callback();
|
|
10045
|
+
return await storage.telemetry.withSpan(name, {
|
|
10046
|
+
component: "wallet-storage",
|
|
10047
|
+
parent: parent?.context,
|
|
10048
|
+
attributes
|
|
10049
|
+
}, async (span) => await callback(span));
|
|
10050
|
+
}
|
|
10051
|
+
function makeFundingParams(vargs, xinputs, xoutputs, changeBasket, feeModel, availableChangeCount) {
|
|
10052
|
+
return {
|
|
10053
|
+
fixedInputs: xinputs.map((input) => ({
|
|
10054
|
+
satoshis: input.satoshis,
|
|
10055
|
+
unlockingScriptLength: input.unlockingScriptLength
|
|
9956
10056
|
})),
|
|
9957
|
-
fixedOutputs:
|
|
9958
|
-
satoshis:
|
|
9959
|
-
lockingScriptLength:
|
|
10057
|
+
fixedOutputs: xoutputs.map((output) => ({
|
|
10058
|
+
satoshis: output.satoshis,
|
|
10059
|
+
lockingScriptLength: output.lockingScript.length / 2
|
|
9960
10060
|
})),
|
|
9961
|
-
feeModel
|
|
9962
|
-
changeInitialSatoshis: Math.max(1,
|
|
9963
|
-
changeFirstSatoshis: Math.max(1, Math.round(
|
|
10061
|
+
feeModel,
|
|
10062
|
+
changeInitialSatoshis: Math.max(1, changeBasket.minimumDesiredUTXOValue),
|
|
10063
|
+
changeFirstSatoshis: Math.max(1, Math.round(changeBasket.minimumDesiredUTXOValue / 4)),
|
|
9964
10064
|
changeLockingScriptLength: 25,
|
|
9965
10065
|
changeUnlockingScriptLength: 107,
|
|
9966
|
-
targetNetCount:
|
|
10066
|
+
targetNetCount: changeBasket.numberOfDesiredUTXOs - availableChangeCount,
|
|
9967
10067
|
randomVals: vargs.randomVals
|
|
9968
10068
|
};
|
|
9969
|
-
|
|
9970
|
-
|
|
9971
|
-
const
|
|
9972
|
-
|
|
9973
|
-
|
|
9974
|
-
|
|
9975
|
-
|
|
9976
|
-
|
|
9977
|
-
|
|
9978
|
-
|
|
9979
|
-
|
|
9980
|
-
|
|
10069
|
+
}
|
|
10070
|
+
async function prepareFundingPlan(storage, context) {
|
|
10071
|
+
const { userId, vargs, xinputs, xoutputs, changeBasket, noSendChangeIn, feeModel, parent } = context;
|
|
10072
|
+
const excludeSending = !vargs.isDelayed;
|
|
10073
|
+
const candidates = await traceStorageStep(storage, "wallet.storage.create_action.funding_candidates", parent, { "funding.exclude_sending": excludeSending }, async (span) => {
|
|
10074
|
+
const outputs = await storage.findAvailableManagedChangeInputs(userId, changeBasket.basketId, excludeSending);
|
|
10075
|
+
span?.end({ attributes: {
|
|
10076
|
+
"funding.candidate_count": outputs.length,
|
|
10077
|
+
"funding.candidate_satoshis": outputs.reduce((sum, output) => sum + output.satoshis, 0)
|
|
10078
|
+
} });
|
|
10079
|
+
return outputs;
|
|
10080
|
+
});
|
|
10081
|
+
const noSendIds = new Set(noSendChangeIn.map((output) => output.outputId));
|
|
10082
|
+
const available = candidates.filter((output) => !noSendIds.has(output.outputId));
|
|
10083
|
+
const params = makeFundingParams(vargs, xinputs, xoutputs, changeBasket, feeModel, candidates.length);
|
|
10084
|
+
return await traceStorageStep(storage, "wallet.storage.create_action.funding_plan", parent, {
|
|
10085
|
+
"funding.candidate_count": available.length,
|
|
10086
|
+
"funding.no_send_change_count": noSendChangeIn.length
|
|
10087
|
+
}, async (span) => {
|
|
10088
|
+
const allocated = /* @__PURE__ */ new Map();
|
|
10089
|
+
const noSend = [...noSendChangeIn];
|
|
10090
|
+
const allocate = async (targetSatoshis, exactSatoshis) => {
|
|
10091
|
+
let output = noSend.pop();
|
|
10092
|
+
output ??= selectCanonicalChange(available.filter((candidate) => !allocated.has(candidate.outputId)), targetSatoshis, exactSatoshis);
|
|
10093
|
+
if (output == null) return void 0;
|
|
10094
|
+
allocated.set(output.outputId, output);
|
|
10095
|
+
return {
|
|
10096
|
+
outputId: output.outputId,
|
|
10097
|
+
satoshis: output.satoshis
|
|
10098
|
+
};
|
|
10099
|
+
};
|
|
10100
|
+
const release = async (outputId) => {
|
|
10101
|
+
const output = allocated.get(outputId);
|
|
10102
|
+
if (output == null) return;
|
|
10103
|
+
allocated.delete(outputId);
|
|
10104
|
+
if (noSendIds.has(outputId)) noSend.push(output);
|
|
10105
|
+
};
|
|
10106
|
+
const result = await generateChangeSdk(params, allocate, release, vargs.logger, storage.telemetry);
|
|
10107
|
+
const selected = result.allocatedChangeInputs.map((input) => verifyTruthy(allocated.get(input.outputId)));
|
|
10108
|
+
span?.end({ attributes: {
|
|
10109
|
+
"funding.allocated_input_count": selected.length,
|
|
10110
|
+
"funding.change_output_count": result.changeOutputs.length,
|
|
10111
|
+
"funding.fee_satoshis": result.fee,
|
|
10112
|
+
"funding.transaction_size_bytes": result.size
|
|
10113
|
+
} });
|
|
10114
|
+
return {
|
|
10115
|
+
params,
|
|
10116
|
+
result,
|
|
10117
|
+
selected,
|
|
10118
|
+
availableChangeCount: candidates.length
|
|
10119
|
+
};
|
|
10120
|
+
});
|
|
10121
|
+
}
|
|
10122
|
+
async function claimFundingPlan(storage, userId, basketId, excludeSending, transactionId, noSendChangeIn, plan) {
|
|
10123
|
+
if (plan.selected.length === 0) return {
|
|
10124
|
+
outputs: [],
|
|
10125
|
+
sourceTransactionCount: 0,
|
|
10126
|
+
hydratedScriptCount: 0,
|
|
10127
|
+
scriptSourceTransactionCount: 0
|
|
10128
|
+
};
|
|
10129
|
+
const noSendIds = new Set(noSendChangeIn.map((output) => output.outputId));
|
|
10130
|
+
const statuses = ["completed", "unproven"];
|
|
10131
|
+
if (!excludeSending) statuses.push("sending");
|
|
10132
|
+
const claim = await storage.transaction(async (trx) => {
|
|
10133
|
+
const outpoints = plan.selected.map((output) => {
|
|
10134
|
+
if (output.txid == null) throw new WERR_INTERNAL("planned change input is missing txid");
|
|
9981
10135
|
return {
|
|
9982
|
-
|
|
9983
|
-
|
|
10136
|
+
txid: output.txid,
|
|
10137
|
+
vout: output.vout
|
|
9984
10138
|
};
|
|
10139
|
+
});
|
|
10140
|
+
const currentByOutpoint = await storage.findOutputsByOutpointsForUpdate(userId, outpoints, trx, true);
|
|
10141
|
+
const reserved = new Set(await storage.findReservedActionBatchOutputIds(plan.selected.map((output) => output.outputId), trx));
|
|
10142
|
+
const transactionIds = [...new Set(Object.values(currentByOutpoint).map((output) => output.transactionId))];
|
|
10143
|
+
const transactionStatuses = await storage.findTransactionStatusesByIds(userId, transactionIds, trx);
|
|
10144
|
+
const claimed = [];
|
|
10145
|
+
for (const planned of plan.selected) {
|
|
10146
|
+
const current = currentByOutpoint[`${String(planned.txid)}.${planned.vout}`];
|
|
10147
|
+
const currentStatus = current == null ? void 0 : transactionStatuses.get(current.transactionId);
|
|
10148
|
+
const validTransaction = currentStatus != null && statuses.includes(currentStatus);
|
|
10149
|
+
if (current?.outputId !== planned.outputId || current?.satoshis !== planned.satoshis || current?.basketId !== basketId || !isAutoSpendableChangeOutput(current) || reserved.has(current?.outputId ?? -1) || validTransaction !== true) return { conflict: noSendIds.has(planned.outputId) ? "noSendChange" : "candidate" };
|
|
10150
|
+
claimed.push(current);
|
|
10151
|
+
}
|
|
10152
|
+
if (await storage.markChangeInputsSpent(claimed.map((output) => output.outputId), transactionId, trx) !== claimed.length) throw new FundingClaimConflict(claimed.some((output) => noSendIds.has(output.outputId)) ? "noSendChange" : "candidate");
|
|
10153
|
+
for (const output of claimed) {
|
|
10154
|
+
output.spendable = false;
|
|
10155
|
+
output.spentBy = transactionId;
|
|
9985
10156
|
}
|
|
9986
|
-
const basketId = ctx.changeBasket.basketId;
|
|
9987
|
-
const o = await storage.allocateChangeInput(userId, basketId, targetSatoshis, exactSatoshis, !vargs.isDelayed, ctx.transactionId);
|
|
9988
|
-
if (o == null) return void 0;
|
|
9989
|
-
outputs[o.outputId] = o;
|
|
9990
10157
|
return {
|
|
9991
|
-
|
|
9992
|
-
|
|
10158
|
+
outputs: claimed,
|
|
10159
|
+
sourceTransactionCount: transactionIds.length
|
|
9993
10160
|
};
|
|
10161
|
+
}).catch((error) => {
|
|
10162
|
+
if (error instanceof FundingClaimConflict) return { conflict: error.conflict };
|
|
10163
|
+
throw error;
|
|
10164
|
+
});
|
|
10165
|
+
if (claim.outputs == null) return claim;
|
|
10166
|
+
const hydration = await hydrateFundingInputScripts(storage, claim.outputs);
|
|
10167
|
+
return {
|
|
10168
|
+
outputs: claim.outputs,
|
|
10169
|
+
sourceTransactionCount: claim.sourceTransactionCount,
|
|
10170
|
+
...hydration
|
|
9994
10171
|
};
|
|
9995
|
-
|
|
9996
|
-
|
|
9997
|
-
|
|
9998
|
-
|
|
9999
|
-
|
|
10172
|
+
}
|
|
10173
|
+
async function hydrateFundingInputScripts(storage, outputs) {
|
|
10174
|
+
const missing = outputs.filter((output) => output.lockingScript?.length !== output.scriptLength && output.scriptLength != null && output.scriptLength > 0 && output.scriptOffset != null && output.scriptOffset > 0 && output.txid != null && output.txid !== "");
|
|
10175
|
+
if (missing.length === 0) return {
|
|
10176
|
+
hydratedScriptCount: 0,
|
|
10177
|
+
scriptSourceTransactionCount: 0
|
|
10178
|
+
};
|
|
10179
|
+
const byTxid = /* @__PURE__ */ new Map();
|
|
10180
|
+
for (const output of missing) {
|
|
10181
|
+
const txid = verifyTruthy(output.txid);
|
|
10182
|
+
const group = byTxid.get(txid) ?? [];
|
|
10183
|
+
group.push(output);
|
|
10184
|
+
byTxid.set(txid, group);
|
|
10185
|
+
}
|
|
10186
|
+
const groups = [...byTxid.entries()];
|
|
10187
|
+
let cursor = 0;
|
|
10188
|
+
await Promise.all(Array.from({ length: Math.min(8, groups.length) }, async () => {
|
|
10189
|
+
while (cursor < groups.length) {
|
|
10190
|
+
const [txid, group] = groups[cursor++];
|
|
10191
|
+
if (group.length === 1) {
|
|
10192
|
+
await storage.validateOutputScript(group[0]);
|
|
10193
|
+
continue;
|
|
10194
|
+
}
|
|
10195
|
+
const rawTx = await storage.getRawTxOfKnownValidTransaction(txid);
|
|
10196
|
+
if (rawTx != null) for (const output of group) output.lockingScript = rawTx.slice(output.scriptOffset, output.scriptOffset + output.scriptLength);
|
|
10197
|
+
else for (const output of group) await storage.validateOutputScript(output);
|
|
10000
10198
|
}
|
|
10001
|
-
|
|
10002
|
-
|
|
10003
|
-
|
|
10004
|
-
|
|
10199
|
+
}));
|
|
10200
|
+
return {
|
|
10201
|
+
hydratedScriptCount: missing.filter((output) => output.lockingScript?.length === output.scriptLength).length,
|
|
10202
|
+
scriptSourceTransactionCount: groups.length
|
|
10005
10203
|
};
|
|
10006
|
-
|
|
10204
|
+
}
|
|
10205
|
+
async function fundNewTransactionSdk(storage, userId, vargs, ctx, initialPlan, parent) {
|
|
10206
|
+
let plan = initialPlan;
|
|
10207
|
+
let allocatedChange;
|
|
10208
|
+
let retryCount = 0;
|
|
10209
|
+
await traceStorageStep(storage, "wallet.storage.create_action.funding_claim", parent, { "funding.planned_input_count": initialPlan.selected.length }, async (span) => {
|
|
10210
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
10211
|
+
const claim = await claimFundingPlan(storage, userId, ctx.changeBasket.basketId, !vargs.isDelayed, ctx.transactionId, ctx.noSendChangeIn, plan);
|
|
10212
|
+
if (claim.outputs != null) {
|
|
10213
|
+
allocatedChange = claim.outputs;
|
|
10214
|
+
span?.end({ attributes: {
|
|
10215
|
+
"funding.claim_retry_count": retryCount,
|
|
10216
|
+
"funding.source_transaction_count": claim.sourceTransactionCount,
|
|
10217
|
+
"funding.hydrated_script_count": claim.hydratedScriptCount,
|
|
10218
|
+
"funding.script_source_transaction_count": claim.scriptSourceTransactionCount
|
|
10219
|
+
} });
|
|
10220
|
+
return;
|
|
10221
|
+
}
|
|
10222
|
+
if (claim.conflict === "noSendChange") throw new WERR_INVALID_PARAMETER("noSendChange", "outputs that remain spendable during action planning");
|
|
10223
|
+
retryCount++;
|
|
10224
|
+
plan = await prepareFundingPlan(storage, {
|
|
10225
|
+
userId,
|
|
10226
|
+
vargs,
|
|
10227
|
+
xinputs: ctx.xinputs,
|
|
10228
|
+
xoutputs: ctx.xoutputs,
|
|
10229
|
+
changeBasket: ctx.changeBasket,
|
|
10230
|
+
noSendChangeIn: ctx.noSendChangeIn,
|
|
10231
|
+
feeModel: ctx.feeModel,
|
|
10232
|
+
parent
|
|
10233
|
+
});
|
|
10234
|
+
}
|
|
10235
|
+
throw new WERR_INVALID_OPERATION("wallet funding changed repeatedly during action planning; retry createAction");
|
|
10236
|
+
});
|
|
10237
|
+
if (allocatedChange == null) throw new WERR_INTERNAL("funding plan was not claimed");
|
|
10238
|
+
const params = plan.params;
|
|
10239
|
+
const gcr = plan.result;
|
|
10007
10240
|
const nextRandomVal = () => {
|
|
10008
10241
|
let val = 0;
|
|
10009
10242
|
if (vargs.randomVals == null || vargs.randomVals.length === 0) {
|
|
@@ -10031,7 +10264,7 @@ async function fundNewTransactionSdk(storage, userId, vargs, ctx) {
|
|
|
10031
10264
|
const derivationPrefix = randomDerivation(16);
|
|
10032
10265
|
return {
|
|
10033
10266
|
maxPossibleSatoshisAdjustment: gcr.maxPossibleSatoshisAdjustment,
|
|
10034
|
-
allocatedChange
|
|
10267
|
+
allocatedChange,
|
|
10035
10268
|
changeOutputs: gcr.changeOutputs.map((o, i) => ({
|
|
10036
10269
|
created_at: /* @__PURE__ */ new Date(),
|
|
10037
10270
|
updated_at: /* @__PURE__ */ new Date(),
|
|
@@ -10066,12 +10299,24 @@ async function fundNewTransactionSdk(storage, userId, vargs, ctx) {
|
|
|
10066
10299
|
*/
|
|
10067
10300
|
function trimInputBeef(beef, vargs) {
|
|
10068
10301
|
if (vargs.options.returnTXIDOnly) return void 0;
|
|
10069
|
-
const
|
|
10070
|
-
for (const
|
|
10071
|
-
for (const txid of beef.txs.map((btx) => btx.txid)) if (knownTxids[txid]) beef.makeTxidOnly(txid);
|
|
10302
|
+
const hasKnownTxid = makeKnownTxidLookup(vargs.options.knownTxids ?? []);
|
|
10303
|
+
for (const btx of beef.txs) if (hasKnownTxid(btx.txid)) beef.makeTxidOnly(btx.txid);
|
|
10072
10304
|
return beef.toUint8Array();
|
|
10073
10305
|
}
|
|
10074
|
-
|
|
10306
|
+
function makeKnownTxidLookup(knownTxids) {
|
|
10307
|
+
let lookups = 0;
|
|
10308
|
+
let indexed;
|
|
10309
|
+
return (txid) => {
|
|
10310
|
+
lookups++;
|
|
10311
|
+
if (indexed != null) return indexed.has(txid);
|
|
10312
|
+
if (knownTxids.length > 64 && lookups > 4) {
|
|
10313
|
+
indexed = new Set(knownTxids);
|
|
10314
|
+
return indexed.has(txid);
|
|
10315
|
+
}
|
|
10316
|
+
return knownTxids.includes(txid);
|
|
10317
|
+
};
|
|
10318
|
+
}
|
|
10319
|
+
async function mergeAllocatedChangeBeefs(storage, vargs, allocatedChange, beef, parent) {
|
|
10075
10320
|
const options = {
|
|
10076
10321
|
trustSelf: void 0,
|
|
10077
10322
|
knownTxids: vargs.options.knownTxids,
|
|
@@ -10082,25 +10327,52 @@ async function mergeAllocatedChangeBeefs(storage, userId, vargs, allocatedChange
|
|
|
10082
10327
|
minProofLevel: void 0
|
|
10083
10328
|
};
|
|
10084
10329
|
if (vargs.options.returnTXIDOnly) return void 0;
|
|
10085
|
-
const
|
|
10086
|
-
const
|
|
10330
|
+
const knownTxids = vargs.options.knownTxids ?? [];
|
|
10331
|
+
const hasKnownTxid = makeKnownTxidLookup(knownTxids);
|
|
10332
|
+
const missing = Array.from(new Set(allocatedChange.map((output) => verifyTruthy(output.txid)).filter((txid) => beef.findTxid(txid) == null && !hasKnownTxid(txid))));
|
|
10087
10333
|
const fetched = Array.from({ length: missing.length });
|
|
10088
10334
|
const concurrency = Math.min(8, Math.max(1, missing.length));
|
|
10089
10335
|
let cursor = 0;
|
|
10090
|
-
await
|
|
10091
|
-
|
|
10092
|
-
|
|
10093
|
-
|
|
10094
|
-
|
|
10095
|
-
|
|
10096
|
-
|
|
10097
|
-
}
|
|
10098
|
-
|
|
10099
|
-
|
|
10100
|
-
|
|
10101
|
-
|
|
10102
|
-
|
|
10103
|
-
|
|
10336
|
+
await traceStorageStep(storage, "wallet.storage.create_action.beef_fetch", parent, {
|
|
10337
|
+
"beef.allocated_change_count": allocatedChange.length,
|
|
10338
|
+
"beef.distinct_source_count": new Set(allocatedChange.map((output) => output.txid)).size,
|
|
10339
|
+
"beef.known_txid_count": knownTxids.length,
|
|
10340
|
+
"beef.missing_source_count": missing.length,
|
|
10341
|
+
"beef.fetch_concurrency": concurrency
|
|
10342
|
+
}, async (span) => {
|
|
10343
|
+
await Promise.all(Array.from({ length: concurrency }, async () => {
|
|
10344
|
+
while (cursor < missing.length) {
|
|
10345
|
+
const index = cursor++;
|
|
10346
|
+
fetched[index] = await storage.getBeefForTransaction(missing[index], {
|
|
10347
|
+
...options,
|
|
10348
|
+
mergeToBeef: void 0
|
|
10349
|
+
});
|
|
10350
|
+
}
|
|
10351
|
+
}));
|
|
10352
|
+
span?.end({ attributes: {
|
|
10353
|
+
"beef.fetched_tx_count": fetched.reduce((sum, item) => sum + (item?.txs.length ?? 0), 0),
|
|
10354
|
+
"beef.fetched_bump_count": fetched.reduce((sum, item) => sum + (item?.bumps.length ?? 0), 0)
|
|
10355
|
+
} });
|
|
10356
|
+
});
|
|
10357
|
+
await traceStorageStep(storage, "wallet.storage.create_action.beef_merge", parent, { "beef.fragment_count": fetched.length }, async (span) => {
|
|
10358
|
+
for (const fetchedBeef of fetched) {
|
|
10359
|
+
if (fetchedBeef == null) continue;
|
|
10360
|
+
beef.mergeBeef(fetchedBeef);
|
|
10361
|
+
}
|
|
10362
|
+
span?.end({ attributes: {
|
|
10363
|
+
"beef.merged_tx_count": beef.txs.length,
|
|
10364
|
+
"beef.merged_bump_count": beef.bumps.length
|
|
10365
|
+
} });
|
|
10366
|
+
});
|
|
10367
|
+
return await traceStorageStep(storage, "wallet.storage.create_action.beef_trim_serialize", parent, {
|
|
10368
|
+
"beef.tx_count": beef.txs.length,
|
|
10369
|
+
"beef.bump_count": beef.bumps.length,
|
|
10370
|
+
"beef.known_txid_count": knownTxids.length
|
|
10371
|
+
}, async (span) => {
|
|
10372
|
+
const result = trimInputBeef(beef, vargs);
|
|
10373
|
+
span?.end({ attributes: { "beef.result_bytes": result?.length ?? 0 } });
|
|
10374
|
+
return result;
|
|
10375
|
+
});
|
|
10104
10376
|
}
|
|
10105
10377
|
const dirtyHashLookup = {
|
|
10106
10378
|
"00000000000000000019f112ec0a9982926f1258cdcc558dd7c3b7e5dc7fa148": "This is the first header of the invalid SegWit chain.",
|
|
@@ -12715,21 +12987,6 @@ async function cleanupExpiredActionBatches(storage) {
|
|
|
12715
12987
|
});
|
|
12716
12988
|
return released;
|
|
12717
12989
|
}
|
|
12718
|
-
async function availableManagedChange(storage, userId, basketId, excludeSending, trx) {
|
|
12719
|
-
const statuses = ["completed", "unproven"];
|
|
12720
|
-
if (!excludeSending) statuses.push("sending");
|
|
12721
|
-
const outputs = (await storage.findOutputs({
|
|
12722
|
-
partial: {
|
|
12723
|
-
userId,
|
|
12724
|
-
basketId,
|
|
12725
|
-
spendable: true
|
|
12726
|
-
},
|
|
12727
|
-
txStatus: statuses,
|
|
12728
|
-
trx
|
|
12729
|
-
})).filter(isAutoSpendableChangeOutput);
|
|
12730
|
-
const reserved = new Set(await storage.findReservedActionBatchOutputIds(outputs.map((o) => o.outputId), trx));
|
|
12731
|
-
return outputs.filter((output) => output.spentBy == null && !reserved.has(output.outputId));
|
|
12732
|
-
}
|
|
12733
12990
|
function sourceOutputFromBeef(beef, outpoint) {
|
|
12734
12991
|
const output = (beef.findTxid(outpoint.txid)?.tx)?.outputs[outpoint.vout];
|
|
12735
12992
|
if (output == null) return void 0;
|
|
@@ -12894,7 +13151,7 @@ async function beginActionBatch(storage, auth, args) {
|
|
|
12894
13151
|
const explicit = await resolveExplicitOutputs(storage, userId, args.firstAction, outputScriptLengths != null);
|
|
12895
13152
|
const noSendChange = await resolveNoSendChangeOutputs(storage, userId, args.firstAction);
|
|
12896
13153
|
const fixedOutputIds = new Set([...explicit.outputs, ...noSendChange.outputs].map((output) => output.outputId));
|
|
12897
|
-
const available = (await
|
|
13154
|
+
const available = (await storage.findAvailableManagedChangeInputs(userId, changeBasket.basketId, !args.firstAction.isDelayed)).filter((output) => !fixedOutputIds.has(output.outputId));
|
|
12898
13155
|
const target = estimateFirstActionTarget(storage, args.firstAction, explicit.inputSatoshis + noSendChange.inputSatoshis, outputScriptLengths);
|
|
12899
13156
|
const fixedOutputs = [...explicit.outputs, ...noSendChange.outputs];
|
|
12900
13157
|
const funding = chooseReservationPool(available, target, Math.max(0, INITIAL_RESERVATION_LIMIT - fixedOutputs.length), INITIAL_EXTRA_OUTPUTS, false, reservationPlanningCosts(storage, changeBasket));
|
|
@@ -12938,7 +13195,7 @@ async function extendActionBatch(storage, auth, args) {
|
|
|
12938
13195
|
name: "default"
|
|
12939
13196
|
} }));
|
|
12940
13197
|
const alreadyReserved = await storage.findActionBatchOutputIds(batch.actionBatchId);
|
|
12941
|
-
const available = await
|
|
13198
|
+
const available = await storage.findAvailableManagedChangeInputs(userId, basket.basketId, false);
|
|
12942
13199
|
if (!Number.isSafeInteger(args.requestedOutputs) || args.requestedOutputs < 0) throw new WERR_INVALID_PARAMETER("requestedOutputs", "non-negative safe integer");
|
|
12943
13200
|
const requestedCount = Math.min(args.requestedOutputs, 64);
|
|
12944
13201
|
const funding = chooseReservationPool(available, Math.max(1, args.targetSatoshis), requestedCount, 0, true, reservationPlanningCosts(storage, basket));
|
|
@@ -13311,6 +13568,31 @@ async function abortActionBatch(storage, auth, batchId) {
|
|
|
13311
13568
|
});
|
|
13312
13569
|
}
|
|
13313
13570
|
//#endregion
|
|
13571
|
+
//#region ../src/storage/methods/availableManagedChange.ts
|
|
13572
|
+
/**
|
|
13573
|
+
* Return the exact set of wallet-managed outputs currently eligible for
|
|
13574
|
+
* automatic funding. Keeping this predicate shared prevents the planner,
|
|
13575
|
+
* allocator, action-batch reservations, and availability count from drifting.
|
|
13576
|
+
*/
|
|
13577
|
+
async function availableManagedChange(storage, userId, basketId, excludeSending, trx) {
|
|
13578
|
+
const statuses = ["completed", "unproven"];
|
|
13579
|
+
if (!excludeSending) statuses.push("sending");
|
|
13580
|
+
const outputs = (await storage.findOutputs({
|
|
13581
|
+
partial: {
|
|
13582
|
+
userId,
|
|
13583
|
+
basketId,
|
|
13584
|
+
spendable: true,
|
|
13585
|
+
...managedChangeOutputFields
|
|
13586
|
+
},
|
|
13587
|
+
txStatus: statuses,
|
|
13588
|
+
noScript: true,
|
|
13589
|
+
trx
|
|
13590
|
+
})).filter(isAutoSpendableChangeOutput);
|
|
13591
|
+
if (outputs.length === 0) return outputs;
|
|
13592
|
+
const reserved = new Set(await storage.findReservedActionBatchOutputIds(outputs.map((output) => output.outputId), trx));
|
|
13593
|
+
return outputs.filter((output) => !reserved.has(output.outputId));
|
|
13594
|
+
}
|
|
13595
|
+
//#endregion
|
|
13314
13596
|
//#region ../src/storage/StorageProvider.ts
|
|
13315
13597
|
var StorageProvider = class StorageProvider extends StorageReaderWriter {
|
|
13316
13598
|
isDirty = false;
|
|
@@ -13344,6 +13626,33 @@ var StorageProvider = class StorageProvider extends StorageReaderWriter {
|
|
|
13344
13626
|
this.maxRecursionDepth = 12;
|
|
13345
13627
|
this.scriptVerifier = options.scriptVerifier;
|
|
13346
13628
|
}
|
|
13629
|
+
/** Mark a planned set of change inputs spent within the caller's transaction. */
|
|
13630
|
+
async markChangeInputsSpent(outputIds, transactionId, trx) {
|
|
13631
|
+
let updated = 0;
|
|
13632
|
+
const current = await this.findOutputsByIds(outputIds, trx);
|
|
13633
|
+
for (const outputId of outputIds) {
|
|
13634
|
+
const output = current[outputId];
|
|
13635
|
+
if (output == null || !output.spendable || output.spentBy != null) continue;
|
|
13636
|
+
updated += await this.updateOutput(outputId, {
|
|
13637
|
+
spendable: false,
|
|
13638
|
+
spentBy: transactionId
|
|
13639
|
+
}, trx);
|
|
13640
|
+
}
|
|
13641
|
+
return updated;
|
|
13642
|
+
}
|
|
13643
|
+
/** Return unreserved wallet-managed outputs eligible for automatic funding. */
|
|
13644
|
+
async findAvailableManagedChangeInputs(userId, basketId, excludeSending, trx) {
|
|
13645
|
+
return await availableManagedChange(this, userId, basketId, excludeSending, trx);
|
|
13646
|
+
}
|
|
13647
|
+
/** Read the current status of a set of source transactions without loading raw transaction bytes. */
|
|
13648
|
+
async findTransactionStatusesByIds(userId, transactionIds, trx) {
|
|
13649
|
+
const statuses = /* @__PURE__ */ new Map();
|
|
13650
|
+
for (const transactionId of new Set(transactionIds)) {
|
|
13651
|
+
const transaction = await this.findTransactionById(transactionId, trx, true);
|
|
13652
|
+
if (transaction?.userId === userId) statuses.set(transactionId, transaction.status);
|
|
13653
|
+
}
|
|
13654
|
+
return statuses;
|
|
13655
|
+
}
|
|
13347
13656
|
async insertActionBatch(_batch, _trx) {
|
|
13348
13657
|
throw new WERR_NOT_IMPLEMENTED();
|
|
13349
13658
|
}
|
|
@@ -13454,7 +13763,7 @@ var StorageProvider = class StorageProvider extends StorageReaderWriter {
|
|
|
13454
13763
|
}
|
|
13455
13764
|
return byOutpoint;
|
|
13456
13765
|
}
|
|
13457
|
-
async findOutputsByOutpointsForUpdate(userId, outpoints, trx) {
|
|
13766
|
+
async findOutputsByOutpointsForUpdate(userId, outpoints, trx, _noScript = false) {
|
|
13458
13767
|
return await this.findOutputsByOutpoints(userId, outpoints, trx);
|
|
13459
13768
|
}
|
|
13460
13769
|
async findOrInsertOutputBasketsBulk(userId, names, trx) {
|
|
@@ -14891,9 +15200,16 @@ var StorageIdb = class extends StorageProvider {
|
|
|
14891
15200
|
async initDB(storageName, storageIdentityKey) {
|
|
14892
15201
|
const chain = this.chain;
|
|
14893
15202
|
const maxOutputScript = 1024;
|
|
14894
|
-
return await openDB(this.dbName,
|
|
15203
|
+
return await openDB(this.dbName, 3, { upgrade(db, _oldVersion, _newVersion, transaction) {
|
|
14895
15204
|
upgradeAllStoresV1(db);
|
|
14896
15205
|
upgradeActionBatchStoresV2(db);
|
|
15206
|
+
const outputs = transaction.objectStore("outputs");
|
|
15207
|
+
if (!outputs.indexNames.contains("userId_basketId")) outputs.createIndex("userId_basketId", ["userId", "basketId"]);
|
|
15208
|
+
if (!outputs.indexNames.contains("txid_vout_userId")) outputs.createIndex("txid_vout_userId", [
|
|
15209
|
+
"txid",
|
|
15210
|
+
"vout",
|
|
15211
|
+
"userId"
|
|
15212
|
+
], { unique: true });
|
|
14897
15213
|
if (!db.objectStoreNames.contains("settings")) {
|
|
14898
15214
|
if (storageName == null || storageName === "" || storageIdentityKey == null || storageIdentityKey === "") throw new WERR_INVALID_OPERATION("migrate must be called before first access");
|
|
14899
15215
|
const settings = db.createObjectStore("settings", { keyPath: "storageIdentityKey" });
|
|
@@ -15099,11 +15415,53 @@ var StorageIdb = class extends StorageProvider {
|
|
|
15099
15415
|
txStatus,
|
|
15100
15416
|
noScript: true
|
|
15101
15417
|
};
|
|
15102
|
-
|
|
15418
|
+
const outputIds = [];
|
|
15103
15419
|
await this.filterOutputs(args, (r) => {
|
|
15104
|
-
if (isAutoSpendableChangeOutput(r))
|
|
15420
|
+
if (isAutoSpendableChangeOutput(r)) outputIds.push(r.outputId);
|
|
15105
15421
|
});
|
|
15106
|
-
|
|
15422
|
+
const reserved = await this.findReservedActionBatchOutputIds(outputIds);
|
|
15423
|
+
return outputIds.length - reserved.length;
|
|
15424
|
+
}
|
|
15425
|
+
async findTransactionStatusesByIds(userId, transactionIds, trx) {
|
|
15426
|
+
const statuses = /* @__PURE__ */ new Map();
|
|
15427
|
+
if (transactionIds.length === 0) return statuses;
|
|
15428
|
+
const dbTrx = this.toDbTrx(["transactions"], "readonly", trx);
|
|
15429
|
+
const store = dbTrx.objectStore("transactions");
|
|
15430
|
+
for (const transactionId of new Set(transactionIds)) {
|
|
15431
|
+
const transaction = await store.get(transactionId);
|
|
15432
|
+
if (transaction?.userId === userId) statuses.set(transactionId, transaction.status);
|
|
15433
|
+
}
|
|
15434
|
+
if (trx == null) await dbTrx.done;
|
|
15435
|
+
return statuses;
|
|
15436
|
+
}
|
|
15437
|
+
async findOutputsByOutpointsInternal(userId, outpoints, trx, noScript = false) {
|
|
15438
|
+
const byOutpoint = {};
|
|
15439
|
+
if (outpoints.length === 0) return byOutpoint;
|
|
15440
|
+
const dbTrx = this.toDbTrx(noScript ? ["outputs"] : [
|
|
15441
|
+
"outputs",
|
|
15442
|
+
"proven_txs",
|
|
15443
|
+
"proven_tx_reqs"
|
|
15444
|
+
], "readonly", trx);
|
|
15445
|
+
const index = dbTrx.objectStore("outputs").index("txid_vout_userId");
|
|
15446
|
+
const unique = [...new Map(outpoints.map((outpoint) => [`${outpoint.txid}.${outpoint.vout}`, outpoint])).values()];
|
|
15447
|
+
const rows = await Promise.all(unique.map(async (outpoint) => await index.get([
|
|
15448
|
+
outpoint.txid,
|
|
15449
|
+
outpoint.vout,
|
|
15450
|
+
userId
|
|
15451
|
+
])));
|
|
15452
|
+
for (const row of rows) {
|
|
15453
|
+
if (row == null) continue;
|
|
15454
|
+
if (!noScript) await this.validateOutputScript(row, dbTrx);
|
|
15455
|
+
byOutpoint[`${String(row.txid)}.${row.vout}`] = this.validateEntity(row);
|
|
15456
|
+
}
|
|
15457
|
+
if (trx == null) await dbTrx.done;
|
|
15458
|
+
return byOutpoint;
|
|
15459
|
+
}
|
|
15460
|
+
async findOutputsByOutpoints(userId, outpoints, trx) {
|
|
15461
|
+
return await this.findOutputsByOutpointsInternal(userId, outpoints, trx);
|
|
15462
|
+
}
|
|
15463
|
+
async findOutputsByOutpointsForUpdate(userId, outpoints, trx, noScript = false) {
|
|
15464
|
+
return await this.findOutputsByOutpointsInternal(userId, outpoints, trx, noScript);
|
|
15107
15465
|
}
|
|
15108
15466
|
async findCertificatesAuth(auth, args) {
|
|
15109
15467
|
if (auth.userId == null || args.partial.userId != null && args.partial.userId !== 0 && args.partial.userId !== auth.userId) throw new WERR_UNAUTHORIZED();
|
|
@@ -15866,6 +16224,12 @@ var StorageIdb = class extends StorageProvider {
|
|
|
15866
16224
|
partial.vout,
|
|
15867
16225
|
partial.userId
|
|
15868
16226
|
], direction);
|
|
16227
|
+
if (partial?.txid != null && partial.txid !== "" && partial?.vout !== void 0) return store.index("txid_vout_userId").openCursor([
|
|
16228
|
+
partial.txid,
|
|
16229
|
+
partial.vout,
|
|
16230
|
+
partial.userId
|
|
16231
|
+
], direction);
|
|
16232
|
+
if (partial?.basketId !== void 0) return store.index("userId_basketId").openCursor([partial.userId, partial.basketId], direction);
|
|
15869
16233
|
return store.index("userId").openCursor(partial.userId, direction);
|
|
15870
16234
|
}
|
|
15871
16235
|
if (partial?.transactionId !== void 0) return store.index("transactionId").openCursor(partial.transactionId, direction);
|
|
@@ -15873,6 +16237,17 @@ var StorageIdb = class extends StorageProvider {
|
|
|
15873
16237
|
if (partial?.spentBy !== void 0) return store.index("spentBy").openCursor(partial.spentBy, direction);
|
|
15874
16238
|
return store.openCursor(null, direction);
|
|
15875
16239
|
}
|
|
16240
|
+
async eligibleOutputTransactionIds(args, dbTrx) {
|
|
16241
|
+
if (args.txStatus == null) return void 0;
|
|
16242
|
+
const validTransactionIds = /* @__PURE__ */ new Set();
|
|
16243
|
+
const transactions = dbTrx.objectStore("transactions");
|
|
16244
|
+
for (const status of args.txStatus) {
|
|
16245
|
+
const index = args.partial.userId === void 0 ? transactions.index("status") : transactions.index("status_userId");
|
|
16246
|
+
const key = args.partial.userId === void 0 ? status : [status, args.partial.userId];
|
|
16247
|
+
for (const transactionId of await index.getAllKeys(key)) validTransactionIds.add(Number(transactionId));
|
|
16248
|
+
}
|
|
16249
|
+
return validTransactionIds;
|
|
16250
|
+
}
|
|
15876
16251
|
async filterOutputs(args, filtered, tagIds, isQueryModeAll) {
|
|
15877
16252
|
this.assertNoUndefinedInPartial(args.partial);
|
|
15878
16253
|
if (args.partial.lockingScript != null) throw new WERR_INVALID_PARAMETER("args.partial.lockingScript", "undefined. Outputs may not be found by lockingScript value.");
|
|
@@ -15882,15 +16257,10 @@ var StorageIdb = class extends StorageProvider {
|
|
|
15882
16257
|
const dbTrx = this.toDbTrx(stores, "readonly", args.trx);
|
|
15883
16258
|
const direction = args.orderDescending === true ? "prev" : "next";
|
|
15884
16259
|
const store = dbTrx.objectStore("outputs");
|
|
16260
|
+
const validTransactionIds = await this.eligibleOutputTransactionIds(args, dbTrx);
|
|
15885
16261
|
await scanCursor(await this.openOutputsCursor(store, args.partial, direction), args.since, args.paged?.offset ?? 0, args.paged?.limit, async (r) => {
|
|
15886
16262
|
if (!matchesOutputPartial(r, args.partial)) return false;
|
|
15887
|
-
if (
|
|
15888
|
-
if (await this.countTransactions({
|
|
15889
|
-
partial: { transactionId: r.transactionId },
|
|
15890
|
-
status: args.txStatus,
|
|
15891
|
-
trx: dbTrx
|
|
15892
|
-
}) === 0) return false;
|
|
15893
|
-
}
|
|
16263
|
+
if (validTransactionIds != null && !validTransactionIds.has(r.transactionId)) return false;
|
|
15894
16264
|
if (tagIds != null && tagIds.length > 0 && !await this.outputMatchesTags(r.outputId, tagIds, isQueryModeAll, dbTrx)) return false;
|
|
15895
16265
|
return true;
|
|
15896
16266
|
}, (r) => {
|