@bsv/wallet-toolbox-client 2.7.0 → 2.7.1

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.
@@ -5938,6 +5938,18 @@ function requireConclusiveUtxo(classification) {
5938
5938
  }
5939
5939
  //#endregion
5940
5940
  //#region ../src/storage/methods/reconcileFailedTransactionInputs.ts
5941
+ async function quarantineLocalInputs(localInputs, storage, trx) {
5942
+ const staleOutpoints = /* @__PURE__ */ new Set();
5943
+ for (const { output, outpoint } of localInputs) {
5944
+ await storage.updateOutput(verifyId(output.outputId), { spendable: false }, trx);
5945
+ staleOutpoints.add(`${outpoint.txid}.${outpoint.vout}`);
5946
+ }
5947
+ return {
5948
+ checked: localInputs.length,
5949
+ staleConfirmed: localInputs.length,
5950
+ staleOutpoints: [...staleOutpoints]
5951
+ };
5952
+ }
5941
5953
  async function findLocalInputs(req, storage, trx) {
5942
5954
  const outpoints = Transaction.fromBinary(req.rawTx).inputs.map((input) => ({
5943
5955
  txid: input.sourceTXID ?? "",
@@ -5977,21 +5989,22 @@ async function findLocalInputs(req, storage, trx) {
5977
5989
  * able to repair the transaction through the ordinary proof recovery path.
5978
5990
  */
5979
5991
  async function quarantineReqInputs(req, storage, trx, logger) {
5980
- const localInputs = await findLocalInputs(req, storage, trx);
5981
- const staleOutpoints = /* @__PURE__ */ new Set();
5982
- for (const { output, outpoint } of localInputs) {
5983
- await storage.updateOutput(verifyId(output.outputId), { spendable: false }, trx);
5984
- staleOutpoints.add(`${outpoint.txid}.${outpoint.vout}`);
5985
- }
5986
- const result = {
5987
- checked: localInputs.length,
5988
- staleConfirmed: localInputs.length,
5989
- staleOutpoints: [...staleOutpoints]
5990
- };
5992
+ const result = await quarantineLocalInputs(await findLocalInputs(req, storage, trx), storage, trx);
5991
5993
  if (result.staleConfirmed > 0) logger?.log(`quarantineReqInputs: ${result.staleConfirmed} local input copy/copies quarantined for txid=${req.txid}`);
5992
5994
  return result;
5993
5995
  }
5994
5996
  /**
5997
+ * Keep only inputs created by a locally terminal parent unavailable after the
5998
+ * failed-child transition releases its allocations. Other inputs may still be
5999
+ * valid UTXOs and must remain reusable.
6000
+ */
6001
+ async function quarantineReqInputsFromFailedParents(req, failedParentTxids, storage, trx, logger) {
6002
+ const failedParents = new Set(failedParentTxids);
6003
+ const result = await quarantineLocalInputs((await findLocalInputs(req, storage, trx)).filter(({ outpoint }) => failedParents.has(outpoint.txid)), storage, trx);
6004
+ if (result.staleConfirmed > 0) logger?.log(`quarantineReqInputsFromFailedParents: ${result.staleConfirmed} local parent output copy/copies quarantined for txid=${req.txid}`);
6005
+ return result;
6006
+ }
6007
+ /**
5995
6008
  * Ask the configured UTXO-provider collection to classify the request's local
5996
6009
  * inputs and mark only positively confirmed spent outputs unavailable.
5997
6010
  * Provider errors and a collection with no providers are inconclusive and
@@ -29486,9 +29499,9 @@ var TaskReconcilePendingTransactions = class TaskReconcilePendingTransactions ex
29486
29499
  }
29487
29500
  async runTask() {
29488
29501
  const checkpoint = await this.getCheckpoint();
29489
- const updatedBefore = /* @__PURE__ */ new Date(Date.now() - this.minAgeMinutes * 60 * 1e3);
29502
+ const createdBefore = /* @__PURE__ */ new Date(Date.now() - this.minAgeMinutes * 60 * 1e3);
29490
29503
  const reqs = await this.findReqsToReview(checkpoint);
29491
- const eligible = reqs.filter((req) => req.updated_at <= updatedBefore);
29504
+ const eligible = reqs.filter((req) => req.created_at <= createdBefore);
29492
29505
  const provider = eligible.length === 0 ? void 0 : await this.monitor.services.getStatusForTxids(eligible.map((req) => req.txid));
29493
29506
  const results = new Map(provider?.results.map((result) => [result.txid, result]) ?? []);
29494
29507
  let reconciled = 0;
@@ -29497,17 +29510,18 @@ var TaskReconcilePendingTransactions = class TaskReconcilePendingTransactions ex
29497
29510
  const retained = [];
29498
29511
  for (const req of reqs) {
29499
29512
  const result = results.get(req.txid);
29500
- if (req.updated_at > updatedBefore || provider?.status !== "success" || result?.terminal !== true) {
29513
+ if (req.created_at > createdBefore) {
29501
29514
  retained.push(req);
29502
29515
  continue;
29503
29516
  }
29504
- if (!await this.applyTerminalVerdict(req, result, provider.name)) {
29517
+ const applied = await this.applyTerminalVerdict(req, provider?.status === "success" ? result : void 0, provider?.status === "success" ? provider.name : void 0, createdBefore);
29518
+ if (!applied) {
29505
29519
  retained.push(req);
29506
29520
  continue;
29507
29521
  }
29508
29522
  reconciled++;
29509
- if (result.inputConflict === true) inputConflicts++;
29510
- reviewLog += `reconciled ${req.provenTxReqId} ${req.txid} ${result.providerStatus ?? "terminal"} => ${result.inputConflict === true ? "doubleSpend" : "invalid"}\n`;
29523
+ if (applied.result.inputConflict === true) inputConflicts++;
29524
+ reviewLog += `reconciled ${req.provenTxReqId} ${req.txid} ${applied.result.providerStatus ?? "terminal"} => ${applied.result.inputConflict === true ? "doubleSpend" : "invalid"}\n`;
29511
29525
  }
29512
29526
  const fullPage = reqs.length >= this.reviewLimit;
29513
29527
  this.triggerNextMsecs = fullPage ? this.triggerQuickMsecs : this.triggerMsecs;
@@ -29552,13 +29566,18 @@ var TaskReconcilePendingTransactions = class TaskReconcilePendingTransactions ex
29552
29566
  reqs.sourceOffset = offset;
29553
29567
  return reqs;
29554
29568
  }
29555
- async applyTerminalVerdict(reqApi, result, provider) {
29556
- let applied = false;
29569
+ async applyTerminalVerdict(reqApi, providerResult, providerName, createdBefore) {
29570
+ let applied;
29557
29571
  await this.storage.runAsStorageProvider(async (sp) => {
29558
29572
  await sp.transaction(async (trx) => {
29559
29573
  const req = new EntityProvenTxReq(reqApi);
29560
29574
  await req.refreshFromStorage(sp, trx);
29561
29575
  if (!REVIEW_STATUSES.includes(req.status)) return;
29576
+ if (req.created_at > createdBefore) return;
29577
+ const failedParentTxids = await this.findFailedLocalParentTxids(req, sp, trx);
29578
+ const result = providerResult?.terminal === true ? providerResult : providerResult?.status === "known" || providerResult?.status === "mined" ? void 0 : this.failedParentVerdict(req.txid, failedParentTxids);
29579
+ if (result == null) return;
29580
+ const provider = providerResult?.terminal === true ? providerName ?? "transaction-status-provider" : "local-storage";
29562
29581
  req.status = result.inputConflict === true ? "doubleSpend" : "invalid";
29563
29582
  req.addHistoryNote({
29564
29583
  when: (/* @__PURE__ */ new Date()).toISOString(),
@@ -29582,13 +29601,45 @@ var TaskReconcilePendingTransactions = class TaskReconcilePendingTransactions ex
29582
29601
  ...quarantined.staleOutpoints.length > 0 ? { outpoints: quarantined.staleOutpoints.join(",") } : {}
29583
29602
  });
29584
29603
  await req.updateStorageDynamicProperties(sp, trx);
29604
+ } else if (failedParentTxids.length > 0) {
29605
+ const quarantined = await quarantineReqInputsFromFailedParents(req, failedParentTxids, sp, trx);
29606
+ req.addHistoryNote({
29607
+ when: (/* @__PURE__ */ new Date()).toISOString(),
29608
+ what: "proactiveFailedParentQuarantine",
29609
+ checked: quarantined.checked,
29610
+ confirmed: quarantined.staleConfirmed,
29611
+ parentTxids: failedParentTxids.join(","),
29612
+ ...quarantined.staleOutpoints.length > 0 ? { outpoints: quarantined.staleOutpoints.join(",") } : {}
29613
+ });
29614
+ await req.updateStorageDynamicProperties(sp, trx);
29585
29615
  }
29586
- applied = true;
29616
+ applied = { result };
29587
29617
  });
29588
29618
  });
29589
- if (applied) this.monitor.callOnTransactionStatusChanged(reqApi.txid, result.providerStatus ?? "REJECTED");
29619
+ if (applied) this.monitor.callOnTransactionStatusChanged(reqApi.txid, applied.result.providerStatus ?? "REJECTED");
29590
29620
  return applied;
29591
29621
  }
29622
+ async findFailedLocalParentTxids(req, storage, trx) {
29623
+ const parentTxids = [...new Set(Transaction.fromBinary(req.rawTx).inputs.map((input) => input.sourceTXID).filter((txid) => txid != null && txid !== ""))];
29624
+ const failed = [];
29625
+ for (const txid of parentTxids) if ((await storage.findProvenTxReqs({
29626
+ partial: { txid },
29627
+ trx
29628
+ })).some((parent) => parent.status === "invalid" || parent.status === "doubleSpend")) failed.push(txid);
29629
+ return failed;
29630
+ }
29631
+ failedParentVerdict(txid, failedParentTxids) {
29632
+ if (failedParentTxids.length === 0) return void 0;
29633
+ return {
29634
+ txid,
29635
+ status: "unknown",
29636
+ depth: void 0,
29637
+ terminal: true,
29638
+ inputConflict: false,
29639
+ providerStatus: "LOCAL_PARENT_FAILED",
29640
+ description: `Local parent request is terminal: ${failedParentTxids.join(",")}`.slice(0, 512)
29641
+ };
29642
+ }
29592
29643
  };
29593
29644
  //#endregion
29594
29645
  //#region ../src/monitor/tasks/TaskReviewProvenTxs.ts