@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.cjs
CHANGED
|
@@ -4923,6 +4923,12 @@ function upgradeOutputs(db) {
|
|
|
4923
4923
|
autoIncrement: true
|
|
4924
4924
|
});
|
|
4925
4925
|
store.createIndex("userId", "userId");
|
|
4926
|
+
store.createIndex("userId_basketId", ["userId", "basketId"]);
|
|
4927
|
+
store.createIndex("txid_vout_userId", [
|
|
4928
|
+
"txid",
|
|
4929
|
+
"vout",
|
|
4930
|
+
"userId"
|
|
4931
|
+
], { unique: true });
|
|
4926
4932
|
store.createIndex("transactionId", "transactionId");
|
|
4927
4933
|
store.createIndex("basketId", "basketId");
|
|
4928
4934
|
store.createIndex("spentBy", "spentBy");
|
|
@@ -5125,7 +5131,7 @@ async function getBeefForTransaction(storage, txid, options) {
|
|
|
5125
5131
|
if (options.mergeToBeef instanceof _bsv_sdk.Beef) beef = options.mergeToBeef;
|
|
5126
5132
|
else if (options.mergeToBeef != null) beef = _bsv_sdk.Beef.fromBinary(options.mergeToBeef);
|
|
5127
5133
|
else beef = new _bsv_sdk.Beef();
|
|
5128
|
-
const
|
|
5134
|
+
const hasKnownTxid = makeKnownTxidLookup$1(options.knownTxids ?? []);
|
|
5129
5135
|
const scheduled = /* @__PURE__ */ new Set([txid]);
|
|
5130
5136
|
let frontier = [{
|
|
5131
5137
|
txid,
|
|
@@ -5135,7 +5141,7 @@ async function getBeefForTransaction(storage, txid, options) {
|
|
|
5135
5141
|
const concurrency = Number.isFinite(requestedConcurrency) ? Math.max(1, Math.min(32, Math.floor(requestedConcurrency))) : 8;
|
|
5136
5142
|
while (frontier.length > 0) {
|
|
5137
5143
|
const current = frontier.filter((item) => beef.findTxid(item.txid) == null);
|
|
5138
|
-
const resolved = await mapWithConcurrency(current, concurrency, async (item) => await resolveBeefForTransaction(storage, item.txid, options,
|
|
5144
|
+
const resolved = await mapWithConcurrency(current, concurrency, async (item) => await resolveBeefForTransaction(storage, item.txid, options, hasKnownTxid, item.depth));
|
|
5139
5145
|
const next = [];
|
|
5140
5146
|
for (let i = 0; i < resolved.length; i++) {
|
|
5141
5147
|
const result = resolved[i];
|
|
@@ -5152,6 +5158,19 @@ async function getBeefForTransaction(storage, txid, options) {
|
|
|
5152
5158
|
}
|
|
5153
5159
|
return beef;
|
|
5154
5160
|
}
|
|
5161
|
+
function makeKnownTxidLookup$1(knownTxids) {
|
|
5162
|
+
let lookups = 0;
|
|
5163
|
+
let indexed;
|
|
5164
|
+
return (txid) => {
|
|
5165
|
+
lookups++;
|
|
5166
|
+
if (indexed != null) return indexed.has(txid);
|
|
5167
|
+
if (knownTxids.length > 64 && lookups > 4) {
|
|
5168
|
+
indexed = new Set(knownTxids);
|
|
5169
|
+
return indexed.has(txid);
|
|
5170
|
+
}
|
|
5171
|
+
return knownTxids.includes(txid);
|
|
5172
|
+
};
|
|
5173
|
+
}
|
|
5155
5174
|
async function mapWithConcurrency(values, concurrency, mapper) {
|
|
5156
5175
|
const results = Array.from({ length: values.length }, () => void 0);
|
|
5157
5176
|
let cursor = 0;
|
|
@@ -5195,11 +5214,11 @@ async function mergeUsableProvenTransaction(beef, txid, result, options, recursi
|
|
|
5195
5214
|
beef.mergeBump(merklePath);
|
|
5196
5215
|
return beef;
|
|
5197
5216
|
}
|
|
5198
|
-
async function resolveBeefForTransaction(storage, txid, options,
|
|
5217
|
+
async function resolveBeefForTransaction(storage, txid, options, hasKnownTxid, recursionDepth) {
|
|
5199
5218
|
const maxDepth = storage.maxRecursionDepth;
|
|
5200
5219
|
if (maxDepth && maxDepth <= recursionDepth) throw new WERR_INVALID_OPERATION(`Maximum BEEF depth exceeded. Limit is ${storage.maxRecursionDepth}`);
|
|
5201
5220
|
const beef = new _bsv_sdk.Beef();
|
|
5202
|
-
if (
|
|
5221
|
+
if (hasKnownTxid(txid)) {
|
|
5203
5222
|
beef.mergeTxidOnly(txid);
|
|
5204
5223
|
return {
|
|
5205
5224
|
beef,
|
|
@@ -6800,7 +6819,7 @@ function getResultBeef(result) {
|
|
|
6800
6819
|
//#endregion
|
|
6801
6820
|
//#region ../src/signer/methods/createAction.ts
|
|
6802
6821
|
async function createAction$1(wallet, auth, vargs) {
|
|
6803
|
-
if (!wallet.telemetry.enabled) return await createActionCore(wallet, auth, vargs);
|
|
6822
|
+
if (!wallet.telemetry.enabled) return await createActionCore$1(wallet, auth, vargs);
|
|
6804
6823
|
return await wallet.telemetry.withSpan("wallet.create_action", {
|
|
6805
6824
|
component: "wallet-toolbox",
|
|
6806
6825
|
carrier: vargs,
|
|
@@ -6811,7 +6830,7 @@ async function createAction$1(wallet, auth, vargs) {
|
|
|
6811
6830
|
"action.is_sign_action": vargs.isSignAction
|
|
6812
6831
|
}
|
|
6813
6832
|
}, async (span) => {
|
|
6814
|
-
const result = await createActionCore(wallet, auth, vargs, span);
|
|
6833
|
+
const result = await createActionCore$1(wallet, auth, vargs, span);
|
|
6815
6834
|
span.end({ attributes: {
|
|
6816
6835
|
"action.has_transaction": result.tx != null,
|
|
6817
6836
|
"action.has_signable_transaction": result.signableTransaction != null,
|
|
@@ -6820,7 +6839,7 @@ async function createAction$1(wallet, auth, vargs) {
|
|
|
6820
6839
|
return result;
|
|
6821
6840
|
});
|
|
6822
6841
|
}
|
|
6823
|
-
async function createActionCore(wallet, auth, vargs, parent) {
|
|
6842
|
+
async function createActionCore$1(wallet, auth, vargs, parent) {
|
|
6824
6843
|
const r = {};
|
|
6825
6844
|
const logger = vargs.logger;
|
|
6826
6845
|
let prior;
|
|
@@ -9104,7 +9123,16 @@ var Wallet = class {
|
|
|
9104
9123
|
_bsv_sdk.Validation.validateOriginator(originator);
|
|
9105
9124
|
args.options ??= {};
|
|
9106
9125
|
args.options.trustSelf ||= this.trustSelf;
|
|
9107
|
-
if (this.autoKnownTxids && args.options.knownTxids == null) args.options.knownTxids = this.
|
|
9126
|
+
if (this.autoKnownTxids && args.options.knownTxids == null) if (this.telemetry.enabled) args.options.knownTxids = this.telemetry.withSpan("wallet.create_action.prepare_known_txids", {
|
|
9127
|
+
component: "wallet-toolbox",
|
|
9128
|
+
carrier: args,
|
|
9129
|
+
attributes: { "beef.tx_count": this.beef.txs.length }
|
|
9130
|
+
}, (span) => {
|
|
9131
|
+
const knownTxids = this.getKnownTxids(args.options?.knownTxids);
|
|
9132
|
+
span.end({ attributes: { "beef.known_txid_count": knownTxids.length } });
|
|
9133
|
+
return knownTxids;
|
|
9134
|
+
});
|
|
9135
|
+
else args.options.knownTxids = this.getKnownTxids(args.options.knownTxids);
|
|
9108
9136
|
const { auth, vargs } = this.validateAuthAndArgs(args, _bsv_sdk.Validation.validateCreateActionArgs, logger);
|
|
9109
9137
|
logger?.log("validated args");
|
|
9110
9138
|
vargs.includeAllSourceTransactions = this.includeAllSourceTransactions;
|
|
@@ -9423,6 +9451,28 @@ function isAutoSpendableChangeOutput(output) {
|
|
|
9423
9451
|
return isManagedChangeOutput(output) && output.spendable && output.spentBy == null;
|
|
9424
9452
|
}
|
|
9425
9453
|
async function createAction(storage, auth, vargs, _originator) {
|
|
9454
|
+
if (!storage.telemetry.enabled) return await createActionCore(storage, auth, vargs);
|
|
9455
|
+
return await storage.telemetry.withSpan("wallet.storage.create_action", {
|
|
9456
|
+
component: "wallet-storage",
|
|
9457
|
+
carrier: vargs,
|
|
9458
|
+
attributes: {
|
|
9459
|
+
"action.fixed_input_count": vargs.inputs.length,
|
|
9460
|
+
"action.fixed_output_count": vargs.outputs.length,
|
|
9461
|
+
"action.known_txid_count": vargs.options.knownTxids?.length ?? 0,
|
|
9462
|
+
"action.is_delayed": vargs.isDelayed,
|
|
9463
|
+
"action.is_no_send": vargs.isNoSend
|
|
9464
|
+
}
|
|
9465
|
+
}, async (span) => {
|
|
9466
|
+
const result = await createActionCore(storage, auth, vargs, span);
|
|
9467
|
+
span.end({ attributes: {
|
|
9468
|
+
"action.result_input_count": result.inputs.length,
|
|
9469
|
+
"action.result_output_count": result.outputs.length,
|
|
9470
|
+
"action.input_beef_bytes": result.inputBeef?.length ?? 0
|
|
9471
|
+
} });
|
|
9472
|
+
return result;
|
|
9473
|
+
});
|
|
9474
|
+
}
|
|
9475
|
+
async function createActionCore(storage, auth, vargs, parent) {
|
|
9426
9476
|
const logger = vargs.logger;
|
|
9427
9477
|
logger?.group("storage createAction");
|
|
9428
9478
|
if (vargs.isTestWerrReviewActions) throwDummyReviewActions();
|
|
@@ -9442,38 +9492,69 @@ async function createAction(storage, auth, vargs, _originator) {
|
|
|
9442
9492
|
* - Create and return result.
|
|
9443
9493
|
*/
|
|
9444
9494
|
const userId = auth.userId;
|
|
9445
|
-
const { storageBeef, beef, xinputs } = await
|
|
9446
|
-
|
|
9447
|
-
|
|
9448
|
-
|
|
9449
|
-
|
|
9450
|
-
|
|
9451
|
-
userId,
|
|
9452
|
-
|
|
9453
|
-
|
|
9454
|
-
|
|
9455
|
-
|
|
9456
|
-
|
|
9457
|
-
|
|
9458
|
-
|
|
9495
|
+
const { storageBeef, beef, xinputs, xoutputs, changeBasket, noSendChangeIn } = await traceStorageStep(storage, "wallet.storage.create_action.validate", parent, {
|
|
9496
|
+
"action.fixed_input_count": vargs.inputs.length,
|
|
9497
|
+
"action.fixed_output_count": vargs.outputs.length
|
|
9498
|
+
}, async (span) => {
|
|
9499
|
+
const requiredInputs = await validateRequiredInputs(storage, userId, vargs);
|
|
9500
|
+
logger?.log("validated required inputs");
|
|
9501
|
+
const xoutputs = validateRequiredOutputs(storage, userId, vargs);
|
|
9502
|
+
logger?.log("validated required outputs");
|
|
9503
|
+
const changeBasketName = "default";
|
|
9504
|
+
const changeBasket = verifyOne(await storage.findOutputBaskets({ partial: {
|
|
9505
|
+
userId,
|
|
9506
|
+
name: changeBasketName
|
|
9507
|
+
} }), `Invalid outputGeneration basket "${changeBasketName}"`);
|
|
9508
|
+
logger?.log("found change basket");
|
|
9509
|
+
const noSendChangeIn = await validateNoSendChange(storage, userId, vargs, changeBasket);
|
|
9510
|
+
logger?.log("validated noSendChange");
|
|
9511
|
+
span?.end({ attributes: {
|
|
9512
|
+
"action.validated_input_count": requiredInputs.xinputs.length,
|
|
9513
|
+
"action.validated_output_count": xoutputs.length,
|
|
9514
|
+
"action.no_send_change_input_count": noSendChangeIn.length,
|
|
9515
|
+
"action.validated_beef_tx_count": requiredInputs.beef.txs.length
|
|
9516
|
+
} });
|
|
9517
|
+
return {
|
|
9518
|
+
...requiredInputs,
|
|
9519
|
+
xoutputs,
|
|
9520
|
+
changeBasket,
|
|
9521
|
+
noSendChangeIn
|
|
9522
|
+
};
|
|
9523
|
+
});
|
|
9459
9524
|
const feeModel = validateStorageFeeModel(storage.feeModel);
|
|
9460
9525
|
logger?.log(`validated fee model ${JSON.stringify(feeModel)}`);
|
|
9461
|
-
|
|
9462
|
-
|
|
9526
|
+
const initialFundingPlan = await prepareFundingPlan(storage, {
|
|
9527
|
+
userId,
|
|
9528
|
+
vargs,
|
|
9529
|
+
xinputs,
|
|
9530
|
+
xoutputs,
|
|
9531
|
+
changeBasket,
|
|
9532
|
+
noSendChangeIn,
|
|
9533
|
+
feeModel,
|
|
9534
|
+
parent
|
|
9535
|
+
});
|
|
9536
|
+
logger?.log(`planned funding from ${initialFundingPlan.availableChangeCount} change inputs`);
|
|
9463
9537
|
let newTx;
|
|
9464
9538
|
try {
|
|
9465
|
-
|
|
9539
|
+
const storageBeefBytes = storageBeef.toBinary();
|
|
9540
|
+
newTx = await traceStorageStep(storage, "wallet.storage.create_action.create_record", parent, {
|
|
9541
|
+
"action.label_count": vargs.labels.length,
|
|
9542
|
+
"action.storage_beef_bytes": storageBeefBytes.length
|
|
9543
|
+
}, async (span) => {
|
|
9544
|
+
const transaction = await createNewTxRecord(storage, userId, vargs, storageBeefBytes);
|
|
9545
|
+
span?.end({ attributes: { "action.transaction_record_created": true } });
|
|
9546
|
+
return transaction;
|
|
9547
|
+
});
|
|
9466
9548
|
logger?.log("created new transaction record");
|
|
9467
9549
|
const ctx = {
|
|
9468
9550
|
xinputs,
|
|
9469
9551
|
xoutputs,
|
|
9470
9552
|
changeBasket,
|
|
9471
9553
|
noSendChangeIn,
|
|
9472
|
-
availableChangeCount,
|
|
9473
9554
|
feeModel,
|
|
9474
9555
|
transactionId: newTx.transactionId
|
|
9475
9556
|
};
|
|
9476
|
-
const { allocatedChange, changeOutputs, derivationPrefix, maxPossibleSatoshisAdjustment } = await fundNewTransactionSdk(storage, userId, vargs, ctx);
|
|
9557
|
+
const { allocatedChange, changeOutputs, derivationPrefix, maxPossibleSatoshisAdjustment } = await fundNewTransactionSdk(storage, userId, vargs, ctx, initialFundingPlan, parent);
|
|
9477
9558
|
logger?.log("funded new transaction");
|
|
9478
9559
|
if (maxPossibleSatoshisAdjustment != null) {
|
|
9479
9560
|
const a = maxPossibleSatoshisAdjustment;
|
|
@@ -9482,12 +9563,27 @@ async function createAction(storage, auth, vargs, _originator) {
|
|
|
9482
9563
|
logger?.log("adjusted change outputs to max possible");
|
|
9483
9564
|
}
|
|
9484
9565
|
const satoshis = changeOutputs.reduce((a, e) => a + e.satoshis, 0) - allocatedChange.reduce((a, e) => a + e.satoshis, 0);
|
|
9485
|
-
await storage.
|
|
9486
|
-
|
|
9566
|
+
const { outputs, changeVouts } = await traceStorageStep(storage, "wallet.storage.create_action.persist_outputs", parent, {
|
|
9567
|
+
"action.fixed_output_count": ctx.xoutputs.length,
|
|
9568
|
+
"action.change_output_count": changeOutputs.length
|
|
9569
|
+
}, async (span) => {
|
|
9570
|
+
await storage.updateTransaction(newTx.transactionId, { satoshis });
|
|
9571
|
+
const persisted = await createNewOutputs(storage, userId, vargs, ctx, changeOutputs);
|
|
9572
|
+
span?.end({ attributes: { "action.persisted_output_count": persisted.outputs.length } });
|
|
9573
|
+
return persisted;
|
|
9574
|
+
});
|
|
9487
9575
|
logger?.log("created new output records");
|
|
9488
|
-
const inputBeef = await mergeAllocatedChangeBeefs(storage,
|
|
9576
|
+
const inputBeef = await mergeAllocatedChangeBeefs(storage, vargs, allocatedChange, beef, parent);
|
|
9489
9577
|
logger?.log("merged allocated change beefs");
|
|
9490
|
-
const inputs = await
|
|
9578
|
+
const inputs = await traceStorageStep(storage, "wallet.storage.create_action.assemble_inputs", parent, {
|
|
9579
|
+
"action.fixed_input_count": ctx.xinputs.length,
|
|
9580
|
+
"action.funding_input_count": allocatedChange.length,
|
|
9581
|
+
"action.include_source_transactions": vargs.includeAllSourceTransactions
|
|
9582
|
+
}, async (span) => {
|
|
9583
|
+
const assembled = await createNewInputs(storage, userId, vargs, ctx, allocatedChange);
|
|
9584
|
+
span?.end({ attributes: { "action.result_input_count": assembled.length } });
|
|
9585
|
+
return assembled;
|
|
9586
|
+
});
|
|
9491
9587
|
logger?.log("created new inputs");
|
|
9492
9588
|
const r = {
|
|
9493
9589
|
reference: newTx.reference,
|
|
@@ -9766,14 +9862,16 @@ async function createNewTxRecord(storage, userId, vargs, storageBeef) {
|
|
|
9766
9862
|
satoshis: 0,
|
|
9767
9863
|
userId,
|
|
9768
9864
|
isOutgoing: true,
|
|
9769
|
-
inputBEEF: storageBeef
|
|
9865
|
+
inputBEEF: storageBeef,
|
|
9770
9866
|
description: vargs.description,
|
|
9771
9867
|
txid: void 0,
|
|
9772
9868
|
rawTx: void 0
|
|
9773
9869
|
};
|
|
9774
9870
|
newTx.transactionId = await storage.insertTransaction(newTx);
|
|
9775
|
-
|
|
9776
|
-
|
|
9871
|
+
const labelNames = [...new Set(vargs.labels)];
|
|
9872
|
+
const labels = await storage.findOrInsertTxLabelsBulk(userId, labelNames);
|
|
9873
|
+
for (const label of labelNames) {
|
|
9874
|
+
const txLabel = labels[label];
|
|
9777
9875
|
await storage.findOrInsertTxLabelMap(verifyId(newTx.transactionId), verifyId(txLabel.txLabelId));
|
|
9778
9876
|
}
|
|
9779
9877
|
return newTx;
|
|
@@ -9958,87 +10056,222 @@ async function validateNoSendChange(storage, userId, vargs, changeBasket) {
|
|
|
9958
10056
|
const r = [];
|
|
9959
10057
|
if (!vargs.isNoSend) return [];
|
|
9960
10058
|
const noSendChange = vargs.options.noSendChange;
|
|
9961
|
-
if (noSendChange && noSendChange.length > 0)
|
|
9962
|
-
const
|
|
9963
|
-
|
|
9964
|
-
|
|
9965
|
-
|
|
9966
|
-
|
|
9967
|
-
|
|
9968
|
-
|
|
9969
|
-
r.push(output);
|
|
10059
|
+
if (noSendChange && noSendChange.length > 0) {
|
|
10060
|
+
const byOutpoint = await storage.findOutputsByOutpoints(userId, noSendChange);
|
|
10061
|
+
for (const op of noSendChange) {
|
|
10062
|
+
const output = byOutpoint[`${op.txid}.${op.vout}`];
|
|
10063
|
+
if (!isAutoSpendableChangeOutput(output) || !verifyNumber(output.satoshis) || output.basketId !== changeBasket.basketId) throw new WERR_INVALID_PARAMETER("noSendChange outpoint", "wallet-managed BRC-29 change");
|
|
10064
|
+
if (r.some((o) => o.outputId === output.outputId)) throw new WERR_INVALID_PARAMETER("noSendChange outpoint", "unique. Duplicates are not allowed.");
|
|
10065
|
+
r.push(output);
|
|
10066
|
+
}
|
|
9970
10067
|
}
|
|
9971
10068
|
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");
|
|
9972
10069
|
return r;
|
|
9973
10070
|
}
|
|
9974
|
-
|
|
9975
|
-
|
|
9976
|
-
|
|
9977
|
-
|
|
9978
|
-
|
|
9979
|
-
|
|
9980
|
-
|
|
9981
|
-
|
|
9982
|
-
if (
|
|
9983
|
-
|
|
9984
|
-
|
|
9985
|
-
|
|
9986
|
-
|
|
9987
|
-
|
|
9988
|
-
|
|
9989
|
-
|
|
9990
|
-
|
|
10071
|
+
var FundingClaimConflict = class extends Error {
|
|
10072
|
+
conflict;
|
|
10073
|
+
constructor(conflict) {
|
|
10074
|
+
super("createAction funding claim changed concurrently");
|
|
10075
|
+
this.conflict = conflict;
|
|
10076
|
+
}
|
|
10077
|
+
};
|
|
10078
|
+
async function traceStorageStep(storage, name, parent, attributes, callback) {
|
|
10079
|
+
if (!storage.telemetry.enabled) return await callback();
|
|
10080
|
+
return await storage.telemetry.withSpan(name, {
|
|
10081
|
+
component: "wallet-storage",
|
|
10082
|
+
parent: parent?.context,
|
|
10083
|
+
attributes
|
|
10084
|
+
}, async (span) => await callback(span));
|
|
10085
|
+
}
|
|
10086
|
+
function makeFundingParams(vargs, xinputs, xoutputs, changeBasket, feeModel, availableChangeCount) {
|
|
10087
|
+
return {
|
|
10088
|
+
fixedInputs: xinputs.map((input) => ({
|
|
10089
|
+
satoshis: input.satoshis,
|
|
10090
|
+
unlockingScriptLength: input.unlockingScriptLength
|
|
9991
10091
|
})),
|
|
9992
|
-
fixedOutputs:
|
|
9993
|
-
satoshis:
|
|
9994
|
-
lockingScriptLength:
|
|
10092
|
+
fixedOutputs: xoutputs.map((output) => ({
|
|
10093
|
+
satoshis: output.satoshis,
|
|
10094
|
+
lockingScriptLength: output.lockingScript.length / 2
|
|
9995
10095
|
})),
|
|
9996
|
-
feeModel
|
|
9997
|
-
changeInitialSatoshis: Math.max(1,
|
|
9998
|
-
changeFirstSatoshis: Math.max(1, Math.round(
|
|
10096
|
+
feeModel,
|
|
10097
|
+
changeInitialSatoshis: Math.max(1, changeBasket.minimumDesiredUTXOValue),
|
|
10098
|
+
changeFirstSatoshis: Math.max(1, Math.round(changeBasket.minimumDesiredUTXOValue / 4)),
|
|
9999
10099
|
changeLockingScriptLength: 25,
|
|
10000
10100
|
changeUnlockingScriptLength: 107,
|
|
10001
|
-
targetNetCount:
|
|
10101
|
+
targetNetCount: changeBasket.numberOfDesiredUTXOs - availableChangeCount,
|
|
10002
10102
|
randomVals: vargs.randomVals
|
|
10003
10103
|
};
|
|
10004
|
-
|
|
10005
|
-
|
|
10006
|
-
const
|
|
10007
|
-
|
|
10008
|
-
|
|
10009
|
-
|
|
10010
|
-
|
|
10011
|
-
|
|
10012
|
-
|
|
10013
|
-
|
|
10014
|
-
|
|
10015
|
-
|
|
10104
|
+
}
|
|
10105
|
+
async function prepareFundingPlan(storage, context) {
|
|
10106
|
+
const { userId, vargs, xinputs, xoutputs, changeBasket, noSendChangeIn, feeModel, parent } = context;
|
|
10107
|
+
const excludeSending = !vargs.isDelayed;
|
|
10108
|
+
const candidates = await traceStorageStep(storage, "wallet.storage.create_action.funding_candidates", parent, { "funding.exclude_sending": excludeSending }, async (span) => {
|
|
10109
|
+
const outputs = await storage.findAvailableManagedChangeInputs(userId, changeBasket.basketId, excludeSending);
|
|
10110
|
+
span?.end({ attributes: {
|
|
10111
|
+
"funding.candidate_count": outputs.length,
|
|
10112
|
+
"funding.candidate_satoshis": outputs.reduce((sum, output) => sum + output.satoshis, 0)
|
|
10113
|
+
} });
|
|
10114
|
+
return outputs;
|
|
10115
|
+
});
|
|
10116
|
+
const noSendIds = new Set(noSendChangeIn.map((output) => output.outputId));
|
|
10117
|
+
const available = candidates.filter((output) => !noSendIds.has(output.outputId));
|
|
10118
|
+
const params = makeFundingParams(vargs, xinputs, xoutputs, changeBasket, feeModel, candidates.length);
|
|
10119
|
+
return await traceStorageStep(storage, "wallet.storage.create_action.funding_plan", parent, {
|
|
10120
|
+
"funding.candidate_count": available.length,
|
|
10121
|
+
"funding.no_send_change_count": noSendChangeIn.length
|
|
10122
|
+
}, async (span) => {
|
|
10123
|
+
const allocated = /* @__PURE__ */ new Map();
|
|
10124
|
+
const noSend = [...noSendChangeIn];
|
|
10125
|
+
const allocate = async (targetSatoshis, exactSatoshis) => {
|
|
10126
|
+
let output = noSend.pop();
|
|
10127
|
+
output ??= selectCanonicalChange(available.filter((candidate) => !allocated.has(candidate.outputId)), targetSatoshis, exactSatoshis);
|
|
10128
|
+
if (output == null) return void 0;
|
|
10129
|
+
allocated.set(output.outputId, output);
|
|
10130
|
+
return {
|
|
10131
|
+
outputId: output.outputId,
|
|
10132
|
+
satoshis: output.satoshis
|
|
10133
|
+
};
|
|
10134
|
+
};
|
|
10135
|
+
const release = async (outputId) => {
|
|
10136
|
+
const output = allocated.get(outputId);
|
|
10137
|
+
if (output == null) return;
|
|
10138
|
+
allocated.delete(outputId);
|
|
10139
|
+
if (noSendIds.has(outputId)) noSend.push(output);
|
|
10140
|
+
};
|
|
10141
|
+
const result = await generateChangeSdk(params, allocate, release, vargs.logger, storage.telemetry);
|
|
10142
|
+
const selected = result.allocatedChangeInputs.map((input) => verifyTruthy(allocated.get(input.outputId)));
|
|
10143
|
+
span?.end({ attributes: {
|
|
10144
|
+
"funding.allocated_input_count": selected.length,
|
|
10145
|
+
"funding.change_output_count": result.changeOutputs.length,
|
|
10146
|
+
"funding.fee_satoshis": result.fee,
|
|
10147
|
+
"funding.transaction_size_bytes": result.size
|
|
10148
|
+
} });
|
|
10149
|
+
return {
|
|
10150
|
+
params,
|
|
10151
|
+
result,
|
|
10152
|
+
selected,
|
|
10153
|
+
availableChangeCount: candidates.length
|
|
10154
|
+
};
|
|
10155
|
+
});
|
|
10156
|
+
}
|
|
10157
|
+
async function claimFundingPlan(storage, userId, basketId, excludeSending, transactionId, noSendChangeIn, plan) {
|
|
10158
|
+
if (plan.selected.length === 0) return {
|
|
10159
|
+
outputs: [],
|
|
10160
|
+
sourceTransactionCount: 0,
|
|
10161
|
+
hydratedScriptCount: 0,
|
|
10162
|
+
scriptSourceTransactionCount: 0
|
|
10163
|
+
};
|
|
10164
|
+
const noSendIds = new Set(noSendChangeIn.map((output) => output.outputId));
|
|
10165
|
+
const statuses = ["completed", "unproven"];
|
|
10166
|
+
if (!excludeSending) statuses.push("sending");
|
|
10167
|
+
const claim = await storage.transaction(async (trx) => {
|
|
10168
|
+
const outpoints = plan.selected.map((output) => {
|
|
10169
|
+
if (output.txid == null) throw new WERR_INTERNAL("planned change input is missing txid");
|
|
10016
10170
|
return {
|
|
10017
|
-
|
|
10018
|
-
|
|
10171
|
+
txid: output.txid,
|
|
10172
|
+
vout: output.vout
|
|
10019
10173
|
};
|
|
10174
|
+
});
|
|
10175
|
+
const currentByOutpoint = await storage.findOutputsByOutpointsForUpdate(userId, outpoints, trx, true);
|
|
10176
|
+
const reserved = new Set(await storage.findReservedActionBatchOutputIds(plan.selected.map((output) => output.outputId), trx));
|
|
10177
|
+
const transactionIds = [...new Set(Object.values(currentByOutpoint).map((output) => output.transactionId))];
|
|
10178
|
+
const transactionStatuses = await storage.findTransactionStatusesByIds(userId, transactionIds, trx);
|
|
10179
|
+
const claimed = [];
|
|
10180
|
+
for (const planned of plan.selected) {
|
|
10181
|
+
const current = currentByOutpoint[`${String(planned.txid)}.${planned.vout}`];
|
|
10182
|
+
const currentStatus = current == null ? void 0 : transactionStatuses.get(current.transactionId);
|
|
10183
|
+
const validTransaction = currentStatus != null && statuses.includes(currentStatus);
|
|
10184
|
+
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" };
|
|
10185
|
+
claimed.push(current);
|
|
10186
|
+
}
|
|
10187
|
+
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");
|
|
10188
|
+
for (const output of claimed) {
|
|
10189
|
+
output.spendable = false;
|
|
10190
|
+
output.spentBy = transactionId;
|
|
10020
10191
|
}
|
|
10021
|
-
const basketId = ctx.changeBasket.basketId;
|
|
10022
|
-
const o = await storage.allocateChangeInput(userId, basketId, targetSatoshis, exactSatoshis, !vargs.isDelayed, ctx.transactionId);
|
|
10023
|
-
if (o == null) return void 0;
|
|
10024
|
-
outputs[o.outputId] = o;
|
|
10025
10192
|
return {
|
|
10026
|
-
|
|
10027
|
-
|
|
10193
|
+
outputs: claimed,
|
|
10194
|
+
sourceTransactionCount: transactionIds.length
|
|
10028
10195
|
};
|
|
10196
|
+
}).catch((error) => {
|
|
10197
|
+
if (error instanceof FundingClaimConflict) return { conflict: error.conflict };
|
|
10198
|
+
throw error;
|
|
10199
|
+
});
|
|
10200
|
+
if (claim.outputs == null) return claim;
|
|
10201
|
+
const hydration = await hydrateFundingInputScripts(storage, claim.outputs);
|
|
10202
|
+
return {
|
|
10203
|
+
outputs: claim.outputs,
|
|
10204
|
+
sourceTransactionCount: claim.sourceTransactionCount,
|
|
10205
|
+
...hydration
|
|
10029
10206
|
};
|
|
10030
|
-
|
|
10031
|
-
|
|
10032
|
-
|
|
10033
|
-
|
|
10034
|
-
|
|
10207
|
+
}
|
|
10208
|
+
async function hydrateFundingInputScripts(storage, outputs) {
|
|
10209
|
+
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 !== "");
|
|
10210
|
+
if (missing.length === 0) return {
|
|
10211
|
+
hydratedScriptCount: 0,
|
|
10212
|
+
scriptSourceTransactionCount: 0
|
|
10213
|
+
};
|
|
10214
|
+
const byTxid = /* @__PURE__ */ new Map();
|
|
10215
|
+
for (const output of missing) {
|
|
10216
|
+
const txid = verifyTruthy(output.txid);
|
|
10217
|
+
const group = byTxid.get(txid) ?? [];
|
|
10218
|
+
group.push(output);
|
|
10219
|
+
byTxid.set(txid, group);
|
|
10220
|
+
}
|
|
10221
|
+
const groups = [...byTxid.entries()];
|
|
10222
|
+
let cursor = 0;
|
|
10223
|
+
await Promise.all(Array.from({ length: Math.min(8, groups.length) }, async () => {
|
|
10224
|
+
while (cursor < groups.length) {
|
|
10225
|
+
const [txid, group] = groups[cursor++];
|
|
10226
|
+
if (group.length === 1) {
|
|
10227
|
+
await storage.validateOutputScript(group[0]);
|
|
10228
|
+
continue;
|
|
10229
|
+
}
|
|
10230
|
+
const rawTx = await storage.getRawTxOfKnownValidTransaction(txid);
|
|
10231
|
+
if (rawTx != null) for (const output of group) output.lockingScript = rawTx.slice(output.scriptOffset, output.scriptOffset + output.scriptLength);
|
|
10232
|
+
else for (const output of group) await storage.validateOutputScript(output);
|
|
10035
10233
|
}
|
|
10036
|
-
|
|
10037
|
-
|
|
10038
|
-
|
|
10039
|
-
|
|
10234
|
+
}));
|
|
10235
|
+
return {
|
|
10236
|
+
hydratedScriptCount: missing.filter((output) => output.lockingScript?.length === output.scriptLength).length,
|
|
10237
|
+
scriptSourceTransactionCount: groups.length
|
|
10040
10238
|
};
|
|
10041
|
-
|
|
10239
|
+
}
|
|
10240
|
+
async function fundNewTransactionSdk(storage, userId, vargs, ctx, initialPlan, parent) {
|
|
10241
|
+
let plan = initialPlan;
|
|
10242
|
+
let allocatedChange;
|
|
10243
|
+
let retryCount = 0;
|
|
10244
|
+
await traceStorageStep(storage, "wallet.storage.create_action.funding_claim", parent, { "funding.planned_input_count": initialPlan.selected.length }, async (span) => {
|
|
10245
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
10246
|
+
const claim = await claimFundingPlan(storage, userId, ctx.changeBasket.basketId, !vargs.isDelayed, ctx.transactionId, ctx.noSendChangeIn, plan);
|
|
10247
|
+
if (claim.outputs != null) {
|
|
10248
|
+
allocatedChange = claim.outputs;
|
|
10249
|
+
span?.end({ attributes: {
|
|
10250
|
+
"funding.claim_retry_count": retryCount,
|
|
10251
|
+
"funding.source_transaction_count": claim.sourceTransactionCount,
|
|
10252
|
+
"funding.hydrated_script_count": claim.hydratedScriptCount,
|
|
10253
|
+
"funding.script_source_transaction_count": claim.scriptSourceTransactionCount
|
|
10254
|
+
} });
|
|
10255
|
+
return;
|
|
10256
|
+
}
|
|
10257
|
+
if (claim.conflict === "noSendChange") throw new WERR_INVALID_PARAMETER("noSendChange", "outputs that remain spendable during action planning");
|
|
10258
|
+
retryCount++;
|
|
10259
|
+
plan = await prepareFundingPlan(storage, {
|
|
10260
|
+
userId,
|
|
10261
|
+
vargs,
|
|
10262
|
+
xinputs: ctx.xinputs,
|
|
10263
|
+
xoutputs: ctx.xoutputs,
|
|
10264
|
+
changeBasket: ctx.changeBasket,
|
|
10265
|
+
noSendChangeIn: ctx.noSendChangeIn,
|
|
10266
|
+
feeModel: ctx.feeModel,
|
|
10267
|
+
parent
|
|
10268
|
+
});
|
|
10269
|
+
}
|
|
10270
|
+
throw new WERR_INVALID_OPERATION("wallet funding changed repeatedly during action planning; retry createAction");
|
|
10271
|
+
});
|
|
10272
|
+
if (allocatedChange == null) throw new WERR_INTERNAL("funding plan was not claimed");
|
|
10273
|
+
const params = plan.params;
|
|
10274
|
+
const gcr = plan.result;
|
|
10042
10275
|
const nextRandomVal = () => {
|
|
10043
10276
|
let val = 0;
|
|
10044
10277
|
if (vargs.randomVals == null || vargs.randomVals.length === 0) {
|
|
@@ -10066,7 +10299,7 @@ async function fundNewTransactionSdk(storage, userId, vargs, ctx) {
|
|
|
10066
10299
|
const derivationPrefix = randomDerivation(16);
|
|
10067
10300
|
return {
|
|
10068
10301
|
maxPossibleSatoshisAdjustment: gcr.maxPossibleSatoshisAdjustment,
|
|
10069
|
-
allocatedChange
|
|
10302
|
+
allocatedChange,
|
|
10070
10303
|
changeOutputs: gcr.changeOutputs.map((o, i) => ({
|
|
10071
10304
|
created_at: /* @__PURE__ */ new Date(),
|
|
10072
10305
|
updated_at: /* @__PURE__ */ new Date(),
|
|
@@ -10101,12 +10334,24 @@ async function fundNewTransactionSdk(storage, userId, vargs, ctx) {
|
|
|
10101
10334
|
*/
|
|
10102
10335
|
function trimInputBeef(beef, vargs) {
|
|
10103
10336
|
if (vargs.options.returnTXIDOnly) return void 0;
|
|
10104
|
-
const
|
|
10105
|
-
for (const
|
|
10106
|
-
for (const txid of beef.txs.map((btx) => btx.txid)) if (knownTxids[txid]) beef.makeTxidOnly(txid);
|
|
10337
|
+
const hasKnownTxid = makeKnownTxidLookup(vargs.options.knownTxids ?? []);
|
|
10338
|
+
for (const btx of beef.txs) if (hasKnownTxid(btx.txid)) beef.makeTxidOnly(btx.txid);
|
|
10107
10339
|
return beef.toUint8Array();
|
|
10108
10340
|
}
|
|
10109
|
-
|
|
10341
|
+
function makeKnownTxidLookup(knownTxids) {
|
|
10342
|
+
let lookups = 0;
|
|
10343
|
+
let indexed;
|
|
10344
|
+
return (txid) => {
|
|
10345
|
+
lookups++;
|
|
10346
|
+
if (indexed != null) return indexed.has(txid);
|
|
10347
|
+
if (knownTxids.length > 64 && lookups > 4) {
|
|
10348
|
+
indexed = new Set(knownTxids);
|
|
10349
|
+
return indexed.has(txid);
|
|
10350
|
+
}
|
|
10351
|
+
return knownTxids.includes(txid);
|
|
10352
|
+
};
|
|
10353
|
+
}
|
|
10354
|
+
async function mergeAllocatedChangeBeefs(storage, vargs, allocatedChange, beef, parent) {
|
|
10110
10355
|
const options = {
|
|
10111
10356
|
trustSelf: void 0,
|
|
10112
10357
|
knownTxids: vargs.options.knownTxids,
|
|
@@ -10117,25 +10362,52 @@ async function mergeAllocatedChangeBeefs(storage, userId, vargs, allocatedChange
|
|
|
10117
10362
|
minProofLevel: void 0
|
|
10118
10363
|
};
|
|
10119
10364
|
if (vargs.options.returnTXIDOnly) return void 0;
|
|
10120
|
-
const
|
|
10121
|
-
const
|
|
10365
|
+
const knownTxids = vargs.options.knownTxids ?? [];
|
|
10366
|
+
const hasKnownTxid = makeKnownTxidLookup(knownTxids);
|
|
10367
|
+
const missing = Array.from(new Set(allocatedChange.map((output) => verifyTruthy(output.txid)).filter((txid) => beef.findTxid(txid) == null && !hasKnownTxid(txid))));
|
|
10122
10368
|
const fetched = Array.from({ length: missing.length });
|
|
10123
10369
|
const concurrency = Math.min(8, Math.max(1, missing.length));
|
|
10124
10370
|
let cursor = 0;
|
|
10125
|
-
await
|
|
10126
|
-
|
|
10127
|
-
|
|
10128
|
-
|
|
10129
|
-
|
|
10130
|
-
|
|
10131
|
-
|
|
10132
|
-
}
|
|
10133
|
-
|
|
10134
|
-
|
|
10135
|
-
|
|
10136
|
-
|
|
10137
|
-
|
|
10138
|
-
|
|
10371
|
+
await traceStorageStep(storage, "wallet.storage.create_action.beef_fetch", parent, {
|
|
10372
|
+
"beef.allocated_change_count": allocatedChange.length,
|
|
10373
|
+
"beef.distinct_source_count": new Set(allocatedChange.map((output) => output.txid)).size,
|
|
10374
|
+
"beef.known_txid_count": knownTxids.length,
|
|
10375
|
+
"beef.missing_source_count": missing.length,
|
|
10376
|
+
"beef.fetch_concurrency": concurrency
|
|
10377
|
+
}, async (span) => {
|
|
10378
|
+
await Promise.all(Array.from({ length: concurrency }, async () => {
|
|
10379
|
+
while (cursor < missing.length) {
|
|
10380
|
+
const index = cursor++;
|
|
10381
|
+
fetched[index] = await storage.getBeefForTransaction(missing[index], {
|
|
10382
|
+
...options,
|
|
10383
|
+
mergeToBeef: void 0
|
|
10384
|
+
});
|
|
10385
|
+
}
|
|
10386
|
+
}));
|
|
10387
|
+
span?.end({ attributes: {
|
|
10388
|
+
"beef.fetched_tx_count": fetched.reduce((sum, item) => sum + (item?.txs.length ?? 0), 0),
|
|
10389
|
+
"beef.fetched_bump_count": fetched.reduce((sum, item) => sum + (item?.bumps.length ?? 0), 0)
|
|
10390
|
+
} });
|
|
10391
|
+
});
|
|
10392
|
+
await traceStorageStep(storage, "wallet.storage.create_action.beef_merge", parent, { "beef.fragment_count": fetched.length }, async (span) => {
|
|
10393
|
+
for (const fetchedBeef of fetched) {
|
|
10394
|
+
if (fetchedBeef == null) continue;
|
|
10395
|
+
beef.mergeBeef(fetchedBeef);
|
|
10396
|
+
}
|
|
10397
|
+
span?.end({ attributes: {
|
|
10398
|
+
"beef.merged_tx_count": beef.txs.length,
|
|
10399
|
+
"beef.merged_bump_count": beef.bumps.length
|
|
10400
|
+
} });
|
|
10401
|
+
});
|
|
10402
|
+
return await traceStorageStep(storage, "wallet.storage.create_action.beef_trim_serialize", parent, {
|
|
10403
|
+
"beef.tx_count": beef.txs.length,
|
|
10404
|
+
"beef.bump_count": beef.bumps.length,
|
|
10405
|
+
"beef.known_txid_count": knownTxids.length
|
|
10406
|
+
}, async (span) => {
|
|
10407
|
+
const result = trimInputBeef(beef, vargs);
|
|
10408
|
+
span?.end({ attributes: { "beef.result_bytes": result?.length ?? 0 } });
|
|
10409
|
+
return result;
|
|
10410
|
+
});
|
|
10139
10411
|
}
|
|
10140
10412
|
const dirtyHashLookup = {
|
|
10141
10413
|
"00000000000000000019f112ec0a9982926f1258cdcc558dd7c3b7e5dc7fa148": "This is the first header of the invalid SegWit chain.",
|
|
@@ -12750,21 +13022,6 @@ async function cleanupExpiredActionBatches(storage) {
|
|
|
12750
13022
|
});
|
|
12751
13023
|
return released;
|
|
12752
13024
|
}
|
|
12753
|
-
async function availableManagedChange(storage, userId, basketId, excludeSending, trx) {
|
|
12754
|
-
const statuses = ["completed", "unproven"];
|
|
12755
|
-
if (!excludeSending) statuses.push("sending");
|
|
12756
|
-
const outputs = (await storage.findOutputs({
|
|
12757
|
-
partial: {
|
|
12758
|
-
userId,
|
|
12759
|
-
basketId,
|
|
12760
|
-
spendable: true
|
|
12761
|
-
},
|
|
12762
|
-
txStatus: statuses,
|
|
12763
|
-
trx
|
|
12764
|
-
})).filter(isAutoSpendableChangeOutput);
|
|
12765
|
-
const reserved = new Set(await storage.findReservedActionBatchOutputIds(outputs.map((o) => o.outputId), trx));
|
|
12766
|
-
return outputs.filter((output) => output.spentBy == null && !reserved.has(output.outputId));
|
|
12767
|
-
}
|
|
12768
13025
|
function sourceOutputFromBeef(beef, outpoint) {
|
|
12769
13026
|
const output = (beef.findTxid(outpoint.txid)?.tx)?.outputs[outpoint.vout];
|
|
12770
13027
|
if (output == null) return void 0;
|
|
@@ -12929,7 +13186,7 @@ async function beginActionBatch(storage, auth, args) {
|
|
|
12929
13186
|
const explicit = await resolveExplicitOutputs(storage, userId, args.firstAction, outputScriptLengths != null);
|
|
12930
13187
|
const noSendChange = await resolveNoSendChangeOutputs(storage, userId, args.firstAction);
|
|
12931
13188
|
const fixedOutputIds = new Set([...explicit.outputs, ...noSendChange.outputs].map((output) => output.outputId));
|
|
12932
|
-
const available = (await
|
|
13189
|
+
const available = (await storage.findAvailableManagedChangeInputs(userId, changeBasket.basketId, !args.firstAction.isDelayed)).filter((output) => !fixedOutputIds.has(output.outputId));
|
|
12933
13190
|
const target = estimateFirstActionTarget(storage, args.firstAction, explicit.inputSatoshis + noSendChange.inputSatoshis, outputScriptLengths);
|
|
12934
13191
|
const fixedOutputs = [...explicit.outputs, ...noSendChange.outputs];
|
|
12935
13192
|
const funding = chooseReservationPool(available, target, Math.max(0, INITIAL_RESERVATION_LIMIT - fixedOutputs.length), INITIAL_EXTRA_OUTPUTS, false, reservationPlanningCosts(storage, changeBasket));
|
|
@@ -12973,7 +13230,7 @@ async function extendActionBatch(storage, auth, args) {
|
|
|
12973
13230
|
name: "default"
|
|
12974
13231
|
} }));
|
|
12975
13232
|
const alreadyReserved = await storage.findActionBatchOutputIds(batch.actionBatchId);
|
|
12976
|
-
const available = await
|
|
13233
|
+
const available = await storage.findAvailableManagedChangeInputs(userId, basket.basketId, false);
|
|
12977
13234
|
if (!Number.isSafeInteger(args.requestedOutputs) || args.requestedOutputs < 0) throw new WERR_INVALID_PARAMETER("requestedOutputs", "non-negative safe integer");
|
|
12978
13235
|
const requestedCount = Math.min(args.requestedOutputs, 64);
|
|
12979
13236
|
const funding = chooseReservationPool(available, Math.max(1, args.targetSatoshis), requestedCount, 0, true, reservationPlanningCosts(storage, basket));
|
|
@@ -13346,6 +13603,31 @@ async function abortActionBatch(storage, auth, batchId) {
|
|
|
13346
13603
|
});
|
|
13347
13604
|
}
|
|
13348
13605
|
//#endregion
|
|
13606
|
+
//#region ../src/storage/methods/availableManagedChange.ts
|
|
13607
|
+
/**
|
|
13608
|
+
* Return the exact set of wallet-managed outputs currently eligible for
|
|
13609
|
+
* automatic funding. Keeping this predicate shared prevents the planner,
|
|
13610
|
+
* allocator, action-batch reservations, and availability count from drifting.
|
|
13611
|
+
*/
|
|
13612
|
+
async function availableManagedChange(storage, userId, basketId, excludeSending, trx) {
|
|
13613
|
+
const statuses = ["completed", "unproven"];
|
|
13614
|
+
if (!excludeSending) statuses.push("sending");
|
|
13615
|
+
const outputs = (await storage.findOutputs({
|
|
13616
|
+
partial: {
|
|
13617
|
+
userId,
|
|
13618
|
+
basketId,
|
|
13619
|
+
spendable: true,
|
|
13620
|
+
...managedChangeOutputFields
|
|
13621
|
+
},
|
|
13622
|
+
txStatus: statuses,
|
|
13623
|
+
noScript: true,
|
|
13624
|
+
trx
|
|
13625
|
+
})).filter(isAutoSpendableChangeOutput);
|
|
13626
|
+
if (outputs.length === 0) return outputs;
|
|
13627
|
+
const reserved = new Set(await storage.findReservedActionBatchOutputIds(outputs.map((output) => output.outputId), trx));
|
|
13628
|
+
return outputs.filter((output) => !reserved.has(output.outputId));
|
|
13629
|
+
}
|
|
13630
|
+
//#endregion
|
|
13349
13631
|
//#region ../src/storage/StorageProvider.ts
|
|
13350
13632
|
var StorageProvider = class StorageProvider extends StorageReaderWriter {
|
|
13351
13633
|
isDirty = false;
|
|
@@ -13379,6 +13661,33 @@ var StorageProvider = class StorageProvider extends StorageReaderWriter {
|
|
|
13379
13661
|
this.maxRecursionDepth = 12;
|
|
13380
13662
|
this.scriptVerifier = options.scriptVerifier;
|
|
13381
13663
|
}
|
|
13664
|
+
/** Mark a planned set of change inputs spent within the caller's transaction. */
|
|
13665
|
+
async markChangeInputsSpent(outputIds, transactionId, trx) {
|
|
13666
|
+
let updated = 0;
|
|
13667
|
+
const current = await this.findOutputsByIds(outputIds, trx);
|
|
13668
|
+
for (const outputId of outputIds) {
|
|
13669
|
+
const output = current[outputId];
|
|
13670
|
+
if (output == null || !output.spendable || output.spentBy != null) continue;
|
|
13671
|
+
updated += await this.updateOutput(outputId, {
|
|
13672
|
+
spendable: false,
|
|
13673
|
+
spentBy: transactionId
|
|
13674
|
+
}, trx);
|
|
13675
|
+
}
|
|
13676
|
+
return updated;
|
|
13677
|
+
}
|
|
13678
|
+
/** Return unreserved wallet-managed outputs eligible for automatic funding. */
|
|
13679
|
+
async findAvailableManagedChangeInputs(userId, basketId, excludeSending, trx) {
|
|
13680
|
+
return await availableManagedChange(this, userId, basketId, excludeSending, trx);
|
|
13681
|
+
}
|
|
13682
|
+
/** Read the current status of a set of source transactions without loading raw transaction bytes. */
|
|
13683
|
+
async findTransactionStatusesByIds(userId, transactionIds, trx) {
|
|
13684
|
+
const statuses = /* @__PURE__ */ new Map();
|
|
13685
|
+
for (const transactionId of new Set(transactionIds)) {
|
|
13686
|
+
const transaction = await this.findTransactionById(transactionId, trx, true);
|
|
13687
|
+
if (transaction?.userId === userId) statuses.set(transactionId, transaction.status);
|
|
13688
|
+
}
|
|
13689
|
+
return statuses;
|
|
13690
|
+
}
|
|
13382
13691
|
async insertActionBatch(_batch, _trx) {
|
|
13383
13692
|
throw new WERR_NOT_IMPLEMENTED();
|
|
13384
13693
|
}
|
|
@@ -13489,7 +13798,7 @@ var StorageProvider = class StorageProvider extends StorageReaderWriter {
|
|
|
13489
13798
|
}
|
|
13490
13799
|
return byOutpoint;
|
|
13491
13800
|
}
|
|
13492
|
-
async findOutputsByOutpointsForUpdate(userId, outpoints, trx) {
|
|
13801
|
+
async findOutputsByOutpointsForUpdate(userId, outpoints, trx, _noScript = false) {
|
|
13493
13802
|
return await this.findOutputsByOutpoints(userId, outpoints, trx);
|
|
13494
13803
|
}
|
|
13495
13804
|
async findOrInsertOutputBasketsBulk(userId, names, trx) {
|
|
@@ -14926,9 +15235,16 @@ var StorageIdb = class extends StorageProvider {
|
|
|
14926
15235
|
async initDB(storageName, storageIdentityKey) {
|
|
14927
15236
|
const chain = this.chain;
|
|
14928
15237
|
const maxOutputScript = 1024;
|
|
14929
|
-
return await (0, idb.openDB)(this.dbName,
|
|
15238
|
+
return await (0, idb.openDB)(this.dbName, 3, { upgrade(db, _oldVersion, _newVersion, transaction) {
|
|
14930
15239
|
upgradeAllStoresV1(db);
|
|
14931
15240
|
upgradeActionBatchStoresV2(db);
|
|
15241
|
+
const outputs = transaction.objectStore("outputs");
|
|
15242
|
+
if (!outputs.indexNames.contains("userId_basketId")) outputs.createIndex("userId_basketId", ["userId", "basketId"]);
|
|
15243
|
+
if (!outputs.indexNames.contains("txid_vout_userId")) outputs.createIndex("txid_vout_userId", [
|
|
15244
|
+
"txid",
|
|
15245
|
+
"vout",
|
|
15246
|
+
"userId"
|
|
15247
|
+
], { unique: true });
|
|
14932
15248
|
if (!db.objectStoreNames.contains("settings")) {
|
|
14933
15249
|
if (storageName == null || storageName === "" || storageIdentityKey == null || storageIdentityKey === "") throw new WERR_INVALID_OPERATION("migrate must be called before first access");
|
|
14934
15250
|
const settings = db.createObjectStore("settings", { keyPath: "storageIdentityKey" });
|
|
@@ -15134,11 +15450,53 @@ var StorageIdb = class extends StorageProvider {
|
|
|
15134
15450
|
txStatus,
|
|
15135
15451
|
noScript: true
|
|
15136
15452
|
};
|
|
15137
|
-
|
|
15453
|
+
const outputIds = [];
|
|
15138
15454
|
await this.filterOutputs(args, (r) => {
|
|
15139
|
-
if (isAutoSpendableChangeOutput(r))
|
|
15455
|
+
if (isAutoSpendableChangeOutput(r)) outputIds.push(r.outputId);
|
|
15140
15456
|
});
|
|
15141
|
-
|
|
15457
|
+
const reserved = await this.findReservedActionBatchOutputIds(outputIds);
|
|
15458
|
+
return outputIds.length - reserved.length;
|
|
15459
|
+
}
|
|
15460
|
+
async findTransactionStatusesByIds(userId, transactionIds, trx) {
|
|
15461
|
+
const statuses = /* @__PURE__ */ new Map();
|
|
15462
|
+
if (transactionIds.length === 0) return statuses;
|
|
15463
|
+
const dbTrx = this.toDbTrx(["transactions"], "readonly", trx);
|
|
15464
|
+
const store = dbTrx.objectStore("transactions");
|
|
15465
|
+
for (const transactionId of new Set(transactionIds)) {
|
|
15466
|
+
const transaction = await store.get(transactionId);
|
|
15467
|
+
if (transaction?.userId === userId) statuses.set(transactionId, transaction.status);
|
|
15468
|
+
}
|
|
15469
|
+
if (trx == null) await dbTrx.done;
|
|
15470
|
+
return statuses;
|
|
15471
|
+
}
|
|
15472
|
+
async findOutputsByOutpointsInternal(userId, outpoints, trx, noScript = false) {
|
|
15473
|
+
const byOutpoint = {};
|
|
15474
|
+
if (outpoints.length === 0) return byOutpoint;
|
|
15475
|
+
const dbTrx = this.toDbTrx(noScript ? ["outputs"] : [
|
|
15476
|
+
"outputs",
|
|
15477
|
+
"proven_txs",
|
|
15478
|
+
"proven_tx_reqs"
|
|
15479
|
+
], "readonly", trx);
|
|
15480
|
+
const index = dbTrx.objectStore("outputs").index("txid_vout_userId");
|
|
15481
|
+
const unique = [...new Map(outpoints.map((outpoint) => [`${outpoint.txid}.${outpoint.vout}`, outpoint])).values()];
|
|
15482
|
+
const rows = await Promise.all(unique.map(async (outpoint) => await index.get([
|
|
15483
|
+
outpoint.txid,
|
|
15484
|
+
outpoint.vout,
|
|
15485
|
+
userId
|
|
15486
|
+
])));
|
|
15487
|
+
for (const row of rows) {
|
|
15488
|
+
if (row == null) continue;
|
|
15489
|
+
if (!noScript) await this.validateOutputScript(row, dbTrx);
|
|
15490
|
+
byOutpoint[`${String(row.txid)}.${row.vout}`] = this.validateEntity(row);
|
|
15491
|
+
}
|
|
15492
|
+
if (trx == null) await dbTrx.done;
|
|
15493
|
+
return byOutpoint;
|
|
15494
|
+
}
|
|
15495
|
+
async findOutputsByOutpoints(userId, outpoints, trx) {
|
|
15496
|
+
return await this.findOutputsByOutpointsInternal(userId, outpoints, trx);
|
|
15497
|
+
}
|
|
15498
|
+
async findOutputsByOutpointsForUpdate(userId, outpoints, trx, noScript = false) {
|
|
15499
|
+
return await this.findOutputsByOutpointsInternal(userId, outpoints, trx, noScript);
|
|
15142
15500
|
}
|
|
15143
15501
|
async findCertificatesAuth(auth, args) {
|
|
15144
15502
|
if (auth.userId == null || args.partial.userId != null && args.partial.userId !== 0 && args.partial.userId !== auth.userId) throw new WERR_UNAUTHORIZED();
|
|
@@ -15901,6 +16259,12 @@ var StorageIdb = class extends StorageProvider {
|
|
|
15901
16259
|
partial.vout,
|
|
15902
16260
|
partial.userId
|
|
15903
16261
|
], direction);
|
|
16262
|
+
if (partial?.txid != null && partial.txid !== "" && partial?.vout !== void 0) return store.index("txid_vout_userId").openCursor([
|
|
16263
|
+
partial.txid,
|
|
16264
|
+
partial.vout,
|
|
16265
|
+
partial.userId
|
|
16266
|
+
], direction);
|
|
16267
|
+
if (partial?.basketId !== void 0) return store.index("userId_basketId").openCursor([partial.userId, partial.basketId], direction);
|
|
15904
16268
|
return store.index("userId").openCursor(partial.userId, direction);
|
|
15905
16269
|
}
|
|
15906
16270
|
if (partial?.transactionId !== void 0) return store.index("transactionId").openCursor(partial.transactionId, direction);
|
|
@@ -15908,6 +16272,17 @@ var StorageIdb = class extends StorageProvider {
|
|
|
15908
16272
|
if (partial?.spentBy !== void 0) return store.index("spentBy").openCursor(partial.spentBy, direction);
|
|
15909
16273
|
return store.openCursor(null, direction);
|
|
15910
16274
|
}
|
|
16275
|
+
async eligibleOutputTransactionIds(args, dbTrx) {
|
|
16276
|
+
if (args.txStatus == null) return void 0;
|
|
16277
|
+
const validTransactionIds = /* @__PURE__ */ new Set();
|
|
16278
|
+
const transactions = dbTrx.objectStore("transactions");
|
|
16279
|
+
for (const status of args.txStatus) {
|
|
16280
|
+
const index = args.partial.userId === void 0 ? transactions.index("status") : transactions.index("status_userId");
|
|
16281
|
+
const key = args.partial.userId === void 0 ? status : [status, args.partial.userId];
|
|
16282
|
+
for (const transactionId of await index.getAllKeys(key)) validTransactionIds.add(Number(transactionId));
|
|
16283
|
+
}
|
|
16284
|
+
return validTransactionIds;
|
|
16285
|
+
}
|
|
15911
16286
|
async filterOutputs(args, filtered, tagIds, isQueryModeAll) {
|
|
15912
16287
|
this.assertNoUndefinedInPartial(args.partial);
|
|
15913
16288
|
if (args.partial.lockingScript != null) throw new WERR_INVALID_PARAMETER("args.partial.lockingScript", "undefined. Outputs may not be found by lockingScript value.");
|
|
@@ -15917,15 +16292,10 @@ var StorageIdb = class extends StorageProvider {
|
|
|
15917
16292
|
const dbTrx = this.toDbTrx(stores, "readonly", args.trx);
|
|
15918
16293
|
const direction = args.orderDescending === true ? "prev" : "next";
|
|
15919
16294
|
const store = dbTrx.objectStore("outputs");
|
|
16295
|
+
const validTransactionIds = await this.eligibleOutputTransactionIds(args, dbTrx);
|
|
15920
16296
|
await scanCursor(await this.openOutputsCursor(store, args.partial, direction), args.since, args.paged?.offset ?? 0, args.paged?.limit, async (r) => {
|
|
15921
16297
|
if (!matchesOutputPartial(r, args.partial)) return false;
|
|
15922
|
-
if (
|
|
15923
|
-
if (await this.countTransactions({
|
|
15924
|
-
partial: { transactionId: r.transactionId },
|
|
15925
|
-
status: args.txStatus,
|
|
15926
|
-
trx: dbTrx
|
|
15927
|
-
}) === 0) return false;
|
|
15928
|
-
}
|
|
16298
|
+
if (validTransactionIds != null && !validTransactionIds.has(r.transactionId)) return false;
|
|
15929
16299
|
if (tagIds != null && tagIds.length > 0 && !await this.outputMatchesTags(r.outputId, tagIds, isQueryModeAll, dbTrx)) return false;
|
|
15930
16300
|
return true;
|
|
15931
16301
|
}, (r) => {
|