@bsv/wallet-toolbox-client 2.4.22 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/out/index.client.cjs +1557 -403
- package/out/index.client.cjs.map +1 -1
- package/out/index.client.d.cts +206 -20
- package/out/index.client.d.cts.map +1 -1
- package/out/index.client.d.mts +206 -20
- package/out/index.client.d.mts.map +1 -1
- package/out/index.client.mjs +1550 -405
- package/out/index.client.mjs.map +1 -1
- package/package.json +2 -2
package/out/index.client.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";
|
|
@@ -970,6 +970,7 @@ function toWalletNetwork(chain) {
|
|
|
970
970
|
switch (chain) {
|
|
971
971
|
case "main": return "mainnet";
|
|
972
972
|
case "test":
|
|
973
|
+
case "stn":
|
|
973
974
|
case "ttn":
|
|
974
975
|
case "tstn":
|
|
975
976
|
case "mock": return "testnet";
|
|
@@ -983,6 +984,7 @@ function toLookupNetworkPreset(chain) {
|
|
|
983
984
|
switch (chain) {
|
|
984
985
|
case "main": return "mainnet";
|
|
985
986
|
case "test": return "testnet";
|
|
987
|
+
case "stn":
|
|
986
988
|
case "ttn":
|
|
987
989
|
case "tstn":
|
|
988
990
|
case "mock": return "local";
|
|
@@ -1222,7 +1224,11 @@ var ScriptTemplateBRC29 = class {
|
|
|
1222
1224
|
return `${this.params.derivationPrefix ?? ""} ${this.params.derivationSuffix ?? ""}`;
|
|
1223
1225
|
}
|
|
1224
1226
|
getKeyDeriver(privKey) {
|
|
1225
|
-
if (
|
|
1227
|
+
if (this.params.keyDeriver?.rootKey === privKey) return this.params.keyDeriver;
|
|
1228
|
+
if (typeof privKey === "string") {
|
|
1229
|
+
if (this.params.keyDeriver?.rootKey.toHex() === privKey) return this.params.keyDeriver;
|
|
1230
|
+
privKey = PrivateKey.fromHex(privKey);
|
|
1231
|
+
}
|
|
1226
1232
|
if (this.params.keyDeriver == null || this.params.keyDeriver.rootKey.toHex() !== privKey.toHex()) return new CachedKeyDeriver(privKey);
|
|
1227
1233
|
return this.params.keyDeriver;
|
|
1228
1234
|
}
|
|
@@ -1231,8 +1237,11 @@ var ScriptTemplateBRC29 = class {
|
|
|
1231
1237
|
return this.p2pkh.lock(address);
|
|
1232
1238
|
}
|
|
1233
1239
|
unlock(unlockerPrivKey, lockerPubKey, sourceSatoshis, lockingScript) {
|
|
1234
|
-
const derivedPrivateKey = this.getKeyDeriver(unlockerPrivKey).derivePrivateKey(brc29ProtocolID, this.getKeyID(), lockerPubKey)
|
|
1235
|
-
return this.
|
|
1240
|
+
const derivedPrivateKey = this.getKeyDeriver(unlockerPrivKey).derivePrivateKey(brc29ProtocolID, this.getKeyID(), lockerPubKey);
|
|
1241
|
+
return this.unlockWithDerivedPrivateKey(derivedPrivateKey, sourceSatoshis, lockingScript);
|
|
1242
|
+
}
|
|
1243
|
+
unlockWithDerivedPrivateKey(derivedPrivateKey, sourceSatoshis, lockingScript) {
|
|
1244
|
+
return this.p2pkh.unlock(derivedPrivateKey, "all", false, sourceSatoshis, lockingScript);
|
|
1236
1245
|
}
|
|
1237
1246
|
/**
|
|
1238
1247
|
* P2PKH unlock estimateLength is a constant
|
|
@@ -2426,11 +2435,16 @@ var EntityProvenTx = class EntityProvenTx extends EntityBase {
|
|
|
2426
2435
|
/**
|
|
2427
2436
|
* @returns desirialized `MerklePath` object, value is cached.
|
|
2428
2437
|
*/
|
|
2429
|
-
getMerklePath() {
|
|
2430
|
-
|
|
2431
|
-
|
|
2438
|
+
getMerklePath(validateRoots = true) {
|
|
2439
|
+
if (validateRoots) {
|
|
2440
|
+
this._mp ??= MerklePath.fromBinary(this.api.merklePath);
|
|
2441
|
+
return this._mp;
|
|
2442
|
+
}
|
|
2443
|
+
this._mpUnchecked ??= MerklePath.fromBinary(this.api.merklePath, true, false);
|
|
2444
|
+
return this._mpUnchecked;
|
|
2432
2445
|
}
|
|
2433
2446
|
_mp;
|
|
2447
|
+
_mpUnchecked;
|
|
2434
2448
|
get provenTxId() {
|
|
2435
2449
|
return this.api.provenTxId;
|
|
2436
2450
|
}
|
|
@@ -5027,8 +5041,10 @@ async function mergeInputBeefs(rawTx, beef, trustSelf, knownTxids, trx, required
|
|
|
5027
5041
|
for (const input of tx.inputs) {
|
|
5028
5042
|
const sourceTXID = input.sourceTXID ?? "";
|
|
5029
5043
|
if (sourceTXID === "") throw new WERR_INTERNAL("req all transaction inputs must have valid sourceTXID");
|
|
5030
|
-
|
|
5031
|
-
|
|
5044
|
+
const existing = beef.findTxid(sourceTXID);
|
|
5045
|
+
const callerKnows = (requiredLevels == null || requiredLevels === 0) && knownTxids?.includes(sourceTXID) === true;
|
|
5046
|
+
if (existing != null && (!existing.isTxidOnly || callerKnows || trustSelf === "known")) continue;
|
|
5047
|
+
if (callerKnows) beef.mergeTxidOnly(sourceTXID);
|
|
5032
5048
|
else await getValidBeef(sourceTXID, beef, trustSelf, knownTxids, trx, requiredLevels);
|
|
5033
5049
|
}
|
|
5034
5050
|
}
|
|
@@ -5092,26 +5108,22 @@ async function notifyTransactionsOfProof(ids, provenTxId, addNote, updateTransac
|
|
|
5092
5108
|
* @param options
|
|
5093
5109
|
*/
|
|
5094
5110
|
async function getBeefForTransaction(storage, txid, options) {
|
|
5095
|
-
|
|
5096
|
-
if (options.mergeToBeef instanceof Beef) beef = options.mergeToBeef;
|
|
5097
|
-
else if (options.mergeToBeef != null) beef = Beef.fromBinary(options.mergeToBeef);
|
|
5098
|
-
else beef = new Beef();
|
|
5111
|
+
const beef = mergeTarget(options);
|
|
5099
5112
|
const hasKnownTxid = makeKnownTxidLookup$1(options.knownTxids ?? []);
|
|
5100
5113
|
const scheduled = /* @__PURE__ */ new Set([txid]);
|
|
5101
5114
|
let frontier = [{
|
|
5102
5115
|
txid,
|
|
5103
5116
|
depth: 0
|
|
5104
5117
|
}];
|
|
5105
|
-
const
|
|
5106
|
-
const concurrency = Number.isFinite(requestedConcurrency) ? Math.max(1, Math.min(32, Math.floor(requestedConcurrency))) : 8;
|
|
5118
|
+
const concurrency = normalizeConcurrency(options.maxConcurrency);
|
|
5107
5119
|
while (frontier.length > 0) {
|
|
5108
|
-
const current = frontier.filter((item) => beef
|
|
5120
|
+
const current = frontier.filter((item) => needsResolution(beef, item.txid, hasKnownTxid));
|
|
5109
5121
|
const resolved = await mapWithConcurrency(current, concurrency, async (item) => await resolveBeefForTransaction(storage, item.txid, options, hasKnownTxid, item.depth));
|
|
5110
5122
|
const next = [];
|
|
5111
5123
|
for (let i = 0; i < resolved.length; i++) {
|
|
5112
5124
|
const result = resolved[i];
|
|
5113
5125
|
beef.mergeBeef(result.beef);
|
|
5114
|
-
for (const dependency of result.dependencies) if (!scheduled.has(dependency) && beef
|
|
5126
|
+
for (const dependency of result.dependencies) if (!scheduled.has(dependency) && needsResolution(beef, dependency, hasKnownTxid)) {
|
|
5115
5127
|
scheduled.add(dependency);
|
|
5116
5128
|
next.push({
|
|
5117
5129
|
txid: dependency,
|
|
@@ -5123,6 +5135,172 @@ async function getBeefForTransaction(storage, txid, options) {
|
|
|
5123
5135
|
}
|
|
5124
5136
|
return beef;
|
|
5125
5137
|
}
|
|
5138
|
+
/**
|
|
5139
|
+
* Build one aggregate BEEF for several roots while resolving each storage
|
|
5140
|
+
* frontier as a set. This avoids one proof query per funding input on the
|
|
5141
|
+
* createAction success path. Complex proof-level and chain-tracker policies
|
|
5142
|
+
* retain the established single-root implementation.
|
|
5143
|
+
*/
|
|
5144
|
+
async function getBeefForTransactions(storage, txids, options) {
|
|
5145
|
+
const beef = mergeTarget(options);
|
|
5146
|
+
const roots = [...new Set(txids)];
|
|
5147
|
+
if (roots.length === 0) return beef;
|
|
5148
|
+
if (requiresSingleRootPolicy(options)) return await mergeSingleRootFragments(storage, roots, options, beef);
|
|
5149
|
+
const hasKnownTxid = makeKnownTxidLookup$1(options.knownTxids ?? []);
|
|
5150
|
+
const scheduled = new Set(roots);
|
|
5151
|
+
let frontier = roots.map((txid) => ({
|
|
5152
|
+
txid,
|
|
5153
|
+
depth: 0
|
|
5154
|
+
}));
|
|
5155
|
+
while (frontier.length > 0) {
|
|
5156
|
+
const unresolved = collectUnresolvedFrontier(storage, frontier, beef, hasKnownTxid);
|
|
5157
|
+
if (unresolved.length === 0) break;
|
|
5158
|
+
const stored = await storage.getProvenOrRawTxs(unresolved.map((item) => item.txid));
|
|
5159
|
+
if (options.trustSelf !== "known" && unresolved.every((item) => stored.get(item.txid)?.proven != null)) {
|
|
5160
|
+
mergeAllProven(storage, beef, unresolved, stored);
|
|
5161
|
+
break;
|
|
5162
|
+
}
|
|
5163
|
+
const [next, missing] = mergeStoredFrontier(beef, unresolved, stored, options, scheduled, hasKnownTxid);
|
|
5164
|
+
await mergeMissingFragments(storage, beef, missing, options);
|
|
5165
|
+
frontier = next;
|
|
5166
|
+
}
|
|
5167
|
+
return beef;
|
|
5168
|
+
}
|
|
5169
|
+
function mergeTarget(options) {
|
|
5170
|
+
if (options.mergeToBeef instanceof Beef) return options.mergeToBeef;
|
|
5171
|
+
if (options.mergeToBeef != null) return Beef.fromBinary(options.mergeToBeef);
|
|
5172
|
+
return new Beef();
|
|
5173
|
+
}
|
|
5174
|
+
function requiresSingleRootPolicy(options) {
|
|
5175
|
+
return options.ignoreStorage === true || options.minProofLevel !== void 0 || options.chainTracker != null || options.skipInvalidProofs === true;
|
|
5176
|
+
}
|
|
5177
|
+
async function mergeSingleRootFragments(storage, roots, options, beef) {
|
|
5178
|
+
const fragments = await mapWithConcurrency(roots.filter((txid) => beef.findTxid(txid) == null), normalizeConcurrency(options.maxConcurrency), async (txid) => await getBeefForTransaction(storage, txid, {
|
|
5179
|
+
...options,
|
|
5180
|
+
mergeToBeef: void 0
|
|
5181
|
+
}));
|
|
5182
|
+
for (const fragment of fragments) beef.mergeBeef(fragment);
|
|
5183
|
+
return beef;
|
|
5184
|
+
}
|
|
5185
|
+
function collectUnresolvedFrontier(storage, frontier, beef, hasKnownTxid) {
|
|
5186
|
+
const unresolved = [];
|
|
5187
|
+
for (const item of frontier) {
|
|
5188
|
+
if (!needsResolution(beef, item.txid, hasKnownTxid)) continue;
|
|
5189
|
+
if (storage.maxRecursionDepth && storage.maxRecursionDepth <= item.depth) throw new WERR_INVALID_OPERATION(`Maximum BEEF depth exceeded. Limit is ${storage.maxRecursionDepth}`);
|
|
5190
|
+
if (hasKnownTxid(item.txid)) beef.mergeTxidOnly(item.txid);
|
|
5191
|
+
else unresolved.push(item);
|
|
5192
|
+
}
|
|
5193
|
+
return unresolved;
|
|
5194
|
+
}
|
|
5195
|
+
function decodeProvenEntries(storage, unresolved, stored) {
|
|
5196
|
+
const span = storage.telemetry.enabled ? storage.telemetry.startSpan("wallet.storage.beef.decode_proven_batch", {
|
|
5197
|
+
component: "wallet-storage",
|
|
5198
|
+
attributes: { "beef.proven_tx_count": unresolved.length }
|
|
5199
|
+
}) : void 0;
|
|
5200
|
+
try {
|
|
5201
|
+
const entries = unresolved.map((item) => {
|
|
5202
|
+
const proven = stored.get(item.txid).proven;
|
|
5203
|
+
return {
|
|
5204
|
+
rawTx: proven.rawTx,
|
|
5205
|
+
merklePath: new EntityProvenTx(proven).getMerklePath(false),
|
|
5206
|
+
merkleRoot: proven.merkleRoot
|
|
5207
|
+
};
|
|
5208
|
+
});
|
|
5209
|
+
span?.end({ attributes: { "beef.decoded_proof_count": entries.length } });
|
|
5210
|
+
return entries;
|
|
5211
|
+
} catch (error) {
|
|
5212
|
+
span?.end({
|
|
5213
|
+
status: "error",
|
|
5214
|
+
error
|
|
5215
|
+
});
|
|
5216
|
+
throw error;
|
|
5217
|
+
}
|
|
5218
|
+
}
|
|
5219
|
+
function mergeAllProven(storage, beef, unresolved, stored) {
|
|
5220
|
+
const entries = decodeProvenEntries(storage, unresolved, stored);
|
|
5221
|
+
const span = storage.telemetry.enabled ? storage.telemetry.startSpan("wallet.storage.beef.merge_proven_batch", {
|
|
5222
|
+
component: "wallet-storage",
|
|
5223
|
+
attributes: { "beef.proven_tx_count": entries.length }
|
|
5224
|
+
}) : void 0;
|
|
5225
|
+
try {
|
|
5226
|
+
mergeProvenEntries(beef, entries, unresolved, stored);
|
|
5227
|
+
span?.end({ attributes: {
|
|
5228
|
+
"beef.merged_tx_count": entries.length,
|
|
5229
|
+
"beef.result_tx_count": beef.txs.length,
|
|
5230
|
+
"beef.result_bump_count": beef.bumps.length
|
|
5231
|
+
} });
|
|
5232
|
+
} catch (error) {
|
|
5233
|
+
span?.end({
|
|
5234
|
+
status: "error",
|
|
5235
|
+
error
|
|
5236
|
+
});
|
|
5237
|
+
throw error;
|
|
5238
|
+
}
|
|
5239
|
+
}
|
|
5240
|
+
function mergeProvenEntries(beef, entries, unresolved, stored) {
|
|
5241
|
+
if (typeof beef.mergeProvenTxs === "function") {
|
|
5242
|
+
beef.mergeProvenTxs(entries);
|
|
5243
|
+
return;
|
|
5244
|
+
}
|
|
5245
|
+
for (const item of unresolved) {
|
|
5246
|
+
const proven = stored.get(item.txid).proven;
|
|
5247
|
+
beef.mergeRawTx(proven.rawTx);
|
|
5248
|
+
beef.mergeBump(new EntityProvenTx(proven).getMerklePath());
|
|
5249
|
+
}
|
|
5250
|
+
}
|
|
5251
|
+
function mergeStoredFrontier(beef, unresolved, stored, options, scheduled, hasKnownTxid) {
|
|
5252
|
+
const next = [];
|
|
5253
|
+
const missing = [];
|
|
5254
|
+
for (const item of unresolved) {
|
|
5255
|
+
const result = stored.get(item.txid);
|
|
5256
|
+
if (result?.proven != null) mergeStoredProven(beef, item, result, options);
|
|
5257
|
+
else if (result?.rawTx != null) mergeStoredRaw(beef, item, result, options, scheduled, next, hasKnownTxid);
|
|
5258
|
+
else missing.push(item);
|
|
5259
|
+
}
|
|
5260
|
+
return [next, missing];
|
|
5261
|
+
}
|
|
5262
|
+
function mergeStoredProven(beef, item, result, options) {
|
|
5263
|
+
if (options.trustSelf === "known") {
|
|
5264
|
+
beef.mergeTxidOnly(item.txid);
|
|
5265
|
+
return;
|
|
5266
|
+
}
|
|
5267
|
+
const proven = result.proven;
|
|
5268
|
+
beef.mergeRawTx(proven.rawTx);
|
|
5269
|
+
beef.mergeBump(new EntityProvenTx(proven).getMerklePath());
|
|
5270
|
+
}
|
|
5271
|
+
function mergeStoredRaw(beef, item, result, options, scheduled, next, hasKnownTxid) {
|
|
5272
|
+
if (options.trustSelf === "known") {
|
|
5273
|
+
beef.mergeTxidOnly(item.txid);
|
|
5274
|
+
return;
|
|
5275
|
+
}
|
|
5276
|
+
const transaction = beef.mergeRawTx(result.rawTx);
|
|
5277
|
+
if (result.inputBEEF != null) beef.mergeBeef(result.inputBEEF);
|
|
5278
|
+
appendNewDependencies(transaction.inputTxids, item.depth + 1, beef, scheduled, next, hasKnownTxid);
|
|
5279
|
+
}
|
|
5280
|
+
function appendNewDependencies(dependencies, depth, beef, scheduled, next, hasKnownTxid) {
|
|
5281
|
+
for (const txid of dependencies) {
|
|
5282
|
+
if (scheduled.has(txid) || !needsResolution(beef, txid, hasKnownTxid)) continue;
|
|
5283
|
+
scheduled.add(txid);
|
|
5284
|
+
next.push({
|
|
5285
|
+
txid,
|
|
5286
|
+
depth
|
|
5287
|
+
});
|
|
5288
|
+
}
|
|
5289
|
+
}
|
|
5290
|
+
function needsResolution(beef, txid, hasKnownTxid) {
|
|
5291
|
+
const entry = beef.findTxid(txid);
|
|
5292
|
+
return entry == null || entry.isTxidOnly && !hasKnownTxid(txid);
|
|
5293
|
+
}
|
|
5294
|
+
async function mergeMissingFragments(storage, beef, missing, options) {
|
|
5295
|
+
if (missing.length === 0) return;
|
|
5296
|
+
if (options.ignoreServices === true) throw new WERR_INVALID_PARAMETER(`txid ${missing[0].txid}`, `valid transaction on chain ${storage.chain}`);
|
|
5297
|
+
const fragments = await mapWithConcurrency(missing, normalizeConcurrency(options.maxConcurrency), async (item) => await getBeefForTransaction(storage, item.txid, {
|
|
5298
|
+
...options,
|
|
5299
|
+
ignoreStorage: true,
|
|
5300
|
+
mergeToBeef: void 0
|
|
5301
|
+
}));
|
|
5302
|
+
for (const fragment of fragments) beef.mergeBeef(fragment);
|
|
5303
|
+
}
|
|
5126
5304
|
function makeKnownTxidLookup$1(knownTxids) {
|
|
5127
5305
|
let lookups = 0;
|
|
5128
5306
|
let indexed;
|
|
@@ -5136,6 +5314,9 @@ function makeKnownTxidLookup$1(knownTxids) {
|
|
|
5136
5314
|
return knownTxids.includes(txid);
|
|
5137
5315
|
};
|
|
5138
5316
|
}
|
|
5317
|
+
function normalizeConcurrency(value = 8) {
|
|
5318
|
+
return Number.isFinite(value) ? Math.max(1, Math.min(32, Math.floor(value))) : 8;
|
|
5319
|
+
}
|
|
5139
5320
|
async function mapWithConcurrency(values, concurrency, mapper) {
|
|
5140
5321
|
const results = Array.from({ length: values.length }, () => void 0);
|
|
5141
5322
|
let cursor = 0;
|
|
@@ -5264,6 +5445,23 @@ async function createMergedBeefOfTxids(txids, storage) {
|
|
|
5264
5445
|
//#endregion
|
|
5265
5446
|
//#region ../src/storage/methods/processAction.ts
|
|
5266
5447
|
async function processAction$1(storage, auth, args) {
|
|
5448
|
+
if (!storage.telemetry.enabled) return await processActionCore(storage, auth, args);
|
|
5449
|
+
return await storage.telemetry.withSpan("wallet.storage.process_action", {
|
|
5450
|
+
component: "wallet-storage",
|
|
5451
|
+
carrier: args,
|
|
5452
|
+
attributes: {
|
|
5453
|
+
"action.is_new_transaction": args.isNewTx,
|
|
5454
|
+
"action.is_no_send": args.isNoSend,
|
|
5455
|
+
"action.is_delayed": args.isDelayed,
|
|
5456
|
+
"action.send_with_count": args.sendWith.length
|
|
5457
|
+
}
|
|
5458
|
+
}, async (span) => {
|
|
5459
|
+
const result = await processActionCore(storage, auth, args, span);
|
|
5460
|
+
span.end({ attributes: { "action.send_result_count": result.sendWithResults?.length ?? 0 } });
|
|
5461
|
+
return result;
|
|
5462
|
+
});
|
|
5463
|
+
}
|
|
5464
|
+
async function processActionCore(storage, auth, args, parent) {
|
|
5267
5465
|
const logger = args.logger;
|
|
5268
5466
|
logger?.group("storage processAction");
|
|
5269
5467
|
const userId = verifyId(auth.userId);
|
|
@@ -5271,9 +5469,9 @@ async function processAction$1(storage, auth, args) {
|
|
|
5271
5469
|
let req;
|
|
5272
5470
|
const txidsOfReqsToShareWithWorld = [...args.sendWith];
|
|
5273
5471
|
if (args.isNewTx) {
|
|
5274
|
-
const vargs = await validateCommitNewTxToStorageArgs(storage, userId, args);
|
|
5472
|
+
const vargs = await traceProcessStep(storage, "wallet.storage.process_action.validate", parent, async () => await validateCommitNewTxToStorageArgs(storage, userId, args));
|
|
5275
5473
|
logger?.log("validated new tx updates to storage");
|
|
5276
|
-
({req} = await commitNewTxToStorage(storage, userId, vargs));
|
|
5474
|
+
({req} = await traceProcessStep(storage, "wallet.storage.process_action.commit", parent, async () => await commitNewTxToStorage(storage, userId, vargs)));
|
|
5277
5475
|
logger?.log("committed new tx updates to storage ");
|
|
5278
5476
|
if (!req) throw new WERR_INTERNAL();
|
|
5279
5477
|
if (args.isNoSend && !args.isSendWith) logger?.log(`noSend txid ${req.txid}`);
|
|
@@ -5282,12 +5480,19 @@ async function processAction$1(storage, auth, args) {
|
|
|
5282
5480
|
logger?.log(`sending txid ${req.txid}`);
|
|
5283
5481
|
}
|
|
5284
5482
|
}
|
|
5285
|
-
const { swr, ndr } = await shareReqsWithWorld(storage, userId, txidsOfReqsToShareWithWorld, args.isDelayed, void 0, logger);
|
|
5483
|
+
const { swr, ndr } = await traceProcessStep(storage, "wallet.storage.process_action.share", parent, async () => await shareReqsWithWorld(storage, userId, txidsOfReqsToShareWithWorld, args.isDelayed, void 0, logger));
|
|
5286
5484
|
r.sendWithResults = swr;
|
|
5287
5485
|
r.notDelayedResults = ndr;
|
|
5288
5486
|
logger?.groupEnd();
|
|
5289
5487
|
return r;
|
|
5290
5488
|
}
|
|
5489
|
+
async function traceProcessStep(storage, name, parent, callback) {
|
|
5490
|
+
if (parent == null) return await callback();
|
|
5491
|
+
return await storage.telemetry.withSpan(name, {
|
|
5492
|
+
component: "wallet-storage",
|
|
5493
|
+
parent: parent.context
|
|
5494
|
+
}, callback);
|
|
5495
|
+
}
|
|
5291
5496
|
/**
|
|
5292
5497
|
* Verifies that all the txids are known reqs with ready-to-share status.
|
|
5293
5498
|
* Assigns a batch identifier and updates all the provenTxReqs.
|
|
@@ -5458,21 +5663,16 @@ async function validateCommitNewTxToStorageArgs(storage, userId, params) {
|
|
|
5458
5663
|
} }));
|
|
5459
5664
|
if (!transaction.isOutgoing) throw new WERR_INVALID_OPERATION("isOutgoing is not true");
|
|
5460
5665
|
if (transaction.inputBEEF == null) throw new WERR_INVALID_OPERATION();
|
|
5461
|
-
const beef = Beef.fromBinary(asArray(transaction.inputBEEF));
|
|
5462
5666
|
if (transaction.status !== "unsigned" && transaction.status !== "unprocessed") throw new WERR_INVALID_OPERATION(`invalid transaction status ${transaction.status}`);
|
|
5463
5667
|
const transactionId = verifyId(transaction.transactionId);
|
|
5464
|
-
const outputOutputs = await storage.findOutputs({ partial: {
|
|
5668
|
+
const [outputOutputs, commissionRows] = await Promise.all([storage.findOutputs({ partial: {
|
|
5465
5669
|
userId,
|
|
5466
5670
|
transactionId
|
|
5467
|
-
} })
|
|
5468
|
-
const inputOutputs = await storage.findOutputs({ partial: {
|
|
5469
|
-
userId,
|
|
5470
|
-
spentBy: transactionId
|
|
5471
|
-
} });
|
|
5472
|
-
const commission = verifyOneOrNone(await storage.findCommissions({ partial: {
|
|
5671
|
+
} }), storage.commissionSatoshis > 0 ? storage.findCommissions({ partial: {
|
|
5473
5672
|
transactionId,
|
|
5474
5673
|
userId
|
|
5475
|
-
} }));
|
|
5674
|
+
} }) : Promise.resolve([])]);
|
|
5675
|
+
const commission = verifyOneOrNone(commissionRows);
|
|
5476
5676
|
if (storage.commissionSatoshis > 0) {
|
|
5477
5677
|
if (commission == null) throw new WERR_INTERNAL();
|
|
5478
5678
|
if (!tx.outputs.some((x) => x.satoshis === commission.satoshis && x.lockingScript.toHex() === asString(commission.lockingScript))) throw new WERR_INVALID_OPERATION("Transaction did not include an output to cover service fee.");
|
|
@@ -5492,10 +5692,7 @@ async function validateCommitNewTxToStorageArgs(storage, userId, params) {
|
|
|
5492
5692
|
txScriptOffsets,
|
|
5493
5693
|
transactionId,
|
|
5494
5694
|
transaction,
|
|
5495
|
-
inputOutputs,
|
|
5496
5695
|
outputOutputs,
|
|
5497
|
-
commission,
|
|
5498
|
-
beef,
|
|
5499
5696
|
req,
|
|
5500
5697
|
outputUpdates: [],
|
|
5501
5698
|
transactionUpdate: {
|
|
@@ -6173,17 +6370,24 @@ async function generateChangeSdkCore(params, allocateChangeInput, releaseChangeI
|
|
|
6173
6370
|
};
|
|
6174
6371
|
const fixedInputs = params.fixedInputs;
|
|
6175
6372
|
const fixedOutputs = params.fixedOutputs;
|
|
6373
|
+
const fixedFunding = fixedInputs.reduce((sum, input) => sum + input.satoshis, 0);
|
|
6374
|
+
let fixedSpending = fixedOutputs.reduce((sum, output) => sum + output.satoshis, 0);
|
|
6375
|
+
const fixedInputSize = fixedInputs.reduce((sum, input) => sum + transactionInputSize(input.unlockingScriptLength), 0);
|
|
6376
|
+
const fixedOutputSize = fixedOutputs.reduce((sum, output) => sum + transactionOutputSize(output.lockingScriptLength), 0);
|
|
6377
|
+
const changeInputSize = transactionInputSize(params.changeUnlockingScriptLength);
|
|
6378
|
+
const changeOutputSize = transactionOutputSize(params.changeLockingScriptLength);
|
|
6379
|
+
let allocatedFunding = 0;
|
|
6176
6380
|
/**
|
|
6177
6381
|
* @returns sum of transaction fixedInputs satoshis and fundingInputs satoshis
|
|
6178
6382
|
*/
|
|
6179
6383
|
const funding = () => {
|
|
6180
|
-
return
|
|
6384
|
+
return fixedFunding + allocatedFunding;
|
|
6181
6385
|
};
|
|
6182
6386
|
/**
|
|
6183
6387
|
* @returns sum of transaction fixedOutputs satoshis
|
|
6184
6388
|
*/
|
|
6185
6389
|
const spending = () => {
|
|
6186
|
-
return
|
|
6390
|
+
return fixedSpending;
|
|
6187
6391
|
};
|
|
6188
6392
|
/**
|
|
6189
6393
|
* @returns sum of transaction changeOutputs satoshis
|
|
@@ -6193,7 +6397,9 @@ async function generateChangeSdkCore(params, allocateChangeInput, releaseChangeI
|
|
|
6193
6397
|
};
|
|
6194
6398
|
const fee = () => funding() - spending() - change();
|
|
6195
6399
|
const size = (addedChangeInputs, addedChangeOutputs) => {
|
|
6196
|
-
|
|
6400
|
+
const inputCount = fixedInputs.length + r.allocatedChangeInputs.length + (addedChangeInputs || 0);
|
|
6401
|
+
const outputCount = fixedOutputs.length + r.changeOutputs.length + (addedChangeOutputs || 0);
|
|
6402
|
+
return 4 + varUintSize(inputCount) + fixedInputSize + (r.allocatedChangeInputs.length + (addedChangeInputs || 0)) * changeInputSize + varUintSize(outputCount) + fixedOutputSize + (r.changeOutputs.length + (addedChangeOutputs || 0)) * changeOutputSize + 4;
|
|
6197
6403
|
};
|
|
6198
6404
|
/**
|
|
6199
6405
|
* @returns the target fee required for the transaction as currently configured under feeModel.
|
|
@@ -6232,7 +6438,10 @@ async function generateChangeSdkCore(params, allocateChangeInput, releaseChangeI
|
|
|
6232
6438
|
const releaseAllocatedChangeInputs = async () => {
|
|
6233
6439
|
while (r.allocatedChangeInputs.length > 0) {
|
|
6234
6440
|
const i = r.allocatedChangeInputs.pop();
|
|
6235
|
-
if (i != null)
|
|
6441
|
+
if (i != null) {
|
|
6442
|
+
allocatedFunding -= i.satoshis;
|
|
6443
|
+
await releaseChangeInput(i.outputId);
|
|
6444
|
+
}
|
|
6236
6445
|
}
|
|
6237
6446
|
feeExcessNow = feeExcess();
|
|
6238
6447
|
};
|
|
@@ -6267,6 +6476,7 @@ async function generateChangeSdkCore(params, allocateChangeInput, releaseChangeI
|
|
|
6267
6476
|
const allocatedChangeInput = await allocateChangeInput(-feeExcess(1, ao) + (ao === 1 ? 2 * params.changeInitialSatoshis : 0) + changeBuffer, exactSatoshis);
|
|
6268
6477
|
if (allocatedChangeInput == null) return false;
|
|
6269
6478
|
r.allocatedChangeInputs.push(allocatedChangeInput);
|
|
6479
|
+
allocatedFunding += allocatedChangeInput.satoshis;
|
|
6270
6480
|
maybeAddChangeOutput(ao);
|
|
6271
6481
|
return true;
|
|
6272
6482
|
};
|
|
@@ -6278,6 +6488,7 @@ async function generateChangeSdkCore(params, allocateChangeInput, releaseChangeI
|
|
|
6278
6488
|
while (r.changeOutputs.length > 0 && feeExcess() < 0) r.changeOutputs.pop();
|
|
6279
6489
|
if (feeExcess() < 0) break;
|
|
6280
6490
|
removeChurnPairs(r.allocatedChangeInputs, r.changeOutputs);
|
|
6491
|
+
allocatedFunding = r.allocatedChangeInputs.reduce((sum, input) => sum + input.satoshis, 0);
|
|
6281
6492
|
}
|
|
6282
6493
|
};
|
|
6283
6494
|
/**
|
|
@@ -6286,7 +6497,9 @@ async function generateChangeSdkCore(params, allocateChangeInput, releaseChangeI
|
|
|
6286
6497
|
await fundTransaction();
|
|
6287
6498
|
if (feeExcess() < 0 && vgcpr.hasMaxPossibleOutput !== void 0) {
|
|
6288
6499
|
if (fixedOutputs[vgcpr.hasMaxPossibleOutput].satoshis !== 0x775f05a073fff) throw new WERR_INTERNAL();
|
|
6289
|
-
|
|
6500
|
+
const adjustment = feeExcess();
|
|
6501
|
+
fixedOutputs[vgcpr.hasMaxPossibleOutput].satoshis += adjustment;
|
|
6502
|
+
fixedSpending += adjustment;
|
|
6290
6503
|
r.maxPossibleSatoshisAdjustment = {
|
|
6291
6504
|
fixedOutputIndex: vgcpr.hasMaxPossibleOutput,
|
|
6292
6505
|
satoshis: fixedOutputs[vgcpr.hasMaxPossibleOutput].satoshis
|
|
@@ -6305,8 +6518,11 @@ async function generateChangeSdkCore(params, allocateChangeInput, releaseChangeI
|
|
|
6305
6518
|
* If needed, seek funding to avoid overspending on fees without a change output to recapture it.
|
|
6306
6519
|
*/
|
|
6307
6520
|
if (r.changeOutputs.length === 0 && feeExcessNow > 0) {
|
|
6521
|
+
const minimumChange = Math.max(dustFloor, params.changeFirstSatoshis);
|
|
6522
|
+
const totalSatoshisNeeded = spending() + feeTarget(0, 1) + minimumChange;
|
|
6523
|
+
const moreSatoshisNeeded = Math.max(1, totalSatoshisNeeded - funding());
|
|
6308
6524
|
await releaseAllocatedChangeInputs();
|
|
6309
|
-
throw new WERR_INSUFFICIENT_FUNDS(
|
|
6525
|
+
throw new WERR_INSUFFICIENT_FUNDS(totalSatoshisNeeded, moreSatoshisNeeded);
|
|
6310
6526
|
}
|
|
6311
6527
|
/**
|
|
6312
6528
|
* Distribute the excess fees across the changeOutputs added.
|
|
@@ -6630,6 +6846,8 @@ function makeChangeLock(out, dctr, args, changeKeys, wallet) {
|
|
|
6630
6846
|
}
|
|
6631
6847
|
//#endregion
|
|
6632
6848
|
//#region ../src/signer/methods/verifyUnlockScripts.ts
|
|
6849
|
+
const postChronicleHeightFallback = 943816;
|
|
6850
|
+
const canonicalP2PKHScope = TransactionSignature.SIGHASH_ALL + TransactionSignature.SIGHASH_FORKID;
|
|
6633
6851
|
const javaScriptOnlyVerifier = {
|
|
6634
6852
|
shouldVerifySpend: () => false,
|
|
6635
6853
|
verifySpend: async () => {
|
|
@@ -6641,10 +6859,11 @@ function invalidUnlockingScript(inputIndex, detail) {
|
|
|
6641
6859
|
return new WERR_INVALID_PARAMETER(`inputs[${inputIndex}].unlockScript`, `valid.${suffix}`);
|
|
6642
6860
|
}
|
|
6643
6861
|
async function verifyOneSpend(pending, verifier) {
|
|
6862
|
+
const [inputIndex, , spend, context] = pending;
|
|
6644
6863
|
try {
|
|
6645
|
-
if (!(verifier === void 0 ?
|
|
6864
|
+
if (!(verifier === void 0 ? spend.validate(context) : await spend.validateWith(verifier, context))) throw invalidUnlockingScript(inputIndex);
|
|
6646
6865
|
} catch (error) {
|
|
6647
|
-
if (error instanceof ScriptEvaluationError) throw invalidUnlockingScript(
|
|
6866
|
+
if (error instanceof ScriptEvaluationError) throw invalidUnlockingScript(inputIndex, error.message);
|
|
6648
6867
|
throw error;
|
|
6649
6868
|
}
|
|
6650
6869
|
}
|
|
@@ -6654,33 +6873,157 @@ async function verifyPendingSpends(pending, verifier) {
|
|
|
6654
6873
|
return;
|
|
6655
6874
|
}
|
|
6656
6875
|
const batched = [];
|
|
6657
|
-
for (const item of pending) if (verifier.shouldVerifySpend?.(item
|
|
6876
|
+
for (const item of pending) if (verifier.shouldVerifySpend?.(item[2], item[3]) !== false) batched.push(item);
|
|
6658
6877
|
else await verifyOneSpend(item, javaScriptOnlyVerifier);
|
|
6659
6878
|
if (batched.length === 0) return;
|
|
6660
6879
|
let verdicts;
|
|
6661
6880
|
try {
|
|
6662
6881
|
verdicts = await verifier.verifySpendsBatch(batched.map((item) => ({
|
|
6663
|
-
spend: item
|
|
6664
|
-
...item
|
|
6882
|
+
spend: item[2],
|
|
6883
|
+
...item[3]
|
|
6665
6884
|
})));
|
|
6666
6885
|
} catch (error) {
|
|
6667
|
-
if (error instanceof ScriptEvaluationError) throw invalidUnlockingScript(batched[0]
|
|
6886
|
+
if (error instanceof ScriptEvaluationError) throw invalidUnlockingScript(batched[0][0], error.message);
|
|
6668
6887
|
throw error;
|
|
6669
6888
|
}
|
|
6670
6889
|
if (verdicts.length !== batched.length) throw new Error("Script verifier returned an invalid batch result count");
|
|
6671
6890
|
verdicts.forEach((valid, index) => {
|
|
6672
|
-
if (!valid) throw invalidUnlockingScript(batched[index]
|
|
6891
|
+
if (!valid) throw invalidUnlockingScript(batched[index][0]);
|
|
6673
6892
|
});
|
|
6674
6893
|
}
|
|
6675
|
-
function
|
|
6676
|
-
const
|
|
6894
|
+
function wholeTransactionVerifier(verifier) {
|
|
6895
|
+
const candidate = verifier;
|
|
6896
|
+
return typeof candidate?.verifyScripts === "function" ? candidate : void 0;
|
|
6897
|
+
}
|
|
6898
|
+
function digestBatchVerifier(verifier) {
|
|
6899
|
+
const candidate = verifier;
|
|
6900
|
+
if (typeof candidate?.verifyDigestBatch !== "function") return void 0;
|
|
6901
|
+
if (candidate.isReady?.() === false) return void 0;
|
|
6902
|
+
if (candidate.supportsCrypto?.("verifyDigestBatch") === false) return void 0;
|
|
6903
|
+
return candidate;
|
|
6904
|
+
}
|
|
6905
|
+
function equalBytes(left, right) {
|
|
6906
|
+
if (left.length !== right.length) return false;
|
|
6907
|
+
for (let index = 0; index < left.length; index++) if (left[index] !== right[index]) return false;
|
|
6908
|
+
return true;
|
|
6909
|
+
}
|
|
6910
|
+
function isCanonicalP2PKHLock(lock) {
|
|
6911
|
+
return lock.length === 25 && lock[0] === 118 && lock[1] === 169 && lock[2] === 20 && lock[23] === 136 && lock[24] === 172;
|
|
6912
|
+
}
|
|
6913
|
+
function parseCanonicalP2PKHUnlock(unlock, lock) {
|
|
6914
|
+
const signatureLength = unlock[0];
|
|
6915
|
+
if (signatureLength == null || signatureLength < 9 || signatureLength > 73 || unlock.length !== 1 + signatureLength + 1 + 33 || unlock[1 + signatureLength] !== 33) return void 0;
|
|
6916
|
+
const checksig = Array.from(unlock.subarray(1, 1 + signatureLength));
|
|
6917
|
+
const publicKey = unlock.subarray(1 + signatureLength + 1);
|
|
6918
|
+
if (publicKey[0] !== 2 && publicKey[0] !== 3 || !equalBytes(Hash.hash160(publicKey), lock.subarray(3, 23))) return void 0;
|
|
6919
|
+
let signature;
|
|
6920
|
+
try {
|
|
6921
|
+
signature = TransactionSignature.fromChecksigFormat(checksig);
|
|
6922
|
+
} catch {
|
|
6923
|
+
return;
|
|
6924
|
+
}
|
|
6925
|
+
if (signature.scope !== canonicalP2PKHScope || !signature.hasLowS() || !equalBytes(signature.toChecksigFormat(), checksig)) return void 0;
|
|
6926
|
+
return [
|
|
6927
|
+
checksig,
|
|
6928
|
+
publicKey,
|
|
6929
|
+
signature
|
|
6930
|
+
];
|
|
6931
|
+
}
|
|
6932
|
+
/**
|
|
6933
|
+
* Recognizes only the exact canonical P2PKH shape generated by this wallet.
|
|
6934
|
+
* Anything else retains the general-purpose script interpreter/backend path.
|
|
6935
|
+
*/
|
|
6936
|
+
function standardP2PKHDigests(tx) {
|
|
6937
|
+
const cache = { hashOutputsSingle: /* @__PURE__ */ new Map() };
|
|
6938
|
+
const items = [];
|
|
6939
|
+
for (let inputIndex = 0; inputIndex < tx.inputs.length; inputIndex++) {
|
|
6940
|
+
const input = tx.inputs[inputIndex];
|
|
6941
|
+
const sourceTransaction = input.sourceTransaction;
|
|
6942
|
+
const sourceTXID = input.sourceTXID;
|
|
6943
|
+
const unlockingScript = input.unlockingScript;
|
|
6944
|
+
if (sourceTransaction == null || sourceTXID == null || unlockingScript == null) return void 0;
|
|
6945
|
+
const sourceOutput = sourceTransaction.outputs[input.sourceOutputIndex];
|
|
6946
|
+
if (sourceOutput == null) return void 0;
|
|
6947
|
+
const lock = sourceOutput.lockingScript.toUint8Array();
|
|
6948
|
+
if (!isCanonicalP2PKHLock(lock)) return void 0;
|
|
6949
|
+
const parsed = parseCanonicalP2PKHUnlock(unlockingScript.toUint8Array(), lock);
|
|
6950
|
+
if (parsed == null) return void 0;
|
|
6951
|
+
const [checksig, publicKey, signature] = parsed;
|
|
6952
|
+
const preimage = TransactionSignature.formatBytes({
|
|
6953
|
+
sourceTXID,
|
|
6954
|
+
sourceOutputIndex: input.sourceOutputIndex,
|
|
6955
|
+
sourceSatoshis: sourceOutput.satoshis ?? 0,
|
|
6956
|
+
transactionVersion: tx.version,
|
|
6957
|
+
otherInputs: [],
|
|
6958
|
+
allInputs: tx.inputs,
|
|
6959
|
+
outputs: tx.outputs,
|
|
6960
|
+
inputIndex,
|
|
6961
|
+
subscript: sourceOutput.lockingScript,
|
|
6962
|
+
inputSequence: input.sequence ?? 4294967295,
|
|
6963
|
+
lockTime: tx.lockTime,
|
|
6964
|
+
scope: signature.scope,
|
|
6965
|
+
cache
|
|
6966
|
+
});
|
|
6967
|
+
items.push({
|
|
6968
|
+
publicKey,
|
|
6969
|
+
digest: Uint8Array.from(Hash.hash256(preimage)),
|
|
6970
|
+
signature: Uint8Array.from(checksig.slice(0, -1))
|
|
6971
|
+
});
|
|
6972
|
+
}
|
|
6973
|
+
return items;
|
|
6974
|
+
}
|
|
6975
|
+
async function verifyStandardP2PKHDigests(pending, verifier) {
|
|
6976
|
+
if (pending.length === 0) return /* @__PURE__ */ new Set();
|
|
6977
|
+
const items = pending.flatMap((entry) => entry[1]);
|
|
6978
|
+
const verdicts = await verifier.verifyDigestBatch(items);
|
|
6979
|
+
if (verdicts.length !== items.length) throw new Error("Script verifier returned an invalid digest batch result count");
|
|
6980
|
+
const verified = /* @__PURE__ */ new Set();
|
|
6981
|
+
let offset = 0;
|
|
6982
|
+
for (const entry of pending) {
|
|
6983
|
+
const end = offset + entry[1].length;
|
|
6984
|
+
if (verdicts.slice(offset, end).every(Boolean)) verified.add(entry[0]);
|
|
6985
|
+
offset = end;
|
|
6986
|
+
}
|
|
6987
|
+
return verified;
|
|
6988
|
+
}
|
|
6989
|
+
function hydrateTransactionSources(txid, transactions) {
|
|
6990
|
+
const tx = transactions.get(txid);
|
|
6991
|
+
if (tx == null) throw new WERR_INVALID_PARAMETER("txid", `contained in beef, txid ${txid}`);
|
|
6992
|
+
for (let inputIndex = 0; inputIndex < tx.inputs.length; inputIndex++) {
|
|
6993
|
+
const input = tx.inputs[inputIndex];
|
|
6994
|
+
if (input.sourceTXID == null) throw new WERR_INVALID_PARAMETER(`inputs[${inputIndex}].sourceTXID`, "valid");
|
|
6995
|
+
if (input.unlockingScript == null) throw new WERR_INVALID_PARAMETER(`inputs[${inputIndex}].unlockingScript`, "valid");
|
|
6996
|
+
input.sourceTransaction = transactions.get(input.sourceTXID);
|
|
6997
|
+
if (input.sourceTransaction == null) return void 0;
|
|
6998
|
+
if (input.sourceTransaction.outputs[input.sourceOutputIndex] == null) throw new WERR_INVALID_PARAMETER(`inputs[${inputIndex}].sourceOutputIndex`, "reference an output in the source transaction");
|
|
6999
|
+
}
|
|
7000
|
+
return tx;
|
|
7001
|
+
}
|
|
7002
|
+
function transactionIndex(txids, beef) {
|
|
7003
|
+
if (txids.length > 0) beef.findTxid(txids[0]);
|
|
7004
|
+
return new Map(beef.txs.map((item) => [item.txid, item.tx]));
|
|
7005
|
+
}
|
|
7006
|
+
async function verifyWholeTransactions(pending, verifier) {
|
|
7007
|
+
if (pending.length === 0) return /* @__PURE__ */ new Set();
|
|
7008
|
+
let verdicts;
|
|
7009
|
+
try {
|
|
7010
|
+
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]));
|
|
7011
|
+
} catch (error) {
|
|
7012
|
+
if (error instanceof ScriptEvaluationError) return /* @__PURE__ */ new Set();
|
|
7013
|
+
throw error;
|
|
7014
|
+
}
|
|
7015
|
+
if (verdicts.length !== pending.length) throw new Error("Script verifier returned an invalid transaction batch result count");
|
|
7016
|
+
return new Set(pending.filter((_, index) => verdicts[index]).map((item) => item[0]));
|
|
7017
|
+
}
|
|
7018
|
+
function collectTransactionSpends(txid, resultIndex, transactions, result, pending) {
|
|
7019
|
+
const tx = transactions.get(txid);
|
|
6677
7020
|
if (tx == null) throw new WERR_INVALID_PARAMETER("txid", `contained in beef, txid ${txid}`);
|
|
6678
7021
|
const sigHashCache = { hashOutputsSingle: /* @__PURE__ */ new Map() };
|
|
6679
7022
|
for (let inputIndex = 0; inputIndex < tx.inputs.length; inputIndex++) {
|
|
6680
7023
|
const input = tx.inputs[inputIndex];
|
|
6681
7024
|
if (input.sourceTXID == null) throw new WERR_INVALID_PARAMETER(`inputs[${inputIndex}].sourceTXID`, "valid");
|
|
6682
7025
|
if (input.unlockingScript == null) throw new WERR_INVALID_PARAMETER(`inputs[${inputIndex}].unlockingScript`, "valid");
|
|
6683
|
-
input.sourceTransaction =
|
|
7026
|
+
input.sourceTransaction = transactions.get(input.sourceTXID);
|
|
6684
7027
|
if (input.sourceTransaction == null) {
|
|
6685
7028
|
result.skippedInputs++;
|
|
6686
7029
|
continue;
|
|
@@ -6692,11 +7035,10 @@ function collectTransactionSpends(txid, resultIndex, beef, result, pending) {
|
|
|
6692
7035
|
consensus: true,
|
|
6693
7036
|
utxoHeight
|
|
6694
7037
|
};
|
|
6695
|
-
pending.push(
|
|
7038
|
+
pending.push([
|
|
6696
7039
|
inputIndex,
|
|
6697
7040
|
resultIndex,
|
|
6698
|
-
|
|
6699
|
-
spend: new Spend({
|
|
7041
|
+
new Spend({
|
|
6700
7042
|
sourceTXID: input.sourceTXID,
|
|
6701
7043
|
sourceOutputIndex: input.sourceOutputIndex,
|
|
6702
7044
|
lockingScript: sourceOutput.lockingScript,
|
|
@@ -6710,9 +7052,52 @@ function collectTransactionSpends(txid, resultIndex, beef, result, pending) {
|
|
|
6710
7052
|
outputs: tx.outputs,
|
|
6711
7053
|
lockTime: tx.lockTime,
|
|
6712
7054
|
sigHashCache
|
|
6713
|
-
})
|
|
6714
|
-
|
|
7055
|
+
}),
|
|
7056
|
+
context
|
|
7057
|
+
]);
|
|
7058
|
+
}
|
|
7059
|
+
}
|
|
7060
|
+
function collectAcceleratedTransactions(txids, transactions, digestVerifier, enabled) {
|
|
7061
|
+
const hydrated = /* @__PURE__ */ new Map();
|
|
7062
|
+
const digests = [];
|
|
7063
|
+
if (!enabled) return [hydrated, digests];
|
|
7064
|
+
for (let resultIndex = 0; resultIndex < txids.length; resultIndex++) {
|
|
7065
|
+
const tx = hydrateTransactionSources(txids[resultIndex], transactions);
|
|
7066
|
+
if (tx == null) continue;
|
|
7067
|
+
hydrated.set(resultIndex, tx);
|
|
7068
|
+
if (digestVerifier === void 0) continue;
|
|
7069
|
+
const items = standardP2PKHDigests(tx);
|
|
7070
|
+
if (items != null) digests.push([resultIndex, items]);
|
|
6715
7071
|
}
|
|
7072
|
+
return [hydrated, digests];
|
|
7073
|
+
}
|
|
7074
|
+
function collectWholeTransactionVerifications(hydrated, digestAttempted, verifier) {
|
|
7075
|
+
if (verifier === void 0) return [];
|
|
7076
|
+
const pending = [];
|
|
7077
|
+
for (const [resultIndex, tx] of hydrated) {
|
|
7078
|
+
if (digestAttempted.has(resultIndex)) continue;
|
|
7079
|
+
const params = {
|
|
7080
|
+
tx,
|
|
7081
|
+
blockHeight: postChronicleHeightFallback,
|
|
7082
|
+
consensus: true
|
|
7083
|
+
};
|
|
7084
|
+
if (verifier.shouldVerifyScripts?.(params) === false) continue;
|
|
7085
|
+
pending.push([resultIndex, params]);
|
|
7086
|
+
}
|
|
7087
|
+
return pending;
|
|
7088
|
+
}
|
|
7089
|
+
function collectFallbackSpends(txids, transactions, accelerated, results) {
|
|
7090
|
+
const pending = [];
|
|
7091
|
+
for (let resultIndex = 0; resultIndex < txids.length; resultIndex++) {
|
|
7092
|
+
if (!accelerated.has(resultIndex)) {
|
|
7093
|
+
collectTransactionSpends(txids[resultIndex], resultIndex, transactions, results[resultIndex], pending);
|
|
7094
|
+
continue;
|
|
7095
|
+
}
|
|
7096
|
+
const tx = transactions.get(txids[resultIndex]);
|
|
7097
|
+
if (tx == null) throw new WERR_INVALID_PARAMETER("txid", `contained in beef, txid ${txids[resultIndex]}`);
|
|
7098
|
+
results[resultIndex].verifiedInputs = tx.inputs.length;
|
|
7099
|
+
}
|
|
7100
|
+
return pending;
|
|
6716
7101
|
}
|
|
6717
7102
|
/**
|
|
6718
7103
|
* Verifies every resolvable input from several transactions in one optional
|
|
@@ -6723,10 +7108,17 @@ async function verifyUnlockScriptsBatch(txids, beef, verifier) {
|
|
|
6723
7108
|
verifiedInputs: 0,
|
|
6724
7109
|
skippedInputs: 0
|
|
6725
7110
|
}));
|
|
6726
|
-
const
|
|
6727
|
-
|
|
7111
|
+
const transactions = transactionIndex(txids, beef);
|
|
7112
|
+
const digestVerifier = digestBatchVerifier(verifier);
|
|
7113
|
+
const wholeVerifier = wholeTransactionVerifier(verifier);
|
|
7114
|
+
const [hydrated, digestPending] = collectAcceleratedTransactions(txids, transactions, digestVerifier, digestVerifier !== void 0 || wholeVerifier !== void 0);
|
|
7115
|
+
const digestAttempted = new Set(digestPending.map((item) => item[0]));
|
|
7116
|
+
const digestVerified = digestVerifier === void 0 ? /* @__PURE__ */ new Set() : await verifyStandardP2PKHDigests(digestPending, digestVerifier);
|
|
7117
|
+
const wholePending = collectWholeTransactionVerifications(hydrated, digestAttempted, wholeVerifier);
|
|
7118
|
+
const wholeVerified = wholeVerifier === void 0 ? /* @__PURE__ */ new Set() : await verifyWholeTransactions(wholePending, wholeVerifier);
|
|
7119
|
+
const pending = collectFallbackSpends(txids, transactions, /* @__PURE__ */ new Set([...digestVerified, ...wholeVerified]), results);
|
|
6728
7120
|
await verifyPendingSpends(pending, verifier);
|
|
6729
|
-
for (const item of pending) results[item
|
|
7121
|
+
for (const item of pending) results[item[1]].verifiedInputs++;
|
|
6730
7122
|
return results;
|
|
6731
7123
|
}
|
|
6732
7124
|
/**
|
|
@@ -6749,21 +7141,56 @@ async function completeSignedTransaction(prior, spends, wallet) {
|
|
|
6749
7141
|
input.unlockingScript = asBsvSdkScript(spend.unlockingScript);
|
|
6750
7142
|
if (spend.sequenceNumber !== void 0) input.sequence = spend.sequenceNumber;
|
|
6751
7143
|
}
|
|
6752
|
-
|
|
6753
|
-
const
|
|
6754
|
-
|
|
6755
|
-
|
|
6756
|
-
|
|
6757
|
-
|
|
6758
|
-
|
|
6759
|
-
|
|
6760
|
-
|
|
6761
|
-
|
|
6762
|
-
const
|
|
6763
|
-
|
|
6764
|
-
|
|
6765
|
-
|
|
6766
|
-
|
|
7144
|
+
const prepareUnlockingTemplates = (keys) => {
|
|
7145
|
+
const counterparties = /* @__PURE__ */ new Map();
|
|
7146
|
+
const counterparty = (publicKey) => {
|
|
7147
|
+
let parsed = counterparties.get(publicKey);
|
|
7148
|
+
if (parsed == null) {
|
|
7149
|
+
parsed = PublicKey.fromString(publicKey);
|
|
7150
|
+
counterparties.set(publicKey, parsed);
|
|
7151
|
+
}
|
|
7152
|
+
return parsed;
|
|
7153
|
+
};
|
|
7154
|
+
const prepared = prior.pdi.map((pdi) => {
|
|
7155
|
+
return {
|
|
7156
|
+
pdi,
|
|
7157
|
+
template: new ScriptTemplateBRC29({
|
|
7158
|
+
derivationPrefix: pdi.derivationPrefix,
|
|
7159
|
+
derivationSuffix: pdi.derivationSuffix,
|
|
7160
|
+
keyDeriver: wallet.keyDeriver
|
|
7161
|
+
}),
|
|
7162
|
+
unlockerPubKey: counterparty(pdi.unlockerPubKey || keys.publicKey)
|
|
7163
|
+
};
|
|
7164
|
+
});
|
|
7165
|
+
const derivations = prepared.map(({ template, unlockerPubKey }) => ({
|
|
7166
|
+
protocolID: brc29ProtocolID,
|
|
7167
|
+
keyID: template.getKeyID(),
|
|
7168
|
+
counterparty: unlockerPubKey
|
|
7169
|
+
}));
|
|
7170
|
+
const derivedPrivateKeys = wallet.keyDeriver.derivePrivateKeys?.(derivations) ?? derivations.map((derivation) => wallet.keyDeriver.derivePrivateKey(derivation.protocolID, derivation.keyID, derivation.counterparty));
|
|
7171
|
+
for (let index = 0; index < prepared.length; index++) {
|
|
7172
|
+
const { pdi, template } = prepared[index];
|
|
7173
|
+
const unlockTemplate = template.unlockWithDerivedPrivateKey(derivedPrivateKeys[index], pdi.sourceSatoshis, asBsvSdkScript(pdi.lockingScript));
|
|
7174
|
+
const input = prior.tx.inputs[pdi.vin];
|
|
7175
|
+
input.unlockingScriptTemplate = unlockTemplate;
|
|
7176
|
+
}
|
|
7177
|
+
};
|
|
7178
|
+
if (wallet.telemetry.enabled && prior.pdi.length > 0) await wallet.telemetry.withSpan("wallet.crypto.prepare_unlocking_templates", {
|
|
7179
|
+
component: "wallet-toolbox",
|
|
7180
|
+
carrier: prior.args,
|
|
7181
|
+
attributes: { "crypto.managed_input_count": prior.pdi.length }
|
|
7182
|
+
}, async (span) => {
|
|
7183
|
+
const keys = await wallet.telemetry.withSpan("wallet.crypto.client_change_key", {
|
|
7184
|
+
component: "wallet-toolbox",
|
|
7185
|
+
parent: span.context
|
|
7186
|
+
}, () => wallet.getClientChangeKeyPair());
|
|
7187
|
+
await wallet.telemetry.withSpan("wallet.crypto.derive_unlocking_templates", {
|
|
7188
|
+
component: "wallet-toolbox",
|
|
7189
|
+
parent: span.context,
|
|
7190
|
+
attributes: { "crypto.managed_input_count": prior.pdi.length }
|
|
7191
|
+
}, () => prepareUnlockingTemplates(keys));
|
|
7192
|
+
});
|
|
7193
|
+
else if (prior.pdi.length > 0) prepareUnlockingTemplates(wallet.getClientChangeKeyPair());
|
|
6767
7194
|
if (wallet.telemetry.enabled) await wallet.telemetry.withSpan("wallet.crypto.transaction_sign", {
|
|
6768
7195
|
component: "wallet-toolbox",
|
|
6769
7196
|
carrier: prior.args,
|
|
@@ -6819,19 +7246,22 @@ async function createActionCore$1(wallet, auth, vargs, parent) {
|
|
|
6819
7246
|
prior.tx = await traceActionStep(wallet, "wallet.create_action.complete_signing", parent, async () => await completeSignedTransaction(prior, {}, wallet));
|
|
6820
7247
|
logger?.log("completed signed transaction");
|
|
6821
7248
|
r.txid = prior.tx.id("hex");
|
|
6822
|
-
const beef =
|
|
6823
|
-
|
|
6824
|
-
|
|
6825
|
-
|
|
6826
|
-
|
|
6827
|
-
|
|
7249
|
+
const beef = await traceActionStep(wallet, "wallet.create_action.assemble_result_beef", parent, () => {
|
|
7250
|
+
const result = new Beef();
|
|
7251
|
+
if (prior.dcr.inputBeef != null) {
|
|
7252
|
+
const inputBeef = prior.dcr.inputBeef instanceof Uint8Array ? Beef.fromBinaryView(prior.dcr.inputBeef) : Beef.fromBinary(prior.dcr.inputBeef);
|
|
7253
|
+
result.mergeBeef(inputBeef);
|
|
7254
|
+
}
|
|
7255
|
+
result.mergeTransaction(prior.tx);
|
|
7256
|
+
return result;
|
|
7257
|
+
});
|
|
6828
7258
|
logger?.log("merged beef");
|
|
6829
7259
|
await traceActionStep(wallet, "wallet.create_action.verify_unlock_scripts", parent, async () => await verifyUnlockScripts(r.txid, beef, wallet.scriptVerifier));
|
|
6830
7260
|
logger?.log("verified unlock scripts");
|
|
6831
7261
|
r.noSendChange = prior.dcr.noSendChangeOutputVouts?.map((vout) => `${r.txid}.${vout}`);
|
|
6832
7262
|
beef.atomicTxid = r.txid;
|
|
6833
7263
|
setResultBeef(r, beef);
|
|
6834
|
-
if (!vargs.options.returnTXIDOnly) r.tx = beef.toUint8ArrayAtomic(r.txid);
|
|
7264
|
+
if (!vargs.options.returnTXIDOnly) r.tx = await traceActionStep(wallet, "wallet.create_action.serialize_result_beef", parent, () => beef.toUint8ArrayAtomic(r.txid));
|
|
6835
7265
|
}
|
|
6836
7266
|
const { sendWithResults, notDelayedResults } = await traceActionStep(wallet, "wallet.create_action.process", parent, async () => await processAction(prior, wallet, auth, vargs));
|
|
6837
7267
|
logger?.log("processed transaction");
|
|
@@ -7449,6 +7879,50 @@ function selectCanonicalChange(outputs, targetSatoshis, exactSatoshis) {
|
|
|
7449
7879
|
if (over != null) return over;
|
|
7450
7880
|
return outputs.filter((output) => output.satoshis < targetSatoshis).sort((a, b) => b.satoshis - a.satoshis || b.outputId - a.outputId)[0];
|
|
7451
7881
|
}
|
|
7882
|
+
/**
|
|
7883
|
+
* Stateful form of the canonical selector for allocating many inputs from one
|
|
7884
|
+
* candidate set. It preserves exact / least-over / largest-under ordering but
|
|
7885
|
+
* sorts once instead of filtering and sorting the full set per input.
|
|
7886
|
+
*/
|
|
7887
|
+
var CanonicalChangeSelector = class {
|
|
7888
|
+
sorted;
|
|
7889
|
+
allocated = /* @__PURE__ */ new Set();
|
|
7890
|
+
constructor(outputs) {
|
|
7891
|
+
this.sorted = [...outputs].sort((a, b) => a.satoshis - b.satoshis || a.outputId - b.outputId);
|
|
7892
|
+
}
|
|
7893
|
+
take(targetSatoshis, exactSatoshis) {
|
|
7894
|
+
if (exactSatoshis !== void 0) for (let index = this.lowerBound(exactSatoshis); index < this.sorted.length; index++) {
|
|
7895
|
+
const output = this.sorted[index];
|
|
7896
|
+
if (output.satoshis !== exactSatoshis) break;
|
|
7897
|
+
if (!this.allocated.has(output.outputId)) return this.allocate(output);
|
|
7898
|
+
}
|
|
7899
|
+
for (let index = this.lowerBound(targetSatoshis); index < this.sorted.length; index++) {
|
|
7900
|
+
const output = this.sorted[index];
|
|
7901
|
+
if (!this.allocated.has(output.outputId)) return this.allocate(output);
|
|
7902
|
+
}
|
|
7903
|
+
for (let index = this.lowerBound(targetSatoshis) - 1; index >= 0; index--) {
|
|
7904
|
+
const output = this.sorted[index];
|
|
7905
|
+
if (!this.allocated.has(output.outputId)) return this.allocate(output);
|
|
7906
|
+
}
|
|
7907
|
+
}
|
|
7908
|
+
release(outputId) {
|
|
7909
|
+
this.allocated.delete(outputId);
|
|
7910
|
+
}
|
|
7911
|
+
allocate(output) {
|
|
7912
|
+
this.allocated.add(output.outputId);
|
|
7913
|
+
return output;
|
|
7914
|
+
}
|
|
7915
|
+
lowerBound(satoshis) {
|
|
7916
|
+
let low = 0;
|
|
7917
|
+
let high = this.sorted.length;
|
|
7918
|
+
while (low < high) {
|
|
7919
|
+
const middle = low + high >>> 1;
|
|
7920
|
+
if (this.sorted[middle].satoshis < satoshis) low = middle + 1;
|
|
7921
|
+
else high = middle;
|
|
7922
|
+
}
|
|
7923
|
+
return low;
|
|
7924
|
+
}
|
|
7925
|
+
};
|
|
7452
7926
|
function repeatableRandom(randomVals) {
|
|
7453
7927
|
const values = [...randomVals ?? []];
|
|
7454
7928
|
return () => {
|
|
@@ -8584,6 +9058,22 @@ var ActionBatchController = class {
|
|
|
8584
9058
|
};
|
|
8585
9059
|
//#endregion
|
|
8586
9060
|
//#region ../src/Wallet.ts
|
|
9061
|
+
function prepareKnownTxidsForCreateAction(wallet, args) {
|
|
9062
|
+
if (!wallet.autoKnownTxids || args.options?.knownTxids != null) return;
|
|
9063
|
+
if (!wallet.telemetry.enabled) {
|
|
9064
|
+
args.options.knownTxids = wallet.getKnownTxids(args.options?.knownTxids);
|
|
9065
|
+
return;
|
|
9066
|
+
}
|
|
9067
|
+
args.options.knownTxids = wallet.telemetry.withSpan("wallet.create_action.prepare_known_txids", {
|
|
9068
|
+
component: "wallet-toolbox",
|
|
9069
|
+
carrier: args,
|
|
9070
|
+
attributes: { "beef.tx_count": wallet.beef.txs.length }
|
|
9071
|
+
}, (span) => {
|
|
9072
|
+
const knownTxids = wallet.getKnownTxids(args.options?.knownTxids);
|
|
9073
|
+
span.end({ attributes: { "beef.known_txid_count": knownTxids.length } });
|
|
9074
|
+
return knownTxids;
|
|
9075
|
+
});
|
|
9076
|
+
}
|
|
8587
9077
|
/**
|
|
8588
9078
|
* Build a {@link DiscoverCertificatesResult} from contact records so {@link Wallet.discoverByIdentityKey}
|
|
8589
9079
|
* and {@link Wallet.discoverByAttributes} can short-circuit on a local contacts hit. The synthetic
|
|
@@ -9057,6 +9547,7 @@ var Wallet = class {
|
|
|
9057
9547
|
if (this.returnTxidOnly) return beef;
|
|
9058
9548
|
const b = parsedBeef ?? Beef.fromBinary(beef);
|
|
9059
9549
|
if (!b.atomicTxid) throw new WERR_INTERNAL();
|
|
9550
|
+
if (!b.txs.some((btx) => btx.isTxidOnly && !knownTxids?.includes(btx.txid))) return beef;
|
|
9060
9551
|
return this.verifyReturnedTxidOnly(b, knownTxids).toBinaryAtomic(b.atomicTxid);
|
|
9061
9552
|
}
|
|
9062
9553
|
verifyReturnedTxidOnlyBEEF(beef) {
|
|
@@ -9088,16 +9579,7 @@ var Wallet = class {
|
|
|
9088
9579
|
Validation.validateOriginator(originator);
|
|
9089
9580
|
args.options ??= {};
|
|
9090
9581
|
args.options.trustSelf ||= this.trustSelf;
|
|
9091
|
-
|
|
9092
|
-
component: "wallet-toolbox",
|
|
9093
|
-
carrier: args,
|
|
9094
|
-
attributes: { "beef.tx_count": this.beef.txs.length }
|
|
9095
|
-
}, (span) => {
|
|
9096
|
-
const knownTxids = this.getKnownTxids(args.options?.knownTxids);
|
|
9097
|
-
span.end({ attributes: { "beef.known_txid_count": knownTxids.length } });
|
|
9098
|
-
return knownTxids;
|
|
9099
|
-
});
|
|
9100
|
-
else args.options.knownTxids = this.getKnownTxids(args.options.knownTxids);
|
|
9582
|
+
prepareKnownTxidsForCreateAction(this, args);
|
|
9101
9583
|
const { auth, vargs } = this.validateAuthAndArgs(args, Validation.validateCreateActionArgs, logger);
|
|
9102
9584
|
logger?.log("validated args");
|
|
9103
9585
|
vargs.includeAllSourceTransactions = this.includeAllSourceTransactions;
|
|
@@ -9105,9 +9587,25 @@ var Wallet = class {
|
|
|
9105
9587
|
const r = await createAction$1(this, auth, vargs);
|
|
9106
9588
|
logger?.log("action created");
|
|
9107
9589
|
const resultBeef = getResultBeef(r);
|
|
9108
|
-
if (r.tx != null) this.beef.mergeBeefFromParty(this.storageParty, resultBeef ?? r.tx);
|
|
9109
9590
|
if (r.tx != null) {
|
|
9110
|
-
|
|
9591
|
+
const merge = () => this.beef.mergeBeefFromParty(this.storageParty, resultBeef ?? r.tx);
|
|
9592
|
+
if (this.telemetry.enabled) this.telemetry.withSpan("wallet.create_action.merge_result_beef", {
|
|
9593
|
+
component: "wallet-toolbox",
|
|
9594
|
+
carrier: args,
|
|
9595
|
+
attributes: {
|
|
9596
|
+
"beef.retained_tx_count_before": this.beef.txs.length,
|
|
9597
|
+
"beef.result_byte_count": r.tx.length
|
|
9598
|
+
}
|
|
9599
|
+
}, merge);
|
|
9600
|
+
else merge();
|
|
9601
|
+
}
|
|
9602
|
+
if (r.tx != null) {
|
|
9603
|
+
const verify = () => this.verifyReturnedTxidOnlyAtomicBEEF(r.tx, args.options?.knownTxids, resultBeef);
|
|
9604
|
+
r.tx = this.telemetry.enabled ? this.telemetry.withSpan("wallet.create_action.verify_result_beef", {
|
|
9605
|
+
component: "wallet-toolbox",
|
|
9606
|
+
carrier: args,
|
|
9607
|
+
attributes: { "beef.result_byte_count": r.tx.length }
|
|
9608
|
+
}, verify) : verify();
|
|
9111
9609
|
logger?.log("verify returned AtomicBEEF");
|
|
9112
9610
|
}
|
|
9113
9611
|
if (!vargs.isDelayed) throwIfAnyUnsuccessfulCreateActions(r);
|
|
@@ -9488,7 +9986,7 @@ async function createActionCore(storage, auth, vargs, parent) {
|
|
|
9488
9986
|
});
|
|
9489
9987
|
const feeModel = validateStorageFeeModel(storage.feeModel);
|
|
9490
9988
|
logger?.log(`validated fee model ${JSON.stringify(feeModel)}`);
|
|
9491
|
-
const initialFundingPlan = await prepareFundingPlan(storage,
|
|
9989
|
+
const initialFundingPlan = await prepareFundingPlan(storage, [
|
|
9492
9990
|
userId,
|
|
9493
9991
|
vargs,
|
|
9494
9992
|
xinputs,
|
|
@@ -9497,48 +9995,64 @@ async function createActionCore(storage, auth, vargs, parent) {
|
|
|
9497
9995
|
noSendChangeIn,
|
|
9498
9996
|
feeModel,
|
|
9499
9997
|
parent
|
|
9500
|
-
|
|
9998
|
+
]);
|
|
9501
9999
|
logger?.log(`planned funding from ${initialFundingPlan.availableChangeCount} change inputs`);
|
|
10000
|
+
const allocatedBeefPrefetch = startAllocatedChangeBeefPrefetch(storage, vargs, initialFundingPlan.selected, beef, parent);
|
|
10001
|
+
const storageBeefBytes = storageBeef.toBinary();
|
|
9502
10002
|
let newTx;
|
|
10003
|
+
let newTxCommitted = false;
|
|
9503
10004
|
try {
|
|
9504
|
-
const
|
|
9505
|
-
|
|
9506
|
-
"
|
|
9507
|
-
|
|
9508
|
-
|
|
9509
|
-
|
|
9510
|
-
|
|
9511
|
-
|
|
9512
|
-
|
|
9513
|
-
|
|
9514
|
-
|
|
9515
|
-
|
|
9516
|
-
|
|
9517
|
-
|
|
9518
|
-
|
|
9519
|
-
|
|
9520
|
-
|
|
9521
|
-
|
|
9522
|
-
|
|
9523
|
-
|
|
9524
|
-
|
|
9525
|
-
|
|
9526
|
-
|
|
9527
|
-
|
|
9528
|
-
|
|
9529
|
-
|
|
9530
|
-
|
|
9531
|
-
|
|
9532
|
-
|
|
9533
|
-
|
|
9534
|
-
|
|
9535
|
-
|
|
9536
|
-
const
|
|
9537
|
-
|
|
9538
|
-
|
|
10005
|
+
const persisted = await storage.transaction(async (trx) => {
|
|
10006
|
+
const initialSatoshis = fundingPlanSatoshis(initialFundingPlan);
|
|
10007
|
+
newTx = await traceStorageStep(storage, "wallet.storage.create_action.create_record", parent, {
|
|
10008
|
+
"action.label_count": vargs.labels.length,
|
|
10009
|
+
"action.storage_beef_bytes": storageBeefBytes.length
|
|
10010
|
+
}, async (span) => {
|
|
10011
|
+
const transaction = await createNewTxRecord(storage, userId, vargs, storageBeefBytes, initialSatoshis, trx);
|
|
10012
|
+
span?.end({ attributes: { "action.transaction_record_created": true } });
|
|
10013
|
+
return transaction;
|
|
10014
|
+
});
|
|
10015
|
+
logger?.log("created new transaction record");
|
|
10016
|
+
const ctx = {
|
|
10017
|
+
xinputs,
|
|
10018
|
+
xoutputs,
|
|
10019
|
+
changeBasket,
|
|
10020
|
+
noSendChangeIn,
|
|
10021
|
+
feeModel,
|
|
10022
|
+
transactionId: newTx.transactionId
|
|
10023
|
+
};
|
|
10024
|
+
const funded = await fundNewTransactionSdk(storage, userId, vargs, ctx, initialFundingPlan, parent, trx);
|
|
10025
|
+
logger?.log("funded new transaction");
|
|
10026
|
+
if (funded.maxPossibleSatoshisAdjustment != null) {
|
|
10027
|
+
const adjustment = funded.maxPossibleSatoshisAdjustment;
|
|
10028
|
+
if (ctx.xoutputs[adjustment.fixedOutputIndex].satoshis !== 0x775f05a073fff) throw new WERR_INTERNAL();
|
|
10029
|
+
ctx.xoutputs[adjustment.fixedOutputIndex].satoshis = adjustment.satoshis;
|
|
10030
|
+
logger?.log("adjusted change outputs to max possible");
|
|
10031
|
+
}
|
|
10032
|
+
const satoshis = funded.changeOutputs.reduce((sum, output) => sum + output.satoshis, 0) - funded.allocatedChange.reduce((sum, output) => sum + output.satoshis, 0);
|
|
10033
|
+
if (satoshis !== initialSatoshis) {
|
|
10034
|
+
await storage.updateTransaction(newTx.transactionId, { satoshis }, trx);
|
|
10035
|
+
newTx.satoshis = satoshis;
|
|
10036
|
+
}
|
|
10037
|
+
const storedOutputs = await traceStorageStep(storage, "wallet.storage.create_action.persist_outputs", parent, {
|
|
10038
|
+
"action.fixed_output_count": ctx.xoutputs.length,
|
|
10039
|
+
"action.change_output_count": funded.changeOutputs.length
|
|
10040
|
+
}, async (span) => {
|
|
10041
|
+
const result = await createNewOutputs(storage, userId, vargs, ctx, funded.changeOutputs, trx);
|
|
10042
|
+
span?.end({ attributes: { "action.persisted_output_count": result.outputs.length } });
|
|
10043
|
+
return result;
|
|
10044
|
+
});
|
|
10045
|
+
return {
|
|
10046
|
+
...funded,
|
|
10047
|
+
...storedOutputs,
|
|
10048
|
+
ctx
|
|
10049
|
+
};
|
|
9539
10050
|
});
|
|
10051
|
+
newTxCommitted = true;
|
|
10052
|
+
const committedTx = verifyTruthy(newTx);
|
|
10053
|
+
const { allocatedChange, derivationPrefix, outputs, changeVouts, ctx } = persisted;
|
|
9540
10054
|
logger?.log("created new output records");
|
|
9541
|
-
const inputBeef = await mergeAllocatedChangeBeefs(storage, vargs, allocatedChange, beef, parent);
|
|
10055
|
+
const inputBeef = await mergeAllocatedChangeBeefs(storage, vargs, allocatedChange, beef, allocatedBeefPrefetch, parent);
|
|
9542
10056
|
logger?.log("merged allocated change beefs");
|
|
9543
10057
|
const inputs = await traceStorageStep(storage, "wallet.storage.create_action.assemble_inputs", parent, {
|
|
9544
10058
|
"action.fixed_input_count": ctx.xinputs.length,
|
|
@@ -9551,9 +10065,9 @@ async function createActionCore(storage, auth, vargs, parent) {
|
|
|
9551
10065
|
});
|
|
9552
10066
|
logger?.log("created new inputs");
|
|
9553
10067
|
const r = {
|
|
9554
|
-
reference:
|
|
9555
|
-
version:
|
|
9556
|
-
lockTime:
|
|
10068
|
+
reference: committedTx.reference,
|
|
10069
|
+
version: committedTx.version,
|
|
10070
|
+
lockTime: committedTx.lockTime,
|
|
9557
10071
|
inputs,
|
|
9558
10072
|
outputs,
|
|
9559
10073
|
derivationPrefix,
|
|
@@ -9563,9 +10077,15 @@ async function createActionCore(storage, auth, vargs, parent) {
|
|
|
9563
10077
|
logger?.groupEnd();
|
|
9564
10078
|
return r;
|
|
9565
10079
|
} catch (error) {
|
|
10080
|
+
await allocatedBeefPrefetch;
|
|
9566
10081
|
if (newTx?.transactionId != null) try {
|
|
9567
|
-
|
|
9568
|
-
|
|
10082
|
+
if (newTxCommitted) {
|
|
10083
|
+
await storage.updateTransactionStatus("failed", newTx.transactionId);
|
|
10084
|
+
logger?.log(`marked failed createAction transaction ${newTx.transactionId} after construction error`);
|
|
10085
|
+
} else {
|
|
10086
|
+
const failed = await createNewTxRecord(storage, userId, vargs, storageBeefBytes, 0, void 0, "failed");
|
|
10087
|
+
logger?.log(`recorded failed createAction transaction ${failed.transactionId} after rollback`);
|
|
10088
|
+
}
|
|
9569
10089
|
} catch (cleanupError) {
|
|
9570
10090
|
logger?.log(`failed to clean up createAction transaction ${newTx.transactionId}: ${String(cleanupError)}`);
|
|
9571
10091
|
}
|
|
@@ -9715,23 +10235,10 @@ async function getCompetingBeefForReview(storage, txid) {
|
|
|
9715
10235
|
throw e;
|
|
9716
10236
|
}
|
|
9717
10237
|
}
|
|
9718
|
-
/**
|
|
9719
|
-
|
|
9720
|
-
async function persistNewOutput(storage, o, tags, txTags, txBaskets) {
|
|
9721
|
-
o.outputId = await storage.insertOutput(o);
|
|
9722
|
-
const changeVout = o.change && o.purpose === "change" && o.providedBy === "storage" ? o.vout : void 0;
|
|
9723
|
-
for (const tagName of new Set(tags)) {
|
|
9724
|
-
const tag = txTags[tagName];
|
|
9725
|
-
await storage.insertOutputTagMap({
|
|
9726
|
-
outputId: verifyId(o.outputId),
|
|
9727
|
-
outputTagId: verifyId(tag.outputTagId),
|
|
9728
|
-
created_at: /* @__PURE__ */ new Date(),
|
|
9729
|
-
updated_at: /* @__PURE__ */ new Date(),
|
|
9730
|
-
isDeleted: false
|
|
9731
|
-
});
|
|
9732
|
-
}
|
|
10238
|
+
/** Build the SDK descriptor for a persisted output. */
|
|
10239
|
+
function describeNewOutput(o, tags, txBaskets) {
|
|
9733
10240
|
return {
|
|
9734
|
-
changeVout,
|
|
10241
|
+
changeVout: o.change && o.purpose === "change" && o.providedBy === "storage" ? o.vout : void 0,
|
|
9735
10242
|
ro: {
|
|
9736
10243
|
vout: verifyInteger(o.vout),
|
|
9737
10244
|
satoshis: Validation.validateSatoshis(o.satoshis, "o.satoshis"),
|
|
@@ -9746,13 +10253,28 @@ async function persistNewOutput(storage, o, tags, txTags, txBaskets) {
|
|
|
9746
10253
|
}
|
|
9747
10254
|
};
|
|
9748
10255
|
}
|
|
9749
|
-
|
|
10256
|
+
/** Insert the output and attach its tags; return the SDK output descriptor. */
|
|
10257
|
+
async function persistNewOutput(storage, o, tags, txTags, txBaskets, trx) {
|
|
10258
|
+
o.outputId = await storage.insertOutput(o, trx);
|
|
10259
|
+
for (const tagName of new Set(tags)) {
|
|
10260
|
+
const tag = txTags[tagName];
|
|
10261
|
+
await storage.insertOutputTagMap({
|
|
10262
|
+
outputId: verifyId(o.outputId),
|
|
10263
|
+
outputTagId: verifyId(tag.outputTagId),
|
|
10264
|
+
created_at: /* @__PURE__ */ new Date(),
|
|
10265
|
+
updated_at: /* @__PURE__ */ new Date(),
|
|
10266
|
+
isDeleted: false
|
|
10267
|
+
}, trx);
|
|
10268
|
+
}
|
|
10269
|
+
return describeNewOutput(o, tags, txBaskets);
|
|
10270
|
+
}
|
|
10271
|
+
async function createNewOutputs(storage, userId, vargs, ctx, changeOutputs, trx) {
|
|
9750
10272
|
const txBaskets = {};
|
|
9751
10273
|
const basketNames = [...new Set(ctx.xoutputs.map((x) => x.basket).filter((v) => !!v))];
|
|
9752
|
-
Object.assign(txBaskets, await storage.findOrInsertOutputBasketsBulk(userId, basketNames));
|
|
10274
|
+
Object.assign(txBaskets, await storage.findOrInsertOutputBasketsBulk(userId, basketNames, trx));
|
|
9753
10275
|
const txTags = {};
|
|
9754
10276
|
const tagNames = [...new Set(ctx.xoutputs.flatMap((x) => x.tags))];
|
|
9755
|
-
Object.assign(txTags, await storage.findOrInsertOutputTagsBulk(userId, tagNames));
|
|
10277
|
+
Object.assign(txTags, await storage.findOrInsertOutputTagsBulk(userId, tagNames, trx));
|
|
9756
10278
|
const newOutputs = [];
|
|
9757
10279
|
for (const xo of ctx.xoutputs) {
|
|
9758
10280
|
const lockingScript = asArray(xo.lockingScript);
|
|
@@ -9768,7 +10290,7 @@ async function createNewOutputs(storage, userId, vargs, ctx, changeOutputs) {
|
|
|
9768
10290
|
created_at: now,
|
|
9769
10291
|
updated_at: now,
|
|
9770
10292
|
commissionId: 0
|
|
9771
|
-
});
|
|
10293
|
+
}, trx);
|
|
9772
10294
|
const o = makeDefaultOutput(userId, ctx.transactionId, xo.satoshis, xo.vout);
|
|
9773
10295
|
o.lockingScript = lockingScript;
|
|
9774
10296
|
o.providedBy = "storage";
|
|
@@ -9802,10 +10324,12 @@ async function createNewOutputs(storage, userId, vargs, ctx, changeOutputs) {
|
|
|
9802
10324
|
});
|
|
9803
10325
|
}
|
|
9804
10326
|
if (vargs.options.randomizeOutputs) randomizeOutputVouts(newOutputs.map((output) => output.o), vargs.randomVals);
|
|
10327
|
+
const untagged = newOutputs.filter((output) => output.tags.length === 0);
|
|
10328
|
+
await storage.insertOutputs(untagged.map((output) => output.o), trx);
|
|
9805
10329
|
const outputs = [];
|
|
9806
10330
|
const changeVouts = [];
|
|
9807
10331
|
for (const { o, tags } of newOutputs) {
|
|
9808
|
-
const { changeVout, ro } = await persistNewOutput(storage, o, tags, txTags, txBaskets);
|
|
10332
|
+
const { changeVout, ro } = tags.length === 0 ? describeNewOutput(o, tags, txBaskets) : await persistNewOutput(storage, o, tags, txTags, txBaskets, trx);
|
|
9809
10333
|
if (changeVout !== void 0) changeVouts.push(changeVout);
|
|
9810
10334
|
outputs.push(ro);
|
|
9811
10335
|
}
|
|
@@ -9814,7 +10338,7 @@ async function createNewOutputs(storage, userId, vargs, ctx, changeOutputs) {
|
|
|
9814
10338
|
changeVouts
|
|
9815
10339
|
};
|
|
9816
10340
|
}
|
|
9817
|
-
async function createNewTxRecord(storage, userId, vargs, storageBeef) {
|
|
10341
|
+
async function createNewTxRecord(storage, userId, vargs, storageBeef, satoshis = 0, trx, status = "unsigned") {
|
|
9818
10342
|
const now = /* @__PURE__ */ new Date();
|
|
9819
10343
|
const newTx = {
|
|
9820
10344
|
created_at: now,
|
|
@@ -9822,9 +10346,9 @@ async function createNewTxRecord(storage, userId, vargs, storageBeef) {
|
|
|
9822
10346
|
transactionId: 0,
|
|
9823
10347
|
version: vargs.version,
|
|
9824
10348
|
lockTime: vargs.lockTime,
|
|
9825
|
-
status
|
|
10349
|
+
status,
|
|
9826
10350
|
reference: randomBytesBase64(12),
|
|
9827
|
-
satoshis
|
|
10351
|
+
satoshis,
|
|
9828
10352
|
userId,
|
|
9829
10353
|
isOutgoing: true,
|
|
9830
10354
|
inputBEEF: storageBeef,
|
|
@@ -9832,12 +10356,12 @@ async function createNewTxRecord(storage, userId, vargs, storageBeef) {
|
|
|
9832
10356
|
txid: void 0,
|
|
9833
10357
|
rawTx: void 0
|
|
9834
10358
|
};
|
|
9835
|
-
newTx.transactionId = await storage.insertTransaction(newTx);
|
|
10359
|
+
newTx.transactionId = await storage.insertTransaction(newTx, trx);
|
|
9836
10360
|
const labelNames = [...new Set(vargs.labels)];
|
|
9837
|
-
const labels = await storage.findOrInsertTxLabelsBulk(userId, labelNames);
|
|
10361
|
+
const labels = await storage.findOrInsertTxLabelsBulk(userId, labelNames, trx);
|
|
9838
10362
|
for (const label of labelNames) {
|
|
9839
10363
|
const txLabel = labels[label];
|
|
9840
|
-
await storage.findOrInsertTxLabelMap(verifyId(newTx.transactionId), verifyId(txLabel.txLabelId));
|
|
10364
|
+
await storage.findOrInsertTxLabelMap(verifyId(newTx.transactionId), verifyId(txLabel.txLabelId), trx);
|
|
9841
10365
|
}
|
|
9842
10366
|
return newTx;
|
|
9843
10367
|
}
|
|
@@ -10033,6 +10557,9 @@ async function validateNoSendChange(storage, userId, vargs, changeBasket) {
|
|
|
10033
10557
|
if ((await storage.findReservedActionBatchOutputIds(r.map((output) => output.outputId))).length > 0) throw new WERR_INVALID_PARAMETER("noSendChange", "outputs not reserved by an active action batch");
|
|
10034
10558
|
return r;
|
|
10035
10559
|
}
|
|
10560
|
+
function fundingPlanSatoshis(plan) {
|
|
10561
|
+
return plan.result.changeOutputs.reduce((sum, output) => sum + output.satoshis, 0) - plan.selected.reduce((sum, output) => sum + output.satoshis, 0);
|
|
10562
|
+
}
|
|
10036
10563
|
var FundingClaimConflict = class extends Error {
|
|
10037
10564
|
conflict;
|
|
10038
10565
|
constructor(conflict) {
|
|
@@ -10068,10 +10595,10 @@ function makeFundingParams(vargs, xinputs, xoutputs, changeBasket, feeModel, ava
|
|
|
10068
10595
|
};
|
|
10069
10596
|
}
|
|
10070
10597
|
async function prepareFundingPlan(storage, context) {
|
|
10071
|
-
const
|
|
10598
|
+
const [userId, vargs, xinputs, xoutputs, changeBasket, noSendChangeIn, feeModel, parent, trx] = context;
|
|
10072
10599
|
const excludeSending = !vargs.isDelayed;
|
|
10073
10600
|
const candidates = await traceStorageStep(storage, "wallet.storage.create_action.funding_candidates", parent, { "funding.exclude_sending": excludeSending }, async (span) => {
|
|
10074
|
-
const outputs = await storage.
|
|
10601
|
+
const outputs = await storage.findAvailableManagedChangeInputCandidates(userId, changeBasket.basketId, excludeSending, trx);
|
|
10075
10602
|
span?.end({ attributes: {
|
|
10076
10603
|
"funding.candidate_count": outputs.length,
|
|
10077
10604
|
"funding.candidate_satoshis": outputs.reduce((sum, output) => sum + output.satoshis, 0)
|
|
@@ -10086,10 +10613,12 @@ async function prepareFundingPlan(storage, context) {
|
|
|
10086
10613
|
"funding.no_send_change_count": noSendChangeIn.length
|
|
10087
10614
|
}, async (span) => {
|
|
10088
10615
|
const allocated = /* @__PURE__ */ new Map();
|
|
10616
|
+
const availableSelector = new CanonicalChangeSelector(available);
|
|
10089
10617
|
const noSend = [...noSendChangeIn];
|
|
10618
|
+
const noSendById = new Map(noSendChangeIn.map((output) => [output.outputId, output]));
|
|
10090
10619
|
const allocate = async (targetSatoshis, exactSatoshis) => {
|
|
10091
10620
|
let output = noSend.pop();
|
|
10092
|
-
output ??=
|
|
10621
|
+
output ??= availableSelector.take(targetSatoshis, exactSatoshis);
|
|
10093
10622
|
if (output == null) return void 0;
|
|
10094
10623
|
allocated.set(output.outputId, output);
|
|
10095
10624
|
return {
|
|
@@ -10098,10 +10627,11 @@ async function prepareFundingPlan(storage, context) {
|
|
|
10098
10627
|
};
|
|
10099
10628
|
};
|
|
10100
10629
|
const release = async (outputId) => {
|
|
10101
|
-
|
|
10102
|
-
if (output == null) return;
|
|
10630
|
+
if (allocated.get(outputId) == null) return;
|
|
10103
10631
|
allocated.delete(outputId);
|
|
10104
|
-
|
|
10632
|
+
availableSelector.release(outputId);
|
|
10633
|
+
const noSendOutput = noSendById.get(outputId);
|
|
10634
|
+
if (noSendOutput != null) noSend.push(noSendOutput);
|
|
10105
10635
|
};
|
|
10106
10636
|
const result = await generateChangeSdk(params, allocate, release, vargs.logger, storage.telemetry);
|
|
10107
10637
|
const selected = result.allocatedChangeInputs.map((input) => verifyTruthy(allocated.get(input.outputId)));
|
|
@@ -10119,7 +10649,8 @@ async function prepareFundingPlan(storage, context) {
|
|
|
10119
10649
|
};
|
|
10120
10650
|
});
|
|
10121
10651
|
}
|
|
10122
|
-
async function claimFundingPlan(storage,
|
|
10652
|
+
async function claimFundingPlan(storage, request) {
|
|
10653
|
+
const [userId, basketId, excludeSending, transactionId, noSendChangeIn, plan, trx] = request;
|
|
10123
10654
|
if (plan.selected.length === 0) return {
|
|
10124
10655
|
outputs: [],
|
|
10125
10656
|
sourceTransactionCount: 0,
|
|
@@ -10129,27 +10660,16 @@ async function claimFundingPlan(storage, userId, basketId, excludeSending, trans
|
|
|
10129
10660
|
const noSendIds = new Set(noSendChangeIn.map((output) => output.outputId));
|
|
10130
10661
|
const statuses = ["completed", "unproven"];
|
|
10131
10662
|
if (!excludeSending) statuses.push("sending");
|
|
10132
|
-
const claim = await storage.transaction(async (
|
|
10133
|
-
const
|
|
10134
|
-
|
|
10135
|
-
return {
|
|
10136
|
-
txid: output.txid,
|
|
10137
|
-
vout: output.vout
|
|
10138
|
-
};
|
|
10139
|
-
});
|
|
10140
|
-
const currentByOutpoint = await storage.findOutputsByOutpointsForUpdate(userId, outpoints, trx, true);
|
|
10141
|
-
const reserved = new Set(await storage.findReservedActionBatchOutputIds(plan.selected.map((output) => output.outputId), trx));
|
|
10142
|
-
const transactionIds = [...new Set(Object.values(currentByOutpoint).map((output) => output.transactionId))];
|
|
10143
|
-
const transactionStatuses = await storage.findTransactionStatusesByIds(userId, transactionIds, trx);
|
|
10663
|
+
const claim = await storage.transaction(async (claimTrx) => {
|
|
10664
|
+
const currentById = await storage.findFundingOutputsForUpdate(userId, plan.selected.map((output) => output.outputId), statuses, claimTrx);
|
|
10665
|
+
const transactionIds = [...new Set(Object.values(currentById).map((output) => output.transactionId))];
|
|
10144
10666
|
const claimed = [];
|
|
10145
10667
|
for (const planned of plan.selected) {
|
|
10146
|
-
const current =
|
|
10147
|
-
|
|
10148
|
-
const validTransaction = currentStatus != null && statuses.includes(currentStatus);
|
|
10149
|
-
if (current?.outputId !== planned.outputId || current?.satoshis !== planned.satoshis || current?.basketId !== basketId || !isAutoSpendableChangeOutput(current) || reserved.has(current?.outputId ?? -1) || validTransaction !== true) return { conflict: noSendIds.has(planned.outputId) ? "noSendChange" : "candidate" };
|
|
10668
|
+
const current = currentById[planned.outputId];
|
|
10669
|
+
if (current?.outputId !== planned.outputId || current?.satoshis !== planned.satoshis || current?.basketId !== basketId || !isAutoSpendableChangeOutput(current) || current?.txid !== planned.txid || current?.vout !== planned.vout) return { conflict: noSendIds.has(planned.outputId) ? "noSendChange" : "candidate" };
|
|
10150
10670
|
claimed.push(current);
|
|
10151
10671
|
}
|
|
10152
|
-
if (await storage.markChangeInputsSpent(claimed.map((output) => output.outputId), transactionId,
|
|
10672
|
+
if (await storage.markChangeInputsSpent(claimed.map((output) => output.outputId), transactionId, claimTrx) !== claimed.length) throw new FundingClaimConflict(claimed.some((output) => noSendIds.has(output.outputId)) ? "noSendChange" : "candidate");
|
|
10153
10673
|
for (const output of claimed) {
|
|
10154
10674
|
output.spendable = false;
|
|
10155
10675
|
output.spentBy = transactionId;
|
|
@@ -10158,19 +10678,19 @@ async function claimFundingPlan(storage, userId, basketId, excludeSending, trans
|
|
|
10158
10678
|
outputs: claimed,
|
|
10159
10679
|
sourceTransactionCount: transactionIds.length
|
|
10160
10680
|
};
|
|
10161
|
-
}).catch((error) => {
|
|
10681
|
+
}, trx).catch((error) => {
|
|
10162
10682
|
if (error instanceof FundingClaimConflict) return { conflict: error.conflict };
|
|
10163
10683
|
throw error;
|
|
10164
10684
|
});
|
|
10165
10685
|
if (claim.outputs == null) return claim;
|
|
10166
|
-
const hydration = await hydrateFundingInputScripts(storage, claim.outputs);
|
|
10686
|
+
const hydration = await hydrateFundingInputScripts(storage, claim.outputs, trx);
|
|
10167
10687
|
return {
|
|
10168
10688
|
outputs: claim.outputs,
|
|
10169
10689
|
sourceTransactionCount: claim.sourceTransactionCount,
|
|
10170
10690
|
...hydration
|
|
10171
10691
|
};
|
|
10172
10692
|
}
|
|
10173
|
-
async function hydrateFundingInputScripts(storage, outputs) {
|
|
10693
|
+
async function hydrateFundingInputScripts(storage, outputs, trx) {
|
|
10174
10694
|
const missing = outputs.filter((output) => output.lockingScript?.length !== output.scriptLength && output.scriptLength != null && output.scriptLength > 0 && output.scriptOffset != null && output.scriptOffset > 0 && output.txid != null && output.txid !== "");
|
|
10175
10695
|
if (missing.length === 0) return {
|
|
10176
10696
|
hydratedScriptCount: 0,
|
|
@@ -10189,12 +10709,12 @@ async function hydrateFundingInputScripts(storage, outputs) {
|
|
|
10189
10709
|
while (cursor < groups.length) {
|
|
10190
10710
|
const [txid, group] = groups[cursor++];
|
|
10191
10711
|
if (group.length === 1) {
|
|
10192
|
-
await storage.validateOutputScript(group[0]);
|
|
10712
|
+
await storage.validateOutputScript(group[0], trx);
|
|
10193
10713
|
continue;
|
|
10194
10714
|
}
|
|
10195
|
-
const rawTx = await storage.getRawTxOfKnownValidTransaction(txid);
|
|
10715
|
+
const rawTx = await storage.getRawTxOfKnownValidTransaction(txid, void 0, void 0, trx);
|
|
10196
10716
|
if (rawTx != null) for (const output of group) output.lockingScript = rawTx.slice(output.scriptOffset, output.scriptOffset + output.scriptLength);
|
|
10197
|
-
else for (const output of group) await storage.validateOutputScript(output);
|
|
10717
|
+
else for (const output of group) await storage.validateOutputScript(output, trx);
|
|
10198
10718
|
}
|
|
10199
10719
|
}));
|
|
10200
10720
|
return {
|
|
@@ -10202,13 +10722,21 @@ async function hydrateFundingInputScripts(storage, outputs) {
|
|
|
10202
10722
|
scriptSourceTransactionCount: groups.length
|
|
10203
10723
|
};
|
|
10204
10724
|
}
|
|
10205
|
-
async function fundNewTransactionSdk(storage, userId, vargs, ctx, initialPlan, parent) {
|
|
10725
|
+
async function fundNewTransactionSdk(storage, userId, vargs, ctx, initialPlan, parent, trx) {
|
|
10206
10726
|
let plan = initialPlan;
|
|
10207
10727
|
let allocatedChange;
|
|
10208
10728
|
let retryCount = 0;
|
|
10209
10729
|
await traceStorageStep(storage, "wallet.storage.create_action.funding_claim", parent, { "funding.planned_input_count": initialPlan.selected.length }, async (span) => {
|
|
10210
10730
|
for (let attempt = 0; attempt < 3; attempt++) {
|
|
10211
|
-
const claim = await claimFundingPlan(storage,
|
|
10731
|
+
const claim = await claimFundingPlan(storage, [
|
|
10732
|
+
userId,
|
|
10733
|
+
ctx.changeBasket.basketId,
|
|
10734
|
+
!vargs.isDelayed,
|
|
10735
|
+
ctx.transactionId,
|
|
10736
|
+
ctx.noSendChangeIn,
|
|
10737
|
+
plan,
|
|
10738
|
+
trx
|
|
10739
|
+
]);
|
|
10212
10740
|
if (claim.outputs != null) {
|
|
10213
10741
|
allocatedChange = claim.outputs;
|
|
10214
10742
|
span?.end({ attributes: {
|
|
@@ -10221,16 +10749,17 @@ async function fundNewTransactionSdk(storage, userId, vargs, ctx, initialPlan, p
|
|
|
10221
10749
|
}
|
|
10222
10750
|
if (claim.conflict === "noSendChange") throw new WERR_INVALID_PARAMETER("noSendChange", "outputs that remain spendable during action planning");
|
|
10223
10751
|
retryCount++;
|
|
10224
|
-
plan = await prepareFundingPlan(storage,
|
|
10752
|
+
plan = await prepareFundingPlan(storage, [
|
|
10225
10753
|
userId,
|
|
10226
10754
|
vargs,
|
|
10227
|
-
|
|
10228
|
-
|
|
10229
|
-
|
|
10230
|
-
|
|
10231
|
-
|
|
10232
|
-
parent
|
|
10233
|
-
|
|
10755
|
+
ctx.xinputs,
|
|
10756
|
+
ctx.xoutputs,
|
|
10757
|
+
ctx.changeBasket,
|
|
10758
|
+
ctx.noSendChangeIn,
|
|
10759
|
+
ctx.feeModel,
|
|
10760
|
+
parent,
|
|
10761
|
+
trx
|
|
10762
|
+
]);
|
|
10234
10763
|
}
|
|
10235
10764
|
throw new WERR_INVALID_OPERATION("wallet funding changed repeatedly during action planning; retry createAction");
|
|
10236
10765
|
});
|
|
@@ -10316,7 +10845,56 @@ function makeKnownTxidLookup(knownTxids) {
|
|
|
10316
10845
|
return knownTxids.includes(txid);
|
|
10317
10846
|
};
|
|
10318
10847
|
}
|
|
10319
|
-
|
|
10848
|
+
function missingAllocatedChangeTxids(allocatedChange, beef, knownTxids) {
|
|
10849
|
+
const hasKnownTxid = makeKnownTxidLookup(knownTxids);
|
|
10850
|
+
return Array.from(new Set(allocatedChange.map((output) => verifyTruthy(output.txid)).filter((txid) => beef.findTxid(txid) == null && !hasKnownTxid(txid))));
|
|
10851
|
+
}
|
|
10852
|
+
function startAllocatedChangeBeefPrefetch(storage, vargs, allocatedChange, beef, parent) {
|
|
10853
|
+
if (vargs.options.returnTXIDOnly) return Promise.resolve({
|
|
10854
|
+
sourceCount: 0,
|
|
10855
|
+
txids: []
|
|
10856
|
+
});
|
|
10857
|
+
const knownTxids = vargs.options.knownTxids ?? [];
|
|
10858
|
+
const missing = missingAllocatedChangeTxids(allocatedChange, beef, knownTxids);
|
|
10859
|
+
if (missing.length === 0) return Promise.resolve({
|
|
10860
|
+
sourceCount: 0,
|
|
10861
|
+
txids: []
|
|
10862
|
+
});
|
|
10863
|
+
const options = {
|
|
10864
|
+
trustSelf: void 0,
|
|
10865
|
+
knownTxids,
|
|
10866
|
+
ignoreStorage: false,
|
|
10867
|
+
ignoreServices: true,
|
|
10868
|
+
ignoreNewProven: false,
|
|
10869
|
+
minProofLevel: void 0
|
|
10870
|
+
};
|
|
10871
|
+
return traceStorageStep(storage, "wallet.storage.create_action.beef_prefetch", parent, {
|
|
10872
|
+
"beef.planned_source_count": allocatedChange.length,
|
|
10873
|
+
"beef.missing_source_count": missing.length,
|
|
10874
|
+
"beef.storage_batch_count": missing.length === 0 ? 0 : 1
|
|
10875
|
+
}, async (span) => {
|
|
10876
|
+
const fetched = await storage.getBeefForTransactions(missing, options);
|
|
10877
|
+
span?.end({ attributes: {
|
|
10878
|
+
"beef.fetched_tx_count": fetched.txs.length,
|
|
10879
|
+
"beef.fetched_bump_count": fetched.bumps.length
|
|
10880
|
+
} });
|
|
10881
|
+
return fetched;
|
|
10882
|
+
}).then((prefetched) => ({
|
|
10883
|
+
beef: prefetched,
|
|
10884
|
+
sourceCount: missing.length,
|
|
10885
|
+
txids: missing
|
|
10886
|
+
}), (error) => ({
|
|
10887
|
+
error,
|
|
10888
|
+
sourceCount: missing.length,
|
|
10889
|
+
txids: missing
|
|
10890
|
+
}));
|
|
10891
|
+
}
|
|
10892
|
+
function sameTxids(left, right) {
|
|
10893
|
+
if (left.length !== right.length) return false;
|
|
10894
|
+
const expected = new Set(left);
|
|
10895
|
+
return right.every((txid) => expected.has(txid));
|
|
10896
|
+
}
|
|
10897
|
+
async function mergeAllocatedChangeBeefs(storage, vargs, allocatedChange, beef, prefetch, parent) {
|
|
10320
10898
|
const options = {
|
|
10321
10899
|
trustSelf: void 0,
|
|
10322
10900
|
knownTxids: vargs.options.knownTxids,
|
|
@@ -10328,37 +10906,37 @@ async function mergeAllocatedChangeBeefs(storage, vargs, allocatedChange, beef,
|
|
|
10328
10906
|
};
|
|
10329
10907
|
if (vargs.options.returnTXIDOnly) return void 0;
|
|
10330
10908
|
const knownTxids = vargs.options.knownTxids ?? [];
|
|
10331
|
-
const
|
|
10332
|
-
const
|
|
10333
|
-
|
|
10334
|
-
|
|
10335
|
-
|
|
10909
|
+
const requiredBeforePrefetch = missingAllocatedChangeTxids(allocatedChange, beef, knownTxids);
|
|
10910
|
+
const prefetched = await traceStorageStep(storage, "wallet.storage.create_action.beef_prefetch_join", parent, { "beef.prefetch_source_count": 0 }, async (span) => {
|
|
10911
|
+
const result = await prefetch;
|
|
10912
|
+
span?.end({ attributes: { "beef.prefetch_source_count": result.sourceCount } });
|
|
10913
|
+
return result;
|
|
10914
|
+
});
|
|
10915
|
+
const usePrefetch = sameTxids(prefetched.txids, requiredBeforePrefetch);
|
|
10916
|
+
if (usePrefetch && prefetched.error != null) throw prefetched.error;
|
|
10917
|
+
if (usePrefetch && prefetched.beef != null) beef.mergeBeef(prefetched.beef);
|
|
10918
|
+
const missing = missingAllocatedChangeTxids(allocatedChange, beef, knownTxids);
|
|
10919
|
+
let fetched;
|
|
10336
10920
|
await traceStorageStep(storage, "wallet.storage.create_action.beef_fetch", parent, {
|
|
10337
10921
|
"beef.allocated_change_count": allocatedChange.length,
|
|
10338
10922
|
"beef.distinct_source_count": new Set(allocatedChange.map((output) => output.txid)).size,
|
|
10339
10923
|
"beef.known_txid_count": knownTxids.length,
|
|
10340
10924
|
"beef.missing_source_count": missing.length,
|
|
10341
|
-
"beef.fetch_concurrency":
|
|
10925
|
+
"beef.fetch_concurrency": 1,
|
|
10926
|
+
"beef.prefetch_reused": usePrefetch
|
|
10342
10927
|
}, async (span) => {
|
|
10343
|
-
|
|
10344
|
-
|
|
10345
|
-
|
|
10346
|
-
|
|
10347
|
-
...options,
|
|
10348
|
-
mergeToBeef: void 0
|
|
10349
|
-
});
|
|
10350
|
-
}
|
|
10351
|
-
}));
|
|
10928
|
+
if (missing.length > 0) fetched = await storage.getBeefForTransactions(missing, {
|
|
10929
|
+
...options,
|
|
10930
|
+
mergeToBeef: void 0
|
|
10931
|
+
});
|
|
10352
10932
|
span?.end({ attributes: {
|
|
10353
|
-
"beef.fetched_tx_count": fetched
|
|
10354
|
-
"beef.fetched_bump_count": fetched
|
|
10933
|
+
"beef.fetched_tx_count": fetched?.txs.length ?? 0,
|
|
10934
|
+
"beef.fetched_bump_count": fetched?.bumps.length ?? 0,
|
|
10935
|
+
"beef.storage_batch_count": missing.length === 0 ? 0 : 1
|
|
10355
10936
|
} });
|
|
10356
10937
|
});
|
|
10357
|
-
await traceStorageStep(storage, "wallet.storage.create_action.beef_merge", parent, { "beef.fragment_count":
|
|
10358
|
-
|
|
10359
|
-
if (fetchedBeef == null) continue;
|
|
10360
|
-
beef.mergeBeef(fetchedBeef);
|
|
10361
|
-
}
|
|
10938
|
+
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) => {
|
|
10939
|
+
if (fetched != null) beef.mergeBeef(fetched);
|
|
10362
10940
|
span?.end({ attributes: {
|
|
10363
10941
|
"beef.merged_tx_count": beef.txs.length,
|
|
10364
10942
|
"beef.merged_bump_count": beef.bumps.length
|
|
@@ -11156,9 +11734,7 @@ function genesisHeader(chain) {
|
|
|
11156
11734
|
height: 0,
|
|
11157
11735
|
hash: "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f"
|
|
11158
11736
|
};
|
|
11159
|
-
case "test":
|
|
11160
|
-
case "ttn":
|
|
11161
|
-
case "tstn": return {
|
|
11737
|
+
case "test": return {
|
|
11162
11738
|
version: 1,
|
|
11163
11739
|
previousHash: "0000000000000000000000000000000000000000000000000000000000000000",
|
|
11164
11740
|
merkleRoot: "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b",
|
|
@@ -11168,6 +11744,36 @@ function genesisHeader(chain) {
|
|
|
11168
11744
|
height: 0,
|
|
11169
11745
|
hash: "000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943"
|
|
11170
11746
|
};
|
|
11747
|
+
case "stn": return {
|
|
11748
|
+
version: 1,
|
|
11749
|
+
previousHash: "0000000000000000000000000000000000000000000000000000000000000000",
|
|
11750
|
+
merkleRoot: "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b",
|
|
11751
|
+
time: 1296688602,
|
|
11752
|
+
bits: 486604799,
|
|
11753
|
+
nonce: 173779992,
|
|
11754
|
+
height: 0,
|
|
11755
|
+
hash: "6b38bdbcd73a19f7889d23e1fa6166a9de71affceca60ca3bb1b28af8135c594"
|
|
11756
|
+
};
|
|
11757
|
+
case "ttn": return {
|
|
11758
|
+
version: 1,
|
|
11759
|
+
previousHash: "0000000000000000000000000000000000000000000000000000000000000000",
|
|
11760
|
+
merkleRoot: "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b",
|
|
11761
|
+
time: 1755606836,
|
|
11762
|
+
bits: 486604799,
|
|
11763
|
+
nonce: 1092578460,
|
|
11764
|
+
height: 0,
|
|
11765
|
+
hash: "000000000499eabba0a88f5b3747231c74b9191c1a4a04b2c2ea817976b7776d"
|
|
11766
|
+
};
|
|
11767
|
+
case "tstn": return {
|
|
11768
|
+
version: 1,
|
|
11769
|
+
previousHash: "0000000000000000000000000000000000000000000000000000000000000000",
|
|
11770
|
+
merkleRoot: "64452e5b25c65e492ad6a4f5ce9f427ca986626c28315d88de920d66e28cc98f",
|
|
11771
|
+
time: 1782864e3,
|
|
11772
|
+
bits: 486604799,
|
|
11773
|
+
nonce: 1780488216,
|
|
11774
|
+
height: 0,
|
|
11775
|
+
hash: "000000005d221c0e023cb56b5682cf094f32cd959958b40bc931e5797cae706c"
|
|
11776
|
+
};
|
|
11171
11777
|
case "mock": throw new Error("genesisHeader does not support 'mock' chain. Mock chain generates its own genesis block.");
|
|
11172
11778
|
}
|
|
11173
11779
|
}
|
|
@@ -13640,10 +14246,28 @@ var StorageProvider = class StorageProvider extends StorageReaderWriter {
|
|
|
13640
14246
|
}
|
|
13641
14247
|
return updated;
|
|
13642
14248
|
}
|
|
14249
|
+
/**
|
|
14250
|
+
* Insert outputs that do not need their generated ids returned to the
|
|
14251
|
+
* caller. Engines with a multi-row insert override this common-path helper;
|
|
14252
|
+
* the fallback preserves existing storage implementations unchanged.
|
|
14253
|
+
*/
|
|
14254
|
+
async insertOutputs(outputs, trx) {
|
|
14255
|
+
for (const output of outputs) await this.insertOutput(output, trx);
|
|
14256
|
+
}
|
|
13643
14257
|
/** Return unreserved wallet-managed outputs eligible for automatic funding. */
|
|
13644
14258
|
async findAvailableManagedChangeInputs(userId, basketId, excludeSending, trx) {
|
|
13645
14259
|
return await availableManagedChange(this, userId, basketId, excludeSending, trx);
|
|
13646
14260
|
}
|
|
14261
|
+
/** Read only the fields needed by the in-memory funding planner. */
|
|
14262
|
+
async findAvailableManagedChangeInputCandidates(userId, basketId, excludeSending, trx) {
|
|
14263
|
+
return (await this.findAvailableManagedChangeInputs(userId, basketId, excludeSending, trx)).map(({ outputId, transactionId, satoshis, txid, vout }) => ({
|
|
14264
|
+
outputId,
|
|
14265
|
+
transactionId,
|
|
14266
|
+
satoshis,
|
|
14267
|
+
txid,
|
|
14268
|
+
vout
|
|
14269
|
+
}));
|
|
14270
|
+
}
|
|
13647
14271
|
/** Read the current status of a set of source transactions without loading raw transaction bytes. */
|
|
13648
14272
|
async findTransactionStatusesByIds(userId, transactionIds, trx) {
|
|
13649
14273
|
const statuses = /* @__PURE__ */ new Map();
|
|
@@ -13653,6 +14277,36 @@ var StorageProvider = class StorageProvider extends StorageReaderWriter {
|
|
|
13653
14277
|
}
|
|
13654
14278
|
return statuses;
|
|
13655
14279
|
}
|
|
14280
|
+
/**
|
|
14281
|
+
* Lock and return the selected funding rows whose source transaction and
|
|
14282
|
+
* action-batch reservation state still permit allocation.
|
|
14283
|
+
*/
|
|
14284
|
+
async findFundingOutputsForUpdate(userId, outputIds, statuses, trx) {
|
|
14285
|
+
const rows = await this.findOutputsByIds(outputIds, trx);
|
|
14286
|
+
const reserved = new Set(await this.findReservedActionBatchOutputIds(outputIds, trx));
|
|
14287
|
+
const transactionIds = [...new Set(Object.values(rows).map((output) => output.transactionId))];
|
|
14288
|
+
const transactionStatuses = await this.findTransactionStatusesByIds(userId, transactionIds, trx);
|
|
14289
|
+
const eligible = {};
|
|
14290
|
+
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;
|
|
14291
|
+
return eligible;
|
|
14292
|
+
}
|
|
14293
|
+
/**
|
|
14294
|
+
* Resolve several transaction proofs in one storage operation when the
|
|
14295
|
+
* backend supports it. The default preserves compatibility for custom
|
|
14296
|
+
* providers; SQL and IndexedDB providers override this hot path.
|
|
14297
|
+
*/
|
|
14298
|
+
async getProvenOrRawTxs(txids, trx) {
|
|
14299
|
+
const results = /* @__PURE__ */ new Map();
|
|
14300
|
+
const unique = [...new Set(txids)];
|
|
14301
|
+
let cursor = 0;
|
|
14302
|
+
await Promise.all(Array.from({ length: Math.min(8, unique.length) }, async () => {
|
|
14303
|
+
while (cursor < unique.length) {
|
|
14304
|
+
const txid = unique[cursor++];
|
|
14305
|
+
results.set(txid, await this.getProvenOrRawTx(txid, trx));
|
|
14306
|
+
}
|
|
14307
|
+
}));
|
|
14308
|
+
return results;
|
|
14309
|
+
}
|
|
13656
14310
|
async insertActionBatch(_batch, _trx) {
|
|
13657
14311
|
throw new WERR_NOT_IMPLEMENTED();
|
|
13658
14312
|
}
|
|
@@ -13704,6 +14358,10 @@ var StorageProvider = class StorageProvider extends StorageReaderWriter {
|
|
|
13704
14358
|
supportsActionBatchPersistence() {
|
|
13705
14359
|
return false;
|
|
13706
14360
|
}
|
|
14361
|
+
/** Custom providers may require physical expiry cleanup before reservations are queried. */
|
|
14362
|
+
requiresActionBatchCleanupBeforeCreateAction() {
|
|
14363
|
+
return true;
|
|
14364
|
+
}
|
|
13707
14365
|
async beginActionBatch(auth, args) {
|
|
13708
14366
|
if (!this.supportsActionBatchPersistence()) throw new WERR_NOT_IMPLEMENTED("actionBatch capability is not available");
|
|
13709
14367
|
return await beginActionBatch(this, auth, args);
|
|
@@ -14092,7 +14750,7 @@ var StorageProvider = class StorageProvider extends StorageReaderWriter {
|
|
|
14092
14750
|
}
|
|
14093
14751
|
async createAction(auth, args) {
|
|
14094
14752
|
if (auth.userId == null) throw new WERR_UNAUTHORIZED();
|
|
14095
|
-
if (this.supportsActionBatchPersistence()) await cleanupExpiredActionBatches(this);
|
|
14753
|
+
if (this.supportsActionBatchPersistence() && this.requiresActionBatchCleanupBeforeCreateAction()) await cleanupExpiredActionBatches(this);
|
|
14096
14754
|
return await createAction(this, auth, args);
|
|
14097
14755
|
}
|
|
14098
14756
|
async processAction(auth, args) {
|
|
@@ -14182,6 +14840,9 @@ var StorageProvider = class StorageProvider extends StorageReaderWriter {
|
|
|
14182
14840
|
async getBeefForTransaction(txid, options) {
|
|
14183
14841
|
return await getBeefForTransaction(this, txid, options);
|
|
14184
14842
|
}
|
|
14843
|
+
async getBeefForTransactions(txids, options) {
|
|
14844
|
+
return await getBeefForTransactions(this, txids, options);
|
|
14845
|
+
}
|
|
14185
14846
|
async findMonitorEventById(id, trx) {
|
|
14186
14847
|
return verifyOneOrNone(await this.findMonitorEvents({
|
|
14187
14848
|
partial: { id },
|
|
@@ -15140,6 +15801,9 @@ var StorageIdb = class extends StorageProvider {
|
|
|
15140
15801
|
supportsActionBatchPersistence() {
|
|
15141
15802
|
return true;
|
|
15142
15803
|
}
|
|
15804
|
+
requiresActionBatchCleanupBeforeCreateAction() {
|
|
15805
|
+
return false;
|
|
15806
|
+
}
|
|
15143
15807
|
/**
|
|
15144
15808
|
* This method must be called at least once before any other method accesses the database,
|
|
15145
15809
|
* and each time the schema may have updated.
|
|
@@ -15319,6 +15983,50 @@ var StorageIdb = class extends StorageProvider {
|
|
|
15319
15983
|
}
|
|
15320
15984
|
return r;
|
|
15321
15985
|
}
|
|
15986
|
+
async getProvenOrRawTxs(txids, trx) {
|
|
15987
|
+
const results = /* @__PURE__ */ new Map();
|
|
15988
|
+
const unique = [...new Set(txids)];
|
|
15989
|
+
if (unique.length === 0) return results;
|
|
15990
|
+
const dbTrx = this.toDbTrx(["proven_txs", "proven_tx_reqs"], "readonly", trx);
|
|
15991
|
+
const provenIndex = dbTrx.objectStore("proven_txs").index("txid");
|
|
15992
|
+
const requestIndex = dbTrx.objectStore("proven_tx_reqs").index("txid");
|
|
15993
|
+
const usableStatuses = /* @__PURE__ */ new Set([
|
|
15994
|
+
"unsent",
|
|
15995
|
+
"unmined",
|
|
15996
|
+
"unconfirmed",
|
|
15997
|
+
"sending",
|
|
15998
|
+
"nosend",
|
|
15999
|
+
"completed"
|
|
16000
|
+
]);
|
|
16001
|
+
await Promise.all(unique.map(async (txid) => {
|
|
16002
|
+
const proven = await provenIndex.get(txid);
|
|
16003
|
+
if (proven != null) {
|
|
16004
|
+
results.set(txid, {
|
|
16005
|
+
proven: this.validateEntity(proven),
|
|
16006
|
+
rawTx: void 0,
|
|
16007
|
+
inputBEEF: void 0
|
|
16008
|
+
});
|
|
16009
|
+
return;
|
|
16010
|
+
}
|
|
16011
|
+
const request = await requestIndex.get(txid);
|
|
16012
|
+
if (request != null && usableStatuses.has(request.status)) {
|
|
16013
|
+
const validated = this.validateEntity(request);
|
|
16014
|
+
results.set(txid, {
|
|
16015
|
+
proven: void 0,
|
|
16016
|
+
rawTx: Array.from(validated.rawTx),
|
|
16017
|
+
inputBEEF: validated.inputBEEF == null ? void 0 : Array.from(validated.inputBEEF)
|
|
16018
|
+
});
|
|
16019
|
+
return;
|
|
16020
|
+
}
|
|
16021
|
+
results.set(txid, {
|
|
16022
|
+
proven: void 0,
|
|
16023
|
+
rawTx: void 0,
|
|
16024
|
+
inputBEEF: void 0
|
|
16025
|
+
});
|
|
16026
|
+
}));
|
|
16027
|
+
if (trx == null) await dbTrx.done;
|
|
16028
|
+
return results;
|
|
16029
|
+
}
|
|
15322
16030
|
async getRawTxOfKnownValidTransaction(txid, offset, length, trx) {
|
|
15323
16031
|
if (txid == null || txid === "") return void 0;
|
|
15324
16032
|
if (!this.isAvailable()) await this.makeAvailable();
|
|
@@ -15580,6 +16288,7 @@ var StorageIdb = class extends StorageProvider {
|
|
|
15580
16288
|
else cursor = await store.openCursor(null, direction);
|
|
15581
16289
|
await scanCursor(cursor, args.since, args.paged?.offset ?? 0, args.paged?.limit, async (r) => {
|
|
15582
16290
|
if (!matchesProvenTxPartial(r, args.partial)) return false;
|
|
16291
|
+
if (args.txids != null && args.txids.length > 0 && !args.txids.includes(r.txid)) return false;
|
|
15583
16292
|
if (userId !== void 0) {
|
|
15584
16293
|
if (await this.countTransactions({
|
|
15585
16294
|
partial: {
|
|
@@ -16024,11 +16733,18 @@ var StorageIdb = class extends StorageProvider {
|
|
|
16024
16733
|
return rows.map((r) => r.outputId);
|
|
16025
16734
|
}
|
|
16026
16735
|
async findReservedActionBatchOutputIds(outputIds, trx) {
|
|
16027
|
-
const tx = this.toDbTrx(["action_batch_outputs"], "readonly", trx);
|
|
16736
|
+
const tx = this.toDbTrx(["action_batch_outputs", "action_batches"], "readonly", trx);
|
|
16028
16737
|
const store = tx.objectStore("action_batch_outputs");
|
|
16738
|
+
const batchStore = tx.objectStore("action_batches");
|
|
16029
16739
|
if (store.get == null) throw new WERR_INTERNAL("IndexedDB action_batch_outputs store does not support get");
|
|
16030
16740
|
const reserved = [];
|
|
16031
|
-
|
|
16741
|
+
const now = Date.now();
|
|
16742
|
+
for (const outputId of outputIds) {
|
|
16743
|
+
const reservation = await store.get(outputId);
|
|
16744
|
+
if (reservation == null) continue;
|
|
16745
|
+
const batch = await batchStore.get(reservation.actionBatchId);
|
|
16746
|
+
if (batch != null && (batch.status === "active" || batch.status === "prepared") && batch.expiresAt.getTime() > now && batch.hardExpiresAt.getTime() > now) reserved.push(outputId);
|
|
16747
|
+
}
|
|
16032
16748
|
if (trx == null) await tx.done;
|
|
16033
16749
|
return reserved;
|
|
16034
16750
|
}
|
|
@@ -18585,10 +19301,11 @@ var Chaintracks = class {
|
|
|
18585
19301
|
startupError = null;
|
|
18586
19302
|
subscriberCallbacksEnabled = false;
|
|
18587
19303
|
stopMainThread = true;
|
|
18588
|
-
lastPresentHeight =
|
|
19304
|
+
lastPresentHeight = -1;
|
|
18589
19305
|
lastPresentHeightMsecs = 0;
|
|
18590
19306
|
lastPresentHeightMaxAge = 60 * 1e3;
|
|
18591
19307
|
lock = new SingleWriterMultiReaderLock();
|
|
19308
|
+
sourceStatus = /* @__PURE__ */ new Map();
|
|
18592
19309
|
constructor(options) {
|
|
18593
19310
|
this.options = options;
|
|
18594
19311
|
if (options.storage == null) throw new Error("storage is required.");
|
|
@@ -18599,6 +19316,22 @@ var Chaintracks = class {
|
|
|
18599
19316
|
this.storage = options.storage;
|
|
18600
19317
|
this.bulkIngestors = options.bulkIngestors;
|
|
18601
19318
|
this.liveIngestors = options.liveIngestors;
|
|
19319
|
+
for (const [index, source] of this.bulkIngestors.entries()) {
|
|
19320
|
+
const name = this.sourceName("bulk", index, source);
|
|
19321
|
+
this.sourceStatus.set(name, {
|
|
19322
|
+
name,
|
|
19323
|
+
role: "bulk",
|
|
19324
|
+
state: "unknown"
|
|
19325
|
+
});
|
|
19326
|
+
}
|
|
19327
|
+
for (const [index, source] of this.liveIngestors.entries()) {
|
|
19328
|
+
const name = this.sourceName("live", index, source);
|
|
19329
|
+
this.sourceStatus.set(name, {
|
|
19330
|
+
name,
|
|
19331
|
+
role: "live",
|
|
19332
|
+
state: "unknown"
|
|
19333
|
+
});
|
|
19334
|
+
}
|
|
18602
19335
|
this.addLiveRecursionLimit = options.addLiveRecursionLimit;
|
|
18603
19336
|
if (options.logging != null) this.log = options.logging;
|
|
18604
19337
|
this.storage.log = this.log;
|
|
@@ -18613,19 +19346,36 @@ var Chaintracks = class {
|
|
|
18613
19346
|
*/
|
|
18614
19347
|
async getPresentHeight() {
|
|
18615
19348
|
const now = Date.now();
|
|
18616
|
-
if (this.lastPresentHeight && now - this.lastPresentHeightMsecs < this.lastPresentHeightMaxAge) return this.lastPresentHeight;
|
|
18617
|
-
const
|
|
18618
|
-
|
|
18619
|
-
|
|
18620
|
-
|
|
18621
|
-
|
|
18622
|
-
|
|
18623
|
-
|
|
18624
|
-
|
|
18625
|
-
|
|
18626
|
-
|
|
18627
|
-
|
|
18628
|
-
|
|
19349
|
+
if (this.lastPresentHeight >= 0 && now - this.lastPresentHeightMsecs < this.lastPresentHeightMaxAge) return this.lastPresentHeight;
|
|
19350
|
+
for (const [index, bulk] of this.bulkIngestors.entries()) {
|
|
19351
|
+
const source = this.sourceName("bulk", index, bulk);
|
|
19352
|
+
try {
|
|
19353
|
+
const presentHeight = await bulk.getPresentHeight();
|
|
19354
|
+
if (presentHeight != null && Number.isInteger(presentHeight) && presentHeight >= 0) {
|
|
19355
|
+
this.markSourceSuccess(source, "bulk");
|
|
19356
|
+
this.lastPresentHeight = presentHeight;
|
|
19357
|
+
this.lastPresentHeightMsecs = now;
|
|
19358
|
+
return presentHeight;
|
|
19359
|
+
}
|
|
19360
|
+
} catch (uerr) {
|
|
19361
|
+
const error = WalletError.fromUnknown(uerr);
|
|
19362
|
+
this.markSourceFailure(source, "bulk", error);
|
|
19363
|
+
this.log(`Present-height source ${source} failed: ${error.message}`);
|
|
19364
|
+
}
|
|
19365
|
+
}
|
|
19366
|
+
if (this.lastPresentHeight >= 0) return this.lastPresentHeight;
|
|
19367
|
+
try {
|
|
19368
|
+
const ranges = await this.storage.getAvailableHeightRanges();
|
|
19369
|
+
const localHeight = Math.max(ranges.bulk.maxHeight, ranges.live.maxHeight);
|
|
19370
|
+
if (localHeight >= 0) {
|
|
19371
|
+
this.lastPresentHeight = localHeight;
|
|
19372
|
+
this.lastPresentHeightMsecs = now;
|
|
19373
|
+
return localHeight;
|
|
19374
|
+
}
|
|
19375
|
+
} catch (error) {
|
|
19376
|
+
this.log(`Unable to read the locally validated ChainTracks height: ${WalletError.fromUnknown(error).message}`);
|
|
19377
|
+
}
|
|
19378
|
+
throw new Error("No present-height source or locally validated headers are available.");
|
|
18629
19379
|
}
|
|
18630
19380
|
async currentHeight() {
|
|
18631
19381
|
return await this.getPresentHeight();
|
|
@@ -18676,7 +19426,7 @@ var Chaintracks = class {
|
|
|
18676
19426
|
for (const bulkIn of this.bulkIngestors) await bulkIn.setStorage(this.storage, this.log);
|
|
18677
19427
|
for (const liveIn of this.liveIngestors) await liveIn.setStorage(this.storage, this.log);
|
|
18678
19428
|
this.stopMainThread = false;
|
|
18679
|
-
for (const liveIngestor of this.liveIngestors) this.promises.push(this.runLiveIngestor(liveIngestor));
|
|
19429
|
+
for (const [index, liveIngestor] of this.liveIngestors.entries()) this.promises.push(this.runLiveIngestor(liveIngestor, index));
|
|
18680
19430
|
this.promises.push(this.mainThreadShiftLiveHeaders());
|
|
18681
19431
|
while (!this.available && this.startupError == null) await wait(100);
|
|
18682
19432
|
if (this.startupError != null) throw this.startupError;
|
|
@@ -18703,10 +19453,12 @@ var Chaintracks = class {
|
|
|
18703
19453
|
async listening() {
|
|
18704
19454
|
return await this.makeAvailable();
|
|
18705
19455
|
}
|
|
18706
|
-
async runLiveIngestor(liveIngestor) {
|
|
19456
|
+
async runLiveIngestor(liveIngestor, index) {
|
|
18707
19457
|
let restartCount = 0;
|
|
18708
19458
|
const name = liveIngestor.constructor.name;
|
|
19459
|
+
const source = this.sourceName("live", index, liveIngestor);
|
|
18709
19460
|
while (!this.stopMainThread) try {
|
|
19461
|
+
this.markSourceSuccess(source, "live");
|
|
18710
19462
|
await liveIngestor.startListening(this.liveHeaders);
|
|
18711
19463
|
if (this.stopMainThread) return;
|
|
18712
19464
|
restartCount++;
|
|
@@ -18717,6 +19469,7 @@ var Chaintracks = class {
|
|
|
18717
19469
|
if (this.stopMainThread) return;
|
|
18718
19470
|
restartCount++;
|
|
18719
19471
|
const e = WalletError.fromUnknown(error_);
|
|
19472
|
+
this.markSourceFailure(source, "live", e);
|
|
18720
19473
|
const waitMsecs = this.liveIngestorRestartWaitMsecs(restartCount);
|
|
18721
19474
|
this.log(`Live ingestor ${name} failed restart=${restartCount} retryMsecs=${waitMsecs}: ${e.stack ?? e.message}`);
|
|
18722
19475
|
await wait(waitMsecs);
|
|
@@ -18764,7 +19517,8 @@ var Chaintracks = class {
|
|
|
18764
19517
|
storage: this.storage.constructor.name,
|
|
18765
19518
|
bulkIngestors: this.bulkIngestors.map((bulkIngestor) => bulkIngestor.constructor.name),
|
|
18766
19519
|
liveIngestors: this.liveIngestors.map((liveIngestor) => liveIngestor.constructor.name),
|
|
18767
|
-
packages: []
|
|
19520
|
+
packages: [],
|
|
19521
|
+
sources: Array.from(this.sourceStatus.values()).map((status) => ({ ...status }))
|
|
18768
19522
|
};
|
|
18769
19523
|
}
|
|
18770
19524
|
async getHeaders(height, count) {
|
|
@@ -18847,26 +19601,30 @@ var Chaintracks = class {
|
|
|
18847
19601
|
let madeProgress = false;
|
|
18848
19602
|
let hadSuccess = false;
|
|
18849
19603
|
let done = false;
|
|
18850
|
-
for (const bulk of this.bulkIngestors)
|
|
18851
|
-
const
|
|
18852
|
-
|
|
18853
|
-
|
|
18854
|
-
|
|
18855
|
-
|
|
18856
|
-
|
|
18857
|
-
|
|
18858
|
-
|
|
18859
|
-
|
|
18860
|
-
|
|
18861
|
-
|
|
18862
|
-
|
|
18863
|
-
|
|
18864
|
-
|
|
19604
|
+
for (const [index, bulk] of this.bulkIngestors.entries()) {
|
|
19605
|
+
const source = this.sourceName("bulk", index, bulk);
|
|
19606
|
+
try {
|
|
19607
|
+
const beforeBulkMax = before.bulk.maxHeight;
|
|
19608
|
+
const beforeLiveRange = HeightRange.from(newLiveHeaders);
|
|
19609
|
+
const r = await bulk.synchronize(presentHeight, before, newLiveHeaders);
|
|
19610
|
+
hadSuccess = true;
|
|
19611
|
+
this.markSourceSuccess(source, "bulk");
|
|
19612
|
+
newLiveHeaders = r.liveHeaders;
|
|
19613
|
+
after = await this.storage.getAvailableHeightRanges();
|
|
19614
|
+
const added = after.bulk.above(before.bulk);
|
|
19615
|
+
const afterLiveRange = HeightRange.from(newLiveHeaders);
|
|
19616
|
+
if (after.bulk.maxHeight > beforeBulkMax || afterLiveRange.maxHeight > beforeLiveRange.maxHeight) madeProgress = true;
|
|
19617
|
+
before = after;
|
|
19618
|
+
this.log(`Bulk Ingestor: ${added.length} added with ${newLiveHeaders.length} live headers from ${bulk.constructor.name}`);
|
|
19619
|
+
if (r.done) {
|
|
19620
|
+
done = true;
|
|
19621
|
+
break;
|
|
19622
|
+
}
|
|
19623
|
+
} catch (error_) {
|
|
19624
|
+
const e = bulkSyncError = WalletError.fromUnknown(error_);
|
|
19625
|
+
this.markSourceFailure(source, "bulk", e);
|
|
19626
|
+
this.log(`bulk sync error: ${e.message}`);
|
|
18865
19627
|
}
|
|
18866
|
-
} catch (error_) {
|
|
18867
|
-
const e = bulkSyncError = WalletError.fromUnknown(error_);
|
|
18868
|
-
this.log(`bulk sync error: ${e.message}`);
|
|
18869
|
-
if (!this.available) break;
|
|
18870
19628
|
}
|
|
18871
19629
|
if (!this.available && bulkSyncError != null && !hadSuccess) this.startupError = bulkSyncError;
|
|
18872
19630
|
return {
|
|
@@ -18876,10 +19634,41 @@ var Chaintracks = class {
|
|
|
18876
19634
|
madeProgress
|
|
18877
19635
|
};
|
|
18878
19636
|
}
|
|
19637
|
+
sourceName(role, index, source) {
|
|
19638
|
+
return `${role}[${index}]:${source.constructor.name}`;
|
|
19639
|
+
}
|
|
19640
|
+
markSourceSuccess(name, role) {
|
|
19641
|
+
this.sourceStatus.set(name, {
|
|
19642
|
+
...this.sourceStatus.get(name),
|
|
19643
|
+
name,
|
|
19644
|
+
role,
|
|
19645
|
+
state: "healthy",
|
|
19646
|
+
lastSuccess: (/* @__PURE__ */ new Date()).toISOString(),
|
|
19647
|
+
error: void 0
|
|
19648
|
+
});
|
|
19649
|
+
}
|
|
19650
|
+
markSourceFailure(name, role, error) {
|
|
19651
|
+
this.sourceStatus.set(name, {
|
|
19652
|
+
...this.sourceStatus.get(name),
|
|
19653
|
+
name,
|
|
19654
|
+
role,
|
|
19655
|
+
state: "degraded",
|
|
19656
|
+
lastFailure: (/* @__PURE__ */ new Date()).toISOString(),
|
|
19657
|
+
error: error.message
|
|
19658
|
+
});
|
|
19659
|
+
}
|
|
18879
19660
|
async getMissingBlockHeader(hash) {
|
|
18880
|
-
for (const live of this.liveIngestors) {
|
|
18881
|
-
const
|
|
18882
|
-
|
|
19661
|
+
for (const [index, live] of this.liveIngestors.entries()) {
|
|
19662
|
+
const source = this.sourceName("live", index, live);
|
|
19663
|
+
try {
|
|
19664
|
+
const header = await live.getHeaderByHash(hash);
|
|
19665
|
+
this.markSourceSuccess(source, "live");
|
|
19666
|
+
if (header != null) return header;
|
|
19667
|
+
} catch (error) {
|
|
19668
|
+
const resolved = WalletError.fromUnknown(error);
|
|
19669
|
+
this.markSourceFailure(source, "live", resolved);
|
|
19670
|
+
this.log(`Header lookup source ${source} failed: ${resolved.message}`);
|
|
19671
|
+
}
|
|
18883
19672
|
}
|
|
18884
19673
|
}
|
|
18885
19674
|
invalidInsertHeaderResult(ihr) {
|
|
@@ -19212,6 +20001,9 @@ var GoChaintracksServiceClient = class {
|
|
|
19212
20001
|
chain;
|
|
19213
20002
|
baseUrl;
|
|
19214
20003
|
fetcher;
|
|
20004
|
+
requestTimeoutMsecs;
|
|
20005
|
+
reconnectWaitMsecs;
|
|
20006
|
+
reconnectWaitMaxMsecs;
|
|
19215
20007
|
subscriptions = /* @__PURE__ */ new Map();
|
|
19216
20008
|
nextSubscriptionId = 1;
|
|
19217
20009
|
constructor(chain, serviceUrl, options = {}) {
|
|
@@ -19225,6 +20017,15 @@ var GoChaintracksServiceClient = class {
|
|
|
19225
20017
|
}
|
|
19226
20018
|
this.baseUrl = `${base}${prefix}`;
|
|
19227
20019
|
this.fetcher = options.fetch ?? fetch;
|
|
20020
|
+
this.requestTimeoutMsecs = options.requestTimeoutMsecs ?? 3e4;
|
|
20021
|
+
this.reconnectWaitMsecs = options.reconnectWaitMsecs ?? 1e3;
|
|
20022
|
+
this.reconnectWaitMaxMsecs = options.reconnectWaitMaxMsecs ?? 6e4;
|
|
20023
|
+
for (const [name, value] of [
|
|
20024
|
+
["requestTimeoutMsecs", this.requestTimeoutMsecs],
|
|
20025
|
+
["reconnectWaitMsecs", this.reconnectWaitMsecs],
|
|
20026
|
+
["reconnectWaitMaxMsecs", this.reconnectWaitMaxMsecs]
|
|
20027
|
+
]) if (!Number.isSafeInteger(value) || value < 1) throw new Error(`${name} must be a positive integer.`);
|
|
20028
|
+
if (this.reconnectWaitMaxMsecs < this.reconnectWaitMsecs) throw new Error("reconnectWaitMaxMsecs must be greater than or equal to reconnectWaitMsecs.");
|
|
19228
20029
|
}
|
|
19229
20030
|
async currentHeight() {
|
|
19230
20031
|
return await this.getPresentHeight();
|
|
@@ -19234,12 +20035,8 @@ var GoChaintracksServiceClient = class {
|
|
|
19234
20035
|
return h != null && root === asString(h.merkleRoot);
|
|
19235
20036
|
}
|
|
19236
20037
|
async getChain() {
|
|
19237
|
-
|
|
19238
|
-
|
|
19239
|
-
return this.normalizeChain(r.network);
|
|
19240
|
-
} catch {
|
|
19241
|
-
return this.chain;
|
|
19242
|
-
}
|
|
20038
|
+
const r = await this.getJson("/network");
|
|
20039
|
+
return this.normalizeChain(typeof r === "string" ? r : r.network);
|
|
19243
20040
|
}
|
|
19244
20041
|
async getInfo() {
|
|
19245
20042
|
const tip = await this.findChainTipHeader();
|
|
@@ -19254,11 +20051,11 @@ var GoChaintracksServiceClient = class {
|
|
|
19254
20051
|
};
|
|
19255
20052
|
}
|
|
19256
20053
|
async getPresentHeight() {
|
|
19257
|
-
|
|
20054
|
+
const result = await this.getJson("/height");
|
|
20055
|
+
return typeof result === "number" ? result : result.height;
|
|
19258
20056
|
}
|
|
19259
20057
|
async getHeaders(height, count) {
|
|
19260
|
-
|
|
19261
|
-
return Buffer.from(bytes).toString("hex");
|
|
20058
|
+
return asString(await this.getBinary(`/headers.bin?height=${height}&count=${count}`));
|
|
19262
20059
|
}
|
|
19263
20060
|
async findChainTipHeader() {
|
|
19264
20061
|
return await this.getJson("/tip");
|
|
@@ -19314,7 +20111,7 @@ var GoChaintracksServiceClient = class {
|
|
|
19314
20111
|
async subscribe(type, path, onPayload) {
|
|
19315
20112
|
const id = `${type}-${this.nextSubscriptionId++}`;
|
|
19316
20113
|
const abort = new AbortController();
|
|
19317
|
-
const done = this.
|
|
20114
|
+
const done = this.runSseWithReconnect(path, abort.signal, onPayload);
|
|
19318
20115
|
this.subscriptions.set(id, {
|
|
19319
20116
|
id,
|
|
19320
20117
|
type,
|
|
@@ -19326,30 +20123,75 @@ var GoChaintracksServiceClient = class {
|
|
|
19326
20123
|
});
|
|
19327
20124
|
return id;
|
|
19328
20125
|
}
|
|
19329
|
-
async
|
|
19330
|
-
|
|
19331
|
-
|
|
19332
|
-
|
|
20126
|
+
async runSseWithReconnect(path, signal, onPayload) {
|
|
20127
|
+
let failures = 0;
|
|
20128
|
+
while (!signal.aborted) {
|
|
20129
|
+
try {
|
|
20130
|
+
failures = await this.runSse(path, signal, onPayload) ? 0 : failures + 1;
|
|
20131
|
+
} catch {
|
|
20132
|
+
if (signal.aborted) return;
|
|
20133
|
+
failures++;
|
|
20134
|
+
}
|
|
20135
|
+
const multiplier = Math.min(2 ** Math.max(0, failures - 1), 64);
|
|
20136
|
+
const delay = Math.min(this.reconnectWaitMsecs * multiplier, this.reconnectWaitMaxMsecs);
|
|
20137
|
+
await this.waitForReconnect(delay, signal);
|
|
20138
|
+
}
|
|
20139
|
+
}
|
|
20140
|
+
async waitForReconnect(msecs, signal) {
|
|
20141
|
+
if (signal.aborted || msecs <= 0) return;
|
|
20142
|
+
await new Promise((resolve) => {
|
|
20143
|
+
let timeout;
|
|
20144
|
+
const onAbort = () => done();
|
|
20145
|
+
const done = () => {
|
|
20146
|
+
clearTimeout(timeout);
|
|
20147
|
+
signal.removeEventListener("abort", onAbort);
|
|
20148
|
+
resolve();
|
|
20149
|
+
};
|
|
20150
|
+
timeout = setTimeout(done, msecs);
|
|
20151
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
19333
20152
|
});
|
|
19334
|
-
|
|
19335
|
-
|
|
19336
|
-
const
|
|
19337
|
-
const
|
|
19338
|
-
|
|
20153
|
+
}
|
|
20154
|
+
async runSse(path, signal, onPayload) {
|
|
20155
|
+
const controller = new AbortController();
|
|
20156
|
+
const onAbort = () => controller.abort();
|
|
20157
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
20158
|
+
const timeout = setTimeout(() => controller.abort(), this.requestTimeoutMsecs);
|
|
20159
|
+
let receivedEvent = false;
|
|
20160
|
+
const observePayload = (payload) => {
|
|
20161
|
+
receivedEvent = true;
|
|
20162
|
+
onPayload(payload);
|
|
20163
|
+
};
|
|
19339
20164
|
try {
|
|
19340
|
-
|
|
19341
|
-
|
|
19342
|
-
|
|
19343
|
-
|
|
19344
|
-
|
|
20165
|
+
const response = await this.fetcher(this.url(path), {
|
|
20166
|
+
headers: { Accept: "text/event-stream" },
|
|
20167
|
+
signal: controller.signal
|
|
20168
|
+
});
|
|
20169
|
+
clearTimeout(timeout);
|
|
20170
|
+
if (!response.ok) throw new Error(`GET ${this.url(path)} failed ${response.status} ${response.statusText}`);
|
|
20171
|
+
if (response.body == null) throw new Error(`GET ${this.url(path)} returned no response body`);
|
|
20172
|
+
const reader = response.body.getReader();
|
|
20173
|
+
const decoder = new TextDecoder();
|
|
20174
|
+
let buffer = "";
|
|
20175
|
+
try {
|
|
20176
|
+
for (;;) {
|
|
20177
|
+
const { done, value } = await reader.read();
|
|
20178
|
+
if (done) break;
|
|
20179
|
+
buffer += decoder.decode(value, { stream: true });
|
|
20180
|
+
buffer = this.processSseBuffer(buffer, observePayload);
|
|
20181
|
+
}
|
|
20182
|
+
buffer += decoder.decode();
|
|
20183
|
+
this.processSseBuffer(`${buffer}\n\n`, observePayload);
|
|
20184
|
+
} finally {
|
|
20185
|
+
reader.releaseLock();
|
|
19345
20186
|
}
|
|
19346
|
-
buffer += decoder.decode();
|
|
19347
|
-
this.processSseBuffer(`${buffer}\n\n`, onPayload);
|
|
19348
20187
|
} finally {
|
|
19349
|
-
|
|
20188
|
+
clearTimeout(timeout);
|
|
20189
|
+
signal.removeEventListener("abort", onAbort);
|
|
19350
20190
|
}
|
|
20191
|
+
return receivedEvent;
|
|
19351
20192
|
}
|
|
19352
20193
|
processSseBuffer(buffer, onPayload) {
|
|
20194
|
+
buffer = buffer.replaceAll("\r\n", "\n");
|
|
19353
20195
|
for (;;) {
|
|
19354
20196
|
const boundary = buffer.indexOf("\n\n");
|
|
19355
20197
|
if (boundary < 0) return buffer;
|
|
@@ -19368,32 +20210,51 @@ var GoChaintracksServiceClient = class {
|
|
|
19368
20210
|
return r;
|
|
19369
20211
|
}
|
|
19370
20212
|
async getJsonOrUndefined(path) {
|
|
19371
|
-
const response = await this.
|
|
20213
|
+
const response = await this.fetchWithTimeout(this.url(path), { headers: { Accept: "application/json" } });
|
|
19372
20214
|
if (response.status === 404) return void 0;
|
|
19373
20215
|
if (!response.ok) throw new Error(`GET ${this.url(path)} failed ${response.status} ${response.statusText}`);
|
|
19374
|
-
|
|
20216
|
+
const value = await response.json();
|
|
20217
|
+
if (value != null && typeof value === "object" && "status" in value) {
|
|
20218
|
+
const envelope = value;
|
|
20219
|
+
if (envelope.status === "success") return envelope.value;
|
|
20220
|
+
if (envelope.status === "error") throw new Error(envelope.description ?? `GET ${this.url(path)} failed`);
|
|
20221
|
+
}
|
|
20222
|
+
return value;
|
|
19375
20223
|
}
|
|
19376
20224
|
async getBinary(path) {
|
|
19377
|
-
const response = await this.
|
|
20225
|
+
const response = await this.fetchWithTimeout(this.url(path), { headers: { Accept: "application/octet-stream" } });
|
|
19378
20226
|
if (!response.ok) throw new Error(`GET ${this.url(path)} failed ${response.status} ${response.statusText}`);
|
|
19379
20227
|
return new Uint8Array(await response.arrayBuffer());
|
|
19380
20228
|
}
|
|
20229
|
+
async fetchWithTimeout(url, init) {
|
|
20230
|
+
const controller = new AbortController();
|
|
20231
|
+
const timeout = setTimeout(() => controller.abort(), this.requestTimeoutMsecs);
|
|
20232
|
+
try {
|
|
20233
|
+
return await this.fetcher(url, {
|
|
20234
|
+
...init,
|
|
20235
|
+
signal: controller.signal
|
|
20236
|
+
});
|
|
20237
|
+
} finally {
|
|
20238
|
+
clearTimeout(timeout);
|
|
20239
|
+
}
|
|
20240
|
+
}
|
|
19381
20241
|
url(path) {
|
|
19382
20242
|
return `${this.baseUrl}${path}`;
|
|
19383
20243
|
}
|
|
19384
20244
|
normalizeChain(network) {
|
|
19385
|
-
switch (network) {
|
|
20245
|
+
switch (network.trim().toLowerCase()) {
|
|
19386
20246
|
case "main":
|
|
19387
20247
|
case "mainnet": return "main";
|
|
19388
20248
|
case "test":
|
|
19389
20249
|
case "testnet": return "test";
|
|
20250
|
+
case "stn":
|
|
20251
|
+
case "scalingtestnet": return "stn";
|
|
19390
20252
|
case "ttn":
|
|
19391
20253
|
case "teratest":
|
|
19392
20254
|
case "teratestnet": return "ttn";
|
|
19393
20255
|
case "tstn":
|
|
19394
|
-
case "teranodescalingtestnet":
|
|
19395
|
-
|
|
19396
|
-
default: return this.chain;
|
|
20256
|
+
case "teranodescalingtestnet": return "tstn";
|
|
20257
|
+
default: throw new Error(`Unsupported ChainTracks upstream network '${network}'.`);
|
|
19397
20258
|
}
|
|
19398
20259
|
}
|
|
19399
20260
|
};
|
|
@@ -20726,31 +21587,45 @@ var ServiceCollection = class ServiceCollection {
|
|
|
20726
21587
|
//#endregion
|
|
20727
21588
|
//#region ../src/services/networkConfig.ts
|
|
20728
21589
|
/**
|
|
20729
|
-
* Runtime service-endpoint configuration for
|
|
21590
|
+
* Runtime service-endpoint configuration for Teranode networks that do not
|
|
21591
|
+
* have a public, operator-independent service endpoint.
|
|
20730
21592
|
*
|
|
20731
|
-
* Unlike `main`, `test`, and `ttn`, the tstn service endpoints are not public and must not be
|
|
21593
|
+
* Unlike `main`, `test`, and `ttn`, the stn/tstn service endpoints are not public and must not be
|
|
20732
21594
|
* hardcoded in this (public) source tree. They are supplied at runtime through environment
|
|
20733
21595
|
* variables:
|
|
20734
21596
|
*
|
|
21597
|
+
* STN_ARCADE_URL STN Arcade broadcaster / ARC endpoint base.
|
|
21598
|
+
* STN_CHAINTRACKS_URL STN ChainTracks service URL.
|
|
20735
21599
|
* TSTN_ARCADE_URL Arcade broadcaster / ARC endpoint base. Also the fallback host for
|
|
20736
21600
|
* ChainTracks when TSTN_CHAINTRACKS_URL is unset
|
|
20737
21601
|
* (`${TSTN_ARCADE_URL}/chaintracks/v1`, mirroring the ttn layout).
|
|
20738
21602
|
* TSTN_CHAINTRACKS_URL ChainTracks service URL.
|
|
20739
21603
|
*
|
|
20740
|
-
* tstn
|
|
20741
|
-
* WhatsOnChain
|
|
21604
|
+
* stn/tstn run only operator-configured Arcade and ChainTracks services; there is no
|
|
21605
|
+
* documented WhatsOnChain service for them, so no WhatsOnChain endpoint is configured and
|
|
20742
21606
|
* the WhatsOnChain-only lookups (raw tx, utxo status, txid status, script-hash history) are not
|
|
20743
|
-
* available on tstn.
|
|
21607
|
+
* available on stn/tstn.
|
|
20744
21608
|
*
|
|
20745
|
-
* `process` is accessed defensively so importing this module remains safe in
|
|
20746
|
-
*
|
|
20747
|
-
*
|
|
21609
|
+
* `process` is accessed defensively so importing this module remains safe in
|
|
21610
|
+
* browser bundles. Browser applications can still supply an explicit
|
|
21611
|
+
* ChaintracksClientApi without relying on environment variables.
|
|
20748
21612
|
*/
|
|
20749
21613
|
function readEnv(name) {
|
|
20750
21614
|
const value = (typeof process !== "undefined" ? process.env : void 0)?.[name];
|
|
20751
21615
|
return value != null && value.trim() !== "" ? value.trim() : void 0;
|
|
20752
21616
|
}
|
|
20753
|
-
|
|
21617
|
+
/** Credential-free public Arcade host for supported networks. */
|
|
21618
|
+
function publicArcadeUrl(chain) {
|
|
21619
|
+
switch (chain) {
|
|
21620
|
+
case "main": return "https://arcade-v2-us-1.bsvblockchain.tech";
|
|
21621
|
+
case "test": return "https://arcade-v2-testnet-us-1.bsvblockchain.tech";
|
|
21622
|
+
case "ttn": return "https://arcade-v2-ttn-us-1.bsvblockchain.tech";
|
|
21623
|
+
case "stn":
|
|
21624
|
+
case "tstn":
|
|
21625
|
+
case "mock": return;
|
|
21626
|
+
}
|
|
21627
|
+
}
|
|
21628
|
+
const stripTrailingSlash$1 = (url) => {
|
|
20754
21629
|
let end = url.length;
|
|
20755
21630
|
while (end > 0 && url[end - 1] === "/") end--;
|
|
20756
21631
|
return url.slice(0, end);
|
|
@@ -20759,6 +21634,10 @@ const stripTrailingSlash = (url) => {
|
|
|
20759
21634
|
function tstnArcadeUrl() {
|
|
20760
21635
|
return readEnv("TSTN_ARCADE_URL");
|
|
20761
21636
|
}
|
|
21637
|
+
/** Arcade broadcaster / ARC endpoint for stn, or `undefined` when unset. */
|
|
21638
|
+
function stnArcadeUrl() {
|
|
21639
|
+
return readEnv("STN_ARCADE_URL");
|
|
21640
|
+
}
|
|
20762
21641
|
/**
|
|
20763
21642
|
* ChainTracks service URL for tstn. Falls back to `${TSTN_ARCADE_URL}/chaintracks/v1` when
|
|
20764
21643
|
* `TSTN_CHAINTRACKS_URL` is unset (mirrors the ttn layout). Throws when neither is configured.
|
|
@@ -20767,20 +21646,53 @@ function tstnChaintracksUrl() {
|
|
|
20767
21646
|
const explicit = readEnv("TSTN_CHAINTRACKS_URL");
|
|
20768
21647
|
if (explicit != null) return explicit;
|
|
20769
21648
|
const arcade = tstnArcadeUrl();
|
|
20770
|
-
if (arcade != null) return `${stripTrailingSlash(arcade)}/chaintracks/v1`;
|
|
21649
|
+
if (arcade != null) return `${stripTrailingSlash$1(arcade)}/chaintracks/v1`;
|
|
20771
21650
|
throw new Error("tstn chain requires a ChainTracks URL: set TSTN_CHAINTRACKS_URL (or TSTN_ARCADE_URL) in the environment.");
|
|
20772
21651
|
}
|
|
21652
|
+
/**
|
|
21653
|
+
* ChainTracks service URL for stn. Falls back to the configured Arcade host's
|
|
21654
|
+
* legacy-compatible path when STN_CHAINTRACKS_URL is unset.
|
|
21655
|
+
*/
|
|
21656
|
+
function stnChaintracksUrl() {
|
|
21657
|
+
const explicit = readEnv("STN_CHAINTRACKS_URL");
|
|
21658
|
+
if (explicit != null) return explicit;
|
|
21659
|
+
const arcade = stnArcadeUrl();
|
|
21660
|
+
if (arcade != null) return `${stripTrailingSlash$1(arcade)}/chaintracks/v1`;
|
|
21661
|
+
throw new Error("stn chain requires a ChainTracks URL: set STN_CHAINTRACKS_URL (or STN_ARCADE_URL) in the environment.");
|
|
21662
|
+
}
|
|
20773
21663
|
//#endregion
|
|
20774
21664
|
//#region ../src/services/createDefaultWalletServicesOptions.ts
|
|
21665
|
+
function stripTrailingSlash(value) {
|
|
21666
|
+
let end = value.length;
|
|
21667
|
+
while (end > 0 && value[end - 1] === "/") end--;
|
|
21668
|
+
return value.slice(0, end);
|
|
21669
|
+
}
|
|
21670
|
+
function configuredChaintracksClient(chain, serviceUrl) {
|
|
21671
|
+
let path = "";
|
|
21672
|
+
try {
|
|
21673
|
+
path = stripTrailingSlash(new URL(serviceUrl).pathname);
|
|
21674
|
+
} catch {}
|
|
21675
|
+
if (path.endsWith("/v2")) return new GoChaintracksServiceClient(chain, serviceUrl);
|
|
21676
|
+
return new ChaintracksServiceClient(chain, serviceUrl);
|
|
21677
|
+
}
|
|
21678
|
+
/**
|
|
21679
|
+
* Returns the credential-free default ChainTracks client for a supported
|
|
21680
|
+
* public network, or an operator-configured client for stn/tstn.
|
|
21681
|
+
*/
|
|
21682
|
+
function createDefaultChaintracksClient(chain) {
|
|
21683
|
+
switch (chain) {
|
|
21684
|
+
case "main":
|
|
21685
|
+
case "test":
|
|
21686
|
+
case "ttn": return new GoChaintracksServiceClient(chain, arcadeDefaultUrl(chain), { apiPrefix: "/chaintracks/v2" });
|
|
21687
|
+
case "stn": return configuredChaintracksClient(chain, stnChaintracksUrl());
|
|
21688
|
+
case "tstn": return configuredChaintracksClient(chain, tstnChaintracksUrl());
|
|
21689
|
+
}
|
|
21690
|
+
}
|
|
20775
21691
|
function createDefaultWalletServicesOptions(...[chain, arcCallbackUrl, arcCallbackToken, taalArcApiKey, gorillaPoolArcApiKey, bitailsApiKey, deploymentId, chaintracks, arcadeUrl, arcadeApiKey, arcadeCallbackToken]) {
|
|
20776
21692
|
if (chain === "mock") throw new Error("createDefaultWalletServicesOptions does not support 'mock' chain. Use MockServices directly.");
|
|
20777
21693
|
deploymentId ||= `wallet-toolbox-${randomBytesHex(16)}`;
|
|
20778
|
-
let chaintracksUrl;
|
|
20779
|
-
if (chain === "ttn") chaintracksUrl = "https://arcade-v2-ttn-us-1.bsvblockchain.tech/chaintracks/v1";
|
|
20780
|
-
else if (chain === "tstn") chaintracksUrl = tstnChaintracksUrl();
|
|
20781
|
-
else chaintracksUrl = `https://${chain}net-chaintracks.babbage.systems`;
|
|
20782
21694
|
const chaintracksFiatExchangeRatesUrl = "https://mainnet-chaintracks.babbage.systems/getFiatExchangeRates";
|
|
20783
|
-
chaintracks ||=
|
|
21695
|
+
chaintracks ||= createDefaultChaintracksClient(chain);
|
|
20784
21696
|
const o = {
|
|
20785
21697
|
chain,
|
|
20786
21698
|
taalApiKey: void 0,
|
|
@@ -20838,14 +21750,15 @@ function createDefaultWalletServicesOptions(...[chain, arcCallbackUrl, arcCallba
|
|
|
20838
21750
|
}
|
|
20839
21751
|
/**
|
|
20840
21752
|
* Default Arcade (bsv-blockchain/arcade) endpoint per chain.
|
|
20841
|
-
* Returns undefined when no public default is known for the chain
|
|
21753
|
+
* Returns undefined when no public default is known for the chain.
|
|
20842
21754
|
*/
|
|
20843
21755
|
function arcadeDefaultUrl(chain) {
|
|
20844
21756
|
switch (chain) {
|
|
20845
|
-
case "main":
|
|
20846
|
-
case "
|
|
21757
|
+
case "main":
|
|
21758
|
+
case "test":
|
|
21759
|
+
case "ttn": return publicArcadeUrl(chain);
|
|
21760
|
+
case "stn": return stnArcadeUrl();
|
|
20847
21761
|
case "tstn": return tstnArcadeUrl();
|
|
20848
|
-
case "test": return;
|
|
20849
21762
|
case "mock": return;
|
|
20850
21763
|
}
|
|
20851
21764
|
}
|
|
@@ -20853,6 +21766,7 @@ function arcDefaultUrl(chain) {
|
|
|
20853
21766
|
switch (chain) {
|
|
20854
21767
|
case "main": return "https://arc.taal.com";
|
|
20855
21768
|
case "test": return "https://arc-test.taal.com";
|
|
21769
|
+
case "stn": return stnArcadeUrl() ?? "";
|
|
20856
21770
|
case "ttn": return "https://arcade-v2-ttn-us-1.bsvblockchain.tech/";
|
|
20857
21771
|
case "tstn": return tstnArcadeUrl() ?? "";
|
|
20858
21772
|
case "mock": return "";
|
|
@@ -22234,7 +23148,7 @@ var Services = class Services {
|
|
|
22234
23148
|
telemetry;
|
|
22235
23149
|
constructor(optionsOrChain) {
|
|
22236
23150
|
this.chain = typeof optionsOrChain === "string" ? optionsOrChain : optionsOrChain.chain;
|
|
22237
|
-
if (this.chain === "mock") throw new WERR_INVALID_PARAMETER("chain", "'main', 'test', 'ttn', or 'tstn'. Use MockServices for 'mock' chain.");
|
|
23151
|
+
if (this.chain === "mock") throw new WERR_INVALID_PARAMETER("chain", "'main', 'test', 'stn', 'ttn', or 'tstn'. Use MockServices for 'mock' chain.");
|
|
22238
23152
|
this.options = typeof optionsOrChain === "string" ? Services.createDefaultOptions(this.chain) : optionsOrChain;
|
|
22239
23153
|
this.telemetry = new Telemetry(this.options.telemetry);
|
|
22240
23154
|
this.whatsonchain = new WhatsOnChain(this.chain, { apiKey: this.options.whatsOnChainApiKey }, this);
|
|
@@ -22248,7 +23162,7 @@ var Services = class Services {
|
|
|
22248
23162
|
if (this.options.arcGorillaPoolUrl != null && this.options.arcGorillaPoolUrl !== "") this.arcGorillaPool = new ARC(this.options.arcGorillaPoolUrl, this.options.arcGorillaPoolConfig, "arcGorillaPool");
|
|
22249
23163
|
if (this.options.arcadeUrl != null && this.options.arcadeUrl !== "") this.arcade = new Arcade(this.options.arcadeUrl, this.options.arcadeConfig, "arcade");
|
|
22250
23164
|
const hasBitails = this.chain === "main" || this.chain === "test";
|
|
22251
|
-
const hasWhatsOnChain = this.chain
|
|
23165
|
+
const hasWhatsOnChain = this.chain === "main" || this.chain === "test";
|
|
22252
23166
|
if (hasBitails) this.bitails = new Bitails(this.chain, { apiKey: this.options.bitailsApiKey });
|
|
22253
23167
|
return {
|
|
22254
23168
|
hasBitails,
|
|
@@ -22962,9 +23876,22 @@ function classifyMerklePathResponse(status, statusText, retry) {
|
|
|
22962
23876
|
//#endregion
|
|
22963
23877
|
//#region ../src/services/providers/WhatsOnChain.ts
|
|
22964
23878
|
var WhatsOnChainNoServices = class extends SdkWhatsOnChain {
|
|
23879
|
+
requestGate;
|
|
22965
23880
|
constructor(chain = "main", config = {}) {
|
|
22966
23881
|
if (chain === "mock") throw new Error("WhatsOnChain does not support 'mock' chain. Use MockServices directly.");
|
|
22967
23882
|
super(chain, config);
|
|
23883
|
+
this.requestGate = config.requestGate;
|
|
23884
|
+
}
|
|
23885
|
+
async requestWithAnonymousAuthFallback(url, requestOptions) {
|
|
23886
|
+
await this.requestGate?.();
|
|
23887
|
+
const response = await this.httpClient.request(url, requestOptions);
|
|
23888
|
+
if (response.status !== 401 && response.status !== 403 || this.apiKey.trim() === "") return response;
|
|
23889
|
+
if (this.requestGate != null) await this.requestGate();
|
|
23890
|
+
else await wait(350);
|
|
23891
|
+
return await this.httpClient.request(url, {
|
|
23892
|
+
method: "GET",
|
|
23893
|
+
headers: { Accept: "application/json" }
|
|
23894
|
+
});
|
|
22968
23895
|
}
|
|
22969
23896
|
/**
|
|
22970
23897
|
* POST
|
|
@@ -23373,7 +24300,7 @@ var WhatsOnChainNoServices = class extends SdkWhatsOnChain {
|
|
|
23373
24300
|
};
|
|
23374
24301
|
const url = `${this.URL}/block/${hash}/header`;
|
|
23375
24302
|
for (let retry = 0; retry < 2; retry++) {
|
|
23376
|
-
const response = await this.
|
|
24303
|
+
const response = await this.requestWithAnonymousAuthFallback(url, requestOptions);
|
|
23377
24304
|
if (response.statusText === "Too Many Requests" && retry < 2) {
|
|
23378
24305
|
await wait(2e3);
|
|
23379
24306
|
continue;
|
|
@@ -23391,7 +24318,7 @@ var WhatsOnChainNoServices = class extends SdkWhatsOnChain {
|
|
|
23391
24318
|
};
|
|
23392
24319
|
const url = `${this.URL}/chain/info`;
|
|
23393
24320
|
for (let retry = 0; retry < 2; retry++) {
|
|
23394
|
-
const response = await this.
|
|
24321
|
+
const response = await this.requestWithAnonymousAuthFallback(url, requestOptions);
|
|
23395
24322
|
if (response.statusText === "Too Many Requests" && retry < 2) {
|
|
23396
24323
|
await wait(2e3);
|
|
23397
24324
|
continue;
|
|
@@ -23577,12 +24504,16 @@ var WhatsOnChainServices = class WhatsOnChainServices {
|
|
|
23577
24504
|
timeout: 3e4,
|
|
23578
24505
|
userAgent: "BabbageWhatsOnChainServices",
|
|
23579
24506
|
enableCache: true,
|
|
23580
|
-
chainInfoMsecs: 5e3
|
|
24507
|
+
chainInfoMsecs: 5e3,
|
|
24508
|
+
minRequestIntervalMsecs: 350
|
|
23581
24509
|
};
|
|
23582
24510
|
}
|
|
23583
24511
|
static chainInfo = [];
|
|
23584
24512
|
static chainInfoTime = [];
|
|
23585
24513
|
static chainInfoMsecs = [];
|
|
24514
|
+
static chainInfoPromise = {};
|
|
24515
|
+
static requestTail = Promise.resolve();
|
|
24516
|
+
static nextRequestMsecs = 0;
|
|
23586
24517
|
chain;
|
|
23587
24518
|
woc;
|
|
23588
24519
|
constructor(options) {
|
|
@@ -23591,7 +24522,8 @@ var WhatsOnChainServices = class WhatsOnChainServices {
|
|
|
23591
24522
|
apiKey: this.options.apiKey,
|
|
23592
24523
|
timeout: this.options.timeout,
|
|
23593
24524
|
userAgent: this.options.userAgent,
|
|
23594
|
-
enableCache: this.options.enableCache
|
|
24525
|
+
enableCache: this.options.enableCache,
|
|
24526
|
+
requestGate: async () => await this.waitForRateLimit()
|
|
23595
24527
|
};
|
|
23596
24528
|
this.chain = options.chain;
|
|
23597
24529
|
const chainInfoMsecs = WhatsOnChainServices.chainInfoMsecs;
|
|
@@ -23609,7 +24541,16 @@ var WhatsOnChainServices = class WhatsOnChainServices {
|
|
|
23609
24541
|
let update = chainInfo[this.chain] === void 0;
|
|
23610
24542
|
if (!update && chainInfoTime[this.chain] !== void 0) update = now.getTime() - chainInfoTime[this.chain].getTime() > chainInfoMsecs[this.chain];
|
|
23611
24543
|
if (update) {
|
|
23612
|
-
|
|
24544
|
+
let pending = WhatsOnChainServices.chainInfoPromise[this.chain];
|
|
24545
|
+
if (pending == null) {
|
|
24546
|
+
pending = this.woc.getChainInfo();
|
|
24547
|
+
WhatsOnChainServices.chainInfoPromise[this.chain] = pending;
|
|
24548
|
+
}
|
|
24549
|
+
try {
|
|
24550
|
+
chainInfo[this.chain] = await pending;
|
|
24551
|
+
} finally {
|
|
24552
|
+
if (WhatsOnChainServices.chainInfoPromise[this.chain] === pending) delete WhatsOnChainServices.chainInfoPromise[this.chain];
|
|
24553
|
+
}
|
|
23613
24554
|
chainInfoTime[this.chain] = now;
|
|
23614
24555
|
}
|
|
23615
24556
|
if (!chainInfo[this.chain]) throw new Error("Unexpected failure to update chainInfo.");
|
|
@@ -23627,10 +24568,12 @@ var WhatsOnChainServices = class WhatsOnChainServices {
|
|
|
23627
24568
|
*/
|
|
23628
24569
|
async getHeaders(fetch) {
|
|
23629
24570
|
fetch ||= new ChaintracksFetch();
|
|
24571
|
+
await this.waitForRateLimit();
|
|
23630
24572
|
return await fetch.fetchJson(`https://api.whatsonchain.com/v1/bsv/${this.chain}/block/headers`);
|
|
23631
24573
|
}
|
|
23632
24574
|
async getHeaderByteFileLinks(neededRange, fetch) {
|
|
23633
24575
|
fetch ||= new ChaintracksFetch();
|
|
24576
|
+
await this.waitForRateLimit();
|
|
23634
24577
|
const files = await fetch.fetchJson(`https://api.whatsonchain.com/v1/bsv/${this.chain}/block/headers/resources`);
|
|
23635
24578
|
const r = [];
|
|
23636
24579
|
let range;
|
|
@@ -23643,6 +24586,21 @@ var WhatsOnChainServices = class WhatsOnChainServices {
|
|
|
23643
24586
|
}
|
|
23644
24587
|
return r;
|
|
23645
24588
|
}
|
|
24589
|
+
async waitForRateLimit() {
|
|
24590
|
+
let release;
|
|
24591
|
+
const previous = WhatsOnChainServices.requestTail;
|
|
24592
|
+
WhatsOnChainServices.requestTail = new Promise((resolve) => {
|
|
24593
|
+
release = resolve;
|
|
24594
|
+
});
|
|
24595
|
+
await previous;
|
|
24596
|
+
try {
|
|
24597
|
+
const delay = Math.max(0, WhatsOnChainServices.nextRequestMsecs - Date.now());
|
|
24598
|
+
if (delay > 0) await wait(delay);
|
|
24599
|
+
WhatsOnChainServices.nextRequestMsecs = Date.now() + (this.options.minRequestIntervalMsecs ?? 350);
|
|
24600
|
+
} finally {
|
|
24601
|
+
release();
|
|
24602
|
+
}
|
|
24603
|
+
}
|
|
23646
24604
|
};
|
|
23647
24605
|
function wocGetHeadersHeaderToBlockHeader(h) {
|
|
23648
24606
|
const bits = typeof h.bits === "string" ? Number.parseInt(h.bits, 16) : h.bits;
|
|
@@ -23703,6 +24661,51 @@ var BulkIngestorWhatsOnChainCdn = class extends BulkIngestorBase {
|
|
|
23703
24661
|
}
|
|
23704
24662
|
};
|
|
23705
24663
|
//#endregion
|
|
24664
|
+
//#region ../src/services/chaintracker/chaintracks/Ingest/BulkIngestorChaintracks.ts
|
|
24665
|
+
/**
|
|
24666
|
+
* Uses a go-chaintracks/Arcade-compatible service as a validated bulk source.
|
|
24667
|
+
* Retrieved bytes still pass through ChainTracks' local serialization, hash,
|
|
24668
|
+
* continuity, and genesis checks before storage.
|
|
24669
|
+
*/
|
|
24670
|
+
var BulkIngestorChaintracks = class extends BulkIngestorBase {
|
|
24671
|
+
chaintracks;
|
|
24672
|
+
maxHeadersPerRequest;
|
|
24673
|
+
networkChecked = false;
|
|
24674
|
+
constructor(options) {
|
|
24675
|
+
super(options);
|
|
24676
|
+
this.chaintracks = options.chaintracks;
|
|
24677
|
+
this.maxHeadersPerRequest = options.maxHeadersPerRequest ?? 1e3;
|
|
24678
|
+
if (!Number.isInteger(this.maxHeadersPerRequest) || this.maxHeadersPerRequest < 1) throw new Error("maxHeadersPerRequest must be a positive integer.");
|
|
24679
|
+
}
|
|
24680
|
+
async getPresentHeight() {
|
|
24681
|
+
await this.ensureNetwork();
|
|
24682
|
+
return await this.chaintracks.getPresentHeight();
|
|
24683
|
+
}
|
|
24684
|
+
async fetchHeaders(_before, fetchRange, bulkRange, priorLiveHeaders) {
|
|
24685
|
+
if (fetchRange.isEmpty) return priorLiveHeaders;
|
|
24686
|
+
await this.ensureNetwork();
|
|
24687
|
+
let liveHeaders = priorLiveHeaders;
|
|
24688
|
+
let height = fetchRange.minHeight;
|
|
24689
|
+
while (height <= fetchRange.maxHeight) {
|
|
24690
|
+
const requested = Math.min(this.maxHeadersPerRequest, fetchRange.maxHeight - height + 1);
|
|
24691
|
+
const bytes = asUint8Array(await this.chaintracks.getHeaders(height, requested));
|
|
24692
|
+
if (bytes.length === 0) throw new Error(`ChainTracks upstream returned no headers at height ${height}.`);
|
|
24693
|
+
if (bytes.length % 80 !== 0 || bytes.length > requested * 80) throw new Error(`ChainTracks upstream returned ${bytes.length} bytes for ${requested} headers at height ${height}.`);
|
|
24694
|
+
const headers = deserializeBlockHeaders(height, bytes);
|
|
24695
|
+
liveHeaders = await this.storage().addBulkHeaders(headers, bulkRange, liveHeaders);
|
|
24696
|
+
height += headers.length;
|
|
24697
|
+
if (headers.length < requested && height <= fetchRange.maxHeight) throw new Error(`ChainTracks upstream returned ${headers.length} of ${requested} headers at height ${height - headers.length}.`);
|
|
24698
|
+
}
|
|
24699
|
+
return liveHeaders;
|
|
24700
|
+
}
|
|
24701
|
+
async ensureNetwork() {
|
|
24702
|
+
if (this.networkChecked) return;
|
|
24703
|
+
const actual = await this.chaintracks.getChain();
|
|
24704
|
+
if (actual !== this.chain) throw new Error(`ChainTracks upstream network '${actual}' does not match configured chain '${this.chain}'.`);
|
|
24705
|
+
this.networkChecked = true;
|
|
24706
|
+
}
|
|
24707
|
+
};
|
|
24708
|
+
//#endregion
|
|
23706
24709
|
//#region ../src/services/chaintracker/chaintracks/Ingest/LiveIngestorWhatsOnChainPoll.ts
|
|
23707
24710
|
/**
|
|
23708
24711
|
* Reports new headers by polling periodically.
|
|
@@ -23809,9 +24812,17 @@ var LiveIngestorChaintracksSSE = class extends LiveIngestorBase {
|
|
|
23809
24812
|
}
|
|
23810
24813
|
async startListening(liveHeaders) {
|
|
23811
24814
|
this.stopped = false;
|
|
23812
|
-
|
|
24815
|
+
const actual = await this.options.chaintracks.getChain();
|
|
24816
|
+
if (actual !== this.chain) throw new Error(`ChainTracks upstream network '${actual}' does not match configured chain '${this.chain}'.`);
|
|
24817
|
+
if (this.stopped) return;
|
|
24818
|
+
const subscriptionId = await this.options.chaintracks.subscribeHeaders((header) => {
|
|
23813
24819
|
if (!this.stopped) liveHeaders.push(header);
|
|
23814
24820
|
});
|
|
24821
|
+
if (this.stopped) {
|
|
24822
|
+
await this.options.chaintracks.unsubscribe(subscriptionId);
|
|
24823
|
+
return;
|
|
24824
|
+
}
|
|
24825
|
+
this.subscriptionId = subscriptionId;
|
|
23815
24826
|
await new Promise((resolve) => {
|
|
23816
24827
|
this.resolveStopped = resolve;
|
|
23817
24828
|
if (this.stopped) resolve();
|
|
@@ -23824,7 +24835,9 @@ var LiveIngestorChaintracksSSE = class extends LiveIngestorBase {
|
|
|
23824
24835
|
if (subscriptionId != null) this.options.chaintracks.unsubscribe(subscriptionId).catch((e) => {
|
|
23825
24836
|
this.log(`LiveIngestorChaintracksSSE unsubscribe failed: ${e}`);
|
|
23826
24837
|
});
|
|
23827
|
-
this.resolveStopped
|
|
24838
|
+
const resolveStopped = this.resolveStopped;
|
|
24839
|
+
this.resolveStopped = void 0;
|
|
24840
|
+
resolveStopped?.();
|
|
23828
24841
|
}
|
|
23829
24842
|
async shutdown() {
|
|
23830
24843
|
this.stopListening();
|
|
@@ -24520,6 +25533,27 @@ var ChaintracksStorageNoDb = class ChaintracksStorageNoDb extends ChaintracksSto
|
|
|
24520
25533
|
tipHeaderId: 0,
|
|
24521
25534
|
hashToHeaderId: /* @__PURE__ */ new Map()
|
|
24522
25535
|
};
|
|
25536
|
+
static stnData = {
|
|
25537
|
+
chain: "stn",
|
|
25538
|
+
liveHeaders: /* @__PURE__ */ new Map(),
|
|
25539
|
+
maxHeaderId: 0,
|
|
25540
|
+
tipHeaderId: 0,
|
|
25541
|
+
hashToHeaderId: /* @__PURE__ */ new Map()
|
|
25542
|
+
};
|
|
25543
|
+
static ttnData = {
|
|
25544
|
+
chain: "ttn",
|
|
25545
|
+
liveHeaders: /* @__PURE__ */ new Map(),
|
|
25546
|
+
maxHeaderId: 0,
|
|
25547
|
+
tipHeaderId: 0,
|
|
25548
|
+
hashToHeaderId: /* @__PURE__ */ new Map()
|
|
25549
|
+
};
|
|
25550
|
+
static tstnData = {
|
|
25551
|
+
chain: "tstn",
|
|
25552
|
+
liveHeaders: /* @__PURE__ */ new Map(),
|
|
25553
|
+
maxHeaderId: 0,
|
|
25554
|
+
tipHeaderId: 0,
|
|
25555
|
+
hashToHeaderId: /* @__PURE__ */ new Map()
|
|
25556
|
+
};
|
|
24523
25557
|
constructor(options) {
|
|
24524
25558
|
super(options);
|
|
24525
25559
|
}
|
|
@@ -24527,10 +25561,11 @@ var ChaintracksStorageNoDb = class ChaintracksStorageNoDb extends ChaintracksSto
|
|
|
24527
25561
|
async getData() {
|
|
24528
25562
|
switch (this.chain) {
|
|
24529
25563
|
case "main": return ChaintracksStorageNoDb.mainData;
|
|
24530
|
-
case "test":
|
|
24531
|
-
case "
|
|
24532
|
-
case "
|
|
24533
|
-
|
|
25564
|
+
case "test": return ChaintracksStorageNoDb.testData;
|
|
25565
|
+
case "stn": return ChaintracksStorageNoDb.stnData;
|
|
25566
|
+
case "ttn": return ChaintracksStorageNoDb.ttnData;
|
|
25567
|
+
case "tstn": return ChaintracksStorageNoDb.tstnData;
|
|
25568
|
+
default: throw new WERR_INVALID_PARAMETER("chain", `'main', 'test', 'stn', 'ttn', or 'tstn'. '${this.chain}' is unsupported.`);
|
|
24534
25569
|
}
|
|
24535
25570
|
}
|
|
24536
25571
|
async deleteLiveBlockHeaders() {
|
|
@@ -25119,7 +26154,7 @@ var ChaintracksStorageIdb = class extends ChaintracksStorageBase {
|
|
|
25119
26154
|
//#endregion
|
|
25120
26155
|
//#region ../src/services/chaintracker/chaintracks/configureChaintracksIngestors.ts
|
|
25121
26156
|
function resolveDefaultChaintracksArguments(args) {
|
|
25122
|
-
const [chain, whatsonchainApiKey = "", maxPerFile = 1e5, maxRetained = 2, fetch = new ChaintracksFetch(), cdnUrl = "https://cdn.projectbabbage.com/blockheaders/", liveHeightThreshold = 2e3, reorgHeightThreshold = 400, bulkMigrationChunkSize = 500, batchInsertLimit = 400, addLiveRecursionLimit = 36] = args;
|
|
26157
|
+
const [chain, whatsonchainApiKey = "", maxPerFile = 1e5, maxRetained = 2, fetch = new ChaintracksFetch(), cdnUrl = chain === "main" || chain === "test" ? "https://cdn.projectbabbage.com/blockheaders/" : "", liveHeightThreshold = 2e3, reorgHeightThreshold = 400, bulkMigrationChunkSize = 500, batchInsertLimit = 400, addLiveRecursionLimit = 36, sources = {}] = args;
|
|
25123
26158
|
return {
|
|
25124
26159
|
chain,
|
|
25125
26160
|
whatsonchainApiKey,
|
|
@@ -25131,11 +26166,12 @@ function resolveDefaultChaintracksArguments(args) {
|
|
|
25131
26166
|
reorgHeightThreshold,
|
|
25132
26167
|
bulkMigrationChunkSize,
|
|
25133
26168
|
batchInsertLimit,
|
|
25134
|
-
addLiveRecursionLimit
|
|
26169
|
+
addLiveRecursionLimit,
|
|
26170
|
+
sources
|
|
25135
26171
|
};
|
|
25136
26172
|
}
|
|
25137
26173
|
function toDefaultChaintracksArguments(params) {
|
|
25138
|
-
|
|
26174
|
+
const args = [
|
|
25139
26175
|
params.chain,
|
|
25140
26176
|
params.whatsonchainApiKey,
|
|
25141
26177
|
params.maxPerFile,
|
|
@@ -25148,6 +26184,8 @@ function toDefaultChaintracksArguments(params) {
|
|
|
25148
26184
|
params.batchInsertLimit,
|
|
25149
26185
|
params.addLiveRecursionLimit
|
|
25150
26186
|
];
|
|
26187
|
+
if (Object.keys(params.sources).length > 0) args.push(params.sources);
|
|
26188
|
+
return args;
|
|
25151
26189
|
}
|
|
25152
26190
|
function createDefaultBulkFileDataManager(params) {
|
|
25153
26191
|
return new BulkFileDataManager({
|
|
@@ -25190,7 +26228,7 @@ function createAndStartDefaultChaintracks(args, createOptions) {
|
|
|
25190
26228
|
* The caller is responsible for providing the storage implementation.
|
|
25191
26229
|
*/
|
|
25192
26230
|
function buildChaintracksOptionsWithIngestors(params, storage) {
|
|
25193
|
-
const { chain, whatsonchainApiKey, maxPerFile, fetch, cdnUrl, addLiveRecursionLimit } = params;
|
|
26231
|
+
const { chain, whatsonchainApiKey, maxPerFile, fetch, cdnUrl, addLiveRecursionLimit, sources } = params;
|
|
25194
26232
|
const co = {
|
|
25195
26233
|
chain,
|
|
25196
26234
|
storage,
|
|
@@ -25201,35 +26239,58 @@ function buildChaintracksOptionsWithIngestors(params, storage) {
|
|
|
25201
26239
|
readonly: false
|
|
25202
26240
|
};
|
|
25203
26241
|
const jsonResource = `${chain}NetBlockHeaders.json`;
|
|
25204
|
-
|
|
25205
|
-
|
|
25206
|
-
|
|
25207
|
-
|
|
25208
|
-
|
|
25209
|
-
|
|
25210
|
-
|
|
25211
|
-
|
|
25212
|
-
|
|
25213
|
-
|
|
25214
|
-
|
|
25215
|
-
|
|
25216
|
-
|
|
25217
|
-
|
|
25218
|
-
|
|
25219
|
-
|
|
25220
|
-
|
|
25221
|
-
|
|
25222
|
-
|
|
25223
|
-
|
|
25224
|
-
|
|
25225
|
-
|
|
25226
|
-
|
|
25227
|
-
|
|
25228
|
-
|
|
25229
|
-
|
|
25230
|
-
|
|
26242
|
+
if (!sources.disableCdn && cdnUrl !== "") {
|
|
26243
|
+
const bulkCdnOptions = {
|
|
26244
|
+
chain,
|
|
26245
|
+
jsonResource,
|
|
26246
|
+
fetch,
|
|
26247
|
+
cdnUrl,
|
|
26248
|
+
maxPerFile
|
|
26249
|
+
};
|
|
26250
|
+
co.bulkIngestors.push(new BulkIngestorCDNBabbage(bulkCdnOptions));
|
|
26251
|
+
}
|
|
26252
|
+
const chaintracksSource = sources.chaintracks ?? (sources.disableChaintracks ? void 0 : createPublicChaintracksSource(chain));
|
|
26253
|
+
if (chaintracksSource != null) {
|
|
26254
|
+
co.bulkIngestors.push(new BulkIngestorChaintracks({
|
|
26255
|
+
chain,
|
|
26256
|
+
jsonResource,
|
|
26257
|
+
chaintracks: chaintracksSource,
|
|
26258
|
+
maxHeadersPerRequest: sources.remoteMaxHeadersPerRequest
|
|
26259
|
+
}));
|
|
26260
|
+
co.liveIngestors.push(new LiveIngestorChaintracksSSE({
|
|
26261
|
+
chain,
|
|
26262
|
+
chaintracks: chaintracksSource
|
|
26263
|
+
}));
|
|
26264
|
+
}
|
|
26265
|
+
if ((chain === "main" || chain === "test") && !sources.disableWhatsOnChain) {
|
|
26266
|
+
const wocOptions = {
|
|
26267
|
+
chain,
|
|
26268
|
+
apiKey: whatsonchainApiKey,
|
|
26269
|
+
timeout: 3e4,
|
|
26270
|
+
userAgent: "BabbageWhatsOnChainServices",
|
|
26271
|
+
enableCache: true,
|
|
26272
|
+
chainInfoMsecs: 5e3
|
|
26273
|
+
};
|
|
26274
|
+
const bulkOptions = {
|
|
26275
|
+
...wocOptions,
|
|
26276
|
+
jsonResource,
|
|
26277
|
+
idleWait: 5e3
|
|
26278
|
+
};
|
|
26279
|
+
co.bulkIngestors.push(new BulkIngestorWhatsOnChainCdn(bulkOptions));
|
|
26280
|
+
const liveOptions = {
|
|
26281
|
+
...wocOptions,
|
|
26282
|
+
idleWait: 1e5
|
|
26283
|
+
};
|
|
26284
|
+
co.liveIngestors.push(new LiveIngestorWhatsOnChainPoll(liveOptions));
|
|
26285
|
+
}
|
|
26286
|
+
if (co.bulkIngestors.length === 0 || co.liveIngestors.length === 0) throw new Error(`ChainTracks ${chain} requires at least one bulk and live source. Configure sources.chaintracks for Teranode networks.`);
|
|
25231
26287
|
return co;
|
|
25232
26288
|
}
|
|
26289
|
+
function createPublicChaintracksSource(chain) {
|
|
26290
|
+
const serviceUrl = publicArcadeUrl(chain);
|
|
26291
|
+
if (serviceUrl == null) return void 0;
|
|
26292
|
+
return new GoChaintracksServiceClient(chain, serviceUrl, { apiPrefix: "/chaintracks/v2" });
|
|
26293
|
+
}
|
|
25233
26294
|
//#endregion
|
|
25234
26295
|
//#region ../src/services/chaintracker/chaintracks/createDefaultNoDbChaintracksOptions.ts
|
|
25235
26296
|
function createDefaultNoDbChaintracksOptions(...args) {
|
|
@@ -27958,7 +29019,7 @@ function isValidProfile(value) {
|
|
|
27958
29019
|
return typeof profile.name === "string" && profile.name.length > 0 && profile.name.length <= 250 && isByteArrayOfLength(profile.id, 16) && isByteArrayOfLength(profile.primaryPad, 32) && isByteArrayOfLength(profile.privilegedPad, 32) && typeof profile.createdAt === "number" && Number.isFinite(profile.createdAt) && profile.createdAt >= 0;
|
|
27959
29020
|
}
|
|
27960
29021
|
/**
|
|
27961
|
-
* Raised when UMP
|
|
29022
|
+
* Raised when a UMP lookup yields neither a verified token nor a clean empty response.
|
|
27962
29023
|
*
|
|
27963
29024
|
* Callers must offer retry/recovery rather than treating this error as a new
|
|
27964
29025
|
* account. Diagnostics contain counts only and never hashes, keys, or tokens.
|
|
@@ -28056,49 +29117,119 @@ var OverlayUMPTokenInteractor = class {
|
|
|
28056
29117
|
throw new UMPTokenLookupError("lookup-unavailable", diagnostics, { cause: error });
|
|
28057
29118
|
}
|
|
28058
29119
|
const diagnostics = this.toLookupDiagnostics(resolution);
|
|
28059
|
-
if (resolution.answer.outputs.length === 0) {
|
|
28060
|
-
if (!(resolution.progress.isFinal && resolution.progress.hostCount > 0 && resolution.progress.completedHosts === resolution.progress.hostCount && resolution.progress.successfulHosts === resolution.progress.hostCount && resolution.progress.emptyHosts === resolution.progress.hostCount && resolution.progress.failedHosts === 0 && resolution.progress.rejectedHosts === 0 && resolution.progress.freeformHosts === 0)) {
|
|
28061
|
-
this.captureLookupFailure(lookupKind, "lookup-incomplete", diagnostics, startedAt);
|
|
28062
|
-
throw new UMPTokenLookupError("lookup-incomplete", diagnostics);
|
|
28063
|
-
}
|
|
28064
|
-
this.telemetry.capture({
|
|
28065
|
-
name: "wallet-toolbox.ump.lookup.completed",
|
|
28066
|
-
component: "wallet-toolbox.ump",
|
|
28067
|
-
severity: "info",
|
|
28068
|
-
correlationId: diagnostics.correlationId,
|
|
28069
|
-
attributes: {
|
|
28070
|
-
lookupKind,
|
|
28071
|
-
result: "not-found",
|
|
28072
|
-
durationMs: Date.now() - startedAt,
|
|
28073
|
-
...this.lookupDiagnosticAttributes(diagnostics)
|
|
28074
|
-
}
|
|
28075
|
-
});
|
|
28076
|
-
return;
|
|
28077
|
-
}
|
|
28078
29120
|
const tokens = this.parseLookupAnswers(resolution.answer);
|
|
28079
29121
|
const expectedHash = question.query[lookupKind === "presentation" ? "presentationHash" : "recoveryHash"].toLowerCase();
|
|
28080
|
-
|
|
28081
|
-
|
|
28082
|
-
|
|
28083
|
-
|
|
28084
|
-
|
|
29122
|
+
const matchingTokens = tokens.filter((token) => Utils.toHex(lookupKind === "presentation" ? token.presentationHash : token.recoveryHash).toLowerCase() === expectedHash);
|
|
29123
|
+
if (matchingTokens.length > 1) {
|
|
29124
|
+
const newest = this.resolveNewestToken(matchingTokens, resolution.answer.outputs);
|
|
29125
|
+
if (newest != null) {
|
|
29126
|
+
this.captureLookupCompleted(lookupKind, "found", diagnostics, startedAt, { supersededTokens: matchingTokens.length - 1 });
|
|
29127
|
+
return newest;
|
|
29128
|
+
}
|
|
28085
29129
|
const reason = "token-ambiguous";
|
|
28086
29130
|
this.captureLookupFailure(lookupKind, reason, diagnostics, startedAt);
|
|
28087
29131
|
throw new UMPTokenLookupError(reason, diagnostics);
|
|
28088
29132
|
}
|
|
28089
|
-
|
|
28090
|
-
|
|
28091
|
-
|
|
28092
|
-
|
|
28093
|
-
|
|
28094
|
-
|
|
28095
|
-
|
|
28096
|
-
|
|
28097
|
-
|
|
28098
|
-
|
|
29133
|
+
if (matchingTokens.length === 1) {
|
|
29134
|
+
this.captureLookupCompleted(lookupKind, "found", diagnostics, startedAt);
|
|
29135
|
+
return matchingTokens[0];
|
|
29136
|
+
}
|
|
29137
|
+
if (resolution.progress.emptyHosts > 0) {
|
|
29138
|
+
this.captureLookupCompleted(lookupKind, "not-found", diagnostics, startedAt);
|
|
29139
|
+
return;
|
|
29140
|
+
}
|
|
29141
|
+
const reason = resolution.answer.outputs.length > 0 ? "token-malformed" : "lookup-incomplete";
|
|
29142
|
+
this.captureLookupFailure(lookupKind, reason, diagnostics, startedAt);
|
|
29143
|
+
throw new UMPTokenLookupError(reason, diagnostics);
|
|
29144
|
+
}
|
|
29145
|
+
/**
|
|
29146
|
+
* Picks the newest rendition among distinct verified tokens, when possible.
|
|
29147
|
+
*
|
|
29148
|
+
* The on-chain UMP protocol expresses token updates by consumption: the
|
|
29149
|
+
* transaction creating a new rendition spends the previous rendition's
|
|
29150
|
+
* outpoint (there is no rendition counter field in the current format).
|
|
29151
|
+
* A candidate is therefore superseded when any other candidate's ancestry
|
|
29152
|
+
* (available from its BEEF) spends the candidate's outpoint.
|
|
29153
|
+
*
|
|
29154
|
+
* @returns The single unsuperseded candidate, or undefined when supersession
|
|
29155
|
+
* cannot be established for every stale candidate (e.g. forked tokens).
|
|
29156
|
+
*/
|
|
29157
|
+
resolveNewestToken(matchingTokens, outputs) {
|
|
29158
|
+
const candidates = /* @__PURE__ */ new Map();
|
|
29159
|
+
for (const token of matchingTokens) {
|
|
29160
|
+
if (token.currentOutpoint == null) return void 0;
|
|
29161
|
+
candidates.set(token.currentOutpoint, token);
|
|
29162
|
+
}
|
|
29163
|
+
const evidenceByCandidate = /* @__PURE__ */ new Map();
|
|
29164
|
+
for (const output of outputs) try {
|
|
29165
|
+
const tx = Transaction.fromBEEF(output.beef);
|
|
29166
|
+
const outpoint = `${tx.id("hex")}.${output.outputIndex}`;
|
|
29167
|
+
if (!candidates.has(outpoint)) continue;
|
|
29168
|
+
const evidence = evidenceByCandidate.get(outpoint) ?? {
|
|
29169
|
+
txs: [],
|
|
29170
|
+
spent: /* @__PURE__ */ new Set()
|
|
29171
|
+
};
|
|
29172
|
+
evidence.txs.push(tx);
|
|
29173
|
+
this.collectSpentOutpoints(tx, evidence.spent, /* @__PURE__ */ new Set());
|
|
29174
|
+
evidenceByCandidate.set(outpoint, evidence);
|
|
29175
|
+
} catch {}
|
|
29176
|
+
if (evidenceByCandidate.size !== candidates.size) return void 0;
|
|
29177
|
+
const survivors = [...candidates.keys()].filter((outpoint) => ![...evidenceByCandidate.entries()].some(([other, { spent }]) => other !== outpoint && spent.has(outpoint)));
|
|
29178
|
+
if (survivors.length === 1) return candidates.get(survivors[0]);
|
|
29179
|
+
const provenContinuations = survivors.filter((outpoint) => {
|
|
29180
|
+
const evidence = evidenceByCandidate.get(outpoint);
|
|
29181
|
+
const token = candidates.get(outpoint);
|
|
29182
|
+
return evidence != null && token != null && evidence.txs.some((tx) => this.consumesSameIdentityToken(tx, token));
|
|
29183
|
+
});
|
|
29184
|
+
if (provenContinuations.length !== 1) return void 0;
|
|
29185
|
+
return candidates.get(provenContinuations[0]);
|
|
29186
|
+
}
|
|
29187
|
+
/**
|
|
29188
|
+
* Whether `tx` spends an input whose source output (available in the BEEF)
|
|
29189
|
+
* decodes as a UMP token sharing the candidate's presentation or recovery
|
|
29190
|
+
* hash — on-chain proof that the candidate is an update of a same-identity
|
|
29191
|
+
* predecessor rather than an independently minted token.
|
|
29192
|
+
*/
|
|
29193
|
+
consumesSameIdentityToken(tx, token) {
|
|
29194
|
+
const presentationHash = Utils.toHex(token.presentationHash);
|
|
29195
|
+
const recoveryHash = Utils.toHex(token.recoveryHash);
|
|
29196
|
+
for (const input of tx.inputs) {
|
|
29197
|
+
const source = input.sourceTransaction;
|
|
29198
|
+
if (source == null || input.sourceOutputIndex == null) continue;
|
|
29199
|
+
const sourceOutput = source.outputs[input.sourceOutputIndex];
|
|
29200
|
+
if (sourceOutput == null) continue;
|
|
29201
|
+
try {
|
|
29202
|
+
const decoded = PushDrop.decode(sourceOutput.lockingScript);
|
|
29203
|
+
if (decoded.fields == null) continue;
|
|
29204
|
+
const fields = stripVerifiedPushDropSignature(decoded.fields, decoded.lockingPublicKey);
|
|
29205
|
+
if (fields.length < 11 || fields[6]?.length !== 32 || fields[7]?.length !== 32) continue;
|
|
29206
|
+
if (Utils.toHex(fields[6]) === presentationHash || Utils.toHex(fields[7]) === recoveryHash) return true;
|
|
29207
|
+
} catch {
|
|
29208
|
+
continue;
|
|
28099
29209
|
}
|
|
28100
|
-
}
|
|
28101
|
-
return
|
|
29210
|
+
}
|
|
29211
|
+
return false;
|
|
29212
|
+
}
|
|
29213
|
+
/**
|
|
29214
|
+
* Accumulates every outpoint spent by `tx` and by the ancestor transactions
|
|
29215
|
+
* embedded in its BEEF, so supersession is detected even when intermediate
|
|
29216
|
+
* renditions are absent from the lookup answer. Iterative so arbitrarily
|
|
29217
|
+
* long update chains cannot exhaust the call stack.
|
|
29218
|
+
*/
|
|
29219
|
+
collectSpentOutpoints(tx, spent, visited) {
|
|
29220
|
+
const pending = [tx];
|
|
29221
|
+
while (pending.length > 0) {
|
|
29222
|
+
const current = pending.pop();
|
|
29223
|
+
const txid = current.id("hex");
|
|
29224
|
+
if (visited.has(txid)) continue;
|
|
29225
|
+
visited.add(txid);
|
|
29226
|
+
for (const input of current.inputs) {
|
|
29227
|
+
const sourceTxid = input.sourceTXID ?? input.sourceTransaction?.id("hex");
|
|
29228
|
+
if (sourceTxid == null || input.sourceOutputIndex == null) continue;
|
|
29229
|
+
spent.add(`${sourceTxid}.${input.sourceOutputIndex}`);
|
|
29230
|
+
if (input.sourceTransaction != null) pending.push(input.sourceTransaction);
|
|
29231
|
+
}
|
|
29232
|
+
}
|
|
28102
29233
|
}
|
|
28103
29234
|
emptyLookupDiagnostics(correlationId) {
|
|
28104
29235
|
return {
|
|
@@ -28139,6 +29270,21 @@ var OverlayUMPTokenInteractor = class {
|
|
|
28139
29270
|
outputCount: diagnostics.outputCount
|
|
28140
29271
|
};
|
|
28141
29272
|
}
|
|
29273
|
+
captureLookupCompleted(lookupKind, result, diagnostics, startedAt, extraAttributes = {}) {
|
|
29274
|
+
this.telemetry.capture({
|
|
29275
|
+
name: "wallet-toolbox.ump.lookup.completed",
|
|
29276
|
+
component: "wallet-toolbox.ump",
|
|
29277
|
+
severity: "info",
|
|
29278
|
+
correlationId: diagnostics.correlationId,
|
|
29279
|
+
attributes: {
|
|
29280
|
+
lookupKind,
|
|
29281
|
+
result,
|
|
29282
|
+
durationMs: Date.now() - startedAt,
|
|
29283
|
+
...this.lookupDiagnosticAttributes(diagnostics),
|
|
29284
|
+
...extraAttributes
|
|
29285
|
+
}
|
|
29286
|
+
});
|
|
29287
|
+
}
|
|
28142
29288
|
captureLookupFailure(lookupKind, reason, diagnostics, startedAt, error) {
|
|
28143
29289
|
this.telemetry.capture({
|
|
28144
29290
|
name: "wallet-toolbox.ump.lookup.indeterminate",
|
|
@@ -28379,8 +29525,7 @@ var OverlayUMPTokenInteractor = class {
|
|
|
28379
29525
|
throw new UMPTokenLookupError("lookup-unavailable", diagnostics, { cause: error });
|
|
28380
29526
|
}
|
|
28381
29527
|
if (resolution.answer.outputs.length === 0) {
|
|
28382
|
-
|
|
28383
|
-
if (!(p.isFinal && p.hostCount > 0 && p.completedHosts === p.hostCount && p.successfulHosts === p.hostCount && p.emptyHosts === p.hostCount && p.failedHosts === 0 && p.rejectedHosts === 0 && p.freeformHosts === 0)) {
|
|
29528
|
+
if (resolution.progress.emptyHosts === 0) {
|
|
28384
29529
|
const diagnostics = this.toLookupDiagnostics(resolution);
|
|
28385
29530
|
this.captureLookupFailure("outpoint", "lookup-incomplete", diagnostics, startedAt);
|
|
28386
29531
|
throw new UMPTokenLookupError("lookup-incomplete", diagnostics);
|
|
@@ -33724,6 +34869,6 @@ var WalletPermissionsManager = class WalletPermissionsManager {
|
|
|
33724
34869
|
}
|
|
33725
34870
|
};
|
|
33726
34871
|
//#endregion
|
|
33727
|
-
export { ARGON2ID_DEFAULT_HASH_LENGTH, ARGON2ID_DEFAULT_ITERATIONS, ARGON2ID_DEFAULT_MEMORY_KIB, ARGON2ID_DEFAULT_PARALLELISM, ARGON2ID_MAX_ITERATIONS, ARGON2ID_MAX_MEMORY_KIB, ARGON2ID_MAX_PARALLELISM, AuthMethodInteractor, BHServiceClient, BulkFileDataManager, BulkFileDataReader, BulkFilesReader, BulkFilesReaderFs, BulkFilesReaderStorage, BulkIngestorBase, BulkIngestorCDN, BulkIngestorCDNBabbage, BulkIngestorWhatsOnChainCdn, BulkStorageBase, CWIStyleWalletManager, Chaintracks, ChaintracksChainTracker, ChaintracksFetch, ChaintracksFetchError, ChaintracksServiceClient, ChaintracksStorageBase, ChaintracksStorageIdb, ChaintracksStorageNoDb, DEFAULT_PROFILE_ID, DEFAULT_SETTINGS, DevConsoleInteractor, EntityBase, EntityCertificate, EntityCertificateField, EntityCommission, EntityOutput, EntityOutputBasket, EntityOutputTag, EntityOutputTagMap, EntityProvenTx, EntityProvenTxReq, EntitySyncState, EntityTransaction, EntityTxLabel, EntityTxLabelMap, EntityUser, GoChaintracksServiceClient, HeightRange, KDF_MAX_HASH_LENGTH, LiveIngestorBase, LiveIngestorChaintracksSSE, LiveIngestorWhatsOnChainPoll, MAX_STATE_SNAPSHOT_BYTES, MergeEntity, Monitor, OverlayUMPTokenInteractor, PBKDF2_MAX_ITERATIONS, PBKDF2_NUM_ROUNDS, PersonaIDInteractor, PrivilegedKeyManager, ScriptTemplateBRC29, Services, SetupClient, SimpleWalletManager, StorageClient, StorageIdb, StorageProvider, StorageSyncReader, TESTNET_DEFAULT_SETTINGS, TwilioPhoneInteractor, UMPTokenLookupError, WABAccountContinuityError, WABClient, WABClientError, WABTransport, Wallet, WalletAuthenticationManager, WalletLogger, WalletPermissionsManager, WalletSettingsManager, WalletSigner, WalletStorageManager, WhatsOnChainServices, arcDefaultUrl, arcGorillaPoolUrl, arcadeDefaultUrl, arraysEqual, asArray, asBsvSdkPrivateKey, asBsvSdkPublickKey, asBsvSdkScript, asBsvSdkTx, asString, asUint8Array, brc29ProtocolID, convertProofToMerklePath, createDefaultIdbChaintracksOptions, createDefaultNoDbChaintracksOptions, createDefaultWalletServicesOptions, createIdbChaintracks, createNoDbChaintracks, createSyncMap, decryptBRC39, doubleSha256BE, doubleSha256LE, encryptBRC39, exportBRC38, exportBRC38Json, exportBRC39, getIdentityKey, getLabelToSpecOp, getListOutputsSpecOp, importBRC38, importBRC39, isAutoSpendableChangeOutput, isBaseBlockHeader, isBlockHeader, isLive, isLiveBlockHeader, isManagedChangeOutput, logCreateActionArgs, logWalletError, logger, makeAtomicBeef, makeBrc114ActionTimeLabel, managedChangeOutputFields, maxDate, optionalArraysEqual, outputColumnsWithoutLockingScript, parseBRC38Json, parseBrc114ActionTimeLabels, parseFileLink, parseTxScriptOffsets, partitionActionLabels, randomBytes, randomBytesBase64, randomBytesHex, sdk_exports as sdk, selectBulkHeaderFiles, sha256Hash, stampLog, stampLogFormat, tableAuthSessionToPeerSession, throwDummyReviewActions, toBinaryBaseBlockHeader, toLookupNetworkPreset, toWalletNetwork, transactionColumnsWithoutRawTx, blockHeaderUtilities_exports as utils, validateScriptHash, validateSecondsSinceEpoch, validateStorageFeeModel, verifyHexString, verifyId, verifyInteger, verifyNumber, verifyOne, verifyOneOrNone, verifyOptionalHexString, verifyTruthy, wait, wocGetHeadersHeaderToBlockHeader };
|
|
34872
|
+
export { ARGON2ID_DEFAULT_HASH_LENGTH, ARGON2ID_DEFAULT_ITERATIONS, ARGON2ID_DEFAULT_MEMORY_KIB, ARGON2ID_DEFAULT_PARALLELISM, ARGON2ID_MAX_ITERATIONS, ARGON2ID_MAX_MEMORY_KIB, ARGON2ID_MAX_PARALLELISM, AuthMethodInteractor, BHServiceClient, BulkFileDataManager, BulkFileDataReader, BulkFilesReader, BulkFilesReaderFs, BulkFilesReaderStorage, BulkIngestorBase, BulkIngestorCDN, BulkIngestorCDNBabbage, BulkIngestorChaintracks, BulkIngestorWhatsOnChainCdn, BulkStorageBase, CWIStyleWalletManager, Chaintracks, ChaintracksChainTracker, ChaintracksFetch, ChaintracksFetchError, ChaintracksServiceClient, ChaintracksStorageBase, ChaintracksStorageIdb, ChaintracksStorageNoDb, DEFAULT_PROFILE_ID, DEFAULT_SETTINGS, DevConsoleInteractor, EntityBase, EntityCertificate, EntityCertificateField, EntityCommission, EntityOutput, EntityOutputBasket, EntityOutputTag, EntityOutputTagMap, EntityProvenTx, EntityProvenTxReq, EntitySyncState, EntityTransaction, EntityTxLabel, EntityTxLabelMap, EntityUser, GoChaintracksServiceClient, HeightRange, KDF_MAX_HASH_LENGTH, LiveIngestorBase, LiveIngestorChaintracksSSE, LiveIngestorWhatsOnChainPoll, MAX_STATE_SNAPSHOT_BYTES, MergeEntity, Monitor, OverlayUMPTokenInteractor, PBKDF2_MAX_ITERATIONS, PBKDF2_NUM_ROUNDS, PersonaIDInteractor, PrivilegedKeyManager, ScriptTemplateBRC29, Services, SetupClient, SimpleWalletManager, StorageClient, StorageIdb, StorageProvider, StorageSyncReader, TESTNET_DEFAULT_SETTINGS, TwilioPhoneInteractor, UMPTokenLookupError, WABAccountContinuityError, WABClient, WABClientError, WABTransport, Wallet, WalletAuthenticationManager, WalletLogger, WalletPermissionsManager, WalletSettingsManager, WalletSigner, WalletStorageManager, WhatsOnChainServices, arcDefaultUrl, arcGorillaPoolUrl, arcadeDefaultUrl, arraysEqual, asArray, asBsvSdkPrivateKey, asBsvSdkPublickKey, asBsvSdkScript, asBsvSdkTx, asString, asUint8Array, brc29ProtocolID, buildChaintracksOptionsWithIngestors, convertProofToMerklePath, createAndStartDefaultChaintracks, createDefaultBulkFileDataManager, createDefaultChaintracksClient, createDefaultChaintracksStorageOptions, createDefaultIdbChaintracksOptions, createDefaultNoDbChaintracksOptions, createDefaultWalletServicesOptions, createIdbChaintracks, createNoDbChaintracks, createSyncMap, decryptBRC39, doubleSha256BE, doubleSha256LE, encryptBRC39, exportBRC38, exportBRC38Json, exportBRC39, getIdentityKey, getLabelToSpecOp, getListOutputsSpecOp, importBRC38, importBRC39, isAutoSpendableChangeOutput, isBaseBlockHeader, isBlockHeader, isLive, isLiveBlockHeader, isManagedChangeOutput, logCreateActionArgs, logWalletError, logger, makeAtomicBeef, makeBrc114ActionTimeLabel, managedChangeOutputFields, maxDate, optionalArraysEqual, outputColumnsWithoutLockingScript, parseBRC38Json, parseBrc114ActionTimeLabels, parseFileLink, parseTxScriptOffsets, partitionActionLabels, randomBytes, randomBytesBase64, randomBytesHex, resolveDefaultChaintracksArguments, sdk_exports as sdk, selectBulkHeaderFiles, sha256Hash, stampLog, stampLogFormat, startChaintracks, tableAuthSessionToPeerSession, throwDummyReviewActions, toBinaryBaseBlockHeader, toDefaultChaintracksArguments, toLookupNetworkPreset, toWalletNetwork, transactionColumnsWithoutRawTx, blockHeaderUtilities_exports as utils, validateScriptHash, validateSecondsSinceEpoch, validateStorageFeeModel, verifyHexString, verifyId, verifyInteger, verifyNumber, verifyOne, verifyOneOrNone, verifyOptionalHexString, verifyTruthy, wait, wocGetHeadersHeaderToBlockHeader };
|
|
33728
34873
|
|
|
33729
34874
|
//# sourceMappingURL=index.client.mjs.map
|