Converted id module. Renamed things.
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
"name": "@wisehodl/roots",
|
"name": "@wisehodl/roots",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"description": "A minimal Nostr protocol library for typescript",
|
"description": "A minimal Nostr protocol library for typescript",
|
||||||
"type": "./dist/types.d.ts",
|
"types": "./dist/types.d.ts",
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { EventID } from "./id";
|
||||||
|
import { Sign } from "./sign";
|
||||||
|
|
||||||
|
export const Event = {
|
||||||
|
...EventID,
|
||||||
|
...Sign,
|
||||||
|
};
|
||||||
|
|||||||
222
src/id.test.ts
222
src/id.test.ts
@@ -1,2 +1,220 @@
|
|||||||
import { test } from "vitest";
|
import { describe, test, expect } from "vitest";
|
||||||
test("placeholder", () => {});
|
import { EventID } from "./id";
|
||||||
|
import { testEvent, testPK } from "./util.test";
|
||||||
|
import type { EventData } from "./types";
|
||||||
|
|
||||||
|
interface IDTestCase {
|
||||||
|
name: string;
|
||||||
|
event: EventData;
|
||||||
|
expected: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const idTestCases: IDTestCase[] = [
|
||||||
|
{
|
||||||
|
name: "minimal event",
|
||||||
|
event: {
|
||||||
|
pubkey: testPK,
|
||||||
|
created_at: testEvent.created_at,
|
||||||
|
kind: 1,
|
||||||
|
tags: [],
|
||||||
|
content: "",
|
||||||
|
id: "",
|
||||||
|
sig: "",
|
||||||
|
},
|
||||||
|
expected:
|
||||||
|
"13a55672a600398894592f4cb338652d4936caffe5d3718d11597582bb030c39",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "alphanumeric content",
|
||||||
|
event: {
|
||||||
|
pubkey: testPK,
|
||||||
|
created_at: testEvent.created_at,
|
||||||
|
kind: 1,
|
||||||
|
tags: [],
|
||||||
|
content: "hello world",
|
||||||
|
id: "",
|
||||||
|
sig: "",
|
||||||
|
},
|
||||||
|
expected:
|
||||||
|
"c7a702e6158744ca03508bbb4c90f9dbb0d6e88fefbfaa511d5ab24b4e3c48ad",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unicode content",
|
||||||
|
event: {
|
||||||
|
pubkey: testPK,
|
||||||
|
created_at: testEvent.created_at,
|
||||||
|
kind: 1,
|
||||||
|
tags: [],
|
||||||
|
content: "hello world 😀",
|
||||||
|
id: "",
|
||||||
|
sig: "",
|
||||||
|
},
|
||||||
|
expected:
|
||||||
|
"e42083fafbf9a39f97914fd9a27cedb38c429ac3ca8814288414eaad1f472fe8",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "escaped content",
|
||||||
|
event: {
|
||||||
|
pubkey: testPK,
|
||||||
|
created_at: testEvent.created_at,
|
||||||
|
kind: 1,
|
||||||
|
tags: [],
|
||||||
|
content: '"You say yes."\\n\\t"I say no."',
|
||||||
|
id: "",
|
||||||
|
sig: "",
|
||||||
|
},
|
||||||
|
expected:
|
||||||
|
"343de133996a766bf00561945b6f2b2717d4905275976ca75c1d7096b7d1900c",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "json content",
|
||||||
|
event: {
|
||||||
|
pubkey: testPK,
|
||||||
|
created_at: testEvent.created_at,
|
||||||
|
kind: 1,
|
||||||
|
tags: [],
|
||||||
|
content: '{"field": ["value","value"],"numeral": 123}',
|
||||||
|
id: "",
|
||||||
|
sig: "",
|
||||||
|
},
|
||||||
|
expected:
|
||||||
|
"c6140190453ee947efb790e70541a9d37c41604d1f29e4185da4325621ed5270",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty tag",
|
||||||
|
event: {
|
||||||
|
pubkey: testPK,
|
||||||
|
created_at: testEvent.created_at,
|
||||||
|
kind: 1,
|
||||||
|
tags: [["a", ""]],
|
||||||
|
content: "",
|
||||||
|
id: "",
|
||||||
|
sig: "",
|
||||||
|
},
|
||||||
|
expected:
|
||||||
|
"7d3e394c75916362436f11c603b1a89b40b50817550cfe522a90d769655007a4",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "single tag",
|
||||||
|
event: {
|
||||||
|
pubkey: testPK,
|
||||||
|
created_at: testEvent.created_at,
|
||||||
|
kind: 1,
|
||||||
|
tags: [["a", "value"]],
|
||||||
|
content: "",
|
||||||
|
id: "",
|
||||||
|
sig: "",
|
||||||
|
},
|
||||||
|
expected:
|
||||||
|
"7db394e274fb893edbd9f4aa9ff189d4f3264bf1a29cef8f614e83ebf6fa19fe",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "optional tag values",
|
||||||
|
event: {
|
||||||
|
pubkey: testPK,
|
||||||
|
created_at: testEvent.created_at,
|
||||||
|
kind: 1,
|
||||||
|
tags: [["a", "value", "optional"]],
|
||||||
|
content: "",
|
||||||
|
id: "",
|
||||||
|
sig: "",
|
||||||
|
},
|
||||||
|
expected:
|
||||||
|
"656b47884200959e0c03054292c453cfc4beea00b592d92c0f557bff765e9d34",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple tags",
|
||||||
|
event: {
|
||||||
|
pubkey: testPK,
|
||||||
|
created_at: testEvent.created_at,
|
||||||
|
kind: 1,
|
||||||
|
tags: [
|
||||||
|
["a", "value", "optional"],
|
||||||
|
["b", "another"],
|
||||||
|
["c", "data"],
|
||||||
|
],
|
||||||
|
content: "",
|
||||||
|
id: "",
|
||||||
|
sig: "",
|
||||||
|
},
|
||||||
|
expected:
|
||||||
|
"f7c27f2eacda7ece5123a4f82db56145ba59f7c9e6c5eeb88552763664506b06",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unicode tag",
|
||||||
|
event: {
|
||||||
|
pubkey: testPK,
|
||||||
|
created_at: testEvent.created_at,
|
||||||
|
kind: 1,
|
||||||
|
tags: [["a", "😀"]],
|
||||||
|
content: "",
|
||||||
|
id: "",
|
||||||
|
sig: "",
|
||||||
|
},
|
||||||
|
expected:
|
||||||
|
"fd2798d165d9bf46acbe817735dc8cedacd4c42dfd9380792487d4902539e986",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "zero timestamp",
|
||||||
|
event: {
|
||||||
|
pubkey: testPK,
|
||||||
|
created_at: 0,
|
||||||
|
kind: 1,
|
||||||
|
tags: [],
|
||||||
|
content: "",
|
||||||
|
id: "",
|
||||||
|
sig: "",
|
||||||
|
},
|
||||||
|
expected:
|
||||||
|
"9ca742f2e2eea72ad6e0277a6287e2bb16a3e47d64b8468bc98474e266cf0ec2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "negative timestamp",
|
||||||
|
event: {
|
||||||
|
pubkey: testPK,
|
||||||
|
created_at: -1760740551,
|
||||||
|
kind: 1,
|
||||||
|
tags: [],
|
||||||
|
content: "",
|
||||||
|
id: "",
|
||||||
|
sig: "",
|
||||||
|
},
|
||||||
|
expected:
|
||||||
|
"4740b027040bb4d0ee8e885f567a80277097da70cddd143d8a6dadf97f6faaa3",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "max int64 timestamp",
|
||||||
|
event: {
|
||||||
|
pubkey: testPK,
|
||||||
|
created_at: 9007199254740991,
|
||||||
|
kind: 1,
|
||||||
|
tags: [],
|
||||||
|
content: "",
|
||||||
|
id: "",
|
||||||
|
sig: "",
|
||||||
|
},
|
||||||
|
expected:
|
||||||
|
"7aa9e4bca8058ab819b6ce062efb2f8423f598bcb3d9f4b5b46b2f587b182a55",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "different kind",
|
||||||
|
event: {
|
||||||
|
pubkey: testPK,
|
||||||
|
created_at: testEvent.created_at,
|
||||||
|
kind: 20021,
|
||||||
|
tags: [],
|
||||||
|
content: "",
|
||||||
|
id: "",
|
||||||
|
sig: "",
|
||||||
|
},
|
||||||
|
expected:
|
||||||
|
"995c4894c264e6b9558cb94b7b34008768d53801b99960b47298d4e3e23fadd3",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
describe("EventID.getID", () => {
|
||||||
|
test.each(idTestCases)("$name", ({ event, expected }) => {
|
||||||
|
const actual = EventID.getID(event);
|
||||||
|
expect(actual).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
36
src/id.ts
36
src/id.ts
@@ -0,0 +1,36 @@
|
|||||||
|
import { sha256 } from "@noble/hashes/sha2.js";
|
||||||
|
import type { EventData } from "./types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serializes an event into canonical JSON array format for ID computation.
|
||||||
|
* Returns: [0, pubkey, created_at, kind, tags, content]
|
||||||
|
* @param event - Event to serialize
|
||||||
|
* @returns Canonical JSON string
|
||||||
|
*/
|
||||||
|
function serialize(event: EventData): string {
|
||||||
|
const serialized = [
|
||||||
|
0,
|
||||||
|
event.pubkey,
|
||||||
|
event.created_at,
|
||||||
|
event.kind,
|
||||||
|
event.tags,
|
||||||
|
event.content,
|
||||||
|
];
|
||||||
|
return JSON.stringify(serialized);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes the event ID as a lowercase hex-encoded SHA-256 hash.
|
||||||
|
* @param event - Event to compute ID for
|
||||||
|
* @returns 64-character lowercase hexadecimal event ID
|
||||||
|
*/
|
||||||
|
function getID(event: EventData): string {
|
||||||
|
const serialized = serialize(event);
|
||||||
|
const hash = sha256(new TextEncoder().encode(serialized));
|
||||||
|
return Buffer.from(hash).toString("hex");
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EventID = {
|
||||||
|
serialize,
|
||||||
|
getID,
|
||||||
|
};
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export * from "./types";
|
||||||
|
export * from "./constants";
|
||||||
|
export * from "./errors";
|
||||||
|
export { Event } from "./event";
|
||||||
|
export { Keys } from "./keys";
|
||||||
|
|||||||
@@ -5,32 +5,32 @@ import { testSK, testPK } from "./util.test";
|
|||||||
|
|
||||||
describe("Keys.generatePrivate", () => {
|
describe("Keys.generatePrivate", () => {
|
||||||
test("returns 64 hex characters", () => {
|
test("returns 64 hex characters", () => {
|
||||||
const privateKey = Keys.generatePrivate();
|
const privateKey = Keys.generatePrivateKey();
|
||||||
expect(privateKey).toMatch(HEX_64_PATTERN);
|
expect(privateKey).toMatch(HEX_64_PATTERN);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("generates unique keys", () => {
|
test("generates unique keys", () => {
|
||||||
const key1 = Keys.generatePrivate();
|
const key1 = Keys.generatePrivateKey();
|
||||||
const key2 = Keys.generatePrivate();
|
const key2 = Keys.generatePrivateKey();
|
||||||
expect(key1).not.toBe(key2);
|
expect(key1).not.toBe(key2);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Keys.getPublic", () => {
|
describe("Keys.getPublic", () => {
|
||||||
test("derives correct public key", () => {
|
test("derives correct public key", () => {
|
||||||
const publicKey = Keys.getPublic(testSK);
|
const publicKey = Keys.getPublicKey(testSK);
|
||||||
expect(publicKey).toBe(testPK);
|
expect(publicKey).toBe(testPK);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("throws on invalid private key - too short", () => {
|
test("throws on invalid private key - too short", () => {
|
||||||
expect(() => Keys.getPublic("abc123")).toThrow(
|
expect(() => Keys.getPublicKey("abc123")).toThrow(
|
||||||
"private key must be 64 lowercase hex characters",
|
"private key must be 64 lowercase hex characters",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("throws on invalid private key - non-hex", () => {
|
test("throws on invalid private key - non-hex", () => {
|
||||||
expect(() =>
|
expect(() =>
|
||||||
Keys.getPublic(
|
Keys.getPublicKey(
|
||||||
"zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz",
|
"zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz",
|
||||||
),
|
),
|
||||||
).toThrow("private key must be 64 lowercase hex characters");
|
).toThrow("private key must be 64 lowercase hex characters");
|
||||||
@@ -38,7 +38,7 @@ describe("Keys.getPublic", () => {
|
|||||||
|
|
||||||
test("throws on invalid private key - uppercase", () => {
|
test("throws on invalid private key - uppercase", () => {
|
||||||
expect(() =>
|
expect(() =>
|
||||||
Keys.getPublic(
|
Keys.getPublicKey(
|
||||||
"F43A0435F69529F310BBD1D6263D2FBF0977F54BFE2310CC37AE5904B83BB167",
|
"F43A0435F69529F310BBD1D6263D2FBF0977F54BFE2310CC37AE5904B83BB167",
|
||||||
),
|
),
|
||||||
).toThrow("private key must be 64 lowercase hex characters");
|
).toThrow("private key must be 64 lowercase hex characters");
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { MalformedPrivKeyError } from "./errors";
|
|||||||
* Generates a new random secp256k1 private key.
|
* Generates a new random secp256k1 private key.
|
||||||
* @returns 64-character lowercase hexadecimal string
|
* @returns 64-character lowercase hexadecimal string
|
||||||
*/
|
*/
|
||||||
function generatePrivate(): string {
|
function generatePrivateKey(): string {
|
||||||
const { secretKey } = schnorr.keygen();
|
const { secretKey } = schnorr.keygen();
|
||||||
return Buffer.from(secretKey).toString("hex");
|
return Buffer.from(secretKey).toString("hex");
|
||||||
}
|
}
|
||||||
@@ -17,7 +17,7 @@ function generatePrivate(): string {
|
|||||||
* @returns 64-character lowercase hexadecimal public key (x-coordinate only)
|
* @returns 64-character lowercase hexadecimal public key (x-coordinate only)
|
||||||
* @throws {MalformedPrivKeyError} If private key is not 64 lowercase hex characters
|
* @throws {MalformedPrivKeyError} If private key is not 64 lowercase hex characters
|
||||||
*/
|
*/
|
||||||
function getPublic(privateKey: string): string {
|
function getPublicKey(privateKey: string): string {
|
||||||
if (!HEX_64_PATTERN.test(privateKey)) {
|
if (!HEX_64_PATTERN.test(privateKey)) {
|
||||||
throw new MalformedPrivKeyError();
|
throw new MalformedPrivKeyError();
|
||||||
}
|
}
|
||||||
@@ -29,6 +29,6 @@ function getPublic(privateKey: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const Keys = {
|
export const Keys = {
|
||||||
generatePrivate,
|
generatePrivateKey,
|
||||||
getPublic,
|
getPublicKey,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import * as secp from "@noble/secp256k1";
|
import { hashes as secp_hashes } from "@noble/secp256k1";
|
||||||
import { schnorr } from "@noble/secp256k1";
|
import { schnorr } from "@noble/secp256k1";
|
||||||
import { hmac } from "@noble/hashes/hmac.js";
|
import { hmac } from "@noble/hashes/hmac.js";
|
||||||
import { sha256 } from "@noble/hashes/sha2.js";
|
import { sha256 } from "@noble/hashes/sha2.js";
|
||||||
import { MalformedIDError, MalformedPrivKeyError } from "./errors";
|
import { MalformedIDError, MalformedPrivKeyError } from "./errors";
|
||||||
|
|
||||||
secp.hashes.hmacSha256 = (key, msg) => hmac(sha256, key, msg);
|
secp_hashes.hmacSha256 = (key, msg) => hmac(sha256, key, msg);
|
||||||
secp.hashes.sha256 = sha256;
|
secp_hashes.sha256 = sha256;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a Schnorr signature for the given event ID using the provided private key.
|
* Generates a Schnorr signature for the given event ID using the provided private key.
|
||||||
|
|||||||
@@ -6,10 +6,10 @@
|
|||||||
export type Tag = string[];
|
export type Tag = string[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Event represents a Nostr protocol event with its seven required fields.
|
* EventData represents a Nostr protocol event with its seven required fields.
|
||||||
* All fields must be present for a valid event.
|
* All fields must be present for a valid event.
|
||||||
*/
|
*/
|
||||||
export interface Event {
|
export interface EventData {
|
||||||
id: string;
|
id: string;
|
||||||
pubkey: string;
|
pubkey: string;
|
||||||
created_at: number;
|
created_at: number;
|
||||||
@@ -36,10 +36,10 @@ export interface FilterExtensions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Filter defines subscription criteria for events.
|
* FilterData defines subscription criteria for events.
|
||||||
* All conditions within a filter are applied with AND logic.
|
* All conditions within a filter are applied with AND logic.
|
||||||
*/
|
*/
|
||||||
export interface Filter {
|
export interface FilterData {
|
||||||
ids?: string[];
|
ids?: string[];
|
||||||
authors?: string[];
|
authors?: string[];
|
||||||
kinds?: number[];
|
kinds?: number[];
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { test } from "vitest";
|
import { test } from "vitest";
|
||||||
import type { Event } from "./types";
|
import type { EventData } from "./types";
|
||||||
|
|
||||||
test("placeholder", () => {});
|
test("placeholder", () => {});
|
||||||
|
|
||||||
@@ -8,7 +8,7 @@ export const testSK =
|
|||||||
export const testPK =
|
export const testPK =
|
||||||
"cfa87f35acbde29ba1ab3ee42de527b2cad33ac487e80cf2d6405ea0042c8fef";
|
"cfa87f35acbde29ba1ab3ee42de527b2cad33ac487e80cf2d6405ea0042c8fef";
|
||||||
|
|
||||||
export const testEvent: Event = {
|
export const testEvent: EventData = {
|
||||||
id: "c7a702e6158744ca03508bbb4c90f9dbb0d6e88fefbfaa511d5ab24b4e3c48ad",
|
id: "c7a702e6158744ca03508bbb4c90f9dbb0d6e88fefbfaa511d5ab24b4e3c48ad",
|
||||||
pubkey: testPK,
|
pubkey: testPK,
|
||||||
created_at: 1760740551,
|
created_at: 1760740551,
|
||||||
|
|||||||
Reference in New Issue
Block a user