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
88 changes: 87 additions & 1 deletion docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,13 @@

.risk-bar { height: 8px; border-radius: 4px; background: #21262d; overflow: hidden; }
.risk-bar-fill { height: 100%; border-radius: 4px; transition: width 1s ease-out; }

/* Skeleton Loading */
.skeleton { background: linear-gradient(90deg, #21262d 25%, #30363d 50%, #21262d 75%); background-size: 200% 100%; animation: skeleton-loading 1.5s infinite; border-radius: 4px; }
@keyframes skeleton-loading { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }
.skeleton-text { height: 16px; margin-bottom: 8px; }
.skeleton-title { height: 24px; width: 60%; margin-bottom: 12px; }
.skeleton-box { height: 80px; margin-bottom: 12px; }
</style>
</head>
<body class="bg-dark-950 text-gray-100 min-h-screen font-sans">
Expand Down Expand Up @@ -688,6 +695,53 @@ <h3 class="text-sm font-medium text-gray-400 mb-6">Powered By</h3>
return { color: '#ef4444', level: 'CRITICAL', bg: 'bg-neon-red' };
}

// Show skeleton loading
function showSkeletonLoading() {
const resultsPanel = document.getElementById('resultsPanel');
resultsPanel.innerHTML = `
<div class="bg-dark-800 rounded-xl p-6 border border-dark-600 animate-fadeIn">
<div class="flex items-center justify-between mb-4">
<div class="flex-1">
<div class="skeleton skeleton-title"></div>
<div class="skeleton skeleton-text w-32"></div>
</div>
<div class="skeleton w-16 h-16 rounded-lg"></div>
</div>
<div class="skeleton skeleton-box"></div>
<div class="skeleton skeleton-text"></div>
</div>
<div class="bg-dark-800 rounded-xl p-6 border border-dark-600 animate-fadeIn" style="animation-delay: 0.1s">
<div class="skeleton skeleton-title w-40"></div>
<div class="skeleton skeleton-text"></div>
<div class="skeleton skeleton-text"></div>
<div class="skeleton skeleton-text"></div>
</div>
<div class="bg-dark-800 rounded-xl p-6 border border-dark-600 animate-fadeIn" style="animation-delay: 0.2s">
<div class="skeleton skeleton-title w-32"></div>
<div class="grid grid-cols-2 gap-4 mt-4">
<div class="skeleton h-20 rounded-lg"></div>
<div class="skeleton h-20 rounded-lg"></div>
</div>
</div>
`;
}

// Format number helpers
function formatUSD(value) {
if (!value) return 'N/A';
if (value >= 1000000) return '$' + (value / 1000000).toFixed(2) + 'M';
if (value >= 1000) return '$' + (value / 1000).toFixed(2) + 'K';
return '$' + value.toFixed(2);
}

function formatPrice(price) {
if (!price) return 'N/A';
const num = parseFloat(price);
if (num < 0.00001) return '$' + num.toExponential(2);
if (num < 1) return '$' + num.toFixed(6);
return '$' + num.toFixed(2);
}

// Main analyze function
async function analyzeToken() {
const tokenAddress = document.getElementById('tokenAddress').value.trim();
Expand All @@ -701,9 +755,10 @@ <h3 class="text-sm font-medium text-gray-400 mb-6">Powered By</h3>
return;
}

// Disable button
// Disable button and show skeleton
btn.disabled = true;
btn.innerHTML = '<div class="spinner"></div> Analyzing...';
showSkeletonLoading();

// Start simulation animation
const animationPromise = animateSimulationLog();
Expand Down Expand Up @@ -824,7 +879,38 @@ <h3 class="text-lg font-semibold">${data.token_symbol || 'Unknown'}</h3>
<span class="px-2 py-1 bg-dark-700 rounded text-xs">${data.chain_name}</span>
<span class="text-gray-600">•</span>
<span class="font-mono text-xs">${data.token_address.slice(0, 10)}...${data.token_address.slice(-8)}</span>
${data.dex_name ? `<span class="text-gray-600">•</span><span class="text-xs text-neon-cyan">${data.dex_name}</span>` : ''}
</div>
</div>

<!-- Market Data Card (from DexScreener) -->
<div class="bg-dark-800 rounded-xl p-6 border border-dark-600 animate-fadeIn" style="animation-delay: 0.05s">
<h4 class="font-semibold mb-4 flex items-center gap-2">
<span>📊</span> Market Data
<span class="text-xs text-gray-500 font-normal">(via DexScreener)</span>
</h4>
<div class="grid grid-cols-3 gap-4">
<div class="p-3 bg-dark-700 rounded-lg text-center">
<div class="text-xs text-gray-400 mb-1">Price</div>
<div class="font-mono text-neon-green font-semibold">${formatPrice(data.price_usd)}</div>
</div>
<div class="p-3 bg-dark-700 rounded-lg text-center">
<div class="text-xs text-gray-400 mb-1">Liquidity</div>
<div class="font-mono text-neon-cyan font-semibold">${formatUSD(data.liquidity_usd)}</div>
</div>
<div class="p-3 bg-dark-700 rounded-lg text-center">
<div class="text-xs text-gray-400 mb-1">Volume 24h</div>
<div class="font-mono text-neon-purple font-semibold">${formatUSD(data.volume_24h_usd)}</div>
</div>
</div>
${data.pair_address ? `
<div class="mt-3 pt-3 border-t border-dark-600">
<a href="https://dexscreener.com/${data.chain_name.toLowerCase()}/${data.pair_address}" target="_blank"
class="text-xs text-neon-cyan hover:underline flex items-center gap-1">
View on DexScreener →
</a>
</div>
` : ''}
</div>

<!-- Tax Breakdown Card -->
Expand Down
77 changes: 68 additions & 9 deletions src/api/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -226,11 +226,15 @@ pub async fn check_honeypot(
chain_id: req.chain_id,
chain_name: crate::providers::dexscreener::DexScreenerClient::chain_id_to_name_pub(req.chain_id).to_string(),
best_dex: discovered,
token_name: best.base_token.name,
token_symbol: best.base_token.symbol,
token_name: best.base_token.name.clone(),
token_symbol: best.base_token.symbol.clone(),
all_pairs: vec![],
has_v2_liquidity: !v3_only,
total_pairs: pairs.len(),
// Market data
price_usd: best.price_usd.clone(),
volume_24h_usd: best.volume.as_ref().and_then(|v| v.h24),
pair_address: Some(best.pair_address.clone()),
}), v3_only)
}
_ => {
Expand Down Expand Up @@ -260,24 +264,41 @@ pub async fn check_honeypot(
.unwrap_or_else(|| "Unknown DEX".to_string());

info!("⚠️ Token only available on V3/Velodrome-style DEX: {}", dex_name);

// Extract market data from detected_info
let (price_usd, liquidity_usd, volume_24h_usd, pair_address) = match &detected_info {
Some(info) => (
info.price_usd.clone(),
Some(info.best_dex.liquidity_usd),
info.volume_24h_usd,
info.pair_address.clone(),
),
None => (None, None, None, None),
};

let data = HoneypotCheckData {
token_address: req.token_address,
token_name: auto_detected_name,
token_symbol: auto_detected_symbol,
token_decimals: None,
chain_id: effective_chain_id,
chain_name,
chain_name: chain_name.clone(),
native_symbol: "ETH".to_string(),
is_honeypot: false,
risk_score: 0,
risk_score: 70, // HIGH risk - cannot verify
buy_success: false,
sell_success: false,
buy_tax_percent: 0.0,
sell_tax_percent: 0.0,
total_loss_percent: 0.0,
reason: format!("Token only available on {} (V3/Velodrome-style) - not supported yet. Use DEX directly.", dex_name),
simulation_latency_ms: start.elapsed().as_millis() as u64,
// DexScreener market data
price_usd,
liquidity_usd,
volume_24h_usd,
dex_name: Some(dex_name),
pair_address,
};

return Ok(Json(ApiResponse::success(
Expand Down Expand Up @@ -317,11 +338,23 @@ pub async fn check_honeypot(

// Use auto-detected name/symbol if available, otherwise fetch from RPC
let (token_name, token_symbol, token_decimals) = if auto_detected_name.is_some() {
(auto_detected_name, auto_detected_symbol, None)
(auto_detected_name.clone(), auto_detected_symbol.clone(), None)
} else {
let token_info = detector.fetch_token_info(token).await;
(token_info.name, token_info.symbol, token_info.decimals)
};

// Extract market data from detected_info
let (price_usd, liquidity_usd, volume_24h_usd, dex_name, pair_address) = match &detected_info {
Some(info) => (
info.price_usd.clone(),
Some(info.best_dex.liquidity_usd),
info.volume_24h_usd,
Some(info.best_dex.dex_name.clone()),
info.pair_address.clone(),
),
None => (None, None, None, None, None),
};

// Calculate risk score from cached result
let risk_score = calculate_risk_score(&cached_result);
Expand All @@ -343,6 +376,12 @@ pub async fn check_honeypot(
total_loss_percent: cached_result.total_loss_percent,
reason: format!("{} (cached)", cached_result.reason),
simulation_latency_ms: 0, // Instant from cache
// DexScreener market data
price_usd,
liquidity_usd,
volume_24h_usd,
dex_name,
pair_address,
};

return Ok(Json(ApiResponse::success(
Expand Down Expand Up @@ -407,6 +446,18 @@ pub async fn check_honeypot(
(token_info.name, token_info.symbol, token_info.decimals)
};

// Extract market data from detected_info
let (price_usd, liquidity_usd, volume_24h_usd, dex_name, pair_address) = match &detected_info {
Some(info) => (
info.price_usd.clone(),
Some(info.best_dex.liquidity_usd),
info.volume_24h_usd,
Some(info.best_dex.dex_name.clone()),
info.pair_address.clone(),
),
None => (None, None, None, None, None),
};

// Calculate risk score based on actual simulation results
let risk_score = calculate_risk_score(&hp_result);

Expand Down Expand Up @@ -443,6 +494,12 @@ pub async fn check_honeypot(
total_loss_percent: hp_result.total_loss_percent,
reason: hp_result.reason,
simulation_latency_ms: hp_result.latency_ms,
// DexScreener market data
price_usd,
liquidity_usd,
volume_24h_usd,
dex_name,
pair_address,
};

Ok(Json(ApiResponse::success(
Expand Down Expand Up @@ -667,11 +724,13 @@ pub async fn get_stats(State(state): State<Arc<AppState>>) -> Json<ApiResponse<S
/// Calculate risk score from HoneypotResult
/// PERS v2 algorithm implementation
fn calculate_risk_score(result: &crate::core::honeypot::HoneypotResult) -> u8 {
// Special case: No liquidity found - not necessarily dangerous
// Token might just trade on a different DEX
// Special case: No liquidity found - THIS IS SUSPICIOUS!
// If we can't simulate buy/sell, we can't verify safety
// Treat as HIGH RISK (not safe to trade)
if !result.buy_success && !result.sell_success && !result.is_honeypot && !result.sell_reverted {
// No liquidity = unknown, give neutral score
return 30; // "LOW" risk - we just couldn't test it
// No liquidity = UNVERIFIED = HIGH RISK
// User should NOT trade tokens we can't verify
return 70; // "HIGH" risk - cannot verify safety
}

// Base score based on simulation results
Expand Down
19 changes: 19 additions & 0 deletions src/api/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,25 @@ pub struct HoneypotCheckData {
pub total_loss_percent: f64,
pub reason: String,
pub simulation_latency_ms: u64,

// ============================================
// DexScreener Market Data (NEW!)
// ============================================
/// Price in USD from DexScreener
#[serde(skip_serializing_if = "Option::is_none")]
pub price_usd: Option<String>,
/// Liquidity in USD
#[serde(skip_serializing_if = "Option::is_none")]
pub liquidity_usd: Option<f64>,
/// 24h trading volume in USD
#[serde(skip_serializing_if = "Option::is_none")]
pub volume_24h_usd: Option<f64>,
/// DEX name where token trades
#[serde(skip_serializing_if = "Option::is_none")]
pub dex_name: Option<String>,
/// Pair address on DEX
#[serde(skip_serializing_if = "Option::is_none")]
pub pair_address: Option<String>,
}

// ============================================
Expand Down
10 changes: 7 additions & 3 deletions src/core/honeypot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -418,18 +418,22 @@ impl HoneypotDetector {
}
}

// No liquidity found on any DEX
// No liquidity found on any DEX - HIGH RISK!
Ok(HoneypotResult {
is_honeypot: false,
reason: format!("No liquidity found on {} (tried: {})", self.chain_name, tried_dexes.join(", ")),
reason: format!("⚠️ UNVERIFIED - No V2 liquidity on {} (tried: {}). Cannot confirm safety!", self.chain_name, tried_dexes.join(", ")),
buy_success: false,
sell_success: false,
sell_reverted: false,
buy_tax_percent: 0.0,
sell_tax_percent: 0.0,
total_loss_percent: 0.0,
access_control_penalty,
risk_factors: vec![format!("No pair on: {}", tried_dexes.join(", "))],
risk_factors: vec![
format!("No V2 pair found on: {}", tried_dexes.join(", ")),
"Token may trade on V3/unsupported DEX".to_string(),
"Cannot verify buy/sell safety".to_string(),
],
latency_ms,
})
}
Expand Down
13 changes: 13 additions & 0 deletions src/providers/dexscreener.rs
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,10 @@ impl DexScreenerClient {
// Add info about V3-only tokens
has_v2_liquidity: v2_pairs_count > 0,
total_pairs: all_pairs_count,
// Market data from DexScreener
price_usd: best_pair.price_usd.clone(),
volume_24h_usd: best_pair.volume.as_ref().and_then(|v| v.h24),
pair_address: Some(best_pair.pair_address.clone()),
})
}

Expand Down Expand Up @@ -280,6 +284,15 @@ pub struct AutoDetectedToken {
pub has_v2_liquidity: bool,
/// Total number of pairs (including V3)
pub total_pairs: usize,
// ============================================
// Market Data from DexScreener (NEW!)
// ============================================
/// Price in USD
pub price_usd: Option<String>,
/// 24h trading volume
pub volume_24h_usd: Option<f64>,
/// Pair address
pub pair_address: Option<String>,
}

impl DexPair {
Expand Down