mirror of
https://github.com/aljazceru/cdk.git
synced 2026-01-04 05:25:26 +01:00
feat: bip353 (#969)
This commit is contained in:
@@ -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 })
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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?;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user