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