Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions payjoin-ffi/src/receive/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ pub mod error;
#[cfg(feature = "uniffi")]
pub mod uni;

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct SessionEvent(payjoin::receive::v2::SessionEvent);

#[derive(Debug)]
pub struct NewReceiver(payjoin::receive::v2::NewReceiver);

Expand Down
3 changes: 3 additions & 0 deletions payjoin-ffi/src/receive/uni.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ pub use crate::receive::{
use crate::uri::error::IntoUrlError;
use crate::{ClientResponse, OhttpKeys, OutputSubstitution, Request};

#[derive(Clone, uniffi::Object, serde::Serialize, serde::Deserialize)]
pub struct SessionEvent(super::SessionEvent);

#[derive(Debug, uniffi::Object)]
pub struct NewReceiver(pub super::NewReceiver);

Expand Down
4 changes: 2 additions & 2 deletions payjoin/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ exclude = ["tests"]
[features]
default = ["v2"]
#[doc = "Core features for payjoin state machines"]
_core = ["bitcoin/rand-std", "serde_json", "url", "bitcoin_uri", "serde"]
_core = ["bitcoin/rand-std", "serde_json", "url", "bitcoin_uri", "serde", "bitcoin/serde"]
directory = []
v1 = ["_core"]
v2 = ["_core", "bitcoin/serde", "hpke", "dep:http", "bhttp", "ohttp", "url/serde", "directory"]
v2 = ["_core", "hpke", "dep:http", "bhttp", "ohttp", "url/serde", "directory"]
#[doc = "Functions to fetch OHTTP keys via CONNECT proxy using reqwest. Enables `v2` since only `v2` uses OHTTP."]
io = ["v2", "reqwest/rustls-tls"]
_danger-local-https = ["reqwest/rustls-tls", "rustls"]
Expand Down
19 changes: 16 additions & 3 deletions payjoin/src/core/version.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use core::fmt;

use serde::{Serialize, Serializer};
use serde::{Deserialize, Deserializer, Serialize, Serializer};

/// The Payjoin version
///
Expand All @@ -16,8 +16,7 @@ use serde::{Serialize, Serializer};
/// ```
///
/// # Note
/// - Only [`Serialize`] is implemented for json array serialization in the `unsupported-version` error message,
/// not [`serde::Deserialize`], as deserialization is not required for this type.
/// - Both [`Serialize`] and [`Deserialize`] are implemented for json array serialization in the `unsupported-version` error message,
/// - [`fmt::Display`] and [`fmt::Debug`] output the `u8` representation for compatibility with BIP 78/77
/// and to match the expected wire format.
#[repr(u8)]
Expand Down Expand Up @@ -45,3 +44,17 @@ impl Serialize for Version {
(*self as u8).serialize(serializer)
}
}

impl<'de> Deserialize<'de> for Version {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let v = u8::deserialize(deserializer)?;
match v {
1 => Ok(Version::One),
2 => Ok(Version::Two),
_ => Err(serde::de::Error::custom("Invalid version")),
}
}
}
3 changes: 1 addition & 2 deletions payjoin/src/output_substitution.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
/// Whether the receiver is allowed to substitute original outputs or not.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "v2", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum OutputSubstitution {
Enabled,
Disabled,
Expand Down
2 changes: 1 addition & 1 deletion payjoin/src/receive/optional_parameters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use log::warn;
use crate::output_substitution::OutputSubstitution;
use crate::Version;

#[derive(Debug, Clone)]
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
pub(crate) struct Params {
// version
pub v: Version,
Expand Down
17 changes: 9 additions & 8 deletions payjoin/src/receive/v1/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ use bitcoin::psbt::Psbt;
use bitcoin::secp256k1::rand::seq::SliceRandom;
use bitcoin::secp256k1::rand::{self, Rng};
use bitcoin::{Amount, FeeRate, OutPoint, Script, TxIn, TxOut, Weight};
use serde::{Deserialize, Serialize};

use super::error::{
InputContributionError, InternalInputContributionError, InternalOutputSubstitutionError,
Expand All @@ -57,7 +58,7 @@ pub use exclusive::*;
/// transaction with extract_tx_to_schedule_broadcast() and schedule, followed by checking
/// that the transaction can be broadcast with check_broadcast_suitability. Otherwise it is safe to
/// call assume_interactive_receive to proceed with validation.
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct UncheckedProposal {
pub(crate) psbt: Psbt,
pub(crate) params: Params,
Expand Down Expand Up @@ -128,7 +129,7 @@ impl UncheckedProposal {
/// Typestate to validate that the Original PSBT has no receiver-owned inputs.
///
/// Call [`Self::check_inputs_not_owned`] to proceed.
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct MaybeInputsOwned {
psbt: Psbt,
params: Params,
Expand Down Expand Up @@ -171,7 +172,7 @@ impl MaybeInputsOwned {
/// Typestate to validate that the Original PSBT has no inputs that have been seen before.
///
/// Call [`Self::check_no_inputs_seen_before`] to proceed.
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct MaybeInputsSeen {
psbt: Psbt,
params: Params,
Expand Down Expand Up @@ -203,7 +204,7 @@ impl MaybeInputsSeen {
///
/// Only accept PSBTs that send us money.
/// Identify those outputs with [`Self::identify_receiver_outputs`] to proceed.
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct OutputsUnknown {
psbt: Psbt,
params: Params,
Expand Down Expand Up @@ -255,7 +256,7 @@ impl OutputsUnknown {
/// A checked proposal that the receiver may substitute or add outputs to
///
/// Call [`Self::commit_outputs`] to proceed.
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct WantsOutputs {
original_psbt: Psbt,
payjoin_psbt: Psbt,
Expand Down Expand Up @@ -388,7 +389,7 @@ fn interleave_shuffle<T: Clone, R: rand::Rng>(original: &mut Vec<T>, new: &mut [
/// A checked proposal that the receiver may contribute inputs to to make a payjoin
///
/// Call [`Self::commit_inputs`] to proceed.
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct WantsInputs {
original_psbt: Psbt,
payjoin_psbt: Psbt,
Expand Down Expand Up @@ -551,7 +552,7 @@ impl WantsInputs {
/// sender will accept.
///
/// Call [`Self::finalize_proposal`] to return a finalized [`PayjoinProposal`].
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ProvisionalProposal {
original_psbt: Psbt,
payjoin_psbt: Psbt,
Expand Down Expand Up @@ -768,7 +769,7 @@ impl ProvisionalProposal {

/// A finalized payjoin proposal, complete with fees and receiver signatures, that the sender
/// should find acceptable.
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct PayjoinProposal {
payjoin_psbt: Psbt,
}
Expand Down
4 changes: 2 additions & 2 deletions payjoin/src/receive/v2/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use bitcoin::psbt::Psbt;
use bitcoin::{Address, FeeRate, OutPoint, Script, TxOut};
pub(crate) use error::InternalSessionError;
pub use error::SessionError;
pub use persist::ReceiverToken;
pub use persist::{ReceiverToken, SessionEvent};
use serde::de::Deserializer;
use serde::{Deserialize, Serialize};
use url::Url;
Expand All @@ -32,7 +32,7 @@ const SUPPORTED_VERSIONS: &[Version] = &[Version::One, Version::Two];
static TWENTY_FOUR_HOURS_DEFAULT_EXPIRY: Duration = Duration::from_secs(60 * 60 * 24);

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct SessionContext {
pub struct SessionContext {
#[serde(deserialize_with = "deserialize_address_assume_checked")]
address: Address,
directory: url::Url,
Expand Down
75 changes: 74 additions & 1 deletion payjoin/src/receive/v2/persist.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
use std::fmt::{self, Display};

use super::{Receiver, WithContext};
use serde::{Deserialize, Serialize};

use super::{Receiver, SessionContext, WithContext};
use crate::persist::{self};
use crate::receive::v1;
use crate::uri::ShortId;

/// Opaque key type for the receiver
Expand All @@ -25,3 +28,73 @@ impl persist::Value for Receiver<WithContext> {

fn key(&self) -> Self::Key { ReceiverToken(self.context.id()) }
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
/// Represents a piece of information that the receiver has obtained from the session
/// Each event can be used to transition the receiver state machine to a new state
pub enum SessionEvent {
Created(SessionContext),
UncheckedProposal(v1::UncheckedProposal),
MaybeInputsOwned(v1::MaybeInputsOwned),
MaybeInputsSeen(v1::MaybeInputsSeen),
OutputsUnknown(v1::OutputsUnknown),
WantsOutputs(v1::WantsOutputs),
WantsInputs(v1::WantsInputs),
ProvisionalProposal(v1::ProvisionalProposal),
PayjoinProposal(v1::PayjoinProposal),
/// Session is invalid. This is a irrecoverable error. Fallback tx should be broadcasted.
/// TODO this should be any error type that is impl std::error and works well with serde, or as a fallback can be formatted as a string
/// Reason being in some cases we still want to preserve the error b/c we can action on it. For now this is a terminal state and there is nothing to replay and is saved to be displayed.
/// b/c its a terminal state and there is nothing to replay. So serialization will be lossy and that is fine.
Comment on lines +45 to +48
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't really understand the TODO here, maybe this needs to be reworded? Is it a TODO because it needs the rest of the SEL changes to be implemented first?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No.
The TODO is saying: instead of a string repr of the error it should be any error type that plays nicely with serde. In the future we may want to take action on why a session closed. However its a TODO because we dont impl serde::serialize/deserialize on our error types.

SessionInvalid(String),
}

#[cfg(test)]
mod tests {
use super::*;
use crate::receive::v1::test::unchecked_proposal_from_test_vector;
use crate::receive::v2::test::SHARED_CONTEXT;

#[test]
fn test_session_event_serialization_roundtrip() {
let unchecked_proposal = unchecked_proposal_from_test_vector();
let maybe_inputs_owned = unchecked_proposal.clone().assume_interactive_receiver();
let maybe_inputs_seen = maybe_inputs_owned
.clone()
.check_inputs_not_owned(|_| Ok(false))
.expect("No inputs should be owned");
let outputs_unknown = maybe_inputs_seen
.clone()
.check_no_inputs_seen_before(|_| Ok(false))
.expect("No inputs should be seen before");
let wants_outputs = outputs_unknown
.clone()
.identify_receiver_outputs(|_| Ok(true))
.expect("Outputs should be identified");
let wants_inputs = wants_outputs.clone().commit_outputs();
let provisional_proposal = wants_inputs.clone().commit_inputs();
let payjoin_proposal = provisional_proposal
.clone()
.finalize_proposal(|psbt| Ok(psbt.clone()), None, None)
.expect("Payjoin proposal should be finalized");

let test_cases = vec![
SessionEvent::Created(SHARED_CONTEXT.clone()),
SessionEvent::UncheckedProposal(unchecked_proposal),
SessionEvent::MaybeInputsOwned(maybe_inputs_owned),
SessionEvent::MaybeInputsSeen(maybe_inputs_seen),
SessionEvent::OutputsUnknown(outputs_unknown),
SessionEvent::WantsOutputs(wants_outputs),
SessionEvent::WantsInputs(wants_inputs),
SessionEvent::ProvisionalProposal(provisional_proposal),
SessionEvent::PayjoinProposal(payjoin_proposal),
];

for event in test_cases {
let serialized = serde_json::to_string(&event).expect("Serialization should not fail");
let deserialized: SessionEvent =
serde_json::from_str(&serialized).expect("Deserialization should not fail");
assert_eq!(event, deserialized);
}
}
}
Loading