1use crate::http::fork;
2use crate::http::transactions::{self, TransactionState};
3use crate::{
4 config::{HttpLimitsConfig, RuntimePlan, TransactionConfig},
5 health::HealthMonitor,
6 ProgramRuntimeCatalog, ProgramRuntimeDefinition, SolanaGatewayUsageObservation,
7 SolanaGatewayUsageObserver, SolanaGatewayUsageSurface,
8};
9use anyhow::Result;
10use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
11use base64::Engine as _;
12use dashmap::DashMap;
13use http_body_util::BodyExt;
14use http_body_util::Full;
15use hyper::body::Bytes;
16use hyper::header::{
17 HeaderValue, ACCESS_CONTROL_ALLOW_HEADERS, ACCESS_CONTROL_ALLOW_METHODS,
18 ACCESS_CONTROL_ALLOW_ORIGIN, ACCESS_CONTROL_EXPOSE_HEADERS, ACCESS_CONTROL_MAX_AGE,
19};
20use hyper::server::conn::http1;
21use hyper::service::service_fn;
22use hyper::{Method, Request, Response, StatusCode};
23use hyper_util::rt::{TokioIo, TokioTimer};
24use reqwest::Client;
25use serde::Deserialize;
26use serde_json::{json, Value};
27use std::convert::Infallible;
28use std::env;
29use std::net::SocketAddr;
30use std::sync::Arc;
31use std::time::{Duration, SystemTime, UNIX_EPOCH};
32use tokio::net::TcpListener;
33use tokio::task::JoinSet;
34use tokio_util::sync::CancellationToken;
35use tracing::{error, info, warn};
36
37use crate::websocket::auth::{AuthDecision, AuthDeny, ConnectionAuthRequest, WebSocketAuthPlugin};
38use arete_auth::{SCOPE_FORK_READ, SCOPE_READ};
39
40use crate::token_discovery::{OwnerTokenAccountsProvider, TokenDiscoveryError};
41use arete_solana_contracts::{Contextual, OwnerTokenAccountsRequest, ReadContext, ReadOptions};
42
43const HTTP_CONNECTION_DRAIN_TIMEOUT: Duration = Duration::from_secs(5);
44
45#[derive(Clone, Debug)]
47pub struct HttpHealthConfig {
48 pub bind_address: SocketAddr,
49}
50
51impl Default for HttpHealthConfig {
52 fn default() -> Self {
53 Self {
54 bind_address: "[::]:8081".parse().expect("valid socket address"),
55 }
56 }
57}
58
59impl HttpHealthConfig {
60 pub fn new(bind_address: impl Into<SocketAddr>) -> Self {
61 Self {
62 bind_address: bind_address.into(),
63 }
64 }
65}
66
67#[derive(Clone)]
68struct HttpRequestState {
69 health_monitor: Arc<Option<HealthMonitor>>,
70 snapshot_runtime: Option<crate::snapshot::SnapshotRuntime>,
71 runtime_plan: RuntimePlan,
72 rpc_url: Arc<Option<String>>,
73 rpc_client: ReadRpcClient,
74 body_limits: BodyLimits,
75 program_runtime_catalog: Arc<ProgramRuntimeCatalog>,
76 auth_plugin: Arc<Option<Arc<dyn WebSocketAuthPlugin>>>,
77 limit_state: Arc<HttpLimitState>,
78 transaction_state: Arc<Option<TransactionState>>,
79 token_accounts_provider: Option<Arc<dyn OwnerTokenAccountsProvider>>,
80 solana_gateway_usage_observer: Option<Arc<dyn SolanaGatewayUsageObserver>>,
81 solana_gateway_target_id: Arc<Option<String>>,
82 program_read_binding_target_id: Arc<Option<String>>,
83}
84
85pub struct HttpHealthServer {
87 shutdown: Option<CancellationToken>,
89 bind_addr: SocketAddr,
90 health_monitor: Option<HealthMonitor>,
91 snapshot_runtime: Option<crate::snapshot::SnapshotRuntime>,
92 runtime_plan: RuntimePlan,
93 program_runtime_catalog: ProgramRuntimeCatalog,
94 auth_plugin: Option<Arc<dyn WebSocketAuthPlugin>>,
95 transaction_config: Option<TransactionConfig>,
96 token_accounts_provider: Option<Arc<dyn OwnerTokenAccountsProvider>>,
97 solana_gateway_usage_observer: Option<Arc<dyn SolanaGatewayUsageObserver>>,
98 solana_gateway_target_id: Option<String>,
99 program_read_binding_target_id: Option<String>,
100 read_rpc_url: Option<String>,
102 limits: HttpLimitsConfig,
103 #[cfg(feature = "otel")]
104 metrics: Option<Arc<crate::metrics::Metrics>>,
105}
106
107impl HttpHealthServer {
108 pub fn new(bind_addr: SocketAddr) -> Self {
109 Self {
110 bind_addr,
111 shutdown: None,
112 health_monitor: None,
113 snapshot_runtime: None,
114 runtime_plan: RuntimePlan::http(),
115 program_runtime_catalog: ProgramRuntimeCatalog::default(),
116 auth_plugin: None,
117 transaction_config: None,
118 token_accounts_provider: None,
119 solana_gateway_usage_observer: None,
120 solana_gateway_target_id: None,
121 program_read_binding_target_id: None,
122 read_rpc_url: None,
123 limits: HttpLimitsConfig::default(),
124 #[cfg(feature = "otel")]
125 metrics: None,
126 }
127 }
128
129 #[cfg(test)]
130 pub(crate) fn with_read_rpc_url(mut self, rpc_url: impl Into<String>) -> Self {
131 self.read_rpc_url = Some(rpc_url.into());
132 self
133 }
134
135 pub fn with_limits(mut self, limits: HttpLimitsConfig) -> Self {
137 self.limits = limits;
138 self
139 }
140
141 pub fn with_health_monitor(mut self, monitor: HealthMonitor) -> Self {
142 self.health_monitor = Some(monitor);
143 self
144 }
145
146 pub fn with_snapshot_runtime(
147 mut self,
148 snapshot_runtime: crate::snapshot::SnapshotRuntime,
149 ) -> Self {
150 self.snapshot_runtime = Some(snapshot_runtime);
151 self
152 }
153
154 pub fn with_runtime_plan(mut self, runtime_plan: RuntimePlan) -> Self {
155 self.runtime_plan = runtime_plan;
156 self
157 }
158
159 pub fn with_program_runtime_catalog(mut self, catalog: ProgramRuntimeCatalog) -> Self {
160 self.program_runtime_catalog = catalog;
161 self
162 }
163
164 pub fn with_auth_plugin(mut self, plugin: Arc<dyn WebSocketAuthPlugin>) -> Self {
165 self.auth_plugin = Some(plugin);
166 self
167 }
168
169 pub fn with_transaction_config(mut self, config: TransactionConfig) -> Self {
170 self.runtime_plan.transactions = config.enabled;
171 self.transaction_config = Some(config);
172 self
173 }
174
175 pub fn with_owner_token_accounts_provider(
176 mut self,
177 provider: Arc<dyn OwnerTokenAccountsProvider>,
178 ) -> Self {
179 self.token_accounts_provider = Some(provider);
180 self
181 }
182
183 pub fn with_solana_gateway_usage_observer(
184 mut self,
185 observer: Arc<dyn SolanaGatewayUsageObserver>,
186 ) -> Self {
187 self.solana_gateway_usage_observer = Some(observer);
188 self
189 }
190
191 pub fn with_solana_gateway_target(mut self, target_id: impl Into<String>) -> Self {
192 self.solana_gateway_target_id = Some(target_id.into());
193 self
194 }
195
196 pub fn with_program_read_binding_target(mut self, target_id: impl Into<String>) -> Self {
197 self.program_read_binding_target_id = Some(target_id.into());
198 self
199 }
200
201 #[cfg(feature = "otel")]
202 pub fn with_metrics(mut self, metrics: Option<Arc<crate::metrics::Metrics>>) -> Self {
203 self.metrics = metrics;
204 self
205 }
206
207 pub fn with_shutdown(mut self, token: CancellationToken) -> Self {
210 self.shutdown = Some(token);
211 self
212 }
213
214 pub async fn start(self) -> Result<()> {
215 info!("Starting HTTP health server on {}", self.bind_addr);
216 self.limits.validate()?;
217
218 let listener = TcpListener::bind(&self.bind_addr).await?;
219 info!("HTTP health server listening on {}", self.bind_addr);
220
221 let transaction_state = self
222 .transaction_config
223 .filter(|config| config.enabled)
224 .map(TransactionState::new)
225 .transpose()?;
226 let transaction_state = match (
227 transaction_state,
228 self.solana_gateway_usage_observer.as_ref(),
229 self.solana_gateway_target_id.as_ref(),
230 ) {
231 (Some(state), Some(observer), Some(target_id)) => {
232 Some(state.with_solana_gateway_usage_observer(observer.clone(), target_id.clone()))
233 }
234 (state, _, _) => state,
235 };
236 #[cfg(feature = "otel")]
237 let transaction_state = transaction_state.map(|state| state.with_metrics(self.metrics));
238 let request_state = HttpRequestState {
239 health_monitor: Arc::new(self.health_monitor),
240 snapshot_runtime: self.snapshot_runtime,
241 runtime_plan: self.runtime_plan,
242 rpc_url: Arc::new(self.read_rpc_url.or_else(resolve_rpc_url)),
243 rpc_client: ReadRpcClient {
244 client: Client::builder()
245 .connect_timeout(self.limits.upstream_connect_timeout)
246 .timeout(self.limits.upstream_timeout)
247 .build()?,
248 max_response_bytes: self.limits.max_upstream_response_bytes,
249 },
250 body_limits: BodyLimits {
251 max_bytes: self.limits.max_body_bytes,
252 read_timeout: self.limits.body_read_timeout,
253 },
254 program_runtime_catalog: Arc::new(self.program_runtime_catalog),
255 auth_plugin: Arc::new(self.auth_plugin),
256 limit_state: Arc::new(HttpLimitState::default()),
257 transaction_state: Arc::new(transaction_state),
258 token_accounts_provider: self.token_accounts_provider,
259 solana_gateway_usage_observer: self.solana_gateway_usage_observer,
260 solana_gateway_target_id: Arc::new(self.solana_gateway_target_id),
261 program_read_binding_target_id: Arc::new(self.program_read_binding_target_id),
262 };
263
264 let shutdown = self.shutdown.unwrap_or_default();
265 let header_read_timeout = self.limits.header_read_timeout;
266 let bind_addr = self.bind_addr;
267 let mut connections = accept_connections(&listener, &shutdown, |stream, remote_addr| {
268 let io = TokioIo::new(stream);
269 let request_state = request_state.clone();
270 let connection_shutdown = shutdown.clone();
271 async move {
272 let service = service_fn(move |req| {
273 let request_state = request_state.clone();
274 async move { handle_request(remote_addr, req, request_state).await }
275 });
276 let connection = http1::Builder::new()
278 .timer(TokioTimer::new())
279 .header_read_timeout(header_read_timeout)
280 .serve_connection(io, service);
281 tokio::pin!(connection);
282 let result = tokio::select! {
283 result = &mut connection => result,
284 _ = connection_shutdown.cancelled() => {
285 connection.as_mut().graceful_shutdown();
286 connection.await
287 }
288 };
289 if let Err(e) = result {
290 error!("HTTP connection error: {}", e);
291 }
292 }
293 })
294 .await;
295 info!("HTTP health server on {} stopping", bind_addr);
296
297 if tokio::time::timeout(HTTP_CONNECTION_DRAIN_TIMEOUT, async {
298 while connections.join_next().await.is_some() {}
299 })
300 .await
301 .is_err()
302 {
303 warn!(
304 "HTTP connections did not drain within {:?}; aborting them",
305 HTTP_CONNECTION_DRAIN_TIMEOUT
306 );
307 connections.abort_all();
308 while connections.join_next().await.is_some() {}
309 }
310
311 Ok(())
312 }
313}
314
315async fn accept_connections<F, Fut>(
322 listener: &TcpListener,
323 shutdown: &CancellationToken,
324 mut serve: F,
325) -> JoinSet<()>
326where
327 F: FnMut(tokio::net::TcpStream, SocketAddr) -> Fut,
328 Fut: std::future::Future<Output = ()> + Send + 'static,
329{
330 let mut connections = JoinSet::new();
331 loop {
332 let accepted = tokio::select! {
333 _ = shutdown.cancelled() => break,
334 Some(_) = connections.join_next(), if !connections.is_empty() => continue,
335 accepted = listener.accept() => accepted,
336 };
337 match accepted {
338 Ok((stream, remote_addr)) => {
339 connections.spawn(serve(stream, remote_addr));
340 }
341 Err(e) => {
342 error!("Failed to accept HTTP connection: {}", e);
343 }
344 }
345 }
346 connections
347}
348
349async fn handle_request(
350 remote_addr: SocketAddr,
351 mut req: Request<hyper::body::Incoming>,
352 state: HttpRequestState,
353) -> Result<Response<Full<Bytes>>, Infallible> {
354 req.extensions_mut().insert(state.body_limits);
356 if req.method() == Method::OPTIONS {
357 return Ok(with_cors(
358 Response::builder()
359 .status(StatusCode::NO_CONTENT)
360 .body(Full::new(Bytes::new()))
361 .unwrap(),
362 ));
363 }
364
365 let response = handle_request_inner(remote_addr, req, state).await?;
366 Ok(with_cors(response))
367}
368
369fn with_cors(mut response: Response<Full<Bytes>>) -> Response<Full<Bytes>> {
370 let headers = response.headers_mut();
371 headers.insert(ACCESS_CONTROL_ALLOW_ORIGIN, HeaderValue::from_static("*"));
372 headers.insert(
373 ACCESS_CONTROL_ALLOW_METHODS,
374 HeaderValue::from_static("GET, POST, OPTIONS"),
375 );
376 headers.insert(
377 ACCESS_CONTROL_ALLOW_HEADERS,
378 HeaderValue::from_static("Authorization, Content-Type"),
379 );
380 headers.insert(
381 ACCESS_CONTROL_EXPOSE_HEADERS,
382 HeaderValue::from_static(
383 "Retry-After, X-Error-Code, X-Request-Id, X-Arete-Upstream-Attempted, X-Arete-Program-Release-Hash, X-Arete-Idl-Content-Hash, X-Arete-Account-Address, X-Arete-Account-Exists",
384 ),
385 );
386 headers.insert(ACCESS_CONTROL_MAX_AGE, HeaderValue::from_static("86400"));
387 response
388}
389
390async fn handle_request_inner(
391 remote_addr: SocketAddr,
392 req: Request<hyper::body::Incoming>,
393 state: HttpRequestState,
394) -> Result<Response<Full<Bytes>>, Infallible> {
395 let HttpRequestState {
396 health_monitor,
397 snapshot_runtime,
398 runtime_plan,
399 rpc_url,
400 rpc_client,
401 body_limits,
402 program_runtime_catalog,
403 auth_plugin,
404 limit_state,
405 transaction_state,
406 token_accounts_provider,
407 solana_gateway_usage_observer,
408 solana_gateway_target_id,
409 program_read_binding_target_id,
410 } = state;
411 let path = req.uri().path().to_string();
412
413 match path.as_str() {
414 "/health" | "/healthz" if runtime_plan.health => {
415 Ok(Response::builder()
417 .status(StatusCode::OK)
418 .header("Content-Type", "text/plain")
419 .body(Full::new(Bytes::from("OK")))
420 .unwrap())
421 }
422 "/ready" | "/readiness" if runtime_plan.health => {
423 let stream_ready = match health_monitor.as_ref() {
427 Some(monitor) => monitor.is_healthy().await,
428 None => true,
430 };
431 let snapshot_ready = snapshot_runtime
432 .as_ref()
433 .is_none_or(crate::snapshot::SnapshotRuntime::resume_gate_ready);
434 if stream_ready && snapshot_ready {
435 Ok(Response::builder()
436 .status(StatusCode::OK)
437 .header("Content-Type", "text/plain")
438 .body(Full::new(Bytes::from("READY")))
439 .unwrap())
440 } else {
441 Ok(Response::builder()
442 .status(StatusCode::SERVICE_UNAVAILABLE)
443 .header("Content-Type", "text/plain")
444 .body(Full::new(Bytes::from("NOT READY")))
445 .unwrap())
446 }
447 }
448 "/status" if runtime_plan.health => {
449 if let Some(monitor) = health_monitor.as_ref() {
451 let status = monitor.status().await;
452 let error_count = monitor.error_count().await;
453 let is_healthy = monitor.is_healthy().await;
454
455 let status_json = serde_json::json!({
456 "healthy": is_healthy,
457 "status": format!("{:?}", status),
458 "error_count": error_count
459 });
460
461 let status_code = if is_healthy {
462 StatusCode::OK
463 } else {
464 StatusCode::SERVICE_UNAVAILABLE
465 };
466
467 Ok(Response::builder()
468 .status(status_code)
469 .header("Content-Type", "application/json")
470 .body(Full::new(Bytes::from(status_json.to_string())))
471 .unwrap())
472 } else {
473 let status_json = serde_json::json!({
474 "healthy": true,
475 "status": "no_monitor",
476 "error_count": 0
477 });
478
479 Ok(Response::builder()
480 .status(StatusCode::OK)
481 .header("Content-Type", "application/json")
482 .body(Full::new(Bytes::from(status_json.to_string())))
483 .unwrap())
484 }
485 }
486 _ if runtime_plan.transactions && path.starts_with("/transactions/") => {
487 let Some(transaction_state) = transaction_state.as_ref() else {
488 return Ok(error_response(StatusCode::NOT_FOUND, "Not Found"));
489 };
490 let client_addr = transaction_state.client_addr(remote_addr, req.headers());
491 let auth_context = match authorize_http_request(
492 client_addr,
493 &req,
494 auth_plugin.as_ref().as_ref(),
495 &limit_state,
496 None,
497 false,
498 solana_gateway_target_id.as_deref(),
499 )
500 .await
501 {
502 Ok(context) => context,
503 Err(response) => return Ok(transaction_auth_error(response, path.as_str())),
504 };
505 Ok(transactions::handle(
506 client_addr,
507 req,
508 auth_context,
509 transaction_state.clone(),
510 body_limits.read_timeout,
511 )
512 .await)
513 }
514 _ if runtime_plan.chain_reads && path.starts_with("/chain/") => {
515 let auth_context = match authorize_http_request(
516 remote_addr,
517 &req,
518 auth_plugin.as_ref().as_ref(),
519 &limit_state,
520 Some(SCOPE_READ),
521 true,
522 solana_gateway_target_id.as_deref(),
523 )
524 .await
525 {
526 Ok(context) => context,
527 Err(response) => return Ok(response),
528 };
529 Ok(handle_chain_request(
530 req,
531 path.as_str(),
532 ChainRequestServices {
533 rpc_url,
534 rpc_client,
535 auth_context,
536 usage_observer: solana_gateway_usage_observer.as_ref(),
537 solana_gateway_target_id: solana_gateway_target_id.as_deref(),
538 provider: token_accounts_provider.as_ref(),
539 },
540 )
541 .await)
542 }
543 _ if runtime_plan.chain_reads && path == fork::FORK_RPC_PATH => {
544 if req.method() != Method::POST {
545 return Ok(error_response(
546 StatusCode::METHOD_NOT_ALLOWED,
547 "The fork endpoint accepts POST only",
548 ));
549 }
550 let auth_context = match authorize_http_request(
551 remote_addr,
552 &req,
553 auth_plugin.as_ref().as_ref(),
554 &limit_state,
555 Some(SCOPE_FORK_READ),
556 true,
557 solana_gateway_target_id.as_deref(),
558 )
559 .await
560 {
561 Ok(context) => context,
562 Err(response) => return Ok(response),
563 };
564 let body =
565 match tokio::time::timeout(body_limits.read_timeout, fork::read_body(req)).await {
566 Ok(Ok(body)) => body,
567 Ok(Err(response)) => return Ok(response),
568 Err(_) => return Ok(body_timeout_response()),
569 };
570 let payload = fork::parse_payload(&body);
571 if let Some(context) = auth_context.as_ref() {
574 if let Err(deny) =
575 enforce_http_limits_weighted(context, &limit_state, payload.weight() - 1)
576 {
577 return Ok(auth_deny_response(&deny));
578 }
579 }
580 let services = fork::ForkServices {
581 rpc_url: rpc_url.as_ref().as_deref(),
582 client: &rpc_client,
583 auth_context: auth_context.as_ref(),
584 usage_observer: solana_gateway_usage_observer.as_ref(),
585 target_id: solana_gateway_target_id.as_deref(),
586 max_response_bytes: fork::MAX_FORK_RESPONSE_BYTES,
587 };
588 Ok(fork::execute(payload, &services).await)
589 }
590 _ if path.starts_with("/v1/releases/") => {
591 if !runtime_plan.program_reads {
592 return Ok(program_read_error_response(
593 ProgramReadError::ProgramReadsDisabled,
594 ));
595 }
596 let auth_context = match authorize_http_request(
597 remote_addr,
598 &req,
599 auth_plugin.as_ref().as_ref(),
600 &limit_state,
601 Some(SCOPE_READ),
602 true,
603 None,
604 )
605 .await
606 {
607 Ok(context) => context,
608 Err(response) => return Ok(response),
609 };
610 Ok(handle_program_account_request(
611 req,
612 path.as_str(),
613 rpc_url,
614 rpc_client,
615 program_runtime_catalog,
616 auth_context,
617 program_read_binding_target_id,
618 )
619 .await)
620 }
621 _ => Ok(Response::builder()
622 .status(StatusCode::NOT_FOUND)
623 .header("Content-Type", "text/plain")
624 .body(Full::new(Bytes::from("Not Found")))
625 .unwrap()),
626 }
627}
628
629fn transaction_auth_error(response: Response<Full<Bytes>>, path: &str) -> Response<Full<Bytes>> {
630 let status = response.status();
631 let code = response
632 .headers()
633 .get("X-Error-Code")
634 .and_then(|value| value.to_str().ok())
635 .unwrap_or("authentication_failed")
636 .to_string();
637 let request_id = uuid::Uuid::new_v4().to_string();
638 let mut value = json!({
639 "code": code,
640 "message": "Transaction request authentication failed",
641 "retryable": status == StatusCode::TOO_MANY_REQUESTS || status == StatusCode::UNAUTHORIZED,
642 "requestId": request_id,
643 });
644 if path == "/transactions/v1/send" {
645 value["submissionState"] = json!("not_submitted");
646 }
647 Response::builder()
648 .status(status)
649 .header("Content-Type", "application/json")
650 .header("X-Error-Code", code)
651 .header("X-Request-Id", request_id)
652 .header("X-Arete-Upstream-Attempted", "false")
653 .body(Full::new(Bytes::from(value.to_string())))
654 .expect("valid transaction authentication response")
655}
656
657fn capability_error(
658 status: StatusCode,
659 code: &str,
660 message: impl Into<String>,
661) -> Response<Full<Bytes>> {
662 let mut response = json_response(status, json!({ "code": code, "error": message.into() }));
663 response.headers_mut().insert(
664 "X-Error-Code",
665 HeaderValue::from_str(code).expect("static error code"),
666 );
667 response
668}
669
670async fn handle_owner_token_accounts(
671 req: Request<hyper::body::Incoming>,
672 provider: Option<&Arc<dyn OwnerTokenAccountsProvider>>,
673 auth: Option<&crate::websocket::auth::AuthContext>,
674) -> Response<Full<Bytes>> {
675 let request = match read_json_body::<OwnerTokenAccountsRequest>(req).await {
676 Ok(request) => request,
677 Err(response) if response.status() != StatusCode::BAD_REQUEST => return response,
678 Err(_) => {
679 return capability_error(
680 StatusCode::BAD_REQUEST,
681 "invalid_request",
682 "Invalid JSON request",
683 )
684 }
685 };
686 if let Err(error) = request.validate() {
687 return capability_error(StatusCode::BAD_REQUEST, "invalid_request", error);
688 }
689 if request.limit as usize
690 > batch_address_limit(auth, arete_solana_contracts::MAX_PAGE_SIZE as usize)
691 {
692 return capability_error(
693 StatusCode::BAD_REQUEST,
694 "invalid_request",
695 "limit exceeds the session's address limit",
696 );
697 }
698 let Some(provider) = provider else {
699 return capability_error(
700 StatusCode::NOT_IMPLEMENTED,
701 "unsupported_capability",
702 "Owner token discovery is not configured",
703 );
704 };
705 match provider.owner_token_accounts(&request).await {
706 Ok(page) => match page.validate(&request) {
707 Ok(()) => json_response(
708 StatusCode::OK,
709 serde_json::to_value(page).expect("serializable page"),
710 ),
711 Err(error) => {
712 capability_error(StatusCode::BAD_GATEWAY, "invalid_provider_response", error)
713 }
714 },
715 Err(TokenDiscoveryError::InvalidCursor(error)) => {
716 capability_error(StatusCode::BAD_REQUEST, "invalid_cursor", error)
717 }
718 Err(TokenDiscoveryError::Unsupported(error)) => {
719 capability_error(StatusCode::NOT_IMPLEMENTED, "unsupported_capability", error)
720 }
721 Err(TokenDiscoveryError::Unavailable(error)) => {
722 capability_error(StatusCode::BAD_GATEWAY, "provider_unavailable", error)
723 }
724 }
725}
726
727#[derive(Deserialize)]
728#[serde(deny_unknown_fields)]
729struct ContextualAccountBody {
730 address: String,
731 #[serde(default)]
732 options: ReadOptions,
733}
734
735#[derive(Deserialize)]
736#[serde(deny_unknown_fields)]
737struct ContextualAccountsBody {
738 addresses: Vec<String>,
739 #[serde(default)]
740 options: ReadOptions,
741}
742
743async fn handle_contextual_accounts(
744 req: Request<hyper::body::Incoming>,
745 path: &str,
746 rpc_url: Option<&str>,
747 client: &ReadRpcClient,
748 auth: Option<&crate::websocket::auth::AuthContext>,
749) -> Response<Full<Bytes>> {
750 let single = path == "/chain/v1/account";
751 let (addresses, options) = if single {
752 match read_json_body::<ContextualAccountBody>(req).await {
753 Ok(body) => (vec![body.address], body.options),
754 Err(response) if response.status() != StatusCode::BAD_REQUEST => return response,
755 Err(_) => {
756 return capability_error(
757 StatusCode::BAD_REQUEST,
758 "invalid_request",
759 "Invalid JSON request",
760 )
761 }
762 }
763 } else {
764 match read_json_body::<ContextualAccountsBody>(req).await {
765 Ok(body) => (body.addresses, body.options),
766 Err(response) if response.status() != StatusCode::BAD_REQUEST => return response,
767 Err(_) => {
768 return capability_error(
769 StatusCode::BAD_REQUEST,
770 "invalid_request",
771 "Invalid JSON request",
772 )
773 }
774 }
775 };
776 if addresses.len() > batch_address_limit(auth, MAX_CHAIN_BATCH_ADDRESSES) {
777 return capability_error(
778 StatusCode::BAD_REQUEST,
779 "invalid_request",
780 "addresses exceeds the batch limit",
781 );
782 }
783 for address in &addresses {
784 if let Err(error) = arete_solana_contracts::validate_address(address) {
785 return capability_error(StatusCode::BAD_REQUEST, "invalid_request", error);
786 }
787 }
788 if addresses.is_empty() {
789 return json_response(StatusCode::OK, json!({ "context": null, "value": [] }));
790 }
791 let Some(rpc_url) = rpc_url else {
792 return capability_error(
793 StatusCode::SERVICE_UNAVAILABLE,
794 "rpc_unavailable",
795 "No RPC URL configured for chain reads",
796 );
797 };
798 let (method, address) = if single {
799 ("getAccountInfo", json!(addresses[0]))
800 } else {
801 ("getMultipleAccounts", json!(addresses))
802 };
803 match rpc_call(
804 client,
805 rpc_url,
806 method,
807 json!([address, options.rpc_config()]),
808 )
809 .await
810 .and_then(|result| contextual_accounts_json(&addresses, single, options, &result))
811 {
812 Ok(value) => json_response(StatusCode::OK, value),
813 Err(error) => {
814 let (message, correlation_id) = upstream_error_message(&error);
815 let code = "invalid_upstream_response";
816 let mut response = json_response(
817 StatusCode::BAD_GATEWAY,
818 json!({ "code": code, "error": message, "correlationId": correlation_id }),
819 );
820 let headers = response.headers_mut();
821 headers.insert("X-Error-Code", HeaderValue::from_static(code));
822 if let Ok(value) = HeaderValue::from_str(&correlation_id) {
823 headers.insert(CORRELATION_ID_HEADER, value);
824 }
825 response
826 }
827 }
828}
829
830fn contextual_accounts_json(
831 addresses: &[String],
832 single: bool,
833 options: ReadOptions,
834 result: &Value,
835) -> anyhow::Result<Value> {
836 let slot = result
837 .pointer("/context/slot")
838 .and_then(Value::as_u64)
839 .ok_or_else(|| anyhow::anyhow!("Missing actual read context"))?;
840 if options.min_context_slot.is_some_and(|min| slot < min) {
841 anyhow::bail!("Read context is below minContextSlot");
842 }
843 let value = result
844 .get("value")
845 .ok_or_else(|| anyhow::anyhow!("Missing account result"))?;
846 let values = if single {
847 vec![value.clone()]
848 } else {
849 value
850 .as_array()
851 .ok_or_else(|| anyhow::anyhow!("Invalid batch result"))?
852 .clone()
853 };
854 if values.len() != addresses.len() {
855 anyhow::bail!("Account result count does not match addresses");
856 }
857 let mut items = Vec::new();
858 for (address, account) in addresses.iter().zip(values) {
859 if account.is_null() {
860 items.push(Value::Null);
861 continue;
862 }
863 if account.get("lamports").and_then(Value::as_u64).is_none()
864 || account.get("owner").and_then(Value::as_str).is_none()
865 || account.get("executable").and_then(Value::as_bool).is_none()
866 || account.pointer("/data/0").and_then(Value::as_str).is_none()
867 || account.pointer("/data/1").and_then(Value::as_str) != Some("base64")
868 {
869 anyhow::bail!("Malformed raw account result");
870 }
871 items.push(raw_account_json(address, &account));
872 }
873 Ok(serde_json::to_value(Contextual {
874 context: Some(ReadContext { slot }),
875 value: if single {
876 items.remove(0)
877 } else {
878 json!(items)
879 },
880 })?)
881}
882
883#[derive(Debug, Deserialize)]
884struct AddressesBody {
885 addresses: Vec<String>,
886}
887
888#[derive(Debug, Deserialize)]
889struct BalanceBody {
890 owner: String,
891 mint: String,
892 #[serde(default, rename = "tokenProgram")]
893 token_program: Option<String>,
894 #[serde(default, rename = "minContextSlot")]
895 min_context_slot: Option<String>,
896}
897
898#[derive(Debug, Deserialize)]
899struct NativeBalanceBody {
900 address: String,
901 #[serde(default, rename = "minContextSlot")]
902 min_context_slot: Option<String>,
903}
904
905#[derive(Debug, Deserialize)]
906struct AccountsBody {
907 addresses: Vec<String>,
908}
909
910#[derive(Default)]
911struct HttpLimitState {
912 per_subject_per_minute: DashMap<String, (u64, u32)>,
913 per_account_per_minute: DashMap<String, (u64, u32)>,
916 swept_minute: std::sync::atomic::AtomicU64,
918}
919
920impl HttpLimitState {
921 fn sweep(&self, now_bucket: u64) {
927 let previous = self
928 .swept_minute
929 .swap(now_bucket, std::sync::atomic::Ordering::Relaxed);
930 if previous == now_bucket {
931 return;
932 }
933 for buckets in [&self.per_subject_per_minute, &self.per_account_per_minute] {
934 buckets.retain(|_, (minute, _)| *minute >= now_bucket);
935 }
936 }
937}
938
939fn resolve_rpc_url() -> Option<String> {
940 ["ARETE_READ_RPC_URL", "SOLANA_RPC_URL", "RPC_URL"]
941 .iter()
942 .find_map(|key| env::var(key).ok())
943 .filter(|value| !value.is_empty())
944}
945
946fn json_response(status: StatusCode, value: Value) -> Response<Full<Bytes>> {
947 Response::builder()
948 .status(status)
949 .header("Content-Type", "application/json")
950 .body(Full::new(Bytes::from(value.to_string())))
951 .unwrap()
952}
953
954fn error_response(status: StatusCode, message: impl Into<String>) -> Response<Full<Bytes>> {
955 json_response(status, json!({ "error": message.into() }))
956}
957
958const CORRELATION_ID_HEADER: &str = "X-Correlation-Id";
959const UPSTREAM_RPC_FAILED: &str = "Upstream RPC request failed";
960
961#[derive(Debug)]
966struct UpstreamRpcError(String);
967
968impl std::fmt::Display for UpstreamRpcError {
969 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
970 f.write_str(&self.0)
971 }
972}
973
974impl std::error::Error for UpstreamRpcError {}
975
976impl From<reqwest::Error> for UpstreamRpcError {
977 fn from(error: reqwest::Error) -> Self {
978 let error = error.without_url();
980 Self(format!(
981 "{error} (timeout: {}, connect: {}, status: {:?})",
982 error.is_timeout(),
983 error.is_connect(),
984 error.status().map(|status| status.as_u16()),
985 ))
986 }
987}
988
989fn upstream_error_message(error: &anyhow::Error) -> (String, String) {
991 let correlation_id = uuid::Uuid::new_v4().to_string();
992 warn!(
993 correlation_id = %correlation_id,
994 error = %error,
995 "Chain read upstream request failed"
996 );
997 let message = if error.downcast_ref::<UpstreamRpcError>().is_some() {
998 UPSTREAM_RPC_FAILED.to_string()
999 } else {
1000 error.to_string()
1002 };
1003 (message, correlation_id)
1004}
1005
1006fn upstream_error_response(error: &anyhow::Error) -> Response<Full<Bytes>> {
1007 let (message, correlation_id) = upstream_error_message(error);
1008 let mut response = json_response(
1009 StatusCode::BAD_GATEWAY,
1010 json!({ "error": message, "correlationId": correlation_id }),
1011 );
1012 if let Ok(value) = HeaderValue::from_str(&correlation_id) {
1013 response.headers_mut().insert(CORRELATION_ID_HEADER, value);
1014 }
1015 response
1016}
1017
1018fn parse_min_context_slot(value: Option<&str>) -> std::result::Result<Option<u64>, &'static str> {
1019 value
1020 .map(|slot| {
1021 slot.parse::<u64>()
1022 .map_err(|_| "minContextSlot must be a decimal u64 string")
1023 })
1024 .transpose()
1025}
1026
1027fn auth_deny_response(deny: &AuthDeny) -> Response<Full<Bytes>> {
1028 let mut builder = Response::builder()
1029 .status(deny.http_status)
1030 .header("Content-Type", "application/json")
1031 .header("X-Error-Code", deny.code.as_str());
1032
1033 if let Some(reset_at) = deny.reset_at {
1034 if let Ok(duration) = reset_at.duration_since(SystemTime::now()) {
1035 builder = builder.header("Retry-After", duration.as_secs().to_string());
1036 }
1037 }
1038
1039 builder
1040 .body(Full::new(Bytes::from(
1041 json!({
1042 "error": deny.reason,
1043 "message": deny.reason,
1044 "code": deny.code.as_str(),
1045 "retryable": deny.code.should_retry(),
1046 "fatal": !deny.code.should_retry() && !deny.code.should_refresh_token(),
1047 })
1048 .to_string(),
1049 )))
1050 .unwrap()
1051}
1052
1053#[allow(clippy::result_large_err)]
1054async fn authorize_http_request(
1055 remote_addr: SocketAddr,
1056 req: &Request<hyper::body::Incoming>,
1057 auth_plugin: Option<&Arc<dyn WebSocketAuthPlugin>>,
1058 limit_state: &HttpLimitState,
1059 required_scope: Option<&str>,
1060 enforce_read_limits: bool,
1061 solana_gateway_target_id: Option<&str>,
1062) -> std::result::Result<Option<crate::websocket::auth::AuthContext>, Response<Full<Bytes>>> {
1063 let Some(plugin) = auth_plugin else {
1064 return Ok(None);
1065 };
1066
1067 let mut auth_request = ConnectionAuthRequest::from_http_request(remote_addr, req);
1068 auth_request.query = None;
1070 let decision = plugin.authorize(&auth_request).await;
1071 let context = match decision {
1072 AuthDecision::Allow(context) => context,
1073 AuthDecision::Deny(deny) => return Err(auth_deny_response(&deny)),
1074 };
1075
1076 if let Some(target_id) = solana_gateway_target_id {
1077 if let Err(error) =
1078 arete_auth::SolanaGatewayAuthorization::validate_target(&context, target_id)
1079 {
1080 return Err(Response::builder()
1081 .status(StatusCode::FORBIDDEN)
1082 .header("Content-Type", "application/json")
1083 .header("X-Error-Code", "invalid_gateway_target")
1084 .body(Full::new(Bytes::from(
1085 json!({
1086 "error": "invalid_gateway_target",
1087 "message": error.to_string(),
1088 "code": "invalid_gateway_target",
1089 "retryable": false,
1090 "fatal": true
1091 })
1092 .to_string(),
1093 )))
1094 .expect("valid gateway target error response"));
1095 }
1096 }
1097
1098 if let Some(required_scope) = required_scope {
1099 if !context.has_scope(required_scope) {
1100 return Err(json_response(
1101 StatusCode::FORBIDDEN,
1102 json!({
1103 "error": "insufficient_scope",
1104 "message": format!("Required scope: {required_scope}"),
1105 "code": "insufficient_scope",
1106 "retryable": false,
1107 "fatal": true
1108 }),
1109 ));
1110 }
1111 }
1112
1113 if enforce_read_limits {
1114 enforce_http_limits(&context, limit_state).map_err(|deny| auth_deny_response(&deny))?;
1115 }
1116 Ok(Some(context))
1117}
1118
1119fn enforce_http_limits(
1120 context: &crate::websocket::auth::AuthContext,
1121 limit_state: &HttpLimitState,
1122) -> std::result::Result<(), Box<AuthDeny>> {
1123 enforce_http_limits_weighted(context, limit_state, 1)
1124}
1125
1126fn enforce_http_limits_weighted(
1134 context: &crate::websocket::auth::AuthContext,
1135 limit_state: &HttpLimitState,
1136 weight: usize,
1137) -> std::result::Result<(), Box<AuthDeny>> {
1138 if weight == 0 {
1139 return Ok(());
1140 }
1141 let weight = u32::try_from(weight).unwrap_or(u32::MAX);
1142 let now_bucket = SystemTime::now()
1143 .duration_since(UNIX_EPOCH)
1144 .unwrap_or(Duration::from_secs(0))
1145 .as_secs()
1146 / 60;
1147 limit_state.sweep(now_bucket);
1148 let denied = || {
1149 Box::new(AuthDeny::rate_limited(
1150 Duration::from_secs(60),
1151 "http reads",
1152 ))
1153 };
1154
1155 let session = context.limits.max_http_requests_per_minute.map(|limit| {
1156 (
1157 format!("{}:{}", context.subject, context.metering_key),
1158 limit,
1159 )
1160 });
1161 if let Some((key, limit)) = &session {
1162 if !charge_bucket(
1163 &limit_state.per_subject_per_minute,
1164 key,
1165 *limit,
1166 weight,
1167 now_bucket,
1168 ) {
1169 return Err(denied());
1170 }
1171 }
1172
1173 let account = (!context.is_legacy_policy() && context.account_key.is_some())
1174 .then_some(context.account_limits.max_http_requests_per_minute)
1175 .flatten();
1176 if let Some(limit) = account {
1177 if !charge_bucket(
1178 &limit_state.per_account_per_minute,
1179 context.account_key(),
1180 limit,
1181 weight,
1182 now_bucket,
1183 ) {
1184 if let Some((key, _)) = &session {
1186 if let Some(mut entry) = limit_state.per_subject_per_minute.get_mut(key) {
1187 if entry.0 == now_bucket {
1188 entry.1 = entry.1.saturating_sub(weight);
1189 }
1190 }
1191 }
1192 return Err(denied());
1193 }
1194 }
1195 Ok(())
1196}
1197
1198fn charge_bucket(
1200 buckets: &DashMap<String, (u64, u32)>,
1201 key: &str,
1202 limit: u32,
1203 weight: u32,
1204 now_bucket: u64,
1205) -> bool {
1206 let mut entry = buckets.entry(key.to_string()).or_insert((now_bucket, 0));
1207 if entry.0 != now_bucket {
1208 *entry = (now_bucket, 0);
1209 }
1210 if entry.1.saturating_add(weight) > limit {
1211 return false;
1212 }
1213 entry.1 += weight;
1214 true
1215}
1216
1217#[derive(Clone, Copy, Debug)]
1220struct BodyLimits {
1221 max_bytes: usize,
1222 read_timeout: Duration,
1223}
1224
1225impl Default for BodyLimits {
1226 fn default() -> Self {
1227 let limits = HttpLimitsConfig::default();
1228 Self {
1229 max_bytes: limits.max_body_bytes,
1230 read_timeout: limits.body_read_timeout,
1231 }
1232 }
1233}
1234
1235fn body_too_large_response() -> Response<Full<Bytes>> {
1236 error_response(
1237 StatusCode::PAYLOAD_TOO_LARGE,
1238 "Request body exceeds the endpoint limit",
1239 )
1240}
1241
1242fn body_timeout_response() -> Response<Full<Bytes>> {
1243 error_response(
1244 StatusCode::REQUEST_TIMEOUT,
1245 "Request body was not received in time",
1246 )
1247}
1248
1249#[allow(clippy::result_large_err)]
1252async fn read_json_body<T: for<'de> Deserialize<'de>>(
1253 req: Request<hyper::body::Incoming>,
1254) -> std::result::Result<T, Response<Full<Bytes>>> {
1255 let limits = req
1256 .extensions()
1257 .get::<BodyLimits>()
1258 .copied()
1259 .unwrap_or_default();
1260 if req
1261 .headers()
1262 .get(hyper::header::CONTENT_LENGTH)
1263 .and_then(|value| value.to_str().ok())
1264 .and_then(|value| value.parse::<u64>().ok())
1265 .is_some_and(|length| length > limits.max_bytes as u64)
1266 {
1267 return Err(body_too_large_response());
1268 }
1269 let body = http_body_util::Limited::new(req.into_body(), limits.max_bytes);
1270 let collected = match tokio::time::timeout(limits.read_timeout, body.collect()).await {
1271 Ok(Ok(collected)) => collected,
1272 Ok(Err(error)) if error.is::<http_body_util::LengthLimitError>() => {
1273 return Err(body_too_large_response())
1274 }
1275 Ok(Err(_)) => {
1276 return Err(error_response(
1277 StatusCode::BAD_REQUEST,
1278 "Request body could not be read",
1279 ))
1280 }
1281 Err(_) => return Err(body_timeout_response()),
1282 };
1283 serde_json::from_slice::<T>(&collected.to_bytes())
1284 .map_err(|err| error_response(StatusCode::BAD_REQUEST, err.to_string()))
1285}
1286
1287#[derive(Clone, Copy)]
1288enum ChainUsageOperation {
1289 Exists,
1290 Lamports,
1291 NativeBalance,
1292 RentExemption,
1293 Clock,
1294 Account,
1295 Mint,
1296 TokenAccount,
1297 Accounts,
1298 Balances,
1299 OwnerTokenAccounts,
1300 ContextualAccounts,
1301}
1302
1303impl ChainUsageOperation {
1304 fn from_request(method: &Method, path: &str) -> Option<Self> {
1305 match (method.as_str(), path) {
1306 ("GET", path) if path.starts_with("/chain/exists/") => Some(Self::Exists),
1307 ("GET", path) if path.starts_with("/chain/lamports/") => Some(Self::Lamports),
1308 ("POST", "/chain/native-balance") => Some(Self::NativeBalance),
1309 ("GET", path) if path.starts_with("/chain/rent-exemption/") => {
1310 Some(Self::RentExemption)
1311 }
1312 ("GET", "/chain/clock") => Some(Self::Clock),
1313 ("GET", path) if path.starts_with("/chain/accounts/") => Some(Self::Account),
1314 ("GET", path) if path.starts_with("/chain/mints/") => Some(Self::Mint),
1315 ("GET", path) if path.starts_with("/chain/token-accounts/") => Some(Self::TokenAccount),
1316 ("POST", "/chain/accounts") => Some(Self::Accounts),
1317 ("POST", "/chain/v1/account") => Some(Self::Account),
1318 ("POST", "/chain/v1/accounts") => Some(Self::ContextualAccounts),
1319 ("POST", "/chain/v1/owner-token-accounts") => Some(Self::OwnerTokenAccounts),
1320 ("POST", "/chain/balances") => Some(Self::Balances),
1321 _ => None,
1322 }
1323 }
1324
1325 fn name(self) -> &'static str {
1326 match self {
1327 Self::Exists => "exists",
1328 Self::Lamports => "lamports",
1329 Self::NativeBalance => "native_balance",
1330 Self::RentExemption => "rent_exemption",
1331 Self::Clock => "clock",
1332 Self::Account => "account",
1333 Self::Mint => "mint",
1334 Self::TokenAccount => "token_account",
1335 Self::Accounts => "accounts",
1336 Self::Balances => "balances",
1337 Self::OwnerTokenAccounts => "owner_token_accounts",
1338 Self::ContextualAccounts => "contextual_accounts",
1339 }
1340 }
1341
1342 fn address_count(self, status: StatusCode, body: &[u8]) -> u64 {
1343 if !status.is_success() {
1344 return 0;
1345 }
1346 match self {
1347 Self::RentExemption | Self::Clock => 0,
1348 Self::Balances => 2,
1349 Self::ContextualAccounts | Self::OwnerTokenAccounts => {
1350 serde_json::from_slice::<Value>(body)
1351 .ok()
1352 .and_then(|value| {
1353 value
1354 .get(if matches!(self, Self::ContextualAccounts) {
1355 "value"
1356 } else {
1357 "items"
1358 })?
1359 .as_array()
1360 .map(|items| items.len() as u64)
1361 })
1362 .unwrap_or_default()
1363 }
1364 Self::Accounts => serde_json::from_slice::<Value>(body)
1365 .ok()
1366 .and_then(|value| value.get("items")?.as_array().map(|items| items.len()))
1367 .and_then(|count| count.try_into().ok())
1368 .unwrap_or_default(),
1369 _ => 1,
1370 }
1371 }
1372}
1373
1374struct ChainRequestServices<'a> {
1375 rpc_url: Arc<Option<String>>,
1376 rpc_client: ReadRpcClient,
1377 auth_context: Option<crate::websocket::auth::AuthContext>,
1378 usage_observer: Option<&'a Arc<dyn SolanaGatewayUsageObserver>>,
1379 solana_gateway_target_id: Option<&'a str>,
1380 provider: Option<&'a Arc<dyn OwnerTokenAccountsProvider>>,
1381}
1382
1383async fn handle_chain_request(
1384 req: Request<hyper::body::Incoming>,
1385 path: &str,
1386 services: ChainRequestServices<'_>,
1387) -> Response<Full<Bytes>> {
1388 let ChainRequestServices {
1389 rpc_url,
1390 rpc_client,
1391 auth_context,
1392 usage_observer,
1393 solana_gateway_target_id,
1394 provider,
1395 } = services;
1396 let operation = ChainUsageOperation::from_request(req.method(), path);
1397 let response = handle_chain_request_inner(
1398 req,
1399 path,
1400 rpc_url,
1401 rpc_client,
1402 auth_context.clone(),
1403 provider,
1404 )
1405 .await;
1406 let status = response.status();
1407 let (parts, body) = response.into_parts();
1408 let collected = match body.collect().await {
1409 Ok(collected) => collected,
1410 Err(error) => match error {},
1411 };
1412 let bytes = collected.to_bytes();
1413 if let (Some(operation), Some(observer), Some(target_id)) =
1414 (operation, usage_observer, solana_gateway_target_id)
1415 {
1416 observer
1417 .observe(SolanaGatewayUsageObservation {
1418 target_id: target_id.to_string(),
1419 auth_context,
1420 surface: SolanaGatewayUsageSurface::ChainRead,
1421 operation: operation.name(),
1422 result: if status.is_success() { "ok" } else { "error" },
1423 address_count: operation.address_count(status, &bytes),
1424 response_bytes: bytes.len().try_into().unwrap_or(u64::MAX),
1425 })
1426 .await;
1427 }
1428 Response::from_parts(parts, Full::new(bytes))
1429}
1430
1431async fn handle_chain_request_inner(
1432 req: Request<hyper::body::Incoming>,
1433 path: &str,
1434 rpc_url: Arc<Option<String>>,
1435 rpc_client: ReadRpcClient,
1436 auth_context: Option<crate::websocket::auth::AuthContext>,
1437 provider: Option<&Arc<dyn OwnerTokenAccountsProvider>>,
1438) -> Response<Full<Bytes>> {
1439 if req.method() == Method::POST && path == "/chain/v1/owner-token-accounts" {
1440 return handle_owner_token_accounts(req, provider, auth_context.as_ref()).await;
1441 }
1442 if req.method() == Method::POST && matches!(path, "/chain/v1/account" | "/chain/v1/accounts") {
1443 return handle_contextual_accounts(
1444 req,
1445 path,
1446 rpc_url.as_ref().as_deref(),
1447 &rpc_client,
1448 auth_context.as_ref(),
1449 )
1450 .await;
1451 }
1452 let Some(rpc_url) = rpc_url.as_ref() else {
1453 return error_response(
1454 StatusCode::SERVICE_UNAVAILABLE,
1455 "No RPC URL configured for chain reads",
1456 );
1457 };
1458
1459 match (req.method().as_str(), path) {
1460 ("GET", path) if path.starts_with("/chain/exists/") => {
1461 let address = path.trim_start_matches("/chain/exists/");
1462 match rpc_get_account_info(&rpc_client, rpc_url, address).await {
1463 Ok(value) => json_response(StatusCode::OK, json!({ "exists": !value.is_null() })),
1464 Err(err) => upstream_error_response(&err),
1465 }
1466 }
1467 ("GET", path) if path.starts_with("/chain/lamports/") => {
1468 let address = path.trim_start_matches("/chain/lamports/");
1469 match rpc_call(
1470 &rpc_client,
1471 rpc_url,
1472 "getBalance",
1473 json!([address, { "commitment": "confirmed" }]),
1474 )
1475 .await
1476 {
1477 Ok(value) => json_response(
1478 StatusCode::OK,
1479 json!({ "lamports": value.pointer("/value").and_then(Value::as_u64).unwrap_or(0) }),
1480 ),
1481 Err(err) => upstream_error_response(&err),
1482 }
1483 }
1484 ("POST", "/chain/native-balance") => match read_json_body::<NativeBalanceBody>(req).await {
1485 Ok(body) => {
1486 let min_context_slot =
1487 match parse_min_context_slot(body.min_context_slot.as_deref()) {
1488 Ok(slot) => slot,
1489 Err(message) => return error_response(StatusCode::BAD_REQUEST, message),
1490 };
1491 match rpc_get_native_balance(&rpc_client, rpc_url, &body.address, min_context_slot)
1492 .await
1493 {
1494 Ok(balance) => json_response(StatusCode::OK, balance),
1495 Err(err) => upstream_error_response(&err),
1496 }
1497 }
1498 Err(response) => response,
1499 },
1500 ("GET", path) if path.starts_with("/chain/rent-exemption/") => {
1501 let raw_space = path.trim_start_matches("/chain/rent-exemption/");
1502 let Ok(space) = raw_space.parse::<u64>() else {
1503 return error_response(
1504 StatusCode::BAD_REQUEST,
1505 "rent-exemption space must be an integer",
1506 );
1507 };
1508 match rpc_call(
1509 &rpc_client,
1510 rpc_url,
1511 "getMinimumBalanceForRentExemption",
1512 json!([space, { "commitment": "confirmed" }]),
1513 )
1514 .await
1515 {
1516 Ok(value) => json_response(
1517 StatusCode::OK,
1518 json!({ "lamports": value.as_u64().unwrap_or(0) }),
1519 ),
1520 Err(err) => upstream_error_response(&err),
1521 }
1522 }
1523 ("GET", "/chain/clock") => {
1524 let slot = rpc_call(
1525 &rpc_client,
1526 rpc_url,
1527 "getSlot",
1528 json!([{ "commitment": "confirmed" }]),
1529 )
1530 .await;
1531 let epoch_info = rpc_call(
1532 &rpc_client,
1533 rpc_url,
1534 "getEpochInfo",
1535 json!([{ "commitment": "confirmed" }]),
1536 )
1537 .await;
1538 match (slot, epoch_info) {
1539 (Ok(slot_value), Ok(epoch_value)) => {
1540 let slot_num = slot_value.as_u64().unwrap_or(0);
1541 let unix_timestamp =
1542 rpc_call(&rpc_client, rpc_url, "getBlockTime", json!([slot_num]))
1543 .await
1544 .ok()
1545 .and_then(|value| value.as_i64())
1546 .unwrap_or_default();
1547 json_response(
1548 StatusCode::OK,
1549 json!({
1550 "slot": slot_num,
1551 "epoch": epoch_value.get("epoch").and_then(Value::as_u64),
1552 "leaderScheduleEpoch": epoch_value.get("leaderScheduleSlotOffset").and_then(Value::as_u64),
1553 "unixTimestamp": unix_timestamp,
1554 }),
1555 )
1556 }
1557 (Err(err), _) | (_, Err(err)) => upstream_error_response(&err),
1558 }
1559 }
1560 ("GET", path) if path.starts_with("/chain/accounts/") => {
1561 let address = path.trim_start_matches("/chain/accounts/");
1562 match rpc_get_account_info(&rpc_client, rpc_url, address).await {
1563 Ok(value) if value.is_null() => json_response(StatusCode::OK, Value::Null),
1564 Ok(value) => json_response(StatusCode::OK, raw_account_json(address, &value)),
1565 Err(err) => upstream_error_response(&err),
1566 }
1567 }
1568 ("GET", path) if path.starts_with("/chain/mints/") => {
1569 let address = path.trim_start_matches("/chain/mints/");
1570 match rpc_get_parsed_account_info(&rpc_client, rpc_url, address).await {
1571 Ok(Some(value)) => json_response(StatusCode::OK, mint_info_json(address, &value)),
1572 Ok(None) => json_response(StatusCode::OK, Value::Null),
1573 Err(err) => upstream_error_response(&err),
1574 }
1575 }
1576 ("GET", path) if path.starts_with("/chain/token-accounts/") => {
1577 let address = path.trim_start_matches("/chain/token-accounts/");
1578 match rpc_get_parsed_account_info(&rpc_client, rpc_url, address).await {
1579 Ok(Some(value)) => {
1580 json_response(StatusCode::OK, token_account_json(address, &value))
1581 }
1582 Ok(None) => json_response(StatusCode::OK, Value::Null),
1583 Err(err) => upstream_error_response(&err),
1584 }
1585 }
1586 ("POST", "/chain/accounts") => match read_json_body::<AccountsBody>(req).await {
1587 Ok(body) => {
1588 let configured_limit =
1589 batch_address_limit(auth_context.as_ref(), MAX_CHAIN_BATCH_ADDRESSES);
1590 if body.addresses.len() > configured_limit {
1591 return error_response(
1592 StatusCode::BAD_REQUEST,
1593 format!(
1594 "addresses exceeds the {configured_limit}-address limit for one batch"
1595 ),
1596 );
1597 }
1598 if body.addresses.is_empty() {
1599 return json_response(StatusCode::OK, json!({ "items": [] }));
1600 }
1601 match rpc_get_multiple_accounts(&rpc_client, rpc_url, &body.addresses).await {
1602 Ok(values) if values.len() == body.addresses.len() => json_response(
1605 StatusCode::OK,
1606 json!({ "items": batch_accounts_json(&body.addresses, &values) }),
1607 ),
1608 Ok(values) => error_response(
1609 StatusCode::BAD_GATEWAY,
1610 format!(
1611 "getMultipleAccounts returned {} entries for {} addresses",
1612 values.len(),
1613 body.addresses.len()
1614 ),
1615 ),
1616 Err(err) => upstream_error_response(&err),
1617 }
1618 }
1619 Err(response) => response,
1620 },
1621 ("POST", "/chain/balances") => match read_json_body::<BalanceBody>(req).await {
1622 Ok(body) => {
1623 let min_context_slot =
1624 match parse_min_context_slot(body.min_context_slot.as_deref()) {
1625 Ok(slot) => slot,
1626 Err(message) => return error_response(StatusCode::BAD_REQUEST, message),
1627 };
1628 match rpc_get_token_balance(
1629 &rpc_client,
1630 rpc_url,
1631 &body.owner,
1632 &body.mint,
1633 body.token_program.as_deref(),
1634 min_context_slot,
1635 )
1636 .await
1637 {
1638 Ok(balance) => json_response(StatusCode::OK, balance),
1639 Err(err) => upstream_error_response(&err),
1640 }
1641 }
1642 Err(response) => response,
1643 },
1644 _ => error_response(StatusCode::NOT_FOUND, "Not Found"),
1645 }
1646}
1647
1648async fn handle_program_account_request(
1649 req: Request<hyper::body::Incoming>,
1650 path: &str,
1651 rpc_url: Arc<Option<String>>,
1652 rpc_client: ReadRpcClient,
1653 program_runtime_catalog: Arc<ProgramRuntimeCatalog>,
1654 auth_context: Option<crate::websocket::auth::AuthContext>,
1655 program_read_binding_target_id: Arc<Option<String>>,
1656) -> Response<Full<Bytes>> {
1657 let route = match ProgramReadRoute::parse(req.method(), path) {
1658 Ok(route) => route,
1659 Err(error) => return program_read_error_response(error),
1660 };
1661 let release_hash = match route.release_hash.parse() {
1662 Ok(release_hash) => release_hash,
1663 Err(_) => return program_read_error_response(ProgramReadError::InvalidReleaseHash),
1664 };
1665 let Some(definition) = program_runtime_catalog.get(&release_hash).cloned() else {
1666 return program_read_error_response(ProgramReadError::ReleaseNotFound);
1667 };
1668 if let Err(error) = authorize_program_read(
1669 auth_context.as_ref(),
1670 program_read_binding_target_id.as_deref(),
1671 &definition,
1672 ) {
1673 return with_release_metadata(program_read_error_response(error), &definition, None, None);
1674 }
1675 if matches!(route.operation, ProgramReadOperation::NativeQuery) {
1676 return with_release_metadata(
1677 capability_error(
1678 StatusCode::NOT_IMPLEMENTED,
1679 "unsupported_capability",
1680 "Native position query requires a platform service for this release/type/filter",
1681 ),
1682 &definition,
1683 None,
1684 None,
1685 );
1686 }
1687 let Some(rpc_url) = rpc_url.as_ref() else {
1688 return with_release_metadata(
1689 program_read_error_response(ProgramReadError::RpcNotConfigured),
1690 &definition,
1691 None,
1692 None,
1693 );
1694 };
1695
1696 match route.operation {
1697 ProgramReadOperation::NativeQuery => unreachable!("handled before RPC admission"),
1698 ProgramReadOperation::ContextualFetch { address } => {
1699 let options = match read_json_body::<ProgramContextOptionsBody>(req).await {
1700 Ok(body) => body.options,
1701 Err(response) if response.status() != StatusCode::BAD_REQUEST => return response,
1702 Err(_) => return program_read_error_response(ProgramReadError::InvalidRequest),
1703 };
1704 handle_contextual_program_accounts(
1705 &definition,
1706 &route.account,
1707 ContextualProgramRead {
1708 addresses: vec![address],
1709 single: true,
1710 options,
1711 },
1712 rpc_url,
1713 &rpc_client,
1714 auth_context.as_ref(),
1715 )
1716 .await
1717 }
1718 ProgramReadOperation::ContextualBatch => {
1719 let body = match read_json_body::<ContextualAccountsBody>(req).await {
1720 Ok(body) => body,
1721 Err(response) if response.status() != StatusCode::BAD_REQUEST => return response,
1722 Err(_) => return program_read_error_response(ProgramReadError::InvalidRequest),
1723 };
1724 handle_contextual_program_accounts(
1725 &definition,
1726 &route.account,
1727 ContextualProgramRead {
1728 addresses: body.addresses,
1729 single: false,
1730 options: body.options,
1731 },
1732 rpc_url,
1733 &rpc_client,
1734 auth_context.as_ref(),
1735 )
1736 .await
1737 }
1738 ProgramReadOperation::Fetch { address } => {
1739 let account_value = match rpc_get_account_info(&rpc_client, rpc_url, &address).await {
1740 Ok(value) => value,
1741 Err(error) => {
1742 error!("Program account RPC read failed: {}", error);
1743 return with_release_metadata(
1744 program_read_error_response(ProgramReadError::RpcFailed),
1745 &definition,
1746 Some(&address),
1747 None,
1748 );
1749 }
1750 };
1751 match decode_release_account(&definition, &route.account, &account_value).await {
1752 AccountReadOutcome::Missing => with_release_metadata(
1753 json_response(StatusCode::OK, Value::Null),
1754 &definition,
1755 Some(&address),
1756 Some(false),
1757 ),
1758 AccountReadOutcome::Value(value) => with_release_metadata(
1759 json_response(StatusCode::OK, value),
1760 &definition,
1761 Some(&address),
1762 Some(true),
1763 ),
1764 AccountReadOutcome::Error(error) => with_release_metadata(
1765 program_read_error_response(error),
1766 &definition,
1767 Some(&address),
1768 Some(true),
1769 ),
1770 }
1771 }
1772 ProgramReadOperation::Exists { address } => {
1773 let account_value = match rpc_get_account_info(&rpc_client, rpc_url, &address).await {
1774 Ok(value) => value,
1775 Err(error) => {
1776 error!("Program account existence RPC read failed: {}", error);
1777 return with_release_metadata(
1778 program_read_error_response(ProgramReadError::RpcFailed),
1779 &definition,
1780 Some(&address),
1781 None,
1782 );
1783 }
1784 };
1785 let exists = !account_value.is_null();
1786 if exists && !account_owner_matches(&definition, &account_value) {
1787 return with_release_metadata(
1788 program_read_error_response(ProgramReadError::AccountOwnerMismatch),
1789 &definition,
1790 Some(&address),
1791 Some(true),
1792 );
1793 }
1794 with_release_metadata(
1795 json_response(StatusCode::OK, json!({ "exists": exists })),
1796 &definition,
1797 Some(&address),
1798 Some(exists),
1799 )
1800 }
1801 ProgramReadOperation::Batch => {
1802 let body = match read_json_body::<AddressesBody>(req).await {
1803 Ok(body) => body,
1804 Err(response) if response.status() != StatusCode::BAD_REQUEST => return response,
1805 Err(_) => return program_read_error_response(ProgramReadError::InvalidRequest),
1806 };
1807 let configured_limit =
1808 batch_address_limit(auth_context.as_ref(), MAX_PROGRAM_BATCH_ADDRESSES);
1809 if body.addresses.len() > configured_limit {
1810 return with_release_metadata(
1811 program_read_error_response(ProgramReadError::BatchLimitExceeded),
1812 &definition,
1813 None,
1814 None,
1815 );
1816 }
1817 if body.addresses.is_empty() {
1818 return with_release_metadata(
1819 json_response(StatusCode::OK, json!({ "items": [] })),
1820 &definition,
1821 None,
1822 None,
1823 );
1824 }
1825
1826 let values =
1827 match rpc_get_multiple_accounts(&rpc_client, rpc_url, &body.addresses).await {
1828 Ok(values) if values.len() == body.addresses.len() => values,
1829 Ok(_) => {
1830 return with_release_metadata(
1831 program_read_error_response(ProgramReadError::RpcResponseInvalid),
1832 &definition,
1833 None,
1834 None,
1835 )
1836 }
1837 Err(error) => {
1838 error!("Program account batch RPC read failed: {}", error);
1839 return with_release_metadata(
1840 program_read_error_response(ProgramReadError::RpcFailed),
1841 &definition,
1842 None,
1843 None,
1844 );
1845 }
1846 };
1847
1848 let outcomes = futures_util::future::join_all(
1849 values
1850 .iter()
1851 .map(|value| decode_release_account(&definition, &route.account, value)),
1852 )
1853 .await;
1854 let mut items = Vec::with_capacity(outcomes.len());
1855 for (address, outcome) in body.addresses.iter().zip(outcomes) {
1856 let item = match outcome {
1857 AccountReadOutcome::Missing => {
1858 json!({ "address": address, "status": "missing" })
1859 }
1860 AccountReadOutcome::Value(value) => {
1861 json!({ "address": address, "status": "ok", "value": value })
1862 }
1863 AccountReadOutcome::Error(error) => json!({
1864 "address": address,
1865 "status": "error",
1866 "error": { "code": error.code() }
1867 }),
1868 };
1869 items.push(item);
1870 }
1871 with_release_metadata(
1872 json_response(StatusCode::OK, json!({ "items": items })),
1873 &definition,
1874 None,
1875 None,
1876 )
1877 }
1878 }
1879}
1880
1881const MAX_PROGRAM_BATCH_ADDRESSES: usize = 100;
1882#[derive(Deserialize)]
1883#[serde(deny_unknown_fields)]
1884struct ProgramContextOptionsBody {
1885 #[serde(default)]
1886 options: ReadOptions,
1887}
1888
1889struct ContextualProgramRead {
1890 addresses: Vec<String>,
1891 single: bool,
1892 options: ReadOptions,
1893}
1894
1895async fn handle_contextual_program_accounts(
1896 definition: &ProgramRuntimeDefinition,
1897 account: &str,
1898 read: ContextualProgramRead,
1899 rpc_url: &str,
1900 client: &ReadRpcClient,
1901 auth: Option<&crate::websocket::auth::AuthContext>,
1902) -> Response<Full<Bytes>> {
1903 let ContextualProgramRead {
1904 addresses,
1905 single,
1906 options,
1907 } = read;
1908 if addresses.len() > batch_address_limit(auth, MAX_PROGRAM_BATCH_ADDRESSES) {
1909 return with_release_metadata(
1910 program_read_error_response(ProgramReadError::BatchLimitExceeded),
1911 definition,
1912 None,
1913 None,
1914 );
1915 }
1916 for address in &addresses {
1917 if arete_solana_contracts::validate_address(address).is_err() {
1918 return program_read_error_response(ProgramReadError::InvalidRequest);
1919 }
1920 }
1921 if addresses.is_empty() {
1922 return with_release_metadata(
1923 json_response(
1924 StatusCode::OK,
1925 json!({ "context": null, "value": { "items": [] } }),
1926 ),
1927 definition,
1928 None,
1929 None,
1930 );
1931 }
1932 let method = if single {
1933 "getAccountInfo"
1934 } else {
1935 "getMultipleAccounts"
1936 };
1937 let input = if single {
1938 json!(addresses[0])
1939 } else {
1940 json!(addresses)
1941 };
1942 let result = match rpc_call(
1943 client,
1944 rpc_url,
1945 method,
1946 json!([input, options.rpc_config()]),
1947 )
1948 .await
1949 {
1950 Ok(result) => result,
1951 Err(_) => {
1952 return with_release_metadata(
1953 program_read_error_response(ProgramReadError::RpcFailed),
1954 definition,
1955 None,
1956 None,
1957 )
1958 }
1959 };
1960 let context = match result.pointer("/context/slot").and_then(Value::as_u64) {
1961 Some(slot) if options.min_context_slot.is_none_or(|min| slot >= min) => {
1962 ReadContext { slot }
1963 }
1964 _ => {
1965 return with_release_metadata(
1966 program_read_error_response(ProgramReadError::RpcResponseInvalid),
1967 definition,
1968 None,
1969 None,
1970 )
1971 }
1972 };
1973 let values = match result.get("value") {
1974 Some(value) if single => vec![value.clone()],
1975 Some(Value::Array(values)) if values.len() == addresses.len() => values.clone(),
1976 _ => {
1977 return with_release_metadata(
1978 program_read_error_response(ProgramReadError::RpcResponseInvalid),
1979 definition,
1980 None,
1981 None,
1982 )
1983 }
1984 };
1985 let outcomes = futures_util::future::join_all(
1986 values
1987 .iter()
1988 .map(|value| decode_release_account(definition, account, value)),
1989 )
1990 .await;
1991 let mut items = Vec::new();
1992 for (address, outcome) in addresses.iter().zip(outcomes) {
1993 if single {
1994 let (value, exists) = match outcome {
1995 AccountReadOutcome::Missing => (Value::Null, false),
1996 AccountReadOutcome::Value(value) => (value, true),
1997 AccountReadOutcome::Error(error) => {
1998 return with_release_metadata(
1999 program_read_error_response(error),
2000 definition,
2001 Some(address),
2002 Some(true),
2003 )
2004 }
2005 };
2006 return with_release_metadata(
2007 json_response(
2008 StatusCode::OK,
2009 json!({ "context": context, "value": value }),
2010 ),
2011 definition,
2012 Some(address),
2013 Some(exists),
2014 );
2015 }
2016 items.push(match outcome {
2017 AccountReadOutcome::Missing => json!({ "address": address, "status": "missing" }),
2018 AccountReadOutcome::Value(value) => {
2019 json!({ "address": address, "status": "ok", "value": value })
2020 }
2021 AccountReadOutcome::Error(error) => {
2022 json!({ "address": address, "status": "error", "error": { "code": error.code() } })
2023 }
2024 });
2025 }
2026 with_release_metadata(
2027 json_response(
2028 StatusCode::OK,
2029 json!({ "context": context, "value": { "items": items } }),
2030 ),
2031 definition,
2032 None,
2033 None,
2034 )
2035}
2036
2037const MAX_PROGRAM_ACCOUNT_BYTES: usize = 10 * 1024 * 1024;
2038const PROGRAM_DECODE_TIMEOUT: Duration = Duration::from_secs(2);
2039
2040fn authorize_program_read(
2041 auth_context: Option<&crate::websocket::auth::AuthContext>,
2042 expected_target_id: Option<&str>,
2043 definition: &ProgramRuntimeDefinition,
2044) -> std::result::Result<(), ProgramReadError> {
2045 let Some(context) = auth_context else {
2046 return Ok(());
2047 };
2048 let Some(expected_target_id) = expected_target_id.filter(|target_id| !target_id.is_empty())
2049 else {
2050 return Err(ProgramReadError::AuthorizationNotConfigured);
2051 };
2052
2053 arete_auth::ProgramReadAuthorization::try_from_context(
2054 context,
2055 expected_target_id,
2056 &definition.program_id,
2057 &definition.program_release_hash.to_string(),
2058 )
2059 .map(|_| ())
2060 .map_err(|_| ProgramReadError::Unauthorized)
2061}
2062
2063#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2064enum ProgramReadError {
2065 NotFound,
2066 ProgramReadsDisabled,
2067 InvalidReleaseHash,
2068 ReleaseNotFound,
2069 InvalidRequest,
2070 Unauthorized,
2071 AuthorizationNotConfigured,
2072 BatchLimitExceeded,
2073 RpcNotConfigured,
2074 RpcFailed,
2075 RpcResponseInvalid,
2076 AccountOwnerMismatch,
2077 AccountDataInvalid,
2078 AccountDataTooLarge,
2079 AccountDecodeFailed,
2080 AccountDecodeTimeout,
2081}
2082
2083impl ProgramReadError {
2084 fn code(self) -> &'static str {
2085 match self {
2086 Self::NotFound => "NOT_FOUND",
2087 Self::ProgramReadsDisabled => "PROGRAM_READS_DISABLED",
2088 Self::InvalidReleaseHash => "INVALID_PROGRAM_RELEASE_HASH",
2089 Self::ReleaseNotFound => "PROGRAM_RELEASE_NOT_FOUND",
2090 Self::InvalidRequest => "INVALID_REQUEST",
2091 Self::Unauthorized => "PROGRAM_READ_UNAUTHORIZED",
2092 Self::AuthorizationNotConfigured => "PROGRAM_READ_AUTH_NOT_CONFIGURED",
2093 Self::BatchLimitExceeded => "BATCH_LIMIT_EXCEEDED",
2094 Self::RpcNotConfigured => "READ_RPC_NOT_CONFIGURED",
2095 Self::RpcFailed => "RPC_REQUEST_FAILED",
2096 Self::RpcResponseInvalid => "RPC_RESPONSE_INVALID",
2097 Self::AccountOwnerMismatch => "ACCOUNT_OWNER_MISMATCH",
2098 Self::AccountDataInvalid => "ACCOUNT_DATA_INVALID",
2099 Self::AccountDataTooLarge => "ACCOUNT_DATA_TOO_LARGE",
2100 Self::AccountDecodeFailed => "ACCOUNT_DECODE_FAILED",
2101 Self::AccountDecodeTimeout => "ACCOUNT_DECODE_TIMEOUT",
2102 }
2103 }
2104
2105 fn status(self) -> StatusCode {
2106 match self {
2107 Self::NotFound | Self::ReleaseNotFound => StatusCode::NOT_FOUND,
2108 Self::InvalidReleaseHash | Self::InvalidRequest => StatusCode::BAD_REQUEST,
2109 Self::Unauthorized => StatusCode::FORBIDDEN,
2110 Self::AuthorizationNotConfigured => StatusCode::SERVICE_UNAVAILABLE,
2111 Self::BatchLimitExceeded | Self::AccountDataTooLarge => StatusCode::PAYLOAD_TOO_LARGE,
2112 Self::ProgramReadsDisabled | Self::RpcNotConfigured => StatusCode::SERVICE_UNAVAILABLE,
2113 Self::RpcFailed | Self::RpcResponseInvalid | Self::AccountDataInvalid => {
2114 StatusCode::BAD_GATEWAY
2115 }
2116 Self::AccountOwnerMismatch | Self::AccountDecodeFailed => {
2117 StatusCode::UNPROCESSABLE_ENTITY
2118 }
2119 Self::AccountDecodeTimeout => StatusCode::GATEWAY_TIMEOUT,
2120 }
2121 }
2122}
2123
2124fn program_read_error_response(error: ProgramReadError) -> Response<Full<Bytes>> {
2125 Response::builder()
2126 .status(error.status())
2127 .header("Content-Type", "application/json")
2128 .header("X-Error-Code", error.code())
2129 .body(Full::new(Bytes::from(
2130 json!({ "error": { "code": error.code() } }).to_string(),
2131 )))
2132 .expect("valid program read error response")
2133}
2134
2135#[derive(Debug)]
2136struct ProgramReadRoute {
2137 release_hash: String,
2138 account: String,
2139 operation: ProgramReadOperation,
2140}
2141
2142#[derive(Debug)]
2143enum ProgramReadOperation {
2144 ContextualFetch { address: String },
2145 ContextualBatch,
2146 NativeQuery,
2147 Fetch { address: String },
2148 Batch,
2149 Exists { address: String },
2150}
2151
2152impl ProgramReadRoute {
2153 fn parse(method: &Method, path: &str) -> std::result::Result<Self, ProgramReadError> {
2154 let segments: Vec<&str> = path.trim_start_matches('/').split('/').collect();
2155 if segments.len() < 5
2156 || segments[0] != "v1"
2157 || segments[1] != "releases"
2158 || segments[2].is_empty()
2159 || segments[3] != "accounts"
2160 || segments[4].is_empty()
2161 {
2162 return Err(ProgramReadError::NotFound);
2163 }
2164 let operation = match (method, segments.as_slice()) {
2165 (&Method::POST, [_, _, _, _, _]) => ProgramReadOperation::Batch,
2166 (&Method::POST, [_, _, _, _, _, "query"]) => ProgramReadOperation::NativeQuery,
2167 (&Method::POST, [_, _, _, _, _, "context"]) => ProgramReadOperation::ContextualBatch,
2168 (&Method::POST, [_, _, _, _, _, address, "context"]) if !address.is_empty() => {
2169 ProgramReadOperation::ContextualFetch {
2170 address: (*address).to_string(),
2171 }
2172 }
2173 (&Method::GET, [_, _, _, _, _, address]) if !address.is_empty() => {
2174 ProgramReadOperation::Fetch {
2175 address: (*address).to_string(),
2176 }
2177 }
2178 (&Method::GET, [_, _, _, _, _, address, "exists"]) if !address.is_empty() => {
2179 ProgramReadOperation::Exists {
2180 address: (*address).to_string(),
2181 }
2182 }
2183 _ => return Err(ProgramReadError::NotFound),
2184 };
2185 Ok(Self {
2186 release_hash: segments[2].to_string(),
2187 account: segments[4].to_string(),
2188 operation,
2189 })
2190 }
2191}
2192
2193enum AccountReadOutcome {
2194 Missing,
2195 Value(Value),
2196 Error(ProgramReadError),
2197}
2198
2199fn account_owner_matches(definition: &ProgramRuntimeDefinition, value: &Value) -> bool {
2200 value.get("owner").and_then(Value::as_str) == Some(definition.program_id.as_str())
2201}
2202
2203async fn decode_release_account(
2204 definition: &ProgramRuntimeDefinition,
2205 account: &str,
2206 value: &Value,
2207) -> AccountReadOutcome {
2208 if value.is_null() {
2209 return AccountReadOutcome::Missing;
2210 }
2211 if !account_owner_matches(definition, value) {
2212 return AccountReadOutcome::Error(ProgramReadError::AccountOwnerMismatch);
2213 }
2214 let data = match decode_account_bytes(value) {
2215 Some(data) => data,
2216 None => return AccountReadOutcome::Error(ProgramReadError::AccountDataInvalid),
2217 };
2218 if data.len() > MAX_PROGRAM_ACCOUNT_BYTES {
2219 return AccountReadOutcome::Error(ProgramReadError::AccountDataTooLarge);
2220 }
2221
2222 let reader = definition.account_reader.clone();
2223 let account = account.to_string();
2224 let decode = tokio::task::spawn_blocking(move || reader(&account, &data));
2225 match tokio::time::timeout(PROGRAM_DECODE_TIMEOUT, decode).await {
2226 Ok(Ok(Ok(value))) => AccountReadOutcome::Value(value),
2227 Ok(Ok(Err(_))) | Ok(Err(_)) => {
2228 AccountReadOutcome::Error(ProgramReadError::AccountDecodeFailed)
2229 }
2230 Err(_) => AccountReadOutcome::Error(ProgramReadError::AccountDecodeTimeout),
2231 }
2232}
2233
2234fn with_release_metadata(
2235 mut response: Response<Full<Bytes>>,
2236 definition: &ProgramRuntimeDefinition,
2237 address: Option<&str>,
2238 exists: Option<bool>,
2239) -> Response<Full<Bytes>> {
2240 let headers = response.headers_mut();
2241 if let Ok(value) = HeaderValue::from_str(&definition.program_release_hash.to_string()) {
2242 headers.insert("X-Arete-Program-Release-Hash", value);
2243 }
2244 if let Ok(value) = HeaderValue::from_str(&definition.idl_content_hash.to_string()) {
2245 headers.insert("X-Arete-Idl-Content-Hash", value);
2246 }
2247 if let Some(address) = address {
2248 if let Ok(value) = HeaderValue::from_str(address) {
2249 headers.insert("X-Arete-Account-Address", value);
2250 }
2251 }
2252 if let Some(exists) = exists {
2253 headers.insert(
2254 "X-Arete-Account-Exists",
2255 HeaderValue::from_static(if exists { "true" } else { "false" }),
2256 );
2257 }
2258 response
2259}
2260
2261#[derive(Clone)]
2264struct ReadRpcClient {
2265 client: Client,
2266 max_response_bytes: usize,
2267}
2268
2269impl std::ops::Deref for ReadRpcClient {
2270 type Target = Client;
2271
2272 fn deref(&self) -> &Client {
2273 &self.client
2274 }
2275}
2276
2277async fn read_capped_response(
2279 response: reqwest::Response,
2280 max_bytes: usize,
2281) -> std::result::Result<Vec<u8>, UpstreamRpcError> {
2282 let too_large = || UpstreamRpcError(format!("response exceeds {max_bytes} bytes"));
2283 if response
2284 .content_length()
2285 .is_some_and(|length| length > max_bytes as u64)
2286 {
2287 return Err(too_large());
2288 }
2289 let mut stream = response.bytes_stream();
2290 let mut bytes = Vec::new();
2291 while let Some(chunk) = futures_util::StreamExt::next(&mut stream).await {
2292 let chunk = chunk.map_err(UpstreamRpcError::from)?;
2293 if bytes.len().saturating_add(chunk.len()) > max_bytes {
2294 return Err(too_large());
2295 }
2296 bytes.extend_from_slice(&chunk);
2297 }
2298 Ok(bytes)
2299}
2300
2301async fn rpc_call(
2302 client: &ReadRpcClient,
2303 rpc_url: &str,
2304 method: &str,
2305 params: Value,
2306) -> anyhow::Result<Value> {
2307 let response = client
2308 .post(rpc_url)
2309 .json(&json!({
2310 "jsonrpc": "2.0",
2311 "id": "arete-read",
2312 "method": method,
2313 "params": params,
2314 }))
2315 .send()
2316 .await
2317 .and_then(reqwest::Response::error_for_status)
2318 .map_err(UpstreamRpcError::from)?;
2319 let body = read_capped_response(response, client.max_response_bytes).await?;
2320 let value = serde_json::from_slice::<Value>(&body)
2321 .map_err(|error| UpstreamRpcError(format!("invalid JSON-RPC response: {error}")))?;
2322 if let Some(error) = value.get("error") {
2323 return Err(UpstreamRpcError(format!("JSON-RPC error: {error}")).into());
2324 }
2325 Ok(value.get("result").cloned().unwrap_or(Value::Null))
2326}
2327
2328fn rpc_read_config(encoding: Option<&str>, min_context_slot: Option<u64>) -> Value {
2329 let mut config = json!({ "commitment": "confirmed" });
2330 let object = config
2331 .as_object_mut()
2332 .expect("RPC read config is always an object");
2333 if let Some(encoding) = encoding {
2334 object.insert("encoding".to_string(), json!(encoding));
2335 }
2336 if let Some(min_context_slot) = min_context_slot {
2337 object.insert("minContextSlot".to_string(), json!(min_context_slot));
2338 }
2339 config
2340}
2341
2342async fn rpc_get_native_balance(
2343 client: &ReadRpcClient,
2344 rpc_url: &str,
2345 address: &str,
2346 min_context_slot: Option<u64>,
2347) -> anyhow::Result<Value> {
2348 let result = rpc_call(
2349 client,
2350 rpc_url,
2351 "getBalance",
2352 json!([address, rpc_read_config(None, min_context_slot)]),
2353 )
2354 .await?;
2355 contextual_native_balance_json(&result)
2356}
2357
2358fn contextual_native_balance_json(result: &Value) -> anyhow::Result<Value> {
2359 let lamports = result
2360 .get("value")
2361 .and_then(Value::as_u64)
2362 .ok_or_else(|| anyhow::anyhow!("getBalance response is missing a u64 value"))?;
2363 let context_slot = result
2364 .pointer("/context/slot")
2365 .and_then(Value::as_u64)
2366 .ok_or_else(|| anyhow::anyhow!("getBalance response is missing a u64 context slot"))?;
2367 Ok(json!({
2368 "lamports": lamports.to_string(),
2369 "contextSlot": context_slot.to_string(),
2370 }))
2371}
2372
2373async fn rpc_get_account_info(
2374 client: &ReadRpcClient,
2375 rpc_url: &str,
2376 address: &str,
2377) -> anyhow::Result<Value> {
2378 let result = rpc_call(
2379 client,
2380 rpc_url,
2381 "getAccountInfo",
2382 json!([address, { "encoding": "base64", "commitment": "confirmed" }]),
2383 )
2384 .await?;
2385 Ok(result.get("value").cloned().unwrap_or(Value::Null))
2386}
2387
2388async fn rpc_get_multiple_accounts(
2389 client: &ReadRpcClient,
2390 rpc_url: &str,
2391 addresses: &[String],
2392) -> anyhow::Result<Vec<Value>> {
2393 let result = rpc_call(
2394 client,
2395 rpc_url,
2396 "getMultipleAccounts",
2397 json!([addresses, { "encoding": "base64", "commitment": "confirmed" }]),
2398 )
2399 .await?;
2400 Ok(result
2401 .get("value")
2402 .and_then(Value::as_array)
2403 .cloned()
2404 .unwrap_or_default())
2405}
2406
2407async fn rpc_get_parsed_account_info(
2408 client: &ReadRpcClient,
2409 rpc_url: &str,
2410 address: &str,
2411) -> anyhow::Result<Option<Value>> {
2412 let result = rpc_call(
2413 client,
2414 rpc_url,
2415 "getAccountInfo",
2416 json!([address, { "encoding": "jsonParsed", "commitment": "confirmed" }]),
2417 )
2418 .await?;
2419 Ok(result
2420 .get("value")
2421 .cloned()
2422 .filter(|value| !value.is_null()))
2423}
2424
2425async fn rpc_get_token_balance(
2426 client: &ReadRpcClient,
2427 rpc_url: &str,
2428 owner: &str,
2429 mint: &str,
2430 token_program: Option<&str>,
2431 min_context_slot: Option<u64>,
2432) -> anyhow::Result<Value> {
2433 let filter = token_program
2434 .map(|program_id| json!({ "programId": program_id }))
2435 .unwrap_or_else(|| json!({ "mint": mint }));
2436 let result = rpc_call(
2437 client,
2438 rpc_url,
2439 "getTokenAccountsByOwner",
2440 json!([
2441 owner,
2442 filter,
2443 rpc_read_config(Some("jsonParsed"), min_context_slot)
2444 ]),
2445 )
2446 .await?;
2447
2448 contextual_token_balance_json(&result, owner, mint, token_program)
2449}
2450
2451fn contextual_token_balance_json(
2452 result: &Value,
2453 owner: &str,
2454 mint: &str,
2455 token_program: Option<&str>,
2456) -> anyhow::Result<Value> {
2457 let context_slot = result
2458 .pointer("/context/slot")
2459 .and_then(Value::as_u64)
2460 .ok_or_else(|| {
2461 anyhow::anyhow!("getTokenAccountsByOwner response is missing a u64 context slot")
2462 })?;
2463
2464 let account = result
2465 .get("value")
2466 .and_then(Value::as_array)
2467 .and_then(|items| {
2468 items.iter().find(|item| {
2469 item.pointer("/account/data/parsed/info/mint")
2470 .and_then(Value::as_str)
2471 == Some(mint)
2472 })
2473 })
2474 .cloned();
2475
2476 if let Some(account) = account {
2477 let pubkey = account.get("pubkey").and_then(Value::as_str);
2478 let info = account.pointer("/account/data/parsed/info");
2479 return Ok(json!({
2480 "exists": true,
2481 "address": pubkey,
2482 "owner": owner,
2483 "mint": mint,
2484 "tokenProgram": token_program,
2485 "amount": info.and_then(|value| value.pointer("/tokenAmount/amount")).and_then(Value::as_str).unwrap_or("0"),
2486 "decimals": info.and_then(|value| value.pointer("/tokenAmount/decimals")).and_then(Value::as_u64),
2487 "uiAmountString": info.and_then(|value| value.pointer("/tokenAmount/uiAmountString")).and_then(Value::as_str),
2488 "contextSlot": context_slot.to_string(),
2489 }));
2490 }
2491
2492 Ok(json!({
2493 "exists": false,
2494 "address": Value::Null,
2495 "owner": owner,
2496 "mint": mint,
2497 "tokenProgram": token_program,
2498 "amount": "0",
2499 "decimals": Value::Null,
2500 "uiAmountString": Value::Null,
2501 "contextSlot": context_slot.to_string(),
2502 }))
2503}
2504
2505fn decode_account_bytes(value: &Value) -> Option<Vec<u8>> {
2506 let data = value.get("data")?.as_array()?;
2507 let encoded = data.first()?.as_str()?;
2508 BASE64_STANDARD.decode(encoded).ok()
2509}
2510
2511fn raw_account_json(address: &str, value: &Value) -> Value {
2519 json!({
2520 "address": address,
2521 "ownerProgram": value.get("owner").and_then(Value::as_str).unwrap_or_default(),
2522 "lamports": value
2523 .get("lamports")
2524 .and_then(Value::as_u64)
2525 .unwrap_or(0)
2526 .to_string(),
2527 "executable": value.get("executable").and_then(Value::as_bool).unwrap_or(false),
2528 "data": value
2529 .pointer("/data/0")
2530 .and_then(Value::as_str)
2531 .unwrap_or_default(),
2532 })
2533}
2534
2535pub(crate) fn batch_address_limit(
2543 auth_context: Option<&crate::websocket::auth::AuthContext>,
2544 protocol_max: usize,
2545) -> usize {
2546 auth_context
2547 .and_then(|ctx| ctx.limits.max_http_batch_addresses)
2548 .map(|limit| limit as usize)
2549 .unwrap_or(protocol_max)
2550 .min(protocol_max)
2551}
2552
2553const MAX_CHAIN_BATCH_ADDRESSES: usize = 100;
2555
2556fn batch_accounts_json(addresses: &[String], values: &[Value]) -> Vec<Value> {
2559 addresses
2560 .iter()
2561 .zip(values)
2562 .map(|(address, value)| {
2563 if value.is_null() {
2564 Value::Null
2565 } else {
2566 raw_account_json(address, value)
2567 }
2568 })
2569 .collect()
2570}
2571
2572fn mint_info_json(address: &str, value: &Value) -> Value {
2573 let owner_program = value
2574 .get("owner")
2575 .and_then(Value::as_str)
2576 .unwrap_or_default();
2577 let info = value.pointer("/data/parsed/info");
2578 json!({
2579 "address": address,
2580 "ownerProgram": owner_program,
2581 "decimals": info.and_then(|v| v.get("decimals")).and_then(Value::as_u64),
2582 "supply": info.and_then(|v| v.get("supply")).and_then(Value::as_str),
2583 "mintAuthority": info.and_then(|v| v.get("mintAuthority")).and_then(Value::as_str),
2584 "freezeAuthority": info.and_then(|v| v.get("freezeAuthority")).and_then(Value::as_str),
2585 })
2586}
2587
2588fn token_account_json(address: &str, value: &Value) -> Value {
2589 let owner_program = value
2590 .get("owner")
2591 .and_then(Value::as_str)
2592 .unwrap_or_default();
2593 let info = value.pointer("/data/parsed/info");
2594 json!({
2595 "address": address,
2596 "ownerProgram": owner_program,
2597 "mint": info.and_then(|v| v.get("mint")).and_then(Value::as_str),
2598 "owner": info.and_then(|v| v.get("owner")).and_then(Value::as_str),
2599 "amount": info.and_then(|v| v.pointer("/tokenAmount/amount")).and_then(Value::as_str),
2600 "uiAmountString": info.and_then(|v| v.pointer("/tokenAmount/uiAmountString")).and_then(Value::as_str),
2601 })
2602}
2603
2604#[cfg(test)]
2605mod tests {
2606 use super::*;
2607
2608 #[test]
2609 fn cors_headers_allow_browser_sdk_reads() {
2610 let response = with_cors(
2611 Response::builder()
2612 .status(StatusCode::OK)
2613 .body(Full::new(Bytes::new()))
2614 .unwrap(),
2615 );
2616
2617 assert_eq!(response.headers()[ACCESS_CONTROL_ALLOW_ORIGIN], "*");
2618 assert_eq!(
2619 response.headers()[ACCESS_CONTROL_ALLOW_METHODS],
2620 "GET, POST, OPTIONS"
2621 );
2622 assert_eq!(
2623 response.headers()[ACCESS_CONTROL_ALLOW_HEADERS],
2624 "Authorization, Content-Type"
2625 );
2626 }
2627
2628 #[test]
2629 fn native_balance_serializes_u64_values_as_decimal_strings() {
2630 let value = contextual_native_balance_json(&json!({
2631 "context": { "slot": 9_007_199_254_740_995_u64 },
2632 "value": 9_007_199_254_740_993_u64,
2633 }))
2634 .unwrap();
2635
2636 assert_eq!(value["lamports"], "9007199254740993");
2637 assert_eq!(value["contextSlot"], "9007199254740995");
2638 }
2639
2640 #[test]
2643 fn batch_accounts_keep_position_when_an_account_is_absent() {
2644 let addresses = vec!["a".to_string(), "b".to_string(), "c".to_string()];
2645 let values = vec![
2646 json!({ "owner": "prog", "lamports": 7u64, "executable": false, "data": ["AQI=", "base64"] }),
2647 Value::Null,
2648 json!({ "owner": "prog", "lamports": 9u64, "executable": false, "data": ["AwQ=", "base64"] }),
2649 ];
2650
2651 let items = batch_accounts_json(&addresses, &values);
2652
2653 assert_eq!(items.len(), 3);
2654 assert_eq!(items[0]["address"], "a");
2655 assert_eq!(items[0]["lamports"], "7");
2656 assert_eq!(items[0]["data"], "AQI=");
2657 assert_eq!(items[1], Value::Null, "absent account keeps its slot");
2658 assert_eq!(items[2]["address"], "c");
2659 assert_eq!(items[2]["lamports"], "9");
2660 }
2661
2662 #[test]
2666 fn batch_accounts_serialize_lamports_as_decimal_strings() {
2667 let addresses = vec!["big".to_string()];
2668 let values = vec![json!({
2669 "owner": "prog",
2670 "lamports": 9_007_199_254_740_993_u64,
2671 "executable": false,
2672 "data": ["AQI=", "base64"],
2673 })];
2674
2675 let items = batch_accounts_json(&addresses, &values);
2676
2677 assert_eq!(items[0]["lamports"], "9007199254740993");
2678 }
2679
2680 fn auth_context_allowing(
2681 max_batch_addresses: Option<u32>,
2682 ) -> crate::websocket::auth::AuthContext {
2683 crate::websocket::auth::AuthContext {
2684 subject: "test".to_string(),
2685 issuer: "test-issuer".to_string(),
2686 audience: "test-audience".to_string(),
2687 key_class: arete_auth::KeyClass::Publishable,
2688 metering_key: "meter-test".to_string(),
2689 deployment_id: None,
2690 target_kind: None,
2691 target_id: None,
2692 program_id: None,
2693 program_release_hash: None,
2694 expires_at: u64::MAX,
2695 scope: "read".to_string(),
2696 limits: arete_auth::Limits {
2697 max_http_batch_addresses: max_batch_addresses,
2698 ..Default::default()
2699 },
2700 plan: None,
2701 origin: None,
2702 client_ip: None,
2703 jti: "test-jti".to_string(),
2704 actor_key: None,
2705 account_key: None,
2706 consumer_key: None,
2707 policy_version: None,
2708 account_limits: arete_auth::Limits::default(),
2709 row_scopes: None,
2710 }
2711 }
2712
2713 #[test]
2717 fn a_session_ceiling_bounds_the_batch_below_the_protocol_limit() {
2718 let context = auth_context_allowing(Some(10));
2719
2720 assert_eq!(
2721 batch_address_limit(Some(&context), MAX_CHAIN_BATCH_ADDRESSES),
2722 10
2723 );
2724 }
2725
2726 #[test]
2728 fn a_session_ceiling_cannot_exceed_the_protocol_limit() {
2729 let context = auth_context_allowing(Some(1_000));
2730
2731 assert_eq!(
2732 batch_address_limit(Some(&context), MAX_CHAIN_BATCH_ADDRESSES),
2733 MAX_CHAIN_BATCH_ADDRESSES
2734 );
2735 }
2736
2737 #[test]
2739 fn an_absent_ceiling_falls_back_to_the_protocol_limit() {
2740 let context = auth_context_allowing(None);
2741
2742 assert_eq!(
2743 batch_address_limit(Some(&context), MAX_CHAIN_BATCH_ADDRESSES),
2744 MAX_CHAIN_BATCH_ADDRESSES
2745 );
2746 assert_eq!(
2747 batch_address_limit(None, MAX_PROGRAM_BATCH_ADDRESSES),
2748 MAX_PROGRAM_BATCH_ADDRESSES
2749 );
2750 }
2751
2752 #[test]
2753 fn batch_accounts_json_is_empty_for_no_addresses() {
2754 assert!(batch_accounts_json(&[], &[]).is_empty());
2755 }
2756
2757 #[test]
2760 fn batch_item_matches_the_single_address_shape() {
2761 let value = json!({ "owner": "prog", "lamports": 5u64, "executable": true, "data": ["BQY=", "base64"] });
2762 let addresses = vec!["solo".to_string()];
2763
2764 assert_eq!(
2765 batch_accounts_json(&addresses, std::slice::from_ref(&value))[0],
2766 raw_account_json("solo", &value)
2767 );
2768 }
2769
2770 #[test]
2771 fn rpc_read_config_propagates_min_context_slot() {
2772 let config = rpc_read_config(Some("jsonParsed"), Some(9_007_199_254_740_997));
2773
2774 assert_eq!(config["commitment"], "confirmed");
2775 assert_eq!(config["encoding"], "jsonParsed");
2776 assert_eq!(config["minContextSlot"], 9_007_199_254_740_997_u64);
2777 }
2778
2779 #[test]
2780 fn token_balance_preserves_raw_amount_and_stringifies_context_slot() {
2781 let value = contextual_token_balance_json(
2782 &json!({
2783 "context": { "slot": 9_007_199_254_740_995_u64 },
2784 "value": [{
2785 "pubkey": "token-account",
2786 "account": {
2787 "data": {
2788 "parsed": {
2789 "info": {
2790 "mint": "mint",
2791 "tokenAmount": {
2792 "amount": "18446744073709551615",
2793 "decimals": 9,
2794 "uiAmountString": "18446744073.709551615"
2795 }
2796 }
2797 }
2798 }
2799 }
2800 }]
2801 }),
2802 "owner",
2803 "mint",
2804 None,
2805 )
2806 .unwrap();
2807
2808 assert_eq!(value["amount"], "18446744073709551615");
2809 assert_eq!(value["contextSlot"], "9007199254740995");
2810 }
2811
2812 #[test]
2813 fn balance_bodies_accept_decimal_string_min_context_slots() {
2814 let native: NativeBalanceBody = serde_json::from_value(json!({
2815 "address": "owner",
2816 "minContextSlot": "9007199254740997",
2817 }))
2818 .unwrap();
2819 let token: BalanceBody = serde_json::from_value(json!({
2820 "owner": "owner",
2821 "mint": "mint",
2822 "minContextSlot": "9007199254740999",
2823 }))
2824 .unwrap();
2825
2826 assert_eq!(
2827 parse_min_context_slot(native.min_context_slot.as_deref()).unwrap(),
2828 Some(9_007_199_254_740_997)
2829 );
2830 assert_eq!(
2831 parse_min_context_slot(token.min_context_slot.as_deref()).unwrap(),
2832 Some(9_007_199_254_740_999)
2833 );
2834 }
2835
2836 fn runtime_definition(reader: crate::ProgramAccountReaderFn) -> ProgramRuntimeDefinition {
2837 let program_spec_hash = crate::ProgramSpecHash::from_digest([1; 32]);
2838 let idl_content_hash = crate::IdlContentHash::from_digest([2; 32]);
2839 let normalized_idl_hash = crate::NormalizedIdlHash::from_digest([3; 32]);
2840 let program_release_hash = arete_hash::OssGeneratedProgramReleaseV1::new(
2841 "Program111",
2842 program_spec_hash,
2843 idl_content_hash,
2844 normalized_idl_hash,
2845 )
2846 .hash()
2847 .unwrap();
2848 ProgramRuntimeDefinition {
2849 program_id: "Program111".to_string(),
2850 program_spec_hash,
2851 idl_content_hash,
2852 normalized_idl_hash,
2853 program_release_hash,
2854 account_reader: reader,
2855 }
2856 }
2857
2858 fn program_read_context(
2859 target_id: &str,
2860 program_id: &str,
2861 release_hash: &str,
2862 ) -> crate::websocket::auth::AuthContext {
2863 crate::websocket::auth::AuthContext::from_claims(
2864 arete_auth::SessionClaims::program_read_builder(
2865 "issuer",
2866 "user:1",
2867 target_id,
2868 program_id,
2869 release_hash,
2870 )
2871 .build(),
2872 )
2873 }
2874
2875 #[test]
2876 fn program_read_auth_is_bound_to_target_program_and_release() {
2877 let definition = runtime_definition(Arc::new(|_, _| Ok(Value::Null)));
2878 let release_hash = definition.program_release_hash.to_string();
2879 let valid = program_read_context("binding-1", &definition.program_id, &release_hash);
2880
2881 assert_eq!(
2882 authorize_program_read(Some(&valid), Some("binding-1"), &definition),
2883 Ok(())
2884 );
2885
2886 let wrong_target = program_read_context("binding-2", &definition.program_id, &release_hash);
2887 let wrong_program = program_read_context("binding-1", "Program222", &release_hash);
2888 let wrong_release = program_read_context(
2889 "binding-1",
2890 &definition.program_id,
2891 "arete:h1:program-release:sha256:different",
2892 );
2893
2894 for context in [&wrong_target, &wrong_program, &wrong_release] {
2895 assert_eq!(
2896 authorize_program_read(Some(context), Some("binding-1"), &definition),
2897 Err(ProgramReadError::Unauthorized)
2898 );
2899 }
2900 assert_eq!(
2901 authorize_program_read(Some(&valid), None, &definition),
2902 Err(ProgramReadError::AuthorizationNotConfigured)
2903 );
2904 assert_eq!(authorize_program_read(None, None, &definition), Ok(()));
2905 }
2906
2907 #[test]
2908 fn release_routes_are_exact_and_legacy_program_routes_are_not_accepted() {
2909 let hash = crate::ProgramReleaseHash::from_digest([4; 32]);
2910 let fetch_path = format!("/v1/releases/{hash}/accounts/Vault/address");
2911 let exists_path = format!("{fetch_path}/exists");
2912 let batch_path = format!("/v1/releases/{hash}/accounts/Vault");
2913
2914 assert!(matches!(
2915 ProgramReadRoute::parse(&Method::GET, &fetch_path)
2916 .unwrap()
2917 .operation,
2918 ProgramReadOperation::Fetch { .. }
2919 ));
2920 assert!(matches!(
2921 ProgramReadRoute::parse(&Method::GET, &exists_path)
2922 .unwrap()
2923 .operation,
2924 ProgramReadOperation::Exists { .. }
2925 ));
2926 assert!(matches!(
2927 ProgramReadRoute::parse(&Method::POST, &batch_path)
2928 .unwrap()
2929 .operation,
2930 ProgramReadOperation::Batch
2931 ));
2932 assert_eq!(
2933 ProgramReadRoute::parse(&Method::GET, "/programs/demo/accounts/Vault/address")
2934 .unwrap_err(),
2935 ProgramReadError::NotFound
2936 );
2937 }
2938
2939 #[tokio::test]
2940 async fn owner_mismatch_is_rejected_before_decoder_execution() {
2941 use std::sync::atomic::{AtomicBool, Ordering};
2942
2943 let called = Arc::new(AtomicBool::new(false));
2944 let called_by_reader = called.clone();
2945 let definition = runtime_definition(Arc::new(move |_, _| {
2946 called_by_reader.store(true, Ordering::SeqCst);
2947 Ok(json!({ "decoded": true }))
2948 }));
2949 let value = json!({
2950 "owner": "DifferentProgram",
2951 "data": [BASE64_STANDARD.encode([1, 2, 3]), "base64"]
2952 });
2953
2954 assert!(matches!(
2955 decode_release_account(&definition, "Vault", &value).await,
2956 AccountReadOutcome::Error(ProgramReadError::AccountOwnerMismatch)
2957 ));
2958 assert!(!called.load(Ordering::SeqCst));
2959 }
2960
2961 #[tokio::test]
2962 async fn decode_failures_remain_typed_errors_and_metadata_is_public_only() {
2963 let definition = runtime_definition(Arc::new(|_, _| anyhow::bail!("private diagnostic")));
2964 let value = json!({
2965 "owner": "Program111",
2966 "data": [BASE64_STANDARD.encode([1, 2, 3]), "base64"]
2967 });
2968 assert!(matches!(
2969 decode_release_account(&definition, "Vault", &value).await,
2970 AccountReadOutcome::Error(ProgramReadError::AccountDecodeFailed)
2971 ));
2972
2973 let response = with_release_metadata(
2974 program_read_error_response(ProgramReadError::AccountDecodeFailed),
2975 &definition,
2976 Some("address"),
2977 Some(true),
2978 );
2979 assert_eq!(response.headers()["X-Error-Code"], "ACCOUNT_DECODE_FAILED");
2980 assert_eq!(
2981 response.headers()["X-Arete-Program-Release-Hash"],
2982 definition.program_release_hash.to_string()
2983 );
2984 assert_eq!(
2985 response.headers()["X-Arete-Idl-Content-Hash"],
2986 definition.idl_content_hash.to_string()
2987 );
2988 let body = response.into_body().collect().await.unwrap().to_bytes();
2989 let body = std::str::from_utf8(&body).unwrap();
2990 assert_eq!(body, r#"{"error":{"code":"ACCOUNT_DECODE_FAILED"}}"#);
2991 assert!(!body.contains("private diagnostic"));
2992 assert!(!body.contains("decoder"));
2993 }
2994
2995 struct ScopeTokenPlugin;
2997
2998 #[async_trait::async_trait]
2999 impl WebSocketAuthPlugin for ScopeTokenPlugin {
3000 async fn authorize(&self, request: &ConnectionAuthRequest) -> AuthDecision {
3001 let Some(scope) = request.bearer_token() else {
3002 return AuthDecision::Deny(AuthDeny::token_missing());
3003 };
3004 let mut claims =
3005 arete_auth::SessionClaims::solana_gateway_builder("issuer", "user:1", "gateway-1")
3006 .with_scope(scope.replace('+', " "))
3007 .with_metering_key("account:1")
3008 .build();
3009 if scope.contains("limited") {
3010 claims.scope = SCOPE_FORK_READ.to_string();
3011 claims.limits = Some(arete_auth::Limits {
3012 max_http_requests_per_minute: Some(3),
3013 ..Default::default()
3014 });
3015 }
3016 if scope.contains("narrow") {
3017 claims.scope = SCOPE_FORK_READ.to_string();
3018 claims.limits = Some(arete_auth::Limits {
3019 max_http_batch_addresses: Some(2),
3020 ..Default::default()
3021 });
3022 }
3023 AuthDecision::Allow(crate::websocket::auth::AuthContext::from_claims(claims))
3024 }
3025
3026 fn as_any(&self) -> &dyn std::any::Any {
3027 self
3028 }
3029 }
3030
3031 async fn start_gateway(rpc_url: &str) -> (String, CancellationToken) {
3032 start_gateway_with_limits(rpc_url, HttpLimitsConfig::default()).await
3033 }
3034
3035 async fn start_gateway_with_limits(
3036 rpc_url: &str,
3037 limits: HttpLimitsConfig,
3038 ) -> (String, CancellationToken) {
3039 let port = std::net::TcpListener::bind("127.0.0.1:0")
3040 .unwrap()
3041 .local_addr()
3042 .unwrap()
3043 .port();
3044 let addr: SocketAddr = format!("127.0.0.1:{port}").parse().unwrap();
3045 let shutdown = CancellationToken::new();
3046 let server = HttpHealthServer::new(addr)
3047 .with_runtime_plan(RuntimePlan::solana_gateway())
3048 .with_auth_plugin(Arc::new(ScopeTokenPlugin))
3049 .with_solana_gateway_target("gateway-1")
3050 .with_read_rpc_url(rpc_url)
3051 .with_limits(limits)
3052 .with_shutdown(shutdown.clone());
3053 tokio::spawn(server.start());
3054 for _ in 0..100 {
3055 if tokio::net::TcpStream::connect(addr).await.is_ok() {
3056 break;
3057 }
3058 tokio::time::sleep(Duration::from_millis(10)).await;
3059 }
3060 (format!("http://{addr}"), shutdown)
3061 }
3062
3063 #[tokio::test]
3064 async fn the_fork_route_requires_the_exact_fork_read_scope() {
3065 let rpc = crate::http::fork::tests::mock_rpc(crate::http::fork::tests::result_responder(
3066 json!(77),
3067 ))
3068 .await;
3069 let (base, shutdown) = start_gateway(&rpc.url).await;
3070 let client = Client::new();
3071 let body = json!({ "jsonrpc": "2.0", "id": 1, "method": "getSlot" });
3072 let post = |token: Option<&str>| {
3073 let mut request = client.post(format!("{base}/rpc/v1/fork")).json(&body);
3074 if let Some(token) = token {
3075 request = request.bearer_auth(token);
3076 }
3077 request.send()
3078 };
3079
3080 let missing = post(None).await.unwrap();
3081 assert_eq!(missing.status(), StatusCode::UNAUTHORIZED);
3082
3083 for scope in [
3084 "read",
3085 "read+transaction:inspect+transaction:send",
3086 "fork:read-extra",
3087 ] {
3088 let denied = post(Some(scope)).await.unwrap();
3089 assert_eq!(denied.status(), StatusCode::FORBIDDEN, "{scope}");
3090 let value: Value = denied.json().await.unwrap();
3091 assert_eq!(value["code"], "insufficient_scope");
3092 }
3093 assert!(rpc.requests.lock().unwrap().is_empty());
3094
3095 let allowed = post(Some("fork:read")).await.unwrap();
3096 assert_eq!(allowed.status(), StatusCode::OK);
3097 let value: Value = allowed.json().await.unwrap();
3098 assert_eq!(value, json!({ "jsonrpc": "2.0", "id": 1, "result": 77 }));
3099
3100 let chain = client
3102 .get(format!("{base}/chain/clock"))
3103 .bearer_auth("fork:read")
3104 .send()
3105 .await
3106 .unwrap();
3107 assert_eq!(chain.status(), StatusCode::FORBIDDEN);
3108
3109 let get = client
3110 .get(format!("{base}/rpc/v1/fork"))
3111 .bearer_auth("fork:read")
3112 .send()
3113 .await
3114 .unwrap();
3115 assert_eq!(get.status(), StatusCode::METHOD_NOT_ALLOWED);
3116 shutdown.cancel();
3117 }
3118
3119 #[tokio::test]
3120 async fn a_fork_batch_is_charged_per_entry() {
3121 let rpc = crate::http::fork::tests::mock_rpc(crate::http::fork::tests::result_responder(
3122 json!(1),
3123 ))
3124 .await;
3125 let (base, shutdown) = start_gateway(&rpc.url).await;
3126 let client = Client::new();
3127 let call = |id: u64| json!({ "jsonrpc": "2.0", "id": id, "method": "getSlot" });
3128 let batch = json!([call(1), call(2), call(3), call(4)]);
3129
3130 let refused = client
3131 .post(format!("{base}/rpc/v1/fork"))
3132 .bearer_auth("limited")
3133 .json(&batch)
3134 .send()
3135 .await
3136 .unwrap();
3137 assert_eq!(refused.status(), StatusCode::TOO_MANY_REQUESTS);
3138 assert!(rpc.requests.lock().unwrap().is_empty());
3139
3140 let within = client
3141 .post(format!("{base}/rpc/v1/fork"))
3142 .bearer_auth("limited")
3143 .json(&json!([call(1), call(2)]))
3144 .send()
3145 .await
3146 .unwrap();
3147 assert_eq!(within.status(), StatusCode::OK);
3148 assert_eq!(rpc.requests.lock().unwrap().len(), 2);
3149 shutdown.cancel();
3150 }
3151
3152 #[tokio::test]
3156 async fn a_fork_multiple_accounts_call_is_bounded_by_the_session_ceiling() {
3157 let rpc = crate::http::fork::tests::mock_rpc(crate::http::fork::tests::result_responder(
3158 json!({ "context": { "slot": 1 }, "value": [null, null] }),
3159 ))
3160 .await;
3161 let (base, shutdown) = start_gateway(&rpc.url).await;
3162 let client = Client::new();
3163 let key = "11111111111111111111111111111111";
3164 let read = |count: usize| {
3165 json!({
3166 "jsonrpc": "2.0",
3167 "id": 1,
3168 "method": "getMultipleAccounts",
3169 "params": [vec![key; count]],
3170 })
3171 };
3172
3173 let refused: Value = client
3174 .post(format!("{base}/rpc/v1/fork"))
3175 .bearer_auth("narrow")
3176 .json(&read(3))
3177 .send()
3178 .await
3179 .unwrap()
3180 .json()
3181 .await
3182 .unwrap();
3183 assert_eq!(refused["error"]["code"], -32602);
3184 assert!(rpc.requests.lock().unwrap().is_empty());
3185
3186 let within: Value = client
3187 .post(format!("{base}/rpc/v1/fork"))
3188 .bearer_auth("narrow")
3189 .json(&read(2))
3190 .send()
3191 .await
3192 .unwrap()
3193 .json()
3194 .await
3195 .unwrap();
3196 assert!(within.get("result").is_some(), "{within}");
3197 assert_eq!(rpc.requests.lock().unwrap().len(), 1);
3198 shutdown.cancel();
3199 }
3200
3201 #[tokio::test]
3206 async fn a_stalled_transaction_body_holds_no_admission_permit() {
3207 use tokio::io::{AsyncReadExt, AsyncWriteExt};
3208
3209 let port = std::net::TcpListener::bind("127.0.0.1:0")
3210 .unwrap()
3211 .local_addr()
3212 .unwrap()
3213 .port();
3214 let addr: SocketAddr = format!("127.0.0.1:{port}").parse().unwrap();
3215 let shutdown = CancellationToken::new();
3216 let server = HttpHealthServer::new(addr)
3217 .with_transaction_config(TransactionConfig {
3218 enabled: true,
3219 rpc_url: Some("http://127.0.0.1:9".to_string()),
3220 allow_unauthenticated: true,
3221 inspect_concurrency: 1,
3222 send_concurrency: 1,
3223 ..TransactionConfig::default()
3224 })
3225 .with_limits(HttpLimitsConfig {
3226 body_read_timeout: Duration::from_millis(500),
3227 ..HttpLimitsConfig::default()
3228 })
3229 .with_shutdown(shutdown.clone());
3230 tokio::spawn(server.start());
3231 let mut stalled = loop {
3232 if let Ok(stream) = tokio::net::TcpStream::connect(addr).await {
3233 break stream;
3234 }
3235 tokio::time::sleep(Duration::from_millis(10)).await;
3236 };
3237 stalled
3238 .write_all(
3239 b"POST /transactions/v1/fee HTTP/1.1\r\nHost: test\r\nContent-Type: application/json\r\nContent-Length: 64\r\n\r\n{",
3240 )
3241 .await
3242 .unwrap();
3243 tokio::time::sleep(Duration::from_millis(100)).await;
3244
3245 let response = Client::new()
3246 .post(format!("http://{addr}/transactions/v1/fee"))
3247 .header("Content-Type", "application/json")
3248 .body("not json")
3249 .send()
3250 .await
3251 .unwrap();
3252 assert_eq!(response.status(), StatusCode::BAD_REQUEST);
3253
3254 let mut answer = Vec::new();
3255 tokio::time::timeout(Duration::from_secs(5), async {
3256 let mut buffer = [0; 1024];
3257 while !String::from_utf8_lossy(&answer).contains("\r\n\r\n") {
3258 let read = stalled.read(&mut buffer).await.unwrap();
3259 assert_ne!(read, 0, "the stalled request gets an answer");
3260 answer.extend_from_slice(&buffer[..read]);
3261 }
3262 })
3263 .await
3264 .expect("the stalled body is cut off at the deadline");
3265 assert!(
3266 String::from_utf8_lossy(&answer).starts_with("HTTP/1.1 408"),
3267 "{}",
3268 String::from_utf8_lossy(&answer)
3269 );
3270 shutdown.cancel();
3271 }
3272
3273 #[tokio::test]
3276 async fn finished_http_connections_are_reaped_while_serving() {
3277 use std::sync::atomic::{AtomicUsize, Ordering};
3278
3279 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
3280 let addr = listener.local_addr().unwrap();
3281 let shutdown = CancellationToken::new();
3282 let served = Arc::new(AtomicUsize::new(0));
3283 let accept = {
3284 let shutdown = shutdown.clone();
3285 let served = served.clone();
3286 tokio::spawn(async move {
3287 accept_connections(&listener, &shutdown, |_stream, _addr| {
3288 let served = served.clone();
3289 async move {
3290 served.fetch_add(1, Ordering::SeqCst);
3291 }
3292 })
3293 .await
3294 })
3295 };
3296
3297 for _ in 0..20 {
3298 drop(tokio::net::TcpStream::connect(addr).await.unwrap());
3299 }
3300 tokio::time::timeout(Duration::from_secs(5), async {
3301 while served.load(Ordering::SeqCst) < 20 {
3302 tokio::time::sleep(Duration::from_millis(10)).await;
3303 }
3304 })
3305 .await
3306 .expect("every connection is served");
3307 tokio::time::sleep(Duration::from_millis(50)).await;
3308 shutdown.cancel();
3309
3310 let connections = accept.await.unwrap();
3311 assert_eq!(connections.len(), 0);
3312 }
3313
3314 fn account_session(
3315 consumer: &str,
3316 account_limit: Option<u32>,
3317 ) -> crate::websocket::auth::AuthContext {
3318 crate::websocket::auth::AuthContext {
3319 subject: consumer.to_string(),
3320 metering_key: consumer.to_string(),
3321 limits: arete_auth::Limits {
3322 max_http_requests_per_minute: Some(100),
3323 ..Default::default()
3324 },
3325 actor_key: Some("user:1".to_string()),
3326 account_key: Some("account:1".to_string()),
3327 consumer_key: Some(consumer.to_string()),
3328 policy_version: Some(1),
3329 account_limits: arete_auth::Limits {
3330 max_http_requests_per_minute: account_limit,
3331 ..Default::default()
3332 },
3333 ..auth_context_allowing(None)
3334 }
3335 }
3336
3337 #[test]
3340 fn http_rate_buckets_of_earlier_minutes_are_swept() {
3341 let limit_state = HttpLimitState::default();
3342 limit_state
3343 .per_subject_per_minute
3344 .insert("old-session".into(), (10, 3));
3345 limit_state
3346 .per_account_per_minute
3347 .insert("old-account".into(), (10, 3));
3348 limit_state
3349 .per_account_per_minute
3350 .insert("current-account".into(), (11, 1));
3351
3352 limit_state.sweep(11);
3353
3354 assert!(limit_state.per_subject_per_minute.is_empty());
3355 assert_eq!(limit_state.per_account_per_minute.len(), 1);
3356 assert!(limit_state
3357 .per_account_per_minute
3358 .contains_key("current-account"));
3359 }
3360
3361 #[test]
3365 fn http_reads_count_against_the_account_quota() {
3366 let limit_state = HttpLimitState::default();
3367 let first = account_session("consumer:a", Some(3));
3368 let second = account_session("consumer:b", Some(3));
3369
3370 enforce_http_limits_weighted(&first, &limit_state, 2).unwrap();
3371 enforce_http_limits_weighted(&second, &limit_state, 1).unwrap();
3372 let deny = enforce_http_limits_weighted(&second, &limit_state, 1).unwrap_err();
3373 assert_eq!(
3374 deny.code,
3375 crate::websocket::auth::AuthErrorCode::RateLimitExceeded
3376 );
3377 let session = limit_state
3378 .per_subject_per_minute
3379 .get("consumer:b:consumer:b")
3380 .unwrap()
3381 .1;
3382 assert_eq!(session, 1, "the refused read is refunded");
3383
3384 let unclaimed = HttpLimitState::default();
3386 let open = account_session("consumer:a", None);
3387 enforce_http_limits_weighted(&open, &unclaimed, 50).unwrap();
3388 assert!(unclaimed.per_account_per_minute.is_empty());
3389 }
3390
3391 #[tokio::test]
3392 async fn chain_read_upstream_failures_never_reveal_the_rpc_url() {
3393 type Responder = crate::http::fork::tests::Responder;
3394 let leaky: [(&str, Responder); 3] = [
3395 (
3396 "http status",
3397 Arc::new(|_| (429, "rate limited at /secret-token".into())),
3398 ),
3399 (
3400 "json-rpc error",
3401 Arc::new(|_| {
3402 (
3403 200,
3404 json!({
3405 "jsonrpc": "2.0",
3406 "id": "arete-read",
3407 "error": { "code": -32005, "message": "see /secret-token" }
3408 })
3409 .to_string(),
3410 )
3411 }),
3412 ),
3413 (
3414 "malformed body",
3415 Arc::new(|_| (200, "<html>/secret-token</html>".into())),
3416 ),
3417 ];
3418 let unreachable = {
3419 let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
3420 let url = format!("http://{}", listener.local_addr().unwrap());
3421 drop(listener);
3422 url
3423 };
3424 let mut upstreams = Vec::new();
3425 for (label, responder) in leaky {
3426 let rpc = crate::http::fork::tests::mock_rpc(responder).await;
3427 upstreams.push((label, rpc.url));
3428 }
3429 upstreams.push(("unreachable", unreachable));
3430
3431 let client = Client::new();
3432 for (label, upstream) in upstreams {
3433 let rpc_url = format!("{upstream}/secret-token?api-key=hunter2");
3434 let port = upstream.rsplit(':').next().unwrap().to_string();
3435 let (base, shutdown) = start_gateway(&rpc_url).await;
3436 for request in [
3437 client.get(format!("{base}/chain/clock")),
3438 client.get(format!("{base}/chain/exists/{}", "1".repeat(32))),
3439 client
3440 .post(format!("{base}/chain/v1/account"))
3441 .json(&json!({ "address": "11111111111111111111111111111111" })),
3442 ] {
3443 let response = request.bearer_auth("read").send().await.unwrap();
3444 assert_eq!(response.status(), StatusCode::BAD_GATEWAY, "{label}");
3445 let correlation_id = response.headers()[CORRELATION_ID_HEADER]
3446 .to_str()
3447 .unwrap()
3448 .to_string();
3449 let text = response.text().await.unwrap();
3450 for secret in ["secret-token", "hunter2", "api-key", "127.0.0.1", &port] {
3451 assert!(!text.contains(secret), "{label}: {secret} in {text}");
3452 }
3453 let value: Value = serde_json::from_str(&text).unwrap();
3454 assert_eq!(value["error"], UPSTREAM_RPC_FAILED, "{label}");
3455 assert!(text.contains(&correlation_id), "{label}");
3456 }
3457 shutdown.cancel();
3458 }
3459 }
3460
3461 fn small_limits() -> HttpLimitsConfig {
3462 HttpLimitsConfig {
3463 max_body_bytes: 1024,
3464 header_read_timeout: Duration::from_millis(300),
3465 body_read_timeout: Duration::from_millis(300),
3466 upstream_connect_timeout: Duration::from_millis(300),
3467 upstream_timeout: Duration::from_millis(300),
3468 max_upstream_response_bytes: 1024,
3469 }
3470 }
3471
3472 #[tokio::test]
3473 async fn oversized_request_bodies_are_refused_with_413() {
3474 let rpc = crate::http::fork::tests::mock_rpc(crate::http::fork::tests::result_responder(
3475 json!(1),
3476 ))
3477 .await;
3478 let (base, shutdown) = start_gateway_with_limits(&rpc.url, small_limits()).await;
3479 let client = Client::new();
3480 let padding = " ".repeat(4096);
3481 let oversized = format!(r#"{{"address":"11111111111111111111111111111111"{padding}}}"#);
3482 for path in [
3483 "/chain/v1/account",
3484 "/chain/native-balance",
3485 "/chain/accounts",
3486 ] {
3487 let response = client
3489 .post(format!("{base}{path}"))
3490 .bearer_auth("read")
3491 .header("Content-Type", "application/json")
3492 .body(oversized.clone())
3493 .send()
3494 .await
3495 .unwrap();
3496 assert_eq!(response.status(), StatusCode::PAYLOAD_TOO_LARGE, "{path}");
3497
3498 let chunks = futures_util::stream::iter(
3500 oversized
3501 .as_bytes()
3502 .chunks(512)
3503 .map(|chunk| Ok::<_, std::io::Error>(Bytes::copy_from_slice(chunk)))
3504 .collect::<Vec<_>>(),
3505 );
3506 let response = client
3507 .post(format!("{base}{path}"))
3508 .bearer_auth("read")
3509 .body(reqwest::Body::wrap_stream(chunks))
3510 .send()
3511 .await
3512 .unwrap();
3513 assert_eq!(
3514 response.status(),
3515 StatusCode::PAYLOAD_TOO_LARGE,
3516 "{path} chunked"
3517 );
3518 }
3519 assert!(rpc.requests.lock().unwrap().is_empty());
3520 shutdown.cancel();
3521 }
3522
3523 #[tokio::test]
3524 async fn a_body_that_stalls_is_answered_with_408() {
3525 use tokio::io::{AsyncReadExt, AsyncWriteExt};
3526
3527 let rpc = crate::http::fork::tests::mock_rpc(crate::http::fork::tests::result_responder(
3528 json!(1),
3529 ))
3530 .await;
3531 let (base, shutdown) = start_gateway_with_limits(&rpc.url, small_limits()).await;
3532 let addr = base.trim_start_matches("http://");
3533 let mut stream = tokio::net::TcpStream::connect(addr).await.unwrap();
3534 stream
3535 .write_all(
3536 format!(
3537 "POST /chain/v1/account HTTP/1.1\r\nHost: {addr}\r\nAuthorization: Bearer read\r\nContent-Type: application/json\r\nContent-Length: 200\r\n\r\n{{\"addr"
3538 )
3539 .as_bytes(),
3540 )
3541 .await
3542 .unwrap();
3543 let mut response = vec![0; 1024];
3544 let read = tokio::time::timeout(Duration::from_secs(5), stream.read(&mut response))
3545 .await
3546 .expect("the server answers a stalled body before the test deadline")
3547 .unwrap();
3548 let response = String::from_utf8_lossy(&response[..read]);
3549 assert!(response.starts_with("HTTP/1.1 408"), "{response}");
3550 shutdown.cancel();
3551 }
3552
3553 #[tokio::test]
3554 async fn a_client_that_never_finishes_its_headers_is_disconnected() {
3555 use tokio::io::{AsyncReadExt, AsyncWriteExt};
3556
3557 let (base, shutdown) =
3558 start_gateway_with_limits("http://127.0.0.1:9/unused", small_limits()).await;
3559 let addr = base.trim_start_matches("http://");
3560 let mut stream = tokio::net::TcpStream::connect(addr).await.unwrap();
3561 stream.write_all(b"GET /health HTTP/1.1\r\n").await.unwrap();
3562 let mut buffer = vec![0; 1024];
3563 let closed = tokio::time::timeout(Duration::from_secs(5), async {
3564 loop {
3565 match stream.read(&mut buffer).await {
3566 Ok(0) | Err(_) => break,
3567 Ok(_) => continue,
3568 }
3569 }
3570 })
3571 .await;
3572 assert!(
3573 closed.is_ok(),
3574 "the connection is closed at the header deadline"
3575 );
3576 shutdown.cancel();
3577 }
3578
3579 #[tokio::test]
3580 async fn a_slow_upstream_times_out_as_a_502() {
3581 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
3583 let upstream = format!("http://{}", listener.local_addr().unwrap());
3584 tokio::spawn(async move {
3585 let mut held = Vec::new();
3586 while let Ok((stream, _)) = listener.accept().await {
3587 held.push(stream);
3588 }
3589 });
3590 let (base, shutdown) = start_gateway_with_limits(&upstream, small_limits()).await;
3591 let started = std::time::Instant::now();
3592 let response = Client::new()
3593 .get(format!("{base}/chain/clock"))
3594 .bearer_auth("read")
3595 .send()
3596 .await
3597 .unwrap();
3598 assert_eq!(response.status(), StatusCode::BAD_GATEWAY);
3599 assert!(started.elapsed() < Duration::from_secs(5));
3600 let value: Value = response.json().await.unwrap();
3601 assert_eq!(value["error"], UPSTREAM_RPC_FAILED);
3602 shutdown.cancel();
3603 }
3604
3605 #[tokio::test]
3606 async fn an_oversized_upstream_response_is_refused() {
3607 let rpc = crate::http::fork::tests::mock_rpc(crate::http::fork::tests::result_responder(
3608 json!("x".repeat(4096)),
3609 ))
3610 .await;
3611 let (base, shutdown) = start_gateway_with_limits(&rpc.url, small_limits()).await;
3612 let response = Client::new()
3613 .get(format!("{base}/chain/clock"))
3614 .bearer_auth("read")
3615 .send()
3616 .await
3617 .unwrap();
3618 assert_eq!(response.status(), StatusCode::BAD_GATEWAY);
3619 let value: Value = response.json().await.unwrap();
3620 assert_eq!(value["error"], UPSTREAM_RPC_FAILED);
3621 shutdown.cancel();
3622 }
3623}