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,238 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "bun:test";
|
|
2
|
-
import type { ReceiveOperation } from "@cashu/coco-core";
|
|
3
|
-
import { Database } from "bun:sqlite";
|
|
4
|
-
import {
|
|
5
|
-
clearInterruptedReceiveReservations,
|
|
6
|
-
deleteReceiveTokenReservation,
|
|
7
|
-
groupExecutingReceives,
|
|
8
|
-
hashReceiveToken,
|
|
9
|
-
initReceiveDedupSchema,
|
|
10
|
-
listProcessingReceiveTokens,
|
|
11
|
-
reconcileExecutingReceives,
|
|
12
|
-
reserveReceiveToken,
|
|
13
|
-
updateReceiveToken,
|
|
14
|
-
type ReceiveReconcileSource,
|
|
15
|
-
} from "./receive-dedup";
|
|
16
|
-
|
|
17
|
-
function operation(
|
|
18
|
-
id: string,
|
|
19
|
-
secret: string,
|
|
20
|
-
outputB: string,
|
|
21
|
-
createdAt = 1,
|
|
22
|
-
): Extract<ReceiveOperation, { state: "executing" }> {
|
|
23
|
-
return {
|
|
24
|
-
id,
|
|
25
|
-
mintUrl: "https://mint.example",
|
|
26
|
-
unit: "sat",
|
|
27
|
-
amount: 1,
|
|
28
|
-
state: "executing",
|
|
29
|
-
createdAt,
|
|
30
|
-
updatedAt: createdAt,
|
|
31
|
-
fee: 0,
|
|
32
|
-
inputProofs: [{ amount: 1, id: "keyset", secret, C: "02" }],
|
|
33
|
-
outputData: {
|
|
34
|
-
keep: [
|
|
35
|
-
{
|
|
36
|
-
blindedMessage: { amount: 1, id: "keyset", B_: outputB },
|
|
37
|
-
blindingFactor: "1",
|
|
38
|
-
secret: "00",
|
|
39
|
-
},
|
|
40
|
-
],
|
|
41
|
-
send: [],
|
|
42
|
-
},
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function source(options: {
|
|
47
|
-
operations: ReceiveOperation[];
|
|
48
|
-
states?: string[];
|
|
49
|
-
restored?: string[];
|
|
50
|
-
saved?: string[];
|
|
51
|
-
}) {
|
|
52
|
-
const rolledBack: Array<{ id: string; reason: string }> = [];
|
|
53
|
-
let stateChecks = 0;
|
|
54
|
-
let restoreChecks = 0;
|
|
55
|
-
const reconcileSource: ReceiveReconcileSource = {
|
|
56
|
-
listExecuting: async () => options.operations,
|
|
57
|
-
checkProofStates: async (op) => {
|
|
58
|
-
stateChecks++;
|
|
59
|
-
return (options.states ?? op.inputProofs.map(() => "SPENT")).map((state) => ({ state }));
|
|
60
|
-
},
|
|
61
|
-
restoreOutputs: async (_mintUrl, outputs) => {
|
|
62
|
-
restoreChecks++;
|
|
63
|
-
const restored = new Set(options.restored ?? []);
|
|
64
|
-
return outputs.filter((output) => restored.has(output.B_));
|
|
65
|
-
},
|
|
66
|
-
hasSavedOutputs: async (op) => (options.saved ?? []).includes(op.id),
|
|
67
|
-
rollBack: async (op, reason) => {
|
|
68
|
-
rolledBack.push({ id: op.id, reason });
|
|
69
|
-
},
|
|
70
|
-
};
|
|
71
|
-
return {
|
|
72
|
-
source: reconcileSource,
|
|
73
|
-
rolledBack,
|
|
74
|
-
stateChecks: () => stateChecks,
|
|
75
|
-
restoreChecks: () => restoreChecks,
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
describe("receive token hashing and reservation", () => {
|
|
80
|
-
it("hashes the exact encoded token deterministically", () => {
|
|
81
|
-
expect(hashReceiveToken("cashuA")).toBe(hashReceiveToken("cashuA"));
|
|
82
|
-
expect(hashReceiveToken("cashuA")).not.toBe(hashReceiveToken("cashuB"));
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it("atomically reserves an exact token once", () => {
|
|
86
|
-
const database = new Database(":memory:");
|
|
87
|
-
initReceiveDedupSchema(database);
|
|
88
|
-
const first = reserveReceiveToken(database, "cashuA");
|
|
89
|
-
const second = reserveReceiveToken(database, "cashuA");
|
|
90
|
-
expect(first.acquired).toBe(true);
|
|
91
|
-
expect(second).toMatchObject({
|
|
92
|
-
acquired: false,
|
|
93
|
-
existing: { state: "processing", operationId: null },
|
|
94
|
-
});
|
|
95
|
-
database.close();
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it("persists successful status and releases retryable reservations", () => {
|
|
99
|
-
const database = new Database(":memory:");
|
|
100
|
-
initReceiveDedupSchema(database);
|
|
101
|
-
const reservation = reserveReceiveToken(database, "cashuA");
|
|
102
|
-
updateReceiveToken(database, reservation.tokenHash, {
|
|
103
|
-
state: "succeeded",
|
|
104
|
-
operationId: "receive-1",
|
|
105
|
-
});
|
|
106
|
-
expect(reserveReceiveToken(database, "cashuA").existing).toMatchObject({
|
|
107
|
-
state: "succeeded",
|
|
108
|
-
operationId: "receive-1",
|
|
109
|
-
});
|
|
110
|
-
deleteReceiveTokenReservation(database, reservation.tokenHash);
|
|
111
|
-
expect(reserveReceiveToken(database, "cashuA").acquired).toBe(true);
|
|
112
|
-
database.close();
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
it("clears only interrupted reservations without Coco operation ids", () => {
|
|
116
|
-
const database = new Database(":memory:");
|
|
117
|
-
initReceiveDedupSchema(database);
|
|
118
|
-
const orphan = reserveReceiveToken(database, "orphan");
|
|
119
|
-
const linked = reserveReceiveToken(database, "linked");
|
|
120
|
-
updateReceiveToken(database, linked.tokenHash, {
|
|
121
|
-
state: "processing",
|
|
122
|
-
operationId: "receive-1",
|
|
123
|
-
});
|
|
124
|
-
expect(clearInterruptedReceiveReservations(database)).toBe(1);
|
|
125
|
-
expect(listProcessingReceiveTokens(database)).toEqual([
|
|
126
|
-
expect.objectContaining({ tokenHash: linked.tokenHash, operationId: "receive-1" }),
|
|
127
|
-
]);
|
|
128
|
-
expect(reserveReceiveToken(database, "orphan").tokenHash).toBe(orphan.tokenHash);
|
|
129
|
-
database.close();
|
|
130
|
-
});
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
describe("legacy receive grouping", () => {
|
|
134
|
-
it("groups identical input proofs independent of operation id", () => {
|
|
135
|
-
const groups = groupExecutingReceives([
|
|
136
|
-
operation("one", "same", "B1"),
|
|
137
|
-
operation("two", "same", "B2"),
|
|
138
|
-
operation("three", "different", "B3"),
|
|
139
|
-
]);
|
|
140
|
-
expect(groups.map((group) => group.map((op) => op.id))).toEqual([
|
|
141
|
-
["one", "two"],
|
|
142
|
-
["three"],
|
|
143
|
-
]);
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
it("checks an unspent duplicate group once and retains one operation", async () => {
|
|
147
|
-
const mock = source({
|
|
148
|
-
operations: [operation("one", "same", "B1", 1), operation("two", "same", "B2", 2)],
|
|
149
|
-
states: ["UNSPENT"],
|
|
150
|
-
});
|
|
151
|
-
const result = await reconcileExecutingReceives(mock.source);
|
|
152
|
-
expect(mock.stateChecks()).toBe(1);
|
|
153
|
-
expect(mock.restoreChecks()).toBe(0);
|
|
154
|
-
expect(mock.rolledBack.map((entry) => entry.id)).toEqual(["two"]);
|
|
155
|
-
expect(result).toMatchObject({ rolledBack: 1, retained: 1, unresolved: 0 });
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
it("rolls back all spent duplicates when restore finds no outputs", async () => {
|
|
159
|
-
const mock = source({
|
|
160
|
-
operations: [operation("one", "same", "B1"), operation("two", "same", "B2")],
|
|
161
|
-
states: ["SPENT"],
|
|
162
|
-
restored: [],
|
|
163
|
-
});
|
|
164
|
-
const result = await reconcileExecutingReceives(mock.source);
|
|
165
|
-
expect(mock.stateChecks()).toBe(1);
|
|
166
|
-
expect(mock.restoreChecks()).toBe(1);
|
|
167
|
-
expect(mock.rolledBack.map((entry) => entry.id)).toEqual(["one", "two"]);
|
|
168
|
-
expect(result.rolledBack).toBe(2);
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
it("retains the operation whose output the mint can restore", async () => {
|
|
172
|
-
const mock = source({
|
|
173
|
-
operations: [operation("one", "same", "B1"), operation("two", "same", "B2")],
|
|
174
|
-
states: ["SPENT"],
|
|
175
|
-
restored: ["B2"],
|
|
176
|
-
});
|
|
177
|
-
const result = await reconcileExecutingReceives(mock.source);
|
|
178
|
-
expect(mock.rolledBack.map((entry) => entry.id)).toEqual(["one"]);
|
|
179
|
-
expect(result).toMatchObject({ rolledBack: 1, retained: 1 });
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
it("leaves a group untouched when the mint returns incomplete states", async () => {
|
|
183
|
-
const first = operation("one", "same", "B1");
|
|
184
|
-
first.inputProofs.push({ amount: 1, id: "keyset", secret: "other", C: "03" });
|
|
185
|
-
const second = operation("two", "same", "B2");
|
|
186
|
-
second.inputProofs.push({ amount: 1, id: "keyset", secret: "other", C: "03" });
|
|
187
|
-
const mock = source({
|
|
188
|
-
operations: [first, second],
|
|
189
|
-
states: ["SPENT"],
|
|
190
|
-
});
|
|
191
|
-
const result = await reconcileExecutingReceives(mock.source);
|
|
192
|
-
expect(mock.rolledBack).toEqual([]);
|
|
193
|
-
expect(mock.restoreChecks()).toBe(0);
|
|
194
|
-
expect(result.unresolved).toBe(2);
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
it("leaves a group untouched when proof states are mixed", async () => {
|
|
198
|
-
const first = operation("one", "same", "B1");
|
|
199
|
-
first.inputProofs.push({ amount: 1, id: "keyset", secret: "other", C: "03" });
|
|
200
|
-
const second = operation("two", "same", "B2");
|
|
201
|
-
second.inputProofs.push({ amount: 1, id: "keyset", secret: "other", C: "03" });
|
|
202
|
-
const mock = source({
|
|
203
|
-
operations: [first, second],
|
|
204
|
-
states: ["SPENT", "UNSPENT"],
|
|
205
|
-
});
|
|
206
|
-
const result = await reconcileExecutingReceives(mock.source);
|
|
207
|
-
expect(mock.rolledBack).toEqual([]);
|
|
208
|
-
expect(result.unresolved).toBe(2);
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
it("scales mint checks with unique input groups rather than row count", async () => {
|
|
212
|
-
const operations: ReceiveOperation[] = [];
|
|
213
|
-
for (let group = 0; group < 47; group++) {
|
|
214
|
-
const repeats = group < 44 ? 41 : 40; // 1,924 rows total
|
|
215
|
-
for (let index = 0; index < repeats; index++) {
|
|
216
|
-
operations.push(
|
|
217
|
-
operation(
|
|
218
|
-
`operation-${group}-${index}`,
|
|
219
|
-
`secret-${group}`,
|
|
220
|
-
`B-${group}-${index}`,
|
|
221
|
-
index,
|
|
222
|
-
),
|
|
223
|
-
);
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
expect(operations).toHaveLength(1924);
|
|
227
|
-
const mock = source({ operations, states: ["SPENT"], restored: [] });
|
|
228
|
-
const result = await reconcileExecutingReceives(mock.source);
|
|
229
|
-
expect(mock.stateChecks()).toBe(47);
|
|
230
|
-
expect(mock.restoreChecks()).toBe(47);
|
|
231
|
-
expect(result).toMatchObject({
|
|
232
|
-
executing: 1924,
|
|
233
|
-
uniqueGroups: 47,
|
|
234
|
-
rolledBack: 1924,
|
|
235
|
-
unresolved: 0,
|
|
236
|
-
});
|
|
237
|
-
});
|
|
238
|
-
});
|