@bsv/wallet-toolbox-client 2.6.5 → 2.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/out/index.client.cjs +1854 -406
- package/out/index.client.cjs.map +1 -1
- package/out/index.client.d.cts +202 -14
- package/out/index.client.d.cts.map +1 -1
- package/out/index.client.d.mts +202 -14
- package/out/index.client.d.mts.map +1 -1
- package/out/index.client.mjs +1840 -407
- package/out/index.client.mjs.map +1 -1
- package/package.json +3 -3
package/out/index.client.cjs
CHANGED
|
@@ -175,23 +175,10 @@ var WalletError = class WalletError extends Error {
|
|
|
175
175
|
* @returns stringified JSON representation of the error such that it can be desirialized to a WalletError.
|
|
176
176
|
*/
|
|
177
177
|
static unknownToJson(error) {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
const ctor = ctorName != null && typeof ctorName.name === "string" ? ctorName.name : void 0;
|
|
183
|
-
const name = t === "object" && error !== null && typeof error.name === "string" ? error.name : "";
|
|
184
|
-
const message = t === "object" && error !== null && typeof error.message === "string" ? error.message : "";
|
|
185
|
-
const hasToJson = t === "object" && typeof error?.toJson === "function";
|
|
186
|
-
if (ctor != null && ctor !== "" && ctor.startsWith("WERR_") && hasToJson) json = error.toJson();
|
|
187
|
-
else if (name !== "" && message !== "") {
|
|
188
|
-
e = new WalletError(name, message);
|
|
189
|
-
json = e.toJson();
|
|
190
|
-
} else {
|
|
191
|
-
e = new WalletError("WERR_UNKNOWN", String(error));
|
|
192
|
-
json = e.toJson();
|
|
193
|
-
}
|
|
194
|
-
return json;
|
|
178
|
+
const candidate = error;
|
|
179
|
+
if ((error instanceof WalletError || candidate?.isError === true) && typeof candidate.toJson === "function") return candidate.toJson();
|
|
180
|
+
if (typeof candidate?.name === "string" && typeof candidate.message === "string") return new WalletError(candidate.name, candidate.message).toJson();
|
|
181
|
+
return new WalletError("WERR_UNKNOWN", String(error)).toJson();
|
|
195
182
|
}
|
|
196
183
|
};
|
|
197
184
|
//#endregion
|
|
@@ -225,6 +212,47 @@ var WERR_INVALID_OPERATION = class extends WalletError {
|
|
|
225
212
|
}
|
|
226
213
|
};
|
|
227
214
|
/**
|
|
215
|
+
* A destructive UTXO review could not obtain a conclusive verdict for every
|
|
216
|
+
* candidate. Consumers can match this name and retry later without parsing the
|
|
217
|
+
* human-readable message.
|
|
218
|
+
*/
|
|
219
|
+
var WERR_UTXO_REVIEW_INCONCLUSIVE = class extends WalletError {
|
|
220
|
+
checked;
|
|
221
|
+
confirmedSpent;
|
|
222
|
+
unknown;
|
|
223
|
+
constructor(checked, confirmedSpent, unknown) {
|
|
224
|
+
super("WERR_UTXO_REVIEW_INCONCLUSIVE", `UTXO review was inconclusive for ${unknown} of ${checked} candidates; no outputs were changed.`);
|
|
225
|
+
this.checked = checked;
|
|
226
|
+
this.confirmedSpent = confirmedSpent;
|
|
227
|
+
this.unknown = unknown;
|
|
228
|
+
}
|
|
229
|
+
toJson() {
|
|
230
|
+
const obj = JSON.parse(super.toJson());
|
|
231
|
+
obj.checked = this.checked;
|
|
232
|
+
obj.confirmedSpent = this.confirmedSpent;
|
|
233
|
+
obj.unknown = this.unknown;
|
|
234
|
+
return JSON.stringify(obj);
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
/**
|
|
238
|
+
* An action-batch lifecycle operation cannot continue in the batch's current state.
|
|
239
|
+
*/
|
|
240
|
+
var WERRActionBatchState = class extends WalletError {
|
|
241
|
+
state;
|
|
242
|
+
batchId;
|
|
243
|
+
constructor(state, batchId) {
|
|
244
|
+
super("WERR_ACTION_BATCH_STATE", `Action batch cannot continue because it is ${state}.`);
|
|
245
|
+
this.state = state;
|
|
246
|
+
this.batchId = batchId;
|
|
247
|
+
}
|
|
248
|
+
toJson() {
|
|
249
|
+
const obj = JSON.parse(super.toJson());
|
|
250
|
+
obj.state = this.state;
|
|
251
|
+
obj.batchId = this.batchId;
|
|
252
|
+
return JSON.stringify(obj);
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
/**
|
|
228
256
|
* Unable to broadcast transaction at this time.
|
|
229
257
|
*/
|
|
230
258
|
var WERR_BROADCAST_UNAVAILABLE = class extends WalletError {
|
|
@@ -422,6 +450,10 @@ function WalletErrorFromJson(json) {
|
|
|
422
450
|
let e;
|
|
423
451
|
const obj = json;
|
|
424
452
|
switch (obj.name) {
|
|
453
|
+
case "WERR_ACTION_BATCH_STATE":
|
|
454
|
+
e = new WERRActionBatchState(obj.state, obj.batchId);
|
|
455
|
+
e.message = obj.message;
|
|
456
|
+
break;
|
|
425
457
|
case "WERR_NOT_IMPLEMENTED":
|
|
426
458
|
e = new WERR_NOT_IMPLEMENTED(obj.message);
|
|
427
459
|
break;
|
|
@@ -431,6 +463,10 @@ function WalletErrorFromJson(json) {
|
|
|
431
463
|
case "WERR_INVALID_OPERATION":
|
|
432
464
|
e = new WERR_INVALID_OPERATION(obj.message);
|
|
433
465
|
break;
|
|
466
|
+
case "WERR_UTXO_REVIEW_INCONCLUSIVE":
|
|
467
|
+
e = new WERR_UTXO_REVIEW_INCONCLUSIVE(obj.checked, obj.confirmedSpent, obj.unknown);
|
|
468
|
+
e.message = obj.message;
|
|
469
|
+
break;
|
|
434
470
|
case "WERR_BROADCAST_UNAVAILABLE":
|
|
435
471
|
e = new WERR_BROADCAST_UNAVAILABLE(obj.message);
|
|
436
472
|
break;
|
|
@@ -507,9 +543,10 @@ const specOpWalletManagedUtxos = "284570a6213a74ba861c38b1cf790e1e400d9cf9324454
|
|
|
507
543
|
/**
|
|
508
544
|
* `listOutputs` special operation basket name value.
|
|
509
545
|
*
|
|
510
|
-
* Returns currently spendable wallet change outputs
|
|
546
|
+
* Returns currently spendable wallet change outputs conclusively confirmed spent.
|
|
547
|
+
* The operation rejects if any candidate cannot be classified conclusively.
|
|
511
548
|
*
|
|
512
|
-
* Optional tag value 'release'. If present, updates
|
|
549
|
+
* Optional tag value 'release'. If present, atomically updates only confirmed-spent change outputs to not spendable after rechecking current ownership and allocation state.
|
|
513
550
|
*
|
|
514
551
|
* Optional tag value 'all'. If present, processes all spendable true outputs, independent of baskets, but basket must be defined.
|
|
515
552
|
*/
|
|
@@ -839,6 +876,7 @@ var sdk_exports = /* @__PURE__ */ __exportAll({
|
|
|
839
876
|
ProvenTxReqNonTerminalStatus: () => ProvenTxReqNonTerminalStatus,
|
|
840
877
|
ProvenTxReqTerminalStatus: () => ProvenTxReqTerminalStatus,
|
|
841
878
|
Validation: () => _bsv_sdk.Validation,
|
|
879
|
+
WERR_ACTION_BATCH_STATE: () => WERRActionBatchState,
|
|
842
880
|
WERR_BAD_REQUEST: () => WERR_BAD_REQUEST,
|
|
843
881
|
WERR_BROADCAST_UNAVAILABLE: () => WERR_BROADCAST_UNAVAILABLE,
|
|
844
882
|
WERR_INSUFFICIENT_FUNDS: () => WERR_INSUFFICIENT_FUNDS,
|
|
@@ -853,6 +891,7 @@ var sdk_exports = /* @__PURE__ */ __exportAll({
|
|
|
853
891
|
WERR_NOT_IMPLEMENTED: () => WERR_NOT_IMPLEMENTED,
|
|
854
892
|
WERR_REVIEW_ACTIONS: () => WERR_REVIEW_ACTIONS,
|
|
855
893
|
WERR_UNAUTHORIZED: () => WERR_UNAUTHORIZED,
|
|
894
|
+
WERR_UTXO_REVIEW_INCONCLUSIVE: () => WERR_UTXO_REVIEW_INCONCLUSIVE,
|
|
856
895
|
WalletError: () => WalletError,
|
|
857
896
|
WalletErrorFromJson: () => WalletErrorFromJson,
|
|
858
897
|
isCreateActionSpecOp: () => isCreateActionSpecOp,
|
|
@@ -1019,8 +1058,8 @@ function toLookupNetworkPreset(chain) {
|
|
|
1019
1058
|
switch (chain) {
|
|
1020
1059
|
case "main": return "mainnet";
|
|
1021
1060
|
case "test": return "testnet";
|
|
1061
|
+
case "ttn": return "teratestnet";
|
|
1022
1062
|
case "stn":
|
|
1023
|
-
case "ttn":
|
|
1024
1063
|
case "tstn":
|
|
1025
1064
|
case "mock": return "local";
|
|
1026
1065
|
}
|
|
@@ -1391,6 +1430,42 @@ function makeBrc114ActionTimeLabel(unixMillis) {
|
|
|
1391
1430
|
return `action time ${unixMillis}`;
|
|
1392
1431
|
}
|
|
1393
1432
|
//#endregion
|
|
1433
|
+
//#region ../src/utility/brc153ReferenceLabels.ts
|
|
1434
|
+
const BRC153_REFERENCE_PREFIX = "reference ";
|
|
1435
|
+
/**
|
|
1436
|
+
* Build the BRC-153 synthetic listActions label for an action reference.
|
|
1437
|
+
* Encodes reference bytes as lowercase hex (labels are lowercased by validation).
|
|
1438
|
+
*/
|
|
1439
|
+
function makeBrc153ReferenceLabel(referenceBase64) {
|
|
1440
|
+
const bytes = _bsv_sdk.Utils.toArray(referenceBase64, "base64");
|
|
1441
|
+
return `${BRC153_REFERENCE_PREFIX}${_bsv_sdk.Utils.toHex(bytes)}`;
|
|
1442
|
+
}
|
|
1443
|
+
/**
|
|
1444
|
+
* True iff the label uses the reserved BRC-153 reference prefix.
|
|
1445
|
+
*/
|
|
1446
|
+
function isBrc153ReferenceLabel(label) {
|
|
1447
|
+
return label.startsWith(BRC153_REFERENCE_PREFIX);
|
|
1448
|
+
}
|
|
1449
|
+
/**
|
|
1450
|
+
* Ensure labels contain exactly one wallet-authored `reference <hex>`.
|
|
1451
|
+
* Any existing reserved-prefix labels are replaced.
|
|
1452
|
+
*/
|
|
1453
|
+
function applyBrc153ReferenceLabel(labels, referenceBase64) {
|
|
1454
|
+
const next = labels.filter((label) => !isBrc153ReferenceLabel(label));
|
|
1455
|
+
next.push(makeBrc153ReferenceLabel(referenceBase64));
|
|
1456
|
+
return next;
|
|
1457
|
+
}
|
|
1458
|
+
/**
|
|
1459
|
+
* Parse a BRC-153 synthetic reference label back to the BRC-100 Base64String reference.
|
|
1460
|
+
* Returns undefined if the label is not a valid reference label.
|
|
1461
|
+
*/
|
|
1462
|
+
function parseBrc153ReferenceLabel(label) {
|
|
1463
|
+
if (!label.startsWith("reference ")) return void 0;
|
|
1464
|
+
const hex = label.slice(10);
|
|
1465
|
+
if (hex.length === 0 || hex.length % 2 !== 0 || !/^[0-9a-f]+$/.test(hex)) return void 0;
|
|
1466
|
+
return _bsv_sdk.Utils.toBase64(_bsv_sdk.Utils.toArray(hex, "hex"));
|
|
1467
|
+
}
|
|
1468
|
+
//#endregion
|
|
1394
1469
|
//#region ../src/storage/schema/entities/EntityBase.ts
|
|
1395
1470
|
var EntityBase = class {
|
|
1396
1471
|
api;
|
|
@@ -2094,6 +2169,58 @@ var EntityOutput = class EntityOutput extends EntityBase {
|
|
|
2094
2169
|
}
|
|
2095
2170
|
};
|
|
2096
2171
|
//#endregion
|
|
2172
|
+
//#region ../src/storage/methods/managedChangePolicy.ts
|
|
2173
|
+
/** Historical default retained only to identify untouched wallet baskets. */
|
|
2174
|
+
const LEGACY_MANAGED_CHANGE_MINIMUM_SATOSHIS = 32;
|
|
2175
|
+
/**
|
|
2176
|
+
* Default liquidity policy for wallet-managed change.
|
|
2177
|
+
*
|
|
2178
|
+
* The preferred minimum is deliberately much larger than the dust threshold.
|
|
2179
|
+
* Dust answers "can this output ever be spent economically?"; this value
|
|
2180
|
+
* answers "is this output useful as an independently selectable liquidity
|
|
2181
|
+
* unit at contemporary fee rates?".
|
|
2182
|
+
*/
|
|
2183
|
+
const DEFAULT_MANAGED_CHANGE_TARGET_UTXOS = 144;
|
|
2184
|
+
const DEFAULT_MANAGED_CHANGE_MINIMUM_SATOSHIS = 5e3;
|
|
2185
|
+
const DEFAULT_MANAGED_CHANGE_MAX_OUTPUTS_PER_ACTION = 8;
|
|
2186
|
+
const DEFAULT_MANAGED_CHANGE_MIGRATION_INPUTS_PER_ACTION = 4;
|
|
2187
|
+
const DEFAULT_MANAGED_CHANGE_PENDING_COMPARISON_INPUTS = 16;
|
|
2188
|
+
/** True only for the exact historical default that is safe to auto-upgrade. */
|
|
2189
|
+
function isLegacyManagedChangeBasketDefault(basket) {
|
|
2190
|
+
return basket.name === "default" && basket.numberOfDesiredUTXOs === 144 && basket.minimumDesiredUTXOValue === 32;
|
|
2191
|
+
}
|
|
2192
|
+
/**
|
|
2193
|
+
* Normalize a legacy default while retaining every other field and every
|
|
2194
|
+
* operator-selected non-default value. Used by migrations, sync, and restore.
|
|
2195
|
+
*/
|
|
2196
|
+
function upgradeLegacyManagedChangeBasketDefault(basket) {
|
|
2197
|
+
if (!isLegacyManagedChangeBasketDefault(basket)) return basket;
|
|
2198
|
+
return {
|
|
2199
|
+
...basket,
|
|
2200
|
+
minimumDesiredUTXOValue: DEFAULT_MANAGED_CHANGE_MINIMUM_SATOSHIS
|
|
2201
|
+
};
|
|
2202
|
+
}
|
|
2203
|
+
function defaultManagedChangePolicy() {
|
|
2204
|
+
return {
|
|
2205
|
+
maxOutputsPerAction: 8,
|
|
2206
|
+
migrationInputsPerAction: 4,
|
|
2207
|
+
pendingComparisonInputs: 16
|
|
2208
|
+
};
|
|
2209
|
+
}
|
|
2210
|
+
function validateManagedChangePolicy(options) {
|
|
2211
|
+
const policy = {
|
|
2212
|
+
...defaultManagedChangePolicy(),
|
|
2213
|
+
...options
|
|
2214
|
+
};
|
|
2215
|
+
const validateLimit = (value, name, minimum) => {
|
|
2216
|
+
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`);
|
|
2217
|
+
};
|
|
2218
|
+
validateLimit(policy.maxOutputsPerAction, "maxOutputsPerAction", 1);
|
|
2219
|
+
validateLimit(policy.migrationInputsPerAction, "migrationInputsPerAction", 0);
|
|
2220
|
+
validateLimit(policy.pendingComparisonInputs, "pendingComparisonInputs", 1);
|
|
2221
|
+
return policy;
|
|
2222
|
+
}
|
|
2223
|
+
//#endregion
|
|
2097
2224
|
//#region ../src/storage/schema/entities/EntityOutputBasket.ts
|
|
2098
2225
|
var EntityOutputBasket = class EntityOutputBasket extends EntityBase {
|
|
2099
2226
|
constructor(api) {
|
|
@@ -2196,13 +2323,15 @@ var EntityOutputBasket = class EntityOutputBasket extends EntityBase {
|
|
|
2196
2323
|
this.userId = userId;
|
|
2197
2324
|
this.name ||= "default";
|
|
2198
2325
|
this.basketId = 0;
|
|
2326
|
+
this.api = upgradeLegacyManagedChangeBasketDefault(this.api);
|
|
2199
2327
|
this.basketId = await storage.insertOutputBasket(this.toApi(), trx);
|
|
2200
2328
|
}
|
|
2201
2329
|
async mergeExisting(storage, since, ei, syncMap, trx) {
|
|
2202
2330
|
let wasMerged = false;
|
|
2203
2331
|
if (ei.updated_at > this.updated_at) {
|
|
2204
|
-
|
|
2205
|
-
this.
|
|
2332
|
+
const incoming = upgradeLegacyManagedChangeBasketDefault(ei);
|
|
2333
|
+
this.minimumDesiredUTXOValue = incoming.minimumDesiredUTXOValue;
|
|
2334
|
+
this.numberOfDesiredUTXOs = incoming.numberOfDesiredUTXOs;
|
|
2206
2335
|
this.isDeleted = ei.isDeleted;
|
|
2207
2336
|
this.updated_at = new Date(Math.max(ei.updated_at.getTime(), this.updated_at.getTime()));
|
|
2208
2337
|
await storage.updateOutputBasket(this.id, this.toApi(), trx);
|
|
@@ -4372,6 +4501,12 @@ var WalletStorageManager = class {
|
|
|
4372
4501
|
async renewActionBatch(batchId) {
|
|
4373
4502
|
return await this.runAsWriter(async (writer) => await writer.renewActionBatch(await this.getAuth(true), batchId));
|
|
4374
4503
|
}
|
|
4504
|
+
async resumeActionBatch(args) {
|
|
4505
|
+
return await this.runAsWriter(async (writer) => {
|
|
4506
|
+
if (writer.resumeActionBatch == null) throw new WERR_NOT_IMPLEMENTED("action batch resume is not available");
|
|
4507
|
+
return await writer.resumeActionBatch(await this.getAuth(true), args);
|
|
4508
|
+
});
|
|
4509
|
+
}
|
|
4375
4510
|
async prepareActionBatchCommit(manifest) {
|
|
4376
4511
|
return await this.runAsWriter(async (writer) => await writer.prepareActionBatchCommit(await this.getAuth(true), manifest));
|
|
4377
4512
|
}
|
|
@@ -4726,8 +4861,20 @@ var WalletStorageManager = class {
|
|
|
4726
4861
|
});
|
|
4727
4862
|
return log;
|
|
4728
4863
|
}
|
|
4864
|
+
/**
|
|
4865
|
+
* Return the remote HTTP(S) endpoint for a managed store, if any.
|
|
4866
|
+
*
|
|
4867
|
+
* Duck-types `endpointUrl` on the provider (as set by `StorageClientBase`).
|
|
4868
|
+
* Do **not** key this off `constructor.name === 'StorageClient'`: production
|
|
4869
|
+
* minifiers (Vite/esbuild/webpack) rename classes, so that check fails and
|
|
4870
|
+
* every remote store reports `endpointURL: undefined` even though the URL is
|
|
4871
|
+
* present. Consumers that match backups by URL (e.g. making a remote store
|
|
4872
|
+
* primary) then fail while sync still works, because sync walks `_backups`
|
|
4873
|
+
* without needing `endpointURL`.
|
|
4874
|
+
*/
|
|
4729
4875
|
getStoreEndpointURL(store) {
|
|
4730
|
-
|
|
4876
|
+
const url = store.storage.endpointUrl;
|
|
4877
|
+
if (typeof url === "string" && url.length > 0) return url;
|
|
4731
4878
|
}
|
|
4732
4879
|
getStores() {
|
|
4733
4880
|
const stores = [];
|
|
@@ -5153,7 +5300,7 @@ async function getBeefForTransaction(storage, txid, options) {
|
|
|
5153
5300
|
const concurrency = normalizeConcurrency(options.maxConcurrency);
|
|
5154
5301
|
while (frontier.length > 0) {
|
|
5155
5302
|
const current = frontier.filter((item) => needsResolution(beef, item.txid, hasKnownTxid));
|
|
5156
|
-
const resolved = await mapWithConcurrency(current, concurrency, async (item) => await resolveBeefForTransaction(storage, item.txid, options, hasKnownTxid, item.depth));
|
|
5303
|
+
const resolved = await mapWithConcurrency$1(current, concurrency, async (item) => await resolveBeefForTransaction(storage, item.txid, options, hasKnownTxid, item.depth));
|
|
5157
5304
|
const next = [];
|
|
5158
5305
|
for (let i = 0; i < resolved.length; i++) {
|
|
5159
5306
|
const result = resolved[i];
|
|
@@ -5210,7 +5357,7 @@ function requiresSingleRootPolicy(options) {
|
|
|
5210
5357
|
return options.ignoreStorage === true || options.minProofLevel !== void 0 || options.chainTracker != null || options.skipInvalidProofs === true;
|
|
5211
5358
|
}
|
|
5212
5359
|
async function mergeSingleRootFragments(storage, roots, options, beef) {
|
|
5213
|
-
const fragments = await mapWithConcurrency(roots.filter((txid) => beef.findTxid(txid) == null), normalizeConcurrency(options.maxConcurrency), async (txid) => await getBeefForTransaction(storage, txid, {
|
|
5360
|
+
const fragments = await mapWithConcurrency$1(roots.filter((txid) => beef.findTxid(txid) == null), normalizeConcurrency(options.maxConcurrency), async (txid) => await getBeefForTransaction(storage, txid, {
|
|
5214
5361
|
...options,
|
|
5215
5362
|
mergeToBeef: void 0
|
|
5216
5363
|
}));
|
|
@@ -5329,7 +5476,7 @@ function needsResolution(beef, txid, hasKnownTxid) {
|
|
|
5329
5476
|
async function mergeMissingFragments(storage, beef, missing, options) {
|
|
5330
5477
|
if (missing.length === 0) return;
|
|
5331
5478
|
if (options.ignoreServices === true) throw new WERR_INVALID_PARAMETER(`txid ${missing[0].txid}`, `valid transaction on chain ${storage.chain}`);
|
|
5332
|
-
const fragments = await mapWithConcurrency(missing, normalizeConcurrency(options.maxConcurrency), async (item) => await getBeefForTransaction(storage, item.txid, {
|
|
5479
|
+
const fragments = await mapWithConcurrency$1(missing, normalizeConcurrency(options.maxConcurrency), async (item) => await getBeefForTransaction(storage, item.txid, {
|
|
5333
5480
|
...options,
|
|
5334
5481
|
ignoreStorage: true,
|
|
5335
5482
|
mergeToBeef: void 0
|
|
@@ -5352,7 +5499,7 @@ function makeKnownTxidLookup$1(knownTxids) {
|
|
|
5352
5499
|
function normalizeConcurrency(value = 8) {
|
|
5353
5500
|
return Number.isFinite(value) ? Math.max(1, Math.min(32, Math.floor(value))) : 8;
|
|
5354
5501
|
}
|
|
5355
|
-
async function mapWithConcurrency(values, concurrency, mapper) {
|
|
5502
|
+
async function mapWithConcurrency$1(values, concurrency, mapper) {
|
|
5356
5503
|
const results = Array.from({ length: values.length }, () => void 0);
|
|
5357
5504
|
let cursor = 0;
|
|
5358
5505
|
await Promise.all(Array.from({ length: Math.min(concurrency, values.length) }, async () => {
|
|
@@ -5763,6 +5910,190 @@ async function commitNewTxToStorage(storage, userId, vargs) {
|
|
|
5763
5910
|
return r;
|
|
5764
5911
|
}
|
|
5765
5912
|
//#endregion
|
|
5913
|
+
//#region ../src/services/classifyOutputUtxo.ts
|
|
5914
|
+
async function mapWithConcurrency(values, maxConcurrency, worker) {
|
|
5915
|
+
const results = Array.from({ length: values.length });
|
|
5916
|
+
let nextIndex = 0;
|
|
5917
|
+
const workers = Array.from({ length: Math.min(maxConcurrency, values.length) }, async () => {
|
|
5918
|
+
while (nextIndex < values.length) {
|
|
5919
|
+
const index = nextIndex++;
|
|
5920
|
+
results[index] = await worker(values[index], index);
|
|
5921
|
+
}
|
|
5922
|
+
});
|
|
5923
|
+
await Promise.all(workers);
|
|
5924
|
+
return results;
|
|
5925
|
+
}
|
|
5926
|
+
/**
|
|
5927
|
+
* Classify an output without ever converting provider absence or failure into
|
|
5928
|
+
* positive spent evidence. This helper deliberately catches provider throws so
|
|
5929
|
+
* batch callers can finish classifying every candidate before deciding whether
|
|
5930
|
+
* a destructive operation is safe.
|
|
5931
|
+
*/
|
|
5932
|
+
async function classifyOutputUtxo(services, output) {
|
|
5933
|
+
if (output.lockingScript == null || output.lockingScript.length === 0) return {
|
|
5934
|
+
verdict: "unknown",
|
|
5935
|
+
provider: "<no-locking-script>",
|
|
5936
|
+
error: new WERR_INVALID_PARAMETER("output.lockingScript", "validated by storage provider validateOutputScript.")
|
|
5937
|
+
};
|
|
5938
|
+
if (typeof output.txid !== "string" || !/^[0-9a-fA-F]{64}$/.test(output.txid) || !Number.isSafeInteger(output.vout) || output.vout < 0) return {
|
|
5939
|
+
verdict: "unknown",
|
|
5940
|
+
provider: "<invalid-outpoint>",
|
|
5941
|
+
error: new WERR_INVALID_PARAMETER("output outpoint", "a 32-byte transaction ID and non-negative vout")
|
|
5942
|
+
};
|
|
5943
|
+
try {
|
|
5944
|
+
const hash = services.hashOutputScript(_bsv_sdk.Utils.toHex(output.lockingScript));
|
|
5945
|
+
const result = await services.getUtxoStatus(hash, void 0, `${output.txid}.${output.vout}`);
|
|
5946
|
+
const provider = typeof result?.name === "string" && result.name.length > 0 ? result.name : "<invalid-provider-result>";
|
|
5947
|
+
if (result?.status !== "success" || typeof result.isUtxo !== "boolean" || provider === "<invalid-provider-result>") return {
|
|
5948
|
+
verdict: "unknown",
|
|
5949
|
+
provider,
|
|
5950
|
+
error: result?.error
|
|
5951
|
+
};
|
|
5952
|
+
return {
|
|
5953
|
+
verdict: result.isUtxo ? "unspent" : "spent",
|
|
5954
|
+
provider
|
|
5955
|
+
};
|
|
5956
|
+
} catch (error) {
|
|
5957
|
+
return {
|
|
5958
|
+
verdict: "unknown",
|
|
5959
|
+
provider: "<provider-error>",
|
|
5960
|
+
error
|
|
5961
|
+
};
|
|
5962
|
+
}
|
|
5963
|
+
}
|
|
5964
|
+
/**
|
|
5965
|
+
* Convert an internal tri-state classification to the historical boolean
|
|
5966
|
+
* contract only when a provider supplied a conclusive answer.
|
|
5967
|
+
*/
|
|
5968
|
+
function requireConclusiveUtxo(classification) {
|
|
5969
|
+
if (classification.verdict === "unspent") return true;
|
|
5970
|
+
if (classification.verdict === "spent") return false;
|
|
5971
|
+
if (classification.error instanceof Error) throw classification.error;
|
|
5972
|
+
throw new WERR_INVALID_OPERATION(`UTXO provider ${classification.provider} did not return a conclusive result.`);
|
|
5973
|
+
}
|
|
5974
|
+
//#endregion
|
|
5975
|
+
//#region ../src/storage/methods/reconcileFailedTransactionInputs.ts
|
|
5976
|
+
async function quarantineLocalInputs(localInputs, storage, trx) {
|
|
5977
|
+
const staleOutpoints = /* @__PURE__ */ new Set();
|
|
5978
|
+
for (const { output, outpoint } of localInputs) {
|
|
5979
|
+
await storage.updateOutput(verifyId(output.outputId), { spendable: false }, trx);
|
|
5980
|
+
staleOutpoints.add(`${outpoint.txid}.${outpoint.vout}`);
|
|
5981
|
+
}
|
|
5982
|
+
return {
|
|
5983
|
+
checked: localInputs.length,
|
|
5984
|
+
staleConfirmed: localInputs.length,
|
|
5985
|
+
staleOutpoints: [...staleOutpoints]
|
|
5986
|
+
};
|
|
5987
|
+
}
|
|
5988
|
+
async function findLocalInputs(req, storage, trx) {
|
|
5989
|
+
const outpoints = _bsv_sdk.Transaction.fromBinary(req.rawTx).inputs.map((input) => ({
|
|
5990
|
+
txid: input.sourceTXID ?? "",
|
|
5991
|
+
vout: input.sourceOutputIndex ?? 0
|
|
5992
|
+
})).filter((outpoint) => outpoint.txid !== "");
|
|
5993
|
+
if (outpoints.length === 0) return [];
|
|
5994
|
+
const transactions = await storage.findTransactions({
|
|
5995
|
+
partial: { txid: req.txid },
|
|
5996
|
+
noRawTx: true,
|
|
5997
|
+
trx
|
|
5998
|
+
});
|
|
5999
|
+
const userIds = [...new Set(transactions.map((transaction) => transaction.userId))];
|
|
6000
|
+
const localInputs = [];
|
|
6001
|
+
const localKeys = /* @__PURE__ */ new Set();
|
|
6002
|
+
for (const userId of userIds) {
|
|
6003
|
+
const byOutpoint = await storage.findOutputsByOutpoints(userId, outpoints, trx);
|
|
6004
|
+
for (const outpoint of outpoints) {
|
|
6005
|
+
const output = byOutpoint[`${outpoint.txid}.${outpoint.vout}`];
|
|
6006
|
+
const localKey = `${userId}:${outpoint.txid}.${outpoint.vout}`;
|
|
6007
|
+
if (output != null && !localKeys.has(localKey)) {
|
|
6008
|
+
localKeys.add(localKey);
|
|
6009
|
+
localInputs.push({
|
|
6010
|
+
output,
|
|
6011
|
+
outpoint
|
|
6012
|
+
});
|
|
6013
|
+
}
|
|
6014
|
+
}
|
|
6015
|
+
}
|
|
6016
|
+
return localInputs;
|
|
6017
|
+
}
|
|
6018
|
+
/**
|
|
6019
|
+
* Conservatively quarantine every locally-owned input of a transaction after
|
|
6020
|
+
* Arcade supplies explicit missing-input/conflict evidence. This path is
|
|
6021
|
+
* deliberately independent of explorer or UTXO providers: a positive
|
|
6022
|
+
* broadcaster verdict is enough to stop the failed transaction from feeding
|
|
6023
|
+
* the same inputs into another action. A later validated mined proof remains
|
|
6024
|
+
* able to repair the transaction through the ordinary proof recovery path.
|
|
6025
|
+
*/
|
|
6026
|
+
async function quarantineReqInputs(req, storage, trx, logger) {
|
|
6027
|
+
const result = await quarantineLocalInputs(await findLocalInputs(req, storage, trx), storage, trx);
|
|
6028
|
+
if (result.staleConfirmed > 0) logger?.log(`quarantineReqInputs: ${result.staleConfirmed} local input copy/copies quarantined for txid=${req.txid}`);
|
|
6029
|
+
return result;
|
|
6030
|
+
}
|
|
6031
|
+
/**
|
|
6032
|
+
* Keep only inputs created by a locally terminal parent unavailable after the
|
|
6033
|
+
* failed-child transition releases its allocations. Other inputs may still be
|
|
6034
|
+
* valid UTXOs and must remain reusable.
|
|
6035
|
+
*/
|
|
6036
|
+
async function quarantineReqInputsFromFailedParents(req, failedParentTxids, storage, trx, logger) {
|
|
6037
|
+
const failedParents = new Set(failedParentTxids);
|
|
6038
|
+
const result = await quarantineLocalInputs((await findLocalInputs(req, storage, trx)).filter(({ outpoint }) => failedParents.has(outpoint.txid)), storage, trx);
|
|
6039
|
+
if (result.staleConfirmed > 0) logger?.log(`quarantineReqInputsFromFailedParents: ${result.staleConfirmed} local parent output copy/copies quarantined for txid=${req.txid}`);
|
|
6040
|
+
return result;
|
|
6041
|
+
}
|
|
6042
|
+
/**
|
|
6043
|
+
* Ask the configured UTXO-provider collection to classify the request's local
|
|
6044
|
+
* inputs and mark only positively confirmed spent outputs unavailable.
|
|
6045
|
+
* Provider errors and a collection with no providers are inconclusive and
|
|
6046
|
+
* never count as spent.
|
|
6047
|
+
*/
|
|
6048
|
+
async function markConfirmedStaleReqInputs(req, storage, services, trx, logger) {
|
|
6049
|
+
const result = {
|
|
6050
|
+
checked: 0,
|
|
6051
|
+
staleConfirmed: 0,
|
|
6052
|
+
staleOutpoints: []
|
|
6053
|
+
};
|
|
6054
|
+
const localInputs = await findLocalInputs(req, storage, trx);
|
|
6055
|
+
const verdicts = /* @__PURE__ */ new Map();
|
|
6056
|
+
const uniqueInputs = /* @__PURE__ */ new Map();
|
|
6057
|
+
for (const localInput of localInputs) {
|
|
6058
|
+
const key = `${localInput.outpoint.txid}.${localInput.outpoint.vout}`;
|
|
6059
|
+
if (!uniqueInputs.has(key)) uniqueInputs.set(key, localInput);
|
|
6060
|
+
}
|
|
6061
|
+
await mapWithConcurrency([...uniqueInputs.values()], 4, async ({ output, outpoint }) => {
|
|
6062
|
+
const key = `${outpoint.txid}.${outpoint.vout}`;
|
|
6063
|
+
if (output.lockingScript == null) try {
|
|
6064
|
+
await storage.validateOutputScript(output, trx);
|
|
6065
|
+
} catch {
|
|
6066
|
+
verdicts.set(key, "inconclusive");
|
|
6067
|
+
return;
|
|
6068
|
+
}
|
|
6069
|
+
if (output.lockingScript == null) {
|
|
6070
|
+
verdicts.set(key, "inconclusive");
|
|
6071
|
+
return;
|
|
6072
|
+
}
|
|
6073
|
+
const classification = await classifyOutputUtxo(services, output);
|
|
6074
|
+
let verdict;
|
|
6075
|
+
if (classification.verdict === "unknown") verdict = "inconclusive";
|
|
6076
|
+
else if (classification.verdict === "unspent") verdict = "utxo";
|
|
6077
|
+
else verdict = "stale";
|
|
6078
|
+
verdicts.set(key, verdict);
|
|
6079
|
+
});
|
|
6080
|
+
const staleOutpoints = /* @__PURE__ */ new Set();
|
|
6081
|
+
for (const { output, outpoint } of localInputs) {
|
|
6082
|
+
const key = `${outpoint.txid}.${outpoint.vout}`;
|
|
6083
|
+
const verdict = verdicts.get(key);
|
|
6084
|
+
if (verdict == null || verdict === "inconclusive") continue;
|
|
6085
|
+
result.checked++;
|
|
6086
|
+
if (verdict === "stale") {
|
|
6087
|
+
await storage.updateOutput(verifyId(output.outputId), { spendable: false }, trx);
|
|
6088
|
+
result.staleConfirmed++;
|
|
6089
|
+
staleOutpoints.add(key);
|
|
6090
|
+
}
|
|
6091
|
+
}
|
|
6092
|
+
result.staleOutpoints = [...staleOutpoints];
|
|
6093
|
+
if (result.staleConfirmed > 0) logger?.log(`markConfirmedStaleReqInputs: ${result.staleConfirmed} of ${result.checked} local input copy/copies confirmed spent for txid=${req.txid}`);
|
|
6094
|
+
return result;
|
|
6095
|
+
}
|
|
6096
|
+
//#endregion
|
|
5766
6097
|
//#region ../src/storage/methods/attemptToPostReqsToNetwork.ts
|
|
5767
6098
|
/**
|
|
5768
6099
|
* Attempt to post one or more `ProvenTxReq` with status 'unsent'
|
|
@@ -6102,64 +6433,7 @@ async function confirmDoubleSpend(ar, beef, storage, services, logger) {
|
|
|
6102
6433
|
* that were actually evicted (added to history note for diagnostics).
|
|
6103
6434
|
*/
|
|
6104
6435
|
async function markStaleInputsAsSpent(ar, storage, services, trx, logger) {
|
|
6105
|
-
|
|
6106
|
-
checked: 0,
|
|
6107
|
-
staleConfirmed: 0,
|
|
6108
|
-
staleOutpoints: []
|
|
6109
|
-
};
|
|
6110
|
-
const req = ar.vreq.req;
|
|
6111
|
-
const txIds = req.notify.transactionIds;
|
|
6112
|
-
if (txIds == null || txIds.length === 0) return result;
|
|
6113
|
-
const txRecord = (await storage.findTransactions({
|
|
6114
|
-
partial: { transactionId: txIds[0] },
|
|
6115
|
-
noRawTx: true,
|
|
6116
|
-
trx
|
|
6117
|
-
}))[0];
|
|
6118
|
-
if (txRecord == null) return result;
|
|
6119
|
-
const userId = txRecord.userId;
|
|
6120
|
-
const outpoints = _bsv_sdk.Transaction.fromBinary(req.rawTx).inputs.map((i) => ({
|
|
6121
|
-
txid: i.sourceTXID ?? "",
|
|
6122
|
-
vout: i.sourceOutputIndex ?? 0
|
|
6123
|
-
})).filter((o) => o.txid !== "");
|
|
6124
|
-
if (outpoints.length === 0) return result;
|
|
6125
|
-
const byOutpoint = await storage.findOutputsByOutpoints(userId, outpoints, trx);
|
|
6126
|
-
const checks = await Promise.all(outpoints.map(async (outpoint) => {
|
|
6127
|
-
const localOutput = byOutpoint[`${outpoint.txid}.${outpoint.vout}`];
|
|
6128
|
-
if (localOutput == null) return { kind: "skipped" };
|
|
6129
|
-
if (localOutput.lockingScript == null) try {
|
|
6130
|
-
await storage.validateOutputScript(localOutput, trx);
|
|
6131
|
-
} catch {
|
|
6132
|
-
return { kind: "skipped" };
|
|
6133
|
-
}
|
|
6134
|
-
let isStillUtxo;
|
|
6135
|
-
try {
|
|
6136
|
-
isStillUtxo = await services.isUtxo(localOutput);
|
|
6137
|
-
} catch {
|
|
6138
|
-
return {
|
|
6139
|
-
kind: "service-error",
|
|
6140
|
-
localOutput
|
|
6141
|
-
};
|
|
6142
|
-
}
|
|
6143
|
-
return isStillUtxo ? {
|
|
6144
|
-
kind: "still-utxo",
|
|
6145
|
-
localOutput
|
|
6146
|
-
} : {
|
|
6147
|
-
kind: "stale",
|
|
6148
|
-
localOutput,
|
|
6149
|
-
outpoint
|
|
6150
|
-
};
|
|
6151
|
-
}));
|
|
6152
|
-
for (const c of checks) {
|
|
6153
|
-
if (c.kind === "skipped") continue;
|
|
6154
|
-
result.checked++;
|
|
6155
|
-
if (c.kind === "stale") {
|
|
6156
|
-
await storage.updateOutput(c.localOutput.outputId, { spendable: false }, trx);
|
|
6157
|
-
result.staleConfirmed++;
|
|
6158
|
-
result.staleOutpoints.push(`${c.outpoint.txid}.${c.outpoint.vout}`);
|
|
6159
|
-
}
|
|
6160
|
-
}
|
|
6161
|
-
if (result.staleConfirmed > 0) logger?.log(`markStaleInputsAsSpent: ${result.staleConfirmed} of ${result.checked} input(s) confirmed-spent on chain for txid=${req.txid}`);
|
|
6162
|
-
return result;
|
|
6436
|
+
return await markConfirmedStaleReqInputs(ar.vreq.req, storage, services, trx, logger);
|
|
6163
6437
|
}
|
|
6164
6438
|
//#endregion
|
|
6165
6439
|
//#region ../src/storage/methods/listCertificates.ts
|
|
@@ -6312,6 +6586,75 @@ function removeDustOutputs(changeOutputs, dustFloor) {
|
|
|
6312
6586
|
largest.satoshis += removed.satoshis;
|
|
6313
6587
|
}
|
|
6314
6588
|
}
|
|
6589
|
+
async function migrateLegacyChangeInputs(request) {
|
|
6590
|
+
const { params, result } = request;
|
|
6591
|
+
if (!params.surplusPoolShaping || result.changeOutputs.length === 0) return;
|
|
6592
|
+
if (request.targetNetCount <= request.netChangeCount()) return;
|
|
6593
|
+
const migrationLimit = params.maxMigrationInputs === -1 ? Number.MAX_SAFE_INTEGER : params.maxMigrationInputs ?? 0;
|
|
6594
|
+
for (let migrated = 0; migrated < migrationLimit; migrated++) {
|
|
6595
|
+
const marginalInputFee = request.feeTarget(1) - request.feeTarget();
|
|
6596
|
+
const candidate = await request.allocateChangeInput(0);
|
|
6597
|
+
if (candidate == null) break;
|
|
6598
|
+
if (candidate.satoshis >= params.changeInitialSatoshis || candidate.satoshis <= marginalInputFee) {
|
|
6599
|
+
await request.releaseChangeInput(candidate.outputId);
|
|
6600
|
+
break;
|
|
6601
|
+
}
|
|
6602
|
+
request.recordAllocatedInput(candidate);
|
|
6603
|
+
}
|
|
6604
|
+
}
|
|
6605
|
+
/**
|
|
6606
|
+
* Materialize the first managed-change output from surplus that is already in
|
|
6607
|
+
* the transaction. This is especially important for explicit/fixed inputs:
|
|
6608
|
+
* their value can fully fund an action before the allocator loop runs, but the
|
|
6609
|
+
* shaping policy must still capture the remainder without gathering another
|
|
6610
|
+
* wallet-managed input.
|
|
6611
|
+
*/
|
|
6612
|
+
function materializeSurplusChangeOutput(request) {
|
|
6613
|
+
const { params, result } = request;
|
|
6614
|
+
if (!params.surplusPoolShaping || result.changeOutputs.length > 0) return;
|
|
6615
|
+
const availableAfterOutputFee = request.feeExcess(0, 1);
|
|
6616
|
+
if (availableAfterOutputFee < request.dustFloor) return;
|
|
6617
|
+
result.changeOutputs.push({
|
|
6618
|
+
satoshis: Math.min(availableAfterOutputFee, Math.max(request.dustFloor, params.changeFirstSatoshis)),
|
|
6619
|
+
lockingScriptLength: params.changeLockingScriptLength
|
|
6620
|
+
});
|
|
6621
|
+
request.feeExcess();
|
|
6622
|
+
}
|
|
6623
|
+
/**
|
|
6624
|
+
* Preserve the historical compatibility retry when another input can make a
|
|
6625
|
+
* viable change output, except when surplus-only shaping has nothing economic
|
|
6626
|
+
* to return. In that case the bounded remainder stays in the miner fee instead
|
|
6627
|
+
* of manufacturing change from an additional wallet input.
|
|
6628
|
+
*/
|
|
6629
|
+
async function requireViableChangeOrRetainBoundedFee(request) {
|
|
6630
|
+
const { params, result } = request;
|
|
6631
|
+
const feeExcessNow = request.feeExcess();
|
|
6632
|
+
if (result.changeOutputs.length > 0 || feeExcessNow <= 0) return;
|
|
6633
|
+
if (params.surplusPoolShaping === true && request.feeExcess(0, 1) < request.dustFloor) return;
|
|
6634
|
+
const minimumChange = Math.max(request.dustFloor, params.changeFirstSatoshis);
|
|
6635
|
+
const totalSatoshisNeeded = request.spending() + request.feeTarget(0, 1) + minimumChange;
|
|
6636
|
+
const moreSatoshisNeeded = Math.max(1, totalSatoshisNeeded - request.funding());
|
|
6637
|
+
await request.releaseAllocatedChangeInputs();
|
|
6638
|
+
throw new WERR_INSUFFICIENT_FUNDS(totalSatoshisNeeded, moreSatoshisNeeded);
|
|
6639
|
+
}
|
|
6640
|
+
function shapeSurplusChangeOutputs(request) {
|
|
6641
|
+
const { params, result } = request;
|
|
6642
|
+
if (!params.surplusPoolShaping || result.changeOutputs.length !== 1) return;
|
|
6643
|
+
if (request.targetNetCount <= request.netChangeCount()) return;
|
|
6644
|
+
const originalSatoshis = result.changeOutputs[0].satoshis;
|
|
6645
|
+
const desiredOutputs = Math.min(request.maxChangeOutputs, Math.max(1, request.targetNetCount + result.allocatedChangeInputs.length));
|
|
6646
|
+
for (let count = desiredOutputs; count > 1; count--) {
|
|
6647
|
+
const addedOutputs = count - 1;
|
|
6648
|
+
const distributable = originalSatoshis - (request.feeTarget(0, addedOutputs) - request.feeTarget());
|
|
6649
|
+
if (distributable < count * params.changeInitialSatoshis) continue;
|
|
6650
|
+
result.changeOutputs = Array.from({ length: count }, () => ({
|
|
6651
|
+
satoshis: params.changeInitialSatoshis,
|
|
6652
|
+
lockingScriptLength: params.changeLockingScriptLength
|
|
6653
|
+
}));
|
|
6654
|
+
distributeExcessFees(result.changeOutputs, params.changeInitialSatoshis, distributable - count * params.changeInitialSatoshis, request.rand);
|
|
6655
|
+
break;
|
|
6656
|
+
}
|
|
6657
|
+
}
|
|
6315
6658
|
/**
|
|
6316
6659
|
* Simplifications:
|
|
6317
6660
|
* - only support one change type with fixed length scripts.
|
|
@@ -6383,7 +6726,8 @@ async function generateChangeSdkCore(params, allocateChangeInput, releaseChangeI
|
|
|
6383
6726
|
* Applies the per-transaction limit so that the UTXO pool grows
|
|
6384
6727
|
* gradually rather than all at once.
|
|
6385
6728
|
*/
|
|
6386
|
-
const maxChangeOutputs = params.maxChangeOutputs ?? 8;
|
|
6729
|
+
const maxChangeOutputs = params.maxChangeOutputs === -1 ? Number.MAX_SAFE_INTEGER : params.maxChangeOutputs ?? 8;
|
|
6730
|
+
const surplusPoolShaping = params.surplusPoolShaping === true;
|
|
6387
6731
|
const randomVals = [...params.randomVals || []];
|
|
6388
6732
|
const nextRandomVal = () => {
|
|
6389
6733
|
let val = 0;
|
|
@@ -6466,6 +6810,7 @@ async function generateChangeSdkCore(params, allocateChangeInput, releaseChangeI
|
|
|
6466
6810
|
return r.changeOutputs.length - r.allocatedChangeInputs.length;
|
|
6467
6811
|
};
|
|
6468
6812
|
const addOutputToBalanceNewInput = () => {
|
|
6813
|
+
if (surplusPoolShaping) return false;
|
|
6469
6814
|
if (!hasTargetNetCount) return false;
|
|
6470
6815
|
if (r.changeOutputs.length >= maxChangeOutputs) return false;
|
|
6471
6816
|
return netChangeCount() - 1 < targetNetCount;
|
|
@@ -6481,6 +6826,7 @@ async function generateChangeSdkCore(params, allocateChangeInput, releaseChangeI
|
|
|
6481
6826
|
feeExcessNow = feeExcess();
|
|
6482
6827
|
};
|
|
6483
6828
|
const addDesiredChangeOutputs = () => {
|
|
6829
|
+
if (surplusPoolShaping) return;
|
|
6484
6830
|
while (r.changeOutputs.length < maxChangeOutputs && (hasTargetNetCount && targetNetCount > netChangeCount() || r.changeOutputs.length === 0 && feeExcess() > 0)) {
|
|
6485
6831
|
const satoshis = r.changeOutputs.length === 0 ? Math.max(dustFloor, params.changeFirstSatoshis) : Math.max(dustFloor, params.changeInitialSatoshis);
|
|
6486
6832
|
r.changeOutputs.push({
|
|
@@ -6496,7 +6842,8 @@ async function generateChangeSdkCore(params, allocateChangeInput, releaseChangeI
|
|
|
6496
6842
|
if (removingOutputs || feeExcess() <= 0) return;
|
|
6497
6843
|
if (!((ao === 1 || r.changeOutputs.length === 0) && r.changeOutputs.length < maxChangeOutputs)) return;
|
|
6498
6844
|
const cap = r.changeOutputs.length === 0 ? params.changeFirstSatoshis : params.changeInitialSatoshis;
|
|
6499
|
-
const
|
|
6845
|
+
const outputFunding = surplusPoolShaping ? feeExcess(0, 1) : feeExcess();
|
|
6846
|
+
const satoshis = Math.min(outputFunding, Math.max(dustFloor, cap));
|
|
6500
6847
|
if (satoshis >= dustFloor) r.changeOutputs.push({
|
|
6501
6848
|
satoshis,
|
|
6502
6849
|
lockingScriptLength: params.changeLockingScriptLength
|
|
@@ -6541,6 +6888,18 @@ async function generateChangeSdkCore(params, allocateChangeInput, releaseChangeI
|
|
|
6541
6888
|
};
|
|
6542
6889
|
}
|
|
6543
6890
|
/**
|
|
6891
|
+
* The action may already be funded entirely by explicit/fixed inputs. In
|
|
6892
|
+
* that case the allocator loop never runs, so capture the existing surplus
|
|
6893
|
+
* here before the no-change compatibility guard. This operation cannot
|
|
6894
|
+
* allocate an input; bounded legacy migration remains a separate step.
|
|
6895
|
+
*/
|
|
6896
|
+
materializeSurplusChangeOutput({
|
|
6897
|
+
params,
|
|
6898
|
+
result: r,
|
|
6899
|
+
dustFloor,
|
|
6900
|
+
feeExcess
|
|
6901
|
+
});
|
|
6902
|
+
/**
|
|
6544
6903
|
* Trigger an account funding event if we don't have enough to cover this transaction.
|
|
6545
6904
|
*/
|
|
6546
6905
|
if (feeExcess() < 0) {
|
|
@@ -6551,19 +6910,63 @@ async function generateChangeSdkCore(params, allocateChangeInput, releaseChangeI
|
|
|
6551
6910
|
}
|
|
6552
6911
|
/**
|
|
6553
6912
|
* If needed, seek funding to avoid overspending on fees without a change output to recapture it.
|
|
6913
|
+
* An economically unreturnable shaping remainder stays in the miner fee;
|
|
6914
|
+
* gathering another input solely to manufacture change would violate
|
|
6915
|
+
* surplus-only shaping and make the action less efficient.
|
|
6554
6916
|
*/
|
|
6555
|
-
|
|
6556
|
-
|
|
6557
|
-
|
|
6558
|
-
|
|
6559
|
-
|
|
6560
|
-
|
|
6561
|
-
|
|
6917
|
+
await requireViableChangeOrRetainBoundedFee({
|
|
6918
|
+
params,
|
|
6919
|
+
result: r,
|
|
6920
|
+
dustFloor,
|
|
6921
|
+
feeExcess,
|
|
6922
|
+
feeTarget,
|
|
6923
|
+
funding,
|
|
6924
|
+
spending,
|
|
6925
|
+
releaseAllocatedChangeInputs
|
|
6926
|
+
});
|
|
6927
|
+
/**
|
|
6928
|
+
* Progressively retire economically useful legacy fragments without ever
|
|
6929
|
+
* making them necessary for the requested action. The target of zero asks
|
|
6930
|
+
* canonical allocators for their smallest remaining output. A candidate at
|
|
6931
|
+
* or above the preferred value is not legacy migration material and is
|
|
6932
|
+
* immediately released.
|
|
6933
|
+
*/
|
|
6934
|
+
await migrateLegacyChangeInputs({
|
|
6935
|
+
params,
|
|
6936
|
+
result: r,
|
|
6937
|
+
targetNetCount,
|
|
6938
|
+
netChangeCount,
|
|
6939
|
+
feeTarget,
|
|
6940
|
+
allocateChangeInput,
|
|
6941
|
+
releaseChangeInput,
|
|
6942
|
+
recordAllocatedInput: (candidate) => {
|
|
6943
|
+
r.allocatedChangeInputs.push(candidate);
|
|
6944
|
+
allocatedFunding += candidate.satoshis;
|
|
6945
|
+
feeExcessNow = feeExcess();
|
|
6946
|
+
}
|
|
6947
|
+
});
|
|
6562
6948
|
/**
|
|
6563
6949
|
* Distribute the excess fees across the changeOutputs added.
|
|
6564
6950
|
*/
|
|
6565
6951
|
feeExcessNow = distributeExcessFees(r.changeOutputs, params.changeInitialSatoshis, feeExcessNow, rand);
|
|
6566
6952
|
/**
|
|
6953
|
+
* Pool growth is funded only from the surplus already present in the
|
|
6954
|
+
* transaction. Splitting one change output increases the serialized fee;
|
|
6955
|
+
* that exact delta is deducted before assigning the new outputs. If the
|
|
6956
|
+
* preferred minimum cannot be met, the transaction retains one smaller
|
|
6957
|
+
* output instead of gathering more inputs or refusing an otherwise valid
|
|
6958
|
+
* action.
|
|
6959
|
+
*/
|
|
6960
|
+
shapeSurplusChangeOutputs({
|
|
6961
|
+
params,
|
|
6962
|
+
result: r,
|
|
6963
|
+
targetNetCount,
|
|
6964
|
+
netChangeCount,
|
|
6965
|
+
maxChangeOutputs,
|
|
6966
|
+
feeTarget,
|
|
6967
|
+
rand
|
|
6968
|
+
});
|
|
6969
|
+
/**
|
|
6567
6970
|
* Remove any change outputs that ended up below the dust floor after distribution.
|
|
6568
6971
|
* Consolidates removed satoshis into the largest remaining output.
|
|
6569
6972
|
*/
|
|
@@ -6594,7 +6997,11 @@ function validateGenerateChangeSdkResult(params, r) {
|
|
|
6594
6997
|
ok = false;
|
|
6595
6998
|
}
|
|
6596
6999
|
const feeRequired = Math.ceil((r.size || 0) / 1e3 * (r.satsPerKb || 0));
|
|
6597
|
-
|
|
7000
|
+
const minSpendTxSize = transactionSize([params.changeUnlockingScriptLength], [params.changeLockingScriptLength]);
|
|
7001
|
+
const dustFloor = Math.max(1, Math.ceil(minSpendTxSize / 1e3 * (r.satsPerKb || 0)) * 2);
|
|
7002
|
+
const feeWithChangeOutput = Math.ceil(((r.size || 0) + transactionOutputSize(params.changeLockingScriptLength)) / 1e3 * (r.satsPerKb || 0));
|
|
7003
|
+
const isBoundedUnreturnableShapingSurplus = params.surplusPoolShaping === true && r.changeOutputs.length === 0 && r.fee > feeRequired && r.fee - feeWithChangeOutput < dustFloor;
|
|
7004
|
+
if (feeRequired !== r.fee && !isBoundedUnreturnableShapingSurplus) {
|
|
6598
7005
|
log += `required fee error ${feeRequired} !== ${r.fee};`;
|
|
6599
7006
|
ok = false;
|
|
6600
7007
|
}
|
|
@@ -6632,6 +7039,8 @@ function validateGenerateChangeSdkParams(params) {
|
|
|
6632
7039
|
params.feeModel = validateStorageFeeModel(params.feeModel);
|
|
6633
7040
|
if (params.feeModel.model !== "sat/kb") throw new WERR_INVALID_PARAMETER("feeModel.model", "'sat/kb'");
|
|
6634
7041
|
_bsv_sdk.Validation.validateOptionalInteger(params.targetNetCount, "targetNetCount");
|
|
7042
|
+
if (params.maxChangeOutputs !== -1) _bsv_sdk.Validation.validateOptionalInteger(params.maxChangeOutputs, "maxChangeOutputs", 1);
|
|
7043
|
+
if (params.maxMigrationInputs !== -1) _bsv_sdk.Validation.validateOptionalInteger(params.maxMigrationInputs, "maxMigrationInputs", 0);
|
|
6635
7044
|
_bsv_sdk.Validation.validateSatoshis(params.changeFirstSatoshis, "changeFirstSatoshis", 1);
|
|
6636
7045
|
_bsv_sdk.Validation.validateSatoshis(params.changeInitialSatoshis, "changeInitialSatoshis", 1);
|
|
6637
7046
|
_bsv_sdk.Validation.validateInteger(params.changeLockingScriptLength, "changeLockingScriptLength");
|
|
@@ -8099,6 +8508,8 @@ async function planFunding(state, args, explicit, noSendChange) {
|
|
|
8099
8508
|
const allocated = /* @__PURE__ */ new Map();
|
|
8100
8509
|
const noSend = [...noSendChange];
|
|
8101
8510
|
const changeBasket = state.begin.changeBasket;
|
|
8511
|
+
const policy = validateManagedChangePolicy(state.begin.managedChangePolicy);
|
|
8512
|
+
const preferredSatoshis = Math.max(1, changeBasket.minimumDesiredUTXOValue);
|
|
8102
8513
|
const params = {
|
|
8103
8514
|
fixedInputs: explicit.map((output, index) => ({
|
|
8104
8515
|
satoshis: output.satoshis,
|
|
@@ -8112,11 +8523,14 @@ async function planFunding(state, args, explicit, noSendChange) {
|
|
|
8112
8523
|
lockingScriptLength: 25
|
|
8113
8524
|
}] : []],
|
|
8114
8525
|
feeModel: state.begin.feeModel,
|
|
8115
|
-
changeInitialSatoshis:
|
|
8116
|
-
changeFirstSatoshis:
|
|
8526
|
+
changeInitialSatoshis: preferredSatoshis,
|
|
8527
|
+
changeFirstSatoshis: preferredSatoshis,
|
|
8117
8528
|
changeLockingScriptLength: 25,
|
|
8118
8529
|
changeUnlockingScriptLength: 107,
|
|
8119
8530
|
targetNetCount: changeBasket.numberOfDesiredUTXOs - state.estimatedChangeCount,
|
|
8531
|
+
maxChangeOutputs: policy.maxOutputsPerAction,
|
|
8532
|
+
surplusPoolShaping: true,
|
|
8533
|
+
maxMigrationInputs: policy.migrationInputsPerAction,
|
|
8120
8534
|
randomVals: args.randomVals
|
|
8121
8535
|
};
|
|
8122
8536
|
const allocate = async (targetSatoshis, exactSatoshis) => {
|
|
@@ -8135,7 +8549,18 @@ async function planFunding(state, args, explicit, noSendChange) {
|
|
|
8135
8549
|
allocated.delete(outputId);
|
|
8136
8550
|
if (noSendChange.includes(output)) noSend.push(output);
|
|
8137
8551
|
};
|
|
8138
|
-
|
|
8552
|
+
let result;
|
|
8553
|
+
try {
|
|
8554
|
+
result = await generateChangeSdk(params, allocate, release, args.logger);
|
|
8555
|
+
} catch (error) {
|
|
8556
|
+
if (!(error instanceof WERR_INSUFFICIENT_FUNDS)) throw error;
|
|
8557
|
+
result = await generateChangeSdk({
|
|
8558
|
+
...params,
|
|
8559
|
+
changeFirstSatoshis: 1,
|
|
8560
|
+
surplusPoolShaping: false,
|
|
8561
|
+
maxMigrationInputs: 0
|
|
8562
|
+
}, allocate, release, args.logger);
|
|
8563
|
+
}
|
|
8139
8564
|
return {
|
|
8140
8565
|
allocated: result.allocatedChangeInputs.map((input) => verifyTruthy(allocated.get(input.outputId))),
|
|
8141
8566
|
changeSatoshis: result.changeOutputs.map((output) => output.satoshis),
|
|
@@ -8393,6 +8818,19 @@ async function compressActionBatchPackItems(items, encoding, maxBytes, maxItems)
|
|
|
8393
8818
|
function mergeUnique(values) {
|
|
8394
8819
|
return [...new Set(values)];
|
|
8395
8820
|
}
|
|
8821
|
+
function isActionBatchStateError(error) {
|
|
8822
|
+
if (error instanceof WERRActionBatchState) return true;
|
|
8823
|
+
if (error == null || typeof error !== "object") return false;
|
|
8824
|
+
const candidate = error;
|
|
8825
|
+
return (candidate.name === "WERR_ACTION_BATCH_STATE" || candidate.code === "WERR_ACTION_BATCH_STATE") && typeof candidate.state === "string";
|
|
8826
|
+
}
|
|
8827
|
+
function isActionBatchCapacityError(error) {
|
|
8828
|
+
if (error == null || typeof error !== "object") return false;
|
|
8829
|
+
const candidate = error;
|
|
8830
|
+
const code = candidate.name === "Error" ? candidate.code : candidate.name ?? candidate.code;
|
|
8831
|
+
if (code === "WERR_INVALID_PARAMETER" && candidate.parameter === "firstAction") return true;
|
|
8832
|
+
return code === "WERR_INVALID_OPERATION" && typeof candidate.message === "string" && candidate.message.includes("action batch") && (candidate.message.includes("maximum") || candidate.message.includes("reserve"));
|
|
8833
|
+
}
|
|
8396
8834
|
function nextGeometricTarget(value) {
|
|
8397
8835
|
return Math.min(Number.MAX_SAFE_INTEGER, value * 2);
|
|
8398
8836
|
}
|
|
@@ -8439,6 +8877,7 @@ var ActionBatchWorkspace = class {
|
|
|
8439
8877
|
planned = /* @__PURE__ */ new Map();
|
|
8440
8878
|
actions = [];
|
|
8441
8879
|
lockingScripts = /* @__PURE__ */ new Map();
|
|
8880
|
+
canResume;
|
|
8442
8881
|
ewmaConfirmedInputs = 1;
|
|
8443
8882
|
ewmaConfirmedSatoshis = 0;
|
|
8444
8883
|
hasConfirmedSample = false;
|
|
@@ -8458,6 +8897,7 @@ var ActionBatchWorkspace = class {
|
|
|
8458
8897
|
this.batchId = begin.batchId;
|
|
8459
8898
|
this.state = makePlannerState(begin, firstAction);
|
|
8460
8899
|
this.expiresAt = Date.parse(begin.expiresAt);
|
|
8900
|
+
this.canResume = capabilities.resume === true && wallet.storage.resumeActionBatch != null;
|
|
8461
8901
|
this.eagerUploadLanes = Array.from({ length: capabilities.packedUploads == null ? 1 : Math.max(1, capabilities.maxConcurrentUploads) }, async () => {});
|
|
8462
8902
|
}
|
|
8463
8903
|
get usesCompactManifest() {
|
|
@@ -8471,6 +8911,15 @@ var ActionBatchWorkspace = class {
|
|
|
8471
8911
|
if (this.actions.some((action) => action.reference === reference || action.txid === reference)) return true;
|
|
8472
8912
|
return Object.entries(this.wallet.pendingSignActions).some(([pendingReference, pending]) => this.planned.has(pendingReference) && pending.tx.id("hex") === reference);
|
|
8473
8913
|
}
|
|
8914
|
+
ownsTransaction(txid) {
|
|
8915
|
+
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);
|
|
8916
|
+
}
|
|
8917
|
+
references(args) {
|
|
8918
|
+
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));
|
|
8919
|
+
}
|
|
8920
|
+
referencesSendWith(txids) {
|
|
8921
|
+
return txids.some((txid) => this.ownsTransaction(txid));
|
|
8922
|
+
}
|
|
8474
8923
|
get isEmpty() {
|
|
8475
8924
|
return this.planned.size === 0 && this.actions.length === 0;
|
|
8476
8925
|
}
|
|
@@ -8595,12 +9044,32 @@ var ActionBatchWorkspace = class {
|
|
|
8595
9044
|
satoshis: extension.reservedOutputs.reduce((sum, output) => sum + output.satoshis, 0)
|
|
8596
9045
|
};
|
|
8597
9046
|
}
|
|
9047
|
+
async resume() {
|
|
9048
|
+
const outpoints = [...new Map([...this.state.reserved.values(), ...this.state.explicit.values()].filter((output) => output.txid != null).map((output) => [`${output.txid}.${output.vout}`, {
|
|
9049
|
+
txid: output.txid,
|
|
9050
|
+
vout: output.vout
|
|
9051
|
+
}])).values()];
|
|
9052
|
+
const result = await this.wallet.storage.resumeActionBatch({
|
|
9053
|
+
batchId: this.batchId,
|
|
9054
|
+
outpoints
|
|
9055
|
+
});
|
|
9056
|
+
this.expiresAt = Date.parse(result.expiresAt);
|
|
9057
|
+
}
|
|
8598
9058
|
async renewIfNeeded() {
|
|
8599
|
-
if (Date.now() >= this.expiresAt)
|
|
9059
|
+
if (Date.now() >= this.expiresAt) {
|
|
9060
|
+
if (this.canResume) await this.resume();
|
|
9061
|
+
return;
|
|
9062
|
+
}
|
|
8600
9063
|
const renewAt = this.expiresAt - this.capabilities.leaseMs * .2;
|
|
8601
9064
|
if (Date.now() < renewAt) return;
|
|
8602
9065
|
this.renewal ??= this.wallet.storage.renewActionBatch(this.batchId).then((result) => {
|
|
8603
9066
|
this.expiresAt = Date.parse(result.expiresAt);
|
|
9067
|
+
}).catch(async (error) => {
|
|
9068
|
+
if (isActionBatchStateError(error) && error.state === "expired") {
|
|
9069
|
+
if (this.canResume) await this.resume();
|
|
9070
|
+
return;
|
|
9071
|
+
}
|
|
9072
|
+
throw error;
|
|
8604
9073
|
}).finally(() => {
|
|
8605
9074
|
this.renewal = void 0;
|
|
8606
9075
|
});
|
|
@@ -9031,24 +9500,42 @@ var ActionBatchController = class {
|
|
|
9031
9500
|
this.workspace = new ActionBatchWorkspace(this.wallet, begin, args, capabilities);
|
|
9032
9501
|
return this.workspace;
|
|
9033
9502
|
}
|
|
9503
|
+
async retire(workspace) {
|
|
9504
|
+
await workspace.abort().catch(() => void 0);
|
|
9505
|
+
if (this.workspace === workspace) this.workspace = void 0;
|
|
9506
|
+
}
|
|
9507
|
+
async recover(workspace, input, isDelayed = false, resume = false) {
|
|
9508
|
+
try {
|
|
9509
|
+
if (resume) await workspace.resume();
|
|
9510
|
+
return Array.isArray(input) ? await workspace.commit(input, isDelayed) : await workspace.plan(input);
|
|
9511
|
+
} catch (error) {
|
|
9512
|
+
if (!resume && isActionBatchStateError(error) && error.state === "expired" && workspace.canResume) return await this.recover(workspace, input, isDelayed, true);
|
|
9513
|
+
if (isActionBatchStateError(error) && error.state !== "expired") await this.retire(workspace);
|
|
9514
|
+
throw error;
|
|
9515
|
+
}
|
|
9516
|
+
}
|
|
9034
9517
|
async plan(args) {
|
|
9035
9518
|
return await this.runExclusive(async () => {
|
|
9036
9519
|
if (!args.isNewTx) return void 0;
|
|
9037
|
-
|
|
9038
|
-
|
|
9039
|
-
|
|
9040
|
-
|
|
9041
|
-
workspace = await this.begin(args);
|
|
9042
|
-
if (workspace == null) return void 0;
|
|
9043
|
-
beganWorkspace = true;
|
|
9520
|
+
const workspace = this.workspace;
|
|
9521
|
+
if (workspace != null) {
|
|
9522
|
+
if (!workspace.references(args)) return void 0;
|
|
9523
|
+
return await this.recover(workspace, args);
|
|
9044
9524
|
}
|
|
9525
|
+
if (!args.isNoSend) return void 0;
|
|
9526
|
+
let begun;
|
|
9045
9527
|
try {
|
|
9046
|
-
|
|
9528
|
+
begun = await this.begin(args);
|
|
9047
9529
|
} catch (error) {
|
|
9048
|
-
if (
|
|
9049
|
-
|
|
9050
|
-
|
|
9051
|
-
|
|
9530
|
+
if (isActionBatchCapacityError(error)) return void 0;
|
|
9531
|
+
throw error;
|
|
9532
|
+
}
|
|
9533
|
+
if (begun == null) return void 0;
|
|
9534
|
+
try {
|
|
9535
|
+
return await begun.plan(args);
|
|
9536
|
+
} catch (error) {
|
|
9537
|
+
await this.retire(begun);
|
|
9538
|
+
if (isActionBatchCapacityError(error)) return void 0;
|
|
9052
9539
|
throw error;
|
|
9053
9540
|
}
|
|
9054
9541
|
});
|
|
@@ -9058,11 +9545,21 @@ var ActionBatchController = class {
|
|
|
9058
9545
|
const workspace = this.workspace;
|
|
9059
9546
|
if (workspace == null) return void 0;
|
|
9060
9547
|
if (prior != null && !workspace.ownsReference(prior.reference)) return void 0;
|
|
9548
|
+
if (prior == null && !workspace.referencesSendWith(args.options.sendWith)) return void 0;
|
|
9061
9549
|
if (prior != null) workspace.stage(prior, args);
|
|
9062
|
-
|
|
9550
|
+
const referencesWorkspace = workspace.referencesSendWith(args.options.sendWith);
|
|
9551
|
+
if (prior != null && args.isNoSend && args.isSendWith && !referencesWorkspace) return await this.wallet.storage.processAction({
|
|
9552
|
+
isNewTx: false,
|
|
9553
|
+
isSendWith: true,
|
|
9554
|
+
isNoSend: false,
|
|
9555
|
+
isDelayed: args.isDelayed,
|
|
9556
|
+
sendWith: [...args.options.sendWith],
|
|
9557
|
+
logger: args.logger
|
|
9558
|
+
});
|
|
9559
|
+
if (!(referencesWorkspace || prior != null && !args.isNoSend)) return { sendWithResults: [] };
|
|
9063
9560
|
const sendWith = [...args.options.sendWith];
|
|
9064
9561
|
if (prior != null && !args.isNoSend && !sendWith.includes(prior.tx.id("hex"))) sendWith.push(prior.tx.id("hex"));
|
|
9065
|
-
const result = await
|
|
9562
|
+
const result = await this.recover(workspace, sendWith, args.isDelayed);
|
|
9066
9563
|
this.workspace = void 0;
|
|
9067
9564
|
return result;
|
|
9068
9565
|
});
|
|
@@ -9806,14 +10303,15 @@ var Wallet = class {
|
|
|
9806
10303
|
}
|
|
9807
10304
|
/**
|
|
9808
10305
|
* Uses `listOutputs` special operation to review the spendability via `Services` of
|
|
9809
|
-
* outputs currently considered spendable. Returns
|
|
10306
|
+
* outputs currently considered spendable. Returns only outputs conclusively
|
|
10307
|
+
* confirmed spent. Rejects the review if any provider result is inconclusive.
|
|
9810
10308
|
*
|
|
9811
10309
|
* Ignores the `limit` and `offset` properties.
|
|
9812
10310
|
*
|
|
9813
10311
|
* @param all Defaults to false. If false, only change outputs ('default' basket) are reviewed. If true, all spendable outputs are reviewed.
|
|
9814
|
-
* @param release Defaults to false. If true, sets
|
|
10312
|
+
* @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.
|
|
9815
10313
|
* @param optionalArgs Optional. Additional tags will constrain the outputs processed.
|
|
9816
|
-
* @returns outputs
|
|
10314
|
+
* @returns outputs previously considered spendable but conclusively confirmed spent.
|
|
9817
10315
|
*/
|
|
9818
10316
|
async reviewSpendableOutputs(all = false, release = false, optionalArgs) {
|
|
9819
10317
|
const args = {
|
|
@@ -10021,7 +10519,7 @@ async function createActionCore(storage, auth, vargs, parent) {
|
|
|
10021
10519
|
});
|
|
10022
10520
|
const feeModel = validateStorageFeeModel(storage.feeModel);
|
|
10023
10521
|
logger?.log(`validated fee model ${JSON.stringify(feeModel)}`);
|
|
10024
|
-
const initialFundingPlan = await
|
|
10522
|
+
const initialFundingPlan = await prepareFundingPlanWithLiquidityPolicy(storage, [
|
|
10025
10523
|
userId,
|
|
10026
10524
|
vargs,
|
|
10027
10525
|
xinputs,
|
|
@@ -10609,7 +11107,9 @@ async function traceStorageStep(storage, name, parent, attributes, callback) {
|
|
|
10609
11107
|
attributes
|
|
10610
11108
|
}, async (span) => await callback(span));
|
|
10611
11109
|
}
|
|
10612
|
-
function makeFundingParams(
|
|
11110
|
+
function makeFundingParams(args) {
|
|
11111
|
+
const { storage, vargs, xinputs, xoutputs, changeBasket, feeModel, healthyChangeCount, compatibilityFallback } = args;
|
|
11112
|
+
const preferredSatoshis = Math.max(1, changeBasket.minimumDesiredUTXOValue);
|
|
10613
11113
|
return {
|
|
10614
11114
|
fixedInputs: xinputs.map((input) => ({
|
|
10615
11115
|
satoshis: input.satoshis,
|
|
@@ -10620,35 +11120,52 @@ function makeFundingParams(vargs, xinputs, xoutputs, changeBasket, feeModel, ava
|
|
|
10620
11120
|
lockingScriptLength: output.lockingScript.length / 2
|
|
10621
11121
|
})),
|
|
10622
11122
|
feeModel,
|
|
10623
|
-
changeInitialSatoshis:
|
|
10624
|
-
changeFirstSatoshis:
|
|
11123
|
+
changeInitialSatoshis: preferredSatoshis,
|
|
11124
|
+
changeFirstSatoshis: compatibilityFallback ? 1 : preferredSatoshis,
|
|
10625
11125
|
changeLockingScriptLength: 25,
|
|
10626
11126
|
changeUnlockingScriptLength: 107,
|
|
10627
|
-
targetNetCount: changeBasket.numberOfDesiredUTXOs -
|
|
11127
|
+
targetNetCount: changeBasket.numberOfDesiredUTXOs - healthyChangeCount,
|
|
11128
|
+
maxChangeOutputs: storage.managedChangePolicy.maxOutputsPerAction,
|
|
11129
|
+
surplusPoolShaping: !compatibilityFallback,
|
|
11130
|
+
maxMigrationInputs: compatibilityFallback ? 0 : storage.managedChangePolicy.migrationInputsPerAction,
|
|
10628
11131
|
randomVals: vargs.randomVals
|
|
10629
11132
|
};
|
|
10630
11133
|
}
|
|
10631
|
-
async function
|
|
10632
|
-
const [userId, vargs, xinputs, xoutputs, changeBasket, noSendChangeIn, feeModel
|
|
10633
|
-
const candidates = await traceStorageStep(storage, "wallet.storage.create_action.funding_candidates", parent, { "funding.exclude_sending": excludeSending }, async (span) => {
|
|
10634
|
-
const outputs = await storage.findAvailableManagedChangeInputCandidates(userId, changeBasket.basketId, excludeSending, trx);
|
|
10635
|
-
span?.end({ attributes: {
|
|
10636
|
-
"funding.candidate_count": outputs.length,
|
|
10637
|
-
"funding.candidate_satoshis": outputs.reduce((sum, output) => sum + output.satoshis, 0)
|
|
10638
|
-
} });
|
|
10639
|
-
return outputs;
|
|
10640
|
-
});
|
|
11134
|
+
async function buildFundingPlan(storage, context, candidates, eligibleStatuses, policyTier, compatibilityFallback, parent) {
|
|
11135
|
+
const [userId, vargs, xinputs, xoutputs, changeBasket, noSendChangeIn, feeModel] = context;
|
|
10641
11136
|
const noSendIds = new Set(noSendChangeIn.map((output) => output.outputId));
|
|
10642
|
-
const available = candidates.filter((output) => !noSendIds.has(output.outputId));
|
|
10643
|
-
const
|
|
11137
|
+
const available = candidates.filter((output) => !noSendIds.has(output.outputId) && eligibleStatuses.includes(output.transactionStatus));
|
|
11138
|
+
const preferredSatoshis = Math.max(1, changeBasket.minimumDesiredUTXOValue);
|
|
11139
|
+
const params = makeFundingParams({
|
|
11140
|
+
storage,
|
|
11141
|
+
vargs,
|
|
11142
|
+
xinputs,
|
|
11143
|
+
xoutputs,
|
|
11144
|
+
changeBasket,
|
|
11145
|
+
feeModel,
|
|
11146
|
+
healthyChangeCount: compatibilityFallback ? candidates.filter((output) => eligibleStatuses.includes(output.transactionStatus)).length : candidates.filter((output) => output.satoshis >= preferredSatoshis).length,
|
|
11147
|
+
compatibilityFallback
|
|
11148
|
+
});
|
|
11149
|
+
const noSendStatuses = await storage.findTransactionStatusesByIds(userId, noSendChangeIn.map((output) => output.transactionId));
|
|
11150
|
+
const plannedNoSend = noSendChangeIn.map((output) => ({
|
|
11151
|
+
outputId: output.outputId,
|
|
11152
|
+
transactionId: output.transactionId,
|
|
11153
|
+
satoshis: output.satoshis,
|
|
11154
|
+
txid: output.txid,
|
|
11155
|
+
vout: output.vout,
|
|
11156
|
+
transactionStatus: noSendStatuses.get(output.transactionId) ?? "nosend"
|
|
11157
|
+
}));
|
|
11158
|
+
const claimStatuses = [.../* @__PURE__ */ new Set([...eligibleStatuses, ...plannedNoSend.map((output) => output.transactionStatus)])];
|
|
10644
11159
|
return await traceStorageStep(storage, "wallet.storage.create_action.funding_plan", parent, {
|
|
10645
11160
|
"funding.candidate_count": available.length,
|
|
10646
|
-
"funding.no_send_change_count": noSendChangeIn.length
|
|
11161
|
+
"funding.no_send_change_count": noSendChangeIn.length,
|
|
11162
|
+
"funding.policy_tier": policyTier,
|
|
11163
|
+
"funding.compatibility_fallback": compatibilityFallback
|
|
10647
11164
|
}, async (span) => {
|
|
10648
11165
|
const allocated = /* @__PURE__ */ new Map();
|
|
10649
11166
|
const availableSelector = new CanonicalChangeSelector(available);
|
|
10650
|
-
const noSend = [...
|
|
10651
|
-
const noSendById = new Map(
|
|
11167
|
+
const noSend = [...plannedNoSend];
|
|
11168
|
+
const noSendById = new Map(plannedNoSend.map((output) => [output.outputId, output]));
|
|
10652
11169
|
const allocate = async (targetSatoshis, exactSatoshis) => {
|
|
10653
11170
|
let output = noSend.pop();
|
|
10654
11171
|
output ??= availableSelector.take(targetSatoshis, exactSatoshis);
|
|
@@ -10679,31 +11196,118 @@ async function prepareFundingPlan(storage, context) {
|
|
|
10679
11196
|
result,
|
|
10680
11197
|
selected,
|
|
10681
11198
|
availableChangeCount: candidates.length,
|
|
10682
|
-
|
|
11199
|
+
eligibleStatuses: claimStatuses,
|
|
11200
|
+
policyTier,
|
|
11201
|
+
compatibilityFallback
|
|
10683
11202
|
};
|
|
10684
11203
|
});
|
|
10685
11204
|
}
|
|
10686
|
-
async function
|
|
10687
|
-
const
|
|
11205
|
+
async function fundingPlanSerializedCost(storage, plan, knownTxids) {
|
|
11206
|
+
const txids = [...new Set(plan.selected.map((candidate) => verifyTruthy(candidate.txid)))];
|
|
11207
|
+
if (txids.length === 0) return plan.result.size;
|
|
10688
11208
|
try {
|
|
10689
|
-
|
|
10690
|
-
|
|
10691
|
-
|
|
10692
|
-
|
|
10693
|
-
|
|
10694
|
-
|
|
11209
|
+
const beef = await storage.getBeefForTransactions(txids, {
|
|
11210
|
+
knownTxids,
|
|
11211
|
+
ignoreStorage: false,
|
|
11212
|
+
ignoreServices: true,
|
|
11213
|
+
ignoreNewProven: false
|
|
11214
|
+
});
|
|
11215
|
+
return plan.result.size + beef.toUint8Array().length;
|
|
11216
|
+
} catch {
|
|
11217
|
+
return Number.MAX_SAFE_INTEGER;
|
|
11218
|
+
}
|
|
11219
|
+
}
|
|
11220
|
+
const FUNDING_TIERS = [
|
|
11221
|
+
{
|
|
11222
|
+
policyTier: "completed",
|
|
11223
|
+
statuses: ["completed"]
|
|
11224
|
+
},
|
|
11225
|
+
{
|
|
11226
|
+
policyTier: "unproven",
|
|
11227
|
+
statuses: ["completed", "unproven"]
|
|
11228
|
+
},
|
|
11229
|
+
{
|
|
11230
|
+
policyTier: "sending",
|
|
11231
|
+
statuses: [
|
|
11232
|
+
"completed",
|
|
11233
|
+
"unproven",
|
|
11234
|
+
"sending"
|
|
11235
|
+
]
|
|
11236
|
+
}
|
|
11237
|
+
];
|
|
11238
|
+
async function resolveFundingCandidates(storage, userId, basketId, parent, trx) {
|
|
11239
|
+
const rawCandidates = await traceStorageStep(storage, "wallet.storage.create_action.funding_candidates", parent, { "funding.include_pending": true }, async (span) => {
|
|
11240
|
+
const outputs = await storage.findAvailableManagedChangeInputCandidates(userId, basketId, false, trx);
|
|
11241
|
+
span?.end({ attributes: {
|
|
11242
|
+
"funding.candidate_count": outputs.length,
|
|
11243
|
+
"funding.candidate_satoshis": outputs.reduce((sum, output) => sum + output.satoshis, 0),
|
|
11244
|
+
"funding.completed_candidate_count": outputs.filter((output) => output.transactionStatus === "completed").length,
|
|
11245
|
+
"funding.unproven_candidate_count": outputs.filter((output) => output.transactionStatus === "unproven").length,
|
|
11246
|
+
"funding.sending_candidate_count": outputs.filter((output) => output.transactionStatus === "sending").length
|
|
11247
|
+
} });
|
|
11248
|
+
return outputs;
|
|
11249
|
+
});
|
|
11250
|
+
const missingStatusIds = rawCandidates.filter((output) => output.transactionStatus == null).map((output) => output.transactionId);
|
|
11251
|
+
const missingStatuses = await storage.findTransactionStatusesByIds(userId, missingStatusIds, trx);
|
|
11252
|
+
const candidates = rawCandidates.map((output) => ({
|
|
11253
|
+
...output,
|
|
11254
|
+
transactionStatus: output.transactionStatus ?? missingStatuses.get(output.transactionId)
|
|
11255
|
+
}));
|
|
11256
|
+
if (candidates.some((output) => output.transactionStatus == null)) throw new WERR_INTERNAL("managed change candidate is missing its source transaction status");
|
|
11257
|
+
return candidates;
|
|
11258
|
+
}
|
|
11259
|
+
async function buildFundingPlanForTier(storage, context, candidates, tier, parent) {
|
|
11260
|
+
try {
|
|
11261
|
+
return { plan: await buildFundingPlan(storage, context, candidates, tier.statuses, tier.policyTier, false, parent) };
|
|
10695
11262
|
} catch (error) {
|
|
10696
|
-
if (!
|
|
10697
|
-
|
|
10698
|
-
|
|
10699
|
-
|
|
10700
|
-
|
|
10701
|
-
|
|
10702
|
-
|
|
11263
|
+
if (!(error instanceof WERR_INSUFFICIENT_FUNDS)) throw error;
|
|
11264
|
+
}
|
|
11265
|
+
try {
|
|
11266
|
+
return { plan: await buildFundingPlan(storage, context, candidates, tier.statuses, "compatibility", true, parent) };
|
|
11267
|
+
} catch (error) {
|
|
11268
|
+
if (!(error instanceof WERR_INSUFFICIENT_FUNDS)) throw error;
|
|
11269
|
+
return { fundingError: error };
|
|
11270
|
+
}
|
|
11271
|
+
}
|
|
11272
|
+
function firstPlanNeedsNoPendingComparison(storage, plan) {
|
|
11273
|
+
const threshold = storage.managedChangePolicy.pendingComparisonInputs;
|
|
11274
|
+
return threshold === -1 || plan.selected.length <= threshold;
|
|
11275
|
+
}
|
|
11276
|
+
async function chooseLowestSerializedFundingPlan(storage, plans, knownTxids) {
|
|
11277
|
+
let chosen = plans[0];
|
|
11278
|
+
let chosenCost = await fundingPlanSerializedCost(storage, chosen, knownTxids);
|
|
11279
|
+
for (const alternative of plans.slice(1)) {
|
|
11280
|
+
const cost = await fundingPlanSerializedCost(storage, alternative, knownTxids);
|
|
11281
|
+
if (cost < chosenCost) {
|
|
11282
|
+
chosen = alternative;
|
|
11283
|
+
chosenCost = cost;
|
|
11284
|
+
}
|
|
11285
|
+
}
|
|
11286
|
+
return chosen;
|
|
11287
|
+
}
|
|
11288
|
+
async function prepareFundingPlanWithLiquidityPolicy(storage, context, parent, trx) {
|
|
11289
|
+
const [userId, vargs, , , changeBasket] = context;
|
|
11290
|
+
const candidates = await resolveFundingCandidates(storage, userId, changeBasket.basketId, parent, trx);
|
|
11291
|
+
const successful = [];
|
|
11292
|
+
let fundingError;
|
|
11293
|
+
for (const tier of FUNDING_TIERS) {
|
|
11294
|
+
const attempted = await buildFundingPlanForTier(storage, context, candidates, tier, parent);
|
|
11295
|
+
fundingError = attempted.fundingError ?? fundingError;
|
|
11296
|
+
const plan = attempted.plan;
|
|
11297
|
+
if (plan != null) {
|
|
11298
|
+
successful.push(plan);
|
|
11299
|
+
if (successful.length === 1 && firstPlanNeedsNoPendingComparison(storage, plan)) return plan;
|
|
11300
|
+
}
|
|
11301
|
+
}
|
|
11302
|
+
if (successful.length > 0) {
|
|
11303
|
+
if (successful.length === 1) return successful[0];
|
|
11304
|
+
return await chooseLowestSerializedFundingPlan(storage, successful, vargs.options.knownTxids);
|
|
10703
11305
|
}
|
|
11306
|
+
if (fundingError != null) throw fundingError;
|
|
11307
|
+
throw new WERR_INTERNAL("funding policy produced neither a plan nor an error");
|
|
10704
11308
|
}
|
|
10705
11309
|
async function claimFundingPlan(storage, request) {
|
|
10706
|
-
const [userId, basketId,
|
|
11310
|
+
const [userId, basketId, eligibleStatuses, transactionId, noSendChangeIn, plan, trx] = request;
|
|
10707
11311
|
if (plan.selected.length === 0) return {
|
|
10708
11312
|
outputs: [],
|
|
10709
11313
|
sourceTransactionCount: 0,
|
|
@@ -10711,10 +11315,8 @@ async function claimFundingPlan(storage, request) {
|
|
|
10711
11315
|
scriptSourceTransactionCount: 0
|
|
10712
11316
|
};
|
|
10713
11317
|
const noSendIds = new Set(noSendChangeIn.map((output) => output.outputId));
|
|
10714
|
-
const statuses = ["completed", "unproven"];
|
|
10715
|
-
if (!excludeSending) statuses.push("sending");
|
|
10716
11318
|
const claim = await storage.transaction(async (claimTrx) => {
|
|
10717
|
-
const currentById = await storage.findFundingOutputsForUpdate(userId, plan.selected.map((output) => output.outputId),
|
|
11319
|
+
const currentById = await storage.findFundingOutputsForUpdate(userId, plan.selected.map((output) => output.outputId), eligibleStatuses, claimTrx);
|
|
10718
11320
|
const transactionIds = [...new Set(Object.values(currentById).map((output) => output.transactionId))];
|
|
10719
11321
|
const claimed = [];
|
|
10720
11322
|
for (const planned of plan.selected) {
|
|
@@ -10784,7 +11386,7 @@ async function fundNewTransactionSdk(storage, userId, vargs, ctx, initialPlan, p
|
|
|
10784
11386
|
const claim = await claimFundingPlan(storage, [
|
|
10785
11387
|
userId,
|
|
10786
11388
|
ctx.changeBasket.basketId,
|
|
10787
|
-
plan.
|
|
11389
|
+
plan.eligibleStatuses,
|
|
10788
11390
|
ctx.transactionId,
|
|
10789
11391
|
ctx.noSendChangeIn,
|
|
10790
11392
|
plan,
|
|
@@ -10802,7 +11404,7 @@ async function fundNewTransactionSdk(storage, userId, vargs, ctx, initialPlan, p
|
|
|
10802
11404
|
}
|
|
10803
11405
|
if (claim.conflict === "noSendChange") throw new WERR_INVALID_PARAMETER("noSendChange", "outputs that remain spendable during action planning");
|
|
10804
11406
|
retryCount++;
|
|
10805
|
-
plan = await
|
|
11407
|
+
plan = await prepareFundingPlanWithLiquidityPolicy(storage, [
|
|
10806
11408
|
userId,
|
|
10807
11409
|
vargs,
|
|
10808
11410
|
ctx.xinputs,
|
|
@@ -12955,7 +13557,7 @@ var StorageReaderWriter = class extends StorageReader {
|
|
|
12955
13557
|
userId: user.userId,
|
|
12956
13558
|
name: "default",
|
|
12957
13559
|
numberOfDesiredUTXOs: 144,
|
|
12958
|
-
minimumDesiredUTXOValue:
|
|
13560
|
+
minimumDesiredUTXOValue: DEFAULT_MANAGED_CHANGE_MINIMUM_SATOSHIS,
|
|
12959
13561
|
isDeleted: false
|
|
12960
13562
|
});
|
|
12961
13563
|
break;
|
|
@@ -13236,8 +13838,10 @@ function validateActionBatchInlinePayload(manifest) {
|
|
|
13236
13838
|
for (const { digest, bytes } of inlineBlobs) if (actionBatchBlobDigest(bytes) !== digest) throw new WERR_INVALID_PARAMETER("inlineBlobs", `content matching digest ${digest}`);
|
|
13237
13839
|
}
|
|
13238
13840
|
function requireUploadableBatch(batch) {
|
|
13239
|
-
if (batch == null
|
|
13240
|
-
if (batch.
|
|
13841
|
+
if (batch == null) throw new WERRActionBatchState("missing");
|
|
13842
|
+
if (batch.status === "committed") throw new WERRActionBatchState("committed", batch.batchId);
|
|
13843
|
+
if (batch.status === "aborted") throw new WERRActionBatchState("aborted", batch.batchId);
|
|
13844
|
+
if (batch.hardExpiresAt.getTime() <= Date.now()) throw new WERRActionBatchState("hard-expired", batch.batchId);
|
|
13241
13845
|
return batch;
|
|
13242
13846
|
}
|
|
13243
13847
|
function manifestPhysicalDigests(manifest) {
|
|
@@ -13597,7 +14201,10 @@ const ACTION_BATCH_LEASE_MS = 900 * 1e3;
|
|
|
13597
14201
|
const ACTION_BATCH_HARD_LIFETIME_MS = 3600 * 1e3;
|
|
13598
14202
|
const INITIAL_RESERVATION_LIMIT = 8;
|
|
13599
14203
|
const INITIAL_EXTRA_OUTPUTS = 3;
|
|
13600
|
-
function
|
|
14204
|
+
function isValidOutpoint(outpoint) {
|
|
14205
|
+
return /^[0-9a-f]{64}$/.test(outpoint.txid) && Number.isSafeInteger(outpoint.vout) && outpoint.vout >= 0;
|
|
14206
|
+
}
|
|
14207
|
+
function getActionBatchCapabilities(maxReservedOutputs = 256, supportsResume = false) {
|
|
13601
14208
|
return { actionBatch: {
|
|
13602
14209
|
version: 1,
|
|
13603
14210
|
maxInlineBytes: ACTION_BATCH_MAX_INLINE_BYTES,
|
|
@@ -13605,6 +14212,8 @@ function getActionBatchCapabilities() {
|
|
|
13605
14212
|
maxConcurrentUploads: 4,
|
|
13606
14213
|
leaseMs: ACTION_BATCH_LEASE_MS,
|
|
13607
14214
|
hardLifetimeMs: ACTION_BATCH_HARD_LIFETIME_MS,
|
|
14215
|
+
resume: supportsResume ? true : void 0,
|
|
14216
|
+
maxReservedOutputs,
|
|
13608
14217
|
compactBegin: true,
|
|
13609
14218
|
manifestVersion: 2,
|
|
13610
14219
|
commitByDigest: true,
|
|
@@ -13620,6 +14229,14 @@ function getActionBatchCapabilities() {
|
|
|
13620
14229
|
function activeExpiry(batch, now = /* @__PURE__ */ new Date()) {
|
|
13621
14230
|
return batch.status !== "active" && batch.status !== "prepared" || batch.expiresAt.getTime() <= now.getTime() || batch.hardExpiresAt.getTime() <= now.getTime();
|
|
13622
14231
|
}
|
|
14232
|
+
function actionBatchErrorState(batch, now = /* @__PURE__ */ new Date()) {
|
|
14233
|
+
if (batch == null) return "missing";
|
|
14234
|
+
if (batch.hardExpiresAt.getTime() <= now.getTime()) return "hard-expired";
|
|
14235
|
+
if (batch.status === "aborted") return "aborted";
|
|
14236
|
+
if (batch.status === "committed") return "committed";
|
|
14237
|
+
if (batch.status === "expired" || batch.expiresAt.getTime() <= now.getTime()) return "expired";
|
|
14238
|
+
if (batch.status !== "active" && batch.status !== "prepared") return "inactive";
|
|
14239
|
+
}
|
|
13623
14240
|
function canExpire(batch, now = /* @__PURE__ */ new Date()) {
|
|
13624
14241
|
return (batch.status === "active" || batch.status === "prepared") && (batch.expiresAt.getTime() <= now.getTime() || batch.hardExpiresAt.getTime() <= now.getTime());
|
|
13625
14242
|
}
|
|
@@ -13705,12 +14322,22 @@ function estimateFirstActionTarget(storage, args, inputSatoshis, outputScriptLen
|
|
|
13705
14322
|
const minFee = Math.ceil(transactionSize([...inputLengths, 107], outputLengths) * fee / 1e3);
|
|
13706
14323
|
return Math.max(1, outputSatoshis + minFee - inputSatoshis);
|
|
13707
14324
|
}
|
|
14325
|
+
function selectReservationChange(candidates, targetSatoshis) {
|
|
14326
|
+
for (const status of [
|
|
14327
|
+
"completed",
|
|
14328
|
+
"unproven",
|
|
14329
|
+
"sending"
|
|
14330
|
+
]) {
|
|
14331
|
+
const selected = selectCanonicalChange(candidates.filter((candidate) => candidate.transactionStatus === status), targetSatoshis);
|
|
14332
|
+
if (selected != null) return selected;
|
|
14333
|
+
}
|
|
14334
|
+
}
|
|
13708
14335
|
function chooseReservationPool(candidates, targetSatoshis, limit, extras, fillLimit, planningCosts) {
|
|
13709
14336
|
const remaining = candidates.filter((output) => output.satoshis > planningCosts.marginalInputFee);
|
|
13710
14337
|
const chosen = [];
|
|
13711
14338
|
let deficit = targetSatoshis + planningCosts.firstChangeCost;
|
|
13712
14339
|
while (deficit > 0 && chosen.length < limit) {
|
|
13713
|
-
const output =
|
|
14340
|
+
const output = selectReservationChange(remaining, deficit + planningCosts.marginalInputFee);
|
|
13714
14341
|
if (output == null) break;
|
|
13715
14342
|
chosen.push(output);
|
|
13716
14343
|
remaining.splice(remaining.indexOf(output), 1);
|
|
@@ -13718,14 +14345,18 @@ function chooseReservationPool(candidates, targetSatoshis, limit, extras, fillLi
|
|
|
13718
14345
|
}
|
|
13719
14346
|
const desiredCount = fillLimit ? limit : Math.min(limit, chosen.length + extras);
|
|
13720
14347
|
while (remaining.length > 0 && chosen.length < desiredCount) {
|
|
13721
|
-
const output =
|
|
14348
|
+
const output = selectReservationChange(remaining, targetSatoshis);
|
|
13722
14349
|
if (output == null) break;
|
|
13723
14350
|
chosen.push(output);
|
|
13724
14351
|
remaining.splice(remaining.indexOf(output), 1);
|
|
13725
14352
|
}
|
|
13726
14353
|
return chosen;
|
|
13727
14354
|
}
|
|
13728
|
-
function
|
|
14355
|
+
function reservationOutput(candidate) {
|
|
14356
|
+
const { transactionStatus: _transactionStatus, ...output } = candidate;
|
|
14357
|
+
return output;
|
|
14358
|
+
}
|
|
14359
|
+
function reservationPlanningCosts(storage, _basket) {
|
|
13729
14360
|
const satsPerKb = validateStorageFeeModel(storage.feeModel).value ?? 0;
|
|
13730
14361
|
const minimumSpendSize = transactionSize([107], [25]);
|
|
13731
14362
|
const minimumSpendFee = Math.ceil(minimumSpendSize * satsPerKb / 1e3);
|
|
@@ -13733,7 +14364,7 @@ function reservationPlanningCosts(storage, basket) {
|
|
|
13733
14364
|
const marginalInputSize = transactionSize([107], []) - transactionSize([], []);
|
|
13734
14365
|
const marginalOutputSize = transactionSize([], [25]) - transactionSize([], []);
|
|
13735
14366
|
return {
|
|
13736
|
-
firstChangeCost:
|
|
14367
|
+
firstChangeCost: dustFloor + Math.ceil(marginalOutputSize * satsPerKb / 1e3),
|
|
13737
14368
|
marginalInputFee: Math.ceil(marginalInputSize * satsPerKb / 1e3)
|
|
13738
14369
|
};
|
|
13739
14370
|
}
|
|
@@ -13808,18 +14439,25 @@ async function beginActionBatch(storage, auth, args) {
|
|
|
13808
14439
|
const explicit = await resolveExplicitOutputs(storage, userId, args.firstAction, outputScriptLengths != null);
|
|
13809
14440
|
const noSendChange = await resolveNoSendChangeOutputs(storage, userId, args.firstAction);
|
|
13810
14441
|
const fixedOutputIds = new Set([...explicit.outputs, ...noSendChange.outputs].map((output) => output.outputId));
|
|
13811
|
-
const
|
|
14442
|
+
const availableOutputs = (await storage.findAvailableManagedChangeInputs(userId, changeBasket.basketId, false)).filter((output) => !fixedOutputIds.has(output.outputId));
|
|
14443
|
+
const availableStatuses = await storage.findTransactionStatusesByIds(userId, availableOutputs.map((output) => output.transactionId));
|
|
14444
|
+
const available = availableOutputs.map((output) => ({
|
|
14445
|
+
...output,
|
|
14446
|
+
transactionStatus: availableStatuses.get(output.transactionId) ?? "sending"
|
|
14447
|
+
}));
|
|
13812
14448
|
const target = estimateFirstActionTarget(storage, args.firstAction, explicit.inputSatoshis + noSendChange.inputSatoshis, outputScriptLengths);
|
|
13813
|
-
const fixedOutputs = [...explicit.outputs, ...noSendChange.outputs];
|
|
13814
|
-
const
|
|
14449
|
+
const fixedOutputs = [...new Map([...explicit.outputs, ...noSendChange.outputs].map((output) => [output.outputId, output])).values()];
|
|
14450
|
+
const maxReservedOutputs = storage.actionBatchMaxReservedOutputs;
|
|
14451
|
+
if (maxReservedOutputs >= 0 && fixedOutputs.length > maxReservedOutputs) throw new WERR_INVALID_PARAMETER("firstAction", `no more than ${maxReservedOutputs} persisted inputs`);
|
|
14452
|
+
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);
|
|
13815
14453
|
const batch = newBatch(userId, args.batchId);
|
|
13816
14454
|
await storage.transaction(async (trx) => {
|
|
13817
14455
|
await storage.insertActionBatch(batch, trx);
|
|
13818
|
-
await reserveOutputs(storage, batch, [...fixedOutputs, ...
|
|
14456
|
+
await reserveOutputs(storage, batch, [...fixedOutputs, ...fundingOutputs], trx);
|
|
13819
14457
|
});
|
|
13820
14458
|
let fundingResult;
|
|
13821
14459
|
try {
|
|
13822
|
-
fundingResult = await makeFundingResult(storage, args.firstAction, [...
|
|
14460
|
+
fundingResult = await makeFundingResult(storage, args.firstAction, [...fundingOutputs, ...fixedOutputs]);
|
|
13823
14461
|
} catch (error) {
|
|
13824
14462
|
await storage.transaction(async (trx) => await releaseBatchState(storage, batch, "aborted", trx));
|
|
13825
14463
|
throw error;
|
|
@@ -13833,41 +14471,63 @@ async function beginActionBatch(storage, auth, args) {
|
|
|
13833
14471
|
feeModel: validateStorageFeeModel(storage.feeModel),
|
|
13834
14472
|
commissionSatoshis: storage.commissionSatoshis,
|
|
13835
14473
|
commissionPubKeyHex: storage.commissionPubKeyHex,
|
|
13836
|
-
availableChangeCount: available.length,
|
|
13837
|
-
|
|
14474
|
+
availableChangeCount: available.filter((output) => output.satoshis >= Math.max(1, changeBasket.minimumDesiredUTXOValue)).length,
|
|
14475
|
+
managedChangePolicy: {
|
|
14476
|
+
maxOutputsPerAction: storage.managedChangePolicy.maxOutputsPerAction,
|
|
14477
|
+
migrationInputsPerAction: storage.managedChangePolicy.migrationInputsPerAction
|
|
14478
|
+
},
|
|
14479
|
+
reservedOutputs: fundingResult.outputs.filter((output) => fundingOutputs.some((candidate) => candidate.outputId === output.outputId)),
|
|
13838
14480
|
explicitOutputs: fundingResult.outputs.filter((output) => explicitIds.has(output.outputId)),
|
|
13839
14481
|
inputBeef: fundingResult.beef
|
|
13840
14482
|
};
|
|
13841
14483
|
}
|
|
13842
|
-
function requireLiveBatch(batch) {
|
|
13843
|
-
|
|
14484
|
+
function requireLiveBatch(batch, batchId) {
|
|
14485
|
+
const state = actionBatchErrorState(batch);
|
|
14486
|
+
if (state != null) throw new WERRActionBatchState(state, batchId ?? batch?.batchId);
|
|
14487
|
+
if (batch == null) throw new WERRActionBatchState("missing", batchId);
|
|
13844
14488
|
return batch;
|
|
13845
14489
|
}
|
|
13846
14490
|
async function extendActionBatch(storage, auth, args) {
|
|
13847
14491
|
const userId = verifyId(auth.userId);
|
|
13848
14492
|
await cleanupExpiredActionBatches(storage);
|
|
13849
|
-
const batch = requireLiveBatch(await storage.findActionBatch(userId, args.batchId));
|
|
14493
|
+
const batch = requireLiveBatch(await storage.findActionBatch(userId, args.batchId), args.batchId);
|
|
13850
14494
|
const basket = verifyOne(await storage.findOutputBaskets({ partial: {
|
|
13851
14495
|
userId,
|
|
13852
14496
|
name: "default"
|
|
13853
14497
|
} }));
|
|
13854
14498
|
const alreadyReserved = await storage.findActionBatchOutputIds(batch.actionBatchId);
|
|
13855
|
-
const available = await storage.findAvailableManagedChangeInputs(userId, basket.basketId, false);
|
|
13856
14499
|
if (!Number.isSafeInteger(args.requestedOutputs) || args.requestedOutputs < 0) throw new WERR_INVALID_PARAMETER("requestedOutputs", "non-negative safe integer");
|
|
13857
|
-
|
|
13858
|
-
const
|
|
14500
|
+
if (!Number.isSafeInteger(args.targetSatoshis) || args.targetSatoshis < 0) throw new WERR_INVALID_PARAMETER("targetSatoshis", "non-negative safe integer");
|
|
14501
|
+
const maxReservedOutputs = storage.actionBatchMaxReservedOutputs;
|
|
14502
|
+
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`);
|
|
14503
|
+
const remainingCapacity = maxReservedOutputs < 0 ? Number.MAX_SAFE_INTEGER : maxReservedOutputs - alreadyReserved.length;
|
|
14504
|
+
if (remainingCapacity <= 0 && (args.requestedOutputs > 0 || args.explicitOutpoints.length > 0)) throw new WERR_INVALID_OPERATION(`action batch already holds the maximum of ${maxReservedOutputs} outputs`);
|
|
13859
14505
|
const explicitByOutpoint = await storage.findOutputsByOutpoints(userId, args.explicitOutpoints);
|
|
13860
|
-
const explicit = Object.values(explicitByOutpoint).filter((output) => !alreadyReserved.includes(output.outputId));
|
|
13861
|
-
|
|
14506
|
+
const explicit = [...new Map(Object.values(explicitByOutpoint).filter((output) => !alreadyReserved.includes(output.outputId)).map((output) => [output.outputId, output])).values()];
|
|
14507
|
+
if (explicit.length > remainingCapacity) throw new WERR_INVALID_PARAMETER("explicitOutpoints", `no more than ${remainingCapacity} additional reserved outputs`);
|
|
14508
|
+
const explicitIds = new Set(explicit.map((output) => output.outputId));
|
|
14509
|
+
const availableOutputs = (await storage.findAvailableManagedChangeInputs(userId, basket.basketId, false)).filter((output) => !explicitIds.has(output.outputId));
|
|
14510
|
+
const availableStatuses = await storage.findTransactionStatusesByIds(userId, availableOutputs.map((output) => output.transactionId));
|
|
14511
|
+
const available = availableOutputs.map((output) => ({
|
|
14512
|
+
...output,
|
|
14513
|
+
transactionStatus: availableStatuses.get(output.transactionId) ?? "sending"
|
|
14514
|
+
}));
|
|
14515
|
+
const requestedCount = Math.min(args.requestedOutputs, 64, remainingCapacity - explicit.length);
|
|
14516
|
+
const fundingOutputs = chooseReservationPool(available, Math.max(1, args.targetSatoshis), requestedCount, 0, true, reservationPlanningCosts(storage, basket)).map(reservationOutput);
|
|
14517
|
+
const fundingResult = await makeFundingResult(storage, argsToFundingShape(args.includeSourceTransactions), [...fundingOutputs, ...explicit]);
|
|
13862
14518
|
const expiresAt = new Date(Math.min(batch.hardExpiresAt.getTime(), Date.now() + ACTION_BATCH_LEASE_MS));
|
|
13863
14519
|
await storage.transaction(async (trx) => {
|
|
13864
|
-
const current = requireLiveBatch(await storage.findActionBatchForUpdate(userId, args.batchId, trx));
|
|
13865
|
-
|
|
14520
|
+
const current = requireLiveBatch(await storage.findActionBatchForUpdate(userId, args.batchId, trx), args.batchId);
|
|
14521
|
+
const additions = [...new Map([...fundingOutputs, ...explicit].map((output) => [output.outputId, output])).values()];
|
|
14522
|
+
const currentReserved = new Set(await storage.findActionBatchOutputIds(current.actionBatchId, trx));
|
|
14523
|
+
const newAdditions = additions.filter((output) => !currentReserved.has(output.outputId));
|
|
14524
|
+
if (maxReservedOutputs >= 0 && currentReserved.size + newAdditions.length > maxReservedOutputs) throw new WERR_INVALID_OPERATION(`action batch cannot reserve more than ${maxReservedOutputs} outputs`);
|
|
14525
|
+
await reserveOutputs(storage, current, newAdditions, trx);
|
|
13866
14526
|
await storage.updateActionBatch(current.actionBatchId, { expiresAt }, trx);
|
|
13867
14527
|
});
|
|
13868
14528
|
return {
|
|
13869
14529
|
expiresAt: expiresAt.toISOString(),
|
|
13870
|
-
reservedOutputs: fundingResult.outputs.filter((output) =>
|
|
14530
|
+
reservedOutputs: fundingResult.outputs.filter((output) => fundingOutputs.some((candidate) => candidate.outputId === output.outputId)),
|
|
13871
14531
|
explicitOutputs: fundingResult.outputs.filter((output) => explicit.some((candidate) => candidate.outputId === output.outputId)),
|
|
13872
14532
|
inputBeef: fundingResult.beef
|
|
13873
14533
|
};
|
|
@@ -13903,14 +14563,64 @@ function argsToFundingShape(includeSourceTransactions) {
|
|
|
13903
14563
|
async function renewActionBatch(storage, auth, batchId) {
|
|
13904
14564
|
const userId = verifyId(auth.userId);
|
|
13905
14565
|
return await storage.transaction(async (trx) => {
|
|
13906
|
-
const batch = requireLiveBatch(await storage.findActionBatchForUpdate(userId, batchId, trx));
|
|
14566
|
+
const batch = requireLiveBatch(await storage.findActionBatchForUpdate(userId, batchId, trx), batchId);
|
|
13907
14567
|
const expiresAt = new Date(Math.min(batch.hardExpiresAt.getTime(), Date.now() + ACTION_BATCH_LEASE_MS));
|
|
13908
14568
|
await storage.updateActionBatch(batch.actionBatchId, { expiresAt }, trx);
|
|
13909
14569
|
return { expiresAt: expiresAt.toISOString() };
|
|
13910
14570
|
});
|
|
13911
14571
|
}
|
|
14572
|
+
function validateResumeOutpoints(storage, args) {
|
|
14573
|
+
const unique = new Set(args.outpoints.map((outpoint) => `${outpoint.txid}.${outpoint.vout}`));
|
|
14574
|
+
const maxReservedOutputs = storage.actionBatchMaxReservedOutputs;
|
|
14575
|
+
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`);
|
|
14576
|
+
}
|
|
14577
|
+
/**
|
|
14578
|
+
* Reacquire exactly the persisted inputs owned by one expired client
|
|
14579
|
+
* workspace. This is deliberately explicit: another action may not become a
|
|
14580
|
+
* member merely because it happens to use the same Wallet instance.
|
|
14581
|
+
*/
|
|
14582
|
+
async function resumeActionBatch(storage, auth, args) {
|
|
14583
|
+
const userId = verifyId(auth.userId);
|
|
14584
|
+
validateResumeOutpoints(storage, args);
|
|
14585
|
+
await cleanupExpiredActionBatches(storage);
|
|
14586
|
+
return await storage.transaction(async (trx) => {
|
|
14587
|
+
const batch = await storage.findActionBatchForUpdate(userId, args.batchId, trx);
|
|
14588
|
+
const state = actionBatchErrorState(batch);
|
|
14589
|
+
if (state != null && state !== "expired") throw new WERRActionBatchState(state, args.batchId);
|
|
14590
|
+
if (batch == null) throw new WERRActionBatchState("missing", args.batchId);
|
|
14591
|
+
const expiresAt = new Date(Math.min(batch.hardExpiresAt.getTime(), Date.now() + ACTION_BATCH_LEASE_MS));
|
|
14592
|
+
if (state == null) {
|
|
14593
|
+
const reserved = new Set(await storage.findActionBatchOutputIds(batch.actionBatchId, trx));
|
|
14594
|
+
const current = await storage.findOutputsByOutpointsForUpdate(userId, args.outpoints, trx);
|
|
14595
|
+
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);
|
|
14596
|
+
await storage.updateActionBatch(batch.actionBatchId, { expiresAt }, trx);
|
|
14597
|
+
return { expiresAt: expiresAt.toISOString() };
|
|
14598
|
+
}
|
|
14599
|
+
const stored = await storage.findOutputsByOutpointsForUpdate(userId, args.outpoints, trx);
|
|
14600
|
+
const outputs = args.outpoints.map((outpoint) => stored[`${outpoint.txid}.${outpoint.vout}`]);
|
|
14601
|
+
if (outputs.some((output) => output == null || !output.spendable || output.spentBy != null)) throw new WERRActionBatchState("conflicted", args.batchId);
|
|
14602
|
+
await storage.deleteActionBatchOutputReservations(batch.actionBatchId, trx);
|
|
14603
|
+
const exactOutputs = outputs;
|
|
14604
|
+
if ((await storage.findReservedActionBatchOutputIds(exactOutputs.map((output) => output.outputId), trx)).length > 0) throw new WERRActionBatchState("conflicted", args.batchId);
|
|
14605
|
+
const now = /* @__PURE__ */ new Date();
|
|
14606
|
+
await storage.reserveActionBatchOutputs(exactOutputs.map((output) => ({
|
|
14607
|
+
actionBatchId: batch.actionBatchId,
|
|
14608
|
+
outputId: output.outputId,
|
|
14609
|
+
created_at: now,
|
|
14610
|
+
updated_at: now
|
|
14611
|
+
})), trx);
|
|
14612
|
+
await storage.updateActionBatch(batch.actionBatchId, {
|
|
14613
|
+
status: "active",
|
|
14614
|
+
expiresAt,
|
|
14615
|
+
manifest: void 0,
|
|
14616
|
+
manifestDigest: void 0,
|
|
14617
|
+
uploadDigests: void 0
|
|
14618
|
+
}, trx);
|
|
14619
|
+
return { expiresAt: expiresAt.toISOString() };
|
|
14620
|
+
});
|
|
14621
|
+
}
|
|
13912
14622
|
async function reacquireManifestInputs(storage, userId, batch, validated, trx) {
|
|
13913
|
-
if (batch.hardExpiresAt.getTime() <= Date.now()) throw new
|
|
14623
|
+
if (batch.hardExpiresAt.getTime() <= Date.now()) throw new WERRActionBatchState("hard-expired", batch.batchId);
|
|
13914
14624
|
const stagedTxids = new Set(validated.actions.map(({ action }) => action.txid));
|
|
13915
14625
|
const outpoints = [...new Map(validated.actions.flatMap(({ action }) => action.plan.inputs).filter((input) => !stagedTxids.has(input.sourceTxid)).map((input) => [`${input.sourceTxid}.${input.sourceVout}`, {
|
|
13916
14626
|
txid: input.sourceTxid,
|
|
@@ -14077,7 +14787,7 @@ async function persistAction(...[storage, userId, validated, reservedOutputIds,
|
|
|
14077
14787
|
async function persistManifestAtomically(storage, userId, batch, manifest, validated) {
|
|
14078
14788
|
return await storage.transaction(async (trx) => {
|
|
14079
14789
|
const current = await storage.findActionBatchForUpdate(userId, batch.batchId, trx);
|
|
14080
|
-
if (current == null) throw new
|
|
14790
|
+
if (current == null) throw new WERRActionBatchState("missing", batch.batchId);
|
|
14081
14791
|
if (current.status === "committed") {
|
|
14082
14792
|
if (current.manifestDigest !== manifest.digest) throw new WERR_INVALID_OPERATION("batch committed with another manifest");
|
|
14083
14793
|
return {
|
|
@@ -14085,12 +14795,12 @@ async function persistManifestAtomically(storage, userId, batch, manifest, valid
|
|
|
14085
14795
|
alreadyCommitted: true
|
|
14086
14796
|
};
|
|
14087
14797
|
}
|
|
14088
|
-
if (current.status === "aborted") throw new
|
|
14798
|
+
if (current.status === "aborted") throw new WERRActionBatchState("aborted", batch.batchId);
|
|
14089
14799
|
if (current.manifestDigest != null && current.manifestDigest !== manifest.digest) throw new WERR_INVALID_OPERATION("batch prepared with another manifest");
|
|
14090
14800
|
if (current.status === "expired" || activeExpiry(current)) {
|
|
14091
|
-
if (current.hardExpiresAt.getTime() <= Date.now()) throw new
|
|
14801
|
+
if (current.hardExpiresAt.getTime() <= Date.now()) throw new WERRActionBatchState("hard-expired", batch.batchId);
|
|
14092
14802
|
await reacquireManifestInputs(storage, userId, current, validated, trx);
|
|
14093
|
-
} else requireLiveBatch(current);
|
|
14803
|
+
} else requireLiveBatch(current, batch.batchId);
|
|
14094
14804
|
const reservedOutputIds = new Set(await storage.findActionBatchOutputIds(current.actionBatchId, trx));
|
|
14095
14805
|
const stagedTxids = new Set(validated.actions.map(({ action }) => action.txid));
|
|
14096
14806
|
const inputOutpoints = [...new Map(validated.actions.flatMap(({ action }) => action.plan.inputs).filter((input) => !stagedTxids.has(input.sourceTxid)).map((input) => [`${input.sourceTxid}.${input.sourceVout}`, {
|
|
@@ -14168,14 +14878,14 @@ async function completeCommittedBatch(storage, userId, batch, manifest, alreadyC
|
|
|
14168
14878
|
}
|
|
14169
14879
|
async function commitActionBatchOnce(storage, userId, manifest) {
|
|
14170
14880
|
const batch = await storage.findActionBatch(userId, manifest.batchId);
|
|
14171
|
-
if (batch == null) throw new
|
|
14881
|
+
if (batch == null) throw new WERRActionBatchState("missing", manifest.batchId);
|
|
14172
14882
|
if (batch.status === "committed") {
|
|
14173
14883
|
if (batch.manifestDigest !== manifest.digest) throw new WERR_INVALID_OPERATION("batch committed with another manifest");
|
|
14174
14884
|
return await completeCommittedBatch(storage, userId, batch, manifest, true);
|
|
14175
14885
|
}
|
|
14176
|
-
if (batch.status === "aborted") throw new
|
|
14177
|
-
if (!(batch.status === "expired" || activeExpiry(batch))) requireLiveBatch(batch);
|
|
14178
|
-
else if (batch.hardExpiresAt.getTime() <= Date.now()) throw new
|
|
14886
|
+
if (batch.status === "aborted") throw new WERRActionBatchState("aborted", manifest.batchId);
|
|
14887
|
+
if (!(batch.status === "expired" || activeExpiry(batch))) requireLiveBatch(batch, manifest.batchId);
|
|
14888
|
+
else if (batch.hardExpiresAt.getTime() <= Date.now()) throw new WERRActionBatchState("hard-expired", manifest.batchId);
|
|
14179
14889
|
if (batch.manifestDigest != null && batch.manifestDigest !== manifest.digest) throw new WERR_INVALID_OPERATION("batch prepared with another manifest");
|
|
14180
14890
|
const persisted = await persistManifestAtomically(storage, userId, batch, manifest, await validateManifestActions(storage, batch, manifest));
|
|
14181
14891
|
return await completeCommittedBatch(storage, userId, persisted.batch, manifest, persisted.alreadyCommitted, persisted.share);
|
|
@@ -14258,6 +14968,8 @@ var StorageProvider = class StorageProvider extends StorageReaderWriter {
|
|
|
14258
14968
|
commissionSatoshis;
|
|
14259
14969
|
commissionPubKeyHex;
|
|
14260
14970
|
maxRecursionDepth;
|
|
14971
|
+
actionBatchMaxReservedOutputs;
|
|
14972
|
+
managedChangePolicy;
|
|
14261
14973
|
scriptVerifier;
|
|
14262
14974
|
static defaultOptions() {
|
|
14263
14975
|
return {
|
|
@@ -14266,7 +14978,9 @@ var StorageProvider = class StorageProvider extends StorageReaderWriter {
|
|
|
14266
14978
|
value: 100
|
|
14267
14979
|
},
|
|
14268
14980
|
commissionSatoshis: 0,
|
|
14269
|
-
commissionPubKeyHex: void 0
|
|
14981
|
+
commissionPubKeyHex: void 0,
|
|
14982
|
+
actionBatchMaxReservedOutputs: 256,
|
|
14983
|
+
managedChangePolicy: defaultManagedChangePolicy()
|
|
14270
14984
|
};
|
|
14271
14985
|
}
|
|
14272
14986
|
static createStorageBaseOptions(chain) {
|
|
@@ -14281,6 +14995,10 @@ var StorageProvider = class StorageProvider extends StorageReaderWriter {
|
|
|
14281
14995
|
this.commissionPubKeyHex = options.commissionPubKeyHex;
|
|
14282
14996
|
this.commissionSatoshis = options.commissionSatoshis;
|
|
14283
14997
|
this.maxRecursionDepth = 12;
|
|
14998
|
+
const maxReservedOutputs = options.actionBatchMaxReservedOutputs ?? 256;
|
|
14999
|
+
if (maxReservedOutputs !== -1 && (!Number.isSafeInteger(maxReservedOutputs) || maxReservedOutputs < 1)) throw new WERR_INVALID_PARAMETER("actionBatchMaxReservedOutputs", "a positive safe integer or -1 for unlimited");
|
|
15000
|
+
this.actionBatchMaxReservedOutputs = maxReservedOutputs;
|
|
15001
|
+
this.managedChangePolicy = validateManagedChangePolicy(options.managedChangePolicy);
|
|
14284
15002
|
this.scriptVerifier = options.scriptVerifier;
|
|
14285
15003
|
}
|
|
14286
15004
|
/** Mark a planned set of change inputs spent within the caller's transaction. */
|
|
@@ -14311,12 +15029,16 @@ var StorageProvider = class StorageProvider extends StorageReaderWriter {
|
|
|
14311
15029
|
}
|
|
14312
15030
|
/** Read only the fields needed by the in-memory funding planner. */
|
|
14313
15031
|
async findAvailableManagedChangeInputCandidates(userId, basketId, excludeSending, trx) {
|
|
14314
|
-
|
|
15032
|
+
const outputs = await this.findAvailableManagedChangeInputs(userId, basketId, excludeSending, trx);
|
|
15033
|
+
const findStatuses = this.findTransactionStatusesByIds?.bind(this);
|
|
15034
|
+
const statuses = findStatuses == null ? /* @__PURE__ */ new Map() : await findStatuses(userId, outputs.map((output) => output.transactionId), trx);
|
|
15035
|
+
return outputs.map(({ outputId, transactionId, satoshis, txid, vout }) => ({
|
|
14315
15036
|
outputId,
|
|
14316
15037
|
transactionId,
|
|
14317
15038
|
satoshis,
|
|
14318
15039
|
txid,
|
|
14319
|
-
vout
|
|
15040
|
+
vout,
|
|
15041
|
+
transactionStatus: statuses.get(transactionId)
|
|
14320
15042
|
}));
|
|
14321
15043
|
}
|
|
14322
15044
|
/** Read the current status of a set of source transactions without loading raw transaction bytes. */
|
|
@@ -14404,7 +15126,7 @@ var StorageProvider = class StorageProvider extends StorageReaderWriter {
|
|
|
14404
15126
|
throw new WERR_NOT_IMPLEMENTED();
|
|
14405
15127
|
}
|
|
14406
15128
|
async getCapabilities() {
|
|
14407
|
-
return this.supportsActionBatchPersistence() ? getActionBatchCapabilities() : {};
|
|
15129
|
+
return this.supportsActionBatchPersistence() ? getActionBatchCapabilities(this.actionBatchMaxReservedOutputs, true) : {};
|
|
14408
15130
|
}
|
|
14409
15131
|
supportsActionBatchPersistence() {
|
|
14410
15132
|
return false;
|
|
@@ -14423,6 +15145,9 @@ var StorageProvider = class StorageProvider extends StorageReaderWriter {
|
|
|
14423
15145
|
async renewActionBatch(auth, batchId) {
|
|
14424
15146
|
return await renewActionBatch(this, auth, batchId);
|
|
14425
15147
|
}
|
|
15148
|
+
async resumeActionBatch(auth, args) {
|
|
15149
|
+
return await resumeActionBatch(this, auth, args);
|
|
15150
|
+
}
|
|
14426
15151
|
async prepareActionBatchCommit(auth, manifest) {
|
|
14427
15152
|
return await prepareActionBatchCommit(this, auth, manifest);
|
|
14428
15153
|
}
|
|
@@ -15020,7 +15745,12 @@ var StorageProvider = class StorageProvider extends StorageReaderWriter {
|
|
|
15020
15745
|
for (const output of outputs) {
|
|
15021
15746
|
const outputId = verifyId(output.outputId);
|
|
15022
15747
|
await this.validateOutputScript(output);
|
|
15023
|
-
|
|
15748
|
+
if (output.lockingScript == null) {
|
|
15749
|
+
verdicts.set(outputId, void 0);
|
|
15750
|
+
continue;
|
|
15751
|
+
}
|
|
15752
|
+
const classification = await classifyOutputUtxo(services, output);
|
|
15753
|
+
verdicts.set(outputId, classification.verdict === "unknown" ? void 0 : classification.verdict === "unspent");
|
|
15024
15754
|
}
|
|
15025
15755
|
}
|
|
15026
15756
|
return verdicts;
|
|
@@ -15182,16 +15912,12 @@ var StorageProvider = class StorageProvider extends StorageReaderWriter {
|
|
|
15182
15912
|
} });
|
|
15183
15913
|
for (const o of outputs) {
|
|
15184
15914
|
if (!o.spendable) continue;
|
|
15185
|
-
|
|
15915
|
+
await this.validateOutputScript(o);
|
|
15916
|
+
if (!requireConclusiveUtxo(await classifyOutputUtxo(services, o))) invalidSpendableOutputs.push(o);
|
|
15186
15917
|
}
|
|
15187
15918
|
}
|
|
15188
15919
|
return { invalidSpendableOutputs };
|
|
15189
15920
|
}
|
|
15190
|
-
async checkOutputIsUtxo(o, services) {
|
|
15191
|
-
if (o.lockingScript == null || o.lockingScript.length === 0) return false;
|
|
15192
|
-
const hash = services.hashOutputScript(asString(o.lockingScript));
|
|
15193
|
-
return (await services.getUtxoStatus(hash, void 0, `${o.txid ?? ""}.${o.vout ?? ""}`)).isUtxo === true;
|
|
15194
|
-
}
|
|
15195
15921
|
async updateProvenTxReqDynamics(id, update, trx) {
|
|
15196
15922
|
const partial = {};
|
|
15197
15923
|
if (update.updated_at != null) partial.updated_at = update.updated_at;
|
|
@@ -15301,6 +16027,7 @@ const getLabelToSpecOp = () => {
|
|
|
15301
16027
|
//#region ../src/storage/methods/listActionsIdb.ts
|
|
15302
16028
|
async function enrichIdbActionLabels(storage, tx, action, timeFilterRequested) {
|
|
15303
16029
|
action.labels = (await storage.getLabelsForTransactionId(tx.transactionId)).map((l) => l.label);
|
|
16030
|
+
if (tx.reference != null && tx.reference !== "") action.labels = applyBrc153ReferenceLabel(action.labels, tx.reference);
|
|
15304
16031
|
if (timeFilterRequested) {
|
|
15305
16032
|
const ts = tx.created_at ? new Date(tx.created_at).getTime() : NaN;
|
|
15306
16033
|
if (!Number.isNaN(ts)) {
|
|
@@ -15419,20 +16146,129 @@ async function listActionsIdb(storage, auth, vargs) {
|
|
|
15419
16146
|
return r;
|
|
15420
16147
|
}
|
|
15421
16148
|
//#endregion
|
|
15422
|
-
//#region ../src/storage/methods/
|
|
15423
|
-
const
|
|
15424
|
-
|
|
15425
|
-
|
|
15426
|
-
|
|
15427
|
-
|
|
15428
|
-
|
|
15429
|
-
|
|
16149
|
+
//#region ../src/storage/methods/reviewUtxoOutputs.ts
|
|
16150
|
+
const MAX_AUDIT_PROVIDERS = 8;
|
|
16151
|
+
const MAX_PROVIDER_NAME_LENGTH = 128;
|
|
16152
|
+
const UTXO_REVIEW_PROVIDER_TIMEOUT_MSECS = 5e3;
|
|
16153
|
+
function diagnosticsFor(classifications, releasedOutputIds) {
|
|
16154
|
+
const allProviders = [...new Set(classifications.map((result) => result.status.provider.slice(0, MAX_PROVIDER_NAME_LENGTH)))];
|
|
16155
|
+
const providers = allProviders.slice(0, MAX_AUDIT_PROVIDERS);
|
|
16156
|
+
const confirmedSpent = classifications.filter((result) => result.status.verdict === "spent");
|
|
16157
|
+
const released = confirmedSpent.filter((result) => releasedOutputIds.has(result.output.outputId));
|
|
16158
|
+
return {
|
|
16159
|
+
checked: classifications.length,
|
|
16160
|
+
confirmedUnspent: classifications.filter((result) => result.status.verdict === "unspent").length,
|
|
16161
|
+
confirmedSpent: confirmedSpent.length,
|
|
16162
|
+
unknown: classifications.filter((result) => result.status.verdict === "unknown").length,
|
|
16163
|
+
confirmedSpentSatoshis: confirmedSpent.reduce((sum, result) => sum + result.output.satoshis, 0),
|
|
16164
|
+
released: released.length,
|
|
16165
|
+
releasedSatoshis: released.reduce((sum, result) => sum + result.output.satoshis, 0),
|
|
16166
|
+
providers,
|
|
16167
|
+
providerCount: allProviders.length,
|
|
16168
|
+
providersTruncated: allProviders.length > providers.length
|
|
16169
|
+
};
|
|
16170
|
+
}
|
|
16171
|
+
function auditDetails(diagnostics, userId, releaseMode, reason) {
|
|
16172
|
+
return JSON.stringify({
|
|
16173
|
+
operation: "specOpInvalidChange",
|
|
16174
|
+
reason,
|
|
16175
|
+
releaseMode,
|
|
16176
|
+
userId,
|
|
16177
|
+
...diagnostics
|
|
16178
|
+
});
|
|
16179
|
+
}
|
|
16180
|
+
async function classifyCandidates(storage, outputs) {
|
|
16181
|
+
const services = storage.getServices();
|
|
16182
|
+
return await mapWithConcurrency(outputs.filter((output) => output.basketId != null), 4, async (output) => {
|
|
16183
|
+
try {
|
|
16184
|
+
await storage.validateOutputScript(output);
|
|
16185
|
+
} catch (error) {
|
|
16186
|
+
return {
|
|
16187
|
+
output,
|
|
16188
|
+
status: {
|
|
16189
|
+
verdict: "unknown",
|
|
16190
|
+
provider: "<script-validation-error>",
|
|
16191
|
+
error
|
|
16192
|
+
}
|
|
16193
|
+
};
|
|
16194
|
+
}
|
|
16195
|
+
let timeout;
|
|
16196
|
+
const providerCall = classifyOutputUtxo(services, output);
|
|
16197
|
+
const timedClassification = new Promise((resolve) => {
|
|
16198
|
+
timeout = setTimeout(() => resolve({
|
|
16199
|
+
verdict: "unknown",
|
|
16200
|
+
provider: "<review-timeout>",
|
|
16201
|
+
error: /* @__PURE__ */ new Error(`UTXO classification exceeded ${UTXO_REVIEW_PROVIDER_TIMEOUT_MSECS}ms`)
|
|
16202
|
+
}), UTXO_REVIEW_PROVIDER_TIMEOUT_MSECS);
|
|
16203
|
+
});
|
|
16204
|
+
try {
|
|
16205
|
+
return {
|
|
16206
|
+
output,
|
|
16207
|
+
status: await Promise.race([providerCall, timedClassification])
|
|
16208
|
+
};
|
|
16209
|
+
} finally {
|
|
16210
|
+
if (timeout != null) clearTimeout(timeout);
|
|
16211
|
+
providerCall.catch(() => void 0);
|
|
16212
|
+
}
|
|
16213
|
+
});
|
|
16214
|
+
}
|
|
16215
|
+
async function releaseConfirmedSpent(storage, auth, classifications, releaseMode) {
|
|
16216
|
+
const confirmedSpent = classifications.filter((result) => result.status.verdict === "spent");
|
|
16217
|
+
const releasedOutputIds = /* @__PURE__ */ new Set();
|
|
16218
|
+
await storage.transaction(async (trx) => {
|
|
16219
|
+
for (const { output } of confirmedSpent) {
|
|
16220
|
+
const current = await storage.findOutputById(output.outputId, trx, true);
|
|
16221
|
+
if (current == null || current.userId !== auth.userId) throw new WERR_INVALID_PARAMETER("outputId", `owned by user ${auth.userId}`);
|
|
16222
|
+
if (current.spendable !== true || current.spentBy != null) throw new WERR_INVALID_OPERATION(`Output ${output.outputId} changed state during UTXO review; no outputs were changed.`);
|
|
16223
|
+
if (await storage.updateOutput(verifyId(output.outputId), { spendable: false }, trx) !== 1) throw new WERR_INVALID_PARAMETER("outputId", `updated exactly once: ${output.outputId}`);
|
|
16224
|
+
releasedOutputIds.add(output.outputId);
|
|
16225
|
+
}
|
|
16226
|
+
const now = /* @__PURE__ */ new Date();
|
|
16227
|
+
const diagnostics = diagnosticsFor(classifications, releasedOutputIds);
|
|
16228
|
+
await storage.insertMonitorEvent({
|
|
16229
|
+
created_at: now,
|
|
16230
|
+
updated_at: now,
|
|
16231
|
+
id: 0,
|
|
16232
|
+
event: releaseMode === "atomic" ? "InvalidChangeRelease" : "InvalidChangeConclusiveRelease",
|
|
16233
|
+
details: auditDetails(diagnostics, auth.userId, releaseMode, "provider-confirmed-spent")
|
|
16234
|
+
}, trx);
|
|
16235
|
+
});
|
|
16236
|
+
return releasedOutputIds;
|
|
16237
|
+
}
|
|
16238
|
+
/**
|
|
16239
|
+
* Classify a bounded set of wallet outputs without treating provider failure as
|
|
16240
|
+
* proof that an output was spent. Read-only reviews always return their
|
|
16241
|
+
* conclusive and unknown partitions. Atomic release requires every verdict to
|
|
16242
|
+
* be conclusive; the operator-only conclusive mode releases the positively
|
|
16243
|
+
* spent subset while retaining and reporting unknowns.
|
|
16244
|
+
*/
|
|
16245
|
+
async function reviewUtxoOutputs(storage, auth, outputs, releaseMode = "none") {
|
|
16246
|
+
const classifications = await classifyCandidates(storage, outputs);
|
|
16247
|
+
const confirmedSpentOutputs = classifications.filter((result) => result.status.verdict === "spent").map((result) => result.output);
|
|
16248
|
+
const unknownOutputs = classifications.filter((result) => result.status.verdict === "unknown").map((result) => result.output);
|
|
16249
|
+
if (releaseMode === "atomic" && unknownOutputs.length > 0) {
|
|
16250
|
+
const diagnostics = diagnosticsFor(classifications, /* @__PURE__ */ new Set());
|
|
16251
|
+
const now = /* @__PURE__ */ new Date();
|
|
16252
|
+
await storage.insertMonitorEvent({
|
|
16253
|
+
created_at: now,
|
|
16254
|
+
updated_at: now,
|
|
16255
|
+
id: 0,
|
|
16256
|
+
event: "InvalidChangeReleaseBlocked",
|
|
16257
|
+
details: auditDetails(diagnostics, auth.userId, releaseMode, "inconclusive-provider-result")
|
|
15430
16258
|
});
|
|
15431
|
-
|
|
15432
|
-
if (active.length >= maxConcurrency) await Promise.race(active);
|
|
16259
|
+
throw new WERR_UTXO_REVIEW_INCONCLUSIVE(diagnostics.checked, diagnostics.confirmedSpent, diagnostics.unknown);
|
|
15433
16260
|
}
|
|
15434
|
-
await
|
|
16261
|
+
const releasedOutputIds = releaseMode === "none" ? /* @__PURE__ */ new Set() : await releaseConfirmedSpent(storage, auth, classifications, releaseMode);
|
|
16262
|
+
for (const output of confirmedSpentOutputs) if (releasedOutputIds.has(output.outputId)) output.spendable = false;
|
|
16263
|
+
return {
|
|
16264
|
+
classifications,
|
|
16265
|
+
confirmedSpentOutputs,
|
|
16266
|
+
unknownOutputs,
|
|
16267
|
+
diagnostics: diagnosticsFor(classifications, releasedOutputIds)
|
|
16268
|
+
};
|
|
15435
16269
|
}
|
|
16270
|
+
//#endregion
|
|
16271
|
+
//#region ../src/storage/methods/ListOutputsSpecOp.ts
|
|
15436
16272
|
const getBasketToSpecOp = () => {
|
|
15437
16273
|
return {
|
|
15438
16274
|
[specOpWalletBalance]: {
|
|
@@ -15463,23 +16299,7 @@ const getBasketToSpecOp = () => {
|
|
|
15463
16299
|
includeSpent: false,
|
|
15464
16300
|
tagsToIntercept: ["release", "all"],
|
|
15465
16301
|
filterOutputs: async (s, auth, vargs, specOpTags, outputs) => {
|
|
15466
|
-
|
|
15467
|
-
const invalidOutputIds = /* @__PURE__ */ new Set();
|
|
15468
|
-
const services = s.getServices();
|
|
15469
|
-
await runWithConcurrency(outputs, INVALID_CHANGE_MAX_CONCURRENCY, async (o) => {
|
|
15470
|
-
if (!o.basketId) return;
|
|
15471
|
-
await s.validateOutputScript(o);
|
|
15472
|
-
let ok = false;
|
|
15473
|
-
if (o.lockingScript != null && o.lockingScript.length > 0) ok = await services.isUtxo(o);
|
|
15474
|
-
else ok = void 0;
|
|
15475
|
-
if (ok === false) invalidOutputIds.add(o.outputId);
|
|
15476
|
-
});
|
|
15477
|
-
const filteredOutputs = outputs.filter((o) => invalidOutputIds.has(o.outputId));
|
|
15478
|
-
if (specOpTags.includes("release")) await runWithConcurrency(filteredOutputs, INVALID_CHANGE_MAX_CONCURRENCY, async (o) => {
|
|
15479
|
-
await s.updateOutput(o.outputId, { spendable: false });
|
|
15480
|
-
o.spendable = false;
|
|
15481
|
-
});
|
|
15482
|
-
return filteredOutputs;
|
|
16302
|
+
return (await reviewUtxoOutputs(s, auth, outputs, specOpTags.includes("release") ? "atomic" : "none")).confirmedSpentOutputs;
|
|
15483
16303
|
}
|
|
15484
16304
|
},
|
|
15485
16305
|
[specOpSetWalletChangeParams]: {
|
|
@@ -15632,7 +16452,7 @@ async function loadIdbOutputs(query) {
|
|
|
15632
16452
|
"nosend",
|
|
15633
16453
|
"sending"
|
|
15634
16454
|
],
|
|
15635
|
-
noScript: true,
|
|
16455
|
+
noScript: specOp?.includeOutputScripts !== true,
|
|
15636
16456
|
orderDescending
|
|
15637
16457
|
};
|
|
15638
16458
|
const pageManagedChange = specOp?.managedChangeOnly === true && specOp.ignoreLimit !== true;
|
|
@@ -15714,7 +16534,10 @@ async function listOutputsIdb(storage, auth, vargs, _originator) {
|
|
|
15714
16534
|
});
|
|
15715
16535
|
result.totalOutputs = totalOutputs;
|
|
15716
16536
|
if (specOp != null) {
|
|
15717
|
-
if (specOp.filterOutputs != null)
|
|
16537
|
+
if (specOp.filterOutputs != null) {
|
|
16538
|
+
outputs = await specOp.filterOutputs(storage, auth, vargs, specOpTags, outputs);
|
|
16539
|
+
result.totalOutputs = outputs.length;
|
|
16540
|
+
}
|
|
15718
16541
|
if (specOp.resultFromOutputs != null) return specOp.resultFromOutputs(storage, auth, vargs, specOpTags, outputs);
|
|
15719
16542
|
}
|
|
15720
16543
|
await hydrateIdbOutputResult(storage, outputs, vargs, result);
|
|
@@ -15845,6 +16668,7 @@ async function scanCursor(cursor, since, offset, limit, matches, accept) {
|
|
|
15845
16668
|
var StorageIdb = class extends StorageProvider {
|
|
15846
16669
|
dbName;
|
|
15847
16670
|
db;
|
|
16671
|
+
managedChangeDefaultsMigrated = false;
|
|
15848
16672
|
constructor(options) {
|
|
15849
16673
|
super(options);
|
|
15850
16674
|
this.dbName = `wallet-toolbox-${this.chain}net`;
|
|
@@ -15882,6 +16706,7 @@ var StorageIdb = class extends StorageProvider {
|
|
|
15882
16706
|
async verifyDB(storageName, storageIdentityKey) {
|
|
15883
16707
|
if (this.db != null) return this.db;
|
|
15884
16708
|
this.db = await this.initDB(storageName, storageIdentityKey);
|
|
16709
|
+
await this.migrateManagedChangeDefaults(this.db);
|
|
15885
16710
|
this._settings = (await this.db.getAll("settings"))[0];
|
|
15886
16711
|
this.whenLastAccess = /* @__PURE__ */ new Date();
|
|
15887
16712
|
return this.db;
|
|
@@ -15915,7 +16740,7 @@ var StorageIdb = class extends StorageProvider {
|
|
|
15915
16740
|
async initDB(storageName, storageIdentityKey) {
|
|
15916
16741
|
const chain = this.chain;
|
|
15917
16742
|
const maxOutputScript = 1024;
|
|
15918
|
-
return await (0, idb.openDB)(this.dbName,
|
|
16743
|
+
return await (0, idb.openDB)(this.dbName, 4, { upgrade(db, _oldVersion, _newVersion, transaction) {
|
|
15919
16744
|
upgradeAllStoresV1(db);
|
|
15920
16745
|
upgradeActionBatchStoresV2(db);
|
|
15921
16746
|
const outputs = transaction.objectStore("outputs");
|
|
@@ -15941,6 +16766,22 @@ var StorageIdb = class extends StorageProvider {
|
|
|
15941
16766
|
}
|
|
15942
16767
|
} });
|
|
15943
16768
|
}
|
|
16769
|
+
async migrateManagedChangeDefaults(db) {
|
|
16770
|
+
if (this.managedChangeDefaultsMigrated) return;
|
|
16771
|
+
const trx = db.transaction("output_baskets", "readwrite");
|
|
16772
|
+
let cursor = await trx.store.openCursor();
|
|
16773
|
+
while (cursor != null) {
|
|
16774
|
+
const basket = cursor.value;
|
|
16775
|
+
if (isLegacyManagedChangeBasketDefault(basket)) await cursor.update({
|
|
16776
|
+
...basket,
|
|
16777
|
+
minimumDesiredUTXOValue: DEFAULT_MANAGED_CHANGE_MINIMUM_SATOSHIS,
|
|
16778
|
+
updated_at: /* @__PURE__ */ new Date()
|
|
16779
|
+
});
|
|
16780
|
+
cursor = await cursor.continue();
|
|
16781
|
+
}
|
|
16782
|
+
await trx.done;
|
|
16783
|
+
this.managedChangeDefaultsMigrated = true;
|
|
16784
|
+
}
|
|
15944
16785
|
async reviewStatus(args) {
|
|
15945
16786
|
return await reviewStatusIdb(this, args);
|
|
15946
16787
|
}
|
|
@@ -16695,6 +17536,7 @@ var StorageIdb = class extends StorageProvider {
|
|
|
16695
17536
|
if (this.db != null) this.db.close();
|
|
16696
17537
|
this.db = void 0;
|
|
16697
17538
|
this._settings = void 0;
|
|
17539
|
+
this.managedChangeDefaultsMigrated = false;
|
|
16698
17540
|
}
|
|
16699
17541
|
allStores = [
|
|
16700
17542
|
"action_batches",
|
|
@@ -17867,6 +18709,9 @@ var StorageClientBase = class {
|
|
|
17867
18709
|
async renewActionBatch(auth, batchId) {
|
|
17868
18710
|
return await this.rpcCall("renewActionBatch", [auth, batchId]);
|
|
17869
18711
|
}
|
|
18712
|
+
async resumeActionBatch(auth, args) {
|
|
18713
|
+
return await this.rpcCall("resumeActionBatch", [auth, args]);
|
|
18714
|
+
}
|
|
17870
18715
|
async prepareActionBatchCommit(auth, manifest) {
|
|
17871
18716
|
return await this.rpcCall("prepareActionBatchCommit", [auth, manifest]);
|
|
17872
18717
|
}
|
|
@@ -18671,7 +19516,7 @@ async function restoreBRC38(storage, data) {
|
|
|
18671
19516
|
await storage.transaction(async (trx) => {
|
|
18672
19517
|
await storage.insertUser({ ...data.user }, trx);
|
|
18673
19518
|
for (const row of data.provenTxs) await storage.insertProvenTx({ ...row }, trx);
|
|
18674
|
-
for (const row of data.outputBaskets) await storage.insertOutputBasket({ ...row }, trx);
|
|
19519
|
+
for (const row of data.outputBaskets) await storage.insertOutputBasket(upgradeLegacyManagedChangeBasketDefault({ ...row }), trx);
|
|
18675
19520
|
for (const row of data.outputTags) await storage.insertOutputTag({ ...row }, trx);
|
|
18676
19521
|
for (const row of data.txLabels) await storage.insertTxLabel({ ...row }, trx);
|
|
18677
19522
|
for (const row of data.transactions) await storage.insertTransaction({ ...row }, trx);
|
|
@@ -21730,13 +22575,37 @@ function configuredChaintracksClient(chain, serviceUrl) {
|
|
|
21730
22575
|
return new ChaintracksServiceClient(chain, serviceUrl);
|
|
21731
22576
|
}
|
|
21732
22577
|
/**
|
|
22578
|
+
* True when running under a browser-style runtime — a real browser tab, or
|
|
22579
|
+
* the embedded webview a desktop wallet shell hosts its wallet logic in
|
|
22580
|
+
* (Metanet Client / User Wallet are Tauri WKWebViews) — where `fetch` is
|
|
22581
|
+
* subject to CORS enforcement that Node-style runtimes do not apply.
|
|
22582
|
+
*/
|
|
22583
|
+
function isBrowserRuntime() {
|
|
22584
|
+
return typeof window !== "undefined" && window.document !== void 0;
|
|
22585
|
+
}
|
|
22586
|
+
/**
|
|
21733
22587
|
* Returns the credential-free default ChainTracks client for a supported
|
|
21734
22588
|
* public network, or an operator-configured client for stn/tstn.
|
|
22589
|
+
*
|
|
22590
|
+
* BROWSER RUNTIMES get the legacy CORS-enabled Chaintracks service for
|
|
22591
|
+
* main/test. The Go Chaintracks deployments (`arcade-v2-*.bsvblockchain.tech`)
|
|
22592
|
+
* currently serve no `Access-Control-Allow-Origin` header and answer OPTIONS
|
|
22593
|
+
* preflights with 404 (verified live 2026-08-11), so every fetch from a
|
|
22594
|
+
* browser-hosted wallet is CORS-blocked (WebKit surfaces it as
|
|
22595
|
+
* `TypeError: Load failed`) and the wallet loses `getHeight`, headers and
|
|
22596
|
+
* merkle-root validation wholesale. The repository service contract
|
|
22597
|
+
* (AGENTS.md: "browser, mobile, and unknown-domain clients must not be
|
|
22598
|
+
* silently blocked by CORS") requires a default that browsers can actually
|
|
22599
|
+
* reach. Once the Go deployments serve CORS (and a browser-run conformance
|
|
22600
|
+
* check proves it), this branch can be removed and browsers can share the v2
|
|
22601
|
+
* default. Node runtimes are unchanged.
|
|
21735
22602
|
*/
|
|
21736
22603
|
function createDefaultChaintracksClient(chain) {
|
|
21737
22604
|
switch (chain) {
|
|
21738
22605
|
case "main":
|
|
21739
22606
|
case "test":
|
|
22607
|
+
if (isBrowserRuntime()) return new ChaintracksServiceClient(chain, `https://${chain}net-chaintracks.babbage.systems`);
|
|
22608
|
+
return new GoChaintracksServiceClient(chain, arcadeDefaultUrl(chain), { apiPrefix: "/chaintracks/v2" });
|
|
21740
22609
|
case "ttn": return new GoChaintracksServiceClient(chain, arcadeDefaultUrl(chain), { apiPrefix: "/chaintracks/v2" });
|
|
21741
22610
|
case "stn": return configuredChaintracksClient(chain, stnChaintracksUrl());
|
|
21742
22611
|
case "tstn": return configuredChaintracksClient(chain, tstnChaintracksUrl());
|
|
@@ -21775,7 +22644,7 @@ function createDefaultWalletServicesOptions(...[chain, arcCallbackUrl, arcCallba
|
|
|
21775
22644
|
exchangeratesapiKey: void 0,
|
|
21776
22645
|
chaintracksFiatExchangeRatesUrl,
|
|
21777
22646
|
chaintracks,
|
|
21778
|
-
arcUrl: arcDefaultUrl(chain),
|
|
22647
|
+
arcUrl: chain === "ttn" ? "" : arcDefaultUrl(chain),
|
|
21779
22648
|
arcConfig: {
|
|
21780
22649
|
apiKey: taalArcApiKey ?? void 0,
|
|
21781
22650
|
deploymentId,
|
|
@@ -21791,7 +22660,7 @@ function createDefaultWalletServicesOptions(...[chain, arcCallbackUrl, arcCallba
|
|
|
21791
22660
|
},
|
|
21792
22661
|
bitailsApiKey
|
|
21793
22662
|
};
|
|
21794
|
-
const resolvedArcadeUrl = arcadeUrl;
|
|
22663
|
+
const resolvedArcadeUrl = arcadeUrl ?? (chain === "ttn" ? arcadeDefaultUrl(chain) : void 0);
|
|
21795
22664
|
if (resolvedArcadeUrl != null && resolvedArcadeUrl !== "") {
|
|
21796
22665
|
o.arcadeUrl = resolvedArcadeUrl;
|
|
21797
22666
|
o.arcadeConfig = {
|
|
@@ -21821,7 +22690,7 @@ function arcDefaultUrl(chain) {
|
|
|
21821
22690
|
case "main": return "https://arc.taal.com";
|
|
21822
22691
|
case "test": return "https://arc-test.taal.com";
|
|
21823
22692
|
case "stn": return stnArcadeUrl() ?? "";
|
|
21824
|
-
case "ttn": return "
|
|
22693
|
+
case "ttn": return "";
|
|
21825
22694
|
case "tstn": return tstnArcadeUrl() ?? "";
|
|
21826
22695
|
case "mock": return "";
|
|
21827
22696
|
}
|
|
@@ -22193,6 +23062,40 @@ var ARC = class {
|
|
|
22193
23062
|
}
|
|
22194
23063
|
};
|
|
22195
23064
|
//#endregion
|
|
23065
|
+
//#region ../src/services/providers/arcadeStatus.ts
|
|
23066
|
+
/**
|
|
23067
|
+
* Canonical classification shared by Arcade polling and SSE. Keeping this in
|
|
23068
|
+
* one place prevents a provider from calling a rejection "unknown" while the
|
|
23069
|
+
* monitor's event path calls the same status terminal.
|
|
23070
|
+
*/
|
|
23071
|
+
function classifyArcadeRejection(event) {
|
|
23072
|
+
const extraInfo = event.extraInfo?.trim() ?? "";
|
|
23073
|
+
const detail = extraInfo.toLowerCase();
|
|
23074
|
+
if (event.status === 476 || detail.startsWith("parent rejected") || detail.includes("not final") || detail.includes("non-final")) return {
|
|
23075
|
+
terminal: false,
|
|
23076
|
+
inputConflict: false,
|
|
23077
|
+
reqStatus: "invalid",
|
|
23078
|
+
reason: event.status === 476 ? "transaction is not final" : "Arcade reported a retryable parent/locktime condition"
|
|
23079
|
+
};
|
|
23080
|
+
const orphanConflict = isArcDoubleSpendTxStatus(event.txStatus);
|
|
23081
|
+
const inputConflict = orphanConflict || event.status === 462 || event.status === 466 || /(?:utxo|input).*(?:spent|missing|conflict)|missing[- ]inputs?|already spent/.test(detail);
|
|
23082
|
+
const classifiedValidatorFailure = event.status != null && event.status >= 460 && event.status <= 475;
|
|
23083
|
+
const explicitInvalidStatus = isArcInvalidTxStatus(event.txStatus) && event.txStatus !== "REJECTED";
|
|
23084
|
+
const terminal = inputConflict || classifiedValidatorFailure || explicitInvalidStatus || extraInfo !== "";
|
|
23085
|
+
let reason = "Arcade supplied no durable rejection evidence";
|
|
23086
|
+
if (orphanConflict) reason = `Arcade reported ${event.txStatus}`;
|
|
23087
|
+
else if (inputConflict) reason = "Arcade supplied confirmed missing-input/conflict evidence";
|
|
23088
|
+
else if (classifiedValidatorFailure) reason = `Arcade supplied terminal validator code ${String(event.status)}`;
|
|
23089
|
+
else if (explicitInvalidStatus) reason = `Arcade reported terminal ${event.txStatus}`;
|
|
23090
|
+
else if (extraInfo !== "") reason = "Arcade supplied a terminal validator rejection reason";
|
|
23091
|
+
return {
|
|
23092
|
+
terminal,
|
|
23093
|
+
inputConflict,
|
|
23094
|
+
reqStatus: inputConflict ? "doubleSpend" : "invalid",
|
|
23095
|
+
reason
|
|
23096
|
+
};
|
|
23097
|
+
}
|
|
23098
|
+
//#endregion
|
|
22196
23099
|
//#region ../src/services/providers/Arcade.ts
|
|
22197
23100
|
function defaultDeploymentId() {
|
|
22198
23101
|
return `ts-sdk-${_bsv_sdk.Utils.toHex((0, _bsv_sdk.Random)(16))}`;
|
|
@@ -22462,6 +23365,73 @@ var Arcade = class {
|
|
|
22462
23365
|
return (await this.httpClient.request(`${this.URL}/tx/${txid}`, requestOptions)).data;
|
|
22463
23366
|
}
|
|
22464
23367
|
/**
|
|
23368
|
+
* Adapt Arcade's lifecycle endpoint to the shared transaction-status
|
|
23369
|
+
* provider contract. This lets monitor reconciliation remain operational
|
|
23370
|
+
* when an explorer such as WhatsOnChain is absent. Only network-observed
|
|
23371
|
+
* states count as known; RECEIVED/PENDING_RETRY and terminal rejection
|
|
23372
|
+
* states remain unknown, while MINED/IMMUTABLE are authoritative mined
|
|
23373
|
+
* observations whose proof is validated separately.
|
|
23374
|
+
*/
|
|
23375
|
+
async getStatusForTxids(txids) {
|
|
23376
|
+
const r = {
|
|
23377
|
+
name: this.name,
|
|
23378
|
+
status: "success",
|
|
23379
|
+
results: []
|
|
23380
|
+
};
|
|
23381
|
+
let firstError;
|
|
23382
|
+
r.results = await Promise.all(txids.map(async (txid) => {
|
|
23383
|
+
try {
|
|
23384
|
+
const response = await this.httpClient.request(`${this.URL}/tx/${txid}`, {
|
|
23385
|
+
method: "GET",
|
|
23386
|
+
headers: this.requestHeaders()
|
|
23387
|
+
});
|
|
23388
|
+
if (Number(response.status) === 404) return {
|
|
23389
|
+
txid,
|
|
23390
|
+
status: "unknown",
|
|
23391
|
+
depth: void 0
|
|
23392
|
+
};
|
|
23393
|
+
if (!response.ok || Number(response.status) !== 200) throw new WERR_INVALID_OPERATION(`Arcade transaction status response ${String(response.status)}`);
|
|
23394
|
+
const status = response.data.txStatus;
|
|
23395
|
+
if (status === "MINED" || status === "IMMUTABLE") return {
|
|
23396
|
+
txid,
|
|
23397
|
+
status: "mined",
|
|
23398
|
+
depth: 1
|
|
23399
|
+
};
|
|
23400
|
+
if (status === "ACCEPTED_BY_NETWORK" || status === "SEEN_ON_NETWORK" || status === "SEEN_MULTIPLE_NODES") return {
|
|
23401
|
+
txid,
|
|
23402
|
+
status: "known",
|
|
23403
|
+
depth: 0
|
|
23404
|
+
};
|
|
23405
|
+
const classification = classifyArcadeRejection(response.data);
|
|
23406
|
+
return {
|
|
23407
|
+
txid,
|
|
23408
|
+
status: "unknown",
|
|
23409
|
+
depth: void 0,
|
|
23410
|
+
providerStatus: status,
|
|
23411
|
+
...classification.terminal ? {
|
|
23412
|
+
terminal: true,
|
|
23413
|
+
inputConflict: classification.inputConflict,
|
|
23414
|
+
statusCode: response.data.status,
|
|
23415
|
+
description: (response.data.extraInfo || classification.reason).slice(0, 512),
|
|
23416
|
+
competingTxs: (response.data.competingTxs ?? []).slice(0, 24)
|
|
23417
|
+
} : {}
|
|
23418
|
+
};
|
|
23419
|
+
} catch (error_) {
|
|
23420
|
+
firstError ??= WalletError.fromUnknown(error_);
|
|
23421
|
+
return {
|
|
23422
|
+
txid,
|
|
23423
|
+
status: "unknown",
|
|
23424
|
+
depth: void 0
|
|
23425
|
+
};
|
|
23426
|
+
}
|
|
23427
|
+
}));
|
|
23428
|
+
if (firstError != null) {
|
|
23429
|
+
r.status = "error";
|
|
23430
|
+
r.error = firstError;
|
|
23431
|
+
}
|
|
23432
|
+
return r;
|
|
23433
|
+
}
|
|
23434
|
+
/**
|
|
22465
23435
|
* `getMerklePath` provider: obtain a BUMP merkle proof for a mined transaction from Arcade.
|
|
22466
23436
|
*
|
|
22467
23437
|
* Arcade only has a proof for transactions it tracked (i.e. broadcast through it) that have
|
|
@@ -23252,6 +24222,10 @@ var Services = class Services {
|
|
|
23252
24222
|
name: "WhatsOnChain",
|
|
23253
24223
|
service: this.whatsonchain.getStatusForTxids.bind(this.whatsonchain)
|
|
23254
24224
|
});
|
|
24225
|
+
if (this.arcade != null) this.getStatusForTxidsServices.add({
|
|
24226
|
+
name: "Arcade",
|
|
24227
|
+
service: this.arcade.getStatusForTxids.bind(this.arcade)
|
|
24228
|
+
});
|
|
23255
24229
|
this.getScriptHashHistoryServices = new ServiceCollection("getScriptHashHistory");
|
|
23256
24230
|
if (hasWhatsOnChain) this.getScriptHashHistoryServices.add({
|
|
23257
24231
|
name: "WhatsOnChain",
|
|
@@ -23268,7 +24242,7 @@ var Services = class Services {
|
|
|
23268
24242
|
name: "GorillaPoolArcBeef",
|
|
23269
24243
|
service: this.arcGorillaPool.postBeef.bind(this.arcGorillaPool)
|
|
23270
24244
|
});
|
|
23271
|
-
this.postBeefServices.add({
|
|
24245
|
+
if (this.options.arcUrl != null && this.options.arcUrl !== "") this.postBeefServices.add({
|
|
23272
24246
|
name: "TaalArcBeef",
|
|
23273
24247
|
service: this.arcTaal.postBeef.bind(this.arcTaal)
|
|
23274
24248
|
});
|
|
@@ -23354,30 +24328,65 @@ var Services = class Services {
|
|
|
23354
24328
|
async getStatusForTxids(txids, useNext) {
|
|
23355
24329
|
const services = this.getStatusForTxidsServices;
|
|
23356
24330
|
if (useNext === true) services.next();
|
|
23357
|
-
let
|
|
24331
|
+
let fallback = {
|
|
23358
24332
|
name: "<noservices>",
|
|
23359
24333
|
status: "error",
|
|
23360
24334
|
error: new WERR_INTERNAL("No services available."),
|
|
23361
24335
|
results: []
|
|
23362
24336
|
};
|
|
24337
|
+
const resultsByTxid = /* @__PURE__ */ new Map();
|
|
24338
|
+
const unresolved = new Set(txids);
|
|
24339
|
+
const providerNames = [];
|
|
24340
|
+
let successfulProvider = false;
|
|
24341
|
+
const rank = (result) => {
|
|
24342
|
+
if (result.status === "mined") return 4;
|
|
24343
|
+
if (result.status === "known") return 3;
|
|
24344
|
+
if (result.terminal === true) return 2;
|
|
24345
|
+
return 1;
|
|
24346
|
+
};
|
|
23363
24347
|
for (let tries = 0; tries < services.count; tries++) {
|
|
23364
24348
|
const stc = services.serviceToCall;
|
|
23365
24349
|
try {
|
|
23366
|
-
const
|
|
24350
|
+
const requestedTxids = [...unresolved];
|
|
24351
|
+
if (requestedTxids.length === 0) break;
|
|
24352
|
+
const r = await this.getStatusForTxidsBatched(stc, requestedTxids);
|
|
23367
24353
|
if (r.status === "success") {
|
|
23368
24354
|
services.addServiceCallSuccess(stc);
|
|
23369
|
-
|
|
23370
|
-
|
|
24355
|
+
successfulProvider = true;
|
|
24356
|
+
providerNames.push(r.name);
|
|
24357
|
+
for (const result of r.results) {
|
|
24358
|
+
const current = resultsByTxid.get(result.txid);
|
|
24359
|
+
if (current == null || rank(result) > rank(current)) resultsByTxid.set(result.txid, result);
|
|
24360
|
+
if (result.status === "mined" || result.status === "known") unresolved.delete(result.txid);
|
|
24361
|
+
}
|
|
24362
|
+
services.next();
|
|
24363
|
+
continue;
|
|
23371
24364
|
}
|
|
24365
|
+
fallback = r;
|
|
23372
24366
|
if (r.error != null) services.addServiceCallError(stc, r.error);
|
|
23373
24367
|
else services.addServiceCallFailure(stc);
|
|
23374
24368
|
} catch (error_) {
|
|
23375
24369
|
const e = WalletError.fromUnknown(error_);
|
|
24370
|
+
fallback = {
|
|
24371
|
+
name: stc.providerName,
|
|
24372
|
+
status: "error",
|
|
24373
|
+
error: e,
|
|
24374
|
+
results: []
|
|
24375
|
+
};
|
|
23376
24376
|
services.addServiceCallError(stc, e);
|
|
23377
24377
|
}
|
|
23378
24378
|
services.next();
|
|
23379
24379
|
}
|
|
23380
|
-
return
|
|
24380
|
+
if (!successfulProvider) return fallback;
|
|
24381
|
+
return {
|
|
24382
|
+
name: [...new Set(providerNames)].join(","),
|
|
24383
|
+
status: "success",
|
|
24384
|
+
results: txids.map((txid) => resultsByTxid.get(txid) ?? {
|
|
24385
|
+
txid,
|
|
24386
|
+
status: "unknown",
|
|
24387
|
+
depth: void 0
|
|
24388
|
+
})
|
|
24389
|
+
};
|
|
23381
24390
|
}
|
|
23382
24391
|
async getStatusForTxidsBatched(stc, txids) {
|
|
23383
24392
|
const results = [];
|
|
@@ -23404,9 +24413,7 @@ var Services = class Services {
|
|
|
23404
24413
|
return _bsv_sdk.Utils.toHex(sha256Hash(_bsv_sdk.Utils.toArray(script, "hex")));
|
|
23405
24414
|
}
|
|
23406
24415
|
async isUtxo(output) {
|
|
23407
|
-
|
|
23408
|
-
const hash = this.hashOutputScript(_bsv_sdk.Utils.toHex(output.lockingScript));
|
|
23409
|
-
return (await this.getUtxoStatus(hash, void 0, `${output.txid ?? ""}.${output.vout}`)).isUtxo === true;
|
|
24416
|
+
return requireConclusiveUtxo(await classifyOutputUtxo(this, output));
|
|
23410
24417
|
}
|
|
23411
24418
|
async getUtxoStatus(output, outputFormat, outpoint, useNext, logger) {
|
|
23412
24419
|
const services = this.getUtxoStatusServices;
|
|
@@ -23632,7 +24639,15 @@ var Services = class Services {
|
|
|
23632
24639
|
const method = async () => {
|
|
23633
24640
|
return await this.options.chaintracks.currentHeight();
|
|
23634
24641
|
};
|
|
23635
|
-
|
|
24642
|
+
try {
|
|
24643
|
+
return await this.invokeChaintracksWithRetry(method, "current_height");
|
|
24644
|
+
} catch (error_) {
|
|
24645
|
+
try {
|
|
24646
|
+
return (await this.whatsonchain.getChainInfo()).blocks;
|
|
24647
|
+
} catch {
|
|
24648
|
+
throw error_;
|
|
24649
|
+
}
|
|
24650
|
+
}
|
|
23636
24651
|
}
|
|
23637
24652
|
async hashToHeader(hash) {
|
|
23638
24653
|
const method = async () => {
|
|
@@ -27351,17 +28366,34 @@ var ArcSSEClient = class {
|
|
|
27351
28366
|
console.log(`${TAG} connected`);
|
|
27352
28367
|
});
|
|
27353
28368
|
this.es.addEventListener("status", (event) => {
|
|
28369
|
+
let data;
|
|
27354
28370
|
try {
|
|
27355
|
-
|
|
27356
|
-
console.log(`${TAG} event: txid=${data.txid} status=${data.txStatus}`);
|
|
27357
|
-
if (typeof event.lastEventId === "string" && event.lastEventId !== "") {
|
|
27358
|
-
this._lastEventId = event.lastEventId;
|
|
27359
|
-
this.options.onLastEventIdChanged?.(event.lastEventId);
|
|
27360
|
-
}
|
|
27361
|
-
this.options.onEvent(data);
|
|
28371
|
+
data = JSON.parse(event.data);
|
|
27362
28372
|
} catch {
|
|
27363
28373
|
console.log(`${TAG} malformed event: ${String(event.data).substring(0, 200)}`);
|
|
28374
|
+
return;
|
|
28375
|
+
}
|
|
28376
|
+
console.log(`${TAG} event: txid=${data.txid} status=${data.txStatus}`);
|
|
28377
|
+
const receivedEventId = typeof event.lastEventId === "string" && event.lastEventId !== "" ? event.lastEventId : void 0;
|
|
28378
|
+
data.eventId = receivedEventId;
|
|
28379
|
+
let processing;
|
|
28380
|
+
try {
|
|
28381
|
+
processing = this.options.onEvent(data);
|
|
28382
|
+
} catch (error) {
|
|
28383
|
+
const e = error instanceof Error ? error : new Error(String(error));
|
|
28384
|
+
console.log(`${TAG} event processing failed: ${e.message}`);
|
|
28385
|
+
this.options.onError?.(e);
|
|
28386
|
+
return;
|
|
27364
28387
|
}
|
|
28388
|
+
Promise.resolve(processing).then(async () => {
|
|
28389
|
+
if (receivedEventId == null) return;
|
|
28390
|
+
await this.options.onLastEventIdChanged?.(receivedEventId);
|
|
28391
|
+
this._lastEventId = receivedEventId;
|
|
28392
|
+
}).catch((error) => {
|
|
28393
|
+
const e = error instanceof Error ? error : new Error(String(error));
|
|
28394
|
+
console.log(`${TAG} event processing failed: ${e.message}`);
|
|
28395
|
+
this.options.onError?.(e);
|
|
28396
|
+
});
|
|
27365
28397
|
});
|
|
27366
28398
|
this.es.addEventListener("error", (event) => {
|
|
27367
28399
|
console.log(`${TAG} error:`, JSON.stringify(event));
|
|
@@ -27396,94 +28428,6 @@ var ArcSSEClient = class {
|
|
|
27396
28428
|
}
|
|
27397
28429
|
};
|
|
27398
28430
|
//#endregion
|
|
27399
|
-
//#region ../src/monitor/tasks/TaskUnFail.ts
|
|
27400
|
-
/**
|
|
27401
|
-
* Setting provenTxReq status to 'unfail' when 'invalid' will attempt to find a merklePath, and if successful:
|
|
27402
|
-
*
|
|
27403
|
-
* 1. set the req status to 'unmined'
|
|
27404
|
-
* 2. set the referenced txs to 'unproven'
|
|
27405
|
-
* 3. determine if any inputs match user's existing outputs and if so update spentBy and spendable of those outputs.
|
|
27406
|
-
* 4. set the txs outputs to spendable
|
|
27407
|
-
*
|
|
27408
|
-
* If it fails (to find a merklePath), returns the req status to 'invalid'.
|
|
27409
|
-
*/
|
|
27410
|
-
var TaskUnFail = class TaskUnFail extends WalletMonitorTask {
|
|
27411
|
-
triggerMsecs;
|
|
27412
|
-
static taskName = "UnFail";
|
|
27413
|
-
/**
|
|
27414
|
-
* Set to true to trigger running this task
|
|
27415
|
-
*/
|
|
27416
|
-
static checkNowRequested = false;
|
|
27417
|
-
static get checkNow() {
|
|
27418
|
-
return this.checkNowRequested;
|
|
27419
|
-
}
|
|
27420
|
-
static set checkNow(value) {
|
|
27421
|
-
this.checkNowRequested = value;
|
|
27422
|
-
}
|
|
27423
|
-
constructor(monitor, triggerMsecs = Monitor.oneMinute * 10) {
|
|
27424
|
-
super(monitor, TaskUnFail.taskName);
|
|
27425
|
-
this.triggerMsecs = triggerMsecs;
|
|
27426
|
-
}
|
|
27427
|
-
trigger(nowMsecsSinceEpoch) {
|
|
27428
|
-
return { run: TaskUnFail.checkNow || this.triggerMsecs > 0 && nowMsecsSinceEpoch - this.lastRunMsecsSinceEpoch > this.triggerMsecs };
|
|
27429
|
-
}
|
|
27430
|
-
async runTask() {
|
|
27431
|
-
let log = "";
|
|
27432
|
-
TaskUnFail.checkNow = false;
|
|
27433
|
-
const limit = 100;
|
|
27434
|
-
let offset = 0;
|
|
27435
|
-
for (;;) {
|
|
27436
|
-
const reqs = await this.storage.findProvenTxReqs({
|
|
27437
|
-
partial: {},
|
|
27438
|
-
status: ["unfail"],
|
|
27439
|
-
paged: {
|
|
27440
|
-
limit,
|
|
27441
|
-
offset
|
|
27442
|
-
}
|
|
27443
|
-
});
|
|
27444
|
-
if (reqs.length === 0) break;
|
|
27445
|
-
log += `${reqs.length} reqs with status 'unfail'\n`;
|
|
27446
|
-
const r = await this.unfail(reqs, 2);
|
|
27447
|
-
log += `${r.log}\n`;
|
|
27448
|
-
if (reqs.length < limit) break;
|
|
27449
|
-
offset += limit;
|
|
27450
|
-
}
|
|
27451
|
-
return log;
|
|
27452
|
-
}
|
|
27453
|
-
async unfail(reqs, indent = 0) {
|
|
27454
|
-
let log = "";
|
|
27455
|
-
for (const reqApi of reqs) {
|
|
27456
|
-
const req = new EntityProvenTxReq(reqApi);
|
|
27457
|
-
log += " ".repeat(indent);
|
|
27458
|
-
log += `reqId ${reqApi.provenTxReqId} txid ${reqApi.txid}: `;
|
|
27459
|
-
if ((await this.monitor.services.getMerklePath(req.txid)).merklePath != null) {
|
|
27460
|
-
log += "unfailed. status is now 'unmined'\n";
|
|
27461
|
-
log += await this.unfailReq(req, indent + 2);
|
|
27462
|
-
} else {
|
|
27463
|
-
req.status = "invalid";
|
|
27464
|
-
log += "returned to status 'invalid'\n";
|
|
27465
|
-
await req.updateStorageDynamicProperties(this.storage);
|
|
27466
|
-
}
|
|
27467
|
-
}
|
|
27468
|
-
return { log };
|
|
27469
|
-
}
|
|
27470
|
-
/**
|
|
27471
|
-
* 2. set the referenced txs to 'unproven'
|
|
27472
|
-
* 3. determine if any inputs match user's existing outputs and if so update spentBy and spendable of those outputs.
|
|
27473
|
-
* 4. set the txs outputs to spendable
|
|
27474
|
-
*
|
|
27475
|
-
* @param req
|
|
27476
|
-
* @param indent
|
|
27477
|
-
* @returns
|
|
27478
|
-
*/
|
|
27479
|
-
async unfailReq(req, indent) {
|
|
27480
|
-
return await this.storage.runAsStorageProvider(async (sp) => await sp.unfailTransactionsForProof(req, indent, {
|
|
27481
|
-
status: "unmined",
|
|
27482
|
-
attempts: 0
|
|
27483
|
-
}));
|
|
27484
|
-
}
|
|
27485
|
-
};
|
|
27486
|
-
//#endregion
|
|
27487
28431
|
//#region ../src/monitor/tasks/TaskArcSSE.ts
|
|
27488
28432
|
/**
|
|
27489
28433
|
* Monitor task that receives transaction status updates from Arcade via SSE
|
|
@@ -27527,16 +28471,14 @@ var TaskArcadeSSE = class TaskArcadeSSE extends WalletMonitorTask {
|
|
|
27527
28471
|
arcApiKey: arcadeApiKey,
|
|
27528
28472
|
lastEventId,
|
|
27529
28473
|
EventSourceClass,
|
|
27530
|
-
onEvent: (event) => {
|
|
27531
|
-
this.pendingEvents.push(
|
|
27532
|
-
|
|
28474
|
+
onEvent: async (event) => await new Promise((acknowledge) => {
|
|
28475
|
+
this.pendingEvents.push({
|
|
28476
|
+
event,
|
|
28477
|
+
acknowledge
|
|
28478
|
+
});
|
|
28479
|
+
}),
|
|
27533
28480
|
onError: (err) => {
|
|
27534
28481
|
this.logSetupEvent(`SSE error: ${err.message}`);
|
|
27535
|
-
},
|
|
27536
|
-
onLastEventIdChanged: (id) => {
|
|
27537
|
-
this.monitor.options.saveLastSSEEventId?.(id).catch((e) => {
|
|
27538
|
-
this.logSetupEvent(`failed to persist lastEventId: ${stringifyError(e)}`);
|
|
27539
|
-
});
|
|
27540
28482
|
}
|
|
27541
28483
|
});
|
|
27542
28484
|
this.sseClient.connect();
|
|
@@ -27550,10 +28492,21 @@ var TaskArcadeSSE = class TaskArcadeSSE extends WalletMonitorTask {
|
|
|
27550
28492
|
return { run: this.pendingEvents.length > 0 };
|
|
27551
28493
|
}
|
|
27552
28494
|
async runTask() {
|
|
27553
|
-
const
|
|
27554
|
-
if (
|
|
28495
|
+
const eventCount = this.pendingEvents.length;
|
|
28496
|
+
if (eventCount === 0) return "";
|
|
27555
28497
|
let log = "";
|
|
27556
|
-
for (
|
|
28498
|
+
for (let i = 0; i < eventCount; i++) {
|
|
28499
|
+
const pending = this.pendingEvents[0];
|
|
28500
|
+
log += await this.processStatusEvent(pending.event);
|
|
28501
|
+
if (pending.event.eventId != null) try {
|
|
28502
|
+
await this.monitor.options.saveLastSSEEventId?.(pending.event.eventId);
|
|
28503
|
+
} catch (e) {
|
|
28504
|
+
await this.logSetupEvent(`failed to persist lastEventId: ${stringifyError(e)}`);
|
|
28505
|
+
throw e;
|
|
28506
|
+
}
|
|
28507
|
+
this.pendingEvents.shift();
|
|
28508
|
+
pending.acknowledge();
|
|
28509
|
+
}
|
|
27557
28510
|
return log;
|
|
27558
28511
|
}
|
|
27559
28512
|
async fetchNow() {
|
|
@@ -27569,28 +28522,26 @@ var TaskArcadeSSE = class TaskArcadeSSE extends WalletMonitorTask {
|
|
|
27569
28522
|
}
|
|
27570
28523
|
for (const reqApi of reqs) {
|
|
27571
28524
|
const req = new EntityProvenTxReq(reqApi);
|
|
27572
|
-
const
|
|
27573
|
-
if (ProvenTxReqTerminalStatus.includes(req.status) && !
|
|
28525
|
+
const canRecoverTerminalRequest = this.canRecoverTerminalRequest(req, event);
|
|
28526
|
+
if (ProvenTxReqTerminalStatus.includes(req.status) && !canRecoverTerminalRequest) {
|
|
27574
28527
|
log += ` req ${req.id} already terminal: ${req.status}\n`;
|
|
27575
28528
|
continue;
|
|
27576
28529
|
}
|
|
27577
28530
|
const note = {
|
|
27578
28531
|
when: (/* @__PURE__ */ new Date()).toISOString(),
|
|
27579
28532
|
what: "arcSSE",
|
|
27580
|
-
arcStatus: event.txStatus
|
|
28533
|
+
arcStatus: event.txStatus,
|
|
28534
|
+
...event.timestamp !== "" ? { arcTimestamp: event.timestamp } : {},
|
|
28535
|
+
...event.status != null ? { statusCode: event.status } : {},
|
|
28536
|
+
...event.extraInfo != null && event.extraInfo !== "" ? { extraInfo: event.extraInfo.slice(0, 512) } : {}
|
|
27581
28537
|
};
|
|
27582
28538
|
log += await this.applyStatusEvent(req, event, note);
|
|
27583
28539
|
}
|
|
27584
28540
|
this.monitor.callOnTransactionStatusChanged(event.txid, event.txStatus);
|
|
27585
28541
|
return log;
|
|
27586
28542
|
}
|
|
27587
|
-
|
|
27588
|
-
return req.status === "invalid" && (
|
|
27589
|
-
"SENT_TO_NETWORK",
|
|
27590
|
-
"ACCEPTED_BY_NETWORK",
|
|
27591
|
-
"SEEN_ON_NETWORK",
|
|
27592
|
-
"SEEN_MULTIPLE_NODES"
|
|
27593
|
-
].includes(event.txStatus) || ["MINED", "IMMUTABLE"].includes(event.txStatus));
|
|
28543
|
+
canRecoverTerminalRequest(req, event) {
|
|
28544
|
+
return (req.status === "invalid" || req.status === "doubleSpend") && ["MINED", "IMMUTABLE"].includes(event.txStatus);
|
|
27594
28545
|
}
|
|
27595
28546
|
async applyStatusEvent(req, event, note) {
|
|
27596
28547
|
switch (event.txStatus) {
|
|
@@ -27600,11 +28551,16 @@ var TaskArcadeSSE = class TaskArcadeSSE extends WalletMonitorTask {
|
|
|
27600
28551
|
case "SEEN_MULTIPLE_NODES": return await this.applyAcceptedStatus(req, note);
|
|
27601
28552
|
case "MINED":
|
|
27602
28553
|
case "IMMUTABLE": return await this.applyMinedStatus(req, note);
|
|
27603
|
-
case "DOUBLE_SPEND_ATTEMPTED":
|
|
27604
|
-
case "
|
|
28554
|
+
case "DOUBLE_SPEND_ATTEMPTED":
|
|
28555
|
+
case "SEEN_IN_ORPHAN_MEMPOOL": return await this.applyDoubleSpendStatus(req, event, note);
|
|
28556
|
+
case "REJECTED": return await this.applyRejectedStatus(req, event, note);
|
|
28557
|
+
case "RECEIVED":
|
|
28558
|
+
case "PENDING_RETRY":
|
|
28559
|
+
case "STUMP_PROCESSING":
|
|
28560
|
+
case "UNKNOWN":
|
|
27605
28561
|
req.addHistoryNote(note);
|
|
27606
28562
|
await req.updateStorageDynamicProperties(this.storage);
|
|
27607
|
-
return ` req ${req.id}
|
|
28563
|
+
return ` req ${req.id} ${event.txStatus} recorded; awaiting resolution\n`;
|
|
27608
28564
|
default: return ` req ${req.id} unhandled status: ${event.txStatus}\n`;
|
|
27609
28565
|
}
|
|
27610
28566
|
}
|
|
@@ -27612,43 +28568,73 @@ var TaskArcadeSSE = class TaskArcadeSSE extends WalletMonitorTask {
|
|
|
27612
28568
|
if (![
|
|
27613
28569
|
"unsent",
|
|
27614
28570
|
"sending",
|
|
27615
|
-
"callback"
|
|
27616
|
-
"invalid"
|
|
28571
|
+
"callback"
|
|
27617
28572
|
].includes(req.status)) return "";
|
|
27618
|
-
|
|
27619
|
-
let log = "";
|
|
27620
|
-
if (wasInvalid) log += await new TaskUnFail(this.monitor).unfailReq(req, 4);
|
|
27621
|
-
else if (req.notify.transactionIds != null) await this.storage.runAsStorageProvider(async (sp) => {
|
|
28573
|
+
if (req.notify.transactionIds != null) await this.storage.runAsStorageProvider(async (sp) => {
|
|
27622
28574
|
await sp.updateTransactionsStatus(req.notify.transactionIds ?? [], "unproven");
|
|
27623
28575
|
});
|
|
27624
28576
|
req.status = "unmined";
|
|
27625
|
-
if (wasInvalid) req.attempts = 0;
|
|
27626
28577
|
req.wasBroadcast = true;
|
|
27627
28578
|
req.addHistoryNote(note);
|
|
27628
28579
|
await req.updateStorageDynamicProperties(this.storage);
|
|
27629
|
-
return
|
|
28580
|
+
return ` req ${req.id} => unmined\n`;
|
|
27630
28581
|
}
|
|
27631
28582
|
async applyMinedStatus(req, note) {
|
|
27632
|
-
let log = "";
|
|
27633
|
-
if (req.status === "invalid") {
|
|
27634
|
-
log += await new TaskUnFail(this.monitor).unfailReq(req, 4);
|
|
27635
|
-
req.status = "unmined";
|
|
27636
|
-
req.attempts = 0;
|
|
27637
|
-
req.wasBroadcast = true;
|
|
27638
|
-
}
|
|
27639
28583
|
req.addHistoryNote(note);
|
|
27640
28584
|
await req.updateStorageDynamicProperties(this.storage);
|
|
27641
|
-
return
|
|
28585
|
+
return await this.fetchProofFromServices(req);
|
|
27642
28586
|
}
|
|
27643
|
-
async applyDoubleSpendStatus(req, note) {
|
|
27644
|
-
req
|
|
27645
|
-
|
|
27646
|
-
|
|
27647
|
-
|
|
27648
|
-
|
|
27649
|
-
|
|
28587
|
+
async applyDoubleSpendStatus(req, event, note) {
|
|
28588
|
+
return await this.applyTerminalFailure(req, note, classifyArcadeRejection(event));
|
|
28589
|
+
}
|
|
28590
|
+
classifyRejection(event) {
|
|
28591
|
+
return classifyArcadeRejection(event);
|
|
28592
|
+
}
|
|
28593
|
+
async applyRejectedStatus(req, event, note) {
|
|
28594
|
+
const classification = this.classifyRejection(event);
|
|
28595
|
+
if (!classification.terminal) {
|
|
28596
|
+
req.addHistoryNote({
|
|
28597
|
+
...note,
|
|
28598
|
+
what: "arcSSERejectionPending",
|
|
28599
|
+
reason: classification.reason
|
|
28600
|
+
});
|
|
28601
|
+
await req.updateStorageDynamicProperties(this.storage);
|
|
28602
|
+
return ` req ${req.id} rejection recorded; awaiting resolution\n`;
|
|
28603
|
+
}
|
|
28604
|
+
return await this.applyTerminalFailure(req, note, classification);
|
|
28605
|
+
}
|
|
28606
|
+
async applyTerminalFailure(req, note, classification) {
|
|
28607
|
+
let quarantined = {
|
|
28608
|
+
checked: 0,
|
|
28609
|
+
staleConfirmed: 0,
|
|
28610
|
+
staleOutpoints: []
|
|
28611
|
+
};
|
|
28612
|
+
await this.storage.runAsStorageProvider(async (sp) => {
|
|
28613
|
+
await sp.transaction(async (trx) => {
|
|
28614
|
+
req.status = classification.reqStatus;
|
|
28615
|
+
req.addHistoryNote({
|
|
28616
|
+
...note,
|
|
28617
|
+
what: "arcSSETerminalRejection",
|
|
28618
|
+
reason: classification.reason,
|
|
28619
|
+
inputConflict: classification.inputConflict
|
|
28620
|
+
});
|
|
28621
|
+
await req.updateStorageDynamicProperties(sp, trx);
|
|
28622
|
+
const ids = req.notify.transactionIds;
|
|
28623
|
+
if (ids != null) await sp.updateTransactionsStatus(ids, "failed", trx);
|
|
28624
|
+
if (classification.inputConflict) {
|
|
28625
|
+
quarantined = await quarantineReqInputs(req, sp, trx);
|
|
28626
|
+
req.addHistoryNote({
|
|
28627
|
+
when: (/* @__PURE__ */ new Date()).toISOString(),
|
|
28628
|
+
what: "arcSSEInputQuarantine",
|
|
28629
|
+
checked: quarantined.checked,
|
|
28630
|
+
confirmed: quarantined.staleConfirmed,
|
|
28631
|
+
...quarantined.staleOutpoints.length > 0 ? { outpoints: quarantined.staleOutpoints.join(",") } : {}
|
|
28632
|
+
});
|
|
28633
|
+
await req.updateStorageDynamicProperties(sp, trx);
|
|
28634
|
+
}
|
|
28635
|
+
});
|
|
27650
28636
|
});
|
|
27651
|
-
return ` req ${req.id} =>
|
|
28637
|
+
return ` req ${req.id} => ${classification.reqStatus}; quarantined ${quarantined.staleConfirmed} local input copy/copies\n`;
|
|
27652
28638
|
}
|
|
27653
28639
|
/**
|
|
27654
28640
|
* Complete a MINED/IMMUTABLE status by using the configured proof providers.
|
|
@@ -28060,9 +29046,104 @@ var TaskCheckNoSends = class TaskCheckNoSends extends WalletMonitorTask {
|
|
|
28060
29046
|
}
|
|
28061
29047
|
};
|
|
28062
29048
|
//#endregion
|
|
29049
|
+
//#region ../src/monitor/tasks/TaskUnFail.ts
|
|
29050
|
+
/**
|
|
29051
|
+
* Setting provenTxReq status to 'unfail' when 'invalid' will attempt to find a merklePath, and if successful:
|
|
29052
|
+
*
|
|
29053
|
+
* 1. set the req status to 'unmined'
|
|
29054
|
+
* 2. set the referenced txs to 'unproven'
|
|
29055
|
+
* 3. determine if any inputs match user's existing outputs and if so update spentBy and spendable of those outputs.
|
|
29056
|
+
* 4. set the txs outputs to spendable
|
|
29057
|
+
*
|
|
29058
|
+
* If it fails (to find a merklePath), returns the req status to 'invalid'.
|
|
29059
|
+
*/
|
|
29060
|
+
var TaskUnFail = class TaskUnFail extends WalletMonitorTask {
|
|
29061
|
+
triggerMsecs;
|
|
29062
|
+
static taskName = "UnFail";
|
|
29063
|
+
/**
|
|
29064
|
+
* Set to true to trigger running this task
|
|
29065
|
+
*/
|
|
29066
|
+
static checkNowRequested = false;
|
|
29067
|
+
static get checkNow() {
|
|
29068
|
+
return this.checkNowRequested;
|
|
29069
|
+
}
|
|
29070
|
+
static set checkNow(value) {
|
|
29071
|
+
this.checkNowRequested = value;
|
|
29072
|
+
}
|
|
29073
|
+
constructor(monitor, triggerMsecs = Monitor.oneMinute * 10) {
|
|
29074
|
+
super(monitor, TaskUnFail.taskName);
|
|
29075
|
+
this.triggerMsecs = triggerMsecs;
|
|
29076
|
+
}
|
|
29077
|
+
trigger(nowMsecsSinceEpoch) {
|
|
29078
|
+
return { run: TaskUnFail.checkNow || this.triggerMsecs > 0 && nowMsecsSinceEpoch - this.lastRunMsecsSinceEpoch > this.triggerMsecs };
|
|
29079
|
+
}
|
|
29080
|
+
async runTask() {
|
|
29081
|
+
let log = "";
|
|
29082
|
+
TaskUnFail.checkNow = false;
|
|
29083
|
+
const limit = 100;
|
|
29084
|
+
let offset = 0;
|
|
29085
|
+
for (;;) {
|
|
29086
|
+
const reqs = await this.storage.findProvenTxReqs({
|
|
29087
|
+
partial: {},
|
|
29088
|
+
status: ["unfail"],
|
|
29089
|
+
paged: {
|
|
29090
|
+
limit,
|
|
29091
|
+
offset
|
|
29092
|
+
}
|
|
29093
|
+
});
|
|
29094
|
+
if (reqs.length === 0) break;
|
|
29095
|
+
log += `${reqs.length} reqs with status 'unfail'\n`;
|
|
29096
|
+
const r = await this.unfail(reqs, 2);
|
|
29097
|
+
log += `${r.log}\n`;
|
|
29098
|
+
if (reqs.length < limit) break;
|
|
29099
|
+
offset += limit;
|
|
29100
|
+
}
|
|
29101
|
+
return log;
|
|
29102
|
+
}
|
|
29103
|
+
async unfail(reqs, indent = 0) {
|
|
29104
|
+
let log = "";
|
|
29105
|
+
for (const reqApi of reqs) {
|
|
29106
|
+
const req = new EntityProvenTxReq(reqApi);
|
|
29107
|
+
log += " ".repeat(indent);
|
|
29108
|
+
log += `reqId ${reqApi.provenTxReqId} txid ${reqApi.txid}: `;
|
|
29109
|
+
if ((await this.monitor.services.getMerklePath(req.txid)).merklePath != null) {
|
|
29110
|
+
log += "unfailed. status is now 'unmined'\n";
|
|
29111
|
+
log += await this.unfailReq(req, indent + 2);
|
|
29112
|
+
} else {
|
|
29113
|
+
req.status = "invalid";
|
|
29114
|
+
log += "returned to status 'invalid'\n";
|
|
29115
|
+
await req.updateStorageDynamicProperties(this.storage);
|
|
29116
|
+
}
|
|
29117
|
+
}
|
|
29118
|
+
return { log };
|
|
29119
|
+
}
|
|
29120
|
+
/**
|
|
29121
|
+
* 2. set the referenced txs to 'unproven'
|
|
29122
|
+
* 3. determine if any inputs match user's existing outputs and if so update spentBy and spendable of those outputs.
|
|
29123
|
+
* 4. set the txs outputs to spendable
|
|
29124
|
+
*
|
|
29125
|
+
* @param req
|
|
29126
|
+
* @param indent
|
|
29127
|
+
* @returns
|
|
29128
|
+
*/
|
|
29129
|
+
async unfailReq(req, indent) {
|
|
29130
|
+
return await this.storage.runAsStorageProvider(async (sp) => await sp.unfailTransactionsForProof(req, indent, {
|
|
29131
|
+
status: "unmined",
|
|
29132
|
+
attempts: 0
|
|
29133
|
+
}));
|
|
29134
|
+
}
|
|
29135
|
+
};
|
|
29136
|
+
//#endregion
|
|
28063
29137
|
//#region ../src/monitor/tasks/TaskReviewUtxos.ts
|
|
29138
|
+
const REVIEW_PAGE_DEFAULT_LIMIT = 20;
|
|
29139
|
+
const REVIEW_PAGE_MAX_LIMIT = 250;
|
|
28064
29140
|
/**
|
|
28065
|
-
* Use the reviewByIdentityKey method to
|
|
29141
|
+
* Use the reviewByIdentityKey method to scan the UTXOs of a specific user by
|
|
29142
|
+
* identity key. The scan is read-only unless the caller explicitly requests
|
|
29143
|
+
* release, and release remains blocked if any provider result is inconclusive.
|
|
29144
|
+
* Operator UIs should use reviewPageByIdentityKey: it bounds each provider
|
|
29145
|
+
* round-trip and may explicitly release only the conclusive spent subset while
|
|
29146
|
+
* reporting unknowns.
|
|
28066
29147
|
*
|
|
28067
29148
|
* The task itself is disabled and will not run on a schedule; review must be triggered manually by calling reviewByIdentityKey.
|
|
28068
29149
|
*/
|
|
@@ -28079,7 +29160,7 @@ var TaskReviewUtxos = class TaskReviewUtxos extends WalletMonitorTask {
|
|
|
28079
29160
|
static set checkNow(value) {
|
|
28080
29161
|
this.checkNowRequested = value;
|
|
28081
29162
|
}
|
|
28082
|
-
constructor(monitor, triggerMsecs = 0, userLimit = 10, userOffset = 0, tags = ["
|
|
29163
|
+
constructor(monitor, triggerMsecs = 0, userLimit = 10, userOffset = 0, tags = ["all"]) {
|
|
28083
29164
|
super(monitor, TaskReviewUtxos.taskName);
|
|
28084
29165
|
this.triggerMsecs = triggerMsecs;
|
|
28085
29166
|
this.userLimit = userLimit;
|
|
@@ -28093,8 +29174,8 @@ var TaskReviewUtxos = class TaskReviewUtxos extends WalletMonitorTask {
|
|
|
28093
29174
|
TaskReviewUtxos.checkNow = false;
|
|
28094
29175
|
return "TaskReviewUtxos is disabled; use reviewByIdentityKey instead.\n";
|
|
28095
29176
|
}
|
|
28096
|
-
async reviewByIdentityKey(identityKey, mode = "all") {
|
|
28097
|
-
const tags = ["release", ...mode === "all" ? ["all"] : []];
|
|
29177
|
+
async reviewByIdentityKey(identityKey, mode = "all", release = false) {
|
|
29178
|
+
const tags = [...release ? ["release"] : [], ...mode === "all" ? ["all"] : []];
|
|
28098
29179
|
const vargs = {
|
|
28099
29180
|
basket: specOpInvalidChange,
|
|
28100
29181
|
tags,
|
|
@@ -28122,8 +29203,150 @@ var TaskReviewUtxos = class TaskReviewUtxos extends WalletMonitorTask {
|
|
|
28122
29203
|
return this.toUserLog(user, result.outputs, result.totalOutputs, total, tags);
|
|
28123
29204
|
});
|
|
28124
29205
|
}
|
|
29206
|
+
async reviewPageByIdentityKey(identityKey, mode = "all", release = false, pageLimit = REVIEW_PAGE_DEFAULT_LIMIT, offset = 0) {
|
|
29207
|
+
pageLimit = Math.min(Math.max(Math.trunc(pageLimit), 1), REVIEW_PAGE_MAX_LIMIT);
|
|
29208
|
+
offset = Math.max(Math.trunc(offset), 0);
|
|
29209
|
+
return await this.storage.runAsStorageProvider(async (sp) => {
|
|
29210
|
+
const user = (await sp.findUsers({ partial: { identityKey } }))[0];
|
|
29211
|
+
if (!user) return {
|
|
29212
|
+
found: false,
|
|
29213
|
+
identityKey,
|
|
29214
|
+
mode,
|
|
29215
|
+
release,
|
|
29216
|
+
offset,
|
|
29217
|
+
pageLimit,
|
|
29218
|
+
sourceScanned: 0,
|
|
29219
|
+
complete: true,
|
|
29220
|
+
checked: 0,
|
|
29221
|
+
confirmedUnspent: 0,
|
|
29222
|
+
confirmedSpent: 0,
|
|
29223
|
+
unknown: 0,
|
|
29224
|
+
confirmedSpentSatoshis: 0,
|
|
29225
|
+
released: 0,
|
|
29226
|
+
releasedSatoshis: 0,
|
|
29227
|
+
providers: [],
|
|
29228
|
+
providerCount: 0,
|
|
29229
|
+
providersTruncated: false,
|
|
29230
|
+
log: `identityKey ${identityKey} was not found\n`
|
|
29231
|
+
};
|
|
29232
|
+
let basketId;
|
|
29233
|
+
if (mode === "change") {
|
|
29234
|
+
basketId = (await sp.findOutputBaskets({ partial: {
|
|
29235
|
+
userId: user.userId,
|
|
29236
|
+
name: "default"
|
|
29237
|
+
} }))[0]?.basketId;
|
|
29238
|
+
if (basketId == null) return this.emptyPage(user, mode, release, pageLimit, offset);
|
|
29239
|
+
}
|
|
29240
|
+
const sourceOutputs = await sp.findOutputs({
|
|
29241
|
+
partial: {
|
|
29242
|
+
userId: user.userId,
|
|
29243
|
+
spendable: true,
|
|
29244
|
+
...basketId != null ? { basketId } : {}
|
|
29245
|
+
},
|
|
29246
|
+
txStatus: [
|
|
29247
|
+
"completed",
|
|
29248
|
+
"unproven",
|
|
29249
|
+
"nosend",
|
|
29250
|
+
"sending"
|
|
29251
|
+
],
|
|
29252
|
+
noScript: true,
|
|
29253
|
+
paged: {
|
|
29254
|
+
limit: pageLimit,
|
|
29255
|
+
offset
|
|
29256
|
+
}
|
|
29257
|
+
});
|
|
29258
|
+
const candidates = sourceOutputs.filter((output) => output.basketId != null);
|
|
29259
|
+
const review = await reviewUtxoOutputs(sp, {
|
|
29260
|
+
userId: user.userId,
|
|
29261
|
+
identityKey: user.identityKey
|
|
29262
|
+
}, candidates, release ? "conclusive" : "none");
|
|
29263
|
+
const complete = sourceOutputs.length < pageLimit;
|
|
29264
|
+
const nextOffset = complete ? void 0 : offset + sourceOutputs.length - review.diagnostics.released;
|
|
29265
|
+
const target = mode === "all" ? "spendable utxos" : "spendable change utxos";
|
|
29266
|
+
const action = release ? "released" : "found";
|
|
29267
|
+
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`;
|
|
29268
|
+
for (const output of review.confirmedSpentOutputs) log += ` ${output.txid}.${output.vout} ${output.satoshis} now ${output.spendable ? "spendable" : "spent"}\n`;
|
|
29269
|
+
if (review.unknownOutputs.length > 0) log += ` ${review.unknownOutputs.length} output(s) quarantined from release pending a conclusive provider result\n`;
|
|
29270
|
+
if (nextOffset != null) log += ` continue at offset ${nextOffset}\n`;
|
|
29271
|
+
return {
|
|
29272
|
+
found: true,
|
|
29273
|
+
userId: user.userId,
|
|
29274
|
+
identityKey: user.identityKey,
|
|
29275
|
+
mode,
|
|
29276
|
+
release,
|
|
29277
|
+
offset,
|
|
29278
|
+
pageLimit,
|
|
29279
|
+
sourceScanned: sourceOutputs.length,
|
|
29280
|
+
complete,
|
|
29281
|
+
...nextOffset != null ? { nextOffset } : {},
|
|
29282
|
+
...review.diagnostics,
|
|
29283
|
+
log
|
|
29284
|
+
};
|
|
29285
|
+
});
|
|
29286
|
+
}
|
|
29287
|
+
emptyPage(user, mode, release, pageLimit, offset) {
|
|
29288
|
+
return {
|
|
29289
|
+
found: true,
|
|
29290
|
+
userId: user.userId,
|
|
29291
|
+
identityKey: user.identityKey,
|
|
29292
|
+
mode,
|
|
29293
|
+
release,
|
|
29294
|
+
offset,
|
|
29295
|
+
pageLimit,
|
|
29296
|
+
sourceScanned: 0,
|
|
29297
|
+
complete: true,
|
|
29298
|
+
checked: 0,
|
|
29299
|
+
confirmedUnspent: 0,
|
|
29300
|
+
confirmedSpent: 0,
|
|
29301
|
+
unknown: 0,
|
|
29302
|
+
confirmedSpentSatoshis: 0,
|
|
29303
|
+
released: 0,
|
|
29304
|
+
releasedSatoshis: 0,
|
|
29305
|
+
providers: [],
|
|
29306
|
+
providerCount: 0,
|
|
29307
|
+
providersTruncated: false,
|
|
29308
|
+
log: `userId ${user.userId}: no invalid utxos found, ${user.identityKey}\n`
|
|
29309
|
+
};
|
|
29310
|
+
}
|
|
29311
|
+
/**
|
|
29312
|
+
* Report managed-change liquidity without changing it. Monitor deliberately
|
|
29313
|
+
* has no signing authority; progressive migration occurs only during a
|
|
29314
|
+
* caller-authorized createAction.
|
|
29315
|
+
*/
|
|
29316
|
+
async reviewManagedChangeByIdentityKey(identityKey) {
|
|
29317
|
+
return await this.storage.runAsStorageProvider(async (sp) => {
|
|
29318
|
+
const user = (await sp.findUsers({ partial: { identityKey } }))[0];
|
|
29319
|
+
if (user == null) return `identityKey ${identityKey} was not found\n`;
|
|
29320
|
+
const basket = verifyOne(await sp.findOutputBaskets({ partial: {
|
|
29321
|
+
userId: user.userId,
|
|
29322
|
+
name: "default"
|
|
29323
|
+
} }));
|
|
29324
|
+
const outputs = (await sp.findOutputs({
|
|
29325
|
+
partial: {
|
|
29326
|
+
userId: user.userId,
|
|
29327
|
+
basketId: basket.basketId,
|
|
29328
|
+
spendable: true,
|
|
29329
|
+
...managedChangeOutputFields
|
|
29330
|
+
},
|
|
29331
|
+
txStatus: [
|
|
29332
|
+
"completed",
|
|
29333
|
+
"unproven",
|
|
29334
|
+
"sending"
|
|
29335
|
+
],
|
|
29336
|
+
noScript: true
|
|
29337
|
+
})).filter(isAutoSpendableChangeOutput);
|
|
29338
|
+
const reserved = new Set(await sp.findReservedActionBatchOutputIds(outputs.map((output) => output.outputId)));
|
|
29339
|
+
const statuses = await sp.findTransactionStatusesByIds(user.userId, outputs.map((output) => output.transactionId));
|
|
29340
|
+
const preferred = Math.max(1, basket.minimumDesiredUTXOValue);
|
|
29341
|
+
const healthy = outputs.filter((output) => output.satoshis >= preferred);
|
|
29342
|
+
const undersized = outputs.filter((output) => output.satoshis < preferred);
|
|
29343
|
+
const countStatus = (status) => outputs.filter((output) => statuses.get(output.transactionId) === status).length;
|
|
29344
|
+
const satoshis = outputs.reduce((sum, output) => sum + output.satoshis, 0);
|
|
29345
|
+
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`;
|
|
29346
|
+
});
|
|
29347
|
+
}
|
|
28125
29348
|
toUserLog(user, outputs, totalOutputs, total, tags) {
|
|
28126
|
-
const action = tags.includes("release") ? "updated to unspendable" : "
|
|
29349
|
+
const action = tags.includes("release") ? "confirmed spent and updated to unspendable" : "confirmed spent";
|
|
28127
29350
|
const target = tags.includes("all") ? "spendable utxos" : "spendable change utxos";
|
|
28128
29351
|
let log = `userId ${user.userId}: ${totalOutputs} ${target} ${action}, total ${total}, ${user.identityKey}\n`;
|
|
28129
29352
|
for (const output of outputs) log += ` ${output.outpoint} ${output.satoshis} now ${output.spendable ? "spendable" : "spent"}\n`;
|
|
@@ -28132,6 +29355,13 @@ var TaskReviewUtxos = class TaskReviewUtxos extends WalletMonitorTask {
|
|
|
28132
29355
|
};
|
|
28133
29356
|
//#endregion
|
|
28134
29357
|
//#region ../src/monitor/tasks/TaskReviewDoubleSpends.ts
|
|
29358
|
+
function hasDurableInputConflict(req) {
|
|
29359
|
+
try {
|
|
29360
|
+
return JSON.parse(req.history).notes?.some((note) => note.inputConflict === true || note.what === "arcSSEInputQuarantine") === true;
|
|
29361
|
+
} catch {
|
|
29362
|
+
return true;
|
|
29363
|
+
}
|
|
29364
|
+
}
|
|
28135
29365
|
/**
|
|
28136
29366
|
* Review recent reqs in terminal 'doubleSpend' state and move any false positives
|
|
28137
29367
|
* back to 'unfail' so existing recovery handling can re-process them.
|
|
@@ -28199,14 +29429,16 @@ var TaskReviewDoubleSpends = class TaskReviewDoubleSpends extends WalletMonitorT
|
|
|
28199
29429
|
let lastRetainedDoubleSpendIndex = -1;
|
|
28200
29430
|
let retainedDoubleSpendCount = 0;
|
|
28201
29431
|
for (const req of reqs) {
|
|
28202
|
-
const
|
|
29432
|
+
const gsr = await this.monitor.services.getStatusForTxids([req.txid]);
|
|
29433
|
+
const status = gsr.status === "success" ? gsr.results[0]?.status : void 0;
|
|
29434
|
+
const durableInputConflict = hasDurableInputConflict(req);
|
|
28203
29435
|
reviewed.push(req);
|
|
28204
|
-
if (status === "
|
|
29436
|
+
if (!(status === "mined" || status === "known" && !durableInputConflict)) {
|
|
28205
29437
|
lastRetainedDoubleSpendIndex = reviewed.length - 1;
|
|
28206
29438
|
retainedDoubleSpendCount += 1;
|
|
28207
29439
|
} else {
|
|
28208
29440
|
unfails.push(req.provenTxReqId);
|
|
28209
|
-
log += `unfail ${req.provenTxReqId} ${req.txid} status:${status}\n`;
|
|
29441
|
+
log += `unfail ${req.provenTxReqId} ${req.txid} status:${status} durableInputConflict:${String(durableInputConflict)}\n`;
|
|
28210
29442
|
}
|
|
28211
29443
|
}
|
|
28212
29444
|
if (unfails.length > 0) await this.storage.runAsStorageProvider(async (sp) => {
|
|
@@ -28245,6 +29477,206 @@ var TaskReviewDoubleSpends = class TaskReviewDoubleSpends extends WalletMonitorT
|
|
|
28245
29477
|
}
|
|
28246
29478
|
};
|
|
28247
29479
|
//#endregion
|
|
29480
|
+
//#region ../src/monitor/tasks/TaskReconcilePendingTransactions.ts
|
|
29481
|
+
const REVIEW_STATUSES = [
|
|
29482
|
+
"callback",
|
|
29483
|
+
"unmined",
|
|
29484
|
+
"sending",
|
|
29485
|
+
"unknown",
|
|
29486
|
+
"unconfirmed"
|
|
29487
|
+
];
|
|
29488
|
+
/**
|
|
29489
|
+
* Poll durable transaction lifecycle state for aged, non-terminal requests.
|
|
29490
|
+
* This closes the gap where an Arcade rejection event was missed before the
|
|
29491
|
+
* monitor subscribed or while it was offline. Unknown/provider-error results
|
|
29492
|
+
* never mutate storage; only a provider's explicit terminal verdict does.
|
|
29493
|
+
*/
|
|
29494
|
+
var TaskReconcilePendingTransactions = class TaskReconcilePendingTransactions extends WalletMonitorTask {
|
|
29495
|
+
triggerMsecs;
|
|
29496
|
+
reviewLimit;
|
|
29497
|
+
minAgeMinutes;
|
|
29498
|
+
triggerQuickMsecs;
|
|
29499
|
+
static taskName = "ReconcilePendingTransactions";
|
|
29500
|
+
triggerNextMsecs;
|
|
29501
|
+
constructor(monitor, triggerMsecs = Monitor.oneMinute * 12, reviewLimit = 100, minAgeMinutes = 60, triggerQuickMsecs = Monitor.oneMinute) {
|
|
29502
|
+
super(monitor, TaskReconcilePendingTransactions.taskName);
|
|
29503
|
+
this.triggerMsecs = triggerMsecs;
|
|
29504
|
+
this.reviewLimit = reviewLimit;
|
|
29505
|
+
this.minAgeMinutes = minAgeMinutes;
|
|
29506
|
+
this.triggerQuickMsecs = triggerQuickMsecs;
|
|
29507
|
+
this.triggerNextMsecs = this.triggerQuickMsecs;
|
|
29508
|
+
}
|
|
29509
|
+
trigger(nowMsecsSinceEpoch) {
|
|
29510
|
+
return { run: this.triggerNextMsecs > 0 && nowMsecsSinceEpoch - this.lastRunMsecsSinceEpoch > this.triggerNextMsecs };
|
|
29511
|
+
}
|
|
29512
|
+
async getCheckpoint() {
|
|
29513
|
+
let events = [];
|
|
29514
|
+
await this.storage.runAsStorageProvider(async (sp) => {
|
|
29515
|
+
events = await sp.findMonitorEvents({
|
|
29516
|
+
partial: { event: TaskReconcilePendingTransactions.taskName },
|
|
29517
|
+
orderDescending: true,
|
|
29518
|
+
paged: { limit: 5 }
|
|
29519
|
+
});
|
|
29520
|
+
});
|
|
29521
|
+
for (const event of events) {
|
|
29522
|
+
if (!event.details) continue;
|
|
29523
|
+
try {
|
|
29524
|
+
const parsed = JSON.parse(event.details);
|
|
29525
|
+
if (parsed.cycleComplete === true) return void 0;
|
|
29526
|
+
if (typeof parsed.resumeOffset === "number") return {
|
|
29527
|
+
resumeOffset: parsed.resumeOffset,
|
|
29528
|
+
expectedProvenTxReqId: parsed.expectedProvenTxReqId
|
|
29529
|
+
};
|
|
29530
|
+
} catch {
|
|
29531
|
+
continue;
|
|
29532
|
+
}
|
|
29533
|
+
}
|
|
29534
|
+
}
|
|
29535
|
+
async runTask() {
|
|
29536
|
+
const checkpoint = await this.getCheckpoint();
|
|
29537
|
+
const createdBefore = /* @__PURE__ */ new Date(Date.now() - this.minAgeMinutes * 60 * 1e3);
|
|
29538
|
+
const reqs = await this.findReqsToReview(checkpoint);
|
|
29539
|
+
const eligible = reqs.filter((req) => req.created_at <= createdBefore);
|
|
29540
|
+
const provider = eligible.length === 0 ? void 0 : await this.monitor.services.getStatusForTxids(eligible.map((req) => req.txid));
|
|
29541
|
+
const results = new Map(provider?.results.map((result) => [result.txid, result]) ?? []);
|
|
29542
|
+
let reconciled = 0;
|
|
29543
|
+
let inputConflicts = 0;
|
|
29544
|
+
let reviewLog = "";
|
|
29545
|
+
const retained = [];
|
|
29546
|
+
for (const req of reqs) {
|
|
29547
|
+
const result = results.get(req.txid);
|
|
29548
|
+
if (req.created_at > createdBefore) {
|
|
29549
|
+
retained.push(req);
|
|
29550
|
+
continue;
|
|
29551
|
+
}
|
|
29552
|
+
const applied = await this.applyTerminalVerdict(req, provider?.status === "success" ? result : void 0, provider?.status === "success" ? provider.name : void 0, createdBefore);
|
|
29553
|
+
if (!applied) {
|
|
29554
|
+
retained.push(req);
|
|
29555
|
+
continue;
|
|
29556
|
+
}
|
|
29557
|
+
reconciled++;
|
|
29558
|
+
if (applied.result.inputConflict === true) inputConflicts++;
|
|
29559
|
+
reviewLog += `reconciled ${req.provenTxReqId} ${req.txid} ${applied.result.providerStatus ?? "terminal"} => ${applied.result.inputConflict === true ? "doubleSpend" : "invalid"}\n`;
|
|
29560
|
+
}
|
|
29561
|
+
const fullPage = reqs.length >= this.reviewLimit;
|
|
29562
|
+
this.triggerNextMsecs = fullPage ? this.triggerQuickMsecs : this.triggerMsecs;
|
|
29563
|
+
let resumeOffset;
|
|
29564
|
+
let expectedProvenTxReqId;
|
|
29565
|
+
if (fullPage) if (retained.length === 0) resumeOffset = reqs.sourceOffset ?? 0;
|
|
29566
|
+
else {
|
|
29567
|
+
resumeOffset = (reqs.sourceOffset ?? 0) + retained.length - 1;
|
|
29568
|
+
expectedProvenTxReqId = retained.at(-1)?.provenTxReqId;
|
|
29569
|
+
}
|
|
29570
|
+
return JSON.stringify({
|
|
29571
|
+
reviewed: eligible.length,
|
|
29572
|
+
reconciled,
|
|
29573
|
+
inputConflicts,
|
|
29574
|
+
retained: retained.length,
|
|
29575
|
+
reviewLimit: this.reviewLimit,
|
|
29576
|
+
minAgeMinutes: this.minAgeMinutes,
|
|
29577
|
+
cycleComplete: !fullPage,
|
|
29578
|
+
...resumeOffset != null ? { resumeOffset } : {},
|
|
29579
|
+
...expectedProvenTxReqId != null ? { expectedProvenTxReqId } : {},
|
|
29580
|
+
reviewLog
|
|
29581
|
+
});
|
|
29582
|
+
}
|
|
29583
|
+
async findReqsToReview(checkpoint) {
|
|
29584
|
+
let offset = checkpoint?.resumeOffset ?? 0;
|
|
29585
|
+
if (checkpoint?.expectedProvenTxReqId != null) offset = (await this.storage.findProvenTxReqs({
|
|
29586
|
+
partial: {},
|
|
29587
|
+
status: [...REVIEW_STATUSES],
|
|
29588
|
+
paged: {
|
|
29589
|
+
limit: 1,
|
|
29590
|
+
offset
|
|
29591
|
+
}
|
|
29592
|
+
}))[0]?.provenTxReqId === checkpoint.expectedProvenTxReqId ? offset + 1 : 0;
|
|
29593
|
+
const reqs = await this.storage.findProvenTxReqs({
|
|
29594
|
+
partial: {},
|
|
29595
|
+
status: [...REVIEW_STATUSES],
|
|
29596
|
+
paged: {
|
|
29597
|
+
limit: this.reviewLimit,
|
|
29598
|
+
offset
|
|
29599
|
+
}
|
|
29600
|
+
});
|
|
29601
|
+
reqs.sourceOffset = offset;
|
|
29602
|
+
return reqs;
|
|
29603
|
+
}
|
|
29604
|
+
async applyTerminalVerdict(reqApi, providerResult, providerName, createdBefore) {
|
|
29605
|
+
let applied;
|
|
29606
|
+
await this.storage.runAsStorageProvider(async (sp) => {
|
|
29607
|
+
await sp.transaction(async (trx) => {
|
|
29608
|
+
const req = new EntityProvenTxReq(reqApi);
|
|
29609
|
+
await req.refreshFromStorage(sp, trx);
|
|
29610
|
+
if (!REVIEW_STATUSES.includes(req.status)) return;
|
|
29611
|
+
if (req.created_at > createdBefore) return;
|
|
29612
|
+
const failedParentTxids = await this.findFailedLocalParentTxids(req, sp, trx);
|
|
29613
|
+
const result = providerResult?.terminal === true ? providerResult : providerResult?.status === "known" || providerResult?.status === "mined" ? void 0 : this.failedParentVerdict(req.txid, failedParentTxids);
|
|
29614
|
+
if (result == null) return;
|
|
29615
|
+
const provider = providerResult?.terminal === true ? providerName ?? "transaction-status-provider" : "local-storage";
|
|
29616
|
+
req.status = result.inputConflict === true ? "doubleSpend" : "invalid";
|
|
29617
|
+
req.addHistoryNote({
|
|
29618
|
+
when: (/* @__PURE__ */ new Date()).toISOString(),
|
|
29619
|
+
what: "proactiveTerminalReconciliation",
|
|
29620
|
+
provider,
|
|
29621
|
+
providerStatus: result.providerStatus,
|
|
29622
|
+
statusCode: result.statusCode,
|
|
29623
|
+
description: result.description,
|
|
29624
|
+
inputConflict: result.inputConflict === true,
|
|
29625
|
+
competingTxs: result.competingTxs?.join(",")
|
|
29626
|
+
});
|
|
29627
|
+
await req.updateStorageDynamicProperties(sp, trx);
|
|
29628
|
+
if (req.notify.transactionIds != null) await sp.updateTransactionsStatus(req.notify.transactionIds, "failed", trx);
|
|
29629
|
+
if (result.inputConflict === true) {
|
|
29630
|
+
const quarantined = await quarantineReqInputs(req, sp, trx);
|
|
29631
|
+
req.addHistoryNote({
|
|
29632
|
+
when: (/* @__PURE__ */ new Date()).toISOString(),
|
|
29633
|
+
what: "proactiveInputQuarantine",
|
|
29634
|
+
checked: quarantined.checked,
|
|
29635
|
+
confirmed: quarantined.staleConfirmed,
|
|
29636
|
+
...quarantined.staleOutpoints.length > 0 ? { outpoints: quarantined.staleOutpoints.join(",") } : {}
|
|
29637
|
+
});
|
|
29638
|
+
await req.updateStorageDynamicProperties(sp, trx);
|
|
29639
|
+
} else if (failedParentTxids.length > 0) {
|
|
29640
|
+
const quarantined = await quarantineReqInputsFromFailedParents(req, failedParentTxids, sp, trx);
|
|
29641
|
+
req.addHistoryNote({
|
|
29642
|
+
when: (/* @__PURE__ */ new Date()).toISOString(),
|
|
29643
|
+
what: "proactiveFailedParentQuarantine",
|
|
29644
|
+
checked: quarantined.checked,
|
|
29645
|
+
confirmed: quarantined.staleConfirmed,
|
|
29646
|
+
parentTxids: failedParentTxids.join(","),
|
|
29647
|
+
...quarantined.staleOutpoints.length > 0 ? { outpoints: quarantined.staleOutpoints.join(",") } : {}
|
|
29648
|
+
});
|
|
29649
|
+
await req.updateStorageDynamicProperties(sp, trx);
|
|
29650
|
+
}
|
|
29651
|
+
applied = { result };
|
|
29652
|
+
});
|
|
29653
|
+
});
|
|
29654
|
+
if (applied) this.monitor.callOnTransactionStatusChanged(reqApi.txid, applied.result.providerStatus ?? "REJECTED");
|
|
29655
|
+
return applied;
|
|
29656
|
+
}
|
|
29657
|
+
async findFailedLocalParentTxids(req, storage, trx) {
|
|
29658
|
+
const parentTxids = [...new Set(_bsv_sdk.Transaction.fromBinary(req.rawTx).inputs.map((input) => input.sourceTXID).filter((txid) => txid != null && txid !== ""))];
|
|
29659
|
+
const failed = [];
|
|
29660
|
+
for (const txid of parentTxids) if ((await storage.findProvenTxReqs({
|
|
29661
|
+
partial: { txid },
|
|
29662
|
+
trx
|
|
29663
|
+
})).some((parent) => parent.status === "invalid" || parent.status === "doubleSpend")) failed.push(txid);
|
|
29664
|
+
return failed;
|
|
29665
|
+
}
|
|
29666
|
+
failedParentVerdict(txid, failedParentTxids) {
|
|
29667
|
+
if (failedParentTxids.length === 0) return void 0;
|
|
29668
|
+
return {
|
|
29669
|
+
txid,
|
|
29670
|
+
status: "unknown",
|
|
29671
|
+
depth: void 0,
|
|
29672
|
+
terminal: true,
|
|
29673
|
+
inputConflict: false,
|
|
29674
|
+
providerStatus: "LOCAL_PARENT_FAILED",
|
|
29675
|
+
description: `Local parent request is terminal: ${failedParentTxids.join(",")}`.slice(0, 512)
|
|
29676
|
+
};
|
|
29677
|
+
}
|
|
29678
|
+
};
|
|
29679
|
+
//#endregion
|
|
28248
29680
|
//#region ../src/monitor/tasks/TaskReviewProvenTxs.ts
|
|
28249
29681
|
/**
|
|
28250
29682
|
* Backup verification task for recent proven_txs records.
|
|
@@ -28503,14 +29935,14 @@ var Monitor = class Monitor {
|
|
|
28503
29935
|
purgeFailedAge: 5 * Monitor.oneDay
|
|
28504
29936
|
};
|
|
28505
29937
|
addAllTasksToOther() {
|
|
28506
|
-
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));
|
|
29938
|
+
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));
|
|
28507
29939
|
if (this.chain === "mock") this._otherTasks.push(new TaskMineBlock(this));
|
|
28508
29940
|
}
|
|
28509
29941
|
/**
|
|
28510
29942
|
* Default tasks with settings appropriate for a single user storage
|
|
28511
29943
|
*/
|
|
28512
29944
|
addDefaultTasks() {
|
|
28513
|
-
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));
|
|
29945
|
+
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));
|
|
28514
29946
|
this._otherTasks.push(new TaskPurge(this, this.defaultPurgeParams, 6 * Monitor.oneHour), new TaskReviewUtxos(this));
|
|
28515
29947
|
if (this.chain === "mock") this._tasks.push(new TaskMineBlock(this));
|
|
28516
29948
|
}
|
|
@@ -28518,7 +29950,7 @@ var Monitor = class Monitor {
|
|
|
28518
29950
|
* Tasks appropriate for multi-user storage
|
|
28519
29951
|
*/
|
|
28520
29952
|
addMultiUserTasks() {
|
|
28521
|
-
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));
|
|
29953
|
+
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));
|
|
28522
29954
|
this._otherTasks.push(new TaskPurge(this, this.defaultPurgeParams), new TaskReviewUtxos(this));
|
|
28523
29955
|
if (this.chain === "mock") this._tasks.push(new TaskMineBlock(this));
|
|
28524
29956
|
}
|
|
@@ -28889,6 +30321,7 @@ var SetupClient = class SetupClient {
|
|
|
28889
30321
|
model: "sat/kb",
|
|
28890
30322
|
value: 100
|
|
28891
30323
|
},
|
|
30324
|
+
managedChangePolicy: args.managedChangePolicy,
|
|
28892
30325
|
scriptVerifier: args.scriptVerifier
|
|
28893
30326
|
});
|
|
28894
30327
|
await storage.migrate(args.databaseName, randomBytesHex(33));
|
|
@@ -33478,7 +34911,7 @@ var WalletPermissionsManager = class WalletPermissionsManager {
|
|
|
33478
34911
|
basket: basketName,
|
|
33479
34912
|
tags
|
|
33480
34913
|
}],
|
|
33481
|
-
options: { acceptDelayedBroadcast:
|
|
34914
|
+
options: { acceptDelayedBroadcast: true }
|
|
33482
34915
|
}, this.adminOriginator);
|
|
33483
34916
|
}
|
|
33484
34917
|
async mapWithConcurrency(items, concurrency, fn) {
|
|
@@ -33542,7 +34975,7 @@ var WalletPermissionsManager = class WalletPermissionsManager {
|
|
|
33542
34975
|
await this.createAction({
|
|
33543
34976
|
description: `Grant ${built.length} permissions`,
|
|
33544
34977
|
outputs: built.map((b) => b.output),
|
|
33545
|
-
options: { acceptDelayedBroadcast:
|
|
34978
|
+
options: { acceptDelayedBroadcast: true }
|
|
33546
34979
|
}, this.adminOriginator);
|
|
33547
34980
|
return built.map((b) => b.request);
|
|
33548
34981
|
}, strict);
|
|
@@ -34939,6 +36372,7 @@ exports.ARGON2ID_MAX_MEMORY_KIB = ARGON2ID_MAX_MEMORY_KIB;
|
|
|
34939
36372
|
exports.ARGON2ID_MAX_PARALLELISM = ARGON2ID_MAX_PARALLELISM;
|
|
34940
36373
|
exports.AuthMethodInteractor = AuthMethodInteractor;
|
|
34941
36374
|
exports.BHServiceClient = BHServiceClient;
|
|
36375
|
+
exports.BRC153_REFERENCE_PREFIX = BRC153_REFERENCE_PREFIX;
|
|
34942
36376
|
exports.BulkFileDataManager = BulkFileDataManager;
|
|
34943
36377
|
exports.BulkFileDataReader = BulkFileDataReader;
|
|
34944
36378
|
exports.BulkFilesReader = BulkFilesReader;
|
|
@@ -34959,6 +36393,11 @@ exports.ChaintracksServiceClient = ChaintracksServiceClient;
|
|
|
34959
36393
|
exports.ChaintracksStorageBase = ChaintracksStorageBase;
|
|
34960
36394
|
exports.ChaintracksStorageIdb = ChaintracksStorageIdb;
|
|
34961
36395
|
exports.ChaintracksStorageNoDb = ChaintracksStorageNoDb;
|
|
36396
|
+
exports.DEFAULT_MANAGED_CHANGE_MAX_OUTPUTS_PER_ACTION = DEFAULT_MANAGED_CHANGE_MAX_OUTPUTS_PER_ACTION;
|
|
36397
|
+
exports.DEFAULT_MANAGED_CHANGE_MIGRATION_INPUTS_PER_ACTION = DEFAULT_MANAGED_CHANGE_MIGRATION_INPUTS_PER_ACTION;
|
|
36398
|
+
exports.DEFAULT_MANAGED_CHANGE_MINIMUM_SATOSHIS = DEFAULT_MANAGED_CHANGE_MINIMUM_SATOSHIS;
|
|
36399
|
+
exports.DEFAULT_MANAGED_CHANGE_PENDING_COMPARISON_INPUTS = DEFAULT_MANAGED_CHANGE_PENDING_COMPARISON_INPUTS;
|
|
36400
|
+
exports.DEFAULT_MANAGED_CHANGE_TARGET_UTXOS = DEFAULT_MANAGED_CHANGE_TARGET_UTXOS;
|
|
34962
36401
|
exports.DEFAULT_PROFILE_ID = DEFAULT_PROFILE_ID;
|
|
34963
36402
|
exports.DEFAULT_SETTINGS = DEFAULT_SETTINGS;
|
|
34964
36403
|
exports.DevConsoleInteractor = DevConsoleInteractor;
|
|
@@ -34980,6 +36419,7 @@ exports.EntityUser = EntityUser;
|
|
|
34980
36419
|
exports.GoChaintracksServiceClient = GoChaintracksServiceClient;
|
|
34981
36420
|
exports.HeightRange = HeightRange;
|
|
34982
36421
|
exports.KDF_MAX_HASH_LENGTH = KDF_MAX_HASH_LENGTH;
|
|
36422
|
+
exports.LEGACY_MANAGED_CHANGE_MINIMUM_SATOSHIS = LEGACY_MANAGED_CHANGE_MINIMUM_SATOSHIS;
|
|
34983
36423
|
exports.LiveIngestorBase = LiveIngestorBase;
|
|
34984
36424
|
exports.LiveIngestorChaintracksSSE = LiveIngestorChaintracksSSE;
|
|
34985
36425
|
exports.LiveIngestorWhatsOnChainPoll = LiveIngestorWhatsOnChainPoll;
|
|
@@ -35014,6 +36454,7 @@ exports.WalletSettingsManager = WalletSettingsManager;
|
|
|
35014
36454
|
exports.WalletSigner = WalletSigner;
|
|
35015
36455
|
exports.WalletStorageManager = WalletStorageManager;
|
|
35016
36456
|
exports.WhatsOnChainServices = WhatsOnChainServices;
|
|
36457
|
+
exports.applyBrc153ReferenceLabel = applyBrc153ReferenceLabel;
|
|
35017
36458
|
exports.arcDefaultUrl = arcDefaultUrl;
|
|
35018
36459
|
exports.arcGorillaPoolUrl = arcGorillaPoolUrl;
|
|
35019
36460
|
exports.arcadeDefaultUrl = arcadeDefaultUrl;
|
|
@@ -35039,6 +36480,7 @@ exports.createIdbChaintracks = createIdbChaintracks;
|
|
|
35039
36480
|
exports.createNoDbChaintracks = createNoDbChaintracks;
|
|
35040
36481
|
exports.createSyncMap = createSyncMap;
|
|
35041
36482
|
exports.decryptBRC39 = decryptBRC39;
|
|
36483
|
+
exports.defaultManagedChangePolicy = defaultManagedChangePolicy;
|
|
35042
36484
|
exports.doubleSha256BE = doubleSha256BE;
|
|
35043
36485
|
exports.doubleSha256LE = doubleSha256LE;
|
|
35044
36486
|
exports.encryptBRC39 = encryptBRC39;
|
|
@@ -35053,6 +36495,8 @@ exports.importBRC39 = importBRC39;
|
|
|
35053
36495
|
exports.isAutoSpendableChangeOutput = isAutoSpendableChangeOutput;
|
|
35054
36496
|
exports.isBaseBlockHeader = isBaseBlockHeader;
|
|
35055
36497
|
exports.isBlockHeader = isBlockHeader;
|
|
36498
|
+
exports.isBrc153ReferenceLabel = isBrc153ReferenceLabel;
|
|
36499
|
+
exports.isLegacyManagedChangeBasketDefault = isLegacyManagedChangeBasketDefault;
|
|
35056
36500
|
exports.isLive = isLive;
|
|
35057
36501
|
exports.isLiveBlockHeader = isLiveBlockHeader;
|
|
35058
36502
|
exports.isManagedChangeOutput = isManagedChangeOutput;
|
|
@@ -35061,12 +36505,14 @@ exports.logWalletError = logWalletError;
|
|
|
35061
36505
|
exports.logger = logger;
|
|
35062
36506
|
exports.makeAtomicBeef = makeAtomicBeef;
|
|
35063
36507
|
exports.makeBrc114ActionTimeLabel = makeBrc114ActionTimeLabel;
|
|
36508
|
+
exports.makeBrc153ReferenceLabel = makeBrc153ReferenceLabel;
|
|
35064
36509
|
exports.managedChangeOutputFields = managedChangeOutputFields;
|
|
35065
36510
|
exports.maxDate = maxDate;
|
|
35066
36511
|
exports.optionalArraysEqual = optionalArraysEqual;
|
|
35067
36512
|
exports.outputColumnsWithoutLockingScript = outputColumnsWithoutLockingScript;
|
|
35068
36513
|
exports.parseBRC38Json = parseBRC38Json;
|
|
35069
36514
|
exports.parseBrc114ActionTimeLabels = parseBrc114ActionTimeLabels;
|
|
36515
|
+
exports.parseBrc153ReferenceLabel = parseBrc153ReferenceLabel;
|
|
35070
36516
|
exports.parseFileLink = parseFileLink;
|
|
35071
36517
|
exports.parseTxScriptOffsets = parseTxScriptOffsets;
|
|
35072
36518
|
exports.partitionActionLabels = partitionActionLabels;
|
|
@@ -35092,12 +36538,14 @@ exports.toDefaultChaintracksArguments = toDefaultChaintracksArguments;
|
|
|
35092
36538
|
exports.toLookupNetworkPreset = toLookupNetworkPreset;
|
|
35093
36539
|
exports.toWalletNetwork = toWalletNetwork;
|
|
35094
36540
|
exports.transactionColumnsWithoutRawTx = transactionColumnsWithoutRawTx;
|
|
36541
|
+
exports.upgradeLegacyManagedChangeBasketDefault = upgradeLegacyManagedChangeBasketDefault;
|
|
35095
36542
|
Object.defineProperty(exports, "utils", {
|
|
35096
36543
|
enumerable: true,
|
|
35097
36544
|
get: function() {
|
|
35098
36545
|
return blockHeaderUtilities_exports;
|
|
35099
36546
|
}
|
|
35100
36547
|
});
|
|
36548
|
+
exports.validateManagedChangePolicy = validateManagedChangePolicy;
|
|
35101
36549
|
exports.validateScriptHash = validateScriptHash;
|
|
35102
36550
|
exports.validateSecondsSinceEpoch = validateSecondsSinceEpoch;
|
|
35103
36551
|
exports.validateStorageFeeModel = validateStorageFeeModel;
|