Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add ability to receive payjoin transactions
Browse files Browse the repository at this point in the history
Allows the node wallet to receive payjoin transactions as specified in
BIP78.
jbesraa committed Jun 5, 2024
1 parent 0f110b7 commit 035ad65
Showing 11 changed files with 706 additions and 17 deletions.
3 changes: 3 additions & 0 deletions bindings/ldk_node.udl
Original file line number Diff line number Diff line change
@@ -180,6 +180,9 @@ enum NodeError {
"PayjoinRequestCreationFailed",
"PayjoinResponseProcessingFailed",
"PayjoinRequestTimeout",
"PayjoinReceiverUnavailable",
"PayjoinReceiverRequestValidationFailed",
"PayjoinReceiverEnrollementFailed"
};

dictionary NodeStatus {
75 changes: 74 additions & 1 deletion src/builder.rs
Original file line number Diff line number Diff line change
@@ -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;
@@ -99,6 +100,13 @@ struct PayjoinSenderConfig {
payjoin_relay: String,
}

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

impl Default for LiquiditySourceConfig {
fn default() -> Self {
Self { lsps2_service: None }
@@ -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 {
@@ -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,
}
}

@@ -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_relay: String, payjoin_directory: String, ohttp_keys: Option<String>,
) -> &mut Self {
self.payjoin_receiver_config =
Some(PayjoinReceiverConfig { payjoin_relay, payjoin_directory, 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.
@@ -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,
@@ -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,
@@ -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_relay: String, payjoin_directory: String, ohttp_keys: Option<String>,
) {
self.inner.write().unwrap().set_payjoin_receiver_config(
payjoin_relay,
payjoin_directory,
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) {
@@ -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
@@ -1010,6 +1044,44 @@ 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,
)))
},
_ => {
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));
@@ -1030,6 +1102,7 @@ fn build_with_store_internal(
chain_monitor,
output_sweeper,
payjoin_sender,
payjoin_receiver,
peer_manager,
connection_manager,
keys_manager,
15 changes: 15 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
@@ -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 {
@@ -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.")
},
}
}
}
7 changes: 7 additions & 0 deletions src/io/utils.rs
Original file line number Diff line number Diff line change
@@ -511,6 +511,13 @@ pub(crate) fn check_namespace_key_validity(
Ok(())
}

pub(crate) fn ohttp_headers() -> reqwest::header::HeaderMap<reqwest::header::HeaderValue> {
let mut headers = reqwest::header::HeaderMap::new();
let header_value = reqwest::header::HeaderValue::from_static("message/ohttp-req");
headers.insert(reqwest::header::CONTENT_TYPE, header_value);
headers
}

#[cfg(test)]
mod tests {
use super::*;
29 changes: 29 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -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;
@@ -131,6 +132,7 @@ use connection::ConnectionManager;
use event::{EventHandler, EventQueue};
use gossip::GossipSource;
use liquidity::LiquiditySource;
use payjoin_receiver::PayjoinReceiver;
use payment::store::PaymentStore;
use payment::{
Bolt11Payment, Bolt12Payment, OnchainPayment, PayjoinPayment, PaymentDetails,
@@ -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>,
@@ -626,6 +629,28 @@ impl Node {
}
});

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),
@@ -938,9 +963,11 @@ impl Node {
#[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),
)
}
@@ -956,9 +983,11 @@ impl Node {
#[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),
)
}
Loading

0 comments on commit 035ad65

Please sign in to comment.