diff --git a/Cargo.toml b/Cargo.toml index d4a87b2a2..aa43bca67 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,23 +28,23 @@ 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 = { 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" } -#lightning-persister = { git = "https://github.com/lightningdevkit/rust-lightning", branch="main" } -#lightning-background-processor = { git = "https://github.com/lightningdevkit/rust-lightning", branch="main", features = ["futures"] } -#lightning-rapid-gossip-sync = { git = "https://github.com/lightningdevkit/rust-lightning", branch="main" } -#lightning-transaction-sync = { git = "https://github.com/lightningdevkit/rust-lightning", branch="main", features = ["esplora-async"] } -#lightning-liquidity = { git = "https://github.com/lightningdevkit/lightning-liquidity", branch="main", 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.git", branch="0.0.123-with-funding-brodsafe-event", features = ["std"] } +lightning-invoice = { git = "https://github.com/jbesraa/rust-lightning.git", branch="0.0.123-with-funding-brodsafe-event" } +lightning-net-tokio = { git = "https://github.com/jbesraa/rust-lightning.git", branch="0.0.123-with-funding-brodsafe-event" } +lightning-persister = { git = "https://github.com/jbesraa/rust-lightning.git", branch="0.0.123-with-funding-brodsafe-event" } +lightning-background-processor = { git = "https://github.com/jbesraa/rust-lightning.git", branch="0.0.123-with-funding-brodsafe-event", features = ["futures"] } +lightning-rapid-gossip-sync = { git = "https://github.com/jbesraa/rust-lightning.git", branch="0.0.123-with-funding-brodsafe-event" } +lightning-transaction-sync = { git = "https://github.com/jbesraa/rust-lightning.git", branch="0.0.123-with-funding-brodsafe-event", features = ["esplora-async"] } +lightning-liquidity = { git = "https://github.com/jbesraa/lightning-liquidity", branch="pj-fixes", features = ["std"] } #lightning = { path = "../rust-lightning/lightning", features = ["std"] } #lightning-invoice = { path = "../rust-lightning/lightning-invoice" } @@ -68,6 +68,7 @@ tokio = { version = "1.37", default-features = false, features = [ "rt-multi-thr esplora-client = { version = "0.6", default-features = false } libc = "0.2" uniffi = { version = "0.26.0", features = ["build"], optional = true } +payjoin = { version = "0.16.0", default-features = false, features = ["send", "receive", "v2"] } [target.'cfg(vss)'.dependencies] vss-client = "0.2" @@ -77,8 +78,8 @@ 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 = { git = "https://github.com/lightningdevkit/rust-lightning", branch="main", features = ["std", "_test_utils"] } +# lightning = { version = "0.0.123", features = ["std", "_test_utils"] } +lightning = { git = "https://github.com/jbesraa/rust-lightning.git", branch="0.0.123-with-funding-brodsafe-event", 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" @@ -101,3 +102,8 @@ panic = "abort" [profile.dev] panic = "abort" + +[[example]] +name = "ldk-node-with-payjoin-support" +path = "examples/ldk-node-with-payjoin-support.rs" + diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index 2723db573..f08c1161e 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -63,6 +63,7 @@ interface Node { Bolt12Payment bolt12_payment(); SpontaneousPayment spontaneous_payment(); OnchainPayment onchain_payment(); + PayjoinPayment payjoin_payment(); [Throws=NodeError] void connect(PublicKey node_id, SocketAddress address, boolean persist); [Throws=NodeError] @@ -148,6 +149,13 @@ interface OnchainPayment { Txid send_all_to_address([ByRef]Address address); }; +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", @@ -196,6 +204,14 @@ enum NodeError { "InsufficientFunds", "LiquiditySourceUnavailable", "LiquidityFeeTooHigh", + "PayjoinUnavailable", + "PayjoinUriInvalid", + "PayjoinRequestMissingAmount", + "PayjoinRequestCreationFailed", + "PayjoinResponseProcessingFailed", + "PayjoinReceiverUnavailable", + "PayjoinReceiverRequestValidationFailed", + "PayjoinReceiverEnrollementFailed" }; dictionary NodeStatus { @@ -227,6 +243,7 @@ enum BuildError { "KVStoreSetupFailed", "WalletSetupFailed", "LoggerSetupFailed", + "InvalidPayjoinConfig", }; [Enum] @@ -238,6 +255,9 @@ 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, ScriptBuf receipient); + PayjoinPaymentSuccess(Txid txid, u64 amount, ScriptBuf receipient); + PayjoinPaymentFailed(Txid? txid, u64 amount, ScriptBuf receipient, PayjoinPaymentFailureReason reason); }; enum PaymentFailureReason { @@ -249,6 +269,13 @@ enum PaymentFailureReason { "UnexpectedError", }; +enum PayjoinPaymentFailureReason { + "Timeout", + "TransactionFinalisationFailed", + "InvalidReceiverResponse", +}; + + [Enum] interface ClosureReason { CounterpartyForceClosed(UntrustedString peer_msg); @@ -274,6 +301,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 PaymentDirection { @@ -499,3 +527,6 @@ typedef string Mnemonic; [Custom] typedef string UntrustedString; + +[Custom] +typedef string ScriptBuf; diff --git a/examples/ldk-node-with-payjoin-support.rs b/examples/ldk-node-with-payjoin-support.rs new file mode 100644 index 000000000..627f172b6 --- /dev/null +++ b/examples/ldk-node-with-payjoin-support.rs @@ -0,0 +1,71 @@ +use ldk_node::bitcoin::Network; +use ldk_node::{Builder, LogLevel}; + +fn main() { + let mut builder = Builder::new(); + builder.set_log_level(LogLevel::Gossip); + builder.set_network(Network::Testnet); + builder.set_esplora_server("https://blockstream.info/testnet/api".to_string()); + builder.set_gossip_source_rgs( + "https://rapidsync.lightningdevkit.org/testnet/snapshot".to_string(), + ); + + // Payjoin directory is needed only if you are setting up Payjoin receiver, + // not required for Payjoin sender. + let payjoin_directory = "https://payjo.in".to_string(); + // Payjoin relay is required for both Payjoin receiver and sender. + let payjoin_relay = "https://pj.bobspacebkk.com".to_string(); + + // Enable sending payjoin transactions + // builder.set_payjoin_sender_config(payjoin_relay.clone()); + // ohttp keys refer to the Payjoin directory keys that are needed for the Payjoin receiver + // enrollement. If those keys are not provided the node will attempt to fetch them for you. + // let ohttp_keys = None; + // Enable receiving payjoin transactions + builder.set_payjoin_config(payjoin_directory, payjoin_relay); + + let node = builder.build().unwrap(); + + node.start().unwrap(); + + // Receiving payjoin transaction + let payjoin_payment = node.payjoin_payment(); + let amount_to_receive = bitcoin::Amount::from_sat(1000); + let payjoin_uri = payjoin_payment.receive(amount_to_receive).unwrap(); + let payjoin_uri = payjoin_uri.to_string(); + + println!("Payjoin URI: {}", payjoin_uri); + + //** Open a channel from incoming payjoin transactions ***// + // let payjoin_payment = node.payjoin_payment(); + // let channel_amount_sats = bitcoin::Amount::from_sat(10000); + // use bitcoin::secp256k1::PublicKey; + // use lightning::ln::msgs::SocketAddress; + // let counterparty_node_id: PublicKey = unimplemented!(); + // let counterparty_address: SocketAddress = unimplemented!(); + // let payjoin_uri = match payjoin_payment.receive_with_channel_opening(channel_amount_sats, None, true, + // counterparty_node_id, counterparty_address, + // ).await { + // Ok(a) => a, + // Err(e) => { + // panic!("{}", e); + // }, + // }; + // let payjoin_uri = payjoin_uri.to_string(); + // println!("Payjoin URI: {}", payjoin_uri); + + //** Sending payjoin transaction **// + // let payjoin_uri = payjoin::Uri::try_from(payjoin_uri).unwrap(); + // match payjoin_payment.send(payjoin_uri, None, None).await { + // Ok(Some(txid)) => { + // dbg!("Sent transaction and got a response. Transaction completed") + // }, + // Ok(None) => { + // dbg!("Sent transaction and got no response. We will keep polling the response for the next 24hours") + // }, + // Err(e) => { + // dbg!(e); + // } + // } + node.stop().unwrap(); +} diff --git a/src/builder.rs b/src/builder.rs index a2a93aa79..ce456d4c2 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -11,6 +11,8 @@ 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::PayjoinReceiver; +use crate::payment::payjoin::handler::PayjoinHandler; use crate::payment::store::PaymentStore; use crate::peer_store::PeerStore; use crate::tx_broadcaster::TransactionBroadcaster; @@ -93,6 +95,13 @@ struct LiquiditySourceConfig { lsps2_service: Option<(SocketAddress, PublicKey, Option)>, } +#[derive(Debug, Clone)] +struct PayjoinConfig { + payjoin_directory: payjoin::Url, + payjoin_relay: payjoin::Url, + ohttp_keys: Option, +} + impl Default for LiquiditySourceConfig { fn default() -> Self { Self { lsps2_service: None } @@ -132,6 +141,8 @@ pub enum BuildError { WalletSetupFailed, /// We failed to setup the logger. LoggerSetupFailed, + /// Invalid Payjoin configuration. + InvalidPayjoinConfig, } impl fmt::Display for BuildError { @@ -152,6 +163,10 @@ impl fmt::Display for BuildError { Self::KVStoreSetupFailed => write!(f, "Failed to setup KVStore."), Self::WalletSetupFailed => write!(f, "Failed to setup onchain wallet."), Self::LoggerSetupFailed => write!(f, "Failed to setup the logger."), + Self::InvalidPayjoinConfig => write!( + f, + "Invalid Payjoin configuration. Make sure the provided arguments are valid URLs." + ), } } } @@ -172,6 +187,7 @@ pub struct NodeBuilder { chain_data_source_config: Option, gossip_source_config: Option, liquidity_source_config: Option, + payjoin_config: Option, } impl NodeBuilder { @@ -187,12 +203,14 @@ impl NodeBuilder { let chain_data_source_config = None; let gossip_source_config = None; let liquidity_source_config = None; + let payjoin_config = None; Self { config, entropy_source_config, chain_data_source_config, gossip_source_config, liquidity_source_config, + payjoin_config, } } @@ -247,6 +265,27 @@ impl NodeBuilder { self } + /// Configures the [`Node`] instance to enable payjoin transactions. + pub fn set_payjoin_config( + &mut self, payjoin_directory: String, payjoin_relay: String, ohttp_keys: Option, + ) -> Result<&mut Self, BuildError> { + let payjoin_relay = + payjoin::Url::parse(&payjoin_relay).map_err(|_| BuildError::InvalidPayjoinConfig)?; + let payjoin_directory = payjoin::Url::parse(&payjoin_directory) + .map_err(|_| BuildError::InvalidPayjoinConfig)?; + let ohttp_keys = if let Some(ohttp_keys) = ohttp_keys { + let keys = match payjoin::OhttpKeys::decode(ohttp_keys.as_bytes()) { + Ok(keys) => keys, + Err(_) => return Err(BuildError::InvalidPayjoinConfig), + }; + Some(keys) + } else { + None + }; + self.payjoin_config = Some(PayjoinConfig { payjoin_directory, payjoin_relay, ohttp_keys }); + Ok(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. @@ -365,6 +404,7 @@ impl NodeBuilder { self.chain_data_source_config.as_ref(), self.gossip_source_config.as_ref(), self.liquidity_source_config.as_ref(), + self.payjoin_config.as_ref(), seed_bytes, logger, vss_store, @@ -386,6 +426,7 @@ impl NodeBuilder { self.chain_data_source_config.as_ref(), self.gossip_source_config.as_ref(), self.liquidity_source_config.as_ref(), + self.payjoin_config.as_ref(), seed_bytes, logger, kv_store, @@ -453,6 +494,11 @@ impl ArcedNodeBuilder { self.inner.write().unwrap().set_gossip_source_p2p(); } + /// Configures the [`Node`] instance to enable payjoin transactions. + pub fn set_payjoin_config(&self, payjoin_relay: String) -> Result<(), BuildError> { + self.inner.write().unwrap().set_payjoin_config(payjoin_relay).map(|_| ()) + } + /// 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) { @@ -521,8 +567,9 @@ impl ArcedNodeBuilder { fn build_with_store_internal( config: Arc, chain_data_source_config: Option<&ChainDataSourceConfig>, gossip_source_config: Option<&GossipSourceConfig>, - liquidity_source_config: Option<&LiquiditySourceConfig>, seed_bytes: [u8; 64], - logger: Arc, kv_store: Arc, + liquidity_source_config: Option<&LiquiditySourceConfig>, + payjoin_config: Option<&PayjoinConfig>, seed_bytes: [u8; 64], logger: Arc, + kv_store: Arc, ) -> Result { // Initialize the on-chain wallet and chain access let xprv = bitcoin::bip32::ExtendedPrivKey::new_master(config.network.into(), &seed_bytes) @@ -966,6 +1013,26 @@ fn build_with_store_internal( let (stop_sender, _) = tokio::sync::watch::channel(()); let (event_handling_stopped_sender, _) = tokio::sync::watch::channel(()); + let mut payjoin_handler = None; + let mut payjoin_receiver = None; + if let Some(pj_config) = payjoin_config { + payjoin_handler = Some(Arc::new(PayjoinHandler::new( + pj_config.payjoin_relay.clone(), + Arc::clone(&tx_sync), + Arc::clone(&event_queue), + Arc::clone(&wallet), + Arc::clone(&payment_store), + ))); + payjoin_receiver = Some(Arc::new(PayjoinReceiver::new( + Arc::clone(&logger), + Arc::clone(&wallet), + Arc::clone(&channel_manager), + Arc::clone(&config), + pj_config.payjoin_directory.clone(), + pj_config.payjoin_relay.clone(), + pj_config.ohttp_keys.clone(), + ))); + } 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)); @@ -987,6 +1054,8 @@ fn build_with_store_internal( channel_manager, chain_monitor, output_sweeper, + payjoin_handler, + payjoin_receiver, peer_manager, connection_manager, keys_manager, diff --git a/src/config.rs b/src/config.rs index d0e72080f..3d4cb6e5e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -40,6 +40,15 @@ pub(crate) const RESOLVED_CHANNEL_MONITOR_ARCHIVAL_INTERVAL: u32 = 6; // The time in-between peer reconnection attempts. pub(crate) const PEER_RECONNECTION_INTERVAL: Duration = Duration::from_secs(10); +// The time before payjoin sender requests timeout. +pub(crate) const PAYJOIN_REQUEST_TIMEOUT: Duration = Duration::from_secs(30); + +// The time before payjoin sender try to send the next request. +pub(crate) const PAYJOIN_RETRY_INTERVAL: Duration = Duration::from_secs(3); + +// The total time payjoin sender try to send a request. +pub(crate) const PAYJOIN_REQUEST_TOTAL_DURATION: Duration = Duration::from_secs(24 * 60 * 60); + // The time in-between RGS sync attempts. pub(crate) const RGS_SYNC_INTERVAL: Duration = Duration::from_secs(60 * 60); diff --git a/src/error.rs b/src/error.rs index a8671d9a7..5510a51ee 100644 --- a/src/error.rs +++ b/src/error.rs @@ -95,6 +95,22 @@ pub enum Error { LiquiditySourceUnavailable, /// The given operation failed due to the LSP's required opening fee being too high. LiquidityFeeTooHigh, + /// Failed to access Payjoin sender object. + PayjoinUnavailable, + /// Payjoin URI is invalid. + PayjoinUriInvalid, + /// Amount is neither user-provided nor defined in the URI. + PayjoinRequestMissingAmount, + /// Failed to build a Payjoin request. + PayjoinRequestCreationFailed, + /// Payjoin response processing failed. + PayjoinResponseProcessingFailed, + /// Failed to access payjoin receiver object. + PayjoinReceiverUnavailable, + /// Failed to enroll payjoin receiver. + PayjoinReceiverEnrollementFailed, + /// Failed to validate an incoming payjoin request. + PayjoinReceiverRequestValidationFailed, } impl fmt::Display for Error { @@ -162,6 +178,30 @@ 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::PayjoinUnavailable => { + write!(f, "Failed to access Payjoin sender object. Make sure you have enabled Payjoin sending support.") + }, + Self::PayjoinRequestMissingAmount => { + write!(f, "Amount is neither user-provided nor defined in the URI.") + }, + Self::PayjoinRequestCreationFailed => { + write!(f, "Failed construct a Payjoin request") + }, + Self::PayjoinUriInvalid => { + write!(f, "The provided Payjoin URI is invalid") + }, + Self::PayjoinResponseProcessingFailed => { + write!(f, "Payjoin receiver responded to our request with an invalid response that was ignored") + }, + Self::PayjoinReceiverUnavailable => { + write!(f, "Failed to access payjoin receiver object. Make sure you have enabled Payjoin receiving support.") + }, + Self::PayjoinReceiverRequestValidationFailed => { + write!(f, "Failed to validate an incoming payjoin request. Payjoin sender request didnt pass the payjoin validation steps.") + }, + Self::PayjoinReceiverEnrollementFailed => { + write!(f, "Failed to enroll payjoin receiver. Make sure the configured Payjoin directory & Payjoin relay are available.") + }, } } } @@ -182,3 +222,9 @@ impl From for Error { Self::TxSyncFailed } } + +impl From for Error { + fn from(_e: reqwest::Error) -> Self { + Self::PayjoinRequestCreationFailed + } +} diff --git a/src/event.rs b/src/event.rs index 838df4230..bb04d55f8 100644 --- a/src/event.rs +++ b/src/event.rs @@ -1,3 +1,4 @@ +use crate::payjoin_receiver::PayjoinReceiver; use crate::types::{DynStore, Sweeper, Wallet}; use crate::{ @@ -24,6 +25,7 @@ use lightning::events::{ClosureReason, PaymentPurpose}; use lightning::events::{Event as LdkEvent, PaymentFailureReason}; use lightning::impl_writeable_tlv_based_enum; use lightning::ln::channelmanager::PaymentId; +use lightning::ln::msgs::DecodeError; use lightning::ln::{ChannelId, PaymentHash}; use lightning::routing::gossip::NodeId; use lightning::util::errors::APIError; @@ -143,6 +145,73 @@ pub enum Event { /// This will be `None` for events serialized by LDK Node v0.2.1 and prior. reason: Option, }, + /// Failed to send Payjoin transaction. + /// + /// This event is emitted when our attempt to send Payjoin transaction fail. + PayjoinPaymentPending { + /// Transaction ID of the successfully sent Payjoin transaction. + txid: bitcoin::Txid, + /// docs + amount: u64, + /// docs + receipient: bitcoin::ScriptBuf, + }, + /// A Payjoin transaction has been successfully sent. + /// + /// This event is emitted when we send a Payjoin transaction and it was accepted by the + /// receiver, and then finalised and broadcasted by us. + PayjoinPaymentSuccess { + /// Transaction ID of the successfully sent Payjoin transaction. + txid: bitcoin::Txid, + /// docs + amount: u64, + /// docs + receipient: bitcoin::ScriptBuf, + }, + /// Failed to send Payjoin transaction. + /// + /// This event is emitted when our attempt to send Payjoin transaction fail. + PayjoinPaymentFailed { + /// Transaction ID of the successfully sent Payjoin transaction. + txid: Option, + /// docs + amount: u64, + /// docs + receipient: bitcoin::ScriptBuf, + /// Reason for the failure. + reason: PayjoinPaymentFailureReason, + }, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum PayjoinPaymentFailureReason { + Timeout, + TransactionFinalisationFailed, + InvalidReceiverResponse, + RequestFailed, +} + +impl Readable for PayjoinPaymentFailureReason { + fn read(reader: &mut R) -> Result { + match u8::read(reader)? { + 0 => Ok(Self::Timeout), + 1 => Ok(Self::TransactionFinalisationFailed), + 2 => Ok(Self::InvalidReceiverResponse), + 3 => Ok(Self::RequestFailed), + _ => Err(DecodeError::InvalidValue), + } + } +} + +impl Writeable for PayjoinPaymentFailureReason { + fn write(&self, writer: &mut W) -> Result<(), std::io::Error> { + match *self { + Self::Timeout => 0u8.write(writer), + Self::TransactionFinalisationFailed => 1u8.write(writer), + Self::InvalidReceiverResponse => 2u8.write(writer), + Self::RequestFailed => 3u8.write(writer), + } + } } impl_writeable_tlv_based_enum!(Event, @@ -184,6 +253,22 @@ impl_writeable_tlv_based_enum!(Event, (2, payment_id, required), (4, claimable_amount_msat, required), (6, claim_deadline, option), + }, + (7, PayjoinPaymentPending) => { + (0, txid, required), + (2, amount, required), + (4, receipient, required), + }, + (8, PayjoinPaymentSuccess) => { + (0, txid, required), + (2, amount, required), + (4, receipient, required), + }, + (9, PayjoinPaymentFailed) => { + (0, amount, required), + (1, txid, option), + (2, receipient, required), + (4, reason, required), }; ); @@ -354,6 +439,7 @@ where network_graph: Arc, payment_store: Arc>, peer_store: Arc>, + payjoin_receiver: Option>, runtime: Arc>>, logger: L, config: Arc, @@ -368,8 +454,9 @@ where bump_tx_event_handler: Arc, channel_manager: Arc, connection_manager: Arc>, output_sweeper: Arc, network_graph: Arc, - payment_store: Arc>, peer_store: Arc>, - runtime: Arc>>, logger: L, config: Arc, + payment_store: Arc>, payjoin_receiver: Option>, + peer_store: Arc>, runtime: Arc>>, + logger: L, config: Arc, ) -> Self { Self { event_queue, @@ -380,6 +467,7 @@ where output_sweeper, network_graph, payment_store, + payjoin_receiver, peer_store, logger, runtime, @@ -394,6 +482,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 @@ -404,6 +493,18 @@ 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, @@ -1066,6 +1167,45 @@ where ); } }, + LdkEvent::FundingTxBroadcastSafe { funding_tx, .. } => { + use crate::io::utils::ohttp_headers; + if let Some(payjoin_receiver) = self.payjoin_receiver.clone() { + let is_payjoin_channel = + payjoin_receiver.set_funding_tx_signed(funding_tx.clone()).await; + if let Some((url, body)) = is_payjoin_channel { + log_info!( + self.logger, + "Detected payjoin channel transaction. Sending payjoin sender request for transaction {}", + funding_tx.txid() + ); + let headers = ohttp_headers(); + let client = reqwest::Client::builder().build().unwrap(); + match client.post(url).body(body).headers(headers).send().await { + Ok(response) => { + if response.status().is_success() { + log_info!( + self.logger, + "Responded to 'Payjoin Sender' successfuly" + ); + } else { + log_info!( + self.logger, + "Got unsuccessful response from 'Payjoin Sender': {}", + response.status() + ); + } + }, + Err(e) => { + log_error!( + self.logger, + "Failed to send a response to 'Payjoin Sender': {}", + e + ); + }, + }; + } + } + }, LdkEvent::ChannelPending { channel_id, user_channel_id, diff --git a/src/io/utils.rs b/src/io/utils.rs index 77cc56f55..d298318f5 100644 --- a/src/io/utils.rs +++ b/src/io/utils.rs @@ -511,6 +511,15 @@ pub(crate) fn check_namespace_key_validity( Ok(()) } +pub(crate) 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 +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/lib.rs b/src/lib.rs index de2a0badf..71d2f0953 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -89,6 +89,8 @@ pub mod io; mod liquidity; mod logger; mod message_handler; +mod payjoin_channel_scheduler; +mod payjoin_receiver; pub mod payment; mod peer_store; mod sweep; @@ -109,6 +111,8 @@ pub use error::Error as NodeError; use error::Error; pub use event::Event; +use payjoin_receiver::PayjoinReceiver; +use payment::payjoin::handler::PayjoinHandler; pub use types::ChannelConfig; pub use io::utils::generate_entropy_mnemonic; @@ -133,7 +137,10 @@ use gossip::GossipSource; use graph::NetworkGraph; use liquidity::LiquiditySource; use payment::store::PaymentStore; -use payment::{Bolt11Payment, Bolt12Payment, OnchainPayment, PaymentDetails, SpontaneousPayment}; +use payment::{ + Bolt11Payment, Bolt12Payment, OnchainPayment, PayjoinPayment, PaymentDetails, + SpontaneousPayment, +}; use peer_store::{PeerInfo, PeerStore}; use types::{ Broadcaster, BumpTransactionEventHandler, ChainMonitor, ChannelManager, DynStore, FeeEstimator, @@ -185,6 +192,8 @@ pub struct Node { output_sweeper: Arc, peer_manager: Arc, connection_manager: Arc>>, + payjoin_handler: Option>, + payjoin_receiver: Option>, keys_manager: Arc, network_graph: Arc, gossip_source: Arc, @@ -365,6 +374,10 @@ 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 = match &self.payjoin_handler { + Some(pjh) => Some(Arc::clone(pjh)), + None => None, + }; 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(); @@ -384,11 +397,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 { @@ -685,6 +701,30 @@ impl Node { Arc::clone(&self.logger), )); + // Check every 5 seconds if we have received a payjoin transaction to our enrolled + // subdirectory with the configured Payjoin directory. + 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 = 5; + 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; + } + } + } + }); + } + let event_handler = Arc::new(EventHandler::new( Arc::clone(&self.event_queue), Arc::clone(&self.wallet), @@ -694,6 +734,7 @@ impl Node { Arc::clone(&self.output_sweeper), Arc::clone(&self.network_graph), Arc::clone(&self.payment_store), + self.payjoin_receiver.clone(), Arc::clone(&self.peer_store), Arc::clone(&self.runtime), Arc::clone(&self.logger), @@ -1063,6 +1104,56 @@ impl Node { )) } + /// Returns a payment handler allowing to send payjoin payments. + /// + /// In order to utilize the Payjoin functionality, it's necessary + /// to configure your node 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(); + let payjoin_receiver = self.payjoin_receiver.as_ref(); + PayjoinPayment::new( + Arc::clone(&self.runtime), + payjoin_handler.map(Arc::clone), + payjoin_receiver.map(Arc::clone), + Arc::clone(&self.config), + Arc::clone(&self.logger), + Arc::clone(&self.wallet), + Arc::clone(&self.tx_broadcaster), + Arc::clone(&self.peer_store), + Arc::clone(&self.channel_manager), + Arc::clone(&self.connection_manager), + Arc::clone(&self.payment_store), + ) + } + + /// Returns a payment handler allowing to send payjoin payments. + /// + /// In order to utilize the Payjoin functionality, it's necessary + /// to configure your node 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(); + let payjoin_receiver = self.payjoin_receiver.as_ref(); + Arc::new(PayjoinPayment::new( + Arc::clone(&self.runtime), + payjoin_handler.map(Arc::clone), + payjoin_receiver.map(Arc::clone), + Arc::clone(&self.config), + Arc::clone(&self.logger), + Arc::clone(&self.wallet), + Arc::clone(&self.tx_broadcaster), + Arc::clone(&self.peer_store), + Arc::clone(&self.channel_manager), + Arc::clone(&self.connection_manager), + Arc::clone(&self.payment_store), + )) + } + /// Retrieve a list of known channels. pub fn list_channels(&self) -> Vec { self.channel_manager.list_channels().into_iter().map(|c| c.into()).collect() @@ -1264,11 +1355,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/src/payjoin_channel_scheduler.rs b/src/payjoin_channel_scheduler.rs new file mode 100644 index 000000000..6f30e3252 --- /dev/null +++ b/src/payjoin_channel_scheduler.rs @@ -0,0 +1,251 @@ +use bitcoin::{secp256k1::PublicKey, Network, ScriptBuf, TxOut}; + +#[derive(Clone)] +pub struct PayjoinChannelScheduler { + channels: Vec, +} + +impl PayjoinChannelScheduler { + pub(crate) fn new() -> Self { + Self { channels: vec![] } + } + + pub(crate) 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); + }, + _ => {}, + } + } + + pub(crate) 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 + } + + pub(crate) 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 + } + + pub(crate) 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 accepted state. + /// + /// If more than one channel matches the given channel amount, the channel with the oldest + /// creation date will be returned. + pub(crate) fn get_next_channel( + &self, channel_amount: bitcoin::Amount, network: Network, + ) -> 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(), network); + 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 + } + + 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() + } +} + +#[derive(Clone, Debug)] +pub(crate) struct PayjoinChannel { + state: ScheduledChannelState, + channel_value_satoshi: bitcoin::Amount, + channel_id: u128, + counterparty_node_id: PublicKey, + created_at: u64, +} + +impl PayjoinChannel { + pub(crate) 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(crate) fn channel_value_satoshi(&self) -> bitcoin::Amount { + self.channel_value_satoshi + } + + pub(crate) fn channel_id(&self) -> u128 { + self.channel_id + } + + pub(crate) fn counterparty_node_id(&self) -> PublicKey { + self.counterparty_node_id + } + + pub(crate) fn output_script(&self) -> Option<&ScriptBuf> { + self.state.output_script() + } + + pub(crate) fn temporary_channel_id(&self) -> Option<[u8; 32]> { + self.state.temporary_channel_id() + } + + pub(crate) 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, ()), +} + +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 { + assert_eq!(funding_tx_params.output_script, output.script_pubkey); + *self = ScheduledChannelState::FundingTxSigned(funding_tx_params.clone(), ()); + res = true; + } + return res; + } +} diff --git a/src/payjoin_receiver.rs b/src/payjoin_receiver.rs new file mode 100644 index 000000000..c1ef51061 --- /dev/null +++ b/src/payjoin_receiver.rs @@ -0,0 +1,540 @@ +use crate::error::Error; +use crate::io::utils::ohttp_headers; +use crate::logger::FilesystemLogger; +use crate::payjoin_channel_scheduler::{PayjoinChannel, PayjoinChannelScheduler}; +use crate::types::{ChannelManager, Wallet}; +use crate::Config; +use bitcoin::{ScriptBuf, Transaction}; +use lightning::ln::ChannelId; +use lightning::log_info; +use lightning::util::logger::Logger; +use payjoin::receive::v2::{Enrolled, Enroller, ProvisionalProposal, UncheckedProposal}; +use payjoin::{OhttpKeys, PjUriBuilder}; +use payjoin::{PjUri, Url}; +use std::ops::Deref; +use std::sync::Arc; +use tokio::sync::RwLock; + +/// Implements Payjoin protocol as specified in [BIP77] +/// +/// [BIP77]: https://github.com/bitcoin/bips/blob/3b863a402e0250658985f08a455a6cd103e269e5/bip-0077.mediawiki +pub(crate) struct PayjoinReceiver { + logger: Arc, + wallet: Arc, + channel_manager: Arc, + channel_scheduler: RwLock, + /// Directory receiver wish to enroll with + payjoin_directory: Url, + /// Proxy server receiver wish to make requests through + payjoin_relay: Url, + /// Enrollement object indicates a successful enrollement if is defined. + enrolled: RwLock>, + /// Directory keys. + /// + /// Optional as they can be fetched on behalf of the user if not provided. + /// They are required in order to enroll. + ohttp_keys: RwLock>, + config: Arc, +} + +impl PayjoinReceiver { + pub(crate) fn new( + logger: Arc, wallet: Arc, channel_manager: Arc, + config: Arc, payjoin_directory: payjoin::Url, payjoin_relay: payjoin::Url, + ohttp_keys: Option, + ) -> Self { + Self { + logger, + wallet, + channel_manager, + channel_scheduler: RwLock::new(PayjoinChannelScheduler::new()), + config, + payjoin_directory, + payjoin_relay, + enrolled: RwLock::new(None), + ohttp_keys: RwLock::new(ohttp_keys), + } + } + + /// Before receiving Payjoin transactions we `enroll` with a Payjoin directory + /// and we acquire a subdirectory we can receive Payjoin transactions to while offline. + /// + /// This function returns [BIP21] URI with Payjoin parameters. + /// + /// [BIP21]: https://github.com/bitcoin/bips/blob/master/bip-0021.mediawiki + pub(crate) async fn receive(&self, amount: bitcoin::Amount) -> Result { + if !self.is_enrolled().await { + self.enroll().await?; + } + let enrolled = self.enrolled.read().await; + let enrolled = match enrolled.as_ref() { + Some(enrolled) => enrolled, + None => { + log_info!(self.logger, "Payjoin Receiver: Not enrolled"); + return Err(Error::PayjoinReceiverUnavailable); + }, + }; + let fallback_target = enrolled.fallback_target(); + let ohttp_keys = self.ohttp_keys.read().await; + let ohttp_keys = match ohttp_keys.as_ref() { + Some(okeys) => okeys, + None => { + log_info!(self.logger, "Payjoin Receiver: No ohttp keys"); + return Err(Error::PayjoinReceiverUnavailable); + }, + }; + let address = self.wallet.get_new_address()?; + let pj_part = match payjoin::Url::parse(&fallback_target) { + Ok(pj_part) => pj_part, + Err(_) => { + log_info!(self.logger, "Payjoin Receiver: Invalid fallback target"); + return Err(Error::PayjoinReceiverUnavailable); + }, + }; + let payjoin_uri = + PjUriBuilder::new(address, pj_part, Some(ohttp_keys.clone())).amount(amount).build(); + Ok(payjoin_uri) + } + + pub(crate) async fn set_channel_accepted( + &self, channel_id: u128, output_script: &ScriptBuf, temporary_channel_id: [u8; 32], + ) -> bool { + let mut scheduler = self.channel_scheduler.write().await; + scheduler.set_channel_accepted(channel_id, output_script, temporary_channel_id) + } + + /// After enrolling, we should periodacly check if we have received any Payjoin transactions. + /// + /// This function will try to fetch pending Payjoin requests from the subdirectory, and if a + /// successful response received, we validate the request as specified in [BIP78]. After + /// validation we check if we have a pending matching channel, and if so, we try fund the channel + /// with the incoming funds from the payjoin request. Otherwise, we accept the Payjoin request + /// normally by trying to preserve privacy, finalise the Payjoin proposal and send it back the + /// the Payjoin sender. + /// + /// [BIP78]: https://github.com/bitcoin/bips/blob/master/bip-0078.mediawiki#user-content-Receivers_original_PSBT_checklist + pub(crate) async fn process_payjoin_request(&self) { + let mut enrolled = self.enrolled.write().await; + if let Some(mut enrolled) = enrolled.take() { + let (req, context) = match enrolled.extract_req() { + Ok(req) => req, + Err(e) => { + log_info!( + self.logger, + "Payjoin Receiver: Unable to extract enrollement request and context{}", + e + ); + return; + }, + }; + + let client = reqwest::Client::new(); + let response = match client + .post(req.url.to_string()) + .body(req.body) + .headers(ohttp_headers()) + .send() + .await + { + Ok(response) => response, + Err(e) => { + log_info!( + self.logger, + "Payjoin Receiver: Unable to fetch payjoin request {}", + e + ); + return; + }, + }; + if response.status() != reqwest::StatusCode::OK { + log_info!( + self.logger, + "Payjoin Receiver: Got non-200 response from directory server {}", + response.status() + ); + return; + }; + let response = match response.bytes().await { + Ok(response) => response, + Err(e) => { + log_info!(self.logger, "Payjoin Receiver: Error reading response {}", e); + return; + }, + }; + if response.is_empty() { + log_info!(self.logger, "Payjoin Receiver: Empty response from directory server"); + return; + }; + let response = match enrolled.process_res(response.to_vec().as_slice(), context) { + Ok(response) => response, + Err(e) => { + log_info!( + self.logger, + "Payjoin Receiver: Unable to process payjoin request {}", + e + ); + return; + }, + }; + let unchecked_proposal = match response { + Some(proposal) => proposal, + None => { + return; + }, + }; + let original_tx = unchecked_proposal.extract_tx_to_schedule_broadcast(); + let provisional_proposal = match self.validate_payjoin_request(unchecked_proposal).await + { + Ok(proposal) => proposal, + Err(e) => { + log_info!(self.logger, "Payjoin Validation: {}", e); + return; + }, + }; + let amount = match self.wallet.funds_directed_to_us(&original_tx) { + Ok(a) => a, + Err(e) => { + // This should not happen in practice as the validation checks would fail if + // the sender didnt include us in the outputs + log_info!(self.logger, "Not able to find any ouput directed to us: {}", e); + return; + }, + }; + let mut scheduler = self.channel_scheduler.write().await; + let network = self.config.network; + if let Some(channel) = scheduler.get_next_channel(amount, network) { + log_info!(self.logger, "Found a channel match for incoming Payjoin request"); + let (channel_id, funding_tx_address, temporary_channel_id, _, counterparty_node_id) = + channel; + let mut channel_provisional_proposal = provisional_proposal.clone(); + channel_provisional_proposal.substitute_output_address(funding_tx_address); + let payjoin_proposal = match channel_provisional_proposal + .finalize_proposal(|psbt| Ok(psbt.clone()), None) + { + Ok(proposal) => proposal, + Err(e) => { + dbg!(&e); + return; + }, + }; + let (receiver_request, _) = match payjoin_proposal.clone().extract_v2_req() { + Ok((req, ctx)) => (req, ctx), + Err(e) => { + dbg!(&e); + return; + }, + }; + let tx = payjoin_proposal.psbt().clone().extract_tx(); + scheduler.set_funding_tx_created( + channel_id, + &receiver_request.url, + receiver_request.body, + ); + match self.channel_manager.unsafe_manual_funding_transaction_generated( + &ChannelId::from_bytes(temporary_channel_id), + &counterparty_node_id, + tx.clone(), + ) { + Ok(_) => { + // Created Funding Transaction and waiting for `FundingTxBroadcastSafe` event before returning a response + log_info!(self.logger, "Created channel funding transaction from Payjoin request and waiting for `FundingTxBroadcastSafe`"); + }, + Err(_) => { + log_info!( + self.logger, + "Unable to channel create funding tx from Payjoin request" + ); + }, + } + } else { + log_info!( + self.logger, + "Couldnt match a channel to Payjoin request, accepting normally" + ); + self.accept_payjoin_transaction(provisional_proposal).await; + } + } else { + log_info!(self.logger, "Payjoin Receiver: Unable to get enrolled object"); + } + } + + async fn accept_payjoin_transaction(&self, mut provisional_proposal: ProvisionalProposal) { + // Preserve privacy + let (candidate_inputs, utxo_set) = match self.wallet.payjoin_receiver_candidate_input() { + Ok(a) => a, + Err(e) => { + log_info!(self.logger, "Didnt find candidate inputs: {}", e); + return; + }, + }; + match provisional_proposal.try_preserving_privacy(candidate_inputs) { + Ok(selected_outpoint) => { + if let Some(selected_utxo) = utxo_set.iter().find(|i| { + i.outpoint.txid == selected_outpoint.txid + && i.outpoint.vout == selected_outpoint.vout + }) { + let txo_to_contribute = bitcoin::TxOut { + value: selected_utxo.txout.value, + script_pubkey: selected_utxo.txout.script_pubkey.clone(), + }; + let outpoint_to_contribute = bitcoin::OutPoint { + txid: selected_utxo.outpoint.txid, + vout: selected_utxo.outpoint.vout, + }; + provisional_proposal + .contribute_witness_input(txo_to_contribute, outpoint_to_contribute); + } + }, + Err(_) => { + log_info!(self.logger, "Failed to select utxos to improve payjoin request privacy. Payjoin proceeds regardless"); + }, + }; + // Finalise Payjoin Proposal + let mut payjoin_proposal = match provisional_proposal.finalize_proposal( + |psbt| { + self.wallet.prepare_payjoin_proposal(psbt.clone()).map_err(|e| { + log_info!(self.logger, "Payjoin Receiver: Unable to sign proposal {}", e); + payjoin::Error::Server(e.into()) + }) + }, + None, + ) { + Ok(proposal) => proposal, + Err(e) => { + log_info!(self.logger, "Payjoin Receiver: Unable to finalize proposal {}", e); + return; + }, + }; + + let (receiver_request, _) = match payjoin_proposal.extract_v2_req() { + Ok(req) => req, + Err(e) => { + log_info!(self.logger, "Payjoin Receiver: Unable to extract V2 request {}", e); + return; + }, + }; + // Send Payjoin Proposal response back to Payjoin sender + match reqwest::Client::new() + .post(&receiver_request.url.to_string()) + .body(receiver_request.body) + .headers(ohttp_headers()) + .send() + .await + { + Ok(response) => { + if response.status() == reqwest::StatusCode::OK { + log_info!(self.logger, "Payjoin Receiver: Payjoin response sent to sender"); + } else { + log_info!( + self.logger, + "Payjoin Receiver: Got non-200 response from directory {}", + response.status() + ); + } + }, + Err(e) => { + log_info!( + self.logger, + "Payjoin Receiver: Unable to make request to directory {}", + e + ); + }, + }; + } + + /// Enrolls a Payjoin receiver with the specified Payjoin directory. + /// + /// If directory `ohttp_keys` are not provided, they will be fetched from the directory using + /// the Payjoin relay as proxy to improve privacy and not expose users IP address. + async fn enroll(&self) -> Result<(), Error> { + let ohttp_keys = match self.ohttp_keys.read().await.deref() { + Some(okeys) => okeys.clone(), + None => { + let payjoin_directory = &self.payjoin_directory; + let payjoin_directory = match payjoin_directory.join("/ohttp-keys") { + Ok(payjoin_directory) => payjoin_directory, + Err(e) => { + log_info!( + self.logger, + "Payjoin Receiver: Unable to construct ohttp keys url {}", + e + ); + return Err(Error::PayjoinReceiverEnrollementFailed); + }, + }; + let proxy = match reqwest::Proxy::all(self.payjoin_relay.to_string()) { + Ok(proxy) => proxy, + Err(e) => { + log_info!( + self.logger, + "Payjoin Receiver: Unable to construct reqwest proxy {}", + e + ); + return Err(Error::PayjoinReceiverEnrollementFailed); + }, + }; + let client = match reqwest::Client::builder().proxy(proxy).build() { + Ok(client) => client, + Err(e) => { + log_info!( + self.logger, + "Payjoin Receiver: Unable to construct reqwest client {}", + e + ); + return Err(Error::PayjoinReceiverEnrollementFailed); + }, + }; + let response = match client.get(payjoin_directory).send().await { + Ok(response) => response, + Err(e) => { + log_info!( + self.logger, + "Payjoin Receiver: Unable to make request to fetch ohttp keys {}", + e + ); + return Err(Error::PayjoinReceiverEnrollementFailed); + }, + }; + if response.status() != reqwest::StatusCode::OK { + log_info!( + self.logger, + "Payjoin Receiver: Got non 200 response when fetching ohttp keys {}", + response.status() + ); + return Err(Error::PayjoinReceiverEnrollementFailed); + } + let response = match response.bytes().await { + Ok(response) => response, + Err(e) => { + log_info!( + self.logger, + "Payjoin Receiver: Error reading ohttp keys response {}", + e + ); + return Err(Error::PayjoinReceiverEnrollementFailed); + }, + }; + OhttpKeys::decode(response.to_vec().as_slice()).map_err(|e| { + log_info!(self.logger, "Payjoin Receiver: Unable to decode ohttp keys {}", e); + Error::PayjoinReceiverEnrollementFailed + })? + }, + }; + let mut enroller = Enroller::from_directory_config( + self.payjoin_directory.clone(), + ohttp_keys.clone(), + self.payjoin_relay.clone(), + ); + let (req, ctx) = match enroller.extract_req() { + Ok(req) => req, + Err(e) => { + log_info!( + self.logger, + "Payjoin Receiver: unable to extract enrollement request {}", + e + ); + return Err(Error::PayjoinReceiverEnrollementFailed); + }, + }; + let response = match reqwest::Client::new() + .post(&req.url.to_string()) + .body(req.body) + .headers(ohttp_headers()) + .send() + .await + { + Ok(response) => response, + Err(_) => { + log_info!(self.logger, "Payjoin Receiver: unable to make enrollement request"); + return Err(Error::PayjoinReceiverEnrollementFailed); + }, + }; + let response = match response.bytes().await { + Ok(response) => response, + Err(_) => { + panic!("Error reading response"); + }, + }; + let enrolled = match enroller.process_res(response.to_vec().as_slice(), ctx) { + Ok(enrolled) => enrolled, + Err(e) => { + log_info!( + self.logger, + "Payjoin Receiver: unable to process enrollement response {}", + e + ); + return Err(Error::PayjoinReceiverEnrollementFailed); + }, + }; + + *self.ohttp_keys.write().await = Some(ohttp_keys); + *self.enrolled.write().await = Some(enrolled); + Ok(()) + } + + async fn is_enrolled(&self) -> bool { + self.enrolled.read().await.deref().is_some() + && self.ohttp_keys.read().await.deref().is_some() + } + + /// Schedule a channel to opened upon receiving a Payjoin tranasction value with the same + /// channel funding amount. + pub(crate) async fn schedule_channel( + &self, amount: bitcoin::Amount, counterparty_node_id: bitcoin::secp256k1::PublicKey, + channel_id: u128, + ) { + let channel = PayjoinChannel::new(amount, counterparty_node_id, channel_id); + self.channel_scheduler.write().await.schedule( + channel.channel_value_satoshi(), + channel.counterparty_node_id(), + channel.channel_id(), + ); + } + + /// This should only be called upon receiving [`Event::FundingTxBroadcastSafe`] + /// + /// [`Event::FundingTxBroadcastSafe`]: lightning::events::Event::FundingTxBroadcastSafe + pub(crate) async fn set_funding_tx_signed( + &self, funding_tx: Transaction, + ) -> Option<(payjoin::Url, Vec)> { + self.channel_scheduler.write().await.set_funding_tx_signed(funding_tx) + } + + /// Validate an incoming Payjoin request as specified in [BIP78]. + /// + /// [BIP78]: https://github.com/bitcoin/bips/blob/master/bip-0078.mediawiki#user-content-Receivers_original_PSBT_checklist + async fn validate_payjoin_request( + &self, proposal: UncheckedProposal, + ) -> Result { + let wallet = &self.wallet; + let proposal = proposal.assume_interactive_receiver(); + let proposal = proposal + .check_inputs_not_owned(|script| { + Ok(wallet.is_mine(&script.to_owned()).unwrap_or(false)) + }) + .map_err(|e| { + log_info!(self.logger, "Inputs owned by us check failed {}", e); + Error::PayjoinReceiverRequestValidationFailed + })?; + let proposal = proposal.check_no_mixed_input_scripts().map_err(|e| { + log_info!(self.logger, "Mixed input scripts check failed {}", e); + Error::PayjoinReceiverRequestValidationFailed + })?; + // Fixme: discuss how to handle this, instead of the Ok(false) we should have a way to + // store seen outpoints and check against them + let proposal = + proposal.check_no_inputs_seen_before(|_outpoint| Ok(false)).map_err(|e| { + log_info!(self.logger, "Inputs seen before check failed {}", e); + Error::PayjoinReceiverRequestValidationFailed + })?; + let provisional_proposal = proposal + .identify_receiver_outputs(|script| { + Ok(wallet.is_mine(&script.to_owned()).unwrap_or(false)) + }) + .map_err(|e| { + log_info!(self.logger, "Identify receiver outputs failed {}", e); + Error::PayjoinReceiverRequestValidationFailed + })?; + Ok(provisional_proposal) + } +} diff --git a/src/payment/mod.rs b/src/payment/mod.rs index 1862bf2df..aa681a67d 100644 --- a/src/payment/mod.rs +++ b/src/payment/mod.rs @@ -3,9 +3,11 @@ mod bolt11; mod bolt12; mod onchain; +pub(crate) mod payjoin; mod spontaneous; pub(crate) mod store; +pub use self::payjoin::PayjoinPayment; pub use bolt11::Bolt11Payment; pub use bolt12::Bolt12Payment; pub use onchain::OnchainPayment; diff --git a/src/payment/payjoin/handler.rs b/src/payment/payjoin/handler.rs new file mode 100644 index 000000000..d9956ad12 --- /dev/null +++ b/src/payment/payjoin/handler.rs @@ -0,0 +1,288 @@ +use lightning::ln::channelmanager::PaymentId; + +use crate::config::PAYJOIN_REQUEST_TIMEOUT; +use crate::error::Error; +use crate::event::PayjoinPaymentFailureReason; +use crate::io::utils::ohttp_headers; +use crate::payment::store::PaymentDetailsUpdate; +use crate::payment::PaymentStatus; +use crate::types::{ChainSource, EventQueue, PaymentStore, Wallet}; +use crate::Event; + +use bitcoin::address::NetworkChecked; +use bitcoin::block::Header; +use bitcoin::psbt::Psbt; +use bitcoin::{Address, Amount, BlockHash, Script, Transaction, Txid}; +use lightning::chain::channelmonitor::ANTI_REORG_DELAY; +use lightning::chain::transaction::TransactionData; +use lightning::chain::{Filter, WatchedOutput}; + +use std::sync::{Arc, RwLock}; + +#[derive(Clone, Debug)] +enum PayjoinTransaction { + PendingFirstConfirmation { + original_psbt: Psbt, + tx: Transaction, + receiver: Address, + amount: Amount, + }, + PendingThresholdConfirmations { + original_psbt: Psbt, + tx: Transaction, + receiver: Address, + amount: Amount, + first_confirmation_height: u32, + first_confirmation_hash: BlockHash, + }, +} + +impl PayjoinTransaction { + fn txid(&self) -> Option { + match self { + PayjoinTransaction::PendingFirstConfirmation { tx, .. } => Some(tx.txid()), + PayjoinTransaction::PendingThresholdConfirmations { tx, .. } => Some(tx.txid()), + } + } + fn original_psbt(&self) -> &Psbt { + match self { + PayjoinTransaction::PendingFirstConfirmation { original_psbt, .. } => original_psbt, + PayjoinTransaction::PendingThresholdConfirmations { original_psbt, .. } => { + original_psbt + }, + } + } + fn first_confirmation_height(&self) -> Option { + match self { + PayjoinTransaction::PendingFirstConfirmation { .. } => None, + PayjoinTransaction::PendingThresholdConfirmations { + first_confirmation_height, .. + } => Some(*first_confirmation_height), + } + } + fn amount(&self) -> Amount { + match self { + PayjoinTransaction::PendingFirstConfirmation { amount, .. } => *amount, + PayjoinTransaction::PendingThresholdConfirmations { amount, .. } => *amount, + } + } + fn receiver(&self) -> Address { + match self { + PayjoinTransaction::PendingFirstConfirmation { receiver, .. } => receiver.clone(), + PayjoinTransaction::PendingThresholdConfirmations { receiver, .. } => receiver.clone(), + } + } +} + +pub(crate) struct PayjoinHandler { + payjoin_relay: payjoin::Url, + chain_source: Arc, + transactions: RwLock>, + event_queue: Arc, + wallet: Arc, + payment_store: Arc, +} + +impl PayjoinHandler { + pub(crate) fn new( + payjoin_relay: payjoin::Url, chain_source: Arc, event_queue: Arc, + wallet: Arc, payment_store: Arc, + ) -> Self { + Self { + payjoin_relay, + transactions: RwLock::new(Vec::new()), + chain_source, + event_queue, + wallet, + payment_store, + } + } + + pub(crate) async fn send_request( + &self, payjoin_uri: payjoin::Uri<'_, NetworkChecked>, original_psbt: &mut Psbt, + ) -> Result, Error> { + let (request, context) = payjoin::send::RequestBuilder::from_psbt_and_uri( + original_psbt.clone(), + payjoin_uri.clone(), + ) + .and_then(|b| b.build_non_incentivizing()) + .and_then(|mut c| c.extract_v2(self.payjoin_relay.clone())) + .map_err(|_e| Error::PayjoinRequestCreationFailed)?; + let response = reqwest::Client::new() + .post(request.url.clone()) + .body(request.body.clone()) + .timeout(PAYJOIN_REQUEST_TIMEOUT) + .headers(ohttp_headers()) + .send() + .await?; + let response = response.error_for_status()?; + let response = response.bytes().await?; + let response = response.to_vec(); + context + .process_response(&mut response.as_slice()) + .map_err(|_e| Error::PayjoinResponseProcessingFailed) + } + + pub(crate) fn handle_request_failure( + &self, payjoin_uri: payjoin::Uri, original_psbt: &Psbt, + ) -> Result<(), Error> { + self.event_queue.add_event(Event::PayjoinPaymentFailed { + txid: Some(original_psbt.unsigned_tx.txid()), + receipient: payjoin_uri.address.clone().into(), + amount: payjoin_uri.amount.unwrap().to_sat(), + reason: PayjoinPaymentFailureReason::RequestFailed, + }) + } + + pub(crate) fn handle_request_timeout( + &self, payjoin_uri: payjoin::Uri, original_psbt: &Psbt, + ) -> Result<(), Error> { + self.event_queue.add_event(Event::PayjoinPaymentFailed { + txid: Some(original_psbt.unsigned_tx.txid()), + receipient: payjoin_uri.address.clone().into(), + amount: payjoin_uri.amount.unwrap().to_sat(), + reason: PayjoinPaymentFailureReason::Timeout, + }) + } + + pub(crate) fn process_response( + &self, payjoin_proposal: &mut Psbt, original_psbt: &mut Psbt, + payjoin_uri: payjoin::Uri, + ) -> Result { + let wallet = self.wallet.clone(); + wallet.sign_payjoin_proposal(payjoin_proposal, original_psbt)?; + let tx = payjoin_proposal.clone().extract_tx(); + let our_input = + tx.output.iter().find(|output| wallet.is_mine(&output.script_pubkey).unwrap_or(false)); + if let Some(our_input) = our_input { + self.transactions.write().unwrap().push(PayjoinTransaction::PendingFirstConfirmation { + original_psbt: original_psbt.clone(), + tx: tx.clone(), + receiver: payjoin_uri.address.clone(), + amount: payjoin_uri.amount.unwrap_or_default(), + }); + let txid = tx.txid(); + self.register_tx(&txid, &our_input.script_pubkey); + self.event_queue.add_event(Event::PayjoinPaymentPending { + txid, + amount: payjoin_uri.amount.unwrap_or_default().to_sat(), + receipient: payjoin_uri.address.clone().into(), + })?; + Ok(tx) + } else { + self.event_queue.add_event(Event::PayjoinPaymentFailed { + txid: None, + amount: payjoin_uri.amount.unwrap_or_default().to_sat(), + receipient: payjoin_uri.address.clone().into(), + reason: PayjoinPaymentFailureReason::TransactionFinalisationFailed, + })?; + Err(Error::PayjoinReceiverRequestValidationFailed) // fixeror + } + } + + fn internal_transactions_confirmed( + &self, header: &Header, txdata: &TransactionData, height: u32, + ) { + let (_, tx) = txdata[0]; + let confirmed_tx_txid = tx.txid(); + let mut transactions = self.transactions.write().unwrap(); + let position = match transactions.iter().position(|o| o.txid() == Some(confirmed_tx_txid)) { + Some(position) => position, + None => { + return; + }, + }; + let pj_tx = transactions.remove(position); + match pj_tx { + PayjoinTransaction::PendingFirstConfirmation { + ref tx, + receiver, + amount, + original_psbt, + } => { + transactions.push(PayjoinTransaction::PendingThresholdConfirmations { + original_psbt, + tx: tx.clone(), + receiver, + amount, + first_confirmation_height: height, + first_confirmation_hash: header.block_hash(), + }); + }, + _ => { + unreachable!() + }, + }; + } + + fn internal_get_relevant_txids(&self) -> Vec<(Txid, u32, Option)> { + let state_lock = self.transactions.read().unwrap(); + state_lock + .iter() + .filter_map(|o| match o { + PayjoinTransaction::PendingThresholdConfirmations { + tx, + first_confirmation_height, + first_confirmation_hash, + .. + } => Some(( + tx.clone().txid(), + first_confirmation_height.clone(), + Some(first_confirmation_hash.clone()), + )), + _ => None, + }) + .collect::>() + } + + fn internal_best_block_updated(&self, height: u32) { + let mut transactions = self.transactions.write().unwrap(); + transactions.retain(|tx| { + if let (Some(first_conf), Some(txid)) = (tx.first_confirmation_height(), tx.txid()) { + if height - first_conf >= ANTI_REORG_DELAY { + let payment_id: [u8; 32] = + tx.original_psbt().unsigned_tx.txid()[..].try_into().unwrap(); + let mut update_details = PaymentDetailsUpdate::new(PaymentId(payment_id)); + update_details.status = Some(PaymentStatus::Succeeded); + let _ = self.payment_store.update(&update_details); + let _ = self.event_queue.add_event(Event::PayjoinPaymentSuccess { + txid, + amount: tx.amount().to_sat(), + receipient: tx.receiver().into(), + }); + false + } else { + true + } + } else { + true + } + }); + } +} + +impl Filter for PayjoinHandler { + fn register_tx(&self, txid: &Txid, script_pubkey: &Script) { + self.chain_source.register_tx(txid, script_pubkey); + } + + fn register_output(&self, output: WatchedOutput) { + self.chain_source.register_output(output); + } +} + +impl lightning::chain::Confirm for PayjoinHandler { + fn transactions_confirmed(&self, header: &Header, txdata: &TransactionData, height: u32) { + self.internal_transactions_confirmed(header, txdata, height); + } + + fn transaction_unconfirmed(&self, _txid: &Txid) {} + + fn best_block_updated(&self, _header: &Header, height: u32) { + self.internal_best_block_updated(height); + } + + fn get_relevant_txids(&self) -> Vec<(Txid, u32, Option)> { + self.internal_get_relevant_txids() + } +} diff --git a/src/payment/payjoin/mod.rs b/src/payment/payjoin/mod.rs new file mode 100644 index 000000000..3c71f61d1 --- /dev/null +++ b/src/payment/payjoin/mod.rs @@ -0,0 +1,318 @@ +//! Holds a payment handler allowing to send Payjoin payments. + +use lightning::chain::chaininterface::BroadcasterInterface; +use lightning::ln::channelmanager::PaymentId; +use lightning::log_error; + +use crate::config::{PAYJOIN_REQUEST_TOTAL_DURATION, PAYJOIN_RETRY_INTERVAL}; +use crate::logger::{FilesystemLogger, Logger}; +use crate::types::{Broadcaster, ChannelManager, PaymentStore, Wallet}; +use bitcoin::secp256k1::PublicKey; +use lightning::ln::msgs::SocketAddress; +use lightning::util::config::{ChannelHandshakeConfig, UserConfig}; +use payjoin::PjUri; + +use crate::connection::ConnectionManager; +use crate::payjoin_receiver::PayjoinReceiver; +use crate::peer_store::{PeerInfo, PeerStore}; +use crate::{error::Error, Config}; + +use std::sync::{Arc, RwLock}; + +pub(crate) mod handler; + +use handler::PayjoinHandler; + +use super::{PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus}; + +/// A payment handler allowing to send Payjoin payments. +/// +/// Payjoin transactions can be used to improve privacy by breaking the common-input-ownership +/// heuristic when Payjoin receivers contribute input(s) to the transaction. They can also be used to +/// save on fees, as the Payjoin receiver can direct the incoming funds to open a lightning +/// channel, forwards the funds to another address, or simply consolidate UTXOs. +/// +/// Payjoin [`BIP77`] implementation. Compatible also with previous Payjoin version [`BIP78`]. +/// +/// Should be retrieved by calling [`Node::payjoin_payment`]. +/// +/// In a Payjoin, both the sender and receiver contribute inputs to the transaction in a +/// coordinated manner. The Payjoin mechanism is also called pay-to-endpoint(P2EP). +/// +/// The Payjoin receiver endpoint address is communicated through a [`BIP21`] URI, along with the +/// payment address and amount. In the Payjoin process, parties edit, sign and pass iterations of +/// the transaction between each other, before a final version is broadcasted by the Payjoin +/// sender. [`BIP77`] codifies a protocol with 2 iterations (or one round of interaction beyond +/// address sharing). +/// +/// [`BIP77`] Defines the Payjoin process to happen asynchronously, with the Payjoin receiver +/// enrolling with a Payjoin Directory to receive Payjoin requests. The Payjoin sender can then +/// make requests through a proxy server, Payjoin Relay, to the Payjoin receiver even if the +/// receiver is offline. This mechanism requires the Payjoin sender to regulary check for responses +/// from the Payjoin receiver as implemented in [`Node::payjoin_payment::send`]. +/// +/// A Payjoin Relay is a proxy server that forwards Payjoin requests from the Payjoin sender to the +/// Payjoin receiver subdirectory. A Payjoin Relay can be run by anyone. Public Payjoin Relay servers are: +/// - +/// +/// A Payjoin directory is a service that allows Payjoin receivers to receive Payjoin requests +/// offline. A Payjoin directory can be run by anyone. Public Payjoin Directory servers are: +/// - +/// +/// For futher information on Payjoin, please refer to the BIPs included in this documentation. Or +/// visit the [Payjoin website](https://payjoin.org). +/// +/// [`Node::payjoin_payment`]: crate::Node::payjoin_payment +/// [`Node::payjoin_payment::send`]: crate::payment::PayjoinPayment::send +/// [`BIP21`]: https://github.com/bitcoin/bips/blob/master/bip-0021.mediawiki +/// [`BIP78`]: https://github.com/bitcoin/bips/blob/master/bip-0078.mediawiki +/// [`BIP77`]: https://github.com/bitcoin/bips/blob/3b863a402e0250658985f08a455a6cd103e269e5/bip-0077.mediawiki +pub struct PayjoinPayment { + runtime: Arc>>, + payjoin_handler: Option>, + receiver: Option>, + config: Arc, + logger: Arc, + wallet: Arc, + tx_broadcaster: Arc, + peer_store: Arc>>, + channel_manager: Arc, + connection_manager: Arc>>, + payment_store: Arc, +} + +impl PayjoinPayment { + pub(crate) fn new( + runtime: Arc>>, + payjoin_handler: Option>, receiver: Option>, + config: Arc, logger: Arc, wallet: Arc, + tx_broadcaster: Arc, peer_store: Arc>>, + channel_manager: Arc, + connection_manager: Arc>>, + payment_store: Arc, + ) -> Self { + Self { + runtime, + payjoin_handler, + receiver, + config, + logger, + wallet, + tx_broadcaster, + peer_store, + channel_manager, + connection_manager, + payment_store, + } + } + + /// Send a Payjoin transaction to the address specified in the `payjoin_uri`. + /// + /// The `payjoin_uri` argument is expected to be a valid [`BIP21`] URI with Payjoin parameters + /// set. + /// + /// Due to the asynchronous nature of the Payjoin process, this method will return immediately + /// after constucting the Payjoin request and sending it in the background. The result of the + /// operation will be communicated through the event queue. If the Payjoin request is + /// successful, [`Event::PayjoinTxSendSuccess`] event will be added to the event queue. + /// Otherwise, [`Event::PayjoinTxSendFailed`] is added. + /// + /// The total duration of the Payjoin process is defined in `PAYJOIN_REQUEST_TOTAL_DURATION`. + /// If the Payjoin receiver does not respond within this duration, the process is considered + /// failed. Note, the Payjoin receiver can still broadcast the original PSBT shared with them as + /// part of our request in a regular transaction if we timed out, or for any other reason. The + /// Payjoin sender should monitor the blockchain for such transactions and handle them + /// accordingly. + /// + /// [`BIP21`]: https://github.com/bitcoin/bips/blob/master/bip-0021.mediawiki + /// [`BIP77`]: https://github.com/bitcoin/bips/blob/d7ffad81e605e958dcf7c2ae1f4c797a8631f146/bip-0077.mediawiki + /// [`Event::PayjoinTxSendSuccess`]: crate::Event::PayjoinTxSendSuccess + /// [`Event::PayjoinTxSendFailed`]: crate::Event::PayjoinTxSendFailed + pub fn send(&self, payjoin_uri: String) -> Result<(), Error> { + let rt_lock = self.runtime.read().unwrap(); + if rt_lock.is_none() { + return Err(Error::NotRunning); + } + let payjoin_handler = self.payjoin_handler.as_ref().ok_or(Error::PayjoinUnavailable)?; + let payjoin_uri = + payjoin::Uri::try_from(payjoin_uri).map_err(|_| Error::PayjoinUriInvalid).and_then( + |uri| uri.require_network(self.config.network).map_err(|_| Error::InvalidNetwork), + )?; + let original_psbt = self.wallet.build_payjoin_transaction(payjoin_uri.clone())?; + let payjoin_handler = Arc::clone(payjoin_handler); + let runtime = rt_lock.as_ref().unwrap(); + let tx_broadcaster = Arc::clone(&self.tx_broadcaster); + let logger = Arc::clone(&self.logger); + let payment_store = Arc::clone(&self.payment_store); + let payment_id = original_psbt.unsigned_tx.txid()[..].try_into().unwrap(); + payment_store.insert(PaymentDetails::new( + PaymentId(payment_id), + PaymentKind::Payjoin, + payjoin_uri.amount.map(|a| a.to_sat()), + PaymentDirection::Outbound, + PaymentStatus::Pending, + ))?; + runtime.spawn(async move { + let mut interval = tokio::time::interval(PAYJOIN_RETRY_INTERVAL); + loop { + tokio::select! { + _ = tokio::time::sleep(PAYJOIN_REQUEST_TOTAL_DURATION) => { + let _ = payjoin_handler.handle_request_timeout(payjoin_uri.clone(), &original_psbt); + break; + } + _ = interval.tick() => { + let payjoin_uri = payjoin_uri.clone(); + match payjoin_handler.send_request(payjoin_uri.clone(), &mut original_psbt.clone()).await { + Ok(Some(mut proposal)) => { + let _ = payjoin_handler.process_response(&mut proposal, &mut original_psbt.clone(), payjoin_uri).inspect(|tx| { + tx_broadcaster.broadcast_transactions(&[&tx]); + }).inspect_err(|e| { + log_error!(logger, "Failed to process Payjoin response: {}", e); + }); + break; + }, + Ok(None) => { + continue; + } + Err(e) => { + log_error!(logger, "Failed to send Payjoin request : {}", e); + let _ = payjoin_handler.handle_request_failure(payjoin_uri.clone(), &original_psbt); + break; + }, + } + } + } + } + }); + return Ok(()); + } + + /// Send a Payjoin transaction to the address specified in the `payjoin_uri`. + /// + /// The `payjoin_uri` argument is expected to be a valid [`BIP21`] URI with Payjoin parameters + /// set. + /// + /// This method will ignore the amount specified in the `payjoin_uri` and use the `amount_sats` + /// instead. The `amount_sats` argument is expected to be in satoshis. + /// + /// Due to the asynchronous nature of the Payjoin process, this method will return immediately + /// after constucting the Payjoin request and sending it in the background. The result of the + /// operation will be communicated through the event queue. If the Payjoin request is + /// successful, [`Event::PayjoinTxSendSuccess`] event will be added to the event queue. + /// Otherwise, [`Event::PayjoinTxSendFailed`] is added. + /// + /// The total duration of the Payjoin process is defined in `PAYJOIN_REQUEST_TOTAL_DURATION`. + /// If the Payjoin receiver does not respond within this duration, the process is considered + /// failed. Note, the Payjoin receiver can still broadcast the original PSBT shared with them as + /// part of our request in a regular transaction if we timed out, or for any other reason. The + /// Payjoin sender should monitor the blockchain for such transactions and handle them + /// accordingly. + /// + /// [`BIP21`]: https://github.com/bitcoin/bips/blob/master/bip-0021.mediawiki + /// [`BIP77`]: https://github.com/bitcoin/bips/blob/d7ffad81e605e958dcf7c2ae1f4c797a8631f146/bip-0077.mediawiki + /// [`Event::PayjoinTxSendSuccess`]: crate::Event::PayjoinTxSendSuccess + /// [`Event::PayjoinTxSendFailed`]: crate::Event::PayjoinTxSendFailed + pub fn send_with_amount(&self, payjoin_uri: String, amount_sats: u64) -> Result<(), Error> { + let mut payjoin_uri = + payjoin::Uri::try_from(payjoin_uri).map_err(|_| Error::PayjoinUriInvalid).and_then( + |uri| uri.require_network(self.config.network).map_err(|_| Error::InvalidNetwork), + )?; + payjoin_uri.amount = Some(bitcoin::Amount::from_sat(amount_sats)); + self.send(payjoin_uri.to_string()) + } + + /// Receive onchain Payjoin transaction. + /// + /// This method will enroll with the configured Payjoin directory if not already, + /// and returns a [BIP21] URI pointing to our enrolled subdirectory that you can share with + /// Payjoin sender. + /// + /// [BIP21]: https://github.com/bitcoin/bips/blob/master/bip-0021.mediawiki + pub fn receive(&self, amount: bitcoin::Amount) -> Result { + let rt_lock = self.runtime.read().unwrap(); + if rt_lock.is_none() { + return Err(Error::NotRunning); + } + if let Some(receiver) = &self.receiver { + let runtime = rt_lock.as_ref().unwrap(); + runtime.handle().block_on(async { receiver.receive(amount).await }) + } else { + Err(Error::PayjoinReceiverUnavailable) + } + } + + /// Receive on chain Payjoin transaction and open a channel in a single transaction. + /// + /// This method will enroll with the configured Payjoin directory if not already, + /// and before returning a [BIP21] URI pointing to our enrolled subdirectory to share with + /// Payjoin sender, we start the channel opening process and halt it when we receive + /// `accept_channel` from counterparty node. Once the Payjoin request is received, we move + /// forward with the channel opening process. + /// + /// [BIP21]: https://github.com/bitcoin/bips/blob/master/bip-0021.mediawiki + pub fn receive_with_channel_opening( + &self, channel_amount_sats: u64, push_msat: Option, announce_channel: bool, + node_id: PublicKey, address: SocketAddress, + ) -> Result { + use rand::Rng; + let rt_lock = self.runtime.read().unwrap(); + if rt_lock.is_none() { + return Err(Error::NotRunning); + } + if let Some(receiver) = &self.receiver { + let user_channel_id: u128 = rand::thread_rng().gen::(); + let runtime = rt_lock.as_ref().unwrap(); + runtime.handle().block_on(async { + receiver + .schedule_channel( + bitcoin::Amount::from_sat(channel_amount_sats), + node_id, + user_channel_id, + ) + .await; + }); + 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); + 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); + + runtime.handle().block_on(async { + let _ = con_cm.connect_peer_if_necessary(con_node_id, con_addr).await; + }); + + match self.channel_manager.create_channel( + peer_info.node_id, + channel_amount_sats, + push_msat, + user_channel_id, + None, + Some(user_config), + ) { + Ok(_) => { + self.peer_store.add_peer(peer_info)?; + }, + Err(_) => { + return Err(Error::ChannelCreationFailed); + }, + }; + + runtime.handle().block_on(async { + let payjoin_uri = + receiver.receive(bitcoin::Amount::from_sat(channel_amount_sats)).await?; + Ok(payjoin_uri) + }) + } else { + Err(Error::PayjoinReceiverUnavailable) + } + } +} diff --git a/src/payment/payjoin/send.rs b/src/payment/payjoin/send.rs new file mode 100644 index 000000000..f981b3ec2 --- /dev/null +++ b/src/payment/payjoin/send.rs @@ -0,0 +1,76 @@ +use crate::config::{PAYJOIN_REQUEST_TIMEOUT, PAYJOIN_RETRY_INTERVAL}; +use crate::io::utils::ohttp_headers; +use crate::logger::FilesystemLogger; + +use lightning::util::logger::Logger; +use lightning::{log_error, log_info}; + +use std::sync::Arc; + +pub(crate) struct PayjoinSender { + logger: Arc, + payjoin_relay: payjoin::Url, +} + +impl PayjoinSender { + pub(crate) fn new(logger: Arc, payjoin_relay: payjoin::Url) -> Self { + Self { logger, payjoin_relay } + } + + pub(crate) fn payjoin_relay(&self) -> &payjoin::Url { + &self.payjoin_relay + } + + pub(crate) async fn send_request(&self, request: &payjoin::Request) -> Option> { + let response = match reqwest::Client::new() + .post(request.url.clone()) + .body(request.body.clone()) + .timeout(PAYJOIN_REQUEST_TIMEOUT) + .headers(ohttp_headers()) + .send() + .await + { + Ok(response) => response, + Err(e) => { + log_error!( + self.logger, + "Error trying to poll Payjoin response: {}, retrying in {} seconds", + e, + PAYJOIN_RETRY_INTERVAL.as_secs() + ); + return None; + }, + }; + if response.status() == reqwest::StatusCode::OK { + match response.bytes().await.and_then(|r| Ok(r.to_vec())) { + Ok(response) => { + if response.is_empty() { + log_info!( + self.logger, + "Got empty response while polling Payjoin response, retrying in {} seconds", PAYJOIN_RETRY_INTERVAL.as_secs() + ); + return None; + } + return Some(response); + }, + Err(e) => { + log_error!( + self.logger, + "Error reading polling Payjoin response: {}, retrying in {} seconds", + e, + PAYJOIN_RETRY_INTERVAL.as_secs() + ); + return None; + }, + }; + } else { + log_info!( + self.logger, + "Got status code {} while polling Payjoin response, retrying in {} seconds", + response.status(), + PAYJOIN_RETRY_INTERVAL.as_secs() + ); + return None; + } + } +} diff --git a/src/payment/store.rs b/src/payment/store.rs index eb3ac091f..3f0481d9b 100644 --- a/src/payment/store.rs +++ b/src/payment/store.rs @@ -232,6 +232,8 @@ pub enum PaymentKind { /// The pre-image used by the payment. preimage: Option, }, + /// A Payjoin payment. + Payjoin, } impl_writeable_tlv_based_enum!(PaymentKind, @@ -261,7 +263,8 @@ impl_writeable_tlv_based_enum!(PaymentKind, (0, hash, option), (2, preimage, option), (4, secret, option), - }; + }, + (12, Payjoin) => {}; ); /// Limits applying to how much fee we allow an LSP to deduct from the payment amount. diff --git a/src/tx_broadcaster.rs b/src/tx_broadcaster.rs index 4492bcfc6..2a3867ebc 100644 --- a/src/tx_broadcaster.rs +++ b/src/tx_broadcaster.rs @@ -114,8 +114,10 @@ where { fn broadcast_transactions(&self, txs: &[&Transaction]) { let package = txs.iter().map(|&t| t.clone()).collect::>(); - self.queue_sender.try_send(package).unwrap_or_else(|e| { + let ret = self.queue_sender.try_send(package).unwrap_or_else(|e| { + dbg!(&e); log_error!(self.logger, "Failed to broadcast transactions: {}", e); }); + dbg!(&ret); } } diff --git a/src/types.rs b/src/types.rs index 0c2faeb78..ad21e2310 100644 --- a/src/types.rs +++ b/src/types.rs @@ -72,6 +72,8 @@ pub(crate) type Wallet = crate::wallet::Wallet< Arc, >; +pub(crate) type EventQueue = crate::event::EventQueue>; + pub(crate) type KeysManager = crate::wallet::WalletKeysManager< bdk::database::SqliteDatabase, Arc, @@ -140,6 +142,8 @@ pub(crate) type BumpTransactionEventHandler = Arc, >; +pub(crate) type PaymentStore = crate::payment::store::PaymentStore>; + /// A local, potentially user-provided, identifier of a channel. /// /// By default, this will be randomly generated for the user to ensure local uniqueness. diff --git a/src/uniffi_types.rs b/src/uniffi_types.rs index 9dd7e5699..f5fccbc20 100644 --- a/src/uniffi_types.rs +++ b/src/uniffi_types.rs @@ -1,3 +1,4 @@ +pub use crate::event::PayjoinPaymentFailureReason; pub use crate::graph::{ChannelInfo, ChannelUpdateInfo, NodeAnnouncementInfo, NodeInfo}; pub use crate::payment::store::{LSPFeeLimits, PaymentDirection, PaymentKind, PaymentStatus}; @@ -11,7 +12,7 @@ pub use lightning::util::string::UntrustedString; pub use lightning_invoice::Bolt11Invoice; -pub use bitcoin::{Address, BlockHash, Network, OutPoint, Txid}; +pub use bitcoin::{Address, BlockHash, Network, OutPoint, ScriptBuf, Txid}; pub use bip39::Mnemonic; @@ -31,6 +32,18 @@ use lightning_invoice::SignedRawBolt11Invoice; use std::convert::TryInto; use std::str::FromStr; +impl UniffiCustomTypeConverter for ScriptBuf { + type Builtin = String; + + fn into_custom(val: Self::Builtin) -> uniffi::Result { + Ok(ScriptBuf::from_hex(&val).map_err(|_| Error::InvalidPublicKey)?) + } + + fn from_custom(obj: Self) -> Self::Builtin { + obj.to_hex_string() + } +} + impl UniffiCustomTypeConverter for PublicKey { type Builtin = String; diff --git a/src/wallet.rs b/src/wallet.rs index 0da3f6db8..c8e5ddb23 100644 --- a/src/wallet.rs +++ b/src/wallet.rs @@ -3,6 +3,7 @@ use crate::logger::{log_error, log_info, log_trace, Logger}; use crate::config::BDK_WALLET_SYNC_TIMEOUT_SECS; use crate::Error; +use bitcoin::psbt::Psbt; use lightning::chain::chaininterface::{BroadcasterInterface, ConfirmationTarget, FeeEstimator}; use lightning::events::bump_transaction::{Utxo, WalletSource}; @@ -18,10 +19,10 @@ use lightning::util::message_signing; use bdk::blockchain::EsploraBlockchain; use bdk::database::BatchDatabase; use bdk::wallet::AddressIndex; -use bdk::{Balance, FeeRate}; +use bdk::{Balance, FeeRate, LocalUtxo}; use bdk::{SignOptions, SyncOptions}; -use bitcoin::address::{Payload, WitnessVersion}; +use bitcoin::address::{NetworkChecked, Payload, WitnessVersion}; use bitcoin::bech32::u5; use bitcoin::blockdata::constants::WITNESS_SCALE_FACTOR; use bitcoin::blockdata::locktime::absolute::LockTime; @@ -34,6 +35,7 @@ use bitcoin::secp256k1::ecdsa::{RecoverableSignature, Signature}; use bitcoin::secp256k1::{PublicKey, Scalar, Secp256k1, SecretKey, Signing}; use bitcoin::{ScriptBuf, Transaction, TxOut, Txid}; +use std::collections::{BTreeMap, HashMap}; use std::ops::{Deref, DerefMut}; use std::sync::{Arc, Mutex, RwLock}; use std::time::Duration; @@ -149,6 +151,117 @@ where res } + // Returns the total value of all outputs in the given transaction that are directed to us + 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 build_payjoin_transaction( + &self, payjoin_uri: payjoin::Uri, + ) -> Result { + let locked_wallet = self.inner.lock().unwrap(); + let network = locked_wallet.network(); + let output_script = payjoin_uri.address.script_pubkey(); + let amount = payjoin_uri.amount.ok_or(Error::PayjoinRequestMissingAmount)?.to_sat(); + let fee_rate = match network { + bitcoin::Network::Regtest => 1000.0, + _ => self + .fee_estimator + .get_est_sat_per_1000_weight(ConfirmationTarget::OutputSpendingFee) as f32, + }; + let fee_rate = FeeRate::from_sat_per_kwu(fee_rate); + let mut tx_builder = locked_wallet.build_tx(); + tx_builder.add_recipient(output_script, amount).fee_rate(fee_rate).enable_rbf(); + let mut psbt = match tx_builder.finish() { + Ok((psbt, _)) => { + log_trace!(self.logger, "Created Payjoin transaction: {:?}", psbt); + psbt + }, + Err(err) => { + log_error!(self.logger, "Failed to create Payjoin transaction: {}", err); + return Err(err.into()); + }, + }; + locked_wallet.sign(&mut psbt, SignOptions::default())?; + Ok(psbt) + } + + pub(crate) fn sign_payjoin_proposal( + &self, payjoin_proposal_psbt: &mut Psbt, original_psbt: &mut Psbt, + ) -> Result { + // BDK only signs scripts that match its target descriptor by iterating through input map. + // The BIP 78 spec makes receiver clear sender input map UTXOs, so process_response will + // fail unless they're cleared. A PSBT unsigned_tx.input references input OutPoints and + // not a Script, so the sender signer must either be able to sign based on OutPoint UTXO + // lookup or otherwise re-introduce the Script from original_psbt. Since BDK PSBT signer + // only checks Input map Scripts for match against its descriptor, it won't sign if they're + // empty. Re-add the scripts from the original_psbt in order for BDK to sign properly. + // reference: https://github.com/bitcoindevkit/bdk-cli/pull/156#discussion_r1261300637 + let mut original_inputs = + original_psbt.unsigned_tx.input.iter().zip(&mut original_psbt.inputs).peekable(); + for (proposed_txin, proposed_psbtin) in + payjoin_proposal_psbt.unsigned_tx.input.iter().zip(&mut payjoin_proposal_psbt.inputs) + { + 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 wallet = self.inner.lock().unwrap(); + let is_signed = wallet.sign(payjoin_proposal_psbt, SignOptions::default())?; + Ok(is_signed) + } + + // Returns a list of unspent outputs that can be used as inputs to improve the privacy of a + // payjoin transaction. + pub(crate) fn payjoin_receiver_candidate_input( + &self, + ) -> Result<(HashMap, Vec), Error> { + let locked_wallet = self.inner.lock().unwrap(); + let utxo_set = locked_wallet.list_unspent()?; + let candidate_inputs = utxo_set + .iter() + .filter_map(|utxo| { + if !utxo.is_spent { + Some((bitcoin::Amount::from_sat(utxo.txout.value), utxo.outpoint)) + } else { + None + } + }) + .collect(); + Ok((candidate_inputs, utxo_set)) + } + + pub(crate) fn prepare_payjoin_proposal(&self, mut psbt: Psbt) -> Result { + let wallet = self.inner.lock().unwrap(); + let mut sign_options = SignOptions::default(); + sign_options.trust_witness_utxo = true; + wallet.sign(&mut psbt, sign_options)?; + // Clear derivation paths from the PSBT as required by BIP78/BIP77 + psbt.inputs.iter_mut().for_each(|i| { + i.bip32_derivation = BTreeMap::new(); + }); + psbt.outputs.iter_mut().for_each(|o| { + o.bip32_derivation = BTreeMap::new(); + }); + 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 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 5959bd58e..edccad541 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -147,6 +147,40 @@ 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) => {{ + match $node.wait_next_event() { + ref e @ Event::PayjoinPaymentSuccess { 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_sent_successfully_event; + 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( @@ -258,6 +292,22 @@ pub(crate) fn setup_two_nodes( (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(false); + let node_a_payjoin_receiver = setup_payjoin_node(electrsd, config_a); + + println!("\n== Node B =="); + let mut config_b = random_config(false); + if allow_0conf { + config_b.trusted_peers_0conf.push(node_a_payjoin_receiver.node_id()); + } + let node_b_payjoin_sender = setup_payjoin_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); @@ -270,6 +320,21 @@ 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_directory = "https://payjo.in".to_string(); + let payjoin_relay = "https://pj.bobspacebkk.com".to_string(); + builder.set_payjoin_config(payjoin_directory, payjoin_relay, None).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..43ec91c8f --- /dev/null +++ b/tests/integration_tests_payjoin.rs @@ -0,0 +1,93 @@ +mod common; + +use common::{ + expect_payjoin_tx_sent_successfully_event, generate_blocks_and_wait, + premine_and_distribute_funds, setup_bitcoind_and_electrsd, setup_two_payjoin_nodes, + wait_for_tx, +}; + +use bitcoin::Amount; +use ldk_node::{ + payment::{PaymentDirection, PaymentKind, PaymentStatus}, + Event, +}; + +use crate::common::expect_payjoin_tx_pending_event; + +#[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 addr_a = node_a_pj_receiver.onchain_payment().new_address().unwrap(); + let premine_amount_sat = 100_000_00; + premine_and_distribute_funds( + &bitcoind.client, + &electrsd.client, + vec![addr_b, addr_a], + 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, 100_000_00); + assert_eq!(node_a_pj_receiver.next_event(), None); + let payjoin_payment = node_a_pj_receiver.payjoin_payment(); + let payjoin_uri = payjoin_payment.receive(Amount::from_sat(80_000)).unwrap(); + let payjoin_uri = payjoin_uri.to_string(); + dbg!(&payjoin_uri); + let sender_payjoin_payment = node_b_pj_sender.payjoin_payment(); + assert!(sender_payjoin_payment.send(payjoin_uri).is_ok()); + let txid = expect_payjoin_tx_pending_event!(node_b_pj_sender); + let payments = node_b_pj_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); + wait_for_tx(&electrsd.client, txid); + generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 3); + node_b_pj_sender.sync_wallets().unwrap(); + generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 4); + node_b_pj_sender.sync_wallets().unwrap(); + let payments = node_b_pj_sender.list_payments(); + let payment = payments.first().unwrap(); + assert_eq!(payment.status, PaymentStatus::Succeeded); + expect_payjoin_tx_sent_successfully_event!(node_b_pj_sender); + let node_b_balance = node_b_pj_sender.list_balances(); + assert!(node_b_balance.total_onchain_balance_sats < premine_amount_sat - 80000); +} + +#[test] +fn send_payjoin_with_amount() { + 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 addr_a = node_a_pj_receiver.onchain_payment().new_address().unwrap(); + let premine_amount_sat = 100_000_00; + premine_and_distribute_funds( + &bitcoind.client, + &electrsd.client, + vec![addr_b, addr_a], + 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, 100_000_00); + assert_eq!(node_a_pj_receiver.next_event(), None); + let payjoin_payment = node_a_pj_receiver.payjoin_payment(); + let payjoin_uri = payjoin_payment.receive(Amount::from_sat(100_000_000)).unwrap(); + let payjoin_uri = payjoin_uri.to_string(); + dbg!(&payjoin_uri); + let sender_payjoin_payment = node_b_pj_sender.payjoin_payment(); + assert!(sender_payjoin_payment.send_with_amount(payjoin_uri, 80_000).is_ok()); + let _txid = expect_payjoin_tx_pending_event!(node_b_pj_sender); + generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 3); + node_b_pj_sender.sync_wallets().unwrap(); + generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 4); + node_b_pj_sender.sync_wallets().unwrap(); + let _txid = expect_payjoin_tx_sent_successfully_event!(node_b_pj_sender); + let node_b_balance = node_b_pj_sender.list_balances(); + assert!(node_b_balance.total_onchain_balance_sats < premine_amount_sat - 80000); +} diff --git a/tests/integration_tests_payjoin_with_channel_opening.rs b/tests/integration_tests_payjoin_with_channel_opening.rs new file mode 100644 index 000000000..5e1ad4fa4 --- /dev/null +++ b/tests/integration_tests_payjoin_with_channel_opening.rs @@ -0,0 +1,77 @@ +mod common; + +use common::{ + expect_channel_pending_event, expect_channel_ready_event, + expect_payjoin_tx_sent_successfully_event, generate_blocks_and_wait, + premine_and_distribute_funds, setup_bitcoind_and_electrsd, setup_two_payjoin_nodes, + wait_for_tx, +}; + +use bitcoin::Amount; +use ldk_node::Event; + +use crate::common::expect_payjoin_tx_pending_event; + +#[test] +fn send_receive_payjoin_transaction_with_channel_opening() { + 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 addr_a = node_a_pj_receiver.onchain_payment().new_address().unwrap(); + let premine_amount_sat = 100_000_00; + premine_and_distribute_funds( + &bitcoind.client, + &electrsd.client, + vec![addr_b, addr_a], + 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, 100_000_00); + assert_eq!(node_a_pj_receiver.next_event(), None); + assert_eq!(node_a_pj_receiver.list_channels().len(), 0); + let payjoin_payment = node_a_pj_receiver.payjoin_payment(); + let node_b_listening_address = + node_b_pj_sender.listening_addresses().unwrap().get(0).unwrap().clone(); + let payjoin_uri = payjoin_payment + .receive_with_channel_opening( + 80_000, + None, + false, + node_b_pj_sender.node_id(), + node_b_listening_address, + ) + .unwrap(); + let payjoin_uri = payjoin_uri.to_string(); + let sender_payjoin_payment = node_b_pj_sender.payjoin_payment(); + assert!(sender_payjoin_payment.send(payjoin_uri).is_ok()); + expect_channel_pending_event!(node_a_pj_receiver, node_b_pj_sender.node_id()); + expect_channel_pending_event!(node_b_pj_sender, node_a_pj_receiver.node_id()); + let txid = expect_payjoin_tx_pending_event!(node_b_pj_sender); + wait_for_tx(&electrsd.client, txid); + generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 1); + node_b_pj_sender.sync_wallets().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_b_balance = node_b_pj_sender.list_balances(); + assert!(node_b_balance.total_onchain_balance_sats < premine_amount_sat - 80000); + expect_channel_ready_event!(node_a_pj_receiver, node_b_pj_sender.node_id()); + expect_channel_ready_event!(node_b_pj_sender, node_a_pj_receiver.node_id()); + let _ = expect_payjoin_tx_sent_successfully_event!(node_b_pj_sender); + let channels = node_a_pj_receiver.list_channels(); + let channel = channels.get(0).unwrap(); + assert_eq!(channel.channel_value_sats, 80_000); + assert!(channel.is_channel_ready); + assert!(channel.is_usable); + + assert_eq!(node_a_pj_receiver.list_peers().get(0).unwrap().is_connected, true); + assert_eq!(node_a_pj_receiver.list_peers().get(0).unwrap().is_persisted, true); + assert_eq!(node_a_pj_receiver.list_peers().get(0).unwrap().node_id, node_b_pj_sender.node_id()); + + let invoice_amount_1_msat = 2500_000; + let invoice = + node_b_pj_sender.bolt11_payment().receive(invoice_amount_1_msat, "test", 1000).unwrap(); + assert!(node_a_pj_receiver.bolt11_payment().send(&invoice).is_ok()); +}