Skip to content

Commit 6a35f02

Browse files
handle policy restrictions from bundler service (#4)
* handle policy restrictions from bundler service * cleanup
1 parent eb6877f commit 6a35f02

File tree

1 file changed

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

1 file changed

+84
-1
lines changed

executors/src/external_bundler/send.rs

Lines changed: 84 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 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+
564586
fn 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

Comments
 (0)