openzeppelin_relayer/jobs/handlers/
transaction_submission_handler.rs

1//! Transaction submission handler for processing submission jobs.
2//!
3//! Handles the submission of prepared transactions to networks:
4//! - Submits transactions to appropriate networks
5//! - Handles different submission commands (Submit, Cancel, Resubmit)
6//! - Updates transaction status after submission
7//! - Enqueues status monitoring jobs
8use actix_web::web::ThinData;
9use apalis::prelude::{Attempt, Data, *};
10use eyre::Result;
11use tracing::{debug, info, instrument};
12
13use crate::{
14    constants::{
15        WORKER_TRANSACTION_CANCEL_RETRIES, WORKER_TRANSACTION_RESEND_RETRIES,
16        WORKER_TRANSACTION_RESUBMIT_RETRIES, WORKER_TRANSACTION_SUBMIT_RETRIES,
17    },
18    domain::{get_relayer_transaction, get_transaction_by_id, Transaction},
19    jobs::{handle_result, Job, TransactionCommand, TransactionSend},
20    models::DefaultAppState,
21    observability::request_id::set_request_id,
22};
23
24#[instrument(
25    level = "info",
26    skip(job, state),
27    fields(
28        request_id = ?job.request_id,
29        job_id = %job.message_id,
30        job_type = %job.job_type.to_string(),
31        attempt = %attempt.current(),
32        tx_id = %job.data.transaction_id,
33        relayer_id = %job.data.relayer_id,
34        command = ?job.data.command,
35    )
36)]
37pub async fn transaction_submission_handler(
38    job: Job<TransactionSend>,
39    state: Data<ThinData<DefaultAppState>>,
40    attempt: Attempt,
41) -> Result<(), Error> {
42    if let Some(request_id) = job.request_id.clone() {
43        set_request_id(request_id);
44    }
45
46    debug!(
47        "handling transaction submission {}",
48        job.data.transaction_id
49    );
50
51    let command = job.data.command.clone();
52    let result = handle_request(job.data, state.clone()).await;
53
54    // Handle result with command-specific retry logic
55    handle_result(
56        result,
57        attempt,
58        "Transaction Submission",
59        get_max_retries(&command),
60    )
61}
62
63/// Get max retry count based on command type
64fn get_max_retries(command: &TransactionCommand) -> usize {
65    match command {
66        TransactionCommand::Submit => WORKER_TRANSACTION_SUBMIT_RETRIES,
67        TransactionCommand::Resubmit => WORKER_TRANSACTION_RESUBMIT_RETRIES,
68        TransactionCommand::Cancel { .. } => WORKER_TRANSACTION_CANCEL_RETRIES,
69        TransactionCommand::Resend => WORKER_TRANSACTION_RESEND_RETRIES,
70    }
71}
72
73async fn handle_request(
74    status_request: TransactionSend,
75    state: Data<ThinData<DefaultAppState>>,
76) -> Result<()> {
77    let relayer_transaction =
78        get_relayer_transaction(status_request.relayer_id.clone(), &state).await?;
79
80    let transaction = get_transaction_by_id(status_request.transaction_id, &state).await?;
81
82    match status_request.command {
83        TransactionCommand::Submit => {
84            relayer_transaction.submit_transaction(transaction).await?;
85        }
86        TransactionCommand::Cancel { reason } => {
87            info!(
88                reason = %reason,
89                "cancelling transaction {}", transaction.id
90            );
91            relayer_transaction.submit_transaction(transaction).await?;
92        }
93        TransactionCommand::Resubmit => {
94            debug!(
95                "resubmitting transaction with updated parameters {}",
96                transaction.id
97            );
98            relayer_transaction
99                .resubmit_transaction(transaction)
100                .await?;
101        }
102        TransactionCommand::Resend => {
103            debug!("resending transaction {}", transaction.id);
104            relayer_transaction.submit_transaction(transaction).await?;
105        }
106    };
107
108    debug!("transaction handled successfully");
109
110    Ok(())
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116    use std::collections::HashMap;
117
118    #[tokio::test]
119    async fn test_submission_handler_job_validation() {
120        // Create a job with Submit command
121        let submit_job = TransactionSend::submit("tx123", "relayer-1");
122        let job = Job::new(crate::jobs::JobType::TransactionSend, submit_job);
123
124        // Validate the job data
125        match job.data.command {
126            TransactionCommand::Submit => {}
127            _ => panic!("Expected Submit command"),
128        }
129        assert_eq!(job.data.transaction_id, "tx123");
130        assert_eq!(job.data.relayer_id, "relayer-1");
131        assert!(job.data.metadata.is_none());
132
133        // Create a job with Cancel command
134        let cancel_job = TransactionSend::cancel("tx123", "relayer-1", "user requested");
135        let job = Job::new(crate::jobs::JobType::TransactionSend, cancel_job);
136
137        // Validate the job data
138        match job.data.command {
139            TransactionCommand::Cancel { reason } => {
140                assert_eq!(reason, "user requested");
141            }
142            _ => panic!("Expected Cancel command"),
143        }
144    }
145
146    #[tokio::test]
147    async fn test_submission_job_with_metadata() {
148        // Create a job with metadata
149        let mut metadata = HashMap::new();
150        metadata.insert("gas_price".to_string(), "20000000000".to_string());
151
152        let submit_job =
153            TransactionSend::submit("tx123", "relayer-1").with_metadata(metadata.clone());
154
155        // Validate the metadata
156        assert!(submit_job.metadata.is_some());
157        let job_metadata = submit_job.metadata.unwrap();
158        assert_eq!(job_metadata.get("gas_price").unwrap(), "20000000000");
159    }
160
161    mod get_max_retries_tests {
162        use super::*;
163
164        #[test]
165        fn test_submit_command_retries() {
166            let command = TransactionCommand::Submit;
167            let retries = get_max_retries(&command);
168
169            assert_eq!(
170                retries, WORKER_TRANSACTION_SUBMIT_RETRIES,
171                "Submit command should use WORKER_TRANSACTION_SUBMIT_RETRIES"
172            );
173        }
174
175        #[test]
176        fn test_resubmit_command_retries() {
177            let command = TransactionCommand::Resubmit;
178            let retries = get_max_retries(&command);
179
180            assert_eq!(
181                retries, WORKER_TRANSACTION_RESUBMIT_RETRIES,
182                "Resubmit command should use WORKER_TRANSACTION_RESUBMIT_RETRIES"
183            );
184        }
185
186        #[test]
187        fn test_cancel_command_retries() {
188            let command = TransactionCommand::Cancel {
189                reason: "test cancel".to_string(),
190            };
191            let retries = get_max_retries(&command);
192
193            assert_eq!(
194                retries, WORKER_TRANSACTION_CANCEL_RETRIES,
195                "Cancel command should use WORKER_TRANSACTION_CANCEL_RETRIES"
196            );
197        }
198
199        #[test]
200        fn test_resend_command_retries() {
201            let command = TransactionCommand::Resend;
202            let retries = get_max_retries(&command);
203
204            assert_eq!(
205                retries, WORKER_TRANSACTION_RESEND_RETRIES,
206                "Resend command should use WORKER_TRANSACTION_RESEND_RETRIES"
207            );
208        }
209    }
210}