From 0c684a94e1f35593223c41100cebced800886130 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 | 1 + bindings/ldk_node.udl | 16 ++++ src/builder.rs | 52 +++++++++++- src/config.rs | 9 ++ src/error.rs | 25 ++++++ src/event.rs | 21 +++++ src/io/utils.rs | 9 ++ src/lib.rs | 43 +++++++++- src/payjoin_sender.rs | 173 ++++++++++++++++++++++++++++++++++++++ src/payment/mod.rs | 2 + src/payment/payjoin.rs | 184 +++++++++++++++++++++++++++++++++++++++++ src/types.rs | 3 + src/wallet.rs | 36 ++++++++ 13 files changed, 569 insertions(+), 5 deletions(-) create mode 100644 src/payjoin_sender.rs create mode 100644 src/payment/payjoin.rs 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..d17b0cd66 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,11 @@ enum NodeError { "InsufficientFunds", "LiquiditySourceUnavailable", "LiquidityFeeTooHigh", + "PayjoinUnavailable", + "PayjoinUriInvalid", + "PayjoinRequestMissingAmount", + "PayjoinRequestCreationFailed", + "PayjoinResponseProcessingFailed", }; dictionary NodeStatus { @@ -227,6 +240,7 @@ enum BuildError { "KVStoreSetupFailed", "WalletSetupFailed", "LoggerSetupFailed", + "InvalidPayjoinConfig", }; [Enum] @@ -238,6 +252,8 @@ 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); + PayjoinTxSendSuccess(Txid txid); + PayjoinTxSendFailed(string reason); }; enum PaymentFailureReason { diff --git a/src/builder.rs b/src/builder.rs index a2a93aa79..612539635 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -16,7 +16,7 @@ use crate::peer_store::PeerStore; use crate::tx_broadcaster::TransactionBroadcaster; use crate::types::{ ChainMonitor, ChannelManager, DynStore, GossipSync, Graph, KeysManager, MessageRouter, - OnionMessenger, PeerManager, + OnionMessenger, PayjoinSender, PeerManager, }; use crate::wallet::Wallet; use crate::{LogLevel, Node}; @@ -93,6 +93,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 +137,8 @@ pub enum BuildError { WalletSetupFailed, /// We failed to setup the logger. LoggerSetupFailed, + /// Invalid Payjoin configuration. + InvalidPayjoinConfig, } impl fmt::Display for BuildError { @@ -152,6 +159,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 +183,7 @@ pub struct NodeBuilder { chain_data_source_config: Option, gossip_source_config: Option, liquidity_source_config: Option, + payjoin_config: Option, } impl NodeBuilder { @@ -187,12 +199,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 +261,15 @@ 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,15 @@ 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 +555,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 +1001,16 @@ fn build_with_store_internal( let (stop_sender, _) = tokio::sync::watch::channel(()); let (event_handling_stopped_sender, _) = tokio::sync::watch::channel(()); + let mut payjoin_sender = None; + if let Some(pj_config) = payjoin_config { + payjoin_sender = Some(Arc::new(PayjoinSender::new( + Arc::clone(&logger), + Arc::clone(&wallet), + Arc::clone(&tx_broadcaster), + pj_config.payjoin_relay.clone(), + ))); + } + let is_listening = Arc::new(AtomicBool::new(false)); let latest_wallet_sync_timestamp = Arc::new(RwLock::new(None)); let latest_onchain_wallet_sync_timestamp = Arc::new(RwLock::new(None)); @@ -987,6 +1032,7 @@ fn build_with_store_internal( channel_manager, chain_monitor, output_sweeper, + payjoin_sender, 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..1ea1b5e64 100644 --- a/src/error.rs +++ b/src/error.rs @@ -95,6 +95,16 @@ pub enum Error { LiquiditySourceUnavailable, /// The given operation failed due to the LSP's required opening fee being too high. LiquidityFeeTooHigh, + /// Failed to access Payjoin sender object. + PayjoinUnavailable, + /// Payjoin URI is invalid. + PayjoinUriInvalid, + /// Amount is neither user-provided nor defined in the URI. + PayjoinRequestMissingAmount, + /// Failed to build a Payjoin request. + PayjoinRequestCreationFailed, + /// Payjoin response processing failed. + PayjoinResponseProcessingFailed, } impl fmt::Display for Error { @@ -162,6 +172,21 @@ impl fmt::Display for Error { Self::LiquidityFeeTooHigh => { write!(f, "The given operation failed due to the LSP's required opening fee being too high.") }, + Self::PayjoinUnavailable => { + write!(f, "Failed to access Payjoin sender object. Make sure you have enabled Payjoin sending support.") + }, + Self::PayjoinRequestMissingAmount => { + write!(f, "Amount is neither user-provided nor defined in the URI.") + }, + Self::PayjoinRequestCreationFailed => { + write!(f, "Failed construct a Payjoin request") + }, + Self::PayjoinUriInvalid => { + write!(f, "The provided Payjoin URI is invalid") + }, + Self::PayjoinResponseProcessingFailed => { + write!(f, "Payjoin receiver responded to our request with an invalid response that was ignored") + }, } } } diff --git a/src/event.rs b/src/event.rs index 838df4230..41cba200a 100644 --- a/src/event.rs +++ b/src/event.rs @@ -143,6 +143,21 @@ pub enum Event { /// This will be `None` for events serialized by LDK Node v0.2.1 and prior. reason: Option, }, + /// 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. + PayjoinTxSendSuccess { + /// Transaction ID of the successfully sent Payjoin transaction. + txid: bitcoin::Txid, + }, + /// Failed to send Payjoin transaction. + /// + /// This event is emitted when our attempt to send Payjoin transaction fail. + PayjoinTxSendFailed { + /// Reason for the failure. + reason: String, + }, } impl_writeable_tlv_based_enum!(Event, @@ -184,6 +199,12 @@ impl_writeable_tlv_based_enum!(Event, (2, payment_id, required), (4, claimable_amount_msat, required), (6, claim_deadline, option), + }, + (7, PayjoinTxSendSuccess) => { + (0, txid, required), + }, + (8, PayjoinTxSendFailed) => { + (0, reason, required), }; ); diff --git a/src/io/utils.rs b/src/io/utils.rs index 77cc56f55..d298318f5 100644 --- a/src/io/utils.rs +++ b/src/io/utils.rs @@ -511,6 +511,15 @@ pub(crate) fn check_namespace_key_validity( Ok(()) } +pub(crate) fn ohttp_headers() -> reqwest::header::HeaderMap { + let mut headers = reqwest::header::HeaderMap::new(); + headers.insert( + reqwest::header::CONTENT_TYPE, + reqwest::header::HeaderValue::from_static("message/ohttp-req"), + ); + headers +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/lib.rs b/src/lib.rs index de2a0badf..fa71fa39c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -89,6 +89,7 @@ pub mod io; mod liquidity; mod logger; mod message_handler; +mod payjoin_sender; pub mod payment; mod peer_store; mod sweep; @@ -133,11 +134,14 @@ 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, - Graph, KeysManager, PeerManager, Router, Scorer, Sweeper, Wallet, + Graph, KeysManager, PayjoinSender, PeerManager, Router, Scorer, Sweeper, Wallet, }; pub use types::{ChannelDetails, PeerDetails, UserChannelId}; @@ -185,6 +189,7 @@ pub struct Node { output_sweeper: Arc, peer_manager: Arc, connection_manager: Arc>>, + payjoin_sender: Option>, keys_manager: Arc, network_graph: Arc, gossip_source: Arc, @@ -1063,6 +1068,40 @@ 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_sender = self.payjoin_sender.as_ref(); + PayjoinPayment::new( + Arc::clone(&self.runtime), + payjoin_sender.map(Arc::clone), + Arc::clone(&self.config), + Arc::clone(&self.event_queue), + ) + } + + /// 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_sender = self.payjoin_sender.as_ref(); + Arc::new(PayjoinPayment::new( + Arc::clone(&self.runtime), + payjoin_sender.map(Arc::clone), + Arc::clone(&self.config), + Arc::clone(&self.event_queue), + )) + } + /// 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/payjoin_sender.rs b/src/payjoin_sender.rs new file mode 100644 index 000000000..5eea7510c --- /dev/null +++ b/src/payjoin_sender.rs @@ -0,0 +1,173 @@ +use crate::config::{ + PAYJOIN_REQUEST_TIMEOUT, PAYJOIN_RETRY_INTERVAL, +}; +use crate::error::Error; +use crate::io::utils::ohttp_headers; +use crate::logger::FilesystemLogger; +use crate::types::Wallet; + +use lightning::chain::chaininterface::BroadcasterInterface; +use lightning::util::logger::Logger; +use lightning::{log_error, log_info}; + +use bitcoin::{psbt::Psbt, Txid}; + +use std::ops::Deref; +use std::sync::Arc; + +pub(crate) struct PayjoinSender +where + B::Target: BroadcasterInterface, +{ + logger: Arc, + wallet: Arc, + payjoin_relay: payjoin::Url, + broadcaster: B, +} + +impl PayjoinSender +where + B::Target: BroadcasterInterface, +{ + pub(crate) fn new( + logger: Arc, wallet: Arc, broadcaster: B, payjoin_relay: payjoin::Url, + ) -> Self { + Self { logger, wallet, broadcaster, payjoin_relay } + } + + pub(crate) fn create_payjoin_request( + &self, payjoin_uri: payjoin::Uri<'static, bitcoin::address::NetworkChecked>, + amount: Option, + ) -> Result<(Psbt, payjoin::Request, payjoin::send::ContextV2), Error> { + let amount_to_send = match (amount, payjoin_uri.amount) { + (Some(amount), _) => amount, + (None, Some(amount)) => amount, + (None, None) => return Err(Error::PayjoinRequestMissingAmount), + }; + let original_psbt = self.wallet.build_payjoin_transaction( + payjoin_uri.address.script_pubkey(), + amount_to_send.to_sat(), + )?; + let (request_data, request_context) = + payjoin::send::RequestBuilder::from_psbt_and_uri(original_psbt.clone(), payjoin_uri) + .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 extract payjoin request: {}", e); + Error::PayjoinRequestCreationFailed + })?; + Ok((original_psbt, request_data, request_context)) + } + + pub(crate) async fn fetch(&self, request: &payjoin::Request) -> Option> { + let response = match reqwest::Client::new() + .post(request.url.clone()) + .body(request.body.clone()) + .timeout(PAYJOIN_REQUEST_TIMEOUT) + .headers(ohttp_headers()) + .send() + .await + { + Ok(response) => response, + Err(e) => { + log_error!( + self.logger, + "Error trying to poll Payjoin response: {}, retrying in {} seconds", + e, + PAYJOIN_RETRY_INTERVAL.as_secs() + ); + return None; + }, + }; + if response.status() == reqwest::StatusCode::OK { + match response.bytes().await.and_then(|r| Ok(r.to_vec())) { + Ok(response) => { + if response.is_empty() { + log_info!( + self.logger, + "Got empty response while polling Payjoin response, retrying in {} seconds", PAYJOIN_RETRY_INTERVAL.as_secs() + ); + return None; + } + return Some(response); + }, + Err(e) => { + log_error!( + self.logger, + "Error reading polling Payjoin response: {}, retrying in {} seconds", + e, + PAYJOIN_RETRY_INTERVAL.as_secs() + ); + return None; + }, + }; + } else { + log_info!( + self.logger, + "Got status code {} while polling Payjoin response, retrying in {} seconds", + response.status(), + PAYJOIN_RETRY_INTERVAL.as_secs() + ); + return None; + } + } + + pub(crate) fn process_payjoin_response( + &self, context: payjoin::send::ContextV2, response: Vec, original_psbt: &mut Psbt, + ) -> Result { + let psbt = context.process_response(&mut response.as_slice()); + match psbt { + Ok(Some(psbt)) => { + let txid = self.finalise_payjoin_transaction(psbt, original_psbt)?; + return Ok(txid); + }, + Ok(None) => return Err(Error::PayjoinResponseProcessingFailed), + Err(e) => { + log_error!(self.logger, "Failed to process Payjoin response: {}", e); + return Err(Error::PayjoinResponseProcessingFailed); + }, + } + } + + fn finalise_payjoin_transaction( + &self, mut payjoin_proposal_psbt: 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(); + } + } + } + + match self.wallet.sign_transaction(&mut payjoin_proposal_psbt) { + Ok(true) => { + let tx = payjoin_proposal_psbt.extract_tx(); + self.broadcaster.broadcast_transactions(&[&tx]); + Ok(tx.txid()) + }, + Ok(false) => { + log_error!(self.logger, "Unable to finalise Payjoin transaction: signing failed"); + Err(Error::PayjoinResponseProcessingFailed) + }, + Err(e) => { + log_error!(self.logger, "Failed to sign Payjoin proposal: {}", e); + Err(Error::PayjoinResponseProcessingFailed) + }, + } + } +} diff --git a/src/payment/mod.rs b/src/payment/mod.rs index 1862bf2df..11f58bb77 100644 --- a/src/payment/mod.rs +++ b/src/payment/mod.rs @@ -3,9 +3,11 @@ mod bolt11; mod bolt12; mod onchain; +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.rs b/src/payment/payjoin.rs new file mode 100644 index 000000000..5b13b20b4 --- /dev/null +++ b/src/payment/payjoin.rs @@ -0,0 +1,184 @@ +//! Holds a payment handler allowing to send Payjoin payments. + +use crate::config::{PAYJOIN_REQUEST_TOTAL_DURATION, PAYJOIN_RETRY_INTERVAL}; +use crate::types::{EventQueue, PayjoinSender}; +use crate::Event; +use crate::{error::Error, Config}; + +use std::sync::{Arc, RwLock}; + +/// 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>>, + sender: Option>, + config: Arc, + event_queue: Arc, +} + +impl PayjoinPayment { + pub(crate) fn new( + runtime: Arc>>, sender: Option>, + config: Arc, event_queue: Arc, + ) -> Self { + Self { runtime, sender, config, event_queue } + } + + /// Send a Payjoin transaction to the address specified in the `payjoin_uri`. + /// + /// The `payjoin_uri` argument is expected to be a valid [`BIP21`] URI with Payjoin parameters + /// set. + /// + /// Due to the asynchronous nature of the Payjoin process, this method will return immediately + /// after constucting the Payjoin request and sending it in the background. The result of the + /// operation will be communicated through the event queue. If the Payjoin request is + /// successful, [`Event::PayjoinTxSendSuccess`] event will be added to the event queue. + /// Otherwise, [`Event::PayjoinTxSendFailed`] is added. + /// + /// The total duration of the Payjoin process is defined in `PAYJOIN_REQUEST_TOTAL_DURATION`. + /// If the Payjoin receiver does not respond within this duration, the process is considered + /// failed. Note, the Payjoin receiver can still broadcast the original PSBT shared with them as + /// part of our request in a regular transaction if we timed out, or for any other reason. The + /// Payjoin sender should monitor the blockchain for such transactions and handle them + /// accordingly. + /// + /// [`BIP21`]: https://github.com/bitcoin/bips/blob/master/bip-0021.mediawiki + /// [`BIP77`]: https://github.com/bitcoin/bips/blob/d7ffad81e605e958dcf7c2ae1f4c797a8631f146/bip-0077.mediawiki + /// [`Event::PayjoinTxSendSuccess`]: crate::Event::PayjoinTxSendSuccess + /// [`Event::PayjoinTxSendFailed`]: crate::Event::PayjoinTxSendFailed + pub fn send(&self, payjoin_uri: String) -> Result<(), Error> { + let rt_lock = self.runtime.read().unwrap(); + if rt_lock.is_none() { + return Err(Error::NotRunning); + } + let sender = match &self.sender { + Some(sender) => sender, + None => return Err(Error::PayjoinUnavailable), + }; + let payjoin_uri = match payjoin::Uri::try_from(payjoin_uri) { + Ok(uri) => uri, + Err(_) => return Err(Error::PayjoinUriInvalid), + }; + let payjoin_uri = match payjoin_uri.require_network(self.config.network) { + Ok(uri) => uri, + Err(_) => return Err(Error::InvalidNetwork), + }; + let sender = Arc::clone(sender); + let (mut original_psbt, request, context) = + sender.create_payjoin_request(payjoin_uri, None)?; + let runtime = rt_lock.as_ref().unwrap(); + let event_queue = Arc::clone(&self.event_queue); + runtime.spawn(async move { + let mut interval = tokio::time::interval(PAYJOIN_RETRY_INTERVAL); + loop { + tokio::select! { + _ = tokio::time::sleep(PAYJOIN_REQUEST_TOTAL_DURATION) => { + let _ = event_queue.add_event(Event::PayjoinTxSendFailed { + reason: "Payjoin request timed out.".to_string(), + }); + break; + } + _ = interval.tick() => { + match sender.fetch(&request).await { + Some(response) => { + match sender.process_payjoin_response(context, response, &mut original_psbt) { + Ok(txid) => { + let _ = event_queue.add_event(Event::PayjoinTxSendSuccess { txid }); + break; + }, + Err(e) => { + let _ = event_queue + .add_event(Event::PayjoinTxSendFailed { reason: e.to_string() }); + break; + }, + } + }, + None => { + continue; + }, + }; + } + } + } + }); + return Ok(()); + } + + /// Send a Payjoin transaction to the address specified in the `payjoin_uri`. + /// + /// The `payjoin_uri` argument is expected to be a valid [`BIP21`] URI with Payjoin parameters + /// set. + /// + /// This method will ignore the amount specified in the `payjoin_uri` and use the `amount_sats` + /// instead. The `amount_sats` argument is expected to be in satoshis. + /// + /// Due to the asynchronous nature of the Payjoin process, this method will return immediately + /// after constucting the Payjoin request and sending it in the background. The result of the + /// operation will be communicated through the event queue. If the Payjoin request is + /// successful, [`Event::PayjoinTxSendSuccess`] event will be added to the event queue. + /// Otherwise, [`Event::PayjoinTxSendFailed`] is added. + /// + /// The total duration of the Payjoin process is defined in `PAYJOIN_REQUEST_TOTAL_DURATION`. + /// If the Payjoin receiver does not respond within this duration, the process is considered + /// failed. Note, the Payjoin receiver can still broadcast the original PSBT shared with them as + /// part of our request in a regular transaction if we timed out, or for any other reason. The + /// Payjoin sender should monitor the blockchain for such transactions and handle them + /// accordingly. + /// + /// [`BIP21`]: https://github.com/bitcoin/bips/blob/master/bip-0021.mediawiki + /// [`BIP77`]: https://github.com/bitcoin/bips/blob/d7ffad81e605e958dcf7c2ae1f4c797a8631f146/bip-0077.mediawiki + /// [`Event::PayjoinTxSendSuccess`]: crate::Event::PayjoinTxSendSuccess + /// [`Event::PayjoinTxSendFailed`]: crate::Event::PayjoinTxSendFailed + pub fn send_with_amount(&self, payjoin_uri: String, amount_sats: u64) -> Result<(), Error> { + let 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/types.rs b/src/types.rs index 0c2faeb78..46ff5da86 100644 --- a/src/types.rs +++ b/src/types.rs @@ -72,6 +72,9 @@ pub(crate) type Wallet = crate::wallet::Wallet< Arc, >; +pub(crate) type PayjoinSender = crate::payjoin_sender::PayjoinSender>; +pub(crate) type EventQueue = crate::event::EventQueue>; + pub(crate) type KeysManager = crate::wallet::WalletKeysManager< bdk::database::SqliteDatabase, Arc, diff --git a/src/wallet.rs b/src/wallet.rs index 0da3f6db8..021714e06 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}; @@ -149,6 +150,41 @@ where res } + pub(crate) fn build_payjoin_transaction( + &self, output_script: ScriptBuf, value_sats: u64, + ) -> Result { + let locked_wallet = self.inner.lock().unwrap(); + let network = locked_wallet.network(); + 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, value_sats).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_transaction(&self, psbt: &mut Psbt) -> Result { + let wallet = self.inner.lock().unwrap(); + let is_signed = wallet.sign(psbt, SignOptions::default())?; + Ok(is_signed) + } + pub(crate) fn create_funding_transaction( &self, output_script: ScriptBuf, value_sats: u64, confirmation_target: ConfirmationTarget, locktime: LockTime,