@bsv/wallet-toolbox-client 2.6.5 → 2.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/out/index.client.cjs +1854 -406
- package/out/index.client.cjs.map +1 -1
- package/out/index.client.d.cts +202 -14
- package/out/index.client.d.cts.map +1 -1
- package/out/index.client.d.mts +202 -14
- package/out/index.client.d.mts.map +1 -1
- package/out/index.client.mjs +1840 -407
- package/out/index.client.mjs.map +1 -1
- package/package.json +3 -3
package/out/index.client.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,190 @@ 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 quarantineLocalInputs(localInputs, storage, trx) {
|
|
5942
|
+
const staleOutpoints = /* @__PURE__ */ new Set();
|
|
5943
|
+
for (const { output, outpoint } of localInputs) {
|
|
5944
|
+
await storage.updateOutput(verifyId(output.outputId), { spendable: false }, trx);
|
|
5945
|
+
staleOutpoints.add(`${outpoint.txid}.${outpoint.vout}`);
|
|
5946
|
+
}
|
|
5947
|
+
return {
|
|
5948
|
+
checked: localInputs.length,
|
|
5949
|
+
staleConfirmed: localInputs.length,
|
|
5950
|
+
staleOutpoints: [...staleOutpoints]
|
|
5951
|
+
};
|
|
5952
|
+
}
|
|
5953
|
+
async function findLocalInputs(req, storage, trx) {
|
|
5954
|
+
const outpoints = Transaction.fromBinary(req.rawTx).inputs.map((input) => ({
|
|
5955
|
+
txid: input.sourceTXID ?? "",
|
|
5956
|
+
vout: input.sourceOutputIndex ?? 0
|
|
5957
|
+
})).filter((outpoint) => outpoint.txid !== "");
|
|
5958
|
+
if (outpoints.length === 0) return [];
|
|
5959
|
+
const transactions = await storage.findTransactions({
|
|
5960
|
+
partial: { txid: req.txid },
|
|
5961
|
+
noRawTx: true,
|
|
5962
|
+
trx
|
|
5963
|
+
});
|
|
5964
|
+
const userIds = [...new Set(transactions.map((transaction) => transaction.userId))];
|
|
5965
|
+
const localInputs = [];
|
|
5966
|
+
const localKeys = /* @__PURE__ */ new Set();
|
|
5967
|
+
for (const userId of userIds) {
|
|
5968
|
+
const byOutpoint = await storage.findOutputsByOutpoints(userId, outpoints, trx);
|
|
5969
|
+
for (const outpoint of outpoints) {
|
|
5970
|
+
const output = byOutpoint[`${outpoint.txid}.${outpoint.vout}`];
|
|
5971
|
+
const localKey = `${userId}:${outpoint.txid}.${outpoint.vout}`;
|
|
5972
|
+
if (output != null && !localKeys.has(localKey)) {
|
|
5973
|
+
localKeys.add(localKey);
|
|
5974
|
+
localInputs.push({
|
|
5975
|
+
output,
|
|
5976
|
+
outpoint
|
|
5977
|
+
});
|
|
5978
|
+
}
|
|
5979
|
+
}
|
|
5980
|
+
}
|
|
5981
|
+
return localInputs;
|
|
5982
|
+
}
|
|
5983
|
+
/**
|
|
5984
|
+
* Conservatively quarantine every locally-owned input of a transaction after
|
|
5985
|
+
* Arcade supplies explicit missing-input/conflict evidence. This path is
|
|
5986
|
+
* deliberately independent of explorer or UTXO providers: a positive
|
|
5987
|
+
* broadcaster verdict is enough to stop the failed transaction from feeding
|
|
5988
|
+
* the same inputs into another action. A later validated mined proof remains
|
|
5989
|
+
* able to repair the transaction through the ordinary proof recovery path.
|
|
5990
|
+
*/
|
|
5991
|
+
async function quarantineReqInputs(req, storage, trx, logger) {
|
|
5992
|
+
const result = await quarantineLocalInputs(await findLocalInputs(req, storage, trx), storage, trx);
|
|
5993
|
+
if (result.staleConfirmed > 0) logger?.log(`quarantineReqInputs: ${result.staleConfirmed} local input copy/copies quarantined for txid=${req.txid}`);
|
|
5994
|
+
return result;
|
|
5995
|
+
}
|
|
5996
|
+
/**
|
|
5997
|
+
* Keep only inputs created by a locally terminal parent unavailable after the
|
|
5998
|
+
* failed-child transition releases its allocations. Other inputs may still be
|
|
5999
|
+
* valid UTXOs and must remain reusable.
|
|
6000
|
+
*/
|
|
6001
|
+
async function quarantineReqInputsFromFailedParents(req, failedParentTxids, storage, trx, logger) {
|
|
6002
|
+
const failedParents = new Set(failedParentTxids);
|
|
6003
|
+
const result = await quarantineLocalInputs((await findLocalInputs(req, storage, trx)).filter(({ outpoint }) => failedParents.has(outpoint.txid)), storage, trx);
|
|
6004
|
+
if (result.staleConfirmed > 0) logger?.log(`quarantineReqInputsFromFailedParents: ${result.staleConfirmed} local parent output copy/copies quarantined for txid=${req.txid}`);
|
|
6005
|
+
return result;
|
|
6006
|
+
}
|
|
6007
|
+
/**
|
|
6008
|
+
* Ask the configured UTXO-provider collection to classify the request's local
|
|
6009
|
+
* inputs and mark only positively confirmed spent outputs unavailable.
|
|
6010
|
+
* Provider errors and a collection with no providers are inconclusive and
|
|
6011
|
+
* never count as spent.
|
|
6012
|
+
*/
|
|
6013
|
+
async function markConfirmedStaleReqInputs(req, storage, services, trx, logger) {
|
|
6014
|
+
const result = {
|
|
6015
|
+
checked: 0,
|
|
6016
|
+
staleConfirmed: 0,
|
|
6017
|
+
staleOutpoints: []
|
|
6018
|
+
};
|
|
6019
|
+
const localInputs = await findLocalInputs(req, storage, trx);
|
|
6020
|
+
const verdicts = /* @__PURE__ */ new Map();
|
|
6021
|
+
const uniqueInputs = /* @__PURE__ */ new Map();
|
|
6022
|
+
for (const localInput of localInputs) {
|
|
6023
|
+
const key = `${localInput.outpoint.txid}.${localInput.outpoint.vout}`;
|
|
6024
|
+
if (!uniqueInputs.has(key)) uniqueInputs.set(key, localInput);
|
|
6025
|
+
}
|
|
6026
|
+
await mapWithConcurrency([...uniqueInputs.values()], 4, async ({ output, outpoint }) => {
|
|
6027
|
+
const key = `${outpoint.txid}.${outpoint.vout}`;
|
|
6028
|
+
if (output.lockingScript == null) try {
|
|
6029
|
+
await storage.validateOutputScript(output, trx);
|
|
6030
|
+
} catch {
|
|
6031
|
+
verdicts.set(key, "inconclusive");
|
|
6032
|
+
return;
|
|
6033
|
+
}
|
|
6034
|
+
if (output.lockingScript == null) {
|
|
6035
|
+
verdicts.set(key, "inconclusive");
|
|
6036
|
+
return;
|
|
6037
|
+
}
|
|
6038
|
+
const classification = await classifyOutputUtxo(services, output);
|
|
6039
|
+
let verdict;
|
|
6040
|
+
if (classification.verdict === "unknown") verdict = "inconclusive";
|
|
6041
|
+
else if (classification.verdict === "unspent") verdict = "utxo";
|
|
6042
|
+
else verdict = "stale";
|
|
6043
|
+
verdicts.set(key, verdict);
|
|
6044
|
+
});
|
|
6045
|
+
const staleOutpoints = /* @__PURE__ */ new Set();
|
|
6046
|
+
for (const { output, outpoint } of localInputs) {
|
|
6047
|
+
const key = `${outpoint.txid}.${outpoint.vout}`;
|
|
6048
|
+
const verdict = verdicts.get(key);
|
|
6049
|
+
if (verdict == null || verdict === "inconclusive") continue;
|
|
6050
|
+
result.checked++;
|
|
6051
|
+
if (verdict === "stale") {
|
|
6052
|
+
await storage.updateOutput(verifyId(output.outputId), { spendable: false }, trx);
|
|
6053
|
+
result.staleConfirmed++;
|
|
6054
|
+
staleOutpoints.add(key);
|
|
6055
|
+
}
|
|
6056
|
+
}
|
|
6057
|
+
result.staleOutpoints = [...staleOutpoints];
|
|
6058
|
+
if (result.staleConfirmed > 0) logger?.log(`markConfirmedStaleReqInputs: ${result.staleConfirmed} of ${result.checked} local input copy/copies confirmed spent for txid=${req.txid}`);
|
|
6059
|
+
return result;
|
|
6060
|
+
}
|
|
6061
|
+
//#endregion
|
|
5731
6062
|
//#region ../src/storage/methods/attemptToPostReqsToNetwork.ts
|
|
5732
6063
|
/**
|
|
5733
6064
|
* Attempt to post one or more `ProvenTxReq` with status 'unsent'
|
|
@@ -6067,64 +6398,7 @@ async function confirmDoubleSpend(ar, beef, storage, services, logger) {
|
|
|
6067
6398
|
* that were actually evicted (added to history note for diagnostics).
|
|
6068
6399
|
*/
|
|
6069
6400
|
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;
|
|
6401
|
+
return await markConfirmedStaleReqInputs(ar.vreq.req, storage, services, trx, logger);
|
|
6128
6402
|
}
|
|
6129
6403
|
//#endregion
|
|
6130
6404
|
//#region ../src/storage/methods/listCertificates.ts
|
|
@@ -6277,6 +6551,75 @@ function removeDustOutputs(changeOutputs, dustFloor) {
|
|
|
6277
6551
|
largest.satoshis += removed.satoshis;
|
|
6278
6552
|
}
|
|
6279
6553
|
}
|
|
6554
|
+
async function migrateLegacyChangeInputs(request) {
|
|
6555
|
+
const { params, result } = request;
|
|
6556
|
+
if (!params.surplusPoolShaping || result.changeOutputs.length === 0) return;
|
|
6557
|
+
if (request.targetNetCount <= request.netChangeCount()) return;
|
|
6558
|
+
const migrationLimit = params.maxMigrationInputs === -1 ? Number.MAX_SAFE_INTEGER : params.maxMigrationInputs ?? 0;
|
|
6559
|
+
for (let migrated = 0; migrated < migrationLimit; migrated++) {
|
|
6560
|
+
const marginalInputFee = request.feeTarget(1) - request.feeTarget();
|
|
6561
|
+
const candidate = await request.allocateChangeInput(0);
|
|
6562
|
+
if (candidate == null) break;
|
|
6563
|
+
if (candidate.satoshis >= params.changeInitialSatoshis || candidate.satoshis <= marginalInputFee) {
|
|
6564
|
+
await request.releaseChangeInput(candidate.outputId);
|
|
6565
|
+
break;
|
|
6566
|
+
}
|
|
6567
|
+
request.recordAllocatedInput(candidate);
|
|
6568
|
+
}
|
|
6569
|
+
}
|
|
6570
|
+
/**
|
|
6571
|
+
* Materialize the first managed-change output from surplus that is already in
|
|
6572
|
+
* the transaction. This is especially important for explicit/fixed inputs:
|
|
6573
|
+
* their value can fully fund an action before the allocator loop runs, but the
|
|
6574
|
+
* shaping policy must still capture the remainder without gathering another
|
|
6575
|
+
* wallet-managed input.
|
|
6576
|
+
*/
|
|
6577
|
+
function materializeSurplusChangeOutput(request) {
|
|
6578
|
+
const { params, result } = request;
|
|
6579
|
+
if (!params.surplusPoolShaping || result.changeOutputs.length > 0) return;
|
|
6580
|
+
const availableAfterOutputFee = request.feeExcess(0, 1);
|
|
6581
|
+
if (availableAfterOutputFee < request.dustFloor) return;
|
|
6582
|
+
result.changeOutputs.push({
|
|
6583
|
+
satoshis: Math.min(availableAfterOutputFee, Math.max(request.dustFloor, params.changeFirstSatoshis)),
|
|
6584
|
+
lockingScriptLength: params.changeLockingScriptLength
|
|
6585
|
+
});
|
|
6586
|
+
request.feeExcess();
|
|
6587
|
+
}
|
|
6588
|
+
/**
|
|
6589
|
+
* Preserve the historical compatibility retry when another input can make a
|
|
6590
|
+
* viable change output, except when surplus-only shaping has nothing economic
|
|
6591
|
+
* to return. In that case the bounded remainder stays in the miner fee instead
|
|
6592
|
+
* of manufacturing change from an additional wallet input.
|
|
6593
|
+
*/
|
|
6594
|
+
async function requireViableChangeOrRetainBoundedFee(request) {
|
|
6595
|
+
const { params, result } = request;
|
|
6596
|
+
const feeExcessNow = request.feeExcess();
|
|
6597
|
+
if (result.changeOutputs.length > 0 || feeExcessNow <= 0) return;
|
|
6598
|
+
if (params.surplusPoolShaping === true && request.feeExcess(0, 1) < request.dustFloor) return;
|
|
6599
|
+
const minimumChange = Math.max(request.dustFloor, params.changeFirstSatoshis);
|
|
6600
|
+
const totalSatoshisNeeded = request.spending() + request.feeTarget(0, 1) + minimumChange;
|
|
6601
|
+
const moreSatoshisNeeded = Math.max(1, totalSatoshisNeeded - request.funding());
|
|
6602
|
+
await request.releaseAllocatedChangeInputs();
|
|
6603
|
+
throw new WERR_INSUFFICIENT_FUNDS(totalSatoshisNeeded, moreSatoshisNeeded);
|
|
6604
|
+
}
|
|
6605
|
+
function shapeSurplusChangeOutputs(request) {
|
|
6606
|
+
const { params, result } = request;
|
|
6607
|
+
if (!params.surplusPoolShaping || result.changeOutputs.length !== 1) return;
|
|
6608
|
+
if (request.targetNetCount <= request.netChangeCount()) return;
|
|
6609
|
+
const originalSatoshis = result.changeOutputs[0].satoshis;
|
|
6610
|
+
const desiredOutputs = Math.min(request.maxChangeOutputs, Math.max(1, request.targetNetCount + result.allocatedChangeInputs.length));
|
|
6611
|
+
for (let count = desiredOutputs; count > 1; count--) {
|
|
6612
|
+
const addedOutputs = count - 1;
|
|
6613
|
+
const distributable = originalSatoshis - (request.feeTarget(0, addedOutputs) - request.feeTarget());
|
|
6614
|
+
if (distributable < count * params.changeInitialSatoshis) continue;
|
|
6615
|
+
result.changeOutputs = Array.from({ length: count }, () => ({
|
|
6616
|
+
satoshis: params.changeInitialSatoshis,
|
|
6617
|
+
lockingScriptLength: params.changeLockingScriptLength
|
|
6618
|
+
}));
|
|
6619
|
+
distributeExcessFees(result.changeOutputs, params.changeInitialSatoshis, distributable - count * params.changeInitialSatoshis, request.rand);
|
|
6620
|
+
break;
|
|
6621
|
+
}
|
|
6622
|
+
}
|
|
6280
6623
|
/**
|
|
6281
6624
|
* Simplifications:
|
|
6282
6625
|
* - only support one change type with fixed length scripts.
|
|
@@ -6348,7 +6691,8 @@ async function generateChangeSdkCore(params, allocateChangeInput, releaseChangeI
|
|
|
6348
6691
|
* Applies the per-transaction limit so that the UTXO pool grows
|
|
6349
6692
|
* gradually rather than all at once.
|
|
6350
6693
|
*/
|
|
6351
|
-
const maxChangeOutputs = params.maxChangeOutputs ?? 8;
|
|
6694
|
+
const maxChangeOutputs = params.maxChangeOutputs === -1 ? Number.MAX_SAFE_INTEGER : params.maxChangeOutputs ?? 8;
|
|
6695
|
+
const surplusPoolShaping = params.surplusPoolShaping === true;
|
|
6352
6696
|
const randomVals = [...params.randomVals || []];
|
|
6353
6697
|
const nextRandomVal = () => {
|
|
6354
6698
|
let val = 0;
|
|
@@ -6431,6 +6775,7 @@ async function generateChangeSdkCore(params, allocateChangeInput, releaseChangeI
|
|
|
6431
6775
|
return r.changeOutputs.length - r.allocatedChangeInputs.length;
|
|
6432
6776
|
};
|
|
6433
6777
|
const addOutputToBalanceNewInput = () => {
|
|
6778
|
+
if (surplusPoolShaping) return false;
|
|
6434
6779
|
if (!hasTargetNetCount) return false;
|
|
6435
6780
|
if (r.changeOutputs.length >= maxChangeOutputs) return false;
|
|
6436
6781
|
return netChangeCount() - 1 < targetNetCount;
|
|
@@ -6446,6 +6791,7 @@ async function generateChangeSdkCore(params, allocateChangeInput, releaseChangeI
|
|
|
6446
6791
|
feeExcessNow = feeExcess();
|
|
6447
6792
|
};
|
|
6448
6793
|
const addDesiredChangeOutputs = () => {
|
|
6794
|
+
if (surplusPoolShaping) return;
|
|
6449
6795
|
while (r.changeOutputs.length < maxChangeOutputs && (hasTargetNetCount && targetNetCount > netChangeCount() || r.changeOutputs.length === 0 && feeExcess() > 0)) {
|
|
6450
6796
|
const satoshis = r.changeOutputs.length === 0 ? Math.max(dustFloor, params.changeFirstSatoshis) : Math.max(dustFloor, params.changeInitialSatoshis);
|
|
6451
6797
|
r.changeOutputs.push({
|
|
@@ -6461,7 +6807,8 @@ async function generateChangeSdkCore(params, allocateChangeInput, releaseChangeI
|
|
|
6461
6807
|
if (removingOutputs || feeExcess() <= 0) return;
|
|
6462
6808
|
if (!((ao === 1 || r.changeOutputs.length === 0) && r.changeOutputs.length < maxChangeOutputs)) return;
|
|
6463
6809
|
const cap = r.changeOutputs.length === 0 ? params.changeFirstSatoshis : params.changeInitialSatoshis;
|
|
6464
|
-
const
|
|
6810
|
+
const outputFunding = surplusPoolShaping ? feeExcess(0, 1) : feeExcess();
|
|
6811
|
+
const satoshis = Math.min(outputFunding, Math.max(dustFloor, cap));
|
|
6465
6812
|
if (satoshis >= dustFloor) r.changeOutputs.push({
|
|
6466
6813
|
satoshis,
|
|
6467
6814
|
lockingScriptLength: params.changeLockingScriptLength
|
|
@@ -6506,6 +6853,18 @@ async function generateChangeSdkCore(params, allocateChangeInput, releaseChangeI
|
|
|
6506
6853
|
};
|
|
6507
6854
|
}
|
|
6508
6855
|
/**
|
|
6856
|
+
* The action may already be funded entirely by explicit/fixed inputs. In
|
|
6857
|
+
* that case the allocator loop never runs, so capture the existing surplus
|
|
6858
|
+
* here before the no-change compatibility guard. This operation cannot
|
|
6859
|
+
* allocate an input; bounded legacy migration remains a separate step.
|
|
6860
|
+
*/
|
|
6861
|
+
materializeSurplusChangeOutput({
|
|
6862
|
+
params,
|
|
6863
|
+
result: r,
|
|
6864
|
+
dustFloor,
|
|
6865
|
+
feeExcess
|
|
6866
|
+
});
|
|
6867
|
+
/**
|
|
6509
6868
|
* Trigger an account funding event if we don't have enough to cover this transaction.
|
|
6510
6869
|
*/
|
|
6511
6870
|
if (feeExcess() < 0) {
|
|
@@ -6516,19 +6875,63 @@ async function generateChangeSdkCore(params, allocateChangeInput, releaseChangeI
|
|
|
6516
6875
|
}
|
|
6517
6876
|
/**
|
|
6518
6877
|
* If needed, seek funding to avoid overspending on fees without a change output to recapture it.
|
|
6878
|
+
* An economically unreturnable shaping remainder stays in the miner fee;
|
|
6879
|
+
* gathering another input solely to manufacture change would violate
|
|
6880
|
+
* surplus-only shaping and make the action less efficient.
|
|
6519
6881
|
*/
|
|
6520
|
-
|
|
6521
|
-
|
|
6522
|
-
|
|
6523
|
-
|
|
6524
|
-
|
|
6525
|
-
|
|
6526
|
-
|
|
6882
|
+
await requireViableChangeOrRetainBoundedFee({
|
|
6883
|
+
params,
|
|
6884
|
+
result: r,
|
|
6885
|
+
dustFloor,
|
|
6886
|
+
feeExcess,
|
|
6887
|
+
feeTarget,
|
|
6888
|
+
funding,
|
|
6889
|
+
spending,
|
|
6890
|
+
releaseAllocatedChangeInputs
|
|
6891
|
+
});
|
|
6892
|
+
/**
|
|
6893
|
+
* Progressively retire economically useful legacy fragments without ever
|
|
6894
|
+
* making them necessary for the requested action. The target of zero asks
|
|
6895
|
+
* canonical allocators for their smallest remaining output. A candidate at
|
|
6896
|
+
* or above the preferred value is not legacy migration material and is
|
|
6897
|
+
* immediately released.
|
|
6898
|
+
*/
|
|
6899
|
+
await migrateLegacyChangeInputs({
|
|
6900
|
+
params,
|
|
6901
|
+
result: r,
|
|
6902
|
+
targetNetCount,
|
|
6903
|
+
netChangeCount,
|
|
6904
|
+
feeTarget,
|
|
6905
|
+
allocateChangeInput,
|
|
6906
|
+
releaseChangeInput,
|
|
6907
|
+
recordAllocatedInput: (candidate) => {
|
|
6908
|
+
r.allocatedChangeInputs.push(candidate);
|
|
6909
|
+
allocatedFunding += candidate.satoshis;
|
|
6910
|
+
feeExcessNow = feeExcess();
|
|
6911
|
+
}
|
|
6912
|
+
});
|
|
6527
6913
|
/**
|
|
6528
6914
|
* Distribute the excess fees across the changeOutputs added.
|
|
6529
6915
|
*/
|
|
6530
6916
|
feeExcessNow = distributeExcessFees(r.changeOutputs, params.changeInitialSatoshis, feeExcessNow, rand);
|
|
6531
6917
|
/**
|
|
6918
|
+
* Pool growth is funded only from the surplus already present in the
|
|
6919
|
+
* transaction. Splitting one change output increases the serialized fee;
|
|
6920
|
+
* that exact delta is deducted before assigning the new outputs. If the
|
|
6921
|
+
* preferred minimum cannot be met, the transaction retains one smaller
|
|
6922
|
+
* output instead of gathering more inputs or refusing an otherwise valid
|
|
6923
|
+
* action.
|
|
6924
|
+
*/
|
|
6925
|
+
shapeSurplusChangeOutputs({
|
|
6926
|
+
params,
|
|
6927
|
+
result: r,
|
|
6928
|
+
targetNetCount,
|
|
6929
|
+
netChangeCount,
|
|
6930
|
+
maxChangeOutputs,
|
|
6931
|
+
feeTarget,
|
|
6932
|
+
rand
|
|
6933
|
+
});
|
|
6934
|
+
/**
|
|
6532
6935
|
* Remove any change outputs that ended up below the dust floor after distribution.
|
|
6533
6936
|
* Consolidates removed satoshis into the largest remaining output.
|
|
6534
6937
|
*/
|
|
@@ -6559,7 +6962,11 @@ function validateGenerateChangeSdkResult(params, r) {
|
|
|
6559
6962
|
ok = false;
|
|
6560
6963
|
}
|
|
6561
6964
|
const feeRequired = Math.ceil((r.size || 0) / 1e3 * (r.satsPerKb || 0));
|
|
6562
|
-
|
|
6965
|
+
const minSpendTxSize = transactionSize([params.changeUnlockingScriptLength], [params.changeLockingScriptLength]);
|
|
6966
|
+
const dustFloor = Math.max(1, Math.ceil(minSpendTxSize / 1e3 * (r.satsPerKb || 0)) * 2);
|
|
6967
|
+
const feeWithChangeOutput = Math.ceil(((r.size || 0) + transactionOutputSize(params.changeLockingScriptLength)) / 1e3 * (r.satsPerKb || 0));
|
|
6968
|
+
const isBoundedUnreturnableShapingSurplus = params.surplusPoolShaping === true && r.changeOutputs.length === 0 && r.fee > feeRequired && r.fee - feeWithChangeOutput < dustFloor;
|
|
6969
|
+
if (feeRequired !== r.fee && !isBoundedUnreturnableShapingSurplus) {
|
|
6563
6970
|
log += `required fee error ${feeRequired} !== ${r.fee};`;
|
|
6564
6971
|
ok = false;
|
|
6565
6972
|
}
|
|
@@ -6597,6 +7004,8 @@ function validateGenerateChangeSdkParams(params) {
|
|
|
6597
7004
|
params.feeModel = validateStorageFeeModel(params.feeModel);
|
|
6598
7005
|
if (params.feeModel.model !== "sat/kb") throw new WERR_INVALID_PARAMETER("feeModel.model", "'sat/kb'");
|
|
6599
7006
|
Validation.validateOptionalInteger(params.targetNetCount, "targetNetCount");
|
|
7007
|
+
if (params.maxChangeOutputs !== -1) Validation.validateOptionalInteger(params.maxChangeOutputs, "maxChangeOutputs", 1);
|
|
7008
|
+
if (params.maxMigrationInputs !== -1) Validation.validateOptionalInteger(params.maxMigrationInputs, "maxMigrationInputs", 0);
|
|
6600
7009
|
Validation.validateSatoshis(params.changeFirstSatoshis, "changeFirstSatoshis", 1);
|
|
6601
7010
|
Validation.validateSatoshis(params.changeInitialSatoshis, "changeInitialSatoshis", 1);
|
|
6602
7011
|
Validation.validateInteger(params.changeLockingScriptLength, "changeLockingScriptLength");
|
|
@@ -8064,6 +8473,8 @@ async function planFunding(state, args, explicit, noSendChange) {
|
|
|
8064
8473
|
const allocated = /* @__PURE__ */ new Map();
|
|
8065
8474
|
const noSend = [...noSendChange];
|
|
8066
8475
|
const changeBasket = state.begin.changeBasket;
|
|
8476
|
+
const policy = validateManagedChangePolicy(state.begin.managedChangePolicy);
|
|
8477
|
+
const preferredSatoshis = Math.max(1, changeBasket.minimumDesiredUTXOValue);
|
|
8067
8478
|
const params = {
|
|
8068
8479
|
fixedInputs: explicit.map((output, index) => ({
|
|
8069
8480
|
satoshis: output.satoshis,
|
|
@@ -8077,11 +8488,14 @@ async function planFunding(state, args, explicit, noSendChange) {
|
|
|
8077
8488
|
lockingScriptLength: 25
|
|
8078
8489
|
}] : []],
|
|
8079
8490
|
feeModel: state.begin.feeModel,
|
|
8080
|
-
changeInitialSatoshis:
|
|
8081
|
-
changeFirstSatoshis:
|
|
8491
|
+
changeInitialSatoshis: preferredSatoshis,
|
|
8492
|
+
changeFirstSatoshis: preferredSatoshis,
|
|
8082
8493
|
changeLockingScriptLength: 25,
|
|
8083
8494
|
changeUnlockingScriptLength: 107,
|
|
8084
8495
|
targetNetCount: changeBasket.numberOfDesiredUTXOs - state.estimatedChangeCount,
|
|
8496
|
+
maxChangeOutputs: policy.maxOutputsPerAction,
|
|
8497
|
+
surplusPoolShaping: true,
|
|
8498
|
+
maxMigrationInputs: policy.migrationInputsPerAction,
|
|
8085
8499
|
randomVals: args.randomVals
|
|
8086
8500
|
};
|
|
8087
8501
|
const allocate = async (targetSatoshis, exactSatoshis) => {
|
|
@@ -8100,7 +8514,18 @@ async function planFunding(state, args, explicit, noSendChange) {
|
|
|
8100
8514
|
allocated.delete(outputId);
|
|
8101
8515
|
if (noSendChange.includes(output)) noSend.push(output);
|
|
8102
8516
|
};
|
|
8103
|
-
|
|
8517
|
+
let result;
|
|
8518
|
+
try {
|
|
8519
|
+
result = await generateChangeSdk(params, allocate, release, args.logger);
|
|
8520
|
+
} catch (error) {
|
|
8521
|
+
if (!(error instanceof WERR_INSUFFICIENT_FUNDS)) throw error;
|
|
8522
|
+
result = await generateChangeSdk({
|
|
8523
|
+
...params,
|
|
8524
|
+
changeFirstSatoshis: 1,
|
|
8525
|
+
surplusPoolShaping: false,
|
|
8526
|
+
maxMigrationInputs: 0
|
|
8527
|
+
}, allocate, release, args.logger);
|
|
8528
|
+
}
|
|
8104
8529
|
return {
|
|
8105
8530
|
allocated: result.allocatedChangeInputs.map((input) => verifyTruthy(allocated.get(input.outputId))),
|
|
8106
8531
|
changeSatoshis: result.changeOutputs.map((output) => output.satoshis),
|
|
@@ -8358,6 +8783,19 @@ async function compressActionBatchPackItems(items, encoding, maxBytes, maxItems)
|
|
|
8358
8783
|
function mergeUnique(values) {
|
|
8359
8784
|
return [...new Set(values)];
|
|
8360
8785
|
}
|
|
8786
|
+
function isActionBatchStateError(error) {
|
|
8787
|
+
if (error instanceof WERRActionBatchState) return true;
|
|
8788
|
+
if (error == null || typeof error !== "object") return false;
|
|
8789
|
+
const candidate = error;
|
|
8790
|
+
return (candidate.name === "WERR_ACTION_BATCH_STATE" || candidate.code === "WERR_ACTION_BATCH_STATE") && typeof candidate.state === "string";
|
|
8791
|
+
}
|
|
8792
|
+
function isActionBatchCapacityError(error) {
|
|
8793
|
+
if (error == null || typeof error !== "object") return false;
|
|
8794
|
+
const candidate = error;
|
|
8795
|
+
const code = candidate.name === "Error" ? candidate.code : candidate.name ?? candidate.code;
|
|
8796
|
+
if (code === "WERR_INVALID_PARAMETER" && candidate.parameter === "firstAction") return true;
|
|
8797
|
+
return code === "WERR_INVALID_OPERATION" && typeof candidate.message === "string" && candidate.message.includes("action batch") && (candidate.message.includes("maximum") || candidate.message.includes("reserve"));
|
|
8798
|
+
}
|
|
8361
8799
|
function nextGeometricTarget(value) {
|
|
8362
8800
|
return Math.min(Number.MAX_SAFE_INTEGER, value * 2);
|
|
8363
8801
|
}
|
|
@@ -8404,6 +8842,7 @@ var ActionBatchWorkspace = class {
|
|
|
8404
8842
|
planned = /* @__PURE__ */ new Map();
|
|
8405
8843
|
actions = [];
|
|
8406
8844
|
lockingScripts = /* @__PURE__ */ new Map();
|
|
8845
|
+
canResume;
|
|
8407
8846
|
ewmaConfirmedInputs = 1;
|
|
8408
8847
|
ewmaConfirmedSatoshis = 0;
|
|
8409
8848
|
hasConfirmedSample = false;
|
|
@@ -8423,6 +8862,7 @@ var ActionBatchWorkspace = class {
|
|
|
8423
8862
|
this.batchId = begin.batchId;
|
|
8424
8863
|
this.state = makePlannerState(begin, firstAction);
|
|
8425
8864
|
this.expiresAt = Date.parse(begin.expiresAt);
|
|
8865
|
+
this.canResume = capabilities.resume === true && wallet.storage.resumeActionBatch != null;
|
|
8426
8866
|
this.eagerUploadLanes = Array.from({ length: capabilities.packedUploads == null ? 1 : Math.max(1, capabilities.maxConcurrentUploads) }, async () => {});
|
|
8427
8867
|
}
|
|
8428
8868
|
get usesCompactManifest() {
|
|
@@ -8436,6 +8876,15 @@ var ActionBatchWorkspace = class {
|
|
|
8436
8876
|
if (this.actions.some((action) => action.reference === reference || action.txid === reference)) return true;
|
|
8437
8877
|
return Object.entries(this.wallet.pendingSignActions).some(([pendingReference, pending]) => this.planned.has(pendingReference) && pending.tx.id("hex") === reference);
|
|
8438
8878
|
}
|
|
8879
|
+
ownsTransaction(txid) {
|
|
8880
|
+
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);
|
|
8881
|
+
}
|
|
8882
|
+
references(args) {
|
|
8883
|
+
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));
|
|
8884
|
+
}
|
|
8885
|
+
referencesSendWith(txids) {
|
|
8886
|
+
return txids.some((txid) => this.ownsTransaction(txid));
|
|
8887
|
+
}
|
|
8439
8888
|
get isEmpty() {
|
|
8440
8889
|
return this.planned.size === 0 && this.actions.length === 0;
|
|
8441
8890
|
}
|
|
@@ -8560,12 +9009,32 @@ var ActionBatchWorkspace = class {
|
|
|
8560
9009
|
satoshis: extension.reservedOutputs.reduce((sum, output) => sum + output.satoshis, 0)
|
|
8561
9010
|
};
|
|
8562
9011
|
}
|
|
9012
|
+
async resume() {
|
|
9013
|
+
const outpoints = [...new Map([...this.state.reserved.values(), ...this.state.explicit.values()].filter((output) => output.txid != null).map((output) => [`${output.txid}.${output.vout}`, {
|
|
9014
|
+
txid: output.txid,
|
|
9015
|
+
vout: output.vout
|
|
9016
|
+
}])).values()];
|
|
9017
|
+
const result = await this.wallet.storage.resumeActionBatch({
|
|
9018
|
+
batchId: this.batchId,
|
|
9019
|
+
outpoints
|
|
9020
|
+
});
|
|
9021
|
+
this.expiresAt = Date.parse(result.expiresAt);
|
|
9022
|
+
}
|
|
8563
9023
|
async renewIfNeeded() {
|
|
8564
|
-
if (Date.now() >= this.expiresAt)
|
|
9024
|
+
if (Date.now() >= this.expiresAt) {
|
|
9025
|
+
if (this.canResume) await this.resume();
|
|
9026
|
+
return;
|
|
9027
|
+
}
|
|
8565
9028
|
const renewAt = this.expiresAt - this.capabilities.leaseMs * .2;
|
|
8566
9029
|
if (Date.now() < renewAt) return;
|
|
8567
9030
|
this.renewal ??= this.wallet.storage.renewActionBatch(this.batchId).then((result) => {
|
|
8568
9031
|
this.expiresAt = Date.parse(result.expiresAt);
|
|
9032
|
+
}).catch(async (error) => {
|
|
9033
|
+
if (isActionBatchStateError(error) && error.state === "expired") {
|
|
9034
|
+
if (this.canResume) await this.resume();
|
|
9035
|
+
return;
|
|
9036
|
+
}
|
|
9037
|
+
throw error;
|
|
8569
9038
|
}).finally(() => {
|
|
8570
9039
|
this.renewal = void 0;
|
|
8571
9040
|
});
|
|
@@ -8996,24 +9465,42 @@ var ActionBatchController = class {
|
|
|
8996
9465
|
this.workspace = new ActionBatchWorkspace(this.wallet, begin, args, capabilities);
|
|
8997
9466
|
return this.workspace;
|
|
8998
9467
|
}
|
|
9468
|
+
async retire(workspace) {
|
|
9469
|
+
await workspace.abort().catch(() => void 0);
|
|
9470
|
+
if (this.workspace === workspace) this.workspace = void 0;
|
|
9471
|
+
}
|
|
9472
|
+
async recover(workspace, input, isDelayed = false, resume = false) {
|
|
9473
|
+
try {
|
|
9474
|
+
if (resume) await workspace.resume();
|
|
9475
|
+
return Array.isArray(input) ? await workspace.commit(input, isDelayed) : await workspace.plan(input);
|
|
9476
|
+
} catch (error) {
|
|
9477
|
+
if (!resume && isActionBatchStateError(error) && error.state === "expired" && workspace.canResume) return await this.recover(workspace, input, isDelayed, true);
|
|
9478
|
+
if (isActionBatchStateError(error) && error.state !== "expired") await this.retire(workspace);
|
|
9479
|
+
throw error;
|
|
9480
|
+
}
|
|
9481
|
+
}
|
|
8999
9482
|
async plan(args) {
|
|
9000
9483
|
return await this.runExclusive(async () => {
|
|
9001
9484
|
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;
|
|
9485
|
+
const workspace = this.workspace;
|
|
9486
|
+
if (workspace != null) {
|
|
9487
|
+
if (!workspace.references(args)) return void 0;
|
|
9488
|
+
return await this.recover(workspace, args);
|
|
9009
9489
|
}
|
|
9490
|
+
if (!args.isNoSend) return void 0;
|
|
9491
|
+
let begun;
|
|
9010
9492
|
try {
|
|
9011
|
-
|
|
9493
|
+
begun = await this.begin(args);
|
|
9012
9494
|
} catch (error) {
|
|
9013
|
-
if (
|
|
9014
|
-
|
|
9015
|
-
|
|
9016
|
-
|
|
9495
|
+
if (isActionBatchCapacityError(error)) return void 0;
|
|
9496
|
+
throw error;
|
|
9497
|
+
}
|
|
9498
|
+
if (begun == null) return void 0;
|
|
9499
|
+
try {
|
|
9500
|
+
return await begun.plan(args);
|
|
9501
|
+
} catch (error) {
|
|
9502
|
+
await this.retire(begun);
|
|
9503
|
+
if (isActionBatchCapacityError(error)) return void 0;
|
|
9017
9504
|
throw error;
|
|
9018
9505
|
}
|
|
9019
9506
|
});
|
|
@@ -9023,11 +9510,21 @@ var ActionBatchController = class {
|
|
|
9023
9510
|
const workspace = this.workspace;
|
|
9024
9511
|
if (workspace == null) return void 0;
|
|
9025
9512
|
if (prior != null && !workspace.ownsReference(prior.reference)) return void 0;
|
|
9513
|
+
if (prior == null && !workspace.referencesSendWith(args.options.sendWith)) return void 0;
|
|
9026
9514
|
if (prior != null) workspace.stage(prior, args);
|
|
9027
|
-
|
|
9515
|
+
const referencesWorkspace = workspace.referencesSendWith(args.options.sendWith);
|
|
9516
|
+
if (prior != null && args.isNoSend && args.isSendWith && !referencesWorkspace) return await this.wallet.storage.processAction({
|
|
9517
|
+
isNewTx: false,
|
|
9518
|
+
isSendWith: true,
|
|
9519
|
+
isNoSend: false,
|
|
9520
|
+
isDelayed: args.isDelayed,
|
|
9521
|
+
sendWith: [...args.options.sendWith],
|
|
9522
|
+
logger: args.logger
|
|
9523
|
+
});
|
|
9524
|
+
if (!(referencesWorkspace || prior != null && !args.isNoSend)) return { sendWithResults: [] };
|
|
9028
9525
|
const sendWith = [...args.options.sendWith];
|
|
9029
9526
|
if (prior != null && !args.isNoSend && !sendWith.includes(prior.tx.id("hex"))) sendWith.push(prior.tx.id("hex"));
|
|
9030
|
-
const result = await
|
|
9527
|
+
const result = await this.recover(workspace, sendWith, args.isDelayed);
|
|
9031
9528
|
this.workspace = void 0;
|
|
9032
9529
|
return result;
|
|
9033
9530
|
});
|
|
@@ -9771,14 +10268,15 @@ var Wallet = class {
|
|
|
9771
10268
|
}
|
|
9772
10269
|
/**
|
|
9773
10270
|
* Uses `listOutputs` special operation to review the spendability via `Services` of
|
|
9774
|
-
* outputs currently considered spendable. Returns
|
|
10271
|
+
* outputs currently considered spendable. Returns only outputs conclusively
|
|
10272
|
+
* confirmed spent. Rejects the review if any provider result is inconclusive.
|
|
9775
10273
|
*
|
|
9776
10274
|
* Ignores the `limit` and `offset` properties.
|
|
9777
10275
|
*
|
|
9778
10276
|
* @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
|
|
10277
|
+
* @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
10278
|
* @param optionalArgs Optional. Additional tags will constrain the outputs processed.
|
|
9781
|
-
* @returns outputs
|
|
10279
|
+
* @returns outputs previously considered spendable but conclusively confirmed spent.
|
|
9782
10280
|
*/
|
|
9783
10281
|
async reviewSpendableOutputs(all = false, release = false, optionalArgs) {
|
|
9784
10282
|
const args = {
|
|
@@ -9986,7 +10484,7 @@ async function createActionCore(storage, auth, vargs, parent) {
|
|
|
9986
10484
|
});
|
|
9987
10485
|
const feeModel = validateStorageFeeModel(storage.feeModel);
|
|
9988
10486
|
logger?.log(`validated fee model ${JSON.stringify(feeModel)}`);
|
|
9989
|
-
const initialFundingPlan = await
|
|
10487
|
+
const initialFundingPlan = await prepareFundingPlanWithLiquidityPolicy(storage, [
|
|
9990
10488
|
userId,
|
|
9991
10489
|
vargs,
|
|
9992
10490
|
xinputs,
|
|
@@ -10574,7 +11072,9 @@ async function traceStorageStep(storage, name, parent, attributes, callback) {
|
|
|
10574
11072
|
attributes
|
|
10575
11073
|
}, async (span) => await callback(span));
|
|
10576
11074
|
}
|
|
10577
|
-
function makeFundingParams(
|
|
11075
|
+
function makeFundingParams(args) {
|
|
11076
|
+
const { storage, vargs, xinputs, xoutputs, changeBasket, feeModel, healthyChangeCount, compatibilityFallback } = args;
|
|
11077
|
+
const preferredSatoshis = Math.max(1, changeBasket.minimumDesiredUTXOValue);
|
|
10578
11078
|
return {
|
|
10579
11079
|
fixedInputs: xinputs.map((input) => ({
|
|
10580
11080
|
satoshis: input.satoshis,
|
|
@@ -10585,35 +11085,52 @@ function makeFundingParams(vargs, xinputs, xoutputs, changeBasket, feeModel, ava
|
|
|
10585
11085
|
lockingScriptLength: output.lockingScript.length / 2
|
|
10586
11086
|
})),
|
|
10587
11087
|
feeModel,
|
|
10588
|
-
changeInitialSatoshis:
|
|
10589
|
-
changeFirstSatoshis:
|
|
11088
|
+
changeInitialSatoshis: preferredSatoshis,
|
|
11089
|
+
changeFirstSatoshis: compatibilityFallback ? 1 : preferredSatoshis,
|
|
10590
11090
|
changeLockingScriptLength: 25,
|
|
10591
11091
|
changeUnlockingScriptLength: 107,
|
|
10592
|
-
targetNetCount: changeBasket.numberOfDesiredUTXOs -
|
|
11092
|
+
targetNetCount: changeBasket.numberOfDesiredUTXOs - healthyChangeCount,
|
|
11093
|
+
maxChangeOutputs: storage.managedChangePolicy.maxOutputsPerAction,
|
|
11094
|
+
surplusPoolShaping: !compatibilityFallback,
|
|
11095
|
+
maxMigrationInputs: compatibilityFallback ? 0 : storage.managedChangePolicy.migrationInputsPerAction,
|
|
10593
11096
|
randomVals: vargs.randomVals
|
|
10594
11097
|
};
|
|
10595
11098
|
}
|
|
10596
|
-
async function
|
|
10597
|
-
const [userId, vargs, xinputs, xoutputs, changeBasket, noSendChangeIn, feeModel
|
|
10598
|
-
const candidates = await traceStorageStep(storage, "wallet.storage.create_action.funding_candidates", parent, { "funding.exclude_sending": excludeSending }, async (span) => {
|
|
10599
|
-
const outputs = await storage.findAvailableManagedChangeInputCandidates(userId, changeBasket.basketId, excludeSending, trx);
|
|
10600
|
-
span?.end({ attributes: {
|
|
10601
|
-
"funding.candidate_count": outputs.length,
|
|
10602
|
-
"funding.candidate_satoshis": outputs.reduce((sum, output) => sum + output.satoshis, 0)
|
|
10603
|
-
} });
|
|
10604
|
-
return outputs;
|
|
10605
|
-
});
|
|
11099
|
+
async function buildFundingPlan(storage, context, candidates, eligibleStatuses, policyTier, compatibilityFallback, parent) {
|
|
11100
|
+
const [userId, vargs, xinputs, xoutputs, changeBasket, noSendChangeIn, feeModel] = context;
|
|
10606
11101
|
const noSendIds = new Set(noSendChangeIn.map((output) => output.outputId));
|
|
10607
|
-
const available = candidates.filter((output) => !noSendIds.has(output.outputId));
|
|
10608
|
-
const
|
|
11102
|
+
const available = candidates.filter((output) => !noSendIds.has(output.outputId) && eligibleStatuses.includes(output.transactionStatus));
|
|
11103
|
+
const preferredSatoshis = Math.max(1, changeBasket.minimumDesiredUTXOValue);
|
|
11104
|
+
const params = makeFundingParams({
|
|
11105
|
+
storage,
|
|
11106
|
+
vargs,
|
|
11107
|
+
xinputs,
|
|
11108
|
+
xoutputs,
|
|
11109
|
+
changeBasket,
|
|
11110
|
+
feeModel,
|
|
11111
|
+
healthyChangeCount: compatibilityFallback ? candidates.filter((output) => eligibleStatuses.includes(output.transactionStatus)).length : candidates.filter((output) => output.satoshis >= preferredSatoshis).length,
|
|
11112
|
+
compatibilityFallback
|
|
11113
|
+
});
|
|
11114
|
+
const noSendStatuses = await storage.findTransactionStatusesByIds(userId, noSendChangeIn.map((output) => output.transactionId));
|
|
11115
|
+
const plannedNoSend = noSendChangeIn.map((output) => ({
|
|
11116
|
+
outputId: output.outputId,
|
|
11117
|
+
transactionId: output.transactionId,
|
|
11118
|
+
satoshis: output.satoshis,
|
|
11119
|
+
txid: output.txid,
|
|
11120
|
+
vout: output.vout,
|
|
11121
|
+
transactionStatus: noSendStatuses.get(output.transactionId) ?? "nosend"
|
|
11122
|
+
}));
|
|
11123
|
+
const claimStatuses = [.../* @__PURE__ */ new Set([...eligibleStatuses, ...plannedNoSend.map((output) => output.transactionStatus)])];
|
|
10609
11124
|
return await traceStorageStep(storage, "wallet.storage.create_action.funding_plan", parent, {
|
|
10610
11125
|
"funding.candidate_count": available.length,
|
|
10611
|
-
"funding.no_send_change_count": noSendChangeIn.length
|
|
11126
|
+
"funding.no_send_change_count": noSendChangeIn.length,
|
|
11127
|
+
"funding.policy_tier": policyTier,
|
|
11128
|
+
"funding.compatibility_fallback": compatibilityFallback
|
|
10612
11129
|
}, async (span) => {
|
|
10613
11130
|
const allocated = /* @__PURE__ */ new Map();
|
|
10614
11131
|
const availableSelector = new CanonicalChangeSelector(available);
|
|
10615
|
-
const noSend = [...
|
|
10616
|
-
const noSendById = new Map(
|
|
11132
|
+
const noSend = [...plannedNoSend];
|
|
11133
|
+
const noSendById = new Map(plannedNoSend.map((output) => [output.outputId, output]));
|
|
10617
11134
|
const allocate = async (targetSatoshis, exactSatoshis) => {
|
|
10618
11135
|
let output = noSend.pop();
|
|
10619
11136
|
output ??= availableSelector.take(targetSatoshis, exactSatoshis);
|
|
@@ -10644,31 +11161,118 @@ async function prepareFundingPlan(storage, context) {
|
|
|
10644
11161
|
result,
|
|
10645
11162
|
selected,
|
|
10646
11163
|
availableChangeCount: candidates.length,
|
|
10647
|
-
|
|
11164
|
+
eligibleStatuses: claimStatuses,
|
|
11165
|
+
policyTier,
|
|
11166
|
+
compatibilityFallback
|
|
10648
11167
|
};
|
|
10649
11168
|
});
|
|
10650
11169
|
}
|
|
10651
|
-
async function
|
|
10652
|
-
const
|
|
11170
|
+
async function fundingPlanSerializedCost(storage, plan, knownTxids) {
|
|
11171
|
+
const txids = [...new Set(plan.selected.map((candidate) => verifyTruthy(candidate.txid)))];
|
|
11172
|
+
if (txids.length === 0) return plan.result.size;
|
|
10653
11173
|
try {
|
|
10654
|
-
|
|
10655
|
-
|
|
10656
|
-
|
|
10657
|
-
|
|
10658
|
-
|
|
10659
|
-
|
|
11174
|
+
const beef = await storage.getBeefForTransactions(txids, {
|
|
11175
|
+
knownTxids,
|
|
11176
|
+
ignoreStorage: false,
|
|
11177
|
+
ignoreServices: true,
|
|
11178
|
+
ignoreNewProven: false
|
|
11179
|
+
});
|
|
11180
|
+
return plan.result.size + beef.toUint8Array().length;
|
|
11181
|
+
} catch {
|
|
11182
|
+
return Number.MAX_SAFE_INTEGER;
|
|
11183
|
+
}
|
|
11184
|
+
}
|
|
11185
|
+
const FUNDING_TIERS = [
|
|
11186
|
+
{
|
|
11187
|
+
policyTier: "completed",
|
|
11188
|
+
statuses: ["completed"]
|
|
11189
|
+
},
|
|
11190
|
+
{
|
|
11191
|
+
policyTier: "unproven",
|
|
11192
|
+
statuses: ["completed", "unproven"]
|
|
11193
|
+
},
|
|
11194
|
+
{
|
|
11195
|
+
policyTier: "sending",
|
|
11196
|
+
statuses: [
|
|
11197
|
+
"completed",
|
|
11198
|
+
"unproven",
|
|
11199
|
+
"sending"
|
|
11200
|
+
]
|
|
11201
|
+
}
|
|
11202
|
+
];
|
|
11203
|
+
async function resolveFundingCandidates(storage, userId, basketId, parent, trx) {
|
|
11204
|
+
const rawCandidates = await traceStorageStep(storage, "wallet.storage.create_action.funding_candidates", parent, { "funding.include_pending": true }, async (span) => {
|
|
11205
|
+
const outputs = await storage.findAvailableManagedChangeInputCandidates(userId, basketId, false, trx);
|
|
11206
|
+
span?.end({ attributes: {
|
|
11207
|
+
"funding.candidate_count": outputs.length,
|
|
11208
|
+
"funding.candidate_satoshis": outputs.reduce((sum, output) => sum + output.satoshis, 0),
|
|
11209
|
+
"funding.completed_candidate_count": outputs.filter((output) => output.transactionStatus === "completed").length,
|
|
11210
|
+
"funding.unproven_candidate_count": outputs.filter((output) => output.transactionStatus === "unproven").length,
|
|
11211
|
+
"funding.sending_candidate_count": outputs.filter((output) => output.transactionStatus === "sending").length
|
|
11212
|
+
} });
|
|
11213
|
+
return outputs;
|
|
11214
|
+
});
|
|
11215
|
+
const missingStatusIds = rawCandidates.filter((output) => output.transactionStatus == null).map((output) => output.transactionId);
|
|
11216
|
+
const missingStatuses = await storage.findTransactionStatusesByIds(userId, missingStatusIds, trx);
|
|
11217
|
+
const candidates = rawCandidates.map((output) => ({
|
|
11218
|
+
...output,
|
|
11219
|
+
transactionStatus: output.transactionStatus ?? missingStatuses.get(output.transactionId)
|
|
11220
|
+
}));
|
|
11221
|
+
if (candidates.some((output) => output.transactionStatus == null)) throw new WERR_INTERNAL("managed change candidate is missing its source transaction status");
|
|
11222
|
+
return candidates;
|
|
11223
|
+
}
|
|
11224
|
+
async function buildFundingPlanForTier(storage, context, candidates, tier, parent) {
|
|
11225
|
+
try {
|
|
11226
|
+
return { plan: await buildFundingPlan(storage, context, candidates, tier.statuses, tier.policyTier, false, parent) };
|
|
10660
11227
|
} catch (error) {
|
|
10661
|
-
if (!
|
|
10662
|
-
|
|
10663
|
-
|
|
10664
|
-
|
|
10665
|
-
|
|
10666
|
-
|
|
10667
|
-
|
|
11228
|
+
if (!(error instanceof WERR_INSUFFICIENT_FUNDS)) throw error;
|
|
11229
|
+
}
|
|
11230
|
+
try {
|
|
11231
|
+
return { plan: await buildFundingPlan(storage, context, candidates, tier.statuses, "compatibility", true, parent) };
|
|
11232
|
+
} catch (error) {
|
|
11233
|
+
if (!(error instanceof WERR_INSUFFICIENT_FUNDS)) throw error;
|
|
11234
|
+
return { fundingError: error };
|
|
11235
|
+
}
|
|
11236
|
+
}
|
|
11237
|
+
function firstPlanNeedsNoPendingComparison(storage, plan) {
|
|
11238
|
+
const threshold = storage.managedChangePolicy.pendingComparisonInputs;
|
|
11239
|
+
return threshold === -1 || plan.selected.length <= threshold;
|
|
11240
|
+
}
|
|
11241
|
+
async function chooseLowestSerializedFundingPlan(storage, plans, knownTxids) {
|
|
11242
|
+
let chosen = plans[0];
|
|
11243
|
+
let chosenCost = await fundingPlanSerializedCost(storage, chosen, knownTxids);
|
|
11244
|
+
for (const alternative of plans.slice(1)) {
|
|
11245
|
+
const cost = await fundingPlanSerializedCost(storage, alternative, knownTxids);
|
|
11246
|
+
if (cost < chosenCost) {
|
|
11247
|
+
chosen = alternative;
|
|
11248
|
+
chosenCost = cost;
|
|
11249
|
+
}
|
|
11250
|
+
}
|
|
11251
|
+
return chosen;
|
|
11252
|
+
}
|
|
11253
|
+
async function prepareFundingPlanWithLiquidityPolicy(storage, context, parent, trx) {
|
|
11254
|
+
const [userId, vargs, , , changeBasket] = context;
|
|
11255
|
+
const candidates = await resolveFundingCandidates(storage, userId, changeBasket.basketId, parent, trx);
|
|
11256
|
+
const successful = [];
|
|
11257
|
+
let fundingError;
|
|
11258
|
+
for (const tier of FUNDING_TIERS) {
|
|
11259
|
+
const attempted = await buildFundingPlanForTier(storage, context, candidates, tier, parent);
|
|
11260
|
+
fundingError = attempted.fundingError ?? fundingError;
|
|
11261
|
+
const plan = attempted.plan;
|
|
11262
|
+
if (plan != null) {
|
|
11263
|
+
successful.push(plan);
|
|
11264
|
+
if (successful.length === 1 && firstPlanNeedsNoPendingComparison(storage, plan)) return plan;
|
|
11265
|
+
}
|
|
11266
|
+
}
|
|
11267
|
+
if (successful.length > 0) {
|
|
11268
|
+
if (successful.length === 1) return successful[0];
|
|
11269
|
+
return await chooseLowestSerializedFundingPlan(storage, successful, vargs.options.knownTxids);
|
|
10668
11270
|
}
|
|
11271
|
+
if (fundingError != null) throw fundingError;
|
|
11272
|
+
throw new WERR_INTERNAL("funding policy produced neither a plan nor an error");
|
|
10669
11273
|
}
|
|
10670
11274
|
async function claimFundingPlan(storage, request) {
|
|
10671
|
-
const [userId, basketId,
|
|
11275
|
+
const [userId, basketId, eligibleStatuses, transactionId, noSendChangeIn, plan, trx] = request;
|
|
10672
11276
|
if (plan.selected.length === 0) return {
|
|
10673
11277
|
outputs: [],
|
|
10674
11278
|
sourceTransactionCount: 0,
|
|
@@ -10676,10 +11280,8 @@ async function claimFundingPlan(storage, request) {
|
|
|
10676
11280
|
scriptSourceTransactionCount: 0
|
|
10677
11281
|
};
|
|
10678
11282
|
const noSendIds = new Set(noSendChangeIn.map((output) => output.outputId));
|
|
10679
|
-
const statuses = ["completed", "unproven"];
|
|
10680
|
-
if (!excludeSending) statuses.push("sending");
|
|
10681
11283
|
const claim = await storage.transaction(async (claimTrx) => {
|
|
10682
|
-
const currentById = await storage.findFundingOutputsForUpdate(userId, plan.selected.map((output) => output.outputId),
|
|
11284
|
+
const currentById = await storage.findFundingOutputsForUpdate(userId, plan.selected.map((output) => output.outputId), eligibleStatuses, claimTrx);
|
|
10683
11285
|
const transactionIds = [...new Set(Object.values(currentById).map((output) => output.transactionId))];
|
|
10684
11286
|
const claimed = [];
|
|
10685
11287
|
for (const planned of plan.selected) {
|
|
@@ -10749,7 +11351,7 @@ async function fundNewTransactionSdk(storage, userId, vargs, ctx, initialPlan, p
|
|
|
10749
11351
|
const claim = await claimFundingPlan(storage, [
|
|
10750
11352
|
userId,
|
|
10751
11353
|
ctx.changeBasket.basketId,
|
|
10752
|
-
plan.
|
|
11354
|
+
plan.eligibleStatuses,
|
|
10753
11355
|
ctx.transactionId,
|
|
10754
11356
|
ctx.noSendChangeIn,
|
|
10755
11357
|
plan,
|
|
@@ -10767,7 +11369,7 @@ async function fundNewTransactionSdk(storage, userId, vargs, ctx, initialPlan, p
|
|
|
10767
11369
|
}
|
|
10768
11370
|
if (claim.conflict === "noSendChange") throw new WERR_INVALID_PARAMETER("noSendChange", "outputs that remain spendable during action planning");
|
|
10769
11371
|
retryCount++;
|
|
10770
|
-
plan = await
|
|
11372
|
+
plan = await prepareFundingPlanWithLiquidityPolicy(storage, [
|
|
10771
11373
|
userId,
|
|
10772
11374
|
vargs,
|
|
10773
11375
|
ctx.xinputs,
|
|
@@ -12920,7 +13522,7 @@ var StorageReaderWriter = class extends StorageReader {
|
|
|
12920
13522
|
userId: user.userId,
|
|
12921
13523
|
name: "default",
|
|
12922
13524
|
numberOfDesiredUTXOs: 144,
|
|
12923
|
-
minimumDesiredUTXOValue:
|
|
13525
|
+
minimumDesiredUTXOValue: DEFAULT_MANAGED_CHANGE_MINIMUM_SATOSHIS,
|
|
12924
13526
|
isDeleted: false
|
|
12925
13527
|
});
|
|
12926
13528
|
break;
|
|
@@ -13201,8 +13803,10 @@ function validateActionBatchInlinePayload(manifest) {
|
|
|
13201
13803
|
for (const { digest, bytes } of inlineBlobs) if (actionBatchBlobDigest(bytes) !== digest) throw new WERR_INVALID_PARAMETER("inlineBlobs", `content matching digest ${digest}`);
|
|
13202
13804
|
}
|
|
13203
13805
|
function requireUploadableBatch(batch) {
|
|
13204
|
-
if (batch == null
|
|
13205
|
-
if (batch.
|
|
13806
|
+
if (batch == null) throw new WERRActionBatchState("missing");
|
|
13807
|
+
if (batch.status === "committed") throw new WERRActionBatchState("committed", batch.batchId);
|
|
13808
|
+
if (batch.status === "aborted") throw new WERRActionBatchState("aborted", batch.batchId);
|
|
13809
|
+
if (batch.hardExpiresAt.getTime() <= Date.now()) throw new WERRActionBatchState("hard-expired", batch.batchId);
|
|
13206
13810
|
return batch;
|
|
13207
13811
|
}
|
|
13208
13812
|
function manifestPhysicalDigests(manifest) {
|
|
@@ -13562,7 +14166,10 @@ const ACTION_BATCH_LEASE_MS = 900 * 1e3;
|
|
|
13562
14166
|
const ACTION_BATCH_HARD_LIFETIME_MS = 3600 * 1e3;
|
|
13563
14167
|
const INITIAL_RESERVATION_LIMIT = 8;
|
|
13564
14168
|
const INITIAL_EXTRA_OUTPUTS = 3;
|
|
13565
|
-
function
|
|
14169
|
+
function isValidOutpoint(outpoint) {
|
|
14170
|
+
return /^[0-9a-f]{64}$/.test(outpoint.txid) && Number.isSafeInteger(outpoint.vout) && outpoint.vout >= 0;
|
|
14171
|
+
}
|
|
14172
|
+
function getActionBatchCapabilities(maxReservedOutputs = 256, supportsResume = false) {
|
|
13566
14173
|
return { actionBatch: {
|
|
13567
14174
|
version: 1,
|
|
13568
14175
|
maxInlineBytes: ACTION_BATCH_MAX_INLINE_BYTES,
|
|
@@ -13570,6 +14177,8 @@ function getActionBatchCapabilities() {
|
|
|
13570
14177
|
maxConcurrentUploads: 4,
|
|
13571
14178
|
leaseMs: ACTION_BATCH_LEASE_MS,
|
|
13572
14179
|
hardLifetimeMs: ACTION_BATCH_HARD_LIFETIME_MS,
|
|
14180
|
+
resume: supportsResume ? true : void 0,
|
|
14181
|
+
maxReservedOutputs,
|
|
13573
14182
|
compactBegin: true,
|
|
13574
14183
|
manifestVersion: 2,
|
|
13575
14184
|
commitByDigest: true,
|
|
@@ -13585,6 +14194,14 @@ function getActionBatchCapabilities() {
|
|
|
13585
14194
|
function activeExpiry(batch, now = /* @__PURE__ */ new Date()) {
|
|
13586
14195
|
return batch.status !== "active" && batch.status !== "prepared" || batch.expiresAt.getTime() <= now.getTime() || batch.hardExpiresAt.getTime() <= now.getTime();
|
|
13587
14196
|
}
|
|
14197
|
+
function actionBatchErrorState(batch, now = /* @__PURE__ */ new Date()) {
|
|
14198
|
+
if (batch == null) return "missing";
|
|
14199
|
+
if (batch.hardExpiresAt.getTime() <= now.getTime()) return "hard-expired";
|
|
14200
|
+
if (batch.status === "aborted") return "aborted";
|
|
14201
|
+
if (batch.status === "committed") return "committed";
|
|
14202
|
+
if (batch.status === "expired" || batch.expiresAt.getTime() <= now.getTime()) return "expired";
|
|
14203
|
+
if (batch.status !== "active" && batch.status !== "prepared") return "inactive";
|
|
14204
|
+
}
|
|
13588
14205
|
function canExpire(batch, now = /* @__PURE__ */ new Date()) {
|
|
13589
14206
|
return (batch.status === "active" || batch.status === "prepared") && (batch.expiresAt.getTime() <= now.getTime() || batch.hardExpiresAt.getTime() <= now.getTime());
|
|
13590
14207
|
}
|
|
@@ -13670,12 +14287,22 @@ function estimateFirstActionTarget(storage, args, inputSatoshis, outputScriptLen
|
|
|
13670
14287
|
const minFee = Math.ceil(transactionSize([...inputLengths, 107], outputLengths) * fee / 1e3);
|
|
13671
14288
|
return Math.max(1, outputSatoshis + minFee - inputSatoshis);
|
|
13672
14289
|
}
|
|
14290
|
+
function selectReservationChange(candidates, targetSatoshis) {
|
|
14291
|
+
for (const status of [
|
|
14292
|
+
"completed",
|
|
14293
|
+
"unproven",
|
|
14294
|
+
"sending"
|
|
14295
|
+
]) {
|
|
14296
|
+
const selected = selectCanonicalChange(candidates.filter((candidate) => candidate.transactionStatus === status), targetSatoshis);
|
|
14297
|
+
if (selected != null) return selected;
|
|
14298
|
+
}
|
|
14299
|
+
}
|
|
13673
14300
|
function chooseReservationPool(candidates, targetSatoshis, limit, extras, fillLimit, planningCosts) {
|
|
13674
14301
|
const remaining = candidates.filter((output) => output.satoshis > planningCosts.marginalInputFee);
|
|
13675
14302
|
const chosen = [];
|
|
13676
14303
|
let deficit = targetSatoshis + planningCosts.firstChangeCost;
|
|
13677
14304
|
while (deficit > 0 && chosen.length < limit) {
|
|
13678
|
-
const output =
|
|
14305
|
+
const output = selectReservationChange(remaining, deficit + planningCosts.marginalInputFee);
|
|
13679
14306
|
if (output == null) break;
|
|
13680
14307
|
chosen.push(output);
|
|
13681
14308
|
remaining.splice(remaining.indexOf(output), 1);
|
|
@@ -13683,14 +14310,18 @@ function chooseReservationPool(candidates, targetSatoshis, limit, extras, fillLi
|
|
|
13683
14310
|
}
|
|
13684
14311
|
const desiredCount = fillLimit ? limit : Math.min(limit, chosen.length + extras);
|
|
13685
14312
|
while (remaining.length > 0 && chosen.length < desiredCount) {
|
|
13686
|
-
const output =
|
|
14313
|
+
const output = selectReservationChange(remaining, targetSatoshis);
|
|
13687
14314
|
if (output == null) break;
|
|
13688
14315
|
chosen.push(output);
|
|
13689
14316
|
remaining.splice(remaining.indexOf(output), 1);
|
|
13690
14317
|
}
|
|
13691
14318
|
return chosen;
|
|
13692
14319
|
}
|
|
13693
|
-
function
|
|
14320
|
+
function reservationOutput(candidate) {
|
|
14321
|
+
const { transactionStatus: _transactionStatus, ...output } = candidate;
|
|
14322
|
+
return output;
|
|
14323
|
+
}
|
|
14324
|
+
function reservationPlanningCosts(storage, _basket) {
|
|
13694
14325
|
const satsPerKb = validateStorageFeeModel(storage.feeModel).value ?? 0;
|
|
13695
14326
|
const minimumSpendSize = transactionSize([107], [25]);
|
|
13696
14327
|
const minimumSpendFee = Math.ceil(minimumSpendSize * satsPerKb / 1e3);
|
|
@@ -13698,7 +14329,7 @@ function reservationPlanningCosts(storage, basket) {
|
|
|
13698
14329
|
const marginalInputSize = transactionSize([107], []) - transactionSize([], []);
|
|
13699
14330
|
const marginalOutputSize = transactionSize([], [25]) - transactionSize([], []);
|
|
13700
14331
|
return {
|
|
13701
|
-
firstChangeCost:
|
|
14332
|
+
firstChangeCost: dustFloor + Math.ceil(marginalOutputSize * satsPerKb / 1e3),
|
|
13702
14333
|
marginalInputFee: Math.ceil(marginalInputSize * satsPerKb / 1e3)
|
|
13703
14334
|
};
|
|
13704
14335
|
}
|
|
@@ -13773,18 +14404,25 @@ async function beginActionBatch(storage, auth, args) {
|
|
|
13773
14404
|
const explicit = await resolveExplicitOutputs(storage, userId, args.firstAction, outputScriptLengths != null);
|
|
13774
14405
|
const noSendChange = await resolveNoSendChangeOutputs(storage, userId, args.firstAction);
|
|
13775
14406
|
const fixedOutputIds = new Set([...explicit.outputs, ...noSendChange.outputs].map((output) => output.outputId));
|
|
13776
|
-
const
|
|
14407
|
+
const availableOutputs = (await storage.findAvailableManagedChangeInputs(userId, changeBasket.basketId, false)).filter((output) => !fixedOutputIds.has(output.outputId));
|
|
14408
|
+
const availableStatuses = await storage.findTransactionStatusesByIds(userId, availableOutputs.map((output) => output.transactionId));
|
|
14409
|
+
const available = availableOutputs.map((output) => ({
|
|
14410
|
+
...output,
|
|
14411
|
+
transactionStatus: availableStatuses.get(output.transactionId) ?? "sending"
|
|
14412
|
+
}));
|
|
13777
14413
|
const target = estimateFirstActionTarget(storage, args.firstAction, explicit.inputSatoshis + noSendChange.inputSatoshis, outputScriptLengths);
|
|
13778
|
-
const fixedOutputs = [...explicit.outputs, ...noSendChange.outputs];
|
|
13779
|
-
const
|
|
14414
|
+
const fixedOutputs = [...new Map([...explicit.outputs, ...noSendChange.outputs].map((output) => [output.outputId, output])).values()];
|
|
14415
|
+
const maxReservedOutputs = storage.actionBatchMaxReservedOutputs;
|
|
14416
|
+
if (maxReservedOutputs >= 0 && fixedOutputs.length > maxReservedOutputs) throw new WERR_INVALID_PARAMETER("firstAction", `no more than ${maxReservedOutputs} persisted inputs`);
|
|
14417
|
+
const fundingOutputs = chooseReservationPool(available, target, Math.max(0, (maxReservedOutputs < 0 ? INITIAL_RESERVATION_LIMIT : Math.min(INITIAL_RESERVATION_LIMIT, maxReservedOutputs)) - fixedOutputs.length), INITIAL_EXTRA_OUTPUTS, false, reservationPlanningCosts(storage, changeBasket)).map(reservationOutput);
|
|
13780
14418
|
const batch = newBatch(userId, args.batchId);
|
|
13781
14419
|
await storage.transaction(async (trx) => {
|
|
13782
14420
|
await storage.insertActionBatch(batch, trx);
|
|
13783
|
-
await reserveOutputs(storage, batch, [...fixedOutputs, ...
|
|
14421
|
+
await reserveOutputs(storage, batch, [...fixedOutputs, ...fundingOutputs], trx);
|
|
13784
14422
|
});
|
|
13785
14423
|
let fundingResult;
|
|
13786
14424
|
try {
|
|
13787
|
-
fundingResult = await makeFundingResult(storage, args.firstAction, [...
|
|
14425
|
+
fundingResult = await makeFundingResult(storage, args.firstAction, [...fundingOutputs, ...fixedOutputs]);
|
|
13788
14426
|
} catch (error) {
|
|
13789
14427
|
await storage.transaction(async (trx) => await releaseBatchState(storage, batch, "aborted", trx));
|
|
13790
14428
|
throw error;
|
|
@@ -13798,41 +14436,63 @@ async function beginActionBatch(storage, auth, args) {
|
|
|
13798
14436
|
feeModel: validateStorageFeeModel(storage.feeModel),
|
|
13799
14437
|
commissionSatoshis: storage.commissionSatoshis,
|
|
13800
14438
|
commissionPubKeyHex: storage.commissionPubKeyHex,
|
|
13801
|
-
availableChangeCount: available.length,
|
|
13802
|
-
|
|
14439
|
+
availableChangeCount: available.filter((output) => output.satoshis >= Math.max(1, changeBasket.minimumDesiredUTXOValue)).length,
|
|
14440
|
+
managedChangePolicy: {
|
|
14441
|
+
maxOutputsPerAction: storage.managedChangePolicy.maxOutputsPerAction,
|
|
14442
|
+
migrationInputsPerAction: storage.managedChangePolicy.migrationInputsPerAction
|
|
14443
|
+
},
|
|
14444
|
+
reservedOutputs: fundingResult.outputs.filter((output) => fundingOutputs.some((candidate) => candidate.outputId === output.outputId)),
|
|
13803
14445
|
explicitOutputs: fundingResult.outputs.filter((output) => explicitIds.has(output.outputId)),
|
|
13804
14446
|
inputBeef: fundingResult.beef
|
|
13805
14447
|
};
|
|
13806
14448
|
}
|
|
13807
|
-
function requireLiveBatch(batch) {
|
|
13808
|
-
|
|
14449
|
+
function requireLiveBatch(batch, batchId) {
|
|
14450
|
+
const state = actionBatchErrorState(batch);
|
|
14451
|
+
if (state != null) throw new WERRActionBatchState(state, batchId ?? batch?.batchId);
|
|
14452
|
+
if (batch == null) throw new WERRActionBatchState("missing", batchId);
|
|
13809
14453
|
return batch;
|
|
13810
14454
|
}
|
|
13811
14455
|
async function extendActionBatch(storage, auth, args) {
|
|
13812
14456
|
const userId = verifyId(auth.userId);
|
|
13813
14457
|
await cleanupExpiredActionBatches(storage);
|
|
13814
|
-
const batch = requireLiveBatch(await storage.findActionBatch(userId, args.batchId));
|
|
14458
|
+
const batch = requireLiveBatch(await storage.findActionBatch(userId, args.batchId), args.batchId);
|
|
13815
14459
|
const basket = verifyOne(await storage.findOutputBaskets({ partial: {
|
|
13816
14460
|
userId,
|
|
13817
14461
|
name: "default"
|
|
13818
14462
|
} }));
|
|
13819
14463
|
const alreadyReserved = await storage.findActionBatchOutputIds(batch.actionBatchId);
|
|
13820
|
-
const available = await storage.findAvailableManagedChangeInputs(userId, basket.basketId, false);
|
|
13821
14464
|
if (!Number.isSafeInteger(args.requestedOutputs) || args.requestedOutputs < 0) throw new WERR_INVALID_PARAMETER("requestedOutputs", "non-negative safe integer");
|
|
13822
|
-
|
|
13823
|
-
const
|
|
14465
|
+
if (!Number.isSafeInteger(args.targetSatoshis) || args.targetSatoshis < 0) throw new WERR_INVALID_PARAMETER("targetSatoshis", "non-negative safe integer");
|
|
14466
|
+
const maxReservedOutputs = storage.actionBatchMaxReservedOutputs;
|
|
14467
|
+
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`);
|
|
14468
|
+
const remainingCapacity = maxReservedOutputs < 0 ? Number.MAX_SAFE_INTEGER : maxReservedOutputs - alreadyReserved.length;
|
|
14469
|
+
if (remainingCapacity <= 0 && (args.requestedOutputs > 0 || args.explicitOutpoints.length > 0)) throw new WERR_INVALID_OPERATION(`action batch already holds the maximum of ${maxReservedOutputs} outputs`);
|
|
13824
14470
|
const explicitByOutpoint = await storage.findOutputsByOutpoints(userId, args.explicitOutpoints);
|
|
13825
|
-
const explicit = Object.values(explicitByOutpoint).filter((output) => !alreadyReserved.includes(output.outputId));
|
|
13826
|
-
|
|
14471
|
+
const explicit = [...new Map(Object.values(explicitByOutpoint).filter((output) => !alreadyReserved.includes(output.outputId)).map((output) => [output.outputId, output])).values()];
|
|
14472
|
+
if (explicit.length > remainingCapacity) throw new WERR_INVALID_PARAMETER("explicitOutpoints", `no more than ${remainingCapacity} additional reserved outputs`);
|
|
14473
|
+
const explicitIds = new Set(explicit.map((output) => output.outputId));
|
|
14474
|
+
const availableOutputs = (await storage.findAvailableManagedChangeInputs(userId, basket.basketId, false)).filter((output) => !explicitIds.has(output.outputId));
|
|
14475
|
+
const availableStatuses = await storage.findTransactionStatusesByIds(userId, availableOutputs.map((output) => output.transactionId));
|
|
14476
|
+
const available = availableOutputs.map((output) => ({
|
|
14477
|
+
...output,
|
|
14478
|
+
transactionStatus: availableStatuses.get(output.transactionId) ?? "sending"
|
|
14479
|
+
}));
|
|
14480
|
+
const requestedCount = Math.min(args.requestedOutputs, 64, remainingCapacity - explicit.length);
|
|
14481
|
+
const fundingOutputs = chooseReservationPool(available, Math.max(1, args.targetSatoshis), requestedCount, 0, true, reservationPlanningCosts(storage, basket)).map(reservationOutput);
|
|
14482
|
+
const fundingResult = await makeFundingResult(storage, argsToFundingShape(args.includeSourceTransactions), [...fundingOutputs, ...explicit]);
|
|
13827
14483
|
const expiresAt = new Date(Math.min(batch.hardExpiresAt.getTime(), Date.now() + ACTION_BATCH_LEASE_MS));
|
|
13828
14484
|
await storage.transaction(async (trx) => {
|
|
13829
|
-
const current = requireLiveBatch(await storage.findActionBatchForUpdate(userId, args.batchId, trx));
|
|
13830
|
-
|
|
14485
|
+
const current = requireLiveBatch(await storage.findActionBatchForUpdate(userId, args.batchId, trx), args.batchId);
|
|
14486
|
+
const additions = [...new Map([...fundingOutputs, ...explicit].map((output) => [output.outputId, output])).values()];
|
|
14487
|
+
const currentReserved = new Set(await storage.findActionBatchOutputIds(current.actionBatchId, trx));
|
|
14488
|
+
const newAdditions = additions.filter((output) => !currentReserved.has(output.outputId));
|
|
14489
|
+
if (maxReservedOutputs >= 0 && currentReserved.size + newAdditions.length > maxReservedOutputs) throw new WERR_INVALID_OPERATION(`action batch cannot reserve more than ${maxReservedOutputs} outputs`);
|
|
14490
|
+
await reserveOutputs(storage, current, newAdditions, trx);
|
|
13831
14491
|
await storage.updateActionBatch(current.actionBatchId, { expiresAt }, trx);
|
|
13832
14492
|
});
|
|
13833
14493
|
return {
|
|
13834
14494
|
expiresAt: expiresAt.toISOString(),
|
|
13835
|
-
reservedOutputs: fundingResult.outputs.filter((output) =>
|
|
14495
|
+
reservedOutputs: fundingResult.outputs.filter((output) => fundingOutputs.some((candidate) => candidate.outputId === output.outputId)),
|
|
13836
14496
|
explicitOutputs: fundingResult.outputs.filter((output) => explicit.some((candidate) => candidate.outputId === output.outputId)),
|
|
13837
14497
|
inputBeef: fundingResult.beef
|
|
13838
14498
|
};
|
|
@@ -13868,14 +14528,64 @@ function argsToFundingShape(includeSourceTransactions) {
|
|
|
13868
14528
|
async function renewActionBatch(storage, auth, batchId) {
|
|
13869
14529
|
const userId = verifyId(auth.userId);
|
|
13870
14530
|
return await storage.transaction(async (trx) => {
|
|
13871
|
-
const batch = requireLiveBatch(await storage.findActionBatchForUpdate(userId, batchId, trx));
|
|
14531
|
+
const batch = requireLiveBatch(await storage.findActionBatchForUpdate(userId, batchId, trx), batchId);
|
|
13872
14532
|
const expiresAt = new Date(Math.min(batch.hardExpiresAt.getTime(), Date.now() + ACTION_BATCH_LEASE_MS));
|
|
13873
14533
|
await storage.updateActionBatch(batch.actionBatchId, { expiresAt }, trx);
|
|
13874
14534
|
return { expiresAt: expiresAt.toISOString() };
|
|
13875
14535
|
});
|
|
13876
14536
|
}
|
|
14537
|
+
function validateResumeOutpoints(storage, args) {
|
|
14538
|
+
const unique = new Set(args.outpoints.map((outpoint) => `${outpoint.txid}.${outpoint.vout}`));
|
|
14539
|
+
const maxReservedOutputs = storage.actionBatchMaxReservedOutputs;
|
|
14540
|
+
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`);
|
|
14541
|
+
}
|
|
14542
|
+
/**
|
|
14543
|
+
* Reacquire exactly the persisted inputs owned by one expired client
|
|
14544
|
+
* workspace. This is deliberately explicit: another action may not become a
|
|
14545
|
+
* member merely because it happens to use the same Wallet instance.
|
|
14546
|
+
*/
|
|
14547
|
+
async function resumeActionBatch(storage, auth, args) {
|
|
14548
|
+
const userId = verifyId(auth.userId);
|
|
14549
|
+
validateResumeOutpoints(storage, args);
|
|
14550
|
+
await cleanupExpiredActionBatches(storage);
|
|
14551
|
+
return await storage.transaction(async (trx) => {
|
|
14552
|
+
const batch = await storage.findActionBatchForUpdate(userId, args.batchId, trx);
|
|
14553
|
+
const state = actionBatchErrorState(batch);
|
|
14554
|
+
if (state != null && state !== "expired") throw new WERRActionBatchState(state, args.batchId);
|
|
14555
|
+
if (batch == null) throw new WERRActionBatchState("missing", args.batchId);
|
|
14556
|
+
const expiresAt = new Date(Math.min(batch.hardExpiresAt.getTime(), Date.now() + ACTION_BATCH_LEASE_MS));
|
|
14557
|
+
if (state == null) {
|
|
14558
|
+
const reserved = new Set(await storage.findActionBatchOutputIds(batch.actionBatchId, trx));
|
|
14559
|
+
const current = await storage.findOutputsByOutpointsForUpdate(userId, args.outpoints, trx);
|
|
14560
|
+
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);
|
|
14561
|
+
await storage.updateActionBatch(batch.actionBatchId, { expiresAt }, trx);
|
|
14562
|
+
return { expiresAt: expiresAt.toISOString() };
|
|
14563
|
+
}
|
|
14564
|
+
const stored = await storage.findOutputsByOutpointsForUpdate(userId, args.outpoints, trx);
|
|
14565
|
+
const outputs = args.outpoints.map((outpoint) => stored[`${outpoint.txid}.${outpoint.vout}`]);
|
|
14566
|
+
if (outputs.some((output) => output == null || !output.spendable || output.spentBy != null)) throw new WERRActionBatchState("conflicted", args.batchId);
|
|
14567
|
+
await storage.deleteActionBatchOutputReservations(batch.actionBatchId, trx);
|
|
14568
|
+
const exactOutputs = outputs;
|
|
14569
|
+
if ((await storage.findReservedActionBatchOutputIds(exactOutputs.map((output) => output.outputId), trx)).length > 0) throw new WERRActionBatchState("conflicted", args.batchId);
|
|
14570
|
+
const now = /* @__PURE__ */ new Date();
|
|
14571
|
+
await storage.reserveActionBatchOutputs(exactOutputs.map((output) => ({
|
|
14572
|
+
actionBatchId: batch.actionBatchId,
|
|
14573
|
+
outputId: output.outputId,
|
|
14574
|
+
created_at: now,
|
|
14575
|
+
updated_at: now
|
|
14576
|
+
})), trx);
|
|
14577
|
+
await storage.updateActionBatch(batch.actionBatchId, {
|
|
14578
|
+
status: "active",
|
|
14579
|
+
expiresAt,
|
|
14580
|
+
manifest: void 0,
|
|
14581
|
+
manifestDigest: void 0,
|
|
14582
|
+
uploadDigests: void 0
|
|
14583
|
+
}, trx);
|
|
14584
|
+
return { expiresAt: expiresAt.toISOString() };
|
|
14585
|
+
});
|
|
14586
|
+
}
|
|
13877
14587
|
async function reacquireManifestInputs(storage, userId, batch, validated, trx) {
|
|
13878
|
-
if (batch.hardExpiresAt.getTime() <= Date.now()) throw new
|
|
14588
|
+
if (batch.hardExpiresAt.getTime() <= Date.now()) throw new WERRActionBatchState("hard-expired", batch.batchId);
|
|
13879
14589
|
const stagedTxids = new Set(validated.actions.map(({ action }) => action.txid));
|
|
13880
14590
|
const outpoints = [...new Map(validated.actions.flatMap(({ action }) => action.plan.inputs).filter((input) => !stagedTxids.has(input.sourceTxid)).map((input) => [`${input.sourceTxid}.${input.sourceVout}`, {
|
|
13881
14591
|
txid: input.sourceTxid,
|
|
@@ -14042,7 +14752,7 @@ async function persistAction(...[storage, userId, validated, reservedOutputIds,
|
|
|
14042
14752
|
async function persistManifestAtomically(storage, userId, batch, manifest, validated) {
|
|
14043
14753
|
return await storage.transaction(async (trx) => {
|
|
14044
14754
|
const current = await storage.findActionBatchForUpdate(userId, batch.batchId, trx);
|
|
14045
|
-
if (current == null) throw new
|
|
14755
|
+
if (current == null) throw new WERRActionBatchState("missing", batch.batchId);
|
|
14046
14756
|
if (current.status === "committed") {
|
|
14047
14757
|
if (current.manifestDigest !== manifest.digest) throw new WERR_INVALID_OPERATION("batch committed with another manifest");
|
|
14048
14758
|
return {
|
|
@@ -14050,12 +14760,12 @@ async function persistManifestAtomically(storage, userId, batch, manifest, valid
|
|
|
14050
14760
|
alreadyCommitted: true
|
|
14051
14761
|
};
|
|
14052
14762
|
}
|
|
14053
|
-
if (current.status === "aborted") throw new
|
|
14763
|
+
if (current.status === "aborted") throw new WERRActionBatchState("aborted", batch.batchId);
|
|
14054
14764
|
if (current.manifestDigest != null && current.manifestDigest !== manifest.digest) throw new WERR_INVALID_OPERATION("batch prepared with another manifest");
|
|
14055
14765
|
if (current.status === "expired" || activeExpiry(current)) {
|
|
14056
|
-
if (current.hardExpiresAt.getTime() <= Date.now()) throw new
|
|
14766
|
+
if (current.hardExpiresAt.getTime() <= Date.now()) throw new WERRActionBatchState("hard-expired", batch.batchId);
|
|
14057
14767
|
await reacquireManifestInputs(storage, userId, current, validated, trx);
|
|
14058
|
-
} else requireLiveBatch(current);
|
|
14768
|
+
} else requireLiveBatch(current, batch.batchId);
|
|
14059
14769
|
const reservedOutputIds = new Set(await storage.findActionBatchOutputIds(current.actionBatchId, trx));
|
|
14060
14770
|
const stagedTxids = new Set(validated.actions.map(({ action }) => action.txid));
|
|
14061
14771
|
const inputOutpoints = [...new Map(validated.actions.flatMap(({ action }) => action.plan.inputs).filter((input) => !stagedTxids.has(input.sourceTxid)).map((input) => [`${input.sourceTxid}.${input.sourceVout}`, {
|
|
@@ -14133,14 +14843,14 @@ async function completeCommittedBatch(storage, userId, batch, manifest, alreadyC
|
|
|
14133
14843
|
}
|
|
14134
14844
|
async function commitActionBatchOnce(storage, userId, manifest) {
|
|
14135
14845
|
const batch = await storage.findActionBatch(userId, manifest.batchId);
|
|
14136
|
-
if (batch == null) throw new
|
|
14846
|
+
if (batch == null) throw new WERRActionBatchState("missing", manifest.batchId);
|
|
14137
14847
|
if (batch.status === "committed") {
|
|
14138
14848
|
if (batch.manifestDigest !== manifest.digest) throw new WERR_INVALID_OPERATION("batch committed with another manifest");
|
|
14139
14849
|
return await completeCommittedBatch(storage, userId, batch, manifest, true);
|
|
14140
14850
|
}
|
|
14141
|
-
if (batch.status === "aborted") throw new
|
|
14142
|
-
if (!(batch.status === "expired" || activeExpiry(batch))) requireLiveBatch(batch);
|
|
14143
|
-
else if (batch.hardExpiresAt.getTime() <= Date.now()) throw new
|
|
14851
|
+
if (batch.status === "aborted") throw new WERRActionBatchState("aborted", manifest.batchId);
|
|
14852
|
+
if (!(batch.status === "expired" || activeExpiry(batch))) requireLiveBatch(batch, manifest.batchId);
|
|
14853
|
+
else if (batch.hardExpiresAt.getTime() <= Date.now()) throw new WERRActionBatchState("hard-expired", manifest.batchId);
|
|
14144
14854
|
if (batch.manifestDigest != null && batch.manifestDigest !== manifest.digest) throw new WERR_INVALID_OPERATION("batch prepared with another manifest");
|
|
14145
14855
|
const persisted = await persistManifestAtomically(storage, userId, batch, manifest, await validateManifestActions(storage, batch, manifest));
|
|
14146
14856
|
return await completeCommittedBatch(storage, userId, persisted.batch, manifest, persisted.alreadyCommitted, persisted.share);
|
|
@@ -14223,6 +14933,8 @@ var StorageProvider = class StorageProvider extends StorageReaderWriter {
|
|
|
14223
14933
|
commissionSatoshis;
|
|
14224
14934
|
commissionPubKeyHex;
|
|
14225
14935
|
maxRecursionDepth;
|
|
14936
|
+
actionBatchMaxReservedOutputs;
|
|
14937
|
+
managedChangePolicy;
|
|
14226
14938
|
scriptVerifier;
|
|
14227
14939
|
static defaultOptions() {
|
|
14228
14940
|
return {
|
|
@@ -14231,7 +14943,9 @@ var StorageProvider = class StorageProvider extends StorageReaderWriter {
|
|
|
14231
14943
|
value: 100
|
|
14232
14944
|
},
|
|
14233
14945
|
commissionSatoshis: 0,
|
|
14234
|
-
commissionPubKeyHex: void 0
|
|
14946
|
+
commissionPubKeyHex: void 0,
|
|
14947
|
+
actionBatchMaxReservedOutputs: 256,
|
|
14948
|
+
managedChangePolicy: defaultManagedChangePolicy()
|
|
14235
14949
|
};
|
|
14236
14950
|
}
|
|
14237
14951
|
static createStorageBaseOptions(chain) {
|
|
@@ -14246,6 +14960,10 @@ var StorageProvider = class StorageProvider extends StorageReaderWriter {
|
|
|
14246
14960
|
this.commissionPubKeyHex = options.commissionPubKeyHex;
|
|
14247
14961
|
this.commissionSatoshis = options.commissionSatoshis;
|
|
14248
14962
|
this.maxRecursionDepth = 12;
|
|
14963
|
+
const maxReservedOutputs = options.actionBatchMaxReservedOutputs ?? 256;
|
|
14964
|
+
if (maxReservedOutputs !== -1 && (!Number.isSafeInteger(maxReservedOutputs) || maxReservedOutputs < 1)) throw new WERR_INVALID_PARAMETER("actionBatchMaxReservedOutputs", "a positive safe integer or -1 for unlimited");
|
|
14965
|
+
this.actionBatchMaxReservedOutputs = maxReservedOutputs;
|
|
14966
|
+
this.managedChangePolicy = validateManagedChangePolicy(options.managedChangePolicy);
|
|
14249
14967
|
this.scriptVerifier = options.scriptVerifier;
|
|
14250
14968
|
}
|
|
14251
14969
|
/** Mark a planned set of change inputs spent within the caller's transaction. */
|
|
@@ -14276,12 +14994,16 @@ var StorageProvider = class StorageProvider extends StorageReaderWriter {
|
|
|
14276
14994
|
}
|
|
14277
14995
|
/** Read only the fields needed by the in-memory funding planner. */
|
|
14278
14996
|
async findAvailableManagedChangeInputCandidates(userId, basketId, excludeSending, trx) {
|
|
14279
|
-
|
|
14997
|
+
const outputs = await this.findAvailableManagedChangeInputs(userId, basketId, excludeSending, trx);
|
|
14998
|
+
const findStatuses = this.findTransactionStatusesByIds?.bind(this);
|
|
14999
|
+
const statuses = findStatuses == null ? /* @__PURE__ */ new Map() : await findStatuses(userId, outputs.map((output) => output.transactionId), trx);
|
|
15000
|
+
return outputs.map(({ outputId, transactionId, satoshis, txid, vout }) => ({
|
|
14280
15001
|
outputId,
|
|
14281
15002
|
transactionId,
|
|
14282
15003
|
satoshis,
|
|
14283
15004
|
txid,
|
|
14284
|
-
vout
|
|
15005
|
+
vout,
|
|
15006
|
+
transactionStatus: statuses.get(transactionId)
|
|
14285
15007
|
}));
|
|
14286
15008
|
}
|
|
14287
15009
|
/** Read the current status of a set of source transactions without loading raw transaction bytes. */
|
|
@@ -14369,7 +15091,7 @@ var StorageProvider = class StorageProvider extends StorageReaderWriter {
|
|
|
14369
15091
|
throw new WERR_NOT_IMPLEMENTED();
|
|
14370
15092
|
}
|
|
14371
15093
|
async getCapabilities() {
|
|
14372
|
-
return this.supportsActionBatchPersistence() ? getActionBatchCapabilities() : {};
|
|
15094
|
+
return this.supportsActionBatchPersistence() ? getActionBatchCapabilities(this.actionBatchMaxReservedOutputs, true) : {};
|
|
14373
15095
|
}
|
|
14374
15096
|
supportsActionBatchPersistence() {
|
|
14375
15097
|
return false;
|
|
@@ -14388,6 +15110,9 @@ var StorageProvider = class StorageProvider extends StorageReaderWriter {
|
|
|
14388
15110
|
async renewActionBatch(auth, batchId) {
|
|
14389
15111
|
return await renewActionBatch(this, auth, batchId);
|
|
14390
15112
|
}
|
|
15113
|
+
async resumeActionBatch(auth, args) {
|
|
15114
|
+
return await resumeActionBatch(this, auth, args);
|
|
15115
|
+
}
|
|
14391
15116
|
async prepareActionBatchCommit(auth, manifest) {
|
|
14392
15117
|
return await prepareActionBatchCommit(this, auth, manifest);
|
|
14393
15118
|
}
|
|
@@ -14985,7 +15710,12 @@ var StorageProvider = class StorageProvider extends StorageReaderWriter {
|
|
|
14985
15710
|
for (const output of outputs) {
|
|
14986
15711
|
const outputId = verifyId(output.outputId);
|
|
14987
15712
|
await this.validateOutputScript(output);
|
|
14988
|
-
|
|
15713
|
+
if (output.lockingScript == null) {
|
|
15714
|
+
verdicts.set(outputId, void 0);
|
|
15715
|
+
continue;
|
|
15716
|
+
}
|
|
15717
|
+
const classification = await classifyOutputUtxo(services, output);
|
|
15718
|
+
verdicts.set(outputId, classification.verdict === "unknown" ? void 0 : classification.verdict === "unspent");
|
|
14989
15719
|
}
|
|
14990
15720
|
}
|
|
14991
15721
|
return verdicts;
|
|
@@ -15147,16 +15877,12 @@ var StorageProvider = class StorageProvider extends StorageReaderWriter {
|
|
|
15147
15877
|
} });
|
|
15148
15878
|
for (const o of outputs) {
|
|
15149
15879
|
if (!o.spendable) continue;
|
|
15150
|
-
|
|
15880
|
+
await this.validateOutputScript(o);
|
|
15881
|
+
if (!requireConclusiveUtxo(await classifyOutputUtxo(services, o))) invalidSpendableOutputs.push(o);
|
|
15151
15882
|
}
|
|
15152
15883
|
}
|
|
15153
15884
|
return { invalidSpendableOutputs };
|
|
15154
15885
|
}
|
|
15155
|
-
async checkOutputIsUtxo(o, services) {
|
|
15156
|
-
if (o.lockingScript == null || o.lockingScript.length === 0) return false;
|
|
15157
|
-
const hash = services.hashOutputScript(asString(o.lockingScript));
|
|
15158
|
-
return (await services.getUtxoStatus(hash, void 0, `${o.txid ?? ""}.${o.vout ?? ""}`)).isUtxo === true;
|
|
15159
|
-
}
|
|
15160
15886
|
async updateProvenTxReqDynamics(id, update, trx) {
|
|
15161
15887
|
const partial = {};
|
|
15162
15888
|
if (update.updated_at != null) partial.updated_at = update.updated_at;
|
|
@@ -15266,6 +15992,7 @@ const getLabelToSpecOp = () => {
|
|
|
15266
15992
|
//#region ../src/storage/methods/listActionsIdb.ts
|
|
15267
15993
|
async function enrichIdbActionLabels(storage, tx, action, timeFilterRequested) {
|
|
15268
15994
|
action.labels = (await storage.getLabelsForTransactionId(tx.transactionId)).map((l) => l.label);
|
|
15995
|
+
if (tx.reference != null && tx.reference !== "") action.labels = applyBrc153ReferenceLabel(action.labels, tx.reference);
|
|
15269
15996
|
if (timeFilterRequested) {
|
|
15270
15997
|
const ts = tx.created_at ? new Date(tx.created_at).getTime() : NaN;
|
|
15271
15998
|
if (!Number.isNaN(ts)) {
|
|
@@ -15384,20 +16111,129 @@ async function listActionsIdb(storage, auth, vargs) {
|
|
|
15384
16111
|
return r;
|
|
15385
16112
|
}
|
|
15386
16113
|
//#endregion
|
|
15387
|
-
//#region ../src/storage/methods/
|
|
15388
|
-
const
|
|
15389
|
-
|
|
15390
|
-
|
|
15391
|
-
|
|
15392
|
-
|
|
15393
|
-
|
|
15394
|
-
|
|
16114
|
+
//#region ../src/storage/methods/reviewUtxoOutputs.ts
|
|
16115
|
+
const MAX_AUDIT_PROVIDERS = 8;
|
|
16116
|
+
const MAX_PROVIDER_NAME_LENGTH = 128;
|
|
16117
|
+
const UTXO_REVIEW_PROVIDER_TIMEOUT_MSECS = 5e3;
|
|
16118
|
+
function diagnosticsFor(classifications, releasedOutputIds) {
|
|
16119
|
+
const allProviders = [...new Set(classifications.map((result) => result.status.provider.slice(0, MAX_PROVIDER_NAME_LENGTH)))];
|
|
16120
|
+
const providers = allProviders.slice(0, MAX_AUDIT_PROVIDERS);
|
|
16121
|
+
const confirmedSpent = classifications.filter((result) => result.status.verdict === "spent");
|
|
16122
|
+
const released = confirmedSpent.filter((result) => releasedOutputIds.has(result.output.outputId));
|
|
16123
|
+
return {
|
|
16124
|
+
checked: classifications.length,
|
|
16125
|
+
confirmedUnspent: classifications.filter((result) => result.status.verdict === "unspent").length,
|
|
16126
|
+
confirmedSpent: confirmedSpent.length,
|
|
16127
|
+
unknown: classifications.filter((result) => result.status.verdict === "unknown").length,
|
|
16128
|
+
confirmedSpentSatoshis: confirmedSpent.reduce((sum, result) => sum + result.output.satoshis, 0),
|
|
16129
|
+
released: released.length,
|
|
16130
|
+
releasedSatoshis: released.reduce((sum, result) => sum + result.output.satoshis, 0),
|
|
16131
|
+
providers,
|
|
16132
|
+
providerCount: allProviders.length,
|
|
16133
|
+
providersTruncated: allProviders.length > providers.length
|
|
16134
|
+
};
|
|
16135
|
+
}
|
|
16136
|
+
function auditDetails(diagnostics, userId, releaseMode, reason) {
|
|
16137
|
+
return JSON.stringify({
|
|
16138
|
+
operation: "specOpInvalidChange",
|
|
16139
|
+
reason,
|
|
16140
|
+
releaseMode,
|
|
16141
|
+
userId,
|
|
16142
|
+
...diagnostics
|
|
16143
|
+
});
|
|
16144
|
+
}
|
|
16145
|
+
async function classifyCandidates(storage, outputs) {
|
|
16146
|
+
const services = storage.getServices();
|
|
16147
|
+
return await mapWithConcurrency(outputs.filter((output) => output.basketId != null), 4, async (output) => {
|
|
16148
|
+
try {
|
|
16149
|
+
await storage.validateOutputScript(output);
|
|
16150
|
+
} catch (error) {
|
|
16151
|
+
return {
|
|
16152
|
+
output,
|
|
16153
|
+
status: {
|
|
16154
|
+
verdict: "unknown",
|
|
16155
|
+
provider: "<script-validation-error>",
|
|
16156
|
+
error
|
|
16157
|
+
}
|
|
16158
|
+
};
|
|
16159
|
+
}
|
|
16160
|
+
let timeout;
|
|
16161
|
+
const providerCall = classifyOutputUtxo(services, output);
|
|
16162
|
+
const timedClassification = new Promise((resolve) => {
|
|
16163
|
+
timeout = setTimeout(() => resolve({
|
|
16164
|
+
verdict: "unknown",
|
|
16165
|
+
provider: "<review-timeout>",
|
|
16166
|
+
error: /* @__PURE__ */ new Error(`UTXO classification exceeded ${UTXO_REVIEW_PROVIDER_TIMEOUT_MSECS}ms`)
|
|
16167
|
+
}), UTXO_REVIEW_PROVIDER_TIMEOUT_MSECS);
|
|
16168
|
+
});
|
|
16169
|
+
try {
|
|
16170
|
+
return {
|
|
16171
|
+
output,
|
|
16172
|
+
status: await Promise.race([providerCall, timedClassification])
|
|
16173
|
+
};
|
|
16174
|
+
} finally {
|
|
16175
|
+
if (timeout != null) clearTimeout(timeout);
|
|
16176
|
+
providerCall.catch(() => void 0);
|
|
16177
|
+
}
|
|
16178
|
+
});
|
|
16179
|
+
}
|
|
16180
|
+
async function releaseConfirmedSpent(storage, auth, classifications, releaseMode) {
|
|
16181
|
+
const confirmedSpent = classifications.filter((result) => result.status.verdict === "spent");
|
|
16182
|
+
const releasedOutputIds = /* @__PURE__ */ new Set();
|
|
16183
|
+
await storage.transaction(async (trx) => {
|
|
16184
|
+
for (const { output } of confirmedSpent) {
|
|
16185
|
+
const current = await storage.findOutputById(output.outputId, trx, true);
|
|
16186
|
+
if (current == null || current.userId !== auth.userId) throw new WERR_INVALID_PARAMETER("outputId", `owned by user ${auth.userId}`);
|
|
16187
|
+
if (current.spendable !== true || current.spentBy != null) throw new WERR_INVALID_OPERATION(`Output ${output.outputId} changed state during UTXO review; no outputs were changed.`);
|
|
16188
|
+
if (await storage.updateOutput(verifyId(output.outputId), { spendable: false }, trx) !== 1) throw new WERR_INVALID_PARAMETER("outputId", `updated exactly once: ${output.outputId}`);
|
|
16189
|
+
releasedOutputIds.add(output.outputId);
|
|
16190
|
+
}
|
|
16191
|
+
const now = /* @__PURE__ */ new Date();
|
|
16192
|
+
const diagnostics = diagnosticsFor(classifications, releasedOutputIds);
|
|
16193
|
+
await storage.insertMonitorEvent({
|
|
16194
|
+
created_at: now,
|
|
16195
|
+
updated_at: now,
|
|
16196
|
+
id: 0,
|
|
16197
|
+
event: releaseMode === "atomic" ? "InvalidChangeRelease" : "InvalidChangeConclusiveRelease",
|
|
16198
|
+
details: auditDetails(diagnostics, auth.userId, releaseMode, "provider-confirmed-spent")
|
|
16199
|
+
}, trx);
|
|
16200
|
+
});
|
|
16201
|
+
return releasedOutputIds;
|
|
16202
|
+
}
|
|
16203
|
+
/**
|
|
16204
|
+
* Classify a bounded set of wallet outputs without treating provider failure as
|
|
16205
|
+
* proof that an output was spent. Read-only reviews always return their
|
|
16206
|
+
* conclusive and unknown partitions. Atomic release requires every verdict to
|
|
16207
|
+
* be conclusive; the operator-only conclusive mode releases the positively
|
|
16208
|
+
* spent subset while retaining and reporting unknowns.
|
|
16209
|
+
*/
|
|
16210
|
+
async function reviewUtxoOutputs(storage, auth, outputs, releaseMode = "none") {
|
|
16211
|
+
const classifications = await classifyCandidates(storage, outputs);
|
|
16212
|
+
const confirmedSpentOutputs = classifications.filter((result) => result.status.verdict === "spent").map((result) => result.output);
|
|
16213
|
+
const unknownOutputs = classifications.filter((result) => result.status.verdict === "unknown").map((result) => result.output);
|
|
16214
|
+
if (releaseMode === "atomic" && unknownOutputs.length > 0) {
|
|
16215
|
+
const diagnostics = diagnosticsFor(classifications, /* @__PURE__ */ new Set());
|
|
16216
|
+
const now = /* @__PURE__ */ new Date();
|
|
16217
|
+
await storage.insertMonitorEvent({
|
|
16218
|
+
created_at: now,
|
|
16219
|
+
updated_at: now,
|
|
16220
|
+
id: 0,
|
|
16221
|
+
event: "InvalidChangeReleaseBlocked",
|
|
16222
|
+
details: auditDetails(diagnostics, auth.userId, releaseMode, "inconclusive-provider-result")
|
|
15395
16223
|
});
|
|
15396
|
-
|
|
15397
|
-
if (active.length >= maxConcurrency) await Promise.race(active);
|
|
16224
|
+
throw new WERR_UTXO_REVIEW_INCONCLUSIVE(diagnostics.checked, diagnostics.confirmedSpent, diagnostics.unknown);
|
|
15398
16225
|
}
|
|
15399
|
-
await
|
|
16226
|
+
const releasedOutputIds = releaseMode === "none" ? /* @__PURE__ */ new Set() : await releaseConfirmedSpent(storage, auth, classifications, releaseMode);
|
|
16227
|
+
for (const output of confirmedSpentOutputs) if (releasedOutputIds.has(output.outputId)) output.spendable = false;
|
|
16228
|
+
return {
|
|
16229
|
+
classifications,
|
|
16230
|
+
confirmedSpentOutputs,
|
|
16231
|
+
unknownOutputs,
|
|
16232
|
+
diagnostics: diagnosticsFor(classifications, releasedOutputIds)
|
|
16233
|
+
};
|
|
15400
16234
|
}
|
|
16235
|
+
//#endregion
|
|
16236
|
+
//#region ../src/storage/methods/ListOutputsSpecOp.ts
|
|
15401
16237
|
const getBasketToSpecOp = () => {
|
|
15402
16238
|
return {
|
|
15403
16239
|
[specOpWalletBalance]: {
|
|
@@ -15428,23 +16264,7 @@ const getBasketToSpecOp = () => {
|
|
|
15428
16264
|
includeSpent: false,
|
|
15429
16265
|
tagsToIntercept: ["release", "all"],
|
|
15430
16266
|
filterOutputs: async (s, auth, vargs, specOpTags, outputs) => {
|
|
15431
|
-
|
|
15432
|
-
const invalidOutputIds = /* @__PURE__ */ new Set();
|
|
15433
|
-
const services = s.getServices();
|
|
15434
|
-
await runWithConcurrency(outputs, INVALID_CHANGE_MAX_CONCURRENCY, async (o) => {
|
|
15435
|
-
if (!o.basketId) return;
|
|
15436
|
-
await s.validateOutputScript(o);
|
|
15437
|
-
let ok = false;
|
|
15438
|
-
if (o.lockingScript != null && o.lockingScript.length > 0) ok = await services.isUtxo(o);
|
|
15439
|
-
else ok = void 0;
|
|
15440
|
-
if (ok === false) invalidOutputIds.add(o.outputId);
|
|
15441
|
-
});
|
|
15442
|
-
const filteredOutputs = outputs.filter((o) => invalidOutputIds.has(o.outputId));
|
|
15443
|
-
if (specOpTags.includes("release")) await runWithConcurrency(filteredOutputs, INVALID_CHANGE_MAX_CONCURRENCY, async (o) => {
|
|
15444
|
-
await s.updateOutput(o.outputId, { spendable: false });
|
|
15445
|
-
o.spendable = false;
|
|
15446
|
-
});
|
|
15447
|
-
return filteredOutputs;
|
|
16267
|
+
return (await reviewUtxoOutputs(s, auth, outputs, specOpTags.includes("release") ? "atomic" : "none")).confirmedSpentOutputs;
|
|
15448
16268
|
}
|
|
15449
16269
|
},
|
|
15450
16270
|
[specOpSetWalletChangeParams]: {
|
|
@@ -15597,7 +16417,7 @@ async function loadIdbOutputs(query) {
|
|
|
15597
16417
|
"nosend",
|
|
15598
16418
|
"sending"
|
|
15599
16419
|
],
|
|
15600
|
-
noScript: true,
|
|
16420
|
+
noScript: specOp?.includeOutputScripts !== true,
|
|
15601
16421
|
orderDescending
|
|
15602
16422
|
};
|
|
15603
16423
|
const pageManagedChange = specOp?.managedChangeOnly === true && specOp.ignoreLimit !== true;
|
|
@@ -15679,7 +16499,10 @@ async function listOutputsIdb(storage, auth, vargs, _originator) {
|
|
|
15679
16499
|
});
|
|
15680
16500
|
result.totalOutputs = totalOutputs;
|
|
15681
16501
|
if (specOp != null) {
|
|
15682
|
-
if (specOp.filterOutputs != null)
|
|
16502
|
+
if (specOp.filterOutputs != null) {
|
|
16503
|
+
outputs = await specOp.filterOutputs(storage, auth, vargs, specOpTags, outputs);
|
|
16504
|
+
result.totalOutputs = outputs.length;
|
|
16505
|
+
}
|
|
15683
16506
|
if (specOp.resultFromOutputs != null) return specOp.resultFromOutputs(storage, auth, vargs, specOpTags, outputs);
|
|
15684
16507
|
}
|
|
15685
16508
|
await hydrateIdbOutputResult(storage, outputs, vargs, result);
|
|
@@ -15810,6 +16633,7 @@ async function scanCursor(cursor, since, offset, limit, matches, accept) {
|
|
|
15810
16633
|
var StorageIdb = class extends StorageProvider {
|
|
15811
16634
|
dbName;
|
|
15812
16635
|
db;
|
|
16636
|
+
managedChangeDefaultsMigrated = false;
|
|
15813
16637
|
constructor(options) {
|
|
15814
16638
|
super(options);
|
|
15815
16639
|
this.dbName = `wallet-toolbox-${this.chain}net`;
|
|
@@ -15847,6 +16671,7 @@ var StorageIdb = class extends StorageProvider {
|
|
|
15847
16671
|
async verifyDB(storageName, storageIdentityKey) {
|
|
15848
16672
|
if (this.db != null) return this.db;
|
|
15849
16673
|
this.db = await this.initDB(storageName, storageIdentityKey);
|
|
16674
|
+
await this.migrateManagedChangeDefaults(this.db);
|
|
15850
16675
|
this._settings = (await this.db.getAll("settings"))[0];
|
|
15851
16676
|
this.whenLastAccess = /* @__PURE__ */ new Date();
|
|
15852
16677
|
return this.db;
|
|
@@ -15880,7 +16705,7 @@ var StorageIdb = class extends StorageProvider {
|
|
|
15880
16705
|
async initDB(storageName, storageIdentityKey) {
|
|
15881
16706
|
const chain = this.chain;
|
|
15882
16707
|
const maxOutputScript = 1024;
|
|
15883
|
-
return await openDB(this.dbName,
|
|
16708
|
+
return await openDB(this.dbName, 4, { upgrade(db, _oldVersion, _newVersion, transaction) {
|
|
15884
16709
|
upgradeAllStoresV1(db);
|
|
15885
16710
|
upgradeActionBatchStoresV2(db);
|
|
15886
16711
|
const outputs = transaction.objectStore("outputs");
|
|
@@ -15906,6 +16731,22 @@ var StorageIdb = class extends StorageProvider {
|
|
|
15906
16731
|
}
|
|
15907
16732
|
} });
|
|
15908
16733
|
}
|
|
16734
|
+
async migrateManagedChangeDefaults(db) {
|
|
16735
|
+
if (this.managedChangeDefaultsMigrated) return;
|
|
16736
|
+
const trx = db.transaction("output_baskets", "readwrite");
|
|
16737
|
+
let cursor = await trx.store.openCursor();
|
|
16738
|
+
while (cursor != null) {
|
|
16739
|
+
const basket = cursor.value;
|
|
16740
|
+
if (isLegacyManagedChangeBasketDefault(basket)) await cursor.update({
|
|
16741
|
+
...basket,
|
|
16742
|
+
minimumDesiredUTXOValue: DEFAULT_MANAGED_CHANGE_MINIMUM_SATOSHIS,
|
|
16743
|
+
updated_at: /* @__PURE__ */ new Date()
|
|
16744
|
+
});
|
|
16745
|
+
cursor = await cursor.continue();
|
|
16746
|
+
}
|
|
16747
|
+
await trx.done;
|
|
16748
|
+
this.managedChangeDefaultsMigrated = true;
|
|
16749
|
+
}
|
|
15909
16750
|
async reviewStatus(args) {
|
|
15910
16751
|
return await reviewStatusIdb(this, args);
|
|
15911
16752
|
}
|
|
@@ -16660,6 +17501,7 @@ var StorageIdb = class extends StorageProvider {
|
|
|
16660
17501
|
if (this.db != null) this.db.close();
|
|
16661
17502
|
this.db = void 0;
|
|
16662
17503
|
this._settings = void 0;
|
|
17504
|
+
this.managedChangeDefaultsMigrated = false;
|
|
16663
17505
|
}
|
|
16664
17506
|
allStores = [
|
|
16665
17507
|
"action_batches",
|
|
@@ -17832,6 +18674,9 @@ var StorageClientBase = class {
|
|
|
17832
18674
|
async renewActionBatch(auth, batchId) {
|
|
17833
18675
|
return await this.rpcCall("renewActionBatch", [auth, batchId]);
|
|
17834
18676
|
}
|
|
18677
|
+
async resumeActionBatch(auth, args) {
|
|
18678
|
+
return await this.rpcCall("resumeActionBatch", [auth, args]);
|
|
18679
|
+
}
|
|
17835
18680
|
async prepareActionBatchCommit(auth, manifest) {
|
|
17836
18681
|
return await this.rpcCall("prepareActionBatchCommit", [auth, manifest]);
|
|
17837
18682
|
}
|
|
@@ -18636,7 +19481,7 @@ async function restoreBRC38(storage, data) {
|
|
|
18636
19481
|
await storage.transaction(async (trx) => {
|
|
18637
19482
|
await storage.insertUser({ ...data.user }, trx);
|
|
18638
19483
|
for (const row of data.provenTxs) await storage.insertProvenTx({ ...row }, trx);
|
|
18639
|
-
for (const row of data.outputBaskets) await storage.insertOutputBasket({ ...row }, trx);
|
|
19484
|
+
for (const row of data.outputBaskets) await storage.insertOutputBasket(upgradeLegacyManagedChangeBasketDefault({ ...row }), trx);
|
|
18640
19485
|
for (const row of data.outputTags) await storage.insertOutputTag({ ...row }, trx);
|
|
18641
19486
|
for (const row of data.txLabels) await storage.insertTxLabel({ ...row }, trx);
|
|
18642
19487
|
for (const row of data.transactions) await storage.insertTransaction({ ...row }, trx);
|
|
@@ -21695,13 +22540,37 @@ function configuredChaintracksClient(chain, serviceUrl) {
|
|
|
21695
22540
|
return new ChaintracksServiceClient(chain, serviceUrl);
|
|
21696
22541
|
}
|
|
21697
22542
|
/**
|
|
22543
|
+
* True when running under a browser-style runtime — a real browser tab, or
|
|
22544
|
+
* the embedded webview a desktop wallet shell hosts its wallet logic in
|
|
22545
|
+
* (Metanet Client / User Wallet are Tauri WKWebViews) — where `fetch` is
|
|
22546
|
+
* subject to CORS enforcement that Node-style runtimes do not apply.
|
|
22547
|
+
*/
|
|
22548
|
+
function isBrowserRuntime() {
|
|
22549
|
+
return typeof window !== "undefined" && window.document !== void 0;
|
|
22550
|
+
}
|
|
22551
|
+
/**
|
|
21698
22552
|
* Returns the credential-free default ChainTracks client for a supported
|
|
21699
22553
|
* public network, or an operator-configured client for stn/tstn.
|
|
22554
|
+
*
|
|
22555
|
+
* BROWSER RUNTIMES get the legacy CORS-enabled Chaintracks service for
|
|
22556
|
+
* main/test. The Go Chaintracks deployments (`arcade-v2-*.bsvblockchain.tech`)
|
|
22557
|
+
* currently serve no `Access-Control-Allow-Origin` header and answer OPTIONS
|
|
22558
|
+
* preflights with 404 (verified live 2026-08-11), so every fetch from a
|
|
22559
|
+
* browser-hosted wallet is CORS-blocked (WebKit surfaces it as
|
|
22560
|
+
* `TypeError: Load failed`) and the wallet loses `getHeight`, headers and
|
|
22561
|
+
* merkle-root validation wholesale. The repository service contract
|
|
22562
|
+
* (AGENTS.md: "browser, mobile, and unknown-domain clients must not be
|
|
22563
|
+
* silently blocked by CORS") requires a default that browsers can actually
|
|
22564
|
+
* reach. Once the Go deployments serve CORS (and a browser-run conformance
|
|
22565
|
+
* check proves it), this branch can be removed and browsers can share the v2
|
|
22566
|
+
* default. Node runtimes are unchanged.
|
|
21700
22567
|
*/
|
|
21701
22568
|
function createDefaultChaintracksClient(chain) {
|
|
21702
22569
|
switch (chain) {
|
|
21703
22570
|
case "main":
|
|
21704
22571
|
case "test":
|
|
22572
|
+
if (isBrowserRuntime()) return new ChaintracksServiceClient(chain, `https://${chain}net-chaintracks.babbage.systems`);
|
|
22573
|
+
return new GoChaintracksServiceClient(chain, arcadeDefaultUrl(chain), { apiPrefix: "/chaintracks/v2" });
|
|
21705
22574
|
case "ttn": return new GoChaintracksServiceClient(chain, arcadeDefaultUrl(chain), { apiPrefix: "/chaintracks/v2" });
|
|
21706
22575
|
case "stn": return configuredChaintracksClient(chain, stnChaintracksUrl());
|
|
21707
22576
|
case "tstn": return configuredChaintracksClient(chain, tstnChaintracksUrl());
|
|
@@ -21740,7 +22609,7 @@ function createDefaultWalletServicesOptions(...[chain, arcCallbackUrl, arcCallba
|
|
|
21740
22609
|
exchangeratesapiKey: void 0,
|
|
21741
22610
|
chaintracksFiatExchangeRatesUrl,
|
|
21742
22611
|
chaintracks,
|
|
21743
|
-
arcUrl: arcDefaultUrl(chain),
|
|
22612
|
+
arcUrl: chain === "ttn" ? "" : arcDefaultUrl(chain),
|
|
21744
22613
|
arcConfig: {
|
|
21745
22614
|
apiKey: taalArcApiKey ?? void 0,
|
|
21746
22615
|
deploymentId,
|
|
@@ -21756,7 +22625,7 @@ function createDefaultWalletServicesOptions(...[chain, arcCallbackUrl, arcCallba
|
|
|
21756
22625
|
},
|
|
21757
22626
|
bitailsApiKey
|
|
21758
22627
|
};
|
|
21759
|
-
const resolvedArcadeUrl = arcadeUrl;
|
|
22628
|
+
const resolvedArcadeUrl = arcadeUrl ?? (chain === "ttn" ? arcadeDefaultUrl(chain) : void 0);
|
|
21760
22629
|
if (resolvedArcadeUrl != null && resolvedArcadeUrl !== "") {
|
|
21761
22630
|
o.arcadeUrl = resolvedArcadeUrl;
|
|
21762
22631
|
o.arcadeConfig = {
|
|
@@ -21786,7 +22655,7 @@ function arcDefaultUrl(chain) {
|
|
|
21786
22655
|
case "main": return "https://arc.taal.com";
|
|
21787
22656
|
case "test": return "https://arc-test.taal.com";
|
|
21788
22657
|
case "stn": return stnArcadeUrl() ?? "";
|
|
21789
|
-
case "ttn": return "
|
|
22658
|
+
case "ttn": return "";
|
|
21790
22659
|
case "tstn": return tstnArcadeUrl() ?? "";
|
|
21791
22660
|
case "mock": return "";
|
|
21792
22661
|
}
|
|
@@ -22158,6 +23027,40 @@ var ARC = class {
|
|
|
22158
23027
|
}
|
|
22159
23028
|
};
|
|
22160
23029
|
//#endregion
|
|
23030
|
+
//#region ../src/services/providers/arcadeStatus.ts
|
|
23031
|
+
/**
|
|
23032
|
+
* Canonical classification shared by Arcade polling and SSE. Keeping this in
|
|
23033
|
+
* one place prevents a provider from calling a rejection "unknown" while the
|
|
23034
|
+
* monitor's event path calls the same status terminal.
|
|
23035
|
+
*/
|
|
23036
|
+
function classifyArcadeRejection(event) {
|
|
23037
|
+
const extraInfo = event.extraInfo?.trim() ?? "";
|
|
23038
|
+
const detail = extraInfo.toLowerCase();
|
|
23039
|
+
if (event.status === 476 || detail.startsWith("parent rejected") || detail.includes("not final") || detail.includes("non-final")) return {
|
|
23040
|
+
terminal: false,
|
|
23041
|
+
inputConflict: false,
|
|
23042
|
+
reqStatus: "invalid",
|
|
23043
|
+
reason: event.status === 476 ? "transaction is not final" : "Arcade reported a retryable parent/locktime condition"
|
|
23044
|
+
};
|
|
23045
|
+
const orphanConflict = isArcDoubleSpendTxStatus(event.txStatus);
|
|
23046
|
+
const inputConflict = orphanConflict || event.status === 462 || event.status === 466 || /(?:utxo|input).*(?:spent|missing|conflict)|missing[- ]inputs?|already spent/.test(detail);
|
|
23047
|
+
const classifiedValidatorFailure = event.status != null && event.status >= 460 && event.status <= 475;
|
|
23048
|
+
const explicitInvalidStatus = isArcInvalidTxStatus(event.txStatus) && event.txStatus !== "REJECTED";
|
|
23049
|
+
const terminal = inputConflict || classifiedValidatorFailure || explicitInvalidStatus || extraInfo !== "";
|
|
23050
|
+
let reason = "Arcade supplied no durable rejection evidence";
|
|
23051
|
+
if (orphanConflict) reason = `Arcade reported ${event.txStatus}`;
|
|
23052
|
+
else if (inputConflict) reason = "Arcade supplied confirmed missing-input/conflict evidence";
|
|
23053
|
+
else if (classifiedValidatorFailure) reason = `Arcade supplied terminal validator code ${String(event.status)}`;
|
|
23054
|
+
else if (explicitInvalidStatus) reason = `Arcade reported terminal ${event.txStatus}`;
|
|
23055
|
+
else if (extraInfo !== "") reason = "Arcade supplied a terminal validator rejection reason";
|
|
23056
|
+
return {
|
|
23057
|
+
terminal,
|
|
23058
|
+
inputConflict,
|
|
23059
|
+
reqStatus: inputConflict ? "doubleSpend" : "invalid",
|
|
23060
|
+
reason
|
|
23061
|
+
};
|
|
23062
|
+
}
|
|
23063
|
+
//#endregion
|
|
22161
23064
|
//#region ../src/services/providers/Arcade.ts
|
|
22162
23065
|
function defaultDeploymentId() {
|
|
22163
23066
|
return `ts-sdk-${Utils.toHex(Random(16))}`;
|
|
@@ -22427,6 +23330,73 @@ var Arcade = class {
|
|
|
22427
23330
|
return (await this.httpClient.request(`${this.URL}/tx/${txid}`, requestOptions)).data;
|
|
22428
23331
|
}
|
|
22429
23332
|
/**
|
|
23333
|
+
* Adapt Arcade's lifecycle endpoint to the shared transaction-status
|
|
23334
|
+
* provider contract. This lets monitor reconciliation remain operational
|
|
23335
|
+
* when an explorer such as WhatsOnChain is absent. Only network-observed
|
|
23336
|
+
* states count as known; RECEIVED/PENDING_RETRY and terminal rejection
|
|
23337
|
+
* states remain unknown, while MINED/IMMUTABLE are authoritative mined
|
|
23338
|
+
* observations whose proof is validated separately.
|
|
23339
|
+
*/
|
|
23340
|
+
async getStatusForTxids(txids) {
|
|
23341
|
+
const r = {
|
|
23342
|
+
name: this.name,
|
|
23343
|
+
status: "success",
|
|
23344
|
+
results: []
|
|
23345
|
+
};
|
|
23346
|
+
let firstError;
|
|
23347
|
+
r.results = await Promise.all(txids.map(async (txid) => {
|
|
23348
|
+
try {
|
|
23349
|
+
const response = await this.httpClient.request(`${this.URL}/tx/${txid}`, {
|
|
23350
|
+
method: "GET",
|
|
23351
|
+
headers: this.requestHeaders()
|
|
23352
|
+
});
|
|
23353
|
+
if (Number(response.status) === 404) return {
|
|
23354
|
+
txid,
|
|
23355
|
+
status: "unknown",
|
|
23356
|
+
depth: void 0
|
|
23357
|
+
};
|
|
23358
|
+
if (!response.ok || Number(response.status) !== 200) throw new WERR_INVALID_OPERATION(`Arcade transaction status response ${String(response.status)}`);
|
|
23359
|
+
const status = response.data.txStatus;
|
|
23360
|
+
if (status === "MINED" || status === "IMMUTABLE") return {
|
|
23361
|
+
txid,
|
|
23362
|
+
status: "mined",
|
|
23363
|
+
depth: 1
|
|
23364
|
+
};
|
|
23365
|
+
if (status === "ACCEPTED_BY_NETWORK" || status === "SEEN_ON_NETWORK" || status === "SEEN_MULTIPLE_NODES") return {
|
|
23366
|
+
txid,
|
|
23367
|
+
status: "known",
|
|
23368
|
+
depth: 0
|
|
23369
|
+
};
|
|
23370
|
+
const classification = classifyArcadeRejection(response.data);
|
|
23371
|
+
return {
|
|
23372
|
+
txid,
|
|
23373
|
+
status: "unknown",
|
|
23374
|
+
depth: void 0,
|
|
23375
|
+
providerStatus: status,
|
|
23376
|
+
...classification.terminal ? {
|
|
23377
|
+
terminal: true,
|
|
23378
|
+
inputConflict: classification.inputConflict,
|
|
23379
|
+
statusCode: response.data.status,
|
|
23380
|
+
description: (response.data.extraInfo || classification.reason).slice(0, 512),
|
|
23381
|
+
competingTxs: (response.data.competingTxs ?? []).slice(0, 24)
|
|
23382
|
+
} : {}
|
|
23383
|
+
};
|
|
23384
|
+
} catch (error_) {
|
|
23385
|
+
firstError ??= WalletError.fromUnknown(error_);
|
|
23386
|
+
return {
|
|
23387
|
+
txid,
|
|
23388
|
+
status: "unknown",
|
|
23389
|
+
depth: void 0
|
|
23390
|
+
};
|
|
23391
|
+
}
|
|
23392
|
+
}));
|
|
23393
|
+
if (firstError != null) {
|
|
23394
|
+
r.status = "error";
|
|
23395
|
+
r.error = firstError;
|
|
23396
|
+
}
|
|
23397
|
+
return r;
|
|
23398
|
+
}
|
|
23399
|
+
/**
|
|
22430
23400
|
* `getMerklePath` provider: obtain a BUMP merkle proof for a mined transaction from Arcade.
|
|
22431
23401
|
*
|
|
22432
23402
|
* Arcade only has a proof for transactions it tracked (i.e. broadcast through it) that have
|
|
@@ -23217,6 +24187,10 @@ var Services = class Services {
|
|
|
23217
24187
|
name: "WhatsOnChain",
|
|
23218
24188
|
service: this.whatsonchain.getStatusForTxids.bind(this.whatsonchain)
|
|
23219
24189
|
});
|
|
24190
|
+
if (this.arcade != null) this.getStatusForTxidsServices.add({
|
|
24191
|
+
name: "Arcade",
|
|
24192
|
+
service: this.arcade.getStatusForTxids.bind(this.arcade)
|
|
24193
|
+
});
|
|
23220
24194
|
this.getScriptHashHistoryServices = new ServiceCollection("getScriptHashHistory");
|
|
23221
24195
|
if (hasWhatsOnChain) this.getScriptHashHistoryServices.add({
|
|
23222
24196
|
name: "WhatsOnChain",
|
|
@@ -23233,7 +24207,7 @@ var Services = class Services {
|
|
|
23233
24207
|
name: "GorillaPoolArcBeef",
|
|
23234
24208
|
service: this.arcGorillaPool.postBeef.bind(this.arcGorillaPool)
|
|
23235
24209
|
});
|
|
23236
|
-
this.postBeefServices.add({
|
|
24210
|
+
if (this.options.arcUrl != null && this.options.arcUrl !== "") this.postBeefServices.add({
|
|
23237
24211
|
name: "TaalArcBeef",
|
|
23238
24212
|
service: this.arcTaal.postBeef.bind(this.arcTaal)
|
|
23239
24213
|
});
|
|
@@ -23319,30 +24293,65 @@ var Services = class Services {
|
|
|
23319
24293
|
async getStatusForTxids(txids, useNext) {
|
|
23320
24294
|
const services = this.getStatusForTxidsServices;
|
|
23321
24295
|
if (useNext === true) services.next();
|
|
23322
|
-
let
|
|
24296
|
+
let fallback = {
|
|
23323
24297
|
name: "<noservices>",
|
|
23324
24298
|
status: "error",
|
|
23325
24299
|
error: new WERR_INTERNAL("No services available."),
|
|
23326
24300
|
results: []
|
|
23327
24301
|
};
|
|
24302
|
+
const resultsByTxid = /* @__PURE__ */ new Map();
|
|
24303
|
+
const unresolved = new Set(txids);
|
|
24304
|
+
const providerNames = [];
|
|
24305
|
+
let successfulProvider = false;
|
|
24306
|
+
const rank = (result) => {
|
|
24307
|
+
if (result.status === "mined") return 4;
|
|
24308
|
+
if (result.status === "known") return 3;
|
|
24309
|
+
if (result.terminal === true) return 2;
|
|
24310
|
+
return 1;
|
|
24311
|
+
};
|
|
23328
24312
|
for (let tries = 0; tries < services.count; tries++) {
|
|
23329
24313
|
const stc = services.serviceToCall;
|
|
23330
24314
|
try {
|
|
23331
|
-
const
|
|
24315
|
+
const requestedTxids = [...unresolved];
|
|
24316
|
+
if (requestedTxids.length === 0) break;
|
|
24317
|
+
const r = await this.getStatusForTxidsBatched(stc, requestedTxids);
|
|
23332
24318
|
if (r.status === "success") {
|
|
23333
24319
|
services.addServiceCallSuccess(stc);
|
|
23334
|
-
|
|
23335
|
-
|
|
24320
|
+
successfulProvider = true;
|
|
24321
|
+
providerNames.push(r.name);
|
|
24322
|
+
for (const result of r.results) {
|
|
24323
|
+
const current = resultsByTxid.get(result.txid);
|
|
24324
|
+
if (current == null || rank(result) > rank(current)) resultsByTxid.set(result.txid, result);
|
|
24325
|
+
if (result.status === "mined" || result.status === "known") unresolved.delete(result.txid);
|
|
24326
|
+
}
|
|
24327
|
+
services.next();
|
|
24328
|
+
continue;
|
|
23336
24329
|
}
|
|
24330
|
+
fallback = r;
|
|
23337
24331
|
if (r.error != null) services.addServiceCallError(stc, r.error);
|
|
23338
24332
|
else services.addServiceCallFailure(stc);
|
|
23339
24333
|
} catch (error_) {
|
|
23340
24334
|
const e = WalletError.fromUnknown(error_);
|
|
24335
|
+
fallback = {
|
|
24336
|
+
name: stc.providerName,
|
|
24337
|
+
status: "error",
|
|
24338
|
+
error: e,
|
|
24339
|
+
results: []
|
|
24340
|
+
};
|
|
23341
24341
|
services.addServiceCallError(stc, e);
|
|
23342
24342
|
}
|
|
23343
24343
|
services.next();
|
|
23344
24344
|
}
|
|
23345
|
-
return
|
|
24345
|
+
if (!successfulProvider) return fallback;
|
|
24346
|
+
return {
|
|
24347
|
+
name: [...new Set(providerNames)].join(","),
|
|
24348
|
+
status: "success",
|
|
24349
|
+
results: txids.map((txid) => resultsByTxid.get(txid) ?? {
|
|
24350
|
+
txid,
|
|
24351
|
+
status: "unknown",
|
|
24352
|
+
depth: void 0
|
|
24353
|
+
})
|
|
24354
|
+
};
|
|
23346
24355
|
}
|
|
23347
24356
|
async getStatusForTxidsBatched(stc, txids) {
|
|
23348
24357
|
const results = [];
|
|
@@ -23369,9 +24378,7 @@ var Services = class Services {
|
|
|
23369
24378
|
return Utils.toHex(sha256Hash(Utils.toArray(script, "hex")));
|
|
23370
24379
|
}
|
|
23371
24380
|
async isUtxo(output) {
|
|
23372
|
-
|
|
23373
|
-
const hash = this.hashOutputScript(Utils.toHex(output.lockingScript));
|
|
23374
|
-
return (await this.getUtxoStatus(hash, void 0, `${output.txid ?? ""}.${output.vout}`)).isUtxo === true;
|
|
24381
|
+
return requireConclusiveUtxo(await classifyOutputUtxo(this, output));
|
|
23375
24382
|
}
|
|
23376
24383
|
async getUtxoStatus(output, outputFormat, outpoint, useNext, logger) {
|
|
23377
24384
|
const services = this.getUtxoStatusServices;
|
|
@@ -23597,7 +24604,15 @@ var Services = class Services {
|
|
|
23597
24604
|
const method = async () => {
|
|
23598
24605
|
return await this.options.chaintracks.currentHeight();
|
|
23599
24606
|
};
|
|
23600
|
-
|
|
24607
|
+
try {
|
|
24608
|
+
return await this.invokeChaintracksWithRetry(method, "current_height");
|
|
24609
|
+
} catch (error_) {
|
|
24610
|
+
try {
|
|
24611
|
+
return (await this.whatsonchain.getChainInfo()).blocks;
|
|
24612
|
+
} catch {
|
|
24613
|
+
throw error_;
|
|
24614
|
+
}
|
|
24615
|
+
}
|
|
23601
24616
|
}
|
|
23602
24617
|
async hashToHeader(hash) {
|
|
23603
24618
|
const method = async () => {
|
|
@@ -27316,17 +28331,34 @@ var ArcSSEClient = class {
|
|
|
27316
28331
|
console.log(`${TAG} connected`);
|
|
27317
28332
|
});
|
|
27318
28333
|
this.es.addEventListener("status", (event) => {
|
|
28334
|
+
let data;
|
|
27319
28335
|
try {
|
|
27320
|
-
|
|
27321
|
-
console.log(`${TAG} event: txid=${data.txid} status=${data.txStatus}`);
|
|
27322
|
-
if (typeof event.lastEventId === "string" && event.lastEventId !== "") {
|
|
27323
|
-
this._lastEventId = event.lastEventId;
|
|
27324
|
-
this.options.onLastEventIdChanged?.(event.lastEventId);
|
|
27325
|
-
}
|
|
27326
|
-
this.options.onEvent(data);
|
|
28336
|
+
data = JSON.parse(event.data);
|
|
27327
28337
|
} catch {
|
|
27328
28338
|
console.log(`${TAG} malformed event: ${String(event.data).substring(0, 200)}`);
|
|
28339
|
+
return;
|
|
28340
|
+
}
|
|
28341
|
+
console.log(`${TAG} event: txid=${data.txid} status=${data.txStatus}`);
|
|
28342
|
+
const receivedEventId = typeof event.lastEventId === "string" && event.lastEventId !== "" ? event.lastEventId : void 0;
|
|
28343
|
+
data.eventId = receivedEventId;
|
|
28344
|
+
let processing;
|
|
28345
|
+
try {
|
|
28346
|
+
processing = this.options.onEvent(data);
|
|
28347
|
+
} catch (error) {
|
|
28348
|
+
const e = error instanceof Error ? error : new Error(String(error));
|
|
28349
|
+
console.log(`${TAG} event processing failed: ${e.message}`);
|
|
28350
|
+
this.options.onError?.(e);
|
|
28351
|
+
return;
|
|
27329
28352
|
}
|
|
28353
|
+
Promise.resolve(processing).then(async () => {
|
|
28354
|
+
if (receivedEventId == null) return;
|
|
28355
|
+
await this.options.onLastEventIdChanged?.(receivedEventId);
|
|
28356
|
+
this._lastEventId = receivedEventId;
|
|
28357
|
+
}).catch((error) => {
|
|
28358
|
+
const e = error instanceof Error ? error : new Error(String(error));
|
|
28359
|
+
console.log(`${TAG} event processing failed: ${e.message}`);
|
|
28360
|
+
this.options.onError?.(e);
|
|
28361
|
+
});
|
|
27330
28362
|
});
|
|
27331
28363
|
this.es.addEventListener("error", (event) => {
|
|
27332
28364
|
console.log(`${TAG} error:`, JSON.stringify(event));
|
|
@@ -27361,94 +28393,6 @@ var ArcSSEClient = class {
|
|
|
27361
28393
|
}
|
|
27362
28394
|
};
|
|
27363
28395
|
//#endregion
|
|
27364
|
-
//#region ../src/monitor/tasks/TaskUnFail.ts
|
|
27365
|
-
/**
|
|
27366
|
-
* Setting provenTxReq status to 'unfail' when 'invalid' will attempt to find a merklePath, and if successful:
|
|
27367
|
-
*
|
|
27368
|
-
* 1. set the req status to 'unmined'
|
|
27369
|
-
* 2. set the referenced txs to 'unproven'
|
|
27370
|
-
* 3. determine if any inputs match user's existing outputs and if so update spentBy and spendable of those outputs.
|
|
27371
|
-
* 4. set the txs outputs to spendable
|
|
27372
|
-
*
|
|
27373
|
-
* If it fails (to find a merklePath), returns the req status to 'invalid'.
|
|
27374
|
-
*/
|
|
27375
|
-
var TaskUnFail = class TaskUnFail extends WalletMonitorTask {
|
|
27376
|
-
triggerMsecs;
|
|
27377
|
-
static taskName = "UnFail";
|
|
27378
|
-
/**
|
|
27379
|
-
* Set to true to trigger running this task
|
|
27380
|
-
*/
|
|
27381
|
-
static checkNowRequested = false;
|
|
27382
|
-
static get checkNow() {
|
|
27383
|
-
return this.checkNowRequested;
|
|
27384
|
-
}
|
|
27385
|
-
static set checkNow(value) {
|
|
27386
|
-
this.checkNowRequested = value;
|
|
27387
|
-
}
|
|
27388
|
-
constructor(monitor, triggerMsecs = Monitor.oneMinute * 10) {
|
|
27389
|
-
super(monitor, TaskUnFail.taskName);
|
|
27390
|
-
this.triggerMsecs = triggerMsecs;
|
|
27391
|
-
}
|
|
27392
|
-
trigger(nowMsecsSinceEpoch) {
|
|
27393
|
-
return { run: TaskUnFail.checkNow || this.triggerMsecs > 0 && nowMsecsSinceEpoch - this.lastRunMsecsSinceEpoch > this.triggerMsecs };
|
|
27394
|
-
}
|
|
27395
|
-
async runTask() {
|
|
27396
|
-
let log = "";
|
|
27397
|
-
TaskUnFail.checkNow = false;
|
|
27398
|
-
const limit = 100;
|
|
27399
|
-
let offset = 0;
|
|
27400
|
-
for (;;) {
|
|
27401
|
-
const reqs = await this.storage.findProvenTxReqs({
|
|
27402
|
-
partial: {},
|
|
27403
|
-
status: ["unfail"],
|
|
27404
|
-
paged: {
|
|
27405
|
-
limit,
|
|
27406
|
-
offset
|
|
27407
|
-
}
|
|
27408
|
-
});
|
|
27409
|
-
if (reqs.length === 0) break;
|
|
27410
|
-
log += `${reqs.length} reqs with status 'unfail'\n`;
|
|
27411
|
-
const r = await this.unfail(reqs, 2);
|
|
27412
|
-
log += `${r.log}\n`;
|
|
27413
|
-
if (reqs.length < limit) break;
|
|
27414
|
-
offset += limit;
|
|
27415
|
-
}
|
|
27416
|
-
return log;
|
|
27417
|
-
}
|
|
27418
|
-
async unfail(reqs, indent = 0) {
|
|
27419
|
-
let log = "";
|
|
27420
|
-
for (const reqApi of reqs) {
|
|
27421
|
-
const req = new EntityProvenTxReq(reqApi);
|
|
27422
|
-
log += " ".repeat(indent);
|
|
27423
|
-
log += `reqId ${reqApi.provenTxReqId} txid ${reqApi.txid}: `;
|
|
27424
|
-
if ((await this.monitor.services.getMerklePath(req.txid)).merklePath != null) {
|
|
27425
|
-
log += "unfailed. status is now 'unmined'\n";
|
|
27426
|
-
log += await this.unfailReq(req, indent + 2);
|
|
27427
|
-
} else {
|
|
27428
|
-
req.status = "invalid";
|
|
27429
|
-
log += "returned to status 'invalid'\n";
|
|
27430
|
-
await req.updateStorageDynamicProperties(this.storage);
|
|
27431
|
-
}
|
|
27432
|
-
}
|
|
27433
|
-
return { log };
|
|
27434
|
-
}
|
|
27435
|
-
/**
|
|
27436
|
-
* 2. set the referenced txs to 'unproven'
|
|
27437
|
-
* 3. determine if any inputs match user's existing outputs and if so update spentBy and spendable of those outputs.
|
|
27438
|
-
* 4. set the txs outputs to spendable
|
|
27439
|
-
*
|
|
27440
|
-
* @param req
|
|
27441
|
-
* @param indent
|
|
27442
|
-
* @returns
|
|
27443
|
-
*/
|
|
27444
|
-
async unfailReq(req, indent) {
|
|
27445
|
-
return await this.storage.runAsStorageProvider(async (sp) => await sp.unfailTransactionsForProof(req, indent, {
|
|
27446
|
-
status: "unmined",
|
|
27447
|
-
attempts: 0
|
|
27448
|
-
}));
|
|
27449
|
-
}
|
|
27450
|
-
};
|
|
27451
|
-
//#endregion
|
|
27452
28396
|
//#region ../src/monitor/tasks/TaskArcSSE.ts
|
|
27453
28397
|
/**
|
|
27454
28398
|
* Monitor task that receives transaction status updates from Arcade via SSE
|
|
@@ -27492,16 +28436,14 @@ var TaskArcadeSSE = class TaskArcadeSSE extends WalletMonitorTask {
|
|
|
27492
28436
|
arcApiKey: arcadeApiKey,
|
|
27493
28437
|
lastEventId,
|
|
27494
28438
|
EventSourceClass,
|
|
27495
|
-
onEvent: (event) => {
|
|
27496
|
-
this.pendingEvents.push(
|
|
27497
|
-
|
|
28439
|
+
onEvent: async (event) => await new Promise((acknowledge) => {
|
|
28440
|
+
this.pendingEvents.push({
|
|
28441
|
+
event,
|
|
28442
|
+
acknowledge
|
|
28443
|
+
});
|
|
28444
|
+
}),
|
|
27498
28445
|
onError: (err) => {
|
|
27499
28446
|
this.logSetupEvent(`SSE error: ${err.message}`);
|
|
27500
|
-
},
|
|
27501
|
-
onLastEventIdChanged: (id) => {
|
|
27502
|
-
this.monitor.options.saveLastSSEEventId?.(id).catch((e) => {
|
|
27503
|
-
this.logSetupEvent(`failed to persist lastEventId: ${stringifyError(e)}`);
|
|
27504
|
-
});
|
|
27505
28447
|
}
|
|
27506
28448
|
});
|
|
27507
28449
|
this.sseClient.connect();
|
|
@@ -27515,10 +28457,21 @@ var TaskArcadeSSE = class TaskArcadeSSE extends WalletMonitorTask {
|
|
|
27515
28457
|
return { run: this.pendingEvents.length > 0 };
|
|
27516
28458
|
}
|
|
27517
28459
|
async runTask() {
|
|
27518
|
-
const
|
|
27519
|
-
if (
|
|
28460
|
+
const eventCount = this.pendingEvents.length;
|
|
28461
|
+
if (eventCount === 0) return "";
|
|
27520
28462
|
let log = "";
|
|
27521
|
-
for (
|
|
28463
|
+
for (let i = 0; i < eventCount; i++) {
|
|
28464
|
+
const pending = this.pendingEvents[0];
|
|
28465
|
+
log += await this.processStatusEvent(pending.event);
|
|
28466
|
+
if (pending.event.eventId != null) try {
|
|
28467
|
+
await this.monitor.options.saveLastSSEEventId?.(pending.event.eventId);
|
|
28468
|
+
} catch (e) {
|
|
28469
|
+
await this.logSetupEvent(`failed to persist lastEventId: ${stringifyError(e)}`);
|
|
28470
|
+
throw e;
|
|
28471
|
+
}
|
|
28472
|
+
this.pendingEvents.shift();
|
|
28473
|
+
pending.acknowledge();
|
|
28474
|
+
}
|
|
27522
28475
|
return log;
|
|
27523
28476
|
}
|
|
27524
28477
|
async fetchNow() {
|
|
@@ -27534,28 +28487,26 @@ var TaskArcadeSSE = class TaskArcadeSSE extends WalletMonitorTask {
|
|
|
27534
28487
|
}
|
|
27535
28488
|
for (const reqApi of reqs) {
|
|
27536
28489
|
const req = new EntityProvenTxReq(reqApi);
|
|
27537
|
-
const
|
|
27538
|
-
if (ProvenTxReqTerminalStatus.includes(req.status) && !
|
|
28490
|
+
const canRecoverTerminalRequest = this.canRecoverTerminalRequest(req, event);
|
|
28491
|
+
if (ProvenTxReqTerminalStatus.includes(req.status) && !canRecoverTerminalRequest) {
|
|
27539
28492
|
log += ` req ${req.id} already terminal: ${req.status}\n`;
|
|
27540
28493
|
continue;
|
|
27541
28494
|
}
|
|
27542
28495
|
const note = {
|
|
27543
28496
|
when: (/* @__PURE__ */ new Date()).toISOString(),
|
|
27544
28497
|
what: "arcSSE",
|
|
27545
|
-
arcStatus: event.txStatus
|
|
28498
|
+
arcStatus: event.txStatus,
|
|
28499
|
+
...event.timestamp !== "" ? { arcTimestamp: event.timestamp } : {},
|
|
28500
|
+
...event.status != null ? { statusCode: event.status } : {},
|
|
28501
|
+
...event.extraInfo != null && event.extraInfo !== "" ? { extraInfo: event.extraInfo.slice(0, 512) } : {}
|
|
27546
28502
|
};
|
|
27547
28503
|
log += await this.applyStatusEvent(req, event, note);
|
|
27548
28504
|
}
|
|
27549
28505
|
this.monitor.callOnTransactionStatusChanged(event.txid, event.txStatus);
|
|
27550
28506
|
return log;
|
|
27551
28507
|
}
|
|
27552
|
-
|
|
27553
|
-
return req.status === "invalid" && (
|
|
27554
|
-
"SENT_TO_NETWORK",
|
|
27555
|
-
"ACCEPTED_BY_NETWORK",
|
|
27556
|
-
"SEEN_ON_NETWORK",
|
|
27557
|
-
"SEEN_MULTIPLE_NODES"
|
|
27558
|
-
].includes(event.txStatus) || ["MINED", "IMMUTABLE"].includes(event.txStatus));
|
|
28508
|
+
canRecoverTerminalRequest(req, event) {
|
|
28509
|
+
return (req.status === "invalid" || req.status === "doubleSpend") && ["MINED", "IMMUTABLE"].includes(event.txStatus);
|
|
27559
28510
|
}
|
|
27560
28511
|
async applyStatusEvent(req, event, note) {
|
|
27561
28512
|
switch (event.txStatus) {
|
|
@@ -27565,11 +28516,16 @@ var TaskArcadeSSE = class TaskArcadeSSE extends WalletMonitorTask {
|
|
|
27565
28516
|
case "SEEN_MULTIPLE_NODES": return await this.applyAcceptedStatus(req, note);
|
|
27566
28517
|
case "MINED":
|
|
27567
28518
|
case "IMMUTABLE": return await this.applyMinedStatus(req, note);
|
|
27568
|
-
case "DOUBLE_SPEND_ATTEMPTED":
|
|
27569
|
-
case "
|
|
28519
|
+
case "DOUBLE_SPEND_ATTEMPTED":
|
|
28520
|
+
case "SEEN_IN_ORPHAN_MEMPOOL": return await this.applyDoubleSpendStatus(req, event, note);
|
|
28521
|
+
case "REJECTED": return await this.applyRejectedStatus(req, event, note);
|
|
28522
|
+
case "RECEIVED":
|
|
28523
|
+
case "PENDING_RETRY":
|
|
28524
|
+
case "STUMP_PROCESSING":
|
|
28525
|
+
case "UNKNOWN":
|
|
27570
28526
|
req.addHistoryNote(note);
|
|
27571
28527
|
await req.updateStorageDynamicProperties(this.storage);
|
|
27572
|
-
return ` req ${req.id}
|
|
28528
|
+
return ` req ${req.id} ${event.txStatus} recorded; awaiting resolution\n`;
|
|
27573
28529
|
default: return ` req ${req.id} unhandled status: ${event.txStatus}\n`;
|
|
27574
28530
|
}
|
|
27575
28531
|
}
|
|
@@ -27577,43 +28533,73 @@ var TaskArcadeSSE = class TaskArcadeSSE extends WalletMonitorTask {
|
|
|
27577
28533
|
if (![
|
|
27578
28534
|
"unsent",
|
|
27579
28535
|
"sending",
|
|
27580
|
-
"callback"
|
|
27581
|
-
"invalid"
|
|
28536
|
+
"callback"
|
|
27582
28537
|
].includes(req.status)) return "";
|
|
27583
|
-
|
|
27584
|
-
let log = "";
|
|
27585
|
-
if (wasInvalid) log += await new TaskUnFail(this.monitor).unfailReq(req, 4);
|
|
27586
|
-
else if (req.notify.transactionIds != null) await this.storage.runAsStorageProvider(async (sp) => {
|
|
28538
|
+
if (req.notify.transactionIds != null) await this.storage.runAsStorageProvider(async (sp) => {
|
|
27587
28539
|
await sp.updateTransactionsStatus(req.notify.transactionIds ?? [], "unproven");
|
|
27588
28540
|
});
|
|
27589
28541
|
req.status = "unmined";
|
|
27590
|
-
if (wasInvalid) req.attempts = 0;
|
|
27591
28542
|
req.wasBroadcast = true;
|
|
27592
28543
|
req.addHistoryNote(note);
|
|
27593
28544
|
await req.updateStorageDynamicProperties(this.storage);
|
|
27594
|
-
return
|
|
28545
|
+
return ` req ${req.id} => unmined\n`;
|
|
27595
28546
|
}
|
|
27596
28547
|
async applyMinedStatus(req, note) {
|
|
27597
|
-
let log = "";
|
|
27598
|
-
if (req.status === "invalid") {
|
|
27599
|
-
log += await new TaskUnFail(this.monitor).unfailReq(req, 4);
|
|
27600
|
-
req.status = "unmined";
|
|
27601
|
-
req.attempts = 0;
|
|
27602
|
-
req.wasBroadcast = true;
|
|
27603
|
-
}
|
|
27604
28548
|
req.addHistoryNote(note);
|
|
27605
28549
|
await req.updateStorageDynamicProperties(this.storage);
|
|
27606
|
-
return
|
|
28550
|
+
return await this.fetchProofFromServices(req);
|
|
27607
28551
|
}
|
|
27608
|
-
async applyDoubleSpendStatus(req, note) {
|
|
27609
|
-
req
|
|
27610
|
-
|
|
27611
|
-
|
|
27612
|
-
|
|
27613
|
-
|
|
27614
|
-
|
|
28552
|
+
async applyDoubleSpendStatus(req, event, note) {
|
|
28553
|
+
return await this.applyTerminalFailure(req, note, classifyArcadeRejection(event));
|
|
28554
|
+
}
|
|
28555
|
+
classifyRejection(event) {
|
|
28556
|
+
return classifyArcadeRejection(event);
|
|
28557
|
+
}
|
|
28558
|
+
async applyRejectedStatus(req, event, note) {
|
|
28559
|
+
const classification = this.classifyRejection(event);
|
|
28560
|
+
if (!classification.terminal) {
|
|
28561
|
+
req.addHistoryNote({
|
|
28562
|
+
...note,
|
|
28563
|
+
what: "arcSSERejectionPending",
|
|
28564
|
+
reason: classification.reason
|
|
28565
|
+
});
|
|
28566
|
+
await req.updateStorageDynamicProperties(this.storage);
|
|
28567
|
+
return ` req ${req.id} rejection recorded; awaiting resolution\n`;
|
|
28568
|
+
}
|
|
28569
|
+
return await this.applyTerminalFailure(req, note, classification);
|
|
28570
|
+
}
|
|
28571
|
+
async applyTerminalFailure(req, note, classification) {
|
|
28572
|
+
let quarantined = {
|
|
28573
|
+
checked: 0,
|
|
28574
|
+
staleConfirmed: 0,
|
|
28575
|
+
staleOutpoints: []
|
|
28576
|
+
};
|
|
28577
|
+
await this.storage.runAsStorageProvider(async (sp) => {
|
|
28578
|
+
await sp.transaction(async (trx) => {
|
|
28579
|
+
req.status = classification.reqStatus;
|
|
28580
|
+
req.addHistoryNote({
|
|
28581
|
+
...note,
|
|
28582
|
+
what: "arcSSETerminalRejection",
|
|
28583
|
+
reason: classification.reason,
|
|
28584
|
+
inputConflict: classification.inputConflict
|
|
28585
|
+
});
|
|
28586
|
+
await req.updateStorageDynamicProperties(sp, trx);
|
|
28587
|
+
const ids = req.notify.transactionIds;
|
|
28588
|
+
if (ids != null) await sp.updateTransactionsStatus(ids, "failed", trx);
|
|
28589
|
+
if (classification.inputConflict) {
|
|
28590
|
+
quarantined = await quarantineReqInputs(req, sp, trx);
|
|
28591
|
+
req.addHistoryNote({
|
|
28592
|
+
when: (/* @__PURE__ */ new Date()).toISOString(),
|
|
28593
|
+
what: "arcSSEInputQuarantine",
|
|
28594
|
+
checked: quarantined.checked,
|
|
28595
|
+
confirmed: quarantined.staleConfirmed,
|
|
28596
|
+
...quarantined.staleOutpoints.length > 0 ? { outpoints: quarantined.staleOutpoints.join(",") } : {}
|
|
28597
|
+
});
|
|
28598
|
+
await req.updateStorageDynamicProperties(sp, trx);
|
|
28599
|
+
}
|
|
28600
|
+
});
|
|
27615
28601
|
});
|
|
27616
|
-
return ` req ${req.id} =>
|
|
28602
|
+
return ` req ${req.id} => ${classification.reqStatus}; quarantined ${quarantined.staleConfirmed} local input copy/copies\n`;
|
|
27617
28603
|
}
|
|
27618
28604
|
/**
|
|
27619
28605
|
* Complete a MINED/IMMUTABLE status by using the configured proof providers.
|
|
@@ -28025,9 +29011,104 @@ var TaskCheckNoSends = class TaskCheckNoSends extends WalletMonitorTask {
|
|
|
28025
29011
|
}
|
|
28026
29012
|
};
|
|
28027
29013
|
//#endregion
|
|
29014
|
+
//#region ../src/monitor/tasks/TaskUnFail.ts
|
|
29015
|
+
/**
|
|
29016
|
+
* Setting provenTxReq status to 'unfail' when 'invalid' will attempt to find a merklePath, and if successful:
|
|
29017
|
+
*
|
|
29018
|
+
* 1. set the req status to 'unmined'
|
|
29019
|
+
* 2. set the referenced txs to 'unproven'
|
|
29020
|
+
* 3. determine if any inputs match user's existing outputs and if so update spentBy and spendable of those outputs.
|
|
29021
|
+
* 4. set the txs outputs to spendable
|
|
29022
|
+
*
|
|
29023
|
+
* If it fails (to find a merklePath), returns the req status to 'invalid'.
|
|
29024
|
+
*/
|
|
29025
|
+
var TaskUnFail = class TaskUnFail extends WalletMonitorTask {
|
|
29026
|
+
triggerMsecs;
|
|
29027
|
+
static taskName = "UnFail";
|
|
29028
|
+
/**
|
|
29029
|
+
* Set to true to trigger running this task
|
|
29030
|
+
*/
|
|
29031
|
+
static checkNowRequested = false;
|
|
29032
|
+
static get checkNow() {
|
|
29033
|
+
return this.checkNowRequested;
|
|
29034
|
+
}
|
|
29035
|
+
static set checkNow(value) {
|
|
29036
|
+
this.checkNowRequested = value;
|
|
29037
|
+
}
|
|
29038
|
+
constructor(monitor, triggerMsecs = Monitor.oneMinute * 10) {
|
|
29039
|
+
super(monitor, TaskUnFail.taskName);
|
|
29040
|
+
this.triggerMsecs = triggerMsecs;
|
|
29041
|
+
}
|
|
29042
|
+
trigger(nowMsecsSinceEpoch) {
|
|
29043
|
+
return { run: TaskUnFail.checkNow || this.triggerMsecs > 0 && nowMsecsSinceEpoch - this.lastRunMsecsSinceEpoch > this.triggerMsecs };
|
|
29044
|
+
}
|
|
29045
|
+
async runTask() {
|
|
29046
|
+
let log = "";
|
|
29047
|
+
TaskUnFail.checkNow = false;
|
|
29048
|
+
const limit = 100;
|
|
29049
|
+
let offset = 0;
|
|
29050
|
+
for (;;) {
|
|
29051
|
+
const reqs = await this.storage.findProvenTxReqs({
|
|
29052
|
+
partial: {},
|
|
29053
|
+
status: ["unfail"],
|
|
29054
|
+
paged: {
|
|
29055
|
+
limit,
|
|
29056
|
+
offset
|
|
29057
|
+
}
|
|
29058
|
+
});
|
|
29059
|
+
if (reqs.length === 0) break;
|
|
29060
|
+
log += `${reqs.length} reqs with status 'unfail'\n`;
|
|
29061
|
+
const r = await this.unfail(reqs, 2);
|
|
29062
|
+
log += `${r.log}\n`;
|
|
29063
|
+
if (reqs.length < limit) break;
|
|
29064
|
+
offset += limit;
|
|
29065
|
+
}
|
|
29066
|
+
return log;
|
|
29067
|
+
}
|
|
29068
|
+
async unfail(reqs, indent = 0) {
|
|
29069
|
+
let log = "";
|
|
29070
|
+
for (const reqApi of reqs) {
|
|
29071
|
+
const req = new EntityProvenTxReq(reqApi);
|
|
29072
|
+
log += " ".repeat(indent);
|
|
29073
|
+
log += `reqId ${reqApi.provenTxReqId} txid ${reqApi.txid}: `;
|
|
29074
|
+
if ((await this.monitor.services.getMerklePath(req.txid)).merklePath != null) {
|
|
29075
|
+
log += "unfailed. status is now 'unmined'\n";
|
|
29076
|
+
log += await this.unfailReq(req, indent + 2);
|
|
29077
|
+
} else {
|
|
29078
|
+
req.status = "invalid";
|
|
29079
|
+
log += "returned to status 'invalid'\n";
|
|
29080
|
+
await req.updateStorageDynamicProperties(this.storage);
|
|
29081
|
+
}
|
|
29082
|
+
}
|
|
29083
|
+
return { log };
|
|
29084
|
+
}
|
|
29085
|
+
/**
|
|
29086
|
+
* 2. set the referenced txs to 'unproven'
|
|
29087
|
+
* 3. determine if any inputs match user's existing outputs and if so update spentBy and spendable of those outputs.
|
|
29088
|
+
* 4. set the txs outputs to spendable
|
|
29089
|
+
*
|
|
29090
|
+
* @param req
|
|
29091
|
+
* @param indent
|
|
29092
|
+
* @returns
|
|
29093
|
+
*/
|
|
29094
|
+
async unfailReq(req, indent) {
|
|
29095
|
+
return await this.storage.runAsStorageProvider(async (sp) => await sp.unfailTransactionsForProof(req, indent, {
|
|
29096
|
+
status: "unmined",
|
|
29097
|
+
attempts: 0
|
|
29098
|
+
}));
|
|
29099
|
+
}
|
|
29100
|
+
};
|
|
29101
|
+
//#endregion
|
|
28028
29102
|
//#region ../src/monitor/tasks/TaskReviewUtxos.ts
|
|
29103
|
+
const REVIEW_PAGE_DEFAULT_LIMIT = 20;
|
|
29104
|
+
const REVIEW_PAGE_MAX_LIMIT = 250;
|
|
28029
29105
|
/**
|
|
28030
|
-
* Use the reviewByIdentityKey method to
|
|
29106
|
+
* Use the reviewByIdentityKey method to scan the UTXOs of a specific user by
|
|
29107
|
+
* identity key. The scan is read-only unless the caller explicitly requests
|
|
29108
|
+
* release, and release remains blocked if any provider result is inconclusive.
|
|
29109
|
+
* Operator UIs should use reviewPageByIdentityKey: it bounds each provider
|
|
29110
|
+
* round-trip and may explicitly release only the conclusive spent subset while
|
|
29111
|
+
* reporting unknowns.
|
|
28031
29112
|
*
|
|
28032
29113
|
* The task itself is disabled and will not run on a schedule; review must be triggered manually by calling reviewByIdentityKey.
|
|
28033
29114
|
*/
|
|
@@ -28044,7 +29125,7 @@ var TaskReviewUtxos = class TaskReviewUtxos extends WalletMonitorTask {
|
|
|
28044
29125
|
static set checkNow(value) {
|
|
28045
29126
|
this.checkNowRequested = value;
|
|
28046
29127
|
}
|
|
28047
|
-
constructor(monitor, triggerMsecs = 0, userLimit = 10, userOffset = 0, tags = ["
|
|
29128
|
+
constructor(monitor, triggerMsecs = 0, userLimit = 10, userOffset = 0, tags = ["all"]) {
|
|
28048
29129
|
super(monitor, TaskReviewUtxos.taskName);
|
|
28049
29130
|
this.triggerMsecs = triggerMsecs;
|
|
28050
29131
|
this.userLimit = userLimit;
|
|
@@ -28058,8 +29139,8 @@ var TaskReviewUtxos = class TaskReviewUtxos extends WalletMonitorTask {
|
|
|
28058
29139
|
TaskReviewUtxos.checkNow = false;
|
|
28059
29140
|
return "TaskReviewUtxos is disabled; use reviewByIdentityKey instead.\n";
|
|
28060
29141
|
}
|
|
28061
|
-
async reviewByIdentityKey(identityKey, mode = "all") {
|
|
28062
|
-
const tags = ["release", ...mode === "all" ? ["all"] : []];
|
|
29142
|
+
async reviewByIdentityKey(identityKey, mode = "all", release = false) {
|
|
29143
|
+
const tags = [...release ? ["release"] : [], ...mode === "all" ? ["all"] : []];
|
|
28063
29144
|
const vargs = {
|
|
28064
29145
|
basket: specOpInvalidChange,
|
|
28065
29146
|
tags,
|
|
@@ -28087,8 +29168,150 @@ var TaskReviewUtxos = class TaskReviewUtxos extends WalletMonitorTask {
|
|
|
28087
29168
|
return this.toUserLog(user, result.outputs, result.totalOutputs, total, tags);
|
|
28088
29169
|
});
|
|
28089
29170
|
}
|
|
29171
|
+
async reviewPageByIdentityKey(identityKey, mode = "all", release = false, pageLimit = REVIEW_PAGE_DEFAULT_LIMIT, offset = 0) {
|
|
29172
|
+
pageLimit = Math.min(Math.max(Math.trunc(pageLimit), 1), REVIEW_PAGE_MAX_LIMIT);
|
|
29173
|
+
offset = Math.max(Math.trunc(offset), 0);
|
|
29174
|
+
return await this.storage.runAsStorageProvider(async (sp) => {
|
|
29175
|
+
const user = (await sp.findUsers({ partial: { identityKey } }))[0];
|
|
29176
|
+
if (!user) return {
|
|
29177
|
+
found: false,
|
|
29178
|
+
identityKey,
|
|
29179
|
+
mode,
|
|
29180
|
+
release,
|
|
29181
|
+
offset,
|
|
29182
|
+
pageLimit,
|
|
29183
|
+
sourceScanned: 0,
|
|
29184
|
+
complete: true,
|
|
29185
|
+
checked: 0,
|
|
29186
|
+
confirmedUnspent: 0,
|
|
29187
|
+
confirmedSpent: 0,
|
|
29188
|
+
unknown: 0,
|
|
29189
|
+
confirmedSpentSatoshis: 0,
|
|
29190
|
+
released: 0,
|
|
29191
|
+
releasedSatoshis: 0,
|
|
29192
|
+
providers: [],
|
|
29193
|
+
providerCount: 0,
|
|
29194
|
+
providersTruncated: false,
|
|
29195
|
+
log: `identityKey ${identityKey} was not found\n`
|
|
29196
|
+
};
|
|
29197
|
+
let basketId;
|
|
29198
|
+
if (mode === "change") {
|
|
29199
|
+
basketId = (await sp.findOutputBaskets({ partial: {
|
|
29200
|
+
userId: user.userId,
|
|
29201
|
+
name: "default"
|
|
29202
|
+
} }))[0]?.basketId;
|
|
29203
|
+
if (basketId == null) return this.emptyPage(user, mode, release, pageLimit, offset);
|
|
29204
|
+
}
|
|
29205
|
+
const sourceOutputs = await sp.findOutputs({
|
|
29206
|
+
partial: {
|
|
29207
|
+
userId: user.userId,
|
|
29208
|
+
spendable: true,
|
|
29209
|
+
...basketId != null ? { basketId } : {}
|
|
29210
|
+
},
|
|
29211
|
+
txStatus: [
|
|
29212
|
+
"completed",
|
|
29213
|
+
"unproven",
|
|
29214
|
+
"nosend",
|
|
29215
|
+
"sending"
|
|
29216
|
+
],
|
|
29217
|
+
noScript: true,
|
|
29218
|
+
paged: {
|
|
29219
|
+
limit: pageLimit,
|
|
29220
|
+
offset
|
|
29221
|
+
}
|
|
29222
|
+
});
|
|
29223
|
+
const candidates = sourceOutputs.filter((output) => output.basketId != null);
|
|
29224
|
+
const review = await reviewUtxoOutputs(sp, {
|
|
29225
|
+
userId: user.userId,
|
|
29226
|
+
identityKey: user.identityKey
|
|
29227
|
+
}, candidates, release ? "conclusive" : "none");
|
|
29228
|
+
const complete = sourceOutputs.length < pageLimit;
|
|
29229
|
+
const nextOffset = complete ? void 0 : offset + sourceOutputs.length - review.diagnostics.released;
|
|
29230
|
+
const target = mode === "all" ? "spendable utxos" : "spendable change utxos";
|
|
29231
|
+
const action = release ? "released" : "found";
|
|
29232
|
+
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`;
|
|
29233
|
+
for (const output of review.confirmedSpentOutputs) log += ` ${output.txid}.${output.vout} ${output.satoshis} now ${output.spendable ? "spendable" : "spent"}\n`;
|
|
29234
|
+
if (review.unknownOutputs.length > 0) log += ` ${review.unknownOutputs.length} output(s) quarantined from release pending a conclusive provider result\n`;
|
|
29235
|
+
if (nextOffset != null) log += ` continue at offset ${nextOffset}\n`;
|
|
29236
|
+
return {
|
|
29237
|
+
found: true,
|
|
29238
|
+
userId: user.userId,
|
|
29239
|
+
identityKey: user.identityKey,
|
|
29240
|
+
mode,
|
|
29241
|
+
release,
|
|
29242
|
+
offset,
|
|
29243
|
+
pageLimit,
|
|
29244
|
+
sourceScanned: sourceOutputs.length,
|
|
29245
|
+
complete,
|
|
29246
|
+
...nextOffset != null ? { nextOffset } : {},
|
|
29247
|
+
...review.diagnostics,
|
|
29248
|
+
log
|
|
29249
|
+
};
|
|
29250
|
+
});
|
|
29251
|
+
}
|
|
29252
|
+
emptyPage(user, mode, release, pageLimit, offset) {
|
|
29253
|
+
return {
|
|
29254
|
+
found: true,
|
|
29255
|
+
userId: user.userId,
|
|
29256
|
+
identityKey: user.identityKey,
|
|
29257
|
+
mode,
|
|
29258
|
+
release,
|
|
29259
|
+
offset,
|
|
29260
|
+
pageLimit,
|
|
29261
|
+
sourceScanned: 0,
|
|
29262
|
+
complete: true,
|
|
29263
|
+
checked: 0,
|
|
29264
|
+
confirmedUnspent: 0,
|
|
29265
|
+
confirmedSpent: 0,
|
|
29266
|
+
unknown: 0,
|
|
29267
|
+
confirmedSpentSatoshis: 0,
|
|
29268
|
+
released: 0,
|
|
29269
|
+
releasedSatoshis: 0,
|
|
29270
|
+
providers: [],
|
|
29271
|
+
providerCount: 0,
|
|
29272
|
+
providersTruncated: false,
|
|
29273
|
+
log: `userId ${user.userId}: no invalid utxos found, ${user.identityKey}\n`
|
|
29274
|
+
};
|
|
29275
|
+
}
|
|
29276
|
+
/**
|
|
29277
|
+
* Report managed-change liquidity without changing it. Monitor deliberately
|
|
29278
|
+
* has no signing authority; progressive migration occurs only during a
|
|
29279
|
+
* caller-authorized createAction.
|
|
29280
|
+
*/
|
|
29281
|
+
async reviewManagedChangeByIdentityKey(identityKey) {
|
|
29282
|
+
return await this.storage.runAsStorageProvider(async (sp) => {
|
|
29283
|
+
const user = (await sp.findUsers({ partial: { identityKey } }))[0];
|
|
29284
|
+
if (user == null) return `identityKey ${identityKey} was not found\n`;
|
|
29285
|
+
const basket = verifyOne(await sp.findOutputBaskets({ partial: {
|
|
29286
|
+
userId: user.userId,
|
|
29287
|
+
name: "default"
|
|
29288
|
+
} }));
|
|
29289
|
+
const outputs = (await sp.findOutputs({
|
|
29290
|
+
partial: {
|
|
29291
|
+
userId: user.userId,
|
|
29292
|
+
basketId: basket.basketId,
|
|
29293
|
+
spendable: true,
|
|
29294
|
+
...managedChangeOutputFields
|
|
29295
|
+
},
|
|
29296
|
+
txStatus: [
|
|
29297
|
+
"completed",
|
|
29298
|
+
"unproven",
|
|
29299
|
+
"sending"
|
|
29300
|
+
],
|
|
29301
|
+
noScript: true
|
|
29302
|
+
})).filter(isAutoSpendableChangeOutput);
|
|
29303
|
+
const reserved = new Set(await sp.findReservedActionBatchOutputIds(outputs.map((output) => output.outputId)));
|
|
29304
|
+
const statuses = await sp.findTransactionStatusesByIds(user.userId, outputs.map((output) => output.transactionId));
|
|
29305
|
+
const preferred = Math.max(1, basket.minimumDesiredUTXOValue);
|
|
29306
|
+
const healthy = outputs.filter((output) => output.satoshis >= preferred);
|
|
29307
|
+
const undersized = outputs.filter((output) => output.satoshis < preferred);
|
|
29308
|
+
const countStatus = (status) => outputs.filter((output) => statuses.get(output.transactionId) === status).length;
|
|
29309
|
+
const satoshis = outputs.reduce((sum, output) => sum + output.satoshis, 0);
|
|
29310
|
+
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`;
|
|
29311
|
+
});
|
|
29312
|
+
}
|
|
28090
29313
|
toUserLog(user, outputs, totalOutputs, total, tags) {
|
|
28091
|
-
const action = tags.includes("release") ? "updated to unspendable" : "
|
|
29314
|
+
const action = tags.includes("release") ? "confirmed spent and updated to unspendable" : "confirmed spent";
|
|
28092
29315
|
const target = tags.includes("all") ? "spendable utxos" : "spendable change utxos";
|
|
28093
29316
|
let log = `userId ${user.userId}: ${totalOutputs} ${target} ${action}, total ${total}, ${user.identityKey}\n`;
|
|
28094
29317
|
for (const output of outputs) log += ` ${output.outpoint} ${output.satoshis} now ${output.spendable ? "spendable" : "spent"}\n`;
|
|
@@ -28097,6 +29320,13 @@ var TaskReviewUtxos = class TaskReviewUtxos extends WalletMonitorTask {
|
|
|
28097
29320
|
};
|
|
28098
29321
|
//#endregion
|
|
28099
29322
|
//#region ../src/monitor/tasks/TaskReviewDoubleSpends.ts
|
|
29323
|
+
function hasDurableInputConflict(req) {
|
|
29324
|
+
try {
|
|
29325
|
+
return JSON.parse(req.history).notes?.some((note) => note.inputConflict === true || note.what === "arcSSEInputQuarantine") === true;
|
|
29326
|
+
} catch {
|
|
29327
|
+
return true;
|
|
29328
|
+
}
|
|
29329
|
+
}
|
|
28100
29330
|
/**
|
|
28101
29331
|
* Review recent reqs in terminal 'doubleSpend' state and move any false positives
|
|
28102
29332
|
* back to 'unfail' so existing recovery handling can re-process them.
|
|
@@ -28164,14 +29394,16 @@ var TaskReviewDoubleSpends = class TaskReviewDoubleSpends extends WalletMonitorT
|
|
|
28164
29394
|
let lastRetainedDoubleSpendIndex = -1;
|
|
28165
29395
|
let retainedDoubleSpendCount = 0;
|
|
28166
29396
|
for (const req of reqs) {
|
|
28167
|
-
const
|
|
29397
|
+
const gsr = await this.monitor.services.getStatusForTxids([req.txid]);
|
|
29398
|
+
const status = gsr.status === "success" ? gsr.results[0]?.status : void 0;
|
|
29399
|
+
const durableInputConflict = hasDurableInputConflict(req);
|
|
28168
29400
|
reviewed.push(req);
|
|
28169
|
-
if (status === "
|
|
29401
|
+
if (!(status === "mined" || status === "known" && !durableInputConflict)) {
|
|
28170
29402
|
lastRetainedDoubleSpendIndex = reviewed.length - 1;
|
|
28171
29403
|
retainedDoubleSpendCount += 1;
|
|
28172
29404
|
} else {
|
|
28173
29405
|
unfails.push(req.provenTxReqId);
|
|
28174
|
-
log += `unfail ${req.provenTxReqId} ${req.txid} status:${status}\n`;
|
|
29406
|
+
log += `unfail ${req.provenTxReqId} ${req.txid} status:${status} durableInputConflict:${String(durableInputConflict)}\n`;
|
|
28175
29407
|
}
|
|
28176
29408
|
}
|
|
28177
29409
|
if (unfails.length > 0) await this.storage.runAsStorageProvider(async (sp) => {
|
|
@@ -28210,6 +29442,206 @@ var TaskReviewDoubleSpends = class TaskReviewDoubleSpends extends WalletMonitorT
|
|
|
28210
29442
|
}
|
|
28211
29443
|
};
|
|
28212
29444
|
//#endregion
|
|
29445
|
+
//#region ../src/monitor/tasks/TaskReconcilePendingTransactions.ts
|
|
29446
|
+
const REVIEW_STATUSES = [
|
|
29447
|
+
"callback",
|
|
29448
|
+
"unmined",
|
|
29449
|
+
"sending",
|
|
29450
|
+
"unknown",
|
|
29451
|
+
"unconfirmed"
|
|
29452
|
+
];
|
|
29453
|
+
/**
|
|
29454
|
+
* Poll durable transaction lifecycle state for aged, non-terminal requests.
|
|
29455
|
+
* This closes the gap where an Arcade rejection event was missed before the
|
|
29456
|
+
* monitor subscribed or while it was offline. Unknown/provider-error results
|
|
29457
|
+
* never mutate storage; only a provider's explicit terminal verdict does.
|
|
29458
|
+
*/
|
|
29459
|
+
var TaskReconcilePendingTransactions = class TaskReconcilePendingTransactions extends WalletMonitorTask {
|
|
29460
|
+
triggerMsecs;
|
|
29461
|
+
reviewLimit;
|
|
29462
|
+
minAgeMinutes;
|
|
29463
|
+
triggerQuickMsecs;
|
|
29464
|
+
static taskName = "ReconcilePendingTransactions";
|
|
29465
|
+
triggerNextMsecs;
|
|
29466
|
+
constructor(monitor, triggerMsecs = Monitor.oneMinute * 12, reviewLimit = 100, minAgeMinutes = 60, triggerQuickMsecs = Monitor.oneMinute) {
|
|
29467
|
+
super(monitor, TaskReconcilePendingTransactions.taskName);
|
|
29468
|
+
this.triggerMsecs = triggerMsecs;
|
|
29469
|
+
this.reviewLimit = reviewLimit;
|
|
29470
|
+
this.minAgeMinutes = minAgeMinutes;
|
|
29471
|
+
this.triggerQuickMsecs = triggerQuickMsecs;
|
|
29472
|
+
this.triggerNextMsecs = this.triggerQuickMsecs;
|
|
29473
|
+
}
|
|
29474
|
+
trigger(nowMsecsSinceEpoch) {
|
|
29475
|
+
return { run: this.triggerNextMsecs > 0 && nowMsecsSinceEpoch - this.lastRunMsecsSinceEpoch > this.triggerNextMsecs };
|
|
29476
|
+
}
|
|
29477
|
+
async getCheckpoint() {
|
|
29478
|
+
let events = [];
|
|
29479
|
+
await this.storage.runAsStorageProvider(async (sp) => {
|
|
29480
|
+
events = await sp.findMonitorEvents({
|
|
29481
|
+
partial: { event: TaskReconcilePendingTransactions.taskName },
|
|
29482
|
+
orderDescending: true,
|
|
29483
|
+
paged: { limit: 5 }
|
|
29484
|
+
});
|
|
29485
|
+
});
|
|
29486
|
+
for (const event of events) {
|
|
29487
|
+
if (!event.details) continue;
|
|
29488
|
+
try {
|
|
29489
|
+
const parsed = JSON.parse(event.details);
|
|
29490
|
+
if (parsed.cycleComplete === true) return void 0;
|
|
29491
|
+
if (typeof parsed.resumeOffset === "number") return {
|
|
29492
|
+
resumeOffset: parsed.resumeOffset,
|
|
29493
|
+
expectedProvenTxReqId: parsed.expectedProvenTxReqId
|
|
29494
|
+
};
|
|
29495
|
+
} catch {
|
|
29496
|
+
continue;
|
|
29497
|
+
}
|
|
29498
|
+
}
|
|
29499
|
+
}
|
|
29500
|
+
async runTask() {
|
|
29501
|
+
const checkpoint = await this.getCheckpoint();
|
|
29502
|
+
const createdBefore = /* @__PURE__ */ new Date(Date.now() - this.minAgeMinutes * 60 * 1e3);
|
|
29503
|
+
const reqs = await this.findReqsToReview(checkpoint);
|
|
29504
|
+
const eligible = reqs.filter((req) => req.created_at <= createdBefore);
|
|
29505
|
+
const provider = eligible.length === 0 ? void 0 : await this.monitor.services.getStatusForTxids(eligible.map((req) => req.txid));
|
|
29506
|
+
const results = new Map(provider?.results.map((result) => [result.txid, result]) ?? []);
|
|
29507
|
+
let reconciled = 0;
|
|
29508
|
+
let inputConflicts = 0;
|
|
29509
|
+
let reviewLog = "";
|
|
29510
|
+
const retained = [];
|
|
29511
|
+
for (const req of reqs) {
|
|
29512
|
+
const result = results.get(req.txid);
|
|
29513
|
+
if (req.created_at > createdBefore) {
|
|
29514
|
+
retained.push(req);
|
|
29515
|
+
continue;
|
|
29516
|
+
}
|
|
29517
|
+
const applied = await this.applyTerminalVerdict(req, provider?.status === "success" ? result : void 0, provider?.status === "success" ? provider.name : void 0, createdBefore);
|
|
29518
|
+
if (!applied) {
|
|
29519
|
+
retained.push(req);
|
|
29520
|
+
continue;
|
|
29521
|
+
}
|
|
29522
|
+
reconciled++;
|
|
29523
|
+
if (applied.result.inputConflict === true) inputConflicts++;
|
|
29524
|
+
reviewLog += `reconciled ${req.provenTxReqId} ${req.txid} ${applied.result.providerStatus ?? "terminal"} => ${applied.result.inputConflict === true ? "doubleSpend" : "invalid"}\n`;
|
|
29525
|
+
}
|
|
29526
|
+
const fullPage = reqs.length >= this.reviewLimit;
|
|
29527
|
+
this.triggerNextMsecs = fullPage ? this.triggerQuickMsecs : this.triggerMsecs;
|
|
29528
|
+
let resumeOffset;
|
|
29529
|
+
let expectedProvenTxReqId;
|
|
29530
|
+
if (fullPage) if (retained.length === 0) resumeOffset = reqs.sourceOffset ?? 0;
|
|
29531
|
+
else {
|
|
29532
|
+
resumeOffset = (reqs.sourceOffset ?? 0) + retained.length - 1;
|
|
29533
|
+
expectedProvenTxReqId = retained.at(-1)?.provenTxReqId;
|
|
29534
|
+
}
|
|
29535
|
+
return JSON.stringify({
|
|
29536
|
+
reviewed: eligible.length,
|
|
29537
|
+
reconciled,
|
|
29538
|
+
inputConflicts,
|
|
29539
|
+
retained: retained.length,
|
|
29540
|
+
reviewLimit: this.reviewLimit,
|
|
29541
|
+
minAgeMinutes: this.minAgeMinutes,
|
|
29542
|
+
cycleComplete: !fullPage,
|
|
29543
|
+
...resumeOffset != null ? { resumeOffset } : {},
|
|
29544
|
+
...expectedProvenTxReqId != null ? { expectedProvenTxReqId } : {},
|
|
29545
|
+
reviewLog
|
|
29546
|
+
});
|
|
29547
|
+
}
|
|
29548
|
+
async findReqsToReview(checkpoint) {
|
|
29549
|
+
let offset = checkpoint?.resumeOffset ?? 0;
|
|
29550
|
+
if (checkpoint?.expectedProvenTxReqId != null) offset = (await this.storage.findProvenTxReqs({
|
|
29551
|
+
partial: {},
|
|
29552
|
+
status: [...REVIEW_STATUSES],
|
|
29553
|
+
paged: {
|
|
29554
|
+
limit: 1,
|
|
29555
|
+
offset
|
|
29556
|
+
}
|
|
29557
|
+
}))[0]?.provenTxReqId === checkpoint.expectedProvenTxReqId ? offset + 1 : 0;
|
|
29558
|
+
const reqs = await this.storage.findProvenTxReqs({
|
|
29559
|
+
partial: {},
|
|
29560
|
+
status: [...REVIEW_STATUSES],
|
|
29561
|
+
paged: {
|
|
29562
|
+
limit: this.reviewLimit,
|
|
29563
|
+
offset
|
|
29564
|
+
}
|
|
29565
|
+
});
|
|
29566
|
+
reqs.sourceOffset = offset;
|
|
29567
|
+
return reqs;
|
|
29568
|
+
}
|
|
29569
|
+
async applyTerminalVerdict(reqApi, providerResult, providerName, createdBefore) {
|
|
29570
|
+
let applied;
|
|
29571
|
+
await this.storage.runAsStorageProvider(async (sp) => {
|
|
29572
|
+
await sp.transaction(async (trx) => {
|
|
29573
|
+
const req = new EntityProvenTxReq(reqApi);
|
|
29574
|
+
await req.refreshFromStorage(sp, trx);
|
|
29575
|
+
if (!REVIEW_STATUSES.includes(req.status)) return;
|
|
29576
|
+
if (req.created_at > createdBefore) return;
|
|
29577
|
+
const failedParentTxids = await this.findFailedLocalParentTxids(req, sp, trx);
|
|
29578
|
+
const result = providerResult?.terminal === true ? providerResult : providerResult?.status === "known" || providerResult?.status === "mined" ? void 0 : this.failedParentVerdict(req.txid, failedParentTxids);
|
|
29579
|
+
if (result == null) return;
|
|
29580
|
+
const provider = providerResult?.terminal === true ? providerName ?? "transaction-status-provider" : "local-storage";
|
|
29581
|
+
req.status = result.inputConflict === true ? "doubleSpend" : "invalid";
|
|
29582
|
+
req.addHistoryNote({
|
|
29583
|
+
when: (/* @__PURE__ */ new Date()).toISOString(),
|
|
29584
|
+
what: "proactiveTerminalReconciliation",
|
|
29585
|
+
provider,
|
|
29586
|
+
providerStatus: result.providerStatus,
|
|
29587
|
+
statusCode: result.statusCode,
|
|
29588
|
+
description: result.description,
|
|
29589
|
+
inputConflict: result.inputConflict === true,
|
|
29590
|
+
competingTxs: result.competingTxs?.join(",")
|
|
29591
|
+
});
|
|
29592
|
+
await req.updateStorageDynamicProperties(sp, trx);
|
|
29593
|
+
if (req.notify.transactionIds != null) await sp.updateTransactionsStatus(req.notify.transactionIds, "failed", trx);
|
|
29594
|
+
if (result.inputConflict === true) {
|
|
29595
|
+
const quarantined = await quarantineReqInputs(req, sp, trx);
|
|
29596
|
+
req.addHistoryNote({
|
|
29597
|
+
when: (/* @__PURE__ */ new Date()).toISOString(),
|
|
29598
|
+
what: "proactiveInputQuarantine",
|
|
29599
|
+
checked: quarantined.checked,
|
|
29600
|
+
confirmed: quarantined.staleConfirmed,
|
|
29601
|
+
...quarantined.staleOutpoints.length > 0 ? { outpoints: quarantined.staleOutpoints.join(",") } : {}
|
|
29602
|
+
});
|
|
29603
|
+
await req.updateStorageDynamicProperties(sp, trx);
|
|
29604
|
+
} else if (failedParentTxids.length > 0) {
|
|
29605
|
+
const quarantined = await quarantineReqInputsFromFailedParents(req, failedParentTxids, sp, trx);
|
|
29606
|
+
req.addHistoryNote({
|
|
29607
|
+
when: (/* @__PURE__ */ new Date()).toISOString(),
|
|
29608
|
+
what: "proactiveFailedParentQuarantine",
|
|
29609
|
+
checked: quarantined.checked,
|
|
29610
|
+
confirmed: quarantined.staleConfirmed,
|
|
29611
|
+
parentTxids: failedParentTxids.join(","),
|
|
29612
|
+
...quarantined.staleOutpoints.length > 0 ? { outpoints: quarantined.staleOutpoints.join(",") } : {}
|
|
29613
|
+
});
|
|
29614
|
+
await req.updateStorageDynamicProperties(sp, trx);
|
|
29615
|
+
}
|
|
29616
|
+
applied = { result };
|
|
29617
|
+
});
|
|
29618
|
+
});
|
|
29619
|
+
if (applied) this.monitor.callOnTransactionStatusChanged(reqApi.txid, applied.result.providerStatus ?? "REJECTED");
|
|
29620
|
+
return applied;
|
|
29621
|
+
}
|
|
29622
|
+
async findFailedLocalParentTxids(req, storage, trx) {
|
|
29623
|
+
const parentTxids = [...new Set(Transaction.fromBinary(req.rawTx).inputs.map((input) => input.sourceTXID).filter((txid) => txid != null && txid !== ""))];
|
|
29624
|
+
const failed = [];
|
|
29625
|
+
for (const txid of parentTxids) if ((await storage.findProvenTxReqs({
|
|
29626
|
+
partial: { txid },
|
|
29627
|
+
trx
|
|
29628
|
+
})).some((parent) => parent.status === "invalid" || parent.status === "doubleSpend")) failed.push(txid);
|
|
29629
|
+
return failed;
|
|
29630
|
+
}
|
|
29631
|
+
failedParentVerdict(txid, failedParentTxids) {
|
|
29632
|
+
if (failedParentTxids.length === 0) return void 0;
|
|
29633
|
+
return {
|
|
29634
|
+
txid,
|
|
29635
|
+
status: "unknown",
|
|
29636
|
+
depth: void 0,
|
|
29637
|
+
terminal: true,
|
|
29638
|
+
inputConflict: false,
|
|
29639
|
+
providerStatus: "LOCAL_PARENT_FAILED",
|
|
29640
|
+
description: `Local parent request is terminal: ${failedParentTxids.join(",")}`.slice(0, 512)
|
|
29641
|
+
};
|
|
29642
|
+
}
|
|
29643
|
+
};
|
|
29644
|
+
//#endregion
|
|
28213
29645
|
//#region ../src/monitor/tasks/TaskReviewProvenTxs.ts
|
|
28214
29646
|
/**
|
|
28215
29647
|
* Backup verification task for recent proven_txs records.
|
|
@@ -28468,14 +29900,14 @@ var Monitor = class Monitor {
|
|
|
28468
29900
|
purgeFailedAge: 5 * Monitor.oneDay
|
|
28469
29901
|
};
|
|
28470
29902
|
addAllTasksToOther() {
|
|
28471
|
-
this._otherTasks.push(new TaskClock(this), new TaskNewHeader(this), new TaskMonitorCallHistory(this), new TaskSendWaiting(this), new TaskCheckForProofs(this), new TaskCheckNoSends(this), new TaskFailAbandoned(this), new TaskUnFail(this), new TaskReviewStatus(this), new TaskReorg(this), new TaskReviewUtxos(this), new TaskReviewDoubleSpends(this), new TaskReviewProvenTxs(this), new TaskCleanupActionBatches(this), new TaskPurge(this, this.defaultPurgeParams));
|
|
29903
|
+
this._otherTasks.push(new TaskClock(this), new TaskNewHeader(this), new TaskMonitorCallHistory(this), new TaskSendWaiting(this), new TaskCheckForProofs(this), new TaskCheckNoSends(this), new TaskFailAbandoned(this), new TaskUnFail(this), new TaskReviewStatus(this), new TaskReorg(this), new TaskReviewUtxos(this), new TaskReviewDoubleSpends(this), new TaskReconcilePendingTransactions(this), new TaskReviewProvenTxs(this), new TaskCleanupActionBatches(this), new TaskPurge(this, this.defaultPurgeParams));
|
|
28472
29904
|
if (this.chain === "mock") this._otherTasks.push(new TaskMineBlock(this));
|
|
28473
29905
|
}
|
|
28474
29906
|
/**
|
|
28475
29907
|
* Default tasks with settings appropriate for a single user storage
|
|
28476
29908
|
*/
|
|
28477
29909
|
addDefaultTasks() {
|
|
28478
|
-
this._tasks.push(new TaskClock(this), new TaskNewHeader(this), new TaskMonitorCallHistory(this), new TaskSendWaiting(this, 8 * Monitor.oneSecond, 7 * Monitor.oneSecond), new TaskCheckForProofs(this, 2 * Monitor.oneHour), new TaskCheckNoSends(this), new TaskFailAbandoned(this, 8 * Monitor.oneMinute), new TaskUnFail(this), new TaskReviewStatus(this), new TaskReorg(this), new TaskReviewDoubleSpends(this), new TaskReviewProvenTxs(this), new TaskCleanupActionBatches(this), new TaskArcadeSSE(this));
|
|
29910
|
+
this._tasks.push(new TaskClock(this), new TaskNewHeader(this), new TaskMonitorCallHistory(this), new TaskSendWaiting(this, 8 * Monitor.oneSecond, 7 * Monitor.oneSecond), new TaskCheckForProofs(this, 2 * Monitor.oneHour), new TaskCheckNoSends(this), new TaskFailAbandoned(this, 8 * Monitor.oneMinute), new TaskUnFail(this), new TaskReviewStatus(this), new TaskReorg(this), new TaskReviewDoubleSpends(this), new TaskReconcilePendingTransactions(this), new TaskReviewProvenTxs(this), new TaskCleanupActionBatches(this), new TaskArcadeSSE(this));
|
|
28479
29911
|
this._otherTasks.push(new TaskPurge(this, this.defaultPurgeParams, 6 * Monitor.oneHour), new TaskReviewUtxos(this));
|
|
28480
29912
|
if (this.chain === "mock") this._tasks.push(new TaskMineBlock(this));
|
|
28481
29913
|
}
|
|
@@ -28483,7 +29915,7 @@ var Monitor = class Monitor {
|
|
|
28483
29915
|
* Tasks appropriate for multi-user storage
|
|
28484
29916
|
*/
|
|
28485
29917
|
addMultiUserTasks() {
|
|
28486
|
-
this._tasks.push(new TaskClock(this), new TaskNewHeader(this), new TaskMonitorCallHistory(this), new TaskSendWaiting(this, 8 * Monitor.oneSecond, 7 * Monitor.oneSecond), new TaskCheckForProofs(this, 2 * Monitor.oneHour), new TaskCheckNoSends(this), new TaskFailAbandoned(this, 8 * Monitor.oneMinute), new TaskUnFail(this), new TaskReviewStatus(this), new TaskReorg(this), new TaskReviewDoubleSpends(this), new TaskReviewProvenTxs(this), new TaskCleanupActionBatches(this), new TaskArcadeSSE(this));
|
|
29918
|
+
this._tasks.push(new TaskClock(this), new TaskNewHeader(this), new TaskMonitorCallHistory(this), new TaskSendWaiting(this, 8 * Monitor.oneSecond, 7 * Monitor.oneSecond), new TaskCheckForProofs(this, 2 * Monitor.oneHour), new TaskCheckNoSends(this), new TaskFailAbandoned(this, 8 * Monitor.oneMinute), new TaskUnFail(this), new TaskReviewStatus(this), new TaskReorg(this), new TaskReviewDoubleSpends(this), new TaskReconcilePendingTransactions(this), new TaskReviewProvenTxs(this), new TaskCleanupActionBatches(this), new TaskArcadeSSE(this));
|
|
28487
29919
|
this._otherTasks.push(new TaskPurge(this, this.defaultPurgeParams), new TaskReviewUtxos(this));
|
|
28488
29920
|
if (this.chain === "mock") this._tasks.push(new TaskMineBlock(this));
|
|
28489
29921
|
}
|
|
@@ -28854,6 +30286,7 @@ var SetupClient = class SetupClient {
|
|
|
28854
30286
|
model: "sat/kb",
|
|
28855
30287
|
value: 100
|
|
28856
30288
|
},
|
|
30289
|
+
managedChangePolicy: args.managedChangePolicy,
|
|
28857
30290
|
scriptVerifier: args.scriptVerifier
|
|
28858
30291
|
});
|
|
28859
30292
|
await storage.migrate(args.databaseName, randomBytesHex(33));
|
|
@@ -33443,7 +34876,7 @@ var WalletPermissionsManager = class WalletPermissionsManager {
|
|
|
33443
34876
|
basket: basketName,
|
|
33444
34877
|
tags
|
|
33445
34878
|
}],
|
|
33446
|
-
options: { acceptDelayedBroadcast:
|
|
34879
|
+
options: { acceptDelayedBroadcast: true }
|
|
33447
34880
|
}, this.adminOriginator);
|
|
33448
34881
|
}
|
|
33449
34882
|
async mapWithConcurrency(items, concurrency, fn) {
|
|
@@ -33507,7 +34940,7 @@ var WalletPermissionsManager = class WalletPermissionsManager {
|
|
|
33507
34940
|
await this.createAction({
|
|
33508
34941
|
description: `Grant ${built.length} permissions`,
|
|
33509
34942
|
outputs: built.map((b) => b.output),
|
|
33510
|
-
options: { acceptDelayedBroadcast:
|
|
34943
|
+
options: { acceptDelayedBroadcast: true }
|
|
33511
34944
|
}, this.adminOriginator);
|
|
33512
34945
|
return built.map((b) => b.request);
|
|
33513
34946
|
}, strict);
|
|
@@ -34895,6 +36328,6 @@ var WalletPermissionsManager = class WalletPermissionsManager {
|
|
|
34895
36328
|
}
|
|
34896
36329
|
};
|
|
34897
36330
|
//#endregion
|
|
34898
|
-
export { ARGON2ID_DEFAULT_HASH_LENGTH, ARGON2ID_DEFAULT_ITERATIONS, ARGON2ID_DEFAULT_MEMORY_KIB, ARGON2ID_DEFAULT_PARALLELISM, ARGON2ID_MAX_ITERATIONS, ARGON2ID_MAX_MEMORY_KIB, ARGON2ID_MAX_PARALLELISM, AuthMethodInteractor, BHServiceClient, BulkFileDataManager, BulkFileDataReader, BulkFilesReader, BulkFilesReaderFs, BulkFilesReaderStorage, BulkIngestorBase, BulkIngestorCDN, BulkIngestorCDNBabbage, BulkIngestorChaintracks, BulkIngestorWhatsOnChainCdn, BulkStorageBase, CWIStyleWalletManager, Chaintracks, ChaintracksChainTracker, ChaintracksFetch, ChaintracksFetchError, ChaintracksServiceClient, ChaintracksStorageBase, ChaintracksStorageIdb, ChaintracksStorageNoDb, DEFAULT_PROFILE_ID, DEFAULT_SETTINGS, DevConsoleInteractor, EntityBase, EntityCertificate, EntityCertificateField, EntityCommission, EntityOutput, EntityOutputBasket, EntityOutputTag, EntityOutputTagMap, EntityProvenTx, EntityProvenTxReq, EntitySyncState, EntityTransaction, EntityTxLabel, EntityTxLabelMap, EntityUser, GoChaintracksServiceClient, HeightRange, KDF_MAX_HASH_LENGTH, LiveIngestorBase, LiveIngestorChaintracksSSE, LiveIngestorWhatsOnChainPoll, MAX_STATE_SNAPSHOT_BYTES, MergeEntity, Monitor, OverlayUMPTokenInteractor, PBKDF2_MAX_ITERATIONS, PBKDF2_NUM_ROUNDS, PersonaIDInteractor, PrivilegedKeyManager, ScriptTemplateBRC29, Services, SetupClient, SimpleWalletManager, StorageClient, StorageIdb, StorageProvider, StorageSyncReader, TESTNET_DEFAULT_SETTINGS, TwilioPhoneInteractor, UMPTokenLookupError, WABAccountContinuityError, WABClient, WABClientError, WABTransport, Wallet, WalletAuthenticationManager, WalletLogger, WalletPermissionsManager, WalletSettingsManager, WalletSigner, WalletStorageManager, WhatsOnChainServices, arcDefaultUrl, arcGorillaPoolUrl, arcadeDefaultUrl, arraysEqual, asArray, asBsvSdkPrivateKey, asBsvSdkPublickKey, asBsvSdkScript, asBsvSdkTx, asString, asUint8Array, brc29ProtocolID, buildChaintracksOptionsWithIngestors, convertProofToMerklePath, createAndStartDefaultChaintracks, createDefaultBulkFileDataManager, createDefaultChaintracksClient, createDefaultChaintracksStorageOptions, createDefaultIdbChaintracksOptions, createDefaultNoDbChaintracksOptions, createDefaultWalletServicesOptions, createIdbChaintracks, createNoDbChaintracks, createSyncMap, decryptBRC39, doubleSha256BE, doubleSha256LE, encryptBRC39, exportBRC38, exportBRC38Json, exportBRC39, getIdentityKey, getLabelToSpecOp, getListOutputsSpecOp, importBRC38, importBRC39, isAutoSpendableChangeOutput, isBaseBlockHeader, isBlockHeader, isLive, isLiveBlockHeader, isManagedChangeOutput, logCreateActionArgs, logWalletError, logger, makeAtomicBeef, makeBrc114ActionTimeLabel, managedChangeOutputFields, maxDate, optionalArraysEqual, outputColumnsWithoutLockingScript, parseBRC38Json, parseBrc114ActionTimeLabels, parseFileLink, parseTxScriptOffsets, partitionActionLabels, randomBytes, randomBytesBase64, randomBytesHex, resolveDefaultChaintracksArguments, sdk_exports as sdk, selectBulkHeaderFiles, sha256Hash, stampLog, stampLogFormat, startChaintracks, tableAuthSessionToPeerSession, throwDummyReviewActions, toBinaryBaseBlockHeader, toDefaultChaintracksArguments, toLookupNetworkPreset, toWalletNetwork, transactionColumnsWithoutRawTx, blockHeaderUtilities_exports as utils, validateScriptHash, validateSecondsSinceEpoch, validateStorageFeeModel, verifyHexString, verifyId, verifyInteger, verifyNumber, verifyOne, verifyOneOrNone, verifyOptionalHexString, verifyTruthy, wait, wocGetHeadersHeaderToBlockHeader };
|
|
36331
|
+
export { ARGON2ID_DEFAULT_HASH_LENGTH, ARGON2ID_DEFAULT_ITERATIONS, ARGON2ID_DEFAULT_MEMORY_KIB, ARGON2ID_DEFAULT_PARALLELISM, ARGON2ID_MAX_ITERATIONS, ARGON2ID_MAX_MEMORY_KIB, ARGON2ID_MAX_PARALLELISM, AuthMethodInteractor, BHServiceClient, BRC153_REFERENCE_PREFIX, BulkFileDataManager, BulkFileDataReader, BulkFilesReader, BulkFilesReaderFs, BulkFilesReaderStorage, BulkIngestorBase, BulkIngestorCDN, BulkIngestorCDNBabbage, BulkIngestorChaintracks, BulkIngestorWhatsOnChainCdn, BulkStorageBase, CWIStyleWalletManager, Chaintracks, ChaintracksChainTracker, ChaintracksFetch, ChaintracksFetchError, ChaintracksServiceClient, ChaintracksStorageBase, ChaintracksStorageIdb, ChaintracksStorageNoDb, DEFAULT_MANAGED_CHANGE_MAX_OUTPUTS_PER_ACTION, DEFAULT_MANAGED_CHANGE_MIGRATION_INPUTS_PER_ACTION, DEFAULT_MANAGED_CHANGE_MINIMUM_SATOSHIS, DEFAULT_MANAGED_CHANGE_PENDING_COMPARISON_INPUTS, DEFAULT_MANAGED_CHANGE_TARGET_UTXOS, DEFAULT_PROFILE_ID, DEFAULT_SETTINGS, DevConsoleInteractor, EntityBase, EntityCertificate, EntityCertificateField, EntityCommission, EntityOutput, EntityOutputBasket, EntityOutputTag, EntityOutputTagMap, EntityProvenTx, EntityProvenTxReq, EntitySyncState, EntityTransaction, EntityTxLabel, EntityTxLabelMap, EntityUser, GoChaintracksServiceClient, HeightRange, KDF_MAX_HASH_LENGTH, LEGACY_MANAGED_CHANGE_MINIMUM_SATOSHIS, LiveIngestorBase, LiveIngestorChaintracksSSE, LiveIngestorWhatsOnChainPoll, MAX_STATE_SNAPSHOT_BYTES, MergeEntity, Monitor, OverlayUMPTokenInteractor, PBKDF2_MAX_ITERATIONS, PBKDF2_NUM_ROUNDS, PersonaIDInteractor, PrivilegedKeyManager, ScriptTemplateBRC29, Services, SetupClient, SimpleWalletManager, StorageClient, StorageIdb, StorageProvider, StorageSyncReader, TESTNET_DEFAULT_SETTINGS, TwilioPhoneInteractor, UMPTokenLookupError, WABAccountContinuityError, WABClient, WABClientError, WABTransport, Wallet, WalletAuthenticationManager, WalletLogger, WalletPermissionsManager, WalletSettingsManager, WalletSigner, WalletStorageManager, WhatsOnChainServices, applyBrc153ReferenceLabel, arcDefaultUrl, arcGorillaPoolUrl, arcadeDefaultUrl, arraysEqual, asArray, asBsvSdkPrivateKey, asBsvSdkPublickKey, asBsvSdkScript, asBsvSdkTx, asString, asUint8Array, brc29ProtocolID, buildChaintracksOptionsWithIngestors, convertProofToMerklePath, createAndStartDefaultChaintracks, createDefaultBulkFileDataManager, createDefaultChaintracksClient, createDefaultChaintracksStorageOptions, createDefaultIdbChaintracksOptions, createDefaultNoDbChaintracksOptions, createDefaultWalletServicesOptions, createIdbChaintracks, createNoDbChaintracks, createSyncMap, decryptBRC39, defaultManagedChangePolicy, doubleSha256BE, doubleSha256LE, encryptBRC39, exportBRC38, exportBRC38Json, exportBRC39, getIdentityKey, getLabelToSpecOp, getListOutputsSpecOp, importBRC38, importBRC39, isAutoSpendableChangeOutput, isBaseBlockHeader, isBlockHeader, isBrc153ReferenceLabel, isLegacyManagedChangeBasketDefault, isLive, isLiveBlockHeader, isManagedChangeOutput, logCreateActionArgs, logWalletError, logger, makeAtomicBeef, makeBrc114ActionTimeLabel, makeBrc153ReferenceLabel, managedChangeOutputFields, maxDate, optionalArraysEqual, outputColumnsWithoutLockingScript, parseBRC38Json, parseBrc114ActionTimeLabels, parseBrc153ReferenceLabel, parseFileLink, parseTxScriptOffsets, partitionActionLabels, randomBytes, randomBytesBase64, randomBytesHex, resolveDefaultChaintracksArguments, sdk_exports as sdk, selectBulkHeaderFiles, sha256Hash, stampLog, stampLogFormat, startChaintracks, tableAuthSessionToPeerSession, throwDummyReviewActions, toBinaryBaseBlockHeader, toDefaultChaintracksArguments, toLookupNetworkPreset, toWalletNetwork, transactionColumnsWithoutRawTx, upgradeLegacyManagedChangeBasketDefault, blockHeaderUtilities_exports as utils, validateManagedChangePolicy, validateScriptHash, validateSecondsSinceEpoch, validateStorageFeeModel, verifyHexString, verifyId, verifyInteger, verifyNumber, verifyOne, verifyOneOrNone, verifyOptionalHexString, verifyTruthy, wait, wocGetHeadersHeaderToBlockHeader };
|
|
34899
36332
|
|
|
34900
36333
|
//# sourceMappingURL=index.client.mjs.map
|