Skip to content

Commit

Permalink
Add ability to send payjoin transactions.
Browse files Browse the repository at this point in the history
Implements the payjoin sender part as describe in BIP77.

This would allow the on chain wallet linked to LDK node to send payjoin
transactions.
  • Loading branch information
jbesraa committed May 22, 2024
1 parent b7c4862 commit 14e9c40
Show file tree
Hide file tree
Showing 7 changed files with 337 additions and 0 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
3 changes: 3 additions & 0 deletions bindings/ldk_node.udl
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,9 @@ enum NodeError {
"InsufficientFunds",
"LiquiditySourceUnavailable",
"LiquidityFeeTooHigh",
"PayjoinRequestAmountMissing",
"ConstructingPayjoinRequestFailed",
"PayjoinSenderUnavailable"
};

dictionary NodeStatus {
Expand Down
34 changes: 34 additions & 0 deletions src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use crate::io::sqlite_store::SqliteStore;
use crate::liquidity::LiquiditySource;
use crate::logger::{log_error, log_info, FilesystemLogger, Logger};
use crate::message_handler::NodeCustomMessageHandler;
use crate::payjoin_sender::PayjoinSender;
use crate::payment::store::PaymentStore;
use crate::peer_store::PeerStore;
use crate::tx_broadcaster::TransactionBroadcaster;
Expand Down Expand Up @@ -94,6 +95,11 @@ struct LiquiditySourceConfig {
lsps2_service: Option<(SocketAddress, PublicKey, Option<String>)>,
}

#[derive(Debug, Clone)]
struct PayjoinSenderConfig {
payjoin_relay: payjoin::Url,
}

impl Default for LiquiditySourceConfig {
fn default() -> Self {
Self { lsps2_service: None }
Expand Down Expand Up @@ -173,6 +179,7 @@ pub struct NodeBuilder {
chain_data_source_config: Option<ChainDataSourceConfig>,
gossip_source_config: Option<GossipSourceConfig>,
liquidity_source_config: Option<LiquiditySourceConfig>,
payjoin_sender_config: Option<PayjoinSenderConfig>,
}

impl NodeBuilder {
Expand All @@ -188,12 +195,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,
}
}

Expand Down Expand Up @@ -248,6 +257,12 @@ impl NodeBuilder {
self
}

/// Configures the [`Node`] instance to enable sending payjoin transactions.
pub fn set_payjoin_sender_config(&mut self, payjoin_relay: payjoin::Url) -> &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.
Expand Down Expand Up @@ -369,6 +384,7 @@ impl NodeBuilder {
seed_bytes,
logger,
vss_store,
self.payjoin_sender_config.as_ref(),
)
}

Expand All @@ -390,6 +406,7 @@ impl NodeBuilder {
seed_bytes,
logger,
kv_store,
self.payjoin_sender_config.as_ref(),
)
}
}
Expand Down Expand Up @@ -454,6 +471,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: payjoin::Url) {
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) {
Expand Down Expand Up @@ -524,6 +546,7 @@ fn build_with_store_internal(
gossip_source_config: Option<&GossipSourceConfig>,
liquidity_source_config: Option<&LiquiditySourceConfig>, seed_bytes: [u8; 64],
logger: Arc<FilesystemLogger>, kv_store: Arc<DynStore>,
payjoin_sender_config: Option<&PayjoinSenderConfig>,
) -> Result<Node, BuildError> {
// Initialize the on-chain wallet and chain access
let xprv = bitcoin::bip32::ExtendedPrivKey::new_master(config.network.into(), &seed_bytes)
Expand Down Expand Up @@ -973,6 +996,16 @@ fn build_with_store_internal(
};

let (stop_sender, _) = tokio::sync::watch::channel(());
let payjoin_sender = if let Some(payjoin_sender_config) = payjoin_sender_config {
let payjoin_sender = PayjoinSender::new(
Arc::clone(&logger),
Arc::clone(&wallet),
&payjoin_sender_config.payjoin_relay,
);
Some(Arc::new(payjoin_sender))
} else {
None
};

let is_listening = Arc::new(AtomicBool::new(false));
let latest_wallet_sync_timestamp = Arc::new(RwLock::new(None));
Expand All @@ -993,6 +1026,7 @@ fn build_with_store_internal(
channel_manager,
chain_monitor,
output_sweeper,
payjoin_sender,
peer_manager,
connection_manager,
keys_manager,
Expand Down
11 changes: 11 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,12 @@ pub enum Error {
LiquiditySourceUnavailable,
/// The given operation failed due to the LSP's required opening fee being too high.
LiquidityFeeTooHigh,
/// Amount is not prvoided and neither defined in the URI.
PayjoinRequestAmountMissing,
/// Failed to build a payjoin request.
ConstructingPayjoinRequestFailed,
/// Failed to access payjoin sender object.
PayjoinSenderUnavailable,
}

impl fmt::Display for Error {
Expand Down Expand Up @@ -121,7 +127,12 @@ 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::ConstructingPayjoinRequestFailed => write!(f, "Failed construct a payjoin request.
Make sure the provided URI is valid and the configured payjoin relay as available."),
Self::PayjoinRequestAmountMissing => {
write!(f, "Amount is not provided and neither defined in the URI.")
},
Self::PayjoinSenderUnavailable => write!(f, "Failed to access payjoin sender object. Make sure you have configured the node to act as payjoin sender."),
}
}
}
Expand Down
58 changes: 58 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -99,6 +100,7 @@ mod wallet;

pub use bip39;
pub use bitcoin;
use bitcoin::address::NetworkChecked;
pub use lightning;
pub use lightning_invoice;

Expand All @@ -108,6 +110,7 @@ pub use error::Error as NodeError;
use error::Error;

pub use event::Event;
use payjoin_sender::PayjoinSender;
pub use types::ChannelConfig;

pub use io::utils::generate_entropy_mnemonic;
Expand Down Expand Up @@ -181,6 +184,7 @@ pub struct Node {
output_sweeper: Arc<Sweeper>,
peer_manager: Arc<PeerManager>,
connection_manager: Arc<ConnectionManager<Arc<FilesystemLogger>>>,
payjoin_sender: Option<Arc<PayjoinSender<Arc<FilesystemLogger>>>>,
keys_manager: Arc<KeysManager>,
network_graph: Arc<NetworkGraph>,
gossip_source: Arc<GossipSource>,
Expand Down Expand Up @@ -697,6 +701,60 @@ impl Node {
Ok(())
}

/// 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`
/// and `amount` 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.
///
/// After constructing the `Original PSBT`, the method will extract the payjoin request data
/// from the `Original PSBT` and `payjoin_uri` utilising the `payjoin` crate.
///
/// Then we start a background process that will run for 1 hour, polling the payjoin endpoint
/// every 10 seconds. If an `OK` (ie status code == 200) is received, polling will stop and we
/// will try to process the response from the payjoin receiver. If the response(or `Payjoin
/// Proposal`) is valid, we will finalise the transaction and broadcast it to the network.
///
/// Notice that the `Txid` returned from this method is the `Original PSBT` transaction id, but
/// the `Payjoin Proposal` transaction id could be different if the receiver changed the
/// transaction.
pub fn send_payjoin_transaction(
&self, payjoin_uri: payjoin::Uri<'static, NetworkChecked>, amount: Option<bitcoin::Amount>,
) -> Result<Option<bitcoin::Txid>, Error> {
let rt_lock = self.runtime.read().unwrap();
if rt_lock.is_none() {
return Err(Error::NotRunning);
}
let payjoin_sender =
Arc::clone(self.payjoin_sender.as_ref().ok_or(Error::PayjoinSenderUnavailable)?);
let original_psbt = payjoin_sender.create_payjoin_request(payjoin_uri.clone(), amount)?;
let txid = original_psbt.clone().unsigned_tx.txid();
let (request, context) =
payjoin_sender.extract_request_data(payjoin_uri, original_psbt.clone())?;

let time = std::time::Instant::now();
let runtime = rt_lock.as_ref().unwrap();
runtime.spawn(async move {
let response = payjoin_sender.poll(&request, time).await;
if let Some(response) = response {
let psbt = context.process_response(&mut response.as_slice());
match psbt {
Ok(Some(psbt)) => {
let finalized =
payjoin_sender.finalise_payjoin_tx(psbt, original_psbt.clone());
if let Ok(txid) = finalized {
let txid: bitcoin::Txid = txid.into();
return Some(txid);
}
},
_ => return None,
}
}
None
});
Ok(Some(txid))
}

/// Disconnects all peers, stops all running background tasks, and shuts down [`Node`].
///
/// After this returns most API methods will return [`Error::NotRunning`].
Expand Down
Loading

0 comments on commit 14e9c40

Please sign in to comment.