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

payjoin / rust-payjoin / 19678524663

25 Nov 2025 05:31PM UTC coverage: 83.578% (+0.04%) from 83.539%
19678524663

Pull #1035

github

web-flow
Merge 2198e3f50 into baa63f60e
Pull Request #1035: Payjoin-cli should cache ohttp-keys for re-use

50 of 54 new or added lines in 1 file covered. (92.59%)

7 existing lines in 1 file now uncovered.

9034 of 10809 relevant lines covered (83.58%)

458.24 hits per line

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

81.9
/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<url::Url>,
17
    failed_relays: Vec<url::Url>,
18
}
19

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

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

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

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

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

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

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

50
    println!("Bootstrapping private network transport over Oblivious HTTP");
3✔
51
    let fetched_keys = fetch_ohttp_keys(config, directory, relay_manager).await?;
3✔
52

53
    Ok(fetched_keys)
3✔
54
}
5✔
55

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

65
    loop {
66
        let failed_relays =
3✔
67
            relay_manager.lock().expect("Lock should not be poisoned").get_failed_relays();
3✔
68

69
        let remaining_relays: Vec<_> =
3✔
70
            relays.iter().filter(|r| !failed_relays.contains(r)).cloned().collect();
3✔
71

72
        if remaining_relays.is_empty() {
3✔
UNCOV
73
            return Err(anyhow!("No valid relays available"));
×
74
        }
3✔
75

76
        let selected_relay =
3✔
77
            match remaining_relays.choose(&mut payjoin::bitcoin::key::rand::thread_rng()) {
3✔
78
                Some(relay) => relay.clone(),
3✔
UNCOV
79
                None => return Err(anyhow!("Failed to select from remaining relays")),
×
80
            };
81

82
        relay_manager
3✔
83
            .lock()
3✔
84
            .expect("Lock should not be poisoned")
3✔
85
            .set_selected_relay(selected_relay.clone());
3✔
86

87
        // try cache for this selected relay first
88
        if let Some(cached) = read_cached_ohttp_keys(&selected_relay) {
3✔
89
            println!("using Cached keys  for relay: {selected_relay}");
2✔
90
            if !is_expired(&cached) && cached.relay_url == selected_relay {
2✔
91
                return Ok(ValidatedOhttpKeys {
2✔
92
                    ohttp_keys: cached.keys,
2✔
93
                    relay_url: cached.relay_url,
2✔
94
                });
2✔
NEW
UNCOV
95
            }
×
96
        }
1✔
97

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

121
        match ohttp_keys {
×
122
            Ok(keys) => {
1✔
123
                // Cache the keys if they are not already cached for this relay
124
                if read_cached_ohttp_keys(&selected_relay).is_none() {
1✔
125
                    if let Err(e) = cache_ohttp_keys(&keys, &selected_relay) {
1✔
NEW
126
                        tracing::debug!(
×
NEW
127
                            "Failed to cache OHTTP keys for relay {selected_relay}: {e:?}"
×
128
                        );
129
                    }
1✔
NEW
UNCOV
130
                }
×
131
                return Ok(ValidatedOhttpKeys { ohttp_keys: keys, relay_url: selected_relay });
1✔
132
            }
133
            Err(payjoin::io::Error::UnexpectedStatusCode(e)) => {
×
UNCOV
134
                return Err(payjoin::io::Error::UnexpectedStatusCode(e).into());
×
135
            }
136
            Err(e) => {
×
137
                tracing::debug!("Failed to connect to relay: {selected_relay}, {e:?}");
×
138
                relay_manager
×
139
                    .lock()
×
140
                    .expect("Lock should not be poisoned")
×
UNCOV
141
                    .add_failed_relay(selected_relay);
×
142
            }
143
        }
144
    }
145
}
3✔
146

147
#[derive(Serialize, Deserialize, Debug)]
148
struct CachedOhttpKeys {
149
    keys: payjoin::OhttpKeys,
150
    relay_url: url::Url,
151
    fetched_at: u64,
152
}
153

154
fn get_cache_file(relay_url: &url::Url) -> PathBuf {
5✔
155
    dirs::cache_dir()
5✔
156
        .unwrap()
5✔
157
        .join("payjoin-cli")
5✔
158
        .join(relay_url.host_str().unwrap())
5✔
159
        .join("ohttp-keys.json")
5✔
160
}
5✔
161

162
fn read_cached_ohttp_keys(relay_url: &url::Url) -> Option<CachedOhttpKeys> {
4✔
163
    let cache_file = get_cache_file(relay_url);
4✔
164
    if !cache_file.exists() {
4✔
165
        return None;
2✔
166
    }
2✔
167
    let data = fs::read_to_string(cache_file).ok().unwrap();
2✔
168
    serde_json::from_str(&data).ok()
2✔
169
}
4✔
170

171
fn cache_ohttp_keys(ohttp_keys: &payjoin::OhttpKeys, relay_url: &url::Url) -> Result<()> {
1✔
172
    let cached = CachedOhttpKeys {
1✔
173
        keys: ohttp_keys.clone(),
1✔
174
        relay_url: relay_url.clone(),
1✔
175
        fetched_at: SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs(),
1✔
176
    };
1✔
177

178
    let serialized = serde_json::to_string(&cached)?;
1✔
179
    let path = get_cache_file(relay_url);
1✔
180
    fs::create_dir_all(path.parent().unwrap())?;
1✔
181
    fs::write(path, serialized)?;
1✔
182
    Ok(())
1✔
183
}
1✔
184

185
fn is_expired(cached_keys: &CachedOhttpKeys) -> bool {
2✔
186
    let now = SystemTime::now()
2✔
187
        .duration_since(SystemTime::UNIX_EPOCH)
2✔
188
        .unwrap_or(Duration::ZERO)
2✔
189
        .as_secs();
2✔
190
    now.saturating_sub(cached_keys.fetched_at) > CACHE_DURATION.as_secs()
2✔
191
}
2✔
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