openzeppelin_relayer/jobs/handlers/
transaction_submission_handler.rs1use 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(
56 result,
57 attempt,
58 "Transaction Submission",
59 get_max_retries(&command),
60 )
61}
62
63fn 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 let submit_job = TransactionSend::submit("tx123", "relayer-1");
122 let job = Job::new(crate::jobs::JobType::TransactionSend, submit_job);
123
124 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 let cancel_job = TransactionSend::cancel("tx123", "relayer-1", "user requested");
135 let job = Job::new(crate::jobs::JobType::TransactionSend, cancel_job);
136
137 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 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 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}