Class NostrVoucherBackupRepository
- All Implemented Interfaces:
VoucherBackupPort
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 Summary
ConstructorsConstructorDescriptionNostrVoucherBackupRepository(@NonNull NostrClientAdapter nostrClient) Creates a NostrVoucherBackupRepository with default timeouts.NostrVoucherBackupRepository(@NonNull NostrClientAdapter nostrClient, long publishTimeoutMs, long queryTimeoutMs) Creates a NostrVoucherBackupRepository with custom timeouts. -
Method Summary
Modifier and TypeMethodDescriptionvoidbackup(@NonNull List<SignedVoucher> vouchers, @NonNull String userPrivateKey) Backs up vouchers to Nostr using encrypted DMs to self.Gets the underlying Nostr client adapter.booleanhasBackups(@NonNull String userPrivateKey) Checks if backups exist for the given user.Restores vouchers from Nostr backup events.Methods inherited from class java.lang.Object
clone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, waitMethods inherited from interface xyz.tcheeric.cashu.voucher.app.ports.VoucherBackupPort
deleteBackups
-
Constructor Details
-
NostrVoucherBackupRepository
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 millisecondsqueryTimeoutMs- 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:
- Validates the private key format
- Derives the public key from private key
- Creates a VoucherBackupPayload with all vouchers
- Encrypts payload with NIP-44 (XChaCha20-Poly1305)
- Creates kind 4 event (encrypted DM to self)
- 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:
backupin interfaceVoucherBackupPort- 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 invalidVoucherNostrException- if backup fails (encryption, publishing, etc.)
-
restore
Restores vouchers from Nostr backup events.This method:
- Validates the private key format
- Derives the public key from private key
- Queries all encrypted DM events to self with "backup" tag
- Decrypts each event using NIP-44
- Extracts vouchers from each backup
- Merges and deduplicates by voucher ID (newest timestamp wins)
- 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:
restorein interfaceVoucherBackupPort- 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 invalidVoucherNostrException- if restore fails (decryption, network, etc.)
-
hasBackups
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:
hasBackupsin interfaceVoucherBackupPort- 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 invalidVoucherNostrException- if check fails (network, etc.)
-
getNostrClient
Gets the underlying Nostr client adapter.- Returns:
- the Nostr client adapter
-