routstrd 0.3.11 → 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/IMPLEMENTATION.md +253 -0
- package/README.md +46 -0
- package/SECURITY.md +23 -0
- package/SKILL.md +13 -3
- package/bun.lock +68 -41
- package/dist/daemon/index.js +48837 -11383
- package/dist/index.js +5027 -421
- package/package.json +8 -3
- package/src/cli.test.ts +55 -0
- package/src/cli.ts +601 -217
- package/src/daemon/args.ts +8 -1
- package/src/daemon/fatal-error.test.ts +104 -0
- package/src/daemon/fatal-error.ts +48 -0
- package/src/daemon/models.ts +119 -39
- 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.npc.test.ts +252 -0
- package/src/daemon/wallet/coco-client.test.ts +669 -0
- package/src/daemon/wallet/coco-client.ts +1628 -0
- package/src/daemon/wallet/cocod-client.ts +135 -3
- package/src/daemon/wallet/diagnostics.test.ts +378 -0
- package/src/daemon/wallet/diagnostics.ts +471 -0
- package/src/daemon/wallet/fixtures/cocod-0.0.24-wallet.db.gz +0 -0
- package/src/daemon/wallet/migration.test.ts +171 -0
- package/src/daemon/wallet/migration.ts +154 -0
- package/src/daemon/wallet/paths.ts +33 -0
- 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/integrations/claudecode.ts +2 -1
- package/src/integrations/hermes.ts +46 -6
- package/src/integrations/openclaw.ts +3 -2
- package/src/integrations/opencode.ts +3 -2
- package/src/integrations/pi.ts +3 -2
- package/src/integrations/registry.ts +7 -5
- package/src/start-daemon.ts +189 -23
- package/src/utils/clients.ts +21 -0
- package/src/utils/config.ts +11 -0
- package/src/utils/daemon-client.ts +104 -25
- package/src/utils/logger.ts +42 -28
- package/tests/integrations/hermes.test.ts +42 -5
- package/tests/utils/daemon-client.test.ts +61 -0
- package/tests/wallet/short-keyset-token.test.ts +106 -0
package/src/cli.ts
CHANGED
|
@@ -15,8 +15,9 @@ import {
|
|
|
15
15
|
deleteClientAction,
|
|
16
16
|
addClientAction,
|
|
17
17
|
} from "./utils/clients";
|
|
18
|
-
import { existsSync, mkdirSync } from "fs";
|
|
18
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "fs";
|
|
19
19
|
import { execSync } from "child_process";
|
|
20
|
+
import { dirname, join } from "path";
|
|
20
21
|
import {
|
|
21
22
|
CONFIG_DIR,
|
|
22
23
|
DB_PATH,
|
|
@@ -25,16 +26,33 @@ import {
|
|
|
25
26
|
LOGS_DIR,
|
|
26
27
|
type RoutstrdConfig,
|
|
27
28
|
} from "./utils/config";
|
|
28
|
-
import { logger } from "./utils/logger";
|
|
29
|
+
import { COCO_LOGS_DIR, logger } from "./utils/logger";
|
|
29
30
|
import { setupIntegration, runIntegrationsForClients } from "./integrations";
|
|
31
|
+
import {
|
|
32
|
+
assertLegacyCocodNotRunning,
|
|
33
|
+
claimLegacyCocodPidFile,
|
|
34
|
+
stopLegacyCocod,
|
|
35
|
+
} from "./daemon/wallet/coco-client";
|
|
36
|
+
import { migrateLegacyWallet } from "./daemon/wallet/migration";
|
|
37
|
+
import {
|
|
38
|
+
diagnoseWallets,
|
|
39
|
+
renderWalletDoctor,
|
|
40
|
+
summarizeWalletDirectory,
|
|
41
|
+
WalletMigrationConflictError,
|
|
42
|
+
} from "./daemon/wallet/diagnostics";
|
|
43
|
+
import {
|
|
44
|
+
legacyCocodDir,
|
|
45
|
+
legacyCocodPidPath,
|
|
46
|
+
legacyCocodSocketPath,
|
|
47
|
+
walletDir as defaultWalletDir,
|
|
48
|
+
walletPidPath,
|
|
49
|
+
} from "./daemon/wallet/paths";
|
|
30
50
|
import { getClientsList } from "./utils/clients";
|
|
31
51
|
import * as QRCode from "qrcode";
|
|
32
52
|
import { normalizeNostrPubkey, npubFromPubkey, npubFromSecretKey } from "./utils/nip98";
|
|
33
53
|
import { generateSecretKey, nip19 } from "nostr-tools";
|
|
34
|
-
import {
|
|
35
|
-
|
|
36
|
-
resolveCocodExecutable,
|
|
37
|
-
} from "./daemon/wallet/cocod-client";
|
|
54
|
+
import { generateMnemonic } from "@scure/bip39";
|
|
55
|
+
import { wordlist } from "@scure/bip39/wordlists/english.js";
|
|
38
56
|
import packageJson from "../package.json" with { type: "json" };
|
|
39
57
|
import {
|
|
40
58
|
compareVersions,
|
|
@@ -86,32 +104,42 @@ async function printLightningInvoice(invoice: string): Promise<void> {
|
|
|
86
104
|
console.log(`${qr}\nInvoice:\n${invoice}`);
|
|
87
105
|
}
|
|
88
106
|
|
|
89
|
-
|
|
90
|
-
|
|
107
|
+
export function initializeWallet(walletDir = defaultWalletDir()): void {
|
|
108
|
+
const walletConfig = join(walletDir, "config.json");
|
|
91
109
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
stderr: "inherit",
|
|
97
|
-
},
|
|
98
|
-
);
|
|
110
|
+
// The wallet directory and config contain the plaintext seed phrase. Correct
|
|
111
|
+
// permissions on existing installations as well as newly created ones.
|
|
112
|
+
mkdirSync(walletDir, { recursive: true, mode: 0o700 });
|
|
113
|
+
chmodSync(walletDir, 0o700);
|
|
99
114
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
console.
|
|
103
|
-
|
|
104
|
-
);
|
|
105
|
-
throw new Error("cocod installation failed");
|
|
115
|
+
if (existsSync(walletConfig)) {
|
|
116
|
+
chmodSync(walletConfig, 0o600);
|
|
117
|
+
console.log("Wallet already initialized.");
|
|
118
|
+
return;
|
|
106
119
|
}
|
|
107
120
|
|
|
108
|
-
|
|
121
|
+
const mnemonic = generateMnemonic(wordlist);
|
|
122
|
+
const config = {
|
|
123
|
+
version: 1,
|
|
124
|
+
mnemonic,
|
|
125
|
+
encrypted: false,
|
|
126
|
+
createdAt: new Date().toISOString(),
|
|
127
|
+
};
|
|
128
|
+
writeFileSync(walletConfig, JSON.stringify(config, null, 2), {
|
|
129
|
+
mode: 0o600,
|
|
130
|
+
flag: "wx",
|
|
131
|
+
});
|
|
132
|
+
console.log("Initialized. Mnemonic:", mnemonic);
|
|
133
|
+
console.log("IMPORTANT: Write down this mnemonic and keep it safe!");
|
|
109
134
|
}
|
|
110
135
|
|
|
111
136
|
/**
|
|
112
|
-
* Restart the routstrd
|
|
113
|
-
*
|
|
114
|
-
*
|
|
137
|
+
* Restart the routstrd daemon after an update so the new binary takes
|
|
138
|
+
* effect immediately. Failures are collected and reported but never
|
|
139
|
+
* roll back the update itself.
|
|
140
|
+
*
|
|
141
|
+
* Note: cocod is no longer a separate process — the wallet now runs
|
|
142
|
+
* in-process via coco-core, so there is nothing else to restart.
|
|
115
143
|
*/
|
|
116
144
|
async function restartDaemonsAfterUpdate(): Promise<void> {
|
|
117
145
|
const config = await loadConfig();
|
|
@@ -129,23 +157,28 @@ async function restartDaemonsAfterUpdate(): Promise<void> {
|
|
|
129
157
|
} else {
|
|
130
158
|
console.log("\nRestarting routstrd daemon...");
|
|
131
159
|
|
|
132
|
-
// Graceful stop — the /stop endpoint closes the HTTP server
|
|
133
|
-
// (draining active connections) then exits.
|
|
134
160
|
await callDaemon("/stop", { method: "POST" });
|
|
135
161
|
|
|
136
|
-
for
|
|
162
|
+
// Wait for HTTP health check to fail AND wallet lock to be released.
|
|
163
|
+
const pidFilePath = walletPidPath();
|
|
164
|
+
for (let i = 0; i < 100; i++) {
|
|
137
165
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
138
|
-
|
|
166
|
+
const healthDown = !(await isDaemonRunning());
|
|
167
|
+
const pidFileReleased = !existsSync(pidFilePath);
|
|
168
|
+
if (healthDown && pidFileReleased) break;
|
|
139
169
|
}
|
|
140
170
|
|
|
141
171
|
if (await isDaemonRunning()) {
|
|
142
|
-
throw new Error("routstrd did not stop within
|
|
172
|
+
throw new Error("routstrd did not stop within 10 seconds");
|
|
143
173
|
}
|
|
144
174
|
console.log("routstrd daemon stopped.");
|
|
145
175
|
|
|
176
|
+
await stopLegacyCocod();
|
|
177
|
+
|
|
146
178
|
console.log("Starting routstrd daemon...");
|
|
147
179
|
await startDaemon({
|
|
148
180
|
port: String(config.port || 8008),
|
|
181
|
+
host: config.host || undefined,
|
|
149
182
|
provider: config.provider || undefined,
|
|
150
183
|
});
|
|
151
184
|
console.log("routstrd daemon restarted.");
|
|
@@ -156,80 +189,19 @@ async function restartDaemonsAfterUpdate(): Promise<void> {
|
|
|
156
189
|
}
|
|
157
190
|
}
|
|
158
191
|
|
|
159
|
-
// --- cocod daemon ---
|
|
160
|
-
try {
|
|
161
|
-
const cocodExecutable = resolveCocodExecutable(config.cocodPath);
|
|
162
|
-
|
|
163
|
-
// Check whether cocod is currently running.
|
|
164
|
-
const pingProc = Bun.spawn([cocodExecutable, "ping"], {
|
|
165
|
-
stdout: "ignore",
|
|
166
|
-
stderr: "ignore",
|
|
167
|
-
});
|
|
168
|
-
const cocodWasRunning = (await pingProc.exited) === 0;
|
|
169
|
-
|
|
170
|
-
if (!cocodWasRunning) {
|
|
171
|
-
console.log("\ncocod daemon was not running — skipping restart.");
|
|
172
|
-
} else {
|
|
173
|
-
console.log("\nRestarting cocod daemon...");
|
|
174
|
-
|
|
175
|
-
const stopProc = Bun.spawn([cocodExecutable, "stop"], {
|
|
176
|
-
stdout: "inherit",
|
|
177
|
-
stderr: "inherit",
|
|
178
|
-
});
|
|
179
|
-
const stopCode = await stopProc.exited;
|
|
180
|
-
if (stopCode !== 0) {
|
|
181
|
-
throw new Error(`cocod stop exited with code ${stopCode}`);
|
|
182
|
-
}
|
|
183
|
-
console.log("cocod daemon stopped.");
|
|
184
|
-
|
|
185
|
-
// Start cocod in the background (detached, like the wallet client does).
|
|
186
|
-
const env = { ...process.env };
|
|
187
|
-
const startProc = Bun.spawn([cocodExecutable, "daemon"], {
|
|
188
|
-
stdout: "ignore",
|
|
189
|
-
stderr: "ignore",
|
|
190
|
-
stdin: "ignore",
|
|
191
|
-
detached: true,
|
|
192
|
-
env,
|
|
193
|
-
});
|
|
194
|
-
startProc.unref?.();
|
|
195
|
-
|
|
196
|
-
// Poll until cocod responds to ping (max ~10 s).
|
|
197
|
-
let started = false;
|
|
198
|
-
for (let i = 0; i < 100; i++) {
|
|
199
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
200
|
-
const poll = Bun.spawn([cocodExecutable, "ping"], {
|
|
201
|
-
stdout: "ignore",
|
|
202
|
-
stderr: "ignore",
|
|
203
|
-
});
|
|
204
|
-
if ((await poll.exited) === 0) {
|
|
205
|
-
started = true;
|
|
206
|
-
break;
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
if (!started) {
|
|
211
|
-
throw new Error("cocod did not come back up within 10 seconds");
|
|
212
|
-
}
|
|
213
|
-
console.log("cocod daemon restarted.");
|
|
214
|
-
}
|
|
215
|
-
} catch (error) {
|
|
216
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
217
|
-
failures.push(`cocod daemon: ${msg}`);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
192
|
// --- report ---
|
|
221
193
|
if (failures.length > 0) {
|
|
222
|
-
console.error("\n⚠
|
|
194
|
+
console.error("\n⚠ Failed to restart daemon:");
|
|
223
195
|
for (const f of failures) {
|
|
224
196
|
console.error(` - ${f}`);
|
|
225
197
|
}
|
|
226
198
|
console.error(
|
|
227
|
-
"The update was applied but may not take effect until
|
|
199
|
+
"The update was applied but may not take effect until the daemon is manually restarted.",
|
|
228
200
|
);
|
|
229
201
|
process.exit(1);
|
|
230
202
|
}
|
|
231
203
|
|
|
232
|
-
console.log("\n✓
|
|
204
|
+
console.log("\n✓ Daemon restarted successfully.");
|
|
233
205
|
}
|
|
234
206
|
|
|
235
207
|
async function requireLocalDaemon(): Promise<void> {
|
|
@@ -274,76 +246,31 @@ async function initDaemon(): Promise<void> {
|
|
|
274
246
|
console.log(`You can view it in the config file at: ${CONFIG_FILE}\n`);
|
|
275
247
|
}
|
|
276
248
|
|
|
277
|
-
if (!(await isCocodInstalled(config.cocodPath))) {
|
|
278
|
-
if (config.cocodPath) {
|
|
279
|
-
logger.error(
|
|
280
|
-
`Configured cocod executable was not found: ${config.cocodPath}`,
|
|
281
|
-
);
|
|
282
|
-
return;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
await installCocodOrExit();
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
const cocodExecutable = resolveCocodExecutable(config.cocodPath);
|
|
289
|
-
|
|
290
249
|
console.log(`Database will be stored at: ${DB_PATH}`);
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
|
|
250
|
+
await stopLegacyCocod();
|
|
251
|
+
|
|
252
|
+
const migration = await migrateLegacyWallet({
|
|
253
|
+
assertLegacyStopped: () =>
|
|
254
|
+
assertLegacyCocodNotRunning({
|
|
255
|
+
socketPath: legacyCocodSocketPath(),
|
|
256
|
+
}),
|
|
257
|
+
acquireLegacyLock: () => {
|
|
258
|
+
mkdirSync(dirname(legacyCocodPidPath()), {
|
|
259
|
+
recursive: true,
|
|
260
|
+
mode: 0o700,
|
|
261
|
+
});
|
|
262
|
+
return claimLegacyCocodPidFile({
|
|
263
|
+
pidFilePath: legacyCocodPidPath(),
|
|
264
|
+
});
|
|
265
|
+
},
|
|
296
266
|
});
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
const stdoutDone = initProc.stdout
|
|
302
|
-
? initProc.stdout.pipeTo(
|
|
303
|
-
new WritableStream<Uint8Array>({
|
|
304
|
-
write(chunk) {
|
|
305
|
-
const text = new TextDecoder().decode(chunk);
|
|
306
|
-
initStdout += text;
|
|
307
|
-
process.stdout.write(text);
|
|
308
|
-
},
|
|
309
|
-
}),
|
|
310
|
-
)
|
|
311
|
-
: Promise.resolve();
|
|
312
|
-
|
|
313
|
-
const stderrDone = initProc.stderr
|
|
314
|
-
? initProc.stderr.pipeTo(
|
|
315
|
-
new WritableStream<Uint8Array>({
|
|
316
|
-
write(chunk) {
|
|
317
|
-
const text = new TextDecoder().decode(chunk);
|
|
318
|
-
initStderr += text;
|
|
319
|
-
process.stderr.write(text);
|
|
320
|
-
},
|
|
321
|
-
}),
|
|
322
|
-
)
|
|
323
|
-
: Promise.resolve();
|
|
324
|
-
|
|
325
|
-
const [initCode] = await Promise.all([
|
|
326
|
-
initProc.exited,
|
|
327
|
-
stdoutDone,
|
|
328
|
-
stderrDone,
|
|
329
|
-
]);
|
|
330
|
-
const combinedOutput = `${initStdout}\n${initStderr}`.toLowerCase();
|
|
331
|
-
const alreadyInitialized = combinedOutput.includes("already initialized");
|
|
332
|
-
|
|
333
|
-
if (initCode !== 0 && !alreadyInitialized) {
|
|
334
|
-
console.error(
|
|
335
|
-
"Failed to initialize cocod. Please run 'cocod init' manually.",
|
|
336
|
-
);
|
|
337
|
-
return;
|
|
267
|
+
if (migration.status === "migrated") {
|
|
268
|
+
console.log(`Migrated wallet from ${migration.from} to ${migration.to}.`);
|
|
269
|
+
for (const warning of migration.cleanupWarnings) console.warn(warning);
|
|
338
270
|
}
|
|
271
|
+
initializeWallet();
|
|
339
272
|
|
|
340
|
-
|
|
341
|
-
console.log("cocod is already initialized.");
|
|
342
|
-
} else {
|
|
343
|
-
console.log("cocod initialized successfully.");
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
await startDaemon({ port: String(config.port || 8008) });
|
|
273
|
+
await startDaemon({ port: String(config.port || 8008), host: config.host || undefined });
|
|
347
274
|
|
|
348
275
|
await setupIntegration(config);
|
|
349
276
|
|
|
@@ -366,7 +293,7 @@ program
|
|
|
366
293
|
|
|
367
294
|
program
|
|
368
295
|
.command("update")
|
|
369
|
-
.description("Update routstrd
|
|
296
|
+
.description("Update routstrd to the latest version")
|
|
370
297
|
.action(async () => {
|
|
371
298
|
const packages = [
|
|
372
299
|
{ name: "routstrd", label: "routstrd" },
|
|
@@ -421,7 +348,7 @@ program
|
|
|
421
348
|
.description("Refund pending tokens and API keys to a specified mint")
|
|
422
349
|
.option(
|
|
423
350
|
"-m, --mint-url <mintUrl>",
|
|
424
|
-
"Mint URL to refund to (defaults to
|
|
351
|
+
"Mint URL to refund to (defaults to active wallet mint)",
|
|
425
352
|
)
|
|
426
353
|
.option("-y, --yes", "Skip confirmation prompt", false)
|
|
427
354
|
.option("--xcashu", "Refund xcashu tokens only (uses refundXcashuTokens)", false)
|
|
@@ -435,18 +362,19 @@ program
|
|
|
435
362
|
console.log(balanceResult.error);
|
|
436
363
|
process.exit(1);
|
|
437
364
|
}
|
|
438
|
-
const
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
365
|
+
const output = balanceResult.output as
|
|
366
|
+
| {
|
|
367
|
+
balances?: Record<string, number>;
|
|
368
|
+
activeMint?: string;
|
|
369
|
+
}
|
|
370
|
+
| undefined;
|
|
371
|
+
mintUrl =
|
|
372
|
+
output?.activeMint ??
|
|
373
|
+
(output?.balances ? Object.keys(output.balances)[0] : undefined);
|
|
374
|
+
if (!mintUrl) {
|
|
446
375
|
console.log("No mint URLs found in wallet balance");
|
|
447
376
|
process.exit(1);
|
|
448
377
|
}
|
|
449
|
-
mintUrl = Object.keys(balances)[0];
|
|
450
378
|
console.log(`Using mint URL: ${mintUrl}`);
|
|
451
379
|
}
|
|
452
380
|
|
|
@@ -523,12 +451,36 @@ program
|
|
|
523
451
|
}
|
|
524
452
|
});
|
|
525
453
|
|
|
526
|
-
// Remote - configure a remote daemon URL
|
|
454
|
+
// Remote - show or configure a remote daemon URL
|
|
527
455
|
program
|
|
528
|
-
.command("remote
|
|
529
|
-
.description(
|
|
456
|
+
.command("remote [url]")
|
|
457
|
+
.description(
|
|
458
|
+
"Show the configured remote daemon, or configure one. With no URL, prints the current remote node; pass a URL to set one up.",
|
|
459
|
+
)
|
|
530
460
|
.option("--auth-url <authUrl>", "URL of the auth proxy for management commands (npubs, clients, usage)")
|
|
531
|
-
.action(async (url: string, options: { authUrl?: string }) => {
|
|
461
|
+
.action(async (url: string | undefined, options: { authUrl?: string }) => {
|
|
462
|
+
const config = await loadConfig();
|
|
463
|
+
|
|
464
|
+
// No URL provided -> show the current remote node, or tell the user to set one up.
|
|
465
|
+
if (!url) {
|
|
466
|
+
if (config.daemonUrl) {
|
|
467
|
+
console.log(`Remote daemon URL: ${config.daemonUrl}`);
|
|
468
|
+
if (config.authUrl) {
|
|
469
|
+
console.log(`Auth proxy URL: ${config.authUrl}`);
|
|
470
|
+
}
|
|
471
|
+
const npub = getUserNpub(config);
|
|
472
|
+
if (npub) {
|
|
473
|
+
console.log(`Nostr identity: ${npub}`);
|
|
474
|
+
}
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
console.error("No remote node is set up.");
|
|
478
|
+
console.error(
|
|
479
|
+
"Pass a URL to set one up, e.g. 'routstrd remote https://<host>:<port>'.",
|
|
480
|
+
);
|
|
481
|
+
process.exit(1);
|
|
482
|
+
}
|
|
483
|
+
|
|
532
484
|
try {
|
|
533
485
|
new URL(url);
|
|
534
486
|
} catch {
|
|
@@ -549,7 +501,6 @@ program
|
|
|
549
501
|
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
550
502
|
}
|
|
551
503
|
|
|
552
|
-
const config = await loadConfig();
|
|
553
504
|
const updates: Partial<RoutstrdConfig> = { daemonUrl: url };
|
|
554
505
|
if (options.authUrl) {
|
|
555
506
|
updates.authUrl = options.authUrl;
|
|
@@ -586,15 +537,58 @@ program
|
|
|
586
537
|
}
|
|
587
538
|
});
|
|
588
539
|
|
|
540
|
+
// Local - switch back to local daemon mode (removes daemonUrl/authUrl)
|
|
541
|
+
program
|
|
542
|
+
.command("local")
|
|
543
|
+
.description("Switch back to local daemon mode (removes remote daemon URL)")
|
|
544
|
+
.action(async () => {
|
|
545
|
+
const config = await loadConfig();
|
|
546
|
+
|
|
547
|
+
if (!config.daemonUrl) {
|
|
548
|
+
console.log("Already in local mode — no remote daemon URL is configured.");
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const previousDaemonUrl = config.daemonUrl;
|
|
553
|
+
const previousAuthUrl = config.authUrl;
|
|
554
|
+
|
|
555
|
+
const { daemonUrl: _daemonUrl, authUrl: _authUrl, ...rest } = config;
|
|
556
|
+
const updatedConfig: RoutstrdConfig = rest;
|
|
557
|
+
|
|
558
|
+
await Bun.write(CONFIG_FILE, JSON.stringify(updatedConfig, null, 2));
|
|
559
|
+
|
|
560
|
+
console.log("Switched back to local daemon mode.");
|
|
561
|
+
if (previousDaemonUrl) {
|
|
562
|
+
console.log(` Removed remote daemon URL: ${previousDaemonUrl}`);
|
|
563
|
+
}
|
|
564
|
+
if (previousAuthUrl) {
|
|
565
|
+
console.log(` Removed auth proxy URL: ${previousAuthUrl}`);
|
|
566
|
+
}
|
|
567
|
+
console.log(
|
|
568
|
+
`\nUse 'routstrd onboard' for first time using local mode or 'routstrd start' to start the local daemon if you already are using it.`,
|
|
569
|
+
);
|
|
570
|
+
console.log(`You can view the config file at: ${CONFIG_FILE}`);
|
|
571
|
+
});
|
|
572
|
+
|
|
589
573
|
// Onboard - initialize the daemon
|
|
590
574
|
program
|
|
591
575
|
.command("onboard")
|
|
592
576
|
.description(
|
|
593
|
-
"Initialize routstrd (creates config directory and initializes
|
|
577
|
+
"Initialize routstrd (creates config directory and initializes wallet)",
|
|
594
578
|
)
|
|
595
579
|
.action(async () => {
|
|
596
580
|
await requireLocalDaemon();
|
|
597
|
-
|
|
581
|
+
try {
|
|
582
|
+
await initDaemon();
|
|
583
|
+
} catch (error) {
|
|
584
|
+
// An expected, user-actionable refusal — print the structured message
|
|
585
|
+
// without Bun's unhandled-rejection source snippet and stack trace.
|
|
586
|
+
if (error instanceof WalletMigrationConflictError) {
|
|
587
|
+
console.error(error.message);
|
|
588
|
+
process.exit(1);
|
|
589
|
+
}
|
|
590
|
+
throw error;
|
|
591
|
+
}
|
|
598
592
|
});
|
|
599
593
|
|
|
600
594
|
// Start - start the background daemon
|
|
@@ -602,21 +596,24 @@ program
|
|
|
602
596
|
.command("start")
|
|
603
597
|
.description("Start the background daemon")
|
|
604
598
|
.option("--port <port>", "Port to listen on")
|
|
599
|
+
.option("--host <host>", "Bind address (default: 127.0.0.1)")
|
|
605
600
|
.option("-p, --provider <provider>", "Default provider to use")
|
|
606
|
-
.action(async (options: { port?: string; provider?: string }) => {
|
|
601
|
+
.action(async (options: { port?: string; host?: string; provider?: string }) => {
|
|
607
602
|
await requireLocalDaemon();
|
|
608
603
|
const config = await loadConfig();
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
:
|
|
613
|
-
|
|
604
|
+
await stopLegacyCocod();
|
|
605
|
+
try {
|
|
606
|
+
await startDaemon({
|
|
607
|
+
port: options.port || String(config.port || 8008),
|
|
608
|
+
host: options.host || config.host || undefined,
|
|
609
|
+
provider: options.provider,
|
|
610
|
+
});
|
|
611
|
+
} catch (error) {
|
|
612
|
+
// startDaemon embeds the failed daemon's own output in the message
|
|
613
|
+
// (including the wallet-conflict report), so print it without a stack.
|
|
614
|
+
console.error(error instanceof Error ? error.message : error);
|
|
614
615
|
process.exit(1);
|
|
615
616
|
}
|
|
616
|
-
await startDaemon({
|
|
617
|
-
port: options.port || String(config.port || 8008),
|
|
618
|
-
provider: options.provider,
|
|
619
|
-
});
|
|
620
617
|
});
|
|
621
618
|
|
|
622
619
|
// Status - check daemon status
|
|
@@ -654,9 +651,93 @@ program
|
|
|
654
651
|
program
|
|
655
652
|
.command("balance")
|
|
656
653
|
.description("Get wallet and API key balances")
|
|
657
|
-
.
|
|
654
|
+
.option("--api-keys", "List all stored API keys (baseUrl + key + balance)", false)
|
|
655
|
+
.option(
|
|
656
|
+
"--delete-api-keys <baseUrl>",
|
|
657
|
+
"Delete the API key stored for the given provider base URL (refunds balance first)",
|
|
658
|
+
)
|
|
659
|
+
.option(
|
|
660
|
+
"--mint-url <url>",
|
|
661
|
+
"Mint URL to refund the deleted API key balance to (defaults to active wallet mint)",
|
|
662
|
+
)
|
|
663
|
+
.action(async (options: { apiKeys: boolean; deleteApiKeys?: string; mintUrl?: string }) => {
|
|
658
664
|
await ensureDaemonRunning();
|
|
659
665
|
|
|
666
|
+
if (options.deleteApiKeys) {
|
|
667
|
+
const baseUrl = options.deleteApiKeys;
|
|
668
|
+
const queryParts = [`baseUrl=${encodeURIComponent(baseUrl)}`];
|
|
669
|
+
if (options.mintUrl) {
|
|
670
|
+
queryParts.push(`mintUrl=${encodeURIComponent(options.mintUrl)}`);
|
|
671
|
+
}
|
|
672
|
+
const result = await callDaemon(
|
|
673
|
+
`/keys/api/delete?${queryParts.join("&")}`,
|
|
674
|
+
{ method: "DELETE" },
|
|
675
|
+
);
|
|
676
|
+
if (result.error) {
|
|
677
|
+
console.log(result.error);
|
|
678
|
+
process.exit(1);
|
|
679
|
+
}
|
|
680
|
+
const out = result.output as
|
|
681
|
+
| {
|
|
682
|
+
baseUrl?: string;
|
|
683
|
+
removed?: boolean;
|
|
684
|
+
refunded?: boolean;
|
|
685
|
+
refundedAmount?: number;
|
|
686
|
+
refundMessage?: string;
|
|
687
|
+
message?: string;
|
|
688
|
+
}
|
|
689
|
+
| undefined;
|
|
690
|
+
if (out) {
|
|
691
|
+
console.log(out.message ?? `Removed API key for ${baseUrl}`);
|
|
692
|
+
if (out.refunded && out.refundedAmount !== undefined) {
|
|
693
|
+
console.log(` Refunded: ${out.refundedAmount} sats`);
|
|
694
|
+
} else if (out.refundMessage) {
|
|
695
|
+
console.log(` Refund: ${out.refundMessage}`);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
if (options.apiKeys) {
|
|
702
|
+
const result = await callDaemon("/keys/api");
|
|
703
|
+
if (result.error) {
|
|
704
|
+
console.log(result.error);
|
|
705
|
+
process.exit(1);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
const data = result.output as
|
|
709
|
+
| {
|
|
710
|
+
apiKeys: Array<{
|
|
711
|
+
baseUrl: string;
|
|
712
|
+
key: string;
|
|
713
|
+
balance: number;
|
|
714
|
+
lastUsed: number | null;
|
|
715
|
+
}>;
|
|
716
|
+
count: number;
|
|
717
|
+
total: number;
|
|
718
|
+
unit: string;
|
|
719
|
+
}
|
|
720
|
+
| undefined;
|
|
721
|
+
|
|
722
|
+
console.log("=== API Keys ===\n");
|
|
723
|
+
if (!data || data.apiKeys.length === 0) {
|
|
724
|
+
console.log(" No API keys stored.");
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
for (const k of data.apiKeys) {
|
|
728
|
+
const lastUsed = k.lastUsed
|
|
729
|
+
? new Date(k.lastUsed).toISOString()
|
|
730
|
+
: "never";
|
|
731
|
+
console.log(` ${k.baseUrl}`);
|
|
732
|
+
console.log(` key: ${k.key}`);
|
|
733
|
+
console.log(` balance: ${k.balance} ${data.unit}`);
|
|
734
|
+
console.log(` lastUsed: ${lastUsed}`);
|
|
735
|
+
console.log("");
|
|
736
|
+
}
|
|
737
|
+
console.log(` Total: ${data.apiKeys.length} key(s), ${data.total} ${data.unit}`);
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
|
|
660
741
|
const [walletResult, keysResult] = await Promise.all([
|
|
661
742
|
callDaemon("/balance"),
|
|
662
743
|
callDaemon("/keys/balance"),
|
|
@@ -780,6 +861,7 @@ program
|
|
|
780
861
|
context_length?: number;
|
|
781
862
|
providers: Array<{
|
|
782
863
|
baseUrl: string;
|
|
864
|
+
disabled?: boolean;
|
|
783
865
|
pricing: {
|
|
784
866
|
prompt: number;
|
|
785
867
|
completion: number;
|
|
@@ -806,7 +888,9 @@ program
|
|
|
806
888
|
}
|
|
807
889
|
console.log(`\n Providers (${modelData.providers.length}):`);
|
|
808
890
|
for (const provider of modelData.providers) {
|
|
809
|
-
console.log(
|
|
891
|
+
console.log(
|
|
892
|
+
`\n ${provider.baseUrl}${provider.disabled ? " [Disabled]" : ""}`,
|
|
893
|
+
);
|
|
810
894
|
console.log(
|
|
811
895
|
` Prompt: ${(provider.pricing.prompt * 1000000).toFixed(2)} sats/M tokens`,
|
|
812
896
|
);
|
|
@@ -1270,6 +1354,103 @@ npubsCmd
|
|
|
1270
1354
|
);
|
|
1271
1355
|
});
|
|
1272
1356
|
|
|
1357
|
+
program
|
|
1358
|
+
.command("history")
|
|
1359
|
+
.description("Show wallet transaction history")
|
|
1360
|
+
.option("-n, --limit <number>", "Number of entries to show", "50")
|
|
1361
|
+
.option("--offset <number>", "Number of entries to skip", "0")
|
|
1362
|
+
.option("-v, --verbose", "Show full details including encoded Cashu tokens")
|
|
1363
|
+
.option("--json", "Output raw JSON with token objects (no encoding)")
|
|
1364
|
+
.action(async (options: { limit: string; offset: string; verbose: boolean; json: boolean }) => {
|
|
1365
|
+
await ensureDaemonRunning();
|
|
1366
|
+
|
|
1367
|
+
const limit = Math.min(parseInt(options.limit, 10) || 50, 1000);
|
|
1368
|
+
const offset = parseInt(options.offset, 10) || 0;
|
|
1369
|
+
|
|
1370
|
+
const result = await callDaemon(
|
|
1371
|
+
`/wallet/history?offset=${offset}&limit=${limit}`,
|
|
1372
|
+
);
|
|
1373
|
+
|
|
1374
|
+
if (result.error) {
|
|
1375
|
+
console.log(result.error);
|
|
1376
|
+
process.exit(1);
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
const data = result.output as
|
|
1380
|
+
| { entries?: Record<string, unknown>[]; offset?: number; limit?: number }
|
|
1381
|
+
| undefined;
|
|
1382
|
+
const entries = data?.entries || [];
|
|
1383
|
+
|
|
1384
|
+
if (entries.length === 0) {
|
|
1385
|
+
console.log("No transaction history yet.");
|
|
1386
|
+
return;
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
if (options.json) {
|
|
1390
|
+
const jsonOutput = entries.map((e: Record<string, unknown>) => {
|
|
1391
|
+
const entry = { ...e };
|
|
1392
|
+
delete entry.encodedToken;
|
|
1393
|
+
return entry;
|
|
1394
|
+
});
|
|
1395
|
+
console.log(JSON.stringify(jsonOutput, null, 2));
|
|
1396
|
+
return;
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
if (options.verbose) {
|
|
1400
|
+
for (const entry of entries) {
|
|
1401
|
+
const e = entry as Record<string, unknown>;
|
|
1402
|
+
const display: Record<string, unknown> = {};
|
|
1403
|
+
for (const key of Object.keys(e).sort()) {
|
|
1404
|
+
display[key] = e[key];
|
|
1405
|
+
}
|
|
1406
|
+
if (e.encodedToken && e.token) {
|
|
1407
|
+
display.token = e.encodedToken;
|
|
1408
|
+
delete display.encodedToken;
|
|
1409
|
+
}
|
|
1410
|
+
console.log(JSON.stringify(display, null, 2));
|
|
1411
|
+
}
|
|
1412
|
+
return;
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
const idCol = "ID";
|
|
1416
|
+
const timeCol = "Date/Time";
|
|
1417
|
+
const typeCol = "Type";
|
|
1418
|
+
const mintCol = "Mint";
|
|
1419
|
+
const amtCol = "Amount";
|
|
1420
|
+
|
|
1421
|
+
const rows = entries.map((entry: Record<string, unknown>) => {
|
|
1422
|
+
const id = String(entry.id ?? "");
|
|
1423
|
+
const time = new Date(Number(entry.createdAt)).toISOString().replace("T", " ").slice(0, 19);
|
|
1424
|
+
const type = String(entry.type ?? "").toUpperCase();
|
|
1425
|
+
const mint = String(entry.mintUrl ?? "");
|
|
1426
|
+
const unit = String(entry.unit ?? "sat");
|
|
1427
|
+
const amount = `${entry.amount} ${unit}`;
|
|
1428
|
+
return { id, time, type, mint, amount };
|
|
1429
|
+
});
|
|
1430
|
+
|
|
1431
|
+
const widths = {
|
|
1432
|
+
id: Math.max(idCol.length, ...rows.map((r) => r.id.length)),
|
|
1433
|
+
time: Math.max(timeCol.length, ...rows.map((r) => r.time.length)),
|
|
1434
|
+
type: Math.max(typeCol.length, ...rows.map((r) => r.type.length)),
|
|
1435
|
+
mint: Math.max(mintCol.length, ...rows.map((r) => r.mint.length)),
|
|
1436
|
+
amount: Math.max(amtCol.length, ...rows.map((r) => r.amount.length)),
|
|
1437
|
+
};
|
|
1438
|
+
|
|
1439
|
+
const pad = (s: string, w: number) => s.padEnd(w);
|
|
1440
|
+
const sep = Object.values(widths).map((w) => "-".repeat(w)).join(" | ");
|
|
1441
|
+
|
|
1442
|
+
console.log(
|
|
1443
|
+
`${pad(idCol, widths.id)} | ${pad(timeCol, widths.time)} | ${pad(typeCol, widths.type)} | ${pad(mintCol, widths.mint)} | ${pad(amtCol, widths.amount)}`,
|
|
1444
|
+
);
|
|
1445
|
+
console.log(sep);
|
|
1446
|
+
|
|
1447
|
+
for (const row of rows) {
|
|
1448
|
+
console.log(
|
|
1449
|
+
`${pad(row.id, widths.id)} | ${pad(row.time, widths.time)} | ${pad(row.type, widths.type)} | ${pad(row.mint, widths.mint)} | ${pad(row.amount, widths.amount)}`,
|
|
1450
|
+
);
|
|
1451
|
+
}
|
|
1452
|
+
});
|
|
1453
|
+
|
|
1273
1454
|
// Monitor - interactive TUI
|
|
1274
1455
|
program
|
|
1275
1456
|
.command("monitor")
|
|
@@ -1367,6 +1548,16 @@ walletCmd
|
|
|
1367
1548
|
await handleDaemonCommand("/wallet/status");
|
|
1368
1549
|
});
|
|
1369
1550
|
|
|
1551
|
+
walletCmd
|
|
1552
|
+
.command("doctor")
|
|
1553
|
+
.description("Diagnose conflicting wallets (current routstrd wallet vs legacy cocod)")
|
|
1554
|
+
.action(async () => {
|
|
1555
|
+
const target = summarizeWalletDirectory(defaultWalletDir(), "canonical");
|
|
1556
|
+
const source = summarizeWalletDirectory(legacyCocodDir(), "legacy");
|
|
1557
|
+
console.log(renderWalletDoctor(target, source));
|
|
1558
|
+
if (diagnoseWallets(target, source).conflict) process.exit(1);
|
|
1559
|
+
});
|
|
1560
|
+
|
|
1370
1561
|
walletCmd
|
|
1371
1562
|
.command("unlock <passphrase>")
|
|
1372
1563
|
.description("Unlock the wallet")
|
|
@@ -1384,6 +1575,113 @@ walletCmd
|
|
|
1384
1575
|
await handleDaemonCommand("/wallet/balance");
|
|
1385
1576
|
});
|
|
1386
1577
|
|
|
1578
|
+
walletCmd
|
|
1579
|
+
.command("cleanup")
|
|
1580
|
+
.description("Clear stuck pending/in-flight wallet operations")
|
|
1581
|
+
.option("--mint-url <url>", "Only clean up operations for this mint URL")
|
|
1582
|
+
.option(
|
|
1583
|
+
"--min-age <hours>",
|
|
1584
|
+
"Minimum age for reclaiming sends/cancelling melts, in hours (default: 168, one week; expired mint quotes are always failed)",
|
|
1585
|
+
"168",
|
|
1586
|
+
)
|
|
1587
|
+
.option("--dry-run", "Report what would be cleaned without applying changes", false)
|
|
1588
|
+
.option("-y, --yes", "Skip confirmation prompt", false)
|
|
1589
|
+
.action(
|
|
1590
|
+
async (options: {
|
|
1591
|
+
mintUrl?: string;
|
|
1592
|
+
minAge: string;
|
|
1593
|
+
dryRun: boolean;
|
|
1594
|
+
yes: boolean;
|
|
1595
|
+
}) => {
|
|
1596
|
+
const minAgeHours = Number.parseFloat(options.minAge);
|
|
1597
|
+
if (!Number.isFinite(minAgeHours) || minAgeHours < 0) {
|
|
1598
|
+
console.error(`Invalid --min-age value: ${options.minAge}`);
|
|
1599
|
+
process.exit(1);
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
if (!options.dryRun && !options.yes) {
|
|
1603
|
+
const rl = require("readline").createInterface({
|
|
1604
|
+
input: process.stdin,
|
|
1605
|
+
output: process.stdout,
|
|
1606
|
+
});
|
|
1607
|
+
const answer = await new Promise<string>((resolve) => {
|
|
1608
|
+
rl.question(
|
|
1609
|
+
"This will fail expired mint quotes, reclaim old pending sends, and cancel prepared melts. Continue? [y/N] ",
|
|
1610
|
+
(value: string) => {
|
|
1611
|
+
rl.close();
|
|
1612
|
+
resolve(value.trim().toLowerCase());
|
|
1613
|
+
},
|
|
1614
|
+
);
|
|
1615
|
+
});
|
|
1616
|
+
if (answer !== "y" && answer !== "yes") {
|
|
1617
|
+
console.log("Aborted.");
|
|
1618
|
+
return;
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
try {
|
|
1623
|
+
await ensureDaemonRunning();
|
|
1624
|
+
|
|
1625
|
+
const result = await callDaemon("/wallet/cleanup", {
|
|
1626
|
+
method: "POST",
|
|
1627
|
+
body: {
|
|
1628
|
+
mintUrl: options.mintUrl,
|
|
1629
|
+
minAgeMs: Math.round(minAgeHours * 60 * 60 * 1000),
|
|
1630
|
+
dryRun: options.dryRun === true,
|
|
1631
|
+
},
|
|
1632
|
+
});
|
|
1633
|
+
|
|
1634
|
+
if (result.error) {
|
|
1635
|
+
console.log(result.error);
|
|
1636
|
+
process.exit(1);
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
const output = result.output as
|
|
1640
|
+
| {
|
|
1641
|
+
dryRun?: boolean;
|
|
1642
|
+
failedMintQuotes?: number;
|
|
1643
|
+
reclaimedSends?: number;
|
|
1644
|
+
cancelledMelts?: number;
|
|
1645
|
+
skipped?: number;
|
|
1646
|
+
errors?: Array<{ operationId: string; error: string }>;
|
|
1647
|
+
}
|
|
1648
|
+
| undefined;
|
|
1649
|
+
|
|
1650
|
+
if (output) {
|
|
1651
|
+
const prefix = output.dryRun ? "Would clean up:" : "Cleaned up:";
|
|
1652
|
+
console.log(prefix);
|
|
1653
|
+
console.log(
|
|
1654
|
+
` Expired mint quotes failed: ${output.failedMintQuotes ?? 0}`,
|
|
1655
|
+
);
|
|
1656
|
+
console.log(` Pending sends reclaimed: ${output.reclaimedSends ?? 0}`);
|
|
1657
|
+
console.log(
|
|
1658
|
+
` Prepared melts cancelled: ${output.cancelledMelts ?? 0}`,
|
|
1659
|
+
);
|
|
1660
|
+
console.log(
|
|
1661
|
+
` Skipped (still recent or already terminal): ${output.skipped ?? 0}`,
|
|
1662
|
+
);
|
|
1663
|
+
if (output.errors && output.errors.length > 0) {
|
|
1664
|
+
console.log("\nErrors:");
|
|
1665
|
+
for (const e of output.errors) {
|
|
1666
|
+
console.log(` - ${e.operationId}: ${e.error}`);
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
} catch (error) {
|
|
1671
|
+
const message = (error as Error).message;
|
|
1672
|
+
if (
|
|
1673
|
+
message?.includes("fetch failed") ||
|
|
1674
|
+
message?.includes("Connection refused")
|
|
1675
|
+
) {
|
|
1676
|
+
console.error("Daemon is not running");
|
|
1677
|
+
process.exit(1);
|
|
1678
|
+
}
|
|
1679
|
+
console.error(message);
|
|
1680
|
+
process.exit(1);
|
|
1681
|
+
}
|
|
1682
|
+
},
|
|
1683
|
+
);
|
|
1684
|
+
|
|
1387
1685
|
const walletReceiveCmd = walletCmd
|
|
1388
1686
|
.command("receive")
|
|
1389
1687
|
.description("Wallet receive operations");
|
|
@@ -1495,6 +1793,47 @@ walletMintsCmd
|
|
|
1495
1793
|
});
|
|
1496
1794
|
});
|
|
1497
1795
|
|
|
1796
|
+
walletMintsCmd
|
|
1797
|
+
.command("set-default <url>")
|
|
1798
|
+
.description("Set the default mint for wallet operations")
|
|
1799
|
+
.action(async (url: string) => {
|
|
1800
|
+
await handleDaemonCommand("/wallet/mints/default", {
|
|
1801
|
+
method: "POST",
|
|
1802
|
+
body: { url },
|
|
1803
|
+
});
|
|
1804
|
+
});
|
|
1805
|
+
|
|
1806
|
+
const walletNpcCmd = walletCmd
|
|
1807
|
+
.command("npc")
|
|
1808
|
+
.description("NPC (npubx.cash) Lightning address operations");
|
|
1809
|
+
|
|
1810
|
+
walletNpcCmd
|
|
1811
|
+
.command("address")
|
|
1812
|
+
.description("Show this wallet's NPC Lightning address")
|
|
1813
|
+
.action(async () => {
|
|
1814
|
+
await handleDaemonCommand("/wallet/npc/address");
|
|
1815
|
+
});
|
|
1816
|
+
|
|
1817
|
+
walletNpcCmd
|
|
1818
|
+
.command("username <name>")
|
|
1819
|
+
.description(
|
|
1820
|
+
"Claim an NPC username (pays the claim fee from the wallet with --confirm)",
|
|
1821
|
+
)
|
|
1822
|
+
.option("--confirm", "Confirm payment of the username claim fee")
|
|
1823
|
+
.action(async (name: string, options: { confirm?: boolean }) => {
|
|
1824
|
+
await handleDaemonCommand("/wallet/npc/username", {
|
|
1825
|
+
method: "POST",
|
|
1826
|
+
body: { username: name, confirm: options.confirm === true },
|
|
1827
|
+
});
|
|
1828
|
+
});
|
|
1829
|
+
|
|
1830
|
+
walletNpcCmd
|
|
1831
|
+
.command("sync")
|
|
1832
|
+
.description("Manually sync paid NPC quotes into the wallet")
|
|
1833
|
+
.action(async () => {
|
|
1834
|
+
await handleDaemonCommand("/wallet/npc/sync", { method: "POST" });
|
|
1835
|
+
});
|
|
1836
|
+
|
|
1498
1837
|
// ── NWC (Nostr Wallet Connect) commands ─────────────────────────
|
|
1499
1838
|
|
|
1500
1839
|
const nwcCmd = program
|
|
@@ -1661,6 +2000,8 @@ serviceCmd
|
|
|
1661
2000
|
|
|
1662
2001
|
console.log("Starting routstrd via PM2...");
|
|
1663
2002
|
try {
|
|
2003
|
+
await stopLegacyCocod();
|
|
2004
|
+
|
|
1664
2005
|
// Use --interpreter bun to ensure it runs with bun
|
|
1665
2006
|
execSync(`pm2 start "${daemonPath}" --name routstrd --interpreter bun`, {
|
|
1666
2007
|
stdio: "inherit",
|
|
@@ -1708,8 +2049,9 @@ program
|
|
|
1708
2049
|
.command("restart")
|
|
1709
2050
|
.description("Restart the background daemon")
|
|
1710
2051
|
.option("--port <port>", "Port to listen on")
|
|
2052
|
+
.option("--host <host>", "Bind address (default: 127.0.0.1)")
|
|
1711
2053
|
.option("-p, --provider <provider>", "Default provider to use")
|
|
1712
|
-
.action(async (options: { port?: string; provider?: string }) => {
|
|
2054
|
+
.action(async (options: { port?: string; host?: string; provider?: string }) => {
|
|
1713
2055
|
await requireLocalDaemon();
|
|
1714
2056
|
const config = await loadConfig();
|
|
1715
2057
|
const wasRunning = await isDaemonRunning();
|
|
@@ -1718,16 +2060,19 @@ program
|
|
|
1718
2060
|
console.log("Stopping daemon...");
|
|
1719
2061
|
await callDaemon("/stop", { method: "POST" });
|
|
1720
2062
|
|
|
1721
|
-
// Wait for
|
|
1722
|
-
|
|
2063
|
+
// Wait for HTTP health check to fail AND wallet lock to be released.
|
|
2064
|
+
const pidFilePath = walletPidPath();
|
|
2065
|
+
for (let i = 0; i < 100; i++) {
|
|
1723
2066
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
1724
|
-
|
|
2067
|
+
const healthDown = !(await isDaemonRunning());
|
|
2068
|
+
const pidFileReleased = !existsSync(pidFilePath);
|
|
2069
|
+
if (healthDown && pidFileReleased) {
|
|
1725
2070
|
break;
|
|
1726
2071
|
}
|
|
1727
2072
|
}
|
|
1728
2073
|
|
|
1729
2074
|
if (await isDaemonRunning()) {
|
|
1730
|
-
logger.error("Daemon failed to stop within
|
|
2075
|
+
logger.error("Daemon failed to stop within 10 seconds");
|
|
1731
2076
|
process.exit(1);
|
|
1732
2077
|
}
|
|
1733
2078
|
console.log("Daemon stopped.");
|
|
@@ -1735,9 +2080,12 @@ program
|
|
|
1735
2080
|
console.log("Daemon was not running.");
|
|
1736
2081
|
}
|
|
1737
2082
|
|
|
2083
|
+
await stopLegacyCocod();
|
|
2084
|
+
|
|
1738
2085
|
console.log("Starting daemon...");
|
|
1739
2086
|
await startDaemon({
|
|
1740
2087
|
port: options.port || String(config.port || 8008),
|
|
2088
|
+
host: options.host || config.host || undefined,
|
|
1741
2089
|
provider: options.provider,
|
|
1742
2090
|
});
|
|
1743
2091
|
console.log("Daemon restarted.");
|
|
@@ -1820,34 +2168,71 @@ program
|
|
|
1820
2168
|
console.log("Starting daemon...");
|
|
1821
2169
|
await startDaemon({
|
|
1822
2170
|
port: String(config.port || 8008),
|
|
2171
|
+
host: config.host || undefined,
|
|
1823
2172
|
provider: config.provider || undefined,
|
|
1824
2173
|
});
|
|
1825
2174
|
console.log(`Daemon restarted with mode '${selectedMode}'.`);
|
|
1826
2175
|
});
|
|
1827
2176
|
|
|
1828
2177
|
// Logs
|
|
1829
|
-
function getLogFileForDate(date: Date = new Date()): string {
|
|
2178
|
+
function getLogFileForDate(logsDir: string, date: Date = new Date()): string {
|
|
1830
2179
|
const year = date.getFullYear();
|
|
1831
2180
|
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
1832
2181
|
const day = String(date.getDate()).padStart(2, "0");
|
|
1833
|
-
return `${
|
|
2182
|
+
return `${logsDir}/${year}-${month}-${day}.log`;
|
|
2183
|
+
}
|
|
2184
|
+
|
|
2185
|
+
function readLastLines(file: string, lines: number): string {
|
|
2186
|
+
const content = readFileSync(file, "utf8");
|
|
2187
|
+
const allLines = content.replace(/\r\n/g, "\n").split("\n");
|
|
2188
|
+
if (allLines.at(-1) === "") allLines.pop();
|
|
2189
|
+
return allLines.slice(-lines).join("\n");
|
|
2190
|
+
}
|
|
2191
|
+
|
|
2192
|
+
async function followLogFile(file: string, lines: number): Promise<void> {
|
|
2193
|
+
const initial = readLastLines(file, lines);
|
|
2194
|
+
if (initial) {
|
|
2195
|
+
console.log(initial);
|
|
2196
|
+
}
|
|
2197
|
+
|
|
2198
|
+
let position = statSync(file).size;
|
|
2199
|
+
while (true) {
|
|
2200
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2201
|
+
if (!existsSync(file)) {
|
|
2202
|
+
continue;
|
|
2203
|
+
}
|
|
2204
|
+
|
|
2205
|
+
const size = statSync(file).size;
|
|
2206
|
+
if (size < position) {
|
|
2207
|
+
position = 0;
|
|
2208
|
+
}
|
|
2209
|
+
if (size === position) {
|
|
2210
|
+
continue;
|
|
2211
|
+
}
|
|
2212
|
+
|
|
2213
|
+
const text = await Bun.file(file).slice(position, size).text();
|
|
2214
|
+
process.stdout.write(text);
|
|
2215
|
+
position = size;
|
|
2216
|
+
}
|
|
1834
2217
|
}
|
|
1835
2218
|
|
|
1836
2219
|
program
|
|
1837
2220
|
.command("logs")
|
|
1838
2221
|
.description("View daemon logs")
|
|
1839
2222
|
.option("-f, --follow", "Follow log output", false)
|
|
2223
|
+
.option("-c, --coco", "Show Cashu wallet-engine (coco) logs instead of daemon logs", false)
|
|
1840
2224
|
.option("-n, --lines <number>", "Number of lines to show", "50")
|
|
1841
|
-
.action(async (options: { follow: boolean; lines: string }) => {
|
|
2225
|
+
.action(async (options: { follow: boolean; lines: string; coco: boolean }) => {
|
|
1842
2226
|
await requireLocalDaemon();
|
|
1843
|
-
const
|
|
2227
|
+
const logsDir = options.coco ? COCO_LOGS_DIR : LOGS_DIR;
|
|
2228
|
+
const todayFile = getLogFileForDate(logsDir);
|
|
1844
2229
|
const yesterday = new Date();
|
|
1845
2230
|
yesterday.setDate(yesterday.getDate() - 1);
|
|
1846
|
-
const yesterdayFile = getLogFileForDate(yesterday);
|
|
2231
|
+
const yesterdayFile = getLogFileForDate(logsDir, yesterday);
|
|
1847
2232
|
|
|
1848
2233
|
if (!existsSync(todayFile) && !existsSync(yesterdayFile)) {
|
|
1849
2234
|
console.log("No log files found. Daemon may not have started yet.");
|
|
1850
|
-
console.log(`Logs directory: ${
|
|
2235
|
+
console.log(`Logs directory: ${logsDir}`);
|
|
1851
2236
|
process.exit(1);
|
|
1852
2237
|
}
|
|
1853
2238
|
|
|
@@ -1858,24 +2243,23 @@ program
|
|
|
1858
2243
|
});
|
|
1859
2244
|
|
|
1860
2245
|
if (options.follow) {
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
}
|
|
1866
|
-
|
|
1867
|
-
const exitCode = await proc.exited;
|
|
1868
|
-
process.exit(exitCode);
|
|
2246
|
+
if (existsSync(todayFile)) {
|
|
2247
|
+
await followLogFile(todayFile, lines);
|
|
2248
|
+
} else {
|
|
2249
|
+
console.log("No log file for today to follow.");
|
|
2250
|
+
}
|
|
2251
|
+
return;
|
|
1869
2252
|
}
|
|
1870
2253
|
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
2254
|
+
for (const file of logFiles) {
|
|
2255
|
+
if (logFiles.length > 1) {
|
|
2256
|
+
console.log(`==> ${file} <==`);
|
|
2257
|
+
}
|
|
2258
|
+
const output = readLastLines(file, lines);
|
|
2259
|
+
if (output) {
|
|
2260
|
+
console.log(output);
|
|
2261
|
+
}
|
|
2262
|
+
}
|
|
1879
2263
|
});
|
|
1880
2264
|
|
|
1881
2265
|
export function cli(args: string[]) {
|