Skip to content

Commit

Permalink
Expose PayjoinPayment module
Browse files Browse the repository at this point in the history
  • Loading branch information
jbesraa committed Nov 8, 2024
1 parent 833d9b3 commit 9d94240
Show file tree
Hide file tree
Showing 6 changed files with 518 additions and 2 deletions.
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ electrum-client = { version = "0.21.0", default-features = true }
bitcoincore-rpc = { version = "0.19.0", default-features = false }
proptest = "1.0.0"
regex = "1.5.6"
payjoin = { version = "0.16.0", default-features = false, features = ["send", "v2", "receive"] }
reqwest = { version = "0.11", default-features = false, features = ["json", "rustls-tls", "blocking"] }

[target.'cfg(not(no_download))'.dev-dependencies]
electrsd = { version = "0.29.0", features = ["legacy", "esplora_a33e97e1", "bitcoind_25_0"] }
Expand Down
27 changes: 27 additions & 0 deletions bindings/ldk_node.udl
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ interface Node {
SpontaneousPayment spontaneous_payment();
OnchainPayment onchain_payment();
UnifiedQrPayment unified_qr_payment();
PayjoinPayment payjoin_payment();
[Throws=NodeError]
void connect(PublicKey node_id, SocketAddress address, boolean persist);
[Throws=NodeError]
Expand Down Expand Up @@ -171,6 +172,13 @@ interface UnifiedQrPayment {
QrPaymentResult send([ByRef]string uri_str);
};

interface PayjoinPayment {
[Throws=NodeError]
void send(string payjoin_uri);
[Throws=NodeError]
void send_with_amount(string payjoin_uri, u64 amount_sats);
};

[Error]
enum NodeError {
"AlreadyRunning",
Expand Down Expand Up @@ -222,6 +230,12 @@ enum NodeError {
"InsufficientFunds",
"LiquiditySourceUnavailable",
"LiquidityFeeTooHigh",
"PayjoinUnavailable",
"PayjoinUriInvalid",
"PayjoinRequestMissingAmount",
"PayjoinRequestCreationFailed",
"PayjoinRequestSendingFailed",
"PayjoinResponseProcessingFailed",
};

dictionary NodeStatus {
Expand Down Expand Up @@ -249,6 +263,7 @@ enum BuildError {
"InvalidChannelMonitor",
"InvalidListeningAddresses",
"InvalidNodeAlias",
"InvalidPayjoinConfig",
"ReadFailed",
"WriteFailed",
"StoragePathAccessFailed",
Expand Down Expand Up @@ -280,6 +295,9 @@ interface Event {
ChannelPending(ChannelId channel_id, UserChannelId user_channel_id, ChannelId former_temporary_channel_id, PublicKey counterparty_node_id, OutPoint funding_txo);
ChannelReady(ChannelId channel_id, UserChannelId user_channel_id, PublicKey? counterparty_node_id);
ChannelClosed(ChannelId channel_id, UserChannelId user_channel_id, PublicKey? counterparty_node_id, ClosureReason? reason);
PayjoinPaymentAwaitingConfirmation(Txid txid, u64 amount_sats);
PayjoinPaymentSuccessful(Txid txid, u64 amount_sats, boolean is_original_psbt_modified);
PayjoinPaymentFailed(Txid txid, u64 amount_sats, PayjoinPaymentFailureReason reason);
};

enum PaymentFailureReason {
Expand All @@ -294,6 +312,12 @@ enum PaymentFailureReason {
"InvoiceRequestRejected",
};

enum PayjoinPaymentFailureReason {
"Timeout",
"RequestSendingFailed",
"ResponseProcessingFailed",
};

[Enum]
interface ClosureReason {
CounterpartyForceClosed(UntrustedString peer_msg);
Expand All @@ -320,6 +344,7 @@ interface PaymentKind {
Bolt12Offer(PaymentHash? hash, PaymentPreimage? preimage, PaymentSecret? secret, OfferId offer_id, UntrustedString? payer_note, u64? quantity);
Bolt12Refund(PaymentHash? hash, PaymentPreimage? preimage, PaymentSecret? secret, UntrustedString? payer_note, u64? quantity);
Spontaneous(PaymentHash hash, PaymentPreimage? preimage);
Payjoin();
};

[Enum]
Expand Down Expand Up @@ -352,6 +377,8 @@ dictionary PaymentDetails {
PaymentDirection direction;
PaymentStatus status;
u64 latest_update_timestamp;
Txid? txid;
BestBlock? best_block;
};

dictionary SendingParameters {
Expand Down
21 changes: 21 additions & 0 deletions src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ use crate::types::{
use crate::wallet::persist::KVStoreWalletPersister;
use crate::wallet::Wallet;
use crate::{io, NodeMetrics};
use crate::PayjoinHandler;
use crate::{LogLevel, Node};

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

let payjoin_handler = payjoin_config.map(|pj_config| {
Arc::new(PayjoinHandler::new(
Arc::clone(&tx_sync),
Arc::clone(&event_queue),
Arc::clone(&logger),
pj_config.payjoin_relay.clone(),
Arc::clone(&payment_store),
Arc::clone(&wallet),
))
});

let is_listening = Arc::new(AtomicBool::new(false));
let latest_wallet_sync_timestamp = Arc::new(RwLock::new(None));
let latest_onchain_wallet_sync_timestamp = Arc::new(RwLock::new(None));
let latest_fee_rate_cache_update_timestamp = Arc::new(RwLock::new(None));
let latest_rgs_snapshot_timestamp = Arc::new(RwLock::new(None));
let latest_node_announcement_broadcast_timestamp = Arc::new(RwLock::new(None));
let latest_channel_monitor_archival_height = Arc::new(RwLock::new(None));

Ok(Node {
runtime,
stop_sender,
Expand All @@ -1241,6 +1261,7 @@ fn build_with_store_internal(
channel_manager,
chain_monitor,
output_sweeper,
payjoin_handler,
peer_manager,
onion_messenger,
connection_manager,
Expand Down
122 changes: 120 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,10 @@ pub use balance::{BalanceDetails, LightningBalance, PendingSweepBalance};
pub use error::Error as NodeError;
use error::Error;

#[cfg(feature = "uniffi")]
use crate::event::PayjoinPaymentFailureReason;
pub use event::Event;
use payment::payjoin::handler::PayjoinHandler;

pub use io::utils::generate_entropy_mnemonic;

Expand All @@ -131,8 +134,8 @@ use graph::NetworkGraph;
use io::utils::write_node_metrics;
use liquidity::LiquiditySource;
use payment::{
Bolt11Payment, Bolt12Payment, OnchainPayment, PaymentDetails, SpontaneousPayment,
UnifiedQrPayment,
Bolt11Payment, Bolt12Payment, OnchainPayment, PayjoinPayment, PaymentDetails,
SpontaneousPayment, UnifiedQrPayment,
};
use peer_store::{PeerInfo, PeerStore};
use types::{
Expand Down Expand Up @@ -185,6 +188,7 @@ pub struct Node {
peer_manager: Arc<PeerManager>,
onion_messenger: Arc<OnionMessenger>,
connection_manager: Arc<ConnectionManager<Arc<FilesystemLogger>>>,
payjoin_handler: Option<Arc<PayjoinHandler>>,
keys_manager: Arc<KeysManager>,
network_graph: Arc<Graph>,
gossip_source: Arc<GossipSource>,
Expand Down Expand Up @@ -252,6 +256,68 @@ impl Node {
.continuously_sync_wallets(stop_sync_receiver, sync_cman, sync_cmon, sync_sweeper)
.await;
});
let sync_logger = Arc::clone(&self.logger);
let sync_payjoin = &self.payjoin_handler.as_ref();
let sync_payjoin = sync_payjoin.map(Arc::clone);
let sync_wallet_timestamp = Arc::clone(&self.latest_wallet_sync_timestamp);
let sync_monitor_archival_height = Arc::clone(&self.latest_channel_monitor_archival_height);
let mut stop_sync = self.stop_sender.subscribe();
let wallet_sync_interval_secs =
self.config.wallet_sync_interval_secs.max(WALLET_SYNC_INTERVAL_MINIMUM_SECS);
runtime.spawn(async move {
let mut wallet_sync_interval =
tokio::time::interval(Duration::from_secs(wallet_sync_interval_secs));
wallet_sync_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
loop {
tokio::select! {
_ = stop_sync.changed() => {
log_trace!(
sync_logger,
"Stopping background syncing Lightning wallet.",
);
return;
}
_ = wallet_sync_interval.tick() => {
let mut confirmables = vec![
&*sync_cman as &(dyn Confirm + Sync + Send),
&*sync_cmon as &(dyn Confirm + Sync + Send),
&*sync_sweeper as &(dyn Confirm + Sync + Send),
];
if let Some(sync_payjoin) = sync_payjoin.as_ref() {
confirmables.push(sync_payjoin.as_ref() as &(dyn Confirm + Sync + Send));
}
let now = Instant::now();
let timeout_fut = tokio::time::timeout(Duration::from_secs(LDK_WALLET_SYNC_TIMEOUT_SECS), tx_sync.sync(confirmables));
match timeout_fut.await {
Ok(res) => match res {
Ok(()) => {
log_trace!(
sync_logger,
"Background sync of Lightning wallet finished in {}ms.",
now.elapsed().as_millis()
);
let unix_time_secs_opt =
SystemTime::now().duration_since(UNIX_EPOCH).ok().map(|d| d.as_secs());
*sync_wallet_timestamp.write().unwrap() = unix_time_secs_opt;

periodically_archive_fully_resolved_monitors(
Arc::clone(&archive_cman),
Arc::clone(&archive_cmon),
Arc::clone(&sync_monitor_archival_height)
);
}
Err(e) => {
log_error!(sync_logger, "Background sync of Lightning wallet failed: {}", e)
}
}
Err(e) => {
log_error!(sync_logger, "Background sync of Lightning wallet timed out: {}", e)
}
}
}
}
}
});

if self.gossip_source.is_rgs() {
let gossip_source = Arc::clone(&self.gossip_source);
Expand Down Expand Up @@ -947,6 +1013,42 @@ impl Node {
))
}

/// Returns a Payjoin payment handler allowing to send Payjoin transactions
///
/// in order to utilize Payjoin functionality, it is necessary to configure a Payjoin relay
/// using [`set_payjoin_config`].
///
/// [`set_payjoin_config`]: crate::builder::NodeBuilder::set_payjoin_config
#[cfg(not(feature = "uniffi"))]
pub fn payjoin_payment(&self) -> PayjoinPayment {
let payjoin_handler = self.payjoin_handler.as_ref();
PayjoinPayment::new(
Arc::clone(&self.config),
Arc::clone(&self.logger),
payjoin_handler.map(Arc::clone),
Arc::clone(&self.runtime),
Arc::clone(&self.tx_broadcaster),
)
}

/// Returns a Payjoin payment handler allowing to send Payjoin transactions.
///
/// in order to utilize Payjoin functionality, it is necessary to configure a Payjoin relay
/// using [`set_payjoin_config`].
///
/// [`set_payjoin_config`]: crate::builder::NodeBuilder::set_payjoin_config
#[cfg(feature = "uniffi")]
pub fn payjoin_payment(&self) -> Arc<PayjoinPayment> {
let payjoin_handler = self.payjoin_handler.as_ref();
Arc::new(PayjoinPayment::new(
Arc::clone(&self.config),
Arc::clone(&self.logger),
payjoin_handler.map(Arc::clone),
Arc::clone(&self.runtime),
Arc::clone(&self.tx_broadcaster),
))
}

/// Retrieve a list of known channels.
pub fn list_channels(&self) -> Vec<ChannelDetails> {
self.channel_manager.list_channels().into_iter().map(|c| c.into()).collect()
Expand Down Expand Up @@ -1205,6 +1307,22 @@ impl Node {
let sync_cman = Arc::clone(&self.channel_manager);
let sync_cmon = Arc::clone(&self.chain_monitor);
let sync_sweeper = Arc::clone(&self.output_sweeper);
let sync_logger = Arc::clone(&self.logger);
let sync_payjoin = &self.payjoin_handler.as_ref();
let mut confirmables = vec![
&*sync_cman as &(dyn Confirm + Sync + Send),
&*sync_cmon as &(dyn Confirm + Sync + Send),
&*sync_sweeper as &(dyn Confirm + Sync + Send),
];
if let Some(sync_payjoin) = sync_payjoin {
confirmables.push(sync_payjoin.as_ref() as &(dyn Confirm + Sync + Send));
}
let sync_wallet_timestamp = Arc::clone(&self.latest_wallet_sync_timestamp);
let sync_fee_rate_update_timestamp =
Arc::clone(&self.latest_fee_rate_cache_update_timestamp);
let sync_onchain_wallet_timestamp = Arc::clone(&self.latest_onchain_wallet_sync_timestamp);
let sync_monitor_archival_height = Arc::clone(&self.latest_channel_monitor_archival_height);

tokio::task::block_in_place(move || {
tokio::runtime::Builder::new_multi_thread().enable_all().build().unwrap().block_on(
async move {
Expand Down
49 changes: 49 additions & 0 deletions tests/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,41 @@ macro_rules! expect_payment_successful_event {

pub(crate) use expect_payment_successful_event;

macro_rules! expect_payjoin_tx_sent_successfully_event {
($node: expr, $is_original_psbt_modified: expr) => {{
match $node.wait_next_event() {
ref e @ Event::PayjoinPaymentSuccessful { txid, is_original_psbt_modified, .. } => {
println!("{} got event {:?}", $node.node_id(), e);
assert_eq!(is_original_psbt_modified, $is_original_psbt_modified);
$node.event_handled();
txid
},
ref e => {
panic!("{} got unexpected event!: {:?}", std::stringify!($node), e);
},
}
}};
}

pub(crate) use expect_payjoin_tx_sent_successfully_event;

macro_rules! expect_payjoin_await_confirmation {
($node: expr) => {{
match $node.wait_next_event() {
ref e @ Event::PayjoinPaymentAwaitingConfirmation { txid, .. } => {
println!("{} got event {:?}", $node.node_id(), e);
$node.event_handled();
txid
},
ref e => {
panic!("{} got unexpected event!: {:?}", std::stringify!($node), e);
},
}
}};
}

pub(crate) use expect_payjoin_await_confirmation;

pub(crate) fn setup_bitcoind_and_electrsd() -> (BitcoinD, ElectrsD) {
let bitcoind_exe =
env::var("BITCOIND_EXE").ok().or_else(|| bitcoind::downloaded_exe_path().ok()).expect(
Expand Down Expand Up @@ -317,6 +352,20 @@ pub(crate) fn setup_node(
node
}

pub(crate) fn setup_payjoin_node(electrsd: &ElectrsD, config: Config) -> TestNode {
let esplora_url = format!("http://{}", electrsd.esplora_url.as_ref().unwrap());
setup_builder!(builder, config);
builder.set_esplora_server(esplora_url.clone());
let payjoin_relay = "https://pj.bobspacebkk.com".to_string();
builder.set_payjoin_config(payjoin_relay).unwrap();
let test_sync_store = Arc::new(TestSyncStore::new(config.storage_dir_path.into()));
let node = builder.build_with_store(test_sync_store).unwrap();
node.start().unwrap();
assert!(node.status().is_running);
assert!(node.status().latest_fee_rate_cache_update_timestamp.is_some());
node
}

pub(crate) fn generate_blocks_and_wait<E: ElectrumApi>(
bitcoind: &BitcoindClient, electrs: &E, num: usize,
) {
Expand Down
Loading

0 comments on commit 9d94240

Please sign in to comment.