Skip to main content

Sixty Nuts - NIP-60 Cashu Wallet Implementation

Project description

Sixty Nuts - A NIP-60 Cashu Wallet in Python

A lightweight, stateless Cashu wallet implementation following NIP-60 specification for Nostr-based wallet state management.

Features

  • NIP-60 Compliant: Full implementation of the NIP-60 specification
  • NIP-44 Encryption: Secure encryption using the NIP-44 v2 standard
  • Stateless Design: Wallet state stored on Nostr relays
  • Multi-Mint Support: Can work with multiple Cashu mints
  • Async/Await: Modern Python async implementation
  • LNURL Support: Send to Lightning Addresses and other LNURL formats

Installation

pip install sixty-nuts

Usage

Basic Setup

import asyncio
from sixty_nuts import Wallet

async def main():
    # Create wallet with private key (hex or nsec format)
    wallet = await Wallet.create(
        nsec="your_nostr_private_key_hex",  # or "nsec1..." bech32 format
        mint_urls=["https://mint.minibits.cash/Bitcoin"],
        relays=["wss://relay.damus.io", "wss://nos.lol", "wss://nostr.wine"]
    )
    
    # Or use context manager for automatic cleanup
    async with Wallet(
        nsec="your_nostr_private_key_hex",
        mint_urls=["https://mint.minibits.cash/Bitcoin"]
    ) as wallet:
        # Wallet operations here
        pass

asyncio.run(main())

Minting Tokens (Receiving via Lightning)

async def mint_tokens(wallet: Wallet):
    # Create a Lightning invoice for 1000 sats
    invoice, payment_confirmation = await wallet.mint_async(1000)
    
    print(f"Pay this Lightning invoice: {invoice}")
    print("Waiting for payment...")
    
    # Wait for payment (with 5 minute timeout)
    paid = await payment_confirmation
    
    if paid:
        print("Payment received! Tokens minted.")
        # Check wallet balance
        state = await wallet.fetch_wallet_state()
        print(f"New balance: {state.balance} sats")
    else:
        print("Payment timed out")

Sending Tokens

async def send_tokens(wallet: Wallet):
    # Send 100 sats as a Cashu token
    amount = 100
    
    # Check balance first
    state = await wallet.fetch_wallet_state()
    print(f"Current balance: {state.balance} sats")
    
    if state.balance >= amount:
        # Create a Cashu token
        token = await wallet.send(amount)
        print(f"Send this token to recipient: {token}")
        
        # Check new balance
        new_state = await wallet.fetch_wallet_state()
        print(f"New balance: {new_state.balance} sats")

Receiving Tokens

async def receive_tokens(wallet: Wallet):
    # Redeem a received Cashu token
    token = "cashuA..."  # Token received from someone
    
    try:
        await wallet.redeem(token)
        print("Token redeemed successfully!")
        
        # Check new balance
        state = await wallet.fetch_wallet_state()
        print(f"New balance: {state.balance} sats")
    except Exception as e:
        print(f"Failed to redeem token: {e}")

Paying Lightning Invoices (Melting)

async def pay_invoice(wallet: Wallet):
    # Pay a Lightning invoice using your tokens
    invoice = "lnbc..."  # Lightning invoice to pay
    
    try:
        await wallet.melt(invoice)
        print("Invoice paid successfully!")
        
        # Check remaining balance
        state = await wallet.fetch_wallet_state()
        print(f"Remaining balance: {state.balance} sats")
    except Exception as e:
        print(f"Payment failed: {e}")

Checking Wallet State

async def check_wallet(wallet: Wallet):
    # Fetch current wallet state from Nostr relays
    state = await wallet.fetch_wallet_state()
    
    print(f"Balance: {state.balance} sats")
    print(f"Number of proofs: {len(state.proofs)}")
    print(f"Connected mints: {list(state.mint_keysets.keys())}")
    
    # Show proof denominations
    denominations = {}
    for proof in state.proofs:
        amount = proof["amount"]
        denominations[amount] = denominations.get(amount, 0) + 1
    
    print("Denominations:")
    for amount, count in sorted(denominations.items()):
        print(f"  {amount} sat: {count} proof(s)")

Sending to Lightning Addresses (LNURL)

async def send_to_lightning_address(wallet: Wallet):
    # Send to a Lightning Address (user@domain.com format)
    lnurl = "satoshi@bitcoin.org"
    amount = 500  # sats
    
    try:
        paid_amount = await wallet.send_to_lnurl(lnurl, amount)
        print(f"Successfully sent {paid_amount} sats to {lnurl}")
    except Exception as e:
        print(f"Failed to send: {e}")
    
    # You can also send to other LNURL formats:
    # - Bech32 encoded: "LNURL1DP68GURN8GHJ7..."
    # - With prefix: "lightning:user@domain.com"
    # - Direct URL: "https://lnurl.service.com/pay/..."
    
    # Custom fee parameters
    await wallet.send_to_lnurl(
        lnurl,
        amount=1000,
        fee_estimate=0.02,  # 2% fee estimate
        max_fee=50,         # Maximum 50 sats fee
    )

Complete Example

import asyncio
from sixty_nuts import Wallet

async def example_wallet_operations():
    # Initialize wallet
    async with Wallet(
        nsec="your_nostr_private_key_hex",
        mint_urls=["https://mint.minibits.cash/Bitcoin"],
        currency="sat"
    ) as wallet:
        # Check initial balance
        state = await wallet.fetch_wallet_state()
        print(f"Initial balance: {state.balance} sats")
        
        # Mint some tokens
        if state.balance < 1000:
            invoice, task = await wallet.mint_async(1000)
            print(f"Pay invoice to add funds: {invoice}")
            paid = await task
            if paid:
                print("Funded!")
        
        # Send tokens
        if state.balance >= 100:
            token = await wallet.send(100)
            print(f"Token to share: {token}")
        
        # Send to Lightning Address
        if state.balance >= 500:
            await wallet.send_to_lnurl("user@ln.tips", 500)
            print("Sent to Lightning Address!")
        
        # Final balance
        final_state = await wallet.fetch_wallet_state()
        print(f"Final balance: {final_state.balance} sats")

if __name__ == "__main__":
    asyncio.run(example_wallet_operations())

Architecture

  • wallet.py - Main wallet implementation
  • crypto.py - Cryptographic primitives (BDHKE and NIP-44 v2 encryption)
  • mint.py - Cashu mint API client
  • relay.py - Nostr relay WebSocket client
  • lnurl.py - LNURL protocol support for Lightning Address payments

Security Notes

⚠️ Important: This implementation includes proper NIP-44 encryption for wallet data stored on relays. However:

  • The Cashu blinding implementation is simplified and needs proper BDHKE implementation for production use
  • Proof-to-event tracking needs to be implemented for full NIP-60 compliance
  • Consider the security limitations of storing wallet state on public relays

TODO

Core Implementation

  • Proof-to-Event-ID Mapping: Implement proper mapping between proofs and their containing event IDs (wallet.py:66)

    • Currently missing in WalletState dataclass
    • Required for accurate token event management and proper deletion when proofs are spent
  • Quote Tracking (NIP-60): Implement quote tracking as per NIP-60 specification (wallet.py:776)

    • Need to publish kind 7374 events for mint quotes
    • Track quote expiration and status
  • Minted Quote Tracking: Properly track minted quotes to avoid double-minting (wallet.py:806)

    • Maintain state of which quotes have been successfully minted
    • Check existing token events for quote IDs in tags
  • Coin Selection Algorithm: Implement better coin selection algorithm (wallet.py:956)

    • Current implementation is naive (first-fit)
    • Should optimize for privacy and minimize number of proofs used

Security & Cryptography

  • Implement proper BDHKE blinding for Cashu operations
  • Improve proof tracking to correctly identify which proofs belong to which token events (wallet.py:1031)

Features

  • Support for P2PK ecash (NIP-61)
  • Add comprehensive test suite
  • Implement wallet recovery from relay state
  • Add multi-mint transaction support

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

sixty_nuts-0.0.2.tar.gz (54.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

sixty_nuts-0.0.2-py3-none-any.whl (27.2 kB view details)

Uploaded Python 3

File details

Details for the file sixty_nuts-0.0.2.tar.gz.

File metadata

  • Download URL: sixty_nuts-0.0.2.tar.gz
  • Upload date:
  • Size: 54.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.6.13

File hashes

Hashes for sixty_nuts-0.0.2.tar.gz
Algorithm Hash digest
SHA256 23b6e6e2f97f6368b1f250ef8e0b96497c9e6239858ab420485bc4172a02ce94
MD5 efc3d804ceb9317c96110ffff275700c
BLAKE2b-256 5f4893c84e8b28b6cd35f692b607e5083152e0fce7f12757b3b8991baec2f501

See more details on using hashes here.

File details

Details for the file sixty_nuts-0.0.2-py3-none-any.whl.

File metadata

  • Download URL: sixty_nuts-0.0.2-py3-none-any.whl
  • Upload date:
  • Size: 27.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.6.13

File hashes

Hashes for sixty_nuts-0.0.2-py3-none-any.whl
Algorithm Hash digest
SHA256 8bd72f238dc503ec64134ff75a3d455f376a3744301e074eb5b2ebc0455c32b9
MD5 92a4f61e4f09fc50edf65bf9c6c3c51f
BLAKE2b-256 74f3ea964f81e3da5eec27669ce13e074508399139357cf8d3fa8509118c3c8e

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page