Skip to content

Commit 12a41ad

Browse files
committed
receive payjoin payments
1 parent 80fb49b commit 12a41ad

File tree

15 files changed

+1449
-3
lines changed

15 files changed

+1449
-3
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 = "e4d519b95b26916dc6efa22f8f1cc11a818ce7a7" }
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/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: 135 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ use bdk_chain::bdk_core::spk_client::{
1515
};
1616
use bdk_electrum::BdkElectrumClient;
1717
use bdk_wallet::{KeychainKind as BdkKeyChainKind, Update as BdkUpdate};
18-
use bitcoin::{FeeRate, Network, Script, ScriptBuf, Transaction, Txid};
18+
use bitcoin::{FeeRate, Network, OutPoint, Script, ScriptBuf, Transaction, Txid};
1919
use electrum_client::{
2020
Batch, Client as ElectrumClient, ConfigBuilder as ElectrumConfigBuilder, ElectrumApi,
2121
};
@@ -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,125 @@ 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+
}
712+
713+
async fn is_outpoint_spent(&self, outpoint: &OutPoint) -> Result<bool, Error> {
714+
// First get the transaction to find the scriptPubKey of the output
715+
let tx = match self.get_transaction(&outpoint.txid).await? {
716+
Some(tx) => tx,
717+
None => {
718+
// Transaction doesn't exist, so outpoint can't be spent
719+
// (or never existed)
720+
return Ok(false);
721+
},
722+
};
723+
724+
// Check if the output index is valid
725+
let vout = outpoint.vout as usize;
726+
if vout >= tx.output.len() {
727+
// Invalid output index
728+
return Ok(false);
729+
}
730+
731+
let script_pubkey = &tx.output[vout].script_pubkey;
732+
let electrum_client = Arc::clone(&self.electrum_client);
733+
let script_pubkey_clone = script_pubkey.clone();
734+
let outpoint_txid = outpoint.txid;
735+
let outpoint_vout = outpoint.vout;
736+
737+
let spawn_fut = self
738+
.runtime
739+
.spawn_blocking(move || electrum_client.script_list_unspent(&script_pubkey_clone));
740+
let timeout_fut = tokio::time::timeout(
741+
Duration::from_secs(
742+
self.sync_config.timeouts_config.lightning_wallet_sync_timeout_secs,
743+
),
744+
spawn_fut,
745+
);
746+
747+
match timeout_fut.await {
748+
Ok(res) => match res {
749+
Ok(inner_res) => match inner_res {
750+
Ok(unspent_list) => {
751+
// Check if our outpoint is in the unspent list
752+
let is_unspent = unspent_list.iter().any(|u| {
753+
u.tx_hash == outpoint_txid && u.tx_pos == outpoint_vout as usize
754+
});
755+
// Return true if spent (not in unspent list)
756+
Ok(!is_unspent)
757+
},
758+
Err(e) => {
759+
log_error!(
760+
self.logger,
761+
"Failed to check if outpoint {} is spent: {}",
762+
outpoint,
763+
e
764+
);
765+
Err(Error::TxSyncFailed)
766+
},
767+
},
768+
Err(e) => {
769+
log_error!(
770+
self.logger,
771+
"Failed to check if outpoint {} is spent: {}",
772+
outpoint,
773+
e
774+
);
775+
Err(Error::TxSyncFailed)
776+
},
777+
},
778+
Err(e) => {
779+
log_error!(
780+
self.logger,
781+
"Failed to check if outpoint {} is spent due to timeout: {}",
782+
outpoint,
783+
e
784+
);
785+
Err(Error::TxSyncTimeout)
786+
},
787+
}
788+
}
655789
}
656790

657791
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, 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};
@@ -459,6 +459,38 @@ impl ChainSource {
459459
}
460460
}
461461
}
462+
463+
pub(crate) fn can_broadcast_transaction(&self, tx: &Transaction) -> Result<bool, Error> {
464+
tokio::task::block_in_place(|| {
465+
tokio::runtime::Handle::current().block_on(async {
466+
match &self.kind {
467+
ChainSourceKind::Bitcoind(bitcoind_chain_source) => {
468+
bitcoind_chain_source.can_broadcast_transaction(tx).await
469+
},
470+
ChainSourceKind::Esplora{..} => {
471+
// Esplora doesn't support testmempoolaccept equivalent.
472+
unreachable!("Mempool accept testing is not supported with Esplora backend. Use BitcoindRpc for this functionality.")
473+
},
474+
ChainSourceKind::Electrum{..} => {
475+
// Electrum doesn't support testmempoolaccept equivalent.
476+
unreachable!("Mempool accept testing is not supported with Electrum backend. Use BitcoindRpc for this functionality.")
477+
},
478+
}
479+
})
480+
})
481+
}
482+
483+
pub(crate) fn get_transaction(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
484+
tokio::task::block_in_place(|| {
485+
tokio::runtime::Handle::current().block_on(async {
486+
match &self.kind {
487+
ChainSourceKind::Bitcoind(bitcoind) => bitcoind.get_transaction(txid).await,
488+
ChainSourceKind::Esplora(esplora) => esplora.get_transaction(txid).await,
489+
ChainSourceKind::Electrum(electrum) => electrum.get_transaction(txid).await,
490+
}
491+
})
492+
})
493+
}
462494
}
463495

464496
impl Filter for ChainSource {

src/config.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ use std::time::Duration;
1212

1313
use bitcoin::secp256k1::PublicKey;
1414
use bitcoin::Network;
15+
use bitreq::URL;
1516
use lightning::ln::msgs::SocketAddress;
1617
use lightning::routing::gossip::NodeAlias;
1718
use lightning::routing::router::RouteParametersConfig;
@@ -127,7 +128,8 @@ pub(crate) const HRN_RESOLUTION_TIMEOUT_SECS: u64 = 5;
127128
/// | `probing_liquidity_limit_multiplier` | 3 |
128129
/// | `log_level` | Debug |
129130
/// | `anchor_channels_config` | Some(..) |
130-
/// | `route_parameters` | None |
131+
/// | `route_parameters` | None |
132+
/// | `payjoin_config` | None |
131133
///
132134
/// See [`AnchorChannelsConfig`] and [`RouteParametersConfig`] for more information regarding their
133135
/// respective default values.
@@ -192,6 +194,8 @@ pub struct Config {
192194
/// **Note:** If unset, default parameters will be used, and you will be able to override the
193195
/// parameters on a per-payment basis in the corresponding method calls.
194196
pub route_parameters: Option<RouteParametersConfig>,
197+
/// Configuration options for PayJoin payments.
198+
pub payjoin_config: Option<PayjoinConfig>,
195199
}
196200

197201
impl Default for Config {
@@ -206,6 +210,7 @@ impl Default for Config {
206210
anchor_channels_config: Some(AnchorChannelsConfig::default()),
207211
route_parameters: None,
208212
node_alias: None,
213+
payjoin_config: None,
209214
}
210215
}
211216
}
@@ -608,6 +613,15 @@ pub enum AsyncPaymentsRole {
608613
Server,
609614
}
610615

616+
/// Configuration options for PayJoin payments.
617+
#[derive(Debug, Clone)]
618+
pub struct PayjoinConfig {
619+
/// The URL of the PayJoin directory to use for discovering PayJoin receivers.
620+
pub payjoin_directory: URL,
621+
/// The URL of the OHTTP relay to use for sending OHTTP requests to PayJoin receivers.
622+
pub ohttp_relay: URL,
623+
}
624+
611625
#[cfg(test)]
612626
mod tests {
613627
use std::str::FromStr;

0 commit comments

Comments
 (0)