Skip to content

Commit 01e8ab8

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 6ea269c commit 01e8ab8

File tree

14 files changed

+830
-6
lines changed

14 files changed

+830
-6
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: 28 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,9 @@ 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+
PayjoinPaymentPending(Txid txid, u64 amount, ScriptBuf receipient);
256+
PayjoinPaymentSuccess(Txid txid, u64 amount, ScriptBuf receipient);
257+
PayjoinPaymentFailed(Txid? txid, u64 amount, ScriptBuf receipient, PayjoinPaymentFailureReason reason);
241258
};
242259

243260
enum PaymentFailureReason {
@@ -249,6 +266,13 @@ enum PaymentFailureReason {
249266
"UnexpectedError",
250267
};
251268

269+
enum PayjoinPaymentFailureReason {
270+
"Timeout",
271+
"TransactionFinalisationFailed",
272+
"InvalidReceiverResponse",
273+
};
274+
275+
252276
[Enum]
253277
interface ClosureReason {
254278
CounterpartyForceClosed(UntrustedString peer_msg);
@@ -274,6 +298,7 @@ interface PaymentKind {
274298
Bolt12Offer(PaymentHash? hash, PaymentPreimage? preimage, PaymentSecret? secret, OfferId offer_id);
275299
Bolt12Refund(PaymentHash? hash, PaymentPreimage? preimage, PaymentSecret? secret);
276300
Spontaneous(PaymentHash hash, PaymentPreimage? preimage);
301+
Payjoin();
277302
};
278303

279304
enum PaymentDirection {
@@ -499,3 +524,6 @@ typedef string Mnemonic;
499524

500525
[Custom]
501526
typedef string UntrustedString;
527+
528+
[Custom]
529+
typedef string ScriptBuf;

src/builder.rs

Lines changed: 47 additions & 2 deletions
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,19 @@ 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 mut payjoin_handler = None;
1001+
if let Some(pj_config) = payjoin_config {
1002+
payjoin_handler = Some(Arc::new(PayjoinHandler::new(
1003+
Arc::clone(&tx_broadcaster),
1004+
Arc::clone(&logger),
1005+
pj_config.payjoin_relay.clone(),
1006+
Arc::clone(&tx_sync),
1007+
Arc::clone(&event_queue),
1008+
Arc::clone(&wallet),
1009+
Arc::clone(&payment_store),
1010+
)));
1011+
}
1012+
9691013
let is_listening = Arc::new(AtomicBool::new(false));
9701014
let latest_wallet_sync_timestamp = Arc::new(RwLock::new(None));
9711015
let latest_onchain_wallet_sync_timestamp = Arc::new(RwLock::new(None));
@@ -987,6 +1031,7 @@ fn build_with_store_internal(
9871031
channel_manager,
9881032
chain_monitor,
9891033
output_sweeper,
1034+
payjoin_handler,
9901035
peer_manager,
9911036
connection_manager,
9921037
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: 31 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
}
@@ -182,3 +207,9 @@ impl From<lightning_transaction_sync::TxSyncError> for Error {
182207
Self::TxSyncFailed
183208
}
184209
}
210+
211+
impl From<reqwest::Error> for Error {
212+
fn from(_e: reqwest::Error) -> Self {
213+
Self::PayjoinRequestCreationFailed
214+
}
215+
}

0 commit comments

Comments
 (0)