Skip to content

Commit db47845

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 77a0bbe commit db47845

16 files changed

+1333
-10
lines changed

Cargo.toml

+4
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ tokio = { version = "1.37", default-features = false, features = [ "rt-multi-thr
6969
esplora-client = { version = "0.6", default-features = false }
7070
libc = "0.2"
7171
uniffi = { version = "0.26.0", features = ["build"], optional = true }
72+
payjoin = { version = "0.16.0", default-features = false, features = ["send", "v2"] }
7273

7374
[target.'cfg(vss)'.dependencies]
7475
vss-client = "0.2"
@@ -85,6 +86,9 @@ bitcoincore-rpc = { version = "0.17.0", default-features = false }
8586
proptest = "1.0.0"
8687
regex = "1.5.6"
8788

89+
payjoin = { version = "0.16.0", default-features = false, features = ["send", "v2", "receive"] }
90+
reqwest = { version = "0.11", default-features = false, features = ["json", "rustls-tls", "blocking"] }
91+
8892
[target.'cfg(not(no_download))'.dev-dependencies]
8993
electrsd = { version = "0.26.0", features = ["legacy", "esplora_a33e97e1", "bitcoind_25_0"] }
9094

bindings/ldk_node.udl

+30
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ interface Node {
6464
SpontaneousPayment spontaneous_payment();
6565
OnchainPayment onchain_payment();
6666
UnifiedQrPayment unified_qr_payment();
67+
PayjoinPayment payjoin_payment();
6768
[Throws=NodeError]
6869
void connect(PublicKey node_id, SocketAddress address, boolean persist);
6970
[Throws=NodeError]
@@ -156,6 +157,13 @@ interface UnifiedQrPayment {
156157
QrPaymentResult send([ByRef]string uri_str);
157158
};
158159

160+
interface PayjoinPayment {
161+
[Throws=NodeError]
162+
void send(string payjoin_uri);
163+
[Throws=NodeError]
164+
void send_with_amount(string payjoin_uri, u64 amount_sats);
165+
};
166+
159167
[Error]
160168
enum NodeError {
161169
"AlreadyRunning",
@@ -206,6 +214,12 @@ enum NodeError {
206214
"InsufficientFunds",
207215
"LiquiditySourceUnavailable",
208216
"LiquidityFeeTooHigh",
217+
"PayjoinUnavailable",
218+
"PayjoinUriInvalid",
219+
"PayjoinRequestMissingAmount",
220+
"PayjoinRequestCreationFailed",
221+
"PayjoinRequestSendingFailed",
222+
"PayjoinResponseProcessingFailed",
209223
};
210224

211225
dictionary NodeStatus {
@@ -231,6 +245,7 @@ enum BuildError {
231245
"InvalidSystemTime",
232246
"InvalidChannelMonitor",
233247
"InvalidListeningAddresses",
248+
"InvalidPayjoinConfig",
234249
"ReadFailed",
235250
"WriteFailed",
236251
"StoragePathAccessFailed",
@@ -248,6 +263,11 @@ interface Event {
248263
ChannelPending(ChannelId channel_id, UserChannelId user_channel_id, ChannelId former_temporary_channel_id, PublicKey counterparty_node_id, OutPoint funding_txo);
249264
ChannelReady(ChannelId channel_id, UserChannelId user_channel_id, PublicKey? counterparty_node_id);
250265
ChannelClosed(ChannelId channel_id, UserChannelId user_channel_id, PublicKey? counterparty_node_id, ClosureReason? reason);
266+
PayjoinPaymentPending(Txid txid, u64 amount_sats, ScriptBuf recipient);
267+
PayjoinPaymentBroadcasted(Txid txid, u64 amount_sats, ScriptBuf recipient);
268+
PayjoinPaymentSuccessful(Txid txid, u64 amount_sats, ScriptBuf recipient);
269+
PayjoinPaymentFailed(Txid txid, u64 amount_sats, ScriptBuf recipient, PayjoinPaymentFailureReason reason);
270+
PayjoinPaymentOriginalPsbtBroadcasted(Txid txid, u64 amount_sats, ScriptBuf recipient);
251271
};
252272

253273
enum PaymentFailureReason {
@@ -259,6 +279,12 @@ enum PaymentFailureReason {
259279
"UnexpectedError",
260280
};
261281

282+
enum PayjoinPaymentFailureReason {
283+
"Timeout",
284+
"RequestSendingFailed",
285+
"ResponseProcessingFailed",
286+
};
287+
262288
[Enum]
263289
interface ClosureReason {
264290
CounterpartyForceClosed(UntrustedString peer_msg);
@@ -284,6 +310,7 @@ interface PaymentKind {
284310
Bolt12Offer(PaymentHash? hash, PaymentPreimage? preimage, PaymentSecret? secret, OfferId offer_id);
285311
Bolt12Refund(PaymentHash? hash, PaymentPreimage? preimage, PaymentSecret? secret);
286312
Spontaneous(PaymentHash hash, PaymentPreimage? preimage);
313+
Payjoin();
287314
};
288315

289316
[Enum]
@@ -516,3 +543,6 @@ typedef string Mnemonic;
516543

517544
[Custom]
518545
typedef string UntrustedString;
546+
547+
[Custom]
548+
typedef string ScriptBuf;

src/builder.rs

+45-2
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::payment::payjoin::handler::PayjoinHandler;
1415
use crate::payment::store::PaymentStore;
1516
use crate::peer_store::PeerStore;
1617
use crate::tx_broadcaster::TransactionBroadcaster;
@@ -93,6 +94,11 @@ struct LiquiditySourceConfig {
9394
lsps2_service: Option<(SocketAddress, PublicKey, Option<String>)>,
9495
}
9596

97+
#[derive(Debug, Clone)]
98+
struct PayjoinConfig {
99+
payjoin_relay: payjoin::Url,
100+
}
101+
96102
impl Default for LiquiditySourceConfig {
97103
fn default() -> Self {
98104
Self { lsps2_service: None }
@@ -132,6 +138,8 @@ pub enum BuildError {
132138
WalletSetupFailed,
133139
/// We failed to setup the logger.
134140
LoggerSetupFailed,
141+
/// Invalid Payjoin configuration.
142+
InvalidPayjoinConfig,
135143
}
136144

137145
impl fmt::Display for BuildError {
@@ -152,6 +160,10 @@ impl fmt::Display for BuildError {
152160
Self::KVStoreSetupFailed => write!(f, "Failed to setup KVStore."),
153161
Self::WalletSetupFailed => write!(f, "Failed to setup onchain wallet."),
154162
Self::LoggerSetupFailed => write!(f, "Failed to setup the logger."),
163+
Self::InvalidPayjoinConfig => write!(
164+
f,
165+
"Invalid Payjoin configuration. Make sure the provided arguments are valid URLs."
166+
),
155167
}
156168
}
157169
}
@@ -172,6 +184,7 @@ pub struct NodeBuilder {
172184
chain_data_source_config: Option<ChainDataSourceConfig>,
173185
gossip_source_config: Option<GossipSourceConfig>,
174186
liquidity_source_config: Option<LiquiditySourceConfig>,
187+
payjoin_config: Option<PayjoinConfig>,
175188
}
176189

177190
impl NodeBuilder {
@@ -187,12 +200,14 @@ impl NodeBuilder {
187200
let chain_data_source_config = None;
188201
let gossip_source_config = None;
189202
let liquidity_source_config = None;
203+
let payjoin_config = None;
190204
Self {
191205
config,
192206
entropy_source_config,
193207
chain_data_source_config,
194208
gossip_source_config,
195209
liquidity_source_config,
210+
payjoin_config,
196211
}
197212
}
198213

@@ -247,6 +262,14 @@ impl NodeBuilder {
247262
self
248263
}
249264

265+
/// Configures the [`Node`] instance to enable payjoin transactions.
266+
pub fn set_payjoin_config(&mut self, payjoin_relay: String) -> Result<&mut Self, BuildError> {
267+
let payjoin_relay =
268+
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,11 @@ 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(&self, payjoin_relay: String) -> Result<(), BuildError> {
483+
self.inner.write().unwrap().set_payjoin_config(payjoin_relay).map(|_| ())
484+
}
485+
456486
/// Configures the [`Node`] instance to source its gossip data from the given RapidGossipSync
457487
/// server.
458488
pub fn set_gossip_source_rgs(&self, rgs_server_url: String) {
@@ -521,8 +551,9 @@ impl ArcedNodeBuilder {
521551
fn build_with_store_internal(
522552
config: Arc<Config>, chain_data_source_config: Option<&ChainDataSourceConfig>,
523553
gossip_source_config: Option<&GossipSourceConfig>,
524-
liquidity_source_config: Option<&LiquiditySourceConfig>, seed_bytes: [u8; 64],
525-
logger: Arc<FilesystemLogger>, kv_store: Arc<DynStore>,
554+
liquidity_source_config: Option<&LiquiditySourceConfig>,
555+
payjoin_config: Option<&PayjoinConfig>, seed_bytes: [u8; 64], logger: Arc<FilesystemLogger>,
556+
kv_store: Arc<DynStore>,
526557
) -> Result<Node, BuildError> {
527558
// Initialize the on-chain wallet and chain access
528559
let xprv = bitcoin::bip32::ExtendedPrivKey::new_master(config.network.into(), &seed_bytes)
@@ -966,6 +997,17 @@ fn build_with_store_internal(
966997
let (stop_sender, _) = tokio::sync::watch::channel(());
967998
let (event_handling_stopped_sender, _) = tokio::sync::watch::channel(());
968999

1000+
let payjoin_handler = payjoin_config.map(|pj_config| {
1001+
Arc::new(PayjoinHandler::new(
1002+
Arc::clone(&tx_sync),
1003+
Arc::clone(&event_queue),
1004+
Arc::clone(&logger),
1005+
pj_config.payjoin_relay.clone(),
1006+
Arc::clone(&payment_store),
1007+
Arc::clone(&wallet),
1008+
))
1009+
});
1010+
9691011
let is_listening = Arc::new(AtomicBool::new(false));
9701012
let latest_wallet_sync_timestamp = Arc::new(RwLock::new(None));
9711013
let latest_onchain_wallet_sync_timestamp = Arc::new(RwLock::new(None));
@@ -987,6 +1029,7 @@ fn build_with_store_internal(
9871029
channel_manager,
9881030
chain_monitor,
9891031
output_sweeper,
1032+
payjoin_handler,
9901033
peer_manager,
9911034
connection_manager,
9921035
keys_manager,

src/config.rs

+9
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 a payjoin http request is considered timed out.
44+
pub(crate) const PAYJOIN_REQUEST_TIMEOUT: Duration = Duration::from_secs(30);
45+
46+
// The duration between retries of a payjoin http request.
47+
pub(crate) const PAYJOIN_RETRY_INTERVAL: Duration = Duration::from_secs(3);
48+
49+
// The total duration of retrying to send a payjoin http 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

+36
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,18 @@ pub enum Error {
9999
LiquiditySourceUnavailable,
100100
/// The given operation failed due to the LSP's required opening fee being too high.
101101
LiquidityFeeTooHigh,
102+
/// Failed to access Payjoin object.
103+
PayjoinUnavailable,
104+
/// Payjoin URI is invalid.
105+
PayjoinUriInvalid,
106+
/// Amount is neither user-provided nor defined in the URI.
107+
PayjoinRequestMissingAmount,
108+
/// Failed to build a Payjoin request.
109+
PayjoinRequestCreationFailed,
110+
/// Failed to send Payjoin request.
111+
PayjoinRequestSendingFailed,
112+
/// Payjoin response processing failed.
113+
PayjoinResponseProcessingFailed,
102114
}
103115

104116
impl fmt::Display for Error {
@@ -168,6 +180,30 @@ impl fmt::Display for Error {
168180
Self::LiquidityFeeTooHigh => {
169181
write!(f, "The given operation failed due to the LSP's required opening fee being too high.")
170182
},
183+
Self::PayjoinUnavailable => {
184+
write!(
185+
f,
186+
"Failed to access Payjoin object. Make sure you have enabled Payjoin support."
187+
)
188+
},
189+
Self::PayjoinRequestMissingAmount => {
190+
write!(
191+
f,
192+
"Amount is neither user-provided nor defined in the provided Payjoin URI."
193+
)
194+
},
195+
Self::PayjoinRequestCreationFailed => {
196+
write!(f, "Failed construct a Payjoin request")
197+
},
198+
Self::PayjoinUriInvalid => {
199+
write!(f, "The provided Payjoin URI is invalid")
200+
},
201+
Self::PayjoinRequestSendingFailed => {
202+
write!(f, "Failed to send Payjoin request")
203+
},
204+
Self::PayjoinResponseProcessingFailed => {
205+
write!(f, "Payjoin receiver responded to our request with an invalid response")
206+
},
171207
}
172208
}
173209
}

0 commit comments

Comments
 (0)