nostr
package: vastly simplify the API (#412)
* vastly simplify the api * add missing await * add eose to emitter * add eose to conn * add eose to the client * eose test * improve test suite, add dm tests * demonstrate that nostr-rs-relay auth options don't work * readme files * cleanup * fetch relay info * test readyState * export fetchRelayInfo * cleanup * better async/await linting * use strictEqual in tests * additional eslint rules * allow arbitrary extensions * saner error handling * update README * implement nip-02 --------- Co-authored-by: Kieran <kieran@harkin.me>
This commit is contained in:
212
packages/nostr/src/event/index.ts
Normal file
212
packages/nostr/src/event/index.ts
Normal file
@ -0,0 +1,212 @@
|
||||
import {
|
||||
PublicKey,
|
||||
sha256,
|
||||
schnorrSign,
|
||||
schnorrVerify,
|
||||
getPublicKey,
|
||||
HexOrBechPrivateKey,
|
||||
parsePrivateKey,
|
||||
} from "../crypto"
|
||||
import { Timestamp, unixTimestamp, NostrError } from "../common"
|
||||
import { TextNote } from "./text"
|
||||
import { getUserMetadata, SetMetadata } from "./set-metadata"
|
||||
import {
|
||||
DirectMessage,
|
||||
getMessage,
|
||||
getPrevious,
|
||||
getRecipient,
|
||||
} from "./direct-message"
|
||||
import { ContactList, getContacts } from "./contact-list"
|
||||
|
||||
// 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.DirectMessage
|
||||
| EventKind.ContactList
|
||||
>
|
||||
}
|
||||
|
||||
export type Event =
|
||||
| SetMetadata
|
||||
| TextNote
|
||||
| ContactList
|
||||
| DirectMessage
|
||||
| Unknown
|
||||
|
||||
/**
|
||||
* Event ID encoded as hex.
|
||||
*/
|
||||
export type EventId = string
|
||||
|
||||
/**
|
||||
* An unsigned event.
|
||||
*/
|
||||
export type Unsigned<T extends Event | RawEvent> = {
|
||||
[Property in keyof UnsignedWithPubkey<T> as Exclude<
|
||||
Property,
|
||||
"pubkey"
|
||||
>]: T[Property]
|
||||
} & {
|
||||
pubkey?: PublicKey
|
||||
}
|
||||
|
||||
// TODO This doesn't need to be exposed by the lib
|
||||
/**
|
||||
* Same as @see {@link Unsigned}, but with the pubkey field.
|
||||
*/
|
||||
export type UnsignedWithPubkey<T extends Event | RawEvent> = {
|
||||
[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<T extends Event | RawEvent>(
|
||||
event: Unsigned<T>,
|
||||
priv?: HexOrBechPrivateKey
|
||||
): Promise<T> {
|
||||
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<T>
|
||||
)
|
||||
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<Event> {
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
if (event.kind === EventKind.DirectMessage) {
|
||||
return {
|
||||
...event,
|
||||
kind: EventKind.DirectMessage,
|
||||
getMessage,
|
||||
getRecipient,
|
||||
getPrevious,
|
||||
}
|
||||
}
|
||||
|
||||
if (event.kind === EventKind.ContactList) {
|
||||
return {
|
||||
...event,
|
||||
kind: EventKind.ContactList,
|
||||
getContacts,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...event,
|
||||
kind: event.kind,
|
||||
}
|
||||
}
|
||||
|
||||
async function serializeEventId(
|
||||
event: UnsignedWithPubkey<RawEvent>
|
||||
): Promise<EventId> {
|
||||
// It's not defined whether JSON.stringify produces a string with whitespace stripped.
|
||||
// Building the JSON string manually as follows 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 = `[${event.tags
|
||||
.map((tag) => `[${tag.map((v) => `"${v}"`).join(",")}]`)
|
||||
.join(",")}]`
|
||||
const serialized = `[0,"${event.pubkey}",${event.created_at},${event.kind},${serializedTags},"${event.content}"]`
|
||||
return await sha256(Uint8Array.from(charCodes(serialized)))
|
||||
}
|
||||
|
||||
function* charCodes(data: string): Iterable<number> {
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
yield data.charCodeAt(i)
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user