diff --git a/src/polymarket_insider_tracker/profiler/funding.py b/src/polymarket_insider_tracker/profiler/funding.py index 34213e0..2c24b64 100644 --- a/src/polymarket_insider_tracker/profiler/funding.py +++ b/src/polymarket_insider_tracker/profiler/funding.py @@ -26,8 +26,13 @@ logger = logging.getLogger(__name__) USDC_BRIDGED = "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174" USDC_NATIVE = "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359" -# ERC20 Transfer event signature +# ERC20 Transfer event signature. ``HexBytes.hex()`` returns a *bare* hex +# string without the ``0x`` prefix; publicnode tolerates that, but stricter +# providers (e.g. drpc — which we use as the fallback) reject it with +# ``invalid argument 0: hex string without 0x prefix``. Always pass the +# 0x-prefixed form to ``eth_getLogs``. TRANSFER_EVENT_SIGNATURE = AsyncWeb3.keccak(text="Transfer(address,address,uint256)") +TRANSFER_EVENT_TOPIC = "0x" + TRANSFER_EVENT_SIGNATURE.hex().removeprefix("0x") # eth_getLogs block-range chunking. Most public Polygon RPCs (publicnode, ankr, # llamarpc) cap the range at 10_000 blocks per call; pick a window slightly @@ -278,7 +283,7 @@ class FundingTracer: # Pad address to 32 bytes for topic filter padded_to = "0x" + to_address.lower().replace("0x", "").zfill(64) topics = [ - TRANSFER_EVENT_SIGNATURE.hex(), # Transfer event + TRANSFER_EVENT_TOPIC, # Transfer event (must be 0x-prefixed for drpc) None, # from (any) padded_to, # to (target address) ] diff --git a/tests/profiler/test_funding.py b/tests/profiler/test_funding.py index 312cc65..fc61286 100644 --- a/tests/profiler/test_funding.py +++ b/tests/profiler/test_funding.py @@ -338,7 +338,9 @@ class TestGetTransferLogs: # Verify topics structure assert len(call_args["topics"]) == 3 - assert call_args["topics"][0] == TRANSFER_EVENT_SIGNATURE.hex() + # The Transfer event topic must be 0x-prefixed; drpc rejects bare hex. + assert call_args["topics"][0] == "0x" + TRANSFER_EVENT_SIGNATURE.hex().removeprefix("0x") + assert call_args["topics"][0].startswith("0x") assert call_args["topics"][1] is None # from (any) # to address should be padded to 32 bytes assert call_args["topics"][2].endswith(TEST_WALLET.lower().replace("0x", "")) @@ -587,6 +589,40 @@ class TestGetTransferLogs: assert DEFAULT_MAX_LOOKBACK_BLOCKS <= 100_000 + @pytest.mark.asyncio + async def test_get_transfer_logs_topic_is_0x_prefixed( + self, + funding_tracer: FundingTracer, + mock_polygon_client: MagicMock, + ) -> None: + """The Transfer event topic passed to ``eth_getLogs`` must begin with ``0x``. + + ``HexBytes.hex()`` returns a bare hex string. publicnode tolerates + that, but stricter providers like drpc (our fallback) reject it with + ``invalid argument 0: hex string without 0x prefix`` and every chunk + in the trace fails. This guards against regressing back to the + bare-hex form. + """ + mock_w3 = MagicMock() + mock_w3.eth.get_logs = AsyncMock(return_value=[]) + mock_polygon_client._w3 = mock_w3 + + await funding_tracer._get_transfer_logs( + to_address=TEST_WALLET, + token_address=USDC_BRIDGED, + from_block=1, + to_block=8_000, + ) + + topics = mock_w3.eth.get_logs.call_args[0][0]["topics"] + assert topics[0].startswith("0x") + # And the topic also has to be 32 bytes (64 hex chars) as required by + # the JSON-RPC spec. + assert len(topics[0]) == 2 + 64 + # The padded `to` topic was already 0x-prefixed; double-check that + # didn't regress either. + assert topics[2].startswith("0x") + class TestLogToFundingTransfer: """Tests for _log_to_funding_transfer method."""