@bsv/wallet-toolbox-client 2.6.4 → 2.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/out/index.client.cjs +1812 -399
- 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 +1798 -400
- 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,177 @@ 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 findLocalInputs(req, storage, trx) {
|
|
5977
|
+
const outpoints = _bsv_sdk.Transaction.fromBinary(req.rawTx).inputs.map((input) => ({
|
|
5978
|
+
txid: input.sourceTXID ?? "",
|
|
5979
|
+
vout: input.sourceOutputIndex ?? 0
|
|
5980
|
+
})).filter((outpoint) => outpoint.txid !== "");
|
|
5981
|
+
if (outpoints.length === 0) return [];
|
|
5982
|
+
const transactions = await storage.findTransactions({
|
|
5983
|
+
partial: { txid: req.txid },
|
|
5984
|
+
noRawTx: true,
|
|
5985
|
+
trx
|
|
5986
|
+
});
|
|
5987
|
+
const userIds = [...new Set(transactions.map((transaction) => transaction.userId))];
|
|
5988
|
+
const localInputs = [];
|
|
5989
|
+
const localKeys = /* @__PURE__ */ new Set();
|
|
5990
|
+
for (const userId of userIds) {
|
|
5991
|
+
const byOutpoint = await storage.findOutputsByOutpoints(userId, outpoints, trx);
|
|
5992
|
+
for (const outpoint of outpoints) {
|
|
5993
|
+
const output = byOutpoint[`${outpoint.txid}.${outpoint.vout}`];
|
|
5994
|
+
const localKey = `${userId}:${outpoint.txid}.${outpoint.vout}`;
|
|
5995
|
+
if (output != null && !localKeys.has(localKey)) {
|
|
5996
|
+
localKeys.add(localKey);
|
|
5997
|
+
localInputs.push({
|
|
5998
|
+
output,
|
|
5999
|
+
outpoint
|
|
6000
|
+
});
|
|
6001
|
+
}
|
|
6002
|
+
}
|
|
6003
|
+
}
|
|
6004
|
+
return localInputs;
|
|
6005
|
+
}
|
|
6006
|
+
/**
|
|
6007
|
+
* Conservatively quarantine every locally-owned input of a transaction after
|
|
6008
|
+
* Arcade supplies explicit missing-input/conflict evidence. This path is
|
|
6009
|
+
* deliberately independent of explorer or UTXO providers: a positive
|
|
6010
|
+
* broadcaster verdict is enough to stop the failed transaction from feeding
|
|
6011
|
+
* the same inputs into another action. A later validated mined proof remains
|
|
6012
|
+
* able to repair the transaction through the ordinary proof recovery path.
|
|
6013
|
+
*/
|
|
6014
|
+
async function quarantineReqInputs(req, storage, trx, logger) {
|
|
6015
|
+
const localInputs = await findLocalInputs(req, storage, trx);
|
|
6016
|
+
const staleOutpoints = /* @__PURE__ */ new Set();
|
|
6017
|
+
for (const { output, outpoint } of localInputs) {
|
|
6018
|
+
await storage.updateOutput(verifyId(output.outputId), { spendable: false }, trx);
|
|
6019
|
+
staleOutpoints.add(`${outpoint.txid}.${outpoint.vout}`);
|
|
6020
|
+
}
|
|
6021
|
+
const result = {
|
|
6022
|
+
checked: localInputs.length,
|
|
6023
|
+
staleConfirmed: localInputs.length,
|
|
6024
|
+
staleOutpoints: [...staleOutpoints]
|
|
6025
|
+
};
|
|
6026
|
+
if (result.staleConfirmed > 0) logger?.log(`quarantineReqInputs: ${result.staleConfirmed} local input copy/copies quarantined for txid=${req.txid}`);
|
|
6027
|
+
return result;
|
|
6028
|
+
}
|
|
6029
|
+
/**
|
|
6030
|
+
* Ask the configured UTXO-provider collection to classify the request's local
|
|
6031
|
+
* inputs and mark only positively confirmed spent outputs unavailable.
|
|
6032
|
+
* Provider errors and a collection with no providers are inconclusive and
|
|
6033
|
+
* never count as spent.
|
|
6034
|
+
*/
|
|
6035
|
+
async function markConfirmedStaleReqInputs(req, storage, services, trx, logger) {
|
|
6036
|
+
const result = {
|
|
6037
|
+
checked: 0,
|
|
6038
|
+
staleConfirmed: 0,
|
|
6039
|
+
staleOutpoints: []
|
|
6040
|
+
};
|
|
6041
|
+
const localInputs = await findLocalInputs(req, storage, trx);
|
|
6042
|
+
const verdicts = /* @__PURE__ */ new Map();
|
|
6043
|
+
const uniqueInputs = /* @__PURE__ */ new Map();
|
|
6044
|
+
for (const localInput of localInputs) {
|
|
6045
|
+
const key = `${localInput.outpoint.txid}.${localInput.outpoint.vout}`;
|
|
6046
|
+
if (!uniqueInputs.has(key)) uniqueInputs.set(key, localInput);
|
|
6047
|
+
}
|
|
6048
|
+
await mapWithConcurrency([...uniqueInputs.values()], 4, async ({ output, outpoint }) => {
|
|
6049
|
+
const key = `${outpoint.txid}.${outpoint.vout}`;
|
|
6050
|
+
if (output.lockingScript == null) try {
|
|
6051
|
+
await storage.validateOutputScript(output, trx);
|
|
6052
|
+
} catch {
|
|
6053
|
+
verdicts.set(key, "inconclusive");
|
|
6054
|
+
return;
|
|
6055
|
+
}
|
|
6056
|
+
if (output.lockingScript == null) {
|
|
6057
|
+
verdicts.set(key, "inconclusive");
|
|
6058
|
+
return;
|
|
6059
|
+
}
|
|
6060
|
+
const classification = await classifyOutputUtxo(services, output);
|
|
6061
|
+
let verdict;
|
|
6062
|
+
if (classification.verdict === "unknown") verdict = "inconclusive";
|
|
6063
|
+
else if (classification.verdict === "unspent") verdict = "utxo";
|
|
6064
|
+
else verdict = "stale";
|
|
6065
|
+
verdicts.set(key, verdict);
|
|
6066
|
+
});
|
|
6067
|
+
const staleOutpoints = /* @__PURE__ */ new Set();
|
|
6068
|
+
for (const { output, outpoint } of localInputs) {
|
|
6069
|
+
const key = `${outpoint.txid}.${outpoint.vout}`;
|
|
6070
|
+
const verdict = verdicts.get(key);
|
|
6071
|
+
if (verdict == null || verdict === "inconclusive") continue;
|
|
6072
|
+
result.checked++;
|
|
6073
|
+
if (verdict === "stale") {
|
|
6074
|
+
await storage.updateOutput(verifyId(output.outputId), { spendable: false }, trx);
|
|
6075
|
+
result.staleConfirmed++;
|
|
6076
|
+
staleOutpoints.add(key);
|
|
6077
|
+
}
|
|
6078
|
+
}
|
|
6079
|
+
result.staleOutpoints = [...staleOutpoints];
|
|
6080
|
+
if (result.staleConfirmed > 0) logger?.log(`markConfirmedStaleReqInputs: ${result.staleConfirmed} of ${result.checked} local input copy/copies confirmed spent for txid=${req.txid}`);
|
|
6081
|
+
return result;
|
|
6082
|
+
}
|
|
6083
|
+
//#endregion
|
|
5766
6084
|
//#region ../src/storage/methods/attemptToPostReqsToNetwork.ts
|
|
5767
6085
|
/**
|
|
5768
6086
|
* Attempt to post one or more `ProvenTxReq` with status 'unsent'
|
|
@@ -6102,64 +6420,7 @@ async function confirmDoubleSpend(ar, beef, storage, services, logger) {
|
|
|
6102
6420
|
* that were actually evicted (added to history note for diagnostics).
|
|
6103
6421
|
*/
|
|
6104
6422
|
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;
|
|
6423
|
+
return await markConfirmedStaleReqInputs(ar.vreq.req, storage, services, trx, logger);
|
|
6163
6424
|
}
|
|
6164
6425
|
//#endregion
|
|
6165
6426
|
//#region ../src/storage/methods/listCertificates.ts
|
|
@@ -6312,6 +6573,75 @@ function removeDustOutputs(changeOutputs, dustFloor) {
|
|
|
6312
6573
|
largest.satoshis += removed.satoshis;
|
|
6313
6574
|
}
|
|
6314
6575
|
}
|
|
6576
|
+
async function migrateLegacyChangeInputs(request) {
|
|
6577
|
+
const { params, result } = request;
|
|
6578
|
+
if (!params.surplusPoolShaping || result.changeOutputs.length === 0) return;
|
|
6579
|
+
if (request.targetNetCount <= request.netChangeCount()) return;
|
|
6580
|
+
const migrationLimit = params.maxMigrationInputs === -1 ? Number.MAX_SAFE_INTEGER : params.maxMigrationInputs ?? 0;
|
|
6581
|
+
for (let migrated = 0; migrated < migrationLimit; migrated++) {
|
|
6582
|
+
const marginalInputFee = request.feeTarget(1) - request.feeTarget();
|
|
6583
|
+
const candidate = await request.allocateChangeInput(0);
|
|
6584
|
+
if (candidate == null) break;
|
|
6585
|
+
if (candidate.satoshis >= params.changeInitialSatoshis || candidate.satoshis <= marginalInputFee) {
|
|
6586
|
+
await request.releaseChangeInput(candidate.outputId);
|
|
6587
|
+
break;
|
|
6588
|
+
}
|
|
6589
|
+
request.recordAllocatedInput(candidate);
|
|
6590
|
+
}
|
|
6591
|
+
}
|
|
6592
|
+
/**
|
|
6593
|
+
* Materialize the first managed-change output from surplus that is already in
|
|
6594
|
+
* the transaction. This is especially important for explicit/fixed inputs:
|
|
6595
|
+
* their value can fully fund an action before the allocator loop runs, but the
|
|
6596
|
+
* shaping policy must still capture the remainder without gathering another
|
|
6597
|
+
* wallet-managed input.
|
|
6598
|
+
*/
|
|
6599
|
+
function materializeSurplusChangeOutput(request) {
|
|
6600
|
+
const { params, result } = request;
|
|
6601
|
+
if (!params.surplusPoolShaping || result.changeOutputs.length > 0) return;
|
|
6602
|
+
const availableAfterOutputFee = request.feeExcess(0, 1);
|
|
6603
|
+
if (availableAfterOutputFee < request.dustFloor) return;
|
|
6604
|
+
result.changeOutputs.push({
|
|
6605
|
+
satoshis: Math.min(availableAfterOutputFee, Math.max(request.dustFloor, params.changeFirstSatoshis)),
|
|
6606
|
+
lockingScriptLength: params.changeLockingScriptLength
|
|
6607
|
+
});
|
|
6608
|
+
request.feeExcess();
|
|
6609
|
+
}
|
|
6610
|
+
/**
|
|
6611
|
+
* Preserve the historical compatibility retry when another input can make a
|
|
6612
|
+
* viable change output, except when surplus-only shaping has nothing economic
|
|
6613
|
+
* to return. In that case the bounded remainder stays in the miner fee instead
|
|
6614
|
+
* of manufacturing change from an additional wallet input.
|
|
6615
|
+
*/
|
|
6616
|
+
async function requireViableChangeOrRetainBoundedFee(request) {
|
|
6617
|
+
const { params, result } = request;
|
|
6618
|
+
const feeExcessNow = request.feeExcess();
|
|
6619
|
+
if (result.changeOutputs.length > 0 || feeExcessNow <= 0) return;
|
|
6620
|
+
if (params.surplusPoolShaping === true && request.feeExcess(0, 1) < request.dustFloor) return;
|
|
6621
|
+
const minimumChange = Math.max(request.dustFloor, params.changeFirstSatoshis);
|
|
6622
|
+
const totalSatoshisNeeded = request.spending() + request.feeTarget(0, 1) + minimumChange;
|
|
6623
|
+
const moreSatoshisNeeded = Math.max(1, totalSatoshisNeeded - request.funding());
|
|
6624
|
+
await request.releaseAllocatedChangeInputs();
|
|
6625
|
+
throw new WERR_INSUFFICIENT_FUNDS(totalSatoshisNeeded, moreSatoshisNeeded);
|
|
6626
|
+
}
|
|
6627
|
+
function shapeSurplusChangeOutputs(request) {
|
|
6628
|
+
const { params, result } = request;
|
|
6629
|
+
if (!params.surplusPoolShaping || result.changeOutputs.length !== 1) return;
|
|
6630
|
+
if (request.targetNetCount <= request.netChangeCount()) return;
|
|
6631
|
+
const originalSatoshis = result.changeOutputs[0].satoshis;
|
|
6632
|
+
const desiredOutputs = Math.min(request.maxChangeOutputs, Math.max(1, request.targetNetCount + result.allocatedChangeInputs.length));
|
|
6633
|
+
for (let count = desiredOutputs; count > 1; count--) {
|
|
6634
|
+
const addedOutputs = count - 1;
|
|
6635
|
+
const distributable = originalSatoshis - (request.feeTarget(0, addedOutputs) - request.feeTarget());
|
|
6636
|
+
if (distributable < count * params.changeInitialSatoshis) continue;
|
|
6637
|
+
result.changeOutputs = Array.from({ length: count }, () => ({
|
|
6638
|
+
satoshis: params.changeInitialSatoshis,
|
|
6639
|
+
lockingScriptLength: params.changeLockingScriptLength
|
|
6640
|
+
}));
|
|
6641
|
+
distributeExcessFees(result.changeOutputs, params.changeInitialSatoshis, distributable - count * params.changeInitialSatoshis, request.rand);
|
|
6642
|
+
break;
|
|
6643
|
+
}
|
|
6644
|
+
}
|
|
6315
6645
|
/**
|
|
6316
6646
|
* Simplifications:
|
|
6317
6647
|
* - only support one change type with fixed length scripts.
|
|
@@ -6383,7 +6713,8 @@ async function generateChangeSdkCore(params, allocateChangeInput, releaseChangeI
|
|
|
6383
6713
|
* Applies the per-transaction limit so that the UTXO pool grows
|
|
6384
6714
|
* gradually rather than all at once.
|
|
6385
6715
|
*/
|
|
6386
|
-
const maxChangeOutputs = params.maxChangeOutputs ?? 8;
|
|
6716
|
+
const maxChangeOutputs = params.maxChangeOutputs === -1 ? Number.MAX_SAFE_INTEGER : params.maxChangeOutputs ?? 8;
|
|
6717
|
+
const surplusPoolShaping = params.surplusPoolShaping === true;
|
|
6387
6718
|
const randomVals = [...params.randomVals || []];
|
|
6388
6719
|
const nextRandomVal = () => {
|
|
6389
6720
|
let val = 0;
|
|
@@ -6466,6 +6797,7 @@ async function generateChangeSdkCore(params, allocateChangeInput, releaseChangeI
|
|
|
6466
6797
|
return r.changeOutputs.length - r.allocatedChangeInputs.length;
|
|
6467
6798
|
};
|
|
6468
6799
|
const addOutputToBalanceNewInput = () => {
|
|
6800
|
+
if (surplusPoolShaping) return false;
|
|
6469
6801
|
if (!hasTargetNetCount) return false;
|
|
6470
6802
|
if (r.changeOutputs.length >= maxChangeOutputs) return false;
|
|
6471
6803
|
return netChangeCount() - 1 < targetNetCount;
|
|
@@ -6481,6 +6813,7 @@ async function generateChangeSdkCore(params, allocateChangeInput, releaseChangeI
|
|
|
6481
6813
|
feeExcessNow = feeExcess();
|
|
6482
6814
|
};
|
|
6483
6815
|
const addDesiredChangeOutputs = () => {
|
|
6816
|
+
if (surplusPoolShaping) return;
|
|
6484
6817
|
while (r.changeOutputs.length < maxChangeOutputs && (hasTargetNetCount && targetNetCount > netChangeCount() || r.changeOutputs.length === 0 && feeExcess() > 0)) {
|
|
6485
6818
|
const satoshis = r.changeOutputs.length === 0 ? Math.max(dustFloor, params.changeFirstSatoshis) : Math.max(dustFloor, params.changeInitialSatoshis);
|
|
6486
6819
|
r.changeOutputs.push({
|
|
@@ -6496,7 +6829,8 @@ async function generateChangeSdkCore(params, allocateChangeInput, releaseChangeI
|
|
|
6496
6829
|
if (removingOutputs || feeExcess() <= 0) return;
|
|
6497
6830
|
if (!((ao === 1 || r.changeOutputs.length === 0) && r.changeOutputs.length < maxChangeOutputs)) return;
|
|
6498
6831
|
const cap = r.changeOutputs.length === 0 ? params.changeFirstSatoshis : params.changeInitialSatoshis;
|
|
6499
|
-
const
|
|
6832
|
+
const outputFunding = surplusPoolShaping ? feeExcess(0, 1) : feeExcess();
|
|
6833
|
+
const satoshis = Math.min(outputFunding, Math.max(dustFloor, cap));
|
|
6500
6834
|
if (satoshis >= dustFloor) r.changeOutputs.push({
|
|
6501
6835
|
satoshis,
|
|
6502
6836
|
lockingScriptLength: params.changeLockingScriptLength
|
|
@@ -6541,6 +6875,18 @@ async function generateChangeSdkCore(params, allocateChangeInput, releaseChangeI
|
|
|
6541
6875
|
};
|
|
6542
6876
|
}
|
|
6543
6877
|
/**
|
|
6878
|
+
* The action may already be funded entirely by explicit/fixed inputs. In
|
|
6879
|
+
* that case the allocator loop never runs, so capture the existing surplus
|
|
6880
|
+
* here before the no-change compatibility guard. This operation cannot
|
|
6881
|
+
* allocate an input; bounded legacy migration remains a separate step.
|
|
6882
|
+
*/
|
|
6883
|
+
materializeSurplusChangeOutput({
|
|
6884
|
+
params,
|
|
6885
|
+
result: r,
|
|
6886
|
+
dustFloor,
|
|
6887
|
+
feeExcess
|
|
6888
|
+
});
|
|
6889
|
+
/**
|
|
6544
6890
|
* Trigger an account funding event if we don't have enough to cover this transaction.
|
|
6545
6891
|
*/
|
|
6546
6892
|
if (feeExcess() < 0) {
|
|
@@ -6551,19 +6897,63 @@ async function generateChangeSdkCore(params, allocateChangeInput, releaseChangeI
|
|
|
6551
6897
|
}
|
|
6552
6898
|
/**
|
|
6553
6899
|
* If needed, seek funding to avoid overspending on fees without a change output to recapture it.
|
|
6900
|
+
* An economically unreturnable shaping remainder stays in the miner fee;
|
|
6901
|
+
* gathering another input solely to manufacture change would violate
|
|
6902
|
+
* surplus-only shaping and make the action less efficient.
|
|
6554
6903
|
*/
|
|
6555
|
-
|
|
6556
|
-
|
|
6557
|
-
|
|
6558
|
-
|
|
6559
|
-
|
|
6560
|
-
|
|
6561
|
-
|
|
6904
|
+
await requireViableChangeOrRetainBoundedFee({
|
|
6905
|
+
params,
|
|
6906
|
+
result: r,
|
|
6907
|
+
dustFloor,
|
|
6908
|
+
feeExcess,
|
|
6909
|
+
feeTarget,
|
|
6910
|
+
funding,
|
|
6911
|
+
spending,
|
|
6912
|
+
releaseAllocatedChangeInputs
|
|
6913
|
+
});
|
|
6914
|
+
/**
|
|
6915
|
+
* Progressively retire economically useful legacy fragments without ever
|
|
6916
|
+
* making them necessary for the requested action. The target of zero asks
|
|
6917
|
+
* canonical allocators for their smallest remaining output. A candidate at
|
|
6918
|
+
* or above the preferred value is not legacy migration material and is
|
|
6919
|
+
* immediately released.
|
|
6920
|
+
*/
|
|
6921
|
+
await migrateLegacyChangeInputs({
|
|
6922
|
+
params,
|
|
6923
|
+
result: r,
|
|
6924
|
+
targetNetCount,
|
|
6925
|
+
netChangeCount,
|
|
6926
|
+
feeTarget,
|
|
6927
|
+
allocateChangeInput,
|
|
6928
|
+
releaseChangeInput,
|
|
6929
|
+
recordAllocatedInput: (candidate) => {
|
|
6930
|
+
r.allocatedChangeInputs.push(candidate);
|
|
6931
|
+
allocatedFunding += candidate.satoshis;
|
|
6932
|
+
feeExcessNow = feeExcess();
|
|
6933
|
+
}
|
|
6934
|
+
});
|
|
6562
6935
|
/**
|
|
6563
6936
|
* Distribute the excess fees across the changeOutputs added.
|
|
6564
6937
|
*/
|
|
6565
6938
|
feeExcessNow = distributeExcessFees(r.changeOutputs, params.changeInitialSatoshis, feeExcessNow, rand);
|
|
6566
6939
|
/**
|
|
6940
|
+
* Pool growth is funded only from the surplus already present in the
|
|
6941
|
+
* transaction. Splitting one change output increases the serialized fee;
|
|
6942
|
+
* that exact delta is deducted before assigning the new outputs. If the
|
|
6943
|
+
* preferred minimum cannot be met, the transaction retains one smaller
|
|
6944
|
+
* output instead of gathering more inputs or refusing an otherwise valid
|
|
6945
|
+
* action.
|
|
6946
|
+
*/
|
|
6947
|
+
shapeSurplusChangeOutputs({
|
|
6948
|
+
params,
|
|
6949
|
+
result: r,
|
|
6950
|
+
targetNetCount,
|
|
6951
|
+
netChangeCount,
|
|
6952
|
+
maxChangeOutputs,
|
|
6953
|
+
feeTarget,
|
|
6954
|
+
rand
|
|
6955
|
+
});
|
|
6956
|
+
/**
|
|
6567
6957
|
* Remove any change outputs that ended up below the dust floor after distribution.
|
|
6568
6958
|
* Consolidates removed satoshis into the largest remaining output.
|
|
6569
6959
|
*/
|
|
@@ -6594,7 +6984,11 @@ function validateGenerateChangeSdkResult(params, r) {
|
|
|
6594
6984
|
ok = false;
|
|
6595
6985
|
}
|
|
6596
6986
|
const feeRequired = Math.ceil((r.size || 0) / 1e3 * (r.satsPerKb || 0));
|
|
6597
|
-
|
|
6987
|
+
const minSpendTxSize = transactionSize([params.changeUnlockingScriptLength], [params.changeLockingScriptLength]);
|
|
6988
|
+
const dustFloor = Math.max(1, Math.ceil(minSpendTxSize / 1e3 * (r.satsPerKb || 0)) * 2);
|
|
6989
|
+
const feeWithChangeOutput = Math.ceil(((r.size || 0) + transactionOutputSize(params.changeLockingScriptLength)) / 1e3 * (r.satsPerKb || 0));
|
|
6990
|
+
const isBoundedUnreturnableShapingSurplus = params.surplusPoolShaping === true && r.changeOutputs.length === 0 && r.fee > feeRequired && r.fee - feeWithChangeOutput < dustFloor;
|
|
6991
|
+
if (feeRequired !== r.fee && !isBoundedUnreturnableShapingSurplus) {
|
|
6598
6992
|
log += `required fee error ${feeRequired} !== ${r.fee};`;
|
|
6599
6993
|
ok = false;
|
|
6600
6994
|
}
|
|
@@ -6632,6 +7026,8 @@ function validateGenerateChangeSdkParams(params) {
|
|
|
6632
7026
|
params.feeModel = validateStorageFeeModel(params.feeModel);
|
|
6633
7027
|
if (params.feeModel.model !== "sat/kb") throw new WERR_INVALID_PARAMETER("feeModel.model", "'sat/kb'");
|
|
6634
7028
|
_bsv_sdk.Validation.validateOptionalInteger(params.targetNetCount, "targetNetCount");
|
|
7029
|
+
if (params.maxChangeOutputs !== -1) _bsv_sdk.Validation.validateOptionalInteger(params.maxChangeOutputs, "maxChangeOutputs", 1);
|
|
7030
|
+
if (params.maxMigrationInputs !== -1) _bsv_sdk.Validation.validateOptionalInteger(params.maxMigrationInputs, "maxMigrationInputs", 0);
|
|
6635
7031
|
_bsv_sdk.Validation.validateSatoshis(params.changeFirstSatoshis, "changeFirstSatoshis", 1);
|
|
6636
7032
|
_bsv_sdk.Validation.validateSatoshis(params.changeInitialSatoshis, "changeInitialSatoshis", 1);
|
|
6637
7033
|
_bsv_sdk.Validation.validateInteger(params.changeLockingScriptLength, "changeLockingScriptLength");
|
|
@@ -8099,6 +8495,8 @@ async function planFunding(state, args, explicit, noSendChange) {
|
|
|
8099
8495
|
const allocated = /* @__PURE__ */ new Map();
|
|
8100
8496
|
const noSend = [...noSendChange];
|
|
8101
8497
|
const changeBasket = state.begin.changeBasket;
|
|
8498
|
+
const policy = validateManagedChangePolicy(state.begin.managedChangePolicy);
|
|
8499
|
+
const preferredSatoshis = Math.max(1, changeBasket.minimumDesiredUTXOValue);
|
|
8102
8500
|
const params = {
|
|
8103
8501
|
fixedInputs: explicit.map((output, index) => ({
|
|
8104
8502
|
satoshis: output.satoshis,
|
|
@@ -8112,11 +8510,14 @@ async function planFunding(state, args, explicit, noSendChange) {
|
|
|
8112
8510
|
lockingScriptLength: 25
|
|
8113
8511
|
}] : []],
|
|
8114
8512
|
feeModel: state.begin.feeModel,
|
|
8115
|
-
changeInitialSatoshis:
|
|
8116
|
-
changeFirstSatoshis:
|
|
8513
|
+
changeInitialSatoshis: preferredSatoshis,
|
|
8514
|
+
changeFirstSatoshis: preferredSatoshis,
|
|
8117
8515
|
changeLockingScriptLength: 25,
|
|
8118
8516
|
changeUnlockingScriptLength: 107,
|
|
8119
8517
|
targetNetCount: changeBasket.numberOfDesiredUTXOs - state.estimatedChangeCount,
|
|
8518
|
+
maxChangeOutputs: policy.maxOutputsPerAction,
|
|
8519
|
+
surplusPoolShaping: true,
|
|
8520
|
+
maxMigrationInputs: policy.migrationInputsPerAction,
|
|
8120
8521
|
randomVals: args.randomVals
|
|
8121
8522
|
};
|
|
8122
8523
|
const allocate = async (targetSatoshis, exactSatoshis) => {
|
|
@@ -8135,7 +8536,18 @@ async function planFunding(state, args, explicit, noSendChange) {
|
|
|
8135
8536
|
allocated.delete(outputId);
|
|
8136
8537
|
if (noSendChange.includes(output)) noSend.push(output);
|
|
8137
8538
|
};
|
|
8138
|
-
|
|
8539
|
+
let result;
|
|
8540
|
+
try {
|
|
8541
|
+
result = await generateChangeSdk(params, allocate, release, args.logger);
|
|
8542
|
+
} catch (error) {
|
|
8543
|
+
if (!(error instanceof WERR_INSUFFICIENT_FUNDS)) throw error;
|
|
8544
|
+
result = await generateChangeSdk({
|
|
8545
|
+
...params,
|
|
8546
|
+
changeFirstSatoshis: 1,
|
|
8547
|
+
surplusPoolShaping: false,
|
|
8548
|
+
maxMigrationInputs: 0
|
|
8549
|
+
}, allocate, release, args.logger);
|
|
8550
|
+
}
|
|
8139
8551
|
return {
|
|
8140
8552
|
allocated: result.allocatedChangeInputs.map((input) => verifyTruthy(allocated.get(input.outputId))),
|
|
8141
8553
|
changeSatoshis: result.changeOutputs.map((output) => output.satoshis),
|
|
@@ -8393,6 +8805,19 @@ async function compressActionBatchPackItems(items, encoding, maxBytes, maxItems)
|
|
|
8393
8805
|
function mergeUnique(values) {
|
|
8394
8806
|
return [...new Set(values)];
|
|
8395
8807
|
}
|
|
8808
|
+
function isActionBatchStateError(error) {
|
|
8809
|
+
if (error instanceof WERRActionBatchState) return true;
|
|
8810
|
+
if (error == null || typeof error !== "object") return false;
|
|
8811
|
+
const candidate = error;
|
|
8812
|
+
return (candidate.name === "WERR_ACTION_BATCH_STATE" || candidate.code === "WERR_ACTION_BATCH_STATE") && typeof candidate.state === "string";
|
|
8813
|
+
}
|
|
8814
|
+
function isActionBatchCapacityError(error) {
|
|
8815
|
+
if (error == null || typeof error !== "object") return false;
|
|
8816
|
+
const candidate = error;
|
|
8817
|
+
const code = candidate.name === "Error" ? candidate.code : candidate.name ?? candidate.code;
|
|
8818
|
+
if (code === "WERR_INVALID_PARAMETER" && candidate.parameter === "firstAction") return true;
|
|
8819
|
+
return code === "WERR_INVALID_OPERATION" && typeof candidate.message === "string" && candidate.message.includes("action batch") && (candidate.message.includes("maximum") || candidate.message.includes("reserve"));
|
|
8820
|
+
}
|
|
8396
8821
|
function nextGeometricTarget(value) {
|
|
8397
8822
|
return Math.min(Number.MAX_SAFE_INTEGER, value * 2);
|
|
8398
8823
|
}
|
|
@@ -8439,6 +8864,7 @@ var ActionBatchWorkspace = class {
|
|
|
8439
8864
|
planned = /* @__PURE__ */ new Map();
|
|
8440
8865
|
actions = [];
|
|
8441
8866
|
lockingScripts = /* @__PURE__ */ new Map();
|
|
8867
|
+
canResume;
|
|
8442
8868
|
ewmaConfirmedInputs = 1;
|
|
8443
8869
|
ewmaConfirmedSatoshis = 0;
|
|
8444
8870
|
hasConfirmedSample = false;
|
|
@@ -8458,6 +8884,7 @@ var ActionBatchWorkspace = class {
|
|
|
8458
8884
|
this.batchId = begin.batchId;
|
|
8459
8885
|
this.state = makePlannerState(begin, firstAction);
|
|
8460
8886
|
this.expiresAt = Date.parse(begin.expiresAt);
|
|
8887
|
+
this.canResume = capabilities.resume === true && wallet.storage.resumeActionBatch != null;
|
|
8461
8888
|
this.eagerUploadLanes = Array.from({ length: capabilities.packedUploads == null ? 1 : Math.max(1, capabilities.maxConcurrentUploads) }, async () => {});
|
|
8462
8889
|
}
|
|
8463
8890
|
get usesCompactManifest() {
|
|
@@ -8471,6 +8898,15 @@ var ActionBatchWorkspace = class {
|
|
|
8471
8898
|
if (this.actions.some((action) => action.reference === reference || action.txid === reference)) return true;
|
|
8472
8899
|
return Object.entries(this.wallet.pendingSignActions).some(([pendingReference, pending]) => this.planned.has(pendingReference) && pending.tx.id("hex") === reference);
|
|
8473
8900
|
}
|
|
8901
|
+
ownsTransaction(txid) {
|
|
8902
|
+
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);
|
|
8903
|
+
}
|
|
8904
|
+
references(args) {
|
|
8905
|
+
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));
|
|
8906
|
+
}
|
|
8907
|
+
referencesSendWith(txids) {
|
|
8908
|
+
return txids.some((txid) => this.ownsTransaction(txid));
|
|
8909
|
+
}
|
|
8474
8910
|
get isEmpty() {
|
|
8475
8911
|
return this.planned.size === 0 && this.actions.length === 0;
|
|
8476
8912
|
}
|
|
@@ -8595,12 +9031,32 @@ var ActionBatchWorkspace = class {
|
|
|
8595
9031
|
satoshis: extension.reservedOutputs.reduce((sum, output) => sum + output.satoshis, 0)
|
|
8596
9032
|
};
|
|
8597
9033
|
}
|
|
9034
|
+
async resume() {
|
|
9035
|
+
const outpoints = [...new Map([...this.state.reserved.values(), ...this.state.explicit.values()].filter((output) => output.txid != null).map((output) => [`${output.txid}.${output.vout}`, {
|
|
9036
|
+
txid: output.txid,
|
|
9037
|
+
vout: output.vout
|
|
9038
|
+
}])).values()];
|
|
9039
|
+
const result = await this.wallet.storage.resumeActionBatch({
|
|
9040
|
+
batchId: this.batchId,
|
|
9041
|
+
outpoints
|
|
9042
|
+
});
|
|
9043
|
+
this.expiresAt = Date.parse(result.expiresAt);
|
|
9044
|
+
}
|
|
8598
9045
|
async renewIfNeeded() {
|
|
8599
|
-
if (Date.now() >= this.expiresAt)
|
|
9046
|
+
if (Date.now() >= this.expiresAt) {
|
|
9047
|
+
if (this.canResume) await this.resume();
|
|
9048
|
+
return;
|
|
9049
|
+
}
|
|
8600
9050
|
const renewAt = this.expiresAt - this.capabilities.leaseMs * .2;
|
|
8601
9051
|
if (Date.now() < renewAt) return;
|
|
8602
9052
|
this.renewal ??= this.wallet.storage.renewActionBatch(this.batchId).then((result) => {
|
|
8603
9053
|
this.expiresAt = Date.parse(result.expiresAt);
|
|
9054
|
+
}).catch(async (error) => {
|
|
9055
|
+
if (isActionBatchStateError(error) && error.state === "expired") {
|
|
9056
|
+
if (this.canResume) await this.resume();
|
|
9057
|
+
return;
|
|
9058
|
+
}
|
|
9059
|
+
throw error;
|
|
8604
9060
|
}).finally(() => {
|
|
8605
9061
|
this.renewal = void 0;
|
|
8606
9062
|
});
|
|
@@ -9031,24 +9487,42 @@ var ActionBatchController = class {
|
|
|
9031
9487
|
this.workspace = new ActionBatchWorkspace(this.wallet, begin, args, capabilities);
|
|
9032
9488
|
return this.workspace;
|
|
9033
9489
|
}
|
|
9490
|
+
async retire(workspace) {
|
|
9491
|
+
await workspace.abort().catch(() => void 0);
|
|
9492
|
+
if (this.workspace === workspace) this.workspace = void 0;
|
|
9493
|
+
}
|
|
9494
|
+
async recover(workspace, input, isDelayed = false, resume = false) {
|
|
9495
|
+
try {
|
|
9496
|
+
if (resume) await workspace.resume();
|
|
9497
|
+
return Array.isArray(input) ? await workspace.commit(input, isDelayed) : await workspace.plan(input);
|
|
9498
|
+
} catch (error) {
|
|
9499
|
+
if (!resume && isActionBatchStateError(error) && error.state === "expired" && workspace.canResume) return await this.recover(workspace, input, isDelayed, true);
|
|
9500
|
+
if (isActionBatchStateError(error) && error.state !== "expired") await this.retire(workspace);
|
|
9501
|
+
throw error;
|
|
9502
|
+
}
|
|
9503
|
+
}
|
|
9034
9504
|
async plan(args) {
|
|
9035
9505
|
return await this.runExclusive(async () => {
|
|
9036
9506
|
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;
|
|
9507
|
+
const workspace = this.workspace;
|
|
9508
|
+
if (workspace != null) {
|
|
9509
|
+
if (!workspace.references(args)) return void 0;
|
|
9510
|
+
return await this.recover(workspace, args);
|
|
9044
9511
|
}
|
|
9512
|
+
if (!args.isNoSend) return void 0;
|
|
9513
|
+
let begun;
|
|
9045
9514
|
try {
|
|
9046
|
-
|
|
9515
|
+
begun = await this.begin(args);
|
|
9047
9516
|
} catch (error) {
|
|
9048
|
-
if (
|
|
9049
|
-
|
|
9050
|
-
|
|
9051
|
-
|
|
9517
|
+
if (isActionBatchCapacityError(error)) return void 0;
|
|
9518
|
+
throw error;
|
|
9519
|
+
}
|
|
9520
|
+
if (begun == null) return void 0;
|
|
9521
|
+
try {
|
|
9522
|
+
return await begun.plan(args);
|
|
9523
|
+
} catch (error) {
|
|
9524
|
+
await this.retire(begun);
|
|
9525
|
+
if (isActionBatchCapacityError(error)) return void 0;
|
|
9052
9526
|
throw error;
|
|
9053
9527
|
}
|
|
9054
9528
|
});
|
|
@@ -9058,11 +9532,21 @@ var ActionBatchController = class {
|
|
|
9058
9532
|
const workspace = this.workspace;
|
|
9059
9533
|
if (workspace == null) return void 0;
|
|
9060
9534
|
if (prior != null && !workspace.ownsReference(prior.reference)) return void 0;
|
|
9535
|
+
if (prior == null && !workspace.referencesSendWith(args.options.sendWith)) return void 0;
|
|
9061
9536
|
if (prior != null) workspace.stage(prior, args);
|
|
9062
|
-
|
|
9537
|
+
const referencesWorkspace = workspace.referencesSendWith(args.options.sendWith);
|
|
9538
|
+
if (prior != null && args.isNoSend && args.isSendWith && !referencesWorkspace) return await this.wallet.storage.processAction({
|
|
9539
|
+
isNewTx: false,
|
|
9540
|
+
isSendWith: true,
|
|
9541
|
+
isNoSend: false,
|
|
9542
|
+
isDelayed: args.isDelayed,
|
|
9543
|
+
sendWith: [...args.options.sendWith],
|
|
9544
|
+
logger: args.logger
|
|
9545
|
+
});
|
|
9546
|
+
if (!(referencesWorkspace || prior != null && !args.isNoSend)) return { sendWithResults: [] };
|
|
9063
9547
|
const sendWith = [...args.options.sendWith];
|
|
9064
9548
|
if (prior != null && !args.isNoSend && !sendWith.includes(prior.tx.id("hex"))) sendWith.push(prior.tx.id("hex"));
|
|
9065
|
-
const result = await
|
|
9549
|
+
const result = await this.recover(workspace, sendWith, args.isDelayed);
|
|
9066
9550
|
this.workspace = void 0;
|
|
9067
9551
|
return result;
|
|
9068
9552
|
});
|
|
@@ -9806,14 +10290,15 @@ var Wallet = class {
|
|
|
9806
10290
|
}
|
|
9807
10291
|
/**
|
|
9808
10292
|
* Uses `listOutputs` special operation to review the spendability via `Services` of
|
|
9809
|
-
* outputs currently considered spendable. Returns
|
|
10293
|
+
* outputs currently considered spendable. Returns only outputs conclusively
|
|
10294
|
+
* confirmed spent. Rejects the review if any provider result is inconclusive.
|
|
9810
10295
|
*
|
|
9811
10296
|
* Ignores the `limit` and `offset` properties.
|
|
9812
10297
|
*
|
|
9813
10298
|
* @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
|
|
10299
|
+
* @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
10300
|
* @param optionalArgs Optional. Additional tags will constrain the outputs processed.
|
|
9816
|
-
* @returns outputs
|
|
10301
|
+
* @returns outputs previously considered spendable but conclusively confirmed spent.
|
|
9817
10302
|
*/
|
|
9818
10303
|
async reviewSpendableOutputs(all = false, release = false, optionalArgs) {
|
|
9819
10304
|
const args = {
|
|
@@ -10021,16 +10506,15 @@ async function createActionCore(storage, auth, vargs, parent) {
|
|
|
10021
10506
|
});
|
|
10022
10507
|
const feeModel = validateStorageFeeModel(storage.feeModel);
|
|
10023
10508
|
logger?.log(`validated fee model ${JSON.stringify(feeModel)}`);
|
|
10024
|
-
const initialFundingPlan = await
|
|
10509
|
+
const initialFundingPlan = await prepareFundingPlanWithLiquidityPolicy(storage, [
|
|
10025
10510
|
userId,
|
|
10026
10511
|
vargs,
|
|
10027
10512
|
xinputs,
|
|
10028
10513
|
xoutputs,
|
|
10029
10514
|
changeBasket,
|
|
10030
10515
|
noSendChangeIn,
|
|
10031
|
-
feeModel
|
|
10032
|
-
|
|
10033
|
-
]);
|
|
10516
|
+
feeModel
|
|
10517
|
+
], parent);
|
|
10034
10518
|
logger?.log(`planned funding from ${initialFundingPlan.availableChangeCount} change inputs`);
|
|
10035
10519
|
const allocatedBeefPrefetch = startAllocatedChangeBeefPrefetch(storage, vargs, initialFundingPlan.selected, beef, parent);
|
|
10036
10520
|
const storageBeefBytes = storageBeef.toBinary();
|
|
@@ -10610,7 +11094,9 @@ async function traceStorageStep(storage, name, parent, attributes, callback) {
|
|
|
10610
11094
|
attributes
|
|
10611
11095
|
}, async (span) => await callback(span));
|
|
10612
11096
|
}
|
|
10613
|
-
function makeFundingParams(
|
|
11097
|
+
function makeFundingParams(args) {
|
|
11098
|
+
const { storage, vargs, xinputs, xoutputs, changeBasket, feeModel, healthyChangeCount, compatibilityFallback } = args;
|
|
11099
|
+
const preferredSatoshis = Math.max(1, changeBasket.minimumDesiredUTXOValue);
|
|
10614
11100
|
return {
|
|
10615
11101
|
fixedInputs: xinputs.map((input) => ({
|
|
10616
11102
|
satoshis: input.satoshis,
|
|
@@ -10621,36 +11107,52 @@ function makeFundingParams(vargs, xinputs, xoutputs, changeBasket, feeModel, ava
|
|
|
10621
11107
|
lockingScriptLength: output.lockingScript.length / 2
|
|
10622
11108
|
})),
|
|
10623
11109
|
feeModel,
|
|
10624
|
-
changeInitialSatoshis:
|
|
10625
|
-
changeFirstSatoshis:
|
|
11110
|
+
changeInitialSatoshis: preferredSatoshis,
|
|
11111
|
+
changeFirstSatoshis: compatibilityFallback ? 1 : preferredSatoshis,
|
|
10626
11112
|
changeLockingScriptLength: 25,
|
|
10627
11113
|
changeUnlockingScriptLength: 107,
|
|
10628
|
-
targetNetCount: changeBasket.numberOfDesiredUTXOs -
|
|
11114
|
+
targetNetCount: changeBasket.numberOfDesiredUTXOs - healthyChangeCount,
|
|
11115
|
+
maxChangeOutputs: storage.managedChangePolicy.maxOutputsPerAction,
|
|
11116
|
+
surplusPoolShaping: !compatibilityFallback,
|
|
11117
|
+
maxMigrationInputs: compatibilityFallback ? 0 : storage.managedChangePolicy.migrationInputsPerAction,
|
|
10629
11118
|
randomVals: vargs.randomVals
|
|
10630
11119
|
};
|
|
10631
11120
|
}
|
|
10632
|
-
async function
|
|
10633
|
-
const [userId, vargs, xinputs, xoutputs, changeBasket, noSendChangeIn, feeModel
|
|
10634
|
-
const excludeSending = !vargs.isDelayed;
|
|
10635
|
-
const candidates = await traceStorageStep(storage, "wallet.storage.create_action.funding_candidates", parent, { "funding.exclude_sending": excludeSending }, async (span) => {
|
|
10636
|
-
const outputs = await storage.findAvailableManagedChangeInputCandidates(userId, changeBasket.basketId, excludeSending, trx);
|
|
10637
|
-
span?.end({ attributes: {
|
|
10638
|
-
"funding.candidate_count": outputs.length,
|
|
10639
|
-
"funding.candidate_satoshis": outputs.reduce((sum, output) => sum + output.satoshis, 0)
|
|
10640
|
-
} });
|
|
10641
|
-
return outputs;
|
|
10642
|
-
});
|
|
11121
|
+
async function buildFundingPlan(storage, context, candidates, eligibleStatuses, policyTier, compatibilityFallback, parent) {
|
|
11122
|
+
const [userId, vargs, xinputs, xoutputs, changeBasket, noSendChangeIn, feeModel] = context;
|
|
10643
11123
|
const noSendIds = new Set(noSendChangeIn.map((output) => output.outputId));
|
|
10644
|
-
const available = candidates.filter((output) => !noSendIds.has(output.outputId));
|
|
10645
|
-
const
|
|
11124
|
+
const available = candidates.filter((output) => !noSendIds.has(output.outputId) && eligibleStatuses.includes(output.transactionStatus));
|
|
11125
|
+
const preferredSatoshis = Math.max(1, changeBasket.minimumDesiredUTXOValue);
|
|
11126
|
+
const params = makeFundingParams({
|
|
11127
|
+
storage,
|
|
11128
|
+
vargs,
|
|
11129
|
+
xinputs,
|
|
11130
|
+
xoutputs,
|
|
11131
|
+
changeBasket,
|
|
11132
|
+
feeModel,
|
|
11133
|
+
healthyChangeCount: compatibilityFallback ? candidates.filter((output) => eligibleStatuses.includes(output.transactionStatus)).length : candidates.filter((output) => output.satoshis >= preferredSatoshis).length,
|
|
11134
|
+
compatibilityFallback
|
|
11135
|
+
});
|
|
11136
|
+
const noSendStatuses = await storage.findTransactionStatusesByIds(userId, noSendChangeIn.map((output) => output.transactionId));
|
|
11137
|
+
const plannedNoSend = noSendChangeIn.map((output) => ({
|
|
11138
|
+
outputId: output.outputId,
|
|
11139
|
+
transactionId: output.transactionId,
|
|
11140
|
+
satoshis: output.satoshis,
|
|
11141
|
+
txid: output.txid,
|
|
11142
|
+
vout: output.vout,
|
|
11143
|
+
transactionStatus: noSendStatuses.get(output.transactionId) ?? "nosend"
|
|
11144
|
+
}));
|
|
11145
|
+
const claimStatuses = [.../* @__PURE__ */ new Set([...eligibleStatuses, ...plannedNoSend.map((output) => output.transactionStatus)])];
|
|
10646
11146
|
return await traceStorageStep(storage, "wallet.storage.create_action.funding_plan", parent, {
|
|
10647
11147
|
"funding.candidate_count": available.length,
|
|
10648
|
-
"funding.no_send_change_count": noSendChangeIn.length
|
|
11148
|
+
"funding.no_send_change_count": noSendChangeIn.length,
|
|
11149
|
+
"funding.policy_tier": policyTier,
|
|
11150
|
+
"funding.compatibility_fallback": compatibilityFallback
|
|
10649
11151
|
}, async (span) => {
|
|
10650
11152
|
const allocated = /* @__PURE__ */ new Map();
|
|
10651
11153
|
const availableSelector = new CanonicalChangeSelector(available);
|
|
10652
|
-
const noSend = [...
|
|
10653
|
-
const noSendById = new Map(
|
|
11154
|
+
const noSend = [...plannedNoSend];
|
|
11155
|
+
const noSendById = new Map(plannedNoSend.map((output) => [output.outputId, output]));
|
|
10654
11156
|
const allocate = async (targetSatoshis, exactSatoshis) => {
|
|
10655
11157
|
let output = noSend.pop();
|
|
10656
11158
|
output ??= availableSelector.take(targetSatoshis, exactSatoshis);
|
|
@@ -10680,12 +11182,119 @@ async function prepareFundingPlan(storage, context) {
|
|
|
10680
11182
|
params,
|
|
10681
11183
|
result,
|
|
10682
11184
|
selected,
|
|
10683
|
-
availableChangeCount: candidates.length
|
|
11185
|
+
availableChangeCount: candidates.length,
|
|
11186
|
+
eligibleStatuses: claimStatuses,
|
|
11187
|
+
policyTier,
|
|
11188
|
+
compatibilityFallback
|
|
10684
11189
|
};
|
|
10685
11190
|
});
|
|
10686
11191
|
}
|
|
11192
|
+
async function fundingPlanSerializedCost(storage, plan, knownTxids) {
|
|
11193
|
+
const txids = [...new Set(plan.selected.map((candidate) => verifyTruthy(candidate.txid)))];
|
|
11194
|
+
if (txids.length === 0) return plan.result.size;
|
|
11195
|
+
try {
|
|
11196
|
+
const beef = await storage.getBeefForTransactions(txids, {
|
|
11197
|
+
knownTxids,
|
|
11198
|
+
ignoreStorage: false,
|
|
11199
|
+
ignoreServices: true,
|
|
11200
|
+
ignoreNewProven: false
|
|
11201
|
+
});
|
|
11202
|
+
return plan.result.size + beef.toUint8Array().length;
|
|
11203
|
+
} catch {
|
|
11204
|
+
return Number.MAX_SAFE_INTEGER;
|
|
11205
|
+
}
|
|
11206
|
+
}
|
|
11207
|
+
const FUNDING_TIERS = [
|
|
11208
|
+
{
|
|
11209
|
+
policyTier: "completed",
|
|
11210
|
+
statuses: ["completed"]
|
|
11211
|
+
},
|
|
11212
|
+
{
|
|
11213
|
+
policyTier: "unproven",
|
|
11214
|
+
statuses: ["completed", "unproven"]
|
|
11215
|
+
},
|
|
11216
|
+
{
|
|
11217
|
+
policyTier: "sending",
|
|
11218
|
+
statuses: [
|
|
11219
|
+
"completed",
|
|
11220
|
+
"unproven",
|
|
11221
|
+
"sending"
|
|
11222
|
+
]
|
|
11223
|
+
}
|
|
11224
|
+
];
|
|
11225
|
+
async function resolveFundingCandidates(storage, userId, basketId, parent, trx) {
|
|
11226
|
+
const rawCandidates = await traceStorageStep(storage, "wallet.storage.create_action.funding_candidates", parent, { "funding.include_pending": true }, async (span) => {
|
|
11227
|
+
const outputs = await storage.findAvailableManagedChangeInputCandidates(userId, basketId, false, trx);
|
|
11228
|
+
span?.end({ attributes: {
|
|
11229
|
+
"funding.candidate_count": outputs.length,
|
|
11230
|
+
"funding.candidate_satoshis": outputs.reduce((sum, output) => sum + output.satoshis, 0),
|
|
11231
|
+
"funding.completed_candidate_count": outputs.filter((output) => output.transactionStatus === "completed").length,
|
|
11232
|
+
"funding.unproven_candidate_count": outputs.filter((output) => output.transactionStatus === "unproven").length,
|
|
11233
|
+
"funding.sending_candidate_count": outputs.filter((output) => output.transactionStatus === "sending").length
|
|
11234
|
+
} });
|
|
11235
|
+
return outputs;
|
|
11236
|
+
});
|
|
11237
|
+
const missingStatusIds = rawCandidates.filter((output) => output.transactionStatus == null).map((output) => output.transactionId);
|
|
11238
|
+
const missingStatuses = await storage.findTransactionStatusesByIds(userId, missingStatusIds, trx);
|
|
11239
|
+
const candidates = rawCandidates.map((output) => ({
|
|
11240
|
+
...output,
|
|
11241
|
+
transactionStatus: output.transactionStatus ?? missingStatuses.get(output.transactionId)
|
|
11242
|
+
}));
|
|
11243
|
+
if (candidates.some((output) => output.transactionStatus == null)) throw new WERR_INTERNAL("managed change candidate is missing its source transaction status");
|
|
11244
|
+
return candidates;
|
|
11245
|
+
}
|
|
11246
|
+
async function buildFundingPlanForTier(storage, context, candidates, tier, parent) {
|
|
11247
|
+
try {
|
|
11248
|
+
return { plan: await buildFundingPlan(storage, context, candidates, tier.statuses, tier.policyTier, false, parent) };
|
|
11249
|
+
} catch (error) {
|
|
11250
|
+
if (!(error instanceof WERR_INSUFFICIENT_FUNDS)) throw error;
|
|
11251
|
+
}
|
|
11252
|
+
try {
|
|
11253
|
+
return { plan: await buildFundingPlan(storage, context, candidates, tier.statuses, "compatibility", true, parent) };
|
|
11254
|
+
} catch (error) {
|
|
11255
|
+
if (!(error instanceof WERR_INSUFFICIENT_FUNDS)) throw error;
|
|
11256
|
+
return { fundingError: error };
|
|
11257
|
+
}
|
|
11258
|
+
}
|
|
11259
|
+
function firstPlanNeedsNoPendingComparison(storage, plan) {
|
|
11260
|
+
const threshold = storage.managedChangePolicy.pendingComparisonInputs;
|
|
11261
|
+
return threshold === -1 || plan.selected.length <= threshold;
|
|
11262
|
+
}
|
|
11263
|
+
async function chooseLowestSerializedFundingPlan(storage, plans, knownTxids) {
|
|
11264
|
+
let chosen = plans[0];
|
|
11265
|
+
let chosenCost = await fundingPlanSerializedCost(storage, chosen, knownTxids);
|
|
11266
|
+
for (const alternative of plans.slice(1)) {
|
|
11267
|
+
const cost = await fundingPlanSerializedCost(storage, alternative, knownTxids);
|
|
11268
|
+
if (cost < chosenCost) {
|
|
11269
|
+
chosen = alternative;
|
|
11270
|
+
chosenCost = cost;
|
|
11271
|
+
}
|
|
11272
|
+
}
|
|
11273
|
+
return chosen;
|
|
11274
|
+
}
|
|
11275
|
+
async function prepareFundingPlanWithLiquidityPolicy(storage, context, parent, trx) {
|
|
11276
|
+
const [userId, vargs, , , changeBasket] = context;
|
|
11277
|
+
const candidates = await resolveFundingCandidates(storage, userId, changeBasket.basketId, parent, trx);
|
|
11278
|
+
const successful = [];
|
|
11279
|
+
let fundingError;
|
|
11280
|
+
for (const tier of FUNDING_TIERS) {
|
|
11281
|
+
const attempted = await buildFundingPlanForTier(storage, context, candidates, tier, parent);
|
|
11282
|
+
fundingError = attempted.fundingError ?? fundingError;
|
|
11283
|
+
const plan = attempted.plan;
|
|
11284
|
+
if (plan != null) {
|
|
11285
|
+
successful.push(plan);
|
|
11286
|
+
if (successful.length === 1 && firstPlanNeedsNoPendingComparison(storage, plan)) return plan;
|
|
11287
|
+
}
|
|
11288
|
+
}
|
|
11289
|
+
if (successful.length > 0) {
|
|
11290
|
+
if (successful.length === 1) return successful[0];
|
|
11291
|
+
return await chooseLowestSerializedFundingPlan(storage, successful, vargs.options.knownTxids);
|
|
11292
|
+
}
|
|
11293
|
+
if (fundingError != null) throw fundingError;
|
|
11294
|
+
throw new WERR_INTERNAL("funding policy produced neither a plan nor an error");
|
|
11295
|
+
}
|
|
10687
11296
|
async function claimFundingPlan(storage, request) {
|
|
10688
|
-
const [userId, basketId,
|
|
11297
|
+
const [userId, basketId, eligibleStatuses, transactionId, noSendChangeIn, plan, trx] = request;
|
|
10689
11298
|
if (plan.selected.length === 0) return {
|
|
10690
11299
|
outputs: [],
|
|
10691
11300
|
sourceTransactionCount: 0,
|
|
@@ -10693,10 +11302,8 @@ async function claimFundingPlan(storage, request) {
|
|
|
10693
11302
|
scriptSourceTransactionCount: 0
|
|
10694
11303
|
};
|
|
10695
11304
|
const noSendIds = new Set(noSendChangeIn.map((output) => output.outputId));
|
|
10696
|
-
const statuses = ["completed", "unproven"];
|
|
10697
|
-
if (!excludeSending) statuses.push("sending");
|
|
10698
11305
|
const claim = await storage.transaction(async (claimTrx) => {
|
|
10699
|
-
const currentById = await storage.findFundingOutputsForUpdate(userId, plan.selected.map((output) => output.outputId),
|
|
11306
|
+
const currentById = await storage.findFundingOutputsForUpdate(userId, plan.selected.map((output) => output.outputId), eligibleStatuses, claimTrx);
|
|
10700
11307
|
const transactionIds = [...new Set(Object.values(currentById).map((output) => output.transactionId))];
|
|
10701
11308
|
const claimed = [];
|
|
10702
11309
|
for (const planned of plan.selected) {
|
|
@@ -10766,7 +11373,7 @@ async function fundNewTransactionSdk(storage, userId, vargs, ctx, initialPlan, p
|
|
|
10766
11373
|
const claim = await claimFundingPlan(storage, [
|
|
10767
11374
|
userId,
|
|
10768
11375
|
ctx.changeBasket.basketId,
|
|
10769
|
-
|
|
11376
|
+
plan.eligibleStatuses,
|
|
10770
11377
|
ctx.transactionId,
|
|
10771
11378
|
ctx.noSendChangeIn,
|
|
10772
11379
|
plan,
|
|
@@ -10784,17 +11391,15 @@ async function fundNewTransactionSdk(storage, userId, vargs, ctx, initialPlan, p
|
|
|
10784
11391
|
}
|
|
10785
11392
|
if (claim.conflict === "noSendChange") throw new WERR_INVALID_PARAMETER("noSendChange", "outputs that remain spendable during action planning");
|
|
10786
11393
|
retryCount++;
|
|
10787
|
-
plan = await
|
|
11394
|
+
plan = await prepareFundingPlanWithLiquidityPolicy(storage, [
|
|
10788
11395
|
userId,
|
|
10789
11396
|
vargs,
|
|
10790
11397
|
ctx.xinputs,
|
|
10791
11398
|
ctx.xoutputs,
|
|
10792
11399
|
ctx.changeBasket,
|
|
10793
11400
|
ctx.noSendChangeIn,
|
|
10794
|
-
ctx.feeModel
|
|
10795
|
-
|
|
10796
|
-
trx
|
|
10797
|
-
]);
|
|
11401
|
+
ctx.feeModel
|
|
11402
|
+
], parent, trx);
|
|
10798
11403
|
}
|
|
10799
11404
|
throw new WERR_INVALID_OPERATION("wallet funding changed repeatedly during action planning; retry createAction");
|
|
10800
11405
|
});
|
|
@@ -12939,7 +13544,7 @@ var StorageReaderWriter = class extends StorageReader {
|
|
|
12939
13544
|
userId: user.userId,
|
|
12940
13545
|
name: "default",
|
|
12941
13546
|
numberOfDesiredUTXOs: 144,
|
|
12942
|
-
minimumDesiredUTXOValue:
|
|
13547
|
+
minimumDesiredUTXOValue: DEFAULT_MANAGED_CHANGE_MINIMUM_SATOSHIS,
|
|
12943
13548
|
isDeleted: false
|
|
12944
13549
|
});
|
|
12945
13550
|
break;
|
|
@@ -13220,8 +13825,10 @@ function validateActionBatchInlinePayload(manifest) {
|
|
|
13220
13825
|
for (const { digest, bytes } of inlineBlobs) if (actionBatchBlobDigest(bytes) !== digest) throw new WERR_INVALID_PARAMETER("inlineBlobs", `content matching digest ${digest}`);
|
|
13221
13826
|
}
|
|
13222
13827
|
function requireUploadableBatch(batch) {
|
|
13223
|
-
if (batch == null
|
|
13224
|
-
if (batch.
|
|
13828
|
+
if (batch == null) throw new WERRActionBatchState("missing");
|
|
13829
|
+
if (batch.status === "committed") throw new WERRActionBatchState("committed", batch.batchId);
|
|
13830
|
+
if (batch.status === "aborted") throw new WERRActionBatchState("aborted", batch.batchId);
|
|
13831
|
+
if (batch.hardExpiresAt.getTime() <= Date.now()) throw new WERRActionBatchState("hard-expired", batch.batchId);
|
|
13225
13832
|
return batch;
|
|
13226
13833
|
}
|
|
13227
13834
|
function manifestPhysicalDigests(manifest) {
|
|
@@ -13581,7 +14188,10 @@ const ACTION_BATCH_LEASE_MS = 900 * 1e3;
|
|
|
13581
14188
|
const ACTION_BATCH_HARD_LIFETIME_MS = 3600 * 1e3;
|
|
13582
14189
|
const INITIAL_RESERVATION_LIMIT = 8;
|
|
13583
14190
|
const INITIAL_EXTRA_OUTPUTS = 3;
|
|
13584
|
-
function
|
|
14191
|
+
function isValidOutpoint(outpoint) {
|
|
14192
|
+
return /^[0-9a-f]{64}$/.test(outpoint.txid) && Number.isSafeInteger(outpoint.vout) && outpoint.vout >= 0;
|
|
14193
|
+
}
|
|
14194
|
+
function getActionBatchCapabilities(maxReservedOutputs = 256, supportsResume = false) {
|
|
13585
14195
|
return { actionBatch: {
|
|
13586
14196
|
version: 1,
|
|
13587
14197
|
maxInlineBytes: ACTION_BATCH_MAX_INLINE_BYTES,
|
|
@@ -13589,6 +14199,8 @@ function getActionBatchCapabilities() {
|
|
|
13589
14199
|
maxConcurrentUploads: 4,
|
|
13590
14200
|
leaseMs: ACTION_BATCH_LEASE_MS,
|
|
13591
14201
|
hardLifetimeMs: ACTION_BATCH_HARD_LIFETIME_MS,
|
|
14202
|
+
resume: supportsResume ? true : void 0,
|
|
14203
|
+
maxReservedOutputs,
|
|
13592
14204
|
compactBegin: true,
|
|
13593
14205
|
manifestVersion: 2,
|
|
13594
14206
|
commitByDigest: true,
|
|
@@ -13604,6 +14216,14 @@ function getActionBatchCapabilities() {
|
|
|
13604
14216
|
function activeExpiry(batch, now = /* @__PURE__ */ new Date()) {
|
|
13605
14217
|
return batch.status !== "active" && batch.status !== "prepared" || batch.expiresAt.getTime() <= now.getTime() || batch.hardExpiresAt.getTime() <= now.getTime();
|
|
13606
14218
|
}
|
|
14219
|
+
function actionBatchErrorState(batch, now = /* @__PURE__ */ new Date()) {
|
|
14220
|
+
if (batch == null) return "missing";
|
|
14221
|
+
if (batch.hardExpiresAt.getTime() <= now.getTime()) return "hard-expired";
|
|
14222
|
+
if (batch.status === "aborted") return "aborted";
|
|
14223
|
+
if (batch.status === "committed") return "committed";
|
|
14224
|
+
if (batch.status === "expired" || batch.expiresAt.getTime() <= now.getTime()) return "expired";
|
|
14225
|
+
if (batch.status !== "active" && batch.status !== "prepared") return "inactive";
|
|
14226
|
+
}
|
|
13607
14227
|
function canExpire(batch, now = /* @__PURE__ */ new Date()) {
|
|
13608
14228
|
return (batch.status === "active" || batch.status === "prepared") && (batch.expiresAt.getTime() <= now.getTime() || batch.hardExpiresAt.getTime() <= now.getTime());
|
|
13609
14229
|
}
|
|
@@ -13689,12 +14309,22 @@ function estimateFirstActionTarget(storage, args, inputSatoshis, outputScriptLen
|
|
|
13689
14309
|
const minFee = Math.ceil(transactionSize([...inputLengths, 107], outputLengths) * fee / 1e3);
|
|
13690
14310
|
return Math.max(1, outputSatoshis + minFee - inputSatoshis);
|
|
13691
14311
|
}
|
|
14312
|
+
function selectReservationChange(candidates, targetSatoshis) {
|
|
14313
|
+
for (const status of [
|
|
14314
|
+
"completed",
|
|
14315
|
+
"unproven",
|
|
14316
|
+
"sending"
|
|
14317
|
+
]) {
|
|
14318
|
+
const selected = selectCanonicalChange(candidates.filter((candidate) => candidate.transactionStatus === status), targetSatoshis);
|
|
14319
|
+
if (selected != null) return selected;
|
|
14320
|
+
}
|
|
14321
|
+
}
|
|
13692
14322
|
function chooseReservationPool(candidates, targetSatoshis, limit, extras, fillLimit, planningCosts) {
|
|
13693
14323
|
const remaining = candidates.filter((output) => output.satoshis > planningCosts.marginalInputFee);
|
|
13694
14324
|
const chosen = [];
|
|
13695
14325
|
let deficit = targetSatoshis + planningCosts.firstChangeCost;
|
|
13696
14326
|
while (deficit > 0 && chosen.length < limit) {
|
|
13697
|
-
const output =
|
|
14327
|
+
const output = selectReservationChange(remaining, deficit + planningCosts.marginalInputFee);
|
|
13698
14328
|
if (output == null) break;
|
|
13699
14329
|
chosen.push(output);
|
|
13700
14330
|
remaining.splice(remaining.indexOf(output), 1);
|
|
@@ -13702,14 +14332,18 @@ function chooseReservationPool(candidates, targetSatoshis, limit, extras, fillLi
|
|
|
13702
14332
|
}
|
|
13703
14333
|
const desiredCount = fillLimit ? limit : Math.min(limit, chosen.length + extras);
|
|
13704
14334
|
while (remaining.length > 0 && chosen.length < desiredCount) {
|
|
13705
|
-
const output =
|
|
14335
|
+
const output = selectReservationChange(remaining, targetSatoshis);
|
|
13706
14336
|
if (output == null) break;
|
|
13707
14337
|
chosen.push(output);
|
|
13708
14338
|
remaining.splice(remaining.indexOf(output), 1);
|
|
13709
14339
|
}
|
|
13710
14340
|
return chosen;
|
|
13711
14341
|
}
|
|
13712
|
-
function
|
|
14342
|
+
function reservationOutput(candidate) {
|
|
14343
|
+
const { transactionStatus: _transactionStatus, ...output } = candidate;
|
|
14344
|
+
return output;
|
|
14345
|
+
}
|
|
14346
|
+
function reservationPlanningCosts(storage, _basket) {
|
|
13713
14347
|
const satsPerKb = validateStorageFeeModel(storage.feeModel).value ?? 0;
|
|
13714
14348
|
const minimumSpendSize = transactionSize([107], [25]);
|
|
13715
14349
|
const minimumSpendFee = Math.ceil(minimumSpendSize * satsPerKb / 1e3);
|
|
@@ -13717,7 +14351,7 @@ function reservationPlanningCosts(storage, basket) {
|
|
|
13717
14351
|
const marginalInputSize = transactionSize([107], []) - transactionSize([], []);
|
|
13718
14352
|
const marginalOutputSize = transactionSize([], [25]) - transactionSize([], []);
|
|
13719
14353
|
return {
|
|
13720
|
-
firstChangeCost:
|
|
14354
|
+
firstChangeCost: dustFloor + Math.ceil(marginalOutputSize * satsPerKb / 1e3),
|
|
13721
14355
|
marginalInputFee: Math.ceil(marginalInputSize * satsPerKb / 1e3)
|
|
13722
14356
|
};
|
|
13723
14357
|
}
|
|
@@ -13792,18 +14426,25 @@ async function beginActionBatch(storage, auth, args) {
|
|
|
13792
14426
|
const explicit = await resolveExplicitOutputs(storage, userId, args.firstAction, outputScriptLengths != null);
|
|
13793
14427
|
const noSendChange = await resolveNoSendChangeOutputs(storage, userId, args.firstAction);
|
|
13794
14428
|
const fixedOutputIds = new Set([...explicit.outputs, ...noSendChange.outputs].map((output) => output.outputId));
|
|
13795
|
-
const
|
|
14429
|
+
const availableOutputs = (await storage.findAvailableManagedChangeInputs(userId, changeBasket.basketId, false)).filter((output) => !fixedOutputIds.has(output.outputId));
|
|
14430
|
+
const availableStatuses = await storage.findTransactionStatusesByIds(userId, availableOutputs.map((output) => output.transactionId));
|
|
14431
|
+
const available = availableOutputs.map((output) => ({
|
|
14432
|
+
...output,
|
|
14433
|
+
transactionStatus: availableStatuses.get(output.transactionId) ?? "sending"
|
|
14434
|
+
}));
|
|
13796
14435
|
const target = estimateFirstActionTarget(storage, args.firstAction, explicit.inputSatoshis + noSendChange.inputSatoshis, outputScriptLengths);
|
|
13797
|
-
const fixedOutputs = [...explicit.outputs, ...noSendChange.outputs];
|
|
13798
|
-
const
|
|
14436
|
+
const fixedOutputs = [...new Map([...explicit.outputs, ...noSendChange.outputs].map((output) => [output.outputId, output])).values()];
|
|
14437
|
+
const maxReservedOutputs = storage.actionBatchMaxReservedOutputs;
|
|
14438
|
+
if (maxReservedOutputs >= 0 && fixedOutputs.length > maxReservedOutputs) throw new WERR_INVALID_PARAMETER("firstAction", `no more than ${maxReservedOutputs} persisted inputs`);
|
|
14439
|
+
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);
|
|
13799
14440
|
const batch = newBatch(userId, args.batchId);
|
|
13800
14441
|
await storage.transaction(async (trx) => {
|
|
13801
14442
|
await storage.insertActionBatch(batch, trx);
|
|
13802
|
-
await reserveOutputs(storage, batch, [...fixedOutputs, ...
|
|
14443
|
+
await reserveOutputs(storage, batch, [...fixedOutputs, ...fundingOutputs], trx);
|
|
13803
14444
|
});
|
|
13804
14445
|
let fundingResult;
|
|
13805
14446
|
try {
|
|
13806
|
-
fundingResult = await makeFundingResult(storage, args.firstAction, [...
|
|
14447
|
+
fundingResult = await makeFundingResult(storage, args.firstAction, [...fundingOutputs, ...fixedOutputs]);
|
|
13807
14448
|
} catch (error) {
|
|
13808
14449
|
await storage.transaction(async (trx) => await releaseBatchState(storage, batch, "aborted", trx));
|
|
13809
14450
|
throw error;
|
|
@@ -13817,41 +14458,63 @@ async function beginActionBatch(storage, auth, args) {
|
|
|
13817
14458
|
feeModel: validateStorageFeeModel(storage.feeModel),
|
|
13818
14459
|
commissionSatoshis: storage.commissionSatoshis,
|
|
13819
14460
|
commissionPubKeyHex: storage.commissionPubKeyHex,
|
|
13820
|
-
availableChangeCount: available.length,
|
|
13821
|
-
|
|
14461
|
+
availableChangeCount: available.filter((output) => output.satoshis >= Math.max(1, changeBasket.minimumDesiredUTXOValue)).length,
|
|
14462
|
+
managedChangePolicy: {
|
|
14463
|
+
maxOutputsPerAction: storage.managedChangePolicy.maxOutputsPerAction,
|
|
14464
|
+
migrationInputsPerAction: storage.managedChangePolicy.migrationInputsPerAction
|
|
14465
|
+
},
|
|
14466
|
+
reservedOutputs: fundingResult.outputs.filter((output) => fundingOutputs.some((candidate) => candidate.outputId === output.outputId)),
|
|
13822
14467
|
explicitOutputs: fundingResult.outputs.filter((output) => explicitIds.has(output.outputId)),
|
|
13823
14468
|
inputBeef: fundingResult.beef
|
|
13824
14469
|
};
|
|
13825
14470
|
}
|
|
13826
|
-
function requireLiveBatch(batch) {
|
|
13827
|
-
|
|
14471
|
+
function requireLiveBatch(batch, batchId) {
|
|
14472
|
+
const state = actionBatchErrorState(batch);
|
|
14473
|
+
if (state != null) throw new WERRActionBatchState(state, batchId ?? batch?.batchId);
|
|
14474
|
+
if (batch == null) throw new WERRActionBatchState("missing", batchId);
|
|
13828
14475
|
return batch;
|
|
13829
14476
|
}
|
|
13830
14477
|
async function extendActionBatch(storage, auth, args) {
|
|
13831
14478
|
const userId = verifyId(auth.userId);
|
|
13832
14479
|
await cleanupExpiredActionBatches(storage);
|
|
13833
|
-
const batch = requireLiveBatch(await storage.findActionBatch(userId, args.batchId));
|
|
14480
|
+
const batch = requireLiveBatch(await storage.findActionBatch(userId, args.batchId), args.batchId);
|
|
13834
14481
|
const basket = verifyOne(await storage.findOutputBaskets({ partial: {
|
|
13835
14482
|
userId,
|
|
13836
14483
|
name: "default"
|
|
13837
14484
|
} }));
|
|
13838
14485
|
const alreadyReserved = await storage.findActionBatchOutputIds(batch.actionBatchId);
|
|
13839
|
-
const available = await storage.findAvailableManagedChangeInputs(userId, basket.basketId, false);
|
|
13840
14486
|
if (!Number.isSafeInteger(args.requestedOutputs) || args.requestedOutputs < 0) throw new WERR_INVALID_PARAMETER("requestedOutputs", "non-negative safe integer");
|
|
13841
|
-
|
|
13842
|
-
const
|
|
14487
|
+
if (!Number.isSafeInteger(args.targetSatoshis) || args.targetSatoshis < 0) throw new WERR_INVALID_PARAMETER("targetSatoshis", "non-negative safe integer");
|
|
14488
|
+
const maxReservedOutputs = storage.actionBatchMaxReservedOutputs;
|
|
14489
|
+
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`);
|
|
14490
|
+
const remainingCapacity = maxReservedOutputs < 0 ? Number.MAX_SAFE_INTEGER : maxReservedOutputs - alreadyReserved.length;
|
|
14491
|
+
if (remainingCapacity <= 0 && (args.requestedOutputs > 0 || args.explicitOutpoints.length > 0)) throw new WERR_INVALID_OPERATION(`action batch already holds the maximum of ${maxReservedOutputs} outputs`);
|
|
13843
14492
|
const explicitByOutpoint = await storage.findOutputsByOutpoints(userId, args.explicitOutpoints);
|
|
13844
|
-
const explicit = Object.values(explicitByOutpoint).filter((output) => !alreadyReserved.includes(output.outputId));
|
|
13845
|
-
|
|
14493
|
+
const explicit = [...new Map(Object.values(explicitByOutpoint).filter((output) => !alreadyReserved.includes(output.outputId)).map((output) => [output.outputId, output])).values()];
|
|
14494
|
+
if (explicit.length > remainingCapacity) throw new WERR_INVALID_PARAMETER("explicitOutpoints", `no more than ${remainingCapacity} additional reserved outputs`);
|
|
14495
|
+
const explicitIds = new Set(explicit.map((output) => output.outputId));
|
|
14496
|
+
const availableOutputs = (await storage.findAvailableManagedChangeInputs(userId, basket.basketId, false)).filter((output) => !explicitIds.has(output.outputId));
|
|
14497
|
+
const availableStatuses = await storage.findTransactionStatusesByIds(userId, availableOutputs.map((output) => output.transactionId));
|
|
14498
|
+
const available = availableOutputs.map((output) => ({
|
|
14499
|
+
...output,
|
|
14500
|
+
transactionStatus: availableStatuses.get(output.transactionId) ?? "sending"
|
|
14501
|
+
}));
|
|
14502
|
+
const requestedCount = Math.min(args.requestedOutputs, 64, remainingCapacity - explicit.length);
|
|
14503
|
+
const fundingOutputs = chooseReservationPool(available, Math.max(1, args.targetSatoshis), requestedCount, 0, true, reservationPlanningCosts(storage, basket)).map(reservationOutput);
|
|
14504
|
+
const fundingResult = await makeFundingResult(storage, argsToFundingShape(args.includeSourceTransactions), [...fundingOutputs, ...explicit]);
|
|
13846
14505
|
const expiresAt = new Date(Math.min(batch.hardExpiresAt.getTime(), Date.now() + ACTION_BATCH_LEASE_MS));
|
|
13847
14506
|
await storage.transaction(async (trx) => {
|
|
13848
|
-
const current = requireLiveBatch(await storage.findActionBatchForUpdate(userId, args.batchId, trx));
|
|
13849
|
-
|
|
14507
|
+
const current = requireLiveBatch(await storage.findActionBatchForUpdate(userId, args.batchId, trx), args.batchId);
|
|
14508
|
+
const additions = [...new Map([...fundingOutputs, ...explicit].map((output) => [output.outputId, output])).values()];
|
|
14509
|
+
const currentReserved = new Set(await storage.findActionBatchOutputIds(current.actionBatchId, trx));
|
|
14510
|
+
const newAdditions = additions.filter((output) => !currentReserved.has(output.outputId));
|
|
14511
|
+
if (maxReservedOutputs >= 0 && currentReserved.size + newAdditions.length > maxReservedOutputs) throw new WERR_INVALID_OPERATION(`action batch cannot reserve more than ${maxReservedOutputs} outputs`);
|
|
14512
|
+
await reserveOutputs(storage, current, newAdditions, trx);
|
|
13850
14513
|
await storage.updateActionBatch(current.actionBatchId, { expiresAt }, trx);
|
|
13851
14514
|
});
|
|
13852
14515
|
return {
|
|
13853
14516
|
expiresAt: expiresAt.toISOString(),
|
|
13854
|
-
reservedOutputs: fundingResult.outputs.filter((output) =>
|
|
14517
|
+
reservedOutputs: fundingResult.outputs.filter((output) => fundingOutputs.some((candidate) => candidate.outputId === output.outputId)),
|
|
13855
14518
|
explicitOutputs: fundingResult.outputs.filter((output) => explicit.some((candidate) => candidate.outputId === output.outputId)),
|
|
13856
14519
|
inputBeef: fundingResult.beef
|
|
13857
14520
|
};
|
|
@@ -13887,14 +14550,64 @@ function argsToFundingShape(includeSourceTransactions) {
|
|
|
13887
14550
|
async function renewActionBatch(storage, auth, batchId) {
|
|
13888
14551
|
const userId = verifyId(auth.userId);
|
|
13889
14552
|
return await storage.transaction(async (trx) => {
|
|
13890
|
-
const batch = requireLiveBatch(await storage.findActionBatchForUpdate(userId, batchId, trx));
|
|
14553
|
+
const batch = requireLiveBatch(await storage.findActionBatchForUpdate(userId, batchId, trx), batchId);
|
|
13891
14554
|
const expiresAt = new Date(Math.min(batch.hardExpiresAt.getTime(), Date.now() + ACTION_BATCH_LEASE_MS));
|
|
13892
14555
|
await storage.updateActionBatch(batch.actionBatchId, { expiresAt }, trx);
|
|
13893
14556
|
return { expiresAt: expiresAt.toISOString() };
|
|
13894
14557
|
});
|
|
13895
14558
|
}
|
|
14559
|
+
function validateResumeOutpoints(storage, args) {
|
|
14560
|
+
const unique = new Set(args.outpoints.map((outpoint) => `${outpoint.txid}.${outpoint.vout}`));
|
|
14561
|
+
const maxReservedOutputs = storage.actionBatchMaxReservedOutputs;
|
|
14562
|
+
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`);
|
|
14563
|
+
}
|
|
14564
|
+
/**
|
|
14565
|
+
* Reacquire exactly the persisted inputs owned by one expired client
|
|
14566
|
+
* workspace. This is deliberately explicit: another action may not become a
|
|
14567
|
+
* member merely because it happens to use the same Wallet instance.
|
|
14568
|
+
*/
|
|
14569
|
+
async function resumeActionBatch(storage, auth, args) {
|
|
14570
|
+
const userId = verifyId(auth.userId);
|
|
14571
|
+
validateResumeOutpoints(storage, args);
|
|
14572
|
+
await cleanupExpiredActionBatches(storage);
|
|
14573
|
+
return await storage.transaction(async (trx) => {
|
|
14574
|
+
const batch = await storage.findActionBatchForUpdate(userId, args.batchId, trx);
|
|
14575
|
+
const state = actionBatchErrorState(batch);
|
|
14576
|
+
if (state != null && state !== "expired") throw new WERRActionBatchState(state, args.batchId);
|
|
14577
|
+
if (batch == null) throw new WERRActionBatchState("missing", args.batchId);
|
|
14578
|
+
const expiresAt = new Date(Math.min(batch.hardExpiresAt.getTime(), Date.now() + ACTION_BATCH_LEASE_MS));
|
|
14579
|
+
if (state == null) {
|
|
14580
|
+
const reserved = new Set(await storage.findActionBatchOutputIds(batch.actionBatchId, trx));
|
|
14581
|
+
const current = await storage.findOutputsByOutpointsForUpdate(userId, args.outpoints, trx);
|
|
14582
|
+
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);
|
|
14583
|
+
await storage.updateActionBatch(batch.actionBatchId, { expiresAt }, trx);
|
|
14584
|
+
return { expiresAt: expiresAt.toISOString() };
|
|
14585
|
+
}
|
|
14586
|
+
const stored = await storage.findOutputsByOutpointsForUpdate(userId, args.outpoints, trx);
|
|
14587
|
+
const outputs = args.outpoints.map((outpoint) => stored[`${outpoint.txid}.${outpoint.vout}`]);
|
|
14588
|
+
if (outputs.some((output) => output == null || !output.spendable || output.spentBy != null)) throw new WERRActionBatchState("conflicted", args.batchId);
|
|
14589
|
+
await storage.deleteActionBatchOutputReservations(batch.actionBatchId, trx);
|
|
14590
|
+
const exactOutputs = outputs;
|
|
14591
|
+
if ((await storage.findReservedActionBatchOutputIds(exactOutputs.map((output) => output.outputId), trx)).length > 0) throw new WERRActionBatchState("conflicted", args.batchId);
|
|
14592
|
+
const now = /* @__PURE__ */ new Date();
|
|
14593
|
+
await storage.reserveActionBatchOutputs(exactOutputs.map((output) => ({
|
|
14594
|
+
actionBatchId: batch.actionBatchId,
|
|
14595
|
+
outputId: output.outputId,
|
|
14596
|
+
created_at: now,
|
|
14597
|
+
updated_at: now
|
|
14598
|
+
})), trx);
|
|
14599
|
+
await storage.updateActionBatch(batch.actionBatchId, {
|
|
14600
|
+
status: "active",
|
|
14601
|
+
expiresAt,
|
|
14602
|
+
manifest: void 0,
|
|
14603
|
+
manifestDigest: void 0,
|
|
14604
|
+
uploadDigests: void 0
|
|
14605
|
+
}, trx);
|
|
14606
|
+
return { expiresAt: expiresAt.toISOString() };
|
|
14607
|
+
});
|
|
14608
|
+
}
|
|
13896
14609
|
async function reacquireManifestInputs(storage, userId, batch, validated, trx) {
|
|
13897
|
-
if (batch.hardExpiresAt.getTime() <= Date.now()) throw new
|
|
14610
|
+
if (batch.hardExpiresAt.getTime() <= Date.now()) throw new WERRActionBatchState("hard-expired", batch.batchId);
|
|
13898
14611
|
const stagedTxids = new Set(validated.actions.map(({ action }) => action.txid));
|
|
13899
14612
|
const outpoints = [...new Map(validated.actions.flatMap(({ action }) => action.plan.inputs).filter((input) => !stagedTxids.has(input.sourceTxid)).map((input) => [`${input.sourceTxid}.${input.sourceVout}`, {
|
|
13900
14613
|
txid: input.sourceTxid,
|
|
@@ -14061,7 +14774,7 @@ async function persistAction(...[storage, userId, validated, reservedOutputIds,
|
|
|
14061
14774
|
async function persistManifestAtomically(storage, userId, batch, manifest, validated) {
|
|
14062
14775
|
return await storage.transaction(async (trx) => {
|
|
14063
14776
|
const current = await storage.findActionBatchForUpdate(userId, batch.batchId, trx);
|
|
14064
|
-
if (current == null) throw new
|
|
14777
|
+
if (current == null) throw new WERRActionBatchState("missing", batch.batchId);
|
|
14065
14778
|
if (current.status === "committed") {
|
|
14066
14779
|
if (current.manifestDigest !== manifest.digest) throw new WERR_INVALID_OPERATION("batch committed with another manifest");
|
|
14067
14780
|
return {
|
|
@@ -14069,12 +14782,12 @@ async function persistManifestAtomically(storage, userId, batch, manifest, valid
|
|
|
14069
14782
|
alreadyCommitted: true
|
|
14070
14783
|
};
|
|
14071
14784
|
}
|
|
14072
|
-
if (current.status === "aborted") throw new
|
|
14785
|
+
if (current.status === "aborted") throw new WERRActionBatchState("aborted", batch.batchId);
|
|
14073
14786
|
if (current.manifestDigest != null && current.manifestDigest !== manifest.digest) throw new WERR_INVALID_OPERATION("batch prepared with another manifest");
|
|
14074
14787
|
if (current.status === "expired" || activeExpiry(current)) {
|
|
14075
|
-
if (current.hardExpiresAt.getTime() <= Date.now()) throw new
|
|
14788
|
+
if (current.hardExpiresAt.getTime() <= Date.now()) throw new WERRActionBatchState("hard-expired", batch.batchId);
|
|
14076
14789
|
await reacquireManifestInputs(storage, userId, current, validated, trx);
|
|
14077
|
-
} else requireLiveBatch(current);
|
|
14790
|
+
} else requireLiveBatch(current, batch.batchId);
|
|
14078
14791
|
const reservedOutputIds = new Set(await storage.findActionBatchOutputIds(current.actionBatchId, trx));
|
|
14079
14792
|
const stagedTxids = new Set(validated.actions.map(({ action }) => action.txid));
|
|
14080
14793
|
const inputOutpoints = [...new Map(validated.actions.flatMap(({ action }) => action.plan.inputs).filter((input) => !stagedTxids.has(input.sourceTxid)).map((input) => [`${input.sourceTxid}.${input.sourceVout}`, {
|
|
@@ -14152,14 +14865,14 @@ async function completeCommittedBatch(storage, userId, batch, manifest, alreadyC
|
|
|
14152
14865
|
}
|
|
14153
14866
|
async function commitActionBatchOnce(storage, userId, manifest) {
|
|
14154
14867
|
const batch = await storage.findActionBatch(userId, manifest.batchId);
|
|
14155
|
-
if (batch == null) throw new
|
|
14868
|
+
if (batch == null) throw new WERRActionBatchState("missing", manifest.batchId);
|
|
14156
14869
|
if (batch.status === "committed") {
|
|
14157
14870
|
if (batch.manifestDigest !== manifest.digest) throw new WERR_INVALID_OPERATION("batch committed with another manifest");
|
|
14158
14871
|
return await completeCommittedBatch(storage, userId, batch, manifest, true);
|
|
14159
14872
|
}
|
|
14160
|
-
if (batch.status === "aborted") throw new
|
|
14161
|
-
if (!(batch.status === "expired" || activeExpiry(batch))) requireLiveBatch(batch);
|
|
14162
|
-
else if (batch.hardExpiresAt.getTime() <= Date.now()) throw new
|
|
14873
|
+
if (batch.status === "aborted") throw new WERRActionBatchState("aborted", manifest.batchId);
|
|
14874
|
+
if (!(batch.status === "expired" || activeExpiry(batch))) requireLiveBatch(batch, manifest.batchId);
|
|
14875
|
+
else if (batch.hardExpiresAt.getTime() <= Date.now()) throw new WERRActionBatchState("hard-expired", manifest.batchId);
|
|
14163
14876
|
if (batch.manifestDigest != null && batch.manifestDigest !== manifest.digest) throw new WERR_INVALID_OPERATION("batch prepared with another manifest");
|
|
14164
14877
|
const persisted = await persistManifestAtomically(storage, userId, batch, manifest, await validateManifestActions(storage, batch, manifest));
|
|
14165
14878
|
return await completeCommittedBatch(storage, userId, persisted.batch, manifest, persisted.alreadyCommitted, persisted.share);
|
|
@@ -14242,6 +14955,8 @@ var StorageProvider = class StorageProvider extends StorageReaderWriter {
|
|
|
14242
14955
|
commissionSatoshis;
|
|
14243
14956
|
commissionPubKeyHex;
|
|
14244
14957
|
maxRecursionDepth;
|
|
14958
|
+
actionBatchMaxReservedOutputs;
|
|
14959
|
+
managedChangePolicy;
|
|
14245
14960
|
scriptVerifier;
|
|
14246
14961
|
static defaultOptions() {
|
|
14247
14962
|
return {
|
|
@@ -14250,7 +14965,9 @@ var StorageProvider = class StorageProvider extends StorageReaderWriter {
|
|
|
14250
14965
|
value: 100
|
|
14251
14966
|
},
|
|
14252
14967
|
commissionSatoshis: 0,
|
|
14253
|
-
commissionPubKeyHex: void 0
|
|
14968
|
+
commissionPubKeyHex: void 0,
|
|
14969
|
+
actionBatchMaxReservedOutputs: 256,
|
|
14970
|
+
managedChangePolicy: defaultManagedChangePolicy()
|
|
14254
14971
|
};
|
|
14255
14972
|
}
|
|
14256
14973
|
static createStorageBaseOptions(chain) {
|
|
@@ -14265,6 +14982,10 @@ var StorageProvider = class StorageProvider extends StorageReaderWriter {
|
|
|
14265
14982
|
this.commissionPubKeyHex = options.commissionPubKeyHex;
|
|
14266
14983
|
this.commissionSatoshis = options.commissionSatoshis;
|
|
14267
14984
|
this.maxRecursionDepth = 12;
|
|
14985
|
+
const maxReservedOutputs = options.actionBatchMaxReservedOutputs ?? 256;
|
|
14986
|
+
if (maxReservedOutputs !== -1 && (!Number.isSafeInteger(maxReservedOutputs) || maxReservedOutputs < 1)) throw new WERR_INVALID_PARAMETER("actionBatchMaxReservedOutputs", "a positive safe integer or -1 for unlimited");
|
|
14987
|
+
this.actionBatchMaxReservedOutputs = maxReservedOutputs;
|
|
14988
|
+
this.managedChangePolicy = validateManagedChangePolicy(options.managedChangePolicy);
|
|
14268
14989
|
this.scriptVerifier = options.scriptVerifier;
|
|
14269
14990
|
}
|
|
14270
14991
|
/** Mark a planned set of change inputs spent within the caller's transaction. */
|
|
@@ -14295,12 +15016,16 @@ var StorageProvider = class StorageProvider extends StorageReaderWriter {
|
|
|
14295
15016
|
}
|
|
14296
15017
|
/** Read only the fields needed by the in-memory funding planner. */
|
|
14297
15018
|
async findAvailableManagedChangeInputCandidates(userId, basketId, excludeSending, trx) {
|
|
14298
|
-
|
|
15019
|
+
const outputs = await this.findAvailableManagedChangeInputs(userId, basketId, excludeSending, trx);
|
|
15020
|
+
const findStatuses = this.findTransactionStatusesByIds?.bind(this);
|
|
15021
|
+
const statuses = findStatuses == null ? /* @__PURE__ */ new Map() : await findStatuses(userId, outputs.map((output) => output.transactionId), trx);
|
|
15022
|
+
return outputs.map(({ outputId, transactionId, satoshis, txid, vout }) => ({
|
|
14299
15023
|
outputId,
|
|
14300
15024
|
transactionId,
|
|
14301
15025
|
satoshis,
|
|
14302
15026
|
txid,
|
|
14303
|
-
vout
|
|
15027
|
+
vout,
|
|
15028
|
+
transactionStatus: statuses.get(transactionId)
|
|
14304
15029
|
}));
|
|
14305
15030
|
}
|
|
14306
15031
|
/** Read the current status of a set of source transactions without loading raw transaction bytes. */
|
|
@@ -14388,7 +15113,7 @@ var StorageProvider = class StorageProvider extends StorageReaderWriter {
|
|
|
14388
15113
|
throw new WERR_NOT_IMPLEMENTED();
|
|
14389
15114
|
}
|
|
14390
15115
|
async getCapabilities() {
|
|
14391
|
-
return this.supportsActionBatchPersistence() ? getActionBatchCapabilities() : {};
|
|
15116
|
+
return this.supportsActionBatchPersistence() ? getActionBatchCapabilities(this.actionBatchMaxReservedOutputs, true) : {};
|
|
14392
15117
|
}
|
|
14393
15118
|
supportsActionBatchPersistence() {
|
|
14394
15119
|
return false;
|
|
@@ -14407,6 +15132,9 @@ var StorageProvider = class StorageProvider extends StorageReaderWriter {
|
|
|
14407
15132
|
async renewActionBatch(auth, batchId) {
|
|
14408
15133
|
return await renewActionBatch(this, auth, batchId);
|
|
14409
15134
|
}
|
|
15135
|
+
async resumeActionBatch(auth, args) {
|
|
15136
|
+
return await resumeActionBatch(this, auth, args);
|
|
15137
|
+
}
|
|
14410
15138
|
async prepareActionBatchCommit(auth, manifest) {
|
|
14411
15139
|
return await prepareActionBatchCommit(this, auth, manifest);
|
|
14412
15140
|
}
|
|
@@ -15004,7 +15732,12 @@ var StorageProvider = class StorageProvider extends StorageReaderWriter {
|
|
|
15004
15732
|
for (const output of outputs) {
|
|
15005
15733
|
const outputId = verifyId(output.outputId);
|
|
15006
15734
|
await this.validateOutputScript(output);
|
|
15007
|
-
|
|
15735
|
+
if (output.lockingScript == null) {
|
|
15736
|
+
verdicts.set(outputId, void 0);
|
|
15737
|
+
continue;
|
|
15738
|
+
}
|
|
15739
|
+
const classification = await classifyOutputUtxo(services, output);
|
|
15740
|
+
verdicts.set(outputId, classification.verdict === "unknown" ? void 0 : classification.verdict === "unspent");
|
|
15008
15741
|
}
|
|
15009
15742
|
}
|
|
15010
15743
|
return verdicts;
|
|
@@ -15166,16 +15899,12 @@ var StorageProvider = class StorageProvider extends StorageReaderWriter {
|
|
|
15166
15899
|
} });
|
|
15167
15900
|
for (const o of outputs) {
|
|
15168
15901
|
if (!o.spendable) continue;
|
|
15169
|
-
|
|
15902
|
+
await this.validateOutputScript(o);
|
|
15903
|
+
if (!requireConclusiveUtxo(await classifyOutputUtxo(services, o))) invalidSpendableOutputs.push(o);
|
|
15170
15904
|
}
|
|
15171
15905
|
}
|
|
15172
15906
|
return { invalidSpendableOutputs };
|
|
15173
15907
|
}
|
|
15174
|
-
async checkOutputIsUtxo(o, services) {
|
|
15175
|
-
if (o.lockingScript == null || o.lockingScript.length === 0) return false;
|
|
15176
|
-
const hash = services.hashOutputScript(asString(o.lockingScript));
|
|
15177
|
-
return (await services.getUtxoStatus(hash, void 0, `${o.txid ?? ""}.${o.vout ?? ""}`)).isUtxo === true;
|
|
15178
|
-
}
|
|
15179
15908
|
async updateProvenTxReqDynamics(id, update, trx) {
|
|
15180
15909
|
const partial = {};
|
|
15181
15910
|
if (update.updated_at != null) partial.updated_at = update.updated_at;
|
|
@@ -15285,6 +16014,7 @@ const getLabelToSpecOp = () => {
|
|
|
15285
16014
|
//#region ../src/storage/methods/listActionsIdb.ts
|
|
15286
16015
|
async function enrichIdbActionLabels(storage, tx, action, timeFilterRequested) {
|
|
15287
16016
|
action.labels = (await storage.getLabelsForTransactionId(tx.transactionId)).map((l) => l.label);
|
|
16017
|
+
if (tx.reference != null && tx.reference !== "") action.labels = applyBrc153ReferenceLabel(action.labels, tx.reference);
|
|
15288
16018
|
if (timeFilterRequested) {
|
|
15289
16019
|
const ts = tx.created_at ? new Date(tx.created_at).getTime() : NaN;
|
|
15290
16020
|
if (!Number.isNaN(ts)) {
|
|
@@ -15403,20 +16133,129 @@ async function listActionsIdb(storage, auth, vargs) {
|
|
|
15403
16133
|
return r;
|
|
15404
16134
|
}
|
|
15405
16135
|
//#endregion
|
|
15406
|
-
//#region ../src/storage/methods/
|
|
15407
|
-
const
|
|
15408
|
-
|
|
15409
|
-
|
|
15410
|
-
|
|
15411
|
-
|
|
15412
|
-
|
|
15413
|
-
|
|
16136
|
+
//#region ../src/storage/methods/reviewUtxoOutputs.ts
|
|
16137
|
+
const MAX_AUDIT_PROVIDERS = 8;
|
|
16138
|
+
const MAX_PROVIDER_NAME_LENGTH = 128;
|
|
16139
|
+
const UTXO_REVIEW_PROVIDER_TIMEOUT_MSECS = 5e3;
|
|
16140
|
+
function diagnosticsFor(classifications, releasedOutputIds) {
|
|
16141
|
+
const allProviders = [...new Set(classifications.map((result) => result.status.provider.slice(0, MAX_PROVIDER_NAME_LENGTH)))];
|
|
16142
|
+
const providers = allProviders.slice(0, MAX_AUDIT_PROVIDERS);
|
|
16143
|
+
const confirmedSpent = classifications.filter((result) => result.status.verdict === "spent");
|
|
16144
|
+
const released = confirmedSpent.filter((result) => releasedOutputIds.has(result.output.outputId));
|
|
16145
|
+
return {
|
|
16146
|
+
checked: classifications.length,
|
|
16147
|
+
confirmedUnspent: classifications.filter((result) => result.status.verdict === "unspent").length,
|
|
16148
|
+
confirmedSpent: confirmedSpent.length,
|
|
16149
|
+
unknown: classifications.filter((result) => result.status.verdict === "unknown").length,
|
|
16150
|
+
confirmedSpentSatoshis: confirmedSpent.reduce((sum, result) => sum + result.output.satoshis, 0),
|
|
16151
|
+
released: released.length,
|
|
16152
|
+
releasedSatoshis: released.reduce((sum, result) => sum + result.output.satoshis, 0),
|
|
16153
|
+
providers,
|
|
16154
|
+
providerCount: allProviders.length,
|
|
16155
|
+
providersTruncated: allProviders.length > providers.length
|
|
16156
|
+
};
|
|
16157
|
+
}
|
|
16158
|
+
function auditDetails(diagnostics, userId, releaseMode, reason) {
|
|
16159
|
+
return JSON.stringify({
|
|
16160
|
+
operation: "specOpInvalidChange",
|
|
16161
|
+
reason,
|
|
16162
|
+
releaseMode,
|
|
16163
|
+
userId,
|
|
16164
|
+
...diagnostics
|
|
16165
|
+
});
|
|
16166
|
+
}
|
|
16167
|
+
async function classifyCandidates(storage, outputs) {
|
|
16168
|
+
const services = storage.getServices();
|
|
16169
|
+
return await mapWithConcurrency(outputs.filter((output) => output.basketId != null), 4, async (output) => {
|
|
16170
|
+
try {
|
|
16171
|
+
await storage.validateOutputScript(output);
|
|
16172
|
+
} catch (error) {
|
|
16173
|
+
return {
|
|
16174
|
+
output,
|
|
16175
|
+
status: {
|
|
16176
|
+
verdict: "unknown",
|
|
16177
|
+
provider: "<script-validation-error>",
|
|
16178
|
+
error
|
|
16179
|
+
}
|
|
16180
|
+
};
|
|
16181
|
+
}
|
|
16182
|
+
let timeout;
|
|
16183
|
+
const providerCall = classifyOutputUtxo(services, output);
|
|
16184
|
+
const timedClassification = new Promise((resolve) => {
|
|
16185
|
+
timeout = setTimeout(() => resolve({
|
|
16186
|
+
verdict: "unknown",
|
|
16187
|
+
provider: "<review-timeout>",
|
|
16188
|
+
error: /* @__PURE__ */ new Error(`UTXO classification exceeded ${UTXO_REVIEW_PROVIDER_TIMEOUT_MSECS}ms`)
|
|
16189
|
+
}), UTXO_REVIEW_PROVIDER_TIMEOUT_MSECS);
|
|
15414
16190
|
});
|
|
15415
|
-
|
|
15416
|
-
|
|
16191
|
+
try {
|
|
16192
|
+
return {
|
|
16193
|
+
output,
|
|
16194
|
+
status: await Promise.race([providerCall, timedClassification])
|
|
16195
|
+
};
|
|
16196
|
+
} finally {
|
|
16197
|
+
if (timeout != null) clearTimeout(timeout);
|
|
16198
|
+
providerCall.catch(() => void 0);
|
|
16199
|
+
}
|
|
16200
|
+
});
|
|
16201
|
+
}
|
|
16202
|
+
async function releaseConfirmedSpent(storage, auth, classifications, releaseMode) {
|
|
16203
|
+
const confirmedSpent = classifications.filter((result) => result.status.verdict === "spent");
|
|
16204
|
+
const releasedOutputIds = /* @__PURE__ */ new Set();
|
|
16205
|
+
await storage.transaction(async (trx) => {
|
|
16206
|
+
for (const { output } of confirmedSpent) {
|
|
16207
|
+
const current = await storage.findOutputById(output.outputId, trx, true);
|
|
16208
|
+
if (current == null || current.userId !== auth.userId) throw new WERR_INVALID_PARAMETER("outputId", `owned by user ${auth.userId}`);
|
|
16209
|
+
if (current.spendable !== true || current.spentBy != null) throw new WERR_INVALID_OPERATION(`Output ${output.outputId} changed state during UTXO review; no outputs were changed.`);
|
|
16210
|
+
if (await storage.updateOutput(verifyId(output.outputId), { spendable: false }, trx) !== 1) throw new WERR_INVALID_PARAMETER("outputId", `updated exactly once: ${output.outputId}`);
|
|
16211
|
+
releasedOutputIds.add(output.outputId);
|
|
16212
|
+
}
|
|
16213
|
+
const now = /* @__PURE__ */ new Date();
|
|
16214
|
+
const diagnostics = diagnosticsFor(classifications, releasedOutputIds);
|
|
16215
|
+
await storage.insertMonitorEvent({
|
|
16216
|
+
created_at: now,
|
|
16217
|
+
updated_at: now,
|
|
16218
|
+
id: 0,
|
|
16219
|
+
event: releaseMode === "atomic" ? "InvalidChangeRelease" : "InvalidChangeConclusiveRelease",
|
|
16220
|
+
details: auditDetails(diagnostics, auth.userId, releaseMode, "provider-confirmed-spent")
|
|
16221
|
+
}, trx);
|
|
16222
|
+
});
|
|
16223
|
+
return releasedOutputIds;
|
|
16224
|
+
}
|
|
16225
|
+
/**
|
|
16226
|
+
* Classify a bounded set of wallet outputs without treating provider failure as
|
|
16227
|
+
* proof that an output was spent. Read-only reviews always return their
|
|
16228
|
+
* conclusive and unknown partitions. Atomic release requires every verdict to
|
|
16229
|
+
* be conclusive; the operator-only conclusive mode releases the positively
|
|
16230
|
+
* spent subset while retaining and reporting unknowns.
|
|
16231
|
+
*/
|
|
16232
|
+
async function reviewUtxoOutputs(storage, auth, outputs, releaseMode = "none") {
|
|
16233
|
+
const classifications = await classifyCandidates(storage, outputs);
|
|
16234
|
+
const confirmedSpentOutputs = classifications.filter((result) => result.status.verdict === "spent").map((result) => result.output);
|
|
16235
|
+
const unknownOutputs = classifications.filter((result) => result.status.verdict === "unknown").map((result) => result.output);
|
|
16236
|
+
if (releaseMode === "atomic" && unknownOutputs.length > 0) {
|
|
16237
|
+
const diagnostics = diagnosticsFor(classifications, /* @__PURE__ */ new Set());
|
|
16238
|
+
const now = /* @__PURE__ */ new Date();
|
|
16239
|
+
await storage.insertMonitorEvent({
|
|
16240
|
+
created_at: now,
|
|
16241
|
+
updated_at: now,
|
|
16242
|
+
id: 0,
|
|
16243
|
+
event: "InvalidChangeReleaseBlocked",
|
|
16244
|
+
details: auditDetails(diagnostics, auth.userId, releaseMode, "inconclusive-provider-result")
|
|
16245
|
+
});
|
|
16246
|
+
throw new WERR_UTXO_REVIEW_INCONCLUSIVE(diagnostics.checked, diagnostics.confirmedSpent, diagnostics.unknown);
|
|
15417
16247
|
}
|
|
15418
|
-
await
|
|
16248
|
+
const releasedOutputIds = releaseMode === "none" ? /* @__PURE__ */ new Set() : await releaseConfirmedSpent(storage, auth, classifications, releaseMode);
|
|
16249
|
+
for (const output of confirmedSpentOutputs) if (releasedOutputIds.has(output.outputId)) output.spendable = false;
|
|
16250
|
+
return {
|
|
16251
|
+
classifications,
|
|
16252
|
+
confirmedSpentOutputs,
|
|
16253
|
+
unknownOutputs,
|
|
16254
|
+
diagnostics: diagnosticsFor(classifications, releasedOutputIds)
|
|
16255
|
+
};
|
|
15419
16256
|
}
|
|
16257
|
+
//#endregion
|
|
16258
|
+
//#region ../src/storage/methods/ListOutputsSpecOp.ts
|
|
15420
16259
|
const getBasketToSpecOp = () => {
|
|
15421
16260
|
return {
|
|
15422
16261
|
[specOpWalletBalance]: {
|
|
@@ -15447,23 +16286,7 @@ const getBasketToSpecOp = () => {
|
|
|
15447
16286
|
includeSpent: false,
|
|
15448
16287
|
tagsToIntercept: ["release", "all"],
|
|
15449
16288
|
filterOutputs: async (s, auth, vargs, specOpTags, outputs) => {
|
|
15450
|
-
|
|
15451
|
-
const invalidOutputIds = /* @__PURE__ */ new Set();
|
|
15452
|
-
const services = s.getServices();
|
|
15453
|
-
await runWithConcurrency(outputs, INVALID_CHANGE_MAX_CONCURRENCY, async (o) => {
|
|
15454
|
-
if (!o.basketId) return;
|
|
15455
|
-
await s.validateOutputScript(o);
|
|
15456
|
-
let ok = false;
|
|
15457
|
-
if (o.lockingScript != null && o.lockingScript.length > 0) ok = await services.isUtxo(o);
|
|
15458
|
-
else ok = void 0;
|
|
15459
|
-
if (ok === false) invalidOutputIds.add(o.outputId);
|
|
15460
|
-
});
|
|
15461
|
-
const filteredOutputs = outputs.filter((o) => invalidOutputIds.has(o.outputId));
|
|
15462
|
-
if (specOpTags.includes("release")) await runWithConcurrency(filteredOutputs, INVALID_CHANGE_MAX_CONCURRENCY, async (o) => {
|
|
15463
|
-
await s.updateOutput(o.outputId, { spendable: false });
|
|
15464
|
-
o.spendable = false;
|
|
15465
|
-
});
|
|
15466
|
-
return filteredOutputs;
|
|
16289
|
+
return (await reviewUtxoOutputs(s, auth, outputs, specOpTags.includes("release") ? "atomic" : "none")).confirmedSpentOutputs;
|
|
15467
16290
|
}
|
|
15468
16291
|
},
|
|
15469
16292
|
[specOpSetWalletChangeParams]: {
|
|
@@ -15616,7 +16439,7 @@ async function loadIdbOutputs(query) {
|
|
|
15616
16439
|
"nosend",
|
|
15617
16440
|
"sending"
|
|
15618
16441
|
],
|
|
15619
|
-
noScript: true,
|
|
16442
|
+
noScript: specOp?.includeOutputScripts !== true,
|
|
15620
16443
|
orderDescending
|
|
15621
16444
|
};
|
|
15622
16445
|
const pageManagedChange = specOp?.managedChangeOnly === true && specOp.ignoreLimit !== true;
|
|
@@ -15698,7 +16521,10 @@ async function listOutputsIdb(storage, auth, vargs, _originator) {
|
|
|
15698
16521
|
});
|
|
15699
16522
|
result.totalOutputs = totalOutputs;
|
|
15700
16523
|
if (specOp != null) {
|
|
15701
|
-
if (specOp.filterOutputs != null)
|
|
16524
|
+
if (specOp.filterOutputs != null) {
|
|
16525
|
+
outputs = await specOp.filterOutputs(storage, auth, vargs, specOpTags, outputs);
|
|
16526
|
+
result.totalOutputs = outputs.length;
|
|
16527
|
+
}
|
|
15702
16528
|
if (specOp.resultFromOutputs != null) return specOp.resultFromOutputs(storage, auth, vargs, specOpTags, outputs);
|
|
15703
16529
|
}
|
|
15704
16530
|
await hydrateIdbOutputResult(storage, outputs, vargs, result);
|
|
@@ -15829,6 +16655,7 @@ async function scanCursor(cursor, since, offset, limit, matches, accept) {
|
|
|
15829
16655
|
var StorageIdb = class extends StorageProvider {
|
|
15830
16656
|
dbName;
|
|
15831
16657
|
db;
|
|
16658
|
+
managedChangeDefaultsMigrated = false;
|
|
15832
16659
|
constructor(options) {
|
|
15833
16660
|
super(options);
|
|
15834
16661
|
this.dbName = `wallet-toolbox-${this.chain}net`;
|
|
@@ -15866,6 +16693,7 @@ var StorageIdb = class extends StorageProvider {
|
|
|
15866
16693
|
async verifyDB(storageName, storageIdentityKey) {
|
|
15867
16694
|
if (this.db != null) return this.db;
|
|
15868
16695
|
this.db = await this.initDB(storageName, storageIdentityKey);
|
|
16696
|
+
await this.migrateManagedChangeDefaults(this.db);
|
|
15869
16697
|
this._settings = (await this.db.getAll("settings"))[0];
|
|
15870
16698
|
this.whenLastAccess = /* @__PURE__ */ new Date();
|
|
15871
16699
|
return this.db;
|
|
@@ -15899,7 +16727,7 @@ var StorageIdb = class extends StorageProvider {
|
|
|
15899
16727
|
async initDB(storageName, storageIdentityKey) {
|
|
15900
16728
|
const chain = this.chain;
|
|
15901
16729
|
const maxOutputScript = 1024;
|
|
15902
|
-
return await (0, idb.openDB)(this.dbName,
|
|
16730
|
+
return await (0, idb.openDB)(this.dbName, 4, { upgrade(db, _oldVersion, _newVersion, transaction) {
|
|
15903
16731
|
upgradeAllStoresV1(db);
|
|
15904
16732
|
upgradeActionBatchStoresV2(db);
|
|
15905
16733
|
const outputs = transaction.objectStore("outputs");
|
|
@@ -15925,6 +16753,22 @@ var StorageIdb = class extends StorageProvider {
|
|
|
15925
16753
|
}
|
|
15926
16754
|
} });
|
|
15927
16755
|
}
|
|
16756
|
+
async migrateManagedChangeDefaults(db) {
|
|
16757
|
+
if (this.managedChangeDefaultsMigrated) return;
|
|
16758
|
+
const trx = db.transaction("output_baskets", "readwrite");
|
|
16759
|
+
let cursor = await trx.store.openCursor();
|
|
16760
|
+
while (cursor != null) {
|
|
16761
|
+
const basket = cursor.value;
|
|
16762
|
+
if (isLegacyManagedChangeBasketDefault(basket)) await cursor.update({
|
|
16763
|
+
...basket,
|
|
16764
|
+
minimumDesiredUTXOValue: DEFAULT_MANAGED_CHANGE_MINIMUM_SATOSHIS,
|
|
16765
|
+
updated_at: /* @__PURE__ */ new Date()
|
|
16766
|
+
});
|
|
16767
|
+
cursor = await cursor.continue();
|
|
16768
|
+
}
|
|
16769
|
+
await trx.done;
|
|
16770
|
+
this.managedChangeDefaultsMigrated = true;
|
|
16771
|
+
}
|
|
15928
16772
|
async reviewStatus(args) {
|
|
15929
16773
|
return await reviewStatusIdb(this, args);
|
|
15930
16774
|
}
|
|
@@ -16679,6 +17523,7 @@ var StorageIdb = class extends StorageProvider {
|
|
|
16679
17523
|
if (this.db != null) this.db.close();
|
|
16680
17524
|
this.db = void 0;
|
|
16681
17525
|
this._settings = void 0;
|
|
17526
|
+
this.managedChangeDefaultsMigrated = false;
|
|
16682
17527
|
}
|
|
16683
17528
|
allStores = [
|
|
16684
17529
|
"action_batches",
|
|
@@ -17851,6 +18696,9 @@ var StorageClientBase = class {
|
|
|
17851
18696
|
async renewActionBatch(auth, batchId) {
|
|
17852
18697
|
return await this.rpcCall("renewActionBatch", [auth, batchId]);
|
|
17853
18698
|
}
|
|
18699
|
+
async resumeActionBatch(auth, args) {
|
|
18700
|
+
return await this.rpcCall("resumeActionBatch", [auth, args]);
|
|
18701
|
+
}
|
|
17854
18702
|
async prepareActionBatchCommit(auth, manifest) {
|
|
17855
18703
|
return await this.rpcCall("prepareActionBatchCommit", [auth, manifest]);
|
|
17856
18704
|
}
|
|
@@ -18655,7 +19503,7 @@ async function restoreBRC38(storage, data) {
|
|
|
18655
19503
|
await storage.transaction(async (trx) => {
|
|
18656
19504
|
await storage.insertUser({ ...data.user }, trx);
|
|
18657
19505
|
for (const row of data.provenTxs) await storage.insertProvenTx({ ...row }, trx);
|
|
18658
|
-
for (const row of data.outputBaskets) await storage.insertOutputBasket({ ...row }, trx);
|
|
19506
|
+
for (const row of data.outputBaskets) await storage.insertOutputBasket(upgradeLegacyManagedChangeBasketDefault({ ...row }), trx);
|
|
18659
19507
|
for (const row of data.outputTags) await storage.insertOutputTag({ ...row }, trx);
|
|
18660
19508
|
for (const row of data.txLabels) await storage.insertTxLabel({ ...row }, trx);
|
|
18661
19509
|
for (const row of data.transactions) await storage.insertTransaction({ ...row }, trx);
|
|
@@ -21714,13 +22562,37 @@ function configuredChaintracksClient(chain, serviceUrl) {
|
|
|
21714
22562
|
return new ChaintracksServiceClient(chain, serviceUrl);
|
|
21715
22563
|
}
|
|
21716
22564
|
/**
|
|
22565
|
+
* True when running under a browser-style runtime — a real browser tab, or
|
|
22566
|
+
* the embedded webview a desktop wallet shell hosts its wallet logic in
|
|
22567
|
+
* (Metanet Client / User Wallet are Tauri WKWebViews) — where `fetch` is
|
|
22568
|
+
* subject to CORS enforcement that Node-style runtimes do not apply.
|
|
22569
|
+
*/
|
|
22570
|
+
function isBrowserRuntime() {
|
|
22571
|
+
return typeof window !== "undefined" && window.document !== void 0;
|
|
22572
|
+
}
|
|
22573
|
+
/**
|
|
21717
22574
|
* Returns the credential-free default ChainTracks client for a supported
|
|
21718
22575
|
* public network, or an operator-configured client for stn/tstn.
|
|
22576
|
+
*
|
|
22577
|
+
* BROWSER RUNTIMES get the legacy CORS-enabled Chaintracks service for
|
|
22578
|
+
* main/test. The Go Chaintracks deployments (`arcade-v2-*.bsvblockchain.tech`)
|
|
22579
|
+
* currently serve no `Access-Control-Allow-Origin` header and answer OPTIONS
|
|
22580
|
+
* preflights with 404 (verified live 2026-08-11), so every fetch from a
|
|
22581
|
+
* browser-hosted wallet is CORS-blocked (WebKit surfaces it as
|
|
22582
|
+
* `TypeError: Load failed`) and the wallet loses `getHeight`, headers and
|
|
22583
|
+
* merkle-root validation wholesale. The repository service contract
|
|
22584
|
+
* (AGENTS.md: "browser, mobile, and unknown-domain clients must not be
|
|
22585
|
+
* silently blocked by CORS") requires a default that browsers can actually
|
|
22586
|
+
* reach. Once the Go deployments serve CORS (and a browser-run conformance
|
|
22587
|
+
* check proves it), this branch can be removed and browsers can share the v2
|
|
22588
|
+
* default. Node runtimes are unchanged.
|
|
21719
22589
|
*/
|
|
21720
22590
|
function createDefaultChaintracksClient(chain) {
|
|
21721
22591
|
switch (chain) {
|
|
21722
22592
|
case "main":
|
|
21723
22593
|
case "test":
|
|
22594
|
+
if (isBrowserRuntime()) return new ChaintracksServiceClient(chain, `https://${chain}net-chaintracks.babbage.systems`);
|
|
22595
|
+
return new GoChaintracksServiceClient(chain, arcadeDefaultUrl(chain), { apiPrefix: "/chaintracks/v2" });
|
|
21724
22596
|
case "ttn": return new GoChaintracksServiceClient(chain, arcadeDefaultUrl(chain), { apiPrefix: "/chaintracks/v2" });
|
|
21725
22597
|
case "stn": return configuredChaintracksClient(chain, stnChaintracksUrl());
|
|
21726
22598
|
case "tstn": return configuredChaintracksClient(chain, tstnChaintracksUrl());
|
|
@@ -21759,7 +22631,7 @@ function createDefaultWalletServicesOptions(...[chain, arcCallbackUrl, arcCallba
|
|
|
21759
22631
|
exchangeratesapiKey: void 0,
|
|
21760
22632
|
chaintracksFiatExchangeRatesUrl,
|
|
21761
22633
|
chaintracks,
|
|
21762
|
-
arcUrl: arcDefaultUrl(chain),
|
|
22634
|
+
arcUrl: chain === "ttn" ? "" : arcDefaultUrl(chain),
|
|
21763
22635
|
arcConfig: {
|
|
21764
22636
|
apiKey: taalArcApiKey ?? void 0,
|
|
21765
22637
|
deploymentId,
|
|
@@ -21775,7 +22647,7 @@ function createDefaultWalletServicesOptions(...[chain, arcCallbackUrl, arcCallba
|
|
|
21775
22647
|
},
|
|
21776
22648
|
bitailsApiKey
|
|
21777
22649
|
};
|
|
21778
|
-
const resolvedArcadeUrl = arcadeUrl;
|
|
22650
|
+
const resolvedArcadeUrl = arcadeUrl ?? (chain === "ttn" ? arcadeDefaultUrl(chain) : void 0);
|
|
21779
22651
|
if (resolvedArcadeUrl != null && resolvedArcadeUrl !== "") {
|
|
21780
22652
|
o.arcadeUrl = resolvedArcadeUrl;
|
|
21781
22653
|
o.arcadeConfig = {
|
|
@@ -21805,7 +22677,7 @@ function arcDefaultUrl(chain) {
|
|
|
21805
22677
|
case "main": return "https://arc.taal.com";
|
|
21806
22678
|
case "test": return "https://arc-test.taal.com";
|
|
21807
22679
|
case "stn": return stnArcadeUrl() ?? "";
|
|
21808
|
-
case "ttn": return "
|
|
22680
|
+
case "ttn": return "";
|
|
21809
22681
|
case "tstn": return tstnArcadeUrl() ?? "";
|
|
21810
22682
|
case "mock": return "";
|
|
21811
22683
|
}
|
|
@@ -22177,6 +23049,40 @@ var ARC = class {
|
|
|
22177
23049
|
}
|
|
22178
23050
|
};
|
|
22179
23051
|
//#endregion
|
|
23052
|
+
//#region ../src/services/providers/arcadeStatus.ts
|
|
23053
|
+
/**
|
|
23054
|
+
* Canonical classification shared by Arcade polling and SSE. Keeping this in
|
|
23055
|
+
* one place prevents a provider from calling a rejection "unknown" while the
|
|
23056
|
+
* monitor's event path calls the same status terminal.
|
|
23057
|
+
*/
|
|
23058
|
+
function classifyArcadeRejection(event) {
|
|
23059
|
+
const extraInfo = event.extraInfo?.trim() ?? "";
|
|
23060
|
+
const detail = extraInfo.toLowerCase();
|
|
23061
|
+
if (event.status === 476 || detail.startsWith("parent rejected") || detail.includes("not final") || detail.includes("non-final")) return {
|
|
23062
|
+
terminal: false,
|
|
23063
|
+
inputConflict: false,
|
|
23064
|
+
reqStatus: "invalid",
|
|
23065
|
+
reason: event.status === 476 ? "transaction is not final" : "Arcade reported a retryable parent/locktime condition"
|
|
23066
|
+
};
|
|
23067
|
+
const orphanConflict = isArcDoubleSpendTxStatus(event.txStatus);
|
|
23068
|
+
const inputConflict = orphanConflict || event.status === 462 || event.status === 466 || /(?:utxo|input).*(?:spent|missing|conflict)|missing[- ]inputs?|already spent/.test(detail);
|
|
23069
|
+
const classifiedValidatorFailure = event.status != null && event.status >= 460 && event.status <= 475;
|
|
23070
|
+
const explicitInvalidStatus = isArcInvalidTxStatus(event.txStatus) && event.txStatus !== "REJECTED";
|
|
23071
|
+
const terminal = inputConflict || classifiedValidatorFailure || explicitInvalidStatus || extraInfo !== "";
|
|
23072
|
+
let reason = "Arcade supplied no durable rejection evidence";
|
|
23073
|
+
if (orphanConflict) reason = `Arcade reported ${event.txStatus}`;
|
|
23074
|
+
else if (inputConflict) reason = "Arcade supplied confirmed missing-input/conflict evidence";
|
|
23075
|
+
else if (classifiedValidatorFailure) reason = `Arcade supplied terminal validator code ${String(event.status)}`;
|
|
23076
|
+
else if (explicitInvalidStatus) reason = `Arcade reported terminal ${event.txStatus}`;
|
|
23077
|
+
else if (extraInfo !== "") reason = "Arcade supplied a terminal validator rejection reason";
|
|
23078
|
+
return {
|
|
23079
|
+
terminal,
|
|
23080
|
+
inputConflict,
|
|
23081
|
+
reqStatus: inputConflict ? "doubleSpend" : "invalid",
|
|
23082
|
+
reason
|
|
23083
|
+
};
|
|
23084
|
+
}
|
|
23085
|
+
//#endregion
|
|
22180
23086
|
//#region ../src/services/providers/Arcade.ts
|
|
22181
23087
|
function defaultDeploymentId() {
|
|
22182
23088
|
return `ts-sdk-${_bsv_sdk.Utils.toHex((0, _bsv_sdk.Random)(16))}`;
|
|
@@ -22446,6 +23352,73 @@ var Arcade = class {
|
|
|
22446
23352
|
return (await this.httpClient.request(`${this.URL}/tx/${txid}`, requestOptions)).data;
|
|
22447
23353
|
}
|
|
22448
23354
|
/**
|
|
23355
|
+
* Adapt Arcade's lifecycle endpoint to the shared transaction-status
|
|
23356
|
+
* provider contract. This lets monitor reconciliation remain operational
|
|
23357
|
+
* when an explorer such as WhatsOnChain is absent. Only network-observed
|
|
23358
|
+
* states count as known; RECEIVED/PENDING_RETRY and terminal rejection
|
|
23359
|
+
* states remain unknown, while MINED/IMMUTABLE are authoritative mined
|
|
23360
|
+
* observations whose proof is validated separately.
|
|
23361
|
+
*/
|
|
23362
|
+
async getStatusForTxids(txids) {
|
|
23363
|
+
const r = {
|
|
23364
|
+
name: this.name,
|
|
23365
|
+
status: "success",
|
|
23366
|
+
results: []
|
|
23367
|
+
};
|
|
23368
|
+
let firstError;
|
|
23369
|
+
r.results = await Promise.all(txids.map(async (txid) => {
|
|
23370
|
+
try {
|
|
23371
|
+
const response = await this.httpClient.request(`${this.URL}/tx/${txid}`, {
|
|
23372
|
+
method: "GET",
|
|
23373
|
+
headers: this.requestHeaders()
|
|
23374
|
+
});
|
|
23375
|
+
if (Number(response.status) === 404) return {
|
|
23376
|
+
txid,
|
|
23377
|
+
status: "unknown",
|
|
23378
|
+
depth: void 0
|
|
23379
|
+
};
|
|
23380
|
+
if (!response.ok || Number(response.status) !== 200) throw new WERR_INVALID_OPERATION(`Arcade transaction status response ${String(response.status)}`);
|
|
23381
|
+
const status = response.data.txStatus;
|
|
23382
|
+
if (status === "MINED" || status === "IMMUTABLE") return {
|
|
23383
|
+
txid,
|
|
23384
|
+
status: "mined",
|
|
23385
|
+
depth: 1
|
|
23386
|
+
};
|
|
23387
|
+
if (status === "ACCEPTED_BY_NETWORK" || status === "SEEN_ON_NETWORK" || status === "SEEN_MULTIPLE_NODES") return {
|
|
23388
|
+
txid,
|
|
23389
|
+
status: "known",
|
|
23390
|
+
depth: 0
|
|
23391
|
+
};
|
|
23392
|
+
const classification = classifyArcadeRejection(response.data);
|
|
23393
|
+
return {
|
|
23394
|
+
txid,
|
|
23395
|
+
status: "unknown",
|
|
23396
|
+
depth: void 0,
|
|
23397
|
+
providerStatus: status,
|
|
23398
|
+
...classification.terminal ? {
|
|
23399
|
+
terminal: true,
|
|
23400
|
+
inputConflict: classification.inputConflict,
|
|
23401
|
+
statusCode: response.data.status,
|
|
23402
|
+
description: (response.data.extraInfo || classification.reason).slice(0, 512),
|
|
23403
|
+
competingTxs: (response.data.competingTxs ?? []).slice(0, 24)
|
|
23404
|
+
} : {}
|
|
23405
|
+
};
|
|
23406
|
+
} catch (error_) {
|
|
23407
|
+
firstError ??= WalletError.fromUnknown(error_);
|
|
23408
|
+
return {
|
|
23409
|
+
txid,
|
|
23410
|
+
status: "unknown",
|
|
23411
|
+
depth: void 0
|
|
23412
|
+
};
|
|
23413
|
+
}
|
|
23414
|
+
}));
|
|
23415
|
+
if (firstError != null) {
|
|
23416
|
+
r.status = "error";
|
|
23417
|
+
r.error = firstError;
|
|
23418
|
+
}
|
|
23419
|
+
return r;
|
|
23420
|
+
}
|
|
23421
|
+
/**
|
|
22449
23422
|
* `getMerklePath` provider: obtain a BUMP merkle proof for a mined transaction from Arcade.
|
|
22450
23423
|
*
|
|
22451
23424
|
* Arcade only has a proof for transactions it tracked (i.e. broadcast through it) that have
|
|
@@ -23236,6 +24209,10 @@ var Services = class Services {
|
|
|
23236
24209
|
name: "WhatsOnChain",
|
|
23237
24210
|
service: this.whatsonchain.getStatusForTxids.bind(this.whatsonchain)
|
|
23238
24211
|
});
|
|
24212
|
+
if (this.arcade != null) this.getStatusForTxidsServices.add({
|
|
24213
|
+
name: "Arcade",
|
|
24214
|
+
service: this.arcade.getStatusForTxids.bind(this.arcade)
|
|
24215
|
+
});
|
|
23239
24216
|
this.getScriptHashHistoryServices = new ServiceCollection("getScriptHashHistory");
|
|
23240
24217
|
if (hasWhatsOnChain) this.getScriptHashHistoryServices.add({
|
|
23241
24218
|
name: "WhatsOnChain",
|
|
@@ -23252,7 +24229,7 @@ var Services = class Services {
|
|
|
23252
24229
|
name: "GorillaPoolArcBeef",
|
|
23253
24230
|
service: this.arcGorillaPool.postBeef.bind(this.arcGorillaPool)
|
|
23254
24231
|
});
|
|
23255
|
-
this.postBeefServices.add({
|
|
24232
|
+
if (this.options.arcUrl != null && this.options.arcUrl !== "") this.postBeefServices.add({
|
|
23256
24233
|
name: "TaalArcBeef",
|
|
23257
24234
|
service: this.arcTaal.postBeef.bind(this.arcTaal)
|
|
23258
24235
|
});
|
|
@@ -23338,30 +24315,65 @@ var Services = class Services {
|
|
|
23338
24315
|
async getStatusForTxids(txids, useNext) {
|
|
23339
24316
|
const services = this.getStatusForTxidsServices;
|
|
23340
24317
|
if (useNext === true) services.next();
|
|
23341
|
-
let
|
|
24318
|
+
let fallback = {
|
|
23342
24319
|
name: "<noservices>",
|
|
23343
24320
|
status: "error",
|
|
23344
24321
|
error: new WERR_INTERNAL("No services available."),
|
|
23345
24322
|
results: []
|
|
23346
24323
|
};
|
|
24324
|
+
const resultsByTxid = /* @__PURE__ */ new Map();
|
|
24325
|
+
const unresolved = new Set(txids);
|
|
24326
|
+
const providerNames = [];
|
|
24327
|
+
let successfulProvider = false;
|
|
24328
|
+
const rank = (result) => {
|
|
24329
|
+
if (result.status === "mined") return 4;
|
|
24330
|
+
if (result.status === "known") return 3;
|
|
24331
|
+
if (result.terminal === true) return 2;
|
|
24332
|
+
return 1;
|
|
24333
|
+
};
|
|
23347
24334
|
for (let tries = 0; tries < services.count; tries++) {
|
|
23348
24335
|
const stc = services.serviceToCall;
|
|
23349
24336
|
try {
|
|
23350
|
-
const
|
|
24337
|
+
const requestedTxids = [...unresolved];
|
|
24338
|
+
if (requestedTxids.length === 0) break;
|
|
24339
|
+
const r = await this.getStatusForTxidsBatched(stc, requestedTxids);
|
|
23351
24340
|
if (r.status === "success") {
|
|
23352
24341
|
services.addServiceCallSuccess(stc);
|
|
23353
|
-
|
|
23354
|
-
|
|
24342
|
+
successfulProvider = true;
|
|
24343
|
+
providerNames.push(r.name);
|
|
24344
|
+
for (const result of r.results) {
|
|
24345
|
+
const current = resultsByTxid.get(result.txid);
|
|
24346
|
+
if (current == null || rank(result) > rank(current)) resultsByTxid.set(result.txid, result);
|
|
24347
|
+
if (result.status === "mined" || result.status === "known") unresolved.delete(result.txid);
|
|
24348
|
+
}
|
|
24349
|
+
services.next();
|
|
24350
|
+
continue;
|
|
23355
24351
|
}
|
|
24352
|
+
fallback = r;
|
|
23356
24353
|
if (r.error != null) services.addServiceCallError(stc, r.error);
|
|
23357
24354
|
else services.addServiceCallFailure(stc);
|
|
23358
24355
|
} catch (error_) {
|
|
23359
24356
|
const e = WalletError.fromUnknown(error_);
|
|
24357
|
+
fallback = {
|
|
24358
|
+
name: stc.providerName,
|
|
24359
|
+
status: "error",
|
|
24360
|
+
error: e,
|
|
24361
|
+
results: []
|
|
24362
|
+
};
|
|
23360
24363
|
services.addServiceCallError(stc, e);
|
|
23361
24364
|
}
|
|
23362
24365
|
services.next();
|
|
23363
24366
|
}
|
|
23364
|
-
return
|
|
24367
|
+
if (!successfulProvider) return fallback;
|
|
24368
|
+
return {
|
|
24369
|
+
name: [...new Set(providerNames)].join(","),
|
|
24370
|
+
status: "success",
|
|
24371
|
+
results: txids.map((txid) => resultsByTxid.get(txid) ?? {
|
|
24372
|
+
txid,
|
|
24373
|
+
status: "unknown",
|
|
24374
|
+
depth: void 0
|
|
24375
|
+
})
|
|
24376
|
+
};
|
|
23365
24377
|
}
|
|
23366
24378
|
async getStatusForTxidsBatched(stc, txids) {
|
|
23367
24379
|
const results = [];
|
|
@@ -23388,9 +24400,7 @@ var Services = class Services {
|
|
|
23388
24400
|
return _bsv_sdk.Utils.toHex(sha256Hash(_bsv_sdk.Utils.toArray(script, "hex")));
|
|
23389
24401
|
}
|
|
23390
24402
|
async isUtxo(output) {
|
|
23391
|
-
|
|
23392
|
-
const hash = this.hashOutputScript(_bsv_sdk.Utils.toHex(output.lockingScript));
|
|
23393
|
-
return (await this.getUtxoStatus(hash, void 0, `${output.txid ?? ""}.${output.vout}`)).isUtxo === true;
|
|
24403
|
+
return requireConclusiveUtxo(await classifyOutputUtxo(this, output));
|
|
23394
24404
|
}
|
|
23395
24405
|
async getUtxoStatus(output, outputFormat, outpoint, useNext, logger) {
|
|
23396
24406
|
const services = this.getUtxoStatusServices;
|
|
@@ -23616,7 +24626,15 @@ var Services = class Services {
|
|
|
23616
24626
|
const method = async () => {
|
|
23617
24627
|
return await this.options.chaintracks.currentHeight();
|
|
23618
24628
|
};
|
|
23619
|
-
|
|
24629
|
+
try {
|
|
24630
|
+
return await this.invokeChaintracksWithRetry(method, "current_height");
|
|
24631
|
+
} catch (error_) {
|
|
24632
|
+
try {
|
|
24633
|
+
return (await this.whatsonchain.getChainInfo()).blocks;
|
|
24634
|
+
} catch {
|
|
24635
|
+
throw error_;
|
|
24636
|
+
}
|
|
24637
|
+
}
|
|
23620
24638
|
}
|
|
23621
24639
|
async hashToHeader(hash) {
|
|
23622
24640
|
const method = async () => {
|
|
@@ -27335,17 +28353,34 @@ var ArcSSEClient = class {
|
|
|
27335
28353
|
console.log(`${TAG} connected`);
|
|
27336
28354
|
});
|
|
27337
28355
|
this.es.addEventListener("status", (event) => {
|
|
28356
|
+
let data;
|
|
27338
28357
|
try {
|
|
27339
|
-
|
|
27340
|
-
console.log(`${TAG} event: txid=${data.txid} status=${data.txStatus}`);
|
|
27341
|
-
if (typeof event.lastEventId === "string" && event.lastEventId !== "") {
|
|
27342
|
-
this._lastEventId = event.lastEventId;
|
|
27343
|
-
this.options.onLastEventIdChanged?.(event.lastEventId);
|
|
27344
|
-
}
|
|
27345
|
-
this.options.onEvent(data);
|
|
28358
|
+
data = JSON.parse(event.data);
|
|
27346
28359
|
} catch {
|
|
27347
28360
|
console.log(`${TAG} malformed event: ${String(event.data).substring(0, 200)}`);
|
|
28361
|
+
return;
|
|
28362
|
+
}
|
|
28363
|
+
console.log(`${TAG} event: txid=${data.txid} status=${data.txStatus}`);
|
|
28364
|
+
const receivedEventId = typeof event.lastEventId === "string" && event.lastEventId !== "" ? event.lastEventId : void 0;
|
|
28365
|
+
data.eventId = receivedEventId;
|
|
28366
|
+
let processing;
|
|
28367
|
+
try {
|
|
28368
|
+
processing = this.options.onEvent(data);
|
|
28369
|
+
} catch (error) {
|
|
28370
|
+
const e = error instanceof Error ? error : new Error(String(error));
|
|
28371
|
+
console.log(`${TAG} event processing failed: ${e.message}`);
|
|
28372
|
+
this.options.onError?.(e);
|
|
28373
|
+
return;
|
|
27348
28374
|
}
|
|
28375
|
+
Promise.resolve(processing).then(async () => {
|
|
28376
|
+
if (receivedEventId == null) return;
|
|
28377
|
+
await this.options.onLastEventIdChanged?.(receivedEventId);
|
|
28378
|
+
this._lastEventId = receivedEventId;
|
|
28379
|
+
}).catch((error) => {
|
|
28380
|
+
const e = error instanceof Error ? error : new Error(String(error));
|
|
28381
|
+
console.log(`${TAG} event processing failed: ${e.message}`);
|
|
28382
|
+
this.options.onError?.(e);
|
|
28383
|
+
});
|
|
27349
28384
|
});
|
|
27350
28385
|
this.es.addEventListener("error", (event) => {
|
|
27351
28386
|
console.log(`${TAG} error:`, JSON.stringify(event));
|
|
@@ -27380,94 +28415,6 @@ var ArcSSEClient = class {
|
|
|
27380
28415
|
}
|
|
27381
28416
|
};
|
|
27382
28417
|
//#endregion
|
|
27383
|
-
//#region ../src/monitor/tasks/TaskUnFail.ts
|
|
27384
|
-
/**
|
|
27385
|
-
* Setting provenTxReq status to 'unfail' when 'invalid' will attempt to find a merklePath, and if successful:
|
|
27386
|
-
*
|
|
27387
|
-
* 1. set the req status to 'unmined'
|
|
27388
|
-
* 2. set the referenced txs to 'unproven'
|
|
27389
|
-
* 3. determine if any inputs match user's existing outputs and if so update spentBy and spendable of those outputs.
|
|
27390
|
-
* 4. set the txs outputs to spendable
|
|
27391
|
-
*
|
|
27392
|
-
* If it fails (to find a merklePath), returns the req status to 'invalid'.
|
|
27393
|
-
*/
|
|
27394
|
-
var TaskUnFail = class TaskUnFail extends WalletMonitorTask {
|
|
27395
|
-
triggerMsecs;
|
|
27396
|
-
static taskName = "UnFail";
|
|
27397
|
-
/**
|
|
27398
|
-
* Set to true to trigger running this task
|
|
27399
|
-
*/
|
|
27400
|
-
static checkNowRequested = false;
|
|
27401
|
-
static get checkNow() {
|
|
27402
|
-
return this.checkNowRequested;
|
|
27403
|
-
}
|
|
27404
|
-
static set checkNow(value) {
|
|
27405
|
-
this.checkNowRequested = value;
|
|
27406
|
-
}
|
|
27407
|
-
constructor(monitor, triggerMsecs = Monitor.oneMinute * 10) {
|
|
27408
|
-
super(monitor, TaskUnFail.taskName);
|
|
27409
|
-
this.triggerMsecs = triggerMsecs;
|
|
27410
|
-
}
|
|
27411
|
-
trigger(nowMsecsSinceEpoch) {
|
|
27412
|
-
return { run: TaskUnFail.checkNow || this.triggerMsecs > 0 && nowMsecsSinceEpoch - this.lastRunMsecsSinceEpoch > this.triggerMsecs };
|
|
27413
|
-
}
|
|
27414
|
-
async runTask() {
|
|
27415
|
-
let log = "";
|
|
27416
|
-
TaskUnFail.checkNow = false;
|
|
27417
|
-
const limit = 100;
|
|
27418
|
-
let offset = 0;
|
|
27419
|
-
for (;;) {
|
|
27420
|
-
const reqs = await this.storage.findProvenTxReqs({
|
|
27421
|
-
partial: {},
|
|
27422
|
-
status: ["unfail"],
|
|
27423
|
-
paged: {
|
|
27424
|
-
limit,
|
|
27425
|
-
offset
|
|
27426
|
-
}
|
|
27427
|
-
});
|
|
27428
|
-
if (reqs.length === 0) break;
|
|
27429
|
-
log += `${reqs.length} reqs with status 'unfail'\n`;
|
|
27430
|
-
const r = await this.unfail(reqs, 2);
|
|
27431
|
-
log += `${r.log}\n`;
|
|
27432
|
-
if (reqs.length < limit) break;
|
|
27433
|
-
offset += limit;
|
|
27434
|
-
}
|
|
27435
|
-
return log;
|
|
27436
|
-
}
|
|
27437
|
-
async unfail(reqs, indent = 0) {
|
|
27438
|
-
let log = "";
|
|
27439
|
-
for (const reqApi of reqs) {
|
|
27440
|
-
const req = new EntityProvenTxReq(reqApi);
|
|
27441
|
-
log += " ".repeat(indent);
|
|
27442
|
-
log += `reqId ${reqApi.provenTxReqId} txid ${reqApi.txid}: `;
|
|
27443
|
-
if ((await this.monitor.services.getMerklePath(req.txid)).merklePath != null) {
|
|
27444
|
-
log += "unfailed. status is now 'unmined'\n";
|
|
27445
|
-
log += await this.unfailReq(req, indent + 2);
|
|
27446
|
-
} else {
|
|
27447
|
-
req.status = "invalid";
|
|
27448
|
-
log += "returned to status 'invalid'\n";
|
|
27449
|
-
await req.updateStorageDynamicProperties(this.storage);
|
|
27450
|
-
}
|
|
27451
|
-
}
|
|
27452
|
-
return { log };
|
|
27453
|
-
}
|
|
27454
|
-
/**
|
|
27455
|
-
* 2. set the referenced txs to 'unproven'
|
|
27456
|
-
* 3. determine if any inputs match user's existing outputs and if so update spentBy and spendable of those outputs.
|
|
27457
|
-
* 4. set the txs outputs to spendable
|
|
27458
|
-
*
|
|
27459
|
-
* @param req
|
|
27460
|
-
* @param indent
|
|
27461
|
-
* @returns
|
|
27462
|
-
*/
|
|
27463
|
-
async unfailReq(req, indent) {
|
|
27464
|
-
return await this.storage.runAsStorageProvider(async (sp) => await sp.unfailTransactionsForProof(req, indent, {
|
|
27465
|
-
status: "unmined",
|
|
27466
|
-
attempts: 0
|
|
27467
|
-
}));
|
|
27468
|
-
}
|
|
27469
|
-
};
|
|
27470
|
-
//#endregion
|
|
27471
28418
|
//#region ../src/monitor/tasks/TaskArcSSE.ts
|
|
27472
28419
|
/**
|
|
27473
28420
|
* Monitor task that receives transaction status updates from Arcade via SSE
|
|
@@ -27511,16 +28458,14 @@ var TaskArcadeSSE = class TaskArcadeSSE extends WalletMonitorTask {
|
|
|
27511
28458
|
arcApiKey: arcadeApiKey,
|
|
27512
28459
|
lastEventId,
|
|
27513
28460
|
EventSourceClass,
|
|
27514
|
-
onEvent: (event) => {
|
|
27515
|
-
this.pendingEvents.push(
|
|
27516
|
-
|
|
28461
|
+
onEvent: async (event) => await new Promise((acknowledge) => {
|
|
28462
|
+
this.pendingEvents.push({
|
|
28463
|
+
event,
|
|
28464
|
+
acknowledge
|
|
28465
|
+
});
|
|
28466
|
+
}),
|
|
27517
28467
|
onError: (err) => {
|
|
27518
28468
|
this.logSetupEvent(`SSE error: ${err.message}`);
|
|
27519
|
-
},
|
|
27520
|
-
onLastEventIdChanged: (id) => {
|
|
27521
|
-
this.monitor.options.saveLastSSEEventId?.(id).catch((e) => {
|
|
27522
|
-
this.logSetupEvent(`failed to persist lastEventId: ${stringifyError(e)}`);
|
|
27523
|
-
});
|
|
27524
28469
|
}
|
|
27525
28470
|
});
|
|
27526
28471
|
this.sseClient.connect();
|
|
@@ -27534,10 +28479,21 @@ var TaskArcadeSSE = class TaskArcadeSSE extends WalletMonitorTask {
|
|
|
27534
28479
|
return { run: this.pendingEvents.length > 0 };
|
|
27535
28480
|
}
|
|
27536
28481
|
async runTask() {
|
|
27537
|
-
const
|
|
27538
|
-
if (
|
|
28482
|
+
const eventCount = this.pendingEvents.length;
|
|
28483
|
+
if (eventCount === 0) return "";
|
|
27539
28484
|
let log = "";
|
|
27540
|
-
for (
|
|
28485
|
+
for (let i = 0; i < eventCount; i++) {
|
|
28486
|
+
const pending = this.pendingEvents[0];
|
|
28487
|
+
log += await this.processStatusEvent(pending.event);
|
|
28488
|
+
if (pending.event.eventId != null) try {
|
|
28489
|
+
await this.monitor.options.saveLastSSEEventId?.(pending.event.eventId);
|
|
28490
|
+
} catch (e) {
|
|
28491
|
+
await this.logSetupEvent(`failed to persist lastEventId: ${stringifyError(e)}`);
|
|
28492
|
+
throw e;
|
|
28493
|
+
}
|
|
28494
|
+
this.pendingEvents.shift();
|
|
28495
|
+
pending.acknowledge();
|
|
28496
|
+
}
|
|
27541
28497
|
return log;
|
|
27542
28498
|
}
|
|
27543
28499
|
async fetchNow() {
|
|
@@ -27553,28 +28509,26 @@ var TaskArcadeSSE = class TaskArcadeSSE extends WalletMonitorTask {
|
|
|
27553
28509
|
}
|
|
27554
28510
|
for (const reqApi of reqs) {
|
|
27555
28511
|
const req = new EntityProvenTxReq(reqApi);
|
|
27556
|
-
const
|
|
27557
|
-
if (ProvenTxReqTerminalStatus.includes(req.status) && !
|
|
28512
|
+
const canRecoverTerminalRequest = this.canRecoverTerminalRequest(req, event);
|
|
28513
|
+
if (ProvenTxReqTerminalStatus.includes(req.status) && !canRecoverTerminalRequest) {
|
|
27558
28514
|
log += ` req ${req.id} already terminal: ${req.status}\n`;
|
|
27559
28515
|
continue;
|
|
27560
28516
|
}
|
|
27561
28517
|
const note = {
|
|
27562
28518
|
when: (/* @__PURE__ */ new Date()).toISOString(),
|
|
27563
28519
|
what: "arcSSE",
|
|
27564
|
-
arcStatus: event.txStatus
|
|
28520
|
+
arcStatus: event.txStatus,
|
|
28521
|
+
...event.timestamp !== "" ? { arcTimestamp: event.timestamp } : {},
|
|
28522
|
+
...event.status != null ? { statusCode: event.status } : {},
|
|
28523
|
+
...event.extraInfo != null && event.extraInfo !== "" ? { extraInfo: event.extraInfo.slice(0, 512) } : {}
|
|
27565
28524
|
};
|
|
27566
28525
|
log += await this.applyStatusEvent(req, event, note);
|
|
27567
28526
|
}
|
|
27568
28527
|
this.monitor.callOnTransactionStatusChanged(event.txid, event.txStatus);
|
|
27569
28528
|
return log;
|
|
27570
28529
|
}
|
|
27571
|
-
|
|
27572
|
-
return req.status === "invalid" && (
|
|
27573
|
-
"SENT_TO_NETWORK",
|
|
27574
|
-
"ACCEPTED_BY_NETWORK",
|
|
27575
|
-
"SEEN_ON_NETWORK",
|
|
27576
|
-
"SEEN_MULTIPLE_NODES"
|
|
27577
|
-
].includes(event.txStatus) || ["MINED", "IMMUTABLE"].includes(event.txStatus));
|
|
28530
|
+
canRecoverTerminalRequest(req, event) {
|
|
28531
|
+
return (req.status === "invalid" || req.status === "doubleSpend") && ["MINED", "IMMUTABLE"].includes(event.txStatus);
|
|
27578
28532
|
}
|
|
27579
28533
|
async applyStatusEvent(req, event, note) {
|
|
27580
28534
|
switch (event.txStatus) {
|
|
@@ -27584,11 +28538,16 @@ var TaskArcadeSSE = class TaskArcadeSSE extends WalletMonitorTask {
|
|
|
27584
28538
|
case "SEEN_MULTIPLE_NODES": return await this.applyAcceptedStatus(req, note);
|
|
27585
28539
|
case "MINED":
|
|
27586
28540
|
case "IMMUTABLE": return await this.applyMinedStatus(req, note);
|
|
27587
|
-
case "DOUBLE_SPEND_ATTEMPTED":
|
|
27588
|
-
case "
|
|
28541
|
+
case "DOUBLE_SPEND_ATTEMPTED":
|
|
28542
|
+
case "SEEN_IN_ORPHAN_MEMPOOL": return await this.applyDoubleSpendStatus(req, event, note);
|
|
28543
|
+
case "REJECTED": return await this.applyRejectedStatus(req, event, note);
|
|
28544
|
+
case "RECEIVED":
|
|
28545
|
+
case "PENDING_RETRY":
|
|
28546
|
+
case "STUMP_PROCESSING":
|
|
28547
|
+
case "UNKNOWN":
|
|
27589
28548
|
req.addHistoryNote(note);
|
|
27590
28549
|
await req.updateStorageDynamicProperties(this.storage);
|
|
27591
|
-
return ` req ${req.id}
|
|
28550
|
+
return ` req ${req.id} ${event.txStatus} recorded; awaiting resolution\n`;
|
|
27592
28551
|
default: return ` req ${req.id} unhandled status: ${event.txStatus}\n`;
|
|
27593
28552
|
}
|
|
27594
28553
|
}
|
|
@@ -27596,43 +28555,73 @@ var TaskArcadeSSE = class TaskArcadeSSE extends WalletMonitorTask {
|
|
|
27596
28555
|
if (![
|
|
27597
28556
|
"unsent",
|
|
27598
28557
|
"sending",
|
|
27599
|
-
"callback"
|
|
27600
|
-
"invalid"
|
|
28558
|
+
"callback"
|
|
27601
28559
|
].includes(req.status)) return "";
|
|
27602
|
-
|
|
27603
|
-
let log = "";
|
|
27604
|
-
if (wasInvalid) log += await new TaskUnFail(this.monitor).unfailReq(req, 4);
|
|
27605
|
-
else if (req.notify.transactionIds != null) await this.storage.runAsStorageProvider(async (sp) => {
|
|
28560
|
+
if (req.notify.transactionIds != null) await this.storage.runAsStorageProvider(async (sp) => {
|
|
27606
28561
|
await sp.updateTransactionsStatus(req.notify.transactionIds ?? [], "unproven");
|
|
27607
28562
|
});
|
|
27608
28563
|
req.status = "unmined";
|
|
27609
|
-
if (wasInvalid) req.attempts = 0;
|
|
27610
28564
|
req.wasBroadcast = true;
|
|
27611
28565
|
req.addHistoryNote(note);
|
|
27612
28566
|
await req.updateStorageDynamicProperties(this.storage);
|
|
27613
|
-
return
|
|
28567
|
+
return ` req ${req.id} => unmined\n`;
|
|
27614
28568
|
}
|
|
27615
28569
|
async applyMinedStatus(req, note) {
|
|
27616
|
-
let log = "";
|
|
27617
|
-
if (req.status === "invalid") {
|
|
27618
|
-
log += await new TaskUnFail(this.monitor).unfailReq(req, 4);
|
|
27619
|
-
req.status = "unmined";
|
|
27620
|
-
req.attempts = 0;
|
|
27621
|
-
req.wasBroadcast = true;
|
|
27622
|
-
}
|
|
27623
28570
|
req.addHistoryNote(note);
|
|
27624
28571
|
await req.updateStorageDynamicProperties(this.storage);
|
|
27625
|
-
return
|
|
28572
|
+
return await this.fetchProofFromServices(req);
|
|
27626
28573
|
}
|
|
27627
|
-
async applyDoubleSpendStatus(req, note) {
|
|
27628
|
-
req
|
|
27629
|
-
|
|
27630
|
-
|
|
27631
|
-
|
|
27632
|
-
|
|
27633
|
-
|
|
28574
|
+
async applyDoubleSpendStatus(req, event, note) {
|
|
28575
|
+
return await this.applyTerminalFailure(req, note, classifyArcadeRejection(event));
|
|
28576
|
+
}
|
|
28577
|
+
classifyRejection(event) {
|
|
28578
|
+
return classifyArcadeRejection(event);
|
|
28579
|
+
}
|
|
28580
|
+
async applyRejectedStatus(req, event, note) {
|
|
28581
|
+
const classification = this.classifyRejection(event);
|
|
28582
|
+
if (!classification.terminal) {
|
|
28583
|
+
req.addHistoryNote({
|
|
28584
|
+
...note,
|
|
28585
|
+
what: "arcSSERejectionPending",
|
|
28586
|
+
reason: classification.reason
|
|
28587
|
+
});
|
|
28588
|
+
await req.updateStorageDynamicProperties(this.storage);
|
|
28589
|
+
return ` req ${req.id} rejection recorded; awaiting resolution\n`;
|
|
28590
|
+
}
|
|
28591
|
+
return await this.applyTerminalFailure(req, note, classification);
|
|
28592
|
+
}
|
|
28593
|
+
async applyTerminalFailure(req, note, classification) {
|
|
28594
|
+
let quarantined = {
|
|
28595
|
+
checked: 0,
|
|
28596
|
+
staleConfirmed: 0,
|
|
28597
|
+
staleOutpoints: []
|
|
28598
|
+
};
|
|
28599
|
+
await this.storage.runAsStorageProvider(async (sp) => {
|
|
28600
|
+
await sp.transaction(async (trx) => {
|
|
28601
|
+
req.status = classification.reqStatus;
|
|
28602
|
+
req.addHistoryNote({
|
|
28603
|
+
...note,
|
|
28604
|
+
what: "arcSSETerminalRejection",
|
|
28605
|
+
reason: classification.reason,
|
|
28606
|
+
inputConflict: classification.inputConflict
|
|
28607
|
+
});
|
|
28608
|
+
await req.updateStorageDynamicProperties(sp, trx);
|
|
28609
|
+
const ids = req.notify.transactionIds;
|
|
28610
|
+
if (ids != null) await sp.updateTransactionsStatus(ids, "failed", trx);
|
|
28611
|
+
if (classification.inputConflict) {
|
|
28612
|
+
quarantined = await quarantineReqInputs(req, sp, trx);
|
|
28613
|
+
req.addHistoryNote({
|
|
28614
|
+
when: (/* @__PURE__ */ new Date()).toISOString(),
|
|
28615
|
+
what: "arcSSEInputQuarantine",
|
|
28616
|
+
checked: quarantined.checked,
|
|
28617
|
+
confirmed: quarantined.staleConfirmed,
|
|
28618
|
+
...quarantined.staleOutpoints.length > 0 ? { outpoints: quarantined.staleOutpoints.join(",") } : {}
|
|
28619
|
+
});
|
|
28620
|
+
await req.updateStorageDynamicProperties(sp, trx);
|
|
28621
|
+
}
|
|
28622
|
+
});
|
|
27634
28623
|
});
|
|
27635
|
-
return ` req ${req.id} =>
|
|
28624
|
+
return ` req ${req.id} => ${classification.reqStatus}; quarantined ${quarantined.staleConfirmed} local input copy/copies\n`;
|
|
27636
28625
|
}
|
|
27637
28626
|
/**
|
|
27638
28627
|
* Complete a MINED/IMMUTABLE status by using the configured proof providers.
|
|
@@ -28044,9 +29033,104 @@ var TaskCheckNoSends = class TaskCheckNoSends extends WalletMonitorTask {
|
|
|
28044
29033
|
}
|
|
28045
29034
|
};
|
|
28046
29035
|
//#endregion
|
|
29036
|
+
//#region ../src/monitor/tasks/TaskUnFail.ts
|
|
29037
|
+
/**
|
|
29038
|
+
* Setting provenTxReq status to 'unfail' when 'invalid' will attempt to find a merklePath, and if successful:
|
|
29039
|
+
*
|
|
29040
|
+
* 1. set the req status to 'unmined'
|
|
29041
|
+
* 2. set the referenced txs to 'unproven'
|
|
29042
|
+
* 3. determine if any inputs match user's existing outputs and if so update spentBy and spendable of those outputs.
|
|
29043
|
+
* 4. set the txs outputs to spendable
|
|
29044
|
+
*
|
|
29045
|
+
* If it fails (to find a merklePath), returns the req status to 'invalid'.
|
|
29046
|
+
*/
|
|
29047
|
+
var TaskUnFail = class TaskUnFail extends WalletMonitorTask {
|
|
29048
|
+
triggerMsecs;
|
|
29049
|
+
static taskName = "UnFail";
|
|
29050
|
+
/**
|
|
29051
|
+
* Set to true to trigger running this task
|
|
29052
|
+
*/
|
|
29053
|
+
static checkNowRequested = false;
|
|
29054
|
+
static get checkNow() {
|
|
29055
|
+
return this.checkNowRequested;
|
|
29056
|
+
}
|
|
29057
|
+
static set checkNow(value) {
|
|
29058
|
+
this.checkNowRequested = value;
|
|
29059
|
+
}
|
|
29060
|
+
constructor(monitor, triggerMsecs = Monitor.oneMinute * 10) {
|
|
29061
|
+
super(monitor, TaskUnFail.taskName);
|
|
29062
|
+
this.triggerMsecs = triggerMsecs;
|
|
29063
|
+
}
|
|
29064
|
+
trigger(nowMsecsSinceEpoch) {
|
|
29065
|
+
return { run: TaskUnFail.checkNow || this.triggerMsecs > 0 && nowMsecsSinceEpoch - this.lastRunMsecsSinceEpoch > this.triggerMsecs };
|
|
29066
|
+
}
|
|
29067
|
+
async runTask() {
|
|
29068
|
+
let log = "";
|
|
29069
|
+
TaskUnFail.checkNow = false;
|
|
29070
|
+
const limit = 100;
|
|
29071
|
+
let offset = 0;
|
|
29072
|
+
for (;;) {
|
|
29073
|
+
const reqs = await this.storage.findProvenTxReqs({
|
|
29074
|
+
partial: {},
|
|
29075
|
+
status: ["unfail"],
|
|
29076
|
+
paged: {
|
|
29077
|
+
limit,
|
|
29078
|
+
offset
|
|
29079
|
+
}
|
|
29080
|
+
});
|
|
29081
|
+
if (reqs.length === 0) break;
|
|
29082
|
+
log += `${reqs.length} reqs with status 'unfail'\n`;
|
|
29083
|
+
const r = await this.unfail(reqs, 2);
|
|
29084
|
+
log += `${r.log}\n`;
|
|
29085
|
+
if (reqs.length < limit) break;
|
|
29086
|
+
offset += limit;
|
|
29087
|
+
}
|
|
29088
|
+
return log;
|
|
29089
|
+
}
|
|
29090
|
+
async unfail(reqs, indent = 0) {
|
|
29091
|
+
let log = "";
|
|
29092
|
+
for (const reqApi of reqs) {
|
|
29093
|
+
const req = new EntityProvenTxReq(reqApi);
|
|
29094
|
+
log += " ".repeat(indent);
|
|
29095
|
+
log += `reqId ${reqApi.provenTxReqId} txid ${reqApi.txid}: `;
|
|
29096
|
+
if ((await this.monitor.services.getMerklePath(req.txid)).merklePath != null) {
|
|
29097
|
+
log += "unfailed. status is now 'unmined'\n";
|
|
29098
|
+
log += await this.unfailReq(req, indent + 2);
|
|
29099
|
+
} else {
|
|
29100
|
+
req.status = "invalid";
|
|
29101
|
+
log += "returned to status 'invalid'\n";
|
|
29102
|
+
await req.updateStorageDynamicProperties(this.storage);
|
|
29103
|
+
}
|
|
29104
|
+
}
|
|
29105
|
+
return { log };
|
|
29106
|
+
}
|
|
29107
|
+
/**
|
|
29108
|
+
* 2. set the referenced txs to 'unproven'
|
|
29109
|
+
* 3. determine if any inputs match user's existing outputs and if so update spentBy and spendable of those outputs.
|
|
29110
|
+
* 4. set the txs outputs to spendable
|
|
29111
|
+
*
|
|
29112
|
+
* @param req
|
|
29113
|
+
* @param indent
|
|
29114
|
+
* @returns
|
|
29115
|
+
*/
|
|
29116
|
+
async unfailReq(req, indent) {
|
|
29117
|
+
return await this.storage.runAsStorageProvider(async (sp) => await sp.unfailTransactionsForProof(req, indent, {
|
|
29118
|
+
status: "unmined",
|
|
29119
|
+
attempts: 0
|
|
29120
|
+
}));
|
|
29121
|
+
}
|
|
29122
|
+
};
|
|
29123
|
+
//#endregion
|
|
28047
29124
|
//#region ../src/monitor/tasks/TaskReviewUtxos.ts
|
|
29125
|
+
const REVIEW_PAGE_DEFAULT_LIMIT = 20;
|
|
29126
|
+
const REVIEW_PAGE_MAX_LIMIT = 250;
|
|
28048
29127
|
/**
|
|
28049
|
-
* Use the reviewByIdentityKey method to
|
|
29128
|
+
* Use the reviewByIdentityKey method to scan the UTXOs of a specific user by
|
|
29129
|
+
* identity key. The scan is read-only unless the caller explicitly requests
|
|
29130
|
+
* release, and release remains blocked if any provider result is inconclusive.
|
|
29131
|
+
* Operator UIs should use reviewPageByIdentityKey: it bounds each provider
|
|
29132
|
+
* round-trip and may explicitly release only the conclusive spent subset while
|
|
29133
|
+
* reporting unknowns.
|
|
28050
29134
|
*
|
|
28051
29135
|
* The task itself is disabled and will not run on a schedule; review must be triggered manually by calling reviewByIdentityKey.
|
|
28052
29136
|
*/
|
|
@@ -28063,7 +29147,7 @@ var TaskReviewUtxos = class TaskReviewUtxos extends WalletMonitorTask {
|
|
|
28063
29147
|
static set checkNow(value) {
|
|
28064
29148
|
this.checkNowRequested = value;
|
|
28065
29149
|
}
|
|
28066
|
-
constructor(monitor, triggerMsecs = 0, userLimit = 10, userOffset = 0, tags = ["
|
|
29150
|
+
constructor(monitor, triggerMsecs = 0, userLimit = 10, userOffset = 0, tags = ["all"]) {
|
|
28067
29151
|
super(monitor, TaskReviewUtxos.taskName);
|
|
28068
29152
|
this.triggerMsecs = triggerMsecs;
|
|
28069
29153
|
this.userLimit = userLimit;
|
|
@@ -28077,8 +29161,8 @@ var TaskReviewUtxos = class TaskReviewUtxos extends WalletMonitorTask {
|
|
|
28077
29161
|
TaskReviewUtxos.checkNow = false;
|
|
28078
29162
|
return "TaskReviewUtxos is disabled; use reviewByIdentityKey instead.\n";
|
|
28079
29163
|
}
|
|
28080
|
-
async reviewByIdentityKey(identityKey, mode = "all") {
|
|
28081
|
-
const tags = ["release", ...mode === "all" ? ["all"] : []];
|
|
29164
|
+
async reviewByIdentityKey(identityKey, mode = "all", release = false) {
|
|
29165
|
+
const tags = [...release ? ["release"] : [], ...mode === "all" ? ["all"] : []];
|
|
28082
29166
|
const vargs = {
|
|
28083
29167
|
basket: specOpInvalidChange,
|
|
28084
29168
|
tags,
|
|
@@ -28106,8 +29190,150 @@ var TaskReviewUtxos = class TaskReviewUtxos extends WalletMonitorTask {
|
|
|
28106
29190
|
return this.toUserLog(user, result.outputs, result.totalOutputs, total, tags);
|
|
28107
29191
|
});
|
|
28108
29192
|
}
|
|
29193
|
+
async reviewPageByIdentityKey(identityKey, mode = "all", release = false, pageLimit = REVIEW_PAGE_DEFAULT_LIMIT, offset = 0) {
|
|
29194
|
+
pageLimit = Math.min(Math.max(Math.trunc(pageLimit), 1), REVIEW_PAGE_MAX_LIMIT);
|
|
29195
|
+
offset = Math.max(Math.trunc(offset), 0);
|
|
29196
|
+
return await this.storage.runAsStorageProvider(async (sp) => {
|
|
29197
|
+
const user = (await sp.findUsers({ partial: { identityKey } }))[0];
|
|
29198
|
+
if (!user) return {
|
|
29199
|
+
found: false,
|
|
29200
|
+
identityKey,
|
|
29201
|
+
mode,
|
|
29202
|
+
release,
|
|
29203
|
+
offset,
|
|
29204
|
+
pageLimit,
|
|
29205
|
+
sourceScanned: 0,
|
|
29206
|
+
complete: true,
|
|
29207
|
+
checked: 0,
|
|
29208
|
+
confirmedUnspent: 0,
|
|
29209
|
+
confirmedSpent: 0,
|
|
29210
|
+
unknown: 0,
|
|
29211
|
+
confirmedSpentSatoshis: 0,
|
|
29212
|
+
released: 0,
|
|
29213
|
+
releasedSatoshis: 0,
|
|
29214
|
+
providers: [],
|
|
29215
|
+
providerCount: 0,
|
|
29216
|
+
providersTruncated: false,
|
|
29217
|
+
log: `identityKey ${identityKey} was not found\n`
|
|
29218
|
+
};
|
|
29219
|
+
let basketId;
|
|
29220
|
+
if (mode === "change") {
|
|
29221
|
+
basketId = (await sp.findOutputBaskets({ partial: {
|
|
29222
|
+
userId: user.userId,
|
|
29223
|
+
name: "default"
|
|
29224
|
+
} }))[0]?.basketId;
|
|
29225
|
+
if (basketId == null) return this.emptyPage(user, mode, release, pageLimit, offset);
|
|
29226
|
+
}
|
|
29227
|
+
const sourceOutputs = await sp.findOutputs({
|
|
29228
|
+
partial: {
|
|
29229
|
+
userId: user.userId,
|
|
29230
|
+
spendable: true,
|
|
29231
|
+
...basketId != null ? { basketId } : {}
|
|
29232
|
+
},
|
|
29233
|
+
txStatus: [
|
|
29234
|
+
"completed",
|
|
29235
|
+
"unproven",
|
|
29236
|
+
"nosend",
|
|
29237
|
+
"sending"
|
|
29238
|
+
],
|
|
29239
|
+
noScript: true,
|
|
29240
|
+
paged: {
|
|
29241
|
+
limit: pageLimit,
|
|
29242
|
+
offset
|
|
29243
|
+
}
|
|
29244
|
+
});
|
|
29245
|
+
const candidates = sourceOutputs.filter((output) => output.basketId != null);
|
|
29246
|
+
const review = await reviewUtxoOutputs(sp, {
|
|
29247
|
+
userId: user.userId,
|
|
29248
|
+
identityKey: user.identityKey
|
|
29249
|
+
}, candidates, release ? "conclusive" : "none");
|
|
29250
|
+
const complete = sourceOutputs.length < pageLimit;
|
|
29251
|
+
const nextOffset = complete ? void 0 : offset + sourceOutputs.length - review.diagnostics.released;
|
|
29252
|
+
const target = mode === "all" ? "spendable utxos" : "spendable change utxos";
|
|
29253
|
+
const action = release ? "released" : "found";
|
|
29254
|
+
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`;
|
|
29255
|
+
for (const output of review.confirmedSpentOutputs) log += ` ${output.txid}.${output.vout} ${output.satoshis} now ${output.spendable ? "spendable" : "spent"}\n`;
|
|
29256
|
+
if (review.unknownOutputs.length > 0) log += ` ${review.unknownOutputs.length} output(s) quarantined from release pending a conclusive provider result\n`;
|
|
29257
|
+
if (nextOffset != null) log += ` continue at offset ${nextOffset}\n`;
|
|
29258
|
+
return {
|
|
29259
|
+
found: true,
|
|
29260
|
+
userId: user.userId,
|
|
29261
|
+
identityKey: user.identityKey,
|
|
29262
|
+
mode,
|
|
29263
|
+
release,
|
|
29264
|
+
offset,
|
|
29265
|
+
pageLimit,
|
|
29266
|
+
sourceScanned: sourceOutputs.length,
|
|
29267
|
+
complete,
|
|
29268
|
+
...nextOffset != null ? { nextOffset } : {},
|
|
29269
|
+
...review.diagnostics,
|
|
29270
|
+
log
|
|
29271
|
+
};
|
|
29272
|
+
});
|
|
29273
|
+
}
|
|
29274
|
+
emptyPage(user, mode, release, pageLimit, offset) {
|
|
29275
|
+
return {
|
|
29276
|
+
found: true,
|
|
29277
|
+
userId: user.userId,
|
|
29278
|
+
identityKey: user.identityKey,
|
|
29279
|
+
mode,
|
|
29280
|
+
release,
|
|
29281
|
+
offset,
|
|
29282
|
+
pageLimit,
|
|
29283
|
+
sourceScanned: 0,
|
|
29284
|
+
complete: true,
|
|
29285
|
+
checked: 0,
|
|
29286
|
+
confirmedUnspent: 0,
|
|
29287
|
+
confirmedSpent: 0,
|
|
29288
|
+
unknown: 0,
|
|
29289
|
+
confirmedSpentSatoshis: 0,
|
|
29290
|
+
released: 0,
|
|
29291
|
+
releasedSatoshis: 0,
|
|
29292
|
+
providers: [],
|
|
29293
|
+
providerCount: 0,
|
|
29294
|
+
providersTruncated: false,
|
|
29295
|
+
log: `userId ${user.userId}: no invalid utxos found, ${user.identityKey}\n`
|
|
29296
|
+
};
|
|
29297
|
+
}
|
|
29298
|
+
/**
|
|
29299
|
+
* Report managed-change liquidity without changing it. Monitor deliberately
|
|
29300
|
+
* has no signing authority; progressive migration occurs only during a
|
|
29301
|
+
* caller-authorized createAction.
|
|
29302
|
+
*/
|
|
29303
|
+
async reviewManagedChangeByIdentityKey(identityKey) {
|
|
29304
|
+
return await this.storage.runAsStorageProvider(async (sp) => {
|
|
29305
|
+
const user = (await sp.findUsers({ partial: { identityKey } }))[0];
|
|
29306
|
+
if (user == null) return `identityKey ${identityKey} was not found\n`;
|
|
29307
|
+
const basket = verifyOne(await sp.findOutputBaskets({ partial: {
|
|
29308
|
+
userId: user.userId,
|
|
29309
|
+
name: "default"
|
|
29310
|
+
} }));
|
|
29311
|
+
const outputs = (await sp.findOutputs({
|
|
29312
|
+
partial: {
|
|
29313
|
+
userId: user.userId,
|
|
29314
|
+
basketId: basket.basketId,
|
|
29315
|
+
spendable: true,
|
|
29316
|
+
...managedChangeOutputFields
|
|
29317
|
+
},
|
|
29318
|
+
txStatus: [
|
|
29319
|
+
"completed",
|
|
29320
|
+
"unproven",
|
|
29321
|
+
"sending"
|
|
29322
|
+
],
|
|
29323
|
+
noScript: true
|
|
29324
|
+
})).filter(isAutoSpendableChangeOutput);
|
|
29325
|
+
const reserved = new Set(await sp.findReservedActionBatchOutputIds(outputs.map((output) => output.outputId)));
|
|
29326
|
+
const statuses = await sp.findTransactionStatusesByIds(user.userId, outputs.map((output) => output.transactionId));
|
|
29327
|
+
const preferred = Math.max(1, basket.minimumDesiredUTXOValue);
|
|
29328
|
+
const healthy = outputs.filter((output) => output.satoshis >= preferred);
|
|
29329
|
+
const undersized = outputs.filter((output) => output.satoshis < preferred);
|
|
29330
|
+
const countStatus = (status) => outputs.filter((output) => statuses.get(output.transactionId) === status).length;
|
|
29331
|
+
const satoshis = outputs.reduce((sum, output) => sum + output.satoshis, 0);
|
|
29332
|
+
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`;
|
|
29333
|
+
});
|
|
29334
|
+
}
|
|
28109
29335
|
toUserLog(user, outputs, totalOutputs, total, tags) {
|
|
28110
|
-
const action = tags.includes("release") ? "updated to unspendable" : "
|
|
29336
|
+
const action = tags.includes("release") ? "confirmed spent and updated to unspendable" : "confirmed spent";
|
|
28111
29337
|
const target = tags.includes("all") ? "spendable utxos" : "spendable change utxos";
|
|
28112
29338
|
let log = `userId ${user.userId}: ${totalOutputs} ${target} ${action}, total ${total}, ${user.identityKey}\n`;
|
|
28113
29339
|
for (const output of outputs) log += ` ${output.outpoint} ${output.satoshis} now ${output.spendable ? "spendable" : "spent"}\n`;
|
|
@@ -28116,6 +29342,13 @@ var TaskReviewUtxos = class TaskReviewUtxos extends WalletMonitorTask {
|
|
|
28116
29342
|
};
|
|
28117
29343
|
//#endregion
|
|
28118
29344
|
//#region ../src/monitor/tasks/TaskReviewDoubleSpends.ts
|
|
29345
|
+
function hasDurableInputConflict(req) {
|
|
29346
|
+
try {
|
|
29347
|
+
return JSON.parse(req.history).notes?.some((note) => note.inputConflict === true || note.what === "arcSSEInputQuarantine") === true;
|
|
29348
|
+
} catch {
|
|
29349
|
+
return true;
|
|
29350
|
+
}
|
|
29351
|
+
}
|
|
28119
29352
|
/**
|
|
28120
29353
|
* Review recent reqs in terminal 'doubleSpend' state and move any false positives
|
|
28121
29354
|
* back to 'unfail' so existing recovery handling can re-process them.
|
|
@@ -28183,14 +29416,16 @@ var TaskReviewDoubleSpends = class TaskReviewDoubleSpends extends WalletMonitorT
|
|
|
28183
29416
|
let lastRetainedDoubleSpendIndex = -1;
|
|
28184
29417
|
let retainedDoubleSpendCount = 0;
|
|
28185
29418
|
for (const req of reqs) {
|
|
28186
|
-
const
|
|
29419
|
+
const gsr = await this.monitor.services.getStatusForTxids([req.txid]);
|
|
29420
|
+
const status = gsr.status === "success" ? gsr.results[0]?.status : void 0;
|
|
29421
|
+
const durableInputConflict = hasDurableInputConflict(req);
|
|
28187
29422
|
reviewed.push(req);
|
|
28188
|
-
if (status === "
|
|
29423
|
+
if (!(status === "mined" || status === "known" && !durableInputConflict)) {
|
|
28189
29424
|
lastRetainedDoubleSpendIndex = reviewed.length - 1;
|
|
28190
29425
|
retainedDoubleSpendCount += 1;
|
|
28191
29426
|
} else {
|
|
28192
29427
|
unfails.push(req.provenTxReqId);
|
|
28193
|
-
log += `unfail ${req.provenTxReqId} ${req.txid} status:${status}\n`;
|
|
29428
|
+
log += `unfail ${req.provenTxReqId} ${req.txid} status:${status} durableInputConflict:${String(durableInputConflict)}\n`;
|
|
28194
29429
|
}
|
|
28195
29430
|
}
|
|
28196
29431
|
if (unfails.length > 0) await this.storage.runAsStorageProvider(async (sp) => {
|
|
@@ -28229,6 +29464,168 @@ var TaskReviewDoubleSpends = class TaskReviewDoubleSpends extends WalletMonitorT
|
|
|
28229
29464
|
}
|
|
28230
29465
|
};
|
|
28231
29466
|
//#endregion
|
|
29467
|
+
//#region ../src/monitor/tasks/TaskReconcilePendingTransactions.ts
|
|
29468
|
+
const REVIEW_STATUSES = [
|
|
29469
|
+
"callback",
|
|
29470
|
+
"unmined",
|
|
29471
|
+
"sending",
|
|
29472
|
+
"unknown",
|
|
29473
|
+
"unconfirmed"
|
|
29474
|
+
];
|
|
29475
|
+
/**
|
|
29476
|
+
* Poll durable transaction lifecycle state for aged, non-terminal requests.
|
|
29477
|
+
* This closes the gap where an Arcade rejection event was missed before the
|
|
29478
|
+
* monitor subscribed or while it was offline. Unknown/provider-error results
|
|
29479
|
+
* never mutate storage; only a provider's explicit terminal verdict does.
|
|
29480
|
+
*/
|
|
29481
|
+
var TaskReconcilePendingTransactions = class TaskReconcilePendingTransactions extends WalletMonitorTask {
|
|
29482
|
+
triggerMsecs;
|
|
29483
|
+
reviewLimit;
|
|
29484
|
+
minAgeMinutes;
|
|
29485
|
+
triggerQuickMsecs;
|
|
29486
|
+
static taskName = "ReconcilePendingTransactions";
|
|
29487
|
+
triggerNextMsecs;
|
|
29488
|
+
constructor(monitor, triggerMsecs = Monitor.oneMinute * 12, reviewLimit = 100, minAgeMinutes = 60, triggerQuickMsecs = Monitor.oneMinute) {
|
|
29489
|
+
super(monitor, TaskReconcilePendingTransactions.taskName);
|
|
29490
|
+
this.triggerMsecs = triggerMsecs;
|
|
29491
|
+
this.reviewLimit = reviewLimit;
|
|
29492
|
+
this.minAgeMinutes = minAgeMinutes;
|
|
29493
|
+
this.triggerQuickMsecs = triggerQuickMsecs;
|
|
29494
|
+
this.triggerNextMsecs = this.triggerQuickMsecs;
|
|
29495
|
+
}
|
|
29496
|
+
trigger(nowMsecsSinceEpoch) {
|
|
29497
|
+
return { run: this.triggerNextMsecs > 0 && nowMsecsSinceEpoch - this.lastRunMsecsSinceEpoch > this.triggerNextMsecs };
|
|
29498
|
+
}
|
|
29499
|
+
async getCheckpoint() {
|
|
29500
|
+
let events = [];
|
|
29501
|
+
await this.storage.runAsStorageProvider(async (sp) => {
|
|
29502
|
+
events = await sp.findMonitorEvents({
|
|
29503
|
+
partial: { event: TaskReconcilePendingTransactions.taskName },
|
|
29504
|
+
orderDescending: true,
|
|
29505
|
+
paged: { limit: 5 }
|
|
29506
|
+
});
|
|
29507
|
+
});
|
|
29508
|
+
for (const event of events) {
|
|
29509
|
+
if (!event.details) continue;
|
|
29510
|
+
try {
|
|
29511
|
+
const parsed = JSON.parse(event.details);
|
|
29512
|
+
if (parsed.cycleComplete === true) return void 0;
|
|
29513
|
+
if (typeof parsed.resumeOffset === "number") return {
|
|
29514
|
+
resumeOffset: parsed.resumeOffset,
|
|
29515
|
+
expectedProvenTxReqId: parsed.expectedProvenTxReqId
|
|
29516
|
+
};
|
|
29517
|
+
} catch {
|
|
29518
|
+
continue;
|
|
29519
|
+
}
|
|
29520
|
+
}
|
|
29521
|
+
}
|
|
29522
|
+
async runTask() {
|
|
29523
|
+
const checkpoint = await this.getCheckpoint();
|
|
29524
|
+
const updatedBefore = /* @__PURE__ */ new Date(Date.now() - this.minAgeMinutes * 60 * 1e3);
|
|
29525
|
+
const reqs = await this.findReqsToReview(checkpoint);
|
|
29526
|
+
const eligible = reqs.filter((req) => req.updated_at <= updatedBefore);
|
|
29527
|
+
const provider = eligible.length === 0 ? void 0 : await this.monitor.services.getStatusForTxids(eligible.map((req) => req.txid));
|
|
29528
|
+
const results = new Map(provider?.results.map((result) => [result.txid, result]) ?? []);
|
|
29529
|
+
let reconciled = 0;
|
|
29530
|
+
let inputConflicts = 0;
|
|
29531
|
+
let reviewLog = "";
|
|
29532
|
+
const retained = [];
|
|
29533
|
+
for (const req of reqs) {
|
|
29534
|
+
const result = results.get(req.txid);
|
|
29535
|
+
if (req.updated_at > updatedBefore || provider?.status !== "success" || result?.terminal !== true) {
|
|
29536
|
+
retained.push(req);
|
|
29537
|
+
continue;
|
|
29538
|
+
}
|
|
29539
|
+
if (!await this.applyTerminalVerdict(req, result, provider.name)) {
|
|
29540
|
+
retained.push(req);
|
|
29541
|
+
continue;
|
|
29542
|
+
}
|
|
29543
|
+
reconciled++;
|
|
29544
|
+
if (result.inputConflict === true) inputConflicts++;
|
|
29545
|
+
reviewLog += `reconciled ${req.provenTxReqId} ${req.txid} ${result.providerStatus ?? "terminal"} => ${result.inputConflict === true ? "doubleSpend" : "invalid"}\n`;
|
|
29546
|
+
}
|
|
29547
|
+
const fullPage = reqs.length >= this.reviewLimit;
|
|
29548
|
+
this.triggerNextMsecs = fullPage ? this.triggerQuickMsecs : this.triggerMsecs;
|
|
29549
|
+
let resumeOffset;
|
|
29550
|
+
let expectedProvenTxReqId;
|
|
29551
|
+
if (fullPage) if (retained.length === 0) resumeOffset = reqs.sourceOffset ?? 0;
|
|
29552
|
+
else {
|
|
29553
|
+
resumeOffset = (reqs.sourceOffset ?? 0) + retained.length - 1;
|
|
29554
|
+
expectedProvenTxReqId = retained.at(-1)?.provenTxReqId;
|
|
29555
|
+
}
|
|
29556
|
+
return JSON.stringify({
|
|
29557
|
+
reviewed: eligible.length,
|
|
29558
|
+
reconciled,
|
|
29559
|
+
inputConflicts,
|
|
29560
|
+
retained: retained.length,
|
|
29561
|
+
reviewLimit: this.reviewLimit,
|
|
29562
|
+
minAgeMinutes: this.minAgeMinutes,
|
|
29563
|
+
cycleComplete: !fullPage,
|
|
29564
|
+
...resumeOffset != null ? { resumeOffset } : {},
|
|
29565
|
+
...expectedProvenTxReqId != null ? { expectedProvenTxReqId } : {},
|
|
29566
|
+
reviewLog
|
|
29567
|
+
});
|
|
29568
|
+
}
|
|
29569
|
+
async findReqsToReview(checkpoint) {
|
|
29570
|
+
let offset = checkpoint?.resumeOffset ?? 0;
|
|
29571
|
+
if (checkpoint?.expectedProvenTxReqId != null) offset = (await this.storage.findProvenTxReqs({
|
|
29572
|
+
partial: {},
|
|
29573
|
+
status: [...REVIEW_STATUSES],
|
|
29574
|
+
paged: {
|
|
29575
|
+
limit: 1,
|
|
29576
|
+
offset
|
|
29577
|
+
}
|
|
29578
|
+
}))[0]?.provenTxReqId === checkpoint.expectedProvenTxReqId ? offset + 1 : 0;
|
|
29579
|
+
const reqs = await this.storage.findProvenTxReqs({
|
|
29580
|
+
partial: {},
|
|
29581
|
+
status: [...REVIEW_STATUSES],
|
|
29582
|
+
paged: {
|
|
29583
|
+
limit: this.reviewLimit,
|
|
29584
|
+
offset
|
|
29585
|
+
}
|
|
29586
|
+
});
|
|
29587
|
+
reqs.sourceOffset = offset;
|
|
29588
|
+
return reqs;
|
|
29589
|
+
}
|
|
29590
|
+
async applyTerminalVerdict(reqApi, result, provider) {
|
|
29591
|
+
let applied = false;
|
|
29592
|
+
await this.storage.runAsStorageProvider(async (sp) => {
|
|
29593
|
+
await sp.transaction(async (trx) => {
|
|
29594
|
+
const req = new EntityProvenTxReq(reqApi);
|
|
29595
|
+
await req.refreshFromStorage(sp, trx);
|
|
29596
|
+
if (!REVIEW_STATUSES.includes(req.status)) return;
|
|
29597
|
+
req.status = result.inputConflict === true ? "doubleSpend" : "invalid";
|
|
29598
|
+
req.addHistoryNote({
|
|
29599
|
+
when: (/* @__PURE__ */ new Date()).toISOString(),
|
|
29600
|
+
what: "proactiveTerminalReconciliation",
|
|
29601
|
+
provider,
|
|
29602
|
+
providerStatus: result.providerStatus,
|
|
29603
|
+
statusCode: result.statusCode,
|
|
29604
|
+
description: result.description,
|
|
29605
|
+
inputConflict: result.inputConflict === true,
|
|
29606
|
+
competingTxs: result.competingTxs?.join(",")
|
|
29607
|
+
});
|
|
29608
|
+
await req.updateStorageDynamicProperties(sp, trx);
|
|
29609
|
+
if (req.notify.transactionIds != null) await sp.updateTransactionsStatus(req.notify.transactionIds, "failed", trx);
|
|
29610
|
+
if (result.inputConflict === true) {
|
|
29611
|
+
const quarantined = await quarantineReqInputs(req, sp, trx);
|
|
29612
|
+
req.addHistoryNote({
|
|
29613
|
+
when: (/* @__PURE__ */ new Date()).toISOString(),
|
|
29614
|
+
what: "proactiveInputQuarantine",
|
|
29615
|
+
checked: quarantined.checked,
|
|
29616
|
+
confirmed: quarantined.staleConfirmed,
|
|
29617
|
+
...quarantined.staleOutpoints.length > 0 ? { outpoints: quarantined.staleOutpoints.join(",") } : {}
|
|
29618
|
+
});
|
|
29619
|
+
await req.updateStorageDynamicProperties(sp, trx);
|
|
29620
|
+
}
|
|
29621
|
+
applied = true;
|
|
29622
|
+
});
|
|
29623
|
+
});
|
|
29624
|
+
if (applied) this.monitor.callOnTransactionStatusChanged(reqApi.txid, result.providerStatus ?? "REJECTED");
|
|
29625
|
+
return applied;
|
|
29626
|
+
}
|
|
29627
|
+
};
|
|
29628
|
+
//#endregion
|
|
28232
29629
|
//#region ../src/monitor/tasks/TaskReviewProvenTxs.ts
|
|
28233
29630
|
/**
|
|
28234
29631
|
* Backup verification task for recent proven_txs records.
|
|
@@ -28487,14 +29884,14 @@ var Monitor = class Monitor {
|
|
|
28487
29884
|
purgeFailedAge: 5 * Monitor.oneDay
|
|
28488
29885
|
};
|
|
28489
29886
|
addAllTasksToOther() {
|
|
28490
|
-
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));
|
|
29887
|
+
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));
|
|
28491
29888
|
if (this.chain === "mock") this._otherTasks.push(new TaskMineBlock(this));
|
|
28492
29889
|
}
|
|
28493
29890
|
/**
|
|
28494
29891
|
* Default tasks with settings appropriate for a single user storage
|
|
28495
29892
|
*/
|
|
28496
29893
|
addDefaultTasks() {
|
|
28497
|
-
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));
|
|
29894
|
+
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));
|
|
28498
29895
|
this._otherTasks.push(new TaskPurge(this, this.defaultPurgeParams, 6 * Monitor.oneHour), new TaskReviewUtxos(this));
|
|
28499
29896
|
if (this.chain === "mock") this._tasks.push(new TaskMineBlock(this));
|
|
28500
29897
|
}
|
|
@@ -28502,7 +29899,7 @@ var Monitor = class Monitor {
|
|
|
28502
29899
|
* Tasks appropriate for multi-user storage
|
|
28503
29900
|
*/
|
|
28504
29901
|
addMultiUserTasks() {
|
|
28505
|
-
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));
|
|
29902
|
+
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));
|
|
28506
29903
|
this._otherTasks.push(new TaskPurge(this, this.defaultPurgeParams), new TaskReviewUtxos(this));
|
|
28507
29904
|
if (this.chain === "mock") this._tasks.push(new TaskMineBlock(this));
|
|
28508
29905
|
}
|
|
@@ -28873,6 +30270,7 @@ var SetupClient = class SetupClient {
|
|
|
28873
30270
|
model: "sat/kb",
|
|
28874
30271
|
value: 100
|
|
28875
30272
|
},
|
|
30273
|
+
managedChangePolicy: args.managedChangePolicy,
|
|
28876
30274
|
scriptVerifier: args.scriptVerifier
|
|
28877
30275
|
});
|
|
28878
30276
|
await storage.migrate(args.databaseName, randomBytesHex(33));
|
|
@@ -33462,7 +34860,7 @@ var WalletPermissionsManager = class WalletPermissionsManager {
|
|
|
33462
34860
|
basket: basketName,
|
|
33463
34861
|
tags
|
|
33464
34862
|
}],
|
|
33465
|
-
options: { acceptDelayedBroadcast:
|
|
34863
|
+
options: { acceptDelayedBroadcast: true }
|
|
33466
34864
|
}, this.adminOriginator);
|
|
33467
34865
|
}
|
|
33468
34866
|
async mapWithConcurrency(items, concurrency, fn) {
|
|
@@ -33526,7 +34924,7 @@ var WalletPermissionsManager = class WalletPermissionsManager {
|
|
|
33526
34924
|
await this.createAction({
|
|
33527
34925
|
description: `Grant ${built.length} permissions`,
|
|
33528
34926
|
outputs: built.map((b) => b.output),
|
|
33529
|
-
options: { acceptDelayedBroadcast:
|
|
34927
|
+
options: { acceptDelayedBroadcast: true }
|
|
33530
34928
|
}, this.adminOriginator);
|
|
33531
34929
|
return built.map((b) => b.request);
|
|
33532
34930
|
}, strict);
|
|
@@ -34923,6 +36321,7 @@ exports.ARGON2ID_MAX_MEMORY_KIB = ARGON2ID_MAX_MEMORY_KIB;
|
|
|
34923
36321
|
exports.ARGON2ID_MAX_PARALLELISM = ARGON2ID_MAX_PARALLELISM;
|
|
34924
36322
|
exports.AuthMethodInteractor = AuthMethodInteractor;
|
|
34925
36323
|
exports.BHServiceClient = BHServiceClient;
|
|
36324
|
+
exports.BRC153_REFERENCE_PREFIX = BRC153_REFERENCE_PREFIX;
|
|
34926
36325
|
exports.BulkFileDataManager = BulkFileDataManager;
|
|
34927
36326
|
exports.BulkFileDataReader = BulkFileDataReader;
|
|
34928
36327
|
exports.BulkFilesReader = BulkFilesReader;
|
|
@@ -34943,6 +36342,11 @@ exports.ChaintracksServiceClient = ChaintracksServiceClient;
|
|
|
34943
36342
|
exports.ChaintracksStorageBase = ChaintracksStorageBase;
|
|
34944
36343
|
exports.ChaintracksStorageIdb = ChaintracksStorageIdb;
|
|
34945
36344
|
exports.ChaintracksStorageNoDb = ChaintracksStorageNoDb;
|
|
36345
|
+
exports.DEFAULT_MANAGED_CHANGE_MAX_OUTPUTS_PER_ACTION = DEFAULT_MANAGED_CHANGE_MAX_OUTPUTS_PER_ACTION;
|
|
36346
|
+
exports.DEFAULT_MANAGED_CHANGE_MIGRATION_INPUTS_PER_ACTION = DEFAULT_MANAGED_CHANGE_MIGRATION_INPUTS_PER_ACTION;
|
|
36347
|
+
exports.DEFAULT_MANAGED_CHANGE_MINIMUM_SATOSHIS = DEFAULT_MANAGED_CHANGE_MINIMUM_SATOSHIS;
|
|
36348
|
+
exports.DEFAULT_MANAGED_CHANGE_PENDING_COMPARISON_INPUTS = DEFAULT_MANAGED_CHANGE_PENDING_COMPARISON_INPUTS;
|
|
36349
|
+
exports.DEFAULT_MANAGED_CHANGE_TARGET_UTXOS = DEFAULT_MANAGED_CHANGE_TARGET_UTXOS;
|
|
34946
36350
|
exports.DEFAULT_PROFILE_ID = DEFAULT_PROFILE_ID;
|
|
34947
36351
|
exports.DEFAULT_SETTINGS = DEFAULT_SETTINGS;
|
|
34948
36352
|
exports.DevConsoleInteractor = DevConsoleInteractor;
|
|
@@ -34964,6 +36368,7 @@ exports.EntityUser = EntityUser;
|
|
|
34964
36368
|
exports.GoChaintracksServiceClient = GoChaintracksServiceClient;
|
|
34965
36369
|
exports.HeightRange = HeightRange;
|
|
34966
36370
|
exports.KDF_MAX_HASH_LENGTH = KDF_MAX_HASH_LENGTH;
|
|
36371
|
+
exports.LEGACY_MANAGED_CHANGE_MINIMUM_SATOSHIS = LEGACY_MANAGED_CHANGE_MINIMUM_SATOSHIS;
|
|
34967
36372
|
exports.LiveIngestorBase = LiveIngestorBase;
|
|
34968
36373
|
exports.LiveIngestorChaintracksSSE = LiveIngestorChaintracksSSE;
|
|
34969
36374
|
exports.LiveIngestorWhatsOnChainPoll = LiveIngestorWhatsOnChainPoll;
|
|
@@ -34998,6 +36403,7 @@ exports.WalletSettingsManager = WalletSettingsManager;
|
|
|
34998
36403
|
exports.WalletSigner = WalletSigner;
|
|
34999
36404
|
exports.WalletStorageManager = WalletStorageManager;
|
|
35000
36405
|
exports.WhatsOnChainServices = WhatsOnChainServices;
|
|
36406
|
+
exports.applyBrc153ReferenceLabel = applyBrc153ReferenceLabel;
|
|
35001
36407
|
exports.arcDefaultUrl = arcDefaultUrl;
|
|
35002
36408
|
exports.arcGorillaPoolUrl = arcGorillaPoolUrl;
|
|
35003
36409
|
exports.arcadeDefaultUrl = arcadeDefaultUrl;
|
|
@@ -35023,6 +36429,7 @@ exports.createIdbChaintracks = createIdbChaintracks;
|
|
|
35023
36429
|
exports.createNoDbChaintracks = createNoDbChaintracks;
|
|
35024
36430
|
exports.createSyncMap = createSyncMap;
|
|
35025
36431
|
exports.decryptBRC39 = decryptBRC39;
|
|
36432
|
+
exports.defaultManagedChangePolicy = defaultManagedChangePolicy;
|
|
35026
36433
|
exports.doubleSha256BE = doubleSha256BE;
|
|
35027
36434
|
exports.doubleSha256LE = doubleSha256LE;
|
|
35028
36435
|
exports.encryptBRC39 = encryptBRC39;
|
|
@@ -35037,6 +36444,8 @@ exports.importBRC39 = importBRC39;
|
|
|
35037
36444
|
exports.isAutoSpendableChangeOutput = isAutoSpendableChangeOutput;
|
|
35038
36445
|
exports.isBaseBlockHeader = isBaseBlockHeader;
|
|
35039
36446
|
exports.isBlockHeader = isBlockHeader;
|
|
36447
|
+
exports.isBrc153ReferenceLabel = isBrc153ReferenceLabel;
|
|
36448
|
+
exports.isLegacyManagedChangeBasketDefault = isLegacyManagedChangeBasketDefault;
|
|
35040
36449
|
exports.isLive = isLive;
|
|
35041
36450
|
exports.isLiveBlockHeader = isLiveBlockHeader;
|
|
35042
36451
|
exports.isManagedChangeOutput = isManagedChangeOutput;
|
|
@@ -35045,12 +36454,14 @@ exports.logWalletError = logWalletError;
|
|
|
35045
36454
|
exports.logger = logger;
|
|
35046
36455
|
exports.makeAtomicBeef = makeAtomicBeef;
|
|
35047
36456
|
exports.makeBrc114ActionTimeLabel = makeBrc114ActionTimeLabel;
|
|
36457
|
+
exports.makeBrc153ReferenceLabel = makeBrc153ReferenceLabel;
|
|
35048
36458
|
exports.managedChangeOutputFields = managedChangeOutputFields;
|
|
35049
36459
|
exports.maxDate = maxDate;
|
|
35050
36460
|
exports.optionalArraysEqual = optionalArraysEqual;
|
|
35051
36461
|
exports.outputColumnsWithoutLockingScript = outputColumnsWithoutLockingScript;
|
|
35052
36462
|
exports.parseBRC38Json = parseBRC38Json;
|
|
35053
36463
|
exports.parseBrc114ActionTimeLabels = parseBrc114ActionTimeLabels;
|
|
36464
|
+
exports.parseBrc153ReferenceLabel = parseBrc153ReferenceLabel;
|
|
35054
36465
|
exports.parseFileLink = parseFileLink;
|
|
35055
36466
|
exports.parseTxScriptOffsets = parseTxScriptOffsets;
|
|
35056
36467
|
exports.partitionActionLabels = partitionActionLabels;
|
|
@@ -35076,12 +36487,14 @@ exports.toDefaultChaintracksArguments = toDefaultChaintracksArguments;
|
|
|
35076
36487
|
exports.toLookupNetworkPreset = toLookupNetworkPreset;
|
|
35077
36488
|
exports.toWalletNetwork = toWalletNetwork;
|
|
35078
36489
|
exports.transactionColumnsWithoutRawTx = transactionColumnsWithoutRawTx;
|
|
36490
|
+
exports.upgradeLegacyManagedChangeBasketDefault = upgradeLegacyManagedChangeBasketDefault;
|
|
35079
36491
|
Object.defineProperty(exports, "utils", {
|
|
35080
36492
|
enumerable: true,
|
|
35081
36493
|
get: function() {
|
|
35082
36494
|
return blockHeaderUtilities_exports;
|
|
35083
36495
|
}
|
|
35084
36496
|
});
|
|
36497
|
+
exports.validateManagedChangePolicy = validateManagedChangePolicy;
|
|
35085
36498
|
exports.validateScriptHash = validateScriptHash;
|
|
35086
36499
|
exports.validateSecondsSinceEpoch = validateSecondsSinceEpoch;
|
|
35087
36500
|
exports.validateStorageFeeModel = validateStorageFeeModel;
|