-
Notifications
You must be signed in to change notification settings - Fork 129
Add Payjoin Receiver Support #746
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -93,6 +93,7 @@ interface Node { | |
| SpontaneousPayment spontaneous_payment(); | ||
| OnchainPayment onchain_payment(); | ||
| UnifiedPayment unified_payment(); | ||
| PayjoinPayment payjoin_payment(); | ||
| LSPS1Liquidity lsps1_liquidity(); | ||
| [Throws=NodeError] | ||
| void lnurl_auth(string lnurl); | ||
|
|
@@ -157,6 +158,8 @@ interface FeeRate { | |
|
|
||
| typedef interface UnifiedPayment; | ||
|
|
||
| typedef interface PayjoinPayment; | ||
|
|
||
| typedef interface LSPS1Liquidity; | ||
|
|
||
| [Error] | ||
|
|
@@ -221,6 +224,9 @@ enum NodeError { | |
| "LnurlAuthFailed", | ||
| "LnurlAuthTimeout", | ||
| "InvalidLnurl", | ||
| "PayjoinNotConfigured", | ||
| "PayjoinSessionCreationFailed", | ||
| "PayjoinSessionFailed" | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| }; | ||
|
|
||
| typedef dictionary NodeStatus; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -45,7 +45,7 @@ use vss_client::headers::VssHeaderProvider; | |
| use crate::chain::ChainSource; | ||
| use crate::config::{ | ||
| default_user_config, may_announce_channel, AnnounceError, AsyncPaymentsRole, | ||
| BitcoindRestClientConfig, Config, ElectrumSyncConfig, EsploraSyncConfig, | ||
| BitcoindRestClientConfig, Config, ElectrumSyncConfig, EsploraSyncConfig, PayjoinConfig, | ||
| DEFAULT_ESPLORA_SERVER_URL, DEFAULT_LOG_FILENAME, DEFAULT_LOG_LEVEL, | ||
| }; | ||
| use crate::connection::ConnectionManager; | ||
|
|
@@ -56,12 +56,13 @@ use crate::gossip::GossipSource; | |
| use crate::io::sqlite_store::SqliteStore; | ||
| use crate::io::utils::{ | ||
| read_event_queue, read_external_pathfinding_scores_from_cache, read_network_graph, | ||
| read_node_metrics, read_output_sweeper, read_payments, read_peer_info, read_pending_payments, | ||
| read_scorer, write_node_metrics, | ||
| read_node_metrics, read_output_sweeper, read_payjoin_sessions, read_payments, read_peer_info, | ||
| read_pending_payments, read_scorer, write_node_metrics, | ||
| }; | ||
| use crate::io::vss_store::VssStoreBuilder; | ||
| use crate::io::{ | ||
| self, PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE, | ||
| self, PAYJOIN_SESSION_STORE_PRIMARY_NAMESPACE, PAYJOIN_SESSION_STORE_SECONDARY_NAMESPACE, | ||
| PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE, | ||
| PENDING_PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, | ||
| PENDING_PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE, | ||
| }; | ||
|
|
@@ -72,13 +73,14 @@ use crate::lnurl_auth::LnurlAuth; | |
| use crate::logger::{log_error, LdkLogger, LogLevel, LogWriter, Logger}; | ||
| use crate::message_handler::NodeCustomMessageHandler; | ||
| use crate::payment::asynchronous::om_mailbox::OnionMessageMailbox; | ||
| use crate::payment::payjoin::manager::PayjoinManager; | ||
| use crate::peer_store::PeerStore; | ||
| use crate::runtime::{Runtime, RuntimeSpawner}; | ||
| use crate::tx_broadcaster::TransactionBroadcaster; | ||
| use crate::types::{ | ||
| AsyncPersister, ChainMonitor, ChannelManager, DynStore, DynStoreWrapper, GossipSync, Graph, | ||
| KeysManager, MessageRouter, OnionMessenger, PaymentStore, PeerManager, PendingPaymentStore, | ||
| Persister, SyncAndAsyncKVStore, | ||
| KeysManager, MessageRouter, OnionMessenger, PayjoinSessionStore, PaymentStore, PeerManager, | ||
| PendingPaymentStore, Persister, SyncAndAsyncKVStore, | ||
| }; | ||
| use crate::wallet::persist::KVStoreWalletPersister; | ||
| use crate::wallet::Wallet; | ||
|
|
@@ -549,6 +551,15 @@ impl NodeBuilder { | |
| Ok(self) | ||
| } | ||
|
|
||
| /// Configures the [`Node`] instance to enable payjoin payments. | ||
| /// | ||
| /// The `payjoin_config` specifies the PayJoin directory and OHTTP relay URLs required | ||
| /// for payjoin V2 protocol. | ||
| pub fn set_payjoin_config(&mut self, payjoin_config: PayjoinConfig) -> &mut Self { | ||
| self.config.payjoin_config = Some(payjoin_config); | ||
| self | ||
| } | ||
|
|
||
| /// Configures the [`Node`] to resync chain data from genesis on first startup, recovering any | ||
| /// historical wallet funds. | ||
| /// | ||
|
|
@@ -972,6 +983,14 @@ impl ArcedNodeBuilder { | |
| self.inner.write().unwrap().set_async_payments_role(role).map(|_| ()) | ||
| } | ||
|
|
||
| /// Configures the [`Node`] instance to enable payjoin payments. | ||
| /// | ||
| /// The `payjoin_config` specifies the PayJoin directory and OHTTP relay URLs required | ||
| /// for payjoin V2 protocol. | ||
| pub fn set_payjoin_config(&self, payjoin_config: PayjoinConfig) { | ||
| self.inner.write().unwrap().set_payjoin_config(payjoin_config); | ||
| } | ||
|
|
||
| /// Configures the [`Node`] to resync chain data from genesis on first startup, recovering any | ||
| /// historical wallet funds. | ||
| /// | ||
|
|
@@ -1151,12 +1170,13 @@ fn build_with_store_internal( | |
|
|
||
| let kv_store_ref = Arc::clone(&kv_store); | ||
| let logger_ref = Arc::clone(&logger); | ||
| let (payment_store_res, node_metris_res, pending_payment_store_res) = | ||
| let (payment_store_res, node_metris_res, pending_payment_store_res, payjoin_session_store_res) = | ||
| runtime.block_on(async move { | ||
| tokio::join!( | ||
| read_payments(&*kv_store_ref, Arc::clone(&logger_ref)), | ||
| read_node_metrics(&*kv_store_ref, Arc::clone(&logger_ref)), | ||
| read_pending_payments(&*kv_store_ref, Arc::clone(&logger_ref)) | ||
| read_pending_payments(&*kv_store_ref, Arc::clone(&logger_ref)), | ||
| read_payjoin_sessions(&*kv_store_ref, Arc::clone(&logger_ref)) | ||
| ) | ||
| }); | ||
|
|
||
|
|
@@ -1841,6 +1861,34 @@ fn build_with_store_internal( | |
|
|
||
| let pathfinding_scores_sync_url = pathfinding_scores_sync_config.map(|c| c.url.clone()); | ||
|
|
||
| let payjoin_session_store = match payjoin_session_store_res { | ||
| Ok(payjoin_sessions) => Arc::new(PayjoinSessionStore::new( | ||
| payjoin_sessions, | ||
| PAYJOIN_SESSION_STORE_PRIMARY_NAMESPACE.to_string(), | ||
| PAYJOIN_SESSION_STORE_SECONDARY_NAMESPACE.to_string(), | ||
| Arc::clone(&kv_store), | ||
| Arc::clone(&logger), | ||
| )), | ||
| Err(e) => { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| log_error!(logger, "Failed to read payjoin session data from store: {}", e); | ||
| return Err(BuildError::ReadFailed); | ||
| }, | ||
| }; | ||
|
|
||
| let payjoin_manager = Arc::new(PayjoinManager::new( | ||
| Arc::clone(&payjoin_session_store), | ||
| Arc::clone(&logger), | ||
| Arc::clone(&config), | ||
| Arc::clone(&wallet), | ||
| Arc::clone(&fee_estimator), | ||
| Arc::clone(&chain_source), | ||
| Arc::clone(&channel_manager), | ||
| stop_sender.subscribe(), | ||
| Arc::clone(&payment_store), | ||
| Arc::clone(&pending_payment_store), | ||
| Arc::clone(&tx_broadcaster), | ||
| )); | ||
|
|
||
| #[cfg(cycle_tests)] | ||
| let mut _leak_checker = crate::LeakChecker(Vec::new()); | ||
| #[cfg(cycle_tests)] | ||
|
|
@@ -1888,6 +1936,7 @@ fn build_with_store_internal( | |
| hrn_resolver, | ||
| #[cfg(cycle_tests)] | ||
| _leak_checker, | ||
| payjoin_manager, | ||
| }) | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -33,7 +33,7 @@ use serde::Serialize; | |
| use super::WalletSyncStatus; | ||
| use crate::config::{ | ||
| BitcoindRestClientConfig, Config, DEFAULT_FEE_RATE_CACHE_UPDATE_TIMEOUT_SECS, | ||
| DEFAULT_TX_BROADCAST_TIMEOUT_SECS, | ||
| DEFAULT_TX_BROADCAST_TIMEOUT_SECS, DEFAULT_TX_LOOKUP_TIMEOUT_SECS, | ||
| }; | ||
| use crate::fee_estimator::{ | ||
| apply_post_estimation_adjustments, get_all_conf_targets, get_num_block_defaults_for_target, | ||
|
|
@@ -620,6 +620,57 @@ impl BitcoindChainSource { | |
| } | ||
| } | ||
| } | ||
|
|
||
| pub(crate) async fn can_broadcast_transaction(&self, tx: &Transaction) -> Result<bool, Error> { | ||
| let timeout_fut = tokio::time::timeout( | ||
| Duration::from_secs(DEFAULT_TX_BROADCAST_TIMEOUT_SECS), | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. since |
||
| self.api_client.test_mempool_accept(tx), | ||
| ); | ||
|
|
||
| match timeout_fut.await { | ||
| Ok(res) => res.map_err(|e| { | ||
| log_error!( | ||
| self.logger, | ||
| "Failed to test mempool accept for transaction {}: {}", | ||
| tx.compute_txid(), | ||
| e | ||
| ); | ||
| Error::WalletOperationFailed | ||
| }), | ||
| Err(e) => { | ||
| log_error!( | ||
| self.logger, | ||
| "Failed to test mempool accept for transaction {} due to timeout: {}", | ||
| tx.compute_txid(), | ||
| e | ||
| ); | ||
| log_trace!( | ||
| self.logger, | ||
| "Failed test mempool accept transaction bytes: {}", | ||
| log_bytes!(tx.encode()) | ||
| ); | ||
| Err(Error::WalletOperationTimeout) | ||
| }, | ||
| } | ||
| } | ||
|
|
||
| pub(crate) async fn get_transaction(&self, txid: &Txid) -> Result<Option<Transaction>, Error> { | ||
| let timeout_fut = tokio::time::timeout( | ||
| Duration::from_secs(DEFAULT_TX_LOOKUP_TIMEOUT_SECS), | ||
| self.api_client.get_raw_transaction(txid), | ||
| ); | ||
|
|
||
| match timeout_fut.await { | ||
| Ok(res) => res.map_err(|e| { | ||
| log_error!(self.logger, "Failed to get transaction {}: {}", txid, e); | ||
| Error::TxSyncFailed | ||
| }), | ||
| Err(e) => { | ||
| log_error!(self.logger, "Failed to get transaction {} due to timeout: {}", txid, e); | ||
| Err(Error::TxSyncTimeout) | ||
| }, | ||
| } | ||
| } | ||
| } | ||
|
|
||
| #[derive(Clone)] | ||
|
|
@@ -1179,6 +1230,34 @@ impl BitcoindClient { | |
| .collect(); | ||
| Ok(evicted_txids) | ||
| } | ||
|
|
||
| /// Tests whether the provided transaction would be accepted by the mempool. | ||
| pub(crate) async fn test_mempool_accept( | ||
| &self, tx: &Transaction, | ||
| ) -> Result<bool, RpcClientError> { | ||
| match self { | ||
| BitcoindClient::Rpc { rpc_client, .. } => { | ||
| Self::test_mempool_accept_inner(Arc::clone(rpc_client), tx).await | ||
| }, | ||
| BitcoindClient::Rest { rpc_client, .. } => { | ||
| // We rely on the internal RPC client to make this call, as this | ||
| // operation is not supported by Bitcoin Core's REST interface. | ||
| Self::test_mempool_accept_inner(Arc::clone(rpc_client), tx).await | ||
| }, | ||
| } | ||
| } | ||
|
|
||
| async fn test_mempool_accept_inner( | ||
| rpc_client: Arc<RpcClient>, tx: &Transaction, | ||
| ) -> Result<bool, RpcClientError> { | ||
| let tx_serialized = bitcoin::consensus::encode::serialize_hex(tx); | ||
| let tx_array = serde_json::json!([tx_serialized]); | ||
|
|
||
| rpc_client | ||
| .call_method::<TestMempoolAcceptResponse>("testmempoolaccept", &[tx_array]) | ||
| .await | ||
| .map(|resp| resp.0) | ||
| } | ||
| } | ||
|
|
||
| impl BlockSource for BitcoindClient { | ||
|
|
@@ -1334,6 +1413,23 @@ impl TryInto<GetMempoolEntryResponse> for JsonResponse { | |
| } | ||
| } | ||
|
|
||
| pub(crate) struct TestMempoolAcceptResponse(pub bool); | ||
|
|
||
| impl TryInto<TestMempoolAcceptResponse> for JsonResponse { | ||
| type Error = String; | ||
| fn try_into(self) -> Result<TestMempoolAcceptResponse, String> { | ||
| let array = | ||
| self.0.as_array().ok_or("Failed to parse testmempoolaccept response".to_string())?; | ||
| let first = | ||
| array.first().ok_or("Empty array response from testmempoolaccept".to_string())?; | ||
| let allowed = first | ||
| .get("allowed") | ||
| .and_then(|v| v.as_bool()) | ||
| .ok_or("Missing 'allowed' field in testmempoolaccept response".to_string())?; | ||
| Ok(TestMempoolAcceptResponse(allowed)) | ||
| } | ||
| } | ||
|
|
||
| #[derive(Debug, Clone)] | ||
| pub(crate) struct MempoolEntry { | ||
| /// The transaction id | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -288,6 +288,21 @@ impl ElectrumChainSource { | |
| electrum_client.broadcast(tx).await; | ||
| } | ||
| } | ||
|
|
||
| pub(crate) async fn get_transaction(&self, txid: &Txid) -> Result<Option<Transaction>, Error> { | ||
| let electrum_client: Arc<ElectrumRuntimeClient> = | ||
| if let Some(client) = self.electrum_runtime_status.read().unwrap().client().as_ref() { | ||
| Arc::clone(client) | ||
| } else { | ||
| debug_assert!( | ||
| false, | ||
| "We should have started the chain source before getting transactions" | ||
| ); | ||
| return Err(Error::TxSyncFailed); | ||
| }; | ||
|
|
||
| electrum_client.get_transaction(txid).await | ||
| } | ||
| } | ||
|
|
||
| impl Filter for ElectrumChainSource { | ||
|
|
@@ -652,6 +667,48 @@ impl ElectrumRuntimeClient { | |
|
|
||
| Ok(new_fee_rate_cache) | ||
| } | ||
|
|
||
| async fn get_transaction(&self, txid: &Txid) -> Result<Option<Transaction>, Error> { | ||
| let electrum_client = Arc::clone(&self.electrum_client); | ||
| let txid_copy = *txid; | ||
|
|
||
| let spawn_fut = | ||
| self.runtime.spawn_blocking(move || electrum_client.transaction_get(&txid_copy)); | ||
| let timeout_fut = tokio::time::timeout( | ||
| Duration::from_secs( | ||
| self.sync_config.timeouts_config.lightning_wallet_sync_timeout_secs, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is a single onchain tx lookup, should this use |
||
| ), | ||
| spawn_fut, | ||
| ); | ||
|
|
||
| match timeout_fut.await { | ||
| Ok(res) => match res { | ||
| Ok(inner_res) => match inner_res { | ||
| Ok(tx) => Ok(Some(tx)), | ||
| Err(e) => { | ||
| // Check if it's a "not found" error | ||
| let error_str = e.to_string(); | ||
| if error_str.contains("No such mempool or blockchain transaction") | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. matching errors by string seems fragile, as the message varies by server implementation. |
||
| || error_str.contains("not found") | ||
| { | ||
| Ok(None) | ||
| } else { | ||
| log_error!(self.logger, "Failed to get transaction {}: {}", txid, e); | ||
| Err(Error::TxSyncFailed) | ||
| } | ||
| }, | ||
| }, | ||
| Err(e) => { | ||
| log_error!(self.logger, "Failed to get transaction {}: {}", txid, e); | ||
| Err(Error::TxSyncFailed) | ||
| }, | ||
| }, | ||
| Err(e) => { | ||
| log_error!(self.logger, "Failed to get transaction {} due to timeout: {}", txid, e); | ||
| Err(Error::TxSyncTimeout) | ||
| }, | ||
| } | ||
| } | ||
| } | ||
|
|
||
| impl Filter for ElectrumRuntimeClient { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should this pin a
rev?