@bsv/wallet-toolbox-client 2.4.22 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/out/index.client.cjs +1557 -403
- package/out/index.client.cjs.map +1 -1
- package/out/index.client.d.cts +206 -20
- package/out/index.client.d.cts.map +1 -1
- package/out/index.client.d.mts +206 -20
- package/out/index.client.d.mts.map +1 -1
- package/out/index.client.mjs +1550 -405
- package/out/index.client.mjs.map +1 -1
- package/package.json +2 -2
package/out/index.client.cjs
CHANGED
|
@@ -1005,6 +1005,7 @@ function toWalletNetwork(chain) {
|
|
|
1005
1005
|
switch (chain) {
|
|
1006
1006
|
case "main": return "mainnet";
|
|
1007
1007
|
case "test":
|
|
1008
|
+
case "stn":
|
|
1008
1009
|
case "ttn":
|
|
1009
1010
|
case "tstn":
|
|
1010
1011
|
case "mock": return "testnet";
|
|
@@ -1018,6 +1019,7 @@ function toLookupNetworkPreset(chain) {
|
|
|
1018
1019
|
switch (chain) {
|
|
1019
1020
|
case "main": return "mainnet";
|
|
1020
1021
|
case "test": return "testnet";
|
|
1022
|
+
case "stn":
|
|
1021
1023
|
case "ttn":
|
|
1022
1024
|
case "tstn":
|
|
1023
1025
|
case "mock": return "local";
|
|
@@ -1257,7 +1259,11 @@ var ScriptTemplateBRC29 = class {
|
|
|
1257
1259
|
return `${this.params.derivationPrefix ?? ""} ${this.params.derivationSuffix ?? ""}`;
|
|
1258
1260
|
}
|
|
1259
1261
|
getKeyDeriver(privKey) {
|
|
1260
|
-
if (
|
|
1262
|
+
if (this.params.keyDeriver?.rootKey === privKey) return this.params.keyDeriver;
|
|
1263
|
+
if (typeof privKey === "string") {
|
|
1264
|
+
if (this.params.keyDeriver?.rootKey.toHex() === privKey) return this.params.keyDeriver;
|
|
1265
|
+
privKey = _bsv_sdk.PrivateKey.fromHex(privKey);
|
|
1266
|
+
}
|
|
1261
1267
|
if (this.params.keyDeriver == null || this.params.keyDeriver.rootKey.toHex() !== privKey.toHex()) return new _bsv_sdk.CachedKeyDeriver(privKey);
|
|
1262
1268
|
return this.params.keyDeriver;
|
|
1263
1269
|
}
|
|
@@ -1266,8 +1272,11 @@ var ScriptTemplateBRC29 = class {
|
|
|
1266
1272
|
return this.p2pkh.lock(address);
|
|
1267
1273
|
}
|
|
1268
1274
|
unlock(unlockerPrivKey, lockerPubKey, sourceSatoshis, lockingScript) {
|
|
1269
|
-
const derivedPrivateKey = this.getKeyDeriver(unlockerPrivKey).derivePrivateKey(brc29ProtocolID, this.getKeyID(), lockerPubKey)
|
|
1270
|
-
return this.
|
|
1275
|
+
const derivedPrivateKey = this.getKeyDeriver(unlockerPrivKey).derivePrivateKey(brc29ProtocolID, this.getKeyID(), lockerPubKey);
|
|
1276
|
+
return this.unlockWithDerivedPrivateKey(derivedPrivateKey, sourceSatoshis, lockingScript);
|
|
1277
|
+
}
|
|
1278
|
+
unlockWithDerivedPrivateKey(derivedPrivateKey, sourceSatoshis, lockingScript) {
|
|
1279
|
+
return this.p2pkh.unlock(derivedPrivateKey, "all", false, sourceSatoshis, lockingScript);
|
|
1271
1280
|
}
|
|
1272
1281
|
/**
|
|
1273
1282
|
* P2PKH unlock estimateLength is a constant
|
|
@@ -2461,11 +2470,16 @@ var EntityProvenTx = class EntityProvenTx extends EntityBase {
|
|
|
2461
2470
|
/**
|
|
2462
2471
|
* @returns desirialized `MerklePath` object, value is cached.
|
|
2463
2472
|
*/
|
|
2464
|
-
getMerklePath() {
|
|
2465
|
-
|
|
2466
|
-
|
|
2473
|
+
getMerklePath(validateRoots = true) {
|
|
2474
|
+
if (validateRoots) {
|
|
2475
|
+
this._mp ??= _bsv_sdk.MerklePath.fromBinary(this.api.merklePath);
|
|
2476
|
+
return this._mp;
|
|
2477
|
+
}
|
|
2478
|
+
this._mpUnchecked ??= _bsv_sdk.MerklePath.fromBinary(this.api.merklePath, true, false);
|
|
2479
|
+
return this._mpUnchecked;
|
|
2467
2480
|
}
|
|
2468
2481
|
_mp;
|
|
2482
|
+
_mpUnchecked;
|
|
2469
2483
|
get provenTxId() {
|
|
2470
2484
|
return this.api.provenTxId;
|
|
2471
2485
|
}
|
|
@@ -5062,8 +5076,10 @@ async function mergeInputBeefs(rawTx, beef, trustSelf, knownTxids, trx, required
|
|
|
5062
5076
|
for (const input of tx.inputs) {
|
|
5063
5077
|
const sourceTXID = input.sourceTXID ?? "";
|
|
5064
5078
|
if (sourceTXID === "") throw new WERR_INTERNAL("req all transaction inputs must have valid sourceTXID");
|
|
5065
|
-
|
|
5066
|
-
|
|
5079
|
+
const existing = beef.findTxid(sourceTXID);
|
|
5080
|
+
const callerKnows = (requiredLevels == null || requiredLevels === 0) && knownTxids?.includes(sourceTXID) === true;
|
|
5081
|
+
if (existing != null && (!existing.isTxidOnly || callerKnows || trustSelf === "known")) continue;
|
|
5082
|
+
if (callerKnows) beef.mergeTxidOnly(sourceTXID);
|
|
5067
5083
|
else await getValidBeef(sourceTXID, beef, trustSelf, knownTxids, trx, requiredLevels);
|
|
5068
5084
|
}
|
|
5069
5085
|
}
|
|
@@ -5127,26 +5143,22 @@ async function notifyTransactionsOfProof(ids, provenTxId, addNote, updateTransac
|
|
|
5127
5143
|
* @param options
|
|
5128
5144
|
*/
|
|
5129
5145
|
async function getBeefForTransaction(storage, txid, options) {
|
|
5130
|
-
|
|
5131
|
-
if (options.mergeToBeef instanceof _bsv_sdk.Beef) beef = options.mergeToBeef;
|
|
5132
|
-
else if (options.mergeToBeef != null) beef = _bsv_sdk.Beef.fromBinary(options.mergeToBeef);
|
|
5133
|
-
else beef = new _bsv_sdk.Beef();
|
|
5146
|
+
const beef = mergeTarget(options);
|
|
5134
5147
|
const hasKnownTxid = makeKnownTxidLookup$1(options.knownTxids ?? []);
|
|
5135
5148
|
const scheduled = /* @__PURE__ */ new Set([txid]);
|
|
5136
5149
|
let frontier = [{
|
|
5137
5150
|
txid,
|
|
5138
5151
|
depth: 0
|
|
5139
5152
|
}];
|
|
5140
|
-
const
|
|
5141
|
-
const concurrency = Number.isFinite(requestedConcurrency) ? Math.max(1, Math.min(32, Math.floor(requestedConcurrency))) : 8;
|
|
5153
|
+
const concurrency = normalizeConcurrency(options.maxConcurrency);
|
|
5142
5154
|
while (frontier.length > 0) {
|
|
5143
|
-
const current = frontier.filter((item) => beef
|
|
5155
|
+
const current = frontier.filter((item) => needsResolution(beef, item.txid, hasKnownTxid));
|
|
5144
5156
|
const resolved = await mapWithConcurrency(current, concurrency, async (item) => await resolveBeefForTransaction(storage, item.txid, options, hasKnownTxid, item.depth));
|
|
5145
5157
|
const next = [];
|
|
5146
5158
|
for (let i = 0; i < resolved.length; i++) {
|
|
5147
5159
|
const result = resolved[i];
|
|
5148
5160
|
beef.mergeBeef(result.beef);
|
|
5149
|
-
for (const dependency of result.dependencies) if (!scheduled.has(dependency) && beef
|
|
5161
|
+
for (const dependency of result.dependencies) if (!scheduled.has(dependency) && needsResolution(beef, dependency, hasKnownTxid)) {
|
|
5150
5162
|
scheduled.add(dependency);
|
|
5151
5163
|
next.push({
|
|
5152
5164
|
txid: dependency,
|
|
@@ -5158,6 +5170,172 @@ async function getBeefForTransaction(storage, txid, options) {
|
|
|
5158
5170
|
}
|
|
5159
5171
|
return beef;
|
|
5160
5172
|
}
|
|
5173
|
+
/**
|
|
5174
|
+
* Build one aggregate BEEF for several roots while resolving each storage
|
|
5175
|
+
* frontier as a set. This avoids one proof query per funding input on the
|
|
5176
|
+
* createAction success path. Complex proof-level and chain-tracker policies
|
|
5177
|
+
* retain the established single-root implementation.
|
|
5178
|
+
*/
|
|
5179
|
+
async function getBeefForTransactions(storage, txids, options) {
|
|
5180
|
+
const beef = mergeTarget(options);
|
|
5181
|
+
const roots = [...new Set(txids)];
|
|
5182
|
+
if (roots.length === 0) return beef;
|
|
5183
|
+
if (requiresSingleRootPolicy(options)) return await mergeSingleRootFragments(storage, roots, options, beef);
|
|
5184
|
+
const hasKnownTxid = makeKnownTxidLookup$1(options.knownTxids ?? []);
|
|
5185
|
+
const scheduled = new Set(roots);
|
|
5186
|
+
let frontier = roots.map((txid) => ({
|
|
5187
|
+
txid,
|
|
5188
|
+
depth: 0
|
|
5189
|
+
}));
|
|
5190
|
+
while (frontier.length > 0) {
|
|
5191
|
+
const unresolved = collectUnresolvedFrontier(storage, frontier, beef, hasKnownTxid);
|
|
5192
|
+
if (unresolved.length === 0) break;
|
|
5193
|
+
const stored = await storage.getProvenOrRawTxs(unresolved.map((item) => item.txid));
|
|
5194
|
+
if (options.trustSelf !== "known" && unresolved.every((item) => stored.get(item.txid)?.proven != null)) {
|
|
5195
|
+
mergeAllProven(storage, beef, unresolved, stored);
|
|
5196
|
+
break;
|
|
5197
|
+
}
|
|
5198
|
+
const [next, missing] = mergeStoredFrontier(beef, unresolved, stored, options, scheduled, hasKnownTxid);
|
|
5199
|
+
await mergeMissingFragments(storage, beef, missing, options);
|
|
5200
|
+
frontier = next;
|
|
5201
|
+
}
|
|
5202
|
+
return beef;
|
|
5203
|
+
}
|
|
5204
|
+
function mergeTarget(options) {
|
|
5205
|
+
if (options.mergeToBeef instanceof _bsv_sdk.Beef) return options.mergeToBeef;
|
|
5206
|
+
if (options.mergeToBeef != null) return _bsv_sdk.Beef.fromBinary(options.mergeToBeef);
|
|
5207
|
+
return new _bsv_sdk.Beef();
|
|
5208
|
+
}
|
|
5209
|
+
function requiresSingleRootPolicy(options) {
|
|
5210
|
+
return options.ignoreStorage === true || options.minProofLevel !== void 0 || options.chainTracker != null || options.skipInvalidProofs === true;
|
|
5211
|
+
}
|
|
5212
|
+
async function mergeSingleRootFragments(storage, roots, options, beef) {
|
|
5213
|
+
const fragments = await mapWithConcurrency(roots.filter((txid) => beef.findTxid(txid) == null), normalizeConcurrency(options.maxConcurrency), async (txid) => await getBeefForTransaction(storage, txid, {
|
|
5214
|
+
...options,
|
|
5215
|
+
mergeToBeef: void 0
|
|
5216
|
+
}));
|
|
5217
|
+
for (const fragment of fragments) beef.mergeBeef(fragment);
|
|
5218
|
+
return beef;
|
|
5219
|
+
}
|
|
5220
|
+
function collectUnresolvedFrontier(storage, frontier, beef, hasKnownTxid) {
|
|
5221
|
+
const unresolved = [];
|
|
5222
|
+
for (const item of frontier) {
|
|
5223
|
+
if (!needsResolution(beef, item.txid, hasKnownTxid)) continue;
|
|
5224
|
+
if (storage.maxRecursionDepth && storage.maxRecursionDepth <= item.depth) throw new WERR_INVALID_OPERATION(`Maximum BEEF depth exceeded. Limit is ${storage.maxRecursionDepth}`);
|
|
5225
|
+
if (hasKnownTxid(item.txid)) beef.mergeTxidOnly(item.txid);
|
|
5226
|
+
else unresolved.push(item);
|
|
5227
|
+
}
|
|
5228
|
+
return unresolved;
|
|
5229
|
+
}
|
|
5230
|
+
function decodeProvenEntries(storage, unresolved, stored) {
|
|
5231
|
+
const span = storage.telemetry.enabled ? storage.telemetry.startSpan("wallet.storage.beef.decode_proven_batch", {
|
|
5232
|
+
component: "wallet-storage",
|
|
5233
|
+
attributes: { "beef.proven_tx_count": unresolved.length }
|
|
5234
|
+
}) : void 0;
|
|
5235
|
+
try {
|
|
5236
|
+
const entries = unresolved.map((item) => {
|
|
5237
|
+
const proven = stored.get(item.txid).proven;
|
|
5238
|
+
return {
|
|
5239
|
+
rawTx: proven.rawTx,
|
|
5240
|
+
merklePath: new EntityProvenTx(proven).getMerklePath(false),
|
|
5241
|
+
merkleRoot: proven.merkleRoot
|
|
5242
|
+
};
|
|
5243
|
+
});
|
|
5244
|
+
span?.end({ attributes: { "beef.decoded_proof_count": entries.length } });
|
|
5245
|
+
return entries;
|
|
5246
|
+
} catch (error) {
|
|
5247
|
+
span?.end({
|
|
5248
|
+
status: "error",
|
|
5249
|
+
error
|
|
5250
|
+
});
|
|
5251
|
+
throw error;
|
|
5252
|
+
}
|
|
5253
|
+
}
|
|
5254
|
+
function mergeAllProven(storage, beef, unresolved, stored) {
|
|
5255
|
+
const entries = decodeProvenEntries(storage, unresolved, stored);
|
|
5256
|
+
const span = storage.telemetry.enabled ? storage.telemetry.startSpan("wallet.storage.beef.merge_proven_batch", {
|
|
5257
|
+
component: "wallet-storage",
|
|
5258
|
+
attributes: { "beef.proven_tx_count": entries.length }
|
|
5259
|
+
}) : void 0;
|
|
5260
|
+
try {
|
|
5261
|
+
mergeProvenEntries(beef, entries, unresolved, stored);
|
|
5262
|
+
span?.end({ attributes: {
|
|
5263
|
+
"beef.merged_tx_count": entries.length,
|
|
5264
|
+
"beef.result_tx_count": beef.txs.length,
|
|
5265
|
+
"beef.result_bump_count": beef.bumps.length
|
|
5266
|
+
} });
|
|
5267
|
+
} catch (error) {
|
|
5268
|
+
span?.end({
|
|
5269
|
+
status: "error",
|
|
5270
|
+
error
|
|
5271
|
+
});
|
|
5272
|
+
throw error;
|
|
5273
|
+
}
|
|
5274
|
+
}
|
|
5275
|
+
function mergeProvenEntries(beef, entries, unresolved, stored) {
|
|
5276
|
+
if (typeof beef.mergeProvenTxs === "function") {
|
|
5277
|
+
beef.mergeProvenTxs(entries);
|
|
5278
|
+
return;
|
|
5279
|
+
}
|
|
5280
|
+
for (const item of unresolved) {
|
|
5281
|
+
const proven = stored.get(item.txid).proven;
|
|
5282
|
+
beef.mergeRawTx(proven.rawTx);
|
|
5283
|
+
beef.mergeBump(new EntityProvenTx(proven).getMerklePath());
|
|
5284
|
+
}
|
|
5285
|
+
}
|
|
5286
|
+
function mergeStoredFrontier(beef, unresolved, stored, options, scheduled, hasKnownTxid) {
|
|
5287
|
+
const next = [];
|
|
5288
|
+
const missing = [];
|
|
5289
|
+
for (const item of unresolved) {
|
|
5290
|
+
const result = stored.get(item.txid);
|
|
5291
|
+
if (result?.proven != null) mergeStoredProven(beef, item, result, options);
|
|
5292
|
+
else if (result?.rawTx != null) mergeStoredRaw(beef, item, result, options, scheduled, next, hasKnownTxid);
|
|
5293
|
+
else missing.push(item);
|
|
5294
|
+
}
|
|
5295
|
+
return [next, missing];
|
|
5296
|
+
}
|
|
5297
|
+
function mergeStoredProven(beef, item, result, options) {
|
|
5298
|
+
if (options.trustSelf === "known") {
|
|
5299
|
+
beef.mergeTxidOnly(item.txid);
|
|
5300
|
+
return;
|
|
5301
|
+
}
|
|
5302
|
+
const proven = result.proven;
|
|
5303
|
+
beef.mergeRawTx(proven.rawTx);
|
|
5304
|
+
beef.mergeBump(new EntityProvenTx(proven).getMerklePath());
|
|
5305
|
+
}
|
|
5306
|
+
function mergeStoredRaw(beef, item, result, options, scheduled, next, hasKnownTxid) {
|
|
5307
|
+
if (options.trustSelf === "known") {
|
|
5308
|
+
beef.mergeTxidOnly(item.txid);
|
|
5309
|
+
return;
|
|
5310
|
+
}
|
|
5311
|
+
const transaction = beef.mergeRawTx(result.rawTx);
|
|
5312
|
+
if (result.inputBEEF != null) beef.mergeBeef(result.inputBEEF);
|
|
5313
|
+
appendNewDependencies(transaction.inputTxids, item.depth + 1, beef, scheduled, next, hasKnownTxid);
|
|
5314
|
+
}
|
|
5315
|
+
function appendNewDependencies(dependencies, depth, beef, scheduled, next, hasKnownTxid) {
|
|
5316
|
+
for (const txid of dependencies) {
|
|
5317
|
+
if (scheduled.has(txid) || !needsResolution(beef, txid, hasKnownTxid)) continue;
|
|
5318
|
+
scheduled.add(txid);
|
|
5319
|
+
next.push({
|
|
5320
|
+
txid,
|
|
5321
|
+
depth
|
|
5322
|
+
});
|
|
5323
|
+
}
|
|
5324
|
+
}
|
|
5325
|
+
function needsResolution(beef, txid, hasKnownTxid) {
|
|
5326
|
+
const entry = beef.findTxid(txid);
|
|
5327
|
+
return entry == null || entry.isTxidOnly && !hasKnownTxid(txid);
|
|
5328
|
+
}
|
|
5329
|
+
async function mergeMissingFragments(storage, beef, missing, options) {
|
|
5330
|
+
if (missing.length === 0) return;
|
|
5331
|
+
if (options.ignoreServices === true) throw new WERR_INVALID_PARAMETER(`txid ${missing[0].txid}`, `valid transaction on chain ${storage.chain}`);
|
|
5332
|
+
const fragments = await mapWithConcurrency(missing, normalizeConcurrency(options.maxConcurrency), async (item) => await getBeefForTransaction(storage, item.txid, {
|
|
5333
|
+
...options,
|
|
5334
|
+
ignoreStorage: true,
|
|
5335
|
+
mergeToBeef: void 0
|
|
5336
|
+
}));
|
|
5337
|
+
for (const fragment of fragments) beef.mergeBeef(fragment);
|
|
5338
|
+
}
|
|
5161
5339
|
function makeKnownTxidLookup$1(knownTxids) {
|
|
5162
5340
|
let lookups = 0;
|
|
5163
5341
|
let indexed;
|
|
@@ -5171,6 +5349,9 @@ function makeKnownTxidLookup$1(knownTxids) {
|
|
|
5171
5349
|
return knownTxids.includes(txid);
|
|
5172
5350
|
};
|
|
5173
5351
|
}
|
|
5352
|
+
function normalizeConcurrency(value = 8) {
|
|
5353
|
+
return Number.isFinite(value) ? Math.max(1, Math.min(32, Math.floor(value))) : 8;
|
|
5354
|
+
}
|
|
5174
5355
|
async function mapWithConcurrency(values, concurrency, mapper) {
|
|
5175
5356
|
const results = Array.from({ length: values.length }, () => void 0);
|
|
5176
5357
|
let cursor = 0;
|
|
@@ -5299,6 +5480,23 @@ async function createMergedBeefOfTxids(txids, storage) {
|
|
|
5299
5480
|
//#endregion
|
|
5300
5481
|
//#region ../src/storage/methods/processAction.ts
|
|
5301
5482
|
async function processAction$1(storage, auth, args) {
|
|
5483
|
+
if (!storage.telemetry.enabled) return await processActionCore(storage, auth, args);
|
|
5484
|
+
return await storage.telemetry.withSpan("wallet.storage.process_action", {
|
|
5485
|
+
component: "wallet-storage",
|
|
5486
|
+
carrier: args,
|
|
5487
|
+
attributes: {
|
|
5488
|
+
"action.is_new_transaction": args.isNewTx,
|
|
5489
|
+
"action.is_no_send": args.isNoSend,
|
|
5490
|
+
"action.is_delayed": args.isDelayed,
|
|
5491
|
+
"action.send_with_count": args.sendWith.length
|
|
5492
|
+
}
|
|
5493
|
+
}, async (span) => {
|
|
5494
|
+
const result = await processActionCore(storage, auth, args, span);
|
|
5495
|
+
span.end({ attributes: { "action.send_result_count": result.sendWithResults?.length ?? 0 } });
|
|
5496
|
+
return result;
|
|
5497
|
+
});
|
|
5498
|
+
}
|
|
5499
|
+
async function processActionCore(storage, auth, args, parent) {
|
|
5302
5500
|
const logger = args.logger;
|
|
5303
5501
|
logger?.group("storage processAction");
|
|
5304
5502
|
const userId = verifyId(auth.userId);
|
|
@@ -5306,9 +5504,9 @@ async function processAction$1(storage, auth, args) {
|
|
|
5306
5504
|
let req;
|
|
5307
5505
|
const txidsOfReqsToShareWithWorld = [...args.sendWith];
|
|
5308
5506
|
if (args.isNewTx) {
|
|
5309
|
-
const vargs = await validateCommitNewTxToStorageArgs(storage, userId, args);
|
|
5507
|
+
const vargs = await traceProcessStep(storage, "wallet.storage.process_action.validate", parent, async () => await validateCommitNewTxToStorageArgs(storage, userId, args));
|
|
5310
5508
|
logger?.log("validated new tx updates to storage");
|
|
5311
|
-
({req} = await commitNewTxToStorage(storage, userId, vargs));
|
|
5509
|
+
({req} = await traceProcessStep(storage, "wallet.storage.process_action.commit", parent, async () => await commitNewTxToStorage(storage, userId, vargs)));
|
|
5312
5510
|
logger?.log("committed new tx updates to storage ");
|
|
5313
5511
|
if (!req) throw new WERR_INTERNAL();
|
|
5314
5512
|
if (args.isNoSend && !args.isSendWith) logger?.log(`noSend txid ${req.txid}`);
|
|
@@ -5317,12 +5515,19 @@ async function processAction$1(storage, auth, args) {
|
|
|
5317
5515
|
logger?.log(`sending txid ${req.txid}`);
|
|
5318
5516
|
}
|
|
5319
5517
|
}
|
|
5320
|
-
const { swr, ndr } = await shareReqsWithWorld(storage, userId, txidsOfReqsToShareWithWorld, args.isDelayed, void 0, logger);
|
|
5518
|
+
const { swr, ndr } = await traceProcessStep(storage, "wallet.storage.process_action.share", parent, async () => await shareReqsWithWorld(storage, userId, txidsOfReqsToShareWithWorld, args.isDelayed, void 0, logger));
|
|
5321
5519
|
r.sendWithResults = swr;
|
|
5322
5520
|
r.notDelayedResults = ndr;
|
|
5323
5521
|
logger?.groupEnd();
|
|
5324
5522
|
return r;
|
|
5325
5523
|
}
|
|
5524
|
+
async function traceProcessStep(storage, name, parent, callback) {
|
|
5525
|
+
if (parent == null) return await callback();
|
|
5526
|
+
return await storage.telemetry.withSpan(name, {
|
|
5527
|
+
component: "wallet-storage",
|
|
5528
|
+
parent: parent.context
|
|
5529
|
+
}, callback);
|
|
5530
|
+
}
|
|
5326
5531
|
/**
|
|
5327
5532
|
* Verifies that all the txids are known reqs with ready-to-share status.
|
|
5328
5533
|
* Assigns a batch identifier and updates all the provenTxReqs.
|
|
@@ -5493,21 +5698,16 @@ async function validateCommitNewTxToStorageArgs(storage, userId, params) {
|
|
|
5493
5698
|
} }));
|
|
5494
5699
|
if (!transaction.isOutgoing) throw new WERR_INVALID_OPERATION("isOutgoing is not true");
|
|
5495
5700
|
if (transaction.inputBEEF == null) throw new WERR_INVALID_OPERATION();
|
|
5496
|
-
const beef = _bsv_sdk.Beef.fromBinary(asArray(transaction.inputBEEF));
|
|
5497
5701
|
if (transaction.status !== "unsigned" && transaction.status !== "unprocessed") throw new WERR_INVALID_OPERATION(`invalid transaction status ${transaction.status}`);
|
|
5498
5702
|
const transactionId = verifyId(transaction.transactionId);
|
|
5499
|
-
const outputOutputs = await storage.findOutputs({ partial: {
|
|
5703
|
+
const [outputOutputs, commissionRows] = await Promise.all([storage.findOutputs({ partial: {
|
|
5500
5704
|
userId,
|
|
5501
5705
|
transactionId
|
|
5502
|
-
} })
|
|
5503
|
-
const inputOutputs = await storage.findOutputs({ partial: {
|
|
5504
|
-
userId,
|
|
5505
|
-
spentBy: transactionId
|
|
5506
|
-
} });
|
|
5507
|
-
const commission = verifyOneOrNone(await storage.findCommissions({ partial: {
|
|
5706
|
+
} }), storage.commissionSatoshis > 0 ? storage.findCommissions({ partial: {
|
|
5508
5707
|
transactionId,
|
|
5509
5708
|
userId
|
|
5510
|
-
} }));
|
|
5709
|
+
} }) : Promise.resolve([])]);
|
|
5710
|
+
const commission = verifyOneOrNone(commissionRows);
|
|
5511
5711
|
if (storage.commissionSatoshis > 0) {
|
|
5512
5712
|
if (commission == null) throw new WERR_INTERNAL();
|
|
5513
5713
|
if (!tx.outputs.some((x) => x.satoshis === commission.satoshis && x.lockingScript.toHex() === asString(commission.lockingScript))) throw new WERR_INVALID_OPERATION("Transaction did not include an output to cover service fee.");
|
|
@@ -5527,10 +5727,7 @@ async function validateCommitNewTxToStorageArgs(storage, userId, params) {
|
|
|
5527
5727
|
txScriptOffsets,
|
|
5528
5728
|
transactionId,
|
|
5529
5729
|
transaction,
|
|
5530
|
-
inputOutputs,
|
|
5531
5730
|
outputOutputs,
|
|
5532
|
-
commission,
|
|
5533
|
-
beef,
|
|
5534
5731
|
req,
|
|
5535
5732
|
outputUpdates: [],
|
|
5536
5733
|
transactionUpdate: {
|
|
@@ -6208,17 +6405,24 @@ async function generateChangeSdkCore(params, allocateChangeInput, releaseChangeI
|
|
|
6208
6405
|
};
|
|
6209
6406
|
const fixedInputs = params.fixedInputs;
|
|
6210
6407
|
const fixedOutputs = params.fixedOutputs;
|
|
6408
|
+
const fixedFunding = fixedInputs.reduce((sum, input) => sum + input.satoshis, 0);
|
|
6409
|
+
let fixedSpending = fixedOutputs.reduce((sum, output) => sum + output.satoshis, 0);
|
|
6410
|
+
const fixedInputSize = fixedInputs.reduce((sum, input) => sum + transactionInputSize(input.unlockingScriptLength), 0);
|
|
6411
|
+
const fixedOutputSize = fixedOutputs.reduce((sum, output) => sum + transactionOutputSize(output.lockingScriptLength), 0);
|
|
6412
|
+
const changeInputSize = transactionInputSize(params.changeUnlockingScriptLength);
|
|
6413
|
+
const changeOutputSize = transactionOutputSize(params.changeLockingScriptLength);
|
|
6414
|
+
let allocatedFunding = 0;
|
|
6211
6415
|
/**
|
|
6212
6416
|
* @returns sum of transaction fixedInputs satoshis and fundingInputs satoshis
|
|
6213
6417
|
*/
|
|
6214
6418
|
const funding = () => {
|
|
6215
|
-
return
|
|
6419
|
+
return fixedFunding + allocatedFunding;
|
|
6216
6420
|
};
|
|
6217
6421
|
/**
|
|
6218
6422
|
* @returns sum of transaction fixedOutputs satoshis
|
|
6219
6423
|
*/
|
|
6220
6424
|
const spending = () => {
|
|
6221
|
-
return
|
|
6425
|
+
return fixedSpending;
|
|
6222
6426
|
};
|
|
6223
6427
|
/**
|
|
6224
6428
|
* @returns sum of transaction changeOutputs satoshis
|
|
@@ -6228,7 +6432,9 @@ async function generateChangeSdkCore(params, allocateChangeInput, releaseChangeI
|
|
|
6228
6432
|
};
|
|
6229
6433
|
const fee = () => funding() - spending() - change();
|
|
6230
6434
|
const size = (addedChangeInputs, addedChangeOutputs) => {
|
|
6231
|
-
|
|
6435
|
+
const inputCount = fixedInputs.length + r.allocatedChangeInputs.length + (addedChangeInputs || 0);
|
|
6436
|
+
const outputCount = fixedOutputs.length + r.changeOutputs.length + (addedChangeOutputs || 0);
|
|
6437
|
+
return 4 + varUintSize(inputCount) + fixedInputSize + (r.allocatedChangeInputs.length + (addedChangeInputs || 0)) * changeInputSize + varUintSize(outputCount) + fixedOutputSize + (r.changeOutputs.length + (addedChangeOutputs || 0)) * changeOutputSize + 4;
|
|
6232
6438
|
};
|
|
6233
6439
|
/**
|
|
6234
6440
|
* @returns the target fee required for the transaction as currently configured under feeModel.
|
|
@@ -6267,7 +6473,10 @@ async function generateChangeSdkCore(params, allocateChangeInput, releaseChangeI
|
|
|
6267
6473
|
const releaseAllocatedChangeInputs = async () => {
|
|
6268
6474
|
while (r.allocatedChangeInputs.length > 0) {
|
|
6269
6475
|
const i = r.allocatedChangeInputs.pop();
|
|
6270
|
-
if (i != null)
|
|
6476
|
+
if (i != null) {
|
|
6477
|
+
allocatedFunding -= i.satoshis;
|
|
6478
|
+
await releaseChangeInput(i.outputId);
|
|
6479
|
+
}
|
|
6271
6480
|
}
|
|
6272
6481
|
feeExcessNow = feeExcess();
|
|
6273
6482
|
};
|
|
@@ -6302,6 +6511,7 @@ async function generateChangeSdkCore(params, allocateChangeInput, releaseChangeI
|
|
|
6302
6511
|
const allocatedChangeInput = await allocateChangeInput(-feeExcess(1, ao) + (ao === 1 ? 2 * params.changeInitialSatoshis : 0) + changeBuffer, exactSatoshis);
|
|
6303
6512
|
if (allocatedChangeInput == null) return false;
|
|
6304
6513
|
r.allocatedChangeInputs.push(allocatedChangeInput);
|
|
6514
|
+
allocatedFunding += allocatedChangeInput.satoshis;
|
|
6305
6515
|
maybeAddChangeOutput(ao);
|
|
6306
6516
|
return true;
|
|
6307
6517
|
};
|
|
@@ -6313,6 +6523,7 @@ async function generateChangeSdkCore(params, allocateChangeInput, releaseChangeI
|
|
|
6313
6523
|
while (r.changeOutputs.length > 0 && feeExcess() < 0) r.changeOutputs.pop();
|
|
6314
6524
|
if (feeExcess() < 0) break;
|
|
6315
6525
|
removeChurnPairs(r.allocatedChangeInputs, r.changeOutputs);
|
|
6526
|
+
allocatedFunding = r.allocatedChangeInputs.reduce((sum, input) => sum + input.satoshis, 0);
|
|
6316
6527
|
}
|
|
6317
6528
|
};
|
|
6318
6529
|
/**
|
|
@@ -6321,7 +6532,9 @@ async function generateChangeSdkCore(params, allocateChangeInput, releaseChangeI
|
|
|
6321
6532
|
await fundTransaction();
|
|
6322
6533
|
if (feeExcess() < 0 && vgcpr.hasMaxPossibleOutput !== void 0) {
|
|
6323
6534
|
if (fixedOutputs[vgcpr.hasMaxPossibleOutput].satoshis !== 0x775f05a073fff) throw new WERR_INTERNAL();
|
|
6324
|
-
|
|
6535
|
+
const adjustment = feeExcess();
|
|
6536
|
+
fixedOutputs[vgcpr.hasMaxPossibleOutput].satoshis += adjustment;
|
|
6537
|
+
fixedSpending += adjustment;
|
|
6325
6538
|
r.maxPossibleSatoshisAdjustment = {
|
|
6326
6539
|
fixedOutputIndex: vgcpr.hasMaxPossibleOutput,
|
|
6327
6540
|
satoshis: fixedOutputs[vgcpr.hasMaxPossibleOutput].satoshis
|
|
@@ -6340,8 +6553,11 @@ async function generateChangeSdkCore(params, allocateChangeInput, releaseChangeI
|
|
|
6340
6553
|
* If needed, seek funding to avoid overspending on fees without a change output to recapture it.
|
|
6341
6554
|
*/
|
|
6342
6555
|
if (r.changeOutputs.length === 0 && feeExcessNow > 0) {
|
|
6556
|
+
const minimumChange = Math.max(dustFloor, params.changeFirstSatoshis);
|
|
6557
|
+
const totalSatoshisNeeded = spending() + feeTarget(0, 1) + minimumChange;
|
|
6558
|
+
const moreSatoshisNeeded = Math.max(1, totalSatoshisNeeded - funding());
|
|
6343
6559
|
await releaseAllocatedChangeInputs();
|
|
6344
|
-
throw new WERR_INSUFFICIENT_FUNDS(
|
|
6560
|
+
throw new WERR_INSUFFICIENT_FUNDS(totalSatoshisNeeded, moreSatoshisNeeded);
|
|
6345
6561
|
}
|
|
6346
6562
|
/**
|
|
6347
6563
|
* Distribute the excess fees across the changeOutputs added.
|
|
@@ -6665,6 +6881,8 @@ function makeChangeLock(out, dctr, args, changeKeys, wallet) {
|
|
|
6665
6881
|
}
|
|
6666
6882
|
//#endregion
|
|
6667
6883
|
//#region ../src/signer/methods/verifyUnlockScripts.ts
|
|
6884
|
+
const postChronicleHeightFallback = 943816;
|
|
6885
|
+
const canonicalP2PKHScope = _bsv_sdk.TransactionSignature.SIGHASH_ALL + _bsv_sdk.TransactionSignature.SIGHASH_FORKID;
|
|
6668
6886
|
const javaScriptOnlyVerifier = {
|
|
6669
6887
|
shouldVerifySpend: () => false,
|
|
6670
6888
|
verifySpend: async () => {
|
|
@@ -6676,10 +6894,11 @@ function invalidUnlockingScript(inputIndex, detail) {
|
|
|
6676
6894
|
return new WERR_INVALID_PARAMETER(`inputs[${inputIndex}].unlockScript`, `valid.${suffix}`);
|
|
6677
6895
|
}
|
|
6678
6896
|
async function verifyOneSpend(pending, verifier) {
|
|
6897
|
+
const [inputIndex, , spend, context] = pending;
|
|
6679
6898
|
try {
|
|
6680
|
-
if (!(verifier === void 0 ?
|
|
6899
|
+
if (!(verifier === void 0 ? spend.validate(context) : await spend.validateWith(verifier, context))) throw invalidUnlockingScript(inputIndex);
|
|
6681
6900
|
} catch (error) {
|
|
6682
|
-
if (error instanceof _bsv_sdk.ScriptEvaluationError) throw invalidUnlockingScript(
|
|
6901
|
+
if (error instanceof _bsv_sdk.ScriptEvaluationError) throw invalidUnlockingScript(inputIndex, error.message);
|
|
6683
6902
|
throw error;
|
|
6684
6903
|
}
|
|
6685
6904
|
}
|
|
@@ -6689,33 +6908,157 @@ async function verifyPendingSpends(pending, verifier) {
|
|
|
6689
6908
|
return;
|
|
6690
6909
|
}
|
|
6691
6910
|
const batched = [];
|
|
6692
|
-
for (const item of pending) if (verifier.shouldVerifySpend?.(item
|
|
6911
|
+
for (const item of pending) if (verifier.shouldVerifySpend?.(item[2], item[3]) !== false) batched.push(item);
|
|
6693
6912
|
else await verifyOneSpend(item, javaScriptOnlyVerifier);
|
|
6694
6913
|
if (batched.length === 0) return;
|
|
6695
6914
|
let verdicts;
|
|
6696
6915
|
try {
|
|
6697
6916
|
verdicts = await verifier.verifySpendsBatch(batched.map((item) => ({
|
|
6698
|
-
spend: item
|
|
6699
|
-
...item
|
|
6917
|
+
spend: item[2],
|
|
6918
|
+
...item[3]
|
|
6700
6919
|
})));
|
|
6701
6920
|
} catch (error) {
|
|
6702
|
-
if (error instanceof _bsv_sdk.ScriptEvaluationError) throw invalidUnlockingScript(batched[0]
|
|
6921
|
+
if (error instanceof _bsv_sdk.ScriptEvaluationError) throw invalidUnlockingScript(batched[0][0], error.message);
|
|
6703
6922
|
throw error;
|
|
6704
6923
|
}
|
|
6705
6924
|
if (verdicts.length !== batched.length) throw new Error("Script verifier returned an invalid batch result count");
|
|
6706
6925
|
verdicts.forEach((valid, index) => {
|
|
6707
|
-
if (!valid) throw invalidUnlockingScript(batched[index]
|
|
6926
|
+
if (!valid) throw invalidUnlockingScript(batched[index][0]);
|
|
6708
6927
|
});
|
|
6709
6928
|
}
|
|
6710
|
-
function
|
|
6711
|
-
const
|
|
6929
|
+
function wholeTransactionVerifier(verifier) {
|
|
6930
|
+
const candidate = verifier;
|
|
6931
|
+
return typeof candidate?.verifyScripts === "function" ? candidate : void 0;
|
|
6932
|
+
}
|
|
6933
|
+
function digestBatchVerifier(verifier) {
|
|
6934
|
+
const candidate = verifier;
|
|
6935
|
+
if (typeof candidate?.verifyDigestBatch !== "function") return void 0;
|
|
6936
|
+
if (candidate.isReady?.() === false) return void 0;
|
|
6937
|
+
if (candidate.supportsCrypto?.("verifyDigestBatch") === false) return void 0;
|
|
6938
|
+
return candidate;
|
|
6939
|
+
}
|
|
6940
|
+
function equalBytes(left, right) {
|
|
6941
|
+
if (left.length !== right.length) return false;
|
|
6942
|
+
for (let index = 0; index < left.length; index++) if (left[index] !== right[index]) return false;
|
|
6943
|
+
return true;
|
|
6944
|
+
}
|
|
6945
|
+
function isCanonicalP2PKHLock(lock) {
|
|
6946
|
+
return lock.length === 25 && lock[0] === 118 && lock[1] === 169 && lock[2] === 20 && lock[23] === 136 && lock[24] === 172;
|
|
6947
|
+
}
|
|
6948
|
+
function parseCanonicalP2PKHUnlock(unlock, lock) {
|
|
6949
|
+
const signatureLength = unlock[0];
|
|
6950
|
+
if (signatureLength == null || signatureLength < 9 || signatureLength > 73 || unlock.length !== 1 + signatureLength + 1 + 33 || unlock[1 + signatureLength] !== 33) return void 0;
|
|
6951
|
+
const checksig = Array.from(unlock.subarray(1, 1 + signatureLength));
|
|
6952
|
+
const publicKey = unlock.subarray(1 + signatureLength + 1);
|
|
6953
|
+
if (publicKey[0] !== 2 && publicKey[0] !== 3 || !equalBytes(_bsv_sdk.Hash.hash160(publicKey), lock.subarray(3, 23))) return void 0;
|
|
6954
|
+
let signature;
|
|
6955
|
+
try {
|
|
6956
|
+
signature = _bsv_sdk.TransactionSignature.fromChecksigFormat(checksig);
|
|
6957
|
+
} catch {
|
|
6958
|
+
return;
|
|
6959
|
+
}
|
|
6960
|
+
if (signature.scope !== canonicalP2PKHScope || !signature.hasLowS() || !equalBytes(signature.toChecksigFormat(), checksig)) return void 0;
|
|
6961
|
+
return [
|
|
6962
|
+
checksig,
|
|
6963
|
+
publicKey,
|
|
6964
|
+
signature
|
|
6965
|
+
];
|
|
6966
|
+
}
|
|
6967
|
+
/**
|
|
6968
|
+
* Recognizes only the exact canonical P2PKH shape generated by this wallet.
|
|
6969
|
+
* Anything else retains the general-purpose script interpreter/backend path.
|
|
6970
|
+
*/
|
|
6971
|
+
function standardP2PKHDigests(tx) {
|
|
6972
|
+
const cache = { hashOutputsSingle: /* @__PURE__ */ new Map() };
|
|
6973
|
+
const items = [];
|
|
6974
|
+
for (let inputIndex = 0; inputIndex < tx.inputs.length; inputIndex++) {
|
|
6975
|
+
const input = tx.inputs[inputIndex];
|
|
6976
|
+
const sourceTransaction = input.sourceTransaction;
|
|
6977
|
+
const sourceTXID = input.sourceTXID;
|
|
6978
|
+
const unlockingScript = input.unlockingScript;
|
|
6979
|
+
if (sourceTransaction == null || sourceTXID == null || unlockingScript == null) return void 0;
|
|
6980
|
+
const sourceOutput = sourceTransaction.outputs[input.sourceOutputIndex];
|
|
6981
|
+
if (sourceOutput == null) return void 0;
|
|
6982
|
+
const lock = sourceOutput.lockingScript.toUint8Array();
|
|
6983
|
+
if (!isCanonicalP2PKHLock(lock)) return void 0;
|
|
6984
|
+
const parsed = parseCanonicalP2PKHUnlock(unlockingScript.toUint8Array(), lock);
|
|
6985
|
+
if (parsed == null) return void 0;
|
|
6986
|
+
const [checksig, publicKey, signature] = parsed;
|
|
6987
|
+
const preimage = _bsv_sdk.TransactionSignature.formatBytes({
|
|
6988
|
+
sourceTXID,
|
|
6989
|
+
sourceOutputIndex: input.sourceOutputIndex,
|
|
6990
|
+
sourceSatoshis: sourceOutput.satoshis ?? 0,
|
|
6991
|
+
transactionVersion: tx.version,
|
|
6992
|
+
otherInputs: [],
|
|
6993
|
+
allInputs: tx.inputs,
|
|
6994
|
+
outputs: tx.outputs,
|
|
6995
|
+
inputIndex,
|
|
6996
|
+
subscript: sourceOutput.lockingScript,
|
|
6997
|
+
inputSequence: input.sequence ?? 4294967295,
|
|
6998
|
+
lockTime: tx.lockTime,
|
|
6999
|
+
scope: signature.scope,
|
|
7000
|
+
cache
|
|
7001
|
+
});
|
|
7002
|
+
items.push({
|
|
7003
|
+
publicKey,
|
|
7004
|
+
digest: Uint8Array.from(_bsv_sdk.Hash.hash256(preimage)),
|
|
7005
|
+
signature: Uint8Array.from(checksig.slice(0, -1))
|
|
7006
|
+
});
|
|
7007
|
+
}
|
|
7008
|
+
return items;
|
|
7009
|
+
}
|
|
7010
|
+
async function verifyStandardP2PKHDigests(pending, verifier) {
|
|
7011
|
+
if (pending.length === 0) return /* @__PURE__ */ new Set();
|
|
7012
|
+
const items = pending.flatMap((entry) => entry[1]);
|
|
7013
|
+
const verdicts = await verifier.verifyDigestBatch(items);
|
|
7014
|
+
if (verdicts.length !== items.length) throw new Error("Script verifier returned an invalid digest batch result count");
|
|
7015
|
+
const verified = /* @__PURE__ */ new Set();
|
|
7016
|
+
let offset = 0;
|
|
7017
|
+
for (const entry of pending) {
|
|
7018
|
+
const end = offset + entry[1].length;
|
|
7019
|
+
if (verdicts.slice(offset, end).every(Boolean)) verified.add(entry[0]);
|
|
7020
|
+
offset = end;
|
|
7021
|
+
}
|
|
7022
|
+
return verified;
|
|
7023
|
+
}
|
|
7024
|
+
function hydrateTransactionSources(txid, transactions) {
|
|
7025
|
+
const tx = transactions.get(txid);
|
|
7026
|
+
if (tx == null) throw new WERR_INVALID_PARAMETER("txid", `contained in beef, txid ${txid}`);
|
|
7027
|
+
for (let inputIndex = 0; inputIndex < tx.inputs.length; inputIndex++) {
|
|
7028
|
+
const input = tx.inputs[inputIndex];
|
|
7029
|
+
if (input.sourceTXID == null) throw new WERR_INVALID_PARAMETER(`inputs[${inputIndex}].sourceTXID`, "valid");
|
|
7030
|
+
if (input.unlockingScript == null) throw new WERR_INVALID_PARAMETER(`inputs[${inputIndex}].unlockingScript`, "valid");
|
|
7031
|
+
input.sourceTransaction = transactions.get(input.sourceTXID);
|
|
7032
|
+
if (input.sourceTransaction == null) return void 0;
|
|
7033
|
+
if (input.sourceTransaction.outputs[input.sourceOutputIndex] == null) throw new WERR_INVALID_PARAMETER(`inputs[${inputIndex}].sourceOutputIndex`, "reference an output in the source transaction");
|
|
7034
|
+
}
|
|
7035
|
+
return tx;
|
|
7036
|
+
}
|
|
7037
|
+
function transactionIndex(txids, beef) {
|
|
7038
|
+
if (txids.length > 0) beef.findTxid(txids[0]);
|
|
7039
|
+
return new Map(beef.txs.map((item) => [item.txid, item.tx]));
|
|
7040
|
+
}
|
|
7041
|
+
async function verifyWholeTransactions(pending, verifier) {
|
|
7042
|
+
if (pending.length === 0) return /* @__PURE__ */ new Set();
|
|
7043
|
+
let verdicts;
|
|
7044
|
+
try {
|
|
7045
|
+
verdicts = verifier.verifyScriptsBatch === void 0 ? await Promise.all(pending.map(async (item) => await verifier.verifyScripts(item[1]))) : await verifier.verifyScriptsBatch(pending.map((item) => item[1]));
|
|
7046
|
+
} catch (error) {
|
|
7047
|
+
if (error instanceof _bsv_sdk.ScriptEvaluationError) return /* @__PURE__ */ new Set();
|
|
7048
|
+
throw error;
|
|
7049
|
+
}
|
|
7050
|
+
if (verdicts.length !== pending.length) throw new Error("Script verifier returned an invalid transaction batch result count");
|
|
7051
|
+
return new Set(pending.filter((_, index) => verdicts[index]).map((item) => item[0]));
|
|
7052
|
+
}
|
|
7053
|
+
function collectTransactionSpends(txid, resultIndex, transactions, result, pending) {
|
|
7054
|
+
const tx = transactions.get(txid);
|
|
6712
7055
|
if (tx == null) throw new WERR_INVALID_PARAMETER("txid", `contained in beef, txid ${txid}`);
|
|
6713
7056
|
const sigHashCache = { hashOutputsSingle: /* @__PURE__ */ new Map() };
|
|
6714
7057
|
for (let inputIndex = 0; inputIndex < tx.inputs.length; inputIndex++) {
|
|
6715
7058
|
const input = tx.inputs[inputIndex];
|
|
6716
7059
|
if (input.sourceTXID == null) throw new WERR_INVALID_PARAMETER(`inputs[${inputIndex}].sourceTXID`, "valid");
|
|
6717
7060
|
if (input.unlockingScript == null) throw new WERR_INVALID_PARAMETER(`inputs[${inputIndex}].unlockingScript`, "valid");
|
|
6718
|
-
input.sourceTransaction =
|
|
7061
|
+
input.sourceTransaction = transactions.get(input.sourceTXID);
|
|
6719
7062
|
if (input.sourceTransaction == null) {
|
|
6720
7063
|
result.skippedInputs++;
|
|
6721
7064
|
continue;
|
|
@@ -6727,11 +7070,10 @@ function collectTransactionSpends(txid, resultIndex, beef, result, pending) {
|
|
|
6727
7070
|
consensus: true,
|
|
6728
7071
|
utxoHeight
|
|
6729
7072
|
};
|
|
6730
|
-
pending.push(
|
|
7073
|
+
pending.push([
|
|
6731
7074
|
inputIndex,
|
|
6732
7075
|
resultIndex,
|
|
6733
|
-
|
|
6734
|
-
spend: new _bsv_sdk.Spend({
|
|
7076
|
+
new _bsv_sdk.Spend({
|
|
6735
7077
|
sourceTXID: input.sourceTXID,
|
|
6736
7078
|
sourceOutputIndex: input.sourceOutputIndex,
|
|
6737
7079
|
lockingScript: sourceOutput.lockingScript,
|
|
@@ -6745,9 +7087,52 @@ function collectTransactionSpends(txid, resultIndex, beef, result, pending) {
|
|
|
6745
7087
|
outputs: tx.outputs,
|
|
6746
7088
|
lockTime: tx.lockTime,
|
|
6747
7089
|
sigHashCache
|
|
6748
|
-
})
|
|
6749
|
-
|
|
7090
|
+
}),
|
|
7091
|
+
context
|
|
7092
|
+
]);
|
|
7093
|
+
}
|
|
7094
|
+
}
|
|
7095
|
+
function collectAcceleratedTransactions(txids, transactions, digestVerifier, enabled) {
|
|
7096
|
+
const hydrated = /* @__PURE__ */ new Map();
|
|
7097
|
+
const digests = [];
|
|
7098
|
+
if (!enabled) return [hydrated, digests];
|
|
7099
|
+
for (let resultIndex = 0; resultIndex < txids.length; resultIndex++) {
|
|
7100
|
+
const tx = hydrateTransactionSources(txids[resultIndex], transactions);
|
|
7101
|
+
if (tx == null) continue;
|
|
7102
|
+
hydrated.set(resultIndex, tx);
|
|
7103
|
+
if (digestVerifier === void 0) continue;
|
|
7104
|
+
const items = standardP2PKHDigests(tx);
|
|
7105
|
+
if (items != null) digests.push([resultIndex, items]);
|
|
6750
7106
|
}
|
|
7107
|
+
return [hydrated, digests];
|
|
7108
|
+
}
|
|
7109
|
+
function collectWholeTransactionVerifications(hydrated, digestAttempted, verifier) {
|
|
7110
|
+
if (verifier === void 0) return [];
|
|
7111
|
+
const pending = [];
|
|
7112
|
+
for (const [resultIndex, tx] of hydrated) {
|
|
7113
|
+
if (digestAttempted.has(resultIndex)) continue;
|
|
7114
|
+
const params = {
|
|
7115
|
+
tx,
|
|
7116
|
+
blockHeight: postChronicleHeightFallback,
|
|
7117
|
+
consensus: true
|
|
7118
|
+
};
|
|
7119
|
+
if (verifier.shouldVerifyScripts?.(params) === false) continue;
|
|
7120
|
+
pending.push([resultIndex, params]);
|
|
7121
|
+
}
|
|
7122
|
+
return pending;
|
|
7123
|
+
}
|
|
7124
|
+
function collectFallbackSpends(txids, transactions, accelerated, results) {
|
|
7125
|
+
const pending = [];
|
|
7126
|
+
for (let resultIndex = 0; resultIndex < txids.length; resultIndex++) {
|
|
7127
|
+
if (!accelerated.has(resultIndex)) {
|
|
7128
|
+
collectTransactionSpends(txids[resultIndex], resultIndex, transactions, results[resultIndex], pending);
|
|
7129
|
+
continue;
|
|
7130
|
+
}
|
|
7131
|
+
const tx = transactions.get(txids[resultIndex]);
|
|
7132
|
+
if (tx == null) throw new WERR_INVALID_PARAMETER("txid", `contained in beef, txid ${txids[resultIndex]}`);
|
|
7133
|
+
results[resultIndex].verifiedInputs = tx.inputs.length;
|
|
7134
|
+
}
|
|
7135
|
+
return pending;
|
|
6751
7136
|
}
|
|
6752
7137
|
/**
|
|
6753
7138
|
* Verifies every resolvable input from several transactions in one optional
|
|
@@ -6758,10 +7143,17 @@ async function verifyUnlockScriptsBatch(txids, beef, verifier) {
|
|
|
6758
7143
|
verifiedInputs: 0,
|
|
6759
7144
|
skippedInputs: 0
|
|
6760
7145
|
}));
|
|
6761
|
-
const
|
|
6762
|
-
|
|
7146
|
+
const transactions = transactionIndex(txids, beef);
|
|
7147
|
+
const digestVerifier = digestBatchVerifier(verifier);
|
|
7148
|
+
const wholeVerifier = wholeTransactionVerifier(verifier);
|
|
7149
|
+
const [hydrated, digestPending] = collectAcceleratedTransactions(txids, transactions, digestVerifier, digestVerifier !== void 0 || wholeVerifier !== void 0);
|
|
7150
|
+
const digestAttempted = new Set(digestPending.map((item) => item[0]));
|
|
7151
|
+
const digestVerified = digestVerifier === void 0 ? /* @__PURE__ */ new Set() : await verifyStandardP2PKHDigests(digestPending, digestVerifier);
|
|
7152
|
+
const wholePending = collectWholeTransactionVerifications(hydrated, digestAttempted, wholeVerifier);
|
|
7153
|
+
const wholeVerified = wholeVerifier === void 0 ? /* @__PURE__ */ new Set() : await verifyWholeTransactions(wholePending, wholeVerifier);
|
|
7154
|
+
const pending = collectFallbackSpends(txids, transactions, /* @__PURE__ */ new Set([...digestVerified, ...wholeVerified]), results);
|
|
6763
7155
|
await verifyPendingSpends(pending, verifier);
|
|
6764
|
-
for (const item of pending) results[item
|
|
7156
|
+
for (const item of pending) results[item[1]].verifiedInputs++;
|
|
6765
7157
|
return results;
|
|
6766
7158
|
}
|
|
6767
7159
|
/**
|
|
@@ -6784,21 +7176,56 @@ async function completeSignedTransaction(prior, spends, wallet) {
|
|
|
6784
7176
|
input.unlockingScript = asBsvSdkScript(spend.unlockingScript);
|
|
6785
7177
|
if (spend.sequenceNumber !== void 0) input.sequence = spend.sequenceNumber;
|
|
6786
7178
|
}
|
|
6787
|
-
|
|
6788
|
-
const
|
|
6789
|
-
|
|
6790
|
-
|
|
6791
|
-
|
|
6792
|
-
|
|
6793
|
-
|
|
6794
|
-
|
|
6795
|
-
|
|
6796
|
-
|
|
6797
|
-
const
|
|
6798
|
-
|
|
6799
|
-
|
|
6800
|
-
|
|
6801
|
-
|
|
7179
|
+
const prepareUnlockingTemplates = (keys) => {
|
|
7180
|
+
const counterparties = /* @__PURE__ */ new Map();
|
|
7181
|
+
const counterparty = (publicKey) => {
|
|
7182
|
+
let parsed = counterparties.get(publicKey);
|
|
7183
|
+
if (parsed == null) {
|
|
7184
|
+
parsed = _bsv_sdk.PublicKey.fromString(publicKey);
|
|
7185
|
+
counterparties.set(publicKey, parsed);
|
|
7186
|
+
}
|
|
7187
|
+
return parsed;
|
|
7188
|
+
};
|
|
7189
|
+
const prepared = prior.pdi.map((pdi) => {
|
|
7190
|
+
return {
|
|
7191
|
+
pdi,
|
|
7192
|
+
template: new ScriptTemplateBRC29({
|
|
7193
|
+
derivationPrefix: pdi.derivationPrefix,
|
|
7194
|
+
derivationSuffix: pdi.derivationSuffix,
|
|
7195
|
+
keyDeriver: wallet.keyDeriver
|
|
7196
|
+
}),
|
|
7197
|
+
unlockerPubKey: counterparty(pdi.unlockerPubKey || keys.publicKey)
|
|
7198
|
+
};
|
|
7199
|
+
});
|
|
7200
|
+
const derivations = prepared.map(({ template, unlockerPubKey }) => ({
|
|
7201
|
+
protocolID: brc29ProtocolID,
|
|
7202
|
+
keyID: template.getKeyID(),
|
|
7203
|
+
counterparty: unlockerPubKey
|
|
7204
|
+
}));
|
|
7205
|
+
const derivedPrivateKeys = wallet.keyDeriver.derivePrivateKeys?.(derivations) ?? derivations.map((derivation) => wallet.keyDeriver.derivePrivateKey(derivation.protocolID, derivation.keyID, derivation.counterparty));
|
|
7206
|
+
for (let index = 0; index < prepared.length; index++) {
|
|
7207
|
+
const { pdi, template } = prepared[index];
|
|
7208
|
+
const unlockTemplate = template.unlockWithDerivedPrivateKey(derivedPrivateKeys[index], pdi.sourceSatoshis, asBsvSdkScript(pdi.lockingScript));
|
|
7209
|
+
const input = prior.tx.inputs[pdi.vin];
|
|
7210
|
+
input.unlockingScriptTemplate = unlockTemplate;
|
|
7211
|
+
}
|
|
7212
|
+
};
|
|
7213
|
+
if (wallet.telemetry.enabled && prior.pdi.length > 0) await wallet.telemetry.withSpan("wallet.crypto.prepare_unlocking_templates", {
|
|
7214
|
+
component: "wallet-toolbox",
|
|
7215
|
+
carrier: prior.args,
|
|
7216
|
+
attributes: { "crypto.managed_input_count": prior.pdi.length }
|
|
7217
|
+
}, async (span) => {
|
|
7218
|
+
const keys = await wallet.telemetry.withSpan("wallet.crypto.client_change_key", {
|
|
7219
|
+
component: "wallet-toolbox",
|
|
7220
|
+
parent: span.context
|
|
7221
|
+
}, () => wallet.getClientChangeKeyPair());
|
|
7222
|
+
await wallet.telemetry.withSpan("wallet.crypto.derive_unlocking_templates", {
|
|
7223
|
+
component: "wallet-toolbox",
|
|
7224
|
+
parent: span.context,
|
|
7225
|
+
attributes: { "crypto.managed_input_count": prior.pdi.length }
|
|
7226
|
+
}, () => prepareUnlockingTemplates(keys));
|
|
7227
|
+
});
|
|
7228
|
+
else if (prior.pdi.length > 0) prepareUnlockingTemplates(wallet.getClientChangeKeyPair());
|
|
6802
7229
|
if (wallet.telemetry.enabled) await wallet.telemetry.withSpan("wallet.crypto.transaction_sign", {
|
|
6803
7230
|
component: "wallet-toolbox",
|
|
6804
7231
|
carrier: prior.args,
|
|
@@ -6854,19 +7281,22 @@ async function createActionCore$1(wallet, auth, vargs, parent) {
|
|
|
6854
7281
|
prior.tx = await traceActionStep(wallet, "wallet.create_action.complete_signing", parent, async () => await completeSignedTransaction(prior, {}, wallet));
|
|
6855
7282
|
logger?.log("completed signed transaction");
|
|
6856
7283
|
r.txid = prior.tx.id("hex");
|
|
6857
|
-
const beef =
|
|
6858
|
-
|
|
6859
|
-
|
|
6860
|
-
|
|
6861
|
-
|
|
6862
|
-
|
|
7284
|
+
const beef = await traceActionStep(wallet, "wallet.create_action.assemble_result_beef", parent, () => {
|
|
7285
|
+
const result = new _bsv_sdk.Beef();
|
|
7286
|
+
if (prior.dcr.inputBeef != null) {
|
|
7287
|
+
const inputBeef = prior.dcr.inputBeef instanceof Uint8Array ? _bsv_sdk.Beef.fromBinaryView(prior.dcr.inputBeef) : _bsv_sdk.Beef.fromBinary(prior.dcr.inputBeef);
|
|
7288
|
+
result.mergeBeef(inputBeef);
|
|
7289
|
+
}
|
|
7290
|
+
result.mergeTransaction(prior.tx);
|
|
7291
|
+
return result;
|
|
7292
|
+
});
|
|
6863
7293
|
logger?.log("merged beef");
|
|
6864
7294
|
await traceActionStep(wallet, "wallet.create_action.verify_unlock_scripts", parent, async () => await verifyUnlockScripts(r.txid, beef, wallet.scriptVerifier));
|
|
6865
7295
|
logger?.log("verified unlock scripts");
|
|
6866
7296
|
r.noSendChange = prior.dcr.noSendChangeOutputVouts?.map((vout) => `${r.txid}.${vout}`);
|
|
6867
7297
|
beef.atomicTxid = r.txid;
|
|
6868
7298
|
setResultBeef(r, beef);
|
|
6869
|
-
if (!vargs.options.returnTXIDOnly) r.tx = beef.toUint8ArrayAtomic(r.txid);
|
|
7299
|
+
if (!vargs.options.returnTXIDOnly) r.tx = await traceActionStep(wallet, "wallet.create_action.serialize_result_beef", parent, () => beef.toUint8ArrayAtomic(r.txid));
|
|
6870
7300
|
}
|
|
6871
7301
|
const { sendWithResults, notDelayedResults } = await traceActionStep(wallet, "wallet.create_action.process", parent, async () => await processAction(prior, wallet, auth, vargs));
|
|
6872
7302
|
logger?.log("processed transaction");
|
|
@@ -7484,6 +7914,50 @@ function selectCanonicalChange(outputs, targetSatoshis, exactSatoshis) {
|
|
|
7484
7914
|
if (over != null) return over;
|
|
7485
7915
|
return outputs.filter((output) => output.satoshis < targetSatoshis).sort((a, b) => b.satoshis - a.satoshis || b.outputId - a.outputId)[0];
|
|
7486
7916
|
}
|
|
7917
|
+
/**
|
|
7918
|
+
* Stateful form of the canonical selector for allocating many inputs from one
|
|
7919
|
+
* candidate set. It preserves exact / least-over / largest-under ordering but
|
|
7920
|
+
* sorts once instead of filtering and sorting the full set per input.
|
|
7921
|
+
*/
|
|
7922
|
+
var CanonicalChangeSelector = class {
|
|
7923
|
+
sorted;
|
|
7924
|
+
allocated = /* @__PURE__ */ new Set();
|
|
7925
|
+
constructor(outputs) {
|
|
7926
|
+
this.sorted = [...outputs].sort((a, b) => a.satoshis - b.satoshis || a.outputId - b.outputId);
|
|
7927
|
+
}
|
|
7928
|
+
take(targetSatoshis, exactSatoshis) {
|
|
7929
|
+
if (exactSatoshis !== void 0) for (let index = this.lowerBound(exactSatoshis); index < this.sorted.length; index++) {
|
|
7930
|
+
const output = this.sorted[index];
|
|
7931
|
+
if (output.satoshis !== exactSatoshis) break;
|
|
7932
|
+
if (!this.allocated.has(output.outputId)) return this.allocate(output);
|
|
7933
|
+
}
|
|
7934
|
+
for (let index = this.lowerBound(targetSatoshis); index < this.sorted.length; index++) {
|
|
7935
|
+
const output = this.sorted[index];
|
|
7936
|
+
if (!this.allocated.has(output.outputId)) return this.allocate(output);
|
|
7937
|
+
}
|
|
7938
|
+
for (let index = this.lowerBound(targetSatoshis) - 1; index >= 0; index--) {
|
|
7939
|
+
const output = this.sorted[index];
|
|
7940
|
+
if (!this.allocated.has(output.outputId)) return this.allocate(output);
|
|
7941
|
+
}
|
|
7942
|
+
}
|
|
7943
|
+
release(outputId) {
|
|
7944
|
+
this.allocated.delete(outputId);
|
|
7945
|
+
}
|
|
7946
|
+
allocate(output) {
|
|
7947
|
+
this.allocated.add(output.outputId);
|
|
7948
|
+
return output;
|
|
7949
|
+
}
|
|
7950
|
+
lowerBound(satoshis) {
|
|
7951
|
+
let low = 0;
|
|
7952
|
+
let high = this.sorted.length;
|
|
7953
|
+
while (low < high) {
|
|
7954
|
+
const middle = low + high >>> 1;
|
|
7955
|
+
if (this.sorted[middle].satoshis < satoshis) low = middle + 1;
|
|
7956
|
+
else high = middle;
|
|
7957
|
+
}
|
|
7958
|
+
return low;
|
|
7959
|
+
}
|
|
7960
|
+
};
|
|
7487
7961
|
function repeatableRandom(randomVals) {
|
|
7488
7962
|
const values = [...randomVals ?? []];
|
|
7489
7963
|
return () => {
|
|
@@ -8619,6 +9093,22 @@ var ActionBatchController = class {
|
|
|
8619
9093
|
};
|
|
8620
9094
|
//#endregion
|
|
8621
9095
|
//#region ../src/Wallet.ts
|
|
9096
|
+
function prepareKnownTxidsForCreateAction(wallet, args) {
|
|
9097
|
+
if (!wallet.autoKnownTxids || args.options?.knownTxids != null) return;
|
|
9098
|
+
if (!wallet.telemetry.enabled) {
|
|
9099
|
+
args.options.knownTxids = wallet.getKnownTxids(args.options?.knownTxids);
|
|
9100
|
+
return;
|
|
9101
|
+
}
|
|
9102
|
+
args.options.knownTxids = wallet.telemetry.withSpan("wallet.create_action.prepare_known_txids", {
|
|
9103
|
+
component: "wallet-toolbox",
|
|
9104
|
+
carrier: args,
|
|
9105
|
+
attributes: { "beef.tx_count": wallet.beef.txs.length }
|
|
9106
|
+
}, (span) => {
|
|
9107
|
+
const knownTxids = wallet.getKnownTxids(args.options?.knownTxids);
|
|
9108
|
+
span.end({ attributes: { "beef.known_txid_count": knownTxids.length } });
|
|
9109
|
+
return knownTxids;
|
|
9110
|
+
});
|
|
9111
|
+
}
|
|
8622
9112
|
/**
|
|
8623
9113
|
* Build a {@link DiscoverCertificatesResult} from contact records so {@link Wallet.discoverByIdentityKey}
|
|
8624
9114
|
* and {@link Wallet.discoverByAttributes} can short-circuit on a local contacts hit. The synthetic
|
|
@@ -9092,6 +9582,7 @@ var Wallet = class {
|
|
|
9092
9582
|
if (this.returnTxidOnly) return beef;
|
|
9093
9583
|
const b = parsedBeef ?? _bsv_sdk.Beef.fromBinary(beef);
|
|
9094
9584
|
if (!b.atomicTxid) throw new WERR_INTERNAL();
|
|
9585
|
+
if (!b.txs.some((btx) => btx.isTxidOnly && !knownTxids?.includes(btx.txid))) return beef;
|
|
9095
9586
|
return this.verifyReturnedTxidOnly(b, knownTxids).toBinaryAtomic(b.atomicTxid);
|
|
9096
9587
|
}
|
|
9097
9588
|
verifyReturnedTxidOnlyBEEF(beef) {
|
|
@@ -9123,16 +9614,7 @@ var Wallet = class {
|
|
|
9123
9614
|
_bsv_sdk.Validation.validateOriginator(originator);
|
|
9124
9615
|
args.options ??= {};
|
|
9125
9616
|
args.options.trustSelf ||= this.trustSelf;
|
|
9126
|
-
|
|
9127
|
-
component: "wallet-toolbox",
|
|
9128
|
-
carrier: args,
|
|
9129
|
-
attributes: { "beef.tx_count": this.beef.txs.length }
|
|
9130
|
-
}, (span) => {
|
|
9131
|
-
const knownTxids = this.getKnownTxids(args.options?.knownTxids);
|
|
9132
|
-
span.end({ attributes: { "beef.known_txid_count": knownTxids.length } });
|
|
9133
|
-
return knownTxids;
|
|
9134
|
-
});
|
|
9135
|
-
else args.options.knownTxids = this.getKnownTxids(args.options.knownTxids);
|
|
9617
|
+
prepareKnownTxidsForCreateAction(this, args);
|
|
9136
9618
|
const { auth, vargs } = this.validateAuthAndArgs(args, _bsv_sdk.Validation.validateCreateActionArgs, logger);
|
|
9137
9619
|
logger?.log("validated args");
|
|
9138
9620
|
vargs.includeAllSourceTransactions = this.includeAllSourceTransactions;
|
|
@@ -9140,9 +9622,25 @@ var Wallet = class {
|
|
|
9140
9622
|
const r = await createAction$1(this, auth, vargs);
|
|
9141
9623
|
logger?.log("action created");
|
|
9142
9624
|
const resultBeef = getResultBeef(r);
|
|
9143
|
-
if (r.tx != null) this.beef.mergeBeefFromParty(this.storageParty, resultBeef ?? r.tx);
|
|
9144
9625
|
if (r.tx != null) {
|
|
9145
|
-
|
|
9626
|
+
const merge = () => this.beef.mergeBeefFromParty(this.storageParty, resultBeef ?? r.tx);
|
|
9627
|
+
if (this.telemetry.enabled) this.telemetry.withSpan("wallet.create_action.merge_result_beef", {
|
|
9628
|
+
component: "wallet-toolbox",
|
|
9629
|
+
carrier: args,
|
|
9630
|
+
attributes: {
|
|
9631
|
+
"beef.retained_tx_count_before": this.beef.txs.length,
|
|
9632
|
+
"beef.result_byte_count": r.tx.length
|
|
9633
|
+
}
|
|
9634
|
+
}, merge);
|
|
9635
|
+
else merge();
|
|
9636
|
+
}
|
|
9637
|
+
if (r.tx != null) {
|
|
9638
|
+
const verify = () => this.verifyReturnedTxidOnlyAtomicBEEF(r.tx, args.options?.knownTxids, resultBeef);
|
|
9639
|
+
r.tx = this.telemetry.enabled ? this.telemetry.withSpan("wallet.create_action.verify_result_beef", {
|
|
9640
|
+
component: "wallet-toolbox",
|
|
9641
|
+
carrier: args,
|
|
9642
|
+
attributes: { "beef.result_byte_count": r.tx.length }
|
|
9643
|
+
}, verify) : verify();
|
|
9146
9644
|
logger?.log("verify returned AtomicBEEF");
|
|
9147
9645
|
}
|
|
9148
9646
|
if (!vargs.isDelayed) throwIfAnyUnsuccessfulCreateActions(r);
|
|
@@ -9523,7 +10021,7 @@ async function createActionCore(storage, auth, vargs, parent) {
|
|
|
9523
10021
|
});
|
|
9524
10022
|
const feeModel = validateStorageFeeModel(storage.feeModel);
|
|
9525
10023
|
logger?.log(`validated fee model ${JSON.stringify(feeModel)}`);
|
|
9526
|
-
const initialFundingPlan = await prepareFundingPlan(storage,
|
|
10024
|
+
const initialFundingPlan = await prepareFundingPlan(storage, [
|
|
9527
10025
|
userId,
|
|
9528
10026
|
vargs,
|
|
9529
10027
|
xinputs,
|
|
@@ -9532,48 +10030,64 @@ async function createActionCore(storage, auth, vargs, parent) {
|
|
|
9532
10030
|
noSendChangeIn,
|
|
9533
10031
|
feeModel,
|
|
9534
10032
|
parent
|
|
9535
|
-
|
|
10033
|
+
]);
|
|
9536
10034
|
logger?.log(`planned funding from ${initialFundingPlan.availableChangeCount} change inputs`);
|
|
10035
|
+
const allocatedBeefPrefetch = startAllocatedChangeBeefPrefetch(storage, vargs, initialFundingPlan.selected, beef, parent);
|
|
10036
|
+
const storageBeefBytes = storageBeef.toBinary();
|
|
9537
10037
|
let newTx;
|
|
10038
|
+
let newTxCommitted = false;
|
|
9538
10039
|
try {
|
|
9539
|
-
const
|
|
9540
|
-
|
|
9541
|
-
"
|
|
9542
|
-
|
|
9543
|
-
|
|
9544
|
-
|
|
9545
|
-
|
|
9546
|
-
|
|
9547
|
-
|
|
9548
|
-
|
|
9549
|
-
|
|
9550
|
-
|
|
9551
|
-
|
|
9552
|
-
|
|
9553
|
-
|
|
9554
|
-
|
|
9555
|
-
|
|
9556
|
-
|
|
9557
|
-
|
|
9558
|
-
|
|
9559
|
-
|
|
9560
|
-
|
|
9561
|
-
|
|
9562
|
-
|
|
9563
|
-
|
|
9564
|
-
|
|
9565
|
-
|
|
9566
|
-
|
|
9567
|
-
|
|
9568
|
-
|
|
9569
|
-
|
|
9570
|
-
|
|
9571
|
-
const
|
|
9572
|
-
|
|
9573
|
-
|
|
10040
|
+
const persisted = await storage.transaction(async (trx) => {
|
|
10041
|
+
const initialSatoshis = fundingPlanSatoshis(initialFundingPlan);
|
|
10042
|
+
newTx = await traceStorageStep(storage, "wallet.storage.create_action.create_record", parent, {
|
|
10043
|
+
"action.label_count": vargs.labels.length,
|
|
10044
|
+
"action.storage_beef_bytes": storageBeefBytes.length
|
|
10045
|
+
}, async (span) => {
|
|
10046
|
+
const transaction = await createNewTxRecord(storage, userId, vargs, storageBeefBytes, initialSatoshis, trx);
|
|
10047
|
+
span?.end({ attributes: { "action.transaction_record_created": true } });
|
|
10048
|
+
return transaction;
|
|
10049
|
+
});
|
|
10050
|
+
logger?.log("created new transaction record");
|
|
10051
|
+
const ctx = {
|
|
10052
|
+
xinputs,
|
|
10053
|
+
xoutputs,
|
|
10054
|
+
changeBasket,
|
|
10055
|
+
noSendChangeIn,
|
|
10056
|
+
feeModel,
|
|
10057
|
+
transactionId: newTx.transactionId
|
|
10058
|
+
};
|
|
10059
|
+
const funded = await fundNewTransactionSdk(storage, userId, vargs, ctx, initialFundingPlan, parent, trx);
|
|
10060
|
+
logger?.log("funded new transaction");
|
|
10061
|
+
if (funded.maxPossibleSatoshisAdjustment != null) {
|
|
10062
|
+
const adjustment = funded.maxPossibleSatoshisAdjustment;
|
|
10063
|
+
if (ctx.xoutputs[adjustment.fixedOutputIndex].satoshis !== 0x775f05a073fff) throw new WERR_INTERNAL();
|
|
10064
|
+
ctx.xoutputs[adjustment.fixedOutputIndex].satoshis = adjustment.satoshis;
|
|
10065
|
+
logger?.log("adjusted change outputs to max possible");
|
|
10066
|
+
}
|
|
10067
|
+
const satoshis = funded.changeOutputs.reduce((sum, output) => sum + output.satoshis, 0) - funded.allocatedChange.reduce((sum, output) => sum + output.satoshis, 0);
|
|
10068
|
+
if (satoshis !== initialSatoshis) {
|
|
10069
|
+
await storage.updateTransaction(newTx.transactionId, { satoshis }, trx);
|
|
10070
|
+
newTx.satoshis = satoshis;
|
|
10071
|
+
}
|
|
10072
|
+
const storedOutputs = await traceStorageStep(storage, "wallet.storage.create_action.persist_outputs", parent, {
|
|
10073
|
+
"action.fixed_output_count": ctx.xoutputs.length,
|
|
10074
|
+
"action.change_output_count": funded.changeOutputs.length
|
|
10075
|
+
}, async (span) => {
|
|
10076
|
+
const result = await createNewOutputs(storage, userId, vargs, ctx, funded.changeOutputs, trx);
|
|
10077
|
+
span?.end({ attributes: { "action.persisted_output_count": result.outputs.length } });
|
|
10078
|
+
return result;
|
|
10079
|
+
});
|
|
10080
|
+
return {
|
|
10081
|
+
...funded,
|
|
10082
|
+
...storedOutputs,
|
|
10083
|
+
ctx
|
|
10084
|
+
};
|
|
9574
10085
|
});
|
|
10086
|
+
newTxCommitted = true;
|
|
10087
|
+
const committedTx = verifyTruthy(newTx);
|
|
10088
|
+
const { allocatedChange, derivationPrefix, outputs, changeVouts, ctx } = persisted;
|
|
9575
10089
|
logger?.log("created new output records");
|
|
9576
|
-
const inputBeef = await mergeAllocatedChangeBeefs(storage, vargs, allocatedChange, beef, parent);
|
|
10090
|
+
const inputBeef = await mergeAllocatedChangeBeefs(storage, vargs, allocatedChange, beef, allocatedBeefPrefetch, parent);
|
|
9577
10091
|
logger?.log("merged allocated change beefs");
|
|
9578
10092
|
const inputs = await traceStorageStep(storage, "wallet.storage.create_action.assemble_inputs", parent, {
|
|
9579
10093
|
"action.fixed_input_count": ctx.xinputs.length,
|
|
@@ -9586,9 +10100,9 @@ async function createActionCore(storage, auth, vargs, parent) {
|
|
|
9586
10100
|
});
|
|
9587
10101
|
logger?.log("created new inputs");
|
|
9588
10102
|
const r = {
|
|
9589
|
-
reference:
|
|
9590
|
-
version:
|
|
9591
|
-
lockTime:
|
|
10103
|
+
reference: committedTx.reference,
|
|
10104
|
+
version: committedTx.version,
|
|
10105
|
+
lockTime: committedTx.lockTime,
|
|
9592
10106
|
inputs,
|
|
9593
10107
|
outputs,
|
|
9594
10108
|
derivationPrefix,
|
|
@@ -9598,9 +10112,15 @@ async function createActionCore(storage, auth, vargs, parent) {
|
|
|
9598
10112
|
logger?.groupEnd();
|
|
9599
10113
|
return r;
|
|
9600
10114
|
} catch (error) {
|
|
10115
|
+
await allocatedBeefPrefetch;
|
|
9601
10116
|
if (newTx?.transactionId != null) try {
|
|
9602
|
-
|
|
9603
|
-
|
|
10117
|
+
if (newTxCommitted) {
|
|
10118
|
+
await storage.updateTransactionStatus("failed", newTx.transactionId);
|
|
10119
|
+
logger?.log(`marked failed createAction transaction ${newTx.transactionId} after construction error`);
|
|
10120
|
+
} else {
|
|
10121
|
+
const failed = await createNewTxRecord(storage, userId, vargs, storageBeefBytes, 0, void 0, "failed");
|
|
10122
|
+
logger?.log(`recorded failed createAction transaction ${failed.transactionId} after rollback`);
|
|
10123
|
+
}
|
|
9604
10124
|
} catch (cleanupError) {
|
|
9605
10125
|
logger?.log(`failed to clean up createAction transaction ${newTx.transactionId}: ${String(cleanupError)}`);
|
|
9606
10126
|
}
|
|
@@ -9750,23 +10270,10 @@ async function getCompetingBeefForReview(storage, txid) {
|
|
|
9750
10270
|
throw e;
|
|
9751
10271
|
}
|
|
9752
10272
|
}
|
|
9753
|
-
/**
|
|
9754
|
-
|
|
9755
|
-
async function persistNewOutput(storage, o, tags, txTags, txBaskets) {
|
|
9756
|
-
o.outputId = await storage.insertOutput(o);
|
|
9757
|
-
const changeVout = o.change && o.purpose === "change" && o.providedBy === "storage" ? o.vout : void 0;
|
|
9758
|
-
for (const tagName of new Set(tags)) {
|
|
9759
|
-
const tag = txTags[tagName];
|
|
9760
|
-
await storage.insertOutputTagMap({
|
|
9761
|
-
outputId: verifyId(o.outputId),
|
|
9762
|
-
outputTagId: verifyId(tag.outputTagId),
|
|
9763
|
-
created_at: /* @__PURE__ */ new Date(),
|
|
9764
|
-
updated_at: /* @__PURE__ */ new Date(),
|
|
9765
|
-
isDeleted: false
|
|
9766
|
-
});
|
|
9767
|
-
}
|
|
10273
|
+
/** Build the SDK descriptor for a persisted output. */
|
|
10274
|
+
function describeNewOutput(o, tags, txBaskets) {
|
|
9768
10275
|
return {
|
|
9769
|
-
changeVout,
|
|
10276
|
+
changeVout: o.change && o.purpose === "change" && o.providedBy === "storage" ? o.vout : void 0,
|
|
9770
10277
|
ro: {
|
|
9771
10278
|
vout: verifyInteger(o.vout),
|
|
9772
10279
|
satoshis: _bsv_sdk.Validation.validateSatoshis(o.satoshis, "o.satoshis"),
|
|
@@ -9781,13 +10288,28 @@ async function persistNewOutput(storage, o, tags, txTags, txBaskets) {
|
|
|
9781
10288
|
}
|
|
9782
10289
|
};
|
|
9783
10290
|
}
|
|
9784
|
-
|
|
10291
|
+
/** Insert the output and attach its tags; return the SDK output descriptor. */
|
|
10292
|
+
async function persistNewOutput(storage, o, tags, txTags, txBaskets, trx) {
|
|
10293
|
+
o.outputId = await storage.insertOutput(o, trx);
|
|
10294
|
+
for (const tagName of new Set(tags)) {
|
|
10295
|
+
const tag = txTags[tagName];
|
|
10296
|
+
await storage.insertOutputTagMap({
|
|
10297
|
+
outputId: verifyId(o.outputId),
|
|
10298
|
+
outputTagId: verifyId(tag.outputTagId),
|
|
10299
|
+
created_at: /* @__PURE__ */ new Date(),
|
|
10300
|
+
updated_at: /* @__PURE__ */ new Date(),
|
|
10301
|
+
isDeleted: false
|
|
10302
|
+
}, trx);
|
|
10303
|
+
}
|
|
10304
|
+
return describeNewOutput(o, tags, txBaskets);
|
|
10305
|
+
}
|
|
10306
|
+
async function createNewOutputs(storage, userId, vargs, ctx, changeOutputs, trx) {
|
|
9785
10307
|
const txBaskets = {};
|
|
9786
10308
|
const basketNames = [...new Set(ctx.xoutputs.map((x) => x.basket).filter((v) => !!v))];
|
|
9787
|
-
Object.assign(txBaskets, await storage.findOrInsertOutputBasketsBulk(userId, basketNames));
|
|
10309
|
+
Object.assign(txBaskets, await storage.findOrInsertOutputBasketsBulk(userId, basketNames, trx));
|
|
9788
10310
|
const txTags = {};
|
|
9789
10311
|
const tagNames = [...new Set(ctx.xoutputs.flatMap((x) => x.tags))];
|
|
9790
|
-
Object.assign(txTags, await storage.findOrInsertOutputTagsBulk(userId, tagNames));
|
|
10312
|
+
Object.assign(txTags, await storage.findOrInsertOutputTagsBulk(userId, tagNames, trx));
|
|
9791
10313
|
const newOutputs = [];
|
|
9792
10314
|
for (const xo of ctx.xoutputs) {
|
|
9793
10315
|
const lockingScript = asArray(xo.lockingScript);
|
|
@@ -9803,7 +10325,7 @@ async function createNewOutputs(storage, userId, vargs, ctx, changeOutputs) {
|
|
|
9803
10325
|
created_at: now,
|
|
9804
10326
|
updated_at: now,
|
|
9805
10327
|
commissionId: 0
|
|
9806
|
-
});
|
|
10328
|
+
}, trx);
|
|
9807
10329
|
const o = makeDefaultOutput(userId, ctx.transactionId, xo.satoshis, xo.vout);
|
|
9808
10330
|
o.lockingScript = lockingScript;
|
|
9809
10331
|
o.providedBy = "storage";
|
|
@@ -9837,10 +10359,12 @@ async function createNewOutputs(storage, userId, vargs, ctx, changeOutputs) {
|
|
|
9837
10359
|
});
|
|
9838
10360
|
}
|
|
9839
10361
|
if (vargs.options.randomizeOutputs) randomizeOutputVouts(newOutputs.map((output) => output.o), vargs.randomVals);
|
|
10362
|
+
const untagged = newOutputs.filter((output) => output.tags.length === 0);
|
|
10363
|
+
await storage.insertOutputs(untagged.map((output) => output.o), trx);
|
|
9840
10364
|
const outputs = [];
|
|
9841
10365
|
const changeVouts = [];
|
|
9842
10366
|
for (const { o, tags } of newOutputs) {
|
|
9843
|
-
const { changeVout, ro } = await persistNewOutput(storage, o, tags, txTags, txBaskets);
|
|
10367
|
+
const { changeVout, ro } = tags.length === 0 ? describeNewOutput(o, tags, txBaskets) : await persistNewOutput(storage, o, tags, txTags, txBaskets, trx);
|
|
9844
10368
|
if (changeVout !== void 0) changeVouts.push(changeVout);
|
|
9845
10369
|
outputs.push(ro);
|
|
9846
10370
|
}
|
|
@@ -9849,7 +10373,7 @@ async function createNewOutputs(storage, userId, vargs, ctx, changeOutputs) {
|
|
|
9849
10373
|
changeVouts
|
|
9850
10374
|
};
|
|
9851
10375
|
}
|
|
9852
|
-
async function createNewTxRecord(storage, userId, vargs, storageBeef) {
|
|
10376
|
+
async function createNewTxRecord(storage, userId, vargs, storageBeef, satoshis = 0, trx, status = "unsigned") {
|
|
9853
10377
|
const now = /* @__PURE__ */ new Date();
|
|
9854
10378
|
const newTx = {
|
|
9855
10379
|
created_at: now,
|
|
@@ -9857,9 +10381,9 @@ async function createNewTxRecord(storage, userId, vargs, storageBeef) {
|
|
|
9857
10381
|
transactionId: 0,
|
|
9858
10382
|
version: vargs.version,
|
|
9859
10383
|
lockTime: vargs.lockTime,
|
|
9860
|
-
status
|
|
10384
|
+
status,
|
|
9861
10385
|
reference: randomBytesBase64(12),
|
|
9862
|
-
satoshis
|
|
10386
|
+
satoshis,
|
|
9863
10387
|
userId,
|
|
9864
10388
|
isOutgoing: true,
|
|
9865
10389
|
inputBEEF: storageBeef,
|
|
@@ -9867,12 +10391,12 @@ async function createNewTxRecord(storage, userId, vargs, storageBeef) {
|
|
|
9867
10391
|
txid: void 0,
|
|
9868
10392
|
rawTx: void 0
|
|
9869
10393
|
};
|
|
9870
|
-
newTx.transactionId = await storage.insertTransaction(newTx);
|
|
10394
|
+
newTx.transactionId = await storage.insertTransaction(newTx, trx);
|
|
9871
10395
|
const labelNames = [...new Set(vargs.labels)];
|
|
9872
|
-
const labels = await storage.findOrInsertTxLabelsBulk(userId, labelNames);
|
|
10396
|
+
const labels = await storage.findOrInsertTxLabelsBulk(userId, labelNames, trx);
|
|
9873
10397
|
for (const label of labelNames) {
|
|
9874
10398
|
const txLabel = labels[label];
|
|
9875
|
-
await storage.findOrInsertTxLabelMap(verifyId(newTx.transactionId), verifyId(txLabel.txLabelId));
|
|
10399
|
+
await storage.findOrInsertTxLabelMap(verifyId(newTx.transactionId), verifyId(txLabel.txLabelId), trx);
|
|
9876
10400
|
}
|
|
9877
10401
|
return newTx;
|
|
9878
10402
|
}
|
|
@@ -10068,6 +10592,9 @@ async function validateNoSendChange(storage, userId, vargs, changeBasket) {
|
|
|
10068
10592
|
if ((await storage.findReservedActionBatchOutputIds(r.map((output) => output.outputId))).length > 0) throw new WERR_INVALID_PARAMETER("noSendChange", "outputs not reserved by an active action batch");
|
|
10069
10593
|
return r;
|
|
10070
10594
|
}
|
|
10595
|
+
function fundingPlanSatoshis(plan) {
|
|
10596
|
+
return plan.result.changeOutputs.reduce((sum, output) => sum + output.satoshis, 0) - plan.selected.reduce((sum, output) => sum + output.satoshis, 0);
|
|
10597
|
+
}
|
|
10071
10598
|
var FundingClaimConflict = class extends Error {
|
|
10072
10599
|
conflict;
|
|
10073
10600
|
constructor(conflict) {
|
|
@@ -10103,10 +10630,10 @@ function makeFundingParams(vargs, xinputs, xoutputs, changeBasket, feeModel, ava
|
|
|
10103
10630
|
};
|
|
10104
10631
|
}
|
|
10105
10632
|
async function prepareFundingPlan(storage, context) {
|
|
10106
|
-
const
|
|
10633
|
+
const [userId, vargs, xinputs, xoutputs, changeBasket, noSendChangeIn, feeModel, parent, trx] = context;
|
|
10107
10634
|
const excludeSending = !vargs.isDelayed;
|
|
10108
10635
|
const candidates = await traceStorageStep(storage, "wallet.storage.create_action.funding_candidates", parent, { "funding.exclude_sending": excludeSending }, async (span) => {
|
|
10109
|
-
const outputs = await storage.
|
|
10636
|
+
const outputs = await storage.findAvailableManagedChangeInputCandidates(userId, changeBasket.basketId, excludeSending, trx);
|
|
10110
10637
|
span?.end({ attributes: {
|
|
10111
10638
|
"funding.candidate_count": outputs.length,
|
|
10112
10639
|
"funding.candidate_satoshis": outputs.reduce((sum, output) => sum + output.satoshis, 0)
|
|
@@ -10121,10 +10648,12 @@ async function prepareFundingPlan(storage, context) {
|
|
|
10121
10648
|
"funding.no_send_change_count": noSendChangeIn.length
|
|
10122
10649
|
}, async (span) => {
|
|
10123
10650
|
const allocated = /* @__PURE__ */ new Map();
|
|
10651
|
+
const availableSelector = new CanonicalChangeSelector(available);
|
|
10124
10652
|
const noSend = [...noSendChangeIn];
|
|
10653
|
+
const noSendById = new Map(noSendChangeIn.map((output) => [output.outputId, output]));
|
|
10125
10654
|
const allocate = async (targetSatoshis, exactSatoshis) => {
|
|
10126
10655
|
let output = noSend.pop();
|
|
10127
|
-
output ??=
|
|
10656
|
+
output ??= availableSelector.take(targetSatoshis, exactSatoshis);
|
|
10128
10657
|
if (output == null) return void 0;
|
|
10129
10658
|
allocated.set(output.outputId, output);
|
|
10130
10659
|
return {
|
|
@@ -10133,10 +10662,11 @@ async function prepareFundingPlan(storage, context) {
|
|
|
10133
10662
|
};
|
|
10134
10663
|
};
|
|
10135
10664
|
const release = async (outputId) => {
|
|
10136
|
-
|
|
10137
|
-
if (output == null) return;
|
|
10665
|
+
if (allocated.get(outputId) == null) return;
|
|
10138
10666
|
allocated.delete(outputId);
|
|
10139
|
-
|
|
10667
|
+
availableSelector.release(outputId);
|
|
10668
|
+
const noSendOutput = noSendById.get(outputId);
|
|
10669
|
+
if (noSendOutput != null) noSend.push(noSendOutput);
|
|
10140
10670
|
};
|
|
10141
10671
|
const result = await generateChangeSdk(params, allocate, release, vargs.logger, storage.telemetry);
|
|
10142
10672
|
const selected = result.allocatedChangeInputs.map((input) => verifyTruthy(allocated.get(input.outputId)));
|
|
@@ -10154,7 +10684,8 @@ async function prepareFundingPlan(storage, context) {
|
|
|
10154
10684
|
};
|
|
10155
10685
|
});
|
|
10156
10686
|
}
|
|
10157
|
-
async function claimFundingPlan(storage,
|
|
10687
|
+
async function claimFundingPlan(storage, request) {
|
|
10688
|
+
const [userId, basketId, excludeSending, transactionId, noSendChangeIn, plan, trx] = request;
|
|
10158
10689
|
if (plan.selected.length === 0) return {
|
|
10159
10690
|
outputs: [],
|
|
10160
10691
|
sourceTransactionCount: 0,
|
|
@@ -10164,27 +10695,16 @@ async function claimFundingPlan(storage, userId, basketId, excludeSending, trans
|
|
|
10164
10695
|
const noSendIds = new Set(noSendChangeIn.map((output) => output.outputId));
|
|
10165
10696
|
const statuses = ["completed", "unproven"];
|
|
10166
10697
|
if (!excludeSending) statuses.push("sending");
|
|
10167
|
-
const claim = await storage.transaction(async (
|
|
10168
|
-
const
|
|
10169
|
-
|
|
10170
|
-
return {
|
|
10171
|
-
txid: output.txid,
|
|
10172
|
-
vout: output.vout
|
|
10173
|
-
};
|
|
10174
|
-
});
|
|
10175
|
-
const currentByOutpoint = await storage.findOutputsByOutpointsForUpdate(userId, outpoints, trx, true);
|
|
10176
|
-
const reserved = new Set(await storage.findReservedActionBatchOutputIds(plan.selected.map((output) => output.outputId), trx));
|
|
10177
|
-
const transactionIds = [...new Set(Object.values(currentByOutpoint).map((output) => output.transactionId))];
|
|
10178
|
-
const transactionStatuses = await storage.findTransactionStatusesByIds(userId, transactionIds, trx);
|
|
10698
|
+
const claim = await storage.transaction(async (claimTrx) => {
|
|
10699
|
+
const currentById = await storage.findFundingOutputsForUpdate(userId, plan.selected.map((output) => output.outputId), statuses, claimTrx);
|
|
10700
|
+
const transactionIds = [...new Set(Object.values(currentById).map((output) => output.transactionId))];
|
|
10179
10701
|
const claimed = [];
|
|
10180
10702
|
for (const planned of plan.selected) {
|
|
10181
|
-
const current =
|
|
10182
|
-
|
|
10183
|
-
const validTransaction = currentStatus != null && statuses.includes(currentStatus);
|
|
10184
|
-
if (current?.outputId !== planned.outputId || current?.satoshis !== planned.satoshis || current?.basketId !== basketId || !isAutoSpendableChangeOutput(current) || reserved.has(current?.outputId ?? -1) || validTransaction !== true) return { conflict: noSendIds.has(planned.outputId) ? "noSendChange" : "candidate" };
|
|
10703
|
+
const current = currentById[planned.outputId];
|
|
10704
|
+
if (current?.outputId !== planned.outputId || current?.satoshis !== planned.satoshis || current?.basketId !== basketId || !isAutoSpendableChangeOutput(current) || current?.txid !== planned.txid || current?.vout !== planned.vout) return { conflict: noSendIds.has(planned.outputId) ? "noSendChange" : "candidate" };
|
|
10185
10705
|
claimed.push(current);
|
|
10186
10706
|
}
|
|
10187
|
-
if (await storage.markChangeInputsSpent(claimed.map((output) => output.outputId), transactionId,
|
|
10707
|
+
if (await storage.markChangeInputsSpent(claimed.map((output) => output.outputId), transactionId, claimTrx) !== claimed.length) throw new FundingClaimConflict(claimed.some((output) => noSendIds.has(output.outputId)) ? "noSendChange" : "candidate");
|
|
10188
10708
|
for (const output of claimed) {
|
|
10189
10709
|
output.spendable = false;
|
|
10190
10710
|
output.spentBy = transactionId;
|
|
@@ -10193,19 +10713,19 @@ async function claimFundingPlan(storage, userId, basketId, excludeSending, trans
|
|
|
10193
10713
|
outputs: claimed,
|
|
10194
10714
|
sourceTransactionCount: transactionIds.length
|
|
10195
10715
|
};
|
|
10196
|
-
}).catch((error) => {
|
|
10716
|
+
}, trx).catch((error) => {
|
|
10197
10717
|
if (error instanceof FundingClaimConflict) return { conflict: error.conflict };
|
|
10198
10718
|
throw error;
|
|
10199
10719
|
});
|
|
10200
10720
|
if (claim.outputs == null) return claim;
|
|
10201
|
-
const hydration = await hydrateFundingInputScripts(storage, claim.outputs);
|
|
10721
|
+
const hydration = await hydrateFundingInputScripts(storage, claim.outputs, trx);
|
|
10202
10722
|
return {
|
|
10203
10723
|
outputs: claim.outputs,
|
|
10204
10724
|
sourceTransactionCount: claim.sourceTransactionCount,
|
|
10205
10725
|
...hydration
|
|
10206
10726
|
};
|
|
10207
10727
|
}
|
|
10208
|
-
async function hydrateFundingInputScripts(storage, outputs) {
|
|
10728
|
+
async function hydrateFundingInputScripts(storage, outputs, trx) {
|
|
10209
10729
|
const missing = outputs.filter((output) => output.lockingScript?.length !== output.scriptLength && output.scriptLength != null && output.scriptLength > 0 && output.scriptOffset != null && output.scriptOffset > 0 && output.txid != null && output.txid !== "");
|
|
10210
10730
|
if (missing.length === 0) return {
|
|
10211
10731
|
hydratedScriptCount: 0,
|
|
@@ -10224,12 +10744,12 @@ async function hydrateFundingInputScripts(storage, outputs) {
|
|
|
10224
10744
|
while (cursor < groups.length) {
|
|
10225
10745
|
const [txid, group] = groups[cursor++];
|
|
10226
10746
|
if (group.length === 1) {
|
|
10227
|
-
await storage.validateOutputScript(group[0]);
|
|
10747
|
+
await storage.validateOutputScript(group[0], trx);
|
|
10228
10748
|
continue;
|
|
10229
10749
|
}
|
|
10230
|
-
const rawTx = await storage.getRawTxOfKnownValidTransaction(txid);
|
|
10750
|
+
const rawTx = await storage.getRawTxOfKnownValidTransaction(txid, void 0, void 0, trx);
|
|
10231
10751
|
if (rawTx != null) for (const output of group) output.lockingScript = rawTx.slice(output.scriptOffset, output.scriptOffset + output.scriptLength);
|
|
10232
|
-
else for (const output of group) await storage.validateOutputScript(output);
|
|
10752
|
+
else for (const output of group) await storage.validateOutputScript(output, trx);
|
|
10233
10753
|
}
|
|
10234
10754
|
}));
|
|
10235
10755
|
return {
|
|
@@ -10237,13 +10757,21 @@ async function hydrateFundingInputScripts(storage, outputs) {
|
|
|
10237
10757
|
scriptSourceTransactionCount: groups.length
|
|
10238
10758
|
};
|
|
10239
10759
|
}
|
|
10240
|
-
async function fundNewTransactionSdk(storage, userId, vargs, ctx, initialPlan, parent) {
|
|
10760
|
+
async function fundNewTransactionSdk(storage, userId, vargs, ctx, initialPlan, parent, trx) {
|
|
10241
10761
|
let plan = initialPlan;
|
|
10242
10762
|
let allocatedChange;
|
|
10243
10763
|
let retryCount = 0;
|
|
10244
10764
|
await traceStorageStep(storage, "wallet.storage.create_action.funding_claim", parent, { "funding.planned_input_count": initialPlan.selected.length }, async (span) => {
|
|
10245
10765
|
for (let attempt = 0; attempt < 3; attempt++) {
|
|
10246
|
-
const claim = await claimFundingPlan(storage,
|
|
10766
|
+
const claim = await claimFundingPlan(storage, [
|
|
10767
|
+
userId,
|
|
10768
|
+
ctx.changeBasket.basketId,
|
|
10769
|
+
!vargs.isDelayed,
|
|
10770
|
+
ctx.transactionId,
|
|
10771
|
+
ctx.noSendChangeIn,
|
|
10772
|
+
plan,
|
|
10773
|
+
trx
|
|
10774
|
+
]);
|
|
10247
10775
|
if (claim.outputs != null) {
|
|
10248
10776
|
allocatedChange = claim.outputs;
|
|
10249
10777
|
span?.end({ attributes: {
|
|
@@ -10256,16 +10784,17 @@ async function fundNewTransactionSdk(storage, userId, vargs, ctx, initialPlan, p
|
|
|
10256
10784
|
}
|
|
10257
10785
|
if (claim.conflict === "noSendChange") throw new WERR_INVALID_PARAMETER("noSendChange", "outputs that remain spendable during action planning");
|
|
10258
10786
|
retryCount++;
|
|
10259
|
-
plan = await prepareFundingPlan(storage,
|
|
10787
|
+
plan = await prepareFundingPlan(storage, [
|
|
10260
10788
|
userId,
|
|
10261
10789
|
vargs,
|
|
10262
|
-
|
|
10263
|
-
|
|
10264
|
-
|
|
10265
|
-
|
|
10266
|
-
|
|
10267
|
-
parent
|
|
10268
|
-
|
|
10790
|
+
ctx.xinputs,
|
|
10791
|
+
ctx.xoutputs,
|
|
10792
|
+
ctx.changeBasket,
|
|
10793
|
+
ctx.noSendChangeIn,
|
|
10794
|
+
ctx.feeModel,
|
|
10795
|
+
parent,
|
|
10796
|
+
trx
|
|
10797
|
+
]);
|
|
10269
10798
|
}
|
|
10270
10799
|
throw new WERR_INVALID_OPERATION("wallet funding changed repeatedly during action planning; retry createAction");
|
|
10271
10800
|
});
|
|
@@ -10351,7 +10880,56 @@ function makeKnownTxidLookup(knownTxids) {
|
|
|
10351
10880
|
return knownTxids.includes(txid);
|
|
10352
10881
|
};
|
|
10353
10882
|
}
|
|
10354
|
-
|
|
10883
|
+
function missingAllocatedChangeTxids(allocatedChange, beef, knownTxids) {
|
|
10884
|
+
const hasKnownTxid = makeKnownTxidLookup(knownTxids);
|
|
10885
|
+
return Array.from(new Set(allocatedChange.map((output) => verifyTruthy(output.txid)).filter((txid) => beef.findTxid(txid) == null && !hasKnownTxid(txid))));
|
|
10886
|
+
}
|
|
10887
|
+
function startAllocatedChangeBeefPrefetch(storage, vargs, allocatedChange, beef, parent) {
|
|
10888
|
+
if (vargs.options.returnTXIDOnly) return Promise.resolve({
|
|
10889
|
+
sourceCount: 0,
|
|
10890
|
+
txids: []
|
|
10891
|
+
});
|
|
10892
|
+
const knownTxids = vargs.options.knownTxids ?? [];
|
|
10893
|
+
const missing = missingAllocatedChangeTxids(allocatedChange, beef, knownTxids);
|
|
10894
|
+
if (missing.length === 0) return Promise.resolve({
|
|
10895
|
+
sourceCount: 0,
|
|
10896
|
+
txids: []
|
|
10897
|
+
});
|
|
10898
|
+
const options = {
|
|
10899
|
+
trustSelf: void 0,
|
|
10900
|
+
knownTxids,
|
|
10901
|
+
ignoreStorage: false,
|
|
10902
|
+
ignoreServices: true,
|
|
10903
|
+
ignoreNewProven: false,
|
|
10904
|
+
minProofLevel: void 0
|
|
10905
|
+
};
|
|
10906
|
+
return traceStorageStep(storage, "wallet.storage.create_action.beef_prefetch", parent, {
|
|
10907
|
+
"beef.planned_source_count": allocatedChange.length,
|
|
10908
|
+
"beef.missing_source_count": missing.length,
|
|
10909
|
+
"beef.storage_batch_count": missing.length === 0 ? 0 : 1
|
|
10910
|
+
}, async (span) => {
|
|
10911
|
+
const fetched = await storage.getBeefForTransactions(missing, options);
|
|
10912
|
+
span?.end({ attributes: {
|
|
10913
|
+
"beef.fetched_tx_count": fetched.txs.length,
|
|
10914
|
+
"beef.fetched_bump_count": fetched.bumps.length
|
|
10915
|
+
} });
|
|
10916
|
+
return fetched;
|
|
10917
|
+
}).then((prefetched) => ({
|
|
10918
|
+
beef: prefetched,
|
|
10919
|
+
sourceCount: missing.length,
|
|
10920
|
+
txids: missing
|
|
10921
|
+
}), (error) => ({
|
|
10922
|
+
error,
|
|
10923
|
+
sourceCount: missing.length,
|
|
10924
|
+
txids: missing
|
|
10925
|
+
}));
|
|
10926
|
+
}
|
|
10927
|
+
function sameTxids(left, right) {
|
|
10928
|
+
if (left.length !== right.length) return false;
|
|
10929
|
+
const expected = new Set(left);
|
|
10930
|
+
return right.every((txid) => expected.has(txid));
|
|
10931
|
+
}
|
|
10932
|
+
async function mergeAllocatedChangeBeefs(storage, vargs, allocatedChange, beef, prefetch, parent) {
|
|
10355
10933
|
const options = {
|
|
10356
10934
|
trustSelf: void 0,
|
|
10357
10935
|
knownTxids: vargs.options.knownTxids,
|
|
@@ -10363,37 +10941,37 @@ async function mergeAllocatedChangeBeefs(storage, vargs, allocatedChange, beef,
|
|
|
10363
10941
|
};
|
|
10364
10942
|
if (vargs.options.returnTXIDOnly) return void 0;
|
|
10365
10943
|
const knownTxids = vargs.options.knownTxids ?? [];
|
|
10366
|
-
const
|
|
10367
|
-
const
|
|
10368
|
-
|
|
10369
|
-
|
|
10370
|
-
|
|
10944
|
+
const requiredBeforePrefetch = missingAllocatedChangeTxids(allocatedChange, beef, knownTxids);
|
|
10945
|
+
const prefetched = await traceStorageStep(storage, "wallet.storage.create_action.beef_prefetch_join", parent, { "beef.prefetch_source_count": 0 }, async (span) => {
|
|
10946
|
+
const result = await prefetch;
|
|
10947
|
+
span?.end({ attributes: { "beef.prefetch_source_count": result.sourceCount } });
|
|
10948
|
+
return result;
|
|
10949
|
+
});
|
|
10950
|
+
const usePrefetch = sameTxids(prefetched.txids, requiredBeforePrefetch);
|
|
10951
|
+
if (usePrefetch && prefetched.error != null) throw prefetched.error;
|
|
10952
|
+
if (usePrefetch && prefetched.beef != null) beef.mergeBeef(prefetched.beef);
|
|
10953
|
+
const missing = missingAllocatedChangeTxids(allocatedChange, beef, knownTxids);
|
|
10954
|
+
let fetched;
|
|
10371
10955
|
await traceStorageStep(storage, "wallet.storage.create_action.beef_fetch", parent, {
|
|
10372
10956
|
"beef.allocated_change_count": allocatedChange.length,
|
|
10373
10957
|
"beef.distinct_source_count": new Set(allocatedChange.map((output) => output.txid)).size,
|
|
10374
10958
|
"beef.known_txid_count": knownTxids.length,
|
|
10375
10959
|
"beef.missing_source_count": missing.length,
|
|
10376
|
-
"beef.fetch_concurrency":
|
|
10960
|
+
"beef.fetch_concurrency": 1,
|
|
10961
|
+
"beef.prefetch_reused": usePrefetch
|
|
10377
10962
|
}, async (span) => {
|
|
10378
|
-
|
|
10379
|
-
|
|
10380
|
-
|
|
10381
|
-
|
|
10382
|
-
...options,
|
|
10383
|
-
mergeToBeef: void 0
|
|
10384
|
-
});
|
|
10385
|
-
}
|
|
10386
|
-
}));
|
|
10963
|
+
if (missing.length > 0) fetched = await storage.getBeefForTransactions(missing, {
|
|
10964
|
+
...options,
|
|
10965
|
+
mergeToBeef: void 0
|
|
10966
|
+
});
|
|
10387
10967
|
span?.end({ attributes: {
|
|
10388
|
-
"beef.fetched_tx_count": fetched
|
|
10389
|
-
"beef.fetched_bump_count": fetched
|
|
10968
|
+
"beef.fetched_tx_count": fetched?.txs.length ?? 0,
|
|
10969
|
+
"beef.fetched_bump_count": fetched?.bumps.length ?? 0,
|
|
10970
|
+
"beef.storage_batch_count": missing.length === 0 ? 0 : 1
|
|
10390
10971
|
} });
|
|
10391
10972
|
});
|
|
10392
|
-
await traceStorageStep(storage, "wallet.storage.create_action.beef_merge", parent, { "beef.fragment_count":
|
|
10393
|
-
|
|
10394
|
-
if (fetchedBeef == null) continue;
|
|
10395
|
-
beef.mergeBeef(fetchedBeef);
|
|
10396
|
-
}
|
|
10973
|
+
await traceStorageStep(storage, "wallet.storage.create_action.beef_merge", parent, { "beef.fragment_count": (usePrefetch && prefetched.beef != null ? 1 : 0) + (fetched == null ? 0 : 1) }, async (span) => {
|
|
10974
|
+
if (fetched != null) beef.mergeBeef(fetched);
|
|
10397
10975
|
span?.end({ attributes: {
|
|
10398
10976
|
"beef.merged_tx_count": beef.txs.length,
|
|
10399
10977
|
"beef.merged_bump_count": beef.bumps.length
|
|
@@ -11191,9 +11769,7 @@ function genesisHeader(chain) {
|
|
|
11191
11769
|
height: 0,
|
|
11192
11770
|
hash: "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f"
|
|
11193
11771
|
};
|
|
11194
|
-
case "test":
|
|
11195
|
-
case "ttn":
|
|
11196
|
-
case "tstn": return {
|
|
11772
|
+
case "test": return {
|
|
11197
11773
|
version: 1,
|
|
11198
11774
|
previousHash: "0000000000000000000000000000000000000000000000000000000000000000",
|
|
11199
11775
|
merkleRoot: "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b",
|
|
@@ -11203,6 +11779,36 @@ function genesisHeader(chain) {
|
|
|
11203
11779
|
height: 0,
|
|
11204
11780
|
hash: "000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943"
|
|
11205
11781
|
};
|
|
11782
|
+
case "stn": return {
|
|
11783
|
+
version: 1,
|
|
11784
|
+
previousHash: "0000000000000000000000000000000000000000000000000000000000000000",
|
|
11785
|
+
merkleRoot: "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b",
|
|
11786
|
+
time: 1296688602,
|
|
11787
|
+
bits: 486604799,
|
|
11788
|
+
nonce: 173779992,
|
|
11789
|
+
height: 0,
|
|
11790
|
+
hash: "6b38bdbcd73a19f7889d23e1fa6166a9de71affceca60ca3bb1b28af8135c594"
|
|
11791
|
+
};
|
|
11792
|
+
case "ttn": return {
|
|
11793
|
+
version: 1,
|
|
11794
|
+
previousHash: "0000000000000000000000000000000000000000000000000000000000000000",
|
|
11795
|
+
merkleRoot: "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b",
|
|
11796
|
+
time: 1755606836,
|
|
11797
|
+
bits: 486604799,
|
|
11798
|
+
nonce: 1092578460,
|
|
11799
|
+
height: 0,
|
|
11800
|
+
hash: "000000000499eabba0a88f5b3747231c74b9191c1a4a04b2c2ea817976b7776d"
|
|
11801
|
+
};
|
|
11802
|
+
case "tstn": return {
|
|
11803
|
+
version: 1,
|
|
11804
|
+
previousHash: "0000000000000000000000000000000000000000000000000000000000000000",
|
|
11805
|
+
merkleRoot: "64452e5b25c65e492ad6a4f5ce9f427ca986626c28315d88de920d66e28cc98f",
|
|
11806
|
+
time: 1782864e3,
|
|
11807
|
+
bits: 486604799,
|
|
11808
|
+
nonce: 1780488216,
|
|
11809
|
+
height: 0,
|
|
11810
|
+
hash: "000000005d221c0e023cb56b5682cf094f32cd959958b40bc931e5797cae706c"
|
|
11811
|
+
};
|
|
11206
11812
|
case "mock": throw new Error("genesisHeader does not support 'mock' chain. Mock chain generates its own genesis block.");
|
|
11207
11813
|
}
|
|
11208
11814
|
}
|
|
@@ -13675,10 +14281,28 @@ var StorageProvider = class StorageProvider extends StorageReaderWriter {
|
|
|
13675
14281
|
}
|
|
13676
14282
|
return updated;
|
|
13677
14283
|
}
|
|
14284
|
+
/**
|
|
14285
|
+
* Insert outputs that do not need their generated ids returned to the
|
|
14286
|
+
* caller. Engines with a multi-row insert override this common-path helper;
|
|
14287
|
+
* the fallback preserves existing storage implementations unchanged.
|
|
14288
|
+
*/
|
|
14289
|
+
async insertOutputs(outputs, trx) {
|
|
14290
|
+
for (const output of outputs) await this.insertOutput(output, trx);
|
|
14291
|
+
}
|
|
13678
14292
|
/** Return unreserved wallet-managed outputs eligible for automatic funding. */
|
|
13679
14293
|
async findAvailableManagedChangeInputs(userId, basketId, excludeSending, trx) {
|
|
13680
14294
|
return await availableManagedChange(this, userId, basketId, excludeSending, trx);
|
|
13681
14295
|
}
|
|
14296
|
+
/** Read only the fields needed by the in-memory funding planner. */
|
|
14297
|
+
async findAvailableManagedChangeInputCandidates(userId, basketId, excludeSending, trx) {
|
|
14298
|
+
return (await this.findAvailableManagedChangeInputs(userId, basketId, excludeSending, trx)).map(({ outputId, transactionId, satoshis, txid, vout }) => ({
|
|
14299
|
+
outputId,
|
|
14300
|
+
transactionId,
|
|
14301
|
+
satoshis,
|
|
14302
|
+
txid,
|
|
14303
|
+
vout
|
|
14304
|
+
}));
|
|
14305
|
+
}
|
|
13682
14306
|
/** Read the current status of a set of source transactions without loading raw transaction bytes. */
|
|
13683
14307
|
async findTransactionStatusesByIds(userId, transactionIds, trx) {
|
|
13684
14308
|
const statuses = /* @__PURE__ */ new Map();
|
|
@@ -13688,6 +14312,36 @@ var StorageProvider = class StorageProvider extends StorageReaderWriter {
|
|
|
13688
14312
|
}
|
|
13689
14313
|
return statuses;
|
|
13690
14314
|
}
|
|
14315
|
+
/**
|
|
14316
|
+
* Lock and return the selected funding rows whose source transaction and
|
|
14317
|
+
* action-batch reservation state still permit allocation.
|
|
14318
|
+
*/
|
|
14319
|
+
async findFundingOutputsForUpdate(userId, outputIds, statuses, trx) {
|
|
14320
|
+
const rows = await this.findOutputsByIds(outputIds, trx);
|
|
14321
|
+
const reserved = new Set(await this.findReservedActionBatchOutputIds(outputIds, trx));
|
|
14322
|
+
const transactionIds = [...new Set(Object.values(rows).map((output) => output.transactionId))];
|
|
14323
|
+
const transactionStatuses = await this.findTransactionStatusesByIds(userId, transactionIds, trx);
|
|
14324
|
+
const eligible = {};
|
|
14325
|
+
for (const output of Object.values(rows)) if (output.userId === userId && !reserved.has(output.outputId) && statuses.includes(transactionStatuses.get(output.transactionId))) eligible[output.outputId] = output;
|
|
14326
|
+
return eligible;
|
|
14327
|
+
}
|
|
14328
|
+
/**
|
|
14329
|
+
* Resolve several transaction proofs in one storage operation when the
|
|
14330
|
+
* backend supports it. The default preserves compatibility for custom
|
|
14331
|
+
* providers; SQL and IndexedDB providers override this hot path.
|
|
14332
|
+
*/
|
|
14333
|
+
async getProvenOrRawTxs(txids, trx) {
|
|
14334
|
+
const results = /* @__PURE__ */ new Map();
|
|
14335
|
+
const unique = [...new Set(txids)];
|
|
14336
|
+
let cursor = 0;
|
|
14337
|
+
await Promise.all(Array.from({ length: Math.min(8, unique.length) }, async () => {
|
|
14338
|
+
while (cursor < unique.length) {
|
|
14339
|
+
const txid = unique[cursor++];
|
|
14340
|
+
results.set(txid, await this.getProvenOrRawTx(txid, trx));
|
|
14341
|
+
}
|
|
14342
|
+
}));
|
|
14343
|
+
return results;
|
|
14344
|
+
}
|
|
13691
14345
|
async insertActionBatch(_batch, _trx) {
|
|
13692
14346
|
throw new WERR_NOT_IMPLEMENTED();
|
|
13693
14347
|
}
|
|
@@ -13739,6 +14393,10 @@ var StorageProvider = class StorageProvider extends StorageReaderWriter {
|
|
|
13739
14393
|
supportsActionBatchPersistence() {
|
|
13740
14394
|
return false;
|
|
13741
14395
|
}
|
|
14396
|
+
/** Custom providers may require physical expiry cleanup before reservations are queried. */
|
|
14397
|
+
requiresActionBatchCleanupBeforeCreateAction() {
|
|
14398
|
+
return true;
|
|
14399
|
+
}
|
|
13742
14400
|
async beginActionBatch(auth, args) {
|
|
13743
14401
|
if (!this.supportsActionBatchPersistence()) throw new WERR_NOT_IMPLEMENTED("actionBatch capability is not available");
|
|
13744
14402
|
return await beginActionBatch(this, auth, args);
|
|
@@ -14127,7 +14785,7 @@ var StorageProvider = class StorageProvider extends StorageReaderWriter {
|
|
|
14127
14785
|
}
|
|
14128
14786
|
async createAction(auth, args) {
|
|
14129
14787
|
if (auth.userId == null) throw new WERR_UNAUTHORIZED();
|
|
14130
|
-
if (this.supportsActionBatchPersistence()) await cleanupExpiredActionBatches(this);
|
|
14788
|
+
if (this.supportsActionBatchPersistence() && this.requiresActionBatchCleanupBeforeCreateAction()) await cleanupExpiredActionBatches(this);
|
|
14131
14789
|
return await createAction(this, auth, args);
|
|
14132
14790
|
}
|
|
14133
14791
|
async processAction(auth, args) {
|
|
@@ -14217,6 +14875,9 @@ var StorageProvider = class StorageProvider extends StorageReaderWriter {
|
|
|
14217
14875
|
async getBeefForTransaction(txid, options) {
|
|
14218
14876
|
return await getBeefForTransaction(this, txid, options);
|
|
14219
14877
|
}
|
|
14878
|
+
async getBeefForTransactions(txids, options) {
|
|
14879
|
+
return await getBeefForTransactions(this, txids, options);
|
|
14880
|
+
}
|
|
14220
14881
|
async findMonitorEventById(id, trx) {
|
|
14221
14882
|
return verifyOneOrNone(await this.findMonitorEvents({
|
|
14222
14883
|
partial: { id },
|
|
@@ -15175,6 +15836,9 @@ var StorageIdb = class extends StorageProvider {
|
|
|
15175
15836
|
supportsActionBatchPersistence() {
|
|
15176
15837
|
return true;
|
|
15177
15838
|
}
|
|
15839
|
+
requiresActionBatchCleanupBeforeCreateAction() {
|
|
15840
|
+
return false;
|
|
15841
|
+
}
|
|
15178
15842
|
/**
|
|
15179
15843
|
* This method must be called at least once before any other method accesses the database,
|
|
15180
15844
|
* and each time the schema may have updated.
|
|
@@ -15354,6 +16018,50 @@ var StorageIdb = class extends StorageProvider {
|
|
|
15354
16018
|
}
|
|
15355
16019
|
return r;
|
|
15356
16020
|
}
|
|
16021
|
+
async getProvenOrRawTxs(txids, trx) {
|
|
16022
|
+
const results = /* @__PURE__ */ new Map();
|
|
16023
|
+
const unique = [...new Set(txids)];
|
|
16024
|
+
if (unique.length === 0) return results;
|
|
16025
|
+
const dbTrx = this.toDbTrx(["proven_txs", "proven_tx_reqs"], "readonly", trx);
|
|
16026
|
+
const provenIndex = dbTrx.objectStore("proven_txs").index("txid");
|
|
16027
|
+
const requestIndex = dbTrx.objectStore("proven_tx_reqs").index("txid");
|
|
16028
|
+
const usableStatuses = /* @__PURE__ */ new Set([
|
|
16029
|
+
"unsent",
|
|
16030
|
+
"unmined",
|
|
16031
|
+
"unconfirmed",
|
|
16032
|
+
"sending",
|
|
16033
|
+
"nosend",
|
|
16034
|
+
"completed"
|
|
16035
|
+
]);
|
|
16036
|
+
await Promise.all(unique.map(async (txid) => {
|
|
16037
|
+
const proven = await provenIndex.get(txid);
|
|
16038
|
+
if (proven != null) {
|
|
16039
|
+
results.set(txid, {
|
|
16040
|
+
proven: this.validateEntity(proven),
|
|
16041
|
+
rawTx: void 0,
|
|
16042
|
+
inputBEEF: void 0
|
|
16043
|
+
});
|
|
16044
|
+
return;
|
|
16045
|
+
}
|
|
16046
|
+
const request = await requestIndex.get(txid);
|
|
16047
|
+
if (request != null && usableStatuses.has(request.status)) {
|
|
16048
|
+
const validated = this.validateEntity(request);
|
|
16049
|
+
results.set(txid, {
|
|
16050
|
+
proven: void 0,
|
|
16051
|
+
rawTx: Array.from(validated.rawTx),
|
|
16052
|
+
inputBEEF: validated.inputBEEF == null ? void 0 : Array.from(validated.inputBEEF)
|
|
16053
|
+
});
|
|
16054
|
+
return;
|
|
16055
|
+
}
|
|
16056
|
+
results.set(txid, {
|
|
16057
|
+
proven: void 0,
|
|
16058
|
+
rawTx: void 0,
|
|
16059
|
+
inputBEEF: void 0
|
|
16060
|
+
});
|
|
16061
|
+
}));
|
|
16062
|
+
if (trx == null) await dbTrx.done;
|
|
16063
|
+
return results;
|
|
16064
|
+
}
|
|
15357
16065
|
async getRawTxOfKnownValidTransaction(txid, offset, length, trx) {
|
|
15358
16066
|
if (txid == null || txid === "") return void 0;
|
|
15359
16067
|
if (!this.isAvailable()) await this.makeAvailable();
|
|
@@ -15615,6 +16323,7 @@ var StorageIdb = class extends StorageProvider {
|
|
|
15615
16323
|
else cursor = await store.openCursor(null, direction);
|
|
15616
16324
|
await scanCursor(cursor, args.since, args.paged?.offset ?? 0, args.paged?.limit, async (r) => {
|
|
15617
16325
|
if (!matchesProvenTxPartial(r, args.partial)) return false;
|
|
16326
|
+
if (args.txids != null && args.txids.length > 0 && !args.txids.includes(r.txid)) return false;
|
|
15618
16327
|
if (userId !== void 0) {
|
|
15619
16328
|
if (await this.countTransactions({
|
|
15620
16329
|
partial: {
|
|
@@ -16059,11 +16768,18 @@ var StorageIdb = class extends StorageProvider {
|
|
|
16059
16768
|
return rows.map((r) => r.outputId);
|
|
16060
16769
|
}
|
|
16061
16770
|
async findReservedActionBatchOutputIds(outputIds, trx) {
|
|
16062
|
-
const tx = this.toDbTrx(["action_batch_outputs"], "readonly", trx);
|
|
16771
|
+
const tx = this.toDbTrx(["action_batch_outputs", "action_batches"], "readonly", trx);
|
|
16063
16772
|
const store = tx.objectStore("action_batch_outputs");
|
|
16773
|
+
const batchStore = tx.objectStore("action_batches");
|
|
16064
16774
|
if (store.get == null) throw new WERR_INTERNAL("IndexedDB action_batch_outputs store does not support get");
|
|
16065
16775
|
const reserved = [];
|
|
16066
|
-
|
|
16776
|
+
const now = Date.now();
|
|
16777
|
+
for (const outputId of outputIds) {
|
|
16778
|
+
const reservation = await store.get(outputId);
|
|
16779
|
+
if (reservation == null) continue;
|
|
16780
|
+
const batch = await batchStore.get(reservation.actionBatchId);
|
|
16781
|
+
if (batch != null && (batch.status === "active" || batch.status === "prepared") && batch.expiresAt.getTime() > now && batch.hardExpiresAt.getTime() > now) reserved.push(outputId);
|
|
16782
|
+
}
|
|
16067
16783
|
if (trx == null) await tx.done;
|
|
16068
16784
|
return reserved;
|
|
16069
16785
|
}
|
|
@@ -18620,10 +19336,11 @@ var Chaintracks = class {
|
|
|
18620
19336
|
startupError = null;
|
|
18621
19337
|
subscriberCallbacksEnabled = false;
|
|
18622
19338
|
stopMainThread = true;
|
|
18623
|
-
lastPresentHeight =
|
|
19339
|
+
lastPresentHeight = -1;
|
|
18624
19340
|
lastPresentHeightMsecs = 0;
|
|
18625
19341
|
lastPresentHeightMaxAge = 60 * 1e3;
|
|
18626
19342
|
lock = new SingleWriterMultiReaderLock();
|
|
19343
|
+
sourceStatus = /* @__PURE__ */ new Map();
|
|
18627
19344
|
constructor(options) {
|
|
18628
19345
|
this.options = options;
|
|
18629
19346
|
if (options.storage == null) throw new Error("storage is required.");
|
|
@@ -18634,6 +19351,22 @@ var Chaintracks = class {
|
|
|
18634
19351
|
this.storage = options.storage;
|
|
18635
19352
|
this.bulkIngestors = options.bulkIngestors;
|
|
18636
19353
|
this.liveIngestors = options.liveIngestors;
|
|
19354
|
+
for (const [index, source] of this.bulkIngestors.entries()) {
|
|
19355
|
+
const name = this.sourceName("bulk", index, source);
|
|
19356
|
+
this.sourceStatus.set(name, {
|
|
19357
|
+
name,
|
|
19358
|
+
role: "bulk",
|
|
19359
|
+
state: "unknown"
|
|
19360
|
+
});
|
|
19361
|
+
}
|
|
19362
|
+
for (const [index, source] of this.liveIngestors.entries()) {
|
|
19363
|
+
const name = this.sourceName("live", index, source);
|
|
19364
|
+
this.sourceStatus.set(name, {
|
|
19365
|
+
name,
|
|
19366
|
+
role: "live",
|
|
19367
|
+
state: "unknown"
|
|
19368
|
+
});
|
|
19369
|
+
}
|
|
18637
19370
|
this.addLiveRecursionLimit = options.addLiveRecursionLimit;
|
|
18638
19371
|
if (options.logging != null) this.log = options.logging;
|
|
18639
19372
|
this.storage.log = this.log;
|
|
@@ -18648,19 +19381,36 @@ var Chaintracks = class {
|
|
|
18648
19381
|
*/
|
|
18649
19382
|
async getPresentHeight() {
|
|
18650
19383
|
const now = Date.now();
|
|
18651
|
-
if (this.lastPresentHeight && now - this.lastPresentHeightMsecs < this.lastPresentHeightMaxAge) return this.lastPresentHeight;
|
|
18652
|
-
const
|
|
18653
|
-
|
|
18654
|
-
|
|
18655
|
-
|
|
18656
|
-
|
|
18657
|
-
|
|
18658
|
-
|
|
18659
|
-
|
|
18660
|
-
|
|
18661
|
-
|
|
18662
|
-
|
|
18663
|
-
|
|
19384
|
+
if (this.lastPresentHeight >= 0 && now - this.lastPresentHeightMsecs < this.lastPresentHeightMaxAge) return this.lastPresentHeight;
|
|
19385
|
+
for (const [index, bulk] of this.bulkIngestors.entries()) {
|
|
19386
|
+
const source = this.sourceName("bulk", index, bulk);
|
|
19387
|
+
try {
|
|
19388
|
+
const presentHeight = await bulk.getPresentHeight();
|
|
19389
|
+
if (presentHeight != null && Number.isInteger(presentHeight) && presentHeight >= 0) {
|
|
19390
|
+
this.markSourceSuccess(source, "bulk");
|
|
19391
|
+
this.lastPresentHeight = presentHeight;
|
|
19392
|
+
this.lastPresentHeightMsecs = now;
|
|
19393
|
+
return presentHeight;
|
|
19394
|
+
}
|
|
19395
|
+
} catch (uerr) {
|
|
19396
|
+
const error = WalletError.fromUnknown(uerr);
|
|
19397
|
+
this.markSourceFailure(source, "bulk", error);
|
|
19398
|
+
this.log(`Present-height source ${source} failed: ${error.message}`);
|
|
19399
|
+
}
|
|
19400
|
+
}
|
|
19401
|
+
if (this.lastPresentHeight >= 0) return this.lastPresentHeight;
|
|
19402
|
+
try {
|
|
19403
|
+
const ranges = await this.storage.getAvailableHeightRanges();
|
|
19404
|
+
const localHeight = Math.max(ranges.bulk.maxHeight, ranges.live.maxHeight);
|
|
19405
|
+
if (localHeight >= 0) {
|
|
19406
|
+
this.lastPresentHeight = localHeight;
|
|
19407
|
+
this.lastPresentHeightMsecs = now;
|
|
19408
|
+
return localHeight;
|
|
19409
|
+
}
|
|
19410
|
+
} catch (error) {
|
|
19411
|
+
this.log(`Unable to read the locally validated ChainTracks height: ${WalletError.fromUnknown(error).message}`);
|
|
19412
|
+
}
|
|
19413
|
+
throw new Error("No present-height source or locally validated headers are available.");
|
|
18664
19414
|
}
|
|
18665
19415
|
async currentHeight() {
|
|
18666
19416
|
return await this.getPresentHeight();
|
|
@@ -18711,7 +19461,7 @@ var Chaintracks = class {
|
|
|
18711
19461
|
for (const bulkIn of this.bulkIngestors) await bulkIn.setStorage(this.storage, this.log);
|
|
18712
19462
|
for (const liveIn of this.liveIngestors) await liveIn.setStorage(this.storage, this.log);
|
|
18713
19463
|
this.stopMainThread = false;
|
|
18714
|
-
for (const liveIngestor of this.liveIngestors) this.promises.push(this.runLiveIngestor(liveIngestor));
|
|
19464
|
+
for (const [index, liveIngestor] of this.liveIngestors.entries()) this.promises.push(this.runLiveIngestor(liveIngestor, index));
|
|
18715
19465
|
this.promises.push(this.mainThreadShiftLiveHeaders());
|
|
18716
19466
|
while (!this.available && this.startupError == null) await wait(100);
|
|
18717
19467
|
if (this.startupError != null) throw this.startupError;
|
|
@@ -18738,10 +19488,12 @@ var Chaintracks = class {
|
|
|
18738
19488
|
async listening() {
|
|
18739
19489
|
return await this.makeAvailable();
|
|
18740
19490
|
}
|
|
18741
|
-
async runLiveIngestor(liveIngestor) {
|
|
19491
|
+
async runLiveIngestor(liveIngestor, index) {
|
|
18742
19492
|
let restartCount = 0;
|
|
18743
19493
|
const name = liveIngestor.constructor.name;
|
|
19494
|
+
const source = this.sourceName("live", index, liveIngestor);
|
|
18744
19495
|
while (!this.stopMainThread) try {
|
|
19496
|
+
this.markSourceSuccess(source, "live");
|
|
18745
19497
|
await liveIngestor.startListening(this.liveHeaders);
|
|
18746
19498
|
if (this.stopMainThread) return;
|
|
18747
19499
|
restartCount++;
|
|
@@ -18752,6 +19504,7 @@ var Chaintracks = class {
|
|
|
18752
19504
|
if (this.stopMainThread) return;
|
|
18753
19505
|
restartCount++;
|
|
18754
19506
|
const e = WalletError.fromUnknown(error_);
|
|
19507
|
+
this.markSourceFailure(source, "live", e);
|
|
18755
19508
|
const waitMsecs = this.liveIngestorRestartWaitMsecs(restartCount);
|
|
18756
19509
|
this.log(`Live ingestor ${name} failed restart=${restartCount} retryMsecs=${waitMsecs}: ${e.stack ?? e.message}`);
|
|
18757
19510
|
await wait(waitMsecs);
|
|
@@ -18799,7 +19552,8 @@ var Chaintracks = class {
|
|
|
18799
19552
|
storage: this.storage.constructor.name,
|
|
18800
19553
|
bulkIngestors: this.bulkIngestors.map((bulkIngestor) => bulkIngestor.constructor.name),
|
|
18801
19554
|
liveIngestors: this.liveIngestors.map((liveIngestor) => liveIngestor.constructor.name),
|
|
18802
|
-
packages: []
|
|
19555
|
+
packages: [],
|
|
19556
|
+
sources: Array.from(this.sourceStatus.values()).map((status) => ({ ...status }))
|
|
18803
19557
|
};
|
|
18804
19558
|
}
|
|
18805
19559
|
async getHeaders(height, count) {
|
|
@@ -18882,26 +19636,30 @@ var Chaintracks = class {
|
|
|
18882
19636
|
let madeProgress = false;
|
|
18883
19637
|
let hadSuccess = false;
|
|
18884
19638
|
let done = false;
|
|
18885
|
-
for (const bulk of this.bulkIngestors)
|
|
18886
|
-
const
|
|
18887
|
-
|
|
18888
|
-
|
|
18889
|
-
|
|
18890
|
-
|
|
18891
|
-
|
|
18892
|
-
|
|
18893
|
-
|
|
18894
|
-
|
|
18895
|
-
|
|
18896
|
-
|
|
18897
|
-
|
|
18898
|
-
|
|
18899
|
-
|
|
19639
|
+
for (const [index, bulk] of this.bulkIngestors.entries()) {
|
|
19640
|
+
const source = this.sourceName("bulk", index, bulk);
|
|
19641
|
+
try {
|
|
19642
|
+
const beforeBulkMax = before.bulk.maxHeight;
|
|
19643
|
+
const beforeLiveRange = HeightRange.from(newLiveHeaders);
|
|
19644
|
+
const r = await bulk.synchronize(presentHeight, before, newLiveHeaders);
|
|
19645
|
+
hadSuccess = true;
|
|
19646
|
+
this.markSourceSuccess(source, "bulk");
|
|
19647
|
+
newLiveHeaders = r.liveHeaders;
|
|
19648
|
+
after = await this.storage.getAvailableHeightRanges();
|
|
19649
|
+
const added = after.bulk.above(before.bulk);
|
|
19650
|
+
const afterLiveRange = HeightRange.from(newLiveHeaders);
|
|
19651
|
+
if (after.bulk.maxHeight > beforeBulkMax || afterLiveRange.maxHeight > beforeLiveRange.maxHeight) madeProgress = true;
|
|
19652
|
+
before = after;
|
|
19653
|
+
this.log(`Bulk Ingestor: ${added.length} added with ${newLiveHeaders.length} live headers from ${bulk.constructor.name}`);
|
|
19654
|
+
if (r.done) {
|
|
19655
|
+
done = true;
|
|
19656
|
+
break;
|
|
19657
|
+
}
|
|
19658
|
+
} catch (error_) {
|
|
19659
|
+
const e = bulkSyncError = WalletError.fromUnknown(error_);
|
|
19660
|
+
this.markSourceFailure(source, "bulk", e);
|
|
19661
|
+
this.log(`bulk sync error: ${e.message}`);
|
|
18900
19662
|
}
|
|
18901
|
-
} catch (error_) {
|
|
18902
|
-
const e = bulkSyncError = WalletError.fromUnknown(error_);
|
|
18903
|
-
this.log(`bulk sync error: ${e.message}`);
|
|
18904
|
-
if (!this.available) break;
|
|
18905
19663
|
}
|
|
18906
19664
|
if (!this.available && bulkSyncError != null && !hadSuccess) this.startupError = bulkSyncError;
|
|
18907
19665
|
return {
|
|
@@ -18911,10 +19669,41 @@ var Chaintracks = class {
|
|
|
18911
19669
|
madeProgress
|
|
18912
19670
|
};
|
|
18913
19671
|
}
|
|
19672
|
+
sourceName(role, index, source) {
|
|
19673
|
+
return `${role}[${index}]:${source.constructor.name}`;
|
|
19674
|
+
}
|
|
19675
|
+
markSourceSuccess(name, role) {
|
|
19676
|
+
this.sourceStatus.set(name, {
|
|
19677
|
+
...this.sourceStatus.get(name),
|
|
19678
|
+
name,
|
|
19679
|
+
role,
|
|
19680
|
+
state: "healthy",
|
|
19681
|
+
lastSuccess: (/* @__PURE__ */ new Date()).toISOString(),
|
|
19682
|
+
error: void 0
|
|
19683
|
+
});
|
|
19684
|
+
}
|
|
19685
|
+
markSourceFailure(name, role, error) {
|
|
19686
|
+
this.sourceStatus.set(name, {
|
|
19687
|
+
...this.sourceStatus.get(name),
|
|
19688
|
+
name,
|
|
19689
|
+
role,
|
|
19690
|
+
state: "degraded",
|
|
19691
|
+
lastFailure: (/* @__PURE__ */ new Date()).toISOString(),
|
|
19692
|
+
error: error.message
|
|
19693
|
+
});
|
|
19694
|
+
}
|
|
18914
19695
|
async getMissingBlockHeader(hash) {
|
|
18915
|
-
for (const live of this.liveIngestors) {
|
|
18916
|
-
const
|
|
18917
|
-
|
|
19696
|
+
for (const [index, live] of this.liveIngestors.entries()) {
|
|
19697
|
+
const source = this.sourceName("live", index, live);
|
|
19698
|
+
try {
|
|
19699
|
+
const header = await live.getHeaderByHash(hash);
|
|
19700
|
+
this.markSourceSuccess(source, "live");
|
|
19701
|
+
if (header != null) return header;
|
|
19702
|
+
} catch (error) {
|
|
19703
|
+
const resolved = WalletError.fromUnknown(error);
|
|
19704
|
+
this.markSourceFailure(source, "live", resolved);
|
|
19705
|
+
this.log(`Header lookup source ${source} failed: ${resolved.message}`);
|
|
19706
|
+
}
|
|
18918
19707
|
}
|
|
18919
19708
|
}
|
|
18920
19709
|
invalidInsertHeaderResult(ihr) {
|
|
@@ -19247,6 +20036,9 @@ var GoChaintracksServiceClient = class {
|
|
|
19247
20036
|
chain;
|
|
19248
20037
|
baseUrl;
|
|
19249
20038
|
fetcher;
|
|
20039
|
+
requestTimeoutMsecs;
|
|
20040
|
+
reconnectWaitMsecs;
|
|
20041
|
+
reconnectWaitMaxMsecs;
|
|
19250
20042
|
subscriptions = /* @__PURE__ */ new Map();
|
|
19251
20043
|
nextSubscriptionId = 1;
|
|
19252
20044
|
constructor(chain, serviceUrl, options = {}) {
|
|
@@ -19260,6 +20052,15 @@ var GoChaintracksServiceClient = class {
|
|
|
19260
20052
|
}
|
|
19261
20053
|
this.baseUrl = `${base}${prefix}`;
|
|
19262
20054
|
this.fetcher = options.fetch ?? fetch;
|
|
20055
|
+
this.requestTimeoutMsecs = options.requestTimeoutMsecs ?? 3e4;
|
|
20056
|
+
this.reconnectWaitMsecs = options.reconnectWaitMsecs ?? 1e3;
|
|
20057
|
+
this.reconnectWaitMaxMsecs = options.reconnectWaitMaxMsecs ?? 6e4;
|
|
20058
|
+
for (const [name, value] of [
|
|
20059
|
+
["requestTimeoutMsecs", this.requestTimeoutMsecs],
|
|
20060
|
+
["reconnectWaitMsecs", this.reconnectWaitMsecs],
|
|
20061
|
+
["reconnectWaitMaxMsecs", this.reconnectWaitMaxMsecs]
|
|
20062
|
+
]) if (!Number.isSafeInteger(value) || value < 1) throw new Error(`${name} must be a positive integer.`);
|
|
20063
|
+
if (this.reconnectWaitMaxMsecs < this.reconnectWaitMsecs) throw new Error("reconnectWaitMaxMsecs must be greater than or equal to reconnectWaitMsecs.");
|
|
19263
20064
|
}
|
|
19264
20065
|
async currentHeight() {
|
|
19265
20066
|
return await this.getPresentHeight();
|
|
@@ -19269,12 +20070,8 @@ var GoChaintracksServiceClient = class {
|
|
|
19269
20070
|
return h != null && root === asString(h.merkleRoot);
|
|
19270
20071
|
}
|
|
19271
20072
|
async getChain() {
|
|
19272
|
-
|
|
19273
|
-
|
|
19274
|
-
return this.normalizeChain(r.network);
|
|
19275
|
-
} catch {
|
|
19276
|
-
return this.chain;
|
|
19277
|
-
}
|
|
20073
|
+
const r = await this.getJson("/network");
|
|
20074
|
+
return this.normalizeChain(typeof r === "string" ? r : r.network);
|
|
19278
20075
|
}
|
|
19279
20076
|
async getInfo() {
|
|
19280
20077
|
const tip = await this.findChainTipHeader();
|
|
@@ -19289,11 +20086,11 @@ var GoChaintracksServiceClient = class {
|
|
|
19289
20086
|
};
|
|
19290
20087
|
}
|
|
19291
20088
|
async getPresentHeight() {
|
|
19292
|
-
|
|
20089
|
+
const result = await this.getJson("/height");
|
|
20090
|
+
return typeof result === "number" ? result : result.height;
|
|
19293
20091
|
}
|
|
19294
20092
|
async getHeaders(height, count) {
|
|
19295
|
-
|
|
19296
|
-
return Buffer.from(bytes).toString("hex");
|
|
20093
|
+
return asString(await this.getBinary(`/headers.bin?height=${height}&count=${count}`));
|
|
19297
20094
|
}
|
|
19298
20095
|
async findChainTipHeader() {
|
|
19299
20096
|
return await this.getJson("/tip");
|
|
@@ -19349,7 +20146,7 @@ var GoChaintracksServiceClient = class {
|
|
|
19349
20146
|
async subscribe(type, path, onPayload) {
|
|
19350
20147
|
const id = `${type}-${this.nextSubscriptionId++}`;
|
|
19351
20148
|
const abort = new AbortController();
|
|
19352
|
-
const done = this.
|
|
20149
|
+
const done = this.runSseWithReconnect(path, abort.signal, onPayload);
|
|
19353
20150
|
this.subscriptions.set(id, {
|
|
19354
20151
|
id,
|
|
19355
20152
|
type,
|
|
@@ -19361,30 +20158,75 @@ var GoChaintracksServiceClient = class {
|
|
|
19361
20158
|
});
|
|
19362
20159
|
return id;
|
|
19363
20160
|
}
|
|
19364
|
-
async
|
|
19365
|
-
|
|
19366
|
-
|
|
19367
|
-
|
|
20161
|
+
async runSseWithReconnect(path, signal, onPayload) {
|
|
20162
|
+
let failures = 0;
|
|
20163
|
+
while (!signal.aborted) {
|
|
20164
|
+
try {
|
|
20165
|
+
failures = await this.runSse(path, signal, onPayload) ? 0 : failures + 1;
|
|
20166
|
+
} catch {
|
|
20167
|
+
if (signal.aborted) return;
|
|
20168
|
+
failures++;
|
|
20169
|
+
}
|
|
20170
|
+
const multiplier = Math.min(2 ** Math.max(0, failures - 1), 64);
|
|
20171
|
+
const delay = Math.min(this.reconnectWaitMsecs * multiplier, this.reconnectWaitMaxMsecs);
|
|
20172
|
+
await this.waitForReconnect(delay, signal);
|
|
20173
|
+
}
|
|
20174
|
+
}
|
|
20175
|
+
async waitForReconnect(msecs, signal) {
|
|
20176
|
+
if (signal.aborted || msecs <= 0) return;
|
|
20177
|
+
await new Promise((resolve) => {
|
|
20178
|
+
let timeout;
|
|
20179
|
+
const onAbort = () => done();
|
|
20180
|
+
const done = () => {
|
|
20181
|
+
clearTimeout(timeout);
|
|
20182
|
+
signal.removeEventListener("abort", onAbort);
|
|
20183
|
+
resolve();
|
|
20184
|
+
};
|
|
20185
|
+
timeout = setTimeout(done, msecs);
|
|
20186
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
19368
20187
|
});
|
|
19369
|
-
|
|
19370
|
-
|
|
19371
|
-
const
|
|
19372
|
-
const
|
|
19373
|
-
|
|
20188
|
+
}
|
|
20189
|
+
async runSse(path, signal, onPayload) {
|
|
20190
|
+
const controller = new AbortController();
|
|
20191
|
+
const onAbort = () => controller.abort();
|
|
20192
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
20193
|
+
const timeout = setTimeout(() => controller.abort(), this.requestTimeoutMsecs);
|
|
20194
|
+
let receivedEvent = false;
|
|
20195
|
+
const observePayload = (payload) => {
|
|
20196
|
+
receivedEvent = true;
|
|
20197
|
+
onPayload(payload);
|
|
20198
|
+
};
|
|
19374
20199
|
try {
|
|
19375
|
-
|
|
19376
|
-
|
|
19377
|
-
|
|
19378
|
-
|
|
19379
|
-
|
|
20200
|
+
const response = await this.fetcher(this.url(path), {
|
|
20201
|
+
headers: { Accept: "text/event-stream" },
|
|
20202
|
+
signal: controller.signal
|
|
20203
|
+
});
|
|
20204
|
+
clearTimeout(timeout);
|
|
20205
|
+
if (!response.ok) throw new Error(`GET ${this.url(path)} failed ${response.status} ${response.statusText}`);
|
|
20206
|
+
if (response.body == null) throw new Error(`GET ${this.url(path)} returned no response body`);
|
|
20207
|
+
const reader = response.body.getReader();
|
|
20208
|
+
const decoder = new TextDecoder();
|
|
20209
|
+
let buffer = "";
|
|
20210
|
+
try {
|
|
20211
|
+
for (;;) {
|
|
20212
|
+
const { done, value } = await reader.read();
|
|
20213
|
+
if (done) break;
|
|
20214
|
+
buffer += decoder.decode(value, { stream: true });
|
|
20215
|
+
buffer = this.processSseBuffer(buffer, observePayload);
|
|
20216
|
+
}
|
|
20217
|
+
buffer += decoder.decode();
|
|
20218
|
+
this.processSseBuffer(`${buffer}\n\n`, observePayload);
|
|
20219
|
+
} finally {
|
|
20220
|
+
reader.releaseLock();
|
|
19380
20221
|
}
|
|
19381
|
-
buffer += decoder.decode();
|
|
19382
|
-
this.processSseBuffer(`${buffer}\n\n`, onPayload);
|
|
19383
20222
|
} finally {
|
|
19384
|
-
|
|
20223
|
+
clearTimeout(timeout);
|
|
20224
|
+
signal.removeEventListener("abort", onAbort);
|
|
19385
20225
|
}
|
|
20226
|
+
return receivedEvent;
|
|
19386
20227
|
}
|
|
19387
20228
|
processSseBuffer(buffer, onPayload) {
|
|
20229
|
+
buffer = buffer.replaceAll("\r\n", "\n");
|
|
19388
20230
|
for (;;) {
|
|
19389
20231
|
const boundary = buffer.indexOf("\n\n");
|
|
19390
20232
|
if (boundary < 0) return buffer;
|
|
@@ -19403,32 +20245,51 @@ var GoChaintracksServiceClient = class {
|
|
|
19403
20245
|
return r;
|
|
19404
20246
|
}
|
|
19405
20247
|
async getJsonOrUndefined(path) {
|
|
19406
|
-
const response = await this.
|
|
20248
|
+
const response = await this.fetchWithTimeout(this.url(path), { headers: { Accept: "application/json" } });
|
|
19407
20249
|
if (response.status === 404) return void 0;
|
|
19408
20250
|
if (!response.ok) throw new Error(`GET ${this.url(path)} failed ${response.status} ${response.statusText}`);
|
|
19409
|
-
|
|
20251
|
+
const value = await response.json();
|
|
20252
|
+
if (value != null && typeof value === "object" && "status" in value) {
|
|
20253
|
+
const envelope = value;
|
|
20254
|
+
if (envelope.status === "success") return envelope.value;
|
|
20255
|
+
if (envelope.status === "error") throw new Error(envelope.description ?? `GET ${this.url(path)} failed`);
|
|
20256
|
+
}
|
|
20257
|
+
return value;
|
|
19410
20258
|
}
|
|
19411
20259
|
async getBinary(path) {
|
|
19412
|
-
const response = await this.
|
|
20260
|
+
const response = await this.fetchWithTimeout(this.url(path), { headers: { Accept: "application/octet-stream" } });
|
|
19413
20261
|
if (!response.ok) throw new Error(`GET ${this.url(path)} failed ${response.status} ${response.statusText}`);
|
|
19414
20262
|
return new Uint8Array(await response.arrayBuffer());
|
|
19415
20263
|
}
|
|
20264
|
+
async fetchWithTimeout(url, init) {
|
|
20265
|
+
const controller = new AbortController();
|
|
20266
|
+
const timeout = setTimeout(() => controller.abort(), this.requestTimeoutMsecs);
|
|
20267
|
+
try {
|
|
20268
|
+
return await this.fetcher(url, {
|
|
20269
|
+
...init,
|
|
20270
|
+
signal: controller.signal
|
|
20271
|
+
});
|
|
20272
|
+
} finally {
|
|
20273
|
+
clearTimeout(timeout);
|
|
20274
|
+
}
|
|
20275
|
+
}
|
|
19416
20276
|
url(path) {
|
|
19417
20277
|
return `${this.baseUrl}${path}`;
|
|
19418
20278
|
}
|
|
19419
20279
|
normalizeChain(network) {
|
|
19420
|
-
switch (network) {
|
|
20280
|
+
switch (network.trim().toLowerCase()) {
|
|
19421
20281
|
case "main":
|
|
19422
20282
|
case "mainnet": return "main";
|
|
19423
20283
|
case "test":
|
|
19424
20284
|
case "testnet": return "test";
|
|
20285
|
+
case "stn":
|
|
20286
|
+
case "scalingtestnet": return "stn";
|
|
19425
20287
|
case "ttn":
|
|
19426
20288
|
case "teratest":
|
|
19427
20289
|
case "teratestnet": return "ttn";
|
|
19428
20290
|
case "tstn":
|
|
19429
|
-
case "teranodescalingtestnet":
|
|
19430
|
-
|
|
19431
|
-
default: return this.chain;
|
|
20291
|
+
case "teranodescalingtestnet": return "tstn";
|
|
20292
|
+
default: throw new Error(`Unsupported ChainTracks upstream network '${network}'.`);
|
|
19432
20293
|
}
|
|
19433
20294
|
}
|
|
19434
20295
|
};
|
|
@@ -20761,31 +21622,45 @@ var ServiceCollection = class ServiceCollection {
|
|
|
20761
21622
|
//#endregion
|
|
20762
21623
|
//#region ../src/services/networkConfig.ts
|
|
20763
21624
|
/**
|
|
20764
|
-
* Runtime service-endpoint configuration for
|
|
21625
|
+
* Runtime service-endpoint configuration for Teranode networks that do not
|
|
21626
|
+
* have a public, operator-independent service endpoint.
|
|
20765
21627
|
*
|
|
20766
|
-
* Unlike `main`, `test`, and `ttn`, the tstn service endpoints are not public and must not be
|
|
21628
|
+
* Unlike `main`, `test`, and `ttn`, the stn/tstn service endpoints are not public and must not be
|
|
20767
21629
|
* hardcoded in this (public) source tree. They are supplied at runtime through environment
|
|
20768
21630
|
* variables:
|
|
20769
21631
|
*
|
|
21632
|
+
* STN_ARCADE_URL STN Arcade broadcaster / ARC endpoint base.
|
|
21633
|
+
* STN_CHAINTRACKS_URL STN ChainTracks service URL.
|
|
20770
21634
|
* TSTN_ARCADE_URL Arcade broadcaster / ARC endpoint base. Also the fallback host for
|
|
20771
21635
|
* ChainTracks when TSTN_CHAINTRACKS_URL is unset
|
|
20772
21636
|
* (`${TSTN_ARCADE_URL}/chaintracks/v1`, mirroring the ttn layout).
|
|
20773
21637
|
* TSTN_CHAINTRACKS_URL ChainTracks service URL.
|
|
20774
21638
|
*
|
|
20775
|
-
* tstn
|
|
20776
|
-
* WhatsOnChain
|
|
21639
|
+
* stn/tstn run only operator-configured Arcade and ChainTracks services; there is no
|
|
21640
|
+
* documented WhatsOnChain service for them, so no WhatsOnChain endpoint is configured and
|
|
20777
21641
|
* the WhatsOnChain-only lookups (raw tx, utxo status, txid status, script-hash history) are not
|
|
20778
|
-
* available on tstn.
|
|
21642
|
+
* available on stn/tstn.
|
|
20779
21643
|
*
|
|
20780
|
-
* `process` is accessed defensively so importing this module remains safe in
|
|
20781
|
-
*
|
|
20782
|
-
*
|
|
21644
|
+
* `process` is accessed defensively so importing this module remains safe in
|
|
21645
|
+
* browser bundles. Browser applications can still supply an explicit
|
|
21646
|
+
* ChaintracksClientApi without relying on environment variables.
|
|
20783
21647
|
*/
|
|
20784
21648
|
function readEnv(name) {
|
|
20785
21649
|
const value = (typeof process !== "undefined" ? process.env : void 0)?.[name];
|
|
20786
21650
|
return value != null && value.trim() !== "" ? value.trim() : void 0;
|
|
20787
21651
|
}
|
|
20788
|
-
|
|
21652
|
+
/** Credential-free public Arcade host for supported networks. */
|
|
21653
|
+
function publicArcadeUrl(chain) {
|
|
21654
|
+
switch (chain) {
|
|
21655
|
+
case "main": return "https://arcade-v2-us-1.bsvblockchain.tech";
|
|
21656
|
+
case "test": return "https://arcade-v2-testnet-us-1.bsvblockchain.tech";
|
|
21657
|
+
case "ttn": return "https://arcade-v2-ttn-us-1.bsvblockchain.tech";
|
|
21658
|
+
case "stn":
|
|
21659
|
+
case "tstn":
|
|
21660
|
+
case "mock": return;
|
|
21661
|
+
}
|
|
21662
|
+
}
|
|
21663
|
+
const stripTrailingSlash$1 = (url) => {
|
|
20789
21664
|
let end = url.length;
|
|
20790
21665
|
while (end > 0 && url[end - 1] === "/") end--;
|
|
20791
21666
|
return url.slice(0, end);
|
|
@@ -20794,6 +21669,10 @@ const stripTrailingSlash = (url) => {
|
|
|
20794
21669
|
function tstnArcadeUrl() {
|
|
20795
21670
|
return readEnv("TSTN_ARCADE_URL");
|
|
20796
21671
|
}
|
|
21672
|
+
/** Arcade broadcaster / ARC endpoint for stn, or `undefined` when unset. */
|
|
21673
|
+
function stnArcadeUrl() {
|
|
21674
|
+
return readEnv("STN_ARCADE_URL");
|
|
21675
|
+
}
|
|
20797
21676
|
/**
|
|
20798
21677
|
* ChainTracks service URL for tstn. Falls back to `${TSTN_ARCADE_URL}/chaintracks/v1` when
|
|
20799
21678
|
* `TSTN_CHAINTRACKS_URL` is unset (mirrors the ttn layout). Throws when neither is configured.
|
|
@@ -20802,20 +21681,53 @@ function tstnChaintracksUrl() {
|
|
|
20802
21681
|
const explicit = readEnv("TSTN_CHAINTRACKS_URL");
|
|
20803
21682
|
if (explicit != null) return explicit;
|
|
20804
21683
|
const arcade = tstnArcadeUrl();
|
|
20805
|
-
if (arcade != null) return `${stripTrailingSlash(arcade)}/chaintracks/v1`;
|
|
21684
|
+
if (arcade != null) return `${stripTrailingSlash$1(arcade)}/chaintracks/v1`;
|
|
20806
21685
|
throw new Error("tstn chain requires a ChainTracks URL: set TSTN_CHAINTRACKS_URL (or TSTN_ARCADE_URL) in the environment.");
|
|
20807
21686
|
}
|
|
21687
|
+
/**
|
|
21688
|
+
* ChainTracks service URL for stn. Falls back to the configured Arcade host's
|
|
21689
|
+
* legacy-compatible path when STN_CHAINTRACKS_URL is unset.
|
|
21690
|
+
*/
|
|
21691
|
+
function stnChaintracksUrl() {
|
|
21692
|
+
const explicit = readEnv("STN_CHAINTRACKS_URL");
|
|
21693
|
+
if (explicit != null) return explicit;
|
|
21694
|
+
const arcade = stnArcadeUrl();
|
|
21695
|
+
if (arcade != null) return `${stripTrailingSlash$1(arcade)}/chaintracks/v1`;
|
|
21696
|
+
throw new Error("stn chain requires a ChainTracks URL: set STN_CHAINTRACKS_URL (or STN_ARCADE_URL) in the environment.");
|
|
21697
|
+
}
|
|
20808
21698
|
//#endregion
|
|
20809
21699
|
//#region ../src/services/createDefaultWalletServicesOptions.ts
|
|
21700
|
+
function stripTrailingSlash(value) {
|
|
21701
|
+
let end = value.length;
|
|
21702
|
+
while (end > 0 && value[end - 1] === "/") end--;
|
|
21703
|
+
return value.slice(0, end);
|
|
21704
|
+
}
|
|
21705
|
+
function configuredChaintracksClient(chain, serviceUrl) {
|
|
21706
|
+
let path = "";
|
|
21707
|
+
try {
|
|
21708
|
+
path = stripTrailingSlash(new URL(serviceUrl).pathname);
|
|
21709
|
+
} catch {}
|
|
21710
|
+
if (path.endsWith("/v2")) return new GoChaintracksServiceClient(chain, serviceUrl);
|
|
21711
|
+
return new ChaintracksServiceClient(chain, serviceUrl);
|
|
21712
|
+
}
|
|
21713
|
+
/**
|
|
21714
|
+
* Returns the credential-free default ChainTracks client for a supported
|
|
21715
|
+
* public network, or an operator-configured client for stn/tstn.
|
|
21716
|
+
*/
|
|
21717
|
+
function createDefaultChaintracksClient(chain) {
|
|
21718
|
+
switch (chain) {
|
|
21719
|
+
case "main":
|
|
21720
|
+
case "test":
|
|
21721
|
+
case "ttn": return new GoChaintracksServiceClient(chain, arcadeDefaultUrl(chain), { apiPrefix: "/chaintracks/v2" });
|
|
21722
|
+
case "stn": return configuredChaintracksClient(chain, stnChaintracksUrl());
|
|
21723
|
+
case "tstn": return configuredChaintracksClient(chain, tstnChaintracksUrl());
|
|
21724
|
+
}
|
|
21725
|
+
}
|
|
20810
21726
|
function createDefaultWalletServicesOptions(...[chain, arcCallbackUrl, arcCallbackToken, taalArcApiKey, gorillaPoolArcApiKey, bitailsApiKey, deploymentId, chaintracks, arcadeUrl, arcadeApiKey, arcadeCallbackToken]) {
|
|
20811
21727
|
if (chain === "mock") throw new Error("createDefaultWalletServicesOptions does not support 'mock' chain. Use MockServices directly.");
|
|
20812
21728
|
deploymentId ||= `wallet-toolbox-${randomBytesHex(16)}`;
|
|
20813
|
-
let chaintracksUrl;
|
|
20814
|
-
if (chain === "ttn") chaintracksUrl = "https://arcade-v2-ttn-us-1.bsvblockchain.tech/chaintracks/v1";
|
|
20815
|
-
else if (chain === "tstn") chaintracksUrl = tstnChaintracksUrl();
|
|
20816
|
-
else chaintracksUrl = `https://${chain}net-chaintracks.babbage.systems`;
|
|
20817
21729
|
const chaintracksFiatExchangeRatesUrl = "https://mainnet-chaintracks.babbage.systems/getFiatExchangeRates";
|
|
20818
|
-
chaintracks ||=
|
|
21730
|
+
chaintracks ||= createDefaultChaintracksClient(chain);
|
|
20819
21731
|
const o = {
|
|
20820
21732
|
chain,
|
|
20821
21733
|
taalApiKey: void 0,
|
|
@@ -20873,14 +21785,15 @@ function createDefaultWalletServicesOptions(...[chain, arcCallbackUrl, arcCallba
|
|
|
20873
21785
|
}
|
|
20874
21786
|
/**
|
|
20875
21787
|
* Default Arcade (bsv-blockchain/arcade) endpoint per chain.
|
|
20876
|
-
* Returns undefined when no public default is known for the chain
|
|
21788
|
+
* Returns undefined when no public default is known for the chain.
|
|
20877
21789
|
*/
|
|
20878
21790
|
function arcadeDefaultUrl(chain) {
|
|
20879
21791
|
switch (chain) {
|
|
20880
|
-
case "main":
|
|
20881
|
-
case "
|
|
21792
|
+
case "main":
|
|
21793
|
+
case "test":
|
|
21794
|
+
case "ttn": return publicArcadeUrl(chain);
|
|
21795
|
+
case "stn": return stnArcadeUrl();
|
|
20882
21796
|
case "tstn": return tstnArcadeUrl();
|
|
20883
|
-
case "test": return;
|
|
20884
21797
|
case "mock": return;
|
|
20885
21798
|
}
|
|
20886
21799
|
}
|
|
@@ -20888,6 +21801,7 @@ function arcDefaultUrl(chain) {
|
|
|
20888
21801
|
switch (chain) {
|
|
20889
21802
|
case "main": return "https://arc.taal.com";
|
|
20890
21803
|
case "test": return "https://arc-test.taal.com";
|
|
21804
|
+
case "stn": return stnArcadeUrl() ?? "";
|
|
20891
21805
|
case "ttn": return "https://arcade-v2-ttn-us-1.bsvblockchain.tech/";
|
|
20892
21806
|
case "tstn": return tstnArcadeUrl() ?? "";
|
|
20893
21807
|
case "mock": return "";
|
|
@@ -22269,7 +23183,7 @@ var Services = class Services {
|
|
|
22269
23183
|
telemetry;
|
|
22270
23184
|
constructor(optionsOrChain) {
|
|
22271
23185
|
this.chain = typeof optionsOrChain === "string" ? optionsOrChain : optionsOrChain.chain;
|
|
22272
|
-
if (this.chain === "mock") throw new WERR_INVALID_PARAMETER("chain", "'main', 'test', 'ttn', or 'tstn'. Use MockServices for 'mock' chain.");
|
|
23186
|
+
if (this.chain === "mock") throw new WERR_INVALID_PARAMETER("chain", "'main', 'test', 'stn', 'ttn', or 'tstn'. Use MockServices for 'mock' chain.");
|
|
22273
23187
|
this.options = typeof optionsOrChain === "string" ? Services.createDefaultOptions(this.chain) : optionsOrChain;
|
|
22274
23188
|
this.telemetry = new _bsv_sdk.Telemetry(this.options.telemetry);
|
|
22275
23189
|
this.whatsonchain = new WhatsOnChain(this.chain, { apiKey: this.options.whatsOnChainApiKey }, this);
|
|
@@ -22283,7 +23197,7 @@ var Services = class Services {
|
|
|
22283
23197
|
if (this.options.arcGorillaPoolUrl != null && this.options.arcGorillaPoolUrl !== "") this.arcGorillaPool = new ARC(this.options.arcGorillaPoolUrl, this.options.arcGorillaPoolConfig, "arcGorillaPool");
|
|
22284
23198
|
if (this.options.arcadeUrl != null && this.options.arcadeUrl !== "") this.arcade = new Arcade(this.options.arcadeUrl, this.options.arcadeConfig, "arcade");
|
|
22285
23199
|
const hasBitails = this.chain === "main" || this.chain === "test";
|
|
22286
|
-
const hasWhatsOnChain = this.chain
|
|
23200
|
+
const hasWhatsOnChain = this.chain === "main" || this.chain === "test";
|
|
22287
23201
|
if (hasBitails) this.bitails = new Bitails(this.chain, { apiKey: this.options.bitailsApiKey });
|
|
22288
23202
|
return {
|
|
22289
23203
|
hasBitails,
|
|
@@ -22997,9 +23911,22 @@ function classifyMerklePathResponse(status, statusText, retry) {
|
|
|
22997
23911
|
//#endregion
|
|
22998
23912
|
//#region ../src/services/providers/WhatsOnChain.ts
|
|
22999
23913
|
var WhatsOnChainNoServices = class extends SdkWhatsOnChain {
|
|
23914
|
+
requestGate;
|
|
23000
23915
|
constructor(chain = "main", config = {}) {
|
|
23001
23916
|
if (chain === "mock") throw new Error("WhatsOnChain does not support 'mock' chain. Use MockServices directly.");
|
|
23002
23917
|
super(chain, config);
|
|
23918
|
+
this.requestGate = config.requestGate;
|
|
23919
|
+
}
|
|
23920
|
+
async requestWithAnonymousAuthFallback(url, requestOptions) {
|
|
23921
|
+
await this.requestGate?.();
|
|
23922
|
+
const response = await this.httpClient.request(url, requestOptions);
|
|
23923
|
+
if (response.status !== 401 && response.status !== 403 || this.apiKey.trim() === "") return response;
|
|
23924
|
+
if (this.requestGate != null) await this.requestGate();
|
|
23925
|
+
else await wait(350);
|
|
23926
|
+
return await this.httpClient.request(url, {
|
|
23927
|
+
method: "GET",
|
|
23928
|
+
headers: { Accept: "application/json" }
|
|
23929
|
+
});
|
|
23003
23930
|
}
|
|
23004
23931
|
/**
|
|
23005
23932
|
* POST
|
|
@@ -23408,7 +24335,7 @@ var WhatsOnChainNoServices = class extends SdkWhatsOnChain {
|
|
|
23408
24335
|
};
|
|
23409
24336
|
const url = `${this.URL}/block/${hash}/header`;
|
|
23410
24337
|
for (let retry = 0; retry < 2; retry++) {
|
|
23411
|
-
const response = await this.
|
|
24338
|
+
const response = await this.requestWithAnonymousAuthFallback(url, requestOptions);
|
|
23412
24339
|
if (response.statusText === "Too Many Requests" && retry < 2) {
|
|
23413
24340
|
await wait(2e3);
|
|
23414
24341
|
continue;
|
|
@@ -23426,7 +24353,7 @@ var WhatsOnChainNoServices = class extends SdkWhatsOnChain {
|
|
|
23426
24353
|
};
|
|
23427
24354
|
const url = `${this.URL}/chain/info`;
|
|
23428
24355
|
for (let retry = 0; retry < 2; retry++) {
|
|
23429
|
-
const response = await this.
|
|
24356
|
+
const response = await this.requestWithAnonymousAuthFallback(url, requestOptions);
|
|
23430
24357
|
if (response.statusText === "Too Many Requests" && retry < 2) {
|
|
23431
24358
|
await wait(2e3);
|
|
23432
24359
|
continue;
|
|
@@ -23612,12 +24539,16 @@ var WhatsOnChainServices = class WhatsOnChainServices {
|
|
|
23612
24539
|
timeout: 3e4,
|
|
23613
24540
|
userAgent: "BabbageWhatsOnChainServices",
|
|
23614
24541
|
enableCache: true,
|
|
23615
|
-
chainInfoMsecs: 5e3
|
|
24542
|
+
chainInfoMsecs: 5e3,
|
|
24543
|
+
minRequestIntervalMsecs: 350
|
|
23616
24544
|
};
|
|
23617
24545
|
}
|
|
23618
24546
|
static chainInfo = [];
|
|
23619
24547
|
static chainInfoTime = [];
|
|
23620
24548
|
static chainInfoMsecs = [];
|
|
24549
|
+
static chainInfoPromise = {};
|
|
24550
|
+
static requestTail = Promise.resolve();
|
|
24551
|
+
static nextRequestMsecs = 0;
|
|
23621
24552
|
chain;
|
|
23622
24553
|
woc;
|
|
23623
24554
|
constructor(options) {
|
|
@@ -23626,7 +24557,8 @@ var WhatsOnChainServices = class WhatsOnChainServices {
|
|
|
23626
24557
|
apiKey: this.options.apiKey,
|
|
23627
24558
|
timeout: this.options.timeout,
|
|
23628
24559
|
userAgent: this.options.userAgent,
|
|
23629
|
-
enableCache: this.options.enableCache
|
|
24560
|
+
enableCache: this.options.enableCache,
|
|
24561
|
+
requestGate: async () => await this.waitForRateLimit()
|
|
23630
24562
|
};
|
|
23631
24563
|
this.chain = options.chain;
|
|
23632
24564
|
const chainInfoMsecs = WhatsOnChainServices.chainInfoMsecs;
|
|
@@ -23644,7 +24576,16 @@ var WhatsOnChainServices = class WhatsOnChainServices {
|
|
|
23644
24576
|
let update = chainInfo[this.chain] === void 0;
|
|
23645
24577
|
if (!update && chainInfoTime[this.chain] !== void 0) update = now.getTime() - chainInfoTime[this.chain].getTime() > chainInfoMsecs[this.chain];
|
|
23646
24578
|
if (update) {
|
|
23647
|
-
|
|
24579
|
+
let pending = WhatsOnChainServices.chainInfoPromise[this.chain];
|
|
24580
|
+
if (pending == null) {
|
|
24581
|
+
pending = this.woc.getChainInfo();
|
|
24582
|
+
WhatsOnChainServices.chainInfoPromise[this.chain] = pending;
|
|
24583
|
+
}
|
|
24584
|
+
try {
|
|
24585
|
+
chainInfo[this.chain] = await pending;
|
|
24586
|
+
} finally {
|
|
24587
|
+
if (WhatsOnChainServices.chainInfoPromise[this.chain] === pending) delete WhatsOnChainServices.chainInfoPromise[this.chain];
|
|
24588
|
+
}
|
|
23648
24589
|
chainInfoTime[this.chain] = now;
|
|
23649
24590
|
}
|
|
23650
24591
|
if (!chainInfo[this.chain]) throw new Error("Unexpected failure to update chainInfo.");
|
|
@@ -23662,10 +24603,12 @@ var WhatsOnChainServices = class WhatsOnChainServices {
|
|
|
23662
24603
|
*/
|
|
23663
24604
|
async getHeaders(fetch) {
|
|
23664
24605
|
fetch ||= new ChaintracksFetch();
|
|
24606
|
+
await this.waitForRateLimit();
|
|
23665
24607
|
return await fetch.fetchJson(`https://api.whatsonchain.com/v1/bsv/${this.chain}/block/headers`);
|
|
23666
24608
|
}
|
|
23667
24609
|
async getHeaderByteFileLinks(neededRange, fetch) {
|
|
23668
24610
|
fetch ||= new ChaintracksFetch();
|
|
24611
|
+
await this.waitForRateLimit();
|
|
23669
24612
|
const files = await fetch.fetchJson(`https://api.whatsonchain.com/v1/bsv/${this.chain}/block/headers/resources`);
|
|
23670
24613
|
const r = [];
|
|
23671
24614
|
let range;
|
|
@@ -23678,6 +24621,21 @@ var WhatsOnChainServices = class WhatsOnChainServices {
|
|
|
23678
24621
|
}
|
|
23679
24622
|
return r;
|
|
23680
24623
|
}
|
|
24624
|
+
async waitForRateLimit() {
|
|
24625
|
+
let release;
|
|
24626
|
+
const previous = WhatsOnChainServices.requestTail;
|
|
24627
|
+
WhatsOnChainServices.requestTail = new Promise((resolve) => {
|
|
24628
|
+
release = resolve;
|
|
24629
|
+
});
|
|
24630
|
+
await previous;
|
|
24631
|
+
try {
|
|
24632
|
+
const delay = Math.max(0, WhatsOnChainServices.nextRequestMsecs - Date.now());
|
|
24633
|
+
if (delay > 0) await wait(delay);
|
|
24634
|
+
WhatsOnChainServices.nextRequestMsecs = Date.now() + (this.options.minRequestIntervalMsecs ?? 350);
|
|
24635
|
+
} finally {
|
|
24636
|
+
release();
|
|
24637
|
+
}
|
|
24638
|
+
}
|
|
23681
24639
|
};
|
|
23682
24640
|
function wocGetHeadersHeaderToBlockHeader(h) {
|
|
23683
24641
|
const bits = typeof h.bits === "string" ? Number.parseInt(h.bits, 16) : h.bits;
|
|
@@ -23738,6 +24696,51 @@ var BulkIngestorWhatsOnChainCdn = class extends BulkIngestorBase {
|
|
|
23738
24696
|
}
|
|
23739
24697
|
};
|
|
23740
24698
|
//#endregion
|
|
24699
|
+
//#region ../src/services/chaintracker/chaintracks/Ingest/BulkIngestorChaintracks.ts
|
|
24700
|
+
/**
|
|
24701
|
+
* Uses a go-chaintracks/Arcade-compatible service as a validated bulk source.
|
|
24702
|
+
* Retrieved bytes still pass through ChainTracks' local serialization, hash,
|
|
24703
|
+
* continuity, and genesis checks before storage.
|
|
24704
|
+
*/
|
|
24705
|
+
var BulkIngestorChaintracks = class extends BulkIngestorBase {
|
|
24706
|
+
chaintracks;
|
|
24707
|
+
maxHeadersPerRequest;
|
|
24708
|
+
networkChecked = false;
|
|
24709
|
+
constructor(options) {
|
|
24710
|
+
super(options);
|
|
24711
|
+
this.chaintracks = options.chaintracks;
|
|
24712
|
+
this.maxHeadersPerRequest = options.maxHeadersPerRequest ?? 1e3;
|
|
24713
|
+
if (!Number.isInteger(this.maxHeadersPerRequest) || this.maxHeadersPerRequest < 1) throw new Error("maxHeadersPerRequest must be a positive integer.");
|
|
24714
|
+
}
|
|
24715
|
+
async getPresentHeight() {
|
|
24716
|
+
await this.ensureNetwork();
|
|
24717
|
+
return await this.chaintracks.getPresentHeight();
|
|
24718
|
+
}
|
|
24719
|
+
async fetchHeaders(_before, fetchRange, bulkRange, priorLiveHeaders) {
|
|
24720
|
+
if (fetchRange.isEmpty) return priorLiveHeaders;
|
|
24721
|
+
await this.ensureNetwork();
|
|
24722
|
+
let liveHeaders = priorLiveHeaders;
|
|
24723
|
+
let height = fetchRange.minHeight;
|
|
24724
|
+
while (height <= fetchRange.maxHeight) {
|
|
24725
|
+
const requested = Math.min(this.maxHeadersPerRequest, fetchRange.maxHeight - height + 1);
|
|
24726
|
+
const bytes = asUint8Array(await this.chaintracks.getHeaders(height, requested));
|
|
24727
|
+
if (bytes.length === 0) throw new Error(`ChainTracks upstream returned no headers at height ${height}.`);
|
|
24728
|
+
if (bytes.length % 80 !== 0 || bytes.length > requested * 80) throw new Error(`ChainTracks upstream returned ${bytes.length} bytes for ${requested} headers at height ${height}.`);
|
|
24729
|
+
const headers = deserializeBlockHeaders(height, bytes);
|
|
24730
|
+
liveHeaders = await this.storage().addBulkHeaders(headers, bulkRange, liveHeaders);
|
|
24731
|
+
height += headers.length;
|
|
24732
|
+
if (headers.length < requested && height <= fetchRange.maxHeight) throw new Error(`ChainTracks upstream returned ${headers.length} of ${requested} headers at height ${height - headers.length}.`);
|
|
24733
|
+
}
|
|
24734
|
+
return liveHeaders;
|
|
24735
|
+
}
|
|
24736
|
+
async ensureNetwork() {
|
|
24737
|
+
if (this.networkChecked) return;
|
|
24738
|
+
const actual = await this.chaintracks.getChain();
|
|
24739
|
+
if (actual !== this.chain) throw new Error(`ChainTracks upstream network '${actual}' does not match configured chain '${this.chain}'.`);
|
|
24740
|
+
this.networkChecked = true;
|
|
24741
|
+
}
|
|
24742
|
+
};
|
|
24743
|
+
//#endregion
|
|
23741
24744
|
//#region ../src/services/chaintracker/chaintracks/Ingest/LiveIngestorWhatsOnChainPoll.ts
|
|
23742
24745
|
/**
|
|
23743
24746
|
* Reports new headers by polling periodically.
|
|
@@ -23844,9 +24847,17 @@ var LiveIngestorChaintracksSSE = class extends LiveIngestorBase {
|
|
|
23844
24847
|
}
|
|
23845
24848
|
async startListening(liveHeaders) {
|
|
23846
24849
|
this.stopped = false;
|
|
23847
|
-
|
|
24850
|
+
const actual = await this.options.chaintracks.getChain();
|
|
24851
|
+
if (actual !== this.chain) throw new Error(`ChainTracks upstream network '${actual}' does not match configured chain '${this.chain}'.`);
|
|
24852
|
+
if (this.stopped) return;
|
|
24853
|
+
const subscriptionId = await this.options.chaintracks.subscribeHeaders((header) => {
|
|
23848
24854
|
if (!this.stopped) liveHeaders.push(header);
|
|
23849
24855
|
});
|
|
24856
|
+
if (this.stopped) {
|
|
24857
|
+
await this.options.chaintracks.unsubscribe(subscriptionId);
|
|
24858
|
+
return;
|
|
24859
|
+
}
|
|
24860
|
+
this.subscriptionId = subscriptionId;
|
|
23850
24861
|
await new Promise((resolve) => {
|
|
23851
24862
|
this.resolveStopped = resolve;
|
|
23852
24863
|
if (this.stopped) resolve();
|
|
@@ -23859,7 +24870,9 @@ var LiveIngestorChaintracksSSE = class extends LiveIngestorBase {
|
|
|
23859
24870
|
if (subscriptionId != null) this.options.chaintracks.unsubscribe(subscriptionId).catch((e) => {
|
|
23860
24871
|
this.log(`LiveIngestorChaintracksSSE unsubscribe failed: ${e}`);
|
|
23861
24872
|
});
|
|
23862
|
-
this.resolveStopped
|
|
24873
|
+
const resolveStopped = this.resolveStopped;
|
|
24874
|
+
this.resolveStopped = void 0;
|
|
24875
|
+
resolveStopped?.();
|
|
23863
24876
|
}
|
|
23864
24877
|
async shutdown() {
|
|
23865
24878
|
this.stopListening();
|
|
@@ -24555,6 +25568,27 @@ var ChaintracksStorageNoDb = class ChaintracksStorageNoDb extends ChaintracksSto
|
|
|
24555
25568
|
tipHeaderId: 0,
|
|
24556
25569
|
hashToHeaderId: /* @__PURE__ */ new Map()
|
|
24557
25570
|
};
|
|
25571
|
+
static stnData = {
|
|
25572
|
+
chain: "stn",
|
|
25573
|
+
liveHeaders: /* @__PURE__ */ new Map(),
|
|
25574
|
+
maxHeaderId: 0,
|
|
25575
|
+
tipHeaderId: 0,
|
|
25576
|
+
hashToHeaderId: /* @__PURE__ */ new Map()
|
|
25577
|
+
};
|
|
25578
|
+
static ttnData = {
|
|
25579
|
+
chain: "ttn",
|
|
25580
|
+
liveHeaders: /* @__PURE__ */ new Map(),
|
|
25581
|
+
maxHeaderId: 0,
|
|
25582
|
+
tipHeaderId: 0,
|
|
25583
|
+
hashToHeaderId: /* @__PURE__ */ new Map()
|
|
25584
|
+
};
|
|
25585
|
+
static tstnData = {
|
|
25586
|
+
chain: "tstn",
|
|
25587
|
+
liveHeaders: /* @__PURE__ */ new Map(),
|
|
25588
|
+
maxHeaderId: 0,
|
|
25589
|
+
tipHeaderId: 0,
|
|
25590
|
+
hashToHeaderId: /* @__PURE__ */ new Map()
|
|
25591
|
+
};
|
|
24558
25592
|
constructor(options) {
|
|
24559
25593
|
super(options);
|
|
24560
25594
|
}
|
|
@@ -24562,10 +25596,11 @@ var ChaintracksStorageNoDb = class ChaintracksStorageNoDb extends ChaintracksSto
|
|
|
24562
25596
|
async getData() {
|
|
24563
25597
|
switch (this.chain) {
|
|
24564
25598
|
case "main": return ChaintracksStorageNoDb.mainData;
|
|
24565
|
-
case "test":
|
|
24566
|
-
case "
|
|
24567
|
-
case "
|
|
24568
|
-
|
|
25599
|
+
case "test": return ChaintracksStorageNoDb.testData;
|
|
25600
|
+
case "stn": return ChaintracksStorageNoDb.stnData;
|
|
25601
|
+
case "ttn": return ChaintracksStorageNoDb.ttnData;
|
|
25602
|
+
case "tstn": return ChaintracksStorageNoDb.tstnData;
|
|
25603
|
+
default: throw new WERR_INVALID_PARAMETER("chain", `'main', 'test', 'stn', 'ttn', or 'tstn'. '${this.chain}' is unsupported.`);
|
|
24569
25604
|
}
|
|
24570
25605
|
}
|
|
24571
25606
|
async deleteLiveBlockHeaders() {
|
|
@@ -25154,7 +26189,7 @@ var ChaintracksStorageIdb = class extends ChaintracksStorageBase {
|
|
|
25154
26189
|
//#endregion
|
|
25155
26190
|
//#region ../src/services/chaintracker/chaintracks/configureChaintracksIngestors.ts
|
|
25156
26191
|
function resolveDefaultChaintracksArguments(args) {
|
|
25157
|
-
const [chain, whatsonchainApiKey = "", maxPerFile = 1e5, maxRetained = 2, fetch = new ChaintracksFetch(), cdnUrl = "https://cdn.projectbabbage.com/blockheaders/", liveHeightThreshold = 2e3, reorgHeightThreshold = 400, bulkMigrationChunkSize = 500, batchInsertLimit = 400, addLiveRecursionLimit = 36] = args;
|
|
26192
|
+
const [chain, whatsonchainApiKey = "", maxPerFile = 1e5, maxRetained = 2, fetch = new ChaintracksFetch(), cdnUrl = chain === "main" || chain === "test" ? "https://cdn.projectbabbage.com/blockheaders/" : "", liveHeightThreshold = 2e3, reorgHeightThreshold = 400, bulkMigrationChunkSize = 500, batchInsertLimit = 400, addLiveRecursionLimit = 36, sources = {}] = args;
|
|
25158
26193
|
return {
|
|
25159
26194
|
chain,
|
|
25160
26195
|
whatsonchainApiKey,
|
|
@@ -25166,11 +26201,12 @@ function resolveDefaultChaintracksArguments(args) {
|
|
|
25166
26201
|
reorgHeightThreshold,
|
|
25167
26202
|
bulkMigrationChunkSize,
|
|
25168
26203
|
batchInsertLimit,
|
|
25169
|
-
addLiveRecursionLimit
|
|
26204
|
+
addLiveRecursionLimit,
|
|
26205
|
+
sources
|
|
25170
26206
|
};
|
|
25171
26207
|
}
|
|
25172
26208
|
function toDefaultChaintracksArguments(params) {
|
|
25173
|
-
|
|
26209
|
+
const args = [
|
|
25174
26210
|
params.chain,
|
|
25175
26211
|
params.whatsonchainApiKey,
|
|
25176
26212
|
params.maxPerFile,
|
|
@@ -25183,6 +26219,8 @@ function toDefaultChaintracksArguments(params) {
|
|
|
25183
26219
|
params.batchInsertLimit,
|
|
25184
26220
|
params.addLiveRecursionLimit
|
|
25185
26221
|
];
|
|
26222
|
+
if (Object.keys(params.sources).length > 0) args.push(params.sources);
|
|
26223
|
+
return args;
|
|
25186
26224
|
}
|
|
25187
26225
|
function createDefaultBulkFileDataManager(params) {
|
|
25188
26226
|
return new BulkFileDataManager({
|
|
@@ -25225,7 +26263,7 @@ function createAndStartDefaultChaintracks(args, createOptions) {
|
|
|
25225
26263
|
* The caller is responsible for providing the storage implementation.
|
|
25226
26264
|
*/
|
|
25227
26265
|
function buildChaintracksOptionsWithIngestors(params, storage) {
|
|
25228
|
-
const { chain, whatsonchainApiKey, maxPerFile, fetch, cdnUrl, addLiveRecursionLimit } = params;
|
|
26266
|
+
const { chain, whatsonchainApiKey, maxPerFile, fetch, cdnUrl, addLiveRecursionLimit, sources } = params;
|
|
25229
26267
|
const co = {
|
|
25230
26268
|
chain,
|
|
25231
26269
|
storage,
|
|
@@ -25236,35 +26274,58 @@ function buildChaintracksOptionsWithIngestors(params, storage) {
|
|
|
25236
26274
|
readonly: false
|
|
25237
26275
|
};
|
|
25238
26276
|
const jsonResource = `${chain}NetBlockHeaders.json`;
|
|
25239
|
-
|
|
25240
|
-
|
|
25241
|
-
|
|
25242
|
-
|
|
25243
|
-
|
|
25244
|
-
|
|
25245
|
-
|
|
25246
|
-
|
|
25247
|
-
|
|
25248
|
-
|
|
25249
|
-
|
|
25250
|
-
|
|
25251
|
-
|
|
25252
|
-
|
|
25253
|
-
|
|
25254
|
-
|
|
25255
|
-
|
|
25256
|
-
|
|
25257
|
-
|
|
25258
|
-
|
|
25259
|
-
|
|
25260
|
-
|
|
25261
|
-
|
|
25262
|
-
|
|
25263
|
-
|
|
25264
|
-
|
|
25265
|
-
|
|
26277
|
+
if (!sources.disableCdn && cdnUrl !== "") {
|
|
26278
|
+
const bulkCdnOptions = {
|
|
26279
|
+
chain,
|
|
26280
|
+
jsonResource,
|
|
26281
|
+
fetch,
|
|
26282
|
+
cdnUrl,
|
|
26283
|
+
maxPerFile
|
|
26284
|
+
};
|
|
26285
|
+
co.bulkIngestors.push(new BulkIngestorCDNBabbage(bulkCdnOptions));
|
|
26286
|
+
}
|
|
26287
|
+
const chaintracksSource = sources.chaintracks ?? (sources.disableChaintracks ? void 0 : createPublicChaintracksSource(chain));
|
|
26288
|
+
if (chaintracksSource != null) {
|
|
26289
|
+
co.bulkIngestors.push(new BulkIngestorChaintracks({
|
|
26290
|
+
chain,
|
|
26291
|
+
jsonResource,
|
|
26292
|
+
chaintracks: chaintracksSource,
|
|
26293
|
+
maxHeadersPerRequest: sources.remoteMaxHeadersPerRequest
|
|
26294
|
+
}));
|
|
26295
|
+
co.liveIngestors.push(new LiveIngestorChaintracksSSE({
|
|
26296
|
+
chain,
|
|
26297
|
+
chaintracks: chaintracksSource
|
|
26298
|
+
}));
|
|
26299
|
+
}
|
|
26300
|
+
if ((chain === "main" || chain === "test") && !sources.disableWhatsOnChain) {
|
|
26301
|
+
const wocOptions = {
|
|
26302
|
+
chain,
|
|
26303
|
+
apiKey: whatsonchainApiKey,
|
|
26304
|
+
timeout: 3e4,
|
|
26305
|
+
userAgent: "BabbageWhatsOnChainServices",
|
|
26306
|
+
enableCache: true,
|
|
26307
|
+
chainInfoMsecs: 5e3
|
|
26308
|
+
};
|
|
26309
|
+
const bulkOptions = {
|
|
26310
|
+
...wocOptions,
|
|
26311
|
+
jsonResource,
|
|
26312
|
+
idleWait: 5e3
|
|
26313
|
+
};
|
|
26314
|
+
co.bulkIngestors.push(new BulkIngestorWhatsOnChainCdn(bulkOptions));
|
|
26315
|
+
const liveOptions = {
|
|
26316
|
+
...wocOptions,
|
|
26317
|
+
idleWait: 1e5
|
|
26318
|
+
};
|
|
26319
|
+
co.liveIngestors.push(new LiveIngestorWhatsOnChainPoll(liveOptions));
|
|
26320
|
+
}
|
|
26321
|
+
if (co.bulkIngestors.length === 0 || co.liveIngestors.length === 0) throw new Error(`ChainTracks ${chain} requires at least one bulk and live source. Configure sources.chaintracks for Teranode networks.`);
|
|
25266
26322
|
return co;
|
|
25267
26323
|
}
|
|
26324
|
+
function createPublicChaintracksSource(chain) {
|
|
26325
|
+
const serviceUrl = publicArcadeUrl(chain);
|
|
26326
|
+
if (serviceUrl == null) return void 0;
|
|
26327
|
+
return new GoChaintracksServiceClient(chain, serviceUrl, { apiPrefix: "/chaintracks/v2" });
|
|
26328
|
+
}
|
|
25268
26329
|
//#endregion
|
|
25269
26330
|
//#region ../src/services/chaintracker/chaintracks/createDefaultNoDbChaintracksOptions.ts
|
|
25270
26331
|
function createDefaultNoDbChaintracksOptions(...args) {
|
|
@@ -27993,7 +29054,7 @@ function isValidProfile(value) {
|
|
|
27993
29054
|
return typeof profile.name === "string" && profile.name.length > 0 && profile.name.length <= 250 && isByteArrayOfLength(profile.id, 16) && isByteArrayOfLength(profile.primaryPad, 32) && isByteArrayOfLength(profile.privilegedPad, 32) && typeof profile.createdAt === "number" && Number.isFinite(profile.createdAt) && profile.createdAt >= 0;
|
|
27994
29055
|
}
|
|
27995
29056
|
/**
|
|
27996
|
-
* Raised when UMP
|
|
29057
|
+
* Raised when a UMP lookup yields neither a verified token nor a clean empty response.
|
|
27997
29058
|
*
|
|
27998
29059
|
* Callers must offer retry/recovery rather than treating this error as a new
|
|
27999
29060
|
* account. Diagnostics contain counts only and never hashes, keys, or tokens.
|
|
@@ -28091,49 +29152,119 @@ var OverlayUMPTokenInteractor = class {
|
|
|
28091
29152
|
throw new UMPTokenLookupError("lookup-unavailable", diagnostics, { cause: error });
|
|
28092
29153
|
}
|
|
28093
29154
|
const diagnostics = this.toLookupDiagnostics(resolution);
|
|
28094
|
-
if (resolution.answer.outputs.length === 0) {
|
|
28095
|
-
if (!(resolution.progress.isFinal && resolution.progress.hostCount > 0 && resolution.progress.completedHosts === resolution.progress.hostCount && resolution.progress.successfulHosts === resolution.progress.hostCount && resolution.progress.emptyHosts === resolution.progress.hostCount && resolution.progress.failedHosts === 0 && resolution.progress.rejectedHosts === 0 && resolution.progress.freeformHosts === 0)) {
|
|
28096
|
-
this.captureLookupFailure(lookupKind, "lookup-incomplete", diagnostics, startedAt);
|
|
28097
|
-
throw new UMPTokenLookupError("lookup-incomplete", diagnostics);
|
|
28098
|
-
}
|
|
28099
|
-
this.telemetry.capture({
|
|
28100
|
-
name: "wallet-toolbox.ump.lookup.completed",
|
|
28101
|
-
component: "wallet-toolbox.ump",
|
|
28102
|
-
severity: "info",
|
|
28103
|
-
correlationId: diagnostics.correlationId,
|
|
28104
|
-
attributes: {
|
|
28105
|
-
lookupKind,
|
|
28106
|
-
result: "not-found",
|
|
28107
|
-
durationMs: Date.now() - startedAt,
|
|
28108
|
-
...this.lookupDiagnosticAttributes(diagnostics)
|
|
28109
|
-
}
|
|
28110
|
-
});
|
|
28111
|
-
return;
|
|
28112
|
-
}
|
|
28113
29155
|
const tokens = this.parseLookupAnswers(resolution.answer);
|
|
28114
29156
|
const expectedHash = question.query[lookupKind === "presentation" ? "presentationHash" : "recoveryHash"].toLowerCase();
|
|
28115
|
-
|
|
28116
|
-
|
|
28117
|
-
|
|
28118
|
-
|
|
28119
|
-
|
|
29157
|
+
const matchingTokens = tokens.filter((token) => _bsv_sdk.Utils.toHex(lookupKind === "presentation" ? token.presentationHash : token.recoveryHash).toLowerCase() === expectedHash);
|
|
29158
|
+
if (matchingTokens.length > 1) {
|
|
29159
|
+
const newest = this.resolveNewestToken(matchingTokens, resolution.answer.outputs);
|
|
29160
|
+
if (newest != null) {
|
|
29161
|
+
this.captureLookupCompleted(lookupKind, "found", diagnostics, startedAt, { supersededTokens: matchingTokens.length - 1 });
|
|
29162
|
+
return newest;
|
|
29163
|
+
}
|
|
28120
29164
|
const reason = "token-ambiguous";
|
|
28121
29165
|
this.captureLookupFailure(lookupKind, reason, diagnostics, startedAt);
|
|
28122
29166
|
throw new UMPTokenLookupError(reason, diagnostics);
|
|
28123
29167
|
}
|
|
28124
|
-
|
|
28125
|
-
|
|
28126
|
-
|
|
28127
|
-
|
|
28128
|
-
|
|
28129
|
-
|
|
28130
|
-
|
|
28131
|
-
|
|
28132
|
-
|
|
28133
|
-
|
|
29168
|
+
if (matchingTokens.length === 1) {
|
|
29169
|
+
this.captureLookupCompleted(lookupKind, "found", diagnostics, startedAt);
|
|
29170
|
+
return matchingTokens[0];
|
|
29171
|
+
}
|
|
29172
|
+
if (resolution.progress.emptyHosts > 0) {
|
|
29173
|
+
this.captureLookupCompleted(lookupKind, "not-found", diagnostics, startedAt);
|
|
29174
|
+
return;
|
|
29175
|
+
}
|
|
29176
|
+
const reason = resolution.answer.outputs.length > 0 ? "token-malformed" : "lookup-incomplete";
|
|
29177
|
+
this.captureLookupFailure(lookupKind, reason, diagnostics, startedAt);
|
|
29178
|
+
throw new UMPTokenLookupError(reason, diagnostics);
|
|
29179
|
+
}
|
|
29180
|
+
/**
|
|
29181
|
+
* Picks the newest rendition among distinct verified tokens, when possible.
|
|
29182
|
+
*
|
|
29183
|
+
* The on-chain UMP protocol expresses token updates by consumption: the
|
|
29184
|
+
* transaction creating a new rendition spends the previous rendition's
|
|
29185
|
+
* outpoint (there is no rendition counter field in the current format).
|
|
29186
|
+
* A candidate is therefore superseded when any other candidate's ancestry
|
|
29187
|
+
* (available from its BEEF) spends the candidate's outpoint.
|
|
29188
|
+
*
|
|
29189
|
+
* @returns The single unsuperseded candidate, or undefined when supersession
|
|
29190
|
+
* cannot be established for every stale candidate (e.g. forked tokens).
|
|
29191
|
+
*/
|
|
29192
|
+
resolveNewestToken(matchingTokens, outputs) {
|
|
29193
|
+
const candidates = /* @__PURE__ */ new Map();
|
|
29194
|
+
for (const token of matchingTokens) {
|
|
29195
|
+
if (token.currentOutpoint == null) return void 0;
|
|
29196
|
+
candidates.set(token.currentOutpoint, token);
|
|
29197
|
+
}
|
|
29198
|
+
const evidenceByCandidate = /* @__PURE__ */ new Map();
|
|
29199
|
+
for (const output of outputs) try {
|
|
29200
|
+
const tx = _bsv_sdk.Transaction.fromBEEF(output.beef);
|
|
29201
|
+
const outpoint = `${tx.id("hex")}.${output.outputIndex}`;
|
|
29202
|
+
if (!candidates.has(outpoint)) continue;
|
|
29203
|
+
const evidence = evidenceByCandidate.get(outpoint) ?? {
|
|
29204
|
+
txs: [],
|
|
29205
|
+
spent: /* @__PURE__ */ new Set()
|
|
29206
|
+
};
|
|
29207
|
+
evidence.txs.push(tx);
|
|
29208
|
+
this.collectSpentOutpoints(tx, evidence.spent, /* @__PURE__ */ new Set());
|
|
29209
|
+
evidenceByCandidate.set(outpoint, evidence);
|
|
29210
|
+
} catch {}
|
|
29211
|
+
if (evidenceByCandidate.size !== candidates.size) return void 0;
|
|
29212
|
+
const survivors = [...candidates.keys()].filter((outpoint) => ![...evidenceByCandidate.entries()].some(([other, { spent }]) => other !== outpoint && spent.has(outpoint)));
|
|
29213
|
+
if (survivors.length === 1) return candidates.get(survivors[0]);
|
|
29214
|
+
const provenContinuations = survivors.filter((outpoint) => {
|
|
29215
|
+
const evidence = evidenceByCandidate.get(outpoint);
|
|
29216
|
+
const token = candidates.get(outpoint);
|
|
29217
|
+
return evidence != null && token != null && evidence.txs.some((tx) => this.consumesSameIdentityToken(tx, token));
|
|
29218
|
+
});
|
|
29219
|
+
if (provenContinuations.length !== 1) return void 0;
|
|
29220
|
+
return candidates.get(provenContinuations[0]);
|
|
29221
|
+
}
|
|
29222
|
+
/**
|
|
29223
|
+
* Whether `tx` spends an input whose source output (available in the BEEF)
|
|
29224
|
+
* decodes as a UMP token sharing the candidate's presentation or recovery
|
|
29225
|
+
* hash — on-chain proof that the candidate is an update of a same-identity
|
|
29226
|
+
* predecessor rather than an independently minted token.
|
|
29227
|
+
*/
|
|
29228
|
+
consumesSameIdentityToken(tx, token) {
|
|
29229
|
+
const presentationHash = _bsv_sdk.Utils.toHex(token.presentationHash);
|
|
29230
|
+
const recoveryHash = _bsv_sdk.Utils.toHex(token.recoveryHash);
|
|
29231
|
+
for (const input of tx.inputs) {
|
|
29232
|
+
const source = input.sourceTransaction;
|
|
29233
|
+
if (source == null || input.sourceOutputIndex == null) continue;
|
|
29234
|
+
const sourceOutput = source.outputs[input.sourceOutputIndex];
|
|
29235
|
+
if (sourceOutput == null) continue;
|
|
29236
|
+
try {
|
|
29237
|
+
const decoded = _bsv_sdk.PushDrop.decode(sourceOutput.lockingScript);
|
|
29238
|
+
if (decoded.fields == null) continue;
|
|
29239
|
+
const fields = stripVerifiedPushDropSignature(decoded.fields, decoded.lockingPublicKey);
|
|
29240
|
+
if (fields.length < 11 || fields[6]?.length !== 32 || fields[7]?.length !== 32) continue;
|
|
29241
|
+
if (_bsv_sdk.Utils.toHex(fields[6]) === presentationHash || _bsv_sdk.Utils.toHex(fields[7]) === recoveryHash) return true;
|
|
29242
|
+
} catch {
|
|
29243
|
+
continue;
|
|
28134
29244
|
}
|
|
28135
|
-
}
|
|
28136
|
-
return
|
|
29245
|
+
}
|
|
29246
|
+
return false;
|
|
29247
|
+
}
|
|
29248
|
+
/**
|
|
29249
|
+
* Accumulates every outpoint spent by `tx` and by the ancestor transactions
|
|
29250
|
+
* embedded in its BEEF, so supersession is detected even when intermediate
|
|
29251
|
+
* renditions are absent from the lookup answer. Iterative so arbitrarily
|
|
29252
|
+
* long update chains cannot exhaust the call stack.
|
|
29253
|
+
*/
|
|
29254
|
+
collectSpentOutpoints(tx, spent, visited) {
|
|
29255
|
+
const pending = [tx];
|
|
29256
|
+
while (pending.length > 0) {
|
|
29257
|
+
const current = pending.pop();
|
|
29258
|
+
const txid = current.id("hex");
|
|
29259
|
+
if (visited.has(txid)) continue;
|
|
29260
|
+
visited.add(txid);
|
|
29261
|
+
for (const input of current.inputs) {
|
|
29262
|
+
const sourceTxid = input.sourceTXID ?? input.sourceTransaction?.id("hex");
|
|
29263
|
+
if (sourceTxid == null || input.sourceOutputIndex == null) continue;
|
|
29264
|
+
spent.add(`${sourceTxid}.${input.sourceOutputIndex}`);
|
|
29265
|
+
if (input.sourceTransaction != null) pending.push(input.sourceTransaction);
|
|
29266
|
+
}
|
|
29267
|
+
}
|
|
28137
29268
|
}
|
|
28138
29269
|
emptyLookupDiagnostics(correlationId) {
|
|
28139
29270
|
return {
|
|
@@ -28174,6 +29305,21 @@ var OverlayUMPTokenInteractor = class {
|
|
|
28174
29305
|
outputCount: diagnostics.outputCount
|
|
28175
29306
|
};
|
|
28176
29307
|
}
|
|
29308
|
+
captureLookupCompleted(lookupKind, result, diagnostics, startedAt, extraAttributes = {}) {
|
|
29309
|
+
this.telemetry.capture({
|
|
29310
|
+
name: "wallet-toolbox.ump.lookup.completed",
|
|
29311
|
+
component: "wallet-toolbox.ump",
|
|
29312
|
+
severity: "info",
|
|
29313
|
+
correlationId: diagnostics.correlationId,
|
|
29314
|
+
attributes: {
|
|
29315
|
+
lookupKind,
|
|
29316
|
+
result,
|
|
29317
|
+
durationMs: Date.now() - startedAt,
|
|
29318
|
+
...this.lookupDiagnosticAttributes(diagnostics),
|
|
29319
|
+
...extraAttributes
|
|
29320
|
+
}
|
|
29321
|
+
});
|
|
29322
|
+
}
|
|
28177
29323
|
captureLookupFailure(lookupKind, reason, diagnostics, startedAt, error) {
|
|
28178
29324
|
this.telemetry.capture({
|
|
28179
29325
|
name: "wallet-toolbox.ump.lookup.indeterminate",
|
|
@@ -28414,8 +29560,7 @@ var OverlayUMPTokenInteractor = class {
|
|
|
28414
29560
|
throw new UMPTokenLookupError("lookup-unavailable", diagnostics, { cause: error });
|
|
28415
29561
|
}
|
|
28416
29562
|
if (resolution.answer.outputs.length === 0) {
|
|
28417
|
-
|
|
28418
|
-
if (!(p.isFinal && p.hostCount > 0 && p.completedHosts === p.hostCount && p.successfulHosts === p.hostCount && p.emptyHosts === p.hostCount && p.failedHosts === 0 && p.rejectedHosts === 0 && p.freeformHosts === 0)) {
|
|
29563
|
+
if (resolution.progress.emptyHosts === 0) {
|
|
28419
29564
|
const diagnostics = this.toLookupDiagnostics(resolution);
|
|
28420
29565
|
this.captureLookupFailure("outpoint", "lookup-incomplete", diagnostics, startedAt);
|
|
28421
29566
|
throw new UMPTokenLookupError("lookup-incomplete", diagnostics);
|
|
@@ -33776,6 +34921,7 @@ exports.BulkFilesReaderStorage = BulkFilesReaderStorage;
|
|
|
33776
34921
|
exports.BulkIngestorBase = BulkIngestorBase;
|
|
33777
34922
|
exports.BulkIngestorCDN = BulkIngestorCDN;
|
|
33778
34923
|
exports.BulkIngestorCDNBabbage = BulkIngestorCDNBabbage;
|
|
34924
|
+
exports.BulkIngestorChaintracks = BulkIngestorChaintracks;
|
|
33779
34925
|
exports.BulkIngestorWhatsOnChainCdn = BulkIngestorWhatsOnChainCdn;
|
|
33780
34926
|
exports.BulkStorageBase = BulkStorageBase;
|
|
33781
34927
|
exports.CWIStyleWalletManager = CWIStyleWalletManager;
|
|
@@ -33854,7 +35000,12 @@ exports.asBsvSdkTx = asBsvSdkTx;
|
|
|
33854
35000
|
exports.asString = asString;
|
|
33855
35001
|
exports.asUint8Array = asUint8Array;
|
|
33856
35002
|
exports.brc29ProtocolID = brc29ProtocolID;
|
|
35003
|
+
exports.buildChaintracksOptionsWithIngestors = buildChaintracksOptionsWithIngestors;
|
|
33857
35004
|
exports.convertProofToMerklePath = convertProofToMerklePath;
|
|
35005
|
+
exports.createAndStartDefaultChaintracks = createAndStartDefaultChaintracks;
|
|
35006
|
+
exports.createDefaultBulkFileDataManager = createDefaultBulkFileDataManager;
|
|
35007
|
+
exports.createDefaultChaintracksClient = createDefaultChaintracksClient;
|
|
35008
|
+
exports.createDefaultChaintracksStorageOptions = createDefaultChaintracksStorageOptions;
|
|
33858
35009
|
exports.createDefaultIdbChaintracksOptions = createDefaultIdbChaintracksOptions;
|
|
33859
35010
|
exports.createDefaultNoDbChaintracksOptions = createDefaultNoDbChaintracksOptions;
|
|
33860
35011
|
exports.createDefaultWalletServicesOptions = createDefaultWalletServicesOptions;
|
|
@@ -33896,6 +35047,7 @@ exports.partitionActionLabels = partitionActionLabels;
|
|
|
33896
35047
|
exports.randomBytes = randomBytes;
|
|
33897
35048
|
exports.randomBytesBase64 = randomBytesBase64;
|
|
33898
35049
|
exports.randomBytesHex = randomBytesHex;
|
|
35050
|
+
exports.resolveDefaultChaintracksArguments = resolveDefaultChaintracksArguments;
|
|
33899
35051
|
Object.defineProperty(exports, "sdk", {
|
|
33900
35052
|
enumerable: true,
|
|
33901
35053
|
get: function() {
|
|
@@ -33906,9 +35058,11 @@ exports.selectBulkHeaderFiles = selectBulkHeaderFiles;
|
|
|
33906
35058
|
exports.sha256Hash = sha256Hash;
|
|
33907
35059
|
exports.stampLog = stampLog;
|
|
33908
35060
|
exports.stampLogFormat = stampLogFormat;
|
|
35061
|
+
exports.startChaintracks = startChaintracks;
|
|
33909
35062
|
exports.tableAuthSessionToPeerSession = tableAuthSessionToPeerSession;
|
|
33910
35063
|
exports.throwDummyReviewActions = throwDummyReviewActions;
|
|
33911
35064
|
exports.toBinaryBaseBlockHeader = toBinaryBaseBlockHeader;
|
|
35065
|
+
exports.toDefaultChaintracksArguments = toDefaultChaintracksArguments;
|
|
33912
35066
|
exports.toLookupNetworkPreset = toLookupNetworkPreset;
|
|
33913
35067
|
exports.toWalletNetwork = toWalletNetwork;
|
|
33914
35068
|
exports.transactionColumnsWithoutRawTx = transactionColumnsWithoutRawTx;
|