From aad3e09acd88e0fd1b41e8df4a6543a24dc6ed8e Mon Sep 17 00:00:00 2001 From: jbesraa Date: Mon, 20 May 2024 21:31:13 +0300 Subject: [PATCH] Allow to send payjoin transactions Implements the payjoin sender as describe in BIP77. This would allow the on chain wallet linked to LDK node to send payjoin transactions. --- Cargo.toml | 4 + bindings/ldk_node.udl | 30 ++ src/builder.rs | 47 ++- src/config.rs | 9 + src/error.rs | 36 +++ src/event.rs | 143 ++++++++- src/lib.rs | 57 +++- src/payment/mod.rs | 2 + src/payment/payjoin/handler.rs | 451 +++++++++++++++++++++++++++++ src/payment/payjoin/mod.rs | 180 ++++++++++++ src/payment/store.rs | 5 +- src/types.rs | 4 + src/uniffi_types.rs | 14 +- src/wallet.rs | 57 +++- tests/common/mod.rs | 82 ++++++ tests/integration_tests_payjoin.rs | 224 ++++++++++++++ 16 files changed, 1335 insertions(+), 10 deletions(-) create mode 100644 src/payment/payjoin/handler.rs create mode 100644 src/payment/payjoin/mod.rs create mode 100644 tests/integration_tests_payjoin.rs diff --git a/Cargo.toml b/Cargo.toml index 206f5f2dd..c399f9f70 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -69,6 +69,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", "v2"] } [target.'cfg(vss)'.dependencies] vss-client = "0.2" @@ -85,6 +86,9 @@ bitcoincore-rpc = { version = "0.17.0", default-features = false } proptest = "1.0.0" regex = "1.5.6" +payjoin = { version = "0.16.0", default-features = false, features = ["send", "v2", "receive"] } +reqwest = { version = "0.11", default-features = false, features = ["json", "rustls-tls", "blocking"] } + [target.'cfg(not(no_download))'.dev-dependencies] electrsd = { version = "0.26.0", features = ["legacy", "esplora_a33e97e1", "bitcoind_25_0"] } diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index aedf9f6ab..d725be738 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -64,6 +64,7 @@ interface Node { SpontaneousPayment spontaneous_payment(); OnchainPayment onchain_payment(); UnifiedQrPayment unified_qr_payment(); + PayjoinPayment payjoin_payment(); [Throws=NodeError] void connect(PublicKey node_id, SocketAddress address, boolean persist); [Throws=NodeError] @@ -156,6 +157,13 @@ interface UnifiedQrPayment { QrPaymentResult send([ByRef]string uri_str); }; +interface PayjoinPayment { + [Throws=NodeError] + void send(string payjoin_uri); + [Throws=NodeError] + void send_with_amount(string payjoin_uri, u64 amount_sats); +}; + [Error] enum NodeError { "AlreadyRunning", @@ -206,6 +214,12 @@ enum NodeError { "InsufficientFunds", "LiquiditySourceUnavailable", "LiquidityFeeTooHigh", + "PayjoinUnavailable", + "PayjoinUriInvalid", + "PayjoinRequestMissingAmount", + "PayjoinRequestCreationFailed", + "PayjoinRequestSendingFailed", + "PayjoinResponseProcessingFailed", }; dictionary NodeStatus { @@ -231,6 +245,7 @@ enum BuildError { "InvalidSystemTime", "InvalidChannelMonitor", "InvalidListeningAddresses", + "InvalidPayjoinConfig", "ReadFailed", "WriteFailed", "StoragePathAccessFailed", @@ -248,6 +263,11 @@ interface Event { ChannelPending(ChannelId channel_id, UserChannelId user_channel_id, ChannelId former_temporary_channel_id, PublicKey counterparty_node_id, OutPoint funding_txo); ChannelReady(ChannelId channel_id, UserChannelId user_channel_id, PublicKey? counterparty_node_id); ChannelClosed(ChannelId channel_id, UserChannelId user_channel_id, PublicKey? counterparty_node_id, ClosureReason? reason); + PayjoinPaymentPending(Txid txid, u64 amount_sats, ScriptBuf recipient); + PayjoinPaymentBroadcasted(Txid txid, u64 amount_sats, ScriptBuf recipient); + PayjoinPaymentSuccessful(Txid txid, u64 amount_sats, ScriptBuf recipient); + PayjoinPaymentFailed(Txid txid, u64 amount_sats, ScriptBuf recipient, PayjoinPaymentFailureReason reason); + PayjoinPaymentOriginalPsbtBroadcasted(Txid txid, u64 amount_sats, ScriptBuf recipient); }; enum PaymentFailureReason { @@ -259,6 +279,12 @@ enum PaymentFailureReason { "UnexpectedError", }; +enum PayjoinPaymentFailureReason { + "Timeout", + "RequestSendingFailed", + "ResponseProcessingFailed", +}; + [Enum] interface ClosureReason { CounterpartyForceClosed(UntrustedString peer_msg); @@ -284,6 +310,7 @@ interface PaymentKind { Bolt12Offer(PaymentHash? hash, PaymentPreimage? preimage, PaymentSecret? secret, OfferId offer_id); Bolt12Refund(PaymentHash? hash, PaymentPreimage? preimage, PaymentSecret? secret); Spontaneous(PaymentHash hash, PaymentPreimage? preimage); + Payjoin(); }; [Enum] @@ -516,3 +543,6 @@ typedef string Mnemonic; [Custom] typedef string UntrustedString; + +[Custom] +typedef string ScriptBuf; diff --git a/src/builder.rs b/src/builder.rs index a2a93aa79..9f3f4afe5 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -11,6 +11,7 @@ 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::payment::payjoin::handler::PayjoinHandler; use crate::payment::store::PaymentStore; use crate::peer_store::PeerStore; use crate::tx_broadcaster::TransactionBroadcaster; @@ -93,6 +94,11 @@ struct LiquiditySourceConfig { lsps2_service: Option<(SocketAddress, PublicKey, Option)>, } +#[derive(Debug, Clone)] +struct PayjoinConfig { + payjoin_relay: payjoin::Url, +} + impl Default for LiquiditySourceConfig { fn default() -> Self { Self { lsps2_service: None } @@ -132,6 +138,8 @@ pub enum BuildError { WalletSetupFailed, /// We failed to setup the logger. LoggerSetupFailed, + /// Invalid Payjoin configuration. + InvalidPayjoinConfig, } impl fmt::Display for BuildError { @@ -152,6 +160,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 +184,7 @@ pub struct NodeBuilder { chain_data_source_config: Option, gossip_source_config: Option, liquidity_source_config: Option, + payjoin_config: Option, } impl NodeBuilder { @@ -187,12 +200,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 +262,14 @@ impl NodeBuilder { self } + /// Configures the [`Node`] instance to enable payjoin transactions. + pub fn set_payjoin_config(&mut self, payjoin_relay: String) -> Result<&mut Self, BuildError> { + let payjoin_relay = + payjoin::Url::parse(&payjoin_relay).map_err(|_| BuildError::InvalidPayjoinConfig)?; + self.payjoin_config = Some(PayjoinConfig { payjoin_relay }); + 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 +388,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 +410,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 +478,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 +551,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 +997,17 @@ fn build_with_store_internal( let (stop_sender, _) = tokio::sync::watch::channel(()); let (event_handling_stopped_sender, _) = tokio::sync::watch::channel(()); + let payjoin_handler = payjoin_config.map(|pj_config| { + Arc::new(PayjoinHandler::new( + Arc::clone(&tx_sync), + Arc::clone(&event_queue), + Arc::clone(&logger), + pj_config.payjoin_relay.clone(), + Arc::clone(&payment_store), + Arc::clone(&wallet), + )) + }); + let is_listening = Arc::new(AtomicBool::new(false)); let latest_wallet_sync_timestamp = Arc::new(RwLock::new(None)); let latest_onchain_wallet_sync_timestamp = Arc::new(RwLock::new(None)); @@ -987,6 +1029,7 @@ fn build_with_store_internal( channel_manager, chain_monitor, output_sweeper, + payjoin_handler, peer_manager, connection_manager, keys_manager, diff --git a/src/config.rs b/src/config.rs index d0e72080f..0cff9a92a 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 a payjoin http request is considered timed out. +pub(crate) const PAYJOIN_REQUEST_TIMEOUT: Duration = Duration::from_secs(30); + +// The duration between retries of a payjoin http request. +pub(crate) const PAYJOIN_RETRY_INTERVAL: Duration = Duration::from_secs(3); + +// The total duration of retrying to send a payjoin http 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 7506b013b..84b918abe 100644 --- a/src/error.rs +++ b/src/error.rs @@ -99,6 +99,18 @@ pub enum Error { LiquiditySourceUnavailable, /// The given operation failed due to the LSP's required opening fee being too high. LiquidityFeeTooHigh, + /// Failed to access Payjoin 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, + /// Failed to send Payjoin request. + PayjoinRequestSendingFailed, + /// Payjoin response processing failed. + PayjoinResponseProcessingFailed, } impl fmt::Display for Error { @@ -168,6 +180,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 object. Make sure you have enabled Payjoin support." + ) + }, + Self::PayjoinRequestMissingAmount => { + write!( + f, + "Amount is neither user-provided nor defined in the provided Payjoin URI." + ) + }, + Self::PayjoinRequestCreationFailed => { + write!(f, "Failed construct a Payjoin request") + }, + Self::PayjoinUriInvalid => { + write!(f, "The provided Payjoin URI is invalid") + }, + Self::PayjoinRequestSendingFailed => { + write!(f, "Failed to send Payjoin request") + }, + Self::PayjoinResponseProcessingFailed => { + write!(f, "Payjoin receiver responded to our request with an invalid response") + }, } } } diff --git a/src/event.rs b/src/event.rs index e319ab5e4..ae56e08aa 100644 --- a/src/event.rs +++ b/src/event.rs @@ -143,8 +143,96 @@ pub enum Event { /// This will be `None` for events serialized by LDK Node v0.2.1 and prior. reason: Option, }, + /// This event is emitted when we initiate a Payjoin transaction and before negotiating with + /// the receiver. + PayjoinPaymentPending { + /// Transaction ID of original PSBT. + txid: bitcoin::Txid, + /// Transaction amount as specified in the Payjoin URI in case of using + /// [`PayjoinPayment::send`] or as specified by the user if using + /// [`PayjoinPayment::send_with_amount`]. + /// + /// [`PayjoinPayment::send`]: crate::PayjoinPayment::send + /// [`PayjoinPayment::send_with_amount`]: crate::PayjoinPayment::send_with_amount + amount_sats: u64, + /// Note that this refers to the recipient as defined in the used Payjoin URI, and does + /// not take into account any output substitution that may have occurred. + recipient: bitcoin::ScriptBuf, + }, + /// This event is emitted when we have successfully negotiated a Payjoin transaction with the + /// receiver and we have finalised and broadcasted the transaction. + /// + /// This does not necessarily imply the Payjoin transaction is fully successful. + PayjoinPaymentBroadcasted { + /// Transaction ID of the finalised Payjoin transaction. i.e., the final transaction + /// after we have successfully negotiated with the receiver. + txid: bitcoin::Txid, + /// Transaction amount as specified in the Payjoin URI in case of using + /// [`PayjoinPayment::send`] or as specified by the user if using + /// [`PayjoinPayment::send_with_amount`]. + /// + /// [`PayjoinPayment::send`]: crate::PayjoinPayment::send + /// [`PayjoinPayment::send_with_amount`]: crate::PayjoinPayment::send_with_amount + amount_sats: u64, + /// Note that this refers to the recipient as defined in the provided Payjoin URI, and does + /// not take into account any output substitution that may have occurred. + recipient: bitcoin::ScriptBuf, + }, + /// This event is emitted when a Payjoin transaction has at least six block confirmations. + PayjoinPaymentSuccessful { + /// This can refer to the original PSBT or to the finalised Payjoin transaction. Depending + /// if the Payjoin negotiation was successful or the receiver decided to broadcast the + /// original PSBT. + txid: bitcoin::Txid, + /// Transaction amount as specified in the Payjoin URI in case of using + /// [`PayjoinPayment::send`] or as specified by the user if using + /// [`PayjoinPayment::send_with_amount`]. + /// + /// [`PayjoinPayment::send`]: crate::PayjoinPayment::send + /// [`PayjoinPayment::send_with_amount`]: crate::PayjoinPayment::send_with_amount + amount_sats: u64, + /// Note that this refers to the recipient as defined in the used Payjoin URI, and does + /// not take into account any output substitution that may have occurred. + recipient: bitcoin::ScriptBuf, + }, + /// A Payjoin transaction has failed. + /// + /// This event is emitted when an attempt to send a Payjoin transaction failed. + PayjoinPaymentFailed { + /// This can refer to the original PSBT or to the finalised Payjoin transaction. Depending + /// on the stage of the Payjoin process when the failure occurred. + txid: bitcoin::Txid, + /// Transaction amount as specified in the Payjoin URI in case of using + /// [`PayjoinPayment::send`] or as specified by the user if using + /// [`PayjoinPayment::send_with_amount`]. + /// + /// [`PayjoinPayment::send`]: crate::PayjoinPayment::send + /// [`PayjoinPayment::send_with_amount`]: crate::PayjoinPayment::send_with_amount + amount_sats: u64, + /// Note that this refers to the recipient as defined in the used Payjoin URI, and does + /// not take into account any output substitution that may have occurred. + recipient: bitcoin::ScriptBuf, + /// Failure reason. + reason: PayjoinPaymentFailureReason, + }, + /// This event is emitted when the Payjoin receiver has received our offer but decided to + /// broadcast the `original_psbt` without any modifications. i.e., the receiver has declined to + /// participate in the Payjoin transaction and will receive the funds in a regular transaction. + PayjoinPaymentOriginalPsbtBroadcasted { + /// Transaction ID of original PSBT. + txid: bitcoin::Txid, + /// Transaction amount as specified in the Payjoin URI in case of using + /// [`PayjoinPayment::send`] or as specified by the user if using + /// [`PayjoinPayment::send_with_amount`]. + /// + /// [`PayjoinPayment::send`]: crate::PayjoinPayment::send + /// [`PayjoinPayment::send_with_amount`]: crate::PayjoinPayment::send_with_amount + amount_sats: u64, + /// Note that this refers to the recipient as defined in the used Payjoin URI, and does + /// not take into account any output substitution that may have occurred. + recipient: bitcoin::ScriptBuf, + }, } - impl_writeable_tlv_based_enum!(Event, (0, PaymentSuccessful) => { (0, payment_hash, required), @@ -184,9 +272,62 @@ 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_sats, required), + (4, recipient, required), + }, + (8, PayjoinPaymentBroadcasted) => { + (0, txid, required), + (2, amount_sats, required), + (4, recipient, required), + }, + (9, PayjoinPaymentSuccessful) => { + (0, txid, required), + (2, amount_sats, required), + (4, recipient, required), + }, + (10, PayjoinPaymentFailed) => { + (0, amount_sats, required), + (2, txid, required), + (4, recipient, required), + (6, reason, required), + }, + (11, PayjoinPaymentOriginalPsbtBroadcasted) => { + (0, txid, required), + (2, amount_sats, required), + (4, recipient, required), }; ); +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum PayjoinPaymentFailureReason { + /// The request failed as we did not receive a response in time. + /// + /// This is considered a failure but the receiver can still broadcast the original PSBT, in + /// which case a `PayjoinPaymentOriginalPsbtBroadcasted` event will be emitted. + Timeout, + /// Failed to send the Payjoin request. + /// + /// Sending a Payjoin request can fail due to insufficient funds, network issues, or other reasons. The + /// exact reason can be determined by inspecting the logs. + RequestSendingFailed, + /// Processing the received response failed. + /// + /// The received response was invalid, i.e., the receiver responded with an invalid Payjoin + /// proposal that does not adhere to the [`BIP78`] specification. + /// + /// [`BIP78`]: https://github.com/bitcoin/bips/blob/master/bip-0078.mediawiki + ResponseProcessingFailed, +} + +impl_writeable_tlv_based_enum!(PayjoinPaymentFailureReason, + (0, Timeout) => {}, + (1, RequestSendingFailed) => {}, + (2, ResponseProcessingFailed) => {}; +); + pub struct EventQueue where L::Target: Logger, diff --git a/src/lib.rs b/src/lib.rs index 206fe52d8..48c7728db 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -108,7 +108,10 @@ pub use config::{default_config, AnchorChannelsConfig, Config}; pub use error::Error as NodeError; use error::Error; +#[cfg(feature = "uniffi")] +use crate::event::PayjoinPaymentFailureReason; pub use event::Event; +use payment::payjoin::handler::PayjoinHandler; pub use types::ChannelConfig; pub use io::utils::generate_entropy_mnemonic; @@ -134,8 +137,8 @@ use graph::NetworkGraph; use liquidity::LiquiditySource; use payment::store::PaymentStore; use payment::{ - Bolt11Payment, Bolt12Payment, OnchainPayment, PaymentDetails, SpontaneousPayment, - UnifiedQrPayment, + Bolt11Payment, Bolt12Payment, OnchainPayment, PayjoinPayment, PaymentDetails, + SpontaneousPayment, UnifiedQrPayment, }; use peer_store::{PeerInfo, PeerStore}; use types::{ @@ -188,6 +191,7 @@ pub struct Node { output_sweeper: Arc, peer_manager: Arc, connection_manager: Arc>>, + payjoin_handler: Option>, keys_manager: Arc, network_graph: Arc, gossip_source: Arc, @@ -380,6 +384,8 @@ impl Node { let archive_cmon = Arc::clone(&self.chain_monitor); let sync_sweeper = Arc::clone(&self.output_sweeper); let sync_logger = Arc::clone(&self.logger); + let sync_payjoin = &self.payjoin_handler.as_ref(); + let sync_payjoin = sync_payjoin.map(Arc::clone); let sync_wallet_timestamp = Arc::clone(&self.latest_wallet_sync_timestamp); let sync_monitor_archival_height = Arc::clone(&self.latest_channel_monitor_archival_height); let mut stop_sync = self.stop_sender.subscribe(); @@ -399,11 +405,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 { @@ -1109,6 +1118,42 @@ impl Node { )) } + /// Returns a Payjoin payment handler allowing to send Payjoin transactions + /// + /// in order to utilize Payjoin functionality, it is necessary to configure a Payjoin relay + /// using [`set_payjoin_config`]. + /// + /// [`set_payjoin_config`]: crate::builder::NodeBuilder::set_payjoin_config + #[cfg(not(feature = "uniffi"))] + pub fn payjoin_payment(&self) -> PayjoinPayment { + let payjoin_handler = self.payjoin_handler.as_ref(); + PayjoinPayment::new( + Arc::clone(&self.config), + Arc::clone(&self.logger), + payjoin_handler.map(Arc::clone), + Arc::clone(&self.runtime), + Arc::clone(&self.tx_broadcaster), + ) + } + + /// Returns a Payjoin payment handler allowing to send Payjoin transactions. + /// + /// in order to utilize Payjoin functionality, it is necessary to configure a Payjoin relay + /// using [`set_payjoin_config`]. + /// + /// [`set_payjoin_config`]: crate::builder::NodeBuilder::set_payjoin_config + #[cfg(feature = "uniffi")] + pub fn payjoin_payment(&self) -> Arc { + let payjoin_handler = self.payjoin_handler.as_ref(); + Arc::new(PayjoinPayment::new( + Arc::clone(&self.config), + Arc::clone(&self.logger), + payjoin_handler.map(Arc::clone), + Arc::clone(&self.runtime), + Arc::clone(&self.tx_broadcaster), + )) + } + /// Retrieve a list of known channels. pub fn list_channels(&self) -> Vec { self.channel_manager.list_channels().into_iter().map(|c| c.into()).collect() @@ -1310,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/payment/mod.rs b/src/payment/mod.rs index ac4fc5663..3346e0fd7 100644 --- a/src/payment/mod.rs +++ b/src/payment/mod.rs @@ -3,10 +3,12 @@ mod bolt11; mod bolt12; mod onchain; +pub(crate) mod payjoin; mod spontaneous; pub(crate) mod store; mod unified_qr; +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..8532557fa --- /dev/null +++ b/src/payment/payjoin/handler.rs @@ -0,0 +1,451 @@ +use bitcoin::address::NetworkChecked; +use bitcoin::block::Header; +use bitcoin::psbt::Psbt; +use bitcoin::{Address, Amount, BlockHash, Script, Transaction, Txid}; + +use crate::config::PAYJOIN_REQUEST_TIMEOUT; +use crate::error::Error; +use crate::event::PayjoinPaymentFailureReason; +use crate::logger::FilesystemLogger; +use crate::payment::store::PaymentDetailsUpdate; +use crate::payment::PaymentKind; +use crate::payment::{PaymentDirection, PaymentStatus}; +use crate::types::{ChainSource, EventQueue, PaymentStore, Wallet}; +use crate::Event; +use crate::PaymentDetails; + +use lightning::chain::channelmonitor::ANTI_REORG_DELAY; +use lightning::chain::transaction::TransactionData; +use lightning::chain::{Confirm, Filter}; +use lightning::ln::channelmanager::PaymentId; +use lightning::log_error; +use lightning::util::logger::Logger; + +use std::sync::{Arc, RwLock}; + +pub(crate) struct PayjoinHandler { + chain_source: Arc, + event_queue: Arc, + logger: Arc, + payjoin_relay: payjoin::Url, + payment_store: Arc, + transactions: RwLock>, + wallet: Arc, +} + +impl PayjoinHandler { + pub(crate) fn new( + chain_source: Arc, event_queue: Arc, + logger: Arc, payjoin_relay: payjoin::Url, + payment_store: Arc, wallet: Arc, + ) -> Self { + Self { + chain_source, + event_queue, + logger, + payjoin_relay, + payment_store, + transactions: RwLock::new(Vec::new()), + wallet, + } + } + + pub(crate) fn start_request( + &self, payjoin_uri: payjoin::Uri<'_, NetworkChecked>, + ) -> Result { + let amount = payjoin_uri.amount.ok_or(Error::PayjoinRequestMissingAmount)?; + let receiver = payjoin_uri.address.clone(); + let original_psbt = + self.wallet.build_payjoin_transaction(amount, receiver.clone().into())?; + self.transactions.write().unwrap().push(PayjoinTransaction::new( + original_psbt.clone(), + receiver.clone(), + amount, + )); + let payment_id = original_psbt.unsigned_tx.txid()[..].try_into().map_err(|_| { + log_error!(self.logger, "Failed to start Payjoin request: invalid payment id"); + Error::PayjoinRequestSendingFailed + })?; + self.payment_store.insert(PaymentDetails::new( + PaymentId(payment_id), + PaymentKind::Payjoin, + Some(amount.to_sat()), + PaymentDirection::Outbound, + PaymentStatus::Pending, + ))?; + let tx = original_psbt.clone().unsigned_tx; + // watch original transaction in case it gets broadcasted by the receiver + self.chain_source.register_tx(&tx.txid(), Script::empty()); + self.event_queue.add_event(Event::PayjoinPaymentPending { + txid: tx.txid(), + amount_sats: amount.to_sat(), + recipient: receiver.into(), + })?; + Ok(original_psbt) + } + + 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| { + log_error!(self.logger, "Failed to create Payjoin request: {}", e); + Error::PayjoinRequestCreationFailed + })?; + let mut headers = reqwest::header::HeaderMap::new(); + headers.insert( + reqwest::header::CONTENT_TYPE, + reqwest::header::HeaderValue::from_static("message/ohttp-req"), + ); + let response = reqwest::Client::new() + .post(request.url.clone()) + .body(request.body.clone()) + .timeout(PAYJOIN_REQUEST_TIMEOUT) + .headers(headers) + .send() + .await + .and_then(|r| r.error_for_status()) + .map_err(|e| { + log_error!(self.logger, "Failed to send Payjoin request: {}", e); + Error::PayjoinRequestSendingFailed + })?; + let response = response.bytes().await.map_err(|e| { + log_error!( + self.logger, + "Failed to send Payjoin request, receiver invalid response: {}", + e + ); + Error::PayjoinRequestSendingFailed + })?; + let response = response.to_vec(); + context.process_response(&mut response.as_slice()).map_err(|e| { + log_error!(self.logger, "Failed to process Payjoin response: {}", e); + Error::PayjoinResponseProcessingFailed + }) + } + + pub(crate) fn process_response( + &self, payjoin_proposal: &mut Psbt, original_psbt: &mut Psbt, + ) -> Result { + let wallet = self.wallet.clone(); + wallet.sign_payjoin_proposal(payjoin_proposal, original_psbt)?; + let proposal_tx = payjoin_proposal.clone().extract_tx(); + let mut transactions = self.transactions.write().unwrap(); + let pos = transactions.iter().position(|t| t.original_psbt() == original_psbt); + if let Some(pos) = pos { + let pj_tx = transactions.remove(pos); + let pj_tx = PayjoinTransaction::PendingFirstConfirmation { + original_psbt: original_psbt.clone(), + tx: proposal_tx.clone(), + receiver: pj_tx.receiver().clone(), + amount: pj_tx.amount().clone(), + }; + transactions.push(pj_tx.clone()); + let txid = proposal_tx.txid(); + // watch proposal transaction + self.chain_source.register_tx(&txid, Script::empty()); + self.event_queue.add_event(Event::PayjoinPaymentBroadcasted { + txid, + amount_sats: pj_tx.amount().to_sat(), + recipient: pj_tx.receiver().clone().into(), + })?; + Ok(proposal_tx) + } else { + log_error!(self.logger, "Failed to process Payjoin response: transaction not found"); + Err(Error::PayjoinResponseProcessingFailed) + } + } + + pub(crate) fn handle_request_failure( + &self, original_psbt: &Psbt, reason: PayjoinPaymentFailureReason, + ) -> Result<(), Error> { + let mut transactions = self.transactions.write().unwrap(); + let pos = transactions.iter().position(|t| t.original_psbt() == original_psbt); + if let Some(pos) = pos { + let tx = transactions.remove(pos); + let payment_id: [u8; 32] = + tx.original_psbt().unsigned_tx.txid()[..].try_into().map_err(|_| { + log_error!( + self.logger, + "Failed to handle request failure for Payjoin payment: invalid payment id" + ); + Error::PayjoinRequestSendingFailed + })?; + let mut update_details = PaymentDetailsUpdate::new(PaymentId(payment_id)); + update_details.status = Some(PaymentStatus::Failed); + let _ = self.payment_store.update(&update_details); + self.event_queue.add_event(Event::PayjoinPaymentFailed { + txid: original_psbt.unsigned_tx.txid(), + recipient: tx.receiver().clone().into(), + amount_sats: tx.amount().to_sat(), + reason, + }) + } else { + log_error!( + self.logger, + "Failed to handle request failure for Payjoin payment: transaction not found" + ); + Err(Error::PayjoinRequestSendingFailed) + } + } + + 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(); + *transactions = transactions + .iter() + .filter_map(|t| { + if t.txid() == confirmed_tx_txid { + match t { + PayjoinTransaction::PendingReceiverResponse { + original_psbt, + receiver, + amount, + } => { + println!("Payjoin receiver broadcasted original psbt transaction!"); + let _ = self.event_queue.add_event( + Event::PayjoinPaymentOriginalPsbtBroadcasted { + txid: confirmed_tx_txid, + amount_sats: amount.to_sat(), + recipient: receiver.clone().into(), + }, + ); + Some(PayjoinTransaction::PendingThresholdConfirmations { + original_psbt: original_psbt.clone(), + tx: tx.clone(), + receiver: receiver.clone(), + amount: *amount, + first_confirmation_height: height, + first_confirmation_hash: header.block_hash(), + }) + }, + PayjoinTransaction::PendingFirstConfirmation { + ref tx, + receiver, + amount, + original_psbt, + } => { + if confirmed_tx_txid == original_psbt.unsigned_tx.txid() { + let _ = self.event_queue.add_event( + Event::PayjoinPaymentOriginalPsbtBroadcasted { + txid: confirmed_tx_txid, + amount_sats: amount.to_sat(), + recipient: receiver.clone().into(), + }, + ); + }; + Some(PayjoinTransaction::PendingThresholdConfirmations { + original_psbt: original_psbt.clone(), + tx: tx.clone(), + receiver: receiver.clone(), + amount: *amount, + first_confirmation_height: height, + first_confirmation_hash: header.block_hash(), + }) + }, + _ => Some(t.clone()), + } + } else { + Some(t.clone()) + } + }) + .collect(); + } + + 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, + Some(first_confirmation_hash.clone()), + )), + _ => None, + }) + .collect::>() + } + + fn internal_best_block_updated(&self, height: u32, block_hash: BlockHash) { + let mut transactions = self.transactions.write().unwrap(); + *transactions = transactions + .iter() + .filter_map(|tx| { + if tx.is_waiting_first_confirmation() { + Some(PayjoinTransaction::PendingThresholdConfirmations { + original_psbt: tx.original_psbt().clone(), + tx: tx.payjoin_tx().unwrap().clone(), + amount: tx.amount().clone(), + receiver: tx.receiver().clone().into(), + first_confirmation_height: height, + first_confirmation_hash: block_hash, + }) + } else if let Some(first_conf) = tx.first_confirmation_height() { + if height - first_conf >= ANTI_REORG_DELAY { + let payment_id: [u8; 32] = + match tx.original_psbt().unsigned_tx.txid()[..].try_into() { + Ok(id) => id, + Err(e) => { + log_error!( + self.logger, + "Failed to get payment_id for {} : {}", + &tx, + e + ); + return None; + }, + }; + + 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::PayjoinPaymentSuccessful { + txid: tx.txid(), + amount_sats: tx.amount().to_sat(), + recipient: tx.receiver().clone().into(), + }); + None + } else { + Some(tx.clone()) + } + } else { + Some(tx.clone()) + } + }) + .collect(); + } +} + +impl Confirm for PayjoinHandler { + fn transactions_confirmed(&self, header: &Header, txdata: &TransactionData, height: u32) { + self.internal_transactions_confirmed(header, txdata, height); + } + + fn get_relevant_txids(&self) -> Vec<(Txid, u32, Option)> { + self.internal_get_relevant_txids() + } + + fn best_block_updated(&self, header: &Header, height: u32) { + let block_hash = header.block_hash(); + self.internal_best_block_updated(height, block_hash); + } + + fn transaction_unconfirmed(&self, _txid: &Txid) {} +} + +#[derive(Clone, Debug)] +enum PayjoinTransaction { + PendingReceiverResponse { + original_psbt: Psbt, + receiver: Address, + amount: Amount, + }, + 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 std::fmt::Display for PayjoinTransaction { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + PayjoinTransaction::PendingReceiverResponse { original_psbt, .. } => { + write!(f, "PendingReceiverResponse({})", original_psbt.unsigned_tx.txid()) + }, + PayjoinTransaction::PendingFirstConfirmation { tx, .. } => { + write!(f, "PendingFirstConfirmation({})", tx.txid()) + }, + PayjoinTransaction::PendingThresholdConfirmations { tx, .. } => { + write!(f, "PendingThresholdConfirmations({})", tx.txid()) + }, + } + } +} + +impl PayjoinTransaction { + fn new(original_psbt: Psbt, receiver: Address, amount: Amount) -> Self { + PayjoinTransaction::PendingReceiverResponse { original_psbt, receiver, amount } + } + + fn txid(&self) -> Txid { + match self { + PayjoinTransaction::PendingReceiverResponse { original_psbt, .. } => { + original_psbt.unsigned_tx.txid() + }, + PayjoinTransaction::PendingFirstConfirmation { tx, .. } => tx.txid(), + PayjoinTransaction::PendingThresholdConfirmations { tx, .. } => tx.txid(), + } + } + + fn original_psbt(&self) -> &Psbt { + match self { + PayjoinTransaction::PendingReceiverResponse { original_psbt, .. } => original_psbt, + PayjoinTransaction::PendingFirstConfirmation { original_psbt, .. } => original_psbt, + PayjoinTransaction::PendingThresholdConfirmations { original_psbt, .. } => { + original_psbt + }, + } + } + + fn first_confirmation_height(&self) -> Option { + match self { + PayjoinTransaction::PendingReceiverResponse { .. } => None, + PayjoinTransaction::PendingFirstConfirmation { .. } => None, + PayjoinTransaction::PendingThresholdConfirmations { + first_confirmation_height, .. + } => Some(*first_confirmation_height), + } + } + + fn payjoin_tx(&self) -> Option<&Transaction> { + match self { + PayjoinTransaction::PendingReceiverResponse { .. } => None, + PayjoinTransaction::PendingFirstConfirmation { tx, .. } => Some(tx), + PayjoinTransaction::PendingThresholdConfirmations { tx, .. } => Some(tx), + } + } + + fn amount(&self) -> &Amount { + match self { + PayjoinTransaction::PendingReceiverResponse { amount, .. } => amount, + PayjoinTransaction::PendingFirstConfirmation { amount, .. } => amount, + PayjoinTransaction::PendingThresholdConfirmations { amount, .. } => amount, + } + } + + fn receiver(&self) -> &Address { + match self { + PayjoinTransaction::PendingReceiverResponse { receiver, .. } => receiver, + PayjoinTransaction::PendingFirstConfirmation { receiver, .. } => receiver, + PayjoinTransaction::PendingThresholdConfirmations { receiver, .. } => receiver, + } + } + + fn is_waiting_first_confirmation(&self) -> bool { + matches!(self, PayjoinTransaction::PendingFirstConfirmation { .. }) + } +} diff --git a/src/payment/payjoin/mod.rs b/src/payment/payjoin/mod.rs new file mode 100644 index 000000000..456717b0b --- /dev/null +++ b/src/payment/payjoin/mod.rs @@ -0,0 +1,180 @@ +use crate::config::{PAYJOIN_REQUEST_TOTAL_DURATION, PAYJOIN_RETRY_INTERVAL}; +use crate::event::PayjoinPaymentFailureReason; +use crate::logger::{FilesystemLogger, Logger}; +use crate::types::Broadcaster; +use crate::{error::Error, Config}; + +use lightning::chain::chaininterface::BroadcasterInterface; +use lightning::{log_error, log_info}; + +use std::sync::{Arc, RwLock}; + +pub(crate) mod handler; +use handler::PayjoinHandler; + +/// Payjoin payment handler. +/// +/// 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 { + config: Arc, + logger: Arc, + payjoin_handler: Option>, + runtime: Arc>>>, + tx_broadcaster: Arc, +} + +impl PayjoinPayment { + pub(crate) fn new( + config: Arc, logger: Arc, + payjoin_handler: Option>, + runtime: Arc>>>, + tx_broadcaster: Arc, + ) -> Self { + Self { config, logger, payjoin_handler, runtime, tx_broadcaster } + } + + /// 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. + /// + /// Due to the asynchronous nature of the Payjoin process, this method will return immediately + /// after constructing 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::PayjoinPaymentSuccessful`] event will be added to the event queue. + /// Otherwise, [`Event::PayjoinPaymentFailed`] 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 Block-chain 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::PayjoinPaymentSuccessful`]: crate::Event::PayjoinPaymentSuccessful + /// [`Event::PayjoinPaymentFailed`]: crate::Event::PayjoinPaymentFailed + 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 = payjoin_handler.start_request(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); + 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_failure(&original_psbt, PayjoinPaymentFailureReason::Timeout); + 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)) => { + match payjoin_handler.process_response(&mut proposal, &mut original_psbt.clone()) { + Ok(tx) => { + tx_broadcaster.broadcast_transactions(&[&tx]); + }, + Err(e) => { + log_error!(logger, "Failed to process Payjoin response: {}", e); + let _ = payjoin_handler.handle_request_failure(&original_psbt, PayjoinPaymentFailureReason::ResponseProcessingFailed); + }, + }; + break; + }, + Ok(None) => { + log_info!(logger, "Payjoin request sent, waiting for response..."); + continue; + } + Err(e) => { + log_error!(logger, "Failed to send Payjoin request : {}", e); + let _ = payjoin_handler.handle_request_failure(&original_psbt, PayjoinPaymentFailureReason::RequestSendingFailed); + break; + }, + } + } + } + } + }); + 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. + /// + /// Due to the asynchronous nature of the Payjoin process, this method will return immediately + /// after constructing 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::PayjoinPaymentSuccessful`] event will be added to the event queue. + /// Otherwise, [`Event::PayjoinPaymentFailed`] 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 Block-chain 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::PayjoinPaymentSuccessful`]: crate::Event::PayjoinPaymentSuccessful + /// [`Event::PayjoinPaymentFailed`]: crate::Event::PayjoinPaymentFailed + 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()) + } +} 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/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 7c2142091..cf7d8115f 100644 --- a/src/uniffi_types.rs +++ b/src/uniffi_types.rs @@ -17,7 +17,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; @@ -37,6 +37,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..80d56542d 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}; @@ -32,7 +33,7 @@ use bitcoin::psbt::PartiallySignedTransaction; use bitcoin::secp256k1::ecdh::SharedSecret; use bitcoin::secp256k1::ecdsa::{RecoverableSignature, Signature}; use bitcoin::secp256k1::{PublicKey, Scalar, Secp256k1, SecretKey, Signing}; -use bitcoin::{ScriptBuf, Transaction, TxOut, Txid}; +use bitcoin::{Amount, ScriptBuf, Transaction, TxOut, Txid}; use std::ops::{Deref, DerefMut}; use std::sync::{Arc, Mutex, RwLock}; @@ -149,6 +150,60 @@ where res } + pub(crate) fn build_payjoin_transaction( + &self, amount: Amount, recipient: ScriptBuf, + ) -> Result { + let fee_rate = self + .fee_estimator + .get_est_sat_per_1000_weight(ConfirmationTarget::OutputSpendingFee) as f32; + let fee_rate = FeeRate::from_sat_per_kwu(fee_rate); + let locked_wallet = self.inner.lock().unwrap(); + let mut tx_builder = locked_wallet.build_tx(); + tx_builder.add_recipient(recipient, amount.to_sat()).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) + } + 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..91592d3d3 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -147,6 +147,74 @@ 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::PayjoinPaymentSuccessful { 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; + +macro_rules! expect_payjoin_tx_broadcasted { + ($node: expr) => {{ + match $node.wait_next_event() { + ref e @ Event::PayjoinPaymentBroadcasted { txid, .. } => { + println!("{} got event {:?}", $node.node_id(), e); + $node.event_handled(); + txid + }, + ref e => { + panic!("{} got unexpected event!: {:?}", std::stringify!($node), e); + }, + } + }}; +} + +pub(crate) use expect_payjoin_tx_broadcasted; + +macro_rules! expect_payjoin_original_psbt_broadcasted { + ($node: expr) => {{ + match $node.wait_next_event() { + ref e @ Event::PayjoinPaymentOriginalPsbtBroadcasted { 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_original_psbt_broadcasted; + pub(crate) fn setup_bitcoind_and_electrsd() -> (BitcoinD, ElectrsD) { let bitcoind_exe = env::var("BITCOIND_EXE").ok().or_else(|| bitcoind::downloaded_exe_path().ok()).expect( @@ -270,6 +338,20 @@ pub(crate) fn setup_node(electrsd: &ElectrsD, config: Config) -> TestNode { node } +pub(crate) fn setup_payjoin_node(electrsd: &ElectrsD, config: Config) -> TestNode { + let esplora_url = format!("http://{}", electrsd.esplora_url.as_ref().unwrap()); + setup_builder!(builder, config); + builder.set_esplora_server(esplora_url.clone()); + let payjoin_relay = "https://pj.bobspacebkk.com".to_string(); + builder.set_payjoin_config(payjoin_relay).unwrap(); + let test_sync_store = Arc::new(TestSyncStore::new(config.storage_dir_path.into())); + let node = builder.build_with_store(test_sync_store).unwrap(); + node.start().unwrap(); + assert!(node.status().is_running); + assert!(node.status().latest_fee_rate_cache_update_timestamp.is_some()); + node +} + pub(crate) fn generate_blocks_and_wait( bitcoind: &BitcoindClient, electrs: &E, num: usize, ) { diff --git a/tests/integration_tests_payjoin.rs b/tests/integration_tests_payjoin.rs new file mode 100644 index 000000000..b6a8fa67d --- /dev/null +++ b/tests/integration_tests_payjoin.rs @@ -0,0 +1,224 @@ +mod common; + +use common::{ + expect_payjoin_tx_sent_successfully_event, generate_blocks_and_wait, + premine_and_distribute_funds, setup_bitcoind_and_electrsd, wait_for_tx, +}; + +use bitcoin::Amount; +use ldk_node::{ + payment::{PaymentDirection, PaymentKind, PaymentStatus}, + Event, +}; +use payjoin::{ + receive::v2::{Enrolled, Enroller}, + OhttpKeys, PjUriBuilder, +}; + +use crate::common::{ + expect_payjoin_original_psbt_broadcasted, expect_payjoin_tx_broadcasted, + expect_payjoin_tx_pending_event, random_config, setup_payjoin_node, setup_two_nodes, +}; + +struct PayjoinReceiver { + ohttp_keys: OhttpKeys, + enrolled: Enrolled, +} + +impl PayjoinReceiver { + fn enroll() -> Self { + let payjoin_directory = payjoin::Url::parse("https://payjo.in").unwrap(); + let payjoin_relay = payjoin::Url::parse("https://pj.bobspacebkk.com").unwrap(); + let ohttp_keys = { + let payjoin_directory = payjoin_directory.join("/ohttp-keys").unwrap(); + let proxy = reqwest::Proxy::all(payjoin_relay.clone()).unwrap(); + let client = reqwest::blocking::Client::builder().proxy(proxy).build().unwrap(); + let response = client.get(payjoin_directory).send().unwrap(); + let response = response.bytes().unwrap(); + OhttpKeys::decode(response.to_vec().as_slice()).unwrap() + }; + let mut enroller = Enroller::from_directory_config( + payjoin_directory.clone(), + ohttp_keys.clone(), + payjoin_relay.clone(), + ); + let (req, ctx) = enroller.extract_req().unwrap(); + let mut headers = reqwest::header::HeaderMap::new(); + headers.insert( + reqwest::header::CONTENT_TYPE, + reqwest::header::HeaderValue::from_static("message/ohttp-req"), + ); + let response = reqwest::blocking::Client::new() + .post(&req.url.to_string()) + .body(req.body) + .headers(headers) + .send() + .unwrap(); + let response = match response.bytes() { + Ok(response) => response, + Err(_) => { + panic!("Error reading response"); + }, + }; + let enrolled = enroller.process_res(response.to_vec().as_slice(), ctx).unwrap(); + Self { ohttp_keys, enrolled } + } + + pub(crate) fn receive( + &self, amount: bitcoin::Amount, receiving_address: bitcoin::Address, + ) -> String { + let enrolled = self.enrolled.clone(); + let fallback_target = enrolled.fallback_target(); + let ohttp_keys = self.ohttp_keys.clone(); + let pj_part = payjoin::Url::parse(&fallback_target).unwrap(); + let payjoin_uri = PjUriBuilder::new(receiving_address, pj_part, Some(ohttp_keys.clone())) + .amount(amount) + .build(); + payjoin_uri.to_string() + } + + pub(crate) fn process_payjoin_request(self, substitue_address: Option) { + let mut enrolled = self.enrolled; + let (req, context) = enrolled.extract_req().unwrap(); + + let client = reqwest::blocking::Client::new(); + let response = client + .post(req.url.to_string()) + .body(req.body) + .headers(PayjoinReceiver::ohttp_headers()) + .send() + .unwrap(); + let response = response.bytes().unwrap(); + let response = enrolled.process_res(response.to_vec().as_slice(), context).unwrap(); + let unchecked_proposal = response.unwrap(); + + let proposal = unchecked_proposal.assume_interactive_receiver(); + let proposal = proposal.check_inputs_not_owned(|_script| Ok(false)).unwrap(); + let proposal = proposal.check_no_mixed_input_scripts().unwrap(); + let proposal = proposal.check_no_inputs_seen_before(|_outpoint| Ok(false)).unwrap(); + let mut provisional_proposal = + proposal.identify_receiver_outputs(|_script| Ok(true)).unwrap(); + + if let Some(substitue_address) = substitue_address { + provisional_proposal.substitute_output_address(substitue_address); + } + + // Finalise Payjoin Proposal + let mut payjoin_proposal = + provisional_proposal.finalize_proposal(|psbt| Ok(psbt.clone()), None).unwrap(); + + let (receiver_request, _) = payjoin_proposal.extract_v2_req().unwrap(); + reqwest::blocking::Client::new() + .post(&receiver_request.url.to_string()) + .body(receiver_request.body) + .headers(PayjoinReceiver::ohttp_headers()) + .send() + .unwrap(); + } + + fn ohttp_headers() -> reqwest::header::HeaderMap { + let mut headers = reqwest::header::HeaderMap::new(); + headers.insert( + reqwest::header::CONTENT_TYPE, + reqwest::header::HeaderValue::from_static("message/ohttp-req"), + ); + headers + } +} + +#[ignore] +#[test] +fn send_payjoin_transaction_original_psbt_used() { + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let config_a = random_config(false); + let (receiver, _) = setup_two_nodes(&electrsd, false, false, false); + let payjoin_sender = setup_payjoin_node(&electrsd, config_a); + let addr_a = payjoin_sender.onchain_payment().new_address().unwrap(); + let premine_amount_sat = 100_000_00; + premine_and_distribute_funds( + &bitcoind.client, + &electrsd.client, + vec![addr_a], + Amount::from_sat(premine_amount_sat), + ); + payjoin_sender.sync_wallets().unwrap(); + assert_eq!(payjoin_sender.list_balances().spendable_onchain_balance_sats, premine_amount_sat); + assert_eq!(payjoin_sender.list_balances().spendable_onchain_balance_sats, 100_000_00); + assert_eq!(payjoin_sender.next_event(), None); + + let payjoin_receiver = PayjoinReceiver::enroll(); + let payjoin_uri = payjoin_receiver + .receive(Amount::from_sat(80_000), receiver.onchain_payment().new_address().unwrap()); + + assert!(payjoin_sender.payjoin_payment().send(payjoin_uri).is_ok()); + + let txid = expect_payjoin_tx_pending_event!(payjoin_sender); + let payments = payjoin_sender.list_payments(); + let payment = payments.first().unwrap(); + assert_eq!(payment.amount_msat, Some(80_000)); + assert_eq!(payment.status, PaymentStatus::Pending); + assert_eq!(payment.direction, PaymentDirection::Outbound); + assert_eq!(payment.kind, PaymentKind::Payjoin); + + payjoin_receiver.process_payjoin_request(None); + + expect_payjoin_tx_broadcasted!(payjoin_sender); + + wait_for_tx(&electrsd.client, txid); + generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 3); + payjoin_sender.sync_wallets().unwrap(); + expect_payjoin_original_psbt_broadcasted!(&payjoin_sender); + generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 4); + payjoin_sender.sync_wallets().unwrap(); + + expect_payjoin_tx_sent_successfully_event!(payjoin_sender); +} + +#[test] +fn send_payjoin_transaction() { + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let config_a = random_config(false); + let (receiver, _) = setup_two_nodes(&electrsd, false, false, false); + let payjoin_sender = setup_payjoin_node(&electrsd, config_a); + let addr_a = payjoin_sender.onchain_payment().new_address().unwrap(); + let premine_amount_sat = 100_000_00; + premine_and_distribute_funds( + &bitcoind.client, + &electrsd.client, + vec![addr_a], + Amount::from_sat(premine_amount_sat), + ); + payjoin_sender.sync_wallets().unwrap(); + assert_eq!(payjoin_sender.list_balances().spendable_onchain_balance_sats, premine_amount_sat); + assert_eq!(payjoin_sender.list_balances().spendable_onchain_balance_sats, 100_000_00); + assert_eq!(payjoin_sender.next_event(), None); + + let payjoin_receiver = PayjoinReceiver::enroll(); + let payjoin_uri = payjoin_receiver + .receive(Amount::from_sat(80_000), receiver.onchain_payment().new_address().unwrap()); + + assert!(payjoin_sender.payjoin_payment().send(payjoin_uri).is_ok()); + + let _txid = expect_payjoin_tx_pending_event!(payjoin_sender); + let payments = payjoin_sender.list_payments(); + let payment = payments.first().unwrap(); + assert_eq!(payment.amount_msat, Some(80_000)); + assert_eq!(payment.status, PaymentStatus::Pending); + assert_eq!(payment.direction, PaymentDirection::Outbound); + assert_eq!(payment.kind, PaymentKind::Payjoin); + + let substitue_address = receiver.onchain_payment().new_address().unwrap(); + payjoin_receiver.process_payjoin_request(Some(substitue_address)); + + let txid = expect_payjoin_tx_broadcasted!(payjoin_sender); + + wait_for_tx(&electrsd.client, txid); + generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 1); + payjoin_sender.sync_wallets().unwrap(); + generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 3); + payjoin_sender.sync_wallets().unwrap(); + generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 4); + payjoin_sender.sync_wallets().unwrap(); + + expect_payjoin_tx_sent_successfully_event!(payjoin_sender); +}