@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.
@@ -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 knownTxids = new Set(options.knownTxids ?? []);
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, knownTxids, item.depth));
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, knownTxids, recursionDepth) {
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 (knownTxids.has(txid)) {
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.getKnownTxids(args.options.knownTxids);
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 validateRequiredInputs(storage, userId, vargs);
9411
- logger?.log("validated required inputs");
9412
- const xoutputs = validateRequiredOutputs(storage, userId, vargs);
9413
- logger?.log("validated required outputs");
9414
- const changeBasketName = "default";
9415
- const changeBasket = verifyOne(await storage.findOutputBaskets({ partial: {
9416
- userId,
9417
- name: changeBasketName
9418
- } }), `Invalid outputGeneration basket "${changeBasketName}"`);
9419
- logger?.log("found change basket");
9420
- const noSendChangeIn = await validateNoSendChange(storage, userId, vargs, changeBasket);
9421
- logger?.log("validated noSendChange");
9422
- const availableChangeCount = await storage.countChangeInputs(userId, changeBasket.basketId, !vargs.isDelayed);
9423
- logger?.log(`counted change inputs ${availableChangeCount}`);
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
- await preflightInsufficientFundsFastPath(vargs, xinputs, xoutputs, noSendChangeIn, availableChangeCount, feeModel);
9427
- logger?.log("passed insufficient-funds preflight");
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
- newTx = await createNewTxRecord(storage, userId, vargs, storageBeef);
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.updateTransaction(newTx.transactionId, { satoshis });
9451
- const { outputs, changeVouts } = await createNewOutputs(storage, userId, vargs, ctx, changeOutputs);
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, userId, vargs, allocatedChange, beef);
9541
+ const inputBeef = await mergeAllocatedChangeBeefs(storage, vargs, allocatedChange, beef, parent);
9454
9542
  logger?.log("merged allocated change beefs");
9455
- const inputs = await createNewInputs(storage, userId, vargs, ctx, allocatedChange);
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.toBinary(),
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
- for (const label of vargs.labels) {
9741
- const txLabel = await storage.findOrInsertTxLabel(userId, label);
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) for (const op of noSendChange) {
9927
- const output = verifyOneOrNone(await storage.findOutputs({ partial: {
9928
- userId,
9929
- txid: op.txid,
9930
- vout: op.vout
9931
- } }));
9932
- if (!isAutoSpendableChangeOutput(output) || !verifyNumber(output.satoshis) || output.basketId !== changeBasket.basketId) throw new WERR_INVALID_PARAMETER("noSendChange outpoint", "wallet-managed BRC-29 change");
9933
- if (r.some((o) => o.outputId === output.outputId)) throw new WERR_INVALID_PARAMETER("noSendChange outpoint", "unique. Duplicates are not allowed.");
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
- async function preflightInsufficientFundsFastPath(vargs, xinputs, xoutputs, noSendChangeIn, availableChangeCount, feeModel) {
9940
- if (feeModel.model !== "sat/kb" || !feeModel.value) return;
9941
- const fixedInputSatoshis = xinputs.reduce((a, e) => a + e.satoshis, 0);
9942
- const noSendSatoshis = noSendChangeIn.reduce((a, e) => a + Number(e.satoshis || 0), 0);
9943
- const spending = xoutputs.reduce((a, e) => a + e.satoshis, 0);
9944
- const minSize = transactionSize(xinputs.map((i) => i.unlockingScriptLength || 0), xoutputs.map((o) => Math.floor(o.lockingScript.length / 2)));
9945
- const minRequired = spending + Math.ceil(minSize / 1e3 * feeModel.value);
9946
- const fixedAvailable = fixedInputSatoshis + noSendSatoshis;
9947
- if (fixedAvailable >= minRequired) return;
9948
- const deficit = minRequired - fixedAvailable;
9949
- if (availableChangeCount <= 0) throw new WERR_INSUFFICIENT_FUNDS(minRequired, deficit);
9950
- }
9951
- async function fundNewTransactionSdk(storage, userId, vargs, ctx) {
9952
- const params = {
9953
- fixedInputs: ctx.xinputs.map((xi) => ({
9954
- satoshis: xi.satoshis,
9955
- unlockingScriptLength: xi.unlockingScriptLength
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: ctx.xoutputs.map((xo) => ({
9958
- satoshis: xo.satoshis,
9959
- lockingScriptLength: xo.lockingScript.length / 2
10057
+ fixedOutputs: xoutputs.map((output) => ({
10058
+ satoshis: output.satoshis,
10059
+ lockingScriptLength: output.lockingScript.length / 2
9960
10060
  })),
9961
- feeModel: ctx.feeModel,
9962
- changeInitialSatoshis: Math.max(1, ctx.changeBasket.minimumDesiredUTXOValue),
9963
- changeFirstSatoshis: Math.max(1, Math.round(ctx.changeBasket.minimumDesiredUTXOValue / 4)),
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: ctx.changeBasket.numberOfDesiredUTXOs - ctx.availableChangeCount,
10066
+ targetNetCount: changeBasket.numberOfDesiredUTXOs - availableChangeCount,
9967
10067
  randomVals: vargs.randomVals
9968
10068
  };
9969
- const noSendChange = [...ctx.noSendChangeIn];
9970
- const outputs = {};
9971
- const allocateChangeInput = async (targetSatoshis, exactSatoshis) => {
9972
- if (noSendChange.length > 0) {
9973
- const o = noSendChange.pop();
9974
- outputs[o.outputId] = o;
9975
- await storage.updateOutput(o.outputId, {
9976
- spendable: false,
9977
- spentBy: ctx.transactionId
9978
- });
9979
- o.spendable = false;
9980
- o.spentBy = ctx.transactionId;
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
- outputId: o.outputId,
9983
- satoshis: o.satoshis
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
- outputId: o.outputId,
9992
- satoshis: o.satoshis
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
- const releaseChangeInput = async (outputId) => {
9996
- const nsco = ctx.noSendChangeIn.find((o) => o.outputId === outputId);
9997
- if (nsco != null) {
9998
- noSendChange.push(nsco);
9999
- return;
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
- await storage.updateOutput(outputId, {
10002
- spendable: true,
10003
- spentBy: void 0
10004
- });
10199
+ }));
10200
+ return {
10201
+ hydratedScriptCount: missing.filter((output) => output.lockingScript?.length === output.scriptLength).length,
10202
+ scriptSourceTransactionCount: groups.length
10005
10203
  };
10006
- const gcr = await generateChangeSdk(params, allocateChangeInput, releaseChangeInput, vargs.logger, storage.telemetry);
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: gcr.allocatedChangeInputs.map((i) => outputs[i.outputId]),
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 knownTxids = {};
10070
- for (const txid of vargs.options.knownTxids || []) knownTxids[txid] = true;
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
- async function mergeAllocatedChangeBeefs(storage, userId, vargs, allocatedChange, beef) {
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 known = new Set(vargs.options.knownTxids ?? []);
10086
- const missing = Array.from(new Set(allocatedChange.map((o) => o.txid).filter((txid) => beef.findTxid(txid) == null && !known.has(txid))));
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 Promise.all(Array.from({ length: concurrency }, async () => {
10091
- while (cursor < missing.length) {
10092
- const index = cursor++;
10093
- fetched[index] = await storage.getBeefForTransaction(missing[index], {
10094
- ...options,
10095
- mergeToBeef: void 0
10096
- });
10097
- }
10098
- }));
10099
- for (const fetchedBeef of fetched) {
10100
- if (fetchedBeef == null) continue;
10101
- beef.mergeBeef(fetchedBeef);
10102
- }
10103
- return trimInputBeef(beef, vargs);
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 availableManagedChange(storage, userId, changeBasket.basketId, !args.firstAction.isDelayed)).filter((output) => !fixedOutputIds.has(output.outputId));
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 availableManagedChange(storage, userId, basket.basketId, false);
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, 2, { upgrade(db) {
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
- let count = 0;
15418
+ const outputIds = [];
15103
15419
  await this.filterOutputs(args, (r) => {
15104
- if (isAutoSpendableChangeOutput(r)) count++;
15420
+ if (isAutoSpendableChangeOutput(r)) outputIds.push(r.outputId);
15105
15421
  });
15106
- return count;
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 (args.txStatus !== void 0) {
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) => {