Skip to content

Commit ff71f08

Browse files
committed
Add ability to receive payjoin transactions
Allows the node wallet to receive payjoin transactions as specified in BIP78.
1 parent cbcbdd7 commit ff71f08

12 files changed

+772
-19
lines changed

Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ bdk = { version = "0.29.0", default-features = false, features = ["std", "async-
5959

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

6565
rand = "0.8.5"

bindings/ldk_node.udl

+3
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,9 @@ enum NodeError {
157157
"PayjoinRequestCreationFailed",
158158
"PayjoinResponseProcessingFailed",
159159
"PayjoinRequestTimeout",
160+
"PayjoinReceiverUnavailable",
161+
"PayjoinReceiverRequestValidationFailed",
162+
"PayjoinReceiverEnrollementFailed"
160163
};
161164

162165
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
@@ -83,6 +83,12 @@ pub enum Error {
8383
PayjoinResponseProcessingFailed,
8484
/// Payjoin request timed out.
8585
PayjoinRequestTimeout,
86+
/// Failed to access payjoin receiver object.
87+
PayjoinReceiverUnavailable,
88+
/// Failed to enroll payjoin receiver.
89+
PayjoinReceiverEnrollementFailed,
90+
/// Failed to validate an incoming payjoin request.
91+
PayjoinReceiverRequestValidationFailed,
8692
}
8793

8894
impl fmt::Display for Error {
@@ -152,6 +158,15 @@ impl fmt::Display for Error {
152158
Self::PayjoinRequestTimeout => {
153159
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")
154160
},
161+
Self::PayjoinReceiverUnavailable => {
162+
write!(f, "Failed to access payjoin receiver object. Make sure you have enabled Payjoin receiving support.")
163+
},
164+
Self::PayjoinReceiverRequestValidationFailed => {
165+
write!(f, "Failed to validate an incoming payjoin request. Payjoin sender request didnt pass the payjoin validation steps.")
166+
},
167+
Self::PayjoinReceiverEnrollementFailed => {
168+
write!(f, "Failed to enroll payjoin receiver. Make sure the configured Payjoin directory & Payjoin relay are available.")
169+
},
155170
}
156171
}
157172
}

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::{Bolt11Payment, OnchainPayment, PayjoinPayment, PaymentDetails, SpontaneousPayment};
136138
use peer_store::{PeerInfo, PeerStore};
@@ -184,6 +186,7 @@ pub struct Node {
184186
peer_manager: Arc<PeerManager>,
185187
connection_manager: Arc<ConnectionManager<Arc<FilesystemLogger>>>,
186188
payjoin_sender: Option<Arc<PayjoinSender>>,
189+
payjoin_receiver: Option<Arc<PayjoinReceiver>>,
187190
keys_manager: Arc<KeysManager>,
188191
network_graph: Arc<NetworkGraph>,
189192
gossip_source: Arc<GossipSource>,
@@ -620,6 +623,28 @@ impl Node {
620623
}
621624
});
622625

626+
if let Some(payjoin_receiver) = &self.payjoin_receiver {
627+
let mut stop_payjoin_server = self.stop_sender.subscribe();
628+
let payjoin_receiver = Arc::clone(&payjoin_receiver);
629+
let payjoin_check_interval = 5;
630+
runtime.spawn(async move {
631+
let mut payjoin_interval =
632+
tokio::time::interval(Duration::from_secs(payjoin_check_interval));
633+
payjoin_interval.reset();
634+
payjoin_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
635+
loop {
636+
tokio::select! {
637+
_ = stop_payjoin_server.changed() => {
638+
return;
639+
}
640+
_ = payjoin_interval.tick() => {
641+
let _ = payjoin_receiver.process_payjoin_request().await;
642+
}
643+
}
644+
}
645+
});
646+
}
647+
623648
let event_handler = Arc::new(EventHandler::new(
624649
Arc::clone(&self.event_queue),
625650
Arc::clone(&self.wallet),
@@ -905,9 +930,11 @@ impl Node {
905930
#[cfg(not(feature = "uniffi"))]
906931
pub fn payjoin_payment(&self) -> PayjoinPayment {
907932
let payjoin_sender = self.payjoin_sender.as_ref();
933+
let payjoin_receiver = self.payjoin_receiver.as_ref();
908934
PayjoinPayment::new(
909935
Arc::clone(&self.runtime),
910936
payjoin_sender.map(Arc::clone),
937+
payjoin_receiver.map(Arc::clone),
911938
Arc::clone(&self.config),
912939
)
913940
}
@@ -923,9 +950,11 @@ impl Node {
923950
#[cfg(feature = "uniffi")]
924951
pub fn payjoin_payment(&self) -> PayjoinPayment {
925952
let payjoin_sender = self.payjoin_sender.as_ref();
953+
let payjoin_receiver = self.payjoin_receiver.as_ref();
926954
PayjoinPayment::new(
927955
Arc::clone(&self.runtime),
928956
payjoin_sender.map(Arc::clone),
957+
payjoin_receiver.map(Arc::clone),
929958
Arc::clone(&self.config),
930959
)
931960
}

0 commit comments

Comments
 (0)