use crate::chain::{ata_pda, SolClient};
use crate::checks::bundles::{mint_deltas_detailed, BuyerInfo, LaunchTx};
use crate::config;
use crate::types::{Flag, Severity};
use serde_json::{json, Value};
use std::collections::{BTreeSet, HashMap, HashSet};
const MAX_HOLDER_NODES: usize = 50;
const MAX_SNIPER_NODES: usize = 40;
const DUST_PCT: f64 = 0.01; const MAX_CLUSTER_SIZE: usize = 1000;
const UNION_THRESHOLD: f64 = 1.0;
const W_TRANSFER: f64 = 1.0;
const W_FUNDING: f64 = 1.0;
const W_FUNDING_ANCESTOR: f64 = 0.5;
const W_BUNDLE: f64 = 1.0;
const W_SAME_SLOT: f64 = 0.5;
const W_TIP_HEURISTIC: f64 = 0.4; const HUB_SIG_FLOOR: usize = 1000; const JITO_PROOF_BUDGET: usize = 15;
pub struct ClusterInputs<'a> {
pub holders: &'a [(String, u128)],
pub buyers: &'a [BuyerInfo],
pub dev: Option<&'a str>,
pub excluded: &'a HashSet<String>,
pub supply: u128,
pub launch_slot: Option<u64>,
pub launch_txs: &'a [LaunchTx],
pub launch_coverage: f64,
pub holders_live: bool,
}
pub struct ClusterOutput {
pub section: Value,
pub flags: Vec<Flag>,
pub supersedes_raw_top10: bool,
}
#[derive(Default, Clone)]
struct Node {
pct: f64,
roles: BTreeSet<&'static str>,
fresh: Option<bool>,
first_funder: Option<String>,
still_holds_pct: Option<f64>,
bought_pct: Option<f64>,
label: Option<String>,
}
struct Budget {
left: usize,
}
impl Budget {
fn take(&mut self, n: usize) -> bool {
if self.left >= n {
self.left -= n;
true
} else {
false
}
}
}
struct Dsu {
parent: Vec<usize>,
}
impl Dsu {
fn new(n: usize) -> Self {
Dsu {
parent: (0..n).collect(),
}
}
fn find(&mut self, i: usize) -> usize {
if self.parent[i] != i {
let r = self.find(self.parent[i]);
self.parent[i] = r;
}
self.parent[i]
}
fn union(&mut self, a: usize, b: usize) {
let (ra, rb) = (self.find(a), self.find(b));
if ra != rb {
self.parent[ra.max(rb)] = ra.min(rb);
}
}
}
fn round2(n: f64) -> f64 {
(n * 100.0).round() / 100.0
}
pub fn jito_tip(tx: &Value) -> Option<(String, u64)> {
let tips: HashSet<&str> = config::JITO_TIP_ACCOUNTS.iter().copied().collect();
let check = |ins: &Value| -> Option<(String, u64)> {
let parsed = ins.get("parsed")?;
if parsed.get("type").and_then(|t| t.as_str()) != Some("transfer") {
return None;
}
let info = parsed.get("info")?;
let dest = info.get("destination").and_then(|d| d.as_str())?;
if !tips.contains(dest) {
return None;
}
let lamports = info.get("lamports").and_then(|l| l.as_u64())?;
(lamports >= 1000).then(|| (dest.to_string(), lamports))
};
if let Some(list) = tx
.pointer("/transaction/message/instructions")
.and_then(|v| v.as_array())
{
for ins in list {
if let Some(hit) = check(ins) {
return Some(hit);
}
}
}
if let Some(groups) = tx
.pointer("/meta/innerInstructions")
.and_then(|v| v.as_array())
{
for g in groups {
if let Some(list) = g.get("instructions").and_then(|v| v.as_array()) {
for ins in list {
if let Some(hit) = check(ins) {
return Some(hit);
}
}
}
}
}
None
}
pub struct EvidenceGraph {
pub index: HashMap<String, usize>,
pub wallets: Vec<String>,
pub pair_weight: HashMap<(usize, usize), f64>,
pub edges: Vec<EdgeJson>,
}
pub struct EdgeJson {
pub from: String,
pub to: String,
pub kind: &'static str,
pub weight: f64,
pub amount: Option<String>,
pub at: Option<u64>,
pub proof: Option<String>,
}
impl EvidenceGraph {
pub fn new(wallets: Vec<String>) -> Self {
let index = wallets
.iter()
.enumerate()
.map(|(i, w)| (w.clone(), i))
.collect();
EvidenceGraph {
index,
wallets,
pair_weight: HashMap::new(),
edges: vec![],
}
}
#[allow(clippy::too_many_arguments)]
pub fn add(
&mut self,
a: &str,
b: &str,
kind: &'static str,
weight: f64,
amount: Option<String>,
at: Option<u64>,
proof: Option<String>,
) {
if a == b {
return;
}
if let (Some(&ia), Some(&ib)) = (self.index.get(a), self.index.get(b)) {
let key = (ia.min(ib), ia.max(ib));
*self.pair_weight.entry(key).or_insert(0.0) += weight;
self.edges.push(EdgeJson {
from: a.to_string(),
to: b.to_string(),
kind,
weight,
amount,
at,
proof,
});
}
}
pub fn add_display(
&mut self,
from: &str,
to: &str,
kind: &'static str,
weight: f64,
at: Option<u64>,
) {
self.edges.push(EdgeJson {
from: from.to_string(),
to: to.to_string(),
kind,
weight,
amount: None,
at,
proof: None,
});
}
pub fn clusters(&self) -> Vec<Vec<usize>> {
let mut dsu = Dsu::new(self.wallets.len());
for (&(a, b), &w) in &self.pair_weight {
if w >= UNION_THRESHOLD {
dsu.union(a, b);
}
}
let mut groups: HashMap<usize, Vec<usize>> = HashMap::new();
for i in 0..self.wallets.len() {
groups.entry(dsu.find(i)).or_default().push(i);
}
let mut out: Vec<Vec<usize>> = groups
.into_values()
.filter(|g| g.len() >= 2 && g.len() <= MAX_CLUSTER_SIZE)
.collect();
for g in &mut out {
g.sort();
}
out.sort();
out
}
}
pub fn ancestors_of(
wallet: &str,
funder_cache: &HashMap<String, Option<String>>,
chain_parent: &HashMap<String, String>,
funder_stop: &HashMap<String, bool>,
max_hops: usize,
) -> Vec<String> {
let mut out = vec![];
let mut cur = match funder_cache.get(wallet) {
Some(Some(f)) => f.clone(),
_ => return out,
};
for _ in 0..max_hops {
let stop = funder_stop.get(&cur).copied().unwrap_or(true);
out.push(cur.clone());
if stop {
break;
}
match chain_parent.get(&cur) {
Some(p) => cur = p.clone(),
None => break,
}
}
out
}
pub fn apply_funding_evidence(
graph: &mut EvidenceGraph,
core: &[String],
funder_cache: &HashMap<String, Option<String>>,
chain_parent: &HashMap<String, String>,
funder_stop: &HashMap<String, bool>,
) {
let max_hops = 3usize;
let mut by_direct: HashMap<String, Vec<String>> = HashMap::new();
let mut by_ancestor: HashMap<String, BTreeSet<String>> = HashMap::new();
for w in core {
let anc = ancestors_of(w, funder_cache, chain_parent, funder_stop, max_hops);
for (hop, f) in anc.iter().enumerate() {
if funder_stop.get(f).copied().unwrap_or(true) {
if hop == 0 && graph.index.contains_key(w) {
graph.add_display(f, w, "funding", 0.0, None);
}
break;
}
if hop == 0 {
by_direct.entry(f.clone()).or_default().push(w.clone());
}
by_ancestor.entry(f.clone()).or_default().insert(w.clone());
}
}
for (f, ws) in &mut by_direct {
ws.sort();
ws.dedup();
for w in ws.iter() {
graph.add_display(f, w, "funding", W_FUNDING, None);
}
for i in 0..ws.len() {
for j in i + 1..ws.len() {
graph.add(&ws[i], &ws[j], "funding", W_FUNDING, None, None, None);
}
}
}
for (f, ws) in &by_ancestor {
let ws: Vec<&String> = ws.iter().collect();
if ws.len() < 2 {
continue;
}
for i in 0..ws.len() {
for j in i + 1..ws.len() {
let direct_shared = by_direct
.get(f)
.map(|d| d.contains(ws[i]) && d.contains(ws[j]))
.unwrap_or(false);
if !direct_shared {
graph.add(
ws[i],
ws[j],
"funding",
W_FUNDING_ANCESTOR,
None,
None,
None,
);
}
}
}
}
}
pub async fn analyze(
client: &SolClient,
mint: &str,
inputs: ClusterInputs<'_>,
deep: bool,
) -> ClusterOutput {
let supply = inputs.supply;
let pct = |v: u128| -> f64 {
v.saturating_mul(10000)
.checked_div(supply)
.map(|bp| round2(bp as f64 / 100.0))
.unwrap_or(0.0)
};
let cex: HashMap<&str, &str> = config::cex_hot_wallets().into_iter().collect();
let mut budget = Budget {
left: if deep { 400 } else { 120 },
};
let mut notes: Vec<String> = vec![];
let mut nodes: HashMap<String, Node> = HashMap::new();
let mut core: Vec<String> = vec![];
let push_core = |addr: &str, nodes: &mut HashMap<String, Node>, core: &mut Vec<String>| {
if !nodes.contains_key(addr) {
nodes.insert(addr.to_string(), Node::default());
core.push(addr.to_string());
}
};
let mut holders_analyzed = 0usize;
for (owner, amount) in inputs.holders.iter().take(MAX_HOLDER_NODES) {
let p = pct(*amount);
if p < DUST_PCT || inputs.excluded.contains(owner) || cex.contains_key(owner.as_str()) {
continue;
}
push_core(owner, &mut nodes, &mut core);
let n = nodes.get_mut(owner).unwrap();
n.pct = p;
n.roles.insert("holder");
holders_analyzed += 1;
}
let mut snipers_analyzed = 0usize;
for b in inputs.buyers.iter().take(MAX_SNIPER_NODES) {
let bp = pct(b.amount);
if bp < DUST_PCT || inputs.excluded.contains(&b.owner) || cex.contains_key(b.owner.as_str())
{
continue;
}
push_core(&b.owner, &mut nodes, &mut core);
let n = nodes.get_mut(&b.owner).unwrap();
n.roles.insert("sniper");
n.bought_pct = Some(bp);
n.fresh = n.fresh.or(b.fresh);
if let Some(h) = b.held {
let hp = pct(h);
n.still_holds_pct = Some(hp);
if !n.roles.contains("holder") {
n.pct = hp;
}
}
snipers_analyzed += 1;
}
if let Some(d) = inputs.dev {
if !inputs.excluded.contains(d) {
push_core(d, &mut nodes, &mut core);
nodes.get_mut(d).unwrap().roles.insert("dev");
}
}
if core.len() < 2 {
return ClusterOutput {
section: json!({
"schema": 1,
"scope": {"holders_analyzed": holders_analyzed, "snipers_analyzed": snipers_analyzed,
"funder_hops": 0, "coverage": "unavailable",
"note": "fewer than two analyzable wallets — nothing to cluster"},
"nodes": [], "edges": [], "clusters": [],
"effective_concentration": Value::Null,
}),
flags: vec![],
supersedes_raw_top10: false,
};
}
let mut graph = EvidenceGraph::new(core.clone());
let buyer_by_owner: HashMap<&str, &BuyerInfo> = inputs
.buyers
.iter()
.map(|b| (b.owner.as_str(), b))
.collect();
let mut tipped_txs: HashMap<String, u64> = HashMap::new(); for lt in inputs.launch_txs {
if jito_tip(<.tx).is_some() {
tipped_txs.insert(lt.signature.clone(), lt.slot);
}
}
for lt in inputs.launch_txs {
let recips: Vec<String> = mint_deltas_detailed(<.tx, mint)
.into_iter()
.map(|d| d.owner)
.filter(|o| graph.index.contains_key(o))
.collect();
for i in 0..recips.len() {
for j in i + 1..recips.len() {
graph.add(
&recips[i],
&recips[j],
"bundle",
W_BUNDLE,
None,
Some(lt.slot),
None,
);
}
}
}
let mut jito_api_ok: Option<bool> = None;
let mut bundle_of_tx: HashMap<String, String> = HashMap::new();
{
let mut looked = 0usize;
let mut candidates: Vec<(&String, u64)> =
tipped_txs.iter().map(|(s, slot)| (s, *slot)).collect();
candidates.sort();
for (sig, _slot) in candidates {
if looked >= JITO_PROOF_BUDGET {
break;
}
let involves_core = inputs
.launch_txs
.iter()
.find(|t| &t.signature == sig)
.map(|t| {
graph.index.contains_key(&t.fee_payer)
|| mint_deltas_detailed(&t.tx, mint)
.iter()
.any(|d| graph.index.contains_key(&d.owner))
})
.unwrap_or(false);
if !involves_core {
continue;
}
looked += 1;
match client.jito_bundle_id(sig).await {
Some(bid) => {
jito_api_ok = Some(true);
bundle_of_tx.insert(sig.clone(), bid);
}
None => {
if jito_api_ok.is_none() {
jito_api_ok = Some(false);
}
}
}
}
}
let mut by_bundle: HashMap<String, BTreeSet<String>> = HashMap::new();
for lt in inputs.launch_txs {
let Some(bid) = bundle_of_tx.get(<.signature) else {
continue;
};
for d in mint_deltas_detailed(<.tx, mint) {
if graph.index.contains_key(&d.owner) {
by_bundle
.entry(bid.clone())
.or_default()
.insert(d.owner.clone());
}
}
if graph.index.contains_key(<.fee_payer) {
by_bundle
.entry(bid.clone())
.or_default()
.insert(lt.fee_payer.clone());
}
}
let mut proven_bundle_ids: Vec<String> = vec![];
for (bid, members) in &by_bundle {
let m: Vec<&String> = members.iter().collect();
if m.len() >= 2 {
proven_bundle_ids.push(bid.clone());
}
for i in 0..m.len() {
for j in i + 1..m.len() {
graph.add(
m[i],
m[j],
"bundle",
W_BUNDLE,
None,
None,
Some(format!("jito:{bid}")),
);
}
}
}
proven_bundle_ids.sort();
if let Some(launch_slot) = inputs.launch_slot {
let mut by_slot: HashMap<u64, Vec<&str>> = HashMap::new();
for b in inputs.buyers {
if graph.index.contains_key(&b.owner) && pct(b.amount) >= DUST_PCT {
by_slot.entry(b.first_slot).or_default().push(&b.owner);
}
}
for (slot, mut wallets) in by_slot {
wallets.sort();
wallets.dedup();
if wallets.len() < 3 {
continue; }
wallets.truncate(25); for i in 0..wallets.len() {
for j in i + 1..wallets.len() {
graph.add(
wallets[i],
wallets[j],
"same_block",
W_SAME_SLOT,
None,
Some(slot),
None,
);
let tipped = |w: &str| {
buyer_by_owner
.get(w)
.map(|b| b.signatures.iter().any(|s| tipped_txs.contains_key(s)))
.unwrap_or(false)
};
if tipped(wallets[i]) && tipped(wallets[j]) {
graph.add(
wallets[i],
wallets[j],
"bundle",
W_TIP_HEURISTIC,
None,
Some(slot),
None,
);
}
}
}
}
let _ = launch_slot;
}
let mut probe_order: Vec<String> = vec![];
if let Some(d) = inputs.dev {
probe_order.push(d.to_string());
}
for b in inputs.buyers.iter().take(MAX_SNIPER_NODES) {
if graph.index.contains_key(&b.owner) {
probe_order.push(b.owner.clone());
}
}
for (o, _) in inputs.holders.iter().take(MAX_HOLDER_NODES) {
if graph.index.contains_key(o) {
probe_order.push(o.clone());
}
}
probe_order.dedup();
let mut funder_cache: HashMap<String, Option<String>> = HashMap::new();
let mut hub_cache: HashMap<String, bool> = HashMap::new();
let mut funding_probes_done = 0usize;
let mut funding_probes_wanted = 0usize;
for w in &probe_order {
funding_probes_wanted += 1;
if funder_cache.contains_key(w) {
continue;
}
if !budget.take(4) {
break;
}
let f = match client.first_funding(w).await {
Ok((f, _count)) => f,
Err(_) => None,
};
funder_cache.insert(w.clone(), f);
funding_probes_done += 1;
}
let mut funder_stop: HashMap<String, bool> = HashMap::new();
let distinct_funders: BTreeSet<String> = funder_cache.values().flatten().cloned().collect();
for f in &distinct_funders {
let stop = if cex.contains_key(f.as_str()) {
true
} else if let Some(&h) = hub_cache.get(f) {
h
} else if budget.take(1) {
let h = client
.signatures(f, 1000, None)
.await
.map(|s| s.len() >= HUB_SIG_FLOOR)
.unwrap_or(false);
hub_cache.insert(f.clone(), h);
h
} else {
notes.push(
"funder-hub probes ran out of budget; unknown funders treated as stop-nodes".into(),
);
true
};
funder_stop.insert(f.clone(), stop);
}
let mut fundees: HashMap<String, Vec<String>> = HashMap::new();
for (w, f) in &funder_cache {
if let Some(f) = f {
fundees.entry(f.clone()).or_default().push(w.clone());
}
}
let mut chain_parent: HashMap<String, String> = HashMap::new(); let max_hops = 3usize;
let mut frontier: Vec<String> = distinct_funders
.iter()
.filter(|f| !funder_stop.get(*f).copied().unwrap_or(true))
.filter(|f| {
deep || fundees.get(*f).map(|v| v.len()).unwrap_or(0) >= 2 || {
inputs
.dev
.map(|d| funder_cache.get(d) == Some(&Some((*f).clone())))
.unwrap_or(false)
}
})
.cloned()
.collect();
for _hop in 1..max_hops {
let mut next = vec![];
for f in &frontier {
if chain_parent.contains_key(f) || !budget.take(4) {
continue;
}
if let Ok((Some(pf), _)) = client.first_funding(f).await {
let stop = cex.contains_key(pf.as_str()) || {
if let Some(&h) = hub_cache.get(&pf) {
h
} else if budget.take(1) {
let h = client
.signatures(&pf, 1000, None)
.await
.map(|s| s.len() >= HUB_SIG_FLOOR)
.unwrap_or(false);
hub_cache.insert(pf.clone(), h);
h
} else {
true
}
};
funder_stop.entry(pf.clone()).or_insert(stop);
chain_parent.insert(f.clone(), pf.clone());
if !stop {
next.push(pf);
}
}
}
frontier = next;
if frontier.is_empty() {
break;
}
}
let ancestors = |w: &str| -> Vec<String> {
ancestors_of(w, &funder_cache, &chain_parent, &funder_stop, max_hops)
};
for w in &core {
let anc = ancestors(w);
if let Some(f) = anc.first() {
nodes.get_mut(w).unwrap().first_funder = Some(f.clone());
}
}
apply_funding_evidence(
&mut graph,
&core,
&funder_cache,
&chain_parent,
&funder_stop,
);
let mut transfer_targets: Vec<&String> = core
.iter()
.filter(|w| {
let n = &nodes[*w];
n.roles.contains("holder") && !n.roles.contains("sniper")
})
.collect();
let mut rest: Vec<&String> = core
.iter()
.filter(|w| nodes[*w].roles.contains("holder") && nodes[*w].roles.contains("sniper"))
.collect();
transfer_targets.append(&mut rest);
let max_transfer_probes = if deep {
25
} else if inputs.launch_txs.is_empty() {
18
} else {
10
};
let dust_raw = (supply / 10_000).max(1); let mut transfer_in_from: HashMap<String, HashSet<String>> = HashMap::new();
for w in transfer_targets.into_iter().take(max_transfer_probes) {
if !budget.take(1) {
break;
}
let mut sigs: Vec<String> = vec![];
for tp in [config::SPL_TOKEN, config::TOKEN_2022] {
if let Some(ata) = ata_pda(w, mint, tp) {
if let Ok(list) = client.signatures(&ata, 30, None).await {
sigs.extend(
list.into_iter()
.filter(|s| s.err.is_none())
.map(|s| s.signature),
);
}
if !sigs.is_empty() {
break; }
}
}
let launch_sigs: HashSet<&str> = inputs
.launch_txs
.iter()
.map(|t| t.signature.as_str())
.collect();
for sig in sigs
.into_iter()
.filter(|s| !launch_sigs.contains(s.as_str()))
.take(4)
{
if !budget.take(1) {
break;
}
let Ok(tx) = client.transaction(&sig).await else {
continue;
};
let slot = tx.get("slot").and_then(|s| s.as_u64());
let deltas = mint_deltas_detailed(&tx, mint);
let gainers: Vec<&crate::checks::bundles::MintDelta> = deltas
.iter()
.filter(|d| graph.index.contains_key(&d.owner) && d.amount >= dust_raw)
.collect();
let losers: Vec<String> = losers_of(&tx, mint, dust_raw)
.into_iter()
.filter(|o| graph.index.contains_key(o))
.collect();
for g in &gainers {
for l in &losers {
if *l != g.owner {
graph.add(
l,
&g.owner,
"transfer",
W_TRANSFER,
Some(g.amount.to_string()),
slot,
None,
);
transfer_in_from
.entry(g.owner.clone())
.or_default()
.insert(l.clone());
}
}
}
}
}
let groups = graph.clusters();
let dev_chain: HashSet<String> = inputs
.dev
.map(|d| {
let mut s: HashSet<String> = ancestors(d).into_iter().collect();
s.insert(d.to_string());
s
})
.unwrap_or_default();
let mut clusters_json = vec![];
let mut cluster_of: HashMap<String, usize> = HashMap::new();
let mut largest_cluster_pct = 0.0f64;
let mut largest_cluster_bought = 0.0f64;
for (ci, g) in groups.iter().enumerate() {
let wallets: Vec<String> = g.iter().map(|&i| graph.wallets[i].clone()).collect();
for w in &wallets {
cluster_of.insert(w.clone(), ci + 1);
}
let pct_combined = round2(wallets.iter().map(|w| nodes[w].pct).sum());
let pct_bought = round2(
wallets
.iter()
.map(|w| nodes[w].bought_pct.unwrap_or(0.0))
.sum(),
);
let mut kind_w: HashMap<&str, f64> = HashMap::new();
let mut proof: Option<String> = None;
let wset: HashSet<&String> = wallets.iter().collect();
for e in &graph.edges {
if wset.contains(&e.from) && wset.contains(&e.to) && e.weight > 0.0 {
*kind_w.entry(e.kind).or_insert(0.0) += e.weight;
if proof.is_none() && e.proof.is_some() {
proof = e.proof.clone();
}
}
}
let mut kinds: Vec<(&str, f64)> = kind_w.into_iter().collect();
kinds.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
let reason = match (kinds.first(), kinds.len()) {
(Some(("bundle", _)), _) if proof.is_some() => "jito_bundle",
(Some((k, _)), n) if n <= 1 => match *k {
"funding" => "common_funder",
"transfer" => "transfer_ring",
"same_block" => "same_block_bundle",
"bundle" => "same_block_bundle",
_ => "mixed",
},
(Some((k, w1)), _) => {
let w2 = kinds.get(1).map(|(_, w)| *w).unwrap_or(0.0);
if *w1 >= w2 * 2.0 {
match *k {
"funding" => "common_funder",
"transfer" => "transfer_ring",
"same_block" => "same_block_bundle",
"bundle" => {
if proof.is_some() {
"jito_bundle"
} else {
"same_block_bundle"
}
}
_ => "mixed",
}
} else {
"mixed"
}
}
_ => "mixed",
};
let mut funder_count: HashMap<&String, usize> = HashMap::new();
for w in &wallets {
if let Some(f) = &nodes[w].first_funder {
*funder_count.entry(f).or_insert(0) += 1;
}
}
let root = {
let mut cands: Vec<(&String, usize)> =
funder_count.into_iter().filter(|(_, c)| *c >= 2).collect();
cands.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(b.0)));
cands.first().map(|(f, _)| (*f).clone()).or_else(|| {
proof
.as_ref()
.map(|p| p.trim_start_matches("jito:").to_string())
})
};
let fresh_known: Vec<bool> = wallets.iter().filter_map(|w| nodes[w].fresh).collect();
let fresh_ratio = if fresh_known.is_empty() {
Value::Null
} else {
json!(round2(
fresh_known.iter().filter(|f| **f).count() as f64 / fresh_known.len() as f64
))
};
largest_cluster_pct = largest_cluster_pct.max(pct_combined);
if pct_combined >= largest_cluster_pct {
largest_cluster_bought = pct_bought;
}
let note = match (reason, &root) {
("common_funder", Some(r)) => format!(
"{} wallets funded by {} hold {pct_combined}% combined",
wallets.len(),
short(r)
),
("jito_bundle", _) => format!(
"{} wallets PROVEN coordinated via a shared Jito bundle",
wallets.len()
),
("transfer_ring", _) => format!(
"{} wallets moving this token among themselves",
wallets.len()
),
("same_block_bundle", _) => format!(
"{} wallets bought in coordinated same-slot bursts",
wallets.len()
),
_ => format!(
"{} wallets linked by multiple evidence kinds",
wallets.len()
),
};
clusters_json.push(json!({
"id": ci + 1,
"wallets": wallets,
"pct_combined": pct_combined,
"pct_bought": pct_bought,
"reason": reason,
"root": root,
"fresh_ratio": fresh_ratio,
"proof": proof,
"note": note,
}));
}
let raw_top10: f64 = {
let mut ps: Vec<f64> = core.iter().map(|w| nodes[w].pct).collect();
ps.sort_by(|a, b| b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal));
round2(ps.iter().take(10).sum())
};
let entity_pcts: Vec<f64> = {
let mut ent: HashMap<usize, f64> = HashMap::new();
let mut singles: Vec<f64> = vec![];
for w in &core {
match cluster_of.get(w) {
Some(&c) => *ent.entry(c).or_insert(0.0) += nodes[w].pct,
None => singles.push(nodes[w].pct),
}
}
let mut all: Vec<f64> = ent.into_values().chain(singles).collect();
all.sort_by(|a, b| b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal));
all
};
let clustered_top10 = round2(entity_pcts.iter().take(10).sum());
let gini = gini_coefficient(&entity_pcts);
let full_launch = inputs.launch_coverage >= 0.999 && !inputs.launch_txs.is_empty();
let funding_complete = funding_probes_done >= funding_probes_wanted.min(core.len());
let coverage = if full_launch && inputs.holders_live && funding_complete {
"full"
} else if inputs.launch_txs.is_empty() && !inputs.holders_live {
"unavailable"
} else {
"partial"
};
if !inputs.holders_live {
notes.push("RPC blocked the live holder list — holder-side clustering limited".into());
}
if !full_launch {
if inputs.launch_txs.is_empty() {
notes.push(
"launch window not analyzed — clustering used funding + transfer evidence only"
.into(),
);
} else {
notes.push(format!(
"launch-window parse coverage {}%",
(inputs.launch_coverage * 100.0).round()
));
}
}
if !funding_complete {
notes.push(format!(
"funding probes covered {funding_probes_done}/{} wallets (budget)",
funding_probes_wanted.min(core.len())
));
}
match jito_api_ok {
Some(true) => notes.push(format!(
"Jito bundle-proof API live: {} proven bundle(s)",
proven_bundle_ids.len()
)),
Some(false) => notes.push("Jito proof API unavailable — tip heuristic only".into()),
None => {
if !tipped_txs.is_empty() {
notes.push(format!(
"{} Jito-tipped launch tx(s) (heuristic; proof not probed)",
tipped_txs.len()
));
}
}
}
if let Some(p) = client.rpc_pressure() {
notes.push(p);
}
let mut flags = vec![];
let honesty = if coverage == "full" {
""
} else {
" (clustering coverage was partial — treat this as a floor)"
};
if largest_cluster_pct >= 20.0 {
flags.push(Flag::new(
"cluster_dominance",
Severity::Danger,
"One entity controls a huge share",
format!("The largest wallet cluster holds {largest_cluster_pct}% of the supply across multiple wallets{honesty}."),
25,
));
} else if largest_cluster_pct >= 10.0 {
flags.push(Flag::new(
"cluster_dominance",
Severity::Warning,
"Coordinated wallets hold a notable share",
format!(
"The largest wallet cluster holds {largest_cluster_pct}% of the supply{honesty}."
),
12,
));
} else if largest_cluster_bought >= 20.0 {
flags.push(Flag::new(
"cluster_dominance",
Severity::Warning,
"A coordinated cluster bought big (mostly sold since)",
format!("A wallet cluster acquired {largest_cluster_bought}% at launch but now holds {largest_cluster_pct}%{honesty}."),
12,
));
}
if !dev_chain.is_empty() {
let dev_funded: Vec<&String> = core
.iter()
.filter(|w| {
nodes[*w].roles.contains("sniper")
&& !nodes[*w].roles.contains("dev")
&& ancestors(w).iter().any(|a| dev_chain.contains(a))
})
.collect();
if dev_funded.len() >= 3 {
flags.push(Flag::new(
"deployer_funded_snipers",
Severity::Danger,
"Deployer funded the snipers",
format!(
"{} launch-window buyers trace their funding back to the deployer — the \"snipers\" are the dev's own wallets.",
dev_funded.len()
),
25,
));
}
}
let insider_pct: f64 = core
.iter()
.filter(|w| {
let n = &nodes[*w];
n.roles.contains("holder")
&& !n.roles.contains("sniper")
&& !n.roles.contains("dev")
&& (n
.first_funder
.as_ref()
.map(|f| dev_chain.contains(f))
.unwrap_or(false)
|| transfer_in_from
.get(*w)
.map(|srcs| srcs.iter().any(|s| dev_chain.contains(s)))
.unwrap_or(false))
})
.map(|w| nodes[w].pct)
.sum();
let insider_pct = round2(insider_pct);
if insider_pct >= 10.0 {
flags.push(Flag::new(
"insider_holders",
Severity::Danger,
"Insider supply that never bought",
format!("Wallets tied to the creator hold {insider_pct}% of the supply without ever buying — it was transferred or pre-funded to them."),
20,
));
}
let mut supersedes = false;
if clustered_top10 >= 60.0 {
supersedes = coverage == "full";
flags.push(Flag::new(
"effective_concentration",
Severity::Danger,
"Extreme entity concentration",
format!("Merging coordinated wallets into entities, the top 10 control {clustered_top10}% of the supply (raw per-wallet number: {raw_top10}%){}{honesty}.",
if supersedes { " — this supersedes the raw top-10 read" } else { "" }),
30,
));
} else if clustered_top10 >= 30.0 {
supersedes = coverage == "full";
flags.push(Flag::new(
"effective_concentration",
Severity::Warning,
"High entity concentration",
format!("Merging coordinated wallets into entities, the top 10 control {clustered_top10}% of the supply (raw: {raw_top10}%){}{honesty}.",
if supersedes { " — this supersedes the raw top-10 read" } else { "" }),
15,
));
}
let bundled_held: f64 = {
let bundled_wallets: HashSet<&String> = graph
.edges
.iter()
.filter(|e| e.kind == "bundle" && (e.proof.is_some() || e.weight >= W_BUNDLE))
.flat_map(|e| [&e.from, &e.to])
.collect();
round2(bundled_wallets.iter().map(|w| nodes[*w].pct).sum())
};
if bundled_held >= 30.0 {
flags.push(Flag::new(
"bundled_supply_held",
Severity::Danger,
"Bundled wallets still hold a dominant share",
format!("Wallets that bought via coordinated bundles STILL hold {bundled_held}% of the supply — one actor's exit wipes the chart."),
25,
));
} else if bundled_held >= 10.0 {
flags.push(Flag::new(
"bundled_supply_held",
Severity::Warning,
"Bundled wallets still hold a notable share",
format!("Wallets that bought via coordinated bundles still hold {bundled_held}% of the supply."),
12,
));
}
if !proven_bundle_ids.is_empty() {
flags.push(Flag::new(
"jito_proven_bundle",
Severity::Warning,
"Jito-PROVEN coordinated launch buys",
format!(
"{} launch bundle(s) confirmed via Jito's bundle records — wallets buying in one bundle are provably a single actor.",
proven_bundle_ids.len()
),
10,
));
}
if coverage != "full" {
for f in &mut flags {
if f.severity == Severity::Good {
f.severity = Severity::Info;
}
}
}
let referenced: BTreeSet<String> = graph
.edges
.iter()
.flat_map(|e| [e.from.clone(), e.to.clone()])
.collect();
let mut nodes_json = vec![];
let mut ordered: Vec<&String> = core.iter().collect();
ordered.sort_by(|a, b| {
nodes[*b]
.pct
.partial_cmp(&nodes[*a].pct)
.unwrap_or(std::cmp::Ordering::Equal)
.then(a.cmp(b))
});
for w in ordered {
let n = &nodes[w];
nodes_json.push(json!({
"address": w,
"pct": n.pct,
"roles": n.roles.iter().collect::<Vec<_>>(),
"fresh": n.fresh,
"first_funder": n.first_funder,
"still_holds_pct": n.still_holds_pct,
"bought_pct": n.bought_pct,
"label": n.label,
"cluster": cluster_of.get(w),
}));
}
for addr in referenced {
if nodes.contains_key(&addr) {
continue;
}
let (role, label) = if let Some(l) = cex.get(addr.as_str()) {
("cex", Some(format!("{l} hot wallet")))
} else if funder_stop.get(&addr).copied().unwrap_or(false) {
("cex", Some("high-activity hub".to_string()))
} else {
("funder", None)
};
nodes_json.push(json!({
"address": addr,
"pct": 0.0,
"roles": [role],
"fresh": Value::Null,
"first_funder": chain_parent.get(&addr),
"still_holds_pct": Value::Null,
"bought_pct": Value::Null,
"label": label,
"cluster": Value::Null,
}));
}
let edges_json: Vec<Value> = graph
.edges
.iter()
.map(|e| {
json!({
"from": e.from, "to": e.to, "kind": e.kind, "weight": e.weight,
"amount": e.amount, "at": e.at, "proof": e.proof,
})
})
.collect();
let hops_walked = if chain_parent.is_empty() { 1 } else { max_hops };
let section = json!({
"schema": 1,
"scope": {
"holders_analyzed": holders_analyzed,
"snipers_analyzed": snipers_analyzed,
"funder_hops": hops_walked,
"coverage": coverage,
"note": if notes.is_empty() { "full-coverage clustering".to_string() } else { notes.join("; ") },
},
"nodes": nodes_json,
"edges": edges_json,
"clusters": clusters_json,
"effective_concentration": {
"top10_raw_pct": raw_top10,
"top10_clustered_pct": clustered_top10,
"largest_cluster_pct": round2(largest_cluster_pct),
"gini": gini,
},
});
ClusterOutput {
section,
flags,
supersedes_raw_top10: supersedes,
}
}
fn losers_of(tx: &Value, mint: &str, dust_raw: u128) -> Vec<String> {
let read = |key: &str| -> HashMap<String, u128> {
let mut m = HashMap::new();
if let Some(list) = tx
.pointer(&format!("/meta/{key}"))
.and_then(|v| v.as_array())
{
for b in list {
if b.get("mint").and_then(|v| v.as_str()) != Some(mint) {
continue;
}
let Some(owner) = b.get("owner").and_then(|v| v.as_str()) else {
continue;
};
let amt: u128 = b
.pointer("/uiTokenAmount/amount")
.and_then(|v| v.as_str())
.and_then(|s| s.parse().ok())
.unwrap_or(0);
*m.entry(owner.to_string()).or_insert(0) += amt;
}
}
m
};
let pre = read("preTokenBalances");
let post = read("postTokenBalances");
let mut out = vec![];
for (owner, before) in pre {
let after = post.get(&owner).copied().unwrap_or(0);
if before > after && before - after >= dust_raw {
out.push(owner);
}
}
out.sort();
out
}
fn gini_coefficient(pcts: &[f64]) -> Option<f64> {
let vals: Vec<f64> = pcts.iter().copied().filter(|p| *p > 0.0).collect();
let n = vals.len();
if n < 2 {
return None;
}
let mut sorted = vals.clone();
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let sum: f64 = sorted.iter().sum();
if sum <= 0.0 {
return None;
}
let weighted: f64 = sorted
.iter()
.enumerate()
.map(|(i, v)| (i as f64 + 1.0) * v)
.sum();
Some(round2(
(2.0 * weighted) / (n as f64 * sum) - (n as f64 + 1.0) / n as f64,
))
}
fn short(a: &str) -> String {
if a.len() < 12 {
a.to_string()
} else {
format!("{}…{}", &a[..4], &a[a.len() - 4..])
}
}