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

payjoin / rust-payjoin / 17978600409

24 Sep 2025 01:41PM UTC coverage: 84.563% (+0.04%) from 84.525%
17978600409

Pull #1035

github

web-flow
Merge 3744b038c into 6a2386dbc
Pull Request #1035: WIP: Payjoin-cli should cache ohttp-keys for re-use

51 of 55 new or added lines in 1 file covered. (92.73%)

5 existing lines in 1 file now uncovered.

8628 of 10203 relevant lines covered (84.56%)

479.63 hits per line

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

82.05
/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); }
3✔
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() }
3✔
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(
6✔
38
    config: &Config,
6✔
39
    directory: Option<payjoin::Url>,
6✔
40
    relay_manager: Arc<Mutex<RelayManager>>,
6✔
41
) -> Result<ValidatedOhttpKeys> {
6✔
42
    println!("before first some");
6✔
43
    if let Some(ohttp_keys) = config.v2()?.ohttp_keys.clone() {
6✔
44
        println!("Using OHTTP Keys from config");
3✔
45
        return Ok(ValidatedOhttpKeys {
46
            ohttp_keys,
3✔
47
            relay_url: config.v2()?.ohttp_relays[0].clone(),
3✔
48
        });
49
    }
3✔
50

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

54
    Ok(fetched_keys)
3✔
55
}
6✔
56

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

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

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

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

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

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

88
        // try cache for this selected relay first
89
        if let Some(cached) = read_cached_ohttp_keys(&selected_relay) {
3✔
90
            println!("using Cached keys  for relay: {}", selected_relay);
2✔
91
            if !is_expired(&cached) && cached.relay_url == selected_relay {
2✔
92
                return Ok(ValidatedOhttpKeys {
2✔
93
                    ohttp_keys: cached.keys,
2✔
94
                    relay_url: cached.relay_url,
2✔
95
                });
2✔
NEW
96
            }
×
97
        }
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.as_str(),
1✔
106
                        payjoin_directory.as_str(),
1✔
107
                        cert_der,
1✔
108
                    )
1✔
109
                    .await
1✔
110
                } else {
111
                    payjoin::io::fetch_ohttp_keys(
×
112
                        selected_relay.as_str(),
×
113
                        payjoin_directory.as_str(),
×
114
                    )
×
UNCOV
115
                    .await
×
116
                }
117
            }
118
            #[cfg(not(feature = "_manual-tls"))]
119
            payjoin::io::fetch_ohttp_keys(selected_relay.as_str(), payjoin_directory.as_str()).await
120
        };
121

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

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

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

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

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

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

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