NAME

Net::Nostr::Event - Nostr protocol event object

SYNOPSIS

use Net::Nostr::Event;
use Net::Nostr::Key;

# Typical usage: create via Key (sets pubkey and signs automatically)
my $key   = Net::Nostr::Key->new;
my $event = $key->create_event(kind => 1, content => 'hello', tags => []);
say $event->id;   # 64-char hex sha256
say $event->sig;  # 128-char hex signature

# Manual construction (local builder -- defaults created_at, tags, id)
my $event = Net::Nostr::Event->new(
    pubkey     => $key->pubkey_hex,
    kind       => 1,
    content    => 'hello world',
    tags       => [['t', 'nostr']],
    created_at => 1700000000,
);

# Parse from wire (strict -- all 7 fields required, no defaults)
my $event = Net::Nostr::Event->from_wire(\%hash);

say $event->json_serialize;  # canonical JSON array for hashing
my $hash = $event->to_hash;  # { id, pubkey, created_at, kind, tags, content, sig }

DESCRIPTION

Represents a Nostr event as defined by NIP-01. Handles canonical JSON serialization, automatic ID computation, kind classification, and signature verification.

Events are immutable after construction. The body fields (id, pubkey, created_at, kind, tags, content) are read-only. The only writable field is sig, which does not participate in the event ID computation. Tags are deep-copied on input and output so that callers cannot invalidate an event through retained references. This prevents a class of bugs where mutating a field silently invalidates the event ID and any existing signature.

CONSTRUCTOR

new

my $event = Net::Nostr::Event->new(
    pubkey     => $hex_pubkey,
    kind       => 1,
    content    => 'hello',
    tags       => [['p', $pubkey]],
    created_at => time(),
    sig        => $hex_sig,
);

Strict builder for local event construction. pubkey, kind, and content are required. tags defaults to [], created_at defaults to time(), and id is automatically computed from the canonical serialization. If id is passed explicitly, it is preserved as-is.

For events parsed from the wire, use "from_wire" instead, which requires all seven NIP-01 fields and does not apply any defaults.

Croaks if any required field is missing or if values fail format validation:

  • pubkey must be 64-character lowercase hex

  • kind must be an integer between 0 and 65535

  • content must be defined

  • sig, if provided, must be 128-character lowercase hex

  • id, if provided, must be 64-character lowercase hex

  • Unknown arguments are rejected

from_wire

my $event = Net::Nostr::Event->from_wire(\%hash);

Strict wire parser. Constructs an event from a hashref received over the wire (e.g. from JSON-decoded protocol messages). All seven NIP-01 event fields are required: id, pubkey, created_at, kind, tags, content, sig. No defaults are applied. Croaks if any field is missing, undefined, or fails format validation.

This is the entry point used by "parse" in Net::Nostr::Message for EVENT and AUTH messages. Use "new" for local event construction where defaults (created_at, tags, id) are convenient.

my $hash = { id => '...', pubkey => '...', created_at => 1000,
             kind => 1, tags => [], content => 'hi', sig => '...' };
my $event = Net::Nostr::Event->from_wire($hash);

ACCESSORS

All body accessors are read-only. Attempting to set them after construction will croak. sig is the only writable accessor.

id

my $id = $event->id;  # '3bf0c63f...' (64-char hex)

Returns the event ID, a SHA-256 hex digest of the canonical serialization. Read-only.

pubkey

my $pubkey = $event->pubkey;

Returns the author's public key as a 64-character hex string. Read-only.

created_at

my $ts = $event->created_at;  # Unix timestamp

Returns the event creation timestamp. Read-only.

kind

my $kind = $event->kind;  # 1

Returns the event kind (integer). Read-only.

tags

my $tags = $event->tags;  # [['p', 'abc...'], ['e', 'def...']]

Returns a deep copy of the tags arrayref. Each tag is an arrayref of strings. Read-only. All tags must be provided at construction time.

Tags are deep-copied both on input (during construction) and on output (from this accessor and "to_hash"), so callers cannot accidentally mutate the event's internal state through retained references.

content

my $content = $event->content;

Returns the event content string. Read-only.

sig

my $sig = $event->sig;           # get
$event->sig($hex_signature);     # set

Gets or sets the Schnorr signature as a 128-character lowercase hex string. This is the only writable field because the signature does not participate in event ID computation. Setting undef clears the signature. The setter croaks if the value is defined but not valid 128-char lowercase hex.

json_serialize

my $json = $event->json_serialize;

Returns the canonical JSON serialization used for ID computation: [0, pubkey, created_at, kind, tags, content]. The output is UTF-8 encoded with no extra whitespace.

to_hash

my $hash = $event->to_hash;
# { id => '...', pubkey => '...', created_at => 1000,
#   kind => 1, tags => [...], content => '...', sig => '...' }

Returns a hashref with all seven event fields. The tags value is a deep copy, so mutating it will not affect the event. Useful for JSON encoding the full event object.

METHODS

difficulty

my $bits = $event->difficulty;  # e.g. 21

Returns the Proof of Work difficulty of the event, defined as the number of leading zero bits in the event ID (NIP-13). For example, an ID starting with 000006d8 has 21 leading zero bits.

my $event = $key->create_event(kind => 1, content => 'hello', tags => []);
my $mined = $event->mine(16);
say $mined->difficulty;  # >= 16

committed_target_difficulty

my $target = $event->committed_target_difficulty;  # e.g. 20, or undef

Returns the committed target difficulty from the nonce tag's third entry (NIP-13), or undef if no nonce tag or no target is present. This allows clients and relays to reject events where the miner committed to a lower difficulty than required, even if the actual difficulty happens to be higher.

my $mined = $event->mine(20);
say $mined->committed_target_difficulty;  # 20

mine

my $mined = $event->mine($target_difficulty);

Returns a new Net::Nostr::Event with a nonce tag that gives the event at least $target_difficulty leading zero bits in its ID (NIP-13). The original event is not modified. The nonce tag's third entry records the committed target difficulty.

The returned event is unsigned -- call $key->sign_event($mined) to sign it after mining.

my $event = $key->create_event(kind => 1, content => 'hello', tags => []);
my $mined = $event->mine(20);
$key->sign_event($mined);
say $mined->difficulty;  # >= 20

Existing tags are preserved. If the event already has a nonce tag, it is replaced. The created_at timestamp is updated during mining.

Since the NIP-01 event ID does not commit to the signature, mining can be delegated to a third party (delegated Proof of Work).

d_tag

my $d = $event->d_tag;  # '' if no d tag

Returns the value of the first d tag, or empty string if none exists. Used for addressable event deduplication (kinds 30000-39999).

my $event = Net::Nostr::Event->new(
    pubkey => 'a' x 64, kind => 30023,
    content => '', tags => [['d', 'my-article']],
);
say $event->d_tag;  # 'my-article'

expiration

my $ts = $event->expiration;  # Unix timestamp, or undef

Returns the value of the expiration tag (NIP-40) as a number, or undef if the event has no expiration tag.

my $event = Net::Nostr::Event->new(
    pubkey => 'a' x 64, kind => 1, content => 'temp',
    tags => [['expiration', '1600000000']],
);
say $event->expiration;  # 1600000000

is_expired

my $bool = $event->is_expired;
my $bool = $event->is_expired($now);

Returns true if the event has an expiration tag (NIP-40) and the expiration time has passed. Accepts an optional Unix timestamp to compare against (defaults to time()). Returns false if there is no expiration tag.

if ($event->is_expired) {
    # ignore or discard the event
}

content_warning

my $reason = $event->content_warning;  # string, '' or undef

Returns the value of the content-warning tag (NIP-36), or undef if the event has no content warning tag. Returns an empty string if the tag is present but has no reason.

my $event = Net::Nostr::Event->new(
    pubkey => 'a' x 64, kind => 1, content => 'sensitive',
    tags => [['content-warning', 'spoiler']],
);
say $event->content_warning;  # 'spoiler'

has_content_warning

my $bool = $event->has_content_warning;

Returns true if the event has a content-warning tag (NIP-36). Clients can use this to hide content until the user opts in.

if ($event->has_content_warning) {
    # hide content behind a warning
}

content_warning_tag

my $tag = Net::Nostr::Event->content_warning_tag('spoiler');
my $tag = Net::Nostr::Event->content_warning_tag();

Class method that creates a content-warning tag arrayref, suitable for inclusion in an event's tags. The reason is optional.

my $event = Net::Nostr::Event->new(
    pubkey  => 'a' x 64,
    kind    => 1,
    content => 'spoiler content',
    tags    => [Net::Nostr::Event->content_warning_tag('spoiler')],
);

is_regular

$event->is_regular;  # true for kinds 1, 2, 4-44, 1000-9999

Returns true if the event kind is a regular (non-replaceable, non-ephemeral, non-addressable) kind.

is_replaceable

$event->is_replaceable;  # true for kinds 0, 3, 10000-19999

Returns true if the event kind is replaceable (only latest per pubkey+kind is kept).

is_ephemeral

$event->is_ephemeral;  # true for kinds 20000-29999

Returns true if the event kind is ephemeral (broadcast but never stored).

is_addressable

$event->is_addressable;  # true for kinds 30000-39999

Returns true if the event kind is addressable (only latest per pubkey+kind+d_tag is kept).

is_protected

$event->is_protected;  # true if ["-"] tag is present

Returns true if the event contains a ["-"] tag (NIP-70). Protected events can only be published to relays by their author. Relays MUST reject protected events unless the client has authenticated (NIP-42) as the event's pubkey.

validate

$event->validate;  # croaks on failure, returns 1 on success

Full cryptographic validation: recomputes the event ID from the canonical serialization, compares it to the stored id, and verifies the Schnorr signature against the event's own pubkey. Croaks if the event is unsigned, the ID does not match, or the signature is invalid.

This is the method callers should use to verify events received from untrusted sources (relays, peers, files).

my $event = Net::Nostr::Message->parse($json)->event;
$event->validate;  # croaks if tampered or forged

verify_sig

my $valid = $event->verify_sig($key);

Low-level signature check: verifies the Schnorr signature against the stored id using the given Net::Nostr::Key object. Croaks if the key's pubkey does not match $event->pubkey. Does not recompute the event ID -- use "validate" for full verification.

my $key   = Net::Nostr::Key->new;
my $event = $key->create_event(kind => 1, content => 'signed', tags => []);
say $event->verify_sig($key);  # 1

SEE ALSO

NIP-01, NIP-36, NIP-40, Net::Nostr, Net::Nostr::Key