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

payjoin / rust-payjoin / 14867099581

06 May 2025 06:32PM UTC coverage: 82.358% (+0.3%) from 82.057%
14867099581

Pull #686

github

web-flow
Merge 1be656857 into eb26494ed
Pull Request #686: Catch new match arm cargo mutants

48 of 55 new or added lines in 2 files covered. (87.27%)

3 existing lines in 1 file now uncovered.

5490 of 6666 relevant lines covered (82.36%)

694.21 hits per line

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

82.47
/payjoin/src/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

9
#[derive(Debug, Clone)]
10
pub(crate) struct Params {
11
    // version
12
    pub v: usize,
13
    // disableoutputsubstitution
14
    pub output_substitution: OutputSubstitution,
15
    // maxadditionalfeecontribution, additionalfeeoutputindex
16
    pub additional_fee_contribution: Option<(bitcoin::Amount, usize)>,
17
    // minfeerate
18
    pub min_fee_rate: FeeRate,
19
    #[cfg(feature = "_multiparty")]
20
    /// Opt in to optimistic psbt merge
21
    pub optimistic_merge: bool,
22
}
23

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

37
impl Params {
38
    pub fn from_query_pairs<K, V, I>(
40✔
39
        pairs: I,
40✔
40
        supported_versions: &'static [usize],
40✔
41
    ) -> Result<Self, Error>
40✔
42
    where
40✔
43
        I: Iterator<Item = (K, V)>,
40✔
44
        K: Borrow<str> + Into<String>,
40✔
45
        V: Borrow<str> + Into<String>,
40✔
46
    {
40✔
47
        let mut params = Params::default();
40✔
48

40✔
49
        let mut additional_fee_output_index = None;
40✔
50
        let mut max_additional_fee_contribution = None;
40✔
51

52
        for (key, v) in pairs {
147✔
53
            match (key.borrow(), v.borrow()) {
108✔
54
                ("v", version) =>
108✔
55
                    params.v = match version.parse::<usize>() {
29✔
56
                        Ok(version) if supported_versions.contains(&version) => version,
29✔
57
                        _ => return Err(Error::UnknownVersion { supported_versions }),
1✔
58
                    },
59
                ("additionalfeeoutputindex", index) =>
79✔
60
                    additional_fee_output_index = match index.parse::<usize>() {
21✔
61
                        Ok(index) => Some(index),
21✔
62
                        Err(_error) => {
×
63
                            warn!("bad `additionalfeeoutputindex` query value '{index}': {_error}");
×
64
                            None
×
65
                        }
66
                    },
67
                ("maxadditionalfeecontribution", fee) =>
58✔
68
                    max_additional_fee_contribution =
22✔
69
                        match bitcoin::Amount::from_str_in(fee, bitcoin::Denomination::Satoshi) {
22✔
70
                            Ok(contribution) => Some(contribution),
22✔
71
                            Err(_error) => {
×
72
                                warn!(
×
73
                                "bad `maxadditionalfeecontribution` query value '{fee}': {_error}"
×
74
                            );
75
                                None
×
76
                            }
77
                        },
78
                ("minfeerate", fee_rate) => {
36✔
79
                    params.min_fee_rate = match fee_rate.parse::<f32>() {
15✔
80
                        Ok(fee_rate_sat_per_vb) => {
15✔
81
                            // TODO Parse with serde when rust-bitcoin supports it
15✔
82
                            let fee_rate_sat_per_kwu = fee_rate_sat_per_vb * 250.0_f32;
15✔
83
                            // since it's a minimum, we want to round up
15✔
84
                            FeeRate::from_sat_per_kwu(fee_rate_sat_per_kwu.ceil() as u64)
15✔
85
                        }
86
                        Err(_) => return Err(Error::FeeRate),
×
87
                    }
88
                }
89
                ("disableoutputsubstitution", v) =>
21✔
90
                    params.output_substitution = if v == "true" {
5✔
91
                        OutputSubstitution::Disabled
5✔
92
                    } else {
93
                        OutputSubstitution::Enabled
×
94
                    },
95
                #[cfg(feature = "_multiparty")]
96
                ("optimisticmerge", v) => params.optimistic_merge = v == "true",
16✔
97
                _ => (),
×
98
            }
99
        }
100

101
        match (max_additional_fee_contribution, additional_fee_output_index) {
39✔
102
            (Some(amount), Some(index)) =>
21✔
103
                params.additional_fee_contribution = Some((amount, index)),
21✔
104
            (Some(_), None) | (None, Some(_)) => {
105
                warn!("only one additional-fee parameter specified: {params:?}");
1✔
106
            }
107
            (None, None) => (),
17✔
108
        }
109

110
        log::debug!("parsed optional parameters: {params:?}");
39✔
111
        Ok(params)
39✔
112
    }
40✔
113
}
114

115
#[derive(Debug, PartialEq, Eq)]
116
pub(crate) enum Error {
117
    UnknownVersion { supported_versions: &'static [usize] },
118
    FeeRate,
119
}
120

121
impl fmt::Display for Error {
122
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
×
123
        match self {
×
124
            Error::UnknownVersion { .. } => write!(f, "unknown version"),
×
125
            Error::FeeRate => write!(f, "could not parse feerate"),
×
126
        }
127
    }
×
128
}
129

130
impl std::error::Error for Error {
131
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { None }
×
132
}
133

134
#[cfg(test)]
135
pub(crate) mod test {
136
    use bitcoin::{Amount, FeeRate};
137

138
    use super::*;
139

140
    #[test]
141
    fn test_parse_params() {
1✔
142
        let pairs = url::form_urlencoded::parse(b"&maxadditionalfeecontribution=182&additionalfeeoutputindex=0&minfeerate=2&disableoutputsubstitution=true&optimisticmerge=true");
1✔
143
        let params =
1✔
144
            Params::from_query_pairs(pairs, &[1]).expect("Could not parse params from query pairs");
1✔
145
        assert_eq!(params.v, 1);
1✔
146
        assert_eq!(params.output_substitution, OutputSubstitution::Disabled);
1✔
147
        assert_eq!(params.additional_fee_contribution, Some((Amount::from_sat(182), 0)));
1✔
148
        assert_eq!(
1✔
149
            params.min_fee_rate,
1✔
150
            FeeRate::from_sat_per_vb(2).expect("Could not calculate feerate")
1✔
151
        );
1✔
152
        #[cfg(feature = "_multiparty")]
153
        assert!(params.optimistic_merge)
1✔
154
    }
1✔
155

156
    #[test]
157
    fn test_version_mismatch() {
1✔
158
        let pairs = url::form_urlencoded::parse(b"&v=2");
1✔
159
        let params = Params::from_query_pairs(pairs, &[1]);
1✔
160
        match params {
1✔
NEW
161
            Ok(_) => panic!("Expected error, got success"),
×
162
            Err(err) => assert_eq!(err, Error::UnknownVersion { supported_versions: &[1] }),
1✔
163
        }
164
    }
1✔
165

166
    #[test]
167
    fn test_one_fee_param_supplied() {
1✔
168
        let pairs = url::form_urlencoded::parse(b"&maxadditionalfeecontribution=182");
1✔
169
        let params =
1✔
170
            Params::from_query_pairs(pairs, &[1]).expect("Could not parse params from query pairs");
1✔
171
        assert_eq!(params.additional_fee_contribution, None)
1✔
172
    }
1✔
173
}
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