File size: 8,694 Bytes
8f82127
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
#old version, do not refer




# indexer.py
import time
import json
import argparse
from decimal import Decimal, getcontext
from web3 import Web3
import sqlite3
from typing import List

# --- Configuration ---
RPC_URL = "https://polygon-rpc.com/"
POL_TOKEN = Web3.to_checksum_address("0x455e53CBB86018Ac2B8092FdCd39d8444aFFC3F6")

# Binance addresses provided (checksum)
BINANCE_ADDRESSES = {
    Web3.to_checksum_address("0xF977814e90dA44bFA03b6295A0616a897441aceC"),
    Web3.to_checksum_address("0xe7804c37c13166fF0b37F5aE0BB07A3aEbb6e245"),
    Web3.to_checksum_address("0x505e71695E9bc45943c58adEC1650577BcA68fD9"),
    Web3.to_checksum_address("0x290275e3db66394C52272398959845170E4DCb88"),
    Web3.to_checksum_address("0xD5C08681719445A5Fdce2Bda98b341A49050d821"),
    Web3.to_checksum_address("0x082489A616aB4D46d1947eE3F912e080815b08DA"),
}

# Decimal precision (enough for 18 decimals)
getcontext().prec = 40

# ERC20 Transfer event signature topic
TRANSFER_TOPIC = Web3.keccak(text="Transfer(address,address,uint256)").hex()

# --- DB helpers ---
DB_PATH = "src/pol_indexer.sqlite"

SCHEMA_SQL = open("src/schema.sql", "r").read()

def get_db_conn():
    conn = sqlite3.connect(DB_PATH, timeout=30)
    conn.row_factory = sqlite3.Row
    return conn

def init_db():
    conn = get_db_conn()
    cur = conn.cursor()
    cur.executescript(SCHEMA_SQL)
    # initialize meta last_processed_block to current block if missing
    cur.execute("SELECT value FROM meta WHERE key = 'last_processed_block'")
    row = cur.fetchone()
    if row is None:
        # set last_processed_block to current chain head to avoid backfill
        w3 = Web3(Web3.HTTPProvider(RPC_URL))
        head = w3.eth.block_number
        cur.execute("INSERT OR REPLACE INTO meta (key, value) VALUES (?, ?)", ("last_processed_block", str(head)))
        print(f"[DB] Initialized last_processed_block = {head} (real-time only, no backfill)")
    conn.commit()
    conn.close()

def get_last_processed_block() -> int:
    conn = get_db_conn()
    cur = conn.cursor()
    cur.execute("SELECT value FROM meta WHERE key = 'last_processed_block'")
    v = int(cur.fetchone()["value"])
    conn.close()
    return v

def set_last_processed_block(n: int):
    conn = get_db_conn()
    cur = conn.cursor()
    cur.execute("INSERT OR REPLACE INTO meta (key, value) VALUES (?, ?)", ("last_processed_block", str(n)))
    conn.commit()
    conn.close()

def insert_raw_log(tx_hash, log_index, block_number, address, topics, data):
    conn = get_db_conn()
    cur = conn.cursor()
    try:
        cur.execute("""
            INSERT OR IGNORE INTO raw_logs (tx_hash, log_index, block_number, address, topics, data, received_at)
            VALUES (?, ?, ?, ?, ?, ?, ?)
        """, (tx_hash, log_index, block_number, address, json.dumps(topics), data, int(time.time())))
        conn.commit()
    finally:
        conn.close()

def insert_transfer(tx_hash, block_number, log_index, from_addr, to_addr, amount_raw, amount_dec, token_address):
    conn = get_db_conn()
    cur = conn.cursor()
    try:
        cur.execute("""
            INSERT OR IGNORE INTO transfers
            (tx_hash, block_number, log_index, from_address, to_address, amount_raw, amount_dec, token_address, received_at)
            VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
        """, (tx_hash, block_number, log_index, from_addr, to_addr, str(amount_raw), str(amount_dec), token_address, int(time.time())))
        conn.commit()
    finally:
        conn.close()

def get_latest_cumulative() -> Decimal:
    conn = get_db_conn()
    cur = conn.cursor()
    cur.execute("SELECT cumulative_value FROM netflow ORDER BY created_at DESC LIMIT 1")
    row = cur.fetchone()
    conn.close()
    if row:
        return Decimal(row["cumulative_value"])
    return Decimal("0")

def append_cumulative(new_cum: Decimal):
    conn = get_db_conn()
    cur = conn.cursor()
    cur.execute("INSERT INTO netflow (created_at, cumulative_value) VALUES (?, ?)", (int(time.time()), str(new_cum)))
    conn.commit()
    conn.close()

# --- Web3 setup ---
w3 = Web3(Web3.HTTPProvider(RPC_URL))

# Minimal ABI for Transfer event decoding (so we can use contract.events)
ERC20_TRANSFER_ABI = [{
    "anonymous": False,
    "inputs": [
        {"indexed": True, "name": "from", "type": "address"},
        {"indexed": True, "name": "to", "type": "address"},
        {"indexed": False, "name": "value", "type": "uint256"}
    ],
    "name": "Transfer",
    "type": "event"
}]
token_contract = w3.eth.contract(address=POL_TOKEN, abi=ERC20_TRANSFER_ABI)

# --- Processing logic ---
def decode_transfer_log(log) -> dict:
    """
    Accepts a log (dict) and decodes it to a Transfer event using web3.
    Returns dict with keys: from, to, value (int)
    """
    # use process_log which needs the log in web3 format
    ev = token_contract.events.Transfer().process_log(log)
    return {
        "from": Web3.to_checksum_address(ev["args"]["from"]),
        "to": Web3.to_checksum_address(ev["args"]["to"]),
        "value": int(ev["args"]["value"])
    }

def process_logs_for_block(block_number: int) -> Decimal:
    """
    Fetch logs for POL token for a single block, store raw + normalized transfers,
    and return the net delta (Decimal) to add to cumulative netflow for Binance:
      +value if to_address in BINANCE_ADDRESSES
      -value if from_address in BINANCE_ADDRESSES
    """
    delta = Decimal("0")
    try:
        logs = w3.eth.get_logs({
            "fromBlock": block_number,
            "toBlock": block_number,
            "address": POL_TOKEN
        })
    except Exception as e:
        print(f"[RPC] get_logs failed for block {block_number}: {e}")
        return delta

    for log in logs:
        # insert raw
        tx_hash = log["transactionHash"].hex()
        log_index = log["logIndex"]
        insert_raw_log(tx_hash, log_index, block_number, log["address"], [t.hex() for t in log["topics"]], log["data"])

        # decode transfer
        try:
            decoded = decode_transfer_log(log)
        except Exception as e:
            print(f"[Decode] Failed to decode log {tx_hash} idx {log_index}: {e}")
            continue

        amt_raw = decoded["value"]                 # integer
        # convert to decimal string (POL has 18 decimals). Use Decimal for precision.
        amt_dec = (Decimal(amt_raw) / Decimal(10**18)).normalize()
        insert_transfer(tx_hash, block_number, log_index, decoded["from"], decoded["to"], amt_raw, str(amt_dec), POL_TOKEN)

        # compute delta for Binance
        if decoded["to"] in BINANCE_ADDRESSES:
            delta += (Decimal(amt_raw) / Decimal(10**18))
        if decoded["from"] in BINANCE_ADDRESSES:
            delta -= (Decimal(amt_raw) / Decimal(10**18))

        print(f"[POL Transfer] blk {block_number} | {decoded['from']} -> {decoded['to']} | {amt_dec} POL")

    return delta

# Main real-time loop
def start_realtime_loop(poll_interval: float = 1.5):
    print("[Indexer] Starting real-time loop")
    init_db()
    last = get_last_processed_block()

    while True:
        try:
            head = w3.eth.block_number
        except Exception as e:
            print("[RPC] cannot get block_number:", e)
            time.sleep(5)
            continue

        if head > last:
            # process blocks one by one (small windows)
            for blk in range(last + 1, head + 1):
                print(f"[Indexer] Processing block {blk} / {head}")
                delta = process_logs_for_block(blk)  # Decimal
                if delta != Decimal("0"):
                    # update cumulative netflow incrementally
                    current = get_latest_cumulative()
                    new_cum = (current + delta).normalize()
                    append_cumulative(new_cum)
                    print(f"[Netflow] Delta {delta} POL applied. New cumulative: {new_cum} POL")
                # update last processed block even if no logs, so we don't reprocess
                set_last_processed_block(blk)
                last = blk
        else:
            # nothing new
            time.sleep(poll_interval)

# CLI to get current netflow
def get_current_netflow() -> Decimal:
    init_db()
    return get_latest_cumulative()

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="POL -> Binance real-time indexer")
    parser.add_argument("cmd", choices=["start", "netflow"], help="start = run indexer, netflow = print current cumulative")
    args = parser.parse_args()

    if args.cmd == "start":
        start_realtime_loop()
    elif args.cmd == "netflow":
        v = get_current_netflow()
        print(f"Cumulative POL netflow to Binance: {v} POL")