Class NostrVoucherLedgerRepository

java.lang.Object
xyz.tcheeric.cashu.voucher.nostr.NostrVoucherLedgerRepository
All Implemented Interfaces:
VoucherLedgerPort

public class NostrVoucherLedgerRepository extends Object implements VoucherLedgerPort
Nostr implementation of the VoucherLedgerPort for public voucher audit trail.

This adapter implements the voucher ledger using Nostr's NIP-33 (parameterized replaceable events) to create a public, queryable audit trail of voucher lifecycle.

Hexagonal Architecture

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

NIP-33 Storage Model

Each voucher is stored as a kind 30078 parameterized replaceable event:

  • Event ID: Unique per update (hash of event content)
  • d tag: "voucher:{voucherId}" - ensures replaceability per voucher
  • Replaceability: New events with same d tag replace old ones
  • Content: JSON serialized SignedVoucher + status
  • Tags: status, amount, unit, expiry for queryability

Key Features

  • Public audit trail - anyone can query voucher status
  • Automatic deduplication via NIP-33 replaceability
  • Multi-relay redundancy for availability
  • Status history via timestamped events

Usage Example

 // Initialize repository
 NostrClientAdapter client = new NostrClientAdapter(relays, 5000, 3);
 PublicKey issuerPubKey = new PublicKey("issuer_hex_pubkey");
 NostrVoucherLedgerRepository ledger = new NostrVoucherLedgerRepository(client, issuerPubKey);

 try {
     client.connect();

     // Publish new voucher
     SignedVoucher voucher = ...;
     ledger.publish(voucher, VoucherStatus.ISSUED);

     // Query status
     Optional<VoucherStatus> status = ledger.queryStatus(voucherId);

     // Update status
     ledger.updateStatus(voucherId, VoucherStatus.REDEEMED);

 } 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)
  • Serialization errors (invalid voucher data)
  • Publishing failures (all relays reject event)
  • Query timeouts (no response from relays)
See Also:
  • Constructor Details

    • NostrVoucherLedgerRepository

      public NostrVoucherLedgerRepository(@NonNull @NonNull NostrClientAdapter nostrClient, @NonNull @NonNull nostr.base.PublicKey issuerPublicKey)
      Creates a NostrVoucherLedgerRepository with default timeouts.
      Parameters:
      nostrClient - the Nostr client adapter (must not be null)
      issuerPublicKey - the issuer's public key for signing events (must not be null)
      Throws:
      IllegalArgumentException - if parameters are null
    • NostrVoucherLedgerRepository

      public NostrVoucherLedgerRepository(@NonNull @NonNull NostrClientAdapter nostrClient, @NonNull @NonNull nostr.base.PublicKey issuerPublicKey, long publishTimeoutMs, long queryTimeoutMs)
      Creates a NostrVoucherLedgerRepository with custom timeouts.
      Parameters:
      nostrClient - the Nostr client adapter (must not be null)
      issuerPublicKey - the issuer's public key for signing events (must not be null)
      publishTimeoutMs - timeout for publishing events in milliseconds
      queryTimeoutMs - timeout for querying events in milliseconds
      Throws:
      IllegalArgumentException - if parameters are invalid
  • Method Details

    • publish

      public void publish(@NonNull @NonNull SignedVoucher voucher, @NonNull @NonNull VoucherStatus status)
      Publishes a voucher to the Nostr ledger with the given status.

      This method:

      1. Creates a VoucherLedgerEvent (kind 30078) from the voucher
      2. Sets the issuer's public key
      3. Publishes to all connected relays
      4. Waits for at least one relay to confirm

      If a voucher with the same ID already exists, NIP-33 ensures this event replaces the previous one (based on created_at timestamp).

      Specified by:
      publish in interface VoucherLedgerPort
      Parameters:
      voucher - the signed voucher to publish (must not be null)
      status - the initial status (must not be null)
      Throws:
      IllegalArgumentException - if parameters are null
      VoucherNostrException - if publishing fails on all relays
    • queryStatus

      public Optional<VoucherStatus> queryStatus(@NonNull @NonNull String voucherId)
      Queries the current status of a voucher from the Nostr ledger.

      This method:

      1. Constructs a subscription ID for the query
      2. Queries all connected relays for events with d tag = "voucher:{voucherId}"
      3. Extracts status from the most recent event (highest created_at)
      Specified by:
      queryStatus in interface VoucherLedgerPort
      Parameters:
      voucherId - the unique voucher identifier (must not be null or blank)
      Returns:
      the current status, or empty if voucher not found
      Throws:
      IllegalArgumentException - if voucherId is null or blank
      VoucherNostrException - if query fails
    • updateStatus

      public void updateStatus(@NonNull @NonNull String voucherId, @NonNull @NonNull VoucherStatus newStatus)
      Updates the status of an existing voucher in the Nostr ledger.

      This method:

      1. Queries the current voucher from the ledger
      2. Creates a new VoucherLedgerEvent with updated status
      3. Publishes the new event (replaces old via NIP-33)

      Note: This requires retrieving the full voucher first, so we can re-publish it with the new status. The NIP-33 d tag ensures the new event replaces the old.

      Specified by:
      updateStatus in interface VoucherLedgerPort
      Parameters:
      voucherId - the unique voucher identifier (must not be null or blank)
      newStatus - the new status to set (must not be null)
      Throws:
      IllegalArgumentException - if parameters are invalid
      VoucherNostrException - if update fails or voucher not found
    • queryVoucher

      public Optional<SignedVoucher> queryVoucher(@NonNull @NonNull String voucherId)
      Queries the full voucher details from the Nostr ledger.

      This method retrieves the complete SignedVoucher from the event content. It's used internally by updateStatus(String, VoucherStatus) to get the voucher before republishing with new status.

      Specified by:
      queryVoucher in interface VoucherLedgerPort
      Parameters:
      voucherId - the unique voucher identifier (must not be null or blank)
      Returns:
      the complete voucher, or empty if not found
      Throws:
      IllegalArgumentException - if voucherId is null or blank
      VoucherNostrException - if query fails or deserialization fails
    • getIssuerPublicKey

      public nostr.base.PublicKey getIssuerPublicKey()
      Gets the issuer's public key used for signing events.
      Returns:
      the issuer's public key
    • getNostrClient

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