Skip to content

Commit 0c684a9

Browse files
committed
Allow to send payjoin transactions
Implements the payjoin sender as describe in BIP77. This would allow the on chain wallet linked to LDK node to send payjoin transactions.
1 parent 66fec69 commit 0c684a9

13 files changed

+569
-5
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ tokio = { version = "1.37", default-features = false, features = [ "rt-multi-thr
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.16.0", default-features = false, features = ["send", "v2"] }
7172

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

bindings/ldk_node.udl

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ interface Node {
6363
Bolt12Payment bolt12_payment();
6464
SpontaneousPayment spontaneous_payment();
6565
OnchainPayment onchain_payment();
66+
PayjoinPayment payjoin_payment();
6667
[Throws=NodeError]
6768
void connect(PublicKey node_id, SocketAddress address, boolean persist);
6869
[Throws=NodeError]
@@ -148,6 +149,13 @@ interface OnchainPayment {
148149
Txid send_all_to_address([ByRef]Address address);
149150
};
150151

152+
interface PayjoinPayment {
153+
[Throws=NodeError]
154+
void send(string payjoin_uri);
155+
[Throws=NodeError]
156+
void send_with_amount(string payjoin_uri, u64 amount_sats);
157+
};
158+
151159
[Error]
152160
enum NodeError {
153161
"AlreadyRunning",
@@ -196,6 +204,11 @@ enum NodeError {
196204
"InsufficientFunds",
197205
"LiquiditySourceUnavailable",
198206
"LiquidityFeeTooHigh",
207+
"PayjoinUnavailable",
208+
"PayjoinUriInvalid",
209+
"PayjoinRequestMissingAmount",
210+
"PayjoinRequestCreationFailed",
211+
"PayjoinResponseProcessingFailed",
199212
};
200213

201214
dictionary NodeStatus {
@@ -227,6 +240,7 @@ enum BuildError {
227240
"KVStoreSetupFailed",
228241
"WalletSetupFailed",
229242
"LoggerSetupFailed",
243+
"InvalidPayjoinConfig",
230244
};
231245

232246
[Enum]
@@ -238,6 +252,8 @@ interface Event {
238252
ChannelPending(ChannelId channel_id, UserChannelId user_channel_id, ChannelId former_temporary_channel_id, PublicKey counterparty_node_id, OutPoint funding_txo);
239253
ChannelReady(ChannelId channel_id, UserChannelId user_channel_id, PublicKey? counterparty_node_id);
240254
ChannelClosed(ChannelId channel_id, UserChannelId user_channel_id, PublicKey? counterparty_node_id, ClosureReason? reason);
255+
PayjoinTxSendSuccess(Txid txid);
256+
PayjoinTxSendFailed(string reason);
241257
};
242258

243259
enum PaymentFailureReason {

src/builder.rs

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ use crate::peer_store::PeerStore;
1616
use crate::tx_broadcaster::TransactionBroadcaster;
1717
use crate::types::{
1818
ChainMonitor, ChannelManager, DynStore, GossipSync, Graph, KeysManager, MessageRouter,
19-
OnionMessenger, PeerManager,
19+
OnionMessenger, PayjoinSender, PeerManager,
2020
};
2121
use crate::wallet::Wallet;
2222
use crate::{LogLevel, Node};
@@ -93,6 +93,11 @@ struct LiquiditySourceConfig {
9393
lsps2_service: Option<(SocketAddress, PublicKey, Option<String>)>,
9494
}
9595

96+
#[derive(Debug, Clone)]
97+
struct PayjoinConfig {
98+
payjoin_relay: payjoin::Url,
99+
}
100+
96101
impl Default for LiquiditySourceConfig {
97102
fn default() -> Self {
98103
Self { lsps2_service: None }
@@ -132,6 +137,8 @@ pub enum BuildError {
132137
WalletSetupFailed,
133138
/// We failed to setup the logger.
134139
LoggerSetupFailed,
140+
/// Invalid Payjoin configuration.
141+
InvalidPayjoinConfig,
135142
}
136143

137144
impl fmt::Display for BuildError {
@@ -152,6 +159,10 @@ impl fmt::Display for BuildError {
152159
Self::KVStoreSetupFailed => write!(f, "Failed to setup KVStore."),
153160
Self::WalletSetupFailed => write!(f, "Failed to setup onchain wallet."),
154161
Self::LoggerSetupFailed => write!(f, "Failed to setup the logger."),
162+
Self::InvalidPayjoinConfig => write!(
163+
f,
164+
"Invalid Payjoin configuration. Make sure the provided arguments are valid URLs."
165+
),
155166
}
156167
}
157168
}
@@ -172,6 +183,7 @@ pub struct NodeBuilder {
172183
chain_data_source_config: Option<ChainDataSourceConfig>,
173184
gossip_source_config: Option<GossipSourceConfig>,
174185
liquidity_source_config: Option<LiquiditySourceConfig>,
186+
payjoin_config: Option<PayjoinConfig>,
175187
}
176188

177189
impl NodeBuilder {
@@ -187,12 +199,14 @@ impl NodeBuilder {
187199
let chain_data_source_config = None;
188200
let gossip_source_config = None;
189201
let liquidity_source_config = None;
202+
let payjoin_config = None;
190203
Self {
191204
config,
192205
entropy_source_config,
193206
chain_data_source_config,
194207
gossip_source_config,
195208
liquidity_source_config,
209+
payjoin_config,
196210
}
197211
}
198212

@@ -247,6 +261,15 @@ impl NodeBuilder {
247261
self
248262
}
249263

264+
/// Configures the [`Node`] instance to enable payjoin transactions.
265+
pub fn set_payjoin_config(
266+
&mut self, payjoin_relay: String
267+
) -> Result<&mut Self, BuildError> {
268+
let payjoin_relay = payjoin::Url::parse(&payjoin_relay).map_err(|_| BuildError::InvalidPayjoinConfig)?;
269+
self.payjoin_config = Some(PayjoinConfig { payjoin_relay });
270+
Ok(self)
271+
}
272+
250273
/// Configures the [`Node`] instance to source its inbound liquidity from the given
251274
/// [LSPS2](https://github.com/BitcoinAndLightningLayerSpecs/lsp/blob/main/LSPS2/README.md)
252275
/// service.
@@ -365,6 +388,7 @@ impl NodeBuilder {
365388
self.chain_data_source_config.as_ref(),
366389
self.gossip_source_config.as_ref(),
367390
self.liquidity_source_config.as_ref(),
391+
self.payjoin_config.as_ref(),
368392
seed_bytes,
369393
logger,
370394
vss_store,
@@ -386,6 +410,7 @@ impl NodeBuilder {
386410
self.chain_data_source_config.as_ref(),
387411
self.gossip_source_config.as_ref(),
388412
self.liquidity_source_config.as_ref(),
413+
self.payjoin_config.as_ref(),
389414
seed_bytes,
390415
logger,
391416
kv_store,
@@ -453,6 +478,15 @@ impl ArcedNodeBuilder {
453478
self.inner.write().unwrap().set_gossip_source_p2p();
454479
}
455480

481+
/// Configures the [`Node`] instance to enable payjoin transactions.
482+
pub fn set_payjoin_config(
483+
&self, payjoin_relay: String,
484+
) -> Result<(), BuildError> {
485+
self.inner.write().unwrap().set_payjoin_config(
486+
payjoin_relay,
487+
).map(|_| ())
488+
}
489+
456490
/// Configures the [`Node`] instance to source its gossip data from the given RapidGossipSync
457491
/// server.
458492
pub fn set_gossip_source_rgs(&self, rgs_server_url: String) {
@@ -521,8 +555,9 @@ impl ArcedNodeBuilder {
521555
fn build_with_store_internal(
522556
config: Arc<Config>, chain_data_source_config: Option<&ChainDataSourceConfig>,
523557
gossip_source_config: Option<&GossipSourceConfig>,
524-
liquidity_source_config: Option<&LiquiditySourceConfig>, seed_bytes: [u8; 64],
525-
logger: Arc<FilesystemLogger>, kv_store: Arc<DynStore>,
558+
liquidity_source_config: Option<&LiquiditySourceConfig>,
559+
payjoin_config: Option<&PayjoinConfig>, seed_bytes: [u8; 64], logger: Arc<FilesystemLogger>,
560+
kv_store: Arc<DynStore>,
526561
) -> Result<Node, BuildError> {
527562
// Initialize the on-chain wallet and chain access
528563
let xprv = bitcoin::bip32::ExtendedPrivKey::new_master(config.network.into(), &seed_bytes)
@@ -966,6 +1001,16 @@ fn build_with_store_internal(
9661001
let (stop_sender, _) = tokio::sync::watch::channel(());
9671002
let (event_handling_stopped_sender, _) = tokio::sync::watch::channel(());
9681003

1004+
let mut payjoin_sender = None;
1005+
if let Some(pj_config) = payjoin_config {
1006+
payjoin_sender = Some(Arc::new(PayjoinSender::new(
1007+
Arc::clone(&logger),
1008+
Arc::clone(&wallet),
1009+
Arc::clone(&tx_broadcaster),
1010+
pj_config.payjoin_relay.clone(),
1011+
)));
1012+
}
1013+
9691014
let is_listening = Arc::new(AtomicBool::new(false));
9701015
let latest_wallet_sync_timestamp = Arc::new(RwLock::new(None));
9711016
let latest_onchain_wallet_sync_timestamp = Arc::new(RwLock::new(None));
@@ -987,6 +1032,7 @@ fn build_with_store_internal(
9871032
channel_manager,
9881033
chain_monitor,
9891034
output_sweeper,
1035+
payjoin_sender,
9901036
peer_manager,
9911037
connection_manager,
9921038
keys_manager,

src/config.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,15 @@ pub(crate) const RESOLVED_CHANNEL_MONITOR_ARCHIVAL_INTERVAL: u32 = 6;
4040
// The time in-between peer reconnection attempts.
4141
pub(crate) const PEER_RECONNECTION_INTERVAL: Duration = Duration::from_secs(10);
4242

43+
// The time before payjoin sender requests timeout.
44+
pub(crate) const PAYJOIN_REQUEST_TIMEOUT: Duration = Duration::from_secs(30);
45+
46+
// The time before payjoin sender try to send the next request.
47+
pub(crate) const PAYJOIN_RETRY_INTERVAL: Duration = Duration::from_secs(3);
48+
49+
// The total time payjoin sender try to send a request.
50+
pub(crate) const PAYJOIN_REQUEST_TOTAL_DURATION: Duration = Duration::from_secs(24 * 60 * 60);
51+
4352
// The time in-between RGS sync attempts.
4453
pub(crate) const RGS_SYNC_INTERVAL: Duration = Duration::from_secs(60 * 60);
4554

src/error.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,16 @@ pub enum Error {
9595
LiquiditySourceUnavailable,
9696
/// The given operation failed due to the LSP's required opening fee being too high.
9797
LiquidityFeeTooHigh,
98+
/// Failed to access Payjoin sender object.
99+
PayjoinUnavailable,
100+
/// Payjoin URI is invalid.
101+
PayjoinUriInvalid,
102+
/// Amount is neither user-provided nor defined in the URI.
103+
PayjoinRequestMissingAmount,
104+
/// Failed to build a Payjoin request.
105+
PayjoinRequestCreationFailed,
106+
/// Payjoin response processing failed.
107+
PayjoinResponseProcessingFailed,
98108
}
99109

100110
impl fmt::Display for Error {
@@ -162,6 +172,21 @@ impl fmt::Display for Error {
162172
Self::LiquidityFeeTooHigh => {
163173
write!(f, "The given operation failed due to the LSP's required opening fee being too high.")
164174
},
175+
Self::PayjoinUnavailable => {
176+
write!(f, "Failed to access Payjoin sender object. Make sure you have enabled Payjoin sending support.")
177+
},
178+
Self::PayjoinRequestMissingAmount => {
179+
write!(f, "Amount is neither user-provided nor defined in the URI.")
180+
},
181+
Self::PayjoinRequestCreationFailed => {
182+
write!(f, "Failed construct a Payjoin request")
183+
},
184+
Self::PayjoinUriInvalid => {
185+
write!(f, "The provided Payjoin URI is invalid")
186+
},
187+
Self::PayjoinResponseProcessingFailed => {
188+
write!(f, "Payjoin receiver responded to our request with an invalid response that was ignored")
189+
},
165190
}
166191
}
167192
}

src/event.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,21 @@ pub enum Event {
143143
/// This will be `None` for events serialized by LDK Node v0.2.1 and prior.
144144
reason: Option<ClosureReason>,
145145
},
146+
/// A Payjoin transaction has been successfully sent.
147+
///
148+
/// This event is emitted when we send a Payjoin transaction and it was accepted by the
149+
/// receiver, and then finalised and broadcasted by us.
150+
PayjoinTxSendSuccess {
151+
/// Transaction ID of the successfully sent Payjoin transaction.
152+
txid: bitcoin::Txid,
153+
},
154+
/// Failed to send Payjoin transaction.
155+
///
156+
/// This event is emitted when our attempt to send Payjoin transaction fail.
157+
PayjoinTxSendFailed {
158+
/// Reason for the failure.
159+
reason: String,
160+
},
146161
}
147162

148163
impl_writeable_tlv_based_enum!(Event,
@@ -184,6 +199,12 @@ impl_writeable_tlv_based_enum!(Event,
184199
(2, payment_id, required),
185200
(4, claimable_amount_msat, required),
186201
(6, claim_deadline, option),
202+
},
203+
(7, PayjoinTxSendSuccess) => {
204+
(0, txid, required),
205+
},
206+
(8, PayjoinTxSendFailed) => {
207+
(0, reason, required),
187208
};
188209
);
189210

src/io/utils.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -511,6 +511,15 @@ pub(crate) fn check_namespace_key_validity(
511511
Ok(())
512512
}
513513

514+
pub(crate) fn ohttp_headers() -> reqwest::header::HeaderMap {
515+
let mut headers = reqwest::header::HeaderMap::new();
516+
headers.insert(
517+
reqwest::header::CONTENT_TYPE,
518+
reqwest::header::HeaderValue::from_static("message/ohttp-req"),
519+
);
520+
headers
521+
}
522+
514523
#[cfg(test)]
515524
mod tests {
516525
use super::*;

src/lib.rs

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ pub mod io;
8989
mod liquidity;
9090
mod logger;
9191
mod message_handler;
92+
mod payjoin_sender;
9293
pub mod payment;
9394
mod peer_store;
9495
mod sweep;
@@ -133,11 +134,14 @@ use gossip::GossipSource;
133134
use graph::NetworkGraph;
134135
use liquidity::LiquiditySource;
135136
use payment::store::PaymentStore;
136-
use payment::{Bolt11Payment, Bolt12Payment, OnchainPayment, PaymentDetails, SpontaneousPayment};
137+
use payment::{
138+
Bolt11Payment, Bolt12Payment, OnchainPayment, PayjoinPayment, PaymentDetails,
139+
SpontaneousPayment,
140+
};
137141
use peer_store::{PeerInfo, PeerStore};
138142
use types::{
139143
Broadcaster, BumpTransactionEventHandler, ChainMonitor, ChannelManager, DynStore, FeeEstimator,
140-
Graph, KeysManager, PeerManager, Router, Scorer, Sweeper, Wallet,
144+
Graph, KeysManager, PayjoinSender, PeerManager, Router, Scorer, Sweeper, Wallet,
141145
};
142146
pub use types::{ChannelDetails, PeerDetails, UserChannelId};
143147

@@ -185,6 +189,7 @@ pub struct Node {
185189
output_sweeper: Arc<Sweeper>,
186190
peer_manager: Arc<PeerManager>,
187191
connection_manager: Arc<ConnectionManager<Arc<FilesystemLogger>>>,
192+
payjoin_sender: Option<Arc<PayjoinSender>>,
188193
keys_manager: Arc<KeysManager>,
189194
network_graph: Arc<Graph>,
190195
gossip_source: Arc<GossipSource>,
@@ -1063,6 +1068,40 @@ impl Node {
10631068
))
10641069
}
10651070

1071+
/// Returns a payment handler allowing to send payjoin payments.
1072+
///
1073+
/// In order to utilize the Payjoin functionality, it's necessary
1074+
/// to configure your node using [`set_payjoin_config`].
1075+
///
1076+
/// [`set_payjoin_config`]: crate::builder::NodeBuilder::set_payjoin_config
1077+
#[cfg(not(feature = "uniffi"))]
1078+
pub fn payjoin_payment(&self) -> PayjoinPayment {
1079+
let payjoin_sender = self.payjoin_sender.as_ref();
1080+
PayjoinPayment::new(
1081+
Arc::clone(&self.runtime),
1082+
payjoin_sender.map(Arc::clone),
1083+
Arc::clone(&self.config),
1084+
Arc::clone(&self.event_queue),
1085+
)
1086+
}
1087+
1088+
/// Returns a payment handler allowing to send payjoin payments.
1089+
///
1090+
/// In order to utilize the Payjoin functionality, it's necessary
1091+
/// to configure your node using [`set_payjoin_config`].
1092+
///
1093+
/// [`set_payjoin_config`]: crate::builder::NodeBuilder::set_payjoin_config
1094+
#[cfg(feature = "uniffi")]
1095+
pub fn payjoin_payment(&self) -> Arc<PayjoinPayment> {
1096+
let payjoin_sender = self.payjoin_sender.as_ref();
1097+
Arc::new(PayjoinPayment::new(
1098+
Arc::clone(&self.runtime),
1099+
payjoin_sender.map(Arc::clone),
1100+
Arc::clone(&self.config),
1101+
Arc::clone(&self.event_queue),
1102+
))
1103+
}
1104+
10661105
/// Retrieve a list of known channels.
10671106
pub fn list_channels(&self) -> Vec<ChannelDetails> {
10681107
self.channel_manager.list_channels().into_iter().map(|c| c.into()).collect()

0 commit comments

Comments
 (0)