@@ -45,7 +45,7 @@ use crate::{
4545 EvmNetwork , HealthCheckFailure , JsonRpcRequest , JsonRpcResponse , NetworkRepoModel ,
4646 NetworkRpcRequest , NetworkRpcResult , NetworkTransactionRequest , NetworkType ,
4747 PaginationQuery , RelayerRepoModel , RelayerStatus , RepositoryError , RpcErrorCodes ,
48- TransactionRepoModel , TransactionStatus ,
48+ TransactionRepoModel , TransactionStatus , TransactionUpdateRequest ,
4949 } ,
5050 repositories:: { NetworkRepository , RelayerRepository , Repository , TransactionRepository } ,
5151 services:: {
@@ -57,7 +57,7 @@ use crate::{
5757} ;
5858use async_trait:: async_trait;
5959use eyre:: Result ;
60- use tracing:: { debug, info, instrument, warn} ;
60+ use tracing:: { debug, error , info, instrument, warn} ;
6161
6262use super :: { create_error_response, create_success_response, EvmTransactionValidator } ;
6363use crate :: utils:: { map_provider_error, sanitize_error_description} ;
@@ -292,16 +292,11 @@ where
292292 . await
293293 . map_err ( |e| RepositoryError :: TransactionFailure ( e. to_string ( ) ) ) ?;
294294
295- // Queue preparation job (immediate)
296- self . job_producer
297- . produce_transaction_request_job (
298- TransactionRequest :: new ( transaction. id . clone ( ) , transaction. relayer_id . clone ( ) ) ,
299- None ,
300- )
301- . await ?;
302-
303- // Queue status check job (with initial delay)
304- self . job_producer
295+ // Status check FIRST - this is our safety net for monitoring.
296+ // If this fails, mark transaction as failed and don't proceed.
297+ // This ensures we never have an unmonitored transaction.
298+ if let Err ( e) = self
299+ . job_producer
305300 . produce_check_transaction_status_job (
306301 TransactionStatusCheck :: new (
307302 transaction. id . clone ( ) ,
@@ -312,6 +307,44 @@ where
312307 EVM_STATUS_CHECK_INITIAL_DELAY_SECONDS ,
313308 ) ) ,
314309 )
310+ . await
311+ {
312+ // Status queue failed - mark transaction as failed to prevent orphaned tx
313+ error ! (
314+ relayer_id = %self . relayer. id,
315+ transaction_id = %transaction. id,
316+ error = %e,
317+ "Status check queue push failed - marking transaction as failed"
318+ ) ;
319+ if let Err ( update_err) = self
320+ . transaction_repository
321+ . partial_update (
322+ transaction. id . clone ( ) ,
323+ TransactionUpdateRequest {
324+ status : Some ( TransactionStatus :: Failed ) ,
325+ status_reason : Some ( "Queue unavailable" . to_string ( ) ) ,
326+ ..Default :: default ( )
327+ } ,
328+ )
329+ . await
330+ {
331+ warn ! (
332+ relayer_id = %self . relayer. id,
333+ transaction_id = %transaction. id,
334+ error = %update_err,
335+ "Failed to mark transaction as failed after queue push failure"
336+ ) ;
337+ }
338+ return Err ( e. into ( ) ) ;
339+ }
340+
341+ // Now safe to push transaction request.
342+ // Even if this fails, status check will monitor and detect the stuck transaction.
343+ self . job_producer
344+ . produce_transaction_request_job (
345+ TransactionRequest :: new ( transaction. id . clone ( ) , transaction. relayer_id . clone ( ) ) ,
346+ None ,
347+ )
315348 . await ?;
316349
317350 Ok ( transaction)
@@ -961,6 +994,138 @@ mod tests {
961994 assert ! ( result. is_ok( ) ) ;
962995 }
963996
997+ #[ tokio:: test]
998+ async fn test_process_transaction_request_status_check_failure_returns_error ( ) {
999+ let (
1000+ provider,
1001+ relayer_repo,
1002+ mut network_repo,
1003+ mut tx_repo,
1004+ mut job_producer,
1005+ signer,
1006+ counter,
1007+ ) = setup_mocks ( ) ;
1008+ let relayer_model = create_test_relayer ( ) ;
1009+
1010+ let network_tx = NetworkTransactionRequest :: Evm ( crate :: models:: EvmTransactionRequest {
1011+ to : Some ( "0xRecipient" . to_string ( ) ) ,
1012+ value : U256 :: from ( 1000000000000000000u64 ) ,
1013+ data : Some ( "0xData" . to_string ( ) ) ,
1014+ gas_limit : Some ( 21000 ) ,
1015+ gas_price : Some ( 20000000000 ) ,
1016+ max_fee_per_gas : None ,
1017+ max_priority_fee_per_gas : None ,
1018+ speed : None ,
1019+ valid_until : None ,
1020+ } ) ;
1021+
1022+ network_repo
1023+ . expect_get_by_name ( )
1024+ . with ( eq ( NetworkType :: Evm ) , eq ( "mainnet" ) )
1025+ . returning ( |_, _| Ok ( Some ( create_test_network_repo_model ( ) ) ) ) ;
1026+
1027+ tx_repo. expect_create ( ) . returning ( Ok ) ;
1028+ // When status check fails, transaction is marked as failed
1029+ tx_repo
1030+ . expect_partial_update ( )
1031+ . returning ( |_, _| Ok ( TransactionRepoModel :: default ( ) ) ) ;
1032+
1033+ // Status check fails
1034+ job_producer
1035+ . expect_produce_check_transaction_status_job ( )
1036+ . returning ( |_, _| {
1037+ Box :: pin ( ready ( Err ( crate :: jobs:: JobProducerError :: QueueError (
1038+ "Failed to queue job" . to_string ( ) ,
1039+ ) ) ) )
1040+ } ) ;
1041+
1042+ // Transaction request should NOT be called when status check fails
1043+ // (no expectation set = test fails if called)
1044+
1045+ let relayer = EvmRelayer :: new (
1046+ relayer_model,
1047+ signer,
1048+ provider,
1049+ create_test_evm_network ( ) ,
1050+ Arc :: new ( relayer_repo) ,
1051+ Arc :: new ( network_repo) ,
1052+ Arc :: new ( tx_repo) ,
1053+ Arc :: new ( counter) ,
1054+ Arc :: new ( job_producer) ,
1055+ )
1056+ . unwrap ( ) ;
1057+
1058+ let result = relayer. process_transaction_request ( network_tx) . await ;
1059+ assert ! ( result. is_err( ) ) ;
1060+ }
1061+
1062+ #[ tokio:: test]
1063+ async fn test_process_transaction_request_status_check_failure_marks_tx_failed ( ) {
1064+ let (
1065+ provider,
1066+ relayer_repo,
1067+ mut network_repo,
1068+ mut tx_repo,
1069+ mut job_producer,
1070+ signer,
1071+ counter,
1072+ ) = setup_mocks ( ) ;
1073+ let relayer_model = create_test_relayer ( ) ;
1074+
1075+ let network_tx = NetworkTransactionRequest :: Evm ( crate :: models:: EvmTransactionRequest {
1076+ to : Some ( "0xRecipient" . to_string ( ) ) ,
1077+ value : U256 :: from ( 1000000000000000000u64 ) ,
1078+ data : Some ( "0xData" . to_string ( ) ) ,
1079+ gas_limit : Some ( 21000 ) ,
1080+ gas_price : Some ( 20000000000 ) ,
1081+ max_fee_per_gas : None ,
1082+ max_priority_fee_per_gas : None ,
1083+ speed : None ,
1084+ valid_until : None ,
1085+ } ) ;
1086+
1087+ network_repo
1088+ . expect_get_by_name ( )
1089+ . with ( eq ( NetworkType :: Evm ) , eq ( "mainnet" ) )
1090+ . returning ( |_, _| Ok ( Some ( create_test_network_repo_model ( ) ) ) ) ;
1091+
1092+ tx_repo. expect_create ( ) . returning ( Ok ) ;
1093+
1094+ // Verify partial_update is called with correct status and reason
1095+ tx_repo
1096+ . expect_partial_update ( )
1097+ . withf ( |_tx_id, update| {
1098+ update. status == Some ( TransactionStatus :: Failed )
1099+ && update. status_reason == Some ( "Queue unavailable" . to_string ( ) )
1100+ } )
1101+ . returning ( |_, _| Ok ( TransactionRepoModel :: default ( ) ) ) ;
1102+
1103+ job_producer
1104+ . expect_produce_check_transaction_status_job ( )
1105+ . returning ( |_, _| {
1106+ Box :: pin ( ready ( Err ( crate :: jobs:: JobProducerError :: QueueError (
1107+ "Redis timeout" . to_string ( ) ,
1108+ ) ) ) )
1109+ } ) ;
1110+
1111+ let relayer = EvmRelayer :: new (
1112+ relayer_model,
1113+ signer,
1114+ provider,
1115+ create_test_evm_network ( ) ,
1116+ Arc :: new ( relayer_repo) ,
1117+ Arc :: new ( network_repo) ,
1118+ Arc :: new ( tx_repo) ,
1119+ Arc :: new ( counter) ,
1120+ Arc :: new ( job_producer) ,
1121+ )
1122+ . unwrap ( ) ;
1123+
1124+ let result = relayer. process_transaction_request ( network_tx) . await ;
1125+ assert ! ( result. is_err( ) ) ;
1126+ // The mock verification (withf) ensures partial_update was called correctly
1127+ }
1128+
9641129 #[ tokio:: test]
9651130 async fn test_validate_min_balance_sufficient ( ) {
9661131 let ( mut provider, relayer_repo, network_repo, tx_repo, job_producer, signer, counter) =
0 commit comments