Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 83 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ shlex = { version = "1.3.0", optional = true }
payjoin = { version = "1.0.0-rc.1", features = ["v1", "v2", "io", "_test-utils"], optional = true}
reqwest = { version = "0.12.23", default-features = false, optional = true }
url = { version = "2.5.4", optional = true }
bitcoin-payment-instructions = { version = "0.5.0", optional = true}

[features]
default = ["repl", "sqlite"]
Expand All @@ -49,7 +50,8 @@ redb = ["bdk_redb"]
cbf = ["bdk_kyoto", "_payjoin-dependencies"]
electrum = ["bdk_electrum", "_payjoin-dependencies"]
esplora = ["bdk_esplora", "_payjoin-dependencies"]
rpc = ["bdk_bitcoind_rpc", "_payjoin-dependencies"]
rpc = ["bdk_bitcoind_rpc", "_payjoin-dependencies"]
dns_payment = ["bitcoin-payment-instructions"]

# Internal features
_payjoin-dependencies = ["payjoin", "reqwest", "url"]
Expand Down
10 changes: 10 additions & 0 deletions src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -277,13 +277,23 @@ pub enum OfflineWalletSubCommand {
Transactions,
/// Returns the current wallet balance.
Balance,
/// Resolves the given DNS payment instructions
ResolveDnsRecipient {
/// Human Readable Name to resolve
hrn: String,
/// The amount you're willing to send to the HRN
amount: u64,
},
/// Creates a new unsigned transaction.
CreateTx {
/// Adds a recipient to the transaction.
// Clap Doesn't support complex vector parsing https://github.com/clap-rs/clap/issues/1704.
// Address and amount parsing is done at run time in handler function.
#[arg(env = "ADDRESS:SAT", long = "to", required = true, value_parser = parse_recipient)]
recipients: Vec<(ScriptBuf, u64)>,
/// Adds DNS recipients to the transaction
#[arg(long = "to_dns")]
dns_recipients: Option<String>,
/// Sends all the funds (or all the selected utxos). Requires only one recipient with value 0.
#[arg(long = "send_all", short = 'a')]
send_all: bool,
Expand Down
107 changes: 107 additions & 0 deletions src/dns_payment_instructions.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
use bdk_wallet::bitcoin::{Address, Amount, Network};
use bitcoin_payment_instructions::{
FixedAmountPaymentInstructions, ParseError, PaymentInstructions, PaymentMethod,
PaymentMethodType, amount, dns_resolver::DNSHrnResolver, hrn_resolution::HrnResolver,
};
use core::{net::SocketAddr, str::FromStr};

async fn parse_dns_instructions(
hrn: &str,
resolver: &impl HrnResolver,
network: Network,
) -> Result<PaymentInstructions, ParseError> {
let instructions = PaymentInstructions::parse(hrn, network, resolver, true).await?;
Ok(instructions)
}

#[derive(Debug)]
#[allow(dead_code)]
pub struct Payment {
pub address: Address,
pub amount: Amount,
pub min_amount: Option<Amount>,
pub max_amount: Option<Amount>,
pub dnssec_proof: Option<Vec<u8>>,
}

fn process_fixed_instructions(
instructions: &FixedAmountPaymentInstructions,
) -> Result<Payment, ParseError> {
// 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.milli_sats());

Ok(Payment {
address: addr.clone(),
amount: onchain_amount,
min_amount: None,
max_amount: None,
dnssec_proof: instructions.bip_353_dnssec_proof().clone(),
})
}

pub async fn resolve_dns_recipient(
hrn: &str,
amount: Amount,
network: Network,
) -> Result<Payment, ParseError> {
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()
.map(|amnt| Amount::from_sat(amnt.sats_rounding_up()));
let max_amount = instructions
.max_amt()
.map(|amnt| Amount::from_sat(amnt.sats_rounding_up()));

let fixed_instructions = instructions
.set_amount(
amount::Amount::from_sats(amount.to_sat()).unwrap(),
&resolver,
)
.await
.map_err(|s| ParseError::InvalidInstructions(s))?;

let mut instructions = process_fixed_instructions(&fixed_instructions)?;

instructions.min_amount = min_amount;
instructions.max_amount = max_amount;

Ok(instructions)
}

PaymentInstructions::FixedAmount(instructions) => process_fixed_instructions(&instructions),
}
}
48 changes: 45 additions & 3 deletions src/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@
//! This module describes all the command handling logic used by bdk-cli.
use crate::commands::OfflineWalletSubCommand::*;
use crate::commands::*;
use crate::dns_payment_instructions::resolve_dns_recipient;
use crate::error::BDKCliError as Error;
#[cfg(any(feature = "sqlite", feature = "redb"))]
use crate::persister::Persister;
use crate::utils::*;
#[cfg(feature = "redb")]
use bdk_redb::Store as RedbStore;
use bdk_wallet::bip39::{Language, Mnemonic};
use bdk_wallet::bitcoin::ScriptBuf;
use bdk_wallet::bitcoin::base64::Engine;
use bdk_wallet::bitcoin::base64::prelude::BASE64_STANDARD;
use bdk_wallet::bitcoin::{
Expand Down Expand Up @@ -94,7 +96,7 @@ const NUMS_UNSPENDABLE_KEY_HEX: &str =
/// Execute an offline wallet sub-command
///
/// Offline wallet sub-commands are described in [`OfflineWalletSubCommand`].
pub fn handle_offline_wallet_subcommand(
pub async fn handle_offline_wallet_subcommand(
wallet: &mut Wallet,
wallet_opts: &WalletOpts,
cli_opts: &CliOpts,
Expand Down Expand Up @@ -328,8 +330,22 @@ pub fn handle_offline_wallet_subcommand(
}
}

ResolveDnsRecipient { hrn, amount } => {
let resolved = resolve_dns_recipient(&hrn, Amount::from_sat(amount), wallet.network())
.await
.map_err(|e| Error::Generic(format!("{:?}", e)))?;

Ok(serde_json::to_string_pretty(&json!({
"hrn": hrn,
"recipient": resolved.address,
"min_amount": resolved.min_amount.unwrap_or_default(),
"max_amount": resolved.max_amount.unwrap_or_default(),
}))?)
}

CreateTx {
recipients,
dns_recipients,
send_all,
enable_rbf,
offline_signer,
Expand All @@ -346,10 +362,34 @@ pub fn handle_offline_wallet_subcommand(
if send_all {
tx_builder.drain_wallet().drain_to(recipients[0].0.clone());
} else {
let recipients = recipients
let mut recipients: Vec<(ScriptBuf, Amount)> = recipients
.into_iter()
.map(|(script, amount)| (script, Amount::from_sat(amount)))
.collect();

if let Some(recip) = dns_recipients {
let parsed_recip = parse_dns_recipients(&recip)
.await
.map_err(|pe| Error::Generic(format!("Resolution failed: {pe}")))?;

// Validates if the amount the user wants to send is in the range of what the payment instructions returned
parsed_recip.iter().try_for_each(|(_, r)| {
let amount = r.amount.to_sat();
if r.min_amount.map_or(false, |min| amount < min.to_sat()) {
return Err(Error::Generic("Amount lesser than min".to_string()));
}
if r.max_amount.map_or(false, |max| amount > max.to_sat()) {
return Err(Error::Generic("Amount greater than max".to_string()));
}
Ok(())
})?;

let mut vec_recip = parsed_recip
.iter()
.map(|recip| (recip.1.address.script_pubkey(), recip.1.amount))
.collect::<Vec<_>>();
recipients.append(&mut vec_recip);
}
tx_builder.set_recipients(recipients);
}

Expand Down Expand Up @@ -1046,7 +1086,8 @@ pub(crate) async fn handle_command(cli_opts: CliOpts) -> Result<String, Error> {
wallet_opts,
&cli_opts,
offline_subcommand.clone(),
)?;
)
.await?;
wallet.persist(&mut persister)?;
result
};
Expand Down Expand Up @@ -1194,6 +1235,7 @@ async fn respond(
} => {
let value =
handle_offline_wallet_subcommand(wallet, wallet_opts, cli_opts, offline_subcommand)
.await
.map_err(|e| e.to_string())?;
Some(value)
}
Expand Down
Loading
Loading