routstrd 0.4.7 → 0.4.9
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 +96 -6
- package/SKILL.md +341 -0
- package/dist/daemon/index.js +31092 -31824
- package/dist/index.js +162637 -14155
- package/package.json +9 -4
- package/src/cli.ts +279 -110
- package/src/daemon/args.ts +4 -5
- package/src/daemon/http/index.ts +37 -2
- package/src/daemon/index.ts +94 -33
- package/src/daemon/wallet/auto-refill.ts +1 -1
- package/src/daemon/wallet/coco-client.ts +205 -3
- package/src/daemon/wallet/cocod-client.ts +19 -3
- package/src/daemon/wallet/index.ts +1 -1
- package/src/index.ts +9 -1
- package/src/runtime.ts +55 -0
- package/src/start-daemon.ts +4 -8
- package/src/utils/clients.ts +74 -1
- package/src/utils/config.ts +14 -0
- package/src/utils/daemon-stop.ts +125 -0
- package/src/utils/standalone-update.ts +214 -0
- package/src/utils/update-checker.ts +81 -18
- package/src/version.ts +3 -0
- package/src/cli.test.ts +0 -192
- package/src/daemon/fatal-error.test.ts +0 -104
- package/src/daemon/http/usage-summary.test.ts +0 -272
- package/src/daemon/models.test.ts +0 -81
- package/src/daemon/wallet/cleanup.test.ts +0 -168
- package/src/daemon/wallet/coco-client.npc.test.ts +0 -252
- package/src/daemon/wallet/coco-client.test.ts +0 -669
- package/src/daemon/wallet/diagnostics.test.ts +0 -378
- package/src/daemon/wallet/fixtures/cocod-0.0.24-wallet.db.gz +0 -0
- package/src/daemon/wallet/migration.test.ts +0 -171
- package/src/daemon/wallet/receive-dedup.test.ts +0 -238
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "bun:test";
|
|
2
|
-
import { selectCleanupOperations } from "./cleanup";
|
|
3
|
-
|
|
4
|
-
const NOW_MS = 1_800_000_000_000;
|
|
5
|
-
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
6
|
-
|
|
7
|
-
function mint(overrides: Record<string, unknown>) {
|
|
8
|
-
return {
|
|
9
|
-
id: "mint-1",
|
|
10
|
-
state: "pending",
|
|
11
|
-
expiry: NOW_MS / 1000 - 1000, // expired
|
|
12
|
-
updatedAt: NOW_MS - 2 * DAY_MS,
|
|
13
|
-
lastObservedRemoteState: undefined,
|
|
14
|
-
...overrides,
|
|
15
|
-
};
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function send(overrides: Record<string, unknown>) {
|
|
19
|
-
return {
|
|
20
|
-
id: "send-1",
|
|
21
|
-
state: "pending",
|
|
22
|
-
updatedAt: NOW_MS - 2 * DAY_MS,
|
|
23
|
-
...overrides,
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function melt(overrides: Record<string, unknown>) {
|
|
28
|
-
return {
|
|
29
|
-
id: "melt-1",
|
|
30
|
-
state: "prepared",
|
|
31
|
-
updatedAt: NOW_MS - 2 * DAY_MS,
|
|
32
|
-
...overrides,
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
describe("selectCleanupOperations", () => {
|
|
37
|
-
it("selects expired pending mint quotes that are old enough", () => {
|
|
38
|
-
const result = selectCleanupOperations({
|
|
39
|
-
mints: [mint({ id: "a" })],
|
|
40
|
-
sends: [],
|
|
41
|
-
melts: [],
|
|
42
|
-
nowMs: NOW_MS,
|
|
43
|
-
minAgeMs: DAY_MS,
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
expect(result.mintsToFail.map((op) => op.id)).toEqual(["a"]);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it("ignores mint quotes that have not expired", () => {
|
|
50
|
-
const result = selectCleanupOperations({
|
|
51
|
-
mints: [mint({ id: "a", expiry: NOW_MS / 1000 + 1000 })],
|
|
52
|
-
sends: [],
|
|
53
|
-
melts: [],
|
|
54
|
-
nowMs: NOW_MS,
|
|
55
|
-
minAgeMs: DAY_MS,
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
expect(result.mintsToFail).toEqual([]);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it("ignores expired mint quotes already observed as PAID", () => {
|
|
62
|
-
const result = selectCleanupOperations({
|
|
63
|
-
mints: [mint({ id: "a", lastObservedRemoteState: "PAID" })],
|
|
64
|
-
sends: [],
|
|
65
|
-
melts: [],
|
|
66
|
-
nowMs: NOW_MS,
|
|
67
|
-
minAgeMs: DAY_MS,
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
expect(result.mintsToFail).toEqual([]);
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it("ignores expired mint quotes already observed as ISSUED", () => {
|
|
74
|
-
const result = selectCleanupOperations({
|
|
75
|
-
mints: [mint({ id: "a", lastObservedRemoteState: "ISSUED" })],
|
|
76
|
-
sends: [],
|
|
77
|
-
melts: [],
|
|
78
|
-
nowMs: NOW_MS,
|
|
79
|
-
minAgeMs: DAY_MS,
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
expect(result.mintsToFail).toEqual([]);
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it("ignores mint quotes without an expiry", () => {
|
|
86
|
-
const result = selectCleanupOperations({
|
|
87
|
-
mints: [mint({ id: "a", expiry: 0 })],
|
|
88
|
-
sends: [],
|
|
89
|
-
melts: [],
|
|
90
|
-
nowMs: NOW_MS,
|
|
91
|
-
minAgeMs: DAY_MS,
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
expect(result.mintsToFail).toEqual([]);
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
it("selects expired mint quotes even when the watcher recently touched them", () => {
|
|
98
|
-
const result = selectCleanupOperations({
|
|
99
|
-
mints: [mint({ id: "a", updatedAt: NOW_MS - 60_000 })],
|
|
100
|
-
sends: [],
|
|
101
|
-
melts: [],
|
|
102
|
-
nowMs: NOW_MS,
|
|
103
|
-
minAgeMs: DAY_MS,
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
expect(result.mintsToFail.map((op) => op.id)).toEqual(["a"]);
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
it("selects stale pending sends for reclaim", () => {
|
|
110
|
-
const result = selectCleanupOperations({
|
|
111
|
-
mints: [],
|
|
112
|
-
sends: [send({ id: "s" })],
|
|
113
|
-
melts: [],
|
|
114
|
-
nowMs: NOW_MS,
|
|
115
|
-
minAgeMs: DAY_MS,
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
expect(result.sendsToReclaim.map((op) => op.id)).toEqual(["s"]);
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it("ignores recent pending sends", () => {
|
|
122
|
-
const result = selectCleanupOperations({
|
|
123
|
-
mints: [],
|
|
124
|
-
sends: [send({ id: "s", updatedAt: NOW_MS - 60_000 })],
|
|
125
|
-
melts: [],
|
|
126
|
-
nowMs: NOW_MS,
|
|
127
|
-
minAgeMs: DAY_MS,
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
expect(result.sendsToReclaim).toEqual([]);
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
it("ignores non-pending sends even when old", () => {
|
|
134
|
-
const result = selectCleanupOperations({
|
|
135
|
-
mints: [],
|
|
136
|
-
sends: [send({ id: "s", state: "executing" })],
|
|
137
|
-
melts: [],
|
|
138
|
-
nowMs: NOW_MS,
|
|
139
|
-
minAgeMs: DAY_MS,
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
expect(result.sendsToReclaim).toEqual([]);
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
it("selects stale prepared melts for cancellation", () => {
|
|
146
|
-
const result = selectCleanupOperations({
|
|
147
|
-
mints: [],
|
|
148
|
-
sends: [],
|
|
149
|
-
melts: [melt({ id: "m" })],
|
|
150
|
-
nowMs: NOW_MS,
|
|
151
|
-
minAgeMs: DAY_MS,
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
expect(result.meltsToCancel.map((op) => op.id)).toEqual(["m"]);
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
it("ignores non-prepared melts", () => {
|
|
158
|
-
const result = selectCleanupOperations({
|
|
159
|
-
mints: [],
|
|
160
|
-
sends: [],
|
|
161
|
-
melts: [melt({ id: "m", state: "pending" })],
|
|
162
|
-
nowMs: NOW_MS,
|
|
163
|
-
minAgeMs: DAY_MS,
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
expect(result.meltsToCancel).toEqual([]);
|
|
167
|
-
});
|
|
168
|
-
});
|
|
@@ -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
|
-
});
|