Skip to content

Commit 19c69bb

Browse files
committed
receive payjoin payments
1 parent fae2746 commit 19c69bb

File tree

20 files changed

+1819
-16
lines changed

20 files changed

+1819
-16
lines changed

Cargo.toml

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

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

bindings/ldk_node.udl

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ interface Node {
9393
SpontaneousPayment spontaneous_payment();
9494
OnchainPayment onchain_payment();
9595
UnifiedPayment unified_payment();
96+
PayjoinPayment payjoin_payment();
9697
LSPS1Liquidity lsps1_liquidity();
9798
[Throws=NodeError]
9899
void lnurl_auth(string lnurl);
@@ -157,6 +158,8 @@ interface FeeRate {
157158

158159
typedef interface UnifiedPayment;
159160

161+
typedef interface PayjoinPayment;
162+
160163
typedef interface LSPS1Liquidity;
161164

162165
[Error]
@@ -221,6 +224,9 @@ enum NodeError {
221224
"LnurlAuthFailed",
222225
"LnurlAuthTimeout",
223226
"InvalidLnurl",
227+
"PayjoinNotConfigured",
228+
"PayjoinSessionCreationFailed",
229+
"PayjoinSessionFailed"
224230
};
225231

226232
typedef dictionary NodeStatus;

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
};
@@ -72,13 +73,14 @@ use crate::lnurl_auth::LnurlAuth;
7273
use crate::logger::{log_error, LdkLogger, LogLevel, LogWriter, Logger};
7374
use crate::message_handler::NodeCustomMessageHandler;
7475
use crate::payment::asynchronous::om_mailbox::OnionMessageMailbox;
76+
use crate::payment::payjoin::manager::PayjoinManager;
7577
use crate::peer_store::PeerStore;
7678
use crate::runtime::{Runtime, RuntimeSpawner};
7779
use crate::tx_broadcaster::TransactionBroadcaster;
7880
use crate::types::{
7981
AsyncPersister, ChainMonitor, ChannelManager, DynStore, DynStoreWrapper, GossipSync, Graph,
80-
KeysManager, MessageRouter, OnionMessenger, PaymentStore, PeerManager, PendingPaymentStore,
81-
Persister, SyncAndAsyncKVStore,
82+
KeysManager, MessageRouter, OnionMessenger, PayjoinSessionStore, PaymentStore, PeerManager,
83+
PendingPaymentStore, Persister, SyncAndAsyncKVStore,
8284
};
8385
use crate::wallet::persist::KVStoreWalletPersister;
8486
use crate::wallet::Wallet;
@@ -549,6 +551,15 @@ impl NodeBuilder {
549551
Ok(self)
550552
}
551553

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

986+
/// Configures the [`Node`] instance to enable payjoin payments.
987+
///
988+
/// The `payjoin_config` specifies the PayJoin directory and OHTTP relay URLs required
989+
/// for payjoin V2 protocol.
990+
pub fn set_payjoin_config(&self, payjoin_config: PayjoinConfig) {
991+
self.inner.write().unwrap().set_payjoin_config(payjoin_config);
992+
}
993+
975994
/// Configures the [`Node`] to resync chain data from genesis on first startup, recovering any
976995
/// historical wallet funds.
977996
///
@@ -1151,12 +1170,13 @@ fn build_with_store_internal(
11511170

11521171
let kv_store_ref = Arc::clone(&kv_store);
11531172
let logger_ref = Arc::clone(&logger);
1154-
let (payment_store_res, node_metris_res, pending_payment_store_res) =
1173+
let (payment_store_res, node_metris_res, pending_payment_store_res, payjoin_session_store_res) =
11551174
runtime.block_on(async move {
11561175
tokio::join!(
11571176
read_payments(&*kv_store_ref, Arc::clone(&logger_ref)),
11581177
read_node_metrics(&*kv_store_ref, Arc::clone(&logger_ref)),
1159-
read_pending_payments(&*kv_store_ref, Arc::clone(&logger_ref))
1178+
read_pending_payments(&*kv_store_ref, Arc::clone(&logger_ref)),
1179+
read_payjoin_sessions(&*kv_store_ref, Arc::clone(&logger_ref))
11601180
)
11611181
});
11621182

@@ -1841,6 +1861,34 @@ fn build_with_store_internal(
18411861

18421862
let pathfinding_scores_sync_url = pathfinding_scores_sync_config.map(|c| c.url.clone());
18431863

1864+
let payjoin_session_store = match payjoin_session_store_res {
1865+
Ok(payjoin_sessions) => Arc::new(PayjoinSessionStore::new(
1866+
payjoin_sessions,
1867+
PAYJOIN_SESSION_STORE_PRIMARY_NAMESPACE.to_string(),
1868+
PAYJOIN_SESSION_STORE_SECONDARY_NAMESPACE.to_string(),
1869+
Arc::clone(&kv_store),
1870+
Arc::clone(&logger),
1871+
)),
1872+
Err(e) => {
1873+
log_error!(logger, "Failed to read payjoin session data from store: {}", e);
1874+
return Err(BuildError::ReadFailed);
1875+
},
1876+
};
1877+
1878+
let payjoin_manager = Arc::new(PayjoinManager::new(
1879+
Arc::clone(&payjoin_session_store),
1880+
Arc::clone(&logger),
1881+
Arc::clone(&config),
1882+
Arc::clone(&wallet),
1883+
Arc::clone(&fee_estimator),
1884+
Arc::clone(&chain_source),
1885+
Arc::clone(&channel_manager),
1886+
stop_sender.subscribe(),
1887+
Arc::clone(&payment_store),
1888+
Arc::clone(&pending_payment_store),
1889+
Arc::clone(&tx_broadcaster),
1890+
));
1891+
18441892
#[cfg(cycle_tests)]
18451893
let mut _leak_checker = crate::LeakChecker(Vec::new());
18461894
#[cfg(cycle_tests)]
@@ -1888,6 +1936,7 @@ fn build_with_store_internal(
18881936
hrn_resolver,
18891937
#[cfg(cycle_tests)]
18901938
_leak_checker,
1939+
payjoin_manager,
18911940
})
18921941
}
18931942

src/chain/bitcoind.rs

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ use serde::Serialize;
3333
use super::WalletSyncStatus;
3434
use crate::config::{
3535
BitcoindRestClientConfig, Config, DEFAULT_FEE_RATE_CACHE_UPDATE_TIMEOUT_SECS,
36-
DEFAULT_TX_BROADCAST_TIMEOUT_SECS,
36+
DEFAULT_TX_BROADCAST_TIMEOUT_SECS, DEFAULT_TX_LOOKUP_TIMEOUT_SECS,
3737
};
3838
use crate::fee_estimator::{
3939
apply_post_estimation_adjustments, get_all_conf_targets, get_num_block_defaults_for_target,
@@ -620,6 +620,57 @@ impl BitcoindChainSource {
620620
}
621621
}
622622
}
623+
624+
pub(crate) async fn can_broadcast_transaction(&self, tx: &Transaction) -> Result<bool, Error> {
625+
let timeout_fut = tokio::time::timeout(
626+
Duration::from_secs(DEFAULT_TX_BROADCAST_TIMEOUT_SECS),
627+
self.api_client.test_mempool_accept(tx),
628+
);
629+
630+
match timeout_fut.await {
631+
Ok(res) => res.map_err(|e| {
632+
log_error!(
633+
self.logger,
634+
"Failed to test mempool accept for transaction {}: {}",
635+
tx.compute_txid(),
636+
e
637+
);
638+
Error::WalletOperationFailed
639+
}),
640+
Err(e) => {
641+
log_error!(
642+
self.logger,
643+
"Failed to test mempool accept for transaction {} due to timeout: {}",
644+
tx.compute_txid(),
645+
e
646+
);
647+
log_trace!(
648+
self.logger,
649+
"Failed test mempool accept transaction bytes: {}",
650+
log_bytes!(tx.encode())
651+
);
652+
Err(Error::WalletOperationTimeout)
653+
},
654+
}
655+
}
656+
657+
pub(crate) async fn get_transaction(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
658+
let timeout_fut = tokio::time::timeout(
659+
Duration::from_secs(DEFAULT_TX_LOOKUP_TIMEOUT_SECS),
660+
self.api_client.get_raw_transaction(txid),
661+
);
662+
663+
match timeout_fut.await {
664+
Ok(res) => res.map_err(|e| {
665+
log_error!(self.logger, "Failed to get transaction {}: {}", txid, e);
666+
Error::TxSyncFailed
667+
}),
668+
Err(e) => {
669+
log_error!(self.logger, "Failed to get transaction {} due to timeout: {}", txid, e);
670+
Err(Error::TxSyncTimeout)
671+
},
672+
}
673+
}
623674
}
624675

625676
#[derive(Clone)]
@@ -1179,6 +1230,34 @@ impl BitcoindClient {
11791230
.collect();
11801231
Ok(evicted_txids)
11811232
}
1233+
1234+
/// Tests whether the provided transaction would be accepted by the mempool.
1235+
pub(crate) async fn test_mempool_accept(
1236+
&self, tx: &Transaction,
1237+
) -> Result<bool, RpcClientError> {
1238+
match self {
1239+
BitcoindClient::Rpc { rpc_client, .. } => {
1240+
Self::test_mempool_accept_inner(Arc::clone(rpc_client), tx).await
1241+
},
1242+
BitcoindClient::Rest { rpc_client, .. } => {
1243+
// We rely on the internal RPC client to make this call, as this
1244+
// operation is not supported by Bitcoin Core's REST interface.
1245+
Self::test_mempool_accept_inner(Arc::clone(rpc_client), tx).await
1246+
},
1247+
}
1248+
}
1249+
1250+
async fn test_mempool_accept_inner(
1251+
rpc_client: Arc<RpcClient>, tx: &Transaction,
1252+
) -> Result<bool, RpcClientError> {
1253+
let tx_serialized = bitcoin::consensus::encode::serialize_hex(tx);
1254+
let tx_array = serde_json::json!([tx_serialized]);
1255+
1256+
rpc_client
1257+
.call_method::<TestMempoolAcceptResponse>("testmempoolaccept", &[tx_array])
1258+
.await
1259+
.map(|resp| resp.0)
1260+
}
11821261
}
11831262

11841263
impl BlockSource for BitcoindClient {
@@ -1334,6 +1413,23 @@ impl TryInto<GetMempoolEntryResponse> for JsonResponse {
13341413
}
13351414
}
13361415

1416+
pub(crate) struct TestMempoolAcceptResponse(pub bool);
1417+
1418+
impl TryInto<TestMempoolAcceptResponse> for JsonResponse {
1419+
type Error = String;
1420+
fn try_into(self) -> Result<TestMempoolAcceptResponse, String> {
1421+
let array =
1422+
self.0.as_array().ok_or("Failed to parse testmempoolaccept response".to_string())?;
1423+
let first =
1424+
array.first().ok_or("Empty array response from testmempoolaccept".to_string())?;
1425+
let allowed = first
1426+
.get("allowed")
1427+
.and_then(|v| v.as_bool())
1428+
.ok_or("Missing 'allowed' field in testmempoolaccept response".to_string())?;
1429+
Ok(TestMempoolAcceptResponse(allowed))
1430+
}
1431+
}
1432+
13371433
#[derive(Debug, Clone)]
13381434
pub(crate) struct MempoolEntry {
13391435
/// The transaction id

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 {

0 commit comments

Comments
 (0)