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

payjoin / rust-payjoin / 24517552168

16 Apr 2026 03:00PM UTC coverage: 84.74% (+0.4%) from 84.38%
24517552168

Pull #1377

github

web-flow
Merge 6efecbe76 into 7b9057d1f
Pull Request #1377: Use internal Url struct in favor of url::Url to minimize the url dep in payjoin

534 of 574 new or added lines in 16 files covered. (93.03%)

3 existing lines in 2 files now uncovered.

11267 of 13296 relevant lines covered (84.74%)

402.74 hits per line

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

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

4
use bitcoin::FeeRate;
5
use percent_encoding_rfc3986::percent_decode_str;
6
use tracing::warn;
7

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

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

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

34
impl Params {
35
    /// Warn when only one parameter is present rather than failing the entire payjoin process.
36
    ///
37
    /// This allows for graceful degradation and doesn't halt the payjoin process
38
    /// due to incomplete optional parameters, while still alerting about the unusual
39
    /// configuration that prevents fee adjustment capability.
40
    fn handle_additonal_fee_param(
56✔
41
        &mut self,
56✔
42
        max_additional_fee_contribution: Option<bitcoin::Amount>,
56✔
43
        additional_fee_output_index: Option<usize>,
56✔
44
    ) {
56✔
45
        match (max_additional_fee_contribution, additional_fee_output_index) {
56✔
46
            (Some(amount), Some(index)) => {
50✔
47
                self.additional_fee_contribution = Some((amount, index));
50✔
48
            }
50✔
49
            (Some(_), None) | (None, Some(_)) => {
50
                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:?}");
×
51
            }
52
            (None, None) => (), // Neither parameter provided, normal case
6✔
53
        }
54
    }
56✔
55

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

67
        let mut additional_fee_output_index = None;
57✔
68
        let mut max_additional_fee_contribution = None;
57✔
69

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

117
        params.handle_additonal_fee_param(
56✔
118
            max_additional_fee_contribution,
56✔
119
            additional_fee_output_index,
56✔
120
        );
121

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

126
    pub fn from_query_str(
56✔
127
        query: &str,
56✔
128
        supported_versions: &'static [Version],
56✔
129
    ) -> Result<Self, Error> {
56✔
130
        let pairs = query
56✔
131
            .split('&')
56✔
132
            .filter(|s| !s.is_empty())
134✔
133
            .map(|pair| {
133✔
134
                let (k, v) = pair.split_once('=').ok_or(Error::MalformedQuery)?;
133✔
135
                let key = percent_decode_str(k)
133✔
136
                    .map_err(|_| Error::MalformedQuery)?
133✔
137
                    .decode_utf8()
133✔
138
                    .map_err(|_| Error::MalformedQuery)?;
133✔
139
                let val = percent_decode_str(v)
133✔
140
                    .map_err(|_| Error::MalformedQuery)?
133✔
141
                    .decode_utf8()
133✔
142
                    .map_err(|_| Error::MalformedQuery)?;
133✔
143
                Ok((key, val))
133✔
144
            })
133✔
145
            .collect::<Result<Vec<_>, Error>>()?;
56✔
146
        Self::from_query_pairs(pairs.into_iter(), supported_versions)
56✔
147
    }
56✔
148
}
149

150
#[derive(Debug, PartialEq, Eq)]
151
pub(crate) enum Error {
152
    UnknownVersion { supported_versions: &'static [Version] },
153
    FeeRate,
154
    MalformedQuery,
155
}
156

157
impl fmt::Display for Error {
158
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
×
159
        match self {
×
160
            Error::UnknownVersion { .. } => write!(f, "unknown version"),
×
161
            Error::FeeRate => write!(f, "could not parse feerate"),
×
NEW
162
            Error::MalformedQuery => write!(f, "malformed query parameter encoding"),
×
163
        }
164
    }
×
165
}
166

167
impl std::error::Error for Error {
168
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { None }
×
169
}
170

171
#[cfg(test)]
172
pub(crate) mod test {
173
    use bitcoin::{Amount, FeeRate};
174

175
    use super::*;
176
    use crate::receive::optional_parameters::Params;
177
    use crate::Version;
178

179
    #[test]
180
    fn test_parse_params() {
1✔
181
        let params = Params::from_query_str("&maxadditionalfeecontribution=182&additionalfeeoutputindex=0&minfeerate=2&disableoutputsubstitution=true&optimisticmerge=true", &[Version::One])
1✔
182
            .expect("Could not parse params from query str");
1✔
183
        assert_eq!(params.v, Version::One);
1✔
184
        assert_eq!(params.output_substitution, OutputSubstitution::Disabled);
1✔
185
        assert_eq!(params.additional_fee_contribution, Some((Amount::from_sat(182), 0)));
1✔
186
        assert_eq!(
1✔
187
            params.min_fee_rate,
188
            FeeRate::from_sat_per_vb(2).expect("Could not calculate feerate")
1✔
189
        );
190
    }
1✔
191

192
    #[test]
193
    fn from_query_pairs_unsupported_versions() {
1✔
194
        let invalid_pair: Vec<(&str, &str)> = vec![("v", "888")];
1✔
195
        let supported_versions = &[Version::One, Version::Two];
1✔
196
        let params = Params::from_query_pairs(invalid_pair.into_iter(), supported_versions);
1✔
197
        assert!(params.is_err());
1✔
198
        assert_eq!(params.err().unwrap(), Error::UnknownVersion { supported_versions });
1✔
199
    }
1✔
200
}
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