diff --git a/Cargo.toml b/Cargo.toml index 4c4422461..7dff4907b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,15 +28,28 @@ panic = 'abort' # Abort on panic default = [] [dependencies] -lightning = { version = "0.0.123", features = ["std"] } -lightning-invoice = { version = "0.31.0" } -lightning-net-tokio = { version = "0.0.123" } -lightning-persister = { version = "0.0.123" } -lightning-background-processor = { version = "0.0.123", features = ["futures"] } -lightning-rapid-gossip-sync = { version = "0.0.123" } -lightning-transaction-sync = { version = "0.0.123", features = ["esplora-async-https", "time"] } -lightning-liquidity = { version = "0.1.0-alpha.4", features = ["std"] } - +# lightning = { version = "0.0.123", features = ["std"] } +# lightning-invoice = { version = "0.31.0" } +# lightning-net-tokio = { version = "0.0.123" } +# lightning-persister = { version = "0.0.123" } +# lightning-background-processor = { version = "0.0.123", features = ["futures"] } +# lightning-rapid-gossip-sync = { version = "0.0.123" } +# lightning-transaction-sync = { version = "0.0.123", features = ["esplora-async-https", "time"] } +# lightning-liquidity = { version = "0.1.0-alpha.4", features = ["std"] } +lightning = { git = "https://github.com/jbesraa/rust-lightning", branch = "danger-funding-generated", features = ["std"] } +lightning-invoice = { git = "https://github.com/jbesraa/rust-lightning", branch = "danger-funding-generated" } +lightning-net-tokio = { git = "https://github.com/jbesraa/rust-lightning", branch = "danger-funding-generated" } +lightning-persister = { git = "https://github.com/jbesraa/rust-lightning", branch = "danger-funding-generated" } +lightning-background-processor = { git = "https://github.com/jbesraa/rust-lightning", branch = "danger-funding-generated", features = ["futures"] } +lightning-rapid-gossip-sync = { git = "https://github.com/jbesraa/rust-lightning", branch = "danger-funding-generated" } +lightning-transaction-sync = { git = "https://github.com/jbesraa/rust-lightning", branch = "danger-funding-generated", features = ["esplora-async-https", "time"] } +#lightning-liquidity = { version = "0.1.0-alpha.1", features = ["std"] } + +# lightning-liquidity = {path = "../../lightning-liquidity" git = "https://github.com/jbesraa/lightning-liquidity", rev = "b6ac60d", features = ["std"] } +lightning-liquidity = { git = "https://github.com/jbesraa/lightning-liquidity", branch = "pj-fixes", features = ["std"] } +# lightning-liquidity = { git = "https://github.com/tnull/lightning-liquidity", rev = "abf7088c0e03221c0f122e797f34802c9e99a3d4", features = ["std"] } + +# payjoin = { git = "https://github.com/jbesraa/rust-payjoin.git", rev = "9e4f454", features = ["v2", "receive", "send"] } #lightning = { git = "https://github.com/lightningdevkit/rust-lightning", branch="main", features = ["std"] } #lightning-invoice = { git = "https://github.com/lightningdevkit/rust-lightning", branch="main" } #lightning-net-tokio = { git = "https://github.com/lightningdevkit/rust-lightning", branch="main" } @@ -57,9 +70,9 @@ lightning-liquidity = { version = "0.1.0-alpha.4", features = ["std"] } bdk = { version = "0.29.0", default-features = false, features = ["std", "async-interface", "use-esplora-async", "sqlite-bundled", "keys-bip39"]} -reqwest = { version = "0.11", default-features = false, features = ["json", "rustls-tls"] } +reqwest = { version = "0.11", default-features = false, features = ["json", "rustls-tls", "blocking"] } rusqlite = { version = "0.28.0", features = ["bundled"] } -bitcoin = "0.30.2" +bitcoin = { version = "0.30.2", features = ["bitcoinconsensus"] } bip39 = "2.0.0" rand = "0.8.5" @@ -68,6 +81,7 @@ tokio = { version = "1", default-features = false, features = [ "rt-multi-thread esplora-client = { version = "0.6", default-features = false } libc = "0.2" uniffi = { version = "0.26.0", features = ["build"], optional = true } +payjoin = { version = "0.15.0", features = ["v2", "send", "receive"] } [target.'cfg(vss)'.dependencies] vss-client = "0.2" @@ -77,12 +91,14 @@ prost = { version = "0.11.6", default-features = false} winapi = { version = "0.3", features = ["winbase"] } [dev-dependencies] -lightning = { version = "0.0.123", features = ["std", "_test_utils"] } +# lightning = { version = "0.0.123", features = ["std", "_test_utils"] } +lightning = { git = "https://github.com/jbesraa/rust-lightning", branch = "danger-funding-generated", features = ["std", "_test_utils"] } #lightning = { git = "https://github.com/lightningdevkit/rust-lightning", branch="main", features = ["std", "_test_utils"] } 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" +reqwest = { version = "0.11", default-features = false, features = ["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 58fab0d52..b73cae720 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -151,6 +151,13 @@ enum NodeError { "InsufficientFunds", "LiquiditySourceUnavailable", "LiquidityFeeTooHigh", + "PayjoinReqwest", + "PayjoinValidation", + "PayjoinEnrollment", + "PayjoinUri", + "PayjoinReceiver", + "PayjoinSender", + "BitcoinConsensusFailed", }; dictionary NodeStatus { diff --git a/src/builder.rs b/src/builder.rs index 6d3db420f..edc740042 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -11,6 +11,11 @@ use crate::io::sqlite_store::SqliteStore; use crate::liquidity::LiquiditySource; use crate::logger::{log_error, log_info, FilesystemLogger, Logger}; use crate::message_handler::NodeCustomMessageHandler; +use crate::payjoin_receiver::{ + enroll_payjoin_receivers, PayjoinLightningReceiver, PayjoinReceiver, +}; +use crate::payjoin_scheduler::PayjoinScheduler; +use crate::payjoin_sender::PayjoinSender; use crate::payment::store::PaymentStore; use crate::peer_store::PeerStore; use crate::tx_broadcaster::TransactionBroadcaster; @@ -94,6 +99,18 @@ struct LiquiditySourceConfig { lsps2_service: Option<(SocketAddress, PublicKey, Option)>, } +#[derive(Debug, Clone)] +struct PayjoinReceiverConfig { + payjoin_directory: payjoin::Url, + payjoin_relay: payjoin::Url, + ohttp_keys: Option, +} + +#[derive(Debug, Clone)] +struct PayjoinSenderConfig { + payjoin_relay: payjoin::Url, +} + impl Default for LiquiditySourceConfig { fn default() -> Self { Self { lsps2_service: None } @@ -173,6 +190,8 @@ pub struct NodeBuilder { chain_data_source_config: Option, gossip_source_config: Option, liquidity_source_config: Option, + payjoin_receiver_config: Option, + payjoin_sender_config: Option, } impl NodeBuilder { @@ -188,12 +207,16 @@ impl NodeBuilder { let chain_data_source_config = None; let gossip_source_config = None; let liquidity_source_config = None; + let payjoin_receiver_config = None; + let payjoin_sender_config = None; Self { config, entropy_source_config, chain_data_source_config, gossip_source_config, liquidity_source_config, + payjoin_receiver_config, + payjoin_sender_config, } } @@ -248,6 +271,24 @@ impl NodeBuilder { self } + /// Configures the [`Node`] instance to source its gossip data from the given RapidGossipSync + /// server. + pub fn set_payjoin_sender_config(&mut self, payjoin_relay: payjoin::Url) -> &mut Self { + self.payjoin_sender_config = Some(PayjoinSenderConfig { payjoin_relay }); + self + } + + /// Configures the [`Node`] instance to source its gossip data from the given RapidGossipSync + /// server. + pub fn set_payjoin_receiver_config( + &mut self, payjoin_directory: payjoin::Url, payjoin_relay: payjoin::Url, + ohttp_keys: Option, + ) -> &mut Self { + self.payjoin_receiver_config = + Some(PayjoinReceiverConfig { payjoin_directory, payjoin_relay, ohttp_keys }); + self + } + /// Configures the [`Node`] instance to source its inbound liquidity from the given /// [LSPS2](https://github.com/BitcoinAndLightningLayerSpecs/lsp/blob/main/LSPS2/README.md) /// service. @@ -369,6 +410,8 @@ impl NodeBuilder { seed_bytes, logger, vss_store, + self.payjoin_receiver_config.as_ref(), + self.payjoin_sender_config.as_ref(), ) } @@ -390,6 +433,8 @@ impl NodeBuilder { seed_bytes, logger, kv_store, + self.payjoin_receiver_config.as_ref(), + self.payjoin_sender_config.as_ref(), ) } } @@ -454,6 +499,23 @@ impl ArcedNodeBuilder { self.inner.write().unwrap().set_gossip_source_p2p(); } + /// payjoin sender config + pub fn set_payjoin_sender_config(&self, payjoin_relay: payjoin::Url) { + self.inner.write().unwrap().set_payjoin_sender_config(payjoin_relay); + } + + /// payjoin receiver config + pub fn set_payjoin_receiver_config( + &self, payjoin_directory: payjoin::Url, payjoin_relay: payjoin::Url, + ohttp_keys: Option, + ) { + self.inner.write().unwrap().set_payjoin_receiver_config( + payjoin_directory, + payjoin_relay, + ohttp_keys, + ); + } + /// Configures the [`Node`] instance to source its gossip data from the given RapidGossipSync /// server. pub fn set_gossip_source_rgs(&self, rgs_server_url: String) { @@ -524,6 +586,8 @@ fn build_with_store_internal( gossip_source_config: Option<&GossipSourceConfig>, liquidity_source_config: Option<&LiquiditySourceConfig>, seed_bytes: [u8; 64], logger: Arc, kv_store: Arc, + payjoin_receiver_config: Option<&PayjoinReceiverConfig>, + payjoin_sender_config: Option<&PayjoinSenderConfig>, ) -> Result { // Initialize the on-chain wallet and chain access let xprv = bitcoin::bip32::ExtendedPrivKey::new_master(config.network.into(), &seed_bytes) @@ -556,6 +620,7 @@ fn build_with_store_internal( log_error!(logger, "Failed to set up wallet: {}", e); BuildError::WalletSetupFailed })?; + let payjoin_scheduler = Arc::new(tokio::sync::Mutex::new(PayjoinScheduler::new())); let (blockchain, tx_sync, tx_broadcaster, fee_estimator) = match chain_data_source_config { Some(ChainDataSourceConfig::Esplora(server_url)) => { @@ -566,6 +631,7 @@ fn build_with_store_internal( let tx_broadcaster = Arc::new(TransactionBroadcaster::new( tx_sync.client().clone(), Arc::clone(&logger), + Arc::clone(&payjoin_scheduler), )); let fee_estimator = Arc::new(OnchainFeeEstimator::new( tx_sync.client().clone(), @@ -584,6 +650,7 @@ fn build_with_store_internal( let tx_broadcaster = Arc::new(TransactionBroadcaster::new( tx_sync.client().clone(), Arc::clone(&logger), + Arc::clone(&payjoin_scheduler), )); let fee_estimator = Arc::new(OnchainFeeEstimator::new( tx_sync.client().clone(), @@ -973,6 +1040,49 @@ fn build_with_store_internal( }; let (stop_sender, _) = tokio::sync::watch::channel(()); + let payjoin_sender = if let Some(payjoin_sender_config) = payjoin_sender_config { + let payjoin_sender = PayjoinSender::new( + Arc::clone(&logger), + Arc::clone(&wallet), + &payjoin_sender_config.payjoin_relay, + ); + Some(Arc::new(payjoin_sender)) + } else { + None + }; + let (payjoin_receiver, payjoin_lightning_receiver) = + if let Some(payjoin_receiver_config) = payjoin_receiver_config { + let enrollement = enroll_payjoin_receivers( + &payjoin_receiver_config.ohttp_keys, + &payjoin_receiver_config.payjoin_directory, + &payjoin_receiver_config.payjoin_relay, + ) + .ok(); + if let Some(enrollement) = enrollement { + let (payjoin_enrollement, lightning_enrollement, ohttp_keys) = enrollement; + dbg!("Enrolled payjoin receiver"); + let payjoin_receiver = PayjoinReceiver::new( + Arc::clone(&logger), + Arc::clone(&wallet), + payjoin_enrollement, + ohttp_keys.clone(), + ); + + let payjoin_lightning_receiver = PayjoinLightningReceiver::new( + Arc::clone(&logger), + Arc::clone(&wallet), + Arc::clone(&channel_manager), + Arc::clone(&payjoin_scheduler), + lightning_enrollement, + ohttp_keys, + ); + (Some(Arc::new(payjoin_receiver)), Some(Arc::new(payjoin_lightning_receiver))) + } else { + (None, None) + } + } else { + (None, None) + }; let is_listening = Arc::new(AtomicBool::new(false)); let latest_wallet_sync_timestamp = Arc::new(RwLock::new(None)); @@ -993,6 +1103,9 @@ fn build_with_store_internal( channel_manager, chain_monitor, output_sweeper, + payjoin_receiver, + payjoin_sender, + payjoin_lightning_receiver, peer_manager, connection_manager, keys_manager, diff --git a/src/error.rs b/src/error.rs index 5acc75af8..00be44481 100644 --- a/src/error.rs +++ b/src/error.rs @@ -71,6 +71,20 @@ pub enum Error { LiquiditySourceUnavailable, /// The given operation failed due to the LSP's required opening fee being too high. LiquidityFeeTooHigh, + /// Payjoin errors + PayjoinReqwest, + /// Payjoin errors + PayjoinValidation, + /// Payjoin errors + PayjoinEnrollment, + /// Payjoin errors + PayjoinUri, + /// Payjoin errors + PayjoinReceiver, + /// Payjoin errors + PayjoinSender, + /// Payjoin errors + BitcoinConsensusFailed, } impl fmt::Display for Error { @@ -122,10 +136,23 @@ impl fmt::Display for Error { Self::LiquidityFeeTooHigh => { write!(f, "The given operation failed due to the LSP's required opening fee being too high.") }, + Self::PayjoinReqwest => write!(f, "PayjoinLightning: http error"), + Self::PayjoinValidation => write!(f, "PayjoinLightning: payjoin request validation failed."), + Self::PayjoinEnrollment => write!(f, "PayjoinLightning: payjoin enrollment failed. Maybe the configured payjoin directory or payjoin relay are not valid?"), + Self::PayjoinUri => write!(f, "PayjoinLightning: Failed to construct payjoin URI."), + Self::PayjoinSender => write!(f, "Failed to send payjoin."), + Self::PayjoinReceiver => write!(f, "Failed to receive payjoin."), + Self::BitcoinConsensusFailed => write!(f, "Bitcoin consensus failed."), } } } +impl From for Error { + fn from(_e: payjoin::Error) -> Self { + return Self::PayjoinValidation; + } +} + impl std::error::Error for Error {} impl From for Error { diff --git a/src/event.rs b/src/event.rs index 78188452f..9abdf8709 100644 --- a/src/event.rs +++ b/src/event.rs @@ -1,3 +1,4 @@ +use crate::payjoin_receiver::PayjoinLightningReceiver; use crate::types::{DynStore, Sweeper, Wallet}; use crate::{ hex_utils, ChannelManager, Config, Error, NetworkGraph, PeerInfo, PeerStore, UserChannelId, @@ -322,6 +323,7 @@ where runtime: Arc>>, logger: L, config: Arc, + payjoin_receiver: Option>>, } impl EventHandler @@ -333,6 +335,7 @@ where output_sweeper: Arc, network_graph: Arc, payment_store: Arc>, peer_store: Arc>, runtime: Arc>>, logger: L, config: Arc, + payjoin_receiver: Option>>, ) -> Self { Self { event_queue, @@ -341,6 +344,7 @@ where output_sweeper, network_graph, payment_store, + payjoin_receiver, peer_store, logger, runtime, @@ -355,6 +359,7 @@ where counterparty_node_id, channel_value_satoshis, output_script, + user_channel_id, .. } => { // Construct the raw transaction with the output that is paid the amount of the @@ -365,6 +370,19 @@ where let cur_height = self.channel_manager.current_best_block().height; let locktime = LockTime::from_height(cur_height).unwrap_or(LockTime::ZERO); + if let Some(payjoin_receiver) = self.payjoin_receiver.clone() { + if payjoin_receiver + .set_channel_accepted( + user_channel_id, + &output_script, + temporary_channel_id.0, + ) + .await + { + return; + } + } + // Sign the final funding transaction and broadcast it. match self.wallet.create_funding_transaction( output_script, diff --git a/src/lib.rs b/src/lib.rs index 3d619cebb..8a26e06ee 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -88,6 +88,10 @@ pub mod io; mod liquidity; mod logger; mod message_handler; +// mod payjoin_handler; +mod payjoin_receiver; +mod payjoin_scheduler; +mod payjoin_sender; pub mod payment; mod peer_store; mod sweep; @@ -99,6 +103,7 @@ mod wallet; pub use bip39; pub use bitcoin; +use bitcoin::address::NetworkChecked; pub use lightning; pub use lightning_invoice; @@ -108,6 +113,10 @@ pub use error::Error as NodeError; use error::Error; pub use event::Event; +use payjoin::PjUri; +use payjoin_receiver::{PayjoinLightningReceiver, PayjoinReceiver}; +use payjoin_scheduler::PayjoinChannel; +use payjoin_sender::PayjoinSender; pub use types::ChannelConfig; pub use io::utils::generate_entropy_mnemonic; @@ -181,6 +190,9 @@ pub struct Node { output_sweeper: Arc, peer_manager: Arc, connection_manager: Arc>>, + payjoin_receiver: Option>>>, + payjoin_sender: Option>>>, + payjoin_lightning_receiver: Option>>>, keys_manager: Arc, network_graph: Arc, gossip_source: Arc, @@ -491,6 +503,73 @@ impl Node { }); } + if let Some(payjoin_receiver) = &self.payjoin_receiver { + let mut stop_payjoin_server = self.stop_sender.subscribe(); + let payjoin_receiver = Arc::clone(&payjoin_receiver); + let payjoin_check_interval = 10; + runtime.spawn(async move { + let mut payjoin_interval = + tokio::time::interval(Duration::from_secs(payjoin_check_interval)); + payjoin_interval.reset(); + payjoin_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); + loop { + tokio::select! { + _ = stop_payjoin_server.changed() => { + return; + } + _ = payjoin_interval.tick() => { + let _ = payjoin_receiver.process_payjoin_request().await; + } + } + } + }); + } + + if let Some(payjoin_lightning_receiver) = &self.payjoin_lightning_receiver { + let mut stop_payjoin_server = self.stop_sender.subscribe(); + let payjoin_lightning_receiver = Arc::clone(&payjoin_lightning_receiver); + let payjoin_check_interval = 10; + runtime.spawn(async move { + let mut lightning_payjoin_interval = + tokio::time::interval(Duration::from_secs(payjoin_check_interval)); + lightning_payjoin_interval.reset(); + lightning_payjoin_interval + .set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); + loop { + tokio::select! { + _ = stop_payjoin_server.changed() => { + return; + } + _ = lightning_payjoin_interval.tick() => { + let _ = payjoin_lightning_receiver.process_payjoin_request().await; + } + } + } + }); + } + + // if let Some(payjoin_sender) = &self.payjoin_sender { + // let mut stop_payjoin_server = self.stop_sender.subscribe(); + // let payjoin_sender = Arc::clone(&payjoin_sender); + // let payjoin_check_interval = 2; + // runtime.spawn(async move { + // let mut payjoin_interval = + // tokio::time::interval(Duration::from_secs(payjoin_check_interval)); + // payjoin_interval.reset(); + // payjoin_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); + // loop { + // tokio::select! { + // _ = stop_payjoin_server.changed() => { + // return; + // } + // _ = payjoin_interval.tick() => { + // let _ = payjoin_sender.process_payjoin_response().await; + // } + // } + // } + // }); + // } + // Regularly reconnect to persisted peers. let connect_cm = Arc::clone(&self.connection_manager); let connect_pm = Arc::clone(&self.peer_manager); @@ -628,6 +707,7 @@ impl Node { Arc::clone(&self.runtime), Arc::clone(&self.logger), Arc::clone(&self.config), + self.payjoin_lightning_receiver.clone(), )); // Setup background processing @@ -697,6 +777,148 @@ impl Node { Ok(()) } + /// This method can be used to send a payjoin transaction as defined + /// in BIP77. + /// + /// The method will construct an `Original PSBT` from the data + /// provided in the `payjoin_uri` and `amount` parameters. The + /// amount must be set either in the `payjoin_uri` or in the + /// `amount` parameter. If the amount is set in both, the paramter + /// amount parameter will be used. + /// + /// After constructing the `Original PSBT`, the method will + /// extract the payjoin request data from the `Original PSBT` + /// utilising the `payjoin` crate. + /// + /// Then we start a background process to that will run for 1 + /// hour, polling the payjoin endpoint every 10 seconds. If an `OK` (ie status code == 200) + /// is received, polling will stop and we will try to process the + /// response from the payjoin receiver. If the response(or `Payjoin Proposal`) is valid, we will finalise the + /// transaction and broadcast it to the network. + /// + /// Notice that the `Txid` returned from this method is the + /// `Original PSBT` transaction id, but the `Payjoin Proposal` + /// transaction id could be different if the receiver changed the + /// transaction. + pub async fn send_payjoin_transaction( + &self, payjoin_uri: payjoin::Uri<'static, NetworkChecked>, amount: Option, + ) -> Result, Error> { + let rt_lock = self.runtime.read().unwrap(); + if rt_lock.is_none() { + return Err(Error::NotRunning); + } + let payjoin_sender = Arc::clone(self.payjoin_sender.as_ref().ok_or(Error::PayjoinSender)?); + let original_psbt = payjoin_sender.create_payjoin_request(payjoin_uri.clone(), amount)?; + let txid = original_psbt.clone().unsigned_tx.txid(); + let (request, context) = + payjoin_sender.extract_request_data(payjoin_uri, original_psbt.clone())?; + + let time = std::time::Instant::now(); + let runtime = rt_lock.as_ref().unwrap(); + runtime.spawn(async move { + let response = payjoin_sender.poll(&request, time).await; + if let Some(response) = response { + let psbt = context.process_response(&mut response.as_slice()); + match psbt { + Ok(Some(psbt)) => { + let finalized = + payjoin_sender.finalise_payjoin_tx(psbt, original_psbt.clone()); + if let Ok(txid) = finalized { + let txid: bitcoin::Txid = txid.into(); + return Some(txid); + } + }, + _ => return None, + } + } + None + }); + Ok(Some(txid)) + } + + /// Creates a payjoin URI with the given amount that can be used to request a payjoin + /// transaction. + pub fn request_payjoin_transaction(&self, amount_sats: u64) -> Result { + let rt_lock = self.runtime.read().unwrap(); + if rt_lock.is_none() { + return Err(Error::NotRunning); + } + let payjoin_receiver = match self.payjoin_receiver.as_ref() { + Some(pr) => pr, + None => { + dbg!("unable to get payjoin receiver"); + return Err(Error::PayjoinReceiver); + }, + }; + let payjoin_uri = payjoin_receiver.payjoin_uri(bitcoin::Amount::from_sat(amount_sats)); + payjoin_uri + } + + /// Requests a payjoin transaction with the given amount and a corresponding lightning channel + /// opening. + /// + /// This method will attempt to open a channel with the given node if the URI returned is + /// called. If the payjoin transaction is received but the channel opening fails, a normal + /// payjoin transaction will be conducted. + /// + /// The channel opening will start immediately after this method is called and is halted at the + /// `ChannelAccepted` state until the payjoin transaction is received and handeled by + /// `PayjoinReceiver::process_payjoin_request`. + pub fn request_payjoin_transaction_with_channel_opening( + &self, channel_amount_sats: u64, push_msat: Option, announce_channel: bool, + node_id: PublicKey, address: SocketAddress, + ) -> Result { + let rt_lock = self.runtime.read().unwrap(); + if rt_lock.is_none() { + return Err(Error::NotRunning); + } + let runtime = rt_lock.as_ref().unwrap(); + let user_channel_id: u128 = rand::thread_rng().gen::(); + let payjoin_receiver = + self.payjoin_lightning_receiver.as_ref().ok_or(Error::PayjoinReceiver)?; + payjoin_receiver.schedule_channel( + bitcoin::Amount::from_sat(channel_amount_sats), + node_id, + user_channel_id, + ); + let user_config = UserConfig { + channel_handshake_limits: Default::default(), + channel_handshake_config: ChannelHandshakeConfig { + announced_channel: announce_channel, + ..Default::default() + }, + ..Default::default() + }; + let push_msat = push_msat.unwrap_or(0); + self.internal_connect_open_channel( + node_id, + channel_amount_sats, + push_msat, + user_channel_id, + address, + Some(user_config), + runtime, + )?; + let payjoin_uri = + payjoin_receiver.payjoin_uri(bitcoin::Amount::from_sat(channel_amount_sats))?; + Ok(payjoin_uri) + } + + /// List all scheduled payjoin channels. + pub fn list_scheduled_payjoin_channels(&self) -> Result, Error> { + if let Some(payjoin_receiver) = self.payjoin_lightning_receiver.clone() { + Ok(payjoin_receiver.list_scheduled_channels()) + } else { + Ok(Vec::new()) + } + } + + /// List all scheduled payjoin channels. + pub fn list_transactions(&self) -> Result, Error> { + let transactions = self.wallet.list_transactions()?; + Ok(transactions) + } + /// Disconnects all peers, stops all running background tasks, and shuts down [`Node`]. /// /// After this returns most API methods will return [`Error::NotRunning`]. @@ -980,20 +1202,6 @@ impl Node { return Err(Error::InsufficientFunds); } - let peer_info = PeerInfo { node_id, address }; - - let con_node_id = peer_info.node_id; - let con_addr = peer_info.address.clone(); - let con_cm = Arc::clone(&self.connection_manager); - - // We need to use our main runtime here as a local runtime might not be around to poll - // connection futures going forward. - tokio::task::block_in_place(move || { - runtime.block_on(async move { - con_cm.connect_peer_if_necessary(con_node_id, con_addr).await - }) - })?; - let channel_config = (*(channel_config.unwrap_or_default())).clone().into(); let user_config = UserConfig { channel_handshake_limits: Default::default(), @@ -1007,6 +1215,33 @@ impl Node { let push_msat = push_to_counterparty_msat.unwrap_or(0); let user_channel_id: u128 = rand::thread_rng().gen::(); + self.internal_connect_open_channel( + node_id, + channel_amount_sats, + push_msat, + user_channel_id, + address, + Some(user_config), + runtime, + ) + } + + fn internal_connect_open_channel( + &self, node_id: PublicKey, channel_amount_sats: u64, push_msat: u64, user_channel_id: u128, + address: SocketAddress, user_config: Option, runtime: &tokio::runtime::Runtime, + ) -> Result { + let peer_info = PeerInfo { node_id, address }; + let con_node_id = peer_info.node_id; + let con_addr = peer_info.address.clone(); + let con_cm = Arc::clone(&self.connection_manager); + + // We need to use our main runtime here as a local runtime might not be around to poll + // connection futures going forward. + tokio::task::block_in_place(move || { + runtime.block_on(async move { + con_cm.connect_peer_if_necessary(con_node_id, con_addr).await + }) + })?; match self.channel_manager.create_channel( peer_info.node_id, @@ -1014,7 +1249,7 @@ impl Node { push_msat, user_channel_id, None, - Some(user_config), + user_config, ) { Ok(_) => { log_info!( diff --git a/src/payjoin_receiver.rs b/src/payjoin_receiver.rs new file mode 100644 index 000000000..0b567e426 --- /dev/null +++ b/src/payjoin_receiver.rs @@ -0,0 +1,454 @@ +use crate::error::Error; +use crate::payjoin_scheduler::PayjoinScheduler; +use crate::types::{ChannelManager, Wallet}; +use crate::PayjoinChannel; +use bitcoin::secp256k1::PublicKey; +use bitcoin::ScriptBuf; +use lightning::ln::ChannelId; +use lightning::util::logger::Logger; +use lightning::{log_error, log_info}; +use payjoin::receive::v2::{Enrolled, Enroller, ProvisionalProposal, UncheckedProposal}; +use payjoin::{OhttpKeys, PjUriBuilder}; +use payjoin::{PjUri, Url}; +use reqwest::header::HeaderMap; +use reqwest::header::HeaderValue; +use std::ops::Deref; +use std::sync::Arc; +use tokio::sync::Mutex; + +/// Payjoin Receiver +async fn post_request(url: &payjoin::Url, body: Vec) -> Result, Error> { + let headers = payjoin_receiver_request_headers(); + let client = reqwest::Client::new(); + let response = + client.post(url.to_string()).body(body).headers(headers).send().await.map_err(|e| { + dbg!(&e); + Error::PayjoinReceiver + })?; + let response = response.bytes().await.map_err(|e| { + dbg!(&e); + Error::PayjoinReceiver + })?; + Ok(response.to_vec()) +} + +pub fn enroll_payjoin_receivers( + ohttp_keys: &Option, payjoin_directory: &Url, payjoin_relay: &Url, +) -> Result<(Enrolled, Enrolled, OhttpKeys), Error> { + let ohttp_keys = match ohttp_keys { + Some(ohttp_keys) => ohttp_keys.clone(), + None => { + let payjoin_directory = + payjoin_directory.join("/ohttp-keys").map_err(|_| Error::PayjoinEnrollment)?; + let res = BlockingHttpClient::new_proxy(payjoin_relay) + .and_then(|c| c.get(&payjoin_directory))?; + OhttpKeys::decode(res.as_slice()).map_err(|e| { + dbg!(&e); + Error::PayjoinEnrollment + })? + }, + }; + let enrolled = { + let mut enroller = Enroller::from_directory_config( + payjoin_directory.clone(), + ohttp_keys.clone(), + payjoin_relay.clone(), + ); + let (req, ctx) = enroller.extract_req()?; + dbg!("normal subdirectory", enroller.subdirectory()); + let headers = payjoin_receiver_request_headers(); + let response = BlockingHttpClient::new() + .and_then(|c| c.post(&req.url.to_string(), req.body, headers, None))?; + let response = match response.bytes() { + Ok(response) => response, + Err(e) => { + dbg!(&e); + return Err(Error::PayjoinEnrollment); + }, + }; + let enrolled = enroller.process_res(response.to_vec().as_slice(), ctx)?; + enrolled + }; + + let lightning_enrolled = { + let mut enroller = Enroller::from_directory_config( + payjoin_directory.clone(), + ohttp_keys.clone(), + payjoin_relay.clone(), + ); + dbg!("lightning subdirectory", enroller.subdirectory()); + let (req, ctx) = enroller.extract_req()?; + let headers = payjoin_receiver_request_headers(); + let response = BlockingHttpClient::new() + .and_then(|c| c.post(&req.url.to_string(), req.body, headers, None))?; + let response = match response.bytes() { + Ok(response) => response, + Err(e) => { + dbg!(&e); + return Err(Error::PayjoinEnrollment); + }, + }; + let enrolled = enroller.process_res(response.to_vec().as_slice(), ctx)?; + enrolled + }; + + Ok((enrolled, lightning_enrolled, ohttp_keys)) +} + +pub(crate) struct PayjoinLightningReceiver +where + L::Target: Logger, +{ + logger: L, + scheduler: Arc>, + wallet: Arc, + channel_manager: Arc, + ohttp_keys: OhttpKeys, + enrolled: Enrolled, +} + +impl PayjoinLightningReceiver +where + L::Target: Logger, +{ + pub fn new( + logger: L, wallet: Arc, channel_manager: Arc, + scheduler: Arc>, enrolled: Enrolled, ohttp_keys: OhttpKeys, + ) -> Self { + Self { logger, wallet, channel_manager, scheduler, enrolled, ohttp_keys } + } + + async fn fetch_payjoin_request(&self) -> Result, Error> { + let mut enrolled = self.enrolled.clone(); + let (req, context) = match enrolled.extract_req() { + Ok(req) => req, + Err(e) => { + dbg!("Error: {}", &e); + log_error!(self.logger, "Payjoin Receiver: {}", e); + return Err(Error::PayjoinReceiver); + }, + }; + let payjoin_request = post_request(&req.url, req.body).await?; + Ok(enrolled.process_res(payjoin_request.as_slice(), context)?) + } + + pub(crate) async fn process_payjoin_request(&self) { + let min_fee_rate = bitcoin::FeeRate::from_sat_per_vb(1); + let mut scheduler = self.scheduler.lock().await; + if scheduler.list_channels().is_empty() {} + if !scheduler.in_progress() { + let unchecked_proposal = match self.fetch_payjoin_request().await { + Ok(Some(proposal)) => proposal, + _ => { + return; + }, + }; + let tx = unchecked_proposal.extract_tx_to_schedule_broadcast(); + match scheduler.add_seen_tx(&tx) { + true => {}, + false => { + dbg!("Input seen before"); + log_error!(self.logger, "Payjoin Receiver: Seen tx before"); + return; + }, + }; + let (provisional_proposal, amount_to_us) = + match self.validate_payjoin_request(unchecked_proposal, min_fee_rate).await { + Ok(proposal) => proposal, + Err(_e) => { + return; + }, + }; + let scheduled_channel = scheduler.get_next_channel(amount_to_us); + if let Some(channel) = scheduled_channel { + let (channel_id, address, temporary_channel_id, _, counterparty_node_id) = channel; + let mut channel_provisional_proposal = provisional_proposal.clone(); + channel_provisional_proposal.substitute_output_address(address); + let payjoin_proposal = match channel_provisional_proposal + .finalize_proposal(|psbt| Ok(psbt.clone()), None) + { + Ok(proposal) => proposal, + Err(e) => { + log_error!(self.logger, "Payjoin Receiver: {}", e); + return; + }, + }; + let (receiver_request, _) = match payjoin_proposal.clone().extract_v2_req() { + Ok((req, ctx)) => (req, ctx), + Err(e) => { + log_error!(self.logger, "Payjoin Receiver: {}", e); + return; + }, + }; + let tx = payjoin_proposal.psbt().clone().extract_tx(); + if let (true, Ok(())) = ( + scheduler.set_funding_tx_created( + channel_id, + &receiver_request.url, + receiver_request.body, + ), + self.channel_manager.unsafe_funding_transaction_generated( + &ChannelId::from_bytes(temporary_channel_id), + &counterparty_node_id, + tx.clone(), + ), + ) { + return; + } + } + } + } + + pub(crate) fn payjoin_uri(&self, amount: bitcoin::Amount) -> Result { + let address = self.wallet.get_new_address()?; + let pj_part = Url::parse(&self.enrolled.fallback_target()).map_err(|e| { + dbg!(&e); + Error::PayjoinUri + })?; + let payjoin_uri = PjUriBuilder::new(address, pj_part, Some(self.ohttp_keys.clone())) + .amount(amount) + .build(); + Ok(payjoin_uri) + } + + pub(crate) fn schedule_channel( + &self, amount: bitcoin::Amount, counterparty_node_id: PublicKey, channel_id: u128, + ) { + let channel = PayjoinChannel::new(amount, counterparty_node_id, channel_id); + self.scheduler.blocking_lock().schedule( + channel.channel_value_satoshi(), + channel.counterparty_node_id(), + channel.channel_id(), + ); + } + + pub(crate) fn list_scheduled_channels(&self) -> Vec { + self.scheduler.blocking_lock().list_channels().clone() + } + + pub(crate) async fn set_channel_accepted( + &self, channel_id: u128, output_script: &ScriptBuf, temporary_channel_id: [u8; 32], + ) -> bool { + let mut scheduler = self.scheduler.lock().await; + scheduler.set_channel_accepted(channel_id, output_script, temporary_channel_id) + } + + async fn validate_payjoin_request( + &self, proposal: UncheckedProposal, min_fee_rate: Option, + ) -> Result<(ProvisionalProposal, bitcoin::Amount), Error> { + let tx = proposal.extract_tx_to_schedule_broadcast(); + let verified = self.wallet.verify_tx(&tx).await; + let amount_to_us = self.wallet.funds_directed_to_us(&tx).unwrap_or_default(); + let proposal = + proposal.check_broadcast_suitability(min_fee_rate, |_t| Ok(verified.is_ok()))?; + let proposal = proposal.check_inputs_not_owned(|script| { + Ok(self.wallet.is_mine(&script.to_owned()).unwrap_or(false)) + })?; + let proposal = match proposal.check_no_mixed_input_scripts() { + Ok(proposal) => proposal, + Err(_) => { + log_error!(self.logger, "Payjoin Receiver: Mixed input scripts"); + return Err(Error::PayjoinReceiver); + }, + }; + let proposal = proposal.check_no_inputs_seen_before(|_outpoint| Ok(false))?; + let original_proposal = proposal.clone().identify_receiver_outputs(|script| { + Ok(self.wallet.is_mine(&script.to_owned()).unwrap_or(false)) + })?; + Ok((original_proposal, amount_to_us)) + } +} + +pub(crate) struct PayjoinReceiver +where + L::Target: Logger, +{ + logger: L, + wallet: Arc, + enrolled: Enrolled, + ohttp_keys: OhttpKeys, +} + +impl PayjoinReceiver +where + L::Target: Logger, +{ + pub fn new(logger: L, wallet: Arc, enrolled: Enrolled, ohttp_keys: OhttpKeys) -> Self { + Self { logger, wallet, enrolled, ohttp_keys } + } + + async fn fetch_payjoin_request(&self) -> Result, Error> { + dbg!("fetching pajoing request"); + let mut enrolled = self.enrolled.clone(); + dbg!("1"); + let (req, context) = match enrolled.extract_req() { + Ok(req) => req, + Err(e) => { + dbg!("Error: {}", &e); + log_error!(self.logger, "Payjoin Receiver: {}", e); + return Err(Error::PayjoinReceiver); + }, + }; + dbg!("2"); + let payjoin_request = post_request(&req.url, req.body).await?; + dbg!("3"); + let response = match enrolled.process_res(payjoin_request.as_slice(), context) { + Ok(response) => response, + Err(e) => { + dbg!("Error process payjoin request: {}", &e); + log_error!(self.logger, "Payjoin Receiver: {}", e); + return Err(Error::PayjoinReceiver); + }, + }; + dbg!("4"); + Ok(response) + } + + pub(crate) async fn process_payjoin_request(&self) { + dbg!("processing normal payjoin request"); + let min_fee_rate = bitcoin::FeeRate::from_sat_per_vb(1); + let unchecked_proposal = match self.fetch_payjoin_request().await { + Ok(Some(proposal)) => proposal, + _ => { + dbg!("NONE"); + return; + }, + }; + dbg!("got unchecked proposal"); + let (provisional_proposal, _) = + match self.validate_payjoin_request(unchecked_proposal, min_fee_rate).await { + Ok(proposal) => proposal, + Err(_e) => { + dbg!("Error validating payjoin request"); + return; + }, + }; + dbg!("finalizing payjoin request"); + self.accept_normal_payjoin_request(provisional_proposal).await + } + + async fn accept_normal_payjoin_request(&self, provisional_proposal: ProvisionalProposal) { + let mut finalized_proposal = + match provisional_proposal.finalize_proposal(|psbt| Ok(psbt.clone()), None) { + Ok(proposal) => proposal, + Err(e) => { + log_error!(self.logger, "Payjoin Receiver: {}", e); + return; + }, + }; + let (receiver_request, _) = match finalized_proposal.extract_v2_req() { + Ok(req) => req, + Err(e) => { + log_error!(self.logger, "Payjoin Receiver: {}", e); + return; + }, + }; + match post_request(&receiver_request.url, receiver_request.body).await { + Ok(_response) => { + log_info!(self.logger, "Payjoin Receiver: Payjoin request sent to sender"); + }, + Err(e) => { + log_error!(self.logger, "Payjoin Receiver: {}", e); + }, + } + } + + pub(crate) fn payjoin_uri(&self, amount: bitcoin::Amount) -> Result { + let address = self.wallet.get_new_address()?; + let pj_part = + Url::parse(&self.enrolled.fallback_target()).map_err(|_| Error::PayjoinUri)?; + let payjoin_uri = PjUriBuilder::new(address, pj_part, Some(self.ohttp_keys.clone())) + .amount(amount) + .build(); + Ok(payjoin_uri) + } + + async fn validate_payjoin_request( + &self, proposal: UncheckedProposal, min_fee_rate: Option, + ) -> Result<(ProvisionalProposal, bitcoin::Amount), Error> { + let tx = proposal.extract_tx_to_schedule_broadcast(); + let verified = self.wallet.verify_tx(&tx).await; + let amount_to_us = self.wallet.funds_directed_to_us(&tx).unwrap_or_default(); + let proposal = + proposal.check_broadcast_suitability(min_fee_rate, |_t| Ok(verified.is_ok()))?; + let proposal = proposal.check_inputs_not_owned(|script| { + Ok(self.wallet.is_mine(&script.to_owned()).unwrap_or(false)) + })?; + let proposal = match proposal.check_no_mixed_input_scripts() { + Ok(proposal) => proposal, + Err(_) => { + log_error!(self.logger, "Payjoin Receiver: Mixed input scripts"); + return Err(Error::PayjoinReceiver); + }, + }; + let proposal = proposal.check_no_inputs_seen_before(|_outpoint| Ok(false))?; + let original_proposal = proposal.clone().identify_receiver_outputs(|script| { + Ok(self.wallet.is_mine(&script.to_owned()).unwrap_or(false)) + })?; + Ok((original_proposal, amount_to_us)) + } +} + +pub(crate) fn payjoin_receiver_request_headers() -> HeaderMap { + let mut headers = HeaderMap::new(); + let header_value = HeaderValue::from_static("message/ohttp-req"); + headers.insert(reqwest::header::CONTENT_TYPE, header_value); + headers +} + +struct BlockingHttpClient { + client: reqwest::blocking::Client, +} + +impl BlockingHttpClient { + fn new() -> Result { + let client = + reqwest::blocking::Client::builder().build().map_err(|_| Error::PayjoinReqwest)?; + Ok(Self { client }) + } + + fn new_proxy(payjoin_relay: &Url) -> Result { + let proxy = + reqwest::Proxy::all(payjoin_relay.to_string()).map_err(|_| Error::PayjoinReqwest)?; + let client = reqwest::blocking::Client::builder() + .proxy(proxy) + .build() + .map_err(|_| Error::PayjoinReqwest)?; + Ok(Self { client }) + } + + fn get(&self, url: &Url) -> Result, Error> { + Ok(self + .client + .get(url.to_string()) + .send() + .and_then(|response| response.error_for_status()) + .map_err(|e| { + dbg!(&e); + Error::PayjoinReqwest + })? + .bytes() + .map_err(|e| { + dbg!(&e); + Error::PayjoinReqwest + })? + .to_vec()) + } + + fn post( + &self, url: &str, body: Vec, headers: HeaderMap, + timeout: Option, + ) -> Result { + Ok(self + .client + .post(url) + .timeout(timeout.unwrap_or(std::time::Duration::from_secs(35))) + .headers(headers) + .body(body) + .send() + .map_err(|e| { + dbg!(&e); + Error::PayjoinReqwest + })?) + } +} diff --git a/src/payjoin_scheduler.rs b/src/payjoin_scheduler.rs new file mode 100644 index 000000000..7a24e8d7e --- /dev/null +++ b/src/payjoin_scheduler.rs @@ -0,0 +1,352 @@ +use bitcoin::{secp256k1::PublicKey, ScriptBuf, Transaction, TxOut}; + +#[derive(Clone)] +pub struct PayjoinScheduler { + channels: Vec, + seen_txs: Vec, +} + +impl PayjoinScheduler { + /// Create a new empty channel scheduler. + pub fn new() -> Self { + Self { channels: vec![], seen_txs: vec![] } + } + /// Schedule a new channel. + /// + /// The channel will be created with `ScheduledChannelState::ChannelCreated` state. + pub fn schedule( + &mut self, channel_value_satoshi: bitcoin::Amount, counterparty_node_id: PublicKey, + channel_id: u128, + ) { + let channel = PayjoinChannel::new(channel_value_satoshi, counterparty_node_id, channel_id); + match channel.state { + ScheduledChannelState::ChannelCreated => { + self.channels.push(channel); + }, + _ => {}, + } + } + /// Mark a channel as accepted. + /// + /// The channel will be updated to `ScheduledChannelState::ChannelAccepted` state. + pub fn set_channel_accepted( + &mut self, channel_id: u128, output_script: &ScriptBuf, temporary_channel_id: [u8; 32], + ) -> bool { + for channel in &mut self.channels { + if channel.channel_id() == channel_id { + channel.state.set_channel_accepted(output_script, temporary_channel_id); + return true; + } + } + false + } + /// Mark a channel as funding tx created. + /// + /// The channel will be updated to `ScheduledChannelState::FundingTxCreated` state. + pub fn set_funding_tx_created( + &mut self, channel_id: u128, url: &payjoin::Url, body: Vec, + ) -> bool { + for channel in &mut self.channels { + if channel.channel_id() == channel_id { + return channel.state.set_channel_funding_tx_created(url.clone(), body); + } + } + false + } + /// Mark a channel as funding tx signed. + /// + /// The channel will be updated to `ScheduledChannelState::FundingTxSigned` state. + pub fn set_funding_tx_signed( + &mut self, tx: bitcoin::Transaction, + ) -> Option<(payjoin::Url, Vec)> { + for output in tx.output.iter() { + if let Some(mut channel) = self.internal_find_by_tx_out(&output.clone()) { + let info = channel.request_info(); + if info.is_some() && channel.state.set_channel_funding_tx_signed(output.clone()) { + return info; + } + } + } + None + } + /// Get the next channel matching the given channel amount. + /// + /// The channel must be in the accepted state. + /// + /// If more than one channel matches the given channel amount, the channel with the oldest + /// creation date will be returned. + pub fn get_next_channel( + &self, channel_amount: bitcoin::Amount, + ) -> Option<(u128, bitcoin::Address, [u8; 32], bitcoin::Amount, bitcoin::secp256k1::PublicKey)> + { + let channel = self + .channels + .iter() + .filter(|channel| { + channel.channel_value_satoshi() == channel_amount + && channel.is_channel_accepted() + && channel.output_script().is_some() + && channel.temporary_channel_id().is_some() + }) + .min_by_key(|channel| channel.created_at()); + + if let Some(channel) = channel { + let address = bitcoin::Address::from_script( + &channel.output_script().unwrap(), + bitcoin::Network::Regtest, // fixme + ); + if let Ok(address) = address { + return Some(( + channel.channel_id(), + address, + channel.temporary_channel_id().unwrap(), + channel.channel_value_satoshi(), + channel.counterparty_node_id(), + )); + } + }; + None + } + + /// List all channels. + pub fn list_channels(&self) -> &Vec { + &self.channels + } + + pub fn in_progress(&self) -> bool { + self.channels.iter().any(|channel| !channel.is_channel_accepted()) + } + + pub fn add_seen_tx(&mut self, tx: &Transaction) -> bool { + for input in tx.input.iter() { + for tx in self.seen_txs.clone() { + if tx.input.contains(&input) { + return false; + } + } + } + self.seen_txs.push(tx.clone()); + return true; + } + + // pub fn seen_outpoints(&self, tx: &bitcoin::Transaction) -> bool { + // tx.input.iter().any(|input| self.seen_outpoints_internal(&input.previous_output)) + // } + + // fn seen_outpoints_internal(&self, outpoint: &bitcoin::OutPoint) -> bool { + // self.seen_txs + // .iter() + // .any(|seen_tx| seen_tx.input.iter().any(|input| &input.previous_output == outpoint)) + // } + + fn internal_find_by_tx_out(&self, txout: &TxOut) -> Option { + let channel = self.channels.iter().find(|channel| { + return Some(&txout.script_pubkey) == channel.output_script(); + }); + channel.cloned() + } +} + +/// A struct representing a scheduled channel. +#[derive(Clone, Debug)] +pub struct PayjoinChannel { + state: ScheduledChannelState, + channel_value_satoshi: bitcoin::Amount, + channel_id: u128, + counterparty_node_id: PublicKey, + created_at: u64, +} + +impl PayjoinChannel { + pub fn new( + channel_value_satoshi: bitcoin::Amount, counterparty_node_id: PublicKey, channel_id: u128, + ) -> Self { + Self { + state: ScheduledChannelState::ChannelCreated, + channel_value_satoshi, + channel_id, + counterparty_node_id, + created_at: 0, + } + } + + fn is_channel_accepted(&self) -> bool { + match self.state { + ScheduledChannelState::ChannelAccepted(..) => true, + _ => false, + } + } + + pub fn channel_value_satoshi(&self) -> bitcoin::Amount { + self.channel_value_satoshi + } + + /// Get the user channel id. + pub fn channel_id(&self) -> u128 { + self.channel_id + } + + /// Get the counterparty node id. + pub fn counterparty_node_id(&self) -> PublicKey { + self.counterparty_node_id + } + + /// Get the output script. + pub fn output_script(&self) -> Option<&ScriptBuf> { + self.state.output_script() + } + + /// Get the temporary channel id. + pub fn temporary_channel_id(&self) -> Option<[u8; 32]> { + self.state.temporary_channel_id() + } + + /// Get the temporary channel id. + pub fn tx_out(&self) -> Option<&TxOut> { + match &self.state { + ScheduledChannelState::FundingTxSigned(_, txout) => Some(txout), + _ => None, + } + } + + pub fn request_info(&self) -> Option<(payjoin::Url, Vec)> { + match &self.state { + ScheduledChannelState::FundingTxCreated(_, url, body) => { + Some((url.clone(), body.clone())) + }, + _ => None, + } + } + + fn created_at(&self) -> u64 { + self.created_at + } +} + +#[derive(Clone, Debug)] +struct FundingTxParams { + output_script: ScriptBuf, + temporary_channel_id: [u8; 32], +} + +impl FundingTxParams { + fn new(output_script: ScriptBuf, temporary_channel_id: [u8; 32]) -> Self { + Self { output_script, temporary_channel_id } + } +} + +#[derive(Clone, Debug)] +enum ScheduledChannelState { + ChannelCreated, + ChannelAccepted(FundingTxParams), + FundingTxCreated(FundingTxParams, payjoin::Url, Vec), + FundingTxSigned(FundingTxParams, TxOut), +} + +impl ScheduledChannelState { + fn output_script(&self) -> Option<&ScriptBuf> { + match self { + ScheduledChannelState::ChannelAccepted(funding_tx_params) => { + Some(&funding_tx_params.output_script) + }, + ScheduledChannelState::FundingTxCreated(funding_tx_params, _, _) => { + Some(&funding_tx_params.output_script) + }, + ScheduledChannelState::FundingTxSigned(funding_tx_params, _) => { + Some(&funding_tx_params.output_script) + }, + _ => None, + } + } + + fn temporary_channel_id(&self) -> Option<[u8; 32]> { + match self { + ScheduledChannelState::ChannelAccepted(funding_tx_params) => { + Some(funding_tx_params.temporary_channel_id) + }, + ScheduledChannelState::FundingTxCreated(funding_tx_params, _, _) => { + Some(funding_tx_params.temporary_channel_id) + }, + ScheduledChannelState::FundingTxSigned(funding_tx_params, _) => { + Some(funding_tx_params.temporary_channel_id) + }, + _ => None, + } + } + + fn set_channel_accepted( + &mut self, output_script: &ScriptBuf, temporary_channel_id: [u8; 32], + ) -> bool { + if let ScheduledChannelState::ChannelCreated = self { + *self = ScheduledChannelState::ChannelAccepted(FundingTxParams::new( + output_script.clone(), + temporary_channel_id, + )); + return true; + } + return false; + } + + fn set_channel_funding_tx_created(&mut self, url: payjoin::Url, body: Vec) -> bool { + if let ScheduledChannelState::ChannelAccepted(funding_tx_params) = self { + *self = ScheduledChannelState::FundingTxCreated(funding_tx_params.clone(), url, body); + return true; + } + return false; + } + + fn set_channel_funding_tx_signed(&mut self, output: TxOut) -> bool { + let mut res = false; + if let ScheduledChannelState::FundingTxCreated(funding_tx_params, _, _) = self { + *self = + ScheduledChannelState::FundingTxSigned(funding_tx_params.clone(), output.clone()); + res = true; + } + return res; + } +} + +// #[cfg(test)] +// mod tests { +// use std::str::FromStr; + +// use super::*; +// use bitcoin::{ +// psbt::Psbt, +// secp256k1::{self, Secp256k1}, +// }; + +// #[ignore] +// #[test] +// fn test_channel_scheduler() { +// let create_pubkey = || -> PublicKey { +// let secp = Secp256k1::new(); +// PublicKey::from_secret_key(&secp, &secp256k1::SecretKey::from_slice(&[1; 32]).unwrap()) +// }; +// let channel_value_satoshi = 100; +// let node_id = create_pubkey(); +// let channel_id: u128 = 0; +// let mut channel_scheduler = PayjoinScheduler::new(); +// channel_scheduler.schedule( +// bitcoin::Amount::from_sat(channel_value_satoshi), +// node_id, +// channel_id, +// ); +// assert_eq!(channel_scheduler.channels.len(), 1); +// assert_eq!(channel_scheduler.is_channel_created(channel_id), true); +// channel_scheduler.set_channel_accepted( +// channel_id, +// &ScriptBuf::from(vec![1, 2, 3]), +// [0; 32], +// ); +// assert_eq!(channel_scheduler.is_channel_accepted(channel_id), true); +// let str_psbt = "cHNidP8BAHMCAAAAAY8nutGgJdyYGXWiBEb45Hoe9lWGbkxh/6bNiOJdCDuDAAAAAAD+////AtyVuAUAAAAAF6kUHehJ8GnSdBUOOv6ujXLrWmsJRDCHgIQeAAAAAAAXqRR3QJbbz0hnQ8IvQ0fptGn+votneofTAAAAAAEBIKgb1wUAAAAAF6kU3k4ekGHKWRNbA1rV5tR5kEVDVNCHAQcXFgAUx4pFclNVgo1WWAdN1SYNX8tphTABCGsCRzBEAiB8Q+A6dep+Rz92vhy26lT0AjZn4PRLi8Bf9qoB/CMk0wIgP/Rj2PWZ3gEjUkTlhDRNAQ0gXwTO7t9n+V14pZ6oljUBIQMVmsAaoNWHVMS02LfTSe0e388LNitPa1UQZyOihY+FFgABABYAFEb2Giu6c4KO5YW0pfw3lGp9jMUUAAA="; +// let mock_transaction = Psbt::from_str(str_psbt).unwrap(); +// let _our_txout = mock_transaction.clone().extract_tx().output[0].clone(); +// // channel_scheduler.set_funding_tx_created(channel_id, mock_transaction.clone()); +// // let tx_id = mock_transaction.extract_tx().txid(); +// // assert_eq!(channel_scheduler.is_funding_tx_created(&tx_id), true); +// // channel_scheduler.set_funding_tx_signed(tx_id); +// // assert_eq!(channel_scheduler.is_funding_tx_signed(&tx_id), true); +// } +// } diff --git a/src/payjoin_sender.rs b/src/payjoin_sender.rs new file mode 100644 index 000000000..8cb87bfae --- /dev/null +++ b/src/payjoin_sender.rs @@ -0,0 +1,194 @@ +/// An implementation of payjoin v2 sender as described in BIP-77. +use bdk::SignOptions; +use bitcoin::address::NetworkChecked; +use bitcoin::psbt::{Input, PartiallySignedTransaction, Psbt}; +use bitcoin::Txid; +use lightning::util::logger::Logger; +use lightning::{log_error, log_info}; +use payjoin::send::ContextV2; +use payjoin::Url; +use reqwest::header::{HeaderMap, HeaderValue}; +use reqwest::StatusCode; +use std::ops::Deref; +use std::sync::Arc; +use std::time::Instant; +use tokio::time::sleep; + +use crate::error::Error; +use crate::types::Wallet; + +pub(crate) struct PayjoinSender +where + L::Target: Logger, +{ + logger: L, + wallet: Arc, + payjoin_relay: Url, +} + +impl PayjoinSender +where + L::Target: Logger, +{ + pub(crate) fn new(logger: L, wallet: Arc, payjoin_relay: &Url) -> Self { + Self { logger, wallet, payjoin_relay: payjoin_relay.clone() } + } + + // Create payjoin request based on the payjoin URI parameters. This function builds a PSBT + // based on the amount and receiver address extracted from the payjoin URI, that can be used to + // send a payjoin request to the receiver using `PayjoinSender::send_payjoin_request`. + pub(crate) fn create_payjoin_request( + &self, payjoin_uri: payjoin::Uri<'static, NetworkChecked>, amount: Option, + ) -> Result { + let amount_to_send = match (amount, payjoin_uri.amount) { + (Some(amount), _) => amount, + (None, Some(amount)) => amount, + (None, None) => return Err(Error::PayjoinSender), + }; + let receiver_address = payjoin_uri.address.clone().script_pubkey(); + let mut sign_options = SignOptions::default(); + sign_options.trust_witness_utxo = true; + let original_psbt = self.wallet.build_transaction( + receiver_address, + amount_to_send.to_sat(), + sign_options, + )?; + Ok(original_psbt) + } + + pub(crate) fn extract_request_data( + &self, payjoin_uri: payjoin::Uri<'static, NetworkChecked>, original_psbt: Psbt, + ) -> Result<(payjoin::send::Request, ContextV2), Error> { + let mut request_context = + payjoin::send::RequestBuilder::from_psbt_and_uri(original_psbt.clone(), payjoin_uri) + .and_then(|b| b.build_non_incentivizing()) + .map_err(|e| { + dbg!(&e); + log_error!( + self.logger, + "Payjoin Sender: send: Error building payjoin request {}", + e + ); + Error::PayjoinSender + })?; + let (sender_request, sender_ctx) = + request_context.extract_v2(self.payjoin_relay.clone()).map_err(|e| { + dbg!(&e); + log_error!( + self.logger, + "Payjoin Sender: send: Error extracting payjoin request: {}", + e + ); + Error::PayjoinSender + })?; + Ok((sender_request, sender_ctx)) + } + + pub(crate) async fn poll( + &self, request: &payjoin::send::Request, time: Instant, + ) -> Option> { + let duration = std::time::Duration::from_secs(3600); + let sleep = || sleep(std::time::Duration::from_secs(10)); + loop { + if time.elapsed() > duration { + log_info!(self.logger, "Payjoin Sender: Polling timed out"); + return None; + } + let client = reqwest::Client::new(); + + let response = match client + .post(request.url.clone()) + .body(request.body.clone()) + .headers(ohttp_req_header()) + .send() + .await + { + Ok(response) => response, + Err(e) => { + log_info!(self.logger, "Payjoin Sender: Error polling request: {}", e); + sleep().await; + continue; + }, + }; + let response = match response.error_for_status() { + Ok(response) => response, + Err(e) => { + log_info!(self.logger, "Payjoin Sender: Status Error polling request: {}", e); + sleep().await; + continue; + }, + }; + + if response.status() == StatusCode::OK { + let response = match response.bytes().await { + Ok(response) => response.to_vec(), + Err(e) => { + log_info!( + self.logger, + "Payjoin Sender: Error reading polling response: {}", + e + ); + sleep().await; + continue; + }, + }; + if response.is_empty() { + log_info!(self.logger, "Payjoin Sender: Got empty response while polling"); + sleep().await; + continue; + } + return Some(response); + } else { + log_info!( + self.logger, + "Payjoin Sender: Error sending request, got status code + {}", + response.status() + ); + sleep().await; + continue; + } + } + } + + // finalise the payjoin transaction and broadcast it + pub(crate) fn finalise_payjoin_tx( + &self, mut psbt: Psbt, ocean_psbt: Psbt, + ) -> Result { + let mut ocean_psbt = ocean_psbt.clone(); + // for BDK, we need to reintroduce utxo from original psbt. + // Otherwise we wont be able to sign the transaction. + fn input_pairs( + psbt: &mut PartiallySignedTransaction, + ) -> Box + '_> { + Box::new(psbt.unsigned_tx.input.iter().zip(&mut psbt.inputs)) + } + + // get original inputs from original psbt clone (ocean_psbt) + let mut original_inputs = input_pairs(&mut ocean_psbt).peekable(); + for (proposed_txin, proposed_psbtin) in input_pairs(&mut psbt) { + if let Some((original_txin, original_psbtin)) = original_inputs.peek() { + if proposed_txin.previous_output == original_txin.previous_output { + proposed_psbtin.witness_utxo = original_psbtin.witness_utxo.clone(); + proposed_psbtin.non_witness_utxo = original_psbtin.non_witness_utxo.clone(); + original_inputs.next(); + } + } + } + + let mut sign_options = SignOptions::default(); + sign_options.trust_witness_utxo = true; + sign_options.try_finalize = true; + let (_is_signed, psbt) = self.wallet.sign_transaction(&psbt, sign_options)?; + let tx = psbt.extract_tx(); + self.wallet.broadcast_transaction(&tx); + let txid = tx.txid(); + Ok(txid) + } +} + +fn ohttp_req_header() -> HeaderMap { + let mut headers = HeaderMap::new(); + headers.insert(reqwest::header::CONTENT_TYPE, HeaderValue::from_static("message/ohttp-req")); + headers +} diff --git a/src/tx_broadcaster.rs b/src/tx_broadcaster.rs index 40483f578..559044f9e 100644 --- a/src/tx_broadcaster.rs +++ b/src/tx_broadcaster.rs @@ -1,6 +1,9 @@ use crate::logger::{log_bytes, log_debug, log_error, log_trace, Logger}; +use crate::payjoin_receiver::payjoin_receiver_request_headers; +use crate::payjoin_scheduler::PayjoinScheduler; use lightning::chain::chaininterface::BroadcasterInterface; +use lightning::log_info; use lightning::util::ser::Writeable; use esplora_client::AsyncClient as EsploraClient; @@ -11,6 +14,7 @@ use tokio::sync::mpsc; use tokio::sync::Mutex; use std::ops::Deref; +use std::sync::Arc; use std::time::Duration; const BCAST_PACKAGE_QUEUE_SIZE: usize = 50; @@ -23,21 +27,91 @@ where queue_receiver: Mutex>>, esplora_client: EsploraClient, logger: L, + payjoin_scheduler: Arc>, } impl TransactionBroadcaster where L::Target: Logger, { - pub(crate) fn new(esplora_client: EsploraClient, logger: L) -> Self { + pub(crate) fn new( + esplora_client: EsploraClient, logger: L, payjoin_scheduler: Arc>, + ) -> Self { let (queue_sender, queue_receiver) = mpsc::channel(BCAST_PACKAGE_QUEUE_SIZE); - Self { queue_sender, queue_receiver: Mutex::new(queue_receiver), esplora_client, logger } + Self { + queue_sender, + queue_receiver: Mutex::new(queue_receiver), + esplora_client, + logger, + payjoin_scheduler, + } } pub(crate) async fn process_queue(&self) { let mut receiver = self.queue_receiver.lock().await; while let Some(next_package) = receiver.recv().await { for tx in &next_package { + if tx.input.iter().any(|input| input.witness.is_empty()) { + log_info!( + self.logger, + "Skipping broadcast of transaction {} with empty witness, checking for payjoin.", + tx.txid() + ); + dbg!("Skipping broadcast of transaction {} with empty witness, checking for payjoin.", tx.txid()); + let is_payjoin_channel = + self.payjoin_scheduler.lock().await.set_funding_tx_signed(tx.clone()); + if let Some((url, body)) = is_payjoin_channel { + log_info!( + self.logger, + "Detected payjoin channel transaction. Sending payjoin sender request for transaction {}", + tx.txid() + ); + dbg!("Detected payjoin channel transaction. Sending payjoin sender request for transaction {}", tx.txid()); + + let headers = payjoin_receiver_request_headers(); + let client = match reqwest::Client::builder().build() { + Ok(client) => client, + Err(e) => { + log_error!( + self.logger, + "Failed to create reqwest client for payjoin receiver request: {}", + e + ); + continue; + }, + }; + match client.post(url).body(body).headers(headers).send().await { + Ok(res) => { + if res.status().is_success() { + log_info!( + self.logger, + "Successfully sent payjoin receiver request for transaction {}", + tx.txid() + ); + dbg!("Successfully sent payjoin receiver request for transaction {}", tx.txid()); + } else { + dbg!("Failed to send payjoin receiver request for transaction {}: {}", tx.txid(), res.status()); + log_error!( + self.logger, + "Failed to send payjoin receiver request for transaction {}: {}", + tx.txid(), + res.status() + ); + } + }, + Err(e) => { + dbg!("Failed to send payjoin receiver request for transaction {}: {}", tx.txid(), &e); + log_error!( + self.logger, + "Failed to send payjoin receiver request for transaction {}: {}", + tx.txid(), + e + ); + }, + } + continue; + } + } match self.esplora_client.broadcast(tx).await { Ok(()) => { log_trace!(self.logger, "Successfully broadcast transaction {}", tx.txid()); diff --git a/src/wallet.rs b/src/wallet.rs index 674cb6786..19ecff796 100644 --- a/src/wallet.rs +++ b/src/wallet.rs @@ -2,6 +2,7 @@ use crate::logger::{log_error, log_info, log_trace, Logger}; use crate::Error; +use bitcoin::psbt::Psbt; use lightning::chain::chaininterface::{BroadcasterInterface, ConfirmationTarget, FeeEstimator}; use lightning::ln::msgs::{DecodeError, UnsignedGossipMessage}; @@ -16,7 +17,7 @@ use lightning::util::message_signing; use bdk::blockchain::EsploraBlockchain; use bdk::database::BatchDatabase; use bdk::wallet::AddressIndex; -use bdk::FeeRate; +use bdk::{FeeRate, TransactionDetails}; use bdk::{SignOptions, SyncOptions}; use bitcoin::bech32::u5; @@ -24,7 +25,7 @@ use bitcoin::blockdata::locktime::absolute::LockTime; use bitcoin::secp256k1::ecdh::SharedSecret; use bitcoin::secp256k1::ecdsa::{RecoverableSignature, Signature}; use bitcoin::secp256k1::{PublicKey, Scalar, Secp256k1, SecretKey, Signing}; -use bitcoin::{ScriptBuf, Transaction, TxOut, Txid}; +use bitcoin::{bitcoinconsensus, ScriptBuf, Transaction, TxOut, Txid}; use std::ops::Deref; use std::sync::{Arc, Condvar, Mutex}; @@ -111,6 +112,128 @@ where res } + /// Returns the total value of all outputs in the given PSBT that + /// are directed to us in satoshis. + pub(crate) fn funds_directed_to_us(&self, tx: &Transaction) -> Result { + let locked_wallet = self.inner.lock().unwrap(); + let total_value = tx.output.iter().fold(0, |acc, output| { + match locked_wallet.is_mine(&output.script_pubkey) { + Ok(true) => acc + output.value, + _ => acc, + } + }); + Ok(bitcoin::Amount::from_sat(total_value)) + } + + pub(crate) fn broadcast_transaction(&self, transaction: &Transaction) { + self.broadcaster.broadcast_transactions(&[transaction]); + } + + pub(crate) fn build_transaction( + &self, output_script: ScriptBuf, value_sats: u64, sign_options: SignOptions, + ) -> Result { + let fee_rate = FeeRate::from_sat_per_kwu(1000 as f32); + + let locked_wallet = self.inner.lock().unwrap(); + let mut tx_builder = locked_wallet.build_tx(); + + tx_builder.add_recipient(output_script, value_sats).fee_rate(fee_rate).enable_rbf(); + + let mut psbt = match tx_builder.finish() { + Ok((psbt, _)) => { + log_trace!(self.logger, "Created PSBT: {:?}", psbt); + psbt + }, + Err(err) => { + log_error!(self.logger, "Failed to create PSBT: {}", err); + return Err(err.into()); + }, + }; + + locked_wallet.sign(&mut psbt, sign_options)?; + + Ok(psbt) + } + + pub(crate) fn is_mine(&self, script: &ScriptBuf) -> Result { + let locked_wallet = self.inner.lock().unwrap(); + Ok(locked_wallet.is_mine(script)?) + } + + pub(crate) fn list_transactions(&self) -> Result, Error> { + let locked_wallet = self.inner.lock().unwrap(); + Ok(locked_wallet.list_transactions(false)?) + } + + /// Verifies that the given transaction meets the bitcoin consensus rules. + pub async fn verify_tx(&self, tx: &Transaction) -> Result<(), Error> { + let serialized_tx = bitcoin::consensus::serialize(&tx); + // Loop through all the inputs + for (index, input) in tx.input.iter().enumerate() { + let input = input.clone(); + let txid = input.previous_output.txid; + let prev_tx = match self.blockchain.get_tx(&txid).await { + Ok(prev_tx) => prev_tx, + Err(e) => { + log_error!( + self.logger, + "Failed to verify transaction: blockchain error {} for txid {}", + e, + &txid + ); + return Err(Error::BitcoinConsensusFailed); + }, + }; + if let Some(prev_tx) = prev_tx { + let spent_output = match prev_tx.output.get(input.previous_output.vout as usize) { + Some(output) => output, + None => { + log_error!( + self.logger, + "Failed to verify transaction: missing output {} in tx {}", + input.previous_output.vout, + txid + ); + return Err(Error::BitcoinConsensusFailed); + }, + }; + match bitcoinconsensus::verify( + &spent_output.script_pubkey.to_bytes(), + spent_output.value, + &serialized_tx, + index, + ) { + Ok(()) => {}, + Err(e) => { + log_error!(self.logger, "Failed to verify transaction: {}", e); + return Err(Error::BitcoinConsensusFailed); + }, + } + } else { + if tx.is_coin_base() { + continue; + } else { + log_error!( + self.logger, + "Failed to verify transaction: missing previous transaction {}", + txid + ); + return Err(Error::BitcoinConsensusFailed); + } + } + } + Ok(()) + } + + pub(crate) fn sign_transaction( + &self, psbt: &Psbt, options: SignOptions, + ) -> Result<(bool, Psbt), Error> { + let wallet = self.inner.lock().unwrap(); + let mut psbt = psbt.clone(); + let is_signed = wallet.sign(&mut psbt, options)?; + Ok((is_signed, psbt)) + } + pub(crate) fn create_funding_transaction( &self, output_script: ScriptBuf, value_sats: u64, confirmation_target: ConfirmationTarget, locktime: LockTime, diff --git a/tests/common/mod.rs b/tests/common/mod.rs index bcb47accb..7c3fdb8a4 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -210,6 +210,22 @@ pub(crate) fn setup_two_nodes(electrsd: &ElectrsD, allow_0conf: bool) -> (TestNo (node_a, node_b) } +pub(crate) fn setup_two_payjoin_nodes( + electrsd: &ElectrsD, allow_0conf: bool, +) -> (TestNode, TestNode) { + println!("== Node A =="); + let config_a = random_config(); + let node_a_payjoin_receiver = setup_payjoin_receiver_node(electrsd, config_a); + + println!("\n== Node B =="); + let mut config_b = random_config(); + if allow_0conf { + config_b.trusted_peers_0conf.push(node_a_payjoin_receiver.node_id()); + } + let node_b_payjoin_sender = setup_payjoin_sender_node(electrsd, config_b); + (node_a_payjoin_receiver, node_b_payjoin_sender) +} + pub(crate) fn setup_node(electrsd: &ElectrsD, config: Config) -> TestNode { let esplora_url = format!("http://{}", electrsd.esplora_url.as_ref().unwrap()); setup_builder!(builder, config); @@ -222,6 +238,40 @@ pub(crate) fn setup_node(electrsd: &ElectrsD, config: Config) -> TestNode { node } +pub(crate) fn setup_payjoin_sender_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 = payjoin::Url::parse("https://pj.bobspacebkk.com").unwrap(); + builder.set_payjoin_sender_config(payjoin_relay); + 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 setup_payjoin_receiver_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_directory = payjoin::Url::parse("https://payjo.in").unwrap(); + let payjoin_relay = payjoin::Url::parse("https://pj.bobspacebkk.com").unwrap(); + let payjoin_ohttp_keys = [ + 1, 0, 32, 221, 207, 106, 162, 243, 25, 188, 252, 203, 135, 197, 199, 128, 63, 42, 243, 165, + 134, 237, 41, 143, 66, 243, 218, 152, 36, 239, 18, 139, 158, 40, 27, 0, 4, 0, 1, 0, 3, + ]; + let payjoin_ohttp_keys = payjoin::OhttpKeys::decode(payjoin_ohttp_keys.as_slice()).unwrap(); + builder.set_payjoin_receiver_config(payjoin_directory, payjoin_relay, Some(payjoin_ohttp_keys)); + 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..51b9bc081 --- /dev/null +++ b/tests/integration_tests_payjoin.rs @@ -0,0 +1,46 @@ +mod common; + +use crate::common::{ + generate_blocks_and_wait, premine_and_distribute_funds, setup_two_payjoin_nodes, wait_for_tx, +}; +use bitcoin::Amount; +use common::setup_bitcoind_and_electrsd; + +#[test] +fn send_receive_regular_payjoin_transaction() { + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let (node_a_pj_receiver, node_b_pj_sender) = setup_two_payjoin_nodes(&electrsd, false); + let addr_b = node_b_pj_sender.onchain_payment().new_address().unwrap(); + let premine_amount_sat = 100_000_00; + premine_and_distribute_funds( + &bitcoind.client, + &electrsd.client, + vec![addr_b], + Amount::from_sat(premine_amount_sat), + ); + node_a_pj_receiver.sync_wallets().unwrap(); + node_b_pj_sender.sync_wallets().unwrap(); + assert_eq!(node_b_pj_sender.list_balances().spendable_onchain_balance_sats, premine_amount_sat); + assert_eq!(node_a_pj_receiver.list_balances().spendable_onchain_balance_sats, 0); + assert_eq!(node_a_pj_receiver.next_event(), None); + assert_eq!(node_a_pj_receiver.list_channels().len(), 0); + let payjoin_uri = node_a_pj_receiver.request_payjoin_transaction(80_000).unwrap(); + let txid = tokio::runtime::Runtime::new().unwrap().handle().block_on(async { + let txid = node_b_pj_sender + .send_payjoin_transaction( + payjoin::Uri::try_from(payjoin_uri.to_string()).unwrap().assume_checked(), + None, + ) + .await; + txid + }); + dbg!(&txid); + wait_for_tx(&electrsd.client, txid.unwrap().unwrap()); + generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6); + node_a_pj_receiver.sync_wallets().unwrap(); + node_b_pj_sender.sync_wallets().unwrap(); + let node_a_balance = node_a_pj_receiver.list_balances(); + let node_b_balance = node_b_pj_sender.list_balances(); + assert_eq!(node_a_balance.total_onchain_balance_sats, 80000); + assert!(node_b_balance.total_onchain_balance_sats < premine_amount_sat - 80000); +} diff --git a/tests/integration_tests_payjoin_lightning.rs b/tests/integration_tests_payjoin_lightning.rs new file mode 100644 index 000000000..d904a098c --- /dev/null +++ b/tests/integration_tests_payjoin_lightning.rs @@ -0,0 +1,84 @@ +mod common; +use std::{thread::sleep, time::Duration}; + +use crate::common::{ + expect_channel_pending_event, expect_channel_ready_event, generate_blocks_and_wait, + premine_and_distribute_funds, setup_two_payjoin_nodes, wait_for_tx, +}; +use bitcoin::Amount; +use common::setup_bitcoind_and_electrsd; +use ldk_node::Event; + +#[test] +fn send_receive_with_channel_opening_payjoin_transaction() { + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let (node_a, node_b) = setup_two_payjoin_nodes(&electrsd, false); + let addr_b = node_b.onchain_payment().new_address().unwrap(); + let premine_amount_sat = 100_000_00; + premine_and_distribute_funds( + &bitcoind.client, + &electrsd.client, + vec![addr_b], + Amount::from_sat(premine_amount_sat), + ); + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + assert_eq!(node_a.list_balances().spendable_onchain_balance_sats, 0); + assert_eq!(node_b.list_balances().spendable_onchain_balance_sats, premine_amount_sat); + assert_eq!(node_a.next_event(), None); + assert_eq!(node_a.list_channels().len(), 0); + assert_eq!(node_b.next_event(), None); + assert_eq!(node_b.list_channels().len(), 0); + let funding_amount_sat = 80_000; + let node_b_listening_address = node_b.listening_addresses().unwrap().get(0).unwrap().clone(); + let payjoin_uri = node_a.request_payjoin_transaction_with_channel_opening( + funding_amount_sat, + None, + false, + node_b.node_id(), + node_b_listening_address, + ); + let payjoin_uri = match payjoin_uri { + Ok(payjoin_uri) => payjoin_uri, + Err(e) => { + dbg!(&e); + assert!(false); + panic!("should generate payjoin uri"); + }, + }; + + let _ = tokio::runtime::Runtime::new().unwrap().handle().block_on(async { + let txid = node_b + .send_payjoin_transaction( + payjoin::Uri::try_from(payjoin_uri.to_string()).unwrap().assume_checked(), + None, + ) + .await; + txid + }); + expect_channel_pending_event!(node_a, node_b.node_id()); + expect_channel_pending_event!(node_b, node_a.node_id()); + let channels = node_a.list_channels(); + let channel = channels.get(0).unwrap(); + wait_for_tx(&electrsd.client, channel.funding_txo.unwrap().txid); + sleep(Duration::from_secs(1)); + generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6); + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + expect_channel_ready_event!(node_a, node_b.node_id()); + expect_channel_ready_event!(node_b, node_a.node_id()); + let channels = node_a.list_channels(); + let channel = channels.get(0).unwrap(); + assert_eq!(channel.channel_value_sats, funding_amount_sat); + assert_eq!(channel.confirmations.unwrap(), 6); + assert!(channel.is_channel_ready); + assert!(channel.is_usable); + + assert_eq!(node_a.list_peers().get(0).unwrap().is_connected, true); + assert_eq!(node_a.list_peers().get(0).unwrap().is_persisted, true); + assert_eq!(node_a.list_peers().get(0).unwrap().node_id, node_b.node_id()); + + let invoice_amount_1_msat = 2500_000; + let invoice = node_b.bolt11_payment().receive(invoice_amount_1_msat, "test", 1000).unwrap(); + assert!(node_a.bolt11_payment().send(&invoice).is_ok()); +}