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

payjoin / rust-payjoin / 14228368139

02 Apr 2025 07:54PM UTC coverage: 80.879% (-0.7%) from 81.608%
14228368139

Pull #631

github

web-flow
Merge fe2ceba4a into fde867b93
Pull Request #631: Create clear command in payjoin-cli

26 of 89 new or added lines in 5 files covered. (29.21%)

1 existing line in 1 file now uncovered.

5279 of 6527 relevant lines covered (80.88%)

708.4 hits per line

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

98.2
/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
#[cfg(not(any(feature = "v1", feature = "v2")))]
13
compile_error!("At least one of the features ['v1', 'v2'] must be enabled");
14

15
#[tokio::main]
16
async fn main() -> Result<()> {
8✔
17
    env_logger::init();
8✔
18

8✔
19
    let matches = cli();
8✔
20
    let config = Config::new(&matches)?;
8✔
21

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

8✔
60
    match matches.subcommand() {
8✔
61
        Some(("send", sub_matches)) => {
8✔
62
            let bip21 = sub_matches.get_one::<String>("BIP21").context("Missing BIP21 argument")?;
8✔
63
            let fee_rate = sub_matches
8✔
64
                .get_one::<FeeRate>("fee_rate")
4✔
65
                .context("Missing --fee-rate argument")?;
4✔
66
            app.send_payjoin(bip21, *fee_rate).await?;
8✔
67
        }
8✔
68
        Some(("receive", sub_matches)) => {
8✔
69
            let amount =
8✔
70
                sub_matches.get_one::<Amount>("AMOUNT").context("Missing AMOUNT argument")?;
8✔
71
            app.receive_payjoin(*amount).await?;
8✔
72
        }
8✔
73
        #[cfg(feature = "v2")]
8✔
74
        Some(("resume", _)) => {
8✔
75
            if matches.get_flag("bip78") {
8✔
76
                anyhow::bail!("Resume command is only available with BIP77 (v2)");
8✔
77
            }
8✔
78
            println!("resume");
1✔
79
            app.resume_payjoins().await?;
1✔
80
        }
8✔
81
        #[cfg(feature = "v2")]
8✔
82
        Some(("clear", sub_matches)) => {
8✔
83
            let bip21 =
8✔
84
                sub_matches.try_get_one::<String>("BIP21").context("Missing BIP21 argument")?;
8✔
85
            if matches.get_flag("bip78") {
8✔
86
                anyhow::bail!("Clear command is only available with BIP77 (v2)");
8✔
87
            }
8✔
NEW
88
            println!("clear");
×
NEW
89
            if bip21.is_some() {
×
90
                app.clear_payjoins(Some(bip21.unwrap())).await?;
8✔
91
            } else {
8✔
92
                app.clear_payjoins(None).await?;
8✔
93
            }
8✔
94
        }
8✔
95
        _ => unreachable!(), // If all subcommands are defined above, anything else is unreachabe!()
8✔
96
    }
8✔
97

8✔
98
    Ok(())
8✔
99
}
8✔
100

101
fn cli() -> ArgMatches {
2✔
102
    let mut cmd = Command::new("payjoin")
2✔
103
        .version(env!("CARGO_PKG_VERSION"))
2✔
104
        .about("Payjoin - bitcoin scaling, savings, and privacy by default")
2✔
105
        .arg(
2✔
106
            Arg::new("bip77")
2✔
107
                .long("bip77")
2✔
108
                .help("Use BIP77 (v2) protocol (default)")
2✔
109
                .conflicts_with("bip78")
2✔
110
                .action(clap::ArgAction::SetTrue),
2✔
111
        )
2✔
112
        .arg(
2✔
113
            Arg::new("bip78")
2✔
114
                .long("bip78")
2✔
115
                .help("Use BIP78 (v1) protocol")
2✔
116
                .conflicts_with("bip77")
2✔
117
                .action(clap::ArgAction::SetTrue),
2✔
118
        )
2✔
119
        .arg(
2✔
120
            Arg::new("rpchost")
2✔
121
                .long("rpchost")
2✔
122
                .short('r')
2✔
123
                .num_args(1)
2✔
124
                .help("The port of the bitcoin node")
2✔
125
                .value_parser(value_parser!(Url)),
2✔
126
        )
2✔
127
        .arg(
2✔
128
            Arg::new("cookie_file")
2✔
129
                .long("cookie-file")
2✔
130
                .short('c')
2✔
131
                .num_args(1)
2✔
132
                .help("Path to the cookie file of the bitcoin node"),
2✔
133
        )
2✔
134
        .arg(
2✔
135
            Arg::new("rpcuser")
2✔
136
                .long("rpcuser")
2✔
137
                .num_args(1)
2✔
138
                .help("The username for the bitcoin node"),
2✔
139
        )
2✔
140
        .arg(
2✔
141
            Arg::new("rpcpassword")
2✔
142
                .long("rpcpassword")
2✔
143
                .num_args(1)
2✔
144
                .help("The password for the bitcoin node"),
2✔
145
        )
2✔
146
        .arg(Arg::new("db_path").short('d').long("db-path").help("Sets a custom database path"))
2✔
147
        .subcommand_required(true);
2✔
148

6✔
149
    // Conditional arguments based on features
6✔
150
    #[cfg(feature = "v2")]
6✔
151
    {
6✔
152
        cmd = cmd.arg(
6✔
153
            Arg::new("ohttp_relay")
6✔
154
                .long("ohttp-relay")
6✔
155
                .help("The ohttp relay url")
6✔
156
                .value_parser(value_parser!(Url)),
6✔
157
        );
6✔
158
    }
6✔
159

160
    cmd = cmd.subcommand(
8✔
161
        Command::new("send")
8✔
162
            .arg_required_else_help(true)
8✔
163
            .arg(arg!(<BIP21> "The `bitcoin:...` payjoin uri to send to"))
8✔
164
            .arg_required_else_help(true)
8✔
165
            .arg(
8✔
166
                Arg::new("fee_rate")
8✔
167
                    .long("fee-rate")
8✔
168
                    .value_name("FEE_SAT_PER_VB")
8✔
169
                    .help("Fee rate in sat/vB")
8✔
170
                    .value_parser(parse_fee_rate_in_sat_per_vb),
8✔
171
            ),
8✔
172
    );
173

174
    let mut receive_cmd = Command::new("receive")
8✔
175
        .arg_required_else_help(true)
8✔
176
        .arg(arg!(<AMOUNT> "The amount to receive in satoshis").value_parser(parse_amount_in_sat));
8✔
177

6✔
178
    #[cfg(feature = "v2")]
6✔
179
    {
6✔
180
        cmd = cmd.subcommand(Command::new("resume"));
6✔
181
    }
6✔
182

2✔
183
    #[cfg(feature = "v2")]
2✔
184
    {
2✔
185
        cmd =
6✔
186
            cmd.subcommand(Command::new("clear").arg_required_else_help(false).arg(
6✔
187
                arg!([BIP21] "The `bitcoin:...` payjoin uri identifying the session to clear"),
6✔
188
            ));
2✔
189
    }
2✔
190

2✔
191
    // Conditional arguments based on features for the receive subcommand
2✔
192
    receive_cmd = receive_cmd.arg(
6✔
193
        Arg::new("max_fee_rate")
6✔
194
            .long("max-fee-rate")
6✔
195
            .num_args(1)
6✔
196
            .help("The maximum effective fee rate the receiver is willing to pay (in sat/vB)")
6✔
197
            .value_parser(parse_fee_rate_in_sat_per_vb),
6✔
198
    );
6✔
199
    #[cfg(feature = "v1")]
6✔
200
    {
6✔
201
        receive_cmd = receive_cmd.arg(
6✔
202
            Arg::new("port")
6✔
203
                .long("port")
6✔
204
                .short('p')
6✔
205
                .num_args(1)
6✔
206
                .help("The local port to listen on"),
6✔
207
        );
6✔
208
        receive_cmd = receive_cmd.arg(
6✔
209
            Arg::new("pj_endpoint")
6✔
210
                .long("pj-endpoint")
6✔
211
                .short('e')
6✔
212
                .num_args(1)
6✔
213
                .help("The `pj=` endpoint to receive the payjoin request")
6✔
214
                .value_parser(value_parser!(Url)),
6✔
215
        );
6✔
216
    }
6✔
217

6✔
218
    #[cfg(feature = "v2")]
6✔
219
    {
6✔
220
        receive_cmd = receive_cmd.arg(
6✔
221
            Arg::new("pj_directory")
6✔
222
                .long("pj-directory")
6✔
223
                .num_args(1)
6✔
224
                .help("The directory to store payjoin requests")
6✔
225
                .value_parser(value_parser!(Url)),
6✔
226
        );
6✔
227
        receive_cmd = receive_cmd
6✔
228
            .arg(Arg::new("ohttp_keys").long("ohttp-keys").help("The ohttp key config file path"));
6✔
229
    }
6✔
230

6✔
231
    cmd = cmd.subcommand(receive_cmd);
6✔
232
    cmd.get_matches()
6✔
233
}
6✔
234

235
fn parse_amount_in_sat(s: &str) -> Result<Amount, ParseAmountError> {
3✔
236
    Amount::from_str_in(s, payjoin::bitcoin::Denomination::Satoshi)
3✔
237
}
3✔
238

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