NAME
Net::Nostr::Zap - NIP-57 Lightning Zaps
SYNOPSIS
use Net::Nostr::Zap qw(
lud16_to_url encode_lnurl decode_lnurl
bolt11_amount callback_url calculate_splits
);
use Net::Nostr::Key;
my $key = Net::Nostr::Key->new;
# Create a zap request (kind 9734)
my $zap_req = Net::Nostr::Zap->new_request(
p => $recipient_pubkey,
relays => ['wss://relay.example.com'],
amount => '21000',
lnurl => encode_lnurl('https://example.com/.well-known/lnurlp/alice'),
e => $event_id,
k => '1',
);
my $event = $zap_req->to_event(pubkey => $key->pubkey_hex);
$key->sign_event($event);
# Send zap request to recipient's LNURL callback
my $url = callback_url('https://lnurl.example.com/callback',
amount => 21000,
nostr => $event,
lnurl => $zap_req->lnurl,
);
# Parse a received zap request
my $req = Net::Nostr::Zap->request_from_event($event);
say $req->p; # recipient pubkey
say $req->amount; # '21000'
# Create a zap receipt (kind 9735)
my $zap_receipt = Net::Nostr::Zap->new_receipt(
p => $recipient_pubkey,
bolt11 => $bolt11_invoice,
description => $zap_request_json,
sender => $sender_pubkey,
e => $event_id,
preimage => $preimage_hex,
);
my $receipt_event = $zap_receipt->to_event(pubkey => $server_pubkey);
# Parse a received zap receipt
my $receipt = Net::Nostr::Zap->receipt_from_event($receipt_event);
say $receipt->bolt11;
my $embedded_req = $receipt->zap_request; # Net::Nostr::Event
# Validate a zap request (Appendix D)
Net::Nostr::Zap->validate_request($event);
Net::Nostr::Zap->validate_request($event, amount => 21000);
# Validate a zap receipt (Appendix F)
Net::Nostr::Zap->validate_receipt($receipt_event,
nostr_pubkey => $expected_pubkey,
);
# Convert lightning address to LNURL pay endpoint URL
my $pay_url = lud16_to_url('alice@example.com');
# https://example.com/.well-known/lnurlp/alice
# Parse bolt11 invoice amount in millisats
my $msats = bolt11_amount('lnbc10u1p3unwfu...'); # 1_000_000
# Calculate zap splits from zap tags (Appendix G)
my @splits = calculate_splits(@zap_tags);
for my $split (@splits) {
say "$split->{pubkey}: $split->{percentage}%";
}
DESCRIPTION
Implements NIP-57 Lightning Zaps, which defines two event types for recording lightning payments between Nostr users:
Zap request (kind 9734) - Created by the sender and sent to the recipient's LNURL pay callback URL (not published to relays).
Zap receipt (kind 9735) - Created by the recipient's lightning wallet when the invoice is paid, and published to relays.
CONSTRUCTORS
new_request
my $zap = Net::Nostr::Zap->new_request(
p => $recipient_pubkey, # required
relays => \@relay_urls, # required
amount => '21000', # millisats, recommended
lnurl => 'lnurl1...', # recommended
e => $event_id, # optional, if zapping an event
a => '30023:pk:slug', # optional, for addressable events
k => '1', # optional, target event kind
content => 'Great post!', # optional message
);
Creates a zap request. p (recipient pubkey) and relays are required.
new_receipt
my $zap = Net::Nostr::Zap->new_receipt(
p => $recipient_pubkey, # required
bolt11 => $invoice, # required
description => $zap_request_json, # required
sender => $sender_pubkey, # optional (P tag)
e => $event_id, # optional
a => '30023:pk:slug', # optional
k => '1', # optional
preimage => $preimage_hex, # optional
);
Creates a zap receipt. p, bolt11, and description are required. The description must be the JSON-encoded zap request event.
request_from_event
my $zap = Net::Nostr::Zap->request_from_event($event);
Parses a kind 9734 event into a Zap object. Croaks if the event is not kind 9734.
my $zap = Net::Nostr::Zap->request_from_event($event);
say $zap->p; # recipient pubkey
say $zap->amount; # millisats or undef
receipt_from_event
my $zap = Net::Nostr::Zap->receipt_from_event($event);
Parses a kind 9735 event into a Zap object. Croaks if the event is not kind 9735.
my $zap = Net::Nostr::Zap->receipt_from_event($receipt_event);
say $zap->bolt11; # bolt11 invoice
say $zap->description; # JSON zap request
METHODS
to_event
my $event = $zap->to_event(pubkey => $hex_pubkey);
my $event = $zap->to_event(pubkey => $hex, created_at => time());
Creates a Net::Nostr::Event from the zap object. Extra arguments are passed through to the Event constructor.
For zap requests, creates a kind 9734 event. For zap receipts, creates a kind 9735 event with empty content.
my $event = $zap_req->to_event(pubkey => $key->pubkey_hex);
$key->sign_event($event);
zap_request
my $event = $zap_receipt->zap_request;
Parses the description tag of a zap receipt back into a Net::Nostr::Event object representing the original zap request. Only available on receipt objects.
my $receipt = Net::Nostr::Zap->receipt_from_event($event);
my $req = $receipt->zap_request;
say $req->pubkey; # the sender's pubkey
p
my $pubkey = $zap->p;
Returns the recipient's pubkey (from the p tag).
relays
my $relays = $zap->relays; # arrayref
Returns the relay URLs (zap request only).
amount
my $msats = $zap->amount; # '21000' or undef
Returns the amount in millisats (zap request only). This is a string.
lnurl
my $lnurl = $zap->lnurl; # 'lnurl1...' or undef
Returns the bech32-encoded LNURL (zap request only).
e
my $event_id = $zap->e; # hex or undef
Returns the zapped event ID.
a
my $coord = $zap->a; # 'kind:pubkey:d-tag' or undef
Returns the addressable event coordinate.
k
my $kind = $zap->k; # '1' or undef
Returns the stringified kind of the target event.
content
my $msg = $zap->content;
Returns the zap message (zap request) or empty string (zap receipt).
bolt11
my $invoice = $zap->bolt11;
Returns the bolt11 invoice (zap receipt only).
description
my $json = $zap->description;
Returns the JSON-encoded zap request (zap receipt only).
sender
my $pubkey = $zap->sender; # hex or undef
Returns the sender's pubkey from the P tag (zap receipt only).
preimage
my $preimage = $zap->preimage; # hex or undef
Returns the payment preimage (zap receipt only).
CLASS METHODS
validate_request
Net::Nostr::Zap->validate_request($event);
Net::Nostr::Zap->validate_request($event, amount => 21000);
Net::Nostr::Zap->validate_request($event, receipt_pubkey => $pubkey);
Validates a zap request event per Appendix D of NIP-57. Croaks on validation failure. Checks:
- 1. Valid Schnorr signature
- 3. Exactly one
ptag - 5. Should have
relaystag (warns if missing) - 6.
amounttag must matchamountparameter if both present - 7.
atag must be a valid event coordinate
my $key = Net::Nostr::Key->new;
my $event = $zap_req->to_event(pubkey => $key->pubkey_hex);
$key->sign_event($event);
Net::Nostr::Zap->validate_request($event, amount => 21000);
validate_receipt
Net::Nostr::Zap->validate_receipt($event,
nostr_pubkey => $expected,
lnurl => $recipient_lnurl,
);
Validates a zap receipt event per Appendix F of NIP-57. Croaks on validation failure. Checks:
Receipt pubkey must match
nostr_pubkey(the recipient's LNURL server pubkey)Must have
p,bolt11, anddescriptiontagsInvoice amount must match the zap request's
amounttag if presentIf
lnurlis provided and the zap request contains anlnurltag, warns if they do not match (SHOULD per spec)
Net::Nostr::Zap->validate_receipt($receipt_event,
nostr_pubkey => $server_pubkey,
lnurl => $recipient_lnurl,
);
FUNCTIONS
All functions are exportable. None are exported by default.
lud16_to_url
my $url = lud16_to_url('alice@example.com');
# https://example.com/.well-known/lnurlp/alice
Converts a lightning address (LUD-16 format) to its LNURL pay endpoint URL.
my $url = lud16_to_url('bob@pay.domain.org');
# https://pay.domain.org/.well-known/lnurlp/bob
encode_lnurl
my $lnurl = encode_lnurl('https://example.com/.well-known/lnurlp/alice');
# lnurl1dp68gurn8ghj7...
Encodes a URL as a bech32 string with the lnurl prefix.
decode_lnurl
my $url = decode_lnurl('lnurl1dp68gurn8ghj7...');
# https://example.com/.well-known/lnurlp/alice
Decodes a bech32-encoded LNURL back to a URL string. Croaks if the prefix is not lnurl.
bolt11_amount
my $msats = bolt11_amount('lnbc10u1p3unwfu...'); # 1_000_000
Extracts the amount in millisats from a bolt11 lightning invoice. Returns undef if the invoice has no amount. Croaks if the string is not a valid bolt11 invoice.
Amount multiplier suffixes:
m = milli (10^-3 BTC) = 100,000,000 millisats per unit
u = micro (10^-6 BTC) = 100,000 millisats per unit
n = nano (10^-9 BTC) = 100 millisats per unit
p = pico (10^-12 BTC) = 0.1 millisats per unit
bolt11_amount('lnbc20m1...') # 2_000_000_000
bolt11_amount('lnbc2500u1...') # 250_000_000
bolt11_amount('lnbc10u1...') # 1_000_000
callback_url
my $url = callback_url($base_callback,
amount => 21000,
nostr => $zap_request_event,
lnurl => 'lnurl1...',
);
Constructs the HTTP GET URL for sending a zap request to the recipient's LNURL callback endpoint (Appendix B). The nostr parameter should be a Net::Nostr::Event object which will be JSON-encoded and URI-escaped.
my $url = callback_url('https://lnurl.example.com/callback',
amount => 21000,
nostr => $signed_event,
lnurl => $lnurl,
);
# https://lnurl.example.com/callback?amount=21000&nostr=%7B...%7D&lnurl=lnurl1...
calculate_splits
my @splits = calculate_splits(@zap_tags);
Calculates zap split percentages from zap tags on an event (Appendix G). Each tag should be an arrayref: ['zap', $pubkey, $relay, $weight].
Returns a list of hashrefs with pubkey, relay, and percentage keys.
If no tags have weights, the split is equal among all recipients. If some tags have weights and others don't, the weightless tags get 0%.
# From spec example: 25%, 25%, 50%
my @splits = calculate_splits(
['zap', $pk1, $relay1, '1'],
['zap', $pk2, $relay2, '1'],
['zap', $pk3, $relay3, '2'],
);
say $splits[0]{percentage}; # 25
say $splits[2]{percentage}; # 50