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

payjoin / rust-payjoin / 15910517105

26 Jun 2025 07:14PM UTC coverage: 85.445% (-0.7%) from 86.155%
15910517105

Pull #769

github

web-flow
Merge 7b1caeff1 into 2f3f46ab1
Pull Request #769: Update receiver typestate documentation for functions shared between v1 and v2 (and some InputPair documentation)

0 of 6 new or added lines in 2 files covered. (0.0%)

36 existing lines in 17 files now uncovered.

7262 of 8499 relevant lines covered (85.45%)

546.68 hits per line

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

81.58
/payjoin-cli/src/app/config.rs
1
use std::path::PathBuf;
2

3
use anyhow::Result;
4
use config::builder::DefaultState;
5
use config::{ConfigError, File, FileFormat};
6
use payjoin::bitcoin::FeeRate;
7
use payjoin::core::version::Version;
8
use serde::Deserialize;
9
use url::Url;
10

11
use crate::cli::{Cli, Commands};
12
use crate::db;
13

14
type Builder = config::builder::ConfigBuilder<DefaultState>;
15

16
#[derive(Debug, Clone, Deserialize)]
17
pub struct BitcoindConfig {
18
    pub rpchost: Url,
19
    pub cookie: Option<PathBuf>,
20
    pub rpcuser: String,
21
    pub rpcpassword: String,
22
}
23

24
#[cfg(feature = "v1")]
25
#[derive(Debug, Clone, Deserialize)]
26
pub struct V1Config {
27
    pub port: u16,
28
    pub pj_endpoint: Url,
29
}
30

31
#[cfg(feature = "v2")]
32
#[derive(Debug, Clone, Deserialize)]
33
pub struct V2Config {
34
    #[serde(deserialize_with = "deserialize_ohttp_keys_from_path")]
35
    pub ohttp_keys: Option<payjoin::OhttpKeys>,
36
    pub ohttp_relays: Vec<Url>,
37
    pub pj_directory: Url,
38
}
39

40
#[allow(clippy::large_enum_variant)]
41
#[derive(Debug, Clone, Deserialize)]
42
#[serde(tag = "version")]
43
pub enum VersionConfig {
44
    #[cfg(feature = "v1")]
45
    #[serde(rename = "v1")]
46
    V1(V1Config),
47
    #[cfg(feature = "v2")]
48
    #[serde(rename = "v2")]
49
    V2(V2Config),
50
}
51

52
#[derive(Debug, Clone, Deserialize)]
53
pub struct Config {
54
    pub db_path: PathBuf,
55
    pub max_fee_rate: Option<FeeRate>,
56
    pub bitcoind: BitcoindConfig,
57
    #[serde(skip)]
58
    pub version: Option<VersionConfig>,
59
}
60

61
impl Config {
62
    /// Check for multiple version flags and return the highest precedence version
63
    fn determine_version(cli: &Cli) -> Result<Version, ConfigError> {
10✔
64
        let mut selected_version = None;
10✔
65

66
        // Check for BIP77 (v2)
67
        if cli.flags.bip77.unwrap_or(false) {
10✔
68
            selected_version = Some(Version::Two);
×
69
        }
10✔
70

71
        // Check for BIP78 (v1)
72
        if cli.flags.bip78.unwrap_or(false) {
10✔
73
            if selected_version.is_some() {
4✔
74
                return Err(ConfigError::Message(
×
75
                    "Multiple version flags specified. Please use only one of: --bip77, --bip78"
×
76
                        .to_string(),
×
77
                ));
×
78
            }
4✔
79
            selected_version = Some(Version::One);
4✔
80
        }
6✔
81

82
        if let Some(version) = selected_version {
10✔
83
            return Ok(version);
4✔
84
        };
6✔
85

86
        // If no version explicitly selected, use default based on available features
87
        #[cfg(feature = "v2")]
88
        return Ok(Version::Two);
6✔
89
        #[cfg(all(feature = "v1", not(feature = "v2")))]
90
        return Ok(Version::One);
×
91
        #[cfg(not(any(feature = "v1", feature = "v2")))]
92
        return Err(ConfigError::Message(
93
            "No valid version available - must compile with v1 or v2 feature".to_string(),
94
        ));
95
    }
10✔
96

97
    pub(crate) fn new(cli: &Cli) -> Result<Self, ConfigError> {
10✔
98
        let mut config = config::Config::builder();
10✔
99
        config = add_bitcoind_defaults(config, cli)?;
10✔
100
        config = add_common_defaults(config, cli)?;
10✔
101

102
        let version = Self::determine_version(cli)?;
10✔
103

104
        match version {
10✔
105
            Version::One => {
106
                #[cfg(feature = "v1")]
107
                {
108
                    config = add_v1_defaults(config, cli)?;
4✔
109
                }
110
                #[cfg(not(feature = "v1"))]
111
                return Err(ConfigError::Message(
112
                    "BIP78 (v1) selected but v1 feature not enabled".to_string(),
113
                ));
114
            }
115
            Version::Two => {
116
                #[cfg(feature = "v2")]
117
                {
118
                    config = add_v2_defaults(config, cli)?;
6✔
119
                }
120
                #[cfg(not(feature = "v2"))]
121
                return Err(ConfigError::Message(
×
122
                    "BIP77 (v2) selected but v2 feature not enabled".to_string(),
×
123
                ));
×
124
            }
125
        }
126

127
        config = handle_subcommands(config, cli)?;
10✔
128
        config = config.add_source(File::new("config.toml", FileFormat::Toml).required(false));
10✔
129

130
        let built_config = config.build()?;
10✔
131

132
        let mut config = Config {
10✔
133
            db_path: built_config.get("db_path")?,
10✔
134
            max_fee_rate: built_config.get("max_fee_rate").ok(),
10✔
135
            bitcoind: built_config.get("bitcoind")?,
10✔
136
            version: None,
10✔
137
        };
138

139
        match version {
10✔
140
            Version::One => {
141
                #[cfg(feature = "v1")]
142
                {
143
                    match built_config.get::<V1Config>("v1") {
4✔
144
                        Ok(v1) => config.version = Some(VersionConfig::V1(v1)),
4✔
145
                        Err(e) =>
×
146
                            return Err(ConfigError::Message(format!(
×
147
                                "Valid V1 configuration is required for BIP78 mode: {e}"
×
148
                            ))),
×
149
                    }
150
                }
151
                #[cfg(not(feature = "v1"))]
152
                return Err(ConfigError::Message(
153
                    "BIP78 (v1) selected but v1 feature not enabled".to_string(),
154
                ));
155
            }
156
            Version::Two => {
157
                #[cfg(feature = "v2")]
158
                {
159
                    match built_config.get::<V2Config>("v2") {
6✔
160
                        Ok(v2) => config.version = Some(VersionConfig::V2(v2)),
6✔
161
                        Err(e) =>
×
162
                            return Err(ConfigError::Message(format!(
×
163
                                "Valid V2 configuration is required for BIP77 mode: {e}"
×
164
                            ))),
×
165
                    }
166
                }
167
                #[cfg(not(feature = "v2"))]
168
                return Err(ConfigError::Message(
×
169
                    "BIP77 (v2) selected but v2 feature not enabled".to_string(),
×
170
                ));
×
171
            }
172
        }
173

174
        if config.version.is_none() {
10✔
175
            return Err(ConfigError::Message(
×
176
                "No valid version configuration found for the specified mode".to_string(),
×
177
            ));
×
178
        }
10✔
179

180
        log::debug!("App config: {config:?}");
10✔
181
        Ok(config)
10✔
182
    }
10✔
183

184
    #[cfg(feature = "v1")]
185
    pub fn v1(&self) -> Result<&V1Config, anyhow::Error> {
6✔
186
        match &self.version {
6✔
187
            Some(VersionConfig::V1(v1_config)) => Ok(v1_config),
6✔
188
            #[allow(unreachable_patterns)]
189
            _ => Err(anyhow::anyhow!("V1 configuration is required for BIP78 mode")),
×
190
        }
191
    }
6✔
192

193
    #[cfg(feature = "v2")]
194
    pub fn v2(&self) -> Result<&V2Config, anyhow::Error> {
16✔
195
        match &self.version {
16✔
196
            Some(VersionConfig::V2(v2_config)) => Ok(v2_config),
16✔
197
            #[allow(unreachable_patterns)]
198
            _ => Err(anyhow::anyhow!("V2 configuration is required for v2 mode")),
×
199
        }
200
    }
16✔
201
}
202

203
/// Set up default values and CLI overrides for Bitcoin RPC connection settings
204
fn add_bitcoind_defaults(config: Builder, cli: &Cli) -> Result<Builder, ConfigError> {
10✔
205
    // Set default values
206
    let config = config
10✔
207
        .set_default("bitcoind.rpchost", "http://localhost:18443")?
10✔
208
        .set_default("bitcoind.cookie", None::<String>)?
10✔
209
        .set_default("bitcoind.rpcuser", "bitcoin")?
10✔
210
        .set_default("bitcoind.rpcpassword", "")?;
10✔
211

212
    // Override config values with command line arguments if applicable
213
    let rpchost = cli.rpchost.as_ref().map(|s| s.as_str());
10✔
214
    let cookie_file = cli.cookie_file.as_ref().map(|p| p.to_string_lossy().into_owned());
10✔
215
    let rpcuser = cli.rpcuser.as_deref();
10✔
216
    let rpcpassword = cli.rpcpassword.as_deref();
10✔
217

218
    config
10✔
219
        .set_override_option("bitcoind.rpchost", rpchost)?
10✔
220
        .set_override_option("bitcoind.cookie", cookie_file)?
10✔
221
        .set_override_option("bitcoind.rpcuser", rpcuser)?
10✔
222
        .set_override_option("bitcoind.rpcpassword", rpcpassword)
10✔
223
}
10✔
224

225
fn add_common_defaults(config: Builder, cli: &Cli) -> Result<Builder, ConfigError> {
10✔
226
    let db_path = cli.db_path.as_ref().map(|p| p.to_string_lossy().into_owned());
10✔
227
    config.set_default("db_path", db::DB_PATH)?.set_override_option("db_path", db_path)
10✔
228
}
10✔
229

230
#[cfg(feature = "v1")]
231
fn add_v1_defaults(config: Builder, cli: &Cli) -> Result<Builder, ConfigError> {
4✔
232
    // Set default values
233
    let config = config
4✔
234
        .set_default("v1.port", 3000_u16)?
4✔
235
        .set_default("v1.pj_endpoint", "https://localhost:3000")?;
4✔
236

237
    // Override config values with command line arguments if applicable
238
    let pj_endpoint = cli.pj_endpoint.as_ref().map(|s| s.as_str());
4✔
239

240
    config
4✔
241
        .set_override_option("v1.port", cli.port)?
4✔
242
        .set_override_option("v1.pj_endpoint", pj_endpoint)
4✔
243
}
4✔
244

245
/// Set up default values and CLI overrides for v2-specific settings
246
#[cfg(feature = "v2")]
247
fn add_v2_defaults(config: Builder, cli: &Cli) -> Result<Builder, ConfigError> {
6✔
248
    // Set default values
249
    let config = config
6✔
250
        .set_default("v2.pj_directory", "https://payjo.in")?
6✔
251
        .set_default("v2.ohttp_keys", None::<String>)?;
6✔
252

253
    // Override config values with command line arguments if applicable
254
    let pj_directory = cli.pj_directory.as_ref().map(|s| s.as_str());
6✔
255
    let ohttp_keys = cli.ohttp_keys.as_ref().map(|p| p.to_string_lossy().into_owned());
6✔
256
    let ohttp_relays = cli
6✔
257
        .ohttp_relays
6✔
258
        .as_ref()
6✔
259
        .map(|urls| urls.iter().map(|url| url.as_str()).collect::<Vec<_>>());
6✔
260

261
    config
6✔
262
        .set_override_option("v2.pj_directory", pj_directory)?
6✔
263
        .set_override_option("v2.ohttp_keys", ohttp_keys)?
6✔
264
        .set_override_option("v2.ohttp_relays", ohttp_relays)
6✔
265
}
6✔
266

267
/// Handles configuration overrides based on CLI subcommands
268
fn handle_subcommands(config: Builder, cli: &Cli) -> Result<Builder, ConfigError> {
10✔
269
    match &cli.command {
10✔
270
        Commands::Send { .. } => Ok(config),
4✔
271
        Commands::Receive {
272
            #[cfg(feature = "v1")]
273
            port,
3✔
274
            #[cfg(feature = "v1")]
275
            pj_endpoint,
3✔
276
            #[cfg(feature = "v2")]
277
            pj_directory,
2✔
278
            #[cfg(feature = "v2")]
279
            ohttp_keys,
2✔
280
            ..
281
        } => {
282
            #[cfg(feature = "v1")]
283
            let config = config
3✔
284
                .set_override_option("v1.port", port.map(|p| p.to_string()))?
3✔
285
                .set_override_option("v1.pj_endpoint", pj_endpoint.as_ref().map(|s| s.as_str()))?;
3✔
286
            #[cfg(feature = "v2")]
287
            let config = config
2✔
288
                .set_override_option("v2.pj_directory", pj_directory.as_ref().map(|s| s.as_str()))?
2✔
289
                .set_override_option(
2✔
290
                    "v2.ohttp_keys",
291
                    ohttp_keys.as_ref().map(|s| s.to_string_lossy().into_owned()),
2✔
UNCOV
292
                )?;
×
293
            Ok(config)
3✔
294
        }
295
        #[cfg(feature = "v2")]
296
        Commands::Resume => Ok(config),
3✔
297
    }
298
}
10✔
299

300
#[cfg(feature = "v2")]
301
fn deserialize_ohttp_keys_from_path<'de, D>(
6✔
302
    deserializer: D,
6✔
303
) -> Result<Option<payjoin::OhttpKeys>, D::Error>
6✔
304
where
6✔
305
    D: serde::Deserializer<'de>,
6✔
306
{
307
    let path_str: Option<String> = Option::deserialize(deserializer)?;
6✔
308

309
    match path_str {
6✔
310
        None => Ok(None),
5✔
311
        Some(path) => std::fs::read(path)
1✔
312
            .map_err(|e| serde::de::Error::custom(format!("Failed to read ohttp_keys file: {e}")))
1✔
313
            .and_then(|bytes| {
1✔
314
                payjoin::OhttpKeys::decode(&bytes).map_err(|e| {
1✔
315
                    serde::de::Error::custom(format!("Failed to decode ohttp keys: {e}"))
×
UNCOV
316
                })
×
317
            })
1✔
318
            .map(Some),
1✔
319
    }
320
}
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