• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

payjoin / rust-payjoin / 13362053154

17 Feb 2025 03:09AM UTC coverage: 79.423% (+0.3%) from 79.127%
13362053154

Pull #538

github

web-flow
Merge de4b03f42 into 39cfaff2a
Pull Request #538: Make payjoin-cli v1 / v2 features additive

376 of 435 new or added lines in 6 files covered. (86.44%)

27 existing lines in 1 file now uncovered.

4130 of 5200 relevant lines covered (79.42%)

885.21 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.99
/payjoin-cli/src/main.rs
1
use anyhow::{Context, Result};
2
use app::config::Config;
3
use app::App as AppTrait;
4
use clap::{arg, value_parser, Arg, ArgMatches, Command};
5
use payjoin::bitcoin::amount::ParseAmountError;
6
use payjoin::bitcoin::{Amount, FeeRate};
7
use url::Url;
8

9
mod app;
10
mod db;
11

12
#[tokio::main]
13
async fn main() -> Result<()> {
8✔
14
    env_logger::init();
8✔
15

8✔
16
    let matches = cli();
8✔
17
    let config = Config::new(&matches)?;
8✔
18

8✔
19
    #[allow(clippy::if_same_then_else)]
8✔
20
    let app: Box<dyn AppTrait> = if matches.get_flag("bip78") {
8✔
21
        #[cfg(feature = "v1")]
8✔
22
        {
8✔
23
            Box::new(crate::app::v1::App::new(config)?)
8✔
24
        }
8✔
25
        #[cfg(not(feature = "v1"))]
8✔
26
        {
8✔
27
            anyhow::bail!(
8✔
28
                "BIP78 (v1) support is not enabled in this build. Recompile with --features v1"
8✔
29
            )
8✔
30
        }
8✔
31
    } else if matches.get_flag("bip77") {
8✔
32
        #[cfg(feature = "v2")]
8✔
33
        {
8✔
34
            Box::new(crate::app::v2::App::new(config)?)
8✔
35
        }
8✔
36
        #[cfg(not(feature = "v2"))]
8✔
37
        {
8✔
38
            anyhow::bail!(
8✔
NEW
39
                "BIP77 (v2) support is not enabled in this build. Recompile with --features v2"
×
NEW
40
            )
×
41
        }
8✔
42
    } else {
8✔
43
        #[cfg(feature = "v2")]
8✔
44
        {
8✔
45
            Box::new(crate::app::v2::App::new(config)?)
8✔
46
        }
8✔
47
        #[cfg(all(feature = "v1", not(feature = "v2")))]
8✔
48
        {
8✔
49
            Box::new(crate::app::v1::App::new(config)?)
8✔
50
        }
8✔
51
        #[cfg(not(any(feature = "v1", feature = "v2")))]
8✔
52
        {
8✔
53
            anyhow::bail!("No valid version available - must compile with v1 or v2 feature")
8✔
54
        }
8✔
55
    };
8✔
56

8✔
57
    match matches.subcommand() {
8✔
58
        Some(("send", sub_matches)) => {
8✔
59
            let bip21 = sub_matches.get_one::<String>("BIP21").context("Missing BIP21 argument")?;
8✔
60
            let fee_rate = sub_matches
8✔
61
                .get_one::<FeeRate>("fee_rate")
4✔
62
                .context("Missing --fee-rate argument")?;
4✔
63
            app.send_payjoin(bip21, *fee_rate).await?;
8✔
64
        }
8✔
65
        Some(("receive", sub_matches)) => {
8✔
66
            let amount =
8✔
67
                sub_matches.get_one::<Amount>("AMOUNT").context("Missing AMOUNT argument")?;
8✔
68
            app.receive_payjoin(*amount).await?;
8✔
69
        }
8✔
70
        #[cfg(feature = "v2")]
8✔
71
        Some(("resume", _)) => {
8✔
72
            if matches.get_flag("bip78") {
8✔
73
                anyhow::bail!("Resume command is only available with BIP77 (v2)");
8✔
74
            }
8✔
75
            println!("resume");
1✔
76
            app.resume_payjoins().await?;
1✔
77
        }
8✔
78
        _ => unreachable!(), // If all subcommands are defined above, anything else is unreachabe!()
8✔
79
    }
8✔
80

8✔
81
    Ok(())
8✔
82
}
8✔
83

84
fn cli() -> ArgMatches {
2✔
85
    let mut cmd = Command::new("payjoin")
2✔
86
        .version(env!("CARGO_PKG_VERSION"))
2✔
87
        .about("Payjoin - bitcoin scaling, savings, and privacy by default")
2✔
88
        .arg(
2✔
89
            Arg::new("bip77")
2✔
90
                .long("bip77")
2✔
91
                .help("Use BIP77 (v2) protocol (default)")
2✔
92
                .conflicts_with("bip78")
2✔
93
                .action(clap::ArgAction::SetTrue),
2✔
94
        )
2✔
95
        .arg(
2✔
96
            Arg::new("bip78")
2✔
97
                .long("bip78")
2✔
98
                .help("Use BIP78 (v1) protocol")
2✔
99
                .conflicts_with("bip77")
2✔
100
                .action(clap::ArgAction::SetTrue),
2✔
101
        )
2✔
102
        .arg(
2✔
103
            Arg::new("rpchost")
2✔
104
                .long("rpchost")
2✔
105
                .short('r')
2✔
106
                .num_args(1)
2✔
107
                .help("The port of the bitcoin node")
2✔
108
                .value_parser(value_parser!(Url)),
2✔
109
        )
2✔
110
        .arg(
2✔
111
            Arg::new("cookie_file")
2✔
112
                .long("cookie-file")
2✔
113
                .short('c')
2✔
114
                .num_args(1)
2✔
115
                .help("Path to the cookie file of the bitcoin node"),
2✔
116
        )
2✔
117
        .arg(
2✔
118
            Arg::new("rpcuser")
2✔
119
                .long("rpcuser")
2✔
120
                .num_args(1)
2✔
121
                .help("The username for the bitcoin node"),
2✔
122
        )
2✔
123
        .arg(
2✔
124
            Arg::new("rpcpassword")
2✔
125
                .long("rpcpassword")
2✔
126
                .num_args(1)
2✔
127
                .help("The password for the bitcoin node"),
2✔
128
        )
2✔
129
        .arg(Arg::new("db_path").short('d').long("db-path").help("Sets a custom database path"))
2✔
130
        .subcommand_required(true);
2✔
131

6✔
132
    // Conditional arguments based on features
6✔
133
    #[cfg(feature = "v2")]
6✔
134
    {
6✔
135
        cmd = cmd.arg(
6✔
136
            Arg::new("ohttp_relay")
6✔
137
                .long("ohttp-relay")
6✔
138
                .help("The ohttp relay url")
6✔
139
                .value_parser(value_parser!(Url)),
6✔
140
        );
6✔
141
    }
6✔
142

143
    cmd = cmd.subcommand(
8✔
144
        Command::new("send")
8✔
145
            .arg_required_else_help(true)
8✔
146
            .arg(arg!(<BIP21> "The `bitcoin:...` payjoin uri to send to"))
8✔
147
            .arg_required_else_help(true)
8✔
148
            .arg(
8✔
149
                Arg::new("fee_rate")
8✔
150
                    .long("fee-rate")
8✔
151
                    .value_name("FEE_SAT_PER_VB")
8✔
152
                    .help("Fee rate in sat/vB")
8✔
153
                    .value_parser(parse_fee_rate_in_sat_per_vb),
8✔
154
            ),
8✔
155
    );
156

157
    let mut receive_cmd = Command::new("receive")
8✔
158
        .arg_required_else_help(true)
8✔
159
        .arg(arg!(<AMOUNT> "The amount to receive in satoshis").value_parser(parse_amount_in_sat))
8✔
160
        .arg_required_else_help(true);
8✔
161

8✔
162
    #[cfg(feature = "v2")]
8✔
163
    let mut cmd = cmd.subcommand(Command::new("resume"));
8✔
164

8✔
165
    // Conditional arguments based on features for the receive subcommand
8✔
166
    receive_cmd = receive_cmd.arg(
8✔
167
        Arg::new("max_fee_rate")
8✔
168
            .long("max-fee-rate")
8✔
169
            .num_args(1)
8✔
170
            .help("The maximum effective fee rate the receiver is willing to pay (in sat/vB)")
8✔
171
            .value_parser(parse_fee_rate_in_sat_per_vb),
8✔
172
    );
8✔
173
    #[cfg(feature = "v1")]
8✔
174
    {
8✔
175
        receive_cmd = receive_cmd.arg(
8✔
176
            Arg::new("port")
8✔
177
                .long("port")
8✔
178
                .short('p')
8✔
179
                .num_args(1)
8✔
180
                .help("The local port to listen on"),
8✔
181
        );
8✔
182
        receive_cmd = receive_cmd.arg(
8✔
183
            Arg::new("pj_endpoint")
8✔
184
                .long("pj-endpoint")
8✔
185
                .short('e')
8✔
186
                .num_args(1)
8✔
187
                .help("The `pj=` endpoint to receive the payjoin request")
8✔
188
                .value_parser(value_parser!(Url)),
8✔
189
        );
8✔
190
    }
8✔
191

8✔
192
    #[cfg(feature = "v2")]
8✔
193
    {
8✔
194
        receive_cmd = receive_cmd.arg(
8✔
195
            Arg::new("pj_directory")
8✔
196
                .long("pj-directory")
8✔
197
                .num_args(1)
8✔
198
                .help("The directory to store payjoin requests")
8✔
199
                .value_parser(value_parser!(Url)),
8✔
200
        );
8✔
201
        receive_cmd = receive_cmd
8✔
202
            .arg(Arg::new("ohttp_keys").long("ohttp-keys").help("The ohttp key config file path"));
8✔
203
    }
8✔
204

8✔
205
    cmd = cmd.subcommand(receive_cmd);
8✔
206
    cmd.get_matches()
8✔
207
}
8✔
208

209
fn parse_amount_in_sat(s: &str) -> Result<Amount, ParseAmountError> {
3✔
210
    Amount::from_str_in(s, payjoin::bitcoin::Denomination::Satoshi)
3✔
211
}
3✔
212

213
fn parse_fee_rate_in_sat_per_vb(s: &str) -> Result<FeeRate, std::num::ParseFloatError> {
4✔
214
    let fee_rate_sat_per_vb: f32 = s.parse()?;
4✔
215
    let fee_rate_sat_per_kwu = fee_rate_sat_per_vb * 250.0_f32;
4✔
216
    Ok(FeeRate::from_sat_per_kwu(fee_rate_sat_per_kwu.ceil() as u64))
4✔
217
}
4✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc