SPB Git

spb/coinexplorer Public MIT

Self-hosted, zero-API-key explorer for stablecoins and major crypto.

Python 60.3% HTML 23.6% JavaScript 8.1% CSS 6.8% SQL 1%
3.3 KB · 101 lines python
Raw Blame History
1#!/usr/bin/env python32# Author: Simon-Pierre Boucher3# Mail: contact@spboucher.ai4"""Run the indexer: one worker thread per chain + price/supply workers,5all under a watchdog that restarts any thread that dies.67    python3 run_indexer.py                        # all configured chains8    python3 run_indexer.py --chains ethereum base # a subset9"""1011import argparse12import logging13import threading1415from indexer import config, db16from indexer.adapters.bitcoin import BitcoinIndexer17from indexer.adapters.solana import SolanaIndexer18from indexer.adapters.tron import TronIndexer19from indexer.enrich import PriceWorker, StatsWorker, SupplyWorker20from indexer.ingest import ChainIndexer2122ADAPTERS = {23    "evm": ChainIndexer,24    "tron": TronIndexer,25    "solana": SolanaIndexer,26    "bitcoin": BitcoinIndexer,27}282930def main():31    ap = argparse.ArgumentParser()32    ap.add_argument("--chains", nargs="*", help="subset of chains (default: all)")33    ap.add_argument("--db", default=config.db_path())34    args = ap.parse_args()3536    logging.basicConfig(37        level=logging.INFO,38        format="%(asctime)s %(levelname)-7s %(message)s",39        datefmt="%H:%M:%S",40    )4142    chains = config.load_chains()43    tokens = config.load_tokens()44    selected = config.selected_chains(chains, tokens, args.chains, set(ADAPTERS))4546    conn = db.connect(args.db)  # main-thread conn: schema + token metadata only47    for chain in selected:48        db.upsert_chain(conn, chain, chains[chain].get("family", "evm"),49                        chains[chain].get("chain_id"))50        db.upsert_tokens(conn, chain, tokens[chain])51    conn.close()5253    stop = threading.Event()5455    # every worker is a (name, factory) pair so the watchdog can rebuild it —56    # workers keep their own retry loops; this is the belt AND the suspenders57    def chain_factory(c):58        return lambda: ADAPTERS[chains[c].get("family", "evm")](59            c, chains[c], tokens[c], args.db).run(stop)6061    factories = {c: chain_factory(c) for c in selected}62    factories["prices"] = lambda: PriceWorker(63        {c: tokens[c] for c in selected}, args.db).run(stop)64    factories["supply"] = lambda: SupplyWorker(65        chains, {c: tokens[c] for c in selected}, args.db).run(stop)66    factories["stats"] = lambda: StatsWorker(args.db).run(stop)6768    def spawn(name):69        t = threading.Thread(target=guarded(name), name=name, daemon=True)70        t.start()71        return t7273    def guarded(name):74        def inner():75            try:76                factories[name]()77            except Exception as e:  # last-resort: workers shouldn't get here78                logging.error("worker %s died: %s", name, e)79        return inner8081    threads = {name: spawn(name) for name in factories}82    logging.info("started %d chain workers + prices + supply (watchdog on)",83                 len(selected))8485    try:86        while not stop.is_set():87            stop.wait(30)88            for name, t in list(threads.items()):89                if not t.is_alive() and not stop.is_set():90                    logging.warning("watchdog: restarting dead worker %s", name)91                    threads[name] = spawn(name)92    except KeyboardInterrupt:93        logging.info("stopping...")94        stop.set()95        for t in threads.values():96            t.join(timeout=10)979899if __name__ == "__main__":100    main()101