Skip to content

Commit bf4c904

Browse files
committed
receive payjoin payments
1 parent c9218c0 commit bf4c904

15 files changed

Lines changed: 1623 additions & 4 deletions

File tree

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 = "6796e87525d6c564e1332354a808730e2ba2ebf8" }
8282

83+
payjoin = { version = "1.0.0-rc.1", 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: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -618,6 +618,89 @@ impl BitcoindChainSource {
618618
}
619619
}
620620
}
621+
622+
pub(crate) async fn can_broadcast_transaction(&self, tx: &Transaction) -> Result<bool, Error> {
623+
let timeout_fut = tokio::time::timeout(
624+
Duration::from_secs(TX_BROADCAST_TIMEOUT_SECS),
625+
self.api_client.test_mempool_accept(tx),
626+
);
627+
628+
match timeout_fut.await {
629+
Ok(res) => res.map_err(|e| {
630+
log_error!(
631+
self.logger,
632+
"Failed to test mempool accept for transaction {}: {}",
633+
tx.compute_txid(),
634+
e
635+
);
636+
Error::TxBroadcastFailed
637+
}),
638+
Err(e) => {
639+
log_error!(
640+
self.logger,
641+
"Failed to test mempool accept for transaction {} due to timeout: {}",
642+
tx.compute_txid(),
643+
e
644+
);
645+
log_trace!(
646+
self.logger,
647+
"Failed test mempool accept transaction bytes: {}",
648+
log_bytes!(tx.encode())
649+
);
650+
Err(Error::TxBroadcastFailed)
651+
},
652+
}
653+
}
654+
655+
pub(crate) async fn get_transaction(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
656+
let timeout_fut = tokio::time::timeout(
657+
Duration::from_secs(TX_BROADCAST_TIMEOUT_SECS),
658+
self.api_client.get_raw_transaction(txid),
659+
);
660+
661+
match timeout_fut.await {
662+
Ok(res) => res.map_err(|e| {
663+
log_error!(self.logger, "Failed to get transaction {}: {}", txid, e);
664+
Error::TxSyncFailed
665+
}),
666+
Err(e) => {
667+
log_error!(self.logger, "Failed to get transaction {} due to timeout: {}", txid, e);
668+
Err(Error::TxSyncTimeout)
669+
},
670+
}
671+
}
672+
673+
pub(crate) async fn is_outpoint_spent(&self, outpoint: &OutPoint) -> Result<bool, Error> {
674+
let timeout_fut = tokio::time::timeout(
675+
Duration::from_secs(TX_BROADCAST_TIMEOUT_SECS),
676+
self.api_client.get_tx_out(outpoint),
677+
);
678+
679+
match timeout_fut.await {
680+
Ok(res) => {
681+
// get_tx_out returns true if output exists (unspent), false if spent
682+
// We want to return true if spent, so invert the result
683+
res.map(|exists| !exists).map_err(|e| {
684+
log_error!(
685+
self.logger,
686+
"Failed to check if outpoint {} is spent: {}",
687+
outpoint,
688+
e
689+
);
690+
Error::TxSyncFailed
691+
})
692+
},
693+
Err(e) => {
694+
log_error!(
695+
self.logger,
696+
"Failed to check if outpoint {} is spent due to timeout: {}",
697+
outpoint,
698+
e
699+
);
700+
Err(Error::TxSyncTimeout)
701+
},
702+
}
703+
}
621704
}
622705

623706
#[derive(Clone)]
@@ -1235,6 +1318,142 @@ impl BitcoindClient {
12351318
.collect();
12361319
Ok(evicted_txids)
12371320
}
1321+
1322+
/// Tests whether the provided transaction would be accepted by the mempool.
1323+
pub(crate) async fn test_mempool_accept(&self, tx: &Transaction) -> std::io::Result<bool> {
1324+
match self {
1325+
BitcoindClient::Rpc { rpc_client, .. } => {
1326+
Self::test_mempool_accept_inner(Arc::clone(rpc_client), tx).await
1327+
},
1328+
BitcoindClient::Rest { rpc_client, .. } => {
1329+
// We rely on the internal RPC client to make this call, as this
1330+
// operation is not supported by Bitcoin Core's REST interface.
1331+
Self::test_mempool_accept_inner(Arc::clone(rpc_client), tx).await
1332+
},
1333+
}
1334+
}
1335+
1336+
async fn test_mempool_accept_inner(
1337+
rpc_client: Arc<RpcClient>, tx: &Transaction,
1338+
) -> std::io::Result<bool> {
1339+
let tx_serialized = bitcoin::consensus::encode::serialize_hex(tx);
1340+
let tx_array = serde_json::json!([tx_serialized]);
1341+
1342+
let resp =
1343+
rpc_client.call_method::<serde_json::Value>("testmempoolaccept", &[tx_array]).await?;
1344+
1345+
if let Some(array) = resp.as_array() {
1346+
if let Some(first_result) = array.first() {
1347+
Ok(first_result.get("allowed").and_then(|v| v.as_bool()).unwrap_or(false))
1348+
} else {
1349+
Err(std::io::Error::new(
1350+
std::io::ErrorKind::Other,
1351+
"Empty array response from testmempoolaccept",
1352+
))
1353+
}
1354+
} else {
1355+
Err(std::io::Error::new(
1356+
std::io::ErrorKind::InvalidData,
1357+
"testmempoolaccept did not return an array",
1358+
))
1359+
}
1360+
}
1361+
1362+
/// Checks if a transaction output is unspent. Returns `true` if the output exists and is
1363+
/// unspent, `false` otherwise.
1364+
pub(crate) async fn get_tx_out(&self, outpoint: &OutPoint) -> std::io::Result<bool> {
1365+
match self {
1366+
BitcoindClient::Rpc { rpc_client, .. } => {
1367+
Self::get_tx_out_rpc(Arc::clone(rpc_client), outpoint).await
1368+
},
1369+
BitcoindClient::Rest { rest_client, .. } => {
1370+
Self::get_tx_out_rest(Arc::clone(rest_client), outpoint).await
1371+
},
1372+
}
1373+
}
1374+
1375+
/// Check if output is unspent via RPC interface.
1376+
async fn get_tx_out_rpc(
1377+
rpc_client: Arc<RpcClient>, outpoint: &OutPoint,
1378+
) -> std::io::Result<bool> {
1379+
let txid_hex = outpoint.txid.to_string();
1380+
let txid_json = serde_json::json!(txid_hex);
1381+
let vout_json = serde_json::json!(outpoint.vout);
1382+
// include_mempool = true to also check mempool for unspent outputs
1383+
let include_mempool_json = serde_json::json!(true);
1384+
1385+
match rpc_client
1386+
.call_method::<serde_json::Value>(
1387+
"gettxout",
1388+
&[txid_json, vout_json, include_mempool_json],
1389+
)
1390+
.await
1391+
{
1392+
Ok(resp) => {
1393+
// If response is null, the output is spent or doesn't exist
1394+
Ok(!resp.is_null())
1395+
},
1396+
Err(e) => Err(std::io::Error::new(
1397+
std::io::ErrorKind::Other,
1398+
format!("Failed to check tx output: {}", e),
1399+
)),
1400+
}
1401+
}
1402+
1403+
/// Check if output is unspent via REST interface.
1404+
async fn get_tx_out_rest(
1405+
rest_client: Arc<RestClient>, outpoint: &OutPoint,
1406+
) -> std::io::Result<bool> {
1407+
let txid_hex = outpoint.txid.to_string();
1408+
// REST endpoint: /rest/getutxos/<checkmempool>/<txid>-<n>.json
1409+
let utxo_path = format!("getutxos/checkmempool/{}-{}.json", txid_hex, outpoint.vout);
1410+
1411+
match rest_client.request_resource::<JsonResponse, serde_json::Value>(&utxo_path).await {
1412+
Ok(resp) => {
1413+
// Check if the utxos array has entries
1414+
if let Some(utxos) = resp.get("utxos") {
1415+
if let Some(arr) = utxos.as_array() {
1416+
return Ok(!arr.is_empty());
1417+
}
1418+
}
1419+
Ok(false)
1420+
},
1421+
Err(e) => match e.kind() {
1422+
std::io::ErrorKind::Other => {
1423+
match e.into_inner() {
1424+
Some(inner) => {
1425+
let http_error_res: Result<Box<HttpError>, _> = inner.downcast();
1426+
match http_error_res {
1427+
Ok(http_error) => {
1428+
// 404 means UTXO not found = spent
1429+
if &http_error.status_code == "404" {
1430+
Ok(false)
1431+
} else {
1432+
Err(std::io::Error::new(
1433+
std::io::ErrorKind::Other,
1434+
http_error,
1435+
))
1436+
}
1437+
},
1438+
Err(_) => Err(std::io::Error::new(
1439+
std::io::ErrorKind::Other,
1440+
"Failed to process getutxos response",
1441+
)),
1442+
}
1443+
},
1444+
None => Err(std::io::Error::new(
1445+
std::io::ErrorKind::Other,
1446+
"Failed to process getutxos response",
1447+
)),
1448+
}
1449+
},
1450+
_ => Err(std::io::Error::new(
1451+
std::io::ErrorKind::Other,
1452+
"Failed to process getutxos response",
1453+
)),
1454+
},
1455+
}
1456+
}
12381457
}
12391458

12401459
impl BlockSource for BitcoindClient {

0 commit comments

Comments
 (0)