Skip to content

Commit

Permalink
Allow to send payjoin transactions
Browse files Browse the repository at this point in the history
Implements the payjoin sender as describe in BIP77.

This would allow the on chain wallet linked to LDK node to send payjoin
transactions.
  • Loading branch information
jbesraa committed Jun 24, 2024
1 parent 66fec69 commit 0c684a9
Show file tree
Hide file tree
Showing 13 changed files with 569 additions and 5 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ tokio = { version = "1.37", default-features = false, features = [ "rt-multi-thr
esplora-client = { version = "0.6", default-features = false }
libc = "0.2"
uniffi = { version = "0.26.0", features = ["build"], optional = true }
payjoin = { version = "0.16.0", default-features = false, features = ["send", "v2"] }

[target.'cfg(vss)'.dependencies]
vss-client = "0.2"
Expand Down
16 changes: 16 additions & 0 deletions bindings/ldk_node.udl
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ interface Node {
Bolt12Payment bolt12_payment();
SpontaneousPayment spontaneous_payment();
OnchainPayment onchain_payment();
PayjoinPayment payjoin_payment();
[Throws=NodeError]
void connect(PublicKey node_id, SocketAddress address, boolean persist);
[Throws=NodeError]
Expand Down Expand Up @@ -148,6 +149,13 @@ interface OnchainPayment {
Txid send_all_to_address([ByRef]Address address);
};

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 @@ -196,6 +204,11 @@ enum NodeError {
"InsufficientFunds",
"LiquiditySourceUnavailable",
"LiquidityFeeTooHigh",
"PayjoinUnavailable",
"PayjoinUriInvalid",
"PayjoinRequestMissingAmount",
"PayjoinRequestCreationFailed",
"PayjoinResponseProcessingFailed",
};

dictionary NodeStatus {
Expand Down Expand Up @@ -227,6 +240,7 @@ enum BuildError {
"KVStoreSetupFailed",
"WalletSetupFailed",
"LoggerSetupFailed",
"InvalidPayjoinConfig",
};

[Enum]
Expand All @@ -238,6 +252,8 @@ 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);
PayjoinTxSendSuccess(Txid txid);
PayjoinTxSendFailed(string reason);
};

enum PaymentFailureReason {
Expand Down
52 changes: 49 additions & 3 deletions src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use crate::peer_store::PeerStore;
use crate::tx_broadcaster::TransactionBroadcaster;
use crate::types::{
ChainMonitor, ChannelManager, DynStore, GossipSync, Graph, KeysManager, MessageRouter,
OnionMessenger, PeerManager,
OnionMessenger, PayjoinSender, PeerManager,
};
use crate::wallet::Wallet;
use crate::{LogLevel, Node};
Expand Down Expand Up @@ -93,6 +93,11 @@ struct LiquiditySourceConfig {
lsps2_service: Option<(SocketAddress, PublicKey, Option<String>)>,
}

#[derive(Debug, Clone)]
struct PayjoinConfig {
payjoin_relay: payjoin::Url,
}

impl Default for LiquiditySourceConfig {
fn default() -> Self {
Self { lsps2_service: None }
Expand Down Expand Up @@ -132,6 +137,8 @@ pub enum BuildError {
WalletSetupFailed,
/// We failed to setup the logger.
LoggerSetupFailed,
/// Invalid Payjoin configuration.
InvalidPayjoinConfig,
}

impl fmt::Display for BuildError {
Expand All @@ -152,6 +159,10 @@ impl fmt::Display for BuildError {
Self::KVStoreSetupFailed => write!(f, "Failed to setup KVStore."),
Self::WalletSetupFailed => write!(f, "Failed to setup onchain wallet."),
Self::LoggerSetupFailed => write!(f, "Failed to setup the logger."),
Self::InvalidPayjoinConfig => write!(
f,
"Invalid Payjoin configuration. Make sure the provided arguments are valid URLs."
),
}
}
}
Expand All @@ -172,6 +183,7 @@ pub struct NodeBuilder {
chain_data_source_config: Option<ChainDataSourceConfig>,
gossip_source_config: Option<GossipSourceConfig>,
liquidity_source_config: Option<LiquiditySourceConfig>,
payjoin_config: Option<PayjoinConfig>,
}

impl NodeBuilder {
Expand All @@ -187,12 +199,14 @@ impl NodeBuilder {
let chain_data_source_config = None;
let gossip_source_config = None;
let liquidity_source_config = None;
let payjoin_config = None;
Self {
config,
entropy_source_config,
chain_data_source_config,
gossip_source_config,
liquidity_source_config,
payjoin_config,
}
}

Expand Down Expand Up @@ -247,6 +261,15 @@ impl NodeBuilder {
self
}

/// Configures the [`Node`] instance to enable payjoin transactions.
pub fn set_payjoin_config(
&mut self, payjoin_relay: String
) -> Result<&mut Self, BuildError> {
let payjoin_relay = payjoin::Url::parse(&payjoin_relay).map_err(|_| BuildError::InvalidPayjoinConfig)?;
self.payjoin_config = Some(PayjoinConfig { payjoin_relay });
Ok(self)
}

/// Configures the [`Node`] instance to source its inbound liquidity from the given
/// [LSPS2](https://github.com/BitcoinAndLightningLayerSpecs/lsp/blob/main/LSPS2/README.md)
/// service.
Expand Down Expand Up @@ -365,6 +388,7 @@ impl NodeBuilder {
self.chain_data_source_config.as_ref(),
self.gossip_source_config.as_ref(),
self.liquidity_source_config.as_ref(),
self.payjoin_config.as_ref(),
seed_bytes,
logger,
vss_store,
Expand All @@ -386,6 +410,7 @@ impl NodeBuilder {
self.chain_data_source_config.as_ref(),
self.gossip_source_config.as_ref(),
self.liquidity_source_config.as_ref(),
self.payjoin_config.as_ref(),
seed_bytes,
logger,
kv_store,
Expand Down Expand Up @@ -453,6 +478,15 @@ impl ArcedNodeBuilder {
self.inner.write().unwrap().set_gossip_source_p2p();
}

/// Configures the [`Node`] instance to enable payjoin transactions.
pub fn set_payjoin_config(
&self, payjoin_relay: String,
) -> Result<(), BuildError> {
self.inner.write().unwrap().set_payjoin_config(
payjoin_relay,
).map(|_| ())
}

/// Configures the [`Node`] instance to source its gossip data from the given RapidGossipSync
/// server.
pub fn set_gossip_source_rgs(&self, rgs_server_url: String) {
Expand Down Expand Up @@ -521,8 +555,9 @@ impl ArcedNodeBuilder {
fn build_with_store_internal(
config: Arc<Config>, chain_data_source_config: Option<&ChainDataSourceConfig>,
gossip_source_config: Option<&GossipSourceConfig>,
liquidity_source_config: Option<&LiquiditySourceConfig>, seed_bytes: [u8; 64],
logger: Arc<FilesystemLogger>, kv_store: Arc<DynStore>,
liquidity_source_config: Option<&LiquiditySourceConfig>,
payjoin_config: Option<&PayjoinConfig>, seed_bytes: [u8; 64], logger: Arc<FilesystemLogger>,
kv_store: Arc<DynStore>,
) -> Result<Node, BuildError> {
// Initialize the on-chain wallet and chain access
let xprv = bitcoin::bip32::ExtendedPrivKey::new_master(config.network.into(), &seed_bytes)
Expand Down Expand Up @@ -966,6 +1001,16 @@ fn build_with_store_internal(
let (stop_sender, _) = tokio::sync::watch::channel(());
let (event_handling_stopped_sender, _) = tokio::sync::watch::channel(());

let mut payjoin_sender = None;
if let Some(pj_config) = payjoin_config {
payjoin_sender = Some(Arc::new(PayjoinSender::new(
Arc::clone(&logger),
Arc::clone(&wallet),
Arc::clone(&tx_broadcaster),
pj_config.payjoin_relay.clone(),
)));
}

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));
Expand All @@ -987,6 +1032,7 @@ fn build_with_store_internal(
channel_manager,
chain_monitor,
output_sweeper,
payjoin_sender,
peer_manager,
connection_manager,
keys_manager,
Expand Down
9 changes: 9 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,15 @@ pub(crate) const RESOLVED_CHANNEL_MONITOR_ARCHIVAL_INTERVAL: u32 = 6;
// The time in-between peer reconnection attempts.
pub(crate) const PEER_RECONNECTION_INTERVAL: Duration = Duration::from_secs(10);

// The time before payjoin sender requests timeout.
pub(crate) const PAYJOIN_REQUEST_TIMEOUT: Duration = Duration::from_secs(30);

// The time before payjoin sender try to send the next request.
pub(crate) const PAYJOIN_RETRY_INTERVAL: Duration = Duration::from_secs(3);

// The total time payjoin sender try to send a request.
pub(crate) const PAYJOIN_REQUEST_TOTAL_DURATION: Duration = Duration::from_secs(24 * 60 * 60);

// The time in-between RGS sync attempts.
pub(crate) const RGS_SYNC_INTERVAL: Duration = Duration::from_secs(60 * 60);

Expand Down
25 changes: 25 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,16 @@ pub enum Error {
LiquiditySourceUnavailable,
/// The given operation failed due to the LSP's required opening fee being too high.
LiquidityFeeTooHigh,
/// Failed to access Payjoin sender object.
PayjoinUnavailable,
/// Payjoin URI is invalid.
PayjoinUriInvalid,
/// Amount is neither user-provided nor defined in the URI.
PayjoinRequestMissingAmount,
/// Failed to build a Payjoin request.
PayjoinRequestCreationFailed,
/// Payjoin response processing failed.
PayjoinResponseProcessingFailed,
}

impl fmt::Display for Error {
Expand Down Expand Up @@ -162,6 +172,21 @@ impl fmt::Display for Error {
Self::LiquidityFeeTooHigh => {
write!(f, "The given operation failed due to the LSP's required opening fee being too high.")
},
Self::PayjoinUnavailable => {
write!(f, "Failed to access Payjoin sender object. Make sure you have enabled Payjoin sending support.")
},
Self::PayjoinRequestMissingAmount => {
write!(f, "Amount is neither user-provided nor defined in the URI.")
},
Self::PayjoinRequestCreationFailed => {
write!(f, "Failed construct a Payjoin request")
},
Self::PayjoinUriInvalid => {
write!(f, "The provided Payjoin URI is invalid")
},
Self::PayjoinResponseProcessingFailed => {
write!(f, "Payjoin receiver responded to our request with an invalid response that was ignored")
},
}
}
}
Expand Down
21 changes: 21 additions & 0 deletions src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,21 @@ pub enum Event {
/// This will be `None` for events serialized by LDK Node v0.2.1 and prior.
reason: Option<ClosureReason>,
},
/// A Payjoin transaction has been successfully sent.
///
/// This event is emitted when we send a Payjoin transaction and it was accepted by the
/// receiver, and then finalised and broadcasted by us.
PayjoinTxSendSuccess {
/// Transaction ID of the successfully sent Payjoin transaction.
txid: bitcoin::Txid,
},
/// Failed to send Payjoin transaction.
///
/// This event is emitted when our attempt to send Payjoin transaction fail.
PayjoinTxSendFailed {
/// Reason for the failure.
reason: String,
},
}

impl_writeable_tlv_based_enum!(Event,
Expand Down Expand Up @@ -184,6 +199,12 @@ impl_writeable_tlv_based_enum!(Event,
(2, payment_id, required),
(4, claimable_amount_msat, required),
(6, claim_deadline, option),
},
(7, PayjoinTxSendSuccess) => {
(0, txid, required),
},
(8, PayjoinTxSendFailed) => {
(0, reason, required),
};
);

Expand Down
9 changes: 9 additions & 0 deletions src/io/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,15 @@ pub(crate) fn check_namespace_key_validity(
Ok(())
}

pub(crate) fn ohttp_headers() -> reqwest::header::HeaderMap {
let mut headers = reqwest::header::HeaderMap::new();
headers.insert(
reqwest::header::CONTENT_TYPE,
reqwest::header::HeaderValue::from_static("message/ohttp-req"),
);
headers
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
43 changes: 41 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ pub mod io;
mod liquidity;
mod logger;
mod message_handler;
mod payjoin_sender;
pub mod payment;
mod peer_store;
mod sweep;
Expand Down Expand Up @@ -133,11 +134,14 @@ use gossip::GossipSource;
use graph::NetworkGraph;
use liquidity::LiquiditySource;
use payment::store::PaymentStore;
use payment::{Bolt11Payment, Bolt12Payment, OnchainPayment, PaymentDetails, SpontaneousPayment};
use payment::{
Bolt11Payment, Bolt12Payment, OnchainPayment, PayjoinPayment, PaymentDetails,
SpontaneousPayment,
};
use peer_store::{PeerInfo, PeerStore};
use types::{
Broadcaster, BumpTransactionEventHandler, ChainMonitor, ChannelManager, DynStore, FeeEstimator,
Graph, KeysManager, PeerManager, Router, Scorer, Sweeper, Wallet,
Graph, KeysManager, PayjoinSender, PeerManager, Router, Scorer, Sweeper, Wallet,
};
pub use types::{ChannelDetails, PeerDetails, UserChannelId};

Expand Down Expand Up @@ -185,6 +189,7 @@ pub struct Node {
output_sweeper: Arc<Sweeper>,
peer_manager: Arc<PeerManager>,
connection_manager: Arc<ConnectionManager<Arc<FilesystemLogger>>>,
payjoin_sender: Option<Arc<PayjoinSender>>,
keys_manager: Arc<KeysManager>,
network_graph: Arc<Graph>,
gossip_source: Arc<GossipSource>,
Expand Down Expand Up @@ -1063,6 +1068,40 @@ impl Node {
))
}

/// Returns a payment handler allowing to send payjoin payments.
///
/// In order to utilize the Payjoin functionality, it's necessary
/// to configure your node 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_sender = self.payjoin_sender.as_ref();
PayjoinPayment::new(
Arc::clone(&self.runtime),
payjoin_sender.map(Arc::clone),
Arc::clone(&self.config),
Arc::clone(&self.event_queue),
)
}

/// Returns a payment handler allowing to send payjoin payments.
///
/// In order to utilize the Payjoin functionality, it's necessary
/// to configure your node 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_sender = self.payjoin_sender.as_ref();
Arc::new(PayjoinPayment::new(
Arc::clone(&self.runtime),
payjoin_sender.map(Arc::clone),
Arc::clone(&self.config),
Arc::clone(&self.event_queue),
))
}

/// 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
Loading

0 comments on commit 0c684a9

Please sign in to comment.