diff --git a/Cargo.toml b/Cargo.toml index d4a87b2a2..8e7bc921a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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", "v2"] } [target.'cfg(vss)'.dependencies] vss-client = "0.2" diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index 2723db573..f4cb31fec 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,12 @@ enum NodeError { "InsufficientFunds", "LiquiditySourceUnavailable", "LiquidityFeeTooHigh", + "PayjoinUnavailable", + "PayjoinUriInvalid", + "PayjoinRequestMissingAmount", + "PayjoinRequestCreationFailed", + "PayjoinRequestSendingFailed", + "PayjoinResponseProcessingFailed", }; dictionary NodeStatus { @@ -227,6 +241,7 @@ enum BuildError { "KVStoreSetupFailed", "WalletSetupFailed", "LoggerSetupFailed", + "InvalidPayjoinConfig", }; [Enum] @@ -238,6 +253,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 +267,12 @@ enum PaymentFailureReason { "UnexpectedError", }; +enum PayjoinPaymentFailureReason { + "Timeout", + "TransactionFinalisationFailed", + "InvalidReceiverResponse", +}; + [Enum] interface ClosureReason { CounterpartyForceClosed(UntrustedString peer_msg); @@ -274,6 +298,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 +524,6 @@ typedef string Mnemonic; [Custom] typedef string UntrustedString; + +[Custom] +typedef string ScriptBuf; diff --git a/src/builder.rs b/src/builder.rs index a2a93aa79..76ea8e64d 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,18 @@ 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_broadcaster), + Arc::clone(&logger), + pj_config.payjoin_relay.clone(), + Arc::clone(&tx_sync), + Arc::clone(&event_queue), + Arc::clone(&wallet), + Arc::clone(&payment_store), + )) + }); + 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 +1030,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..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..b489e9366 100644 --- a/src/error.rs +++ b/src/error.rs @@ -95,6 +95,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 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, + /// Failed to send Payjoin request. + PayjoinRequestSendingFailed, + /// Payjoin response processing failed. + PayjoinResponseProcessingFailed, } impl fmt::Display for Error { @@ -162,6 +174,24 @@ 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::PayjoinRequestSendingFailed => { + write!(f, "Failed to send Payjoin request") + }, + Self::PayjoinResponseProcessingFailed => { + write!(f, "Payjoin receiver responded to our request with an invalid response that was ignored") + }, } } } diff --git a/src/event.rs b/src/event.rs index e319ab5e4..ba5d1f72f 100644 --- a/src/event.rs +++ b/src/event.rs @@ -24,6 +24,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 +144,70 @@ 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, +} + +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), + _ => 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), + } + } } impl_writeable_tlv_based_enum!(Event, @@ -184,6 +249,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), }; ); diff --git a/src/lib.rs b/src/lib.rs index 1c137d355..8aaf2e30d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -109,6 +109,7 @@ pub use error::Error as NodeError; use error::Error; pub use event::Event; +use payment::payjoin::handler::PayjoinHandler; pub use types::ChannelConfig; pub use io::utils::generate_entropy_mnemonic; @@ -133,7 +134,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 +189,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, @@ -1072,6 +1077,42 @@ 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(); + PayjoinPayment::new( + Arc::clone(&self.runtime), + payjoin_handler.map(Arc::clone), + Arc::clone(&self.config), + Arc::clone(&self.logger), + Arc::clone(&self.wallet), + ) + } + + /// 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(); + Arc::new(PayjoinPayment::new( + Arc::clone(&self.runtime), + payjoin_handler.map(Arc::clone), + Arc::clone(&self.config), + Arc::clone(&self.logger), + Arc::clone(&self.wallet), + )) + } + /// Retrieve a list of known channels. pub fn list_channels(&self) -> Vec { self.channel_manager.list_channels().into_iter().map(|c| c.into()).collect() 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..a0660f124 --- /dev/null +++ b/src/payment/payjoin/handler.rs @@ -0,0 +1,329 @@ +use crate::config::PAYJOIN_REQUEST_TIMEOUT; +use crate::error::Error; +use crate::event::PayjoinPaymentFailureReason; +use crate::logger::FilesystemLogger; +use crate::payment::{PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus}; +use crate::types::{Broadcaster, 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::chaininterface::BroadcasterInterface; +use lightning::chain::channelmonitor::ANTI_REORG_DELAY; +use lightning::chain::transaction::TransactionData; +use lightning::chain::{Confirm, Filter, WatchedOutput}; +use lightning::ln::channelmanager::PaymentId; +use lightning::log_error; +use lightning::util::logger::Logger; +use payjoin::send::{ContextV2, RequestBuilder}; +use rand::RngCore; + +use std::sync::{Arc, RwLock}; + +pub(crate) struct PayjoinHandler { + tx_broadcaster: Arc, + logger: Arc, + payjoin_relay: payjoin::Url, + chain_source: Arc, + transactions: RwLock>, + event_queue: Arc, + wallet: Arc, + payment_store: Arc, +} + +impl PayjoinHandler { + pub(crate) fn new( + tx_broadcaster: Arc, logger: Arc, + payjoin_relay: payjoin::Url, chain_source: Arc, event_queue: Arc, + wallet: Arc, payment_store: Arc, + ) -> Self { + Self { + tx_broadcaster, + logger, + payjoin_relay, + transactions: RwLock::new(Vec::new()), + chain_source, + event_queue, + wallet, + payment_store, + } + } + + pub(crate) async fn send_payjoin_transaction( + &self, original_psbt: &mut Psbt, payjoin_uri: payjoin::Uri<'_, NetworkChecked>, + ) -> Result, Error> { + let (request, context) = + 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) + .unwrap(); + let mut random_bytes = [0u8; 32]; + rand::thread_rng().fill_bytes(&mut random_bytes); + self.payment_store.insert(PaymentDetails::new( + PaymentId(random_bytes), + PaymentKind::Payjoin, + payjoin_uri.amount.map(|a| a.to_sat()), + PaymentDirection::Outbound, + PaymentStatus::Pending, + ))?; + let response = self.send_payjoin_ohttp_request(&request).await?; + self.handle_payjoin_transaction_response(response, context, original_psbt, payjoin_uri) + .await + } + + pub(crate) async fn handle_payjoin_transaction_response( + &self, response: Vec, context: ContextV2, original_psbt: &mut Psbt, + payjoin_uri: payjoin::Uri<'_, NetworkChecked>, + ) -> Result, Error> { + let amount = match payjoin_uri.amount { + Some(amt) => amt.to_sat(), + None => return Err(Error::PayjoinRequestMissingAmount), + }; + match context.process_response(&mut response.as_slice()) { + Ok(Some(pj_proposal)) => { + let pj_proposal = &mut pj_proposal.clone(); + let tx = + self.finalise_tx(pj_proposal, &mut original_psbt.clone(), payjoin_uri.clone())?; + self.tx_broadcaster.broadcast_transactions(&[&tx]); + let txid = tx.txid(); + let _ = self.event_queue.add_event(Event::PayjoinPaymentPending { + txid, + amount, + receipient: payjoin_uri.address.clone().into(), + }); + Ok(Some(txid)) + }, + Ok(None) => Ok(None), + Err(_e) => { + let _ = self.event_queue.add_event(Event::PayjoinPaymentFailed { + txid: None, + amount, + receipient: payjoin_uri.address.clone().into(), + reason: PayjoinPaymentFailureReason::InvalidReceiverResponse, + }); + return Err(Error::PayjoinResponseProcessingFailed); + }, + } + } + + fn finalise_tx( + &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(); + if let Some(our_output) = + tx.output.iter().find(|output| wallet.is_mine(&output.script_pubkey).unwrap_or(false)) + { + let mut transactions = self.transactions.write().unwrap(); + let pj_tx = PayjoinTransaction::new( + tx.clone(), + payjoin_uri.address, + payjoin_uri.amount.unwrap_or_default(), + ); + transactions.push(pj_tx); + self.register_tx(&tx.txid(), &our_output.script_pubkey); + Ok(tx) + } else { + Err(Error::PaymentSendingFailed) + } + } + + pub(crate) fn timeout_payjoin_transaction( + &self, payjoin_uri: payjoin::Uri<'_, NetworkChecked>, + ) -> Result<(), Error> { + let amount = match payjoin_uri.amount { + Some(amt) => amt.to_sat(), + None => return Err(Error::PayjoinRequestMissingAmount), + }; + let _ = self.event_queue.add_event(Event::PayjoinPaymentFailed { + txid: None, + receipient: payjoin_uri.address.clone().into(), + amount, + reason: PayjoinPaymentFailureReason::Timeout, + }); + Ok(()) + } + + async fn send_payjoin_ohttp_request( + &self, request: &payjoin::Request, + ) -> Result, Error> { + 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 make Payjoin HTTP request: {}", e); + Error::PayjoinRequestSendingFailed + })?; + let response = response.bytes().await.map_err(|e| { + log_error!(self.logger, "Failed to parse Payjoin HTTP response: {}", e); + Error::PayjoinRequestSendingFailed + })?; + Ok(response.to_vec()) + } + + 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(); + if let Some(position) = + transactions.iter().position(|o| o.txid() == Some(confirmed_tx_txid)) + { + let tx = transactions.remove(position); + tx.to_pending_threshold_confirmations(height, header.block_hash()); + } else { + log_error!( + self.logger, + "Notified about UNTRACKED confirmed payjoin transaction {}", + confirmed_tx_txid + ); + }; + } + + 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 _ = self.event_queue.add_event(Event::PayjoinPaymentSuccess { + txid, + amount: tx.amount().to_sat(), + receipient: tx.receiver().into(), + }); + false + } else { + true + } + } else { + true + } + }); + } + + 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::>() + } +} + +#[derive(Clone, Debug)] +enum PayjoinTransaction { + // PendingReceiverResponse, + PendingFirstConfirmation { + tx: Transaction, + receiver: Address, + amount: Amount, + }, + PendingThresholdConfirmations { + tx: Transaction, + receiver: Address, + amount: Amount, + first_confirmation_height: u32, + first_confirmation_hash: BlockHash, + }, +} + +impl PayjoinTransaction { + fn new(tx: Transaction, receiver: Address, amount: Amount) -> Self { + PayjoinTransaction::PendingFirstConfirmation { tx, receiver, amount } + } + fn txid(&self) -> Option { + match self { + PayjoinTransaction::PendingFirstConfirmation { tx, .. } => Some(tx.txid()), + PayjoinTransaction::PendingThresholdConfirmations { tx, .. } => Some(tx.txid()), + } + } + 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(), + } + } + + fn to_pending_threshold_confirmations(&self, height: u32, hash: BlockHash) -> Self { + match self { + PayjoinTransaction::PendingFirstConfirmation { tx, receiver, amount } => { + PayjoinTransaction::PendingThresholdConfirmations { + tx: tx.clone(), + receiver: receiver.clone(), + amount: *amount, + first_confirmation_height: height, + first_confirmation_hash: hash, + } + }, + _ => unreachable!(), + } + } +} + +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 Confirm for PayjoinHandler { + fn transactions_confirmed(&self, header: &Header, txdata: &TransactionData, height: u32) { + self.internal_transactions_confirmed(header, txdata, height); + } + + 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() + } + + fn transaction_unconfirmed(&self, _txid: &Txid) {} +} diff --git a/src/payment/payjoin/mod.rs b/src/payment/payjoin/mod.rs new file mode 100644 index 000000000..e2d29ece2 --- /dev/null +++ b/src/payment/payjoin/mod.rs @@ -0,0 +1,180 @@ +//! Holds a payment handler allowing to send Payjoin payments. + +use lightning::util::logger::Logger; +use lightning::{log_error, log_info}; + +use crate::config::{PAYJOIN_REQUEST_TOTAL_DURATION, PAYJOIN_RETRY_INTERVAL}; +use crate::logger::FilesystemLogger; +use crate::types::Wallet; +use crate::{error::Error, Config}; + +use std::sync::{Arc, RwLock}; + +pub(crate) mod handler; + +use handler::PayjoinHandler; + +/// 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>>>, + handler: Option>, + config: Arc, + logger: Arc, + wallet: Arc, +} + +impl PayjoinPayment { + pub(crate) fn new( + runtime: Arc>>>, + handler: Option>, config: Arc, logger: Arc, + wallet: Arc, + ) -> Self { + Self { runtime, handler, config, logger, wallet } + } + + /// 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::PayjoinPaymentSuccess`] 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 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::PayjoinPaymentSuccess`]: crate::Event::PayjoinPaymentSuccess + /// [`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_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 mut original_psbt = self.wallet.build_payjoin_transaction(payjoin_uri.clone())?; + let payjoin_handler = self.handler.as_ref().ok_or(Error::PayjoinUnavailable)?; + let payjoin_handler = Arc::clone(payjoin_handler); + let logger = Arc::clone(&self.logger); + log_info!(logger, "Sending Payjoin request to: {}", payjoin_uri.address); + rt_lock.as_ref().unwrap().spawn(async move { + let mut interval = tokio::time::interval(PAYJOIN_RETRY_INTERVAL); + loop { + tokio::select! { + _ = tokio::time::sleep(PAYJOIN_REQUEST_TOTAL_DURATION) => { + log_error!(logger, "Payjoin request timed out."); + let _ = payjoin_handler.timeout_payjoin_transaction(payjoin_uri.clone()); + break; + } + _ = interval.tick() => { + match payjoin_handler.send_payjoin_transaction(&mut original_psbt, payjoin_uri.clone()).await { + Ok(Some(_)) => { + log_info!(logger, "Payjoin transaction sent successfully."); + break + }, + Ok(None) => { + log_info!(logger, "No Payjoin response yet."); + continue + }, + Err(e) => { + log_error!(logger, "Failed to process Payjoin receiver response: {}.", e); + 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::PayjoinPaymentSuccess`] 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 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::PayjoinPaymentSuccess`]: crate::Event::PayjoinPaymentSuccess + /// [`Event::PayjoinPaymentFailed`]: crate::Event::PayjoinPaymentFailed + pub fn send_with_amount(&self, payjoin_uri: String, amount_sats: u64) -> Result<(), Error> { + let payjoin_uri = match payjoin::Uri::try_from(payjoin_uri) { + Ok(uri) => uri, + Err(_) => return Err(Error::PayjoinUriInvalid), + }; + let mut payjoin_uri = match payjoin_uri.require_network(self.config.network) { + Ok(uri) => uri, + Err(_) => return 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 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..6ef136141 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}; @@ -21,7 +22,7 @@ use bdk::wallet::AddressIndex; use bdk::{Balance, FeeRate}; 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; @@ -149,6 +150,72 @@ where res } + 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 locked_wallet = self.inner.lock().unwrap(); + 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) + } + + 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,