use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Severity {
Critical,
Danger,
Warning,
Info,
Good,
}
impl Severity {
pub fn rank(self) -> u8 {
match self {
Severity::Critical => 5,
Severity::Danger => 4,
Severity::Warning => 3,
Severity::Info => 2,
Severity::Good => 1,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Flag {
pub id: String,
pub severity: Severity,
pub title: String,
pub detail: String,
pub weight: i32,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub hard_fail: bool,
}
impl Flag {
pub fn new(id: &str, severity: Severity, title: &str, detail: String, weight: i32) -> Self {
Flag {
id: id.into(),
severity,
title: title.into(),
detail,
weight,
hard_fail: false,
}
}
pub fn hard(mut self) -> Self {
self.hard_fail = true;
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum Verdict {
Avoid,
HighRisk,
Caution,
Fair,
LowRisk,
Unknown,
}
#[derive(Debug, Clone, Serialize, Default)]
pub struct TokenMeta {
pub name: Option<String>,
pub symbol: Option<String>,
pub decimals: Option<u32>,
#[serde(rename = "totalSupply")]
pub total_supply: Option<String>,
#[serde(rename = "holdersCount")]
pub holders_count: Option<u64>,
#[serde(rename = "marketCapUsd")]
pub market_cap_usd: Option<f64>,
#[serde(rename = "volume24hUsd")]
pub volume_24h_usd: Option<f64>,
#[serde(rename = "priceUsd")]
pub price_usd: Option<f64>,
#[serde(rename = "tokenProgram")]
pub token_program: Option<String>,
pub launchpad: Option<String>,
#[serde(rename = "createdAt")]
pub created_at: Option<String>,
}
#[derive(Debug, Clone, Serialize)]
pub struct AuditResult {
pub address: String,
pub chain: &'static str,
#[serde(rename = "scannedAt")]
pub scanned_at: String,
pub token: TokenMeta,
pub score: i32,
pub verdict: Verdict,
pub flags: Vec<Flag>,
pub sections: serde_json::Value,
pub warnings: Vec<String>,
#[serde(rename = "rokhaUrl", skip_serializing_if = "Option::is_none")]
pub rokha_url: Option<String>,
#[serde(
rename = "rokha_app",
skip_serializing_if = "serde_json::Value::is_null"
)]
pub rokha_app: serde_json::Value,
}
pub fn score_flags(flags: &[Flag], token_resolved: bool) -> (i32, Verdict) {
if !token_resolved {
return (0, Verdict::Unknown);
}
let mut score = 100i32;
let mut cap = 100i32;
for f in flags {
score -= f.weight;
if f.hard_fail {
cap = cap.min(if f.severity == Severity::Critical {
5
} else {
20
});
}
}
score = score.clamp(0, cap);
(score, verdict_from_score(score))
}
pub fn verdict_from_score(score: i32) -> Verdict {
if score <= 10 {
Verdict::Avoid
} else if score < 40 {
Verdict::HighRisk
} else if score < 60 {
Verdict::Caution
} else if score < 80 {
Verdict::Fair
} else {
Verdict::LowRisk
}
}