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

payjoin / rust-payjoin / 13350991407

16 Feb 2025 02:43AM UTC coverage: 79.468% (+0.2%) from 79.269%
13350991407

Pull #538

github

web-flow
Merge 0e46f80b5 into 2627ef20f
Pull Request #538: Make payjoin-cli v1 / v2 features additive

363 of 422 new or added lines in 6 files covered. (86.02%)

2 existing lines in 1 file now uncovered.

4122 of 5187 relevant lines covered (79.47%)

887.41 hits per line

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

98.92
/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
    let app: Box<dyn AppTrait> = if matches.get_flag("bip78") {
8✔
20
        #[cfg(feature = "v1")]
8✔
21
        {
8✔
22
            Box::new(crate::app::v1::App::new(config)?)
8✔
23
        }
8✔
24
        #[cfg(not(feature = "v1"))]
8✔
25
        {
8✔
26
            anyhow::bail!(
8✔
27
                "BIP78 (v1) support is not enabled in this build. Recompile with --features v1"
8✔
28
            )
8✔
29
        }
8✔
30
    } else {
8✔
31
        #[cfg(feature = "v2")]
8✔
32
        {
8✔
33
            Box::new(crate::app::v2::App::new(config)?)
8✔
34
        }
8✔
35
        #[cfg(not(feature = "v2"))]
8✔
36
        {
8✔
37
            anyhow::bail!(
8✔
NEW
38
                "BIP77 (v2) support is not enabled in this build. Recompile with --features v2"
×
NEW
39
            )
×
40
        }
8✔
41
    };
8✔
42

8✔
43
    match matches.subcommand() {
8✔
44
        Some(("send", sub_matches)) => {
8✔
45
            let bip21 = sub_matches.get_one::<String>("BIP21").context("Missing BIP21 argument")?;
8✔
46
            let fee_rate = sub_matches
8✔
47
                .get_one::<FeeRate>("fee_rate")
4✔
48
                .context("Missing --fee-rate argument")?;
4✔
49
            app.send_payjoin(bip21, *fee_rate).await?;
8✔
50
        }
8✔
51
        Some(("receive", sub_matches)) => {
8✔
52
            let amount =
8✔
53
                sub_matches.get_one::<Amount>("AMOUNT").context("Missing AMOUNT argument")?;
8✔
54
            app.receive_payjoin(*amount).await?;
8✔
55
        }
8✔
56
        #[cfg(feature = "v2")]
8✔
57
        Some(("resume", _)) => {
8✔
58
            if matches.get_flag("bip78") {
8✔
59
                anyhow::bail!("Resume command is only available with BIP77 (v2)");
8✔
60
            }
8✔
61
            println!("resume");
1✔
62
            app.resume_payjoins().await?;
1✔
63
        }
8✔
64
        _ => unreachable!(), // If all subcommands are defined above, anything else is unreachabe!()
8✔
65
    }
8✔
66

8✔
67
    Ok(())
8✔
68
}
8✔
69

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

6✔
118
    // Conditional arguments based on features
6✔
119
    #[cfg(feature = "v2")]
6✔
120
    {
6✔
121
        cmd = cmd.arg(
6✔
122
            Arg::new("ohttp_relay")
6✔
123
                .long("ohttp-relay")
6✔
124
                .help("The ohttp relay url")
6✔
125
                .value_parser(value_parser!(Url)),
6✔
126
        );
6✔
127
    }
6✔
128

129
    cmd = cmd.subcommand(
8✔
130
        Command::new("send")
8✔
131
            .arg_required_else_help(true)
8✔
132
            .arg(arg!(<BIP21> "The `bitcoin:...` payjoin uri to send to"))
8✔
133
            .arg_required_else_help(true)
8✔
134
            .arg(
8✔
135
                Arg::new("fee_rate")
8✔
136
                    .long("fee-rate")
8✔
137
                    .value_name("FEE_SAT_PER_VB")
8✔
138
                    .help("Fee rate in sat/vB")
8✔
139
                    .value_parser(parse_fee_rate_in_sat_per_vb),
8✔
140
            ),
8✔
141
    );
142

143
    let mut receive_cmd = Command::new("receive")
8✔
144
        .arg_required_else_help(true)
8✔
145
        .arg(arg!(<AMOUNT> "The amount to receive in satoshis").value_parser(parse_amount_in_sat))
8✔
146
        .arg_required_else_help(true);
8✔
147

8✔
148
    #[cfg(feature = "v2")]
8✔
149
    let mut cmd = cmd.subcommand(Command::new("resume"));
8✔
150

8✔
151
    // Conditional arguments based on features for the receive subcommand
8✔
152
    receive_cmd = receive_cmd.arg(
8✔
153
        Arg::new("max_fee_rate")
8✔
154
            .long("max-fee-rate")
8✔
155
            .num_args(1)
8✔
156
            .help("The maximum effective fee rate the receiver is willing to pay (in sat/vB)")
8✔
157
            .value_parser(parse_fee_rate_in_sat_per_vb),
8✔
158
    );
8✔
159
    #[cfg(feature = "v1")]
8✔
160
    {
8✔
161
        receive_cmd = receive_cmd.arg(
8✔
162
            Arg::new("port")
8✔
163
                .long("port")
8✔
164
                .short('p')
8✔
165
                .num_args(1)
8✔
166
                .help("The local port to listen on"),
8✔
167
        );
8✔
168
        receive_cmd = receive_cmd.arg(
8✔
169
            Arg::new("pj_endpoint")
8✔
170
                .long("pj-endpoint")
8✔
171
                .short('e')
8✔
172
                .num_args(1)
8✔
173
                .help("The `pj=` endpoint to receive the payjoin request")
8✔
174
                .value_parser(value_parser!(Url)),
8✔
175
        );
8✔
176
    }
8✔
177

8✔
178
    #[cfg(feature = "v2")]
8✔
179
    {
8✔
180
        receive_cmd = receive_cmd.arg(
8✔
181
            Arg::new("pj_directory")
8✔
182
                .long("pj-directory")
8✔
183
                .num_args(1)
8✔
184
                .help("The directory to store payjoin requests")
8✔
185
                .value_parser(value_parser!(Url)),
8✔
186
        );
8✔
187
        receive_cmd = receive_cmd
8✔
188
            .arg(Arg::new("ohttp_keys").long("ohttp-keys").help("The ohttp key config file path"));
8✔
189
    }
8✔
190

8✔
191
    cmd = cmd.subcommand(receive_cmd);
8✔
192
    cmd.get_matches()
8✔
193
}
8✔
194

195
fn parse_amount_in_sat(s: &str) -> Result<Amount, ParseAmountError> {
3✔
196
    Amount::from_str_in(s, payjoin::bitcoin::Denomination::Satoshi)
3✔
197
}
3✔
198

199
fn parse_fee_rate_in_sat_per_vb(s: &str) -> Result<FeeRate, std::num::ParseFloatError> {
4✔
200
    let fee_rate_sat_per_vb: f32 = s.parse()?;
4✔
201
    let fee_rate_sat_per_kwu = fee_rate_sat_per_vb * 250.0_f32;
4✔
202
    Ok(FeeRate::from_sat_per_kwu(fee_rate_sat_per_kwu.ceil() as u64))
4✔
203
}
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