# 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

  1. Creates and funds a wallet with Cashu tokens
  2. Resolves a BIP-353 address to find payment instructions
  3. Creates a melt quote for the resolved payment
  4. 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

  1. Parse address: Convert alice@example.com to DNS query
  2. DNS lookup: Query alice.user._bitcoin-payment.example.com for TXT records
  3. Extract payment info: Parse Bitcoin URIs from DNS TXT records
  4. 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(&quote.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

  1. DNSSEC Validation: Always validate DNSSEC to prevent DNS spoofing
  2. Offer Verification: Verify BOLT12 offers before payment
  3. Amount Limits: Implement reasonable payment limits
  4. 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