Download graph/entity_graph.py from Anish-g/btc-intel: direct link, hf CLI and curl.
- Browser
- Download file 4.21 kB
-
https://huggingface.co/spaces/Anish-g/btc-intel/resolve/main/graph/entity_graph.py
- Command line
-
hf download hf://spaces/Anish-g/btc-intel/graph/entity_graph.py
-
curl -L -o entity_graph.py https://huggingface.co/spaces/Anish-g/btc-intel/resolve/main/graph/entity_graph.py
4.21 kB
| """Collapse the wallet graph into one node per entity (cluster). | |
| This is the link-analysis view the dashboard draws: a few thousand entities | |
| instead of a few hundred thousand addresses. Flags from the collapse guard ride | |
| along, so an oversized, untrusted cluster is visibly untrusted on screen. | |
| """ | |
| from __future__ import annotations | |
| from collections.abc import Iterable | |
| import networkx as nx | |
| import config | |
| from .builder import Tx, graph_transactions | |
| from .clustering import Clustering | |
| ENTITY = "entity" | |
| def _as_transactions(source) -> list[Tx]: | |
| if isinstance(source, nx.MultiDiGraph): | |
| return list(graph_transactions(source)) | |
| return list(source) | |
| def build_entity_graph(source, clustering: Clustering, cfg: dict | None = None) -> nx.DiGraph: | |
| """Entity -> entity value flow. Self-loops (change, internal moves) are | |
| counted on the node rather than drawn as edges.""" | |
| cfg = cfg or config.load() | |
| txs = _as_transactions(source) | |
| g = nx.DiGraph() | |
| def entity(addr: str) -> str: | |
| return clustering.cluster_of(addr) or addr | |
| for cid, members in clustering.clusters.items(): | |
| g.add_node(cid, node_type=ENTITY, wallets=len(members), | |
| flag=clustering.flags.get(cid), ips=set(), txs=0, internal_txs=0, | |
| value_in=0.0, value_out=0.0) | |
| for tx in txs: | |
| mix = int(tx.txid in clustering.coinjoins) | |
| senders: dict[str, float] = {} | |
| for addr, value in tx.inputs: | |
| senders[entity(addr)] = senders.get(entity(addr), 0.0) + value | |
| receivers: dict[str, float] = {} | |
| for addr, value in tx.outputs: | |
| receivers[entity(addr)] = receivers.get(entity(addr), 0.0) + value | |
| total_in = sum(senders.values()) or 1.0 | |
| for eid in set(senders) | set(receivers): | |
| if eid not in g: | |
| g.add_node(eid, node_type=ENTITY, wallets=1, flag=None, ips=set(), | |
| txs=0, internal_txs=0, value_in=0.0, value_out=0.0) | |
| g.nodes[eid]["txs"] += 1 | |
| g.nodes[eid]["ips"].update(tx.ips) | |
| for eid, value in senders.items(): | |
| g.nodes[eid]["value_out"] += value | |
| for eid, value in receivers.items(): | |
| g.nodes[eid]["value_in"] += value | |
| for src, sent in senders.items(): | |
| share = sent / total_in | |
| for dst, received in receivers.items(): | |
| if src == dst: | |
| g.nodes[src]["internal_txs"] += 1 | |
| continue | |
| value = received * share | |
| if g.has_edge(src, dst): | |
| e = g.edges[src, dst] | |
| e["value"] += value | |
| e["count"] += 1 | |
| e["mix_count"] += mix | |
| e["txids"].append(tx.txid) | |
| e["last_seen"] = max(e["last_seen"], tx.timestamp) if tx.timestamp else e["last_seen"] | |
| else: | |
| # ponytail: full txid list per edge — fine at case scale; swap | |
| # for a count plus a sample if an edge ever holds millions. | |
| # mix_count: how many of those transactions were CoinJoins | |
| # (clustering.is_coinjoin). fusion.taint does not cross an | |
| # edge made of nothing else. | |
| g.add_edge(src, dst, value=value, count=1, mix_count=mix, txids=[tx.txid], | |
| first_seen=tx.timestamp, last_seen=tx.timestamp) | |
| for _, d in g.nodes(data=True): | |
| d["ips"] = sorted(d["ips"]) | |
| d["shared_ip_count"] = len(d["ips"]) | |
| return g | |
| def top_entities(g: nx.DiGraph, by: str = "value_out", limit: int = 10) -> list[tuple[str, float]]: | |
| return sorted(((n, d.get(by, 0)) for n, d in g.nodes(data=True)), | |
| key=lambda x: -x[1])[:limit] | |
| def summary(g: nx.DiGraph) -> dict: | |
| flagged = [n for n, d in g.nodes(data=True) if d.get("flag")] | |
| return {"entities": g.number_of_nodes(), "links": g.number_of_edges(), | |
| "flagged": len(flagged), | |
| "value": round(sum(d["value"] for *_, d in g.edges(data=True)), 8), | |
| "largest_entity": max((d["wallets"] for _, d in g.nodes(data=True)), default=0)} | |