import { PublicKey, sha256, schnorrSign, schnorrVerify, getPublicKey, HexOrBechPrivateKey, parsePrivateKey, } from "../crypto" import { Timestamp, unixTimestamp, NostrError } from "../common" import { TextNote } from "./text" import { getUserMetadata, SetMetadata, verifyInternetIdentifier, } from "./set-metadata" import { DirectMessage, getMessage, getPrevious, getRecipient, } from "./direct-message" import { ContactList, getContacts } from "./contact-list" import { Deletion, getEvents } from "./deletion" // TODO Add remaining event types // TODO // Think about this more // Perhaps the best option is for all these factory methods to have an overload which also accept a private // key as last parameter and return the event already signed // Or maybe opts: { sign?: boolean | HexOrBechPrivateKey } setting sign to true should use nip07, setting // it to a string will use that string as the private key export enum EventKind { SetMetadata = 0, // NIP-01 TextNote = 1, // NIP-01 RecommendServer = 2, // NIP-01 ContactList = 3, // NIP-02 DirectMessage = 4, // NIP-04 Deletion = 5, // NIP-09 Repost = 6, // NIP-18 Reaction = 7, // NIP-25 Relays = 10002, // NIP-65 Auth = 22242, // NIP-42 PubkeyLists = 30000, // NIP-51a NoteLists = 30001, // NIP-51b TagLists = 30002, // NIP-51c ZapRequest = 9734, // NIP 57 ZapReceipt = 9735, // NIP 57 } /** * A nostr event in the format that's sent across the wire. */ export interface RawEvent { id: string pubkey: PublicKey created_at: Timestamp kind: EventKind tags: string[][] content: string sig: string [key: string]: unknown } export interface Unknown extends RawEvent { kind: Exclude< EventKind, | EventKind.SetMetadata | EventKind.TextNote | EventKind.ContactList | EventKind.DirectMessage | EventKind.Deletion > } export type Event = | SetMetadata | TextNote | ContactList | DirectMessage | Deletion | Unknown /** * Event ID encoded as hex. */ export type EventId = string /** * An unsigned event. */ export type Unsigned = { [Property in keyof UnsignedWithPubkey as Exclude< Property, "pubkey" >]: T[Property] } & { pubkey?: PublicKey } /** * Same as @see {@link Unsigned}, but with the pubkey field. */ type UnsignedWithPubkey = { [Property in keyof T as Exclude< Property, "id" | "sig" | "created_at" >]: T[Property] } & { id?: EventId sig?: string created_at?: number } /** * Add the "id," "sig," and "pubkey" fields to the event. Set "created_at" to the current timestamp * if missing. Return the event. */ export async function signEvent( event: Unsigned, priv?: HexOrBechPrivateKey ): Promise { event.created_at ??= unixTimestamp() if (priv !== undefined) { priv = parsePrivateKey(priv) event.pubkey = getPublicKey(priv) const id = await serializeEventId( // This conversion is safe because the pubkey field is set above. event as unknown as UnsignedWithPubkey ) event.id = id event.sig = await schnorrSign(id, priv) return event as T } else { // TODO Try to use NIP-07, otherwise throw throw new NostrError("todo") } } /** * Parse an event from its raw format. */ export async function parseEvent(event: RawEvent): Promise { if (event.id !== (await serializeEventId(event))) { throw new NostrError( `invalid id ${event.id} for event ${JSON.stringify( event )}, expected ${await serializeEventId(event)}` ) } if (!(await schnorrVerify(event.sig, event.id, event.pubkey))) { throw new NostrError(`invalid signature for event ${JSON.stringify(event)}`) } // TODO Validate all the fields. Lowercase hex fields, etc. Make sure everything is correct. // TODO Also validate that tags have at least one element if (event.kind === EventKind.TextNote) { return { ...event, kind: EventKind.TextNote, } } if (event.kind === EventKind.SetMetadata) { return { ...event, kind: EventKind.SetMetadata, getUserMetadata, verifyInternetIdentifier, } } if (event.kind === EventKind.DirectMessage) { return { ...event, kind: EventKind.DirectMessage, getMessage, getRecipient, getPrevious, } } if (event.kind === EventKind.ContactList) { return { ...event, kind: EventKind.ContactList, getContacts, } } if (event.kind === EventKind.Deletion) { return { ...event, kind: EventKind.Deletion, getEvents, } } return { ...event, kind: event.kind, } } async function serializeEventId( event: UnsignedWithPubkey ): Promise { const serialized = JSON.stringify([ 0, event.pubkey, event.created_at, event.kind, event.tags, event.content, ]) return await sha256(Uint8Array.from(charCodes(serialized))) } function* charCodes(data: string): Iterable { for (let i = 0; i < data.length; i++) { yield data.charCodeAt(i) } }