@routstr/cocod 0.0.17 → 0.0.19
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 +9 -5
- package/bun.lock +23 -17
- package/docs/API.md +2 -2
- package/package.json +1 -1
- package/src/cli-shared.ts +43 -7
- package/src/daemon.ts +5 -3
- package/src/routes.ts +47 -7
- package/src/utils/config.ts +1 -1
- package/src/utils/wallet.ts +18 -2
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)
|
|
@@ -98,11 +101,12 @@ cocod x-cashu handle "<encoded-x-cashu-request>"
|
|
|
98
101
|
|
|
99
102
|
Defaults:
|
|
100
103
|
|
|
101
|
-
-
|
|
102
|
-
-
|
|
103
|
-
-
|
|
104
|
-
-
|
|
105
|
-
-
|
|
104
|
+
- Base directory: `~/.cocod` (or `COCOD_DIR`)
|
|
105
|
+
- Socket: `<base>/cocod.sock` (or `COCOD_SOCKET`)
|
|
106
|
+
- PID file: `<base>/cocod.pid` (or `COCOD_PID`)
|
|
107
|
+
- Daemon log: `<base>/daemon.log` (or `COCOD_LOG_FILE`)
|
|
108
|
+
- Config: `<base>/config.json`
|
|
109
|
+
- Database: `<base>/coco.db`
|
|
106
110
|
|
|
107
111
|
Logging defaults:
|
|
108
112
|
|
package/bun.lock
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"lockfileVersion":
|
|
3
|
-
"configVersion": 1,
|
|
2
|
+
"lockfileVersion": 0,
|
|
4
3
|
"workspaces": {
|
|
5
4
|
"": {
|
|
6
|
-
"name": "cocod",
|
|
7
5
|
"dependencies": {
|
|
8
6
|
"@scure/bip39": "^2.0.1",
|
|
9
7
|
"coco-cashu-core": "1.1.2-rc.50",
|
|
@@ -22,25 +20,25 @@
|
|
|
22
20
|
},
|
|
23
21
|
},
|
|
24
22
|
"packages": {
|
|
25
|
-
"@cashu/cashu-ts": ["@cashu/cashu-ts@3.6.
|
|
23
|
+
"@cashu/cashu-ts": ["@cashu/cashu-ts@3.6.4", "", { "dependencies": { "@noble/curves": "^2.0.1", "@noble/hashes": "^2.0.1", "@scure/base": "^2.0.0", "@scure/bip32": "^2.0.1" } }, "sha512-a6Asqk+wPEk9a6BQmdLMeegngj0KHKicABUEtr1cUMLTAUYHVHfdfEuHDlU+bNUqXXyaTssG7yo/ZhfHrgbSkQ=="],
|
|
26
24
|
|
|
27
25
|
"@noble/ciphers": ["@noble/ciphers@2.1.1", "", {}, "sha512-bysYuiVfhxNJuldNXlFEitTVdNnYUc+XNJZd7Qm2a5j1vZHgY+fazadNFWFaMK/2vye0JVlxV3gHmC0WDfAOQw=="],
|
|
28
26
|
|
|
29
|
-
"@noble/curves": ["@noble/curves@2.0
|
|
27
|
+
"@noble/curves": ["@noble/curves@2.2.0", "", { "dependencies": { "@noble/hashes": "2.2.0" } }, "sha512-T/BoHgFXirb0ENSPBquzX0rcjXeM6Lo892a2jlYJkqk83LqZx0l1Of7DzlKJ6jkpvMrkHSnAcgb5JegL8SeIkQ=="],
|
|
30
28
|
|
|
31
|
-
"@noble/hashes": ["@noble/hashes@2.0
|
|
29
|
+
"@noble/hashes": ["@noble/hashes@2.2.0", "", {}, "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg=="],
|
|
32
30
|
|
|
33
|
-
"@scure/base": ["@scure/base@2.
|
|
31
|
+
"@scure/base": ["@scure/base@2.2.0", "", {}, "sha512-b8XEupJibegiXV+tDUseI8oLQc8ei3d/4Jkb2RpbHh3MfE054ov3uIz2dhFkB3FI8iwYkEh0gGCApkrYggkPNg=="],
|
|
34
32
|
|
|
35
|
-
"@scure/bip32": ["@scure/bip32@2.0
|
|
33
|
+
"@scure/bip32": ["@scure/bip32@2.2.0", "", { "dependencies": { "@noble/curves": "2.2.0", "@noble/hashes": "2.2.0", "@scure/base": "2.2.0" } }, "sha512-zFr7t2F+a9+5tB7QbarF2HQNYrgjCNaoLAupZdKkrFMYMozJf5zqH2WJCQibMzm1qQ0QogrxVGO3qXfQDYMaQg=="],
|
|
36
34
|
|
|
37
|
-
"@scure/bip39": ["@scure/bip39@2.0
|
|
35
|
+
"@scure/bip39": ["@scure/bip39@2.2.0", "", { "dependencies": { "@noble/hashes": "2.2.0", "@scure/base": "2.2.0" } }, "sha512-T/Bj/YvYMNkIPq6EENO6/rcs2e7qTNuyoUXf0KBFDmp0ZDu0H2X4Lq6yC3i0c8PcWkov5EbW+yQZZbdMmk154A=="],
|
|
38
36
|
|
|
39
|
-
"@types/bun": ["@types/bun@1.3.
|
|
37
|
+
"@types/bun": ["@types/bun@1.3.13", "", { "dependencies": { "bun-types": "1.3.13" } }, "sha512-9fqXWk5YIHGGnUau9TEi+qdlTYDAnOj+xLCmSTwXfAIqXr2x4tytJb43E9uCvt09zJURKXwAtkoH4nLQfzeTXw=="],
|
|
40
38
|
|
|
41
|
-
"@types/node": ["@types/node@25.
|
|
39
|
+
"@types/node": ["@types/node@25.6.0", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ=="],
|
|
42
40
|
|
|
43
|
-
"bun-types": ["bun-types@1.3.
|
|
41
|
+
"bun-types": ["bun-types@1.3.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-QXKeHLlOLqQX9LgYaHJfzdBaV21T63HhFJnvuRCcjZiaUDpbs5ED1MgxbMra71CsryN/1dAoXuJJJwIv/2drVA=="],
|
|
44
42
|
|
|
45
43
|
"coco-cashu-core": ["coco-cashu-core@1.1.2-rc.50", "", { "dependencies": { "@cashu/cashu-ts": "^3.5.0", "@noble/curves": "^2.0.1", "@noble/hashes": "^2.0.1", "@scure/bip32": "^2.0.1" }, "peerDependencies": { "typescript": "^5" } }, "sha512-eK5YuwvCWpeCwF/GEkMo90FCyek2mS+smQ51wKjkjTKzpkVVlNln52l2h1Wwce4VMnW6GSeL2IgT8IksM8Umuw=="],
|
|
46
44
|
|
|
@@ -48,9 +46,9 @@
|
|
|
48
46
|
|
|
49
47
|
"coco-cashu-sqlite-bun": ["coco-cashu-sqlite-bun@1.1.2-rc.50", "", { "peerDependencies": { "coco-cashu-core": "1.1.2-rc.50", "typescript": "^5" } }, "sha512-wYAZjGmk3Xd75rq7y6AGLji4z5/3v5rWj6xgQoa3Qq+37zKnuCeLqVt+A3x/3EcNRLmNoW/u7vQJTJP4APHhIw=="],
|
|
50
48
|
|
|
51
|
-
"commander": ["commander@14.0.
|
|
49
|
+
"commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="],
|
|
52
50
|
|
|
53
|
-
"nostr-tools": ["nostr-tools@2.
|
|
51
|
+
"nostr-tools": ["nostr-tools@2.23.3", "", { "dependencies": { "@noble/ciphers": "2.1.1", "@noble/curves": "2.0.1", "@noble/hashes": "2.0.1", "@scure/base": "2.0.0", "@scure/bip32": "2.0.1", "@scure/bip39": "2.0.1", "nostr-wasm": "0.1.0" }, "peerDependencies": { "typescript": ">=5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-AALyt9k8xPdF4UV2mlLJ2mgCn4kpTB0DZ8t2r6wjdUh6anfx2cTVBsHUlo9U0EY/cKC5wcNyiMAmRJV5OVEalA=="],
|
|
54
52
|
|
|
55
53
|
"nostr-wasm": ["nostr-wasm@0.1.0", "", {}, "sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA=="],
|
|
56
54
|
|
|
@@ -58,12 +56,20 @@
|
|
|
58
56
|
|
|
59
57
|
"npubcash-types": ["npubcash-types@0.1.1", "", {}, "sha512-/HGfes2cvQpkrWOuUrdemJJhJUQu+xiXl4x+AxQtKUMGCB8uEhfrXJfAQ0n+DgtkIX1YVLOh0a/e6E5LX+fUew=="],
|
|
60
58
|
|
|
61
|
-
"prettier": ["prettier@3.8.
|
|
59
|
+
"prettier": ["prettier@3.8.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw=="],
|
|
62
60
|
|
|
63
61
|
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
|
64
62
|
|
|
65
|
-
"undici-types": ["undici-types@7.
|
|
63
|
+
"undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="],
|
|
66
64
|
|
|
67
|
-
"
|
|
65
|
+
"nostr-tools/@noble/curves": ["@noble/curves@2.0.1", "", { "dependencies": { "@noble/hashes": "2.0.1" } }, "sha512-vs1Az2OOTBiP4q0pwjW5aF0xp9n4MxVrmkFBxc6EKZc6ddYx5gaZiAsZoq0uRRXWbi3AT/sBqn05eRPtn1JCPw=="],
|
|
66
|
+
|
|
67
|
+
"nostr-tools/@noble/hashes": ["@noble/hashes@2.0.1", "", {}, "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw=="],
|
|
68
|
+
|
|
69
|
+
"nostr-tools/@scure/base": ["@scure/base@2.0.0", "", {}, "sha512-3E1kpuZginKkek01ovG8krQ0Z44E3DHPjc5S2rjJw9lZn3KSQOs8S7wqikF/AH7iRanHypj85uGyxk0XAyC37w=="],
|
|
70
|
+
|
|
71
|
+
"nostr-tools/@scure/bip32": ["@scure/bip32@2.0.1", "", { "dependencies": { "@noble/curves": "2.0.1", "@noble/hashes": "2.0.1", "@scure/base": "2.0.0" } }, "sha512-4Md1NI5BzoVP+bhyJaY3K6yMesEFzNS1sE/cP+9nuvE7p/b0kx9XbpDHHFl8dHtufcbdHRUUQdRqLIPHN/s7yA=="],
|
|
72
|
+
|
|
73
|
+
"nostr-tools/@scure/bip39": ["@scure/bip39@2.0.1", "", { "dependencies": { "@noble/hashes": "2.0.1", "@scure/base": "2.0.0" } }, "sha512-PsxdFj/d2AcJcZDX1FXN3dDgitDDTmwf78rKZq1a6c1P1Nan1X/Sxc7667zU3U+AN60g7SxxP0YCVw2H/hBycg=="],
|
|
68
74
|
}
|
|
69
75
|
}
|
package/docs/API.md
CHANGED
|
@@ -59,8 +59,8 @@ All commands are available under `cocod`.
|
|
|
59
59
|
|
|
60
60
|
The CLI talks to the daemon over HTTP on a UNIX socket.
|
|
61
61
|
|
|
62
|
-
-
|
|
63
|
-
-
|
|
62
|
+
- Base directory env var: `COCOD_DIR` (default `~/.cocod`)
|
|
63
|
+
- Socket path env var: `COCOD_SOCKET` (default `<COCOD_DIR>/cocod.sock`)
|
|
64
64
|
|
|
65
65
|
### Response shape
|
|
66
66
|
|
package/package.json
CHANGED
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
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) {
|
package/src/utils/config.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { homedir } from "node:os";
|
|
2
2
|
|
|
3
|
-
export const CONFIG_DIR = `${homedir()}/.cocod`;
|
|
3
|
+
export const CONFIG_DIR = process.env.COCOD_DIR || `${homedir()}/.cocod`;
|
|
4
4
|
export const SOCKET_PATH = process.env.COCOD_SOCKET || `${CONFIG_DIR}/cocod.sock`;
|
|
5
5
|
export const PID_FILE = process.env.COCOD_PID || `${CONFIG_DIR}/cocod.pid`;
|
|
6
6
|
export const LOG_FILE = process.env.COCOD_LOG_FILE || `${CONFIG_DIR}/daemon.log`;
|
package/src/utils/wallet.ts
CHANGED
|
@@ -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
|
|
33
|
-
|
|
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
|
}
|