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