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