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

payjoin / rust-payjoin / 17567208650

08 Sep 2025 11:42PM UTC coverage: 85.998% (+0.05%) from 85.947%
17567208650

Pull #1035

github

web-flow
Merge 8512a2899 into ab1e1a3c6
Pull Request #1035: Payjoin-cli should cache ohttp-keys for re-use

43 of 44 new or added lines in 1 file covered. (97.73%)

4 existing lines in 1 file now uncovered.

8261 of 9606 relevant lines covered (86.0%)

486.79 hits per line

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

86.54
/payjoin-cli/src/app/v2/ohttp.rs
1
use std::fs;
2
use std::path::PathBuf;
3
use std::sync::{Arc, Mutex};
4
use std::time::{Duration, SystemTime};
5

6
use anyhow::{anyhow, Result};
7
use serde::{Deserialize, Serialize};
8

9
use super::Config;
10

11
// 6 months
12
const CACHE_DURATION: Duration = Duration::from_secs(6 * 30 * 24 * 60 * 60);
13

14
#[derive(Debug, Clone)]
15
pub struct RelayManager {
16
    selected_relay: Option<payjoin::Url>,
17
    failed_relays: Vec<payjoin::Url>,
18
}
19

20
impl RelayManager {
21
    pub fn new() -> Self { RelayManager { selected_relay: None, failed_relays: Vec::new() } }
7✔
22

23
    pub fn set_selected_relay(&mut self, relay: payjoin::Url) { self.selected_relay = Some(relay); }
1✔
24

25
    pub fn get_selected_relay(&self) -> Option<payjoin::Url> { self.selected_relay.clone() }
8✔
26

27
    pub fn add_failed_relay(&mut self, relay: payjoin::Url) { self.failed_relays.push(relay); }
×
28

29
    pub fn get_failed_relays(&self) -> Vec<payjoin::Url> { self.failed_relays.clone() }
1✔
30
}
31

32
pub(crate) struct ValidatedOhttpKeys {
33
    pub(crate) ohttp_keys: payjoin::OhttpKeys,
34
    pub(crate) relay_url: payjoin::Url,
35
}
36

37
pub(crate) async fn unwrap_ohttp_keys_or_else_fetch(
7✔
38
    config: &Config,
7✔
39
    directory: Option<payjoin::Url>,
7✔
40
    relay_manager: Arc<Mutex<RelayManager>>,
7✔
41
) -> Result<ValidatedOhttpKeys> {
7✔
42
    if let Some(ohttp_keys) = config.v2()?.ohttp_keys.clone() {
7✔
43
        println!("Using OHTTP Keys from config");
3✔
44
        return Ok(ValidatedOhttpKeys {
45
            ohttp_keys,
3✔
46
            relay_url: config.v2()?.ohttp_relays[0].clone(),
3✔
47
        });
48
    }
4✔
49

50
    if let Some(cached_keys) = read_cached_ohttp_keys() {
4✔
51
        if !is_expired(&cached_keys) {
3✔
52
            println!("Using cached OHTTP keys");
3✔
53
            return Ok(ValidatedOhttpKeys {
3✔
54
                ohttp_keys: cached_keys.keys,
3✔
55
                relay_url: cached_keys.relay_url,
3✔
56
            });
3✔
NEW
UNCOV
57
        }
×
58
    }
1✔
59
    println!("Bootstrapping private network transport over Oblivious HTTP");
1✔
60
    let fetched_keys = fetch_ohttp_keys(config, directory, relay_manager).await?;
1✔
61

62
    // save the keys to cache
63
    cache_ohttp_keys(&fetched_keys.ohttp_keys, &fetched_keys.relay_url)?;
1✔
64

65
    Ok(fetched_keys)
1✔
66
}
7✔
67

68
async fn fetch_ohttp_keys(
1✔
69
    config: &Config,
1✔
70
    directory: Option<payjoin::Url>,
1✔
71
    relay_manager: Arc<Mutex<RelayManager>>,
1✔
72
) -> Result<ValidatedOhttpKeys> {
1✔
73
    use payjoin::bitcoin::secp256k1::rand::prelude::SliceRandom;
74
    let payjoin_directory = directory.unwrap_or(config.v2()?.pj_directory.clone());
1✔
75
    let relays = config.v2()?.ohttp_relays.clone();
1✔
76

77
    loop {
78
        let failed_relays =
1✔
79
            relay_manager.lock().expect("Lock should not be poisoned").get_failed_relays();
1✔
80

81
        let remaining_relays: Vec<_> =
1✔
82
            relays.iter().filter(|r| !failed_relays.contains(r)).cloned().collect();
1✔
83

84
        if remaining_relays.is_empty() {
1✔
UNCOV
85
            return Err(anyhow!("No valid relays available"));
×
86
        }
1✔
87

88
        let selected_relay =
1✔
89
            match remaining_relays.choose(&mut payjoin::bitcoin::key::rand::thread_rng()) {
1✔
90
                Some(relay) => relay.clone(),
1✔
91
                None => return Err(anyhow!("Failed to select from remaining relays")),
×
92
            };
93

94
        relay_manager
1✔
95
            .lock()
1✔
96
            .expect("Lock should not be poisoned")
1✔
97
            .set_selected_relay(selected_relay.clone());
1✔
98

99
        let ohttp_keys = {
1✔
100
            #[cfg(feature = "_manual-tls")]
101
            {
102
                if let Some(cert_path) = config.root_certificate.as_ref() {
1✔
103
                    let cert_der = std::fs::read(cert_path)?;
1✔
104
                    payjoin::io::fetch_ohttp_keys_with_cert(
1✔
105
                        &selected_relay,
1✔
106
                        &payjoin_directory,
1✔
107
                        cert_der,
1✔
108
                    )
1✔
109
                    .await
1✔
110
                } else {
UNCOV
111
                    payjoin::io::fetch_ohttp_keys(&selected_relay, &payjoin_directory).await
×
112
                }
113
            }
114
            #[cfg(not(feature = "_manual-tls"))]
115
            payjoin::io::fetch_ohttp_keys(&selected_relay, &payjoin_directory).await
116
        };
117

UNCOV
118
        match ohttp_keys {
×
119
            Ok(keys) =>
1✔
120
                return Ok(ValidatedOhttpKeys { ohttp_keys: keys, relay_url: selected_relay }),
1✔
121
            Err(payjoin::io::Error::UnexpectedStatusCode(e)) => {
×
122
                return Err(payjoin::io::Error::UnexpectedStatusCode(e).into());
×
123
            }
124
            Err(e) => {
×
125
                tracing::debug!("Failed to connect to relay: {selected_relay}, {e:?}");
×
126
                relay_manager
×
127
                    .lock()
×
128
                    .expect("Lock should not be poisoned")
×
129
                    .add_failed_relay(selected_relay);
×
130
            }
131
        }
132
    }
133
}
1✔
134

135
#[derive(Serialize, Deserialize, Debug)]
136
struct CachedOhttpKeys {
137
    keys: payjoin::OhttpKeys,
138
    relay_url: payjoin::Url,
139
    fetched_at: u64,
140
}
141

142
fn get_cache_file() -> PathBuf {
5✔
143
    let dir = dirs::cache_dir().unwrap();
5✔
144
    dirs::cache_dir().unwrap().join("payjoin-cli").join("ohttp-keys.json")
5✔
145
}
5✔
146

147
fn read_cached_ohttp_keys() -> Option<CachedOhttpKeys> {
4✔
148
    let cache_file = get_cache_file();
4✔
149
    if !cache_file.exists() {
4✔
150
        return None;
1✔
151
    }
3✔
152
    let data = fs::read_to_string(cache_file).ok().unwrap();
3✔
153
    serde_json::from_str(&data).ok()
3✔
154
}
4✔
155

156
fn cache_ohttp_keys(ohttp_keys: &payjoin::OhttpKeys, relay_url: &payjoin::Url) -> Result<()> {
1✔
157
    let cached = CachedOhttpKeys {
1✔
158
        keys: ohttp_keys.clone(),
1✔
159
        relay_url: relay_url.clone(),
1✔
160
        fetched_at: SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs(),
1✔
161
    };
1✔
162

163
    let serialized = serde_json::to_string(&cached)?;
1✔
164
    let path = get_cache_file();
1✔
165
    fs::create_dir_all(path.parent().unwrap())?;
1✔
166
    fs::write(path, serialized)?;
1✔
167
    Ok(())
1✔
168
}
1✔
169

170
fn is_expired(cached_keys: &CachedOhttpKeys) -> bool {
3✔
171
    let now = SystemTime::now()
3✔
172
        .duration_since(SystemTime::UNIX_EPOCH)
3✔
173
        .unwrap_or(Duration::ZERO)
3✔
174
        .as_secs();
3✔
175
    now.saturating_sub(cached_keys.fetched_at) > CACHE_DURATION.as_secs()
3✔
176
}
3✔
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