Skip to content

Commit 9d94240

Browse files
committed
Expose PayjoinPayment module
1 parent 833d9b3 commit 9d94240

File tree

6 files changed

+518
-2
lines changed

6 files changed

+518
-2
lines changed

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,8 @@ electrum-client = { version = "0.21.0", default-features = true }
9090
bitcoincore-rpc = { version = "0.19.0", default-features = false }
9191
proptest = "1.0.0"
9292
regex = "1.5.6"
93+
payjoin = { version = "0.16.0", default-features = false, features = ["send", "v2", "receive"] }
94+
reqwest = { version = "0.11", default-features = false, features = ["json", "rustls-tls", "blocking"] }
9395

9496
[target.'cfg(not(no_download))'.dev-dependencies]
9597
electrsd = { version = "0.29.0", features = ["legacy", "esplora_a33e97e1", "bitcoind_25_0"] }

bindings/ldk_node.udl

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ interface Node {
7878
SpontaneousPayment spontaneous_payment();
7979
OnchainPayment onchain_payment();
8080
UnifiedQrPayment unified_qr_payment();
81+
PayjoinPayment payjoin_payment();
8182
[Throws=NodeError]
8283
void connect(PublicKey node_id, SocketAddress address, boolean persist);
8384
[Throws=NodeError]
@@ -171,6 +172,13 @@ interface UnifiedQrPayment {
171172
QrPaymentResult send([ByRef]string uri_str);
172173
};
173174

175+
interface PayjoinPayment {
176+
[Throws=NodeError]
177+
void send(string payjoin_uri);
178+
[Throws=NodeError]
179+
void send_with_amount(string payjoin_uri, u64 amount_sats);
180+
};
181+
174182
[Error]
175183
enum NodeError {
176184
"AlreadyRunning",
@@ -222,6 +230,12 @@ enum NodeError {
222230
"InsufficientFunds",
223231
"LiquiditySourceUnavailable",
224232
"LiquidityFeeTooHigh",
233+
"PayjoinUnavailable",
234+
"PayjoinUriInvalid",
235+
"PayjoinRequestMissingAmount",
236+
"PayjoinRequestCreationFailed",
237+
"PayjoinRequestSendingFailed",
238+
"PayjoinResponseProcessingFailed",
225239
};
226240

227241
dictionary NodeStatus {
@@ -249,6 +263,7 @@ enum BuildError {
249263
"InvalidChannelMonitor",
250264
"InvalidListeningAddresses",
251265
"InvalidNodeAlias",
266+
"InvalidPayjoinConfig",
252267
"ReadFailed",
253268
"WriteFailed",
254269
"StoragePathAccessFailed",
@@ -280,6 +295,9 @@ interface Event {
280295
ChannelPending(ChannelId channel_id, UserChannelId user_channel_id, ChannelId former_temporary_channel_id, PublicKey counterparty_node_id, OutPoint funding_txo);
281296
ChannelReady(ChannelId channel_id, UserChannelId user_channel_id, PublicKey? counterparty_node_id);
282297
ChannelClosed(ChannelId channel_id, UserChannelId user_channel_id, PublicKey? counterparty_node_id, ClosureReason? reason);
298+
PayjoinPaymentAwaitingConfirmation(Txid txid, u64 amount_sats);
299+
PayjoinPaymentSuccessful(Txid txid, u64 amount_sats, boolean is_original_psbt_modified);
300+
PayjoinPaymentFailed(Txid txid, u64 amount_sats, PayjoinPaymentFailureReason reason);
283301
};
284302

285303
enum PaymentFailureReason {
@@ -294,6 +312,12 @@ enum PaymentFailureReason {
294312
"InvoiceRequestRejected",
295313
};
296314

315+
enum PayjoinPaymentFailureReason {
316+
"Timeout",
317+
"RequestSendingFailed",
318+
"ResponseProcessingFailed",
319+
};
320+
297321
[Enum]
298322
interface ClosureReason {
299323
CounterpartyForceClosed(UntrustedString peer_msg);
@@ -320,6 +344,7 @@ interface PaymentKind {
320344
Bolt12Offer(PaymentHash? hash, PaymentPreimage? preimage, PaymentSecret? secret, OfferId offer_id, UntrustedString? payer_note, u64? quantity);
321345
Bolt12Refund(PaymentHash? hash, PaymentPreimage? preimage, PaymentSecret? secret, UntrustedString? payer_note, u64? quantity);
322346
Spontaneous(PaymentHash hash, PaymentPreimage? preimage);
347+
Payjoin();
323348
};
324349

325350
[Enum]
@@ -352,6 +377,8 @@ dictionary PaymentDetails {
352377
PaymentDirection direction;
353378
PaymentStatus status;
354379
u64 latest_update_timestamp;
380+
Txid? txid;
381+
BestBlock? best_block;
355382
};
356383

357384
dictionary SendingParameters {

src/builder.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ use crate::types::{
2626
use crate::wallet::persist::KVStoreWalletPersister;
2727
use crate::wallet::Wallet;
2828
use crate::{io, NodeMetrics};
29+
use crate::PayjoinHandler;
2930
use crate::{LogLevel, Node};
3031

3132
use lightning::chain::{chainmonitor, BestBlock, Watch};
@@ -1229,6 +1230,25 @@ fn build_with_store_internal(
12291230
let (stop_sender, _) = tokio::sync::watch::channel(());
12301231
let (event_handling_stopped_sender, _) = tokio::sync::watch::channel(());
12311232

1233+
let payjoin_handler = payjoin_config.map(|pj_config| {
1234+
Arc::new(PayjoinHandler::new(
1235+
Arc::clone(&tx_sync),
1236+
Arc::clone(&event_queue),
1237+
Arc::clone(&logger),
1238+
pj_config.payjoin_relay.clone(),
1239+
Arc::clone(&payment_store),
1240+
Arc::clone(&wallet),
1241+
))
1242+
});
1243+
1244+
let is_listening = Arc::new(AtomicBool::new(false));
1245+
let latest_wallet_sync_timestamp = Arc::new(RwLock::new(None));
1246+
let latest_onchain_wallet_sync_timestamp = Arc::new(RwLock::new(None));
1247+
let latest_fee_rate_cache_update_timestamp = Arc::new(RwLock::new(None));
1248+
let latest_rgs_snapshot_timestamp = Arc::new(RwLock::new(None));
1249+
let latest_node_announcement_broadcast_timestamp = Arc::new(RwLock::new(None));
1250+
let latest_channel_monitor_archival_height = Arc::new(RwLock::new(None));
1251+
12321252
Ok(Node {
12331253
runtime,
12341254
stop_sender,
@@ -1241,6 +1261,7 @@ fn build_with_store_internal(
12411261
channel_manager,
12421262
chain_monitor,
12431263
output_sweeper,
1264+
payjoin_handler,
12441265
peer_manager,
12451266
onion_messenger,
12461267
connection_manager,

src/lib.rs

Lines changed: 120 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,10 @@ pub use balance::{BalanceDetails, LightningBalance, PendingSweepBalance};
106106
pub use error::Error as NodeError;
107107
use error::Error;
108108

109+
#[cfg(feature = "uniffi")]
110+
use crate::event::PayjoinPaymentFailureReason;
109111
pub use event::Event;
112+
use payment::payjoin::handler::PayjoinHandler;
110113

111114
pub use io::utils::generate_entropy_mnemonic;
112115

@@ -131,8 +134,8 @@ use graph::NetworkGraph;
131134
use io::utils::write_node_metrics;
132135
use liquidity::LiquiditySource;
133136
use payment::{
134-
Bolt11Payment, Bolt12Payment, OnchainPayment, PaymentDetails, SpontaneousPayment,
135-
UnifiedQrPayment,
137+
Bolt11Payment, Bolt12Payment, OnchainPayment, PayjoinPayment, PaymentDetails,
138+
SpontaneousPayment, UnifiedQrPayment,
136139
};
137140
use peer_store::{PeerInfo, PeerStore};
138141
use types::{
@@ -185,6 +188,7 @@ pub struct Node {
185188
peer_manager: Arc<PeerManager>,
186189
onion_messenger: Arc<OnionMessenger>,
187190
connection_manager: Arc<ConnectionManager<Arc<FilesystemLogger>>>,
191+
payjoin_handler: Option<Arc<PayjoinHandler>>,
188192
keys_manager: Arc<KeysManager>,
189193
network_graph: Arc<Graph>,
190194
gossip_source: Arc<GossipSource>,
@@ -252,6 +256,68 @@ impl Node {
252256
.continuously_sync_wallets(stop_sync_receiver, sync_cman, sync_cmon, sync_sweeper)
253257
.await;
254258
});
259+
let sync_logger = Arc::clone(&self.logger);
260+
let sync_payjoin = &self.payjoin_handler.as_ref();
261+
let sync_payjoin = sync_payjoin.map(Arc::clone);
262+
let sync_wallet_timestamp = Arc::clone(&self.latest_wallet_sync_timestamp);
263+
let sync_monitor_archival_height = Arc::clone(&self.latest_channel_monitor_archival_height);
264+
let mut stop_sync = self.stop_sender.subscribe();
265+
let wallet_sync_interval_secs =
266+
self.config.wallet_sync_interval_secs.max(WALLET_SYNC_INTERVAL_MINIMUM_SECS);
267+
runtime.spawn(async move {
268+
let mut wallet_sync_interval =
269+
tokio::time::interval(Duration::from_secs(wallet_sync_interval_secs));
270+
wallet_sync_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
271+
loop {
272+
tokio::select! {
273+
_ = stop_sync.changed() => {
274+
log_trace!(
275+
sync_logger,
276+
"Stopping background syncing Lightning wallet.",
277+
);
278+
return;
279+
}
280+
_ = wallet_sync_interval.tick() => {
281+
let mut confirmables = vec![
282+
&*sync_cman as &(dyn Confirm + Sync + Send),
283+
&*sync_cmon as &(dyn Confirm + Sync + Send),
284+
&*sync_sweeper as &(dyn Confirm + Sync + Send),
285+
];
286+
if let Some(sync_payjoin) = sync_payjoin.as_ref() {
287+
confirmables.push(sync_payjoin.as_ref() as &(dyn Confirm + Sync + Send));
288+
}
289+
let now = Instant::now();
290+
let timeout_fut = tokio::time::timeout(Duration::from_secs(LDK_WALLET_SYNC_TIMEOUT_SECS), tx_sync.sync(confirmables));
291+
match timeout_fut.await {
292+
Ok(res) => match res {
293+
Ok(()) => {
294+
log_trace!(
295+
sync_logger,
296+
"Background sync of Lightning wallet finished in {}ms.",
297+
now.elapsed().as_millis()
298+
);
299+
let unix_time_secs_opt =
300+
SystemTime::now().duration_since(UNIX_EPOCH).ok().map(|d| d.as_secs());
301+
*sync_wallet_timestamp.write().unwrap() = unix_time_secs_opt;
302+
303+
periodically_archive_fully_resolved_monitors(
304+
Arc::clone(&archive_cman),
305+
Arc::clone(&archive_cmon),
306+
Arc::clone(&sync_monitor_archival_height)
307+
);
308+
}
309+
Err(e) => {
310+
log_error!(sync_logger, "Background sync of Lightning wallet failed: {}", e)
311+
}
312+
}
313+
Err(e) => {
314+
log_error!(sync_logger, "Background sync of Lightning wallet timed out: {}", e)
315+
}
316+
}
317+
}
318+
}
319+
}
320+
});
255321

256322
if self.gossip_source.is_rgs() {
257323
let gossip_source = Arc::clone(&self.gossip_source);
@@ -947,6 +1013,42 @@ impl Node {
9471013
))
9481014
}
9491015

1016+
/// Returns a Payjoin payment handler allowing to send Payjoin transactions
1017+
///
1018+
/// in order to utilize Payjoin functionality, it is necessary to configure a Payjoin relay
1019+
/// using [`set_payjoin_config`].
1020+
///
1021+
/// [`set_payjoin_config`]: crate::builder::NodeBuilder::set_payjoin_config
1022+
#[cfg(not(feature = "uniffi"))]
1023+
pub fn payjoin_payment(&self) -> PayjoinPayment {
1024+
let payjoin_handler = self.payjoin_handler.as_ref();
1025+
PayjoinPayment::new(
1026+
Arc::clone(&self.config),
1027+
Arc::clone(&self.logger),
1028+
payjoin_handler.map(Arc::clone),
1029+
Arc::clone(&self.runtime),
1030+
Arc::clone(&self.tx_broadcaster),
1031+
)
1032+
}
1033+
1034+
/// Returns a Payjoin payment handler allowing to send Payjoin transactions.
1035+
///
1036+
/// in order to utilize Payjoin functionality, it is necessary to configure a Payjoin relay
1037+
/// using [`set_payjoin_config`].
1038+
///
1039+
/// [`set_payjoin_config`]: crate::builder::NodeBuilder::set_payjoin_config
1040+
#[cfg(feature = "uniffi")]
1041+
pub fn payjoin_payment(&self) -> Arc<PayjoinPayment> {
1042+
let payjoin_handler = self.payjoin_handler.as_ref();
1043+
Arc::new(PayjoinPayment::new(
1044+
Arc::clone(&self.config),
1045+
Arc::clone(&self.logger),
1046+
payjoin_handler.map(Arc::clone),
1047+
Arc::clone(&self.runtime),
1048+
Arc::clone(&self.tx_broadcaster),
1049+
))
1050+
}
1051+
9501052
/// Retrieve a list of known channels.
9511053
pub fn list_channels(&self) -> Vec<ChannelDetails> {
9521054
self.channel_manager.list_channels().into_iter().map(|c| c.into()).collect()
@@ -1205,6 +1307,22 @@ impl Node {
12051307
let sync_cman = Arc::clone(&self.channel_manager);
12061308
let sync_cmon = Arc::clone(&self.chain_monitor);
12071309
let sync_sweeper = Arc::clone(&self.output_sweeper);
1310+
let sync_logger = Arc::clone(&self.logger);
1311+
let sync_payjoin = &self.payjoin_handler.as_ref();
1312+
let mut confirmables = vec![
1313+
&*sync_cman as &(dyn Confirm + Sync + Send),
1314+
&*sync_cmon as &(dyn Confirm + Sync + Send),
1315+
&*sync_sweeper as &(dyn Confirm + Sync + Send),
1316+
];
1317+
if let Some(sync_payjoin) = sync_payjoin {
1318+
confirmables.push(sync_payjoin.as_ref() as &(dyn Confirm + Sync + Send));
1319+
}
1320+
let sync_wallet_timestamp = Arc::clone(&self.latest_wallet_sync_timestamp);
1321+
let sync_fee_rate_update_timestamp =
1322+
Arc::clone(&self.latest_fee_rate_cache_update_timestamp);
1323+
let sync_onchain_wallet_timestamp = Arc::clone(&self.latest_onchain_wallet_sync_timestamp);
1324+
let sync_monitor_archival_height = Arc::clone(&self.latest_channel_monitor_archival_height);
1325+
12081326
tokio::task::block_in_place(move || {
12091327
tokio::runtime::Builder::new_multi_thread().enable_all().build().unwrap().block_on(
12101328
async move {

tests/common/mod.rs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,41 @@ macro_rules! expect_payment_successful_event {
154154

155155
pub(crate) use expect_payment_successful_event;
156156

157+
macro_rules! expect_payjoin_tx_sent_successfully_event {
158+
($node: expr, $is_original_psbt_modified: expr) => {{
159+
match $node.wait_next_event() {
160+
ref e @ Event::PayjoinPaymentSuccessful { txid, is_original_psbt_modified, .. } => {
161+
println!("{} got event {:?}", $node.node_id(), e);
162+
assert_eq!(is_original_psbt_modified, $is_original_psbt_modified);
163+
$node.event_handled();
164+
txid
165+
},
166+
ref e => {
167+
panic!("{} got unexpected event!: {:?}", std::stringify!($node), e);
168+
},
169+
}
170+
}};
171+
}
172+
173+
pub(crate) use expect_payjoin_tx_sent_successfully_event;
174+
175+
macro_rules! expect_payjoin_await_confirmation {
176+
($node: expr) => {{
177+
match $node.wait_next_event() {
178+
ref e @ Event::PayjoinPaymentAwaitingConfirmation { txid, .. } => {
179+
println!("{} got event {:?}", $node.node_id(), e);
180+
$node.event_handled();
181+
txid
182+
},
183+
ref e => {
184+
panic!("{} got unexpected event!: {:?}", std::stringify!($node), e);
185+
},
186+
}
187+
}};
188+
}
189+
190+
pub(crate) use expect_payjoin_await_confirmation;
191+
157192
pub(crate) fn setup_bitcoind_and_electrsd() -> (BitcoinD, ElectrsD) {
158193
let bitcoind_exe =
159194
env::var("BITCOIND_EXE").ok().or_else(|| bitcoind::downloaded_exe_path().ok()).expect(
@@ -317,6 +352,20 @@ pub(crate) fn setup_node(
317352
node
318353
}
319354

355+
pub(crate) fn setup_payjoin_node(electrsd: &ElectrsD, config: Config) -> TestNode {
356+
let esplora_url = format!("http://{}", electrsd.esplora_url.as_ref().unwrap());
357+
setup_builder!(builder, config);
358+
builder.set_esplora_server(esplora_url.clone());
359+
let payjoin_relay = "https://pj.bobspacebkk.com".to_string();
360+
builder.set_payjoin_config(payjoin_relay).unwrap();
361+
let test_sync_store = Arc::new(TestSyncStore::new(config.storage_dir_path.into()));
362+
let node = builder.build_with_store(test_sync_store).unwrap();
363+
node.start().unwrap();
364+
assert!(node.status().is_running);
365+
assert!(node.status().latest_fee_rate_cache_update_timestamp.is_some());
366+
node
367+
}
368+
320369
pub(crate) fn generate_blocks_and_wait<E: ElectrumApi>(
321370
bitcoind: &BitcoindClient, electrs: &E, num: usize,
322371
) {

0 commit comments

Comments
 (0)