@@ -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
12401459impl BlockSource for BitcoindClient {
0 commit comments