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
19 changes: 9 additions & 10 deletions payjoin-cli/src/app/v2/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ use payjoin::persist::OptionalTransitionOutcome;
use payjoin::receive::v2::{
process_err_res, replay_event_log as replay_receiver_event_log, Initialized, MaybeInputsOwned,
MaybeInputsSeen, OutputsUnknown, PayjoinProposal, ProvisionalProposal, ReceiveSession,
Receiver, SessionHistory, UncheckedProposal, WantsFeeRange, WantsInputs, WantsOutputs,
Receiver, ReceiverBuilder, SessionHistory, UncheckedProposal, WantsFeeRange, WantsInputs,
WantsOutputs,
};
use payjoin::send::v2::{
replay_event_log as replay_sender_event_log, SendSession, Sender, SenderBuilder, V2GetContext,
Expand Down Expand Up @@ -158,14 +159,12 @@ impl AppTrait for App {
.await?
.ohttp_keys;
let persister = ReceiverPersister::new(self.db.clone())?;
let session = Receiver::create_session(
address,
self.config.v2()?.pj_directory.clone(),
ohttp_keys,
None,
Some(amount),
)?
.save(&persister)?;
let session =
ReceiverBuilder::new(address, self.config.v2()?.pj_directory.clone(), ohttp_keys)?
.with_amount(amount)
.build()
.save(&persister)?;

println!("Receive session established");
let pj_uri = session.pj_uri();
println!("Request Payjoin by sharing this Payjoin Uri:");
Expand Down Expand Up @@ -352,7 +351,7 @@ impl App {
self.finalize_proposal(proposal, persister).await,
ReceiveSession::PayjoinProposal(proposal) =>
self.send_payjoin_proposal(proposal, persister).await,
ReceiveSession::Uninitialized(_) =>
ReceiveSession::Uninitialized =>
return Err(anyhow!("Uninitialized receiver session")),
ReceiveSession::TerminalFailure =>
return Err(anyhow!("Terminal receiver session")),
Expand Down
4 changes: 2 additions & 2 deletions payjoin-ffi/dart/test/test_payjoin_integration_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -134,8 +134,8 @@ payjoin.Initialized create_receiver_context(
String directory,
payjoin.OhttpKeys ohttp_keys,
InMemoryReceiverPersister persister) {
var receiver = payjoin.UninitializedReceiver()
.createSession(address, directory, ohttp_keys, null, null)
var receiver = payjoin.ReceiverBuilder(address, directory, ohttp_keys)
.build()
.save(persister);
return receiver;
}
Expand Down
24 changes: 10 additions & 14 deletions payjoin-ffi/dart/test/test_payjoin_unit_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -103,15 +103,12 @@ void main() {
var persister = InMemoryReceiverPersister("1");
var address = bitcoin.Address(
"tb1q6d3a2w975yny0asuvd9a67ner4nks58ff0q8g4", bitcoin.Network.signet);
payjoin.UninitializedReceiver()
.createSession(
address,
"https://example.com",
payjoin.OhttpKeys.fromString(
"OH1QYPM5JXYNS754Y4R45QWE336QFX6ZR8DQGVQCULVZTV20TFVEYDMFQC"),
null,
null)
.save(persister);
payjoin.ReceiverBuilder(
address,
"https://example.com",
payjoin.OhttpKeys.fromString(
"OH1QYPM5JXYNS754Y4R45QWE336QFX6ZR8DQGVQCULVZTV20TFVEYDMFQC"),
).build().save(persister);
final result = payjoin.replayReceiverEventLog(persister);
expect(result, isA<payjoin.ReplayResult>(),
reason: "persistence should return a replay result");
Expand All @@ -121,15 +118,14 @@ void main() {
var receiver_persister = InMemoryReceiverPersister("1");
var address = bitcoin.Address(
"2MuyMrZHkbHbfjudmKUy45dU4P17pjG2szK", bitcoin.Network.testnet);
var receiver = payjoin.UninitializedReceiver()
.createSession(
var receiver = payjoin.ReceiverBuilder(
address,
"https://example.com",
payjoin.OhttpKeys.fromString(
"OH1QYPM5JXYNS754Y4R45QWE336QFX6ZR8DQGVQCULVZTV20TFVEYDMFQC"),
null,
null)
.save(receiver_persister);
)
.build()
.save(receiver_persister);
var uri = receiver.pjUri();

var sender_persister = InMemorySenderPersister("1");
Expand Down
2 changes: 1 addition & 1 deletion payjoin-ffi/python/test/test_payjoin_integration_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ async def process_receiver_proposal(self, receiver: ReceiveSession, recv_persist
raise Exception(f"Unknown receiver state: {receiver}")

def create_receiver_context(self, receiver_address: bitcoinffi.Address, directory: Url, ohttp_keys: OhttpKeys, recv_persister: InMemoryReceiverSessionEventLog) -> Initialized:
receiver = UninitializedReceiver().create_session(address=receiver_address, directory=directory.as_string(), ohttp_keys=ohttp_keys, expire_after=None, amount=None).save(recv_persister)
receiver = ReceiverBuilder(address=receiver_address, directory=directory.as_string(), ohttp_keys=ohttp_keys).build().save(recv_persister)
return receiver

async def retrieve_receiver_proposal(self, receiver: Initialized, recv_persister: InMemoryReceiverSessionEventLog, ohttp_relay: Url):
Expand Down
12 changes: 4 additions & 8 deletions payjoin-ffi/python/test/test_payjoin_unit_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,11 @@ class TestReceiverPersistence(unittest.TestCase):
def test_receiver_persistence(self):
persister = InMemoryReceiverPersister(1)
address = payjoin.bitcoin.Address("tb1q6d3a2w975yny0asuvd9a67ner4nks58ff0q8g4", payjoin.bitcoin.Network.SIGNET)
payjoin.payjoin_ffi.UninitializedReceiver().create_session(
payjoin.payjoin_ffi.ReceiverBuilder(
address,
"https://example.com",
payjoin.OhttpKeys.from_string("OH1QYPM5JXYNS754Y4R45QWE336QFX6ZR8DQGVQCULVZTV20TFVEYDMFQC"),
None,
None
).save(persister)
).build().save(persister)
result = payjoin.payjoin_ffi.replay_receiver_event_log(persister)
self.assertTrue(result.state().is_INITIALIZED())

Expand All @@ -80,13 +78,11 @@ def test_sender_persistence(self):
# Create a receiver to just get the pj uri
persister = InMemoryReceiverPersister(1)
address = payjoin.bitcoin.Address("2MuyMrZHkbHbfjudmKUy45dU4P17pjG2szK", payjoin.bitcoin.Network.TESTNET)
receiver = payjoin.payjoin_ffi.UninitializedReceiver().create_session(
receiver = payjoin.payjoin_ffi.ReceiverBuilder(
address,
"https://example.com",
payjoin.OhttpKeys.from_string("OH1QYPM5JXYNS754Y4R45QWE336QFX6ZR8DQGVQCULVZTV20TFVEYDMFQC"),
None,
None
).save(persister)
).build().save(persister)
uri = receiver.pj_uri()

persister = InMemorySenderPersister(1)
Expand Down
64 changes: 36 additions & 28 deletions payjoin-ffi/src/receive/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ pub use error::{
ReplyableError, SelectionError, SessionError,
};
use payjoin::bitcoin::psbt::Psbt;
use payjoin::bitcoin::FeeRate;
use payjoin::bitcoin::{Amount, FeeRate};
use payjoin::persist::{MaybeFatalTransition, NextStateTransition};

use crate::bitcoin_ffi::{Address, OutPoint, Script, TxOut};
Expand Down Expand Up @@ -93,7 +93,7 @@ impl From<payjoin::receive::v2::ReceiveSession> for ReceiveSession {
fn from(value: payjoin::receive::v2::ReceiveSession) -> Self {
use payjoin::receive::v2::ReceiveSession;
match value {
ReceiveSession::Uninitialized(_) => Self::Uninitialized,
ReceiveSession::Uninitialized => Self::Uninitialized,
ReceiveSession::Initialized(inner) =>
Self::Initialized { inner: Arc::new(inner.into()) },
ReceiveSession::UncheckedProposal(inner) =>
Expand Down Expand Up @@ -236,47 +236,55 @@ impl InitialReceiveTransition {
}
}

#[derive(uniffi::Object)]
pub struct UninitializedReceiver {}
#[derive(Clone, Debug, uniffi::Object)]
pub struct ReceiverBuilder(payjoin::receive::v2::ReceiverBuilder);

#[uniffi::export]
impl UninitializedReceiver {
#[allow(clippy::new_without_default)]
#[uniffi::constructor]
// TODO: no need for this constructor. `create_session` is the only way to create a receiver.
pub fn new() -> Self { Self {} }

impl ReceiverBuilder {
/// Creates a new [`Initialized`] with the provided parameters.
///
/// # Parameters
/// - `address`: The Bitcoin address for the payjoin session.
/// - `directory`: The URL of the store-and-forward payjoin directory.
/// - `ohttp_keys`: The OHTTP keys used for encrypting and decrypting HTTP requests and responses.
/// - `expire_after`: The duration after which the session expires.
///
/// # Returns
/// A new instance of [`Initialized`].
///
/// # References
/// - [BIP 77: Payjoin Version 2: Serverless Payjoin](https://github.com/bitcoin/bips/blob/master/bip-0077.md)
pub fn create_session(
&self,
#[uniffi::constructor]
pub fn new(
address: Arc<Address>,
directory: String,
ohttp_keys: Arc<OhttpKeys>,
expire_after: Option<u64>,
amount: Option<u64>,
) -> Result<InitialReceiveTransition, IntoUrlError> {
payjoin::receive::v2::Receiver::create_session(
Arc::unwrap_or_clone(address).into(),
directory,
Arc::unwrap_or_clone(ohttp_keys).into(),
expire_after.map(Duration::from_secs),
amount.map(payjoin::bitcoin::Amount::from_sat),
)
.map(|receiver| InitialReceiveTransition(Arc::new(RwLock::new(Some(receiver)))))
.map_err(IntoUrlError::from)
) -> Result<Self, IntoUrlError> {
Ok(Self(
payjoin::receive::v2::ReceiverBuilder::new(
Arc::unwrap_or_clone(address).into(),
directory,
Arc::unwrap_or_clone(ohttp_keys).into(),
)
.map_err(IntoUrlError::from)?,
))
}

pub fn with_amount(&self, amount_sats: u64) -> Self {
Self(self.0.clone().with_amount(Amount::from_sat(amount_sats)))
}

pub fn with_expiry(&self, expiry: u64) -> Self {
Self(self.0.clone().with_expiry(Duration::from_secs(expiry)))
}

pub fn build(&self) -> InitialReceiveTransition {
InitialReceiveTransition(Arc::new(RwLock::new(Some(self.0.clone().build()))))
}
}

impl From<payjoin::receive::v2::ReceiverBuilder> for ReceiverBuilder {
fn from(value: payjoin::receive::v2::ReceiverBuilder) -> Self { Self(value) }
}

impl From<ReceiverBuilder> for payjoin::receive::v2::ReceiverBuilder {
fn from(value: ReceiverBuilder) -> Self { value.0 }
}

#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, uniffi::Object)]
Expand Down
77 changes: 54 additions & 23 deletions payjoin/src/core/receive/v2/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ fn short_id_from_pubkey(pubkey: &HpkePublicKey) -> ShortId {
/// and the state to be updated with the next event over a uniform interface.
#[derive(Debug, Clone, PartialEq)]
pub enum ReceiveSession {
Uninitialized(Receiver<UninitializedReceiver>),
Uninitialized,
Initialized(Receiver<Initialized>),
UncheckedProposal(Receiver<UncheckedProposal>),
MaybeInputsOwned(Receiver<MaybeInputsOwned>),
Expand All @@ -132,7 +132,7 @@ pub enum ReceiveSession {
impl ReceiveSession {
fn process_event(self, event: SessionEvent) -> Result<ReceiveSession, ReplayError> {
match (self, event) {
(ReceiveSession::Uninitialized(_), SessionEvent::Created(context)) =>
(ReceiveSession::Uninitialized, SessionEvent::Created(context)) =>
Ok(ReceiveSession::Initialized(Receiver { state: Initialized { context } })),

(
Expand Down Expand Up @@ -243,14 +243,11 @@ pub fn process_err_res(body: &[u8], context: ohttp::ClientResponse) -> Result<()
process_post_res(body, context).map_err(|e| InternalSessionError::DirectoryResponse(e).into())
}

#[derive(Debug, Clone, PartialEq)]
/// The receiver is not initialized yet, no session context is available yet
pub struct UninitializedReceiver {}

impl State for UninitializedReceiver {}
#[derive(Debug, Clone)]
pub struct ReceiverBuilder(SessionContext);

impl Receiver<UninitializedReceiver> {
/// Creates a new [`Receiver<Initialized>`] with the provided parameters.
impl ReceiverBuilder {
/// Creates a new [`ReceiverBuilder`] with the provided parameters.
///
/// This is the beginning of the receiver protocol in Payjoin v2. It uses the passed address,
/// store-and-forward Payjoin directory URL, and the OHTTP keys to encrypt and decrypt HTTP
Expand All @@ -261,28 +258,42 @@ impl Receiver<UninitializedReceiver> {
///
/// See [BIP 77: Payjoin Version 2: Serverless Payjoin](https://github.com/bitcoin/bips/blob/master/bip-0077.md)
/// for more information on the purpose of each parameter for secure Payjoin v2 functionality.
pub fn create_session(
pub fn new(
address: Address,
directory: impl IntoUrl,
ohttp_keys: OhttpKeys,
expire_after: Option<Duration>,
amount: Option<Amount>,
) -> Result<NextStateTransition<SessionEvent, Receiver<Initialized>>, IntoUrlError> {
) -> Result<Self, IntoUrlError> {
let directory = directory.into_url()?;
let session_context = SessionContext {
address,
directory,
mailbox: None,
ohttp_keys,
expiry: SystemTime::now() + expire_after.unwrap_or(TWENTY_FOUR_HOURS_DEFAULT_EXPIRY),
s: HpkeKeyPair::gen_keypair(),
expiry: SystemTime::now() + TWENTY_FOUR_HOURS_DEFAULT_EXPIRY,
amount: None,
mailbox: None,
e: None,
amount,
};
Ok(NextStateTransition::success(
SessionEvent::Created(session_context.clone()),
Receiver { state: Initialized { context: session_context } },
))
Ok(Self(session_context))
}

pub fn with_expiry(self, expiry: Duration) -> Self {
Self(SessionContext { expiry: SystemTime::now() + expiry, ..self.0 })
}

pub fn with_amount(self, amount: Amount) -> Self {
Self(SessionContext { amount: Some(amount), ..self.0 })
}

pub fn with_mailbox(self, mailbox: impl IntoUrl) -> Result<Self, IntoUrlError> {
Ok(Self(SessionContext { mailbox: Some(mailbox.into_url()?), ..self.0 }))
}

pub fn build(self) -> NextStateTransition<SessionEvent, Receiver<Initialized>> {
NextStateTransition::success(
SessionEvent::Created(self.0.clone()),
Receiver { state: Initialized { context: self.0 } },
)
}
}

Expand Down Expand Up @@ -1346,14 +1357,13 @@ pub mod test {
let now = SystemTime::now();
let noop_persister = NoopSessionPersister::default();

let session = Receiver::create_session(
let session = ReceiverBuilder::new(
SHARED_CONTEXT.address.clone(),
SHARED_CONTEXT.directory.clone(),
SHARED_CONTEXT.ohttp_keys.clone(),
None,
None,
)
.expect("constructor on test vector should not fail")
.build()
.save(&noop_persister)
.expect("Noop persister shouldn't fail");
let session_expiry = session.context.expiry.duration_since(now).unwrap().as_secs();
Expand All @@ -1364,6 +1374,27 @@ pub mod test {
}
}

#[test]
fn build_receiver_with_non_default_expiry() {
let now = SystemTime::now();
let expiry = Duration::from_secs(60);
let noop_persister = NoopSessionPersister::default();
let receiver = ReceiverBuilder::new(
SHARED_CONTEXT.address.clone(),
SHARED_CONTEXT.directory.clone(),
SHARED_CONTEXT.ohttp_keys.clone(),
)
.expect("constructor on test vector should not fail")
.with_expiry(expiry)
.build()
.save(&noop_persister)
.expect("Noop persister shouldn't fail");
assert_eq!(
receiver.context.expiry.duration_since(now).unwrap().as_secs(),
expiry.as_secs()
);
}

#[test]
fn test_v2_pj_uri() {
let uri = Receiver { state: Initialized { context: SHARED_CONTEXT.clone() } }.pj_uri();
Expand Down
7 changes: 4 additions & 3 deletions payjoin/src/core/receive/v2/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use std::time::SystemTime;

use serde::{Deserialize, Serialize};

use super::{ReceiveSession, Receiver, SessionContext, UninitializedReceiver};
use super::{ReceiveSession, SessionContext};
use crate::output_substitution::OutputSubstitution;
use crate::persist::SessionPersister;
use crate::receive::v2::{extract_err_req, SessionError};
Expand Down Expand Up @@ -52,7 +52,7 @@ where
let logs = persister
.load()
.map_err(|e| InternalReplayError::PersistenceFailure(ImplementationError::new(e)))?;
let mut receiver = ReceiveSession::Uninitialized(Receiver { state: UninitializedReceiver {} });
let mut receiver = ReceiveSession::Uninitialized;
let mut history = SessionHistory::default();

for event in logs {
Expand Down Expand Up @@ -182,7 +182,8 @@ mod tests {
use crate::receive::tests::original_from_test_vector;
use crate::receive::v2::test::SHARED_CONTEXT;
use crate::receive::v2::{
Initialized, MaybeInputsOwned, PayjoinProposal, ProvisionalProposal, UncheckedProposal,
Initialized, MaybeInputsOwned, PayjoinProposal, ProvisionalProposal, Receiver,
UncheckedProposal,
};

fn unchecked_receiver_from_test_vector() -> Receiver<UncheckedProposal> {
Expand Down
Loading
Loading