@routstr/cocod 0.0.17 → 0.0.18

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/README.md CHANGED
@@ -66,6 +66,9 @@ cocod history --watch
66
66
  cocod logs
67
67
  cocod logs --follow
68
68
  cocod logs --path
69
+
70
+ # Debug a stuck init/unlock in another terminal
71
+ cocod logs --follow
69
72
  ```
70
73
 
71
74
  ## NPC (Lightning Address)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@routstr/cocod",
3
- "version": "0.0.17",
3
+ "version": "0.0.18",
4
4
  "module": "src/index.ts",
5
5
  "type": "module",
6
6
  "private": false,
package/src/cli-shared.ts CHANGED
@@ -42,8 +42,10 @@ export async function isDaemonRunning(): Promise<boolean> {
42
42
  }
43
43
  }
44
44
 
45
- const DAEMON_POLL_INTERVAL_MS = 100;
45
+ const DAEMON_POLL_INTERVAL_MS = 1_000;
46
46
  const DAEMON_SLOW_START_WARNING_MS = 30_000;
47
+ const DAEMON_START_TIMEOUT_MS = 60_000;
48
+ const DAEMON_START_LOG_LINES = 40;
47
49
 
48
50
  function sleep(ms: number): Promise<void> {
49
51
  return new Promise((resolve) => setTimeout(resolve, ms));
@@ -54,24 +56,50 @@ async function waitForDaemonReady(startedAt: number, warningShown: { value: bool
54
56
  try {
55
57
  const result = await callDaemon("/status");
56
58
  if (typeof result.output === "string") {
57
- const status = result.output;
58
- if (status === "LOCKED" || status === "UNLOCKED" || status === "ERROR") {
59
- return;
60
- }
59
+ return;
61
60
  }
62
61
  } catch {
63
62
  // Daemon may not be accepting requests yet
64
63
  }
65
64
 
66
- if (!warningShown.value && Date.now() - startedAt >= DAEMON_SLOW_START_WARNING_MS) {
65
+ const elapsedMs = Date.now() - startedAt;
66
+
67
+ if (!warningShown.value && elapsedMs >= DAEMON_SLOW_START_WARNING_MS) {
67
68
  warningShown.value = true;
68
69
  console.log("Daemon is taking longer than expected, please wait...");
69
70
  }
70
71
 
72
+ if (elapsedMs >= DAEMON_START_TIMEOUT_MS) {
73
+ throw new Error("Daemon failed to start after 1 minute");
74
+ }
75
+
71
76
  await sleep(DAEMON_POLL_INTERVAL_MS);
72
77
  }
73
78
  }
74
79
 
80
+ function printProgressStep(message: string): void {
81
+ console.log(`• ${message}`);
82
+ }
83
+
84
+ function maybePrintFriendlyProgress(path: string, body?: object): void {
85
+ if (path === "/init") {
86
+ const mintUrl =
87
+ body && "mintUrl" in body && typeof body.mintUrl === "string"
88
+ ? body.mintUrl
89
+ : "https://mint.minibits.cash/Bitcoin";
90
+
91
+ printProgressStep("Preparing wallet...");
92
+ printProgressStep(`Connecting to mint: ${mintUrl}`);
93
+ printProgressStep("This can take a few seconds on first run.");
94
+ return;
95
+ }
96
+
97
+ if (path === "/unlock") {
98
+ printProgressStep("Unlocking wallet...");
99
+ printProgressStep("Reconnecting wallet services...");
100
+ }
101
+ }
102
+
75
103
  export async function startDaemonProcess(): Promise<void> {
76
104
  const proc = Bun.spawn({
77
105
  cmd: ["bun", "run", `${import.meta.dir}/index.ts`, "daemon"],
@@ -91,9 +119,16 @@ export async function startDaemonProcess(): Promise<void> {
91
119
  return;
92
120
  }
93
121
 
94
- if (!warningShown.value && Date.now() - startedAt >= DAEMON_SLOW_START_WARNING_MS) {
122
+ const elapsedMs = Date.now() - startedAt;
123
+
124
+ if (!warningShown.value && elapsedMs >= DAEMON_SLOW_START_WARNING_MS) {
95
125
  warningShown.value = true;
96
126
  console.log("Daemon is taking longer than expected, please wait...");
127
+ console.log(`Tip: run 'cocod logs --follow' or 'tail -n ${DAEMON_START_LOG_LINES} ~/.cocod/daemon.log' in another terminal.`);
128
+ }
129
+
130
+ if (elapsedMs >= DAEMON_START_TIMEOUT_MS) {
131
+ throw new Error("Daemon failed to start after 1 minute");
97
132
  }
98
133
  }
99
134
  }
@@ -113,6 +148,7 @@ export async function handleDaemonCommand(
113
148
  ): Promise<CommandResponse> {
114
149
  try {
115
150
  await ensureDaemonRunning();
151
+ maybePrintFriendlyProgress(path, options.body);
116
152
  const result = await callDaemon(path, options);
117
153
 
118
154
  if (result.error) {
package/src/daemon.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { mnemonicToSeedSync } from "@scure/bip39";
2
2
  import { closeSync, openSync, writeFileSync } from "node:fs";
3
- import { unlink } from "node:fs/promises";
3
+ import { mkdir, unlink } from "node:fs/promises";
4
4
  import process from "node:process";
5
- import { CONFIG_FILE, SOCKET_PATH, PID_FILE } from "./utils/config.js";
5
+ import { CONFIG_DIR, CONFIG_FILE, SOCKET_PATH, PID_FILE } from "./utils/config.js";
6
6
  import { createDaemonLogger, serializeError } from "./utils/logger.js";
7
7
  import { DaemonStateManager } from "./utils/state.js";
8
8
  import { initializeWallet } from "./utils/wallet.js";
@@ -23,6 +23,8 @@ async function isProcessAlive(pid: number): Promise<boolean> {
23
23
  }
24
24
 
25
25
  async function acquirePidLock(logger: ReturnType<typeof createDaemonLogger>): Promise<void> {
26
+ await mkdir(CONFIG_DIR, { recursive: true });
27
+
26
28
  const pidFile = Bun.file(PID_FILE);
27
29
  if (await pidFile.exists()) {
28
30
  const existingPidText = (await pidFile.text()).trim();
@@ -74,7 +76,7 @@ async function acquirePidLock(logger: ReturnType<typeof createDaemonLogger>): Pr
74
76
 
75
77
  export async function startDaemon() {
76
78
  const stateManager = new DaemonStateManager();
77
- const logger = createDaemonLogger();
79
+ const logger = createDaemonLogger({ mirrorToConsole: false });
78
80
 
79
81
  logger.info("daemon.start.requested", {
80
82
  pidFile: PID_FILE,
package/src/routes.ts CHANGED
@@ -36,34 +36,50 @@ export function createRouteHandlers(
36
36
  },
37
37
  "/init": {
38
38
  POST: stateManager.requireUninitialized(async (req: Request) => {
39
+ const initLogger = logger?.child?.({ route: "/init" }) ?? logger;
40
+
39
41
  try {
42
+ initLogger?.info?.("wallet.init.started");
43
+
40
44
  const body = (await req.json()) as {
41
45
  mnemonic?: string;
42
46
  passphrase?: string;
43
47
  mintUrl?: string;
44
48
  };
45
49
 
50
+ initLogger?.info?.("wallet.init.request_parsed", {
51
+ encrypted: Boolean(body.passphrase),
52
+ hasMnemonic: Boolean(body.mnemonic),
53
+ mintUrl: body.mintUrl || "https://mint.minibits.cash/Bitcoin",
54
+ });
55
+
46
56
  let mnemonic: string;
47
57
  if (body.mnemonic) {
58
+ initLogger?.info?.("wallet.init.validating_mnemonic");
48
59
  if (!validateMnemonic(body.mnemonic, wordlist)) {
60
+ initLogger?.warn?.("wallet.init.invalid_mnemonic");
49
61
  return Response.json({ error: "Invalid mnemonic" }, { status: 400 });
50
62
  }
51
63
  mnemonic = body.mnemonic;
52
64
  } else {
65
+ initLogger?.info?.("wallet.init.generating_mnemonic");
53
66
  mnemonic = generateMnemonic(wordlist, 256);
54
67
  }
55
68
 
56
69
  const mintUrl = body.mintUrl || "https://mint.minibits.cash/Bitcoin";
57
70
  const encrypted = !!body.passphrase;
58
71
 
72
+ initLogger?.info?.("wallet.init.resetting_config_file", { configFile: CONFIG_FILE });
59
73
  await Bun.write(CONFIG_FILE, "");
60
74
  await unlink(CONFIG_FILE);
61
75
 
62
76
  let config: WalletConfig;
63
77
 
64
78
  if (encrypted && body.passphrase) {
79
+ initLogger?.info?.("wallet.init.encrypting_mnemonic");
65
80
  const { ciphertext, salt } = await encryptMnemonic(mnemonic, body.passphrase);
66
81
 
82
+ initLogger?.info?.("wallet.init.writing_salt_file", { saltFile: SALT_FILE });
67
83
  await Bun.write(SALT_FILE, salt);
68
84
 
69
85
  config = {
@@ -75,6 +91,7 @@ export function createRouteHandlers(
75
91
  };
76
92
 
77
93
  stateManager.setLocked(ciphertext, mintUrl);
94
+ initLogger?.info?.("wallet.init.completed_locked", { mintUrl, state: "LOCKED" });
78
95
  } else {
79
96
  config = {
80
97
  version: 1,
@@ -84,19 +101,25 @@ export function createRouteHandlers(
84
101
  createdAt: new Date().toISOString(),
85
102
  };
86
103
 
87
- const manager = await initializeWallet(config, undefined, logger);
104
+ initLogger?.info?.("wallet.init.initializing_wallet_manager", { mintUrl });
105
+ const manager = await initializeWallet(config, undefined, initLogger);
106
+ initLogger?.info?.("wallet.init.wallet_manager_ready", { mintUrl });
88
107
  const seed = mnemonicToSeedSync(mnemonic);
89
108
  stateManager.setUnlocked(manager, mintUrl, seed);
109
+ initLogger?.info?.("wallet.init.completed_unlocked", { mintUrl, state: "UNLOCKED" });
90
110
  }
91
111
 
112
+ initLogger?.info?.("wallet.init.writing_config_file", { configFile: CONFIG_FILE });
92
113
  await Bun.write(CONFIG_FILE, JSON.stringify(config, null, 2));
93
114
 
94
115
  const output = encrypted
95
116
  ? `Initialized (locked). Mnemonic: ${mnemonic}\nIMPORTANT: Write down this mnemonic and keep it safe!`
96
117
  : `Initialized. Mnemonic: ${mnemonic}\nIMPORTANT: Write down this mnemonic and keep it safe!`;
97
118
 
119
+ initLogger?.info?.("wallet.init.response_ready", { encrypted, mintUrl });
98
120
  return Response.json({ output });
99
121
  } catch (error) {
122
+ initLogger?.error?.("wallet.init.failed", { error: serializeError(error) });
100
123
  const message = error instanceof Error ? error.message : String(error);
101
124
  return Response.json({ error: `Init failed: ${message}` }, { status: 500 });
102
125
  }
@@ -104,15 +127,21 @@ export function createRouteHandlers(
104
127
  },
105
128
  "/unlock": {
106
129
  POST: stateManager.requireLocked(async (req: Request, state: LockedState) => {
130
+ const unlockLogger = logger?.child?.({ route: "/unlock" }) ?? logger;
131
+
107
132
  try {
133
+ unlockLogger?.info?.("wallet.unlock.started", { mintUrl: state.mintUrl });
108
134
  const body = (await req.json()) as { passphrase: string };
109
135
 
110
136
  if (!body.passphrase) {
137
+ unlockLogger?.warn?.("wallet.unlock.missing_passphrase");
111
138
  return Response.json({ error: "Passphrase required" }, { status: 400 });
112
139
  }
113
140
 
141
+ unlockLogger?.info?.("wallet.unlock.reading_salt_file", { saltFile: SALT_FILE });
114
142
  const salt = await Bun.file(SALT_FILE).text();
115
143
  const { decryptMnemonic } = await import("./utils/crypto.js");
144
+ unlockLogger?.info?.("wallet.unlock.decrypting_mnemonic");
116
145
  const mnemonic = await decryptMnemonic(state.encryptedMnemonic, body.passphrase, salt);
117
146
 
118
147
  const config: WalletConfig = {
@@ -123,13 +152,22 @@ export function createRouteHandlers(
123
152
  createdAt: new Date().toISOString(),
124
153
  };
125
154
 
126
- const manager = await initializeWallet(config, undefined, logger);
155
+ unlockLogger?.info?.("wallet.unlock.initializing_wallet_manager", {
156
+ mintUrl: state.mintUrl,
157
+ });
158
+ const manager = await initializeWallet(config, undefined, unlockLogger);
159
+ unlockLogger?.info?.("wallet.unlock.wallet_manager_ready", { mintUrl: state.mintUrl });
127
160
  const seed = mnemonicToSeedSync(mnemonic);
128
161
 
129
162
  stateManager.setUnlocked(manager, state.mintUrl, seed);
163
+ unlockLogger?.info?.("wallet.unlock.completed", {
164
+ mintUrl: state.mintUrl,
165
+ state: "UNLOCKED",
166
+ });
130
167
 
131
168
  return Response.json({ output: "Unlocked" });
132
169
  } catch (error) {
170
+ unlockLogger?.error?.("wallet.unlock.failed", { error: serializeError(error) });
133
171
  const message = error instanceof Error ? error.message : String(error);
134
172
  return Response.json({ error: `Unlock failed: ${message}` }, { status: 401 });
135
173
  }
@@ -504,11 +542,13 @@ async function runRoute(
504
542
  const durationMs = Math.round(performance.now() - startedAt);
505
543
  const level = response.status >= 500 ? "error" : response.status >= 400 ? "warn" : "info";
506
544
 
507
- requestLogger?.log?.(level, "request.completed", {
508
- durationMs,
509
- state: getState().status,
510
- status: response.status,
511
- });
545
+ if (path !== "/status") {
546
+ requestLogger?.log?.(level, "request.completed", {
547
+ durationMs,
548
+ state: getState().status,
549
+ status: response.status,
550
+ });
551
+ }
512
552
 
513
553
  return response;
514
554
  } catch (error) {
@@ -14,38 +14,54 @@ export async function initializeWallet(
14
14
  passphrase?: string,
15
15
  logger?: Logger,
16
16
  ): Promise<Manager> {
17
+ const walletLogger = logger?.child?.({ component: "wallet-init" }) ?? logger;
18
+ walletLogger?.info?.("wallet.initialize.started", {
19
+ encrypted: config.encrypted,
20
+ mintUrl: config.mintUrl,
21
+ dbFile: DB_FILE,
22
+ });
23
+
17
24
  let mnemonic: string;
18
25
 
19
26
  if (config.encrypted) {
27
+ walletLogger?.info?.("wallet.initialize.decrypting_config_mnemonic", { saltFile: SALT_FILE });
20
28
  if (!passphrase) {
21
29
  throw new Error("Passphrase required for encrypted wallet");
22
30
  }
23
31
  const salt = await Bun.file(SALT_FILE).text();
24
32
  mnemonic = await decryptMnemonic(config.mnemonic, passphrase, salt);
25
33
  } else {
34
+ walletLogger?.info?.("wallet.initialize.using_plaintext_config_mnemonic");
26
35
  mnemonic = config.mnemonic;
27
36
  }
28
37
 
38
+ walletLogger?.info?.("wallet.initialize.derived_mnemonic");
29
39
  const seed = mnemonicToSeedSync(mnemonic);
30
40
 
41
+ walletLogger?.info?.("wallet.initialize.opening_database", { dbFile: DB_FILE });
31
42
  const repo = new SqliteRepositories({ database: new Database(DB_FILE) });
32
- const walletLogger = logger?.child?.({ component: "coco" }) ?? logger;
33
- const cocoLogger = walletLogger ?? new ConsoleLogger("Coco", { level: "info" });
43
+ const cocoLogger = walletLogger?.child?.({ component: "coco" }) ?? new ConsoleLogger("Coco", { level: "info" });
44
+ walletLogger?.info?.("wallet.initialize.preparing_signer");
34
45
  const sk = privateKeyFromSeedWords(mnemonic);
35
46
  const signer = async (t: EventTemplate) => finalizeEvent(t, sk);
47
+ walletLogger?.info?.("wallet.initialize.creating_npc_plugin", { npcUrl: "https://npubx.cash" });
36
48
  const npcPlugin = new NPCPlugin("https://npubx.cash", signer, {
37
49
  useWebsocket: true,
38
50
  logger: cocoLogger,
39
51
  });
52
+ walletLogger?.info?.("wallet.initialize.initializing_coco_core", { mintUrl: config.mintUrl });
40
53
  const coco = await initializeCoco({
41
54
  repo,
42
55
  seedGetter: async () => seed,
43
56
  logger: cocoLogger,
44
57
  });
45
58
 
59
+ walletLogger?.info?.("wallet.initialize.registering_npc_plugin");
46
60
  coco.use(npcPlugin);
47
61
 
62
+ walletLogger?.info?.("wallet.initialize.adding_trusted_mint", { mintUrl: config.mintUrl });
48
63
  await coco.mint.addMint(config.mintUrl, { trusted: true });
64
+ walletLogger?.info?.("wallet.initialize.completed", { mintUrl: config.mintUrl });
49
65
 
50
66
  return coco;
51
67
  }