Skip to content

Commit 92c02cc

Browse files
committed
De/Serialize v2 payload
1 parent 59ca1d6 commit 92c02cc

File tree

10 files changed

+361
-150
lines changed

10 files changed

+361
-150
lines changed

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

payjoin-cli/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ path = "src/main.rs"
1010

1111
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
1212
[features]
13+
v1 = ["payjoin/send", "payjoin/receive"]
1314
native-tls-vendored = ["reqwest/native-tls-vendored"]
1415
local-https = ["rcgen", "rouille/ssl"]
1516
v2 = ["payjoin/v2", "tokio/full"]

payjoin-cli/src/app.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@ impl App {
241241
let buffer = Self::long_poll_get(&client, &receive_endpoint).await?;
242242

243243
log::debug!("Received request");
244-
let proposal = UncheckedProposal::from_base64(&buffer)
244+
let proposal = UncheckedProposal::from_streamed(&buffer)
245245
.map_err(|e| anyhow!("Failed to parse into UncheckedProposal {}", e))?;
246246
let payjoin_psbt = self
247247
.process_proposal(proposal)

payjoin/Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,15 @@ edition = "2018"
1616
send = []
1717
receive = ["rand"]
1818
base64 = ["bitcoin/base64"]
19-
v2 = []
19+
v2 = ["serde", "serde_json"]
2020

2121
[dependencies]
2222
bitcoin = { version = "0.30.0", features = ["base64"] }
2323
bip21 = "0.3.1"
2424
log = { version = "0.4.14"}
2525
rand = { version = "0.8.4", optional = true }
26+
serde = { version = "1.0", optional = true }
27+
serde_json = { version = "1.0", optional = true }
2628
url = "2.2.2"
2729

2830
[dev-dependencies]

payjoin/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ pub mod send;
3030
#[cfg(any(feature = "send", feature = "receive"))]
3131
pub(crate) mod input_type;
3232
#[cfg(any(feature = "send", feature = "receive"))]
33+
pub(crate) mod optional_parameters;
34+
#[cfg(any(feature = "send", feature = "receive"))]
3335
pub(crate) mod psbt;
3436
mod uri;
3537
#[cfg(any(feature = "send", feature = "receive"))]

payjoin/src/optional_parameters.rs

Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
use std::borrow::Borrow;
2+
use std::fmt;
3+
4+
use bitcoin::{Amount, FeeRate};
5+
use log::warn;
6+
use serde::de::{Deserializer, MapAccess, Visitor};
7+
use serde::ser::SerializeMap;
8+
use serde::{Deserialize, Serialize, Serializer};
9+
10+
#[derive(Debug)]
11+
#[cfg_attr(feature = "v2", derive(Deserialize, Serialize))]
12+
pub(crate) struct Params {
13+
// version
14+
#[cfg_attr(
15+
feature = "v2",
16+
serde(skip_serializing_if = "skip_if_default_v", default = "default_v")
17+
)]
18+
pub v: usize,
19+
20+
// disableoutputsubstitution
21+
#[cfg_attr(
22+
feature = "v2",
23+
serde(skip_serializing_if = "skip_if_false", default = "default_output_substitution")
24+
)]
25+
pub disable_output_substitution: bool,
26+
27+
// maxadditionalfeecontribution, additionalfeeoutputindex
28+
#[cfg_attr(
29+
feature = "v2",
30+
serde(
31+
deserialize_with = "deserialize_additional_fee_contribution",
32+
skip_serializing_if = "Option::is_none",
33+
serialize_with = "serialize_additional_fee_contribution"
34+
)
35+
)]
36+
pub additional_fee_contribution: Option<(Amount, usize)>,
37+
38+
// minfeerate
39+
#[cfg_attr(
40+
feature = "v2",
41+
serde(
42+
deserialize_with = "from_sat_per_vb",
43+
skip_serializing_if = "skip_if_zero_rate",
44+
default = "default_min_feerate"
45+
)
46+
)]
47+
pub min_feerate: FeeRate,
48+
}
49+
50+
impl Default for Params {
51+
fn default() -> Self {
52+
Params {
53+
v: 1,
54+
disable_output_substitution: false,
55+
additional_fee_contribution: None,
56+
min_feerate: FeeRate::ZERO,
57+
}
58+
}
59+
}
60+
61+
impl Params {
62+
#[cfg(feature = "receive")]
63+
pub fn from_query_pairs<K, V, I>(pairs: I) -> Result<Self, Error>
64+
where
65+
I: Iterator<Item = (K, V)>,
66+
K: Borrow<str> + Into<String>,
67+
V: Borrow<str> + Into<String>,
68+
{
69+
let mut params = Params::default();
70+
71+
let mut additional_fee_output_index = None;
72+
let mut max_additional_fee_contribution = None;
73+
74+
for (k, v) in pairs {
75+
match (k.borrow(), v.borrow()) {
76+
("v", v) =>
77+
if v != "1" {
78+
return Err(Error::UnknownVersion);
79+
},
80+
("additionalfeeoutputindex", index) =>
81+
additional_fee_output_index = match index.parse::<usize>() {
82+
Ok(index) => Some(index),
83+
Err(_error) => {
84+
warn!(
85+
"bad `additionalfeeoutputindex` query value '{}': {}",
86+
index, _error
87+
);
88+
None
89+
}
90+
},
91+
("maxadditionalfeecontribution", fee) =>
92+
max_additional_fee_contribution =
93+
match bitcoin::Amount::from_str_in(fee, bitcoin::Denomination::Satoshi) {
94+
Ok(contribution) => Some(contribution),
95+
Err(_error) => {
96+
warn!(
97+
"bad `maxadditionalfeecontribution` query value '{}': {}",
98+
fee, _error
99+
);
100+
None
101+
}
102+
},
103+
("minfeerate", feerate) =>
104+
params.min_feerate = match feerate.parse::<f32>() {
105+
Ok(fee_rate_sat_per_vb) => {
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;
108+
// since it's a minnimum, we want to round up
109+
FeeRate::from_sat_per_kwu(fee_rate_sat_per_kwu.ceil() as u64)
110+
}
111+
Err(e) => return Err(Error::FeeRate(e.to_string())),
112+
},
113+
("disableoutputsubstitution", v) =>
114+
params.disable_output_substitution = v == "true",
115+
_ => (),
116+
}
117+
}
118+
119+
match (max_additional_fee_contribution, additional_fee_output_index) {
120+
(Some(amount), Some(index)) =>
121+
params.additional_fee_contribution = Some((amount, index)),
122+
(Some(_), None) | (None, Some(_)) => {
123+
warn!("only one additional-fee parameter specified: {:?}", params);
124+
}
125+
_ => (),
126+
}
127+
128+
log::debug!("parsed optional parameters: {:?}", params);
129+
Ok(params)
130+
}
131+
}
132+
133+
fn deserialize_additional_fee_contribution<'de, D>(
134+
deserializer: D,
135+
) -> Result<Option<(bitcoin::Amount, usize)>, D::Error>
136+
where
137+
D: Deserializer<'de>,
138+
{
139+
struct AdditionalFeeContributionVisitor;
140+
141+
impl<'de> Visitor<'de> for AdditionalFeeContributionVisitor {
142+
type Value = Option<(bitcoin::Amount, usize)>;
143+
144+
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
145+
formatter.write_str("struct params")
146+
}
147+
148+
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
149+
where
150+
A: MapAccess<'de>,
151+
{
152+
let mut additional_fee_output_index: Option<usize> = None;
153+
let mut max_additional_fee_contribution: Option<bitcoin::Amount> = None;
154+
155+
while let Some(key) = map.next_key()? {
156+
match key {
157+
"additional_fee_output_index" => {
158+
additional_fee_output_index = Some(map.next_value()?);
159+
}
160+
"max_additional_fee_contribution" => {
161+
max_additional_fee_contribution =
162+
Some(bitcoin::Amount::from_sat(map.next_value()?));
163+
}
164+
_ => {
165+
// ignore other fields
166+
}
167+
}
168+
}
169+
170+
let additional_fee_contribution =
171+
match (max_additional_fee_contribution, additional_fee_output_index) {
172+
(Some(amount), Some(index)) => Some((amount, index)),
173+
(Some(_), None) | (None, Some(_)) => {
174+
warn!(
175+
"only one additional-fee parameter specified: {:?}, {:?}",
176+
max_additional_fee_contribution, additional_fee_output_index
177+
);
178+
None
179+
}
180+
_ => None,
181+
};
182+
Ok(additional_fee_contribution)
183+
}
184+
}
185+
186+
deserializer.deserialize_map(AdditionalFeeContributionVisitor)
187+
}
188+
189+
fn default_v() -> usize { 2 }
190+
191+
fn default_output_substitution() -> bool { false }
192+
193+
fn default_min_feerate() -> FeeRate { FeeRate::ZERO }
194+
195+
// Function to determine whether to skip serializing a usize if it is 2 (the default)
196+
fn skip_if_default_v(v: &usize) -> bool { *v == 2 }
197+
198+
// Function to determine whether to skip serializing a bool if it is false (the default)
199+
fn skip_if_false(b: &bool) -> bool { !(*b) }
200+
201+
// Function to determine whether to skip serializing a FeeRate if it is ZERO (the default)
202+
fn skip_if_zero_rate(rate: &FeeRate) -> bool {
203+
*rate == FeeRate::ZERO // replace with your actual comparison logic
204+
}
205+
206+
fn from_sat_per_vb<'de, D>(deserializer: D) -> Result<FeeRate, D::Error>
207+
where
208+
D: Deserializer<'de>,
209+
{
210+
let fee_rate_sat_per_vb = f32::deserialize(deserializer)?;
211+
Ok(FeeRate::from_sat_per_kwu((fee_rate_sat_per_vb * 250.0_f32) as u64))
212+
}
213+
214+
fn serialize_amount<S>(amount: &Amount, serializer: S) -> Result<S::Ok, S::Error>
215+
where
216+
S: Serializer,
217+
{
218+
serializer.serialize_u64(amount.to_sat())
219+
}
220+
221+
fn serialize_additional_fee_contribution<S>(
222+
additional_fee_contribution: &Option<(Amount, usize)>,
223+
serializer: S,
224+
) -> Result<S::Ok, S::Error>
225+
where
226+
S: Serializer,
227+
{
228+
let mut map = serializer.serialize_map(None)?;
229+
if let Some((amount, index)) = additional_fee_contribution {
230+
map.serialize_entry("additional_fee_output_index", index)?;
231+
map.serialize_entry("max_additional_fee_contribution", &amount.to_sat())?;
232+
}
233+
map.end()
234+
}
235+
236+
#[derive(Debug)]
237+
pub(crate) enum Error {
238+
UnknownVersion,
239+
FeeRate(String),
240+
#[cfg(feature = "v2")]
241+
Json(serde_json::Error),
242+
}
243+
244+
impl fmt::Display for Error {
245+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
246+
match self {
247+
Error::UnknownVersion => write!(f, "unknown version"),
248+
Error::FeeRate(_) => write!(f, "could not parse feerate"),
249+
#[cfg(feature = "v2")]
250+
Error::Json(e) => write!(f, "could not parse json: {}", e),
251+
}
252+
}
253+
}
254+
255+
impl std::error::Error for Error {
256+
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { None }
257+
}

payjoin/src/receive/error.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ pub(crate) enum InternalRequestError {
4747
InvalidContentType(String),
4848
InvalidContentLength(std::num::ParseIntError),
4949
ContentLengthTooLarge(u64),
50-
SenderParams(super::optional_parameters::Error),
50+
SenderParams(crate::optional_parameters::Error),
5151
/// The raw PSBT fails bip78-specific validation.
5252
InconsistentPsbt(crate::psbt::InconsistentPsbt),
5353
/// The prevtxout is missing
@@ -65,6 +65,9 @@ pub(crate) enum InternalRequestError {
6565
/// Original PSBT input has been seen before. Only automatic receivers, aka "interactive" in the spec
6666
/// look out for these to prevent probing attacks.
6767
InputSeen(bitcoin::OutPoint),
68+
/// Serde deserialization failed
69+
#[cfg(feature = "v2")]
70+
Json(serde_json::Error),
6871
}
6972

7073
impl From<InternalRequestError> for RequestError {
@@ -96,7 +99,7 @@ impl fmt::Display for RequestError {
9699
&format!("Content length too large: {}.", length),
97100
),
98101
InternalRequestError::SenderParams(e) => match e {
99-
super::optional_parameters::Error::UnknownVersion => write_error(
102+
crate::optional_parameters::Error::UnknownVersion => write_error(
100103
f,
101104
"version-unsupported",
102105
"This version of payjoin is not supported.",
@@ -125,6 +128,8 @@ impl fmt::Display for RequestError {
125128
write_error(f, "original-psbt-rejected", &format!("Input Type Error: {}.", e)),
126129
InternalRequestError::InputSeen(_) =>
127130
write_error(f, "original-psbt-rejected", "The receiver rejected the original PSBT."),
131+
#[cfg(feature = "v2")]
132+
InternalRequestError::Json(e) => write_error(f, "json-error", e),
128133
}
129134
}
130135
}

0 commit comments

Comments
 (0)