55//! POST /rpc/{chain} JSON-RPC passthrough (alias -> dRPC slug)
66//! GET /v1/{chain}/balances/{address} Wallet-API balances for one chain
77//! GET /v1/{chain}/history/{address} Wallet-API tx history for one chain
8+ //! GET /beacon/{*path} Beacon light-client proxy for Helios
89//!
910//! The wallet-API routes are a thin per-chain mirror of dRPC's Wallet API: the
1011//! proxy resolves the chain alias to a dRPC slug, injects the shared key,
1516//! source.
1617
1718use crate :: config:: ChainSpec ;
18- use crate :: upstream:: { Drpc , UpstreamError } ;
19+ use crate :: upstream:: { Beacon , Drpc , UpstreamError } ;
1920use axum:: Json ;
2021use axum:: body:: Bytes ;
2122use axum:: extract:: { Path , RawQuery , State } ;
@@ -26,6 +27,7 @@ use std::sync::Arc;
2627
2728pub struct AppState {
2829 pub drpc : Drpc ,
30+ pub beacon : Beacon ,
2931 pub chains : Vec < ChainSpec > ,
3032 pub method_denylist : bool ,
3133}
@@ -50,6 +52,7 @@ pub async fn rpc_handler(
5052 Path ( chain) : Path < String > ,
5153 body : Bytes ,
5254) -> Result < Response , AppError > {
55+ tracing:: info!( chain, body_len = body. len( ) , "TEMP: rpc request received" ) ;
5356 let slug = state. resolve_chain ( & chain) . ok_or ( AppError :: UnknownChain ) ?;
5457 if state. method_denylist {
5558 reject_denied_methods ( & body) ?;
@@ -63,6 +66,7 @@ pub async fn balances_handler(
6366 Path ( ( chain, address) ) : Path < ( String , String ) > ,
6467 RawQuery ( query) : RawQuery ,
6568) -> Result < Response , AppError > {
69+ tracing:: info!( chain, "TEMP: balances request received" ) ;
6670 let slug = state. resolve_chain ( & chain) . ok_or ( AppError :: UnknownChain ) ?;
6771 if !is_valid_eth_address ( & address) {
6872 return Err ( AppError :: BadAddress ) ;
@@ -79,6 +83,7 @@ pub async fn history_handler(
7983 Path ( ( chain, address) ) : Path < ( String , String ) > ,
8084 RawQuery ( query) : RawQuery ,
8185) -> Result < Response , AppError > {
86+ tracing:: info!( chain, "TEMP: history request received" ) ;
8287 let slug = state. resolve_chain ( & chain) . ok_or ( AppError :: UnknownChain ) ?;
8388 if !is_valid_eth_address ( & address) {
8489 return Err ( AppError :: BadAddress ) ;
@@ -90,10 +95,43 @@ pub async fn history_handler(
9095 Ok ( json_passthrough ( code, body) )
9196}
9297
98+ /// Beacon light-client proxy. The wallet's Helios points its consensus RPC at
99+ /// `{KAO}/beacon` and makes plain HTTP GET / JSON requests. We strip the
100+ /// `/beacon` prefix, forward the path + query verbatim to the routed upstream,
101+ /// and return its status, `Content-Type`, and body unchanged.
102+ ///
103+ /// On a pre-response upstream failure we return a clean 5xx (via `AppError`),
104+ /// which Helios retries — never a malformed 200 body, which it treats as fatal.
105+ pub async fn beacon_handler (
106+ State ( state) : State < Arc < AppState > > ,
107+ Path ( path) : Path < String > ,
108+ RawQuery ( query) : RawQuery ,
109+ ) -> Result < Response , AppError > {
110+ // Axum's `{*path}` capture drops the leading slash; the upstream wants an
111+ // absolute beacon-API path (e.g. `/eth/v2/beacon/blocks/14702657`).
112+ let path = format ! ( "/{path}" ) ;
113+ let ( code, content_type, body) = state. beacon . get ( & path, query. as_deref ( ) ) . await ?;
114+ Ok ( beacon_passthrough ( code, content_type, body) )
115+ }
116+
93117/// Build a passthrough response: upstream status (clamped to a valid code),
94118/// `application/json`, and the upstream body verbatim.
95119fn json_passthrough ( code : u16 , body : Vec < u8 > ) -> Response {
96- let status = StatusCode :: from_u16 ( code) . unwrap_or ( StatusCode :: BAD_GATEWAY ) ;
120+ // TEMP: a clamp here is the only way this app emits a 502 with a non-JSON
121+ // (possibly empty) body — exactly the symptom being chased.
122+ let status = StatusCode :: from_u16 ( code) . unwrap_or_else ( |_| {
123+ tracing:: warn!(
124+ code,
125+ body_len = body. len( ) ,
126+ "TEMP: invalid upstream status, clamping to 502"
127+ ) ;
128+ StatusCode :: BAD_GATEWAY
129+ } ) ;
130+ tracing:: info!(
131+ status = code,
132+ body_len = body. len( ) ,
133+ "TEMP: passthrough response"
134+ ) ;
97135 (
98136 status,
99137 [ (
@@ -105,6 +143,16 @@ fn json_passthrough(code: u16, body: Vec<u8>) -> Response {
105143 . into_response ( )
106144}
107145
146+ /// Beacon passthrough: upstream status (clamped) and body verbatim, preserving
147+ /// the upstream `Content-Type` (Helios parses JSON; we don't rewrite the body).
148+ fn beacon_passthrough ( code : u16 , content_type : Option < String > , body : Vec < u8 > ) -> Response {
149+ let status = StatusCode :: from_u16 ( code) . unwrap_or ( StatusCode :: BAD_GATEWAY ) ;
150+ let ct = content_type
151+ . and_then ( |s| HeaderValue :: from_str ( & s) . ok ( ) )
152+ . unwrap_or_else ( || HeaderValue :: from_static ( "application/json" ) ) ;
153+ ( status, [ ( header:: CONTENT_TYPE , ct) ] , body) . into_response ( )
154+ }
155+
108156// ---- validation & method filtering ------------------------------------------
109157
110158fn is_valid_eth_address ( s : & str ) -> bool {
@@ -169,6 +217,8 @@ impl IntoResponse for AppError {
169217 AppError :: BadRequest ( m) => ( StatusCode :: BAD_REQUEST , m) ,
170218 AppError :: Upstream ( m) => ( StatusCode :: BAD_GATEWAY , m) ,
171219 } ;
220+ // TEMP:
221+ tracing:: warn!( status = %status, msg, "TEMP: app error response" ) ;
172222 (
173223 status,
174224 Json ( serde_json:: json!( { "error" : { "message" : msg } } ) ) ,
0 commit comments