Skip to content

Commit 035ad65

Browse files
committed
Add ability to receive payjoin transactions
Allows the node wallet to receive payjoin transactions as specified in BIP78.
1 parent 0f110b7 commit 035ad65

11 files changed

+706
-17
lines changed

bindings/ldk_node.udl

+3
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,9 @@ enum NodeError {
180180
"PayjoinRequestCreationFailed",
181181
"PayjoinResponseProcessingFailed",
182182
"PayjoinRequestTimeout",
183+
"PayjoinReceiverUnavailable",
184+
"PayjoinReceiverRequestValidationFailed",
185+
"PayjoinReceiverEnrollementFailed"
183186
};
184187

185188
dictionary NodeStatus {

src/builder.rs

+74-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use crate::io::sqlite_store::SqliteStore;
1111
use crate::liquidity::LiquiditySource;
1212
use crate::logger::{log_error, log_info, FilesystemLogger, Logger};
1313
use crate::message_handler::NodeCustomMessageHandler;
14+
use crate::payjoin_receiver::PayjoinReceiver;
1415
use crate::payment::store::PaymentStore;
1516
use crate::peer_store::PeerStore;
1617
use crate::tx_broadcaster::TransactionBroadcaster;
@@ -99,6 +100,13 @@ struct PayjoinSenderConfig {
99100
payjoin_relay: String,
100101
}
101102

103+
#[derive(Debug, Clone)]
104+
struct PayjoinReceiverConfig {
105+
payjoin_relay: String,
106+
payjoin_directory: String,
107+
ohttp_keys: Option<String>,
108+
}
109+
102110
impl Default for LiquiditySourceConfig {
103111
fn default() -> Self {
104112
Self { lsps2_service: None }
@@ -179,6 +187,7 @@ pub struct NodeBuilder {
179187
gossip_source_config: Option<GossipSourceConfig>,
180188
liquidity_source_config: Option<LiquiditySourceConfig>,
181189
payjoin_sender_config: Option<PayjoinSenderConfig>,
190+
payjoin_receiver_config: Option<PayjoinReceiverConfig>,
182191
}
183192

184193
impl NodeBuilder {
@@ -195,13 +204,15 @@ impl NodeBuilder {
195204
let gossip_source_config = None;
196205
let liquidity_source_config = None;
197206
let payjoin_sender_config = None;
207+
let payjoin_receiver_config = None;
198208
Self {
199209
config,
200210
entropy_source_config,
201211
chain_data_source_config,
202212
gossip_source_config,
203213
liquidity_source_config,
204214
payjoin_sender_config,
215+
payjoin_receiver_config,
205216
}
206217
}
207218

@@ -262,6 +273,15 @@ impl NodeBuilder {
262273
self
263274
}
264275

276+
/// Configures the [`Node`] instance to enable receiving payjoin transactions.
277+
pub fn set_payjoin_receiver_config(
278+
&mut self, payjoin_relay: String, payjoin_directory: String, ohttp_keys: Option<String>,
279+
) -> &mut Self {
280+
self.payjoin_receiver_config =
281+
Some(PayjoinReceiverConfig { payjoin_relay, payjoin_directory, ohttp_keys });
282+
self
283+
}
284+
265285
/// Configures the [`Node`] instance to source its inbound liquidity from the given
266286
/// [LSPS2](https://github.com/BitcoinAndLightningLayerSpecs/lsp/blob/main/LSPS2/README.md)
267287
/// service.
@@ -381,6 +401,7 @@ impl NodeBuilder {
381401
self.gossip_source_config.as_ref(),
382402
self.liquidity_source_config.as_ref(),
383403
self.payjoin_sender_config.as_ref(),
404+
self.payjoin_receiver_config.as_ref(),
384405
seed_bytes,
385406
logger,
386407
vss_store,
@@ -403,6 +424,7 @@ impl NodeBuilder {
403424
self.gossip_source_config.as_ref(),
404425
self.liquidity_source_config.as_ref(),
405426
self.payjoin_sender_config.as_ref(),
427+
self.payjoin_receiver_config.as_ref(),
406428
seed_bytes,
407429
logger,
408430
kv_store,
@@ -475,6 +497,17 @@ impl ArcedNodeBuilder {
475497
self.inner.write().unwrap().set_payjoin_sender_config(payjoin_relay);
476498
}
477499

500+
/// Configures the [`Node`] instance to enable receiving payjoin transactions.
501+
pub fn set_payjoin_receiver_config(
502+
&mut self, payjoin_relay: String, payjoin_directory: String, ohttp_keys: Option<String>,
503+
) {
504+
self.inner.write().unwrap().set_payjoin_receiver_config(
505+
payjoin_relay,
506+
payjoin_directory,
507+
ohttp_keys,
508+
);
509+
}
510+
478511
/// Configures the [`Node`] instance to source its gossip data from the given RapidGossipSync
479512
/// server.
480513
pub fn set_gossip_source_rgs(&self, rgs_server_url: String) {
@@ -544,7 +577,8 @@ fn build_with_store_internal(
544577
config: Arc<Config>, chain_data_source_config: Option<&ChainDataSourceConfig>,
545578
gossip_source_config: Option<&GossipSourceConfig>,
546579
liquidity_source_config: Option<&LiquiditySourceConfig>,
547-
payjoin_sender_config: Option<&PayjoinSenderConfig>, seed_bytes: [u8; 64],
580+
payjoin_sender_config: Option<&PayjoinSenderConfig>,
581+
payjoin_receiver_config: Option<&PayjoinReceiverConfig>, seed_bytes: [u8; 64],
548582
logger: Arc<FilesystemLogger>, kv_store: Arc<DynStore>,
549583
) -> Result<Node, BuildError> {
550584
// Initialize the on-chain wallet and chain access
@@ -1010,6 +1044,44 @@ fn build_with_store_internal(
10101044
}
10111045
});
10121046

1047+
let payjoin_receiver = payjoin_receiver_config.as_ref().and_then(|prc| {
1048+
match (payjoin::Url::parse(&prc.payjoin_directory), payjoin::Url::parse(&prc.payjoin_relay))
1049+
{
1050+
(Ok(directory), Ok(relay)) => {
1051+
let ohttp_keys = match prc.ohttp_keys.clone() {
1052+
Some(keys) => {
1053+
let keys = match bitcoin::base64::decode(keys) {
1054+
Ok(keys) => keys,
1055+
Err(e) => {
1056+
log_info!(logger, "Failed to decode ohttp keys: the provided key is not a valid Base64 string {}", e);
1057+
return None;
1058+
},
1059+
};
1060+
match payjoin::OhttpKeys::decode(&keys) {
1061+
Ok(ohttp_keys) => Some(ohttp_keys),
1062+
Err(e) => {
1063+
log_info!(logger, "Failed to decode ohttp keys, make sure you provided a valid Ohttp Key as provided by the payjoin directory: {}", e);
1064+
return None;
1065+
},
1066+
}
1067+
},
1068+
None => None,
1069+
};
1070+
Some(Arc::new(PayjoinReceiver::new(
1071+
Arc::clone(&logger),
1072+
Arc::clone(&wallet),
1073+
directory,
1074+
relay,
1075+
ohttp_keys,
1076+
)))
1077+
},
1078+
_ => {
1079+
log_info!(logger, "The provided payjoin relay URL is invalid.");
1080+
None
1081+
},
1082+
}
1083+
});
1084+
10131085
let is_listening = Arc::new(AtomicBool::new(false));
10141086
let latest_wallet_sync_timestamp = Arc::new(RwLock::new(None));
10151087
let latest_onchain_wallet_sync_timestamp = Arc::new(RwLock::new(None));
@@ -1030,6 +1102,7 @@ fn build_with_store_internal(
10301102
chain_monitor,
10311103
output_sweeper,
10321104
payjoin_sender,
1105+
payjoin_receiver,
10331106
peer_manager,
10341107
connection_manager,
10351108
keys_manager,

src/error.rs

+15
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,12 @@ pub enum Error {
9797
PayjoinResponseProcessingFailed,
9898
/// Payjoin request timed out.
9999
PayjoinRequestTimeout,
100+
/// Failed to access payjoin receiver object.
101+
PayjoinReceiverUnavailable,
102+
/// Failed to enroll payjoin receiver.
103+
PayjoinReceiverEnrollementFailed,
104+
/// Failed to validate an incoming payjoin request.
105+
PayjoinReceiverRequestValidationFailed,
100106
}
101107

102108
impl fmt::Display for Error {
@@ -175,6 +181,15 @@ impl fmt::Display for Error {
175181
Self::PayjoinRequestTimeout => {
176182
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")
177183
},
184+
Self::PayjoinReceiverUnavailable => {
185+
write!(f, "Failed to access payjoin receiver object. Make sure you have enabled Payjoin receiving support.")
186+
},
187+
Self::PayjoinReceiverRequestValidationFailed => {
188+
write!(f, "Failed to validate an incoming payjoin request. Payjoin sender request didnt pass the payjoin validation steps.")
189+
},
190+
Self::PayjoinReceiverEnrollementFailed => {
191+
write!(f, "Failed to enroll payjoin receiver. Make sure the configured Payjoin directory & Payjoin relay are available.")
192+
},
178193
}
179194
}
180195
}

src/io/utils.rs

+7
Original file line numberDiff line numberDiff line change
@@ -511,6 +511,13 @@ pub(crate) fn check_namespace_key_validity(
511511
Ok(())
512512
}
513513

514+
pub(crate) fn ohttp_headers() -> reqwest::header::HeaderMap<reqwest::header::HeaderValue> {
515+
let mut headers = reqwest::header::HeaderMap::new();
516+
let header_value = reqwest::header::HeaderValue::from_static("message/ohttp-req");
517+
headers.insert(reqwest::header::CONTENT_TYPE, header_value);
518+
headers
519+
}
520+
514521
#[cfg(test)]
515522
mod tests {
516523
use super::*;

src/lib.rs

+29
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ pub mod io;
8888
mod liquidity;
8989
mod logger;
9090
mod message_handler;
91+
mod payjoin_receiver;
9192
mod payjoin_sender;
9293
pub mod payment;
9394
mod peer_store;
@@ -131,6 +132,7 @@ use connection::ConnectionManager;
131132
use event::{EventHandler, EventQueue};
132133
use gossip::GossipSource;
133134
use liquidity::LiquiditySource;
135+
use payjoin_receiver::PayjoinReceiver;
134136
use payment::store::PaymentStore;
135137
use payment::{
136138
Bolt11Payment, Bolt12Payment, OnchainPayment, PayjoinPayment, PaymentDetails,
@@ -187,6 +189,7 @@ pub struct Node {
187189
peer_manager: Arc<PeerManager>,
188190
connection_manager: Arc<ConnectionManager<Arc<FilesystemLogger>>>,
189191
payjoin_sender: Option<Arc<PayjoinSender>>,
192+
payjoin_receiver: Option<Arc<PayjoinReceiver>>,
190193
keys_manager: Arc<KeysManager>,
191194
network_graph: Arc<NetworkGraph>,
192195
gossip_source: Arc<GossipSource>,
@@ -626,6 +629,28 @@ impl Node {
626629
}
627630
});
628631

632+
if let Some(payjoin_receiver) = &self.payjoin_receiver {
633+
let mut stop_payjoin_server = self.stop_sender.subscribe();
634+
let payjoin_receiver = Arc::clone(&payjoin_receiver);
635+
let payjoin_check_interval = 5;
636+
runtime.spawn(async move {
637+
let mut payjoin_interval =
638+
tokio::time::interval(Duration::from_secs(payjoin_check_interval));
639+
payjoin_interval.reset();
640+
payjoin_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
641+
loop {
642+
tokio::select! {
643+
_ = stop_payjoin_server.changed() => {
644+
return;
645+
}
646+
_ = payjoin_interval.tick() => {
647+
let _ = payjoin_receiver.process_payjoin_request().await;
648+
}
649+
}
650+
}
651+
});
652+
}
653+
629654
let event_handler = Arc::new(EventHandler::new(
630655
Arc::clone(&self.event_queue),
631656
Arc::clone(&self.wallet),
@@ -938,9 +963,11 @@ impl Node {
938963
#[cfg(not(feature = "uniffi"))]
939964
pub fn payjoin_payment(&self) -> PayjoinPayment {
940965
let payjoin_sender = self.payjoin_sender.as_ref();
966+
let payjoin_receiver = self.payjoin_receiver.as_ref();
941967
PayjoinPayment::new(
942968
Arc::clone(&self.runtime),
943969
payjoin_sender.map(Arc::clone),
970+
payjoin_receiver.map(Arc::clone),
944971
Arc::clone(&self.config),
945972
)
946973
}
@@ -956,9 +983,11 @@ impl Node {
956983
#[cfg(feature = "uniffi")]
957984
pub fn payjoin_payment(&self) -> PayjoinPayment {
958985
let payjoin_sender = self.payjoin_sender.as_ref();
986+
let payjoin_receiver = self.payjoin_receiver.as_ref();
959987
PayjoinPayment::new(
960988
Arc::clone(&self.runtime),
961989
payjoin_sender.map(Arc::clone),
990+
payjoin_receiver.map(Arc::clone),
962991
Arc::clone(&self.config),
963992
)
964993
}

0 commit comments

Comments
 (0)