From d372c304e3fbb7f981d3bc714e3e8c96707bffe6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20F=2E=20=C5=A0tignjedec?= Date: Sat, 16 Dec 2023 02:19:07 +0100 Subject: [PATCH 1/9] Implement basic bulk address endpoint in server --- server/Cargo.lock | 61 ++++++++++++++++++++++++++++++ server/Cargo.toml | 2 + server/src/cache.rs | 1 + server/src/http.rs | 6 +++ server/src/routes/address.rs | 73 +++++++++++++++++++++++++++++++++++- server/src/routes/mod.rs | 25 +++++++++++- 6 files changed, 165 insertions(+), 3 deletions(-) diff --git a/server/Cargo.lock b/server/Cargo.lock index ce0ba6e..fe75e25 100644 --- a/server/Cargo.lock +++ b/server/Cargo.lock @@ -1025,6 +1025,7 @@ dependencies = [ "rustls", "serde", "serde_json", + "serde_qs", "serde_with", "sha2", "thiserror", @@ -1035,6 +1036,7 @@ dependencies = [ "tracing-subscriber", "utoipa", "utoipa-swagger-ui", + "validator", ] [[package]] @@ -1951,6 +1953,12 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "if_chain" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb56e1aa765b4b4f3aadfab769793b7087bb03a4ea4920644a6d238e2df5b9ed" + [[package]] name = "impl-codec" version = "0.6.0" @@ -3367,6 +3375,17 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_qs" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0431a35568651e363364210c91983c1da5eb29404d9f0928b67d4ebcfa7d330c" +dependencies = [ + "percent-encoding", + "serde", + "thiserror", +] + [[package]] name = "serde_spanned" version = "0.6.4" @@ -4218,6 +4237,48 @@ dependencies = [ "serde", ] +[[package]] +name = "validator" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b92f40481c04ff1f4f61f304d61793c7b56ff76ac1469f1beb199b1445b253bd" +dependencies = [ + "idna", + "lazy_static", + "regex", + "serde", + "serde_derive", + "serde_json", + "url", + "validator_derive", +] + +[[package]] +name = "validator_derive" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc44ca3088bb3ba384d9aecf40c6a23a676ce23e09bdaca2073d99c207f864af" +dependencies = [ + "if_chain", + "lazy_static", + "proc-macro-error", + "proc-macro2", + "quote", + "regex", + "syn 1.0.109", + "validator_types", +] + +[[package]] +name = "validator_types" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "111abfe30072511849c5910134e8baf8dc05de4c0e5903d681cbd5c9c4d611e3" +dependencies = [ + "proc-macro2", + "syn 1.0.109", +] + [[package]] name = "valuable" version = "0.1.0" diff --git a/server/Cargo.toml b/server/Cargo.toml index b9ad4d6..81a93ad 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -53,3 +53,5 @@ serde_with = "3.3.0" bech32 = "0.10.0-alpha" crc32fast = "1.3.2" ciborium = "0.2.1" +validator = { version = "0.16.1", features = ["derive"] } +serde_qs = "0.12.0" \ No newline at end of file diff --git a/server/src/cache.rs b/server/src/cache.rs index 680a7b3..f529da3 100644 --- a/server/src/cache.rs +++ b/server/src/cache.rs @@ -4,6 +4,7 @@ use axum::async_trait; use enstate_shared::cache::{CacheError, CacheLayer}; use redis::{aio::ConnectionManager, AsyncCommands}; +#[derive(Clone)] pub struct Redis { redis: ConnectionManager, } diff --git a/server/src/http.rs b/server/src/http.rs index f0291ca..5dc0987 100644 --- a/server/src/http.rs +++ b/server/src/http.rs @@ -59,6 +59,12 @@ pub fn setup(state: AppState) -> App { .directory_route("/u/:name_or_address", get(routes::universal::get)) .directory_route("/i/:name_or_address", get(routes::image::get)) .directory_route("/h/:name_or_address", get(routes::header::get)) + // TODO (@antony1060): make better + .directory_route("/bulk/a", get(routes::address::get_bulk)) + // .directory_route("/bulk/n", get(routes::name::get)) + // .directory_route("/bulk/u", get(routes::universal::get)) + // .directory_route("/bulk/i", get(routes::image::get)) + // .directory_route("/bulk/h", get(routes::header::get)) .fallback(routes::four_oh_four::handler) .layer(CorsLayer::permissive()) .layer(TraceLayer::new_for_http()) diff --git a/server/src/routes/address.rs b/server/src/routes/address.rs index fb22de4..10eb6a4 100644 --- a/server/src/routes/address.rs +++ b/server/src/routes/address.rs @@ -6,9 +6,15 @@ use axum::{ Json, }; use enstate_shared::models::profile::Profile; +use ethers_core::types::Address; +use futures::future::try_join_all; +use serde::Deserialize; +use validator::Validate; use crate::cache; -use crate::routes::{http_simple_status_error, profile_http_error_mapper, FreshQuery, RouteError}; +use crate::routes::{ + http_simple_status_error, profile_http_error_mapper, FreshQuery, Qs, RouteError, +}; #[utoipa::path( get, @@ -55,3 +61,68 @@ pub async fn get( Ok(Json(profile)) } + +#[derive(Validate, Deserialize)] +pub struct GetBulkQuery { + #[serde(default)] + #[validate(length(max = 10))] + addresses: Vec, + + #[serde(flatten)] + fresh: FreshQuery, +} + +#[utoipa::path( + get, + path = "/bulk/a/", + responses( + (status = 200, description = "Successfully found address.", body = ENSProfile), + (status = BAD_REQUEST, description = "Invalid address.", body = ErrorResponse), + (status = NOT_FOUND, description = "No name was associated with this address.", body = ErrorResponse), + (status = UNPROCESSABLE_ENTITY, description = "Reverse record not owned by this address.", body = ErrorResponse), + ), + params( + ("addresses" = Vec, Path, description = "Addresses to lookup name data for"), + ) +)] +pub async fn get_bulk( + Qs(query): Qs, + State(state): State>, +) -> Result>, RouteError> { + let addresses = query + .addresses + .iter() + .map(|address| address.parse::
()) + .collect::, _>>() + .map_err(|_| http_simple_status_error(StatusCode::BAD_REQUEST))?; + + let cache = cache::Redis::new(state.redis.clone()); + let rpc = state + .provider + .get_provider() + .ok_or_else(|| http_simple_status_error(StatusCode::INTERNAL_SERVER_ERROR))? + .clone(); + + let opensea_api_key = &state.opensea_api_key; + + let profiles = addresses + .iter() + .map(|address| { + Profile::from_address( + *address, + query.fresh.fresh, + Box::new(cache.clone()), + rpc.clone(), + opensea_api_key, + &state.profile_records, + &state.profile_chains, + ) + }) + .collect::>(); + + let joined = try_join_all(profiles) + .await + .map_err(profile_http_error_mapper)?; + + Ok(Json(joined)) +} diff --git a/server/src/routes/mod.rs b/server/src/routes/mod.rs index 356f5da..02a1499 100644 --- a/server/src/routes/mod.rs +++ b/server/src/routes/mod.rs @@ -1,3 +1,5 @@ +use axum::extract::FromRequestParts; +use axum::http::request::Parts; use axum::http::StatusCode; use axum::Json; use enstate_shared::models::profile::error::ProfileError; @@ -29,8 +31,9 @@ fn bool_or_false<'de, D>(deserializer: D) -> Result where D: Deserializer<'de>, { - let value: Result = Deserialize::deserialize(deserializer); - Ok(value.unwrap_or_default()) + let value: Result = Deserialize::deserialize(deserializer); + // FIXME (@antony1060): + Ok(value.map(|it| it == "true").unwrap_or(false)) } pub type RouteError = (StatusCode, Json); @@ -104,3 +107,21 @@ pub async fn universal_profile_resolve( ) .await } + +pub struct Qs(T); + +#[axum::async_trait] +impl FromRequestParts for Qs +where + T: serde::de::DeserializeOwned, +{ + // TODO (@antony1060): make better + type Rejection = String; + + async fn from_request_parts(parts: &mut Parts, _: &S) -> Result { + let query = parts.uri.query().unwrap_or(""); + Ok(Self( + serde_qs::from_str(query).map_err(|error| error.to_string())?, + )) + } +} From fb28faab7d6b4848c2119aa66d90de90bd26e69b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20F=2E=20=C5=A0tignjedec?= Date: Sat, 16 Dec 2023 02:28:20 +0100 Subject: [PATCH 2/9] Validate --- server/src/routes/address.rs | 8 +++++++- server/src/routes/mod.rs | 10 ++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/server/src/routes/address.rs b/server/src/routes/address.rs index 10eb6a4..9bbee98 100644 --- a/server/src/routes/address.rs +++ b/server/src/routes/address.rs @@ -13,7 +13,7 @@ use validator::Validate; use crate::cache; use crate::routes::{ - http_simple_status_error, profile_http_error_mapper, FreshQuery, Qs, RouteError, + http_error, http_simple_status_error, profile_http_error_mapper, FreshQuery, Qs, RouteError, }; #[utoipa::path( @@ -89,6 +89,12 @@ pub async fn get_bulk( Qs(query): Qs, State(state): State>, ) -> Result>, RouteError> { + query + .validate() + // TODO (@antony1060): more human errors, contemplate life choices (the validate library) + .map_err(|err| http_error(StatusCode::BAD_REQUEST, &err.to_string()))?; + + // TODO (@antony1060): deduplication let addresses = query .addresses .iter() diff --git a/server/src/routes/mod.rs b/server/src/routes/mod.rs index 02a1499..9c4561c 100644 --- a/server/src/routes/mod.rs +++ b/server/src/routes/mod.rs @@ -69,6 +69,16 @@ pub fn http_simple_status_error(status: StatusCode) -> RouteError { ) } +pub fn http_error(status: StatusCode, error: &str) -> RouteError { + ( + status, + Json(ErrorResponse { + status: status.as_u16(), + error: error.to_string(), + }), + ) +} + pub async fn universal_profile_resolve( name_or_address: &str, fresh: bool, From 81e05d41c7f159e37fa8162be5e693350c48b08d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20F=2E=20=C5=A0tignjedec?= Date: Sat, 16 Dec 2023 02:28:37 +0100 Subject: [PATCH 3/9] New line --- server/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/Cargo.toml b/server/Cargo.toml index 81a93ad..142d116 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -54,4 +54,4 @@ bech32 = "0.10.0-alpha" crc32fast = "1.3.2" ciborium = "0.2.1" validator = { version = "0.16.1", features = ["derive"] } -serde_qs = "0.12.0" \ No newline at end of file +serde_qs = "0.12.0" From 6adca4c23035950ecf89a04dd38be14866bbe296 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20F=2E=20=C5=A0tignjedec?= Date: Tue, 19 Dec 2023 20:01:12 +0100 Subject: [PATCH 4/9] Implement other bulk endpoints & small cleanup --- server/Cargo.lock | 49 ------------------ server/Cargo.toml | 1 - server/src/http.rs | 8 +-- server/src/routes/address.rs | 63 ++++++++++------------ server/src/routes/mod.rs | 14 ++++- server/src/routes/name.rs | 89 ++++++++++++++++++++++++++------ server/src/routes/universal.rs | 65 +++++++++++++++++++++-- shared/src/models/profile/mod.rs | 2 +- 8 files changed, 179 insertions(+), 112 deletions(-) diff --git a/server/Cargo.lock b/server/Cargo.lock index fe75e25..9c1ae1d 100644 --- a/server/Cargo.lock +++ b/server/Cargo.lock @@ -1036,7 +1036,6 @@ dependencies = [ "tracing-subscriber", "utoipa", "utoipa-swagger-ui", - "validator", ] [[package]] @@ -1953,12 +1952,6 @@ dependencies = [ "unicode-normalization", ] -[[package]] -name = "if_chain" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb56e1aa765b4b4f3aadfab769793b7087bb03a4ea4920644a6d238e2df5b9ed" - [[package]] name = "impl-codec" version = "0.6.0" @@ -4237,48 +4230,6 @@ dependencies = [ "serde", ] -[[package]] -name = "validator" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b92f40481c04ff1f4f61f304d61793c7b56ff76ac1469f1beb199b1445b253bd" -dependencies = [ - "idna", - "lazy_static", - "regex", - "serde", - "serde_derive", - "serde_json", - "url", - "validator_derive", -] - -[[package]] -name = "validator_derive" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc44ca3088bb3ba384d9aecf40c6a23a676ce23e09bdaca2073d99c207f864af" -dependencies = [ - "if_chain", - "lazy_static", - "proc-macro-error", - "proc-macro2", - "quote", - "regex", - "syn 1.0.109", - "validator_types", -] - -[[package]] -name = "validator_types" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "111abfe30072511849c5910134e8baf8dc05de4c0e5903d681cbd5c9c4d611e3" -dependencies = [ - "proc-macro2", - "syn 1.0.109", -] - [[package]] name = "valuable" version = "0.1.0" diff --git a/server/Cargo.toml b/server/Cargo.toml index 142d116..6c4f225 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -53,5 +53,4 @@ serde_with = "3.3.0" bech32 = "0.10.0-alpha" crc32fast = "1.3.2" ciborium = "0.2.1" -validator = { version = "0.16.1", features = ["derive"] } serde_qs = "0.12.0" diff --git a/server/src/http.rs b/server/src/http.rs index 5dc0987..9582946 100644 --- a/server/src/http.rs +++ b/server/src/http.rs @@ -61,10 +61,10 @@ pub fn setup(state: AppState) -> App { .directory_route("/h/:name_or_address", get(routes::header::get)) // TODO (@antony1060): make better .directory_route("/bulk/a", get(routes::address::get_bulk)) - // .directory_route("/bulk/n", get(routes::name::get)) - // .directory_route("/bulk/u", get(routes::universal::get)) - // .directory_route("/bulk/i", get(routes::image::get)) - // .directory_route("/bulk/h", get(routes::header::get)) + .directory_route("/bulk/n", get(routes::name::get_bulk)) + .directory_route("/bulk/u", get(routes::universal::get_bulk)) + // .directory_route("/bulk/i", get(routes::image::get_bulk)) + // .directory_route("/bulk/h", get(routes::header::get_bulk)) .fallback(routes::four_oh_four::handler) .layer(CorsLayer::permissive()) .layer(TraceLayer::new_for_http()) diff --git a/server/src/routes/address.rs b/server/src/routes/address.rs index 9bbee98..3db464a 100644 --- a/server/src/routes/address.rs +++ b/server/src/routes/address.rs @@ -9,11 +9,11 @@ use enstate_shared::models::profile::Profile; use ethers_core::types::Address; use futures::future::try_join_all; use serde::Deserialize; -use validator::Validate; use crate::cache; use crate::routes::{ - http_error, http_simple_status_error, profile_http_error_mapper, FreshQuery, Qs, RouteError, + http_error, http_simple_status_error, profile_http_error_mapper, validate_bulk_input, + FreshQuery, Qs, RouteError, }; #[utoipa::path( @@ -34,38 +34,21 @@ pub async fn get( Query(query): Query, State(state): State>, ) -> Result, RouteError> { - let address = address - .parse() - .map_err(|_| http_simple_status_error(StatusCode::BAD_REQUEST))?; - - let cache = Box::new(cache::Redis::new(state.redis.clone())); - let rpc = state - .provider - .get_provider() - .ok_or_else(|| http_simple_status_error(StatusCode::INTERNAL_SERVER_ERROR))? - .clone(); - - let opensea_api_key = &state.opensea_api_key; - - let profile = Profile::from_address( - address, - query.fresh, - cache, - rpc, - opensea_api_key, - &state.profile_records, - &state.profile_chains, + get_bulk( + Qs(AddressGetBulkQuery { + fresh: query, + addresses: vec![address], + }), + State(state), ) .await - .map_err(profile_http_error_mapper)?; - - Ok(Json(profile)) + .map(|res| Json(res.0.get(0).expect("index 0 should exist").clone())) } -#[derive(Validate, Deserialize)] -pub struct GetBulkQuery { +#[derive(Deserialize)] +pub struct AddressGetBulkQuery { + // TODO (@antony1060): remove when proper serde error handling #[serde(default)] - #[validate(length(max = 10))] addresses: Vec, #[serde(flatten)] @@ -86,17 +69,23 @@ pub struct GetBulkQuery { ) )] pub async fn get_bulk( - Qs(query): Qs, + Qs(query): Qs, State(state): State>, ) -> Result>, RouteError> { - query - .validate() - // TODO (@antony1060): more human errors, contemplate life choices (the validate library) - .map_err(|err| http_error(StatusCode::BAD_REQUEST, &err.to_string()))?; - - // TODO (@antony1060): deduplication - let addresses = query + let lowercase_addresses = query .addresses + .iter() + .map(|address| address.to_lowercase()) + .collect::>(); + + let addresses = validate_bulk_input(&lowercase_addresses, 10).ok_or_else(|| { + http_error( + StatusCode::BAD_REQUEST, + "input is too long (expected <= 10)", + ) + })?; + + let addresses = addresses .iter() .map(|address| address.parse::
()) .collect::, _>>() diff --git a/server/src/routes/mod.rs b/server/src/routes/mod.rs index 9c4561c..f15417a 100644 --- a/server/src/routes/mod.rs +++ b/server/src/routes/mod.rs @@ -4,6 +4,7 @@ use axum::http::StatusCode; use axum::Json; use enstate_shared::models::profile::error::ProfileError; use enstate_shared::models::profile::Profile; +use enstate_shared::utils::vec::dedup_ord; use ethers::prelude::ProviderError; use ethers::providers::{Http, Provider}; use ethers_core::types::Address; @@ -118,6 +119,18 @@ pub async fn universal_profile_resolve( .await } +// TODO (@antony1060): None only happens when input length > max_len +// result is more appropriate +pub fn validate_bulk_input(input: &[String], max_len: usize) -> Option> { + let unique = dedup_ord(input); + + if unique.len() > max_len { + return None; + } + + Some(unique) +} + pub struct Qs(T); #[axum::async_trait] @@ -125,7 +138,6 @@ impl FromRequestParts for Qs where T: serde::de::DeserializeOwned, { - // TODO (@antony1060): make better type Rejection = String; async fn from_request_parts(parts: &mut Parts, _: &S) -> Result { diff --git a/server/src/routes/name.rs b/server/src/routes/name.rs index b575bba..d0ed3c8 100644 --- a/server/src/routes/name.rs +++ b/server/src/routes/name.rs @@ -6,9 +6,14 @@ use axum::{ Json, }; use enstate_shared::models::profile::Profile; +use futures::future::try_join_all; +use serde::Deserialize; use crate::cache; -use crate::routes::{http_simple_status_error, profile_http_error_mapper, FreshQuery, RouteError}; +use crate::routes::{ + http_error, http_simple_status_error, profile_http_error_mapper, validate_bulk_input, + FreshQuery, Qs, RouteError, +}; #[utoipa::path( get, @@ -26,9 +31,56 @@ pub async fn get( Query(query): Query, State(state): State>, ) -> Result, RouteError> { - let name = name.to_lowercase(); + get_bulk( + Qs(NameGetBulkQuery { + fresh: query, + names: vec![name], + }), + State(state), + ) + .await + .map(|res| Json(res.0.get(0).expect("index 0 should exist").clone())) +} + +#[derive(Deserialize)] +pub struct NameGetBulkQuery { + // TODO (@antony1060): remove when proper serde error handling + #[serde(default)] + names: Vec, + + #[serde(flatten)] + fresh: FreshQuery, +} - let cache = Box::new(cache::Redis::new(state.redis.clone())); +#[utoipa::path( + get, + path = "/bulk/n/", + responses( + (status = 200, description = "Successfully found name.", body = ENSProfile), + (status = NOT_FOUND, description = "No name could be found.", body = ErrorResponse), + ), + params( + ("name" = String, Path, description = "Name to lookup the name data for."), + ) +)] +pub async fn get_bulk( + Qs(query): Qs, + State(state): State>, +) -> Result>, RouteError> { + let names = query + .names + .iter() + .map(|name| name.to_lowercase()) + .collect::>(); + + let names = validate_bulk_input(&names, 10).ok_or_else(|| { + http_error( + StatusCode::BAD_REQUEST, + "input is too long (expected <= 10)", + ) + })?; + + let cache = cache::Redis::new(state.redis.clone()); let rpc = state .provider .get_provider() @@ -37,17 +89,24 @@ pub async fn get( let opensea_api_key = &state.opensea_api_key; - let profile = Profile::from_name( - &name, - query.fresh, - cache, - rpc, - opensea_api_key, - &state.profile_records, - &state.profile_chains, - ) - .await - .map_err(profile_http_error_mapper)?; + let profiles = names + .iter() + .map(|name| { + Profile::from_name( + name, + query.fresh.fresh, + Box::new(cache.clone()), + rpc.clone(), + opensea_api_key, + &state.profile_records, + &state.profile_chains, + ) + }) + .collect::>(); + + let joined = try_join_all(profiles) + .await + .map_err(profile_http_error_mapper)?; - Ok(Json(profile)) + Ok(Json(joined)) } diff --git a/server/src/routes/universal.rs b/server/src/routes/universal.rs index f3075a1..c288c89 100644 --- a/server/src/routes/universal.rs +++ b/server/src/routes/universal.rs @@ -6,10 +6,12 @@ use axum::{ Json, }; use enstate_shared::models::profile::Profile; +use futures::future::try_join_all; +use serde::Deserialize; use crate::routes::{ - http_simple_status_error, profile_http_error_mapper, universal_profile_resolve, FreshQuery, - RouteError, + http_error, http_simple_status_error, profile_http_error_mapper, universal_profile_resolve, + validate_bulk_input, FreshQuery, Qs, RouteError, }; #[utoipa::path( @@ -29,15 +31,70 @@ pub async fn get( Query(query): Query, State(state): State>, ) -> Result, RouteError> { + get_bulk( + Qs(UniversalGetBulkQuery { + fresh: query, + queries: vec![name_or_address], + }), + State(state), + ) + .await + .map(|res| Json(res.0.get(0).expect("index 0 should exist").clone())) +} + +#[derive(Deserialize)] +pub struct UniversalGetBulkQuery { + // TODO (@antony1060): remove when proper serde error handling + #[serde(default)] + queries: Vec, + + #[serde(flatten)] + fresh: FreshQuery, +} + +#[utoipa::path( + get, + path = "/bulk/u/", + responses( + (status = 200, description = "Successfully found name or address.", body = ENSProfile), + (status = NOT_FOUND, description = "No name or address could be found.", body = ErrorResponse), + (status = UNPROCESSABLE_ENTITY, description = "Reverse record not owned by this address.", body = ErrorResponse), + ), + params( + ("name_or_address" = String, Path, description = "Name or address to lookup the name data for."), + ) +)] +pub async fn get_bulk( + Qs(query): Qs, + State(state): State>, +) -> Result>, RouteError> { + let lowercase_queries = query + .queries + .iter() + .map(|input| input.to_lowercase()) + .collect::>(); + + let queries = validate_bulk_input(&lowercase_queries, 10).ok_or_else(|| { + http_error( + StatusCode::BAD_REQUEST, + "input is too long (expected <= 10)", + ) + })?; + let rpc = state .provider .get_provider() .ok_or_else(|| http_simple_status_error(StatusCode::INTERNAL_SERVER_ERROR))? .clone(); - let profile = universal_profile_resolve(&name_or_address, query.fresh, rpc, &state) + let profiles = queries + .iter() + .map(|input| universal_profile_resolve(input, query.fresh.fresh, rpc.clone(), &state)) + .collect::>(); + + let joined = try_join_all(profiles) .await .map_err(profile_http_error_mapper)?; - Ok(Json(profile)) + Ok(Json(joined)) } diff --git a/shared/src/models/profile/mod.rs b/shared/src/models/profile/mod.rs index 0c428f4..6039f16 100644 --- a/shared/src/models/profile/mod.rs +++ b/shared/src/models/profile/mod.rs @@ -8,7 +8,7 @@ pub mod error; pub mod from_address; pub mod from_name; -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct Profile { // Name pub name: String, From f9f326a73462c90c616c8408510fd2c5633bfdf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20F=2E=20=C5=A0tignjedec?= Date: Tue, 19 Dec 2023 23:58:25 +0100 Subject: [PATCH 5/9] Minor cleanup and worker testing --- server/src/routes/address.rs | 4 +-- server/src/routes/mod.rs | 5 ++- server/src/routes/name.rs | 4 +-- server/src/state.rs | 25 +++++++++------ shared/src/models/eip155/mod.rs | 14 ++++++-- shared/src/models/lookup/image.rs | 1 - shared/src/models/multicoin/cointype/coins.rs | 31 ++++++++++++++++++ .../src/models/multicoin/cointype/slip44.rs | 25 +++++++++++++++ shared/src/models/profile/from_address.rs | 2 +- shared/src/models/profile/from_name.rs | 2 +- worker/src/lib.rs | 32 +++++++++---------- worker/src/lookup.rs | 8 +++-- 12 files changed, 109 insertions(+), 44 deletions(-) diff --git a/server/src/routes/address.rs b/server/src/routes/address.rs index 3db464a..f0f1f84 100644 --- a/server/src/routes/address.rs +++ b/server/src/routes/address.rs @@ -10,7 +10,6 @@ use ethers_core::types::Address; use futures::future::try_join_all; use serde::Deserialize; -use crate::cache; use crate::routes::{ http_error, http_simple_status_error, profile_http_error_mapper, validate_bulk_input, FreshQuery, Qs, RouteError, @@ -91,7 +90,6 @@ pub async fn get_bulk( .collect::, _>>() .map_err(|_| http_simple_status_error(StatusCode::BAD_REQUEST))?; - let cache = cache::Redis::new(state.redis.clone()); let rpc = state .provider .get_provider() @@ -106,7 +104,7 @@ pub async fn get_bulk( Profile::from_address( *address, query.fresh.fresh, - Box::new(cache.clone()), + state.cache.as_ref().as_ref(), rpc.clone(), opensea_api_key, &state.profile_records, diff --git a/server/src/routes/mod.rs b/server/src/routes/mod.rs index f15417a..677af4e 100644 --- a/server/src/routes/mod.rs +++ b/server/src/routes/mod.rs @@ -10,7 +10,6 @@ use ethers::providers::{Http, Provider}; use ethers_core::types::Address; use serde::{Deserialize, Deserializer}; -use crate::cache; use crate::models::error::ErrorResponse; pub mod address; @@ -86,10 +85,10 @@ pub async fn universal_profile_resolve( rpc: Provider, state: &crate::AppState, ) -> Result { - let cache = Box::new(cache::Redis::new(state.redis.clone())); - let opensea_api_key = &state.opensea_api_key; + let cache = state.cache.as_ref().as_ref(); + if let Ok(address) = name_or_address.parse::
() { return Profile::from_address( address, diff --git a/server/src/routes/name.rs b/server/src/routes/name.rs index d0ed3c8..31f7b57 100644 --- a/server/src/routes/name.rs +++ b/server/src/routes/name.rs @@ -9,7 +9,6 @@ use enstate_shared::models::profile::Profile; use futures::future::try_join_all; use serde::Deserialize; -use crate::cache; use crate::routes::{ http_error, http_simple_status_error, profile_http_error_mapper, validate_bulk_input, FreshQuery, Qs, RouteError, @@ -80,7 +79,6 @@ pub async fn get_bulk( ) })?; - let cache = cache::Redis::new(state.redis.clone()); let rpc = state .provider .get_provider() @@ -95,7 +93,7 @@ pub async fn get_bulk( Profile::from_name( name, query.fresh.fresh, - Box::new(cache.clone()), + state.cache.as_ref().as_ref(), rpc.clone(), opensea_api_key, &state.profile_records, diff --git a/server/src/state.rs b/server/src/state.rs index 3da5d71..a0f6abc 100644 --- a/server/src/state.rs +++ b/server/src/state.rs @@ -1,17 +1,18 @@ +use enstate_shared::cache::CacheLayer; use std::env; +use std::sync::Arc; use enstate_shared::models::{ multicoin::cointype::{coins::CoinType, Coins}, records::Records, }; -use redis::aio::ConnectionManager; use tracing::info; -use crate::{database, provider}; +use crate::{cache, database, provider}; #[allow(clippy::module_name_repetitions)] pub struct AppState { - pub redis: ConnectionManager, + pub cache: Arc>, pub profile_records: Vec, pub profile_chains: Vec, pub rpc_urls: Vec, @@ -23,21 +24,25 @@ impl AppState { pub async fn new() -> Self { let profile_records = env::var("PROFILE_RECORDS").map_or_else( |_| Records::default().records, - |s| s.split(',').map(std::string::ToString::to_string).collect(), + |s| s.split(',').map(ToString::to_string).collect(), ); let multicoin_chains: Vec = env::var("MULTICOIN_CHAINS").map_or_else( |_| Coins::default().coins, - |_| { - // TODO: Implement chain parsing - vec![] - }, // |s| s.split(",").map(std::string::ToString::to_string).collect(), + |s| { + let numbers = s + .split(',') + .filter_map(|it| it.parse::().ok()) + .collect::>(); + + numbers.iter().map(|num| CoinType::from(*num)).collect() + }, ); let raw_rpc_urls: String = env::var("RPC_URL").expect("RPC_URL should've been set"); let rpc_urls = raw_rpc_urls .split(',') - .map(std::string::ToString::to_string) + .map(ToString::to_string) .collect::>(); info!("Connecting to Redis..."); @@ -52,7 +57,7 @@ impl AppState { env::var("OPENSEA_API_KEY").expect("OPENSEA_API_KEY should've been set"); Self { - redis, + cache: Arc::new(Box::new(cache::Redis::new(redis))), profile_records, profile_chains: multicoin_chains, opensea_api_key, diff --git a/shared/src/models/eip155/mod.rs b/shared/src/models/eip155/mod.rs index 36f899c..2a01c34 100644 --- a/shared/src/models/eip155/mod.rs +++ b/shared/src/models/eip155/mod.rs @@ -130,11 +130,15 @@ pub async fn resolve_eip155( mod tests { use std::env; + use ethers::middleware::MiddlewareBuilder; + use super::*; #[tokio::test] async fn test_calldata_avatar_erc721() { - let provider = Provider::::try_from("https://rpc.ankr.com/eth").unwrap(); + let provider = Provider::::try_from("https://rpc.ankr.com/eth") + .unwrap() + .wrap_into(CCIPReadMiddleware::new); let opensea_api_key = env::var("OPENSEA_API_KEY").unwrap().to_string(); let data = resolve_eip155( @@ -153,7 +157,9 @@ mod tests { #[tokio::test] async fn test_calldata_avatar_erc1155() { - let provider = Provider::::try_from("https://rpc.ankr.com/eth").unwrap(); + let provider = Provider::::try_from("https://rpc.ankr.com/eth") + .unwrap() + .wrap_into(CCIPReadMiddleware::new); let opensea_api_key = env::var("OPENSEA_API_KEY").unwrap().to_string(); let data = resolve_eip155( @@ -176,7 +182,9 @@ mod tests { #[tokio::test] async fn test_calldata_avatar_erc1155_opensea() { - let provider = Provider::::try_from("https://rpc.ankr.com/eth").unwrap(); + let provider = Provider::::try_from("https://rpc.ankr.com/eth") + .unwrap() + .wrap_into(CCIPReadMiddleware::new); let opensea_api_key = env::var("OPENSEA_API_KEY").unwrap().to_string(); let data = resolve_eip155( diff --git a/shared/src/models/lookup/image.rs b/shared/src/models/lookup/image.rs index 4cca1b0..55849eb 100644 --- a/shared/src/models/lookup/image.rs +++ b/shared/src/models/lookup/image.rs @@ -120,7 +120,6 @@ impl ENSLookup for Image { ) .await?; - // TODO: Remove naive approach return Ok(resolved_uri); } diff --git a/shared/src/models/multicoin/cointype/coins.rs b/shared/src/models/multicoin/cointype/coins.rs index 49bd601..c4764aa 100644 --- a/shared/src/models/multicoin/cointype/coins.rs +++ b/shared/src/models/multicoin/cointype/coins.rs @@ -19,6 +19,16 @@ impl From for U256 { } } +impl From for CoinType { + fn from(value: u64) -> CoinType { + if value >= 0x8000_0000 { + return CoinType::Evm(ChainId::from(value & (!0x8000_0000))); + } + + CoinType::Slip44(SLIP44::from(value as u32)) + } +} + impl Display for CoinType { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { let coin_name = match self { @@ -57,4 +67,25 @@ mod tests { assert_eq!(coin_type.to_string(), "2147483748".to_string()); } + + #[test] + fn test_to_coin_type() { + let coin_type: CoinType = CoinType::from(0); + + assert_eq!(coin_type, CoinType::Slip44(SLIP44::Bitcoin)); + } + + #[test] + fn test_to_coin_type_evm() { + let coin_type: CoinType = CoinType::from(1); + + assert_eq!(coin_type, CoinType::Evm(ChainId::Ethereum)); + } + + #[test] + fn test_to_coin_type_evm_gnosis() { + let coin_type: CoinType = CoinType::from(2147483658); + + assert_eq!(coin_type, CoinType::Evm(ChainId::Gnosis)); + } } diff --git a/shared/src/models/multicoin/cointype/slip44.rs b/shared/src/models/multicoin/cointype/slip44.rs index 54a1a7f..e3db683 100644 --- a/shared/src/models/multicoin/cointype/slip44.rs +++ b/shared/src/models/multicoin/cointype/slip44.rs @@ -51,6 +51,31 @@ impl From for U256 { } } +impl From for SLIP44 { + fn from(val: u32) -> SLIP44 { + match val { + 0 => SLIP44::Bitcoin, + 2 => SLIP44::Litecoin, + 3 => SLIP44::Dogecoin, + 60 => SLIP44::Ethereum, + 145 => SLIP44::BitcoinCash, + 61 => SLIP44::EthereumClassic, + 128 => SLIP44::Monero, + 144 => SLIP44::Ripple, + 148 => SLIP44::Stellar, + 1729 => SLIP44::Tezos, + 3030 => SLIP44::Hedera, + 1815 => SLIP44::Cardano, + 137 => SLIP44::Rootstock, + 22 => SLIP44::Monacoin, + 714 => SLIP44::Binance, + 501 => SLIP44::Solana, + 354 => SLIP44::Polkadot, + val => SLIP44::Other(val.into()), + } + } +} + impl From for CoinType { fn from(val: SLIP44) -> Self { CoinType::Slip44(val) diff --git a/shared/src/models/profile/from_address.rs b/shared/src/models/profile/from_address.rs index 814466e..8f5ac7c 100644 --- a/shared/src/models/profile/from_address.rs +++ b/shared/src/models/profile/from_address.rs @@ -11,7 +11,7 @@ impl Profile { pub async fn from_address( address: H160, fresh: bool, - cache: Box, + cache: &dyn crate::cache::CacheLayer, rpc: Provider, opensea_api_key: &str, profile_records: &[String], diff --git a/shared/src/models/profile/from_name.rs b/shared/src/models/profile/from_name.rs index 95b7ec6..31c9e10 100644 --- a/shared/src/models/profile/from_name.rs +++ b/shared/src/models/profile/from_name.rs @@ -25,7 +25,7 @@ impl Profile { pub async fn from_name( name: &str, fresh: bool, - cache: Box, + cache: &dyn crate::cache::CacheLayer, rpc: Provider, opensea_api_key: &str, profile_records: &[String], diff --git a/worker/src/lib.rs b/worker/src/lib.rs index 4a51344..900cb67 100644 --- a/worker/src/lib.rs +++ b/worker/src/lib.rs @@ -1,8 +1,6 @@ -use std::sync::Arc; - use enstate_shared::meta::gen_app_meta; use lazy_static::lazy_static; -use worker::{event, Context, Cors, Env, Method, Request, Response}; +use worker::{event, Context, Cors, Env, Method, Request, Response, RouteContext, Router}; use crate::lookup::LookupType; @@ -16,25 +14,27 @@ lazy_static! { .with_methods(Method::all()); } -#[event(fetch, respond_with_errors)] -async fn main(req: Request, env: Env, _ctx: Context) -> worker::Result { - if req.path() == "/" { - return root_handler().with_cors(&CORS); - } - - let opensea_api_key = env +async fn main_handler(req: Request, ctx: RouteContext<()>) -> worker::Result { + let opensea_api_key = ctx + .env .var("OPENSEA_API_KEY") .expect("OPENSEA_API_KEY should've been set") .to_string(); - let env_arc = Arc::new(env); - - let response = LookupType::from(req.path()) - .process(req, env_arc, &opensea_api_key) + LookupType::from(req.path()) + .process(req, ctx.env, &opensea_api_key) .await - .unwrap_or_else(|f| f); + .unwrap_or_else(|f| f) + .with_cors(&CORS) +} - response.with_cors(&CORS) +#[event(fetch, respond_with_errors)] +async fn main(req: Request, env: Env, _ctx: Context) -> worker::Result { + Router::new() + .get("/", |_, _| root_handler().with_cors(&CORS)) + .get_async("/*", main_handler) + .run(req, env) + .await } fn root_handler() -> Response { diff --git a/worker/src/lookup.rs b/worker/src/lookup.rs index 8087aea..e26ed28 100644 --- a/worker/src/lookup.rs +++ b/worker/src/lookup.rs @@ -51,14 +51,16 @@ impl LookupType { pub async fn process( &self, req: Request, - env: Arc, + env: Env, opensea_api_key: &str, ) -> Result { - let cache = Box::new(CloudflareKVCache::new(env.clone())); + let arc_env = Arc::new(env); + + let cache = Box::new(CloudflareKVCache::new(arc_env.clone())); let profile_records = Records::default().records; let profile_chains = Coins::default().coins; - let rpc_url = env + let rpc_url = arc_env .var("RPC_URL") .map(|x| x.to_string()) .unwrap_or("https://rpc.enstate.rs/v1/mainnet".to_string()); From 3eabb4fcb9f38955ef86848f74b8e65b812c4dee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20F=2E=20=C5=A0tignjedec?= Date: Thu, 21 Dec 2023 14:37:23 +0100 Subject: [PATCH 6/9] Cleanup --- server/src/http.rs | 3 +- server/src/models/bulk.rs | 16 +++ server/src/models/mod.rs | 1 + server/src/provider.rs | 25 +++- server/src/routes/address.rs | 46 ++----- server/src/routes/header.rs | 15 +-- server/src/routes/image.rs | 15 +-- server/src/routes/mod.rs | 66 +++------- server/src/routes/name.rs | 48 ++----- server/src/routes/universal.rs | 40 ++---- server/src/state.rs | 27 ++-- shared/src/models/eip155/mod.rs | 6 +- shared/src/models/lookup/mod.rs | 5 +- .../profile/{from_address.rs => address.rs} | 35 ++--- shared/src/models/profile/error.rs | 3 +- shared/src/models/profile/mod.rs | 22 +++- .../models/profile/{from_name.rs => name.rs} | 35 +++-- shared/src/models/profile/universal.rs | 22 ++++ shared/src/models/universal_resolver/mod.rs | 10 +- shared/src/utils/factory.rs | 21 +++ shared/src/utils/mod.rs | 1 + worker/src/kv_cache.rs | 18 ++- worker/src/lookup.rs | 123 ++++-------------- 23 files changed, 256 insertions(+), 347 deletions(-) create mode 100644 server/src/models/bulk.rs rename shared/src/models/profile/{from_address.rs => address.rs} (56%) rename shared/src/models/profile/{from_name.rs => name.rs} (87%) create mode 100644 shared/src/models/profile/universal.rs create mode 100644 shared/src/utils/factory.rs diff --git a/server/src/http.rs b/server/src/http.rs index 9582946..663c93a 100644 --- a/server/src/http.rs +++ b/server/src/http.rs @@ -12,13 +12,14 @@ use utoipa_swagger_ui::SwaggerUi; use crate::models::error::ErrorResponse; use crate::models::profile::ENSProfile; +use crate::models::bulk::BulkResponse; use crate::routes; use crate::state::AppState; #[derive(OpenApi)] #[openapi( paths(routes::address::get, routes::name::get, routes::universal::get), - components(schemas(ENSProfile, ErrorResponse)) + components(schemas(ENSProfile, BulkResponse, ErrorResponse)) )] pub struct ApiDoc; diff --git a/server/src/models/bulk.rs b/server/src/models/bulk.rs new file mode 100644 index 0000000..0e8fe85 --- /dev/null +++ b/server/src/models/bulk.rs @@ -0,0 +1,16 @@ +use utoipa::ToSchema; + +#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize, ToSchema)] +pub struct BulkResponse { + pub(crate) response_length: usize, + pub(crate) response: Vec, +} + +impl From> for BulkResponse { + fn from(value: Vec) -> Self { + BulkResponse { + response_length: value.len(), + response: value, + } + } +} diff --git a/server/src/models/mod.rs b/server/src/models/mod.rs index a72f2a4..78f5cdd 100644 --- a/server/src/models/mod.rs +++ b/server/src/models/mod.rs @@ -1,2 +1,3 @@ +pub mod bulk; pub mod error; pub mod profile; diff --git a/server/src/provider.rs b/server/src/provider.rs index e107864..9c877d8 100644 --- a/server/src/provider.rs +++ b/server/src/provider.rs @@ -1,9 +1,13 @@ +use std::sync::Arc; + +use enstate_shared::utils::factory::Factory; use ethers::providers::{Http, Provider}; use rand::seq::SliceRandom; +use tracing::warn; #[derive(Clone)] pub struct RoundRobin { - providers: Vec>, + providers: Vec>>, } impl RoundRobin { @@ -11,15 +15,24 @@ impl RoundRobin { Self { providers: rpc_urls .into_iter() - .map(|rpc_url| { - Provider::::try_from(rpc_url).expect("rpc_url should be a valid URL") + .filter_map(|rpc_url| { + let provider = Provider::::try_from(&rpc_url); + if let Err(err) = provider { + warn!("provider {rpc_url} is not valid: {err}"); + } + + provider.ok().map(Arc::new) }) .collect(), } } +} - // returns a random rpc provider - pub fn get_provider(&self) -> Option<&Provider> { - self.providers.choose(&mut rand::thread_rng()) +impl Factory>> for RoundRobin { + fn get_instance(&self) -> Arc> { + self.providers + .choose(&mut rand::thread_rng()) + .expect("provider should exist") + .clone() } } diff --git a/server/src/routes/address.rs b/server/src/routes/address.rs index f0f1f84..4d803fb 100644 --- a/server/src/routes/address.rs +++ b/server/src/routes/address.rs @@ -10,9 +10,10 @@ use ethers_core::types::Address; use futures::future::try_join_all; use serde::Deserialize; +use crate::models::bulk::BulkResponse; use crate::routes::{ - http_error, http_simple_status_error, profile_http_error_mapper, validate_bulk_input, - FreshQuery, Qs, RouteError, + http_simple_status_error, profile_http_error_mapper, validate_bulk_input, FreshQuery, Qs, + RouteError, }; #[utoipa::path( @@ -41,7 +42,7 @@ pub async fn get( State(state), ) .await - .map(|res| Json(res.0.get(0).expect("index 0 should exist").clone())) + .map(|res| Json(res.0.response.get(0).expect("index 0 should exist").clone())) } #[derive(Deserialize)] @@ -58,7 +59,7 @@ pub struct AddressGetBulkQuery { get, path = "/bulk/a/", responses( - (status = 200, description = "Successfully found address.", body = ENSProfile), + (status = 200, description = "Successfully found address.", body = BulkResponse), (status = BAD_REQUEST, description = "Invalid address.", body = ErrorResponse), (status = NOT_FOUND, description = "No name was associated with this address.", body = ErrorResponse), (status = UNPROCESSABLE_ENTITY, description = "Reverse record not owned by this address.", body = ErrorResponse), @@ -70,19 +71,8 @@ pub struct AddressGetBulkQuery { pub async fn get_bulk( Qs(query): Qs, State(state): State>, -) -> Result>, RouteError> { - let lowercase_addresses = query - .addresses - .iter() - .map(|address| address.to_lowercase()) - .collect::>(); - - let addresses = validate_bulk_input(&lowercase_addresses, 10).ok_or_else(|| { - http_error( - StatusCode::BAD_REQUEST, - "input is too long (expected <= 10)", - ) - })?; +) -> Result>, RouteError> { + let addresses = validate_bulk_input(&query.addresses, 10)?; let addresses = addresses .iter() @@ -90,26 +80,12 @@ pub async fn get_bulk( .collect::, _>>() .map_err(|_| http_simple_status_error(StatusCode::BAD_REQUEST))?; - let rpc = state - .provider - .get_provider() - .ok_or_else(|| http_simple_status_error(StatusCode::INTERNAL_SERVER_ERROR))? - .clone(); - - let opensea_api_key = &state.opensea_api_key; - let profiles = addresses .iter() .map(|address| { - Profile::from_address( - *address, - query.fresh.fresh, - state.cache.as_ref().as_ref(), - rpc.clone(), - opensea_api_key, - &state.profile_records, - &state.profile_chains, - ) + state + .service + .resolve_from_address(*address, query.fresh.fresh) }) .collect::>(); @@ -117,5 +93,5 @@ pub async fn get_bulk( .await .map_err(profile_http_error_mapper)?; - Ok(Json(joined)) + Ok(Json(joined.into())) } diff --git a/server/src/routes/header.rs b/server/src/routes/header.rs index 685d913..9baffb4 100644 --- a/server/src/routes/header.rs +++ b/server/src/routes/header.rs @@ -4,10 +4,7 @@ use axum::extract::{Path, Query, State}; use axum::http::StatusCode; use axum::response::Redirect; -use crate::routes::{ - http_simple_status_error, profile_http_error_mapper, universal_profile_resolve, FreshQuery, - RouteError, -}; +use crate::routes::{http_simple_status_error, profile_http_error_mapper, FreshQuery, RouteError}; // #[utoipa::path( // get, @@ -27,13 +24,9 @@ pub async fn get( Query(query): Query, State(state): State>, ) -> Result { - let rpc = state - .provider - .get_provider() - .ok_or_else(|| http_simple_status_error(StatusCode::INTERNAL_SERVER_ERROR))? - .clone(); - - let profile = universal_profile_resolve(&name_or_address, query.fresh, rpc, &state) + let profile = state + .service + .resolve_from_name_or_address(&name_or_address, query.fresh) .await .map_err(profile_http_error_mapper)?; diff --git a/server/src/routes/image.rs b/server/src/routes/image.rs index 7e218a7..7365309 100644 --- a/server/src/routes/image.rs +++ b/server/src/routes/image.rs @@ -4,10 +4,7 @@ use axum::extract::{Path, Query, State}; use axum::http::StatusCode; use axum::response::Redirect; -use crate::routes::{ - http_simple_status_error, profile_http_error_mapper, universal_profile_resolve, FreshQuery, - RouteError, -}; +use crate::routes::{http_simple_status_error, profile_http_error_mapper, FreshQuery, RouteError}; // #[utoipa::path( // get, @@ -27,13 +24,9 @@ pub async fn get( Query(query): Query, State(state): State>, ) -> Result { - let rpc = state - .provider - .get_provider() - .ok_or_else(|| http_simple_status_error(StatusCode::INTERNAL_SERVER_ERROR))? - .clone(); - - let profile = universal_profile_resolve(&name_or_address, query.fresh, rpc, &state) + let profile = state + .service + .resolve_from_name_or_address(&name_or_address, query.fresh) .await .map_err(profile_http_error_mapper)?; diff --git a/server/src/routes/mod.rs b/server/src/routes/mod.rs index 677af4e..4df6456 100644 --- a/server/src/routes/mod.rs +++ b/server/src/routes/mod.rs @@ -3,12 +3,10 @@ use axum::http::request::Parts; use axum::http::StatusCode; use axum::Json; use enstate_shared::models::profile::error::ProfileError; -use enstate_shared::models::profile::Profile; use enstate_shared::utils::vec::dedup_ord; use ethers::prelude::ProviderError; -use ethers::providers::{Http, Provider}; -use ethers_core::types::Address; use serde::{Deserialize, Deserializer}; +use thiserror::Error; use crate::models::error::ErrorResponse; @@ -32,7 +30,6 @@ where D: Deserializer<'de>, { let value: Result = Deserialize::deserialize(deserializer); - // FIXME (@antony1060): Ok(value.map(|it| it == "true").unwrap_or(false)) } @@ -79,55 +76,34 @@ pub fn http_error(status: StatusCode, error: &str) -> RouteError { ) } -pub async fn universal_profile_resolve( - name_or_address: &str, - fresh: bool, - rpc: Provider, - state: &crate::AppState, -) -> Result { - let opensea_api_key = &state.opensea_api_key; - - let cache = state.cache.as_ref().as_ref(); - - if let Ok(address) = name_or_address.parse::
() { - return Profile::from_address( - address, - fresh, - cache, - rpc, - opensea_api_key, - &state.profile_records, - &state.profile_chains, - ) - .await; - } +#[derive(Error, Debug)] +pub enum ValidationError { + #[error("maximum input length exceeded (expected at most {0})")] + MaxLengthExceeded(usize), +} - if !enstate_shared::patterns::test_domain(name_or_address) { - return Err(ProfileError::NotFound); +impl From for RouteError { + fn from(value: ValidationError) -> Self { + http_error(StatusCode::BAD_REQUEST, &value.to_string()) } - - Profile::from_name( - &name_or_address.to_lowercase(), - fresh, - cache, - rpc, - opensea_api_key, - &state.profile_records, - &state.profile_chains, - ) - .await } -// TODO (@antony1060): None only happens when input length > max_len -// result is more appropriate -pub fn validate_bulk_input(input: &[String], max_len: usize) -> Option> { - let unique = dedup_ord(input); +pub fn validate_bulk_input( + input: &[String], + max_len: usize, +) -> Result, ValidationError> { + let unique = dedup_ord( + &input + .iter() + .map(|entry| entry.to_lowercase()) + .collect::>(), + ); if unique.len() > max_len { - return None; + return Err(ValidationError::MaxLengthExceeded(max_len)); } - Some(unique) + Ok(unique) } pub struct Qs(T); diff --git a/server/src/routes/name.rs b/server/src/routes/name.rs index 31f7b57..2911e61 100644 --- a/server/src/routes/name.rs +++ b/server/src/routes/name.rs @@ -1,6 +1,5 @@ use std::sync::Arc; -use axum::http::StatusCode; use axum::{ extract::{Path, Query, State}, Json, @@ -9,10 +8,8 @@ use enstate_shared::models::profile::Profile; use futures::future::try_join_all; use serde::Deserialize; -use crate::routes::{ - http_error, http_simple_status_error, profile_http_error_mapper, validate_bulk_input, - FreshQuery, Qs, RouteError, -}; +use crate::models::bulk::BulkResponse; +use crate::routes::{profile_http_error_mapper, validate_bulk_input, FreshQuery, Qs, RouteError}; #[utoipa::path( get, @@ -38,7 +35,7 @@ pub async fn get( State(state), ) .await - .map(|res| Json(res.0.get(0).expect("index 0 should exist").clone())) + .map(|res| Json(res.0.response.get(0).expect("index 0 should exist").clone())) } #[derive(Deserialize)] @@ -55,7 +52,7 @@ pub struct NameGetBulkQuery { get, path = "/bulk/n/", responses( - (status = 200, description = "Successfully found name.", body = ENSProfile), + (status = 200, description = "Successfully found name.", body = BulkResponse), (status = NOT_FOUND, description = "No name could be found.", body = ErrorResponse), ), params( @@ -65,46 +62,17 @@ pub struct NameGetBulkQuery { pub async fn get_bulk( Qs(query): Qs, State(state): State>, -) -> Result>, RouteError> { - let names = query - .names - .iter() - .map(|name| name.to_lowercase()) - .collect::>(); - - let names = validate_bulk_input(&names, 10).ok_or_else(|| { - http_error( - StatusCode::BAD_REQUEST, - "input is too long (expected <= 10)", - ) - })?; - - let rpc = state - .provider - .get_provider() - .ok_or_else(|| http_simple_status_error(StatusCode::INTERNAL_SERVER_ERROR))? - .clone(); - - let opensea_api_key = &state.opensea_api_key; +) -> Result>, RouteError> { + let names = validate_bulk_input(&query.names, 10)?; let profiles = names .iter() - .map(|name| { - Profile::from_name( - name, - query.fresh.fresh, - state.cache.as_ref().as_ref(), - rpc.clone(), - opensea_api_key, - &state.profile_records, - &state.profile_chains, - ) - }) + .map(|name| state.service.resolve_from_name(name, query.fresh.fresh)) .collect::>(); let joined = try_join_all(profiles) .await .map_err(profile_http_error_mapper)?; - Ok(Json(joined)) + Ok(Json(joined.into())) } diff --git a/server/src/routes/universal.rs b/server/src/routes/universal.rs index c288c89..9de77a0 100644 --- a/server/src/routes/universal.rs +++ b/server/src/routes/universal.rs @@ -1,6 +1,5 @@ use std::sync::Arc; -use axum::http::StatusCode; use axum::{ extract::{Path, Query, State}, Json, @@ -9,10 +8,8 @@ use enstate_shared::models::profile::Profile; use futures::future::try_join_all; use serde::Deserialize; -use crate::routes::{ - http_error, http_simple_status_error, profile_http_error_mapper, universal_profile_resolve, - validate_bulk_input, FreshQuery, Qs, RouteError, -}; +use crate::models::bulk::BulkResponse; +use crate::routes::{profile_http_error_mapper, validate_bulk_input, FreshQuery, Qs, RouteError}; #[utoipa::path( get, @@ -39,7 +36,7 @@ pub async fn get( State(state), ) .await - .map(|res| Json(res.0.get(0).expect("index 0 should exist").clone())) + .map(|res| Json(res.0.response.get(0).expect("index 0 should exist").clone())) } #[derive(Deserialize)] @@ -56,7 +53,7 @@ pub struct UniversalGetBulkQuery { get, path = "/bulk/u/", responses( - (status = 200, description = "Successfully found name or address.", body = ENSProfile), + (status = 200, description = "Successfully found name or address.", body = BulkResponse), (status = NOT_FOUND, description = "No name or address could be found.", body = ErrorResponse), (status = UNPROCESSABLE_ENTITY, description = "Reverse record not owned by this address.", body = ErrorResponse), ), @@ -67,34 +64,21 @@ pub struct UniversalGetBulkQuery { pub async fn get_bulk( Qs(query): Qs, State(state): State>, -) -> Result>, RouteError> { - let lowercase_queries = query - .queries - .iter() - .map(|input| input.to_lowercase()) - .collect::>(); - - let queries = validate_bulk_input(&lowercase_queries, 10).ok_or_else(|| { - http_error( - StatusCode::BAD_REQUEST, - "input is too long (expected <= 10)", - ) - })?; - - let rpc = state - .provider - .get_provider() - .ok_or_else(|| http_simple_status_error(StatusCode::INTERNAL_SERVER_ERROR))? - .clone(); +) -> Result>, RouteError> { + let queries = validate_bulk_input(&query.queries, 10)?; let profiles = queries .iter() - .map(|input| universal_profile_resolve(input, query.fresh.fresh, rpc.clone(), &state)) + .map(|input| { + state + .service + .resolve_from_name_or_address(input, query.fresh.fresh) + }) .collect::>(); let joined = try_join_all(profiles) .await .map_err(profile_http_error_mapper)?; - Ok(Json(joined)) + Ok(Json(joined.into())) } diff --git a/server/src/state.rs b/server/src/state.rs index a0f6abc..b5c45ee 100644 --- a/server/src/state.rs +++ b/server/src/state.rs @@ -1,23 +1,19 @@ -use enstate_shared::cache::CacheLayer; use std::env; use std::sync::Arc; +use enstate_shared::models::profile::ProfileService; use enstate_shared::models::{ multicoin::cointype::{coins::CoinType, Coins}, records::Records, }; use tracing::info; -use crate::{cache, database, provider}; +use crate::provider::RoundRobin; +use crate::{cache, database}; #[allow(clippy::module_name_repetitions)] pub struct AppState { - pub cache: Arc>, - pub profile_records: Vec, - pub profile_chains: Vec, - pub rpc_urls: Vec, - pub opensea_api_key: String, - pub provider: provider::RoundRobin, + pub service: ProfileService, } impl AppState { @@ -51,18 +47,19 @@ impl AppState { info!("Connected to Redis"); - let provider = provider::RoundRobin::new(rpc_urls.clone()); + let provider = RoundRobin::new(rpc_urls); let opensea_api_key = env::var("OPENSEA_API_KEY").expect("OPENSEA_API_KEY should've been set"); Self { - cache: Arc::new(Box::new(cache::Redis::new(redis))), - profile_records, - profile_chains: multicoin_chains, - opensea_api_key, - rpc_urls, - provider, + service: ProfileService { + cache: Box::new(cache::Redis::new(redis)), + rpc: Box::new(provider), + opensea_api_key, + profile_records: Arc::from(profile_records), + profile_chains: Arc::from(multicoin_chains), + }, } } } diff --git a/shared/src/models/eip155/mod.rs b/shared/src/models/eip155/mod.rs index 2a01c34..d8bf06a 100644 --- a/shared/src/models/eip155/mod.rs +++ b/shared/src/models/eip155/mod.rs @@ -1,6 +1,5 @@ use ethers::middleware::Middleware; -use ethers::providers::{Http, Provider, ProviderError}; -use ethers_ccip_read::CCIPReadMiddleware; +use ethers::providers::ProviderError; use ethers_core::{ abi::{ParamType, Token}, types::{transaction::eip2718::TypedTransaction, Bytes, H160, U256}, @@ -10,6 +9,7 @@ use tracing::info; use crate::models::ipfs::{URLFetchError, OPENSEA_BASE_PREFIX}; use crate::models::multicoin::cointype::evm::ChainId; +use crate::models::profile::CCIPProvider; use super::ipfs::IPFSURLUnparsed; @@ -56,7 +56,7 @@ pub async fn resolve_eip155( contract_type: EIP155ContractType, contract_address: &str, token_id: U256, - provider: &CCIPReadMiddleware>, + provider: &CCIPProvider, opensea_api_key: &str, ) -> Result { let chain_id: u64 = chain_id.into(); diff --git a/shared/src/models/lookup/mod.rs b/shared/src/models/lookup/mod.rs index 766845e..bce15a9 100644 --- a/shared/src/models/lookup/mod.rs +++ b/shared/src/models/lookup/mod.rs @@ -2,8 +2,6 @@ use std::fmt::Display; use std::sync::Arc; use async_trait::async_trait; -use ethers::providers::{Http, Provider}; -use ethers_ccip_read::CCIPReadMiddleware; use ethers_core::abi; use ethers_core::abi::Token; use ethers_core::types::H256; @@ -11,6 +9,7 @@ use lazy_static::lazy_static; use thiserror::Error; use crate::models::eip155::EIP155Error; +use crate::models::profile::CCIPProvider; use super::multicoin::decoding::MulticoinDecoderError; @@ -61,7 +60,7 @@ impl Display for dyn ENSLookup + Send + Sync { } } pub struct LookupState { - pub rpc: Arc>>, + pub rpc: Arc, pub opensea_api_key: String, } diff --git a/shared/src/models/profile/from_address.rs b/shared/src/models/profile/address.rs similarity index 56% rename from shared/src/models/profile/from_address.rs rename to shared/src/models/profile/address.rs index 8f5ac7c..beb68f0 100644 --- a/shared/src/models/profile/from_address.rs +++ b/shared/src/models/profile/address.rs @@ -1,26 +1,22 @@ use ethers::{ - providers::{Http, Middleware, Provider, ProviderError}, + providers::{Middleware, ProviderError}, types::H160, }; -use crate::models::multicoin::cointype::coins::CoinType; +use super::{error::ProfileError, Profile, ProfileService}; -use super::{error::ProfileError, Profile}; - -impl Profile { - pub async fn from_address( +impl ProfileService { + pub async fn resolve_from_address( + &self, address: H160, fresh: bool, - cache: &dyn crate::cache::CacheLayer, - rpc: Provider, - opensea_api_key: &str, - profile_records: &[String], - profile_chains: &[CoinType], - ) -> Result { + ) -> Result { let cache_key = format!("a:{address:?}"); + let rpc = self.rpc.get_instance(); + // Get value from the cache otherwise compute - let name = if let Ok(name) = cache.get(&cache_key).await { + let name = if let Ok(name) = self.cache.get(&cache_key).await { name } else { let result = rpc @@ -34,7 +30,7 @@ impl Profile { })?; // Cache the value, and expire it after 10 minutes - cache + self.cache .set(&cache_key, &result, 600) .await .map_err(|_| ProfileError::Other("cache set failed".to_string()))?; @@ -46,15 +42,6 @@ impl Profile { return Err(ProfileError::NotFound); } - Self::from_name( - &name, - fresh, - cache, - rpc, - opensea_api_key, - profile_records, - profile_chains, - ) - .await + self.resolve_from_name(&name, fresh).await } } diff --git a/shared/src/models/profile/error.rs b/shared/src/models/profile/error.rs index be1196d..017a8a2 100644 --- a/shared/src/models/profile/error.rs +++ b/shared/src/models/profile/error.rs @@ -1,6 +1,7 @@ use ethers::prelude::Http; use ethers::providers::{Provider, ProviderError}; use ethers_ccip_read::CCIPReadMiddlewareError; +use std::sync::Arc; use thiserror::Error; #[allow(clippy::module_name_repetitions)] @@ -13,7 +14,7 @@ pub enum ProfileError { RPCError(#[from] ProviderError), #[error("CCIP error: {0}")] - CCIPError(#[from] CCIPReadMiddlewareError>), + CCIPError(#[from] CCIPReadMiddlewareError>>), #[error("DNS encode error: {0}")] DNSEncodeError(String), diff --git a/shared/src/models/profile/mod.rs b/shared/src/models/profile/mod.rs index 6039f16..71ba63b 100644 --- a/shared/src/models/profile/mod.rs +++ b/shared/src/models/profile/mod.rs @@ -1,12 +1,21 @@ use std::collections::BTreeMap; +use std::sync::Arc; +use ethers::prelude::Http; +use ethers::providers::Provider; +use ethers_ccip_read::CCIPReadMiddleware; use serde::{Deserialize, Serialize}; +use crate::models::multicoin::cointype::coins::CoinType; use crate::utils::eip55::EIP55Address; +use crate::utils::factory::Factory; +pub mod address; pub mod error; -pub mod from_address; -pub mod from_name; +pub mod name; +pub mod universal; + +pub type CCIPProvider = CCIPReadMiddleware>>; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Profile { @@ -36,3 +45,12 @@ pub struct Profile { // Errors encountered while fetching & decoding pub errors: BTreeMap, } + +// name feels very java-esque, consider renaming +pub struct ProfileService { + pub cache: Box, + pub rpc: Box>>>, + pub opensea_api_key: String, + pub profile_records: Arc<[String]>, + pub profile_chains: Arc<[CoinType]>, +} diff --git a/shared/src/models/profile/from_name.rs b/shared/src/models/profile/name.rs similarity index 87% rename from shared/src/models/profile/from_name.rs rename to shared/src/models/profile/name.rs index 31c9e10..fa953df 100644 --- a/shared/src/models/profile/from_name.rs +++ b/shared/src/models/profile/name.rs @@ -2,7 +2,6 @@ use std::str::FromStr; use std::{collections::BTreeMap, sync::Arc}; use ethers::middleware::{Middleware, MiddlewareBuilder}; -use ethers::providers::{Http, Provider}; use ethers_ccip_read::CCIPReadMiddleware; use tracing::info; @@ -10,9 +9,9 @@ use crate::cache::CacheError; use crate::models::lookup::image::Image; use crate::models::lookup::multicoin::Multicoin; use crate::models::lookup::ENSLookupError; +use crate::models::profile::ProfileService; use crate::models::{ lookup::{addr::Addr, text::Text, ENSLookup, LookupState}, - multicoin::cointype::coins::CoinType, profile::Profile, universal_resolver::resolve_universal, }; @@ -21,22 +20,20 @@ use crate::utils::eip55::EIP55Address; use super::error::ProfileError; -impl Profile { - pub async fn from_name( +impl ProfileService { + pub async fn resolve_from_name( + &self, name: &str, fresh: bool, - cache: &dyn crate::cache::CacheLayer, - rpc: Provider, - opensea_api_key: &str, - profile_records: &[String], - profile_chains: &[CoinType], - ) -> Result { + ) -> Result { if !test_domain(name) { return Err(ProfileError::NotFound); } let cache_key = format!("n:{name}"); + let rpc = self.rpc.get_instance(); + let rpc = rpc.wrap_into(CCIPReadMiddleware::new); info!( @@ -49,12 +46,12 @@ impl Profile { // If the value is in the cache, return it if !fresh { - if let Ok(value) = cache.get(&cache_key).await { + if let Ok(value) = self.cache.get(&cache_key).await { if value.is_empty() { return Err(ProfileError::NotFound); } - let entry_result: Result = serde_json::from_str(value.as_str()); + let entry_result: Result = serde_json::from_str(value.as_str()); if let Ok(entry) = entry_result { return Ok(entry); } @@ -83,13 +80,13 @@ impl Profile { // Lookup all Records let record_offset = calldata.len(); - for record in profile_records { + for record in self.profile_records.as_ref() { calldata.push(Text::from(record.as_str()).to_boxed()); } // Lookup all chains let chain_offset = calldata.len(); - for chain in profile_chains { + for chain in self.profile_chains.as_ref() { calldata.push( Multicoin { coin_type: chain.clone(), @@ -121,7 +118,7 @@ impl Profile { let lookup_state = LookupState { rpc, - opensea_api_key: opensea_api_key.to_string(), + opensea_api_key: self.opensea_api_key.clone(), }; // Assume results & calldata have the same length @@ -174,7 +171,7 @@ impl Profile { for (index, value) in results[record_offset..chain_offset].iter().enumerate() { if let Some(value) = value { - records.insert(profile_records[index].clone(), value.to_string()); + records.insert(self.profile_records[index].clone(), value.to_string()); } } @@ -182,11 +179,11 @@ impl Profile { for (index, value) in results[chain_offset..].iter().enumerate() { if let Some(value) = value { - chains.insert(profile_chains[index].to_string(), value.to_string()); + chains.insert(self.profile_chains[index].to_string(), value.to_string()); } } - let value = Self { + let value = Profile { name: name.to_string(), address: address.and_then(|it| EIP55Address::from_str(it.as_str()).ok()), avatar, @@ -203,7 +200,7 @@ impl Profile { let response = serde_json::to_string(&value).map_err(|err| ProfileError::Other(err.to_string()))?; - cache + self.cache .set(&cache_key, &response, 600) .await .map_err(|CacheError::Other(err)| { diff --git a/shared/src/models/profile/universal.rs b/shared/src/models/profile/universal.rs new file mode 100644 index 0000000..1135bd5 --- /dev/null +++ b/shared/src/models/profile/universal.rs @@ -0,0 +1,22 @@ +use ethers::addressbook::Address; + +use super::{error::ProfileError, Profile, ProfileService}; + +impl ProfileService { + pub async fn resolve_from_name_or_address( + &self, + name_or_address: &str, + fresh: bool, + ) -> Result { + if let Ok(address) = name_or_address.parse::
() { + return self.resolve_from_address(address, fresh).await; + } + + if !crate::patterns::test_domain(name_or_address) { + return Err(ProfileError::NotFound); + } + + self.resolve_from_name(&name_or_address.to_lowercase(), fresh) + .await + } +} diff --git a/shared/src/models/universal_resolver/mod.rs b/shared/src/models/universal_resolver/mod.rs index 7a8d3f1..0fcf16a 100644 --- a/shared/src/models/universal_resolver/mod.rs +++ b/shared/src/models/universal_resolver/mod.rs @@ -2,16 +2,17 @@ use std::vec; use ethers::prelude::ProviderError::JsonRpcClientError; use ethers::{ - providers::{namehash, Http, Provider}, + providers::namehash, types::{transaction::eip2718::TypedTransaction, Address, Bytes}, }; -use ethers_ccip_read::{CCIPReadMiddleware, CCIPReadMiddlewareError, CCIPRequest}; +use ethers_ccip_read::{CCIPReadMiddlewareError, CCIPRequest}; use ethers_contract::abigen; use ethers_core::abi; use ethers_core::abi::{ParamType, Token}; use lazy_static::lazy_static; use crate::models::lookup::ENSLookup; +use crate::models::profile::CCIPProvider; use crate::utils::dns::dns_encode; use crate::utils::vec::dedup_ord; @@ -37,7 +38,7 @@ const MAGIC_UNIVERSAL_RESOLVER_ERROR_MESSAGE: &str = pub async fn resolve_universal( name: &str, data: &[Box], - provider: &CCIPReadMiddleware>, + provider: &CCIPProvider, ) -> Result<(Vec>, Address, Vec), ProfileError> { let name_hash = namehash(name); @@ -171,6 +172,7 @@ fn urls_from_request(request: &CCIPRequest) -> Vec { #[cfg(test)] mod tests { use std::str::FromStr; + use std::sync::Arc; use ethers::providers::{Http, Provider}; use ethers_ccip_read::CCIPReadMiddleware; @@ -198,7 +200,7 @@ mod tests { let res = universal_resolver::resolve_universal( "antony.sh", &calldata, - &CCIPReadMiddleware::new(provider), + &CCIPReadMiddleware::new(Arc::new(provider)), ) .await .unwrap(); diff --git a/shared/src/utils/factory.rs b/shared/src/utils/factory.rs new file mode 100644 index 0000000..ceda38c --- /dev/null +++ b/shared/src/utils/factory.rs @@ -0,0 +1,21 @@ +use async_trait::async_trait; + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +pub trait Factory: Send + Sync { + fn get_instance(&self) -> T; +} + +pub struct SimpleFactory(T); + +impl Factory for SimpleFactory { + fn get_instance(&self) -> T { + self.0.clone() + } +} + +impl From for SimpleFactory { + fn from(value: T) -> Self { + SimpleFactory(value) + } +} diff --git a/shared/src/utils/mod.rs b/shared/src/utils/mod.rs index 457a5c4..0623444 100644 --- a/shared/src/utils/mod.rs +++ b/shared/src/utils/mod.rs @@ -1,4 +1,5 @@ pub mod dns; pub mod eip55; +pub mod factory; pub mod sha256; pub mod vec; diff --git a/worker/src/kv_cache.rs b/worker/src/kv_cache.rs index 019aecc..17c5964 100644 --- a/worker/src/kv_cache.rs +++ b/worker/src/kv_cache.rs @@ -1,4 +1,4 @@ -use std::sync::Arc; +use std::rc::Rc; use async_trait::async_trait; use enstate_shared::cache::{CacheError, CacheLayer}; @@ -10,11 +10,11 @@ use wasm_bindgen_futures::JsFuture; use worker::Env; pub struct CloudflareKVCache { - ctx: Arc, + ctx: Rc, } impl CloudflareKVCache { - pub fn new(ctx: Arc) -> Self { + pub fn new(ctx: Rc) -> Self { Self { ctx } } } @@ -128,3 +128,15 @@ impl CacheLayer for CloudflareKVCache { Ok(()) } } + +#[cfg(not(target_arch = "wasm32"))] +#[async_trait] +impl CacheLayer for CloudflareKVCache { + async fn get(&self, key: &str) -> Result { + unreachable!() + } + + async fn set(&self, key: &str, value: &str, expires: u32) -> Result<(), CacheError> { + unreachable!() + } +} diff --git a/worker/src/lookup.rs b/worker/src/lookup.rs index e26ed28..71a5127 100644 --- a/worker/src/lookup.rs +++ b/worker/src/lookup.rs @@ -1,10 +1,11 @@ +use std::rc::Rc; use std::sync::Arc; use enstate_shared::cache::CacheLayer; -use enstate_shared::models::multicoin::cointype::coins::CoinType; use enstate_shared::models::profile::error::ProfileError; -use enstate_shared::models::{multicoin::cointype::Coins, profile::Profile, records::Records}; -use ethers::types::Address; +use enstate_shared::models::profile::ProfileService; +use enstate_shared::models::{multicoin::cointype::Coins, records::Records}; +use enstate_shared::utils::factory::SimpleFactory; use ethers::{ providers::{Http, Provider}, types::H160, @@ -26,7 +27,7 @@ pub enum LookupType { impl From for LookupType { fn from(path: String) -> Self { - let split: Vec<&str> = path.split("/").filter(|it| !it.is_empty()).collect(); + let split: Vec<&str> = path.split('/').filter(|it| !it.is_empty()).collect(); if split.len() < 2 { return LookupType::Unknown; @@ -54,18 +55,18 @@ impl LookupType { env: Env, opensea_api_key: &str, ) -> Result { - let arc_env = Arc::new(env); + let rc_env = Rc::new(env); - let cache = Box::new(CloudflareKVCache::new(arc_env.clone())); + let cache: Box = Box::new(CloudflareKVCache::new(rc_env.clone())); let profile_records = Records::default().records; let profile_chains = Coins::default().coins; - let rpc_url = arc_env + let rpc_url = rc_env + .clone() .var("RPC_URL") .map(|x| x.to_string()) .unwrap_or("https://rpc.enstate.rs/v1/mainnet".to_string()); - // TODO: env let rpc = Provider::::try_from(rpc_url) .map_err(|_| Response::error("RPC Failure", 500).unwrap())?; @@ -73,10 +74,15 @@ impl LookupType { .url() .map_err(|_| Response::error("Worker error", 500).unwrap())?; let query = querystring::querify(url.query().unwrap_or("")); - let fresh = query - .into_iter() - .find(|(k, v)| *k == "fresh" && *v == "true") - .is_some(); + let fresh = query.into_iter().any(|(k, v)| k == "fresh" && v == "true"); + + let service = ProfileService { + cache, + rpc: Box::new(SimpleFactory::from(Arc::new(rpc))), + opensea_api_key: opensea_api_key.to_string(), + profile_records: Arc::from(profile_records), + profile_chains: Arc::from(profile_chains), + }; match self { LookupType::Unknown => Ok(Response::from_json(&ErrorResponse { @@ -87,17 +93,10 @@ impl LookupType { .with_status(404)), LookupType::ImageLookup(name_or_address) | LookupType::HeaderLookup(name_or_address) => { - let profile = universal_profile_resolve( - name_or_address, - fresh, - cache, - rpc, - &opensea_api_key, - &profile_records, - &profile_chains, - ) - .await - .map_err(profile_http_error_mapper)?; + let profile = service + .resolve_from_name_or_address(name_or_address, fresh) + .await + .map_err(profile_http_error_mapper)?; let field = match self { LookupType::ImageLookup(_) => profile.avatar, @@ -120,48 +119,19 @@ impl LookupType { } _ => { let profile = match self { - LookupType::NameLookup(name) => { - Profile::from_name( - name, - fresh, - cache, - rpc, - &opensea_api_key, - &profile_records, - &profile_chains, - ) - .await - } + LookupType::NameLookup(name) => service.resolve_from_name(name, fresh).await, LookupType::AddressLookup(address) => { let address = address.parse::(); match address { - Ok(address) => { - Profile::from_address( - address, - fresh, - cache, - rpc, - &opensea_api_key, - &profile_records, - &profile_chains, - ) - .await - } + Ok(address) => service.resolve_from_address(address, fresh).await, Err(_) => Err(ProfileError::NotFound), } } LookupType::NameOrAddressLookup(name_or_address) => { - universal_profile_resolve( - name_or_address, - fresh, - cache, - rpc, - &opensea_api_key, - &profile_records, - &profile_chains, - ) - .await + service + .resolve_from_name_or_address(name_or_address, fresh) + .await } _ => unreachable!(), } @@ -173,42 +143,3 @@ impl LookupType { } } } - -async fn universal_profile_resolve( - name_or_address: &str, - fresh: bool, - cache: Box, - rpc: Provider, - opensea_api_key: &str, - profile_records: &[String], - profile_chains: &[CoinType], -) -> Result { - let address_option: Option
= name_or_address.parse().ok(); - - match address_option { - Some(address) => { - Profile::from_address( - address, - fresh, - cache, - rpc, - opensea_api_key, - profile_records, - profile_chains, - ) - .await - } - None => { - Profile::from_name( - &name_or_address.to_lowercase(), - fresh, - cache, - rpc, - opensea_api_key, - profile_records, - profile_chains, - ) - .await - } - } -} From 1f0368fd78007d68bcb5ed0f767564dac4c152d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20F=2E=20=C5=A0tignjedec?= Date: Thu, 21 Dec 2023 18:02:51 +0100 Subject: [PATCH 7/9] Temp fix for worker --- shared/src/cache/mod.rs | 2 ++ worker/src/lib.rs | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/shared/src/cache/mod.rs b/shared/src/cache/mod.rs index 8aebcea..c506bec 100644 --- a/shared/src/cache/mod.rs +++ b/shared/src/cache/mod.rs @@ -1,3 +1,5 @@ +use std::fmt::Debug; + use async_trait::async_trait; #[derive(Debug)] diff --git a/worker/src/lib.rs b/worker/src/lib.rs index 900cb67..3bdb5e9 100644 --- a/worker/src/lib.rs +++ b/worker/src/lib.rs @@ -32,7 +32,7 @@ async fn main_handler(req: Request, ctx: RouteContext<()>) -> worker::Result worker::Result { Router::new() .get("/", |_, _| root_handler().with_cors(&CORS)) - .get_async("/*", main_handler) + .get_async("/:method/:param", main_handler) .run(req, env) .await } From 1314cbeabfceff81bbe15d6599811ef640045103 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20F=2E=20=C5=A0tignjedec?= Date: Sat, 23 Dec 2023 15:20:10 +0100 Subject: [PATCH 8/9] Reimplement worker routing --- server/src/http.rs | 5 +- worker/Cargo.lock | 19 +++-- worker/Cargo.toml | 2 +- worker/src/http_util.rs | 64 ++++++++++++--- worker/src/kv_cache.rs | 14 +--- worker/src/lib.rs | 78 ++++++++++++++---- worker/src/lookup.rs | 145 --------------------------------- worker/src/routes/address.rs | 28 +++++++ worker/src/routes/header.rs | 27 ++++++ worker/src/routes/image.rs | 27 ++++++ worker/src/routes/mod.rs | 5 ++ worker/src/routes/name.rs | 23 ++++++ worker/src/routes/universal.rs | 23 ++++++ 13 files changed, 264 insertions(+), 196 deletions(-) delete mode 100644 worker/src/lookup.rs create mode 100644 worker/src/routes/address.rs create mode 100644 worker/src/routes/header.rs create mode 100644 worker/src/routes/image.rs create mode 100644 worker/src/routes/mod.rs create mode 100644 worker/src/routes/name.rs create mode 100644 worker/src/routes/universal.rs diff --git a/server/src/http.rs b/server/src/http.rs index 663c93a..80fd9cc 100644 --- a/server/src/http.rs +++ b/server/src/http.rs @@ -10,9 +10,9 @@ use tracing::info; use utoipa::OpenApi; use utoipa_swagger_ui::SwaggerUi; +use crate::models::bulk::BulkResponse; use crate::models::error::ErrorResponse; use crate::models::profile::ENSProfile; -use crate::models::bulk::BulkResponse; use crate::routes; use crate::state::AppState; @@ -60,12 +60,9 @@ pub fn setup(state: AppState) -> App { .directory_route("/u/:name_or_address", get(routes::universal::get)) .directory_route("/i/:name_or_address", get(routes::image::get)) .directory_route("/h/:name_or_address", get(routes::header::get)) - // TODO (@antony1060): make better .directory_route("/bulk/a", get(routes::address::get_bulk)) .directory_route("/bulk/n", get(routes::name::get_bulk)) .directory_route("/bulk/u", get(routes::universal::get_bulk)) - // .directory_route("/bulk/i", get(routes::image::get_bulk)) - // .directory_route("/bulk/h", get(routes::header::get_bulk)) .fallback(routes::four_oh_four::handler) .layer(CorsLayer::permissive()) .layer(TraceLayer::new_for_http()) diff --git a/worker/Cargo.lock b/worker/Cargo.lock index 38289ec..840b319 100644 --- a/worker/Cargo.lock +++ b/worker/Cargo.lock @@ -921,10 +921,10 @@ dependencies = [ "lazy_static", "log", "once_cell", - "querystring", "serde", "serde-wasm-bindgen 0.6.0", "serde_json", + "serde_qs", "thiserror", "wasm-bindgen", "wasm-bindgen-futures", @@ -2671,12 +2671,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "querystring" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9318ead08c799aad12a55a3e78b82e0b6167271ffd1f627b758891282f739187" - [[package]] name = "quote" version = "1.0.33" @@ -3181,6 +3175,17 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_qs" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0431a35568651e363364210c91983c1da5eb29404d9f0928b67d4ebcfa7d330c" +dependencies = [ + "percent-encoding", + "serde", + "thiserror", +] + [[package]] name = "serde_spanned" version = "0.6.4" diff --git a/worker/Cargo.toml b/worker/Cargo.toml index 6edbb24..ef66791 100644 --- a/worker/Cargo.toml +++ b/worker/Cargo.toml @@ -35,10 +35,10 @@ serde = "1.0.189" once_cell = "1.18.0" js-sys = "*" serde-wasm-bindgen = "0.6.0" -querystring = "1.1.0" thiserror = "1.0.50" http = "1.0.0" lazy_static = "1.4.0" +serde_qs = "0.12.0" [build-dependencies] chrono = "0.4.31" diff --git a/worker/src/http_util.rs b/worker/src/http_util.rs index b0af582..003f0e5 100644 --- a/worker/src/http_util.rs +++ b/worker/src/http_util.rs @@ -1,8 +1,31 @@ use enstate_shared::models::profile::error::ProfileError; use ethers::prelude::ProviderError; use http::status::StatusCode; -use serde::Serialize; -use worker::Response; +use serde::de::DeserializeOwned; +use serde::{Deserialize, Deserializer, Serialize}; +use worker::{Error, Request, Response, Url}; + +#[derive(Deserialize)] +pub struct FreshQuery { + #[serde(default, deserialize_with = "bool_or_false")] + pub(crate) fresh: bool, +} + +#[allow(clippy::unnecessary_wraps)] +fn bool_or_false<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let value: Result = Deserialize::deserialize(deserializer); + Ok(value.map(|it| it == "true").unwrap_or(false)) +} + +pub fn parse_query(req: &Request) -> worker::Result { + let url = req.url()?; + let query = url.query().unwrap_or(""); + + serde_qs::from_str::(query).map_err(|_| http_simple_status_error(StatusCode::BAD_REQUEST)) +} #[derive(Serialize)] pub struct ErrorResponse { @@ -10,7 +33,15 @@ pub struct ErrorResponse { pub(crate) error: String, } -pub fn profile_http_error_mapper(err: ProfileError) -> Response { +impl From for Error { + fn from(value: ErrorResponse) -> Self { + let json = serde_json::to_string(&value).expect("error should be json serializable"); + + Self::Json((json, value.status)) + } +} + +pub fn profile_http_error_mapper(err: ProfileError) -> Error { let status = match err { ProfileError::NotFound => StatusCode::NOT_FOUND, ProfileError::CCIPError(_) => StatusCode::BAD_GATEWAY, @@ -18,22 +49,31 @@ pub fn profile_http_error_mapper(err: ProfileError) -> Response { _ => StatusCode::INTERNAL_SERVER_ERROR, }; - Response::from_json(&ErrorResponse { + ErrorResponse { status: status.as_u16(), error: err.to_string(), - }) - .expect("from_json should've succeeded") - .with_status(status.as_u16()) + } + .into() } -pub fn http_simple_status_error(status: StatusCode) -> Response { - Response::from_json(&ErrorResponse { +pub fn http_simple_status_error(status: StatusCode) -> Error { + ErrorResponse { status: status.as_u16(), error: status .canonical_reason() .unwrap_or("Unknown error") .to_string(), - }) - .expect("from_json shoud've succeeded") - .with_status(status.as_u16()) + } + .into() +} + +pub fn redirect_url(url: &str) -> worker::Result { + let url = Url::parse(url).map_err(|_| { + worker::Error::from(ErrorResponse { + status: StatusCode::NOT_ACCEPTABLE.as_u16(), + error: "invalid avatar url".to_string(), + }) + })?; + + Response::redirect(url) } diff --git a/worker/src/kv_cache.rs b/worker/src/kv_cache.rs index 17c5964..04982b6 100644 --- a/worker/src/kv_cache.rs +++ b/worker/src/kv_cache.rs @@ -1,5 +1,3 @@ -use std::rc::Rc; - use async_trait::async_trait; use enstate_shared::cache::{CacheError, CacheLayer}; use js_sys::{Function, Promise, Reflect}; @@ -10,13 +8,7 @@ use wasm_bindgen_futures::JsFuture; use worker::Env; pub struct CloudflareKVCache { - ctx: Rc, -} - -impl CloudflareKVCache { - pub fn new(ctx: Rc) -> Self { - Self { ctx } - } + pub(crate) env: Env, } unsafe impl Send for CloudflareKVCache {} @@ -68,7 +60,7 @@ impl From for CacheError { #[async_trait(?Send)] impl CacheLayer for CloudflareKVCache { async fn get(&self, key: &str) -> Result { - let kv_store = get_js(&self.ctx, "enstate-1")?; + let kv_store = get_js(&self.env, "enstate-1")?; let get_function_value = get_js(&kv_store, "get")?; let get_function = get_function_value @@ -94,7 +86,7 @@ impl CacheLayer for CloudflareKVCache { } async fn set(&self, key: &str, value: &str, expires: u32) -> Result<(), CacheError> { - let kv_store = get_js(&self.ctx, "enstate-1")?; + let kv_store = get_js(&self.env, "enstate-1")?; let put_function_value = get_js(&kv_store, "put")?; let put_function = put_function_value diff --git a/worker/src/lib.rs b/worker/src/lib.rs index 3bdb5e9..cf878ee 100644 --- a/worker/src/lib.rs +++ b/worker/src/lib.rs @@ -1,12 +1,22 @@ +use std::sync::Arc; + +use enstate_shared::cache::CacheLayer; use enstate_shared::meta::gen_app_meta; +use enstate_shared::models::multicoin::cointype::Coins; +use enstate_shared::models::profile::ProfileService; +use enstate_shared::models::records::Records; +use enstate_shared::utils::factory::SimpleFactory; +use ethers::prelude::{Http, Provider}; +use http::StatusCode; use lazy_static::lazy_static; -use worker::{event, Context, Cors, Env, Method, Request, Response, RouteContext, Router}; +use worker::{event, Context, Cors, Env, Method, Request, Response, Router}; -use crate::lookup::LookupType; +use crate::http_util::http_simple_status_error; +use crate::kv_cache::CloudflareKVCache; mod http_util; mod kv_cache; -mod lookup; +mod routes; lazy_static! { static ref CORS: Cors = Cors::default() @@ -14,27 +24,63 @@ lazy_static! { .with_methods(Method::all()); } -async fn main_handler(req: Request, ctx: RouteContext<()>) -> worker::Result { - let opensea_api_key = ctx - .env +#[event(fetch, respond_with_errors)] +async fn main(req: Request, env: Env, _ctx: Context) -> worker::Result { + let opensea_api_key = env .var("OPENSEA_API_KEY") .expect("OPENSEA_API_KEY should've been set") .to_string(); - LookupType::from(req.path()) - .process(req, ctx.env, &opensea_api_key) - .await - .unwrap_or_else(|f| f) - .with_cors(&CORS) -} + let cache: Box = Box::new(CloudflareKVCache { + env: Env::from(env.clone()), + }); + let profile_records = Records::default().records; + let profile_chains = Coins::default().coins; -#[event(fetch, respond_with_errors)] -async fn main(req: Request, env: Env, _ctx: Context) -> worker::Result { - Router::new() + let rpc_url = env + .var("RPC_URL") + .map(|x| x.to_string()) + .unwrap_or("https://rpc.enstate.rs/v1/mainnet".to_string()); + + let rpc = Provider::::try_from(rpc_url) + .map_err(|_| http_simple_status_error(StatusCode::BAD_REQUEST))?; + + let service = ProfileService { + cache, + rpc: Box::new(SimpleFactory::from(Arc::new(rpc))), + opensea_api_key: opensea_api_key.to_string(), + profile_records: Arc::from(profile_records), + profile_chains: Arc::from(profile_chains), + }; + + // TODO (@antony1060): I don't like this, there needs to be a better way + // also, very not efficient in worker context + let response = Router::with_data(service) .get("/", |_, _| root_handler().with_cors(&CORS)) - .get_async("/:method/:param", main_handler) + .get_async("/a/:address", routes::address::get) + .get_async("/n/:name", routes::name::get) + .get_async("/u/:name_or_address", routes::universal::get) + .get_async("/i/:name_or_address", routes::image::get) + .get_async("/h/:name_or_address", routes::header::get) + // .get_async("/bulk/a", main_handler) + // .get_async("/bulk/n", main_handler) + // .get_async("/bulk/u", main_handler) + // .or_else_any_method("*", |_, _| { + // Err(ErrorResponse { + // status: StatusCode::NOT_FOUND.as_u16(), + // error: "Unknown route".to_string(), + // } + // .into()) + // }) .run(req, env) .await + .and_then(|response| response.with_cors(&CORS)); + + if let Err(worker::Error::Json(err)) = response { + return Response::error(err.0, err.1); + } + + response } fn root_handler() -> Response { diff --git a/worker/src/lookup.rs b/worker/src/lookup.rs deleted file mode 100644 index 71a5127..0000000 --- a/worker/src/lookup.rs +++ /dev/null @@ -1,145 +0,0 @@ -use std::rc::Rc; -use std::sync::Arc; - -use enstate_shared::cache::CacheLayer; -use enstate_shared::models::profile::error::ProfileError; -use enstate_shared::models::profile::ProfileService; -use enstate_shared::models::{multicoin::cointype::Coins, records::Records}; -use enstate_shared::utils::factory::SimpleFactory; -use ethers::{ - providers::{Http, Provider}, - types::H160, -}; -use http::StatusCode; -use worker::{Env, Request, Response, Url}; - -use crate::http_util::{http_simple_status_error, profile_http_error_mapper, ErrorResponse}; -use crate::kv_cache::CloudflareKVCache; - -pub enum LookupType { - NameLookup(String), - AddressLookup(String), - NameOrAddressLookup(String), - ImageLookup(String), - HeaderLookup(String), - Unknown, -} - -impl From for LookupType { - fn from(path: String) -> Self { - let split: Vec<&str> = path.split('/').filter(|it| !it.is_empty()).collect(); - - if split.len() < 2 { - return LookupType::Unknown; - } - - let [op, arg] = split[0..2] else { - return LookupType::Unknown; - }; - - match op { - "n" => LookupType::NameLookup(arg.to_string()), - "a" => LookupType::AddressLookup(arg.to_string()), - "u" => LookupType::NameOrAddressLookup(arg.to_string()), - "i" => LookupType::ImageLookup(arg.to_string()), - "h" => LookupType::HeaderLookup(arg.to_string()), - _ => LookupType::Unknown, - } - } -} - -impl LookupType { - pub async fn process( - &self, - req: Request, - env: Env, - opensea_api_key: &str, - ) -> Result { - let rc_env = Rc::new(env); - - let cache: Box = Box::new(CloudflareKVCache::new(rc_env.clone())); - let profile_records = Records::default().records; - let profile_chains = Coins::default().coins; - - let rpc_url = rc_env - .clone() - .var("RPC_URL") - .map(|x| x.to_string()) - .unwrap_or("https://rpc.enstate.rs/v1/mainnet".to_string()); - - let rpc = Provider::::try_from(rpc_url) - .map_err(|_| Response::error("RPC Failure", 500).unwrap())?; - - let url = req - .url() - .map_err(|_| Response::error("Worker error", 500).unwrap())?; - let query = querystring::querify(url.query().unwrap_or("")); - let fresh = query.into_iter().any(|(k, v)| k == "fresh" && v == "true"); - - let service = ProfileService { - cache, - rpc: Box::new(SimpleFactory::from(Arc::new(rpc))), - opensea_api_key: opensea_api_key.to_string(), - profile_records: Arc::from(profile_records), - profile_chains: Arc::from(profile_chains), - }; - - match self { - LookupType::Unknown => Ok(Response::from_json(&ErrorResponse { - status: 404, - error: "Unknown route".to_string(), - }) - .unwrap() - .with_status(404)), - LookupType::ImageLookup(name_or_address) - | LookupType::HeaderLookup(name_or_address) => { - let profile = service - .resolve_from_name_or_address(name_or_address, fresh) - .await - .map_err(profile_http_error_mapper)?; - - let field = match self { - LookupType::ImageLookup(_) => profile.avatar, - LookupType::HeaderLookup(_) => profile.header, - _ => unreachable!(), - }; - - let Some(img) = field else { - return Err(http_simple_status_error(StatusCode::NOT_FOUND)); - }; - - let url = Url::parse(img.as_str()).map_err(|_| { - Response::error("Invalid avatar URL", StatusCode::NOT_ACCEPTABLE.as_u16()) - .expect("status should be in correct range") - })?; - - Ok(Response::redirect(url).map_err(|_| { - Response::error("Worker error", 500).expect("status should be in correct range") - })?) - } - _ => { - let profile = match self { - LookupType::NameLookup(name) => service.resolve_from_name(name, fresh).await, - LookupType::AddressLookup(address) => { - let address = address.parse::(); - - match address { - Ok(address) => service.resolve_from_address(address, fresh).await, - Err(_) => Err(ProfileError::NotFound), - } - } - LookupType::NameOrAddressLookup(name_or_address) => { - service - .resolve_from_name_or_address(name_or_address, fresh) - .await - } - _ => unreachable!(), - } - .map_err(profile_http_error_mapper)?; - - Response::from_json(&profile) - .map_err(|_| http_simple_status_error(StatusCode::INTERNAL_SERVER_ERROR)) - } - } - } -} diff --git a/worker/src/routes/address.rs b/worker/src/routes/address.rs new file mode 100644 index 0000000..b6e3dff --- /dev/null +++ b/worker/src/routes/address.rs @@ -0,0 +1,28 @@ +use enstate_shared::models::profile::ProfileService; +use ethers::addressbook::Address; +use http::StatusCode; +use worker::{Request, Response, RouteContext}; + +use crate::http_util::{ + http_simple_status_error, parse_query, profile_http_error_mapper, FreshQuery, +}; + +pub async fn get(req: Request, ctx: RouteContext) -> worker::Result { + let query: FreshQuery = parse_query(&req)?; + + let address = ctx + .param("address") + .ok_or_else(|| http_simple_status_error(StatusCode::BAD_REQUEST))?; + + let address: Address = address + .parse() + .map_err(|_| http_simple_status_error(StatusCode::BAD_REQUEST))?; + + let profile = ctx + .data + .resolve_from_address(address, query.fresh) + .await + .map_err(profile_http_error_mapper)?; + + Response::from_json(&profile) +} diff --git a/worker/src/routes/header.rs b/worker/src/routes/header.rs new file mode 100644 index 0000000..7d141d5 --- /dev/null +++ b/worker/src/routes/header.rs @@ -0,0 +1,27 @@ +use enstate_shared::models::profile::ProfileService; +use http::StatusCode; +use worker::{Request, Response, RouteContext}; + +use crate::http_util::{ + http_simple_status_error, parse_query, profile_http_error_mapper, redirect_url, FreshQuery, +}; + +pub async fn get(req: Request, ctx: RouteContext) -> worker::Result { + let query: FreshQuery = parse_query(&req)?; + + let name_or_address = ctx + .param("name_or_address") + .ok_or_else(|| http_simple_status_error(StatusCode::BAD_REQUEST))?; + + let profile = ctx + .data + .resolve_from_name_or_address(name_or_address, query.fresh) + .await + .map_err(profile_http_error_mapper)?; + + let Some(header) = profile.header else { + return Err(http_simple_status_error(StatusCode::NOT_FOUND)); + }; + + redirect_url(&header) +} diff --git a/worker/src/routes/image.rs b/worker/src/routes/image.rs new file mode 100644 index 0000000..85f28ee --- /dev/null +++ b/worker/src/routes/image.rs @@ -0,0 +1,27 @@ +use enstate_shared::models::profile::ProfileService; +use http::StatusCode; +use worker::{Request, Response, RouteContext}; + +use crate::http_util::{ + http_simple_status_error, parse_query, profile_http_error_mapper, redirect_url, FreshQuery, +}; + +pub async fn get(req: Request, ctx: RouteContext) -> worker::Result { + let query: FreshQuery = parse_query(&req)?; + + let name_or_address = ctx + .param("name_or_address") + .ok_or_else(|| http_simple_status_error(StatusCode::BAD_REQUEST))?; + + let profile = ctx + .data + .resolve_from_name_or_address(name_or_address, query.fresh) + .await + .map_err(profile_http_error_mapper)?; + + let Some(avatar) = profile.avatar else { + return Err(http_simple_status_error(StatusCode::NOT_FOUND)); + }; + + redirect_url(&avatar) +} diff --git a/worker/src/routes/mod.rs b/worker/src/routes/mod.rs new file mode 100644 index 0000000..23fc02b --- /dev/null +++ b/worker/src/routes/mod.rs @@ -0,0 +1,5 @@ +pub mod address; +pub mod header; +pub mod image; +pub mod name; +pub mod universal; diff --git a/worker/src/routes/name.rs b/worker/src/routes/name.rs new file mode 100644 index 0000000..61b9a48 --- /dev/null +++ b/worker/src/routes/name.rs @@ -0,0 +1,23 @@ +use enstate_shared::models::profile::ProfileService; +use http::StatusCode; +use worker::{Request, Response, RouteContext}; + +use crate::http_util::{ + http_simple_status_error, parse_query, profile_http_error_mapper, FreshQuery, +}; + +pub async fn get(req: Request, ctx: RouteContext) -> worker::Result { + let query: FreshQuery = parse_query(&req)?; + + let name = ctx + .param("name") + .ok_or_else(|| http_simple_status_error(StatusCode::BAD_REQUEST))?; + + let profile = ctx + .data + .resolve_from_name(name, query.fresh) + .await + .map_err(profile_http_error_mapper)?; + + Response::from_json(&profile) +} diff --git a/worker/src/routes/universal.rs b/worker/src/routes/universal.rs new file mode 100644 index 0000000..c0efde0 --- /dev/null +++ b/worker/src/routes/universal.rs @@ -0,0 +1,23 @@ +use enstate_shared::models::profile::ProfileService; +use http::StatusCode; +use worker::{Request, Response, RouteContext}; + +use crate::http_util::{ + http_simple_status_error, parse_query, profile_http_error_mapper, FreshQuery, +}; + +pub async fn get(req: Request, ctx: RouteContext) -> worker::Result { + let query: FreshQuery = parse_query(&req)?; + + let name_or_address = ctx + .param("name_or_address") + .ok_or_else(|| http_simple_status_error(StatusCode::BAD_REQUEST))?; + + let profile = ctx + .data + .resolve_from_name_or_address(name_or_address, query.fresh) + .await + .map_err(profile_http_error_mapper)?; + + Response::from_json(&profile) +} From 8e869e58c57bfa53ddc22f27283f9729460a8213 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20F=2E=20=C5=A0tignjedec?= Date: Sat, 23 Dec 2023 16:27:08 +0100 Subject: [PATCH 9/9] Implement bulk endpoints for worker --- server/src/routes/mod.rs | 2 ++ worker/Cargo.lock | 1 + worker/Cargo.toml | 1 + worker/src/http_util.rs | 38 ++++++++++++++++++++++++++++++++++ worker/src/lib.rs | 36 ++++++++++++++++++++------------ worker/src/routes/address.rs | 36 +++++++++++++++++++++++++++++++- worker/src/routes/name.rs | 30 ++++++++++++++++++++++++++- worker/src/routes/universal.rs | 33 ++++++++++++++++++++++++++++- 8 files changed, 161 insertions(+), 16 deletions(-) diff --git a/server/src/routes/mod.rs b/server/src/routes/mod.rs index 4df6456..282608a 100644 --- a/server/src/routes/mod.rs +++ b/server/src/routes/mod.rs @@ -18,6 +18,8 @@ pub mod name; pub mod root; pub mod universal; +// TODO (@antony1060): cleanup file + #[derive(Deserialize)] pub struct FreshQuery { #[serde(default, deserialize_with = "bool_or_false")] diff --git a/worker/Cargo.lock b/worker/Cargo.lock index 840b319..37c1daa 100644 --- a/worker/Cargo.lock +++ b/worker/Cargo.lock @@ -915,6 +915,7 @@ dependencies = [ "chrono", "enstate_shared", "ethers", + "futures-util", "getrandom", "http 1.0.0", "js-sys", diff --git a/worker/Cargo.toml b/worker/Cargo.toml index ef66791..bf52ca3 100644 --- a/worker/Cargo.toml +++ b/worker/Cargo.toml @@ -39,6 +39,7 @@ thiserror = "1.0.50" http = "1.0.0" lazy_static = "1.4.0" serde_qs = "0.12.0" +futures-util = "0.3.29" [build-dependencies] chrono = "0.4.31" diff --git a/worker/src/http_util.rs b/worker/src/http_util.rs index 003f0e5..3a698a7 100644 --- a/worker/src/http_util.rs +++ b/worker/src/http_util.rs @@ -1,10 +1,14 @@ use enstate_shared::models::profile::error::ProfileError; +use enstate_shared::utils::vec::dedup_ord; use ethers::prelude::ProviderError; use http::status::StatusCode; use serde::de::DeserializeOwned; use serde::{Deserialize, Deserializer, Serialize}; +use thiserror::Error; use worker::{Error, Request, Response, Url}; +// TODO (@antony1060): cleanup file + #[derive(Deserialize)] pub struct FreshQuery { #[serde(default, deserialize_with = "bool_or_false")] @@ -27,6 +31,40 @@ pub fn parse_query(req: &Request) -> worker::Result { serde_qs::from_str::(query).map_err(|_| http_simple_status_error(StatusCode::BAD_REQUEST)) } +#[derive(Error, Debug)] +pub enum ValidationError { + #[error("maximum input length exceeded (expected at most {0})")] + MaxLengthExceeded(usize), +} + +impl From for worker::Error { + fn from(value: ValidationError) -> Self { + ErrorResponse { + status: StatusCode::BAD_REQUEST.as_u16(), + error: value.to_string(), + } + .into() + } +} + +pub fn validate_bulk_input( + input: &[String], + max_len: usize, +) -> Result, ValidationError> { + let unique = dedup_ord( + &input + .iter() + .map(|entry| entry.to_lowercase()) + .collect::>(), + ); + + if unique.len() > max_len { + return Err(ValidationError::MaxLengthExceeded(max_len)); + } + + Ok(unique) +} + #[derive(Serialize)] pub struct ErrorResponse { pub(crate) status: u16, diff --git a/worker/src/lib.rs b/worker/src/lib.rs index cf878ee..04845e0 100644 --- a/worker/src/lib.rs +++ b/worker/src/lib.rs @@ -9,7 +9,7 @@ use enstate_shared::utils::factory::SimpleFactory; use ethers::prelude::{Http, Provider}; use http::StatusCode; use lazy_static::lazy_static; -use worker::{event, Context, Cors, Env, Method, Request, Response, Router}; +use worker::{event, Context, Cors, Env, Headers, Method, Request, Response, Router}; use crate::http_util::http_simple_status_error; use crate::kv_cache::CloudflareKVCache; @@ -62,22 +62,32 @@ async fn main(req: Request, env: Env, _ctx: Context) -> worker::Result .get_async("/u/:name_or_address", routes::universal::get) .get_async("/i/:name_or_address", routes::image::get) .get_async("/h/:name_or_address", routes::header::get) - // .get_async("/bulk/a", main_handler) - // .get_async("/bulk/n", main_handler) - // .get_async("/bulk/u", main_handler) - // .or_else_any_method("*", |_, _| { - // Err(ErrorResponse { - // status: StatusCode::NOT_FOUND.as_u16(), - // error: "Unknown route".to_string(), - // } - // .into()) - // }) + .get_async("/bulk/a", routes::address::get_bulk) + .get_async("/bulk/n", routes::name::get_bulk) + .get_async("/bulk/u", routes::universal::get_bulk) .run(req, env) .await .and_then(|response| response.with_cors(&CORS)); - if let Err(worker::Error::Json(err)) = response { - return Response::error(err.0, err.1); + if let Err(err) = response { + if let worker::Error::Json(json) = err { + return Response::error(json.0, json.1).and_then(|response| { + response + .with_headers(Headers::from_iter( + [("Content-Type", "application/json")].iter(), + )) + .with_cors(&CORS) + }); + } + + return Response::error(err.to_string(), StatusCode::INTERNAL_SERVER_ERROR.as_u16()) + .and_then(|response| { + response + .with_headers(Headers::from_iter( + [("Content-Type", "application/json")].iter(), + )) + .with_cors(&CORS) + }); } response diff --git a/worker/src/routes/address.rs b/worker/src/routes/address.rs index b6e3dff..91b347d 100644 --- a/worker/src/routes/address.rs +++ b/worker/src/routes/address.rs @@ -1,10 +1,13 @@ use enstate_shared::models::profile::ProfileService; use ethers::addressbook::Address; +use futures_util::future::try_join_all; use http::StatusCode; +use serde::Deserialize; use worker::{Request, Response, RouteContext}; use crate::http_util::{ - http_simple_status_error, parse_query, profile_http_error_mapper, FreshQuery, + http_simple_status_error, parse_query, profile_http_error_mapper, validate_bulk_input, + FreshQuery, }; pub async fn get(req: Request, ctx: RouteContext) -> worker::Result { @@ -26,3 +29,34 @@ pub async fn get(req: Request, ctx: RouteContext) -> worker::Res Response::from_json(&profile) } + +#[derive(Deserialize)] +pub struct AddressGetBulkQuery { + addresses: Vec, + + #[serde(flatten)] + fresh: FreshQuery, +} + +pub async fn get_bulk(req: Request, ctx: RouteContext) -> worker::Result { + let query: AddressGetBulkQuery = parse_query(&req)?; + + let addresses = validate_bulk_input(&query.addresses, 10)?; + + let addresses = addresses + .iter() + .map(|address| address.parse::
()) + .collect::, _>>() + .map_err(|_| http_simple_status_error(StatusCode::BAD_REQUEST))?; + + let profiles = addresses + .iter() + .map(|address| ctx.data.resolve_from_address(*address, query.fresh.fresh)) + .collect::>(); + + let joined = try_join_all(profiles) + .await + .map_err(profile_http_error_mapper)?; + + Response::from_json(&joined) +} diff --git a/worker/src/routes/name.rs b/worker/src/routes/name.rs index 61b9a48..0cde98f 100644 --- a/worker/src/routes/name.rs +++ b/worker/src/routes/name.rs @@ -1,9 +1,12 @@ use enstate_shared::models::profile::ProfileService; +use futures_util::future::try_join_all; use http::StatusCode; +use serde::Deserialize; use worker::{Request, Response, RouteContext}; use crate::http_util::{ - http_simple_status_error, parse_query, profile_http_error_mapper, FreshQuery, + http_simple_status_error, parse_query, profile_http_error_mapper, validate_bulk_input, + FreshQuery, }; pub async fn get(req: Request, ctx: RouteContext) -> worker::Result { @@ -21,3 +24,28 @@ pub async fn get(req: Request, ctx: RouteContext) -> worker::Res Response::from_json(&profile) } + +#[derive(Deserialize)] +pub struct NameGetBulkQuery { + names: Vec, + + #[serde(flatten)] + fresh: FreshQuery, +} + +pub async fn get_bulk(req: Request, ctx: RouteContext) -> worker::Result { + let query: NameGetBulkQuery = parse_query(&req)?; + + let names = validate_bulk_input(&query.names, 10)?; + + let profiles = names + .iter() + .map(|name| ctx.data.resolve_from_name(name, query.fresh.fresh)) + .collect::>(); + + let joined = try_join_all(profiles) + .await + .map_err(profile_http_error_mapper)?; + + Response::from_json(&joined) +} diff --git a/worker/src/routes/universal.rs b/worker/src/routes/universal.rs index c0efde0..527c05d 100644 --- a/worker/src/routes/universal.rs +++ b/worker/src/routes/universal.rs @@ -1,9 +1,12 @@ use enstate_shared::models::profile::ProfileService; +use futures_util::future::try_join_all; use http::StatusCode; +use serde::Deserialize; use worker::{Request, Response, RouteContext}; use crate::http_util::{ - http_simple_status_error, parse_query, profile_http_error_mapper, FreshQuery, + http_simple_status_error, parse_query, profile_http_error_mapper, validate_bulk_input, + FreshQuery, }; pub async fn get(req: Request, ctx: RouteContext) -> worker::Result { @@ -21,3 +24,31 @@ pub async fn get(req: Request, ctx: RouteContext) -> worker::Res Response::from_json(&profile) } + +#[derive(Deserialize)] +pub struct UniversalGetBulkQuery { + queries: Vec, + + #[serde(flatten)] + fresh: FreshQuery, +} + +pub async fn get_bulk(req: Request, ctx: RouteContext) -> worker::Result { + let query: UniversalGetBulkQuery = parse_query(&req)?; + + let queries = validate_bulk_input(&query.queries, 10)?; + + let profiles = queries + .iter() + .map(|input| { + ctx.data + .resolve_from_name_or_address(input, query.fresh.fresh) + }) + .collect::>(); + + let joined = try_join_all(profiles) + .await + .map_err(profile_http_error_mapper)?; + + Response::from_json(&joined) +}