@bsv/wallet-toolbox-client 2.6.5 → 2.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/out/index.client.cjs +1803 -406
- package/out/index.client.cjs.map +1 -1
- package/out/index.client.d.cts +202 -14
- package/out/index.client.d.cts.map +1 -1
- package/out/index.client.d.mts +202 -14
- package/out/index.client.d.mts.map +1 -1
- package/out/index.client.mjs +1789 -407
- package/out/index.client.mjs.map +1 -1
- package/package.json +3 -3
package/out/index.client.cjs
CHANGED
|
@@ -175,23 +175,10 @@ var WalletError = class WalletError extends Error {
|
|
|
175
175
|
* @returns stringified JSON representation of the error such that it can be desirialized to a WalletError.
|
|
176
176
|
*/
|
|
177
177
|
static unknownToJson(error) {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
const ctor = ctorName != null && typeof ctorName.name === "string" ? ctorName.name : void 0;
|
|
183
|
-
const name = t === "object" && error !== null && typeof error.name === "string" ? error.name : "";
|
|
184
|
-
const message = t === "object" && error !== null && typeof error.message === "string" ? error.message : "";
|
|
185
|
-
const hasToJson = t === "object" && typeof error?.toJson === "function";
|
|
186
|
-
if (ctor != null && ctor !== "" && ctor.startsWith("WERR_") && hasToJson) json = error.toJson();
|
|
187
|
-
else if (name !== "" && message !== "") {
|
|
188
|
-
e = new WalletError(name, message);
|
|
189
|
-
json = e.toJson();
|
|
190
|
-
} else {
|
|
191
|
-
e = new WalletError("WERR_UNKNOWN", String(error));
|
|
192
|
-
json = e.toJson();
|
|
193
|
-
}
|
|
194
|
-
return json;
|
|
178
|
+
const candidate = error;
|
|
179
|
+
if ((error instanceof WalletError || candidate?.isError === true) && typeof candidate.toJson === "function") return candidate.toJson();
|
|
180
|
+
if (typeof candidate?.name === "string" && typeof candidate.message === "string") return new WalletError(candidate.name, candidate.message).toJson();
|
|
181
|
+
return new WalletError("WERR_UNKNOWN", String(error)).toJson();
|
|
195
182
|
}
|
|
196
183
|
};
|
|
197
184
|
//#endregion
|
|
@@ -225,6 +212,47 @@ var WERR_INVALID_OPERATION = class extends WalletError {
|
|
|
225
212
|
}
|
|
226
213
|
};
|
|
227
214
|
/**
|
|
215
|
+
* A destructive UTXO review could not obtain a conclusive verdict for every
|
|
216
|
+
* candidate. Consumers can match this name and retry later without parsing the
|
|
217
|
+
* human-readable message.
|
|
218
|
+
*/
|
|
219
|
+
var WERR_UTXO_REVIEW_INCONCLUSIVE = class extends WalletError {
|
|
220
|
+
checked;
|
|
221
|
+
confirmedSpent;
|
|
222
|
+
unknown;
|
|
223
|
+
constructor(checked, confirmedSpent, unknown) {
|
|
224
|
+
super("WERR_UTXO_REVIEW_INCONCLUSIVE", `UTXO review was inconclusive for ${unknown} of ${checked} candidates; no outputs were changed.`);
|
|
225
|
+
this.checked = checked;
|
|
226
|
+
this.confirmedSpent = confirmedSpent;
|
|
227
|
+
this.unknown = unknown;
|
|
228
|
+
}
|
|
229
|
+
toJson() {
|
|
230
|
+
const obj = JSON.parse(super.toJson());
|
|
231
|
+
obj.checked = this.checked;
|
|
232
|
+
obj.confirmedSpent = this.confirmedSpent;
|
|
233
|
+
obj.unknown = this.unknown;
|
|
234
|
+
return JSON.stringify(obj);
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
/**
|
|
238
|
+
* An action-batch lifecycle operation cannot continue in the batch's current state.
|
|
239
|
+
*/
|
|
240
|
+
var WERRActionBatchState = class extends WalletError {
|
|
241
|
+
state;
|
|
242
|
+
batchId;
|
|
243
|
+
constructor(state, batchId) {
|
|
244
|
+
super("WERR_ACTION_BATCH_STATE", `Action batch cannot continue because it is ${state}.`);
|
|
245
|
+
this.state = state;
|
|
246
|
+
this.batchId = batchId;
|
|
247
|
+
}
|
|
248
|
+
toJson() {
|
|
249
|
+
const obj = JSON.parse(super.toJson());
|
|
250
|
+
obj.state = this.state;
|
|
251
|
+
obj.batchId = this.batchId;
|
|
252
|
+
return JSON.stringify(obj);
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
/**
|
|
228
256
|
* Unable to broadcast transaction at this time.
|
|
229
257
|
*/
|
|
230
258
|
var WERR_BROADCAST_UNAVAILABLE = class extends WalletError {
|
|
@@ -422,6 +450,10 @@ function WalletErrorFromJson(json) {
|
|
|
422
450
|
let e;
|
|
423
451
|
const obj = json;
|
|
424
452
|
switch (obj.name) {
|
|
453
|
+
case "WERR_ACTION_BATCH_STATE":
|
|
454
|
+
e = new WERRActionBatchState(obj.state, obj.batchId);
|
|
455
|
+
e.message = obj.message;
|
|
456
|
+
break;
|
|
425
457
|
case "WERR_NOT_IMPLEMENTED":
|
|
426
458
|
e = new WERR_NOT_IMPLEMENTED(obj.message);
|
|
427
459
|
break;
|
|
@@ -431,6 +463,10 @@ function WalletErrorFromJson(json) {
|
|
|
431
463
|
case "WERR_INVALID_OPERATION":
|
|
432
464
|
e = new WERR_INVALID_OPERATION(obj.message);
|
|
433
465
|
break;
|
|
466
|
+
case "WERR_UTXO_REVIEW_INCONCLUSIVE":
|
|
467
|
+
e = new WERR_UTXO_REVIEW_INCONCLUSIVE(obj.checked, obj.confirmedSpent, obj.unknown);
|
|
468
|
+
e.message = obj.message;
|
|
469
|
+
break;
|
|
434
470
|
case "WERR_BROADCAST_UNAVAILABLE":
|
|
435
471
|
e = new WERR_BROADCAST_UNAVAILABLE(obj.message);
|
|
436
472
|
break;
|
|
@@ -507,9 +543,10 @@ const specOpWalletManagedUtxos = "284570a6213a74ba861c38b1cf790e1e400d9cf9324454
|
|
|
507
543
|
/**
|
|
508
544
|
* `listOutputs` special operation basket name value.
|
|
509
545
|
*
|
|
510
|
-
* Returns currently spendable wallet change outputs
|
|
546
|
+
* Returns currently spendable wallet change outputs conclusively confirmed spent.
|
|
547
|
+
* The operation rejects if any candidate cannot be classified conclusively.
|
|
511
548
|
*
|
|
512
|
-
* Optional tag value 'release'. If present, updates
|
|
549
|
+
* Optional tag value 'release'. If present, atomically updates only confirmed-spent change outputs to not spendable after rechecking current ownership and allocation state.
|
|
513
550
|
*
|
|
514
551
|
* Optional tag value 'all'. If present, processes all spendable true outputs, independent of baskets, but basket must be defined.
|
|
515
552
|
*/
|
|
@@ -839,6 +876,7 @@ var sdk_exports = /* @__PURE__ */ __exportAll({
|
|
|
839
876
|
ProvenTxReqNonTerminalStatus: () => ProvenTxReqNonTerminalStatus,
|
|
840
877
|
ProvenTxReqTerminalStatus: () => ProvenTxReqTerminalStatus,
|
|
841
878
|
Validation: () => _bsv_sdk.Validation,
|
|
879
|
+
WERR_ACTION_BATCH_STATE: () => WERRActionBatchState,
|
|
842
880
|
WERR_BAD_REQUEST: () => WERR_BAD_REQUEST,
|
|
843
881
|
WERR_BROADCAST_UNAVAILABLE: () => WERR_BROADCAST_UNAVAILABLE,
|
|
844
882
|
WERR_INSUFFICIENT_FUNDS: () => WERR_INSUFFICIENT_FUNDS,
|
|
@@ -853,6 +891,7 @@ var sdk_exports = /* @__PURE__ */ __exportAll({
|
|
|
853
891
|
WERR_NOT_IMPLEMENTED: () => WERR_NOT_IMPLEMENTED,
|
|
854
892
|
WERR_REVIEW_ACTIONS: () => WERR_REVIEW_ACTIONS,
|
|
855
893
|
WERR_UNAUTHORIZED: () => WERR_UNAUTHORIZED,
|
|
894
|
+
WERR_UTXO_REVIEW_INCONCLUSIVE: () => WERR_UTXO_REVIEW_INCONCLUSIVE,
|
|
856
895
|
WalletError: () => WalletError,
|
|
857
896
|
WalletErrorFromJson: () => WalletErrorFromJson,
|
|
858
897
|
isCreateActionSpecOp: () => isCreateActionSpecOp,
|
|
@@ -1019,8 +1058,8 @@ function toLookupNetworkPreset(chain) {
|
|
|
1019
1058
|
switch (chain) {
|
|
1020
1059
|
case "main": return "mainnet";
|
|
1021
1060
|
case "test": return "testnet";
|
|
1061
|
+
case "ttn": return "teratestnet";
|
|
1022
1062
|
case "stn":
|
|
1023
|
-
case "ttn":
|
|
1024
1063
|
case "tstn":
|
|
1025
1064
|
case "mock": return "local";
|
|
1026
1065
|
}
|
|
@@ -1391,6 +1430,42 @@ function makeBrc114ActionTimeLabel(unixMillis) {
|
|
|
1391
1430
|
return `action time ${unixMillis}`;
|
|
1392
1431
|
}
|
|
1393
1432
|
//#endregion
|
|
1433
|
+
//#region ../src/utility/brc153ReferenceLabels.ts
|
|
1434
|
+
const BRC153_REFERENCE_PREFIX = "reference ";
|
|
1435
|
+
/**
|
|
1436
|
+
* Build the BRC-153 synthetic listActions label for an action reference.
|
|
1437
|
+
* Encodes reference bytes as lowercase hex (labels are lowercased by validation).
|
|
1438
|
+
*/
|
|
1439
|
+
function makeBrc153ReferenceLabel(referenceBase64) {
|
|
1440
|
+
const bytes = _bsv_sdk.Utils.toArray(referenceBase64, "base64");
|
|
1441
|
+
return `${BRC153_REFERENCE_PREFIX}${_bsv_sdk.Utils.toHex(bytes)}`;
|
|
1442
|
+
}
|
|
1443
|
+
/**
|
|
1444
|
+
* True iff the label uses the reserved BRC-153 reference prefix.
|
|
1445
|
+
*/
|
|
1446
|
+
function isBrc153ReferenceLabel(label) {
|
|
1447
|
+
return label.startsWith(BRC153_REFERENCE_PREFIX);
|
|
1448
|
+
}
|
|
1449
|
+
/**
|
|
1450
|
+
* Ensure labels contain exactly one wallet-authored `reference <hex>`.
|
|
1451
|
+
* Any existing reserved-prefix labels are replaced.
|
|
1452
|
+
*/
|
|
1453
|
+
function applyBrc153ReferenceLabel(labels, referenceBase64) {
|
|
1454
|
+
const next = labels.filter((label) => !isBrc153ReferenceLabel(label));
|
|
1455
|
+
next.push(makeBrc153ReferenceLabel(referenceBase64));
|
|
1456
|
+
return next;
|
|
1457
|
+
}
|
|
1458
|
+
/**
|
|
1459
|
+
* Parse a BRC-153 synthetic reference label back to the BRC-100 Base64String reference.
|
|
1460
|
+
* Returns undefined if the label is not a valid reference label.
|
|
1461
|
+
*/
|
|
1462
|
+
function parseBrc153ReferenceLabel(label) {
|
|
1463
|
+
if (!label.startsWith("reference ")) return void 0;
|
|
1464
|
+
const hex = label.slice(10);
|
|
1465
|
+
if (hex.length === 0 || hex.length % 2 !== 0 || !/^[0-9a-f]+$/.test(hex)) return void 0;
|
|
1466
|
+
return _bsv_sdk.Utils.toBase64(_bsv_sdk.Utils.toArray(hex, "hex"));
|
|
1467
|
+
}
|
|
1468
|
+
//#endregion
|
|
1394
1469
|
//#region ../src/storage/schema/entities/EntityBase.ts
|
|
1395
1470
|
var EntityBase = class {
|
|
1396
1471
|
api;
|
|
@@ -2094,6 +2169,58 @@ var EntityOutput = class EntityOutput extends EntityBase {
|
|
|
2094
2169
|
}
|
|
2095
2170
|
};
|
|
2096
2171
|
//#endregion
|
|
2172
|
+
//#region ../src/storage/methods/managedChangePolicy.ts
|
|
2173
|
+
/** Historical default retained only to identify untouched wallet baskets. */
|
|
2174
|
+
const LEGACY_MANAGED_CHANGE_MINIMUM_SATOSHIS = 32;
|
|
2175
|
+
/**
|
|
2176
|
+
* Default liquidity policy for wallet-managed change.
|
|
2177
|
+
*
|
|
2178
|
+
* The preferred minimum is deliberately much larger than the dust threshold.
|
|
2179
|
+
* Dust answers "can this output ever be spent economically?"; this value
|
|
2180
|
+
* answers "is this output useful as an independently selectable liquidity
|
|
2181
|
+
* unit at contemporary fee rates?".
|
|
2182
|
+
*/
|
|
2183
|
+
const DEFAULT_MANAGED_CHANGE_TARGET_UTXOS = 144;
|
|
2184
|
+
const DEFAULT_MANAGED_CHANGE_MINIMUM_SATOSHIS = 5e3;
|
|
2185
|
+
const DEFAULT_MANAGED_CHANGE_MAX_OUTPUTS_PER_ACTION = 8;
|
|
2186
|
+
const DEFAULT_MANAGED_CHANGE_MIGRATION_INPUTS_PER_ACTION = 4;
|
|
2187
|
+
const DEFAULT_MANAGED_CHANGE_PENDING_COMPARISON_INPUTS = 16;
|
|
2188
|
+
/** True only for the exact historical default that is safe to auto-upgrade. */
|
|
2189
|
+
function isLegacyManagedChangeBasketDefault(basket) {
|
|
2190
|
+
return basket.name === "default" && basket.numberOfDesiredUTXOs === 144 && basket.minimumDesiredUTXOValue === 32;
|
|
2191
|
+
}
|
|
2192
|
+
/**
|
|
2193
|
+
* Normalize a legacy default while retaining every other field and every
|
|
2194
|
+
* operator-selected non-default value. Used by migrations, sync, and restore.
|
|
2195
|
+
*/
|
|
2196
|
+
function upgradeLegacyManagedChangeBasketDefault(basket) {
|
|
2197
|
+
if (!isLegacyManagedChangeBasketDefault(basket)) return basket;
|
|
2198
|
+
return {
|
|
2199
|
+
...basket,
|
|
2200
|
+
minimumDesiredUTXOValue: DEFAULT_MANAGED_CHANGE_MINIMUM_SATOSHIS
|
|
2201
|
+
};
|
|
2202
|
+
}
|
|
2203
|
+
function defaultManagedChangePolicy() {
|
|
2204
|
+
return {
|
|
2205
|
+
maxOutputsPerAction: 8,
|
|
2206
|
+
migrationInputsPerAction: 4,
|
|
2207
|
+
pendingComparisonInputs: 16
|
|
2208
|
+
};
|
|
2209
|
+
}
|
|
2210
|
+
function validateManagedChangePolicy(options) {
|
|
2211
|
+
const policy = {
|
|
2212
|
+
...defaultManagedChangePolicy(),
|
|
2213
|
+
...options
|
|
2214
|
+
};
|
|
2215
|
+
const validateLimit = (value, name, minimum) => {
|
|
2216
|
+
if (value !== -1 && (!Number.isSafeInteger(value) || value < minimum)) throw new WERR_INVALID_PARAMETER(`managedChangePolicy.${name}`, `${minimum === 0 ? "a non-negative" : "a positive"} safe integer or -1 for unlimited`);
|
|
2217
|
+
};
|
|
2218
|
+
validateLimit(policy.maxOutputsPerAction, "maxOutputsPerAction", 1);
|
|
2219
|
+
validateLimit(policy.migrationInputsPerAction, "migrationInputsPerAction", 0);
|
|
2220
|
+
validateLimit(policy.pendingComparisonInputs, "pendingComparisonInputs", 1);
|
|
2221
|
+
return policy;
|
|
2222
|
+
}
|
|
2223
|
+
//#endregion
|
|
2097
2224
|
//#region ../src/storage/schema/entities/EntityOutputBasket.ts
|
|
2098
2225
|
var EntityOutputBasket = class EntityOutputBasket extends EntityBase {
|
|
2099
2226
|
constructor(api) {
|
|
@@ -2196,13 +2323,15 @@ var EntityOutputBasket = class EntityOutputBasket extends EntityBase {
|
|
|
2196
2323
|
this.userId = userId;
|
|
2197
2324
|
this.name ||= "default";
|
|
2198
2325
|
this.basketId = 0;
|
|
2326
|
+
this.api = upgradeLegacyManagedChangeBasketDefault(this.api);
|
|
2199
2327
|
this.basketId = await storage.insertOutputBasket(this.toApi(), trx);
|
|
2200
2328
|
}
|
|
2201
2329
|
async mergeExisting(storage, since, ei, syncMap, trx) {
|
|
2202
2330
|
let wasMerged = false;
|
|
2203
2331
|
if (ei.updated_at > this.updated_at) {
|
|
2204
|
-
|
|
2205
|
-
this.
|
|
2332
|
+
const incoming = upgradeLegacyManagedChangeBasketDefault(ei);
|
|
2333
|
+
this.minimumDesiredUTXOValue = incoming.minimumDesiredUTXOValue;
|
|
2334
|
+
this.numberOfDesiredUTXOs = incoming.numberOfDesiredUTXOs;
|
|
2206
2335
|
this.isDeleted = ei.isDeleted;
|
|
2207
2336
|
this.updated_at = new Date(Math.max(ei.updated_at.getTime(), this.updated_at.getTime()));
|
|
2208
2337
|
await storage.updateOutputBasket(this.id, this.toApi(), trx);
|
|
@@ -4372,6 +4501,12 @@ var WalletStorageManager = class {
|
|
|
4372
4501
|
async renewActionBatch(batchId) {
|
|
4373
4502
|
return await this.runAsWriter(async (writer) => await writer.renewActionBatch(await this.getAuth(true), batchId));
|
|
4374
4503
|
}
|
|
4504
|
+
async resumeActionBatch(args) {
|
|
4505
|
+
return await this.runAsWriter(async (writer) => {
|
|
4506
|
+
if (writer.resumeActionBatch == null) throw new WERR_NOT_IMPLEMENTED("action batch resume is not available");
|
|
4507
|
+
return await writer.resumeActionBatch(await this.getAuth(true), args);
|
|
4508
|
+
});
|
|
4509
|
+
}
|
|
4375
4510
|
async prepareActionBatchCommit(manifest) {
|
|
4376
4511
|
return await this.runAsWriter(async (writer) => await writer.prepareActionBatchCommit(await this.getAuth(true), manifest));
|
|
4377
4512
|
}
|
|
@@ -4726,8 +4861,20 @@ var WalletStorageManager = class {
|
|
|
4726
4861
|
});
|
|
4727
4862
|
return log;
|
|
4728
4863
|
}
|
|
4864
|
+
/**
|
|
4865
|
+
* Return the remote HTTP(S) endpoint for a managed store, if any.
|
|
4866
|
+
*
|
|
4867
|
+
* Duck-types `endpointUrl` on the provider (as set by `StorageClientBase`).
|
|
4868
|
+
* Do **not** key this off `constructor.name === 'StorageClient'`: production
|
|
4869
|
+
* minifiers (Vite/esbuild/webpack) rename classes, so that check fails and
|
|
4870
|
+
* every remote store reports `endpointURL: undefined` even though the URL is
|
|
4871
|
+
* present. Consumers that match backups by URL (e.g. making a remote store
|
|
4872
|
+
* primary) then fail while sync still works, because sync walks `_backups`
|
|
4873
|
+
* without needing `endpointURL`.
|
|
4874
|
+
*/
|
|
4729
4875
|
getStoreEndpointURL(store) {
|
|
4730
|
-
|
|
4876
|
+
const url = store.storage.endpointUrl;
|
|
4877
|
+
if (typeof url === "string" && url.length > 0) return url;
|
|
4731
4878
|
}
|
|
4732
4879
|
getStores() {
|
|
4733
4880
|
const stores = [];
|
|
@@ -5153,7 +5300,7 @@ async function getBeefForTransaction(storage, txid, options) {
|
|
|
5153
5300
|
const concurrency = normalizeConcurrency(options.maxConcurrency);
|
|
5154
5301
|
while (frontier.length > 0) {
|
|
5155
5302
|
const current = frontier.filter((item) => needsResolution(beef, item.txid, hasKnownTxid));
|
|
5156
|
-
const resolved = await mapWithConcurrency(current, concurrency, async (item) => await resolveBeefForTransaction(storage, item.txid, options, hasKnownTxid, item.depth));
|
|
5303
|
+
const resolved = await mapWithConcurrency$1(current, concurrency, async (item) => await resolveBeefForTransaction(storage, item.txid, options, hasKnownTxid, item.depth));
|
|
5157
5304
|
const next = [];
|
|
5158
5305
|
for (let i = 0; i < resolved.length; i++) {
|
|
5159
5306
|
const result = resolved[i];
|
|
@@ -5210,7 +5357,7 @@ function requiresSingleRootPolicy(options) {
|
|
|
5210
5357
|
return options.ignoreStorage === true || options.minProofLevel !== void 0 || options.chainTracker != null || options.skipInvalidProofs === true;
|
|
5211
5358
|
}
|
|
5212
5359
|
async function mergeSingleRootFragments(storage, roots, options, beef) {
|
|
5213
|
-
const fragments = await mapWithConcurrency(roots.filter((txid) => beef.findTxid(txid) == null), normalizeConcurrency(options.maxConcurrency), async (txid) => await getBeefForTransaction(storage, txid, {
|
|
5360
|
+
const fragments = await mapWithConcurrency$1(roots.filter((txid) => beef.findTxid(txid) == null), normalizeConcurrency(options.maxConcurrency), async (txid) => await getBeefForTransaction(storage, txid, {
|
|
5214
5361
|
...options,
|
|
5215
5362
|
mergeToBeef: void 0
|
|
5216
5363
|
}));
|
|
@@ -5329,7 +5476,7 @@ function needsResolution(beef, txid, hasKnownTxid) {
|
|
|
5329
5476
|
async function mergeMissingFragments(storage, beef, missing, options) {
|
|
5330
5477
|
if (missing.length === 0) return;
|
|
5331
5478
|
if (options.ignoreServices === true) throw new WERR_INVALID_PARAMETER(`txid ${missing[0].txid}`, `valid transaction on chain ${storage.chain}`);
|
|
5332
|
-
const fragments = await mapWithConcurrency(missing, normalizeConcurrency(options.maxConcurrency), async (item) => await getBeefForTransaction(storage, item.txid, {
|
|
5479
|
+
const fragments = await mapWithConcurrency$1(missing, normalizeConcurrency(options.maxConcurrency), async (item) => await getBeefForTransaction(storage, item.txid, {
|
|
5333
5480
|
...options,
|
|
5334
5481
|
ignoreStorage: true,
|
|
5335
5482
|
mergeToBeef: void 0
|
|
@@ -5352,7 +5499,7 @@ function makeKnownTxidLookup$1(knownTxids) {
|
|
|
5352
5499
|
function normalizeConcurrency(value = 8) {
|
|
5353
5500
|
return Number.isFinite(value) ? Math.max(1, Math.min(32, Math.floor(value))) : 8;
|
|
5354
5501
|
}
|
|
5355
|
-
async function mapWithConcurrency(values, concurrency, mapper) {
|
|
5502
|
+
async function mapWithConcurrency$1(values, concurrency, mapper) {
|
|
5356
5503
|
const results = Array.from({ length: values.length }, () => void 0);
|
|
5357
5504
|
let cursor = 0;
|
|
5358
5505
|
await Promise.all(Array.from({ length: Math.min(concurrency, values.length) }, async () => {
|
|
@@ -5763,6 +5910,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,7 +10506,7 @@ 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,
|
|
@@ -10609,7 +11094,9 @@ async function traceStorageStep(storage, name, parent, attributes, callback) {
|
|
|
10609
11094
|
attributes
|
|
10610
11095
|
}, async (span) => await callback(span));
|
|
10611
11096
|
}
|
|
10612
|
-
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);
|
|
10613
11100
|
return {
|
|
10614
11101
|
fixedInputs: xinputs.map((input) => ({
|
|
10615
11102
|
satoshis: input.satoshis,
|
|
@@ -10620,35 +11107,52 @@ function makeFundingParams(vargs, xinputs, xoutputs, changeBasket, feeModel, ava
|
|
|
10620
11107
|
lockingScriptLength: output.lockingScript.length / 2
|
|
10621
11108
|
})),
|
|
10622
11109
|
feeModel,
|
|
10623
|
-
changeInitialSatoshis:
|
|
10624
|
-
changeFirstSatoshis:
|
|
11110
|
+
changeInitialSatoshis: preferredSatoshis,
|
|
11111
|
+
changeFirstSatoshis: compatibilityFallback ? 1 : preferredSatoshis,
|
|
10625
11112
|
changeLockingScriptLength: 25,
|
|
10626
11113
|
changeUnlockingScriptLength: 107,
|
|
10627
|
-
targetNetCount: changeBasket.numberOfDesiredUTXOs -
|
|
11114
|
+
targetNetCount: changeBasket.numberOfDesiredUTXOs - healthyChangeCount,
|
|
11115
|
+
maxChangeOutputs: storage.managedChangePolicy.maxOutputsPerAction,
|
|
11116
|
+
surplusPoolShaping: !compatibilityFallback,
|
|
11117
|
+
maxMigrationInputs: compatibilityFallback ? 0 : storage.managedChangePolicy.migrationInputsPerAction,
|
|
10628
11118
|
randomVals: vargs.randomVals
|
|
10629
11119
|
};
|
|
10630
11120
|
}
|
|
10631
|
-
async function
|
|
10632
|
-
const [userId, vargs, xinputs, xoutputs, changeBasket, noSendChangeIn, feeModel
|
|
10633
|
-
const candidates = await traceStorageStep(storage, "wallet.storage.create_action.funding_candidates", parent, { "funding.exclude_sending": excludeSending }, async (span) => {
|
|
10634
|
-
const outputs = await storage.findAvailableManagedChangeInputCandidates(userId, changeBasket.basketId, excludeSending, trx);
|
|
10635
|
-
span?.end({ attributes: {
|
|
10636
|
-
"funding.candidate_count": outputs.length,
|
|
10637
|
-
"funding.candidate_satoshis": outputs.reduce((sum, output) => sum + output.satoshis, 0)
|
|
10638
|
-
} });
|
|
10639
|
-
return outputs;
|
|
10640
|
-
});
|
|
11121
|
+
async function buildFundingPlan(storage, context, candidates, eligibleStatuses, policyTier, compatibilityFallback, parent) {
|
|
11122
|
+
const [userId, vargs, xinputs, xoutputs, changeBasket, noSendChangeIn, feeModel] = context;
|
|
10641
11123
|
const noSendIds = new Set(noSendChangeIn.map((output) => output.outputId));
|
|
10642
|
-
const available = candidates.filter((output) => !noSendIds.has(output.outputId));
|
|
10643
|
-
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)])];
|
|
10644
11146
|
return await traceStorageStep(storage, "wallet.storage.create_action.funding_plan", parent, {
|
|
10645
11147
|
"funding.candidate_count": available.length,
|
|
10646
|
-
"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
|
|
10647
11151
|
}, async (span) => {
|
|
10648
11152
|
const allocated = /* @__PURE__ */ new Map();
|
|
10649
11153
|
const availableSelector = new CanonicalChangeSelector(available);
|
|
10650
|
-
const noSend = [...
|
|
10651
|
-
const noSendById = new Map(
|
|
11154
|
+
const noSend = [...plannedNoSend];
|
|
11155
|
+
const noSendById = new Map(plannedNoSend.map((output) => [output.outputId, output]));
|
|
10652
11156
|
const allocate = async (targetSatoshis, exactSatoshis) => {
|
|
10653
11157
|
let output = noSend.pop();
|
|
10654
11158
|
output ??= availableSelector.take(targetSatoshis, exactSatoshis);
|
|
@@ -10679,31 +11183,118 @@ async function prepareFundingPlan(storage, context) {
|
|
|
10679
11183
|
result,
|
|
10680
11184
|
selected,
|
|
10681
11185
|
availableChangeCount: candidates.length,
|
|
10682
|
-
|
|
11186
|
+
eligibleStatuses: claimStatuses,
|
|
11187
|
+
policyTier,
|
|
11188
|
+
compatibilityFallback
|
|
10683
11189
|
};
|
|
10684
11190
|
});
|
|
10685
11191
|
}
|
|
10686
|
-
async function
|
|
10687
|
-
const
|
|
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;
|
|
10688
11195
|
try {
|
|
10689
|
-
|
|
10690
|
-
|
|
10691
|
-
|
|
10692
|
-
|
|
10693
|
-
|
|
10694
|
-
|
|
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) };
|
|
10695
11249
|
} catch (error) {
|
|
10696
|
-
if (!
|
|
10697
|
-
|
|
10698
|
-
|
|
10699
|
-
|
|
10700
|
-
|
|
10701
|
-
|
|
10702
|
-
|
|
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);
|
|
10703
11292
|
}
|
|
11293
|
+
if (fundingError != null) throw fundingError;
|
|
11294
|
+
throw new WERR_INTERNAL("funding policy produced neither a plan nor an error");
|
|
10704
11295
|
}
|
|
10705
11296
|
async function claimFundingPlan(storage, request) {
|
|
10706
|
-
const [userId, basketId,
|
|
11297
|
+
const [userId, basketId, eligibleStatuses, transactionId, noSendChangeIn, plan, trx] = request;
|
|
10707
11298
|
if (plan.selected.length === 0) return {
|
|
10708
11299
|
outputs: [],
|
|
10709
11300
|
sourceTransactionCount: 0,
|
|
@@ -10711,10 +11302,8 @@ async function claimFundingPlan(storage, request) {
|
|
|
10711
11302
|
scriptSourceTransactionCount: 0
|
|
10712
11303
|
};
|
|
10713
11304
|
const noSendIds = new Set(noSendChangeIn.map((output) => output.outputId));
|
|
10714
|
-
const statuses = ["completed", "unproven"];
|
|
10715
|
-
if (!excludeSending) statuses.push("sending");
|
|
10716
11305
|
const claim = await storage.transaction(async (claimTrx) => {
|
|
10717
|
-
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);
|
|
10718
11307
|
const transactionIds = [...new Set(Object.values(currentById).map((output) => output.transactionId))];
|
|
10719
11308
|
const claimed = [];
|
|
10720
11309
|
for (const planned of plan.selected) {
|
|
@@ -10784,7 +11373,7 @@ async function fundNewTransactionSdk(storage, userId, vargs, ctx, initialPlan, p
|
|
|
10784
11373
|
const claim = await claimFundingPlan(storage, [
|
|
10785
11374
|
userId,
|
|
10786
11375
|
ctx.changeBasket.basketId,
|
|
10787
|
-
plan.
|
|
11376
|
+
plan.eligibleStatuses,
|
|
10788
11377
|
ctx.transactionId,
|
|
10789
11378
|
ctx.noSendChangeIn,
|
|
10790
11379
|
plan,
|
|
@@ -10802,7 +11391,7 @@ async function fundNewTransactionSdk(storage, userId, vargs, ctx, initialPlan, p
|
|
|
10802
11391
|
}
|
|
10803
11392
|
if (claim.conflict === "noSendChange") throw new WERR_INVALID_PARAMETER("noSendChange", "outputs that remain spendable during action planning");
|
|
10804
11393
|
retryCount++;
|
|
10805
|
-
plan = await
|
|
11394
|
+
plan = await prepareFundingPlanWithLiquidityPolicy(storage, [
|
|
10806
11395
|
userId,
|
|
10807
11396
|
vargs,
|
|
10808
11397
|
ctx.xinputs,
|
|
@@ -12955,7 +13544,7 @@ var StorageReaderWriter = class extends StorageReader {
|
|
|
12955
13544
|
userId: user.userId,
|
|
12956
13545
|
name: "default",
|
|
12957
13546
|
numberOfDesiredUTXOs: 144,
|
|
12958
|
-
minimumDesiredUTXOValue:
|
|
13547
|
+
minimumDesiredUTXOValue: DEFAULT_MANAGED_CHANGE_MINIMUM_SATOSHIS,
|
|
12959
13548
|
isDeleted: false
|
|
12960
13549
|
});
|
|
12961
13550
|
break;
|
|
@@ -13236,8 +13825,10 @@ function validateActionBatchInlinePayload(manifest) {
|
|
|
13236
13825
|
for (const { digest, bytes } of inlineBlobs) if (actionBatchBlobDigest(bytes) !== digest) throw new WERR_INVALID_PARAMETER("inlineBlobs", `content matching digest ${digest}`);
|
|
13237
13826
|
}
|
|
13238
13827
|
function requireUploadableBatch(batch) {
|
|
13239
|
-
if (batch == null
|
|
13240
|
-
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);
|
|
13241
13832
|
return batch;
|
|
13242
13833
|
}
|
|
13243
13834
|
function manifestPhysicalDigests(manifest) {
|
|
@@ -13597,7 +14188,10 @@ const ACTION_BATCH_LEASE_MS = 900 * 1e3;
|
|
|
13597
14188
|
const ACTION_BATCH_HARD_LIFETIME_MS = 3600 * 1e3;
|
|
13598
14189
|
const INITIAL_RESERVATION_LIMIT = 8;
|
|
13599
14190
|
const INITIAL_EXTRA_OUTPUTS = 3;
|
|
13600
|
-
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) {
|
|
13601
14195
|
return { actionBatch: {
|
|
13602
14196
|
version: 1,
|
|
13603
14197
|
maxInlineBytes: ACTION_BATCH_MAX_INLINE_BYTES,
|
|
@@ -13605,6 +14199,8 @@ function getActionBatchCapabilities() {
|
|
|
13605
14199
|
maxConcurrentUploads: 4,
|
|
13606
14200
|
leaseMs: ACTION_BATCH_LEASE_MS,
|
|
13607
14201
|
hardLifetimeMs: ACTION_BATCH_HARD_LIFETIME_MS,
|
|
14202
|
+
resume: supportsResume ? true : void 0,
|
|
14203
|
+
maxReservedOutputs,
|
|
13608
14204
|
compactBegin: true,
|
|
13609
14205
|
manifestVersion: 2,
|
|
13610
14206
|
commitByDigest: true,
|
|
@@ -13620,6 +14216,14 @@ function getActionBatchCapabilities() {
|
|
|
13620
14216
|
function activeExpiry(batch, now = /* @__PURE__ */ new Date()) {
|
|
13621
14217
|
return batch.status !== "active" && batch.status !== "prepared" || batch.expiresAt.getTime() <= now.getTime() || batch.hardExpiresAt.getTime() <= now.getTime();
|
|
13622
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
|
+
}
|
|
13623
14227
|
function canExpire(batch, now = /* @__PURE__ */ new Date()) {
|
|
13624
14228
|
return (batch.status === "active" || batch.status === "prepared") && (batch.expiresAt.getTime() <= now.getTime() || batch.hardExpiresAt.getTime() <= now.getTime());
|
|
13625
14229
|
}
|
|
@@ -13705,12 +14309,22 @@ function estimateFirstActionTarget(storage, args, inputSatoshis, outputScriptLen
|
|
|
13705
14309
|
const minFee = Math.ceil(transactionSize([...inputLengths, 107], outputLengths) * fee / 1e3);
|
|
13706
14310
|
return Math.max(1, outputSatoshis + minFee - inputSatoshis);
|
|
13707
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
|
+
}
|
|
13708
14322
|
function chooseReservationPool(candidates, targetSatoshis, limit, extras, fillLimit, planningCosts) {
|
|
13709
14323
|
const remaining = candidates.filter((output) => output.satoshis > planningCosts.marginalInputFee);
|
|
13710
14324
|
const chosen = [];
|
|
13711
14325
|
let deficit = targetSatoshis + planningCosts.firstChangeCost;
|
|
13712
14326
|
while (deficit > 0 && chosen.length < limit) {
|
|
13713
|
-
const output =
|
|
14327
|
+
const output = selectReservationChange(remaining, deficit + planningCosts.marginalInputFee);
|
|
13714
14328
|
if (output == null) break;
|
|
13715
14329
|
chosen.push(output);
|
|
13716
14330
|
remaining.splice(remaining.indexOf(output), 1);
|
|
@@ -13718,14 +14332,18 @@ function chooseReservationPool(candidates, targetSatoshis, limit, extras, fillLi
|
|
|
13718
14332
|
}
|
|
13719
14333
|
const desiredCount = fillLimit ? limit : Math.min(limit, chosen.length + extras);
|
|
13720
14334
|
while (remaining.length > 0 && chosen.length < desiredCount) {
|
|
13721
|
-
const output =
|
|
14335
|
+
const output = selectReservationChange(remaining, targetSatoshis);
|
|
13722
14336
|
if (output == null) break;
|
|
13723
14337
|
chosen.push(output);
|
|
13724
14338
|
remaining.splice(remaining.indexOf(output), 1);
|
|
13725
14339
|
}
|
|
13726
14340
|
return chosen;
|
|
13727
14341
|
}
|
|
13728
|
-
function
|
|
14342
|
+
function reservationOutput(candidate) {
|
|
14343
|
+
const { transactionStatus: _transactionStatus, ...output } = candidate;
|
|
14344
|
+
return output;
|
|
14345
|
+
}
|
|
14346
|
+
function reservationPlanningCosts(storage, _basket) {
|
|
13729
14347
|
const satsPerKb = validateStorageFeeModel(storage.feeModel).value ?? 0;
|
|
13730
14348
|
const minimumSpendSize = transactionSize([107], [25]);
|
|
13731
14349
|
const minimumSpendFee = Math.ceil(minimumSpendSize * satsPerKb / 1e3);
|
|
@@ -13733,7 +14351,7 @@ function reservationPlanningCosts(storage, basket) {
|
|
|
13733
14351
|
const marginalInputSize = transactionSize([107], []) - transactionSize([], []);
|
|
13734
14352
|
const marginalOutputSize = transactionSize([], [25]) - transactionSize([], []);
|
|
13735
14353
|
return {
|
|
13736
|
-
firstChangeCost:
|
|
14354
|
+
firstChangeCost: dustFloor + Math.ceil(marginalOutputSize * satsPerKb / 1e3),
|
|
13737
14355
|
marginalInputFee: Math.ceil(marginalInputSize * satsPerKb / 1e3)
|
|
13738
14356
|
};
|
|
13739
14357
|
}
|
|
@@ -13808,18 +14426,25 @@ async function beginActionBatch(storage, auth, args) {
|
|
|
13808
14426
|
const explicit = await resolveExplicitOutputs(storage, userId, args.firstAction, outputScriptLengths != null);
|
|
13809
14427
|
const noSendChange = await resolveNoSendChangeOutputs(storage, userId, args.firstAction);
|
|
13810
14428
|
const fixedOutputIds = new Set([...explicit.outputs, ...noSendChange.outputs].map((output) => output.outputId));
|
|
13811
|
-
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
|
+
}));
|
|
13812
14435
|
const target = estimateFirstActionTarget(storage, args.firstAction, explicit.inputSatoshis + noSendChange.inputSatoshis, outputScriptLengths);
|
|
13813
|
-
const fixedOutputs = [...explicit.outputs, ...noSendChange.outputs];
|
|
13814
|
-
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);
|
|
13815
14440
|
const batch = newBatch(userId, args.batchId);
|
|
13816
14441
|
await storage.transaction(async (trx) => {
|
|
13817
14442
|
await storage.insertActionBatch(batch, trx);
|
|
13818
|
-
await reserveOutputs(storage, batch, [...fixedOutputs, ...
|
|
14443
|
+
await reserveOutputs(storage, batch, [...fixedOutputs, ...fundingOutputs], trx);
|
|
13819
14444
|
});
|
|
13820
14445
|
let fundingResult;
|
|
13821
14446
|
try {
|
|
13822
|
-
fundingResult = await makeFundingResult(storage, args.firstAction, [...
|
|
14447
|
+
fundingResult = await makeFundingResult(storage, args.firstAction, [...fundingOutputs, ...fixedOutputs]);
|
|
13823
14448
|
} catch (error) {
|
|
13824
14449
|
await storage.transaction(async (trx) => await releaseBatchState(storage, batch, "aborted", trx));
|
|
13825
14450
|
throw error;
|
|
@@ -13833,41 +14458,63 @@ async function beginActionBatch(storage, auth, args) {
|
|
|
13833
14458
|
feeModel: validateStorageFeeModel(storage.feeModel),
|
|
13834
14459
|
commissionSatoshis: storage.commissionSatoshis,
|
|
13835
14460
|
commissionPubKeyHex: storage.commissionPubKeyHex,
|
|
13836
|
-
availableChangeCount: available.length,
|
|
13837
|
-
|
|
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)),
|
|
13838
14467
|
explicitOutputs: fundingResult.outputs.filter((output) => explicitIds.has(output.outputId)),
|
|
13839
14468
|
inputBeef: fundingResult.beef
|
|
13840
14469
|
};
|
|
13841
14470
|
}
|
|
13842
|
-
function requireLiveBatch(batch) {
|
|
13843
|
-
|
|
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);
|
|
13844
14475
|
return batch;
|
|
13845
14476
|
}
|
|
13846
14477
|
async function extendActionBatch(storage, auth, args) {
|
|
13847
14478
|
const userId = verifyId(auth.userId);
|
|
13848
14479
|
await cleanupExpiredActionBatches(storage);
|
|
13849
|
-
const batch = requireLiveBatch(await storage.findActionBatch(userId, args.batchId));
|
|
14480
|
+
const batch = requireLiveBatch(await storage.findActionBatch(userId, args.batchId), args.batchId);
|
|
13850
14481
|
const basket = verifyOne(await storage.findOutputBaskets({ partial: {
|
|
13851
14482
|
userId,
|
|
13852
14483
|
name: "default"
|
|
13853
14484
|
} }));
|
|
13854
14485
|
const alreadyReserved = await storage.findActionBatchOutputIds(batch.actionBatchId);
|
|
13855
|
-
const available = await storage.findAvailableManagedChangeInputs(userId, basket.basketId, false);
|
|
13856
14486
|
if (!Number.isSafeInteger(args.requestedOutputs) || args.requestedOutputs < 0) throw new WERR_INVALID_PARAMETER("requestedOutputs", "non-negative safe integer");
|
|
13857
|
-
|
|
13858
|
-
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`);
|
|
13859
14492
|
const explicitByOutpoint = await storage.findOutputsByOutpoints(userId, args.explicitOutpoints);
|
|
13860
|
-
const explicit = Object.values(explicitByOutpoint).filter((output) => !alreadyReserved.includes(output.outputId));
|
|
13861
|
-
|
|
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]);
|
|
13862
14505
|
const expiresAt = new Date(Math.min(batch.hardExpiresAt.getTime(), Date.now() + ACTION_BATCH_LEASE_MS));
|
|
13863
14506
|
await storage.transaction(async (trx) => {
|
|
13864
|
-
const current = requireLiveBatch(await storage.findActionBatchForUpdate(userId, args.batchId, trx));
|
|
13865
|
-
|
|
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);
|
|
13866
14513
|
await storage.updateActionBatch(current.actionBatchId, { expiresAt }, trx);
|
|
13867
14514
|
});
|
|
13868
14515
|
return {
|
|
13869
14516
|
expiresAt: expiresAt.toISOString(),
|
|
13870
|
-
reservedOutputs: fundingResult.outputs.filter((output) =>
|
|
14517
|
+
reservedOutputs: fundingResult.outputs.filter((output) => fundingOutputs.some((candidate) => candidate.outputId === output.outputId)),
|
|
13871
14518
|
explicitOutputs: fundingResult.outputs.filter((output) => explicit.some((candidate) => candidate.outputId === output.outputId)),
|
|
13872
14519
|
inputBeef: fundingResult.beef
|
|
13873
14520
|
};
|
|
@@ -13903,14 +14550,64 @@ function argsToFundingShape(includeSourceTransactions) {
|
|
|
13903
14550
|
async function renewActionBatch(storage, auth, batchId) {
|
|
13904
14551
|
const userId = verifyId(auth.userId);
|
|
13905
14552
|
return await storage.transaction(async (trx) => {
|
|
13906
|
-
const batch = requireLiveBatch(await storage.findActionBatchForUpdate(userId, batchId, trx));
|
|
14553
|
+
const batch = requireLiveBatch(await storage.findActionBatchForUpdate(userId, batchId, trx), batchId);
|
|
13907
14554
|
const expiresAt = new Date(Math.min(batch.hardExpiresAt.getTime(), Date.now() + ACTION_BATCH_LEASE_MS));
|
|
13908
14555
|
await storage.updateActionBatch(batch.actionBatchId, { expiresAt }, trx);
|
|
13909
14556
|
return { expiresAt: expiresAt.toISOString() };
|
|
13910
14557
|
});
|
|
13911
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
|
+
}
|
|
13912
14609
|
async function reacquireManifestInputs(storage, userId, batch, validated, trx) {
|
|
13913
|
-
if (batch.hardExpiresAt.getTime() <= Date.now()) throw new
|
|
14610
|
+
if (batch.hardExpiresAt.getTime() <= Date.now()) throw new WERRActionBatchState("hard-expired", batch.batchId);
|
|
13914
14611
|
const stagedTxids = new Set(validated.actions.map(({ action }) => action.txid));
|
|
13915
14612
|
const outpoints = [...new Map(validated.actions.flatMap(({ action }) => action.plan.inputs).filter((input) => !stagedTxids.has(input.sourceTxid)).map((input) => [`${input.sourceTxid}.${input.sourceVout}`, {
|
|
13916
14613
|
txid: input.sourceTxid,
|
|
@@ -14077,7 +14774,7 @@ async function persistAction(...[storage, userId, validated, reservedOutputIds,
|
|
|
14077
14774
|
async function persistManifestAtomically(storage, userId, batch, manifest, validated) {
|
|
14078
14775
|
return await storage.transaction(async (trx) => {
|
|
14079
14776
|
const current = await storage.findActionBatchForUpdate(userId, batch.batchId, trx);
|
|
14080
|
-
if (current == null) throw new
|
|
14777
|
+
if (current == null) throw new WERRActionBatchState("missing", batch.batchId);
|
|
14081
14778
|
if (current.status === "committed") {
|
|
14082
14779
|
if (current.manifestDigest !== manifest.digest) throw new WERR_INVALID_OPERATION("batch committed with another manifest");
|
|
14083
14780
|
return {
|
|
@@ -14085,12 +14782,12 @@ async function persistManifestAtomically(storage, userId, batch, manifest, valid
|
|
|
14085
14782
|
alreadyCommitted: true
|
|
14086
14783
|
};
|
|
14087
14784
|
}
|
|
14088
|
-
if (current.status === "aborted") throw new
|
|
14785
|
+
if (current.status === "aborted") throw new WERRActionBatchState("aborted", batch.batchId);
|
|
14089
14786
|
if (current.manifestDigest != null && current.manifestDigest !== manifest.digest) throw new WERR_INVALID_OPERATION("batch prepared with another manifest");
|
|
14090
14787
|
if (current.status === "expired" || activeExpiry(current)) {
|
|
14091
|
-
if (current.hardExpiresAt.getTime() <= Date.now()) throw new
|
|
14788
|
+
if (current.hardExpiresAt.getTime() <= Date.now()) throw new WERRActionBatchState("hard-expired", batch.batchId);
|
|
14092
14789
|
await reacquireManifestInputs(storage, userId, current, validated, trx);
|
|
14093
|
-
} else requireLiveBatch(current);
|
|
14790
|
+
} else requireLiveBatch(current, batch.batchId);
|
|
14094
14791
|
const reservedOutputIds = new Set(await storage.findActionBatchOutputIds(current.actionBatchId, trx));
|
|
14095
14792
|
const stagedTxids = new Set(validated.actions.map(({ action }) => action.txid));
|
|
14096
14793
|
const inputOutpoints = [...new Map(validated.actions.flatMap(({ action }) => action.plan.inputs).filter((input) => !stagedTxids.has(input.sourceTxid)).map((input) => [`${input.sourceTxid}.${input.sourceVout}`, {
|
|
@@ -14168,14 +14865,14 @@ async function completeCommittedBatch(storage, userId, batch, manifest, alreadyC
|
|
|
14168
14865
|
}
|
|
14169
14866
|
async function commitActionBatchOnce(storage, userId, manifest) {
|
|
14170
14867
|
const batch = await storage.findActionBatch(userId, manifest.batchId);
|
|
14171
|
-
if (batch == null) throw new
|
|
14868
|
+
if (batch == null) throw new WERRActionBatchState("missing", manifest.batchId);
|
|
14172
14869
|
if (batch.status === "committed") {
|
|
14173
14870
|
if (batch.manifestDigest !== manifest.digest) throw new WERR_INVALID_OPERATION("batch committed with another manifest");
|
|
14174
14871
|
return await completeCommittedBatch(storage, userId, batch, manifest, true);
|
|
14175
14872
|
}
|
|
14176
|
-
if (batch.status === "aborted") throw new
|
|
14177
|
-
if (!(batch.status === "expired" || activeExpiry(batch))) requireLiveBatch(batch);
|
|
14178
|
-
else if (batch.hardExpiresAt.getTime() <= Date.now()) throw new
|
|
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);
|
|
14179
14876
|
if (batch.manifestDigest != null && batch.manifestDigest !== manifest.digest) throw new WERR_INVALID_OPERATION("batch prepared with another manifest");
|
|
14180
14877
|
const persisted = await persistManifestAtomically(storage, userId, batch, manifest, await validateManifestActions(storage, batch, manifest));
|
|
14181
14878
|
return await completeCommittedBatch(storage, userId, persisted.batch, manifest, persisted.alreadyCommitted, persisted.share);
|
|
@@ -14258,6 +14955,8 @@ var StorageProvider = class StorageProvider extends StorageReaderWriter {
|
|
|
14258
14955
|
commissionSatoshis;
|
|
14259
14956
|
commissionPubKeyHex;
|
|
14260
14957
|
maxRecursionDepth;
|
|
14958
|
+
actionBatchMaxReservedOutputs;
|
|
14959
|
+
managedChangePolicy;
|
|
14261
14960
|
scriptVerifier;
|
|
14262
14961
|
static defaultOptions() {
|
|
14263
14962
|
return {
|
|
@@ -14266,7 +14965,9 @@ var StorageProvider = class StorageProvider extends StorageReaderWriter {
|
|
|
14266
14965
|
value: 100
|
|
14267
14966
|
},
|
|
14268
14967
|
commissionSatoshis: 0,
|
|
14269
|
-
commissionPubKeyHex: void 0
|
|
14968
|
+
commissionPubKeyHex: void 0,
|
|
14969
|
+
actionBatchMaxReservedOutputs: 256,
|
|
14970
|
+
managedChangePolicy: defaultManagedChangePolicy()
|
|
14270
14971
|
};
|
|
14271
14972
|
}
|
|
14272
14973
|
static createStorageBaseOptions(chain) {
|
|
@@ -14281,6 +14982,10 @@ var StorageProvider = class StorageProvider extends StorageReaderWriter {
|
|
|
14281
14982
|
this.commissionPubKeyHex = options.commissionPubKeyHex;
|
|
14282
14983
|
this.commissionSatoshis = options.commissionSatoshis;
|
|
14283
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);
|
|
14284
14989
|
this.scriptVerifier = options.scriptVerifier;
|
|
14285
14990
|
}
|
|
14286
14991
|
/** Mark a planned set of change inputs spent within the caller's transaction. */
|
|
@@ -14311,12 +15016,16 @@ var StorageProvider = class StorageProvider extends StorageReaderWriter {
|
|
|
14311
15016
|
}
|
|
14312
15017
|
/** Read only the fields needed by the in-memory funding planner. */
|
|
14313
15018
|
async findAvailableManagedChangeInputCandidates(userId, basketId, excludeSending, trx) {
|
|
14314
|
-
|
|
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 }) => ({
|
|
14315
15023
|
outputId,
|
|
14316
15024
|
transactionId,
|
|
14317
15025
|
satoshis,
|
|
14318
15026
|
txid,
|
|
14319
|
-
vout
|
|
15027
|
+
vout,
|
|
15028
|
+
transactionStatus: statuses.get(transactionId)
|
|
14320
15029
|
}));
|
|
14321
15030
|
}
|
|
14322
15031
|
/** Read the current status of a set of source transactions without loading raw transaction bytes. */
|
|
@@ -14404,7 +15113,7 @@ var StorageProvider = class StorageProvider extends StorageReaderWriter {
|
|
|
14404
15113
|
throw new WERR_NOT_IMPLEMENTED();
|
|
14405
15114
|
}
|
|
14406
15115
|
async getCapabilities() {
|
|
14407
|
-
return this.supportsActionBatchPersistence() ? getActionBatchCapabilities() : {};
|
|
15116
|
+
return this.supportsActionBatchPersistence() ? getActionBatchCapabilities(this.actionBatchMaxReservedOutputs, true) : {};
|
|
14408
15117
|
}
|
|
14409
15118
|
supportsActionBatchPersistence() {
|
|
14410
15119
|
return false;
|
|
@@ -14423,6 +15132,9 @@ var StorageProvider = class StorageProvider extends StorageReaderWriter {
|
|
|
14423
15132
|
async renewActionBatch(auth, batchId) {
|
|
14424
15133
|
return await renewActionBatch(this, auth, batchId);
|
|
14425
15134
|
}
|
|
15135
|
+
async resumeActionBatch(auth, args) {
|
|
15136
|
+
return await resumeActionBatch(this, auth, args);
|
|
15137
|
+
}
|
|
14426
15138
|
async prepareActionBatchCommit(auth, manifest) {
|
|
14427
15139
|
return await prepareActionBatchCommit(this, auth, manifest);
|
|
14428
15140
|
}
|
|
@@ -15020,7 +15732,12 @@ var StorageProvider = class StorageProvider extends StorageReaderWriter {
|
|
|
15020
15732
|
for (const output of outputs) {
|
|
15021
15733
|
const outputId = verifyId(output.outputId);
|
|
15022
15734
|
await this.validateOutputScript(output);
|
|
15023
|
-
|
|
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");
|
|
15024
15741
|
}
|
|
15025
15742
|
}
|
|
15026
15743
|
return verdicts;
|
|
@@ -15182,16 +15899,12 @@ var StorageProvider = class StorageProvider extends StorageReaderWriter {
|
|
|
15182
15899
|
} });
|
|
15183
15900
|
for (const o of outputs) {
|
|
15184
15901
|
if (!o.spendable) continue;
|
|
15185
|
-
|
|
15902
|
+
await this.validateOutputScript(o);
|
|
15903
|
+
if (!requireConclusiveUtxo(await classifyOutputUtxo(services, o))) invalidSpendableOutputs.push(o);
|
|
15186
15904
|
}
|
|
15187
15905
|
}
|
|
15188
15906
|
return { invalidSpendableOutputs };
|
|
15189
15907
|
}
|
|
15190
|
-
async checkOutputIsUtxo(o, services) {
|
|
15191
|
-
if (o.lockingScript == null || o.lockingScript.length === 0) return false;
|
|
15192
|
-
const hash = services.hashOutputScript(asString(o.lockingScript));
|
|
15193
|
-
return (await services.getUtxoStatus(hash, void 0, `${o.txid ?? ""}.${o.vout ?? ""}`)).isUtxo === true;
|
|
15194
|
-
}
|
|
15195
15908
|
async updateProvenTxReqDynamics(id, update, trx) {
|
|
15196
15909
|
const partial = {};
|
|
15197
15910
|
if (update.updated_at != null) partial.updated_at = update.updated_at;
|
|
@@ -15301,6 +16014,7 @@ const getLabelToSpecOp = () => {
|
|
|
15301
16014
|
//#region ../src/storage/methods/listActionsIdb.ts
|
|
15302
16015
|
async function enrichIdbActionLabels(storage, tx, action, timeFilterRequested) {
|
|
15303
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);
|
|
15304
16018
|
if (timeFilterRequested) {
|
|
15305
16019
|
const ts = tx.created_at ? new Date(tx.created_at).getTime() : NaN;
|
|
15306
16020
|
if (!Number.isNaN(ts)) {
|
|
@@ -15419,20 +16133,129 @@ async function listActionsIdb(storage, auth, vargs) {
|
|
|
15419
16133
|
return r;
|
|
15420
16134
|
}
|
|
15421
16135
|
//#endregion
|
|
15422
|
-
//#region ../src/storage/methods/
|
|
15423
|
-
const
|
|
15424
|
-
|
|
15425
|
-
|
|
15426
|
-
|
|
15427
|
-
|
|
15428
|
-
|
|
15429
|
-
|
|
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);
|
|
16190
|
+
});
|
|
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")
|
|
15430
16245
|
});
|
|
15431
|
-
|
|
15432
|
-
if (active.length >= maxConcurrency) await Promise.race(active);
|
|
16246
|
+
throw new WERR_UTXO_REVIEW_INCONCLUSIVE(diagnostics.checked, diagnostics.confirmedSpent, diagnostics.unknown);
|
|
15433
16247
|
}
|
|
15434
|
-
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
|
+
};
|
|
15435
16256
|
}
|
|
16257
|
+
//#endregion
|
|
16258
|
+
//#region ../src/storage/methods/ListOutputsSpecOp.ts
|
|
15436
16259
|
const getBasketToSpecOp = () => {
|
|
15437
16260
|
return {
|
|
15438
16261
|
[specOpWalletBalance]: {
|
|
@@ -15463,23 +16286,7 @@ const getBasketToSpecOp = () => {
|
|
|
15463
16286
|
includeSpent: false,
|
|
15464
16287
|
tagsToIntercept: ["release", "all"],
|
|
15465
16288
|
filterOutputs: async (s, auth, vargs, specOpTags, outputs) => {
|
|
15466
|
-
|
|
15467
|
-
const invalidOutputIds = /* @__PURE__ */ new Set();
|
|
15468
|
-
const services = s.getServices();
|
|
15469
|
-
await runWithConcurrency(outputs, INVALID_CHANGE_MAX_CONCURRENCY, async (o) => {
|
|
15470
|
-
if (!o.basketId) return;
|
|
15471
|
-
await s.validateOutputScript(o);
|
|
15472
|
-
let ok = false;
|
|
15473
|
-
if (o.lockingScript != null && o.lockingScript.length > 0) ok = await services.isUtxo(o);
|
|
15474
|
-
else ok = void 0;
|
|
15475
|
-
if (ok === false) invalidOutputIds.add(o.outputId);
|
|
15476
|
-
});
|
|
15477
|
-
const filteredOutputs = outputs.filter((o) => invalidOutputIds.has(o.outputId));
|
|
15478
|
-
if (specOpTags.includes("release")) await runWithConcurrency(filteredOutputs, INVALID_CHANGE_MAX_CONCURRENCY, async (o) => {
|
|
15479
|
-
await s.updateOutput(o.outputId, { spendable: false });
|
|
15480
|
-
o.spendable = false;
|
|
15481
|
-
});
|
|
15482
|
-
return filteredOutputs;
|
|
16289
|
+
return (await reviewUtxoOutputs(s, auth, outputs, specOpTags.includes("release") ? "atomic" : "none")).confirmedSpentOutputs;
|
|
15483
16290
|
}
|
|
15484
16291
|
},
|
|
15485
16292
|
[specOpSetWalletChangeParams]: {
|
|
@@ -15632,7 +16439,7 @@ async function loadIdbOutputs(query) {
|
|
|
15632
16439
|
"nosend",
|
|
15633
16440
|
"sending"
|
|
15634
16441
|
],
|
|
15635
|
-
noScript: true,
|
|
16442
|
+
noScript: specOp?.includeOutputScripts !== true,
|
|
15636
16443
|
orderDescending
|
|
15637
16444
|
};
|
|
15638
16445
|
const pageManagedChange = specOp?.managedChangeOnly === true && specOp.ignoreLimit !== true;
|
|
@@ -15714,7 +16521,10 @@ async function listOutputsIdb(storage, auth, vargs, _originator) {
|
|
|
15714
16521
|
});
|
|
15715
16522
|
result.totalOutputs = totalOutputs;
|
|
15716
16523
|
if (specOp != null) {
|
|
15717
|
-
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
|
+
}
|
|
15718
16528
|
if (specOp.resultFromOutputs != null) return specOp.resultFromOutputs(storage, auth, vargs, specOpTags, outputs);
|
|
15719
16529
|
}
|
|
15720
16530
|
await hydrateIdbOutputResult(storage, outputs, vargs, result);
|
|
@@ -15845,6 +16655,7 @@ async function scanCursor(cursor, since, offset, limit, matches, accept) {
|
|
|
15845
16655
|
var StorageIdb = class extends StorageProvider {
|
|
15846
16656
|
dbName;
|
|
15847
16657
|
db;
|
|
16658
|
+
managedChangeDefaultsMigrated = false;
|
|
15848
16659
|
constructor(options) {
|
|
15849
16660
|
super(options);
|
|
15850
16661
|
this.dbName = `wallet-toolbox-${this.chain}net`;
|
|
@@ -15882,6 +16693,7 @@ var StorageIdb = class extends StorageProvider {
|
|
|
15882
16693
|
async verifyDB(storageName, storageIdentityKey) {
|
|
15883
16694
|
if (this.db != null) return this.db;
|
|
15884
16695
|
this.db = await this.initDB(storageName, storageIdentityKey);
|
|
16696
|
+
await this.migrateManagedChangeDefaults(this.db);
|
|
15885
16697
|
this._settings = (await this.db.getAll("settings"))[0];
|
|
15886
16698
|
this.whenLastAccess = /* @__PURE__ */ new Date();
|
|
15887
16699
|
return this.db;
|
|
@@ -15915,7 +16727,7 @@ var StorageIdb = class extends StorageProvider {
|
|
|
15915
16727
|
async initDB(storageName, storageIdentityKey) {
|
|
15916
16728
|
const chain = this.chain;
|
|
15917
16729
|
const maxOutputScript = 1024;
|
|
15918
|
-
return await (0, idb.openDB)(this.dbName,
|
|
16730
|
+
return await (0, idb.openDB)(this.dbName, 4, { upgrade(db, _oldVersion, _newVersion, transaction) {
|
|
15919
16731
|
upgradeAllStoresV1(db);
|
|
15920
16732
|
upgradeActionBatchStoresV2(db);
|
|
15921
16733
|
const outputs = transaction.objectStore("outputs");
|
|
@@ -15941,6 +16753,22 @@ var StorageIdb = class extends StorageProvider {
|
|
|
15941
16753
|
}
|
|
15942
16754
|
} });
|
|
15943
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
|
+
}
|
|
15944
16772
|
async reviewStatus(args) {
|
|
15945
16773
|
return await reviewStatusIdb(this, args);
|
|
15946
16774
|
}
|
|
@@ -16695,6 +17523,7 @@ var StorageIdb = class extends StorageProvider {
|
|
|
16695
17523
|
if (this.db != null) this.db.close();
|
|
16696
17524
|
this.db = void 0;
|
|
16697
17525
|
this._settings = void 0;
|
|
17526
|
+
this.managedChangeDefaultsMigrated = false;
|
|
16698
17527
|
}
|
|
16699
17528
|
allStores = [
|
|
16700
17529
|
"action_batches",
|
|
@@ -17867,6 +18696,9 @@ var StorageClientBase = class {
|
|
|
17867
18696
|
async renewActionBatch(auth, batchId) {
|
|
17868
18697
|
return await this.rpcCall("renewActionBatch", [auth, batchId]);
|
|
17869
18698
|
}
|
|
18699
|
+
async resumeActionBatch(auth, args) {
|
|
18700
|
+
return await this.rpcCall("resumeActionBatch", [auth, args]);
|
|
18701
|
+
}
|
|
17870
18702
|
async prepareActionBatchCommit(auth, manifest) {
|
|
17871
18703
|
return await this.rpcCall("prepareActionBatchCommit", [auth, manifest]);
|
|
17872
18704
|
}
|
|
@@ -18671,7 +19503,7 @@ async function restoreBRC38(storage, data) {
|
|
|
18671
19503
|
await storage.transaction(async (trx) => {
|
|
18672
19504
|
await storage.insertUser({ ...data.user }, trx);
|
|
18673
19505
|
for (const row of data.provenTxs) await storage.insertProvenTx({ ...row }, trx);
|
|
18674
|
-
for (const row of data.outputBaskets) await storage.insertOutputBasket({ ...row }, trx);
|
|
19506
|
+
for (const row of data.outputBaskets) await storage.insertOutputBasket(upgradeLegacyManagedChangeBasketDefault({ ...row }), trx);
|
|
18675
19507
|
for (const row of data.outputTags) await storage.insertOutputTag({ ...row }, trx);
|
|
18676
19508
|
for (const row of data.txLabels) await storage.insertTxLabel({ ...row }, trx);
|
|
18677
19509
|
for (const row of data.transactions) await storage.insertTransaction({ ...row }, trx);
|
|
@@ -21730,13 +22562,37 @@ function configuredChaintracksClient(chain, serviceUrl) {
|
|
|
21730
22562
|
return new ChaintracksServiceClient(chain, serviceUrl);
|
|
21731
22563
|
}
|
|
21732
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
|
+
/**
|
|
21733
22574
|
* Returns the credential-free default ChainTracks client for a supported
|
|
21734
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.
|
|
21735
22589
|
*/
|
|
21736
22590
|
function createDefaultChaintracksClient(chain) {
|
|
21737
22591
|
switch (chain) {
|
|
21738
22592
|
case "main":
|
|
21739
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" });
|
|
21740
22596
|
case "ttn": return new GoChaintracksServiceClient(chain, arcadeDefaultUrl(chain), { apiPrefix: "/chaintracks/v2" });
|
|
21741
22597
|
case "stn": return configuredChaintracksClient(chain, stnChaintracksUrl());
|
|
21742
22598
|
case "tstn": return configuredChaintracksClient(chain, tstnChaintracksUrl());
|
|
@@ -21775,7 +22631,7 @@ function createDefaultWalletServicesOptions(...[chain, arcCallbackUrl, arcCallba
|
|
|
21775
22631
|
exchangeratesapiKey: void 0,
|
|
21776
22632
|
chaintracksFiatExchangeRatesUrl,
|
|
21777
22633
|
chaintracks,
|
|
21778
|
-
arcUrl: arcDefaultUrl(chain),
|
|
22634
|
+
arcUrl: chain === "ttn" ? "" : arcDefaultUrl(chain),
|
|
21779
22635
|
arcConfig: {
|
|
21780
22636
|
apiKey: taalArcApiKey ?? void 0,
|
|
21781
22637
|
deploymentId,
|
|
@@ -21791,7 +22647,7 @@ function createDefaultWalletServicesOptions(...[chain, arcCallbackUrl, arcCallba
|
|
|
21791
22647
|
},
|
|
21792
22648
|
bitailsApiKey
|
|
21793
22649
|
};
|
|
21794
|
-
const resolvedArcadeUrl = arcadeUrl;
|
|
22650
|
+
const resolvedArcadeUrl = arcadeUrl ?? (chain === "ttn" ? arcadeDefaultUrl(chain) : void 0);
|
|
21795
22651
|
if (resolvedArcadeUrl != null && resolvedArcadeUrl !== "") {
|
|
21796
22652
|
o.arcadeUrl = resolvedArcadeUrl;
|
|
21797
22653
|
o.arcadeConfig = {
|
|
@@ -21821,7 +22677,7 @@ function arcDefaultUrl(chain) {
|
|
|
21821
22677
|
case "main": return "https://arc.taal.com";
|
|
21822
22678
|
case "test": return "https://arc-test.taal.com";
|
|
21823
22679
|
case "stn": return stnArcadeUrl() ?? "";
|
|
21824
|
-
case "ttn": return "
|
|
22680
|
+
case "ttn": return "";
|
|
21825
22681
|
case "tstn": return tstnArcadeUrl() ?? "";
|
|
21826
22682
|
case "mock": return "";
|
|
21827
22683
|
}
|
|
@@ -22193,6 +23049,40 @@ var ARC = class {
|
|
|
22193
23049
|
}
|
|
22194
23050
|
};
|
|
22195
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
|
|
22196
23086
|
//#region ../src/services/providers/Arcade.ts
|
|
22197
23087
|
function defaultDeploymentId() {
|
|
22198
23088
|
return `ts-sdk-${_bsv_sdk.Utils.toHex((0, _bsv_sdk.Random)(16))}`;
|
|
@@ -22462,6 +23352,73 @@ var Arcade = class {
|
|
|
22462
23352
|
return (await this.httpClient.request(`${this.URL}/tx/${txid}`, requestOptions)).data;
|
|
22463
23353
|
}
|
|
22464
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
|
+
/**
|
|
22465
23422
|
* `getMerklePath` provider: obtain a BUMP merkle proof for a mined transaction from Arcade.
|
|
22466
23423
|
*
|
|
22467
23424
|
* Arcade only has a proof for transactions it tracked (i.e. broadcast through it) that have
|
|
@@ -23252,6 +24209,10 @@ var Services = class Services {
|
|
|
23252
24209
|
name: "WhatsOnChain",
|
|
23253
24210
|
service: this.whatsonchain.getStatusForTxids.bind(this.whatsonchain)
|
|
23254
24211
|
});
|
|
24212
|
+
if (this.arcade != null) this.getStatusForTxidsServices.add({
|
|
24213
|
+
name: "Arcade",
|
|
24214
|
+
service: this.arcade.getStatusForTxids.bind(this.arcade)
|
|
24215
|
+
});
|
|
23255
24216
|
this.getScriptHashHistoryServices = new ServiceCollection("getScriptHashHistory");
|
|
23256
24217
|
if (hasWhatsOnChain) this.getScriptHashHistoryServices.add({
|
|
23257
24218
|
name: "WhatsOnChain",
|
|
@@ -23268,7 +24229,7 @@ var Services = class Services {
|
|
|
23268
24229
|
name: "GorillaPoolArcBeef",
|
|
23269
24230
|
service: this.arcGorillaPool.postBeef.bind(this.arcGorillaPool)
|
|
23270
24231
|
});
|
|
23271
|
-
this.postBeefServices.add({
|
|
24232
|
+
if (this.options.arcUrl != null && this.options.arcUrl !== "") this.postBeefServices.add({
|
|
23272
24233
|
name: "TaalArcBeef",
|
|
23273
24234
|
service: this.arcTaal.postBeef.bind(this.arcTaal)
|
|
23274
24235
|
});
|
|
@@ -23354,30 +24315,65 @@ var Services = class Services {
|
|
|
23354
24315
|
async getStatusForTxids(txids, useNext) {
|
|
23355
24316
|
const services = this.getStatusForTxidsServices;
|
|
23356
24317
|
if (useNext === true) services.next();
|
|
23357
|
-
let
|
|
24318
|
+
let fallback = {
|
|
23358
24319
|
name: "<noservices>",
|
|
23359
24320
|
status: "error",
|
|
23360
24321
|
error: new WERR_INTERNAL("No services available."),
|
|
23361
24322
|
results: []
|
|
23362
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
|
+
};
|
|
23363
24334
|
for (let tries = 0; tries < services.count; tries++) {
|
|
23364
24335
|
const stc = services.serviceToCall;
|
|
23365
24336
|
try {
|
|
23366
|
-
const
|
|
24337
|
+
const requestedTxids = [...unresolved];
|
|
24338
|
+
if (requestedTxids.length === 0) break;
|
|
24339
|
+
const r = await this.getStatusForTxidsBatched(stc, requestedTxids);
|
|
23367
24340
|
if (r.status === "success") {
|
|
23368
24341
|
services.addServiceCallSuccess(stc);
|
|
23369
|
-
|
|
23370
|
-
|
|
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;
|
|
23371
24351
|
}
|
|
24352
|
+
fallback = r;
|
|
23372
24353
|
if (r.error != null) services.addServiceCallError(stc, r.error);
|
|
23373
24354
|
else services.addServiceCallFailure(stc);
|
|
23374
24355
|
} catch (error_) {
|
|
23375
24356
|
const e = WalletError.fromUnknown(error_);
|
|
24357
|
+
fallback = {
|
|
24358
|
+
name: stc.providerName,
|
|
24359
|
+
status: "error",
|
|
24360
|
+
error: e,
|
|
24361
|
+
results: []
|
|
24362
|
+
};
|
|
23376
24363
|
services.addServiceCallError(stc, e);
|
|
23377
24364
|
}
|
|
23378
24365
|
services.next();
|
|
23379
24366
|
}
|
|
23380
|
-
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
|
+
};
|
|
23381
24377
|
}
|
|
23382
24378
|
async getStatusForTxidsBatched(stc, txids) {
|
|
23383
24379
|
const results = [];
|
|
@@ -23404,9 +24400,7 @@ var Services = class Services {
|
|
|
23404
24400
|
return _bsv_sdk.Utils.toHex(sha256Hash(_bsv_sdk.Utils.toArray(script, "hex")));
|
|
23405
24401
|
}
|
|
23406
24402
|
async isUtxo(output) {
|
|
23407
|
-
|
|
23408
|
-
const hash = this.hashOutputScript(_bsv_sdk.Utils.toHex(output.lockingScript));
|
|
23409
|
-
return (await this.getUtxoStatus(hash, void 0, `${output.txid ?? ""}.${output.vout}`)).isUtxo === true;
|
|
24403
|
+
return requireConclusiveUtxo(await classifyOutputUtxo(this, output));
|
|
23410
24404
|
}
|
|
23411
24405
|
async getUtxoStatus(output, outputFormat, outpoint, useNext, logger) {
|
|
23412
24406
|
const services = this.getUtxoStatusServices;
|
|
@@ -23632,7 +24626,15 @@ var Services = class Services {
|
|
|
23632
24626
|
const method = async () => {
|
|
23633
24627
|
return await this.options.chaintracks.currentHeight();
|
|
23634
24628
|
};
|
|
23635
|
-
|
|
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
|
+
}
|
|
23636
24638
|
}
|
|
23637
24639
|
async hashToHeader(hash) {
|
|
23638
24640
|
const method = async () => {
|
|
@@ -27351,17 +28353,34 @@ var ArcSSEClient = class {
|
|
|
27351
28353
|
console.log(`${TAG} connected`);
|
|
27352
28354
|
});
|
|
27353
28355
|
this.es.addEventListener("status", (event) => {
|
|
28356
|
+
let data;
|
|
27354
28357
|
try {
|
|
27355
|
-
|
|
27356
|
-
console.log(`${TAG} event: txid=${data.txid} status=${data.txStatus}`);
|
|
27357
|
-
if (typeof event.lastEventId === "string" && event.lastEventId !== "") {
|
|
27358
|
-
this._lastEventId = event.lastEventId;
|
|
27359
|
-
this.options.onLastEventIdChanged?.(event.lastEventId);
|
|
27360
|
-
}
|
|
27361
|
-
this.options.onEvent(data);
|
|
28358
|
+
data = JSON.parse(event.data);
|
|
27362
28359
|
} catch {
|
|
27363
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;
|
|
27364
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
|
+
});
|
|
27365
28384
|
});
|
|
27366
28385
|
this.es.addEventListener("error", (event) => {
|
|
27367
28386
|
console.log(`${TAG} error:`, JSON.stringify(event));
|
|
@@ -27396,94 +28415,6 @@ var ArcSSEClient = class {
|
|
|
27396
28415
|
}
|
|
27397
28416
|
};
|
|
27398
28417
|
//#endregion
|
|
27399
|
-
//#region ../src/monitor/tasks/TaskUnFail.ts
|
|
27400
|
-
/**
|
|
27401
|
-
* Setting provenTxReq status to 'unfail' when 'invalid' will attempt to find a merklePath, and if successful:
|
|
27402
|
-
*
|
|
27403
|
-
* 1. set the req status to 'unmined'
|
|
27404
|
-
* 2. set the referenced txs to 'unproven'
|
|
27405
|
-
* 3. determine if any inputs match user's existing outputs and if so update spentBy and spendable of those outputs.
|
|
27406
|
-
* 4. set the txs outputs to spendable
|
|
27407
|
-
*
|
|
27408
|
-
* If it fails (to find a merklePath), returns the req status to 'invalid'.
|
|
27409
|
-
*/
|
|
27410
|
-
var TaskUnFail = class TaskUnFail extends WalletMonitorTask {
|
|
27411
|
-
triggerMsecs;
|
|
27412
|
-
static taskName = "UnFail";
|
|
27413
|
-
/**
|
|
27414
|
-
* Set to true to trigger running this task
|
|
27415
|
-
*/
|
|
27416
|
-
static checkNowRequested = false;
|
|
27417
|
-
static get checkNow() {
|
|
27418
|
-
return this.checkNowRequested;
|
|
27419
|
-
}
|
|
27420
|
-
static set checkNow(value) {
|
|
27421
|
-
this.checkNowRequested = value;
|
|
27422
|
-
}
|
|
27423
|
-
constructor(monitor, triggerMsecs = Monitor.oneMinute * 10) {
|
|
27424
|
-
super(monitor, TaskUnFail.taskName);
|
|
27425
|
-
this.triggerMsecs = triggerMsecs;
|
|
27426
|
-
}
|
|
27427
|
-
trigger(nowMsecsSinceEpoch) {
|
|
27428
|
-
return { run: TaskUnFail.checkNow || this.triggerMsecs > 0 && nowMsecsSinceEpoch - this.lastRunMsecsSinceEpoch > this.triggerMsecs };
|
|
27429
|
-
}
|
|
27430
|
-
async runTask() {
|
|
27431
|
-
let log = "";
|
|
27432
|
-
TaskUnFail.checkNow = false;
|
|
27433
|
-
const limit = 100;
|
|
27434
|
-
let offset = 0;
|
|
27435
|
-
for (;;) {
|
|
27436
|
-
const reqs = await this.storage.findProvenTxReqs({
|
|
27437
|
-
partial: {},
|
|
27438
|
-
status: ["unfail"],
|
|
27439
|
-
paged: {
|
|
27440
|
-
limit,
|
|
27441
|
-
offset
|
|
27442
|
-
}
|
|
27443
|
-
});
|
|
27444
|
-
if (reqs.length === 0) break;
|
|
27445
|
-
log += `${reqs.length} reqs with status 'unfail'\n`;
|
|
27446
|
-
const r = await this.unfail(reqs, 2);
|
|
27447
|
-
log += `${r.log}\n`;
|
|
27448
|
-
if (reqs.length < limit) break;
|
|
27449
|
-
offset += limit;
|
|
27450
|
-
}
|
|
27451
|
-
return log;
|
|
27452
|
-
}
|
|
27453
|
-
async unfail(reqs, indent = 0) {
|
|
27454
|
-
let log = "";
|
|
27455
|
-
for (const reqApi of reqs) {
|
|
27456
|
-
const req = new EntityProvenTxReq(reqApi);
|
|
27457
|
-
log += " ".repeat(indent);
|
|
27458
|
-
log += `reqId ${reqApi.provenTxReqId} txid ${reqApi.txid}: `;
|
|
27459
|
-
if ((await this.monitor.services.getMerklePath(req.txid)).merklePath != null) {
|
|
27460
|
-
log += "unfailed. status is now 'unmined'\n";
|
|
27461
|
-
log += await this.unfailReq(req, indent + 2);
|
|
27462
|
-
} else {
|
|
27463
|
-
req.status = "invalid";
|
|
27464
|
-
log += "returned to status 'invalid'\n";
|
|
27465
|
-
await req.updateStorageDynamicProperties(this.storage);
|
|
27466
|
-
}
|
|
27467
|
-
}
|
|
27468
|
-
return { log };
|
|
27469
|
-
}
|
|
27470
|
-
/**
|
|
27471
|
-
* 2. set the referenced txs to 'unproven'
|
|
27472
|
-
* 3. determine if any inputs match user's existing outputs and if so update spentBy and spendable of those outputs.
|
|
27473
|
-
* 4. set the txs outputs to spendable
|
|
27474
|
-
*
|
|
27475
|
-
* @param req
|
|
27476
|
-
* @param indent
|
|
27477
|
-
* @returns
|
|
27478
|
-
*/
|
|
27479
|
-
async unfailReq(req, indent) {
|
|
27480
|
-
return await this.storage.runAsStorageProvider(async (sp) => await sp.unfailTransactionsForProof(req, indent, {
|
|
27481
|
-
status: "unmined",
|
|
27482
|
-
attempts: 0
|
|
27483
|
-
}));
|
|
27484
|
-
}
|
|
27485
|
-
};
|
|
27486
|
-
//#endregion
|
|
27487
28418
|
//#region ../src/monitor/tasks/TaskArcSSE.ts
|
|
27488
28419
|
/**
|
|
27489
28420
|
* Monitor task that receives transaction status updates from Arcade via SSE
|
|
@@ -27527,16 +28458,14 @@ var TaskArcadeSSE = class TaskArcadeSSE extends WalletMonitorTask {
|
|
|
27527
28458
|
arcApiKey: arcadeApiKey,
|
|
27528
28459
|
lastEventId,
|
|
27529
28460
|
EventSourceClass,
|
|
27530
|
-
onEvent: (event) => {
|
|
27531
|
-
this.pendingEvents.push(
|
|
27532
|
-
|
|
28461
|
+
onEvent: async (event) => await new Promise((acknowledge) => {
|
|
28462
|
+
this.pendingEvents.push({
|
|
28463
|
+
event,
|
|
28464
|
+
acknowledge
|
|
28465
|
+
});
|
|
28466
|
+
}),
|
|
27533
28467
|
onError: (err) => {
|
|
27534
28468
|
this.logSetupEvent(`SSE error: ${err.message}`);
|
|
27535
|
-
},
|
|
27536
|
-
onLastEventIdChanged: (id) => {
|
|
27537
|
-
this.monitor.options.saveLastSSEEventId?.(id).catch((e) => {
|
|
27538
|
-
this.logSetupEvent(`failed to persist lastEventId: ${stringifyError(e)}`);
|
|
27539
|
-
});
|
|
27540
28469
|
}
|
|
27541
28470
|
});
|
|
27542
28471
|
this.sseClient.connect();
|
|
@@ -27550,10 +28479,21 @@ var TaskArcadeSSE = class TaskArcadeSSE extends WalletMonitorTask {
|
|
|
27550
28479
|
return { run: this.pendingEvents.length > 0 };
|
|
27551
28480
|
}
|
|
27552
28481
|
async runTask() {
|
|
27553
|
-
const
|
|
27554
|
-
if (
|
|
28482
|
+
const eventCount = this.pendingEvents.length;
|
|
28483
|
+
if (eventCount === 0) return "";
|
|
27555
28484
|
let log = "";
|
|
27556
|
-
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
|
+
}
|
|
27557
28497
|
return log;
|
|
27558
28498
|
}
|
|
27559
28499
|
async fetchNow() {
|
|
@@ -27569,28 +28509,26 @@ var TaskArcadeSSE = class TaskArcadeSSE extends WalletMonitorTask {
|
|
|
27569
28509
|
}
|
|
27570
28510
|
for (const reqApi of reqs) {
|
|
27571
28511
|
const req = new EntityProvenTxReq(reqApi);
|
|
27572
|
-
const
|
|
27573
|
-
if (ProvenTxReqTerminalStatus.includes(req.status) && !
|
|
28512
|
+
const canRecoverTerminalRequest = this.canRecoverTerminalRequest(req, event);
|
|
28513
|
+
if (ProvenTxReqTerminalStatus.includes(req.status) && !canRecoverTerminalRequest) {
|
|
27574
28514
|
log += ` req ${req.id} already terminal: ${req.status}\n`;
|
|
27575
28515
|
continue;
|
|
27576
28516
|
}
|
|
27577
28517
|
const note = {
|
|
27578
28518
|
when: (/* @__PURE__ */ new Date()).toISOString(),
|
|
27579
28519
|
what: "arcSSE",
|
|
27580
|
-
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) } : {}
|
|
27581
28524
|
};
|
|
27582
28525
|
log += await this.applyStatusEvent(req, event, note);
|
|
27583
28526
|
}
|
|
27584
28527
|
this.monitor.callOnTransactionStatusChanged(event.txid, event.txStatus);
|
|
27585
28528
|
return log;
|
|
27586
28529
|
}
|
|
27587
|
-
|
|
27588
|
-
return req.status === "invalid" && (
|
|
27589
|
-
"SENT_TO_NETWORK",
|
|
27590
|
-
"ACCEPTED_BY_NETWORK",
|
|
27591
|
-
"SEEN_ON_NETWORK",
|
|
27592
|
-
"SEEN_MULTIPLE_NODES"
|
|
27593
|
-
].includes(event.txStatus) || ["MINED", "IMMUTABLE"].includes(event.txStatus));
|
|
28530
|
+
canRecoverTerminalRequest(req, event) {
|
|
28531
|
+
return (req.status === "invalid" || req.status === "doubleSpend") && ["MINED", "IMMUTABLE"].includes(event.txStatus);
|
|
27594
28532
|
}
|
|
27595
28533
|
async applyStatusEvent(req, event, note) {
|
|
27596
28534
|
switch (event.txStatus) {
|
|
@@ -27600,11 +28538,16 @@ var TaskArcadeSSE = class TaskArcadeSSE extends WalletMonitorTask {
|
|
|
27600
28538
|
case "SEEN_MULTIPLE_NODES": return await this.applyAcceptedStatus(req, note);
|
|
27601
28539
|
case "MINED":
|
|
27602
28540
|
case "IMMUTABLE": return await this.applyMinedStatus(req, note);
|
|
27603
|
-
case "DOUBLE_SPEND_ATTEMPTED":
|
|
27604
|
-
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":
|
|
27605
28548
|
req.addHistoryNote(note);
|
|
27606
28549
|
await req.updateStorageDynamicProperties(this.storage);
|
|
27607
|
-
return ` req ${req.id}
|
|
28550
|
+
return ` req ${req.id} ${event.txStatus} recorded; awaiting resolution\n`;
|
|
27608
28551
|
default: return ` req ${req.id} unhandled status: ${event.txStatus}\n`;
|
|
27609
28552
|
}
|
|
27610
28553
|
}
|
|
@@ -27612,43 +28555,73 @@ var TaskArcadeSSE = class TaskArcadeSSE extends WalletMonitorTask {
|
|
|
27612
28555
|
if (![
|
|
27613
28556
|
"unsent",
|
|
27614
28557
|
"sending",
|
|
27615
|
-
"callback"
|
|
27616
|
-
"invalid"
|
|
28558
|
+
"callback"
|
|
27617
28559
|
].includes(req.status)) return "";
|
|
27618
|
-
|
|
27619
|
-
let log = "";
|
|
27620
|
-
if (wasInvalid) log += await new TaskUnFail(this.monitor).unfailReq(req, 4);
|
|
27621
|
-
else if (req.notify.transactionIds != null) await this.storage.runAsStorageProvider(async (sp) => {
|
|
28560
|
+
if (req.notify.transactionIds != null) await this.storage.runAsStorageProvider(async (sp) => {
|
|
27622
28561
|
await sp.updateTransactionsStatus(req.notify.transactionIds ?? [], "unproven");
|
|
27623
28562
|
});
|
|
27624
28563
|
req.status = "unmined";
|
|
27625
|
-
if (wasInvalid) req.attempts = 0;
|
|
27626
28564
|
req.wasBroadcast = true;
|
|
27627
28565
|
req.addHistoryNote(note);
|
|
27628
28566
|
await req.updateStorageDynamicProperties(this.storage);
|
|
27629
|
-
return
|
|
28567
|
+
return ` req ${req.id} => unmined\n`;
|
|
27630
28568
|
}
|
|
27631
28569
|
async applyMinedStatus(req, note) {
|
|
27632
|
-
let log = "";
|
|
27633
|
-
if (req.status === "invalid") {
|
|
27634
|
-
log += await new TaskUnFail(this.monitor).unfailReq(req, 4);
|
|
27635
|
-
req.status = "unmined";
|
|
27636
|
-
req.attempts = 0;
|
|
27637
|
-
req.wasBroadcast = true;
|
|
27638
|
-
}
|
|
27639
28570
|
req.addHistoryNote(note);
|
|
27640
28571
|
await req.updateStorageDynamicProperties(this.storage);
|
|
27641
|
-
return
|
|
28572
|
+
return await this.fetchProofFromServices(req);
|
|
27642
28573
|
}
|
|
27643
|
-
async applyDoubleSpendStatus(req, note) {
|
|
27644
|
-
req
|
|
27645
|
-
|
|
27646
|
-
|
|
27647
|
-
|
|
27648
|
-
|
|
27649
|
-
|
|
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
|
+
});
|
|
27650
28623
|
});
|
|
27651
|
-
return ` req ${req.id} =>
|
|
28624
|
+
return ` req ${req.id} => ${classification.reqStatus}; quarantined ${quarantined.staleConfirmed} local input copy/copies\n`;
|
|
27652
28625
|
}
|
|
27653
28626
|
/**
|
|
27654
28627
|
* Complete a MINED/IMMUTABLE status by using the configured proof providers.
|
|
@@ -28060,9 +29033,104 @@ var TaskCheckNoSends = class TaskCheckNoSends extends WalletMonitorTask {
|
|
|
28060
29033
|
}
|
|
28061
29034
|
};
|
|
28062
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
|
|
28063
29124
|
//#region ../src/monitor/tasks/TaskReviewUtxos.ts
|
|
29125
|
+
const REVIEW_PAGE_DEFAULT_LIMIT = 20;
|
|
29126
|
+
const REVIEW_PAGE_MAX_LIMIT = 250;
|
|
28064
29127
|
/**
|
|
28065
|
-
* 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.
|
|
28066
29134
|
*
|
|
28067
29135
|
* The task itself is disabled and will not run on a schedule; review must be triggered manually by calling reviewByIdentityKey.
|
|
28068
29136
|
*/
|
|
@@ -28079,7 +29147,7 @@ var TaskReviewUtxos = class TaskReviewUtxos extends WalletMonitorTask {
|
|
|
28079
29147
|
static set checkNow(value) {
|
|
28080
29148
|
this.checkNowRequested = value;
|
|
28081
29149
|
}
|
|
28082
|
-
constructor(monitor, triggerMsecs = 0, userLimit = 10, userOffset = 0, tags = ["
|
|
29150
|
+
constructor(monitor, triggerMsecs = 0, userLimit = 10, userOffset = 0, tags = ["all"]) {
|
|
28083
29151
|
super(monitor, TaskReviewUtxos.taskName);
|
|
28084
29152
|
this.triggerMsecs = triggerMsecs;
|
|
28085
29153
|
this.userLimit = userLimit;
|
|
@@ -28093,8 +29161,8 @@ var TaskReviewUtxos = class TaskReviewUtxos extends WalletMonitorTask {
|
|
|
28093
29161
|
TaskReviewUtxos.checkNow = false;
|
|
28094
29162
|
return "TaskReviewUtxos is disabled; use reviewByIdentityKey instead.\n";
|
|
28095
29163
|
}
|
|
28096
|
-
async reviewByIdentityKey(identityKey, mode = "all") {
|
|
28097
|
-
const tags = ["release", ...mode === "all" ? ["all"] : []];
|
|
29164
|
+
async reviewByIdentityKey(identityKey, mode = "all", release = false) {
|
|
29165
|
+
const tags = [...release ? ["release"] : [], ...mode === "all" ? ["all"] : []];
|
|
28098
29166
|
const vargs = {
|
|
28099
29167
|
basket: specOpInvalidChange,
|
|
28100
29168
|
tags,
|
|
@@ -28122,8 +29190,150 @@ var TaskReviewUtxos = class TaskReviewUtxos extends WalletMonitorTask {
|
|
|
28122
29190
|
return this.toUserLog(user, result.outputs, result.totalOutputs, total, tags);
|
|
28123
29191
|
});
|
|
28124
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
|
+
}
|
|
28125
29335
|
toUserLog(user, outputs, totalOutputs, total, tags) {
|
|
28126
|
-
const action = tags.includes("release") ? "updated to unspendable" : "
|
|
29336
|
+
const action = tags.includes("release") ? "confirmed spent and updated to unspendable" : "confirmed spent";
|
|
28127
29337
|
const target = tags.includes("all") ? "spendable utxos" : "spendable change utxos";
|
|
28128
29338
|
let log = `userId ${user.userId}: ${totalOutputs} ${target} ${action}, total ${total}, ${user.identityKey}\n`;
|
|
28129
29339
|
for (const output of outputs) log += ` ${output.outpoint} ${output.satoshis} now ${output.spendable ? "spendable" : "spent"}\n`;
|
|
@@ -28132,6 +29342,13 @@ var TaskReviewUtxos = class TaskReviewUtxos extends WalletMonitorTask {
|
|
|
28132
29342
|
};
|
|
28133
29343
|
//#endregion
|
|
28134
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
|
+
}
|
|
28135
29352
|
/**
|
|
28136
29353
|
* Review recent reqs in terminal 'doubleSpend' state and move any false positives
|
|
28137
29354
|
* back to 'unfail' so existing recovery handling can re-process them.
|
|
@@ -28199,14 +29416,16 @@ var TaskReviewDoubleSpends = class TaskReviewDoubleSpends extends WalletMonitorT
|
|
|
28199
29416
|
let lastRetainedDoubleSpendIndex = -1;
|
|
28200
29417
|
let retainedDoubleSpendCount = 0;
|
|
28201
29418
|
for (const req of reqs) {
|
|
28202
|
-
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);
|
|
28203
29422
|
reviewed.push(req);
|
|
28204
|
-
if (status === "
|
|
29423
|
+
if (!(status === "mined" || status === "known" && !durableInputConflict)) {
|
|
28205
29424
|
lastRetainedDoubleSpendIndex = reviewed.length - 1;
|
|
28206
29425
|
retainedDoubleSpendCount += 1;
|
|
28207
29426
|
} else {
|
|
28208
29427
|
unfails.push(req.provenTxReqId);
|
|
28209
|
-
log += `unfail ${req.provenTxReqId} ${req.txid} status:${status}\n`;
|
|
29428
|
+
log += `unfail ${req.provenTxReqId} ${req.txid} status:${status} durableInputConflict:${String(durableInputConflict)}\n`;
|
|
28210
29429
|
}
|
|
28211
29430
|
}
|
|
28212
29431
|
if (unfails.length > 0) await this.storage.runAsStorageProvider(async (sp) => {
|
|
@@ -28245,6 +29464,168 @@ var TaskReviewDoubleSpends = class TaskReviewDoubleSpends extends WalletMonitorT
|
|
|
28245
29464
|
}
|
|
28246
29465
|
};
|
|
28247
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
|
|
28248
29629
|
//#region ../src/monitor/tasks/TaskReviewProvenTxs.ts
|
|
28249
29630
|
/**
|
|
28250
29631
|
* Backup verification task for recent proven_txs records.
|
|
@@ -28503,14 +29884,14 @@ var Monitor = class Monitor {
|
|
|
28503
29884
|
purgeFailedAge: 5 * Monitor.oneDay
|
|
28504
29885
|
};
|
|
28505
29886
|
addAllTasksToOther() {
|
|
28506
|
-
this._otherTasks.push(new TaskClock(this), new TaskNewHeader(this), new TaskMonitorCallHistory(this), new TaskSendWaiting(this), new TaskCheckForProofs(this), new TaskCheckNoSends(this), new TaskFailAbandoned(this), new TaskUnFail(this), new TaskReviewStatus(this), new TaskReorg(this), new TaskReviewUtxos(this), new TaskReviewDoubleSpends(this), new TaskReviewProvenTxs(this), new TaskCleanupActionBatches(this), new TaskPurge(this, this.defaultPurgeParams));
|
|
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));
|
|
28507
29888
|
if (this.chain === "mock") this._otherTasks.push(new TaskMineBlock(this));
|
|
28508
29889
|
}
|
|
28509
29890
|
/**
|
|
28510
29891
|
* Default tasks with settings appropriate for a single user storage
|
|
28511
29892
|
*/
|
|
28512
29893
|
addDefaultTasks() {
|
|
28513
|
-
this._tasks.push(new TaskClock(this), new TaskNewHeader(this), new TaskMonitorCallHistory(this), new TaskSendWaiting(this, 8 * Monitor.oneSecond, 7 * Monitor.oneSecond), new TaskCheckForProofs(this, 2 * Monitor.oneHour), new TaskCheckNoSends(this), new TaskFailAbandoned(this, 8 * Monitor.oneMinute), new TaskUnFail(this), new TaskReviewStatus(this), new TaskReorg(this), new TaskReviewDoubleSpends(this), new TaskReviewProvenTxs(this), new TaskCleanupActionBatches(this), new TaskArcadeSSE(this));
|
|
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));
|
|
28514
29895
|
this._otherTasks.push(new TaskPurge(this, this.defaultPurgeParams, 6 * Monitor.oneHour), new TaskReviewUtxos(this));
|
|
28515
29896
|
if (this.chain === "mock") this._tasks.push(new TaskMineBlock(this));
|
|
28516
29897
|
}
|
|
@@ -28518,7 +29899,7 @@ var Monitor = class Monitor {
|
|
|
28518
29899
|
* Tasks appropriate for multi-user storage
|
|
28519
29900
|
*/
|
|
28520
29901
|
addMultiUserTasks() {
|
|
28521
|
-
this._tasks.push(new TaskClock(this), new TaskNewHeader(this), new TaskMonitorCallHistory(this), new TaskSendWaiting(this, 8 * Monitor.oneSecond, 7 * Monitor.oneSecond), new TaskCheckForProofs(this, 2 * Monitor.oneHour), new TaskCheckNoSends(this), new TaskFailAbandoned(this, 8 * Monitor.oneMinute), new TaskUnFail(this), new TaskReviewStatus(this), new TaskReorg(this), new TaskReviewDoubleSpends(this), new TaskReviewProvenTxs(this), new TaskCleanupActionBatches(this), new TaskArcadeSSE(this));
|
|
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));
|
|
28522
29903
|
this._otherTasks.push(new TaskPurge(this, this.defaultPurgeParams), new TaskReviewUtxos(this));
|
|
28523
29904
|
if (this.chain === "mock") this._tasks.push(new TaskMineBlock(this));
|
|
28524
29905
|
}
|
|
@@ -28889,6 +30270,7 @@ var SetupClient = class SetupClient {
|
|
|
28889
30270
|
model: "sat/kb",
|
|
28890
30271
|
value: 100
|
|
28891
30272
|
},
|
|
30273
|
+
managedChangePolicy: args.managedChangePolicy,
|
|
28892
30274
|
scriptVerifier: args.scriptVerifier
|
|
28893
30275
|
});
|
|
28894
30276
|
await storage.migrate(args.databaseName, randomBytesHex(33));
|
|
@@ -33478,7 +34860,7 @@ var WalletPermissionsManager = class WalletPermissionsManager {
|
|
|
33478
34860
|
basket: basketName,
|
|
33479
34861
|
tags
|
|
33480
34862
|
}],
|
|
33481
|
-
options: { acceptDelayedBroadcast:
|
|
34863
|
+
options: { acceptDelayedBroadcast: true }
|
|
33482
34864
|
}, this.adminOriginator);
|
|
33483
34865
|
}
|
|
33484
34866
|
async mapWithConcurrency(items, concurrency, fn) {
|
|
@@ -33542,7 +34924,7 @@ var WalletPermissionsManager = class WalletPermissionsManager {
|
|
|
33542
34924
|
await this.createAction({
|
|
33543
34925
|
description: `Grant ${built.length} permissions`,
|
|
33544
34926
|
outputs: built.map((b) => b.output),
|
|
33545
|
-
options: { acceptDelayedBroadcast:
|
|
34927
|
+
options: { acceptDelayedBroadcast: true }
|
|
33546
34928
|
}, this.adminOriginator);
|
|
33547
34929
|
return built.map((b) => b.request);
|
|
33548
34930
|
}, strict);
|
|
@@ -34939,6 +36321,7 @@ exports.ARGON2ID_MAX_MEMORY_KIB = ARGON2ID_MAX_MEMORY_KIB;
|
|
|
34939
36321
|
exports.ARGON2ID_MAX_PARALLELISM = ARGON2ID_MAX_PARALLELISM;
|
|
34940
36322
|
exports.AuthMethodInteractor = AuthMethodInteractor;
|
|
34941
36323
|
exports.BHServiceClient = BHServiceClient;
|
|
36324
|
+
exports.BRC153_REFERENCE_PREFIX = BRC153_REFERENCE_PREFIX;
|
|
34942
36325
|
exports.BulkFileDataManager = BulkFileDataManager;
|
|
34943
36326
|
exports.BulkFileDataReader = BulkFileDataReader;
|
|
34944
36327
|
exports.BulkFilesReader = BulkFilesReader;
|
|
@@ -34959,6 +36342,11 @@ exports.ChaintracksServiceClient = ChaintracksServiceClient;
|
|
|
34959
36342
|
exports.ChaintracksStorageBase = ChaintracksStorageBase;
|
|
34960
36343
|
exports.ChaintracksStorageIdb = ChaintracksStorageIdb;
|
|
34961
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;
|
|
34962
36350
|
exports.DEFAULT_PROFILE_ID = DEFAULT_PROFILE_ID;
|
|
34963
36351
|
exports.DEFAULT_SETTINGS = DEFAULT_SETTINGS;
|
|
34964
36352
|
exports.DevConsoleInteractor = DevConsoleInteractor;
|
|
@@ -34980,6 +36368,7 @@ exports.EntityUser = EntityUser;
|
|
|
34980
36368
|
exports.GoChaintracksServiceClient = GoChaintracksServiceClient;
|
|
34981
36369
|
exports.HeightRange = HeightRange;
|
|
34982
36370
|
exports.KDF_MAX_HASH_LENGTH = KDF_MAX_HASH_LENGTH;
|
|
36371
|
+
exports.LEGACY_MANAGED_CHANGE_MINIMUM_SATOSHIS = LEGACY_MANAGED_CHANGE_MINIMUM_SATOSHIS;
|
|
34983
36372
|
exports.LiveIngestorBase = LiveIngestorBase;
|
|
34984
36373
|
exports.LiveIngestorChaintracksSSE = LiveIngestorChaintracksSSE;
|
|
34985
36374
|
exports.LiveIngestorWhatsOnChainPoll = LiveIngestorWhatsOnChainPoll;
|
|
@@ -35014,6 +36403,7 @@ exports.WalletSettingsManager = WalletSettingsManager;
|
|
|
35014
36403
|
exports.WalletSigner = WalletSigner;
|
|
35015
36404
|
exports.WalletStorageManager = WalletStorageManager;
|
|
35016
36405
|
exports.WhatsOnChainServices = WhatsOnChainServices;
|
|
36406
|
+
exports.applyBrc153ReferenceLabel = applyBrc153ReferenceLabel;
|
|
35017
36407
|
exports.arcDefaultUrl = arcDefaultUrl;
|
|
35018
36408
|
exports.arcGorillaPoolUrl = arcGorillaPoolUrl;
|
|
35019
36409
|
exports.arcadeDefaultUrl = arcadeDefaultUrl;
|
|
@@ -35039,6 +36429,7 @@ exports.createIdbChaintracks = createIdbChaintracks;
|
|
|
35039
36429
|
exports.createNoDbChaintracks = createNoDbChaintracks;
|
|
35040
36430
|
exports.createSyncMap = createSyncMap;
|
|
35041
36431
|
exports.decryptBRC39 = decryptBRC39;
|
|
36432
|
+
exports.defaultManagedChangePolicy = defaultManagedChangePolicy;
|
|
35042
36433
|
exports.doubleSha256BE = doubleSha256BE;
|
|
35043
36434
|
exports.doubleSha256LE = doubleSha256LE;
|
|
35044
36435
|
exports.encryptBRC39 = encryptBRC39;
|
|
@@ -35053,6 +36444,8 @@ exports.importBRC39 = importBRC39;
|
|
|
35053
36444
|
exports.isAutoSpendableChangeOutput = isAutoSpendableChangeOutput;
|
|
35054
36445
|
exports.isBaseBlockHeader = isBaseBlockHeader;
|
|
35055
36446
|
exports.isBlockHeader = isBlockHeader;
|
|
36447
|
+
exports.isBrc153ReferenceLabel = isBrc153ReferenceLabel;
|
|
36448
|
+
exports.isLegacyManagedChangeBasketDefault = isLegacyManagedChangeBasketDefault;
|
|
35056
36449
|
exports.isLive = isLive;
|
|
35057
36450
|
exports.isLiveBlockHeader = isLiveBlockHeader;
|
|
35058
36451
|
exports.isManagedChangeOutput = isManagedChangeOutput;
|
|
@@ -35061,12 +36454,14 @@ exports.logWalletError = logWalletError;
|
|
|
35061
36454
|
exports.logger = logger;
|
|
35062
36455
|
exports.makeAtomicBeef = makeAtomicBeef;
|
|
35063
36456
|
exports.makeBrc114ActionTimeLabel = makeBrc114ActionTimeLabel;
|
|
36457
|
+
exports.makeBrc153ReferenceLabel = makeBrc153ReferenceLabel;
|
|
35064
36458
|
exports.managedChangeOutputFields = managedChangeOutputFields;
|
|
35065
36459
|
exports.maxDate = maxDate;
|
|
35066
36460
|
exports.optionalArraysEqual = optionalArraysEqual;
|
|
35067
36461
|
exports.outputColumnsWithoutLockingScript = outputColumnsWithoutLockingScript;
|
|
35068
36462
|
exports.parseBRC38Json = parseBRC38Json;
|
|
35069
36463
|
exports.parseBrc114ActionTimeLabels = parseBrc114ActionTimeLabels;
|
|
36464
|
+
exports.parseBrc153ReferenceLabel = parseBrc153ReferenceLabel;
|
|
35070
36465
|
exports.parseFileLink = parseFileLink;
|
|
35071
36466
|
exports.parseTxScriptOffsets = parseTxScriptOffsets;
|
|
35072
36467
|
exports.partitionActionLabels = partitionActionLabels;
|
|
@@ -35092,12 +36487,14 @@ exports.toDefaultChaintracksArguments = toDefaultChaintracksArguments;
|
|
|
35092
36487
|
exports.toLookupNetworkPreset = toLookupNetworkPreset;
|
|
35093
36488
|
exports.toWalletNetwork = toWalletNetwork;
|
|
35094
36489
|
exports.transactionColumnsWithoutRawTx = transactionColumnsWithoutRawTx;
|
|
36490
|
+
exports.upgradeLegacyManagedChangeBasketDefault = upgradeLegacyManagedChangeBasketDefault;
|
|
35095
36491
|
Object.defineProperty(exports, "utils", {
|
|
35096
36492
|
enumerable: true,
|
|
35097
36493
|
get: function() {
|
|
35098
36494
|
return blockHeaderUtilities_exports;
|
|
35099
36495
|
}
|
|
35100
36496
|
});
|
|
36497
|
+
exports.validateManagedChangePolicy = validateManagedChangePolicy;
|
|
35101
36498
|
exports.validateScriptHash = validateScriptHash;
|
|
35102
36499
|
exports.validateSecondsSinceEpoch = validateSecondsSinceEpoch;
|
|
35103
36500
|
exports.validateStorageFeeModel = validateStorageFeeModel;
|