Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,6 @@ pub(crate) fn rocket(
.attach(fairings::RequestLogger)
.attach(fairings::UsageLogger)
.attach(fairings::RateLimitHeadersFairing)
.attach(routes::tokens::fairing())
.attach(cors))
}

Expand Down
13 changes: 13 additions & 0 deletions src/raindex/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,19 @@ impl RaindexProvider {
self.registry.registry_url()
}

pub(crate) fn get_remote_token_urls(&self) -> Result<Vec<String>, RaindexProviderError> {
let client = self
.registry
.get_raindex_client()
.map_err(|e| RaindexProviderError::ClientInit(e.to_string()))?;
let urls = client
.get_remote_tokens()
.map_err(|e| RaindexProviderError::ClientInit(e.to_string()))?
.map(|cfg| cfg.urls.into_iter().map(|u| u.to_string()).collect())
.unwrap_or_default();
Ok(urls)
}

pub(crate) async fn run_with_client<T, F, Fut>(&self, f: F) -> Result<T, RaindexProviderError>
where
T: Send + 'static,
Expand Down
4 changes: 2 additions & 2 deletions src/routes/admin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ mod tests {
let client = TestClientBuilder::new().build().await;
let (key_id, secret) = seed_admin_key(&client).await;
let header = basic_auth_header(&key_id, &secret);
let new_url = mock_raindex_registry_url().await;
let new_url = mock_raindex_registry_url(None).await;

let response = client
.put("/admin/registry")
Expand Down Expand Up @@ -189,7 +189,7 @@ mod tests {
let client = TestClientBuilder::new().build().await;
let (key_id, secret) = seed_admin_key(&client).await;
let header = basic_auth_header(&key_id, &secret);
let new_url = mock_raindex_registry_url().await;
let new_url = mock_raindex_registry_url(None).await;

client
.put("/admin/registry")
Expand Down
82 changes: 29 additions & 53 deletions src/routes/tokens.rs
Original file line number Diff line number Diff line change
@@ -1,53 +1,16 @@
use crate::auth::AuthenticatedKey;
use crate::error::{ApiError, ApiErrorResponse};
use crate::fairings::{GlobalRateLimit, TracingSpan};
use crate::raindex::SharedRaindexProvider;
use crate::types::tokens::{RemoteTokenList, TokenInfo, TokenListResponse};
use rocket::fairing::AdHoc;
use rocket::serde::json::Json;
use rocket::{Route, State};
use std::time::Duration;
use tracing::Instrument;

const TOKEN_LIST_URL: &str = "https://raw.githubusercontent.com/S01-Issuer/st0x-tokens/ad1a637a79d5a220ad089aecdc5b7239d3473f6e/src/st0xTokens.json";
const TARGET_CHAIN_ID: u32 = crate::CHAIN_ID;
const TOKEN_LIST_TIMEOUT_SECS: u64 = 10;

pub(crate) struct TokensConfig {
pub(crate) url: String,
pub(crate) client: reqwest::Client,
}

impl Default for TokensConfig {
fn default() -> Self {
Self {
url: TOKEN_LIST_URL.to_string(),
client: reqwest::Client::new(),
}
}
}

impl TokensConfig {
#[cfg(test)]
pub(crate) fn with_url(url: impl Into<String>) -> Self {
Self {
url: url.into(),
client: reqwest::Client::new(),
}
}
}

pub(crate) fn fairing() -> AdHoc {
AdHoc::on_ignite("Tokens Config", |rocket| async {
if rocket.state::<TokensConfig>().is_some() {
tracing::info!("TokensConfig already managed; skipping default initialization");
rocket
} else {
tracing::info!(url = %TOKEN_LIST_URL, "initializing default TokensConfig");
rocket.manage(TokensConfig::default())
}
})
}

#[derive(Debug, thiserror::Error)]
enum TokenError {
#[error("failed to fetch token list: {0}")]
Expand All @@ -56,6 +19,8 @@ enum TokenError {
Deserialize(reqwest::Error),
#[error("token list returned non-200 status: {0}")]
BadStatus(reqwest::StatusCode),
#[error("no token list URL configured in registry")]
NoTokenListUrl,
}

impl From<TokenError> for ApiError {
Expand All @@ -82,16 +47,19 @@ pub async fn get_tokens(
_global: GlobalRateLimit,
_key: AuthenticatedKey,
span: TracingSpan,
tokens_config: &State<TokensConfig>,
shared_raindex: &State<SharedRaindexProvider>,
) -> Result<Json<TokenListResponse>, ApiError> {
let url = tokens_config.url.clone();
let client = tokens_config.client.clone();
let raindex = shared_raindex.read().await;
let urls = raindex.get_remote_token_urls()?;
let url = urls.first().ok_or(TokenError::NoTokenListUrl)?.to_string();
drop(raindex);
Comment on lines +52 to +55
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Ensure request logging happens before fallible registry URL resolution.

Line [52]-Line [55] can fail before the "request received" log at Line [58], so some requests may return without required request-level tracing. Move URL resolution inside the instrumented block after initial request logging.

🔧 Proposed fix
 pub async fn get_tokens(
     _global: GlobalRateLimit,
     _key: AuthenticatedKey,
     span: TracingSpan,
     shared_raindex: &State<SharedRaindexProvider>,
 ) -> Result<Json<TokenListResponse>, ApiError> {
-    let raindex = shared_raindex.read().await;
-    let urls = raindex.get_remote_token_urls()?;
-    let url = urls.first().ok_or(TokenError::NoTokenListUrl)?.to_string();
-    drop(raindex);
-
     async move {
         tracing::info!("request received");
+        let url = {
+            let raindex = shared_raindex.read().await;
+            let urls = raindex.get_remote_token_urls()?;
+            urls.first().cloned().ok_or_else(|| {
+                tracing::warn!("no token list URL configured in registry");
+                TokenError::NoTokenListUrl
+            })?
+        };
 
         tracing::info!(url = %url, timeout_secs = TOKEN_LIST_TIMEOUT_SECS, "fetching token list");
As per coding guidelines, “Every route handler must log appropriately using tracing (request received, errors, key decisions)”.

Also applies to: 57-60

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/routes/tokens.rs` around lines 52 - 55, The registry URL resolution using
shared_raindex.read().await and raindex.get_remote_token_urls() which can return
TokenError::NoTokenListUrl must be moved inside the instrumented/request-scoped
block that logs "request received" so tracing always records the request before
any fallible operations; locate the code referencing shared_raindex and
get_remote_token_urls() in tokens.rs and move it into the block where the
request is instrumented/logged (the area around the "request received" tracing
call), ensure you drop the read guard after use, and keep error handling
(TokenError::NoTokenListUrl) unchanged but after the initial log so
request-level tracing is always emitted.


async move {
tracing::info!("request received");

tracing::info!(url = %url, timeout_secs = TOKEN_LIST_TIMEOUT_SECS, "fetching token list");

let response = client
let response = reqwest::Client::new()
.get(&url)
.timeout(Duration::from_secs(TOKEN_LIST_TIMEOUT_SECS))
.send()
Expand Down Expand Up @@ -139,7 +107,9 @@ pub fn routes() -> Vec<Route> {

#[cfg(test)]
mod tests {
use crate::test_helpers::{basic_auth_header, seed_api_key, TestClientBuilder};
use crate::test_helpers::{
basic_auth_header, mock_raindex_registry_url, seed_api_key, TestClientBuilder,
};
use rocket::http::{Header, Status};

async fn mock_server(response: &'static [u8]) -> String {
Expand All @@ -157,6 +127,14 @@ mod tests {
format!("http://{addr}")
}

async fn build_client_with_token_url(token_url: &str) -> rocket::local::asynchronous::Client {
let registry_url = mock_raindex_registry_url(Some(token_url)).await;
TestClientBuilder::new()
.raindex_registry_url(registry_url)
.build()
.await
}

#[rocket::async_test]
async fn test_get_tokens_returns_token_list() {
let body = r#"{"tokens":[{"chainId":8453,"address":"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913","name":"USD Coin","symbol":"USDC","decimals":6,"extensions":{"isin":"US1234567890"}}]}"#;
Expand All @@ -168,7 +146,7 @@ mod tests {
let response_bytes: &'static [u8] =
Box::leak(response_bytes.into_bytes().into_boxed_slice());
let url = mock_server(response_bytes).await;
let client = TestClientBuilder::new().token_list_url(&url).build().await;
let client = build_client_with_token_url(&url).await;
let (key_id, secret) = seed_api_key(&client).await;
let header = basic_auth_header(&key_id, &secret);
let response = client
Expand Down Expand Up @@ -203,7 +181,7 @@ mod tests {
let response_bytes: &'static [u8] =
Box::leak(response_bytes.into_bytes().into_boxed_slice());
let url = mock_server(response_bytes).await;
let client = TestClientBuilder::new().token_list_url(&url).build().await;
let client = build_client_with_token_url(&url).await;
let (key_id, secret) = seed_api_key(&client).await;
let header = basic_auth_header(&key_id, &secret);
let response = client
Expand All @@ -229,7 +207,7 @@ mod tests {
let response_bytes: &'static [u8] =
Box::leak(response_bytes.into_bytes().into_boxed_slice());
let url = mock_server(response_bytes).await;
let client = TestClientBuilder::new().token_list_url(&url).build().await;
let client = build_client_with_token_url(&url).await;
let (key_id, secret) = seed_api_key(&client).await;
let header = basic_auth_header(&key_id, &secret);
let response = client
Expand All @@ -255,7 +233,7 @@ mod tests {
let response_bytes: &'static [u8] =
Box::leak(response_bytes.into_bytes().into_boxed_slice());
let url = mock_server(response_bytes).await;
let client = TestClientBuilder::new().token_list_url(&url).build().await;
let client = build_client_with_token_url(&url).await;
let (key_id, secret) = seed_api_key(&client).await;
let header = basic_auth_header(&key_id, &secret);
let response = client
Expand All @@ -280,7 +258,7 @@ mod tests {
b"HTTP/1.1 500 Internal Server Error\r\nConnection: close\r\nContent-Length: 0\r\n\r\n",
)
.await;
let client = TestClientBuilder::new().token_list_url(&url).build().await;
let client = build_client_with_token_url(&url).await;
let (key_id, secret) = seed_api_key(&client).await;
let header = basic_auth_header(&key_id, &secret);
let response = client
Expand All @@ -304,7 +282,7 @@ mod tests {
b"HTTP/1.1 200 OK\r\nConnection: close\r\nContent-Type: application/json\r\nContent-Length: 11\r\n\r\nnot-json!!!",
)
.await;
let client = TestClientBuilder::new().token_list_url(&url).build().await;
let client = build_client_with_token_url(&url).await;
let (key_id, secret) = seed_api_key(&client).await;
let header = basic_auth_header(&key_id, &secret);
let response = client
Expand All @@ -327,10 +305,8 @@ mod tests {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
drop(listener);
let client = TestClientBuilder::new()
.token_list_url(format!("http://{addr}"))
.build()
.await;
let url = format!("http://{addr}");
let client = build_client_with_token_url(&url).await;
let (key_id, secret) = seed_api_key(&client).await;
let header = basic_auth_header(&key_id, &secret);
let response = client
Expand Down
64 changes: 15 additions & 49 deletions src/test_helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ pub(crate) async fn client() -> Client {

pub(crate) struct TestClientBuilder {
rate_limiter: crate::fairings::RateLimiter,
token_list_url: Option<String>,
raindex_registry_url: Option<String>,
raindex_config: Option<crate::raindex::RaindexProvider>,
}
Expand All @@ -22,7 +21,6 @@ impl TestClientBuilder {
pub(crate) fn new() -> Self {
Self {
rate_limiter: crate::fairings::RateLimiter::new(10000, 10000),
token_list_url: None,
raindex_registry_url: None,
raindex_config: None,
}
Expand All @@ -33,8 +31,8 @@ impl TestClientBuilder {
self
}

pub(crate) fn token_list_url(mut self, url: impl Into<String>) -> Self {
self.token_list_url = Some(url.into());
pub(crate) fn raindex_registry_url(mut self, url: impl Into<String>) -> Self {
self.raindex_registry_url = Some(url.into());
self
}

Expand All @@ -49,17 +47,12 @@ impl TestClientBuilder {
.await
.expect("database init");

let token_list_url = match self.token_list_url {
Some(url) => url,
None => mock_token_list_url().await,
};

let raindex_config = match self.raindex_config {
Some(config) => config,
None => {
let registry_url = match self.raindex_registry_url {
Some(url) => url,
None => mock_raindex_registry_url().await,
None => mock_raindex_registry_url(None).await,
};
crate::raindex::RaindexProvider::load(&registry_url)
.await
Expand All @@ -70,47 +63,14 @@ impl TestClientBuilder {
let shared_raindex = tokio::sync::RwLock::new(raindex_config);
let docs_dir = std::env::temp_dir().to_string_lossy().into_owned();
let rocket = crate::rocket(pool, self.rate_limiter, shared_raindex, docs_dir)
.expect("valid rocket instance")
.manage(crate::routes::tokens::TokensConfig::with_url(
token_list_url,
));
.expect("valid rocket instance");

Client::tracked(rocket).await.expect("valid client")
}
}

async fn mock_token_list_url() -> String {
const BODY: &str = r#"{"tokens":[{"chainId":8453,"address":"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913","name":"USD Coin","symbol":"USDC","decimals":6}]}"#;

let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
.await
.expect("bind mock token server");
let addr = listener.local_addr().expect("mock token server address");
let response = format!(
"HTTP/1.1 200 OK\r\nConnection: close\r\nContent-Type: application/json\r\nContent-Length: {}\r\n\r\n{BODY}",
BODY.len()
);

tokio::spawn(async move {
loop {
let Ok((mut socket, _)) = listener.accept().await else {
break;
};

let response = response.clone();
tokio::spawn(async move {
let mut buf = [0u8; 1024];
let _ = tokio::io::AsyncReadExt::read(&mut socket, &mut buf).await;
let _ = tokio::io::AsyncWriteExt::write_all(&mut socket, response.as_bytes()).await;
});
}
});

format!("http://{addr}")
}

pub(crate) async fn mock_raindex_config() -> crate::raindex::RaindexProvider {
let registry_url = mock_raindex_registry_url().await;
let registry_url = mock_raindex_registry_url(None).await;
crate::raindex::RaindexProvider::load(&registry_url)
.await
.expect("mock raindex config")
Expand All @@ -123,8 +83,13 @@ pub(crate) async fn mock_invalid_raindex_config() -> crate::raindex::RaindexProv
.expect("mock invalid raindex config")
}

pub(crate) async fn mock_raindex_registry_url() -> String {
let settings = r#"version: 4
pub(crate) async fn mock_raindex_registry_url(token_list_url: Option<&str>) -> String {
let using_tokens = match token_list_url {
Some(url) => format!("\nusing-tokens-from:\n - {url}\n"),
None => String::new(),
};
let settings = format!(
r#"version: 4
networks:
base:
rpcs:
Expand All @@ -147,8 +112,9 @@ tokens:
token1:
address: 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913
network: base
"#;
mock_raindex_registry_url_with_settings(settings).await
{using_tokens}"#
);
mock_raindex_registry_url_with_settings(&settings).await
}

pub(crate) async fn mock_raindex_registry_url_with_settings(settings: &str) -> String {
Expand Down