@openclaw/nostr 2026.3.12 → 2026.5.1-beta.2

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 (48) hide show
  1. package/README.md +6 -0
  2. package/api.ts +10 -0
  3. package/channel-plugin-api.ts +1 -0
  4. package/index.ts +60 -36
  5. package/openclaw.plugin.json +190 -1
  6. package/package.json +41 -9
  7. package/runtime-api.ts +6 -0
  8. package/setup-api.ts +1 -0
  9. package/setup-entry.ts +9 -0
  10. package/setup-plugin-api.ts +3 -0
  11. package/src/channel-api.ts +15 -0
  12. package/src/channel.inbound.test.ts +176 -0
  13. package/src/channel.outbound.test.ts +89 -49
  14. package/src/channel.setup.ts +231 -0
  15. package/src/channel.test.ts +439 -71
  16. package/src/channel.ts +146 -284
  17. package/src/config-schema.ts +18 -12
  18. package/src/default-relays.ts +1 -0
  19. package/src/gateway.ts +302 -0
  20. package/src/inbound-direct-dm-runtime.ts +1 -0
  21. package/src/metrics.ts +6 -6
  22. package/src/nostr-bus.fuzz.test.ts +74 -247
  23. package/src/nostr-bus.inbound.test.ts +526 -0
  24. package/src/nostr-bus.integration.test.ts +88 -64
  25. package/src/nostr-bus.test.ts +22 -31
  26. package/src/nostr-bus.ts +206 -136
  27. package/src/nostr-key-utils.ts +94 -0
  28. package/src/nostr-profile-core.ts +134 -0
  29. package/src/nostr-profile-http-runtime.ts +6 -0
  30. package/src/nostr-profile-http.test.ts +310 -192
  31. package/src/nostr-profile-http.ts +51 -36
  32. package/src/nostr-profile-import.ts +3 -3
  33. package/src/nostr-profile-url-safety.ts +21 -0
  34. package/src/nostr-profile.fuzz.test.ts +7 -57
  35. package/src/nostr-profile.test.ts +16 -14
  36. package/src/nostr-profile.ts +13 -146
  37. package/src/nostr-state-store.test.ts +106 -2
  38. package/src/nostr-state-store.ts +46 -49
  39. package/src/runtime.ts +6 -3
  40. package/src/seen-tracker.ts +1 -1
  41. package/src/session-route.ts +25 -0
  42. package/src/setup-surface.ts +265 -0
  43. package/src/test-fixtures.ts +45 -0
  44. package/src/types.ts +26 -25
  45. package/test-api.ts +1 -0
  46. package/tsconfig.json +16 -0
  47. package/CHANGELOG.md +0 -110
  48. 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 = createSeenTracker({ maxEntries: 100, ttlMs: 60000 });
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 = createSeenTracker({ maxEntries: 100, ttlMs: 60000 });
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 = createSeenTracker({ maxEntries: 100, ttlMs: 60000 });
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 = createSeenTracker({ maxEntries: 100, ttlMs: 60000 });
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 = createSeenTracker({ maxEntries: 100, ttlMs: 60000 });
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 = createSeenTracker({ maxEntries: 3, ttlMs: 60000 });
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 = createSeenTracker({ maxEntries: 3, ttlMs: 60000 });
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 = createSeenTracker({ maxEntries: 1, ttlMs: 60000 });
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 = createSeenTracker({ maxEntries: 2, ttlMs: 60000 });
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 = createSeenTracker({
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 = createSeenTracker({
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: MetricEvent[] = [];
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: MetricEvent[] = [];
215
- const metrics = createMetrics((event) => events.push(event));
240
+ const { events, metrics } = createCollectingMetrics();
216
241
 
217
- metrics.emit("relay.connect", 1, { relay: "wss://relay.example.com" });
242
+ metrics.emit("relay.connect", 1, { relay: TEST_RELAY_URL });
218
243
 
219
- expect(events[0].labels).toEqual({ relay: "wss://relay.example.com" });
244
+ expect(events[0].labels).toEqual({ relay: TEST_RELAY_URL });
220
245
  });
221
246
 
222
247
  it("accumulates counters in snapshot", () => {
223
- const metrics = createMetrics();
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 = createMetrics();
264
+ const metrics = createPlainMetrics();
240
265
 
241
- metrics.emit("relay.connect", 1, { relay: "wss://relay1.com" });
242
- metrics.emit("relay.connect", 1, { relay: "wss://relay2.com" });
243
- metrics.emit("relay.error", 1, { relay: "wss://relay1.com" });
244
- metrics.emit("relay.error", 1, { relay: "wss://relay1.com" });
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["wss://relay1.com"]).toBeDefined();
248
- expect(snapshot.relays["wss://relay1.com"].connects).toBe(1);
249
- expect(snapshot.relays["wss://relay1.com"].errors).toBe(2);
250
- expect(snapshot.relays["wss://relay2.com"].connects).toBe(1);
251
- expect(snapshot.relays["wss://relay2.com"].errors).toBe(0);
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 = createMetrics();
280
+ const metrics = createPlainMetrics();
256
281
 
257
- metrics.emit("relay.circuit_breaker.open", 1, { relay: "wss://relay.com" });
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["wss://relay.com"].circuitBreakerState).toBe("open");
261
- expect(snapshot.relays["wss://relay.com"].circuitBreakerOpens).toBe(1);
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: "wss://relay.com" });
288
+ metrics.emit("relay.circuit_breaker.close", 1, { relay: TEST_RELAY_URL_PRIMARY });
264
289
 
265
290
  snapshot = metrics.getSnapshot();
266
- expect(snapshot.relays["wss://relay.com"].circuitBreakerState).toBe("closed");
267
- expect(snapshot.relays["wss://relay.com"].circuitBreakerCloses).toBe(1);
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 = createMetrics();
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 = createMetrics();
323
+ const metrics = createPlainMetrics();
299
324
 
300
- metrics.emit("relay.message.event", 1, { relay: "wss://relay.com" });
301
- metrics.emit("relay.message.eose", 1, { relay: "wss://relay.com" });
302
- metrics.emit("relay.message.closed", 1, { relay: "wss://relay.com" });
303
- metrics.emit("relay.message.notice", 1, { relay: "wss://relay.com" });
304
- metrics.emit("relay.message.ok", 1, { relay: "wss://relay.com" });
305
- metrics.emit("relay.message.auth", 1, { relay: "wss://relay.com" });
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["wss://relay.com"];
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 = createMetrics();
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 = createMetrics();
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 = createMetrics();
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: "wss://relay.com" });
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: "wss://relay.com" });
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: MetricEvent[] = [];
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: "wss://relay.com" });
412
+ metrics.emit("relay.error", 1, { relay: TEST_RELAY_URL_PRIMARY });
389
413
  }
390
- metrics.emit("relay.circuit_breaker.open", 1, { relay: "wss://relay.com" });
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: "wss://relay.com" });
394
- metrics.emit("relay.circuit_breaker.close", 1, { relay: "wss://relay.com" });
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 = createMetrics();
434
+ const metrics = createPlainMetrics();
411
435
 
412
436
  // Simulate mixed success/failure pattern
413
- metrics.emit("relay.connect", 1, { relay: "wss://good-relay.com" });
414
- metrics.emit("relay.connect", 1, { relay: "wss://bad-relay.com" });
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: "wss://bad-relay.com" });
417
- metrics.emit("relay.error", 1, { relay: "wss://bad-relay.com" });
418
- metrics.emit("relay.error", 1, { relay: "wss://bad-relay.com" });
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["wss://good-relay.com"].errors).toBe(0);
422
- expect(snapshot.relays["wss://bad-relay.com"].errors).toBe(3);
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 * Math.pow(2, attempt);
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);
@@ -5,27 +5,24 @@ import {
5
5
  isValidPubkey,
6
6
  normalizePubkey,
7
7
  pubkeyToNpub,
8
- } from "./nostr-bus.js";
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(TEST_HEX_KEY);
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(TEST_HEX_KEY.toLowerCase());
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(TEST_HEX_KEY.toUpperCase());
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(` ${TEST_HEX_KEY} `);
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(`${TEST_HEX_KEY}\n`);
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(TEST_HEX_KEY.slice(0, 63))).toThrow(
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(TEST_HEX_KEY + "0")).toThrow(
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" + TEST_HEX_KEY)).toThrow(
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
- const validHex = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
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 = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0";
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
- const validHex = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
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
- const hex = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
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(TEST_HEX_KEY);
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(TEST_HEX_KEY);
171
- const pubkey2 = getPublicKeyFromPrivate(TEST_HEX_KEY);
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 hex = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
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 hex = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
189
- const npub1 = pubkeyToNpub(hex);
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 lower = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
196
- const upper = lower.toUpperCase();
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
  });