@openclaw/nostr 2026.3.13 → 2026.5.2-beta.1
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 +6 -0
- package/api.ts +10 -0
- package/channel-plugin-api.ts +1 -0
- package/index.ts +60 -36
- package/openclaw.plugin.json +190 -1
- package/package.json +41 -9
- package/runtime-api.ts +6 -0
- package/setup-api.ts +1 -0
- package/setup-entry.ts +9 -0
- package/setup-plugin-api.ts +3 -0
- package/src/channel-api.ts +15 -0
- package/src/channel.inbound.test.ts +176 -0
- package/src/channel.outbound.test.ts +89 -49
- package/src/channel.setup.ts +231 -0
- package/src/channel.test.ts +439 -71
- package/src/channel.ts +147 -283
- package/src/config-schema.ts +18 -12
- package/src/default-relays.ts +1 -0
- package/src/gateway.ts +302 -0
- package/src/inbound-direct-dm-runtime.ts +1 -0
- package/src/metrics.ts +6 -6
- package/src/nostr-bus.fuzz.test.ts +74 -247
- package/src/nostr-bus.inbound.test.ts +526 -0
- package/src/nostr-bus.integration.test.ts +88 -64
- package/src/nostr-bus.test.ts +22 -31
- package/src/nostr-bus.ts +206 -136
- package/src/nostr-key-utils.ts +94 -0
- package/src/nostr-profile-core.ts +134 -0
- package/src/nostr-profile-http-runtime.ts +6 -0
- package/src/nostr-profile-http.test.ts +276 -167
- package/src/nostr-profile-http.ts +51 -36
- package/src/nostr-profile-import.ts +3 -3
- package/src/nostr-profile-url-safety.ts +21 -0
- package/src/nostr-profile.fuzz.test.ts +7 -57
- package/src/nostr-profile.test.ts +16 -14
- package/src/nostr-profile.ts +13 -146
- package/src/nostr-state-store.test.ts +106 -2
- package/src/nostr-state-store.ts +46 -49
- package/src/runtime.ts +6 -3
- package/src/seen-tracker.ts +1 -1
- package/src/session-route.ts +25 -0
- package/src/setup-surface.ts +265 -0
- package/src/test-fixtures.ts +45 -0
- package/src/types.ts +26 -25
- package/test-api.ts +1 -0
- package/tsconfig.json +16 -0
- package/CHANGELOG.md +0 -116
- package/src/types.test.ts +0 -175
|
@@ -1,6 +1,33 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from "vitest";
|
|
2
2
|
import { createMetrics, createNoopMetrics, type MetricEvent } from "./metrics.js";
|
|
3
3
|
import { createSeenTracker } from "./seen-tracker.js";
|
|
4
|
+
import { TEST_RELAY_URL } from "./test-fixtures.js";
|
|
5
|
+
|
|
6
|
+
const TEST_RELAY_URL_1 = "wss://relay1.com";
|
|
7
|
+
const TEST_RELAY_URL_2 = "wss://relay2.com";
|
|
8
|
+
const TEST_RELAY_URL_PRIMARY = "wss://relay.com";
|
|
9
|
+
const TEST_RELAY_URL_GOOD = "wss://good-relay.com";
|
|
10
|
+
const TEST_RELAY_URL_BAD = "wss://bad-relay.com";
|
|
11
|
+
|
|
12
|
+
function createTracker(overrides?: Partial<Parameters<typeof createSeenTracker>[0]>) {
|
|
13
|
+
return createSeenTracker({
|
|
14
|
+
maxEntries: 100,
|
|
15
|
+
ttlMs: 60000,
|
|
16
|
+
...overrides,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function createCollectingMetrics() {
|
|
21
|
+
const events: MetricEvent[] = [];
|
|
22
|
+
return {
|
|
23
|
+
events,
|
|
24
|
+
metrics: createMetrics((event) => events.push(event)),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function createPlainMetrics() {
|
|
29
|
+
return createMetrics();
|
|
30
|
+
}
|
|
4
31
|
|
|
5
32
|
// ============================================================================
|
|
6
33
|
// Seen Tracker Integration Tests
|
|
@@ -9,7 +36,7 @@ import { createSeenTracker } from "./seen-tracker.js";
|
|
|
9
36
|
describe("SeenTracker", () => {
|
|
10
37
|
describe("basic operations", () => {
|
|
11
38
|
it("tracks seen IDs", () => {
|
|
12
|
-
const tracker =
|
|
39
|
+
const tracker = createTracker();
|
|
13
40
|
|
|
14
41
|
// First check returns false and adds
|
|
15
42
|
expect(tracker.has("id1")).toBe(false);
|
|
@@ -20,7 +47,7 @@ describe("SeenTracker", () => {
|
|
|
20
47
|
});
|
|
21
48
|
|
|
22
49
|
it("peek does not add", () => {
|
|
23
|
-
const tracker =
|
|
50
|
+
const tracker = createTracker();
|
|
24
51
|
|
|
25
52
|
expect(tracker.peek("id1")).toBe(false);
|
|
26
53
|
expect(tracker.peek("id1")).toBe(false); // Still false
|
|
@@ -32,7 +59,7 @@ describe("SeenTracker", () => {
|
|
|
32
59
|
});
|
|
33
60
|
|
|
34
61
|
it("delete removes entries", () => {
|
|
35
|
-
const tracker =
|
|
62
|
+
const tracker = createTracker();
|
|
36
63
|
|
|
37
64
|
tracker.add("id1");
|
|
38
65
|
expect(tracker.peek("id1")).toBe(true);
|
|
@@ -44,7 +71,7 @@ describe("SeenTracker", () => {
|
|
|
44
71
|
});
|
|
45
72
|
|
|
46
73
|
it("clear removes all entries", () => {
|
|
47
|
-
const tracker =
|
|
74
|
+
const tracker = createTracker();
|
|
48
75
|
|
|
49
76
|
tracker.add("id1");
|
|
50
77
|
tracker.add("id2");
|
|
@@ -59,7 +86,7 @@ describe("SeenTracker", () => {
|
|
|
59
86
|
});
|
|
60
87
|
|
|
61
88
|
it("seed pre-populates entries", () => {
|
|
62
|
-
const tracker =
|
|
89
|
+
const tracker = createTracker();
|
|
63
90
|
|
|
64
91
|
tracker.seed(["id1", "id2", "id3"]);
|
|
65
92
|
expect(tracker.size()).toBe(3);
|
|
@@ -73,7 +100,7 @@ describe("SeenTracker", () => {
|
|
|
73
100
|
|
|
74
101
|
describe("LRU eviction", () => {
|
|
75
102
|
it("evicts least recently used when at capacity", () => {
|
|
76
|
-
const tracker =
|
|
103
|
+
const tracker = createTracker({ maxEntries: 3 });
|
|
77
104
|
|
|
78
105
|
tracker.add("id1");
|
|
79
106
|
tracker.add("id2");
|
|
@@ -92,7 +119,7 @@ describe("SeenTracker", () => {
|
|
|
92
119
|
});
|
|
93
120
|
|
|
94
121
|
it("accessing an entry moves it to front (prevents eviction)", () => {
|
|
95
|
-
const tracker =
|
|
122
|
+
const tracker = createTracker({ maxEntries: 3 });
|
|
96
123
|
|
|
97
124
|
tracker.add("id1");
|
|
98
125
|
tracker.add("id2");
|
|
@@ -112,7 +139,7 @@ describe("SeenTracker", () => {
|
|
|
112
139
|
});
|
|
113
140
|
|
|
114
141
|
it("handles capacity of 1", () => {
|
|
115
|
-
const tracker =
|
|
142
|
+
const tracker = createTracker({ maxEntries: 1 });
|
|
116
143
|
|
|
117
144
|
tracker.add("id1");
|
|
118
145
|
expect(tracker.peek("id1")).toBe(true);
|
|
@@ -125,7 +152,7 @@ describe("SeenTracker", () => {
|
|
|
125
152
|
});
|
|
126
153
|
|
|
127
154
|
it("seed respects maxEntries", () => {
|
|
128
|
-
const tracker =
|
|
155
|
+
const tracker = createTracker({ maxEntries: 2 });
|
|
129
156
|
|
|
130
157
|
tracker.seed(["id1", "id2", "id3", "id4"]);
|
|
131
158
|
expect(tracker.size()).toBe(2);
|
|
@@ -142,7 +169,7 @@ describe("SeenTracker", () => {
|
|
|
142
169
|
it("expires entries after TTL", async () => {
|
|
143
170
|
vi.useFakeTimers();
|
|
144
171
|
|
|
145
|
-
const tracker =
|
|
172
|
+
const tracker = createTracker({
|
|
146
173
|
maxEntries: 100,
|
|
147
174
|
ttlMs: 100,
|
|
148
175
|
pruneIntervalMs: 50,
|
|
@@ -164,7 +191,7 @@ describe("SeenTracker", () => {
|
|
|
164
191
|
it("has() refreshes TTL", async () => {
|
|
165
192
|
vi.useFakeTimers();
|
|
166
193
|
|
|
167
|
-
const tracker =
|
|
194
|
+
const tracker = createTracker({
|
|
168
195
|
maxEntries: 100,
|
|
169
196
|
ttlMs: 100,
|
|
170
197
|
pruneIntervalMs: 50,
|
|
@@ -197,8 +224,7 @@ describe("SeenTracker", () => {
|
|
|
197
224
|
describe("Metrics", () => {
|
|
198
225
|
describe("createMetrics", () => {
|
|
199
226
|
it("emits metric events to callback", () => {
|
|
200
|
-
const events
|
|
201
|
-
const metrics = createMetrics((event) => events.push(event));
|
|
227
|
+
const { events, metrics } = createCollectingMetrics();
|
|
202
228
|
|
|
203
229
|
metrics.emit("event.received");
|
|
204
230
|
metrics.emit("event.processed");
|
|
@@ -211,16 +237,15 @@ describe("Metrics", () => {
|
|
|
211
237
|
});
|
|
212
238
|
|
|
213
239
|
it("includes labels in metric events", () => {
|
|
214
|
-
const events
|
|
215
|
-
const metrics = createMetrics((event) => events.push(event));
|
|
240
|
+
const { events, metrics } = createCollectingMetrics();
|
|
216
241
|
|
|
217
|
-
metrics.emit("relay.connect", 1, { relay:
|
|
242
|
+
metrics.emit("relay.connect", 1, { relay: TEST_RELAY_URL });
|
|
218
243
|
|
|
219
|
-
expect(events[0].labels).toEqual({ relay:
|
|
244
|
+
expect(events[0].labels).toEqual({ relay: TEST_RELAY_URL });
|
|
220
245
|
});
|
|
221
246
|
|
|
222
247
|
it("accumulates counters in snapshot", () => {
|
|
223
|
-
const metrics =
|
|
248
|
+
const metrics = createPlainMetrics();
|
|
224
249
|
|
|
225
250
|
metrics.emit("event.received");
|
|
226
251
|
metrics.emit("event.received");
|
|
@@ -236,39 +261,39 @@ describe("Metrics", () => {
|
|
|
236
261
|
});
|
|
237
262
|
|
|
238
263
|
it("tracks per-relay stats", () => {
|
|
239
|
-
const metrics =
|
|
264
|
+
const metrics = createPlainMetrics();
|
|
240
265
|
|
|
241
|
-
metrics.emit("relay.connect", 1, { relay:
|
|
242
|
-
metrics.emit("relay.connect", 1, { relay:
|
|
243
|
-
metrics.emit("relay.error", 1, { relay:
|
|
244
|
-
metrics.emit("relay.error", 1, { relay:
|
|
266
|
+
metrics.emit("relay.connect", 1, { relay: TEST_RELAY_URL_1 });
|
|
267
|
+
metrics.emit("relay.connect", 1, { relay: TEST_RELAY_URL_2 });
|
|
268
|
+
metrics.emit("relay.error", 1, { relay: TEST_RELAY_URL_1 });
|
|
269
|
+
metrics.emit("relay.error", 1, { relay: TEST_RELAY_URL_1 });
|
|
245
270
|
|
|
246
271
|
const snapshot = metrics.getSnapshot();
|
|
247
|
-
expect(snapshot.relays[
|
|
248
|
-
expect(snapshot.relays[
|
|
249
|
-
expect(snapshot.relays[
|
|
250
|
-
expect(snapshot.relays[
|
|
251
|
-
expect(snapshot.relays[
|
|
272
|
+
expect(snapshot.relays[TEST_RELAY_URL_1]).toBeDefined();
|
|
273
|
+
expect(snapshot.relays[TEST_RELAY_URL_1].connects).toBe(1);
|
|
274
|
+
expect(snapshot.relays[TEST_RELAY_URL_1].errors).toBe(2);
|
|
275
|
+
expect(snapshot.relays[TEST_RELAY_URL_2].connects).toBe(1);
|
|
276
|
+
expect(snapshot.relays[TEST_RELAY_URL_2].errors).toBe(0);
|
|
252
277
|
});
|
|
253
278
|
|
|
254
279
|
it("tracks circuit breaker state changes", () => {
|
|
255
|
-
const metrics =
|
|
280
|
+
const metrics = createPlainMetrics();
|
|
256
281
|
|
|
257
|
-
metrics.emit("relay.circuit_breaker.open", 1, { relay:
|
|
282
|
+
metrics.emit("relay.circuit_breaker.open", 1, { relay: TEST_RELAY_URL_PRIMARY });
|
|
258
283
|
|
|
259
284
|
let snapshot = metrics.getSnapshot();
|
|
260
|
-
expect(snapshot.relays[
|
|
261
|
-
expect(snapshot.relays[
|
|
285
|
+
expect(snapshot.relays[TEST_RELAY_URL_PRIMARY].circuitBreakerState).toBe("open");
|
|
286
|
+
expect(snapshot.relays[TEST_RELAY_URL_PRIMARY].circuitBreakerOpens).toBe(1);
|
|
262
287
|
|
|
263
|
-
metrics.emit("relay.circuit_breaker.close", 1, { relay:
|
|
288
|
+
metrics.emit("relay.circuit_breaker.close", 1, { relay: TEST_RELAY_URL_PRIMARY });
|
|
264
289
|
|
|
265
290
|
snapshot = metrics.getSnapshot();
|
|
266
|
-
expect(snapshot.relays[
|
|
267
|
-
expect(snapshot.relays[
|
|
291
|
+
expect(snapshot.relays[TEST_RELAY_URL_PRIMARY].circuitBreakerState).toBe("closed");
|
|
292
|
+
expect(snapshot.relays[TEST_RELAY_URL_PRIMARY].circuitBreakerCloses).toBe(1);
|
|
268
293
|
});
|
|
269
294
|
|
|
270
295
|
it("tracks all rejection reasons", () => {
|
|
271
|
-
const metrics =
|
|
296
|
+
const metrics = createPlainMetrics();
|
|
272
297
|
|
|
273
298
|
metrics.emit("event.rejected.invalid_shape");
|
|
274
299
|
metrics.emit("event.rejected.wrong_kind");
|
|
@@ -295,17 +320,17 @@ describe("Metrics", () => {
|
|
|
295
320
|
});
|
|
296
321
|
|
|
297
322
|
it("tracks relay message types", () => {
|
|
298
|
-
const metrics =
|
|
323
|
+
const metrics = createPlainMetrics();
|
|
299
324
|
|
|
300
|
-
metrics.emit("relay.message.event", 1, { relay:
|
|
301
|
-
metrics.emit("relay.message.eose", 1, { relay:
|
|
302
|
-
metrics.emit("relay.message.closed", 1, { relay:
|
|
303
|
-
metrics.emit("relay.message.notice", 1, { relay:
|
|
304
|
-
metrics.emit("relay.message.ok", 1, { relay:
|
|
305
|
-
metrics.emit("relay.message.auth", 1, { relay:
|
|
325
|
+
metrics.emit("relay.message.event", 1, { relay: TEST_RELAY_URL_PRIMARY });
|
|
326
|
+
metrics.emit("relay.message.eose", 1, { relay: TEST_RELAY_URL_PRIMARY });
|
|
327
|
+
metrics.emit("relay.message.closed", 1, { relay: TEST_RELAY_URL_PRIMARY });
|
|
328
|
+
metrics.emit("relay.message.notice", 1, { relay: TEST_RELAY_URL_PRIMARY });
|
|
329
|
+
metrics.emit("relay.message.ok", 1, { relay: TEST_RELAY_URL_PRIMARY });
|
|
330
|
+
metrics.emit("relay.message.auth", 1, { relay: TEST_RELAY_URL_PRIMARY });
|
|
306
331
|
|
|
307
332
|
const snapshot = metrics.getSnapshot();
|
|
308
|
-
const relay = snapshot.relays[
|
|
333
|
+
const relay = snapshot.relays[TEST_RELAY_URL_PRIMARY];
|
|
309
334
|
expect(relay.messagesReceived.event).toBe(1);
|
|
310
335
|
expect(relay.messagesReceived.eose).toBe(1);
|
|
311
336
|
expect(relay.messagesReceived.closed).toBe(1);
|
|
@@ -315,7 +340,7 @@ describe("Metrics", () => {
|
|
|
315
340
|
});
|
|
316
341
|
|
|
317
342
|
it("tracks decrypt success/failure", () => {
|
|
318
|
-
const metrics =
|
|
343
|
+
const metrics = createPlainMetrics();
|
|
319
344
|
|
|
320
345
|
metrics.emit("decrypt.success");
|
|
321
346
|
metrics.emit("decrypt.success");
|
|
@@ -327,7 +352,7 @@ describe("Metrics", () => {
|
|
|
327
352
|
});
|
|
328
353
|
|
|
329
354
|
it("tracks memory gauges (replaces rather than accumulates)", () => {
|
|
330
|
-
const metrics =
|
|
355
|
+
const metrics = createPlainMetrics();
|
|
331
356
|
|
|
332
357
|
metrics.emit("memory.seen_tracker_size", 100);
|
|
333
358
|
metrics.emit("memory.seen_tracker_size", 150);
|
|
@@ -338,11 +363,11 @@ describe("Metrics", () => {
|
|
|
338
363
|
});
|
|
339
364
|
|
|
340
365
|
it("reset clears all counters", () => {
|
|
341
|
-
const metrics =
|
|
366
|
+
const metrics = createPlainMetrics();
|
|
342
367
|
|
|
343
368
|
metrics.emit("event.received");
|
|
344
369
|
metrics.emit("event.processed");
|
|
345
|
-
metrics.emit("relay.connect", 1, { relay:
|
|
370
|
+
metrics.emit("relay.connect", 1, { relay: TEST_RELAY_URL_PRIMARY });
|
|
346
371
|
|
|
347
372
|
metrics.reset();
|
|
348
373
|
|
|
@@ -359,7 +384,7 @@ describe("Metrics", () => {
|
|
|
359
384
|
|
|
360
385
|
expect(() => {
|
|
361
386
|
metrics.emit("event.received");
|
|
362
|
-
metrics.emit("relay.connect", 1, { relay:
|
|
387
|
+
metrics.emit("relay.connect", 1, { relay: TEST_RELAY_URL_PRIMARY });
|
|
363
388
|
}).not.toThrow();
|
|
364
389
|
});
|
|
365
390
|
|
|
@@ -380,18 +405,17 @@ describe("Metrics", () => {
|
|
|
380
405
|
describe("Circuit Breaker Behavior", () => {
|
|
381
406
|
// Test the circuit breaker logic through metrics emissions
|
|
382
407
|
it("emits circuit breaker metrics in correct sequence", () => {
|
|
383
|
-
const events
|
|
384
|
-
const metrics = createMetrics((event) => events.push(event));
|
|
408
|
+
const { events, metrics } = createCollectingMetrics();
|
|
385
409
|
|
|
386
410
|
// Simulate 5 failures -> open
|
|
387
411
|
for (let i = 0; i < 5; i++) {
|
|
388
|
-
metrics.emit("relay.error", 1, { relay:
|
|
412
|
+
metrics.emit("relay.error", 1, { relay: TEST_RELAY_URL_PRIMARY });
|
|
389
413
|
}
|
|
390
|
-
metrics.emit("relay.circuit_breaker.open", 1, { relay:
|
|
414
|
+
metrics.emit("relay.circuit_breaker.open", 1, { relay: TEST_RELAY_URL_PRIMARY });
|
|
391
415
|
|
|
392
416
|
// Simulate recovery
|
|
393
|
-
metrics.emit("relay.circuit_breaker.half_open", 1, { relay:
|
|
394
|
-
metrics.emit("relay.circuit_breaker.close", 1, { relay:
|
|
417
|
+
metrics.emit("relay.circuit_breaker.half_open", 1, { relay: TEST_RELAY_URL_PRIMARY });
|
|
418
|
+
metrics.emit("relay.circuit_breaker.close", 1, { relay: TEST_RELAY_URL_PRIMARY });
|
|
395
419
|
|
|
396
420
|
const cbEvents = events.filter((e) => e.name.startsWith("relay.circuit_breaker"));
|
|
397
421
|
expect(cbEvents).toHaveLength(3);
|
|
@@ -407,19 +431,19 @@ describe("Circuit Breaker Behavior", () => {
|
|
|
407
431
|
|
|
408
432
|
describe("Health Scoring", () => {
|
|
409
433
|
it("metrics track relay errors for health scoring", () => {
|
|
410
|
-
const metrics =
|
|
434
|
+
const metrics = createPlainMetrics();
|
|
411
435
|
|
|
412
436
|
// Simulate mixed success/failure pattern
|
|
413
|
-
metrics.emit("relay.connect", 1, { relay:
|
|
414
|
-
metrics.emit("relay.connect", 1, { relay:
|
|
437
|
+
metrics.emit("relay.connect", 1, { relay: TEST_RELAY_URL_GOOD });
|
|
438
|
+
metrics.emit("relay.connect", 1, { relay: TEST_RELAY_URL_BAD });
|
|
415
439
|
|
|
416
|
-
metrics.emit("relay.error", 1, { relay:
|
|
417
|
-
metrics.emit("relay.error", 1, { relay:
|
|
418
|
-
metrics.emit("relay.error", 1, { relay:
|
|
440
|
+
metrics.emit("relay.error", 1, { relay: TEST_RELAY_URL_BAD });
|
|
441
|
+
metrics.emit("relay.error", 1, { relay: TEST_RELAY_URL_BAD });
|
|
442
|
+
metrics.emit("relay.error", 1, { relay: TEST_RELAY_URL_BAD });
|
|
419
443
|
|
|
420
444
|
const snapshot = metrics.getSnapshot();
|
|
421
|
-
expect(snapshot.relays[
|
|
422
|
-
expect(snapshot.relays[
|
|
445
|
+
expect(snapshot.relays[TEST_RELAY_URL_GOOD].errors).toBe(0);
|
|
446
|
+
expect(snapshot.relays[TEST_RELAY_URL_BAD].errors).toBe(3);
|
|
423
447
|
});
|
|
424
448
|
});
|
|
425
449
|
|
|
@@ -435,7 +459,7 @@ describe("Reconnect Backoff", () => {
|
|
|
435
459
|
const JITTER = 0.3;
|
|
436
460
|
|
|
437
461
|
for (let attempt = 0; attempt < 10; attempt++) {
|
|
438
|
-
const exponential = BASE *
|
|
462
|
+
const exponential = BASE * 2 ** attempt;
|
|
439
463
|
const capped = Math.min(exponential, MAX);
|
|
440
464
|
const minDelay = capped * (1 - JITTER);
|
|
441
465
|
const maxDelay = capped * (1 + JITTER);
|
package/src/nostr-bus.test.ts
CHANGED
|
@@ -5,27 +5,24 @@ import {
|
|
|
5
5
|
isValidPubkey,
|
|
6
6
|
normalizePubkey,
|
|
7
7
|
pubkeyToNpub,
|
|
8
|
-
} from "./nostr-
|
|
9
|
-
|
|
10
|
-
// Test private key (DO NOT use in production - this is a known test key)
|
|
11
|
-
const TEST_HEX_KEY = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
|
|
12
|
-
const TEST_NSEC = "nsec1qypqxpq9qtpqscx7peytzfwtdjmcv0mrz5rjpej8vjppfkqfqy8skqfv3l";
|
|
8
|
+
} from "./nostr-key-utils.js";
|
|
9
|
+
import { TEST_HEX_PRIVATE_KEY, TEST_NSEC } from "./test-fixtures.js";
|
|
13
10
|
|
|
14
11
|
describe("validatePrivateKey", () => {
|
|
15
12
|
describe("hex format", () => {
|
|
16
13
|
it("accepts valid 64-char hex key", () => {
|
|
17
|
-
const result = validatePrivateKey(
|
|
14
|
+
const result = validatePrivateKey(TEST_HEX_PRIVATE_KEY);
|
|
18
15
|
expect(result).toBeInstanceOf(Uint8Array);
|
|
19
16
|
expect(result.length).toBe(32);
|
|
20
17
|
});
|
|
21
18
|
|
|
22
19
|
it("accepts lowercase hex", () => {
|
|
23
|
-
const result = validatePrivateKey(
|
|
20
|
+
const result = validatePrivateKey(TEST_HEX_PRIVATE_KEY.toLowerCase());
|
|
24
21
|
expect(result).toBeInstanceOf(Uint8Array);
|
|
25
22
|
});
|
|
26
23
|
|
|
27
24
|
it("accepts uppercase hex", () => {
|
|
28
|
-
const result = validatePrivateKey(
|
|
25
|
+
const result = validatePrivateKey(TEST_HEX_PRIVATE_KEY.toUpperCase());
|
|
29
26
|
expect(result).toBeInstanceOf(Uint8Array);
|
|
30
27
|
});
|
|
31
28
|
|
|
@@ -36,23 +33,23 @@ describe("validatePrivateKey", () => {
|
|
|
36
33
|
});
|
|
37
34
|
|
|
38
35
|
it("trims whitespace", () => {
|
|
39
|
-
const result = validatePrivateKey(` ${
|
|
36
|
+
const result = validatePrivateKey(` ${TEST_HEX_PRIVATE_KEY} `);
|
|
40
37
|
expect(result).toBeInstanceOf(Uint8Array);
|
|
41
38
|
});
|
|
42
39
|
|
|
43
40
|
it("trims newlines", () => {
|
|
44
|
-
const result = validatePrivateKey(`${
|
|
41
|
+
const result = validatePrivateKey(`${TEST_HEX_PRIVATE_KEY}\n`);
|
|
45
42
|
expect(result).toBeInstanceOf(Uint8Array);
|
|
46
43
|
});
|
|
47
44
|
|
|
48
45
|
it("rejects 63-char hex (too short)", () => {
|
|
49
|
-
expect(() => validatePrivateKey(
|
|
46
|
+
expect(() => validatePrivateKey(TEST_HEX_PRIVATE_KEY.slice(0, 63))).toThrow(
|
|
50
47
|
"Private key must be 64 hex characters",
|
|
51
48
|
);
|
|
52
49
|
});
|
|
53
50
|
|
|
54
51
|
it("rejects 65-char hex (too long)", () => {
|
|
55
|
-
expect(() => validatePrivateKey(
|
|
52
|
+
expect(() => validatePrivateKey(TEST_HEX_PRIVATE_KEY + "0")).toThrow(
|
|
56
53
|
"Private key must be 64 hex characters",
|
|
57
54
|
);
|
|
58
55
|
});
|
|
@@ -71,7 +68,7 @@ describe("validatePrivateKey", () => {
|
|
|
71
68
|
});
|
|
72
69
|
|
|
73
70
|
it("rejects key with 0x prefix", () => {
|
|
74
|
-
expect(() => validatePrivateKey("0x" +
|
|
71
|
+
expect(() => validatePrivateKey("0x" + TEST_HEX_PRIVATE_KEY)).toThrow(
|
|
75
72
|
"Private key must be 64 hex characters",
|
|
76
73
|
);
|
|
77
74
|
});
|
|
@@ -93,8 +90,7 @@ describe("validatePrivateKey", () => {
|
|
|
93
90
|
describe("isValidPubkey", () => {
|
|
94
91
|
describe("hex format", () => {
|
|
95
92
|
it("accepts valid 64-char hex pubkey", () => {
|
|
96
|
-
|
|
97
|
-
expect(isValidPubkey(validHex)).toBe(true);
|
|
93
|
+
expect(isValidPubkey(TEST_HEX_PRIVATE_KEY)).toBe(true);
|
|
98
94
|
});
|
|
99
95
|
|
|
100
96
|
it("accepts uppercase hex", () => {
|
|
@@ -108,7 +104,7 @@ describe("isValidPubkey", () => {
|
|
|
108
104
|
});
|
|
109
105
|
|
|
110
106
|
it("rejects 65-char hex", () => {
|
|
111
|
-
const longHex =
|
|
107
|
+
const longHex = `${TEST_HEX_PRIVATE_KEY}0`;
|
|
112
108
|
expect(isValidPubkey(longHex)).toBe(false);
|
|
113
109
|
});
|
|
114
110
|
|
|
@@ -134,8 +130,7 @@ describe("isValidPubkey", () => {
|
|
|
134
130
|
});
|
|
135
131
|
|
|
136
132
|
it("handles whitespace-padded input", () => {
|
|
137
|
-
|
|
138
|
-
expect(isValidPubkey(` ${validHex} `)).toBe(true);
|
|
133
|
+
expect(isValidPubkey(` ${TEST_HEX_PRIVATE_KEY} `)).toBe(true);
|
|
139
134
|
});
|
|
140
135
|
});
|
|
141
136
|
});
|
|
@@ -149,8 +144,7 @@ describe("normalizePubkey", () => {
|
|
|
149
144
|
});
|
|
150
145
|
|
|
151
146
|
it("trims whitespace", () => {
|
|
152
|
-
|
|
153
|
-
expect(normalizePubkey(` ${hex} `)).toBe(hex);
|
|
147
|
+
expect(normalizePubkey(` ${TEST_HEX_PRIVATE_KEY} `)).toBe(TEST_HEX_PRIVATE_KEY);
|
|
154
148
|
});
|
|
155
149
|
|
|
156
150
|
it("rejects invalid hex", () => {
|
|
@@ -161,14 +155,14 @@ describe("normalizePubkey", () => {
|
|
|
161
155
|
|
|
162
156
|
describe("getPublicKeyFromPrivate", () => {
|
|
163
157
|
it("derives public key from hex private key", () => {
|
|
164
|
-
const pubkey = getPublicKeyFromPrivate(
|
|
158
|
+
const pubkey = getPublicKeyFromPrivate(TEST_HEX_PRIVATE_KEY);
|
|
165
159
|
expect(pubkey).toMatch(/^[0-9a-f]{64}$/);
|
|
166
160
|
expect(pubkey.length).toBe(64);
|
|
167
161
|
});
|
|
168
162
|
|
|
169
163
|
it("derives consistent public key", () => {
|
|
170
|
-
const pubkey1 = getPublicKeyFromPrivate(
|
|
171
|
-
const pubkey2 = getPublicKeyFromPrivate(
|
|
164
|
+
const pubkey1 = getPublicKeyFromPrivate(TEST_HEX_PRIVATE_KEY);
|
|
165
|
+
const pubkey2 = getPublicKeyFromPrivate(TEST_HEX_PRIVATE_KEY);
|
|
172
166
|
expect(pubkey1).toBe(pubkey2);
|
|
173
167
|
});
|
|
174
168
|
|
|
@@ -179,21 +173,18 @@ describe("getPublicKeyFromPrivate", () => {
|
|
|
179
173
|
|
|
180
174
|
describe("pubkeyToNpub", () => {
|
|
181
175
|
it("converts hex pubkey to npub format", () => {
|
|
182
|
-
const
|
|
183
|
-
const npub = pubkeyToNpub(hex);
|
|
176
|
+
const npub = pubkeyToNpub(TEST_HEX_PRIVATE_KEY);
|
|
184
177
|
expect(npub).toMatch(/^npub1[a-z0-9]+$/);
|
|
185
178
|
});
|
|
186
179
|
|
|
187
180
|
it("produces consistent output", () => {
|
|
188
|
-
const
|
|
189
|
-
const
|
|
190
|
-
const npub2 = pubkeyToNpub(hex);
|
|
181
|
+
const npub1 = pubkeyToNpub(TEST_HEX_PRIVATE_KEY);
|
|
182
|
+
const npub2 = pubkeyToNpub(TEST_HEX_PRIVATE_KEY);
|
|
191
183
|
expect(npub1).toBe(npub2);
|
|
192
184
|
});
|
|
193
185
|
|
|
194
186
|
it("normalizes uppercase hex first", () => {
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
expect(pubkeyToNpub(lower)).toBe(pubkeyToNpub(upper));
|
|
187
|
+
const upper = TEST_HEX_PRIVATE_KEY.toUpperCase();
|
|
188
|
+
expect(pubkeyToNpub(TEST_HEX_PRIVATE_KEY)).toBe(pubkeyToNpub(upper));
|
|
198
189
|
});
|
|
199
190
|
});
|