# Author: Simon-Pierre Boucher # Mail: contact@spboucher.ai """Unit checks for the EVM ingestion loop (plain asserts — run with python).""" import pathlib import sys import threading sys.path.insert(0, str(pathlib.Path(__file__).parent.parent)) from indexer import db from indexer.decode import TRANSFER_TOPIC from indexer.ingest import ChainIndexer from indexer.rpc import RpcError USDT = "0xdac17f958d2ee523a2206206994597c13d831ec7" CFG = {"rpcs": ["http://unused.invalid"], "max_range": 8, "confirmations": 2} TOKENS = [{"symbol": "USDT", "address": USDT, "decimals": 6, "native": True}] def fake_log(block, log_index=0, amount=1_000_000): pad = lambda a: "0x" + a[2:].rjust(64, "0") return { "address": USDT, "topics": [TRANSFER_TOPIC, pad("0x" + "11" * 20), pad("0x" + "22" * 20)], "data": hex(amount), "blockNumber": hex(block), "blockHash": "0x" + f"{block:x}".rjust(64, "b"), "transactionHash": "0x" + f"{block:x}{log_index:x}".rjust(64, "a"), "logIndex": hex(log_index), } class FakePool: """Serves synthetic logs; refuses getLogs ranges wider than `cap`.""" def __init__(self, cap=2, blocks_with_logs=()): self.cap = cap self.blocks = set(blocks_with_logs) self.getlogs_calls = [] self.rotations = 0 def call(self, method, params=None): if method == "eth_getLogs": f = params[0] frm, to = int(f["fromBlock"], 16), int(f["toBlock"], 16) self.getlogs_calls.append((frm, to)) if to - frm + 1 > self.cap: raise RpcError(-32005, "query returned more than 10000 results") return [fake_log(b) for b in range(frm, to + 1) if b in self.blocks] if method == "eth_getBlockByNumber": n = int(params[0], 16) return {"number": hex(n), "timestamp": hex(1_785_000_000 + n), "hash": "0x" + f"{n:x}".rjust(64, "b"), "parentHash": "0x" + f"{n - 1:x}".rjust(64, "b")} raise AssertionError(f"unexpected method {method}") def batch(self, calls): return [self.call(m, p) for m, p in calls] def rotate(self): self.rotations += 1 @property def current_url(self): return "http://fake" def stats(self): return [] def make_indexer(pool): ix = ChainIndexer("testchain", CFG, TOKENS, ":memory:") ix.pool = pool ix.conn = db.connect(":memory:") return ix def test_try_range_halves_and_stays_contiguous(): pool = FakePool(cap=2, blocks_with_logs={101, 103, 105}) ix = make_indexer(pool) stop = threading.Event() covered, rows = 100, 0 while covered < 106: end, n = ix.try_range(covered + 1, 106, stop) assert end > covered, "no forward progress" covered, rows = end, rows + n assert covered == 106 assert rows == 3, f"expected 3 transfers, got {rows}" assert ix.range == 2, "range should have halved to the node's cap" # every accepted sweep must be adjacent to the previous one (no gaps) accepted = [c for c in pool.getlogs_calls if c[1] - c[0] + 1 <= pool.cap] for (f1, t1), (f2, _) in zip(accepted, accepted[1:]): assert f2 == t1 + 1, f"gap between sweeps: ..{t1} then {f2}.." def test_decode_lands_in_db(): pool = FakePool(cap=10, blocks_with_logs={42}) ix = make_indexer(pool) n = ix.process_range(40, 45) assert n == 1 row = ix.conn.execute("SELECT * FROM transfers").fetchone() assert row is not None (chain, block, _bh, _tx, _li, ts, token, symbol, frm, to, amount, decimals) = row assert (chain, block, token, symbol) == ("testchain", 42, USDT, "USDT") assert frm == "0x" + "11" * 20 and to == "0x" + "22" * 20 assert amount == "1000000" and decimals == 6 assert ts == 1_785_000_000 + 42 def test_rollback_removes_reorged_rows(): pool = FakePool(cap=10, blocks_with_logs={10, 11, 12}) ix = make_indexer(pool) ix.process_range(10, 12) db.set_cursor(ix.conn, "testchain", 12, "0xdead") db.rollback(ix.conn, "testchain", 10) left = [r[0] for r in ix.conn.execute("SELECT block FROM transfers")] assert left == [10], f"only block 10 should survive, got {left}" assert db.get_cursor(ix.conn, "testchain")[0] == 10 def test_parent_link_check(): pool = FakePool(cap=10) ix = make_indexer(pool) good = "0x" + f"{50:x}".rjust(64, "b") # header(51).parentHash assert ix.links_to_cursor(50, good) assert not ix.links_to_cursor(50, "0x" + "f" * 64) if __name__ == "__main__": for name, fn in sorted(globals().items()): if name.startswith("test_"): fn() print(f"ok {name}") print("all ingest tests passed")