@@ -17,6 +17,7 @@ use engine_core::{
17
17
userop:: UserOpSigner ,
18
18
} ;
19
19
use serde:: { Deserialize , Serialize } ;
20
+ use serde_json;
20
21
use std:: { sync:: Arc , time:: Duration } ;
21
22
use twmq:: {
22
23
FailHookData , NackHookData , Queue , SuccessHookData , UserCancellable ,
@@ -74,6 +75,14 @@ pub struct ExternalBundlerSendResult {
74
75
pub deployment_lock_acquired : bool ,
75
76
}
76
77
78
+ // --- Policy Error Structure ---
79
+ #[ derive( Serialize , Deserialize , Debug , Clone ) ]
80
+ #[ serde( rename_all = "camelCase" ) ]
81
+ pub struct PaymasterPolicyResponse {
82
+ pub policy_id : String ,
83
+ pub reason : String ,
84
+ }
85
+
77
86
// --- Error Types ---
78
87
#[ derive( Serialize , Deserialize , Debug , Clone , thiserror:: Error ) ]
79
88
#[ serde( rename_all = "SCREAMING_SNAKE_CASE" , tag = "errorCode" ) ]
@@ -119,6 +128,12 @@ pub enum ExternalBundlerSendError {
119
128
inner_error : Option < EngineError > ,
120
129
} ,
121
130
131
+ #[ error( "Policy restriction error: {reason} (Policy ID: {policy_id})" ) ]
132
+ PolicyRestriction {
133
+ policy_id : String ,
134
+ reason : String ,
135
+ } ,
136
+
122
137
#[ error( "Invalid RPC Credentials: {message}" ) ]
123
138
InvalidRpcCredentials { message : String } ,
124
139
@@ -403,7 +418,7 @@ where
403
418
. map_err ( |e| {
404
419
let mapped_error =
405
420
map_build_error ( & e, smart_account. address , nonce, needs_init_code) ;
406
- if is_build_error_retryable ( & e ) {
421
+ if is_external_bundler_error_retryable ( & mapped_error ) {
407
422
mapped_error. nack ( Some ( Duration :: from_secs ( 10 ) ) , RequeuePosition :: Last )
408
423
} else {
409
424
mapped_error. fail ( )
@@ -561,12 +576,42 @@ where
561
576
}
562
577
563
578
// --- Error Mapping Helpers ---
579
+
580
+ /// Attempts to parse a policy error from an error message/body
581
+ fn try_parse_policy_error ( error_body : & str ) -> Option < PaymasterPolicyResponse > {
582
+ // Try to parse the error body as JSON containing policy error response
583
+ serde_json:: from_str :: < PaymasterPolicyResponse > ( error_body) . ok ( )
584
+ }
585
+
564
586
fn map_build_error (
565
587
engine_error : & EngineError ,
566
588
account_address : Address ,
567
589
nonce : U256 ,
568
590
had_lock : bool ,
569
591
) -> ExternalBundlerSendError {
592
+ // First check if this is a paymaster policy error
593
+ if let EngineError :: PaymasterError { kind, .. } = engine_error {
594
+ match kind {
595
+ RpcErrorKind :: TransportHttpError { body, .. } => {
596
+ if let Some ( policy_response) = try_parse_policy_error ( body) {
597
+ return ExternalBundlerSendError :: PolicyRestriction {
598
+ policy_id : policy_response. policy_id ,
599
+ reason : policy_response. reason ,
600
+ } ;
601
+ }
602
+ }
603
+ RpcErrorKind :: DeserError { text, .. } => {
604
+ if let Some ( policy_error) = try_parse_policy_error ( text) {
605
+ return ExternalBundlerSendError :: PolicyRestriction {
606
+ policy_id : policy_error. policy_id ,
607
+ reason : policy_error. reason ,
608
+ } ;
609
+ }
610
+ }
611
+ _ => { }
612
+ }
613
+ }
614
+
570
615
let stage = match engine_error {
571
616
EngineError :: RpcError { .. } | EngineError :: PaymasterError { .. } => "BUILDING" . to_string ( ) ,
572
617
EngineError :: BundlerError { .. } => "BUNDLING" . to_string ( ) ,
@@ -728,3 +773,41 @@ fn is_bundler_error_retryable(error_msg: &str) -> bool {
728
773
// Retry everything else (network issues, 5xx errors, timeouts, etc.)
729
774
true
730
775
}
776
+
777
+ /// Determines if an ExternalBundlerSendError should be retried
778
+ fn is_external_bundler_error_retryable ( e : & ExternalBundlerSendError ) -> bool {
779
+ match e {
780
+ // Policy restrictions are never retryable
781
+ ExternalBundlerSendError :: PolicyRestriction { .. } => false ,
782
+
783
+ // For other errors, check their inner EngineError if present
784
+ ExternalBundlerSendError :: UserOpBuildFailed { inner_error : Some ( inner) , .. } => {
785
+ is_build_error_retryable ( inner)
786
+ }
787
+ ExternalBundlerSendError :: BundlerSendFailed { inner_error : Some ( inner) , .. } => {
788
+ is_build_error_retryable ( inner)
789
+ }
790
+
791
+ // User cancellations are not retryable
792
+ ExternalBundlerSendError :: UserCancelled => false ,
793
+
794
+ // Account determination failures are generally not retryable (validation errors)
795
+ ExternalBundlerSendError :: AccountDeterminationFailed { .. } => false ,
796
+
797
+ // Invalid account salt is not retryable (validation error)
798
+ ExternalBundlerSendError :: InvalidAccountSalt { .. } => false ,
799
+
800
+ // Invalid RPC credentials are not retryable (auth error)
801
+ ExternalBundlerSendError :: InvalidRpcCredentials { .. } => false ,
802
+
803
+ // Deployment locked and chain service errors can be retried
804
+ ExternalBundlerSendError :: DeploymentLocked { .. } => true ,
805
+ ExternalBundlerSendError :: ChainServiceError { .. } => true ,
806
+
807
+ // Internal errors can be retried
808
+ ExternalBundlerSendError :: InternalError { .. } => true ,
809
+
810
+ // Default to not retryable for safety
811
+ _ => false ,
812
+ }
813
+ }
0 commit comments