@bsv/wallet-toolbox-client 2.4.22 → 2.5.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 +1034 -264
- package/out/index.client.cjs.map +1 -1
- package/out/index.client.d.cts +66 -8
- package/out/index.client.d.cts.map +1 -1
- package/out/index.client.d.mts +66 -8
- package/out/index.client.d.mts.map +1 -1
- package/out/index.client.mjs +1035 -265
- package/out/index.client.mjs.map +1 -1
- package/package.json +2 -2
package/out/index.client.cjs
CHANGED
|
@@ -1257,7 +1257,11 @@ var ScriptTemplateBRC29 = class {
|
|
|
1257
1257
|
return `${this.params.derivationPrefix ?? ""} ${this.params.derivationSuffix ?? ""}`;
|
|
1258
1258
|
}
|
|
1259
1259
|
getKeyDeriver(privKey) {
|
|
1260
|
-
if (
|
|
1260
|
+
if (this.params.keyDeriver?.rootKey === privKey) return this.params.keyDeriver;
|
|
1261
|
+
if (typeof privKey === "string") {
|
|
1262
|
+
if (this.params.keyDeriver?.rootKey.toHex() === privKey) return this.params.keyDeriver;
|
|
1263
|
+
privKey = _bsv_sdk.PrivateKey.fromHex(privKey);
|
|
1264
|
+
}
|
|
1261
1265
|
if (this.params.keyDeriver == null || this.params.keyDeriver.rootKey.toHex() !== privKey.toHex()) return new _bsv_sdk.CachedKeyDeriver(privKey);
|
|
1262
1266
|
return this.params.keyDeriver;
|
|
1263
1267
|
}
|
|
@@ -1266,8 +1270,11 @@ var ScriptTemplateBRC29 = class {
|
|
|
1266
1270
|
return this.p2pkh.lock(address);
|
|
1267
1271
|
}
|
|
1268
1272
|
unlock(unlockerPrivKey, lockerPubKey, sourceSatoshis, lockingScript) {
|
|
1269
|
-
const derivedPrivateKey = this.getKeyDeriver(unlockerPrivKey).derivePrivateKey(brc29ProtocolID, this.getKeyID(), lockerPubKey)
|
|
1270
|
-
return this.
|
|
1273
|
+
const derivedPrivateKey = this.getKeyDeriver(unlockerPrivKey).derivePrivateKey(brc29ProtocolID, this.getKeyID(), lockerPubKey);
|
|
1274
|
+
return this.unlockWithDerivedPrivateKey(derivedPrivateKey, sourceSatoshis, lockingScript);
|
|
1275
|
+
}
|
|
1276
|
+
unlockWithDerivedPrivateKey(derivedPrivateKey, sourceSatoshis, lockingScript) {
|
|
1277
|
+
return this.p2pkh.unlock(derivedPrivateKey, "all", false, sourceSatoshis, lockingScript);
|
|
1271
1278
|
}
|
|
1272
1279
|
/**
|
|
1273
1280
|
* P2PKH unlock estimateLength is a constant
|
|
@@ -2461,11 +2468,16 @@ var EntityProvenTx = class EntityProvenTx extends EntityBase {
|
|
|
2461
2468
|
/**
|
|
2462
2469
|
* @returns desirialized `MerklePath` object, value is cached.
|
|
2463
2470
|
*/
|
|
2464
|
-
getMerklePath() {
|
|
2465
|
-
|
|
2466
|
-
|
|
2471
|
+
getMerklePath(validateRoots = true) {
|
|
2472
|
+
if (validateRoots) {
|
|
2473
|
+
this._mp ??= _bsv_sdk.MerklePath.fromBinary(this.api.merklePath);
|
|
2474
|
+
return this._mp;
|
|
2475
|
+
}
|
|
2476
|
+
this._mpUnchecked ??= _bsv_sdk.MerklePath.fromBinary(this.api.merklePath, true, false);
|
|
2477
|
+
return this._mpUnchecked;
|
|
2467
2478
|
}
|
|
2468
2479
|
_mp;
|
|
2480
|
+
_mpUnchecked;
|
|
2469
2481
|
get provenTxId() {
|
|
2470
2482
|
return this.api.provenTxId;
|
|
2471
2483
|
}
|
|
@@ -5062,8 +5074,10 @@ async function mergeInputBeefs(rawTx, beef, trustSelf, knownTxids, trx, required
|
|
|
5062
5074
|
for (const input of tx.inputs) {
|
|
5063
5075
|
const sourceTXID = input.sourceTXID ?? "";
|
|
5064
5076
|
if (sourceTXID === "") throw new WERR_INTERNAL("req all transaction inputs must have valid sourceTXID");
|
|
5065
|
-
|
|
5066
|
-
|
|
5077
|
+
const existing = beef.findTxid(sourceTXID);
|
|
5078
|
+
const callerKnows = (requiredLevels == null || requiredLevels === 0) && knownTxids?.includes(sourceTXID) === true;
|
|
5079
|
+
if (existing != null && (!existing.isTxidOnly || callerKnows || trustSelf === "known")) continue;
|
|
5080
|
+
if (callerKnows) beef.mergeTxidOnly(sourceTXID);
|
|
5067
5081
|
else await getValidBeef(sourceTXID, beef, trustSelf, knownTxids, trx, requiredLevels);
|
|
5068
5082
|
}
|
|
5069
5083
|
}
|
|
@@ -5127,26 +5141,22 @@ async function notifyTransactionsOfProof(ids, provenTxId, addNote, updateTransac
|
|
|
5127
5141
|
* @param options
|
|
5128
5142
|
*/
|
|
5129
5143
|
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();
|
|
5144
|
+
const beef = mergeTarget(options);
|
|
5134
5145
|
const hasKnownTxid = makeKnownTxidLookup$1(options.knownTxids ?? []);
|
|
5135
5146
|
const scheduled = /* @__PURE__ */ new Set([txid]);
|
|
5136
5147
|
let frontier = [{
|
|
5137
5148
|
txid,
|
|
5138
5149
|
depth: 0
|
|
5139
5150
|
}];
|
|
5140
|
-
const
|
|
5141
|
-
const concurrency = Number.isFinite(requestedConcurrency) ? Math.max(1, Math.min(32, Math.floor(requestedConcurrency))) : 8;
|
|
5151
|
+
const concurrency = normalizeConcurrency(options.maxConcurrency);
|
|
5142
5152
|
while (frontier.length > 0) {
|
|
5143
|
-
const current = frontier.filter((item) => beef
|
|
5153
|
+
const current = frontier.filter((item) => needsResolution(beef, item.txid, hasKnownTxid));
|
|
5144
5154
|
const resolved = await mapWithConcurrency(current, concurrency, async (item) => await resolveBeefForTransaction(storage, item.txid, options, hasKnownTxid, item.depth));
|
|
5145
5155
|
const next = [];
|
|
5146
5156
|
for (let i = 0; i < resolved.length; i++) {
|
|
5147
5157
|
const result = resolved[i];
|
|
5148
5158
|
beef.mergeBeef(result.beef);
|
|
5149
|
-
for (const dependency of result.dependencies) if (!scheduled.has(dependency) && beef
|
|
5159
|
+
for (const dependency of result.dependencies) if (!scheduled.has(dependency) && needsResolution(beef, dependency, hasKnownTxid)) {
|
|
5150
5160
|
scheduled.add(dependency);
|
|
5151
5161
|
next.push({
|
|
5152
5162
|
txid: dependency,
|
|
@@ -5158,6 +5168,172 @@ async function getBeefForTransaction(storage, txid, options) {
|
|
|
5158
5168
|
}
|
|
5159
5169
|
return beef;
|
|
5160
5170
|
}
|
|
5171
|
+
/**
|
|
5172
|
+
* Build one aggregate BEEF for several roots while resolving each storage
|
|
5173
|
+
* frontier as a set. This avoids one proof query per funding input on the
|
|
5174
|
+
* createAction success path. Complex proof-level and chain-tracker policies
|
|
5175
|
+
* retain the established single-root implementation.
|
|
5176
|
+
*/
|
|
5177
|
+
async function getBeefForTransactions(storage, txids, options) {
|
|
5178
|
+
const beef = mergeTarget(options);
|
|
5179
|
+
const roots = [...new Set(txids)];
|
|
5180
|
+
if (roots.length === 0) return beef;
|
|
5181
|
+
if (requiresSingleRootPolicy(options)) return await mergeSingleRootFragments(storage, roots, options, beef);
|
|
5182
|
+
const hasKnownTxid = makeKnownTxidLookup$1(options.knownTxids ?? []);
|
|
5183
|
+
const scheduled = new Set(roots);
|
|
5184
|
+
let frontier = roots.map((txid) => ({
|
|
5185
|
+
txid,
|
|
5186
|
+
depth: 0
|
|
5187
|
+
}));
|
|
5188
|
+
while (frontier.length > 0) {
|
|
5189
|
+
const unresolved = collectUnresolvedFrontier(storage, frontier, beef, hasKnownTxid);
|
|
5190
|
+
if (unresolved.length === 0) break;
|
|
5191
|
+
const stored = await storage.getProvenOrRawTxs(unresolved.map((item) => item.txid));
|
|
5192
|
+
if (options.trustSelf !== "known" && unresolved.every((item) => stored.get(item.txid)?.proven != null)) {
|
|
5193
|
+
mergeAllProven(storage, beef, unresolved, stored);
|
|
5194
|
+
break;
|
|
5195
|
+
}
|
|
5196
|
+
const [next, missing] = mergeStoredFrontier(beef, unresolved, stored, options, scheduled, hasKnownTxid);
|
|
5197
|
+
await mergeMissingFragments(storage, beef, missing, options);
|
|
5198
|
+
frontier = next;
|
|
5199
|
+
}
|
|
5200
|
+
return beef;
|
|
5201
|
+
}
|
|
5202
|
+
function mergeTarget(options) {
|
|
5203
|
+
if (options.mergeToBeef instanceof _bsv_sdk.Beef) return options.mergeToBeef;
|
|
5204
|
+
if (options.mergeToBeef != null) return _bsv_sdk.Beef.fromBinary(options.mergeToBeef);
|
|
5205
|
+
return new _bsv_sdk.Beef();
|
|
5206
|
+
}
|
|
5207
|
+
function requiresSingleRootPolicy(options) {
|
|
5208
|
+
return options.ignoreStorage === true || options.minProofLevel !== void 0 || options.chainTracker != null || options.skipInvalidProofs === true;
|
|
5209
|
+
}
|
|
5210
|
+
async function mergeSingleRootFragments(storage, roots, options, beef) {
|
|
5211
|
+
const fragments = await mapWithConcurrency(roots.filter((txid) => beef.findTxid(txid) == null), normalizeConcurrency(options.maxConcurrency), async (txid) => await getBeefForTransaction(storage, txid, {
|
|
5212
|
+
...options,
|
|
5213
|
+
mergeToBeef: void 0
|
|
5214
|
+
}));
|
|
5215
|
+
for (const fragment of fragments) beef.mergeBeef(fragment);
|
|
5216
|
+
return beef;
|
|
5217
|
+
}
|
|
5218
|
+
function collectUnresolvedFrontier(storage, frontier, beef, hasKnownTxid) {
|
|
5219
|
+
const unresolved = [];
|
|
5220
|
+
for (const item of frontier) {
|
|
5221
|
+
if (!needsResolution(beef, item.txid, hasKnownTxid)) continue;
|
|
5222
|
+
if (storage.maxRecursionDepth && storage.maxRecursionDepth <= item.depth) throw new WERR_INVALID_OPERATION(`Maximum BEEF depth exceeded. Limit is ${storage.maxRecursionDepth}`);
|
|
5223
|
+
if (hasKnownTxid(item.txid)) beef.mergeTxidOnly(item.txid);
|
|
5224
|
+
else unresolved.push(item);
|
|
5225
|
+
}
|
|
5226
|
+
return unresolved;
|
|
5227
|
+
}
|
|
5228
|
+
function decodeProvenEntries(storage, unresolved, stored) {
|
|
5229
|
+
const span = storage.telemetry.enabled ? storage.telemetry.startSpan("wallet.storage.beef.decode_proven_batch", {
|
|
5230
|
+
component: "wallet-storage",
|
|
5231
|
+
attributes: { "beef.proven_tx_count": unresolved.length }
|
|
5232
|
+
}) : void 0;
|
|
5233
|
+
try {
|
|
5234
|
+
const entries = unresolved.map((item) => {
|
|
5235
|
+
const proven = stored.get(item.txid).proven;
|
|
5236
|
+
return {
|
|
5237
|
+
rawTx: proven.rawTx,
|
|
5238
|
+
merklePath: new EntityProvenTx(proven).getMerklePath(false),
|
|
5239
|
+
merkleRoot: proven.merkleRoot
|
|
5240
|
+
};
|
|
5241
|
+
});
|
|
5242
|
+
span?.end({ attributes: { "beef.decoded_proof_count": entries.length } });
|
|
5243
|
+
return entries;
|
|
5244
|
+
} catch (error) {
|
|
5245
|
+
span?.end({
|
|
5246
|
+
status: "error",
|
|
5247
|
+
error
|
|
5248
|
+
});
|
|
5249
|
+
throw error;
|
|
5250
|
+
}
|
|
5251
|
+
}
|
|
5252
|
+
function mergeAllProven(storage, beef, unresolved, stored) {
|
|
5253
|
+
const entries = decodeProvenEntries(storage, unresolved, stored);
|
|
5254
|
+
const span = storage.telemetry.enabled ? storage.telemetry.startSpan("wallet.storage.beef.merge_proven_batch", {
|
|
5255
|
+
component: "wallet-storage",
|
|
5256
|
+
attributes: { "beef.proven_tx_count": entries.length }
|
|
5257
|
+
}) : void 0;
|
|
5258
|
+
try {
|
|
5259
|
+
mergeProvenEntries(beef, entries, unresolved, stored);
|
|
5260
|
+
span?.end({ attributes: {
|
|
5261
|
+
"beef.merged_tx_count": entries.length,
|
|
5262
|
+
"beef.result_tx_count": beef.txs.length,
|
|
5263
|
+
"beef.result_bump_count": beef.bumps.length
|
|
5264
|
+
} });
|
|
5265
|
+
} catch (error) {
|
|
5266
|
+
span?.end({
|
|
5267
|
+
status: "error",
|
|
5268
|
+
error
|
|
5269
|
+
});
|
|
5270
|
+
throw error;
|
|
5271
|
+
}
|
|
5272
|
+
}
|
|
5273
|
+
function mergeProvenEntries(beef, entries, unresolved, stored) {
|
|
5274
|
+
if (typeof beef.mergeProvenTxs === "function") {
|
|
5275
|
+
beef.mergeProvenTxs(entries);
|
|
5276
|
+
return;
|
|
5277
|
+
}
|
|
5278
|
+
for (const item of unresolved) {
|
|
5279
|
+
const proven = stored.get(item.txid).proven;
|
|
5280
|
+
beef.mergeRawTx(proven.rawTx);
|
|
5281
|
+
beef.mergeBump(new EntityProvenTx(proven).getMerklePath());
|
|
5282
|
+
}
|
|
5283
|
+
}
|
|
5284
|
+
function mergeStoredFrontier(beef, unresolved, stored, options, scheduled, hasKnownTxid) {
|
|
5285
|
+
const next = [];
|
|
5286
|
+
const missing = [];
|
|
5287
|
+
for (const item of unresolved) {
|
|
5288
|
+
const result = stored.get(item.txid);
|
|
5289
|
+
if (result?.proven != null) mergeStoredProven(beef, item, result, options);
|
|
5290
|
+
else if (result?.rawTx != null) mergeStoredRaw(beef, item, result, options, scheduled, next, hasKnownTxid);
|
|
5291
|
+
else missing.push(item);
|
|
5292
|
+
}
|
|
5293
|
+
return [next, missing];
|
|
5294
|
+
}
|
|
5295
|
+
function mergeStoredProven(beef, item, result, options) {
|
|
5296
|
+
if (options.trustSelf === "known") {
|
|
5297
|
+
beef.mergeTxidOnly(item.txid);
|
|
5298
|
+
return;
|
|
5299
|
+
}
|
|
5300
|
+
const proven = result.proven;
|
|
5301
|
+
beef.mergeRawTx(proven.rawTx);
|
|
5302
|
+
beef.mergeBump(new EntityProvenTx(proven).getMerklePath());
|
|
5303
|
+
}
|
|
5304
|
+
function mergeStoredRaw(beef, item, result, options, scheduled, next, hasKnownTxid) {
|
|
5305
|
+
if (options.trustSelf === "known") {
|
|
5306
|
+
beef.mergeTxidOnly(item.txid);
|
|
5307
|
+
return;
|
|
5308
|
+
}
|
|
5309
|
+
const transaction = beef.mergeRawTx(result.rawTx);
|
|
5310
|
+
if (result.inputBEEF != null) beef.mergeBeef(result.inputBEEF);
|
|
5311
|
+
appendNewDependencies(transaction.inputTxids, item.depth + 1, beef, scheduled, next, hasKnownTxid);
|
|
5312
|
+
}
|
|
5313
|
+
function appendNewDependencies(dependencies, depth, beef, scheduled, next, hasKnownTxid) {
|
|
5314
|
+
for (const txid of dependencies) {
|
|
5315
|
+
if (scheduled.has(txid) || !needsResolution(beef, txid, hasKnownTxid)) continue;
|
|
5316
|
+
scheduled.add(txid);
|
|
5317
|
+
next.push({
|
|
5318
|
+
txid,
|
|
5319
|
+
depth
|
|
5320
|
+
});
|
|
5321
|
+
}
|
|
5322
|
+
}
|
|
5323
|
+
function needsResolution(beef, txid, hasKnownTxid) {
|
|
5324
|
+
const entry = beef.findTxid(txid);
|
|
5325
|
+
return entry == null || entry.isTxidOnly && !hasKnownTxid(txid);
|
|
5326
|
+
}
|
|
5327
|
+
async function mergeMissingFragments(storage, beef, missing, options) {
|
|
5328
|
+
if (missing.length === 0) return;
|
|
5329
|
+
if (options.ignoreServices === true) throw new WERR_INVALID_PARAMETER(`txid ${missing[0].txid}`, `valid transaction on chain ${storage.chain}`);
|
|
5330
|
+
const fragments = await mapWithConcurrency(missing, normalizeConcurrency(options.maxConcurrency), async (item) => await getBeefForTransaction(storage, item.txid, {
|
|
5331
|
+
...options,
|
|
5332
|
+
ignoreStorage: true,
|
|
5333
|
+
mergeToBeef: void 0
|
|
5334
|
+
}));
|
|
5335
|
+
for (const fragment of fragments) beef.mergeBeef(fragment);
|
|
5336
|
+
}
|
|
5161
5337
|
function makeKnownTxidLookup$1(knownTxids) {
|
|
5162
5338
|
let lookups = 0;
|
|
5163
5339
|
let indexed;
|
|
@@ -5171,6 +5347,9 @@ function makeKnownTxidLookup$1(knownTxids) {
|
|
|
5171
5347
|
return knownTxids.includes(txid);
|
|
5172
5348
|
};
|
|
5173
5349
|
}
|
|
5350
|
+
function normalizeConcurrency(value = 8) {
|
|
5351
|
+
return Number.isFinite(value) ? Math.max(1, Math.min(32, Math.floor(value))) : 8;
|
|
5352
|
+
}
|
|
5174
5353
|
async function mapWithConcurrency(values, concurrency, mapper) {
|
|
5175
5354
|
const results = Array.from({ length: values.length }, () => void 0);
|
|
5176
5355
|
let cursor = 0;
|
|
@@ -5299,6 +5478,23 @@ async function createMergedBeefOfTxids(txids, storage) {
|
|
|
5299
5478
|
//#endregion
|
|
5300
5479
|
//#region ../src/storage/methods/processAction.ts
|
|
5301
5480
|
async function processAction$1(storage, auth, args) {
|
|
5481
|
+
if (!storage.telemetry.enabled) return await processActionCore(storage, auth, args);
|
|
5482
|
+
return await storage.telemetry.withSpan("wallet.storage.process_action", {
|
|
5483
|
+
component: "wallet-storage",
|
|
5484
|
+
carrier: args,
|
|
5485
|
+
attributes: {
|
|
5486
|
+
"action.is_new_transaction": args.isNewTx,
|
|
5487
|
+
"action.is_no_send": args.isNoSend,
|
|
5488
|
+
"action.is_delayed": args.isDelayed,
|
|
5489
|
+
"action.send_with_count": args.sendWith.length
|
|
5490
|
+
}
|
|
5491
|
+
}, async (span) => {
|
|
5492
|
+
const result = await processActionCore(storage, auth, args, span);
|
|
5493
|
+
span.end({ attributes: { "action.send_result_count": result.sendWithResults?.length ?? 0 } });
|
|
5494
|
+
return result;
|
|
5495
|
+
});
|
|
5496
|
+
}
|
|
5497
|
+
async function processActionCore(storage, auth, args, parent) {
|
|
5302
5498
|
const logger = args.logger;
|
|
5303
5499
|
logger?.group("storage processAction");
|
|
5304
5500
|
const userId = verifyId(auth.userId);
|
|
@@ -5306,9 +5502,9 @@ async function processAction$1(storage, auth, args) {
|
|
|
5306
5502
|
let req;
|
|
5307
5503
|
const txidsOfReqsToShareWithWorld = [...args.sendWith];
|
|
5308
5504
|
if (args.isNewTx) {
|
|
5309
|
-
const vargs = await validateCommitNewTxToStorageArgs(storage, userId, args);
|
|
5505
|
+
const vargs = await traceProcessStep(storage, "wallet.storage.process_action.validate", parent, async () => await validateCommitNewTxToStorageArgs(storage, userId, args));
|
|
5310
5506
|
logger?.log("validated new tx updates to storage");
|
|
5311
|
-
({req} = await commitNewTxToStorage(storage, userId, vargs));
|
|
5507
|
+
({req} = await traceProcessStep(storage, "wallet.storage.process_action.commit", parent, async () => await commitNewTxToStorage(storage, userId, vargs)));
|
|
5312
5508
|
logger?.log("committed new tx updates to storage ");
|
|
5313
5509
|
if (!req) throw new WERR_INTERNAL();
|
|
5314
5510
|
if (args.isNoSend && !args.isSendWith) logger?.log(`noSend txid ${req.txid}`);
|
|
@@ -5317,12 +5513,19 @@ async function processAction$1(storage, auth, args) {
|
|
|
5317
5513
|
logger?.log(`sending txid ${req.txid}`);
|
|
5318
5514
|
}
|
|
5319
5515
|
}
|
|
5320
|
-
const { swr, ndr } = await shareReqsWithWorld(storage, userId, txidsOfReqsToShareWithWorld, args.isDelayed, void 0, logger);
|
|
5516
|
+
const { swr, ndr } = await traceProcessStep(storage, "wallet.storage.process_action.share", parent, async () => await shareReqsWithWorld(storage, userId, txidsOfReqsToShareWithWorld, args.isDelayed, void 0, logger));
|
|
5321
5517
|
r.sendWithResults = swr;
|
|
5322
5518
|
r.notDelayedResults = ndr;
|
|
5323
5519
|
logger?.groupEnd();
|
|
5324
5520
|
return r;
|
|
5325
5521
|
}
|
|
5522
|
+
async function traceProcessStep(storage, name, parent, callback) {
|
|
5523
|
+
if (parent == null) return await callback();
|
|
5524
|
+
return await storage.telemetry.withSpan(name, {
|
|
5525
|
+
component: "wallet-storage",
|
|
5526
|
+
parent: parent.context
|
|
5527
|
+
}, callback);
|
|
5528
|
+
}
|
|
5326
5529
|
/**
|
|
5327
5530
|
* Verifies that all the txids are known reqs with ready-to-share status.
|
|
5328
5531
|
* Assigns a batch identifier and updates all the provenTxReqs.
|
|
@@ -5493,21 +5696,16 @@ async function validateCommitNewTxToStorageArgs(storage, userId, params) {
|
|
|
5493
5696
|
} }));
|
|
5494
5697
|
if (!transaction.isOutgoing) throw new WERR_INVALID_OPERATION("isOutgoing is not true");
|
|
5495
5698
|
if (transaction.inputBEEF == null) throw new WERR_INVALID_OPERATION();
|
|
5496
|
-
const beef = _bsv_sdk.Beef.fromBinary(asArray(transaction.inputBEEF));
|
|
5497
5699
|
if (transaction.status !== "unsigned" && transaction.status !== "unprocessed") throw new WERR_INVALID_OPERATION(`invalid transaction status ${transaction.status}`);
|
|
5498
5700
|
const transactionId = verifyId(transaction.transactionId);
|
|
5499
|
-
const outputOutputs = await storage.findOutputs({ partial: {
|
|
5701
|
+
const [outputOutputs, commissionRows] = await Promise.all([storage.findOutputs({ partial: {
|
|
5500
5702
|
userId,
|
|
5501
5703
|
transactionId
|
|
5502
|
-
} })
|
|
5503
|
-
const inputOutputs = await storage.findOutputs({ partial: {
|
|
5504
|
-
userId,
|
|
5505
|
-
spentBy: transactionId
|
|
5506
|
-
} });
|
|
5507
|
-
const commission = verifyOneOrNone(await storage.findCommissions({ partial: {
|
|
5704
|
+
} }), storage.commissionSatoshis > 0 ? storage.findCommissions({ partial: {
|
|
5508
5705
|
transactionId,
|
|
5509
5706
|
userId
|
|
5510
|
-
} }));
|
|
5707
|
+
} }) : Promise.resolve([])]);
|
|
5708
|
+
const commission = verifyOneOrNone(commissionRows);
|
|
5511
5709
|
if (storage.commissionSatoshis > 0) {
|
|
5512
5710
|
if (commission == null) throw new WERR_INTERNAL();
|
|
5513
5711
|
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 +5725,7 @@ async function validateCommitNewTxToStorageArgs(storage, userId, params) {
|
|
|
5527
5725
|
txScriptOffsets,
|
|
5528
5726
|
transactionId,
|
|
5529
5727
|
transaction,
|
|
5530
|
-
inputOutputs,
|
|
5531
5728
|
outputOutputs,
|
|
5532
|
-
commission,
|
|
5533
|
-
beef,
|
|
5534
5729
|
req,
|
|
5535
5730
|
outputUpdates: [],
|
|
5536
5731
|
transactionUpdate: {
|
|
@@ -6208,17 +6403,24 @@ async function generateChangeSdkCore(params, allocateChangeInput, releaseChangeI
|
|
|
6208
6403
|
};
|
|
6209
6404
|
const fixedInputs = params.fixedInputs;
|
|
6210
6405
|
const fixedOutputs = params.fixedOutputs;
|
|
6406
|
+
const fixedFunding = fixedInputs.reduce((sum, input) => sum + input.satoshis, 0);
|
|
6407
|
+
let fixedSpending = fixedOutputs.reduce((sum, output) => sum + output.satoshis, 0);
|
|
6408
|
+
const fixedInputSize = fixedInputs.reduce((sum, input) => sum + transactionInputSize(input.unlockingScriptLength), 0);
|
|
6409
|
+
const fixedOutputSize = fixedOutputs.reduce((sum, output) => sum + transactionOutputSize(output.lockingScriptLength), 0);
|
|
6410
|
+
const changeInputSize = transactionInputSize(params.changeUnlockingScriptLength);
|
|
6411
|
+
const changeOutputSize = transactionOutputSize(params.changeLockingScriptLength);
|
|
6412
|
+
let allocatedFunding = 0;
|
|
6211
6413
|
/**
|
|
6212
6414
|
* @returns sum of transaction fixedInputs satoshis and fundingInputs satoshis
|
|
6213
6415
|
*/
|
|
6214
6416
|
const funding = () => {
|
|
6215
|
-
return
|
|
6417
|
+
return fixedFunding + allocatedFunding;
|
|
6216
6418
|
};
|
|
6217
6419
|
/**
|
|
6218
6420
|
* @returns sum of transaction fixedOutputs satoshis
|
|
6219
6421
|
*/
|
|
6220
6422
|
const spending = () => {
|
|
6221
|
-
return
|
|
6423
|
+
return fixedSpending;
|
|
6222
6424
|
};
|
|
6223
6425
|
/**
|
|
6224
6426
|
* @returns sum of transaction changeOutputs satoshis
|
|
@@ -6228,7 +6430,9 @@ async function generateChangeSdkCore(params, allocateChangeInput, releaseChangeI
|
|
|
6228
6430
|
};
|
|
6229
6431
|
const fee = () => funding() - spending() - change();
|
|
6230
6432
|
const size = (addedChangeInputs, addedChangeOutputs) => {
|
|
6231
|
-
|
|
6433
|
+
const inputCount = fixedInputs.length + r.allocatedChangeInputs.length + (addedChangeInputs || 0);
|
|
6434
|
+
const outputCount = fixedOutputs.length + r.changeOutputs.length + (addedChangeOutputs || 0);
|
|
6435
|
+
return 4 + varUintSize(inputCount) + fixedInputSize + (r.allocatedChangeInputs.length + (addedChangeInputs || 0)) * changeInputSize + varUintSize(outputCount) + fixedOutputSize + (r.changeOutputs.length + (addedChangeOutputs || 0)) * changeOutputSize + 4;
|
|
6232
6436
|
};
|
|
6233
6437
|
/**
|
|
6234
6438
|
* @returns the target fee required for the transaction as currently configured under feeModel.
|
|
@@ -6267,7 +6471,10 @@ async function generateChangeSdkCore(params, allocateChangeInput, releaseChangeI
|
|
|
6267
6471
|
const releaseAllocatedChangeInputs = async () => {
|
|
6268
6472
|
while (r.allocatedChangeInputs.length > 0) {
|
|
6269
6473
|
const i = r.allocatedChangeInputs.pop();
|
|
6270
|
-
if (i != null)
|
|
6474
|
+
if (i != null) {
|
|
6475
|
+
allocatedFunding -= i.satoshis;
|
|
6476
|
+
await releaseChangeInput(i.outputId);
|
|
6477
|
+
}
|
|
6271
6478
|
}
|
|
6272
6479
|
feeExcessNow = feeExcess();
|
|
6273
6480
|
};
|
|
@@ -6302,6 +6509,7 @@ async function generateChangeSdkCore(params, allocateChangeInput, releaseChangeI
|
|
|
6302
6509
|
const allocatedChangeInput = await allocateChangeInput(-feeExcess(1, ao) + (ao === 1 ? 2 * params.changeInitialSatoshis : 0) + changeBuffer, exactSatoshis);
|
|
6303
6510
|
if (allocatedChangeInput == null) return false;
|
|
6304
6511
|
r.allocatedChangeInputs.push(allocatedChangeInput);
|
|
6512
|
+
allocatedFunding += allocatedChangeInput.satoshis;
|
|
6305
6513
|
maybeAddChangeOutput(ao);
|
|
6306
6514
|
return true;
|
|
6307
6515
|
};
|
|
@@ -6313,6 +6521,7 @@ async function generateChangeSdkCore(params, allocateChangeInput, releaseChangeI
|
|
|
6313
6521
|
while (r.changeOutputs.length > 0 && feeExcess() < 0) r.changeOutputs.pop();
|
|
6314
6522
|
if (feeExcess() < 0) break;
|
|
6315
6523
|
removeChurnPairs(r.allocatedChangeInputs, r.changeOutputs);
|
|
6524
|
+
allocatedFunding = r.allocatedChangeInputs.reduce((sum, input) => sum + input.satoshis, 0);
|
|
6316
6525
|
}
|
|
6317
6526
|
};
|
|
6318
6527
|
/**
|
|
@@ -6321,7 +6530,9 @@ async function generateChangeSdkCore(params, allocateChangeInput, releaseChangeI
|
|
|
6321
6530
|
await fundTransaction();
|
|
6322
6531
|
if (feeExcess() < 0 && vgcpr.hasMaxPossibleOutput !== void 0) {
|
|
6323
6532
|
if (fixedOutputs[vgcpr.hasMaxPossibleOutput].satoshis !== 0x775f05a073fff) throw new WERR_INTERNAL();
|
|
6324
|
-
|
|
6533
|
+
const adjustment = feeExcess();
|
|
6534
|
+
fixedOutputs[vgcpr.hasMaxPossibleOutput].satoshis += adjustment;
|
|
6535
|
+
fixedSpending += adjustment;
|
|
6325
6536
|
r.maxPossibleSatoshisAdjustment = {
|
|
6326
6537
|
fixedOutputIndex: vgcpr.hasMaxPossibleOutput,
|
|
6327
6538
|
satoshis: fixedOutputs[vgcpr.hasMaxPossibleOutput].satoshis
|
|
@@ -6340,8 +6551,11 @@ async function generateChangeSdkCore(params, allocateChangeInput, releaseChangeI
|
|
|
6340
6551
|
* If needed, seek funding to avoid overspending on fees without a change output to recapture it.
|
|
6341
6552
|
*/
|
|
6342
6553
|
if (r.changeOutputs.length === 0 && feeExcessNow > 0) {
|
|
6554
|
+
const minimumChange = Math.max(dustFloor, params.changeFirstSatoshis);
|
|
6555
|
+
const totalSatoshisNeeded = spending() + feeTarget(0, 1) + minimumChange;
|
|
6556
|
+
const moreSatoshisNeeded = Math.max(1, totalSatoshisNeeded - funding());
|
|
6343
6557
|
await releaseAllocatedChangeInputs();
|
|
6344
|
-
throw new WERR_INSUFFICIENT_FUNDS(
|
|
6558
|
+
throw new WERR_INSUFFICIENT_FUNDS(totalSatoshisNeeded, moreSatoshisNeeded);
|
|
6345
6559
|
}
|
|
6346
6560
|
/**
|
|
6347
6561
|
* Distribute the excess fees across the changeOutputs added.
|
|
@@ -6665,6 +6879,8 @@ function makeChangeLock(out, dctr, args, changeKeys, wallet) {
|
|
|
6665
6879
|
}
|
|
6666
6880
|
//#endregion
|
|
6667
6881
|
//#region ../src/signer/methods/verifyUnlockScripts.ts
|
|
6882
|
+
const postChronicleHeightFallback = 943816;
|
|
6883
|
+
const canonicalP2PKHScope = _bsv_sdk.TransactionSignature.SIGHASH_ALL + _bsv_sdk.TransactionSignature.SIGHASH_FORKID;
|
|
6668
6884
|
const javaScriptOnlyVerifier = {
|
|
6669
6885
|
shouldVerifySpend: () => false,
|
|
6670
6886
|
verifySpend: async () => {
|
|
@@ -6676,10 +6892,11 @@ function invalidUnlockingScript(inputIndex, detail) {
|
|
|
6676
6892
|
return new WERR_INVALID_PARAMETER(`inputs[${inputIndex}].unlockScript`, `valid.${suffix}`);
|
|
6677
6893
|
}
|
|
6678
6894
|
async function verifyOneSpend(pending, verifier) {
|
|
6895
|
+
const [inputIndex, , spend, context] = pending;
|
|
6679
6896
|
try {
|
|
6680
|
-
if (!(verifier === void 0 ?
|
|
6897
|
+
if (!(verifier === void 0 ? spend.validate(context) : await spend.validateWith(verifier, context))) throw invalidUnlockingScript(inputIndex);
|
|
6681
6898
|
} catch (error) {
|
|
6682
|
-
if (error instanceof _bsv_sdk.ScriptEvaluationError) throw invalidUnlockingScript(
|
|
6899
|
+
if (error instanceof _bsv_sdk.ScriptEvaluationError) throw invalidUnlockingScript(inputIndex, error.message);
|
|
6683
6900
|
throw error;
|
|
6684
6901
|
}
|
|
6685
6902
|
}
|
|
@@ -6689,33 +6906,157 @@ async function verifyPendingSpends(pending, verifier) {
|
|
|
6689
6906
|
return;
|
|
6690
6907
|
}
|
|
6691
6908
|
const batched = [];
|
|
6692
|
-
for (const item of pending) if (verifier.shouldVerifySpend?.(item
|
|
6909
|
+
for (const item of pending) if (verifier.shouldVerifySpend?.(item[2], item[3]) !== false) batched.push(item);
|
|
6693
6910
|
else await verifyOneSpend(item, javaScriptOnlyVerifier);
|
|
6694
6911
|
if (batched.length === 0) return;
|
|
6695
6912
|
let verdicts;
|
|
6696
6913
|
try {
|
|
6697
6914
|
verdicts = await verifier.verifySpendsBatch(batched.map((item) => ({
|
|
6698
|
-
spend: item
|
|
6699
|
-
...item
|
|
6915
|
+
spend: item[2],
|
|
6916
|
+
...item[3]
|
|
6700
6917
|
})));
|
|
6701
6918
|
} catch (error) {
|
|
6702
|
-
if (error instanceof _bsv_sdk.ScriptEvaluationError) throw invalidUnlockingScript(batched[0]
|
|
6919
|
+
if (error instanceof _bsv_sdk.ScriptEvaluationError) throw invalidUnlockingScript(batched[0][0], error.message);
|
|
6703
6920
|
throw error;
|
|
6704
6921
|
}
|
|
6705
6922
|
if (verdicts.length !== batched.length) throw new Error("Script verifier returned an invalid batch result count");
|
|
6706
6923
|
verdicts.forEach((valid, index) => {
|
|
6707
|
-
if (!valid) throw invalidUnlockingScript(batched[index]
|
|
6924
|
+
if (!valid) throw invalidUnlockingScript(batched[index][0]);
|
|
6708
6925
|
});
|
|
6709
6926
|
}
|
|
6710
|
-
function
|
|
6711
|
-
const
|
|
6927
|
+
function wholeTransactionVerifier(verifier) {
|
|
6928
|
+
const candidate = verifier;
|
|
6929
|
+
return typeof candidate?.verifyScripts === "function" ? candidate : void 0;
|
|
6930
|
+
}
|
|
6931
|
+
function digestBatchVerifier(verifier) {
|
|
6932
|
+
const candidate = verifier;
|
|
6933
|
+
if (typeof candidate?.verifyDigestBatch !== "function") return void 0;
|
|
6934
|
+
if (candidate.isReady?.() === false) return void 0;
|
|
6935
|
+
if (candidate.supportsCrypto?.("verifyDigestBatch") === false) return void 0;
|
|
6936
|
+
return candidate;
|
|
6937
|
+
}
|
|
6938
|
+
function equalBytes(left, right) {
|
|
6939
|
+
if (left.length !== right.length) return false;
|
|
6940
|
+
for (let index = 0; index < left.length; index++) if (left[index] !== right[index]) return false;
|
|
6941
|
+
return true;
|
|
6942
|
+
}
|
|
6943
|
+
function isCanonicalP2PKHLock(lock) {
|
|
6944
|
+
return lock.length === 25 && lock[0] === 118 && lock[1] === 169 && lock[2] === 20 && lock[23] === 136 && lock[24] === 172;
|
|
6945
|
+
}
|
|
6946
|
+
function parseCanonicalP2PKHUnlock(unlock, lock) {
|
|
6947
|
+
const signatureLength = unlock[0];
|
|
6948
|
+
if (signatureLength == null || signatureLength < 9 || signatureLength > 73 || unlock.length !== 1 + signatureLength + 1 + 33 || unlock[1 + signatureLength] !== 33) return void 0;
|
|
6949
|
+
const checksig = Array.from(unlock.subarray(1, 1 + signatureLength));
|
|
6950
|
+
const publicKey = unlock.subarray(1 + signatureLength + 1);
|
|
6951
|
+
if (publicKey[0] !== 2 && publicKey[0] !== 3 || !equalBytes(_bsv_sdk.Hash.hash160(publicKey), lock.subarray(3, 23))) return void 0;
|
|
6952
|
+
let signature;
|
|
6953
|
+
try {
|
|
6954
|
+
signature = _bsv_sdk.TransactionSignature.fromChecksigFormat(checksig);
|
|
6955
|
+
} catch {
|
|
6956
|
+
return;
|
|
6957
|
+
}
|
|
6958
|
+
if (signature.scope !== canonicalP2PKHScope || !signature.hasLowS() || !equalBytes(signature.toChecksigFormat(), checksig)) return void 0;
|
|
6959
|
+
return [
|
|
6960
|
+
checksig,
|
|
6961
|
+
publicKey,
|
|
6962
|
+
signature
|
|
6963
|
+
];
|
|
6964
|
+
}
|
|
6965
|
+
/**
|
|
6966
|
+
* Recognizes only the exact canonical P2PKH shape generated by this wallet.
|
|
6967
|
+
* Anything else retains the general-purpose script interpreter/backend path.
|
|
6968
|
+
*/
|
|
6969
|
+
function standardP2PKHDigests(tx) {
|
|
6970
|
+
const cache = { hashOutputsSingle: /* @__PURE__ */ new Map() };
|
|
6971
|
+
const items = [];
|
|
6972
|
+
for (let inputIndex = 0; inputIndex < tx.inputs.length; inputIndex++) {
|
|
6973
|
+
const input = tx.inputs[inputIndex];
|
|
6974
|
+
const sourceTransaction = input.sourceTransaction;
|
|
6975
|
+
const sourceTXID = input.sourceTXID;
|
|
6976
|
+
const unlockingScript = input.unlockingScript;
|
|
6977
|
+
if (sourceTransaction == null || sourceTXID == null || unlockingScript == null) return void 0;
|
|
6978
|
+
const sourceOutput = sourceTransaction.outputs[input.sourceOutputIndex];
|
|
6979
|
+
if (sourceOutput == null) return void 0;
|
|
6980
|
+
const lock = sourceOutput.lockingScript.toUint8Array();
|
|
6981
|
+
if (!isCanonicalP2PKHLock(lock)) return void 0;
|
|
6982
|
+
const parsed = parseCanonicalP2PKHUnlock(unlockingScript.toUint8Array(), lock);
|
|
6983
|
+
if (parsed == null) return void 0;
|
|
6984
|
+
const [checksig, publicKey, signature] = parsed;
|
|
6985
|
+
const preimage = _bsv_sdk.TransactionSignature.formatBytes({
|
|
6986
|
+
sourceTXID,
|
|
6987
|
+
sourceOutputIndex: input.sourceOutputIndex,
|
|
6988
|
+
sourceSatoshis: sourceOutput.satoshis ?? 0,
|
|
6989
|
+
transactionVersion: tx.version,
|
|
6990
|
+
otherInputs: [],
|
|
6991
|
+
allInputs: tx.inputs,
|
|
6992
|
+
outputs: tx.outputs,
|
|
6993
|
+
inputIndex,
|
|
6994
|
+
subscript: sourceOutput.lockingScript,
|
|
6995
|
+
inputSequence: input.sequence ?? 4294967295,
|
|
6996
|
+
lockTime: tx.lockTime,
|
|
6997
|
+
scope: signature.scope,
|
|
6998
|
+
cache
|
|
6999
|
+
});
|
|
7000
|
+
items.push({
|
|
7001
|
+
publicKey,
|
|
7002
|
+
digest: Uint8Array.from(_bsv_sdk.Hash.hash256(preimage)),
|
|
7003
|
+
signature: Uint8Array.from(checksig.slice(0, -1))
|
|
7004
|
+
});
|
|
7005
|
+
}
|
|
7006
|
+
return items;
|
|
7007
|
+
}
|
|
7008
|
+
async function verifyStandardP2PKHDigests(pending, verifier) {
|
|
7009
|
+
if (pending.length === 0) return /* @__PURE__ */ new Set();
|
|
7010
|
+
const items = pending.flatMap((entry) => entry[1]);
|
|
7011
|
+
const verdicts = await verifier.verifyDigestBatch(items);
|
|
7012
|
+
if (verdicts.length !== items.length) throw new Error("Script verifier returned an invalid digest batch result count");
|
|
7013
|
+
const verified = /* @__PURE__ */ new Set();
|
|
7014
|
+
let offset = 0;
|
|
7015
|
+
for (const entry of pending) {
|
|
7016
|
+
const end = offset + entry[1].length;
|
|
7017
|
+
if (verdicts.slice(offset, end).every(Boolean)) verified.add(entry[0]);
|
|
7018
|
+
offset = end;
|
|
7019
|
+
}
|
|
7020
|
+
return verified;
|
|
7021
|
+
}
|
|
7022
|
+
function hydrateTransactionSources(txid, transactions) {
|
|
7023
|
+
const tx = transactions.get(txid);
|
|
7024
|
+
if (tx == null) throw new WERR_INVALID_PARAMETER("txid", `contained in beef, txid ${txid}`);
|
|
7025
|
+
for (let inputIndex = 0; inputIndex < tx.inputs.length; inputIndex++) {
|
|
7026
|
+
const input = tx.inputs[inputIndex];
|
|
7027
|
+
if (input.sourceTXID == null) throw new WERR_INVALID_PARAMETER(`inputs[${inputIndex}].sourceTXID`, "valid");
|
|
7028
|
+
if (input.unlockingScript == null) throw new WERR_INVALID_PARAMETER(`inputs[${inputIndex}].unlockingScript`, "valid");
|
|
7029
|
+
input.sourceTransaction = transactions.get(input.sourceTXID);
|
|
7030
|
+
if (input.sourceTransaction == null) return void 0;
|
|
7031
|
+
if (input.sourceTransaction.outputs[input.sourceOutputIndex] == null) throw new WERR_INVALID_PARAMETER(`inputs[${inputIndex}].sourceOutputIndex`, "reference an output in the source transaction");
|
|
7032
|
+
}
|
|
7033
|
+
return tx;
|
|
7034
|
+
}
|
|
7035
|
+
function transactionIndex(txids, beef) {
|
|
7036
|
+
if (txids.length > 0) beef.findTxid(txids[0]);
|
|
7037
|
+
return new Map(beef.txs.map((item) => [item.txid, item.tx]));
|
|
7038
|
+
}
|
|
7039
|
+
async function verifyWholeTransactions(pending, verifier) {
|
|
7040
|
+
if (pending.length === 0) return /* @__PURE__ */ new Set();
|
|
7041
|
+
let verdicts;
|
|
7042
|
+
try {
|
|
7043
|
+
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]));
|
|
7044
|
+
} catch (error) {
|
|
7045
|
+
if (error instanceof _bsv_sdk.ScriptEvaluationError) return /* @__PURE__ */ new Set();
|
|
7046
|
+
throw error;
|
|
7047
|
+
}
|
|
7048
|
+
if (verdicts.length !== pending.length) throw new Error("Script verifier returned an invalid transaction batch result count");
|
|
7049
|
+
return new Set(pending.filter((_, index) => verdicts[index]).map((item) => item[0]));
|
|
7050
|
+
}
|
|
7051
|
+
function collectTransactionSpends(txid, resultIndex, transactions, result, pending) {
|
|
7052
|
+
const tx = transactions.get(txid);
|
|
6712
7053
|
if (tx == null) throw new WERR_INVALID_PARAMETER("txid", `contained in beef, txid ${txid}`);
|
|
6713
7054
|
const sigHashCache = { hashOutputsSingle: /* @__PURE__ */ new Map() };
|
|
6714
7055
|
for (let inputIndex = 0; inputIndex < tx.inputs.length; inputIndex++) {
|
|
6715
7056
|
const input = tx.inputs[inputIndex];
|
|
6716
7057
|
if (input.sourceTXID == null) throw new WERR_INVALID_PARAMETER(`inputs[${inputIndex}].sourceTXID`, "valid");
|
|
6717
7058
|
if (input.unlockingScript == null) throw new WERR_INVALID_PARAMETER(`inputs[${inputIndex}].unlockingScript`, "valid");
|
|
6718
|
-
input.sourceTransaction =
|
|
7059
|
+
input.sourceTransaction = transactions.get(input.sourceTXID);
|
|
6719
7060
|
if (input.sourceTransaction == null) {
|
|
6720
7061
|
result.skippedInputs++;
|
|
6721
7062
|
continue;
|
|
@@ -6727,11 +7068,10 @@ function collectTransactionSpends(txid, resultIndex, beef, result, pending) {
|
|
|
6727
7068
|
consensus: true,
|
|
6728
7069
|
utxoHeight
|
|
6729
7070
|
};
|
|
6730
|
-
pending.push(
|
|
7071
|
+
pending.push([
|
|
6731
7072
|
inputIndex,
|
|
6732
7073
|
resultIndex,
|
|
6733
|
-
|
|
6734
|
-
spend: new _bsv_sdk.Spend({
|
|
7074
|
+
new _bsv_sdk.Spend({
|
|
6735
7075
|
sourceTXID: input.sourceTXID,
|
|
6736
7076
|
sourceOutputIndex: input.sourceOutputIndex,
|
|
6737
7077
|
lockingScript: sourceOutput.lockingScript,
|
|
@@ -6745,9 +7085,52 @@ function collectTransactionSpends(txid, resultIndex, beef, result, pending) {
|
|
|
6745
7085
|
outputs: tx.outputs,
|
|
6746
7086
|
lockTime: tx.lockTime,
|
|
6747
7087
|
sigHashCache
|
|
6748
|
-
})
|
|
6749
|
-
|
|
7088
|
+
}),
|
|
7089
|
+
context
|
|
7090
|
+
]);
|
|
7091
|
+
}
|
|
7092
|
+
}
|
|
7093
|
+
function collectAcceleratedTransactions(txids, transactions, digestVerifier, enabled) {
|
|
7094
|
+
const hydrated = /* @__PURE__ */ new Map();
|
|
7095
|
+
const digests = [];
|
|
7096
|
+
if (!enabled) return [hydrated, digests];
|
|
7097
|
+
for (let resultIndex = 0; resultIndex < txids.length; resultIndex++) {
|
|
7098
|
+
const tx = hydrateTransactionSources(txids[resultIndex], transactions);
|
|
7099
|
+
if (tx == null) continue;
|
|
7100
|
+
hydrated.set(resultIndex, tx);
|
|
7101
|
+
if (digestVerifier === void 0) continue;
|
|
7102
|
+
const items = standardP2PKHDigests(tx);
|
|
7103
|
+
if (items != null) digests.push([resultIndex, items]);
|
|
7104
|
+
}
|
|
7105
|
+
return [hydrated, digests];
|
|
7106
|
+
}
|
|
7107
|
+
function collectWholeTransactionVerifications(hydrated, digestAttempted, verifier) {
|
|
7108
|
+
if (verifier === void 0) return [];
|
|
7109
|
+
const pending = [];
|
|
7110
|
+
for (const [resultIndex, tx] of hydrated) {
|
|
7111
|
+
if (digestAttempted.has(resultIndex)) continue;
|
|
7112
|
+
const params = {
|
|
7113
|
+
tx,
|
|
7114
|
+
blockHeight: postChronicleHeightFallback,
|
|
7115
|
+
consensus: true
|
|
7116
|
+
};
|
|
7117
|
+
if (verifier.shouldVerifyScripts?.(params) === false) continue;
|
|
7118
|
+
pending.push([resultIndex, params]);
|
|
7119
|
+
}
|
|
7120
|
+
return pending;
|
|
7121
|
+
}
|
|
7122
|
+
function collectFallbackSpends(txids, transactions, accelerated, results) {
|
|
7123
|
+
const pending = [];
|
|
7124
|
+
for (let resultIndex = 0; resultIndex < txids.length; resultIndex++) {
|
|
7125
|
+
if (!accelerated.has(resultIndex)) {
|
|
7126
|
+
collectTransactionSpends(txids[resultIndex], resultIndex, transactions, results[resultIndex], pending);
|
|
7127
|
+
continue;
|
|
7128
|
+
}
|
|
7129
|
+
const tx = transactions.get(txids[resultIndex]);
|
|
7130
|
+
if (tx == null) throw new WERR_INVALID_PARAMETER("txid", `contained in beef, txid ${txids[resultIndex]}`);
|
|
7131
|
+
results[resultIndex].verifiedInputs = tx.inputs.length;
|
|
6750
7132
|
}
|
|
7133
|
+
return pending;
|
|
6751
7134
|
}
|
|
6752
7135
|
/**
|
|
6753
7136
|
* Verifies every resolvable input from several transactions in one optional
|
|
@@ -6758,10 +7141,17 @@ async function verifyUnlockScriptsBatch(txids, beef, verifier) {
|
|
|
6758
7141
|
verifiedInputs: 0,
|
|
6759
7142
|
skippedInputs: 0
|
|
6760
7143
|
}));
|
|
6761
|
-
const
|
|
6762
|
-
|
|
7144
|
+
const transactions = transactionIndex(txids, beef);
|
|
7145
|
+
const digestVerifier = digestBatchVerifier(verifier);
|
|
7146
|
+
const wholeVerifier = wholeTransactionVerifier(verifier);
|
|
7147
|
+
const [hydrated, digestPending] = collectAcceleratedTransactions(txids, transactions, digestVerifier, digestVerifier !== void 0 || wholeVerifier !== void 0);
|
|
7148
|
+
const digestAttempted = new Set(digestPending.map((item) => item[0]));
|
|
7149
|
+
const digestVerified = digestVerifier === void 0 ? /* @__PURE__ */ new Set() : await verifyStandardP2PKHDigests(digestPending, digestVerifier);
|
|
7150
|
+
const wholePending = collectWholeTransactionVerifications(hydrated, digestAttempted, wholeVerifier);
|
|
7151
|
+
const wholeVerified = wholeVerifier === void 0 ? /* @__PURE__ */ new Set() : await verifyWholeTransactions(wholePending, wholeVerifier);
|
|
7152
|
+
const pending = collectFallbackSpends(txids, transactions, /* @__PURE__ */ new Set([...digestVerified, ...wholeVerified]), results);
|
|
6763
7153
|
await verifyPendingSpends(pending, verifier);
|
|
6764
|
-
for (const item of pending) results[item
|
|
7154
|
+
for (const item of pending) results[item[1]].verifiedInputs++;
|
|
6765
7155
|
return results;
|
|
6766
7156
|
}
|
|
6767
7157
|
/**
|
|
@@ -6784,21 +7174,56 @@ async function completeSignedTransaction(prior, spends, wallet) {
|
|
|
6784
7174
|
input.unlockingScript = asBsvSdkScript(spend.unlockingScript);
|
|
6785
7175
|
if (spend.sequenceNumber !== void 0) input.sequence = spend.sequenceNumber;
|
|
6786
7176
|
}
|
|
6787
|
-
|
|
6788
|
-
const
|
|
6789
|
-
|
|
6790
|
-
|
|
6791
|
-
|
|
6792
|
-
|
|
6793
|
-
|
|
6794
|
-
|
|
6795
|
-
|
|
6796
|
-
|
|
6797
|
-
const
|
|
6798
|
-
|
|
6799
|
-
|
|
6800
|
-
|
|
6801
|
-
|
|
7177
|
+
const prepareUnlockingTemplates = (keys) => {
|
|
7178
|
+
const counterparties = /* @__PURE__ */ new Map();
|
|
7179
|
+
const counterparty = (publicKey) => {
|
|
7180
|
+
let parsed = counterparties.get(publicKey);
|
|
7181
|
+
if (parsed == null) {
|
|
7182
|
+
parsed = _bsv_sdk.PublicKey.fromString(publicKey);
|
|
7183
|
+
counterparties.set(publicKey, parsed);
|
|
7184
|
+
}
|
|
7185
|
+
return parsed;
|
|
7186
|
+
};
|
|
7187
|
+
const prepared = prior.pdi.map((pdi) => {
|
|
7188
|
+
return {
|
|
7189
|
+
pdi,
|
|
7190
|
+
template: new ScriptTemplateBRC29({
|
|
7191
|
+
derivationPrefix: pdi.derivationPrefix,
|
|
7192
|
+
derivationSuffix: pdi.derivationSuffix,
|
|
7193
|
+
keyDeriver: wallet.keyDeriver
|
|
7194
|
+
}),
|
|
7195
|
+
unlockerPubKey: counterparty(pdi.unlockerPubKey || keys.publicKey)
|
|
7196
|
+
};
|
|
7197
|
+
});
|
|
7198
|
+
const derivations = prepared.map(({ template, unlockerPubKey }) => ({
|
|
7199
|
+
protocolID: brc29ProtocolID,
|
|
7200
|
+
keyID: template.getKeyID(),
|
|
7201
|
+
counterparty: unlockerPubKey
|
|
7202
|
+
}));
|
|
7203
|
+
const derivedPrivateKeys = wallet.keyDeriver.derivePrivateKeys?.(derivations) ?? derivations.map((derivation) => wallet.keyDeriver.derivePrivateKey(derivation.protocolID, derivation.keyID, derivation.counterparty));
|
|
7204
|
+
for (let index = 0; index < prepared.length; index++) {
|
|
7205
|
+
const { pdi, template } = prepared[index];
|
|
7206
|
+
const unlockTemplate = template.unlockWithDerivedPrivateKey(derivedPrivateKeys[index], pdi.sourceSatoshis, asBsvSdkScript(pdi.lockingScript));
|
|
7207
|
+
const input = prior.tx.inputs[pdi.vin];
|
|
7208
|
+
input.unlockingScriptTemplate = unlockTemplate;
|
|
7209
|
+
}
|
|
7210
|
+
};
|
|
7211
|
+
if (wallet.telemetry.enabled && prior.pdi.length > 0) await wallet.telemetry.withSpan("wallet.crypto.prepare_unlocking_templates", {
|
|
7212
|
+
component: "wallet-toolbox",
|
|
7213
|
+
carrier: prior.args,
|
|
7214
|
+
attributes: { "crypto.managed_input_count": prior.pdi.length }
|
|
7215
|
+
}, async (span) => {
|
|
7216
|
+
const keys = await wallet.telemetry.withSpan("wallet.crypto.client_change_key", {
|
|
7217
|
+
component: "wallet-toolbox",
|
|
7218
|
+
parent: span.context
|
|
7219
|
+
}, () => wallet.getClientChangeKeyPair());
|
|
7220
|
+
await wallet.telemetry.withSpan("wallet.crypto.derive_unlocking_templates", {
|
|
7221
|
+
component: "wallet-toolbox",
|
|
7222
|
+
parent: span.context,
|
|
7223
|
+
attributes: { "crypto.managed_input_count": prior.pdi.length }
|
|
7224
|
+
}, () => prepareUnlockingTemplates(keys));
|
|
7225
|
+
});
|
|
7226
|
+
else if (prior.pdi.length > 0) prepareUnlockingTemplates(wallet.getClientChangeKeyPair());
|
|
6802
7227
|
if (wallet.telemetry.enabled) await wallet.telemetry.withSpan("wallet.crypto.transaction_sign", {
|
|
6803
7228
|
component: "wallet-toolbox",
|
|
6804
7229
|
carrier: prior.args,
|
|
@@ -6854,19 +7279,22 @@ async function createActionCore$1(wallet, auth, vargs, parent) {
|
|
|
6854
7279
|
prior.tx = await traceActionStep(wallet, "wallet.create_action.complete_signing", parent, async () => await completeSignedTransaction(prior, {}, wallet));
|
|
6855
7280
|
logger?.log("completed signed transaction");
|
|
6856
7281
|
r.txid = prior.tx.id("hex");
|
|
6857
|
-
const beef =
|
|
6858
|
-
|
|
6859
|
-
|
|
6860
|
-
|
|
6861
|
-
|
|
6862
|
-
|
|
7282
|
+
const beef = await traceActionStep(wallet, "wallet.create_action.assemble_result_beef", parent, () => {
|
|
7283
|
+
const result = new _bsv_sdk.Beef();
|
|
7284
|
+
if (prior.dcr.inputBeef != null) {
|
|
7285
|
+
const inputBeef = prior.dcr.inputBeef instanceof Uint8Array ? _bsv_sdk.Beef.fromBinaryView(prior.dcr.inputBeef) : _bsv_sdk.Beef.fromBinary(prior.dcr.inputBeef);
|
|
7286
|
+
result.mergeBeef(inputBeef);
|
|
7287
|
+
}
|
|
7288
|
+
result.mergeTransaction(prior.tx);
|
|
7289
|
+
return result;
|
|
7290
|
+
});
|
|
6863
7291
|
logger?.log("merged beef");
|
|
6864
7292
|
await traceActionStep(wallet, "wallet.create_action.verify_unlock_scripts", parent, async () => await verifyUnlockScripts(r.txid, beef, wallet.scriptVerifier));
|
|
6865
7293
|
logger?.log("verified unlock scripts");
|
|
6866
7294
|
r.noSendChange = prior.dcr.noSendChangeOutputVouts?.map((vout) => `${r.txid}.${vout}`);
|
|
6867
7295
|
beef.atomicTxid = r.txid;
|
|
6868
7296
|
setResultBeef(r, beef);
|
|
6869
|
-
if (!vargs.options.returnTXIDOnly) r.tx = beef.toUint8ArrayAtomic(r.txid);
|
|
7297
|
+
if (!vargs.options.returnTXIDOnly) r.tx = await traceActionStep(wallet, "wallet.create_action.serialize_result_beef", parent, () => beef.toUint8ArrayAtomic(r.txid));
|
|
6870
7298
|
}
|
|
6871
7299
|
const { sendWithResults, notDelayedResults } = await traceActionStep(wallet, "wallet.create_action.process", parent, async () => await processAction(prior, wallet, auth, vargs));
|
|
6872
7300
|
logger?.log("processed transaction");
|
|
@@ -7484,6 +7912,50 @@ function selectCanonicalChange(outputs, targetSatoshis, exactSatoshis) {
|
|
|
7484
7912
|
if (over != null) return over;
|
|
7485
7913
|
return outputs.filter((output) => output.satoshis < targetSatoshis).sort((a, b) => b.satoshis - a.satoshis || b.outputId - a.outputId)[0];
|
|
7486
7914
|
}
|
|
7915
|
+
/**
|
|
7916
|
+
* Stateful form of the canonical selector for allocating many inputs from one
|
|
7917
|
+
* candidate set. It preserves exact / least-over / largest-under ordering but
|
|
7918
|
+
* sorts once instead of filtering and sorting the full set per input.
|
|
7919
|
+
*/
|
|
7920
|
+
var CanonicalChangeSelector = class {
|
|
7921
|
+
sorted;
|
|
7922
|
+
allocated = /* @__PURE__ */ new Set();
|
|
7923
|
+
constructor(outputs) {
|
|
7924
|
+
this.sorted = [...outputs].sort((a, b) => a.satoshis - b.satoshis || a.outputId - b.outputId);
|
|
7925
|
+
}
|
|
7926
|
+
take(targetSatoshis, exactSatoshis) {
|
|
7927
|
+
if (exactSatoshis !== void 0) for (let index = this.lowerBound(exactSatoshis); index < this.sorted.length; index++) {
|
|
7928
|
+
const output = this.sorted[index];
|
|
7929
|
+
if (output.satoshis !== exactSatoshis) break;
|
|
7930
|
+
if (!this.allocated.has(output.outputId)) return this.allocate(output);
|
|
7931
|
+
}
|
|
7932
|
+
for (let index = this.lowerBound(targetSatoshis); index < this.sorted.length; index++) {
|
|
7933
|
+
const output = this.sorted[index];
|
|
7934
|
+
if (!this.allocated.has(output.outputId)) return this.allocate(output);
|
|
7935
|
+
}
|
|
7936
|
+
for (let index = this.lowerBound(targetSatoshis) - 1; index >= 0; index--) {
|
|
7937
|
+
const output = this.sorted[index];
|
|
7938
|
+
if (!this.allocated.has(output.outputId)) return this.allocate(output);
|
|
7939
|
+
}
|
|
7940
|
+
}
|
|
7941
|
+
release(outputId) {
|
|
7942
|
+
this.allocated.delete(outputId);
|
|
7943
|
+
}
|
|
7944
|
+
allocate(output) {
|
|
7945
|
+
this.allocated.add(output.outputId);
|
|
7946
|
+
return output;
|
|
7947
|
+
}
|
|
7948
|
+
lowerBound(satoshis) {
|
|
7949
|
+
let low = 0;
|
|
7950
|
+
let high = this.sorted.length;
|
|
7951
|
+
while (low < high) {
|
|
7952
|
+
const middle = low + high >>> 1;
|
|
7953
|
+
if (this.sorted[middle].satoshis < satoshis) low = middle + 1;
|
|
7954
|
+
else high = middle;
|
|
7955
|
+
}
|
|
7956
|
+
return low;
|
|
7957
|
+
}
|
|
7958
|
+
};
|
|
7487
7959
|
function repeatableRandom(randomVals) {
|
|
7488
7960
|
const values = [...randomVals ?? []];
|
|
7489
7961
|
return () => {
|
|
@@ -8619,6 +9091,22 @@ var ActionBatchController = class {
|
|
|
8619
9091
|
};
|
|
8620
9092
|
//#endregion
|
|
8621
9093
|
//#region ../src/Wallet.ts
|
|
9094
|
+
function prepareKnownTxidsForCreateAction(wallet, args) {
|
|
9095
|
+
if (!wallet.autoKnownTxids || args.options?.knownTxids != null) return;
|
|
9096
|
+
if (!wallet.telemetry.enabled) {
|
|
9097
|
+
args.options.knownTxids = wallet.getKnownTxids(args.options?.knownTxids);
|
|
9098
|
+
return;
|
|
9099
|
+
}
|
|
9100
|
+
args.options.knownTxids = wallet.telemetry.withSpan("wallet.create_action.prepare_known_txids", {
|
|
9101
|
+
component: "wallet-toolbox",
|
|
9102
|
+
carrier: args,
|
|
9103
|
+
attributes: { "beef.tx_count": wallet.beef.txs.length }
|
|
9104
|
+
}, (span) => {
|
|
9105
|
+
const knownTxids = wallet.getKnownTxids(args.options?.knownTxids);
|
|
9106
|
+
span.end({ attributes: { "beef.known_txid_count": knownTxids.length } });
|
|
9107
|
+
return knownTxids;
|
|
9108
|
+
});
|
|
9109
|
+
}
|
|
8622
9110
|
/**
|
|
8623
9111
|
* Build a {@link DiscoverCertificatesResult} from contact records so {@link Wallet.discoverByIdentityKey}
|
|
8624
9112
|
* and {@link Wallet.discoverByAttributes} can short-circuit on a local contacts hit. The synthetic
|
|
@@ -9092,6 +9580,7 @@ var Wallet = class {
|
|
|
9092
9580
|
if (this.returnTxidOnly) return beef;
|
|
9093
9581
|
const b = parsedBeef ?? _bsv_sdk.Beef.fromBinary(beef);
|
|
9094
9582
|
if (!b.atomicTxid) throw new WERR_INTERNAL();
|
|
9583
|
+
if (!b.txs.some((btx) => btx.isTxidOnly && !knownTxids?.includes(btx.txid))) return beef;
|
|
9095
9584
|
return this.verifyReturnedTxidOnly(b, knownTxids).toBinaryAtomic(b.atomicTxid);
|
|
9096
9585
|
}
|
|
9097
9586
|
verifyReturnedTxidOnlyBEEF(beef) {
|
|
@@ -9123,16 +9612,7 @@ var Wallet = class {
|
|
|
9123
9612
|
_bsv_sdk.Validation.validateOriginator(originator);
|
|
9124
9613
|
args.options ??= {};
|
|
9125
9614
|
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);
|
|
9615
|
+
prepareKnownTxidsForCreateAction(this, args);
|
|
9136
9616
|
const { auth, vargs } = this.validateAuthAndArgs(args, _bsv_sdk.Validation.validateCreateActionArgs, logger);
|
|
9137
9617
|
logger?.log("validated args");
|
|
9138
9618
|
vargs.includeAllSourceTransactions = this.includeAllSourceTransactions;
|
|
@@ -9140,9 +9620,25 @@ var Wallet = class {
|
|
|
9140
9620
|
const r = await createAction$1(this, auth, vargs);
|
|
9141
9621
|
logger?.log("action created");
|
|
9142
9622
|
const resultBeef = getResultBeef(r);
|
|
9143
|
-
if (r.tx != null) this.beef.mergeBeefFromParty(this.storageParty, resultBeef ?? r.tx);
|
|
9144
9623
|
if (r.tx != null) {
|
|
9145
|
-
|
|
9624
|
+
const merge = () => this.beef.mergeBeefFromParty(this.storageParty, resultBeef ?? r.tx);
|
|
9625
|
+
if (this.telemetry.enabled) this.telemetry.withSpan("wallet.create_action.merge_result_beef", {
|
|
9626
|
+
component: "wallet-toolbox",
|
|
9627
|
+
carrier: args,
|
|
9628
|
+
attributes: {
|
|
9629
|
+
"beef.retained_tx_count_before": this.beef.txs.length,
|
|
9630
|
+
"beef.result_byte_count": r.tx.length
|
|
9631
|
+
}
|
|
9632
|
+
}, merge);
|
|
9633
|
+
else merge();
|
|
9634
|
+
}
|
|
9635
|
+
if (r.tx != null) {
|
|
9636
|
+
const verify = () => this.verifyReturnedTxidOnlyAtomicBEEF(r.tx, args.options?.knownTxids, resultBeef);
|
|
9637
|
+
r.tx = this.telemetry.enabled ? this.telemetry.withSpan("wallet.create_action.verify_result_beef", {
|
|
9638
|
+
component: "wallet-toolbox",
|
|
9639
|
+
carrier: args,
|
|
9640
|
+
attributes: { "beef.result_byte_count": r.tx.length }
|
|
9641
|
+
}, verify) : verify();
|
|
9146
9642
|
logger?.log("verify returned AtomicBEEF");
|
|
9147
9643
|
}
|
|
9148
9644
|
if (!vargs.isDelayed) throwIfAnyUnsuccessfulCreateActions(r);
|
|
@@ -9523,7 +10019,7 @@ async function createActionCore(storage, auth, vargs, parent) {
|
|
|
9523
10019
|
});
|
|
9524
10020
|
const feeModel = validateStorageFeeModel(storage.feeModel);
|
|
9525
10021
|
logger?.log(`validated fee model ${JSON.stringify(feeModel)}`);
|
|
9526
|
-
const initialFundingPlan = await prepareFundingPlan(storage,
|
|
10022
|
+
const initialFundingPlan = await prepareFundingPlan(storage, [
|
|
9527
10023
|
userId,
|
|
9528
10024
|
vargs,
|
|
9529
10025
|
xinputs,
|
|
@@ -9532,48 +10028,64 @@ async function createActionCore(storage, auth, vargs, parent) {
|
|
|
9532
10028
|
noSendChangeIn,
|
|
9533
10029
|
feeModel,
|
|
9534
10030
|
parent
|
|
9535
|
-
|
|
10031
|
+
]);
|
|
9536
10032
|
logger?.log(`planned funding from ${initialFundingPlan.availableChangeCount} change inputs`);
|
|
10033
|
+
const allocatedBeefPrefetch = startAllocatedChangeBeefPrefetch(storage, vargs, initialFundingPlan.selected, beef, parent);
|
|
10034
|
+
const storageBeefBytes = storageBeef.toBinary();
|
|
9537
10035
|
let newTx;
|
|
10036
|
+
let newTxCommitted = false;
|
|
9538
10037
|
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
|
-
|
|
10038
|
+
const persisted = await storage.transaction(async (trx) => {
|
|
10039
|
+
const initialSatoshis = fundingPlanSatoshis(initialFundingPlan);
|
|
10040
|
+
newTx = await traceStorageStep(storage, "wallet.storage.create_action.create_record", parent, {
|
|
10041
|
+
"action.label_count": vargs.labels.length,
|
|
10042
|
+
"action.storage_beef_bytes": storageBeefBytes.length
|
|
10043
|
+
}, async (span) => {
|
|
10044
|
+
const transaction = await createNewTxRecord(storage, userId, vargs, storageBeefBytes, initialSatoshis, trx);
|
|
10045
|
+
span?.end({ attributes: { "action.transaction_record_created": true } });
|
|
10046
|
+
return transaction;
|
|
10047
|
+
});
|
|
10048
|
+
logger?.log("created new transaction record");
|
|
10049
|
+
const ctx = {
|
|
10050
|
+
xinputs,
|
|
10051
|
+
xoutputs,
|
|
10052
|
+
changeBasket,
|
|
10053
|
+
noSendChangeIn,
|
|
10054
|
+
feeModel,
|
|
10055
|
+
transactionId: newTx.transactionId
|
|
10056
|
+
};
|
|
10057
|
+
const funded = await fundNewTransactionSdk(storage, userId, vargs, ctx, initialFundingPlan, parent, trx);
|
|
10058
|
+
logger?.log("funded new transaction");
|
|
10059
|
+
if (funded.maxPossibleSatoshisAdjustment != null) {
|
|
10060
|
+
const adjustment = funded.maxPossibleSatoshisAdjustment;
|
|
10061
|
+
if (ctx.xoutputs[adjustment.fixedOutputIndex].satoshis !== 0x775f05a073fff) throw new WERR_INTERNAL();
|
|
10062
|
+
ctx.xoutputs[adjustment.fixedOutputIndex].satoshis = adjustment.satoshis;
|
|
10063
|
+
logger?.log("adjusted change outputs to max possible");
|
|
10064
|
+
}
|
|
10065
|
+
const satoshis = funded.changeOutputs.reduce((sum, output) => sum + output.satoshis, 0) - funded.allocatedChange.reduce((sum, output) => sum + output.satoshis, 0);
|
|
10066
|
+
if (satoshis !== initialSatoshis) {
|
|
10067
|
+
await storage.updateTransaction(newTx.transactionId, { satoshis }, trx);
|
|
10068
|
+
newTx.satoshis = satoshis;
|
|
10069
|
+
}
|
|
10070
|
+
const storedOutputs = await traceStorageStep(storage, "wallet.storage.create_action.persist_outputs", parent, {
|
|
10071
|
+
"action.fixed_output_count": ctx.xoutputs.length,
|
|
10072
|
+
"action.change_output_count": funded.changeOutputs.length
|
|
10073
|
+
}, async (span) => {
|
|
10074
|
+
const result = await createNewOutputs(storage, userId, vargs, ctx, funded.changeOutputs, trx);
|
|
10075
|
+
span?.end({ attributes: { "action.persisted_output_count": result.outputs.length } });
|
|
10076
|
+
return result;
|
|
10077
|
+
});
|
|
10078
|
+
return {
|
|
10079
|
+
...funded,
|
|
10080
|
+
...storedOutputs,
|
|
10081
|
+
ctx
|
|
10082
|
+
};
|
|
9574
10083
|
});
|
|
10084
|
+
newTxCommitted = true;
|
|
10085
|
+
const committedTx = verifyTruthy(newTx);
|
|
10086
|
+
const { allocatedChange, derivationPrefix, outputs, changeVouts, ctx } = persisted;
|
|
9575
10087
|
logger?.log("created new output records");
|
|
9576
|
-
const inputBeef = await mergeAllocatedChangeBeefs(storage, vargs, allocatedChange, beef, parent);
|
|
10088
|
+
const inputBeef = await mergeAllocatedChangeBeefs(storage, vargs, allocatedChange, beef, allocatedBeefPrefetch, parent);
|
|
9577
10089
|
logger?.log("merged allocated change beefs");
|
|
9578
10090
|
const inputs = await traceStorageStep(storage, "wallet.storage.create_action.assemble_inputs", parent, {
|
|
9579
10091
|
"action.fixed_input_count": ctx.xinputs.length,
|
|
@@ -9586,9 +10098,9 @@ async function createActionCore(storage, auth, vargs, parent) {
|
|
|
9586
10098
|
});
|
|
9587
10099
|
logger?.log("created new inputs");
|
|
9588
10100
|
const r = {
|
|
9589
|
-
reference:
|
|
9590
|
-
version:
|
|
9591
|
-
lockTime:
|
|
10101
|
+
reference: committedTx.reference,
|
|
10102
|
+
version: committedTx.version,
|
|
10103
|
+
lockTime: committedTx.lockTime,
|
|
9592
10104
|
inputs,
|
|
9593
10105
|
outputs,
|
|
9594
10106
|
derivationPrefix,
|
|
@@ -9598,9 +10110,15 @@ async function createActionCore(storage, auth, vargs, parent) {
|
|
|
9598
10110
|
logger?.groupEnd();
|
|
9599
10111
|
return r;
|
|
9600
10112
|
} catch (error) {
|
|
10113
|
+
await allocatedBeefPrefetch;
|
|
9601
10114
|
if (newTx?.transactionId != null) try {
|
|
9602
|
-
|
|
9603
|
-
|
|
10115
|
+
if (newTxCommitted) {
|
|
10116
|
+
await storage.updateTransactionStatus("failed", newTx.transactionId);
|
|
10117
|
+
logger?.log(`marked failed createAction transaction ${newTx.transactionId} after construction error`);
|
|
10118
|
+
} else {
|
|
10119
|
+
const failed = await createNewTxRecord(storage, userId, vargs, storageBeefBytes, 0, void 0, "failed");
|
|
10120
|
+
logger?.log(`recorded failed createAction transaction ${failed.transactionId} after rollback`);
|
|
10121
|
+
}
|
|
9604
10122
|
} catch (cleanupError) {
|
|
9605
10123
|
logger?.log(`failed to clean up createAction transaction ${newTx.transactionId}: ${String(cleanupError)}`);
|
|
9606
10124
|
}
|
|
@@ -9750,23 +10268,10 @@ async function getCompetingBeefForReview(storage, txid) {
|
|
|
9750
10268
|
throw e;
|
|
9751
10269
|
}
|
|
9752
10270
|
}
|
|
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
|
-
}
|
|
10271
|
+
/** Build the SDK descriptor for a persisted output. */
|
|
10272
|
+
function describeNewOutput(o, tags, txBaskets) {
|
|
9768
10273
|
return {
|
|
9769
|
-
changeVout,
|
|
10274
|
+
changeVout: o.change && o.purpose === "change" && o.providedBy === "storage" ? o.vout : void 0,
|
|
9770
10275
|
ro: {
|
|
9771
10276
|
vout: verifyInteger(o.vout),
|
|
9772
10277
|
satoshis: _bsv_sdk.Validation.validateSatoshis(o.satoshis, "o.satoshis"),
|
|
@@ -9781,13 +10286,28 @@ async function persistNewOutput(storage, o, tags, txTags, txBaskets) {
|
|
|
9781
10286
|
}
|
|
9782
10287
|
};
|
|
9783
10288
|
}
|
|
9784
|
-
|
|
10289
|
+
/** Insert the output and attach its tags; return the SDK output descriptor. */
|
|
10290
|
+
async function persistNewOutput(storage, o, tags, txTags, txBaskets, trx) {
|
|
10291
|
+
o.outputId = await storage.insertOutput(o, trx);
|
|
10292
|
+
for (const tagName of new Set(tags)) {
|
|
10293
|
+
const tag = txTags[tagName];
|
|
10294
|
+
await storage.insertOutputTagMap({
|
|
10295
|
+
outputId: verifyId(o.outputId),
|
|
10296
|
+
outputTagId: verifyId(tag.outputTagId),
|
|
10297
|
+
created_at: /* @__PURE__ */ new Date(),
|
|
10298
|
+
updated_at: /* @__PURE__ */ new Date(),
|
|
10299
|
+
isDeleted: false
|
|
10300
|
+
}, trx);
|
|
10301
|
+
}
|
|
10302
|
+
return describeNewOutput(o, tags, txBaskets);
|
|
10303
|
+
}
|
|
10304
|
+
async function createNewOutputs(storage, userId, vargs, ctx, changeOutputs, trx) {
|
|
9785
10305
|
const txBaskets = {};
|
|
9786
10306
|
const basketNames = [...new Set(ctx.xoutputs.map((x) => x.basket).filter((v) => !!v))];
|
|
9787
|
-
Object.assign(txBaskets, await storage.findOrInsertOutputBasketsBulk(userId, basketNames));
|
|
10307
|
+
Object.assign(txBaskets, await storage.findOrInsertOutputBasketsBulk(userId, basketNames, trx));
|
|
9788
10308
|
const txTags = {};
|
|
9789
10309
|
const tagNames = [...new Set(ctx.xoutputs.flatMap((x) => x.tags))];
|
|
9790
|
-
Object.assign(txTags, await storage.findOrInsertOutputTagsBulk(userId, tagNames));
|
|
10310
|
+
Object.assign(txTags, await storage.findOrInsertOutputTagsBulk(userId, tagNames, trx));
|
|
9791
10311
|
const newOutputs = [];
|
|
9792
10312
|
for (const xo of ctx.xoutputs) {
|
|
9793
10313
|
const lockingScript = asArray(xo.lockingScript);
|
|
@@ -9803,7 +10323,7 @@ async function createNewOutputs(storage, userId, vargs, ctx, changeOutputs) {
|
|
|
9803
10323
|
created_at: now,
|
|
9804
10324
|
updated_at: now,
|
|
9805
10325
|
commissionId: 0
|
|
9806
|
-
});
|
|
10326
|
+
}, trx);
|
|
9807
10327
|
const o = makeDefaultOutput(userId, ctx.transactionId, xo.satoshis, xo.vout);
|
|
9808
10328
|
o.lockingScript = lockingScript;
|
|
9809
10329
|
o.providedBy = "storage";
|
|
@@ -9837,10 +10357,12 @@ async function createNewOutputs(storage, userId, vargs, ctx, changeOutputs) {
|
|
|
9837
10357
|
});
|
|
9838
10358
|
}
|
|
9839
10359
|
if (vargs.options.randomizeOutputs) randomizeOutputVouts(newOutputs.map((output) => output.o), vargs.randomVals);
|
|
10360
|
+
const untagged = newOutputs.filter((output) => output.tags.length === 0);
|
|
10361
|
+
await storage.insertOutputs(untagged.map((output) => output.o), trx);
|
|
9840
10362
|
const outputs = [];
|
|
9841
10363
|
const changeVouts = [];
|
|
9842
10364
|
for (const { o, tags } of newOutputs) {
|
|
9843
|
-
const { changeVout, ro } = await persistNewOutput(storage, o, tags, txTags, txBaskets);
|
|
10365
|
+
const { changeVout, ro } = tags.length === 0 ? describeNewOutput(o, tags, txBaskets) : await persistNewOutput(storage, o, tags, txTags, txBaskets, trx);
|
|
9844
10366
|
if (changeVout !== void 0) changeVouts.push(changeVout);
|
|
9845
10367
|
outputs.push(ro);
|
|
9846
10368
|
}
|
|
@@ -9849,7 +10371,7 @@ async function createNewOutputs(storage, userId, vargs, ctx, changeOutputs) {
|
|
|
9849
10371
|
changeVouts
|
|
9850
10372
|
};
|
|
9851
10373
|
}
|
|
9852
|
-
async function createNewTxRecord(storage, userId, vargs, storageBeef) {
|
|
10374
|
+
async function createNewTxRecord(storage, userId, vargs, storageBeef, satoshis = 0, trx, status = "unsigned") {
|
|
9853
10375
|
const now = /* @__PURE__ */ new Date();
|
|
9854
10376
|
const newTx = {
|
|
9855
10377
|
created_at: now,
|
|
@@ -9857,9 +10379,9 @@ async function createNewTxRecord(storage, userId, vargs, storageBeef) {
|
|
|
9857
10379
|
transactionId: 0,
|
|
9858
10380
|
version: vargs.version,
|
|
9859
10381
|
lockTime: vargs.lockTime,
|
|
9860
|
-
status
|
|
10382
|
+
status,
|
|
9861
10383
|
reference: randomBytesBase64(12),
|
|
9862
|
-
satoshis
|
|
10384
|
+
satoshis,
|
|
9863
10385
|
userId,
|
|
9864
10386
|
isOutgoing: true,
|
|
9865
10387
|
inputBEEF: storageBeef,
|
|
@@ -9867,12 +10389,12 @@ async function createNewTxRecord(storage, userId, vargs, storageBeef) {
|
|
|
9867
10389
|
txid: void 0,
|
|
9868
10390
|
rawTx: void 0
|
|
9869
10391
|
};
|
|
9870
|
-
newTx.transactionId = await storage.insertTransaction(newTx);
|
|
10392
|
+
newTx.transactionId = await storage.insertTransaction(newTx, trx);
|
|
9871
10393
|
const labelNames = [...new Set(vargs.labels)];
|
|
9872
|
-
const labels = await storage.findOrInsertTxLabelsBulk(userId, labelNames);
|
|
10394
|
+
const labels = await storage.findOrInsertTxLabelsBulk(userId, labelNames, trx);
|
|
9873
10395
|
for (const label of labelNames) {
|
|
9874
10396
|
const txLabel = labels[label];
|
|
9875
|
-
await storage.findOrInsertTxLabelMap(verifyId(newTx.transactionId), verifyId(txLabel.txLabelId));
|
|
10397
|
+
await storage.findOrInsertTxLabelMap(verifyId(newTx.transactionId), verifyId(txLabel.txLabelId), trx);
|
|
9876
10398
|
}
|
|
9877
10399
|
return newTx;
|
|
9878
10400
|
}
|
|
@@ -10068,6 +10590,9 @@ async function validateNoSendChange(storage, userId, vargs, changeBasket) {
|
|
|
10068
10590
|
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
10591
|
return r;
|
|
10070
10592
|
}
|
|
10593
|
+
function fundingPlanSatoshis(plan) {
|
|
10594
|
+
return plan.result.changeOutputs.reduce((sum, output) => sum + output.satoshis, 0) - plan.selected.reduce((sum, output) => sum + output.satoshis, 0);
|
|
10595
|
+
}
|
|
10071
10596
|
var FundingClaimConflict = class extends Error {
|
|
10072
10597
|
conflict;
|
|
10073
10598
|
constructor(conflict) {
|
|
@@ -10103,10 +10628,10 @@ function makeFundingParams(vargs, xinputs, xoutputs, changeBasket, feeModel, ava
|
|
|
10103
10628
|
};
|
|
10104
10629
|
}
|
|
10105
10630
|
async function prepareFundingPlan(storage, context) {
|
|
10106
|
-
const
|
|
10631
|
+
const [userId, vargs, xinputs, xoutputs, changeBasket, noSendChangeIn, feeModel, parent, trx] = context;
|
|
10107
10632
|
const excludeSending = !vargs.isDelayed;
|
|
10108
10633
|
const candidates = await traceStorageStep(storage, "wallet.storage.create_action.funding_candidates", parent, { "funding.exclude_sending": excludeSending }, async (span) => {
|
|
10109
|
-
const outputs = await storage.
|
|
10634
|
+
const outputs = await storage.findAvailableManagedChangeInputCandidates(userId, changeBasket.basketId, excludeSending, trx);
|
|
10110
10635
|
span?.end({ attributes: {
|
|
10111
10636
|
"funding.candidate_count": outputs.length,
|
|
10112
10637
|
"funding.candidate_satoshis": outputs.reduce((sum, output) => sum + output.satoshis, 0)
|
|
@@ -10121,10 +10646,12 @@ async function prepareFundingPlan(storage, context) {
|
|
|
10121
10646
|
"funding.no_send_change_count": noSendChangeIn.length
|
|
10122
10647
|
}, async (span) => {
|
|
10123
10648
|
const allocated = /* @__PURE__ */ new Map();
|
|
10649
|
+
const availableSelector = new CanonicalChangeSelector(available);
|
|
10124
10650
|
const noSend = [...noSendChangeIn];
|
|
10651
|
+
const noSendById = new Map(noSendChangeIn.map((output) => [output.outputId, output]));
|
|
10125
10652
|
const allocate = async (targetSatoshis, exactSatoshis) => {
|
|
10126
10653
|
let output = noSend.pop();
|
|
10127
|
-
output ??=
|
|
10654
|
+
output ??= availableSelector.take(targetSatoshis, exactSatoshis);
|
|
10128
10655
|
if (output == null) return void 0;
|
|
10129
10656
|
allocated.set(output.outputId, output);
|
|
10130
10657
|
return {
|
|
@@ -10133,10 +10660,11 @@ async function prepareFundingPlan(storage, context) {
|
|
|
10133
10660
|
};
|
|
10134
10661
|
};
|
|
10135
10662
|
const release = async (outputId) => {
|
|
10136
|
-
|
|
10137
|
-
if (output == null) return;
|
|
10663
|
+
if (allocated.get(outputId) == null) return;
|
|
10138
10664
|
allocated.delete(outputId);
|
|
10139
|
-
|
|
10665
|
+
availableSelector.release(outputId);
|
|
10666
|
+
const noSendOutput = noSendById.get(outputId);
|
|
10667
|
+
if (noSendOutput != null) noSend.push(noSendOutput);
|
|
10140
10668
|
};
|
|
10141
10669
|
const result = await generateChangeSdk(params, allocate, release, vargs.logger, storage.telemetry);
|
|
10142
10670
|
const selected = result.allocatedChangeInputs.map((input) => verifyTruthy(allocated.get(input.outputId)));
|
|
@@ -10154,7 +10682,8 @@ async function prepareFundingPlan(storage, context) {
|
|
|
10154
10682
|
};
|
|
10155
10683
|
});
|
|
10156
10684
|
}
|
|
10157
|
-
async function claimFundingPlan(storage,
|
|
10685
|
+
async function claimFundingPlan(storage, request) {
|
|
10686
|
+
const [userId, basketId, excludeSending, transactionId, noSendChangeIn, plan, trx] = request;
|
|
10158
10687
|
if (plan.selected.length === 0) return {
|
|
10159
10688
|
outputs: [],
|
|
10160
10689
|
sourceTransactionCount: 0,
|
|
@@ -10164,27 +10693,16 @@ async function claimFundingPlan(storage, userId, basketId, excludeSending, trans
|
|
|
10164
10693
|
const noSendIds = new Set(noSendChangeIn.map((output) => output.outputId));
|
|
10165
10694
|
const statuses = ["completed", "unproven"];
|
|
10166
10695
|
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);
|
|
10696
|
+
const claim = await storage.transaction(async (claimTrx) => {
|
|
10697
|
+
const currentById = await storage.findFundingOutputsForUpdate(userId, plan.selected.map((output) => output.outputId), statuses, claimTrx);
|
|
10698
|
+
const transactionIds = [...new Set(Object.values(currentById).map((output) => output.transactionId))];
|
|
10179
10699
|
const claimed = [];
|
|
10180
10700
|
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" };
|
|
10701
|
+
const current = currentById[planned.outputId];
|
|
10702
|
+
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
10703
|
claimed.push(current);
|
|
10186
10704
|
}
|
|
10187
|
-
if (await storage.markChangeInputsSpent(claimed.map((output) => output.outputId), transactionId,
|
|
10705
|
+
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
10706
|
for (const output of claimed) {
|
|
10189
10707
|
output.spendable = false;
|
|
10190
10708
|
output.spentBy = transactionId;
|
|
@@ -10193,19 +10711,19 @@ async function claimFundingPlan(storage, userId, basketId, excludeSending, trans
|
|
|
10193
10711
|
outputs: claimed,
|
|
10194
10712
|
sourceTransactionCount: transactionIds.length
|
|
10195
10713
|
};
|
|
10196
|
-
}).catch((error) => {
|
|
10714
|
+
}, trx).catch((error) => {
|
|
10197
10715
|
if (error instanceof FundingClaimConflict) return { conflict: error.conflict };
|
|
10198
10716
|
throw error;
|
|
10199
10717
|
});
|
|
10200
10718
|
if (claim.outputs == null) return claim;
|
|
10201
|
-
const hydration = await hydrateFundingInputScripts(storage, claim.outputs);
|
|
10719
|
+
const hydration = await hydrateFundingInputScripts(storage, claim.outputs, trx);
|
|
10202
10720
|
return {
|
|
10203
10721
|
outputs: claim.outputs,
|
|
10204
10722
|
sourceTransactionCount: claim.sourceTransactionCount,
|
|
10205
10723
|
...hydration
|
|
10206
10724
|
};
|
|
10207
10725
|
}
|
|
10208
|
-
async function hydrateFundingInputScripts(storage, outputs) {
|
|
10726
|
+
async function hydrateFundingInputScripts(storage, outputs, trx) {
|
|
10209
10727
|
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
10728
|
if (missing.length === 0) return {
|
|
10211
10729
|
hydratedScriptCount: 0,
|
|
@@ -10224,12 +10742,12 @@ async function hydrateFundingInputScripts(storage, outputs) {
|
|
|
10224
10742
|
while (cursor < groups.length) {
|
|
10225
10743
|
const [txid, group] = groups[cursor++];
|
|
10226
10744
|
if (group.length === 1) {
|
|
10227
|
-
await storage.validateOutputScript(group[0]);
|
|
10745
|
+
await storage.validateOutputScript(group[0], trx);
|
|
10228
10746
|
continue;
|
|
10229
10747
|
}
|
|
10230
|
-
const rawTx = await storage.getRawTxOfKnownValidTransaction(txid);
|
|
10748
|
+
const rawTx = await storage.getRawTxOfKnownValidTransaction(txid, void 0, void 0, trx);
|
|
10231
10749
|
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);
|
|
10750
|
+
else for (const output of group) await storage.validateOutputScript(output, trx);
|
|
10233
10751
|
}
|
|
10234
10752
|
}));
|
|
10235
10753
|
return {
|
|
@@ -10237,13 +10755,21 @@ async function hydrateFundingInputScripts(storage, outputs) {
|
|
|
10237
10755
|
scriptSourceTransactionCount: groups.length
|
|
10238
10756
|
};
|
|
10239
10757
|
}
|
|
10240
|
-
async function fundNewTransactionSdk(storage, userId, vargs, ctx, initialPlan, parent) {
|
|
10758
|
+
async function fundNewTransactionSdk(storage, userId, vargs, ctx, initialPlan, parent, trx) {
|
|
10241
10759
|
let plan = initialPlan;
|
|
10242
10760
|
let allocatedChange;
|
|
10243
10761
|
let retryCount = 0;
|
|
10244
10762
|
await traceStorageStep(storage, "wallet.storage.create_action.funding_claim", parent, { "funding.planned_input_count": initialPlan.selected.length }, async (span) => {
|
|
10245
10763
|
for (let attempt = 0; attempt < 3; attempt++) {
|
|
10246
|
-
const claim = await claimFundingPlan(storage,
|
|
10764
|
+
const claim = await claimFundingPlan(storage, [
|
|
10765
|
+
userId,
|
|
10766
|
+
ctx.changeBasket.basketId,
|
|
10767
|
+
!vargs.isDelayed,
|
|
10768
|
+
ctx.transactionId,
|
|
10769
|
+
ctx.noSendChangeIn,
|
|
10770
|
+
plan,
|
|
10771
|
+
trx
|
|
10772
|
+
]);
|
|
10247
10773
|
if (claim.outputs != null) {
|
|
10248
10774
|
allocatedChange = claim.outputs;
|
|
10249
10775
|
span?.end({ attributes: {
|
|
@@ -10256,16 +10782,17 @@ async function fundNewTransactionSdk(storage, userId, vargs, ctx, initialPlan, p
|
|
|
10256
10782
|
}
|
|
10257
10783
|
if (claim.conflict === "noSendChange") throw new WERR_INVALID_PARAMETER("noSendChange", "outputs that remain spendable during action planning");
|
|
10258
10784
|
retryCount++;
|
|
10259
|
-
plan = await prepareFundingPlan(storage,
|
|
10785
|
+
plan = await prepareFundingPlan(storage, [
|
|
10260
10786
|
userId,
|
|
10261
10787
|
vargs,
|
|
10262
|
-
|
|
10263
|
-
|
|
10264
|
-
|
|
10265
|
-
|
|
10266
|
-
|
|
10267
|
-
parent
|
|
10268
|
-
|
|
10788
|
+
ctx.xinputs,
|
|
10789
|
+
ctx.xoutputs,
|
|
10790
|
+
ctx.changeBasket,
|
|
10791
|
+
ctx.noSendChangeIn,
|
|
10792
|
+
ctx.feeModel,
|
|
10793
|
+
parent,
|
|
10794
|
+
trx
|
|
10795
|
+
]);
|
|
10269
10796
|
}
|
|
10270
10797
|
throw new WERR_INVALID_OPERATION("wallet funding changed repeatedly during action planning; retry createAction");
|
|
10271
10798
|
});
|
|
@@ -10351,7 +10878,56 @@ function makeKnownTxidLookup(knownTxids) {
|
|
|
10351
10878
|
return knownTxids.includes(txid);
|
|
10352
10879
|
};
|
|
10353
10880
|
}
|
|
10354
|
-
|
|
10881
|
+
function missingAllocatedChangeTxids(allocatedChange, beef, knownTxids) {
|
|
10882
|
+
const hasKnownTxid = makeKnownTxidLookup(knownTxids);
|
|
10883
|
+
return Array.from(new Set(allocatedChange.map((output) => verifyTruthy(output.txid)).filter((txid) => beef.findTxid(txid) == null && !hasKnownTxid(txid))));
|
|
10884
|
+
}
|
|
10885
|
+
function startAllocatedChangeBeefPrefetch(storage, vargs, allocatedChange, beef, parent) {
|
|
10886
|
+
if (vargs.options.returnTXIDOnly) return Promise.resolve({
|
|
10887
|
+
sourceCount: 0,
|
|
10888
|
+
txids: []
|
|
10889
|
+
});
|
|
10890
|
+
const knownTxids = vargs.options.knownTxids ?? [];
|
|
10891
|
+
const missing = missingAllocatedChangeTxids(allocatedChange, beef, knownTxids);
|
|
10892
|
+
if (missing.length === 0) return Promise.resolve({
|
|
10893
|
+
sourceCount: 0,
|
|
10894
|
+
txids: []
|
|
10895
|
+
});
|
|
10896
|
+
const options = {
|
|
10897
|
+
trustSelf: void 0,
|
|
10898
|
+
knownTxids,
|
|
10899
|
+
ignoreStorage: false,
|
|
10900
|
+
ignoreServices: true,
|
|
10901
|
+
ignoreNewProven: false,
|
|
10902
|
+
minProofLevel: void 0
|
|
10903
|
+
};
|
|
10904
|
+
return traceStorageStep(storage, "wallet.storage.create_action.beef_prefetch", parent, {
|
|
10905
|
+
"beef.planned_source_count": allocatedChange.length,
|
|
10906
|
+
"beef.missing_source_count": missing.length,
|
|
10907
|
+
"beef.storage_batch_count": missing.length === 0 ? 0 : 1
|
|
10908
|
+
}, async (span) => {
|
|
10909
|
+
const fetched = await storage.getBeefForTransactions(missing, options);
|
|
10910
|
+
span?.end({ attributes: {
|
|
10911
|
+
"beef.fetched_tx_count": fetched.txs.length,
|
|
10912
|
+
"beef.fetched_bump_count": fetched.bumps.length
|
|
10913
|
+
} });
|
|
10914
|
+
return fetched;
|
|
10915
|
+
}).then((prefetched) => ({
|
|
10916
|
+
beef: prefetched,
|
|
10917
|
+
sourceCount: missing.length,
|
|
10918
|
+
txids: missing
|
|
10919
|
+
}), (error) => ({
|
|
10920
|
+
error,
|
|
10921
|
+
sourceCount: missing.length,
|
|
10922
|
+
txids: missing
|
|
10923
|
+
}));
|
|
10924
|
+
}
|
|
10925
|
+
function sameTxids(left, right) {
|
|
10926
|
+
if (left.length !== right.length) return false;
|
|
10927
|
+
const expected = new Set(left);
|
|
10928
|
+
return right.every((txid) => expected.has(txid));
|
|
10929
|
+
}
|
|
10930
|
+
async function mergeAllocatedChangeBeefs(storage, vargs, allocatedChange, beef, prefetch, parent) {
|
|
10355
10931
|
const options = {
|
|
10356
10932
|
trustSelf: void 0,
|
|
10357
10933
|
knownTxids: vargs.options.knownTxids,
|
|
@@ -10363,37 +10939,37 @@ async function mergeAllocatedChangeBeefs(storage, vargs, allocatedChange, beef,
|
|
|
10363
10939
|
};
|
|
10364
10940
|
if (vargs.options.returnTXIDOnly) return void 0;
|
|
10365
10941
|
const knownTxids = vargs.options.knownTxids ?? [];
|
|
10366
|
-
const
|
|
10367
|
-
const
|
|
10368
|
-
|
|
10369
|
-
|
|
10370
|
-
|
|
10942
|
+
const requiredBeforePrefetch = missingAllocatedChangeTxids(allocatedChange, beef, knownTxids);
|
|
10943
|
+
const prefetched = await traceStorageStep(storage, "wallet.storage.create_action.beef_prefetch_join", parent, { "beef.prefetch_source_count": 0 }, async (span) => {
|
|
10944
|
+
const result = await prefetch;
|
|
10945
|
+
span?.end({ attributes: { "beef.prefetch_source_count": result.sourceCount } });
|
|
10946
|
+
return result;
|
|
10947
|
+
});
|
|
10948
|
+
const usePrefetch = sameTxids(prefetched.txids, requiredBeforePrefetch);
|
|
10949
|
+
if (usePrefetch && prefetched.error != null) throw prefetched.error;
|
|
10950
|
+
if (usePrefetch && prefetched.beef != null) beef.mergeBeef(prefetched.beef);
|
|
10951
|
+
const missing = missingAllocatedChangeTxids(allocatedChange, beef, knownTxids);
|
|
10952
|
+
let fetched;
|
|
10371
10953
|
await traceStorageStep(storage, "wallet.storage.create_action.beef_fetch", parent, {
|
|
10372
10954
|
"beef.allocated_change_count": allocatedChange.length,
|
|
10373
10955
|
"beef.distinct_source_count": new Set(allocatedChange.map((output) => output.txid)).size,
|
|
10374
10956
|
"beef.known_txid_count": knownTxids.length,
|
|
10375
10957
|
"beef.missing_source_count": missing.length,
|
|
10376
|
-
"beef.fetch_concurrency":
|
|
10958
|
+
"beef.fetch_concurrency": 1,
|
|
10959
|
+
"beef.prefetch_reused": usePrefetch
|
|
10377
10960
|
}, async (span) => {
|
|
10378
|
-
|
|
10379
|
-
|
|
10380
|
-
|
|
10381
|
-
|
|
10382
|
-
...options,
|
|
10383
|
-
mergeToBeef: void 0
|
|
10384
|
-
});
|
|
10385
|
-
}
|
|
10386
|
-
}));
|
|
10961
|
+
if (missing.length > 0) fetched = await storage.getBeefForTransactions(missing, {
|
|
10962
|
+
...options,
|
|
10963
|
+
mergeToBeef: void 0
|
|
10964
|
+
});
|
|
10387
10965
|
span?.end({ attributes: {
|
|
10388
|
-
"beef.fetched_tx_count": fetched
|
|
10389
|
-
"beef.fetched_bump_count": fetched
|
|
10966
|
+
"beef.fetched_tx_count": fetched?.txs.length ?? 0,
|
|
10967
|
+
"beef.fetched_bump_count": fetched?.bumps.length ?? 0,
|
|
10968
|
+
"beef.storage_batch_count": missing.length === 0 ? 0 : 1
|
|
10390
10969
|
} });
|
|
10391
10970
|
});
|
|
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
|
-
}
|
|
10971
|
+
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) => {
|
|
10972
|
+
if (fetched != null) beef.mergeBeef(fetched);
|
|
10397
10973
|
span?.end({ attributes: {
|
|
10398
10974
|
"beef.merged_tx_count": beef.txs.length,
|
|
10399
10975
|
"beef.merged_bump_count": beef.bumps.length
|
|
@@ -13675,10 +14251,28 @@ var StorageProvider = class StorageProvider extends StorageReaderWriter {
|
|
|
13675
14251
|
}
|
|
13676
14252
|
return updated;
|
|
13677
14253
|
}
|
|
14254
|
+
/**
|
|
14255
|
+
* Insert outputs that do not need their generated ids returned to the
|
|
14256
|
+
* caller. Engines with a multi-row insert override this common-path helper;
|
|
14257
|
+
* the fallback preserves existing storage implementations unchanged.
|
|
14258
|
+
*/
|
|
14259
|
+
async insertOutputs(outputs, trx) {
|
|
14260
|
+
for (const output of outputs) await this.insertOutput(output, trx);
|
|
14261
|
+
}
|
|
13678
14262
|
/** Return unreserved wallet-managed outputs eligible for automatic funding. */
|
|
13679
14263
|
async findAvailableManagedChangeInputs(userId, basketId, excludeSending, trx) {
|
|
13680
14264
|
return await availableManagedChange(this, userId, basketId, excludeSending, trx);
|
|
13681
14265
|
}
|
|
14266
|
+
/** Read only the fields needed by the in-memory funding planner. */
|
|
14267
|
+
async findAvailableManagedChangeInputCandidates(userId, basketId, excludeSending, trx) {
|
|
14268
|
+
return (await this.findAvailableManagedChangeInputs(userId, basketId, excludeSending, trx)).map(({ outputId, transactionId, satoshis, txid, vout }) => ({
|
|
14269
|
+
outputId,
|
|
14270
|
+
transactionId,
|
|
14271
|
+
satoshis,
|
|
14272
|
+
txid,
|
|
14273
|
+
vout
|
|
14274
|
+
}));
|
|
14275
|
+
}
|
|
13682
14276
|
/** Read the current status of a set of source transactions without loading raw transaction bytes. */
|
|
13683
14277
|
async findTransactionStatusesByIds(userId, transactionIds, trx) {
|
|
13684
14278
|
const statuses = /* @__PURE__ */ new Map();
|
|
@@ -13688,6 +14282,36 @@ var StorageProvider = class StorageProvider extends StorageReaderWriter {
|
|
|
13688
14282
|
}
|
|
13689
14283
|
return statuses;
|
|
13690
14284
|
}
|
|
14285
|
+
/**
|
|
14286
|
+
* Lock and return the selected funding rows whose source transaction and
|
|
14287
|
+
* action-batch reservation state still permit allocation.
|
|
14288
|
+
*/
|
|
14289
|
+
async findFundingOutputsForUpdate(userId, outputIds, statuses, trx) {
|
|
14290
|
+
const rows = await this.findOutputsByIds(outputIds, trx);
|
|
14291
|
+
const reserved = new Set(await this.findReservedActionBatchOutputIds(outputIds, trx));
|
|
14292
|
+
const transactionIds = [...new Set(Object.values(rows).map((output) => output.transactionId))];
|
|
14293
|
+
const transactionStatuses = await this.findTransactionStatusesByIds(userId, transactionIds, trx);
|
|
14294
|
+
const eligible = {};
|
|
14295
|
+
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;
|
|
14296
|
+
return eligible;
|
|
14297
|
+
}
|
|
14298
|
+
/**
|
|
14299
|
+
* Resolve several transaction proofs in one storage operation when the
|
|
14300
|
+
* backend supports it. The default preserves compatibility for custom
|
|
14301
|
+
* providers; SQL and IndexedDB providers override this hot path.
|
|
14302
|
+
*/
|
|
14303
|
+
async getProvenOrRawTxs(txids, trx) {
|
|
14304
|
+
const results = /* @__PURE__ */ new Map();
|
|
14305
|
+
const unique = [...new Set(txids)];
|
|
14306
|
+
let cursor = 0;
|
|
14307
|
+
await Promise.all(Array.from({ length: Math.min(8, unique.length) }, async () => {
|
|
14308
|
+
while (cursor < unique.length) {
|
|
14309
|
+
const txid = unique[cursor++];
|
|
14310
|
+
results.set(txid, await this.getProvenOrRawTx(txid, trx));
|
|
14311
|
+
}
|
|
14312
|
+
}));
|
|
14313
|
+
return results;
|
|
14314
|
+
}
|
|
13691
14315
|
async insertActionBatch(_batch, _trx) {
|
|
13692
14316
|
throw new WERR_NOT_IMPLEMENTED();
|
|
13693
14317
|
}
|
|
@@ -13739,6 +14363,10 @@ var StorageProvider = class StorageProvider extends StorageReaderWriter {
|
|
|
13739
14363
|
supportsActionBatchPersistence() {
|
|
13740
14364
|
return false;
|
|
13741
14365
|
}
|
|
14366
|
+
/** Custom providers may require physical expiry cleanup before reservations are queried. */
|
|
14367
|
+
requiresActionBatchCleanupBeforeCreateAction() {
|
|
14368
|
+
return true;
|
|
14369
|
+
}
|
|
13742
14370
|
async beginActionBatch(auth, args) {
|
|
13743
14371
|
if (!this.supportsActionBatchPersistence()) throw new WERR_NOT_IMPLEMENTED("actionBatch capability is not available");
|
|
13744
14372
|
return await beginActionBatch(this, auth, args);
|
|
@@ -14127,7 +14755,7 @@ var StorageProvider = class StorageProvider extends StorageReaderWriter {
|
|
|
14127
14755
|
}
|
|
14128
14756
|
async createAction(auth, args) {
|
|
14129
14757
|
if (auth.userId == null) throw new WERR_UNAUTHORIZED();
|
|
14130
|
-
if (this.supportsActionBatchPersistence()) await cleanupExpiredActionBatches(this);
|
|
14758
|
+
if (this.supportsActionBatchPersistence() && this.requiresActionBatchCleanupBeforeCreateAction()) await cleanupExpiredActionBatches(this);
|
|
14131
14759
|
return await createAction(this, auth, args);
|
|
14132
14760
|
}
|
|
14133
14761
|
async processAction(auth, args) {
|
|
@@ -14217,6 +14845,9 @@ var StorageProvider = class StorageProvider extends StorageReaderWriter {
|
|
|
14217
14845
|
async getBeefForTransaction(txid, options) {
|
|
14218
14846
|
return await getBeefForTransaction(this, txid, options);
|
|
14219
14847
|
}
|
|
14848
|
+
async getBeefForTransactions(txids, options) {
|
|
14849
|
+
return await getBeefForTransactions(this, txids, options);
|
|
14850
|
+
}
|
|
14220
14851
|
async findMonitorEventById(id, trx) {
|
|
14221
14852
|
return verifyOneOrNone(await this.findMonitorEvents({
|
|
14222
14853
|
partial: { id },
|
|
@@ -15175,6 +15806,9 @@ var StorageIdb = class extends StorageProvider {
|
|
|
15175
15806
|
supportsActionBatchPersistence() {
|
|
15176
15807
|
return true;
|
|
15177
15808
|
}
|
|
15809
|
+
requiresActionBatchCleanupBeforeCreateAction() {
|
|
15810
|
+
return false;
|
|
15811
|
+
}
|
|
15178
15812
|
/**
|
|
15179
15813
|
* This method must be called at least once before any other method accesses the database,
|
|
15180
15814
|
* and each time the schema may have updated.
|
|
@@ -15354,6 +15988,50 @@ var StorageIdb = class extends StorageProvider {
|
|
|
15354
15988
|
}
|
|
15355
15989
|
return r;
|
|
15356
15990
|
}
|
|
15991
|
+
async getProvenOrRawTxs(txids, trx) {
|
|
15992
|
+
const results = /* @__PURE__ */ new Map();
|
|
15993
|
+
const unique = [...new Set(txids)];
|
|
15994
|
+
if (unique.length === 0) return results;
|
|
15995
|
+
const dbTrx = this.toDbTrx(["proven_txs", "proven_tx_reqs"], "readonly", trx);
|
|
15996
|
+
const provenIndex = dbTrx.objectStore("proven_txs").index("txid");
|
|
15997
|
+
const requestIndex = dbTrx.objectStore("proven_tx_reqs").index("txid");
|
|
15998
|
+
const usableStatuses = /* @__PURE__ */ new Set([
|
|
15999
|
+
"unsent",
|
|
16000
|
+
"unmined",
|
|
16001
|
+
"unconfirmed",
|
|
16002
|
+
"sending",
|
|
16003
|
+
"nosend",
|
|
16004
|
+
"completed"
|
|
16005
|
+
]);
|
|
16006
|
+
await Promise.all(unique.map(async (txid) => {
|
|
16007
|
+
const proven = await provenIndex.get(txid);
|
|
16008
|
+
if (proven != null) {
|
|
16009
|
+
results.set(txid, {
|
|
16010
|
+
proven: this.validateEntity(proven),
|
|
16011
|
+
rawTx: void 0,
|
|
16012
|
+
inputBEEF: void 0
|
|
16013
|
+
});
|
|
16014
|
+
return;
|
|
16015
|
+
}
|
|
16016
|
+
const request = await requestIndex.get(txid);
|
|
16017
|
+
if (request != null && usableStatuses.has(request.status)) {
|
|
16018
|
+
const validated = this.validateEntity(request);
|
|
16019
|
+
results.set(txid, {
|
|
16020
|
+
proven: void 0,
|
|
16021
|
+
rawTx: Array.from(validated.rawTx),
|
|
16022
|
+
inputBEEF: validated.inputBEEF == null ? void 0 : Array.from(validated.inputBEEF)
|
|
16023
|
+
});
|
|
16024
|
+
return;
|
|
16025
|
+
}
|
|
16026
|
+
results.set(txid, {
|
|
16027
|
+
proven: void 0,
|
|
16028
|
+
rawTx: void 0,
|
|
16029
|
+
inputBEEF: void 0
|
|
16030
|
+
});
|
|
16031
|
+
}));
|
|
16032
|
+
if (trx == null) await dbTrx.done;
|
|
16033
|
+
return results;
|
|
16034
|
+
}
|
|
15357
16035
|
async getRawTxOfKnownValidTransaction(txid, offset, length, trx) {
|
|
15358
16036
|
if (txid == null || txid === "") return void 0;
|
|
15359
16037
|
if (!this.isAvailable()) await this.makeAvailable();
|
|
@@ -15615,6 +16293,7 @@ var StorageIdb = class extends StorageProvider {
|
|
|
15615
16293
|
else cursor = await store.openCursor(null, direction);
|
|
15616
16294
|
await scanCursor(cursor, args.since, args.paged?.offset ?? 0, args.paged?.limit, async (r) => {
|
|
15617
16295
|
if (!matchesProvenTxPartial(r, args.partial)) return false;
|
|
16296
|
+
if (args.txids != null && args.txids.length > 0 && !args.txids.includes(r.txid)) return false;
|
|
15618
16297
|
if (userId !== void 0) {
|
|
15619
16298
|
if (await this.countTransactions({
|
|
15620
16299
|
partial: {
|
|
@@ -16059,11 +16738,18 @@ var StorageIdb = class extends StorageProvider {
|
|
|
16059
16738
|
return rows.map((r) => r.outputId);
|
|
16060
16739
|
}
|
|
16061
16740
|
async findReservedActionBatchOutputIds(outputIds, trx) {
|
|
16062
|
-
const tx = this.toDbTrx(["action_batch_outputs"], "readonly", trx);
|
|
16741
|
+
const tx = this.toDbTrx(["action_batch_outputs", "action_batches"], "readonly", trx);
|
|
16063
16742
|
const store = tx.objectStore("action_batch_outputs");
|
|
16743
|
+
const batchStore = tx.objectStore("action_batches");
|
|
16064
16744
|
if (store.get == null) throw new WERR_INTERNAL("IndexedDB action_batch_outputs store does not support get");
|
|
16065
16745
|
const reserved = [];
|
|
16066
|
-
|
|
16746
|
+
const now = Date.now();
|
|
16747
|
+
for (const outputId of outputIds) {
|
|
16748
|
+
const reservation = await store.get(outputId);
|
|
16749
|
+
if (reservation == null) continue;
|
|
16750
|
+
const batch = await batchStore.get(reservation.actionBatchId);
|
|
16751
|
+
if (batch != null && (batch.status === "active" || batch.status === "prepared") && batch.expiresAt.getTime() > now && batch.hardExpiresAt.getTime() > now) reserved.push(outputId);
|
|
16752
|
+
}
|
|
16067
16753
|
if (trx == null) await tx.done;
|
|
16068
16754
|
return reserved;
|
|
16069
16755
|
}
|
|
@@ -27993,7 +28679,7 @@ function isValidProfile(value) {
|
|
|
27993
28679
|
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
28680
|
}
|
|
27995
28681
|
/**
|
|
27996
|
-
* Raised when UMP
|
|
28682
|
+
* Raised when a UMP lookup yields neither a verified token nor a clean empty response.
|
|
27997
28683
|
*
|
|
27998
28684
|
* Callers must offer retry/recovery rather than treating this error as a new
|
|
27999
28685
|
* account. Diagnostics contain counts only and never hashes, keys, or tokens.
|
|
@@ -28091,49 +28777,119 @@ var OverlayUMPTokenInteractor = class {
|
|
|
28091
28777
|
throw new UMPTokenLookupError("lookup-unavailable", diagnostics, { cause: error });
|
|
28092
28778
|
}
|
|
28093
28779
|
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
28780
|
const tokens = this.parseLookupAnswers(resolution.answer);
|
|
28114
28781
|
const expectedHash = question.query[lookupKind === "presentation" ? "presentationHash" : "recoveryHash"].toLowerCase();
|
|
28115
|
-
|
|
28116
|
-
|
|
28117
|
-
|
|
28118
|
-
|
|
28119
|
-
|
|
28782
|
+
const matchingTokens = tokens.filter((token) => _bsv_sdk.Utils.toHex(lookupKind === "presentation" ? token.presentationHash : token.recoveryHash).toLowerCase() === expectedHash);
|
|
28783
|
+
if (matchingTokens.length > 1) {
|
|
28784
|
+
const newest = this.resolveNewestToken(matchingTokens, resolution.answer.outputs);
|
|
28785
|
+
if (newest != null) {
|
|
28786
|
+
this.captureLookupCompleted(lookupKind, "found", diagnostics, startedAt, { supersededTokens: matchingTokens.length - 1 });
|
|
28787
|
+
return newest;
|
|
28788
|
+
}
|
|
28120
28789
|
const reason = "token-ambiguous";
|
|
28121
28790
|
this.captureLookupFailure(lookupKind, reason, diagnostics, startedAt);
|
|
28122
28791
|
throw new UMPTokenLookupError(reason, diagnostics);
|
|
28123
28792
|
}
|
|
28124
|
-
|
|
28125
|
-
|
|
28126
|
-
|
|
28127
|
-
|
|
28128
|
-
|
|
28129
|
-
|
|
28130
|
-
|
|
28131
|
-
|
|
28132
|
-
|
|
28133
|
-
|
|
28793
|
+
if (matchingTokens.length === 1) {
|
|
28794
|
+
this.captureLookupCompleted(lookupKind, "found", diagnostics, startedAt);
|
|
28795
|
+
return matchingTokens[0];
|
|
28796
|
+
}
|
|
28797
|
+
if (resolution.progress.emptyHosts > 0) {
|
|
28798
|
+
this.captureLookupCompleted(lookupKind, "not-found", diagnostics, startedAt);
|
|
28799
|
+
return;
|
|
28800
|
+
}
|
|
28801
|
+
const reason = resolution.answer.outputs.length > 0 ? "token-malformed" : "lookup-incomplete";
|
|
28802
|
+
this.captureLookupFailure(lookupKind, reason, diagnostics, startedAt);
|
|
28803
|
+
throw new UMPTokenLookupError(reason, diagnostics);
|
|
28804
|
+
}
|
|
28805
|
+
/**
|
|
28806
|
+
* Picks the newest rendition among distinct verified tokens, when possible.
|
|
28807
|
+
*
|
|
28808
|
+
* The on-chain UMP protocol expresses token updates by consumption: the
|
|
28809
|
+
* transaction creating a new rendition spends the previous rendition's
|
|
28810
|
+
* outpoint (there is no rendition counter field in the current format).
|
|
28811
|
+
* A candidate is therefore superseded when any other candidate's ancestry
|
|
28812
|
+
* (available from its BEEF) spends the candidate's outpoint.
|
|
28813
|
+
*
|
|
28814
|
+
* @returns The single unsuperseded candidate, or undefined when supersession
|
|
28815
|
+
* cannot be established for every stale candidate (e.g. forked tokens).
|
|
28816
|
+
*/
|
|
28817
|
+
resolveNewestToken(matchingTokens, outputs) {
|
|
28818
|
+
const candidates = /* @__PURE__ */ new Map();
|
|
28819
|
+
for (const token of matchingTokens) {
|
|
28820
|
+
if (token.currentOutpoint == null) return void 0;
|
|
28821
|
+
candidates.set(token.currentOutpoint, token);
|
|
28822
|
+
}
|
|
28823
|
+
const evidenceByCandidate = /* @__PURE__ */ new Map();
|
|
28824
|
+
for (const output of outputs) try {
|
|
28825
|
+
const tx = _bsv_sdk.Transaction.fromBEEF(output.beef);
|
|
28826
|
+
const outpoint = `${tx.id("hex")}.${output.outputIndex}`;
|
|
28827
|
+
if (!candidates.has(outpoint)) continue;
|
|
28828
|
+
const evidence = evidenceByCandidate.get(outpoint) ?? {
|
|
28829
|
+
txs: [],
|
|
28830
|
+
spent: /* @__PURE__ */ new Set()
|
|
28831
|
+
};
|
|
28832
|
+
evidence.txs.push(tx);
|
|
28833
|
+
this.collectSpentOutpoints(tx, evidence.spent, /* @__PURE__ */ new Set());
|
|
28834
|
+
evidenceByCandidate.set(outpoint, evidence);
|
|
28835
|
+
} catch {}
|
|
28836
|
+
if (evidenceByCandidate.size !== candidates.size) return void 0;
|
|
28837
|
+
const survivors = [...candidates.keys()].filter((outpoint) => ![...evidenceByCandidate.entries()].some(([other, { spent }]) => other !== outpoint && spent.has(outpoint)));
|
|
28838
|
+
if (survivors.length === 1) return candidates.get(survivors[0]);
|
|
28839
|
+
const provenContinuations = survivors.filter((outpoint) => {
|
|
28840
|
+
const evidence = evidenceByCandidate.get(outpoint);
|
|
28841
|
+
const token = candidates.get(outpoint);
|
|
28842
|
+
return evidence != null && token != null && evidence.txs.some((tx) => this.consumesSameIdentityToken(tx, token));
|
|
28843
|
+
});
|
|
28844
|
+
if (provenContinuations.length !== 1) return void 0;
|
|
28845
|
+
return candidates.get(provenContinuations[0]);
|
|
28846
|
+
}
|
|
28847
|
+
/**
|
|
28848
|
+
* Whether `tx` spends an input whose source output (available in the BEEF)
|
|
28849
|
+
* decodes as a UMP token sharing the candidate's presentation or recovery
|
|
28850
|
+
* hash — on-chain proof that the candidate is an update of a same-identity
|
|
28851
|
+
* predecessor rather than an independently minted token.
|
|
28852
|
+
*/
|
|
28853
|
+
consumesSameIdentityToken(tx, token) {
|
|
28854
|
+
const presentationHash = _bsv_sdk.Utils.toHex(token.presentationHash);
|
|
28855
|
+
const recoveryHash = _bsv_sdk.Utils.toHex(token.recoveryHash);
|
|
28856
|
+
for (const input of tx.inputs) {
|
|
28857
|
+
const source = input.sourceTransaction;
|
|
28858
|
+
if (source == null || input.sourceOutputIndex == null) continue;
|
|
28859
|
+
const sourceOutput = source.outputs[input.sourceOutputIndex];
|
|
28860
|
+
if (sourceOutput == null) continue;
|
|
28861
|
+
try {
|
|
28862
|
+
const decoded = _bsv_sdk.PushDrop.decode(sourceOutput.lockingScript);
|
|
28863
|
+
if (decoded.fields == null) continue;
|
|
28864
|
+
const fields = stripVerifiedPushDropSignature(decoded.fields, decoded.lockingPublicKey);
|
|
28865
|
+
if (fields.length < 11 || fields[6]?.length !== 32 || fields[7]?.length !== 32) continue;
|
|
28866
|
+
if (_bsv_sdk.Utils.toHex(fields[6]) === presentationHash || _bsv_sdk.Utils.toHex(fields[7]) === recoveryHash) return true;
|
|
28867
|
+
} catch {
|
|
28868
|
+
continue;
|
|
28134
28869
|
}
|
|
28135
|
-
}
|
|
28136
|
-
return
|
|
28870
|
+
}
|
|
28871
|
+
return false;
|
|
28872
|
+
}
|
|
28873
|
+
/**
|
|
28874
|
+
* Accumulates every outpoint spent by `tx` and by the ancestor transactions
|
|
28875
|
+
* embedded in its BEEF, so supersession is detected even when intermediate
|
|
28876
|
+
* renditions are absent from the lookup answer. Iterative so arbitrarily
|
|
28877
|
+
* long update chains cannot exhaust the call stack.
|
|
28878
|
+
*/
|
|
28879
|
+
collectSpentOutpoints(tx, spent, visited) {
|
|
28880
|
+
const pending = [tx];
|
|
28881
|
+
while (pending.length > 0) {
|
|
28882
|
+
const current = pending.pop();
|
|
28883
|
+
const txid = current.id("hex");
|
|
28884
|
+
if (visited.has(txid)) continue;
|
|
28885
|
+
visited.add(txid);
|
|
28886
|
+
for (const input of current.inputs) {
|
|
28887
|
+
const sourceTxid = input.sourceTXID ?? input.sourceTransaction?.id("hex");
|
|
28888
|
+
if (sourceTxid == null || input.sourceOutputIndex == null) continue;
|
|
28889
|
+
spent.add(`${sourceTxid}.${input.sourceOutputIndex}`);
|
|
28890
|
+
if (input.sourceTransaction != null) pending.push(input.sourceTransaction);
|
|
28891
|
+
}
|
|
28892
|
+
}
|
|
28137
28893
|
}
|
|
28138
28894
|
emptyLookupDiagnostics(correlationId) {
|
|
28139
28895
|
return {
|
|
@@ -28174,6 +28930,21 @@ var OverlayUMPTokenInteractor = class {
|
|
|
28174
28930
|
outputCount: diagnostics.outputCount
|
|
28175
28931
|
};
|
|
28176
28932
|
}
|
|
28933
|
+
captureLookupCompleted(lookupKind, result, diagnostics, startedAt, extraAttributes = {}) {
|
|
28934
|
+
this.telemetry.capture({
|
|
28935
|
+
name: "wallet-toolbox.ump.lookup.completed",
|
|
28936
|
+
component: "wallet-toolbox.ump",
|
|
28937
|
+
severity: "info",
|
|
28938
|
+
correlationId: diagnostics.correlationId,
|
|
28939
|
+
attributes: {
|
|
28940
|
+
lookupKind,
|
|
28941
|
+
result,
|
|
28942
|
+
durationMs: Date.now() - startedAt,
|
|
28943
|
+
...this.lookupDiagnosticAttributes(diagnostics),
|
|
28944
|
+
...extraAttributes
|
|
28945
|
+
}
|
|
28946
|
+
});
|
|
28947
|
+
}
|
|
28177
28948
|
captureLookupFailure(lookupKind, reason, diagnostics, startedAt, error) {
|
|
28178
28949
|
this.telemetry.capture({
|
|
28179
28950
|
name: "wallet-toolbox.ump.lookup.indeterminate",
|
|
@@ -28414,8 +29185,7 @@ var OverlayUMPTokenInteractor = class {
|
|
|
28414
29185
|
throw new UMPTokenLookupError("lookup-unavailable", diagnostics, { cause: error });
|
|
28415
29186
|
}
|
|
28416
29187
|
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)) {
|
|
29188
|
+
if (resolution.progress.emptyHosts === 0) {
|
|
28419
29189
|
const diagnostics = this.toLookupDiagnostics(resolution);
|
|
28420
29190
|
this.captureLookupFailure("outpoint", "lookup-incomplete", diagnostics, startedAt);
|
|
28421
29191
|
throw new UMPTokenLookupError("lookup-incomplete", diagnostics);
|