@@ -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 PaymasterPolicyError {
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,53 @@ 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 < PaymasterPolicyError > {
582
+ // Try to parse the error body as JSON containing policy error
583
+ if let Ok ( policy_error) = serde_json:: from_str :: < PaymasterPolicyError > ( error_body) {
584
+ return Some ( policy_error) ;
585
+ }
586
+
587
+ // Also check if the error message contains policy error information
588
+ if error_body. contains ( "policyId" ) && error_body. contains ( "reason" ) {
589
+ if let Ok ( policy_error) = serde_json:: from_str :: < PaymasterPolicyError > ( error_body) {
590
+ return Some ( policy_error) ;
591
+ }
592
+ }
593
+
594
+ None
595
+ }
596
+
564
597
fn map_build_error (
565
598
engine_error : & EngineError ,
566
599
account_address : Address ,
567
600
nonce : U256 ,
568
601
had_lock : bool ,
569
602
) -> ExternalBundlerSendError {
603
+ // First check if this is a paymaster policy error
604
+ if let EngineError :: PaymasterError { kind, .. } = engine_error {
605
+ match kind {
606
+ RpcErrorKind :: TransportHttpError { body, .. } => {
607
+ if let Some ( policy_error) = try_parse_policy_error ( body) {
608
+ return ExternalBundlerSendError :: PolicyRestriction {
609
+ policy_id : policy_error. policy_id ,
610
+ reason : policy_error. reason ,
611
+ } ;
612
+ }
613
+ }
614
+ RpcErrorKind :: DeserError { text, .. } => {
615
+ if let Some ( policy_error) = try_parse_policy_error ( text) {
616
+ return ExternalBundlerSendError :: PolicyRestriction {
617
+ policy_id : policy_error. policy_id ,
618
+ reason : policy_error. reason ,
619
+ } ;
620
+ }
621
+ }
622
+ _ => { }
623
+ }
624
+ }
625
+
570
626
let stage = match engine_error {
571
627
EngineError :: RpcError { .. } | EngineError :: PaymasterError { .. } => "BUILDING" . to_string ( ) ,
572
628
EngineError :: BundlerError { .. } => "BUNDLING" . to_string ( ) ,
@@ -728,3 +784,41 @@ fn is_bundler_error_retryable(error_msg: &str) -> bool {
728
784
// Retry everything else (network issues, 5xx errors, timeouts, etc.)
729
785
true
730
786
}
787
+
788
+ /// Determines if an ExternalBundlerSendError should be retried
789
+ fn is_external_bundler_error_retryable ( e : & ExternalBundlerSendError ) -> bool {
790
+ match e {
791
+ // Policy restrictions are never retryable
792
+ ExternalBundlerSendError :: PolicyRestriction { .. } => false ,
793
+
794
+ // For other errors, check their inner EngineError if present
795
+ ExternalBundlerSendError :: UserOpBuildFailed { inner_error : Some ( inner) , .. } => {
796
+ is_build_error_retryable ( inner)
797
+ }
798
+ ExternalBundlerSendError :: BundlerSendFailed { inner_error : Some ( inner) , .. } => {
799
+ is_build_error_retryable ( inner)
800
+ }
801
+
802
+ // User cancellations are not retryable
803
+ ExternalBundlerSendError :: UserCancelled => false ,
804
+
805
+ // Account determination failures are generally not retryable (validation errors)
806
+ ExternalBundlerSendError :: AccountDeterminationFailed { .. } => false ,
807
+
808
+ // Invalid account salt is not retryable (validation error)
809
+ ExternalBundlerSendError :: InvalidAccountSalt { .. } => false ,
810
+
811
+ // Invalid RPC credentials are not retryable (auth error)
812
+ ExternalBundlerSendError :: InvalidRpcCredentials { .. } => false ,
813
+
814
+ // Deployment locked and chain service errors can be retried
815
+ ExternalBundlerSendError :: DeploymentLocked { .. } => true ,
816
+ ExternalBundlerSendError :: ChainServiceError { .. } => true ,
817
+
818
+ // Internal errors can be retried
819
+ ExternalBundlerSendError :: InternalError { .. } => true ,
820
+
821
+ // Default to not retryable for safety
822
+ _ => false ,
823
+ }
824
+ }
0 commit comments