Skip to content

Commit b4d283f

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 0e02969 commit b4d283f

13 files changed

+641
-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", 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.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: 18 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]
@@ -140,6 +141,13 @@ interface OnchainPayment {
140141
Txid send_all_to_address([ByRef]Address address);
141142
};
142143

144+
interface PayjoinPayment {
145+
[Throws=NodeError]
146+
void send(string payjoin_uri);
147+
[Throws=NodeError]
148+
void send_with_amount(string payjoin_uri, u64 amount_sats);
149+
};
150+
143151
[Error]
144152
enum NodeError {
145153
"AlreadyRunning",
@@ -184,6 +192,13 @@ enum NodeError {
184192
"InsufficientFunds",
185193
"LiquiditySourceUnavailable",
186194
"LiquidityFeeTooHigh",
195+
"PayjoinSenderUnavailable",
196+
"PayjoinUriInvalid",
197+
"PayjoinNetworkMismatch",
198+
"PayjoinRequestMissingAmount",
199+
"PayjoinRequestCreationFailed",
200+
"PayjoinResponseProcessingFailed",
201+
"PayjoinRequestTimeout",
187202
};
188203

189204
dictionary NodeStatus {
@@ -215,6 +230,7 @@ enum BuildError {
215230
"KVStoreSetupFailed",
216231
"WalletSetupFailed",
217232
"LoggerSetupFailed",
233+
"InvalidPayjoinConfig",
218234
};
219235

220236
[Enum]
@@ -225,6 +241,8 @@ interface Event {
225241
ChannelPending(ChannelId channel_id, UserChannelId user_channel_id, ChannelId former_temporary_channel_id, PublicKey counterparty_node_id, OutPoint funding_txo);
226242
ChannelReady(ChannelId channel_id, UserChannelId user_channel_id, PublicKey? counterparty_node_id);
227243
ChannelClosed(ChannelId channel_id, UserChannelId user_channel_id, PublicKey? counterparty_node_id, ClosureReason? reason);
244+
PayjoinTxSendSuccess(Txid txid);
245+
PayjoinTxSendFailed(string reason);
228246
};
229247

230248
enum PaymentFailureReason {

src/builder.rs

Lines changed: 64 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,15 @@ struct LiquiditySourceConfig {
9393
lsps2_service: Option<(SocketAddress, PublicKey, Option<String>)>,
9494
}
9595

96+
#[derive(Debug, Clone)]
97+
struct PayjoinConfig {
98+
#[allow(dead_code)]
99+
payjoin_directory: String,
100+
payjoin_relay: String,
101+
#[allow(dead_code)]
102+
ohttp_keys: Option<String>,
103+
}
104+
96105
impl Default for LiquiditySourceConfig {
97106
fn default() -> Self {
98107
Self { lsps2_service: None }
@@ -132,6 +141,8 @@ pub enum BuildError {
132141
WalletSetupFailed,
133142
/// We failed to setup the logger.
134143
LoggerSetupFailed,
144+
/// Invalid Payjoin configuration.
145+
InvalidPayjoinConfig,
135146
}
136147

137148
impl fmt::Display for BuildError {
@@ -152,6 +163,10 @@ impl fmt::Display for BuildError {
152163
Self::KVStoreSetupFailed => write!(f, "Failed to setup KVStore."),
153164
Self::WalletSetupFailed => write!(f, "Failed to setup onchain wallet."),
154165
Self::LoggerSetupFailed => write!(f, "Failed to setup the logger."),
166+
Self::InvalidPayjoinConfig => write!(
167+
f,
168+
"Invalid Payjoin configuration. Make sure the provided arguments are valid URLs."
169+
),
155170
}
156171
}
157172
}
@@ -172,6 +187,7 @@ pub struct NodeBuilder {
172187
chain_data_source_config: Option<ChainDataSourceConfig>,
173188
gossip_source_config: Option<GossipSourceConfig>,
174189
liquidity_source_config: Option<LiquiditySourceConfig>,
190+
payjoin_config: Option<PayjoinConfig>,
175191
}
176192

177193
impl NodeBuilder {
@@ -187,12 +203,14 @@ impl NodeBuilder {
187203
let chain_data_source_config = None;
188204
let gossip_source_config = None;
189205
let liquidity_source_config = None;
206+
let payjoin_config = None;
190207
Self {
191208
config,
192209
entropy_source_config,
193210
chain_data_source_config,
194211
gossip_source_config,
195212
liquidity_source_config,
213+
payjoin_config,
196214
}
197215
}
198216

@@ -247,6 +265,14 @@ impl NodeBuilder {
247265
self
248266
}
249267

268+
/// Configures the [`Node`] instance to enable payjoin transactions.
269+
pub fn set_payjoin_config(
270+
&mut self, payjoin_directory: String, payjoin_relay: String, ohttp_keys: Option<String>,
271+
) -> &mut Self {
272+
self.payjoin_config = Some(PayjoinConfig { payjoin_directory, payjoin_relay, ohttp_keys });
273+
self
274+
}
275+
250276
/// Configures the [`Node`] instance to source its inbound liquidity from the given
251277
/// [LSPS2](https://github.com/BitcoinAndLightningLayerSpecs/lsp/blob/main/LSPS2/README.md)
252278
/// service.
@@ -365,6 +391,7 @@ impl NodeBuilder {
365391
self.chain_data_source_config.as_ref(),
366392
self.gossip_source_config.as_ref(),
367393
self.liquidity_source_config.as_ref(),
394+
self.payjoin_config.as_ref(),
368395
seed_bytes,
369396
logger,
370397
vss_store,
@@ -386,6 +413,7 @@ impl NodeBuilder {
386413
self.chain_data_source_config.as_ref(),
387414
self.gossip_source_config.as_ref(),
388415
self.liquidity_source_config.as_ref(),
416+
self.payjoin_config.as_ref(),
389417
seed_bytes,
390418
logger,
391419
kv_store,
@@ -453,6 +481,17 @@ impl ArcedNodeBuilder {
453481
self.inner.write().unwrap().set_gossip_source_p2p();
454482
}
455483

484+
/// Configures the [`Node`] instance to enable payjoin transactions.
485+
pub fn set_payjoin_config(
486+
&self, payjoin_directory: String, payjoin_relay: String, ohttp_keys: Option<String>,
487+
) {
488+
self.inner.write().unwrap().set_payjoin_config(
489+
payjoin_directory,
490+
payjoin_relay,
491+
ohttp_keys,
492+
);
493+
}
494+
456495
/// Configures the [`Node`] instance to source its gossip data from the given RapidGossipSync
457496
/// server.
458497
pub fn set_gossip_source_rgs(&self, rgs_server_url: String) {
@@ -521,8 +560,9 @@ impl ArcedNodeBuilder {
521560
fn build_with_store_internal(
522561
config: Arc<Config>, chain_data_source_config: Option<&ChainDataSourceConfig>,
523562
gossip_source_config: Option<&GossipSourceConfig>,
524-
liquidity_source_config: Option<&LiquiditySourceConfig>, seed_bytes: [u8; 64],
525-
logger: Arc<FilesystemLogger>, kv_store: Arc<DynStore>,
563+
liquidity_source_config: Option<&LiquiditySourceConfig>,
564+
payjoin_config: Option<&PayjoinConfig>, seed_bytes: [u8; 64], logger: Arc<FilesystemLogger>,
565+
kv_store: Arc<DynStore>,
526566
) -> Result<Node, BuildError> {
527567
// Initialize the on-chain wallet and chain access
528568
let xprv = bitcoin::bip32::ExtendedPrivKey::new_master(config.network.into(), &seed_bytes)
@@ -960,6 +1000,26 @@ fn build_with_store_internal(
9601000

9611001
let (stop_sender, _) = tokio::sync::watch::channel(());
9621002

1003+
let mut payjoin_sender = None;
1004+
if let Some((_payjoin_directory, payjoin_relay, _ohttp_keys)) = payjoin_config
1005+
.as_ref()
1006+
.map(|pc| (pc.payjoin_directory.clone(), pc.payjoin_relay.clone(), pc.ohttp_keys.clone()))
1007+
{
1008+
match PayjoinSender::new(
1009+
Arc::clone(&logger),
1010+
Arc::clone(&wallet),
1011+
Arc::clone(&tx_broadcaster),
1012+
&payjoin_relay,
1013+
) {
1014+
Ok(payjoin_sender_) => {
1015+
payjoin_sender = Some(Arc::new(payjoin_sender_));
1016+
},
1017+
Err(_) => {
1018+
return Err(BuildError::InvalidPayjoinConfig);
1019+
},
1020+
}
1021+
}
1022+
9631023
let is_listening = Arc::new(AtomicBool::new(false));
9641024
let latest_wallet_sync_timestamp = Arc::new(RwLock::new(None));
9651025
let latest_onchain_wallet_sync_timestamp = Arc::new(RwLock::new(None));
@@ -980,6 +1040,7 @@ fn build_with_store_internal(
9801040
channel_manager,
9811041
chain_monitor,
9821042
output_sweeper,
1043+
payjoin_sender,
9831044
peer_manager,
9841045
connection_manager,
9851046
keys_manager,

src/config.rs

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

40+
// The time before payjoin sender request timeout.
41+
pub(crate) const PAYJOIN_REQUEST_TIMEOUT: Duration = Duration::from_secs(30);
42+
43+
// The time before payjoin sender try to send the next request.
44+
pub(crate) const PAYJOIN_SLEEP_TIME_BETWEEN_REQUESTS: Duration = Duration::from_secs(3);
45+
46+
// The total time payjoin sender try to send a request.
47+
pub(crate) const PAYJOIN_REQUEST_TOTAL_DURATION: Duration = Duration::from_secs(3600);
48+
4049
// The time in-between RGS sync attempts.
4150
pub(crate) const RGS_SYNC_INTERVAL: Duration = Duration::from_secs(60 * 60);
4251

src/error.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,20 @@ pub enum Error {
8787
LiquiditySourceUnavailable,
8888
/// The given operation failed due to the LSP's required opening fee being too high.
8989
LiquidityFeeTooHigh,
90+
/// Failed to access Payjoin sender object.
91+
PayjoinSenderUnavailable,
92+
/// Payjoin URI is invalid.
93+
PayjoinUriInvalid,
94+
/// Payjoin URI network mismatch.
95+
PayjoinNetworkMismatch,
96+
/// Amount is neither user-provided nor defined in the URI.
97+
PayjoinRequestMissingAmount,
98+
/// Failed to build a Payjoin request.
99+
PayjoinRequestCreationFailed,
100+
/// Payjoin response processing failed.
101+
PayjoinResponseProcessingFailed,
102+
/// Payjoin request timed out.
103+
PayjoinRequestTimeout,
90104
}
91105

92106
impl fmt::Display for Error {
@@ -148,6 +162,30 @@ impl fmt::Display for Error {
148162
Self::LiquidityFeeTooHigh => {
149163
write!(f, "The given operation failed due to the LSP's required opening fee being too high.")
150164
},
165+
Self::PayjoinSenderUnavailable => {
166+
write!(f, "Failed to access Payjoin sender object. Make sure you have enabled Payjoin sending support.")
167+
},
168+
Self::PayjoinRequestMissingAmount => {
169+
write!(f, "Amount is neither user-provided nor defined in the URI.")
170+
},
171+
Self::PayjoinRequestCreationFailed => {
172+
write!(f, "Failed construct a Payjoin request")
173+
},
174+
Self::PayjoinUriInvalid => {
175+
write!(f, "The provided Payjoin URI is invalid")
176+
},
177+
Self::PayjoinNetworkMismatch => {
178+
write!(f, "The provided Payjoin URI does not match the node network.")
179+
},
180+
Self::PayjoinResponseProcessingFailed => {
181+
write!(f, "Payjoin receiver responded to our request with an invalid response that was ignored")
182+
},
183+
Self::PayjoinRequestTimeout => {
184+
write!(
185+
f,
186+
"Payjoin receiver did not respond to our request within the timeout period."
187+
)
188+
},
151189
}
152190
}
153191
}

src/event.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,21 @@ pub enum Event {
121121
/// This will be `None` for events serialized by LDK Node v0.2.1 and prior.
122122
reason: Option<ClosureReason>,
123123
},
124+
/// A Payjoin transaction has been successfully sent.
125+
///
126+
/// This event is emitted when we send a Payjoin transaction and it was accepted by the
127+
/// receiver, and then finalised and broadcasted by us.
128+
PayjoinTxSendSuccess {
129+
/// Transaction ID of the successfully sent Payjoin transaction.
130+
txid: bitcoin::Txid,
131+
},
132+
/// Failed to send Payjoin transaction.
133+
///
134+
/// This event is emitted when our attempt to send Payjoin transaction fail.
135+
PayjoinTxSendFailed {
136+
/// Reason for the failure.
137+
reason: String,
138+
},
124139
}
125140

126141
impl_writeable_tlv_based_enum!(Event,
@@ -156,6 +171,12 @@ impl_writeable_tlv_based_enum!(Event,
156171
(1, counterparty_node_id, option),
157172
(2, user_channel_id, required),
158173
(3, reason, upgradable_option),
174+
},
175+
(6, PayjoinTxSendSuccess) => {
176+
(0, txid, required),
177+
},
178+
(7, PayjoinTxSendFailed) => {
179+
(0, reason, required),
159180
};
160181
);
161182

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::*;

0 commit comments

Comments
 (0)