Class NostrVoucherBackupRepository

java.lang.Object
xyz.tcheeric.cashu.voucher.nostr.NostrVoucherBackupRepository
All Implemented Interfaces:
VoucherBackupPort

public class NostrVoucherBackupRepository extends Object implements VoucherBackupPort
Nostr implementation of the VoucherBackupPort for private voucher backups.

This adapter implements voucher backup and restore using Nostr's NIP-17 (private direct messages) with NIP-44 (versioned encryption) to create secure, private, recoverable voucher backups.

Hexagonal Architecture

This is an adapter that implements the VoucherBackupPort defined in the application layer. It translates domain backup operations into Nostr protocol operations.

NIP-17 + NIP-44 Backup Model

Voucher backups are stored as encrypted direct messages to self:

  • Event Kind: 4 (encrypted direct message)
  • Encryption: NIP-44 versioned encryption (XChaCha20-Poly1305)
  • Recipient: User's own public key (self-addressed)
  • Content: Encrypted JSON array of SignedVoucher objects
  • Tags: "p" (recipient pubkey), "backup" (identification tag)

Key Features

  • Privacy: Only user can decrypt their own backups
  • Redundancy: Multi-relay storage for availability
  • Recovery: Restore vouchers from any device with user's private key
  • Deduplication: Automatic merging of multiple backups by voucher ID
  • Incremental: Each backup creates a new event (append-only)

Why This Matters

Unlike deterministic secrets (NUT-13), vouchers are non-deterministic. If a user loses their wallet, they cannot regenerate voucher secrets from a seed phrase. This backup mechanism ensures vouchers can always be recovered.

Usage Example

 // Initialize repository
 NostrClientAdapter client = new NostrClientAdapter(relays, 5000, 3);
 NostrVoucherBackupRepository backup = new NostrVoucherBackupRepository(client);

 try {
     client.connect();

     // Backup vouchers
     List<SignedVoucher> vouchers = wallet.getVouchers();
     backup.backup(vouchers, userNostrPrivateKey);

     // Restore vouchers (e.g., after device loss)
     List<SignedVoucher> restored = backup.restore(userNostrPrivateKey);
     wallet.addVouchers(restored);

 } finally {
     client.disconnect();
 }
 

Thread Safety

This class is thread-safe as it delegates to the thread-safe NostrClientAdapter.

Error Handling

All methods may throw VoucherNostrException for:

  • Network failures (relay unreachable)
  • Encryption/decryption errors (invalid key, corrupted data)
  • Serialization errors (invalid voucher data)
  • Publishing failures (all relays reject event)
  • Query timeouts (no response from relays)
See Also:
  • Constructor Details

    • NostrVoucherBackupRepository

      public NostrVoucherBackupRepository(@NonNull @NonNull NostrClientAdapter nostrClient)
      Creates a NostrVoucherBackupRepository with default timeouts.
      Parameters:
      nostrClient - the Nostr client adapter (must not be null)
      Throws:
      IllegalArgumentException - if nostrClient is null
    • NostrVoucherBackupRepository

      public NostrVoucherBackupRepository(@NonNull @NonNull NostrClientAdapter nostrClient, long publishTimeoutMs, long queryTimeoutMs)
      Creates a NostrVoucherBackupRepository with custom timeouts.
      Parameters:
      nostrClient - the Nostr client adapter (must not be null)
      publishTimeoutMs - timeout for publishing backup events in milliseconds
      queryTimeoutMs - timeout for querying backup events in milliseconds
      Throws:
      IllegalArgumentException - if parameters are invalid
  • Method Details

    • backup

      public void backup(@NonNull @NonNull List<SignedVoucher> vouchers, @NonNull @NonNull String userPrivateKey)
      Backs up vouchers to Nostr using encrypted DMs to self.

      This method:

      1. Validates the private key format
      2. Derives the public key from private key
      3. Creates a VoucherBackupPayload with all vouchers
      4. Encrypts payload with NIP-44 (XChaCha20-Poly1305)
      5. Creates kind 4 event (encrypted DM to self)
      6. Publishes to all connected relays

      Each backup creates a new event. Multiple backups are merged during restore. This is an append-only operation - old backups are not deleted.

      Specified by:
      backup in interface VoucherBackupPort
      Parameters:
      vouchers - the list of vouchers to backup (must not be null, can be empty)
      userPrivateKey - the user's Nostr private key in hex format (must not be null or blank)
      Throws:
      IllegalArgumentException - if parameters are invalid
      VoucherNostrException - if backup fails (encryption, publishing, etc.)
    • restore

      public List<SignedVoucher> restore(@NonNull @NonNull String userPrivateKey)
      Restores vouchers from Nostr backup events.

      This method:

      1. Validates the private key format
      2. Derives the public key from private key
      3. Queries all encrypted DM events to self with "backup" tag
      4. Decrypts each event using NIP-44
      5. Extracts vouchers from each backup
      6. Merges and deduplicates by voucher ID (newest timestamp wins)
      7. Returns the complete list of unique vouchers

      If multiple backups exist for the same voucher ID, the one with the latest backup timestamp is used. This handles the case where a voucher's status may have changed between backups.

      Specified by:
      restore in interface VoucherBackupPort
      Parameters:
      userPrivateKey - the user's Nostr private key in hex format (must not be null or blank)
      Returns:
      list of restored vouchers (never null, may be empty if no backups found)
      Throws:
      IllegalArgumentException - if userPrivateKey is invalid
      VoucherNostrException - if restore fails (decryption, network, etc.)
    • hasBackups

      public boolean hasBackups(@NonNull @NonNull String userPrivateKey)
      Checks if backups exist for the given user.

      This implementation queries Nostr relays for backup events without decrypting them, providing a faster check than full restore.

      Specified by:
      hasBackups in interface VoucherBackupPort
      Parameters:
      userPrivateKey - the user's Nostr private key (must not be null or blank)
      Returns:
      true if at least one backup event exists, false otherwise
      Throws:
      IllegalArgumentException - if userPrivateKey is invalid
      VoucherNostrException - if check fails (network, etc.)
    • getNostrClient

      public NostrClientAdapter getNostrClient()
      Gets the underlying Nostr client adapter.
      Returns:
      the Nostr client adapter