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 7376 - Spending history event. Records balance changes with direction, amount, and event references. Clients SHOULD publish these when the balance changes. The e tags with redeemed markers SHOULD be left unencrypted in public tags.
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).

SEE ALSO

NIP-60, Net::Nostr, Net::Nostr::Event, Net::Nostr::Nutzap