use crate::AppState;
use axum::Json;
use axum::extract::State;
use axum::response::IntoResponse;
use scope_cli::cli::insights::{self, InsightsArgs};
use serde::Deserialize;
use std::sync::Arc;
use super::{error_response, status_for};
#[derive(Debug, Deserialize)]
pub struct InsightsRequest {
pub target: String,
pub chain: Option<String>,
#[serde(default)]
pub decode: bool,
#[serde(default)]
pub trace: bool,
}
pub async fn handle(
State(state): State<Arc<AppState>>,
Json(req): Json<InsightsRequest>,
) -> impl IntoResponse {
let resolved = match super::resolve_address_book(&req.target, &state.config) {
Ok(r) => r,
Err(e) => return error_response(status_for(&e), e.to_string()),
};
let target_str = resolved.value;
let chain_override = req.chain.or(resolved.chain);
let target = insights::infer_target(&target_str, chain_override.as_deref());
let target_type = match &target {
insights::InferredTarget::Address { chain } => {
serde_json::json!({ "type": "address", "chain": chain })
}
insights::InferredTarget::Transaction { chain } => {
serde_json::json!({ "type": "transaction", "chain": chain })
}
insights::InferredTarget::Token { chain } => {
serde_json::json!({ "type": "token", "chain": chain })
}
};
let args = InsightsArgs {
target: target_str.clone(),
token_contract: None,
chain: chain_override,
decode: req.decode,
trace: req.trace,
health: Default::default(),
};
match &target {
insights::InferredTarget::Address { chain } => {
let addr_args = scope_cli::cli::address::AddressArgs {
address: target_str.clone(),
chain: chain.clone(),
format: None,
include_txs: false,
include_tokens: true,
limit: 10,
report: None,
dossier: false,
};
let client: Box<dyn scope::chains::ChainClient> =
match state.factory.create_chain_client(chain) {
Ok(c) => c,
Err(e) => return error_response(status_for(&e), e.to_string()),
};
match scope_cli::cli::address::analyze_address(
&addr_args,
client.as_ref(),
&scope_cli::cli::output::Output::stdio().without_prompts(),
)
.await
{
Ok(report) => Json(serde_json::json!({
"target_info": target_type,
"data": report,
}))
.into_response(),
Err(e) => error_response(status_for(&e), e.to_string()),
}
}
insights::InferredTarget::Transaction { chain } => {
match scope_cli::cli::tx::fetch_transaction_report(
&target_str,
chain,
args.decode,
args.trace,
state.factory.as_ref(),
)
.await
{
Ok(report) => Json(serde_json::json!({
"target_info": target_type,
"data": report,
}))
.into_response(),
Err(e) => error_response(status_for(&e), e.to_string()),
}
}
insights::InferredTarget::Token { chain } => {
match scope_cli::cli::crawl::fetch_analytics_for_input(
&target_str,
chain,
scope_cli::cli::crawl::Period::Hour24,
10,
state.factory.as_ref(),
None,
&scope_cli::cli::output::Output::stdio().without_prompts(),
)
.await
{
Ok(analytics) => Json(serde_json::json!({
"target_info": target_type,
"data": analytics,
}))
.into_response(),
Err(e) => error_response(status_for(&e), e.to_string()),
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_deserialize_full() {
let json = serde_json::json!({
"target": "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
"chain": "polygon",
"decode": true,
"trace": true
});
let req: InsightsRequest = serde_json::from_value(json).unwrap();
assert_eq!(req.target, "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2");
assert_eq!(req.chain, Some("polygon".to_string()));
assert!(req.decode);
assert!(req.trace);
}
#[test]
fn test_deserialize_minimal() {
let json = serde_json::json!({
"target": "0x1234567890123456789012345678901234567890"
});
let req: InsightsRequest = serde_json::from_value(json).unwrap();
assert_eq!(req.target, "0x1234567890123456789012345678901234567890");
assert_eq!(req.chain, None);
assert!(!req.decode);
assert!(!req.trace);
}
#[test]
fn test_with_chain_override() {
let json = serde_json::json!({
"target": "USDC",
"chain": "ethereum"
});
let req: InsightsRequest = serde_json::from_value(json).unwrap();
assert_eq!(req.target, "USDC");
assert_eq!(req.chain, Some("ethereum".to_string()));
assert!(!req.decode);
assert!(!req.trace);
}
#[test]
fn test_flags() {
let json_decode = serde_json::json!({
"target": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890",
"decode": true,
"trace": false
});
let req_decode: InsightsRequest = serde_json::from_value(json_decode).unwrap();
assert!(req_decode.decode);
assert!(!req_decode.trace);
let json_trace = serde_json::json!({
"target": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890",
"decode": false,
"trace": true
});
let req_trace: InsightsRequest = serde_json::from_value(json_trace).unwrap();
assert!(!req_trace.decode);
assert!(req_trace.trace);
let json_both = serde_json::json!({
"target": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890",
"decode": true,
"trace": true
});
let req_both: InsightsRequest = serde_json::from_value(json_both).unwrap();
assert!(req_both.decode);
assert!(req_both.trace);
}
#[tokio::test]
async fn test_handle_insights_address() {
use crate::AppState;
use axum::extract::State;
use axum::response::IntoResponse;
use scope::chains::DefaultClientFactory;
use scope::config::Config;
let config = Config::default();
let http: std::sync::Arc<dyn scope::http::HttpClient> =
std::sync::Arc::new(scope::http::NativeHttpClient::new().unwrap());
let factory = DefaultClientFactory {
chains_config: config.chains.clone(),
http,
};
let state = std::sync::Arc::new(AppState::new(config, factory));
let req = InsightsRequest {
target: "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2".to_string(),
chain: None,
decode: false,
trace: false,
};
let response = handle(State(state), axum::Json(req)).await.into_response();
let status = response.status();
assert!(status.is_success() || status.is_client_error() || status.is_server_error());
}
#[tokio::test]
async fn test_handle_insights_token() {
use crate::AppState;
use axum::extract::State;
use axum::response::IntoResponse;
use scope::chains::DefaultClientFactory;
use scope::config::Config;
let config = Config::default();
let http: std::sync::Arc<dyn scope::http::HttpClient> =
std::sync::Arc::new(scope::http::NativeHttpClient::new().unwrap());
let factory = DefaultClientFactory {
chains_config: config.chains.clone(),
http,
};
let state = std::sync::Arc::new(AppState::new(config, factory));
let req = InsightsRequest {
target: "USDC".to_string(),
chain: Some("ethereum".to_string()),
decode: false,
trace: false,
};
let response = handle(State(state), axum::Json(req)).await.into_response();
let status = response.status();
assert!(status.is_success() || status.is_client_error() || status.is_server_error());
}
#[tokio::test]
async fn test_handle_insights_tx() {
use crate::AppState;
use axum::extract::State;
use axum::response::IntoResponse;
use scope::chains::DefaultClientFactory;
use scope::config::Config;
let config = Config::default();
let http: std::sync::Arc<dyn scope::http::HttpClient> =
std::sync::Arc::new(scope::http::NativeHttpClient::new().unwrap());
let factory = DefaultClientFactory {
chains_config: config.chains.clone(),
http,
};
let state = std::sync::Arc::new(AppState::new(config, factory));
let req = InsightsRequest {
target: "0xabc123def456789012345678901234567890123456789012345678901234abcd"
.to_string(),
chain: None,
decode: true,
trace: false,
};
let response = handle(State(state), axum::Json(req)).await.into_response();
let status = response.status();
assert!(status.is_success() || status.is_client_error() || status.is_server_error());
}
#[tokio::test]
async fn test_handle_insights_address_analyze_error() {
use crate::AppState;
use axum::extract::State;
use axum::http::StatusCode;
use axum::response::IntoResponse;
use scope::chains::DefaultClientFactory;
use scope::config::Config;
let config = Config::default();
let http: std::sync::Arc<dyn scope::http::HttpClient> =
std::sync::Arc::new(scope::http::NativeHttpClient::new().unwrap());
let factory = DefaultClientFactory {
chains_config: config.chains.clone(),
http,
};
let state = std::sync::Arc::new(AppState::new(config, factory));
let req = InsightsRequest {
target: "0x0000000000000000000000000000000000000000".to_string(),
chain: Some("ethereum".to_string()),
decode: false,
trace: false,
};
let response = handle(State(state), axum::Json(req)).await.into_response();
if response.status() == StatusCode::INTERNAL_SERVER_ERROR {
let body = axum::body::to_bytes(response.into_body(), 1_000_000)
.await
.unwrap();
let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert!(json.get("error").is_some());
}
}
#[tokio::test]
async fn test_handle_insights_tx_error() {
use crate::AppState;
use axum::extract::State;
use axum::http::StatusCode;
use axum::response::IntoResponse;
use scope::chains::DefaultClientFactory;
use scope::config::Config;
let config = Config::default();
let http: std::sync::Arc<dyn scope::http::HttpClient> =
std::sync::Arc::new(scope::http::NativeHttpClient::new().unwrap());
let factory = DefaultClientFactory {
chains_config: config.chains.clone(),
http,
};
let state = std::sync::Arc::new(AppState::new(config, factory));
let req = InsightsRequest {
target: "0x0000000000000000000000000000000000000000000000000000000000000000"
.to_string(),
chain: Some("ethereum".to_string()),
decode: false,
trace: false,
};
let response = handle(State(state), axum::Json(req)).await.into_response();
if response.status() == StatusCode::INTERNAL_SERVER_ERROR {
let body = axum::body::to_bytes(response.into_body(), 1_000_000)
.await
.unwrap();
let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert!(json.get("error").is_some());
}
}
#[tokio::test]
async fn test_handle_insights_token_error() {
use crate::AppState;
use axum::extract::State;
use axum::http::StatusCode;
use axum::response::IntoResponse;
use scope::chains::DefaultClientFactory;
use scope::config::Config;
let config = Config::default();
let http: std::sync::Arc<dyn scope::http::HttpClient> =
std::sync::Arc::new(scope::http::NativeHttpClient::new().unwrap());
let factory = DefaultClientFactory {
chains_config: config.chains.clone(),
http,
};
let state = std::sync::Arc::new(AppState::new(config, factory));
let req = InsightsRequest {
target: "NONEXISTENT_TOKEN_XYZ_123".to_string(),
chain: Some("ethereum".to_string()),
decode: false,
trace: false,
};
let response = handle(State(state), axum::Json(req)).await.into_response();
if response.status() == StatusCode::INTERNAL_SERVER_ERROR {
let body = axum::body::to_bytes(response.into_body(), 1_000_000)
.await
.unwrap();
let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert!(json.get("error").is_some());
}
}
#[tokio::test]
async fn test_handle_insights_unsupported_chain_bad_request() {
use crate::AppState;
use axum::extract::State;
use axum::http::StatusCode;
use axum::response::IntoResponse;
use scope::chains::DefaultClientFactory;
use scope::config::Config;
let tmp = tempfile::tempdir().unwrap();
let config = Config {
address_book: scope::config::AddressBookConfig {
data_dir: Some(tmp.path().to_path_buf()),
},
..Default::default()
};
let http: std::sync::Arc<dyn scope::http::HttpClient> =
std::sync::Arc::new(scope::http::NativeHttpClient::new().unwrap());
let factory = DefaultClientFactory {
chains_config: config.chains.clone(),
http,
};
let state = std::sync::Arc::new(AppState::new(config, factory));
let req = InsightsRequest {
target: "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2".to_string(),
chain: Some("bitcoin".to_string()), decode: false,
trace: false,
};
let response = handle(State(state), axum::Json(req)).await.into_response();
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
let body = axum::body::to_bytes(response.into_body(), 1_000_000)
.await
.unwrap();
let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert!(
json["error"]
.as_str()
.unwrap()
.contains("Unsupported chain")
);
}
#[tokio::test]
async fn test_handle_insights_label_not_found() {
use crate::AppState;
use axum::extract::State;
use axum::http::StatusCode;
use axum::response::IntoResponse;
use scope::chains::DefaultClientFactory;
use scope::config::Config;
let tmp = tempfile::tempdir().unwrap();
let config = Config {
address_book: scope::config::AddressBookConfig {
data_dir: Some(tmp.path().to_path_buf()),
},
..Default::default()
};
let http: std::sync::Arc<dyn scope::http::HttpClient> =
std::sync::Arc::new(scope::http::NativeHttpClient::new().unwrap());
let factory = DefaultClientFactory {
chains_config: config.chains.clone(),
http,
};
let state = std::sync::Arc::new(AppState::new(config, factory));
let req = InsightsRequest {
target: "@no-such-label".to_string(),
chain: None,
decode: false,
trace: false,
};
let response = handle(State(state), axum::Json(req)).await.into_response();
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
let body = axum::body::to_bytes(response.into_body(), 1_000_000)
.await
.unwrap();
let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert!(json["error"].as_str().unwrap().contains("@no-such-label"));
}
}