nostr package part 1

This commit is contained in:
ennmichael
2023-02-15 21:46:05 +01:00
parent 20f2f40549
commit 0193436d96
23 changed files with 511 additions and 86 deletions

174
packages/nostr/src/event.ts Normal file
View File

@ -0,0 +1,174 @@
import { ProtocolError } from "./error"
import { RawEvent } from "./raw"
import * as secp from "@noble/secp256k1"
import { PublicKey } from "./keypair"
import { parseHex } from "./util"
// TODO This file is missing proper documentation
// TODO Add remaining event types
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
}
export type Event = SetMetadataEvent | TextNoteEvent | UnknownEvent
interface EventCommon {
pubkey: PublicKey
createdAt: Date
}
export interface SetMetadataEvent extends EventCommon {
kind: EventKind.SetMetadata
userMetadata: UserMetadata
}
export interface UserMetadata {
name: string
about: string
picture: string
}
export interface TextNoteEvent extends EventCommon {
kind: EventKind.TextNote
note: string
}
export interface UnknownEvent extends EventCommon {
kind: Exclude<EventKind, EventKind.SetMetadata | EventKind.TextNote>
}
export async function createEvent(raw: RawEvent): Promise<Event> {
const pubkey = new PublicKey(raw.pubkey)
const createdAt = new Date(raw.created_at * 1000)
const event = {
pubkey,
createdAt,
}
await checkSignature(raw, event)
return (
createSetMetadataEvent(raw, event) ??
createTextNodeEvent(raw, event) ??
createUnknownEvent(raw, event)
)
}
function createSetMetadataEvent(
raw: RawEvent,
event: EventCommon
): SetMetadataEvent | undefined {
if (raw.kind !== EventKind.SetMetadata) {
return undefined
}
const userMetadata = parseJson(raw.content)
if (
typeof userMetadata["name"] !== "string" ||
typeof userMetadata["about"] !== "string" ||
typeof userMetadata["picture"] !== "string"
) {
throw new ProtocolError(`invalid user metadata: ${userMetadata}`)
}
return {
...event,
kind: EventKind.SetMetadata,
userMetadata,
}
}
function createTextNodeEvent(
raw: RawEvent,
event: EventCommon
): TextNoteEvent | undefined {
if (raw.kind !== EventKind.TextNote) {
return undefined
}
return {
...event,
kind: EventKind.TextNote,
note: raw.content,
}
}
function createUnknownEvent(raw: RawEvent, event: EventCommon): UnknownEvent {
return {
...event,
kind: raw.kind,
}
}
export class EventId {
#hex: string
constructor(hex: string | Uint8Array) {
this.#hex = parseHex(hex)
if (this.#hex.length !== 128) {
throw new ProtocolError(`invalid event id: ${this.#hex}`)
}
}
toString(): string {
return this.#hex
}
}
async function checkSignature(
raw: RawEvent,
event: EventCommon
): Promise<void> {
const id = serializeId(raw)
const bytes = await secp.schnorr.sign(id.toString(), event.pubkey.toString())
const hex = secp.utils.bytesToHex(bytes).toLowerCase()
if (hex.toString() !== raw.sig) {
throw new ProtocolError("invalid signature: ${hex}")
}
}
async function serializeId(raw: RawEvent): Promise<EventId> {
// It's not defined whether JSON.stringify produces a string with whitespace stripped.
// Building the JSON string manually this way ensures that there's no whitespace.
// In hindsight using JSON as a data format for hashing and signing is not the best
// design decision.
const serializedTags = `[${raw.tags
.map((tag) => `[${tag.map((v) => `"${v}"`).join(",")}]`)
.join(",")}]`
const serialized = `[0,"${raw.pubkey}",${raw.created_at},${raw.kind},${serializedTags},"${raw.content}"]`
const hash = await secp.utils.sha256(Uint8Array.from(charCodes(serialized)))
return new EventId(secp.utils.bytesToHex(hash).toLowerCase())
}
function parseJson(data: string) {
try {
return JSON.parse(data)
} catch (e) {
throw new ProtocolError(`invalid json: ${data}`)
}
}
function* charCodes(data: string): Iterable<number> {
for (let i = 0; i < length; i++) {
yield data.charCodeAt(i)
}
}
// TODO This is an example of how this API can be used, remove this later
function isItNice(e: Event): void {
if (e.kind === EventKind.SetMetadata) {
console.log(e.userMetadata)
} else if (e.kind === EventKind.TextNote) {
console.log(e.note)
}
}