Skip to content

Commit 56b1660

Browse files
committed
Expose PayjoinPayment module
1 parent ce87a44 commit 56b1660

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",
@@ -281,6 +296,9 @@ interface Event {
281296
ChannelPending(ChannelId channel_id, UserChannelId user_channel_id, ChannelId former_temporary_channel_id, PublicKey counterparty_node_id, OutPoint funding_txo);
282297
ChannelReady(ChannelId channel_id, UserChannelId user_channel_id, PublicKey? counterparty_node_id);
283298
ChannelClosed(ChannelId channel_id, UserChannelId user_channel_id, PublicKey? counterparty_node_id, ClosureReason? reason);
299+
PayjoinPaymentAwaitingConfirmation(Txid txid, u64 amount_sats);
300+
PayjoinPaymentSuccessful(Txid txid, u64 amount_sats, boolean is_original_psbt_modified);
301+
PayjoinPaymentFailed(Txid txid, u64 amount_sats, PayjoinPaymentFailureReason reason);
284302
};
285303

286304
enum PaymentFailureReason {
@@ -295,6 +313,12 @@ enum PaymentFailureReason {
295313
"InvoiceRequestRejected",
296314
};
297315

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

326351
[Enum]
@@ -353,6 +378,8 @@ dictionary PaymentDetails {
353378
PaymentDirection direction;
354379
PaymentStatus status;
355380
u64 latest_update_timestamp;
381+
Txid? txid;
382+
BestBlock? best_block;
356383
};
357384

358385
dictionary SendingParameters {

src/builder.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ use crate::types::{
2828
use crate::wallet::persist::KVStoreWalletPersister;
2929
use crate::wallet::Wallet;
3030
use crate::{io, NodeMetrics};
31+
use crate::PayjoinHandler;
3132
use crate::{LogLevel, Node};
3233

3334
use lightning::chain::{chainmonitor, BestBlock, Watch};
@@ -1231,6 +1232,25 @@ fn build_with_store_internal(
12311232
let (stop_sender, _) = tokio::sync::watch::channel(());
12321233
let (event_handling_stopped_sender, _) = tokio::sync::watch::channel(());
12331234

1235+
let payjoin_handler = payjoin_config.map(|pj_config| {
1236+
Arc::new(PayjoinHandler::new(
1237+
Arc::clone(&tx_sync),
1238+
Arc::clone(&event_queue),
1239+
Arc::clone(&logger),
1240+
pj_config.payjoin_relay.clone(),
1241+
Arc::clone(&payment_store),
1242+
Arc::clone(&wallet),
1243+
))
1244+
});
1245+
1246+
let is_listening = Arc::new(AtomicBool::new(false));
1247+
let latest_wallet_sync_timestamp = Arc::new(RwLock::new(None));
1248+
let latest_onchain_wallet_sync_timestamp = Arc::new(RwLock::new(None));
1249+
let latest_fee_rate_cache_update_timestamp = Arc::new(RwLock::new(None));
1250+
let latest_rgs_snapshot_timestamp = Arc::new(RwLock::new(None));
1251+
let latest_node_announcement_broadcast_timestamp = Arc::new(RwLock::new(None));
1252+
let latest_channel_monitor_archival_height = Arc::new(RwLock::new(None));
1253+
12341254
Ok(Node {
12351255
runtime,
12361256
stop_sender,
@@ -1243,6 +1263,7 @@ fn build_with_store_internal(
12431263
channel_manager,
12441264
chain_monitor,
12451265
output_sweeper,
1266+
payjoin_handler,
12461267
peer_manager,
12471268
onion_messenger,
12481269
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

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

258324
if self.gossip_source.is_rgs() {
259325
let gossip_source = Arc::clone(&self.gossip_source);
@@ -960,6 +1026,42 @@ impl Node {
9601026
))
9611027
}
9621028

1029+
/// Returns a Payjoin payment handler allowing to send Payjoin transactions
1030+
///
1031+
/// in order to utilize Payjoin functionality, it is necessary to configure a Payjoin relay
1032+
/// using [`set_payjoin_config`].
1033+
///
1034+
/// [`set_payjoin_config`]: crate::builder::NodeBuilder::set_payjoin_config
1035+
#[cfg(not(feature = "uniffi"))]
1036+
pub fn payjoin_payment(&self) -> PayjoinPayment {
1037+
let payjoin_handler = self.payjoin_handler.as_ref();
1038+
PayjoinPayment::new(
1039+
Arc::clone(&self.config),
1040+
Arc::clone(&self.logger),
1041+
payjoin_handler.map(Arc::clone),
1042+
Arc::clone(&self.runtime),
1043+
Arc::clone(&self.tx_broadcaster),
1044+
)
1045+
}
1046+
1047+
/// Returns a Payjoin payment handler allowing to send Payjoin transactions.
1048+
///
1049+
/// in order to utilize Payjoin functionality, it is necessary to configure a Payjoin relay
1050+
/// using [`set_payjoin_config`].
1051+
///
1052+
/// [`set_payjoin_config`]: crate::builder::NodeBuilder::set_payjoin_config
1053+
#[cfg(feature = "uniffi")]
1054+
pub fn payjoin_payment(&self) -> Arc<PayjoinPayment> {
1055+
let payjoin_handler = self.payjoin_handler.as_ref();
1056+
Arc::new(PayjoinPayment::new(
1057+
Arc::clone(&self.config),
1058+
Arc::clone(&self.logger),
1059+
payjoin_handler.map(Arc::clone),
1060+
Arc::clone(&self.runtime),
1061+
Arc::clone(&self.tx_broadcaster),
1062+
))
1063+
}
1064+
9631065
/// Retrieve a list of known channels.
9641066
pub fn list_channels(&self) -> Vec<ChannelDetails> {
9651067
self.channel_manager.list_channels().into_iter().map(|c| c.into()).collect()
@@ -1218,6 +1320,22 @@ impl Node {
12181320
let sync_cman = Arc::clone(&self.channel_manager);
12191321
let sync_cmon = Arc::clone(&self.chain_monitor);
12201322
let sync_sweeper = Arc::clone(&self.output_sweeper);
1323+
let sync_logger = Arc::clone(&self.logger);
1324+
let sync_payjoin = &self.payjoin_handler.as_ref();
1325+
let mut confirmables = vec![
1326+
&*sync_cman as &(dyn Confirm + Sync + Send),
1327+
&*sync_cmon as &(dyn Confirm + Sync + Send),
1328+
&*sync_sweeper as &(dyn Confirm + Sync + Send),
1329+
];
1330+
if let Some(sync_payjoin) = sync_payjoin {
1331+
confirmables.push(sync_payjoin.as_ref() as &(dyn Confirm + Sync + Send));
1332+
}
1333+
let sync_wallet_timestamp = Arc::clone(&self.latest_wallet_sync_timestamp);
1334+
let sync_fee_rate_update_timestamp =
1335+
Arc::clone(&self.latest_fee_rate_cache_update_timestamp);
1336+
let sync_onchain_wallet_timestamp = Arc::clone(&self.latest_onchain_wallet_sync_timestamp);
1337+
let sync_monitor_archival_height = Arc::clone(&self.latest_channel_monitor_archival_height);
1338+
12211339
tokio::task::block_in_place(move || {
12221340
tokio::runtime::Builder::new_multi_thread().enable_all().build().unwrap().block_on(
12231341
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)