@bsv/wallet-toolbox-client 2.4.22 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/out/index.client.cjs +1034 -264
- package/out/index.client.cjs.map +1 -1
- package/out/index.client.d.cts +66 -8
- package/out/index.client.d.cts.map +1 -1
- package/out/index.client.d.mts +66 -8
- package/out/index.client.d.mts.map +1 -1
- package/out/index.client.mjs +1035 -265
- package/out/index.client.mjs.map +1 -1
- package/package.json +2 -2
package/out/index.client.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
|
|
2
|
-
import { AuthFetch, BEEF_V1, BEEF_V2, Beef, BeefParty, BigNumber, BlockHeadersService, CachedKeyDeriver, Certificate, Curve, Hash, LocalKVStore, LockingScript, LookupResolver, MasterCertificate, MerklePath, P2PKH, PrivateKey, ProtoWallet, PublicKey, PushDrop, RPuzzle, Random, SHIPBroadcaster, Script, ScriptEvaluationError, Signature, Spend, SymmetricKey, Telemetry, Transaction, Utils, Validation, Validation as Validation$1, VerifiableCertificate, createNonce, defaultHttpClient, verifyNonce } from "@bsv/sdk";
|
|
2
|
+
import { AuthFetch, BEEF_V1, BEEF_V2, Beef, BeefParty, BigNumber, BlockHeadersService, CachedKeyDeriver, Certificate, Curve, Hash, LocalKVStore, LockingScript, LookupResolver, MasterCertificate, MerklePath, P2PKH, PrivateKey, ProtoWallet, PublicKey, PushDrop, RPuzzle, Random, SHIPBroadcaster, Script, ScriptEvaluationError, Signature, Spend, SymmetricKey, Telemetry, Transaction, TransactionSignature, Utils, Validation, Validation as Validation$1, VerifiableCertificate, createNonce, defaultHttpClient, verifyNonce } from "@bsv/sdk";
|
|
3
3
|
import { deleteDB, openDB } from "idb";
|
|
4
4
|
import { AESGCM, AESGCMDecrypt } from "@bsv/sdk/primitives/AESGCM";
|
|
5
5
|
import argon2Api from "hash-wasm/dist/argon2.umd.min.js";
|
|
@@ -1222,7 +1222,11 @@ var ScriptTemplateBRC29 = class {
|
|
|
1222
1222
|
return `${this.params.derivationPrefix ?? ""} ${this.params.derivationSuffix ?? ""}`;
|
|
1223
1223
|
}
|
|
1224
1224
|
getKeyDeriver(privKey) {
|
|
1225
|
-
if (
|
|
1225
|
+
if (this.params.keyDeriver?.rootKey === privKey) return this.params.keyDeriver;
|
|
1226
|
+
if (typeof privKey === "string") {
|
|
1227
|
+
if (this.params.keyDeriver?.rootKey.toHex() === privKey) return this.params.keyDeriver;
|
|
1228
|
+
privKey = PrivateKey.fromHex(privKey);
|
|
1229
|
+
}
|
|
1226
1230
|
if (this.params.keyDeriver == null || this.params.keyDeriver.rootKey.toHex() !== privKey.toHex()) return new CachedKeyDeriver(privKey);
|
|
1227
1231
|
return this.params.keyDeriver;
|
|
1228
1232
|
}
|
|
@@ -1231,8 +1235,11 @@ var ScriptTemplateBRC29 = class {
|
|
|
1231
1235
|
return this.p2pkh.lock(address);
|
|
1232
1236
|
}
|
|
1233
1237
|
unlock(unlockerPrivKey, lockerPubKey, sourceSatoshis, lockingScript) {
|
|
1234
|
-
const derivedPrivateKey = this.getKeyDeriver(unlockerPrivKey).derivePrivateKey(brc29ProtocolID, this.getKeyID(), lockerPubKey)
|
|
1235
|
-
return this.
|
|
1238
|
+
const derivedPrivateKey = this.getKeyDeriver(unlockerPrivKey).derivePrivateKey(brc29ProtocolID, this.getKeyID(), lockerPubKey);
|
|
1239
|
+
return this.unlockWithDerivedPrivateKey(derivedPrivateKey, sourceSatoshis, lockingScript);
|
|
1240
|
+
}
|
|
1241
|
+
unlockWithDerivedPrivateKey(derivedPrivateKey, sourceSatoshis, lockingScript) {
|
|
1242
|
+
return this.p2pkh.unlock(derivedPrivateKey, "all", false, sourceSatoshis, lockingScript);
|
|
1236
1243
|
}
|
|
1237
1244
|
/**
|
|
1238
1245
|
* P2PKH unlock estimateLength is a constant
|
|
@@ -2426,11 +2433,16 @@ var EntityProvenTx = class EntityProvenTx extends EntityBase {
|
|
|
2426
2433
|
/**
|
|
2427
2434
|
* @returns desirialized `MerklePath` object, value is cached.
|
|
2428
2435
|
*/
|
|
2429
|
-
getMerklePath() {
|
|
2430
|
-
|
|
2431
|
-
|
|
2436
|
+
getMerklePath(validateRoots = true) {
|
|
2437
|
+
if (validateRoots) {
|
|
2438
|
+
this._mp ??= MerklePath.fromBinary(this.api.merklePath);
|
|
2439
|
+
return this._mp;
|
|
2440
|
+
}
|
|
2441
|
+
this._mpUnchecked ??= MerklePath.fromBinary(this.api.merklePath, true, false);
|
|
2442
|
+
return this._mpUnchecked;
|
|
2432
2443
|
}
|
|
2433
2444
|
_mp;
|
|
2445
|
+
_mpUnchecked;
|
|
2434
2446
|
get provenTxId() {
|
|
2435
2447
|
return this.api.provenTxId;
|
|
2436
2448
|
}
|
|
@@ -5027,8 +5039,10 @@ async function mergeInputBeefs(rawTx, beef, trustSelf, knownTxids, trx, required
|
|
|
5027
5039
|
for (const input of tx.inputs) {
|
|
5028
5040
|
const sourceTXID = input.sourceTXID ?? "";
|
|
5029
5041
|
if (sourceTXID === "") throw new WERR_INTERNAL("req all transaction inputs must have valid sourceTXID");
|
|
5030
|
-
|
|
5031
|
-
|
|
5042
|
+
const existing = beef.findTxid(sourceTXID);
|
|
5043
|
+
const callerKnows = (requiredLevels == null || requiredLevels === 0) && knownTxids?.includes(sourceTXID) === true;
|
|
5044
|
+
if (existing != null && (!existing.isTxidOnly || callerKnows || trustSelf === "known")) continue;
|
|
5045
|
+
if (callerKnows) beef.mergeTxidOnly(sourceTXID);
|
|
5032
5046
|
else await getValidBeef(sourceTXID, beef, trustSelf, knownTxids, trx, requiredLevels);
|
|
5033
5047
|
}
|
|
5034
5048
|
}
|
|
@@ -5092,26 +5106,22 @@ async function notifyTransactionsOfProof(ids, provenTxId, addNote, updateTransac
|
|
|
5092
5106
|
* @param options
|
|
5093
5107
|
*/
|
|
5094
5108
|
async function getBeefForTransaction(storage, txid, options) {
|
|
5095
|
-
|
|
5096
|
-
if (options.mergeToBeef instanceof Beef) beef = options.mergeToBeef;
|
|
5097
|
-
else if (options.mergeToBeef != null) beef = Beef.fromBinary(options.mergeToBeef);
|
|
5098
|
-
else beef = new Beef();
|
|
5109
|
+
const beef = mergeTarget(options);
|
|
5099
5110
|
const hasKnownTxid = makeKnownTxidLookup$1(options.knownTxids ?? []);
|
|
5100
5111
|
const scheduled = /* @__PURE__ */ new Set([txid]);
|
|
5101
5112
|
let frontier = [{
|
|
5102
5113
|
txid,
|
|
5103
5114
|
depth: 0
|
|
5104
5115
|
}];
|
|
5105
|
-
const
|
|
5106
|
-
const concurrency = Number.isFinite(requestedConcurrency) ? Math.max(1, Math.min(32, Math.floor(requestedConcurrency))) : 8;
|
|
5116
|
+
const concurrency = normalizeConcurrency(options.maxConcurrency);
|
|
5107
5117
|
while (frontier.length > 0) {
|
|
5108
|
-
const current = frontier.filter((item) => beef
|
|
5118
|
+
const current = frontier.filter((item) => needsResolution(beef, item.txid, hasKnownTxid));
|
|
5109
5119
|
const resolved = await mapWithConcurrency(current, concurrency, async (item) => await resolveBeefForTransaction(storage, item.txid, options, hasKnownTxid, item.depth));
|
|
5110
5120
|
const next = [];
|
|
5111
5121
|
for (let i = 0; i < resolved.length; i++) {
|
|
5112
5122
|
const result = resolved[i];
|
|
5113
5123
|
beef.mergeBeef(result.beef);
|
|
5114
|
-
for (const dependency of result.dependencies) if (!scheduled.has(dependency) && beef
|
|
5124
|
+
for (const dependency of result.dependencies) if (!scheduled.has(dependency) && needsResolution(beef, dependency, hasKnownTxid)) {
|
|
5115
5125
|
scheduled.add(dependency);
|
|
5116
5126
|
next.push({
|
|
5117
5127
|
txid: dependency,
|
|
@@ -5123,6 +5133,172 @@ async function getBeefForTransaction(storage, txid, options) {
|
|
|
5123
5133
|
}
|
|
5124
5134
|
return beef;
|
|
5125
5135
|
}
|
|
5136
|
+
/**
|
|
5137
|
+
* Build one aggregate BEEF for several roots while resolving each storage
|
|
5138
|
+
* frontier as a set. This avoids one proof query per funding input on the
|
|
5139
|
+
* createAction success path. Complex proof-level and chain-tracker policies
|
|
5140
|
+
* retain the established single-root implementation.
|
|
5141
|
+
*/
|
|
5142
|
+
async function getBeefForTransactions(storage, txids, options) {
|
|
5143
|
+
const beef = mergeTarget(options);
|
|
5144
|
+
const roots = [...new Set(txids)];
|
|
5145
|
+
if (roots.length === 0) return beef;
|
|
5146
|
+
if (requiresSingleRootPolicy(options)) return await mergeSingleRootFragments(storage, roots, options, beef);
|
|
5147
|
+
const hasKnownTxid = makeKnownTxidLookup$1(options.knownTxids ?? []);
|
|
5148
|
+
const scheduled = new Set(roots);
|
|
5149
|
+
let frontier = roots.map((txid) => ({
|
|
5150
|
+
txid,
|
|
5151
|
+
depth: 0
|
|
5152
|
+
}));
|
|
5153
|
+
while (frontier.length > 0) {
|
|
5154
|
+
const unresolved = collectUnresolvedFrontier(storage, frontier, beef, hasKnownTxid);
|
|
5155
|
+
if (unresolved.length === 0) break;
|
|
5156
|
+
const stored = await storage.getProvenOrRawTxs(unresolved.map((item) => item.txid));
|
|
5157
|
+
if (options.trustSelf !== "known" && unresolved.every((item) => stored.get(item.txid)?.proven != null)) {
|
|
5158
|
+
mergeAllProven(storage, beef, unresolved, stored);
|
|
5159
|
+
break;
|
|
5160
|
+
}
|
|
5161
|
+
const [next, missing] = mergeStoredFrontier(beef, unresolved, stored, options, scheduled, hasKnownTxid);
|
|
5162
|
+
await mergeMissingFragments(storage, beef, missing, options);
|
|
5163
|
+
frontier = next;
|
|
5164
|
+
}
|
|
5165
|
+
return beef;
|
|
5166
|
+
}
|
|
5167
|
+
function mergeTarget(options) {
|
|
5168
|
+
if (options.mergeToBeef instanceof Beef) return options.mergeToBeef;
|
|
5169
|
+
if (options.mergeToBeef != null) return Beef.fromBinary(options.mergeToBeef);
|
|
5170
|
+
return new Beef();
|
|
5171
|
+
}
|
|
5172
|
+
function requiresSingleRootPolicy(options) {
|
|
5173
|
+
return options.ignoreStorage === true || options.minProofLevel !== void 0 || options.chainTracker != null || options.skipInvalidProofs === true;
|
|
5174
|
+
}
|
|
5175
|
+
async function mergeSingleRootFragments(storage, roots, options, beef) {
|
|
5176
|
+
const fragments = await mapWithConcurrency(roots.filter((txid) => beef.findTxid(txid) == null), normalizeConcurrency(options.maxConcurrency), async (txid) => await getBeefForTransaction(storage, txid, {
|
|
5177
|
+
...options,
|
|
5178
|
+
mergeToBeef: void 0
|
|
5179
|
+
}));
|
|
5180
|
+
for (const fragment of fragments) beef.mergeBeef(fragment);
|
|
5181
|
+
return beef;
|
|
5182
|
+
}
|
|
5183
|
+
function collectUnresolvedFrontier(storage, frontier, beef, hasKnownTxid) {
|
|
5184
|
+
const unresolved = [];
|
|
5185
|
+
for (const item of frontier) {
|
|
5186
|
+
if (!needsResolution(beef, item.txid, hasKnownTxid)) continue;
|
|
5187
|
+
if (storage.maxRecursionDepth && storage.maxRecursionDepth <= item.depth) throw new WERR_INVALID_OPERATION(`Maximum BEEF depth exceeded. Limit is ${storage.maxRecursionDepth}`);
|
|
5188
|
+
if (hasKnownTxid(item.txid)) beef.mergeTxidOnly(item.txid);
|
|
5189
|
+
else unresolved.push(item);
|
|
5190
|
+
}
|
|
5191
|
+
return unresolved;
|
|
5192
|
+
}
|
|
5193
|
+
function decodeProvenEntries(storage, unresolved, stored) {
|
|
5194
|
+
const span = storage.telemetry.enabled ? storage.telemetry.startSpan("wallet.storage.beef.decode_proven_batch", {
|
|
5195
|
+
component: "wallet-storage",
|
|
5196
|
+
attributes: { "beef.proven_tx_count": unresolved.length }
|
|
5197
|
+
}) : void 0;
|
|
5198
|
+
try {
|
|
5199
|
+
const entries = unresolved.map((item) => {
|
|
5200
|
+
const proven = stored.get(item.txid).proven;
|
|
5201
|
+
return {
|
|
5202
|
+
rawTx: proven.rawTx,
|
|
5203
|
+
merklePath: new EntityProvenTx(proven).getMerklePath(false),
|
|
5204
|
+
merkleRoot: proven.merkleRoot
|
|
5205
|
+
};
|
|
5206
|
+
});
|
|
5207
|
+
span?.end({ attributes: { "beef.decoded_proof_count": entries.length } });
|
|
5208
|
+
return entries;
|
|
5209
|
+
} catch (error) {
|
|
5210
|
+
span?.end({
|
|
5211
|
+
status: "error",
|
|
5212
|
+
error
|
|
5213
|
+
});
|
|
5214
|
+
throw error;
|
|
5215
|
+
}
|
|
5216
|
+
}
|
|
5217
|
+
function mergeAllProven(storage, beef, unresolved, stored) {
|
|
5218
|
+
const entries = decodeProvenEntries(storage, unresolved, stored);
|
|
5219
|
+
const span = storage.telemetry.enabled ? storage.telemetry.startSpan("wallet.storage.beef.merge_proven_batch", {
|
|
5220
|
+
component: "wallet-storage",
|
|
5221
|
+
attributes: { "beef.proven_tx_count": entries.length }
|
|
5222
|
+
}) : void 0;
|
|
5223
|
+
try {
|
|
5224
|
+
mergeProvenEntries(beef, entries, unresolved, stored);
|
|
5225
|
+
span?.end({ attributes: {
|
|
5226
|
+
"beef.merged_tx_count": entries.length,
|
|
5227
|
+
"beef.result_tx_count": beef.txs.length,
|
|
5228
|
+
"beef.result_bump_count": beef.bumps.length
|
|
5229
|
+
} });
|
|
5230
|
+
} catch (error) {
|
|
5231
|
+
span?.end({
|
|
5232
|
+
status: "error",
|
|
5233
|
+
error
|
|
5234
|
+
});
|
|
5235
|
+
throw error;
|
|
5236
|
+
}
|
|
5237
|
+
}
|
|
5238
|
+
function mergeProvenEntries(beef, entries, unresolved, stored) {
|
|
5239
|
+
if (typeof beef.mergeProvenTxs === "function") {
|
|
5240
|
+
beef.mergeProvenTxs(entries);
|
|
5241
|
+
return;
|
|
5242
|
+
}
|
|
5243
|
+
for (const item of unresolved) {
|
|
5244
|
+
const proven = stored.get(item.txid).proven;
|
|
5245
|
+
beef.mergeRawTx(proven.rawTx);
|
|
5246
|
+
beef.mergeBump(new EntityProvenTx(proven).getMerklePath());
|
|
5247
|
+
}
|
|
5248
|
+
}
|
|
5249
|
+
function mergeStoredFrontier(beef, unresolved, stored, options, scheduled, hasKnownTxid) {
|
|
5250
|
+
const next = [];
|
|
5251
|
+
const missing = [];
|
|
5252
|
+
for (const item of unresolved) {
|
|
5253
|
+
const result = stored.get(item.txid);
|
|
5254
|
+
if (result?.proven != null) mergeStoredProven(beef, item, result, options);
|
|
5255
|
+
else if (result?.rawTx != null) mergeStoredRaw(beef, item, result, options, scheduled, next, hasKnownTxid);
|
|
5256
|
+
else missing.push(item);
|
|
5257
|
+
}
|
|
5258
|
+
return [next, missing];
|
|
5259
|
+
}
|
|
5260
|
+
function mergeStoredProven(beef, item, result, options) {
|
|
5261
|
+
if (options.trustSelf === "known") {
|
|
5262
|
+
beef.mergeTxidOnly(item.txid);
|
|
5263
|
+
return;
|
|
5264
|
+
}
|
|
5265
|
+
const proven = result.proven;
|
|
5266
|
+
beef.mergeRawTx(proven.rawTx);
|
|
5267
|
+
beef.mergeBump(new EntityProvenTx(proven).getMerklePath());
|
|
5268
|
+
}
|
|
5269
|
+
function mergeStoredRaw(beef, item, result, options, scheduled, next, hasKnownTxid) {
|
|
5270
|
+
if (options.trustSelf === "known") {
|
|
5271
|
+
beef.mergeTxidOnly(item.txid);
|
|
5272
|
+
return;
|
|
5273
|
+
}
|
|
5274
|
+
const transaction = beef.mergeRawTx(result.rawTx);
|
|
5275
|
+
if (result.inputBEEF != null) beef.mergeBeef(result.inputBEEF);
|
|
5276
|
+
appendNewDependencies(transaction.inputTxids, item.depth + 1, beef, scheduled, next, hasKnownTxid);
|
|
5277
|
+
}
|
|
5278
|
+
function appendNewDependencies(dependencies, depth, beef, scheduled, next, hasKnownTxid) {
|
|
5279
|
+
for (const txid of dependencies) {
|
|
5280
|
+
if (scheduled.has(txid) || !needsResolution(beef, txid, hasKnownTxid)) continue;
|
|
5281
|
+
scheduled.add(txid);
|
|
5282
|
+
next.push({
|
|
5283
|
+
txid,
|
|
5284
|
+
depth
|
|
5285
|
+
});
|
|
5286
|
+
}
|
|
5287
|
+
}
|
|
5288
|
+
function needsResolution(beef, txid, hasKnownTxid) {
|
|
5289
|
+
const entry = beef.findTxid(txid);
|
|
5290
|
+
return entry == null || entry.isTxidOnly && !hasKnownTxid(txid);
|
|
5291
|
+
}
|
|
5292
|
+
async function mergeMissingFragments(storage, beef, missing, options) {
|
|
5293
|
+
if (missing.length === 0) return;
|
|
5294
|
+
if (options.ignoreServices === true) throw new WERR_INVALID_PARAMETER(`txid ${missing[0].txid}`, `valid transaction on chain ${storage.chain}`);
|
|
5295
|
+
const fragments = await mapWithConcurrency(missing, normalizeConcurrency(options.maxConcurrency), async (item) => await getBeefForTransaction(storage, item.txid, {
|
|
5296
|
+
...options,
|
|
5297
|
+
ignoreStorage: true,
|
|
5298
|
+
mergeToBeef: void 0
|
|
5299
|
+
}));
|
|
5300
|
+
for (const fragment of fragments) beef.mergeBeef(fragment);
|
|
5301
|
+
}
|
|
5126
5302
|
function makeKnownTxidLookup$1(knownTxids) {
|
|
5127
5303
|
let lookups = 0;
|
|
5128
5304
|
let indexed;
|
|
@@ -5136,6 +5312,9 @@ function makeKnownTxidLookup$1(knownTxids) {
|
|
|
5136
5312
|
return knownTxids.includes(txid);
|
|
5137
5313
|
};
|
|
5138
5314
|
}
|
|
5315
|
+
function normalizeConcurrency(value = 8) {
|
|
5316
|
+
return Number.isFinite(value) ? Math.max(1, Math.min(32, Math.floor(value))) : 8;
|
|
5317
|
+
}
|
|
5139
5318
|
async function mapWithConcurrency(values, concurrency, mapper) {
|
|
5140
5319
|
const results = Array.from({ length: values.length }, () => void 0);
|
|
5141
5320
|
let cursor = 0;
|
|
@@ -5264,6 +5443,23 @@ async function createMergedBeefOfTxids(txids, storage) {
|
|
|
5264
5443
|
//#endregion
|
|
5265
5444
|
//#region ../src/storage/methods/processAction.ts
|
|
5266
5445
|
async function processAction$1(storage, auth, args) {
|
|
5446
|
+
if (!storage.telemetry.enabled) return await processActionCore(storage, auth, args);
|
|
5447
|
+
return await storage.telemetry.withSpan("wallet.storage.process_action", {
|
|
5448
|
+
component: "wallet-storage",
|
|
5449
|
+
carrier: args,
|
|
5450
|
+
attributes: {
|
|
5451
|
+
"action.is_new_transaction": args.isNewTx,
|
|
5452
|
+
"action.is_no_send": args.isNoSend,
|
|
5453
|
+
"action.is_delayed": args.isDelayed,
|
|
5454
|
+
"action.send_with_count": args.sendWith.length
|
|
5455
|
+
}
|
|
5456
|
+
}, async (span) => {
|
|
5457
|
+
const result = await processActionCore(storage, auth, args, span);
|
|
5458
|
+
span.end({ attributes: { "action.send_result_count": result.sendWithResults?.length ?? 0 } });
|
|
5459
|
+
return result;
|
|
5460
|
+
});
|
|
5461
|
+
}
|
|
5462
|
+
async function processActionCore(storage, auth, args, parent) {
|
|
5267
5463
|
const logger = args.logger;
|
|
5268
5464
|
logger?.group("storage processAction");
|
|
5269
5465
|
const userId = verifyId(auth.userId);
|
|
@@ -5271,9 +5467,9 @@ async function processAction$1(storage, auth, args) {
|
|
|
5271
5467
|
let req;
|
|
5272
5468
|
const txidsOfReqsToShareWithWorld = [...args.sendWith];
|
|
5273
5469
|
if (args.isNewTx) {
|
|
5274
|
-
const vargs = await validateCommitNewTxToStorageArgs(storage, userId, args);
|
|
5470
|
+
const vargs = await traceProcessStep(storage, "wallet.storage.process_action.validate", parent, async () => await validateCommitNewTxToStorageArgs(storage, userId, args));
|
|
5275
5471
|
logger?.log("validated new tx updates to storage");
|
|
5276
|
-
({req} = await commitNewTxToStorage(storage, userId, vargs));
|
|
5472
|
+
({req} = await traceProcessStep(storage, "wallet.storage.process_action.commit", parent, async () => await commitNewTxToStorage(storage, userId, vargs)));
|
|
5277
5473
|
logger?.log("committed new tx updates to storage ");
|
|
5278
5474
|
if (!req) throw new WERR_INTERNAL();
|
|
5279
5475
|
if (args.isNoSend && !args.isSendWith) logger?.log(`noSend txid ${req.txid}`);
|
|
@@ -5282,12 +5478,19 @@ async function processAction$1(storage, auth, args) {
|
|
|
5282
5478
|
logger?.log(`sending txid ${req.txid}`);
|
|
5283
5479
|
}
|
|
5284
5480
|
}
|
|
5285
|
-
const { swr, ndr } = await shareReqsWithWorld(storage, userId, txidsOfReqsToShareWithWorld, args.isDelayed, void 0, logger);
|
|
5481
|
+
const { swr, ndr } = await traceProcessStep(storage, "wallet.storage.process_action.share", parent, async () => await shareReqsWithWorld(storage, userId, txidsOfReqsToShareWithWorld, args.isDelayed, void 0, logger));
|
|
5286
5482
|
r.sendWithResults = swr;
|
|
5287
5483
|
r.notDelayedResults = ndr;
|
|
5288
5484
|
logger?.groupEnd();
|
|
5289
5485
|
return r;
|
|
5290
5486
|
}
|
|
5487
|
+
async function traceProcessStep(storage, name, parent, callback) {
|
|
5488
|
+
if (parent == null) return await callback();
|
|
5489
|
+
return await storage.telemetry.withSpan(name, {
|
|
5490
|
+
component: "wallet-storage",
|
|
5491
|
+
parent: parent.context
|
|
5492
|
+
}, callback);
|
|
5493
|
+
}
|
|
5291
5494
|
/**
|
|
5292
5495
|
* Verifies that all the txids are known reqs with ready-to-share status.
|
|
5293
5496
|
* Assigns a batch identifier and updates all the provenTxReqs.
|
|
@@ -5458,21 +5661,16 @@ async function validateCommitNewTxToStorageArgs(storage, userId, params) {
|
|
|
5458
5661
|
} }));
|
|
5459
5662
|
if (!transaction.isOutgoing) throw new WERR_INVALID_OPERATION("isOutgoing is not true");
|
|
5460
5663
|
if (transaction.inputBEEF == null) throw new WERR_INVALID_OPERATION();
|
|
5461
|
-
const beef = Beef.fromBinary(asArray(transaction.inputBEEF));
|
|
5462
5664
|
if (transaction.status !== "unsigned" && transaction.status !== "unprocessed") throw new WERR_INVALID_OPERATION(`invalid transaction status ${transaction.status}`);
|
|
5463
5665
|
const transactionId = verifyId(transaction.transactionId);
|
|
5464
|
-
const outputOutputs = await storage.findOutputs({ partial: {
|
|
5666
|
+
const [outputOutputs, commissionRows] = await Promise.all([storage.findOutputs({ partial: {
|
|
5465
5667
|
userId,
|
|
5466
5668
|
transactionId
|
|
5467
|
-
} })
|
|
5468
|
-
const inputOutputs = await storage.findOutputs({ partial: {
|
|
5469
|
-
userId,
|
|
5470
|
-
spentBy: transactionId
|
|
5471
|
-
} });
|
|
5472
|
-
const commission = verifyOneOrNone(await storage.findCommissions({ partial: {
|
|
5669
|
+
} }), storage.commissionSatoshis > 0 ? storage.findCommissions({ partial: {
|
|
5473
5670
|
transactionId,
|
|
5474
5671
|
userId
|
|
5475
|
-
} }));
|
|
5672
|
+
} }) : Promise.resolve([])]);
|
|
5673
|
+
const commission = verifyOneOrNone(commissionRows);
|
|
5476
5674
|
if (storage.commissionSatoshis > 0) {
|
|
5477
5675
|
if (commission == null) throw new WERR_INTERNAL();
|
|
5478
5676
|
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.");
|
|
@@ -5492,10 +5690,7 @@ async function validateCommitNewTxToStorageArgs(storage, userId, params) {
|
|
|
5492
5690
|
txScriptOffsets,
|
|
5493
5691
|
transactionId,
|
|
5494
5692
|
transaction,
|
|
5495
|
-
inputOutputs,
|
|
5496
5693
|
outputOutputs,
|
|
5497
|
-
commission,
|
|
5498
|
-
beef,
|
|
5499
5694
|
req,
|
|
5500
5695
|
outputUpdates: [],
|
|
5501
5696
|
transactionUpdate: {
|
|
@@ -6173,17 +6368,24 @@ async function generateChangeSdkCore(params, allocateChangeInput, releaseChangeI
|
|
|
6173
6368
|
};
|
|
6174
6369
|
const fixedInputs = params.fixedInputs;
|
|
6175
6370
|
const fixedOutputs = params.fixedOutputs;
|
|
6371
|
+
const fixedFunding = fixedInputs.reduce((sum, input) => sum + input.satoshis, 0);
|
|
6372
|
+
let fixedSpending = fixedOutputs.reduce((sum, output) => sum + output.satoshis, 0);
|
|
6373
|
+
const fixedInputSize = fixedInputs.reduce((sum, input) => sum + transactionInputSize(input.unlockingScriptLength), 0);
|
|
6374
|
+
const fixedOutputSize = fixedOutputs.reduce((sum, output) => sum + transactionOutputSize(output.lockingScriptLength), 0);
|
|
6375
|
+
const changeInputSize = transactionInputSize(params.changeUnlockingScriptLength);
|
|
6376
|
+
const changeOutputSize = transactionOutputSize(params.changeLockingScriptLength);
|
|
6377
|
+
let allocatedFunding = 0;
|
|
6176
6378
|
/**
|
|
6177
6379
|
* @returns sum of transaction fixedInputs satoshis and fundingInputs satoshis
|
|
6178
6380
|
*/
|
|
6179
6381
|
const funding = () => {
|
|
6180
|
-
return
|
|
6382
|
+
return fixedFunding + allocatedFunding;
|
|
6181
6383
|
};
|
|
6182
6384
|
/**
|
|
6183
6385
|
* @returns sum of transaction fixedOutputs satoshis
|
|
6184
6386
|
*/
|
|
6185
6387
|
const spending = () => {
|
|
6186
|
-
return
|
|
6388
|
+
return fixedSpending;
|
|
6187
6389
|
};
|
|
6188
6390
|
/**
|
|
6189
6391
|
* @returns sum of transaction changeOutputs satoshis
|
|
@@ -6193,7 +6395,9 @@ async function generateChangeSdkCore(params, allocateChangeInput, releaseChangeI
|
|
|
6193
6395
|
};
|
|
6194
6396
|
const fee = () => funding() - spending() - change();
|
|
6195
6397
|
const size = (addedChangeInputs, addedChangeOutputs) => {
|
|
6196
|
-
|
|
6398
|
+
const inputCount = fixedInputs.length + r.allocatedChangeInputs.length + (addedChangeInputs || 0);
|
|
6399
|
+
const outputCount = fixedOutputs.length + r.changeOutputs.length + (addedChangeOutputs || 0);
|
|
6400
|
+
return 4 + varUintSize(inputCount) + fixedInputSize + (r.allocatedChangeInputs.length + (addedChangeInputs || 0)) * changeInputSize + varUintSize(outputCount) + fixedOutputSize + (r.changeOutputs.length + (addedChangeOutputs || 0)) * changeOutputSize + 4;
|
|
6197
6401
|
};
|
|
6198
6402
|
/**
|
|
6199
6403
|
* @returns the target fee required for the transaction as currently configured under feeModel.
|
|
@@ -6232,7 +6436,10 @@ async function generateChangeSdkCore(params, allocateChangeInput, releaseChangeI
|
|
|
6232
6436
|
const releaseAllocatedChangeInputs = async () => {
|
|
6233
6437
|
while (r.allocatedChangeInputs.length > 0) {
|
|
6234
6438
|
const i = r.allocatedChangeInputs.pop();
|
|
6235
|
-
if (i != null)
|
|
6439
|
+
if (i != null) {
|
|
6440
|
+
allocatedFunding -= i.satoshis;
|
|
6441
|
+
await releaseChangeInput(i.outputId);
|
|
6442
|
+
}
|
|
6236
6443
|
}
|
|
6237
6444
|
feeExcessNow = feeExcess();
|
|
6238
6445
|
};
|
|
@@ -6267,6 +6474,7 @@ async function generateChangeSdkCore(params, allocateChangeInput, releaseChangeI
|
|
|
6267
6474
|
const allocatedChangeInput = await allocateChangeInput(-feeExcess(1, ao) + (ao === 1 ? 2 * params.changeInitialSatoshis : 0) + changeBuffer, exactSatoshis);
|
|
6268
6475
|
if (allocatedChangeInput == null) return false;
|
|
6269
6476
|
r.allocatedChangeInputs.push(allocatedChangeInput);
|
|
6477
|
+
allocatedFunding += allocatedChangeInput.satoshis;
|
|
6270
6478
|
maybeAddChangeOutput(ao);
|
|
6271
6479
|
return true;
|
|
6272
6480
|
};
|
|
@@ -6278,6 +6486,7 @@ async function generateChangeSdkCore(params, allocateChangeInput, releaseChangeI
|
|
|
6278
6486
|
while (r.changeOutputs.length > 0 && feeExcess() < 0) r.changeOutputs.pop();
|
|
6279
6487
|
if (feeExcess() < 0) break;
|
|
6280
6488
|
removeChurnPairs(r.allocatedChangeInputs, r.changeOutputs);
|
|
6489
|
+
allocatedFunding = r.allocatedChangeInputs.reduce((sum, input) => sum + input.satoshis, 0);
|
|
6281
6490
|
}
|
|
6282
6491
|
};
|
|
6283
6492
|
/**
|
|
@@ -6286,7 +6495,9 @@ async function generateChangeSdkCore(params, allocateChangeInput, releaseChangeI
|
|
|
6286
6495
|
await fundTransaction();
|
|
6287
6496
|
if (feeExcess() < 0 && vgcpr.hasMaxPossibleOutput !== void 0) {
|
|
6288
6497
|
if (fixedOutputs[vgcpr.hasMaxPossibleOutput].satoshis !== 0x775f05a073fff) throw new WERR_INTERNAL();
|
|
6289
|
-
|
|
6498
|
+
const adjustment = feeExcess();
|
|
6499
|
+
fixedOutputs[vgcpr.hasMaxPossibleOutput].satoshis += adjustment;
|
|
6500
|
+
fixedSpending += adjustment;
|
|
6290
6501
|
r.maxPossibleSatoshisAdjustment = {
|
|
6291
6502
|
fixedOutputIndex: vgcpr.hasMaxPossibleOutput,
|
|
6292
6503
|
satoshis: fixedOutputs[vgcpr.hasMaxPossibleOutput].satoshis
|
|
@@ -6305,8 +6516,11 @@ async function generateChangeSdkCore(params, allocateChangeInput, releaseChangeI
|
|
|
6305
6516
|
* If needed, seek funding to avoid overspending on fees without a change output to recapture it.
|
|
6306
6517
|
*/
|
|
6307
6518
|
if (r.changeOutputs.length === 0 && feeExcessNow > 0) {
|
|
6519
|
+
const minimumChange = Math.max(dustFloor, params.changeFirstSatoshis);
|
|
6520
|
+
const totalSatoshisNeeded = spending() + feeTarget(0, 1) + minimumChange;
|
|
6521
|
+
const moreSatoshisNeeded = Math.max(1, totalSatoshisNeeded - funding());
|
|
6308
6522
|
await releaseAllocatedChangeInputs();
|
|
6309
|
-
throw new WERR_INSUFFICIENT_FUNDS(
|
|
6523
|
+
throw new WERR_INSUFFICIENT_FUNDS(totalSatoshisNeeded, moreSatoshisNeeded);
|
|
6310
6524
|
}
|
|
6311
6525
|
/**
|
|
6312
6526
|
* Distribute the excess fees across the changeOutputs added.
|
|
@@ -6630,6 +6844,8 @@ function makeChangeLock(out, dctr, args, changeKeys, wallet) {
|
|
|
6630
6844
|
}
|
|
6631
6845
|
//#endregion
|
|
6632
6846
|
//#region ../src/signer/methods/verifyUnlockScripts.ts
|
|
6847
|
+
const postChronicleHeightFallback = 943816;
|
|
6848
|
+
const canonicalP2PKHScope = TransactionSignature.SIGHASH_ALL + TransactionSignature.SIGHASH_FORKID;
|
|
6633
6849
|
const javaScriptOnlyVerifier = {
|
|
6634
6850
|
shouldVerifySpend: () => false,
|
|
6635
6851
|
verifySpend: async () => {
|
|
@@ -6641,10 +6857,11 @@ function invalidUnlockingScript(inputIndex, detail) {
|
|
|
6641
6857
|
return new WERR_INVALID_PARAMETER(`inputs[${inputIndex}].unlockScript`, `valid.${suffix}`);
|
|
6642
6858
|
}
|
|
6643
6859
|
async function verifyOneSpend(pending, verifier) {
|
|
6860
|
+
const [inputIndex, , spend, context] = pending;
|
|
6644
6861
|
try {
|
|
6645
|
-
if (!(verifier === void 0 ?
|
|
6862
|
+
if (!(verifier === void 0 ? spend.validate(context) : await spend.validateWith(verifier, context))) throw invalidUnlockingScript(inputIndex);
|
|
6646
6863
|
} catch (error) {
|
|
6647
|
-
if (error instanceof ScriptEvaluationError) throw invalidUnlockingScript(
|
|
6864
|
+
if (error instanceof ScriptEvaluationError) throw invalidUnlockingScript(inputIndex, error.message);
|
|
6648
6865
|
throw error;
|
|
6649
6866
|
}
|
|
6650
6867
|
}
|
|
@@ -6654,33 +6871,157 @@ async function verifyPendingSpends(pending, verifier) {
|
|
|
6654
6871
|
return;
|
|
6655
6872
|
}
|
|
6656
6873
|
const batched = [];
|
|
6657
|
-
for (const item of pending) if (verifier.shouldVerifySpend?.(item
|
|
6874
|
+
for (const item of pending) if (verifier.shouldVerifySpend?.(item[2], item[3]) !== false) batched.push(item);
|
|
6658
6875
|
else await verifyOneSpend(item, javaScriptOnlyVerifier);
|
|
6659
6876
|
if (batched.length === 0) return;
|
|
6660
6877
|
let verdicts;
|
|
6661
6878
|
try {
|
|
6662
6879
|
verdicts = await verifier.verifySpendsBatch(batched.map((item) => ({
|
|
6663
|
-
spend: item
|
|
6664
|
-
...item
|
|
6880
|
+
spend: item[2],
|
|
6881
|
+
...item[3]
|
|
6665
6882
|
})));
|
|
6666
6883
|
} catch (error) {
|
|
6667
|
-
if (error instanceof ScriptEvaluationError) throw invalidUnlockingScript(batched[0]
|
|
6884
|
+
if (error instanceof ScriptEvaluationError) throw invalidUnlockingScript(batched[0][0], error.message);
|
|
6668
6885
|
throw error;
|
|
6669
6886
|
}
|
|
6670
6887
|
if (verdicts.length !== batched.length) throw new Error("Script verifier returned an invalid batch result count");
|
|
6671
6888
|
verdicts.forEach((valid, index) => {
|
|
6672
|
-
if (!valid) throw invalidUnlockingScript(batched[index]
|
|
6889
|
+
if (!valid) throw invalidUnlockingScript(batched[index][0]);
|
|
6673
6890
|
});
|
|
6674
6891
|
}
|
|
6675
|
-
function
|
|
6676
|
-
const
|
|
6892
|
+
function wholeTransactionVerifier(verifier) {
|
|
6893
|
+
const candidate = verifier;
|
|
6894
|
+
return typeof candidate?.verifyScripts === "function" ? candidate : void 0;
|
|
6895
|
+
}
|
|
6896
|
+
function digestBatchVerifier(verifier) {
|
|
6897
|
+
const candidate = verifier;
|
|
6898
|
+
if (typeof candidate?.verifyDigestBatch !== "function") return void 0;
|
|
6899
|
+
if (candidate.isReady?.() === false) return void 0;
|
|
6900
|
+
if (candidate.supportsCrypto?.("verifyDigestBatch") === false) return void 0;
|
|
6901
|
+
return candidate;
|
|
6902
|
+
}
|
|
6903
|
+
function equalBytes(left, right) {
|
|
6904
|
+
if (left.length !== right.length) return false;
|
|
6905
|
+
for (let index = 0; index < left.length; index++) if (left[index] !== right[index]) return false;
|
|
6906
|
+
return true;
|
|
6907
|
+
}
|
|
6908
|
+
function isCanonicalP2PKHLock(lock) {
|
|
6909
|
+
return lock.length === 25 && lock[0] === 118 && lock[1] === 169 && lock[2] === 20 && lock[23] === 136 && lock[24] === 172;
|
|
6910
|
+
}
|
|
6911
|
+
function parseCanonicalP2PKHUnlock(unlock, lock) {
|
|
6912
|
+
const signatureLength = unlock[0];
|
|
6913
|
+
if (signatureLength == null || signatureLength < 9 || signatureLength > 73 || unlock.length !== 1 + signatureLength + 1 + 33 || unlock[1 + signatureLength] !== 33) return void 0;
|
|
6914
|
+
const checksig = Array.from(unlock.subarray(1, 1 + signatureLength));
|
|
6915
|
+
const publicKey = unlock.subarray(1 + signatureLength + 1);
|
|
6916
|
+
if (publicKey[0] !== 2 && publicKey[0] !== 3 || !equalBytes(Hash.hash160(publicKey), lock.subarray(3, 23))) return void 0;
|
|
6917
|
+
let signature;
|
|
6918
|
+
try {
|
|
6919
|
+
signature = TransactionSignature.fromChecksigFormat(checksig);
|
|
6920
|
+
} catch {
|
|
6921
|
+
return;
|
|
6922
|
+
}
|
|
6923
|
+
if (signature.scope !== canonicalP2PKHScope || !signature.hasLowS() || !equalBytes(signature.toChecksigFormat(), checksig)) return void 0;
|
|
6924
|
+
return [
|
|
6925
|
+
checksig,
|
|
6926
|
+
publicKey,
|
|
6927
|
+
signature
|
|
6928
|
+
];
|
|
6929
|
+
}
|
|
6930
|
+
/**
|
|
6931
|
+
* Recognizes only the exact canonical P2PKH shape generated by this wallet.
|
|
6932
|
+
* Anything else retains the general-purpose script interpreter/backend path.
|
|
6933
|
+
*/
|
|
6934
|
+
function standardP2PKHDigests(tx) {
|
|
6935
|
+
const cache = { hashOutputsSingle: /* @__PURE__ */ new Map() };
|
|
6936
|
+
const items = [];
|
|
6937
|
+
for (let inputIndex = 0; inputIndex < tx.inputs.length; inputIndex++) {
|
|
6938
|
+
const input = tx.inputs[inputIndex];
|
|
6939
|
+
const sourceTransaction = input.sourceTransaction;
|
|
6940
|
+
const sourceTXID = input.sourceTXID;
|
|
6941
|
+
const unlockingScript = input.unlockingScript;
|
|
6942
|
+
if (sourceTransaction == null || sourceTXID == null || unlockingScript == null) return void 0;
|
|
6943
|
+
const sourceOutput = sourceTransaction.outputs[input.sourceOutputIndex];
|
|
6944
|
+
if (sourceOutput == null) return void 0;
|
|
6945
|
+
const lock = sourceOutput.lockingScript.toUint8Array();
|
|
6946
|
+
if (!isCanonicalP2PKHLock(lock)) return void 0;
|
|
6947
|
+
const parsed = parseCanonicalP2PKHUnlock(unlockingScript.toUint8Array(), lock);
|
|
6948
|
+
if (parsed == null) return void 0;
|
|
6949
|
+
const [checksig, publicKey, signature] = parsed;
|
|
6950
|
+
const preimage = TransactionSignature.formatBytes({
|
|
6951
|
+
sourceTXID,
|
|
6952
|
+
sourceOutputIndex: input.sourceOutputIndex,
|
|
6953
|
+
sourceSatoshis: sourceOutput.satoshis ?? 0,
|
|
6954
|
+
transactionVersion: tx.version,
|
|
6955
|
+
otherInputs: [],
|
|
6956
|
+
allInputs: tx.inputs,
|
|
6957
|
+
outputs: tx.outputs,
|
|
6958
|
+
inputIndex,
|
|
6959
|
+
subscript: sourceOutput.lockingScript,
|
|
6960
|
+
inputSequence: input.sequence ?? 4294967295,
|
|
6961
|
+
lockTime: tx.lockTime,
|
|
6962
|
+
scope: signature.scope,
|
|
6963
|
+
cache
|
|
6964
|
+
});
|
|
6965
|
+
items.push({
|
|
6966
|
+
publicKey,
|
|
6967
|
+
digest: Uint8Array.from(Hash.hash256(preimage)),
|
|
6968
|
+
signature: Uint8Array.from(checksig.slice(0, -1))
|
|
6969
|
+
});
|
|
6970
|
+
}
|
|
6971
|
+
return items;
|
|
6972
|
+
}
|
|
6973
|
+
async function verifyStandardP2PKHDigests(pending, verifier) {
|
|
6974
|
+
if (pending.length === 0) return /* @__PURE__ */ new Set();
|
|
6975
|
+
const items = pending.flatMap((entry) => entry[1]);
|
|
6976
|
+
const verdicts = await verifier.verifyDigestBatch(items);
|
|
6977
|
+
if (verdicts.length !== items.length) throw new Error("Script verifier returned an invalid digest batch result count");
|
|
6978
|
+
const verified = /* @__PURE__ */ new Set();
|
|
6979
|
+
let offset = 0;
|
|
6980
|
+
for (const entry of pending) {
|
|
6981
|
+
const end = offset + entry[1].length;
|
|
6982
|
+
if (verdicts.slice(offset, end).every(Boolean)) verified.add(entry[0]);
|
|
6983
|
+
offset = end;
|
|
6984
|
+
}
|
|
6985
|
+
return verified;
|
|
6986
|
+
}
|
|
6987
|
+
function hydrateTransactionSources(txid, transactions) {
|
|
6988
|
+
const tx = transactions.get(txid);
|
|
6989
|
+
if (tx == null) throw new WERR_INVALID_PARAMETER("txid", `contained in beef, txid ${txid}`);
|
|
6990
|
+
for (let inputIndex = 0; inputIndex < tx.inputs.length; inputIndex++) {
|
|
6991
|
+
const input = tx.inputs[inputIndex];
|
|
6992
|
+
if (input.sourceTXID == null) throw new WERR_INVALID_PARAMETER(`inputs[${inputIndex}].sourceTXID`, "valid");
|
|
6993
|
+
if (input.unlockingScript == null) throw new WERR_INVALID_PARAMETER(`inputs[${inputIndex}].unlockingScript`, "valid");
|
|
6994
|
+
input.sourceTransaction = transactions.get(input.sourceTXID);
|
|
6995
|
+
if (input.sourceTransaction == null) return void 0;
|
|
6996
|
+
if (input.sourceTransaction.outputs[input.sourceOutputIndex] == null) throw new WERR_INVALID_PARAMETER(`inputs[${inputIndex}].sourceOutputIndex`, "reference an output in the source transaction");
|
|
6997
|
+
}
|
|
6998
|
+
return tx;
|
|
6999
|
+
}
|
|
7000
|
+
function transactionIndex(txids, beef) {
|
|
7001
|
+
if (txids.length > 0) beef.findTxid(txids[0]);
|
|
7002
|
+
return new Map(beef.txs.map((item) => [item.txid, item.tx]));
|
|
7003
|
+
}
|
|
7004
|
+
async function verifyWholeTransactions(pending, verifier) {
|
|
7005
|
+
if (pending.length === 0) return /* @__PURE__ */ new Set();
|
|
7006
|
+
let verdicts;
|
|
7007
|
+
try {
|
|
7008
|
+
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]));
|
|
7009
|
+
} catch (error) {
|
|
7010
|
+
if (error instanceof ScriptEvaluationError) return /* @__PURE__ */ new Set();
|
|
7011
|
+
throw error;
|
|
7012
|
+
}
|
|
7013
|
+
if (verdicts.length !== pending.length) throw new Error("Script verifier returned an invalid transaction batch result count");
|
|
7014
|
+
return new Set(pending.filter((_, index) => verdicts[index]).map((item) => item[0]));
|
|
7015
|
+
}
|
|
7016
|
+
function collectTransactionSpends(txid, resultIndex, transactions, result, pending) {
|
|
7017
|
+
const tx = transactions.get(txid);
|
|
6677
7018
|
if (tx == null) throw new WERR_INVALID_PARAMETER("txid", `contained in beef, txid ${txid}`);
|
|
6678
7019
|
const sigHashCache = { hashOutputsSingle: /* @__PURE__ */ new Map() };
|
|
6679
7020
|
for (let inputIndex = 0; inputIndex < tx.inputs.length; inputIndex++) {
|
|
6680
7021
|
const input = tx.inputs[inputIndex];
|
|
6681
7022
|
if (input.sourceTXID == null) throw new WERR_INVALID_PARAMETER(`inputs[${inputIndex}].sourceTXID`, "valid");
|
|
6682
7023
|
if (input.unlockingScript == null) throw new WERR_INVALID_PARAMETER(`inputs[${inputIndex}].unlockingScript`, "valid");
|
|
6683
|
-
input.sourceTransaction =
|
|
7024
|
+
input.sourceTransaction = transactions.get(input.sourceTXID);
|
|
6684
7025
|
if (input.sourceTransaction == null) {
|
|
6685
7026
|
result.skippedInputs++;
|
|
6686
7027
|
continue;
|
|
@@ -6692,11 +7033,10 @@ function collectTransactionSpends(txid, resultIndex, beef, result, pending) {
|
|
|
6692
7033
|
consensus: true,
|
|
6693
7034
|
utxoHeight
|
|
6694
7035
|
};
|
|
6695
|
-
pending.push(
|
|
7036
|
+
pending.push([
|
|
6696
7037
|
inputIndex,
|
|
6697
7038
|
resultIndex,
|
|
6698
|
-
|
|
6699
|
-
spend: new Spend({
|
|
7039
|
+
new Spend({
|
|
6700
7040
|
sourceTXID: input.sourceTXID,
|
|
6701
7041
|
sourceOutputIndex: input.sourceOutputIndex,
|
|
6702
7042
|
lockingScript: sourceOutput.lockingScript,
|
|
@@ -6710,9 +7050,52 @@ function collectTransactionSpends(txid, resultIndex, beef, result, pending) {
|
|
|
6710
7050
|
outputs: tx.outputs,
|
|
6711
7051
|
lockTime: tx.lockTime,
|
|
6712
7052
|
sigHashCache
|
|
6713
|
-
})
|
|
6714
|
-
|
|
7053
|
+
}),
|
|
7054
|
+
context
|
|
7055
|
+
]);
|
|
7056
|
+
}
|
|
7057
|
+
}
|
|
7058
|
+
function collectAcceleratedTransactions(txids, transactions, digestVerifier, enabled) {
|
|
7059
|
+
const hydrated = /* @__PURE__ */ new Map();
|
|
7060
|
+
const digests = [];
|
|
7061
|
+
if (!enabled) return [hydrated, digests];
|
|
7062
|
+
for (let resultIndex = 0; resultIndex < txids.length; resultIndex++) {
|
|
7063
|
+
const tx = hydrateTransactionSources(txids[resultIndex], transactions);
|
|
7064
|
+
if (tx == null) continue;
|
|
7065
|
+
hydrated.set(resultIndex, tx);
|
|
7066
|
+
if (digestVerifier === void 0) continue;
|
|
7067
|
+
const items = standardP2PKHDigests(tx);
|
|
7068
|
+
if (items != null) digests.push([resultIndex, items]);
|
|
7069
|
+
}
|
|
7070
|
+
return [hydrated, digests];
|
|
7071
|
+
}
|
|
7072
|
+
function collectWholeTransactionVerifications(hydrated, digestAttempted, verifier) {
|
|
7073
|
+
if (verifier === void 0) return [];
|
|
7074
|
+
const pending = [];
|
|
7075
|
+
for (const [resultIndex, tx] of hydrated) {
|
|
7076
|
+
if (digestAttempted.has(resultIndex)) continue;
|
|
7077
|
+
const params = {
|
|
7078
|
+
tx,
|
|
7079
|
+
blockHeight: postChronicleHeightFallback,
|
|
7080
|
+
consensus: true
|
|
7081
|
+
};
|
|
7082
|
+
if (verifier.shouldVerifyScripts?.(params) === false) continue;
|
|
7083
|
+
pending.push([resultIndex, params]);
|
|
7084
|
+
}
|
|
7085
|
+
return pending;
|
|
7086
|
+
}
|
|
7087
|
+
function collectFallbackSpends(txids, transactions, accelerated, results) {
|
|
7088
|
+
const pending = [];
|
|
7089
|
+
for (let resultIndex = 0; resultIndex < txids.length; resultIndex++) {
|
|
7090
|
+
if (!accelerated.has(resultIndex)) {
|
|
7091
|
+
collectTransactionSpends(txids[resultIndex], resultIndex, transactions, results[resultIndex], pending);
|
|
7092
|
+
continue;
|
|
7093
|
+
}
|
|
7094
|
+
const tx = transactions.get(txids[resultIndex]);
|
|
7095
|
+
if (tx == null) throw new WERR_INVALID_PARAMETER("txid", `contained in beef, txid ${txids[resultIndex]}`);
|
|
7096
|
+
results[resultIndex].verifiedInputs = tx.inputs.length;
|
|
6715
7097
|
}
|
|
7098
|
+
return pending;
|
|
6716
7099
|
}
|
|
6717
7100
|
/**
|
|
6718
7101
|
* Verifies every resolvable input from several transactions in one optional
|
|
@@ -6723,10 +7106,17 @@ async function verifyUnlockScriptsBatch(txids, beef, verifier) {
|
|
|
6723
7106
|
verifiedInputs: 0,
|
|
6724
7107
|
skippedInputs: 0
|
|
6725
7108
|
}));
|
|
6726
|
-
const
|
|
6727
|
-
|
|
7109
|
+
const transactions = transactionIndex(txids, beef);
|
|
7110
|
+
const digestVerifier = digestBatchVerifier(verifier);
|
|
7111
|
+
const wholeVerifier = wholeTransactionVerifier(verifier);
|
|
7112
|
+
const [hydrated, digestPending] = collectAcceleratedTransactions(txids, transactions, digestVerifier, digestVerifier !== void 0 || wholeVerifier !== void 0);
|
|
7113
|
+
const digestAttempted = new Set(digestPending.map((item) => item[0]));
|
|
7114
|
+
const digestVerified = digestVerifier === void 0 ? /* @__PURE__ */ new Set() : await verifyStandardP2PKHDigests(digestPending, digestVerifier);
|
|
7115
|
+
const wholePending = collectWholeTransactionVerifications(hydrated, digestAttempted, wholeVerifier);
|
|
7116
|
+
const wholeVerified = wholeVerifier === void 0 ? /* @__PURE__ */ new Set() : await verifyWholeTransactions(wholePending, wholeVerifier);
|
|
7117
|
+
const pending = collectFallbackSpends(txids, transactions, /* @__PURE__ */ new Set([...digestVerified, ...wholeVerified]), results);
|
|
6728
7118
|
await verifyPendingSpends(pending, verifier);
|
|
6729
|
-
for (const item of pending) results[item
|
|
7119
|
+
for (const item of pending) results[item[1]].verifiedInputs++;
|
|
6730
7120
|
return results;
|
|
6731
7121
|
}
|
|
6732
7122
|
/**
|
|
@@ -6749,21 +7139,56 @@ async function completeSignedTransaction(prior, spends, wallet) {
|
|
|
6749
7139
|
input.unlockingScript = asBsvSdkScript(spend.unlockingScript);
|
|
6750
7140
|
if (spend.sequenceNumber !== void 0) input.sequence = spend.sequenceNumber;
|
|
6751
7141
|
}
|
|
6752
|
-
|
|
6753
|
-
const
|
|
6754
|
-
|
|
6755
|
-
|
|
6756
|
-
|
|
6757
|
-
|
|
6758
|
-
|
|
6759
|
-
|
|
6760
|
-
|
|
6761
|
-
|
|
6762
|
-
const
|
|
6763
|
-
|
|
6764
|
-
|
|
6765
|
-
|
|
6766
|
-
|
|
7142
|
+
const prepareUnlockingTemplates = (keys) => {
|
|
7143
|
+
const counterparties = /* @__PURE__ */ new Map();
|
|
7144
|
+
const counterparty = (publicKey) => {
|
|
7145
|
+
let parsed = counterparties.get(publicKey);
|
|
7146
|
+
if (parsed == null) {
|
|
7147
|
+
parsed = PublicKey.fromString(publicKey);
|
|
7148
|
+
counterparties.set(publicKey, parsed);
|
|
7149
|
+
}
|
|
7150
|
+
return parsed;
|
|
7151
|
+
};
|
|
7152
|
+
const prepared = prior.pdi.map((pdi) => {
|
|
7153
|
+
return {
|
|
7154
|
+
pdi,
|
|
7155
|
+
template: new ScriptTemplateBRC29({
|
|
7156
|
+
derivationPrefix: pdi.derivationPrefix,
|
|
7157
|
+
derivationSuffix: pdi.derivationSuffix,
|
|
7158
|
+
keyDeriver: wallet.keyDeriver
|
|
7159
|
+
}),
|
|
7160
|
+
unlockerPubKey: counterparty(pdi.unlockerPubKey || keys.publicKey)
|
|
7161
|
+
};
|
|
7162
|
+
});
|
|
7163
|
+
const derivations = prepared.map(({ template, unlockerPubKey }) => ({
|
|
7164
|
+
protocolID: brc29ProtocolID,
|
|
7165
|
+
keyID: template.getKeyID(),
|
|
7166
|
+
counterparty: unlockerPubKey
|
|
7167
|
+
}));
|
|
7168
|
+
const derivedPrivateKeys = wallet.keyDeriver.derivePrivateKeys?.(derivations) ?? derivations.map((derivation) => wallet.keyDeriver.derivePrivateKey(derivation.protocolID, derivation.keyID, derivation.counterparty));
|
|
7169
|
+
for (let index = 0; index < prepared.length; index++) {
|
|
7170
|
+
const { pdi, template } = prepared[index];
|
|
7171
|
+
const unlockTemplate = template.unlockWithDerivedPrivateKey(derivedPrivateKeys[index], pdi.sourceSatoshis, asBsvSdkScript(pdi.lockingScript));
|
|
7172
|
+
const input = prior.tx.inputs[pdi.vin];
|
|
7173
|
+
input.unlockingScriptTemplate = unlockTemplate;
|
|
7174
|
+
}
|
|
7175
|
+
};
|
|
7176
|
+
if (wallet.telemetry.enabled && prior.pdi.length > 0) await wallet.telemetry.withSpan("wallet.crypto.prepare_unlocking_templates", {
|
|
7177
|
+
component: "wallet-toolbox",
|
|
7178
|
+
carrier: prior.args,
|
|
7179
|
+
attributes: { "crypto.managed_input_count": prior.pdi.length }
|
|
7180
|
+
}, async (span) => {
|
|
7181
|
+
const keys = await wallet.telemetry.withSpan("wallet.crypto.client_change_key", {
|
|
7182
|
+
component: "wallet-toolbox",
|
|
7183
|
+
parent: span.context
|
|
7184
|
+
}, () => wallet.getClientChangeKeyPair());
|
|
7185
|
+
await wallet.telemetry.withSpan("wallet.crypto.derive_unlocking_templates", {
|
|
7186
|
+
component: "wallet-toolbox",
|
|
7187
|
+
parent: span.context,
|
|
7188
|
+
attributes: { "crypto.managed_input_count": prior.pdi.length }
|
|
7189
|
+
}, () => prepareUnlockingTemplates(keys));
|
|
7190
|
+
});
|
|
7191
|
+
else if (prior.pdi.length > 0) prepareUnlockingTemplates(wallet.getClientChangeKeyPair());
|
|
6767
7192
|
if (wallet.telemetry.enabled) await wallet.telemetry.withSpan("wallet.crypto.transaction_sign", {
|
|
6768
7193
|
component: "wallet-toolbox",
|
|
6769
7194
|
carrier: prior.args,
|
|
@@ -6819,19 +7244,22 @@ async function createActionCore$1(wallet, auth, vargs, parent) {
|
|
|
6819
7244
|
prior.tx = await traceActionStep(wallet, "wallet.create_action.complete_signing", parent, async () => await completeSignedTransaction(prior, {}, wallet));
|
|
6820
7245
|
logger?.log("completed signed transaction");
|
|
6821
7246
|
r.txid = prior.tx.id("hex");
|
|
6822
|
-
const beef =
|
|
6823
|
-
|
|
6824
|
-
|
|
6825
|
-
|
|
6826
|
-
|
|
6827
|
-
|
|
7247
|
+
const beef = await traceActionStep(wallet, "wallet.create_action.assemble_result_beef", parent, () => {
|
|
7248
|
+
const result = new Beef();
|
|
7249
|
+
if (prior.dcr.inputBeef != null) {
|
|
7250
|
+
const inputBeef = prior.dcr.inputBeef instanceof Uint8Array ? Beef.fromBinaryView(prior.dcr.inputBeef) : Beef.fromBinary(prior.dcr.inputBeef);
|
|
7251
|
+
result.mergeBeef(inputBeef);
|
|
7252
|
+
}
|
|
7253
|
+
result.mergeTransaction(prior.tx);
|
|
7254
|
+
return result;
|
|
7255
|
+
});
|
|
6828
7256
|
logger?.log("merged beef");
|
|
6829
7257
|
await traceActionStep(wallet, "wallet.create_action.verify_unlock_scripts", parent, async () => await verifyUnlockScripts(r.txid, beef, wallet.scriptVerifier));
|
|
6830
7258
|
logger?.log("verified unlock scripts");
|
|
6831
7259
|
r.noSendChange = prior.dcr.noSendChangeOutputVouts?.map((vout) => `${r.txid}.${vout}`);
|
|
6832
7260
|
beef.atomicTxid = r.txid;
|
|
6833
7261
|
setResultBeef(r, beef);
|
|
6834
|
-
if (!vargs.options.returnTXIDOnly) r.tx = beef.toUint8ArrayAtomic(r.txid);
|
|
7262
|
+
if (!vargs.options.returnTXIDOnly) r.tx = await traceActionStep(wallet, "wallet.create_action.serialize_result_beef", parent, () => beef.toUint8ArrayAtomic(r.txid));
|
|
6835
7263
|
}
|
|
6836
7264
|
const { sendWithResults, notDelayedResults } = await traceActionStep(wallet, "wallet.create_action.process", parent, async () => await processAction(prior, wallet, auth, vargs));
|
|
6837
7265
|
logger?.log("processed transaction");
|
|
@@ -7449,6 +7877,50 @@ function selectCanonicalChange(outputs, targetSatoshis, exactSatoshis) {
|
|
|
7449
7877
|
if (over != null) return over;
|
|
7450
7878
|
return outputs.filter((output) => output.satoshis < targetSatoshis).sort((a, b) => b.satoshis - a.satoshis || b.outputId - a.outputId)[0];
|
|
7451
7879
|
}
|
|
7880
|
+
/**
|
|
7881
|
+
* Stateful form of the canonical selector for allocating many inputs from one
|
|
7882
|
+
* candidate set. It preserves exact / least-over / largest-under ordering but
|
|
7883
|
+
* sorts once instead of filtering and sorting the full set per input.
|
|
7884
|
+
*/
|
|
7885
|
+
var CanonicalChangeSelector = class {
|
|
7886
|
+
sorted;
|
|
7887
|
+
allocated = /* @__PURE__ */ new Set();
|
|
7888
|
+
constructor(outputs) {
|
|
7889
|
+
this.sorted = [...outputs].sort((a, b) => a.satoshis - b.satoshis || a.outputId - b.outputId);
|
|
7890
|
+
}
|
|
7891
|
+
take(targetSatoshis, exactSatoshis) {
|
|
7892
|
+
if (exactSatoshis !== void 0) for (let index = this.lowerBound(exactSatoshis); index < this.sorted.length; index++) {
|
|
7893
|
+
const output = this.sorted[index];
|
|
7894
|
+
if (output.satoshis !== exactSatoshis) break;
|
|
7895
|
+
if (!this.allocated.has(output.outputId)) return this.allocate(output);
|
|
7896
|
+
}
|
|
7897
|
+
for (let index = this.lowerBound(targetSatoshis); index < this.sorted.length; index++) {
|
|
7898
|
+
const output = this.sorted[index];
|
|
7899
|
+
if (!this.allocated.has(output.outputId)) return this.allocate(output);
|
|
7900
|
+
}
|
|
7901
|
+
for (let index = this.lowerBound(targetSatoshis) - 1; index >= 0; index--) {
|
|
7902
|
+
const output = this.sorted[index];
|
|
7903
|
+
if (!this.allocated.has(output.outputId)) return this.allocate(output);
|
|
7904
|
+
}
|
|
7905
|
+
}
|
|
7906
|
+
release(outputId) {
|
|
7907
|
+
this.allocated.delete(outputId);
|
|
7908
|
+
}
|
|
7909
|
+
allocate(output) {
|
|
7910
|
+
this.allocated.add(output.outputId);
|
|
7911
|
+
return output;
|
|
7912
|
+
}
|
|
7913
|
+
lowerBound(satoshis) {
|
|
7914
|
+
let low = 0;
|
|
7915
|
+
let high = this.sorted.length;
|
|
7916
|
+
while (low < high) {
|
|
7917
|
+
const middle = low + high >>> 1;
|
|
7918
|
+
if (this.sorted[middle].satoshis < satoshis) low = middle + 1;
|
|
7919
|
+
else high = middle;
|
|
7920
|
+
}
|
|
7921
|
+
return low;
|
|
7922
|
+
}
|
|
7923
|
+
};
|
|
7452
7924
|
function repeatableRandom(randomVals) {
|
|
7453
7925
|
const values = [...randomVals ?? []];
|
|
7454
7926
|
return () => {
|
|
@@ -8584,6 +9056,22 @@ var ActionBatchController = class {
|
|
|
8584
9056
|
};
|
|
8585
9057
|
//#endregion
|
|
8586
9058
|
//#region ../src/Wallet.ts
|
|
9059
|
+
function prepareKnownTxidsForCreateAction(wallet, args) {
|
|
9060
|
+
if (!wallet.autoKnownTxids || args.options?.knownTxids != null) return;
|
|
9061
|
+
if (!wallet.telemetry.enabled) {
|
|
9062
|
+
args.options.knownTxids = wallet.getKnownTxids(args.options?.knownTxids);
|
|
9063
|
+
return;
|
|
9064
|
+
}
|
|
9065
|
+
args.options.knownTxids = wallet.telemetry.withSpan("wallet.create_action.prepare_known_txids", {
|
|
9066
|
+
component: "wallet-toolbox",
|
|
9067
|
+
carrier: args,
|
|
9068
|
+
attributes: { "beef.tx_count": wallet.beef.txs.length }
|
|
9069
|
+
}, (span) => {
|
|
9070
|
+
const knownTxids = wallet.getKnownTxids(args.options?.knownTxids);
|
|
9071
|
+
span.end({ attributes: { "beef.known_txid_count": knownTxids.length } });
|
|
9072
|
+
return knownTxids;
|
|
9073
|
+
});
|
|
9074
|
+
}
|
|
8587
9075
|
/**
|
|
8588
9076
|
* Build a {@link DiscoverCertificatesResult} from contact records so {@link Wallet.discoverByIdentityKey}
|
|
8589
9077
|
* and {@link Wallet.discoverByAttributes} can short-circuit on a local contacts hit. The synthetic
|
|
@@ -9057,6 +9545,7 @@ var Wallet = class {
|
|
|
9057
9545
|
if (this.returnTxidOnly) return beef;
|
|
9058
9546
|
const b = parsedBeef ?? Beef.fromBinary(beef);
|
|
9059
9547
|
if (!b.atomicTxid) throw new WERR_INTERNAL();
|
|
9548
|
+
if (!b.txs.some((btx) => btx.isTxidOnly && !knownTxids?.includes(btx.txid))) return beef;
|
|
9060
9549
|
return this.verifyReturnedTxidOnly(b, knownTxids).toBinaryAtomic(b.atomicTxid);
|
|
9061
9550
|
}
|
|
9062
9551
|
verifyReturnedTxidOnlyBEEF(beef) {
|
|
@@ -9088,16 +9577,7 @@ var Wallet = class {
|
|
|
9088
9577
|
Validation.validateOriginator(originator);
|
|
9089
9578
|
args.options ??= {};
|
|
9090
9579
|
args.options.trustSelf ||= this.trustSelf;
|
|
9091
|
-
|
|
9092
|
-
component: "wallet-toolbox",
|
|
9093
|
-
carrier: args,
|
|
9094
|
-
attributes: { "beef.tx_count": this.beef.txs.length }
|
|
9095
|
-
}, (span) => {
|
|
9096
|
-
const knownTxids = this.getKnownTxids(args.options?.knownTxids);
|
|
9097
|
-
span.end({ attributes: { "beef.known_txid_count": knownTxids.length } });
|
|
9098
|
-
return knownTxids;
|
|
9099
|
-
});
|
|
9100
|
-
else args.options.knownTxids = this.getKnownTxids(args.options.knownTxids);
|
|
9580
|
+
prepareKnownTxidsForCreateAction(this, args);
|
|
9101
9581
|
const { auth, vargs } = this.validateAuthAndArgs(args, Validation.validateCreateActionArgs, logger);
|
|
9102
9582
|
logger?.log("validated args");
|
|
9103
9583
|
vargs.includeAllSourceTransactions = this.includeAllSourceTransactions;
|
|
@@ -9105,9 +9585,25 @@ var Wallet = class {
|
|
|
9105
9585
|
const r = await createAction$1(this, auth, vargs);
|
|
9106
9586
|
logger?.log("action created");
|
|
9107
9587
|
const resultBeef = getResultBeef(r);
|
|
9108
|
-
if (r.tx != null) this.beef.mergeBeefFromParty(this.storageParty, resultBeef ?? r.tx);
|
|
9109
9588
|
if (r.tx != null) {
|
|
9110
|
-
|
|
9589
|
+
const merge = () => this.beef.mergeBeefFromParty(this.storageParty, resultBeef ?? r.tx);
|
|
9590
|
+
if (this.telemetry.enabled) this.telemetry.withSpan("wallet.create_action.merge_result_beef", {
|
|
9591
|
+
component: "wallet-toolbox",
|
|
9592
|
+
carrier: args,
|
|
9593
|
+
attributes: {
|
|
9594
|
+
"beef.retained_tx_count_before": this.beef.txs.length,
|
|
9595
|
+
"beef.result_byte_count": r.tx.length
|
|
9596
|
+
}
|
|
9597
|
+
}, merge);
|
|
9598
|
+
else merge();
|
|
9599
|
+
}
|
|
9600
|
+
if (r.tx != null) {
|
|
9601
|
+
const verify = () => this.verifyReturnedTxidOnlyAtomicBEEF(r.tx, args.options?.knownTxids, resultBeef);
|
|
9602
|
+
r.tx = this.telemetry.enabled ? this.telemetry.withSpan("wallet.create_action.verify_result_beef", {
|
|
9603
|
+
component: "wallet-toolbox",
|
|
9604
|
+
carrier: args,
|
|
9605
|
+
attributes: { "beef.result_byte_count": r.tx.length }
|
|
9606
|
+
}, verify) : verify();
|
|
9111
9607
|
logger?.log("verify returned AtomicBEEF");
|
|
9112
9608
|
}
|
|
9113
9609
|
if (!vargs.isDelayed) throwIfAnyUnsuccessfulCreateActions(r);
|
|
@@ -9488,7 +9984,7 @@ async function createActionCore(storage, auth, vargs, parent) {
|
|
|
9488
9984
|
});
|
|
9489
9985
|
const feeModel = validateStorageFeeModel(storage.feeModel);
|
|
9490
9986
|
logger?.log(`validated fee model ${JSON.stringify(feeModel)}`);
|
|
9491
|
-
const initialFundingPlan = await prepareFundingPlan(storage,
|
|
9987
|
+
const initialFundingPlan = await prepareFundingPlan(storage, [
|
|
9492
9988
|
userId,
|
|
9493
9989
|
vargs,
|
|
9494
9990
|
xinputs,
|
|
@@ -9497,48 +9993,64 @@ async function createActionCore(storage, auth, vargs, parent) {
|
|
|
9497
9993
|
noSendChangeIn,
|
|
9498
9994
|
feeModel,
|
|
9499
9995
|
parent
|
|
9500
|
-
|
|
9996
|
+
]);
|
|
9501
9997
|
logger?.log(`planned funding from ${initialFundingPlan.availableChangeCount} change inputs`);
|
|
9998
|
+
const allocatedBeefPrefetch = startAllocatedChangeBeefPrefetch(storage, vargs, initialFundingPlan.selected, beef, parent);
|
|
9999
|
+
const storageBeefBytes = storageBeef.toBinary();
|
|
9502
10000
|
let newTx;
|
|
10001
|
+
let newTxCommitted = false;
|
|
9503
10002
|
try {
|
|
9504
|
-
const
|
|
9505
|
-
|
|
9506
|
-
"
|
|
9507
|
-
|
|
9508
|
-
|
|
9509
|
-
|
|
9510
|
-
|
|
9511
|
-
|
|
9512
|
-
|
|
9513
|
-
|
|
9514
|
-
|
|
9515
|
-
|
|
9516
|
-
|
|
9517
|
-
|
|
9518
|
-
|
|
9519
|
-
|
|
9520
|
-
|
|
9521
|
-
|
|
9522
|
-
|
|
9523
|
-
|
|
9524
|
-
|
|
9525
|
-
|
|
9526
|
-
|
|
9527
|
-
|
|
9528
|
-
|
|
9529
|
-
|
|
9530
|
-
|
|
9531
|
-
|
|
9532
|
-
|
|
9533
|
-
|
|
9534
|
-
|
|
9535
|
-
|
|
9536
|
-
const
|
|
9537
|
-
|
|
9538
|
-
|
|
10003
|
+
const persisted = await storage.transaction(async (trx) => {
|
|
10004
|
+
const initialSatoshis = fundingPlanSatoshis(initialFundingPlan);
|
|
10005
|
+
newTx = await traceStorageStep(storage, "wallet.storage.create_action.create_record", parent, {
|
|
10006
|
+
"action.label_count": vargs.labels.length,
|
|
10007
|
+
"action.storage_beef_bytes": storageBeefBytes.length
|
|
10008
|
+
}, async (span) => {
|
|
10009
|
+
const transaction = await createNewTxRecord(storage, userId, vargs, storageBeefBytes, initialSatoshis, trx);
|
|
10010
|
+
span?.end({ attributes: { "action.transaction_record_created": true } });
|
|
10011
|
+
return transaction;
|
|
10012
|
+
});
|
|
10013
|
+
logger?.log("created new transaction record");
|
|
10014
|
+
const ctx = {
|
|
10015
|
+
xinputs,
|
|
10016
|
+
xoutputs,
|
|
10017
|
+
changeBasket,
|
|
10018
|
+
noSendChangeIn,
|
|
10019
|
+
feeModel,
|
|
10020
|
+
transactionId: newTx.transactionId
|
|
10021
|
+
};
|
|
10022
|
+
const funded = await fundNewTransactionSdk(storage, userId, vargs, ctx, initialFundingPlan, parent, trx);
|
|
10023
|
+
logger?.log("funded new transaction");
|
|
10024
|
+
if (funded.maxPossibleSatoshisAdjustment != null) {
|
|
10025
|
+
const adjustment = funded.maxPossibleSatoshisAdjustment;
|
|
10026
|
+
if (ctx.xoutputs[adjustment.fixedOutputIndex].satoshis !== 0x775f05a073fff) throw new WERR_INTERNAL();
|
|
10027
|
+
ctx.xoutputs[adjustment.fixedOutputIndex].satoshis = adjustment.satoshis;
|
|
10028
|
+
logger?.log("adjusted change outputs to max possible");
|
|
10029
|
+
}
|
|
10030
|
+
const satoshis = funded.changeOutputs.reduce((sum, output) => sum + output.satoshis, 0) - funded.allocatedChange.reduce((sum, output) => sum + output.satoshis, 0);
|
|
10031
|
+
if (satoshis !== initialSatoshis) {
|
|
10032
|
+
await storage.updateTransaction(newTx.transactionId, { satoshis }, trx);
|
|
10033
|
+
newTx.satoshis = satoshis;
|
|
10034
|
+
}
|
|
10035
|
+
const storedOutputs = await traceStorageStep(storage, "wallet.storage.create_action.persist_outputs", parent, {
|
|
10036
|
+
"action.fixed_output_count": ctx.xoutputs.length,
|
|
10037
|
+
"action.change_output_count": funded.changeOutputs.length
|
|
10038
|
+
}, async (span) => {
|
|
10039
|
+
const result = await createNewOutputs(storage, userId, vargs, ctx, funded.changeOutputs, trx);
|
|
10040
|
+
span?.end({ attributes: { "action.persisted_output_count": result.outputs.length } });
|
|
10041
|
+
return result;
|
|
10042
|
+
});
|
|
10043
|
+
return {
|
|
10044
|
+
...funded,
|
|
10045
|
+
...storedOutputs,
|
|
10046
|
+
ctx
|
|
10047
|
+
};
|
|
9539
10048
|
});
|
|
10049
|
+
newTxCommitted = true;
|
|
10050
|
+
const committedTx = verifyTruthy(newTx);
|
|
10051
|
+
const { allocatedChange, derivationPrefix, outputs, changeVouts, ctx } = persisted;
|
|
9540
10052
|
logger?.log("created new output records");
|
|
9541
|
-
const inputBeef = await mergeAllocatedChangeBeefs(storage, vargs, allocatedChange, beef, parent);
|
|
10053
|
+
const inputBeef = await mergeAllocatedChangeBeefs(storage, vargs, allocatedChange, beef, allocatedBeefPrefetch, parent);
|
|
9542
10054
|
logger?.log("merged allocated change beefs");
|
|
9543
10055
|
const inputs = await traceStorageStep(storage, "wallet.storage.create_action.assemble_inputs", parent, {
|
|
9544
10056
|
"action.fixed_input_count": ctx.xinputs.length,
|
|
@@ -9551,9 +10063,9 @@ async function createActionCore(storage, auth, vargs, parent) {
|
|
|
9551
10063
|
});
|
|
9552
10064
|
logger?.log("created new inputs");
|
|
9553
10065
|
const r = {
|
|
9554
|
-
reference:
|
|
9555
|
-
version:
|
|
9556
|
-
lockTime:
|
|
10066
|
+
reference: committedTx.reference,
|
|
10067
|
+
version: committedTx.version,
|
|
10068
|
+
lockTime: committedTx.lockTime,
|
|
9557
10069
|
inputs,
|
|
9558
10070
|
outputs,
|
|
9559
10071
|
derivationPrefix,
|
|
@@ -9563,9 +10075,15 @@ async function createActionCore(storage, auth, vargs, parent) {
|
|
|
9563
10075
|
logger?.groupEnd();
|
|
9564
10076
|
return r;
|
|
9565
10077
|
} catch (error) {
|
|
10078
|
+
await allocatedBeefPrefetch;
|
|
9566
10079
|
if (newTx?.transactionId != null) try {
|
|
9567
|
-
|
|
9568
|
-
|
|
10080
|
+
if (newTxCommitted) {
|
|
10081
|
+
await storage.updateTransactionStatus("failed", newTx.transactionId);
|
|
10082
|
+
logger?.log(`marked failed createAction transaction ${newTx.transactionId} after construction error`);
|
|
10083
|
+
} else {
|
|
10084
|
+
const failed = await createNewTxRecord(storage, userId, vargs, storageBeefBytes, 0, void 0, "failed");
|
|
10085
|
+
logger?.log(`recorded failed createAction transaction ${failed.transactionId} after rollback`);
|
|
10086
|
+
}
|
|
9569
10087
|
} catch (cleanupError) {
|
|
9570
10088
|
logger?.log(`failed to clean up createAction transaction ${newTx.transactionId}: ${String(cleanupError)}`);
|
|
9571
10089
|
}
|
|
@@ -9715,23 +10233,10 @@ async function getCompetingBeefForReview(storage, txid) {
|
|
|
9715
10233
|
throw e;
|
|
9716
10234
|
}
|
|
9717
10235
|
}
|
|
9718
|
-
/**
|
|
9719
|
-
|
|
9720
|
-
async function persistNewOutput(storage, o, tags, txTags, txBaskets) {
|
|
9721
|
-
o.outputId = await storage.insertOutput(o);
|
|
9722
|
-
const changeVout = o.change && o.purpose === "change" && o.providedBy === "storage" ? o.vout : void 0;
|
|
9723
|
-
for (const tagName of new Set(tags)) {
|
|
9724
|
-
const tag = txTags[tagName];
|
|
9725
|
-
await storage.insertOutputTagMap({
|
|
9726
|
-
outputId: verifyId(o.outputId),
|
|
9727
|
-
outputTagId: verifyId(tag.outputTagId),
|
|
9728
|
-
created_at: /* @__PURE__ */ new Date(),
|
|
9729
|
-
updated_at: /* @__PURE__ */ new Date(),
|
|
9730
|
-
isDeleted: false
|
|
9731
|
-
});
|
|
9732
|
-
}
|
|
10236
|
+
/** Build the SDK descriptor for a persisted output. */
|
|
10237
|
+
function describeNewOutput(o, tags, txBaskets) {
|
|
9733
10238
|
return {
|
|
9734
|
-
changeVout,
|
|
10239
|
+
changeVout: o.change && o.purpose === "change" && o.providedBy === "storage" ? o.vout : void 0,
|
|
9735
10240
|
ro: {
|
|
9736
10241
|
vout: verifyInteger(o.vout),
|
|
9737
10242
|
satoshis: Validation.validateSatoshis(o.satoshis, "o.satoshis"),
|
|
@@ -9746,13 +10251,28 @@ async function persistNewOutput(storage, o, tags, txTags, txBaskets) {
|
|
|
9746
10251
|
}
|
|
9747
10252
|
};
|
|
9748
10253
|
}
|
|
9749
|
-
|
|
10254
|
+
/** Insert the output and attach its tags; return the SDK output descriptor. */
|
|
10255
|
+
async function persistNewOutput(storage, o, tags, txTags, txBaskets, trx) {
|
|
10256
|
+
o.outputId = await storage.insertOutput(o, trx);
|
|
10257
|
+
for (const tagName of new Set(tags)) {
|
|
10258
|
+
const tag = txTags[tagName];
|
|
10259
|
+
await storage.insertOutputTagMap({
|
|
10260
|
+
outputId: verifyId(o.outputId),
|
|
10261
|
+
outputTagId: verifyId(tag.outputTagId),
|
|
10262
|
+
created_at: /* @__PURE__ */ new Date(),
|
|
10263
|
+
updated_at: /* @__PURE__ */ new Date(),
|
|
10264
|
+
isDeleted: false
|
|
10265
|
+
}, trx);
|
|
10266
|
+
}
|
|
10267
|
+
return describeNewOutput(o, tags, txBaskets);
|
|
10268
|
+
}
|
|
10269
|
+
async function createNewOutputs(storage, userId, vargs, ctx, changeOutputs, trx) {
|
|
9750
10270
|
const txBaskets = {};
|
|
9751
10271
|
const basketNames = [...new Set(ctx.xoutputs.map((x) => x.basket).filter((v) => !!v))];
|
|
9752
|
-
Object.assign(txBaskets, await storage.findOrInsertOutputBasketsBulk(userId, basketNames));
|
|
10272
|
+
Object.assign(txBaskets, await storage.findOrInsertOutputBasketsBulk(userId, basketNames, trx));
|
|
9753
10273
|
const txTags = {};
|
|
9754
10274
|
const tagNames = [...new Set(ctx.xoutputs.flatMap((x) => x.tags))];
|
|
9755
|
-
Object.assign(txTags, await storage.findOrInsertOutputTagsBulk(userId, tagNames));
|
|
10275
|
+
Object.assign(txTags, await storage.findOrInsertOutputTagsBulk(userId, tagNames, trx));
|
|
9756
10276
|
const newOutputs = [];
|
|
9757
10277
|
for (const xo of ctx.xoutputs) {
|
|
9758
10278
|
const lockingScript = asArray(xo.lockingScript);
|
|
@@ -9768,7 +10288,7 @@ async function createNewOutputs(storage, userId, vargs, ctx, changeOutputs) {
|
|
|
9768
10288
|
created_at: now,
|
|
9769
10289
|
updated_at: now,
|
|
9770
10290
|
commissionId: 0
|
|
9771
|
-
});
|
|
10291
|
+
}, trx);
|
|
9772
10292
|
const o = makeDefaultOutput(userId, ctx.transactionId, xo.satoshis, xo.vout);
|
|
9773
10293
|
o.lockingScript = lockingScript;
|
|
9774
10294
|
o.providedBy = "storage";
|
|
@@ -9802,10 +10322,12 @@ async function createNewOutputs(storage, userId, vargs, ctx, changeOutputs) {
|
|
|
9802
10322
|
});
|
|
9803
10323
|
}
|
|
9804
10324
|
if (vargs.options.randomizeOutputs) randomizeOutputVouts(newOutputs.map((output) => output.o), vargs.randomVals);
|
|
10325
|
+
const untagged = newOutputs.filter((output) => output.tags.length === 0);
|
|
10326
|
+
await storage.insertOutputs(untagged.map((output) => output.o), trx);
|
|
9805
10327
|
const outputs = [];
|
|
9806
10328
|
const changeVouts = [];
|
|
9807
10329
|
for (const { o, tags } of newOutputs) {
|
|
9808
|
-
const { changeVout, ro } = await persistNewOutput(storage, o, tags, txTags, txBaskets);
|
|
10330
|
+
const { changeVout, ro } = tags.length === 0 ? describeNewOutput(o, tags, txBaskets) : await persistNewOutput(storage, o, tags, txTags, txBaskets, trx);
|
|
9809
10331
|
if (changeVout !== void 0) changeVouts.push(changeVout);
|
|
9810
10332
|
outputs.push(ro);
|
|
9811
10333
|
}
|
|
@@ -9814,7 +10336,7 @@ async function createNewOutputs(storage, userId, vargs, ctx, changeOutputs) {
|
|
|
9814
10336
|
changeVouts
|
|
9815
10337
|
};
|
|
9816
10338
|
}
|
|
9817
|
-
async function createNewTxRecord(storage, userId, vargs, storageBeef) {
|
|
10339
|
+
async function createNewTxRecord(storage, userId, vargs, storageBeef, satoshis = 0, trx, status = "unsigned") {
|
|
9818
10340
|
const now = /* @__PURE__ */ new Date();
|
|
9819
10341
|
const newTx = {
|
|
9820
10342
|
created_at: now,
|
|
@@ -9822,9 +10344,9 @@ async function createNewTxRecord(storage, userId, vargs, storageBeef) {
|
|
|
9822
10344
|
transactionId: 0,
|
|
9823
10345
|
version: vargs.version,
|
|
9824
10346
|
lockTime: vargs.lockTime,
|
|
9825
|
-
status
|
|
10347
|
+
status,
|
|
9826
10348
|
reference: randomBytesBase64(12),
|
|
9827
|
-
satoshis
|
|
10349
|
+
satoshis,
|
|
9828
10350
|
userId,
|
|
9829
10351
|
isOutgoing: true,
|
|
9830
10352
|
inputBEEF: storageBeef,
|
|
@@ -9832,12 +10354,12 @@ async function createNewTxRecord(storage, userId, vargs, storageBeef) {
|
|
|
9832
10354
|
txid: void 0,
|
|
9833
10355
|
rawTx: void 0
|
|
9834
10356
|
};
|
|
9835
|
-
newTx.transactionId = await storage.insertTransaction(newTx);
|
|
10357
|
+
newTx.transactionId = await storage.insertTransaction(newTx, trx);
|
|
9836
10358
|
const labelNames = [...new Set(vargs.labels)];
|
|
9837
|
-
const labels = await storage.findOrInsertTxLabelsBulk(userId, labelNames);
|
|
10359
|
+
const labels = await storage.findOrInsertTxLabelsBulk(userId, labelNames, trx);
|
|
9838
10360
|
for (const label of labelNames) {
|
|
9839
10361
|
const txLabel = labels[label];
|
|
9840
|
-
await storage.findOrInsertTxLabelMap(verifyId(newTx.transactionId), verifyId(txLabel.txLabelId));
|
|
10362
|
+
await storage.findOrInsertTxLabelMap(verifyId(newTx.transactionId), verifyId(txLabel.txLabelId), trx);
|
|
9841
10363
|
}
|
|
9842
10364
|
return newTx;
|
|
9843
10365
|
}
|
|
@@ -10033,6 +10555,9 @@ async function validateNoSendChange(storage, userId, vargs, changeBasket) {
|
|
|
10033
10555
|
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");
|
|
10034
10556
|
return r;
|
|
10035
10557
|
}
|
|
10558
|
+
function fundingPlanSatoshis(plan) {
|
|
10559
|
+
return plan.result.changeOutputs.reduce((sum, output) => sum + output.satoshis, 0) - plan.selected.reduce((sum, output) => sum + output.satoshis, 0);
|
|
10560
|
+
}
|
|
10036
10561
|
var FundingClaimConflict = class extends Error {
|
|
10037
10562
|
conflict;
|
|
10038
10563
|
constructor(conflict) {
|
|
@@ -10068,10 +10593,10 @@ function makeFundingParams(vargs, xinputs, xoutputs, changeBasket, feeModel, ava
|
|
|
10068
10593
|
};
|
|
10069
10594
|
}
|
|
10070
10595
|
async function prepareFundingPlan(storage, context) {
|
|
10071
|
-
const
|
|
10596
|
+
const [userId, vargs, xinputs, xoutputs, changeBasket, noSendChangeIn, feeModel, parent, trx] = context;
|
|
10072
10597
|
const excludeSending = !vargs.isDelayed;
|
|
10073
10598
|
const candidates = await traceStorageStep(storage, "wallet.storage.create_action.funding_candidates", parent, { "funding.exclude_sending": excludeSending }, async (span) => {
|
|
10074
|
-
const outputs = await storage.
|
|
10599
|
+
const outputs = await storage.findAvailableManagedChangeInputCandidates(userId, changeBasket.basketId, excludeSending, trx);
|
|
10075
10600
|
span?.end({ attributes: {
|
|
10076
10601
|
"funding.candidate_count": outputs.length,
|
|
10077
10602
|
"funding.candidate_satoshis": outputs.reduce((sum, output) => sum + output.satoshis, 0)
|
|
@@ -10086,10 +10611,12 @@ async function prepareFundingPlan(storage, context) {
|
|
|
10086
10611
|
"funding.no_send_change_count": noSendChangeIn.length
|
|
10087
10612
|
}, async (span) => {
|
|
10088
10613
|
const allocated = /* @__PURE__ */ new Map();
|
|
10614
|
+
const availableSelector = new CanonicalChangeSelector(available);
|
|
10089
10615
|
const noSend = [...noSendChangeIn];
|
|
10616
|
+
const noSendById = new Map(noSendChangeIn.map((output) => [output.outputId, output]));
|
|
10090
10617
|
const allocate = async (targetSatoshis, exactSatoshis) => {
|
|
10091
10618
|
let output = noSend.pop();
|
|
10092
|
-
output ??=
|
|
10619
|
+
output ??= availableSelector.take(targetSatoshis, exactSatoshis);
|
|
10093
10620
|
if (output == null) return void 0;
|
|
10094
10621
|
allocated.set(output.outputId, output);
|
|
10095
10622
|
return {
|
|
@@ -10098,10 +10625,11 @@ async function prepareFundingPlan(storage, context) {
|
|
|
10098
10625
|
};
|
|
10099
10626
|
};
|
|
10100
10627
|
const release = async (outputId) => {
|
|
10101
|
-
|
|
10102
|
-
if (output == null) return;
|
|
10628
|
+
if (allocated.get(outputId) == null) return;
|
|
10103
10629
|
allocated.delete(outputId);
|
|
10104
|
-
|
|
10630
|
+
availableSelector.release(outputId);
|
|
10631
|
+
const noSendOutput = noSendById.get(outputId);
|
|
10632
|
+
if (noSendOutput != null) noSend.push(noSendOutput);
|
|
10105
10633
|
};
|
|
10106
10634
|
const result = await generateChangeSdk(params, allocate, release, vargs.logger, storage.telemetry);
|
|
10107
10635
|
const selected = result.allocatedChangeInputs.map((input) => verifyTruthy(allocated.get(input.outputId)));
|
|
@@ -10119,7 +10647,8 @@ async function prepareFundingPlan(storage, context) {
|
|
|
10119
10647
|
};
|
|
10120
10648
|
});
|
|
10121
10649
|
}
|
|
10122
|
-
async function claimFundingPlan(storage,
|
|
10650
|
+
async function claimFundingPlan(storage, request) {
|
|
10651
|
+
const [userId, basketId, excludeSending, transactionId, noSendChangeIn, plan, trx] = request;
|
|
10123
10652
|
if (plan.selected.length === 0) return {
|
|
10124
10653
|
outputs: [],
|
|
10125
10654
|
sourceTransactionCount: 0,
|
|
@@ -10129,27 +10658,16 @@ async function claimFundingPlan(storage, userId, basketId, excludeSending, trans
|
|
|
10129
10658
|
const noSendIds = new Set(noSendChangeIn.map((output) => output.outputId));
|
|
10130
10659
|
const statuses = ["completed", "unproven"];
|
|
10131
10660
|
if (!excludeSending) statuses.push("sending");
|
|
10132
|
-
const claim = await storage.transaction(async (
|
|
10133
|
-
const
|
|
10134
|
-
|
|
10135
|
-
return {
|
|
10136
|
-
txid: output.txid,
|
|
10137
|
-
vout: output.vout
|
|
10138
|
-
};
|
|
10139
|
-
});
|
|
10140
|
-
const currentByOutpoint = await storage.findOutputsByOutpointsForUpdate(userId, outpoints, trx, true);
|
|
10141
|
-
const reserved = new Set(await storage.findReservedActionBatchOutputIds(plan.selected.map((output) => output.outputId), trx));
|
|
10142
|
-
const transactionIds = [...new Set(Object.values(currentByOutpoint).map((output) => output.transactionId))];
|
|
10143
|
-
const transactionStatuses = await storage.findTransactionStatusesByIds(userId, transactionIds, trx);
|
|
10661
|
+
const claim = await storage.transaction(async (claimTrx) => {
|
|
10662
|
+
const currentById = await storage.findFundingOutputsForUpdate(userId, plan.selected.map((output) => output.outputId), statuses, claimTrx);
|
|
10663
|
+
const transactionIds = [...new Set(Object.values(currentById).map((output) => output.transactionId))];
|
|
10144
10664
|
const claimed = [];
|
|
10145
10665
|
for (const planned of plan.selected) {
|
|
10146
|
-
const current =
|
|
10147
|
-
|
|
10148
|
-
const validTransaction = currentStatus != null && statuses.includes(currentStatus);
|
|
10149
|
-
if (current?.outputId !== planned.outputId || current?.satoshis !== planned.satoshis || current?.basketId !== basketId || !isAutoSpendableChangeOutput(current) || reserved.has(current?.outputId ?? -1) || validTransaction !== true) return { conflict: noSendIds.has(planned.outputId) ? "noSendChange" : "candidate" };
|
|
10666
|
+
const current = currentById[planned.outputId];
|
|
10667
|
+
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" };
|
|
10150
10668
|
claimed.push(current);
|
|
10151
10669
|
}
|
|
10152
|
-
if (await storage.markChangeInputsSpent(claimed.map((output) => output.outputId), transactionId,
|
|
10670
|
+
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");
|
|
10153
10671
|
for (const output of claimed) {
|
|
10154
10672
|
output.spendable = false;
|
|
10155
10673
|
output.spentBy = transactionId;
|
|
@@ -10158,19 +10676,19 @@ async function claimFundingPlan(storage, userId, basketId, excludeSending, trans
|
|
|
10158
10676
|
outputs: claimed,
|
|
10159
10677
|
sourceTransactionCount: transactionIds.length
|
|
10160
10678
|
};
|
|
10161
|
-
}).catch((error) => {
|
|
10679
|
+
}, trx).catch((error) => {
|
|
10162
10680
|
if (error instanceof FundingClaimConflict) return { conflict: error.conflict };
|
|
10163
10681
|
throw error;
|
|
10164
10682
|
});
|
|
10165
10683
|
if (claim.outputs == null) return claim;
|
|
10166
|
-
const hydration = await hydrateFundingInputScripts(storage, claim.outputs);
|
|
10684
|
+
const hydration = await hydrateFundingInputScripts(storage, claim.outputs, trx);
|
|
10167
10685
|
return {
|
|
10168
10686
|
outputs: claim.outputs,
|
|
10169
10687
|
sourceTransactionCount: claim.sourceTransactionCount,
|
|
10170
10688
|
...hydration
|
|
10171
10689
|
};
|
|
10172
10690
|
}
|
|
10173
|
-
async function hydrateFundingInputScripts(storage, outputs) {
|
|
10691
|
+
async function hydrateFundingInputScripts(storage, outputs, trx) {
|
|
10174
10692
|
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 !== "");
|
|
10175
10693
|
if (missing.length === 0) return {
|
|
10176
10694
|
hydratedScriptCount: 0,
|
|
@@ -10189,12 +10707,12 @@ async function hydrateFundingInputScripts(storage, outputs) {
|
|
|
10189
10707
|
while (cursor < groups.length) {
|
|
10190
10708
|
const [txid, group] = groups[cursor++];
|
|
10191
10709
|
if (group.length === 1) {
|
|
10192
|
-
await storage.validateOutputScript(group[0]);
|
|
10710
|
+
await storage.validateOutputScript(group[0], trx);
|
|
10193
10711
|
continue;
|
|
10194
10712
|
}
|
|
10195
|
-
const rawTx = await storage.getRawTxOfKnownValidTransaction(txid);
|
|
10713
|
+
const rawTx = await storage.getRawTxOfKnownValidTransaction(txid, void 0, void 0, trx);
|
|
10196
10714
|
if (rawTx != null) for (const output of group) output.lockingScript = rawTx.slice(output.scriptOffset, output.scriptOffset + output.scriptLength);
|
|
10197
|
-
else for (const output of group) await storage.validateOutputScript(output);
|
|
10715
|
+
else for (const output of group) await storage.validateOutputScript(output, trx);
|
|
10198
10716
|
}
|
|
10199
10717
|
}));
|
|
10200
10718
|
return {
|
|
@@ -10202,13 +10720,21 @@ async function hydrateFundingInputScripts(storage, outputs) {
|
|
|
10202
10720
|
scriptSourceTransactionCount: groups.length
|
|
10203
10721
|
};
|
|
10204
10722
|
}
|
|
10205
|
-
async function fundNewTransactionSdk(storage, userId, vargs, ctx, initialPlan, parent) {
|
|
10723
|
+
async function fundNewTransactionSdk(storage, userId, vargs, ctx, initialPlan, parent, trx) {
|
|
10206
10724
|
let plan = initialPlan;
|
|
10207
10725
|
let allocatedChange;
|
|
10208
10726
|
let retryCount = 0;
|
|
10209
10727
|
await traceStorageStep(storage, "wallet.storage.create_action.funding_claim", parent, { "funding.planned_input_count": initialPlan.selected.length }, async (span) => {
|
|
10210
10728
|
for (let attempt = 0; attempt < 3; attempt++) {
|
|
10211
|
-
const claim = await claimFundingPlan(storage,
|
|
10729
|
+
const claim = await claimFundingPlan(storage, [
|
|
10730
|
+
userId,
|
|
10731
|
+
ctx.changeBasket.basketId,
|
|
10732
|
+
!vargs.isDelayed,
|
|
10733
|
+
ctx.transactionId,
|
|
10734
|
+
ctx.noSendChangeIn,
|
|
10735
|
+
plan,
|
|
10736
|
+
trx
|
|
10737
|
+
]);
|
|
10212
10738
|
if (claim.outputs != null) {
|
|
10213
10739
|
allocatedChange = claim.outputs;
|
|
10214
10740
|
span?.end({ attributes: {
|
|
@@ -10221,16 +10747,17 @@ async function fundNewTransactionSdk(storage, userId, vargs, ctx, initialPlan, p
|
|
|
10221
10747
|
}
|
|
10222
10748
|
if (claim.conflict === "noSendChange") throw new WERR_INVALID_PARAMETER("noSendChange", "outputs that remain spendable during action planning");
|
|
10223
10749
|
retryCount++;
|
|
10224
|
-
plan = await prepareFundingPlan(storage,
|
|
10750
|
+
plan = await prepareFundingPlan(storage, [
|
|
10225
10751
|
userId,
|
|
10226
10752
|
vargs,
|
|
10227
|
-
|
|
10228
|
-
|
|
10229
|
-
|
|
10230
|
-
|
|
10231
|
-
|
|
10232
|
-
parent
|
|
10233
|
-
|
|
10753
|
+
ctx.xinputs,
|
|
10754
|
+
ctx.xoutputs,
|
|
10755
|
+
ctx.changeBasket,
|
|
10756
|
+
ctx.noSendChangeIn,
|
|
10757
|
+
ctx.feeModel,
|
|
10758
|
+
parent,
|
|
10759
|
+
trx
|
|
10760
|
+
]);
|
|
10234
10761
|
}
|
|
10235
10762
|
throw new WERR_INVALID_OPERATION("wallet funding changed repeatedly during action planning; retry createAction");
|
|
10236
10763
|
});
|
|
@@ -10316,7 +10843,56 @@ function makeKnownTxidLookup(knownTxids) {
|
|
|
10316
10843
|
return knownTxids.includes(txid);
|
|
10317
10844
|
};
|
|
10318
10845
|
}
|
|
10319
|
-
|
|
10846
|
+
function missingAllocatedChangeTxids(allocatedChange, beef, knownTxids) {
|
|
10847
|
+
const hasKnownTxid = makeKnownTxidLookup(knownTxids);
|
|
10848
|
+
return Array.from(new Set(allocatedChange.map((output) => verifyTruthy(output.txid)).filter((txid) => beef.findTxid(txid) == null && !hasKnownTxid(txid))));
|
|
10849
|
+
}
|
|
10850
|
+
function startAllocatedChangeBeefPrefetch(storage, vargs, allocatedChange, beef, parent) {
|
|
10851
|
+
if (vargs.options.returnTXIDOnly) return Promise.resolve({
|
|
10852
|
+
sourceCount: 0,
|
|
10853
|
+
txids: []
|
|
10854
|
+
});
|
|
10855
|
+
const knownTxids = vargs.options.knownTxids ?? [];
|
|
10856
|
+
const missing = missingAllocatedChangeTxids(allocatedChange, beef, knownTxids);
|
|
10857
|
+
if (missing.length === 0) return Promise.resolve({
|
|
10858
|
+
sourceCount: 0,
|
|
10859
|
+
txids: []
|
|
10860
|
+
});
|
|
10861
|
+
const options = {
|
|
10862
|
+
trustSelf: void 0,
|
|
10863
|
+
knownTxids,
|
|
10864
|
+
ignoreStorage: false,
|
|
10865
|
+
ignoreServices: true,
|
|
10866
|
+
ignoreNewProven: false,
|
|
10867
|
+
minProofLevel: void 0
|
|
10868
|
+
};
|
|
10869
|
+
return traceStorageStep(storage, "wallet.storage.create_action.beef_prefetch", parent, {
|
|
10870
|
+
"beef.planned_source_count": allocatedChange.length,
|
|
10871
|
+
"beef.missing_source_count": missing.length,
|
|
10872
|
+
"beef.storage_batch_count": missing.length === 0 ? 0 : 1
|
|
10873
|
+
}, async (span) => {
|
|
10874
|
+
const fetched = await storage.getBeefForTransactions(missing, options);
|
|
10875
|
+
span?.end({ attributes: {
|
|
10876
|
+
"beef.fetched_tx_count": fetched.txs.length,
|
|
10877
|
+
"beef.fetched_bump_count": fetched.bumps.length
|
|
10878
|
+
} });
|
|
10879
|
+
return fetched;
|
|
10880
|
+
}).then((prefetched) => ({
|
|
10881
|
+
beef: prefetched,
|
|
10882
|
+
sourceCount: missing.length,
|
|
10883
|
+
txids: missing
|
|
10884
|
+
}), (error) => ({
|
|
10885
|
+
error,
|
|
10886
|
+
sourceCount: missing.length,
|
|
10887
|
+
txids: missing
|
|
10888
|
+
}));
|
|
10889
|
+
}
|
|
10890
|
+
function sameTxids(left, right) {
|
|
10891
|
+
if (left.length !== right.length) return false;
|
|
10892
|
+
const expected = new Set(left);
|
|
10893
|
+
return right.every((txid) => expected.has(txid));
|
|
10894
|
+
}
|
|
10895
|
+
async function mergeAllocatedChangeBeefs(storage, vargs, allocatedChange, beef, prefetch, parent) {
|
|
10320
10896
|
const options = {
|
|
10321
10897
|
trustSelf: void 0,
|
|
10322
10898
|
knownTxids: vargs.options.knownTxids,
|
|
@@ -10328,37 +10904,37 @@ async function mergeAllocatedChangeBeefs(storage, vargs, allocatedChange, beef,
|
|
|
10328
10904
|
};
|
|
10329
10905
|
if (vargs.options.returnTXIDOnly) return void 0;
|
|
10330
10906
|
const knownTxids = vargs.options.knownTxids ?? [];
|
|
10331
|
-
const
|
|
10332
|
-
const
|
|
10333
|
-
|
|
10334
|
-
|
|
10335
|
-
|
|
10907
|
+
const requiredBeforePrefetch = missingAllocatedChangeTxids(allocatedChange, beef, knownTxids);
|
|
10908
|
+
const prefetched = await traceStorageStep(storage, "wallet.storage.create_action.beef_prefetch_join", parent, { "beef.prefetch_source_count": 0 }, async (span) => {
|
|
10909
|
+
const result = await prefetch;
|
|
10910
|
+
span?.end({ attributes: { "beef.prefetch_source_count": result.sourceCount } });
|
|
10911
|
+
return result;
|
|
10912
|
+
});
|
|
10913
|
+
const usePrefetch = sameTxids(prefetched.txids, requiredBeforePrefetch);
|
|
10914
|
+
if (usePrefetch && prefetched.error != null) throw prefetched.error;
|
|
10915
|
+
if (usePrefetch && prefetched.beef != null) beef.mergeBeef(prefetched.beef);
|
|
10916
|
+
const missing = missingAllocatedChangeTxids(allocatedChange, beef, knownTxids);
|
|
10917
|
+
let fetched;
|
|
10336
10918
|
await traceStorageStep(storage, "wallet.storage.create_action.beef_fetch", parent, {
|
|
10337
10919
|
"beef.allocated_change_count": allocatedChange.length,
|
|
10338
10920
|
"beef.distinct_source_count": new Set(allocatedChange.map((output) => output.txid)).size,
|
|
10339
10921
|
"beef.known_txid_count": knownTxids.length,
|
|
10340
10922
|
"beef.missing_source_count": missing.length,
|
|
10341
|
-
"beef.fetch_concurrency":
|
|
10923
|
+
"beef.fetch_concurrency": 1,
|
|
10924
|
+
"beef.prefetch_reused": usePrefetch
|
|
10342
10925
|
}, async (span) => {
|
|
10343
|
-
|
|
10344
|
-
|
|
10345
|
-
|
|
10346
|
-
|
|
10347
|
-
...options,
|
|
10348
|
-
mergeToBeef: void 0
|
|
10349
|
-
});
|
|
10350
|
-
}
|
|
10351
|
-
}));
|
|
10926
|
+
if (missing.length > 0) fetched = await storage.getBeefForTransactions(missing, {
|
|
10927
|
+
...options,
|
|
10928
|
+
mergeToBeef: void 0
|
|
10929
|
+
});
|
|
10352
10930
|
span?.end({ attributes: {
|
|
10353
|
-
"beef.fetched_tx_count": fetched
|
|
10354
|
-
"beef.fetched_bump_count": fetched
|
|
10931
|
+
"beef.fetched_tx_count": fetched?.txs.length ?? 0,
|
|
10932
|
+
"beef.fetched_bump_count": fetched?.bumps.length ?? 0,
|
|
10933
|
+
"beef.storage_batch_count": missing.length === 0 ? 0 : 1
|
|
10355
10934
|
} });
|
|
10356
10935
|
});
|
|
10357
|
-
await traceStorageStep(storage, "wallet.storage.create_action.beef_merge", parent, { "beef.fragment_count":
|
|
10358
|
-
|
|
10359
|
-
if (fetchedBeef == null) continue;
|
|
10360
|
-
beef.mergeBeef(fetchedBeef);
|
|
10361
|
-
}
|
|
10936
|
+
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) => {
|
|
10937
|
+
if (fetched != null) beef.mergeBeef(fetched);
|
|
10362
10938
|
span?.end({ attributes: {
|
|
10363
10939
|
"beef.merged_tx_count": beef.txs.length,
|
|
10364
10940
|
"beef.merged_bump_count": beef.bumps.length
|
|
@@ -13640,10 +14216,28 @@ var StorageProvider = class StorageProvider extends StorageReaderWriter {
|
|
|
13640
14216
|
}
|
|
13641
14217
|
return updated;
|
|
13642
14218
|
}
|
|
14219
|
+
/**
|
|
14220
|
+
* Insert outputs that do not need their generated ids returned to the
|
|
14221
|
+
* caller. Engines with a multi-row insert override this common-path helper;
|
|
14222
|
+
* the fallback preserves existing storage implementations unchanged.
|
|
14223
|
+
*/
|
|
14224
|
+
async insertOutputs(outputs, trx) {
|
|
14225
|
+
for (const output of outputs) await this.insertOutput(output, trx);
|
|
14226
|
+
}
|
|
13643
14227
|
/** Return unreserved wallet-managed outputs eligible for automatic funding. */
|
|
13644
14228
|
async findAvailableManagedChangeInputs(userId, basketId, excludeSending, trx) {
|
|
13645
14229
|
return await availableManagedChange(this, userId, basketId, excludeSending, trx);
|
|
13646
14230
|
}
|
|
14231
|
+
/** Read only the fields needed by the in-memory funding planner. */
|
|
14232
|
+
async findAvailableManagedChangeInputCandidates(userId, basketId, excludeSending, trx) {
|
|
14233
|
+
return (await this.findAvailableManagedChangeInputs(userId, basketId, excludeSending, trx)).map(({ outputId, transactionId, satoshis, txid, vout }) => ({
|
|
14234
|
+
outputId,
|
|
14235
|
+
transactionId,
|
|
14236
|
+
satoshis,
|
|
14237
|
+
txid,
|
|
14238
|
+
vout
|
|
14239
|
+
}));
|
|
14240
|
+
}
|
|
13647
14241
|
/** Read the current status of a set of source transactions without loading raw transaction bytes. */
|
|
13648
14242
|
async findTransactionStatusesByIds(userId, transactionIds, trx) {
|
|
13649
14243
|
const statuses = /* @__PURE__ */ new Map();
|
|
@@ -13653,6 +14247,36 @@ var StorageProvider = class StorageProvider extends StorageReaderWriter {
|
|
|
13653
14247
|
}
|
|
13654
14248
|
return statuses;
|
|
13655
14249
|
}
|
|
14250
|
+
/**
|
|
14251
|
+
* Lock and return the selected funding rows whose source transaction and
|
|
14252
|
+
* action-batch reservation state still permit allocation.
|
|
14253
|
+
*/
|
|
14254
|
+
async findFundingOutputsForUpdate(userId, outputIds, statuses, trx) {
|
|
14255
|
+
const rows = await this.findOutputsByIds(outputIds, trx);
|
|
14256
|
+
const reserved = new Set(await this.findReservedActionBatchOutputIds(outputIds, trx));
|
|
14257
|
+
const transactionIds = [...new Set(Object.values(rows).map((output) => output.transactionId))];
|
|
14258
|
+
const transactionStatuses = await this.findTransactionStatusesByIds(userId, transactionIds, trx);
|
|
14259
|
+
const eligible = {};
|
|
14260
|
+
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;
|
|
14261
|
+
return eligible;
|
|
14262
|
+
}
|
|
14263
|
+
/**
|
|
14264
|
+
* Resolve several transaction proofs in one storage operation when the
|
|
14265
|
+
* backend supports it. The default preserves compatibility for custom
|
|
14266
|
+
* providers; SQL and IndexedDB providers override this hot path.
|
|
14267
|
+
*/
|
|
14268
|
+
async getProvenOrRawTxs(txids, trx) {
|
|
14269
|
+
const results = /* @__PURE__ */ new Map();
|
|
14270
|
+
const unique = [...new Set(txids)];
|
|
14271
|
+
let cursor = 0;
|
|
14272
|
+
await Promise.all(Array.from({ length: Math.min(8, unique.length) }, async () => {
|
|
14273
|
+
while (cursor < unique.length) {
|
|
14274
|
+
const txid = unique[cursor++];
|
|
14275
|
+
results.set(txid, await this.getProvenOrRawTx(txid, trx));
|
|
14276
|
+
}
|
|
14277
|
+
}));
|
|
14278
|
+
return results;
|
|
14279
|
+
}
|
|
13656
14280
|
async insertActionBatch(_batch, _trx) {
|
|
13657
14281
|
throw new WERR_NOT_IMPLEMENTED();
|
|
13658
14282
|
}
|
|
@@ -13704,6 +14328,10 @@ var StorageProvider = class StorageProvider extends StorageReaderWriter {
|
|
|
13704
14328
|
supportsActionBatchPersistence() {
|
|
13705
14329
|
return false;
|
|
13706
14330
|
}
|
|
14331
|
+
/** Custom providers may require physical expiry cleanup before reservations are queried. */
|
|
14332
|
+
requiresActionBatchCleanupBeforeCreateAction() {
|
|
14333
|
+
return true;
|
|
14334
|
+
}
|
|
13707
14335
|
async beginActionBatch(auth, args) {
|
|
13708
14336
|
if (!this.supportsActionBatchPersistence()) throw new WERR_NOT_IMPLEMENTED("actionBatch capability is not available");
|
|
13709
14337
|
return await beginActionBatch(this, auth, args);
|
|
@@ -14092,7 +14720,7 @@ var StorageProvider = class StorageProvider extends StorageReaderWriter {
|
|
|
14092
14720
|
}
|
|
14093
14721
|
async createAction(auth, args) {
|
|
14094
14722
|
if (auth.userId == null) throw new WERR_UNAUTHORIZED();
|
|
14095
|
-
if (this.supportsActionBatchPersistence()) await cleanupExpiredActionBatches(this);
|
|
14723
|
+
if (this.supportsActionBatchPersistence() && this.requiresActionBatchCleanupBeforeCreateAction()) await cleanupExpiredActionBatches(this);
|
|
14096
14724
|
return await createAction(this, auth, args);
|
|
14097
14725
|
}
|
|
14098
14726
|
async processAction(auth, args) {
|
|
@@ -14182,6 +14810,9 @@ var StorageProvider = class StorageProvider extends StorageReaderWriter {
|
|
|
14182
14810
|
async getBeefForTransaction(txid, options) {
|
|
14183
14811
|
return await getBeefForTransaction(this, txid, options);
|
|
14184
14812
|
}
|
|
14813
|
+
async getBeefForTransactions(txids, options) {
|
|
14814
|
+
return await getBeefForTransactions(this, txids, options);
|
|
14815
|
+
}
|
|
14185
14816
|
async findMonitorEventById(id, trx) {
|
|
14186
14817
|
return verifyOneOrNone(await this.findMonitorEvents({
|
|
14187
14818
|
partial: { id },
|
|
@@ -15140,6 +15771,9 @@ var StorageIdb = class extends StorageProvider {
|
|
|
15140
15771
|
supportsActionBatchPersistence() {
|
|
15141
15772
|
return true;
|
|
15142
15773
|
}
|
|
15774
|
+
requiresActionBatchCleanupBeforeCreateAction() {
|
|
15775
|
+
return false;
|
|
15776
|
+
}
|
|
15143
15777
|
/**
|
|
15144
15778
|
* This method must be called at least once before any other method accesses the database,
|
|
15145
15779
|
* and each time the schema may have updated.
|
|
@@ -15319,6 +15953,50 @@ var StorageIdb = class extends StorageProvider {
|
|
|
15319
15953
|
}
|
|
15320
15954
|
return r;
|
|
15321
15955
|
}
|
|
15956
|
+
async getProvenOrRawTxs(txids, trx) {
|
|
15957
|
+
const results = /* @__PURE__ */ new Map();
|
|
15958
|
+
const unique = [...new Set(txids)];
|
|
15959
|
+
if (unique.length === 0) return results;
|
|
15960
|
+
const dbTrx = this.toDbTrx(["proven_txs", "proven_tx_reqs"], "readonly", trx);
|
|
15961
|
+
const provenIndex = dbTrx.objectStore("proven_txs").index("txid");
|
|
15962
|
+
const requestIndex = dbTrx.objectStore("proven_tx_reqs").index("txid");
|
|
15963
|
+
const usableStatuses = /* @__PURE__ */ new Set([
|
|
15964
|
+
"unsent",
|
|
15965
|
+
"unmined",
|
|
15966
|
+
"unconfirmed",
|
|
15967
|
+
"sending",
|
|
15968
|
+
"nosend",
|
|
15969
|
+
"completed"
|
|
15970
|
+
]);
|
|
15971
|
+
await Promise.all(unique.map(async (txid) => {
|
|
15972
|
+
const proven = await provenIndex.get(txid);
|
|
15973
|
+
if (proven != null) {
|
|
15974
|
+
results.set(txid, {
|
|
15975
|
+
proven: this.validateEntity(proven),
|
|
15976
|
+
rawTx: void 0,
|
|
15977
|
+
inputBEEF: void 0
|
|
15978
|
+
});
|
|
15979
|
+
return;
|
|
15980
|
+
}
|
|
15981
|
+
const request = await requestIndex.get(txid);
|
|
15982
|
+
if (request != null && usableStatuses.has(request.status)) {
|
|
15983
|
+
const validated = this.validateEntity(request);
|
|
15984
|
+
results.set(txid, {
|
|
15985
|
+
proven: void 0,
|
|
15986
|
+
rawTx: Array.from(validated.rawTx),
|
|
15987
|
+
inputBEEF: validated.inputBEEF == null ? void 0 : Array.from(validated.inputBEEF)
|
|
15988
|
+
});
|
|
15989
|
+
return;
|
|
15990
|
+
}
|
|
15991
|
+
results.set(txid, {
|
|
15992
|
+
proven: void 0,
|
|
15993
|
+
rawTx: void 0,
|
|
15994
|
+
inputBEEF: void 0
|
|
15995
|
+
});
|
|
15996
|
+
}));
|
|
15997
|
+
if (trx == null) await dbTrx.done;
|
|
15998
|
+
return results;
|
|
15999
|
+
}
|
|
15322
16000
|
async getRawTxOfKnownValidTransaction(txid, offset, length, trx) {
|
|
15323
16001
|
if (txid == null || txid === "") return void 0;
|
|
15324
16002
|
if (!this.isAvailable()) await this.makeAvailable();
|
|
@@ -15580,6 +16258,7 @@ var StorageIdb = class extends StorageProvider {
|
|
|
15580
16258
|
else cursor = await store.openCursor(null, direction);
|
|
15581
16259
|
await scanCursor(cursor, args.since, args.paged?.offset ?? 0, args.paged?.limit, async (r) => {
|
|
15582
16260
|
if (!matchesProvenTxPartial(r, args.partial)) return false;
|
|
16261
|
+
if (args.txids != null && args.txids.length > 0 && !args.txids.includes(r.txid)) return false;
|
|
15583
16262
|
if (userId !== void 0) {
|
|
15584
16263
|
if (await this.countTransactions({
|
|
15585
16264
|
partial: {
|
|
@@ -16024,11 +16703,18 @@ var StorageIdb = class extends StorageProvider {
|
|
|
16024
16703
|
return rows.map((r) => r.outputId);
|
|
16025
16704
|
}
|
|
16026
16705
|
async findReservedActionBatchOutputIds(outputIds, trx) {
|
|
16027
|
-
const tx = this.toDbTrx(["action_batch_outputs"], "readonly", trx);
|
|
16706
|
+
const tx = this.toDbTrx(["action_batch_outputs", "action_batches"], "readonly", trx);
|
|
16028
16707
|
const store = tx.objectStore("action_batch_outputs");
|
|
16708
|
+
const batchStore = tx.objectStore("action_batches");
|
|
16029
16709
|
if (store.get == null) throw new WERR_INTERNAL("IndexedDB action_batch_outputs store does not support get");
|
|
16030
16710
|
const reserved = [];
|
|
16031
|
-
|
|
16711
|
+
const now = Date.now();
|
|
16712
|
+
for (const outputId of outputIds) {
|
|
16713
|
+
const reservation = await store.get(outputId);
|
|
16714
|
+
if (reservation == null) continue;
|
|
16715
|
+
const batch = await batchStore.get(reservation.actionBatchId);
|
|
16716
|
+
if (batch != null && (batch.status === "active" || batch.status === "prepared") && batch.expiresAt.getTime() > now && batch.hardExpiresAt.getTime() > now) reserved.push(outputId);
|
|
16717
|
+
}
|
|
16032
16718
|
if (trx == null) await tx.done;
|
|
16033
16719
|
return reserved;
|
|
16034
16720
|
}
|
|
@@ -27958,7 +28644,7 @@ function isValidProfile(value) {
|
|
|
27958
28644
|
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;
|
|
27959
28645
|
}
|
|
27960
28646
|
/**
|
|
27961
|
-
* Raised when UMP
|
|
28647
|
+
* Raised when a UMP lookup yields neither a verified token nor a clean empty response.
|
|
27962
28648
|
*
|
|
27963
28649
|
* Callers must offer retry/recovery rather than treating this error as a new
|
|
27964
28650
|
* account. Diagnostics contain counts only and never hashes, keys, or tokens.
|
|
@@ -28056,49 +28742,119 @@ var OverlayUMPTokenInteractor = class {
|
|
|
28056
28742
|
throw new UMPTokenLookupError("lookup-unavailable", diagnostics, { cause: error });
|
|
28057
28743
|
}
|
|
28058
28744
|
const diagnostics = this.toLookupDiagnostics(resolution);
|
|
28059
|
-
if (resolution.answer.outputs.length === 0) {
|
|
28060
|
-
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)) {
|
|
28061
|
-
this.captureLookupFailure(lookupKind, "lookup-incomplete", diagnostics, startedAt);
|
|
28062
|
-
throw new UMPTokenLookupError("lookup-incomplete", diagnostics);
|
|
28063
|
-
}
|
|
28064
|
-
this.telemetry.capture({
|
|
28065
|
-
name: "wallet-toolbox.ump.lookup.completed",
|
|
28066
|
-
component: "wallet-toolbox.ump",
|
|
28067
|
-
severity: "info",
|
|
28068
|
-
correlationId: diagnostics.correlationId,
|
|
28069
|
-
attributes: {
|
|
28070
|
-
lookupKind,
|
|
28071
|
-
result: "not-found",
|
|
28072
|
-
durationMs: Date.now() - startedAt,
|
|
28073
|
-
...this.lookupDiagnosticAttributes(diagnostics)
|
|
28074
|
-
}
|
|
28075
|
-
});
|
|
28076
|
-
return;
|
|
28077
|
-
}
|
|
28078
28745
|
const tokens = this.parseLookupAnswers(resolution.answer);
|
|
28079
28746
|
const expectedHash = question.query[lookupKind === "presentation" ? "presentationHash" : "recoveryHash"].toLowerCase();
|
|
28080
|
-
|
|
28081
|
-
|
|
28082
|
-
|
|
28083
|
-
|
|
28084
|
-
|
|
28747
|
+
const matchingTokens = tokens.filter((token) => Utils.toHex(lookupKind === "presentation" ? token.presentationHash : token.recoveryHash).toLowerCase() === expectedHash);
|
|
28748
|
+
if (matchingTokens.length > 1) {
|
|
28749
|
+
const newest = this.resolveNewestToken(matchingTokens, resolution.answer.outputs);
|
|
28750
|
+
if (newest != null) {
|
|
28751
|
+
this.captureLookupCompleted(lookupKind, "found", diagnostics, startedAt, { supersededTokens: matchingTokens.length - 1 });
|
|
28752
|
+
return newest;
|
|
28753
|
+
}
|
|
28085
28754
|
const reason = "token-ambiguous";
|
|
28086
28755
|
this.captureLookupFailure(lookupKind, reason, diagnostics, startedAt);
|
|
28087
28756
|
throw new UMPTokenLookupError(reason, diagnostics);
|
|
28088
28757
|
}
|
|
28089
|
-
|
|
28090
|
-
|
|
28091
|
-
|
|
28092
|
-
|
|
28093
|
-
|
|
28094
|
-
|
|
28095
|
-
|
|
28096
|
-
|
|
28097
|
-
|
|
28098
|
-
|
|
28758
|
+
if (matchingTokens.length === 1) {
|
|
28759
|
+
this.captureLookupCompleted(lookupKind, "found", diagnostics, startedAt);
|
|
28760
|
+
return matchingTokens[0];
|
|
28761
|
+
}
|
|
28762
|
+
if (resolution.progress.emptyHosts > 0) {
|
|
28763
|
+
this.captureLookupCompleted(lookupKind, "not-found", diagnostics, startedAt);
|
|
28764
|
+
return;
|
|
28765
|
+
}
|
|
28766
|
+
const reason = resolution.answer.outputs.length > 0 ? "token-malformed" : "lookup-incomplete";
|
|
28767
|
+
this.captureLookupFailure(lookupKind, reason, diagnostics, startedAt);
|
|
28768
|
+
throw new UMPTokenLookupError(reason, diagnostics);
|
|
28769
|
+
}
|
|
28770
|
+
/**
|
|
28771
|
+
* Picks the newest rendition among distinct verified tokens, when possible.
|
|
28772
|
+
*
|
|
28773
|
+
* The on-chain UMP protocol expresses token updates by consumption: the
|
|
28774
|
+
* transaction creating a new rendition spends the previous rendition's
|
|
28775
|
+
* outpoint (there is no rendition counter field in the current format).
|
|
28776
|
+
* A candidate is therefore superseded when any other candidate's ancestry
|
|
28777
|
+
* (available from its BEEF) spends the candidate's outpoint.
|
|
28778
|
+
*
|
|
28779
|
+
* @returns The single unsuperseded candidate, or undefined when supersession
|
|
28780
|
+
* cannot be established for every stale candidate (e.g. forked tokens).
|
|
28781
|
+
*/
|
|
28782
|
+
resolveNewestToken(matchingTokens, outputs) {
|
|
28783
|
+
const candidates = /* @__PURE__ */ new Map();
|
|
28784
|
+
for (const token of matchingTokens) {
|
|
28785
|
+
if (token.currentOutpoint == null) return void 0;
|
|
28786
|
+
candidates.set(token.currentOutpoint, token);
|
|
28787
|
+
}
|
|
28788
|
+
const evidenceByCandidate = /* @__PURE__ */ new Map();
|
|
28789
|
+
for (const output of outputs) try {
|
|
28790
|
+
const tx = Transaction.fromBEEF(output.beef);
|
|
28791
|
+
const outpoint = `${tx.id("hex")}.${output.outputIndex}`;
|
|
28792
|
+
if (!candidates.has(outpoint)) continue;
|
|
28793
|
+
const evidence = evidenceByCandidate.get(outpoint) ?? {
|
|
28794
|
+
txs: [],
|
|
28795
|
+
spent: /* @__PURE__ */ new Set()
|
|
28796
|
+
};
|
|
28797
|
+
evidence.txs.push(tx);
|
|
28798
|
+
this.collectSpentOutpoints(tx, evidence.spent, /* @__PURE__ */ new Set());
|
|
28799
|
+
evidenceByCandidate.set(outpoint, evidence);
|
|
28800
|
+
} catch {}
|
|
28801
|
+
if (evidenceByCandidate.size !== candidates.size) return void 0;
|
|
28802
|
+
const survivors = [...candidates.keys()].filter((outpoint) => ![...evidenceByCandidate.entries()].some(([other, { spent }]) => other !== outpoint && spent.has(outpoint)));
|
|
28803
|
+
if (survivors.length === 1) return candidates.get(survivors[0]);
|
|
28804
|
+
const provenContinuations = survivors.filter((outpoint) => {
|
|
28805
|
+
const evidence = evidenceByCandidate.get(outpoint);
|
|
28806
|
+
const token = candidates.get(outpoint);
|
|
28807
|
+
return evidence != null && token != null && evidence.txs.some((tx) => this.consumesSameIdentityToken(tx, token));
|
|
28808
|
+
});
|
|
28809
|
+
if (provenContinuations.length !== 1) return void 0;
|
|
28810
|
+
return candidates.get(provenContinuations[0]);
|
|
28811
|
+
}
|
|
28812
|
+
/**
|
|
28813
|
+
* Whether `tx` spends an input whose source output (available in the BEEF)
|
|
28814
|
+
* decodes as a UMP token sharing the candidate's presentation or recovery
|
|
28815
|
+
* hash — on-chain proof that the candidate is an update of a same-identity
|
|
28816
|
+
* predecessor rather than an independently minted token.
|
|
28817
|
+
*/
|
|
28818
|
+
consumesSameIdentityToken(tx, token) {
|
|
28819
|
+
const presentationHash = Utils.toHex(token.presentationHash);
|
|
28820
|
+
const recoveryHash = Utils.toHex(token.recoveryHash);
|
|
28821
|
+
for (const input of tx.inputs) {
|
|
28822
|
+
const source = input.sourceTransaction;
|
|
28823
|
+
if (source == null || input.sourceOutputIndex == null) continue;
|
|
28824
|
+
const sourceOutput = source.outputs[input.sourceOutputIndex];
|
|
28825
|
+
if (sourceOutput == null) continue;
|
|
28826
|
+
try {
|
|
28827
|
+
const decoded = PushDrop.decode(sourceOutput.lockingScript);
|
|
28828
|
+
if (decoded.fields == null) continue;
|
|
28829
|
+
const fields = stripVerifiedPushDropSignature(decoded.fields, decoded.lockingPublicKey);
|
|
28830
|
+
if (fields.length < 11 || fields[6]?.length !== 32 || fields[7]?.length !== 32) continue;
|
|
28831
|
+
if (Utils.toHex(fields[6]) === presentationHash || Utils.toHex(fields[7]) === recoveryHash) return true;
|
|
28832
|
+
} catch {
|
|
28833
|
+
continue;
|
|
28099
28834
|
}
|
|
28100
|
-
}
|
|
28101
|
-
return
|
|
28835
|
+
}
|
|
28836
|
+
return false;
|
|
28837
|
+
}
|
|
28838
|
+
/**
|
|
28839
|
+
* Accumulates every outpoint spent by `tx` and by the ancestor transactions
|
|
28840
|
+
* embedded in its BEEF, so supersession is detected even when intermediate
|
|
28841
|
+
* renditions are absent from the lookup answer. Iterative so arbitrarily
|
|
28842
|
+
* long update chains cannot exhaust the call stack.
|
|
28843
|
+
*/
|
|
28844
|
+
collectSpentOutpoints(tx, spent, visited) {
|
|
28845
|
+
const pending = [tx];
|
|
28846
|
+
while (pending.length > 0) {
|
|
28847
|
+
const current = pending.pop();
|
|
28848
|
+
const txid = current.id("hex");
|
|
28849
|
+
if (visited.has(txid)) continue;
|
|
28850
|
+
visited.add(txid);
|
|
28851
|
+
for (const input of current.inputs) {
|
|
28852
|
+
const sourceTxid = input.sourceTXID ?? input.sourceTransaction?.id("hex");
|
|
28853
|
+
if (sourceTxid == null || input.sourceOutputIndex == null) continue;
|
|
28854
|
+
spent.add(`${sourceTxid}.${input.sourceOutputIndex}`);
|
|
28855
|
+
if (input.sourceTransaction != null) pending.push(input.sourceTransaction);
|
|
28856
|
+
}
|
|
28857
|
+
}
|
|
28102
28858
|
}
|
|
28103
28859
|
emptyLookupDiagnostics(correlationId) {
|
|
28104
28860
|
return {
|
|
@@ -28139,6 +28895,21 @@ var OverlayUMPTokenInteractor = class {
|
|
|
28139
28895
|
outputCount: diagnostics.outputCount
|
|
28140
28896
|
};
|
|
28141
28897
|
}
|
|
28898
|
+
captureLookupCompleted(lookupKind, result, diagnostics, startedAt, extraAttributes = {}) {
|
|
28899
|
+
this.telemetry.capture({
|
|
28900
|
+
name: "wallet-toolbox.ump.lookup.completed",
|
|
28901
|
+
component: "wallet-toolbox.ump",
|
|
28902
|
+
severity: "info",
|
|
28903
|
+
correlationId: diagnostics.correlationId,
|
|
28904
|
+
attributes: {
|
|
28905
|
+
lookupKind,
|
|
28906
|
+
result,
|
|
28907
|
+
durationMs: Date.now() - startedAt,
|
|
28908
|
+
...this.lookupDiagnosticAttributes(diagnostics),
|
|
28909
|
+
...extraAttributes
|
|
28910
|
+
}
|
|
28911
|
+
});
|
|
28912
|
+
}
|
|
28142
28913
|
captureLookupFailure(lookupKind, reason, diagnostics, startedAt, error) {
|
|
28143
28914
|
this.telemetry.capture({
|
|
28144
28915
|
name: "wallet-toolbox.ump.lookup.indeterminate",
|
|
@@ -28379,8 +29150,7 @@ var OverlayUMPTokenInteractor = class {
|
|
|
28379
29150
|
throw new UMPTokenLookupError("lookup-unavailable", diagnostics, { cause: error });
|
|
28380
29151
|
}
|
|
28381
29152
|
if (resolution.answer.outputs.length === 0) {
|
|
28382
|
-
|
|
28383
|
-
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)) {
|
|
29153
|
+
if (resolution.progress.emptyHosts === 0) {
|
|
28384
29154
|
const diagnostics = this.toLookupDiagnostics(resolution);
|
|
28385
29155
|
this.captureLookupFailure("outpoint", "lookup-incomplete", diagnostics, startedAt);
|
|
28386
29156
|
throw new UMPTokenLookupError("lookup-incomplete", diagnostics);
|