Skip to content

Commit ba3c713

Browse files
committed
add resolve_dns_recipient command
1 parent fb7f6c6 commit ba3c713

File tree

6 files changed

+234
-5
lines changed

6 files changed

+234
-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: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,11 @@ 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+
},
280285
/// Creates a new unsigned transaction.
281286
CreateTx {
282287
/// Adds a recipient to the transaction.

src/dns_payment_instructions.rs

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
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+
pub struct Payment {
19+
pub address: Address,
20+
pub amount: Amount,
21+
pub dnssec_proof: Option<Vec<u8>>,
22+
}
23+
24+
fn process_fixed_instructions(
25+
amount: Amount,
26+
instructions: &FixedAmountPaymentInstructions,
27+
) -> Result<Payment, ParseError> {
28+
// Look for on chain payment method as it's the only one we can support
29+
let PaymentMethod::OnChain(addr) = instructions
30+
.methods()
31+
.iter()
32+
.find(|ix| matches!(ix, PaymentMethod::OnChain(_)))
33+
.map(|pm| pm)
34+
.unwrap()
35+
else {
36+
return Err(ParseError::InvalidInstructions(
37+
"Unsupported payment method",
38+
));
39+
};
40+
41+
let Some(onchain_amount) = instructions.onchain_payment_amount() else {
42+
return Err(ParseError::InvalidInstructions(
43+
"On chain amount should be specified",
44+
));
45+
};
46+
47+
// We need this conversion since Amount from instructions is different from Amount from bitcoin
48+
let onchain_amount = Amount::from_sat(onchain_amount.sats_rounding_up());
49+
50+
if onchain_amount != amount {
51+
return Err(ParseError::InvalidInstructions(
52+
"Mismatched amount expected , got",
53+
));
54+
}
55+
56+
Ok(Payment {
57+
address: addr.clone(),
58+
amount: onchain_amount,
59+
dnssec_proof: instructions.bip_353_dnssec_proof().clone(),
60+
})
61+
}
62+
63+
64+
pub async fn resolve_dns_recipient(
65+
hrn: &str,
66+
amount: Amount,
67+
network: Network,
68+
) -> Result<Payment, ParseError> {
69+
let resolver = DNSHrnResolver(SocketAddr::from_str("8.8.8.8:53").expect("Should not fail."));
70+
let payment_instructions = parse_dns_instructions(hrn, &resolver, network).await?;
71+
72+
match payment_instructions {
73+
PaymentInstructions::ConfigurableAmount(instructions) => {
74+
// Look for on chain payment method as it's the only one we can support
75+
if instructions
76+
.methods()
77+
.find(|method| matches!(method.method_type(), PaymentMethodType::OnChain))
78+
.is_none()
79+
{
80+
return Err(ParseError::InvalidInstructions(
81+
"Unsupported payment method",
82+
));
83+
}
84+
85+
let min_amount = instructions.min_amt();
86+
let max_amount = instructions.max_amt();
87+
88+
if min_amount.is_some() {
89+
let min_amount = min_amount
90+
.map(|a| Amount::from_sat(a.sats_rounding_up()))
91+
.unwrap();
92+
if amount < min_amount {
93+
return Err(ParseError::InvalidInstructions(
94+
"Amount lesser than min amount",
95+
));
96+
}
97+
}
98+
99+
if max_amount.is_some() {
100+
let max_amount = max_amount
101+
.map(|a| Amount::from_sat(a.sats_rounding_up()))
102+
.unwrap();
103+
if amount > max_amount {
104+
return Err(ParseError::InvalidInstructions(
105+
"Amount greater than max amount",
106+
));
107+
}
108+
}
109+
110+
let fixed_instructions = instructions
111+
.set_amount(
112+
amount::Amount::from_sats(amount.to_sat()).unwrap(),
113+
&resolver,
114+
)
115+
.await
116+
.map_err(|s| ParseError::InvalidInstructions(s))?;
117+
118+
process_fixed_instructions(amount, &fixed_instructions)
119+
}
120+
121+
PaymentInstructions::FixedAmount(instructions) => {
122+
process_fixed_instructions(amount, &instructions)
123+
}
124+
}
125+
}

src/handlers.rs

Lines changed: 15 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,16 @@ pub fn handle_offline_wallet_subcommand(
328329
}
329330
}
330331

332+
ResolveDnsRecipient { hrn } => {
333+
let resolved = resolve_dns_recipient(&hrn, Amount::from_sat(0), Network::Bitcoin)
334+
.await
335+
.map_err(|e| Error::Generic(format!("{:?}", e)))?;
336+
337+
Ok(serde_json::to_string_pretty(
338+
&json!({"hrn": hrn, "recipient": resolved.address}),
339+
)?)
340+
}
341+
331342
CreateTx {
332343
recipients,
333344
send_all,
@@ -1046,7 +1057,8 @@ pub(crate) async fn handle_command(cli_opts: CliOpts) -> Result<String, Error> {
10461057
wallet_opts,
10471058
&cli_opts,
10481059
offline_subcommand.clone(),
1049-
)?;
1060+
)
1061+
.await?;
10501062
wallet.persist(&mut persister)?;
10511063
result
10521064
};
@@ -1194,6 +1206,7 @@ async fn respond(
11941206
} => {
11951207
let value =
11961208
handle_offline_wallet_subcommand(wallet, wallet_opts, cli_opts, offline_subcommand)
1209+
.await
11971210
.map_err(|e| e.to_string())?;
11981211
Some(value)
11991212
}

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)