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 23, 2024
1 parent b7c4862 commit 21d1070
Show file tree
Hide file tree
Showing 10 changed files with 461 additions and 4 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
6 changes: 6 additions & 0 deletions bindings/ldk_node.udl
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,12 @@ enum NodeError {
"InsufficientFunds",
"LiquiditySourceUnavailable",
"LiquidityFeeTooHigh",
"PayjoinSenderUnavailable",
"PayjoinUriNetworkMismatch",
"PayjoinRequestMissingAmount",
"PayjoinRequestCreationFailed",
"PayjoinResponseProcessingFailed",
"PayjoinRequestTimeout",
};

dictionary NodeStatus {
Expand Down
41 changes: 39 additions & 2 deletions src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -94,6 +94,11 @@ struct LiquiditySourceConfig {
lsps2_service: Option<(SocketAddress, PublicKey, Option<String>)>,
}

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

impl Default for LiquiditySourceConfig {
fn default() -> Self {
Self { lsps2_service: None }
Expand Down Expand Up @@ -173,6 +178,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 +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,
}
}

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -522,7 +543,8 @@ impl ArcedNodeBuilder {
fn build_with_store_internal(
config: Arc<Config>, 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<FilesystemLogger>, kv_store: Arc<DynStore>,
) -> Result<Node, BuildError> {
// Initialize the on-chain wallet and chain access
Expand Down Expand Up @@ -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));
Expand All @@ -993,6 +1029,7 @@ fn build_with_store_internal(
channel_manager,
chain_monitor,
output_sweeper,
payjoin_sender,
peer_manager,
connection_manager,
keys_manager,
Expand Down
30 changes: 30 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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")
},
}
}
}
Expand Down
37 changes: 35 additions & 2 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 @@ -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};
Expand Down Expand Up @@ -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};

Expand Down Expand Up @@ -181,6 +183,7 @@ pub struct Node {
output_sweeper: Arc<Sweeper>,
peer_manager: Arc<PeerManager>,
connection_manager: Arc<ConnectionManager<Arc<FilesystemLogger>>>,
payjoin_sender: Option<Arc<PayjoinSender>>,
keys_manager: Arc<KeysManager>,
network_graph: Arc<NetworkGraph>,
gossip_source: Arc<GossipSource>,
Expand Down Expand Up @@ -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<PayjoinPayment, Error> {
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<PayjoinPayment, Error> {
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<ChannelDetails> {
self.channel_manager.list_channels().into_iter().map(|c| c.into()).collect()
Expand Down
Loading

0 comments on commit 21d1070

Please sign in to comment.