Skip to content

Commit

Permalink
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.
  • Loading branch information
jbesraa committed May 29, 2024
1 parent cbcbdd7 commit 22b2e82
Show file tree
Hide file tree
Showing 12 changed files with 771 additions and 19 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ bdk = { version = "0.29.0", default-features = false, features = ["std", "async-

reqwest = { version = "0.11", default-features = false, features = ["json", "rustls-tls"] }
rusqlite = { version = "0.28.0", features = ["bundled"] }
bitcoin = "0.30.2"
bitcoin = { version = "0.30.2", features = ["bitcoinconsensus"] }
bip39 = "2.0.0"

rand = "0.8.5"
Expand Down
3 changes: 3 additions & 0 deletions bindings/ldk_node.udl
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,9 @@ enum NodeError {
"PayjoinRequestCreationFailed",
"PayjoinResponseProcessingFailed",
"PayjoinRequestTimeout",
"PayjoinReceiverUnavailable",
"PayjoinReceiverRequestValidationFailed",
"PayjoinReceiverEnrollementFailed"
};

dictionary NodeStatus {
Expand Down
75 changes: 74 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_relay: String,
payjoin_directory: 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_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.
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_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) {
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,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));
Expand All @@ -1030,6 +1102,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 @@ -83,6 +83,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 @@ -152,6 +158,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
7 changes: 7 additions & 0 deletions src/io/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::*;
Expand Down
28 changes: 28 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_receiver;
mod payjoin_sender;
pub mod payment;
mod peer_store;
Expand Down Expand Up @@ -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, OnchainPayment, PayjoinPayment, PaymentDetails, SpontaneousPayment};
use peer_store::{PeerInfo, PeerStore};
Expand Down Expand Up @@ -184,6 +186,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 @@ -620,6 +623,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),
Expand Down Expand Up @@ -905,9 +930,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),
)
}
Expand All @@ -926,6 +953,7 @@ impl Node {
PayjoinPayment::new(
Arc::clone(&self.runtime),
payjoin_sender.map(Arc::clone),
None,
Arc::clone(&self.config),
)
}
Expand Down
Loading

0 comments on commit 22b2e82

Please sign in to comment.