From a93803c9406e560af7a6046e08df179c391dd7ee Mon Sep 17 00:00:00 2001 From: Sonkeng Maldini Date: Thu, 11 Dec 2025 15:52:19 +0100 Subject: [PATCH] add bip353 payment instructions support --- Cargo.toml | 2 + src/bip353_payment_instructions.rs | 131 +++++++++++++++++++++++++++++ src/lib.rs | 3 + 3 files changed, 136 insertions(+) create mode 100644 src/bip353_payment_instructions.rs diff --git a/Cargo.toml b/Cargo.toml index 27414e6..98ce1f6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ readme = "README.md" [dependencies] miniscript = { version = "12", default-features = false } bdk_coin_select = "0.4.0" +bitcoin-payment-instructions = { version = "0.5.0", optional = true} [dev-dependencies] anyhow = "1" @@ -25,6 +26,7 @@ bdk_chain = { version = "0.23.0" } [features] default = ["std"] std = ["miniscript/std"] +bip353 = ["bitcoin-payment-instructions"] [[example]] name = "synopsis" diff --git a/src/bip353_payment_instructions.rs b/src/bip353_payment_instructions.rs new file mode 100644 index 0000000..71da314 --- /dev/null +++ b/src/bip353_payment_instructions.rs @@ -0,0 +1,131 @@ +/// This crate adds support for BIP 353 DNS payment instructions support +/// +use crate::bitcoin::{Amount, Network, ScriptBuf}; +use alloc::vec::Vec; +use bitcoin_payment_instructions::{ + amount, dns_resolver::DNSHrnResolver, hrn_resolution::HrnResolver, + FixedAmountPaymentInstructions, ParseError, PaymentInstructions, PaymentMethod, + PaymentMethodType, +}; +use core::{net::SocketAddr, str::FromStr}; + +async fn parse_dns_instructions( + hrn: &str, + resolver: &impl HrnResolver, + network: Network, +) -> Result { + let instructions = PaymentInstructions::parse(hrn, network, resolver, true).await?; + + Ok(instructions) +} + +pub struct Payment { + pub script: ScriptBuf, + pub amount: Amount, + pub dnssec_proof: Option>, +} + +fn process_fixed_instructions( + amount: Amount, + instructions: &FixedAmountPaymentInstructions, +) -> Result { + // Look for on chain payment method as it's the only one we can support + let PaymentMethod::OnChain(addr) = instructions + .methods() + .iter() + .find(|ix| matches!(ix, PaymentMethod::OnChain(_))) + .map(|pm| pm) + .unwrap() + else { + return Err(ParseError::InvalidInstructions( + "Unsupported payment method", + )); + }; + + let Some(onchain_amount) = instructions.onchain_payment_amount() else { + return Err(ParseError::InvalidInstructions( + "On chain amount should be specified", + )); + }; + + // We need this conversion since Amount from instructions is different from Amount from bitcoin + let onchain_amount = Amount::from_sat(onchain_amount.sats_rounding_up()); + + if onchain_amount != amount { + return Err(ParseError::InvalidInstructions( + "Mismatched amount expected , got", + )); + } + + Ok(Payment { + script: addr.script_pubkey(), + amount: onchain_amount, + dnssec_proof: instructions.bip_353_dnssec_proof().clone(), + }) +} + +// If dns instructions provides a fixed amount we can allow the user not putting an amount? +pub async fn resolve_dns_recipient( + hrn: &str, + amount: Amount, + network: Network, +) -> Result { + let resolver = DNSHrnResolver(SocketAddr::from_str("8.8.8.8:53").expect("Should not fail.")); + let payment_instructions = parse_dns_instructions(hrn, &resolver, network).await?; + + match payment_instructions { + PaymentInstructions::ConfigurableAmount(instructions) => { + // Look for on chain payment method as it's the only one we can support + if instructions + .methods() + .find(|method| matches!(method.method_type(), PaymentMethodType::OnChain)) + .is_none() + { + return Err(ParseError::InvalidInstructions( + "Unsupported payment method", + )); + } + + let min_amount = instructions.min_amt(); + let max_amount = instructions.max_amt(); + + if min_amount.is_some() { + let min_amount = min_amount + .map(|a| Amount::from_sat(a.sats_rounding_up())) + .unwrap(); + if amount < min_amount { + return Err(ParseError::InvalidInstructions( + "Amount lesser than min amount", + )); + } + } + + if max_amount.is_some() { + let max_amount = max_amount + .map(|a| Amount::from_sat(a.sats_rounding_up())) + .unwrap(); + if amount > max_amount { + return Err(ParseError::InvalidInstructions( + "Amount greater than max amount", + )); + } + } + + let fixed_instructions = instructions + .set_amount( + amount::Amount::from_sats(amount.to_sat()).unwrap(), + &resolver, + ) + .await + .map_err(|s| ParseError::InvalidInstructions(s))?; + + process_fixed_instructions(amount, &fixed_instructions) + } + + PaymentInstructions::FixedAmount(instructions) => { + process_fixed_instructions(amount, &instructions) + } + } +} + +// pub async fn resolve_dns_recipient_with_resolver() -> Result>; diff --git a/src/lib.rs b/src/lib.rs index bcdf894..8a5eb6e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -35,6 +35,9 @@ pub use selection::*; pub use selector::*; pub use signer::*; +#[cfg(feature = "bip353")] +pub mod bip353_payment_instructions; + #[cfg(feature = "std")] pub(crate) mod collections { #![allow(unused)]