From 1775b2ec44bf1b30a94eac03a78e77afd73b5d87 Mon Sep 17 00:00:00 2001 From: jbesraa Date: Tue, 23 Jul 2024 15:07:11 +0300 Subject: [PATCH 1/6] Implement `PayjoinPayment` .. 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: - https://pj.bobspacebkk.com 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: - https://payjo.in --- Cargo.toml | 1 + src/config.rs | 9 + src/error.rs | 36 ++ src/event.rs | 106 ++++- src/payment/mod.rs | 2 + src/payment/payjoin/handler.rs | 168 ++++++++ src/payment/payjoin/mod.rs | 161 ++++++++ src/payment/store.rs | 5 +- src/wallet.rs | 734 +++++++++++++++++++++++++++++++++ 9 files changed, 1220 insertions(+), 2 deletions(-) create mode 100644 src/payment/payjoin/handler.rs create mode 100644 src/payment/payjoin/mod.rs create mode 100644 src/wallet.rs diff --git a/Cargo.toml b/Cargo.toml index f22092cb5..94ca4b176 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.16.0", default-features = false, features = ["send", "v2"] } vss-client = "0.3" prost = { version = "0.11.6", default-features = false} 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..ab3d56df9 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,7 +318,51 @@ 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) => {}; +>>>>>>> 1136aa4 (Implement `PayjoinPayment` ..) ); pub struct EventQueue 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..8523e7362 --- /dev/null +++ b/src/payment/payjoin/handler.rs @@ -0,0 +1,168 @@ +use bitcoin::address::NetworkChecked; +use bitcoin::psbt::Psbt; +use bitcoin::{Script, Transaction, Txid}; + +use crate::config::PAYJOIN_REQUEST_TIMEOUT; +use crate::error::Error; +use crate::event::PayjoinPaymentFailureReason; +use crate::logger::FilesystemLogger; +use crate::payment::store::PaymentDetailsUpdate; +use crate::payment::PaymentKind; +use crate::payment::{PaymentDirection, PaymentStatus}; +use crate::types::{ChainSource, EventQueue, PaymentStore, Wallet}; +use crate::Event; +use crate::PaymentDetails; + +use lightning::chain::Filter; +use lightning::ln::channelmanager::PaymentId; +use lightning::log_error; +use lightning::util::logger::Logger; + +use std::sync::Arc; + +pub(crate) struct PayjoinHandler { + chain_source: Arc, + event_queue: Arc, + logger: Arc, + payjoin_relay: payjoin::Url, + payment_store: Arc, + wallet: Arc, +} + +impl PayjoinHandler { + pub(crate) fn new( + chain_source: Arc, event_queue: Arc, + logger: Arc, payjoin_relay: payjoin::Url, + payment_store: Arc, wallet: Arc, + ) -> Self { + Self { chain_source, event_queue, logger, payjoin_relay, payment_store, wallet } + } + + pub(crate) fn start_request( + &self, payjoin_uri: payjoin::Uri<'_, NetworkChecked>, + ) -> Result { + let amount = payjoin_uri.amount.ok_or(Error::PayjoinRequestMissingAmount)?; + let receiver = payjoin_uri.address.clone(); + let original_psbt = + self.wallet.build_payjoin_transaction(amount, receiver.clone().into())?; + let tx = original_psbt.clone().unsigned_tx; + let payment_id = self.payment_id(&original_psbt.unsigned_tx.txid()); + self.payment_store.insert(PaymentDetails::new( + payment_id, + PaymentKind::Payjoin, + Some(amount.to_sat()), + PaymentDirection::Outbound, + PaymentStatus::Pending, + ))?; + let mut update_payment = PaymentDetailsUpdate::new(payment_id); + update_payment.txid = Some(tx.txid()); + let _ = self.payment_store.update(&update_payment); + self.chain_source.register_tx(&tx.txid(), Script::empty()); + Ok(original_psbt) + } + + pub(crate) async fn send_request( + &self, payjoin_uri: payjoin::Uri<'_, NetworkChecked>, original_psbt: &mut Psbt, + ) -> Result, Error> { + let (request, context) = payjoin::send::RequestBuilder::from_psbt_and_uri( + original_psbt.clone(), + payjoin_uri.clone(), + ) + .and_then(|b| b.build_non_incentivizing()) + .and_then(|mut c| c.extract_v2(self.payjoin_relay.clone())) + .map_err(|e| { + log_error!(self.logger, "Failed to create Payjoin request: {}", e); + Error::PayjoinRequestCreationFailed + })?; + let mut headers = reqwest::header::HeaderMap::new(); + headers.insert( + reqwest::header::CONTENT_TYPE, + reqwest::header::HeaderValue::from_static("message/ohttp-req"), + ); + let response = reqwest::Client::new() + .post(request.url.clone()) + .body(request.body.clone()) + .timeout(PAYJOIN_REQUEST_TIMEOUT) + .headers(headers) + .send() + .await + .and_then(|r| r.error_for_status()) + .map_err(|e| { + log_error!(self.logger, "Failed to send Payjoin request: {}", e); + Error::PayjoinRequestSendingFailed + })?; + let response = response.bytes().await.map_err(|e| { + log_error!( + self.logger, + "Failed to send Payjoin request, receiver invalid response: {}", + e + ); + Error::PayjoinRequestSendingFailed + })?; + let response = response.to_vec(); + context.process_response(&mut response.as_slice()).map_err(|e| { + log_error!(self.logger, "Failed to process Payjoin response: {}", e); + Error::PayjoinResponseProcessingFailed + }) + } + + pub(crate) fn process_response( + &self, payjoin_proposal: &mut Psbt, original_psbt: &mut Psbt, + ) -> Result { + let wallet = self.wallet.clone(); + wallet.sign_payjoin_proposal(payjoin_proposal, original_psbt)?; + let proposal_tx = payjoin_proposal.clone().extract_tx(); + let payment_store = self.payment_store.clone(); + let payment_id = self.payment_id(&original_psbt.unsigned_tx.txid()); + let payment_details = payment_store.get(&payment_id); + if let Some(payment_details) = payment_details { + let txid = proposal_tx.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.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.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) + } + } +} diff --git a/src/payment/payjoin/mod.rs b/src/payment/payjoin/mod.rs new file mode 100644 index 000000000..fafa587de --- /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> { + let rt_lock = self.runtime.read().unwrap(); + if rt_lock.is_none() { + return Err(Error::NotRunning); + } + let payjoin_handler = self.payjoin_handler.as_ref().ok_or(Error::PayjoinUnavailable)?; + let payjoin_uri = + payjoin::Uri::try_from(payjoin_uri).map_err(|_| Error::PayjoinUriInvalid).and_then( + |uri| uri.require_network(self.config.network).map_err(|_| Error::InvalidNetwork), + )?; + let original_psbt = payjoin_handler.start_request(payjoin_uri.clone())?; + let payjoin_handler = Arc::clone(payjoin_handler); + let runtime = rt_lock.as_ref().unwrap(); + let tx_broadcaster = Arc::clone(&self.tx_broadcaster); + let logger = Arc::clone(&self.logger); + runtime.spawn(async move { + let mut interval = tokio::time::interval(PAYJOIN_RETRY_INTERVAL); + loop { + tokio::select! { + _ = tokio::time::sleep(PAYJOIN_REQUEST_TOTAL_DURATION) => { + let _ = payjoin_handler.handle_request_failure(&original_psbt, PayjoinPaymentFailureReason::Timeout); + break; + } + _ = interval.tick() => { + let payjoin_uri = payjoin_uri.clone(); + match payjoin_handler.send_request(payjoin_uri.clone(), &mut original_psbt.clone()).await { + Ok(Some(mut proposal)) => { + match payjoin_handler.process_response(&mut proposal, &mut original_psbt.clone()) { + Ok(tx) => { + tx_broadcaster.broadcast_transactions(&[&tx]); + }, + Err(e) => { + log_error!(logger, "Failed to process Payjoin response: {}", e); + let _ = payjoin_handler.handle_request_failure(&original_psbt, PayjoinPaymentFailureReason::ResponseProcessingFailed); + }, + }; + break; + }, + Ok(None) => { + log_info!(logger, "Payjoin request sent, waiting for response..."); + continue; + } + Err(e) => { + log_error!(logger, "Failed to send Payjoin request : {}", e); + let _ = payjoin_handler.handle_request_failure(&original_psbt, PayjoinPaymentFailureReason::RequestSendingFailed); + break; + }, + } + } + } + } + }); + Ok(()) + } + + /// Send a Payjoin transaction to the address specified in the `payjoin_uri`. + /// + /// The `payjoin_uri` argument is expected to be a valid [`BIP21`] URI with Payjoin parameters. + /// + /// Due to the asynchronous nature of the Payjoin process, this method will return immediately + /// after constructing the Payjoin request. 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..c7bf8e0fd 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.rs b/src/wallet.rs new file mode 100644 index 000000000..80d56542d --- /dev/null +++ b/src/wallet.rs @@ -0,0 +1,734 @@ +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}; +use lightning::ln::msgs::{DecodeError, UnsignedGossipMessage}; +use lightning::ln::script::ShutdownScript; +use lightning::sign::{ + ChangeDestinationSource, EntropySource, InMemorySigner, KeyMaterial, KeysManager, NodeSigner, + OutputSpender, Recipient, SignerProvider, SpendableOutputDescriptor, +}; + +use lightning::util::message_signing; + +use bdk::blockchain::EsploraBlockchain; +use bdk::database::BatchDatabase; +use bdk::wallet::AddressIndex; +use bdk::{Balance, FeeRate}; +use bdk::{SignOptions, SyncOptions}; + +use bitcoin::address::{Payload, WitnessVersion}; +use bitcoin::bech32::u5; +use bitcoin::blockdata::constants::WITNESS_SCALE_FACTOR; +use bitcoin::blockdata::locktime::absolute::LockTime; +use bitcoin::hash_types::WPubkeyHash; +use bitcoin::hashes::Hash; +use bitcoin::key::XOnlyPublicKey; +use bitcoin::psbt::PartiallySignedTransaction; +use bitcoin::secp256k1::ecdh::SharedSecret; +use bitcoin::secp256k1::ecdsa::{RecoverableSignature, Signature}; +use bitcoin::secp256k1::{PublicKey, Scalar, Secp256k1, SecretKey, Signing}; +use bitcoin::{Amount, ScriptBuf, Transaction, TxOut, Txid}; + +use std::ops::{Deref, DerefMut}; +use std::sync::{Arc, Mutex, RwLock}; +use std::time::Duration; + +enum WalletSyncStatus { + Completed, + InProgress { subscribers: tokio::sync::broadcast::Sender> }, +} + +pub struct Wallet +where + D: BatchDatabase, + B::Target: BroadcasterInterface, + E::Target: FeeEstimator, + L::Target: Logger, +{ + // A BDK blockchain used for wallet sync. + blockchain: EsploraBlockchain, + // A BDK on-chain wallet. + inner: Mutex>, + // A cache storing the most recently retrieved fee rate estimations. + broadcaster: B, + fee_estimator: E, + // A Mutex holding the current sync status. + sync_status: Mutex, + // TODO: Drop this workaround after BDK 1.0 upgrade. + balance_cache: RwLock, + logger: L, +} + +impl Wallet +where + D: BatchDatabase, + B::Target: BroadcasterInterface, + E::Target: FeeEstimator, + L::Target: Logger, +{ + pub(crate) fn new( + blockchain: EsploraBlockchain, wallet: bdk::Wallet, broadcaster: B, fee_estimator: E, + logger: L, + ) -> Self { + let start_balance = wallet.get_balance().unwrap_or(Balance { + immature: 0, + trusted_pending: 0, + untrusted_pending: 0, + confirmed: 0, + }); + + let inner = Mutex::new(wallet); + let sync_status = Mutex::new(WalletSyncStatus::Completed); + let balance_cache = RwLock::new(start_balance); + Self { blockchain, inner, broadcaster, fee_estimator, sync_status, balance_cache, logger } + } + + pub(crate) async fn sync(&self) -> Result<(), Error> { + if let Some(mut sync_receiver) = self.register_or_subscribe_pending_sync() { + log_info!(self.logger, "Sync in progress, skipping."); + return sync_receiver.recv().await.map_err(|e| { + debug_assert!(false, "Failed to receive wallet sync result: {:?}", e); + log_error!(self.logger, "Failed to receive wallet sync result: {:?}", e); + Error::WalletOperationFailed + })?; + } + + let res = { + let wallet_lock = self.inner.lock().unwrap(); + + let wallet_sync_timeout_fut = tokio::time::timeout( + Duration::from_secs(BDK_WALLET_SYNC_TIMEOUT_SECS), + wallet_lock.sync(&self.blockchain, SyncOptions { progress: None }), + ); + + match wallet_sync_timeout_fut.await { + Ok(res) => match res { + Ok(()) => { + // TODO: Drop this workaround after BDK 1.0 upgrade. + // Update balance cache after syncing. + if let Ok(balance) = wallet_lock.get_balance() { + *self.balance_cache.write().unwrap() = balance; + } + Ok(()) + }, + Err(e) => match e { + bdk::Error::Esplora(ref be) => match **be { + bdk::blockchain::esplora::EsploraError::Reqwest(_) => { + log_error!( + self.logger, + "Sync failed due to HTTP connection error: {}", + e + ); + Err(From::from(e)) + }, + _ => { + log_error!(self.logger, "Sync failed due to Esplora error: {}", e); + Err(From::from(e)) + }, + }, + _ => { + log_error!(self.logger, "Wallet sync error: {}", e); + Err(From::from(e)) + }, + }, + }, + Err(e) => { + log_error!(self.logger, "On-chain wallet sync timed out: {}", e); + Err(Error::WalletOperationTimeout) + }, + } + }; + + self.propagate_result_to_subscribers(res); + + res + } + + pub(crate) fn build_payjoin_transaction( + &self, amount: Amount, recipient: ScriptBuf, + ) -> Result { + let fee_rate = self + .fee_estimator + .get_est_sat_per_1000_weight(ConfirmationTarget::OutputSpendingFee) as f32; + let fee_rate = FeeRate::from_sat_per_kwu(fee_rate); + let locked_wallet = self.inner.lock().unwrap(); + let mut tx_builder = locked_wallet.build_tx(); + tx_builder.add_recipient(recipient, amount.to_sat()).fee_rate(fee_rate).enable_rbf(); + let mut psbt = match tx_builder.finish() { + Ok((psbt, _)) => { + log_trace!(self.logger, "Created Payjoin transaction: {:?}", psbt); + psbt + }, + Err(err) => { + log_error!(self.logger, "Failed to create Payjoin transaction: {}", err); + return Err(err.into()); + }, + }; + locked_wallet.sign(&mut psbt, SignOptions::default())?; + Ok(psbt) + } + + pub(crate) fn sign_payjoin_proposal( + &self, payjoin_proposal_psbt: &mut Psbt, original_psbt: &mut Psbt, + ) -> Result { + // BDK only signs scripts that match its target descriptor by iterating through input map. + // The BIP 78 spec makes receiver clear sender input map UTXOs, so process_response will + // fail unless they're cleared. A PSBT unsigned_tx.input references input OutPoints and + // not a Script, so the sender signer must either be able to sign based on OutPoint UTXO + // lookup or otherwise re-introduce the Script from original_psbt. Since BDK PSBT signer + // only checks Input map Scripts for match against its descriptor, it won't sign if they're + // empty. Re-add the scripts from the original_psbt in order for BDK to sign properly. + // reference: https://github.com/bitcoindevkit/bdk-cli/pull/156#discussion_r1261300637 + let mut original_inputs = + original_psbt.unsigned_tx.input.iter().zip(&mut original_psbt.inputs).peekable(); + for (proposed_txin, proposed_psbtin) in + payjoin_proposal_psbt.unsigned_tx.input.iter().zip(&mut payjoin_proposal_psbt.inputs) + { + if let Some((original_txin, original_psbtin)) = original_inputs.peek() { + if proposed_txin.previous_output == original_txin.previous_output { + proposed_psbtin.witness_utxo = original_psbtin.witness_utxo.clone(); + proposed_psbtin.non_witness_utxo = original_psbtin.non_witness_utxo.clone(); + original_inputs.next(); + } + } + } + + let wallet = self.inner.lock().unwrap(); + let is_signed = wallet.sign(payjoin_proposal_psbt, SignOptions::default())?; + Ok(is_signed) + } + + pub(crate) fn create_funding_transaction( + &self, output_script: ScriptBuf, value_sats: u64, confirmation_target: ConfirmationTarget, + locktime: LockTime, + ) -> Result { + let fee_rate = FeeRate::from_sat_per_kwu( + self.fee_estimator.get_est_sat_per_1000_weight(confirmation_target) as f32, + ); + + 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) + .nlocktime(locktime) + .enable_rbf(); + + let mut psbt = match tx_builder.finish() { + Ok((psbt, _)) => { + log_trace!(self.logger, "Created funding PSBT: {:?}", psbt); + psbt + }, + Err(err) => { + log_error!(self.logger, "Failed to create funding transaction: {}", err); + return Err(err.into()); + }, + }; + + match locked_wallet.sign(&mut psbt, SignOptions::default()) { + Ok(finalized) => { + if !finalized { + return Err(Error::OnchainTxCreationFailed); + } + }, + Err(err) => { + log_error!(self.logger, "Failed to create funding transaction: {}", err); + return Err(err.into()); + }, + } + + Ok(psbt.extract_tx()) + } + + pub(crate) fn get_new_address(&self) -> Result { + let address_info = self.inner.lock().unwrap().get_address(AddressIndex::New)?; + Ok(address_info.address) + } + + fn get_new_internal_address(&self) -> Result { + let address_info = + self.inner.lock().unwrap().get_internal_address(AddressIndex::LastUnused)?; + Ok(address_info.address) + } + + pub(crate) fn get_balances( + &self, total_anchor_channels_reserve_sats: u64, + ) -> Result<(u64, u64), Error> { + // TODO: Drop this workaround after BDK 1.0 upgrade. + // We get the balance and update our cache if we can do so without blocking on the wallet + // Mutex. Otherwise, we return a cached value. + let balance = match self.inner.try_lock() { + Ok(wallet_lock) => { + // Update balance cache if we can. + let balance = wallet_lock.get_balance()?; + *self.balance_cache.write().unwrap() = balance.clone(); + balance + }, + Err(_) => self.balance_cache.read().unwrap().clone(), + }; + + let (total, spendable) = ( + balance.get_total(), + balance.get_spendable().saturating_sub(total_anchor_channels_reserve_sats), + ); + + Ok((total, spendable)) + } + + pub(crate) fn get_spendable_amount_sats( + &self, total_anchor_channels_reserve_sats: u64, + ) -> Result { + self.get_balances(total_anchor_channels_reserve_sats).map(|(_, s)| s) + } + + /// Send funds to the given address. + /// + /// If `amount_msat_or_drain` is `None` the wallet will be drained, i.e., all available funds will be + /// spent. + pub(crate) fn send_to_address( + &self, address: &bitcoin::Address, amount_msat_or_drain: Option, + ) -> Result { + let confirmation_target = ConfirmationTarget::OutputSpendingFee; + let fee_rate = FeeRate::from_sat_per_kwu( + self.fee_estimator.get_est_sat_per_1000_weight(confirmation_target) as f32, + ); + + let tx = { + let locked_wallet = self.inner.lock().unwrap(); + let mut tx_builder = locked_wallet.build_tx(); + + if let Some(amount_sats) = amount_msat_or_drain { + tx_builder + .add_recipient(address.script_pubkey(), amount_sats) + .fee_rate(fee_rate) + .enable_rbf(); + } else { + tx_builder + .drain_wallet() + .drain_to(address.script_pubkey()) + .fee_rate(fee_rate) + .enable_rbf(); + } + + let mut psbt = match tx_builder.finish() { + Ok((psbt, _)) => { + log_trace!(self.logger, "Created PSBT: {:?}", psbt); + psbt + }, + Err(err) => { + log_error!(self.logger, "Failed to create transaction: {}", err); + return Err(err.into()); + }, + }; + + match locked_wallet.sign(&mut psbt, SignOptions::default()) { + Ok(finalized) => { + if !finalized { + return Err(Error::OnchainTxCreationFailed); + } + }, + Err(err) => { + log_error!(self.logger, "Failed to create transaction: {}", err); + return Err(err.into()); + }, + } + psbt.extract_tx() + }; + + self.broadcaster.broadcast_transactions(&[&tx]); + + let txid = tx.txid(); + + if let Some(amount_sats) = amount_msat_or_drain { + log_info!( + self.logger, + "Created new transaction {} sending {}sats on-chain to address {}", + txid, + amount_sats, + address + ); + } else { + log_info!( + self.logger, + "Created new transaction {} sending all available on-chain funds to address {}", + txid, + address + ); + } + + Ok(txid) + } + + fn register_or_subscribe_pending_sync( + &self, + ) -> Option>> { + let mut sync_status_lock = self.sync_status.lock().unwrap(); + match sync_status_lock.deref_mut() { + WalletSyncStatus::Completed => { + // We're first to register for a sync. + let (tx, _) = tokio::sync::broadcast::channel(1); + *sync_status_lock = WalletSyncStatus::InProgress { subscribers: tx }; + None + }, + WalletSyncStatus::InProgress { subscribers } => { + // A sync is in-progress, we subscribe. + let rx = subscribers.subscribe(); + Some(rx) + }, + } + } + + fn propagate_result_to_subscribers(&self, res: Result<(), Error>) { + // Send the notification to any other tasks that might be waiting on it by now. + { + let mut sync_status_lock = self.sync_status.lock().unwrap(); + match sync_status_lock.deref_mut() { + WalletSyncStatus::Completed => { + // No sync in-progress, do nothing. + return; + }, + WalletSyncStatus::InProgress { subscribers } => { + // A sync is in-progress, we notify subscribers. + if subscribers.receiver_count() > 0 { + match subscribers.send(res) { + Ok(_) => (), + Err(e) => { + debug_assert!( + false, + "Failed to send wallet sync result to subscribers: {:?}", + e + ); + log_error!( + self.logger, + "Failed to send wallet sync result to subscribers: {:?}", + e + ); + }, + } + } + *sync_status_lock = WalletSyncStatus::Completed; + }, + } + } + } +} + +impl WalletSource for Wallet +where + D: BatchDatabase, + B::Target: BroadcasterInterface, + E::Target: FeeEstimator, + L::Target: Logger, +{ + fn list_confirmed_utxos(&self) -> Result, ()> { + let locked_wallet = self.inner.lock().unwrap(); + let mut utxos = Vec::new(); + let confirmed_txs: Vec = locked_wallet + .list_transactions(false) + .map_err(|e| { + log_error!(self.logger, "Failed to retrieve transactions from wallet: {}", e); + })? + .into_iter() + .filter(|t| t.confirmation_time.is_some()) + .collect(); + let unspent_confirmed_utxos = locked_wallet + .list_unspent() + .map_err(|e| { + log_error!( + self.logger, + "Failed to retrieve unspent transactions from wallet: {}", + e + ); + })? + .into_iter() + .filter(|u| confirmed_txs.iter().find(|t| t.txid == u.outpoint.txid).is_some()); + + for u in unspent_confirmed_utxos { + let payload = Payload::from_script(&u.txout.script_pubkey).map_err(|e| { + log_error!(self.logger, "Failed to retrieve script payload: {}", e); + })?; + + match payload { + Payload::WitnessProgram(program) => match program.version() { + WitnessVersion::V0 if program.program().len() == 20 => { + let wpkh = + WPubkeyHash::from_slice(program.program().as_bytes()).map_err(|e| { + log_error!(self.logger, "Failed to retrieve script payload: {}", e); + })?; + let utxo = Utxo::new_v0_p2wpkh(u.outpoint, u.txout.value, &wpkh); + utxos.push(utxo); + }, + WitnessVersion::V1 => { + XOnlyPublicKey::from_slice(program.program().as_bytes()).map_err(|e| { + log_error!(self.logger, "Failed to retrieve script payload: {}", e); + })?; + + let utxo = Utxo { + outpoint: u.outpoint, + output: TxOut { + value: u.txout.value, + script_pubkey: ScriptBuf::new_witness_program(&program), + }, + satisfaction_weight: 1 /* empty script_sig */ * WITNESS_SCALE_FACTOR as u64 + + 1 /* witness items */ + 1 /* schnorr sig len */ + 64, /* schnorr sig */ + }; + utxos.push(utxo); + }, + _ => { + log_error!( + self.logger, + "Unexpected witness version or length. Version: {}, Length: {}", + program.version(), + program.program().len() + ); + }, + }, + _ => { + log_error!( + self.logger, + "Tried to use a non-witness script. This must never happen." + ); + panic!("Tried to use a non-witness script. This must never happen."); + }, + } + } + + Ok(utxos) + } + + fn get_change_script(&self) -> Result { + let locked_wallet = self.inner.lock().unwrap(); + let address_info = + locked_wallet.get_internal_address(AddressIndex::LastUnused).map_err(|e| { + log_error!(self.logger, "Failed to retrieve new address from wallet: {}", e); + })?; + + Ok(address_info.address.script_pubkey()) + } + + fn sign_psbt(&self, mut psbt: PartiallySignedTransaction) -> Result { + let locked_wallet = self.inner.lock().unwrap(); + + // While BDK populates both `witness_utxo` and `non_witness_utxo` fields, LDK does not. As + // BDK by default doesn't trust the witness UTXO to account for the Segwit bug, we must + // disable it here as otherwise we fail to sign. + let mut sign_options = SignOptions::default(); + sign_options.trust_witness_utxo = true; + + match locked_wallet.sign(&mut psbt, sign_options) { + Ok(_finalized) => { + // BDK will fail to finalize for all LDK-provided inputs of the PSBT. Unfortunately + // we can't check more fine grained if it succeeded for all the other inputs here, + // so we just ignore the returned `finalized` bool. + }, + Err(err) => { + log_error!(self.logger, "Failed to sign transaction: {}", err); + return Err(()); + }, + } + + Ok(psbt.extract_tx()) + } +} + +/// Similar to [`KeysManager`], but overrides the destination and shutdown scripts so they are +/// directly spendable by the BDK wallet. +pub struct WalletKeysManager +where + D: BatchDatabase, + B::Target: BroadcasterInterface, + E::Target: FeeEstimator, + L::Target: Logger, +{ + inner: KeysManager, + wallet: Arc>, + logger: L, +} + +impl WalletKeysManager +where + D: BatchDatabase, + B::Target: BroadcasterInterface, + E::Target: FeeEstimator, + L::Target: Logger, +{ + /// Constructs a `WalletKeysManager` that overrides the destination and shutdown scripts. + /// + /// See [`KeysManager::new`] for more information on `seed`, `starting_time_secs`, and + /// `starting_time_nanos`. + pub fn new( + seed: &[u8; 32], starting_time_secs: u64, starting_time_nanos: u32, + wallet: Arc>, logger: L, + ) -> Self { + let inner = KeysManager::new(seed, starting_time_secs, starting_time_nanos); + Self { inner, wallet, logger } + } + + pub fn sign_message(&self, msg: &[u8]) -> Result { + message_signing::sign(msg, &self.inner.get_node_secret_key()) + .or(Err(Error::MessageSigningFailed)) + } + + pub fn get_node_secret_key(&self) -> SecretKey { + self.inner.get_node_secret_key() + } + + pub fn verify_signature(&self, msg: &[u8], sig: &str, pkey: &PublicKey) -> bool { + message_signing::verify(msg, sig, pkey) + } +} + +impl NodeSigner for WalletKeysManager +where + D: BatchDatabase, + B::Target: BroadcasterInterface, + E::Target: FeeEstimator, + L::Target: Logger, +{ + fn get_node_id(&self, recipient: Recipient) -> Result { + self.inner.get_node_id(recipient) + } + + fn ecdh( + &self, recipient: Recipient, other_key: &PublicKey, tweak: Option<&Scalar>, + ) -> Result { + self.inner.ecdh(recipient, other_key, tweak) + } + + fn get_inbound_payment_key_material(&self) -> KeyMaterial { + self.inner.get_inbound_payment_key_material() + } + + fn sign_invoice( + &self, hrp_bytes: &[u8], invoice_data: &[u5], recipient: Recipient, + ) -> Result { + self.inner.sign_invoice(hrp_bytes, invoice_data, recipient) + } + + fn sign_gossip_message(&self, msg: UnsignedGossipMessage<'_>) -> Result { + self.inner.sign_gossip_message(msg) + } + + fn sign_bolt12_invoice( + &self, invoice: &lightning::offers::invoice::UnsignedBolt12Invoice, + ) -> Result { + self.inner.sign_bolt12_invoice(invoice) + } + + fn sign_bolt12_invoice_request( + &self, invoice_request: &lightning::offers::invoice_request::UnsignedInvoiceRequest, + ) -> Result { + self.inner.sign_bolt12_invoice_request(invoice_request) + } +} + +impl OutputSpender for WalletKeysManager +where + D: BatchDatabase, + B::Target: BroadcasterInterface, + E::Target: FeeEstimator, + L::Target: Logger, +{ + /// See [`KeysManager::spend_spendable_outputs`] for documentation on this method. + fn spend_spendable_outputs( + &self, descriptors: &[&SpendableOutputDescriptor], outputs: Vec, + change_destination_script: ScriptBuf, feerate_sat_per_1000_weight: u32, + locktime: Option, secp_ctx: &Secp256k1, + ) -> Result { + self.inner.spend_spendable_outputs( + descriptors, + outputs, + change_destination_script, + feerate_sat_per_1000_weight, + locktime, + secp_ctx, + ) + } +} + +impl EntropySource for WalletKeysManager +where + D: BatchDatabase, + B::Target: BroadcasterInterface, + E::Target: FeeEstimator, + L::Target: Logger, +{ + fn get_secure_random_bytes(&self) -> [u8; 32] { + self.inner.get_secure_random_bytes() + } +} + +impl SignerProvider for WalletKeysManager +where + D: BatchDatabase, + B::Target: BroadcasterInterface, + E::Target: FeeEstimator, + L::Target: Logger, +{ + type EcdsaSigner = InMemorySigner; + + fn generate_channel_keys_id( + &self, inbound: bool, channel_value_satoshis: u64, user_channel_id: u128, + ) -> [u8; 32] { + self.inner.generate_channel_keys_id(inbound, channel_value_satoshis, user_channel_id) + } + + fn derive_channel_signer( + &self, channel_value_satoshis: u64, channel_keys_id: [u8; 32], + ) -> Self::EcdsaSigner { + self.inner.derive_channel_signer(channel_value_satoshis, channel_keys_id) + } + + fn read_chan_signer(&self, reader: &[u8]) -> Result { + self.inner.read_chan_signer(reader) + } + + fn get_destination_script(&self, _channel_keys_id: [u8; 32]) -> Result { + let address = self.wallet.get_new_address().map_err(|e| { + log_error!(self.logger, "Failed to retrieve new address from wallet: {}", e); + })?; + Ok(address.script_pubkey()) + } + + fn get_shutdown_scriptpubkey(&self) -> Result { + let address = self.wallet.get_new_address().map_err(|e| { + log_error!(self.logger, "Failed to retrieve new address from wallet: {}", e); + })?; + + match address.payload { + Payload::WitnessProgram(program) => ShutdownScript::new_witness_program(&program) + .map_err(|e| { + log_error!(self.logger, "Invalid shutdown script: {:?}", e); + }), + _ => { + log_error!( + self.logger, + "Tried to use a non-witness address. This must never happen." + ); + panic!("Tried to use a non-witness address. This must never happen."); + }, + } + } +} + +impl ChangeDestinationSource for WalletKeysManager +where + D: BatchDatabase, + B::Target: BroadcasterInterface, + E::Target: FeeEstimator, + L::Target: Logger, +{ + fn get_change_destination_script(&self) -> Result { + let address = self.wallet.get_new_internal_address().map_err(|e| { + log_error!(self.logger, "Failed to retrieve new address from wallet: {}", e); + })?; + Ok(address.script_pubkey()) + } +} From 9dec96dba51e3b3881ec81d285803d2384230094 Mon Sep 17 00:00:00 2001 From: jbesraa Date: Wed, 24 Jul 2024 15:17:35 +0300 Subject: [PATCH 2/6] Implement `Confirm` trait to `PaymentHandler` The `Confirm` trait is implemented in order to track the Payjoin transaction(s). We track two different transaction: 1. Original PSBT, which is the initial transaction sent to the Payjoin receiver. The receiver can decide to broadcast this transaction instead of finishing the Payjoin flow. Those we track it. 2. Final Payjoin transaction. The transaction constructed after completing the Payjoin flow, validated and broadcasted by the Payjoin sender. --- src/payment/payjoin/handler.rs | 101 ++++++++++++++++++++++++++++++++- 1 file changed, 99 insertions(+), 2 deletions(-) diff --git a/src/payment/payjoin/handler.rs b/src/payment/payjoin/handler.rs index 8523e7362..703ce71eb 100644 --- a/src/payment/payjoin/handler.rs +++ b/src/payment/payjoin/handler.rs @@ -1,6 +1,8 @@ use bitcoin::address::NetworkChecked; +use bitcoin::block::Header; use bitcoin::psbt::Psbt; -use bitcoin::{Script, Transaction, Txid}; +use bitcoin::{BlockHash, Script, Transaction, Txid}; +use lightning::chain::transaction::TransactionData; use crate::config::PAYJOIN_REQUEST_TIMEOUT; use crate::error::Error; @@ -13,7 +15,7 @@ use crate::types::{ChainSource, EventQueue, PaymentStore, Wallet}; use crate::Event; use crate::PaymentDetails; -use lightning::chain::Filter; +use lightning::chain::{BestBlock, Confirm, Filter}; use lightning::ln::channelmanager::PaymentId; use lightning::log_error; use lightning::util::logger::Logger; @@ -165,4 +167,99 @@ impl PayjoinHandler { Err(Error::PayjoinRequestSendingFailed) } } + + fn internal_transactions_confirmed( + &self, header: &Header, txdata: &TransactionData, height: u32, + ) { + for (_, tx) in txdata { + let confirmed_tx_txid = tx.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 mut 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) {} } From ce87a4439d765dc0eac2104f788bbe9fe33389fc Mon Sep 17 00:00:00 2001 From: jbesraa Date: Tue, 23 Jul 2024 15:09:18 +0300 Subject: [PATCH 3/6] Add `NodeBuilder::PayjoinConfig` to allow .. Payjoin transactions --- src/builder.rs | 34 ++++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/src/builder.rs b/src/builder.rs index fac2ae0c5..f619cfaf2 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -100,6 +100,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 +146,8 @@ pub enum BuildError { WalletSetupFailed, /// We failed to setup the logger. LoggerSetupFailed, + /// Invalid Payjoin configuration. + InvalidPayjoinConfig, } impl fmt::Display for BuildError { @@ -162,6 +169,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 +193,7 @@ pub struct NodeBuilder { chain_data_source_config: Option, gossip_source_config: Option, liquidity_source_config: Option, + payjoin_config: Option, } impl NodeBuilder { @@ -197,12 +209,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 +287,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 +502,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 +524,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 +610,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 +762,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)); From 56b1660f6c19232fdda806c201c59694784fbbf5 Mon Sep 17 00:00:00 2001 From: jbesraa Date: Tue, 23 Jul 2024 15:11:33 +0300 Subject: [PATCH 4/6] Expose `PayjoinPayment` module --- Cargo.toml | 2 + bindings/ldk_node.udl | 27 +++ src/builder.rs | 21 ++ src/lib.rs | 122 +++++++++++- tests/common/mod.rs | 49 +++++ tests/integration_tests_payjoin.rs | 299 +++++++++++++++++++++++++++++ 6 files changed, 518 insertions(+), 2 deletions(-) create mode 100644 tests/integration_tests_payjoin.rs diff --git a/Cargo.toml b/Cargo.toml index 94ca4b176..28e06565b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -90,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.16.0", default-features = false, features = ["send", "v2", "receive"] } +reqwest = { version = "0.11", default-features = false, features = ["json", "rustls-tls", "blocking"] } [target.'cfg(not(no_download))'.dev-dependencies] electrsd = { version = "0.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 f619cfaf2..c3a8649a6 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}; @@ -1231,6 +1232,25 @@ fn build_with_store_internal( let (stop_sender, _) = tokio::sync::watch::channel(()); let (event_handling_stopped_sender, _) = tokio::sync::watch::channel(()); + let payjoin_handler = payjoin_config.map(|pj_config| { + Arc::new(PayjoinHandler::new( + Arc::clone(&tx_sync), + Arc::clone(&event_queue), + Arc::clone(&logger), + pj_config.payjoin_relay.clone(), + Arc::clone(&payment_store), + Arc::clone(&wallet), + )) + }); + + let is_listening = Arc::new(AtomicBool::new(false)); + let latest_wallet_sync_timestamp = Arc::new(RwLock::new(None)); + let latest_onchain_wallet_sync_timestamp = Arc::new(RwLock::new(None)); + let latest_fee_rate_cache_update_timestamp = Arc::new(RwLock::new(None)); + let latest_rgs_snapshot_timestamp = Arc::new(RwLock::new(None)); + let latest_node_announcement_broadcast_timestamp = Arc::new(RwLock::new(None)); + let latest_channel_monitor_archival_height = Arc::new(RwLock::new(None)); + Ok(Node { runtime, stop_sender, @@ -1243,6 +1263,7 @@ fn build_with_store_internal( channel_manager, chain_monitor, output_sweeper, + payjoin_handler, peer_manager, onion_messenger, connection_manager, diff --git a/src/lib.rs b/src/lib.rs index 1e30c61c0..a50af0d55 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, @@ -254,6 +258,68 @@ impl Node { .continuously_sync_wallets(stop_sync_receiver, sync_cman, sync_cmon, sync_sweeper) .await; }); + let sync_logger = Arc::clone(&self.logger); + let sync_payjoin = &self.payjoin_handler.as_ref(); + let sync_payjoin = sync_payjoin.map(Arc::clone); + let sync_wallet_timestamp = Arc::clone(&self.latest_wallet_sync_timestamp); + let sync_monitor_archival_height = Arc::clone(&self.latest_channel_monitor_archival_height); + let mut stop_sync = self.stop_sender.subscribe(); + let wallet_sync_interval_secs = + self.config.wallet_sync_interval_secs.max(WALLET_SYNC_INTERVAL_MINIMUM_SECS); + runtime.spawn(async move { + let mut wallet_sync_interval = + tokio::time::interval(Duration::from_secs(wallet_sync_interval_secs)); + wallet_sync_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); + loop { + tokio::select! { + _ = stop_sync.changed() => { + log_trace!( + sync_logger, + "Stopping background syncing Lightning wallet.", + ); + return; + } + _ = wallet_sync_interval.tick() => { + let mut confirmables = vec![ + &*sync_cman as &(dyn Confirm + Sync + Send), + &*sync_cmon as &(dyn Confirm + Sync + Send), + &*sync_sweeper as &(dyn Confirm + Sync + Send), + ]; + if let Some(sync_payjoin) = sync_payjoin.as_ref() { + confirmables.push(sync_payjoin.as_ref() as &(dyn Confirm + Sync + Send)); + } + let now = Instant::now(); + let timeout_fut = tokio::time::timeout(Duration::from_secs(LDK_WALLET_SYNC_TIMEOUT_SECS), tx_sync.sync(confirmables)); + match timeout_fut.await { + Ok(res) => match res { + Ok(()) => { + log_trace!( + sync_logger, + "Background sync of Lightning wallet finished in {}ms.", + now.elapsed().as_millis() + ); + let unix_time_secs_opt = + SystemTime::now().duration_since(UNIX_EPOCH).ok().map(|d| d.as_secs()); + *sync_wallet_timestamp.write().unwrap() = unix_time_secs_opt; + + periodically_archive_fully_resolved_monitors( + Arc::clone(&archive_cman), + Arc::clone(&archive_cmon), + Arc::clone(&sync_monitor_archival_height) + ); + } + Err(e) => { + log_error!(sync_logger, "Background sync of Lightning wallet failed: {}", e) + } + } + Err(e) => { + log_error!(sync_logger, "Background sync of Lightning wallet timed out: {}", e) + } + } + } + } + } + }); if self.gossip_source.is_rgs() { let gossip_source = Arc::clone(&self.gossip_source); @@ -960,6 +1026,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 +1320,22 @@ 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); + let sync_logger = Arc::clone(&self.logger); + let sync_payjoin = &self.payjoin_handler.as_ref(); + let mut confirmables = vec![ + &*sync_cman as &(dyn Confirm + Sync + Send), + &*sync_cmon as &(dyn Confirm + Sync + Send), + &*sync_sweeper as &(dyn Confirm + Sync + Send), + ]; + if let Some(sync_payjoin) = sync_payjoin { + confirmables.push(sync_payjoin.as_ref() as &(dyn Confirm + Sync + Send)); + } + let sync_wallet_timestamp = Arc::clone(&self.latest_wallet_sync_timestamp); + let sync_fee_rate_update_timestamp = + Arc::clone(&self.latest_fee_rate_cache_update_timestamp); + let sync_onchain_wallet_timestamp = Arc::clone(&self.latest_onchain_wallet_sync_timestamp); + let sync_monitor_archival_height = Arc::clone(&self.latest_channel_monitor_archival_height); + tokio::task::block_in_place(move || { tokio::runtime::Builder::new_multi_thread().enable_all().build().unwrap().block_on( async move { diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 26aff3d11..05146eac7 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..88e83afb2 --- /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); +} From cf788ef671ecd778c2516d56cc09f61d5fb682cb Mon Sep 17 00:00:00 2001 From: esraa Date: Thu, 5 Dec 2024 15:31:42 +0200 Subject: [PATCH 5/6] f --- Cargo.toml | 4 +- src/builder.rs | 9 - src/event.rs | 5 +- src/lib.rs | 77 ---- src/payment/payjoin/handler.rs | 199 ++++----- src/payment/payjoin/mod.rs | 8 +- src/payment/store.rs | 2 +- src/wallet.rs | 734 --------------------------------- src/wallet/mod.rs | 53 +++ 9 files changed, 166 insertions(+), 925 deletions(-) delete mode 100644 src/wallet.rs diff --git a/Cargo.toml b/Cargo.toml index 28e06565b..7654272e4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -75,7 +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.16.0", default-features = false, features = ["send", "v2"] } +payjoin = { version = "0.21.0", default-features = false, features = ["send", "v2"] } vss-client = "0.3" prost = { version = "0.11.6", default-features = false} @@ -90,7 +90,7 @@ 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.16.0", default-features = false, features = ["send", "v2", "receive"] } +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] diff --git a/src/builder.rs b/src/builder.rs index c3a8649a6..2185837d2 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -1234,7 +1234,6 @@ fn build_with_store_internal( let payjoin_handler = payjoin_config.map(|pj_config| { Arc::new(PayjoinHandler::new( - Arc::clone(&tx_sync), Arc::clone(&event_queue), Arc::clone(&logger), pj_config.payjoin_relay.clone(), @@ -1243,14 +1242,6 @@ fn build_with_store_internal( )) }); - 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)); - let latest_fee_rate_cache_update_timestamp = Arc::new(RwLock::new(None)); - let latest_rgs_snapshot_timestamp = Arc::new(RwLock::new(None)); - let latest_node_announcement_broadcast_timestamp = Arc::new(RwLock::new(None)); - let latest_channel_monitor_archival_height = Arc::new(RwLock::new(None)); - Ok(Node { runtime, stop_sender, diff --git a/src/event.rs b/src/event.rs index ab3d56df9..9a2718150 100644 --- a/src/event.rs +++ b/src/event.rs @@ -332,7 +332,7 @@ impl_writeable_tlv_based_enum!(Event, (0, amount_sats, required), (2, txid, required), (4, reason, required), - }; + } ); #[derive(Debug, Clone, PartialEq, Eq)] @@ -361,8 +361,7 @@ pub enum PayjoinPaymentFailureReason { impl_writeable_tlv_based_enum!(PayjoinPaymentFailureReason, (0, Timeout) => {}, (1, RequestSendingFailed) => {}, - (2, ResponseProcessingFailed) => {}; ->>>>>>> 1136aa4 (Implement `PayjoinPayment` ..) + (2, ResponseProcessingFailed) => {} ); pub struct EventQueue diff --git a/src/lib.rs b/src/lib.rs index a50af0d55..129a78204 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -258,68 +258,6 @@ impl Node { .continuously_sync_wallets(stop_sync_receiver, sync_cman, sync_cmon, sync_sweeper) .await; }); - let sync_logger = Arc::clone(&self.logger); - let sync_payjoin = &self.payjoin_handler.as_ref(); - let sync_payjoin = sync_payjoin.map(Arc::clone); - let sync_wallet_timestamp = Arc::clone(&self.latest_wallet_sync_timestamp); - let sync_monitor_archival_height = Arc::clone(&self.latest_channel_monitor_archival_height); - let mut stop_sync = self.stop_sender.subscribe(); - let wallet_sync_interval_secs = - self.config.wallet_sync_interval_secs.max(WALLET_SYNC_INTERVAL_MINIMUM_SECS); - runtime.spawn(async move { - let mut wallet_sync_interval = - tokio::time::interval(Duration::from_secs(wallet_sync_interval_secs)); - wallet_sync_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); - loop { - tokio::select! { - _ = stop_sync.changed() => { - log_trace!( - sync_logger, - "Stopping background syncing Lightning wallet.", - ); - return; - } - _ = wallet_sync_interval.tick() => { - let mut confirmables = vec![ - &*sync_cman as &(dyn Confirm + Sync + Send), - &*sync_cmon as &(dyn Confirm + Sync + Send), - &*sync_sweeper as &(dyn Confirm + Sync + Send), - ]; - if let Some(sync_payjoin) = sync_payjoin.as_ref() { - confirmables.push(sync_payjoin.as_ref() as &(dyn Confirm + Sync + Send)); - } - let now = Instant::now(); - let timeout_fut = tokio::time::timeout(Duration::from_secs(LDK_WALLET_SYNC_TIMEOUT_SECS), tx_sync.sync(confirmables)); - match timeout_fut.await { - Ok(res) => match res { - Ok(()) => { - log_trace!( - sync_logger, - "Background sync of Lightning wallet finished in {}ms.", - now.elapsed().as_millis() - ); - let unix_time_secs_opt = - SystemTime::now().duration_since(UNIX_EPOCH).ok().map(|d| d.as_secs()); - *sync_wallet_timestamp.write().unwrap() = unix_time_secs_opt; - - periodically_archive_fully_resolved_monitors( - Arc::clone(&archive_cman), - Arc::clone(&archive_cmon), - Arc::clone(&sync_monitor_archival_height) - ); - } - Err(e) => { - log_error!(sync_logger, "Background sync of Lightning wallet failed: {}", e) - } - } - Err(e) => { - log_error!(sync_logger, "Background sync of Lightning wallet timed out: {}", e) - } - } - } - } - } - }); if self.gossip_source.is_rgs() { let gossip_source = Arc::clone(&self.gossip_source); @@ -1320,21 +1258,6 @@ 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); - let sync_logger = Arc::clone(&self.logger); - let sync_payjoin = &self.payjoin_handler.as_ref(); - let mut confirmables = vec![ - &*sync_cman as &(dyn Confirm + Sync + Send), - &*sync_cmon as &(dyn Confirm + Sync + Send), - &*sync_sweeper as &(dyn Confirm + Sync + Send), - ]; - if let Some(sync_payjoin) = sync_payjoin { - confirmables.push(sync_payjoin.as_ref() as &(dyn Confirm + Sync + Send)); - } - let sync_wallet_timestamp = Arc::clone(&self.latest_wallet_sync_timestamp); - let sync_fee_rate_update_timestamp = - Arc::clone(&self.latest_fee_rate_cache_update_timestamp); - let sync_onchain_wallet_timestamp = Arc::clone(&self.latest_onchain_wallet_sync_timestamp); - let sync_monitor_archival_height = Arc::clone(&self.latest_channel_monitor_archival_height); tokio::task::block_in_place(move || { tokio::runtime::Builder::new_multi_thread().enable_all().build().unwrap().block_on( diff --git a/src/payment/payjoin/handler.rs b/src/payment/payjoin/handler.rs index 703ce71eb..ccb2cdd80 100644 --- a/src/payment/payjoin/handler.rs +++ b/src/payment/payjoin/handler.rs @@ -1,21 +1,22 @@ -use bitcoin::address::NetworkChecked; use bitcoin::block::Header; use bitcoin::psbt::Psbt; -use bitcoin::{BlockHash, Script, Transaction, Txid}; +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::PayjoinPaymentFailureReason; +use crate::event::{EventQueue, PayjoinPaymentFailureReason}; use crate::logger::FilesystemLogger; -use crate::payment::store::PaymentDetailsUpdate; +use crate::payment::store::{PaymentDetailsUpdate, PaymentStore}; use crate::payment::PaymentKind; use crate::payment::{PaymentDirection, PaymentStatus}; -use crate::types::{ChainSource, EventQueue, PaymentStore, Wallet}; +use crate::types::Wallet; use crate::Event; use crate::PaymentDetails; -use lightning::chain::{BestBlock, Confirm, Filter}; +// use lightning::chain::{BestBlock, Confirm, Filter}; +use lightning::chain::Confirm; use lightning::ln::channelmanager::PaymentId; use lightning::log_error; use lightning::util::logger::Logger; @@ -23,32 +24,38 @@ use lightning::util::logger::Logger; use std::sync::Arc; pub(crate) struct PayjoinHandler { - chain_source: Arc, - event_queue: Arc, + #[allow(dead_code)] + chain_source: Option>, + event_queue: Arc>>, logger: Arc, payjoin_relay: payjoin::Url, - payment_store: Arc, + payment_store: Arc>>, wallet: Arc, } impl PayjoinHandler { pub(crate) fn new( - chain_source: Arc, event_queue: Arc, + event_queue: Arc>>, logger: Arc, payjoin_relay: payjoin::Url, - payment_store: Arc, wallet: Arc, + payment_store: Arc>>, + wallet: Arc, ) -> Self { - Self { chain_source, event_queue, logger, payjoin_relay, payment_store, wallet } + Self { chain_source: None, + event_queue, + logger, payjoin_relay, + payment_store, + wallet } } pub(crate) fn start_request( - &self, payjoin_uri: payjoin::Uri<'_, NetworkChecked>, + &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(&original_psbt.unsigned_tx.txid()); + let payment_id = self.payment_id(&tx.compute_txid()); self.payment_store.insert(PaymentDetails::new( payment_id, PaymentKind::Payjoin, @@ -56,22 +63,19 @@ impl PayjoinHandler { PaymentDirection::Outbound, PaymentStatus::Pending, ))?; - let mut update_payment = PaymentDetailsUpdate::new(payment_id); - update_payment.txid = Some(tx.txid()); - let _ = self.payment_store.update(&update_payment); - self.chain_source.register_tx(&tx.txid(), Script::empty()); + // self.chain_source.register_tx(&tx.txid(), Script::empty()); Ok(original_psbt) } pub(crate) async fn send_request( - &self, payjoin_uri: payjoin::Uri<'_, NetworkChecked>, original_psbt: &mut Psbt, + &self, payjoin_uri: payjoin::PjUri<'_>, original_psbt: &mut Psbt, ) -> Result, Error> { - let (request, context) = payjoin::send::RequestBuilder::from_psbt_and_uri( + let (request, context) = payjoin::send::SenderBuilder::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())) + .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 @@ -102,10 +106,12 @@ impl PayjoinHandler { Error::PayjoinRequestSendingFailed })?; let response = response.to_vec(); - context.process_response(&mut response.as_slice()).map_err(|e| { + 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( @@ -115,14 +121,16 @@ impl PayjoinHandler { 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.txid()); + 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 txid = proposal_tx.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()); + 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 @@ -146,14 +154,14 @@ impl PayjoinHandler { &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.txid()); + 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.txid(), + txid: original_psbt.unsigned_tx.compute_txid(), amount_sats: payment_details .amount_msat .ok_or(Error::PayjoinRequestMissingAmount)?, @@ -169,81 +177,82 @@ impl PayjoinHandler { } fn internal_transactions_confirmed( - &self, header: &Header, txdata: &TransactionData, height: u32, + &self, _header: &Header, _txdata: &TransactionData, _height: u32, ) { - for (_, tx) in txdata { - let confirmed_tx_txid = tx.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 - }, - }); + // 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, - }); - } - } + // } 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 mut 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 + 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) { + 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); - } + 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); + // } } } diff --git a/src/payment/payjoin/mod.rs b/src/payment/payjoin/mod.rs index fafa587de..98d04a2a0 100644 --- a/src/payment/payjoin/mod.rs +++ b/src/payment/payjoin/mod.rs @@ -83,15 +83,15 @@ impl PayjoinPayment { /// [`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 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 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(); diff --git a/src/payment/store.rs b/src/payment/store.rs index c7bf8e0fd..2a5f35c26 100644 --- a/src/payment/store.rs +++ b/src/payment/store.rs @@ -296,7 +296,7 @@ impl_writeable_tlv_based_enum!(PaymentKind, (3, quantity, option), (4, secret, option), }, - (12, Payjoin) => { }; + (12, Payjoin) => { } ); /// Limits applying to how much fee we allow an LSP to deduct from the payment amount. diff --git a/src/wallet.rs b/src/wallet.rs deleted file mode 100644 index 80d56542d..000000000 --- a/src/wallet.rs +++ /dev/null @@ -1,734 +0,0 @@ -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}; -use lightning::ln::msgs::{DecodeError, UnsignedGossipMessage}; -use lightning::ln::script::ShutdownScript; -use lightning::sign::{ - ChangeDestinationSource, EntropySource, InMemorySigner, KeyMaterial, KeysManager, NodeSigner, - OutputSpender, Recipient, SignerProvider, SpendableOutputDescriptor, -}; - -use lightning::util::message_signing; - -use bdk::blockchain::EsploraBlockchain; -use bdk::database::BatchDatabase; -use bdk::wallet::AddressIndex; -use bdk::{Balance, FeeRate}; -use bdk::{SignOptions, SyncOptions}; - -use bitcoin::address::{Payload, WitnessVersion}; -use bitcoin::bech32::u5; -use bitcoin::blockdata::constants::WITNESS_SCALE_FACTOR; -use bitcoin::blockdata::locktime::absolute::LockTime; -use bitcoin::hash_types::WPubkeyHash; -use bitcoin::hashes::Hash; -use bitcoin::key::XOnlyPublicKey; -use bitcoin::psbt::PartiallySignedTransaction; -use bitcoin::secp256k1::ecdh::SharedSecret; -use bitcoin::secp256k1::ecdsa::{RecoverableSignature, Signature}; -use bitcoin::secp256k1::{PublicKey, Scalar, Secp256k1, SecretKey, Signing}; -use bitcoin::{Amount, ScriptBuf, Transaction, TxOut, Txid}; - -use std::ops::{Deref, DerefMut}; -use std::sync::{Arc, Mutex, RwLock}; -use std::time::Duration; - -enum WalletSyncStatus { - Completed, - InProgress { subscribers: tokio::sync::broadcast::Sender> }, -} - -pub struct Wallet -where - D: BatchDatabase, - B::Target: BroadcasterInterface, - E::Target: FeeEstimator, - L::Target: Logger, -{ - // A BDK blockchain used for wallet sync. - blockchain: EsploraBlockchain, - // A BDK on-chain wallet. - inner: Mutex>, - // A cache storing the most recently retrieved fee rate estimations. - broadcaster: B, - fee_estimator: E, - // A Mutex holding the current sync status. - sync_status: Mutex, - // TODO: Drop this workaround after BDK 1.0 upgrade. - balance_cache: RwLock, - logger: L, -} - -impl Wallet -where - D: BatchDatabase, - B::Target: BroadcasterInterface, - E::Target: FeeEstimator, - L::Target: Logger, -{ - pub(crate) fn new( - blockchain: EsploraBlockchain, wallet: bdk::Wallet, broadcaster: B, fee_estimator: E, - logger: L, - ) -> Self { - let start_balance = wallet.get_balance().unwrap_or(Balance { - immature: 0, - trusted_pending: 0, - untrusted_pending: 0, - confirmed: 0, - }); - - let inner = Mutex::new(wallet); - let sync_status = Mutex::new(WalletSyncStatus::Completed); - let balance_cache = RwLock::new(start_balance); - Self { blockchain, inner, broadcaster, fee_estimator, sync_status, balance_cache, logger } - } - - pub(crate) async fn sync(&self) -> Result<(), Error> { - if let Some(mut sync_receiver) = self.register_or_subscribe_pending_sync() { - log_info!(self.logger, "Sync in progress, skipping."); - return sync_receiver.recv().await.map_err(|e| { - debug_assert!(false, "Failed to receive wallet sync result: {:?}", e); - log_error!(self.logger, "Failed to receive wallet sync result: {:?}", e); - Error::WalletOperationFailed - })?; - } - - let res = { - let wallet_lock = self.inner.lock().unwrap(); - - let wallet_sync_timeout_fut = tokio::time::timeout( - Duration::from_secs(BDK_WALLET_SYNC_TIMEOUT_SECS), - wallet_lock.sync(&self.blockchain, SyncOptions { progress: None }), - ); - - match wallet_sync_timeout_fut.await { - Ok(res) => match res { - Ok(()) => { - // TODO: Drop this workaround after BDK 1.0 upgrade. - // Update balance cache after syncing. - if let Ok(balance) = wallet_lock.get_balance() { - *self.balance_cache.write().unwrap() = balance; - } - Ok(()) - }, - Err(e) => match e { - bdk::Error::Esplora(ref be) => match **be { - bdk::blockchain::esplora::EsploraError::Reqwest(_) => { - log_error!( - self.logger, - "Sync failed due to HTTP connection error: {}", - e - ); - Err(From::from(e)) - }, - _ => { - log_error!(self.logger, "Sync failed due to Esplora error: {}", e); - Err(From::from(e)) - }, - }, - _ => { - log_error!(self.logger, "Wallet sync error: {}", e); - Err(From::from(e)) - }, - }, - }, - Err(e) => { - log_error!(self.logger, "On-chain wallet sync timed out: {}", e); - Err(Error::WalletOperationTimeout) - }, - } - }; - - self.propagate_result_to_subscribers(res); - - res - } - - pub(crate) fn build_payjoin_transaction( - &self, amount: Amount, recipient: ScriptBuf, - ) -> Result { - let fee_rate = self - .fee_estimator - .get_est_sat_per_1000_weight(ConfirmationTarget::OutputSpendingFee) as f32; - let fee_rate = FeeRate::from_sat_per_kwu(fee_rate); - let locked_wallet = self.inner.lock().unwrap(); - let mut tx_builder = locked_wallet.build_tx(); - tx_builder.add_recipient(recipient, amount.to_sat()).fee_rate(fee_rate).enable_rbf(); - let mut psbt = match tx_builder.finish() { - Ok((psbt, _)) => { - log_trace!(self.logger, "Created Payjoin transaction: {:?}", psbt); - psbt - }, - Err(err) => { - log_error!(self.logger, "Failed to create Payjoin transaction: {}", err); - return Err(err.into()); - }, - }; - locked_wallet.sign(&mut psbt, SignOptions::default())?; - Ok(psbt) - } - - pub(crate) fn sign_payjoin_proposal( - &self, payjoin_proposal_psbt: &mut Psbt, original_psbt: &mut Psbt, - ) -> Result { - // BDK only signs scripts that match its target descriptor by iterating through input map. - // The BIP 78 spec makes receiver clear sender input map UTXOs, so process_response will - // fail unless they're cleared. A PSBT unsigned_tx.input references input OutPoints and - // not a Script, so the sender signer must either be able to sign based on OutPoint UTXO - // lookup or otherwise re-introduce the Script from original_psbt. Since BDK PSBT signer - // only checks Input map Scripts for match against its descriptor, it won't sign if they're - // empty. Re-add the scripts from the original_psbt in order for BDK to sign properly. - // reference: https://github.com/bitcoindevkit/bdk-cli/pull/156#discussion_r1261300637 - let mut original_inputs = - original_psbt.unsigned_tx.input.iter().zip(&mut original_psbt.inputs).peekable(); - for (proposed_txin, proposed_psbtin) in - payjoin_proposal_psbt.unsigned_tx.input.iter().zip(&mut payjoin_proposal_psbt.inputs) - { - if let Some((original_txin, original_psbtin)) = original_inputs.peek() { - if proposed_txin.previous_output == original_txin.previous_output { - proposed_psbtin.witness_utxo = original_psbtin.witness_utxo.clone(); - proposed_psbtin.non_witness_utxo = original_psbtin.non_witness_utxo.clone(); - original_inputs.next(); - } - } - } - - let wallet = self.inner.lock().unwrap(); - let is_signed = wallet.sign(payjoin_proposal_psbt, SignOptions::default())?; - Ok(is_signed) - } - - pub(crate) fn create_funding_transaction( - &self, output_script: ScriptBuf, value_sats: u64, confirmation_target: ConfirmationTarget, - locktime: LockTime, - ) -> Result { - let fee_rate = FeeRate::from_sat_per_kwu( - self.fee_estimator.get_est_sat_per_1000_weight(confirmation_target) as f32, - ); - - 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) - .nlocktime(locktime) - .enable_rbf(); - - let mut psbt = match tx_builder.finish() { - Ok((psbt, _)) => { - log_trace!(self.logger, "Created funding PSBT: {:?}", psbt); - psbt - }, - Err(err) => { - log_error!(self.logger, "Failed to create funding transaction: {}", err); - return Err(err.into()); - }, - }; - - match locked_wallet.sign(&mut psbt, SignOptions::default()) { - Ok(finalized) => { - if !finalized { - return Err(Error::OnchainTxCreationFailed); - } - }, - Err(err) => { - log_error!(self.logger, "Failed to create funding transaction: {}", err); - return Err(err.into()); - }, - } - - Ok(psbt.extract_tx()) - } - - pub(crate) fn get_new_address(&self) -> Result { - let address_info = self.inner.lock().unwrap().get_address(AddressIndex::New)?; - Ok(address_info.address) - } - - fn get_new_internal_address(&self) -> Result { - let address_info = - self.inner.lock().unwrap().get_internal_address(AddressIndex::LastUnused)?; - Ok(address_info.address) - } - - pub(crate) fn get_balances( - &self, total_anchor_channels_reserve_sats: u64, - ) -> Result<(u64, u64), Error> { - // TODO: Drop this workaround after BDK 1.0 upgrade. - // We get the balance and update our cache if we can do so without blocking on the wallet - // Mutex. Otherwise, we return a cached value. - let balance = match self.inner.try_lock() { - Ok(wallet_lock) => { - // Update balance cache if we can. - let balance = wallet_lock.get_balance()?; - *self.balance_cache.write().unwrap() = balance.clone(); - balance - }, - Err(_) => self.balance_cache.read().unwrap().clone(), - }; - - let (total, spendable) = ( - balance.get_total(), - balance.get_spendable().saturating_sub(total_anchor_channels_reserve_sats), - ); - - Ok((total, spendable)) - } - - pub(crate) fn get_spendable_amount_sats( - &self, total_anchor_channels_reserve_sats: u64, - ) -> Result { - self.get_balances(total_anchor_channels_reserve_sats).map(|(_, s)| s) - } - - /// Send funds to the given address. - /// - /// If `amount_msat_or_drain` is `None` the wallet will be drained, i.e., all available funds will be - /// spent. - pub(crate) fn send_to_address( - &self, address: &bitcoin::Address, amount_msat_or_drain: Option, - ) -> Result { - let confirmation_target = ConfirmationTarget::OutputSpendingFee; - let fee_rate = FeeRate::from_sat_per_kwu( - self.fee_estimator.get_est_sat_per_1000_weight(confirmation_target) as f32, - ); - - let tx = { - let locked_wallet = self.inner.lock().unwrap(); - let mut tx_builder = locked_wallet.build_tx(); - - if let Some(amount_sats) = amount_msat_or_drain { - tx_builder - .add_recipient(address.script_pubkey(), amount_sats) - .fee_rate(fee_rate) - .enable_rbf(); - } else { - tx_builder - .drain_wallet() - .drain_to(address.script_pubkey()) - .fee_rate(fee_rate) - .enable_rbf(); - } - - let mut psbt = match tx_builder.finish() { - Ok((psbt, _)) => { - log_trace!(self.logger, "Created PSBT: {:?}", psbt); - psbt - }, - Err(err) => { - log_error!(self.logger, "Failed to create transaction: {}", err); - return Err(err.into()); - }, - }; - - match locked_wallet.sign(&mut psbt, SignOptions::default()) { - Ok(finalized) => { - if !finalized { - return Err(Error::OnchainTxCreationFailed); - } - }, - Err(err) => { - log_error!(self.logger, "Failed to create transaction: {}", err); - return Err(err.into()); - }, - } - psbt.extract_tx() - }; - - self.broadcaster.broadcast_transactions(&[&tx]); - - let txid = tx.txid(); - - if let Some(amount_sats) = amount_msat_or_drain { - log_info!( - self.logger, - "Created new transaction {} sending {}sats on-chain to address {}", - txid, - amount_sats, - address - ); - } else { - log_info!( - self.logger, - "Created new transaction {} sending all available on-chain funds to address {}", - txid, - address - ); - } - - Ok(txid) - } - - fn register_or_subscribe_pending_sync( - &self, - ) -> Option>> { - let mut sync_status_lock = self.sync_status.lock().unwrap(); - match sync_status_lock.deref_mut() { - WalletSyncStatus::Completed => { - // We're first to register for a sync. - let (tx, _) = tokio::sync::broadcast::channel(1); - *sync_status_lock = WalletSyncStatus::InProgress { subscribers: tx }; - None - }, - WalletSyncStatus::InProgress { subscribers } => { - // A sync is in-progress, we subscribe. - let rx = subscribers.subscribe(); - Some(rx) - }, - } - } - - fn propagate_result_to_subscribers(&self, res: Result<(), Error>) { - // Send the notification to any other tasks that might be waiting on it by now. - { - let mut sync_status_lock = self.sync_status.lock().unwrap(); - match sync_status_lock.deref_mut() { - WalletSyncStatus::Completed => { - // No sync in-progress, do nothing. - return; - }, - WalletSyncStatus::InProgress { subscribers } => { - // A sync is in-progress, we notify subscribers. - if subscribers.receiver_count() > 0 { - match subscribers.send(res) { - Ok(_) => (), - Err(e) => { - debug_assert!( - false, - "Failed to send wallet sync result to subscribers: {:?}", - e - ); - log_error!( - self.logger, - "Failed to send wallet sync result to subscribers: {:?}", - e - ); - }, - } - } - *sync_status_lock = WalletSyncStatus::Completed; - }, - } - } - } -} - -impl WalletSource for Wallet -where - D: BatchDatabase, - B::Target: BroadcasterInterface, - E::Target: FeeEstimator, - L::Target: Logger, -{ - fn list_confirmed_utxos(&self) -> Result, ()> { - let locked_wallet = self.inner.lock().unwrap(); - let mut utxos = Vec::new(); - let confirmed_txs: Vec = locked_wallet - .list_transactions(false) - .map_err(|e| { - log_error!(self.logger, "Failed to retrieve transactions from wallet: {}", e); - })? - .into_iter() - .filter(|t| t.confirmation_time.is_some()) - .collect(); - let unspent_confirmed_utxos = locked_wallet - .list_unspent() - .map_err(|e| { - log_error!( - self.logger, - "Failed to retrieve unspent transactions from wallet: {}", - e - ); - })? - .into_iter() - .filter(|u| confirmed_txs.iter().find(|t| t.txid == u.outpoint.txid).is_some()); - - for u in unspent_confirmed_utxos { - let payload = Payload::from_script(&u.txout.script_pubkey).map_err(|e| { - log_error!(self.logger, "Failed to retrieve script payload: {}", e); - })?; - - match payload { - Payload::WitnessProgram(program) => match program.version() { - WitnessVersion::V0 if program.program().len() == 20 => { - let wpkh = - WPubkeyHash::from_slice(program.program().as_bytes()).map_err(|e| { - log_error!(self.logger, "Failed to retrieve script payload: {}", e); - })?; - let utxo = Utxo::new_v0_p2wpkh(u.outpoint, u.txout.value, &wpkh); - utxos.push(utxo); - }, - WitnessVersion::V1 => { - XOnlyPublicKey::from_slice(program.program().as_bytes()).map_err(|e| { - log_error!(self.logger, "Failed to retrieve script payload: {}", e); - })?; - - let utxo = Utxo { - outpoint: u.outpoint, - output: TxOut { - value: u.txout.value, - script_pubkey: ScriptBuf::new_witness_program(&program), - }, - satisfaction_weight: 1 /* empty script_sig */ * WITNESS_SCALE_FACTOR as u64 + - 1 /* witness items */ + 1 /* schnorr sig len */ + 64, /* schnorr sig */ - }; - utxos.push(utxo); - }, - _ => { - log_error!( - self.logger, - "Unexpected witness version or length. Version: {}, Length: {}", - program.version(), - program.program().len() - ); - }, - }, - _ => { - log_error!( - self.logger, - "Tried to use a non-witness script. This must never happen." - ); - panic!("Tried to use a non-witness script. This must never happen."); - }, - } - } - - Ok(utxos) - } - - fn get_change_script(&self) -> Result { - let locked_wallet = self.inner.lock().unwrap(); - let address_info = - locked_wallet.get_internal_address(AddressIndex::LastUnused).map_err(|e| { - log_error!(self.logger, "Failed to retrieve new address from wallet: {}", e); - })?; - - Ok(address_info.address.script_pubkey()) - } - - fn sign_psbt(&self, mut psbt: PartiallySignedTransaction) -> Result { - let locked_wallet = self.inner.lock().unwrap(); - - // While BDK populates both `witness_utxo` and `non_witness_utxo` fields, LDK does not. As - // BDK by default doesn't trust the witness UTXO to account for the Segwit bug, we must - // disable it here as otherwise we fail to sign. - let mut sign_options = SignOptions::default(); - sign_options.trust_witness_utxo = true; - - match locked_wallet.sign(&mut psbt, sign_options) { - Ok(_finalized) => { - // BDK will fail to finalize for all LDK-provided inputs of the PSBT. Unfortunately - // we can't check more fine grained if it succeeded for all the other inputs here, - // so we just ignore the returned `finalized` bool. - }, - Err(err) => { - log_error!(self.logger, "Failed to sign transaction: {}", err); - return Err(()); - }, - } - - Ok(psbt.extract_tx()) - } -} - -/// Similar to [`KeysManager`], but overrides the destination and shutdown scripts so they are -/// directly spendable by the BDK wallet. -pub struct WalletKeysManager -where - D: BatchDatabase, - B::Target: BroadcasterInterface, - E::Target: FeeEstimator, - L::Target: Logger, -{ - inner: KeysManager, - wallet: Arc>, - logger: L, -} - -impl WalletKeysManager -where - D: BatchDatabase, - B::Target: BroadcasterInterface, - E::Target: FeeEstimator, - L::Target: Logger, -{ - /// Constructs a `WalletKeysManager` that overrides the destination and shutdown scripts. - /// - /// See [`KeysManager::new`] for more information on `seed`, `starting_time_secs`, and - /// `starting_time_nanos`. - pub fn new( - seed: &[u8; 32], starting_time_secs: u64, starting_time_nanos: u32, - wallet: Arc>, logger: L, - ) -> Self { - let inner = KeysManager::new(seed, starting_time_secs, starting_time_nanos); - Self { inner, wallet, logger } - } - - pub fn sign_message(&self, msg: &[u8]) -> Result { - message_signing::sign(msg, &self.inner.get_node_secret_key()) - .or(Err(Error::MessageSigningFailed)) - } - - pub fn get_node_secret_key(&self) -> SecretKey { - self.inner.get_node_secret_key() - } - - pub fn verify_signature(&self, msg: &[u8], sig: &str, pkey: &PublicKey) -> bool { - message_signing::verify(msg, sig, pkey) - } -} - -impl NodeSigner for WalletKeysManager -where - D: BatchDatabase, - B::Target: BroadcasterInterface, - E::Target: FeeEstimator, - L::Target: Logger, -{ - fn get_node_id(&self, recipient: Recipient) -> Result { - self.inner.get_node_id(recipient) - } - - fn ecdh( - &self, recipient: Recipient, other_key: &PublicKey, tweak: Option<&Scalar>, - ) -> Result { - self.inner.ecdh(recipient, other_key, tweak) - } - - fn get_inbound_payment_key_material(&self) -> KeyMaterial { - self.inner.get_inbound_payment_key_material() - } - - fn sign_invoice( - &self, hrp_bytes: &[u8], invoice_data: &[u5], recipient: Recipient, - ) -> Result { - self.inner.sign_invoice(hrp_bytes, invoice_data, recipient) - } - - fn sign_gossip_message(&self, msg: UnsignedGossipMessage<'_>) -> Result { - self.inner.sign_gossip_message(msg) - } - - fn sign_bolt12_invoice( - &self, invoice: &lightning::offers::invoice::UnsignedBolt12Invoice, - ) -> Result { - self.inner.sign_bolt12_invoice(invoice) - } - - fn sign_bolt12_invoice_request( - &self, invoice_request: &lightning::offers::invoice_request::UnsignedInvoiceRequest, - ) -> Result { - self.inner.sign_bolt12_invoice_request(invoice_request) - } -} - -impl OutputSpender for WalletKeysManager -where - D: BatchDatabase, - B::Target: BroadcasterInterface, - E::Target: FeeEstimator, - L::Target: Logger, -{ - /// See [`KeysManager::spend_spendable_outputs`] for documentation on this method. - fn spend_spendable_outputs( - &self, descriptors: &[&SpendableOutputDescriptor], outputs: Vec, - change_destination_script: ScriptBuf, feerate_sat_per_1000_weight: u32, - locktime: Option, secp_ctx: &Secp256k1, - ) -> Result { - self.inner.spend_spendable_outputs( - descriptors, - outputs, - change_destination_script, - feerate_sat_per_1000_weight, - locktime, - secp_ctx, - ) - } -} - -impl EntropySource for WalletKeysManager -where - D: BatchDatabase, - B::Target: BroadcasterInterface, - E::Target: FeeEstimator, - L::Target: Logger, -{ - fn get_secure_random_bytes(&self) -> [u8; 32] { - self.inner.get_secure_random_bytes() - } -} - -impl SignerProvider for WalletKeysManager -where - D: BatchDatabase, - B::Target: BroadcasterInterface, - E::Target: FeeEstimator, - L::Target: Logger, -{ - type EcdsaSigner = InMemorySigner; - - fn generate_channel_keys_id( - &self, inbound: bool, channel_value_satoshis: u64, user_channel_id: u128, - ) -> [u8; 32] { - self.inner.generate_channel_keys_id(inbound, channel_value_satoshis, user_channel_id) - } - - fn derive_channel_signer( - &self, channel_value_satoshis: u64, channel_keys_id: [u8; 32], - ) -> Self::EcdsaSigner { - self.inner.derive_channel_signer(channel_value_satoshis, channel_keys_id) - } - - fn read_chan_signer(&self, reader: &[u8]) -> Result { - self.inner.read_chan_signer(reader) - } - - fn get_destination_script(&self, _channel_keys_id: [u8; 32]) -> Result { - let address = self.wallet.get_new_address().map_err(|e| { - log_error!(self.logger, "Failed to retrieve new address from wallet: {}", e); - })?; - Ok(address.script_pubkey()) - } - - fn get_shutdown_scriptpubkey(&self) -> Result { - let address = self.wallet.get_new_address().map_err(|e| { - log_error!(self.logger, "Failed to retrieve new address from wallet: {}", e); - })?; - - match address.payload { - Payload::WitnessProgram(program) => ShutdownScript::new_witness_program(&program) - .map_err(|e| { - log_error!(self.logger, "Invalid shutdown script: {:?}", e); - }), - _ => { - log_error!( - self.logger, - "Tried to use a non-witness address. This must never happen." - ); - panic!("Tried to use a non-witness address. This must never happen."); - }, - } - } -} - -impl ChangeDestinationSource for WalletKeysManager -where - D: BatchDatabase, - B::Target: BroadcasterInterface, - E::Target: FeeEstimator, - L::Target: Logger, -{ - fn get_change_destination_script(&self) -> Result { - let address = self.wallet.get_new_internal_address().map_err(|e| { - log_error!(self.logger, "Failed to retrieve new address from wallet: {}", e); - })?; - Ok(address.script_pubkey()) - } -} 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, From fbc83fd63fad1385e388db9f70b9be7181137c04 Mon Sep 17 00:00:00 2001 From: esraa Date: Fri, 6 Dec 2024 10:30:43 +0200 Subject: [PATCH 6/6] f --- tests/common/mod.rs | 94 ++--- tests/integration_tests_payjoin.rs | 598 ++++++++++++++--------------- 2 files changed, 346 insertions(+), 346 deletions(-) diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 05146eac7..18e8e5633 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -154,40 +154,40 @@ 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; +// 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 = @@ -352,19 +352,19 @@ 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 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 index 88e83afb2..aadd56345 100644 --- a/tests/integration_tests_payjoin.rs +++ b/tests/integration_tests_payjoin.rs @@ -1,299 +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); -} +// 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); +// }