Skip to content

Commit

Permalink
Add ability to receive Payjoin tx
Browse files Browse the repository at this point in the history
Implements the payjoin receiver part as describe in BIP77.

This would allow the on chain wallet linked to LDK node to receive payjoin
transactions.

Receiving a payjoin transaction requires first to enroll with the
configured Payjoin directory and listening to our enrolled subdirectory
for upcoming request. When a request received, we validate it as
specified in BIP78, prepare our Payjoin proposal and send it back to the
payjoin sender via the subdirectory.
  • Loading branch information
jbesraa committed Jun 11, 2024
1 parent 1de2b65 commit f33294d
Show file tree
Hide file tree
Showing 10 changed files with 763 additions and 7 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +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.16.0", features = ["send", "v2"] }
payjoin = { version = "0.16.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 @@ -180,6 +180,9 @@ enum NodeError {
"PayjoinRequestCreationFailed",
"PayjoinResponseProcessingFailed",
"PayjoinRequestTimeout",
"PayjoinReceiverUnavailable",
"PayjoinReceiverRequestValidationFailed",
"PayjoinReceiverEnrollementFailed"
};

dictionary NodeStatus {
Expand Down
83 changes: 82 additions & 1 deletion 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_receiver::PayjoinReceiver;
use crate::payment::store::PaymentStore;
use crate::peer_store::PeerStore;
use crate::tx_broadcaster::TransactionBroadcaster;
Expand Down Expand Up @@ -99,6 +100,13 @@ struct PayjoinSenderConfig {
payjoin_relay: String,
}

#[derive(Debug, Clone)]
struct PayjoinReceiverConfig {
payjoin_directory: String,
payjoin_relay: String,
ohttp_keys: Option<String>,
}

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

impl NodeBuilder {
Expand All @@ -195,13 +204,15 @@ impl NodeBuilder {
let gossip_source_config = None;
let liquidity_source_config = None;
let payjoin_sender_config = None;
let payjoin_receiver_config = None;
Self {
config,
entropy_source_config,
chain_data_source_config,
gossip_source_config,
liquidity_source_config,
payjoin_sender_config,
payjoin_receiver_config,
}
}

Expand Down Expand Up @@ -262,6 +273,15 @@ impl NodeBuilder {
self
}

/// Configures the [`Node`] instance to enable receiving payjoin transactions.
pub fn set_payjoin_receiver_config(
&mut self, payjoin_directory: String, payjoin_relay: String, ohttp_keys: Option<String>,
) -> &mut Self {
self.payjoin_receiver_config =
Some(PayjoinReceiverConfig { payjoin_directory, payjoin_relay, ohttp_keys });
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 @@ -381,6 +401,7 @@ impl NodeBuilder {
self.gossip_source_config.as_ref(),
self.liquidity_source_config.as_ref(),
self.payjoin_sender_config.as_ref(),
self.payjoin_receiver_config.as_ref(),
seed_bytes,
logger,
vss_store,
Expand All @@ -403,6 +424,7 @@ impl NodeBuilder {
self.gossip_source_config.as_ref(),
self.liquidity_source_config.as_ref(),
self.payjoin_sender_config.as_ref(),
self.payjoin_receiver_config.as_ref(),
seed_bytes,
logger,
kv_store,
Expand Down Expand Up @@ -475,6 +497,17 @@ impl ArcedNodeBuilder {
self.inner.write().unwrap().set_payjoin_sender_config(payjoin_relay);
}

/// Configures the [`Node`] instance to enable receiving payjoin transactions.
pub fn set_payjoin_receiver_config(
&mut self, payjoin_directory: String, payjoin_relay: String, ohttp_keys: Option<String>,
) {
self.inner.write().unwrap().set_payjoin_receiver_config(
payjoin_directory,
payjoin_relay,
ohttp_keys,
);
}

/// 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 @@ -544,7 +577,8 @@ fn build_with_store_internal(
config: Arc<Config>, chain_data_source_config: Option<&ChainDataSourceConfig>,
gossip_source_config: Option<&GossipSourceConfig>,
liquidity_source_config: Option<&LiquiditySourceConfig>,
payjoin_sender_config: Option<&PayjoinSenderConfig>, seed_bytes: [u8; 64],
payjoin_sender_config: Option<&PayjoinSenderConfig>,
payjoin_receiver_config: Option<&PayjoinReceiverConfig>, 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 @@ -1010,6 +1044,52 @@ fn build_with_store_internal(
}
});

let payjoin_receiver = payjoin_receiver_config.as_ref().and_then(|prc| {
match (payjoin::Url::parse(&prc.payjoin_directory), payjoin::Url::parse(&prc.payjoin_relay))
{
(Ok(directory), Ok(relay)) => {
let ohttp_keys = match prc.ohttp_keys.clone() {
Some(keys) => {
let keys = match bitcoin::base64::decode(keys) {
Ok(keys) => keys,
Err(e) => {
log_info!(logger, "Failed to decode ohttp keys: the provided key is not a valid Base64 string {}", e);
return None;
},
};
match payjoin::OhttpKeys::decode(&keys) {
Ok(ohttp_keys) => Some(ohttp_keys),
Err(e) => {
log_info!(logger, "Failed to decode ohttp keys, make sure you provided a valid Ohttp Key as provided by the payjoin directory: {}", e);
return None;
},
}
},
None => None,
};
Some(Arc::new(PayjoinReceiver::new(
Arc::clone(&logger),
Arc::clone(&wallet),
directory,
relay,
ohttp_keys,
)))
},
(Ok(_), Err(e)) => {
log_info!(logger, "The provided payjoin relay url is invalid: {}", e);
None
},
(Err(e), Ok(_)) => {
log_info!(logger, "The provided payjoin directory url is invalid: {}", e);
None
},
_ => {
log_info!(logger, "The provided payjoin relay and payjoin directory urls are 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 @@ -1030,6 +1110,7 @@ fn build_with_store_internal(
chain_monitor,
output_sweeper,
payjoin_sender,
payjoin_receiver,
peer_manager,
connection_manager,
keys_manager,
Expand Down
15 changes: 15 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,12 @@ pub enum Error {
PayjoinResponseProcessingFailed,
/// Payjoin request timed out.
PayjoinRequestTimeout,
/// Failed to access payjoin receiver object.
PayjoinReceiverUnavailable,
/// Failed to enroll payjoin receiver.
PayjoinReceiverEnrollementFailed,
/// Failed to validate an incoming payjoin request.
PayjoinReceiverRequestValidationFailed,
}

impl fmt::Display for Error {
Expand Down Expand Up @@ -175,6 +181,15 @@ impl fmt::Display for Error {
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")
},
Self::PayjoinReceiverUnavailable => {
write!(f, "Failed to access payjoin receiver object. Make sure you have enabled Payjoin receiving support.")
},
Self::PayjoinReceiverRequestValidationFailed => {
write!(f, "Failed to validate an incoming payjoin request. Payjoin sender request didnt pass the payjoin validation steps.")
},
Self::PayjoinReceiverEnrollementFailed => {
write!(f, "Failed to enroll payjoin receiver. Make sure the configured Payjoin directory & Payjoin relay are available.")
},
}
}
}
Expand Down
47 changes: 45 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_receiver;
mod payjoin_sender;
pub mod payment;
mod peer_store;
Expand All @@ -110,6 +111,7 @@ pub use error::Error as NodeError;
use error::Error;

pub use event::Event;
use payjoin_receiver::PayjoinReceiver;
pub use types::ChannelConfig;

pub use io::utils::generate_entropy_mnemonic;
Expand Down Expand Up @@ -187,6 +189,7 @@ pub struct Node {
peer_manager: Arc<PeerManager>,
connection_manager: Arc<ConnectionManager<Arc<FilesystemLogger>>>,
payjoin_sender: Option<Arc<PayjoinSender>>,
payjoin_receiver: Option<Arc<PayjoinReceiver>>,
keys_manager: Arc<KeysManager>,
network_graph: Arc<NetworkGraph>,
gossip_source: Arc<GossipSource>,
Expand Down Expand Up @@ -626,6 +629,30 @@ impl Node {
}
});

// Check every 5 seconds if we have received a payjoin transaction to our enrolled
// subdirectory with the configured Payjoin directory.
if let Some(payjoin_receiver) = &self.payjoin_receiver {
let mut stop_payjoin_server = self.stop_sender.subscribe();
let payjoin_receiver = Arc::clone(&payjoin_receiver);
let payjoin_check_interval = 5;
runtime.spawn(async move {
let mut payjoin_interval =
tokio::time::interval(Duration::from_secs(payjoin_check_interval));
payjoin_interval.reset();
payjoin_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
loop {
tokio::select! {
_ = stop_payjoin_server.changed() => {
return;
}
_ = payjoin_interval.tick() => {
let _ = payjoin_receiver.process_payjoin_request().await;
}
}
}
});
}

let event_handler = Arc::new(EventHandler::new(
Arc::clone(&self.event_queue),
Arc::clone(&self.wallet),
Expand Down Expand Up @@ -929,36 +956,52 @@ impl Node {

/// Returns a payment handler allowing to send payjoin payments.
///
/// In order to utilize the Payjoin functionality, it's necessary
/// In order to utilize the Payjoin sending functionality, it's necessary
/// to configure your node using [`set_payjoin_sender_config`] to
/// set the [`PayjoinSenderConfig`].
///
/// In order to utilize the Payjoin receiving functionality, it's necessary
/// to configure your node using [`set_payjoin_receiver_config`] to
/// set the [`PayjoinReceiverConfig`].
///
/// [`PayjoinSenderConfig`]: [`crate::builder::PayjoinSenderConfig`]
/// [`set_payjoin_sender_config`]: [`crate::builder::NodeBuilder::set_payjoin_sender_config`]
/// [`PayjoinReceiverConfig`]: [`crate::builder::PayjoinReceiverConfig`]
/// [`set_payjoin_receiver_config`]: [`crate::builder::NodeBuilder::set_payjoin_receiver_config`]
#[cfg(not(feature = "uniffi"))]
pub fn payjoin_payment(&self) -> PayjoinPayment {
let payjoin_sender = self.payjoin_sender.as_ref();
let payjoin_receiver = self.payjoin_receiver.as_ref();
PayjoinPayment::new(
Arc::clone(&self.runtime),
payjoin_sender.map(Arc::clone),
payjoin_receiver.map(Arc::clone),
Arc::clone(&self.config),
)
}

/// Returns a payment handler allowing to send payjoin payments.
///
/// In order to utilize the Payjoin functionality, it's necessary
/// In order to utilize the Payjoin sending functionality, it's necessary
/// to configure your node using [`set_payjoin_sender_config`] to
/// set the [`PayjoinSenderConfig`].
///
/// In order to utilize the Payjoin receiving functionality, it's necessary
/// to configure your node using [`set_payjoin_receiver_config`] to
/// set the [`PayjoinReceiverConfig`].
///
/// [`PayjoinSenderConfig`]: [`crate::builder::PayjoinSenderConfig`]
/// [`set_payjoin_sender_config`]: [`crate::builder::NodeBuilder::set_payjoin_sender_config`]
/// [`PayjoinReceiverConfig`]: [`crate::builder::PayjoinReceiverConfig`]
/// [`set_payjoin_receiver_config`]: [`crate::builder::NodeBuilder::set_payjoin_receiver_config`]
#[cfg(feature = "uniffi")]
pub fn payjoin_payment(&self) -> PayjoinPayment {
let payjoin_sender = self.payjoin_sender.as_ref();
let payjoin_receiver = self.payjoin_receiver.as_ref();
PayjoinPayment::new(
Arc::clone(&self.runtime),
payjoin_sender.map(Arc::clone),
payjoin_receiver.map(Arc::clone),
Arc::clone(&self.config),
)
}
Expand Down
Loading

0 comments on commit f33294d

Please sign in to comment.