diff --git a/Cargo.toml b/Cargo.toml index f22092cb5..7654272e4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -75,6 +75,7 @@ libc = "0.2" uniffi = { version = "0.27.3", features = ["build"], optional = true } serde = { version = "1.0.210", default-features = false, features = ["std", "derive"] } serde_json = { version = "1.0.128", default-features = false, features = ["std"] } +payjoin = { version = "0.21.0", default-features = false, features = ["send", "v2"] } vss-client = "0.3" prost = { version = "0.11.6", default-features = false} @@ -89,6 +90,8 @@ electrum-client = { version = "0.21.0", default-features = true } bitcoincore-rpc = { version = "0.19.0", default-features = false } proptest = "1.0.0" regex = "1.5.6" +payjoin = { version = "0.21.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.29.0", features = ["legacy", "esplora_a33e97e1", "bitcoind_25_0"] } diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index 954a6403c..0e41e455e 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -78,6 +78,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] @@ -171,6 +172,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", @@ -222,6 +230,12 @@ enum NodeError { "InsufficientFunds", "LiquiditySourceUnavailable", "LiquidityFeeTooHigh", + "PayjoinUnavailable", + "PayjoinUriInvalid", + "PayjoinRequestMissingAmount", + "PayjoinRequestCreationFailed", + "PayjoinRequestSendingFailed", + "PayjoinResponseProcessingFailed", }; dictionary NodeStatus { @@ -249,6 +263,7 @@ enum BuildError { "InvalidChannelMonitor", "InvalidListeningAddresses", "InvalidNodeAlias", + "InvalidPayjoinConfig", "ReadFailed", "WriteFailed", "StoragePathAccessFailed", @@ -281,6 +296,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); + PayjoinPaymentAwaitingConfirmation(Txid txid, u64 amount_sats); + PayjoinPaymentSuccessful(Txid txid, u64 amount_sats, boolean is_original_psbt_modified); + PayjoinPaymentFailed(Txid txid, u64 amount_sats, PayjoinPaymentFailureReason reason); }; enum PaymentFailureReason { @@ -295,6 +313,12 @@ enum PaymentFailureReason { "InvoiceRequestRejected", }; +enum PayjoinPaymentFailureReason { + "Timeout", + "RequestSendingFailed", + "ResponseProcessingFailed", +}; + [Enum] interface ClosureReason { CounterpartyForceClosed(UntrustedString peer_msg); @@ -321,6 +345,7 @@ interface PaymentKind { Bolt12Offer(PaymentHash? hash, PaymentPreimage? preimage, PaymentSecret? secret, OfferId offer_id, UntrustedString? payer_note, u64? quantity); Bolt12Refund(PaymentHash? hash, PaymentPreimage? preimage, PaymentSecret? secret, UntrustedString? payer_note, u64? quantity); Spontaneous(PaymentHash hash, PaymentPreimage? preimage); + Payjoin(); }; [Enum] @@ -353,6 +378,8 @@ dictionary PaymentDetails { PaymentDirection direction; PaymentStatus status; u64 latest_update_timestamp; + Txid? txid; + BestBlock? best_block; }; dictionary SendingParameters { diff --git a/src/builder.rs b/src/builder.rs index fac2ae0c5..2185837d2 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -28,6 +28,7 @@ use crate::types::{ use crate::wallet::persist::KVStoreWalletPersister; use crate::wallet::Wallet; use crate::{io, NodeMetrics}; +use crate::PayjoinHandler; use crate::{LogLevel, Node}; use lightning::chain::{chainmonitor, BestBlock, Watch}; @@ -100,6 +101,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 } @@ -141,6 +147,8 @@ pub enum BuildError { WalletSetupFailed, /// We failed to setup the logger. LoggerSetupFailed, + /// Invalid Payjoin configuration. + InvalidPayjoinConfig, } impl fmt::Display for BuildError { @@ -162,6 +170,10 @@ impl fmt::Display for BuildError { Self::WalletSetupFailed => write!(f, "Failed to setup onchain wallet."), Self::LoggerSetupFailed => write!(f, "Failed to setup the logger."), Self::InvalidNodeAlias => write!(f, "Given node alias is invalid."), + Self::InvalidPayjoinConfig => write!( + f, + "Invalid Payjoin configuration. Make sure the provided arguments are valid URLs." + ), } } } @@ -182,6 +194,7 @@ pub struct NodeBuilder { chain_data_source_config: Option, gossip_source_config: Option, liquidity_source_config: Option, + payjoin_config: Option, } impl NodeBuilder { @@ -197,12 +210,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, } } @@ -273,6 +288,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. @@ -480,6 +503,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, Arc::new(vss_store), @@ -501,6 +525,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, @@ -586,6 +611,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) { @@ -733,8 +763,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 status fields. let is_listening = Arc::new(AtomicBool::new(false)); @@ -1201,6 +1232,16 @@ 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(&event_queue), + Arc::clone(&logger), + pj_config.payjoin_relay.clone(), + Arc::clone(&payment_store), + Arc::clone(&wallet), + )) + }); + Ok(Node { runtime, stop_sender, @@ -1213,6 +1254,7 @@ fn build_with_store_internal( channel_manager, chain_monitor, output_sweeper, + payjoin_handler, peer_manager, onion_messenger, connection_manager, diff --git a/src/config.rs b/src/config.rs index 00b147e21..b8366c0af 100644 --- a/src/config.rs +++ b/src/config.rs @@ -47,6 +47,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 d1fd848b1..1ed9d1e00 100644 --- a/src/error.rs +++ b/src/error.rs @@ -114,6 +114,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 { @@ -184,6 +196,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 d760d3b58..9a2718150 100644 --- a/src/event.rs +++ b/src/event.rs @@ -206,6 +206,66 @@ 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 have successfully negotiated a Payjoin transaction with the + /// receiver and are waiting for the transaction to be confirmed onchain. + PayjoinPaymentAwaitingConfirmation { + /// 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, + }, + /// This event is emitted when a Payjoin transaction has been successfully confirmed onchain. + /// + /// This event is emitted only after one onchain confirmation. To determine the current number + /// of confirmations, refer to [`PaymentStore::best_block`]:. + /// + /// [`PaymentStore::best_block`]: crate::payment::store::PaymentStore::best_block + PayjoinPaymentSuccessful { + /// This can refer to the original PSBT or to the finalised Payjoin transaction. + /// + /// If [`is_original_psbt_modified`] field is `true`, this refers to the finalised Payjoin + /// transaction. Otherwise, it refers to the original PSBT. + /// + /// In case of this being the original PSBT, the transaction will be a regular transaction + /// and not a Payjoin transaction but will be considered successful as the receiver decided + /// to broadcast the original PSBT or to respond with a Payjoin proposal that was identical + /// to the original PSBT, and they have successfully received the funds. + 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, + /// Indicates whether the Payjoin negotiation was successful or the receiver decided to + /// broadcast the original PSBT. + is_original_psbt_modified: bool, + }, + /// Failed to send a Payjoin transaction. + /// + /// Payjoin payment can fail in different stages due to various reasons, such as network + /// issues, insufficient funds, irresponsive receiver, etc. + 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, + /// Failure reason. + reason: PayjoinPaymentFailureReason, + }, } impl_writeable_tlv_based_enum!(Event, @@ -258,9 +318,52 @@ impl_writeable_tlv_based_enum!(Event, (10, skimmed_fee_msat, option), (12, claim_from_onchain_tx, required), (14, outbound_amount_forwarded_msat, option), + }, + (8, PayjoinPaymentAwaitingConfirmation) => { + (0, txid, required), + (2, amount_sats, required), + }, + (9, PayjoinPaymentSuccessful) => { + (0, txid, required), + (2, amount_sats, required), + (4, is_original_psbt_modified, required), + }, + (10, PayjoinPaymentFailed) => { + (0, amount_sats, required), + (2, txid, required), + (4, reason, required), } ); +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum PayjoinPaymentFailureReason { + /// The request failed in the sending process, i.e., either no funds were available to send or + /// the provided Payjoin URI is invalid or network problem encountered while communicating with + /// the Payjoin relay/directory. The exact reason can be determined by inspecting the logs. + RequestSendingFailed, + /// The received response was invalid, i.e., the receiver responded with an invalid Payjoin + /// proposal that does not adhere to the [`BIP78`] specification. + /// + /// This is considered a failure but the receiver can still broadcast the original PSBT, in + /// which case a `PayjoinPaymentSuccessful` event will be emitted with + /// `is_original_psbt_modified` set to `false` and the `txid` of the original PSBT. + /// + /// [`BIP78`]: https://github.com/bitcoin/bips/blob/master/bip-0078.mediawiki + ResponseProcessingFailed, + /// 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 `PayjoinPaymentSuccessful` event will be emitted with + /// `is_original_psbt_modified` set to `false` and the `txid` of the original PSBT. + Timeout, +} + +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 1e30c61c0..129a78204 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -106,7 +106,10 @@ pub use balance::{BalanceDetails, LightningBalance, PendingSweepBalance}; 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 io::utils::generate_entropy_mnemonic; @@ -132,8 +135,8 @@ use io::utils::write_node_metrics; 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::{ @@ -187,6 +190,7 @@ pub struct Node { peer_manager: Arc, onion_messenger: Arc, connection_manager: Arc>>, + payjoin_handler: Option>, keys_manager: Arc, network_graph: Arc, gossip_source: Arc, @@ -960,6 +964,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() @@ -1218,6 +1258,7 @@ impl Node { let sync_cman = Arc::clone(&self.channel_manager); let sync_cmon = Arc::clone(&self.chain_monitor); let sync_sweeper = Arc::clone(&self.output_sweeper); + tokio::task::block_in_place(move || { tokio::runtime::Builder::new_multi_thread().enable_all().build().unwrap().block_on( async move { diff --git a/src/payment/mod.rs b/src/payment/mod.rs index 5c99cfcf8..94f80346e 100644 --- a/src/payment/mod.rs +++ b/src/payment/mod.rs @@ -10,10 +10,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..ccb2cdd80 --- /dev/null +++ b/src/payment/payjoin/handler.rs @@ -0,0 +1,274 @@ +use bitcoin::block::Header; +use bitcoin::psbt::Psbt; +use bitcoin::{BlockHash, Transaction, Txid}; +use lightning::chain::transaction::TransactionData; + +use crate::chain::ChainSource; +use crate::config::PAYJOIN_REQUEST_TIMEOUT; +use crate::error::Error; +use crate::event::{EventQueue, PayjoinPaymentFailureReason}; +use crate::logger::FilesystemLogger; +use crate::payment::store::{PaymentDetailsUpdate, PaymentStore}; +use crate::payment::PaymentKind; +use crate::payment::{PaymentDirection, PaymentStatus}; +use crate::types::Wallet; +use crate::Event; +use crate::PaymentDetails; + +// use lightning::chain::{BestBlock, Confirm, Filter}; +use lightning::chain::Confirm; +use lightning::ln::channelmanager::PaymentId; +use lightning::log_error; +use lightning::util::logger::Logger; + +use std::sync::Arc; + +pub(crate) struct PayjoinHandler { + #[allow(dead_code)] + chain_source: Option>, + event_queue: Arc>>, + logger: Arc, + payjoin_relay: payjoin::Url, + payment_store: Arc>>, + wallet: Arc, +} + +impl PayjoinHandler { + pub(crate) fn new( + event_queue: Arc>>, + logger: Arc, payjoin_relay: payjoin::Url, + payment_store: Arc>>, + wallet: Arc, + ) -> Self { + Self { chain_source: None, + event_queue, + logger, payjoin_relay, + payment_store, + wallet } + } + + pub(crate) fn start_request( + &self, payjoin_uri: payjoin::PjUri<'_>, + ) -> 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())?; + let tx = original_psbt.clone().unsigned_tx; + let payment_id = self.payment_id(&tx.compute_txid()); + self.payment_store.insert(PaymentDetails::new( + payment_id, + PaymentKind::Payjoin, + Some(amount.to_sat()), + PaymentDirection::Outbound, + PaymentStatus::Pending, + ))?; + // self.chain_source.register_tx(&tx.txid(), Script::empty()); + Ok(original_psbt) + } + + pub(crate) async fn send_request( + &self, payjoin_uri: payjoin::PjUri<'_>, original_psbt: &mut Psbt, + ) -> Result, Error> { + let (request, context) = payjoin::send::SenderBuilder::from_psbt_and_uri( + original_psbt.clone(), + payjoin_uri.clone(), + ) + .and_then(|b| b.build_non_incentivizing(bitcoin::FeeRate::MIN)) + .and_then(|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(); + let _ret = context.process_response(&mut response.as_slice()).map_err(|e| { + log_error!(self.logger, "Failed to process Payjoin response: {}", e); + Error::PayjoinResponseProcessingFailed + }).unwrap(); + Ok(None) + // ret.process_response + } + + 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 payment_store = self.payment_store.clone(); + let payment_id = self.payment_id(&original_psbt.unsigned_tx.compute_txid()); + let payment_details = payment_store.get(&payment_id); + // let payment_details: Option = None; + if let Some(payment_details) = payment_details { + let proposal_tx = proposal_tx.unwrap(); + let txid = proposal_tx.clone().compute_txid(); + // let mut payment_update = PaymentDetailsUpdate::new(payment_id); + // payment_update.txid = Some(txid); + // payment_store.update(&payment_update)?; + // self.chain_source.register_tx(&txid, Script::empty()); + self.event_queue.add_event(Event::PayjoinPaymentAwaitingConfirmation { + txid, + amount_sats: payment_details + .amount_msat + .ok_or(Error::PayjoinRequestMissingAmount)?, + })?; + Ok(proposal_tx) + } else { + log_error!(self.logger, "Failed to process Payjoin response: transaction not found"); + Err(Error::PayjoinResponseProcessingFailed) + } + } + + fn payment_id(&self, original_psbt_txid: &Txid) -> PaymentId { + let payment_id: [u8; 32] = + original_psbt_txid[..].try_into().expect("Unreachable, Txid is 32 bytes"); + PaymentId(payment_id) + } + + pub(crate) fn handle_request_failure( + &self, original_psbt: &Psbt, reason: PayjoinPaymentFailureReason, + ) -> Result<(), Error> { + let payment_store = self.payment_store.clone(); + let payment_id = &self.payment_id(&original_psbt.unsigned_tx.compute_txid()); + let payment_details = payment_store.get(payment_id); + if let Some(payment_details) = payment_details { + let mut update_details = PaymentDetailsUpdate::new(payment_id.clone()); + update_details.status = Some(PaymentStatus::Failed); + let _ = payment_store.update(&update_details); + self.event_queue.add_event(Event::PayjoinPaymentFailed { + txid: original_psbt.unsigned_tx.compute_txid(), + amount_sats: payment_details + .amount_msat + .ok_or(Error::PayjoinRequestMissingAmount)?, + 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, + ) { + // for (_, tx) in txdata { + // let confirmed_tx_txid = tx.compute_txid(); + // let payment_store = self.payment_store.clone(); + // let payment_id = self.payment_id(&confirmed_tx_txid); + // // let payjoin_tx_filter = |payment_details: &&PaymentDetails| { + // payment_details.txid == Some(confirmed_tx_txid) + // && payment_details.amount_msat.is_some() + // }; + // let payjoin_tx_details = payment_store.list_filter(payjoin_tx_filter); + // if let Some(payjoin_tx_details) = payjoin_tx_details.get(0) { + // let mut payment_update = PaymentDetailsUpdate::new(payjoin_tx_details.id); + // payment_update.status = Some(PaymentStatus::Succeeded); + // payment_update.best_block = Some(BestBlock::new(header.block_hash(), height)); + // let _ = payment_store.update(&payment_update); + // let _ = self.event_queue.add_event(Event::PayjoinPaymentSuccessful { + // txid: confirmed_tx_txid, + // amount_sats: payjoin_tx_details + // .amount_msat + // .expect("Unreachable, asserted in `payjoin_tx_filter`"), + // is_original_psbt_modified: if payment_id == payjoin_tx_details.id { + // false + // } else { + // true + // }, + // }); + // check if this is the original psbt transaction + // } else if let Some(payment_details) = payment_store.get(&payment_id) { + // let mut payment_update = PaymentDetailsUpdate::new(payment_id); + // payment_update.status = Some(PaymentStatus::Succeeded); + // let _ = payment_store.update(&payment_update); + // payment_update.best_block = Some(BestBlock::new(header.block_hash(), height)); + // payment_update.txid = Some(confirmed_tx_txid); + // let _ = self.event_queue.add_event(Event::PayjoinPaymentSuccessful { + // txid: confirmed_tx_txid, + // amount_sats: payment_details + // .amount_msat + // .expect("Unreachable, payjoin transactions must have amount"), + // is_original_psbt_modified: false, + // }); + // } + // } + } + + fn internal_get_relevant_txids(&self) -> Vec<(Txid, u32, Option)> { + let payjoin_tx_filter = |payment_details: &&PaymentDetails| { + // payment_details.txid.is_some() + // && + payment_details.status == PaymentStatus::Succeeded + && payment_details.kind == PaymentKind::Payjoin + }; + let _payjoin_tx_details = self.payment_store.list_filter(payjoin_tx_filter); + let ret = Vec::new(); + // for payjoin_tx_details in payjoin_tx_details { + // if let (Some(txid), Some(best_block)) = + // (payjoin_tx_details.txid, payjoin_tx_details.best_block) + // { + // ret.push((txid, best_block.height, Some(best_block.block_hash))); + // } + // } + ret + } + + fn internal_best_block_updated(&self, _height: u32, _block_hash: BlockHash) { + let payment_store = self.payment_store.clone(); + let payjoin_tx_filter = |payment_details: &&PaymentDetails| { + payment_details.kind == PaymentKind::Payjoin + && payment_details.status == PaymentStatus::Succeeded + }; + let _payjoin_tx_details = payment_store.list_filter(payjoin_tx_filter); + // for payjoin_tx_details in payjoin_tx_details { + // let mut payment_update = PaymentDetailsUpdate::new(payjoin_tx_details.id); + // payment_update.best_block = Some(BestBlock::new(block_hash, height)); + // let _ = payment_store.update(&payment_update); + // } + } +} + +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) {} +} diff --git a/src/payment/payjoin/mod.rs b/src/payment/payjoin/mod.rs new file mode 100644 index 000000000..98d04a2a0 --- /dev/null +++ b/src/payment/payjoin/mod.rs @@ -0,0 +1,161 @@ +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 [`BIP77`] implementation. Compatible with previous Payjoin version [`BIP78`]. +/// +/// Should be retrieved by calling [`Node::payjoin_payment`]. +/// +/// 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. +/// +/// In a Payjoin transaction, 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 an optional amount parameter. 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 regularly check for response +/// 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 further information on Payjoin, please 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. The sending part is performed 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 emitted. + /// Otherwise, [`Event::PayjoinPaymentFailed`] is emitted. + /// + /// [`BIP21`]: https://github.com/bitcoin/bips/blob/master/bip-0021.mediawiki + /// [`Event::PayjoinPaymentSuccessful`]: crate::Event::PayjoinPaymentSuccessful + /// [`Event::PayjoinPaymentFailed`]: crate::Event::PayjoinPaymentFailed + pub fn send(&self, payjoin_uri: String) -> Result<(), Error> { + use payjoin::UriExt; + 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 uri = payjoin_uri.clone(); + let payjoin_uri = + payjoin::Uri::try_from(uri).unwrap().assume_checked().check_pj_supported().unwrap(); + 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. The sending part is performed 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 emitted. + /// Otherwise, [`Event::PayjoinPaymentFailed`] is emitted. + /// + /// [`BIP21`]: https://github.com/bitcoin/bips/blob/master/bip-0021.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 ee82544dc..2a5f35c26 100644 --- a/src/payment/store.rs +++ b/src/payment/store.rs @@ -260,6 +260,8 @@ pub enum PaymentKind { /// The pre-image used by the payment. preimage: Option, }, + /// A Payjoin payment. + Payjoin, } impl_writeable_tlv_based_enum!(PaymentKind, @@ -293,7 +295,8 @@ impl_writeable_tlv_based_enum!(PaymentKind, (2, preimage, option), (3, quantity, 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/wallet/mod.rs b/src/wallet/mod.rs index a4d4b066e..920af83f5 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -130,6 +130,59 @@ where Ok(()) } + pub(crate) fn build_payjoin_transaction( + &self, amount: Amount, recipient: ScriptBuf, + ) -> Result { + let fee_rate = self + .fee_estimator + .estimate_fee_rate(ConfirmationTarget::OnchainPayment); + let mut locked_wallet = self.inner.lock().unwrap(); + let mut tx_builder = locked_wallet.build_tx(); + tx_builder.add_recipient(recipient, 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 create_funding_transaction( &self, output_script: ScriptBuf, amount: Amount, confirmation_target: ConfirmationTarget, locktime: LockTime, diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 26aff3d11..18e8e5633 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -154,6 +154,41 @@ macro_rules! expect_payment_successful_event { pub(crate) use expect_payment_successful_event; +// macro_rules! expect_payjoin_tx_sent_successfully_event { +// ($node: expr, $is_original_psbt_modified: expr) => {{ +// match $node.wait_next_event() { +// ref e @ Event::PayjoinPaymentSuccessful { txid, is_original_psbt_modified, .. } => { +// println!("{} got event {:?}", $node.node_id(), e); +// assert_eq!(is_original_psbt_modified, $is_original_psbt_modified); +// $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_await_confirmation { +// ($node: expr) => {{ +// match $node.wait_next_event() { +// ref e @ Event::PayjoinPaymentAwaitingConfirmation { 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_await_confirmation; + 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( @@ -317,6 +352,20 @@ pub(crate) fn setup_node( 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..aadd56345 --- /dev/null +++ b/tests/integration_tests_payjoin.rs @@ -0,0 +1,299 @@ +// 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 bitcoincore_rpc::{Client as BitcoindClient, RawTx, RpcApi}; +// use ldk_node::{ +// payment::{PaymentDirection, PaymentKind, PaymentStatus}, +// Event, +// }; +// use payjoin::{ +// receive::v2::{Enrolled, Enroller}, +// OhttpKeys, PjUriBuilder, +// }; + +// use crate::common::{ +// expect_payjoin_await_confirmation, random_config, setup_node, setup_payjoin_node, +// }; + +// struct PayjoinReceiver { +// ohttp_keys: OhttpKeys, +// enrolled: Enrolled, +// } + +// enum ResponseType<'a> { +// ModifyOriginalPsbt(bitcoin::Address), +// BroadcastWithoutResponse(&'a BitcoindClient), +// } + +// 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, response_type: 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(); +// match response_type { +// Some(ResponseType::BroadcastWithoutResponse(bitcoind)) => { +// let tx = unchecked_proposal.extract_tx_to_schedule_broadcast(); +// let raw_tx = tx.raw_hex(); +// bitcoind.send_raw_transaction(raw_tx).unwrap(); +// return; +// }, +// _ => {}, +// } + +// 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(); + +// match response_type { +// Some(ResponseType::ModifyOriginalPsbt(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 +// } +// } + +// // Test sending payjoin transaction with changes to the original PSBT +// #[test] +// fn send_payjoin_transaction() { +// let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); +// let config_a = random_config(false); +// let config_b = random_config(false); +// let receiver = setup_node(&electrsd, config_a); +// let sender = setup_payjoin_node(&electrsd, config_b); +// let addr_a = 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), +// ); +// sender.sync_wallets().unwrap(); +// assert_eq!(sender.list_balances().spendable_onchain_balance_sats, premine_amount_sat); +// assert_eq!(sender.list_balances().spendable_onchain_balance_sats, 100_000_00); +// assert_eq!(sender.next_event(), None); + +// let payjoin_receiver_handler = PayjoinReceiver::enroll(); +// let payjoin_uri = payjoin_receiver_handler +// .receive(Amount::from_sat(80_000), receiver.onchain_payment().new_address().unwrap()); + +// assert!(sender.payjoin_payment().send(payjoin_uri).is_ok()); + +// let payments = 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(); +// // Receiver modifies the original PSBT +// payjoin_receiver_handler +// .process_payjoin_request(Some(ResponseType::ModifyOriginalPsbt(substitue_address))); + +// let txid = expect_payjoin_await_confirmation!(sender); + +// wait_for_tx(&electrsd.client, txid); +// generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 1); +// sender.sync_wallets().unwrap(); +// let payments = sender.list_payments(); +// let payment = payments.first().unwrap(); +// assert_eq!(payment.amount_msat, Some(80_000)); +// assert_eq!(payment.status, PaymentStatus::Succeeded); +// assert_eq!(payment.direction, PaymentDirection::Outbound); +// assert_eq!(payment.kind, PaymentKind::Payjoin); +// assert_eq!(payment.txid, Some(txid)); + +// expect_payjoin_tx_sent_successfully_event!(sender, true); +// } + +// // Test sending payjoin transaction with original PSBT used eventually +// #[test] +// fn send_payjoin_transaction_original_psbt_used() { +// let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); +// let config_a = random_config(false); +// let config_b = random_config(false); +// let receiver = setup_node(&electrsd, config_b); +// let sender = setup_payjoin_node(&electrsd, config_a); +// let addr_a = 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), +// ); +// sender.sync_wallets().unwrap(); +// assert_eq!(sender.list_balances().spendable_onchain_balance_sats, premine_amount_sat); +// assert_eq!(sender.list_balances().spendable_onchain_balance_sats, 100_000_00); +// assert_eq!(sender.next_event(), None); + +// let payjoin_receiver_handler = PayjoinReceiver::enroll(); +// let payjoin_uri = payjoin_receiver_handler +// .receive(Amount::from_sat(80_000), receiver.onchain_payment().new_address().unwrap()); + +// assert!(sender.payjoin_payment().send(payjoin_uri).is_ok()); + +// let payments = 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); + +// // Receiver does not modify the original PSBT +// payjoin_receiver_handler.process_payjoin_request(None); + +// let txid = expect_payjoin_await_confirmation!(sender); + +// wait_for_tx(&electrsd.client, txid); +// generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 1); +// sender.sync_wallets().unwrap(); + +// let _ = expect_payjoin_tx_sent_successfully_event!(sender, false); +// } + +// // Test sending payjoin transaction with receiver broadcasting and not responding to the payjoin +// // request +// #[test] +// fn send_payjoin_transaction_with_receiver_broadcasting() { +// let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); +// let config_a = random_config(false); +// let config_b = random_config(false); +// let receiver = setup_node(&electrsd, config_b); +// let sender = setup_payjoin_node(&electrsd, config_a); +// let addr_a = 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), +// ); +// sender.sync_wallets().unwrap(); +// assert_eq!(sender.list_balances().spendable_onchain_balance_sats, premine_amount_sat); +// assert_eq!(sender.list_balances().spendable_onchain_balance_sats, 100_000_00); +// assert_eq!(sender.next_event(), None); + +// let payjoin_receiver_handler = PayjoinReceiver::enroll(); +// let payjoin_uri = payjoin_receiver_handler +// .receive(Amount::from_sat(80_000), receiver.onchain_payment().new_address().unwrap()); + +// assert!(sender.payjoin_payment().send(payjoin_uri).is_ok()); + +// let payments = 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 txid = payment.txid.unwrap(); + +// // Receiver broadcasts the transaction without responding to the payjoin request +// payjoin_receiver_handler +// .process_payjoin_request(Some(ResponseType::BroadcastWithoutResponse(&bitcoind.client))); + +// wait_for_tx(&electrsd.client, txid); +// generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 1); +// sender.sync_wallets().unwrap(); +// let payments = sender.list_payments(); +// let payment = payments.first().unwrap(); +// assert_eq!(payment.amount_msat, Some(80_000)); +// assert_eq!(payment.status, PaymentStatus::Succeeded); +// assert_eq!(payment.direction, PaymentDirection::Outbound); +// assert_eq!(payment.kind, PaymentKind::Payjoin); +// assert_eq!(payment.txid, Some(txid)); + +// expect_payjoin_tx_sent_successfully_event!(sender, false); +// }