NAME
Net::Nostr::Wallet - NIP-60 Cashu wallet state management
SYNOPSIS
use Net::Nostr::Wallet;
my $pubkey = 'aa' x 32;
# Build wallet content (plaintext for NIP-44 encryption)
my $wallet_plaintext = Net::Nostr::Wallet->wallet_content(
privkey => 'bb' x 32,
mints => ['https://mint1', 'https://mint2'],
);
# Create wallet event (kind 17375) with pre-encrypted content
my $wallet_ev = Net::Nostr::Wallet->wallet_event(
pubkey => $pubkey,
content => $encrypted_wallet, # NIP-44 encrypted
);
# Build token content (plaintext for NIP-44 encryption)
my $token_plaintext = Net::Nostr::Wallet->token_content(
mint => 'https://stablenut.umint.cash',
unit => 'sat',
proofs => [
{
id => '005c2502034d4f12',
amount => 1,
secret => 'z+zyxAVLRqN9lEjxuNPSyRJzEstbl69Jc1vtimvtkPg=',
C => '0241d98a8197ef238a192d47edf191a9de78b657308937b4f7dd0aa53beae72c46',
},
],
del => ['old-token-event-id'],
);
# Create token event (kind 7375)
my $token_ev = Net::Nostr::Wallet->token_event(
pubkey => $pubkey,
content => $encrypted_token, # NIP-44 encrypted
);
# Build history content
my $history_plaintext = Net::Nostr::Wallet->history_content(
direction => 'out',
amount => '4',
unit => 'sat',
e_tags => [
['event-id-1', '', 'destroyed'],
['event-id-2', '', 'created'],
],
);
# Create history event (kind 7376) with unencrypted redeemed e tags
my $history_ev = Net::Nostr::Wallet->history_event(
pubkey => $pubkey,
content => $encrypted_history, # NIP-44 encrypted
redeemed_ids => [['nutzap-id', 'wss://relay']],
);
# Create quote event (kind 7374, optional)
my $quote_ev = Net::Nostr::Wallet->quote_event(
pubkey => $pubkey,
content => $encrypted_quote_id, # NIP-44 encrypted
mint_url => 'https://mint1',
expiration => time() + 2 * 7 * 86400, # ~2 weeks
);
# Delete spent token (NIP-09 with k:7375 tag)
my $delete_ev = Net::Nostr::Wallet->delete_token(
pubkey => $pubkey,
event_ids => ['spent-token-event-id'],
);
# Parse decrypted content
my $wallet = Net::Nostr::Wallet->parse_wallet_content($decrypted);
say $wallet->privkey;
say join ', ', @{$wallet->mints};
my $token = Net::Nostr::Wallet->parse_token_content($decrypted);
say $token->mint;
say scalar @{$token->proofs};
my $hist = Net::Nostr::Wallet->parse_history_content($decrypted);
say $hist->direction; # 'in' or 'out'
say $hist->amount;
# Parse event public tags
my $parsed = Net::Nostr::Wallet->from_event($event);
DESCRIPTION
Implements NIP-60 Cashu wallet state management over Nostr. A Cashu wallet stores its state in relay events so it is accessible across applications.
Four event kinds are involved:
- kind 17375 - Wallet event (replaceable). Contains NIP-44 encrypted wallet private key and trusted mint URLs. The private key is used exclusively for P2PK ecash operations and MUST NOT be the user's Nostr private key.
- kind 7375 - Token event. Contains NIP-44 encrypted unspent Cashu proofs. Multiple token events can exist per mint, and each can hold multiple proofs. When proofs are spent, the token event MUST be NIP-09 deleted and unspent proofs rolled over into a new token event.
- kind 7374 - Quote redemption event (optional). Tracks mint quote IDs with a NIP-40 expiration of approximately two weeks. Application developers SHOULD prefer local state when possible.
All content fields are NIP-44 encrypted. This module accepts pre-encrypted content for event creation and provides helper methods to build the plaintext payloads and parse decrypted content.
CONSTRUCTOR
new
my $w = Net::Nostr::Wallet->new(%fields);
Creates a new Net::Nostr::Wallet object. Typically returned by "from_event" or the parse_* methods; calling new directly is useful for testing. Croaks on unknown arguments.
CLASS METHODS
wallet_event
my $event = Net::Nostr::Wallet->wallet_event(
pubkey => $hex_pubkey,
content => $encrypted, # NIP-44 encrypted wallet_content()
);
Creates a kind 17375 wallet Net::Nostr::Event. pubkey and content are required. Tags are always empty (all data is in the encrypted content).
token_event
my $event = Net::Nostr::Wallet->token_event(
pubkey => $hex_pubkey,
content => $encrypted, # NIP-44 encrypted token_content()
);
Creates a kind 7375 token Net::Nostr::Event. pubkey and content are required. Tags are always empty.
history_event
my $event = Net::Nostr::Wallet->history_event(
pubkey => $hex_pubkey,
content => $encrypted, # NIP-44 encrypted history_content()
redeemed_ids => [[$event_id, $relay_hint], ...], # optional
);
Creates a kind 7376 spending history Net::Nostr::Event. pubkey and content are required. redeemed_ids is an optional arrayref of [$event_id, $relay_hint] pairs that become unencrypted e tags with the redeemed marker.
quote_event
my $event = Net::Nostr::Wallet->quote_event(
pubkey => $hex_pubkey,
content => $encrypted, # NIP-44 encrypted quote ID
mint_url => 'https://mint',
expiration => $timestamp,
);
Creates a kind 7374 quote redemption Net::Nostr::Event. All parameters are required. The expiration should be approximately two weeks (the maximum time a Lightning payment may be in-flight).
delete_token
my $event = Net::Nostr::Wallet->delete_token(
pubkey => $hex_pubkey,
event_ids => [$token_event_id, ...],
);
Creates a kind 5 NIP-09 deletion Net::Nostr::Event for spent token events. Includes e tags for each token and a ["k", "7375"] tag as required by the spec to allow easy filtering.
wallet_content
my $plaintext = Net::Nostr::Wallet->wallet_content(
privkey => $hex_privkey,
mints => ['https://mint1', 'https://mint2'],
);
Builds the JSON plaintext for a wallet event's content. Returns a JSON string ready for NIP-44 encryption. privkey is the P2PK private key (not the user's Nostr key). mints must have at least one entry.
token_content
my $plaintext = Net::Nostr::Wallet->token_content(
mint => 'https://mint',
proofs => [{ id => '...', amount => 1, secret => '...', C => '...' }],
unit => 'sat', # optional, default 'sat'
del => ['old-token-id'], # optional
);
Builds the JSON plaintext for a token event's content. mint and proofs are required. unit defaults to sat. del lists token event IDs that were destroyed in creating this token (assists with state transitions).
history_content
my $plaintext = Net::Nostr::Wallet->history_content(
direction => 'out', # 'in' or 'out'
amount => '4',
unit => 'sat', # optional, default 'sat'
e_tags => [
[$event_id, $relay_hint, $marker],
...
],
);
Builds the JSON plaintext for a history event's content. direction, amount, and e_tags are required. Each e_tag entry is an arrayref of [$event_id, $relay_hint, $marker] where marker is created, destroyed, or redeemed.
parse_wallet_content
my $wallet = Net::Nostr::Wallet->parse_wallet_content($decrypted_json);
say $wallet->privkey;
say join ', ', @{$wallet->mints};
Parses decrypted wallet content into a Net::Nostr::Wallet object.
parse_token_content
my $token = Net::Nostr::Wallet->parse_token_content($decrypted_json);
say $token->mint;
say $token->unit;
say scalar @{$token->proofs};
say join ', ', @{$token->del};
Parses decrypted token content into a Net::Nostr::Wallet object. unit defaults to sat if not present. del defaults to an empty arrayref.
parse_history_content
my $hist = Net::Nostr::Wallet->parse_history_content($decrypted_json);
say $hist->direction; # 'in' or 'out'
say $hist->amount;
say $hist->unit;
for my $e (@{$hist->e_tags}) {
say "$e->[0] ($e->[2])"; # event_id (marker)
}
Parses decrypted history content into a Net::Nostr::Wallet object. unit defaults to sat if not present.
from_event
my $parsed = Net::Nostr::Wallet->from_event($event);
Parses a kind 17375, 7375, 7376, or 7374 event into a Net::Nostr::Wallet object using only public (unencrypted) tags. For kind 7376, extracts redeemed_ids from public e tags. For kind 7374, extracts mint_url and expiration. Returns undef for unrecognized kinds.
To access encrypted content, first decrypt the event's content with NIP-44, then use "parse_wallet_content", "parse_token_content", or "parse_history_content".
validate
Net::Nostr::Wallet->validate($event);
Validates a wallet-related event. Croaks on invalid structure. For kinds 17375, 7375, and 7376, validation passes since content is encrypted and public tags are optional. For kind 7374, requires expiration and mint tags.
ACCESSORS
Available on objects returned by "from_event" and the parse_* methods. Which accessors contain data depends on what was parsed.
privkey
The P2PK private key (from parsed wallet content).
mints
Arrayref of mint URLs (from parsed wallet content).
mint
The mint URL (from parsed token content).
unit
The base unit, e.g. sat, usd (from parsed token or history content).
proofs
Arrayref of proof hashrefs (from parsed token content).
del
Arrayref of destroyed token event IDs (from parsed token content).
direction
Transaction direction: in or out (from parsed history content).
amount
Transaction amount as string (from parsed history content).
e_tags
Arrayref of [$event_id, $relay_hint, $marker] entries (from parsed history content).
redeemed_ids
Arrayref of redeemed nutzap event IDs from public e tags (from "from_event" on kind 7376).
mint_url
The mint URL from the mint tag (from "from_event" on kind 7374).
expiration
The expiration timestamp (from "from_event" on kind 7374).