Skip to content

Commit a0f3e9d

Browse files
committed
add resolve_dns_recipient command
1 parent fb7f6c6 commit a0f3e9d

File tree

6 files changed

+221
-5
lines changed

6 files changed

+221
-5
lines changed

Cargo.lock

Lines changed: 83 additions & 2 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
@@ -34,6 +34,7 @@ shlex = { version = "1.3.0", optional = true }
3434
payjoin = { version = "1.0.0-rc.1", features = ["v1", "v2", "io", "_test-utils"], optional = true}
3535
reqwest = { version = "0.12.23", default-features = false, optional = true }
3636
url = { version = "2.5.4", optional = true }
37+
bitcoin-payment-instructions = { version = "0.5.0", optional = true}
3738

3839
[features]
3940
default = ["repl", "sqlite"]
@@ -49,7 +50,8 @@ redb = ["bdk_redb"]
4950
cbf = ["bdk_kyoto", "_payjoin-dependencies"]
5051
electrum = ["bdk_electrum", "_payjoin-dependencies"]
5152
esplora = ["bdk_esplora", "_payjoin-dependencies"]
52-
rpc = ["bdk_bitcoind_rpc", "_payjoin-dependencies"]
53+
rpc = ["bdk_bitcoind_rpc", "_payjoin-dependencies"]
54+
dns_payment = ["bitcoin-payment-instructions"]
5355

5456
# Internal features
5557
_payjoin-dependencies = ["payjoin", "reqwest", "url"]

src/commands.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,13 @@ pub enum OfflineWalletSubCommand {
277277
Transactions,
278278
/// Returns the current wallet balance.
279279
Balance,
280+
/// Resolves the given DNS payment instructions
281+
ResolveDnsRecipient {
282+
/// Human Readable Name to resolve
283+
hrn: String,
284+
/// The amount you're willing to send to the HRN
285+
amount: u64,
286+
},
280287
/// Creates a new unsigned transaction.
281288
CreateTx {
282289
/// 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: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
//! This module describes all the command handling logic used by bdk-cli.
1212
use crate::commands::OfflineWalletSubCommand::*;
1313
use crate::commands::*;
14+
use crate::dns_payment_instructions::resolve_dns_recipient;
1415
use crate::error::BDKCliError as Error;
1516
#[cfg(any(feature = "sqlite", feature = "redb"))]
1617
use crate::persister::Persister;
@@ -94,7 +95,7 @@ const NUMS_UNSPENDABLE_KEY_HEX: &str =
9495
/// Execute an offline wallet sub-command
9596
///
9697
/// Offline wallet sub-commands are described in [`OfflineWalletSubCommand`].
97-
pub fn handle_offline_wallet_subcommand(
98+
pub async fn handle_offline_wallet_subcommand(
9899
wallet: &mut Wallet,
99100
wallet_opts: &WalletOpts,
100101
cli_opts: &CliOpts,
@@ -328,6 +329,19 @@ pub fn handle_offline_wallet_subcommand(
328329
}
329330
}
330331

332+
ResolveDnsRecipient { hrn, amount } => {
333+
let resolved = resolve_dns_recipient(&hrn, Amount::from_sat(amount), Network::Bitcoin)
334+
.await
335+
.map_err(|e| Error::Generic(format!("{:?}", e)))?;
336+
337+
Ok(serde_json::to_string_pretty(&json!({
338+
"hrn": hrn,
339+
"recipient": resolved.address,
340+
"min_amount": resolved.min_amount.unwrap_or_default(),
341+
"max_amount": resolved.max_amount.unwrap_or_default(),
342+
}))?)
343+
}
344+
331345
CreateTx {
332346
recipients,
333347
send_all,
@@ -1046,7 +1060,8 @@ pub(crate) async fn handle_command(cli_opts: CliOpts) -> Result<String, Error> {
10461060
wallet_opts,
10471061
&cli_opts,
10481062
offline_subcommand.clone(),
1049-
)?;
1063+
)
1064+
.await?;
10501065
wallet.persist(&mut persister)?;
10511066
result
10521067
};
@@ -1194,6 +1209,7 @@ async fn respond(
11941209
} => {
11951210
let value =
11961211
handle_offline_wallet_subcommand(wallet, wallet_opts, cli_opts, offline_subcommand)
1212+
.await
11971213
.map_err(|e| e.to_string())?;
11981214
Some(value)
11991215
}

src/main.rs

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

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

0 commit comments

Comments
 (0)