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

payjoin / rust-payjoin / 29760723693

20 Jul 2026 04:42PM UTC coverage: 85.722% (-0.6%) from 86.298%
29760723693

Pull #1615

github

web-flow
Merge 4377a7b00 into 79db35e35
Pull Request #1615: feat: port payjoin core to no_std

312 of 419 new or added lines in 25 files covered. (74.46%)

68 existing lines in 7 files now uncovered.

13731 of 16018 relevant lines covered (85.72%)

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

9
use bitcoin::FeeRate;
10
use tracing::warn;
11

12
use crate::output_substitution::OutputSubstitution;
13
use crate::Version;
14

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

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

38
impl Params {
39
    /// Warn 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 unusual
43
    /// configuration that prevents fee adjustment capability.
44
    fn handle_additonal_fee_param(
86✔
45
        &mut self,
86✔
46
        max_additional_fee_contribution: Option<bitcoin::Amount>,
86✔
47
        additional_fee_output_index: Option<usize>,
86✔
48
    ) {
86✔
49
        match (max_additional_fee_contribution, additional_fee_output_index) {
86✔
50
            (Some(amount), Some(index)) => {
80✔
51
                self.additional_fee_contribution = Some((amount, index));
80✔
52
            }
80✔
53
            (Some(_), None) | (None, Some(_)) => {
54
                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:?}");
×
55
            }
56
            (None, None) => (), // Neither parameter provided, normal case
6✔
57
        }
58
    }
86✔
59

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

71
        let mut additional_fee_output_index = None;
87✔
72
        let mut max_additional_fee_contribution = None;
87✔
73

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

121
        params.handle_additonal_fee_param(
86✔
122
            max_additional_fee_contribution,
86✔
123
            additional_fee_output_index,
86✔
124
        );
125

126
        tracing::trace!("parsed optional parameters: {params:?}");
86✔
127
        Ok(params)
86✔
128
    }
87✔
129

130
    pub fn from_query_str(
86✔
131
        query: &str,
86✔
132
        supported_versions: &'static [Version],
86✔
133
    ) -> Result<Self, Error> {
86✔
134
        let pairs =
86✔
135
            query.split('&').filter(|s| !s.is_empty()).filter_map(|pair| pair.split_once('='));
197✔
136
        Self::from_query_pairs(pairs, supported_versions)
86✔
137
    }
86✔
138
}
139

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

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

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

161
#[cfg(test)]
162
pub(crate) mod test {
163
    use bitcoin::FeeRate;
164
    use payjoin_test_utils::MAX_ADDITIONAL_FEE_CONTRIBUTION;
165

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

170
    #[test]
171
    fn test_parse_params() {
1✔
172
        let params = Params::from_query_str("&maxadditionalfeecontribution=182&additionalfeeoutputindex=0&minfeerate=2&disableoutputsubstitution=true&optimisticmerge=true", &[Version::One])
1✔
173
            .expect("Could not parse params from query str");
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((MAX_ADDITIONAL_FEE_CONTRIBUTION, 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
    }
1✔
182

183
    #[test]
184
    fn from_query_pairs_unsupported_versions() {
1✔
185
        let invalid_pair: Vec<(&str, &str)> = vec![("v", "888")];
1✔
186
        let supported_versions = &[Version::One, Version::Two];
1✔
187
        let params = Params::from_query_pairs(invalid_pair.into_iter(), supported_versions);
1✔
188
        assert!(params.is_err());
1✔
189
        assert_eq!(params.err().unwrap(), Error::UnknownVersion { supported_versions });
1✔
190
    }
1✔
191
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc