Skip to content

Commit 818fbcb

Browse files
authored
fix: Improve Evm and Solana transaction request flow (#655)
1 parent 0ecffbb commit 818fbcb

File tree

2 files changed

+340
-22
lines changed

2 files changed

+340
-22
lines changed

src/domain/relayer/evm/evm_relayer.rs

Lines changed: 177 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -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
};
5858
use async_trait::async_trait;
5959
use eyre::Result;
60-
use tracing::{debug, info, instrument, warn};
60+
use tracing::{debug, error, info, instrument, warn};
6161

6262
use super::{create_error_response, create_success_response, EvmTransactionValidator};
6363
use 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

Comments
 (0)