Skip to content

Commit 1805fd7

Browse files
committed
add resolve_dns_recipient command
1 parent b9cf2ac commit 1805fd7

File tree

6 files changed

+222
-3
lines changed

6 files changed

+222
-3
lines changed

Cargo.lock

Lines changed: 81 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ shlex = { version = "1.3.0", optional = true }
3636
payjoin = { version = "1.0.0-rc.1", features = ["v1", "v2", "io", "_test-utils"], optional = true}
3737
reqwest = { version = "0.12.23", default-features = false, optional = true }
3838
url = { version = "2.5.4", optional = true }
39+
bitcoin-payment-instructions = { version = "0.5.0", optional = true}
3940

4041
[features]
4142
default = ["repl", "sqlite"]
@@ -51,7 +52,8 @@ redb = ["bdk_redb"]
5152
cbf = ["bdk_kyoto", "_payjoin-dependencies"]
5253
electrum = ["bdk_electrum", "_payjoin-dependencies"]
5354
esplora = ["bdk_esplora", "_payjoin-dependencies"]
54-
rpc = ["bdk_bitcoind_rpc", "_payjoin-dependencies"]
55+
rpc = ["bdk_bitcoind_rpc", "_payjoin-dependencies"]
56+
dns_payment = ["bitcoin-payment-instructions"]
5557

5658
# Internal features
5759
_payjoin-dependencies = ["payjoin", "reqwest", "url"]

src/commands.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,14 @@ pub enum OfflineWalletSubCommand {
291291
Transactions,
292292
/// Returns the current wallet balance.
293293
Balance,
294+
/// Resolves the given DNS payment instructions
295+
#[cfg(feature = "dns_payment")]
296+
ResolveDnsRecipient {
297+
/// Human Readable Name to resolve
298+
hrn: String,
299+
/// The amount you're willing to send to the HRN
300+
amount: u64,
301+
},
294302
/// Creates a new unsigned transaction.
295303
CreateTx {
296304
/// Adds a recipient to the transaction.

src/dns_payment_instructions.rs

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
use bdk_wallet::bitcoin::{Address, Amount, Network};
2+
use bitcoin_payment_instructions::{
3+
FixedAmountPaymentInstructions, ParseError, PaymentInstructions, PaymentMethod,
4+
PaymentMethodType, amount, dns_resolver::DNSHrnResolver, hrn_resolution::HrnResolver,
5+
};
6+
use core::{net::SocketAddr, str::FromStr};
7+
8+
async fn parse_dns_instructions(
9+
hrn: &str,
10+
resolver: &impl HrnResolver,
11+
network: Network,
12+
) -> Result<PaymentInstructions, ParseError> {
13+
let instructions = PaymentInstructions::parse(hrn, network, resolver, true).await?;
14+
Ok(instructions)
15+
}
16+
17+
#[derive(Debug)]
18+
#[allow(dead_code)]
19+
pub struct Payment {
20+
pub address: Address,
21+
pub amount: Amount,
22+
pub min_amount: Option<Amount>,
23+
pub max_amount: Option<Amount>,
24+
pub dnssec_proof: Option<Vec<u8>>,
25+
}
26+
27+
fn process_fixed_instructions(
28+
instructions: &FixedAmountPaymentInstructions,
29+
) -> Result<Payment, ParseError> {
30+
// Look for on chain payment method as it's the only one we can support
31+
let PaymentMethod::OnChain(addr) = instructions
32+
.methods()
33+
.iter()
34+
.find(|ix| matches!(ix, PaymentMethod::OnChain(_)))
35+
.map(|pm| pm)
36+
.unwrap()
37+
else {
38+
return Err(ParseError::InvalidInstructions(
39+
"Unsupported payment method",
40+
));
41+
};
42+
43+
let Some(onchain_amount) = instructions.onchain_payment_amount() else {
44+
return Err(ParseError::InvalidInstructions(
45+
"On chain amount should be specified",
46+
));
47+
};
48+
49+
// We need this conversion since Amount from instructions is different from Amount from bitcoin
50+
let onchain_amount = Amount::from_sat(onchain_amount.sats_rounding_up());
51+
52+
Ok(Payment {
53+
address: addr.clone(),
54+
amount: onchain_amount,
55+
min_amount: None,
56+
max_amount: None,
57+
dnssec_proof: instructions.bip_353_dnssec_proof().clone(),
58+
})
59+
}
60+
61+
pub async fn resolve_dns_recipient(
62+
hrn: &str,
63+
amount: Amount,
64+
network: Network,
65+
) -> Result<Payment, ParseError> {
66+
let resolver = DNSHrnResolver(SocketAddr::from_str("8.8.8.8:53").expect("Should not fail."));
67+
let payment_instructions = parse_dns_instructions(hrn, &resolver, network).await?;
68+
69+
match payment_instructions {
70+
PaymentInstructions::ConfigurableAmount(instructions) => {
71+
// Look for on chain payment method as it's the only one we can support
72+
if instructions
73+
.methods()
74+
.find(|method| matches!(method.method_type(), PaymentMethodType::OnChain))
75+
.is_none()
76+
{
77+
return Err(ParseError::InvalidInstructions(
78+
"Unsupported payment method",
79+
));
80+
}
81+
82+
let min_amount = instructions
83+
.min_amt()
84+
.map(|amnt| Amount::from_sat(amnt.sats_rounding_up()));
85+
let max_amount = instructions
86+
.max_amt()
87+
.map(|amnt| Amount::from_sat(amnt.sats_rounding_up()));
88+
89+
let fixed_instructions = instructions
90+
.set_amount(
91+
amount::Amount::from_sats(amount.to_sat()).unwrap(),
92+
&resolver,
93+
)
94+
.await
95+
.map_err(|s| ParseError::InvalidInstructions(s))?;
96+
97+
let mut instructions = process_fixed_instructions(&fixed_instructions)?;
98+
99+
instructions.min_amount = min_amount;
100+
instructions.max_amount = max_amount;
101+
102+
Ok(instructions)
103+
}
104+
105+
PaymentInstructions::FixedAmount(instructions) => process_fixed_instructions(&instructions),
106+
}
107+
}

src/handlers.rs

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
use crate::commands::OfflineWalletSubCommand::*;
1313
use crate::commands::*;
1414
use crate::config::{WalletConfig, WalletConfigInner};
15+
#[cfg(feature = "dns_payment")]
16+
use crate::dns_payment_instructions::resolve_dns_recipient;
1517
use crate::error::BDKCliError as Error;
1618
#[cfg(any(feature = "sqlite", feature = "redb"))]
1719
use crate::persister::Persister;
@@ -97,7 +99,7 @@ const NUMS_UNSPENDABLE_KEY_HEX: &str =
9799
/// Execute an offline wallet sub-command
98100
///
99101
/// Offline wallet sub-commands are described in [`OfflineWalletSubCommand`].
100-
pub fn handle_offline_wallet_subcommand(
102+
pub async fn handle_offline_wallet_subcommand(
101103
wallet: &mut Wallet,
102104
wallet_opts: &WalletOpts,
103105
cli_opts: &CliOpts,
@@ -330,6 +332,20 @@ pub fn handle_offline_wallet_subcommand(
330332
)?)
331333
}
332334
}
335+
336+
#[cfg(feature = "dns_payment")]
337+
ResolveDnsRecipient { hrn, amount } => {
338+
let resolved = resolve_dns_recipient(&hrn, Amount::from_sat(amount), Network::Bitcoin)
339+
.await
340+
.map_err(|e| Error::Generic(format!("{:?}", e)))?;
341+
342+
Ok(serde_json::to_string_pretty(&json!({
343+
"hrn": hrn,
344+
"recipient": resolved.address,
345+
"min_amount": resolved.min_amount.unwrap_or_default(),
346+
"max_amount": resolved.max_amount.unwrap_or_default(),
347+
}))?)
348+
}
333349

334350
CreateTx {
335351
recipients,
@@ -1300,7 +1316,8 @@ pub(crate) async fn handle_command(cli_opts: CliOpts) -> Result<String, Error> {
13001316
&wallet_opts,
13011317
&cli_opts,
13021318
offline_subcommand.clone(),
1303-
)?;
1319+
)
1320+
.await?;
13041321
wallet.persist(&mut persister)?;
13051322
result
13061323
};
@@ -1462,6 +1479,7 @@ async fn respond(
14621479
} => {
14631480
let value =
14641481
handle_offline_wallet_subcommand(wallet, wallet_opts, cli_opts, offline_subcommand)
1482+
.await
14651483
.map_err(|e| e.to_string())?;
14661484
Some(value)
14671485
}

src/main.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ mod payjoin;
2525
mod persister;
2626
mod utils;
2727

28+
#[cfg(feature = "dns_payment")]
29+
mod dns_payment_instructions;
30+
2831
use bdk_wallet::bitcoin::Network;
2932
use log::{debug, error, warn};
3033

0 commit comments

Comments
 (0)