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