Восстановление истории и текущих балансов кошелька Polymarket напрямую через Polygon RPC — без API Polymarket и без готовых блокчейн-индексаторов.
Баланс каждого актива считается единственным способом, который не
требует доверия ничему, кроме самого блокчейна: как сумма входящих минус
сумма исходящих переводов по сырым ончейн-событиям, затем сверяется с
eth_call на том же блоке.
- USDC.e (bridged USDC), ERC20:
0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174 - native USDC, ERC20:
0x3c499c542cef5E3811e1192ce70d8cc03d5c3359 - Conditional Tokens Framework (позиции Polymarket), ERC1155:
0x4D97DCd97eC945f40cF65F87097ACe5EA0476045
Отслеживаются события Transfer (ERC20) и TransferSingle/TransferBatch
(ERC1155) — это единственные события, которые меняют баланс.
Три файла, один пайплайн:
config.py— RPC, подключение к Postgres, адреса контрактов, параметры сканирования. Никаких секретов в коде — всё из.env.ingest.py— сбор сырых событий переводов в Postgres:- Находит блок деплоя каждого контракта и самого кошелька через
бинарный поиск по
eth_getCode(кошелёк Polymarket — не обычный EOA, а proxy-контракт, у него тоже есть блок деплоя). Это не оценка и не обращение к внешнему сервису — просто нижняя граница сканирования, выведенная из RPC-примитива. - Сканирует диапазон блоков чанками, параллельно запрашивая
from=walletиto=walletдля каждого события (позиционные топики не позволяют выразить OR между разными полями одним запросом). - Если у провайдера сработал лимит на диапазон/объём ответа — рекурсивно делит диапазон пополам и запрашивает половины отдельно, вместо того чтобы держать в коде захардкоженный "магический" размер чанка под конкретный RPC.
- Пишет строки пачками (
execute_values), сON CONFLICT DO NOTHINGпо первичному ключу лога — безопасно перезапускать. - Продолжает с
MAX(block_number), уже закоммиченного в таблицах, если предыдущий запуск прервался — без отдельного checkpoint-файла. - В конце фиксирует
scanned_to_block— тот самый блок, до которого реально досканировано, а не "текущий последний".
- Находит блок деплоя каждого контракта и самого кошелька через
бинарный поиск по
balance.py— считает баланс SQL-агрегацией (SUM(inflow) - SUM(outflow)) из сырых событий и сверяет сeth_callна блоке, зафиксированном вscan_meta, а не на"latest".
Между тем, как ingest.py останавливается на блоке N, и моментом запуска
balance.py в блокчейне неизбежно проходит ещё сколько-то блоков — в них
могли пройти новые переводы этого же кошелька. Сверка с balanceOf(wallet)
на "latest" в этом случае сравнивает две разные точки во времени:
сумму событий по блок N против состояния на блоке N+k. Расхождение будет
выглядеть как ошибка в расчёте, хотя на самом деле это просто сравнение
несопоставимых моментов. Поэтому и SQL-фильтр (block_number <= scanned_to_block), и eth_call (block_identifier=scanned_to_block)
всегда смотрят на один и тот же зафиксированный номер блока.
cd reconstruct
pip install -r requirements.txt.env (создать в reconstruct/, не коммитить):
POLYGON_RPC_URL=... # архивный RPC с полным доступом к eth_getLogs
WALLET_ADDRESS=0x46b353667fd7d846af3bbeda6584b0e5b883d3de
PG_HOST=localhost
PG_PORT=5433
PG_DB=polymarket
PG_USER=postgres
PG_PASSWORD=...
Postgres можно поднять локально одной командой:
docker run -d --name pm-postgres -e POSTGRES_PASSWORD=... -e POSTGRES_DB=polymarket -p 5433:5432 postgres:16Про выбор RPC — важно: публичные балансировщики (polygon-rpc.com и
аналоги) режут глубину архива и диапазон eth_getLogs, из-за чего полное
сканирование истории физически невозможно. Нужен провайдер с настоящим
архивным доступом к логам (Infura/Alchemy free tier или публичный шлюз
Tenderly — все по-разному ограничивают eth_getLogs, отсюда и
адаптивное дробление диапазона в ingest.py, а не жёстко зашитое число).
Запуск:
python ingest.py # может занять от минут до часов — зависит от активности кошелька и лимитов RPC
python balance.py0x46b353667fd7d846af3bbeda6584b0e5b883d3de
Это не рядовой личный кошелёк, а proxy-контракт (деплоен в блоке
80 813 428, задолго до первого перевода), судя по паттерну активности —
высокочастотный маркет-мейкер/трейдер Polymarket:
- Первая транзакция — вызов
matchOrdersна CTF Exchange, а не входящий перевод, что типично для автоматизированного контрагента ордеров. - Вся релевантная история уложилась в диапазон блоков
80 813 428 — 93 817 332(~13 млн блоков, ~2025–2026 гг.) — контракты при этом задеплоены ещё в 2020 году. Если бы граница сканирования бралась от блока деплоя контрактов, а не кошелька, пришлось бы просканировать почти в 7 раз больше истории без единого релевантного события. - 238 797 переводов USDC.e и 464 483+ строк ERC1155-переводов
(включая разворачивание
TransferBatch). - 392 343 уникальные позиции Conditional Tokens с ненулевым балансом на момент сверки — то есть кошелёк одновременно держит позиции в сотнях тысяч разных исходов рынков.
Итоговые балансы на блоке 93 817 332:
| Актив | Баланс |
|---|---|
| USDC.e | 0 |
| native USDC | 0 |
| CTF-позиции | 392 343 позиции, все сошлись |
Нулевой баланс по обеим стейблкоинам — не аномалия и не признак ошибки: для активного маркет-мейкера это ожидаемо, весь капитал развёрнут в открытые позиции, а не лежит в свободном USDC.
Результат сверки: ВСЁ СОШЛОСЬ — 0 расхождений между суммой сырых
ончейн-событий и состоянием контрактов на блоке 93 817 332.
- Полное сканирование истории конкретного кошелька зависит от лимитов бесплатного RPC-провайдера — на очень активных кошельках счёт может идти на часы даже с адаптивным дроблением и параллелизмом.
- Не учитываются производные/обёрточные позиции вне ERC20/ERC1155 (например, промежуточные состояния внутри самого CTF Exchange до момента, пока актив не стал переводом одного из трёх отслеживаемых контрактов) — баланс считается по тем же примитивам, что видит и сам блокчейн.