Skip to content

Commit 2e54c2c

Browse files
handle policy restrictions from bundler service
1 parent eb6877f commit 2e54c2c

File tree

1 file changed

+95
-1
lines changed
  • executors/src/external_bundler

1 file changed

+95
-1
lines changed

executors/src/external_bundler/send.rs

Lines changed: 95 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ use engine_core::{
1717
userop::UserOpSigner,
1818
};
1919
use serde::{Deserialize, Serialize};
20+
use serde_json;
2021
use std::{sync::Arc, time::Duration};
2122
use 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 PaymasterPolicyError {
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,53 @@ 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<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+
564597
fn map_build_error(
565598
engine_error: &EngineError,
566599
account_address: Address,
567600
nonce: U256,
568601
had_lock: bool,
569602
) -> 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+
570626
let stage = match engine_error {
571627
EngineError::RpcError { .. } | EngineError::PaymasterError { .. } => "BUILDING".to_string(),
572628
EngineError::BundlerError { .. } => "BUNDLING".to_string(),
@@ -728,3 +784,41 @@ fn is_bundler_error_retryable(error_msg: &str) -> bool {
728784
// Retry everything else (network issues, 5xx errors, timeouts, etc.)
729785
true
730786
}
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

Comments
 (0)