# BIP-353 Human Readable Payments Example
This example demonstrates how to use BIP-353 (Human Readable Bitcoin Payment Instructions) with CDK. BIP-353 allows users to share simple email-like addresses such as user@domain.com
instead of complex Bitcoin addresses or Lightning invoices.
# What This Example Does
- Creates and funds a wallet with Cashu tokens
- Resolves a BIP-353 address to find payment instructions
- Creates a melt quote for the resolved payment
- Executes the payment using the BIP-353 address
# Key Concepts
- BIP-353: Standard for human-readable Bitcoin payment instructions
- DNS resolution: Using DNS TXT records to store payment information
- BOLT12 offers: Modern Lightning payment requests embedded in DNS
- Melt quotes: CDK quotes for paying Lightning invoices or offers
# How BIP-353 Works
- Parse address: Convert
alice@example.com
to DNS query - DNS lookup: Query
alice.user._bitcoin-payment.example.com
for TXT records - Extract payment info: Parse Bitcoin URIs from DNS TXT records
- Execute payment: Use CDK to pay the resolved Lightning offer
# Code Example
//! # BIP-353 CDK Example
//!
//! This example demonstrates how to use BIP-353 (Human Readable Bitcoin Payment Instructions)
//! with the CDK wallet. BIP-353 allows users to share simple email-like addresses such as
//! `user@domain.com` instead of complex Bitcoin addresses or Lightning invoices.
//!
//! ## How it works
//!
//! 1. Parse a human-readable address like `alice@example.com`
//! 2. Query DNS TXT records at `alice.user._bitcoin-payment.example.com`
//! 3. Extract Bitcoin URIs from the TXT records
//! 4. Parse payment instructions (Lightning offers, on-chain addresses)
//! 5. Use CDK wallet to execute payments
//!
//! ## Usage
//!
//! ```bash
//! cargo run --example bip353 --features="wallet bip353"
//! ```
//!
//! Note: The example uses a placeholder address that will fail DNS resolution.
//! To test with real addresses, you need a domain with proper BIP-353 DNS records.
use std::sync::Arc;
use std::time::Duration;
use cdk::amount::SplitTarget;
use cdk::nuts::nut00::ProofsMethods;
use cdk::nuts::{CurrencyUnit, MintQuoteState};
use cdk::wallet::Wallet;
use cdk::Amount;
use cdk_sqlite::wallet::memory;
use rand::random;
use tokio::time::sleep;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
println!("BIP-353 CDK Example");
println!("===================");
// Example BIP-353 address - replace with a real one that has BOLT12 offer
// For testing, you might need to set up your own DNS records
let bip353_address = "tsk@thesimplekid.com"; // This is just an example
println!("Attempting to use BIP-353 address: {}", bip353_address);
// Generate a random seed for the wallet
let seed = random::<[u8; 64]>();
// Mint URL and currency unit
let mint_url = "https://fake.thesimplekid.dev";
let unit = CurrencyUnit::Sat;
let initial_amount = Amount::from(1000); // Start with 1000 sats
// Initialize the memory store
let localstore = Arc::new(memory::empty().await?);
// Create a new wallet
let wallet = Wallet::new(mint_url, unit, localstore, seed, None)?;
// First, we need to fund the wallet
println!("Requesting mint quote for {} sats...", initial_amount);
let mint_quote = wallet.mint_quote(initial_amount, None).await?;
println!(
"Pay this invoice to fund the wallet: {}",
mint_quote.request
);
// In a real application, you would wait for the payment
// For this example, we'll just demonstrate the BIP353 melt process
println!("Waiting for payment... (in real use, pay the above invoice)");
// Check quote state (with timeout for demo purposes)
let timeout = Duration::from_secs(30);
let start = std::time::Instant::now();
while start.elapsed() < timeout {
let status = wallet.mint_quote_state(&mint_quote.id).await?;
if status.state == MintQuoteState::Paid {
break;
}
println!("Quote state: {} (waiting...)", status.state);
sleep(Duration::from_secs(2)).await;
}
// Mint the tokens
let proofs = wallet
.mint(&mint_quote.id, SplitTarget::default(), None)
.await?;
let received_amount = proofs.total_amount()?;
println!("Successfully minted {} sats", received_amount);
// Now prepare to pay using the BIP353 address
let payment_amount_sats = 100; // Example: paying 100 sats
println!(
"Attempting to pay {} sats using BIP-353 address...",
payment_amount_sats
);
// Use the new wallet method to resolve BIP353 address and get melt quote
match wallet
.melt_bip353_quote(bip353_address, payment_amount_sats * 1_000)
.await
{
Ok(melt_quote) => {
println!("BIP-353 melt quote received:");
println!(" Quote ID: {}", melt_quote.id);
println!(" Amount: {} sats", melt_quote.amount);
println!(" Fee Reserve: {} sats", melt_quote.fee_reserve);
println!(" State: {}", melt_quote.state);
// Execute the payment
match wallet.melt(&melt_quote.id).await {
Ok(melt_result) => {
println!("BIP-353 payment successful!");
println!(" State: {}", melt_result.state);
println!(" Amount paid: {} sats", melt_result.amount);
println!(" Fee paid: {} sats", melt_result.fee_paid);
if let Some(preimage) = melt_result.preimage {
println!(" Payment preimage: {}", preimage);
}
}
Err(e) => {
println!("BIP-353 payment failed: {}", e);
}
}
}
Err(e) => {
println!("Failed to get BIP-353 melt quote: {}", e);
println!("This could be because:");
println!("1. The BIP-353 address format is invalid");
println!("2. DNS resolution failed (expected for this example)");
println!("3. No Lightning offer found in the DNS records");
println!("4. DNSSEC validation failed");
}
}
Ok(())
}
# Dependencies
Add these dependencies to your Cargo.toml
:
[dependencies]
cdk = { version = "*", default-features = false, features = ["wallet", "bip353"] }
cdk-sqlite = { version = "*", features = ["wallet"] }
tokio = { version = "1", features = ["full"] }
rand = "0.8"
anyhow = "1.0"
# Running This Example
cargo run --features="bip353"
# Expected Output
BIP-353 CDK Example
===================
Attempting to use BIP-353 address: tsk@thesimplekid.com
Requesting mint quote for 1000 sats...
Pay this invoice to fund the wallet: lnbc10u1p...
Waiting for payment... (in real use, pay the above invoice)
Quote state: Unpaid (waiting...)
...
Successfully minted 1000 sats
Attempting to pay 100 sats using BIP-353 address...
Failed to get BIP-353 melt quote: DNS resolution failed
This could be because:
1. The BIP-353 address format is invalid
2. DNS resolution failed (expected for this example)
3. No Lightning offer found in the DNS records
4. DNSSEC validation failed
# Setting Up BIP-353 DNS Records
To test with real BIP-353 addresses, you need to configure DNS TXT records:
# DNS Record Format
For address alice@example.com
, create a TXT record at:
alice.user._bitcoin-payment.example.com
# Record Content
bitcoin:?b12=lno1pg... (BOLT12 offer)
# Example DNS Configuration
# For alice@example.com
alice.user._bitcoin-payment.example.com. IN TXT "bitcoin:?b12=lno1pg9ux8gv9..."
# For payments@mystore.com
payments.user._bitcoin-payment.mystore.com. IN TXT "bitcoin:?b12=lno1qg25ej..."
# Understanding the Flow
# 1. Address Parsing
let bip353_address = "tsk@thesimplekid.com";
// Becomes DNS query: tsk.user._bitcoin-payment.thesimplekid.com
# 2. DNS Resolution
The wallet automatically:
- Constructs the DNS query
- Performs DNSSEC validation
- Extracts Bitcoin URIs from TXT records
- Parses BOLT12 offers or other payment instructions
# 3. Payment Execution
let melt_quote = wallet.melt_bip353_quote(bip353_address, amount_msats).await?;
let result = wallet.melt(&melt_quote.id).await?;
# Use Cases
# 1. E-commerce
// Customer pays simple address instead of complex invoice
let store_address = "payments@mystore.com";
let amount_sats = 2500; // $1 worth
let quote = wallet.melt_bip353_quote(store_address, amount_sats * 1000).await?;
wallet.melt("e.id).await?;
# 2. Personal Payments
// Send money to friends using memorable addresses
let friend_address = "alice@example.com";
let amount = 1000; // 1000 sats
wallet.melt_bip353_quote(friend_address, amount * 1000).await?;
# 3. Subscription Services
// Recurring payments to service providers
let service_address = "billing@vpnservice.com";
let monthly_fee = 5000; // 5000 sats
// Can be automated for recurring payments
# Error Handling
# Common Errors
match wallet.melt_bip353_quote(address, amount).await {
Err(cdk::Error::Bip353DnsResolutionFailed) => {
println!("Could not resolve DNS for {}", address);
},
Err(cdk::Error::Bip353NoValidOffers) => {
println!("No valid payment offers found for {}", address);
},
Err(cdk::Error::Bip353DnsSecValidationFailed) => {
println!("DNSSEC validation failed - potential security issue");
},
Ok(quote) => {
// Process the quote
},
}
# Security Considerations
- DNSSEC Validation: Always validate DNSSEC to prevent DNS spoofing
- Offer Verification: Verify BOLT12 offers before payment
- Amount Limits: Implement reasonable payment limits
- Address Validation: Validate address format before DNS lookup
# Production Tips
# 1. Caching
// Cache DNS resolutions to improve performance
let cache = std::collections::HashMap::new();
// Implement TTL-based caching for DNS responses
# 2. Fallback Options
// Provide fallback for failed BIP-353 resolution
match wallet.melt_bip353_quote(address, amount).await {
Ok(quote) => { /* use BIP-353 */ },
Err(_) => {
// Fallback to manual invoice entry
println!("BIP-353 failed, please provide Lightning invoice manually");
}
}
# 3. User Experience
- Show both the BIP-353 address and resolved payment details
- Provide clear error messages for DNS failures
- Allow manual invoice entry as fallback
# Next Steps
- Learn about BOLT12 offers for recurring payments
- Explore custom HTTP clients for advanced networking
- Try streaming payments for high-volume operations