feat: bip353 (#969)

This commit is contained in:
thesimplekid
2025-08-21 16:16:48 +01:00
committed by GitHub
parent b6f7a75fba
commit 9a3d9b7139
11 changed files with 555 additions and 150 deletions

View File

@@ -11,8 +11,7 @@ rust-version.workspace = true
readme = "README.md"
[features]
default = ["bip353"]
bip353 = ["dep:trust-dns-resolver"]
default = []
sqlcipher = ["cdk-sqlite/sqlcipher"]
# MSRV is not tracked with redb enabled
redb = ["dep:cdk-redb"]
@@ -21,7 +20,7 @@ redb = ["dep:cdk-redb"]
anyhow.workspace = true
bip39.workspace = true
bitcoin.workspace = true
cdk = { workspace = true, default-features = false, features = ["wallet", "auth"]}
cdk = { workspace = true, default-features = false, features = ["wallet", "auth", "bip353"]}
cdk-redb = { workspace = true, features = ["wallet"], optional = true }
cdk-sqlite = { workspace = true, features = ["wallet"] }
clap.workspace = true
@@ -40,4 +39,3 @@ reqwest.workspace = true
url.workspace = true
serde_with.workspace = true
lightning.workspace = true
trust-dns-resolver = { version = "0.23.2", optional = true }

View File

@@ -1,132 +0,0 @@
use std::collections::HashMap;
use std::str::FromStr;
use anyhow::{bail, Result};
use trust_dns_resolver::config::{ResolverConfig, ResolverOpts};
use trust_dns_resolver::TokioAsyncResolver;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Bip353Address {
pub user: String,
pub domain: String,
}
impl Bip353Address {
/// Resolve a human-readable Bitcoin address
pub async fn resolve(self) -> Result<PaymentInstruction> {
// Construct DNS name
let dns_name = format!("{}.user._bitcoin-payment.{}", self.user, self.domain);
// Create a new resolver with DNSSEC validation
let mut opts = ResolverOpts::default();
opts.validate = true; // Enable DNSSEC validation
let resolver = TokioAsyncResolver::tokio(ResolverConfig::default(), opts);
// Query TXT records - with opts.validate=true, this will fail if DNSSEC validation fails
let response = resolver.txt_lookup(&dns_name).await?;
// Extract and concatenate TXT record strings
let mut bitcoin_uris = Vec::new();
for txt in response.iter() {
let txt_data: Vec<String> = txt
.txt_data()
.iter()
.map(|bytes| String::from_utf8_lossy(bytes).into_owned())
.collect();
let concatenated = txt_data.join("");
if concatenated.to_lowercase().starts_with("bitcoin:") {
bitcoin_uris.push(concatenated);
}
}
// BIP-353 requires exactly one Bitcoin URI
match bitcoin_uris.len() {
0 => bail!("No Bitcoin URI found"),
1 => PaymentInstruction::from_uri(&bitcoin_uris[0]),
_ => bail!("Multiple Bitcoin URIs found"),
}
}
}
impl FromStr for Bip353Address {
type Err = anyhow::Error;
/// Parse a human-readable Bitcoin address
fn from_str(address: &str) -> Result<Self, Self::Err> {
let addr = address.trim();
// Remove Bitcoin prefix if present
let addr = addr.strip_prefix("").unwrap_or(addr);
// Split by @
let parts: Vec<&str> = addr.split('@').collect();
if parts.len() != 2 {
bail!("Address is not formatted correctly")
}
let user = parts[0].trim();
let domain = parts[1].trim();
if user.is_empty() || domain.is_empty() {
bail!("User name and domain must not be empty")
}
Ok(Self {
user: user.to_string(),
domain: domain.to_string(),
})
}
}
/// Payment instruction type
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PaymentType {
OnChain,
LightningOffer,
}
/// BIP-353 payment instruction
#[derive(Debug, Clone)]
pub struct PaymentInstruction {
pub parameters: HashMap<PaymentType, String>,
}
impl PaymentInstruction {
/// Parse a payment instruction from a Bitcoin URI
pub fn from_uri(uri: &str) -> Result<Self> {
if !uri.to_lowercase().starts_with("bitcoin:") {
bail!("URI must start with 'bitcoin:'")
}
let mut parameters = HashMap::new();
// Parse URI parameters
if let Some(query_start) = uri.find('?') {
let query = &uri[query_start + 1..];
for pair in query.split('&') {
if let Some(eq_pos) = pair.find('=') {
let key = pair[..eq_pos].to_string();
let value = pair[eq_pos + 1..].to_string();
let payment_type;
// Determine payment type
if key.contains("lno") {
payment_type = PaymentType::LightningOffer;
} else if !uri[8..].contains('?') && uri.len() > 8 {
// Simple on-chain address
payment_type = PaymentType::OnChain;
} else {
continue;
}
parameters.insert(payment_type, value);
}
}
}
Ok(PaymentInstruction { parameters })
}
}

View File

@@ -18,8 +18,6 @@ use tracing::Level;
use tracing_subscriber::EnvFilter;
use url::Url;
#[cfg(feature = "bip353")]
mod bip353;
mod nostr_storage;
mod sub_commands;
mod token_storage;

View File

@@ -1,6 +1,6 @@
use std::str::FromStr;
use anyhow::{anyhow, bail, Result};
use anyhow::{bail, Result};
use cdk::amount::{amount_for_offer, Amount, MSAT_IN_SAT};
use cdk::mint_url::MintUrl;
use cdk::nuts::{CurrencyUnit, MeltOptions};
@@ -12,7 +12,6 @@ use clap::{Args, ValueEnum};
use lightning::offers::offer::Offer;
use tokio::task::JoinSet;
use crate::bip353::{Bip353Address, PaymentType as Bip353PaymentType};
use crate::sub_commands::balance::mint_balances;
use crate::utils::{
get_number_input, get_user_input, get_wallet_by_index, get_wallet_by_mint_url,
@@ -184,22 +183,19 @@ pub async fn pay(
}
PaymentType::Bip353 => {
let bip353_addr = get_user_input("Enter Bip353 address.")?;
let bip353_addr = Bip353Address::from_str(&bip353_addr)?;
let payment_instructions = bip353_addr.resolve().await?;
let offer = payment_instructions
.parameters
.get(&Bip353PaymentType::LightningOffer)
.ok_or(anyhow!("Offer not defined"))?;
let prompt =
"Enter the amount you would like to pay in sats for this amountless offer:";
// BIP353 payments are always amountless for now
let options = create_melt_options(available_funds, None, prompt)?;
// Get melt quote for BOLT12
let quote = wallet.melt_bolt12_quote(offer.to_string(), options).await?;
// Get melt quote for BIP353 address (internally resolves and gets BOLT12 quote)
let quote = wallet
.melt_bip353_quote(
&bip353_addr,
options.expect("Amount is required").amount_msat(),
)
.await?;
process_payment(&wallet, quote).await?;
}
}

View File

@@ -115,6 +115,16 @@ pub enum Error {
#[error("Operation timeout")]
Timeout,
/// BIP353 address parsing error
#[error("Failed to parse BIP353 address: {0}")]
Bip353Parse(String),
/// BIP353 address resolution error
#[error("Failed to resolve BIP353 address: {0}")]
Bip353Resolve(String),
/// BIP353 no Lightning offer found
#[error("No Lightning offer found in BIP353 payment instructions")]
Bip353NoLightningOffer,
/// Internal Error - Send error
#[error("Internal send error: {0}")]
SendError(String),

View File

@@ -15,6 +15,7 @@ default = ["mint", "wallet", "auth"]
wallet = ["dep:futures", "dep:reqwest", "cdk-common/wallet", "dep:rustls"]
mint = ["dep:futures", "dep:reqwest", "cdk-common/mint", "cdk-signatory"]
auth = ["dep:jsonwebtoken", "cdk-common/auth", "cdk-common/auth"]
bip353 = ["dep:trust-dns-resolver"]
# We do not commit to a MSRV with swagger enabled
swagger = ["mint", "dep:utoipa", "cdk-common/swagger"]
bench = []
@@ -42,6 +43,7 @@ url.workspace = true
utoipa = { workspace = true, optional = true }
uuid.workspace = true
jsonwebtoken = { workspace = true, optional = true }
trust-dns-resolver = { version = "0.23.2", optional = true }
# -Z minimal-versions
sync_wrapper = "0.1.2"
@@ -95,6 +97,10 @@ required-features = ["wallet"]
name = "auth_wallet"
required-features = ["wallet", "auth"]
[[example]]
name = "bip353"
required-features = ["wallet", "bip353"]
[dev-dependencies]
rand.workspace = true
cdk-sqlite.workspace = true
@@ -102,6 +108,7 @@ bip39.workspace = true
tracing-subscriber.workspace = true
criterion = "0.6.0"
reqwest = { workspace = true }
anyhow.workspace = true
[[bench]]

View File

@@ -0,0 +1,143 @@
//! # 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(())
}

286
crates/cdk/src/bip353.rs Normal file
View File

@@ -0,0 +1,286 @@
//! BIP-353: Human Readable Bitcoin Payment Instructions
//!
//! This module provides functionality for resolving human-readable Bitcoin addresses
//! according to BIP-353. It allows users to share simple email-like addresses such as
//! `user@domain.com` instead of complex Bitcoin addresses or Lightning invoices.
use std::collections::HashMap;
use std::str::FromStr;
use anyhow::{bail, Result};
use trust_dns_resolver::config::{ResolverConfig, ResolverOpts};
use trust_dns_resolver::TokioAsyncResolver;
/// BIP-353 human-readable Bitcoin address
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Bip353Address {
/// The user part of the address (before @)
pub user: String,
/// The domain part of the address (after @)
pub domain: String,
}
impl Bip353Address {
/// Resolve a human-readable Bitcoin address to payment instructions
///
/// This method performs the following steps:
/// 1. Constructs the DNS name according to BIP-353 format
/// 2. Queries TXT records with DNSSEC validation
/// 3. Extracts Bitcoin URIs from the records
/// 4. Parses the URIs into payment instructions
///
/// # Errors
///
/// This method will return an error if:
/// - DNS resolution fails
/// - DNSSEC validation fails
/// - No Bitcoin URI is found
/// - Multiple Bitcoin URIs are found (BIP-353 requires exactly one)
/// - The URI format is invalid
pub(crate) async fn resolve(self) -> Result<PaymentInstruction> {
// Construct DNS name according to BIP-353
let dns_name = format!("{}.user._bitcoin-payment.{}", self.user, self.domain);
// Create a new resolver with DNSSEC validation
let mut opts = ResolverOpts::default();
opts.validate = true; // Enable DNSSEC validation
let resolver = TokioAsyncResolver::tokio(ResolverConfig::default(), opts);
// Query TXT records - with opts.validate=true, this will fail if DNSSEC validation fails
let response = resolver.txt_lookup(&dns_name).await?;
// Extract and concatenate TXT record strings
let mut bitcoin_uris = Vec::new();
for txt in response.iter() {
let txt_data: Vec<String> = txt
.txt_data()
.iter()
.map(|bytes| String::from_utf8_lossy(bytes).into_owned())
.collect();
let concatenated = txt_data.join("");
if concatenated.to_lowercase().starts_with("bitcoin:") {
bitcoin_uris.push(concatenated);
}
}
// BIP-353 requires exactly one Bitcoin URI
match bitcoin_uris.len() {
0 => bail!("No Bitcoin URI found"),
1 => PaymentInstruction::from_uri(&bitcoin_uris[0]),
_ => bail!("Multiple Bitcoin URIs found"),
}
}
}
impl FromStr for Bip353Address {
type Err = anyhow::Error;
/// Parse a human-readable Bitcoin address from string format
///
/// Accepts formats:
/// - `user@domain.com`
/// - `₿user@domain.com` (with Bitcoin symbol prefix)
///
/// # Errors
///
/// Returns an error if:
/// - The format is not `user@domain`
/// - User or domain parts are empty
fn from_str(address: &str) -> Result<Self, Self::Err> {
let addr = address.trim();
// Remove Bitcoin prefix if present
let addr = addr.strip_prefix("").unwrap_or(addr);
// Split by @
let parts: Vec<&str> = addr.split('@').collect();
if parts.len() != 2 {
bail!("Address is not formatted correctly")
}
let user = parts[0].trim();
let domain = parts[1].trim();
if user.is_empty() || domain.is_empty() {
bail!("User name and domain must not be empty")
}
Ok(Self {
user: user.to_string(),
domain: domain.to_string(),
})
}
}
impl std::fmt::Display for Bip353Address {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}@{}", self.user, self.domain)
}
}
/// Payment instruction type
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PaymentType {
/// On-chain Bitcoin address
OnChain,
/// Lightning Offer (BOLT12)
LightningOffer,
}
/// BIP-353 payment instruction containing parsed payment methods
#[derive(Debug, Clone)]
pub struct PaymentInstruction {
/// Map of payment types to their corresponding values
pub parameters: HashMap<PaymentType, String>,
}
impl PaymentInstruction {
/// Create a new empty payment instruction
pub fn new() -> Self {
Self {
parameters: HashMap::new(),
}
}
/// Parse a payment instruction from a Bitcoin URI
///
/// Extracts various payment methods from the URI:
/// - Lightning offers (parameters containing "lno")
/// - On-chain addresses (address part of the URI)
///
/// # Errors
///
/// Returns an error if the URI doesn't start with "bitcoin:"
pub fn from_uri(uri: &str) -> Result<Self> {
if !uri.to_lowercase().starts_with("bitcoin:") {
bail!("URI must start with 'bitcoin:'")
}
let mut parameters = HashMap::new();
// Parse URI parameters
if let Some(query_start) = uri.find('?') {
let query = &uri[query_start + 1..];
for pair in query.split('&') {
if let Some(eq_pos) = pair.find('=') {
let key = pair[..eq_pos].to_string();
let value = pair[eq_pos + 1..].to_string();
// Determine payment type based on parameter key
if key.contains("lno") {
parameters.insert(PaymentType::LightningOffer, value);
}
// Could add more payment types here as needed
}
}
}
// Check if we have an on-chain address (address part after bitcoin:)
if let Some(query_start) = uri.find('?') {
let addr_part = &uri[8..query_start]; // Skip "bitcoin:"
if !addr_part.is_empty() {
parameters.insert(PaymentType::OnChain, addr_part.to_string());
}
} else {
// No query parameters, check if there's just an address
let addr_part = &uri[8..]; // Skip "bitcoin:"
if !addr_part.is_empty() {
parameters.insert(PaymentType::OnChain, addr_part.to_string());
}
}
Ok(PaymentInstruction { parameters })
}
/// Get a payment method by type
pub fn get(&self, payment_type: &PaymentType) -> Option<&String> {
self.parameters.get(payment_type)
}
}
impl Default for PaymentInstruction {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
impl PaymentInstruction {
/// Check if a payment type is available
pub fn has_payment_type(&self, payment_type: &PaymentType) -> bool {
self.parameters.contains_key(payment_type)
}
}
#[test]
fn test_bip353_address_parsing() {
// Test basic parsing
let addr = Bip353Address::from_str("alice@example.com").unwrap();
assert_eq!(addr.user, "alice");
assert_eq!(addr.domain, "example.com");
// Test with Bitcoin symbol
let addr = Bip353Address::from_str("₿bob@bitcoin.org").unwrap();
assert_eq!(addr.user, "bob");
assert_eq!(addr.domain, "bitcoin.org");
// Test with whitespace
let addr = Bip353Address::from_str(" charlie@test.net ").unwrap();
assert_eq!(addr.user, "charlie");
assert_eq!(addr.domain, "test.net");
// Test display
let addr = Bip353Address {
user: "test".to_string(),
domain: "example.com".to_string(),
};
assert_eq!(addr.to_string(), "test@example.com");
}
#[test]
fn test_bip353_address_parsing_errors() {
// Test invalid formats
assert!(Bip353Address::from_str("invalid").is_err());
assert!(Bip353Address::from_str("@example.com").is_err());
assert!(Bip353Address::from_str("user@").is_err());
assert!(Bip353Address::from_str("user@domain@extra").is_err());
assert!(Bip353Address::from_str("").is_err());
}
#[test]
fn test_payment_instruction_parsing() {
// Test Lightning offer URI
let uri = "bitcoin:?lno=lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pxqrjszs5v2a5m5xwc4mxv6rdjdcn2d3kxccnjdgecf7fz3rf5g4t7gdxhkzm8mpsq5q";
let instruction = PaymentInstruction::from_uri(uri).unwrap();
assert!(instruction.has_payment_type(&PaymentType::LightningOffer));
// Test on-chain address URI
let uri = "bitcoin:bc1qexampleaddress";
let instruction = PaymentInstruction::from_uri(uri).unwrap();
assert!(instruction.has_payment_type(&PaymentType::OnChain));
assert_eq!(
instruction.get(&PaymentType::OnChain).unwrap(),
"bc1qexampleaddress"
);
// Test combined URI
let uri = "bitcoin:bc1qexampleaddress?lno=lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pxqrjszs5v2a5m5xwc4mxv6rdjdcn2d3kxccnjdgecf7fz3rf5g4t7gdxhkzm8mpsq5q";
let instruction = PaymentInstruction::from_uri(uri).unwrap();
assert!(instruction.has_payment_type(&PaymentType::OnChain));
assert!(instruction.has_payment_type(&PaymentType::LightningOffer));
}
#[test]
fn test_payment_instruction_errors() {
// Test invalid URI
assert!(PaymentInstruction::from_uri("invalid:uri").is_err());
assert!(PaymentInstruction::from_uri("").is_err());
}
}

View File

@@ -22,6 +22,9 @@ pub mod mint;
#[cfg(feature = "wallet")]
pub mod wallet;
#[cfg(feature = "bip353")]
mod bip353;
#[cfg(all(any(feature = "wallet", feature = "mint"), feature = "auth"))]
mod oidc_client;

View File

@@ -0,0 +1,94 @@
//! Melt BIP353
//!
//! Implementation of melt functionality for BIP353 human-readable addresses
use std::str::FromStr;
use cdk_common::wallet::MeltQuote;
use tracing::instrument;
#[cfg(feature = "bip353")]
use crate::bip353::{Bip353Address, PaymentType};
use crate::nuts::MeltOptions;
use crate::{Amount, Error, Wallet};
impl Wallet {
/// Melt Quote for BIP353 human-readable address
///
/// This method resolves a BIP353 address (e.g., "alice@example.com") to a Lightning offer
/// and then creates a melt quote for that offer.
///
/// # Arguments
///
/// * `bip353_address` - Human-readable address in the format "user@domain.com"
/// * `amount_msat` - Amount to pay in millisatoshis
///
/// # Returns
///
/// A `MeltQuote` that can be used to execute the payment
///
/// # Errors
///
/// This method will return an error if:
/// - The BIP353 address format is invalid
/// - DNS resolution fails or DNSSEC validation fails
/// - No Lightning offer is found in the payment instructions
/// - The mint fails to provide a quote for the offer
///
/// # Example
///
/// ```rust,no_run
/// use cdk::Amount;
/// # use cdk::Wallet;
/// # async fn example(wallet: Wallet) -> Result<(), cdk::Error> {
/// let quote = wallet
/// .melt_bip353_quote("alice@example.com", Amount::from(100_000)) // 100 sats in msat
/// .await?;
/// # Ok(())
/// # }
/// ```
#[cfg(feature = "bip353")]
#[instrument(skip(self, amount_msat), fields(address = %bip353_address))]
pub async fn melt_bip353_quote(
&self,
bip353_address: &str,
amount_msat: impl Into<Amount>,
) -> Result<MeltQuote, Error> {
// Parse the BIP353 address
let address = Bip353Address::from_str(bip353_address).map_err(|e| {
tracing::error!("Failed to parse BIP353 address '{}': {}", bip353_address, e);
Error::Bip353Parse(e.to_string())
})?;
tracing::debug!("Resolving BIP353 address: {}", address);
// Keep a copy for error reporting
let address_string = address.to_string();
// Resolve the address to get payment instructions
let payment_instructions = address.resolve().await.map_err(|e| {
tracing::error!(
"Failed to resolve BIP353 address '{}': {}",
address_string,
e
);
Error::Bip353Resolve(e.to_string())
})?;
// Extract the Lightning offer from the payment instructions
let offer = payment_instructions
.get(&PaymentType::LightningOffer)
.ok_or_else(|| {
tracing::error!("No Lightning offer found in BIP353 payment instructions");
Error::Bip353NoLightningOffer
})?;
tracing::debug!("Found Lightning offer in BIP353 instructions: {}", offer);
// Create melt options with the provided amount
let options = MeltOptions::new_amountless(amount_msat);
// Create a melt quote for the BOLT12 offer
self.melt_bolt12_quote(offer.clone(), Some(options)).await
}
}

View File

@@ -7,6 +7,8 @@ use tracing::instrument;
use crate::Wallet;
#[cfg(feature = "bip353")]
mod melt_bip353;
mod melt_bolt11;
mod melt_bolt12;