From 27e373a9fa4cc5a90cdd1c47b6e8d51f04a14e79 Mon Sep 17 00:00:00 2001 From: Max Kalashnikoff Date: Tue, 22 Jul 2025 15:47:00 +0200 Subject: [PATCH 1/3] feat: implementing project_plan_limits method to get limits from a new internal api --- .github/workflows/ci.yaml | 2 +- src/project/types/project_data.rs | 16 ++++- src/registry/client.rs | 113 +++++++++++++++++++++++++----- 3 files changed, 111 insertions(+), 20 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 40796e1..b719ec9 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -44,7 +44,7 @@ jobs: - name: "Check Formatting" cmd: fmt args: -- --check - toolchain: nightly + toolchain: stable profile: default - name: "Unit Tests" cmd: test diff --git a/src/project/types/project_data.rs b/src/project/types/project_data.rs index 26649f4..d2e0092 100644 --- a/src/project/types/project_data.rs +++ b/src/project/types/project_data.rs @@ -41,7 +41,6 @@ pub struct ProjectDataWithQuota { pub project_data: ProjectData, pub quota: Quota, } - #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Hash, Clone)] #[serde(rename_all = "camelCase")] pub struct Quota { @@ -50,6 +49,21 @@ pub struct Quota { pub is_valid: bool, } +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Hash, Clone)] +#[serde(rename_all = "camelCase")] +pub struct ProjectDataWithLimits { + pub data: ProjectData, + pub limits: PlanLimits, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Hash, Clone)] +#[serde(rename_all = "camelCase")] +pub struct PlanLimits { + pub tier: String, + pub is_above_rpc_limit: bool, + pub is_above_mau_limit: bool, +} + impl ProjectData { pub fn validate_access( &self, diff --git a/src/registry/client.rs b/src/registry/client.rs index c22b924..3212d89 100644 --- a/src/registry/client.rs +++ b/src/registry/client.rs @@ -1,19 +1,21 @@ use { crate::{ - project::{ProjectData, ProjectDataWithQuota}, + project::{PlanLimits, ProjectData, ProjectDataWithLimits, ProjectDataWithQuota}, registry::error::RegistryError, }, async_trait::async_trait, reqwest::{ header::{self, HeaderValue}, - IntoUrl, - StatusCode, - Url, + IntoUrl, StatusCode, Url, }, serde::de::DeserializeOwned, std::{fmt::Debug, time::Duration}, }; +use once_cell::sync::Lazy; + +static INTERNAL_API_BASE_URI: Lazy = + Lazy::new(|| Url::parse("https://api.reown.com").expect("Invalid internal API base URI")); const INVALID_TOKEN_ERROR: &str = "invalid auth token"; pub type RegistryResult = Result; @@ -25,6 +27,11 @@ pub trait RegistryClient: 'static + Send + Sync + Debug { &self, id: &str, ) -> RegistryResult>; + async fn project_limits(&self, id: &str) -> RegistryResult>; + async fn project_data_with_limits( + &self, + id: &str, + ) -> RegistryResult>; } /// HTTP client configuration. @@ -62,22 +69,27 @@ impl Default for HttpClientConfig { #[derive(Debug, Clone)] pub struct RegistryHttpClient { - base_url: Url, + base_explorer_url: Url, + base_internal_api_url: Url, http_client: reqwest::Client, } impl RegistryHttpClient { - pub fn new(base_url: impl IntoUrl, auth_token: &str, origin: &str) -> RegistryResult { - Self::with_config(base_url, auth_token, origin, Default::default()) + pub fn new( + base_explorer_url: impl IntoUrl, + auth_token: &str, + origin: &str, + ) -> RegistryResult { + Self::with_config(base_explorer_url, auth_token, origin, Default::default()) } pub fn with_config( - base_url: impl IntoUrl, + base_explorer_url: impl IntoUrl, auth_token: &str, origin: &str, config: HttpClientConfig, ) -> RegistryResult { - let mut auth_value = HeaderValue::from_str(&format!("Bearer {}", auth_token)) + let mut auth_value = HeaderValue::from_str(&format!("Bearer {auth_token}")) .map_err(|_| RegistryError::Config(INVALID_TOKEN_ERROR))?; // Make sure we're not leaking auth token in debug output. @@ -90,6 +102,8 @@ impl RegistryHttpClient { HeaderValue::from_str(origin).map_err(RegistryError::OriginParse)?, ); + // We can use the same client for both explorer and internal API + // because the internal API is protected by the same auth token. let mut http_client = reqwest::Client::builder() .default_headers(headers) .pool_idle_timeout(config.pool_idle_timeout) @@ -100,7 +114,10 @@ impl RegistryHttpClient { } Ok(Self { - base_url: base_url.into_url().map_err(RegistryError::BaseUrlIntoUrl)?, + base_explorer_url: base_explorer_url + .into_url() + .map_err(RegistryError::BaseUrlIntoUrl)?, + base_internal_api_url: INTERNAL_API_BASE_URI.clone(), http_client: http_client.build().map_err(RegistryError::BuildClient)?, }) } @@ -114,7 +131,29 @@ impl RegistryHttpClient { return Ok(None); } - let url = build_url(&self.base_url, project_id, quota).map_err(RegistryError::UrlBuild)?; + let url = build_explorer_url(&self.base_explorer_url, project_id, quota) + .map_err(RegistryError::UrlBuild)?; + + let resp = self + .http_client + .get(url) + .send() + .await + .map_err(RegistryError::Transport)?; + + parse_http_response(resp).await + } + + async fn project_limits_impl( + &self, + project_id: &str, + ) -> RegistryResult> { + if !is_valid_project_id(project_id) { + return Ok(None); + } + + let url = build_internal_api_url(&self.base_internal_api_url, project_id) + .map_err(RegistryError::UrlBuild)?; let resp = self .http_client @@ -125,6 +164,25 @@ impl RegistryHttpClient { parse_http_response(resp).await } + + async fn project_data_with_limits_impl( + &self, + project_id: &str, + ) -> RegistryResult> { + if !is_valid_project_id(project_id) { + return Ok(None); + } + let data: ProjectData = match self.project_data_impl(project_id, false).await? { + Some(project_data) => project_data, + None => return Ok(None), + }; + let limits: PlanLimits = match self.project_limits_impl(project_id).await? { + Some(limits) => limits, + None => return Ok(None), + }; + + Ok(Some(ProjectDataWithLimits { data, limits })) + } } #[async_trait] @@ -139,9 +197,24 @@ impl RegistryClient for RegistryHttpClient { ) -> RegistryResult> { self.project_data_impl(project_id, true).await } + + async fn project_limits(&self, project_id: &str) -> RegistryResult> { + self.project_limits_impl(project_id).await + } + + async fn project_data_with_limits( + &self, + project_id: &str, + ) -> RegistryResult> { + self.project_data_with_limits_impl(project_id).await + } } -fn build_url(base_url: &Url, project_id: &str, quota: bool) -> Result { +fn build_explorer_url( + base_url: &Url, + project_id: &str, + quota: bool, +) -> Result { let mut url = base_url.join(&format!("/internal/project/key/{project_id}"))?; if quota { url.query_pairs_mut().append_pair("quotas", "true"); @@ -149,6 +222,12 @@ fn build_url(base_url: &Url, project_id: &str, quota: bool) -> Result Result { + let mut url = base_url.join("/internal/v1/project-limits")?; + url.query_pairs_mut().append_pair("projectId", project_id); + Ok(url) +} + /// Checks if the project ID is formatted properly. It must be 32 hex /// characters. fn is_valid_project_id(project_id: &str) -> bool { @@ -186,9 +265,7 @@ mod test { wiremock::{ http::Method, matchers::{method, path, query_param}, - Mock, - MockServer, - ResponseTemplate, + Mock, MockServer, ResponseTemplate, }, }; @@ -350,11 +427,11 @@ mod test { } #[test] - fn test_build_url() { + fn test_build_explorer_url() { let base_url = Url::parse("http://example.com").unwrap(); let project_id = "a".repeat(32); - let url = build_url(&base_url, &project_id, false).unwrap(); + let url = build_explorer_url(&base_url, &project_id, false).unwrap(); assert_eq!( url.as_str(), "http://example.com/internal/project/key/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" @@ -366,7 +443,7 @@ mod test { let base_url = Url::parse("http://example.com").unwrap(); let project_id = "a".repeat(32); - let url = build_url(&base_url, &project_id, true).unwrap(); + let url = build_explorer_url(&base_url, &project_id, true).unwrap(); assert_eq!( url.as_str(), "http://example.com/internal/project/key/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa?quotas=true" From 7e7f6ebe43fa9ae521d85217e293ade6bf40e492 Mon Sep 17 00:00:00 2001 From: Max Kalashnikoff Date: Wed, 23 Jul 2025 15:00:11 +0200 Subject: [PATCH 2/3] feat: adding st, sv to the client --- src/registry/client.rs | 45 +++++++++++++++++++++++++++++++----------- 1 file changed, 34 insertions(+), 11 deletions(-) diff --git a/src/registry/client.rs b/src/registry/client.rs index 3212d89..6493f3d 100644 --- a/src/registry/client.rs +++ b/src/registry/client.rs @@ -72,6 +72,8 @@ pub struct RegistryHttpClient { base_explorer_url: Url, base_internal_api_url: Url, http_client: reqwest::Client, + st: String, + sv: String, } impl RegistryHttpClient { @@ -79,14 +81,25 @@ impl RegistryHttpClient { base_explorer_url: impl IntoUrl, auth_token: &str, origin: &str, + st: &str, + sv: &str, ) -> RegistryResult { - Self::with_config(base_explorer_url, auth_token, origin, Default::default()) + Self::with_config( + base_explorer_url, + auth_token, + origin, + st, + sv, + Default::default(), + ) } pub fn with_config( base_explorer_url: impl IntoUrl, auth_token: &str, origin: &str, + st: &str, + sv: &str, config: HttpClientConfig, ) -> RegistryResult { let mut auth_value = HeaderValue::from_str(&format!("Bearer {auth_token}")) @@ -119,6 +132,8 @@ impl RegistryHttpClient { .map_err(RegistryError::BaseUrlIntoUrl)?, base_internal_api_url: INTERNAL_API_BASE_URI.clone(), http_client: http_client.build().map_err(RegistryError::BuildClient)?, + st: st.to_string(), + sv: sv.to_string(), }) } @@ -152,8 +167,9 @@ impl RegistryHttpClient { return Ok(None); } - let url = build_internal_api_url(&self.base_internal_api_url, project_id) - .map_err(RegistryError::UrlBuild)?; + let url = + build_internal_api_url(&self.base_internal_api_url, project_id, &self.st, &self.sv) + .map_err(RegistryError::UrlBuild)?; let resp = self .http_client @@ -222,9 +238,16 @@ fn build_explorer_url( Ok(url) } -fn build_internal_api_url(base_url: &Url, project_id: &str) -> Result { +fn build_internal_api_url( + base_url: &Url, + project_id: &str, + st: &str, + sv: &str, +) -> Result { let mut url = base_url.join("/internal/v1/project-limits")?; url.query_pairs_mut().append_pair("projectId", project_id); + url.query_pairs_mut().append_pair("st", st); + url.query_pairs_mut().append_pair("sv", sv); Ok(url) } @@ -300,7 +323,7 @@ mod test { .mount(&mock_server) .await; - let response = RegistryHttpClient::new(mock_server.uri(), "auth", TEST_ORIGIN) + let response = RegistryHttpClient::new(mock_server.uri(), "auth", TEST_ORIGIN, "st", "sv") .unwrap() .project_data(&project_id) .await @@ -334,7 +357,7 @@ mod test { .mount(&mock_server) .await; - let response = RegistryHttpClient::new(mock_server.uri(), "auth", TEST_ORIGIN) + let response = RegistryHttpClient::new(mock_server.uri(), "auth", TEST_ORIGIN, "st", "sv") .unwrap() .project_data_with_quota(&project_id) .await @@ -354,7 +377,7 @@ mod test { .mount(&mock_server) .await; - let response = RegistryHttpClient::new(mock_server.uri(), "auth", TEST_ORIGIN) + let response = RegistryHttpClient::new(mock_server.uri(), "auth", TEST_ORIGIN, "st", "sv") .unwrap() .project_data(&project_id) .await @@ -368,7 +391,7 @@ mod test { let mock_server = MockServer::start().await; - let response = RegistryHttpClient::new(mock_server.uri(), "auth", TEST_ORIGIN) + let response = RegistryHttpClient::new(mock_server.uri(), "auth", TEST_ORIGIN, "st", "sv") .unwrap() .project_data(&project_id) .await @@ -382,7 +405,7 @@ mod test { let mock_server = MockServer::start().await; - let response = RegistryHttpClient::new(mock_server.uri(), "auth", TEST_ORIGIN) + let response = RegistryHttpClient::new(mock_server.uri(), "auth", TEST_ORIGIN, "st", "sv") .unwrap() .project_data(&project_id) .await @@ -396,7 +419,7 @@ mod test { let mock_server = MockServer::start().await; - let response = RegistryHttpClient::new(mock_server.uri(), "auth", TEST_ORIGIN) + let response = RegistryHttpClient::new(mock_server.uri(), "auth", TEST_ORIGIN, "st", "sv") .unwrap() .project_data(&project_id) .await @@ -416,7 +439,7 @@ mod test { .mount(&mock_server) .await; - let result = RegistryHttpClient::new(mock_server.uri(), "auth", TEST_ORIGIN) + let result = RegistryHttpClient::new(mock_server.uri(), "auth", TEST_ORIGIN, "st", "sv") .unwrap() .project_data(&project_id) .await; From 20b10c0c0bb24944d8a402c8b2bf34a6f7e607ae Mon Sep 17 00:00:00 2001 From: Max Kalashnikoff Date: Wed, 23 Jul 2025 15:51:46 +0200 Subject: [PATCH 3/3] fix: fixing the schema error message --- src/registry/client.rs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/registry/client.rs b/src/registry/client.rs index 6493f3d..d9d7c84 100644 --- a/src/registry/client.rs +++ b/src/registry/client.rs @@ -8,10 +8,16 @@ use { header::{self, HeaderValue}, IntoUrl, StatusCode, Url, }, - serde::de::DeserializeOwned, + serde::{de::DeserializeOwned, Deserialize, Serialize}, std::{fmt::Debug, time::Duration}, }; +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Hash, Clone)] +#[serde(rename_all = "camelCase")] +pub struct LimitsResponse { + pub plan_limits: PlanLimits, +} + use once_cell::sync::Lazy; static INTERNAL_API_BASE_URI: Lazy = @@ -27,7 +33,7 @@ pub trait RegistryClient: 'static + Send + Sync + Debug { &self, id: &str, ) -> RegistryResult>; - async fn project_limits(&self, id: &str) -> RegistryResult>; + async fn project_limits(&self, id: &str) -> RegistryResult>; async fn project_data_with_limits( &self, id: &str, @@ -188,12 +194,12 @@ impl RegistryHttpClient { if !is_valid_project_id(project_id) { return Ok(None); } - let data: ProjectData = match self.project_data_impl(project_id, false).await? { + let data: ProjectData = match self.project_data(project_id).await? { Some(project_data) => project_data, None => return Ok(None), }; - let limits: PlanLimits = match self.project_limits_impl(project_id).await? { - Some(limits) => limits, + let limits: PlanLimits = match self.project_limits(project_id).await? { + Some(response) => response.plan_limits, None => return Ok(None), }; @@ -214,7 +220,7 @@ impl RegistryClient for RegistryHttpClient { self.project_data_impl(project_id, true).await } - async fn project_limits(&self, project_id: &str) -> RegistryResult> { + async fn project_limits(&self, project_id: &str) -> RegistryResult> { self.project_limits_impl(project_id).await }