diff --git a/Cargo.toml b/Cargo.toml index 415c1decc..c60a21e54 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -85,6 +85,8 @@ electrum-client = { version = "0.15.1", default-features = true } bitcoincore-rpc = { version = "0.17.0", default-features = false } proptest = "1.0.0" regex = "1.5.6" +payjoin = { version = "0.16.0", default-features = false, features = ["send", "v2", "receive"] } +reqwest = { version = "0.11", default-features = false, features = ["json", "rustls-tls", "blocking"] } [target.'cfg(not(no_download))'.dev-dependencies] electrsd = { version = "0.26.0", features = ["legacy", "esplora_a33e97e1", "bitcoind_25_0"] } diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index aedf9f6ab..d25547f79 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -64,6 +64,7 @@ interface Node { SpontaneousPayment spontaneous_payment(); OnchainPayment onchain_payment(); UnifiedQrPayment unified_qr_payment(); + PayjoinPayment payjoin_payment(); [Throws=NodeError] void connect(PublicKey node_id, SocketAddress address, boolean persist); [Throws=NodeError] @@ -156,6 +157,13 @@ interface UnifiedQrPayment { QrPaymentResult send([ByRef]string uri_str); }; +interface PayjoinPayment { + [Throws=NodeError] + void send(string payjoin_uri); + [Throws=NodeError] + void send_with_amount(string payjoin_uri, u64 amount_sats); +}; + [Error] enum NodeError { "AlreadyRunning", @@ -206,6 +214,12 @@ enum NodeError { "InsufficientFunds", "LiquiditySourceUnavailable", "LiquidityFeeTooHigh", + "PayjoinUnavailable", + "PayjoinUriInvalid", + "PayjoinRequestMissingAmount", + "PayjoinRequestCreationFailed", + "PayjoinRequestSendingFailed", + "PayjoinResponseProcessingFailed", }; dictionary NodeStatus { @@ -231,6 +245,7 @@ enum BuildError { "InvalidSystemTime", "InvalidChannelMonitor", "InvalidListeningAddresses", + "InvalidPayjoinConfig", "ReadFailed", "WriteFailed", "StoragePathAccessFailed", @@ -248,6 +263,11 @@ interface Event { ChannelPending(ChannelId channel_id, UserChannelId user_channel_id, ChannelId former_temporary_channel_id, PublicKey counterparty_node_id, OutPoint funding_txo); ChannelReady(ChannelId channel_id, UserChannelId user_channel_id, PublicKey? counterparty_node_id); ChannelClosed(ChannelId channel_id, UserChannelId user_channel_id, PublicKey? counterparty_node_id, ClosureReason? reason); + PayjoinPaymentPending(Txid txid, u64 amount_sats); + PayjoinPaymentBroadcasted(Txid txid, u64 amount_sats); + PayjoinPaymentSuccessful(Txid txid, u64 amount_sats, boolean is_original_psbt_modified); + PayjoinPaymentFailed(Txid txid, u64 amount_sats, PayjoinPaymentFailureReason reason); + PayjoinPaymentOriginalPsbtBroadcasted(Txid txid, u64 amount_sats); }; enum PaymentFailureReason { @@ -259,6 +279,12 @@ enum PaymentFailureReason { "UnexpectedError", }; +enum PayjoinPaymentFailureReason { + "Timeout", + "RequestSendingFailed", + "ResponseProcessingFailed", +}; + [Enum] interface ClosureReason { CounterpartyForceClosed(UntrustedString peer_msg); @@ -284,6 +310,7 @@ interface PaymentKind { Bolt12Offer(PaymentHash? hash, PaymentPreimage? preimage, PaymentSecret? secret, OfferId offer_id); Bolt12Refund(PaymentHash? hash, PaymentPreimage? preimage, PaymentSecret? secret); Spontaneous(PaymentHash hash, PaymentPreimage? preimage); + Payjoin(); }; [Enum] @@ -316,6 +343,8 @@ dictionary PaymentDetails { PaymentDirection direction; PaymentStatus status; u64 latest_update_timestamp; + Txid? txid; + BestBlock? best_block; }; [NonExhaustive] diff --git a/src/builder.rs b/src/builder.rs index 8170e2697..a7df44aca 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -17,6 +17,7 @@ use crate::types::{ MessageRouter, OnionMessenger, PaymentStore, PeerManager, }; use crate::wallet::Wallet; +use crate::PayjoinHandler; use crate::{LogLevel, Node}; use lightning::chain::{chainmonitor, BestBlock, Watch}; @@ -994,6 +995,17 @@ fn build_with_store_internal( let (stop_sender, _) = tokio::sync::watch::channel(()); let (event_handling_stopped_sender, _) = tokio::sync::watch::channel(()); + let payjoin_handler = payjoin_config.map(|pj_config| { + Arc::new(PayjoinHandler::new( + Arc::clone(&tx_sync), + Arc::clone(&event_queue), + Arc::clone(&logger), + pj_config.payjoin_relay.clone(), + Arc::clone(&payment_store), + Arc::clone(&wallet), + )) + }); + let is_listening = Arc::new(AtomicBool::new(false)); let latest_wallet_sync_timestamp = Arc::new(RwLock::new(None)); let latest_onchain_wallet_sync_timestamp = Arc::new(RwLock::new(None)); @@ -1015,6 +1027,7 @@ fn build_with_store_internal( channel_manager, chain_monitor, output_sweeper, + payjoin_handler, peer_manager, connection_manager, keys_manager, diff --git a/src/lib.rs b/src/lib.rs index ea880bc1d..c8bbe706b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -108,7 +108,10 @@ pub use config::{default_config, AnchorChannelsConfig, Config}; pub use error::Error as NodeError; use error::Error; +#[cfg(feature = "uniffi")] +use crate::event::PayjoinPaymentFailureReason; pub use event::Event; +use payment::payjoin::handler::PayjoinHandler; pub use types::ChannelConfig; pub use io::utils::generate_entropy_mnemonic; @@ -133,8 +136,8 @@ use gossip::GossipSource; use graph::NetworkGraph; use liquidity::LiquiditySource; use payment::{ - Bolt11Payment, Bolt12Payment, OnchainPayment, PaymentDetails, SpontaneousPayment, - UnifiedQrPayment, + Bolt11Payment, Bolt12Payment, OnchainPayment, PayjoinPayment, PaymentDetails, + SpontaneousPayment, UnifiedQrPayment, }; use peer_store::{PeerInfo, PeerStore}; use types::{ @@ -187,6 +190,7 @@ pub struct Node { output_sweeper: Arc, peer_manager: Arc, connection_manager: Arc>>, + payjoin_handler: Option>, keys_manager: Arc, network_graph: Arc, gossip_source: Arc, @@ -379,6 +383,8 @@ impl Node { let archive_cmon = Arc::clone(&self.chain_monitor); let sync_sweeper = Arc::clone(&self.output_sweeper); let sync_logger = Arc::clone(&self.logger); + let sync_payjoin = &self.payjoin_handler.as_ref(); + let sync_payjoin = sync_payjoin.map(Arc::clone); let sync_wallet_timestamp = Arc::clone(&self.latest_wallet_sync_timestamp); let sync_monitor_archival_height = Arc::clone(&self.latest_channel_monitor_archival_height); let mut stop_sync = self.stop_sender.subscribe(); @@ -398,11 +404,14 @@ impl Node { return; } _ = wallet_sync_interval.tick() => { - let confirmables = vec![ + let mut confirmables = vec![ &*sync_cman as &(dyn Confirm + Sync + Send), &*sync_cmon as &(dyn Confirm + Sync + Send), &*sync_sweeper as &(dyn Confirm + Sync + Send), ]; + if let Some(sync_payjoin) = sync_payjoin.as_ref() { + confirmables.push(sync_payjoin.as_ref() as &(dyn Confirm + Sync + Send)); + } let now = Instant::now(); let timeout_fut = tokio::time::timeout(Duration::from_secs(LDK_WALLET_SYNC_TIMEOUT_SECS), tx_sync.sync(confirmables)); match timeout_fut.await { @@ -1108,6 +1117,42 @@ impl Node { )) } + /// Returns a Payjoin payment handler allowing to send Payjoin transactions + /// + /// in order to utilize Payjoin functionality, it is necessary to configure a Payjoin relay + /// using [`set_payjoin_config`]. + /// + /// [`set_payjoin_config`]: crate::builder::NodeBuilder::set_payjoin_config + #[cfg(not(feature = "uniffi"))] + pub fn payjoin_payment(&self) -> PayjoinPayment { + let payjoin_handler = self.payjoin_handler.as_ref(); + PayjoinPayment::new( + Arc::clone(&self.config), + Arc::clone(&self.logger), + payjoin_handler.map(Arc::clone), + Arc::clone(&self.runtime), + Arc::clone(&self.tx_broadcaster), + ) + } + + /// Returns a Payjoin payment handler allowing to send Payjoin transactions. + /// + /// in order to utilize Payjoin functionality, it is necessary to configure a Payjoin relay + /// using [`set_payjoin_config`]. + /// + /// [`set_payjoin_config`]: crate::builder::NodeBuilder::set_payjoin_config + #[cfg(feature = "uniffi")] + pub fn payjoin_payment(&self) -> Arc { + let payjoin_handler = self.payjoin_handler.as_ref(); + Arc::new(PayjoinPayment::new( + Arc::clone(&self.config), + Arc::clone(&self.logger), + payjoin_handler.map(Arc::clone), + Arc::clone(&self.runtime), + Arc::clone(&self.tx_broadcaster), + )) + } + /// Retrieve a list of known channels. pub fn list_channels(&self) -> Vec { self.channel_manager.list_channels().into_iter().map(|c| c.into()).collect() @@ -1309,11 +1354,15 @@ impl Node { let fee_estimator = Arc::clone(&self.fee_estimator); let sync_sweeper = Arc::clone(&self.output_sweeper); let sync_logger = Arc::clone(&self.logger); - let confirmables = vec![ + let sync_payjoin = &self.payjoin_handler.as_ref(); + let mut confirmables = vec![ &*sync_cman as &(dyn Confirm + Sync + Send), &*sync_cmon as &(dyn Confirm + Sync + Send), &*sync_sweeper as &(dyn Confirm + Sync + Send), ]; + if let Some(sync_payjoin) = sync_payjoin { + confirmables.push(sync_payjoin.as_ref() as &(dyn Confirm + Sync + Send)); + } let sync_wallet_timestamp = Arc::clone(&self.latest_wallet_sync_timestamp); let sync_fee_rate_update_timestamp = Arc::clone(&self.latest_fee_rate_cache_update_timestamp); diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 5959bd58e..f8d5e2fb2 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -147,6 +147,58 @@ macro_rules! expect_payment_successful_event { pub(crate) use expect_payment_successful_event; +macro_rules! expect_payjoin_tx_pending_event { + ($node: expr) => {{ + match $node.wait_next_event() { + ref e @ Event::PayjoinPaymentPending { txid, .. } => { + println!("{} got event {:?}", $node.node_id(), e); + $node.event_handled(); + txid + }, + ref e => { + panic!("{} got unexpected event!: {:?}", std::stringify!($node), e); + }, + } + }}; +} + +pub(crate) use expect_payjoin_tx_pending_event; + +macro_rules! expect_payjoin_tx_sent_successfully_event { + ($node: expr, $is_original_psbt_modified: expr) => {{ + match $node.wait_next_event() { + ref e @ Event::PayjoinPaymentSuccessful { txid, is_original_psbt_modified, .. } => { + println!("{} got event {:?}", $node.node_id(), e); + assert_eq!(is_original_psbt_modified, $is_original_psbt_modified); + $node.event_handled(); + txid + }, + ref e => { + panic!("{} got unexpected event!: {:?}", std::stringify!($node), e); + }, + } + }}; +} + +pub(crate) use expect_payjoin_tx_sent_successfully_event; + +macro_rules! expect_payjoin_tx_broadcasted { + ($node: expr) => {{ + match $node.wait_next_event() { + ref e @ Event::PayjoinPaymentBroadcasted { txid, .. } => { + println!("{} got event {:?}", $node.node_id(), e); + $node.event_handled(); + txid + }, + ref e => { + panic!("{} got unexpected event!: {:?}", std::stringify!($node), e); + }, + } + }}; +} + +pub(crate) use expect_payjoin_tx_broadcasted; + pub(crate) fn setup_bitcoind_and_electrsd() -> (BitcoinD, ElectrsD) { let bitcoind_exe = env::var("BITCOIND_EXE").ok().or_else(|| bitcoind::downloaded_exe_path().ok()).expect( @@ -270,6 +322,20 @@ pub(crate) fn setup_node(electrsd: &ElectrsD, config: Config) -> TestNode { node } +pub(crate) fn setup_payjoin_node(electrsd: &ElectrsD, config: Config) -> TestNode { + let esplora_url = format!("http://{}", electrsd.esplora_url.as_ref().unwrap()); + setup_builder!(builder, config); + builder.set_esplora_server(esplora_url.clone()); + let payjoin_relay = "https://pj.bobspacebkk.com".to_string(); + builder.set_payjoin_config(payjoin_relay).unwrap(); + let test_sync_store = Arc::new(TestSyncStore::new(config.storage_dir_path.into())); + let node = builder.build_with_store(test_sync_store).unwrap(); + node.start().unwrap(); + assert!(node.status().is_running); + assert!(node.status().latest_fee_rate_cache_update_timestamp.is_some()); + node +} + pub(crate) fn generate_blocks_and_wait( bitcoind: &BitcoindClient, electrs: &E, num: usize, ) { diff --git a/tests/integration_tests_payjoin.rs b/tests/integration_tests_payjoin.rs new file mode 100644 index 000000000..884ce45da --- /dev/null +++ b/tests/integration_tests_payjoin.rs @@ -0,0 +1,273 @@ +mod common; + +use common::{ + expect_payjoin_tx_sent_successfully_event, generate_blocks_and_wait, + premine_and_distribute_funds, setup_bitcoind_and_electrsd, wait_for_tx, +}; + +use bitcoin::Amount; +use bitcoincore_rpc::{Client as BitcoindClient, RawTx, RpcApi}; +use ldk_node::{ + payment::{PaymentDirection, PaymentKind, PaymentStatus}, + Event, +}; +use payjoin::{ + receive::v2::{Enrolled, Enroller}, + OhttpKeys, PjUriBuilder, +}; + +use crate::common::{ + expect_payjoin_tx_broadcasted, expect_payjoin_tx_pending_event, random_config, + setup_payjoin_node, setup_two_nodes, +}; + +struct PayjoinReceiver { + ohttp_keys: OhttpKeys, + enrolled: Enrolled, +} + +impl PayjoinReceiver { + fn enroll() -> Self { + let payjoin_directory = payjoin::Url::parse("https://payjo.in").unwrap(); + let payjoin_relay = payjoin::Url::parse("https://pj.bobspacebkk.com").unwrap(); + let ohttp_keys = { + let payjoin_directory = payjoin_directory.join("/ohttp-keys").unwrap(); + let proxy = reqwest::Proxy::all(payjoin_relay.clone()).unwrap(); + let client = reqwest::blocking::Client::builder().proxy(proxy).build().unwrap(); + let response = client.get(payjoin_directory).send().unwrap(); + let response = response.bytes().unwrap(); + OhttpKeys::decode(response.to_vec().as_slice()).unwrap() + }; + let mut enroller = Enroller::from_directory_config( + payjoin_directory.clone(), + ohttp_keys.clone(), + payjoin_relay.clone(), + ); + let (req, ctx) = enroller.extract_req().unwrap(); + let mut headers = reqwest::header::HeaderMap::new(); + headers.insert( + reqwest::header::CONTENT_TYPE, + reqwest::header::HeaderValue::from_static("message/ohttp-req"), + ); + let response = reqwest::blocking::Client::new() + .post(&req.url.to_string()) + .body(req.body) + .headers(headers) + .send() + .unwrap(); + let response = match response.bytes() { + Ok(response) => response, + Err(_) => { + panic!("Error reading response"); + }, + }; + let enrolled = enroller.process_res(response.to_vec().as_slice(), ctx).unwrap(); + Self { ohttp_keys, enrolled } + } + + pub(crate) fn receive( + &self, amount: bitcoin::Amount, receiving_address: bitcoin::Address, + ) -> String { + let enrolled = self.enrolled.clone(); + let fallback_target = enrolled.fallback_target(); + let ohttp_keys = self.ohttp_keys.clone(); + let pj_part = payjoin::Url::parse(&fallback_target).unwrap(); + let payjoin_uri = PjUriBuilder::new(receiving_address, pj_part, Some(ohttp_keys.clone())) + .amount(amount) + .build(); + payjoin_uri.to_string() + } + + pub(crate) fn process_payjoin_request( + self, substitue_address: Option, bitcoind: Option<&BitcoindClient>, + ) { + let mut enrolled = self.enrolled; + let (req, context) = enrolled.extract_req().unwrap(); + let client = reqwest::blocking::Client::new(); + let response = client + .post(req.url.to_string()) + .body(req.body) + .headers(PayjoinReceiver::ohttp_headers()) + .send() + .unwrap(); + let response = response.bytes().unwrap(); + let response = enrolled.process_res(response.to_vec().as_slice(), context).unwrap(); + let unchecked_proposal = response.unwrap(); + if let Some(bitcoind) = bitcoind { + let tx = unchecked_proposal.extract_tx_to_schedule_broadcast(); + let raw_tx = tx.raw_hex(); + bitcoind.send_raw_transaction(raw_tx).unwrap(); + return; + } + + let proposal = unchecked_proposal.assume_interactive_receiver(); + let proposal = proposal.check_inputs_not_owned(|_script| Ok(false)).unwrap(); + let proposal = proposal.check_no_mixed_input_scripts().unwrap(); + let proposal = proposal.check_no_inputs_seen_before(|_outpoint| Ok(false)).unwrap(); + let mut provisional_proposal = + proposal.identify_receiver_outputs(|_script| Ok(true)).unwrap(); + + if let Some(substitue_address) = substitue_address { + provisional_proposal.substitute_output_address(substitue_address); + } + + // Finalise Payjoin Proposal + let mut payjoin_proposal = + provisional_proposal.finalize_proposal(|psbt| Ok(psbt.clone()), None).unwrap(); + + let (receiver_request, _) = payjoin_proposal.extract_v2_req().unwrap(); + reqwest::blocking::Client::new() + .post(&receiver_request.url.to_string()) + .body(receiver_request.body) + .headers(PayjoinReceiver::ohttp_headers()) + .send() + .unwrap(); + } + + fn ohttp_headers() -> reqwest::header::HeaderMap { + let mut headers = reqwest::header::HeaderMap::new(); + headers.insert( + reqwest::header::CONTENT_TYPE, + reqwest::header::HeaderValue::from_static("message/ohttp-req"), + ); + headers + } +} + +// Test sending payjoin transaction with original PSBT used eventually +#[test] +fn send_payjoin_transaction_original_psbt_used() { + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let config_a = random_config(false); + let (receiver, _) = setup_two_nodes(&electrsd, false, false, false); + let payjoin_sender = setup_payjoin_node(&electrsd, config_a); + let addr_a = payjoin_sender.onchain_payment().new_address().unwrap(); + let premine_amount_sat = 100_000_00; + premine_and_distribute_funds( + &bitcoind.client, + &electrsd.client, + vec![addr_a], + Amount::from_sat(premine_amount_sat), + ); + payjoin_sender.sync_wallets().unwrap(); + assert_eq!(payjoin_sender.list_balances().spendable_onchain_balance_sats, premine_amount_sat); + assert_eq!(payjoin_sender.list_balances().spendable_onchain_balance_sats, 100_000_00); + assert_eq!(payjoin_sender.next_event(), None); + + let payjoin_receiver = PayjoinReceiver::enroll(); + let payjoin_uri = payjoin_receiver + .receive(Amount::from_sat(80_000), receiver.onchain_payment().new_address().unwrap()); + + assert!(payjoin_sender.payjoin_payment().send(payjoin_uri).is_ok()); + + let txid = expect_payjoin_tx_pending_event!(payjoin_sender); + let payments = payjoin_sender.list_payments(); + let payment = payments.first().unwrap(); + assert_eq!(payment.amount_msat, Some(80_000)); + assert_eq!(payment.status, PaymentStatus::Pending); + assert_eq!(payment.direction, PaymentDirection::Outbound); + assert_eq!(payment.kind, PaymentKind::Payjoin); + + payjoin_receiver.process_payjoin_request(None, None); + + expect_payjoin_tx_broadcasted!(payjoin_sender); + + wait_for_tx(&electrsd.client, txid); + generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 1); + payjoin_sender.sync_wallets().unwrap(); + + let _ = expect_payjoin_tx_sent_successfully_event!(payjoin_sender, false); +} + +// Test sending payjoin transaction with changes to the original PSBT +#[test] +fn send_payjoin_transaction() { + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let config_a = random_config(false); + let (receiver, _) = setup_two_nodes(&electrsd, false, false, false); + let payjoin_sender = setup_payjoin_node(&electrsd, config_a); + let addr_a = payjoin_sender.onchain_payment().new_address().unwrap(); + let premine_amount_sat = 100_000_00; + premine_and_distribute_funds( + &bitcoind.client, + &electrsd.client, + vec![addr_a], + Amount::from_sat(premine_amount_sat), + ); + payjoin_sender.sync_wallets().unwrap(); + assert_eq!(payjoin_sender.list_balances().spendable_onchain_balance_sats, premine_amount_sat); + assert_eq!(payjoin_sender.list_balances().spendable_onchain_balance_sats, 100_000_00); + assert_eq!(payjoin_sender.next_event(), None); + + let payjoin_receiver = PayjoinReceiver::enroll(); + let payjoin_uri = payjoin_receiver + .receive(Amount::from_sat(80_000), receiver.onchain_payment().new_address().unwrap()); + + assert!(payjoin_sender.payjoin_payment().send(payjoin_uri).is_ok()); + + let _txid = expect_payjoin_tx_pending_event!(payjoin_sender); + let payments = payjoin_sender.list_payments(); + let payment = payments.first().unwrap(); + assert_eq!(payment.amount_msat, Some(80_000)); + assert_eq!(payment.status, PaymentStatus::Pending); + assert_eq!(payment.direction, PaymentDirection::Outbound); + assert_eq!(payment.kind, PaymentKind::Payjoin); + + let substitue_address = receiver.onchain_payment().new_address().unwrap(); + payjoin_receiver.process_payjoin_request(Some(substitue_address), None); + + let txid = expect_payjoin_tx_broadcasted!(payjoin_sender); + + wait_for_tx(&electrsd.client, txid); + generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 1); + payjoin_sender.sync_wallets().unwrap(); + + expect_payjoin_tx_sent_successfully_event!(payjoin_sender, true); +} + +// Test sending payjoin transaction with receiver broadcasting and not responding to the payjoin +// request +#[test] +fn send_payjoin_transaction_with_receiver_broadcasting() { + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let config_a = random_config(false); + let (receiver, _) = setup_two_nodes(&electrsd, false, false, false); + let payjoin_sender = setup_payjoin_node(&electrsd, config_a); + let addr_a = payjoin_sender.onchain_payment().new_address().unwrap(); + let premine_amount_sat = 100_000_00; + premine_and_distribute_funds( + &bitcoind.client, + &electrsd.client, + vec![addr_a], + Amount::from_sat(premine_amount_sat), + ); + payjoin_sender.sync_wallets().unwrap(); + assert_eq!(payjoin_sender.list_balances().spendable_onchain_balance_sats, premine_amount_sat); + assert_eq!(payjoin_sender.list_balances().spendable_onchain_balance_sats, 100_000_00); + assert_eq!(payjoin_sender.next_event(), None); + + let payjoin_receiver = PayjoinReceiver::enroll(); + let payjoin_uri = payjoin_receiver + .receive(Amount::from_sat(80_000), receiver.onchain_payment().new_address().unwrap()); + + assert!(payjoin_sender.payjoin_payment().send(payjoin_uri).is_ok()); + + let txid = expect_payjoin_tx_pending_event!(payjoin_sender); + let payments = payjoin_sender.list_payments(); + let payment = payments.first().unwrap(); + assert_eq!(payment.amount_msat, Some(80_000)); + assert_eq!(payment.status, PaymentStatus::Pending); + assert_eq!(payment.direction, PaymentDirection::Outbound); + assert_eq!(payment.kind, PaymentKind::Payjoin); + + let substitue_address = receiver.onchain_payment().new_address().unwrap(); + payjoin_receiver.process_payjoin_request(Some(substitue_address), Some(&bitcoind.client)); + + // let txid = expect_payjoin_tx_broadcasted!(payjoin_sender); + + wait_for_tx(&electrsd.client, txid); + generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 1); + payjoin_sender.sync_wallets().unwrap(); + + expect_payjoin_tx_sent_successfully_event!(payjoin_sender, false); +}