@bsv/wallet-toolbox-client 2.4.21 → 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 +1459 -319
- package/out/index.client.cjs.map +1 -1
- package/out/index.client.d.cts +86 -9
- package/out/index.client.d.cts.map +1 -1
- package/out/index.client.d.mts +86 -9
- package/out/index.client.d.mts.map +1 -1
- package/out/index.client.mjs +1460 -320
- 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
|
}
|
|
@@ -4923,6 +4935,12 @@ function upgradeOutputs(db) {
|
|
|
4923
4935
|
autoIncrement: true
|
|
4924
4936
|
});
|
|
4925
4937
|
store.createIndex("userId", "userId");
|
|
4938
|
+
store.createIndex("userId_basketId", ["userId", "basketId"]);
|
|
4939
|
+
store.createIndex("txid_vout_userId", [
|
|
4940
|
+
"txid",
|
|
4941
|
+
"vout",
|
|
4942
|
+
"userId"
|
|
4943
|
+
], { unique: true });
|
|
4926
4944
|
store.createIndex("transactionId", "transactionId");
|
|
4927
4945
|
store.createIndex("basketId", "basketId");
|
|
4928
4946
|
store.createIndex("spentBy", "spentBy");
|
|
@@ -5056,8 +5074,10 @@ async function mergeInputBeefs(rawTx, beef, trustSelf, knownTxids, trx, required
|
|
|
5056
5074
|
for (const input of tx.inputs) {
|
|
5057
5075
|
const sourceTXID = input.sourceTXID ?? "";
|
|
5058
5076
|
if (sourceTXID === "") throw new WERR_INTERNAL("req all transaction inputs must have valid sourceTXID");
|
|
5059
|
-
|
|
5060
|
-
|
|
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);
|
|
5061
5081
|
else await getValidBeef(sourceTXID, beef, trustSelf, knownTxids, trx, requiredLevels);
|
|
5062
5082
|
}
|
|
5063
5083
|
}
|
|
@@ -5121,26 +5141,22 @@ async function notifyTransactionsOfProof(ids, provenTxId, addNote, updateTransac
|
|
|
5121
5141
|
* @param options
|
|
5122
5142
|
*/
|
|
5123
5143
|
async function getBeefForTransaction(storage, txid, options) {
|
|
5124
|
-
|
|
5125
|
-
|
|
5126
|
-
else if (options.mergeToBeef != null) beef = _bsv_sdk.Beef.fromBinary(options.mergeToBeef);
|
|
5127
|
-
else beef = new _bsv_sdk.Beef();
|
|
5128
|
-
const knownTxids = new Set(options.knownTxids ?? []);
|
|
5144
|
+
const beef = mergeTarget(options);
|
|
5145
|
+
const hasKnownTxid = makeKnownTxidLookup$1(options.knownTxids ?? []);
|
|
5129
5146
|
const scheduled = /* @__PURE__ */ new Set([txid]);
|
|
5130
5147
|
let frontier = [{
|
|
5131
5148
|
txid,
|
|
5132
5149
|
depth: 0
|
|
5133
5150
|
}];
|
|
5134
|
-
const
|
|
5135
|
-
const concurrency = Number.isFinite(requestedConcurrency) ? Math.max(1, Math.min(32, Math.floor(requestedConcurrency))) : 8;
|
|
5151
|
+
const concurrency = normalizeConcurrency(options.maxConcurrency);
|
|
5136
5152
|
while (frontier.length > 0) {
|
|
5137
|
-
const current = frontier.filter((item) => beef
|
|
5138
|
-
const resolved = await mapWithConcurrency(current, concurrency, async (item) => await resolveBeefForTransaction(storage, item.txid, options,
|
|
5153
|
+
const current = frontier.filter((item) => needsResolution(beef, item.txid, hasKnownTxid));
|
|
5154
|
+
const resolved = await mapWithConcurrency(current, concurrency, async (item) => await resolveBeefForTransaction(storage, item.txid, options, hasKnownTxid, item.depth));
|
|
5139
5155
|
const next = [];
|
|
5140
5156
|
for (let i = 0; i < resolved.length; i++) {
|
|
5141
5157
|
const result = resolved[i];
|
|
5142
5158
|
beef.mergeBeef(result.beef);
|
|
5143
|
-
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)) {
|
|
5144
5160
|
scheduled.add(dependency);
|
|
5145
5161
|
next.push({
|
|
5146
5162
|
txid: dependency,
|
|
@@ -5152,6 +5168,188 @@ async function getBeefForTransaction(storage, txid, options) {
|
|
|
5152
5168
|
}
|
|
5153
5169
|
return beef;
|
|
5154
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
|
+
}
|
|
5337
|
+
function makeKnownTxidLookup$1(knownTxids) {
|
|
5338
|
+
let lookups = 0;
|
|
5339
|
+
let indexed;
|
|
5340
|
+
return (txid) => {
|
|
5341
|
+
lookups++;
|
|
5342
|
+
if (indexed != null) return indexed.has(txid);
|
|
5343
|
+
if (knownTxids.length > 64 && lookups > 4) {
|
|
5344
|
+
indexed = new Set(knownTxids);
|
|
5345
|
+
return indexed.has(txid);
|
|
5346
|
+
}
|
|
5347
|
+
return knownTxids.includes(txid);
|
|
5348
|
+
};
|
|
5349
|
+
}
|
|
5350
|
+
function normalizeConcurrency(value = 8) {
|
|
5351
|
+
return Number.isFinite(value) ? Math.max(1, Math.min(32, Math.floor(value))) : 8;
|
|
5352
|
+
}
|
|
5155
5353
|
async function mapWithConcurrency(values, concurrency, mapper) {
|
|
5156
5354
|
const results = Array.from({ length: values.length }, () => void 0);
|
|
5157
5355
|
let cursor = 0;
|
|
@@ -5195,11 +5393,11 @@ async function mergeUsableProvenTransaction(beef, txid, result, options, recursi
|
|
|
5195
5393
|
beef.mergeBump(merklePath);
|
|
5196
5394
|
return beef;
|
|
5197
5395
|
}
|
|
5198
|
-
async function resolveBeefForTransaction(storage, txid, options,
|
|
5396
|
+
async function resolveBeefForTransaction(storage, txid, options, hasKnownTxid, recursionDepth) {
|
|
5199
5397
|
const maxDepth = storage.maxRecursionDepth;
|
|
5200
5398
|
if (maxDepth && maxDepth <= recursionDepth) throw new WERR_INVALID_OPERATION(`Maximum BEEF depth exceeded. Limit is ${storage.maxRecursionDepth}`);
|
|
5201
5399
|
const beef = new _bsv_sdk.Beef();
|
|
5202
|
-
if (
|
|
5400
|
+
if (hasKnownTxid(txid)) {
|
|
5203
5401
|
beef.mergeTxidOnly(txid);
|
|
5204
5402
|
return {
|
|
5205
5403
|
beef,
|
|
@@ -5280,6 +5478,23 @@ async function createMergedBeefOfTxids(txids, storage) {
|
|
|
5280
5478
|
//#endregion
|
|
5281
5479
|
//#region ../src/storage/methods/processAction.ts
|
|
5282
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) {
|
|
5283
5498
|
const logger = args.logger;
|
|
5284
5499
|
logger?.group("storage processAction");
|
|
5285
5500
|
const userId = verifyId(auth.userId);
|
|
@@ -5287,9 +5502,9 @@ async function processAction$1(storage, auth, args) {
|
|
|
5287
5502
|
let req;
|
|
5288
5503
|
const txidsOfReqsToShareWithWorld = [...args.sendWith];
|
|
5289
5504
|
if (args.isNewTx) {
|
|
5290
|
-
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));
|
|
5291
5506
|
logger?.log("validated new tx updates to storage");
|
|
5292
|
-
({req} = await commitNewTxToStorage(storage, userId, vargs));
|
|
5507
|
+
({req} = await traceProcessStep(storage, "wallet.storage.process_action.commit", parent, async () => await commitNewTxToStorage(storage, userId, vargs)));
|
|
5293
5508
|
logger?.log("committed new tx updates to storage ");
|
|
5294
5509
|
if (!req) throw new WERR_INTERNAL();
|
|
5295
5510
|
if (args.isNoSend && !args.isSendWith) logger?.log(`noSend txid ${req.txid}`);
|
|
@@ -5298,12 +5513,19 @@ async function processAction$1(storage, auth, args) {
|
|
|
5298
5513
|
logger?.log(`sending txid ${req.txid}`);
|
|
5299
5514
|
}
|
|
5300
5515
|
}
|
|
5301
|
-
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));
|
|
5302
5517
|
r.sendWithResults = swr;
|
|
5303
5518
|
r.notDelayedResults = ndr;
|
|
5304
5519
|
logger?.groupEnd();
|
|
5305
5520
|
return r;
|
|
5306
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
|
+
}
|
|
5307
5529
|
/**
|
|
5308
5530
|
* Verifies that all the txids are known reqs with ready-to-share status.
|
|
5309
5531
|
* Assigns a batch identifier and updates all the provenTxReqs.
|
|
@@ -5474,21 +5696,16 @@ async function validateCommitNewTxToStorageArgs(storage, userId, params) {
|
|
|
5474
5696
|
} }));
|
|
5475
5697
|
if (!transaction.isOutgoing) throw new WERR_INVALID_OPERATION("isOutgoing is not true");
|
|
5476
5698
|
if (transaction.inputBEEF == null) throw new WERR_INVALID_OPERATION();
|
|
5477
|
-
const beef = _bsv_sdk.Beef.fromBinary(asArray(transaction.inputBEEF));
|
|
5478
5699
|
if (transaction.status !== "unsigned" && transaction.status !== "unprocessed") throw new WERR_INVALID_OPERATION(`invalid transaction status ${transaction.status}`);
|
|
5479
5700
|
const transactionId = verifyId(transaction.transactionId);
|
|
5480
|
-
const outputOutputs = await storage.findOutputs({ partial: {
|
|
5701
|
+
const [outputOutputs, commissionRows] = await Promise.all([storage.findOutputs({ partial: {
|
|
5481
5702
|
userId,
|
|
5482
5703
|
transactionId
|
|
5483
|
-
} })
|
|
5484
|
-
const inputOutputs = await storage.findOutputs({ partial: {
|
|
5485
|
-
userId,
|
|
5486
|
-
spentBy: transactionId
|
|
5487
|
-
} });
|
|
5488
|
-
const commission = verifyOneOrNone(await storage.findCommissions({ partial: {
|
|
5704
|
+
} }), storage.commissionSatoshis > 0 ? storage.findCommissions({ partial: {
|
|
5489
5705
|
transactionId,
|
|
5490
5706
|
userId
|
|
5491
|
-
} }));
|
|
5707
|
+
} }) : Promise.resolve([])]);
|
|
5708
|
+
const commission = verifyOneOrNone(commissionRows);
|
|
5492
5709
|
if (storage.commissionSatoshis > 0) {
|
|
5493
5710
|
if (commission == null) throw new WERR_INTERNAL();
|
|
5494
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.");
|
|
@@ -5508,10 +5725,7 @@ async function validateCommitNewTxToStorageArgs(storage, userId, params) {
|
|
|
5508
5725
|
txScriptOffsets,
|
|
5509
5726
|
transactionId,
|
|
5510
5727
|
transaction,
|
|
5511
|
-
inputOutputs,
|
|
5512
5728
|
outputOutputs,
|
|
5513
|
-
commission,
|
|
5514
|
-
beef,
|
|
5515
5729
|
req,
|
|
5516
5730
|
outputUpdates: [],
|
|
5517
5731
|
transactionUpdate: {
|
|
@@ -6189,17 +6403,24 @@ async function generateChangeSdkCore(params, allocateChangeInput, releaseChangeI
|
|
|
6189
6403
|
};
|
|
6190
6404
|
const fixedInputs = params.fixedInputs;
|
|
6191
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;
|
|
6192
6413
|
/**
|
|
6193
6414
|
* @returns sum of transaction fixedInputs satoshis and fundingInputs satoshis
|
|
6194
6415
|
*/
|
|
6195
6416
|
const funding = () => {
|
|
6196
|
-
return
|
|
6417
|
+
return fixedFunding + allocatedFunding;
|
|
6197
6418
|
};
|
|
6198
6419
|
/**
|
|
6199
6420
|
* @returns sum of transaction fixedOutputs satoshis
|
|
6200
6421
|
*/
|
|
6201
6422
|
const spending = () => {
|
|
6202
|
-
return
|
|
6423
|
+
return fixedSpending;
|
|
6203
6424
|
};
|
|
6204
6425
|
/**
|
|
6205
6426
|
* @returns sum of transaction changeOutputs satoshis
|
|
@@ -6209,7 +6430,9 @@ async function generateChangeSdkCore(params, allocateChangeInput, releaseChangeI
|
|
|
6209
6430
|
};
|
|
6210
6431
|
const fee = () => funding() - spending() - change();
|
|
6211
6432
|
const size = (addedChangeInputs, addedChangeOutputs) => {
|
|
6212
|
-
|
|
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;
|
|
6213
6436
|
};
|
|
6214
6437
|
/**
|
|
6215
6438
|
* @returns the target fee required for the transaction as currently configured under feeModel.
|
|
@@ -6248,7 +6471,10 @@ async function generateChangeSdkCore(params, allocateChangeInput, releaseChangeI
|
|
|
6248
6471
|
const releaseAllocatedChangeInputs = async () => {
|
|
6249
6472
|
while (r.allocatedChangeInputs.length > 0) {
|
|
6250
6473
|
const i = r.allocatedChangeInputs.pop();
|
|
6251
|
-
if (i != null)
|
|
6474
|
+
if (i != null) {
|
|
6475
|
+
allocatedFunding -= i.satoshis;
|
|
6476
|
+
await releaseChangeInput(i.outputId);
|
|
6477
|
+
}
|
|
6252
6478
|
}
|
|
6253
6479
|
feeExcessNow = feeExcess();
|
|
6254
6480
|
};
|
|
@@ -6283,6 +6509,7 @@ async function generateChangeSdkCore(params, allocateChangeInput, releaseChangeI
|
|
|
6283
6509
|
const allocatedChangeInput = await allocateChangeInput(-feeExcess(1, ao) + (ao === 1 ? 2 * params.changeInitialSatoshis : 0) + changeBuffer, exactSatoshis);
|
|
6284
6510
|
if (allocatedChangeInput == null) return false;
|
|
6285
6511
|
r.allocatedChangeInputs.push(allocatedChangeInput);
|
|
6512
|
+
allocatedFunding += allocatedChangeInput.satoshis;
|
|
6286
6513
|
maybeAddChangeOutput(ao);
|
|
6287
6514
|
return true;
|
|
6288
6515
|
};
|
|
@@ -6294,6 +6521,7 @@ async function generateChangeSdkCore(params, allocateChangeInput, releaseChangeI
|
|
|
6294
6521
|
while (r.changeOutputs.length > 0 && feeExcess() < 0) r.changeOutputs.pop();
|
|
6295
6522
|
if (feeExcess() < 0) break;
|
|
6296
6523
|
removeChurnPairs(r.allocatedChangeInputs, r.changeOutputs);
|
|
6524
|
+
allocatedFunding = r.allocatedChangeInputs.reduce((sum, input) => sum + input.satoshis, 0);
|
|
6297
6525
|
}
|
|
6298
6526
|
};
|
|
6299
6527
|
/**
|
|
@@ -6302,7 +6530,9 @@ async function generateChangeSdkCore(params, allocateChangeInput, releaseChangeI
|
|
|
6302
6530
|
await fundTransaction();
|
|
6303
6531
|
if (feeExcess() < 0 && vgcpr.hasMaxPossibleOutput !== void 0) {
|
|
6304
6532
|
if (fixedOutputs[vgcpr.hasMaxPossibleOutput].satoshis !== 0x775f05a073fff) throw new WERR_INTERNAL();
|
|
6305
|
-
|
|
6533
|
+
const adjustment = feeExcess();
|
|
6534
|
+
fixedOutputs[vgcpr.hasMaxPossibleOutput].satoshis += adjustment;
|
|
6535
|
+
fixedSpending += adjustment;
|
|
6306
6536
|
r.maxPossibleSatoshisAdjustment = {
|
|
6307
6537
|
fixedOutputIndex: vgcpr.hasMaxPossibleOutput,
|
|
6308
6538
|
satoshis: fixedOutputs[vgcpr.hasMaxPossibleOutput].satoshis
|
|
@@ -6321,8 +6551,11 @@ async function generateChangeSdkCore(params, allocateChangeInput, releaseChangeI
|
|
|
6321
6551
|
* If needed, seek funding to avoid overspending on fees without a change output to recapture it.
|
|
6322
6552
|
*/
|
|
6323
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());
|
|
6324
6557
|
await releaseAllocatedChangeInputs();
|
|
6325
|
-
throw new WERR_INSUFFICIENT_FUNDS(
|
|
6558
|
+
throw new WERR_INSUFFICIENT_FUNDS(totalSatoshisNeeded, moreSatoshisNeeded);
|
|
6326
6559
|
}
|
|
6327
6560
|
/**
|
|
6328
6561
|
* Distribute the excess fees across the changeOutputs added.
|
|
@@ -6646,6 +6879,8 @@ function makeChangeLock(out, dctr, args, changeKeys, wallet) {
|
|
|
6646
6879
|
}
|
|
6647
6880
|
//#endregion
|
|
6648
6881
|
//#region ../src/signer/methods/verifyUnlockScripts.ts
|
|
6882
|
+
const postChronicleHeightFallback = 943816;
|
|
6883
|
+
const canonicalP2PKHScope = _bsv_sdk.TransactionSignature.SIGHASH_ALL + _bsv_sdk.TransactionSignature.SIGHASH_FORKID;
|
|
6649
6884
|
const javaScriptOnlyVerifier = {
|
|
6650
6885
|
shouldVerifySpend: () => false,
|
|
6651
6886
|
verifySpend: async () => {
|
|
@@ -6657,10 +6892,11 @@ function invalidUnlockingScript(inputIndex, detail) {
|
|
|
6657
6892
|
return new WERR_INVALID_PARAMETER(`inputs[${inputIndex}].unlockScript`, `valid.${suffix}`);
|
|
6658
6893
|
}
|
|
6659
6894
|
async function verifyOneSpend(pending, verifier) {
|
|
6895
|
+
const [inputIndex, , spend, context] = pending;
|
|
6660
6896
|
try {
|
|
6661
|
-
if (!(verifier === void 0 ?
|
|
6897
|
+
if (!(verifier === void 0 ? spend.validate(context) : await spend.validateWith(verifier, context))) throw invalidUnlockingScript(inputIndex);
|
|
6662
6898
|
} catch (error) {
|
|
6663
|
-
if (error instanceof _bsv_sdk.ScriptEvaluationError) throw invalidUnlockingScript(
|
|
6899
|
+
if (error instanceof _bsv_sdk.ScriptEvaluationError) throw invalidUnlockingScript(inputIndex, error.message);
|
|
6664
6900
|
throw error;
|
|
6665
6901
|
}
|
|
6666
6902
|
}
|
|
@@ -6670,33 +6906,157 @@ async function verifyPendingSpends(pending, verifier) {
|
|
|
6670
6906
|
return;
|
|
6671
6907
|
}
|
|
6672
6908
|
const batched = [];
|
|
6673
|
-
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);
|
|
6674
6910
|
else await verifyOneSpend(item, javaScriptOnlyVerifier);
|
|
6675
6911
|
if (batched.length === 0) return;
|
|
6676
6912
|
let verdicts;
|
|
6677
6913
|
try {
|
|
6678
6914
|
verdicts = await verifier.verifySpendsBatch(batched.map((item) => ({
|
|
6679
|
-
spend: item
|
|
6680
|
-
...item
|
|
6915
|
+
spend: item[2],
|
|
6916
|
+
...item[3]
|
|
6681
6917
|
})));
|
|
6682
6918
|
} catch (error) {
|
|
6683
|
-
if (error instanceof _bsv_sdk.ScriptEvaluationError) throw invalidUnlockingScript(batched[0]
|
|
6919
|
+
if (error instanceof _bsv_sdk.ScriptEvaluationError) throw invalidUnlockingScript(batched[0][0], error.message);
|
|
6684
6920
|
throw error;
|
|
6685
6921
|
}
|
|
6686
6922
|
if (verdicts.length !== batched.length) throw new Error("Script verifier returned an invalid batch result count");
|
|
6687
6923
|
verdicts.forEach((valid, index) => {
|
|
6688
|
-
if (!valid) throw invalidUnlockingScript(batched[index]
|
|
6924
|
+
if (!valid) throw invalidUnlockingScript(batched[index][0]);
|
|
6689
6925
|
});
|
|
6690
6926
|
}
|
|
6691
|
-
function
|
|
6692
|
-
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);
|
|
6693
7053
|
if (tx == null) throw new WERR_INVALID_PARAMETER("txid", `contained in beef, txid ${txid}`);
|
|
6694
7054
|
const sigHashCache = { hashOutputsSingle: /* @__PURE__ */ new Map() };
|
|
6695
7055
|
for (let inputIndex = 0; inputIndex < tx.inputs.length; inputIndex++) {
|
|
6696
7056
|
const input = tx.inputs[inputIndex];
|
|
6697
7057
|
if (input.sourceTXID == null) throw new WERR_INVALID_PARAMETER(`inputs[${inputIndex}].sourceTXID`, "valid");
|
|
6698
7058
|
if (input.unlockingScript == null) throw new WERR_INVALID_PARAMETER(`inputs[${inputIndex}].unlockingScript`, "valid");
|
|
6699
|
-
input.sourceTransaction =
|
|
7059
|
+
input.sourceTransaction = transactions.get(input.sourceTXID);
|
|
6700
7060
|
if (input.sourceTransaction == null) {
|
|
6701
7061
|
result.skippedInputs++;
|
|
6702
7062
|
continue;
|
|
@@ -6708,11 +7068,10 @@ function collectTransactionSpends(txid, resultIndex, beef, result, pending) {
|
|
|
6708
7068
|
consensus: true,
|
|
6709
7069
|
utxoHeight
|
|
6710
7070
|
};
|
|
6711
|
-
pending.push(
|
|
7071
|
+
pending.push([
|
|
6712
7072
|
inputIndex,
|
|
6713
7073
|
resultIndex,
|
|
6714
|
-
|
|
6715
|
-
spend: new _bsv_sdk.Spend({
|
|
7074
|
+
new _bsv_sdk.Spend({
|
|
6716
7075
|
sourceTXID: input.sourceTXID,
|
|
6717
7076
|
sourceOutputIndex: input.sourceOutputIndex,
|
|
6718
7077
|
lockingScript: sourceOutput.lockingScript,
|
|
@@ -6726,9 +7085,52 @@ function collectTransactionSpends(txid, resultIndex, beef, result, pending) {
|
|
|
6726
7085
|
outputs: tx.outputs,
|
|
6727
7086
|
lockTime: tx.lockTime,
|
|
6728
7087
|
sigHashCache
|
|
6729
|
-
})
|
|
6730
|
-
|
|
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;
|
|
6731
7132
|
}
|
|
7133
|
+
return pending;
|
|
6732
7134
|
}
|
|
6733
7135
|
/**
|
|
6734
7136
|
* Verifies every resolvable input from several transactions in one optional
|
|
@@ -6739,10 +7141,17 @@ async function verifyUnlockScriptsBatch(txids, beef, verifier) {
|
|
|
6739
7141
|
verifiedInputs: 0,
|
|
6740
7142
|
skippedInputs: 0
|
|
6741
7143
|
}));
|
|
6742
|
-
const
|
|
6743
|
-
|
|
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);
|
|
6744
7153
|
await verifyPendingSpends(pending, verifier);
|
|
6745
|
-
for (const item of pending) results[item
|
|
7154
|
+
for (const item of pending) results[item[1]].verifiedInputs++;
|
|
6746
7155
|
return results;
|
|
6747
7156
|
}
|
|
6748
7157
|
/**
|
|
@@ -6765,21 +7174,56 @@ async function completeSignedTransaction(prior, spends, wallet) {
|
|
|
6765
7174
|
input.unlockingScript = asBsvSdkScript(spend.unlockingScript);
|
|
6766
7175
|
if (spend.sequenceNumber !== void 0) input.sequence = spend.sequenceNumber;
|
|
6767
7176
|
}
|
|
6768
|
-
|
|
6769
|
-
const
|
|
6770
|
-
|
|
6771
|
-
|
|
6772
|
-
|
|
6773
|
-
|
|
6774
|
-
|
|
6775
|
-
|
|
6776
|
-
|
|
6777
|
-
|
|
6778
|
-
const
|
|
6779
|
-
|
|
6780
|
-
|
|
6781
|
-
|
|
6782
|
-
|
|
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());
|
|
6783
7227
|
if (wallet.telemetry.enabled) await wallet.telemetry.withSpan("wallet.crypto.transaction_sign", {
|
|
6784
7228
|
component: "wallet-toolbox",
|
|
6785
7229
|
carrier: prior.args,
|
|
@@ -6800,7 +7244,7 @@ function getResultBeef(result) {
|
|
|
6800
7244
|
//#endregion
|
|
6801
7245
|
//#region ../src/signer/methods/createAction.ts
|
|
6802
7246
|
async function createAction$1(wallet, auth, vargs) {
|
|
6803
|
-
if (!wallet.telemetry.enabled) return await createActionCore(wallet, auth, vargs);
|
|
7247
|
+
if (!wallet.telemetry.enabled) return await createActionCore$1(wallet, auth, vargs);
|
|
6804
7248
|
return await wallet.telemetry.withSpan("wallet.create_action", {
|
|
6805
7249
|
component: "wallet-toolbox",
|
|
6806
7250
|
carrier: vargs,
|
|
@@ -6811,7 +7255,7 @@ async function createAction$1(wallet, auth, vargs) {
|
|
|
6811
7255
|
"action.is_sign_action": vargs.isSignAction
|
|
6812
7256
|
}
|
|
6813
7257
|
}, async (span) => {
|
|
6814
|
-
const result = await createActionCore(wallet, auth, vargs, span);
|
|
7258
|
+
const result = await createActionCore$1(wallet, auth, vargs, span);
|
|
6815
7259
|
span.end({ attributes: {
|
|
6816
7260
|
"action.has_transaction": result.tx != null,
|
|
6817
7261
|
"action.has_signable_transaction": result.signableTransaction != null,
|
|
@@ -6820,7 +7264,7 @@ async function createAction$1(wallet, auth, vargs) {
|
|
|
6820
7264
|
return result;
|
|
6821
7265
|
});
|
|
6822
7266
|
}
|
|
6823
|
-
async function createActionCore(wallet, auth, vargs, parent) {
|
|
7267
|
+
async function createActionCore$1(wallet, auth, vargs, parent) {
|
|
6824
7268
|
const r = {};
|
|
6825
7269
|
const logger = vargs.logger;
|
|
6826
7270
|
let prior;
|
|
@@ -6835,19 +7279,22 @@ async function createActionCore(wallet, auth, vargs, parent) {
|
|
|
6835
7279
|
prior.tx = await traceActionStep(wallet, "wallet.create_action.complete_signing", parent, async () => await completeSignedTransaction(prior, {}, wallet));
|
|
6836
7280
|
logger?.log("completed signed transaction");
|
|
6837
7281
|
r.txid = prior.tx.id("hex");
|
|
6838
|
-
const beef =
|
|
6839
|
-
|
|
6840
|
-
|
|
6841
|
-
|
|
6842
|
-
|
|
6843
|
-
|
|
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
|
+
});
|
|
6844
7291
|
logger?.log("merged beef");
|
|
6845
7292
|
await traceActionStep(wallet, "wallet.create_action.verify_unlock_scripts", parent, async () => await verifyUnlockScripts(r.txid, beef, wallet.scriptVerifier));
|
|
6846
7293
|
logger?.log("verified unlock scripts");
|
|
6847
7294
|
r.noSendChange = prior.dcr.noSendChangeOutputVouts?.map((vout) => `${r.txid}.${vout}`);
|
|
6848
7295
|
beef.atomicTxid = r.txid;
|
|
6849
7296
|
setResultBeef(r, beef);
|
|
6850
|
-
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));
|
|
6851
7298
|
}
|
|
6852
7299
|
const { sendWithResults, notDelayedResults } = await traceActionStep(wallet, "wallet.create_action.process", parent, async () => await processAction(prior, wallet, auth, vargs));
|
|
6853
7300
|
logger?.log("processed transaction");
|
|
@@ -7465,6 +7912,50 @@ function selectCanonicalChange(outputs, targetSatoshis, exactSatoshis) {
|
|
|
7465
7912
|
if (over != null) return over;
|
|
7466
7913
|
return outputs.filter((output) => output.satoshis < targetSatoshis).sort((a, b) => b.satoshis - a.satoshis || b.outputId - a.outputId)[0];
|
|
7467
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
|
+
};
|
|
7468
7959
|
function repeatableRandom(randomVals) {
|
|
7469
7960
|
const values = [...randomVals ?? []];
|
|
7470
7961
|
return () => {
|
|
@@ -8600,6 +9091,22 @@ var ActionBatchController = class {
|
|
|
8600
9091
|
};
|
|
8601
9092
|
//#endregion
|
|
8602
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
|
+
}
|
|
8603
9110
|
/**
|
|
8604
9111
|
* Build a {@link DiscoverCertificatesResult} from contact records so {@link Wallet.discoverByIdentityKey}
|
|
8605
9112
|
* and {@link Wallet.discoverByAttributes} can short-circuit on a local contacts hit. The synthetic
|
|
@@ -9073,6 +9580,7 @@ var Wallet = class {
|
|
|
9073
9580
|
if (this.returnTxidOnly) return beef;
|
|
9074
9581
|
const b = parsedBeef ?? _bsv_sdk.Beef.fromBinary(beef);
|
|
9075
9582
|
if (!b.atomicTxid) throw new WERR_INTERNAL();
|
|
9583
|
+
if (!b.txs.some((btx) => btx.isTxidOnly && !knownTxids?.includes(btx.txid))) return beef;
|
|
9076
9584
|
return this.verifyReturnedTxidOnly(b, knownTxids).toBinaryAtomic(b.atomicTxid);
|
|
9077
9585
|
}
|
|
9078
9586
|
verifyReturnedTxidOnlyBEEF(beef) {
|
|
@@ -9104,7 +9612,7 @@ var Wallet = class {
|
|
|
9104
9612
|
_bsv_sdk.Validation.validateOriginator(originator);
|
|
9105
9613
|
args.options ??= {};
|
|
9106
9614
|
args.options.trustSelf ||= this.trustSelf;
|
|
9107
|
-
|
|
9615
|
+
prepareKnownTxidsForCreateAction(this, args);
|
|
9108
9616
|
const { auth, vargs } = this.validateAuthAndArgs(args, _bsv_sdk.Validation.validateCreateActionArgs, logger);
|
|
9109
9617
|
logger?.log("validated args");
|
|
9110
9618
|
vargs.includeAllSourceTransactions = this.includeAllSourceTransactions;
|
|
@@ -9112,9 +9620,25 @@ var Wallet = class {
|
|
|
9112
9620
|
const r = await createAction$1(this, auth, vargs);
|
|
9113
9621
|
logger?.log("action created");
|
|
9114
9622
|
const resultBeef = getResultBeef(r);
|
|
9115
|
-
if (r.tx != null) this.beef.mergeBeefFromParty(this.storageParty, resultBeef ?? r.tx);
|
|
9116
9623
|
if (r.tx != null) {
|
|
9117
|
-
|
|
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();
|
|
9118
9642
|
logger?.log("verify returned AtomicBEEF");
|
|
9119
9643
|
}
|
|
9120
9644
|
if (!vargs.isDelayed) throwIfAnyUnsuccessfulCreateActions(r);
|
|
@@ -9423,6 +9947,28 @@ function isAutoSpendableChangeOutput(output) {
|
|
|
9423
9947
|
return isManagedChangeOutput(output) && output.spendable && output.spentBy == null;
|
|
9424
9948
|
}
|
|
9425
9949
|
async function createAction(storage, auth, vargs, _originator) {
|
|
9950
|
+
if (!storage.telemetry.enabled) return await createActionCore(storage, auth, vargs);
|
|
9951
|
+
return await storage.telemetry.withSpan("wallet.storage.create_action", {
|
|
9952
|
+
component: "wallet-storage",
|
|
9953
|
+
carrier: vargs,
|
|
9954
|
+
attributes: {
|
|
9955
|
+
"action.fixed_input_count": vargs.inputs.length,
|
|
9956
|
+
"action.fixed_output_count": vargs.outputs.length,
|
|
9957
|
+
"action.known_txid_count": vargs.options.knownTxids?.length ?? 0,
|
|
9958
|
+
"action.is_delayed": vargs.isDelayed,
|
|
9959
|
+
"action.is_no_send": vargs.isNoSend
|
|
9960
|
+
}
|
|
9961
|
+
}, async (span) => {
|
|
9962
|
+
const result = await createActionCore(storage, auth, vargs, span);
|
|
9963
|
+
span.end({ attributes: {
|
|
9964
|
+
"action.result_input_count": result.inputs.length,
|
|
9965
|
+
"action.result_output_count": result.outputs.length,
|
|
9966
|
+
"action.input_beef_bytes": result.inputBeef?.length ?? 0
|
|
9967
|
+
} });
|
|
9968
|
+
return result;
|
|
9969
|
+
});
|
|
9970
|
+
}
|
|
9971
|
+
async function createActionCore(storage, auth, vargs, parent) {
|
|
9426
9972
|
const logger = vargs.logger;
|
|
9427
9973
|
logger?.group("storage createAction");
|
|
9428
9974
|
if (vargs.isTestWerrReviewActions) throwDummyReviewActions();
|
|
@@ -9442,57 +9988,119 @@ async function createAction(storage, auth, vargs, _originator) {
|
|
|
9442
9988
|
* - Create and return result.
|
|
9443
9989
|
*/
|
|
9444
9990
|
const userId = auth.userId;
|
|
9445
|
-
const { storageBeef, beef, xinputs } = await
|
|
9446
|
-
|
|
9447
|
-
|
|
9448
|
-
|
|
9449
|
-
|
|
9450
|
-
|
|
9451
|
-
userId,
|
|
9452
|
-
|
|
9453
|
-
|
|
9454
|
-
|
|
9455
|
-
|
|
9456
|
-
|
|
9457
|
-
|
|
9458
|
-
|
|
9991
|
+
const { storageBeef, beef, xinputs, xoutputs, changeBasket, noSendChangeIn } = await traceStorageStep(storage, "wallet.storage.create_action.validate", parent, {
|
|
9992
|
+
"action.fixed_input_count": vargs.inputs.length,
|
|
9993
|
+
"action.fixed_output_count": vargs.outputs.length
|
|
9994
|
+
}, async (span) => {
|
|
9995
|
+
const requiredInputs = await validateRequiredInputs(storage, userId, vargs);
|
|
9996
|
+
logger?.log("validated required inputs");
|
|
9997
|
+
const xoutputs = validateRequiredOutputs(storage, userId, vargs);
|
|
9998
|
+
logger?.log("validated required outputs");
|
|
9999
|
+
const changeBasketName = "default";
|
|
10000
|
+
const changeBasket = verifyOne(await storage.findOutputBaskets({ partial: {
|
|
10001
|
+
userId,
|
|
10002
|
+
name: changeBasketName
|
|
10003
|
+
} }), `Invalid outputGeneration basket "${changeBasketName}"`);
|
|
10004
|
+
logger?.log("found change basket");
|
|
10005
|
+
const noSendChangeIn = await validateNoSendChange(storage, userId, vargs, changeBasket);
|
|
10006
|
+
logger?.log("validated noSendChange");
|
|
10007
|
+
span?.end({ attributes: {
|
|
10008
|
+
"action.validated_input_count": requiredInputs.xinputs.length,
|
|
10009
|
+
"action.validated_output_count": xoutputs.length,
|
|
10010
|
+
"action.no_send_change_input_count": noSendChangeIn.length,
|
|
10011
|
+
"action.validated_beef_tx_count": requiredInputs.beef.txs.length
|
|
10012
|
+
} });
|
|
10013
|
+
return {
|
|
10014
|
+
...requiredInputs,
|
|
10015
|
+
xoutputs,
|
|
10016
|
+
changeBasket,
|
|
10017
|
+
noSendChangeIn
|
|
10018
|
+
};
|
|
10019
|
+
});
|
|
9459
10020
|
const feeModel = validateStorageFeeModel(storage.feeModel);
|
|
9460
10021
|
logger?.log(`validated fee model ${JSON.stringify(feeModel)}`);
|
|
9461
|
-
|
|
9462
|
-
|
|
10022
|
+
const initialFundingPlan = await prepareFundingPlan(storage, [
|
|
10023
|
+
userId,
|
|
10024
|
+
vargs,
|
|
10025
|
+
xinputs,
|
|
10026
|
+
xoutputs,
|
|
10027
|
+
changeBasket,
|
|
10028
|
+
noSendChangeIn,
|
|
10029
|
+
feeModel,
|
|
10030
|
+
parent
|
|
10031
|
+
]);
|
|
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();
|
|
9463
10035
|
let newTx;
|
|
10036
|
+
let newTxCommitted = false;
|
|
9464
10037
|
try {
|
|
9465
|
-
|
|
9466
|
-
|
|
9467
|
-
|
|
9468
|
-
|
|
9469
|
-
|
|
9470
|
-
|
|
9471
|
-
|
|
9472
|
-
|
|
9473
|
-
|
|
9474
|
-
|
|
9475
|
-
|
|
9476
|
-
|
|
9477
|
-
|
|
9478
|
-
|
|
9479
|
-
|
|
9480
|
-
|
|
9481
|
-
|
|
9482
|
-
|
|
9483
|
-
|
|
9484
|
-
|
|
9485
|
-
|
|
9486
|
-
|
|
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
|
+
};
|
|
10083
|
+
});
|
|
10084
|
+
newTxCommitted = true;
|
|
10085
|
+
const committedTx = verifyTruthy(newTx);
|
|
10086
|
+
const { allocatedChange, derivationPrefix, outputs, changeVouts, ctx } = persisted;
|
|
9487
10087
|
logger?.log("created new output records");
|
|
9488
|
-
const inputBeef = await mergeAllocatedChangeBeefs(storage,
|
|
10088
|
+
const inputBeef = await mergeAllocatedChangeBeefs(storage, vargs, allocatedChange, beef, allocatedBeefPrefetch, parent);
|
|
9489
10089
|
logger?.log("merged allocated change beefs");
|
|
9490
|
-
const inputs = await
|
|
10090
|
+
const inputs = await traceStorageStep(storage, "wallet.storage.create_action.assemble_inputs", parent, {
|
|
10091
|
+
"action.fixed_input_count": ctx.xinputs.length,
|
|
10092
|
+
"action.funding_input_count": allocatedChange.length,
|
|
10093
|
+
"action.include_source_transactions": vargs.includeAllSourceTransactions
|
|
10094
|
+
}, async (span) => {
|
|
10095
|
+
const assembled = await createNewInputs(storage, userId, vargs, ctx, allocatedChange);
|
|
10096
|
+
span?.end({ attributes: { "action.result_input_count": assembled.length } });
|
|
10097
|
+
return assembled;
|
|
10098
|
+
});
|
|
9491
10099
|
logger?.log("created new inputs");
|
|
9492
10100
|
const r = {
|
|
9493
|
-
reference:
|
|
9494
|
-
version:
|
|
9495
|
-
lockTime:
|
|
10101
|
+
reference: committedTx.reference,
|
|
10102
|
+
version: committedTx.version,
|
|
10103
|
+
lockTime: committedTx.lockTime,
|
|
9496
10104
|
inputs,
|
|
9497
10105
|
outputs,
|
|
9498
10106
|
derivationPrefix,
|
|
@@ -9502,9 +10110,15 @@ async function createAction(storage, auth, vargs, _originator) {
|
|
|
9502
10110
|
logger?.groupEnd();
|
|
9503
10111
|
return r;
|
|
9504
10112
|
} catch (error) {
|
|
10113
|
+
await allocatedBeefPrefetch;
|
|
9505
10114
|
if (newTx?.transactionId != null) try {
|
|
9506
|
-
|
|
9507
|
-
|
|
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
|
+
}
|
|
9508
10122
|
} catch (cleanupError) {
|
|
9509
10123
|
logger?.log(`failed to clean up createAction transaction ${newTx.transactionId}: ${String(cleanupError)}`);
|
|
9510
10124
|
}
|
|
@@ -9654,23 +10268,10 @@ async function getCompetingBeefForReview(storage, txid) {
|
|
|
9654
10268
|
throw e;
|
|
9655
10269
|
}
|
|
9656
10270
|
}
|
|
9657
|
-
/**
|
|
9658
|
-
|
|
9659
|
-
async function persistNewOutput(storage, o, tags, txTags, txBaskets) {
|
|
9660
|
-
o.outputId = await storage.insertOutput(o);
|
|
9661
|
-
const changeVout = o.change && o.purpose === "change" && o.providedBy === "storage" ? o.vout : void 0;
|
|
9662
|
-
for (const tagName of new Set(tags)) {
|
|
9663
|
-
const tag = txTags[tagName];
|
|
9664
|
-
await storage.insertOutputTagMap({
|
|
9665
|
-
outputId: verifyId(o.outputId),
|
|
9666
|
-
outputTagId: verifyId(tag.outputTagId),
|
|
9667
|
-
created_at: /* @__PURE__ */ new Date(),
|
|
9668
|
-
updated_at: /* @__PURE__ */ new Date(),
|
|
9669
|
-
isDeleted: false
|
|
9670
|
-
});
|
|
9671
|
-
}
|
|
10271
|
+
/** Build the SDK descriptor for a persisted output. */
|
|
10272
|
+
function describeNewOutput(o, tags, txBaskets) {
|
|
9672
10273
|
return {
|
|
9673
|
-
changeVout,
|
|
10274
|
+
changeVout: o.change && o.purpose === "change" && o.providedBy === "storage" ? o.vout : void 0,
|
|
9674
10275
|
ro: {
|
|
9675
10276
|
vout: verifyInteger(o.vout),
|
|
9676
10277
|
satoshis: _bsv_sdk.Validation.validateSatoshis(o.satoshis, "o.satoshis"),
|
|
@@ -9685,13 +10286,28 @@ async function persistNewOutput(storage, o, tags, txTags, txBaskets) {
|
|
|
9685
10286
|
}
|
|
9686
10287
|
};
|
|
9687
10288
|
}
|
|
9688
|
-
|
|
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) {
|
|
9689
10305
|
const txBaskets = {};
|
|
9690
10306
|
const basketNames = [...new Set(ctx.xoutputs.map((x) => x.basket).filter((v) => !!v))];
|
|
9691
|
-
Object.assign(txBaskets, await storage.findOrInsertOutputBasketsBulk(userId, basketNames));
|
|
10307
|
+
Object.assign(txBaskets, await storage.findOrInsertOutputBasketsBulk(userId, basketNames, trx));
|
|
9692
10308
|
const txTags = {};
|
|
9693
10309
|
const tagNames = [...new Set(ctx.xoutputs.flatMap((x) => x.tags))];
|
|
9694
|
-
Object.assign(txTags, await storage.findOrInsertOutputTagsBulk(userId, tagNames));
|
|
10310
|
+
Object.assign(txTags, await storage.findOrInsertOutputTagsBulk(userId, tagNames, trx));
|
|
9695
10311
|
const newOutputs = [];
|
|
9696
10312
|
for (const xo of ctx.xoutputs) {
|
|
9697
10313
|
const lockingScript = asArray(xo.lockingScript);
|
|
@@ -9707,7 +10323,7 @@ async function createNewOutputs(storage, userId, vargs, ctx, changeOutputs) {
|
|
|
9707
10323
|
created_at: now,
|
|
9708
10324
|
updated_at: now,
|
|
9709
10325
|
commissionId: 0
|
|
9710
|
-
});
|
|
10326
|
+
}, trx);
|
|
9711
10327
|
const o = makeDefaultOutput(userId, ctx.transactionId, xo.satoshis, xo.vout);
|
|
9712
10328
|
o.lockingScript = lockingScript;
|
|
9713
10329
|
o.providedBy = "storage";
|
|
@@ -9741,10 +10357,12 @@ async function createNewOutputs(storage, userId, vargs, ctx, changeOutputs) {
|
|
|
9741
10357
|
});
|
|
9742
10358
|
}
|
|
9743
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);
|
|
9744
10362
|
const outputs = [];
|
|
9745
10363
|
const changeVouts = [];
|
|
9746
10364
|
for (const { o, tags } of newOutputs) {
|
|
9747
|
-
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);
|
|
9748
10366
|
if (changeVout !== void 0) changeVouts.push(changeVout);
|
|
9749
10367
|
outputs.push(ro);
|
|
9750
10368
|
}
|
|
@@ -9753,7 +10371,7 @@ async function createNewOutputs(storage, userId, vargs, ctx, changeOutputs) {
|
|
|
9753
10371
|
changeVouts
|
|
9754
10372
|
};
|
|
9755
10373
|
}
|
|
9756
|
-
async function createNewTxRecord(storage, userId, vargs, storageBeef) {
|
|
10374
|
+
async function createNewTxRecord(storage, userId, vargs, storageBeef, satoshis = 0, trx, status = "unsigned") {
|
|
9757
10375
|
const now = /* @__PURE__ */ new Date();
|
|
9758
10376
|
const newTx = {
|
|
9759
10377
|
created_at: now,
|
|
@@ -9761,20 +10379,22 @@ async function createNewTxRecord(storage, userId, vargs, storageBeef) {
|
|
|
9761
10379
|
transactionId: 0,
|
|
9762
10380
|
version: vargs.version,
|
|
9763
10381
|
lockTime: vargs.lockTime,
|
|
9764
|
-
status
|
|
10382
|
+
status,
|
|
9765
10383
|
reference: randomBytesBase64(12),
|
|
9766
|
-
satoshis
|
|
10384
|
+
satoshis,
|
|
9767
10385
|
userId,
|
|
9768
10386
|
isOutgoing: true,
|
|
9769
|
-
inputBEEF: storageBeef
|
|
10387
|
+
inputBEEF: storageBeef,
|
|
9770
10388
|
description: vargs.description,
|
|
9771
10389
|
txid: void 0,
|
|
9772
10390
|
rawTx: void 0
|
|
9773
10391
|
};
|
|
9774
|
-
newTx.transactionId = await storage.insertTransaction(newTx);
|
|
9775
|
-
|
|
9776
|
-
|
|
9777
|
-
|
|
10392
|
+
newTx.transactionId = await storage.insertTransaction(newTx, trx);
|
|
10393
|
+
const labelNames = [...new Set(vargs.labels)];
|
|
10394
|
+
const labels = await storage.findOrInsertTxLabelsBulk(userId, labelNames, trx);
|
|
10395
|
+
for (const label of labelNames) {
|
|
10396
|
+
const txLabel = labels[label];
|
|
10397
|
+
await storage.findOrInsertTxLabelMap(verifyId(newTx.transactionId), verifyId(txLabel.txLabelId), trx);
|
|
9778
10398
|
}
|
|
9779
10399
|
return newTx;
|
|
9780
10400
|
}
|
|
@@ -9958,87 +10578,227 @@ async function validateNoSendChange(storage, userId, vargs, changeBasket) {
|
|
|
9958
10578
|
const r = [];
|
|
9959
10579
|
if (!vargs.isNoSend) return [];
|
|
9960
10580
|
const noSendChange = vargs.options.noSendChange;
|
|
9961
|
-
if (noSendChange && noSendChange.length > 0)
|
|
9962
|
-
const
|
|
9963
|
-
|
|
9964
|
-
|
|
9965
|
-
|
|
9966
|
-
|
|
9967
|
-
|
|
9968
|
-
|
|
9969
|
-
r.push(output);
|
|
10581
|
+
if (noSendChange && noSendChange.length > 0) {
|
|
10582
|
+
const byOutpoint = await storage.findOutputsByOutpoints(userId, noSendChange);
|
|
10583
|
+
for (const op of noSendChange) {
|
|
10584
|
+
const output = byOutpoint[`${op.txid}.${op.vout}`];
|
|
10585
|
+
if (!isAutoSpendableChangeOutput(output) || !verifyNumber(output.satoshis) || output.basketId !== changeBasket.basketId) throw new WERR_INVALID_PARAMETER("noSendChange outpoint", "wallet-managed BRC-29 change");
|
|
10586
|
+
if (r.some((o) => o.outputId === output.outputId)) throw new WERR_INVALID_PARAMETER("noSendChange outpoint", "unique. Duplicates are not allowed.");
|
|
10587
|
+
r.push(output);
|
|
10588
|
+
}
|
|
9970
10589
|
}
|
|
9971
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");
|
|
9972
10591
|
return r;
|
|
9973
10592
|
}
|
|
9974
|
-
|
|
9975
|
-
|
|
9976
|
-
|
|
9977
|
-
|
|
9978
|
-
|
|
9979
|
-
|
|
9980
|
-
|
|
9981
|
-
|
|
9982
|
-
|
|
9983
|
-
|
|
9984
|
-
|
|
9985
|
-
|
|
9986
|
-
|
|
9987
|
-
|
|
9988
|
-
|
|
9989
|
-
|
|
9990
|
-
|
|
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
|
+
}
|
|
10596
|
+
var FundingClaimConflict = class extends Error {
|
|
10597
|
+
conflict;
|
|
10598
|
+
constructor(conflict) {
|
|
10599
|
+
super("createAction funding claim changed concurrently");
|
|
10600
|
+
this.conflict = conflict;
|
|
10601
|
+
}
|
|
10602
|
+
};
|
|
10603
|
+
async function traceStorageStep(storage, name, parent, attributes, callback) {
|
|
10604
|
+
if (!storage.telemetry.enabled) return await callback();
|
|
10605
|
+
return await storage.telemetry.withSpan(name, {
|
|
10606
|
+
component: "wallet-storage",
|
|
10607
|
+
parent: parent?.context,
|
|
10608
|
+
attributes
|
|
10609
|
+
}, async (span) => await callback(span));
|
|
10610
|
+
}
|
|
10611
|
+
function makeFundingParams(vargs, xinputs, xoutputs, changeBasket, feeModel, availableChangeCount) {
|
|
10612
|
+
return {
|
|
10613
|
+
fixedInputs: xinputs.map((input) => ({
|
|
10614
|
+
satoshis: input.satoshis,
|
|
10615
|
+
unlockingScriptLength: input.unlockingScriptLength
|
|
9991
10616
|
})),
|
|
9992
|
-
fixedOutputs:
|
|
9993
|
-
satoshis:
|
|
9994
|
-
lockingScriptLength:
|
|
10617
|
+
fixedOutputs: xoutputs.map((output) => ({
|
|
10618
|
+
satoshis: output.satoshis,
|
|
10619
|
+
lockingScriptLength: output.lockingScript.length / 2
|
|
9995
10620
|
})),
|
|
9996
|
-
feeModel
|
|
9997
|
-
changeInitialSatoshis: Math.max(1,
|
|
9998
|
-
changeFirstSatoshis: Math.max(1, Math.round(
|
|
10621
|
+
feeModel,
|
|
10622
|
+
changeInitialSatoshis: Math.max(1, changeBasket.minimumDesiredUTXOValue),
|
|
10623
|
+
changeFirstSatoshis: Math.max(1, Math.round(changeBasket.minimumDesiredUTXOValue / 4)),
|
|
9999
10624
|
changeLockingScriptLength: 25,
|
|
10000
10625
|
changeUnlockingScriptLength: 107,
|
|
10001
|
-
targetNetCount:
|
|
10626
|
+
targetNetCount: changeBasket.numberOfDesiredUTXOs - availableChangeCount,
|
|
10002
10627
|
randomVals: vargs.randomVals
|
|
10003
10628
|
};
|
|
10004
|
-
|
|
10005
|
-
|
|
10006
|
-
const
|
|
10007
|
-
|
|
10008
|
-
|
|
10009
|
-
|
|
10010
|
-
|
|
10011
|
-
|
|
10012
|
-
|
|
10013
|
-
|
|
10014
|
-
|
|
10015
|
-
|
|
10629
|
+
}
|
|
10630
|
+
async function prepareFundingPlan(storage, context) {
|
|
10631
|
+
const [userId, vargs, xinputs, xoutputs, changeBasket, noSendChangeIn, feeModel, parent, trx] = context;
|
|
10632
|
+
const excludeSending = !vargs.isDelayed;
|
|
10633
|
+
const candidates = await traceStorageStep(storage, "wallet.storage.create_action.funding_candidates", parent, { "funding.exclude_sending": excludeSending }, async (span) => {
|
|
10634
|
+
const outputs = await storage.findAvailableManagedChangeInputCandidates(userId, changeBasket.basketId, excludeSending, trx);
|
|
10635
|
+
span?.end({ attributes: {
|
|
10636
|
+
"funding.candidate_count": outputs.length,
|
|
10637
|
+
"funding.candidate_satoshis": outputs.reduce((sum, output) => sum + output.satoshis, 0)
|
|
10638
|
+
} });
|
|
10639
|
+
return outputs;
|
|
10640
|
+
});
|
|
10641
|
+
const noSendIds = new Set(noSendChangeIn.map((output) => output.outputId));
|
|
10642
|
+
const available = candidates.filter((output) => !noSendIds.has(output.outputId));
|
|
10643
|
+
const params = makeFundingParams(vargs, xinputs, xoutputs, changeBasket, feeModel, candidates.length);
|
|
10644
|
+
return await traceStorageStep(storage, "wallet.storage.create_action.funding_plan", parent, {
|
|
10645
|
+
"funding.candidate_count": available.length,
|
|
10646
|
+
"funding.no_send_change_count": noSendChangeIn.length
|
|
10647
|
+
}, async (span) => {
|
|
10648
|
+
const allocated = /* @__PURE__ */ new Map();
|
|
10649
|
+
const availableSelector = new CanonicalChangeSelector(available);
|
|
10650
|
+
const noSend = [...noSendChangeIn];
|
|
10651
|
+
const noSendById = new Map(noSendChangeIn.map((output) => [output.outputId, output]));
|
|
10652
|
+
const allocate = async (targetSatoshis, exactSatoshis) => {
|
|
10653
|
+
let output = noSend.pop();
|
|
10654
|
+
output ??= availableSelector.take(targetSatoshis, exactSatoshis);
|
|
10655
|
+
if (output == null) return void 0;
|
|
10656
|
+
allocated.set(output.outputId, output);
|
|
10016
10657
|
return {
|
|
10017
|
-
outputId:
|
|
10018
|
-
satoshis:
|
|
10658
|
+
outputId: output.outputId,
|
|
10659
|
+
satoshis: output.satoshis
|
|
10019
10660
|
};
|
|
10661
|
+
};
|
|
10662
|
+
const release = async (outputId) => {
|
|
10663
|
+
if (allocated.get(outputId) == null) return;
|
|
10664
|
+
allocated.delete(outputId);
|
|
10665
|
+
availableSelector.release(outputId);
|
|
10666
|
+
const noSendOutput = noSendById.get(outputId);
|
|
10667
|
+
if (noSendOutput != null) noSend.push(noSendOutput);
|
|
10668
|
+
};
|
|
10669
|
+
const result = await generateChangeSdk(params, allocate, release, vargs.logger, storage.telemetry);
|
|
10670
|
+
const selected = result.allocatedChangeInputs.map((input) => verifyTruthy(allocated.get(input.outputId)));
|
|
10671
|
+
span?.end({ attributes: {
|
|
10672
|
+
"funding.allocated_input_count": selected.length,
|
|
10673
|
+
"funding.change_output_count": result.changeOutputs.length,
|
|
10674
|
+
"funding.fee_satoshis": result.fee,
|
|
10675
|
+
"funding.transaction_size_bytes": result.size
|
|
10676
|
+
} });
|
|
10677
|
+
return {
|
|
10678
|
+
params,
|
|
10679
|
+
result,
|
|
10680
|
+
selected,
|
|
10681
|
+
availableChangeCount: candidates.length
|
|
10682
|
+
};
|
|
10683
|
+
});
|
|
10684
|
+
}
|
|
10685
|
+
async function claimFundingPlan(storage, request) {
|
|
10686
|
+
const [userId, basketId, excludeSending, transactionId, noSendChangeIn, plan, trx] = request;
|
|
10687
|
+
if (plan.selected.length === 0) return {
|
|
10688
|
+
outputs: [],
|
|
10689
|
+
sourceTransactionCount: 0,
|
|
10690
|
+
hydratedScriptCount: 0,
|
|
10691
|
+
scriptSourceTransactionCount: 0
|
|
10692
|
+
};
|
|
10693
|
+
const noSendIds = new Set(noSendChangeIn.map((output) => output.outputId));
|
|
10694
|
+
const statuses = ["completed", "unproven"];
|
|
10695
|
+
if (!excludeSending) statuses.push("sending");
|
|
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))];
|
|
10699
|
+
const claimed = [];
|
|
10700
|
+
for (const planned of plan.selected) {
|
|
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" };
|
|
10703
|
+
claimed.push(current);
|
|
10704
|
+
}
|
|
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");
|
|
10706
|
+
for (const output of claimed) {
|
|
10707
|
+
output.spendable = false;
|
|
10708
|
+
output.spentBy = transactionId;
|
|
10020
10709
|
}
|
|
10021
|
-
const basketId = ctx.changeBasket.basketId;
|
|
10022
|
-
const o = await storage.allocateChangeInput(userId, basketId, targetSatoshis, exactSatoshis, !vargs.isDelayed, ctx.transactionId);
|
|
10023
|
-
if (o == null) return void 0;
|
|
10024
|
-
outputs[o.outputId] = o;
|
|
10025
10710
|
return {
|
|
10026
|
-
|
|
10027
|
-
|
|
10711
|
+
outputs: claimed,
|
|
10712
|
+
sourceTransactionCount: transactionIds.length
|
|
10028
10713
|
};
|
|
10714
|
+
}, trx).catch((error) => {
|
|
10715
|
+
if (error instanceof FundingClaimConflict) return { conflict: error.conflict };
|
|
10716
|
+
throw error;
|
|
10717
|
+
});
|
|
10718
|
+
if (claim.outputs == null) return claim;
|
|
10719
|
+
const hydration = await hydrateFundingInputScripts(storage, claim.outputs, trx);
|
|
10720
|
+
return {
|
|
10721
|
+
outputs: claim.outputs,
|
|
10722
|
+
sourceTransactionCount: claim.sourceTransactionCount,
|
|
10723
|
+
...hydration
|
|
10029
10724
|
};
|
|
10030
|
-
|
|
10031
|
-
|
|
10032
|
-
|
|
10033
|
-
|
|
10034
|
-
|
|
10725
|
+
}
|
|
10726
|
+
async function hydrateFundingInputScripts(storage, outputs, trx) {
|
|
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 !== "");
|
|
10728
|
+
if (missing.length === 0) return {
|
|
10729
|
+
hydratedScriptCount: 0,
|
|
10730
|
+
scriptSourceTransactionCount: 0
|
|
10731
|
+
};
|
|
10732
|
+
const byTxid = /* @__PURE__ */ new Map();
|
|
10733
|
+
for (const output of missing) {
|
|
10734
|
+
const txid = verifyTruthy(output.txid);
|
|
10735
|
+
const group = byTxid.get(txid) ?? [];
|
|
10736
|
+
group.push(output);
|
|
10737
|
+
byTxid.set(txid, group);
|
|
10738
|
+
}
|
|
10739
|
+
const groups = [...byTxid.entries()];
|
|
10740
|
+
let cursor = 0;
|
|
10741
|
+
await Promise.all(Array.from({ length: Math.min(8, groups.length) }, async () => {
|
|
10742
|
+
while (cursor < groups.length) {
|
|
10743
|
+
const [txid, group] = groups[cursor++];
|
|
10744
|
+
if (group.length === 1) {
|
|
10745
|
+
await storage.validateOutputScript(group[0], trx);
|
|
10746
|
+
continue;
|
|
10747
|
+
}
|
|
10748
|
+
const rawTx = await storage.getRawTxOfKnownValidTransaction(txid, void 0, void 0, trx);
|
|
10749
|
+
if (rawTx != null) for (const output of group) output.lockingScript = rawTx.slice(output.scriptOffset, output.scriptOffset + output.scriptLength);
|
|
10750
|
+
else for (const output of group) await storage.validateOutputScript(output, trx);
|
|
10035
10751
|
}
|
|
10036
|
-
|
|
10037
|
-
|
|
10038
|
-
|
|
10039
|
-
|
|
10752
|
+
}));
|
|
10753
|
+
return {
|
|
10754
|
+
hydratedScriptCount: missing.filter((output) => output.lockingScript?.length === output.scriptLength).length,
|
|
10755
|
+
scriptSourceTransactionCount: groups.length
|
|
10040
10756
|
};
|
|
10041
|
-
|
|
10757
|
+
}
|
|
10758
|
+
async function fundNewTransactionSdk(storage, userId, vargs, ctx, initialPlan, parent, trx) {
|
|
10759
|
+
let plan = initialPlan;
|
|
10760
|
+
let allocatedChange;
|
|
10761
|
+
let retryCount = 0;
|
|
10762
|
+
await traceStorageStep(storage, "wallet.storage.create_action.funding_claim", parent, { "funding.planned_input_count": initialPlan.selected.length }, async (span) => {
|
|
10763
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
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
|
+
]);
|
|
10773
|
+
if (claim.outputs != null) {
|
|
10774
|
+
allocatedChange = claim.outputs;
|
|
10775
|
+
span?.end({ attributes: {
|
|
10776
|
+
"funding.claim_retry_count": retryCount,
|
|
10777
|
+
"funding.source_transaction_count": claim.sourceTransactionCount,
|
|
10778
|
+
"funding.hydrated_script_count": claim.hydratedScriptCount,
|
|
10779
|
+
"funding.script_source_transaction_count": claim.scriptSourceTransactionCount
|
|
10780
|
+
} });
|
|
10781
|
+
return;
|
|
10782
|
+
}
|
|
10783
|
+
if (claim.conflict === "noSendChange") throw new WERR_INVALID_PARAMETER("noSendChange", "outputs that remain spendable during action planning");
|
|
10784
|
+
retryCount++;
|
|
10785
|
+
plan = await prepareFundingPlan(storage, [
|
|
10786
|
+
userId,
|
|
10787
|
+
vargs,
|
|
10788
|
+
ctx.xinputs,
|
|
10789
|
+
ctx.xoutputs,
|
|
10790
|
+
ctx.changeBasket,
|
|
10791
|
+
ctx.noSendChangeIn,
|
|
10792
|
+
ctx.feeModel,
|
|
10793
|
+
parent,
|
|
10794
|
+
trx
|
|
10795
|
+
]);
|
|
10796
|
+
}
|
|
10797
|
+
throw new WERR_INVALID_OPERATION("wallet funding changed repeatedly during action planning; retry createAction");
|
|
10798
|
+
});
|
|
10799
|
+
if (allocatedChange == null) throw new WERR_INTERNAL("funding plan was not claimed");
|
|
10800
|
+
const params = plan.params;
|
|
10801
|
+
const gcr = plan.result;
|
|
10042
10802
|
const nextRandomVal = () => {
|
|
10043
10803
|
let val = 0;
|
|
10044
10804
|
if (vargs.randomVals == null || vargs.randomVals.length === 0) {
|
|
@@ -10066,7 +10826,7 @@ async function fundNewTransactionSdk(storage, userId, vargs, ctx) {
|
|
|
10066
10826
|
const derivationPrefix = randomDerivation(16);
|
|
10067
10827
|
return {
|
|
10068
10828
|
maxPossibleSatoshisAdjustment: gcr.maxPossibleSatoshisAdjustment,
|
|
10069
|
-
allocatedChange
|
|
10829
|
+
allocatedChange,
|
|
10070
10830
|
changeOutputs: gcr.changeOutputs.map((o, i) => ({
|
|
10071
10831
|
created_at: /* @__PURE__ */ new Date(),
|
|
10072
10832
|
updated_at: /* @__PURE__ */ new Date(),
|
|
@@ -10101,12 +10861,73 @@ async function fundNewTransactionSdk(storage, userId, vargs, ctx) {
|
|
|
10101
10861
|
*/
|
|
10102
10862
|
function trimInputBeef(beef, vargs) {
|
|
10103
10863
|
if (vargs.options.returnTXIDOnly) return void 0;
|
|
10104
|
-
const
|
|
10105
|
-
for (const
|
|
10106
|
-
for (const txid of beef.txs.map((btx) => btx.txid)) if (knownTxids[txid]) beef.makeTxidOnly(txid);
|
|
10864
|
+
const hasKnownTxid = makeKnownTxidLookup(vargs.options.knownTxids ?? []);
|
|
10865
|
+
for (const btx of beef.txs) if (hasKnownTxid(btx.txid)) beef.makeTxidOnly(btx.txid);
|
|
10107
10866
|
return beef.toUint8Array();
|
|
10108
10867
|
}
|
|
10109
|
-
|
|
10868
|
+
function makeKnownTxidLookup(knownTxids) {
|
|
10869
|
+
let lookups = 0;
|
|
10870
|
+
let indexed;
|
|
10871
|
+
return (txid) => {
|
|
10872
|
+
lookups++;
|
|
10873
|
+
if (indexed != null) return indexed.has(txid);
|
|
10874
|
+
if (knownTxids.length > 64 && lookups > 4) {
|
|
10875
|
+
indexed = new Set(knownTxids);
|
|
10876
|
+
return indexed.has(txid);
|
|
10877
|
+
}
|
|
10878
|
+
return knownTxids.includes(txid);
|
|
10879
|
+
};
|
|
10880
|
+
}
|
|
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) {
|
|
10110
10931
|
const options = {
|
|
10111
10932
|
trustSelf: void 0,
|
|
10112
10933
|
knownTxids: vargs.options.knownTxids,
|
|
@@ -10117,25 +10938,52 @@ async function mergeAllocatedChangeBeefs(storage, userId, vargs, allocatedChange
|
|
|
10117
10938
|
minProofLevel: void 0
|
|
10118
10939
|
};
|
|
10119
10940
|
if (vargs.options.returnTXIDOnly) return void 0;
|
|
10120
|
-
const
|
|
10121
|
-
const
|
|
10122
|
-
const
|
|
10123
|
-
|
|
10124
|
-
|
|
10125
|
-
|
|
10126
|
-
|
|
10127
|
-
|
|
10128
|
-
|
|
10129
|
-
|
|
10130
|
-
|
|
10131
|
-
|
|
10132
|
-
|
|
10133
|
-
|
|
10134
|
-
|
|
10135
|
-
|
|
10136
|
-
beef.
|
|
10137
|
-
|
|
10138
|
-
|
|
10941
|
+
const knownTxids = vargs.options.knownTxids ?? [];
|
|
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;
|
|
10953
|
+
await traceStorageStep(storage, "wallet.storage.create_action.beef_fetch", parent, {
|
|
10954
|
+
"beef.allocated_change_count": allocatedChange.length,
|
|
10955
|
+
"beef.distinct_source_count": new Set(allocatedChange.map((output) => output.txid)).size,
|
|
10956
|
+
"beef.known_txid_count": knownTxids.length,
|
|
10957
|
+
"beef.missing_source_count": missing.length,
|
|
10958
|
+
"beef.fetch_concurrency": 1,
|
|
10959
|
+
"beef.prefetch_reused": usePrefetch
|
|
10960
|
+
}, async (span) => {
|
|
10961
|
+
if (missing.length > 0) fetched = await storage.getBeefForTransactions(missing, {
|
|
10962
|
+
...options,
|
|
10963
|
+
mergeToBeef: void 0
|
|
10964
|
+
});
|
|
10965
|
+
span?.end({ attributes: {
|
|
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
|
|
10969
|
+
} });
|
|
10970
|
+
});
|
|
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);
|
|
10973
|
+
span?.end({ attributes: {
|
|
10974
|
+
"beef.merged_tx_count": beef.txs.length,
|
|
10975
|
+
"beef.merged_bump_count": beef.bumps.length
|
|
10976
|
+
} });
|
|
10977
|
+
});
|
|
10978
|
+
return await traceStorageStep(storage, "wallet.storage.create_action.beef_trim_serialize", parent, {
|
|
10979
|
+
"beef.tx_count": beef.txs.length,
|
|
10980
|
+
"beef.bump_count": beef.bumps.length,
|
|
10981
|
+
"beef.known_txid_count": knownTxids.length
|
|
10982
|
+
}, async (span) => {
|
|
10983
|
+
const result = trimInputBeef(beef, vargs);
|
|
10984
|
+
span?.end({ attributes: { "beef.result_bytes": result?.length ?? 0 } });
|
|
10985
|
+
return result;
|
|
10986
|
+
});
|
|
10139
10987
|
}
|
|
10140
10988
|
const dirtyHashLookup = {
|
|
10141
10989
|
"00000000000000000019f112ec0a9982926f1258cdcc558dd7c3b7e5dc7fa148": "This is the first header of the invalid SegWit chain.",
|
|
@@ -12750,21 +13598,6 @@ async function cleanupExpiredActionBatches(storage) {
|
|
|
12750
13598
|
});
|
|
12751
13599
|
return released;
|
|
12752
13600
|
}
|
|
12753
|
-
async function availableManagedChange(storage, userId, basketId, excludeSending, trx) {
|
|
12754
|
-
const statuses = ["completed", "unproven"];
|
|
12755
|
-
if (!excludeSending) statuses.push("sending");
|
|
12756
|
-
const outputs = (await storage.findOutputs({
|
|
12757
|
-
partial: {
|
|
12758
|
-
userId,
|
|
12759
|
-
basketId,
|
|
12760
|
-
spendable: true
|
|
12761
|
-
},
|
|
12762
|
-
txStatus: statuses,
|
|
12763
|
-
trx
|
|
12764
|
-
})).filter(isAutoSpendableChangeOutput);
|
|
12765
|
-
const reserved = new Set(await storage.findReservedActionBatchOutputIds(outputs.map((o) => o.outputId), trx));
|
|
12766
|
-
return outputs.filter((output) => output.spentBy == null && !reserved.has(output.outputId));
|
|
12767
|
-
}
|
|
12768
13601
|
function sourceOutputFromBeef(beef, outpoint) {
|
|
12769
13602
|
const output = (beef.findTxid(outpoint.txid)?.tx)?.outputs[outpoint.vout];
|
|
12770
13603
|
if (output == null) return void 0;
|
|
@@ -12929,7 +13762,7 @@ async function beginActionBatch(storage, auth, args) {
|
|
|
12929
13762
|
const explicit = await resolveExplicitOutputs(storage, userId, args.firstAction, outputScriptLengths != null);
|
|
12930
13763
|
const noSendChange = await resolveNoSendChangeOutputs(storage, userId, args.firstAction);
|
|
12931
13764
|
const fixedOutputIds = new Set([...explicit.outputs, ...noSendChange.outputs].map((output) => output.outputId));
|
|
12932
|
-
const available = (await
|
|
13765
|
+
const available = (await storage.findAvailableManagedChangeInputs(userId, changeBasket.basketId, !args.firstAction.isDelayed)).filter((output) => !fixedOutputIds.has(output.outputId));
|
|
12933
13766
|
const target = estimateFirstActionTarget(storage, args.firstAction, explicit.inputSatoshis + noSendChange.inputSatoshis, outputScriptLengths);
|
|
12934
13767
|
const fixedOutputs = [...explicit.outputs, ...noSendChange.outputs];
|
|
12935
13768
|
const funding = chooseReservationPool(available, target, Math.max(0, INITIAL_RESERVATION_LIMIT - fixedOutputs.length), INITIAL_EXTRA_OUTPUTS, false, reservationPlanningCosts(storage, changeBasket));
|
|
@@ -12973,7 +13806,7 @@ async function extendActionBatch(storage, auth, args) {
|
|
|
12973
13806
|
name: "default"
|
|
12974
13807
|
} }));
|
|
12975
13808
|
const alreadyReserved = await storage.findActionBatchOutputIds(batch.actionBatchId);
|
|
12976
|
-
const available = await
|
|
13809
|
+
const available = await storage.findAvailableManagedChangeInputs(userId, basket.basketId, false);
|
|
12977
13810
|
if (!Number.isSafeInteger(args.requestedOutputs) || args.requestedOutputs < 0) throw new WERR_INVALID_PARAMETER("requestedOutputs", "non-negative safe integer");
|
|
12978
13811
|
const requestedCount = Math.min(args.requestedOutputs, 64);
|
|
12979
13812
|
const funding = chooseReservationPool(available, Math.max(1, args.targetSatoshis), requestedCount, 0, true, reservationPlanningCosts(storage, basket));
|
|
@@ -13346,6 +14179,31 @@ async function abortActionBatch(storage, auth, batchId) {
|
|
|
13346
14179
|
});
|
|
13347
14180
|
}
|
|
13348
14181
|
//#endregion
|
|
14182
|
+
//#region ../src/storage/methods/availableManagedChange.ts
|
|
14183
|
+
/**
|
|
14184
|
+
* Return the exact set of wallet-managed outputs currently eligible for
|
|
14185
|
+
* automatic funding. Keeping this predicate shared prevents the planner,
|
|
14186
|
+
* allocator, action-batch reservations, and availability count from drifting.
|
|
14187
|
+
*/
|
|
14188
|
+
async function availableManagedChange(storage, userId, basketId, excludeSending, trx) {
|
|
14189
|
+
const statuses = ["completed", "unproven"];
|
|
14190
|
+
if (!excludeSending) statuses.push("sending");
|
|
14191
|
+
const outputs = (await storage.findOutputs({
|
|
14192
|
+
partial: {
|
|
14193
|
+
userId,
|
|
14194
|
+
basketId,
|
|
14195
|
+
spendable: true,
|
|
14196
|
+
...managedChangeOutputFields
|
|
14197
|
+
},
|
|
14198
|
+
txStatus: statuses,
|
|
14199
|
+
noScript: true,
|
|
14200
|
+
trx
|
|
14201
|
+
})).filter(isAutoSpendableChangeOutput);
|
|
14202
|
+
if (outputs.length === 0) return outputs;
|
|
14203
|
+
const reserved = new Set(await storage.findReservedActionBatchOutputIds(outputs.map((output) => output.outputId), trx));
|
|
14204
|
+
return outputs.filter((output) => !reserved.has(output.outputId));
|
|
14205
|
+
}
|
|
14206
|
+
//#endregion
|
|
13349
14207
|
//#region ../src/storage/StorageProvider.ts
|
|
13350
14208
|
var StorageProvider = class StorageProvider extends StorageReaderWriter {
|
|
13351
14209
|
isDirty = false;
|
|
@@ -13379,6 +14237,81 @@ var StorageProvider = class StorageProvider extends StorageReaderWriter {
|
|
|
13379
14237
|
this.maxRecursionDepth = 12;
|
|
13380
14238
|
this.scriptVerifier = options.scriptVerifier;
|
|
13381
14239
|
}
|
|
14240
|
+
/** Mark a planned set of change inputs spent within the caller's transaction. */
|
|
14241
|
+
async markChangeInputsSpent(outputIds, transactionId, trx) {
|
|
14242
|
+
let updated = 0;
|
|
14243
|
+
const current = await this.findOutputsByIds(outputIds, trx);
|
|
14244
|
+
for (const outputId of outputIds) {
|
|
14245
|
+
const output = current[outputId];
|
|
14246
|
+
if (output == null || !output.spendable || output.spentBy != null) continue;
|
|
14247
|
+
updated += await this.updateOutput(outputId, {
|
|
14248
|
+
spendable: false,
|
|
14249
|
+
spentBy: transactionId
|
|
14250
|
+
}, trx);
|
|
14251
|
+
}
|
|
14252
|
+
return updated;
|
|
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
|
+
}
|
|
14262
|
+
/** Return unreserved wallet-managed outputs eligible for automatic funding. */
|
|
14263
|
+
async findAvailableManagedChangeInputs(userId, basketId, excludeSending, trx) {
|
|
14264
|
+
return await availableManagedChange(this, userId, basketId, excludeSending, trx);
|
|
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
|
+
}
|
|
14276
|
+
/** Read the current status of a set of source transactions without loading raw transaction bytes. */
|
|
14277
|
+
async findTransactionStatusesByIds(userId, transactionIds, trx) {
|
|
14278
|
+
const statuses = /* @__PURE__ */ new Map();
|
|
14279
|
+
for (const transactionId of new Set(transactionIds)) {
|
|
14280
|
+
const transaction = await this.findTransactionById(transactionId, trx, true);
|
|
14281
|
+
if (transaction?.userId === userId) statuses.set(transactionId, transaction.status);
|
|
14282
|
+
}
|
|
14283
|
+
return statuses;
|
|
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
|
+
}
|
|
13382
14315
|
async insertActionBatch(_batch, _trx) {
|
|
13383
14316
|
throw new WERR_NOT_IMPLEMENTED();
|
|
13384
14317
|
}
|
|
@@ -13430,6 +14363,10 @@ var StorageProvider = class StorageProvider extends StorageReaderWriter {
|
|
|
13430
14363
|
supportsActionBatchPersistence() {
|
|
13431
14364
|
return false;
|
|
13432
14365
|
}
|
|
14366
|
+
/** Custom providers may require physical expiry cleanup before reservations are queried. */
|
|
14367
|
+
requiresActionBatchCleanupBeforeCreateAction() {
|
|
14368
|
+
return true;
|
|
14369
|
+
}
|
|
13433
14370
|
async beginActionBatch(auth, args) {
|
|
13434
14371
|
if (!this.supportsActionBatchPersistence()) throw new WERR_NOT_IMPLEMENTED("actionBatch capability is not available");
|
|
13435
14372
|
return await beginActionBatch(this, auth, args);
|
|
@@ -13489,7 +14426,7 @@ var StorageProvider = class StorageProvider extends StorageReaderWriter {
|
|
|
13489
14426
|
}
|
|
13490
14427
|
return byOutpoint;
|
|
13491
14428
|
}
|
|
13492
|
-
async findOutputsByOutpointsForUpdate(userId, outpoints, trx) {
|
|
14429
|
+
async findOutputsByOutpointsForUpdate(userId, outpoints, trx, _noScript = false) {
|
|
13493
14430
|
return await this.findOutputsByOutpoints(userId, outpoints, trx);
|
|
13494
14431
|
}
|
|
13495
14432
|
async findOrInsertOutputBasketsBulk(userId, names, trx) {
|
|
@@ -13818,7 +14755,7 @@ var StorageProvider = class StorageProvider extends StorageReaderWriter {
|
|
|
13818
14755
|
}
|
|
13819
14756
|
async createAction(auth, args) {
|
|
13820
14757
|
if (auth.userId == null) throw new WERR_UNAUTHORIZED();
|
|
13821
|
-
if (this.supportsActionBatchPersistence()) await cleanupExpiredActionBatches(this);
|
|
14758
|
+
if (this.supportsActionBatchPersistence() && this.requiresActionBatchCleanupBeforeCreateAction()) await cleanupExpiredActionBatches(this);
|
|
13822
14759
|
return await createAction(this, auth, args);
|
|
13823
14760
|
}
|
|
13824
14761
|
async processAction(auth, args) {
|
|
@@ -13908,6 +14845,9 @@ var StorageProvider = class StorageProvider extends StorageReaderWriter {
|
|
|
13908
14845
|
async getBeefForTransaction(txid, options) {
|
|
13909
14846
|
return await getBeefForTransaction(this, txid, options);
|
|
13910
14847
|
}
|
|
14848
|
+
async getBeefForTransactions(txids, options) {
|
|
14849
|
+
return await getBeefForTransactions(this, txids, options);
|
|
14850
|
+
}
|
|
13911
14851
|
async findMonitorEventById(id, trx) {
|
|
13912
14852
|
return verifyOneOrNone(await this.findMonitorEvents({
|
|
13913
14853
|
partial: { id },
|
|
@@ -14866,6 +15806,9 @@ var StorageIdb = class extends StorageProvider {
|
|
|
14866
15806
|
supportsActionBatchPersistence() {
|
|
14867
15807
|
return true;
|
|
14868
15808
|
}
|
|
15809
|
+
requiresActionBatchCleanupBeforeCreateAction() {
|
|
15810
|
+
return false;
|
|
15811
|
+
}
|
|
14869
15812
|
/**
|
|
14870
15813
|
* This method must be called at least once before any other method accesses the database,
|
|
14871
15814
|
* and each time the schema may have updated.
|
|
@@ -14926,9 +15869,16 @@ var StorageIdb = class extends StorageProvider {
|
|
|
14926
15869
|
async initDB(storageName, storageIdentityKey) {
|
|
14927
15870
|
const chain = this.chain;
|
|
14928
15871
|
const maxOutputScript = 1024;
|
|
14929
|
-
return await (0, idb.openDB)(this.dbName,
|
|
15872
|
+
return await (0, idb.openDB)(this.dbName, 3, { upgrade(db, _oldVersion, _newVersion, transaction) {
|
|
14930
15873
|
upgradeAllStoresV1(db);
|
|
14931
15874
|
upgradeActionBatchStoresV2(db);
|
|
15875
|
+
const outputs = transaction.objectStore("outputs");
|
|
15876
|
+
if (!outputs.indexNames.contains("userId_basketId")) outputs.createIndex("userId_basketId", ["userId", "basketId"]);
|
|
15877
|
+
if (!outputs.indexNames.contains("txid_vout_userId")) outputs.createIndex("txid_vout_userId", [
|
|
15878
|
+
"txid",
|
|
15879
|
+
"vout",
|
|
15880
|
+
"userId"
|
|
15881
|
+
], { unique: true });
|
|
14932
15882
|
if (!db.objectStoreNames.contains("settings")) {
|
|
14933
15883
|
if (storageName == null || storageName === "" || storageIdentityKey == null || storageIdentityKey === "") throw new WERR_INVALID_OPERATION("migrate must be called before first access");
|
|
14934
15884
|
const settings = db.createObjectStore("settings", { keyPath: "storageIdentityKey" });
|
|
@@ -15038,6 +15988,50 @@ var StorageIdb = class extends StorageProvider {
|
|
|
15038
15988
|
}
|
|
15039
15989
|
return r;
|
|
15040
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
|
+
}
|
|
15041
16035
|
async getRawTxOfKnownValidTransaction(txid, offset, length, trx) {
|
|
15042
16036
|
if (txid == null || txid === "") return void 0;
|
|
15043
16037
|
if (!this.isAvailable()) await this.makeAvailable();
|
|
@@ -15134,11 +16128,53 @@ var StorageIdb = class extends StorageProvider {
|
|
|
15134
16128
|
txStatus,
|
|
15135
16129
|
noScript: true
|
|
15136
16130
|
};
|
|
15137
|
-
|
|
16131
|
+
const outputIds = [];
|
|
15138
16132
|
await this.filterOutputs(args, (r) => {
|
|
15139
|
-
if (isAutoSpendableChangeOutput(r))
|
|
16133
|
+
if (isAutoSpendableChangeOutput(r)) outputIds.push(r.outputId);
|
|
15140
16134
|
});
|
|
15141
|
-
|
|
16135
|
+
const reserved = await this.findReservedActionBatchOutputIds(outputIds);
|
|
16136
|
+
return outputIds.length - reserved.length;
|
|
16137
|
+
}
|
|
16138
|
+
async findTransactionStatusesByIds(userId, transactionIds, trx) {
|
|
16139
|
+
const statuses = /* @__PURE__ */ new Map();
|
|
16140
|
+
if (transactionIds.length === 0) return statuses;
|
|
16141
|
+
const dbTrx = this.toDbTrx(["transactions"], "readonly", trx);
|
|
16142
|
+
const store = dbTrx.objectStore("transactions");
|
|
16143
|
+
for (const transactionId of new Set(transactionIds)) {
|
|
16144
|
+
const transaction = await store.get(transactionId);
|
|
16145
|
+
if (transaction?.userId === userId) statuses.set(transactionId, transaction.status);
|
|
16146
|
+
}
|
|
16147
|
+
if (trx == null) await dbTrx.done;
|
|
16148
|
+
return statuses;
|
|
16149
|
+
}
|
|
16150
|
+
async findOutputsByOutpointsInternal(userId, outpoints, trx, noScript = false) {
|
|
16151
|
+
const byOutpoint = {};
|
|
16152
|
+
if (outpoints.length === 0) return byOutpoint;
|
|
16153
|
+
const dbTrx = this.toDbTrx(noScript ? ["outputs"] : [
|
|
16154
|
+
"outputs",
|
|
16155
|
+
"proven_txs",
|
|
16156
|
+
"proven_tx_reqs"
|
|
16157
|
+
], "readonly", trx);
|
|
16158
|
+
const index = dbTrx.objectStore("outputs").index("txid_vout_userId");
|
|
16159
|
+
const unique = [...new Map(outpoints.map((outpoint) => [`${outpoint.txid}.${outpoint.vout}`, outpoint])).values()];
|
|
16160
|
+
const rows = await Promise.all(unique.map(async (outpoint) => await index.get([
|
|
16161
|
+
outpoint.txid,
|
|
16162
|
+
outpoint.vout,
|
|
16163
|
+
userId
|
|
16164
|
+
])));
|
|
16165
|
+
for (const row of rows) {
|
|
16166
|
+
if (row == null) continue;
|
|
16167
|
+
if (!noScript) await this.validateOutputScript(row, dbTrx);
|
|
16168
|
+
byOutpoint[`${String(row.txid)}.${row.vout}`] = this.validateEntity(row);
|
|
16169
|
+
}
|
|
16170
|
+
if (trx == null) await dbTrx.done;
|
|
16171
|
+
return byOutpoint;
|
|
16172
|
+
}
|
|
16173
|
+
async findOutputsByOutpoints(userId, outpoints, trx) {
|
|
16174
|
+
return await this.findOutputsByOutpointsInternal(userId, outpoints, trx);
|
|
16175
|
+
}
|
|
16176
|
+
async findOutputsByOutpointsForUpdate(userId, outpoints, trx, noScript = false) {
|
|
16177
|
+
return await this.findOutputsByOutpointsInternal(userId, outpoints, trx, noScript);
|
|
15142
16178
|
}
|
|
15143
16179
|
async findCertificatesAuth(auth, args) {
|
|
15144
16180
|
if (auth.userId == null || args.partial.userId != null && args.partial.userId !== 0 && args.partial.userId !== auth.userId) throw new WERR_UNAUTHORIZED();
|
|
@@ -15257,6 +16293,7 @@ var StorageIdb = class extends StorageProvider {
|
|
|
15257
16293
|
else cursor = await store.openCursor(null, direction);
|
|
15258
16294
|
await scanCursor(cursor, args.since, args.paged?.offset ?? 0, args.paged?.limit, async (r) => {
|
|
15259
16295
|
if (!matchesProvenTxPartial(r, args.partial)) return false;
|
|
16296
|
+
if (args.txids != null && args.txids.length > 0 && !args.txids.includes(r.txid)) return false;
|
|
15260
16297
|
if (userId !== void 0) {
|
|
15261
16298
|
if (await this.countTransactions({
|
|
15262
16299
|
partial: {
|
|
@@ -15701,11 +16738,18 @@ var StorageIdb = class extends StorageProvider {
|
|
|
15701
16738
|
return rows.map((r) => r.outputId);
|
|
15702
16739
|
}
|
|
15703
16740
|
async findReservedActionBatchOutputIds(outputIds, trx) {
|
|
15704
|
-
const tx = this.toDbTrx(["action_batch_outputs"], "readonly", trx);
|
|
16741
|
+
const tx = this.toDbTrx(["action_batch_outputs", "action_batches"], "readonly", trx);
|
|
15705
16742
|
const store = tx.objectStore("action_batch_outputs");
|
|
16743
|
+
const batchStore = tx.objectStore("action_batches");
|
|
15706
16744
|
if (store.get == null) throw new WERR_INTERNAL("IndexedDB action_batch_outputs store does not support get");
|
|
15707
16745
|
const reserved = [];
|
|
15708
|
-
|
|
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
|
+
}
|
|
15709
16753
|
if (trx == null) await tx.done;
|
|
15710
16754
|
return reserved;
|
|
15711
16755
|
}
|
|
@@ -15901,6 +16945,12 @@ var StorageIdb = class extends StorageProvider {
|
|
|
15901
16945
|
partial.vout,
|
|
15902
16946
|
partial.userId
|
|
15903
16947
|
], direction);
|
|
16948
|
+
if (partial?.txid != null && partial.txid !== "" && partial?.vout !== void 0) return store.index("txid_vout_userId").openCursor([
|
|
16949
|
+
partial.txid,
|
|
16950
|
+
partial.vout,
|
|
16951
|
+
partial.userId
|
|
16952
|
+
], direction);
|
|
16953
|
+
if (partial?.basketId !== void 0) return store.index("userId_basketId").openCursor([partial.userId, partial.basketId], direction);
|
|
15904
16954
|
return store.index("userId").openCursor(partial.userId, direction);
|
|
15905
16955
|
}
|
|
15906
16956
|
if (partial?.transactionId !== void 0) return store.index("transactionId").openCursor(partial.transactionId, direction);
|
|
@@ -15908,6 +16958,17 @@ var StorageIdb = class extends StorageProvider {
|
|
|
15908
16958
|
if (partial?.spentBy !== void 0) return store.index("spentBy").openCursor(partial.spentBy, direction);
|
|
15909
16959
|
return store.openCursor(null, direction);
|
|
15910
16960
|
}
|
|
16961
|
+
async eligibleOutputTransactionIds(args, dbTrx) {
|
|
16962
|
+
if (args.txStatus == null) return void 0;
|
|
16963
|
+
const validTransactionIds = /* @__PURE__ */ new Set();
|
|
16964
|
+
const transactions = dbTrx.objectStore("transactions");
|
|
16965
|
+
for (const status of args.txStatus) {
|
|
16966
|
+
const index = args.partial.userId === void 0 ? transactions.index("status") : transactions.index("status_userId");
|
|
16967
|
+
const key = args.partial.userId === void 0 ? status : [status, args.partial.userId];
|
|
16968
|
+
for (const transactionId of await index.getAllKeys(key)) validTransactionIds.add(Number(transactionId));
|
|
16969
|
+
}
|
|
16970
|
+
return validTransactionIds;
|
|
16971
|
+
}
|
|
15911
16972
|
async filterOutputs(args, filtered, tagIds, isQueryModeAll) {
|
|
15912
16973
|
this.assertNoUndefinedInPartial(args.partial);
|
|
15913
16974
|
if (args.partial.lockingScript != null) throw new WERR_INVALID_PARAMETER("args.partial.lockingScript", "undefined. Outputs may not be found by lockingScript value.");
|
|
@@ -15917,15 +16978,10 @@ var StorageIdb = class extends StorageProvider {
|
|
|
15917
16978
|
const dbTrx = this.toDbTrx(stores, "readonly", args.trx);
|
|
15918
16979
|
const direction = args.orderDescending === true ? "prev" : "next";
|
|
15919
16980
|
const store = dbTrx.objectStore("outputs");
|
|
16981
|
+
const validTransactionIds = await this.eligibleOutputTransactionIds(args, dbTrx);
|
|
15920
16982
|
await scanCursor(await this.openOutputsCursor(store, args.partial, direction), args.since, args.paged?.offset ?? 0, args.paged?.limit, async (r) => {
|
|
15921
16983
|
if (!matchesOutputPartial(r, args.partial)) return false;
|
|
15922
|
-
if (
|
|
15923
|
-
if (await this.countTransactions({
|
|
15924
|
-
partial: { transactionId: r.transactionId },
|
|
15925
|
-
status: args.txStatus,
|
|
15926
|
-
trx: dbTrx
|
|
15927
|
-
}) === 0) return false;
|
|
15928
|
-
}
|
|
16984
|
+
if (validTransactionIds != null && !validTransactionIds.has(r.transactionId)) return false;
|
|
15929
16985
|
if (tagIds != null && tagIds.length > 0 && !await this.outputMatchesTags(r.outputId, tagIds, isQueryModeAll, dbTrx)) return false;
|
|
15930
16986
|
return true;
|
|
15931
16987
|
}, (r) => {
|
|
@@ -27623,7 +28679,7 @@ function isValidProfile(value) {
|
|
|
27623
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;
|
|
27624
28680
|
}
|
|
27625
28681
|
/**
|
|
27626
|
-
* Raised when UMP
|
|
28682
|
+
* Raised when a UMP lookup yields neither a verified token nor a clean empty response.
|
|
27627
28683
|
*
|
|
27628
28684
|
* Callers must offer retry/recovery rather than treating this error as a new
|
|
27629
28685
|
* account. Diagnostics contain counts only and never hashes, keys, or tokens.
|
|
@@ -27721,49 +28777,119 @@ var OverlayUMPTokenInteractor = class {
|
|
|
27721
28777
|
throw new UMPTokenLookupError("lookup-unavailable", diagnostics, { cause: error });
|
|
27722
28778
|
}
|
|
27723
28779
|
const diagnostics = this.toLookupDiagnostics(resolution);
|
|
27724
|
-
if (resolution.answer.outputs.length === 0) {
|
|
27725
|
-
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)) {
|
|
27726
|
-
this.captureLookupFailure(lookupKind, "lookup-incomplete", diagnostics, startedAt);
|
|
27727
|
-
throw new UMPTokenLookupError("lookup-incomplete", diagnostics);
|
|
27728
|
-
}
|
|
27729
|
-
this.telemetry.capture({
|
|
27730
|
-
name: "wallet-toolbox.ump.lookup.completed",
|
|
27731
|
-
component: "wallet-toolbox.ump",
|
|
27732
|
-
severity: "info",
|
|
27733
|
-
correlationId: diagnostics.correlationId,
|
|
27734
|
-
attributes: {
|
|
27735
|
-
lookupKind,
|
|
27736
|
-
result: "not-found",
|
|
27737
|
-
durationMs: Date.now() - startedAt,
|
|
27738
|
-
...this.lookupDiagnosticAttributes(diagnostics)
|
|
27739
|
-
}
|
|
27740
|
-
});
|
|
27741
|
-
return;
|
|
27742
|
-
}
|
|
27743
28780
|
const tokens = this.parseLookupAnswers(resolution.answer);
|
|
27744
28781
|
const expectedHash = question.query[lookupKind === "presentation" ? "presentationHash" : "recoveryHash"].toLowerCase();
|
|
27745
|
-
|
|
27746
|
-
|
|
27747
|
-
|
|
27748
|
-
|
|
27749
|
-
|
|
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
|
+
}
|
|
27750
28789
|
const reason = "token-ambiguous";
|
|
27751
28790
|
this.captureLookupFailure(lookupKind, reason, diagnostics, startedAt);
|
|
27752
28791
|
throw new UMPTokenLookupError(reason, diagnostics);
|
|
27753
28792
|
}
|
|
27754
|
-
|
|
27755
|
-
|
|
27756
|
-
|
|
27757
|
-
|
|
27758
|
-
|
|
27759
|
-
|
|
27760
|
-
|
|
27761
|
-
|
|
27762
|
-
|
|
27763
|
-
|
|
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;
|
|
27764
28869
|
}
|
|
27765
|
-
}
|
|
27766
|
-
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
|
+
}
|
|
27767
28893
|
}
|
|
27768
28894
|
emptyLookupDiagnostics(correlationId) {
|
|
27769
28895
|
return {
|
|
@@ -27804,6 +28930,21 @@ var OverlayUMPTokenInteractor = class {
|
|
|
27804
28930
|
outputCount: diagnostics.outputCount
|
|
27805
28931
|
};
|
|
27806
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
|
+
}
|
|
27807
28948
|
captureLookupFailure(lookupKind, reason, diagnostics, startedAt, error) {
|
|
27808
28949
|
this.telemetry.capture({
|
|
27809
28950
|
name: "wallet-toolbox.ump.lookup.indeterminate",
|
|
@@ -28044,8 +29185,7 @@ var OverlayUMPTokenInteractor = class {
|
|
|
28044
29185
|
throw new UMPTokenLookupError("lookup-unavailable", diagnostics, { cause: error });
|
|
28045
29186
|
}
|
|
28046
29187
|
if (resolution.answer.outputs.length === 0) {
|
|
28047
|
-
|
|
28048
|
-
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) {
|
|
28049
29189
|
const diagnostics = this.toLookupDiagnostics(resolution);
|
|
28050
29190
|
this.captureLookupFailure("outpoint", "lookup-incomplete", diagnostics, startedAt);
|
|
28051
29191
|
throw new UMPTokenLookupError("lookup-incomplete", diagnostics);
|