From 3a83c39f3ea21d99e47334c57b5d4f70935d7ad5 Mon Sep 17 00:00:00 2001 From: hasundue Date: Thu, 14 Mar 2024 11:48:21 +0900 Subject: [PATCH] WIP: create nips module --- core/deno.json | 6 ++++ core/protocol.ts | 11 +++++-- deno.json | 13 ++++++--- deno.lock | 16 ++++++++-- lib/deno.json | 3 ++ nips/01/mod.ts | 0 nips/01/protocol.ts | 10 +++---- nips/02/mod.ts | 0 nips/07/mod.ts | 1 + nips/07/protocol.ts | 18 +++++------- nips/07/signs.ts | 33 --------------------- nips/42/mod.ts | 0 nips/42/protocol.ts | 5 ++-- nips/deno.json | 20 +++++++++++++ nips/mod.ts | 4 +++ std/deno.json | 11 +++++-- std/events.ts | 27 ----------------- std/events_test.ts | 51 -------------------------------- std/mod.ts | 2 -- std/notes.ts | 5 ++-- std/signs.ts | 71 +++++++++++++++++++++++++++++++++------------ std/signs_test.ts | 19 ++++++------ 22 files changed, 151 insertions(+), 175 deletions(-) create mode 100644 nips/01/mod.ts create mode 100644 nips/02/mod.ts create mode 100644 nips/07/mod.ts delete mode 100644 nips/07/signs.ts create mode 100644 nips/42/mod.ts create mode 100644 nips/deno.json create mode 100644 nips/mod.ts delete mode 100644 std/events.ts delete mode 100644 std/events_test.ts diff --git a/core/deno.json b/core/deno.json index 8beff79..ffe0eb2 100644 --- a/core/deno.json +++ b/core/deno.json @@ -6,5 +6,11 @@ "./clients": "./clients.ts", "./protocol": "./protocol.ts", "./relays": "./relays.ts" + }, + "imports": { + "@lophus/lib": "jsr:@lophus/lib@0.0.14" + }, + "publish": { + "exclude": ["*_test.ts"] } } diff --git a/core/protocol.ts b/core/protocol.ts index a3c7131..a9240e8 100644 --- a/core/protocol.ts +++ b/core/protocol.ts @@ -1,11 +1,10 @@ // deno-lint-ignore-file no-empty-interface /** - * Implementation of unextendable part of NIP-01 (Nostr basic protocol): + * Implementation of the fundamental part of NIP-01 (Nostr basic protocol): * https://github.com/nostr-protocol/nips/blob/master/01.md * - * See also `../../nips/01/protocol.d.ts` for the extendable part. - * + * See also `../../nips/01/protocol.ts` for the remaining "optional" part. * @module */ @@ -59,6 +58,12 @@ export type EventSerializePrecursor = [ content: NostrEvent["content"], ]; +/** An event that has not been signed. */ +export type UnsignedEvent = Omit< + NostrEvent, + "id" | "pubkey" | "sig" +>; + // ---------------------- // Tags // ---------------------- diff --git a/deno.json b/deno.json index 13e26ca..e09cb30 100644 --- a/deno.json +++ b/deno.json @@ -5,12 +5,17 @@ "imports": { "@noble/curves": "npm:@noble/curves@^1.3.0", "@noble/hashes": "npm:@noble/hashes@^1.3.3", - "@lophus/core/clients": "./core/clients.ts", - "@lophus/core/protocol": "./core/protocol.ts", - "@lophus/core/relays": "./core/relays.ts", "@lophus/lib/types": "./lib/types.ts", "@lophus/lib/testing": "./lib/testing.ts", "@lophus/lib/streams": "./lib/streams.ts", + "@lophus/core/clients": "./core/clients.ts", + "@lophus/core/protocol": "./core/protocol.ts", + "@lophus/core/relays": "./core/relays.ts", + "@lophus/nips": "./nips/mod.ts", + "@lophus/nips/01": "./nips/01/mod.ts", + "@lophus/nips/02": "./nips/02/mod.ts", + "@lophus/nips/07": "./nips/07/mod.ts", + "@lophus/nips/42": "./nips/42/mod.ts", "@lophus/std/env": "./std/env.ts", "@lophus/std/events": "./std/events.ts", "@lophus/std/notes": "./std/notes.ts", @@ -26,7 +31,7 @@ "build": "mkdir -p ./dist && deno run -A ./bin/bundle.ts", "cache": "deno cache ./**/*.ts --lock", "check": "deno check ./**/*.ts", - "lock": "deno task cache --lock-write && git add deno.lock", + "lock": "deno task cache --lock-write", "pre-commit": "deno fmt && deno lint && deno task -q check && deno task -q lock && deno task -q test", "test": "deno test -A --no-check", "update": "deno run --allow-read --allow-env --allow-write --allow-net=registry.npmjs.org,jsr.io --allow-run=deno,git https://deno.land/x/molt@0.17.1/cli.ts deno.json --unstable-lock", diff --git a/deno.lock b/deno.lock index 5abc4f5..0c32b25 100644 --- a/deno.lock +++ b/deno.lock @@ -125,8 +125,20 @@ "npm:@noble/hashes@^1.3.3" ], "members": { - "@lophus/core": {}, - "@lophus/lib": {} + "@lophus/core": { + "dependencies": [ + "jsr:@lophus/lib@0.0.14" + ] + }, + "@lophus/lib": {}, + "@lophus/std": { + "dependencies": [ + "jsr:@lophus/core@^0.0.14", + "jsr:@lophus/lib@^0.0.14", + "jsr:@std/dotenv@^0.219.1", + "jsr:@std/streams@^0.219.1" + ] + } } } } diff --git a/lib/deno.json b/lib/deno.json index 5bb8ad2..039bc04 100644 --- a/lib/deno.json +++ b/lib/deno.json @@ -5,5 +5,8 @@ "./streams": "./streams.ts", "./testing": "./testing.ts", "./types": "./types.ts" + }, + "publish": { + "exclude": ["*_test.ts"] } } diff --git a/nips/01/mod.ts b/nips/01/mod.ts new file mode 100644 index 0000000..e69de29 diff --git a/nips/01/protocol.ts b/nips/01/protocol.ts index f098b3d..ee3a687 100644 --- a/nips/01/protocol.ts +++ b/nips/01/protocol.ts @@ -1,16 +1,16 @@ /** - * Implementation of extendable part of NIP-01 (Nostr basic protocol): + * Implementation of the "optional" part of NIP-01 (Nostr basic protocol): * https://github.com/nostr-protocol/nips/blob/master/01.md * - * See also `../../core/protocol.d.ts` for the unextendable part. + * See also `../../core/protocol.ts` for the unextendable part. * * @module */ -import "../../core/protocol.ts"; -import type { Url } from "../../lib/types.ts"; +import "@lophus/core/protocol"; +import type { Url } from "@lophus/lib/types"; -declare module "../../core/protocol.ts" { +declare module "@lophus/core/protocol" { interface NipRecord { 1: { ClientToRelayMessage: "EVENT" | "REQ" | "CLOSE"; diff --git a/nips/02/mod.ts b/nips/02/mod.ts new file mode 100644 index 0000000..e69de29 diff --git a/nips/07/mod.ts b/nips/07/mod.ts new file mode 100644 index 0000000..1bb83a1 --- /dev/null +++ b/nips/07/mod.ts @@ -0,0 +1 @@ +import "./protocol.ts"; diff --git a/nips/07/protocol.ts b/nips/07/protocol.ts index 3ecd70c..80f99fe 100644 --- a/nips/07/protocol.ts +++ b/nips/07/protocol.ts @@ -1,20 +1,16 @@ -import { EventKind, NostrEvent, PublicKey } from "../../core/protocol.ts"; -import type { Optional } from "../../lib/types.ts"; +import { + EventKind, + NostrEvent, + PublicKey, + UnsignedEvent, +} from "@lophus/core/protocol"; -declare module "../../core/protocol.ts" { +declare module "@lophus/core/protocol" { interface NipRecord { 7: Record; } } -/** - * An event that has not been signed. - */ -export type UnsignedEvent = Optional< - NostrEvent, - "id" | "pubkey" | "sig" ->; - declare global { interface Window { nostr?: { diff --git a/nips/07/signs.ts b/nips/07/signs.ts deleted file mode 100644 index 3e8a56a..0000000 --- a/nips/07/signs.ts +++ /dev/null @@ -1,33 +0,0 @@ -import type { Stringified } from "../../lib/types.ts"; -import { EventContent, EventKind, NostrEvent } from "../../core/protocol.ts"; -import { EventInit } from "../../std/events.ts"; -import { Timestamp } from "../../std/times.ts"; -import { UnsignedEvent } from "./protocol.ts"; - -/** - * A transform stream that signs events. - */ -export class Signer extends TransformStream { - constructor() { - // deno-lint-ignore no-window - if (!window.nostr) { - throw new Error("NIP-07 extension not installed"); - } - super({ - transform: (init, controller) => { - controller.enqueue(this.sign(init)); - }, - }); - } - sign(init: EventInit): NostrEvent { - const unsigned = { - created_at: Timestamp.now, - tags: [], - ...init, - content: JSON.stringify(init.content) as Stringified>, - // TODO: Can we avoid this type assertion? - } as UnsignedEvent; - // deno-lint-ignore no-window - return window.nostr!.signEvent(unsigned); - } -} diff --git a/nips/42/mod.ts b/nips/42/mod.ts new file mode 100644 index 0000000..e69de29 diff --git a/nips/42/protocol.ts b/nips/42/protocol.ts index 38ec14a..a6d8b41 100644 --- a/nips/42/protocol.ts +++ b/nips/42/protocol.ts @@ -1,7 +1,6 @@ -import "../../core/protocol.ts"; -import "../../core/relays.ts"; +import "@lophus/core/protocol"; -declare module "../../core/protocol.ts" { +declare module "@lophus/core/protocol" { interface NipRecord { 42: { ClientToRelayMessage: "AUTH"; diff --git a/nips/deno.json b/nips/deno.json new file mode 100644 index 0000000..959b663 --- /dev/null +++ b/nips/deno.json @@ -0,0 +1,20 @@ +{ + "name": "@lophus/nips", + "version": "0.0.14", + "exports": { + ".": "./mod.ts", + "./01": "./01/mod.ts", + "./02": "./02/mod.ts", + "./07": "./07/mod.ts", + "./42": "./42/mod.ts" + }, + "imports": { + "@std/dotenv": "jsr:@std/dotenv@^0.219.1", + "@std/streams": "jsr:@std/streams@^0.219.1", + "@lophus/lib": "jsr:@lophus/lib@0.0.14", + "@lophus/core": "jsr:@lophus/core@0.0.14" + }, + "publish": { + "exclude": ["*_test.ts"] + } +} diff --git a/nips/mod.ts b/nips/mod.ts new file mode 100644 index 0000000..bae31c4 --- /dev/null +++ b/nips/mod.ts @@ -0,0 +1,4 @@ +export * from "./01/mod.ts"; +export * from "./02/mod.ts"; +export * from "./07/mod.ts"; +export * from "./42/mod.ts"; diff --git a/std/deno.json b/std/deno.json index 8655c6b..45b1b61 100644 --- a/std/deno.json +++ b/std/deno.json @@ -4,10 +4,17 @@ "exports": { ".": "./mod.ts", "./env": "./env.ts", - "./events": "./events.ts", - "./notes": "./notes.ts", "./pools": "./pools.ts", "./signs": "./signs.ts", "./times": "./times.ts" + }, + "imports": { + "@std/dotenv": "jsr:@std/dotenv@^0.219.1", + "@std/streams": "jsr:@std/streams@^0.219.1", + "@lophus/lib": "jsr:@lophus/lib@^0.0.14", + "@lophus/core": "jsr:@lophus/core@^0.0.14" + }, + "publish": { + "exclude": ["*_test.ts"] } } diff --git a/std/events.ts b/std/events.ts deleted file mode 100644 index 51b06b8..0000000 --- a/std/events.ts +++ /dev/null @@ -1,27 +0,0 @@ -import type { Stringified } from "@lophus/lib/types"; -import type { - ClientToRelayMessage, - EventContent, - EventKind, - NostrEvent, -} from "@lophus/core/protocol"; -import "../nips/01/protocol.ts"; - -export interface EventInit { - kind: NostrEvent["kind"]; - tags?: NostrEvent["tags"]; - content: EventContent | Stringified>; -} - -import type { Signer } from "./signs.ts"; - -export class EventPublisher - extends TransformStream, ClientToRelayMessage<"EVENT", K>> { - constructor(readonly signer: Signer) { - super({ - transform: (init, controller) => { - controller.enqueue(["EVENT", this.signer.sign(init)]); - }, - }); - } -} diff --git a/std/events_test.ts b/std/events_test.ts deleted file mode 100644 index 5139c1f..0000000 --- a/std/events_test.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { afterAll, beforeAll, describe, it } from "@std/testing/bdd"; -import { assertEquals, assertInstanceOf } from "@std/assert"; -import { MockWebSocket } from "@lophus/lib/testing"; -import { Relay } from "@lophus/core/relays"; -import { PrivateKey, Signer } from "@lophus/std/signs"; -import { EventInit, EventPublisher } from "./events.ts"; - -describe("EventPublisher", () => { - let relay: Relay; - let signer: Signer; - let publisher: EventPublisher<1>; - - beforeAll(() => { - globalThis.WebSocket = MockWebSocket; - signer = new Signer(PrivateKey.generate()); - }); - afterAll(() => { - relay?.close(); - }); - - it("should be a TransformStream", () => { - publisher = new EventPublisher(signer); - assertInstanceOf(publisher, TransformStream); - }); - - // FIXME: dangling promise - // @ts-ignore missing property `ignore` - it.ignore( - "should transform EventInit to ClientToRelayMessage", - async () => { - const { readable, writable } = publisher; - const reader = readable.getReader(); - const writer = writable.getWriter(); - const init = { kind: 1, content: "hello" } satisfies EventInit<1>; - await writer.write(init); - const { value } = await reader.read(); - assertEquals(value, ["EVENT", signer.sign(init)]); - await writer.close(); - await reader.cancel(); - }, - ); - - // FIXME: runtime error - // @ts-ignore missing property `ignore` - it.ignore("should be connectable to a relay", () => { - relay = new Relay("wss://example.com"); - // const writable = pipeThroughFrom(relay.writable, publisher); - // assertInstanceOf(writable, WritableStream); - // await writable.close(); - }); -}); diff --git a/std/mod.ts b/std/mod.ts index 8573389..4a528f0 100644 --- a/std/mod.ts +++ b/std/mod.ts @@ -1,6 +1,4 @@ export * from "./env.ts"; -export * from "./events.ts"; -export * from "./notes.ts"; export * from "./times.ts"; export * from "./signs.ts"; export * from "./pools.ts"; diff --git a/std/notes.ts b/std/notes.ts index 0cca312..0b20bac 100644 --- a/std/notes.ts +++ b/std/notes.ts @@ -1,10 +1,9 @@ -import type { Optional } from "@lophus/lib/types"; import type { NostrEvent, RelayUrl } from "@lophus/core/protocol"; -import { type EventInit } from "@lophus/std/events"; +import { type EventInit } from "@lophus/std/signs"; export type TextNote = EventInit<1>; -export type TextNoteInit = Optional; +export type TextNoteInit = Omit; export class TextNoteComposer extends TransformStream { constructor(readonly opts: { recommendedRelay?: RelayUrl } = {}) { diff --git a/std/signs.ts b/std/signs.ts index 1cfc96c..5691365 100644 --- a/std/signs.ts +++ b/std/signs.ts @@ -1,33 +1,35 @@ import { schnorr } from "@noble/curves/secp256k1"; import { sha256 } from "@noble/hashes/sha256"; import { bytesToHex } from "@noble/hashes/utils"; -import type { Brand, Stringified } from "@lophus/lib/types"; +import type { Stringified } from "@lophus/lib/types"; import type { EventContent, EventId, EventKind, EventSerializePrecursor, NostrEvent, + PrivateKey, + PublicKey, Signature, + UnsignedEvent, } from "@lophus/core/protocol"; -import type { UnsignedEvent } from "../nips/07/protocol.ts"; -import type { EventInit } from "@lophus/std/events"; +import "@lophus/nips/01"; +import "@lophus/nips/07"; import { Timestamp } from "@lophus/std/times"; -export type { UnsignedEvent } from "../nips/07/protocol.ts"; - -export type PrivateKey = Brand; - -export const PrivateKey = { - generate: () => bytesToHex(schnorr.utils.randomPrivateKey()) as PrivateKey, -}; +export function generatePrivateKey(): PrivateKey { + return bytesToHex(schnorr.utils.randomPrivateKey()) as PrivateKey; +} -export type PublicKey = Brand; +export function fromPrivateKey(nsec: PrivateKey): PublicKey { + return bytesToHex(schnorr.getPublicKey(nsec)) as PublicKey; +} -export const PublicKey = { - from: (nsec: PrivateKey) => - bytesToHex(schnorr.getPublicKey(nsec)) as PublicKey, -}; +export interface EventInit { + kind: NostrEvent["kind"]; + tags?: NostrEvent["tags"]; + content: EventContent | Stringified>; +} /** * A transform stream that signs events. @@ -46,16 +48,19 @@ export class Signer extends TransformStream { sign(event: EventInit): NostrEvent { if (isSigned(event)) return event; const unsigned = { - created_at: Timestamp.now, tags: [], ...event, + created_at: Timestamp.now, content: JSON.stringify(event.content) as Stringified>, // TODO: Can we avoid this type assertion? } as UnsignedEvent; - const precursor = { ...unsigned, pubkey: PublicKey.from(this.nsec) }; - const hash = sha256(this.#encoder.encode(serialize(precursor))); + const pubkey = fromPrivateKey(this.nsec); + const hash = sha256( + this.#encoder.encode(serialize({ ...unsigned, pubkey })), + ); return { - ...precursor, + ...unsigned, + pubkey, id: bytesToHex(hash) as EventId, sig: bytesToHex(schnorr.sign(hash, this.nsec)) as Signature, }; @@ -112,3 +117,31 @@ function serialize( } type SerializedEvent = string & { __kind: K }; + +/** + * A transform stream that signs events with a NIP-07 extention. + */ +export class NIP07Signer extends TransformStream { + constructor() { + // deno-lint-ignore no-window + if (!window.nostr) { + throw new Error("NIP-07 extension not installed"); + } + super({ + transform: (init, controller) => { + controller.enqueue(this.sign(init)); + }, + }); + } + sign(init: EventInit): NostrEvent { + const unsigned = { + tags: [], + ...init, + created_at: Timestamp.now, + content: JSON.stringify(init.content) as Stringified>, + // TODO: Can we avoid this type assertion? + } as UnsignedEvent; + // deno-lint-ignore no-window + return window.nostr!.signEvent(unsigned); + } +} diff --git a/std/signs_test.ts b/std/signs_test.ts index 92ffa85..32d7df5 100644 --- a/std/signs_test.ts +++ b/std/signs_test.ts @@ -2,34 +2,33 @@ import { assert, assertEquals } from "@std/assert"; import { describe, it } from "@std/testing/bdd"; import { Stringified } from "@lophus/lib/types"; import { Timestamp } from "@lophus/std/times"; +import { UnsignedEvent } from "@lophus/core/protocol"; import { - PrivateKey, - PublicKey, + fromPrivateKey, + generatePrivateKey, Signer, - UnsignedEvent, Verifier, } from "./signs.ts"; -describe("PrivateKey", () => { +describe("generatePrivateKey", () => { it("generates a private key", () => { - const nsec = PrivateKey.generate(); + const nsec = generatePrivateKey(); assertEquals(nsec.length, 64); }); }); -describe("PublicKey", () => { +describe("fromPrivateKey", () => { it("generates a public key from a private key", () => { - const nsec = PrivateKey.generate(); - const pubkey = PublicKey.from(nsec); + const nsec = generatePrivateKey(); + const pubkey = fromPrivateKey(nsec); assertEquals(pubkey.length, 64); }); }); describe("Signer/Verifier", () => { - const nsec = PrivateKey.generate(); + const nsec = generatePrivateKey(); const signer = new Signer(nsec); const event: UnsignedEvent = { - pubkey: PublicKey.from(nsec), created_at: Timestamp.now, kind: 1, tags: [],