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%
4.7 KB · 136 lines python
Raw Blame History
1# Author: Simon-Pierre Boucher2# Mail: contact@spboucher.ai3"""Unit checks for the EVM ingestion loop (plain asserts — run with python)."""45import pathlib6import sys7import threading89sys.path.insert(0, str(pathlib.Path(__file__).parent.parent))1011from indexer import db12from indexer.decode import TRANSFER_TOPIC13from indexer.ingest import ChainIndexer14from indexer.rpc import RpcError1516USDT = "0xdac17f958d2ee523a2206206994597c13d831ec7"17CFG = {"rpcs": ["http://unused.invalid"], "max_range": 8, "confirmations": 2}18TOKENS = [{"symbol": "USDT", "address": USDT, "decimals": 6, "native": True}]192021def fake_log(block, log_index=0, amount=1_000_000):22    pad = lambda a: "0x" + a[2:].rjust(64, "0")23    return {24        "address": USDT,25        "topics": [TRANSFER_TOPIC, pad("0x" + "11" * 20), pad("0x" + "22" * 20)],26        "data": hex(amount),27        "blockNumber": hex(block),28        "blockHash": "0x" + f"{block:x}".rjust(64, "b"),29        "transactionHash": "0x" + f"{block:x}{log_index:x}".rjust(64, "a"),30        "logIndex": hex(log_index),31    }323334class FakePool:35    """Serves synthetic logs; refuses getLogs ranges wider than `cap`."""3637    def __init__(self, cap=2, blocks_with_logs=()):38        self.cap = cap39        self.blocks = set(blocks_with_logs)40        self.getlogs_calls = []41        self.rotations = 04243    def call(self, method, params=None):44        if method == "eth_getLogs":45            f = params[0]46            frm, to = int(f["fromBlock"], 16), int(f["toBlock"], 16)47            self.getlogs_calls.append((frm, to))48            if to - frm + 1 > self.cap:49                raise RpcError(-32005, "query returned more than 10000 results")50            return [fake_log(b) for b in range(frm, to + 1) if b in self.blocks]51        if method == "eth_getBlockByNumber":52            n = int(params[0], 16)53            return {"number": hex(n), "timestamp": hex(1_785_000_000 + n),54                    "hash": "0x" + f"{n:x}".rjust(64, "b"),55                    "parentHash": "0x" + f"{n - 1:x}".rjust(64, "b")}56        raise AssertionError(f"unexpected method {method}")5758    def batch(self, calls):59        return [self.call(m, p) for m, p in calls]6061    def rotate(self):62        self.rotations += 16364    @property65    def current_url(self):66        return "http://fake"6768    def stats(self):69        return []707172def make_indexer(pool):73    ix = ChainIndexer("testchain", CFG, TOKENS, ":memory:")74    ix.pool = pool75    ix.conn = db.connect(":memory:")76    return ix777879def test_try_range_halves_and_stays_contiguous():80    pool = FakePool(cap=2, blocks_with_logs={101, 103, 105})81    ix = make_indexer(pool)82    stop = threading.Event()83    covered, rows = 100, 084    while covered < 106:85        end, n = ix.try_range(covered + 1, 106, stop)86        assert end > covered, "no forward progress"87        covered, rows = end, rows + n88    assert covered == 10689    assert rows == 3, f"expected 3 transfers, got {rows}"90    assert ix.range == 2, "range should have halved to the node's cap"91    # every accepted sweep must be adjacent to the previous one (no gaps)92    accepted = [c for c in pool.getlogs_calls if c[1] - c[0] + 1 <= pool.cap]93    for (f1, t1), (f2, _) in zip(accepted, accepted[1:]):94        assert f2 == t1 + 1, f"gap between sweeps: ..{t1} then {f2}.."959697def test_decode_lands_in_db():98    pool = FakePool(cap=10, blocks_with_logs={42})99    ix = make_indexer(pool)100    n = ix.process_range(40, 45)101    assert n == 1102    row = ix.conn.execute("SELECT * FROM transfers").fetchone()103    assert row is not None104    (chain, block, _bh, _tx, _li, ts, token, symbol, frm, to, amount, decimals) = row105    assert (chain, block, token, symbol) == ("testchain", 42, USDT, "USDT")106    assert frm == "0x" + "11" * 20 and to == "0x" + "22" * 20107    assert amount == "1000000" and decimals == 6108    assert ts == 1_785_000_000 + 42109110111def test_rollback_removes_reorged_rows():112    pool = FakePool(cap=10, blocks_with_logs={10, 11, 12})113    ix = make_indexer(pool)114    ix.process_range(10, 12)115    db.set_cursor(ix.conn, "testchain", 12, "0xdead")116    db.rollback(ix.conn, "testchain", 10)117    left = [r[0] for r in ix.conn.execute("SELECT block FROM transfers")]118    assert left == [10], f"only block 10 should survive, got {left}"119    assert db.get_cursor(ix.conn, "testchain")[0] == 10120121122def test_parent_link_check():123    pool = FakePool(cap=10)124    ix = make_indexer(pool)125    good = "0x" + f"{50:x}".rjust(64, "b")   # header(51).parentHash126    assert ix.links_to_cursor(50, good)127    assert not ix.links_to_cursor(50, "0x" + "f" * 64)128129130if __name__ == "__main__":131    for name, fn in sorted(globals().items()):132        if name.startswith("test_"):133            fn()134            print(f"ok {name}")135    print("all ingest tests passed")136