routstrd 0.4.0 → 0.4.1
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/COCO-2.0.0-MIGRATION-PLAN.md +1191 -0
- package/README.md +5 -0
- package/dist/daemon/index.js +1096 -143
- package/dist/index.js +572 -124
- package/package.json +1 -1
- package/src/cli.ts +160 -26
- package/src/daemon/fatal-error.test.ts +104 -0
- package/src/daemon/fatal-error.ts +48 -0
- package/src/daemon/wallet/auto-refill.ts +4 -6
- package/src/daemon/wallet/cleanup.test.ts +168 -0
- package/src/daemon/wallet/cleanup.ts +104 -0
- package/src/daemon/wallet/coco-client.test.ts +191 -30
- package/src/daemon/wallet/coco-client.ts +867 -74
- package/src/daemon/wallet/cocod-client.ts +49 -1
- package/src/daemon/wallet/diagnostics.test.ts +378 -0
- package/src/daemon/wallet/diagnostics.ts +471 -0
- package/src/daemon/wallet/migration.test.ts +30 -6
- package/src/daemon/wallet/migration.ts +30 -85
- package/src/daemon/wallet/receive-dedup.test.ts +238 -0
- package/src/daemon/wallet/receive-dedup.ts +384 -0
- package/src/daemon/wallet/wallet-state.ts +75 -0
- package/src/start-daemon.ts +21 -1
- package/tests/integrations/hermes.test.ts +42 -5
package/dist/daemon/index.js
CHANGED
|
@@ -84491,7 +84491,7 @@ var require_public_api = __commonJS((exports) => {
|
|
|
84491
84491
|
|
|
84492
84492
|
// src/daemon/index.ts
|
|
84493
84493
|
import { createServer } from "http";
|
|
84494
|
-
import { existsSync as
|
|
84494
|
+
import { existsSync as existsSync13, unlinkSync as unlinkSync2 } from "fs";
|
|
84495
84495
|
|
|
84496
84496
|
// node_modules/applesauce-relay/dist/group.js
|
|
84497
84497
|
init_event_store2();
|
|
@@ -113595,11 +113595,11 @@ function startAutoRefillLoop(cocod, getWallet, getConfig, intervalMs = 5000) {
|
|
|
113595
113595
|
if (now4 - lastRefillAt < config2.cooldownMs) {
|
|
113596
113596
|
return;
|
|
113597
113597
|
}
|
|
113598
|
-
checkInProgress = true;
|
|
113599
113598
|
const backoffInterval = Math.min(intervalMs * Math.pow(2, consecutiveFailures), 5 * 60 * 1000);
|
|
113600
113599
|
if (now4 - lastAttemptAt < backoffInterval) {
|
|
113601
113600
|
return;
|
|
113602
113601
|
}
|
|
113602
|
+
checkInProgress = true;
|
|
113603
113603
|
try {
|
|
113604
113604
|
const balances = await cocod.getBalances();
|
|
113605
113605
|
const totalBalance = Object.values(balances).reduce((sum, b2) => sum + (typeof b2 === "number" ? b2 : 0), 0);
|
|
@@ -113607,10 +113607,9 @@ function startAutoRefillLoop(cocod, getWallet, getConfig, intervalMs = 5000) {
|
|
|
113607
113607
|
return;
|
|
113608
113608
|
}
|
|
113609
113609
|
logger5.log(`[auto-refill] Balance ${totalBalance} sats < threshold ${config2.threshold}. Refilling ${config2.amount} sats...`);
|
|
113610
|
-
const
|
|
113611
|
-
const mintUrl = mints[0];
|
|
113610
|
+
const mintUrl = await cocod.getDefaultMint();
|
|
113612
113611
|
if (!mintUrl) {
|
|
113613
|
-
logger5.error("[auto-refill] No
|
|
113612
|
+
logger5.error("[auto-refill] No default mint configured");
|
|
113614
113613
|
return;
|
|
113615
113614
|
}
|
|
113616
113615
|
logger5.log(`[auto-refill] Creating BOLT-11 invoice for ${config2.amount} sats via ${mintUrl}...`);
|
|
@@ -128194,26 +128193,6 @@ var PluginHost = class {
|
|
|
128194
128193
|
};
|
|
128195
128194
|
}
|
|
128196
128195
|
};
|
|
128197
|
-
async function initializeCoco(config2) {
|
|
128198
|
-
await config2.repo.init();
|
|
128199
|
-
const coco = new Manager(config2.repo, config2.seedGetter, config2.logger, config2.webSocketFactory, config2.plugins, config2.watchers, config2.processors, config2.subscriptions);
|
|
128200
|
-
await coco.initPlugins();
|
|
128201
|
-
await coco.reconcileLegacyMintQuotes();
|
|
128202
|
-
const mintOperationWatcherConfig = config2.watchers?.mintOperationWatcher;
|
|
128203
|
-
if (!mintOperationWatcherConfig?.disabled)
|
|
128204
|
-
await coco.enableMintOperationWatcher(mintOperationWatcherConfig);
|
|
128205
|
-
const proofStateWatcherConfig = config2.watchers?.proofStateWatcher;
|
|
128206
|
-
if (!proofStateWatcherConfig?.disabled)
|
|
128207
|
-
await coco.enableProofStateWatcher(proofStateWatcherConfig);
|
|
128208
|
-
const mintOperationProcessorConfig = config2.processors?.mintOperationProcessor;
|
|
128209
|
-
if (!mintOperationProcessorConfig?.disabled)
|
|
128210
|
-
await coco.enableMintOperationProcessor(mintOperationProcessorConfig);
|
|
128211
|
-
await coco.ops.send.recovery.run();
|
|
128212
|
-
await coco.ops.melt.recovery.run();
|
|
128213
|
-
await coco.ops.receive.recovery.run();
|
|
128214
|
-
await coco.recoverPendingMintOperations();
|
|
128215
|
-
return coco;
|
|
128216
|
-
}
|
|
128217
128196
|
var Manager = class {
|
|
128218
128197
|
mint;
|
|
128219
128198
|
wallet;
|
|
@@ -136926,6 +136905,8 @@ function getWalletStateMessage(state) {
|
|
|
136926
136905
|
return "Wallet is locked. Unlock it before performing wallet operations.";
|
|
136927
136906
|
case "UNINITIALIZED":
|
|
136928
136907
|
return "Wallet is not initialized. Run 'routstrd onboard' first.";
|
|
136908
|
+
case "RECOVERING":
|
|
136909
|
+
return "Wallet is recovering from a previous run. Balance and send/receive operations will be available once recovery completes.";
|
|
136929
136910
|
case "ERROR":
|
|
136930
136911
|
return "Wallet is in an error state.";
|
|
136931
136912
|
default:
|
|
@@ -136982,6 +136963,15 @@ async function buildStatusOutput(deps) {
|
|
|
136982
136963
|
const mode = getCurrentMode(deps);
|
|
136983
136964
|
try {
|
|
136984
136965
|
const walletState = await deps.walletClient.getStatus();
|
|
136966
|
+
if (walletState === "RECOVERING") {
|
|
136967
|
+
return {
|
|
136968
|
+
daemon: "running",
|
|
136969
|
+
wallet: "recovering",
|
|
136970
|
+
walletState,
|
|
136971
|
+
mode,
|
|
136972
|
+
error: getWalletStateMessage(walletState)
|
|
136973
|
+
};
|
|
136974
|
+
}
|
|
136985
136975
|
if (walletState !== "UNLOCKED") {
|
|
136986
136976
|
return {
|
|
136987
136977
|
daemon: "running",
|
|
@@ -137011,8 +137001,9 @@ async function buildStatusOutput(deps) {
|
|
|
137011
137001
|
}
|
|
137012
137002
|
async function buildWalletDetails(deps) {
|
|
137013
137003
|
const state = await deps.walletClient.getStatus();
|
|
137004
|
+
const recovery = await deps.walletClient.getRecoveryProgress?.();
|
|
137014
137005
|
if (state !== "UNLOCKED") {
|
|
137015
|
-
return { state, ready: false };
|
|
137006
|
+
return { state, ready: false, ...recovery ? { recovery } : {} };
|
|
137016
137007
|
}
|
|
137017
137008
|
const [balances, defaultMint] = await Promise.all([
|
|
137018
137009
|
deps.walletAdapter.getBalances(),
|
|
@@ -137024,7 +137015,8 @@ async function buildWalletDetails(deps) {
|
|
|
137024
137015
|
balances,
|
|
137025
137016
|
unit: "sat",
|
|
137026
137017
|
activeMint: deps.walletAdapter.getActiveMintUrl(),
|
|
137027
|
-
defaultMint
|
|
137018
|
+
defaultMint,
|
|
137019
|
+
...recovery ? { recovery } : {}
|
|
137028
137020
|
};
|
|
137029
137021
|
}
|
|
137030
137022
|
function makeSdkLogger(prefix) {
|
|
@@ -137086,6 +137078,21 @@ function createDaemonRequestHandler(deps) {
|
|
|
137086
137078
|
});
|
|
137087
137079
|
return;
|
|
137088
137080
|
}
|
|
137081
|
+
if (req.method === "POST" && url5.pathname === "/wallet/cleanup") {
|
|
137082
|
+
await respond(res, async () => {
|
|
137083
|
+
if (!deps.walletClient.cleanupStuckOperations) {
|
|
137084
|
+
throw new CocodHttpError(501, "Wallet cleanup is not supported by this wallet client.");
|
|
137085
|
+
}
|
|
137086
|
+
const body = await readJsonBody(req);
|
|
137087
|
+
const result = await deps.walletClient.cleanupStuckOperations({
|
|
137088
|
+
mintUrl: optionalStringField(body, "mintUrl"),
|
|
137089
|
+
minAgeMs: typeof body.minAgeMs === "number" && Number.isFinite(body.minAgeMs) ? body.minAgeMs : undefined,
|
|
137090
|
+
dryRun: body.dryRun === true
|
|
137091
|
+
});
|
|
137092
|
+
return { output: result };
|
|
137093
|
+
});
|
|
137094
|
+
return;
|
|
137095
|
+
}
|
|
137089
137096
|
if (req.method === "POST" && url5.pathname === "/wallet/receive/cashu") {
|
|
137090
137097
|
await respond(res, async () => {
|
|
137091
137098
|
const body = await readJsonBody(req);
|
|
@@ -137550,11 +137557,7 @@ function createDaemonRequestHandler(deps) {
|
|
|
137550
137557
|
}
|
|
137551
137558
|
if (!mintUrl) {
|
|
137552
137559
|
try {
|
|
137553
|
-
|
|
137554
|
-
const mintUrls = Object.keys(walletBalances);
|
|
137555
|
-
if (mintUrls.length > 0) {
|
|
137556
|
-
mintUrl = mintUrls[0];
|
|
137557
|
-
}
|
|
137560
|
+
mintUrl = deps.walletAdapter.getActiveMintUrl() ?? Object.keys(await deps.walletAdapter.getBalances())[0];
|
|
137558
137561
|
} catch {}
|
|
137559
137562
|
}
|
|
137560
137563
|
let refunded = false;
|
|
@@ -157221,6 +157224,216 @@ function mnemonicToSeedSync2(mnemonic, passphrase = "") {
|
|
|
157221
157224
|
});
|
|
157222
157225
|
}
|
|
157223
157226
|
|
|
157227
|
+
// src/daemon/wallet/cleanup.ts
|
|
157228
|
+
function selectCleanupOperations(options) {
|
|
157229
|
+
const { mints, sends, melts, nowMs, minAgeMs } = options;
|
|
157230
|
+
const mintsToFail = mints.filter((op) => op.state === "pending" && op.expiry > 0 && op.expiry * 1000 <= nowMs && op.lastObservedRemoteState !== "PAID" && op.lastObservedRemoteState !== "ISSUED");
|
|
157231
|
+
const sendsToReclaim = sends.filter((op) => op.state === "pending" && nowMs - op.updatedAt >= minAgeMs);
|
|
157232
|
+
const meltsToCancel = melts.filter((op) => op.state === "prepared" && nowMs - op.updatedAt >= minAgeMs);
|
|
157233
|
+
return { mintsToFail, sendsToReclaim, meltsToCancel };
|
|
157234
|
+
}
|
|
157235
|
+
|
|
157236
|
+
// src/daemon/wallet/receive-dedup.ts
|
|
157237
|
+
import { createHash as createHash3 } from "crypto";
|
|
157238
|
+
var TOKEN_TABLE = "routstrd_receive_tokens";
|
|
157239
|
+
var MAINTENANCE_TABLE = "routstrd_wallet_maintenance";
|
|
157240
|
+
var RECONCILE_BACKUP_KEY = "receive_reconcile_v1_backup";
|
|
157241
|
+
function hashReceiveToken(token) {
|
|
157242
|
+
return createHash3("sha256").update(token).digest("hex");
|
|
157243
|
+
}
|
|
157244
|
+
function initReceiveDedupSchema(database) {
|
|
157245
|
+
database.exec(`
|
|
157246
|
+
CREATE TABLE IF NOT EXISTS ${TOKEN_TABLE} (
|
|
157247
|
+
tokenHash TEXT PRIMARY KEY,
|
|
157248
|
+
state TEXT NOT NULL CHECK (state IN ('processing', 'succeeded', 'failed')),
|
|
157249
|
+
operationId TEXT,
|
|
157250
|
+
error TEXT,
|
|
157251
|
+
createdAt INTEGER NOT NULL,
|
|
157252
|
+
updatedAt INTEGER NOT NULL
|
|
157253
|
+
);
|
|
157254
|
+
CREATE TABLE IF NOT EXISTS ${MAINTENANCE_TABLE} (
|
|
157255
|
+
key TEXT PRIMARY KEY,
|
|
157256
|
+
value TEXT NOT NULL,
|
|
157257
|
+
updatedAt INTEGER NOT NULL
|
|
157258
|
+
);
|
|
157259
|
+
`);
|
|
157260
|
+
}
|
|
157261
|
+
function listProcessingReceiveTokens(database) {
|
|
157262
|
+
return database.query(`SELECT tokenHash, state, operationId, error
|
|
157263
|
+
FROM ${TOKEN_TABLE} WHERE state = 'processing'`).all();
|
|
157264
|
+
}
|
|
157265
|
+
function deleteReceiveTokenReservation(database, tokenHash) {
|
|
157266
|
+
database.query(`DELETE FROM ${TOKEN_TABLE} WHERE tokenHash = ?`).run(tokenHash);
|
|
157267
|
+
}
|
|
157268
|
+
function clearInterruptedReceiveReservations(database) {
|
|
157269
|
+
const result = database.query(`DELETE FROM ${TOKEN_TABLE}
|
|
157270
|
+
WHERE state = 'processing' AND operationId IS NULL`).run();
|
|
157271
|
+
return result.changes;
|
|
157272
|
+
}
|
|
157273
|
+
function reserveReceiveToken(database, token) {
|
|
157274
|
+
const tokenHash = hashReceiveToken(token);
|
|
157275
|
+
const now5 = Math.floor(Date.now() / 1000);
|
|
157276
|
+
const result = database.query(`INSERT OR IGNORE INTO ${TOKEN_TABLE}
|
|
157277
|
+
(tokenHash, state, operationId, error, createdAt, updatedAt)
|
|
157278
|
+
VALUES (?, 'processing', NULL, NULL, ?, ?)`).run(tokenHash, now5, now5);
|
|
157279
|
+
if (result.changes > 0)
|
|
157280
|
+
return { acquired: true, tokenHash };
|
|
157281
|
+
const existing = database.query(`SELECT tokenHash, state, operationId, error
|
|
157282
|
+
FROM ${TOKEN_TABLE} WHERE tokenHash = ?`).get(tokenHash);
|
|
157283
|
+
return { acquired: false, tokenHash, ...existing ? { existing } : {} };
|
|
157284
|
+
}
|
|
157285
|
+
function releaseReceiveToken(database, tokenHash) {
|
|
157286
|
+
database.query(`DELETE FROM ${TOKEN_TABLE} WHERE tokenHash = ? AND operationId IS NULL`).run(tokenHash);
|
|
157287
|
+
}
|
|
157288
|
+
function updateReceiveToken(database, tokenHash, update) {
|
|
157289
|
+
database.query(`UPDATE ${TOKEN_TABLE}
|
|
157290
|
+
SET state = ?, operationId = COALESCE(?, operationId), error = ?, updatedAt = ?
|
|
157291
|
+
WHERE tokenHash = ?`).run(update.state, update.operationId ?? null, update.error ?? null, Math.floor(Date.now() / 1000), tokenHash);
|
|
157292
|
+
}
|
|
157293
|
+
function receiveInputFingerprint(operation) {
|
|
157294
|
+
const proofs = operation.inputProofs.map((proof) => ({
|
|
157295
|
+
amount: proof.amount,
|
|
157296
|
+
id: proof.id,
|
|
157297
|
+
secret: proof.secret,
|
|
157298
|
+
C: proof.C,
|
|
157299
|
+
witness: proof.witness ?? null
|
|
157300
|
+
})).sort((left, right) => JSON.stringify(left).localeCompare(JSON.stringify(right)));
|
|
157301
|
+
return createHash3("sha256").update(JSON.stringify([operation.mintUrl, operation.unit, proofs])).digest("hex");
|
|
157302
|
+
}
|
|
157303
|
+
function groupExecutingReceives(operations) {
|
|
157304
|
+
const groups = new Map;
|
|
157305
|
+
for (const operation of operations) {
|
|
157306
|
+
if (operation.state !== "executing")
|
|
157307
|
+
continue;
|
|
157308
|
+
const key = receiveInputFingerprint(operation);
|
|
157309
|
+
const group2 = groups.get(key) ?? [];
|
|
157310
|
+
group2.push(operation);
|
|
157311
|
+
groups.set(key, group2);
|
|
157312
|
+
}
|
|
157313
|
+
return [...groups.values()].map((group2) => group2.sort((left, right) => left.createdAt - right.createdAt || left.id.localeCompare(right.id)));
|
|
157314
|
+
}
|
|
157315
|
+
function outputData(operation) {
|
|
157316
|
+
const value = operation.outputData;
|
|
157317
|
+
if (!value || typeof value !== "object")
|
|
157318
|
+
return null;
|
|
157319
|
+
const candidate = value;
|
|
157320
|
+
if (!Array.isArray(candidate.keep) || !Array.isArray(candidate.send))
|
|
157321
|
+
return null;
|
|
157322
|
+
return { keep: candidate.keep, send: candidate.send };
|
|
157323
|
+
}
|
|
157324
|
+
function outputBlindedMessages(operation) {
|
|
157325
|
+
const data = outputData(operation);
|
|
157326
|
+
if (!data)
|
|
157327
|
+
return [];
|
|
157328
|
+
return [...data.keep, ...data.send].map((entry) => entry?.blindedMessage).filter((message) => Boolean(message && typeof message.B_ === "string" && message.B_ && typeof message.id === "string" && Number.isFinite(message.amount)));
|
|
157329
|
+
}
|
|
157330
|
+
async function rollBackOthers(source, group2, retainedIds, reason) {
|
|
157331
|
+
let count = 0;
|
|
157332
|
+
for (const operation of group2) {
|
|
157333
|
+
if (retainedIds.has(operation.id))
|
|
157334
|
+
continue;
|
|
157335
|
+
await source.rollBack(operation, reason);
|
|
157336
|
+
count++;
|
|
157337
|
+
}
|
|
157338
|
+
return count;
|
|
157339
|
+
}
|
|
157340
|
+
async function reconcileExecutingReceives(source) {
|
|
157341
|
+
const executing = (await source.listExecuting()).filter((operation) => operation.state === "executing");
|
|
157342
|
+
const groups = groupExecutingReceives(executing);
|
|
157343
|
+
const result = {
|
|
157344
|
+
executing: executing.length,
|
|
157345
|
+
uniqueGroups: groups.length,
|
|
157346
|
+
rolledBack: 0,
|
|
157347
|
+
retained: 0,
|
|
157348
|
+
unresolved: 0,
|
|
157349
|
+
recoveryOperationIds: []
|
|
157350
|
+
};
|
|
157351
|
+
for (const group2 of groups) {
|
|
157352
|
+
try {
|
|
157353
|
+
const savedOwners = new Set;
|
|
157354
|
+
for (const operation of group2) {
|
|
157355
|
+
if (await source.hasSavedOutputs(operation))
|
|
157356
|
+
savedOwners.add(operation.id);
|
|
157357
|
+
}
|
|
157358
|
+
if (savedOwners.size > 0) {
|
|
157359
|
+
result.rolledBack += await rollBackOthers(source, group2, savedOwners, "Duplicate receive: outputs already persisted by another operation");
|
|
157360
|
+
result.retained += savedOwners.size;
|
|
157361
|
+
result.recoveryOperationIds.push(...savedOwners);
|
|
157362
|
+
continue;
|
|
157363
|
+
}
|
|
157364
|
+
const states = await source.checkProofStates(group2[0]);
|
|
157365
|
+
const completeStates = states.length === group2[0].inputProofs.length;
|
|
157366
|
+
const allUnspent = completeStates && states.every((state) => state.state === "UNSPENT");
|
|
157367
|
+
const allSpent = completeStates && states.every((state) => state.state === "SPENT");
|
|
157368
|
+
if (allUnspent) {
|
|
157369
|
+
const canonical = group2.find((operation) => outputBlindedMessages(operation).length > 0);
|
|
157370
|
+
if (!canonical) {
|
|
157371
|
+
result.unresolved += group2.length;
|
|
157372
|
+
continue;
|
|
157373
|
+
}
|
|
157374
|
+
result.rolledBack += await rollBackOthers(source, group2, new Set([canonical.id]), "Duplicate receive: identical inputs retained by canonical operation");
|
|
157375
|
+
result.retained++;
|
|
157376
|
+
result.recoveryOperationIds.push(canonical.id);
|
|
157377
|
+
continue;
|
|
157378
|
+
}
|
|
157379
|
+
if (!allSpent) {
|
|
157380
|
+
result.unresolved += group2.length;
|
|
157381
|
+
continue;
|
|
157382
|
+
}
|
|
157383
|
+
const outputOwners = new Map;
|
|
157384
|
+
let malformed = false;
|
|
157385
|
+
for (const operation of group2) {
|
|
157386
|
+
const outputs = outputBlindedMessages(operation);
|
|
157387
|
+
if (outputs.length === 0) {
|
|
157388
|
+
malformed = true;
|
|
157389
|
+
continue;
|
|
157390
|
+
}
|
|
157391
|
+
for (const output12 of outputs) {
|
|
157392
|
+
const entry = outputOwners.get(output12.B_) ?? {
|
|
157393
|
+
message: output12,
|
|
157394
|
+
owners: new Set
|
|
157395
|
+
};
|
|
157396
|
+
entry.owners.add(operation.id);
|
|
157397
|
+
outputOwners.set(output12.B_, entry);
|
|
157398
|
+
}
|
|
157399
|
+
}
|
|
157400
|
+
if (malformed || outputOwners.size === 0) {
|
|
157401
|
+
result.unresolved += group2.length;
|
|
157402
|
+
continue;
|
|
157403
|
+
}
|
|
157404
|
+
const restored = await source.restoreOutputs(group2[0].mintUrl, [...outputOwners.values()].map((entry) => entry.message));
|
|
157405
|
+
const restoredOwners = new Set;
|
|
157406
|
+
for (const output12 of restored) {
|
|
157407
|
+
for (const owner of outputOwners.get(output12.B_)?.owners ?? []) {
|
|
157408
|
+
restoredOwners.add(owner);
|
|
157409
|
+
}
|
|
157410
|
+
}
|
|
157411
|
+
if (restoredOwners.size === 0) {
|
|
157412
|
+
for (const operation of group2) {
|
|
157413
|
+
await source.rollBack(operation, "Recovered: input proofs spent without recoverable outputs");
|
|
157414
|
+
result.rolledBack++;
|
|
157415
|
+
}
|
|
157416
|
+
continue;
|
|
157417
|
+
}
|
|
157418
|
+
result.rolledBack += await rollBackOthers(source, group2, restoredOwners, "Duplicate receive: recoverable outputs belong to another operation");
|
|
157419
|
+
result.retained += restoredOwners.size;
|
|
157420
|
+
result.recoveryOperationIds.push(...restoredOwners);
|
|
157421
|
+
} catch {
|
|
157422
|
+
result.unresolved += group2.length;
|
|
157423
|
+
}
|
|
157424
|
+
}
|
|
157425
|
+
return result;
|
|
157426
|
+
}
|
|
157427
|
+
function getReceiveReconcileBackup(database) {
|
|
157428
|
+
const row = database.query(`SELECT value FROM ${MAINTENANCE_TABLE} WHERE key = ?`).get(RECONCILE_BACKUP_KEY);
|
|
157429
|
+
return row?.value ?? null;
|
|
157430
|
+
}
|
|
157431
|
+
function setReceiveReconcileBackup(database, path2) {
|
|
157432
|
+
database.query(`INSERT INTO ${MAINTENANCE_TABLE} (key, value, updatedAt)
|
|
157433
|
+
VALUES (?, ?, ?)
|
|
157434
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value, updatedAt = excluded.updatedAt`).run(RECONCILE_BACKUP_KEY, path2, Math.floor(Date.now() / 1000));
|
|
157435
|
+
}
|
|
157436
|
+
|
|
157224
157437
|
// src/daemon/wallet/paths.ts
|
|
157225
157438
|
import { join as join10 } from "path";
|
|
157226
157439
|
function homeDir() {
|
|
@@ -157258,6 +157471,12 @@ function startupProgress(message) {
|
|
|
157258
157471
|
logger5.info(message);
|
|
157259
157472
|
console.log(`${STARTUP_LOG_PREFIX} ${message}`);
|
|
157260
157473
|
}
|
|
157474
|
+
var surfacingRecoveryProgress = false;
|
|
157475
|
+
var RECOVERY_STALL_MARKERS = new Map([
|
|
157476
|
+
["SendOperationService\x00Could not reach mint for recovery, will retry later", "Send recovery: mint unreachable"],
|
|
157477
|
+
["ReceiveOperationService\x00Could not reach mint for receive recovery, will retry later", "Receive recovery: mint unreachable"],
|
|
157478
|
+
["MintOperationService\x00Failed to reconcile stale pending mint operation", "Mint recovery: pending operation check failed"]
|
|
157479
|
+
]);
|
|
157261
157480
|
var SAFE_COCO_LOG_FIELDS = new Set([
|
|
157262
157481
|
"module",
|
|
157263
157482
|
"mintUrl",
|
|
@@ -157289,6 +157508,16 @@ function safeCocoMetadata(values2) {
|
|
|
157289
157508
|
function createCocoLogger(bindings = {}) {
|
|
157290
157509
|
const write = (level, message, meta3) => {
|
|
157291
157510
|
const metadata = safeCocoMetadata([bindings, ...meta3]);
|
|
157511
|
+
if (surfacingRecoveryProgress) {
|
|
157512
|
+
const module = typeof bindings.module === "string" ? bindings.module : undefined;
|
|
157513
|
+
if (module) {
|
|
157514
|
+
const stall = RECOVERY_STALL_MARKERS.get(`${module}\x00${message}`);
|
|
157515
|
+
if (stall) {
|
|
157516
|
+
const mintUrl = typeof metadata.mintUrl === "string" ? metadata.mintUrl : undefined;
|
|
157517
|
+
startupProgress(mintUrl ? `${stall} (${mintUrl})` : stall);
|
|
157518
|
+
}
|
|
157519
|
+
}
|
|
157520
|
+
}
|
|
157292
157521
|
cocoLogger[level](`[coco] ${message}`, ...Object.keys(metadata).length > 0 ? [metadata] : []);
|
|
157293
157522
|
};
|
|
157294
157523
|
return {
|
|
@@ -157325,13 +157554,22 @@ function saveConfig(config2, configFile) {
|
|
|
157325
157554
|
throw error53;
|
|
157326
157555
|
}
|
|
157327
157556
|
}
|
|
157557
|
+
function isZombieProcess(pid, readFile7 = (path2) => readFileSync2(path2, "utf-8")) {
|
|
157558
|
+
try {
|
|
157559
|
+
const stat2 = readFile7(`/proc/${pid}/stat`);
|
|
157560
|
+
const commandEnd = stat2.lastIndexOf(")");
|
|
157561
|
+
return commandEnd >= 0 && stat2.charAt(commandEnd + 2) === "Z";
|
|
157562
|
+
} catch {
|
|
157563
|
+
return false;
|
|
157564
|
+
}
|
|
157565
|
+
}
|
|
157328
157566
|
function defaultIsProcessRunning(pid) {
|
|
157329
157567
|
try {
|
|
157330
157568
|
process.kill(pid, 0);
|
|
157331
|
-
return true;
|
|
157332
157569
|
} catch (error53) {
|
|
157333
157570
|
return error53.code === "EPERM";
|
|
157334
157571
|
}
|
|
157572
|
+
return !isZombieProcess(pid);
|
|
157335
157573
|
}
|
|
157336
157574
|
function hasErrorCode(error53, codes) {
|
|
157337
157575
|
let current = error53;
|
|
@@ -157348,24 +157586,7 @@ function hasErrorCode(error53, codes) {
|
|
|
157348
157586
|
}
|
|
157349
157587
|
async function assertLegacyCocodNotRunning(options = {}) {
|
|
157350
157588
|
const socketPath = options.socketPath || legacyCocodSocketPath();
|
|
157351
|
-
const pidFilePath = options.pidFilePath || legacyCocodPidPath();
|
|
157352
157589
|
const pathExists = options.pathExists || existsSync9;
|
|
157353
|
-
const readFile7 = options.readFile || ((path2) => readFileSync2(path2, "utf-8"));
|
|
157354
|
-
const isProcessRunning2 = options.isProcessRunning || defaultIsProcessRunning;
|
|
157355
|
-
const getRunningLegacyPid = () => {
|
|
157356
|
-
if (!pathExists(pidFilePath))
|
|
157357
|
-
return null;
|
|
157358
|
-
try {
|
|
157359
|
-
const pid = Number.parseInt(readFile7(pidFilePath).trim(), 10);
|
|
157360
|
-
return Number.isInteger(pid) && pid > 0 && pid !== options.ignorePid && isProcessRunning2(pid) ? pid : null;
|
|
157361
|
-
} catch {
|
|
157362
|
-
return null;
|
|
157363
|
-
}
|
|
157364
|
-
};
|
|
157365
|
-
const runningPid = getRunningLegacyPid();
|
|
157366
|
-
if (runningPid !== null) {
|
|
157367
|
-
throw new Error(`Legacy cocod daemon is still running with PID ${runningPid}. ` + "Refusing to open the wallet database because cocod and coco-core cannot safely use it at the same time. " + `Run 'cocod stop' or 'kill ${runningPid}' and try again.`);
|
|
157368
|
-
}
|
|
157369
157590
|
if (!pathExists(socketPath))
|
|
157370
157591
|
return;
|
|
157371
157592
|
const fetchImpl = options.fetchImpl || fetch;
|
|
@@ -157378,12 +157599,8 @@ async function assertLegacyCocodNotRunning(options = {}) {
|
|
|
157378
157599
|
await response3.body?.cancel();
|
|
157379
157600
|
} catch (error53) {
|
|
157380
157601
|
if (hasErrorCode(error53, STALE_SOCKET_ERROR_CODES)) {
|
|
157381
|
-
|
|
157382
|
-
|
|
157383
|
-
logger5.debug(`Ignoring stale legacy cocod socket at ${socketPath}`);
|
|
157384
|
-
return;
|
|
157385
|
-
}
|
|
157386
|
-
throw new Error(`Legacy cocod daemon is still running with PID ${newlyRunningPid}. ` + "Refusing to open the wallet database because cocod and coco-core cannot safely use it at the same time. " + `Run 'cocod stop' or 'kill ${newlyRunningPid}' and try again.`, { cause: error53 });
|
|
157602
|
+
logger5.debug(`Ignoring stale legacy cocod socket at ${socketPath}`);
|
|
157603
|
+
return;
|
|
157387
157604
|
}
|
|
157388
157605
|
throw new Error(`Cannot verify whether the legacy cocod daemon has stopped at ${socketPath}. ` + "Refusing to open the wallet database to prevent concurrent access. " + "Run 'cocod stop', verify the daemon has exited, and try again.", { cause: error53 });
|
|
157389
157606
|
}
|
|
@@ -157447,12 +157664,14 @@ async function stopLegacyCocod(options = {}) {
|
|
|
157447
157664
|
function claimLegacyCocodPidFile(options = {}) {
|
|
157448
157665
|
return claimPidFile({
|
|
157449
157666
|
...options,
|
|
157450
|
-
pidFilePath: options.pidFilePath || legacyCocodPidPath()
|
|
157667
|
+
pidFilePath: options.pidFilePath || legacyCocodPidPath(),
|
|
157668
|
+
label: options.label || "legacy cocod exclusion lock"
|
|
157451
157669
|
});
|
|
157452
157670
|
}
|
|
157453
157671
|
function claimPidFile(options) {
|
|
157454
157672
|
const pidFilePath = options.pidFilePath;
|
|
157455
157673
|
const pid = options.pid ?? process.pid;
|
|
157674
|
+
const label = options.label || "wallet process lock";
|
|
157456
157675
|
const openExclusive = options.openExclusive || ((path2) => openSync(path2, "wx", 384));
|
|
157457
157676
|
const writePid = options.writePid || ((fd2, ownerPid) => writeFileSync(fd2, String(ownerPid)));
|
|
157458
157677
|
const closeFile = options.closeFile || closeSync;
|
|
@@ -157469,16 +157688,17 @@ function claimPidFile(options) {
|
|
|
157469
157688
|
try {
|
|
157470
157689
|
stalePid = Number.parseInt(readFile7(pidFilePath).trim(), 10);
|
|
157471
157690
|
} catch {
|
|
157472
|
-
throw new Error(`Cannot claim the
|
|
157691
|
+
throw new Error(`Cannot claim the ${label} at ${pidFilePath}. ` + "Another cocod or routstrd process may be starting. Stop it and try again.", { cause: error53 });
|
|
157473
157692
|
}
|
|
157474
157693
|
if (!Number.isInteger(stalePid) || stalePid <= 0 || isProcessRunning2(stalePid)) {
|
|
157475
|
-
|
|
157694
|
+
const ownerMessage = Number.isInteger(stalePid) && stalePid > 0 ? `PID ${stalePid} is still running and holds it. ` + `Stop that process first ('routstrd stop' or 'kill ${stalePid}').` : "Another cocod or routstrd process may be starting. Stop it and try again.";
|
|
157695
|
+
throw new Error(`Cannot claim the ${label} at ${pidFilePath}: ${ownerMessage}`, { cause: error53 });
|
|
157476
157696
|
}
|
|
157477
157697
|
try {
|
|
157478
157698
|
removeFile(pidFilePath);
|
|
157479
157699
|
fd = openExclusive(pidFilePath);
|
|
157480
157700
|
} catch (retryError) {
|
|
157481
|
-
throw new Error(`Cannot claim the
|
|
157701
|
+
throw new Error(`Cannot claim the ${label} at ${pidFilePath}. ` + "Another cocod or routstrd process may be starting. Stop it and try again.", { cause: retryError });
|
|
157482
157702
|
}
|
|
157483
157703
|
}
|
|
157484
157704
|
try {
|
|
@@ -157492,20 +157712,180 @@ function claimPidFile(options) {
|
|
|
157492
157712
|
closeFile(fd);
|
|
157493
157713
|
}
|
|
157494
157714
|
let released = false;
|
|
157495
|
-
|
|
157715
|
+
const release = () => {
|
|
157496
157716
|
if (released)
|
|
157497
157717
|
return;
|
|
157498
157718
|
released = true;
|
|
157719
|
+
process.removeListener("exit", release);
|
|
157499
157720
|
try {
|
|
157500
157721
|
if (readFile7(pidFilePath).trim() === String(pid)) {
|
|
157501
157722
|
removeFile(pidFilePath);
|
|
157502
157723
|
}
|
|
157503
157724
|
} catch (error53) {
|
|
157504
157725
|
if (error53.code !== "ENOENT") {
|
|
157505
|
-
logger5.warn(`Failed to release
|
|
157726
|
+
logger5.warn(`Failed to release ${label} at ${pidFilePath}:`, error53);
|
|
157506
157727
|
}
|
|
157507
157728
|
}
|
|
157508
157729
|
};
|
|
157730
|
+
process.once("exit", release);
|
|
157731
|
+
return release;
|
|
157732
|
+
}
|
|
157733
|
+
function constructCocoManager(repo, seed) {
|
|
157734
|
+
return new Manager(repo, async () => seed, createCocoLogger());
|
|
157735
|
+
}
|
|
157736
|
+
async function enableCocoManager(coco) {
|
|
157737
|
+
await coco.initPlugins();
|
|
157738
|
+
await coco.reconcileLegacyMintQuotes();
|
|
157739
|
+
await coco.enableMintOperationWatcher();
|
|
157740
|
+
await coco.enableProofStateWatcher();
|
|
157741
|
+
await coco.enableMintOperationProcessor();
|
|
157742
|
+
}
|
|
157743
|
+
var EXPIRED_MINT_OBSERVATION_DEADLINE_MS = 15000;
|
|
157744
|
+
function withTimeout(promise2, timeoutMs) {
|
|
157745
|
+
let timer6;
|
|
157746
|
+
const timeout6 = new Promise((_resolve, reject) => {
|
|
157747
|
+
timer6 = setTimeout(() => reject(new Error("Timed out contacting mint")), timeoutMs);
|
|
157748
|
+
});
|
|
157749
|
+
return Promise.race([promise2, timeout6]).finally(() => {
|
|
157750
|
+
if (timer6 !== undefined)
|
|
157751
|
+
clearTimeout(timer6);
|
|
157752
|
+
});
|
|
157753
|
+
}
|
|
157754
|
+
async function settleExpiredMintQuotes(source, nowMs, deadlineMs = EXPIRED_MINT_OBSERVATION_DEADLINE_MS) {
|
|
157755
|
+
const pendingMints = await source.ops.mint.listPending();
|
|
157756
|
+
const selection = selectCleanupOperations({
|
|
157757
|
+
mints: pendingMints,
|
|
157758
|
+
sends: [],
|
|
157759
|
+
melts: [],
|
|
157760
|
+
nowMs,
|
|
157761
|
+
minAgeMs: 0
|
|
157762
|
+
});
|
|
157763
|
+
const settlement = {
|
|
157764
|
+
failed: 0,
|
|
157765
|
+
leftForRecovery: 0,
|
|
157766
|
+
unobserved: 0
|
|
157767
|
+
};
|
|
157768
|
+
const candidates = selection.mintsToFail;
|
|
157769
|
+
if (candidates.length === 0)
|
|
157770
|
+
return settlement;
|
|
157771
|
+
const startedAt = Date.now();
|
|
157772
|
+
for (const op of candidates) {
|
|
157773
|
+
const remainingMs = deadlineMs - (Date.now() - startedAt);
|
|
157774
|
+
if (remainingMs <= 0) {
|
|
157775
|
+
const skipped = candidates.length - settlement.failed - settlement.leftForRecovery - settlement.unobserved;
|
|
157776
|
+
settlement.unobserved += skipped;
|
|
157777
|
+
startupProgress(`Expired mint quote check budget exhausted; ${skipped} quote(s) left for mint recovery.`);
|
|
157778
|
+
break;
|
|
157779
|
+
}
|
|
157780
|
+
try {
|
|
157781
|
+
const result = await withTimeout(source.mintOperationService.observePendingOperation(op.id), remainingMs);
|
|
157782
|
+
if (result.category === "waiting") {
|
|
157783
|
+
await source.mintOperationService.failPendingOperation({ id: op.id }, {
|
|
157784
|
+
reason: "Expired mint quote confirmed unpaid by mint",
|
|
157785
|
+
retryable: false,
|
|
157786
|
+
observedAt: Date.now()
|
|
157787
|
+
});
|
|
157788
|
+
settlement.failed++;
|
|
157789
|
+
} else {
|
|
157790
|
+
settlement.leftForRecovery++;
|
|
157791
|
+
const observed = result.category === "ready" ? "was paid at the mint" : result.category === "completed" ? "was already issued at the mint" : "failed terminally at the mint";
|
|
157792
|
+
startupProgress(`Expired mint quote ${op.quoteId ?? op.id} at ${op.mintUrl} ${observed}; leaving it for mint recovery.`);
|
|
157793
|
+
}
|
|
157794
|
+
} catch (error53) {
|
|
157795
|
+
settlement.unobserved++;
|
|
157796
|
+
logger5.warn("Could not check expired mint quote; leaving it pending", {
|
|
157797
|
+
operationId: op.id,
|
|
157798
|
+
mintUrl: op.mintUrl,
|
|
157799
|
+
error: error53 instanceof Error ? error53.message : String(error53)
|
|
157800
|
+
});
|
|
157801
|
+
}
|
|
157802
|
+
}
|
|
157803
|
+
return settlement;
|
|
157804
|
+
}
|
|
157805
|
+
async function reconcileDuplicateReceiveOperations(coco, repo) {
|
|
157806
|
+
const internals = coco;
|
|
157807
|
+
const unavailableMints = new Set;
|
|
157808
|
+
const deadline = Date.now() + 45000;
|
|
157809
|
+
const ensureMintBudget = (mintUrl) => {
|
|
157810
|
+
if (Date.now() >= deadline)
|
|
157811
|
+
throw new Error("Receive cleanup time budget exhausted");
|
|
157812
|
+
if (unavailableMints.has(mintUrl))
|
|
157813
|
+
throw new Error("Mint already failed receive cleanup");
|
|
157814
|
+
};
|
|
157815
|
+
const markMintFailure = (mintUrl, error53) => {
|
|
157816
|
+
unavailableMints.add(mintUrl);
|
|
157817
|
+
throw error53;
|
|
157818
|
+
};
|
|
157819
|
+
const source = {
|
|
157820
|
+
listExecuting: () => repo.receiveOperationRepository.getByState("executing"),
|
|
157821
|
+
checkProofStates: async (operation) => {
|
|
157822
|
+
ensureMintBudget(operation.mintUrl);
|
|
157823
|
+
try {
|
|
157824
|
+
return await withTimeout(internals.receiveOperationService.checkProofStatesWithMint(operation.mintUrl, operation.inputProofs), Math.min(15000, Math.max(1, deadline - Date.now())));
|
|
157825
|
+
} catch (error53) {
|
|
157826
|
+
return markMintFailure(operation.mintUrl, error53);
|
|
157827
|
+
}
|
|
157828
|
+
},
|
|
157829
|
+
restoreOutputs: async (mintUrl, outputs) => {
|
|
157830
|
+
ensureMintBudget(mintUrl);
|
|
157831
|
+
const restored = [];
|
|
157832
|
+
const mint = internals.mintAdapter.getCashuMint(mintUrl);
|
|
157833
|
+
for (let index = 0;index < outputs.length; index += 300) {
|
|
157834
|
+
try {
|
|
157835
|
+
const response3 = await withTimeout(mint.restore({ outputs: outputs.slice(index, index + 300) }), Math.min(15000, Math.max(1, deadline - Date.now())));
|
|
157836
|
+
restored.push(...response3.outputs);
|
|
157837
|
+
} catch (error53) {
|
|
157838
|
+
return markMintFailure(mintUrl, error53);
|
|
157839
|
+
}
|
|
157840
|
+
}
|
|
157841
|
+
return restored;
|
|
157842
|
+
},
|
|
157843
|
+
hasSavedOutputs: (operation) => internals.receiveOperationService.hasSavedOutputs(operation),
|
|
157844
|
+
rollBack: async (operation, reason) => {
|
|
157845
|
+
await internals.receiveOperationService.markAsRolledBack(operation, reason);
|
|
157846
|
+
}
|
|
157847
|
+
};
|
|
157848
|
+
return reconcileExecutingReceives(source);
|
|
157849
|
+
}
|
|
157850
|
+
async function runWalletRecovery(coco, onProgress, receiveOperationIds) {
|
|
157851
|
+
surfacingRecoveryProgress = true;
|
|
157852
|
+
let failedMintQuotes = 0;
|
|
157853
|
+
try {
|
|
157854
|
+
onProgress({ phase: "Settling expired mint quotes", failedMintQuotes });
|
|
157855
|
+
const settlement = await settleExpiredMintQuotes({
|
|
157856
|
+
ops: coco.ops,
|
|
157857
|
+
mintOperationService: coco.mintOperationService
|
|
157858
|
+
}, Date.now());
|
|
157859
|
+
failedMintQuotes = settlement.failed;
|
|
157860
|
+
if (settlement.leftForRecovery > 0 || settlement.unobserved > 0) {
|
|
157861
|
+
startupProgress(`Expired mint quotes: ${settlement.failed} failed locally, ` + `${settlement.leftForRecovery} paid/issued (kept for recovery), ` + `${settlement.unobserved} unverifiable (kept pending).`);
|
|
157862
|
+
}
|
|
157863
|
+
onProgress({ phase: "Settled expired mint quotes", failedMintQuotes });
|
|
157864
|
+
onProgress({ phase: "Send recovery", failedMintQuotes });
|
|
157865
|
+
await coco.ops.send.recovery.run();
|
|
157866
|
+
onProgress({ phase: "Melt recovery", failedMintQuotes });
|
|
157867
|
+
await coco.ops.melt.recovery.run();
|
|
157868
|
+
onProgress({ phase: "Receive recovery", failedMintQuotes });
|
|
157869
|
+
if (receiveOperationIds) {
|
|
157870
|
+
for (const operationId of receiveOperationIds) {
|
|
157871
|
+
try {
|
|
157872
|
+
await withTimeout(coco.ops.receive.refresh(operationId), 15000);
|
|
157873
|
+
} catch (error53) {
|
|
157874
|
+
logger5.warn("Targeted receive recovery did not complete", {
|
|
157875
|
+
operationId,
|
|
157876
|
+
error: error53 instanceof Error ? error53.message : String(error53)
|
|
157877
|
+
});
|
|
157878
|
+
}
|
|
157879
|
+
}
|
|
157880
|
+
} else {
|
|
157881
|
+
await coco.ops.receive.recovery.run();
|
|
157882
|
+
}
|
|
157883
|
+
onProgress({ phase: "Mint recovery", failedMintQuotes });
|
|
157884
|
+
await coco.recoverPendingMintOperations();
|
|
157885
|
+
onProgress({ phase: "done", failedMintQuotes });
|
|
157886
|
+
} finally {
|
|
157887
|
+
surfacingRecoveryProgress = false;
|
|
157888
|
+
}
|
|
157509
157889
|
}
|
|
157510
157890
|
async function createCocoClient(options = {}) {
|
|
157511
157891
|
const configDir = options.walletDir || options.configDir || walletDir();
|
|
@@ -157516,12 +157896,12 @@ async function createCocoClient(options = {}) {
|
|
|
157516
157896
|
const legacyPidFile = options.legacyPidPath || legacyCocodPidPath();
|
|
157517
157897
|
const npcBaseUrl = options.npcBaseUrl || NPC_DEFAULT_BASE_URL;
|
|
157518
157898
|
const npcAddressDomain = new URL(npcBaseUrl).host;
|
|
157519
|
-
await assertLegacyCocodNotRunning({
|
|
157520
|
-
socketPath: legacySocket,
|
|
157521
|
-
pidFilePath: legacyPidFile
|
|
157522
|
-
});
|
|
157899
|
+
await assertLegacyCocodNotRunning({ socketPath: legacySocket });
|
|
157523
157900
|
mkdirSync8(dirname7(legacyPidFile), { recursive: true, mode: 448 });
|
|
157524
|
-
const releaseWalletPidClaim = claimPidFile({
|
|
157901
|
+
const releaseWalletPidClaim = claimPidFile({
|
|
157902
|
+
pidFilePath: walletPidFile,
|
|
157903
|
+
label: "routstrd wallet lock"
|
|
157904
|
+
});
|
|
157525
157905
|
let releaseLegacyPidClaim;
|
|
157526
157906
|
try {
|
|
157527
157907
|
releaseLegacyPidClaim = claimLegacyCocodPidFile({
|
|
@@ -157533,7 +157913,21 @@ async function createCocoClient(options = {}) {
|
|
|
157533
157913
|
}
|
|
157534
157914
|
let database;
|
|
157535
157915
|
let coco;
|
|
157916
|
+
let findFinalizedReceiveSibling = async () => null;
|
|
157536
157917
|
let walletConfig = loadConfig2(configFile);
|
|
157918
|
+
let recoveryPhase = "queued";
|
|
157919
|
+
let recoveryFailedMintQuotes = 0;
|
|
157920
|
+
let recoveryDone = false;
|
|
157921
|
+
let recoveryError;
|
|
157922
|
+
const recoveryCounts = {
|
|
157923
|
+
pendingSends: 0,
|
|
157924
|
+
inflightProofs: 0,
|
|
157925
|
+
pendingMints: 0
|
|
157926
|
+
};
|
|
157927
|
+
let recoveryResolve;
|
|
157928
|
+
const recoveryPromise = new Promise((resolve) => {
|
|
157929
|
+
recoveryResolve = resolve;
|
|
157930
|
+
});
|
|
157537
157931
|
try {
|
|
157538
157932
|
startupProgress("Opening Cashu wallet database...");
|
|
157539
157933
|
const mnemonic = walletConfig.mnemonic;
|
|
@@ -157541,22 +157935,88 @@ async function createCocoClient(options = {}) {
|
|
|
157541
157935
|
database = new Database2(dbPath);
|
|
157542
157936
|
const repo = new SqliteRepositories({ database });
|
|
157543
157937
|
await repo.init();
|
|
157938
|
+
initReceiveDedupSchema(database);
|
|
157939
|
+
const interruptedReservations = clearInterruptedReceiveReservations(database);
|
|
157940
|
+
if (interruptedReservations > 0) {
|
|
157941
|
+
logger5.warn("Cleared interrupted receive reservations with no Coco operation", {
|
|
157942
|
+
count: interruptedReservations
|
|
157943
|
+
});
|
|
157944
|
+
}
|
|
157544
157945
|
const [pendingSends, inflightProofs, pendingMints] = await Promise.all([
|
|
157545
157946
|
repo.sendOperationRepository.getPending(),
|
|
157546
157947
|
repo.proofRepository.getInflightProofs(),
|
|
157547
157948
|
repo.mintOperationRepository.getPending()
|
|
157548
157949
|
]);
|
|
157549
|
-
|
|
157950
|
+
recoveryCounts.pendingSends = pendingSends.length;
|
|
157951
|
+
recoveryCounts.inflightProofs = inflightProofs.length;
|
|
157952
|
+
recoveryCounts.pendingMints = pendingMints.length;
|
|
157953
|
+
const recoveryCount = recoveryCounts.pendingSends + recoveryCounts.inflightProofs + recoveryCounts.pendingMints;
|
|
157550
157954
|
if (recoveryCount > 0) {
|
|
157551
|
-
startupProgress(`Recovering wallet state: ${pendingSends
|
|
157955
|
+
startupProgress(`Recovering wallet state in background: ${recoveryCounts.pendingSends} pending sends, ` + `${recoveryCounts.inflightProofs} in-flight proofs, ${recoveryCounts.pendingMints} pending mints.`);
|
|
157552
157956
|
} else {
|
|
157553
157957
|
startupProgress("Initializing Cashu wallet...");
|
|
157554
157958
|
}
|
|
157555
|
-
coco =
|
|
157556
|
-
|
|
157557
|
-
|
|
157558
|
-
|
|
157559
|
-
|
|
157959
|
+
coco = constructCocoManager(repo, seed);
|
|
157960
|
+
const executingReceives = await repo.receiveOperationRepository.getByState("executing");
|
|
157961
|
+
let receiveRecoveryOperationIds;
|
|
157962
|
+
if (executingReceives.length > 0) {
|
|
157963
|
+
startupProgress(`Checking ${executingReceives.length} unfinished Cashu receive operation(s) for duplicates...`);
|
|
157964
|
+
const recordedBackup = getReceiveReconcileBackup(database);
|
|
157965
|
+
if (!recordedBackup || !existsSync9(recordedBackup)) {
|
|
157966
|
+
const backupPath = `${dbPath}.pre-receive-reconcile-${Date.now()}`;
|
|
157967
|
+
database.exec(`VACUUM INTO '${backupPath.replaceAll("'", "''")}'`);
|
|
157968
|
+
setReceiveReconcileBackup(database, backupPath);
|
|
157969
|
+
startupProgress(`Created wallet backup before receive cleanup: ${backupPath}`);
|
|
157970
|
+
}
|
|
157971
|
+
const receiveReconcile = await reconcileDuplicateReceiveOperations(coco, repo);
|
|
157972
|
+
receiveRecoveryOperationIds = receiveReconcile.recoveryOperationIds;
|
|
157973
|
+
startupProgress(`Receive cleanup: ${receiveReconcile.executing} operation(s), ` + `${receiveReconcile.uniqueGroups} unique input set(s), ` + `${receiveReconcile.rolledBack} stale duplicate(s) retired, ` + `${receiveReconcile.unresolved} unresolved.`);
|
|
157974
|
+
}
|
|
157975
|
+
await enableCocoManager(coco);
|
|
157976
|
+
const openDatabase = database;
|
|
157977
|
+
findFinalizedReceiveSibling = async (operation) => {
|
|
157978
|
+
if (!operation)
|
|
157979
|
+
return null;
|
|
157980
|
+
const fingerprint = receiveInputFingerprint(operation);
|
|
157981
|
+
const siblings = await repo.receiveOperationRepository.getByMintUrl(operation.mintUrl);
|
|
157982
|
+
return siblings.find((candidate) => candidate.state === "finalized" && receiveInputFingerprint(candidate) === fingerprint)?.id ?? null;
|
|
157983
|
+
};
|
|
157984
|
+
const syncReceiveReservations = async () => {
|
|
157985
|
+
for (const reservation of listProcessingReceiveTokens(openDatabase)) {
|
|
157986
|
+
if (!reservation.operationId)
|
|
157987
|
+
continue;
|
|
157988
|
+
try {
|
|
157989
|
+
const operation = await coco.ops.receive.get(reservation.operationId);
|
|
157990
|
+
if (operation?.state === "finalized") {
|
|
157991
|
+
updateReceiveToken(openDatabase, reservation.tokenHash, {
|
|
157992
|
+
state: "succeeded",
|
|
157993
|
+
operationId: operation.id
|
|
157994
|
+
});
|
|
157995
|
+
} else if (operation?.state === "rolled_back") {
|
|
157996
|
+
const finalizedSibling = await findFinalizedReceiveSibling(operation);
|
|
157997
|
+
updateReceiveToken(openDatabase, reservation.tokenHash, finalizedSibling ? {
|
|
157998
|
+
state: "succeeded",
|
|
157999
|
+
operationId: finalizedSibling
|
|
158000
|
+
} : {
|
|
158001
|
+
state: "failed",
|
|
158002
|
+
operationId: operation.id,
|
|
158003
|
+
error: operation.error || "Token receive was rolled back"
|
|
158004
|
+
});
|
|
158005
|
+
} else if (operation?.state === "prepared") {
|
|
158006
|
+
await coco.ops.receive.cancel(operation.id, "Cancelled interrupted receive before execution");
|
|
158007
|
+
deleteReceiveTokenReservation(openDatabase, reservation.tokenHash);
|
|
158008
|
+
} else if (!operation) {
|
|
158009
|
+
deleteReceiveTokenReservation(openDatabase, reservation.tokenHash);
|
|
158010
|
+
}
|
|
158011
|
+
} catch (error53) {
|
|
158012
|
+
logger5.warn("Could not reconcile receive token reservation", {
|
|
158013
|
+
operationId: reservation.operationId,
|
|
158014
|
+
error: error53 instanceof Error ? error53.message : String(error53)
|
|
158015
|
+
});
|
|
158016
|
+
}
|
|
158017
|
+
}
|
|
158018
|
+
};
|
|
158019
|
+
await syncReceiveReservations();
|
|
157560
158020
|
const trustedMints = await coco.mint.getAllTrustedMints();
|
|
157561
158021
|
const configuredDefault = walletConfig.defaultMintUrl;
|
|
157562
158022
|
const defaultMintUrl = normalizeMintUrl(configuredDefault || trustedMints[0]?.mintUrl || DEFAULT_MINT_URL);
|
|
@@ -157579,6 +158039,25 @@ async function createCocoClient(options = {}) {
|
|
|
157579
158039
|
coco.use(npcPlugin);
|
|
157580
158040
|
}
|
|
157581
158041
|
startupProgress("Cashu wallet ready.");
|
|
158042
|
+
runWalletRecovery(coco, (progress) => {
|
|
158043
|
+
recoveryPhase = progress.phase;
|
|
158044
|
+
recoveryFailedMintQuotes = progress.failedMintQuotes;
|
|
158045
|
+
if (progress.phase !== "done") {
|
|
158046
|
+
startupProgress(`Wallet recovery: ${progress.phase}...`);
|
|
158047
|
+
}
|
|
158048
|
+
}, receiveRecoveryOperationIds).then(async () => {
|
|
158049
|
+
await syncReceiveReservations();
|
|
158050
|
+
recoveryDone = true;
|
|
158051
|
+
recoveryPhase = "done";
|
|
158052
|
+
recoveryResolve?.();
|
|
158053
|
+
startupProgress("Wallet recovery complete.");
|
|
158054
|
+
}).catch((error53) => {
|
|
158055
|
+
recoveryDone = true;
|
|
158056
|
+
recoveryPhase = "error";
|
|
158057
|
+
recoveryError = error53 instanceof Error ? error53.message : String(error53);
|
|
158058
|
+
recoveryResolve?.();
|
|
158059
|
+
startupProgress(`Wallet recovery failed: ${recoveryError}`);
|
|
158060
|
+
});
|
|
157582
158061
|
} catch (error53) {
|
|
157583
158062
|
database?.close();
|
|
157584
158063
|
releaseLegacyPidClaim();
|
|
@@ -157593,6 +158072,13 @@ async function createCocoClient(options = {}) {
|
|
|
157593
158072
|
return api2;
|
|
157594
158073
|
};
|
|
157595
158074
|
let disposed = false;
|
|
158075
|
+
const waitForRecovery = async () => {
|
|
158076
|
+
if (!recoveryDone)
|
|
158077
|
+
await recoveryPromise;
|
|
158078
|
+
if (recoveryError) {
|
|
158079
|
+
throw new Error(`Wallet is not ready: ${recoveryError}`);
|
|
158080
|
+
}
|
|
158081
|
+
};
|
|
157596
158082
|
return {
|
|
157597
158083
|
async ping() {
|
|
157598
158084
|
try {
|
|
@@ -157603,6 +158089,10 @@ async function createCocoClient(options = {}) {
|
|
|
157603
158089
|
}
|
|
157604
158090
|
},
|
|
157605
158091
|
async getStatus() {
|
|
158092
|
+
if (recoveryError)
|
|
158093
|
+
return "ERROR";
|
|
158094
|
+
if (!recoveryDone)
|
|
158095
|
+
return "RECOVERING";
|
|
157606
158096
|
try {
|
|
157607
158097
|
await coco.wallet.balances.total();
|
|
157608
158098
|
return "UNLOCKED";
|
|
@@ -157610,6 +158100,17 @@ async function createCocoClient(options = {}) {
|
|
|
157610
158100
|
return "ERROR";
|
|
157611
158101
|
}
|
|
157612
158102
|
},
|
|
158103
|
+
async getRecoveryProgress() {
|
|
158104
|
+
return {
|
|
158105
|
+
state: recoveryError ? "ERROR" : recoveryDone ? "UNLOCKED" : "RECOVERING",
|
|
158106
|
+
phase: recoveryPhase,
|
|
158107
|
+
pendingSends: recoveryCounts.pendingSends,
|
|
158108
|
+
inflightProofs: recoveryCounts.inflightProofs,
|
|
158109
|
+
pendingMints: recoveryCounts.pendingMints,
|
|
158110
|
+
failedMintQuotes: recoveryFailedMintQuotes,
|
|
158111
|
+
...recoveryError ? { error: recoveryError } : {}
|
|
158112
|
+
};
|
|
158113
|
+
},
|
|
157613
158114
|
async unlock(_passphrase) {
|
|
157614
158115
|
return "wallet does not require unlocking";
|
|
157615
158116
|
},
|
|
@@ -157621,10 +158122,120 @@ async function createCocoClient(options = {}) {
|
|
|
157621
158122
|
]));
|
|
157622
158123
|
},
|
|
157623
158124
|
async receiveCashu(token) {
|
|
157624
|
-
|
|
157625
|
-
|
|
158125
|
+
const reservation = reserveReceiveToken(database, token);
|
|
158126
|
+
if (!reservation.acquired) {
|
|
158127
|
+
if (reservation.existing?.state === "succeeded") {
|
|
158128
|
+
return "Token already received successfully";
|
|
158129
|
+
}
|
|
158130
|
+
if (reservation.existing?.state === "processing" && reservation.existing.operationId) {
|
|
158131
|
+
try {
|
|
158132
|
+
const operation = await withTimeout(coco.ops.receive.refresh(reservation.existing.operationId), 15000);
|
|
158133
|
+
if (operation.state === "finalized") {
|
|
158134
|
+
updateReceiveToken(database, reservation.tokenHash, {
|
|
158135
|
+
state: "succeeded",
|
|
158136
|
+
operationId: operation.id
|
|
158137
|
+
});
|
|
158138
|
+
return "Token received successfully";
|
|
158139
|
+
}
|
|
158140
|
+
if (operation.state === "rolled_back") {
|
|
158141
|
+
const finalizedSibling = await findFinalizedReceiveSibling(operation);
|
|
158142
|
+
if (finalizedSibling) {
|
|
158143
|
+
updateReceiveToken(database, reservation.tokenHash, {
|
|
158144
|
+
state: "succeeded",
|
|
158145
|
+
operationId: finalizedSibling
|
|
158146
|
+
});
|
|
158147
|
+
return "Token already received successfully";
|
|
158148
|
+
}
|
|
158149
|
+
updateReceiveToken(database, reservation.tokenHash, {
|
|
158150
|
+
state: "failed",
|
|
158151
|
+
operationId: operation.id,
|
|
158152
|
+
error: operation.error || "Token receive was rolled back"
|
|
158153
|
+
});
|
|
158154
|
+
throw new Error(operation.error || "Token receive was rolled back");
|
|
158155
|
+
}
|
|
158156
|
+
} catch (error53) {
|
|
158157
|
+
const latest = await coco.ops.receive.get(reservation.existing.operationId);
|
|
158158
|
+
if (latest?.state === "finalized") {
|
|
158159
|
+
updateReceiveToken(database, reservation.tokenHash, {
|
|
158160
|
+
state: "succeeded",
|
|
158161
|
+
operationId: latest.id
|
|
158162
|
+
});
|
|
158163
|
+
return "Token received successfully";
|
|
158164
|
+
}
|
|
158165
|
+
throw error53;
|
|
158166
|
+
}
|
|
158167
|
+
throw new Error("Token receive is still unresolved");
|
|
158168
|
+
}
|
|
158169
|
+
if (reservation.existing?.state === "processing") {
|
|
158170
|
+
throw new Error("Token receive is already in progress");
|
|
158171
|
+
}
|
|
158172
|
+
throw new Error(reservation.existing?.error || "Token receive previously failed");
|
|
158173
|
+
}
|
|
158174
|
+
let preparedOperationId;
|
|
158175
|
+
try {
|
|
158176
|
+
await waitForRecovery();
|
|
158177
|
+
const prepared = await coco.ops.receive.prepare({ token });
|
|
158178
|
+
preparedOperationId = prepared.id;
|
|
158179
|
+
updateReceiveToken(database, reservation.tokenHash, {
|
|
158180
|
+
state: "processing",
|
|
158181
|
+
operationId: prepared.id
|
|
158182
|
+
});
|
|
158183
|
+
await coco.ops.receive.execute(prepared.id);
|
|
158184
|
+
updateReceiveToken(database, reservation.tokenHash, {
|
|
158185
|
+
state: "succeeded",
|
|
158186
|
+
operationId: prepared.id
|
|
158187
|
+
});
|
|
158188
|
+
return "Token received successfully";
|
|
158189
|
+
} catch (error53) {
|
|
158190
|
+
const message = error53 instanceof Error ? error53.message : String(error53);
|
|
158191
|
+
if (preparedOperationId) {
|
|
158192
|
+
let latest = null;
|
|
158193
|
+
try {
|
|
158194
|
+
latest = await coco.ops.receive.get(preparedOperationId);
|
|
158195
|
+
} catch (lookupError) {
|
|
158196
|
+
logger5.warn("Could not inspect failed receive operation", {
|
|
158197
|
+
operationId: preparedOperationId,
|
|
158198
|
+
error: lookupError instanceof Error ? lookupError.message : String(lookupError)
|
|
158199
|
+
});
|
|
158200
|
+
}
|
|
158201
|
+
if (latest?.state === "finalized") {
|
|
158202
|
+
updateReceiveToken(database, reservation.tokenHash, {
|
|
158203
|
+
state: "succeeded",
|
|
158204
|
+
operationId: preparedOperationId
|
|
158205
|
+
});
|
|
158206
|
+
return "Token received successfully";
|
|
158207
|
+
}
|
|
158208
|
+
if (latest?.state === "executing") {
|
|
158209
|
+
updateReceiveToken(database, reservation.tokenHash, {
|
|
158210
|
+
state: "processing",
|
|
158211
|
+
operationId: preparedOperationId,
|
|
158212
|
+
error: message
|
|
158213
|
+
});
|
|
158214
|
+
} else if (latest?.state === "rolled_back") {
|
|
158215
|
+
const finalizedSibling = await findFinalizedReceiveSibling(latest);
|
|
158216
|
+
if (finalizedSibling) {
|
|
158217
|
+
updateReceiveToken(database, reservation.tokenHash, {
|
|
158218
|
+
state: "succeeded",
|
|
158219
|
+
operationId: finalizedSibling
|
|
158220
|
+
});
|
|
158221
|
+
return "Token already received successfully";
|
|
158222
|
+
}
|
|
158223
|
+
updateReceiveToken(database, reservation.tokenHash, {
|
|
158224
|
+
state: "failed",
|
|
158225
|
+
operationId: preparedOperationId,
|
|
158226
|
+
error: latest.error || message
|
|
158227
|
+
});
|
|
158228
|
+
} else {
|
|
158229
|
+
deleteReceiveTokenReservation(database, reservation.tokenHash);
|
|
158230
|
+
}
|
|
158231
|
+
} else {
|
|
158232
|
+
releaseReceiveToken(database, reservation.tokenHash);
|
|
158233
|
+
}
|
|
158234
|
+
throw error53;
|
|
158235
|
+
}
|
|
157626
158236
|
},
|
|
157627
158237
|
async receiveBolt11(amount, mintUrl) {
|
|
158238
|
+
await waitForRecovery();
|
|
157628
158239
|
const targetMint = mintUrl ? normalizeMintUrl(mintUrl) : walletConfig.defaultMintUrl;
|
|
157629
158240
|
if (!targetMint) {
|
|
157630
158241
|
throw new Error("No trusted mint available for Lightning invoice");
|
|
@@ -157640,6 +158251,7 @@ async function createCocoClient(options = {}) {
|
|
|
157640
158251
|
return op.request;
|
|
157641
158252
|
},
|
|
157642
158253
|
async sendCashu(amount, mintUrl) {
|
|
158254
|
+
await waitForRecovery();
|
|
157643
158255
|
const targetMint = mintUrl ? normalizeMintUrl(mintUrl) : walletConfig.defaultMintUrl;
|
|
157644
158256
|
if (!targetMint) {
|
|
157645
158257
|
throw new Error("No trusted mint available for sending");
|
|
@@ -157652,6 +158264,7 @@ async function createCocoClient(options = {}) {
|
|
|
157652
158264
|
return ar(token);
|
|
157653
158265
|
},
|
|
157654
158266
|
async sendBolt11(invoice, mintUrl) {
|
|
158267
|
+
await waitForRecovery();
|
|
157655
158268
|
const targetMint = mintUrl ? normalizeMintUrl(mintUrl) : walletConfig.defaultMintUrl;
|
|
157656
158269
|
if (!targetMint) {
|
|
157657
158270
|
throw new Error("No trusted mint available for Lightning payment");
|
|
@@ -157669,6 +158282,7 @@ async function createCocoClient(options = {}) {
|
|
|
157669
158282
|
return mints.map((m2) => m2.mintUrl);
|
|
157670
158283
|
},
|
|
157671
158284
|
async addMint(url5) {
|
|
158285
|
+
await waitForRecovery();
|
|
157672
158286
|
const mintUrl = normalizeMintUrl(url5);
|
|
157673
158287
|
await coco.mint.addMint(mintUrl, { trusted: true });
|
|
157674
158288
|
return `Mint ${mintUrl} added successfully`;
|
|
@@ -157680,6 +158294,7 @@ async function createCocoClient(options = {}) {
|
|
|
157680
158294
|
return walletConfig.defaultMintUrl || null;
|
|
157681
158295
|
},
|
|
157682
158296
|
async setDefaultMint(url5) {
|
|
158297
|
+
await waitForRecovery();
|
|
157683
158298
|
const mintUrl = normalizeMintUrl(url5);
|
|
157684
158299
|
const trustedMints = await coco.mint.getAllTrustedMints();
|
|
157685
158300
|
if (!trustedMints.some((mint) => mint.mintUrl === mintUrl)) {
|
|
@@ -157694,6 +158309,7 @@ async function createCocoClient(options = {}) {
|
|
|
157694
158309
|
return;
|
|
157695
158310
|
disposed = true;
|
|
157696
158311
|
try {
|
|
158312
|
+
await recoveryPromise;
|
|
157697
158313
|
await coco.dispose();
|
|
157698
158314
|
} finally {
|
|
157699
158315
|
try {
|
|
@@ -157718,6 +158334,7 @@ async function createCocoClient(options = {}) {
|
|
|
157718
158334
|
};
|
|
157719
158335
|
},
|
|
157720
158336
|
async setNpcUsername(username, confirm) {
|
|
158337
|
+
await waitForRecovery();
|
|
157721
158338
|
const result = await npcApi().setUsername(username, confirm === true);
|
|
157722
158339
|
if (result.success) {
|
|
157723
158340
|
return { success: true };
|
|
@@ -157728,7 +158345,77 @@ async function createCocoClient(options = {}) {
|
|
|
157728
158345
|
};
|
|
157729
158346
|
},
|
|
157730
158347
|
async syncNpc() {
|
|
158348
|
+
await waitForRecovery();
|
|
157731
158349
|
await npcApi().sync();
|
|
158350
|
+
},
|
|
158351
|
+
async cleanupStuckOperations(options2 = {}) {
|
|
158352
|
+
await waitForRecovery();
|
|
158353
|
+
const minAgeMs = options2.minAgeMs ?? 7 * 24 * 60 * 60 * 1000;
|
|
158354
|
+
const dryRun = options2.dryRun === true;
|
|
158355
|
+
const nowMs = Date.now();
|
|
158356
|
+
const [pendingMints, inFlightSends, preparedMelts] = await Promise.all([
|
|
158357
|
+
coco.ops.mint.listPending(),
|
|
158358
|
+
coco.ops.send.listInFlight(),
|
|
158359
|
+
coco.ops.melt.listPrepared()
|
|
158360
|
+
]);
|
|
158361
|
+
const filteredMints = options2.mintUrl ? pendingMints.filter((op) => op.mintUrl === options2.mintUrl) : pendingMints;
|
|
158362
|
+
const filteredSends = options2.mintUrl ? inFlightSends.filter((op) => op.mintUrl === options2.mintUrl) : inFlightSends;
|
|
158363
|
+
const filteredMelts = options2.mintUrl ? preparedMelts.filter((op) => op.mintUrl === options2.mintUrl) : preparedMelts;
|
|
158364
|
+
const selection = selectCleanupOperations({
|
|
158365
|
+
mints: filteredMints,
|
|
158366
|
+
sends: filteredSends,
|
|
158367
|
+
melts: filteredMelts,
|
|
158368
|
+
nowMs,
|
|
158369
|
+
minAgeMs
|
|
158370
|
+
});
|
|
158371
|
+
const errors5 = [];
|
|
158372
|
+
if (!dryRun) {
|
|
158373
|
+
const mintService = coco.mintOperationService;
|
|
158374
|
+
for (const op of selection.mintsToFail) {
|
|
158375
|
+
try {
|
|
158376
|
+
await mintService.failPendingOperation({ id: op.id }, {
|
|
158377
|
+
reason: "Expired unpaid mint quote cleaned up by routstrd",
|
|
158378
|
+
retryable: false,
|
|
158379
|
+
observedAt: nowMs
|
|
158380
|
+
});
|
|
158381
|
+
} catch (error53) {
|
|
158382
|
+
errors5.push({
|
|
158383
|
+
operationId: op.id,
|
|
158384
|
+
error: error53 instanceof Error ? error53.message : String(error53)
|
|
158385
|
+
});
|
|
158386
|
+
}
|
|
158387
|
+
}
|
|
158388
|
+
for (const op of selection.sendsToReclaim) {
|
|
158389
|
+
try {
|
|
158390
|
+
await coco.ops.send.reclaim(op.id);
|
|
158391
|
+
} catch (error53) {
|
|
158392
|
+
errors5.push({
|
|
158393
|
+
operationId: op.id,
|
|
158394
|
+
error: error53 instanceof Error ? error53.message : String(error53)
|
|
158395
|
+
});
|
|
158396
|
+
}
|
|
158397
|
+
}
|
|
158398
|
+
for (const op of selection.meltsToCancel) {
|
|
158399
|
+
try {
|
|
158400
|
+
await coco.ops.melt.cancel(op.id, "Cancelled by wallet cleanup");
|
|
158401
|
+
} catch (error53) {
|
|
158402
|
+
errors5.push({
|
|
158403
|
+
operationId: op.id,
|
|
158404
|
+
error: error53 instanceof Error ? error53.message : String(error53)
|
|
158405
|
+
});
|
|
158406
|
+
}
|
|
158407
|
+
}
|
|
158408
|
+
}
|
|
158409
|
+
const actedOn = selection.mintsToFail.length + selection.sendsToReclaim.length + selection.meltsToCancel.length;
|
|
158410
|
+
const skipped = filteredMints.length + filteredSends.length + filteredMelts.length - actedOn;
|
|
158411
|
+
return {
|
|
158412
|
+
dryRun,
|
|
158413
|
+
failedMintQuotes: selection.mintsToFail.length,
|
|
158414
|
+
reclaimedSends: selection.sendsToReclaim.length,
|
|
158415
|
+
cancelledMelts: selection.meltsToCancel.length,
|
|
158416
|
+
skipped,
|
|
158417
|
+
errors: errors5
|
|
158418
|
+
};
|
|
157732
158419
|
}
|
|
157733
158420
|
};
|
|
157734
158421
|
}
|
|
@@ -157737,16 +158424,25 @@ async function createCocoClient(options = {}) {
|
|
|
157737
158424
|
import {
|
|
157738
158425
|
chmodSync,
|
|
157739
158426
|
copyFileSync,
|
|
157740
|
-
existsSync as
|
|
158427
|
+
existsSync as existsSync12,
|
|
157741
158428
|
mkdirSync as mkdirSync9,
|
|
157742
|
-
readFileSync as readFileSync3,
|
|
157743
158429
|
renameSync as renameSync2,
|
|
157744
158430
|
rmSync,
|
|
157745
|
-
statSync
|
|
158431
|
+
statSync as statSync3
|
|
157746
158432
|
} from "fs";
|
|
158433
|
+
import { Database as Database4 } from "bun:sqlite";
|
|
158434
|
+
import { basename, dirname as dirname8, join as join14 } from "path";
|
|
158435
|
+
|
|
158436
|
+
// src/daemon/wallet/diagnostics.ts
|
|
158437
|
+
import { createHash as createHash4 } from "crypto";
|
|
158438
|
+
import { existsSync as existsSync11, readFileSync as readFileSync4, statSync as statSync2 } from "fs";
|
|
157747
158439
|
import { Database as Database3 } from "bun:sqlite";
|
|
157748
|
-
import {
|
|
157749
|
-
|
|
158440
|
+
import { join as join13 } from "path";
|
|
158441
|
+
|
|
158442
|
+
// src/daemon/wallet/wallet-state.ts
|
|
158443
|
+
import { existsSync as existsSync10, readFileSync as readFileSync3, statSync } from "fs";
|
|
158444
|
+
import { join as join12 } from "path";
|
|
158445
|
+
function walletState(configPath, dbPath) {
|
|
157750
158446
|
const hasConfig = existsSync10(configPath);
|
|
157751
158447
|
const hasDb = existsSync10(dbPath);
|
|
157752
158448
|
if (hasConfig)
|
|
@@ -157762,9 +158458,36 @@ function filesEqual(left, right) {
|
|
|
157762
158458
|
return false;
|
|
157763
158459
|
return readFileSync3(left).equals(readFileSync3(right));
|
|
157764
158460
|
}
|
|
157765
|
-
function
|
|
157766
|
-
|
|
158461
|
+
function classifyWalletMigration(targetDir, sourceDir) {
|
|
158462
|
+
const targetConfig = join12(targetDir, "config.json");
|
|
158463
|
+
const targetDb = join12(targetDir, "coco.db");
|
|
158464
|
+
const sourceConfig = join12(sourceDir, "config.json");
|
|
158465
|
+
const sourceDb = join12(sourceDir, "coco.db");
|
|
158466
|
+
const sourceWal = `${sourceDb}-wal`;
|
|
158467
|
+
const sourceShm = `${sourceDb}-shm`;
|
|
158468
|
+
const targetState = walletState(targetConfig, targetDb);
|
|
158469
|
+
const sourceState = walletState(sourceConfig, sourceDb);
|
|
158470
|
+
if (targetState === "initialized" && sourceState === "initialized") {
|
|
158471
|
+
const configsMatch = filesEqual(targetConfig, sourceConfig);
|
|
158472
|
+
const databasesMatch = !existsSync10(targetDb) && !existsSync10(sourceDb) ? true : filesEqual(targetDb, sourceDb);
|
|
158473
|
+
if (configsMatch && databasesMatch)
|
|
158474
|
+
return { kind: "already-current" };
|
|
158475
|
+
return { kind: "conflict" };
|
|
158476
|
+
}
|
|
158477
|
+
if (targetState === "initialized")
|
|
158478
|
+
return { kind: "already-current" };
|
|
158479
|
+
if (!existsSync10(sourceDb) && (existsSync10(sourceWal) || existsSync10(sourceShm))) {
|
|
158480
|
+
return { kind: "orphaned-sidecars" };
|
|
158481
|
+
}
|
|
158482
|
+
if (targetState === "database-only" || sourceState === "database-only") {
|
|
158483
|
+
return { kind: "database-only", targetState, sourceState };
|
|
158484
|
+
}
|
|
158485
|
+
if (sourceState === "absent")
|
|
158486
|
+
return { kind: "fresh" };
|
|
158487
|
+
return { kind: "migrate" };
|
|
157767
158488
|
}
|
|
158489
|
+
|
|
158490
|
+
// src/daemon/wallet/diagnostics.ts
|
|
157768
158491
|
var SUMMARY_QUERIES = {
|
|
157769
158492
|
proofs: "SELECT mintUrl, state, COUNT(*) count, COALESCE(SUM(amount), 0) amount FROM coco_cashu_proofs GROUP BY mintUrl, state ORDER BY mintUrl, state",
|
|
157770
158493
|
counters: "SELECT mintUrl, keysetId, counter FROM coco_cashu_counters ORDER BY mintUrl, keysetId",
|
|
@@ -157786,8 +158509,241 @@ function verifyDatabase(database, label) {
|
|
|
157786
158509
|
}
|
|
157787
158510
|
return summary;
|
|
157788
158511
|
}
|
|
158512
|
+
function mnemonicFingerprint(mnemonic) {
|
|
158513
|
+
const normalized = mnemonic.trim().split(/\s+/).join(" ");
|
|
158514
|
+
const hex9 = createHash4("sha256").update(normalized).digest("hex");
|
|
158515
|
+
return hex9.slice(0, 16).match(/.{1,4}/g)?.join("-") ?? hex9.slice(0, 16);
|
|
158516
|
+
}
|
|
158517
|
+
function summarizeDbReadonly(dbPath) {
|
|
158518
|
+
let database;
|
|
158519
|
+
try {
|
|
158520
|
+
database = new Database3(dbPath, { readonly: true });
|
|
158521
|
+
const raw = verifyDatabase(database, "Wallet database");
|
|
158522
|
+
const proofs = raw.proofs ?? [];
|
|
158523
|
+
const mints = raw.mints ?? [];
|
|
158524
|
+
const amountByState = {};
|
|
158525
|
+
let totalProofs = 0;
|
|
158526
|
+
let totalAmount = 0;
|
|
158527
|
+
for (const row of proofs) {
|
|
158528
|
+
const count = typeof row.count === "number" ? row.count : 0;
|
|
158529
|
+
const amount = typeof row.amount === "number" ? row.amount : 0;
|
|
158530
|
+
totalProofs += count;
|
|
158531
|
+
totalAmount += amount;
|
|
158532
|
+
const state = row.state || "unknown";
|
|
158533
|
+
amountByState[state] = (amountByState[state] ?? 0) + amount;
|
|
158534
|
+
}
|
|
158535
|
+
return {
|
|
158536
|
+
summary: {
|
|
158537
|
+
proofs,
|
|
158538
|
+
mints,
|
|
158539
|
+
totalProofs,
|
|
158540
|
+
totalAmount,
|
|
158541
|
+
amountByState,
|
|
158542
|
+
distinctMints: new Set(proofs.map((row) => row.mintUrl).filter(Boolean)).size
|
|
158543
|
+
}
|
|
158544
|
+
};
|
|
158545
|
+
} catch (error53) {
|
|
158546
|
+
return { error: error53 instanceof Error ? error53.message : String(error53) };
|
|
158547
|
+
} finally {
|
|
158548
|
+
database?.close();
|
|
158549
|
+
}
|
|
158550
|
+
}
|
|
158551
|
+
function summarizeConfig(configPath) {
|
|
158552
|
+
const result = { exists: false, path: configPath };
|
|
158553
|
+
try {
|
|
158554
|
+
if (!existsSync11(configPath))
|
|
158555
|
+
return result;
|
|
158556
|
+
result.exists = true;
|
|
158557
|
+
result.mtimeMs = statSync2(configPath).mtimeMs;
|
|
158558
|
+
const parsed = JSON.parse(readFileSync4(configPath, "utf-8"));
|
|
158559
|
+
result.encrypted = parsed.encrypted === true;
|
|
158560
|
+
if (typeof parsed.createdAt === "string")
|
|
158561
|
+
result.createdAt = parsed.createdAt;
|
|
158562
|
+
if (typeof parsed.version === "number")
|
|
158563
|
+
result.version = parsed.version;
|
|
158564
|
+
if (typeof parsed.defaultMintUrl === "string") {
|
|
158565
|
+
result.defaultMintUrl = parsed.defaultMintUrl;
|
|
158566
|
+
}
|
|
158567
|
+
if (typeof parsed.mnemonic === "string") {
|
|
158568
|
+
result.hasMnemonic = true;
|
|
158569
|
+
if (!result.encrypted)
|
|
158570
|
+
result.fingerprint = mnemonicFingerprint(parsed.mnemonic);
|
|
158571
|
+
}
|
|
158572
|
+
} catch (error53) {
|
|
158573
|
+
result.error = error53 instanceof Error ? error53.message : String(error53);
|
|
158574
|
+
}
|
|
158575
|
+
return result;
|
|
158576
|
+
}
|
|
158577
|
+
function summarizeDb(dbPath) {
|
|
158578
|
+
const result = { exists: false, path: dbPath };
|
|
158579
|
+
try {
|
|
158580
|
+
if (!existsSync11(dbPath))
|
|
158581
|
+
return result;
|
|
158582
|
+
result.exists = true;
|
|
158583
|
+
const stats = statSync2(dbPath);
|
|
158584
|
+
result.mtimeMs = stats.mtimeMs;
|
|
158585
|
+
result.sizeBytes = stats.size;
|
|
158586
|
+
const { summary, error: error53 } = summarizeDbReadonly(dbPath);
|
|
158587
|
+
if (summary)
|
|
158588
|
+
result.summary = summary;
|
|
158589
|
+
if (error53)
|
|
158590
|
+
result.error = error53;
|
|
158591
|
+
} catch (error53) {
|
|
158592
|
+
result.error = error53 instanceof Error ? error53.message : String(error53);
|
|
158593
|
+
}
|
|
158594
|
+
return result;
|
|
158595
|
+
}
|
|
158596
|
+
function summarizeWalletDirectory(dir, role) {
|
|
158597
|
+
return {
|
|
158598
|
+
dir,
|
|
158599
|
+
role,
|
|
158600
|
+
config: summarizeConfig(join13(dir, "config.json")),
|
|
158601
|
+
db: summarizeDb(join13(dir, "coco.db"))
|
|
158602
|
+
};
|
|
158603
|
+
}
|
|
158604
|
+
|
|
158605
|
+
class WalletMigrationConflictError extends Error {
|
|
158606
|
+
target;
|
|
158607
|
+
source;
|
|
158608
|
+
constructor(target, source) {
|
|
158609
|
+
super(renderWalletConflict(target, source));
|
|
158610
|
+
this.name = "WalletMigrationConflictError";
|
|
158611
|
+
this.target = target;
|
|
158612
|
+
this.source = source;
|
|
158613
|
+
}
|
|
158614
|
+
}
|
|
158615
|
+
function timeAgo(ms) {
|
|
158616
|
+
if (ms === undefined || !Number.isFinite(ms))
|
|
158617
|
+
return;
|
|
158618
|
+
const elapsed = Date.now() - ms;
|
|
158619
|
+
if (elapsed < 0)
|
|
158620
|
+
return;
|
|
158621
|
+
const seconds = Math.floor(elapsed / 1000);
|
|
158622
|
+
if (seconds < 5)
|
|
158623
|
+
return "just now";
|
|
158624
|
+
if (seconds < 60)
|
|
158625
|
+
return `${seconds}s ago`;
|
|
158626
|
+
const minutes = Math.floor(seconds / 60);
|
|
158627
|
+
if (minutes < 60)
|
|
158628
|
+
return `${minutes}m ago`;
|
|
158629
|
+
const hours = Math.floor(minutes / 60);
|
|
158630
|
+
if (hours < 48)
|
|
158631
|
+
return `${hours}h ago`;
|
|
158632
|
+
const days = Math.floor(hours / 24);
|
|
158633
|
+
if (days < 60)
|
|
158634
|
+
return `${days}d ago`;
|
|
158635
|
+
const months = Math.floor(days / 30);
|
|
158636
|
+
if (months < 24)
|
|
158637
|
+
return `${months}mo ago`;
|
|
158638
|
+
return `${Math.floor(days / 365)}y ago`;
|
|
158639
|
+
}
|
|
158640
|
+
function modifiedDescription(config2) {
|
|
158641
|
+
if (config2.createdAt)
|
|
158642
|
+
return `created ${config2.createdAt}`;
|
|
158643
|
+
const ago = timeAgo(config2.mtimeMs);
|
|
158644
|
+
if (ago)
|
|
158645
|
+
return `modified ${ago}`;
|
|
158646
|
+
return "modified time unknown";
|
|
158647
|
+
}
|
|
158648
|
+
function roleLabel(role) {
|
|
158649
|
+
return role === "canonical" ? "current routstrd wallet" : "legacy cocod wallet";
|
|
158650
|
+
}
|
|
158651
|
+
function configLine(config2) {
|
|
158652
|
+
if (!config2.exists)
|
|
158653
|
+
return "config.json absent";
|
|
158654
|
+
if (config2.error)
|
|
158655
|
+
return `config.json present (unreadable: ${config2.error})`;
|
|
158656
|
+
const parts = ["config.json present"];
|
|
158657
|
+
if (config2.fingerprint)
|
|
158658
|
+
parts.push(`fingerprint ${config2.fingerprint}`);
|
|
158659
|
+
else if (config2.encrypted)
|
|
158660
|
+
parts.push("encrypted (mnemonic not readable)");
|
|
158661
|
+
else
|
|
158662
|
+
parts.push("no mnemonic found");
|
|
158663
|
+
parts.push(modifiedDescription(config2));
|
|
158664
|
+
if (config2.defaultMintUrl)
|
|
158665
|
+
parts.push(`default mint ${config2.defaultMintUrl}`);
|
|
158666
|
+
return parts.join(" \xB7 ");
|
|
158667
|
+
}
|
|
158668
|
+
function formatNumber(value) {
|
|
158669
|
+
return value.toLocaleString("en-US");
|
|
158670
|
+
}
|
|
158671
|
+
function amountByStateLine(summary) {
|
|
158672
|
+
const parts = Object.entries(summary.amountByState).filter(([, amount]) => amount > 0);
|
|
158673
|
+
if (parts.length === 0)
|
|
158674
|
+
return "0 sats";
|
|
158675
|
+
return parts.map(([state, amount]) => `${state}: ${formatNumber(amount)} sats`).join(", ");
|
|
158676
|
+
}
|
|
158677
|
+
function dbLine(db) {
|
|
158678
|
+
if (!db.exists)
|
|
158679
|
+
return "coco.db absent";
|
|
158680
|
+
if (db.error && !db.summary)
|
|
158681
|
+
return `coco.db present (unreadable: ${db.error})`;
|
|
158682
|
+
const parts = ["coco.db present"];
|
|
158683
|
+
if (db.summary) {
|
|
158684
|
+
const { totalProofs, mints, distinctMints } = db.summary;
|
|
158685
|
+
const mintCount = mints.length > 0 ? mints.length : distinctMints;
|
|
158686
|
+
parts.push(`${formatNumber(totalProofs)} proof${totalProofs === 1 ? "" : "s"}`);
|
|
158687
|
+
parts.push(amountByStateLine(db.summary));
|
|
158688
|
+
parts.push(`${mintCount} mint${mintCount === 1 ? "" : "s"}`);
|
|
158689
|
+
}
|
|
158690
|
+
return parts.join(" \xB7 ");
|
|
158691
|
+
}
|
|
158692
|
+
function block(diag, letter) {
|
|
158693
|
+
return [
|
|
158694
|
+
` ${letter}) ${diag.dir} (${roleLabel(diag.role)})`,
|
|
158695
|
+
` ${configLine(diag.config)}`,
|
|
158696
|
+
` ${dbLine(diag.db)}`
|
|
158697
|
+
].join(`
|
|
158698
|
+
`);
|
|
158699
|
+
}
|
|
158700
|
+
function moveAsideCommands(target, source) {
|
|
158701
|
+
return [
|
|
158702
|
+
" \u2022 If B is your wallet (keep the legacy wallet):",
|
|
158703
|
+
` mv "${target.dir}" "${target.dir}.old"`,
|
|
158704
|
+
"",
|
|
158705
|
+
" \u2022 If A is your wallet (keep the normal routstrd wallet):",
|
|
158706
|
+
` mv "${source.dir}" "${source.dir}.old"`
|
|
158707
|
+
].join(`
|
|
158708
|
+
`);
|
|
158709
|
+
}
|
|
158710
|
+
function renderResolutionSteps(target, source) {
|
|
158711
|
+
return [
|
|
158712
|
+
"To resolve, decide which wallet is yours, move the other one aside, and run",
|
|
158713
|
+
"'routstrd onboard' (or 'routstrd start') again. Stop the daemon first:",
|
|
158714
|
+
"",
|
|
158715
|
+
" routstrd stop",
|
|
158716
|
+
"",
|
|
158717
|
+
"Then:",
|
|
158718
|
+
"",
|
|
158719
|
+
moveAsideCommands(target, source)
|
|
158720
|
+
].join(`
|
|
158721
|
+
`);
|
|
158722
|
+
}
|
|
158723
|
+
function renderWalletConflict(target, source) {
|
|
158724
|
+
return [
|
|
158725
|
+
"Cannot migrate wallet: two different wallets were found, and routstrd will",
|
|
158726
|
+
"not choose a mnemonic or merge databases automatically \u2014 the wrong choice",
|
|
158727
|
+
"could lose access to your funds.",
|
|
158728
|
+
"",
|
|
158729
|
+
block(target, "A"),
|
|
158730
|
+
block(source, "B"),
|
|
158731
|
+
"",
|
|
158732
|
+
"No files were changed.",
|
|
158733
|
+
"",
|
|
158734
|
+
renderResolutionSteps(target, source),
|
|
158735
|
+
"",
|
|
158736
|
+
"For a full comparison and safe cleanup, run: routstrd wallet doctor"
|
|
158737
|
+
].join(`
|
|
158738
|
+
`);
|
|
158739
|
+
}
|
|
158740
|
+
|
|
158741
|
+
// src/daemon/wallet/migration.ts
|
|
158742
|
+
function sqlString(value) {
|
|
158743
|
+
return `'${value.replaceAll("'", "''")}'`;
|
|
158744
|
+
}
|
|
157789
158745
|
function snapshotDatabase(sourcePath, destinationPath) {
|
|
157790
|
-
const source = new
|
|
158746
|
+
const source = new Database4(sourcePath);
|
|
157791
158747
|
let sourceSummary;
|
|
157792
158748
|
try {
|
|
157793
158749
|
sourceSummary = verifyDatabase(source, "Legacy wallet database");
|
|
@@ -157795,7 +158751,7 @@ function snapshotDatabase(sourcePath, destinationPath) {
|
|
|
157795
158751
|
} finally {
|
|
157796
158752
|
source.close();
|
|
157797
158753
|
}
|
|
157798
|
-
const destination = new
|
|
158754
|
+
const destination = new Database4(destinationPath, { readonly: true });
|
|
157799
158755
|
try {
|
|
157800
158756
|
const destinationSummary = verifyDatabase(destination, "Staged wallet database");
|
|
157801
158757
|
if (JSON.stringify(sourceSummary) !== JSON.stringify(destinationSummary)) {
|
|
@@ -157808,48 +158764,42 @@ function snapshotDatabase(sourcePath, destinationPath) {
|
|
|
157808
158764
|
async function migrateLegacyWallet(options = {}) {
|
|
157809
158765
|
const targetDir = options.walletDir || walletDir();
|
|
157810
158766
|
const sourceDir = options.legacyDir || legacyCocodDir();
|
|
157811
|
-
const
|
|
157812
|
-
const
|
|
157813
|
-
const sourceConfig = join12(sourceDir, "config.json");
|
|
157814
|
-
const sourceDb = join12(sourceDir, "coco.db");
|
|
158767
|
+
const sourceConfig = join14(sourceDir, "config.json");
|
|
158768
|
+
const sourceDb = join14(sourceDir, "coco.db");
|
|
157815
158769
|
const sourceWal = `${sourceDb}-wal`;
|
|
157816
158770
|
const sourceShm = `${sourceDb}-shm`;
|
|
157817
|
-
const
|
|
157818
|
-
|
|
157819
|
-
|
|
157820
|
-
|
|
157821
|
-
|
|
157822
|
-
if (configsMatch && databasesMatch)
|
|
158771
|
+
const classification = classifyWalletMigration(targetDir, sourceDir);
|
|
158772
|
+
switch (classification.kind) {
|
|
158773
|
+
case "fresh":
|
|
158774
|
+
return { status: "fresh" };
|
|
158775
|
+
case "already-current":
|
|
157823
158776
|
return { status: "already-current" };
|
|
157824
|
-
|
|
157825
|
-
|
|
157826
|
-
|
|
157827
|
-
|
|
157828
|
-
|
|
157829
|
-
|
|
157830
|
-
|
|
157831
|
-
|
|
157832
|
-
throw new Error(`Cannot migrate wallet: ${targetDir} is ${targetState} and ${sourceDir} is ${sourceState}. ` + "A coco.db without config.json cannot be opened; restore the matching config first.");
|
|
158777
|
+
case "migrate":
|
|
158778
|
+
break;
|
|
158779
|
+
case "conflict":
|
|
158780
|
+
throw new WalletMigrationConflictError(summarizeWalletDirectory(targetDir, "canonical"), summarizeWalletDirectory(sourceDir, "legacy"));
|
|
158781
|
+
case "orphaned-sidecars":
|
|
158782
|
+
throw new Error(`Cannot migrate wallet: ${sourceDir} contains SQLite sidecar files without coco.db. ` + "Restore the matching main database before migration.");
|
|
158783
|
+
case "database-only":
|
|
158784
|
+
throw new Error(`Cannot migrate wallet: ${targetDir} is ${classification.targetState} and ${sourceDir} is ${classification.sourceState}. ` + "A coco.db without config.json cannot be opened; restore the matching config first.");
|
|
157833
158785
|
}
|
|
157834
|
-
if (sourceState === "absent")
|
|
157835
|
-
return { status: "fresh" };
|
|
157836
158786
|
await options.assertLegacyStopped?.();
|
|
157837
158787
|
const releaseLegacyLock = await options.acquireLegacyLock?.();
|
|
157838
|
-
const stagingDir =
|
|
158788
|
+
const stagingDir = join14(dirname8(targetDir), `.${targetDir.split(/[\\/]/).at(-1) || "wallet"}.staging-${process.pid}-${Date.now()}`);
|
|
157839
158789
|
try {
|
|
157840
158790
|
await options.assertLegacyStopped?.();
|
|
157841
158791
|
mkdirSync9(dirname8(targetDir), { recursive: true, mode: 448 });
|
|
157842
158792
|
mkdirSync9(stagingDir, { mode: 448 });
|
|
157843
|
-
const stagedConfig =
|
|
157844
|
-
const stagedDb =
|
|
158793
|
+
const stagedConfig = join14(stagingDir, "config.json");
|
|
158794
|
+
const stagedDb = join14(stagingDir, "coco.db");
|
|
157845
158795
|
copyFileSync(sourceConfig, stagedConfig);
|
|
157846
158796
|
chmodSync(stagedConfig, 384);
|
|
157847
|
-
const hasSourceDb =
|
|
158797
|
+
const hasSourceDb = existsSync12(sourceDb);
|
|
157848
158798
|
if (hasSourceDb) {
|
|
157849
158799
|
snapshotDatabase(sourceDb, stagedDb);
|
|
157850
158800
|
chmodSync(stagedDb, 384);
|
|
157851
158801
|
}
|
|
157852
|
-
if (
|
|
158802
|
+
if (statSync3(stagedConfig).size !== statSync3(sourceConfig).size) {
|
|
157853
158803
|
throw new Error("Cannot migrate wallet: staged config failed size validation.");
|
|
157854
158804
|
}
|
|
157855
158805
|
renameSync2(stagingDir, targetDir);
|
|
@@ -157860,13 +158810,13 @@ async function migrateLegacyWallet(options = {}) {
|
|
|
157860
158810
|
releaseLegacyLock?.();
|
|
157861
158811
|
}
|
|
157862
158812
|
const cleanupWarnings = [];
|
|
157863
|
-
const sourceFiles = [sourceConfig, sourceDb, sourceWal, sourceShm].filter(
|
|
158813
|
+
const sourceFiles = [sourceConfig, sourceDb, sourceWal, sourceShm].filter(existsSync12);
|
|
157864
158814
|
if (sourceFiles.length > 0) {
|
|
157865
|
-
const archiveDir =
|
|
158815
|
+
const archiveDir = join14(sourceDir, `wallet-migrated-${Date.now()}`);
|
|
157866
158816
|
try {
|
|
157867
158817
|
mkdirSync9(archiveDir, { mode: 448 });
|
|
157868
158818
|
for (const path2 of sourceFiles)
|
|
157869
|
-
renameSync2(path2,
|
|
158819
|
+
renameSync2(path2, join14(archiveDir, basename(path2)));
|
|
157870
158820
|
} catch (error53) {
|
|
157871
158821
|
const message = error53 instanceof Error ? error53.message : String(error53);
|
|
157872
158822
|
cleanupWarnings.push(`Could not archive legacy wallet files: ${message}`);
|
|
@@ -157875,6 +158825,19 @@ async function migrateLegacyWallet(options = {}) {
|
|
|
157875
158825
|
return { status: "migrated", from: sourceDir, to: targetDir, cleanupWarnings };
|
|
157876
158826
|
}
|
|
157877
158827
|
|
|
158828
|
+
// src/daemon/fatal-error.ts
|
|
158829
|
+
function exitOnUncaughtException(error53, exit = (code) => process.exit(code)) {
|
|
158830
|
+
logger5.error("UNCAUGHT EXCEPTION:", error53);
|
|
158831
|
+
console.error("UNCAUGHT EXCEPTION:", error53);
|
|
158832
|
+
exit(1);
|
|
158833
|
+
}
|
|
158834
|
+
function installGlobalErrorHandlers() {
|
|
158835
|
+
process.on("uncaughtException", (error53) => exitOnUncaughtException(error53));
|
|
158836
|
+
process.on("unhandledRejection", (reason) => {
|
|
158837
|
+
logger5.error("UNHANDLED REJECTION:", reason);
|
|
158838
|
+
});
|
|
158839
|
+
}
|
|
158840
|
+
|
|
157878
158841
|
// src/daemon/index.ts
|
|
157879
158842
|
function makeSdkLogger2(prefix) {
|
|
157880
158843
|
const tag2 = prefix ? `[${prefix}]` : undefined;
|
|
@@ -157893,13 +158856,15 @@ function startupProgress2(message) {
|
|
|
157893
158856
|
logger5.info(message);
|
|
157894
158857
|
console.log(`${STARTUP_LOG_PREFIX2} ${message}`);
|
|
157895
158858
|
}
|
|
157896
|
-
|
|
157897
|
-
logger5.error("UNCAUGHT EXCEPTION:", error53);
|
|
157898
|
-
});
|
|
157899
|
-
process.on("unhandledRejection", (reason) => {
|
|
157900
|
-
logger5.error("UNHANDLED REJECTION:", reason);
|
|
157901
|
-
});
|
|
158859
|
+
installGlobalErrorHandlers();
|
|
157902
158860
|
async function main() {
|
|
158861
|
+
let shutdownDaemon = () => process.exit(0);
|
|
158862
|
+
const shutdownForSignal = (signal) => {
|
|
158863
|
+
logger5.log(`Received ${signal}; shutting down...`);
|
|
158864
|
+
shutdownDaemon();
|
|
158865
|
+
};
|
|
158866
|
+
process.once("SIGINT", shutdownForSignal);
|
|
158867
|
+
process.once("SIGTERM", shutdownForSignal);
|
|
157903
158868
|
startupProgress2("Loading configuration...");
|
|
157904
158869
|
const args = parseArgs(process.argv);
|
|
157905
158870
|
const config2 = await loadDaemonConfig();
|
|
@@ -157937,26 +158902,18 @@ async function main() {
|
|
|
157937
158902
|
socketPath: legacyCocodSocketPath(),
|
|
157938
158903
|
pidFilePath: legacyCocodPidPath()
|
|
157939
158904
|
});
|
|
157940
|
-
let migrationLockOwner;
|
|
157941
158905
|
const migration = await migrateLegacyWallet({
|
|
157942
158906
|
assertLegacyStopped: () => assertLegacyCocodNotRunning({
|
|
157943
|
-
socketPath: legacyCocodSocketPath()
|
|
157944
|
-
pidFilePath: legacyCocodPidPath(),
|
|
157945
|
-
ignorePid: migrationLockOwner
|
|
158907
|
+
socketPath: legacyCocodSocketPath()
|
|
157946
158908
|
}),
|
|
157947
158909
|
acquireLegacyLock: () => {
|
|
157948
158910
|
mkdirSync10(dirname9(legacyCocodPidPath()), {
|
|
157949
158911
|
recursive: true,
|
|
157950
158912
|
mode: 448
|
|
157951
158913
|
});
|
|
157952
|
-
|
|
158914
|
+
return claimLegacyCocodPidFile({
|
|
157953
158915
|
pidFilePath: legacyCocodPidPath()
|
|
157954
158916
|
});
|
|
157955
|
-
migrationLockOwner = process.pid;
|
|
157956
|
-
return () => {
|
|
157957
|
-
migrationLockOwner = undefined;
|
|
157958
|
-
release();
|
|
157959
|
-
};
|
|
157960
158917
|
}
|
|
157961
158918
|
});
|
|
157962
158919
|
if (migration.status === "migrated") {
|
|
@@ -157984,9 +158941,6 @@ async function main() {
|
|
|
157984
158941
|
});
|
|
157985
158942
|
const refundClient = new RoutstrClient(walletAdapter, storageAdapter, discoveryAdapter, "min", "apikeys", { logger: daemonSdkLogger });
|
|
157986
158943
|
const server = createServer();
|
|
157987
|
-
let shutdownDaemon = () => {
|
|
157988
|
-
server.close(() => process.exit(0));
|
|
157989
|
-
};
|
|
157990
158944
|
server.on("request", createDaemonRequestHandler({
|
|
157991
158945
|
provider,
|
|
157992
158946
|
server,
|
|
@@ -158011,7 +158965,7 @@ async function main() {
|
|
|
158011
158965
|
}));
|
|
158012
158966
|
Bun.write(PID_FILE, String(process.pid));
|
|
158013
158967
|
try {
|
|
158014
|
-
if (
|
|
158968
|
+
if (existsSync13(SOCKET_PATH)) {
|
|
158015
158969
|
unlinkSync2(SOCKET_PATH);
|
|
158016
158970
|
}
|
|
158017
158971
|
} catch {}
|
|
@@ -158051,12 +159005,12 @@ async function main() {
|
|
|
158051
159005
|
(async () => {
|
|
158052
159006
|
logger5.log("Running scheduled refund...");
|
|
158053
159007
|
try {
|
|
158054
|
-
const
|
|
158055
|
-
const pendingDistribution = (
|
|
159008
|
+
const state = store.getState();
|
|
159009
|
+
const pendingDistribution = (state.cachedTokens || []).map((t) => ({
|
|
158056
159010
|
baseUrl: t.baseUrl,
|
|
158057
159011
|
amount: t.balance || 0
|
|
158058
159012
|
}));
|
|
158059
|
-
const apiKeysStored = (
|
|
159013
|
+
const apiKeysStored = (state.apiKeys || []).map((k6) => ({
|
|
158060
159014
|
baseUrl: k6.baseUrl,
|
|
158061
159015
|
amount: k6.balance || 0
|
|
158062
159016
|
}));
|
|
@@ -158103,12 +159057,11 @@ async function main() {
|
|
|
158103
159057
|
disposeWallet().finally(() => process.exit(0));
|
|
158104
159058
|
});
|
|
158105
159059
|
};
|
|
158106
|
-
|
|
158107
|
-
logger5.
|
|
158108
|
-
|
|
158109
|
-
|
|
158110
|
-
|
|
158111
|
-
process.once("SIGTERM", shutdownForSignal);
|
|
159060
|
+
server.on("error", (error53) => {
|
|
159061
|
+
logger5.error("Failed to start Routstr daemon:", error53);
|
|
159062
|
+
console.error("Failed to start Routstr daemon:", error53 instanceof Error ? error53.message : error53);
|
|
159063
|
+
process.exit(1);
|
|
159064
|
+
});
|
|
158112
159065
|
startupProgress2("Starting HTTP server...");
|
|
158113
159066
|
server.listen(port, host, async () => {
|
|
158114
159067
|
logger5.log(`Routstr daemon listening on http://${host}:${port}/v1`);
|