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

payjoin / rust-payjoin / 24684607356

20 Apr 2026 06:55PM UTC coverage: 84.903% (+0.4%) from 84.502%
24684607356

Pull #1377

github

web-flow
Merge 716a65517 into f93247e3a
Pull Request #1377: Use internal Url struct in favor of url::Url to minimize the url dep in payjoin

467 of 487 new or added lines in 16 files covered. (95.89%)

2 existing lines in 2 files now uncovered.

11343 of 13360 relevant lines covered (84.9%)

401.03 hits per line

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

83.67
/payjoin/src/core/receive/optional_parameters.rs
1
use std::borrow::Borrow;
2
use std::fmt;
3

4
use bitcoin::FeeRate;
5
use tracing::warn;
6

7
use crate::output_substitution::OutputSubstitution;
8
use crate::Version;
9

10
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
11
pub(crate) struct Params {
12
    // version
13
    pub v: Version,
14
    // disableoutputsubstitution
15
    pub output_substitution: OutputSubstitution,
16
    // maxadditionalfeecontribution, additionalfeeoutputindex
17
    pub additional_fee_contribution: Option<(bitcoin::Amount, usize)>,
18
    // minfeerate
19
    pub min_fee_rate: FeeRate,
20
}
21

22
impl Default for Params {
23
    fn default() -> Self {
63✔
24
        Params {
63✔
25
            v: Version::One,
63✔
26
            output_substitution: OutputSubstitution::Enabled,
63✔
27
            additional_fee_contribution: None,
63✔
28
            min_fee_rate: FeeRate::BROADCAST_MIN,
63✔
29
        }
63✔
30
    }
63✔
31
}
32

33
impl Params {
34
    /// Warn when only one parameter is present rather than failing the entire payjoin process.
35
    ///
36
    /// This allows for graceful degradation and doesn't halt the payjoin process
37
    /// due to incomplete optional parameters, while still alerting about the unusual
38
    /// configuration that prevents fee adjustment capability.
39
    fn handle_additonal_fee_param(
57✔
40
        &mut self,
57✔
41
        max_additional_fee_contribution: Option<bitcoin::Amount>,
57✔
42
        additional_fee_output_index: Option<usize>,
57✔
43
    ) {
57✔
44
        match (max_additional_fee_contribution, additional_fee_output_index) {
57✔
45
            (Some(amount), Some(index)) => {
51✔
46
                self.additional_fee_contribution = Some((amount, index));
51✔
47
            }
51✔
48
            (Some(_), None) | (None, Some(_)) => {
49
                warn!("Only one additional fee parameter specified, proceeding without fee adjustment capability. Both maxadditionalfeecontribution and additionalfeeoutputindex must be present for receiver to alter sender's output: {self:?}");
×
50
            }
51
            (None, None) => (), // Neither parameter provided, normal case
6✔
52
        }
53
    }
57✔
54

55
    pub fn from_query_pairs<K, V, I>(
58✔
56
        pairs: I,
58✔
57
        supported_versions: &'static [Version],
58✔
58
    ) -> Result<Self, Error>
58✔
59
    where
58✔
60
        I: Iterator<Item = (K, V)>,
58✔
61
        K: Borrow<str> + Into<String>,
58✔
62
        V: Borrow<str> + Into<String>,
58✔
63
    {
64
        let mut params = Params::default();
58✔
65

66
        let mut additional_fee_output_index = None;
58✔
67
        let mut max_additional_fee_contribution = None;
58✔
68

69
        for (key, v) in pairs {
136✔
70
            match (key.borrow(), v.borrow()) {
136✔
71
                ("v", version) =>
136✔
72
                    params.v = match version {
20✔
73
                        "1" => Version::One,
20✔
74
                        "2" => Version::Two,
7✔
75
                        _ => return Err(Error::UnknownVersion { supported_versions }),
1✔
76
                    },
77
                ("additionalfeeoutputindex", index) =>
116✔
78
                    additional_fee_output_index = match index.parse::<usize>() {
51✔
79
                        Ok(index) => Some(index),
51✔
80
                        Err(_error) => {
×
81
                            warn!("bad `additionalfeeoutputindex` query value '{index}': {_error}");
×
82
                            None
×
83
                        }
84
                    },
85
                ("maxadditionalfeecontribution", fee) =>
65✔
86
                    max_additional_fee_contribution =
87
                        match bitcoin::Amount::from_str_in(fee, bitcoin::Denomination::Satoshi) {
51✔
88
                            Ok(contribution) => Some(contribution),
51✔
89
                            Err(_error) => {
×
90
                                warn!(
×
91
                                "bad `maxadditionalfeecontribution` query value '{fee}': {_error}"
92
                            );
93
                                None
×
94
                            }
95
                        },
96
                ("minfeerate", fee_rate) =>
14✔
97
                    params.min_fee_rate = match fee_rate.parse::<f32>() {
11✔
98
                        Ok(fee_rate_sat_per_vb) => {
11✔
99
                            // TODO Parse with serde when rust-bitcoin supports it
100
                            let fee_rate_sat_per_kwu = fee_rate_sat_per_vb * 250.0_f32;
11✔
101
                            // since it's a minimum, we want to round up
102
                            FeeRate::from_sat_per_kwu(fee_rate_sat_per_kwu.ceil() as u64)
11✔
103
                        }
104
                        Err(_) => return Err(Error::FeeRate),
×
105
                    },
106
                ("disableoutputsubstitution", v) =>
3✔
107
                    params.output_substitution = if v == "true" {
2✔
108
                        OutputSubstitution::Disabled
2✔
109
                    } else {
110
                        OutputSubstitution::Enabled
×
111
                    },
112
                _ => (),
1✔
113
            }
114
        }
115

116
        params.handle_additonal_fee_param(
57✔
117
            max_additional_fee_contribution,
57✔
118
            additional_fee_output_index,
57✔
119
        );
120

121
        tracing::trace!("parsed optional parameters: {params:?}");
57✔
122
        Ok(params)
57✔
123
    }
58✔
124

125
    pub fn from_query_str(
57✔
126
        query: &str,
57✔
127
        supported_versions: &'static [Version],
57✔
128
    ) -> Result<Self, Error> {
57✔
129
        let url = crate::Url::parse(&format!("http://localhost/?{query}"))
57✔
130
            .map_err(|_| Error::MalformedQuery)?;
57✔
131
        Self::from_query_pairs(url.query_pairs().into_iter(), supported_versions)
57✔
132
    }
57✔
133
}
134

135
#[derive(Debug, PartialEq, Eq)]
136
pub(crate) enum Error {
137
    UnknownVersion { supported_versions: &'static [Version] },
138
    FeeRate,
139
    MalformedQuery,
140
}
141

142
impl fmt::Display for Error {
143
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
×
144
        match self {
×
145
            Error::UnknownVersion { .. } => write!(f, "unknown version"),
×
146
            Error::FeeRate => write!(f, "could not parse feerate"),
×
NEW
147
            Error::MalformedQuery => write!(f, "malformed query parameter encoding"),
×
148
        }
149
    }
×
150
}
151

152
impl std::error::Error for Error {
153
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { None }
×
154
}
155

156
#[cfg(test)]
157
pub(crate) mod test {
158
    use bitcoin::{Amount, FeeRate};
159

160
    use super::*;
161
    use crate::receive::optional_parameters::Params;
162
    use crate::Version;
163

164
    #[test]
165
    fn test_parse_params() {
1✔
166
        let params = Params::from_query_str("&maxadditionalfeecontribution=182&additionalfeeoutputindex=0&minfeerate=2&disableoutputsubstitution=true&optimisticmerge=true", &[Version::One])
1✔
167
            .expect("Could not parse params from query str");
1✔
168
        assert_eq!(params.v, Version::One);
1✔
169
        assert_eq!(params.output_substitution, OutputSubstitution::Disabled);
1✔
170
        assert_eq!(params.additional_fee_contribution, Some((Amount::from_sat(182), 0)));
1✔
171
        assert_eq!(
1✔
172
            params.min_fee_rate,
173
            FeeRate::from_sat_per_vb(2).expect("Could not calculate feerate")
1✔
174
        );
175
    }
1✔
176

177
    #[test]
178
    fn from_query_pairs_unsupported_versions() {
1✔
179
        let invalid_pair: Vec<(&str, &str)> = vec![("v", "888")];
1✔
180
        let supported_versions = &[Version::One, Version::Two];
1✔
181
        let params = Params::from_query_pairs(invalid_pair.into_iter(), supported_versions);
1✔
182
        assert!(params.is_err());
1✔
183
        assert_eq!(params.err().unwrap(), Error::UnknownVersion { supported_versions });
1✔
184
    }
1✔
185
}
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