@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.
@@ -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 knownTxids = new Set(options.knownTxids ?? []);
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, knownTxids, item.depth));
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, knownTxids, recursionDepth) {
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 (knownTxids.has(txid)) {
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.getKnownTxids(args.options.knownTxids);
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 validateRequiredInputs(storage, userId, vargs);
9446
- logger?.log("validated required inputs");
9447
- const xoutputs = validateRequiredOutputs(storage, userId, vargs);
9448
- logger?.log("validated required outputs");
9449
- const changeBasketName = "default";
9450
- const changeBasket = verifyOne(await storage.findOutputBaskets({ partial: {
9451
- userId,
9452
- name: changeBasketName
9453
- } }), `Invalid outputGeneration basket "${changeBasketName}"`);
9454
- logger?.log("found change basket");
9455
- const noSendChangeIn = await validateNoSendChange(storage, userId, vargs, changeBasket);
9456
- logger?.log("validated noSendChange");
9457
- const availableChangeCount = await storage.countChangeInputs(userId, changeBasket.basketId, !vargs.isDelayed);
9458
- logger?.log(`counted change inputs ${availableChangeCount}`);
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
- await preflightInsufficientFundsFastPath(vargs, xinputs, xoutputs, noSendChangeIn, availableChangeCount, feeModel);
9462
- logger?.log("passed insufficient-funds preflight");
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
- newTx = await createNewTxRecord(storage, userId, vargs, storageBeef);
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.updateTransaction(newTx.transactionId, { satoshis });
9486
- const { outputs, changeVouts } = await createNewOutputs(storage, userId, vargs, ctx, changeOutputs);
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, userId, vargs, allocatedChange, beef);
9576
+ const inputBeef = await mergeAllocatedChangeBeefs(storage, vargs, allocatedChange, beef, parent);
9489
9577
  logger?.log("merged allocated change beefs");
9490
- const inputs = await createNewInputs(storage, userId, vargs, ctx, allocatedChange);
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.toBinary(),
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
- for (const label of vargs.labels) {
9776
- const txLabel = await storage.findOrInsertTxLabel(userId, label);
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) for (const op of noSendChange) {
9962
- const output = verifyOneOrNone(await storage.findOutputs({ partial: {
9963
- userId,
9964
- txid: op.txid,
9965
- vout: op.vout
9966
- } }));
9967
- if (!isAutoSpendableChangeOutput(output) || !verifyNumber(output.satoshis) || output.basketId !== changeBasket.basketId) throw new WERR_INVALID_PARAMETER("noSendChange outpoint", "wallet-managed BRC-29 change");
9968
- if (r.some((o) => o.outputId === output.outputId)) throw new WERR_INVALID_PARAMETER("noSendChange outpoint", "unique. Duplicates are not allowed.");
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
- async function preflightInsufficientFundsFastPath(vargs, xinputs, xoutputs, noSendChangeIn, availableChangeCount, feeModel) {
9975
- if (feeModel.model !== "sat/kb" || !feeModel.value) return;
9976
- const fixedInputSatoshis = xinputs.reduce((a, e) => a + e.satoshis, 0);
9977
- const noSendSatoshis = noSendChangeIn.reduce((a, e) => a + Number(e.satoshis || 0), 0);
9978
- const spending = xoutputs.reduce((a, e) => a + e.satoshis, 0);
9979
- const minSize = transactionSize(xinputs.map((i) => i.unlockingScriptLength || 0), xoutputs.map((o) => Math.floor(o.lockingScript.length / 2)));
9980
- const minRequired = spending + Math.ceil(minSize / 1e3 * feeModel.value);
9981
- const fixedAvailable = fixedInputSatoshis + noSendSatoshis;
9982
- if (fixedAvailable >= minRequired) return;
9983
- const deficit = minRequired - fixedAvailable;
9984
- if (availableChangeCount <= 0) throw new WERR_INSUFFICIENT_FUNDS(minRequired, deficit);
9985
- }
9986
- async function fundNewTransactionSdk(storage, userId, vargs, ctx) {
9987
- const params = {
9988
- fixedInputs: ctx.xinputs.map((xi) => ({
9989
- satoshis: xi.satoshis,
9990
- unlockingScriptLength: xi.unlockingScriptLength
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: ctx.xoutputs.map((xo) => ({
9993
- satoshis: xo.satoshis,
9994
- lockingScriptLength: xo.lockingScript.length / 2
10092
+ fixedOutputs: xoutputs.map((output) => ({
10093
+ satoshis: output.satoshis,
10094
+ lockingScriptLength: output.lockingScript.length / 2
9995
10095
  })),
9996
- feeModel: ctx.feeModel,
9997
- changeInitialSatoshis: Math.max(1, ctx.changeBasket.minimumDesiredUTXOValue),
9998
- changeFirstSatoshis: Math.max(1, Math.round(ctx.changeBasket.minimumDesiredUTXOValue / 4)),
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: ctx.changeBasket.numberOfDesiredUTXOs - ctx.availableChangeCount,
10101
+ targetNetCount: changeBasket.numberOfDesiredUTXOs - availableChangeCount,
10002
10102
  randomVals: vargs.randomVals
10003
10103
  };
10004
- const noSendChange = [...ctx.noSendChangeIn];
10005
- const outputs = {};
10006
- const allocateChangeInput = async (targetSatoshis, exactSatoshis) => {
10007
- if (noSendChange.length > 0) {
10008
- const o = noSendChange.pop();
10009
- outputs[o.outputId] = o;
10010
- await storage.updateOutput(o.outputId, {
10011
- spendable: false,
10012
- spentBy: ctx.transactionId
10013
- });
10014
- o.spendable = false;
10015
- o.spentBy = ctx.transactionId;
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
- outputId: o.outputId,
10018
- satoshis: o.satoshis
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
- outputId: o.outputId,
10027
- satoshis: o.satoshis
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
- const releaseChangeInput = async (outputId) => {
10031
- const nsco = ctx.noSendChangeIn.find((o) => o.outputId === outputId);
10032
- if (nsco != null) {
10033
- noSendChange.push(nsco);
10034
- return;
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
- await storage.updateOutput(outputId, {
10037
- spendable: true,
10038
- spentBy: void 0
10039
- });
10234
+ }));
10235
+ return {
10236
+ hydratedScriptCount: missing.filter((output) => output.lockingScript?.length === output.scriptLength).length,
10237
+ scriptSourceTransactionCount: groups.length
10040
10238
  };
10041
- const gcr = await generateChangeSdk(params, allocateChangeInput, releaseChangeInput, vargs.logger, storage.telemetry);
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: gcr.allocatedChangeInputs.map((i) => outputs[i.outputId]),
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 knownTxids = {};
10105
- for (const txid of vargs.options.knownTxids || []) knownTxids[txid] = true;
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
- async function mergeAllocatedChangeBeefs(storage, userId, vargs, allocatedChange, beef) {
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 known = new Set(vargs.options.knownTxids ?? []);
10121
- const missing = Array.from(new Set(allocatedChange.map((o) => o.txid).filter((txid) => beef.findTxid(txid) == null && !known.has(txid))));
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 Promise.all(Array.from({ length: concurrency }, async () => {
10126
- while (cursor < missing.length) {
10127
- const index = cursor++;
10128
- fetched[index] = await storage.getBeefForTransaction(missing[index], {
10129
- ...options,
10130
- mergeToBeef: void 0
10131
- });
10132
- }
10133
- }));
10134
- for (const fetchedBeef of fetched) {
10135
- if (fetchedBeef == null) continue;
10136
- beef.mergeBeef(fetchedBeef);
10137
- }
10138
- return trimInputBeef(beef, vargs);
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 availableManagedChange(storage, userId, changeBasket.basketId, !args.firstAction.isDelayed)).filter((output) => !fixedOutputIds.has(output.outputId));
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 availableManagedChange(storage, userId, basket.basketId, false);
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, 2, { upgrade(db) {
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
- let count = 0;
15453
+ const outputIds = [];
15138
15454
  await this.filterOutputs(args, (r) => {
15139
- if (isAutoSpendableChangeOutput(r)) count++;
15455
+ if (isAutoSpendableChangeOutput(r)) outputIds.push(r.outputId);
15140
15456
  });
15141
- return count;
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 (args.txStatus !== void 0) {
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) => {