diff --git a/packages/app/src/Wallet/NostrWalletConnect.ts b/packages/app/src/Wallet/NostrWalletConnect.ts index 5930e60d..37bff9fc 100644 --- a/packages/app/src/Wallet/NostrWalletConnect.ts +++ b/packages/app/src/Wallet/NostrWalletConnect.ts @@ -80,15 +80,16 @@ export class NostrConnectWallet implements LNWallet { return await new Promise(resolve => { this.#conn = new Connection(this.#config.relayUrl, { read: true, write: true }); - this.#conn.OnConnected = () => resolve(true); - this.#conn.Auth = async (c, r) => { + this.#conn.on("connected", () => resolve(true)); + this.#conn.on("auth", async (c, r, cb) => { const eb = new EventBuilder(); eb.kind(EventKind.Auth).tag(["relay", r]).tag(["challenge", c]); - return await eb.buildAndSign(this.#config.secret); - }; - this.#conn.OnEvent = (s, e) => { + const ev = await eb.buildAndSign(this.#config.secret); + cb(ev); + }); + this.#conn.on("event", (s, e) => { this.#onReply(s, e); - }; + }); this.#conn.Connect(); }); } diff --git a/packages/app/src/index.tsx b/packages/app/src/index.tsx index 37f81645..239f060f 100644 --- a/packages/app/src/index.tsx +++ b/packages/app/src/index.tsx @@ -87,13 +87,14 @@ const System = new NostrSystem({ relayMetrics: RelayMetrics, queryOptimizer: hasWasm ? WasmQueryOptimizer : undefined, db: SystemDb, - authHandler: async (c, r) => { - const { id } = LoginStore.snapshot(); - const pub = LoginStore.getPublisher(id); - if (pub) { - return await pub.nip42Auth(c, r); - } - }, +}); + +System.on("auth", async (c, r, cb) => { + const { id } = LoginStore.snapshot(); + const pub = LoginStore.getPublisher(id); + if (pub) { + cb(await pub.nip42Auth(c, r)); + } }); async function fetchProfile(key: string) { diff --git a/packages/system/package.json b/packages/system/package.json index be341bc5..963531ae 100644 --- a/packages/system/package.json +++ b/packages/system/package.json @@ -36,6 +36,7 @@ "@snort/shared": "^1.0.7", "@stablelib/xchacha20": "^1.0.1", "debug": "^4.3.4", + "events": "^3.3.0", "isomorphic-ws": "^5.0.0", "uuid": "^9.0.0", "ws": "^8.14.0" diff --git a/packages/system/src/connection.ts b/packages/system/src/connection.ts index 33cb8301..a0382fca 100644 --- a/packages/system/src/connection.ts +++ b/packages/system/src/connection.ts @@ -8,8 +8,7 @@ import { ConnectionStats } from "./connection-stats"; import { NostrEvent, ReqCommand, ReqFilter, TaggedNostrEvent, u256 } from "./nostr"; import { RelayInfo } from "./relay-info"; import EventKind from "./event-kind"; - -export type AuthHandler = (challenge: string, relay: string) => Promise; +import EventEmitter from "events"; /** * Relay settings @@ -46,7 +45,22 @@ export interface ConnectionStateSnapshot { address: string; } -export class Connection extends ExternalStore { +interface ConnectionEvents { + change: (snapshot: ConnectionStateSnapshot) => void; + connected: (wasReconnect: boolean) => void; + event: (sub: string, e: TaggedNostrEvent) => void; + eose: (sub: string) => void; + disconnect: (code: number) => void; + auth: (challenge: string, relay: string, cb: (ev: NostrEvent) => void) => void; + notice: (msg: string) => void; +} + +export declare interface Connection { + on(event: U, listener: ConnectionEvents[U]): this; + once(event: U, listener: ConnectionEvents[U]): this; +} + +export class Connection extends EventEmitter { #log: debug.Debugger; #ephemeralCheck?: ReturnType; #activity: number = unixNowMs(); @@ -72,16 +86,12 @@ export class Connection extends ExternalStore { IsClosed: boolean; ReconnectTimer?: ReturnType; EventsCallback: Map) => void>; - OnConnected?: (wasReconnect: boolean) => void; - OnEvent?: (sub: string, e: TaggedNostrEvent) => void; - OnEose?: (sub: string) => void; - OnDisconnect?: (code: number) => void; - Auth?: AuthHandler; + AwaitingAuth: Map; Authed = false; Down = true; - constructor(addr: string, options: RelaySettings, auth?: AuthHandler, ephemeral: boolean = false) { + constructor(addr: string, options: RelaySettings, ephemeral: boolean = false) { super(); this.Id = uuid(); this.Address = addr; @@ -89,7 +99,6 @@ export class Connection extends ExternalStore { this.IsClosed = false; this.EventsCallback = new Map(); this.AwaitingAuth = new Map(); - this.Auth = auth; this.#ephemeral = ephemeral; this.#log = debug("Connection").extend(addr); } @@ -154,7 +163,7 @@ export class Connection extends ExternalStore { this.#log(`Open!`); this.Down = false; this.#setupEphemeral(); - this.OnConnected?.(wasReconnect); + this.emit("connected", wasReconnect); this.#sendPendingRaw(); } @@ -182,7 +191,7 @@ export class Connection extends ExternalStore { this.ReconnectTimer = undefined; } - this.OnDisconnect?.(e.code); + this.emit("disconnected", e.code); this.#reset(); this.notifyChange(); } @@ -206,7 +215,7 @@ export class Connection extends ExternalStore { break; } case "EVENT": { - this.OnEvent?.(msg[1] as string, { + this.emit("event", msg[1] as string, { ...(msg[2] as NostrEvent), relays: [this.Address], }); @@ -215,7 +224,7 @@ export class Connection extends ExternalStore { break; } case "EOSE": { - this.OnEose?.(msg[1] as string); + this.emit("eose", msg[1] as string); break; } case "OK": { @@ -230,6 +239,7 @@ export class Connection extends ExternalStore { break; } case "NOTICE": { + this.emit("notice", msg[1]); this.#log(`NOTICE: ${msg[1]}`); break; } @@ -346,7 +356,7 @@ export class Connection extends ExternalStore { CloseReq(id: string) { if (this.ActiveRequests.delete(id)) { this.#sendJson(["CLOSE", id]); - this.OnEose?.(id); + this.emit("eose", id); this.#SendQueuedRequests(); } this.notifyChange(); @@ -435,11 +445,11 @@ export class Connection extends ExternalStore { const authCleanup = () => { this.AwaitingAuth.delete(challenge); }; - if (!this.Auth) { - throw new Error("Auth hook not registered"); - } this.AwaitingAuth.set(challenge, true); - const authEvent = await this.Auth(challenge, this.Address); + const authEvent = await new Promise((resolve, reject) => + this.emit("auth", challenge, this.Address, resolve), + ); + this.#log("Auth result: %o", authEvent); if (!authEvent) { authCleanup(); throw new Error("No auth event"); @@ -486,4 +496,8 @@ export class Connection extends ExternalStore { }, 5_000); } } + + notifyChange() { + this.emit("change", this.takeSnapshot()); + } } diff --git a/packages/system/src/impl/nip46.ts b/packages/system/src/impl/nip46.ts index 78f5de62..eef4f75e 100644 --- a/packages/system/src/impl/nip46.ts +++ b/packages/system/src/impl/nip46.ts @@ -85,10 +85,10 @@ export class Nip46Signer implements EventSigner { } return await new Promise((resolve, reject) => { this.#conn = new Connection(this.#relay, { read: true, write: true }); - this.#conn.OnEvent = async (sub, e) => { + this.#conn.on("event", async (sub, e) => { await this.#onReply(e); - }; - this.#conn.OnConnected = async () => { + }); + this.#conn.on("connected", async () => { this.#conn!.QueueReq( [ "REQ", @@ -110,7 +110,7 @@ export class Nip46Signer implements EventSigner { resolve, }); } - }; + }); this.#conn.Connect(); this.#didInit = true; }); diff --git a/packages/system/src/index.ts b/packages/system/src/index.ts index 51b26af0..25def55e 100644 --- a/packages/system/src/index.ts +++ b/packages/system/src/index.ts @@ -1,4 +1,4 @@ -import { AuthHandler, RelaySettings, ConnectionStateSnapshot, OkResponse } from "./connection"; +import { RelaySettings, ConnectionStateSnapshot, OkResponse } from "./connection"; import { RequestBuilder } from "./request-builder"; import { NoteStore, NoteStoreSnapshotData } from "./note-collection"; import { Query } from "./query"; @@ -46,11 +46,6 @@ export interface SystemInterface { */ checkSigs: boolean; - /** - * Handler function for NIP-42 - */ - HandleAuth?: AuthHandler; - /** * Get a snapshot of the relay connections */ diff --git a/packages/system/src/nostr-system.ts b/packages/system/src/nostr-system.ts index 83529d40..fb8da8b0 100644 --- a/packages/system/src/nostr-system.ts +++ b/packages/system/src/nostr-system.ts @@ -1,8 +1,9 @@ import debug from "debug"; +import EventEmitter from "events"; -import { unwrap, sanitizeRelayUrl, ExternalStore, FeedCache, removeUndefined } from "@snort/shared"; +import { unwrap, sanitizeRelayUrl, FeedCache, removeUndefined } from "@snort/shared"; import { NostrEvent, TaggedNostrEvent } from "./nostr"; -import { AuthHandler, Connection, RelaySettings, ConnectionStateSnapshot, OkResponse } from "./connection"; +import { Connection, RelaySettings, ConnectionStateSnapshot, OkResponse } from "./connection"; import { Query } from "./query"; import { NoteCollection, NoteStore, NoteStoreSnapshotData } from "./note-collection"; import { BuiltRawReqFilter, RequestBuilder, RequestStrategy } from "./request-builder"; @@ -25,10 +26,20 @@ import { RelayCache } from "./gossip-model"; import { QueryOptimizer, DefaultQueryOptimizer } from "./query-optimizer"; import { trimFilters } from "./request-trim"; +interface NostrSystemEvents { + change: (state: SystemSnapshot) => void; + auth: (challenge: string, relay: string, cb: (ev: NostrEvent) => void) => void; +} + +export declare interface NostrSystem { + on(event: U, listener: NostrSystemEvents[U]): this; + once(event: U, listener: NostrSystemEvents[U]): this; +} + /** * Manages nostr content retrieval system */ -export class NostrSystem extends ExternalStore implements SystemInterface { +export class NostrSystem extends EventEmitter implements SystemInterface { #log = debug("System"); /** @@ -41,11 +52,6 @@ export class NostrSystem extends ExternalStore implements System */ Queries: Map = new Map(); - /** - * NIP-42 Auth handler - */ - #handleAuth?: AuthHandler; - /** * Storage class for user relay lists */ @@ -87,7 +93,6 @@ export class NostrSystem extends ExternalStore implements System checkSigs: boolean; constructor(props: { - authHandler?: AuthHandler; relayCache?: FeedCache; profileCache?: FeedCache; relayMetrics?: FeedCache; @@ -97,7 +102,6 @@ export class NostrSystem extends ExternalStore implements System checkSigs?: boolean; }) { super(); - this.#handleAuth = props.authHandler; this.#relayCache = props.relayCache ?? new UserRelaysCache(props.db?.userRelays); this.#profileCache = props.profileCache ?? new UserProfileCache(props.db?.users); this.#relayMetricsCache = props.relayMetrics ?? new RelayMetricCache(props.db?.relayMetrics); @@ -110,14 +114,12 @@ export class NostrSystem extends ExternalStore implements System this.#cleanup(); } - HandleAuth?: AuthHandler | undefined; - get ProfileLoader() { return this.#profileLoader; } get Sockets(): ConnectionStateSnapshot[] { - return [...this.#sockets.values()].map(a => a.snapshot()); + return [...this.#sockets.values()].map(a => a.takeSnapshot()); } get RelayCache(): RelayCache { @@ -149,12 +151,12 @@ export class NostrSystem extends ExternalStore implements System const addr = unwrap(sanitizeRelayUrl(address)); const existing = this.#sockets.get(addr); if (!existing) { - const c = new Connection(addr, options, this.#handleAuth?.bind(this)); + const c = new Connection(addr, options); this.#sockets.set(addr, c); - c.OnEvent = (s, e) => this.#onEvent(s, e); - c.OnEose = s => this.#onEndOfStoredEvents(c, s); - c.OnDisconnect = code => this.#onRelayDisconnect(c, code); - c.OnConnected = r => this.#onRelayConnected(c, r); + c.on("event", (s, e) => this.#onEvent(s, e)); + c.on("eose", s => this.#onEndOfStoredEvents(c, s)); + c.on("disconnect", code => this.#onRelayDisconnect(c, code)); + c.on("connected", r => this.#onRelayConnected(c, r)); await c.Connect(); } else { // update settings if already connected @@ -210,12 +212,12 @@ export class NostrSystem extends ExternalStore implements System try { const addr = unwrap(sanitizeRelayUrl(address)); if (!this.#sockets.has(addr)) { - const c = new Connection(addr, { read: true, write: true }, this.#handleAuth?.bind(this), true); + const c = new Connection(addr, { read: true, write: true }, true); this.#sockets.set(addr, c); - c.OnEvent = (s, e) => this.#onEvent(s, e); - c.OnEose = s => this.#onEndOfStoredEvents(c, s); - c.OnDisconnect = code => this.#onRelayDisconnect(c, code); - c.OnConnected = r => this.#onRelayConnected(c, r); + c.on("event", (s, e) => this.#onEvent(s, e)); + c.on("eose", s => this.#onEndOfStoredEvents(c, s)); + c.on("disconnect", code => this.#onRelayDisconnect(c, code)); + c.on("connected", r => this.#onRelayConnected(c, r)); await c.Connect(); return c; } @@ -404,15 +406,15 @@ export class NostrSystem extends ExternalStore implements System return await existing.SendAsync(ev); } else { return await new Promise((resolve, reject) => { - const c = new Connection(address, { write: true, read: true }, this.#handleAuth?.bind(this), true); + const c = new Connection(address, { write: true, read: true }, true); const t = setTimeout(reject, 10_000); - c.OnConnected = async () => { + c.once("connected", async () => { clearTimeout(t); const rsp = await c.SendAsync(ev); c.Close(); resolve(rsp); - }; + }); c.Connect(); }); } @@ -430,6 +432,10 @@ export class NostrSystem extends ExternalStore implements System }; } + notifyChange() { + this.emit("change", this.takeSnapshot()); + } + #cleanup() { let changed = false; for (const [k, v] of this.Queries) { diff --git a/yarn.lock b/yarn.lock index e8faead9..0ddccac5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3243,6 +3243,7 @@ __metadata: "@types/uuid": ^9.0.2 "@types/ws": ^8.5.5 debug: ^4.3.4 + events: ^3.3.0 isomorphic-ws: ^5.0.0 jest: ^29.5.0 jest-environment-jsdom: ^29.5.0