@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.
@@ -5973,6 +5973,18 @@ function requireConclusiveUtxo(classification) {
5973
5973
  }
5974
5974
  //#endregion
5975
5975
  //#region ../src/storage/methods/reconcileFailedTransactionInputs.ts
5976
+ async function quarantineLocalInputs(localInputs, storage, trx) {
5977
+ const staleOutpoints = /* @__PURE__ */ new Set();
5978
+ for (const { output, outpoint } of localInputs) {
5979
+ await storage.updateOutput(verifyId(output.outputId), { spendable: false }, trx);
5980
+ staleOutpoints.add(`${outpoint.txid}.${outpoint.vout}`);
5981
+ }
5982
+ return {
5983
+ checked: localInputs.length,
5984
+ staleConfirmed: localInputs.length,
5985
+ staleOutpoints: [...staleOutpoints]
5986
+ };
5987
+ }
5976
5988
  async function findLocalInputs(req, storage, trx) {
5977
5989
  const outpoints = _bsv_sdk.Transaction.fromBinary(req.rawTx).inputs.map((input) => ({
5978
5990
  txid: input.sourceTXID ?? "",
@@ -6012,21 +6024,22 @@ async function findLocalInputs(req, storage, trx) {
6012
6024
  * able to repair the transaction through the ordinary proof recovery path.
6013
6025
  */
6014
6026
  async function quarantineReqInputs(req, storage, trx, logger) {
6015
- const localInputs = await findLocalInputs(req, storage, trx);
6016
- const staleOutpoints = /* @__PURE__ */ new Set();
6017
- for (const { output, outpoint } of localInputs) {
6018
- await storage.updateOutput(verifyId(output.outputId), { spendable: false }, trx);
6019
- staleOutpoints.add(`${outpoint.txid}.${outpoint.vout}`);
6020
- }
6021
- const result = {
6022
- checked: localInputs.length,
6023
- staleConfirmed: localInputs.length,
6024
- staleOutpoints: [...staleOutpoints]
6025
- };
6027
+ const result = await quarantineLocalInputs(await findLocalInputs(req, storage, trx), storage, trx);
6026
6028
  if (result.staleConfirmed > 0) logger?.log(`quarantineReqInputs: ${result.staleConfirmed} local input copy/copies quarantined for txid=${req.txid}`);
6027
6029
  return result;
6028
6030
  }
6029
6031
  /**
6032
+ * Keep only inputs created by a locally terminal parent unavailable after the
6033
+ * failed-child transition releases its allocations. Other inputs may still be
6034
+ * valid UTXOs and must remain reusable.
6035
+ */
6036
+ async function quarantineReqInputsFromFailedParents(req, failedParentTxids, storage, trx, logger) {
6037
+ const failedParents = new Set(failedParentTxids);
6038
+ const result = await quarantineLocalInputs((await findLocalInputs(req, storage, trx)).filter(({ outpoint }) => failedParents.has(outpoint.txid)), storage, trx);
6039
+ if (result.staleConfirmed > 0) logger?.log(`quarantineReqInputsFromFailedParents: ${result.staleConfirmed} local parent output copy/copies quarantined for txid=${req.txid}`);
6040
+ return result;
6041
+ }
6042
+ /**
6030
6043
  * Ask the configured UTXO-provider collection to classify the request's local
6031
6044
  * inputs and mark only positively confirmed spent outputs unavailable.
6032
6045
  * Provider errors and a collection with no providers are inconclusive and
@@ -29521,9 +29534,9 @@ var TaskReconcilePendingTransactions = class TaskReconcilePendingTransactions ex
29521
29534
  }
29522
29535
  async runTask() {
29523
29536
  const checkpoint = await this.getCheckpoint();
29524
- const updatedBefore = /* @__PURE__ */ new Date(Date.now() - this.minAgeMinutes * 60 * 1e3);
29537
+ const createdBefore = /* @__PURE__ */ new Date(Date.now() - this.minAgeMinutes * 60 * 1e3);
29525
29538
  const reqs = await this.findReqsToReview(checkpoint);
29526
- const eligible = reqs.filter((req) => req.updated_at <= updatedBefore);
29539
+ const eligible = reqs.filter((req) => req.created_at <= createdBefore);
29527
29540
  const provider = eligible.length === 0 ? void 0 : await this.monitor.services.getStatusForTxids(eligible.map((req) => req.txid));
29528
29541
  const results = new Map(provider?.results.map((result) => [result.txid, result]) ?? []);
29529
29542
  let reconciled = 0;
@@ -29532,17 +29545,18 @@ var TaskReconcilePendingTransactions = class TaskReconcilePendingTransactions ex
29532
29545
  const retained = [];
29533
29546
  for (const req of reqs) {
29534
29547
  const result = results.get(req.txid);
29535
- if (req.updated_at > updatedBefore || provider?.status !== "success" || result?.terminal !== true) {
29548
+ if (req.created_at > createdBefore) {
29536
29549
  retained.push(req);
29537
29550
  continue;
29538
29551
  }
29539
- if (!await this.applyTerminalVerdict(req, result, provider.name)) {
29552
+ const applied = await this.applyTerminalVerdict(req, provider?.status === "success" ? result : void 0, provider?.status === "success" ? provider.name : void 0, createdBefore);
29553
+ if (!applied) {
29540
29554
  retained.push(req);
29541
29555
  continue;
29542
29556
  }
29543
29557
  reconciled++;
29544
- if (result.inputConflict === true) inputConflicts++;
29545
- reviewLog += `reconciled ${req.provenTxReqId} ${req.txid} ${result.providerStatus ?? "terminal"} => ${result.inputConflict === true ? "doubleSpend" : "invalid"}\n`;
29558
+ if (applied.result.inputConflict === true) inputConflicts++;
29559
+ reviewLog += `reconciled ${req.provenTxReqId} ${req.txid} ${applied.result.providerStatus ?? "terminal"} => ${applied.result.inputConflict === true ? "doubleSpend" : "invalid"}\n`;
29546
29560
  }
29547
29561
  const fullPage = reqs.length >= this.reviewLimit;
29548
29562
  this.triggerNextMsecs = fullPage ? this.triggerQuickMsecs : this.triggerMsecs;
@@ -29587,13 +29601,18 @@ var TaskReconcilePendingTransactions = class TaskReconcilePendingTransactions ex
29587
29601
  reqs.sourceOffset = offset;
29588
29602
  return reqs;
29589
29603
  }
29590
- async applyTerminalVerdict(reqApi, result, provider) {
29591
- let applied = false;
29604
+ async applyTerminalVerdict(reqApi, providerResult, providerName, createdBefore) {
29605
+ let applied;
29592
29606
  await this.storage.runAsStorageProvider(async (sp) => {
29593
29607
  await sp.transaction(async (trx) => {
29594
29608
  const req = new EntityProvenTxReq(reqApi);
29595
29609
  await req.refreshFromStorage(sp, trx);
29596
29610
  if (!REVIEW_STATUSES.includes(req.status)) return;
29611
+ if (req.created_at > createdBefore) return;
29612
+ const failedParentTxids = await this.findFailedLocalParentTxids(req, sp, trx);
29613
+ const result = providerResult?.terminal === true ? providerResult : providerResult?.status === "known" || providerResult?.status === "mined" ? void 0 : this.failedParentVerdict(req.txid, failedParentTxids);
29614
+ if (result == null) return;
29615
+ const provider = providerResult?.terminal === true ? providerName ?? "transaction-status-provider" : "local-storage";
29597
29616
  req.status = result.inputConflict === true ? "doubleSpend" : "invalid";
29598
29617
  req.addHistoryNote({
29599
29618
  when: (/* @__PURE__ */ new Date()).toISOString(),
@@ -29617,13 +29636,45 @@ var TaskReconcilePendingTransactions = class TaskReconcilePendingTransactions ex
29617
29636
  ...quarantined.staleOutpoints.length > 0 ? { outpoints: quarantined.staleOutpoints.join(",") } : {}
29618
29637
  });
29619
29638
  await req.updateStorageDynamicProperties(sp, trx);
29639
+ } else if (failedParentTxids.length > 0) {
29640
+ const quarantined = await quarantineReqInputsFromFailedParents(req, failedParentTxids, sp, trx);
29641
+ req.addHistoryNote({
29642
+ when: (/* @__PURE__ */ new Date()).toISOString(),
29643
+ what: "proactiveFailedParentQuarantine",
29644
+ checked: quarantined.checked,
29645
+ confirmed: quarantined.staleConfirmed,
29646
+ parentTxids: failedParentTxids.join(","),
29647
+ ...quarantined.staleOutpoints.length > 0 ? { outpoints: quarantined.staleOutpoints.join(",") } : {}
29648
+ });
29649
+ await req.updateStorageDynamicProperties(sp, trx);
29620
29650
  }
29621
- applied = true;
29651
+ applied = { result };
29622
29652
  });
29623
29653
  });
29624
- if (applied) this.monitor.callOnTransactionStatusChanged(reqApi.txid, result.providerStatus ?? "REJECTED");
29654
+ if (applied) this.monitor.callOnTransactionStatusChanged(reqApi.txid, applied.result.providerStatus ?? "REJECTED");
29625
29655
  return applied;
29626
29656
  }
29657
+ async findFailedLocalParentTxids(req, storage, trx) {
29658
+ const parentTxids = [...new Set(_bsv_sdk.Transaction.fromBinary(req.rawTx).inputs.map((input) => input.sourceTXID).filter((txid) => txid != null && txid !== ""))];
29659
+ const failed = [];
29660
+ for (const txid of parentTxids) if ((await storage.findProvenTxReqs({
29661
+ partial: { txid },
29662
+ trx
29663
+ })).some((parent) => parent.status === "invalid" || parent.status === "doubleSpend")) failed.push(txid);
29664
+ return failed;
29665
+ }
29666
+ failedParentVerdict(txid, failedParentTxids) {
29667
+ if (failedParentTxids.length === 0) return void 0;
29668
+ return {
29669
+ txid,
29670
+ status: "unknown",
29671
+ depth: void 0,
29672
+ terminal: true,
29673
+ inputConflict: false,
29674
+ providerStatus: "LOCAL_PARENT_FAILED",
29675
+ description: `Local parent request is terminal: ${failedParentTxids.join(",")}`.slice(0, 512)
29676
+ };
29677
+ }
29627
29678
  };
29628
29679
  //#endregion
29629
29680
  //#region ../src/monitor/tasks/TaskReviewProvenTxs.ts