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

payjoin / rust-payjoin / 16088505897

05 Jul 2025 01:17PM UTC coverage: 84.886% (-0.01%) from 84.896%
16088505897

Pull #853

github

web-flow
Merge 36157141e into 08c55714b
Pull Request #853: Helper method for optional parameters with docstring

15 of 19 new or added lines in 1 file covered. (78.95%)

15 existing lines in 1 file now uncovered.

7262 of 8555 relevant lines covered (84.89%)

543.16 hits per line

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

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

4
use bitcoin::FeeRate;
5
use log::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
    #[cfg(feature = "_multiparty")]
21
    /// Opt in to optimistic psbt merge
22
    pub optimistic_merge: bool,
23
}
24

25
impl Default for Params {
26
    fn default() -> Self {
52✔
27
        Params {
52✔
28
            v: Version::One,
52✔
29
            output_substitution: OutputSubstitution::Enabled,
52✔
30
            additional_fee_contribution: None,
52✔
31
            min_fee_rate: FeeRate::BROADCAST_MIN,
52✔
32
            #[cfg(feature = "_multiparty")]
52✔
33
            optimistic_merge: false,
52✔
34
        }
52✔
35
    }
52✔
36
}
37

38
impl Params {
39
    /// when only one parameter is present rather than failing the entire payjoin process.
40
    ///
41
    /// This  allows for graceful degradation and doesn't halt the payjoin process
42
    /// due to incomplete optional parameters, while still alerting about the suboptimal
43
    /// configuration that prevents fee adjustment capability.
44
    ///
45
    /// # Arguments
46
    /// * `max_additional_fee_contribution` - Optional maximum fee contribution amount
47
    /// * `additional_fee_output_index` - Optional index of output to adjust for fees
48
    /// * `
49
    fn handle_additonal_fee_param(
47✔
50
        max_additional_fee_contribution: Option<bitcoin::Amount>,
47✔
51
        additional_fee_output_index: Option<usize>,
47✔
52
        params: &mut Params,
47✔
53
    ) {
47✔
54
        match (max_additional_fee_contribution, additional_fee_output_index) {
47✔
55
            (Some(amount), Some(index)) => {
30✔
56
                params.additional_fee_contribution = Some((amount, index));
30✔
57
            }
30✔
58
            (Some(_), None) | (None, Some(_)) => {
NEW
59
                warn!(
×
NEW
UNCOV
60
                    "Only one additional-fee parameter specified, proceeding without fee adjustment capability. \
×
NEW
UNCOV
61
                     Both maxadditionalfeecontribution and additionalfeeoutputindex must be present for receiver \
×
NEW
UNCOV
62
                     to alter sender's output: {params:?}"
×
63
                );
64
            }
65
            (None, None) => (), // Neither parameter provided, normal case
17✔
66
        }
67
    }
47✔
68
    pub fn from_query_pairs<K, V, I>(
48✔
69
        pairs: I,
48✔
70
        supported_versions: &'static [Version],
48✔
71
    ) -> Result<Self, Error>
48✔
72
    where
48✔
73
        I: Iterator<Item = (K, V)>,
48✔
74
        K: Borrow<str> + Into<String>,
48✔
75
        V: Borrow<str> + Into<String>,
48✔
76
    {
77
        let mut params = Params::default();
48✔
78

79
        let mut additional_fee_output_index = None;
48✔
80
        let mut max_additional_fee_contribution = None;
48✔
81

82
        for (key, v) in pairs {
172✔
83
            match (key.borrow(), v.borrow()) {
125✔
84
                ("v", version) =>
125✔
85
                    params.v = match version {
29✔
86
                        "1" => Version::One,
29✔
87
                        "2" => Version::Two,
19✔
88
                        _ => return Err(Error::UnknownVersion { supported_versions }),
1✔
89
                    },
90
                ("additionalfeeoutputindex", index) =>
96✔
91
                    additional_fee_output_index = match index.parse::<usize>() {
30✔
92
                        Ok(index) => Some(index),
30✔
UNCOV
93
                        Err(_error) => {
×
94
                            warn!("bad `additionalfeeoutputindex` query value '{index}': {_error}");
×
95
                            None
×
96
                        }
97
                    },
98
                ("maxadditionalfeecontribution", fee) =>
66✔
99
                    max_additional_fee_contribution =
100
                        match bitcoin::Amount::from_str_in(fee, bitcoin::Denomination::Satoshi) {
30✔
101
                            Ok(contribution) => Some(contribution),
30✔
UNCOV
102
                            Err(_error) => {
×
UNCOV
103
                                warn!(
×
UNCOV
104
                                "bad `maxadditionalfeecontribution` query value '{fee}': {_error}"
×
105
                            );
UNCOV
106
                                None
×
107
                            }
108
                        },
109
                ("minfeerate", fee_rate) =>
36✔
110
                    params.min_fee_rate = match fee_rate.parse::<f32>() {
15✔
111
                        Ok(fee_rate_sat_per_vb) => {
15✔
112
                            // TODO Parse with serde when rust-bitcoin supports it
113
                            let fee_rate_sat_per_kwu = fee_rate_sat_per_vb * 250.0_f32;
15✔
114
                            // since it's a minimum, we want to round up
115
                            FeeRate::from_sat_per_kwu(fee_rate_sat_per_kwu.ceil() as u64)
15✔
116
                        }
UNCOV
117
                        Err(_) => return Err(Error::FeeRate),
×
118
                    },
119
                ("disableoutputsubstitution", v) =>
21✔
120
                    params.output_substitution = if v == "true" {
5✔
121
                        OutputSubstitution::Disabled
5✔
122
                    } else {
123
                        OutputSubstitution::Enabled
×
124
                    },
125
                #[cfg(feature = "_multiparty")]
126
                ("optimisticmerge", v) => params.optimistic_merge = v == "true",
16✔
UNCOV
127
                _ => (),
×
128
            }
129
        }
130

131
        Self::handle_additonal_fee_param(
47✔
132
            max_additional_fee_contribution,
47✔
133
            additional_fee_output_index,
47✔
134
            &mut params,
47✔
135
        );
136

137
        log::debug!("parsed optional parameters: {params:?}");
47✔
138
        Ok(params)
47✔
139
    }
48✔
140
}
141

142
#[derive(Debug, PartialEq, Eq)]
143
pub(crate) enum Error {
144
    UnknownVersion { supported_versions: &'static [Version] },
145
    FeeRate,
146
}
147

148
impl fmt::Display for Error {
149
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
×
UNCOV
150
        match self {
×
UNCOV
151
            Error::UnknownVersion { .. } => write!(f, "unknown version"),
×
UNCOV
152
            Error::FeeRate => write!(f, "could not parse feerate"),
×
153
        }
UNCOV
154
    }
×
155
}
156

157
impl std::error::Error for Error {
UNCOV
158
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { None }
×
159
}
160

161
#[cfg(test)]
162
pub(crate) mod test {
163
    use bitcoin::{Amount, FeeRate};
164

165
    use super::*;
166
    use crate::receive::optional_parameters::Params;
167
    use crate::Version;
168

169
    #[test]
170
    fn test_parse_params() {
1✔
171
        let pairs = url::form_urlencoded::parse(b"&maxadditionalfeecontribution=182&additionalfeeoutputindex=0&minfeerate=2&disableoutputsubstitution=true&optimisticmerge=true");
1✔
172
        let params = Params::from_query_pairs(pairs, &[Version::One])
1✔
173
            .expect("Could not parse params from query pairs");
1✔
174
        assert_eq!(params.v, Version::One);
1✔
175
        assert_eq!(params.output_substitution, OutputSubstitution::Disabled);
1✔
176
        assert_eq!(params.additional_fee_contribution, Some((Amount::from_sat(182), 0)));
1✔
177
        assert_eq!(
1✔
178
            params.min_fee_rate,
179
            FeeRate::from_sat_per_vb(2).expect("Could not calculate feerate")
1✔
180
        );
181
        #[cfg(feature = "_multiparty")]
182
        assert!(params.optimistic_merge)
1✔
183
    }
1✔
184

185
    #[test]
186
    fn from_query_pairs_unsupported_versions() {
1✔
187
        let invalid_pair: Vec<(&str, &str)> = vec![("v", "888")];
1✔
188
        let supported_versions = &[Version::One, Version::Two];
1✔
189
        let params = Params::from_query_pairs(invalid_pair.into_iter(), supported_versions);
1✔
190
        assert!(params.is_err());
1✔
191
        assert_eq!(params.err().unwrap(), Error::UnknownVersion { supported_versions });
1✔
192
    }
1✔
193
}
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