nostr package part 1
This commit is contained in:
174
packages/nostr/src/event.ts
Normal file
174
packages/nostr/src/event.ts
Normal 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)
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user