From d63e9f11d0c25775d20b20c221bf849f3e32cac0 Mon Sep 17 00:00:00 2001 From: xstoicunicornx Date: Fri, 10 Jul 2026 14:07:33 -0700 Subject: [PATCH] Consume self in Monitor check_for_transaction Take the receiver by value instead of by reference so the state machine no longer needs internal clones to construct transient and stasis transitions. Callers get the current state back through Stasis outcomes or transient errors, matching the pattern used by the other receiver transitions. Update the CLI monitoring loop to rebind the state from Stasis and transient-error outcomes instead of reusing a borrowed receiver, and clone the monitor in the typestate unit test where it is exercised multiple times. --- payjoin-cli/src/app/v2/mod.rs | 15 ++++++++++++--- payjoin/src/core/receive/v2/mod.rs | 18 +++++++----------- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/payjoin-cli/src/app/v2/mod.rs b/payjoin-cli/src/app/v2/mod.rs index b1babe65c..05d0e3f22 100644 --- a/payjoin-cli/src/app/v2/mod.rs +++ b/payjoin-cli/src/app/v2/mod.rs @@ -999,7 +999,7 @@ impl App { async fn monitor_payjoin_proposal( &self, - proposal: Receiver, + mut proposal: Receiver, persister: &ReceiverPersister, ) -> Result<()> { // On a session resumption, the receiver will resume again in this state. @@ -1027,8 +1027,17 @@ impl App { println!("Payjoin transaction detected in the mempool!"); return Ok(()); } - Ok(OptionalTransitionOutcome::Stasis(_)) => continue, - Err(_) => continue, + Ok(OptionalTransitionOutcome::Stasis(current_state)) => { + proposal = current_state; + } + Err(e) if e.is_transient() => { + tracing::debug!( + "Transient error checking for transaction, retrying: {e:?}" + ); + proposal = + e.transient_state().expect("transient error carries current state"); + } + Err(e) => return Err(e.into()), } } }) diff --git a/payjoin/src/core/receive/v2/mod.rs b/payjoin/src/core/receive/v2/mod.rs index 1bec60330..fa02402f7 100644 --- a/payjoin/src/core/receive/v2/mod.rs +++ b/payjoin/src/core/receive/v2/mod.rs @@ -1536,7 +1536,7 @@ impl Receiver { /// search for the transaction in the network. Since a non-SegWit input signature is going to /// change the TXID of the Payjoin proposal, it cannot be monitored. pub fn check_for_transaction( - &self, + self, find_transaction: impl Fn(Txid) -> Result, ImplementationError>, ) -> MaybeFatalOrSuccessTransition { let fallback_tx = self.state.fallback_tx(); @@ -1563,7 +1563,7 @@ impl Receiver { Error::Implementation(ImplementationError::from( format!("Payjoin transaction ID mismatch. Expected: {payjoin_txid}, Got: {tx_id}").as_str(), )), - self.clone(), + self, ); } // TODO: should we check for witness and scriptsig on the tx? @@ -1581,10 +1581,7 @@ impl Receiver { } Ok(None) => {} Err(e) => - return MaybeFatalOrSuccessTransition::transient( - Error::Implementation(e), - self.clone(), - ), + return MaybeFatalOrSuccessTransition::transient(Error::Implementation(e), self), } // If the Payjoin proposal was not found, check the fallback transaction, as it is @@ -1596,13 +1593,10 @@ impl Receiver { )), Ok(None) => {} Err(e) => - return MaybeFatalOrSuccessTransition::transient( - Error::Implementation(e), - self.clone(), - ), + return MaybeFatalOrSuccessTransition::transient(Error::Implementation(e), self), } - MaybeFatalOrSuccessTransition::no_results(self.clone()) + MaybeFatalOrSuccessTransition::no_results(self) } } @@ -1759,6 +1753,7 @@ pub mod test { // Nothing was spent, should be in the same state let persister = InMemoryPersister::default(); let res = monitor + .clone() .check_for_transaction(|_| Ok(None)) .save(&persister) .expect("InMemoryPersister shouldn't fail"); @@ -1769,6 +1764,7 @@ pub mod test { // Payjoin was broadcasted, should progress to success let persister = InMemoryPersister::default(); let res = monitor + .clone() .check_for_transaction(|_| Ok(Some(payjoin_tx.clone()))) .save(&persister) .expect("InMemoryPersister shouldn't fail");