Skip to content

Commit ebaacd4

Browse files
committed
Merge branch 'main' of github.com:Quantus-Network/task-master
2 parents 3319ff7 + 4011e13 commit ebaacd4

15 files changed

Lines changed: 2710 additions & 70 deletions

File tree

Cargo.lock

Lines changed: 1581 additions & 60 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,9 @@ uuid = {version = "1.6", features = ["v4", "serde"]}
6969
# HTTP client for GraphQL
7070
reqwest = {version = "0.11", features = ["json"]}
7171

72+
# Ethereum / ENS
73+
alloy = {version = "1.8", features = ["providers", "provider-http", "ens"]}
74+
7275
# Logging
7376
tracing = "0.1"
7477
tracing-subscriber = {version = "0.3", features = ["env-filter"]}

config/default.toml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
base_api_url = "http://localhost:3000/api"
55
host = "127.0.0.1"
66
port = 3000
7+
cors_allowed_origins = ["http://localhost:4321"]
78

89
[blockchain]
910
website_url = "https://www.quantus.com"
@@ -81,3 +82,11 @@ webhook_url = "https://www.webhook_url.com"
8182

8283
[remote_configs]
8384
wallet_configs_file = "../wallet_configs/default_configs.json"
85+
86+
[risk_checker]
87+
etherscan_api_key = "change-me"
88+
etherscan_base_url = "https://api.etherscan.io/v2/api?chainid=1"
89+
infura_api_key = "change-me"
90+
infura_base_url = "https://mainnet.infura.io/v3"
91+
etherscan_calls_per_sec = 3
92+
max_concurrent_requests = 1

config/example.toml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
base_api_url = "http://127.0.0.1:3000/api"
77
host = "127.0.0.1"
88
port = 3000
9+
cors_allowed_origins = ["http://localhost:4321"]
910

1011
[blockchain]
1112
website_url = "http://localhost:3080"
@@ -92,6 +93,14 @@ webhook_url = "https://www.webhook_url.com"
9293
[remote_configs]
9394
wallet_configs_file = "../wallet_configs/default_configs.json"
9495

96+
[risk_checker]
97+
etherscan_api_key = "change-me"
98+
etherscan_base_url = "https://api.etherscan.io/v2/api?chainid=1"
99+
infura_api_key = "change-me"
100+
infura_base_url = "https://mainnet.infura.io/v3"
101+
etherscan_calls_per_sec = 3
102+
max_concurrent_requests = 1
103+
95104
# Example environment variable overrides:
96105
# TASKMASTER_BLOCKCHAIN__NODE_URL="ws://remote-node:9944"
97106
# TASKMASTER_BLOCKCHAIN__WALLET_PASSWORD="super_secure_password"

config/test.toml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
base_api_url = "http://127.0.0.1:3000/api"
55
host = "127.0.0.1"
66
port = 3000
7+
cors_allowed_origins = ["http://localhost:4321"]
78

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

8283
[remote_configs]
8384
wallet_configs_file = "../wallet_configs/test_configs.json"
85+
86+
[risk_checker]
87+
etherscan_api_key = "change-me"
88+
etherscan_base_url = "https://api.etherscan.io/v2/api?chainid=1"
89+
infura_api_key = "change-me"
90+
infura_base_url = "https://mainnet.infura.io/v3"
91+
etherscan_calls_per_sec = 3
92+
max_concurrent_requests = 1

src/config.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use std::path::Path;
22

3+
use axum::http::HeaderValue;
34
use rusx::config::OauthConfig;
45
use serde::{Deserialize, Serialize};
56
use tokio::time;
@@ -19,6 +20,7 @@ pub struct Config {
1920
pub alert: AlertConfig,
2021
pub x_association: XAssociationConfig,
2122
pub remote_configs: RemoteConfigsConfig,
23+
pub risk_checker: RiskCheckerConfig,
2224
}
2325

2426
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -31,6 +33,7 @@ pub struct ServerConfig {
3133
pub host: String,
3234
pub port: u16,
3335
pub base_api_url: String,
36+
pub cors_allowed_origins: Vec<String>,
3437
}
3538

3639
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -99,6 +102,16 @@ pub struct XAssociationConfig {
99102
pub keywords: String,
100103
}
101104

105+
#[derive(Debug, Clone, Serialize, Deserialize)]
106+
pub struct RiskCheckerConfig {
107+
pub etherscan_api_key: String,
108+
pub etherscan_base_url: String,
109+
pub infura_api_key: String,
110+
pub infura_base_url: String,
111+
pub etherscan_calls_per_sec: u32,
112+
pub max_concurrent_requests: usize,
113+
}
114+
102115
impl Config {
103116
pub fn load(config_path: &str) -> Result<Self, config::ConfigError> {
104117
let settings = config::Config::builder()
@@ -159,6 +172,20 @@ impl Config {
159172
&self.x_association.keywords
160173
}
161174

175+
pub fn get_cors_allowed_origins(&self) -> Vec<HeaderValue> {
176+
self.server
177+
.cors_allowed_origins
178+
.iter()
179+
.filter_map(|o| match o.parse() {
180+
Ok(v) => Some(v),
181+
Err(e) => {
182+
tracing::warn!("Skipping invalid CORS origin {:?}: {}", o, e);
183+
None
184+
}
185+
})
186+
.collect()
187+
}
188+
162189
fn resolve_relative_paths(&mut self, config_path: &str) {
163190
let wallet_configs_path = Path::new(&self.remote_configs.wallet_configs_file);
164191
if wallet_configs_path.is_absolute() {
@@ -176,6 +203,7 @@ impl Default for Config {
176203
host: "127.0.0.1".to_string(),
177204
port: 3000,
178205
base_api_url: "http://127.0.0.1:3000/api".to_string(),
206+
cors_allowed_origins: vec!["http://localhost:3000".to_string()],
179207
},
180208
blockchain: BlockchainConfig {
181209
website_url: "https://www.quantus.com".to_string(),
@@ -231,6 +259,14 @@ impl Default for Config {
231259
remote_configs: RemoteConfigsConfig {
232260
wallet_configs_file: "wallet_configs/default_configs.json".to_string(),
233261
},
262+
risk_checker: RiskCheckerConfig {
263+
etherscan_api_key: "change-me".to_string(),
264+
etherscan_base_url: "https://api.etherscan.io/api".to_string(),
265+
infura_api_key: "change-me".to_string(),
266+
infura_base_url: "https://mainnet.infura.io/v3".to_string(),
267+
etherscan_calls_per_sec: 3,
268+
max_concurrent_requests: 1,
269+
},
234270
}
235271
}
236272
}

src/errors.rs

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ use crate::{
1111
db_persistence::DbError,
1212
handlers::{address::AddressHandlerError, auth::AuthHandlerError, referral::ReferralHandlerError, HandlerError},
1313
models::ModelError,
14-
services::{graphql_client::GraphqlError, wallet_config_service::WalletConfigsError},
14+
services::{
15+
graphql_client::GraphqlError, risk_checker_service::RiskCheckerError, wallet_config_service::WalletConfigsError,
16+
},
1517
};
1618

1719
#[derive(Debug, thiserror::Error)]
@@ -38,6 +40,8 @@ pub enum AppError {
3840
Rusx(#[from] SdkError),
3941
#[error("Telegram API error: {1}")]
4042
Telegram(u16, String),
43+
#[error("Risk checker error: {0}")]
44+
RiskChecker(#[from] RiskCheckerError),
4145
}
4246

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

73+
// --- Risk Checker ---
74+
AppError::RiskChecker(err) => map_risk_checker_error(err),
75+
6976
// --- Everything else ---
7077
e @ (AppError::Join(_)
7178
| AppError::Graphql(_)
@@ -175,3 +182,23 @@ fn map_db_error(err: DbError) -> (StatusCode, String) {
175182
fn map_wallet_configs_error(err: WalletConfigsError) -> (StatusCode, String) {
176183
(StatusCode::INTERNAL_SERVER_ERROR, err.to_string())
177184
}
185+
186+
fn map_risk_checker_error(err: RiskCheckerError) -> (StatusCode, String) {
187+
match err {
188+
RiskCheckerError::InvalidInput => (StatusCode::BAD_REQUEST, err.to_string()),
189+
RiskCheckerError::EnsNotFound(name) => (
190+
StatusCode::NOT_FOUND,
191+
format!(
192+
"The ENS name \"{}\" could not be resolved to an Ethereum address. Please verify the .eth name is correct.",
193+
name
194+
),
195+
),
196+
RiskCheckerError::AddressNotFound => (StatusCode::NOT_FOUND, err.to_string()),
197+
RiskCheckerError::RateLimit => (StatusCode::TOO_MANY_REQUESTS, err.to_string()),
198+
RiskCheckerError::NetworkError => (StatusCode::SERVICE_UNAVAILABLE, err.to_string()),
199+
RiskCheckerError::Other(msg) => {
200+
tracing::error!("Risk checker error: {}", msg);
201+
(StatusCode::INTERNAL_SERVER_ERROR, "An internal server error occurred".to_string())
202+
}
203+
}
204+
}

src/handlers/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ pub mod config;
1313
pub mod raid_quest;
1414
pub mod referral;
1515
pub mod relevant_tweet;
16+
pub mod risk_checker;
1617
pub mod tweet_author;
1718

1819
#[derive(Debug, thiserror::Error)]

src/handlers/risk_checker.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
use axum::{
2+
extract::{Path, State},
3+
Json,
4+
};
5+
use serde_json::json;
6+
7+
use crate::{
8+
handlers::SuccessResponse,
9+
http_server::AppState,
10+
services::risk_checker_service::{RiskCheckerError, RiskCheckerService},
11+
AppError,
12+
};
13+
14+
pub async fn handle_get_risk_report(
15+
State(state): State<AppState>,
16+
Path(address_or_ens): Path<String>,
17+
) -> Result<Json<SuccessResponse<serde_json::Value>>, AppError> {
18+
if !RiskCheckerService::is_valid_eth_address(&address_or_ens) && !RiskCheckerService::is_ens_name(&address_or_ens) {
19+
return Err(RiskCheckerError::InvalidInput.into());
20+
}
21+
22+
let report = state.risk_checker_service.generate_report(&address_or_ens).await?;
23+
24+
Ok(SuccessResponse::new(json!(report)))
25+
}

src/http_server.rs

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use axum::http::Method;
12
use axum::{middleware, response::Json, routing::get, Router};
23
use rusx::{PkceCodeVerifier, TwitterGateway};
34
use serde::{Deserialize, Serialize};
@@ -7,13 +8,16 @@ use std::{
78
};
89
use tower::ServiceBuilder;
910
use tower_cookies::CookieManagerLayer;
10-
use tower_http::{cors::CorsLayer, trace::TraceLayer};
11+
use tower_http::{
12+
cors::{AllowHeaders, CorsLayer},
13+
trace::TraceLayer,
14+
};
1115

1216
use crate::{
1317
db_persistence::DbPersistence,
1418
metrics::{metrics_handler, track_metrics, Metrics},
1519
routes::api_routes,
16-
services::wallet_config_service::WalletConfigService,
20+
services::{risk_checker_service::RiskCheckerService, wallet_config_service::WalletConfigService},
1721
Config, GraphqlClient,
1822
};
1923
use chrono::{DateTime, Utc};
@@ -25,6 +29,7 @@ pub struct AppState {
2529
pub metrics: Arc<Metrics>,
2630
pub graphql_client: Arc<GraphqlClient>,
2731
pub wallet_config_service: Arc<WalletConfigService>,
32+
pub risk_checker_service: Arc<RiskCheckerService>,
2833
pub config: Arc<Config>,
2934
pub challenges: Arc<RwLock<HashMap<String, Challenge>>>,
3035
pub oauth_sessions: Arc<Mutex<HashMap<String, PkceCodeVerifier>>>,
@@ -55,11 +60,15 @@ pub fn create_router(state: AppState) -> Router {
5560
.nest("/api", api_routes(state.clone()))
5661
.layer(middleware::from_fn(track_metrics))
5762
.layer(
58-
ServiceBuilder::new()
59-
.layer(TraceLayer::new_for_http())
60-
.layer(CorsLayer::permissive()),
63+
ServiceBuilder::new().layer(TraceLayer::new_for_http()).layer(
64+
CorsLayer::new()
65+
.allow_origin(state.config.get_cors_allowed_origins())
66+
.allow_methods([Method::GET, Method::POST, Method::PUT, Method::DELETE, Method::OPTIONS])
67+
.allow_headers(AllowHeaders::mirror_request())
68+
.allow_credentials(true),
69+
),
6170
)
62-
.layer(CookieManagerLayer::new()) // Enable Cookie support
71+
.layer(CookieManagerLayer::new())
6372
.with_state(state)
6473
}
6574

@@ -88,6 +97,7 @@ pub async fn start_server(
8897
wallet_config_service: Arc::new(WalletConfigService::new(
8998
config.remote_configs.wallet_configs_file.clone(),
9099
)?),
100+
risk_checker_service: Arc::new(RiskCheckerService::new(&config.risk_checker)),
91101
config,
92102
twitter_gateway,
93103
challenges: Arc::new(RwLock::new(HashMap::new())),

0 commit comments

Comments
 (0)