diff --git a/tests/test_chainseed.py b/tests/test_chainseed.py new file mode 100644 index 00000000..2bb0dcfb --- /dev/null +++ b/tests/test_chainseed.py @@ -0,0 +1,192 @@ +"""Stub tests for the chain-seed path (HANDOFF 2026-07-14): fills_from_tx +receipt decoding, forged-event rejection, addresses_in_payload tx hints, and +the on_wallet_activity hint funnel. Run: python3 tests/test_chainseed.py — +needs no network, no cache, no config; stubs the RPC and the engine. +""" +import os +import sys +import tempfile +import time + +sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")) +import copybot # noqa: E402 + +_TMP = tempfile.mkdtemp(prefix="copybot_test_") +WALLET = "0xbadaf319415c17f28824a43ae0cd912b9d84d874" +OTHER = "0x30fb41b5a08fff5d3dd983f6323e3343931a7db4" +EXCH = "0xe111180000d2663c0091e4f400237545b87b996b" +TOK = "113883491847873555347283937250168614361193102079209733484211339732062843429513" + + +def topic_addr(a): + return "0x" + a[2:].lower().rjust(64, "0") + + +def word(v): + return hex(v)[2:].rjust(64, "0") + + +def order_filled_log(maker, cls, token, given, taken, address=EXCH, + topic0=copybot._ORDER_FILLED): + return {"address": address, + "topics": [topic0, "0x" + "ab" * 32, + topic_addr(maker), topic_addr(EXCH)], + "data": "0x" + word(cls) + word(int(token)) + + word(given) + word(taken) + word(0) + word(0) + word(0)} + + +def stub_rpc(receipt_logs, block_ts): + def _rpc(method, params): + if method == "eth_getTransactionReceipt": + return {"blockNumber": "0x100", "logs": receipt_logs} + if method == "eth_getBlockByNumber": + return {"timestamp": hex(block_ts)} + raise AssertionError(f"unexpected rpc {method}") + return _rpc + + +fails = [] + + +def case(name): + def deco(fn): + rpc0, url0, meta0 = copybot._rpc, copybot._RPC_URL, copybot._token_market + try: + fn() + print(f"PASS {name}") + except AssertionError as e: + print(f"FAIL {name}: {e}") + fails.append(name) + finally: + copybot._rpc, copybot._RPC_URL = rpc0, url0 + copybot._token_market = meta0 + return deco + + +def with_meta(): + copybot._token_market = lambda tok: ("Test Market?", "Yes", "0xcond1") + + +@case("BUY decodes: cash given -> size/price/usd from the maker row") +def t1(): + copybot._RPC_URL = "stub" + ts = int(time.time()) - 3 + copybot._rpc = stub_rpc([ + # counterparty's own row — must be ignored (maker != wallet) + order_filled_log(OTHER, 0, TOK, 17_000_000, 20_000_000), + # the wallet's row: gave $3.00 cash, took 20 tokens -> BUY 20 @ 0.15 + order_filled_log(WALLET, 0, TOK, 3_000_000, 20_000_000), + ], ts) + with_meta() + fills = copybot.fills_from_tx("0xt1", WALLET) + assert len(fills) == 1, f"expected 1 fill, got {len(fills)}" + f = fills[0] + assert f["side"] == "BUY" and f["asset"] == TOK + assert abs(f["size"] - 20.0) < 1e-9 and abs(f["price"] - 0.15) < 1e-9 + assert abs(f["usdcSize"] - 3.0) < 1e-9 and f["timestamp"] == ts + assert f["title"] == "Test Market?" and f["conditionId"] == "0xcond1" + + +@case("SELL decodes: tokens given -> reversed amounts") +def t2(): + copybot._RPC_URL = "stub" + copybot._rpc = stub_rpc([ + # gave 10.68 tokens, took $5.1264 -> SELL 10.68 @ 0.48 + order_filled_log(WALLET, 1, TOK, 10_680_000, 5_126_400), + ], 1) + with_meta() + f = copybot.fills_from_tx("0xt2", WALLET)[0] + assert f["side"] == "SELL" + assert abs(f["size"] - 10.68) < 1e-9 and abs(f["price"] - 0.48) < 1e-9 + + +@case("forged events rejected: wrong emitter, wrong topic") +def t3(): + copybot._RPC_URL = "stub" + copybot._rpc = stub_rpc([ + # right shape, but emitted by a contract that is NOT a known exchange + order_filled_log(WALLET, 0, TOK, 3_000_000, 20_000_000, + address="0x" + "66" * 20), + # right emitter, wrong event signature + order_filled_log(WALLET, 0, TOK, 3_000_000, 20_000_000, + topic0="0x" + "77" * 32), + ], 1) + with_meta() + assert copybot.fills_from_tx("0xt3", WALLET) == [] + + +@case("fail-open: no RPC configured / metadata lookup down -> []") +def t4(): + copybot._RPC_URL = None + assert copybot.fills_from_tx("0xt4", WALLET) == [] + copybot._RPC_URL = "stub" + copybot._rpc = stub_rpc([ + order_filled_log(WALLET, 0, TOK, 3_000_000, 20_000_000)], 1) + copybot._token_market = lambda tok: None # gamma down + assert copybot.fills_from_tx("0xt4", WALLET) == [] + + +@case("addresses_in_payload maps watched wallets to their tx hashes") +def t5(): + payload = {"event": {"activity": [ + {"fromAddress": WALLET, "toAddress": EXCH, "hash": "0xaa"}, + {"fromAddress": EXCH, "toAddress": WALLET, "hash": "0xaa"}, # dup tx + {"fromAddress": WALLET, "toAddress": EXCH, "hash": "0xbb"}, + {"fromAddress": OTHER, "toAddress": EXCH, "hash": "0xcc"}, # unwatched + ]}} + got = copybot.addresses_in_payload(payload, {WALLET.lower()}) + assert got == {WALLET.lower(): ["0xaa", "0xbb"]}, got + + +@case("hint funnel: unseen hash seeds handle_trade; seen/skipped hashes don't") +def t6(): + calls = [] + + class Eng: + seen = {"0xseen"} + state = {"my_pos": {}, "trade_cursor": {}} + + def handle_trade(self, wallet, t): + calls.append(t["transactionHash"]) + self.seen.add(t["transactionHash"]) + + class Bot(copybot.Copybot): + def __init__(self): # bare wiring only + self.engine = Eng() + self.names = {} + self.skipped = {"0xskip"} + self.conds = {} + self.lock = __import__("threading").Lock() + + def _fetch_since_cursor(self, wallet): + return [] # the indexer is lagging + + def _drain_fills(self): + return [] + + def check_book(self): + return 0.0 + + class Filt: + def check(self, wallet, t): + return True, None + + bot = Bot() + bot.filt = Filt() + fresh = {"transactionHash": "0xnew", "asset": TOK, "side": "BUY", + "size": 20.0, "price": 0.15, "usdcSize": 3.0, + "title": "Test Market?", "outcome": "Yes", + "conditionId": "0xcond1", "timestamp": int(time.time())} + decoded = {"0xnew": [fresh]} + real = copybot.fills_from_tx + copybot.fills_from_tx = lambda tx, w: decoded.get(tx, []) + try: + bot.on_wallet_activity(WALLET, hint_txs=["0xseen", "0xskip", "0xnew"]) + finally: + copybot.fills_from_tx = real + assert calls == ["0xnew"], calls + + +print() +print("FAILURES:", fails or "none") +sys.exit(1 if fails else 0)