@bsv/wallet-toolbox-client 2.6.5 → 2.7.0

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.
@@ -140,23 +140,10 @@ var WalletError = class WalletError extends Error {
140
140
  * @returns stringified JSON representation of the error such that it can be desirialized to a WalletError.
141
141
  */
142
142
  static unknownToJson(error) {
143
- let json;
144
- let e;
145
- const t = typeof error;
146
- const ctorName = t === "object" && error !== null ? error.constructor : void 0;
147
- const ctor = ctorName != null && typeof ctorName.name === "string" ? ctorName.name : void 0;
148
- const name = t === "object" && error !== null && typeof error.name === "string" ? error.name : "";
149
- const message = t === "object" && error !== null && typeof error.message === "string" ? error.message : "";
150
- const hasToJson = t === "object" && typeof error?.toJson === "function";
151
- if (ctor != null && ctor !== "" && ctor.startsWith("WERR_") && hasToJson) json = error.toJson();
152
- else if (name !== "" && message !== "") {
153
- e = new WalletError(name, message);
154
- json = e.toJson();
155
- } else {
156
- e = new WalletError("WERR_UNKNOWN", String(error));
157
- json = e.toJson();
158
- }
159
- return json;
143
+ const candidate = error;
144
+ if ((error instanceof WalletError || candidate?.isError === true) && typeof candidate.toJson === "function") return candidate.toJson();
145
+ if (typeof candidate?.name === "string" && typeof candidate.message === "string") return new WalletError(candidate.name, candidate.message).toJson();
146
+ return new WalletError("WERR_UNKNOWN", String(error)).toJson();
160
147
  }
161
148
  };
162
149
  //#endregion
@@ -190,6 +177,47 @@ var WERR_INVALID_OPERATION = class extends WalletError {
190
177
  }
191
178
  };
192
179
  /**
180
+ * A destructive UTXO review could not obtain a conclusive verdict for every
181
+ * candidate. Consumers can match this name and retry later without parsing the
182
+ * human-readable message.
183
+ */
184
+ var WERR_UTXO_REVIEW_INCONCLUSIVE = class extends WalletError {
185
+ checked;
186
+ confirmedSpent;
187
+ unknown;
188
+ constructor(checked, confirmedSpent, unknown) {
189
+ super("WERR_UTXO_REVIEW_INCONCLUSIVE", `UTXO review was inconclusive for ${unknown} of ${checked} candidates; no outputs were changed.`);
190
+ this.checked = checked;
191
+ this.confirmedSpent = confirmedSpent;
192
+ this.unknown = unknown;
193
+ }
194
+ toJson() {
195
+ const obj = JSON.parse(super.toJson());
196
+ obj.checked = this.checked;
197
+ obj.confirmedSpent = this.confirmedSpent;
198
+ obj.unknown = this.unknown;
199
+ return JSON.stringify(obj);
200
+ }
201
+ };
202
+ /**
203
+ * An action-batch lifecycle operation cannot continue in the batch's current state.
204
+ */
205
+ var WERRActionBatchState = class extends WalletError {
206
+ state;
207
+ batchId;
208
+ constructor(state, batchId) {
209
+ super("WERR_ACTION_BATCH_STATE", `Action batch cannot continue because it is ${state}.`);
210
+ this.state = state;
211
+ this.batchId = batchId;
212
+ }
213
+ toJson() {
214
+ const obj = JSON.parse(super.toJson());
215
+ obj.state = this.state;
216
+ obj.batchId = this.batchId;
217
+ return JSON.stringify(obj);
218
+ }
219
+ };
220
+ /**
193
221
  * Unable to broadcast transaction at this time.
194
222
  */
195
223
  var WERR_BROADCAST_UNAVAILABLE = class extends WalletError {
@@ -387,6 +415,10 @@ function WalletErrorFromJson(json) {
387
415
  let e;
388
416
  const obj = json;
389
417
  switch (obj.name) {
418
+ case "WERR_ACTION_BATCH_STATE":
419
+ e = new WERRActionBatchState(obj.state, obj.batchId);
420
+ e.message = obj.message;
421
+ break;
390
422
  case "WERR_NOT_IMPLEMENTED":
391
423
  e = new WERR_NOT_IMPLEMENTED(obj.message);
392
424
  break;
@@ -396,6 +428,10 @@ function WalletErrorFromJson(json) {
396
428
  case "WERR_INVALID_OPERATION":
397
429
  e = new WERR_INVALID_OPERATION(obj.message);
398
430
  break;
431
+ case "WERR_UTXO_REVIEW_INCONCLUSIVE":
432
+ e = new WERR_UTXO_REVIEW_INCONCLUSIVE(obj.checked, obj.confirmedSpent, obj.unknown);
433
+ e.message = obj.message;
434
+ break;
399
435
  case "WERR_BROADCAST_UNAVAILABLE":
400
436
  e = new WERR_BROADCAST_UNAVAILABLE(obj.message);
401
437
  break;
@@ -472,9 +508,10 @@ const specOpWalletManagedUtxos = "284570a6213a74ba861c38b1cf790e1e400d9cf9324454
472
508
  /**
473
509
  * `listOutputs` special operation basket name value.
474
510
  *
475
- * Returns currently spendable wallet change outputs that fail to validate as unspent transaction outputs.
511
+ * Returns currently spendable wallet change outputs conclusively confirmed spent.
512
+ * The operation rejects if any candidate cannot be classified conclusively.
476
513
  *
477
- * Optional tag value 'release'. If present, updates invalid change outputs to not spendable.
514
+ * Optional tag value 'release'. If present, atomically updates only confirmed-spent change outputs to not spendable after rechecking current ownership and allocation state.
478
515
  *
479
516
  * Optional tag value 'all'. If present, processes all spendable true outputs, independent of baskets, but basket must be defined.
480
517
  */
@@ -804,6 +841,7 @@ var sdk_exports = /* @__PURE__ */ __exportAll({
804
841
  ProvenTxReqNonTerminalStatus: () => ProvenTxReqNonTerminalStatus,
805
842
  ProvenTxReqTerminalStatus: () => ProvenTxReqTerminalStatus,
806
843
  Validation: () => Validation$1,
844
+ WERR_ACTION_BATCH_STATE: () => WERRActionBatchState,
807
845
  WERR_BAD_REQUEST: () => WERR_BAD_REQUEST,
808
846
  WERR_BROADCAST_UNAVAILABLE: () => WERR_BROADCAST_UNAVAILABLE,
809
847
  WERR_INSUFFICIENT_FUNDS: () => WERR_INSUFFICIENT_FUNDS,
@@ -818,6 +856,7 @@ var sdk_exports = /* @__PURE__ */ __exportAll({
818
856
  WERR_NOT_IMPLEMENTED: () => WERR_NOT_IMPLEMENTED,
819
857
  WERR_REVIEW_ACTIONS: () => WERR_REVIEW_ACTIONS,
820
858
  WERR_UNAUTHORIZED: () => WERR_UNAUTHORIZED,
859
+ WERR_UTXO_REVIEW_INCONCLUSIVE: () => WERR_UTXO_REVIEW_INCONCLUSIVE,
821
860
  WalletError: () => WalletError,
822
861
  WalletErrorFromJson: () => WalletErrorFromJson,
823
862
  isCreateActionSpecOp: () => isCreateActionSpecOp,
@@ -984,8 +1023,8 @@ function toLookupNetworkPreset(chain) {
984
1023
  switch (chain) {
985
1024
  case "main": return "mainnet";
986
1025
  case "test": return "testnet";
1026
+ case "ttn": return "teratestnet";
987
1027
  case "stn":
988
- case "ttn":
989
1028
  case "tstn":
990
1029
  case "mock": return "local";
991
1030
  }
@@ -1356,6 +1395,42 @@ function makeBrc114ActionTimeLabel(unixMillis) {
1356
1395
  return `action time ${unixMillis}`;
1357
1396
  }
1358
1397
  //#endregion
1398
+ //#region ../src/utility/brc153ReferenceLabels.ts
1399
+ const BRC153_REFERENCE_PREFIX = "reference ";
1400
+ /**
1401
+ * Build the BRC-153 synthetic listActions label for an action reference.
1402
+ * Encodes reference bytes as lowercase hex (labels are lowercased by validation).
1403
+ */
1404
+ function makeBrc153ReferenceLabel(referenceBase64) {
1405
+ const bytes = Utils.toArray(referenceBase64, "base64");
1406
+ return `${BRC153_REFERENCE_PREFIX}${Utils.toHex(bytes)}`;
1407
+ }
1408
+ /**
1409
+ * True iff the label uses the reserved BRC-153 reference prefix.
1410
+ */
1411
+ function isBrc153ReferenceLabel(label) {
1412
+ return label.startsWith(BRC153_REFERENCE_PREFIX);
1413
+ }
1414
+ /**
1415
+ * Ensure labels contain exactly one wallet-authored `reference <hex>`.
1416
+ * Any existing reserved-prefix labels are replaced.
1417
+ */
1418
+ function applyBrc153ReferenceLabel(labels, referenceBase64) {
1419
+ const next = labels.filter((label) => !isBrc153ReferenceLabel(label));
1420
+ next.push(makeBrc153ReferenceLabel(referenceBase64));
1421
+ return next;
1422
+ }
1423
+ /**
1424
+ * Parse a BRC-153 synthetic reference label back to the BRC-100 Base64String reference.
1425
+ * Returns undefined if the label is not a valid reference label.
1426
+ */
1427
+ function parseBrc153ReferenceLabel(label) {
1428
+ if (!label.startsWith("reference ")) return void 0;
1429
+ const hex = label.slice(10);
1430
+ if (hex.length === 0 || hex.length % 2 !== 0 || !/^[0-9a-f]+$/.test(hex)) return void 0;
1431
+ return Utils.toBase64(Utils.toArray(hex, "hex"));
1432
+ }
1433
+ //#endregion
1359
1434
  //#region ../src/storage/schema/entities/EntityBase.ts
1360
1435
  var EntityBase = class {
1361
1436
  api;
@@ -2059,6 +2134,58 @@ var EntityOutput = class EntityOutput extends EntityBase {
2059
2134
  }
2060
2135
  };
2061
2136
  //#endregion
2137
+ //#region ../src/storage/methods/managedChangePolicy.ts
2138
+ /** Historical default retained only to identify untouched wallet baskets. */
2139
+ const LEGACY_MANAGED_CHANGE_MINIMUM_SATOSHIS = 32;
2140
+ /**
2141
+ * Default liquidity policy for wallet-managed change.
2142
+ *
2143
+ * The preferred minimum is deliberately much larger than the dust threshold.
2144
+ * Dust answers "can this output ever be spent economically?"; this value
2145
+ * answers "is this output useful as an independently selectable liquidity
2146
+ * unit at contemporary fee rates?".
2147
+ */
2148
+ const DEFAULT_MANAGED_CHANGE_TARGET_UTXOS = 144;
2149
+ const DEFAULT_MANAGED_CHANGE_MINIMUM_SATOSHIS = 5e3;
2150
+ const DEFAULT_MANAGED_CHANGE_MAX_OUTPUTS_PER_ACTION = 8;
2151
+ const DEFAULT_MANAGED_CHANGE_MIGRATION_INPUTS_PER_ACTION = 4;
2152
+ const DEFAULT_MANAGED_CHANGE_PENDING_COMPARISON_INPUTS = 16;
2153
+ /** True only for the exact historical default that is safe to auto-upgrade. */
2154
+ function isLegacyManagedChangeBasketDefault(basket) {
2155
+ return basket.name === "default" && basket.numberOfDesiredUTXOs === 144 && basket.minimumDesiredUTXOValue === 32;
2156
+ }
2157
+ /**
2158
+ * Normalize a legacy default while retaining every other field and every
2159
+ * operator-selected non-default value. Used by migrations, sync, and restore.
2160
+ */
2161
+ function upgradeLegacyManagedChangeBasketDefault(basket) {
2162
+ if (!isLegacyManagedChangeBasketDefault(basket)) return basket;
2163
+ return {
2164
+ ...basket,
2165
+ minimumDesiredUTXOValue: DEFAULT_MANAGED_CHANGE_MINIMUM_SATOSHIS
2166
+ };
2167
+ }
2168
+ function defaultManagedChangePolicy() {
2169
+ return {
2170
+ maxOutputsPerAction: 8,
2171
+ migrationInputsPerAction: 4,
2172
+ pendingComparisonInputs: 16
2173
+ };
2174
+ }
2175
+ function validateManagedChangePolicy(options) {
2176
+ const policy = {
2177
+ ...defaultManagedChangePolicy(),
2178
+ ...options
2179
+ };
2180
+ const validateLimit = (value, name, minimum) => {
2181
+ if (value !== -1 && (!Number.isSafeInteger(value) || value < minimum)) throw new WERR_INVALID_PARAMETER(`managedChangePolicy.${name}`, `${minimum === 0 ? "a non-negative" : "a positive"} safe integer or -1 for unlimited`);
2182
+ };
2183
+ validateLimit(policy.maxOutputsPerAction, "maxOutputsPerAction", 1);
2184
+ validateLimit(policy.migrationInputsPerAction, "migrationInputsPerAction", 0);
2185
+ validateLimit(policy.pendingComparisonInputs, "pendingComparisonInputs", 1);
2186
+ return policy;
2187
+ }
2188
+ //#endregion
2062
2189
  //#region ../src/storage/schema/entities/EntityOutputBasket.ts
2063
2190
  var EntityOutputBasket = class EntityOutputBasket extends EntityBase {
2064
2191
  constructor(api) {
@@ -2161,13 +2288,15 @@ var EntityOutputBasket = class EntityOutputBasket extends EntityBase {
2161
2288
  this.userId = userId;
2162
2289
  this.name ||= "default";
2163
2290
  this.basketId = 0;
2291
+ this.api = upgradeLegacyManagedChangeBasketDefault(this.api);
2164
2292
  this.basketId = await storage.insertOutputBasket(this.toApi(), trx);
2165
2293
  }
2166
2294
  async mergeExisting(storage, since, ei, syncMap, trx) {
2167
2295
  let wasMerged = false;
2168
2296
  if (ei.updated_at > this.updated_at) {
2169
- this.minimumDesiredUTXOValue = ei.minimumDesiredUTXOValue;
2170
- this.numberOfDesiredUTXOs = ei.numberOfDesiredUTXOs;
2297
+ const incoming = upgradeLegacyManagedChangeBasketDefault(ei);
2298
+ this.minimumDesiredUTXOValue = incoming.minimumDesiredUTXOValue;
2299
+ this.numberOfDesiredUTXOs = incoming.numberOfDesiredUTXOs;
2171
2300
  this.isDeleted = ei.isDeleted;
2172
2301
  this.updated_at = new Date(Math.max(ei.updated_at.getTime(), this.updated_at.getTime()));
2173
2302
  await storage.updateOutputBasket(this.id, this.toApi(), trx);
@@ -4337,6 +4466,12 @@ var WalletStorageManager = class {
4337
4466
  async renewActionBatch(batchId) {
4338
4467
  return await this.runAsWriter(async (writer) => await writer.renewActionBatch(await this.getAuth(true), batchId));
4339
4468
  }
4469
+ async resumeActionBatch(args) {
4470
+ return await this.runAsWriter(async (writer) => {
4471
+ if (writer.resumeActionBatch == null) throw new WERR_NOT_IMPLEMENTED("action batch resume is not available");
4472
+ return await writer.resumeActionBatch(await this.getAuth(true), args);
4473
+ });
4474
+ }
4340
4475
  async prepareActionBatchCommit(manifest) {
4341
4476
  return await this.runAsWriter(async (writer) => await writer.prepareActionBatchCommit(await this.getAuth(true), manifest));
4342
4477
  }
@@ -4691,8 +4826,20 @@ var WalletStorageManager = class {
4691
4826
  });
4692
4827
  return log;
4693
4828
  }
4829
+ /**
4830
+ * Return the remote HTTP(S) endpoint for a managed store, if any.
4831
+ *
4832
+ * Duck-types `endpointUrl` on the provider (as set by `StorageClientBase`).
4833
+ * Do **not** key this off `constructor.name === 'StorageClient'`: production
4834
+ * minifiers (Vite/esbuild/webpack) rename classes, so that check fails and
4835
+ * every remote store reports `endpointURL: undefined` even though the URL is
4836
+ * present. Consumers that match backups by URL (e.g. making a remote store
4837
+ * primary) then fail while sync still works, because sync walks `_backups`
4838
+ * without needing `endpointURL`.
4839
+ */
4694
4840
  getStoreEndpointURL(store) {
4695
- if (store.storage.constructor.name === "StorageClient") return store.storage.endpointUrl;
4841
+ const url = store.storage.endpointUrl;
4842
+ if (typeof url === "string" && url.length > 0) return url;
4696
4843
  }
4697
4844
  getStores() {
4698
4845
  const stores = [];
@@ -5118,7 +5265,7 @@ async function getBeefForTransaction(storage, txid, options) {
5118
5265
  const concurrency = normalizeConcurrency(options.maxConcurrency);
5119
5266
  while (frontier.length > 0) {
5120
5267
  const current = frontier.filter((item) => needsResolution(beef, item.txid, hasKnownTxid));
5121
- const resolved = await mapWithConcurrency(current, concurrency, async (item) => await resolveBeefForTransaction(storage, item.txid, options, hasKnownTxid, item.depth));
5268
+ const resolved = await mapWithConcurrency$1(current, concurrency, async (item) => await resolveBeefForTransaction(storage, item.txid, options, hasKnownTxid, item.depth));
5122
5269
  const next = [];
5123
5270
  for (let i = 0; i < resolved.length; i++) {
5124
5271
  const result = resolved[i];
@@ -5175,7 +5322,7 @@ function requiresSingleRootPolicy(options) {
5175
5322
  return options.ignoreStorage === true || options.minProofLevel !== void 0 || options.chainTracker != null || options.skipInvalidProofs === true;
5176
5323
  }
5177
5324
  async function mergeSingleRootFragments(storage, roots, options, beef) {
5178
- const fragments = await mapWithConcurrency(roots.filter((txid) => beef.findTxid(txid) == null), normalizeConcurrency(options.maxConcurrency), async (txid) => await getBeefForTransaction(storage, txid, {
5325
+ const fragments = await mapWithConcurrency$1(roots.filter((txid) => beef.findTxid(txid) == null), normalizeConcurrency(options.maxConcurrency), async (txid) => await getBeefForTransaction(storage, txid, {
5179
5326
  ...options,
5180
5327
  mergeToBeef: void 0
5181
5328
  }));
@@ -5294,7 +5441,7 @@ function needsResolution(beef, txid, hasKnownTxid) {
5294
5441
  async function mergeMissingFragments(storage, beef, missing, options) {
5295
5442
  if (missing.length === 0) return;
5296
5443
  if (options.ignoreServices === true) throw new WERR_INVALID_PARAMETER(`txid ${missing[0].txid}`, `valid transaction on chain ${storage.chain}`);
5297
- const fragments = await mapWithConcurrency(missing, normalizeConcurrency(options.maxConcurrency), async (item) => await getBeefForTransaction(storage, item.txid, {
5444
+ const fragments = await mapWithConcurrency$1(missing, normalizeConcurrency(options.maxConcurrency), async (item) => await getBeefForTransaction(storage, item.txid, {
5298
5445
  ...options,
5299
5446
  ignoreStorage: true,
5300
5447
  mergeToBeef: void 0
@@ -5317,7 +5464,7 @@ function makeKnownTxidLookup$1(knownTxids) {
5317
5464
  function normalizeConcurrency(value = 8) {
5318
5465
  return Number.isFinite(value) ? Math.max(1, Math.min(32, Math.floor(value))) : 8;
5319
5466
  }
5320
- async function mapWithConcurrency(values, concurrency, mapper) {
5467
+ async function mapWithConcurrency$1(values, concurrency, mapper) {
5321
5468
  const results = Array.from({ length: values.length }, () => void 0);
5322
5469
  let cursor = 0;
5323
5470
  await Promise.all(Array.from({ length: Math.min(concurrency, values.length) }, async () => {
@@ -5728,6 +5875,177 @@ async function commitNewTxToStorage(storage, userId, vargs) {
5728
5875
  return r;
5729
5876
  }
5730
5877
  //#endregion
5878
+ //#region ../src/services/classifyOutputUtxo.ts
5879
+ async function mapWithConcurrency(values, maxConcurrency, worker) {
5880
+ const results = Array.from({ length: values.length });
5881
+ let nextIndex = 0;
5882
+ const workers = Array.from({ length: Math.min(maxConcurrency, values.length) }, async () => {
5883
+ while (nextIndex < values.length) {
5884
+ const index = nextIndex++;
5885
+ results[index] = await worker(values[index], index);
5886
+ }
5887
+ });
5888
+ await Promise.all(workers);
5889
+ return results;
5890
+ }
5891
+ /**
5892
+ * Classify an output without ever converting provider absence or failure into
5893
+ * positive spent evidence. This helper deliberately catches provider throws so
5894
+ * batch callers can finish classifying every candidate before deciding whether
5895
+ * a destructive operation is safe.
5896
+ */
5897
+ async function classifyOutputUtxo(services, output) {
5898
+ if (output.lockingScript == null || output.lockingScript.length === 0) return {
5899
+ verdict: "unknown",
5900
+ provider: "<no-locking-script>",
5901
+ error: new WERR_INVALID_PARAMETER("output.lockingScript", "validated by storage provider validateOutputScript.")
5902
+ };
5903
+ if (typeof output.txid !== "string" || !/^[0-9a-fA-F]{64}$/.test(output.txid) || !Number.isSafeInteger(output.vout) || output.vout < 0) return {
5904
+ verdict: "unknown",
5905
+ provider: "<invalid-outpoint>",
5906
+ error: new WERR_INVALID_PARAMETER("output outpoint", "a 32-byte transaction ID and non-negative vout")
5907
+ };
5908
+ try {
5909
+ const hash = services.hashOutputScript(Utils.toHex(output.lockingScript));
5910
+ const result = await services.getUtxoStatus(hash, void 0, `${output.txid}.${output.vout}`);
5911
+ const provider = typeof result?.name === "string" && result.name.length > 0 ? result.name : "<invalid-provider-result>";
5912
+ if (result?.status !== "success" || typeof result.isUtxo !== "boolean" || provider === "<invalid-provider-result>") return {
5913
+ verdict: "unknown",
5914
+ provider,
5915
+ error: result?.error
5916
+ };
5917
+ return {
5918
+ verdict: result.isUtxo ? "unspent" : "spent",
5919
+ provider
5920
+ };
5921
+ } catch (error) {
5922
+ return {
5923
+ verdict: "unknown",
5924
+ provider: "<provider-error>",
5925
+ error
5926
+ };
5927
+ }
5928
+ }
5929
+ /**
5930
+ * Convert an internal tri-state classification to the historical boolean
5931
+ * contract only when a provider supplied a conclusive answer.
5932
+ */
5933
+ function requireConclusiveUtxo(classification) {
5934
+ if (classification.verdict === "unspent") return true;
5935
+ if (classification.verdict === "spent") return false;
5936
+ if (classification.error instanceof Error) throw classification.error;
5937
+ throw new WERR_INVALID_OPERATION(`UTXO provider ${classification.provider} did not return a conclusive result.`);
5938
+ }
5939
+ //#endregion
5940
+ //#region ../src/storage/methods/reconcileFailedTransactionInputs.ts
5941
+ async function findLocalInputs(req, storage, trx) {
5942
+ const outpoints = Transaction.fromBinary(req.rawTx).inputs.map((input) => ({
5943
+ txid: input.sourceTXID ?? "",
5944
+ vout: input.sourceOutputIndex ?? 0
5945
+ })).filter((outpoint) => outpoint.txid !== "");
5946
+ if (outpoints.length === 0) return [];
5947
+ const transactions = await storage.findTransactions({
5948
+ partial: { txid: req.txid },
5949
+ noRawTx: true,
5950
+ trx
5951
+ });
5952
+ const userIds = [...new Set(transactions.map((transaction) => transaction.userId))];
5953
+ const localInputs = [];
5954
+ const localKeys = /* @__PURE__ */ new Set();
5955
+ for (const userId of userIds) {
5956
+ const byOutpoint = await storage.findOutputsByOutpoints(userId, outpoints, trx);
5957
+ for (const outpoint of outpoints) {
5958
+ const output = byOutpoint[`${outpoint.txid}.${outpoint.vout}`];
5959
+ const localKey = `${userId}:${outpoint.txid}.${outpoint.vout}`;
5960
+ if (output != null && !localKeys.has(localKey)) {
5961
+ localKeys.add(localKey);
5962
+ localInputs.push({
5963
+ output,
5964
+ outpoint
5965
+ });
5966
+ }
5967
+ }
5968
+ }
5969
+ return localInputs;
5970
+ }
5971
+ /**
5972
+ * Conservatively quarantine every locally-owned input of a transaction after
5973
+ * Arcade supplies explicit missing-input/conflict evidence. This path is
5974
+ * deliberately independent of explorer or UTXO providers: a positive
5975
+ * broadcaster verdict is enough to stop the failed transaction from feeding
5976
+ * the same inputs into another action. A later validated mined proof remains
5977
+ * able to repair the transaction through the ordinary proof recovery path.
5978
+ */
5979
+ 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
+ };
5991
+ if (result.staleConfirmed > 0) logger?.log(`quarantineReqInputs: ${result.staleConfirmed} local input copy/copies quarantined for txid=${req.txid}`);
5992
+ return result;
5993
+ }
5994
+ /**
5995
+ * Ask the configured UTXO-provider collection to classify the request's local
5996
+ * inputs and mark only positively confirmed spent outputs unavailable.
5997
+ * Provider errors and a collection with no providers are inconclusive and
5998
+ * never count as spent.
5999
+ */
6000
+ async function markConfirmedStaleReqInputs(req, storage, services, trx, logger) {
6001
+ const result = {
6002
+ checked: 0,
6003
+ staleConfirmed: 0,
6004
+ staleOutpoints: []
6005
+ };
6006
+ const localInputs = await findLocalInputs(req, storage, trx);
6007
+ const verdicts = /* @__PURE__ */ new Map();
6008
+ const uniqueInputs = /* @__PURE__ */ new Map();
6009
+ for (const localInput of localInputs) {
6010
+ const key = `${localInput.outpoint.txid}.${localInput.outpoint.vout}`;
6011
+ if (!uniqueInputs.has(key)) uniqueInputs.set(key, localInput);
6012
+ }
6013
+ await mapWithConcurrency([...uniqueInputs.values()], 4, async ({ output, outpoint }) => {
6014
+ const key = `${outpoint.txid}.${outpoint.vout}`;
6015
+ if (output.lockingScript == null) try {
6016
+ await storage.validateOutputScript(output, trx);
6017
+ } catch {
6018
+ verdicts.set(key, "inconclusive");
6019
+ return;
6020
+ }
6021
+ if (output.lockingScript == null) {
6022
+ verdicts.set(key, "inconclusive");
6023
+ return;
6024
+ }
6025
+ const classification = await classifyOutputUtxo(services, output);
6026
+ let verdict;
6027
+ if (classification.verdict === "unknown") verdict = "inconclusive";
6028
+ else if (classification.verdict === "unspent") verdict = "utxo";
6029
+ else verdict = "stale";
6030
+ verdicts.set(key, verdict);
6031
+ });
6032
+ const staleOutpoints = /* @__PURE__ */ new Set();
6033
+ for (const { output, outpoint } of localInputs) {
6034
+ const key = `${outpoint.txid}.${outpoint.vout}`;
6035
+ const verdict = verdicts.get(key);
6036
+ if (verdict == null || verdict === "inconclusive") continue;
6037
+ result.checked++;
6038
+ if (verdict === "stale") {
6039
+ await storage.updateOutput(verifyId(output.outputId), { spendable: false }, trx);
6040
+ result.staleConfirmed++;
6041
+ staleOutpoints.add(key);
6042
+ }
6043
+ }
6044
+ result.staleOutpoints = [...staleOutpoints];
6045
+ if (result.staleConfirmed > 0) logger?.log(`markConfirmedStaleReqInputs: ${result.staleConfirmed} of ${result.checked} local input copy/copies confirmed spent for txid=${req.txid}`);
6046
+ return result;
6047
+ }
6048
+ //#endregion
5731
6049
  //#region ../src/storage/methods/attemptToPostReqsToNetwork.ts
5732
6050
  /**
5733
6051
  * Attempt to post one or more `ProvenTxReq` with status 'unsent'
@@ -6067,64 +6385,7 @@ async function confirmDoubleSpend(ar, beef, storage, services, logger) {
6067
6385
  * that were actually evicted (added to history note for diagnostics).
6068
6386
  */
6069
6387
  async function markStaleInputsAsSpent(ar, storage, services, trx, logger) {
6070
- const result = {
6071
- checked: 0,
6072
- staleConfirmed: 0,
6073
- staleOutpoints: []
6074
- };
6075
- const req = ar.vreq.req;
6076
- const txIds = req.notify.transactionIds;
6077
- if (txIds == null || txIds.length === 0) return result;
6078
- const txRecord = (await storage.findTransactions({
6079
- partial: { transactionId: txIds[0] },
6080
- noRawTx: true,
6081
- trx
6082
- }))[0];
6083
- if (txRecord == null) return result;
6084
- const userId = txRecord.userId;
6085
- const outpoints = Transaction.fromBinary(req.rawTx).inputs.map((i) => ({
6086
- txid: i.sourceTXID ?? "",
6087
- vout: i.sourceOutputIndex ?? 0
6088
- })).filter((o) => o.txid !== "");
6089
- if (outpoints.length === 0) return result;
6090
- const byOutpoint = await storage.findOutputsByOutpoints(userId, outpoints, trx);
6091
- const checks = await Promise.all(outpoints.map(async (outpoint) => {
6092
- const localOutput = byOutpoint[`${outpoint.txid}.${outpoint.vout}`];
6093
- if (localOutput == null) return { kind: "skipped" };
6094
- if (localOutput.lockingScript == null) try {
6095
- await storage.validateOutputScript(localOutput, trx);
6096
- } catch {
6097
- return { kind: "skipped" };
6098
- }
6099
- let isStillUtxo;
6100
- try {
6101
- isStillUtxo = await services.isUtxo(localOutput);
6102
- } catch {
6103
- return {
6104
- kind: "service-error",
6105
- localOutput
6106
- };
6107
- }
6108
- return isStillUtxo ? {
6109
- kind: "still-utxo",
6110
- localOutput
6111
- } : {
6112
- kind: "stale",
6113
- localOutput,
6114
- outpoint
6115
- };
6116
- }));
6117
- for (const c of checks) {
6118
- if (c.kind === "skipped") continue;
6119
- result.checked++;
6120
- if (c.kind === "stale") {
6121
- await storage.updateOutput(c.localOutput.outputId, { spendable: false }, trx);
6122
- result.staleConfirmed++;
6123
- result.staleOutpoints.push(`${c.outpoint.txid}.${c.outpoint.vout}`);
6124
- }
6125
- }
6126
- if (result.staleConfirmed > 0) logger?.log(`markStaleInputsAsSpent: ${result.staleConfirmed} of ${result.checked} input(s) confirmed-spent on chain for txid=${req.txid}`);
6127
- return result;
6388
+ return await markConfirmedStaleReqInputs(ar.vreq.req, storage, services, trx, logger);
6128
6389
  }
6129
6390
  //#endregion
6130
6391
  //#region ../src/storage/methods/listCertificates.ts
@@ -6277,6 +6538,75 @@ function removeDustOutputs(changeOutputs, dustFloor) {
6277
6538
  largest.satoshis += removed.satoshis;
6278
6539
  }
6279
6540
  }
6541
+ async function migrateLegacyChangeInputs(request) {
6542
+ const { params, result } = request;
6543
+ if (!params.surplusPoolShaping || result.changeOutputs.length === 0) return;
6544
+ if (request.targetNetCount <= request.netChangeCount()) return;
6545
+ const migrationLimit = params.maxMigrationInputs === -1 ? Number.MAX_SAFE_INTEGER : params.maxMigrationInputs ?? 0;
6546
+ for (let migrated = 0; migrated < migrationLimit; migrated++) {
6547
+ const marginalInputFee = request.feeTarget(1) - request.feeTarget();
6548
+ const candidate = await request.allocateChangeInput(0);
6549
+ if (candidate == null) break;
6550
+ if (candidate.satoshis >= params.changeInitialSatoshis || candidate.satoshis <= marginalInputFee) {
6551
+ await request.releaseChangeInput(candidate.outputId);
6552
+ break;
6553
+ }
6554
+ request.recordAllocatedInput(candidate);
6555
+ }
6556
+ }
6557
+ /**
6558
+ * Materialize the first managed-change output from surplus that is already in
6559
+ * the transaction. This is especially important for explicit/fixed inputs:
6560
+ * their value can fully fund an action before the allocator loop runs, but the
6561
+ * shaping policy must still capture the remainder without gathering another
6562
+ * wallet-managed input.
6563
+ */
6564
+ function materializeSurplusChangeOutput(request) {
6565
+ const { params, result } = request;
6566
+ if (!params.surplusPoolShaping || result.changeOutputs.length > 0) return;
6567
+ const availableAfterOutputFee = request.feeExcess(0, 1);
6568
+ if (availableAfterOutputFee < request.dustFloor) return;
6569
+ result.changeOutputs.push({
6570
+ satoshis: Math.min(availableAfterOutputFee, Math.max(request.dustFloor, params.changeFirstSatoshis)),
6571
+ lockingScriptLength: params.changeLockingScriptLength
6572
+ });
6573
+ request.feeExcess();
6574
+ }
6575
+ /**
6576
+ * Preserve the historical compatibility retry when another input can make a
6577
+ * viable change output, except when surplus-only shaping has nothing economic
6578
+ * to return. In that case the bounded remainder stays in the miner fee instead
6579
+ * of manufacturing change from an additional wallet input.
6580
+ */
6581
+ async function requireViableChangeOrRetainBoundedFee(request) {
6582
+ const { params, result } = request;
6583
+ const feeExcessNow = request.feeExcess();
6584
+ if (result.changeOutputs.length > 0 || feeExcessNow <= 0) return;
6585
+ if (params.surplusPoolShaping === true && request.feeExcess(0, 1) < request.dustFloor) return;
6586
+ const minimumChange = Math.max(request.dustFloor, params.changeFirstSatoshis);
6587
+ const totalSatoshisNeeded = request.spending() + request.feeTarget(0, 1) + minimumChange;
6588
+ const moreSatoshisNeeded = Math.max(1, totalSatoshisNeeded - request.funding());
6589
+ await request.releaseAllocatedChangeInputs();
6590
+ throw new WERR_INSUFFICIENT_FUNDS(totalSatoshisNeeded, moreSatoshisNeeded);
6591
+ }
6592
+ function shapeSurplusChangeOutputs(request) {
6593
+ const { params, result } = request;
6594
+ if (!params.surplusPoolShaping || result.changeOutputs.length !== 1) return;
6595
+ if (request.targetNetCount <= request.netChangeCount()) return;
6596
+ const originalSatoshis = result.changeOutputs[0].satoshis;
6597
+ const desiredOutputs = Math.min(request.maxChangeOutputs, Math.max(1, request.targetNetCount + result.allocatedChangeInputs.length));
6598
+ for (let count = desiredOutputs; count > 1; count--) {
6599
+ const addedOutputs = count - 1;
6600
+ const distributable = originalSatoshis - (request.feeTarget(0, addedOutputs) - request.feeTarget());
6601
+ if (distributable < count * params.changeInitialSatoshis) continue;
6602
+ result.changeOutputs = Array.from({ length: count }, () => ({
6603
+ satoshis: params.changeInitialSatoshis,
6604
+ lockingScriptLength: params.changeLockingScriptLength
6605
+ }));
6606
+ distributeExcessFees(result.changeOutputs, params.changeInitialSatoshis, distributable - count * params.changeInitialSatoshis, request.rand);
6607
+ break;
6608
+ }
6609
+ }
6280
6610
  /**
6281
6611
  * Simplifications:
6282
6612
  * - only support one change type with fixed length scripts.
@@ -6348,7 +6678,8 @@ async function generateChangeSdkCore(params, allocateChangeInput, releaseChangeI
6348
6678
  * Applies the per-transaction limit so that the UTXO pool grows
6349
6679
  * gradually rather than all at once.
6350
6680
  */
6351
- const maxChangeOutputs = params.maxChangeOutputs ?? 8;
6681
+ const maxChangeOutputs = params.maxChangeOutputs === -1 ? Number.MAX_SAFE_INTEGER : params.maxChangeOutputs ?? 8;
6682
+ const surplusPoolShaping = params.surplusPoolShaping === true;
6352
6683
  const randomVals = [...params.randomVals || []];
6353
6684
  const nextRandomVal = () => {
6354
6685
  let val = 0;
@@ -6431,6 +6762,7 @@ async function generateChangeSdkCore(params, allocateChangeInput, releaseChangeI
6431
6762
  return r.changeOutputs.length - r.allocatedChangeInputs.length;
6432
6763
  };
6433
6764
  const addOutputToBalanceNewInput = () => {
6765
+ if (surplusPoolShaping) return false;
6434
6766
  if (!hasTargetNetCount) return false;
6435
6767
  if (r.changeOutputs.length >= maxChangeOutputs) return false;
6436
6768
  return netChangeCount() - 1 < targetNetCount;
@@ -6446,6 +6778,7 @@ async function generateChangeSdkCore(params, allocateChangeInput, releaseChangeI
6446
6778
  feeExcessNow = feeExcess();
6447
6779
  };
6448
6780
  const addDesiredChangeOutputs = () => {
6781
+ if (surplusPoolShaping) return;
6449
6782
  while (r.changeOutputs.length < maxChangeOutputs && (hasTargetNetCount && targetNetCount > netChangeCount() || r.changeOutputs.length === 0 && feeExcess() > 0)) {
6450
6783
  const satoshis = r.changeOutputs.length === 0 ? Math.max(dustFloor, params.changeFirstSatoshis) : Math.max(dustFloor, params.changeInitialSatoshis);
6451
6784
  r.changeOutputs.push({
@@ -6461,7 +6794,8 @@ async function generateChangeSdkCore(params, allocateChangeInput, releaseChangeI
6461
6794
  if (removingOutputs || feeExcess() <= 0) return;
6462
6795
  if (!((ao === 1 || r.changeOutputs.length === 0) && r.changeOutputs.length < maxChangeOutputs)) return;
6463
6796
  const cap = r.changeOutputs.length === 0 ? params.changeFirstSatoshis : params.changeInitialSatoshis;
6464
- const satoshis = Math.min(feeExcess(), Math.max(dustFloor, cap));
6797
+ const outputFunding = surplusPoolShaping ? feeExcess(0, 1) : feeExcess();
6798
+ const satoshis = Math.min(outputFunding, Math.max(dustFloor, cap));
6465
6799
  if (satoshis >= dustFloor) r.changeOutputs.push({
6466
6800
  satoshis,
6467
6801
  lockingScriptLength: params.changeLockingScriptLength
@@ -6506,6 +6840,18 @@ async function generateChangeSdkCore(params, allocateChangeInput, releaseChangeI
6506
6840
  };
6507
6841
  }
6508
6842
  /**
6843
+ * The action may already be funded entirely by explicit/fixed inputs. In
6844
+ * that case the allocator loop never runs, so capture the existing surplus
6845
+ * here before the no-change compatibility guard. This operation cannot
6846
+ * allocate an input; bounded legacy migration remains a separate step.
6847
+ */
6848
+ materializeSurplusChangeOutput({
6849
+ params,
6850
+ result: r,
6851
+ dustFloor,
6852
+ feeExcess
6853
+ });
6854
+ /**
6509
6855
  * Trigger an account funding event if we don't have enough to cover this transaction.
6510
6856
  */
6511
6857
  if (feeExcess() < 0) {
@@ -6516,19 +6862,63 @@ async function generateChangeSdkCore(params, allocateChangeInput, releaseChangeI
6516
6862
  }
6517
6863
  /**
6518
6864
  * If needed, seek funding to avoid overspending on fees without a change output to recapture it.
6865
+ * An economically unreturnable shaping remainder stays in the miner fee;
6866
+ * gathering another input solely to manufacture change would violate
6867
+ * surplus-only shaping and make the action less efficient.
6519
6868
  */
6520
- if (r.changeOutputs.length === 0 && feeExcessNow > 0) {
6521
- const minimumChange = Math.max(dustFloor, params.changeFirstSatoshis);
6522
- const totalSatoshisNeeded = spending() + feeTarget(0, 1) + minimumChange;
6523
- const moreSatoshisNeeded = Math.max(1, totalSatoshisNeeded - funding());
6524
- await releaseAllocatedChangeInputs();
6525
- throw new WERR_INSUFFICIENT_FUNDS(totalSatoshisNeeded, moreSatoshisNeeded);
6526
- }
6869
+ await requireViableChangeOrRetainBoundedFee({
6870
+ params,
6871
+ result: r,
6872
+ dustFloor,
6873
+ feeExcess,
6874
+ feeTarget,
6875
+ funding,
6876
+ spending,
6877
+ releaseAllocatedChangeInputs
6878
+ });
6879
+ /**
6880
+ * Progressively retire economically useful legacy fragments without ever
6881
+ * making them necessary for the requested action. The target of zero asks
6882
+ * canonical allocators for their smallest remaining output. A candidate at
6883
+ * or above the preferred value is not legacy migration material and is
6884
+ * immediately released.
6885
+ */
6886
+ await migrateLegacyChangeInputs({
6887
+ params,
6888
+ result: r,
6889
+ targetNetCount,
6890
+ netChangeCount,
6891
+ feeTarget,
6892
+ allocateChangeInput,
6893
+ releaseChangeInput,
6894
+ recordAllocatedInput: (candidate) => {
6895
+ r.allocatedChangeInputs.push(candidate);
6896
+ allocatedFunding += candidate.satoshis;
6897
+ feeExcessNow = feeExcess();
6898
+ }
6899
+ });
6527
6900
  /**
6528
6901
  * Distribute the excess fees across the changeOutputs added.
6529
6902
  */
6530
6903
  feeExcessNow = distributeExcessFees(r.changeOutputs, params.changeInitialSatoshis, feeExcessNow, rand);
6531
6904
  /**
6905
+ * Pool growth is funded only from the surplus already present in the
6906
+ * transaction. Splitting one change output increases the serialized fee;
6907
+ * that exact delta is deducted before assigning the new outputs. If the
6908
+ * preferred minimum cannot be met, the transaction retains one smaller
6909
+ * output instead of gathering more inputs or refusing an otherwise valid
6910
+ * action.
6911
+ */
6912
+ shapeSurplusChangeOutputs({
6913
+ params,
6914
+ result: r,
6915
+ targetNetCount,
6916
+ netChangeCount,
6917
+ maxChangeOutputs,
6918
+ feeTarget,
6919
+ rand
6920
+ });
6921
+ /**
6532
6922
  * Remove any change outputs that ended up below the dust floor after distribution.
6533
6923
  * Consolidates removed satoshis into the largest remaining output.
6534
6924
  */
@@ -6559,7 +6949,11 @@ function validateGenerateChangeSdkResult(params, r) {
6559
6949
  ok = false;
6560
6950
  }
6561
6951
  const feeRequired = Math.ceil((r.size || 0) / 1e3 * (r.satsPerKb || 0));
6562
- if (feeRequired !== r.fee) {
6952
+ const minSpendTxSize = transactionSize([params.changeUnlockingScriptLength], [params.changeLockingScriptLength]);
6953
+ const dustFloor = Math.max(1, Math.ceil(minSpendTxSize / 1e3 * (r.satsPerKb || 0)) * 2);
6954
+ const feeWithChangeOutput = Math.ceil(((r.size || 0) + transactionOutputSize(params.changeLockingScriptLength)) / 1e3 * (r.satsPerKb || 0));
6955
+ const isBoundedUnreturnableShapingSurplus = params.surplusPoolShaping === true && r.changeOutputs.length === 0 && r.fee > feeRequired && r.fee - feeWithChangeOutput < dustFloor;
6956
+ if (feeRequired !== r.fee && !isBoundedUnreturnableShapingSurplus) {
6563
6957
  log += `required fee error ${feeRequired} !== ${r.fee};`;
6564
6958
  ok = false;
6565
6959
  }
@@ -6597,6 +6991,8 @@ function validateGenerateChangeSdkParams(params) {
6597
6991
  params.feeModel = validateStorageFeeModel(params.feeModel);
6598
6992
  if (params.feeModel.model !== "sat/kb") throw new WERR_INVALID_PARAMETER("feeModel.model", "'sat/kb'");
6599
6993
  Validation.validateOptionalInteger(params.targetNetCount, "targetNetCount");
6994
+ if (params.maxChangeOutputs !== -1) Validation.validateOptionalInteger(params.maxChangeOutputs, "maxChangeOutputs", 1);
6995
+ if (params.maxMigrationInputs !== -1) Validation.validateOptionalInteger(params.maxMigrationInputs, "maxMigrationInputs", 0);
6600
6996
  Validation.validateSatoshis(params.changeFirstSatoshis, "changeFirstSatoshis", 1);
6601
6997
  Validation.validateSatoshis(params.changeInitialSatoshis, "changeInitialSatoshis", 1);
6602
6998
  Validation.validateInteger(params.changeLockingScriptLength, "changeLockingScriptLength");
@@ -8064,6 +8460,8 @@ async function planFunding(state, args, explicit, noSendChange) {
8064
8460
  const allocated = /* @__PURE__ */ new Map();
8065
8461
  const noSend = [...noSendChange];
8066
8462
  const changeBasket = state.begin.changeBasket;
8463
+ const policy = validateManagedChangePolicy(state.begin.managedChangePolicy);
8464
+ const preferredSatoshis = Math.max(1, changeBasket.minimumDesiredUTXOValue);
8067
8465
  const params = {
8068
8466
  fixedInputs: explicit.map((output, index) => ({
8069
8467
  satoshis: output.satoshis,
@@ -8077,11 +8475,14 @@ async function planFunding(state, args, explicit, noSendChange) {
8077
8475
  lockingScriptLength: 25
8078
8476
  }] : []],
8079
8477
  feeModel: state.begin.feeModel,
8080
- changeInitialSatoshis: Math.max(1, changeBasket.minimumDesiredUTXOValue),
8081
- changeFirstSatoshis: Math.max(1, Math.round(changeBasket.minimumDesiredUTXOValue / 4)),
8478
+ changeInitialSatoshis: preferredSatoshis,
8479
+ changeFirstSatoshis: preferredSatoshis,
8082
8480
  changeLockingScriptLength: 25,
8083
8481
  changeUnlockingScriptLength: 107,
8084
8482
  targetNetCount: changeBasket.numberOfDesiredUTXOs - state.estimatedChangeCount,
8483
+ maxChangeOutputs: policy.maxOutputsPerAction,
8484
+ surplusPoolShaping: true,
8485
+ maxMigrationInputs: policy.migrationInputsPerAction,
8085
8486
  randomVals: args.randomVals
8086
8487
  };
8087
8488
  const allocate = async (targetSatoshis, exactSatoshis) => {
@@ -8100,7 +8501,18 @@ async function planFunding(state, args, explicit, noSendChange) {
8100
8501
  allocated.delete(outputId);
8101
8502
  if (noSendChange.includes(output)) noSend.push(output);
8102
8503
  };
8103
- const result = await generateChangeSdk(params, allocate, release, args.logger);
8504
+ let result;
8505
+ try {
8506
+ result = await generateChangeSdk(params, allocate, release, args.logger);
8507
+ } catch (error) {
8508
+ if (!(error instanceof WERR_INSUFFICIENT_FUNDS)) throw error;
8509
+ result = await generateChangeSdk({
8510
+ ...params,
8511
+ changeFirstSatoshis: 1,
8512
+ surplusPoolShaping: false,
8513
+ maxMigrationInputs: 0
8514
+ }, allocate, release, args.logger);
8515
+ }
8104
8516
  return {
8105
8517
  allocated: result.allocatedChangeInputs.map((input) => verifyTruthy(allocated.get(input.outputId))),
8106
8518
  changeSatoshis: result.changeOutputs.map((output) => output.satoshis),
@@ -8358,6 +8770,19 @@ async function compressActionBatchPackItems(items, encoding, maxBytes, maxItems)
8358
8770
  function mergeUnique(values) {
8359
8771
  return [...new Set(values)];
8360
8772
  }
8773
+ function isActionBatchStateError(error) {
8774
+ if (error instanceof WERRActionBatchState) return true;
8775
+ if (error == null || typeof error !== "object") return false;
8776
+ const candidate = error;
8777
+ return (candidate.name === "WERR_ACTION_BATCH_STATE" || candidate.code === "WERR_ACTION_BATCH_STATE") && typeof candidate.state === "string";
8778
+ }
8779
+ function isActionBatchCapacityError(error) {
8780
+ if (error == null || typeof error !== "object") return false;
8781
+ const candidate = error;
8782
+ const code = candidate.name === "Error" ? candidate.code : candidate.name ?? candidate.code;
8783
+ if (code === "WERR_INVALID_PARAMETER" && candidate.parameter === "firstAction") return true;
8784
+ return code === "WERR_INVALID_OPERATION" && typeof candidate.message === "string" && candidate.message.includes("action batch") && (candidate.message.includes("maximum") || candidate.message.includes("reserve"));
8785
+ }
8361
8786
  function nextGeometricTarget(value) {
8362
8787
  return Math.min(Number.MAX_SAFE_INTEGER, value * 2);
8363
8788
  }
@@ -8404,6 +8829,7 @@ var ActionBatchWorkspace = class {
8404
8829
  planned = /* @__PURE__ */ new Map();
8405
8830
  actions = [];
8406
8831
  lockingScripts = /* @__PURE__ */ new Map();
8832
+ canResume;
8407
8833
  ewmaConfirmedInputs = 1;
8408
8834
  ewmaConfirmedSatoshis = 0;
8409
8835
  hasConfirmedSample = false;
@@ -8423,6 +8849,7 @@ var ActionBatchWorkspace = class {
8423
8849
  this.batchId = begin.batchId;
8424
8850
  this.state = makePlannerState(begin, firstAction);
8425
8851
  this.expiresAt = Date.parse(begin.expiresAt);
8852
+ this.canResume = capabilities.resume === true && wallet.storage.resumeActionBatch != null;
8426
8853
  this.eagerUploadLanes = Array.from({ length: capabilities.packedUploads == null ? 1 : Math.max(1, capabilities.maxConcurrentUploads) }, async () => {});
8427
8854
  }
8428
8855
  get usesCompactManifest() {
@@ -8436,6 +8863,15 @@ var ActionBatchWorkspace = class {
8436
8863
  if (this.actions.some((action) => action.reference === reference || action.txid === reference)) return true;
8437
8864
  return Object.entries(this.wallet.pendingSignActions).some(([pendingReference, pending]) => this.planned.has(pendingReference) && pending.tx.id("hex") === reference);
8438
8865
  }
8866
+ ownsTransaction(txid) {
8867
+ return this.actions.some((action) => action.txid === txid) || Object.entries(this.wallet.pendingSignActions).some(([reference, pending]) => this.planned.has(reference) && pending.tx.id("hex") === txid);
8868
+ }
8869
+ references(args) {
8870
+ return [...args.inputs.map((input) => input.outpoint), ...args.options.noSendChange].some((outpoint) => this.state.staged.has(`${outpoint.txid}.${outpoint.vout}`)) || args.options.sendWith.some((txid) => this.ownsTransaction(txid));
8871
+ }
8872
+ referencesSendWith(txids) {
8873
+ return txids.some((txid) => this.ownsTransaction(txid));
8874
+ }
8439
8875
  get isEmpty() {
8440
8876
  return this.planned.size === 0 && this.actions.length === 0;
8441
8877
  }
@@ -8560,12 +8996,32 @@ var ActionBatchWorkspace = class {
8560
8996
  satoshis: extension.reservedOutputs.reduce((sum, output) => sum + output.satoshis, 0)
8561
8997
  };
8562
8998
  }
8999
+ async resume() {
9000
+ const outpoints = [...new Map([...this.state.reserved.values(), ...this.state.explicit.values()].filter((output) => output.txid != null).map((output) => [`${output.txid}.${output.vout}`, {
9001
+ txid: output.txid,
9002
+ vout: output.vout
9003
+ }])).values()];
9004
+ const result = await this.wallet.storage.resumeActionBatch({
9005
+ batchId: this.batchId,
9006
+ outpoints
9007
+ });
9008
+ this.expiresAt = Date.parse(result.expiresAt);
9009
+ }
8563
9010
  async renewIfNeeded() {
8564
- if (Date.now() >= this.expiresAt) return;
9011
+ if (Date.now() >= this.expiresAt) {
9012
+ if (this.canResume) await this.resume();
9013
+ return;
9014
+ }
8565
9015
  const renewAt = this.expiresAt - this.capabilities.leaseMs * .2;
8566
9016
  if (Date.now() < renewAt) return;
8567
9017
  this.renewal ??= this.wallet.storage.renewActionBatch(this.batchId).then((result) => {
8568
9018
  this.expiresAt = Date.parse(result.expiresAt);
9019
+ }).catch(async (error) => {
9020
+ if (isActionBatchStateError(error) && error.state === "expired") {
9021
+ if (this.canResume) await this.resume();
9022
+ return;
9023
+ }
9024
+ throw error;
8569
9025
  }).finally(() => {
8570
9026
  this.renewal = void 0;
8571
9027
  });
@@ -8996,24 +9452,42 @@ var ActionBatchController = class {
8996
9452
  this.workspace = new ActionBatchWorkspace(this.wallet, begin, args, capabilities);
8997
9453
  return this.workspace;
8998
9454
  }
9455
+ async retire(workspace) {
9456
+ await workspace.abort().catch(() => void 0);
9457
+ if (this.workspace === workspace) this.workspace = void 0;
9458
+ }
9459
+ async recover(workspace, input, isDelayed = false, resume = false) {
9460
+ try {
9461
+ if (resume) await workspace.resume();
9462
+ return Array.isArray(input) ? await workspace.commit(input, isDelayed) : await workspace.plan(input);
9463
+ } catch (error) {
9464
+ if (!resume && isActionBatchStateError(error) && error.state === "expired" && workspace.canResume) return await this.recover(workspace, input, isDelayed, true);
9465
+ if (isActionBatchStateError(error) && error.state !== "expired") await this.retire(workspace);
9466
+ throw error;
9467
+ }
9468
+ }
8999
9469
  async plan(args) {
9000
9470
  return await this.runExclusive(async () => {
9001
9471
  if (!args.isNewTx) return void 0;
9002
- let workspace = this.workspace;
9003
- let beganWorkspace = false;
9004
- if (workspace == null) {
9005
- if (!args.isNoSend) return void 0;
9006
- workspace = await this.begin(args);
9007
- if (workspace == null) return void 0;
9008
- beganWorkspace = true;
9472
+ const workspace = this.workspace;
9473
+ if (workspace != null) {
9474
+ if (!workspace.references(args)) return void 0;
9475
+ return await this.recover(workspace, args);
9009
9476
  }
9477
+ if (!args.isNoSend) return void 0;
9478
+ let begun;
9010
9479
  try {
9011
- return await workspace.plan(args);
9480
+ begun = await this.begin(args);
9012
9481
  } catch (error) {
9013
- if (beganWorkspace) {
9014
- await workspace.abort();
9015
- this.workspace = void 0;
9016
- }
9482
+ if (isActionBatchCapacityError(error)) return void 0;
9483
+ throw error;
9484
+ }
9485
+ if (begun == null) return void 0;
9486
+ try {
9487
+ return await begun.plan(args);
9488
+ } catch (error) {
9489
+ await this.retire(begun);
9490
+ if (isActionBatchCapacityError(error)) return void 0;
9017
9491
  throw error;
9018
9492
  }
9019
9493
  });
@@ -9023,11 +9497,21 @@ var ActionBatchController = class {
9023
9497
  const workspace = this.workspace;
9024
9498
  if (workspace == null) return void 0;
9025
9499
  if (prior != null && !workspace.ownsReference(prior.reference)) return void 0;
9500
+ if (prior == null && !workspace.referencesSendWith(args.options.sendWith)) return void 0;
9026
9501
  if (prior != null) workspace.stage(prior, args);
9027
- if (!(args.isSendWith || prior != null && !args.isNoSend)) return { sendWithResults: [] };
9502
+ const referencesWorkspace = workspace.referencesSendWith(args.options.sendWith);
9503
+ if (prior != null && args.isNoSend && args.isSendWith && !referencesWorkspace) return await this.wallet.storage.processAction({
9504
+ isNewTx: false,
9505
+ isSendWith: true,
9506
+ isNoSend: false,
9507
+ isDelayed: args.isDelayed,
9508
+ sendWith: [...args.options.sendWith],
9509
+ logger: args.logger
9510
+ });
9511
+ if (!(referencesWorkspace || prior != null && !args.isNoSend)) return { sendWithResults: [] };
9028
9512
  const sendWith = [...args.options.sendWith];
9029
9513
  if (prior != null && !args.isNoSend && !sendWith.includes(prior.tx.id("hex"))) sendWith.push(prior.tx.id("hex"));
9030
- const result = await workspace.commit(sendWith, args.isDelayed);
9514
+ const result = await this.recover(workspace, sendWith, args.isDelayed);
9031
9515
  this.workspace = void 0;
9032
9516
  return result;
9033
9517
  });
@@ -9771,14 +10255,15 @@ var Wallet = class {
9771
10255
  }
9772
10256
  /**
9773
10257
  * Uses `listOutputs` special operation to review the spendability via `Services` of
9774
- * outputs currently considered spendable. Returns the outputs that fail to verify.
10258
+ * outputs currently considered spendable. Returns only outputs conclusively
10259
+ * confirmed spent. Rejects the review if any provider result is inconclusive.
9775
10260
  *
9776
10261
  * Ignores the `limit` and `offset` properties.
9777
10262
  *
9778
10263
  * @param all Defaults to false. If false, only change outputs ('default' basket) are reviewed. If true, all spendable outputs are reviewed.
9779
- * @param release Defaults to false. If true, sets outputs that fail to verify to un-spendable (spendable: false)
10264
+ * @param release Defaults to false. If true, atomically sets conclusively spent outputs to un-spendable (spendable: false). No outputs change if any candidate is inconclusive.
9780
10265
  * @param optionalArgs Optional. Additional tags will constrain the outputs processed.
9781
- * @returns outputs which are/where considered spendable but currently fail to verify as spendable.
10266
+ * @returns outputs previously considered spendable but conclusively confirmed spent.
9782
10267
  */
9783
10268
  async reviewSpendableOutputs(all = false, release = false, optionalArgs) {
9784
10269
  const args = {
@@ -9986,7 +10471,7 @@ async function createActionCore(storage, auth, vargs, parent) {
9986
10471
  });
9987
10472
  const feeModel = validateStorageFeeModel(storage.feeModel);
9988
10473
  logger?.log(`validated fee model ${JSON.stringify(feeModel)}`);
9989
- const initialFundingPlan = await prepareFundingPlanWithSendingFallback(storage, [
10474
+ const initialFundingPlan = await prepareFundingPlanWithLiquidityPolicy(storage, [
9990
10475
  userId,
9991
10476
  vargs,
9992
10477
  xinputs,
@@ -10574,7 +11059,9 @@ async function traceStorageStep(storage, name, parent, attributes, callback) {
10574
11059
  attributes
10575
11060
  }, async (span) => await callback(span));
10576
11061
  }
10577
- function makeFundingParams(vargs, xinputs, xoutputs, changeBasket, feeModel, availableChangeCount) {
11062
+ function makeFundingParams(args) {
11063
+ const { storage, vargs, xinputs, xoutputs, changeBasket, feeModel, healthyChangeCount, compatibilityFallback } = args;
11064
+ const preferredSatoshis = Math.max(1, changeBasket.minimumDesiredUTXOValue);
10578
11065
  return {
10579
11066
  fixedInputs: xinputs.map((input) => ({
10580
11067
  satoshis: input.satoshis,
@@ -10585,35 +11072,52 @@ function makeFundingParams(vargs, xinputs, xoutputs, changeBasket, feeModel, ava
10585
11072
  lockingScriptLength: output.lockingScript.length / 2
10586
11073
  })),
10587
11074
  feeModel,
10588
- changeInitialSatoshis: Math.max(1, changeBasket.minimumDesiredUTXOValue),
10589
- changeFirstSatoshis: Math.max(1, Math.round(changeBasket.minimumDesiredUTXOValue / 4)),
11075
+ changeInitialSatoshis: preferredSatoshis,
11076
+ changeFirstSatoshis: compatibilityFallback ? 1 : preferredSatoshis,
10590
11077
  changeLockingScriptLength: 25,
10591
11078
  changeUnlockingScriptLength: 107,
10592
- targetNetCount: changeBasket.numberOfDesiredUTXOs - availableChangeCount,
11079
+ targetNetCount: changeBasket.numberOfDesiredUTXOs - healthyChangeCount,
11080
+ maxChangeOutputs: storage.managedChangePolicy.maxOutputsPerAction,
11081
+ surplusPoolShaping: !compatibilityFallback,
11082
+ maxMigrationInputs: compatibilityFallback ? 0 : storage.managedChangePolicy.migrationInputsPerAction,
10593
11083
  randomVals: vargs.randomVals
10594
11084
  };
10595
11085
  }
10596
- async function prepareFundingPlan(storage, context) {
10597
- const [userId, vargs, xinputs, xoutputs, changeBasket, noSendChangeIn, feeModel, excludeSending, parent, trx] = context;
10598
- const candidates = await traceStorageStep(storage, "wallet.storage.create_action.funding_candidates", parent, { "funding.exclude_sending": excludeSending }, async (span) => {
10599
- const outputs = await storage.findAvailableManagedChangeInputCandidates(userId, changeBasket.basketId, excludeSending, trx);
10600
- span?.end({ attributes: {
10601
- "funding.candidate_count": outputs.length,
10602
- "funding.candidate_satoshis": outputs.reduce((sum, output) => sum + output.satoshis, 0)
10603
- } });
10604
- return outputs;
10605
- });
11086
+ async function buildFundingPlan(storage, context, candidates, eligibleStatuses, policyTier, compatibilityFallback, parent) {
11087
+ const [userId, vargs, xinputs, xoutputs, changeBasket, noSendChangeIn, feeModel] = context;
10606
11088
  const noSendIds = new Set(noSendChangeIn.map((output) => output.outputId));
10607
- const available = candidates.filter((output) => !noSendIds.has(output.outputId));
10608
- const params = makeFundingParams(vargs, xinputs, xoutputs, changeBasket, feeModel, candidates.length);
11089
+ const available = candidates.filter((output) => !noSendIds.has(output.outputId) && eligibleStatuses.includes(output.transactionStatus));
11090
+ const preferredSatoshis = Math.max(1, changeBasket.minimumDesiredUTXOValue);
11091
+ const params = makeFundingParams({
11092
+ storage,
11093
+ vargs,
11094
+ xinputs,
11095
+ xoutputs,
11096
+ changeBasket,
11097
+ feeModel,
11098
+ healthyChangeCount: compatibilityFallback ? candidates.filter((output) => eligibleStatuses.includes(output.transactionStatus)).length : candidates.filter((output) => output.satoshis >= preferredSatoshis).length,
11099
+ compatibilityFallback
11100
+ });
11101
+ const noSendStatuses = await storage.findTransactionStatusesByIds(userId, noSendChangeIn.map((output) => output.transactionId));
11102
+ const plannedNoSend = noSendChangeIn.map((output) => ({
11103
+ outputId: output.outputId,
11104
+ transactionId: output.transactionId,
11105
+ satoshis: output.satoshis,
11106
+ txid: output.txid,
11107
+ vout: output.vout,
11108
+ transactionStatus: noSendStatuses.get(output.transactionId) ?? "nosend"
11109
+ }));
11110
+ const claimStatuses = [.../* @__PURE__ */ new Set([...eligibleStatuses, ...plannedNoSend.map((output) => output.transactionStatus)])];
10609
11111
  return await traceStorageStep(storage, "wallet.storage.create_action.funding_plan", parent, {
10610
11112
  "funding.candidate_count": available.length,
10611
- "funding.no_send_change_count": noSendChangeIn.length
11113
+ "funding.no_send_change_count": noSendChangeIn.length,
11114
+ "funding.policy_tier": policyTier,
11115
+ "funding.compatibility_fallback": compatibilityFallback
10612
11116
  }, async (span) => {
10613
11117
  const allocated = /* @__PURE__ */ new Map();
10614
11118
  const availableSelector = new CanonicalChangeSelector(available);
10615
- const noSend = [...noSendChangeIn];
10616
- const noSendById = new Map(noSendChangeIn.map((output) => [output.outputId, output]));
11119
+ const noSend = [...plannedNoSend];
11120
+ const noSendById = new Map(plannedNoSend.map((output) => [output.outputId, output]));
10617
11121
  const allocate = async (targetSatoshis, exactSatoshis) => {
10618
11122
  let output = noSend.pop();
10619
11123
  output ??= availableSelector.take(targetSatoshis, exactSatoshis);
@@ -10644,31 +11148,118 @@ async function prepareFundingPlan(storage, context) {
10644
11148
  result,
10645
11149
  selected,
10646
11150
  availableChangeCount: candidates.length,
10647
- excludeSending
11151
+ eligibleStatuses: claimStatuses,
11152
+ policyTier,
11153
+ compatibilityFallback
10648
11154
  };
10649
11155
  });
10650
11156
  }
10651
- async function prepareFundingPlanWithSendingFallback(storage, context, parent, trx) {
10652
- const excludeSending = !context[1].isDelayed;
11157
+ async function fundingPlanSerializedCost(storage, plan, knownTxids) {
11158
+ const txids = [...new Set(plan.selected.map((candidate) => verifyTruthy(candidate.txid)))];
11159
+ if (txids.length === 0) return plan.result.size;
10653
11160
  try {
10654
- return await prepareFundingPlan(storage, [
10655
- ...context,
10656
- excludeSending,
10657
- parent,
10658
- trx
10659
- ]);
11161
+ const beef = await storage.getBeefForTransactions(txids, {
11162
+ knownTxids,
11163
+ ignoreStorage: false,
11164
+ ignoreServices: true,
11165
+ ignoreNewProven: false
11166
+ });
11167
+ return plan.result.size + beef.toUint8Array().length;
11168
+ } catch {
11169
+ return Number.MAX_SAFE_INTEGER;
11170
+ }
11171
+ }
11172
+ const FUNDING_TIERS = [
11173
+ {
11174
+ policyTier: "completed",
11175
+ statuses: ["completed"]
11176
+ },
11177
+ {
11178
+ policyTier: "unproven",
11179
+ statuses: ["completed", "unproven"]
11180
+ },
11181
+ {
11182
+ policyTier: "sending",
11183
+ statuses: [
11184
+ "completed",
11185
+ "unproven",
11186
+ "sending"
11187
+ ]
11188
+ }
11189
+ ];
11190
+ async function resolveFundingCandidates(storage, userId, basketId, parent, trx) {
11191
+ const rawCandidates = await traceStorageStep(storage, "wallet.storage.create_action.funding_candidates", parent, { "funding.include_pending": true }, async (span) => {
11192
+ const outputs = await storage.findAvailableManagedChangeInputCandidates(userId, basketId, false, trx);
11193
+ span?.end({ attributes: {
11194
+ "funding.candidate_count": outputs.length,
11195
+ "funding.candidate_satoshis": outputs.reduce((sum, output) => sum + output.satoshis, 0),
11196
+ "funding.completed_candidate_count": outputs.filter((output) => output.transactionStatus === "completed").length,
11197
+ "funding.unproven_candidate_count": outputs.filter((output) => output.transactionStatus === "unproven").length,
11198
+ "funding.sending_candidate_count": outputs.filter((output) => output.transactionStatus === "sending").length
11199
+ } });
11200
+ return outputs;
11201
+ });
11202
+ const missingStatusIds = rawCandidates.filter((output) => output.transactionStatus == null).map((output) => output.transactionId);
11203
+ const missingStatuses = await storage.findTransactionStatusesByIds(userId, missingStatusIds, trx);
11204
+ const candidates = rawCandidates.map((output) => ({
11205
+ ...output,
11206
+ transactionStatus: output.transactionStatus ?? missingStatuses.get(output.transactionId)
11207
+ }));
11208
+ if (candidates.some((output) => output.transactionStatus == null)) throw new WERR_INTERNAL("managed change candidate is missing its source transaction status");
11209
+ return candidates;
11210
+ }
11211
+ async function buildFundingPlanForTier(storage, context, candidates, tier, parent) {
11212
+ try {
11213
+ return { plan: await buildFundingPlan(storage, context, candidates, tier.statuses, tier.policyTier, false, parent) };
10660
11214
  } catch (error) {
10661
- if (!excludeSending || !(error instanceof WERR_INSUFFICIENT_FUNDS)) throw error;
10662
- return await prepareFundingPlan(storage, [
10663
- ...context,
10664
- false,
10665
- parent,
10666
- trx
10667
- ]);
11215
+ if (!(error instanceof WERR_INSUFFICIENT_FUNDS)) throw error;
11216
+ }
11217
+ try {
11218
+ return { plan: await buildFundingPlan(storage, context, candidates, tier.statuses, "compatibility", true, parent) };
11219
+ } catch (error) {
11220
+ if (!(error instanceof WERR_INSUFFICIENT_FUNDS)) throw error;
11221
+ return { fundingError: error };
11222
+ }
11223
+ }
11224
+ function firstPlanNeedsNoPendingComparison(storage, plan) {
11225
+ const threshold = storage.managedChangePolicy.pendingComparisonInputs;
11226
+ return threshold === -1 || plan.selected.length <= threshold;
11227
+ }
11228
+ async function chooseLowestSerializedFundingPlan(storage, plans, knownTxids) {
11229
+ let chosen = plans[0];
11230
+ let chosenCost = await fundingPlanSerializedCost(storage, chosen, knownTxids);
11231
+ for (const alternative of plans.slice(1)) {
11232
+ const cost = await fundingPlanSerializedCost(storage, alternative, knownTxids);
11233
+ if (cost < chosenCost) {
11234
+ chosen = alternative;
11235
+ chosenCost = cost;
11236
+ }
11237
+ }
11238
+ return chosen;
11239
+ }
11240
+ async function prepareFundingPlanWithLiquidityPolicy(storage, context, parent, trx) {
11241
+ const [userId, vargs, , , changeBasket] = context;
11242
+ const candidates = await resolveFundingCandidates(storage, userId, changeBasket.basketId, parent, trx);
11243
+ const successful = [];
11244
+ let fundingError;
11245
+ for (const tier of FUNDING_TIERS) {
11246
+ const attempted = await buildFundingPlanForTier(storage, context, candidates, tier, parent);
11247
+ fundingError = attempted.fundingError ?? fundingError;
11248
+ const plan = attempted.plan;
11249
+ if (plan != null) {
11250
+ successful.push(plan);
11251
+ if (successful.length === 1 && firstPlanNeedsNoPendingComparison(storage, plan)) return plan;
11252
+ }
11253
+ }
11254
+ if (successful.length > 0) {
11255
+ if (successful.length === 1) return successful[0];
11256
+ return await chooseLowestSerializedFundingPlan(storage, successful, vargs.options.knownTxids);
10668
11257
  }
11258
+ if (fundingError != null) throw fundingError;
11259
+ throw new WERR_INTERNAL("funding policy produced neither a plan nor an error");
10669
11260
  }
10670
11261
  async function claimFundingPlan(storage, request) {
10671
- const [userId, basketId, excludeSending, transactionId, noSendChangeIn, plan, trx] = request;
11262
+ const [userId, basketId, eligibleStatuses, transactionId, noSendChangeIn, plan, trx] = request;
10672
11263
  if (plan.selected.length === 0) return {
10673
11264
  outputs: [],
10674
11265
  sourceTransactionCount: 0,
@@ -10676,10 +11267,8 @@ async function claimFundingPlan(storage, request) {
10676
11267
  scriptSourceTransactionCount: 0
10677
11268
  };
10678
11269
  const noSendIds = new Set(noSendChangeIn.map((output) => output.outputId));
10679
- const statuses = ["completed", "unproven"];
10680
- if (!excludeSending) statuses.push("sending");
10681
11270
  const claim = await storage.transaction(async (claimTrx) => {
10682
- const currentById = await storage.findFundingOutputsForUpdate(userId, plan.selected.map((output) => output.outputId), statuses, claimTrx);
11271
+ const currentById = await storage.findFundingOutputsForUpdate(userId, plan.selected.map((output) => output.outputId), eligibleStatuses, claimTrx);
10683
11272
  const transactionIds = [...new Set(Object.values(currentById).map((output) => output.transactionId))];
10684
11273
  const claimed = [];
10685
11274
  for (const planned of plan.selected) {
@@ -10749,7 +11338,7 @@ async function fundNewTransactionSdk(storage, userId, vargs, ctx, initialPlan, p
10749
11338
  const claim = await claimFundingPlan(storage, [
10750
11339
  userId,
10751
11340
  ctx.changeBasket.basketId,
10752
- plan.excludeSending,
11341
+ plan.eligibleStatuses,
10753
11342
  ctx.transactionId,
10754
11343
  ctx.noSendChangeIn,
10755
11344
  plan,
@@ -10767,7 +11356,7 @@ async function fundNewTransactionSdk(storage, userId, vargs, ctx, initialPlan, p
10767
11356
  }
10768
11357
  if (claim.conflict === "noSendChange") throw new WERR_INVALID_PARAMETER("noSendChange", "outputs that remain spendable during action planning");
10769
11358
  retryCount++;
10770
- plan = await prepareFundingPlanWithSendingFallback(storage, [
11359
+ plan = await prepareFundingPlanWithLiquidityPolicy(storage, [
10771
11360
  userId,
10772
11361
  vargs,
10773
11362
  ctx.xinputs,
@@ -12920,7 +13509,7 @@ var StorageReaderWriter = class extends StorageReader {
12920
13509
  userId: user.userId,
12921
13510
  name: "default",
12922
13511
  numberOfDesiredUTXOs: 144,
12923
- minimumDesiredUTXOValue: 32,
13512
+ minimumDesiredUTXOValue: DEFAULT_MANAGED_CHANGE_MINIMUM_SATOSHIS,
12924
13513
  isDeleted: false
12925
13514
  });
12926
13515
  break;
@@ -13201,8 +13790,10 @@ function validateActionBatchInlinePayload(manifest) {
13201
13790
  for (const { digest, bytes } of inlineBlobs) if (actionBatchBlobDigest(bytes) !== digest) throw new WERR_INVALID_PARAMETER("inlineBlobs", `content matching digest ${digest}`);
13202
13791
  }
13203
13792
  function requireUploadableBatch(batch) {
13204
- if (batch == null || batch.status === "committed" || batch.status === "aborted") throw new WERR_INVALID_OPERATION("action batch is not uploadable");
13205
- if (batch.hardExpiresAt.getTime() <= Date.now()) throw new WERR_INVALID_OPERATION("action batch hard lifetime has expired");
13793
+ if (batch == null) throw new WERRActionBatchState("missing");
13794
+ if (batch.status === "committed") throw new WERRActionBatchState("committed", batch.batchId);
13795
+ if (batch.status === "aborted") throw new WERRActionBatchState("aborted", batch.batchId);
13796
+ if (batch.hardExpiresAt.getTime() <= Date.now()) throw new WERRActionBatchState("hard-expired", batch.batchId);
13206
13797
  return batch;
13207
13798
  }
13208
13799
  function manifestPhysicalDigests(manifest) {
@@ -13562,7 +14153,10 @@ const ACTION_BATCH_LEASE_MS = 900 * 1e3;
13562
14153
  const ACTION_BATCH_HARD_LIFETIME_MS = 3600 * 1e3;
13563
14154
  const INITIAL_RESERVATION_LIMIT = 8;
13564
14155
  const INITIAL_EXTRA_OUTPUTS = 3;
13565
- function getActionBatchCapabilities() {
14156
+ function isValidOutpoint(outpoint) {
14157
+ return /^[0-9a-f]{64}$/.test(outpoint.txid) && Number.isSafeInteger(outpoint.vout) && outpoint.vout >= 0;
14158
+ }
14159
+ function getActionBatchCapabilities(maxReservedOutputs = 256, supportsResume = false) {
13566
14160
  return { actionBatch: {
13567
14161
  version: 1,
13568
14162
  maxInlineBytes: ACTION_BATCH_MAX_INLINE_BYTES,
@@ -13570,6 +14164,8 @@ function getActionBatchCapabilities() {
13570
14164
  maxConcurrentUploads: 4,
13571
14165
  leaseMs: ACTION_BATCH_LEASE_MS,
13572
14166
  hardLifetimeMs: ACTION_BATCH_HARD_LIFETIME_MS,
14167
+ resume: supportsResume ? true : void 0,
14168
+ maxReservedOutputs,
13573
14169
  compactBegin: true,
13574
14170
  manifestVersion: 2,
13575
14171
  commitByDigest: true,
@@ -13585,6 +14181,14 @@ function getActionBatchCapabilities() {
13585
14181
  function activeExpiry(batch, now = /* @__PURE__ */ new Date()) {
13586
14182
  return batch.status !== "active" && batch.status !== "prepared" || batch.expiresAt.getTime() <= now.getTime() || batch.hardExpiresAt.getTime() <= now.getTime();
13587
14183
  }
14184
+ function actionBatchErrorState(batch, now = /* @__PURE__ */ new Date()) {
14185
+ if (batch == null) return "missing";
14186
+ if (batch.hardExpiresAt.getTime() <= now.getTime()) return "hard-expired";
14187
+ if (batch.status === "aborted") return "aborted";
14188
+ if (batch.status === "committed") return "committed";
14189
+ if (batch.status === "expired" || batch.expiresAt.getTime() <= now.getTime()) return "expired";
14190
+ if (batch.status !== "active" && batch.status !== "prepared") return "inactive";
14191
+ }
13588
14192
  function canExpire(batch, now = /* @__PURE__ */ new Date()) {
13589
14193
  return (batch.status === "active" || batch.status === "prepared") && (batch.expiresAt.getTime() <= now.getTime() || batch.hardExpiresAt.getTime() <= now.getTime());
13590
14194
  }
@@ -13670,12 +14274,22 @@ function estimateFirstActionTarget(storage, args, inputSatoshis, outputScriptLen
13670
14274
  const minFee = Math.ceil(transactionSize([...inputLengths, 107], outputLengths) * fee / 1e3);
13671
14275
  return Math.max(1, outputSatoshis + minFee - inputSatoshis);
13672
14276
  }
14277
+ function selectReservationChange(candidates, targetSatoshis) {
14278
+ for (const status of [
14279
+ "completed",
14280
+ "unproven",
14281
+ "sending"
14282
+ ]) {
14283
+ const selected = selectCanonicalChange(candidates.filter((candidate) => candidate.transactionStatus === status), targetSatoshis);
14284
+ if (selected != null) return selected;
14285
+ }
14286
+ }
13673
14287
  function chooseReservationPool(candidates, targetSatoshis, limit, extras, fillLimit, planningCosts) {
13674
14288
  const remaining = candidates.filter((output) => output.satoshis > planningCosts.marginalInputFee);
13675
14289
  const chosen = [];
13676
14290
  let deficit = targetSatoshis + planningCosts.firstChangeCost;
13677
14291
  while (deficit > 0 && chosen.length < limit) {
13678
- const output = selectCanonicalChange(remaining, deficit + planningCosts.marginalInputFee);
14292
+ const output = selectReservationChange(remaining, deficit + planningCosts.marginalInputFee);
13679
14293
  if (output == null) break;
13680
14294
  chosen.push(output);
13681
14295
  remaining.splice(remaining.indexOf(output), 1);
@@ -13683,14 +14297,18 @@ function chooseReservationPool(candidates, targetSatoshis, limit, extras, fillLi
13683
14297
  }
13684
14298
  const desiredCount = fillLimit ? limit : Math.min(limit, chosen.length + extras);
13685
14299
  while (remaining.length > 0 && chosen.length < desiredCount) {
13686
- const output = selectCanonicalChange(remaining, targetSatoshis);
14300
+ const output = selectReservationChange(remaining, targetSatoshis);
13687
14301
  if (output == null) break;
13688
14302
  chosen.push(output);
13689
14303
  remaining.splice(remaining.indexOf(output), 1);
13690
14304
  }
13691
14305
  return chosen;
13692
14306
  }
13693
- function reservationPlanningCosts(storage, basket) {
14307
+ function reservationOutput(candidate) {
14308
+ const { transactionStatus: _transactionStatus, ...output } = candidate;
14309
+ return output;
14310
+ }
14311
+ function reservationPlanningCosts(storage, _basket) {
13694
14312
  const satsPerKb = validateStorageFeeModel(storage.feeModel).value ?? 0;
13695
14313
  const minimumSpendSize = transactionSize([107], [25]);
13696
14314
  const minimumSpendFee = Math.ceil(minimumSpendSize * satsPerKb / 1e3);
@@ -13698,7 +14316,7 @@ function reservationPlanningCosts(storage, basket) {
13698
14316
  const marginalInputSize = transactionSize([107], []) - transactionSize([], []);
13699
14317
  const marginalOutputSize = transactionSize([], [25]) - transactionSize([], []);
13700
14318
  return {
13701
- firstChangeCost: Math.max(dustFloor, Math.max(1, Math.round(basket.minimumDesiredUTXOValue / 4))) + Math.ceil(marginalOutputSize * satsPerKb / 1e3),
14319
+ firstChangeCost: dustFloor + Math.ceil(marginalOutputSize * satsPerKb / 1e3),
13702
14320
  marginalInputFee: Math.ceil(marginalInputSize * satsPerKb / 1e3)
13703
14321
  };
13704
14322
  }
@@ -13773,18 +14391,25 @@ async function beginActionBatch(storage, auth, args) {
13773
14391
  const explicit = await resolveExplicitOutputs(storage, userId, args.firstAction, outputScriptLengths != null);
13774
14392
  const noSendChange = await resolveNoSendChangeOutputs(storage, userId, args.firstAction);
13775
14393
  const fixedOutputIds = new Set([...explicit.outputs, ...noSendChange.outputs].map((output) => output.outputId));
13776
- const available = (await storage.findAvailableManagedChangeInputs(userId, changeBasket.basketId, !args.firstAction.isDelayed)).filter((output) => !fixedOutputIds.has(output.outputId));
14394
+ const availableOutputs = (await storage.findAvailableManagedChangeInputs(userId, changeBasket.basketId, false)).filter((output) => !fixedOutputIds.has(output.outputId));
14395
+ const availableStatuses = await storage.findTransactionStatusesByIds(userId, availableOutputs.map((output) => output.transactionId));
14396
+ const available = availableOutputs.map((output) => ({
14397
+ ...output,
14398
+ transactionStatus: availableStatuses.get(output.transactionId) ?? "sending"
14399
+ }));
13777
14400
  const target = estimateFirstActionTarget(storage, args.firstAction, explicit.inputSatoshis + noSendChange.inputSatoshis, outputScriptLengths);
13778
- const fixedOutputs = [...explicit.outputs, ...noSendChange.outputs];
13779
- const funding = chooseReservationPool(available, target, Math.max(0, INITIAL_RESERVATION_LIMIT - fixedOutputs.length), INITIAL_EXTRA_OUTPUTS, false, reservationPlanningCosts(storage, changeBasket));
14401
+ const fixedOutputs = [...new Map([...explicit.outputs, ...noSendChange.outputs].map((output) => [output.outputId, output])).values()];
14402
+ const maxReservedOutputs = storage.actionBatchMaxReservedOutputs;
14403
+ if (maxReservedOutputs >= 0 && fixedOutputs.length > maxReservedOutputs) throw new WERR_INVALID_PARAMETER("firstAction", `no more than ${maxReservedOutputs} persisted inputs`);
14404
+ const fundingOutputs = chooseReservationPool(available, target, Math.max(0, (maxReservedOutputs < 0 ? INITIAL_RESERVATION_LIMIT : Math.min(INITIAL_RESERVATION_LIMIT, maxReservedOutputs)) - fixedOutputs.length), INITIAL_EXTRA_OUTPUTS, false, reservationPlanningCosts(storage, changeBasket)).map(reservationOutput);
13780
14405
  const batch = newBatch(userId, args.batchId);
13781
14406
  await storage.transaction(async (trx) => {
13782
14407
  await storage.insertActionBatch(batch, trx);
13783
- await reserveOutputs(storage, batch, [...fixedOutputs, ...funding], trx);
14408
+ await reserveOutputs(storage, batch, [...fixedOutputs, ...fundingOutputs], trx);
13784
14409
  });
13785
14410
  let fundingResult;
13786
14411
  try {
13787
- fundingResult = await makeFundingResult(storage, args.firstAction, [...funding, ...fixedOutputs]);
14412
+ fundingResult = await makeFundingResult(storage, args.firstAction, [...fundingOutputs, ...fixedOutputs]);
13788
14413
  } catch (error) {
13789
14414
  await storage.transaction(async (trx) => await releaseBatchState(storage, batch, "aborted", trx));
13790
14415
  throw error;
@@ -13798,41 +14423,63 @@ async function beginActionBatch(storage, auth, args) {
13798
14423
  feeModel: validateStorageFeeModel(storage.feeModel),
13799
14424
  commissionSatoshis: storage.commissionSatoshis,
13800
14425
  commissionPubKeyHex: storage.commissionPubKeyHex,
13801
- availableChangeCount: available.length,
13802
- reservedOutputs: fundingResult.outputs.filter((output) => funding.some((candidate) => candidate.outputId === output.outputId)),
14426
+ availableChangeCount: available.filter((output) => output.satoshis >= Math.max(1, changeBasket.minimumDesiredUTXOValue)).length,
14427
+ managedChangePolicy: {
14428
+ maxOutputsPerAction: storage.managedChangePolicy.maxOutputsPerAction,
14429
+ migrationInputsPerAction: storage.managedChangePolicy.migrationInputsPerAction
14430
+ },
14431
+ reservedOutputs: fundingResult.outputs.filter((output) => fundingOutputs.some((candidate) => candidate.outputId === output.outputId)),
13803
14432
  explicitOutputs: fundingResult.outputs.filter((output) => explicitIds.has(output.outputId)),
13804
14433
  inputBeef: fundingResult.beef
13805
14434
  };
13806
14435
  }
13807
- function requireLiveBatch(batch) {
13808
- if (batch == null || activeExpiry(batch)) throw new WERR_INVALID_OPERATION("action batch is not active");
14436
+ function requireLiveBatch(batch, batchId) {
14437
+ const state = actionBatchErrorState(batch);
14438
+ if (state != null) throw new WERRActionBatchState(state, batchId ?? batch?.batchId);
14439
+ if (batch == null) throw new WERRActionBatchState("missing", batchId);
13809
14440
  return batch;
13810
14441
  }
13811
14442
  async function extendActionBatch(storage, auth, args) {
13812
14443
  const userId = verifyId(auth.userId);
13813
14444
  await cleanupExpiredActionBatches(storage);
13814
- const batch = requireLiveBatch(await storage.findActionBatch(userId, args.batchId));
14445
+ const batch = requireLiveBatch(await storage.findActionBatch(userId, args.batchId), args.batchId);
13815
14446
  const basket = verifyOne(await storage.findOutputBaskets({ partial: {
13816
14447
  userId,
13817
14448
  name: "default"
13818
14449
  } }));
13819
14450
  const alreadyReserved = await storage.findActionBatchOutputIds(batch.actionBatchId);
13820
- const available = await storage.findAvailableManagedChangeInputs(userId, basket.basketId, false);
13821
14451
  if (!Number.isSafeInteger(args.requestedOutputs) || args.requestedOutputs < 0) throw new WERR_INVALID_PARAMETER("requestedOutputs", "non-negative safe integer");
13822
- const requestedCount = Math.min(args.requestedOutputs, 64);
13823
- const funding = chooseReservationPool(available, Math.max(1, args.targetSatoshis), requestedCount, 0, true, reservationPlanningCosts(storage, basket));
14452
+ if (!Number.isSafeInteger(args.targetSatoshis) || args.targetSatoshis < 0) throw new WERR_INVALID_PARAMETER("targetSatoshis", "non-negative safe integer");
14453
+ const maxReservedOutputs = storage.actionBatchMaxReservedOutputs;
14454
+ if (maxReservedOutputs >= 0 && args.explicitOutpoints.length > maxReservedOutputs || new Set(args.explicitOutpoints.map((outpoint) => `${outpoint.txid}.${outpoint.vout}`)).size !== args.explicitOutpoints.length || args.explicitOutpoints.some((outpoint) => !isValidOutpoint(outpoint))) throw new WERR_INVALID_PARAMETER("explicitOutpoints", `at most ${maxReservedOutputs < 0 ? "the configured request capacity" : maxReservedOutputs} valid outpoints`);
14455
+ const remainingCapacity = maxReservedOutputs < 0 ? Number.MAX_SAFE_INTEGER : maxReservedOutputs - alreadyReserved.length;
14456
+ if (remainingCapacity <= 0 && (args.requestedOutputs > 0 || args.explicitOutpoints.length > 0)) throw new WERR_INVALID_OPERATION(`action batch already holds the maximum of ${maxReservedOutputs} outputs`);
13824
14457
  const explicitByOutpoint = await storage.findOutputsByOutpoints(userId, args.explicitOutpoints);
13825
- const explicit = Object.values(explicitByOutpoint).filter((output) => !alreadyReserved.includes(output.outputId));
13826
- const fundingResult = await makeFundingResult(storage, argsToFundingShape(args.includeSourceTransactions), [...funding, ...explicit]);
14458
+ const explicit = [...new Map(Object.values(explicitByOutpoint).filter((output) => !alreadyReserved.includes(output.outputId)).map((output) => [output.outputId, output])).values()];
14459
+ if (explicit.length > remainingCapacity) throw new WERR_INVALID_PARAMETER("explicitOutpoints", `no more than ${remainingCapacity} additional reserved outputs`);
14460
+ const explicitIds = new Set(explicit.map((output) => output.outputId));
14461
+ const availableOutputs = (await storage.findAvailableManagedChangeInputs(userId, basket.basketId, false)).filter((output) => !explicitIds.has(output.outputId));
14462
+ const availableStatuses = await storage.findTransactionStatusesByIds(userId, availableOutputs.map((output) => output.transactionId));
14463
+ const available = availableOutputs.map((output) => ({
14464
+ ...output,
14465
+ transactionStatus: availableStatuses.get(output.transactionId) ?? "sending"
14466
+ }));
14467
+ const requestedCount = Math.min(args.requestedOutputs, 64, remainingCapacity - explicit.length);
14468
+ const fundingOutputs = chooseReservationPool(available, Math.max(1, args.targetSatoshis), requestedCount, 0, true, reservationPlanningCosts(storage, basket)).map(reservationOutput);
14469
+ const fundingResult = await makeFundingResult(storage, argsToFundingShape(args.includeSourceTransactions), [...fundingOutputs, ...explicit]);
13827
14470
  const expiresAt = new Date(Math.min(batch.hardExpiresAt.getTime(), Date.now() + ACTION_BATCH_LEASE_MS));
13828
14471
  await storage.transaction(async (trx) => {
13829
- const current = requireLiveBatch(await storage.findActionBatchForUpdate(userId, args.batchId, trx));
13830
- await reserveOutputs(storage, current, [...funding, ...explicit], trx);
14472
+ const current = requireLiveBatch(await storage.findActionBatchForUpdate(userId, args.batchId, trx), args.batchId);
14473
+ const additions = [...new Map([...fundingOutputs, ...explicit].map((output) => [output.outputId, output])).values()];
14474
+ const currentReserved = new Set(await storage.findActionBatchOutputIds(current.actionBatchId, trx));
14475
+ const newAdditions = additions.filter((output) => !currentReserved.has(output.outputId));
14476
+ if (maxReservedOutputs >= 0 && currentReserved.size + newAdditions.length > maxReservedOutputs) throw new WERR_INVALID_OPERATION(`action batch cannot reserve more than ${maxReservedOutputs} outputs`);
14477
+ await reserveOutputs(storage, current, newAdditions, trx);
13831
14478
  await storage.updateActionBatch(current.actionBatchId, { expiresAt }, trx);
13832
14479
  });
13833
14480
  return {
13834
14481
  expiresAt: expiresAt.toISOString(),
13835
- reservedOutputs: fundingResult.outputs.filter((output) => funding.some((candidate) => candidate.outputId === output.outputId)),
14482
+ reservedOutputs: fundingResult.outputs.filter((output) => fundingOutputs.some((candidate) => candidate.outputId === output.outputId)),
13836
14483
  explicitOutputs: fundingResult.outputs.filter((output) => explicit.some((candidate) => candidate.outputId === output.outputId)),
13837
14484
  inputBeef: fundingResult.beef
13838
14485
  };
@@ -13868,14 +14515,64 @@ function argsToFundingShape(includeSourceTransactions) {
13868
14515
  async function renewActionBatch(storage, auth, batchId) {
13869
14516
  const userId = verifyId(auth.userId);
13870
14517
  return await storage.transaction(async (trx) => {
13871
- const batch = requireLiveBatch(await storage.findActionBatchForUpdate(userId, batchId, trx));
14518
+ const batch = requireLiveBatch(await storage.findActionBatchForUpdate(userId, batchId, trx), batchId);
13872
14519
  const expiresAt = new Date(Math.min(batch.hardExpiresAt.getTime(), Date.now() + ACTION_BATCH_LEASE_MS));
13873
14520
  await storage.updateActionBatch(batch.actionBatchId, { expiresAt }, trx);
13874
14521
  return { expiresAt: expiresAt.toISOString() };
13875
14522
  });
13876
14523
  }
14524
+ function validateResumeOutpoints(storage, args) {
14525
+ const unique = new Set(args.outpoints.map((outpoint) => `${outpoint.txid}.${outpoint.vout}`));
14526
+ const maxReservedOutputs = storage.actionBatchMaxReservedOutputs;
14527
+ if (maxReservedOutputs >= 0 && args.outpoints.length > maxReservedOutputs || unique.size !== args.outpoints.length || args.outpoints.some((outpoint) => !isValidOutpoint(outpoint))) throw new WERR_INVALID_PARAMETER("outpoints", `at most ${maxReservedOutputs < 0 ? "the configured request capacity" : maxReservedOutputs} unique valid outpoints`);
14528
+ }
14529
+ /**
14530
+ * Reacquire exactly the persisted inputs owned by one expired client
14531
+ * workspace. This is deliberately explicit: another action may not become a
14532
+ * member merely because it happens to use the same Wallet instance.
14533
+ */
14534
+ async function resumeActionBatch(storage, auth, args) {
14535
+ const userId = verifyId(auth.userId);
14536
+ validateResumeOutpoints(storage, args);
14537
+ await cleanupExpiredActionBatches(storage);
14538
+ return await storage.transaction(async (trx) => {
14539
+ const batch = await storage.findActionBatchForUpdate(userId, args.batchId, trx);
14540
+ const state = actionBatchErrorState(batch);
14541
+ if (state != null && state !== "expired") throw new WERRActionBatchState(state, args.batchId);
14542
+ if (batch == null) throw new WERRActionBatchState("missing", args.batchId);
14543
+ const expiresAt = new Date(Math.min(batch.hardExpiresAt.getTime(), Date.now() + ACTION_BATCH_LEASE_MS));
14544
+ if (state == null) {
14545
+ const reserved = new Set(await storage.findActionBatchOutputIds(batch.actionBatchId, trx));
14546
+ const current = await storage.findOutputsByOutpointsForUpdate(userId, args.outpoints, trx);
14547
+ if (Object.values(current).some((output) => !reserved.has(output.outputId)) || Object.keys(current).length !== args.outpoints.length || reserved.size !== args.outpoints.length) throw new WERRActionBatchState("conflicted", args.batchId);
14548
+ await storage.updateActionBatch(batch.actionBatchId, { expiresAt }, trx);
14549
+ return { expiresAt: expiresAt.toISOString() };
14550
+ }
14551
+ const stored = await storage.findOutputsByOutpointsForUpdate(userId, args.outpoints, trx);
14552
+ const outputs = args.outpoints.map((outpoint) => stored[`${outpoint.txid}.${outpoint.vout}`]);
14553
+ if (outputs.some((output) => output == null || !output.spendable || output.spentBy != null)) throw new WERRActionBatchState("conflicted", args.batchId);
14554
+ await storage.deleteActionBatchOutputReservations(batch.actionBatchId, trx);
14555
+ const exactOutputs = outputs;
14556
+ if ((await storage.findReservedActionBatchOutputIds(exactOutputs.map((output) => output.outputId), trx)).length > 0) throw new WERRActionBatchState("conflicted", args.batchId);
14557
+ const now = /* @__PURE__ */ new Date();
14558
+ await storage.reserveActionBatchOutputs(exactOutputs.map((output) => ({
14559
+ actionBatchId: batch.actionBatchId,
14560
+ outputId: output.outputId,
14561
+ created_at: now,
14562
+ updated_at: now
14563
+ })), trx);
14564
+ await storage.updateActionBatch(batch.actionBatchId, {
14565
+ status: "active",
14566
+ expiresAt,
14567
+ manifest: void 0,
14568
+ manifestDigest: void 0,
14569
+ uploadDigests: void 0
14570
+ }, trx);
14571
+ return { expiresAt: expiresAt.toISOString() };
14572
+ });
14573
+ }
13877
14574
  async function reacquireManifestInputs(storage, userId, batch, validated, trx) {
13878
- if (batch.hardExpiresAt.getTime() <= Date.now()) throw new WERR_INVALID_OPERATION("action batch hard lifetime has expired");
14575
+ if (batch.hardExpiresAt.getTime() <= Date.now()) throw new WERRActionBatchState("hard-expired", batch.batchId);
13879
14576
  const stagedTxids = new Set(validated.actions.map(({ action }) => action.txid));
13880
14577
  const outpoints = [...new Map(validated.actions.flatMap(({ action }) => action.plan.inputs).filter((input) => !stagedTxids.has(input.sourceTxid)).map((input) => [`${input.sourceTxid}.${input.sourceVout}`, {
13881
14578
  txid: input.sourceTxid,
@@ -14042,7 +14739,7 @@ async function persistAction(...[storage, userId, validated, reservedOutputIds,
14042
14739
  async function persistManifestAtomically(storage, userId, batch, manifest, validated) {
14043
14740
  return await storage.transaction(async (trx) => {
14044
14741
  const current = await storage.findActionBatchForUpdate(userId, batch.batchId, trx);
14045
- if (current == null) throw new WERR_INVALID_OPERATION("action batch was not found");
14742
+ if (current == null) throw new WERRActionBatchState("missing", batch.batchId);
14046
14743
  if (current.status === "committed") {
14047
14744
  if (current.manifestDigest !== manifest.digest) throw new WERR_INVALID_OPERATION("batch committed with another manifest");
14048
14745
  return {
@@ -14050,12 +14747,12 @@ async function persistManifestAtomically(storage, userId, batch, manifest, valid
14050
14747
  alreadyCommitted: true
14051
14748
  };
14052
14749
  }
14053
- if (current.status === "aborted") throw new WERR_INVALID_OPERATION("aborted action batch cannot be committed");
14750
+ if (current.status === "aborted") throw new WERRActionBatchState("aborted", batch.batchId);
14054
14751
  if (current.manifestDigest != null && current.manifestDigest !== manifest.digest) throw new WERR_INVALID_OPERATION("batch prepared with another manifest");
14055
14752
  if (current.status === "expired" || activeExpiry(current)) {
14056
- if (current.hardExpiresAt.getTime() <= Date.now()) throw new WERR_INVALID_OPERATION("action batch hard lifetime has expired");
14753
+ if (current.hardExpiresAt.getTime() <= Date.now()) throw new WERRActionBatchState("hard-expired", batch.batchId);
14057
14754
  await reacquireManifestInputs(storage, userId, current, validated, trx);
14058
- } else requireLiveBatch(current);
14755
+ } else requireLiveBatch(current, batch.batchId);
14059
14756
  const reservedOutputIds = new Set(await storage.findActionBatchOutputIds(current.actionBatchId, trx));
14060
14757
  const stagedTxids = new Set(validated.actions.map(({ action }) => action.txid));
14061
14758
  const inputOutpoints = [...new Map(validated.actions.flatMap(({ action }) => action.plan.inputs).filter((input) => !stagedTxids.has(input.sourceTxid)).map((input) => [`${input.sourceTxid}.${input.sourceVout}`, {
@@ -14133,14 +14830,14 @@ async function completeCommittedBatch(storage, userId, batch, manifest, alreadyC
14133
14830
  }
14134
14831
  async function commitActionBatchOnce(storage, userId, manifest) {
14135
14832
  const batch = await storage.findActionBatch(userId, manifest.batchId);
14136
- if (batch == null) throw new WERR_INVALID_OPERATION("action batch was not found");
14833
+ if (batch == null) throw new WERRActionBatchState("missing", manifest.batchId);
14137
14834
  if (batch.status === "committed") {
14138
14835
  if (batch.manifestDigest !== manifest.digest) throw new WERR_INVALID_OPERATION("batch committed with another manifest");
14139
14836
  return await completeCommittedBatch(storage, userId, batch, manifest, true);
14140
14837
  }
14141
- if (batch.status === "aborted") throw new WERR_INVALID_OPERATION("aborted action batch cannot be committed");
14142
- if (!(batch.status === "expired" || activeExpiry(batch))) requireLiveBatch(batch);
14143
- else if (batch.hardExpiresAt.getTime() <= Date.now()) throw new WERR_INVALID_OPERATION("action batch hard lifetime has expired");
14838
+ if (batch.status === "aborted") throw new WERRActionBatchState("aborted", manifest.batchId);
14839
+ if (!(batch.status === "expired" || activeExpiry(batch))) requireLiveBatch(batch, manifest.batchId);
14840
+ else if (batch.hardExpiresAt.getTime() <= Date.now()) throw new WERRActionBatchState("hard-expired", manifest.batchId);
14144
14841
  if (batch.manifestDigest != null && batch.manifestDigest !== manifest.digest) throw new WERR_INVALID_OPERATION("batch prepared with another manifest");
14145
14842
  const persisted = await persistManifestAtomically(storage, userId, batch, manifest, await validateManifestActions(storage, batch, manifest));
14146
14843
  return await completeCommittedBatch(storage, userId, persisted.batch, manifest, persisted.alreadyCommitted, persisted.share);
@@ -14223,6 +14920,8 @@ var StorageProvider = class StorageProvider extends StorageReaderWriter {
14223
14920
  commissionSatoshis;
14224
14921
  commissionPubKeyHex;
14225
14922
  maxRecursionDepth;
14923
+ actionBatchMaxReservedOutputs;
14924
+ managedChangePolicy;
14226
14925
  scriptVerifier;
14227
14926
  static defaultOptions() {
14228
14927
  return {
@@ -14231,7 +14930,9 @@ var StorageProvider = class StorageProvider extends StorageReaderWriter {
14231
14930
  value: 100
14232
14931
  },
14233
14932
  commissionSatoshis: 0,
14234
- commissionPubKeyHex: void 0
14933
+ commissionPubKeyHex: void 0,
14934
+ actionBatchMaxReservedOutputs: 256,
14935
+ managedChangePolicy: defaultManagedChangePolicy()
14235
14936
  };
14236
14937
  }
14237
14938
  static createStorageBaseOptions(chain) {
@@ -14246,6 +14947,10 @@ var StorageProvider = class StorageProvider extends StorageReaderWriter {
14246
14947
  this.commissionPubKeyHex = options.commissionPubKeyHex;
14247
14948
  this.commissionSatoshis = options.commissionSatoshis;
14248
14949
  this.maxRecursionDepth = 12;
14950
+ const maxReservedOutputs = options.actionBatchMaxReservedOutputs ?? 256;
14951
+ if (maxReservedOutputs !== -1 && (!Number.isSafeInteger(maxReservedOutputs) || maxReservedOutputs < 1)) throw new WERR_INVALID_PARAMETER("actionBatchMaxReservedOutputs", "a positive safe integer or -1 for unlimited");
14952
+ this.actionBatchMaxReservedOutputs = maxReservedOutputs;
14953
+ this.managedChangePolicy = validateManagedChangePolicy(options.managedChangePolicy);
14249
14954
  this.scriptVerifier = options.scriptVerifier;
14250
14955
  }
14251
14956
  /** Mark a planned set of change inputs spent within the caller's transaction. */
@@ -14276,12 +14981,16 @@ var StorageProvider = class StorageProvider extends StorageReaderWriter {
14276
14981
  }
14277
14982
  /** Read only the fields needed by the in-memory funding planner. */
14278
14983
  async findAvailableManagedChangeInputCandidates(userId, basketId, excludeSending, trx) {
14279
- return (await this.findAvailableManagedChangeInputs(userId, basketId, excludeSending, trx)).map(({ outputId, transactionId, satoshis, txid, vout }) => ({
14984
+ const outputs = await this.findAvailableManagedChangeInputs(userId, basketId, excludeSending, trx);
14985
+ const findStatuses = this.findTransactionStatusesByIds?.bind(this);
14986
+ const statuses = findStatuses == null ? /* @__PURE__ */ new Map() : await findStatuses(userId, outputs.map((output) => output.transactionId), trx);
14987
+ return outputs.map(({ outputId, transactionId, satoshis, txid, vout }) => ({
14280
14988
  outputId,
14281
14989
  transactionId,
14282
14990
  satoshis,
14283
14991
  txid,
14284
- vout
14992
+ vout,
14993
+ transactionStatus: statuses.get(transactionId)
14285
14994
  }));
14286
14995
  }
14287
14996
  /** Read the current status of a set of source transactions without loading raw transaction bytes. */
@@ -14369,7 +15078,7 @@ var StorageProvider = class StorageProvider extends StorageReaderWriter {
14369
15078
  throw new WERR_NOT_IMPLEMENTED();
14370
15079
  }
14371
15080
  async getCapabilities() {
14372
- return this.supportsActionBatchPersistence() ? getActionBatchCapabilities() : {};
15081
+ return this.supportsActionBatchPersistence() ? getActionBatchCapabilities(this.actionBatchMaxReservedOutputs, true) : {};
14373
15082
  }
14374
15083
  supportsActionBatchPersistence() {
14375
15084
  return false;
@@ -14388,6 +15097,9 @@ var StorageProvider = class StorageProvider extends StorageReaderWriter {
14388
15097
  async renewActionBatch(auth, batchId) {
14389
15098
  return await renewActionBatch(this, auth, batchId);
14390
15099
  }
15100
+ async resumeActionBatch(auth, args) {
15101
+ return await resumeActionBatch(this, auth, args);
15102
+ }
14391
15103
  async prepareActionBatchCommit(auth, manifest) {
14392
15104
  return await prepareActionBatchCommit(this, auth, manifest);
14393
15105
  }
@@ -14985,7 +15697,12 @@ var StorageProvider = class StorageProvider extends StorageReaderWriter {
14985
15697
  for (const output of outputs) {
14986
15698
  const outputId = verifyId(output.outputId);
14987
15699
  await this.validateOutputScript(output);
14988
- verdicts.set(outputId, output.lockingScript == null ? void 0 : await services.isUtxo(output));
15700
+ if (output.lockingScript == null) {
15701
+ verdicts.set(outputId, void 0);
15702
+ continue;
15703
+ }
15704
+ const classification = await classifyOutputUtxo(services, output);
15705
+ verdicts.set(outputId, classification.verdict === "unknown" ? void 0 : classification.verdict === "unspent");
14989
15706
  }
14990
15707
  }
14991
15708
  return verdicts;
@@ -15147,16 +15864,12 @@ var StorageProvider = class StorageProvider extends StorageReaderWriter {
15147
15864
  } });
15148
15865
  for (const o of outputs) {
15149
15866
  if (!o.spendable) continue;
15150
- if (!await this.checkOutputIsUtxo(o, services)) invalidSpendableOutputs.push(o);
15867
+ await this.validateOutputScript(o);
15868
+ if (!requireConclusiveUtxo(await classifyOutputUtxo(services, o))) invalidSpendableOutputs.push(o);
15151
15869
  }
15152
15870
  }
15153
15871
  return { invalidSpendableOutputs };
15154
15872
  }
15155
- async checkOutputIsUtxo(o, services) {
15156
- if (o.lockingScript == null || o.lockingScript.length === 0) return false;
15157
- const hash = services.hashOutputScript(asString(o.lockingScript));
15158
- return (await services.getUtxoStatus(hash, void 0, `${o.txid ?? ""}.${o.vout ?? ""}`)).isUtxo === true;
15159
- }
15160
15873
  async updateProvenTxReqDynamics(id, update, trx) {
15161
15874
  const partial = {};
15162
15875
  if (update.updated_at != null) partial.updated_at = update.updated_at;
@@ -15266,6 +15979,7 @@ const getLabelToSpecOp = () => {
15266
15979
  //#region ../src/storage/methods/listActionsIdb.ts
15267
15980
  async function enrichIdbActionLabels(storage, tx, action, timeFilterRequested) {
15268
15981
  action.labels = (await storage.getLabelsForTransactionId(tx.transactionId)).map((l) => l.label);
15982
+ if (tx.reference != null && tx.reference !== "") action.labels = applyBrc153ReferenceLabel(action.labels, tx.reference);
15269
15983
  if (timeFilterRequested) {
15270
15984
  const ts = tx.created_at ? new Date(tx.created_at).getTime() : NaN;
15271
15985
  if (!Number.isNaN(ts)) {
@@ -15384,20 +16098,129 @@ async function listActionsIdb(storage, auth, vargs) {
15384
16098
  return r;
15385
16099
  }
15386
16100
  //#endregion
15387
- //#region ../src/storage/methods/ListOutputsSpecOp.ts
15388
- const INVALID_CHANGE_MAX_CONCURRENCY = 12;
15389
- async function runWithConcurrency(values, maxConcurrency, worker) {
15390
- const active = [];
15391
- for (const value of values) {
15392
- const task = worker(value).finally(() => {
15393
- const i = active.indexOf(task);
15394
- if (i >= 0) active.splice(i, 1);
16101
+ //#region ../src/storage/methods/reviewUtxoOutputs.ts
16102
+ const MAX_AUDIT_PROVIDERS = 8;
16103
+ const MAX_PROVIDER_NAME_LENGTH = 128;
16104
+ const UTXO_REVIEW_PROVIDER_TIMEOUT_MSECS = 5e3;
16105
+ function diagnosticsFor(classifications, releasedOutputIds) {
16106
+ const allProviders = [...new Set(classifications.map((result) => result.status.provider.slice(0, MAX_PROVIDER_NAME_LENGTH)))];
16107
+ const providers = allProviders.slice(0, MAX_AUDIT_PROVIDERS);
16108
+ const confirmedSpent = classifications.filter((result) => result.status.verdict === "spent");
16109
+ const released = confirmedSpent.filter((result) => releasedOutputIds.has(result.output.outputId));
16110
+ return {
16111
+ checked: classifications.length,
16112
+ confirmedUnspent: classifications.filter((result) => result.status.verdict === "unspent").length,
16113
+ confirmedSpent: confirmedSpent.length,
16114
+ unknown: classifications.filter((result) => result.status.verdict === "unknown").length,
16115
+ confirmedSpentSatoshis: confirmedSpent.reduce((sum, result) => sum + result.output.satoshis, 0),
16116
+ released: released.length,
16117
+ releasedSatoshis: released.reduce((sum, result) => sum + result.output.satoshis, 0),
16118
+ providers,
16119
+ providerCount: allProviders.length,
16120
+ providersTruncated: allProviders.length > providers.length
16121
+ };
16122
+ }
16123
+ function auditDetails(diagnostics, userId, releaseMode, reason) {
16124
+ return JSON.stringify({
16125
+ operation: "specOpInvalidChange",
16126
+ reason,
16127
+ releaseMode,
16128
+ userId,
16129
+ ...diagnostics
16130
+ });
16131
+ }
16132
+ async function classifyCandidates(storage, outputs) {
16133
+ const services = storage.getServices();
16134
+ return await mapWithConcurrency(outputs.filter((output) => output.basketId != null), 4, async (output) => {
16135
+ try {
16136
+ await storage.validateOutputScript(output);
16137
+ } catch (error) {
16138
+ return {
16139
+ output,
16140
+ status: {
16141
+ verdict: "unknown",
16142
+ provider: "<script-validation-error>",
16143
+ error
16144
+ }
16145
+ };
16146
+ }
16147
+ let timeout;
16148
+ const providerCall = classifyOutputUtxo(services, output);
16149
+ const timedClassification = new Promise((resolve) => {
16150
+ timeout = setTimeout(() => resolve({
16151
+ verdict: "unknown",
16152
+ provider: "<review-timeout>",
16153
+ error: /* @__PURE__ */ new Error(`UTXO classification exceeded ${UTXO_REVIEW_PROVIDER_TIMEOUT_MSECS}ms`)
16154
+ }), UTXO_REVIEW_PROVIDER_TIMEOUT_MSECS);
16155
+ });
16156
+ try {
16157
+ return {
16158
+ output,
16159
+ status: await Promise.race([providerCall, timedClassification])
16160
+ };
16161
+ } finally {
16162
+ if (timeout != null) clearTimeout(timeout);
16163
+ providerCall.catch(() => void 0);
16164
+ }
16165
+ });
16166
+ }
16167
+ async function releaseConfirmedSpent(storage, auth, classifications, releaseMode) {
16168
+ const confirmedSpent = classifications.filter((result) => result.status.verdict === "spent");
16169
+ const releasedOutputIds = /* @__PURE__ */ new Set();
16170
+ await storage.transaction(async (trx) => {
16171
+ for (const { output } of confirmedSpent) {
16172
+ const current = await storage.findOutputById(output.outputId, trx, true);
16173
+ if (current == null || current.userId !== auth.userId) throw new WERR_INVALID_PARAMETER("outputId", `owned by user ${auth.userId}`);
16174
+ if (current.spendable !== true || current.spentBy != null) throw new WERR_INVALID_OPERATION(`Output ${output.outputId} changed state during UTXO review; no outputs were changed.`);
16175
+ if (await storage.updateOutput(verifyId(output.outputId), { spendable: false }, trx) !== 1) throw new WERR_INVALID_PARAMETER("outputId", `updated exactly once: ${output.outputId}`);
16176
+ releasedOutputIds.add(output.outputId);
16177
+ }
16178
+ const now = /* @__PURE__ */ new Date();
16179
+ const diagnostics = diagnosticsFor(classifications, releasedOutputIds);
16180
+ await storage.insertMonitorEvent({
16181
+ created_at: now,
16182
+ updated_at: now,
16183
+ id: 0,
16184
+ event: releaseMode === "atomic" ? "InvalidChangeRelease" : "InvalidChangeConclusiveRelease",
16185
+ details: auditDetails(diagnostics, auth.userId, releaseMode, "provider-confirmed-spent")
16186
+ }, trx);
16187
+ });
16188
+ return releasedOutputIds;
16189
+ }
16190
+ /**
16191
+ * Classify a bounded set of wallet outputs without treating provider failure as
16192
+ * proof that an output was spent. Read-only reviews always return their
16193
+ * conclusive and unknown partitions. Atomic release requires every verdict to
16194
+ * be conclusive; the operator-only conclusive mode releases the positively
16195
+ * spent subset while retaining and reporting unknowns.
16196
+ */
16197
+ async function reviewUtxoOutputs(storage, auth, outputs, releaseMode = "none") {
16198
+ const classifications = await classifyCandidates(storage, outputs);
16199
+ const confirmedSpentOutputs = classifications.filter((result) => result.status.verdict === "spent").map((result) => result.output);
16200
+ const unknownOutputs = classifications.filter((result) => result.status.verdict === "unknown").map((result) => result.output);
16201
+ if (releaseMode === "atomic" && unknownOutputs.length > 0) {
16202
+ const diagnostics = diagnosticsFor(classifications, /* @__PURE__ */ new Set());
16203
+ const now = /* @__PURE__ */ new Date();
16204
+ await storage.insertMonitorEvent({
16205
+ created_at: now,
16206
+ updated_at: now,
16207
+ id: 0,
16208
+ event: "InvalidChangeReleaseBlocked",
16209
+ details: auditDetails(diagnostics, auth.userId, releaseMode, "inconclusive-provider-result")
15395
16210
  });
15396
- active.push(task);
15397
- if (active.length >= maxConcurrency) await Promise.race(active);
16211
+ throw new WERR_UTXO_REVIEW_INCONCLUSIVE(diagnostics.checked, diagnostics.confirmedSpent, diagnostics.unknown);
15398
16212
  }
15399
- await Promise.all(active);
16213
+ const releasedOutputIds = releaseMode === "none" ? /* @__PURE__ */ new Set() : await releaseConfirmedSpent(storage, auth, classifications, releaseMode);
16214
+ for (const output of confirmedSpentOutputs) if (releasedOutputIds.has(output.outputId)) output.spendable = false;
16215
+ return {
16216
+ classifications,
16217
+ confirmedSpentOutputs,
16218
+ unknownOutputs,
16219
+ diagnostics: diagnosticsFor(classifications, releasedOutputIds)
16220
+ };
15400
16221
  }
16222
+ //#endregion
16223
+ //#region ../src/storage/methods/ListOutputsSpecOp.ts
15401
16224
  const getBasketToSpecOp = () => {
15402
16225
  return {
15403
16226
  [specOpWalletBalance]: {
@@ -15428,23 +16251,7 @@ const getBasketToSpecOp = () => {
15428
16251
  includeSpent: false,
15429
16252
  tagsToIntercept: ["release", "all"],
15430
16253
  filterOutputs: async (s, auth, vargs, specOpTags, outputs) => {
15431
- if (specOpTags.includes("release")) await s.reviewStatus({ agedLimit: /* @__PURE__ */ new Date(0) });
15432
- const invalidOutputIds = /* @__PURE__ */ new Set();
15433
- const services = s.getServices();
15434
- await runWithConcurrency(outputs, INVALID_CHANGE_MAX_CONCURRENCY, async (o) => {
15435
- if (!o.basketId) return;
15436
- await s.validateOutputScript(o);
15437
- let ok = false;
15438
- if (o.lockingScript != null && o.lockingScript.length > 0) ok = await services.isUtxo(o);
15439
- else ok = void 0;
15440
- if (ok === false) invalidOutputIds.add(o.outputId);
15441
- });
15442
- const filteredOutputs = outputs.filter((o) => invalidOutputIds.has(o.outputId));
15443
- if (specOpTags.includes("release")) await runWithConcurrency(filteredOutputs, INVALID_CHANGE_MAX_CONCURRENCY, async (o) => {
15444
- await s.updateOutput(o.outputId, { spendable: false });
15445
- o.spendable = false;
15446
- });
15447
- return filteredOutputs;
16254
+ return (await reviewUtxoOutputs(s, auth, outputs, specOpTags.includes("release") ? "atomic" : "none")).confirmedSpentOutputs;
15448
16255
  }
15449
16256
  },
15450
16257
  [specOpSetWalletChangeParams]: {
@@ -15597,7 +16404,7 @@ async function loadIdbOutputs(query) {
15597
16404
  "nosend",
15598
16405
  "sending"
15599
16406
  ],
15600
- noScript: true,
16407
+ noScript: specOp?.includeOutputScripts !== true,
15601
16408
  orderDescending
15602
16409
  };
15603
16410
  const pageManagedChange = specOp?.managedChangeOnly === true && specOp.ignoreLimit !== true;
@@ -15679,7 +16486,10 @@ async function listOutputsIdb(storage, auth, vargs, _originator) {
15679
16486
  });
15680
16487
  result.totalOutputs = totalOutputs;
15681
16488
  if (specOp != null) {
15682
- if (specOp.filterOutputs != null) outputs = await specOp.filterOutputs(storage, auth, vargs, specOpTags, outputs);
16489
+ if (specOp.filterOutputs != null) {
16490
+ outputs = await specOp.filterOutputs(storage, auth, vargs, specOpTags, outputs);
16491
+ result.totalOutputs = outputs.length;
16492
+ }
15683
16493
  if (specOp.resultFromOutputs != null) return specOp.resultFromOutputs(storage, auth, vargs, specOpTags, outputs);
15684
16494
  }
15685
16495
  await hydrateIdbOutputResult(storage, outputs, vargs, result);
@@ -15810,6 +16620,7 @@ async function scanCursor(cursor, since, offset, limit, matches, accept) {
15810
16620
  var StorageIdb = class extends StorageProvider {
15811
16621
  dbName;
15812
16622
  db;
16623
+ managedChangeDefaultsMigrated = false;
15813
16624
  constructor(options) {
15814
16625
  super(options);
15815
16626
  this.dbName = `wallet-toolbox-${this.chain}net`;
@@ -15847,6 +16658,7 @@ var StorageIdb = class extends StorageProvider {
15847
16658
  async verifyDB(storageName, storageIdentityKey) {
15848
16659
  if (this.db != null) return this.db;
15849
16660
  this.db = await this.initDB(storageName, storageIdentityKey);
16661
+ await this.migrateManagedChangeDefaults(this.db);
15850
16662
  this._settings = (await this.db.getAll("settings"))[0];
15851
16663
  this.whenLastAccess = /* @__PURE__ */ new Date();
15852
16664
  return this.db;
@@ -15880,7 +16692,7 @@ var StorageIdb = class extends StorageProvider {
15880
16692
  async initDB(storageName, storageIdentityKey) {
15881
16693
  const chain = this.chain;
15882
16694
  const maxOutputScript = 1024;
15883
- return await openDB(this.dbName, 3, { upgrade(db, _oldVersion, _newVersion, transaction) {
16695
+ return await openDB(this.dbName, 4, { upgrade(db, _oldVersion, _newVersion, transaction) {
15884
16696
  upgradeAllStoresV1(db);
15885
16697
  upgradeActionBatchStoresV2(db);
15886
16698
  const outputs = transaction.objectStore("outputs");
@@ -15906,6 +16718,22 @@ var StorageIdb = class extends StorageProvider {
15906
16718
  }
15907
16719
  } });
15908
16720
  }
16721
+ async migrateManagedChangeDefaults(db) {
16722
+ if (this.managedChangeDefaultsMigrated) return;
16723
+ const trx = db.transaction("output_baskets", "readwrite");
16724
+ let cursor = await trx.store.openCursor();
16725
+ while (cursor != null) {
16726
+ const basket = cursor.value;
16727
+ if (isLegacyManagedChangeBasketDefault(basket)) await cursor.update({
16728
+ ...basket,
16729
+ minimumDesiredUTXOValue: DEFAULT_MANAGED_CHANGE_MINIMUM_SATOSHIS,
16730
+ updated_at: /* @__PURE__ */ new Date()
16731
+ });
16732
+ cursor = await cursor.continue();
16733
+ }
16734
+ await trx.done;
16735
+ this.managedChangeDefaultsMigrated = true;
16736
+ }
15909
16737
  async reviewStatus(args) {
15910
16738
  return await reviewStatusIdb(this, args);
15911
16739
  }
@@ -16660,6 +17488,7 @@ var StorageIdb = class extends StorageProvider {
16660
17488
  if (this.db != null) this.db.close();
16661
17489
  this.db = void 0;
16662
17490
  this._settings = void 0;
17491
+ this.managedChangeDefaultsMigrated = false;
16663
17492
  }
16664
17493
  allStores = [
16665
17494
  "action_batches",
@@ -17832,6 +18661,9 @@ var StorageClientBase = class {
17832
18661
  async renewActionBatch(auth, batchId) {
17833
18662
  return await this.rpcCall("renewActionBatch", [auth, batchId]);
17834
18663
  }
18664
+ async resumeActionBatch(auth, args) {
18665
+ return await this.rpcCall("resumeActionBatch", [auth, args]);
18666
+ }
17835
18667
  async prepareActionBatchCommit(auth, manifest) {
17836
18668
  return await this.rpcCall("prepareActionBatchCommit", [auth, manifest]);
17837
18669
  }
@@ -18636,7 +19468,7 @@ async function restoreBRC38(storage, data) {
18636
19468
  await storage.transaction(async (trx) => {
18637
19469
  await storage.insertUser({ ...data.user }, trx);
18638
19470
  for (const row of data.provenTxs) await storage.insertProvenTx({ ...row }, trx);
18639
- for (const row of data.outputBaskets) await storage.insertOutputBasket({ ...row }, trx);
19471
+ for (const row of data.outputBaskets) await storage.insertOutputBasket(upgradeLegacyManagedChangeBasketDefault({ ...row }), trx);
18640
19472
  for (const row of data.outputTags) await storage.insertOutputTag({ ...row }, trx);
18641
19473
  for (const row of data.txLabels) await storage.insertTxLabel({ ...row }, trx);
18642
19474
  for (const row of data.transactions) await storage.insertTransaction({ ...row }, trx);
@@ -21695,13 +22527,37 @@ function configuredChaintracksClient(chain, serviceUrl) {
21695
22527
  return new ChaintracksServiceClient(chain, serviceUrl);
21696
22528
  }
21697
22529
  /**
22530
+ * True when running under a browser-style runtime — a real browser tab, or
22531
+ * the embedded webview a desktop wallet shell hosts its wallet logic in
22532
+ * (Metanet Client / User Wallet are Tauri WKWebViews) — where `fetch` is
22533
+ * subject to CORS enforcement that Node-style runtimes do not apply.
22534
+ */
22535
+ function isBrowserRuntime() {
22536
+ return typeof window !== "undefined" && window.document !== void 0;
22537
+ }
22538
+ /**
21698
22539
  * Returns the credential-free default ChainTracks client for a supported
21699
22540
  * public network, or an operator-configured client for stn/tstn.
22541
+ *
22542
+ * BROWSER RUNTIMES get the legacy CORS-enabled Chaintracks service for
22543
+ * main/test. The Go Chaintracks deployments (`arcade-v2-*.bsvblockchain.tech`)
22544
+ * currently serve no `Access-Control-Allow-Origin` header and answer OPTIONS
22545
+ * preflights with 404 (verified live 2026-08-11), so every fetch from a
22546
+ * browser-hosted wallet is CORS-blocked (WebKit surfaces it as
22547
+ * `TypeError: Load failed`) and the wallet loses `getHeight`, headers and
22548
+ * merkle-root validation wholesale. The repository service contract
22549
+ * (AGENTS.md: "browser, mobile, and unknown-domain clients must not be
22550
+ * silently blocked by CORS") requires a default that browsers can actually
22551
+ * reach. Once the Go deployments serve CORS (and a browser-run conformance
22552
+ * check proves it), this branch can be removed and browsers can share the v2
22553
+ * default. Node runtimes are unchanged.
21700
22554
  */
21701
22555
  function createDefaultChaintracksClient(chain) {
21702
22556
  switch (chain) {
21703
22557
  case "main":
21704
22558
  case "test":
22559
+ if (isBrowserRuntime()) return new ChaintracksServiceClient(chain, `https://${chain}net-chaintracks.babbage.systems`);
22560
+ return new GoChaintracksServiceClient(chain, arcadeDefaultUrl(chain), { apiPrefix: "/chaintracks/v2" });
21705
22561
  case "ttn": return new GoChaintracksServiceClient(chain, arcadeDefaultUrl(chain), { apiPrefix: "/chaintracks/v2" });
21706
22562
  case "stn": return configuredChaintracksClient(chain, stnChaintracksUrl());
21707
22563
  case "tstn": return configuredChaintracksClient(chain, tstnChaintracksUrl());
@@ -21740,7 +22596,7 @@ function createDefaultWalletServicesOptions(...[chain, arcCallbackUrl, arcCallba
21740
22596
  exchangeratesapiKey: void 0,
21741
22597
  chaintracksFiatExchangeRatesUrl,
21742
22598
  chaintracks,
21743
- arcUrl: arcDefaultUrl(chain),
22599
+ arcUrl: chain === "ttn" ? "" : arcDefaultUrl(chain),
21744
22600
  arcConfig: {
21745
22601
  apiKey: taalArcApiKey ?? void 0,
21746
22602
  deploymentId,
@@ -21756,7 +22612,7 @@ function createDefaultWalletServicesOptions(...[chain, arcCallbackUrl, arcCallba
21756
22612
  },
21757
22613
  bitailsApiKey
21758
22614
  };
21759
- const resolvedArcadeUrl = arcadeUrl;
22615
+ const resolvedArcadeUrl = arcadeUrl ?? (chain === "ttn" ? arcadeDefaultUrl(chain) : void 0);
21760
22616
  if (resolvedArcadeUrl != null && resolvedArcadeUrl !== "") {
21761
22617
  o.arcadeUrl = resolvedArcadeUrl;
21762
22618
  o.arcadeConfig = {
@@ -21786,7 +22642,7 @@ function arcDefaultUrl(chain) {
21786
22642
  case "main": return "https://arc.taal.com";
21787
22643
  case "test": return "https://arc-test.taal.com";
21788
22644
  case "stn": return stnArcadeUrl() ?? "";
21789
- case "ttn": return "https://arcade-v2-ttn-us-1.bsvblockchain.tech/";
22645
+ case "ttn": return "";
21790
22646
  case "tstn": return tstnArcadeUrl() ?? "";
21791
22647
  case "mock": return "";
21792
22648
  }
@@ -22158,6 +23014,40 @@ var ARC = class {
22158
23014
  }
22159
23015
  };
22160
23016
  //#endregion
23017
+ //#region ../src/services/providers/arcadeStatus.ts
23018
+ /**
23019
+ * Canonical classification shared by Arcade polling and SSE. Keeping this in
23020
+ * one place prevents a provider from calling a rejection "unknown" while the
23021
+ * monitor's event path calls the same status terminal.
23022
+ */
23023
+ function classifyArcadeRejection(event) {
23024
+ const extraInfo = event.extraInfo?.trim() ?? "";
23025
+ const detail = extraInfo.toLowerCase();
23026
+ if (event.status === 476 || detail.startsWith("parent rejected") || detail.includes("not final") || detail.includes("non-final")) return {
23027
+ terminal: false,
23028
+ inputConflict: false,
23029
+ reqStatus: "invalid",
23030
+ reason: event.status === 476 ? "transaction is not final" : "Arcade reported a retryable parent/locktime condition"
23031
+ };
23032
+ const orphanConflict = isArcDoubleSpendTxStatus(event.txStatus);
23033
+ const inputConflict = orphanConflict || event.status === 462 || event.status === 466 || /(?:utxo|input).*(?:spent|missing|conflict)|missing[- ]inputs?|already spent/.test(detail);
23034
+ const classifiedValidatorFailure = event.status != null && event.status >= 460 && event.status <= 475;
23035
+ const explicitInvalidStatus = isArcInvalidTxStatus(event.txStatus) && event.txStatus !== "REJECTED";
23036
+ const terminal = inputConflict || classifiedValidatorFailure || explicitInvalidStatus || extraInfo !== "";
23037
+ let reason = "Arcade supplied no durable rejection evidence";
23038
+ if (orphanConflict) reason = `Arcade reported ${event.txStatus}`;
23039
+ else if (inputConflict) reason = "Arcade supplied confirmed missing-input/conflict evidence";
23040
+ else if (classifiedValidatorFailure) reason = `Arcade supplied terminal validator code ${String(event.status)}`;
23041
+ else if (explicitInvalidStatus) reason = `Arcade reported terminal ${event.txStatus}`;
23042
+ else if (extraInfo !== "") reason = "Arcade supplied a terminal validator rejection reason";
23043
+ return {
23044
+ terminal,
23045
+ inputConflict,
23046
+ reqStatus: inputConflict ? "doubleSpend" : "invalid",
23047
+ reason
23048
+ };
23049
+ }
23050
+ //#endregion
22161
23051
  //#region ../src/services/providers/Arcade.ts
22162
23052
  function defaultDeploymentId() {
22163
23053
  return `ts-sdk-${Utils.toHex(Random(16))}`;
@@ -22427,6 +23317,73 @@ var Arcade = class {
22427
23317
  return (await this.httpClient.request(`${this.URL}/tx/${txid}`, requestOptions)).data;
22428
23318
  }
22429
23319
  /**
23320
+ * Adapt Arcade's lifecycle endpoint to the shared transaction-status
23321
+ * provider contract. This lets monitor reconciliation remain operational
23322
+ * when an explorer such as WhatsOnChain is absent. Only network-observed
23323
+ * states count as known; RECEIVED/PENDING_RETRY and terminal rejection
23324
+ * states remain unknown, while MINED/IMMUTABLE are authoritative mined
23325
+ * observations whose proof is validated separately.
23326
+ */
23327
+ async getStatusForTxids(txids) {
23328
+ const r = {
23329
+ name: this.name,
23330
+ status: "success",
23331
+ results: []
23332
+ };
23333
+ let firstError;
23334
+ r.results = await Promise.all(txids.map(async (txid) => {
23335
+ try {
23336
+ const response = await this.httpClient.request(`${this.URL}/tx/${txid}`, {
23337
+ method: "GET",
23338
+ headers: this.requestHeaders()
23339
+ });
23340
+ if (Number(response.status) === 404) return {
23341
+ txid,
23342
+ status: "unknown",
23343
+ depth: void 0
23344
+ };
23345
+ if (!response.ok || Number(response.status) !== 200) throw new WERR_INVALID_OPERATION(`Arcade transaction status response ${String(response.status)}`);
23346
+ const status = response.data.txStatus;
23347
+ if (status === "MINED" || status === "IMMUTABLE") return {
23348
+ txid,
23349
+ status: "mined",
23350
+ depth: 1
23351
+ };
23352
+ if (status === "ACCEPTED_BY_NETWORK" || status === "SEEN_ON_NETWORK" || status === "SEEN_MULTIPLE_NODES") return {
23353
+ txid,
23354
+ status: "known",
23355
+ depth: 0
23356
+ };
23357
+ const classification = classifyArcadeRejection(response.data);
23358
+ return {
23359
+ txid,
23360
+ status: "unknown",
23361
+ depth: void 0,
23362
+ providerStatus: status,
23363
+ ...classification.terminal ? {
23364
+ terminal: true,
23365
+ inputConflict: classification.inputConflict,
23366
+ statusCode: response.data.status,
23367
+ description: (response.data.extraInfo || classification.reason).slice(0, 512),
23368
+ competingTxs: (response.data.competingTxs ?? []).slice(0, 24)
23369
+ } : {}
23370
+ };
23371
+ } catch (error_) {
23372
+ firstError ??= WalletError.fromUnknown(error_);
23373
+ return {
23374
+ txid,
23375
+ status: "unknown",
23376
+ depth: void 0
23377
+ };
23378
+ }
23379
+ }));
23380
+ if (firstError != null) {
23381
+ r.status = "error";
23382
+ r.error = firstError;
23383
+ }
23384
+ return r;
23385
+ }
23386
+ /**
22430
23387
  * `getMerklePath` provider: obtain a BUMP merkle proof for a mined transaction from Arcade.
22431
23388
  *
22432
23389
  * Arcade only has a proof for transactions it tracked (i.e. broadcast through it) that have
@@ -23217,6 +24174,10 @@ var Services = class Services {
23217
24174
  name: "WhatsOnChain",
23218
24175
  service: this.whatsonchain.getStatusForTxids.bind(this.whatsonchain)
23219
24176
  });
24177
+ if (this.arcade != null) this.getStatusForTxidsServices.add({
24178
+ name: "Arcade",
24179
+ service: this.arcade.getStatusForTxids.bind(this.arcade)
24180
+ });
23220
24181
  this.getScriptHashHistoryServices = new ServiceCollection("getScriptHashHistory");
23221
24182
  if (hasWhatsOnChain) this.getScriptHashHistoryServices.add({
23222
24183
  name: "WhatsOnChain",
@@ -23233,7 +24194,7 @@ var Services = class Services {
23233
24194
  name: "GorillaPoolArcBeef",
23234
24195
  service: this.arcGorillaPool.postBeef.bind(this.arcGorillaPool)
23235
24196
  });
23236
- this.postBeefServices.add({
24197
+ if (this.options.arcUrl != null && this.options.arcUrl !== "") this.postBeefServices.add({
23237
24198
  name: "TaalArcBeef",
23238
24199
  service: this.arcTaal.postBeef.bind(this.arcTaal)
23239
24200
  });
@@ -23319,30 +24280,65 @@ var Services = class Services {
23319
24280
  async getStatusForTxids(txids, useNext) {
23320
24281
  const services = this.getStatusForTxidsServices;
23321
24282
  if (useNext === true) services.next();
23322
- let r0 = {
24283
+ let fallback = {
23323
24284
  name: "<noservices>",
23324
24285
  status: "error",
23325
24286
  error: new WERR_INTERNAL("No services available."),
23326
24287
  results: []
23327
24288
  };
24289
+ const resultsByTxid = /* @__PURE__ */ new Map();
24290
+ const unresolved = new Set(txids);
24291
+ const providerNames = [];
24292
+ let successfulProvider = false;
24293
+ const rank = (result) => {
24294
+ if (result.status === "mined") return 4;
24295
+ if (result.status === "known") return 3;
24296
+ if (result.terminal === true) return 2;
24297
+ return 1;
24298
+ };
23328
24299
  for (let tries = 0; tries < services.count; tries++) {
23329
24300
  const stc = services.serviceToCall;
23330
24301
  try {
23331
- const r = await this.getStatusForTxidsBatched(stc, txids);
24302
+ const requestedTxids = [...unresolved];
24303
+ if (requestedTxids.length === 0) break;
24304
+ const r = await this.getStatusForTxidsBatched(stc, requestedTxids);
23332
24305
  if (r.status === "success") {
23333
24306
  services.addServiceCallSuccess(stc);
23334
- r0 = r;
23335
- break;
24307
+ successfulProvider = true;
24308
+ providerNames.push(r.name);
24309
+ for (const result of r.results) {
24310
+ const current = resultsByTxid.get(result.txid);
24311
+ if (current == null || rank(result) > rank(current)) resultsByTxid.set(result.txid, result);
24312
+ if (result.status === "mined" || result.status === "known") unresolved.delete(result.txid);
24313
+ }
24314
+ services.next();
24315
+ continue;
23336
24316
  }
24317
+ fallback = r;
23337
24318
  if (r.error != null) services.addServiceCallError(stc, r.error);
23338
24319
  else services.addServiceCallFailure(stc);
23339
24320
  } catch (error_) {
23340
24321
  const e = WalletError.fromUnknown(error_);
24322
+ fallback = {
24323
+ name: stc.providerName,
24324
+ status: "error",
24325
+ error: e,
24326
+ results: []
24327
+ };
23341
24328
  services.addServiceCallError(stc, e);
23342
24329
  }
23343
24330
  services.next();
23344
24331
  }
23345
- return r0;
24332
+ if (!successfulProvider) return fallback;
24333
+ return {
24334
+ name: [...new Set(providerNames)].join(","),
24335
+ status: "success",
24336
+ results: txids.map((txid) => resultsByTxid.get(txid) ?? {
24337
+ txid,
24338
+ status: "unknown",
24339
+ depth: void 0
24340
+ })
24341
+ };
23346
24342
  }
23347
24343
  async getStatusForTxidsBatched(stc, txids) {
23348
24344
  const results = [];
@@ -23369,9 +24365,7 @@ var Services = class Services {
23369
24365
  return Utils.toHex(sha256Hash(Utils.toArray(script, "hex")));
23370
24366
  }
23371
24367
  async isUtxo(output) {
23372
- if (output.lockingScript == null) throw new WERR_INVALID_PARAMETER("output.lockingScript", "validated by storage provider validateOutputScript.");
23373
- const hash = this.hashOutputScript(Utils.toHex(output.lockingScript));
23374
- return (await this.getUtxoStatus(hash, void 0, `${output.txid ?? ""}.${output.vout}`)).isUtxo === true;
24368
+ return requireConclusiveUtxo(await classifyOutputUtxo(this, output));
23375
24369
  }
23376
24370
  async getUtxoStatus(output, outputFormat, outpoint, useNext, logger) {
23377
24371
  const services = this.getUtxoStatusServices;
@@ -23597,7 +24591,15 @@ var Services = class Services {
23597
24591
  const method = async () => {
23598
24592
  return await this.options.chaintracks.currentHeight();
23599
24593
  };
23600
- return await this.invokeChaintracksWithRetry(method, "current_height");
24594
+ try {
24595
+ return await this.invokeChaintracksWithRetry(method, "current_height");
24596
+ } catch (error_) {
24597
+ try {
24598
+ return (await this.whatsonchain.getChainInfo()).blocks;
24599
+ } catch {
24600
+ throw error_;
24601
+ }
24602
+ }
23601
24603
  }
23602
24604
  async hashToHeader(hash) {
23603
24605
  const method = async () => {
@@ -27316,17 +28318,34 @@ var ArcSSEClient = class {
27316
28318
  console.log(`${TAG} connected`);
27317
28319
  });
27318
28320
  this.es.addEventListener("status", (event) => {
28321
+ let data;
27319
28322
  try {
27320
- const data = JSON.parse(event.data);
27321
- console.log(`${TAG} event: txid=${data.txid} status=${data.txStatus}`);
27322
- if (typeof event.lastEventId === "string" && event.lastEventId !== "") {
27323
- this._lastEventId = event.lastEventId;
27324
- this.options.onLastEventIdChanged?.(event.lastEventId);
27325
- }
27326
- this.options.onEvent(data);
28323
+ data = JSON.parse(event.data);
27327
28324
  } catch {
27328
28325
  console.log(`${TAG} malformed event: ${String(event.data).substring(0, 200)}`);
28326
+ return;
28327
+ }
28328
+ console.log(`${TAG} event: txid=${data.txid} status=${data.txStatus}`);
28329
+ const receivedEventId = typeof event.lastEventId === "string" && event.lastEventId !== "" ? event.lastEventId : void 0;
28330
+ data.eventId = receivedEventId;
28331
+ let processing;
28332
+ try {
28333
+ processing = this.options.onEvent(data);
28334
+ } catch (error) {
28335
+ const e = error instanceof Error ? error : new Error(String(error));
28336
+ console.log(`${TAG} event processing failed: ${e.message}`);
28337
+ this.options.onError?.(e);
28338
+ return;
27329
28339
  }
28340
+ Promise.resolve(processing).then(async () => {
28341
+ if (receivedEventId == null) return;
28342
+ await this.options.onLastEventIdChanged?.(receivedEventId);
28343
+ this._lastEventId = receivedEventId;
28344
+ }).catch((error) => {
28345
+ const e = error instanceof Error ? error : new Error(String(error));
28346
+ console.log(`${TAG} event processing failed: ${e.message}`);
28347
+ this.options.onError?.(e);
28348
+ });
27330
28349
  });
27331
28350
  this.es.addEventListener("error", (event) => {
27332
28351
  console.log(`${TAG} error:`, JSON.stringify(event));
@@ -27361,94 +28380,6 @@ var ArcSSEClient = class {
27361
28380
  }
27362
28381
  };
27363
28382
  //#endregion
27364
- //#region ../src/monitor/tasks/TaskUnFail.ts
27365
- /**
27366
- * Setting provenTxReq status to 'unfail' when 'invalid' will attempt to find a merklePath, and if successful:
27367
- *
27368
- * 1. set the req status to 'unmined'
27369
- * 2. set the referenced txs to 'unproven'
27370
- * 3. determine if any inputs match user's existing outputs and if so update spentBy and spendable of those outputs.
27371
- * 4. set the txs outputs to spendable
27372
- *
27373
- * If it fails (to find a merklePath), returns the req status to 'invalid'.
27374
- */
27375
- var TaskUnFail = class TaskUnFail extends WalletMonitorTask {
27376
- triggerMsecs;
27377
- static taskName = "UnFail";
27378
- /**
27379
- * Set to true to trigger running this task
27380
- */
27381
- static checkNowRequested = false;
27382
- static get checkNow() {
27383
- return this.checkNowRequested;
27384
- }
27385
- static set checkNow(value) {
27386
- this.checkNowRequested = value;
27387
- }
27388
- constructor(monitor, triggerMsecs = Monitor.oneMinute * 10) {
27389
- super(monitor, TaskUnFail.taskName);
27390
- this.triggerMsecs = triggerMsecs;
27391
- }
27392
- trigger(nowMsecsSinceEpoch) {
27393
- return { run: TaskUnFail.checkNow || this.triggerMsecs > 0 && nowMsecsSinceEpoch - this.lastRunMsecsSinceEpoch > this.triggerMsecs };
27394
- }
27395
- async runTask() {
27396
- let log = "";
27397
- TaskUnFail.checkNow = false;
27398
- const limit = 100;
27399
- let offset = 0;
27400
- for (;;) {
27401
- const reqs = await this.storage.findProvenTxReqs({
27402
- partial: {},
27403
- status: ["unfail"],
27404
- paged: {
27405
- limit,
27406
- offset
27407
- }
27408
- });
27409
- if (reqs.length === 0) break;
27410
- log += `${reqs.length} reqs with status 'unfail'\n`;
27411
- const r = await this.unfail(reqs, 2);
27412
- log += `${r.log}\n`;
27413
- if (reqs.length < limit) break;
27414
- offset += limit;
27415
- }
27416
- return log;
27417
- }
27418
- async unfail(reqs, indent = 0) {
27419
- let log = "";
27420
- for (const reqApi of reqs) {
27421
- const req = new EntityProvenTxReq(reqApi);
27422
- log += " ".repeat(indent);
27423
- log += `reqId ${reqApi.provenTxReqId} txid ${reqApi.txid}: `;
27424
- if ((await this.monitor.services.getMerklePath(req.txid)).merklePath != null) {
27425
- log += "unfailed. status is now 'unmined'\n";
27426
- log += await this.unfailReq(req, indent + 2);
27427
- } else {
27428
- req.status = "invalid";
27429
- log += "returned to status 'invalid'\n";
27430
- await req.updateStorageDynamicProperties(this.storage);
27431
- }
27432
- }
27433
- return { log };
27434
- }
27435
- /**
27436
- * 2. set the referenced txs to 'unproven'
27437
- * 3. determine if any inputs match user's existing outputs and if so update spentBy and spendable of those outputs.
27438
- * 4. set the txs outputs to spendable
27439
- *
27440
- * @param req
27441
- * @param indent
27442
- * @returns
27443
- */
27444
- async unfailReq(req, indent) {
27445
- return await this.storage.runAsStorageProvider(async (sp) => await sp.unfailTransactionsForProof(req, indent, {
27446
- status: "unmined",
27447
- attempts: 0
27448
- }));
27449
- }
27450
- };
27451
- //#endregion
27452
28383
  //#region ../src/monitor/tasks/TaskArcSSE.ts
27453
28384
  /**
27454
28385
  * Monitor task that receives transaction status updates from Arcade via SSE
@@ -27492,16 +28423,14 @@ var TaskArcadeSSE = class TaskArcadeSSE extends WalletMonitorTask {
27492
28423
  arcApiKey: arcadeApiKey,
27493
28424
  lastEventId,
27494
28425
  EventSourceClass,
27495
- onEvent: (event) => {
27496
- this.pendingEvents.push(event);
27497
- },
28426
+ onEvent: async (event) => await new Promise((acknowledge) => {
28427
+ this.pendingEvents.push({
28428
+ event,
28429
+ acknowledge
28430
+ });
28431
+ }),
27498
28432
  onError: (err) => {
27499
28433
  this.logSetupEvent(`SSE error: ${err.message}`);
27500
- },
27501
- onLastEventIdChanged: (id) => {
27502
- this.monitor.options.saveLastSSEEventId?.(id).catch((e) => {
27503
- this.logSetupEvent(`failed to persist lastEventId: ${stringifyError(e)}`);
27504
- });
27505
28434
  }
27506
28435
  });
27507
28436
  this.sseClient.connect();
@@ -27515,10 +28444,21 @@ var TaskArcadeSSE = class TaskArcadeSSE extends WalletMonitorTask {
27515
28444
  return { run: this.pendingEvents.length > 0 };
27516
28445
  }
27517
28446
  async runTask() {
27518
- const events = this.pendingEvents.splice(0);
27519
- if (events.length === 0) return "";
28447
+ const eventCount = this.pendingEvents.length;
28448
+ if (eventCount === 0) return "";
27520
28449
  let log = "";
27521
- for (const event of events) log += await this.processStatusEvent(event);
28450
+ for (let i = 0; i < eventCount; i++) {
28451
+ const pending = this.pendingEvents[0];
28452
+ log += await this.processStatusEvent(pending.event);
28453
+ if (pending.event.eventId != null) try {
28454
+ await this.monitor.options.saveLastSSEEventId?.(pending.event.eventId);
28455
+ } catch (e) {
28456
+ await this.logSetupEvent(`failed to persist lastEventId: ${stringifyError(e)}`);
28457
+ throw e;
28458
+ }
28459
+ this.pendingEvents.shift();
28460
+ pending.acknowledge();
28461
+ }
27522
28462
  return log;
27523
28463
  }
27524
28464
  async fetchNow() {
@@ -27534,28 +28474,26 @@ var TaskArcadeSSE = class TaskArcadeSSE extends WalletMonitorTask {
27534
28474
  }
27535
28475
  for (const reqApi of reqs) {
27536
28476
  const req = new EntityProvenTxReq(reqApi);
27537
- const canRecoverRejectedRequest = this.canRecoverRejectedRequest(req, event);
27538
- if (ProvenTxReqTerminalStatus.includes(req.status) && !canRecoverRejectedRequest) {
28477
+ const canRecoverTerminalRequest = this.canRecoverTerminalRequest(req, event);
28478
+ if (ProvenTxReqTerminalStatus.includes(req.status) && !canRecoverTerminalRequest) {
27539
28479
  log += ` req ${req.id} already terminal: ${req.status}\n`;
27540
28480
  continue;
27541
28481
  }
27542
28482
  const note = {
27543
28483
  when: (/* @__PURE__ */ new Date()).toISOString(),
27544
28484
  what: "arcSSE",
27545
- arcStatus: event.txStatus
28485
+ arcStatus: event.txStatus,
28486
+ ...event.timestamp !== "" ? { arcTimestamp: event.timestamp } : {},
28487
+ ...event.status != null ? { statusCode: event.status } : {},
28488
+ ...event.extraInfo != null && event.extraInfo !== "" ? { extraInfo: event.extraInfo.slice(0, 512) } : {}
27546
28489
  };
27547
28490
  log += await this.applyStatusEvent(req, event, note);
27548
28491
  }
27549
28492
  this.monitor.callOnTransactionStatusChanged(event.txid, event.txStatus);
27550
28493
  return log;
27551
28494
  }
27552
- canRecoverRejectedRequest(req, event) {
27553
- return req.status === "invalid" && ([
27554
- "SENT_TO_NETWORK",
27555
- "ACCEPTED_BY_NETWORK",
27556
- "SEEN_ON_NETWORK",
27557
- "SEEN_MULTIPLE_NODES"
27558
- ].includes(event.txStatus) || ["MINED", "IMMUTABLE"].includes(event.txStatus));
28495
+ canRecoverTerminalRequest(req, event) {
28496
+ return (req.status === "invalid" || req.status === "doubleSpend") && ["MINED", "IMMUTABLE"].includes(event.txStatus);
27559
28497
  }
27560
28498
  async applyStatusEvent(req, event, note) {
27561
28499
  switch (event.txStatus) {
@@ -27565,11 +28503,16 @@ var TaskArcadeSSE = class TaskArcadeSSE extends WalletMonitorTask {
27565
28503
  case "SEEN_MULTIPLE_NODES": return await this.applyAcceptedStatus(req, note);
27566
28504
  case "MINED":
27567
28505
  case "IMMUTABLE": return await this.applyMinedStatus(req, note);
27568
- case "DOUBLE_SPEND_ATTEMPTED": return await this.applyDoubleSpendStatus(req, note);
27569
- case "REJECTED":
28506
+ case "DOUBLE_SPEND_ATTEMPTED":
28507
+ case "SEEN_IN_ORPHAN_MEMPOOL": return await this.applyDoubleSpendStatus(req, event, note);
28508
+ case "REJECTED": return await this.applyRejectedStatus(req, event, note);
28509
+ case "RECEIVED":
28510
+ case "PENDING_RETRY":
28511
+ case "STUMP_PROCESSING":
28512
+ case "UNKNOWN":
27570
28513
  req.addHistoryNote(note);
27571
28514
  await req.updateStorageDynamicProperties(this.storage);
27572
- return ` req ${req.id} rejection recorded; awaiting resolution\n`;
28515
+ return ` req ${req.id} ${event.txStatus} recorded; awaiting resolution\n`;
27573
28516
  default: return ` req ${req.id} unhandled status: ${event.txStatus}\n`;
27574
28517
  }
27575
28518
  }
@@ -27577,43 +28520,73 @@ var TaskArcadeSSE = class TaskArcadeSSE extends WalletMonitorTask {
27577
28520
  if (![
27578
28521
  "unsent",
27579
28522
  "sending",
27580
- "callback",
27581
- "invalid"
28523
+ "callback"
27582
28524
  ].includes(req.status)) return "";
27583
- const wasInvalid = req.status === "invalid";
27584
- let log = "";
27585
- if (wasInvalid) log += await new TaskUnFail(this.monitor).unfailReq(req, 4);
27586
- else if (req.notify.transactionIds != null) await this.storage.runAsStorageProvider(async (sp) => {
28525
+ if (req.notify.transactionIds != null) await this.storage.runAsStorageProvider(async (sp) => {
27587
28526
  await sp.updateTransactionsStatus(req.notify.transactionIds ?? [], "unproven");
27588
28527
  });
27589
28528
  req.status = "unmined";
27590
- if (wasInvalid) req.attempts = 0;
27591
28529
  req.wasBroadcast = true;
27592
28530
  req.addHistoryNote(note);
27593
28531
  await req.updateStorageDynamicProperties(this.storage);
27594
- return log + ` req ${req.id} => unmined\n`;
28532
+ return ` req ${req.id} => unmined\n`;
27595
28533
  }
27596
28534
  async applyMinedStatus(req, note) {
27597
- let log = "";
27598
- if (req.status === "invalid") {
27599
- log += await new TaskUnFail(this.monitor).unfailReq(req, 4);
27600
- req.status = "unmined";
27601
- req.attempts = 0;
27602
- req.wasBroadcast = true;
27603
- }
27604
28535
  req.addHistoryNote(note);
27605
28536
  await req.updateStorageDynamicProperties(this.storage);
27606
- return log + await this.fetchProofFromServices(req);
28537
+ return await this.fetchProofFromServices(req);
27607
28538
  }
27608
- async applyDoubleSpendStatus(req, note) {
27609
- req.status = "doubleSpend";
27610
- req.addHistoryNote(note);
27611
- await req.updateStorageDynamicProperties(this.storage);
27612
- const ids = req.notify.transactionIds;
27613
- if (ids != null) await this.storage.runAsStorageProvider(async (sp) => {
27614
- await sp.updateTransactionsStatus(ids, "failed");
28539
+ async applyDoubleSpendStatus(req, event, note) {
28540
+ return await this.applyTerminalFailure(req, note, classifyArcadeRejection(event));
28541
+ }
28542
+ classifyRejection(event) {
28543
+ return classifyArcadeRejection(event);
28544
+ }
28545
+ async applyRejectedStatus(req, event, note) {
28546
+ const classification = this.classifyRejection(event);
28547
+ if (!classification.terminal) {
28548
+ req.addHistoryNote({
28549
+ ...note,
28550
+ what: "arcSSERejectionPending",
28551
+ reason: classification.reason
28552
+ });
28553
+ await req.updateStorageDynamicProperties(this.storage);
28554
+ return ` req ${req.id} rejection recorded; awaiting resolution\n`;
28555
+ }
28556
+ return await this.applyTerminalFailure(req, note, classification);
28557
+ }
28558
+ async applyTerminalFailure(req, note, classification) {
28559
+ let quarantined = {
28560
+ checked: 0,
28561
+ staleConfirmed: 0,
28562
+ staleOutpoints: []
28563
+ };
28564
+ await this.storage.runAsStorageProvider(async (sp) => {
28565
+ await sp.transaction(async (trx) => {
28566
+ req.status = classification.reqStatus;
28567
+ req.addHistoryNote({
28568
+ ...note,
28569
+ what: "arcSSETerminalRejection",
28570
+ reason: classification.reason,
28571
+ inputConflict: classification.inputConflict
28572
+ });
28573
+ await req.updateStorageDynamicProperties(sp, trx);
28574
+ const ids = req.notify.transactionIds;
28575
+ if (ids != null) await sp.updateTransactionsStatus(ids, "failed", trx);
28576
+ if (classification.inputConflict) {
28577
+ quarantined = await quarantineReqInputs(req, sp, trx);
28578
+ req.addHistoryNote({
28579
+ when: (/* @__PURE__ */ new Date()).toISOString(),
28580
+ what: "arcSSEInputQuarantine",
28581
+ checked: quarantined.checked,
28582
+ confirmed: quarantined.staleConfirmed,
28583
+ ...quarantined.staleOutpoints.length > 0 ? { outpoints: quarantined.staleOutpoints.join(",") } : {}
28584
+ });
28585
+ await req.updateStorageDynamicProperties(sp, trx);
28586
+ }
28587
+ });
27615
28588
  });
27616
- return ` req ${req.id} => doubleSpend\n`;
28589
+ return ` req ${req.id} => ${classification.reqStatus}; quarantined ${quarantined.staleConfirmed} local input copy/copies\n`;
27617
28590
  }
27618
28591
  /**
27619
28592
  * Complete a MINED/IMMUTABLE status by using the configured proof providers.
@@ -28025,9 +28998,104 @@ var TaskCheckNoSends = class TaskCheckNoSends extends WalletMonitorTask {
28025
28998
  }
28026
28999
  };
28027
29000
  //#endregion
29001
+ //#region ../src/monitor/tasks/TaskUnFail.ts
29002
+ /**
29003
+ * Setting provenTxReq status to 'unfail' when 'invalid' will attempt to find a merklePath, and if successful:
29004
+ *
29005
+ * 1. set the req status to 'unmined'
29006
+ * 2. set the referenced txs to 'unproven'
29007
+ * 3. determine if any inputs match user's existing outputs and if so update spentBy and spendable of those outputs.
29008
+ * 4. set the txs outputs to spendable
29009
+ *
29010
+ * If it fails (to find a merklePath), returns the req status to 'invalid'.
29011
+ */
29012
+ var TaskUnFail = class TaskUnFail extends WalletMonitorTask {
29013
+ triggerMsecs;
29014
+ static taskName = "UnFail";
29015
+ /**
29016
+ * Set to true to trigger running this task
29017
+ */
29018
+ static checkNowRequested = false;
29019
+ static get checkNow() {
29020
+ return this.checkNowRequested;
29021
+ }
29022
+ static set checkNow(value) {
29023
+ this.checkNowRequested = value;
29024
+ }
29025
+ constructor(monitor, triggerMsecs = Monitor.oneMinute * 10) {
29026
+ super(monitor, TaskUnFail.taskName);
29027
+ this.triggerMsecs = triggerMsecs;
29028
+ }
29029
+ trigger(nowMsecsSinceEpoch) {
29030
+ return { run: TaskUnFail.checkNow || this.triggerMsecs > 0 && nowMsecsSinceEpoch - this.lastRunMsecsSinceEpoch > this.triggerMsecs };
29031
+ }
29032
+ async runTask() {
29033
+ let log = "";
29034
+ TaskUnFail.checkNow = false;
29035
+ const limit = 100;
29036
+ let offset = 0;
29037
+ for (;;) {
29038
+ const reqs = await this.storage.findProvenTxReqs({
29039
+ partial: {},
29040
+ status: ["unfail"],
29041
+ paged: {
29042
+ limit,
29043
+ offset
29044
+ }
29045
+ });
29046
+ if (reqs.length === 0) break;
29047
+ log += `${reqs.length} reqs with status 'unfail'\n`;
29048
+ const r = await this.unfail(reqs, 2);
29049
+ log += `${r.log}\n`;
29050
+ if (reqs.length < limit) break;
29051
+ offset += limit;
29052
+ }
29053
+ return log;
29054
+ }
29055
+ async unfail(reqs, indent = 0) {
29056
+ let log = "";
29057
+ for (const reqApi of reqs) {
29058
+ const req = new EntityProvenTxReq(reqApi);
29059
+ log += " ".repeat(indent);
29060
+ log += `reqId ${reqApi.provenTxReqId} txid ${reqApi.txid}: `;
29061
+ if ((await this.monitor.services.getMerklePath(req.txid)).merklePath != null) {
29062
+ log += "unfailed. status is now 'unmined'\n";
29063
+ log += await this.unfailReq(req, indent + 2);
29064
+ } else {
29065
+ req.status = "invalid";
29066
+ log += "returned to status 'invalid'\n";
29067
+ await req.updateStorageDynamicProperties(this.storage);
29068
+ }
29069
+ }
29070
+ return { log };
29071
+ }
29072
+ /**
29073
+ * 2. set the referenced txs to 'unproven'
29074
+ * 3. determine if any inputs match user's existing outputs and if so update spentBy and spendable of those outputs.
29075
+ * 4. set the txs outputs to spendable
29076
+ *
29077
+ * @param req
29078
+ * @param indent
29079
+ * @returns
29080
+ */
29081
+ async unfailReq(req, indent) {
29082
+ return await this.storage.runAsStorageProvider(async (sp) => await sp.unfailTransactionsForProof(req, indent, {
29083
+ status: "unmined",
29084
+ attempts: 0
29085
+ }));
29086
+ }
29087
+ };
29088
+ //#endregion
28028
29089
  //#region ../src/monitor/tasks/TaskReviewUtxos.ts
29090
+ const REVIEW_PAGE_DEFAULT_LIMIT = 20;
29091
+ const REVIEW_PAGE_MAX_LIMIT = 250;
28029
29092
  /**
28030
- * Use the reviewByIdentityKey method to review the utxos of a specific user by their identityKey.
29093
+ * Use the reviewByIdentityKey method to scan the UTXOs of a specific user by
29094
+ * identity key. The scan is read-only unless the caller explicitly requests
29095
+ * release, and release remains blocked if any provider result is inconclusive.
29096
+ * Operator UIs should use reviewPageByIdentityKey: it bounds each provider
29097
+ * round-trip and may explicitly release only the conclusive spent subset while
29098
+ * reporting unknowns.
28031
29099
  *
28032
29100
  * The task itself is disabled and will not run on a schedule; review must be triggered manually by calling reviewByIdentityKey.
28033
29101
  */
@@ -28044,7 +29112,7 @@ var TaskReviewUtxos = class TaskReviewUtxos extends WalletMonitorTask {
28044
29112
  static set checkNow(value) {
28045
29113
  this.checkNowRequested = value;
28046
29114
  }
28047
- constructor(monitor, triggerMsecs = 0, userLimit = 10, userOffset = 0, tags = ["release", "all"]) {
29115
+ constructor(monitor, triggerMsecs = 0, userLimit = 10, userOffset = 0, tags = ["all"]) {
28048
29116
  super(monitor, TaskReviewUtxos.taskName);
28049
29117
  this.triggerMsecs = triggerMsecs;
28050
29118
  this.userLimit = userLimit;
@@ -28058,8 +29126,8 @@ var TaskReviewUtxos = class TaskReviewUtxos extends WalletMonitorTask {
28058
29126
  TaskReviewUtxos.checkNow = false;
28059
29127
  return "TaskReviewUtxos is disabled; use reviewByIdentityKey instead.\n";
28060
29128
  }
28061
- async reviewByIdentityKey(identityKey, mode = "all") {
28062
- const tags = ["release", ...mode === "all" ? ["all"] : []];
29129
+ async reviewByIdentityKey(identityKey, mode = "all", release = false) {
29130
+ const tags = [...release ? ["release"] : [], ...mode === "all" ? ["all"] : []];
28063
29131
  const vargs = {
28064
29132
  basket: specOpInvalidChange,
28065
29133
  tags,
@@ -28087,8 +29155,150 @@ var TaskReviewUtxos = class TaskReviewUtxos extends WalletMonitorTask {
28087
29155
  return this.toUserLog(user, result.outputs, result.totalOutputs, total, tags);
28088
29156
  });
28089
29157
  }
29158
+ async reviewPageByIdentityKey(identityKey, mode = "all", release = false, pageLimit = REVIEW_PAGE_DEFAULT_LIMIT, offset = 0) {
29159
+ pageLimit = Math.min(Math.max(Math.trunc(pageLimit), 1), REVIEW_PAGE_MAX_LIMIT);
29160
+ offset = Math.max(Math.trunc(offset), 0);
29161
+ return await this.storage.runAsStorageProvider(async (sp) => {
29162
+ const user = (await sp.findUsers({ partial: { identityKey } }))[0];
29163
+ if (!user) return {
29164
+ found: false,
29165
+ identityKey,
29166
+ mode,
29167
+ release,
29168
+ offset,
29169
+ pageLimit,
29170
+ sourceScanned: 0,
29171
+ complete: true,
29172
+ checked: 0,
29173
+ confirmedUnspent: 0,
29174
+ confirmedSpent: 0,
29175
+ unknown: 0,
29176
+ confirmedSpentSatoshis: 0,
29177
+ released: 0,
29178
+ releasedSatoshis: 0,
29179
+ providers: [],
29180
+ providerCount: 0,
29181
+ providersTruncated: false,
29182
+ log: `identityKey ${identityKey} was not found\n`
29183
+ };
29184
+ let basketId;
29185
+ if (mode === "change") {
29186
+ basketId = (await sp.findOutputBaskets({ partial: {
29187
+ userId: user.userId,
29188
+ name: "default"
29189
+ } }))[0]?.basketId;
29190
+ if (basketId == null) return this.emptyPage(user, mode, release, pageLimit, offset);
29191
+ }
29192
+ const sourceOutputs = await sp.findOutputs({
29193
+ partial: {
29194
+ userId: user.userId,
29195
+ spendable: true,
29196
+ ...basketId != null ? { basketId } : {}
29197
+ },
29198
+ txStatus: [
29199
+ "completed",
29200
+ "unproven",
29201
+ "nosend",
29202
+ "sending"
29203
+ ],
29204
+ noScript: true,
29205
+ paged: {
29206
+ limit: pageLimit,
29207
+ offset
29208
+ }
29209
+ });
29210
+ const candidates = sourceOutputs.filter((output) => output.basketId != null);
29211
+ const review = await reviewUtxoOutputs(sp, {
29212
+ userId: user.userId,
29213
+ identityKey: user.identityKey
29214
+ }, candidates, release ? "conclusive" : "none");
29215
+ const complete = sourceOutputs.length < pageLimit;
29216
+ const nextOffset = complete ? void 0 : offset + sourceOutputs.length - review.diagnostics.released;
29217
+ const target = mode === "all" ? "spendable utxos" : "spendable change utxos";
29218
+ const action = release ? "released" : "found";
29219
+ let log = `userId ${user.userId}: page checked ${review.diagnostics.checked} ${target}; ${review.diagnostics.confirmedSpent} confirmed spent, ${review.diagnostics.confirmedUnspent} confirmed unspent, ${review.diagnostics.unknown} unknown; ${action} ${review.diagnostics.released}, ${user.identityKey}\n`;
29220
+ for (const output of review.confirmedSpentOutputs) log += ` ${output.txid}.${output.vout} ${output.satoshis} now ${output.spendable ? "spendable" : "spent"}\n`;
29221
+ if (review.unknownOutputs.length > 0) log += ` ${review.unknownOutputs.length} output(s) quarantined from release pending a conclusive provider result\n`;
29222
+ if (nextOffset != null) log += ` continue at offset ${nextOffset}\n`;
29223
+ return {
29224
+ found: true,
29225
+ userId: user.userId,
29226
+ identityKey: user.identityKey,
29227
+ mode,
29228
+ release,
29229
+ offset,
29230
+ pageLimit,
29231
+ sourceScanned: sourceOutputs.length,
29232
+ complete,
29233
+ ...nextOffset != null ? { nextOffset } : {},
29234
+ ...review.diagnostics,
29235
+ log
29236
+ };
29237
+ });
29238
+ }
29239
+ emptyPage(user, mode, release, pageLimit, offset) {
29240
+ return {
29241
+ found: true,
29242
+ userId: user.userId,
29243
+ identityKey: user.identityKey,
29244
+ mode,
29245
+ release,
29246
+ offset,
29247
+ pageLimit,
29248
+ sourceScanned: 0,
29249
+ complete: true,
29250
+ checked: 0,
29251
+ confirmedUnspent: 0,
29252
+ confirmedSpent: 0,
29253
+ unknown: 0,
29254
+ confirmedSpentSatoshis: 0,
29255
+ released: 0,
29256
+ releasedSatoshis: 0,
29257
+ providers: [],
29258
+ providerCount: 0,
29259
+ providersTruncated: false,
29260
+ log: `userId ${user.userId}: no invalid utxos found, ${user.identityKey}\n`
29261
+ };
29262
+ }
29263
+ /**
29264
+ * Report managed-change liquidity without changing it. Monitor deliberately
29265
+ * has no signing authority; progressive migration occurs only during a
29266
+ * caller-authorized createAction.
29267
+ */
29268
+ async reviewManagedChangeByIdentityKey(identityKey) {
29269
+ return await this.storage.runAsStorageProvider(async (sp) => {
29270
+ const user = (await sp.findUsers({ partial: { identityKey } }))[0];
29271
+ if (user == null) return `identityKey ${identityKey} was not found\n`;
29272
+ const basket = verifyOne(await sp.findOutputBaskets({ partial: {
29273
+ userId: user.userId,
29274
+ name: "default"
29275
+ } }));
29276
+ const outputs = (await sp.findOutputs({
29277
+ partial: {
29278
+ userId: user.userId,
29279
+ basketId: basket.basketId,
29280
+ spendable: true,
29281
+ ...managedChangeOutputFields
29282
+ },
29283
+ txStatus: [
29284
+ "completed",
29285
+ "unproven",
29286
+ "sending"
29287
+ ],
29288
+ noScript: true
29289
+ })).filter(isAutoSpendableChangeOutput);
29290
+ const reserved = new Set(await sp.findReservedActionBatchOutputIds(outputs.map((output) => output.outputId)));
29291
+ const statuses = await sp.findTransactionStatusesByIds(user.userId, outputs.map((output) => output.transactionId));
29292
+ const preferred = Math.max(1, basket.minimumDesiredUTXOValue);
29293
+ const healthy = outputs.filter((output) => output.satoshis >= preferred);
29294
+ const undersized = outputs.filter((output) => output.satoshis < preferred);
29295
+ const countStatus = (status) => outputs.filter((output) => statuses.get(output.transactionId) === status).length;
29296
+ const satoshis = outputs.reduce((sum, output) => sum + output.satoshis, 0);
29297
+ return `userId ${user.userId}: managed change ${outputs.length}/${basket.numberOfDesiredUTXOs}, healthy ${healthy.length}, undersized ${undersized.length}, reserved ${reserved.size}, completed ${countStatus("completed")}, unproven ${countStatus("unproven")}, sending ${countStatus("sending")}, satoshis ${satoshis}, preferred minimum ${preferred}\n`;
29298
+ });
29299
+ }
28090
29300
  toUserLog(user, outputs, totalOutputs, total, tags) {
28091
- const action = tags.includes("release") ? "updated to unspendable" : "found";
29301
+ const action = tags.includes("release") ? "confirmed spent and updated to unspendable" : "confirmed spent";
28092
29302
  const target = tags.includes("all") ? "spendable utxos" : "spendable change utxos";
28093
29303
  let log = `userId ${user.userId}: ${totalOutputs} ${target} ${action}, total ${total}, ${user.identityKey}\n`;
28094
29304
  for (const output of outputs) log += ` ${output.outpoint} ${output.satoshis} now ${output.spendable ? "spendable" : "spent"}\n`;
@@ -28097,6 +29307,13 @@ var TaskReviewUtxos = class TaskReviewUtxos extends WalletMonitorTask {
28097
29307
  };
28098
29308
  //#endregion
28099
29309
  //#region ../src/monitor/tasks/TaskReviewDoubleSpends.ts
29310
+ function hasDurableInputConflict(req) {
29311
+ try {
29312
+ return JSON.parse(req.history).notes?.some((note) => note.inputConflict === true || note.what === "arcSSEInputQuarantine") === true;
29313
+ } catch {
29314
+ return true;
29315
+ }
29316
+ }
28100
29317
  /**
28101
29318
  * Review recent reqs in terminal 'doubleSpend' state and move any false positives
28102
29319
  * back to 'unfail' so existing recovery handling can re-process them.
@@ -28164,14 +29381,16 @@ var TaskReviewDoubleSpends = class TaskReviewDoubleSpends extends WalletMonitorT
28164
29381
  let lastRetainedDoubleSpendIndex = -1;
28165
29382
  let retainedDoubleSpendCount = 0;
28166
29383
  for (const req of reqs) {
28167
- const status = (await this.monitor.services.getStatusForTxids([req.txid])).results[0]?.status;
29384
+ const gsr = await this.monitor.services.getStatusForTxids([req.txid]);
29385
+ const status = gsr.status === "success" ? gsr.results[0]?.status : void 0;
29386
+ const durableInputConflict = hasDurableInputConflict(req);
28168
29387
  reviewed.push(req);
28169
- if (status === "unknown") {
29388
+ if (!(status === "mined" || status === "known" && !durableInputConflict)) {
28170
29389
  lastRetainedDoubleSpendIndex = reviewed.length - 1;
28171
29390
  retainedDoubleSpendCount += 1;
28172
29391
  } else {
28173
29392
  unfails.push(req.provenTxReqId);
28174
- log += `unfail ${req.provenTxReqId} ${req.txid} status:${status}\n`;
29393
+ log += `unfail ${req.provenTxReqId} ${req.txid} status:${status} durableInputConflict:${String(durableInputConflict)}\n`;
28175
29394
  }
28176
29395
  }
28177
29396
  if (unfails.length > 0) await this.storage.runAsStorageProvider(async (sp) => {
@@ -28210,6 +29429,168 @@ var TaskReviewDoubleSpends = class TaskReviewDoubleSpends extends WalletMonitorT
28210
29429
  }
28211
29430
  };
28212
29431
  //#endregion
29432
+ //#region ../src/monitor/tasks/TaskReconcilePendingTransactions.ts
29433
+ const REVIEW_STATUSES = [
29434
+ "callback",
29435
+ "unmined",
29436
+ "sending",
29437
+ "unknown",
29438
+ "unconfirmed"
29439
+ ];
29440
+ /**
29441
+ * Poll durable transaction lifecycle state for aged, non-terminal requests.
29442
+ * This closes the gap where an Arcade rejection event was missed before the
29443
+ * monitor subscribed or while it was offline. Unknown/provider-error results
29444
+ * never mutate storage; only a provider's explicit terminal verdict does.
29445
+ */
29446
+ var TaskReconcilePendingTransactions = class TaskReconcilePendingTransactions extends WalletMonitorTask {
29447
+ triggerMsecs;
29448
+ reviewLimit;
29449
+ minAgeMinutes;
29450
+ triggerQuickMsecs;
29451
+ static taskName = "ReconcilePendingTransactions";
29452
+ triggerNextMsecs;
29453
+ constructor(monitor, triggerMsecs = Monitor.oneMinute * 12, reviewLimit = 100, minAgeMinutes = 60, triggerQuickMsecs = Monitor.oneMinute) {
29454
+ super(monitor, TaskReconcilePendingTransactions.taskName);
29455
+ this.triggerMsecs = triggerMsecs;
29456
+ this.reviewLimit = reviewLimit;
29457
+ this.minAgeMinutes = minAgeMinutes;
29458
+ this.triggerQuickMsecs = triggerQuickMsecs;
29459
+ this.triggerNextMsecs = this.triggerQuickMsecs;
29460
+ }
29461
+ trigger(nowMsecsSinceEpoch) {
29462
+ return { run: this.triggerNextMsecs > 0 && nowMsecsSinceEpoch - this.lastRunMsecsSinceEpoch > this.triggerNextMsecs };
29463
+ }
29464
+ async getCheckpoint() {
29465
+ let events = [];
29466
+ await this.storage.runAsStorageProvider(async (sp) => {
29467
+ events = await sp.findMonitorEvents({
29468
+ partial: { event: TaskReconcilePendingTransactions.taskName },
29469
+ orderDescending: true,
29470
+ paged: { limit: 5 }
29471
+ });
29472
+ });
29473
+ for (const event of events) {
29474
+ if (!event.details) continue;
29475
+ try {
29476
+ const parsed = JSON.parse(event.details);
29477
+ if (parsed.cycleComplete === true) return void 0;
29478
+ if (typeof parsed.resumeOffset === "number") return {
29479
+ resumeOffset: parsed.resumeOffset,
29480
+ expectedProvenTxReqId: parsed.expectedProvenTxReqId
29481
+ };
29482
+ } catch {
29483
+ continue;
29484
+ }
29485
+ }
29486
+ }
29487
+ async runTask() {
29488
+ const checkpoint = await this.getCheckpoint();
29489
+ const updatedBefore = /* @__PURE__ */ new Date(Date.now() - this.minAgeMinutes * 60 * 1e3);
29490
+ const reqs = await this.findReqsToReview(checkpoint);
29491
+ const eligible = reqs.filter((req) => req.updated_at <= updatedBefore);
29492
+ const provider = eligible.length === 0 ? void 0 : await this.monitor.services.getStatusForTxids(eligible.map((req) => req.txid));
29493
+ const results = new Map(provider?.results.map((result) => [result.txid, result]) ?? []);
29494
+ let reconciled = 0;
29495
+ let inputConflicts = 0;
29496
+ let reviewLog = "";
29497
+ const retained = [];
29498
+ for (const req of reqs) {
29499
+ const result = results.get(req.txid);
29500
+ if (req.updated_at > updatedBefore || provider?.status !== "success" || result?.terminal !== true) {
29501
+ retained.push(req);
29502
+ continue;
29503
+ }
29504
+ if (!await this.applyTerminalVerdict(req, result, provider.name)) {
29505
+ retained.push(req);
29506
+ continue;
29507
+ }
29508
+ reconciled++;
29509
+ if (result.inputConflict === true) inputConflicts++;
29510
+ reviewLog += `reconciled ${req.provenTxReqId} ${req.txid} ${result.providerStatus ?? "terminal"} => ${result.inputConflict === true ? "doubleSpend" : "invalid"}\n`;
29511
+ }
29512
+ const fullPage = reqs.length >= this.reviewLimit;
29513
+ this.triggerNextMsecs = fullPage ? this.triggerQuickMsecs : this.triggerMsecs;
29514
+ let resumeOffset;
29515
+ let expectedProvenTxReqId;
29516
+ if (fullPage) if (retained.length === 0) resumeOffset = reqs.sourceOffset ?? 0;
29517
+ else {
29518
+ resumeOffset = (reqs.sourceOffset ?? 0) + retained.length - 1;
29519
+ expectedProvenTxReqId = retained.at(-1)?.provenTxReqId;
29520
+ }
29521
+ return JSON.stringify({
29522
+ reviewed: eligible.length,
29523
+ reconciled,
29524
+ inputConflicts,
29525
+ retained: retained.length,
29526
+ reviewLimit: this.reviewLimit,
29527
+ minAgeMinutes: this.minAgeMinutes,
29528
+ cycleComplete: !fullPage,
29529
+ ...resumeOffset != null ? { resumeOffset } : {},
29530
+ ...expectedProvenTxReqId != null ? { expectedProvenTxReqId } : {},
29531
+ reviewLog
29532
+ });
29533
+ }
29534
+ async findReqsToReview(checkpoint) {
29535
+ let offset = checkpoint?.resumeOffset ?? 0;
29536
+ if (checkpoint?.expectedProvenTxReqId != null) offset = (await this.storage.findProvenTxReqs({
29537
+ partial: {},
29538
+ status: [...REVIEW_STATUSES],
29539
+ paged: {
29540
+ limit: 1,
29541
+ offset
29542
+ }
29543
+ }))[0]?.provenTxReqId === checkpoint.expectedProvenTxReqId ? offset + 1 : 0;
29544
+ const reqs = await this.storage.findProvenTxReqs({
29545
+ partial: {},
29546
+ status: [...REVIEW_STATUSES],
29547
+ paged: {
29548
+ limit: this.reviewLimit,
29549
+ offset
29550
+ }
29551
+ });
29552
+ reqs.sourceOffset = offset;
29553
+ return reqs;
29554
+ }
29555
+ async applyTerminalVerdict(reqApi, result, provider) {
29556
+ let applied = false;
29557
+ await this.storage.runAsStorageProvider(async (sp) => {
29558
+ await sp.transaction(async (trx) => {
29559
+ const req = new EntityProvenTxReq(reqApi);
29560
+ await req.refreshFromStorage(sp, trx);
29561
+ if (!REVIEW_STATUSES.includes(req.status)) return;
29562
+ req.status = result.inputConflict === true ? "doubleSpend" : "invalid";
29563
+ req.addHistoryNote({
29564
+ when: (/* @__PURE__ */ new Date()).toISOString(),
29565
+ what: "proactiveTerminalReconciliation",
29566
+ provider,
29567
+ providerStatus: result.providerStatus,
29568
+ statusCode: result.statusCode,
29569
+ description: result.description,
29570
+ inputConflict: result.inputConflict === true,
29571
+ competingTxs: result.competingTxs?.join(",")
29572
+ });
29573
+ await req.updateStorageDynamicProperties(sp, trx);
29574
+ if (req.notify.transactionIds != null) await sp.updateTransactionsStatus(req.notify.transactionIds, "failed", trx);
29575
+ if (result.inputConflict === true) {
29576
+ const quarantined = await quarantineReqInputs(req, sp, trx);
29577
+ req.addHistoryNote({
29578
+ when: (/* @__PURE__ */ new Date()).toISOString(),
29579
+ what: "proactiveInputQuarantine",
29580
+ checked: quarantined.checked,
29581
+ confirmed: quarantined.staleConfirmed,
29582
+ ...quarantined.staleOutpoints.length > 0 ? { outpoints: quarantined.staleOutpoints.join(",") } : {}
29583
+ });
29584
+ await req.updateStorageDynamicProperties(sp, trx);
29585
+ }
29586
+ applied = true;
29587
+ });
29588
+ });
29589
+ if (applied) this.monitor.callOnTransactionStatusChanged(reqApi.txid, result.providerStatus ?? "REJECTED");
29590
+ return applied;
29591
+ }
29592
+ };
29593
+ //#endregion
28213
29594
  //#region ../src/monitor/tasks/TaskReviewProvenTxs.ts
28214
29595
  /**
28215
29596
  * Backup verification task for recent proven_txs records.
@@ -28468,14 +29849,14 @@ var Monitor = class Monitor {
28468
29849
  purgeFailedAge: 5 * Monitor.oneDay
28469
29850
  };
28470
29851
  addAllTasksToOther() {
28471
- this._otherTasks.push(new TaskClock(this), new TaskNewHeader(this), new TaskMonitorCallHistory(this), new TaskSendWaiting(this), new TaskCheckForProofs(this), new TaskCheckNoSends(this), new TaskFailAbandoned(this), new TaskUnFail(this), new TaskReviewStatus(this), new TaskReorg(this), new TaskReviewUtxos(this), new TaskReviewDoubleSpends(this), new TaskReviewProvenTxs(this), new TaskCleanupActionBatches(this), new TaskPurge(this, this.defaultPurgeParams));
29852
+ this._otherTasks.push(new TaskClock(this), new TaskNewHeader(this), new TaskMonitorCallHistory(this), new TaskSendWaiting(this), new TaskCheckForProofs(this), new TaskCheckNoSends(this), new TaskFailAbandoned(this), new TaskUnFail(this), new TaskReviewStatus(this), new TaskReorg(this), new TaskReviewUtxos(this), new TaskReviewDoubleSpends(this), new TaskReconcilePendingTransactions(this), new TaskReviewProvenTxs(this), new TaskCleanupActionBatches(this), new TaskPurge(this, this.defaultPurgeParams));
28472
29853
  if (this.chain === "mock") this._otherTasks.push(new TaskMineBlock(this));
28473
29854
  }
28474
29855
  /**
28475
29856
  * Default tasks with settings appropriate for a single user storage
28476
29857
  */
28477
29858
  addDefaultTasks() {
28478
- this._tasks.push(new TaskClock(this), new TaskNewHeader(this), new TaskMonitorCallHistory(this), new TaskSendWaiting(this, 8 * Monitor.oneSecond, 7 * Monitor.oneSecond), new TaskCheckForProofs(this, 2 * Monitor.oneHour), new TaskCheckNoSends(this), new TaskFailAbandoned(this, 8 * Monitor.oneMinute), new TaskUnFail(this), new TaskReviewStatus(this), new TaskReorg(this), new TaskReviewDoubleSpends(this), new TaskReviewProvenTxs(this), new TaskCleanupActionBatches(this), new TaskArcadeSSE(this));
29859
+ this._tasks.push(new TaskClock(this), new TaskNewHeader(this), new TaskMonitorCallHistory(this), new TaskSendWaiting(this, 8 * Monitor.oneSecond, 7 * Monitor.oneSecond), new TaskCheckForProofs(this, 2 * Monitor.oneHour), new TaskCheckNoSends(this), new TaskFailAbandoned(this, 8 * Monitor.oneMinute), new TaskUnFail(this), new TaskReviewStatus(this), new TaskReorg(this), new TaskReviewDoubleSpends(this), new TaskReconcilePendingTransactions(this), new TaskReviewProvenTxs(this), new TaskCleanupActionBatches(this), new TaskArcadeSSE(this));
28479
29860
  this._otherTasks.push(new TaskPurge(this, this.defaultPurgeParams, 6 * Monitor.oneHour), new TaskReviewUtxos(this));
28480
29861
  if (this.chain === "mock") this._tasks.push(new TaskMineBlock(this));
28481
29862
  }
@@ -28483,7 +29864,7 @@ var Monitor = class Monitor {
28483
29864
  * Tasks appropriate for multi-user storage
28484
29865
  */
28485
29866
  addMultiUserTasks() {
28486
- this._tasks.push(new TaskClock(this), new TaskNewHeader(this), new TaskMonitorCallHistory(this), new TaskSendWaiting(this, 8 * Monitor.oneSecond, 7 * Monitor.oneSecond), new TaskCheckForProofs(this, 2 * Monitor.oneHour), new TaskCheckNoSends(this), new TaskFailAbandoned(this, 8 * Monitor.oneMinute), new TaskUnFail(this), new TaskReviewStatus(this), new TaskReorg(this), new TaskReviewDoubleSpends(this), new TaskReviewProvenTxs(this), new TaskCleanupActionBatches(this), new TaskArcadeSSE(this));
29867
+ this._tasks.push(new TaskClock(this), new TaskNewHeader(this), new TaskMonitorCallHistory(this), new TaskSendWaiting(this, 8 * Monitor.oneSecond, 7 * Monitor.oneSecond), new TaskCheckForProofs(this, 2 * Monitor.oneHour), new TaskCheckNoSends(this), new TaskFailAbandoned(this, 8 * Monitor.oneMinute), new TaskUnFail(this), new TaskReviewStatus(this), new TaskReorg(this), new TaskReviewDoubleSpends(this), new TaskReconcilePendingTransactions(this), new TaskReviewProvenTxs(this), new TaskCleanupActionBatches(this), new TaskArcadeSSE(this));
28487
29868
  this._otherTasks.push(new TaskPurge(this, this.defaultPurgeParams), new TaskReviewUtxos(this));
28488
29869
  if (this.chain === "mock") this._tasks.push(new TaskMineBlock(this));
28489
29870
  }
@@ -28854,6 +30235,7 @@ var SetupClient = class SetupClient {
28854
30235
  model: "sat/kb",
28855
30236
  value: 100
28856
30237
  },
30238
+ managedChangePolicy: args.managedChangePolicy,
28857
30239
  scriptVerifier: args.scriptVerifier
28858
30240
  });
28859
30241
  await storage.migrate(args.databaseName, randomBytesHex(33));
@@ -33443,7 +34825,7 @@ var WalletPermissionsManager = class WalletPermissionsManager {
33443
34825
  basket: basketName,
33444
34826
  tags
33445
34827
  }],
33446
- options: { acceptDelayedBroadcast: false }
34828
+ options: { acceptDelayedBroadcast: true }
33447
34829
  }, this.adminOriginator);
33448
34830
  }
33449
34831
  async mapWithConcurrency(items, concurrency, fn) {
@@ -33507,7 +34889,7 @@ var WalletPermissionsManager = class WalletPermissionsManager {
33507
34889
  await this.createAction({
33508
34890
  description: `Grant ${built.length} permissions`,
33509
34891
  outputs: built.map((b) => b.output),
33510
- options: { acceptDelayedBroadcast: false }
34892
+ options: { acceptDelayedBroadcast: true }
33511
34893
  }, this.adminOriginator);
33512
34894
  return built.map((b) => b.request);
33513
34895
  }, strict);
@@ -34895,6 +36277,6 @@ var WalletPermissionsManager = class WalletPermissionsManager {
34895
36277
  }
34896
36278
  };
34897
36279
  //#endregion
34898
- export { ARGON2ID_DEFAULT_HASH_LENGTH, ARGON2ID_DEFAULT_ITERATIONS, ARGON2ID_DEFAULT_MEMORY_KIB, ARGON2ID_DEFAULT_PARALLELISM, ARGON2ID_MAX_ITERATIONS, ARGON2ID_MAX_MEMORY_KIB, ARGON2ID_MAX_PARALLELISM, AuthMethodInteractor, BHServiceClient, BulkFileDataManager, BulkFileDataReader, BulkFilesReader, BulkFilesReaderFs, BulkFilesReaderStorage, BulkIngestorBase, BulkIngestorCDN, BulkIngestorCDNBabbage, BulkIngestorChaintracks, BulkIngestorWhatsOnChainCdn, BulkStorageBase, CWIStyleWalletManager, Chaintracks, ChaintracksChainTracker, ChaintracksFetch, ChaintracksFetchError, ChaintracksServiceClient, ChaintracksStorageBase, ChaintracksStorageIdb, ChaintracksStorageNoDb, DEFAULT_PROFILE_ID, DEFAULT_SETTINGS, DevConsoleInteractor, EntityBase, EntityCertificate, EntityCertificateField, EntityCommission, EntityOutput, EntityOutputBasket, EntityOutputTag, EntityOutputTagMap, EntityProvenTx, EntityProvenTxReq, EntitySyncState, EntityTransaction, EntityTxLabel, EntityTxLabelMap, EntityUser, GoChaintracksServiceClient, HeightRange, KDF_MAX_HASH_LENGTH, LiveIngestorBase, LiveIngestorChaintracksSSE, LiveIngestorWhatsOnChainPoll, MAX_STATE_SNAPSHOT_BYTES, MergeEntity, Monitor, OverlayUMPTokenInteractor, PBKDF2_MAX_ITERATIONS, PBKDF2_NUM_ROUNDS, PersonaIDInteractor, PrivilegedKeyManager, ScriptTemplateBRC29, Services, SetupClient, SimpleWalletManager, StorageClient, StorageIdb, StorageProvider, StorageSyncReader, TESTNET_DEFAULT_SETTINGS, TwilioPhoneInteractor, UMPTokenLookupError, WABAccountContinuityError, WABClient, WABClientError, WABTransport, Wallet, WalletAuthenticationManager, WalletLogger, WalletPermissionsManager, WalletSettingsManager, WalletSigner, WalletStorageManager, WhatsOnChainServices, arcDefaultUrl, arcGorillaPoolUrl, arcadeDefaultUrl, arraysEqual, asArray, asBsvSdkPrivateKey, asBsvSdkPublickKey, asBsvSdkScript, asBsvSdkTx, asString, asUint8Array, brc29ProtocolID, buildChaintracksOptionsWithIngestors, convertProofToMerklePath, createAndStartDefaultChaintracks, createDefaultBulkFileDataManager, createDefaultChaintracksClient, createDefaultChaintracksStorageOptions, createDefaultIdbChaintracksOptions, createDefaultNoDbChaintracksOptions, createDefaultWalletServicesOptions, createIdbChaintracks, createNoDbChaintracks, createSyncMap, decryptBRC39, doubleSha256BE, doubleSha256LE, encryptBRC39, exportBRC38, exportBRC38Json, exportBRC39, getIdentityKey, getLabelToSpecOp, getListOutputsSpecOp, importBRC38, importBRC39, isAutoSpendableChangeOutput, isBaseBlockHeader, isBlockHeader, isLive, isLiveBlockHeader, isManagedChangeOutput, logCreateActionArgs, logWalletError, logger, makeAtomicBeef, makeBrc114ActionTimeLabel, managedChangeOutputFields, maxDate, optionalArraysEqual, outputColumnsWithoutLockingScript, parseBRC38Json, parseBrc114ActionTimeLabels, parseFileLink, parseTxScriptOffsets, partitionActionLabels, randomBytes, randomBytesBase64, randomBytesHex, resolveDefaultChaintracksArguments, sdk_exports as sdk, selectBulkHeaderFiles, sha256Hash, stampLog, stampLogFormat, startChaintracks, tableAuthSessionToPeerSession, throwDummyReviewActions, toBinaryBaseBlockHeader, toDefaultChaintracksArguments, toLookupNetworkPreset, toWalletNetwork, transactionColumnsWithoutRawTx, blockHeaderUtilities_exports as utils, validateScriptHash, validateSecondsSinceEpoch, validateStorageFeeModel, verifyHexString, verifyId, verifyInteger, verifyNumber, verifyOne, verifyOneOrNone, verifyOptionalHexString, verifyTruthy, wait, wocGetHeadersHeaderToBlockHeader };
36280
+ export { ARGON2ID_DEFAULT_HASH_LENGTH, ARGON2ID_DEFAULT_ITERATIONS, ARGON2ID_DEFAULT_MEMORY_KIB, ARGON2ID_DEFAULT_PARALLELISM, ARGON2ID_MAX_ITERATIONS, ARGON2ID_MAX_MEMORY_KIB, ARGON2ID_MAX_PARALLELISM, AuthMethodInteractor, BHServiceClient, BRC153_REFERENCE_PREFIX, BulkFileDataManager, BulkFileDataReader, BulkFilesReader, BulkFilesReaderFs, BulkFilesReaderStorage, BulkIngestorBase, BulkIngestorCDN, BulkIngestorCDNBabbage, BulkIngestorChaintracks, BulkIngestorWhatsOnChainCdn, BulkStorageBase, CWIStyleWalletManager, Chaintracks, ChaintracksChainTracker, ChaintracksFetch, ChaintracksFetchError, ChaintracksServiceClient, ChaintracksStorageBase, ChaintracksStorageIdb, ChaintracksStorageNoDb, DEFAULT_MANAGED_CHANGE_MAX_OUTPUTS_PER_ACTION, DEFAULT_MANAGED_CHANGE_MIGRATION_INPUTS_PER_ACTION, DEFAULT_MANAGED_CHANGE_MINIMUM_SATOSHIS, DEFAULT_MANAGED_CHANGE_PENDING_COMPARISON_INPUTS, DEFAULT_MANAGED_CHANGE_TARGET_UTXOS, DEFAULT_PROFILE_ID, DEFAULT_SETTINGS, DevConsoleInteractor, EntityBase, EntityCertificate, EntityCertificateField, EntityCommission, EntityOutput, EntityOutputBasket, EntityOutputTag, EntityOutputTagMap, EntityProvenTx, EntityProvenTxReq, EntitySyncState, EntityTransaction, EntityTxLabel, EntityTxLabelMap, EntityUser, GoChaintracksServiceClient, HeightRange, KDF_MAX_HASH_LENGTH, LEGACY_MANAGED_CHANGE_MINIMUM_SATOSHIS, LiveIngestorBase, LiveIngestorChaintracksSSE, LiveIngestorWhatsOnChainPoll, MAX_STATE_SNAPSHOT_BYTES, MergeEntity, Monitor, OverlayUMPTokenInteractor, PBKDF2_MAX_ITERATIONS, PBKDF2_NUM_ROUNDS, PersonaIDInteractor, PrivilegedKeyManager, ScriptTemplateBRC29, Services, SetupClient, SimpleWalletManager, StorageClient, StorageIdb, StorageProvider, StorageSyncReader, TESTNET_DEFAULT_SETTINGS, TwilioPhoneInteractor, UMPTokenLookupError, WABAccountContinuityError, WABClient, WABClientError, WABTransport, Wallet, WalletAuthenticationManager, WalletLogger, WalletPermissionsManager, WalletSettingsManager, WalletSigner, WalletStorageManager, WhatsOnChainServices, applyBrc153ReferenceLabel, arcDefaultUrl, arcGorillaPoolUrl, arcadeDefaultUrl, arraysEqual, asArray, asBsvSdkPrivateKey, asBsvSdkPublickKey, asBsvSdkScript, asBsvSdkTx, asString, asUint8Array, brc29ProtocolID, buildChaintracksOptionsWithIngestors, convertProofToMerklePath, createAndStartDefaultChaintracks, createDefaultBulkFileDataManager, createDefaultChaintracksClient, createDefaultChaintracksStorageOptions, createDefaultIdbChaintracksOptions, createDefaultNoDbChaintracksOptions, createDefaultWalletServicesOptions, createIdbChaintracks, createNoDbChaintracks, createSyncMap, decryptBRC39, defaultManagedChangePolicy, doubleSha256BE, doubleSha256LE, encryptBRC39, exportBRC38, exportBRC38Json, exportBRC39, getIdentityKey, getLabelToSpecOp, getListOutputsSpecOp, importBRC38, importBRC39, isAutoSpendableChangeOutput, isBaseBlockHeader, isBlockHeader, isBrc153ReferenceLabel, isLegacyManagedChangeBasketDefault, isLive, isLiveBlockHeader, isManagedChangeOutput, logCreateActionArgs, logWalletError, logger, makeAtomicBeef, makeBrc114ActionTimeLabel, makeBrc153ReferenceLabel, managedChangeOutputFields, maxDate, optionalArraysEqual, outputColumnsWithoutLockingScript, parseBRC38Json, parseBrc114ActionTimeLabels, parseBrc153ReferenceLabel, parseFileLink, parseTxScriptOffsets, partitionActionLabels, randomBytes, randomBytesBase64, randomBytesHex, resolveDefaultChaintracksArguments, sdk_exports as sdk, selectBulkHeaderFiles, sha256Hash, stampLog, stampLogFormat, startChaintracks, tableAuthSessionToPeerSession, throwDummyReviewActions, toBinaryBaseBlockHeader, toDefaultChaintracksArguments, toLookupNetworkPreset, toWalletNetwork, transactionColumnsWithoutRawTx, upgradeLegacyManagedChangeBasketDefault, blockHeaderUtilities_exports as utils, validateManagedChangePolicy, validateScriptHash, validateSecondsSinceEpoch, validateStorageFeeModel, verifyHexString, verifyId, verifyInteger, verifyNumber, verifyOne, verifyOneOrNone, verifyOptionalHexString, verifyTruthy, wait, wocGetHeadersHeaderToBlockHeader };
34899
36281
 
34900
36282
  //# sourceMappingURL=index.client.mjs.map