From 21d1070efa0c1fc2b6616ab5289aaae88e8dee58 Mon Sep 17 00:00:00 2001 From: jbesraa Date: Mon, 20 May 2024 21:31:13 +0300 Subject: [PATCH] Add ability to send payjoin transactions. Implements the payjoin sender part as describe in BIP77. This would allow the on chain wallet linked to LDK node to send payjoin transactions. --- Cargo.toml | 1 + bindings/ldk_node.udl | 6 + src/builder.rs | 41 ++++++- src/error.rs | 30 +++++ src/lib.rs | 37 +++++- src/payjoin_sender.rs | 250 +++++++++++++++++++++++++++++++++++++++++ src/payment/mod.rs | 2 + src/payment/payjoin.rs | 62 ++++++++++ src/types.rs | 2 + src/wallet.rs | 34 ++++++ 10 files changed, 461 insertions(+), 4 deletions(-) create mode 100644 src/payjoin_sender.rs create mode 100644 src/payment/payjoin.rs diff --git a/Cargo.toml b/Cargo.toml index 4c4422461..934b5d984 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -68,6 +68,7 @@ tokio = { version = "1", default-features = false, features = [ "rt-multi-thread esplora-client = { version = "0.6", default-features = false } libc = "0.2" uniffi = { version = "0.26.0", features = ["build"], optional = true } +payjoin = { version = "0.15.0", features = ["send", "receive", "v2"] } [target.'cfg(vss)'.dependencies] vss-client = "0.2" diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index 58fab0d52..6ba4b6060 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -151,6 +151,12 @@ enum NodeError { "InsufficientFunds", "LiquiditySourceUnavailable", "LiquidityFeeTooHigh", + "PayjoinSenderUnavailable", + "PayjoinUriNetworkMismatch", + "PayjoinRequestMissingAmount", + "PayjoinRequestCreationFailed", + "PayjoinResponseProcessingFailed", + "PayjoinRequestTimeout", }; dictionary NodeStatus { diff --git a/src/builder.rs b/src/builder.rs index 6d3db420f..f318ea4bd 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -16,7 +16,7 @@ use crate::peer_store::PeerStore; use crate::tx_broadcaster::TransactionBroadcaster; use crate::types::{ ChainMonitor, ChannelManager, DynStore, GossipSync, KeysManager, MessageRouter, NetworkGraph, - OnionMessenger, PeerManager, + OnionMessenger, PayjoinSender, PeerManager, }; use crate::wallet::Wallet; use crate::{LogLevel, Node}; @@ -94,6 +94,11 @@ struct LiquiditySourceConfig { lsps2_service: Option<(SocketAddress, PublicKey, Option)>, } +#[derive(Debug, Clone)] +struct PayjoinSenderConfig { + payjoin_relay: String, +} + impl Default for LiquiditySourceConfig { fn default() -> Self { Self { lsps2_service: None } @@ -173,6 +178,7 @@ pub struct NodeBuilder { chain_data_source_config: Option, gossip_source_config: Option, liquidity_source_config: Option, + payjoin_sender_config: Option, } impl NodeBuilder { @@ -188,12 +194,14 @@ impl NodeBuilder { let chain_data_source_config = None; let gossip_source_config = None; let liquidity_source_config = None; + let payjoin_sender_config = None; Self { config, entropy_source_config, chain_data_source_config, gossip_source_config, liquidity_source_config, + payjoin_sender_config, } } @@ -248,6 +256,12 @@ impl NodeBuilder { self } + /// Configures the [`Node`] instance to enable sending payjoin transactions. + pub fn set_payjoin_sender_config(&mut self, payjoin_relay: String) -> &mut Self { + self.payjoin_sender_config = Some(PayjoinSenderConfig { payjoin_relay }); + 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. @@ -366,6 +380,7 @@ impl NodeBuilder { self.chain_data_source_config.as_ref(), self.gossip_source_config.as_ref(), self.liquidity_source_config.as_ref(), + self.payjoin_sender_config.as_ref(), seed_bytes, logger, vss_store, @@ -387,6 +402,7 @@ impl NodeBuilder { self.chain_data_source_config.as_ref(), self.gossip_source_config.as_ref(), self.liquidity_source_config.as_ref(), + self.payjoin_sender_config.as_ref(), seed_bytes, logger, kv_store, @@ -454,6 +470,11 @@ impl ArcedNodeBuilder { self.inner.write().unwrap().set_gossip_source_p2p(); } + /// Configures the [`Node`] instance to enable sending payjoin transactions. + pub fn set_payjoin_sender_config(&self, payjoin_relay: String) { + self.inner.write().unwrap().set_payjoin_sender_config(payjoin_relay); + } + /// 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) { @@ -522,7 +543,8 @@ 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], + liquidity_source_config: Option<&LiquiditySourceConfig>, + payjoin_sender_config: Option<&PayjoinSenderConfig>, seed_bytes: [u8; 64], logger: Arc, kv_store: Arc, ) -> Result { // Initialize the on-chain wallet and chain access @@ -974,6 +996,20 @@ fn build_with_store_internal( let (stop_sender, _) = tokio::sync::watch::channel(()); + let payjoin_sender = payjoin_sender_config.as_ref().and_then(|psc| { + if let Ok(payjoin_relay) = payjoin::Url::parse(&psc.payjoin_relay) { + Some(Arc::new(PayjoinSender::new( + Arc::clone(&logger), + Arc::clone(&wallet), + Arc::clone(&tx_broadcaster), + payjoin_relay, + ))) + } else { + log_info!(logger, "The provided payjoin relay URL is invalid."); + None + } + }); + 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)); @@ -993,6 +1029,7 @@ fn build_with_store_internal( channel_manager, chain_monitor, output_sweeper, + payjoin_sender, peer_manager, connection_manager, keys_manager, diff --git a/src/error.rs b/src/error.rs index 5acc75af8..7c591f374 100644 --- a/src/error.rs +++ b/src/error.rs @@ -71,6 +71,18 @@ pub enum Error { LiquiditySourceUnavailable, /// The given operation failed due to the LSP's required opening fee being too high. LiquidityFeeTooHigh, + /// Failed to access payjoin sender object. + PayjoinSenderUnavailable, + /// Payjoin URI network mismatch. + PayjoinUriNetworkMismatch, + /// Amount is neither user-provided nor defined in the URI. + PayjoinRequestMissingAmount, + /// Failed to build a payjoin request. + PayjoinRequestCreationFailed, + /// Payjoin response processing failed. + PayjoinResponseProcessingFailed, + /// Payjoin request timed out. + PayjoinRequestTimeout, } impl fmt::Display for Error { @@ -122,6 +134,24 @@ impl fmt::Display for Error { Self::LiquidityFeeTooHigh => { write!(f, "The given operation failed due to the LSP's required opening fee being too high.") }, + Self::PayjoinSenderUnavailable => { + write!(f, "Failed to access payjoin sender object. Make sure you have enabled Payjoin sending support.") + }, + Self::PayjoinRequestMissingAmount => { + write!(f, "Amount is neither user-provided nor defined in the URI.") + }, + Self::PayjoinRequestCreationFailed => { + write!(f, "Failed construct a payjoin request. Make sure the provided URI is valid and the configured Payjoin relay is available.") + }, + Self::PayjoinUriNetworkMismatch => { + write!(f, "The Provided Payjoin URI does not match the node network.") + }, + Self::PayjoinResponseProcessingFailed => { + write!(f, "Payjoin receiver responded to our request with an invalid response that was ignored. Notice they can still broadcast the original PSBT we shared with them") + }, + Self::PayjoinRequestTimeout => { + write!(f, "Payjoin receiver did not respond to our request within the timeout period. Notice they can still broadcast the original PSBT we shared with them") + }, } } } diff --git a/src/lib.rs b/src/lib.rs index 3d619cebb..12d21b172 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -88,6 +88,7 @@ pub mod io; mod liquidity; mod logger; mod message_handler; +mod payjoin_sender; pub mod payment; mod peer_store; mod sweep; @@ -101,6 +102,7 @@ pub use bip39; pub use bitcoin; pub use lightning; pub use lightning_invoice; +pub use payjoin::Uri; pub use balance::{BalanceDetails, LightningBalance, PendingSweepBalance}; pub use config::{default_config, Config}; @@ -130,11 +132,11 @@ use event::{EventHandler, EventQueue}; use gossip::GossipSource; use liquidity::LiquiditySource; use payment::store::PaymentStore; -use payment::{Bolt11Payment, OnchainPayment, PaymentDetails, SpontaneousPayment}; +use payment::{Bolt11Payment, OnchainPayment, PayjoinPayment, PaymentDetails, SpontaneousPayment}; use peer_store::{PeerInfo, PeerStore}; use types::{ Broadcaster, ChainMonitor, ChannelManager, DynStore, FeeEstimator, KeysManager, NetworkGraph, - PeerManager, Router, Scorer, Sweeper, Wallet, + PayjoinSender, PeerManager, Router, Scorer, Sweeper, Wallet, }; pub use types::{ChannelDetails, PeerDetails, UserChannelId}; @@ -181,6 +183,7 @@ pub struct Node { output_sweeper: Arc, peer_manager: Arc, connection_manager: Arc>>, + payjoin_sender: Option>, keys_manager: Arc, network_graph: Arc, gossip_source: Arc, @@ -891,6 +894,36 @@ impl Node { )) } + /// Returns a payment handler allowing to send and receive on-chain payments. + #[cfg(not(feature = "uniffi"))] + pub fn payjoin_payment(&self) -> Result { + self.payjoin_sender.as_ref().map_or_else( + || Err(Error::PayjoinSenderUnavailable), + |ps| { + Ok(PayjoinPayment::new( + Arc::clone(&self.runtime), + ps.clone(), + Arc::clone(&self.config), + )) + }, + ) + } + + /// Returns a payment handler allowing to send and receive on-chain payments. + #[cfg(feature = "uniffi")] + pub fn payjoin_payment(&self) -> Result { + self.payjoin_sender.as_ref().map_or_else( + || Err(Error::PayjoinSenderUnavailable), + |ps| { + Ok(PayjoinPayment::new( + Arc::clone(&self.runtime), + ps.clone(), + Arc::clone(&self.config), + )) + }, + ) + } + /// Retrieve a list of known channels. pub fn list_channels(&self) -> Vec { self.channel_manager.list_channels().into_iter().map(|c| c.into()).collect() diff --git a/src/payjoin_sender.rs b/src/payjoin_sender.rs new file mode 100644 index 000000000..ffbd12a60 --- /dev/null +++ b/src/payjoin_sender.rs @@ -0,0 +1,250 @@ +/// An implementation of payjoin v2 sender as described in BIP-77. +use bitcoin::address::NetworkChecked; +use bitcoin::psbt::{Input, PartiallySignedTransaction, Psbt}; +use bitcoin::Txid; +use lightning::chain::chaininterface::BroadcasterInterface; +use lightning::util::logger::Logger; +use lightning::{log_error, log_info}; +use payjoin::send::ContextV2; +use payjoin::Url; +use reqwest::header::{HeaderMap, HeaderValue}; +use reqwest::StatusCode; +use std::ops::Deref; +use std::sync::Arc; + +use crate::error::Error; +use crate::logger::FilesystemLogger; +use crate::types::Wallet; + +pub(crate) struct PayjoinSender +where + B::Target: BroadcasterInterface, +{ + logger: Arc, + wallet: Arc, + payjoin_relay: Url, + broadcaster: B, +} + +impl PayjoinSender +where + B::Target: BroadcasterInterface, +{ + pub(crate) fn new( + logger: Arc, wallet: Arc, broadcaster: B, payjoin_relay: Url, + ) -> Self { + Self { logger, wallet, broadcaster, payjoin_relay } + } + + // This method can be used to send a payjoin transaction as defined in BIP77. + // + // The method will construct an `Original PSBT` from the data provided in the `payjoin_uri`, + // `amount` and `fee_rate` parameters. The amount must be set either in the `payjoin_uri` or in + // the `amount` parameter. If both are set, the paramter amount will be used. If the + // `fee_rate` is not set, a default(what should be this default?) fee rate will be used. + // + // After constructing the `Original PSBT`, the method will extract the Payjoin request data + // from the `Original PSBT` and `payjoin_uri` utilising the `payjoin` crate. + // + // .. how to deal with async? + pub(crate) async fn send( + &self, payjoin_uri: payjoin::Uri<'static, bitcoin::address::NetworkChecked>, + amount: Option, fee_rate: Option, + ) -> Result { + // create payjoin transaction and request data + let (mut original_psbt, request, context) = + self.create_payjoin_request(payjoin_uri, amount, fee_rate)?; + // send payjoin transaction, once an OK 200 response is received, the `poll` + // function will validate the response body type is valid and return the response. + // it can run for a maximum of 1 hour. + let response = self.poll(&request, tokio::time::Instant::now()).await; + + // once we get the response back, we need to use the context created previously by `create_payjoin_request` + // to process the response. `context.process_response` function would return `Ok(None)` if the + // response is 204 ACCEPTED, so we should not hit this path. + if let Some(response) = response { + let psbt = context.process_response(&mut response.as_slice()); + match psbt { + Ok(Some(psbt)) => { + let txid = self.finalise_payjoin_tx(psbt, &mut original_psbt)?; + return Ok(txid); + }, + Ok(None) => return Err(Error::PayjoinResponseProcessingFailed), + Err(e) => { + log_error!( + self.logger, + "Payjoin Sender: send: Error processing payjoin response: {}", + e + ); + return Err(Error::PayjoinResponseProcessingFailed); + }, + } + } else { + // we timed out, 1 hour passed + return Err(Error::PayjoinRequestTimeout); + } + } + + // This method creates an `Original PSBT`, a fully signed and broadcastable transaction that + // can be sent to a Payjoin receiver. + // + // The method will construct a transction based on the `payjoin_uri`, `amount` and `fee_rate` + // parameters, that is valid and can be sent to the Payjoin receiver. + pub(crate) fn create_payjoin_request( + &self, payjoin_uri: payjoin::Uri<'static, NetworkChecked>, amount: Option, + fee_rate: Option, + ) -> Result<(Psbt, payjoin::send::Request, ContextV2), Error> { + let amount_to_send = match (amount, payjoin_uri.amount) { + (Some(amount), _) => amount, + (None, Some(amount)) => amount, + (None, None) => return Err(Error::PayjoinRequestMissingAmount), + }; + let receiver_address = payjoin_uri.address.clone().script_pubkey(); + let original_psbt = self.wallet.build_payjoin_transaction( + receiver_address, + amount_to_send.to_sat(), + fee_rate, + )?; + + let (request_data, request_context) = + self.extract_request_data(payjoin_uri, &original_psbt)?; + Ok((original_psbt, request_data, request_context)) + } + + // This method uses the `payjoin` crate to generate the http request data that is needed to be + // included in the Payjoin request, and the `ContextV2` that is needed to process the Payjoin + // response that is received from the Payjoin receiver(ie the receiver's response to our + // request). + fn extract_request_data( + &self, payjoin_uri: payjoin::Uri<'static, NetworkChecked>, original_psbt: &Psbt, + ) -> Result<(payjoin::send::Request, ContextV2), Error> { + let mut sender_context = + payjoin::send::RequestBuilder::from_psbt_and_uri(original_psbt.clone(), payjoin_uri) + .and_then(|b| b.build_non_incentivizing()) + .map_err(|e| { + log_error!( + self.logger, + "Payjoin Sender: send: Error building payjoin request {}", + e + ); + Error::PayjoinRequestCreationFailed + })?; + let (sender_request, sender_ctx) = + sender_context.extract_v2(self.payjoin_relay.clone()).map_err(|e| { + log_error!( + self.logger, + "Payjoin Sender: send: Error extracting payjoin request: {}", + e + ); + Error::PayjoinRequestCreationFailed + })?; + Ok((sender_request, sender_ctx)) + } + + // This method will try to fetch the Payjoin receiver response for a maximum of one hour. + async fn poll( + &self, request: &payjoin::send::Request, time: tokio::time::Instant, + ) -> Option> { + use tokio::time::Duration; + let duration = Duration::from_secs(3600); + let sleep = || tokio::time::sleep(Duration::from_secs(10)); + loop { + if time.elapsed() > duration { + log_info!(self.logger, "Payjoin Sender: Polling timed out, no response received from the Payjoin receiver"); + return None; + } + let client = reqwest::Client::new(); + + let response = match client + .post(request.url.clone()) + .body(request.body.clone()) + .headers(ohttp_req_header()) + .send() + .await + { + Ok(response) => response, + Err(e) => { + log_info!(self.logger, "Payjoin Sender: Error polling request: {}", e); + sleep().await; + continue; + }, + }; + let response = match response.error_for_status() { + Ok(response) => response, + Err(e) => { + log_info!(self.logger, "Payjoin Sender: Status Error polling request: {}", e); + sleep().await; + continue; + }, + }; + + if response.status() == StatusCode::OK { + let response = match response.bytes().await { + Ok(response) => response.to_vec(), + Err(e) => { + log_info!( + self.logger, + "Payjoin Sender: Error reading polling response: {}", + e + ); + sleep().await; + continue; + }, + }; + if response.is_empty() { + log_info!(self.logger, "Payjoin Sender: Got empty response while polling"); + sleep().await; + continue; + } + return Some(response); + } else { + log_info!( + self.logger, + "Payjoin Sender: Error sending request, got status code + {}", + response.status() + ); + sleep().await; + continue; + } + } + } + + // This method will finalise the transaction and broadcast it to the network. + fn finalise_payjoin_tx( + &self, mut payjoin_proposal_psbt: Psbt, original_psbt: &mut Psbt, + ) -> Result { + // We need to reintroduce utxo from original psbt, otherwise BDK will not sign and finalise + // the transaction. + fn input_pairs( + psbt: &mut PartiallySignedTransaction, + ) -> Box + '_> { + Box::new(psbt.unsigned_tx.input.iter().zip(&mut psbt.inputs)) + } + + // get original inputs from original psbt clone + let mut original_inputs = input_pairs(original_psbt).peekable(); + for (proposed_txin, proposed_psbtin) in input_pairs(&mut payjoin_proposal_psbt) { + 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 is_signed = self.wallet.sign_transaction(&mut payjoin_proposal_psbt)?; + assert!(is_signed, "Payjoin Sender: Failed to sign payjoin proposal"); + let tx = payjoin_proposal_psbt.extract_tx(); + self.broadcaster.broadcast_transactions(&[&tx]); + let txid = tx.txid(); + Ok(txid) + } +} + +fn ohttp_req_header() -> HeaderMap { + let mut headers = HeaderMap::new(); + headers.insert(reqwest::header::CONTENT_TYPE, HeaderValue::from_static("message/ohttp-req")); + headers +} diff --git a/src/payment/mod.rs b/src/payment/mod.rs index 3649f1fcc..4af461caf 100644 --- a/src/payment/mod.rs +++ b/src/payment/mod.rs @@ -2,9 +2,11 @@ mod bolt11; mod onchain; +mod payjoin; mod spontaneous; pub(crate) mod store; +pub use self::payjoin::PayjoinPayment; pub use bolt11::Bolt11Payment; pub use onchain::OnchainPayment; pub use spontaneous::SpontaneousPayment; diff --git a/src/payment/payjoin.rs b/src/payment/payjoin.rs new file mode 100644 index 000000000..3583f4bc6 --- /dev/null +++ b/src/payment/payjoin.rs @@ -0,0 +1,62 @@ +//! Holds a payment handler allowing to send Payjoin payments. + +use crate::types::PayjoinSender; +use crate::{error::Error, Config}; + +use std::sync::{Arc, RwLock}; + +/// A payment handler allowing to send Payjoin payments. +/// +/// Should be retrieved by calling [`Node::payjoin_payment`]. +/// +/// [`Node::payjoin_payment`]: crate::Node::payjoin_payment +pub struct PayjoinPayment { + runtime: Arc>>, + sender: Arc, + config: Arc, +} + +impl PayjoinPayment { + pub(crate) fn new( + runtime: Arc>>, sender: Arc, + config: Arc, + ) -> Self { + Self { runtime, sender, config } + } + + /// Send an on chain Payjoin transaction as specified in [`BIP77`]. + /// + /// ... + /// + /// [`BIP77`]: https://github.com/bitcoin/bips/blob/d7ffad81e605e958dcf7c2ae1f4c797a8631f146/bip-0077.mediawiki + pub async fn send( + &self, payjoin_uri: payjoin::Uri<'static, bitcoin::address::NetworkUnchecked>, + amount: Option, fee_rate: Option, + ) -> Result { + let rt_lock = self.runtime.read().unwrap(); + if rt_lock.is_none() { + return Err(Error::NotRunning); + } + + let payjoin_uri = match payjoin_uri.require_network(self.config.network) { + Ok(uri) => uri, + Err(_) => return Err(Error::PayjoinUriNetworkMismatch), + }; + let runtime = rt_lock.as_ref().unwrap(); + let sender = Arc::clone(&self.sender); + runtime.handle().spawn(async move { + // The following `send` function runs for a maximum of 1 hour, then it times out. if + // during this period the receiver responds, we will fetch the response, finalise and + // broadcast it. if the receiver responds after the timeout, we wont fetch the response + // and they can deicde to just broadcast the original psbt shared with them + let _res = sender.send(payjoin_uri, amount, fee_rate).await; + // we should probably emit an event here to let the user know the the final state + // of the transaction. + // frame or not. + // + }); + // this just says that the request was sent, not that the transaction was finalised or + // broadcasted. + Ok(true) + } +} diff --git a/src/types.rs b/src/types.rs index 68ed36361..f0c93c383 100644 --- a/src/types.rs +++ b/src/types.rs @@ -72,6 +72,8 @@ pub(crate) type Wallet = crate::wallet::Wallet< Arc, >; +pub(crate) type PayjoinSender = crate::payjoin_sender::PayjoinSender>; + pub(crate) type KeysManager = crate::wallet::WalletKeysManager< bdk::database::SqliteDatabase, Arc, diff --git a/src/wallet.rs b/src/wallet.rs index 674cb6786..43b53eb81 100644 --- a/src/wallet.rs +++ b/src/wallet.rs @@ -2,6 +2,7 @@ use crate::logger::{log_error, log_info, log_trace, Logger}; use crate::Error; +use bitcoin::psbt::Psbt; use lightning::chain::chaininterface::{BroadcasterInterface, ConfirmationTarget, FeeEstimator}; use lightning::ln::msgs::{DecodeError, UnsignedGossipMessage}; @@ -111,6 +112,39 @@ where res } + pub(crate) fn build_payjoin_transaction( + &self, output_script: ScriptBuf, value_sats: u64, fee_rate: Option, + ) -> Result { + //Fixme need a better default + let fee_rate = fee_rate.unwrap_or(FeeRate::from_sat_per_kwu(1000 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).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 PSBT: {}", err); + return Err(err.into()); + }, + }; + + locked_wallet.sign(&mut psbt, SignOptions::default())?; + + Ok(psbt) + } + + pub(crate) fn sign_transaction(&self, psbt: &mut Psbt) -> Result { + let wallet = self.inner.lock().unwrap(); + let is_signed = wallet.sign(psbt, SignOptions::default())?; + Ok(is_signed) + } + pub(crate) fn create_funding_transaction( &self, output_script: ScriptBuf, value_sats: u64, confirmation_target: ConfirmationTarget, locktime: LockTime,