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())
Temporary Wallets (No NSEC Required)
If you need a wallet with ephemeral keys that are not stored anywhere, use TempWallet:
import asyncio
from sixty_nuts import TempWallet
async def main():
# Create a temporary wallet with auto-generated keys
# No NSEC required - keys are generated randomly
async with TempWallet(
mint_urls=["https://mint.minibits.cash/Bitcoin"],
currency="sat"
) as wallet:
# Use the wallet normally
state = await wallet.fetch_wallet_state()
print(f"Balance: {state.balance} sats")
# The private key is not stored anywhere
# When the wallet is closed, the keys are lost forever
# Alternative creation methods:
temp_wallet = TempWallet() # Uses default mint
# Or with async factory
temp_wallet2 = await TempWallet.create(
mint_urls=["https://mint.minibits.cash/Bitcoin"],
auto_init=False # Skip relay connections
)
await temp_wallet.aclose()
await temp_wallet2.aclose()
asyncio.run(main())
Note: TempWallet is useful for:
- One-time operations where you don't need to persist the wallet
- Testing and development
- Privacy-focused applications where keys should be ephemeral
- Scenarios where you want to receive/send tokens without storing keys
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 implementationcrypto.py- Cryptographic primitives (BDHKE and NIP-44 v2 encryption)mint.py- Cashu mint API clientrelay.py- Nostr relay WebSocket clientlnurl.py- LNURL protocol support for Lightning Address payments
TODO
Core Implementation
-
Proof-to-Event-ID Mapping: Implement proper mapping between proofs and their containing event IDs (wallet.py:66)
- Currently missing in
WalletStatedataclass - Required for accurate token event management and proper deletion when proofs are spent
- Currently missing in
-
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 (todo)
- 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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file sixty_nuts-0.0.4.tar.gz.
File metadata
- Download URL: sixty_nuts-0.0.4.tar.gz
- Upload date:
- Size: 62.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.6.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d5ddd39368575bbbfe9fa587e248c499f575fc9f8d8eac3fa519b04fe9f29c4b
|
|
| MD5 |
135fbe1b3d104b8629c1111878f8ee09
|
|
| BLAKE2b-256 |
d425ad7f6290dd630cf4d90d341f4e7aa57ae6b281c587ec569d6f276735890c
|
File details
Details for the file sixty_nuts-0.0.4-py3-none-any.whl.
File metadata
- Download URL: sixty_nuts-0.0.4-py3-none-any.whl
- Upload date:
- Size: 31.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.6.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1b5f1bcec7688de05ccfa06d2f8477260db61632b27c5ebd989d3956f35de52d
|
|
| MD5 |
d3c9617a14408a2fcb4c9aff59b8f535
|
|
| BLAKE2b-256 |
ab8c3835cf2e125524971de1f933b42575325c9d4915f3dfa9e7b5b0511b70ad
|