Skip to content

Commit eb97832

Browse files
committed
receive payjoin payments
1 parent b55de44 commit eb97832

File tree

19 files changed

+1811
-16
lines changed

19 files changed

+1811
-16
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: 56 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,33 @@ 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(&kv_store),
1811+
Arc::clone(&logger),
1812+
Arc::clone(&config),
1813+
Arc::clone(&wallet),
1814+
Arc::clone(&fee_estimator),
1815+
Arc::clone(&chain_source),
1816+
stop_sender.subscribe(),
1817+
Arc::clone(&payment_store),
1818+
Arc::clone(&pending_payment_store),
1819+
));
1820+
17741821
#[cfg(cycle_tests)]
17751822
let mut _leak_checker = crate::LeakChecker(Vec::new());
17761823
#[cfg(cycle_tests)]
@@ -1817,6 +1864,7 @@ fn build_with_store_internal(
18171864
hrn_resolver,
18181865
#[cfg(cycle_tests)]
18191866
_leak_checker,
1867+
payjoin_manager,
18201868
})
18211869
}
18221870

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 {

0 commit comments

Comments
 (0)