@bsv/wallet-toolbox-client 2.4.21 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/out/index.client.cjs +1459 -319
- package/out/index.client.cjs.map +1 -1
- package/out/index.client.d.cts +86 -9
- package/out/index.client.d.cts.map +1 -1
- package/out/index.client.d.mts +86 -9
- package/out/index.client.d.mts.map +1 -1
- package/out/index.client.mjs +1460 -320
- package/out/index.client.mjs.map +1 -1
- package/package.json +2 -2
package/out/index.client.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
|
}
|
|
@@ -4888,6 +4900,12 @@ function upgradeOutputs(db) {
|
|
|
4888
4900
|
autoIncrement: true
|
|
4889
4901
|
});
|
|
4890
4902
|
store.createIndex("userId", "userId");
|
|
4903
|
+
store.createIndex("userId_basketId", ["userId", "basketId"]);
|
|
4904
|
+
store.createIndex("txid_vout_userId", [
|
|
4905
|
+
"txid",
|
|
4906
|
+
"vout",
|
|
4907
|
+
"userId"
|
|
4908
|
+
], { unique: true });
|
|
4891
4909
|
store.createIndex("transactionId", "transactionId");
|
|
4892
4910
|
store.createIndex("basketId", "basketId");
|
|
4893
4911
|
store.createIndex("spentBy", "spentBy");
|
|
@@ -5021,8 +5039,10 @@ async function mergeInputBeefs(rawTx, beef, trustSelf, knownTxids, trx, required
|
|
|
5021
5039
|
for (const input of tx.inputs) {
|
|
5022
5040
|
const sourceTXID = input.sourceTXID ?? "";
|
|
5023
5041
|
if (sourceTXID === "") throw new WERR_INTERNAL("req all transaction inputs must have valid sourceTXID");
|
|
5024
|
-
|
|
5025
|
-
|
|
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);
|
|
5026
5046
|
else await getValidBeef(sourceTXID, beef, trustSelf, knownTxids, trx, requiredLevels);
|
|
5027
5047
|
}
|
|
5028
5048
|
}
|
|
@@ -5086,26 +5106,22 @@ async function notifyTransactionsOfProof(ids, provenTxId, addNote, updateTransac
|
|
|
5086
5106
|
* @param options
|
|
5087
5107
|
*/
|
|
5088
5108
|
async function getBeefForTransaction(storage, txid, options) {
|
|
5089
|
-
|
|
5090
|
-
|
|
5091
|
-
else if (options.mergeToBeef != null) beef = Beef.fromBinary(options.mergeToBeef);
|
|
5092
|
-
else beef = new Beef();
|
|
5093
|
-
const knownTxids = new Set(options.knownTxids ?? []);
|
|
5109
|
+
const beef = mergeTarget(options);
|
|
5110
|
+
const hasKnownTxid = makeKnownTxidLookup$1(options.knownTxids ?? []);
|
|
5094
5111
|
const scheduled = /* @__PURE__ */ new Set([txid]);
|
|
5095
5112
|
let frontier = [{
|
|
5096
5113
|
txid,
|
|
5097
5114
|
depth: 0
|
|
5098
5115
|
}];
|
|
5099
|
-
const
|
|
5100
|
-
const concurrency = Number.isFinite(requestedConcurrency) ? Math.max(1, Math.min(32, Math.floor(requestedConcurrency))) : 8;
|
|
5116
|
+
const concurrency = normalizeConcurrency(options.maxConcurrency);
|
|
5101
5117
|
while (frontier.length > 0) {
|
|
5102
|
-
const current = frontier.filter((item) => beef
|
|
5103
|
-
const resolved = await mapWithConcurrency(current, concurrency, async (item) => await resolveBeefForTransaction(storage, item.txid, options,
|
|
5118
|
+
const current = frontier.filter((item) => needsResolution(beef, item.txid, hasKnownTxid));
|
|
5119
|
+
const resolved = await mapWithConcurrency(current, concurrency, async (item) => await resolveBeefForTransaction(storage, item.txid, options, hasKnownTxid, item.depth));
|
|
5104
5120
|
const next = [];
|
|
5105
5121
|
for (let i = 0; i < resolved.length; i++) {
|
|
5106
5122
|
const result = resolved[i];
|
|
5107
5123
|
beef.mergeBeef(result.beef);
|
|
5108
|
-
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)) {
|
|
5109
5125
|
scheduled.add(dependency);
|
|
5110
5126
|
next.push({
|
|
5111
5127
|
txid: dependency,
|
|
@@ -5117,6 +5133,188 @@ async function getBeefForTransaction(storage, txid, options) {
|
|
|
5117
5133
|
}
|
|
5118
5134
|
return beef;
|
|
5119
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
|
+
}
|
|
5302
|
+
function makeKnownTxidLookup$1(knownTxids) {
|
|
5303
|
+
let lookups = 0;
|
|
5304
|
+
let indexed;
|
|
5305
|
+
return (txid) => {
|
|
5306
|
+
lookups++;
|
|
5307
|
+
if (indexed != null) return indexed.has(txid);
|
|
5308
|
+
if (knownTxids.length > 64 && lookups > 4) {
|
|
5309
|
+
indexed = new Set(knownTxids);
|
|
5310
|
+
return indexed.has(txid);
|
|
5311
|
+
}
|
|
5312
|
+
return knownTxids.includes(txid);
|
|
5313
|
+
};
|
|
5314
|
+
}
|
|
5315
|
+
function normalizeConcurrency(value = 8) {
|
|
5316
|
+
return Number.isFinite(value) ? Math.max(1, Math.min(32, Math.floor(value))) : 8;
|
|
5317
|
+
}
|
|
5120
5318
|
async function mapWithConcurrency(values, concurrency, mapper) {
|
|
5121
5319
|
const results = Array.from({ length: values.length }, () => void 0);
|
|
5122
5320
|
let cursor = 0;
|
|
@@ -5160,11 +5358,11 @@ async function mergeUsableProvenTransaction(beef, txid, result, options, recursi
|
|
|
5160
5358
|
beef.mergeBump(merklePath);
|
|
5161
5359
|
return beef;
|
|
5162
5360
|
}
|
|
5163
|
-
async function resolveBeefForTransaction(storage, txid, options,
|
|
5361
|
+
async function resolveBeefForTransaction(storage, txid, options, hasKnownTxid, recursionDepth) {
|
|
5164
5362
|
const maxDepth = storage.maxRecursionDepth;
|
|
5165
5363
|
if (maxDepth && maxDepth <= recursionDepth) throw new WERR_INVALID_OPERATION(`Maximum BEEF depth exceeded. Limit is ${storage.maxRecursionDepth}`);
|
|
5166
5364
|
const beef = new Beef();
|
|
5167
|
-
if (
|
|
5365
|
+
if (hasKnownTxid(txid)) {
|
|
5168
5366
|
beef.mergeTxidOnly(txid);
|
|
5169
5367
|
return {
|
|
5170
5368
|
beef,
|
|
@@ -5245,6 +5443,23 @@ async function createMergedBeefOfTxids(txids, storage) {
|
|
|
5245
5443
|
//#endregion
|
|
5246
5444
|
//#region ../src/storage/methods/processAction.ts
|
|
5247
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) {
|
|
5248
5463
|
const logger = args.logger;
|
|
5249
5464
|
logger?.group("storage processAction");
|
|
5250
5465
|
const userId = verifyId(auth.userId);
|
|
@@ -5252,9 +5467,9 @@ async function processAction$1(storage, auth, args) {
|
|
|
5252
5467
|
let req;
|
|
5253
5468
|
const txidsOfReqsToShareWithWorld = [...args.sendWith];
|
|
5254
5469
|
if (args.isNewTx) {
|
|
5255
|
-
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));
|
|
5256
5471
|
logger?.log("validated new tx updates to storage");
|
|
5257
|
-
({req} = await commitNewTxToStorage(storage, userId, vargs));
|
|
5472
|
+
({req} = await traceProcessStep(storage, "wallet.storage.process_action.commit", parent, async () => await commitNewTxToStorage(storage, userId, vargs)));
|
|
5258
5473
|
logger?.log("committed new tx updates to storage ");
|
|
5259
5474
|
if (!req) throw new WERR_INTERNAL();
|
|
5260
5475
|
if (args.isNoSend && !args.isSendWith) logger?.log(`noSend txid ${req.txid}`);
|
|
@@ -5263,12 +5478,19 @@ async function processAction$1(storage, auth, args) {
|
|
|
5263
5478
|
logger?.log(`sending txid ${req.txid}`);
|
|
5264
5479
|
}
|
|
5265
5480
|
}
|
|
5266
|
-
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));
|
|
5267
5482
|
r.sendWithResults = swr;
|
|
5268
5483
|
r.notDelayedResults = ndr;
|
|
5269
5484
|
logger?.groupEnd();
|
|
5270
5485
|
return r;
|
|
5271
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
|
+
}
|
|
5272
5494
|
/**
|
|
5273
5495
|
* Verifies that all the txids are known reqs with ready-to-share status.
|
|
5274
5496
|
* Assigns a batch identifier and updates all the provenTxReqs.
|
|
@@ -5439,21 +5661,16 @@ async function validateCommitNewTxToStorageArgs(storage, userId, params) {
|
|
|
5439
5661
|
} }));
|
|
5440
5662
|
if (!transaction.isOutgoing) throw new WERR_INVALID_OPERATION("isOutgoing is not true");
|
|
5441
5663
|
if (transaction.inputBEEF == null) throw new WERR_INVALID_OPERATION();
|
|
5442
|
-
const beef = Beef.fromBinary(asArray(transaction.inputBEEF));
|
|
5443
5664
|
if (transaction.status !== "unsigned" && transaction.status !== "unprocessed") throw new WERR_INVALID_OPERATION(`invalid transaction status ${transaction.status}`);
|
|
5444
5665
|
const transactionId = verifyId(transaction.transactionId);
|
|
5445
|
-
const outputOutputs = await storage.findOutputs({ partial: {
|
|
5666
|
+
const [outputOutputs, commissionRows] = await Promise.all([storage.findOutputs({ partial: {
|
|
5446
5667
|
userId,
|
|
5447
5668
|
transactionId
|
|
5448
|
-
} })
|
|
5449
|
-
const inputOutputs = await storage.findOutputs({ partial: {
|
|
5450
|
-
userId,
|
|
5451
|
-
spentBy: transactionId
|
|
5452
|
-
} });
|
|
5453
|
-
const commission = verifyOneOrNone(await storage.findCommissions({ partial: {
|
|
5669
|
+
} }), storage.commissionSatoshis > 0 ? storage.findCommissions({ partial: {
|
|
5454
5670
|
transactionId,
|
|
5455
5671
|
userId
|
|
5456
|
-
} }));
|
|
5672
|
+
} }) : Promise.resolve([])]);
|
|
5673
|
+
const commission = verifyOneOrNone(commissionRows);
|
|
5457
5674
|
if (storage.commissionSatoshis > 0) {
|
|
5458
5675
|
if (commission == null) throw new WERR_INTERNAL();
|
|
5459
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.");
|
|
@@ -5473,10 +5690,7 @@ async function validateCommitNewTxToStorageArgs(storage, userId, params) {
|
|
|
5473
5690
|
txScriptOffsets,
|
|
5474
5691
|
transactionId,
|
|
5475
5692
|
transaction,
|
|
5476
|
-
inputOutputs,
|
|
5477
5693
|
outputOutputs,
|
|
5478
|
-
commission,
|
|
5479
|
-
beef,
|
|
5480
5694
|
req,
|
|
5481
5695
|
outputUpdates: [],
|
|
5482
5696
|
transactionUpdate: {
|
|
@@ -6154,17 +6368,24 @@ async function generateChangeSdkCore(params, allocateChangeInput, releaseChangeI
|
|
|
6154
6368
|
};
|
|
6155
6369
|
const fixedInputs = params.fixedInputs;
|
|
6156
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;
|
|
6157
6378
|
/**
|
|
6158
6379
|
* @returns sum of transaction fixedInputs satoshis and fundingInputs satoshis
|
|
6159
6380
|
*/
|
|
6160
6381
|
const funding = () => {
|
|
6161
|
-
return
|
|
6382
|
+
return fixedFunding + allocatedFunding;
|
|
6162
6383
|
};
|
|
6163
6384
|
/**
|
|
6164
6385
|
* @returns sum of transaction fixedOutputs satoshis
|
|
6165
6386
|
*/
|
|
6166
6387
|
const spending = () => {
|
|
6167
|
-
return
|
|
6388
|
+
return fixedSpending;
|
|
6168
6389
|
};
|
|
6169
6390
|
/**
|
|
6170
6391
|
* @returns sum of transaction changeOutputs satoshis
|
|
@@ -6174,7 +6395,9 @@ async function generateChangeSdkCore(params, allocateChangeInput, releaseChangeI
|
|
|
6174
6395
|
};
|
|
6175
6396
|
const fee = () => funding() - spending() - change();
|
|
6176
6397
|
const size = (addedChangeInputs, addedChangeOutputs) => {
|
|
6177
|
-
|
|
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;
|
|
6178
6401
|
};
|
|
6179
6402
|
/**
|
|
6180
6403
|
* @returns the target fee required for the transaction as currently configured under feeModel.
|
|
@@ -6213,7 +6436,10 @@ async function generateChangeSdkCore(params, allocateChangeInput, releaseChangeI
|
|
|
6213
6436
|
const releaseAllocatedChangeInputs = async () => {
|
|
6214
6437
|
while (r.allocatedChangeInputs.length > 0) {
|
|
6215
6438
|
const i = r.allocatedChangeInputs.pop();
|
|
6216
|
-
if (i != null)
|
|
6439
|
+
if (i != null) {
|
|
6440
|
+
allocatedFunding -= i.satoshis;
|
|
6441
|
+
await releaseChangeInput(i.outputId);
|
|
6442
|
+
}
|
|
6217
6443
|
}
|
|
6218
6444
|
feeExcessNow = feeExcess();
|
|
6219
6445
|
};
|
|
@@ -6248,6 +6474,7 @@ async function generateChangeSdkCore(params, allocateChangeInput, releaseChangeI
|
|
|
6248
6474
|
const allocatedChangeInput = await allocateChangeInput(-feeExcess(1, ao) + (ao === 1 ? 2 * params.changeInitialSatoshis : 0) + changeBuffer, exactSatoshis);
|
|
6249
6475
|
if (allocatedChangeInput == null) return false;
|
|
6250
6476
|
r.allocatedChangeInputs.push(allocatedChangeInput);
|
|
6477
|
+
allocatedFunding += allocatedChangeInput.satoshis;
|
|
6251
6478
|
maybeAddChangeOutput(ao);
|
|
6252
6479
|
return true;
|
|
6253
6480
|
};
|
|
@@ -6259,6 +6486,7 @@ async function generateChangeSdkCore(params, allocateChangeInput, releaseChangeI
|
|
|
6259
6486
|
while (r.changeOutputs.length > 0 && feeExcess() < 0) r.changeOutputs.pop();
|
|
6260
6487
|
if (feeExcess() < 0) break;
|
|
6261
6488
|
removeChurnPairs(r.allocatedChangeInputs, r.changeOutputs);
|
|
6489
|
+
allocatedFunding = r.allocatedChangeInputs.reduce((sum, input) => sum + input.satoshis, 0);
|
|
6262
6490
|
}
|
|
6263
6491
|
};
|
|
6264
6492
|
/**
|
|
@@ -6267,7 +6495,9 @@ async function generateChangeSdkCore(params, allocateChangeInput, releaseChangeI
|
|
|
6267
6495
|
await fundTransaction();
|
|
6268
6496
|
if (feeExcess() < 0 && vgcpr.hasMaxPossibleOutput !== void 0) {
|
|
6269
6497
|
if (fixedOutputs[vgcpr.hasMaxPossibleOutput].satoshis !== 0x775f05a073fff) throw new WERR_INTERNAL();
|
|
6270
|
-
|
|
6498
|
+
const adjustment = feeExcess();
|
|
6499
|
+
fixedOutputs[vgcpr.hasMaxPossibleOutput].satoshis += adjustment;
|
|
6500
|
+
fixedSpending += adjustment;
|
|
6271
6501
|
r.maxPossibleSatoshisAdjustment = {
|
|
6272
6502
|
fixedOutputIndex: vgcpr.hasMaxPossibleOutput,
|
|
6273
6503
|
satoshis: fixedOutputs[vgcpr.hasMaxPossibleOutput].satoshis
|
|
@@ -6286,8 +6516,11 @@ async function generateChangeSdkCore(params, allocateChangeInput, releaseChangeI
|
|
|
6286
6516
|
* If needed, seek funding to avoid overspending on fees without a change output to recapture it.
|
|
6287
6517
|
*/
|
|
6288
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());
|
|
6289
6522
|
await releaseAllocatedChangeInputs();
|
|
6290
|
-
throw new WERR_INSUFFICIENT_FUNDS(
|
|
6523
|
+
throw new WERR_INSUFFICIENT_FUNDS(totalSatoshisNeeded, moreSatoshisNeeded);
|
|
6291
6524
|
}
|
|
6292
6525
|
/**
|
|
6293
6526
|
* Distribute the excess fees across the changeOutputs added.
|
|
@@ -6611,6 +6844,8 @@ function makeChangeLock(out, dctr, args, changeKeys, wallet) {
|
|
|
6611
6844
|
}
|
|
6612
6845
|
//#endregion
|
|
6613
6846
|
//#region ../src/signer/methods/verifyUnlockScripts.ts
|
|
6847
|
+
const postChronicleHeightFallback = 943816;
|
|
6848
|
+
const canonicalP2PKHScope = TransactionSignature.SIGHASH_ALL + TransactionSignature.SIGHASH_FORKID;
|
|
6614
6849
|
const javaScriptOnlyVerifier = {
|
|
6615
6850
|
shouldVerifySpend: () => false,
|
|
6616
6851
|
verifySpend: async () => {
|
|
@@ -6622,10 +6857,11 @@ function invalidUnlockingScript(inputIndex, detail) {
|
|
|
6622
6857
|
return new WERR_INVALID_PARAMETER(`inputs[${inputIndex}].unlockScript`, `valid.${suffix}`);
|
|
6623
6858
|
}
|
|
6624
6859
|
async function verifyOneSpend(pending, verifier) {
|
|
6860
|
+
const [inputIndex, , spend, context] = pending;
|
|
6625
6861
|
try {
|
|
6626
|
-
if (!(verifier === void 0 ?
|
|
6862
|
+
if (!(verifier === void 0 ? spend.validate(context) : await spend.validateWith(verifier, context))) throw invalidUnlockingScript(inputIndex);
|
|
6627
6863
|
} catch (error) {
|
|
6628
|
-
if (error instanceof ScriptEvaluationError) throw invalidUnlockingScript(
|
|
6864
|
+
if (error instanceof ScriptEvaluationError) throw invalidUnlockingScript(inputIndex, error.message);
|
|
6629
6865
|
throw error;
|
|
6630
6866
|
}
|
|
6631
6867
|
}
|
|
@@ -6635,33 +6871,157 @@ async function verifyPendingSpends(pending, verifier) {
|
|
|
6635
6871
|
return;
|
|
6636
6872
|
}
|
|
6637
6873
|
const batched = [];
|
|
6638
|
-
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);
|
|
6639
6875
|
else await verifyOneSpend(item, javaScriptOnlyVerifier);
|
|
6640
6876
|
if (batched.length === 0) return;
|
|
6641
6877
|
let verdicts;
|
|
6642
6878
|
try {
|
|
6643
6879
|
verdicts = await verifier.verifySpendsBatch(batched.map((item) => ({
|
|
6644
|
-
spend: item
|
|
6645
|
-
...item
|
|
6880
|
+
spend: item[2],
|
|
6881
|
+
...item[3]
|
|
6646
6882
|
})));
|
|
6647
6883
|
} catch (error) {
|
|
6648
|
-
if (error instanceof ScriptEvaluationError) throw invalidUnlockingScript(batched[0]
|
|
6884
|
+
if (error instanceof ScriptEvaluationError) throw invalidUnlockingScript(batched[0][0], error.message);
|
|
6649
6885
|
throw error;
|
|
6650
6886
|
}
|
|
6651
6887
|
if (verdicts.length !== batched.length) throw new Error("Script verifier returned an invalid batch result count");
|
|
6652
6888
|
verdicts.forEach((valid, index) => {
|
|
6653
|
-
if (!valid) throw invalidUnlockingScript(batched[index]
|
|
6889
|
+
if (!valid) throw invalidUnlockingScript(batched[index][0]);
|
|
6654
6890
|
});
|
|
6655
6891
|
}
|
|
6656
|
-
function
|
|
6657
|
-
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);
|
|
6658
7018
|
if (tx == null) throw new WERR_INVALID_PARAMETER("txid", `contained in beef, txid ${txid}`);
|
|
6659
7019
|
const sigHashCache = { hashOutputsSingle: /* @__PURE__ */ new Map() };
|
|
6660
7020
|
for (let inputIndex = 0; inputIndex < tx.inputs.length; inputIndex++) {
|
|
6661
7021
|
const input = tx.inputs[inputIndex];
|
|
6662
7022
|
if (input.sourceTXID == null) throw new WERR_INVALID_PARAMETER(`inputs[${inputIndex}].sourceTXID`, "valid");
|
|
6663
7023
|
if (input.unlockingScript == null) throw new WERR_INVALID_PARAMETER(`inputs[${inputIndex}].unlockingScript`, "valid");
|
|
6664
|
-
input.sourceTransaction =
|
|
7024
|
+
input.sourceTransaction = transactions.get(input.sourceTXID);
|
|
6665
7025
|
if (input.sourceTransaction == null) {
|
|
6666
7026
|
result.skippedInputs++;
|
|
6667
7027
|
continue;
|
|
@@ -6673,11 +7033,10 @@ function collectTransactionSpends(txid, resultIndex, beef, result, pending) {
|
|
|
6673
7033
|
consensus: true,
|
|
6674
7034
|
utxoHeight
|
|
6675
7035
|
};
|
|
6676
|
-
pending.push(
|
|
7036
|
+
pending.push([
|
|
6677
7037
|
inputIndex,
|
|
6678
7038
|
resultIndex,
|
|
6679
|
-
|
|
6680
|
-
spend: new Spend({
|
|
7039
|
+
new Spend({
|
|
6681
7040
|
sourceTXID: input.sourceTXID,
|
|
6682
7041
|
sourceOutputIndex: input.sourceOutputIndex,
|
|
6683
7042
|
lockingScript: sourceOutput.lockingScript,
|
|
@@ -6691,9 +7050,52 @@ function collectTransactionSpends(txid, resultIndex, beef, result, pending) {
|
|
|
6691
7050
|
outputs: tx.outputs,
|
|
6692
7051
|
lockTime: tx.lockTime,
|
|
6693
7052
|
sigHashCache
|
|
6694
|
-
})
|
|
6695
|
-
|
|
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;
|
|
6696
7097
|
}
|
|
7098
|
+
return pending;
|
|
6697
7099
|
}
|
|
6698
7100
|
/**
|
|
6699
7101
|
* Verifies every resolvable input from several transactions in one optional
|
|
@@ -6704,10 +7106,17 @@ async function verifyUnlockScriptsBatch(txids, beef, verifier) {
|
|
|
6704
7106
|
verifiedInputs: 0,
|
|
6705
7107
|
skippedInputs: 0
|
|
6706
7108
|
}));
|
|
6707
|
-
const
|
|
6708
|
-
|
|
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);
|
|
6709
7118
|
await verifyPendingSpends(pending, verifier);
|
|
6710
|
-
for (const item of pending) results[item
|
|
7119
|
+
for (const item of pending) results[item[1]].verifiedInputs++;
|
|
6711
7120
|
return results;
|
|
6712
7121
|
}
|
|
6713
7122
|
/**
|
|
@@ -6730,21 +7139,56 @@ async function completeSignedTransaction(prior, spends, wallet) {
|
|
|
6730
7139
|
input.unlockingScript = asBsvSdkScript(spend.unlockingScript);
|
|
6731
7140
|
if (spend.sequenceNumber !== void 0) input.sequence = spend.sequenceNumber;
|
|
6732
7141
|
}
|
|
6733
|
-
|
|
6734
|
-
const
|
|
6735
|
-
|
|
6736
|
-
|
|
6737
|
-
|
|
6738
|
-
|
|
6739
|
-
|
|
6740
|
-
|
|
6741
|
-
|
|
6742
|
-
|
|
6743
|
-
const
|
|
6744
|
-
|
|
6745
|
-
|
|
6746
|
-
|
|
6747
|
-
|
|
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());
|
|
6748
7192
|
if (wallet.telemetry.enabled) await wallet.telemetry.withSpan("wallet.crypto.transaction_sign", {
|
|
6749
7193
|
component: "wallet-toolbox",
|
|
6750
7194
|
carrier: prior.args,
|
|
@@ -6765,7 +7209,7 @@ function getResultBeef(result) {
|
|
|
6765
7209
|
//#endregion
|
|
6766
7210
|
//#region ../src/signer/methods/createAction.ts
|
|
6767
7211
|
async function createAction$1(wallet, auth, vargs) {
|
|
6768
|
-
if (!wallet.telemetry.enabled) return await createActionCore(wallet, auth, vargs);
|
|
7212
|
+
if (!wallet.telemetry.enabled) return await createActionCore$1(wallet, auth, vargs);
|
|
6769
7213
|
return await wallet.telemetry.withSpan("wallet.create_action", {
|
|
6770
7214
|
component: "wallet-toolbox",
|
|
6771
7215
|
carrier: vargs,
|
|
@@ -6776,7 +7220,7 @@ async function createAction$1(wallet, auth, vargs) {
|
|
|
6776
7220
|
"action.is_sign_action": vargs.isSignAction
|
|
6777
7221
|
}
|
|
6778
7222
|
}, async (span) => {
|
|
6779
|
-
const result = await createActionCore(wallet, auth, vargs, span);
|
|
7223
|
+
const result = await createActionCore$1(wallet, auth, vargs, span);
|
|
6780
7224
|
span.end({ attributes: {
|
|
6781
7225
|
"action.has_transaction": result.tx != null,
|
|
6782
7226
|
"action.has_signable_transaction": result.signableTransaction != null,
|
|
@@ -6785,7 +7229,7 @@ async function createAction$1(wallet, auth, vargs) {
|
|
|
6785
7229
|
return result;
|
|
6786
7230
|
});
|
|
6787
7231
|
}
|
|
6788
|
-
async function createActionCore(wallet, auth, vargs, parent) {
|
|
7232
|
+
async function createActionCore$1(wallet, auth, vargs, parent) {
|
|
6789
7233
|
const r = {};
|
|
6790
7234
|
const logger = vargs.logger;
|
|
6791
7235
|
let prior;
|
|
@@ -6800,19 +7244,22 @@ async function createActionCore(wallet, auth, vargs, parent) {
|
|
|
6800
7244
|
prior.tx = await traceActionStep(wallet, "wallet.create_action.complete_signing", parent, async () => await completeSignedTransaction(prior, {}, wallet));
|
|
6801
7245
|
logger?.log("completed signed transaction");
|
|
6802
7246
|
r.txid = prior.tx.id("hex");
|
|
6803
|
-
const beef =
|
|
6804
|
-
|
|
6805
|
-
|
|
6806
|
-
|
|
6807
|
-
|
|
6808
|
-
|
|
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
|
+
});
|
|
6809
7256
|
logger?.log("merged beef");
|
|
6810
7257
|
await traceActionStep(wallet, "wallet.create_action.verify_unlock_scripts", parent, async () => await verifyUnlockScripts(r.txid, beef, wallet.scriptVerifier));
|
|
6811
7258
|
logger?.log("verified unlock scripts");
|
|
6812
7259
|
r.noSendChange = prior.dcr.noSendChangeOutputVouts?.map((vout) => `${r.txid}.${vout}`);
|
|
6813
7260
|
beef.atomicTxid = r.txid;
|
|
6814
7261
|
setResultBeef(r, beef);
|
|
6815
|
-
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));
|
|
6816
7263
|
}
|
|
6817
7264
|
const { sendWithResults, notDelayedResults } = await traceActionStep(wallet, "wallet.create_action.process", parent, async () => await processAction(prior, wallet, auth, vargs));
|
|
6818
7265
|
logger?.log("processed transaction");
|
|
@@ -7430,6 +7877,50 @@ function selectCanonicalChange(outputs, targetSatoshis, exactSatoshis) {
|
|
|
7430
7877
|
if (over != null) return over;
|
|
7431
7878
|
return outputs.filter((output) => output.satoshis < targetSatoshis).sort((a, b) => b.satoshis - a.satoshis || b.outputId - a.outputId)[0];
|
|
7432
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
|
+
};
|
|
7433
7924
|
function repeatableRandom(randomVals) {
|
|
7434
7925
|
const values = [...randomVals ?? []];
|
|
7435
7926
|
return () => {
|
|
@@ -8565,6 +9056,22 @@ var ActionBatchController = class {
|
|
|
8565
9056
|
};
|
|
8566
9057
|
//#endregion
|
|
8567
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
|
+
}
|
|
8568
9075
|
/**
|
|
8569
9076
|
* Build a {@link DiscoverCertificatesResult} from contact records so {@link Wallet.discoverByIdentityKey}
|
|
8570
9077
|
* and {@link Wallet.discoverByAttributes} can short-circuit on a local contacts hit. The synthetic
|
|
@@ -9038,6 +9545,7 @@ var Wallet = class {
|
|
|
9038
9545
|
if (this.returnTxidOnly) return beef;
|
|
9039
9546
|
const b = parsedBeef ?? Beef.fromBinary(beef);
|
|
9040
9547
|
if (!b.atomicTxid) throw new WERR_INTERNAL();
|
|
9548
|
+
if (!b.txs.some((btx) => btx.isTxidOnly && !knownTxids?.includes(btx.txid))) return beef;
|
|
9041
9549
|
return this.verifyReturnedTxidOnly(b, knownTxids).toBinaryAtomic(b.atomicTxid);
|
|
9042
9550
|
}
|
|
9043
9551
|
verifyReturnedTxidOnlyBEEF(beef) {
|
|
@@ -9069,7 +9577,7 @@ var Wallet = class {
|
|
|
9069
9577
|
Validation.validateOriginator(originator);
|
|
9070
9578
|
args.options ??= {};
|
|
9071
9579
|
args.options.trustSelf ||= this.trustSelf;
|
|
9072
|
-
|
|
9580
|
+
prepareKnownTxidsForCreateAction(this, args);
|
|
9073
9581
|
const { auth, vargs } = this.validateAuthAndArgs(args, Validation.validateCreateActionArgs, logger);
|
|
9074
9582
|
logger?.log("validated args");
|
|
9075
9583
|
vargs.includeAllSourceTransactions = this.includeAllSourceTransactions;
|
|
@@ -9077,9 +9585,25 @@ var Wallet = class {
|
|
|
9077
9585
|
const r = await createAction$1(this, auth, vargs);
|
|
9078
9586
|
logger?.log("action created");
|
|
9079
9587
|
const resultBeef = getResultBeef(r);
|
|
9080
|
-
if (r.tx != null) this.beef.mergeBeefFromParty(this.storageParty, resultBeef ?? r.tx);
|
|
9081
9588
|
if (r.tx != null) {
|
|
9082
|
-
|
|
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();
|
|
9083
9607
|
logger?.log("verify returned AtomicBEEF");
|
|
9084
9608
|
}
|
|
9085
9609
|
if (!vargs.isDelayed) throwIfAnyUnsuccessfulCreateActions(r);
|
|
@@ -9388,6 +9912,28 @@ function isAutoSpendableChangeOutput(output) {
|
|
|
9388
9912
|
return isManagedChangeOutput(output) && output.spendable && output.spentBy == null;
|
|
9389
9913
|
}
|
|
9390
9914
|
async function createAction(storage, auth, vargs, _originator) {
|
|
9915
|
+
if (!storage.telemetry.enabled) return await createActionCore(storage, auth, vargs);
|
|
9916
|
+
return await storage.telemetry.withSpan("wallet.storage.create_action", {
|
|
9917
|
+
component: "wallet-storage",
|
|
9918
|
+
carrier: vargs,
|
|
9919
|
+
attributes: {
|
|
9920
|
+
"action.fixed_input_count": vargs.inputs.length,
|
|
9921
|
+
"action.fixed_output_count": vargs.outputs.length,
|
|
9922
|
+
"action.known_txid_count": vargs.options.knownTxids?.length ?? 0,
|
|
9923
|
+
"action.is_delayed": vargs.isDelayed,
|
|
9924
|
+
"action.is_no_send": vargs.isNoSend
|
|
9925
|
+
}
|
|
9926
|
+
}, async (span) => {
|
|
9927
|
+
const result = await createActionCore(storage, auth, vargs, span);
|
|
9928
|
+
span.end({ attributes: {
|
|
9929
|
+
"action.result_input_count": result.inputs.length,
|
|
9930
|
+
"action.result_output_count": result.outputs.length,
|
|
9931
|
+
"action.input_beef_bytes": result.inputBeef?.length ?? 0
|
|
9932
|
+
} });
|
|
9933
|
+
return result;
|
|
9934
|
+
});
|
|
9935
|
+
}
|
|
9936
|
+
async function createActionCore(storage, auth, vargs, parent) {
|
|
9391
9937
|
const logger = vargs.logger;
|
|
9392
9938
|
logger?.group("storage createAction");
|
|
9393
9939
|
if (vargs.isTestWerrReviewActions) throwDummyReviewActions();
|
|
@@ -9407,57 +9953,119 @@ async function createAction(storage, auth, vargs, _originator) {
|
|
|
9407
9953
|
* - Create and return result.
|
|
9408
9954
|
*/
|
|
9409
9955
|
const userId = auth.userId;
|
|
9410
|
-
const { storageBeef, beef, xinputs } = await
|
|
9411
|
-
|
|
9412
|
-
|
|
9413
|
-
|
|
9414
|
-
|
|
9415
|
-
|
|
9416
|
-
userId,
|
|
9417
|
-
|
|
9418
|
-
|
|
9419
|
-
|
|
9420
|
-
|
|
9421
|
-
|
|
9422
|
-
|
|
9423
|
-
|
|
9956
|
+
const { storageBeef, beef, xinputs, xoutputs, changeBasket, noSendChangeIn } = await traceStorageStep(storage, "wallet.storage.create_action.validate", parent, {
|
|
9957
|
+
"action.fixed_input_count": vargs.inputs.length,
|
|
9958
|
+
"action.fixed_output_count": vargs.outputs.length
|
|
9959
|
+
}, async (span) => {
|
|
9960
|
+
const requiredInputs = await validateRequiredInputs(storage, userId, vargs);
|
|
9961
|
+
logger?.log("validated required inputs");
|
|
9962
|
+
const xoutputs = validateRequiredOutputs(storage, userId, vargs);
|
|
9963
|
+
logger?.log("validated required outputs");
|
|
9964
|
+
const changeBasketName = "default";
|
|
9965
|
+
const changeBasket = verifyOne(await storage.findOutputBaskets({ partial: {
|
|
9966
|
+
userId,
|
|
9967
|
+
name: changeBasketName
|
|
9968
|
+
} }), `Invalid outputGeneration basket "${changeBasketName}"`);
|
|
9969
|
+
logger?.log("found change basket");
|
|
9970
|
+
const noSendChangeIn = await validateNoSendChange(storage, userId, vargs, changeBasket);
|
|
9971
|
+
logger?.log("validated noSendChange");
|
|
9972
|
+
span?.end({ attributes: {
|
|
9973
|
+
"action.validated_input_count": requiredInputs.xinputs.length,
|
|
9974
|
+
"action.validated_output_count": xoutputs.length,
|
|
9975
|
+
"action.no_send_change_input_count": noSendChangeIn.length,
|
|
9976
|
+
"action.validated_beef_tx_count": requiredInputs.beef.txs.length
|
|
9977
|
+
} });
|
|
9978
|
+
return {
|
|
9979
|
+
...requiredInputs,
|
|
9980
|
+
xoutputs,
|
|
9981
|
+
changeBasket,
|
|
9982
|
+
noSendChangeIn
|
|
9983
|
+
};
|
|
9984
|
+
});
|
|
9424
9985
|
const feeModel = validateStorageFeeModel(storage.feeModel);
|
|
9425
9986
|
logger?.log(`validated fee model ${JSON.stringify(feeModel)}`);
|
|
9426
|
-
|
|
9427
|
-
|
|
9987
|
+
const initialFundingPlan = await prepareFundingPlan(storage, [
|
|
9988
|
+
userId,
|
|
9989
|
+
vargs,
|
|
9990
|
+
xinputs,
|
|
9991
|
+
xoutputs,
|
|
9992
|
+
changeBasket,
|
|
9993
|
+
noSendChangeIn,
|
|
9994
|
+
feeModel,
|
|
9995
|
+
parent
|
|
9996
|
+
]);
|
|
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();
|
|
9428
10000
|
let newTx;
|
|
10001
|
+
let newTxCommitted = false;
|
|
9429
10002
|
try {
|
|
9430
|
-
|
|
9431
|
-
|
|
9432
|
-
|
|
9433
|
-
|
|
9434
|
-
|
|
9435
|
-
|
|
9436
|
-
|
|
9437
|
-
|
|
9438
|
-
|
|
9439
|
-
|
|
9440
|
-
|
|
9441
|
-
|
|
9442
|
-
|
|
9443
|
-
|
|
9444
|
-
|
|
9445
|
-
|
|
9446
|
-
|
|
9447
|
-
|
|
9448
|
-
|
|
9449
|
-
|
|
9450
|
-
|
|
9451
|
-
|
|
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
|
+
};
|
|
10048
|
+
});
|
|
10049
|
+
newTxCommitted = true;
|
|
10050
|
+
const committedTx = verifyTruthy(newTx);
|
|
10051
|
+
const { allocatedChange, derivationPrefix, outputs, changeVouts, ctx } = persisted;
|
|
9452
10052
|
logger?.log("created new output records");
|
|
9453
|
-
const inputBeef = await mergeAllocatedChangeBeefs(storage,
|
|
10053
|
+
const inputBeef = await mergeAllocatedChangeBeefs(storage, vargs, allocatedChange, beef, allocatedBeefPrefetch, parent);
|
|
9454
10054
|
logger?.log("merged allocated change beefs");
|
|
9455
|
-
const inputs = await
|
|
10055
|
+
const inputs = await traceStorageStep(storage, "wallet.storage.create_action.assemble_inputs", parent, {
|
|
10056
|
+
"action.fixed_input_count": ctx.xinputs.length,
|
|
10057
|
+
"action.funding_input_count": allocatedChange.length,
|
|
10058
|
+
"action.include_source_transactions": vargs.includeAllSourceTransactions
|
|
10059
|
+
}, async (span) => {
|
|
10060
|
+
const assembled = await createNewInputs(storage, userId, vargs, ctx, allocatedChange);
|
|
10061
|
+
span?.end({ attributes: { "action.result_input_count": assembled.length } });
|
|
10062
|
+
return assembled;
|
|
10063
|
+
});
|
|
9456
10064
|
logger?.log("created new inputs");
|
|
9457
10065
|
const r = {
|
|
9458
|
-
reference:
|
|
9459
|
-
version:
|
|
9460
|
-
lockTime:
|
|
10066
|
+
reference: committedTx.reference,
|
|
10067
|
+
version: committedTx.version,
|
|
10068
|
+
lockTime: committedTx.lockTime,
|
|
9461
10069
|
inputs,
|
|
9462
10070
|
outputs,
|
|
9463
10071
|
derivationPrefix,
|
|
@@ -9467,9 +10075,15 @@ async function createAction(storage, auth, vargs, _originator) {
|
|
|
9467
10075
|
logger?.groupEnd();
|
|
9468
10076
|
return r;
|
|
9469
10077
|
} catch (error) {
|
|
10078
|
+
await allocatedBeefPrefetch;
|
|
9470
10079
|
if (newTx?.transactionId != null) try {
|
|
9471
|
-
|
|
9472
|
-
|
|
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
|
+
}
|
|
9473
10087
|
} catch (cleanupError) {
|
|
9474
10088
|
logger?.log(`failed to clean up createAction transaction ${newTx.transactionId}: ${String(cleanupError)}`);
|
|
9475
10089
|
}
|
|
@@ -9619,23 +10233,10 @@ async function getCompetingBeefForReview(storage, txid) {
|
|
|
9619
10233
|
throw e;
|
|
9620
10234
|
}
|
|
9621
10235
|
}
|
|
9622
|
-
/**
|
|
9623
|
-
|
|
9624
|
-
async function persistNewOutput(storage, o, tags, txTags, txBaskets) {
|
|
9625
|
-
o.outputId = await storage.insertOutput(o);
|
|
9626
|
-
const changeVout = o.change && o.purpose === "change" && o.providedBy === "storage" ? o.vout : void 0;
|
|
9627
|
-
for (const tagName of new Set(tags)) {
|
|
9628
|
-
const tag = txTags[tagName];
|
|
9629
|
-
await storage.insertOutputTagMap({
|
|
9630
|
-
outputId: verifyId(o.outputId),
|
|
9631
|
-
outputTagId: verifyId(tag.outputTagId),
|
|
9632
|
-
created_at: /* @__PURE__ */ new Date(),
|
|
9633
|
-
updated_at: /* @__PURE__ */ new Date(),
|
|
9634
|
-
isDeleted: false
|
|
9635
|
-
});
|
|
9636
|
-
}
|
|
10236
|
+
/** Build the SDK descriptor for a persisted output. */
|
|
10237
|
+
function describeNewOutput(o, tags, txBaskets) {
|
|
9637
10238
|
return {
|
|
9638
|
-
changeVout,
|
|
10239
|
+
changeVout: o.change && o.purpose === "change" && o.providedBy === "storage" ? o.vout : void 0,
|
|
9639
10240
|
ro: {
|
|
9640
10241
|
vout: verifyInteger(o.vout),
|
|
9641
10242
|
satoshis: Validation.validateSatoshis(o.satoshis, "o.satoshis"),
|
|
@@ -9650,13 +10251,28 @@ async function persistNewOutput(storage, o, tags, txTags, txBaskets) {
|
|
|
9650
10251
|
}
|
|
9651
10252
|
};
|
|
9652
10253
|
}
|
|
9653
|
-
|
|
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) {
|
|
9654
10270
|
const txBaskets = {};
|
|
9655
10271
|
const basketNames = [...new Set(ctx.xoutputs.map((x) => x.basket).filter((v) => !!v))];
|
|
9656
|
-
Object.assign(txBaskets, await storage.findOrInsertOutputBasketsBulk(userId, basketNames));
|
|
10272
|
+
Object.assign(txBaskets, await storage.findOrInsertOutputBasketsBulk(userId, basketNames, trx));
|
|
9657
10273
|
const txTags = {};
|
|
9658
10274
|
const tagNames = [...new Set(ctx.xoutputs.flatMap((x) => x.tags))];
|
|
9659
|
-
Object.assign(txTags, await storage.findOrInsertOutputTagsBulk(userId, tagNames));
|
|
10275
|
+
Object.assign(txTags, await storage.findOrInsertOutputTagsBulk(userId, tagNames, trx));
|
|
9660
10276
|
const newOutputs = [];
|
|
9661
10277
|
for (const xo of ctx.xoutputs) {
|
|
9662
10278
|
const lockingScript = asArray(xo.lockingScript);
|
|
@@ -9672,7 +10288,7 @@ async function createNewOutputs(storage, userId, vargs, ctx, changeOutputs) {
|
|
|
9672
10288
|
created_at: now,
|
|
9673
10289
|
updated_at: now,
|
|
9674
10290
|
commissionId: 0
|
|
9675
|
-
});
|
|
10291
|
+
}, trx);
|
|
9676
10292
|
const o = makeDefaultOutput(userId, ctx.transactionId, xo.satoshis, xo.vout);
|
|
9677
10293
|
o.lockingScript = lockingScript;
|
|
9678
10294
|
o.providedBy = "storage";
|
|
@@ -9706,10 +10322,12 @@ async function createNewOutputs(storage, userId, vargs, ctx, changeOutputs) {
|
|
|
9706
10322
|
});
|
|
9707
10323
|
}
|
|
9708
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);
|
|
9709
10327
|
const outputs = [];
|
|
9710
10328
|
const changeVouts = [];
|
|
9711
10329
|
for (const { o, tags } of newOutputs) {
|
|
9712
|
-
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);
|
|
9713
10331
|
if (changeVout !== void 0) changeVouts.push(changeVout);
|
|
9714
10332
|
outputs.push(ro);
|
|
9715
10333
|
}
|
|
@@ -9718,7 +10336,7 @@ async function createNewOutputs(storage, userId, vargs, ctx, changeOutputs) {
|
|
|
9718
10336
|
changeVouts
|
|
9719
10337
|
};
|
|
9720
10338
|
}
|
|
9721
|
-
async function createNewTxRecord(storage, userId, vargs, storageBeef) {
|
|
10339
|
+
async function createNewTxRecord(storage, userId, vargs, storageBeef, satoshis = 0, trx, status = "unsigned") {
|
|
9722
10340
|
const now = /* @__PURE__ */ new Date();
|
|
9723
10341
|
const newTx = {
|
|
9724
10342
|
created_at: now,
|
|
@@ -9726,20 +10344,22 @@ async function createNewTxRecord(storage, userId, vargs, storageBeef) {
|
|
|
9726
10344
|
transactionId: 0,
|
|
9727
10345
|
version: vargs.version,
|
|
9728
10346
|
lockTime: vargs.lockTime,
|
|
9729
|
-
status
|
|
10347
|
+
status,
|
|
9730
10348
|
reference: randomBytesBase64(12),
|
|
9731
|
-
satoshis
|
|
10349
|
+
satoshis,
|
|
9732
10350
|
userId,
|
|
9733
10351
|
isOutgoing: true,
|
|
9734
|
-
inputBEEF: storageBeef
|
|
10352
|
+
inputBEEF: storageBeef,
|
|
9735
10353
|
description: vargs.description,
|
|
9736
10354
|
txid: void 0,
|
|
9737
10355
|
rawTx: void 0
|
|
9738
10356
|
};
|
|
9739
|
-
newTx.transactionId = await storage.insertTransaction(newTx);
|
|
9740
|
-
|
|
9741
|
-
|
|
9742
|
-
|
|
10357
|
+
newTx.transactionId = await storage.insertTransaction(newTx, trx);
|
|
10358
|
+
const labelNames = [...new Set(vargs.labels)];
|
|
10359
|
+
const labels = await storage.findOrInsertTxLabelsBulk(userId, labelNames, trx);
|
|
10360
|
+
for (const label of labelNames) {
|
|
10361
|
+
const txLabel = labels[label];
|
|
10362
|
+
await storage.findOrInsertTxLabelMap(verifyId(newTx.transactionId), verifyId(txLabel.txLabelId), trx);
|
|
9743
10363
|
}
|
|
9744
10364
|
return newTx;
|
|
9745
10365
|
}
|
|
@@ -9923,87 +10543,227 @@ async function validateNoSendChange(storage, userId, vargs, changeBasket) {
|
|
|
9923
10543
|
const r = [];
|
|
9924
10544
|
if (!vargs.isNoSend) return [];
|
|
9925
10545
|
const noSendChange = vargs.options.noSendChange;
|
|
9926
|
-
if (noSendChange && noSendChange.length > 0)
|
|
9927
|
-
const
|
|
9928
|
-
|
|
9929
|
-
|
|
9930
|
-
|
|
9931
|
-
|
|
9932
|
-
|
|
9933
|
-
|
|
9934
|
-
r.push(output);
|
|
10546
|
+
if (noSendChange && noSendChange.length > 0) {
|
|
10547
|
+
const byOutpoint = await storage.findOutputsByOutpoints(userId, noSendChange);
|
|
10548
|
+
for (const op of noSendChange) {
|
|
10549
|
+
const output = byOutpoint[`${op.txid}.${op.vout}`];
|
|
10550
|
+
if (!isAutoSpendableChangeOutput(output) || !verifyNumber(output.satoshis) || output.basketId !== changeBasket.basketId) throw new WERR_INVALID_PARAMETER("noSendChange outpoint", "wallet-managed BRC-29 change");
|
|
10551
|
+
if (r.some((o) => o.outputId === output.outputId)) throw new WERR_INVALID_PARAMETER("noSendChange outpoint", "unique. Duplicates are not allowed.");
|
|
10552
|
+
r.push(output);
|
|
10553
|
+
}
|
|
9935
10554
|
}
|
|
9936
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");
|
|
9937
10556
|
return r;
|
|
9938
10557
|
}
|
|
9939
|
-
|
|
9940
|
-
|
|
9941
|
-
|
|
9942
|
-
|
|
9943
|
-
|
|
9944
|
-
|
|
9945
|
-
|
|
9946
|
-
|
|
9947
|
-
|
|
9948
|
-
|
|
9949
|
-
|
|
9950
|
-
|
|
9951
|
-
|
|
9952
|
-
|
|
9953
|
-
|
|
9954
|
-
|
|
9955
|
-
|
|
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
|
+
}
|
|
10561
|
+
var FundingClaimConflict = class extends Error {
|
|
10562
|
+
conflict;
|
|
10563
|
+
constructor(conflict) {
|
|
10564
|
+
super("createAction funding claim changed concurrently");
|
|
10565
|
+
this.conflict = conflict;
|
|
10566
|
+
}
|
|
10567
|
+
};
|
|
10568
|
+
async function traceStorageStep(storage, name, parent, attributes, callback) {
|
|
10569
|
+
if (!storage.telemetry.enabled) return await callback();
|
|
10570
|
+
return await storage.telemetry.withSpan(name, {
|
|
10571
|
+
component: "wallet-storage",
|
|
10572
|
+
parent: parent?.context,
|
|
10573
|
+
attributes
|
|
10574
|
+
}, async (span) => await callback(span));
|
|
10575
|
+
}
|
|
10576
|
+
function makeFundingParams(vargs, xinputs, xoutputs, changeBasket, feeModel, availableChangeCount) {
|
|
10577
|
+
return {
|
|
10578
|
+
fixedInputs: xinputs.map((input) => ({
|
|
10579
|
+
satoshis: input.satoshis,
|
|
10580
|
+
unlockingScriptLength: input.unlockingScriptLength
|
|
9956
10581
|
})),
|
|
9957
|
-
fixedOutputs:
|
|
9958
|
-
satoshis:
|
|
9959
|
-
lockingScriptLength:
|
|
10582
|
+
fixedOutputs: xoutputs.map((output) => ({
|
|
10583
|
+
satoshis: output.satoshis,
|
|
10584
|
+
lockingScriptLength: output.lockingScript.length / 2
|
|
9960
10585
|
})),
|
|
9961
|
-
feeModel
|
|
9962
|
-
changeInitialSatoshis: Math.max(1,
|
|
9963
|
-
changeFirstSatoshis: Math.max(1, Math.round(
|
|
10586
|
+
feeModel,
|
|
10587
|
+
changeInitialSatoshis: Math.max(1, changeBasket.minimumDesiredUTXOValue),
|
|
10588
|
+
changeFirstSatoshis: Math.max(1, Math.round(changeBasket.minimumDesiredUTXOValue / 4)),
|
|
9964
10589
|
changeLockingScriptLength: 25,
|
|
9965
10590
|
changeUnlockingScriptLength: 107,
|
|
9966
|
-
targetNetCount:
|
|
10591
|
+
targetNetCount: changeBasket.numberOfDesiredUTXOs - availableChangeCount,
|
|
9967
10592
|
randomVals: vargs.randomVals
|
|
9968
10593
|
};
|
|
9969
|
-
|
|
9970
|
-
|
|
9971
|
-
const
|
|
9972
|
-
|
|
9973
|
-
|
|
9974
|
-
|
|
9975
|
-
|
|
9976
|
-
|
|
9977
|
-
|
|
9978
|
-
|
|
9979
|
-
|
|
9980
|
-
|
|
10594
|
+
}
|
|
10595
|
+
async function prepareFundingPlan(storage, context) {
|
|
10596
|
+
const [userId, vargs, xinputs, xoutputs, changeBasket, noSendChangeIn, feeModel, parent, trx] = context;
|
|
10597
|
+
const excludeSending = !vargs.isDelayed;
|
|
10598
|
+
const candidates = await traceStorageStep(storage, "wallet.storage.create_action.funding_candidates", parent, { "funding.exclude_sending": excludeSending }, async (span) => {
|
|
10599
|
+
const outputs = await storage.findAvailableManagedChangeInputCandidates(userId, changeBasket.basketId, excludeSending, trx);
|
|
10600
|
+
span?.end({ attributes: {
|
|
10601
|
+
"funding.candidate_count": outputs.length,
|
|
10602
|
+
"funding.candidate_satoshis": outputs.reduce((sum, output) => sum + output.satoshis, 0)
|
|
10603
|
+
} });
|
|
10604
|
+
return outputs;
|
|
10605
|
+
});
|
|
10606
|
+
const noSendIds = new Set(noSendChangeIn.map((output) => output.outputId));
|
|
10607
|
+
const available = candidates.filter((output) => !noSendIds.has(output.outputId));
|
|
10608
|
+
const params = makeFundingParams(vargs, xinputs, xoutputs, changeBasket, feeModel, candidates.length);
|
|
10609
|
+
return await traceStorageStep(storage, "wallet.storage.create_action.funding_plan", parent, {
|
|
10610
|
+
"funding.candidate_count": available.length,
|
|
10611
|
+
"funding.no_send_change_count": noSendChangeIn.length
|
|
10612
|
+
}, async (span) => {
|
|
10613
|
+
const allocated = /* @__PURE__ */ new Map();
|
|
10614
|
+
const availableSelector = new CanonicalChangeSelector(available);
|
|
10615
|
+
const noSend = [...noSendChangeIn];
|
|
10616
|
+
const noSendById = new Map(noSendChangeIn.map((output) => [output.outputId, output]));
|
|
10617
|
+
const allocate = async (targetSatoshis, exactSatoshis) => {
|
|
10618
|
+
let output = noSend.pop();
|
|
10619
|
+
output ??= availableSelector.take(targetSatoshis, exactSatoshis);
|
|
10620
|
+
if (output == null) return void 0;
|
|
10621
|
+
allocated.set(output.outputId, output);
|
|
9981
10622
|
return {
|
|
9982
|
-
outputId:
|
|
9983
|
-
satoshis:
|
|
10623
|
+
outputId: output.outputId,
|
|
10624
|
+
satoshis: output.satoshis
|
|
9984
10625
|
};
|
|
10626
|
+
};
|
|
10627
|
+
const release = async (outputId) => {
|
|
10628
|
+
if (allocated.get(outputId) == null) return;
|
|
10629
|
+
allocated.delete(outputId);
|
|
10630
|
+
availableSelector.release(outputId);
|
|
10631
|
+
const noSendOutput = noSendById.get(outputId);
|
|
10632
|
+
if (noSendOutput != null) noSend.push(noSendOutput);
|
|
10633
|
+
};
|
|
10634
|
+
const result = await generateChangeSdk(params, allocate, release, vargs.logger, storage.telemetry);
|
|
10635
|
+
const selected = result.allocatedChangeInputs.map((input) => verifyTruthy(allocated.get(input.outputId)));
|
|
10636
|
+
span?.end({ attributes: {
|
|
10637
|
+
"funding.allocated_input_count": selected.length,
|
|
10638
|
+
"funding.change_output_count": result.changeOutputs.length,
|
|
10639
|
+
"funding.fee_satoshis": result.fee,
|
|
10640
|
+
"funding.transaction_size_bytes": result.size
|
|
10641
|
+
} });
|
|
10642
|
+
return {
|
|
10643
|
+
params,
|
|
10644
|
+
result,
|
|
10645
|
+
selected,
|
|
10646
|
+
availableChangeCount: candidates.length
|
|
10647
|
+
};
|
|
10648
|
+
});
|
|
10649
|
+
}
|
|
10650
|
+
async function claimFundingPlan(storage, request) {
|
|
10651
|
+
const [userId, basketId, excludeSending, transactionId, noSendChangeIn, plan, trx] = request;
|
|
10652
|
+
if (plan.selected.length === 0) return {
|
|
10653
|
+
outputs: [],
|
|
10654
|
+
sourceTransactionCount: 0,
|
|
10655
|
+
hydratedScriptCount: 0,
|
|
10656
|
+
scriptSourceTransactionCount: 0
|
|
10657
|
+
};
|
|
10658
|
+
const noSendIds = new Set(noSendChangeIn.map((output) => output.outputId));
|
|
10659
|
+
const statuses = ["completed", "unproven"];
|
|
10660
|
+
if (!excludeSending) statuses.push("sending");
|
|
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))];
|
|
10664
|
+
const claimed = [];
|
|
10665
|
+
for (const planned of plan.selected) {
|
|
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" };
|
|
10668
|
+
claimed.push(current);
|
|
10669
|
+
}
|
|
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");
|
|
10671
|
+
for (const output of claimed) {
|
|
10672
|
+
output.spendable = false;
|
|
10673
|
+
output.spentBy = transactionId;
|
|
9985
10674
|
}
|
|
9986
|
-
const basketId = ctx.changeBasket.basketId;
|
|
9987
|
-
const o = await storage.allocateChangeInput(userId, basketId, targetSatoshis, exactSatoshis, !vargs.isDelayed, ctx.transactionId);
|
|
9988
|
-
if (o == null) return void 0;
|
|
9989
|
-
outputs[o.outputId] = o;
|
|
9990
10675
|
return {
|
|
9991
|
-
|
|
9992
|
-
|
|
10676
|
+
outputs: claimed,
|
|
10677
|
+
sourceTransactionCount: transactionIds.length
|
|
9993
10678
|
};
|
|
10679
|
+
}, trx).catch((error) => {
|
|
10680
|
+
if (error instanceof FundingClaimConflict) return { conflict: error.conflict };
|
|
10681
|
+
throw error;
|
|
10682
|
+
});
|
|
10683
|
+
if (claim.outputs == null) return claim;
|
|
10684
|
+
const hydration = await hydrateFundingInputScripts(storage, claim.outputs, trx);
|
|
10685
|
+
return {
|
|
10686
|
+
outputs: claim.outputs,
|
|
10687
|
+
sourceTransactionCount: claim.sourceTransactionCount,
|
|
10688
|
+
...hydration
|
|
9994
10689
|
};
|
|
9995
|
-
|
|
9996
|
-
|
|
9997
|
-
|
|
9998
|
-
|
|
9999
|
-
|
|
10690
|
+
}
|
|
10691
|
+
async function hydrateFundingInputScripts(storage, outputs, trx) {
|
|
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 !== "");
|
|
10693
|
+
if (missing.length === 0) return {
|
|
10694
|
+
hydratedScriptCount: 0,
|
|
10695
|
+
scriptSourceTransactionCount: 0
|
|
10696
|
+
};
|
|
10697
|
+
const byTxid = /* @__PURE__ */ new Map();
|
|
10698
|
+
for (const output of missing) {
|
|
10699
|
+
const txid = verifyTruthy(output.txid);
|
|
10700
|
+
const group = byTxid.get(txid) ?? [];
|
|
10701
|
+
group.push(output);
|
|
10702
|
+
byTxid.set(txid, group);
|
|
10703
|
+
}
|
|
10704
|
+
const groups = [...byTxid.entries()];
|
|
10705
|
+
let cursor = 0;
|
|
10706
|
+
await Promise.all(Array.from({ length: Math.min(8, groups.length) }, async () => {
|
|
10707
|
+
while (cursor < groups.length) {
|
|
10708
|
+
const [txid, group] = groups[cursor++];
|
|
10709
|
+
if (group.length === 1) {
|
|
10710
|
+
await storage.validateOutputScript(group[0], trx);
|
|
10711
|
+
continue;
|
|
10712
|
+
}
|
|
10713
|
+
const rawTx = await storage.getRawTxOfKnownValidTransaction(txid, void 0, void 0, trx);
|
|
10714
|
+
if (rawTx != null) for (const output of group) output.lockingScript = rawTx.slice(output.scriptOffset, output.scriptOffset + output.scriptLength);
|
|
10715
|
+
else for (const output of group) await storage.validateOutputScript(output, trx);
|
|
10000
10716
|
}
|
|
10001
|
-
|
|
10002
|
-
|
|
10003
|
-
|
|
10004
|
-
|
|
10717
|
+
}));
|
|
10718
|
+
return {
|
|
10719
|
+
hydratedScriptCount: missing.filter((output) => output.lockingScript?.length === output.scriptLength).length,
|
|
10720
|
+
scriptSourceTransactionCount: groups.length
|
|
10005
10721
|
};
|
|
10006
|
-
|
|
10722
|
+
}
|
|
10723
|
+
async function fundNewTransactionSdk(storage, userId, vargs, ctx, initialPlan, parent, trx) {
|
|
10724
|
+
let plan = initialPlan;
|
|
10725
|
+
let allocatedChange;
|
|
10726
|
+
let retryCount = 0;
|
|
10727
|
+
await traceStorageStep(storage, "wallet.storage.create_action.funding_claim", parent, { "funding.planned_input_count": initialPlan.selected.length }, async (span) => {
|
|
10728
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
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
|
+
]);
|
|
10738
|
+
if (claim.outputs != null) {
|
|
10739
|
+
allocatedChange = claim.outputs;
|
|
10740
|
+
span?.end({ attributes: {
|
|
10741
|
+
"funding.claim_retry_count": retryCount,
|
|
10742
|
+
"funding.source_transaction_count": claim.sourceTransactionCount,
|
|
10743
|
+
"funding.hydrated_script_count": claim.hydratedScriptCount,
|
|
10744
|
+
"funding.script_source_transaction_count": claim.scriptSourceTransactionCount
|
|
10745
|
+
} });
|
|
10746
|
+
return;
|
|
10747
|
+
}
|
|
10748
|
+
if (claim.conflict === "noSendChange") throw new WERR_INVALID_PARAMETER("noSendChange", "outputs that remain spendable during action planning");
|
|
10749
|
+
retryCount++;
|
|
10750
|
+
plan = await prepareFundingPlan(storage, [
|
|
10751
|
+
userId,
|
|
10752
|
+
vargs,
|
|
10753
|
+
ctx.xinputs,
|
|
10754
|
+
ctx.xoutputs,
|
|
10755
|
+
ctx.changeBasket,
|
|
10756
|
+
ctx.noSendChangeIn,
|
|
10757
|
+
ctx.feeModel,
|
|
10758
|
+
parent,
|
|
10759
|
+
trx
|
|
10760
|
+
]);
|
|
10761
|
+
}
|
|
10762
|
+
throw new WERR_INVALID_OPERATION("wallet funding changed repeatedly during action planning; retry createAction");
|
|
10763
|
+
});
|
|
10764
|
+
if (allocatedChange == null) throw new WERR_INTERNAL("funding plan was not claimed");
|
|
10765
|
+
const params = plan.params;
|
|
10766
|
+
const gcr = plan.result;
|
|
10007
10767
|
const nextRandomVal = () => {
|
|
10008
10768
|
let val = 0;
|
|
10009
10769
|
if (vargs.randomVals == null || vargs.randomVals.length === 0) {
|
|
@@ -10031,7 +10791,7 @@ async function fundNewTransactionSdk(storage, userId, vargs, ctx) {
|
|
|
10031
10791
|
const derivationPrefix = randomDerivation(16);
|
|
10032
10792
|
return {
|
|
10033
10793
|
maxPossibleSatoshisAdjustment: gcr.maxPossibleSatoshisAdjustment,
|
|
10034
|
-
allocatedChange
|
|
10794
|
+
allocatedChange,
|
|
10035
10795
|
changeOutputs: gcr.changeOutputs.map((o, i) => ({
|
|
10036
10796
|
created_at: /* @__PURE__ */ new Date(),
|
|
10037
10797
|
updated_at: /* @__PURE__ */ new Date(),
|
|
@@ -10066,12 +10826,73 @@ async function fundNewTransactionSdk(storage, userId, vargs, ctx) {
|
|
|
10066
10826
|
*/
|
|
10067
10827
|
function trimInputBeef(beef, vargs) {
|
|
10068
10828
|
if (vargs.options.returnTXIDOnly) return void 0;
|
|
10069
|
-
const
|
|
10070
|
-
for (const
|
|
10071
|
-
for (const txid of beef.txs.map((btx) => btx.txid)) if (knownTxids[txid]) beef.makeTxidOnly(txid);
|
|
10829
|
+
const hasKnownTxid = makeKnownTxidLookup(vargs.options.knownTxids ?? []);
|
|
10830
|
+
for (const btx of beef.txs) if (hasKnownTxid(btx.txid)) beef.makeTxidOnly(btx.txid);
|
|
10072
10831
|
return beef.toUint8Array();
|
|
10073
10832
|
}
|
|
10074
|
-
|
|
10833
|
+
function makeKnownTxidLookup(knownTxids) {
|
|
10834
|
+
let lookups = 0;
|
|
10835
|
+
let indexed;
|
|
10836
|
+
return (txid) => {
|
|
10837
|
+
lookups++;
|
|
10838
|
+
if (indexed != null) return indexed.has(txid);
|
|
10839
|
+
if (knownTxids.length > 64 && lookups > 4) {
|
|
10840
|
+
indexed = new Set(knownTxids);
|
|
10841
|
+
return indexed.has(txid);
|
|
10842
|
+
}
|
|
10843
|
+
return knownTxids.includes(txid);
|
|
10844
|
+
};
|
|
10845
|
+
}
|
|
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) {
|
|
10075
10896
|
const options = {
|
|
10076
10897
|
trustSelf: void 0,
|
|
10077
10898
|
knownTxids: vargs.options.knownTxids,
|
|
@@ -10082,25 +10903,52 @@ async function mergeAllocatedChangeBeefs(storage, userId, vargs, allocatedChange
|
|
|
10082
10903
|
minProofLevel: void 0
|
|
10083
10904
|
};
|
|
10084
10905
|
if (vargs.options.returnTXIDOnly) return void 0;
|
|
10085
|
-
const
|
|
10086
|
-
const
|
|
10087
|
-
const
|
|
10088
|
-
|
|
10089
|
-
|
|
10090
|
-
|
|
10091
|
-
|
|
10092
|
-
|
|
10093
|
-
|
|
10094
|
-
|
|
10095
|
-
|
|
10096
|
-
|
|
10097
|
-
|
|
10098
|
-
|
|
10099
|
-
|
|
10100
|
-
|
|
10101
|
-
beef.
|
|
10102
|
-
|
|
10103
|
-
|
|
10906
|
+
const knownTxids = vargs.options.knownTxids ?? [];
|
|
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;
|
|
10918
|
+
await traceStorageStep(storage, "wallet.storage.create_action.beef_fetch", parent, {
|
|
10919
|
+
"beef.allocated_change_count": allocatedChange.length,
|
|
10920
|
+
"beef.distinct_source_count": new Set(allocatedChange.map((output) => output.txid)).size,
|
|
10921
|
+
"beef.known_txid_count": knownTxids.length,
|
|
10922
|
+
"beef.missing_source_count": missing.length,
|
|
10923
|
+
"beef.fetch_concurrency": 1,
|
|
10924
|
+
"beef.prefetch_reused": usePrefetch
|
|
10925
|
+
}, async (span) => {
|
|
10926
|
+
if (missing.length > 0) fetched = await storage.getBeefForTransactions(missing, {
|
|
10927
|
+
...options,
|
|
10928
|
+
mergeToBeef: void 0
|
|
10929
|
+
});
|
|
10930
|
+
span?.end({ attributes: {
|
|
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
|
|
10934
|
+
} });
|
|
10935
|
+
});
|
|
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);
|
|
10938
|
+
span?.end({ attributes: {
|
|
10939
|
+
"beef.merged_tx_count": beef.txs.length,
|
|
10940
|
+
"beef.merged_bump_count": beef.bumps.length
|
|
10941
|
+
} });
|
|
10942
|
+
});
|
|
10943
|
+
return await traceStorageStep(storage, "wallet.storage.create_action.beef_trim_serialize", parent, {
|
|
10944
|
+
"beef.tx_count": beef.txs.length,
|
|
10945
|
+
"beef.bump_count": beef.bumps.length,
|
|
10946
|
+
"beef.known_txid_count": knownTxids.length
|
|
10947
|
+
}, async (span) => {
|
|
10948
|
+
const result = trimInputBeef(beef, vargs);
|
|
10949
|
+
span?.end({ attributes: { "beef.result_bytes": result?.length ?? 0 } });
|
|
10950
|
+
return result;
|
|
10951
|
+
});
|
|
10104
10952
|
}
|
|
10105
10953
|
const dirtyHashLookup = {
|
|
10106
10954
|
"00000000000000000019f112ec0a9982926f1258cdcc558dd7c3b7e5dc7fa148": "This is the first header of the invalid SegWit chain.",
|
|
@@ -12715,21 +13563,6 @@ async function cleanupExpiredActionBatches(storage) {
|
|
|
12715
13563
|
});
|
|
12716
13564
|
return released;
|
|
12717
13565
|
}
|
|
12718
|
-
async function availableManagedChange(storage, userId, basketId, excludeSending, trx) {
|
|
12719
|
-
const statuses = ["completed", "unproven"];
|
|
12720
|
-
if (!excludeSending) statuses.push("sending");
|
|
12721
|
-
const outputs = (await storage.findOutputs({
|
|
12722
|
-
partial: {
|
|
12723
|
-
userId,
|
|
12724
|
-
basketId,
|
|
12725
|
-
spendable: true
|
|
12726
|
-
},
|
|
12727
|
-
txStatus: statuses,
|
|
12728
|
-
trx
|
|
12729
|
-
})).filter(isAutoSpendableChangeOutput);
|
|
12730
|
-
const reserved = new Set(await storage.findReservedActionBatchOutputIds(outputs.map((o) => o.outputId), trx));
|
|
12731
|
-
return outputs.filter((output) => output.spentBy == null && !reserved.has(output.outputId));
|
|
12732
|
-
}
|
|
12733
13566
|
function sourceOutputFromBeef(beef, outpoint) {
|
|
12734
13567
|
const output = (beef.findTxid(outpoint.txid)?.tx)?.outputs[outpoint.vout];
|
|
12735
13568
|
if (output == null) return void 0;
|
|
@@ -12894,7 +13727,7 @@ async function beginActionBatch(storage, auth, args) {
|
|
|
12894
13727
|
const explicit = await resolveExplicitOutputs(storage, userId, args.firstAction, outputScriptLengths != null);
|
|
12895
13728
|
const noSendChange = await resolveNoSendChangeOutputs(storage, userId, args.firstAction);
|
|
12896
13729
|
const fixedOutputIds = new Set([...explicit.outputs, ...noSendChange.outputs].map((output) => output.outputId));
|
|
12897
|
-
const available = (await
|
|
13730
|
+
const available = (await storage.findAvailableManagedChangeInputs(userId, changeBasket.basketId, !args.firstAction.isDelayed)).filter((output) => !fixedOutputIds.has(output.outputId));
|
|
12898
13731
|
const target = estimateFirstActionTarget(storage, args.firstAction, explicit.inputSatoshis + noSendChange.inputSatoshis, outputScriptLengths);
|
|
12899
13732
|
const fixedOutputs = [...explicit.outputs, ...noSendChange.outputs];
|
|
12900
13733
|
const funding = chooseReservationPool(available, target, Math.max(0, INITIAL_RESERVATION_LIMIT - fixedOutputs.length), INITIAL_EXTRA_OUTPUTS, false, reservationPlanningCosts(storage, changeBasket));
|
|
@@ -12938,7 +13771,7 @@ async function extendActionBatch(storage, auth, args) {
|
|
|
12938
13771
|
name: "default"
|
|
12939
13772
|
} }));
|
|
12940
13773
|
const alreadyReserved = await storage.findActionBatchOutputIds(batch.actionBatchId);
|
|
12941
|
-
const available = await
|
|
13774
|
+
const available = await storage.findAvailableManagedChangeInputs(userId, basket.basketId, false);
|
|
12942
13775
|
if (!Number.isSafeInteger(args.requestedOutputs) || args.requestedOutputs < 0) throw new WERR_INVALID_PARAMETER("requestedOutputs", "non-negative safe integer");
|
|
12943
13776
|
const requestedCount = Math.min(args.requestedOutputs, 64);
|
|
12944
13777
|
const funding = chooseReservationPool(available, Math.max(1, args.targetSatoshis), requestedCount, 0, true, reservationPlanningCosts(storage, basket));
|
|
@@ -13311,6 +14144,31 @@ async function abortActionBatch(storage, auth, batchId) {
|
|
|
13311
14144
|
});
|
|
13312
14145
|
}
|
|
13313
14146
|
//#endregion
|
|
14147
|
+
//#region ../src/storage/methods/availableManagedChange.ts
|
|
14148
|
+
/**
|
|
14149
|
+
* Return the exact set of wallet-managed outputs currently eligible for
|
|
14150
|
+
* automatic funding. Keeping this predicate shared prevents the planner,
|
|
14151
|
+
* allocator, action-batch reservations, and availability count from drifting.
|
|
14152
|
+
*/
|
|
14153
|
+
async function availableManagedChange(storage, userId, basketId, excludeSending, trx) {
|
|
14154
|
+
const statuses = ["completed", "unproven"];
|
|
14155
|
+
if (!excludeSending) statuses.push("sending");
|
|
14156
|
+
const outputs = (await storage.findOutputs({
|
|
14157
|
+
partial: {
|
|
14158
|
+
userId,
|
|
14159
|
+
basketId,
|
|
14160
|
+
spendable: true,
|
|
14161
|
+
...managedChangeOutputFields
|
|
14162
|
+
},
|
|
14163
|
+
txStatus: statuses,
|
|
14164
|
+
noScript: true,
|
|
14165
|
+
trx
|
|
14166
|
+
})).filter(isAutoSpendableChangeOutput);
|
|
14167
|
+
if (outputs.length === 0) return outputs;
|
|
14168
|
+
const reserved = new Set(await storage.findReservedActionBatchOutputIds(outputs.map((output) => output.outputId), trx));
|
|
14169
|
+
return outputs.filter((output) => !reserved.has(output.outputId));
|
|
14170
|
+
}
|
|
14171
|
+
//#endregion
|
|
13314
14172
|
//#region ../src/storage/StorageProvider.ts
|
|
13315
14173
|
var StorageProvider = class StorageProvider extends StorageReaderWriter {
|
|
13316
14174
|
isDirty = false;
|
|
@@ -13344,6 +14202,81 @@ var StorageProvider = class StorageProvider extends StorageReaderWriter {
|
|
|
13344
14202
|
this.maxRecursionDepth = 12;
|
|
13345
14203
|
this.scriptVerifier = options.scriptVerifier;
|
|
13346
14204
|
}
|
|
14205
|
+
/** Mark a planned set of change inputs spent within the caller's transaction. */
|
|
14206
|
+
async markChangeInputsSpent(outputIds, transactionId, trx) {
|
|
14207
|
+
let updated = 0;
|
|
14208
|
+
const current = await this.findOutputsByIds(outputIds, trx);
|
|
14209
|
+
for (const outputId of outputIds) {
|
|
14210
|
+
const output = current[outputId];
|
|
14211
|
+
if (output == null || !output.spendable || output.spentBy != null) continue;
|
|
14212
|
+
updated += await this.updateOutput(outputId, {
|
|
14213
|
+
spendable: false,
|
|
14214
|
+
spentBy: transactionId
|
|
14215
|
+
}, trx);
|
|
14216
|
+
}
|
|
14217
|
+
return updated;
|
|
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
|
+
}
|
|
14227
|
+
/** Return unreserved wallet-managed outputs eligible for automatic funding. */
|
|
14228
|
+
async findAvailableManagedChangeInputs(userId, basketId, excludeSending, trx) {
|
|
14229
|
+
return await availableManagedChange(this, userId, basketId, excludeSending, trx);
|
|
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
|
+
}
|
|
14241
|
+
/** Read the current status of a set of source transactions without loading raw transaction bytes. */
|
|
14242
|
+
async findTransactionStatusesByIds(userId, transactionIds, trx) {
|
|
14243
|
+
const statuses = /* @__PURE__ */ new Map();
|
|
14244
|
+
for (const transactionId of new Set(transactionIds)) {
|
|
14245
|
+
const transaction = await this.findTransactionById(transactionId, trx, true);
|
|
14246
|
+
if (transaction?.userId === userId) statuses.set(transactionId, transaction.status);
|
|
14247
|
+
}
|
|
14248
|
+
return statuses;
|
|
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
|
+
}
|
|
13347
14280
|
async insertActionBatch(_batch, _trx) {
|
|
13348
14281
|
throw new WERR_NOT_IMPLEMENTED();
|
|
13349
14282
|
}
|
|
@@ -13395,6 +14328,10 @@ var StorageProvider = class StorageProvider extends StorageReaderWriter {
|
|
|
13395
14328
|
supportsActionBatchPersistence() {
|
|
13396
14329
|
return false;
|
|
13397
14330
|
}
|
|
14331
|
+
/** Custom providers may require physical expiry cleanup before reservations are queried. */
|
|
14332
|
+
requiresActionBatchCleanupBeforeCreateAction() {
|
|
14333
|
+
return true;
|
|
14334
|
+
}
|
|
13398
14335
|
async beginActionBatch(auth, args) {
|
|
13399
14336
|
if (!this.supportsActionBatchPersistence()) throw new WERR_NOT_IMPLEMENTED("actionBatch capability is not available");
|
|
13400
14337
|
return await beginActionBatch(this, auth, args);
|
|
@@ -13454,7 +14391,7 @@ var StorageProvider = class StorageProvider extends StorageReaderWriter {
|
|
|
13454
14391
|
}
|
|
13455
14392
|
return byOutpoint;
|
|
13456
14393
|
}
|
|
13457
|
-
async findOutputsByOutpointsForUpdate(userId, outpoints, trx) {
|
|
14394
|
+
async findOutputsByOutpointsForUpdate(userId, outpoints, trx, _noScript = false) {
|
|
13458
14395
|
return await this.findOutputsByOutpoints(userId, outpoints, trx);
|
|
13459
14396
|
}
|
|
13460
14397
|
async findOrInsertOutputBasketsBulk(userId, names, trx) {
|
|
@@ -13783,7 +14720,7 @@ var StorageProvider = class StorageProvider extends StorageReaderWriter {
|
|
|
13783
14720
|
}
|
|
13784
14721
|
async createAction(auth, args) {
|
|
13785
14722
|
if (auth.userId == null) throw new WERR_UNAUTHORIZED();
|
|
13786
|
-
if (this.supportsActionBatchPersistence()) await cleanupExpiredActionBatches(this);
|
|
14723
|
+
if (this.supportsActionBatchPersistence() && this.requiresActionBatchCleanupBeforeCreateAction()) await cleanupExpiredActionBatches(this);
|
|
13787
14724
|
return await createAction(this, auth, args);
|
|
13788
14725
|
}
|
|
13789
14726
|
async processAction(auth, args) {
|
|
@@ -13873,6 +14810,9 @@ var StorageProvider = class StorageProvider extends StorageReaderWriter {
|
|
|
13873
14810
|
async getBeefForTransaction(txid, options) {
|
|
13874
14811
|
return await getBeefForTransaction(this, txid, options);
|
|
13875
14812
|
}
|
|
14813
|
+
async getBeefForTransactions(txids, options) {
|
|
14814
|
+
return await getBeefForTransactions(this, txids, options);
|
|
14815
|
+
}
|
|
13876
14816
|
async findMonitorEventById(id, trx) {
|
|
13877
14817
|
return verifyOneOrNone(await this.findMonitorEvents({
|
|
13878
14818
|
partial: { id },
|
|
@@ -14831,6 +15771,9 @@ var StorageIdb = class extends StorageProvider {
|
|
|
14831
15771
|
supportsActionBatchPersistence() {
|
|
14832
15772
|
return true;
|
|
14833
15773
|
}
|
|
15774
|
+
requiresActionBatchCleanupBeforeCreateAction() {
|
|
15775
|
+
return false;
|
|
15776
|
+
}
|
|
14834
15777
|
/**
|
|
14835
15778
|
* This method must be called at least once before any other method accesses the database,
|
|
14836
15779
|
* and each time the schema may have updated.
|
|
@@ -14891,9 +15834,16 @@ var StorageIdb = class extends StorageProvider {
|
|
|
14891
15834
|
async initDB(storageName, storageIdentityKey) {
|
|
14892
15835
|
const chain = this.chain;
|
|
14893
15836
|
const maxOutputScript = 1024;
|
|
14894
|
-
return await openDB(this.dbName,
|
|
15837
|
+
return await openDB(this.dbName, 3, { upgrade(db, _oldVersion, _newVersion, transaction) {
|
|
14895
15838
|
upgradeAllStoresV1(db);
|
|
14896
15839
|
upgradeActionBatchStoresV2(db);
|
|
15840
|
+
const outputs = transaction.objectStore("outputs");
|
|
15841
|
+
if (!outputs.indexNames.contains("userId_basketId")) outputs.createIndex("userId_basketId", ["userId", "basketId"]);
|
|
15842
|
+
if (!outputs.indexNames.contains("txid_vout_userId")) outputs.createIndex("txid_vout_userId", [
|
|
15843
|
+
"txid",
|
|
15844
|
+
"vout",
|
|
15845
|
+
"userId"
|
|
15846
|
+
], { unique: true });
|
|
14897
15847
|
if (!db.objectStoreNames.contains("settings")) {
|
|
14898
15848
|
if (storageName == null || storageName === "" || storageIdentityKey == null || storageIdentityKey === "") throw new WERR_INVALID_OPERATION("migrate must be called before first access");
|
|
14899
15849
|
const settings = db.createObjectStore("settings", { keyPath: "storageIdentityKey" });
|
|
@@ -15003,6 +15953,50 @@ var StorageIdb = class extends StorageProvider {
|
|
|
15003
15953
|
}
|
|
15004
15954
|
return r;
|
|
15005
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
|
+
}
|
|
15006
16000
|
async getRawTxOfKnownValidTransaction(txid, offset, length, trx) {
|
|
15007
16001
|
if (txid == null || txid === "") return void 0;
|
|
15008
16002
|
if (!this.isAvailable()) await this.makeAvailable();
|
|
@@ -15099,11 +16093,53 @@ var StorageIdb = class extends StorageProvider {
|
|
|
15099
16093
|
txStatus,
|
|
15100
16094
|
noScript: true
|
|
15101
16095
|
};
|
|
15102
|
-
|
|
16096
|
+
const outputIds = [];
|
|
15103
16097
|
await this.filterOutputs(args, (r) => {
|
|
15104
|
-
if (isAutoSpendableChangeOutput(r))
|
|
16098
|
+
if (isAutoSpendableChangeOutput(r)) outputIds.push(r.outputId);
|
|
15105
16099
|
});
|
|
15106
|
-
|
|
16100
|
+
const reserved = await this.findReservedActionBatchOutputIds(outputIds);
|
|
16101
|
+
return outputIds.length - reserved.length;
|
|
16102
|
+
}
|
|
16103
|
+
async findTransactionStatusesByIds(userId, transactionIds, trx) {
|
|
16104
|
+
const statuses = /* @__PURE__ */ new Map();
|
|
16105
|
+
if (transactionIds.length === 0) return statuses;
|
|
16106
|
+
const dbTrx = this.toDbTrx(["transactions"], "readonly", trx);
|
|
16107
|
+
const store = dbTrx.objectStore("transactions");
|
|
16108
|
+
for (const transactionId of new Set(transactionIds)) {
|
|
16109
|
+
const transaction = await store.get(transactionId);
|
|
16110
|
+
if (transaction?.userId === userId) statuses.set(transactionId, transaction.status);
|
|
16111
|
+
}
|
|
16112
|
+
if (trx == null) await dbTrx.done;
|
|
16113
|
+
return statuses;
|
|
16114
|
+
}
|
|
16115
|
+
async findOutputsByOutpointsInternal(userId, outpoints, trx, noScript = false) {
|
|
16116
|
+
const byOutpoint = {};
|
|
16117
|
+
if (outpoints.length === 0) return byOutpoint;
|
|
16118
|
+
const dbTrx = this.toDbTrx(noScript ? ["outputs"] : [
|
|
16119
|
+
"outputs",
|
|
16120
|
+
"proven_txs",
|
|
16121
|
+
"proven_tx_reqs"
|
|
16122
|
+
], "readonly", trx);
|
|
16123
|
+
const index = dbTrx.objectStore("outputs").index("txid_vout_userId");
|
|
16124
|
+
const unique = [...new Map(outpoints.map((outpoint) => [`${outpoint.txid}.${outpoint.vout}`, outpoint])).values()];
|
|
16125
|
+
const rows = await Promise.all(unique.map(async (outpoint) => await index.get([
|
|
16126
|
+
outpoint.txid,
|
|
16127
|
+
outpoint.vout,
|
|
16128
|
+
userId
|
|
16129
|
+
])));
|
|
16130
|
+
for (const row of rows) {
|
|
16131
|
+
if (row == null) continue;
|
|
16132
|
+
if (!noScript) await this.validateOutputScript(row, dbTrx);
|
|
16133
|
+
byOutpoint[`${String(row.txid)}.${row.vout}`] = this.validateEntity(row);
|
|
16134
|
+
}
|
|
16135
|
+
if (trx == null) await dbTrx.done;
|
|
16136
|
+
return byOutpoint;
|
|
16137
|
+
}
|
|
16138
|
+
async findOutputsByOutpoints(userId, outpoints, trx) {
|
|
16139
|
+
return await this.findOutputsByOutpointsInternal(userId, outpoints, trx);
|
|
16140
|
+
}
|
|
16141
|
+
async findOutputsByOutpointsForUpdate(userId, outpoints, trx, noScript = false) {
|
|
16142
|
+
return await this.findOutputsByOutpointsInternal(userId, outpoints, trx, noScript);
|
|
15107
16143
|
}
|
|
15108
16144
|
async findCertificatesAuth(auth, args) {
|
|
15109
16145
|
if (auth.userId == null || args.partial.userId != null && args.partial.userId !== 0 && args.partial.userId !== auth.userId) throw new WERR_UNAUTHORIZED();
|
|
@@ -15222,6 +16258,7 @@ var StorageIdb = class extends StorageProvider {
|
|
|
15222
16258
|
else cursor = await store.openCursor(null, direction);
|
|
15223
16259
|
await scanCursor(cursor, args.since, args.paged?.offset ?? 0, args.paged?.limit, async (r) => {
|
|
15224
16260
|
if (!matchesProvenTxPartial(r, args.partial)) return false;
|
|
16261
|
+
if (args.txids != null && args.txids.length > 0 && !args.txids.includes(r.txid)) return false;
|
|
15225
16262
|
if (userId !== void 0) {
|
|
15226
16263
|
if (await this.countTransactions({
|
|
15227
16264
|
partial: {
|
|
@@ -15666,11 +16703,18 @@ var StorageIdb = class extends StorageProvider {
|
|
|
15666
16703
|
return rows.map((r) => r.outputId);
|
|
15667
16704
|
}
|
|
15668
16705
|
async findReservedActionBatchOutputIds(outputIds, trx) {
|
|
15669
|
-
const tx = this.toDbTrx(["action_batch_outputs"], "readonly", trx);
|
|
16706
|
+
const tx = this.toDbTrx(["action_batch_outputs", "action_batches"], "readonly", trx);
|
|
15670
16707
|
const store = tx.objectStore("action_batch_outputs");
|
|
16708
|
+
const batchStore = tx.objectStore("action_batches");
|
|
15671
16709
|
if (store.get == null) throw new WERR_INTERNAL("IndexedDB action_batch_outputs store does not support get");
|
|
15672
16710
|
const reserved = [];
|
|
15673
|
-
|
|
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
|
+
}
|
|
15674
16718
|
if (trx == null) await tx.done;
|
|
15675
16719
|
return reserved;
|
|
15676
16720
|
}
|
|
@@ -15866,6 +16910,12 @@ var StorageIdb = class extends StorageProvider {
|
|
|
15866
16910
|
partial.vout,
|
|
15867
16911
|
partial.userId
|
|
15868
16912
|
], direction);
|
|
16913
|
+
if (partial?.txid != null && partial.txid !== "" && partial?.vout !== void 0) return store.index("txid_vout_userId").openCursor([
|
|
16914
|
+
partial.txid,
|
|
16915
|
+
partial.vout,
|
|
16916
|
+
partial.userId
|
|
16917
|
+
], direction);
|
|
16918
|
+
if (partial?.basketId !== void 0) return store.index("userId_basketId").openCursor([partial.userId, partial.basketId], direction);
|
|
15869
16919
|
return store.index("userId").openCursor(partial.userId, direction);
|
|
15870
16920
|
}
|
|
15871
16921
|
if (partial?.transactionId !== void 0) return store.index("transactionId").openCursor(partial.transactionId, direction);
|
|
@@ -15873,6 +16923,17 @@ var StorageIdb = class extends StorageProvider {
|
|
|
15873
16923
|
if (partial?.spentBy !== void 0) return store.index("spentBy").openCursor(partial.spentBy, direction);
|
|
15874
16924
|
return store.openCursor(null, direction);
|
|
15875
16925
|
}
|
|
16926
|
+
async eligibleOutputTransactionIds(args, dbTrx) {
|
|
16927
|
+
if (args.txStatus == null) return void 0;
|
|
16928
|
+
const validTransactionIds = /* @__PURE__ */ new Set();
|
|
16929
|
+
const transactions = dbTrx.objectStore("transactions");
|
|
16930
|
+
for (const status of args.txStatus) {
|
|
16931
|
+
const index = args.partial.userId === void 0 ? transactions.index("status") : transactions.index("status_userId");
|
|
16932
|
+
const key = args.partial.userId === void 0 ? status : [status, args.partial.userId];
|
|
16933
|
+
for (const transactionId of await index.getAllKeys(key)) validTransactionIds.add(Number(transactionId));
|
|
16934
|
+
}
|
|
16935
|
+
return validTransactionIds;
|
|
16936
|
+
}
|
|
15876
16937
|
async filterOutputs(args, filtered, tagIds, isQueryModeAll) {
|
|
15877
16938
|
this.assertNoUndefinedInPartial(args.partial);
|
|
15878
16939
|
if (args.partial.lockingScript != null) throw new WERR_INVALID_PARAMETER("args.partial.lockingScript", "undefined. Outputs may not be found by lockingScript value.");
|
|
@@ -15882,15 +16943,10 @@ var StorageIdb = class extends StorageProvider {
|
|
|
15882
16943
|
const dbTrx = this.toDbTrx(stores, "readonly", args.trx);
|
|
15883
16944
|
const direction = args.orderDescending === true ? "prev" : "next";
|
|
15884
16945
|
const store = dbTrx.objectStore("outputs");
|
|
16946
|
+
const validTransactionIds = await this.eligibleOutputTransactionIds(args, dbTrx);
|
|
15885
16947
|
await scanCursor(await this.openOutputsCursor(store, args.partial, direction), args.since, args.paged?.offset ?? 0, args.paged?.limit, async (r) => {
|
|
15886
16948
|
if (!matchesOutputPartial(r, args.partial)) return false;
|
|
15887
|
-
if (
|
|
15888
|
-
if (await this.countTransactions({
|
|
15889
|
-
partial: { transactionId: r.transactionId },
|
|
15890
|
-
status: args.txStatus,
|
|
15891
|
-
trx: dbTrx
|
|
15892
|
-
}) === 0) return false;
|
|
15893
|
-
}
|
|
16949
|
+
if (validTransactionIds != null && !validTransactionIds.has(r.transactionId)) return false;
|
|
15894
16950
|
if (tagIds != null && tagIds.length > 0 && !await this.outputMatchesTags(r.outputId, tagIds, isQueryModeAll, dbTrx)) return false;
|
|
15895
16951
|
return true;
|
|
15896
16952
|
}, (r) => {
|
|
@@ -27588,7 +28644,7 @@ function isValidProfile(value) {
|
|
|
27588
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;
|
|
27589
28645
|
}
|
|
27590
28646
|
/**
|
|
27591
|
-
* Raised when UMP
|
|
28647
|
+
* Raised when a UMP lookup yields neither a verified token nor a clean empty response.
|
|
27592
28648
|
*
|
|
27593
28649
|
* Callers must offer retry/recovery rather than treating this error as a new
|
|
27594
28650
|
* account. Diagnostics contain counts only and never hashes, keys, or tokens.
|
|
@@ -27686,49 +28742,119 @@ var OverlayUMPTokenInteractor = class {
|
|
|
27686
28742
|
throw new UMPTokenLookupError("lookup-unavailable", diagnostics, { cause: error });
|
|
27687
28743
|
}
|
|
27688
28744
|
const diagnostics = this.toLookupDiagnostics(resolution);
|
|
27689
|
-
if (resolution.answer.outputs.length === 0) {
|
|
27690
|
-
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)) {
|
|
27691
|
-
this.captureLookupFailure(lookupKind, "lookup-incomplete", diagnostics, startedAt);
|
|
27692
|
-
throw new UMPTokenLookupError("lookup-incomplete", diagnostics);
|
|
27693
|
-
}
|
|
27694
|
-
this.telemetry.capture({
|
|
27695
|
-
name: "wallet-toolbox.ump.lookup.completed",
|
|
27696
|
-
component: "wallet-toolbox.ump",
|
|
27697
|
-
severity: "info",
|
|
27698
|
-
correlationId: diagnostics.correlationId,
|
|
27699
|
-
attributes: {
|
|
27700
|
-
lookupKind,
|
|
27701
|
-
result: "not-found",
|
|
27702
|
-
durationMs: Date.now() - startedAt,
|
|
27703
|
-
...this.lookupDiagnosticAttributes(diagnostics)
|
|
27704
|
-
}
|
|
27705
|
-
});
|
|
27706
|
-
return;
|
|
27707
|
-
}
|
|
27708
28745
|
const tokens = this.parseLookupAnswers(resolution.answer);
|
|
27709
28746
|
const expectedHash = question.query[lookupKind === "presentation" ? "presentationHash" : "recoveryHash"].toLowerCase();
|
|
27710
|
-
|
|
27711
|
-
|
|
27712
|
-
|
|
27713
|
-
|
|
27714
|
-
|
|
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
|
+
}
|
|
27715
28754
|
const reason = "token-ambiguous";
|
|
27716
28755
|
this.captureLookupFailure(lookupKind, reason, diagnostics, startedAt);
|
|
27717
28756
|
throw new UMPTokenLookupError(reason, diagnostics);
|
|
27718
28757
|
}
|
|
27719
|
-
|
|
27720
|
-
|
|
27721
|
-
|
|
27722
|
-
|
|
27723
|
-
|
|
27724
|
-
|
|
27725
|
-
|
|
27726
|
-
|
|
27727
|
-
|
|
27728
|
-
|
|
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;
|
|
27729
28834
|
}
|
|
27730
|
-
}
|
|
27731
|
-
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
|
+
}
|
|
27732
28858
|
}
|
|
27733
28859
|
emptyLookupDiagnostics(correlationId) {
|
|
27734
28860
|
return {
|
|
@@ -27769,6 +28895,21 @@ var OverlayUMPTokenInteractor = class {
|
|
|
27769
28895
|
outputCount: diagnostics.outputCount
|
|
27770
28896
|
};
|
|
27771
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
|
+
}
|
|
27772
28913
|
captureLookupFailure(lookupKind, reason, diagnostics, startedAt, error) {
|
|
27773
28914
|
this.telemetry.capture({
|
|
27774
28915
|
name: "wallet-toolbox.ump.lookup.indeterminate",
|
|
@@ -28009,8 +29150,7 @@ var OverlayUMPTokenInteractor = class {
|
|
|
28009
29150
|
throw new UMPTokenLookupError("lookup-unavailable", diagnostics, { cause: error });
|
|
28010
29151
|
}
|
|
28011
29152
|
if (resolution.answer.outputs.length === 0) {
|
|
28012
|
-
|
|
28013
|
-
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) {
|
|
28014
29154
|
const diagnostics = this.toLookupDiagnostics(resolution);
|
|
28015
29155
|
this.captureLookupFailure("outpoint", "lookup-incomplete", diagnostics, startedAt);
|
|
28016
29156
|
throw new UMPTokenLookupError("lookup-incomplete", diagnostics);
|