diff --git a/packages/nostr/src/index.ts b/packages/nostr/src/index.ts index 2e897582..5a4d885e 100644 --- a/packages/nostr/src/index.ts +++ b/packages/nostr/src/index.ts @@ -1,4 +1,3 @@ -export * from "./legacy" import "./nostr-object" // TODO This file should only contain re-exports and only re-export what is needed diff --git a/packages/nostr/src/legacy/.prettierrc b/packages/nostr/src/legacy/.prettierrc deleted file mode 100644 index 0967ef42..00000000 --- a/packages/nostr/src/legacy/.prettierrc +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/packages/nostr/src/legacy/Connection.ts b/packages/nostr/src/legacy/Connection.ts deleted file mode 100644 index c9eff0c9..00000000 --- a/packages/nostr/src/legacy/Connection.ts +++ /dev/null @@ -1,463 +0,0 @@ -import { v4 as uuid } from "uuid"; - -import { DefaultConnectTimeout } from "./Const"; -import { ConnectionStats } from "./ConnectionStats"; -import { RawEvent, ReqCommand, TaggedRawEvent, u256 } from "./index"; -import { RelayInfo } from "./RelayInfo"; -import { unwrap } from "./Util"; - -export type CustomHook = (state: Readonly) => void; -export type AuthHandler = ( - challenge: string, - relay: string -) => Promise; - -/** - * Relay settings - */ -export interface RelaySettings { - read: boolean; - write: boolean; -} - -/** - * Snapshot of connection stats - */ -export interface StateSnapshot { - connected: boolean; - disconnects: number; - avgLatency: number; - events: { - received: number; - send: number; - }; - info?: RelayInfo; - pendingRequests: Array; - activeRequests: Array; - id: string; -} - -export class Connection { - Id: string; - Address: string; - Socket: WebSocket | null = null; - - PendingRaw: Array = []; - PendingRequests: Array<{ - cmd: ReqCommand, - cb: () => void - }> = []; - ActiveRequests = new Set(); - - Settings: RelaySettings; - Info?: RelayInfo; - ConnectTimeout: number = DefaultConnectTimeout; - Stats: ConnectionStats = new ConnectionStats(); - StateHooks: Map = new Map(); - HasStateChange: boolean = true; - CurrentState: StateSnapshot; - LastState: Readonly; - IsClosed: boolean; - ReconnectTimer: ReturnType | null; - EventsCallback: Map void>; - OnConnected?: () => void; - OnEvent?: (sub: string, e: TaggedRawEvent) => void; - OnEose?: (sub: string) => void; - OnDisconnect?: (id: string) => void; - Auth?: AuthHandler; - AwaitingAuth: Map; - Authed = false; - Ephemeral: boolean; - EphemeralTimeout: ReturnType | undefined; - Down = true; - - constructor( - addr: string, - options: RelaySettings, - auth?: AuthHandler, - ephemeral: boolean = false - ) { - this.Id = uuid(); - this.Address = addr; - this.Settings = options; - this.CurrentState = { - connected: false, - disconnects: 0, - avgLatency: 0, - events: { - received: 0, - send: 0, - }, - } as StateSnapshot; - this.LastState = Object.freeze({ ...this.CurrentState }); - this.IsClosed = false; - this.ReconnectTimer = null; - this.EventsCallback = new Map(); - this.AwaitingAuth = new Map(); - this.Auth = auth; - this.Ephemeral = ephemeral; - } - - ResetEphemeralTimeout() { - if (this.EphemeralTimeout) { - clearTimeout(this.EphemeralTimeout); - } - if (this.Ephemeral) { - this.EphemeralTimeout = setTimeout(() => { - this.Close(); - }, 30_000); - } - } - - async Connect() { - try { - if (this.Info === undefined) { - const u = new URL(this.Address); - const rsp = await fetch( - `${u.protocol === "wss:" ? "https:" : "http:"}//${u.host}`, - { - headers: { - accept: "application/nostr+json", - }, - } - ); - if (rsp.ok) { - const data = await rsp.json(); - for (const [k, v] of Object.entries(data)) { - if (v === "unset" || v === "" || v === "~") { - data[k] = undefined; - } - } - this.Info = data; - } - } - } catch (e) { - console.warn("Could not load relay information", e); - } - - if (this.Socket) { - this.Id = uuid(); - this.Socket.onopen = null; - this.Socket.onmessage = null; - this.Socket.onerror = null; - this.Socket.onclose = null; - } - this.IsClosed = false; - this.Socket = new WebSocket(this.Address); - this.Socket.onopen = () => this.OnOpen(); - this.Socket.onmessage = (e) => this.OnMessage(e); - this.Socket.onerror = (e) => this.OnError(e); - this.Socket.onclose = (e) => this.OnClose(e); - } - - Close() { - this.IsClosed = true; - if (this.ReconnectTimer !== null) { - clearTimeout(this.ReconnectTimer); - this.ReconnectTimer = null; - } - this.Socket?.close(); - this.#UpdateState(); - } - - OnOpen() { - this.ConnectTimeout = DefaultConnectTimeout; - console.log(`[${this.Address}] Open!`); - this.Down = false; - if (this.Ephemeral) { - this.ResetEphemeralTimeout(); - } - this.OnConnected?.(); - this.#sendPendingRaw(); - } - - OnClose(e: CloseEvent) { - if (!this.IsClosed) { - this.ConnectTimeout = this.ConnectTimeout * 2; - console.log( - `[${this.Address}] Closed (${e.reason}), trying again in ${( - this.ConnectTimeout / 1000 - ) - .toFixed(0) - .toLocaleString()} sec` - ); - this.ReconnectTimer = setTimeout(() => { - this.Connect(); - }, this.ConnectTimeout); - this.Stats.Disconnects++; - } else { - console.log(`[${this.Address}] Closed!`); - this.ReconnectTimer = null; - } - - this.OnDisconnect?.(this.Id); - this.#ResetQueues(); - // reset connection Id on disconnect, for query-tracking - this.Id = uuid(); - this.#UpdateState(); - } - - OnMessage(e: MessageEvent) { - if (e.data.length > 0) { - const msg = JSON.parse(e.data); - const tag = msg[0]; - switch (tag) { - case "AUTH": { - this._OnAuthAsync(msg[1]) - .then(() => this.#sendPendingRaw()) - .catch(console.error); - this.Stats.EventsReceived++; - this.#UpdateState(); - break; - } - case "EVENT": { - this.OnEvent?.(msg[1], { - ...msg[2], - relays: [this.Address] - }); - this.Stats.EventsReceived++; - this.#UpdateState(); - break; - } - case "EOSE": { - this.OnEose?.(msg[1]); - break; - } - case "OK": { - // feedback to broadcast call - console.debug(`${this.Address} OK: `, msg); - const id = msg[1]; - if (this.EventsCallback.has(id)) { - const cb = unwrap(this.EventsCallback.get(id)); - this.EventsCallback.delete(id); - cb(msg); - } - break; - } - case "NOTICE": { - console.warn(`[${this.Address}] NOTICE: ${msg[1]}`); - break; - } - default: { - console.warn(`Unknown tag: ${tag}`); - break; - } - } - } - } - - OnError(e: Event) { - console.error(e); - this.#UpdateState(); - } - - /** - * Send event on this connection - */ - SendEvent(e: RawEvent) { - if (!this.Settings.write) { - return; - } - const req = ["EVENT", e]; - this.#SendJson(req); - this.Stats.EventsSent++; - this.#UpdateState(); - } - - /** - * Send event on this connection and wait for OK response - */ - async SendAsync(e: RawEvent, timeout = 5000) { - return new Promise((resolve) => { - if (!this.Settings.write) { - resolve(); - return; - } - const t = setTimeout(() => { - resolve(); - }, timeout); - this.EventsCallback.set(e.id, () => { - clearTimeout(t); - resolve(); - }); - - const req = ["EVENT", e]; - this.#SendJson(req); - this.Stats.EventsSent++; - this.#UpdateState(); - }); - } - - /** - * Hook status for connection - */ - StatusHook(fnHook: CustomHook) { - const id = uuid(); - this.StateHooks.set(id, fnHook); - return () => { - this.StateHooks.delete(id); - }; - } - - /** - * Returns the current state of this connection - */ - GetState() { - if (this.HasStateChange) { - this.LastState = Object.freeze({ ...this.CurrentState }); - this.HasStateChange = false; - } - return this.LastState; - } - - /** - * Using relay document to determine if this relay supports a feature - */ - SupportsNip(n: number) { - return this.Info?.supported_nips?.some((a) => a === n) ?? false; - } - - /** - * Queue or send command to the relay - * @param cmd The REQ to send to the server - */ - QueueReq(cmd: ReqCommand, cbSent: () => void) { - if (this.ActiveRequests.size >= this.#maxSubscriptions) { - this.PendingRequests.push({ - cmd, cb: cbSent - }); - console.debug("Queuing:", this.Address, cmd); - } else { - this.ActiveRequests.add(cmd[1]); - this.#SendJson(cmd); - cbSent(); - } - this.#UpdateState(); - } - - CloseReq(id: string) { - if (this.ActiveRequests.delete(id)) { - this.#SendJson(["CLOSE", id]); - this.OnEose?.(id); - this.#SendQueuedRequests(); - } - this.#UpdateState(); - } - - #SendQueuedRequests() { - const canSend = this.#maxSubscriptions - this.ActiveRequests.size; - if (canSend > 0) { - for (let x = 0; x < canSend; x++) { - const p = this.PendingRequests.shift(); - if (p) { - this.ActiveRequests.add(p.cmd[1]); - this.#SendJson(p.cmd); - p.cb(); - console.debug("Sent pending REQ", this.Address, p.cmd); - } - } - } - } - - #ResetQueues() { - this.ActiveRequests.clear(); - this.PendingRequests = []; - this.PendingRaw = []; - this.#UpdateState(); - } - - #UpdateState() { - this.CurrentState.connected = this.Socket?.readyState === WebSocket.OPEN; - this.CurrentState.events.received = this.Stats.EventsReceived; - this.CurrentState.events.send = this.Stats.EventsSent; - this.CurrentState.avgLatency = - this.Stats.Latency.length > 0 - ? this.Stats.Latency.reduce((acc, v) => acc + v, 0) / - this.Stats.Latency.length - : 0; - this.CurrentState.disconnects = this.Stats.Disconnects; - this.CurrentState.info = this.Info; - this.CurrentState.id = this.Id; - this.CurrentState.pendingRequests = [...this.PendingRequests.map(a => a.cmd[1])]; - this.CurrentState.activeRequests = [...this.ActiveRequests]; - this.Stats.Latency = this.Stats.Latency.slice(-20); // trim - this.HasStateChange = true; - this.#NotifyState(); - } - - #NotifyState() { - const state = this.GetState(); - for (const [, h] of this.StateHooks) { - h(state); - } - } - - #SendJson(obj: object) { - const authPending = !this.Authed && (this.AwaitingAuth.size > 0 || this.Info?.limitation?.auth_required === true); - if (this.Socket?.readyState !== WebSocket.OPEN || authPending) { - this.PendingRaw.push(obj); - if (this.Socket?.readyState === WebSocket.CLOSED && this.Ephemeral && this.IsClosed) { - this.Connect() - } - return false; - } - - this.#sendPendingRaw(); - this.#sendOnWire(obj); - } - - #sendPendingRaw() { - while (this.PendingRaw.length > 0) { - const next = this.PendingRaw.shift(); - if (next) { - this.#sendOnWire(next); - } - } - } - - #sendOnWire(obj: unknown) { - if (this.Socket?.readyState !== WebSocket.OPEN) { - throw new Error(`Socket is not open, state is ${this.Socket?.readyState}`); - } - const json = JSON.stringify(obj); - this.Socket.send(json); - return true; - } - - async _OnAuthAsync(challenge: string): Promise { - 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); - return new Promise((resolve) => { - if (!authEvent) { - authCleanup(); - return Promise.reject("no event"); - } - - const t = setTimeout(() => { - authCleanup(); - resolve(); - }, 10_000); - - this.EventsCallback.set(authEvent.id, (msg: boolean[]) => { - clearTimeout(t); - authCleanup(); - if (msg.length > 3 && msg[2] === true) { - this.Authed = true; - } - resolve(); - }); - - this.#sendOnWire(["AUTH", authEvent]); - }); - } - - get #maxSubscriptions() { - return this.Info?.limitation?.max_subscriptions ?? 25; - } -} diff --git a/packages/nostr/src/legacy/ConnectionStats.ts b/packages/nostr/src/legacy/ConnectionStats.ts deleted file mode 100644 index d1248f2d..00000000 --- a/packages/nostr/src/legacy/ConnectionStats.ts +++ /dev/null @@ -1,34 +0,0 @@ -/** - * Stats class for tracking metrics per connection - */ -export class ConnectionStats { - /** - * Last n records of how long between REQ->EOSE - */ - Latency: number[] = []; - - /** - * Total number of REQ's sent on this connection - */ - Subs: number = 0; - - /** - * Count of REQ which took too long and where abandoned - */ - SubsTimeout: number = 0; - - /** - * Total number of EVENT messages received - */ - EventsReceived: number = 0; - - /** - * Total number of EVENT messages sent - */ - EventsSent: number = 0; - - /** - * Total number of times this connection was lost - */ - Disconnects: number = 0; -} diff --git a/packages/nostr/src/legacy/Const.ts b/packages/nostr/src/legacy/Const.ts deleted file mode 100644 index e3502ce0..00000000 --- a/packages/nostr/src/legacy/Const.ts +++ /dev/null @@ -1,4 +0,0 @@ -/** - * Websocket re-connect timeout - */ -export const DefaultConnectTimeout = 2000; diff --git a/packages/nostr/src/legacy/EventKind.ts b/packages/nostr/src/legacy/EventKind.ts deleted file mode 100644 index 512a1f0a..00000000 --- a/packages/nostr/src/legacy/EventKind.ts +++ /dev/null @@ -1,29 +0,0 @@ -enum EventKind { - Unknown = -1, - SetMetadata = 0, - TextNote = 1, - RecommendServer = 2, - ContactList = 3, // NIP-02 - DirectMessage = 4, // NIP-04 - Deletion = 5, // NIP-09 - Repost = 6, // NIP-18 - Reaction = 7, // NIP-25 - BadgeAward = 8, // NIP-58 - SnortSubscriptions = 1000, // NIP-XX - Polls = 6969, // NIP-69 - FileHeader = 1063, // NIP-94 - Relays = 10002, // NIP-65 - Ephemeral = 20_000, - Auth = 22242, // NIP-42 - PubkeyLists = 30000, // NIP-51a - NoteLists = 30001, // NIP-51b - TagLists = 30002, // NIP-51c - Badge = 30009, // NIP-58 - ProfileBadges = 30008, // NIP-58 - ZapstrTrack = 31337, - ZapRequest = 9734, // NIP 57 - ZapReceipt = 9735, // NIP 57 - HttpAuthentication = 27235, // NIP XX - HTTP Authentication -} - -export default EventKind; diff --git a/packages/nostr/src/legacy/Links.ts b/packages/nostr/src/legacy/Links.ts deleted file mode 100644 index 8f3d4a35..00000000 --- a/packages/nostr/src/legacy/Links.ts +++ /dev/null @@ -1,88 +0,0 @@ -import * as utils from "@noble/curves/abstract/utils"; -import { bech32 } from "bech32"; -import { HexKey } from "."; - -export enum NostrPrefix { - PublicKey = "npub", - PrivateKey = "nsec", - Note = "note", - - // TLV prefixes - Profile = "nprofile", - Event = "nevent", - Relay = "nrelay", - Address = "naddr", -} - -export enum TLVEntryType { - Special = 0, - Relay = 1, - Author = 2, - Kind = 3, -} - -export interface TLVEntry { - type: TLVEntryType; - length: number; - value: string | HexKey | number; -} - -export function encodeTLV(prefix: NostrPrefix, id: string, relays?: string[], kind?: number, author?: string) { - const enc = new TextEncoder(); - const buf = prefix === NostrPrefix.Address ? enc.encode(id) : utils.hexToBytes(id); - - const tl0 = [0, buf.length, ...buf]; - const tl1 = - relays - ?.map((a) => { - const data = enc.encode(a); - return [1, data.length, ...data]; - }) - .flat() ?? []; - - const tl2 = author ? [2, 32, ...utils.hexToBytes(author)] : []; - const tl3 = kind ? [3, 4, ...new Uint8Array(new Uint32Array([kind]).buffer).reverse()] : [] - - return bech32.encode(prefix, bech32.toWords([...tl0, ...tl1, ...tl2, ...tl3]), 1_000); -} - -export function decodeTLV(str: string) { - const decoded = bech32.decode(str, 1_000); - const data = bech32.fromWords(decoded.words); - - const entries: TLVEntry[] = []; - let x = 0; - while (x < data.length) { - const t = data[x]; - const l = data[x + 1]; - const v = data.slice(x + 2, x + 2 + l); - entries.push({ - type: t, - length: l, - value: decodeTLVEntry(t, decoded.prefix, new Uint8Array(v)), - }); - x += 2 + l; - } - return entries; -} - -function decodeTLVEntry(type: TLVEntryType, prefix: string, data: Uint8Array) { - switch (type) { - case TLVEntryType.Special: { - if (prefix === NostrPrefix.Address) { - return new TextDecoder("ASCII").decode(data); - } else { - return utils.bytesToHex(data); - } - } - case TLVEntryType.Author: { - return utils.bytesToHex(data); - } - case TLVEntryType.Kind: { - return new Uint32Array(new Uint8Array(data.reverse()).buffer)[0]; - } - case TLVEntryType.Relay: { - return new TextDecoder("ASCII").decode(data); - } - } -} diff --git a/packages/nostr/src/legacy/Nips.ts b/packages/nostr/src/legacy/Nips.ts deleted file mode 100644 index decdfa3c..00000000 --- a/packages/nostr/src/legacy/Nips.ts +++ /dev/null @@ -1,3 +0,0 @@ -export enum Nips { - Search = 50, -} diff --git a/packages/nostr/src/legacy/RelayInfo.ts b/packages/nostr/src/legacy/RelayInfo.ts deleted file mode 100644 index 438fe41f..00000000 --- a/packages/nostr/src/legacy/RelayInfo.ts +++ /dev/null @@ -1,16 +0,0 @@ -export interface RelayInfo { - name?: string; - description?: string; - pubkey?: string; - contact?: string; - supported_nips?: number[]; - software?: string; - version?: string; - limitation?: { - payment_required: boolean; - max_subscriptions: number; - max_filters: number; - max_event_tags: number; - auth_required: boolean - }; -} diff --git a/packages/nostr/src/legacy/Tag.ts b/packages/nostr/src/legacy/Tag.ts deleted file mode 100644 index b3ecfb98..00000000 --- a/packages/nostr/src/legacy/Tag.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { HexKey, u256 } from "./index"; -import { unwrap } from "./Util"; - -export default class Tag { - Original: string[]; - Key: string; - Event?: u256; - PubKey?: HexKey; - Relay?: string; - Marker?: string; - Hashtag?: string; - DTag?: string; - ATag?: string; - Index: number; - Invalid: boolean; - LNURL?: string; - - constructor(tag: string[], index: number) { - this.Original = tag; - this.Key = tag[0]; - this.Index = index; - this.Invalid = false; - - switch (this.Key) { - case "e": { - // ["e", , , ] - this.Event = tag[1]; - this.Relay = tag.length > 2 ? tag[2] : undefined; - this.Marker = tag.length > 3 ? tag[3] : undefined; - if (!this.Event) { - this.Invalid = true; - } - break; - } - case "p": { - // ["p", ] - this.PubKey = tag[1]; - if (!this.PubKey) { - this.Invalid = true; - } - break; - } - case "d": { - this.DTag = tag[1]; - break; - } - case "a": { - this.ATag = tag[1]; - break; - } - case "t": { - this.Hashtag = tag[1]; - break; - } - case "delegation": { - this.PubKey = tag[1]; - break; - } - case "zap": { - this.LNURL = tag[1]; - break; - } - } - } - - ToObject(): string[] | null { - switch (this.Key) { - case "e": { - let ret = ["e", this.Event, this.Relay, this.Marker]; - const trimEnd = ret.reverse().findIndex((a) => a !== undefined); - ret = ret.reverse().slice(0, ret.length - trimEnd); - return ret; - } - case "p": { - return this.PubKey ? ["p", this.PubKey] : null; - } - case "t": { - return ["t", unwrap(this.Hashtag)]; - } - case "d": { - return ["d", unwrap(this.DTag)]; - } - default: { - return this.Original; - } - } - } -} diff --git a/packages/nostr/src/legacy/Util.ts b/packages/nostr/src/legacy/Util.ts deleted file mode 100644 index dcc7147c..00000000 --- a/packages/nostr/src/legacy/Util.ts +++ /dev/null @@ -1,34 +0,0 @@ -import * as utils from "@noble/curves/abstract/utils"; -import { bech32 } from "bech32"; - -export function unwrap(v: T | undefined | null): T { - if (v === undefined || v === null) { - throw new Error("missing value"); - } - return v; -} - -/** - * Convert hex to bech32 - */ -export function hexToBech32(hrp: string, hex?: string) { - if (typeof hex !== "string" || hex.length === 0 || hex.length % 2 !== 0) { - return ""; - } - - try { - const buf = utils.hexToBytes(hex); - return bech32.encode(hrp, bech32.toWords(buf)); - } catch (e) { - console.warn("Invalid hex", hex, e); - return ""; - } -} - -export function sanitizeRelayUrl(url: string) { - try { - return new URL(url).toString(); - } catch { - // ignore - } -} \ No newline at end of file diff --git a/packages/nostr/src/legacy/index.ts b/packages/nostr/src/legacy/index.ts deleted file mode 100644 index 61f9af03..00000000 --- a/packages/nostr/src/legacy/index.ts +++ /dev/null @@ -1,89 +0,0 @@ -export * from "./Connection"; -export { default as EventKind } from "./EventKind"; -export { default as Tag } from "./Tag"; -export * from "./Links"; -export * from "./Nips"; - -import { RelaySettings } from "."; -export type RawEvent = { - id: u256; - pubkey: HexKey; - created_at: number; - kind: number; - tags: Array>; - content: string; - sig: string; -}; - -export interface TaggedRawEvent extends RawEvent { - /** - * A list of relays this event was seen on - */ - relays: string[]; -} - -/** - * Basic raw key as hex - */ -export type HexKey = string; - -/** - * Optional HexKey - */ -export type MaybeHexKey = HexKey | undefined; - -/** - * A 256bit hex id - */ -export type u256 = string; - -export type ReqCommand = [cmd: "REQ", id: string, ...filters: Array]; - -/** - * Raw REQ filter object - */ -export type RawReqFilter = { - ids?: u256[]; - authors?: u256[]; - kinds?: number[]; - "#e"?: u256[]; - "#p"?: u256[]; - "#t"?: string[]; - "#d"?: string[]; - "#r"?: string[]; - search?: string; - since?: number; - until?: number; - limit?: number; -}; - -/** - * Medatadata event content - */ -export type UserMetadata = { - name?: string; - display_name?: string; - about?: string; - picture?: string; - website?: string; - banner?: string; - nip05?: string; - lud06?: string; - lud16?: string; -}; - -/** - * NIP-51 list types - */ -export enum Lists { - Muted = "mute", - Pinned = "pin", - Bookmarked = "bookmark", - Followed = "follow", - Badges = "profile_badges", -} - -export interface FullRelaySettings { - url: string; - settings: RelaySettings; -} diff --git a/packages/nostr/test/setup.ts b/packages/nostr/test/setup.ts index 0d096bbc..2a079c77 100644 --- a/packages/nostr/test/setup.ts +++ b/packages/nostr/test/setup.ts @@ -8,8 +8,7 @@ import { parsePublicKey, PublicKey, } from "../src/crypto" -import { RawEvent } from "../src" -import { signEvent, Unsigned } from "../src/event" +import { RawEvent, signEvent, Unsigned } from "../src/event" export const relayUrl = new URL("ws://localhost:12648")