Skip to content

Commit b4c94f2

Browse files
dancoombsclaude
andauthored
feat(rpc): refactor event provider to use structured error types (#1266)
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent b7acec5 commit b4c94f2

File tree

9 files changed

+327
-145
lines changed

9 files changed

+327
-145
lines changed

.claude/hooks/pre-commit-checks.sh

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
#!/bin/bash
2+
# Pre-commit checks hook for rundler
3+
# Runs cargo fmt and clippy before allowing git commit
4+
5+
set -e
6+
7+
INPUT=$(cat)
8+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
9+
10+
# Only run checks if this is a git commit command
11+
if ! echo "$COMMAND" | grep -q "git commit"; then
12+
exit 0
13+
fi
14+
15+
PROJECT_DIR=$(echo "$INPUT" | jq -r '.cwd // empty')
16+
cd "$PROJECT_DIR"
17+
18+
echo "Running cargo +nightly fmt --all..." >&2
19+
if ! cargo +nightly fmt --all; then
20+
echo "BLOCKED: formatting failed" >&2
21+
exit 2
22+
fi
23+
24+
echo "Running cargo clippy..." >&2
25+
if ! cargo clippy --all --all-features --tests -- -D warnings; then
26+
echo "BLOCKED: clippy check failed" >&2
27+
exit 2
28+
fi
29+
30+
exit 0

.claude/settings.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"hooks": {
3+
"PreToolUse": [
4+
{
5+
"matcher": "Bash",
6+
"hooks": [
7+
{
8+
"type": "command",
9+
"command": ".claude/hooks/pre-commit-checks.sh"
10+
}
11+
]
12+
}
13+
]
14+
}
15+
}

AGENTS.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ Rundler is a Rust workspace.
2323
- Lint must be clean: `cargo clippy --all --all-features --tests -- -D warnings`.
2424
- Follow Rust naming defaults: modules/files `snake_case`, types/traits `UpperCamelCase`, constants `SCREAMING_SNAKE_CASE`.
2525
- Keep crate names in the established `rundler-*` pattern.
26+
- Use the `{variable}` shorthand syntax in format strings, logs, and error messages (e.g. `format!("transaction {tx_hash} missing")` instead of `format!("transaction {} missing", tx_hash)`).
27+
- Always import types rather than using inline paths (e.g. `use crate::eth::events::EventProviderError;` then `EventProviderError`, not `crate::eth::events::EventProviderError` inline). Use `as` renames to resolve conflicts.
28+
- Always qualify function calls with their module or type (e.g. `EthRpcError::from(...)`, `Vec::new()`), but do not qualify types/structs/enums unless needed to resolve ambiguity.
2629

2730
## Testing Guidelines
2831
- Add or update tests for every behavior change (`#[test]` / `#[tokio::test]` near the changed module is common here).

crates/rpc/src/eth/api.rs

Lines changed: 49 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,10 @@ use super::{
2727
error::{EthResult, EthRpcError},
2828
router::EntryPointRouter,
2929
};
30-
use crate::types::{RpcGasEstimate, RpcUserOperationByHash, RpcUserOperationReceipt};
30+
use crate::{
31+
eth::events::EventProviderError,
32+
types::{RpcGasEstimate, RpcUserOperationByHash, RpcUserOperationReceipt},
33+
};
3134

3235
pub(crate) struct EthApi<P> {
3336
pub(crate) chain_spec: ChainSpec,
@@ -143,7 +146,12 @@ where
143146
> = vec![];
144147

145148
for ep in self.router.entry_points() {
146-
futs.push(Box::pin(self.router.get_mined_by_hash(ep, hash)));
149+
futs.push(Box::pin(async move {
150+
self.router
151+
.get_mined_by_hash(ep, hash)
152+
.await
153+
.map_err(EthRpcError::from)
154+
}));
147155
}
148156
futs.push(Box::pin(self.get_pending_user_operation_by_hash(hash)));
149157

@@ -164,30 +172,52 @@ where
164172
}
165173
let tag = tag.unwrap_or(BlockTag::Latest);
166174

167-
let bundle_transaction: Option<B256> = match tag {
168-
BlockTag::Pending => {
169-
if !self.chain_spec.flashblocks_enabled {
170-
return Err(EthRpcError::InvalidParams(
171-
"Unsupported feature: preconfirmation".to_string(),
172-
));
173-
}
174-
let op_status = self
175-
.pool
176-
.get_op_status(hash)
177-
.await
178-
.map_err(EthRpcError::from)?;
175+
if tag == BlockTag::Pending {
176+
if !self.chain_spec.flashblocks_enabled {
177+
return Err(EthRpcError::InvalidParams(
178+
"Pending block tag is not supported on this network".to_string(),
179+
));
180+
}
179181

180-
op_status
181-
.and_then(|s| s.preconf_info)
182-
.map(|info| info.tx_hash)
182+
// Preconfirmed check. If not found fallthrough to pending check.
183+
let op_status = self
184+
.pool
185+
.get_op_status(hash)
186+
.await
187+
.map_err(EthRpcError::from)?;
188+
189+
if let Some(op_status) = op_status
190+
&& let Some(preconf_info) = op_status.preconf_info
191+
{
192+
let ret = self
193+
.router
194+
.get_receipt(&op_status.entry_point, hash, Some(preconf_info.tx_hash))
195+
.await;
196+
match ret {
197+
Ok(Some(receipt)) => return Ok(Some(receipt)),
198+
Ok(None) => {
199+
// fallthrough to pending if not found
200+
tracing::warn!(
201+
"no receipt found for preconfirmed user operation {hash}. Treating as pending."
202+
);
203+
}
204+
Err(EventProviderError::Provider(e)) => {
205+
return Err(EthRpcError::from(EventProviderError::Provider(e)));
206+
}
207+
_ => {
208+
// fallthrough to pending on unexpected errors related to consistency issues or invalid receipts
209+
tracing::warn!(
210+
"unexpected error fetching receipt for preconfirmed user operation {hash}. Treating as pending: {ret:?}"
211+
);
212+
}
213+
};
183214
}
184-
BlockTag::Latest => None,
185215
};
186216

187217
let futs = self
188218
.router
189219
.entry_points()
190-
.map(|ep| self.router.get_receipt(ep, hash, bundle_transaction));
220+
.map(|ep| self.router.get_receipt(ep, hash, None));
191221
let results = future::try_join_all(futs).await?;
192222
Ok(results.into_iter().find_map(|x| x))
193223
}

crates/rpc/src/eth/error.rs

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,10 @@ use rundler_types::{
2727
};
2828
use serde::Serialize;
2929

30-
use crate::error::{rpc_err, rpc_err_with_data};
30+
use crate::{
31+
error::{rpc_err, rpc_err_with_data},
32+
eth::events::EventProviderError,
33+
};
3134

3235
// Error codes borrowed from jsonrpsee
3336
// INVALID_REQUEST_CODE = -32600
@@ -472,6 +475,7 @@ struct ProviderErrorWithContext {
472475

473476
impl From<ProviderErrorWithContext> for EthRpcError {
474477
fn from(e: ProviderErrorWithContext) -> Self {
478+
// Log the full error details at ERROR level for debugging
475479
let inner_msg = match &e.error {
476480
ProviderError::RPC(rpc_error) => match rpc_error {
477481
RpcError::ErrorResp(error_payload) => {
@@ -498,15 +502,16 @@ impl From<ProviderErrorWithContext> for EthRpcError {
498502
format!("other error: {}", error)
499503
}
500504
};
501-
if let Some(context_msg) = e.context {
502-
Self::Internal(anyhow::anyhow!(
503-
"{}: provider error: {}",
504-
context_msg,
505-
inner_msg
506-
))
505+
506+
// Log full error details for debugging
507+
if let Some(context_msg) = &e.context {
508+
tracing::error!("{}: provider error: {}", context_msg, inner_msg);
507509
} else {
508-
Self::Internal(anyhow::anyhow!("provider error: {}", inner_msg))
510+
tracing::error!("provider error: {}", inner_msg);
509511
}
512+
513+
// Return generic error message to user
514+
Self::Internal(anyhow::anyhow!("internal error: rpc provider error"))
510515
}
511516
}
512517

@@ -518,6 +523,24 @@ impl From<ProviderError> for ProviderErrorWithContext {
518523
}
519524
}
520525
}
526+
527+
impl From<EventProviderError> for EthRpcError {
528+
fn from(e: EventProviderError) -> Self {
529+
match e {
530+
// Reuse existing ProviderError conversion for all provider errors
531+
EventProviderError::Provider(provider_err) => Self::from(ProviderErrorWithContext {
532+
error: provider_err,
533+
context: None,
534+
}),
535+
// Log full details server-side, return generic message to user
536+
other => {
537+
tracing::error!("event provider error: {}", other);
538+
Self::Internal(anyhow::anyhow!("internal error: event provider error"))
539+
}
540+
}
541+
}
542+
}
543+
521544
impl From<GasEstimationError> for EthRpcError {
522545
fn from(e: GasEstimationError) -> Self {
523546
match e {

0 commit comments

Comments
 (0)