@@ -8,15 +8,18 @@ use engine_core::{
8
8
use serde:: { Deserialize , Serialize } ;
9
9
use std:: { sync:: Arc , time:: Duration } ;
10
10
use twmq:: {
11
- DurableExecution , FailHookData , NackHookData , Queue , SuccessHookData ,
11
+ DurableExecution , FailHookData , NackHookData , Queue , SuccessHookData , UserCancellable ,
12
12
error:: TwmqError ,
13
13
hooks:: TransactionContext ,
14
- job:: { Job , JobResult , RequeuePosition , ToJobResult } ,
14
+ job:: { BorrowedJob , JobResult , RequeuePosition , ToJobResult } ,
15
15
} ;
16
16
17
- use crate :: webhook:: {
18
- WebhookJobHandler ,
19
- envelope:: { ExecutorStage , HasWebhookOptions , WebhookCapable } ,
17
+ use crate :: {
18
+ webhook:: {
19
+ WebhookJobHandler ,
20
+ envelope:: { ExecutorStage , HasWebhookOptions , WebhookCapable } ,
21
+ } ,
22
+ transaction_registry:: TransactionRegistry ,
20
23
} ;
21
24
22
25
use super :: deployment:: RedisDeploymentLock ;
@@ -66,6 +69,9 @@ pub enum UserOpConfirmationError {
66
69
67
70
#[ error( "Internal error: {message}" ) ]
68
71
InternalError { message : String } ,
72
+
73
+ #[ error( "Transaction cancelled by user" ) ]
74
+ UserCancelled ,
69
75
}
70
76
71
77
impl From < TwmqError > for UserOpConfirmationError {
@@ -76,6 +82,12 @@ impl From<TwmqError> for UserOpConfirmationError {
76
82
}
77
83
}
78
84
85
+ impl UserCancellable for UserOpConfirmationError {
86
+ fn user_cancelled ( ) -> Self {
87
+ UserOpConfirmationError :: UserCancelled
88
+ }
89
+ }
90
+
79
91
// --- Handler ---
80
92
pub struct UserOpConfirmationHandler < CS >
81
93
where
84
96
pub chain_service : Arc < CS > ,
85
97
pub deployment_lock : RedisDeploymentLock ,
86
98
pub webhook_queue : Arc < Queue < WebhookJobHandler > > ,
99
+ pub transaction_registry : Arc < TransactionRegistry > ,
87
100
pub max_confirmation_attempts : u32 ,
88
101
pub confirmation_retry_delay : Duration ,
89
102
}
@@ -96,11 +109,13 @@ where
96
109
chain_service : Arc < CS > ,
97
110
deployment_lock : RedisDeploymentLock ,
98
111
webhook_queue : Arc < Queue < WebhookJobHandler > > ,
112
+ transaction_registry : Arc < TransactionRegistry > ,
99
113
) -> Self {
100
114
Self {
101
115
chain_service,
102
116
deployment_lock,
103
117
webhook_queue,
118
+ transaction_registry,
104
119
max_confirmation_attempts : 20 , // ~5 minutes with 15 second delays
105
120
confirmation_retry_delay : Duration :: from_secs ( 15 ) ,
106
121
}
@@ -121,9 +136,9 @@ where
121
136
type ErrorData = UserOpConfirmationError ;
122
137
type JobData = UserOpConfirmationJobData ;
123
138
124
- #[ tracing:: instrument( skip( self , job) , fields( transaction_id = job. id, stage = Self :: stage_name( ) , executor = Self :: executor_name( ) ) ) ]
125
- async fn process ( & self , job : & Job < Self :: JobData > ) -> JobResult < Self :: Output , Self :: ErrorData > {
126
- let job_data = & job. data ;
139
+ #[ tracing:: instrument( skip( self , job) , fields( transaction_id = job. job . id, stage = Self :: stage_name( ) , executor = Self :: executor_name( ) ) ) ]
140
+ async fn process ( & self , job : & BorrowedJob < Self :: JobData > ) -> JobResult < Self :: Output , Self :: ErrorData > {
141
+ let job_data = & job. job . data ;
127
142
128
143
// 1. Get Chain
129
144
let chain = self
@@ -136,7 +151,7 @@ where
136
151
. map_err_fail ( ) ?;
137
152
138
153
let chain = chain. with_new_default_headers (
139
- job. data
154
+ job. job . data
140
155
. rpc_credentials
141
156
. to_header_map ( )
142
157
. map_err ( |e| UserOpConfirmationError :: InternalError {
@@ -161,17 +176,17 @@ where
161
176
Some ( receipt) => receipt,
162
177
None => {
163
178
// Receipt not available and max attempts reached - permanent failure
164
- if job. attempts >= self . max_confirmation_attempts {
179
+ if job. job . attempts >= self . max_confirmation_attempts {
165
180
return Err ( UserOpConfirmationError :: ReceiptNotAvailable {
166
181
user_op_hash : job_data. user_op_hash . clone ( ) ,
167
- attempt_number : job. attempts ,
182
+ attempt_number : job. job . attempts ,
168
183
} )
169
184
. map_err_fail ( ) ; // FAIL - triggers on_fail hook which will release lock
170
185
}
171
186
172
187
return Err ( UserOpConfirmationError :: ReceiptNotAvailable {
173
188
user_op_hash : job_data. user_op_hash . clone ( ) ,
174
- attempt_number : job. attempts ,
189
+ attempt_number : job. job . attempts ,
175
190
} )
176
191
. map_err_nack ( Some ( self . confirmation_retry_delay ) , RequeuePosition :: Last ) ;
177
192
// NACK - triggers on_nack hook which keeps lock for retry
@@ -197,31 +212,37 @@ where
197
212
198
213
async fn on_success (
199
214
& self ,
200
- job : & Job < Self :: JobData > ,
215
+ job : & BorrowedJob < Self :: JobData > ,
201
216
success_data : SuccessHookData < ' _ , Self :: Output > ,
202
217
tx : & mut TransactionContext < ' _ > ,
203
218
) {
219
+ // Remove transaction from registry since confirmation is complete
220
+ self . transaction_registry . add_remove_command (
221
+ tx. pipeline ( ) ,
222
+ & job. job . data . transaction_id ,
223
+ ) ;
224
+
204
225
// Atomic cleanup: release lock + update cache if lock was acquired
205
- if job. data . deployment_lock_acquired {
226
+ if job. job . data . deployment_lock_acquired {
206
227
self . deployment_lock
207
228
. release_lock_and_update_cache_with_pipeline (
208
229
tx. pipeline ( ) ,
209
- job. data . chain_id ,
210
- & job. data . account_address ,
230
+ job. job . data . chain_id ,
231
+ & job. job . data . account_address ,
211
232
true , // is_deployed = true
212
233
) ;
213
234
214
235
tracing:: info!(
215
- transaction_id = %job. data. transaction_id,
216
- account_address = ?job. data. account_address,
236
+ transaction_id = %job. job . data. transaction_id,
237
+ account_address = ?job. job . data. account_address,
217
238
"Added atomic lock release and cache update to transaction pipeline"
218
239
) ;
219
240
}
220
241
221
242
// Queue success webhook
222
243
if let Err ( e) = self . queue_success_webhook ( job, success_data, tx) {
223
244
tracing:: error!(
224
- transaction_id = %job. data. transaction_id,
245
+ transaction_id = %job. job . data. transaction_id,
225
246
error = %e,
226
247
"Failed to queue success webhook"
227
248
) ;
@@ -230,39 +251,45 @@ where
230
251
231
252
async fn on_nack (
232
253
& self ,
233
- job : & Job < Self :: JobData > ,
254
+ job : & BorrowedJob < Self :: JobData > ,
234
255
nack_data : NackHookData < ' _ , Self :: ErrorData > ,
235
256
tx : & mut TransactionContext < ' _ > ,
236
257
) {
237
258
// NEVER release lock on NACK - job will be retried with the same lock
238
259
// Just queue webhook with current status
239
260
if let Err ( e) = self . queue_nack_webhook ( job, nack_data, tx) {
240
261
tracing:: error!(
241
- transaction_id = %job. data. transaction_id,
262
+ transaction_id = %job. job . data. transaction_id,
242
263
error = %e,
243
264
"Failed to queue nack webhook"
244
265
) ;
245
266
}
246
267
247
268
tracing:: debug!(
248
- transaction_id = %job. data. transaction_id,
249
- attempt = %job. attempts,
269
+ transaction_id = %job. job . data. transaction_id,
270
+ attempt = %job. job . attempts,
250
271
"Confirmation job NACKed, retaining lock for retry"
251
272
) ;
252
273
}
253
274
254
275
async fn on_fail (
255
276
& self ,
256
- job : & Job < Self :: JobData > ,
277
+ job : & BorrowedJob < Self :: JobData > ,
257
278
fail_data : FailHookData < ' _ , Self :: ErrorData > ,
258
279
tx : & mut TransactionContext < ' _ > ,
259
280
) {
281
+ // Remove transaction from registry since it failed permanently
282
+ self . transaction_registry . add_remove_command (
283
+ tx. pipeline ( ) ,
284
+ & job. job . data . transaction_id ,
285
+ ) ;
286
+
260
287
// Always release lock on permanent failure
261
- if job. data . deployment_lock_acquired {
288
+ if job. job . data . deployment_lock_acquired {
262
289
self . deployment_lock . release_lock_with_pipeline (
263
290
tx. pipeline ( ) ,
264
- job. data . chain_id ,
265
- & job. data . account_address ,
291
+ job. job . data . chain_id ,
292
+ & job. job . data . account_address ,
266
293
) ;
267
294
268
295
let failure_reason = match fail_data. error {
@@ -273,8 +300,8 @@ where
273
300
} ;
274
301
275
302
tracing:: error!(
276
- transaction_id = %job. data. transaction_id,
277
- account_address = ?job. data. account_address,
303
+ transaction_id = %job. job . data. transaction_id,
304
+ account_address = ?job. job . data. account_address,
278
305
reason = %failure_reason,
279
306
"Added lock release to transaction pipeline due to permanent failure"
280
307
) ;
@@ -283,7 +310,7 @@ where
283
310
// Queue failure webhook
284
311
if let Err ( e) = self . queue_fail_webhook ( job, fail_data, tx) {
285
312
tracing:: error!(
286
- transaction_id = %job. data. transaction_id,
313
+ transaction_id = %job. job . data. transaction_id,
287
314
error = %e,
288
315
"Failed to queue fail webhook"
289
316
) ;
0 commit comments