|
| 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