168 lines
6.5 KiB
Python
Executable File
168 lines
6.5 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Arkham Intelligence connector — whale tracking, token flows, address intelligence.
|
|
|
|
Requires API key for most endpoints. Set ARKHAM_API_KEY env var.
|
|
Sign up at https://platform.arkhamintelligence.com
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import sys
|
|
from typing import Any
|
|
|
|
import requests
|
|
|
|
BASE = "https://api.arkhamintelligence.com"
|
|
TIMEOUT = 30
|
|
|
|
NOTABLE_ADDRESSES = {
|
|
"vitalik": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
|
|
"justin-sun": "0x3DdfA8eC3052539b6C9549F12cEA2C295cfF5296",
|
|
"binance-hot": "0x28C6c06298d514Db089934071355E5743bf21d60",
|
|
"coinbase-prime": "0xA9D1e08C7793af67e9d92fe308d5697FB81d3E43",
|
|
"aave-treasury": "0x464C71f6c2F760DdA6093dCB91C24c39e5d6e18c",
|
|
"uniswap-deployer": "0x41653c7d61609D856f29355E404F310Ec4142Cfb",
|
|
}
|
|
|
|
|
|
def _get(path: str, params: dict | None = None) -> Any:
|
|
key = os.environ.get("ARKHAM_API_KEY")
|
|
headers = {"User-Agent": "Mozilla/5.0"}
|
|
if key:
|
|
headers["API-Key"] = key
|
|
r = requests.get(f"{BASE}/{path}", params=params, headers=headers, timeout=TIMEOUT)
|
|
if r.status_code in (401, 403) or "api key" in r.text.lower():
|
|
raise EnvironmentError(
|
|
"Arkham API key required. Set ARKHAM_API_KEY env var.\n"
|
|
"Sign up at https://platform.arkhamintelligence.com"
|
|
)
|
|
r.raise_for_status()
|
|
return r.json()
|
|
|
|
|
|
def resolve_address(name_or_addr: str) -> str:
|
|
return NOTABLE_ADDRESSES.get(name_or_addr.lower(), name_or_addr)
|
|
|
|
|
|
# ── Data fetchers ───────────────────────────────────────────────────────────
|
|
|
|
def get_address_info(address: str) -> dict:
|
|
return _get(f"intelligence/address/{resolve_address(address)}")
|
|
|
|
|
|
def get_transfers(address: str, limit: int = 20) -> dict:
|
|
return _get("token/transfers", {"address": resolve_address(address), "limit": limit})
|
|
|
|
|
|
def search_entity(query: str) -> dict:
|
|
return _get("intelligence/search", {"query": query})
|
|
|
|
|
|
# ── Summary helpers ─────────────────────────────────────────────────────────
|
|
|
|
def summary_address(data: dict) -> str:
|
|
lines = ["═══ Address Intelligence ═══", ""]
|
|
if isinstance(data, dict):
|
|
entity = data.get("entity", {}) or {}
|
|
if entity:
|
|
lines.append(f" Entity: {entity.get('name', 'Unknown')}")
|
|
lines.append(f" Type: {entity.get('type', 'Unknown')}")
|
|
lines.append(f" Address: {data.get('address', '?')}")
|
|
labels = data.get("labels", [])
|
|
if labels:
|
|
lines.append(f" Labels: {', '.join(str(l) for l in labels)}")
|
|
else:
|
|
lines.append(f" {data}")
|
|
return "\n".join(lines)
|
|
|
|
|
|
def summary_transfers(data) -> str:
|
|
lines = ["═══ Recent Transfers ═══", ""]
|
|
transfers = data if isinstance(data, list) else (data.get("transfers", data.get("data", [])) if isinstance(data, dict) else [])
|
|
if not transfers:
|
|
lines.append(" No transfers found.")
|
|
return "\n".join(lines)
|
|
for t in transfers[:15]:
|
|
token = t.get("token", {}).get("symbol", "?") if isinstance(t.get("token"), dict) else "?"
|
|
amount = t.get("amount", t.get("value", "?"))
|
|
fr = t.get("from", {})
|
|
to = t.get("to", {})
|
|
fl = (fr.get("label") or fr.get("address", "?")[:12]) if isinstance(fr, dict) else str(fr)[:12]
|
|
tl = (to.get("label") or to.get("address", "?")[:12]) if isinstance(to, dict) else str(to)[:12]
|
|
lines.append(f" {token:<8} {str(amount):>15} {fl} → {tl}")
|
|
return "\n".join(lines)
|
|
|
|
|
|
def summary_notable() -> str:
|
|
lines = ["═══ Notable/Whale Addresses ═══", ""]
|
|
for name, addr in NOTABLE_ADDRESSES.items():
|
|
lines.append(f" {name:<20} {addr}")
|
|
lines.append("")
|
|
lines.append(" Use these as shortcuts: arkham.py address vitalik")
|
|
return "\n".join(lines)
|
|
|
|
|
|
# ── CLI ─────────────────────────────────────────────────────────────────────
|
|
|
|
def main():
|
|
common = argparse.ArgumentParser(add_help=False)
|
|
common.add_argument("--pretty", action="store_true", help="Pretty-print JSON output")
|
|
common.add_argument("--summary", action="store_true", help="Human-readable summary")
|
|
|
|
parser = argparse.ArgumentParser(description="Arkham Intelligence connector", parents=[common])
|
|
sub = parser.add_subparsers(dest="command", required=True)
|
|
|
|
p_addr = sub.add_parser("address", help="Address intelligence", parents=[common])
|
|
p_addr.add_argument("address", help="Ethereum address or notable name")
|
|
|
|
p_tx = sub.add_parser("transfers", help="Recent token transfers", parents=[common])
|
|
p_tx.add_argument("address")
|
|
p_tx.add_argument("--limit", type=int, default=20)
|
|
|
|
p_search = sub.add_parser("search", help="Search entities", parents=[common])
|
|
p_search.add_argument("query")
|
|
|
|
sub.add_parser("notable", help="List notable/whale addresses", parents=[common])
|
|
|
|
args = parser.parse_args()
|
|
|
|
try:
|
|
if args.command == "notable":
|
|
if args.summary:
|
|
print(summary_notable())
|
|
else:
|
|
json.dump(NOTABLE_ADDRESSES, sys.stdout, indent=2 if args.pretty else None)
|
|
print()
|
|
return
|
|
|
|
if args.command == "address":
|
|
data = get_address_info(args.address)
|
|
if args.summary:
|
|
print(summary_address(data)); return
|
|
result = data
|
|
elif args.command == "transfers":
|
|
data = get_transfers(args.address, args.limit)
|
|
if args.summary:
|
|
print(summary_transfers(data)); return
|
|
result = data
|
|
elif args.command == "search":
|
|
result = search_entity(args.query)
|
|
else:
|
|
parser.print_help(); return
|
|
|
|
json.dump(result, sys.stdout, indent=2 if args.pretty else None)
|
|
print()
|
|
|
|
except EnvironmentError as e:
|
|
print(str(e), file=sys.stderr); sys.exit(1)
|
|
except requests.HTTPError as e:
|
|
detail = e.response.text[:200] if e.response is not None else ""
|
|
print(json.dumps({"error": str(e), "detail": detail}), file=sys.stderr); sys.exit(1)
|
|
except Exception as e:
|
|
print(json.dumps({"error": f"{type(e).__name__}: {e}"}), file=sys.stderr); sys.exit(1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|