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
29 changes: 19 additions & 10 deletions payjoin-cli/src/app/v2/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -526,10 +526,8 @@ impl AppTrait for App {
};
}

let send_ids = self.db.get_send_session_ids()?;
let recv_ids = self.db.get_recv_session_ids()?;
let is_sender = send_ids.iter().any(|id| id.0 == session_id.0);
let is_receiver = recv_ids.iter().any(|id| id.0 == session_id.0);
let is_sender = self.db.send_session_exists(&session_id)?;
let is_receiver = self.db.recv_session_exists(&session_id)?;

match (is_sender, is_receiver) {
(true, false) => self.cancel_sender_session(session_id, no_broadcast),
Expand All @@ -546,7 +544,7 @@ impl AppTrait for App {
impl App {
fn cancel_sender_session(&self, session_id: SessionId, no_broadcast: bool) -> Result<()> {
let persister = SenderPersister::from_id(self.db.clone(), session_id.clone());
let (session, _history) = replay_sender_event_log(&persister)?;
let (session, history) = replay_sender_event_log(&persister)?;

let pending: Sender<SenderPendingFallback> = match session {
SendSession::WithReplyKey(sender) => sender.cancel().save(&persister)?,
Expand All @@ -560,8 +558,11 @@ impl App {
);
return Ok(());
}
SendSession::Closed(_) => {
println!("Session {session_id} is already closed. Nothing left to do.");
SendSession::Closed(SenderSessionOutcome::Aborted) => {
println!(
"Session {session_id} was already cancelled. Broadcast the original transaction manually:\n{}",
serialize_hex(&history.fallback_tx())
);
return Ok(());
}
};
Expand All @@ -584,7 +585,7 @@ impl App {

fn cancel_receiver_session(&self, session_id: SessionId, no_broadcast: bool) -> Result<()> {
let persister = ReceiverPersister::from_id(self.db.clone(), session_id.clone());
let (session, _history) = replay_receiver_event_log(&persister)?;
let (session, history) = replay_receiver_event_log(&persister)?;

let pending: Receiver<ReceiverPendingFallback> = match session {
ReceiveSession::Initialized(receiver) => {
Expand Down Expand Up @@ -625,8 +626,16 @@ impl App {
println!("Session {session_id} already completed successfully. Cannot cancel.");
return Ok(());
}
ReceiveSession::Closed(_) => {
println!("Session {session_id} is already closed. Nothing left to do.");
ReceiveSession::Closed(ReceiverSessionOutcome::Aborted) => {
match history.fallback_tx() {
Some(tx) => println!(
"Session {session_id} was already cancelled. Broadcast the fallback transaction manually:\n{}",
serialize_hex(&tx)
),
None => println!(
"Session {session_id} is already closed. No fallback transaction available."
),
}
return Ok(());
}
};
Expand Down
22 changes: 22 additions & 0 deletions payjoin-cli/src/db/v2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,28 @@ impl Database {
}
Ok(session_ids)
}

/// Look up a sender session by ID regardless of active/inactive state.
pub(crate) fn send_session_exists(&self, session_id: &SessionId) -> Result<bool> {
let conn = self.get_connection()?;
let exists: bool = conn.query_row(
"SELECT EXISTS(SELECT 1 FROM send_sessions WHERE session_id = ?1)",
params![session_id.0],
|row| row.get(0),
)?;
Ok(exists)
}

/// Look up a receiver session by ID regardless of active/inactive state.
pub(crate) fn recv_session_exists(&self, session_id: &SessionId) -> Result<bool> {
let conn = self.get_connection()?;
let exists: bool = conn.query_row(
"SELECT EXISTS(SELECT 1 FROM receive_sessions WHERE session_id = ?1)",
params![session_id.0],
|row| row.get(0),
)?;
Ok(exists)
}
Comment on lines +294 to +314

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: find_* reads like it returns the session (find usually yields Option<T>), but this returns bool. wdyt? would *_exists match the return type better?

}

#[cfg(all(test, feature = "v2"))]
Expand Down
107 changes: 105 additions & 2 deletions payjoin-cli/tests/e2e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,26 @@ mod e2e {
res
}

/// Read all lines from `child_stdout` until EOF and return them joined by newlines.
/// Also writes every read line to tokio::io::stdout();
async fn read_all_stdout(child_stdout: &mut tokio::process::ChildStdout) -> String {
let reader = BufReader::new(child_stdout);
let mut lines = reader.lines();
let mut out = String::new();

let mut stdout = tokio::io::stdout();
while let Some(line) = lines.next_line().await.expect("Failed to read line from stdout") {
stdout
.write_all(format!("{line}\n").as_bytes())
.await
.expect("Failed to write to stdout");
out.push_str(&line);
out.push('\n');
}

out
}

async fn send_until_request_timeout(mut cli_sender: Child) -> Result<(), BoxError> {
let mut stdout = cli_sender.stdout.take().expect("failed to take stdout of child process");
let timeout = tokio::time::Duration::from_secs(35);
Expand Down Expand Up @@ -783,6 +803,49 @@ mod e2e {
"fallback tx should be in the mempool after cancel"
);

// Re-run `cancel` on the now-closed session: the command must still
// recognize the session exists and report it is already closed.
let mut cli_cancel_again = Command::new(payjoin_cli)
.arg("--root-certificate")
.arg(cert_path)
.arg("--rpchost")
.arg(&sender_rpchost)
.arg("--cookie-file")
.arg(cookie_file)
.arg("--db-path")
.arg(&sender_db_path)
.arg("--ohttp-relays")
.arg(ohttp_relays)
.arg("cancel")
.arg(session_id.to_string())
.stdout(Stdio::piped())
.stderr(Stdio::inherit())
.spawn()
.expect("Failed to execute payjoin-cli cancel on closed session");

let raw_tx = sender.get_transaction(fallback_txid)?.into_model()?.tx;
let expected_hex = payjoin::bitcoin::consensus::encode::serialize_hex(&raw_tx);

let mut cancel_again_stdout = cli_cancel_again
.stdout
.take()
.expect("failed to take stdout of cancel on closed session");
let cancel_again_output =
tokio::time::timeout(timeout, read_all_stdout(&mut cancel_again_stdout)).await?;
terminate(cli_cancel_again)
.await
.expect("Failed to kill payjoin-cli cancel on closed session");

assert!(
cancel_again_output
.contains(&format!("Session {session_id} was already cancelled")),
"cancel on closed session should reference the session id and report it is already closed; got: {cancel_again_output}"
);
assert!(
cancel_again_output.contains(&expected_hex),
"cancel on closed session should print the fallback transaction consensus hex; got: {cancel_again_output}"
);

Ok(())
}

Expand Down Expand Up @@ -868,8 +931,6 @@ mod e2e {
.arg(ohttp_relays)
.arg("cancel")
.arg(session_id.to_string())
.arg("--role")
.arg("receiver")
.stdout(Stdio::piped())
.stderr(Stdio::inherit())
.spawn()
Expand All @@ -894,6 +955,48 @@ mod e2e {
"cancel should reference the cancelled session id"
);

// Re-run `cancel` on the now-closed session: the command must still
// recognize the session exists and report it is already closed.
let mut cli_cancel_again = Command::new(payjoin_cli)
.arg("--root-certificate")
.arg(cert_path)
.arg("--rpchost")
.arg(&receiver_rpchost)
.arg("--cookie-file")
.arg(cookie_file)
.arg("--db-path")
.arg(&receiver_db_path)
.arg("--ohttp-relays")
.arg(ohttp_relays)
.arg("cancel")
.arg(session_id.to_string())
.stdout(Stdio::piped())
.stderr(Stdio::inherit())
.spawn()
.expect("Failed to execute payjoin-cli cancel on closed session");

let mut cancel_again_stdout = cli_cancel_again
.stdout
.take()
.expect("failed to take stdout of cancel on closed session");
let cancel_again_line = tokio::time::timeout(
timeout,
wait_for_stdout_match(&mut cancel_again_stdout, |l| {
l.contains("is already closed")
}),
)
.await?;
terminate(cli_cancel_again)
.await
.expect("Failed to kill payjoin-cli cancel on closed session");
let cancel_again_output = cancel_again_line
.expect("cancel on closed session should report it is already closed");

assert!(
cancel_again_output.contains(&format!("Session {session_id} is already closed")),
Comment thread
spacebear21 marked this conversation as resolved.
"cancel on closed session should reference the session id and report it is already closed"
);

Ok(())
}

Expand Down
Loading