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

payjoin / rust-payjoin / 25506488115

07 May 2026 03:47PM UTC coverage: 84.4% (-0.8%) from 85.169%
25506488115

Pull #1514

github

web-flow
Merge 5e03dea7d into 676ede97e
Pull Request #1514: [WIP] Add AS-aware relay selection

869 of 1163 new or added lines in 5 files covered. (74.72%)

7 existing lines in 2 files now uncovered.

12281 of 14551 relevant lines covered (84.4%)

375.5 hits per line

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

22.34
/payjoin-cli/src/app/v2/ohttp.rs
1
//! OHTTP relay selection and key bootstrapping for the payjoin-cli.
2
//!
3
//! [`RelaySession`] owns a relay plan plus the current failover cursor for one
4
//! bootstrap, send, or receive flow. Failures advance to the next relay only
5
//! within that flow.
6
use std::time::Duration;
7

8
use anyhow::{anyhow, Result};
9
use reqwest::header::ACCEPT;
10
use reqwest::Proxy;
11

12
use super::relay_selection::{PinnedUrl, RelayPlan};
13
use super::Config;
14

15
#[derive(Debug)]
16
pub(crate) enum RelayAttemptError {
17
    Retryable(anyhow::Error),
18
    Terminal(anyhow::Error),
19
}
20

NEW
21
pub(crate) fn classify_reqwest_error(
×
NEW
22
    err: reqwest::Error,
×
NEW
23
    context: &'static str,
×
NEW
24
) -> RelayAttemptError {
×
NEW
25
    let error = anyhow!("{context}: {err}");
×
NEW
26
    if err.is_timeout() || err.is_connect() || err.is_request() {
×
NEW
27
        RelayAttemptError::Retryable(error)
×
28
    } else {
NEW
29
        RelayAttemptError::Terminal(error)
×
30
    }
NEW
31
}
×
32

33
#[derive(Debug, Clone)]
34
pub(crate) struct RelaySession {
35
    plan: RelayPlan,
36
    current_index: usize,
37
}
38

39
impl RelaySession {
40
    pub(crate) fn new(plan: RelayPlan) -> Self { Self { plan, current_index: 0 } }
9✔
41

42
    pub(crate) fn directory(&self) -> &PinnedUrl { &self.plan.directory }
2✔
43

44
    pub(crate) fn current_relay(&self) -> Result<PinnedUrl> {
11✔
45
        self.plan.relays.get(self.current_index).cloned().ok_or_else(|| {
11✔
NEW
46
            anyhow!("No valid relays available for {}", self.plan.directory.url.as_str())
×
NEW
47
        })
×
48
    }
11✔
49

NEW
50
    pub(crate) fn record_failure(&mut self) { self.current_index += 1; }
×
51
}
52

53
pub(crate) struct ValidatedOhttpKeys {
54
    pub(crate) ohttp_keys: payjoin::OhttpKeys,
55
}
56

57
pub(crate) async fn unwrap_ohttp_keys_or_else_fetch(
2✔
58
    config: &Config,
2✔
59
    relay_session: &mut RelaySession,
2✔
60
) -> Result<ValidatedOhttpKeys> {
2✔
61
    if let Some(ohttp_keys) = config.v2()?.ohttp_keys.clone() {
2✔
62
        println!("Using OHTTP Keys from config");
2✔
63
        Ok(ValidatedOhttpKeys { ohttp_keys })
2✔
64
    } else {
UNCOV
65
        println!("Bootstrapping private network transport over Oblivious HTTP");
×
NEW
66
        fetch_ohttp_keys(config, relay_session).await
×
67
    }
68
}
2✔
69

70
pub(crate) fn http_client_builder(config: &Config) -> Result<reqwest::ClientBuilder> {
11✔
71
    #[cfg(feature = "_manual-tls")]
72
    {
73
        let mut builder = reqwest::ClientBuilder::new().use_rustls_tls().http1_only();
11✔
74
        if let Some(root_cert_path) = config.root_certificate.as_ref() {
11✔
75
            let cert_der = std::fs::read(root_cert_path)?;
11✔
76
            builder = builder
11✔
77
                .add_root_certificate(reqwest::tls::Certificate::from_der(cert_der.as_slice())?);
11✔
NEW
78
        }
×
79
        Ok(builder)
11✔
80
    }
81

82
    #[cfg(not(feature = "_manual-tls"))]
83
    {
84
        let _ = config;
85
        Ok(reqwest::Client::builder().http1_only())
86
    }
87
}
11✔
88

UNCOV
89
async fn fetch_ohttp_keys(
×
UNCOV
90
    config: &Config,
×
NEW
91
    relay_session: &mut RelaySession,
×
UNCOV
92
) -> Result<ValidatedOhttpKeys> {
×
93
    loop {
NEW
94
        let selected_relay = relay_session.current_relay()?;
×
95

NEW
96
        match fetch_ohttp_keys_with_pinned_targets(
×
NEW
97
            config,
×
NEW
98
            &selected_relay,
×
NEW
99
            relay_session.directory(),
×
100
        )
NEW
101
        .await
×
102
        {
NEW
103
            Ok(keys) => return Ok(ValidatedOhttpKeys { ohttp_keys: keys }),
×
NEW
104
            Err(RelayAttemptError::Retryable(error)) => {
×
NEW
105
                tracing::debug!(
×
106
                    "Failed to fetch OHTTP keys via relay {}: {error:?}",
107
                    selected_relay.url
108
                );
NEW
109
                relay_session.record_failure();
×
110
            }
NEW
111
            Err(RelayAttemptError::Terminal(error)) => return Err(error),
×
112
        }
113
    }
NEW
114
}
×
115

NEW
116
async fn fetch_ohttp_keys_with_pinned_targets(
×
NEW
117
    config: &Config,
×
NEW
118
    relay: &PinnedUrl,
×
NEW
119
    directory: &PinnedUrl,
×
NEW
120
) -> std::result::Result<payjoin::OhttpKeys, RelayAttemptError> {
×
NEW
121
    let proxy = Proxy::all(relay.url.as_str()).map_err(|err| {
×
NEW
122
        RelayAttemptError::Terminal(anyhow!("Failed to configure OHTTP relay proxy: {err}"))
×
NEW
123
    })?;
×
NEW
124
    let mut builder = http_client_builder(config)
×
NEW
125
        .map_err(|err| RelayAttemptError::Terminal(anyhow!("Failed to build HTTP client: {err}")))?
×
NEW
126
        .proxy(proxy);
×
127

NEW
128
    if let Some(domain) = relay.domain() {
×
NEW
129
        builder = builder.resolve_to_addrs(domain, &relay.socket_addrs);
×
NEW
130
    }
×
131

NEW
132
    if let Some(directory_domain) = directory.domain() {
×
NEW
133
        builder = builder.resolve_to_addrs(directory_domain, &directory.socket_addrs);
×
NEW
134
    }
×
135

NEW
136
    let client = builder.build().map_err(|err| {
×
NEW
137
        RelayAttemptError::Terminal(anyhow!("Failed to build HTTP client: {err}"))
×
NEW
138
    })?;
×
NEW
139
    let ohttp_keys_url = directory.url.join("/.well-known/ohttp-gateway").map_err(|err| {
×
NEW
140
        RelayAttemptError::Terminal(anyhow!("Failed to construct OHTTP key URL: {err}"))
×
NEW
141
    })?;
×
NEW
142
    let response = client
×
NEW
143
        .get(ohttp_keys_url.as_str())
×
NEW
144
        .timeout(Duration::from_secs(10))
×
NEW
145
        .header(ACCEPT, "application/ohttp-keys")
×
NEW
146
        .send()
×
NEW
147
        .await
×
NEW
148
        .map_err(|err| classify_reqwest_error(err, "Failed to fetch OHTTP keys"))?;
×
149

NEW
150
    if !response.status().is_success() {
×
NEW
151
        return Err(RelayAttemptError::Terminal(anyhow!(
×
NEW
152
            "Unexpected OHTTP key status code {}",
×
NEW
153
            response.status()
×
NEW
154
        )));
×
UNCOV
155
    }
×
156

NEW
157
    let body = response.bytes().await.map_err(|err| {
×
NEW
158
        RelayAttemptError::Terminal(anyhow!("Failed to read OHTTP key response body: {err}"))
×
NEW
159
    })?;
×
NEW
160
    payjoin::OhttpKeys::decode(&body)
×
NEW
161
        .map_err(|err| RelayAttemptError::Terminal(anyhow!("Failed to decode OHTTP keys: {err}")))
×
UNCOV
162
}
×
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