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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,641 changes: 1,581 additions & 60 deletions Cargo.lock

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ uuid = {version = "1.6", features = ["v4", "serde"]}
# HTTP client for GraphQL
reqwest = {version = "0.11", features = ["json"]}

# Ethereum / ENS
alloy = {version = "1.8", features = ["providers", "provider-http", "ens"]}

# Logging
tracing = "0.1"
tracing-subscriber = {version = "0.3", features = ["env-filter"]}
Expand Down
9 changes: 9 additions & 0 deletions config/default.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
base_api_url = "http://localhost:3000/api"
host = "127.0.0.1"
port = 3000
cors_allowed_origins = ["http://localhost:4321"]

[blockchain]
website_url = "https://www.quantus.com"
Expand Down Expand Up @@ -81,3 +82,11 @@ webhook_url = "https://www.webhook_url.com"

[remote_configs]
wallet_configs_file = "../wallet_configs/default_configs.json"

[risk_checker]
etherscan_api_key = "change-me"
etherscan_base_url = "https://api.etherscan.io/v2/api?chainid=1"
infura_api_key = "change-me"
infura_base_url = "https://mainnet.infura.io/v3"
etherscan_calls_per_sec = 3
max_concurrent_requests = 1
9 changes: 9 additions & 0 deletions config/example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
base_api_url = "http://127.0.0.1:3000/api"
host = "127.0.0.1"
port = 3000
cors_allowed_origins = ["http://localhost:4321"]

[blockchain]
website_url = "http://localhost:3080"
Expand Down Expand Up @@ -92,6 +93,14 @@ webhook_url = "https://www.webhook_url.com"
[remote_configs]
wallet_configs_file = "../wallet_configs/default_configs.json"

[risk_checker]
etherscan_api_key = "change-me"
etherscan_base_url = "https://api.etherscan.io/v2/api?chainid=1"
infura_api_key = "change-me"
infura_base_url = "https://mainnet.infura.io/v3"
etherscan_calls_per_sec = 3
max_concurrent_requests = 1

# Example environment variable overrides:
# TASKMASTER_BLOCKCHAIN__NODE_URL="ws://remote-node:9944"
# TASKMASTER_BLOCKCHAIN__WALLET_PASSWORD="super_secure_password"
Expand Down
9 changes: 9 additions & 0 deletions config/test.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
base_api_url = "http://127.0.0.1:3000/api"
host = "127.0.0.1"
port = 3000
cors_allowed_origins = ["http://localhost:4321"]

[blockchain]
website_url = "http://127.0.0.1:3080"
Expand Down Expand Up @@ -81,3 +82,11 @@ webhook_url = "https://www.webhook_url.com"

[remote_configs]
wallet_configs_file = "../wallet_configs/test_configs.json"

[risk_checker]
etherscan_api_key = "change-me"
etherscan_base_url = "https://api.etherscan.io/v2/api?chainid=1"
infura_api_key = "change-me"
infura_base_url = "https://mainnet.infura.io/v3"
etherscan_calls_per_sec = 3
max_concurrent_requests = 1
36 changes: 36 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use std::path::Path;

use axum::http::HeaderValue;
use rusx::config::OauthConfig;
use serde::{Deserialize, Serialize};
use tokio::time;
Expand All @@ -19,6 +20,7 @@ pub struct Config {
pub alert: AlertConfig,
pub x_association: XAssociationConfig,
pub remote_configs: RemoteConfigsConfig,
pub risk_checker: RiskCheckerConfig,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
Expand All @@ -31,6 +33,7 @@ pub struct ServerConfig {
pub host: String,
pub port: u16,
pub base_api_url: String,
pub cors_allowed_origins: Vec<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
Expand Down Expand Up @@ -99,6 +102,16 @@ pub struct XAssociationConfig {
pub keywords: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RiskCheckerConfig {
pub etherscan_api_key: String,
pub etherscan_base_url: String,
pub infura_api_key: String,
pub infura_base_url: String,
pub etherscan_calls_per_sec: u32,
pub max_concurrent_requests: usize,
}

impl Config {
pub fn load(config_path: &str) -> Result<Self, config::ConfigError> {
let settings = config::Config::builder()
Expand Down Expand Up @@ -159,6 +172,20 @@ impl Config {
&self.x_association.keywords
}

pub fn get_cors_allowed_origins(&self) -> Vec<HeaderValue> {
self.server
.cors_allowed_origins
.iter()
.filter_map(|o| match o.parse() {
Ok(v) => Some(v),
Err(e) => {
tracing::warn!("Skipping invalid CORS origin {:?}: {}", o, e);
None
}
})
.collect()
}

fn resolve_relative_paths(&mut self, config_path: &str) {
let wallet_configs_path = Path::new(&self.remote_configs.wallet_configs_file);
if wallet_configs_path.is_absolute() {
Expand All @@ -176,6 +203,7 @@ impl Default for Config {
host: "127.0.0.1".to_string(),
port: 3000,
base_api_url: "http://127.0.0.1:3000/api".to_string(),
cors_allowed_origins: vec!["http://localhost:3000".to_string()],
},
blockchain: BlockchainConfig {
website_url: "https://www.quantus.com".to_string(),
Expand Down Expand Up @@ -231,6 +259,14 @@ impl Default for Config {
remote_configs: RemoteConfigsConfig {
wallet_configs_file: "wallet_configs/default_configs.json".to_string(),
},
risk_checker: RiskCheckerConfig {
etherscan_api_key: "change-me".to_string(),
etherscan_base_url: "https://api.etherscan.io/api".to_string(),
infura_api_key: "change-me".to_string(),
infura_base_url: "https://mainnet.infura.io/v3".to_string(),
etherscan_calls_per_sec: 3,
max_concurrent_requests: 1,
},
}
}
}
29 changes: 28 additions & 1 deletion src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ use crate::{
db_persistence::DbError,
handlers::{address::AddressHandlerError, auth::AuthHandlerError, referral::ReferralHandlerError, HandlerError},
models::ModelError,
services::{graphql_client::GraphqlError, wallet_config_service::WalletConfigsError},
services::{
graphql_client::GraphqlError, risk_checker_service::RiskCheckerError, wallet_config_service::WalletConfigsError,
},
};

#[derive(Debug, thiserror::Error)]
Expand All @@ -38,6 +40,8 @@ pub enum AppError {
Rusx(#[from] SdkError),
#[error("Telegram API error: {1}")]
Telegram(u16, String),
#[error("Risk checker error: {0}")]
RiskChecker(#[from] RiskCheckerError),
}

pub type AppResult<T> = Result<T, AppError>;
Expand Down Expand Up @@ -66,6 +70,9 @@ impl IntoResponse for AppError {
// --- Database ---
AppError::Database(err) => map_db_error(err),

// --- Risk Checker ---
AppError::RiskChecker(err) => map_risk_checker_error(err),

// --- Everything else ---
e @ (AppError::Join(_)
| AppError::Graphql(_)
Expand Down Expand Up @@ -175,3 +182,23 @@ fn map_db_error(err: DbError) -> (StatusCode, String) {
fn map_wallet_configs_error(err: WalletConfigsError) -> (StatusCode, String) {
(StatusCode::INTERNAL_SERVER_ERROR, err.to_string())
}

fn map_risk_checker_error(err: RiskCheckerError) -> (StatusCode, String) {
match err {
RiskCheckerError::InvalidInput => (StatusCode::BAD_REQUEST, err.to_string()),
RiskCheckerError::EnsNotFound(name) => (
StatusCode::NOT_FOUND,
format!(
"The ENS name \"{}\" could not be resolved to an Ethereum address. Please verify the .eth name is correct.",
name
),
),
RiskCheckerError::AddressNotFound => (StatusCode::NOT_FOUND, err.to_string()),
RiskCheckerError::RateLimit => (StatusCode::TOO_MANY_REQUESTS, err.to_string()),
RiskCheckerError::NetworkError => (StatusCode::SERVICE_UNAVAILABLE, err.to_string()),
RiskCheckerError::Other(msg) => {
tracing::error!("Risk checker error: {}", msg);
(StatusCode::INTERNAL_SERVER_ERROR, "An internal server error occurred".to_string())
}
}
}
1 change: 1 addition & 0 deletions src/handlers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ pub mod config;
pub mod raid_quest;
pub mod referral;
pub mod relevant_tweet;
pub mod risk_checker;
pub mod tweet_author;

#[derive(Debug, thiserror::Error)]
Expand Down
25 changes: 25 additions & 0 deletions src/handlers/risk_checker.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
use axum::{
extract::{Path, State},
Json,
};
use serde_json::json;

use crate::{
handlers::SuccessResponse,
http_server::AppState,
services::risk_checker_service::{RiskCheckerError, RiskCheckerService},
AppError,
};

pub async fn handle_get_risk_report(
State(state): State<AppState>,
Path(address_or_ens): Path<String>,
) -> Result<Json<SuccessResponse<serde_json::Value>>, AppError> {
if !RiskCheckerService::is_valid_eth_address(&address_or_ens) && !RiskCheckerService::is_ens_name(&address_or_ens) {
return Err(RiskCheckerError::InvalidInput.into());
}

let report = state.risk_checker_service.generate_report(&address_or_ens).await?;

Ok(SuccessResponse::new(json!(report)))
}
22 changes: 16 additions & 6 deletions src/http_server.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use axum::http::Method;
use axum::{middleware, response::Json, routing::get, Router};
use rusx::{PkceCodeVerifier, TwitterGateway};
use serde::{Deserialize, Serialize};
Expand All @@ -7,13 +8,16 @@ use std::{
};
use tower::ServiceBuilder;
use tower_cookies::CookieManagerLayer;
use tower_http::{cors::CorsLayer, trace::TraceLayer};
use tower_http::{
cors::{AllowHeaders, CorsLayer},
trace::TraceLayer,
};

use crate::{
db_persistence::DbPersistence,
metrics::{metrics_handler, track_metrics, Metrics},
routes::api_routes,
services::wallet_config_service::WalletConfigService,
services::{risk_checker_service::RiskCheckerService, wallet_config_service::WalletConfigService},
Config, GraphqlClient,
};
use chrono::{DateTime, Utc};
Expand All @@ -25,6 +29,7 @@ pub struct AppState {
pub metrics: Arc<Metrics>,
pub graphql_client: Arc<GraphqlClient>,
pub wallet_config_service: Arc<WalletConfigService>,
pub risk_checker_service: Arc<RiskCheckerService>,
pub config: Arc<Config>,
pub challenges: Arc<RwLock<HashMap<String, Challenge>>>,
pub oauth_sessions: Arc<Mutex<HashMap<String, PkceCodeVerifier>>>,
Expand Down Expand Up @@ -55,11 +60,15 @@ pub fn create_router(state: AppState) -> Router {
.nest("/api", api_routes(state.clone()))
.layer(middleware::from_fn(track_metrics))
.layer(
ServiceBuilder::new()
.layer(TraceLayer::new_for_http())
.layer(CorsLayer::permissive()),
ServiceBuilder::new().layer(TraceLayer::new_for_http()).layer(
CorsLayer::new()
.allow_origin(state.config.get_cors_allowed_origins())
.allow_methods([Method::GET, Method::POST, Method::PUT, Method::DELETE, Method::OPTIONS])
.allow_headers(AllowHeaders::mirror_request())
.allow_credentials(true),
),
)
.layer(CookieManagerLayer::new()) // Enable Cookie support
.layer(CookieManagerLayer::new())
.with_state(state)
}

Expand Down Expand Up @@ -88,6 +97,7 @@ pub async fn start_server(
wallet_config_service: Arc::new(WalletConfigService::new(
config.remote_configs.wallet_configs_file.clone(),
)?),
risk_checker_service: Arc::new(RiskCheckerService::new(&config.risk_checker)),
config,
twitter_gateway,
challenges: Arc::new(RwLock::new(HashMap::new())),
Expand Down
3 changes: 3 additions & 0 deletions src/routes/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use auth::auth_routes;
use axum::Router;
use config::config_routes;
use referral::referral_routes;
use risk_checker::risk_checker_routes;

use crate::{
http_server::AppState,
Expand All @@ -17,6 +18,7 @@ pub mod config;
pub mod raid_quest;
pub mod referral;
pub mod relevant_tweet;
pub mod risk_checker;
pub mod tweet_author;

pub fn api_routes(state: AppState) -> Router<AppState> {
Expand All @@ -28,4 +30,5 @@ pub fn api_routes(state: AppState) -> Router<AppState> {
.merge(tweet_author_routes(state.clone()))
.merge(config_routes())
.merge(raid_quest_routes(state))
.merge(risk_checker_routes())
}
7 changes: 7 additions & 0 deletions src/routes/risk_checker.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
use axum::{routing::get, Router};

use crate::{handlers::risk_checker::handle_get_risk_report, http_server::AppState};

pub fn risk_checker_routes() -> Router<AppState> {
Router::new().route("/risk-checker/:address_or_ens", get(handle_get_risk_report))
}
1 change: 1 addition & 0 deletions src/services/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
pub mod alert_service;
pub mod graphql_client;
pub mod raid_leaderboard_service;
pub mod risk_checker_service;
pub mod signature_service;
pub mod telegram_service;
pub mod tweet_synchronizer_service;
Expand Down
Loading
Loading