use chrono::{DateTime, Duration, Utc};
use eyre::Result;
use log::info;
use super::EvmRelayerTransaction;
use super::{
get_age_of_sent_at, has_enough_confirmations, is_noop, is_transaction_valid, make_noop,
too_many_attempts, too_many_noop_attempts,
};
use crate::{
domain::transaction::evm::price_calculator::PriceCalculatorTrait,
jobs::JobProducerTrait,
models::{
EvmNetwork, NetworkTransactionData, RelayerRepoModel, TransactionError,
TransactionRepoModel, TransactionStatus, TransactionUpdateRequest,
},
repositories::{Repository, TransactionCounterTrait, TransactionRepository},
services::{EvmProviderTrait, Signer},
utils::{get_resubmit_timeout_for_speed, get_resubmit_timeout_with_backoff},
};
impl<P, R, T, J, S, C, PC> EvmRelayerTransaction<P, R, T, J, S, C, PC>
where
P: EvmProviderTrait + Send + Sync,
R: Repository<RelayerRepoModel, String> + Send + Sync,
T: TransactionRepository + Send + Sync,
J: JobProducerTrait + Send + Sync,
S: Signer + Send + Sync,
C: TransactionCounterTrait + Send + Sync,
PC: PriceCalculatorTrait + Send + Sync,
{
pub(super) async fn check_transaction_status(
&self,
tx: &TransactionRepoModel,
) -> Result<TransactionStatus, TransactionError> {
if tx.status == TransactionStatus::Expired
|| tx.status == TransactionStatus::Failed
|| tx.status == TransactionStatus::Confirmed
{
return Ok(tx.status.clone());
}
let evm_data = tx.network_data.get_evm_transaction_data()?;
let tx_hash = evm_data
.hash
.as_ref()
.ok_or(TransactionError::UnexpectedError(
"Transaction hash is missing".to_string(),
))?;
let receipt_result = self.provider().get_transaction_receipt(tx_hash).await?;
if let Some(receipt) = receipt_result {
if !receipt.status() {
return Ok(TransactionStatus::Failed);
}
let last_block_number = self.provider().get_block_number().await?;
let tx_block_number = receipt
.block_number
.ok_or(TransactionError::UnexpectedError(
"Transaction receipt missing block number".to_string(),
))?;
if !has_enough_confirmations(tx_block_number, last_block_number, evm_data.chain_id) {
info!("Transaction mined but not confirmed: {}", tx_hash);
return Ok(TransactionStatus::Mined);
}
Ok(TransactionStatus::Confirmed)
} else {
info!("Transaction not yet mined: {}", tx_hash);
Ok(TransactionStatus::Submitted)
}
}
pub(super) async fn should_resubmit(
&self,
tx: &TransactionRepoModel,
) -> Result<bool, TransactionError> {
if tx.status != TransactionStatus::Submitted {
return Err(TransactionError::UnexpectedError(format!(
"Transaction must be in Submitted status to resubmit, found: {:?}",
tx.status
)));
}
let age = get_age_of_sent_at(tx)?;
let timeout = match tx.network_data.get_evm_transaction_data() {
Ok(data) => get_resubmit_timeout_for_speed(&data.speed),
Err(e) => return Err(e),
};
let timeout_with_backoff = get_resubmit_timeout_with_backoff(timeout, tx.hashes.len());
if age > Duration::milliseconds(timeout_with_backoff) {
info!("Transaction has been pending for too long, resubmitting");
return Ok(true);
}
Ok(false)
}
pub(super) async fn should_noop(
&self,
tx: &TransactionRepoModel,
) -> Result<bool, TransactionError> {
if too_many_noop_attempts(tx) {
info!("Transaction has too many NOOP attempts already");
return Ok(false);
}
let evm_data = tx.network_data.get_evm_transaction_data()?;
if is_noop(&evm_data) {
return Ok(false);
}
let network = EvmNetwork::from_id(evm_data.chain_id);
if network.is_rollup() && too_many_attempts(tx) {
info!("Rollup transaction has too many attempts, will replace with NOOP");
return Ok(true);
}
if !is_transaction_valid(&tx.created_at, &tx.valid_until) {
info!("Transaction is expired, will replace with NOOP");
return Ok(true);
}
if tx.status == TransactionStatus::Pending {
let created_at = &tx.created_at;
let created_time = DateTime::parse_from_rfc3339(created_at)
.map_err(|_| {
TransactionError::UnexpectedError("Error parsing created_at time".to_string())
})?
.with_timezone(&Utc);
let age = Utc::now().signed_duration_since(created_time);
if age > Duration::minutes(1) {
info!("Transaction in Pending state for over 1 minute, will replace with NOOP");
return Ok(true);
}
}
Ok(false)
}
pub(super) async fn update_transaction_status_if_needed(
&self,
tx: TransactionRepoModel,
new_status: TransactionStatus,
) -> Result<TransactionRepoModel, TransactionError> {
if tx.status != new_status {
return self.update_transaction_status(tx, new_status).await;
}
Ok(tx)
}
pub(super) async fn prepare_noop_update_request(
&self,
tx: &TransactionRepoModel,
is_cancellation: bool,
) -> Result<TransactionUpdateRequest, TransactionError> {
let mut evm_data = tx.network_data.get_evm_transaction_data()?;
make_noop(&mut evm_data).await?;
let noop_count = tx.noop_count.unwrap_or(0) + 1;
let update_request = TransactionUpdateRequest {
network_data: Some(NetworkTransactionData::Evm(evm_data)),
noop_count: Some(noop_count),
is_canceled: if is_cancellation {
Some(true)
} else {
tx.is_canceled
},
..Default::default()
};
Ok(update_request)
}
async fn handle_submitted_state(
&self,
tx: TransactionRepoModel,
) -> Result<TransactionRepoModel, TransactionError> {
if self.should_resubmit(&tx).await? {
return self.handle_resubmission(tx).await;
}
self.schedule_status_check(&tx, Some(5)).await?;
self.update_transaction_status_if_needed(tx, TransactionStatus::Submitted)
.await
}
async fn handle_resubmission(
&self,
tx: TransactionRepoModel,
) -> Result<TransactionRepoModel, TransactionError> {
info!("Scheduling resubmit job for transaction: {}", tx.id);
let tx_to_process = if self.should_noop(&tx).await? {
self.process_noop_transaction(&tx).await?
} else {
tx
};
self.send_transaction_resubmit_job(&tx_to_process).await?;
Ok(tx_to_process)
}
async fn process_noop_transaction(
&self,
tx: &TransactionRepoModel,
) -> Result<TransactionRepoModel, TransactionError> {
info!("Preparing transaction NOOP before resubmission: {}", tx.id);
let update = self.prepare_noop_update_request(tx, false).await?;
let updated_tx = self
.transaction_repository()
.partial_update(tx.id.clone(), update)
.await?;
self.send_transaction_update_notification(&updated_tx)
.await?;
Ok(updated_tx)
}
async fn handle_pending_state(
&self,
tx: TransactionRepoModel,
) -> Result<TransactionRepoModel, TransactionError> {
if self.should_noop(&tx).await? {
info!("Preparing NOOP for pending transaction: {}", tx.id);
let update = self.prepare_noop_update_request(&tx, false).await?;
let updated_tx = self
.transaction_repository()
.partial_update(tx.id.clone(), update)
.await?;
self.send_transaction_submit_job(&updated_tx).await?;
self.send_transaction_update_notification(&updated_tx)
.await?;
return Ok(updated_tx);
}
Ok(tx)
}
async fn handle_mined_state(
&self,
tx: TransactionRepoModel,
) -> Result<TransactionRepoModel, TransactionError> {
self.schedule_status_check(&tx, Some(5)).await?;
self.update_transaction_status_if_needed(tx, TransactionStatus::Mined)
.await
}
async fn handle_final_state(
&self,
tx: TransactionRepoModel,
status: TransactionStatus,
) -> Result<TransactionRepoModel, TransactionError> {
self.update_transaction_status_if_needed(tx, status).await
}
pub async fn handle_status_impl(
&self,
tx: TransactionRepoModel,
) -> Result<TransactionRepoModel, TransactionError> {
info!("Checking transaction status for tx: {:?}", tx.id);
let status = self.check_transaction_status(&tx).await?;
info!("Transaction status: {:?}", status);
match status {
TransactionStatus::Submitted => self.handle_submitted_state(tx).await,
TransactionStatus::Pending => self.handle_pending_state(tx).await,
TransactionStatus::Mined => self.handle_mined_state(tx).await,
TransactionStatus::Confirmed
| TransactionStatus::Failed
| TransactionStatus::Expired => self.handle_final_state(tx, status).await,
_ => Err(TransactionError::UnexpectedError(format!(
"Unexpected transaction status: {:?}",
status
))),
}
}
}
#[cfg(test)]
mod tests {
use crate::{
domain::{transaction::evm::EvmRelayerTransaction, MockPriceCalculatorTrait},
jobs::MockJobProducerTrait,
models::{
evm::Speed, EvmTransactionData, NetworkTransactionData, NetworkType, RelayerEvmPolicy,
RelayerNetworkPolicy, RelayerRepoModel, TransactionRepoModel, TransactionStatus, U256,
},
repositories::{MockRepository, MockTransactionCounterTrait, MockTransactionRepository},
services::{MockEvmProviderTrait, MockSigner},
};
use alloy::{
consensus::{Eip658Value, Receipt, ReceiptEnvelope, ReceiptWithBloom},
primitives::{b256, Address, BlockHash, Bloom, TxHash},
rpc::types::TransactionReceipt,
};
use chrono::{Duration, Utc};
use std::sync::Arc;
pub struct TestMocks {
pub provider: MockEvmProviderTrait,
pub relayer_repo: MockRepository<RelayerRepoModel, String>,
pub tx_repo: MockTransactionRepository,
pub job_producer: MockJobProducerTrait,
pub signer: MockSigner,
pub counter: MockTransactionCounterTrait,
pub price_calc: MockPriceCalculatorTrait,
}
pub fn default_test_mocks() -> TestMocks {
TestMocks {
provider: MockEvmProviderTrait::new(),
relayer_repo: MockRepository::new(),
tx_repo: MockTransactionRepository::new(),
job_producer: MockJobProducerTrait::new(),
signer: MockSigner::new(),
counter: MockTransactionCounterTrait::new(),
price_calc: MockPriceCalculatorTrait::new(),
}
}
pub fn make_test_transaction(status: TransactionStatus) -> TransactionRepoModel {
TransactionRepoModel {
id: "test-tx-id".to_string(),
relayer_id: "test-relayer-id".to_string(),
status,
created_at: Utc::now().to_rfc3339(),
sent_at: None,
confirmed_at: None,
valid_until: None,
network_type: NetworkType::Evm,
network_data: NetworkTransactionData::Evm(EvmTransactionData {
chain_id: 1,
from: "0xSender".to_string(),
to: Some("0xRecipient".to_string()),
value: U256::from(0),
data: Some("0xData".to_string()),
gas_limit: 21000,
gas_price: Some(20000000000),
max_fee_per_gas: None,
max_priority_fee_per_gas: None,
nonce: None,
signature: None,
hash: None,
speed: Some(Speed::Fast),
raw: None,
}),
priced_at: None,
hashes: Vec::new(),
noop_count: None,
is_canceled: Some(false),
}
}
pub fn make_test_evm_relayer_transaction(
relayer: RelayerRepoModel,
mocks: TestMocks,
) -> EvmRelayerTransaction<
MockEvmProviderTrait,
MockRepository<RelayerRepoModel, String>,
MockTransactionRepository,
MockJobProducerTrait,
MockSigner,
MockTransactionCounterTrait,
MockPriceCalculatorTrait,
> {
EvmRelayerTransaction::new(
relayer,
mocks.provider,
Arc::new(mocks.relayer_repo),
Arc::new(mocks.tx_repo),
Arc::new(mocks.counter),
Arc::new(mocks.job_producer),
mocks.price_calc,
mocks.signer,
)
.unwrap()
}
fn create_test_relayer() -> RelayerRepoModel {
RelayerRepoModel {
id: "test-relayer-id".to_string(),
name: "Test Relayer".to_string(),
paused: false,
system_disabled: false,
network: "test_network".to_string(),
network_type: NetworkType::Evm,
policies: RelayerNetworkPolicy::Evm(RelayerEvmPolicy::default()),
signer_id: "test_signer".to_string(),
address: "0x".to_string(),
notification_id: None,
custom_rpc_urls: None,
}
}
fn make_mock_receipt(status: bool, block_number: Option<u64>) -> TransactionReceipt {
let tx_hash = TxHash::from(b256!(
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
));
let block_hash = BlockHash::from(b256!(
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
));
let from_address = Address::from([0x11; 20]);
TransactionReceipt {
inner: ReceiptEnvelope::Legacy(ReceiptWithBloom {
receipt: Receipt {
status: Eip658Value::Eip658(status), cumulative_gas_used: 0,
logs: vec![],
},
logs_bloom: Bloom::ZERO,
}),
transaction_hash: tx_hash,
transaction_index: Some(0),
block_hash: block_number.map(|_| block_hash), block_number,
gas_used: 21000,
effective_gas_price: 1000,
blob_gas_used: None,
blob_gas_price: None,
from: from_address,
to: None,
contract_address: None,
authorization_list: None,
}
}
mod check_transaction_status_tests {
use super::*;
#[tokio::test]
async fn test_not_mined() {
let mut mocks = default_test_mocks();
let relayer = create_test_relayer();
let mut tx = make_test_transaction(TransactionStatus::Submitted);
if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
evm_data.hash = Some("0xFakeHash".to_string());
}
mocks
.provider
.expect_get_transaction_receipt()
.returning(|_| Box::pin(async { Ok(None) }));
let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
let status = evm_transaction.check_transaction_status(&tx).await.unwrap();
assert_eq!(status, TransactionStatus::Submitted);
}
#[tokio::test]
async fn test_mined_but_not_confirmed() {
let mut mocks = default_test_mocks();
let relayer = create_test_relayer();
let mut tx = make_test_transaction(TransactionStatus::Submitted);
if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
evm_data.hash = Some("0xFakeHash".to_string());
}
mocks
.provider
.expect_get_transaction_receipt()
.returning(|_| Box::pin(async { Ok(Some(make_mock_receipt(true, Some(100)))) }));
mocks
.provider
.expect_get_block_number()
.return_once(|| Box::pin(async { Ok(100) }));
let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
let status = evm_transaction.check_transaction_status(&tx).await.unwrap();
assert_eq!(status, TransactionStatus::Mined);
}
#[tokio::test]
async fn test_confirmed() {
let mut mocks = default_test_mocks();
let relayer = create_test_relayer();
let mut tx = make_test_transaction(TransactionStatus::Submitted);
if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
evm_data.hash = Some("0xFakeHash".to_string());
}
mocks
.provider
.expect_get_transaction_receipt()
.returning(|_| Box::pin(async { Ok(Some(make_mock_receipt(true, Some(100)))) }));
mocks
.provider
.expect_get_block_number()
.return_once(|| Box::pin(async { Ok(113) }));
let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
let status = evm_transaction.check_transaction_status(&tx).await.unwrap();
assert_eq!(status, TransactionStatus::Confirmed);
}
#[tokio::test]
async fn test_failed() {
let mut mocks = default_test_mocks();
let relayer = create_test_relayer();
let mut tx = make_test_transaction(TransactionStatus::Submitted);
if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
evm_data.hash = Some("0xFakeHash".to_string());
}
mocks
.provider
.expect_get_transaction_receipt()
.returning(|_| Box::pin(async { Ok(Some(make_mock_receipt(false, Some(100)))) }));
let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
let status = evm_transaction.check_transaction_status(&tx).await.unwrap();
assert_eq!(status, TransactionStatus::Failed);
}
}
mod should_resubmit_tests {
use super::*;
#[tokio::test]
async fn test_should_resubmit_true() {
let mocks = default_test_mocks();
let relayer = create_test_relayer();
let mut tx = make_test_transaction(TransactionStatus::Submitted);
tx.sent_at = Some((Utc::now() - Duration::seconds(600)).to_rfc3339());
let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
let res = evm_transaction.should_resubmit(&tx).await.unwrap();
assert!(res, "Transaction should be resubmitted after timeout.");
}
#[tokio::test]
async fn test_should_resubmit_false() {
let mocks = default_test_mocks();
let relayer = create_test_relayer();
let mut tx = make_test_transaction(TransactionStatus::Submitted);
tx.sent_at = Some(Utc::now().to_rfc3339());
let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
let res = evm_transaction.should_resubmit(&tx).await.unwrap();
assert!(!res, "Transaction should not be resubmitted immediately.");
}
}
mod should_noop_tests {
use super::*;
#[tokio::test]
async fn test_expired_transaction_triggers_noop() {
let mocks = default_test_mocks();
let relayer = create_test_relayer();
let mut tx = make_test_transaction(TransactionStatus::Submitted);
tx.valid_until = Some((Utc::now() - Duration::seconds(10)).to_rfc3339());
let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
let res = evm_transaction.should_noop(&tx).await.unwrap();
assert!(res, "Expired transaction should be replaced with a NOOP.");
}
}
mod update_transaction_status_tests {
use super::*;
#[tokio::test]
async fn test_no_update_when_status_is_same() {
let mocks = default_test_mocks();
let relayer = create_test_relayer();
let tx = make_test_transaction(TransactionStatus::Submitted);
let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
let updated_tx = evm_transaction
.update_transaction_status_if_needed(tx.clone(), TransactionStatus::Submitted)
.await
.unwrap();
assert_eq!(updated_tx.status, TransactionStatus::Submitted);
assert_eq!(updated_tx.id, tx.id);
}
}
mod prepare_noop_update_request_tests {
use super::*;
#[tokio::test]
async fn test_noop_request_without_cancellation() {
let mocks = default_test_mocks();
let relayer = create_test_relayer();
let mut tx = make_test_transaction(TransactionStatus::Submitted);
tx.noop_count = Some(2);
tx.is_canceled = Some(false);
let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
let update_req = evm_transaction
.prepare_noop_update_request(&tx, false)
.await
.unwrap();
assert_eq!(update_req.noop_count, Some(3));
assert_eq!(update_req.is_canceled, Some(false));
}
#[tokio::test]
async fn test_noop_request_with_cancellation() {
let mocks = default_test_mocks();
let relayer = create_test_relayer();
let mut tx = make_test_transaction(TransactionStatus::Submitted);
tx.noop_count = None;
tx.is_canceled = Some(false);
let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
let update_req = evm_transaction
.prepare_noop_update_request(&tx, true)
.await
.unwrap();
assert_eq!(update_req.noop_count, Some(1));
assert_eq!(update_req.is_canceled, Some(true));
}
}
mod handle_submitted_state_tests {
use super::*;
#[tokio::test]
async fn test_schedules_resubmit_job() {
let mut mocks = default_test_mocks();
let relayer = create_test_relayer();
let mut tx = make_test_transaction(TransactionStatus::Submitted);
tx.sent_at = Some((Utc::now() - Duration::seconds(600)).to_rfc3339());
mocks
.provider
.expect_get_transaction_receipt()
.returning(|_| Box::pin(async { Ok(None) }));
mocks
.job_producer
.expect_produce_submit_transaction_job()
.returning(|_, _| Box::pin(async { Ok(()) }));
mocks
.tx_repo
.expect_partial_update()
.returning(|_, _| Ok(make_test_transaction(TransactionStatus::Submitted)));
let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
let updated_tx = evm_transaction.handle_submitted_state(tx).await.unwrap();
assert_eq!(updated_tx.status, TransactionStatus::Submitted);
}
}
mod handle_pending_state_tests {
use super::*;
#[tokio::test]
async fn test_pending_state_no_noop() {
let mocks = default_test_mocks();
let relayer = create_test_relayer();
let mut tx = make_test_transaction(TransactionStatus::Pending);
tx.created_at = Utc::now().to_rfc3339(); let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
let result = evm_transaction
.handle_pending_state(tx.clone())
.await
.unwrap();
assert_eq!(result.id, tx.id);
assert_eq!(result.status, tx.status);
assert_eq!(result.noop_count, tx.noop_count);
}
#[tokio::test]
async fn test_pending_state_with_noop() {
let mut mocks = default_test_mocks();
let relayer = create_test_relayer();
let mut tx = make_test_transaction(TransactionStatus::Pending);
tx.created_at = (Utc::now() - Duration::minutes(2)).to_rfc3339();
let tx_clone = tx.clone();
mocks
.tx_repo
.expect_partial_update()
.returning(move |_, update| {
let mut updated_tx = tx_clone.clone();
updated_tx.noop_count = update.noop_count;
Ok(updated_tx)
});
mocks
.job_producer
.expect_produce_submit_transaction_job()
.returning(|_, _| Box::pin(async { Ok(()) }));
mocks
.job_producer
.expect_produce_send_notification_job()
.returning(|_, _| Box::pin(async { Ok(()) }));
let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
let result = evm_transaction
.handle_pending_state(tx.clone())
.await
.unwrap();
assert!(result.noop_count.unwrap_or(0) > 0);
}
}
mod handle_mined_state_tests {
use super::*;
#[tokio::test]
async fn test_updates_status_and_schedules_check() {
let mut mocks = default_test_mocks();
let relayer = create_test_relayer();
let tx = make_test_transaction(TransactionStatus::Submitted);
mocks
.job_producer
.expect_produce_check_transaction_status_job()
.returning(|_, _| Box::pin(async { Ok(()) }));
mocks
.tx_repo
.expect_partial_update()
.returning(|_, update| {
let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
updated_tx.status = update.status.unwrap_or(updated_tx.status);
Ok(updated_tx)
});
let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
let result = evm_transaction
.handle_mined_state(tx.clone())
.await
.unwrap();
assert_eq!(result.status, TransactionStatus::Mined);
}
}
mod handle_final_state_tests {
use super::*;
#[tokio::test]
async fn test_final_state_confirmed() {
let mut mocks = default_test_mocks();
let relayer = create_test_relayer();
let tx = make_test_transaction(TransactionStatus::Submitted);
mocks
.tx_repo
.expect_partial_update()
.returning(|_, update| {
let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
updated_tx.status = update.status.unwrap_or(updated_tx.status);
Ok(updated_tx)
});
let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
let result = evm_transaction
.handle_final_state(tx.clone(), TransactionStatus::Confirmed)
.await
.unwrap();
assert_eq!(result.status, TransactionStatus::Confirmed);
}
#[tokio::test]
async fn test_final_state_failed() {
let mut mocks = default_test_mocks();
let relayer = create_test_relayer();
let tx = make_test_transaction(TransactionStatus::Submitted);
mocks
.tx_repo
.expect_partial_update()
.returning(|_, update| {
let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
updated_tx.status = update.status.unwrap_or(updated_tx.status);
Ok(updated_tx)
});
let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
let result = evm_transaction
.handle_final_state(tx.clone(), TransactionStatus::Failed)
.await
.unwrap();
assert_eq!(result.status, TransactionStatus::Failed);
}
#[tokio::test]
async fn test_final_state_expired() {
let mut mocks = default_test_mocks();
let relayer = create_test_relayer();
let tx = make_test_transaction(TransactionStatus::Submitted);
mocks
.tx_repo
.expect_partial_update()
.returning(|_, update| {
let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
updated_tx.status = update.status.unwrap_or(updated_tx.status);
Ok(updated_tx)
});
let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
let result = evm_transaction
.handle_final_state(tx.clone(), TransactionStatus::Expired)
.await
.unwrap();
assert_eq!(result.status, TransactionStatus::Expired);
}
}
mod handle_status_impl_tests {
use super::*;
#[tokio::test]
async fn test_impl_submitted_branch() {
let mut mocks = default_test_mocks();
let relayer = create_test_relayer();
let mut tx = make_test_transaction(TransactionStatus::Submitted);
tx.sent_at = Some((Utc::now() - Duration::seconds(120)).to_rfc3339());
if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
evm_data.hash = Some("0xFakeHash".to_string());
}
mocks
.provider
.expect_get_transaction_receipt()
.returning(|_| Box::pin(async { Ok(None) }));
mocks
.job_producer
.expect_produce_check_transaction_status_job()
.returning(|_, _| Box::pin(async { Ok(()) }));
mocks
.tx_repo
.expect_partial_update()
.returning(|_, update| {
let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
updated_tx.status = update.status.unwrap_or(updated_tx.status);
Ok(updated_tx)
});
let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
let result = evm_transaction.handle_status_impl(tx).await.unwrap();
assert_eq!(result.status, TransactionStatus::Submitted);
}
#[tokio::test]
async fn test_impl_mined_branch() {
let mut mocks = default_test_mocks();
let relayer = create_test_relayer();
let mut tx = make_test_transaction(TransactionStatus::Submitted);
if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
evm_data.hash = Some("0xFakeHash".to_string());
}
mocks
.provider
.expect_get_transaction_receipt()
.returning(|_| Box::pin(async { Ok(Some(make_mock_receipt(true, Some(100)))) }));
mocks
.provider
.expect_get_block_number()
.return_once(|| Box::pin(async { Ok(100) }));
mocks
.job_producer
.expect_produce_check_transaction_status_job()
.returning(|_, _| Box::pin(async { Ok(()) }));
mocks
.tx_repo
.expect_partial_update()
.returning(|_, update| {
let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
updated_tx.status = update.status.unwrap_or(updated_tx.status);
Ok(updated_tx)
});
let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
let result = evm_transaction.handle_status_impl(tx).await.unwrap();
assert_eq!(result.status, TransactionStatus::Mined);
}
#[tokio::test]
async fn test_impl_final_confirmed_branch() {
let mut mocks = default_test_mocks();
let relayer = create_test_relayer();
let tx = make_test_transaction(TransactionStatus::Confirmed);
mocks
.tx_repo
.expect_partial_update()
.returning(|_, update| {
let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
updated_tx.status = update.status.unwrap_or(updated_tx.status);
Ok(updated_tx)
});
let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
let result = evm_transaction.handle_status_impl(tx).await.unwrap();
assert_eq!(result.status, TransactionStatus::Confirmed);
}
#[tokio::test]
async fn test_impl_final_failed_branch() {
let mut mocks = default_test_mocks();
let relayer = create_test_relayer();
let tx = make_test_transaction(TransactionStatus::Failed);
mocks
.tx_repo
.expect_partial_update()
.returning(|_, update| {
let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
updated_tx.status = update.status.unwrap_or(updated_tx.status);
Ok(updated_tx)
});
let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
let result = evm_transaction.handle_status_impl(tx).await.unwrap();
assert_eq!(result.status, TransactionStatus::Failed);
}
#[tokio::test]
async fn test_impl_final_expired_branch() {
let mut mocks = default_test_mocks();
let relayer = create_test_relayer();
let tx = make_test_transaction(TransactionStatus::Expired);
mocks
.tx_repo
.expect_partial_update()
.returning(|_, update| {
let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
updated_tx.status = update.status.unwrap_or(updated_tx.status);
Ok(updated_tx)
});
let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
let result = evm_transaction.handle_status_impl(tx).await.unwrap();
assert_eq!(result.status, TransactionStatus::Expired);
}
}
}