From e1f6500677603c4a3c337bbd24b04a4cacbff486 Mon Sep 17 00:00:00 2001 From: Arda Nakisci Date: Wed, 4 Mar 2026 10:37:23 +0300 Subject: [PATCH 1/3] Update rain.orderbook submodule to 4df6e7dfb Pick up the commit that exposes `get_remote_tokens()` from `RaindexClient`, which parses the `using-tokens-from` field in the registry YAML. --- Cargo.lock | 1 + lib/rain.orderbook | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index afe1cbf..19b5caa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6761,6 +6761,7 @@ dependencies = [ "serde", "serde_json", "thiserror 1.0.69", + "tracing", "url", "wasm-bindgen-utils 0.0.10 (git+https://github.com/rainlanguage/rain.wasm?rev=06990d85a0b7c55378a1c8cca4dd9e2bc34a596a)", ] diff --git a/lib/rain.orderbook b/lib/rain.orderbook index 0aab416..4df6e7d 160000 --- a/lib/rain.orderbook +++ b/lib/rain.orderbook @@ -1 +1 @@ -Subproject commit 0aab4165c6ba7385b58d244d7180e8117aca6d32 +Subproject commit 4df6e7dfb4a1421b56acc0d805c7af3ce350bc60 From 637165d7daba95a65145c602b62230903dc877d1 Mon Sep 17 00:00:00 2001 From: Arda Nakisci Date: Wed, 4 Mar 2026 10:37:53 +0300 Subject: [PATCH 2/3] Use registry-provided token list URL instead of hardcoded constant Replace the hardcoded TOKEN_LIST_URL and TokensConfig fairing with a dynamic lookup via RaindexProvider.get_remote_token_urls(), which reads the using-tokens-from field from the registry YAML. The handler now takes SharedRaindexProvider as a Rocket state parameter, resolves the first configured URL at request time, and fetches the token list with a fresh reqwest client. --- src/main.rs | 1 - src/raindex/config.rs | 13 +++++++ src/routes/tokens.rs | 82 +++++++++++++++---------------------------- 3 files changed, 42 insertions(+), 54 deletions(-) diff --git a/src/main.rs b/src/main.rs index 6436529..2018a2d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -143,7 +143,6 @@ pub(crate) fn rocket( .attach(fairings::RequestLogger) .attach(fairings::UsageLogger) .attach(fairings::RateLimitHeadersFairing) - .attach(routes::tokens::fairing()) .attach(cors)) } diff --git a/src/raindex/config.rs b/src/raindex/config.rs index f234bf3..113c48e 100644 --- a/src/raindex/config.rs +++ b/src/raindex/config.rs @@ -61,6 +61,19 @@ impl RaindexProvider { self.registry.registry_url() } + pub(crate) fn get_remote_token_urls(&self) -> Result, 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(&self, f: F) -> Result where T: Send + 'static, diff --git a/src/routes/tokens.rs b/src/routes/tokens.rs index c54e92f..57c3ef2 100644 --- a/src/routes/tokens.rs +++ b/src/routes/tokens.rs @@ -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) -> Self { - Self { - url: url.into(), - client: reqwest::Client::new(), - } - } -} - -pub(crate) fn fairing() -> AdHoc { - AdHoc::on_ignite("Tokens Config", |rocket| async { - if rocket.state::().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}")] @@ -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 for ApiError { @@ -82,16 +47,19 @@ pub async fn get_tokens( _global: GlobalRateLimit, _key: AuthenticatedKey, span: TracingSpan, - tokens_config: &State, + shared_raindex: &State, ) -> Result, 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); + 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() @@ -139,7 +107,9 @@ pub fn routes() -> Vec { #[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 { @@ -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"}}]}"#; @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 From 17591783ad3bec58690d5f925c81cd38e884489f Mon Sep 17 00:00:00 2001 From: Arda Nakisci Date: Wed, 4 Mar 2026 10:38:04 +0300 Subject: [PATCH 3/3] Update test helpers to inject token URL via mock registry Remove token_list_url from TestClientBuilder and mock_token_list_url(). Instead, mock_raindex_registry_url() now accepts an optional token URL and embeds it as using-tokens-from in the mock registry YAML, so tests exercise the full registry-based URL resolution path. --- src/routes/admin.rs | 4 +-- src/test_helpers.rs | 64 +++++++++++---------------------------------- 2 files changed, 17 insertions(+), 51 deletions(-) diff --git a/src/routes/admin.rs b/src/routes/admin.rs index e2540f5..f6a466b 100644 --- a/src/routes/admin.rs +++ b/src/routes/admin.rs @@ -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") @@ -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") diff --git a/src/test_helpers.rs b/src/test_helpers.rs index 4596509..dc841d2 100644 --- a/src/test_helpers.rs +++ b/src/test_helpers.rs @@ -13,7 +13,6 @@ pub(crate) async fn client() -> Client { pub(crate) struct TestClientBuilder { rate_limiter: crate::fairings::RateLimiter, - token_list_url: Option, raindex_registry_url: Option, raindex_config: Option, } @@ -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, } @@ -33,8 +31,8 @@ impl TestClientBuilder { self } - pub(crate) fn token_list_url(mut self, url: impl Into) -> Self { - self.token_list_url = Some(url.into()); + pub(crate) fn raindex_registry_url(mut self, url: impl Into) -> Self { + self.raindex_registry_url = Some(url.into()); self } @@ -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(®istry_url) .await @@ -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(®istry_url) .await .expect("mock raindex config") @@ -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: @@ -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 {