Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ jobs:
- name: "Check Formatting"
cmd: fmt
args: -- --check
toolchain: nightly
toolchain: stable

@chris13524 chris13524 Jul 23, 2025

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why change this? We typically use nightly formatting which was fine

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I believe we moved to the stable in a recent service change. As I remember, the reason to use nightly was an async trait, which is not an issue for a while. btw, blockchain-api uses a stable.

profile: default
- name: "Unit Tests"
cmd: test
Expand Down
16 changes: 15 additions & 1 deletion src/project/types/project_data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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,
Expand Down
158 changes: 132 additions & 26 deletions src/registry/client.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,27 @@
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,
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<Url> =
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<T> = Result<T, RegistryError>;
Expand All @@ -25,6 +33,11 @@ pub trait RegistryClient: 'static + Send + Sync + Debug {
&self,
id: &str,
) -> RegistryResult<Option<ProjectDataWithQuota>>;
async fn project_limits(&self, id: &str) -> RegistryResult<Option<LimitsResponse>>;
async fn project_data_with_limits(
&self,
id: &str,
) -> RegistryResult<Option<ProjectDataWithLimits>>;
}

/// HTTP client configuration.
Expand Down Expand Up @@ -62,22 +75,40 @@ 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,
st: String,
sv: String,
}

impl RegistryHttpClient {
pub fn new(base_url: impl IntoUrl, auth_token: &str, origin: &str) -> RegistryResult<Self> {
Self::with_config(base_url, auth_token, origin, Default::default())
pub fn new(
base_explorer_url: impl IntoUrl,
auth_token: &str,
origin: &str,
st: &str,
sv: &str,
) -> RegistryResult<Self> {
Self::with_config(
base_explorer_url,
auth_token,
origin,
st,
sv,
Default::default(),
)
}

pub fn with_config(
base_url: impl IntoUrl,
base_explorer_url: impl IntoUrl,
auth_token: &str,
origin: &str,
st: &str,
sv: &str,
config: HttpClientConfig,
) -> RegistryResult<Self> {
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.
Expand All @@ -90,6 +121,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)
Expand All @@ -100,8 +133,13 @@ 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)?,
st: st.to_string(),
sv: sv.to_string(),
})
}

Expand All @@ -114,7 +152,30 @@ 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<T: DeserializeOwned>(
&self,
project_id: &str,
) -> RegistryResult<Option<T>> {
if !is_valid_project_id(project_id) {
return Ok(None);
}

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
Expand All @@ -125,6 +186,25 @@ impl RegistryHttpClient {

parse_http_response(resp).await
}

async fn project_data_with_limits_impl(
&self,
project_id: &str,
) -> RegistryResult<Option<ProjectDataWithLimits>> {
if !is_valid_project_id(project_id) {
return Ok(None);
}
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(project_id).await? {
Some(response) => response.plan_limits,
None => return Ok(None),
};

Ok(Some(ProjectDataWithLimits { data, limits }))
}
}

#[async_trait]
Expand All @@ -139,16 +219,44 @@ impl RegistryClient for RegistryHttpClient {
) -> RegistryResult<Option<ProjectDataWithQuota>> {
self.project_data_impl(project_id, true).await
}

async fn project_limits(&self, project_id: &str) -> RegistryResult<Option<LimitsResponse>> {
self.project_limits_impl(project_id).await
}

async fn project_data_with_limits(
&self,
project_id: &str,
) -> RegistryResult<Option<ProjectDataWithLimits>> {
self.project_data_with_limits_impl(project_id).await
}
}

fn build_url(base_url: &Url, project_id: &str, quota: bool) -> Result<Url, url::ParseError> {
fn build_explorer_url(
base_url: &Url,
project_id: &str,
quota: bool,
) -> Result<Url, url::ParseError> {
let mut url = base_url.join(&format!("/internal/project/key/{project_id}"))?;
if quota {
url.query_pairs_mut().append_pair("quotas", "true");
}
Ok(url)
}

fn build_internal_api_url(
base_url: &Url,
project_id: &str,
st: &str,
sv: &str,
) -> Result<Url, url::ParseError> {
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)
}

/// Checks if the project ID is formatted properly. It must be 32 hex
/// characters.
fn is_valid_project_id(project_id: &str) -> bool {
Expand Down Expand Up @@ -186,9 +294,7 @@ mod test {
wiremock::{
http::Method,
matchers::{method, path, query_param},
Mock,
MockServer,
ResponseTemplate,
Mock, MockServer, ResponseTemplate,
},
};

Expand Down Expand Up @@ -223,7 +329,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
Expand Down Expand Up @@ -257,7 +363,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
Expand All @@ -277,7 +383,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
Expand All @@ -291,7 +397,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
Expand All @@ -305,7 +411,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
Expand All @@ -319,7 +425,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
Expand All @@ -339,7 +445,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;
Expand All @@ -350,11 +456,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"
Expand All @@ -366,7 +472,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"
Expand Down
Loading