Skip to content

Commit 120b089

Browse files
committed
receive payjoin payments
1 parent 2f5a966 commit 120b089

File tree

19 files changed

+1801
-15
lines changed

19 files changed

+1801
-15
lines changed

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ prost = { version = "0.11.6", default-features = false}
8080
#bitcoin-payment-instructions = { version = "0.6" }
8181
bitcoin-payment-instructions = { git = "https://github.com/tnull/bitcoin-payment-instructions", rev = "ea50a9d2a8da524b69a2af43233706666cf2ffa5" }
8282

83+
payjoin = { git = "https://github.com/payjoin/rust-payjoin.git", package = "payjoin", default-features = false, features = ["v2", "io"] }
84+
8385
[target.'cfg(windows)'.dependencies]
8486
winapi = { version = "0.3", features = ["winbase"] }
8587

src/builder.rs

Lines changed: 57 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ use vss_client::headers::VssHeaderProvider;
4545
use crate::chain::ChainSource;
4646
use crate::config::{
4747
default_user_config, may_announce_channel, AnnounceError, AsyncPaymentsRole,
48-
BitcoindRestClientConfig, Config, ElectrumSyncConfig, EsploraSyncConfig,
48+
BitcoindRestClientConfig, Config, ElectrumSyncConfig, EsploraSyncConfig, PayjoinConfig,
4949
DEFAULT_ESPLORA_SERVER_URL, DEFAULT_LOG_FILENAME, DEFAULT_LOG_LEVEL,
5050
};
5151
use crate::connection::ConnectionManager;
@@ -56,12 +56,13 @@ use crate::gossip::GossipSource;
5656
use crate::io::sqlite_store::SqliteStore;
5757
use crate::io::utils::{
5858
read_event_queue, read_external_pathfinding_scores_from_cache, read_network_graph,
59-
read_node_metrics, read_output_sweeper, read_payments, read_peer_info, read_pending_payments,
60-
read_scorer, write_node_metrics,
59+
read_node_metrics, read_output_sweeper, read_payjoin_sessions, read_payments, read_peer_info,
60+
read_pending_payments, read_scorer, write_node_metrics,
6161
};
6262
use crate::io::vss_store::VssStoreBuilder;
6363
use crate::io::{
64-
self, PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE,
64+
self, PAYJOIN_SESSION_STORE_PRIMARY_NAMESPACE, PAYJOIN_SESSION_STORE_SECONDARY_NAMESPACE,
65+
PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE,
6566
PENDING_PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE,
6667
PENDING_PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE,
6768
};
@@ -71,13 +72,14 @@ use crate::liquidity::{
7172
use crate::logger::{log_error, LdkLogger, LogLevel, LogWriter, Logger};
7273
use crate::message_handler::NodeCustomMessageHandler;
7374
use crate::payment::asynchronous::om_mailbox::OnionMessageMailbox;
75+
use crate::payment::payjoin::manager::PayjoinManager;
7476
use crate::peer_store::PeerStore;
7577
use crate::runtime::{Runtime, RuntimeSpawner};
7678
use crate::tx_broadcaster::TransactionBroadcaster;
7779
use crate::types::{
7880
AsyncPersister, ChainMonitor, ChannelManager, DynStore, DynStoreWrapper, GossipSync, Graph,
79-
KeysManager, MessageRouter, OnionMessenger, PaymentStore, PeerManager, PendingPaymentStore,
80-
Persister, SyncAndAsyncKVStore,
81+
KeysManager, MessageRouter, OnionMessenger, PayjoinSessionStore, PaymentStore, PeerManager,
82+
PendingPaymentStore, Persister, SyncAndAsyncKVStore,
8183
};
8284
use crate::wallet::persist::KVStoreWalletPersister;
8385
use crate::wallet::Wallet;
@@ -547,6 +549,15 @@ impl NodeBuilder {
547549
Ok(self)
548550
}
549551

552+
/// Configures the [`Node`] instance to enable payjoin payments.
553+
///
554+
/// The `payjoin_config` specifies the PayJoin directory and OHTTP relay URLs required
555+
/// for payjoin V2 protocol.
556+
pub fn set_payjoin_config(&mut self, payjoin_config: PayjoinConfig) -> &mut Self {
557+
self.config.payjoin_config = Some(payjoin_config);
558+
self
559+
}
560+
550561
/// Configures the [`Node`] to resync chain data from genesis on first startup, recovering any
551562
/// historical wallet funds.
552563
///
@@ -933,6 +944,14 @@ impl ArcedNodeBuilder {
933944
self.inner.write().unwrap().set_async_payments_role(role).map(|_| ())
934945
}
935946

947+
/// Configures the [`Node`] instance to enable payjoin payments.
948+
///
949+
/// The `payjoin_config` specifies the PayJoin directory and OHTTP relay URLs required
950+
/// for payjoin V2 protocol.
951+
pub fn set_payjoin_config(&self, payjoin_config: PayjoinConfig) {
952+
self.inner.write().unwrap().set_payjoin_config(payjoin_config);
953+
}
954+
936955
/// Configures the [`Node`] to resync chain data from genesis on first startup, recovering any
937956
/// historical wallet funds.
938957
///
@@ -1083,12 +1102,13 @@ fn build_with_store_internal(
10831102

10841103
let kv_store_ref = Arc::clone(&kv_store);
10851104
let logger_ref = Arc::clone(&logger);
1086-
let (payment_store_res, node_metris_res, pending_payment_store_res) =
1105+
let (payment_store_res, node_metris_res, pending_payment_store_res, payjoin_session_store_res) =
10871106
runtime.block_on(async move {
10881107
tokio::join!(
10891108
read_payments(&*kv_store_ref, Arc::clone(&logger_ref)),
10901109
read_node_metrics(&*kv_store_ref, Arc::clone(&logger_ref)),
1091-
read_pending_payments(&*kv_store_ref, Arc::clone(&logger_ref))
1110+
read_pending_payments(&*kv_store_ref, Arc::clone(&logger_ref)),
1111+
read_payjoin_sessions(&*kv_store_ref, Arc::clone(&logger_ref))
10921112
)
10931113
});
10941114

@@ -1771,6 +1791,34 @@ fn build_with_store_internal(
17711791

17721792
let pathfinding_scores_sync_url = pathfinding_scores_sync_config.map(|c| c.url.clone());
17731793

1794+
let payjoin_session_store = match payjoin_session_store_res {
1795+
Ok(payjoin_sessions) => Arc::new(PayjoinSessionStore::new(
1796+
payjoin_sessions,
1797+
PAYJOIN_SESSION_STORE_PRIMARY_NAMESPACE.to_string(),
1798+
PAYJOIN_SESSION_STORE_SECONDARY_NAMESPACE.to_string(),
1799+
Arc::clone(&kv_store),
1800+
Arc::clone(&logger),
1801+
)),
1802+
Err(e) => {
1803+
log_error!(logger, "Failed to read payjoin session data from store: {}", e);
1804+
return Err(BuildError::ReadFailed);
1805+
},
1806+
};
1807+
1808+
let payjoin_manager = Arc::new(PayjoinManager::new(
1809+
Arc::clone(&payjoin_session_store),
1810+
Arc::clone(&logger),
1811+
Arc::clone(&config),
1812+
Arc::clone(&wallet),
1813+
Arc::clone(&fee_estimator),
1814+
Arc::clone(&chain_source),
1815+
Arc::clone(&channel_manager),
1816+
stop_sender.subscribe(),
1817+
Arc::clone(&payment_store),
1818+
Arc::clone(&pending_payment_store),
1819+
Arc::clone(&tx_broadcaster),
1820+
));
1821+
17741822
#[cfg(cycle_tests)]
17751823
let mut _leak_checker = crate::LeakChecker(Vec::new());
17761824
#[cfg(cycle_tests)]
@@ -1817,6 +1865,7 @@ fn build_with_store_internal(
18171865
hrn_resolver,
18181866
#[cfg(cycle_tests)]
18191867
_leak_checker,
1868+
payjoin_manager,
18201869
})
18211870
}
18221871

src/chain/bitcoind.rs

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -619,6 +619,57 @@ impl BitcoindChainSource {
619619
}
620620
}
621621
}
622+
623+
pub(crate) async fn can_broadcast_transaction(&self, tx: &Transaction) -> Result<bool, Error> {
624+
let timeout_fut = tokio::time::timeout(
625+
Duration::from_secs(DEFAULT_TX_BROADCAST_TIMEOUT_SECS),
626+
self.api_client.test_mempool_accept(tx),
627+
);
628+
629+
match timeout_fut.await {
630+
Ok(res) => res.map_err(|e| {
631+
log_error!(
632+
self.logger,
633+
"Failed to test mempool accept for transaction {}: {}",
634+
tx.compute_txid(),
635+
e
636+
);
637+
Error::TxBroadcastFailed
638+
}),
639+
Err(e) => {
640+
log_error!(
641+
self.logger,
642+
"Failed to test mempool accept for transaction {} due to timeout: {}",
643+
tx.compute_txid(),
644+
e
645+
);
646+
log_trace!(
647+
self.logger,
648+
"Failed test mempool accept transaction bytes: {}",
649+
log_bytes!(tx.encode())
650+
);
651+
Err(Error::TxBroadcastFailed)
652+
},
653+
}
654+
}
655+
656+
pub(crate) async fn get_transaction(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
657+
let timeout_fut = tokio::time::timeout(
658+
Duration::from_secs(DEFAULT_TX_BROADCAST_TIMEOUT_SECS),
659+
self.api_client.get_raw_transaction(txid),
660+
);
661+
662+
match timeout_fut.await {
663+
Ok(res) => res.map_err(|e| {
664+
log_error!(self.logger, "Failed to get transaction {}: {}", txid, e);
665+
Error::TxSyncFailed
666+
}),
667+
Err(e) => {
668+
log_error!(self.logger, "Failed to get transaction {} due to timeout: {}", txid, e);
669+
Err(Error::TxSyncTimeout)
670+
},
671+
}
672+
}
622673
}
623674

624675
#[derive(Clone)]
@@ -1229,6 +1280,46 @@ impl BitcoindClient {
12291280
.collect();
12301281
Ok(evicted_txids)
12311282
}
1283+
1284+
/// Tests whether the provided transaction would be accepted by the mempool.
1285+
pub(crate) async fn test_mempool_accept(&self, tx: &Transaction) -> std::io::Result<bool> {
1286+
match self {
1287+
BitcoindClient::Rpc { rpc_client, .. } => {
1288+
Self::test_mempool_accept_inner(Arc::clone(rpc_client), tx).await
1289+
},
1290+
BitcoindClient::Rest { rpc_client, .. } => {
1291+
// We rely on the internal RPC client to make this call, as this
1292+
// operation is not supported by Bitcoin Core's REST interface.
1293+
Self::test_mempool_accept_inner(Arc::clone(rpc_client), tx).await
1294+
},
1295+
}
1296+
}
1297+
1298+
async fn test_mempool_accept_inner(
1299+
rpc_client: Arc<RpcClient>, tx: &Transaction,
1300+
) -> std::io::Result<bool> {
1301+
let tx_serialized = bitcoin::consensus::encode::serialize_hex(tx);
1302+
let tx_array = serde_json::json!([tx_serialized]);
1303+
1304+
let resp =
1305+
rpc_client.call_method::<serde_json::Value>("testmempoolaccept", &[tx_array]).await?;
1306+
1307+
if let Some(array) = resp.as_array() {
1308+
if let Some(first_result) = array.first() {
1309+
Ok(first_result.get("allowed").and_then(|v| v.as_bool()).unwrap_or(false))
1310+
} else {
1311+
Err(std::io::Error::new(
1312+
std::io::ErrorKind::Other,
1313+
"Empty array response from testmempoolaccept",
1314+
))
1315+
}
1316+
} else {
1317+
Err(std::io::Error::new(
1318+
std::io::ErrorKind::InvalidData,
1319+
"testmempoolaccept did not return an array",
1320+
))
1321+
}
1322+
}
12321323
}
12331324

12341325
impl BlockSource for BitcoindClient {

src/chain/electrum.rs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,21 @@ impl ElectrumChainSource {
288288
electrum_client.broadcast(tx).await;
289289
}
290290
}
291+
292+
pub(crate) async fn get_transaction(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
293+
let electrum_client: Arc<ElectrumRuntimeClient> =
294+
if let Some(client) = self.electrum_runtime_status.read().unwrap().client().as_ref() {
295+
Arc::clone(client)
296+
} else {
297+
debug_assert!(
298+
false,
299+
"We should have started the chain source before getting transactions"
300+
);
301+
return Err(Error::TxSyncFailed);
302+
};
303+
304+
electrum_client.get_transaction(txid).await
305+
}
291306
}
292307

293308
impl Filter for ElectrumChainSource {
@@ -652,6 +667,48 @@ impl ElectrumRuntimeClient {
652667

653668
Ok(new_fee_rate_cache)
654669
}
670+
671+
async fn get_transaction(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
672+
let electrum_client = Arc::clone(&self.electrum_client);
673+
let txid_copy = *txid;
674+
675+
let spawn_fut =
676+
self.runtime.spawn_blocking(move || electrum_client.transaction_get(&txid_copy));
677+
let timeout_fut = tokio::time::timeout(
678+
Duration::from_secs(
679+
self.sync_config.timeouts_config.lightning_wallet_sync_timeout_secs,
680+
),
681+
spawn_fut,
682+
);
683+
684+
match timeout_fut.await {
685+
Ok(res) => match res {
686+
Ok(inner_res) => match inner_res {
687+
Ok(tx) => Ok(Some(tx)),
688+
Err(e) => {
689+
// Check if it's a "not found" error
690+
let error_str = e.to_string();
691+
if error_str.contains("No such mempool or blockchain transaction")
692+
|| error_str.contains("not found")
693+
{
694+
Ok(None)
695+
} else {
696+
log_error!(self.logger, "Failed to get transaction {}: {}", txid, e);
697+
Err(Error::TxSyncFailed)
698+
}
699+
},
700+
},
701+
Err(e) => {
702+
log_error!(self.logger, "Failed to get transaction {}: {}", txid, e);
703+
Err(Error::TxSyncFailed)
704+
},
705+
},
706+
Err(e) => {
707+
log_error!(self.logger, "Failed to get transaction {} due to timeout: {}", txid, e);
708+
Err(Error::TxSyncTimeout)
709+
},
710+
}
711+
}
655712
}
656713

657714
impl Filter for ElectrumRuntimeClient {

src/chain/esplora.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,13 @@ impl EsploraChainSource {
422422
}
423423
}
424424
}
425+
426+
pub(crate) async fn get_transaction(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
427+
self.esplora_client.get_tx(txid).await.map_err(|e| {
428+
log_error!(self.logger, "Failed to get transaction {}: {}", txid, e);
429+
Error::TxSyncFailed
430+
})
431+
}
425432
}
426433

427434
impl Filter for EsploraChainSource {

src/chain/mod.rs

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use std::collections::HashMap;
1313
use std::sync::{Arc, Mutex, RwLock};
1414
use std::time::Duration;
1515

16-
use bitcoin::{Script, Txid};
16+
use bitcoin::{Script, Transaction, Txid};
1717
use lightning::chain::{BestBlock, Filter};
1818

1919
use crate::chain::bitcoind::{BitcoindChainSource, UtxoSourceClient};
@@ -468,6 +468,38 @@ impl ChainSource {
468468
}
469469
}
470470
}
471+
472+
pub(crate) fn can_broadcast_transaction(&self, tx: &Transaction) -> Result<bool, Error> {
473+
tokio::task::block_in_place(|| {
474+
tokio::runtime::Handle::current().block_on(async {
475+
match &self.kind {
476+
ChainSourceKind::Bitcoind(bitcoind_chain_source) => {
477+
bitcoind_chain_source.can_broadcast_transaction(tx).await
478+
},
479+
ChainSourceKind::Esplora{..} => {
480+
// Esplora doesn't support testmempoolaccept equivalent.
481+
unreachable!("Mempool accept testing is not supported with Esplora backend. Use BitcoindRpc for this functionality.")
482+
},
483+
ChainSourceKind::Electrum{..} => {
484+
// Electrum doesn't support testmempoolaccept equivalent.
485+
unreachable!("Mempool accept testing is not supported with Electrum backend. Use BitcoindRpc for this functionality.")
486+
},
487+
}
488+
})
489+
})
490+
}
491+
492+
pub(crate) fn get_transaction(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
493+
tokio::task::block_in_place(|| {
494+
tokio::runtime::Handle::current().block_on(async {
495+
match &self.kind {
496+
ChainSourceKind::Bitcoind(bitcoind) => bitcoind.get_transaction(txid).await,
497+
ChainSourceKind::Esplora(esplora) => esplora.get_transaction(txid).await,
498+
ChainSourceKind::Electrum(electrum) => electrum.get_transaction(txid).await,
499+
}
500+
})
501+
})
502+
}
471503
}
472504

473505
impl Filter for ChainSource {

0 commit comments

Comments
 (0)