Skip to content

Commit 532c6ef

Browse files
committed
Add payjoin sender
Implements the payjoin sender part as describe in BIP77. This would allow the on chain wallet linked to LDK node to send payjoin transactions.
1 parent b7c4862 commit 532c6ef

File tree

7 files changed

+349
-0
lines changed

7 files changed

+349
-0
lines changed

Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ tokio = { version = "1", default-features = false, features = [ "rt-multi-thread
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.15.0", features = ["send", "receive", "v2"] }
7172

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

bindings/ldk_node.udl

+2
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,8 @@ enum NodeError {
151151
"InsufficientFunds",
152152
"LiquiditySourceUnavailable",
153153
"LiquidityFeeTooHigh",
154+
"PayjoinSender",
155+
"PayjoinRequestAmountMissing"
154156
};
155157

156158
dictionary NodeStatus {

src/builder.rs

+34
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use crate::io::sqlite_store::SqliteStore;
1111
use crate::liquidity::LiquiditySource;
1212
use crate::logger::{log_error, log_info, FilesystemLogger, Logger};
1313
use crate::message_handler::NodeCustomMessageHandler;
14+
use crate::payjoin_sender::PayjoinSender;
1415
use crate::payment::store::PaymentStore;
1516
use crate::peer_store::PeerStore;
1617
use crate::tx_broadcaster::TransactionBroadcaster;
@@ -94,6 +95,11 @@ struct LiquiditySourceConfig {
9495
lsps2_service: Option<(SocketAddress, PublicKey, Option<String>)>,
9596
}
9697

98+
#[derive(Debug, Clone)]
99+
struct PayjoinSenderConfig {
100+
payjoin_relay: payjoin::Url,
101+
}
102+
97103
impl Default for LiquiditySourceConfig {
98104
fn default() -> Self {
99105
Self { lsps2_service: None }
@@ -173,6 +179,7 @@ pub struct NodeBuilder {
173179
chain_data_source_config: Option<ChainDataSourceConfig>,
174180
gossip_source_config: Option<GossipSourceConfig>,
175181
liquidity_source_config: Option<LiquiditySourceConfig>,
182+
payjoin_sender_config: Option<PayjoinSenderConfig>,
176183
}
177184

178185
impl NodeBuilder {
@@ -188,12 +195,14 @@ impl NodeBuilder {
188195
let chain_data_source_config = None;
189196
let gossip_source_config = None;
190197
let liquidity_source_config = None;
198+
let payjoin_sender_config = None;
191199
Self {
192200
config,
193201
entropy_source_config,
194202
chain_data_source_config,
195203
gossip_source_config,
196204
liquidity_source_config,
205+
payjoin_sender_config,
197206
}
198207
}
199208

@@ -248,6 +257,12 @@ impl NodeBuilder {
248257
self
249258
}
250259

260+
/// Configures the [`Node`] instance to enable sending payjoin transactions.
261+
pub fn set_payjoin_sender_config(&mut self, payjoin_relay: payjoin::Url) -> &mut Self {
262+
self.payjoin_sender_config = Some(PayjoinSenderConfig { payjoin_relay });
263+
self
264+
}
265+
251266
/// Configures the [`Node`] instance to source its inbound liquidity from the given
252267
/// [LSPS2](https://github.com/BitcoinAndLightningLayerSpecs/lsp/blob/main/LSPS2/README.md)
253268
/// service.
@@ -369,6 +384,7 @@ impl NodeBuilder {
369384
seed_bytes,
370385
logger,
371386
vss_store,
387+
self.payjoin_sender_config.as_ref(),
372388
)
373389
}
374390

@@ -390,6 +406,7 @@ impl NodeBuilder {
390406
seed_bytes,
391407
logger,
392408
kv_store,
409+
self.payjoin_sender_config.as_ref(),
393410
)
394411
}
395412
}
@@ -454,6 +471,11 @@ impl ArcedNodeBuilder {
454471
self.inner.write().unwrap().set_gossip_source_p2p();
455472
}
456473

474+
/// Configures the [`Node`] instance to enable sending payjoin transactions.
475+
pub fn set_payjoin_sender_config(&self, payjoin_relay: payjoin::Url) {
476+
self.inner.write().unwrap().set_payjoin_sender_config(payjoin_relay);
477+
}
478+
457479
/// Configures the [`Node`] instance to source its gossip data from the given RapidGossipSync
458480
/// server.
459481
pub fn set_gossip_source_rgs(&self, rgs_server_url: String) {
@@ -524,6 +546,7 @@ fn build_with_store_internal(
524546
gossip_source_config: Option<&GossipSourceConfig>,
525547
liquidity_source_config: Option<&LiquiditySourceConfig>, seed_bytes: [u8; 64],
526548
logger: Arc<FilesystemLogger>, kv_store: Arc<DynStore>,
549+
payjoin_sender_config: Option<&PayjoinSenderConfig>,
527550
) -> Result<Node, BuildError> {
528551
// Initialize the on-chain wallet and chain access
529552
let xprv = bitcoin::bip32::ExtendedPrivKey::new_master(config.network.into(), &seed_bytes)
@@ -973,6 +996,16 @@ fn build_with_store_internal(
973996
};
974997

975998
let (stop_sender, _) = tokio::sync::watch::channel(());
999+
let payjoin_sender = if let Some(payjoin_sender_config) = payjoin_sender_config {
1000+
let payjoin_sender = PayjoinSender::new(
1001+
Arc::clone(&logger),
1002+
Arc::clone(&wallet),
1003+
&payjoin_sender_config.payjoin_relay,
1004+
);
1005+
Some(Arc::new(payjoin_sender))
1006+
} else {
1007+
None
1008+
};
9761009

9771010
let is_listening = Arc::new(AtomicBool::new(false));
9781011
let latest_wallet_sync_timestamp = Arc::new(RwLock::new(None));
@@ -993,6 +1026,7 @@ fn build_with_store_internal(
9931026
channel_manager,
9941027
chain_monitor,
9951028
output_sweeper,
1029+
payjoin_sender,
9961030
peer_manager,
9971031
connection_manager,
9981032
keys_manager,

src/error.rs

+8
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,10 @@ pub enum Error {
7171
LiquiditySourceUnavailable,
7272
/// The given operation failed due to the LSP's required opening fee being too high.
7373
LiquidityFeeTooHigh,
74+
/// Amount is not prvoided and neither defined in the URI.
75+
PayjoinRequestAmountMissing,
76+
/// Payjoin errors
77+
PayjoinSender,
7478
}
7579

7680
impl fmt::Display for Error {
@@ -122,6 +126,10 @@ impl fmt::Display for Error {
122126
Self::LiquidityFeeTooHigh => {
123127
write!(f, "The given operation failed due to the LSP's required opening fee being too high.")
124128
},
129+
Self::PayjoinSender => write!(f, "Failed to send payjoin."),
130+
Self::PayjoinRequestAmountMissing => {
131+
write!(f, "Amount is not provided and neither defined in the URI.")
132+
},
125133
}
126134
}
127135
}

src/lib.rs

+63
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ pub mod io;
8888
mod liquidity;
8989
mod logger;
9090
mod message_handler;
91+
mod payjoin_sender;
9192
pub mod payment;
9293
mod peer_store;
9394
mod sweep;
@@ -99,6 +100,7 @@ mod wallet;
99100

100101
pub use bip39;
101102
pub use bitcoin;
103+
use bitcoin::address::NetworkChecked;
102104
pub use lightning;
103105
pub use lightning_invoice;
104106

@@ -108,6 +110,7 @@ pub use error::Error as NodeError;
108110
use error::Error;
109111

110112
pub use event::Event;
113+
use payjoin_sender::PayjoinSender;
111114
pub use types::ChannelConfig;
112115

113116
pub use io::utils::generate_entropy_mnemonic;
@@ -181,6 +184,7 @@ pub struct Node {
181184
output_sweeper: Arc<Sweeper>,
182185
peer_manager: Arc<PeerManager>,
183186
connection_manager: Arc<ConnectionManager<Arc<FilesystemLogger>>>,
187+
payjoin_sender: Option<Arc<PayjoinSender<Arc<FilesystemLogger>>>>,
184188
keys_manager: Arc<KeysManager>,
185189
network_graph: Arc<NetworkGraph>,
186190
gossip_source: Arc<GossipSource>,
@@ -697,6 +701,65 @@ impl Node {
697701
Ok(())
698702
}
699703

704+
/// This method can be used to send a payjoin transaction as defined
705+
/// in BIP77.
706+
///
707+
/// The method will construct an `Original PSBT` from the data
708+
/// provided in the `payjoin_uri` and `amount` parameters. The
709+
/// amount must be set either in the `payjoin_uri` or in the
710+
/// `amount` parameter. If the amount is set in both, the paramter
711+
/// amount parameter will be used.
712+
///
713+
/// After constructing the `Original PSBT`, the method will
714+
/// extract the payjoin request data from the `Original PSBT`
715+
/// utilising the `payjoin` crate.
716+
///
717+
/// Then we start a background process to that will run for 1
718+
/// hour, polling the payjoin endpoint every 10 seconds. If an `OK` (ie status code == 200)
719+
/// is received, polling will stop and we will try to process the
720+
/// response from the payjoin receiver. If the response(or `Payjoin Proposal`) is valid, we will finalise the
721+
/// transaction and broadcast it to the network.
722+
///
723+
/// Notice that the `Txid` returned from this method is the
724+
/// `Original PSBT` transaction id, but the `Payjoin Proposal`
725+
/// transaction id could be different if the receiver changed the
726+
/// transaction.
727+
pub async fn send_payjoin_transaction(
728+
&self, payjoin_uri: payjoin::Uri<'static, NetworkChecked>, amount: Option<bitcoin::Amount>,
729+
) -> Result<Option<bitcoin::Txid>, Error> {
730+
let rt_lock = self.runtime.read().unwrap();
731+
if rt_lock.is_none() {
732+
return Err(Error::NotRunning);
733+
}
734+
let payjoin_sender = Arc::clone(self.payjoin_sender.as_ref().ok_or(Error::PayjoinSender)?);
735+
let original_psbt = payjoin_sender.create_payjoin_request(payjoin_uri.clone(), amount)?;
736+
let txid = original_psbt.clone().unsigned_tx.txid();
737+
let (request, context) =
738+
payjoin_sender.extract_request_data(payjoin_uri, original_psbt.clone())?;
739+
740+
let time = std::time::Instant::now();
741+
let runtime = rt_lock.as_ref().unwrap();
742+
runtime.spawn(async move {
743+
let response = payjoin_sender.poll(&request, time).await;
744+
if let Some(response) = response {
745+
let psbt = context.process_response(&mut response.as_slice());
746+
match psbt {
747+
Ok(Some(psbt)) => {
748+
let finalized =
749+
payjoin_sender.finalise_payjoin_tx(psbt, original_psbt.clone());
750+
if let Ok(txid) = finalized {
751+
let txid: bitcoin::Txid = txid.into();
752+
return Some(txid);
753+
}
754+
},
755+
_ => return None,
756+
}
757+
}
758+
None
759+
});
760+
Ok(Some(txid))
761+
}
762+
700763
/// Disconnects all peers, stops all running background tasks, and shuts down [`Node`].
701764
///
702765
/// After this returns most API methods will return [`Error::NotRunning`].

0 commit comments

Comments
 (0)