routstrd 0.4.8 → 0.4.10

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.
Files changed (37) hide show
  1. package/README.md +124 -5
  2. package/SKILL.md +341 -0
  3. package/dist/daemon/index.js +7995 -14602
  4. package/dist/index.js +155916 -14225
  5. package/package.json +9 -4
  6. package/src/cli.ts +279 -110
  7. package/src/daemon/args.ts +4 -5
  8. package/src/daemon/http/index.ts +37 -2
  9. package/src/daemon/index.ts +94 -33
  10. package/src/daemon/models.ts +13 -10
  11. package/src/daemon/wallet/auto-refill.ts +1 -1
  12. package/src/daemon/wallet/coco-client.ts +205 -3
  13. package/src/daemon/wallet/cocod-client.ts +19 -3
  14. package/src/daemon/wallet/index.ts +1 -1
  15. package/src/index.ts +9 -1
  16. package/src/runtime.ts +55 -0
  17. package/src/start-daemon.ts +4 -8
  18. package/src/tui/usage/app.ts +16 -2
  19. package/src/tui/usage/data.ts +110 -0
  20. package/src/tui/usage/render.ts +45 -20
  21. package/src/utils/clients.ts +74 -1
  22. package/src/utils/config.ts +14 -0
  23. package/src/utils/daemon-stop.ts +125 -0
  24. package/src/utils/standalone-update.ts +214 -0
  25. package/src/utils/update-checker.ts +81 -18
  26. package/src/version.ts +3 -0
  27. package/src/cli.test.ts +0 -192
  28. package/src/daemon/fatal-error.test.ts +0 -104
  29. package/src/daemon/http/usage-summary.test.ts +0 -272
  30. package/src/daemon/models.test.ts +0 -81
  31. package/src/daemon/wallet/cleanup.test.ts +0 -168
  32. package/src/daemon/wallet/coco-client.npc.test.ts +0 -252
  33. package/src/daemon/wallet/coco-client.test.ts +0 -669
  34. package/src/daemon/wallet/diagnostics.test.ts +0 -378
  35. package/src/daemon/wallet/fixtures/cocod-0.0.24-wallet.db.gz +0 -0
  36. package/src/daemon/wallet/migration.test.ts +0 -171
  37. package/src/daemon/wallet/receive-dedup.test.ts +0 -238
@@ -1,252 +0,0 @@
1
- import { afterEach, describe, expect, it, mock } from "bun:test";
2
- import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "fs";
3
- import { tmpdir } from "os";
4
- import { join } from "path";
5
-
6
- // The real NPCPlugin opens a websocket to npubx.cash once the wallet is ready.
7
- // Mock the module before importing coco-client so tests never touch the network.
8
- const npcState = {
9
- info: { name: "alice" as string | null, pubkey: "ab".repeat(32) },
10
- usernameResult: { success: true } as
11
- | { success: true }
12
- | { success: false; pr: { amount: number; mints: string[] } },
13
- setUsernameCalls: [] as Array<{ username: string; attemptPayment?: boolean }>,
14
- syncCalls: 0,
15
- };
16
-
17
- class MockNPCPlugin {
18
- readonly name = "npc";
19
- readonly required = [] as const;
20
- private readonly api = {
21
- getInfo: async () => npcState.info,
22
- setUsername: async (username: string, attemptPayment?: boolean) => {
23
- npcState.setUsernameCalls.push({ username, attemptPayment });
24
- return npcState.usernameResult;
25
- },
26
- sync: async () => {
27
- npcState.syncCalls += 1;
28
- },
29
- };
30
-
31
- onInit(ctx: { registerExtension: (key: string, api: unknown) => void }) {
32
- ctx.registerExtension("npc", this.api);
33
- return () => {};
34
- }
35
- }
36
-
37
- mock.module("coco-cashu-plugin-npc", () => ({
38
- NPCPlugin: MockNPCPlugin,
39
- }));
40
-
41
- const { createCocoClient } = await import("./coco-client");
42
- const { createCocodClient, CocodHttpError } = await import("./cocod-client");
43
-
44
- const TEST_MNEMONIC =
45
- "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
46
-
47
- const tempDirs: string[] = [];
48
-
49
- function makeWalletDir(): string {
50
- const dir = mkdtempSync(join(tmpdir(), "routstrd-coco-npc-test-"));
51
- tempDirs.push(dir);
52
- const walletDir = join(dir, "wallet");
53
- mkdirSync(walletDir, { recursive: true });
54
- writeFileSync(
55
- join(walletDir, "config.json"),
56
- JSON.stringify({ version: 1, mnemonic: TEST_MNEMONIC, encrypted: false }),
57
- );
58
- return walletDir;
59
- }
60
-
61
- function testClientOptions(walletDir = makeWalletDir()) {
62
- return {
63
- walletDir,
64
- legacySocketPath: join(walletDir, "legacy-cocod.sock"),
65
- legacyPidPath: join(walletDir, "legacy-cocod.pid"),
66
- };
67
- }
68
-
69
- function resetNpcState(): void {
70
- npcState.info = { name: "alice", pubkey: "ab".repeat(32) };
71
- npcState.usernameResult = { success: true };
72
- npcState.setUsernameCalls = [];
73
- npcState.syncCalls = 0;
74
- }
75
-
76
- afterEach(() => {
77
- resetNpcState();
78
- for (const dir of tempDirs.splice(0)) {
79
- rmSync(dir, { recursive: true, force: true });
80
- }
81
- });
82
-
83
- describe("in-process coco client NPC integration", () => {
84
- it("registers the plugin and exposes the wallet's NPC lightning address", async () => {
85
- const client = await createCocoClient(testClientOptions());
86
- try {
87
- const info = await client.getNpcAddress();
88
- expect(info).toEqual({
89
- address: "alice@npubx.cash",
90
- name: "alice",
91
- pubkey: "ab".repeat(32),
92
- });
93
- } finally {
94
- await client.dispose?.();
95
- }
96
- });
97
-
98
- it("falls back to the npub address when no username is set", async () => {
99
- npcState.info = { name: null, pubkey: "ab".repeat(32) };
100
- const client = await createCocoClient(testClientOptions());
101
- try {
102
- const info = await client.getNpcAddress();
103
- expect(info.address).toStartWith("npub1");
104
- expect(info.address).toEndWith("@npubx.cash");
105
- expect(info.name).toBeUndefined();
106
- expect(info.pubkey).toBe("ab".repeat(32));
107
- } finally {
108
- await client.dispose?.();
109
- }
110
- });
111
-
112
- it("maps payment-required username claims without paying", async () => {
113
- npcState.usernameResult = {
114
- success: false,
115
- pr: { amount: 21, mints: ["https://mint.example.com"] },
116
- };
117
- const client = await createCocoClient(testClientOptions());
118
- try {
119
- const result = await client.setNpcUsername("bob");
120
- expect(result).toEqual({
121
- success: false,
122
- paymentRequest: { amount: 21, mints: ["https://mint.example.com"] },
123
- });
124
- expect(npcState.setUsernameCalls).toEqual([
125
- { username: "bob", attemptPayment: false },
126
- ]);
127
- } finally {
128
- await client.dispose?.();
129
- }
130
- });
131
-
132
- it("confirms the claim fee payment when requested", async () => {
133
- const client = await createCocoClient(testClientOptions());
134
- try {
135
- const result = await client.setNpcUsername("bob", true);
136
- expect(result).toEqual({ success: true });
137
- expect(npcState.setUsernameCalls).toEqual([
138
- { username: "bob", attemptPayment: true },
139
- ]);
140
- } finally {
141
- await client.dispose?.();
142
- }
143
- });
144
-
145
- it("triggers a manual quote sync", async () => {
146
- const client = await createCocoClient(testClientOptions());
147
- try {
148
- await client.syncNpc();
149
- expect(npcState.syncCalls).toBe(1);
150
- } finally {
151
- await client.dispose?.();
152
- }
153
- });
154
-
155
- it("rejects NPC calls with a clear error when the plugin is disabled", async () => {
156
- const client = await createCocoClient({
157
- ...testClientOptions(),
158
- enableNpc: false,
159
- });
160
- try {
161
- await expect(client.getNpcAddress()).rejects.toThrow(
162
- "NPC plugin is not enabled",
163
- );
164
- await expect(client.syncNpc()).rejects.toThrow(
165
- "NPC plugin is not enabled",
166
- );
167
- } finally {
168
- await client.dispose?.();
169
- }
170
- });
171
- });
172
-
173
- describe("legacy cocod client NPC passthroughs", () => {
174
- type FetchImpl = (
175
- input: string | URL | Request,
176
- init?: RequestInit & { unix: string },
177
- ) => Promise<Response>;
178
-
179
- function legacyClient(handler: (path: string, body?: unknown) => Response) {
180
- const requests: Array<{ path: string; body?: unknown }> = [];
181
- const fetchImpl: FetchImpl = async (input, init) => {
182
- const path = String(input).replace("http://localhost", "");
183
- const body =
184
- typeof init?.body === "string" ? JSON.parse(init.body) : undefined;
185
- if (path !== "/ping") requests.push({ path, body });
186
- if (path === "/ping") {
187
- return new Response(JSON.stringify({ output: "pong" }));
188
- }
189
- return handler(path, body);
190
- };
191
- const client = createCocodClient({
192
- socketPath: "/tmp/routstrd-test-legacy-npc/cocod.sock",
193
- fetchImpl,
194
- });
195
- return { client, requests };
196
- }
197
-
198
- it("fetches and parses a username NPC address", async () => {
199
- const { client, requests } = legacyClient(
200
- () => new Response(JSON.stringify({ output: "alice@npubx.cash" })),
201
- );
202
- const info = await client.getNpcAddress();
203
- expect(info).toEqual({ address: "alice@npubx.cash", name: "alice" });
204
- expect(requests[0]?.path).toBe("/npc/address");
205
- });
206
-
207
- it("omits the name for npub fallback addresses", async () => {
208
- const { client } = legacyClient(
209
- () =>
210
- new Response(JSON.stringify({ output: "npub1abc123@npubx.cash" })),
211
- );
212
- const info = await client.getNpcAddress();
213
- expect(info).toEqual({ address: "npub1abc123@npubx.cash" });
214
- });
215
-
216
- it("posts username claims with the confirm flag", async () => {
217
- const { client, requests } = legacyClient(
218
- () => new Response(JSON.stringify({ output: { success: true } })),
219
- );
220
- const result = await client.setNpcUsername("bob", true);
221
- expect(result).toEqual({ success: true });
222
- expect(requests[0]).toEqual({
223
- path: "/npc/username",
224
- body: { username: "bob", confirm: true },
225
- });
226
- });
227
-
228
- it("preserves the 402 payment-required error from cocod", async () => {
229
- const { client } = legacyClient(
230
- () =>
231
- new Response(
232
- JSON.stringify({ error: "Payment required to set username" }),
233
- { status: 402 },
234
- ),
235
- );
236
- try {
237
- await client.setNpcUsername("bob");
238
- expect.unreachable("should have thrown");
239
- } catch (error) {
240
- expect(error).toBeInstanceOf(CocodHttpError);
241
- expect((error as InstanceType<typeof CocodHttpError>).status).toBe(402);
242
- expect((error as Error).message).toContain("Payment required");
243
- }
244
- });
245
-
246
- it("reports manual sync as unsupported", async () => {
247
- const { client } = legacyClient(
248
- () => new Response(JSON.stringify({ output: {} })),
249
- );
250
- await expect(client.syncNpc()).rejects.toThrow("not supported");
251
- });
252
- });