EventId.ts raw
1 import { nip19 } from 'nostr-tools'
2 import { InvalidEventIdError } from '../errors'
3 import { Pubkey } from './Pubkey'
4 import { RelayUrl } from './RelayUrl'
5
6 /**
7 * Value object representing a Nostr event ID.
8 * Can be created from hex or bech32 (note1/nevent1) formats.
9 * Optionally includes relay hints and author information.
10 */
11 export class EventId {
12 private constructor(
13 private readonly _hex: string,
14 private readonly _kind?: number,
15 private readonly _author?: Pubkey,
16 private readonly _relayHints: RelayUrl[] = []
17 ) {}
18
19 /**
20 * Create an EventId from a 64-character hex string.
21 * @throws InvalidEventIdError if the hex is invalid
22 */
23 static fromHex(hex: string): EventId {
24 if (!/^[0-9a-f]{64}$/.test(hex)) {
25 throw new InvalidEventIdError(hex)
26 }
27 return new EventId(hex)
28 }
29
30 /**
31 * Create an EventId from a bech32 string (note1 or nevent1).
32 * @throws InvalidEventIdError if the bech32 is invalid
33 */
34 static fromBech32(bech32: string): EventId {
35 try {
36 const { type, data } = nip19.decode(bech32)
37
38 switch (type) {
39 case 'note':
40 return new EventId(data)
41
42 case 'nevent': {
43 const relayHints = (data.relays || [])
44 .map((r) => RelayUrl.tryCreate(r))
45 .filter((r): r is RelayUrl => r !== null)
46
47 return new EventId(
48 data.id,
49 data.kind,
50 data.author ? Pubkey.tryFromString(data.author) || undefined : undefined,
51 relayHints
52 )
53 }
54
55 default:
56 throw new InvalidEventIdError(bech32)
57 }
58 } catch (e) {
59 if (e instanceof InvalidEventIdError) throw e
60 throw new InvalidEventIdError(bech32)
61 }
62 }
63
64 /**
65 * Try to create an EventId from any string format (hex, note1, nevent1).
66 * Returns null if the string is invalid.
67 */
68 static tryFromString(input: string): EventId | null {
69 try {
70 if (input.startsWith('note1') || input.startsWith('nevent1')) {
71 return EventId.fromBech32(input)
72 }
73 return EventId.fromHex(input)
74 } catch {
75 return null
76 }
77 }
78
79 /**
80 * Check if a string is a valid event ID (hex format only).
81 */
82 static isValidHex(value: string): boolean {
83 return /^[0-9a-f]{64}$/.test(value)
84 }
85
86 /** The raw 64-character hex value */
87 get hex(): string {
88 return this._hex
89 }
90
91 /** The event kind if known (from nevent) */
92 get kind(): number | undefined {
93 return this._kind
94 }
95
96 /** The event author if known (from nevent) */
97 get author(): Pubkey | undefined {
98 return this._author
99 }
100
101 /** Relay hints for finding this event */
102 get relayHints(): readonly RelayUrl[] {
103 return this._relayHints
104 }
105
106 /** A shortened display format */
107 get formatted(): string {
108 return `${this._hex.slice(0, 8)}...${this._hex.slice(-4)}`
109 }
110
111 /**
112 * Convert to bech32 format.
113 * Returns nevent1 if there's additional metadata, otherwise note1.
114 */
115 toBech32(): string {
116 if (this._kind !== undefined || this._author || this._relayHints.length > 0) {
117 return nip19.neventEncode({
118 id: this._hex,
119 kind: this._kind,
120 author: this._author?.hex,
121 relays: this._relayHints.map((r) => r.value),
122 })
123 }
124 return nip19.noteEncode(this._hex)
125 }
126
127 /** Convert to simple note1 format (no metadata) */
128 toNote(): string {
129 return nip19.noteEncode(this._hex)
130 }
131
132 /** Check equality with another EventId (compares hex only) */
133 equals(other: EventId): boolean {
134 return this._hex === other._hex
135 }
136
137 /** Returns the hex representation */
138 toString(): string {
139 return this._hex
140 }
141
142 /** For JSON serialization */
143 toJSON(): string {
144 return this._hex
145 }
146 }
147