Skip to content

Commit 71f7ce5

Browse files
committed
Add payjoin sender
1 parent b7c4862 commit 71f7ce5

File tree

7 files changed

+382
-0
lines changed

7 files changed

+382
-0
lines changed

Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ tokio = { version = "1", default-features = false, features = [ "rt-multi-thread
6868
esplora-client = { version = "0.6", default-features = false }
6969
libc = "0.2"
7070
uniffi = { version = "0.26.0", features = ["build"], optional = true }
71+
payjoin = { version = "0.15.0", features = ["send", "receive", "v2"] }
7172

7273
[target.'cfg(vss)'.dependencies]
7374
vss-client = "0.2"

bindings/ldk_node.udl

+2
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,8 @@ enum NodeError {
151151
"InsufficientFunds",
152152
"LiquiditySourceUnavailable",
153153
"LiquidityFeeTooHigh",
154+
"PayjoinSender",
155+
"AmountMissing"
154156
};
155157

156158
dictionary NodeStatus {

src/builder.rs

+34
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_sender::PayjoinSender;
1415
use crate::payment::store::PaymentStore;
1516
use crate::peer_store::PeerStore;
1617
use crate::tx_broadcaster::TransactionBroadcaster;
@@ -94,6 +95,11 @@ struct LiquiditySourceConfig {
9495
lsps2_service: Option<(SocketAddress, PublicKey, Option<String>)>,
9596
}
9697

98+
#[derive(Debug, Clone)]
99+
struct PayjoinSenderConfig {
100+
payjoin_relay: payjoin::Url,
101+
}
102+
97103
impl Default for LiquiditySourceConfig {
98104
fn default() -> Self {
99105
Self { lsps2_service: None }
@@ -173,6 +179,7 @@ pub struct NodeBuilder {
173179
chain_data_source_config: Option<ChainDataSourceConfig>,
174180
gossip_source_config: Option<GossipSourceConfig>,
175181
liquidity_source_config: Option<LiquiditySourceConfig>,
182+
payjoin_sender_config: Option<PayjoinSenderConfig>,
176183
}
177184

178185
impl NodeBuilder {
@@ -188,12 +195,14 @@ impl NodeBuilder {
188195
let chain_data_source_config = None;
189196
let gossip_source_config = None;
190197
let liquidity_source_config = None;
198+
let payjoin_sender_config = None;
191199
Self {
192200
config,
193201
entropy_source_config,
194202
chain_data_source_config,
195203
gossip_source_config,
196204
liquidity_source_config,
205+
payjoin_sender_config,
197206
}
198207
}
199208

@@ -248,6 +257,12 @@ impl NodeBuilder {
248257
self
249258
}
250259

260+
/// Configures the [`Node`] instance to enable sending payjoin transactions.
261+
pub fn set_payjoin_sender_config(&mut self, payjoin_relay: payjoin::Url) -> &mut Self {
262+
self.payjoin_sender_config = Some(PayjoinSenderConfig { payjoin_relay });
263+
self
264+
}
265+
251266
/// Configures the [`Node`] instance to source its inbound liquidity from the given
252267
/// [LSPS2](https://github.com/BitcoinAndLightningLayerSpecs/lsp/blob/main/LSPS2/README.md)
253268
/// service.
@@ -369,6 +384,7 @@ impl NodeBuilder {
369384
seed_bytes,
370385
logger,
371386
vss_store,
387+
self.payjoin_sender_config.as_ref(),
372388
)
373389
}
374390

@@ -390,6 +406,7 @@ impl NodeBuilder {
390406
seed_bytes,
391407
logger,
392408
kv_store,
409+
self.payjoin_sender_config.as_ref(),
393410
)
394411
}
395412
}
@@ -454,6 +471,11 @@ impl ArcedNodeBuilder {
454471
self.inner.write().unwrap().set_gossip_source_p2p();
455472
}
456473

474+
/// Configures the [`Node`] instance to enable sending payjoin transactions.
475+
pub fn set_payjoin_sender_config(&self, payjoin_relay: payjoin::Url) {
476+
self.inner.write().unwrap().set_payjoin_sender_config(payjoin_relay);
477+
}
478+
457479
/// Configures the [`Node`] instance to source its gossip data from the given RapidGossipSync
458480
/// server.
459481
pub fn set_gossip_source_rgs(&self, rgs_server_url: String) {
@@ -524,6 +546,7 @@ fn build_with_store_internal(
524546
gossip_source_config: Option<&GossipSourceConfig>,
525547
liquidity_source_config: Option<&LiquiditySourceConfig>, seed_bytes: [u8; 64],
526548
logger: Arc<FilesystemLogger>, kv_store: Arc<DynStore>,
549+
payjoin_sender_config: Option<&PayjoinSenderConfig>,
527550
) -> Result<Node, BuildError> {
528551
// Initialize the on-chain wallet and chain access
529552
let xprv = bitcoin::bip32::ExtendedPrivKey::new_master(config.network.into(), &seed_bytes)
@@ -973,6 +996,16 @@ fn build_with_store_internal(
973996
};
974997

975998
let (stop_sender, _) = tokio::sync::watch::channel(());
999+
let payjoin_sender = if let Some(payjoin_sender_config) = payjoin_sender_config {
1000+
let payjoin_sender = PayjoinSender::new(
1001+
Arc::clone(&logger),
1002+
Arc::clone(&wallet),
1003+
&payjoin_sender_config.payjoin_relay,
1004+
);
1005+
Some(Arc::new(payjoin_sender))
1006+
} else {
1007+
None
1008+
};
9761009

9771010
let is_listening = Arc::new(AtomicBool::new(false));
9781011
let latest_wallet_sync_timestamp = Arc::new(RwLock::new(None));
@@ -993,6 +1026,7 @@ fn build_with_store_internal(
9931026
channel_manager,
9941027
chain_monitor,
9951028
output_sweeper,
1029+
payjoin_sender,
9961030
peer_manager,
9971031
connection_manager,
9981032
keys_manager,

src/error.rs

+8
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,10 @@ pub enum Error {
7171
LiquiditySourceUnavailable,
7272
/// The given operation failed due to the LSP's required opening fee being too high.
7373
LiquidityFeeTooHigh,
74+
/// Amount is not prvoided and neither defined in the URI.
75+
AmountMissing,
76+
/// Payjoin errors
77+
PayjoinSender,
7478
}
7579

7680
impl fmt::Display for Error {
@@ -122,6 +126,10 @@ impl fmt::Display for Error {
122126
Self::LiquidityFeeTooHigh => {
123127
write!(f, "The given operation failed due to the LSP's required opening fee being too high.")
124128
},
129+
Self::PayjoinSender => write!(f, "Failed to send payjoin."),
130+
Self::AmountMissing => {
131+
write!(f, "Amount is not provided and neither defined in the URI.")
132+
},
125133
}
126134
}
127135
}

src/lib.rs

+47
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_sender;
9192
pub mod payment;
9293
mod peer_store;
9394
mod sweep;
@@ -99,6 +100,7 @@ mod wallet;
99100

100101
pub use bip39;
101102
pub use bitcoin;
103+
use bitcoin::address::NetworkChecked;
102104
pub use lightning;
103105
pub use lightning_invoice;
104106

@@ -108,6 +110,7 @@ pub use error::Error as NodeError;
108110
use error::Error;
109111

110112
pub use event::Event;
113+
use payjoin_sender::PayjoinSender;
111114
pub use types::ChannelConfig;
112115

113116
pub use io::utils::generate_entropy_mnemonic;
@@ -181,6 +184,7 @@ pub struct Node {
181184
output_sweeper: Arc<Sweeper>,
182185
peer_manager: Arc<PeerManager>,
183186
connection_manager: Arc<ConnectionManager<Arc<FilesystemLogger>>>,
187+
payjoin_sender: Option<Arc<PayjoinSender<Arc<FilesystemLogger>>>>,
184188
keys_manager: Arc<KeysManager>,
185189
network_graph: Arc<NetworkGraph>,
186190
gossip_source: Arc<GossipSource>,
@@ -491,6 +495,28 @@ impl Node {
491495
});
492496
}
493497

498+
if let Some(payjoin_sender) = &self.payjoin_sender {
499+
let mut stop_payjoin_server = self.stop_sender.subscribe();
500+
let payjoin_sender = Arc::clone(&payjoin_sender);
501+
let payjoin_check_interval = 2;
502+
runtime.spawn(async move {
503+
let mut payjoin_interval =
504+
tokio::time::interval(Duration::from_secs(payjoin_check_interval));
505+
payjoin_interval.reset();
506+
payjoin_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
507+
loop {
508+
tokio::select! {
509+
_ = stop_payjoin_server.changed() => {
510+
return;
511+
}
512+
_ = payjoin_interval.tick() => {
513+
let _ = payjoin_sender.process_payjoin_response().await;
514+
}
515+
}
516+
}
517+
});
518+
}
519+
494520
// Regularly reconnect to persisted peers.
495521
let connect_cm = Arc::clone(&self.connection_manager);
496522
let connect_pm = Arc::clone(&self.peer_manager);
@@ -697,6 +723,27 @@ impl Node {
697723
Ok(())
698724
}
699725

726+
/// Send a payjoin transaction from the node on chain funds to the address as specified in the
727+
/// payjoin URI.
728+
pub async fn send_payjoin_transaction(
729+
&self, payjoin_uri: payjoin::Uri<'static, NetworkChecked>, amount: Option<bitcoin::Amount>,
730+
) -> Result<Option<bitcoin::Txid>, Error> {
731+
let rt_lock = self.runtime.read().unwrap();
732+
if rt_lock.is_none() {
733+
return Err(Error::NotRunning);
734+
}
735+
let payjoin_sender = self.payjoin_sender.as_ref().ok_or(Error::PayjoinSender)?;
736+
let psbt = match payjoin_sender.create_payjoin_request(payjoin_uri.clone(), amount) {
737+
Ok(psbt) => psbt,
738+
Err(e) => {
739+
dbg!("Failed to create payjoin request: {}", e);
740+
log_error!(self.logger, "Failed to create payjoin request: {}", e);
741+
return Err(Error::PayjoinSender);
742+
},
743+
};
744+
payjoin_sender.send_payjoin_request(payjoin_uri, psbt).await
745+
}
746+
700747
/// Disconnects all peers, stops all running background tasks, and shuts down [`Node`].
701748
///
702749
/// After this returns most API methods will return [`Error::NotRunning`].

0 commit comments

Comments
 (0)