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

payjoin / rust-payjoin / 25687166878

11 May 2026 05:46PM UTC coverage: 85.312% (+0.02%) from 85.294%
25687166878

Pull #1399

github

web-flow
Merge 4931cac18 into 67b14ded2
Pull Request #1399: Split resume session relays

48 of 64 new or added lines in 2 files covered. (75.0%)

2 existing lines in 1 file now uncovered.

11675 of 13685 relevant lines covered (85.31%)

395.33 hits per line

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

75.81
/payjoin-cli/src/app/v2/ohttp.rs
1
//! OHTTP relay selection and key bootstrapping for the payjoin-cli.
2
//!
3
//! [`RelayManager`] tracks the currently selected relay and any relays that
4
//! have failed, excluding them from future selections for the lifetime of
5
//! the [`RelayManager`].
6
//!
7
//! `fetch_ohttp_keys` selects a relay at random from the configured list,
8
//! excluding relays that [`RelayManager`] has marked as failed,
9
//! to avoid a fixed contact pattern at the network layer.
10
use std::sync::{Arc, Mutex};
11

12
use anyhow::{anyhow, Result};
13
use payjoin::Url;
14

15
use super::Config;
16

17
#[derive(Debug, Clone)]
18
pub struct RelayManager {
19
    selected_relay: Option<Url>,
20
    failed_relays: Vec<Url>,
21
}
22

23
impl RelayManager {
24
    pub fn new() -> Self { RelayManager { selected_relay: None, failed_relays: Vec::new() } }
9✔
25

26
    pub fn set_selected_relay(&mut self, relay: Url) { self.selected_relay = Some(relay); }
8✔
27

28
    pub fn get_selected_relay(&self) -> Option<Url> { self.selected_relay.clone() }
9✔
29

30
    pub fn add_failed_relay(&mut self, relay: Url) { self.failed_relays.push(relay); }
×
31

32
    pub fn get_failed_relays(&self) -> Vec<Url> { self.failed_relays.clone() }
8✔
33
}
34

35
pub(crate) struct ValidatedOhttpKeys {
36
    pub(crate) ohttp_keys: payjoin::OhttpKeys,
37
    pub(crate) relay_url: Url,
38
}
39

40
pub(crate) async fn unwrap_ohttp_keys_or_else_fetch(
8✔
41
    config: &Config,
8✔
42
    directory: Option<Url>,
8✔
43
    relay_manager: &mut RelayManager,
8✔
44
) -> Result<ValidatedOhttpKeys> {
8✔
45
    if let Some(ohttp_keys) = config.v2()?.ohttp_keys.clone() {
8✔
46
        println!("Using OHTTP Keys from config");
4✔
47
        let validated = fetch_ohttp_keys(config, directory, relay_manager).await?;
4✔
48
        Ok(ValidatedOhttpKeys { ohttp_keys, relay_url: validated.relay_url })
2✔
49
    } else {
50
        println!("Bootstrapping private network transport over Oblivious HTTP");
4✔
51
        let fetched_keys = fetch_ohttp_keys(config, directory, relay_manager).await?;
4✔
52

53
        Ok(fetched_keys)
4✔
54
    }
55
}
6✔
56

57
async fn fetch_ohttp_keys(
8✔
58
    config: &Config,
8✔
59
    directory: Option<Url>,
8✔
60
    relay_manager: &mut RelayManager,
8✔
61
) -> Result<ValidatedOhttpKeys> {
8✔
62
    use payjoin::bitcoin::secp256k1::rand::prelude::SliceRandom;
63
    let payjoin_directory = directory.unwrap_or(config.v2()?.pj_directory.clone());
8✔
64
    let relays = config.v2()?.ohttp_relays.clone();
8✔
65

66
    if relays.len() < 2 {
8✔
67
        tracing::warn!(
8✔
68
            "Only one OHTTP relay configured. Add more ohttp_relays to improve privacy."
69
        );
NEW
70
    }
×
71

72
    loop {
73
        let failed_relays = relay_manager.get_failed_relays();
8✔
74

75
        let remaining_relays: Vec<_> =
8✔
76
            relays.iter().filter(|r| !failed_relays.contains(r)).cloned().collect();
8✔
77

78
        if remaining_relays.is_empty() {
8✔
79
            return Err(anyhow!("No valid relays available"));
×
80
        }
8✔
81

82
        let selected_relay =
8✔
83
            match remaining_relays.choose(&mut payjoin::bitcoin::key::rand::thread_rng()) {
8✔
84
                Some(relay) => relay.clone(),
8✔
85
                None => return Err(anyhow!("Failed to select from remaining relays")),
×
86
            };
87

88
        relay_manager.set_selected_relay(selected_relay.clone());
8✔
89

90
        let ohttp_keys = {
6✔
91
            #[cfg(feature = "_manual-tls")]
92
            {
93
                if let Some(cert_path) = config.root_certificate.as_ref() {
8✔
94
                    let cert_der = std::fs::read(cert_path)?;
8✔
95
                    payjoin::io::fetch_ohttp_keys_with_cert(
8✔
96
                        selected_relay.as_str(),
8✔
97
                        payjoin_directory.as_str(),
8✔
98
                        &cert_der,
8✔
99
                    )
8✔
100
                    .await
8✔
101
                } else {
102
                    payjoin::io::fetch_ohttp_keys(
×
103
                        selected_relay.as_str(),
×
104
                        payjoin_directory.as_str(),
×
105
                    )
×
106
                    .await
×
107
                }
108
            }
109
            #[cfg(not(feature = "_manual-tls"))]
110
            payjoin::io::fetch_ohttp_keys(selected_relay.as_str(), payjoin_directory.as_str()).await
111
        };
112

113
        match ohttp_keys {
×
114
            Ok(keys) =>
6✔
115
                return Ok(ValidatedOhttpKeys { ohttp_keys: keys, relay_url: selected_relay }),
6✔
116
            Err(payjoin::io::Error::UnexpectedStatusCode(e)) => {
×
117
                return Err(payjoin::io::Error::UnexpectedStatusCode(e).into());
×
118
            }
119
            Err(e) => {
×
120
                tracing::debug!("Failed to connect to relay: {selected_relay}, {e:?}");
×
NEW
121
                relay_manager.add_failed_relay(selected_relay);
×
122
            }
123
        }
124
    }
125
}
6✔
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