Skip to content

Commit 2c72fdc

Browse files
Merge pull request #203 from stepandra/main
Add Uranus memepad + DeDust CPMM v3 swap parsers
2 parents 1773580 + d6b3914 commit 2c72fdc

File tree

9 files changed

+438
-4
lines changed

9 files changed

+438
-4
lines changed

datalake/converters/dex_trades.py

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,13 @@
22
from dataclasses import dataclass, asdict
33
import decimal
44
from typing import List
5-
from topics import TOPIC_DEX_SWAPS, TOPIC_GASPUMP_EVENTS, TOPIC_TONFUN, TOPIC_MEMESLAB
5+
from topics import (
6+
TOPIC_DEX_SWAPS,
7+
TOPIC_GASPUMP_EVENTS,
8+
TOPIC_TONFUN,
9+
TOPIC_MEMESLAB,
10+
TOPIC_URANUS,
11+
)
612
from loguru import logger
713
from converters.converter import Converter
814

@@ -16,6 +22,7 @@
1622
PROJECT_TONFUN = "ton.fun"
1723
PROJECT_GASPUMP = "gaspump"
1824
PROJECT_MEMESLAB = "memeslab"
25+
PROJECT_URANUS = "uranus"
1926

2027
EVENT_TYPE_TRADE = "trade"
2128
EVENT_TYPE_LAUNCH = "launch" # launch from bonding curve
@@ -66,7 +73,13 @@ def timestamp(self, obj):
6673
return obj['event_time']
6774

6875
def topics(self) -> List[str]:
69-
return [TOPIC_DEX_SWAPS, TOPIC_GASPUMP_EVENTS, TOPIC_TONFUN, TOPIC_MEMESLAB]
76+
return [
77+
TOPIC_DEX_SWAPS,
78+
TOPIC_GASPUMP_EVENTS,
79+
TOPIC_TONFUN,
80+
TOPIC_MEMESLAB,
81+
TOPIC_URANUS,
82+
]
7083

7184
def convert(self, obj, table_name=None):
7285
trades = []
@@ -211,6 +224,47 @@ def convert(self, obj, table_name=None):
211224
volume_ton=int(self.decode_numeric(obj['ton_amount'])) / 1e9,
212225
volume_usd=self.decode_numeric(obj['volume_usd']),
213226
))
227+
elif table_name == "uranus_trade":
228+
# Uranus memepad trades
229+
is_buy = obj['event_type'] == 'BuyEvent'
230+
ton_amount_raw = obj['amount_in'] if is_buy else obj['amount_out']
231+
common = {
232+
'tx_hash': obj['tx_hash'],
233+
'trace_id': obj['trace_id'],
234+
'project_type': PLATFORM_TYPE_LAUNCHPAD,
235+
'project': PROJECT_URANUS,
236+
'version': 1,
237+
'event_time': obj['event_time'],
238+
'pool_address': obj['meme_master'],
239+
'router_address': None,
240+
'query_id': None,
241+
'referral_address': None,
242+
'platform_tag': None,
243+
}
244+
trades.append(Trade(
245+
**common,
246+
event_type=EVENT_TYPE_TRADE,
247+
trader_address=obj['trader_address'],
248+
token_sold_address=TON_NATIVE_ADDRESS if is_buy else obj['meme_master'],
249+
token_bought_address=obj['meme_master'] if is_buy else TON_NATIVE_ADDRESS,
250+
amount_sold_raw=self.decode_numeric(obj['amount_in'] if is_buy else obj['amount_out']),
251+
amount_bought_raw=self.decode_numeric(obj['amount_out'] if is_buy else obj['amount_in']),
252+
volume_ton=int(self.decode_numeric(ton_amount_raw)) / 1e9,
253+
volume_usd=self.decode_numeric(obj['volume_usd']),
254+
))
255+
if is_buy:
256+
if obj.get('is_graduated') is True:
257+
trades.append(Trade(
258+
**common,
259+
event_type=EVENT_TYPE_LAUNCH,
260+
trader_address=None,
261+
token_sold_address=None,
262+
token_bought_address=None,
263+
amount_sold_raw=None,
264+
amount_bought_raw=None,
265+
volume_ton=None,
266+
volume_usd=None,
267+
))
214268

215269
for trade in trades:
216270
if trade.volume_ton is not None:

datalake/topics.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
TOPIC_GASPUMP_EVENTS = "ton.parsed.gaspump_trade"
1919
TOPIC_TONFUN = "ton.parsed.tonfun_bcl_trade"
2020
TOPIC_MEMESLAB = "ton.parsed.memeslab_trade_event"
21+
TOPIC_URANUS = "ton.parsed.uranus_trade"
2122

2223
TOPIC_DEX_POOLS = "ton.prices.dex_pool"
2324
TOPIC_EXTRA_NFT_SALES = "ton.parsed.extra_nft_sales"

parser/createdb.sql

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,30 @@ ALTER TABLE parsed.memeslab_trade_event DROP CONSTRAINT memeslab_trade_event_pke
190190
ALTER TABLE parsed.memeslab_trade_event ADD PRIMARY KEY (tx_hash, event_type);
191191
COMMIT;
192192

193+
-- Uranus memepad
194+
CREATE TABLE IF NOT EXISTS parsed.uranus_trade (
195+
tx_hash bpchar(44) NULL,
196+
trace_id bpchar(44) NULL,
197+
msg_hash bpchar(44) NULL,
198+
event_time int4 NULL,
199+
meme_master varchar NULL,
200+
event_type varchar NULL,
201+
trader_address varchar NULL,
202+
amount_in numeric NULL,
203+
amount_out numeric NULL,
204+
creator_fee numeric NULL,
205+
protocol_fee numeric NULL,
206+
partner_fee numeric NULL,
207+
referrer_fee numeric NULL,
208+
current_supply numeric NULL,
209+
raised_funds numeric NULL,
210+
is_graduated bool NULL,
211+
volume_usd numeric NULL,
212+
created timestamp NULL,
213+
updated timestamp NULL,
214+
CONSTRAINT uranus_trade_pkey PRIMARY KEY (tx_hash, event_type)
215+
);
216+
193217

194218
-- Adding usd volume for memepads
195219
ALTER TABLE parsed.gaspump_trade ADD column if not exists "volume_usd" numeric NULL;
@@ -266,6 +290,13 @@ EXCEPTION
266290
WHEN duplicate_object THEN null;
267291
END $$;
268292

293+
-- CPMM v3 DEX support
294+
DO $$ BEGIN
295+
ALTER TYPE public.dex_name ADD VALUE 'cpmm_pool_v3' AFTER 'dedust';
296+
EXCEPTION
297+
WHEN duplicate_object THEN null;
298+
END $$;
299+
269300
-- Staking pools
270301

271302
CREATE TABLE IF NOT EXISTS parsed.staking_pools_nominators (

parser/model/dexswap.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
DEX_BIDASK_CLMM = "bidask_clmm"
3636
DEX_BIDASK_DAMM = "bidask_damm"
3737
DEX_MOON = "moon.cx"
38+
DEX_DEDUST_CPMM_V3 = "cpmm_pool_v3"
3839

3940
@dataclass
4041
class DexSwapParsed:

parser/model/uranus.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import decimal
2+
from dataclasses import dataclass
3+
from typing import Optional
4+
5+
6+
@dataclass
7+
class UranusTradeEvent:
8+
__tablename__ = "uranus_trade"
9+
10+
tx_hash: str
11+
trace_id: str
12+
msg_hash: str
13+
event_time: int
14+
meme_master: str
15+
event_type: str # BuyEvent, SellEvent
16+
trader_address: str
17+
18+
# from BuyEvent & SellEvent
19+
amount_in: decimal.Decimal
20+
amount_out: decimal.Decimal
21+
22+
# from TradeFees
23+
creator_fee: decimal.Decimal
24+
protocol_fee: decimal.Decimal
25+
partner_fee: decimal.Decimal
26+
referrer_fee: decimal.Decimal
27+
28+
# from BuyEvent & SellEvent
29+
current_supply: decimal.Decimal
30+
raised_funds: decimal.Decimal
31+
32+
# from BuyEvent
33+
is_graduated: Optional[bool]
34+
35+
# extra info
36+
volume_usd: Optional[decimal.Decimal]

parser/parsers/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
from parsers.accounts.nft_sales import NFTSalesParser
2727
from parsers.message.moon_swap_ton import MoonSwapTON
2828
from parsers.jetton_transfer.moon_swap_jetton import MoonSwapJetton
29+
from parsers.message.dedust_swap_cpmm_v3 import CPMMV3Swap
30+
from parsers.message.uranus import UranusTrade
2931
from model.parser import Parser
3032
from loguru import logger
3133
import os
@@ -56,6 +58,8 @@
5658
BidaskDammSwap(EMULATOR_PATH),
5759
MoonSwapTON(),
5860
MoonSwapJetton(),
61+
CPMMV3Swap(EMULATOR_PATH),
62+
UranusTrade(),
5963

6064
CorePricesUSDT(),
6165
CorePricesLSDstTON(),

parser/parsers/accounts/tvl.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from db import DB
66
from pytoniq_core import Address
77
from model.dexpool import DexPool
8-
from model.dexswap import DEX_DEDUST, DEX_MEGATON, DEX_STON, DEX_STON_V2, DEX_TONCO, DEX_COFFEE, DEX_BIDASK_CLMM, DEX_BIDASK_DAMM, DEX_MOON
8+
from model.dexswap import DEX_DEDUST, DEX_MEGATON, DEX_STON, DEX_STON_V2, DEX_TONCO, DEX_COFFEE, DEX_BIDASK_CLMM, DEX_BIDASK_DAMM, DEX_MOON, DEX_DEDUST_CPMM_V3
99
from model.dedust import read_dedust_asset
1010
from model.coffee import read_coffee_asset
1111
from parsers.message.swap_volume import estimate_tvl
@@ -43,7 +43,7 @@ def _do_parse(self, obj, db: DB, emulator: TvmEmulator):
4343
pool.last_updated = obj['timestamp']
4444

4545
# total supply is required for all cases except TONCO, Bidask DLMM
46-
if pool.platform not in [DEX_TONCO, DEX_BIDASK_CLMM]:
46+
if pool.platform not in [DEX_TONCO, DEX_BIDASK_CLMM, DEX_DEDUST_CPMM_V3]:
4747
try:
4848
pool.total_supply, _, _, _, _= self._execute_method(emulator, 'get_jetton_data', [], db, obj)
4949
except EmulatorException as e:
@@ -210,6 +210,9 @@ def _do_parse(self, obj, db: DB, emulator: TvmEmulator):
210210
pool.lp_fee = lp_fee / 1e4 if lp_fee is not None else None
211211
pool.protocol_fee = protocol_fee / 1e4 if protocol_fee is not None else None
212212
pool.referral_fee = ref_fee / 1e4 if ref_fee is not None else None
213+
elif pool.platform == DEX_DEDUST_CPMM_V3:
214+
logger.warning(f"CPMM v3 TVL parsing not implemented for pool {pool.pool}")
215+
return
213216
else:
214217
raise Exception(f"DEX is not supported: {pool.platform}")
215218

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import base64
2+
from typing import Optional
3+
4+
from db import DB
5+
from loguru import logger
6+
from model.dexswap import DEX_DEDUST_CPMM_V3, DexSwapParsed
7+
from model.parser import TOPIC_MESSAGES, Parser
8+
from parsers.accounts.emulator import EmulatorParser
9+
from parsers.message.swap_volume import estimate_volume
10+
from pytoniq_core import Address, Cell
11+
12+
TON_NATIVE_ADDRESS = (
13+
"0:0000000000000000000000000000000000000000000000000000000000000000"
14+
)
15+
16+
17+
def _addr_or_zero(addr: Optional[Address]) -> str:
18+
return addr.to_str(is_user_friendly=False).upper() if addr else TON_NATIVE_ADDRESS
19+
20+
class CPMMV3Swap(EmulatorParser):
21+
"""Parses swaps emitted by CPMM v3 pools (default & uranus-linked)."""
22+
23+
POOL_CODE_HASHES = {
24+
"OZelwe6JI+k886apjqPvlILGTdiuorqDZWieFNmcdg0=",
25+
}
26+
27+
SWAP_EVENT_OPCODE = Parser.opcode_signed(0x78E79BA4)
28+
29+
def __init__(self, emulator_path: str):
30+
super().__init__(emulator_path)
31+
self.valid_pools: set[str] = set()
32+
33+
def topics(self):
34+
return [TOPIC_MESSAGES]
35+
36+
def predicate(self, obj) -> bool:
37+
return (
38+
obj.get("opcode") == self.SWAP_EVENT_OPCODE
39+
and obj.get("direction") == "out"
40+
and obj.get("destination") is None
41+
)
42+
43+
def prepare(self, db: DB):
44+
super().prepare(db)
45+
46+
def _is_valid_pool(self, pool_address: Address, pool_state: dict) -> bool:
47+
address_key = pool_address.to_str(is_user_friendly=False)
48+
if address_key in self.valid_pools:
49+
return True
50+
51+
if not pool_state or not pool_state.get("code_boc"):
52+
return False
53+
54+
code_hash = base64.b64encode(
55+
Cell.one_from_boc(pool_state["code_boc"])._hash
56+
).decode("utf-8")
57+
58+
if code_hash in self.POOL_CODE_HASHES:
59+
self.valid_pools.add(address_key)
60+
return True
61+
62+
logger.warning(f"Invalid CPMM v3 pool code hash {code_hash} for {address_key}")
63+
return False
64+
65+
def _parse_tokens(self, emulator, db: DB, pool_state: dict):
66+
# get_pool_data returns tuple [status, deposit_active, swap_active, assetX, assetY, ...]
67+
_, _, _, asset_x_cell, asset_y_cell, *_ = self._execute_method(
68+
emulator, "get_pool_data", [], db, pool_state
69+
)
70+
71+
def parse_asset(cell: Cell):
72+
slice_ = cell.begin_parse() if hasattr(cell, "begin_parse") else cell
73+
try:
74+
return slice_.load_address(), True
75+
except ValueError as exc:
76+
logger.warning(f"Failed to parse pool asset cell: {exc}")
77+
return None, False
78+
79+
asset_x, asset_x_ok = parse_asset(asset_x_cell)
80+
asset_y, asset_y_ok = parse_asset(asset_y_cell)
81+
if not asset_x_ok or not asset_y_ok:
82+
return None, None, False
83+
84+
return asset_x, asset_y, True
85+
86+
def handle_internal(self, obj, db: DB):
87+
tx_hash = Parser.require(obj.get("tx_hash"))
88+
if not Parser.require(db.is_tx_successful(tx_hash)):
89+
logger.info(f"Skipping failed tx for {tx_hash}")
90+
return
91+
92+
pool_address = Address(Parser.require(obj.get("source")))
93+
pool_state = Parser.get_account_state_safe(pool_address, db)
94+
if not pool_state or not pool_state.get("code_boc"):
95+
logger.warning(
96+
f"Account state missing for {pool_address.to_str(is_user_friendly=False)}"
97+
)
98+
return
99+
if not self._is_valid_pool(pool_address, pool_state):
100+
return
101+
102+
cell = Parser.message_body(obj, db).begin_parse()
103+
opcode_prefix = Parser.opcode_signed(cell.load_uint(32))
104+
if opcode_prefix != self.SWAP_EVENT_OPCODE:
105+
logger.debug(f"Unexpected opcode {opcode_prefix} for CPMM swap")
106+
return
107+
108+
x_to_y = cell.load_bit()
109+
amount_in = cell.load_coins()
110+
amount_out = cell.load_coins()
111+
if amount_in == 0 or amount_out == 0:
112+
logger.info(f"Skipping zero amount swap for {tx_hash}")
113+
return
114+
115+
initiator = cell.load_address()
116+
cell.load_address()
117+
cell.load_ref()
118+
fees = cell.load_ref().begin_parse()
119+
fees.load_bit()
120+
lp_fee = fees.load_coins()
121+
creator_fee = fees.load_coins()
122+
protocol_fee = fees.load_coins()
123+
partner_fee = fees.load_coins()
124+
referrer_fee = fees.load_coins()
125+
126+
pool_emulator = self._prepare_emulator(pool_state)
127+
asset_x, asset_y, assets_ok = self._parse_tokens(pool_emulator, db, pool_state)
128+
if not assets_ok:
129+
logger.warning(
130+
f"Missing pool assets for {pool_address.to_str(is_user_friendly=False)}"
131+
)
132+
return
133+
src_token = asset_x if x_to_y else asset_y
134+
dst_token = asset_y if x_to_y else asset_x
135+
136+
swap = DexSwapParsed(
137+
tx_hash=tx_hash,
138+
msg_hash=Parser.require(obj.get("msg_hash")),
139+
trace_id=Parser.require(obj.get("trace_id")),
140+
platform=DEX_DEDUST_CPMM_V3,
141+
swap_utime=Parser.require(obj.get("created_at")),
142+
swap_user=initiator.to_str(is_user_friendly=False).upper()
143+
if initiator
144+
else None,
145+
swap_pool=pool_address.to_str(is_user_friendly=False).upper(),
146+
swap_src_token=_addr_or_zero(src_token),
147+
swap_dst_token=_addr_or_zero(dst_token),
148+
swap_src_amount=amount_in,
149+
swap_dst_amount=amount_out,
150+
referral_address=None,
151+
query_id=None,
152+
)
153+
154+
estimate_volume(swap, db)
155+
db.serialize(swap)
156+
db.discover_dex_pool(swap)

0 commit comments

Comments
 (0)