From 77051e337e6923dd3aaa4de890f4e7aea4d5f782 Mon Sep 17 00:00:00 2001 From: Yuval Kogman Date: Sat, 2 Aug 2025 23:44:35 +0200 Subject: [PATCH 1/4] payjoin-cli: handle binding free port in v1 server When binding a socket address with port 0, the OS will allocate a free port. The --port CLI argument supports doing this, but the bound port number was not used in any way. This change checks that if --port 0 is specified, --pj-endpoint does not specify a port, and sets the port to the assigned to the listener by the OS before generating payjoin payment URI. --- payjoin-cli/src/app/config.rs | 10 +++++++++- payjoin-cli/src/app/v1.rs | 35 +++++++++++++++++++++++++---------- 2 files changed, 34 insertions(+), 11 deletions(-) diff --git a/payjoin-cli/src/app/config.rs b/payjoin-cli/src/app/config.rs index 95ad25798..abf449b0e 100644 --- a/payjoin-cli/src/app/config.rs +++ b/payjoin-cli/src/app/config.rs @@ -141,7 +141,15 @@ impl Config { #[cfg(feature = "v1")] { match built_config.get::("v1") { - Ok(v1) => config.version = Some(VersionConfig::V1(v1)), + Ok(v1) => { + if v1.pj_endpoint.port().is_none() != (v1.port == 0) { + return Err(ConfigError::Message( + "If --port is 0, --pj-endpoint may not have a port".to_owned(), + )); + } + + config.version = Some(VersionConfig::V1(v1)) + } Err(e) => return Err(ConfigError::Message(format!( "Valid V1 configuration is required for BIP78 mode: {e}" diff --git a/payjoin-cli/src/app/v1.rs b/payjoin-cli/src/app/v1.rs index 3e4dafda2..87921d6a0 100644 --- a/payjoin-cli/src/app/v1.rs +++ b/payjoin-cli/src/app/v1.rs @@ -99,16 +99,9 @@ impl AppTrait for App { #[allow(clippy::incompatible_msrv)] async fn receive_payjoin(&self, amount: Amount) -> Result<()> { - let pj_uri_string = self.construct_payjoin_uri(amount, None)?; - println!( - "Listening at {}. Configured to accept payjoin at BIP 21 Payjoin Uri:", - self.config.v1()?.port - ); - println!("{}", pj_uri_string); - let mut interrupt = self.interrupt.clone(); tokio::select! { - res = self.start_http_server() => { res?; } + res = self.start_http_server(amount) => { res?; } _ = interrupt.changed() => { println!("Interrupted."); } @@ -146,9 +139,31 @@ impl App { Ok(pj_uri.to_string()) } - async fn start_http_server(&self) -> Result<()> { - let addr = SocketAddr::from(([0, 0, 0, 0], self.config.v1()?.port)); + async fn start_http_server(&self, amount: Amount) -> Result<()> { + let port = self.config.v1()?.port; + let addr = SocketAddr::from(([0, 0, 0, 0], port)); let listener = TcpListener::bind(addr).await?; + + let mut endpoint = self.config.v1()?.pj_endpoint.clone(); + + // If --port 0 is specified, a free port is chosen, so we need to set it + // on the endpoint which must not have a port. + let fallback_endpoint = if port == 0 { + endpoint + .set_port(Some(listener.local_addr()?.port())) + .expect("setting port must succeed"); + Some(endpoint.as_str()) + } else { + None + }; + + let pj_uri_string = self.construct_payjoin_uri(amount, fallback_endpoint)?; + println!( + "Listening at {}. Configured to accept payjoin at BIP 21 Payjoin Uri:", + listener.local_addr()? + ); + println!("{}", pj_uri_string); + let app = self.clone(); #[cfg(feature = "_danger-local-https")] From 03379ad679fa61854a96af302bee33915440e03e Mon Sep 17 00:00:00 2001 From: Yuval Kogman Date: Sat, 2 Aug 2025 23:44:53 +0200 Subject: [PATCH 2/4] e2e test: remove free port binding race condition --- payjoin-cli/tests/e2e.rs | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/payjoin-cli/tests/e2e.rs b/payjoin-cli/tests/e2e.rs index 9001be04d..a57fe18f8 100644 --- a/payjoin-cli/tests/e2e.rs +++ b/payjoin-cli/tests/e2e.rs @@ -25,13 +25,12 @@ mod e2e { let temp_dir = tempdir()?; let receiver_db_path = temp_dir.path().join("receiver_db"); let sender_db_path = temp_dir.path().join("sender_db"); - let port = find_free_port()?; let payjoin_sent = tokio::spawn(async move { let receiver_rpchost = format!("http://{}/wallet/receiver", bitcoind.params.rpc_socket); let sender_rpchost = format!("http://{}/wallet/sender", bitcoind.params.rpc_socket); let cookie_file = &bitcoind.params.cookie_file; - let pj_endpoint = format!("https://localhost:{port}"); + let pj_endpoint = "https://localhost"; let payjoin_cli = env!("CARGO_BIN_EXE_payjoin-cli"); let mut cli_receiver = Command::new(payjoin_cli) @@ -45,9 +44,9 @@ mod e2e { .arg("receive") .arg(RECEIVE_SATS) .arg("--port") - .arg(port.to_string()) + .arg("0") .arg("--pj-endpoint") - .arg(&pj_endpoint) + .arg(pj_endpoint) .stdout(Stdio::piped()) .stderr(Stdio::inherit()) .spawn() @@ -129,11 +128,6 @@ mod e2e { assert!(payjoin_sent, "Payjoin send was not detected"); - fn find_free_port() -> Result { - let listener = std::net::TcpListener::bind("127.0.0.1:0")?; - Ok(listener.local_addr()?.port()) - } - Ok(()) } From e3f1cdcc27a56e939be4435c090d563ad1b06fab Mon Sep 17 00:00:00 2001 From: Yuval Kogman Date: Sat, 2 Aug 2025 23:44:53 +0200 Subject: [PATCH 3/4] payjoin-cli: allow specifying root cert & key This change adds --root-certificate and --certificate-key CLI options, primarily intended for testing. This avoids reading from /tmp/localhost.der as e2e tests previously did, which technically requires `--test-threads 1` to work reliably (in practice this was rarely an issue to to latency in starting the redis test container) --- payjoin-cli/src/app/config.rs | 20 ++++++++ payjoin-cli/src/app/mod.rs | 37 ++++++-------- payjoin-cli/src/app/v1.rs | 34 +++++++------ payjoin-cli/src/app/v2/mod.rs | 89 +++++++++++++++++---------------- payjoin-cli/src/app/v2/ohttp.rs | 22 ++++---- payjoin-cli/src/cli/mod.rs | 8 +++ payjoin-cli/tests/e2e.rs | 38 +++++++++++++- 7 files changed, 156 insertions(+), 92 deletions(-) diff --git a/payjoin-cli/src/app/config.rs b/payjoin-cli/src/app/config.rs index abf449b0e..95e94ecad 100644 --- a/payjoin-cli/src/app/config.rs +++ b/payjoin-cli/src/app/config.rs @@ -56,6 +56,10 @@ pub struct Config { pub bitcoind: BitcoindConfig, #[serde(skip)] pub version: Option, + #[cfg(feature = "_danger-local-https")] + pub root_certificate: Option, + #[cfg(feature = "_danger-local-https")] + pub certificate_key: Option, } impl Config { @@ -134,6 +138,10 @@ impl Config { max_fee_rate: built_config.get("max_fee_rate").ok(), bitcoind: built_config.get("bitcoind")?, version: None, + #[cfg(feature = "_danger-local-https")] + root_certificate: built_config.get("root_certificate").ok(), + #[cfg(feature = "_danger-local-https")] + certificate_key: built_config.get("certificate_key").ok(), }; match version { @@ -274,6 +282,18 @@ fn add_v2_defaults(config: Builder, cli: &Cli) -> Result { /// Handles configuration overrides based on CLI subcommands fn handle_subcommands(config: Builder, cli: &Cli) -> Result { + #[cfg(feature = "_danger-local-https")] + let config = { + config + .set_override_option( + "root_certificate", + Some(cli.root_certificate.as_ref().map(|s| s.to_string_lossy().into_owned())), + )? + .set_override_option( + "certificate_key", + Some(cli.certificate_key.as_ref().map(|s| s.to_string_lossy().into_owned())), + )? + }; match &cli.command { Commands::Send { .. } => Ok(config), Commands::Receive { diff --git a/payjoin-cli/src/app/mod.rs b/payjoin-cli/src/app/mod.rs index ffb56f589..041c99210 100644 --- a/payjoin-cli/src/app/mod.rs +++ b/payjoin-cli/src/app/mod.rs @@ -18,9 +18,6 @@ pub(crate) mod v1; #[cfg(feature = "v2")] pub(crate) mod v2; -#[cfg(feature = "_danger-local-https")] -pub const LOCAL_CERT_FILE: &str = "localhost.der"; - #[async_trait::async_trait] pub trait App: Send + Sync { fn new(config: Config) -> Result @@ -56,29 +53,25 @@ pub trait App: Send + Sync { } #[cfg(feature = "_danger-local-https")] -fn http_agent() -> Result { Ok(http_agent_builder()?.build()?) } +fn http_agent(config: &Config) -> Result { + Ok(http_agent_builder(config.root_certificate.as_ref())?.build()?) +} #[cfg(not(feature = "_danger-local-https"))] -fn http_agent() -> Result { Ok(reqwest::Client::new()) } +fn http_agent(_config: &Config) -> Result { Ok(reqwest::Client::new()) } #[cfg(feature = "_danger-local-https")] -fn http_agent_builder() -> Result { - use rustls::pki_types::CertificateDer; - use rustls::RootCertStore; - - let cert_der = read_local_cert()?; - let mut root_cert_store = RootCertStore::empty(); - root_cert_store.add(CertificateDer::from(cert_der.as_slice()))?; - Ok(reqwest::ClientBuilder::new() - .use_rustls_tls() - .add_root_certificate(reqwest::tls::Certificate::from_der(cert_der.as_slice())?)) -} - -#[cfg(feature = "_danger-local-https")] -fn read_local_cert() -> Result> { - let mut local_cert_path = std::env::temp_dir(); - local_cert_path.push(LOCAL_CERT_FILE); - Ok(std::fs::read(local_cert_path)?) +fn http_agent_builder( + root_cert_path: Option<&std::path::PathBuf>, +) -> Result { + let mut builder = reqwest::ClientBuilder::new().use_rustls_tls(); + + if let Some(root_cert_path) = root_cert_path { + let cert_der = std::fs::read(root_cert_path)?; + builder = + builder.add_root_certificate(reqwest::tls::Certificate::from_der(cert_der.as_slice())?) + } + Ok(builder) } async fn handle_interrupt(tx: watch::Sender<()>) { diff --git a/payjoin-cli/src/app/v1.rs b/payjoin-cli/src/app/v1.rs index 87921d6a0..bb61ae05c 100644 --- a/payjoin-cli/src/app/v1.rs +++ b/payjoin-cli/src/app/v1.rs @@ -26,8 +26,6 @@ use super::wallet::BitcoindWallet; use super::App as AppTrait; use crate::app::{handle_interrupt, http_agent}; use crate::db::Database; -#[cfg(feature = "_danger-local-https")] -pub const LOCAL_CERT_FILE: &str = "localhost.der"; struct Headers<'a>(&'a hyper::HeaderMap); impl payjoin::receive::v1::Headers for Headers<'_> { @@ -70,7 +68,7 @@ impl AppTrait for App { .build_recommended(fee_rate) .with_context(|| "Failed to build payjoin request")? .create_v1_post_request(); - let http = http_agent()?; + let http = http_agent(&self.config)?; let body = String::from_utf8(req.body.clone()).unwrap(); println!("Sending fallback request to {}", &req.url); let response = http @@ -167,7 +165,7 @@ impl App { let app = self.clone(); #[cfg(feature = "_danger-local-https")] - let tls_acceptor = Self::init_tls_acceptor()?; + let tls_acceptor = self.init_tls_acceptor()?; while let Ok((stream, _)) = listener.accept().await { let app = app.clone(); #[cfg(feature = "_danger-local-https")] @@ -194,28 +192,36 @@ impl App { } #[cfg(feature = "_danger-local-https")] - fn init_tls_acceptor() -> Result { - use std::io::Write; - + fn init_tls_acceptor(&self) -> Result { use rustls::pki_types::{CertificateDer, PrivateKeyDer}; use rustls::ServerConfig; use tokio_rustls::TlsAcceptor; - let cert = rcgen::generate_simple_self_signed(vec!["localhost".to_string()])?; - let cert_der = cert.serialize_der()?; - let mut local_cert_path = std::env::temp_dir(); - local_cert_path.push(LOCAL_CERT_FILE); - let mut file = std::fs::File::create(local_cert_path)?; - file.write_all(&cert_der)?; - let key = PrivateKeyDer::try_from(cert.serialize_private_key_der()) + let key_der = std::fs::read( + self.config + .certificate_key + .as_ref() + .expect("certificate key is required if listening with tls"), + )?; + let key = PrivateKeyDer::try_from(key_der.clone()) .map_err(|e| anyhow::anyhow!("Could not parse key: {}", e))?; + + let cert_der = std::fs::read( + self.config + .root_certificate + .as_ref() + .expect("certificate key is required if listening with tls"), + )?; let certs = vec![CertificateDer::from(cert_der)]; + let mut server_config = ServerConfig::builder() .with_no_client_auth() .with_single_cert(certs, key) .map_err(|e| anyhow::anyhow!("TLS error: {}", e))?; + server_config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec(), b"http/1.0".to_vec()]; + Ok(TlsAcceptor::from(Arc::new(server_config))) } diff --git a/payjoin-cli/src/app/v2/mod.rs b/payjoin-cli/src/app/v2/mod.rs index 49e04eb04..2213c7e76 100644 --- a/payjoin-cli/src/app/v2/mod.rs +++ b/payjoin-cli/src/app/v2/mod.rs @@ -183,7 +183,7 @@ impl App { Ok(()) => (), Err(_) => { let (req, v1_ctx) = context.create_v1_post_request(); - let response = post_request(req).await?; + let response = self.post_request(req).await?; let psbt = Arc::new( v1_ctx.process_response(response.bytes().await?.to_vec().as_slice())?, ); @@ -211,7 +211,7 @@ impl App { let (req, ctx) = sender.create_v2_post_request( self.unwrap_relay_or_else_fetch(Some(sender.endpoint().clone())).await?, )?; - let response = post_request(req).await?; + let response = self.post_request(req).await?; println!("Posted original proposal..."); let sender = sender.process_response(&response.bytes().await?, ctx).save(persister)?; self.get_proposed_payjoin_psbt(sender, persister).await @@ -228,7 +228,7 @@ impl App { let (req, ctx) = session.create_poll_request( self.unwrap_relay_or_else_fetch(Some(session.endpoint().clone())).await?, )?; - let response = post_request(req).await?; + let response = self.post_request(req).await?; let res = session.process_response(&response.bytes().await?, ctx).save(persister); match res { Ok(OptionalTransitionOutcome::Progress(psbt)) => { @@ -263,7 +263,7 @@ impl App { loop { let (req, context) = session.create_poll_request(&ohttp_relay)?; println!("Polling receive request..."); - let ohttp_response = post_request(req).await?; + let ohttp_response = self.post_request(req).await?; let state_transition = session .process_response(ohttp_response.bytes().await?.to_vec().as_slice(), context) .save(persister); @@ -324,7 +324,7 @@ impl App { None => None, }; let ohttp_relay = self.unwrap_relay_or_else_fetch(pj_uri).await?; - handle_recoverable_error(&ohttp_relay, &session_history).await?; + self.handle_recoverable_error(&ohttp_relay, &session_history).await?; Err(e) } @@ -468,7 +468,7 @@ impl App { let (req, ohttp_ctx) = proposal .create_post_request(&self.unwrap_relay_or_else_fetch(None).await?) .map_err(|e| anyhow!("v2 req extraction failed {}", e))?; - let res = post_request(req).await?; + let res = self.post_request(req).await?; let payjoin_psbt = proposal.psbt().clone(); proposal.process_response(&res.bytes().await?, ohttp_ctx).save(persister)?; println!( @@ -493,47 +493,48 @@ impl App { }; Ok(ohttp_relay) } -} -/// Handle request error by sending an error response over the directory -async fn handle_recoverable_error( - ohttp_relay: &payjoin::Url, - session_history: &SessionHistory, -) -> Result<()> { - let e = match session_history.terminal_error() { - Some((_, Some(e))) => e, - _ => return Ok(()), - }; - let (err_req, err_ctx) = session_history - .extract_err_req(ohttp_relay)? - .expect("If JsonReply is Some, then err_req and err_ctx should be Some"); - let to_return = anyhow!("Replied with error: {}", e.to_json().to_string()); - - let err_response = match post_request(err_req).await { - Ok(response) => response, - Err(e) => return Err(anyhow!("Failed to post error request: {}", e)), - }; - - let err_bytes = match err_response.bytes().await { - Ok(bytes) => bytes, - Err(e) => return Err(anyhow!("Failed to get error response bytes: {}", e)), - }; - - if let Err(e) = process_err_res(&err_bytes, err_ctx) { - return Err(anyhow!("Failed to process error response: {}", e)); - } + /// Handle request error by sending an error response over the directory + async fn handle_recoverable_error( + &self, + ohttp_relay: &payjoin::Url, + session_history: &SessionHistory, + ) -> Result<()> { + let e = match session_history.terminal_error() { + Some((_, Some(e))) => e, + _ => return Ok(()), + }; + let (err_req, err_ctx) = session_history + .extract_err_req(ohttp_relay)? + .expect("If JsonReply is Some, then err_req and err_ctx should be Some"); + let to_return = anyhow!("Replied with error: {}", e.to_json().to_string()); + + let err_response = match self.post_request(err_req).await { + Ok(response) => response, + Err(e) => return Err(anyhow!("Failed to post error request: {}", e)), + }; - Err(to_return) -} + let err_bytes = match err_response.bytes().await { + Ok(bytes) => bytes, + Err(e) => return Err(anyhow!("Failed to get error response bytes: {}", e)), + }; + + if let Err(e) = process_err_res(&err_bytes, err_ctx) { + return Err(anyhow!("Failed to process error response: {}", e)); + } -async fn post_request(req: payjoin::Request) -> Result { - let http = http_agent()?; - http.post(req.url) - .header("Content-Type", req.content_type) - .body(req.body) - .send() - .await - .map_err(map_reqwest_err) + Err(to_return) + } + + async fn post_request(&self, req: payjoin::Request) -> Result { + let http = http_agent(&self.config)?; + http.post(req.url) + .header("Content-Type", req.content_type) + .body(req.body) + .send() + .await + .map_err(map_reqwest_err) + } } fn map_reqwest_err(e: reqwest::Error) -> anyhow::Error { diff --git a/payjoin-cli/src/app/v2/ohttp.rs b/payjoin-cli/src/app/v2/ohttp.rs index 762867a2e..7173e92c6 100644 --- a/payjoin-cli/src/app/v2/ohttp.rs +++ b/payjoin-cli/src/app/v2/ohttp.rs @@ -80,18 +80,20 @@ async fn fetch_ohttp_keys( let ohttp_keys = { #[cfg(feature = "_danger-local-https")] { - let cert_der = crate::app::read_local_cert()?; - payjoin::io::fetch_ohttp_keys_with_cert( - &selected_relay, - &payjoin_directory, - cert_der, - ) - .await + if let Some(cert_path) = config.root_certificate.as_ref() { + let cert_der = std::fs::read(cert_path)?; + payjoin::io::fetch_ohttp_keys_with_cert( + &selected_relay, + &payjoin_directory, + cert_der, + ) + .await + } else { + payjoin::io::fetch_ohttp_keys(&selected_relay, &payjoin_directory).await + } } #[cfg(not(feature = "_danger-local-https"))] - { - payjoin::io::fetch_ohttp_keys(&selected_relay, &payjoin_directory).await - } + payjoin::io::fetch_ohttp_keys(&selected_relay, &payjoin_directory).await }; match ohttp_keys { diff --git a/payjoin-cli/src/cli/mod.rs b/payjoin-cli/src/cli/mod.rs index 2ace562c6..1e6206576 100644 --- a/payjoin-cli/src/cli/mod.rs +++ b/payjoin-cli/src/cli/mod.rs @@ -75,6 +75,14 @@ pub struct Cli { #[cfg(feature = "v2")] #[arg(long = "pj-directory", help = "The directory to store payjoin requests", value_parser = value_parser!(Url))] pub pj_directory: Option, + + #[cfg(feature = "_danger-local-https")] + #[arg(long = "root-certificate", help = "Specify a TLS certificate to be added as a root", value_parser = value_parser!(PathBuf))] + pub root_certificate: Option, + + #[cfg(feature = "_danger-local-https")] + #[arg(long = "certificate-key", help = "Specify the certificate private key", value_parser = value_parser!(PathBuf))] + pub certificate_key: Option, } #[derive(Subcommand, Debug)] diff --git a/payjoin-cli/tests/e2e.rs b/payjoin-cli/tests/e2e.rs index a57fe18f8..bfbb63909 100644 --- a/payjoin-cli/tests/e2e.rs +++ b/payjoin-cli/tests/e2e.rs @@ -21,6 +21,8 @@ mod e2e { #[cfg(feature = "v1")] #[tokio::test(flavor = "multi_thread", worker_threads = 4)] async fn send_receive_payjoin_v1() -> Result<(), BoxError> { + use payjoin_test_utils::local_cert_key; + let (bitcoind, _sender, _receiver) = init_bitcoind_sender_receiver(None, None)?; let temp_dir = tempdir()?; let receiver_db_path = temp_dir.path().join("receiver_db"); @@ -33,7 +35,25 @@ mod e2e { let pj_endpoint = "https://localhost"; let payjoin_cli = env!("CARGO_BIN_EXE_payjoin-cli"); + let cert = local_cert_key(); + let cert_path = &temp_dir.path().join("localhost.crt"); + tokio::fs::write( + cert_path, + cert.serialize_der().expect("must be able to serialize self signed certificate"), + ) + .await + .expect("must be able to write self signed certificate"); + + let key_path = &temp_dir.path().join("localhost.key"); + tokio::fs::write(key_path, cert.serialize_private_key_der()) + .await + .expect("must be able to write self signed certificate"); + let mut cli_receiver = Command::new(payjoin_cli) + .arg("--root-certificate") + .arg(cert_path) + .arg("--certificate-key") + .arg(key_path) .arg("--bip78") .arg("--rpchost") .arg(&receiver_rpchost) @@ -76,6 +96,8 @@ mod e2e { log::debug!("Got bip21 {}", &bip21); let mut cli_sender = Command::new(payjoin_cli) + .arg("--root-certificate") + .arg(cert_path) .arg("--bip78") .arg("--rpchost") .arg(&sender_rpchost) @@ -156,8 +178,8 @@ mod e2e { let receiver_db_path = temp_dir.path().join("receiver_db"); let sender_db_path = temp_dir.path().join("sender_db"); let (bitcoind, _sender, _receiver) = init_bitcoind_sender_receiver(None, None)?; - let cert_path = std::env::temp_dir().join("localhost.der"); - tokio::fs::write(&cert_path, services.cert()).await?; + let cert_path = &temp_dir.path().join("localhost.der"); + tokio::fs::write(cert_path, services.cert()).await?; services.wait_for_services_ready().await?; let ohttp_keys = services.fetch_ohttp_keys().await?; let ohttp_keys_path = temp_dir.path().join("ohttp_keys"); @@ -173,6 +195,8 @@ mod e2e { let ohttp_relay = &services.ohttp_relay_url().to_string(); let cli_receive_initiator = Command::new(payjoin_cli) + .arg("--root-certificate") + .arg(cert_path) .arg("--rpchost") .arg(&receiver_rpchost) .arg("--cookie-file") @@ -193,6 +217,8 @@ mod e2e { .expect("Failed to execute payjoin-cli"); let bip21 = get_bip21_from_receiver(cli_receive_initiator).await; let cli_send_initiator = Command::new(payjoin_cli) + .arg("--root-certificate") + .arg(cert_path) .arg("--rpchost") .arg(&sender_rpchost) .arg("--cookie-file") @@ -212,6 +238,8 @@ mod e2e { send_until_request_timeout(cli_send_initiator).await?; let cli_receive_resumer = Command::new(payjoin_cli) + .arg("--root-certificate") + .arg(cert_path) .arg("--rpchost") .arg(&receiver_rpchost) .arg("--cookie-file") @@ -228,6 +256,8 @@ mod e2e { respond_with_payjoin(cli_receive_resumer).await?; let cli_send_resumer = Command::new(payjoin_cli) + .arg("--root-certificate") + .arg(cert_path) .arg("--rpchost") .arg(&sender_rpchost) .arg("--cookie-file") @@ -248,6 +278,8 @@ mod e2e { // Check that neither the sender or the receiver have sessions to resume let cli_receive_resumer = Command::new(payjoin_cli) + .arg("--root-certificate") + .arg(cert_path) .arg("--rpchost") .arg(&receiver_rpchost) .arg("--cookie-file") @@ -263,6 +295,8 @@ mod e2e { .expect("Failed to execute payjoin-cli"); check_resume_has_no_sessions(cli_receive_resumer).await?; let cli_send_resumer = Command::new(payjoin_cli) + .arg("--root-certificate") + .arg(cert_path) .arg("--rpchost") .arg(&sender_rpchost) .arg("--cookie-file") From 666b8167fc0a3255de34090eaab34bcf9d4eb43c Mon Sep 17 00:00:00 2001 From: Yuval Kogman Date: Mon, 4 Aug 2025 16:17:25 +0200 Subject: [PATCH 4/4] payjoin-cli: simplify construct_payjoin_uri args Co-authored-by: spacebear --- payjoin-cli/src/app/v1.rs | 25 ++++++------------------- 1 file changed, 6 insertions(+), 19 deletions(-) diff --git a/payjoin-cli/src/app/v1.rs b/payjoin-cli/src/app/v1.rs index bb61ae05c..9e6ccccbb 100644 --- a/payjoin-cli/src/app/v1.rs +++ b/payjoin-cli/src/app/v1.rs @@ -17,7 +17,7 @@ use payjoin::bitcoin::FeeRate; use payjoin::receive::v1::{PayjoinProposal, UncheckedProposal}; use payjoin::receive::ReplyableError::{self, Implementation, V1}; use payjoin::send::v1::SenderBuilder; -use payjoin::{ImplementationError, Uri, UriExt}; +use payjoin::{ImplementationError, IntoUrl, Uri, UriExt}; use tokio::net::TcpListener; use tokio::sync::watch; @@ -114,22 +114,12 @@ impl AppTrait for App { } impl App { - fn construct_payjoin_uri( - &self, - amount: Amount, - fallback_target: Option<&str>, - ) -> Result { + fn construct_payjoin_uri(&self, amount: Amount, endpoint: impl IntoUrl) -> Result { let pj_receiver_address = self.wallet.get_new_address()?; - let pj_part = match fallback_target { - Some(target) => target, - None => self.config.v1()?.pj_endpoint.as_str(), - }; - let pj_part = payjoin::Url::parse(pj_part) - .map_err(|e| anyhow!("Failed to parse pj_endpoint: {}", e))?; let mut pj_uri = payjoin::receive::v1::build_v1_pj_uri( &pj_receiver_address, - &pj_part, + endpoint, payjoin::OutputSubstitution::Enabled, )?; pj_uri.amount = Some(amount); @@ -146,16 +136,13 @@ impl App { // If --port 0 is specified, a free port is chosen, so we need to set it // on the endpoint which must not have a port. - let fallback_endpoint = if port == 0 { + if port == 0 { endpoint .set_port(Some(listener.local_addr()?.port())) .expect("setting port must succeed"); - Some(endpoint.as_str()) - } else { - None - }; + } - let pj_uri_string = self.construct_payjoin_uri(amount, fallback_endpoint)?; + let pj_uri_string = self.construct_payjoin_uri(amount, endpoint)?; println!( "Listening at {}. Configured to accept payjoin at BIP 21 Payjoin Uri:", listener.local_addr()?