온체인 읽기 2편, 코드로 한 코인의 매수자를 전부 뽑았다
이 글은 개인 공부 기록이며 투자 조언이 아닙니다. 등장하는 토큰과 주소는 설명을 위한 예시이자 누구나 볼 수 있는 공개 온체인 데이터이며, 추천이 아닙니다.

1편에서는 거래 하나를 손으로 읽었다. 어떤 사람이 SPACEX라는 토큰을 0.0116 ETH에 산 그 거래다. 근데 코인 하나에는 이런 거래가 수십, 수백, 많으면 수만 개가 쌓인다. 손으로는 못 읽는다.
그래서 이번엔 코드로 한 번에 긁는다. 목표는 하나다. SPACEX를 산 지갑을 전부 뽑아서, 누가 언제 얼마나 샀는지 표로 만든다. 탐색기를 클릭하는 걸 드디어 벗어나는 단계다.
스무 줄이면 된다
특별한 라이브러리도 안 쓴다. 파이썬 기본만으로 블록체인 노드에 직접 물어본다. 노드에 물어보는 창구를 RPC라고 하는데, 공개된 무료 RPC 주소가 여럿 있다.
import json, urllib.request, time
from collections import Counter
RPC = "https://base.drpc.org"
TOKEN = "0xa72c048366469d407a2739bfa58b6f5542f2a435" # SPACEX
TRANSFER = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"
def rpc(method, params):
body = json.dumps({"jsonrpc":"2.0","id":1,"method":method,"params":params}).encode()
req = urllib.request.Request(RPC, data=body, headers={"Content-Type":"application/json"})
return json.loads(urllib.request.urlopen(req, timeout=30).read())["result"]
# 최근 15000블록의 Transfer 이벤트를 1000블록씩 나눠서 긁는다
latest = int(rpc("eth_blockNumber", []), 16)
logs, b = [], latest - 15000
while b < latest:
to = min(b + 1000, latest)
logs += rpc("eth_getLogs", [{"fromBlock":hex(b), "toBlock":hex(to),
"address":TOKEN, "topics":[TRANSFER]}])
b = to + 1
time.sleep(0.25)
한 줄씩 뜯으면 이렇다. TRANSFER는 1편에서 본 그 토큰 이동 이벤트를 가리키는 고유 번호다. 이 번호로 필터를 걸면, SPACEX가 손바뀜한 기록만 골라서 가져온다. eth_getLogs가 "이 조건에 맞는 이벤트 다 줘"라는 요청이다.
주의할 게 하나 있었다. 한 번에 너무 넓은 범위를 달라고 하면 무료 RPC가 거절한다. 그래서 1,000블록씩 잘라서 여러 번 나눠 요청하고, 사이에 잠깐씩 쉰다. 처음엔 이걸 몰라서 계속 차단당했다. 공짜로 쓰는 대신 이런 벽이 있다.
이제 누가 샀는지 세기
가져온 이벤트를 풀어서, 보낸 지갑과 받은 지갑과 수량을 꺼낸다. 그리고 매수를 가려낸다. 매수는 유동성 풀에서 토큰이 지갑으로 나온 것이다. 풀 주소는 따로 몰라도, 거래에 가장 자주 등장하는 주소가 풀이라 그걸로 찾았다.
def ad(t): return "0x" + t[-40:]
rows = [(ad(l["topics"][1]), ad(l["topics"][2]), int(l["data"],16)/1e18,
int(l["blockNumber"],16)) for l in logs if l["data"] != "0x"]
freq = Counter()
for f, t, a, blk in rows: freq[f]+=1; freq[t]+=1
pool = freq.most_common(1)[0][0] # 가장 자주 나오는 주소 = 풀
buys = {}
for f, t, a, blk in rows:
if f == pool and t != pool:
d = buys.setdefault(t, {"tok":0, "n":0, "first":blk})
d["tok"] += a; d["n"] += 1; d["first"] = min(d["first"], blk)
돌렸더니 최근 15,000블록에서 SPACEX 이동이 74건, 매수가 63건, 매수한 지갑이 19개 나왔다. 그리고 풀 주소는 0x2407…8c0e로 잡혔는데, 1편에서 내 ETH가 흘러 들어간 그 풀과 같은 주소였다. 손으로 읽은 것과 코드로 긁은 게 이어지는 순간이라 좀 짜릿했다.
1편의 그 사람이 목록에 있었다
많이 산 지갑을 정렬해서 봤다.
| 지갑 | 산 수량 | 횟수 |
|---|---|---|
| 0x4752…ad24 | 41.9억 개 | 1 |
| 0x639f…8bc1 | 13.9억 개 | 2 |
| 0xb213…b9d8 | 2.25억 개 | 5 |
| 0x22f3…a3d4 | 2.18억 개 | 5 |
| 0xf32f…79d2 | 1.62억 개 | 4 |
맨 아래 0xf32f…79d2가 눈에 익다. 1편에서 우리가 지켜본 바로 그 사람이다. 1편에서는 한 번 사는 걸 봤는데, 전체를 긁어보니 이 사람은 네 번에 나눠서 총 1.6억 개를 샀다. 거래 하나만 볼 때는 안 보이던 게, 전부 모으니 보인다. 이게 코드로 긁는 이유다.
그런데 1위가 이상하다
표 맨 위 0x4752…ad24가 41.9억 개를 샀다고 나온다. 총 공급량이 56억 개인데 한 지갑이 42억 개를? 말이 안 된다.
찾아보니 이 주소는 사람이 아니라 라우터였다. 1편에서 "거래 창구"라고 불렀던 그 컨트랙트다. 매수를 중개하다 보니 토큰이 잠깐 이 창구를 거쳐 가는데, 내 단순한 코드는 그걸 "라우터가 샀다"고 잘못 센 거다.
이게 오늘 제일 크게 배운 거다. 긁어온 날것에는 사람과 배관이 섞여 있다. 라우터, 유동성 풀, MEV 봇 같은 건 사람이 아니라 거래를 처리하는 장치다. 이것들을 "매수자"로 세면 완전히 틀린 결론이 나온다. 진짜 사람만 골라내는 게 다음 숙제다.
2편에서 배운 것
거래 하나를 손으로 읽는 것에서, 코드로 전부 긁어서 표로 만드는 데까지 왔다. 파이썬 기본 라이브러리만으로 됐고, 특별한 도구는 없었다. 대신 무료 RPC의 벽(범위 제한, 차단)을 우회하는 법과, 날것에는 배관이 섞여 있다는 걸 배웠다.
다음 편에서는 그 배관을 걸러내고, 매수에 더해 매도까지 붙인다. 그러면 드디어 진짜 질문에 답할 수 있다. 누가 실제로 벌고, 누가 물렸나. 1편의 그 사람은 지금 웃고 있을까 울고 있을까. 그것도 코드로 확인해보겠다.