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

payjoin / rust-payjoin / 27080196308

07 Jun 2026 02:19AM UTC coverage: 84.823% (-0.5%) from 85.37%
27080196308

Pull #1615

github

web-flow
Merge 249f5241d into 39c066345
Pull Request #1615: feat: port payjoin core to no_std

353 of 457 new or added lines in 22 files covered. (77.24%)

56 existing lines in 6 files now uncovered.

12486 of 14720 relevant lines covered (84.82%)

371.16 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
#[cfg(feature = "std")]
2
use alloc::format;
3
use alloc::string::String;
4
use core::borrow::Borrow;
5
#[cfg(not(feature = "std"))]
6
use core::error;
7
use core::fmt;
8
#[cfg(feature = "std")]
9
use std::error;
10

11
use bitcoin::FeeRate;
12
use tracing::warn;
13

14
use crate::output_substitution::OutputSubstitution;
15
use crate::Version;
16

17
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
18
pub(crate) struct Params {
19
    // version
20
    pub v: Version,
21
    // disableoutputsubstitution
22
    pub output_substitution: OutputSubstitution,
23
    // maxadditionalfeecontribution, additionalfeeoutputindex
24
    pub additional_fee_contribution: Option<(bitcoin::Amount, usize)>,
25
    // minfeerate
26
    pub min_fee_rate: FeeRate,
27
}
28

29
impl Default for Params {
30
    fn default() -> Self {
70✔
31
        Params {
70✔
32
            v: Version::One,
70✔
33
            output_substitution: OutputSubstitution::Enabled,
70✔
34
            additional_fee_contribution: None,
70✔
35
            min_fee_rate: FeeRate::BROADCAST_MIN,
70✔
36
        }
70✔
37
    }
70✔
38
}
39

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

62
    pub fn from_query_pairs<K, V, I>(
63✔
63
        pairs: I,
63✔
64
        supported_versions: &'static [Version],
63✔
65
    ) -> Result<Self, Error>
63✔
66
    where
63✔
67
        I: Iterator<Item = (K, V)>,
63✔
68
        K: Borrow<str> + Into<String>,
63✔
69
        V: Borrow<str> + Into<String>,
63✔
70
    {
71
        let mut params = Params::default();
63✔
72

73
        let mut additional_fee_output_index = None;
63✔
74
        let mut max_additional_fee_contribution = None;
63✔
75

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

123
        params.handle_additonal_fee_param(
62✔
124
            max_additional_fee_contribution,
62✔
125
            additional_fee_output_index,
62✔
126
        );
127

128
        tracing::trace!("parsed optional parameters: {params:?}");
62✔
129
        Ok(params)
62✔
130
    }
63✔
131

132
    #[cfg(feature = "std")]
133
    pub fn from_query_str(
62✔
134
        query: &str,
62✔
135
        supported_versions: &'static [Version],
62✔
136
    ) -> Result<Self, Error> {
62✔
137
        let url = crate::Url::parse(&format!("http://localhost/?{query}"))
62✔
138
            .map_err(|_| Error::MalformedQuery)?;
62✔
139
        Self::from_query_pairs(url.query_pairs().into_iter(), supported_versions)
62✔
140
    }
62✔
141
}
142

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

150
impl fmt::Display for Error {
151
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
×
152
        match self {
×
153
            Error::UnknownVersion { .. } => write!(f, "unknown version"),
×
154
            Error::FeeRate => write!(f, "could not parse feerate"),
×
155
            Error::MalformedQuery => write!(f, "malformed query parameter encoding"),
×
156
        }
157
    }
×
158
}
159

160
impl error::Error for Error {
NEW
161
    fn source(&self) -> Option<&(dyn error::Error + 'static)> { None }
×
162
}
163

164
#[cfg(test)]
165
pub(crate) mod test {
166
    use bitcoin::{Amount, FeeRate};
167

168
    use super::*;
169
    use crate::receive::optional_parameters::Params;
170
    use crate::Version;
171

172
    #[test]
173
    fn test_parse_params() {
1✔
174
        let params = Params::from_query_str("&maxadditionalfeecontribution=182&additionalfeeoutputindex=0&minfeerate=2&disableoutputsubstitution=true&optimisticmerge=true", &[Version::One])
1✔
175
            .expect("Could not parse params from query str");
1✔
176
        assert_eq!(params.v, Version::One);
1✔
177
        assert_eq!(params.output_substitution, OutputSubstitution::Disabled);
1✔
178
        assert_eq!(params.additional_fee_contribution, Some((Amount::from_sat(182), 0)));
1✔
179
        assert_eq!(
1✔
180
            params.min_fee_rate,
181
            FeeRate::from_sat_per_vb(2).expect("Could not calculate feerate")
1✔
182
        );
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