Skip to content

Commit 34ee78d

Browse files
authored
Avoid stringly typed arguments in payjoin-cli App trait (#479)
A more comprehensive approach might be to allow explicit units to be parsed from the string form of these arguments (see also rust-bitcoin/rust-bitcoin#1786) that may be undesirable since it complicates payjoin-cli, and as a reference implementation it should prioritize readability and simplicity over convenience/flexibility of the CLI.
2 parents 71cf9e0 + 7c53399 commit 34ee78d

4 files changed

Lines changed: 30 additions & 25 deletions

File tree

payjoin-cli/src/app/mod.rs

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use bitcoin::TxIn;
77
use bitcoincore_rpc::bitcoin::Amount;
88
use bitcoincore_rpc::RpcApi;
99
use payjoin::bitcoin::psbt::Psbt;
10+
use payjoin::bitcoin::FeeRate;
1011
use payjoin::receive::InputPair;
1112
use payjoin::{bitcoin, PjUri};
1213

@@ -27,18 +28,15 @@ pub trait App {
2728
where
2829
Self: Sized;
2930
fn bitcoind(&self) -> Result<bitcoincore_rpc::Client>;
30-
async fn send_payjoin(&self, bip21: &str, fee_rate: &f32) -> Result<()>;
31-
async fn receive_payjoin(self, amount_arg: &str) -> Result<()>;
31+
async fn send_payjoin(&self, bip21: &str, fee_rate: FeeRate) -> Result<()>;
32+
async fn receive_payjoin(self, amount: Amount) -> Result<()>;
3233

33-
fn create_original_psbt(&self, uri: &PjUri, fee_rate: &f32) -> Result<Psbt> {
34+
fn create_original_psbt(&self, uri: &PjUri, fee_rate: FeeRate) -> Result<Psbt> {
3435
let amount = uri.amount.ok_or_else(|| anyhow!("please specify the amount in the Uri"))?;
3536

3637
// wallet_create_funded_psbt requires a HashMap<address: String, Amount>
3738
let mut outputs = HashMap::with_capacity(1);
3839
outputs.insert(uri.address.to_string(), amount);
39-
let fee_rate_sat_per_kwu = fee_rate * 250.0_f32;
40-
let fee_rate: bitcoin::FeeRate =
41-
bitcoin::FeeRate::from_sat_per_kwu(fee_rate_sat_per_kwu.ceil() as u64);
4240
let fee_sat_per_kvb =
4341
fee_rate.to_sat_per_kwu().checked_mul(4).ok_or(anyhow!("Invalid fee rate"))?;
4442
let fee_per_kvb = Amount::from_sat(fee_sat_per_kvb);

payjoin-cli/src/app/v1.rs

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -68,14 +68,12 @@ impl AppTrait for App {
6868
.with_context(|| "Failed to connect to bitcoind")
6969
}
7070

71-
async fn send_payjoin(&self, bip21: &str, fee_rate: &f32) -> Result<()> {
71+
async fn send_payjoin(&self, bip21: &str, fee_rate: FeeRate) -> Result<()> {
7272
let uri =
7373
Uri::try_from(bip21).map_err(|e| anyhow!("Failed to create URI from BIP21: {}", e))?;
7474
let uri = uri.assume_checked();
7575
let uri = uri.check_pj_supported().map_err(|_| anyhow!("URI does not support Payjoin"))?;
7676
let psbt = self.create_original_psbt(&uri, fee_rate)?;
77-
let fee_rate_sat_per_kwu = fee_rate * 250.0_f32;
78-
let fee_rate = FeeRate::from_sat_per_kwu(fee_rate_sat_per_kwu.ceil() as u64);
7977
let (req, ctx) = SenderBuilder::new(psbt, uri.clone())
8078
.build_recommended(fee_rate)
8179
.with_context(|| "Failed to build payjoin request")?
@@ -109,8 +107,8 @@ impl AppTrait for App {
109107
Ok(())
110108
}
111109

112-
async fn receive_payjoin(self, amount_arg: &str) -> Result<()> {
113-
let pj_uri_string = self.construct_payjoin_uri(amount_arg, None)?;
110+
async fn receive_payjoin(self, amount: Amount) -> Result<()> {
111+
let pj_uri_string = self.construct_payjoin_uri(amount, None)?;
114112
println!(
115113
"Listening at {}. Configured to accept payjoin at BIP 21 Payjoin Uri:",
116114
self.config.port
@@ -125,11 +123,10 @@ impl AppTrait for App {
125123
impl App {
126124
fn construct_payjoin_uri(
127125
&self,
128-
amount_arg: &str,
126+
amount: Amount,
129127
fallback_target: Option<&str>,
130128
) -> Result<String> {
131129
let pj_receiver_address = self.bitcoind()?.get_new_address(None, None)?.assume_checked();
132-
let amount = Amount::from_sat(amount_arg.parse()?);
133130
let pj_part = match fallback_target {
134131
Some(target) => target,
135132
None => self.config.pj_endpoint.as_str(),

payjoin-cli/src/app/v2.rs

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ impl AppTrait for App {
5454
.with_context(|| "Failed to connect to bitcoind")
5555
}
5656

57-
async fn send_payjoin(&self, bip21: &str, fee_rate: &f32) -> Result<()> {
57+
async fn send_payjoin(&self, bip21: &str, fee_rate: FeeRate) -> Result<()> {
5858
use payjoin::UriExt;
5959
let uri =
6060
Uri::try_from(bip21).map_err(|e| anyhow!("Failed to create URI from BIP21: {}", e))?;
@@ -66,8 +66,6 @@ impl AppTrait for App {
6666
Some(send_session) => send_session,
6767
None => {
6868
let psbt = self.create_original_psbt(&uri, fee_rate)?;
69-
let fee_rate_sat_per_kwu = fee_rate * 250.0_f32;
70-
let fee_rate = FeeRate::from_sat_per_kwu(fee_rate_sat_per_kwu.ceil() as u64);
7169
let mut req_ctx = SenderBuilder::new(psbt, uri.clone())
7270
.build_recommended(fee_rate)
7371
.with_context(|| "Failed to build payjoin request")?;
@@ -78,9 +76,8 @@ impl AppTrait for App {
7876
self.spawn_payjoin_sender(req_ctx).await
7977
}
8078

81-
async fn receive_payjoin(self, amount_arg: &str) -> Result<()> {
79+
async fn receive_payjoin(self, amount: Amount) -> Result<()> {
8280
let address = self.bitcoind()?.get_new_address(None, None)?.assume_checked();
83-
let amount = Amount::from_sat(amount_arg.parse()?);
8481
let ohttp_keys = unwrap_ohttp_keys_or_else_fetch(&self.config).await?;
8582
let session =
8683
Receiver::new(address, self.config.pj_directory.clone(), ohttp_keys.clone(), None);

payjoin-cli/src/main.rs

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ use anyhow::{Context, Result};
22
use app::config::AppConfig;
33
use app::App as AppTrait;
44
use clap::{arg, value_parser, Arg, ArgMatches, Command};
5+
use payjoin::bitcoin::amount::ParseAmountError;
6+
use payjoin::bitcoin::{Amount, FeeRate};
57
use url::Url;
68

79
mod app;
@@ -23,14 +25,15 @@ async fn main() -> Result<()> {
2325
match matches.subcommand() {
2426
Some(("send", sub_matches)) => {
2527
let bip21 = sub_matches.get_one::<String>("BIP21").context("Missing BIP21 argument")?;
26-
let fee_rate_sat_per_vb =
27-
sub_matches.get_one::<f32>("fee_rate").context("Missing --fee-rate argument")?;
28-
app.send_payjoin(bip21, fee_rate_sat_per_vb).await?;
28+
let fee_rate = sub_matches
29+
.get_one::<FeeRate>("fee_rate")
30+
.context("Missing --fee-rate argument")?;
31+
app.send_payjoin(bip21, *fee_rate).await?;
2932
}
3033
Some(("receive", sub_matches)) => {
3134
let amount =
32-
sub_matches.get_one::<String>("AMOUNT").context("Missing AMOUNT argument")?;
33-
app.receive_payjoin(amount).await?;
35+
sub_matches.get_one::<Amount>("AMOUNT").context("Missing AMOUNT argument")?;
36+
app.receive_payjoin(*amount).await?;
3437
}
3538
#[cfg(feature = "v2")]
3639
Some(("resume", _)) => {
@@ -98,13 +101,13 @@ fn cli() -> ArgMatches {
98101
.long("fee-rate")
99102
.value_name("FEE_SAT_PER_VB")
100103
.help("Fee rate in sat/vB")
101-
.value_parser(value_parser!(f32)),
104+
.value_parser(parse_feerate_in_sat_per_vb),
102105
),
103106
);
104107

105108
let mut receive_cmd = Command::new("receive")
106109
.arg_required_else_help(true)
107-
.arg(arg!(<AMOUNT> "The amount to receive in satoshis"))
110+
.arg(arg!(<AMOUNT> "The amount to receive in satoshis").value_parser(parse_amount_in_sat))
108111
.arg_required_else_help(true);
109112

110113
#[cfg(feature = "v2")]
@@ -152,3 +155,13 @@ fn cli() -> ArgMatches {
152155
cmd = cmd.subcommand(receive_cmd);
153156
cmd.get_matches()
154157
}
158+
159+
fn parse_amount_in_sat(s: &str) -> Result<Amount, ParseAmountError> {
160+
Amount::from_str_in(s, payjoin::bitcoin::Denomination::Satoshi)
161+
}
162+
163+
fn parse_feerate_in_sat_per_vb(s: &str) -> Result<FeeRate, std::num::ParseFloatError> {
164+
let fee_rate_sat_per_vb: f32 = s.parse()?;
165+
let fee_rate_sat_per_kwu = fee_rate_sat_per_vb * 250.0_f32;
166+
Ok(FeeRate::from_sat_per_kwu(fee_rate_sat_per_kwu.ceil() as u64))
167+
}

0 commit comments

Comments
 (0)