@@ -17,6 +17,7 @@ use engine_core::{
1717 userop:: UserOpSigner ,
1818} ;
1919use serde:: { Deserialize , Serialize } ;
20+ use serde_json;
2021use std:: { sync:: Arc , time:: Duration } ;
2122use twmq:: {
2223 FailHookData , NackHookData , Queue , SuccessHookData , UserCancellable ,
@@ -74,6 +75,14 @@ pub struct ExternalBundlerSendResult {
7475 pub deployment_lock_acquired : bool ,
7576}
7677
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+
7786// --- Error Types ---
7887#[ derive( Serialize , Deserialize , Debug , Clone , thiserror:: Error ) ]
7988#[ serde( rename_all = "SCREAMING_SNAKE_CASE" , tag = "errorCode" ) ]
@@ -119,6 +128,12 @@ pub enum ExternalBundlerSendError {
119128 inner_error : Option < EngineError > ,
120129 } ,
121130
131+ #[ error( "Policy restriction error: {reason} (Policy ID: {policy_id})" ) ]
132+ PolicyRestriction {
133+ policy_id : String ,
134+ reason : String ,
135+ } ,
136+
122137 #[ error( "Invalid RPC Credentials: {message}" ) ]
123138 InvalidRpcCredentials { message : String } ,
124139
@@ -403,7 +418,7 @@ where
403418 . map_err ( |e| {
404419 let mapped_error =
405420 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 ) {
407422 mapped_error. nack ( Some ( Duration :: from_secs ( 10 ) ) , RequeuePosition :: Last )
408423 } else {
409424 mapped_error. fail ( )
@@ -561,12 +576,42 @@ where
561576}
562577
563578// --- 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+
564586fn map_build_error (
565587 engine_error : & EngineError ,
566588 account_address : Address ,
567589 nonce : U256 ,
568590 had_lock : bool ,
569591) -> 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+
570615 let stage = match engine_error {
571616 EngineError :: RpcError { .. } | EngineError :: PaymasterError { .. } => "BUILDING" . to_string ( ) ,
572617 EngineError :: BundlerError { .. } => "BUNDLING" . to_string ( ) ,
@@ -728,3 +773,41 @@ fn is_bundler_error_retryable(error_msg: &str) -> bool {
728773 // Retry everything else (network issues, 5xx errors, timeouts, etc.)
729774 true
730775}
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