diff --git a/sol_parser/account_fillers/raydium.py b/sol_parser/account_fillers/raydium.py index cc45ac9..3979e2d 100644 --- a/sol_parser/account_fillers/raydium.py +++ b/sol_parser/account_fillers/raydium.py @@ -77,8 +77,9 @@ def fill_clmm_close_position_accounts(e: RaydiumClmmClosePositionEvent, get: Acc e.position_nft_mint = get(1) -def fill_cpmm_swap_accounts(_e: RaydiumCpmmSwapEvent, _get: AccountGetter) -> None: - pass +def fill_cpmm_swap_accounts(e: RaydiumCpmmSwapEvent, get: AccountGetter) -> None: + if _empty(e.pool_id): + e.pool_id = get(3) def fill_cpmm_deposit_accounts(e: RaydiumCpmmDepositEvent, get: AccountGetter) -> None: diff --git a/sol_parser/dex_parsers.py b/sol_parser/dex_parsers.py index c4567a9..ab97673 100644 --- a/sol_parser/dex_parsers.py +++ b/sol_parser/dex_parsers.py @@ -7,6 +7,7 @@ from __future__ import annotations +import base64 import struct from typing import Any, Dict, List, Optional @@ -1309,6 +1310,27 @@ def parse_amm_swap_out_from_data(data: bytes, meta: dict) -> Optional[DexEvent]: ) +def parse_amm_ray_log_swap(log: str, meta: dict) -> Optional[DexEvent]: + prefix = "ray_log: " + start = log.find(prefix) + if start < 0: + return None + try: + data = base64.b64decode(log[start + len(prefix):].strip(), validate=True) + except Exception: + return None + if len(data) != 57 or data[0] not in (3, 4): + return None + first = _u64le(data, 1) + second = _u64le(data, 9) + actual = _u64le(data, 49) + if data[0] == 3: + event = _amm_swap_event(meta, Z, Z, first, second, 0, actual) + else: + event = _amm_swap_event(meta, Z, Z, actual, 0, first, second) + return DexEvent(type=EventType.RAYDIUM_AMM_V4_SWAP, data=event) + + def parse_amm_deposit_from_data(data: bytes, meta: dict) -> Optional[DexEvent]: if len(data) < 32 + 32 + 8 + 8 + 8: return None @@ -1404,6 +1426,41 @@ def parse_amm_init2_from_data(data: bytes, meta: dict) -> Optional[DexEvent]: # --- Raydium CPMM --- +def parse_cpmm_swap_event_from_data(data: bytes, meta: dict) -> Optional[DexEvent]: + if len(data) < 32 + 6 * 8 + 1: + return None + o = 0 + pool = _pub(data, o) + o += 32 + input_vault_before = _u64le(data, o) + o += 8 + output_vault_before = _u64le(data, o) + o += 8 + input_amount = _u64le(data, o) + o += 8 + output_amount = _u64le(data, o) + o += 8 + input_transfer_fee = _u64le(data, o) + o += 8 + output_transfer_fee = _u64le(data, o) + o += 8 + base_input = _bool(data, o) + return DexEvent( + type=EventType.RAYDIUM_CPMM_SWAP, + data=RaydiumCpmmSwapEvent( + metadata=_make_meta(meta), + pool_id=pool, + input_vault_before=input_vault_before, + output_vault_before=output_vault_before, + input_amount=input_amount, + output_amount=output_amount, + input_transfer_fee=input_transfer_fee, + output_transfer_fee=output_transfer_fee, + base_input=base_input, + ), + ) + + def parse_cpmm_swap_in_from_data(data: bytes, meta: dict) -> Optional[DexEvent]: if len(data) < 32 + 32 + 8 + 8 + 8 + 1: return None @@ -2759,14 +2816,24 @@ def rp() -> str: # --- Meteora DLMM --- -DLMM_SWAP = _d(143, 190, 90, 218, 196, 30, 51, 222) -DLMM_ADD_LIQ = _d(181, 157, 89, 67, 143, 182, 52, 72) -DLMM_REMOVE_LIQ = _d(80, 85, 209, 72, 24, 206, 35, 178) -DLMM_INIT_POOL = _d(95, 180, 10, 172, 84, 174, 232, 40) +DLMM_SWAP = _d(81, 108, 227, 190, 205, 208, 10, 196) +DLMM_SWAP2 = _d(46, 116, 82, 215, 148, 27, 84, 77) +DLMM_ADD_LIQ = _d(31, 94, 125, 90, 227, 52, 61, 186) +DLMM_REMOVE_LIQ = _d(116, 244, 97, 232, 103, 31, 152, 58) +DLMM_INIT_POOL = _d(185, 74, 252, 125, 27, 215, 188, 111) DLMM_INIT_BIN = _d(11, 18, 155, 194, 33, 115, 238, 119) -DLMM_CREATE_POS = _d(123, 233, 11, 43, 146, 180, 97, 119) -DLMM_CLOSE_POS = _d(94, 168, 102, 45, 59, 122, 137, 54) -DLMM_CLAIM_FEE = _d(152, 70, 208, 111, 104, 91, 44, 1) +DLMM_CREATE_POS = _d(144, 142, 252, 84, 157, 53, 37, 121) +DLMM_CLOSE_POS = _d(255, 196, 16, 107, 28, 202, 53, 128) +DLMM_CLAIM_FEE = _d(75, 122, 154, 48, 140, 74, 123, 163) +DLMM_CLAIM_FEE2 = _d(232, 171, 242, 97, 58, 77, 35, 45) + +DLMM_LEGACY_SWAP = _d(143, 190, 90, 218, 196, 30, 51, 222) +DLMM_LEGACY_ADD_LIQ = _d(181, 157, 89, 67, 143, 182, 52, 72) +DLMM_LEGACY_REMOVE_LIQ = _d(80, 85, 209, 72, 24, 206, 35, 178) +DLMM_LEGACY_INIT_POOL = _d(95, 180, 10, 172, 84, 174, 232, 40) +DLMM_LEGACY_CREATE_POS = _d(123, 233, 11, 43, 146, 180, 97, 119) +DLMM_LEGACY_CLOSE_POS = _d(94, 168, 102, 45, 59, 122, 137, 54) +DLMM_LEGACY_CLAIM_FEE = _d(152, 70, 208, 111, 104, 91, 44, 1) def parse_dlmm_from_program_data(buf: bytes, meta: dict) -> Optional[DexEvent]: @@ -2774,7 +2841,11 @@ def parse_dlmm_from_program_data(buf: bytes, meta: dict) -> Optional[DexEvent]: return None d = _disc8(buf[:8]) data = buf[8:] - if d == DLMM_SWAP: + return parse_dlmm_event_from_data(d, data, meta) + + +def parse_dlmm_event_from_data(d: int, data: bytes, meta: dict) -> Optional[DexEvent]: + if d in (DLMM_SWAP, DLMM_LEGACY_SWAP): if len(data) < 32 + 32 + 4 + 4 + 8 + 8 + 1 + 8 + 8 + 16 + 8: return None o = 0 @@ -2816,7 +2887,51 @@ def parse_dlmm_from_program_data(buf: bytes, meta: dict) -> Optional[DexEvent]: host_fee=hf, ), ) - if d == DLMM_ADD_LIQ: + if d == DLMM_SWAP2: + if len(data) < 32 + 32 + 4 + 4 + 1 + 16 + 8 + 8 + 8 + 8 + 8 + 8 + 8 + 1 + 1: + return None + o = 0 + pool = _pub(data, o) + o += 32 + frm = _pub(data, o) + o += 32 + sb = _i32le(data, o) + o += 4 + eb = _i32le(data, o) + o += 4 + sy = _bool(data, o) + o += 1 + fbps = str(_u128le_int(data, o)) + o += 16 + ai = _u64le(data, o) + o += 8 + o += 8 + ao = _u64le(data, o) + o += 8 + fee = _u64le(data, o) + o += 8 + pf = _u64le(data, o) + o += 8 + o += 8 + hf = _u64le(data, o) + return DexEvent( + type=EventType.METEORA_DLMM_SWAP, + data=MeteoraDlmmSwapEvent( + metadata=_make_meta(meta), + pool=pool, + from_addr=frm, + start_bin_id=sb, + end_bin_id=eb, + amount_in=ai, + amount_out=ao, + swap_for_y=sy, + fee=fee, + protocol_fee=pf, + fee_bps=fbps, + host_fee=hf, + ), + ) + if d in (DLMM_ADD_LIQ, DLMM_LEGACY_ADD_LIQ): if len(data) < 32 + 32 + 32 + 8 + 8 + 4: return None o = 0 @@ -2842,7 +2957,7 @@ def parse_dlmm_from_program_data(buf: bytes, meta: dict) -> Optional[DexEvent]: active_bin_id=ab, ), ) - if d == DLMM_REMOVE_LIQ: + if d in (DLMM_REMOVE_LIQ, DLMM_LEGACY_REMOVE_LIQ): if len(data) < 32 + 32 + 32 + 8 + 8 + 4: return None o = 0 @@ -2869,6 +2984,23 @@ def parse_dlmm_from_program_data(buf: bytes, meta: dict) -> Optional[DexEvent]: ), ) if d == DLMM_INIT_POOL: + if len(data) < 32 + 2 + 32 + 32: + return None + o = 0 + pool = _pub(data, o) + o += 32 + bs = _u16le(data, o) + return DexEvent( + type=EventType.METEORA_DLMM_INITIALIZE_POOL, + data=MeteoraDlmmInitializePoolEvent( + metadata=_make_meta(meta), + pool=pool, + creator=Z, + active_bin_id=0, + bin_step=bs, + ), + ) + if d == DLMM_LEGACY_INIT_POOL: if len(data) < 32 + 32 + 4 + 2: return None o = 0 @@ -2908,6 +3040,26 @@ def parse_dlmm_from_program_data(buf: bytes, meta: dict) -> Optional[DexEvent]: ), ) if d == DLMM_CREATE_POS: + if len(data) < 32 + 32 + 32: + return None + o = 0 + pool = _pub(data, o) + o += 32 + pos = _pub(data, o) + o += 32 + owner = _pub(data, o) + return DexEvent( + type=EventType.METEORA_DLMM_CREATE_POSITION, + data=MeteoraDlmmCreatePositionEvent( + metadata=_make_meta(meta), + pool=pool, + position=pos, + owner=owner, + lower_bin_id=0, + width=0, + ), + ) + if d == DLMM_LEGACY_CREATE_POS: if len(data) < 32 + 32 + 32 + 4 + 4: return None o = 0 @@ -2932,6 +3084,22 @@ def parse_dlmm_from_program_data(buf: bytes, meta: dict) -> Optional[DexEvent]: ), ) if d == DLMM_CLOSE_POS: + if len(data) < 32 + 32: + return None + o = 0 + pos = _pub(data, o) + o += 32 + owner = _pub(data, o) + return DexEvent( + type=EventType.METEORA_DLMM_CLOSE_POSITION, + data=MeteoraDlmmClosePositionEvent( + metadata=_make_meta(meta), + pool=Z, + position=pos, + owner=owner, + ), + ) + if d == DLMM_LEGACY_CLOSE_POS: if len(data) < 32 + 32 + 32: return None o = 0 @@ -2949,7 +3117,7 @@ def parse_dlmm_from_program_data(buf: bytes, meta: dict) -> Optional[DexEvent]: owner=owner, ), ) - if d == DLMM_CLAIM_FEE: + if d in (DLMM_CLAIM_FEE, DLMM_LEGACY_CLAIM_FEE): if len(data) < 32 + 32 + 32 + 8 + 8: return None o = 0 @@ -2973,6 +3141,30 @@ def parse_dlmm_from_program_data(buf: bytes, meta: dict) -> Optional[DexEvent]: fee_y=fy, ), ) + if d == DLMM_CLAIM_FEE2: + if len(data) < 32 + 32 + 32 + 8 + 8 + 4: + return None + o = 0 + pool = _pub(data, o) + o += 32 + pos = _pub(data, o) + o += 32 + owner = _pub(data, o) + o += 32 + fx = _u64le(data, o) + o += 8 + fy = _u64le(data, o) + return DexEvent( + type=EventType.METEORA_DLMM_CLAIM_FEE, + data=MeteoraDlmmClaimFeeEvent( + metadata=_make_meta(meta), + pool=pool, + position=pos, + owner=owner, + fee_x=fx, + fee_y=fy, + ), + ) return None @@ -3129,6 +3321,7 @@ def event_type_for_program_discriminator(program_id: Optional[str], disc: int) - return mapping.get(disc) if program_id == RAYDIUM_CPMM_PROGRAM_ID: mapping = { + _d(64, 198, 205, 232, 38, 8, 113, 226): EventType.RAYDIUM_CPMM_SWAP, _d(143, 190, 90, 218, 196, 30, 51, 222): EventType.RAYDIUM_CPMM_SWAP, _d(55, 217, 98, 86, 163, 74, 180, 173): EventType.RAYDIUM_CPMM_SWAP, _d(233, 146, 209, 142, 207, 104, 64, 188): EventType.RAYDIUM_CPMM_INITIALIZE, @@ -3187,21 +3380,21 @@ def event_type_for_program_discriminator(program_id: Optional[str], disc: int) - return EventType.METEORA_DBC_CURVE_COMPLETE return None if program_id == METEORA_DLMM_PROGRAM_ID: - if disc == DLMM_SWAP: + if disc in (DLMM_SWAP, DLMM_SWAP2, DLMM_LEGACY_SWAP): return EventType.METEORA_DLMM_SWAP - if disc == DLMM_ADD_LIQ: + if disc in (DLMM_ADD_LIQ, DLMM_LEGACY_ADD_LIQ): return EventType.METEORA_DLMM_ADD_LIQUIDITY - if disc == DLMM_REMOVE_LIQ: + if disc in (DLMM_REMOVE_LIQ, DLMM_LEGACY_REMOVE_LIQ): return EventType.METEORA_DLMM_REMOVE_LIQUIDITY - if disc == DLMM_INIT_POOL: + if disc in (DLMM_INIT_POOL, DLMM_LEGACY_INIT_POOL): return EventType.METEORA_DLMM_INITIALIZE_POOL if disc == DLMM_INIT_BIN: return EventType.METEORA_DLMM_INITIALIZE_BIN_ARRAY - if disc == DLMM_CREATE_POS: + if disc in (DLMM_CREATE_POS, DLMM_LEGACY_CREATE_POS): return EventType.METEORA_DLMM_CREATE_POSITION - if disc == DLMM_CLOSE_POS: + if disc in (DLMM_CLOSE_POS, DLMM_LEGACY_CLOSE_POS): return EventType.METEORA_DLMM_CLOSE_POSITION - if disc == DLMM_CLAIM_FEE: + if disc in (DLMM_CLAIM_FEE, DLMM_CLAIM_FEE2, DLMM_LEGACY_CLAIM_FEE): return EventType.METEORA_DLMM_CLAIM_FEE return None return event_type_for_discriminator(disc) @@ -3232,7 +3425,7 @@ def filter_allows_unscoped_discriminator(event_type_filter: Any, disc: int) -> b return filter_wants_pumpfun_trade(event_type_filter) or filter_wants_raydium_launchlab_trade( event_type_filter ) - if disc == DLMM_SWAP: + if disc in (DLMM_SWAP, DLMM_SWAP2, DLMM_LEGACY_SWAP): return event_type_filter.should_include(EventType.RAYDIUM_CPMM_SWAP) or event_type_filter.should_include( EventType.METEORA_DLMM_SWAP ) @@ -3479,6 +3672,8 @@ def dispatch_program_data( return parse_clmm_collect_protocol_from_data(data, meta) return None if program_id == RAYDIUM_CPMM_PROGRAM_ID: + if disc == _d(64, 198, 205, 232, 38, 8, 113, 226): + return parse_cpmm_swap_event_from_data(data, meta) if disc == _d(143, 190, 90, 218, 196, 30, 51, 222): return parse_cpmm_swap_in_from_data(data, meta) if disc == _d(55, 217, 98, 86, 163, 74, 180, 173): diff --git a/sol_parser/grpc_instruction_parser.py b/sol_parser/grpc_instruction_parser.py index 470d5cb..c13d66a 100644 --- a/sol_parser/grpc_instruction_parser.py +++ b/sol_parser/grpc_instruction_parser.py @@ -11,13 +11,14 @@ from .event_types import DexEvent from .grpc_types import ( EventMetadata, + EventType, EventTypeFilter, SubscribeUpdateTransactionInfo, event_type_filter_allows_instruction_parsing, ) from .inner_instruction_parser import parse_inner_instruction from .instructions import parse_inner_compiled_instruction_if_supported, parse_instruction_unified -from .merger import merge_dex_events +from .merger import try_merge_dex_events from .pumpfun_fee_enrich import enrich_pumpfun_same_tx_post_merge @@ -124,36 +125,86 @@ def should_parse_instructions(filter: Optional[EventTypeFilter]) -> bool: return event_type_filter_allows_instruction_parsing(list(inc)) -def merge_instruction_events( - events: List[Tuple[int, Optional[int], DexEvent]], -) -> List[DexEvent]: +_DLMM_EVENT_TYPES = { + EventType.METEORA_DLMM_SWAP, + EventType.METEORA_DLMM_ADD_LIQUIDITY, + EventType.METEORA_DLMM_REMOVE_LIQUIDITY, + EventType.METEORA_DLMM_INITIALIZE_POOL, + EventType.METEORA_DLMM_INITIALIZE_BIN_ARRAY, + EventType.METEORA_DLMM_CREATE_POSITION, + EventType.METEORA_DLMM_CLOSE_POSITION, + EventType.METEORA_DLMM_CLAIM_FEE, +} + + +def _is_dlmm_event_cpi(data: bytes) -> bool: + return len(data) >= 16 and ( + data[:8] == bytes((228, 69, 165, 46, 81, 203, 154, 29)) + or data[8:16] == bytes((155, 167, 108, 32, 122, 76, 173, 64)) + ) + + +def merge_instruction_events(events: List[Tuple[Any, ...]]) -> List[DexEvent]: """对齐 Rust ``merge_instruction_events``。""" if not events: return [] - events = sorted(events, key=lambda x: (x[0], 0 if x[1] is None else 1 + x[1])) + normalized = events if all(len(item) == 5 for item in events) else [ + (item[0], item[1], None, False, item[2]) if len(item) == 3 else item + for item in events + ] + normalized.sort(key=lambda x: (x[0], 0 if x[1] is None else 1 + x[1])) result: List[DexEvent] = [] - pending_outer: Optional[Tuple[int, DexEvent]] = None + outer_target: Optional[Tuple[int, int]] = None + dlmm_targets: List[Tuple[int, Optional[int], int]] = [] - for outer_idx, inner_idx, event in events: + for outer_idx, inner_idx, stack_height, is_dlmm_event_cpi, event in normalized: if inner_idx is None: - if pending_outer is not None: - result.append(pending_outer[1]) - pending_outer = (outer_idx, event) - else: - if pending_outer is not None: - po_idx, mut_outer = pending_outer - pending_outer = None - if po_idx == outer_idx: - merge_dex_events(mut_outer, event) - pending_outer = (outer_idx, mut_outer) - else: - result.append(mut_outer) - result.append(event) + result.append(event) + target_idx = len(result) - 1 + outer_target = (outer_idx, target_idx) + dlmm_targets = ( + [(outer_idx, stack_height, target_idx)] if event.type in _DLMM_EVENT_TYPES else [] + ) + continue + + if is_dlmm_event_cpi: + for candidate_idx in range(len(dlmm_targets) - 1, -1, -1): + target_outer, target_height, target_idx = dlmm_targets[candidate_idx] + direct_child = ( + target_height is None + or stack_height is None + or stack_height == target_height + 1 + ) + if target_outer == outer_idx and direct_child: + del dlmm_targets[candidate_idx + 1:] + if try_merge_dex_events(result[target_idx], event): + break else: result.append(event) + continue + + target_idx: Optional[int] = None + if outer_target is not None and outer_target[0] == outer_idx: + if try_merge_dex_events(result[outer_target[1]], event): + target_idx = outer_target[1] + if target_idx is None: + result.append(event) + target_idx = len(result) - 1 + + if event.type in _DLMM_EVENT_TYPES: + if stack_height is None: + dlmm_targets.clear() + else: + while dlmm_targets and ( + dlmm_targets[-1][0] != outer_idx + or ( + dlmm_targets[-1][1] is not None + and dlmm_targets[-1][1] >= stack_height + ) + ): + dlmm_targets.pop() + dlmm_targets.append((outer_idx, stack_height, target_idx)) - if pending_outer is not None: - result.append(pending_outer[1]) return result @@ -260,7 +311,7 @@ def get_key_b58(i: int) -> str: is_created_buy = detect_pumpfun_create_from_logs(list(meta.log_messages)) - result: List[Tuple[int, Optional[int], DexEvent]] = [] + result: List[Tuple[Any, ...]] = [] for i, ix in enumerate(msg.instructions): pid_idx = ix.program_id_index @@ -272,7 +323,7 @@ def get_key_b58(i: int) -> str: data, accounts, signature, slot, tx_index, block_time_us, grpc_us, filter, pid ) if ev: - result.append((i, None, ev)) + result.append((i, None, 1, False, ev)) for inner in meta.inner_instructions: outer_idx = inner.index @@ -298,7 +349,13 @@ def get_key_b58(i: int) -> str: is_created_buy, ) if ev: - result.append((int(outer_idx), j, ev)) + result.append(( + int(outer_idx), + j, + int(inner_ix.stack_height) if inner_ix.HasField("stack_height") else None, + pid == METEORA_DLMM_PROGRAM_ID and _is_dlmm_event_cpi(data), + ev, + )) merged = merge_instruction_events(result) enrich_pumpfun_same_tx_post_merge(merged) diff --git a/sol_parser/inner_instruction_parser.py b/sol_parser/inner_instruction_parser.py index 34b5ac0..d0ac0bc 100644 --- a/sol_parser/inner_instruction_parser.py +++ b/sol_parser/inner_instruction_parser.py @@ -43,7 +43,7 @@ parse_cpmm_swap_in_from_data, parse_cpmm_swap_out_from_data, parse_cpmm_withdraw_from_data, - parse_dlmm_from_program_data, + parse_dlmm_event_from_data, parse_meteora_damm_from_buf, parse_meteora_add_from_data, parse_meteora_bootstrap_from_data, @@ -184,11 +184,6 @@ def _damm_buf_from_inner(disc16: bytes, inner: bytes) -> bytes: _RAYDIUM_LAUNCHLAB_TRADE = bytes([189, 219, 127, 211, 78, 230, 97, 238, 155, 167, 108, 32, 122, 76, 173, 64]) _RAYDIUM_LAUNCHLAB_POOL_CREATE = bytes([151, 215, 226, 9, 118, 161, 115, 174, 155, 167, 108, 32, 122, 76, 173, 64]) -# DLMM(8 字节 event disc + payload) -def _dlmm_buf_from_inner(disc16: bytes, inner: bytes) -> bytes: - return disc16[:8] + inner - - def _meteora_pools_swap_inner(data: bytes, meta_d: dict) -> Optional[DexEvent]: if len(data) < 16: return None @@ -440,6 +435,9 @@ def emit(ev: Optional[DexEvent]) -> Optional[DexEvent]: if program_id_b58 == METEORA_DLMM_PROGRAM_ID: if filter is not None and not event_type_filter_includes_meteora_dlmm(filter): return None - return emit(parse_dlmm_from_program_data(_dlmm_buf_from_inner(disc16, inner), meta_d)) + event_disc = _event_cpi_disc8(disc16) + if event_disc is None: + return None + return emit(parse_dlmm_event_from_data(event_disc, inner, meta_d)) return None diff --git a/sol_parser/instructions.py b/sol_parser/instructions.py index dd7422c..d4a3dd5 100644 --- a/sol_parser/instructions.py +++ b/sol_parser/instructions.py @@ -97,6 +97,50 @@ def _d(*xs: int) -> int: _DISC_METEORA_POOLS_REMOVE_LIQUIDITY = _d(80, 85, 209, 72, 24, 206, 177, 108) _DISC_METEORA_POOLS_CREATE_POOL = _d(95, 180, 10, 172, 84, 174, 232, 40) +_DISC_DLMM_ADD_LIQUIDITY = _d(181, 157, 89, 67, 143, 182, 52, 72) +_DISC_DLMM_ADD_LIQUIDITY2 = _d(228, 162, 78, 28, 70, 219, 116, 115) +_DISC_DLMM_CLAIM_FEE = _d(169, 32, 79, 137, 136, 232, 70, 137) +_DISC_DLMM_CLAIM_FEE2 = _d(112, 191, 101, 171, 28, 144, 127, 187) +_DISC_DLMM_CLOSE_POSITION = _d(123, 134, 81, 0, 49, 68, 98, 98) +_DISC_DLMM_CLOSE_POSITION2 = _d(174, 90, 35, 115, 186, 40, 147, 226) +_DISC_DLMM_INITIALIZE_BIN_ARRAY = _d(35, 86, 19, 185, 78, 212, 75, 211) +_DISC_DLMM_INITIALIZE_LB_PAIR = _d(45, 154, 237, 210, 221, 15, 166, 92) +_DISC_DLMM_INITIALIZE_LB_PAIR2 = _d(73, 59, 36, 120, 237, 83, 108, 198) +_DISC_DLMM_INITIALIZE_POSITION = _d(219, 192, 234, 71, 190, 191, 102, 80) +_DISC_DLMM_INITIALIZE_POSITION2 = _d(143, 19, 242, 145, 213, 15, 104, 115) +_DISC_DLMM_INITIALIZE_POSITION_PDA = _d(46, 82, 125, 146, 85, 141, 228, 153) +_DISC_DLMM_REMOVE_LIQUIDITY = _d(80, 85, 209, 72, 24, 206, 177, 108) +_DISC_DLMM_REMOVE_LIQUIDITY2 = _d(230, 215, 82, 127, 241, 101, 227, 146) +_DISC_DLMM_SWAP = _d(248, 198, 158, 145, 225, 117, 135, 200) +_DISC_DLMM_SWAP2 = _d(65, 75, 63, 76, 235, 91, 91, 136) +_DISC_DLMM_SWAP_EXACT_OUT = _d(250, 73, 101, 33, 38, 207, 75, 184) +_DISC_DLMM_SWAP_EXACT_OUT2 = _d(43, 215, 247, 132, 137, 60, 243, 81) +_DISC_DLMM_SWAP_WITH_PRICE_IMPACT = _d(56, 173, 230, 208, 173, 228, 156, 205) +_DISC_DLMM_SWAP_WITH_PRICE_IMPACT2 = _d(74, 98, 192, 214, 177, 51, 75, 51) + +_DLMM_INSTRUCTION_DISCS = ( + _DISC_DLMM_INITIALIZE_LB_PAIR, + _DISC_DLMM_INITIALIZE_LB_PAIR2, + _DISC_DLMM_INITIALIZE_BIN_ARRAY, + _DISC_DLMM_ADD_LIQUIDITY, + _DISC_DLMM_ADD_LIQUIDITY2, + _DISC_DLMM_REMOVE_LIQUIDITY, + _DISC_DLMM_REMOVE_LIQUIDITY2, + _DISC_DLMM_INITIALIZE_POSITION, + _DISC_DLMM_INITIALIZE_POSITION2, + _DISC_DLMM_INITIALIZE_POSITION_PDA, + _DISC_DLMM_SWAP, + _DISC_DLMM_SWAP2, + _DISC_DLMM_SWAP_EXACT_OUT, + _DISC_DLMM_SWAP_EXACT_OUT2, + _DISC_DLMM_SWAP_WITH_PRICE_IMPACT, + _DISC_DLMM_SWAP_WITH_PRICE_IMPACT2, + _DISC_DLMM_CLAIM_FEE, + _DISC_DLMM_CLAIM_FEE2, + _DISC_DLMM_CLOSE_POSITION, + _DISC_DLMM_CLOSE_POSITION2, +) + _DISC_CLMM_SWAP = _d(248, 198, 158, 145, 225, 117, 135, 200) _DISC_CLMM_SWAP_V2 = _d(43, 4, 237, 11, 26, 201, 30, 98) _DISC_CLMM_INC_LIQ = _d(133, 29, 89, 223, 69, 238, 176, 10) @@ -324,7 +368,8 @@ def normal_instruction_data_may_parse(program_id: str, instruction_data: bytes) if program_id == RAYDIUM_AMM_V4_PROGRAM_ID: return instruction_data[0] in (1, 3, 4, 7, 9, 11) if program_id == METEORA_DLMM_PROGRAM_ID: - return instruction_data[0] in (0, 1, 2, 7, 8, 11, 13, 14) + disc = _disc8(instruction_data) + return disc in _DLMM_INSTRUCTION_DISCS if disc is not None else False disc = _disc8(instruction_data) if disc is None: return False @@ -1024,94 +1069,120 @@ def parse_meteora_dlmm_instruction( block_time_us: Optional[int], grpc_recv_us: int, ) -> Optional[DexEvent]: - if not data or not accounts: + if len(data) < 8 or not accounts: return None - instruction_type = data[0] - payload = data[1:] + discriminator = struct.unpack_from(" str: + return _get_account_safe(accounts, index - shift if index >= 5 else index) return legacy_dict_to_dex_event({"RaydiumAmmV4Swap": { "metadata": meta, "amm": _get_account_safe(accounts, 1), - "user_source_owner": _get_account_safe(accounts, 17), - "amount_in": 0, "minimum_amount_out": 0, - "max_amount_in": 0, "amount_out": 0, - "token_program": Z, "amm_authority": Z, "amm_open_orders": Z, - "pool_coin_token_account": Z, "pool_pc_token_account": Z, - "serum_program": Z, "serum_market": Z, "serum_bids": Z, - "serum_asks": Z, "serum_event_queue": Z, - "serum_coin_vault_account": Z, "serum_pc_vault_account": Z, - "serum_vault_signer": Z, - "user_source_token_account": Z, - "user_destination_token_account": Z, + "user_source_owner": g(17), + "amount_in": first if instr_type == 9 else 0, + "minimum_amount_out": second if instr_type == 9 else 0, + "max_amount_in": first if instr_type == 11 else 0, + "amount_out": second if instr_type == 11 else 0, + "token_program": g(0), "amm_authority": g(2), "amm_open_orders": g(3), + "pool_coin_token_account": g(5), "pool_pc_token_account": g(6), + "serum_program": g(7), "serum_market": g(8), "serum_bids": g(9), + "serum_asks": g(10), "serum_event_queue": g(11), + "serum_coin_vault_account": g(12), "serum_pc_vault_account": g(13), + "serum_vault_signer": g(14), + "user_source_token_account": g(15), + "user_destination_token_account": g(16), }}) return None @@ -1599,9 +1713,10 @@ def parse_orca_whirlpool_instruction( sqrt_price_limit = int.from_bytes(data[24:40], "little") amount_specified_is_input = data[40] != 0 a_to_b = data[41] != 0 + whirlpool_index = 4 if discriminator == _DISC_ORCA_SWAP_V2 else 2 return legacy_dict_to_dex_event({"OrcaWhirlpoolSwap": { "metadata": meta, - "whirlpool": _get_account_safe(accounts, 1), + "whirlpool": _get_account_safe(accounts, whirlpool_index), "a_to_b": a_to_b, "pre_sqrt_price": str(sqrt_price_limit), "post_sqrt_price": "0", "input_amount": amount if amount_specified_is_input else 0, diff --git a/sol_parser/log_instr_dedup.py b/sol_parser/log_instr_dedup.py index cde047d..bd9d4bf 100644 --- a/sol_parser/log_instr_dedup.py +++ b/sol_parser/log_instr_dedup.py @@ -49,16 +49,19 @@ def _ix_lane(ix_name: Any) -> int: return 0 -PumpfunLaneBase = Tuple[str, str, bool, int] +OccurrenceBase = Tuple[Any, ...] -def _next_occurrence(base: PumpfunLaneBase, counts: Dict[PumpfunLaneBase, int]) -> int: +def _next_occurrence(base: OccurrenceBase, counts: Dict[OccurrenceBase, int]) -> int: current = counts.get(base, 0) counts[base] = current + 1 return current -def _dedupe_key(ev: DexEvent, pumpfun_lane_counts: Dict[PumpfunLaneBase, int]) -> Optional[str]: +def _dedupe_key( + ev: DexEvent, + occurrence_counts: Dict[OccurrenceBase, int], +) -> Optional[str]: data = ev.data if data is None: return None @@ -71,7 +74,7 @@ def _dedupe_key(ev: DexEvent, pumpfun_lane_counts: Dict[PumpfunLaneBase, int]) - bool(getattr(data, "is_buy", False)), lane, ) - occ = _next_occurrence(base, pumpfun_lane_counts) + occ = _next_occurrence(("PumpFun", *base), occurrence_counts) return f"PumpFunTrade|{base[0]}|{base[1]}|{base[2]}|{base[3]}|{occ}" t = ev.type @@ -110,13 +113,32 @@ def _dedupe_key(ev: DexEvent, pumpfun_lane_counts: Dict[PumpfunLaneBase, int]) - if t == EventType.PUMP_SWAP_LIQUIDITY_REMOVED: return f"PumpSwapLiquidityRemoved|{getattr(data, 'pool', '')}|{getattr(data, 'user', '')}" if t == EventType.RAYDIUM_CLMM_SWAP: - return f"RaydiumClmmSwap|{getattr(data, 'pool_state', '')}|{bool(getattr(data, 'zero_for_one', False))}" + pool = getattr(data, "pool_state", "") + occurrence = _next_occurrence(("RaydiumClmm", pool), occurrence_counts) + return f"RaydiumClmmSwap|{pool}|{occurrence}" + if t == EventType.RAYDIUM_CPMM_SWAP: + pool = getattr(data, "pool_id", "") + occurrence = _next_occurrence(("RaydiumCpmm", pool), occurrence_counts) + return f"RaydiumCpmmSwap|{pool}|{occurrence}" if t == EventType.RAYDIUM_AMM_V4_SWAP: - return f"RaydiumAmmV4Swap|{getattr(data, 'amm', '')}" + base_out = getattr(data, "max_amount_in", 0) != 0 + amount = getattr(data, "amount_out" if base_out else "amount_in", 0) + base = (base_out, amount) + occurrence = _next_occurrence(("RaydiumAmmV4", *base), occurrence_counts) + return f"RaydiumAmmV4Swap|{base_out}|{amount}|{occurrence}" + if t == EventType.ORCA_WHIRLPOOL_SWAP: + whirlpool = getattr(data, "whirlpool", "") + occurrence = _next_occurrence(("OrcaWhirlpool", whirlpool), occurrence_counts) + return f"OrcaWhirlpoolSwap|{whirlpool}|{occurrence}" if t == EventType.METEORA_DLMM_SWAP: + base = ( + getattr(data, "pool", ""), + getattr(data, "from_addr", ""), + bool(getattr(data, "swap_for_y", False)), + ) + occurrence = _next_occurrence(("MeteoraDlmm", *base), occurrence_counts) return ( - f"MeteoraDlmmSwap|{getattr(data, 'pool', '')}|" - f"{getattr(data, 'from_addr', '')}|{bool(getattr(data, 'swap_for_y', False))}" + f"MeteoraDlmmSwap|{base[0]}|{base[1]}|{base[2]}|{occurrence}" ) return None @@ -312,6 +334,8 @@ def _merge_grpc_instruction_into_log(log_ev: DexEvent, ix_ev: DexEvent) -> None: "serum_vault_signer", "user_source_token_account", "user_destination_token_account", + "user_source_owner", + "amm", ): _fill_attr(log, attr, ix) elif log_ev.type == EventType.RAYDIUM_LAUNCHLAB_POOL_CREATE and ix_ev.type == EventType.RAYDIUM_LAUNCHLAB_POOL_CREATE: @@ -329,17 +353,17 @@ def dedupe_log_instruction_events( ) -> List[DexEvent]: out: List[DexEvent] = [] index_by_key: Dict[str, int] = {} - log_pumpfun_counts: Dict[PumpfunLaneBase, int] = {} - ix_pumpfun_counts: Dict[PumpfunLaneBase, int] = {} + log_occurrences: Dict[OccurrenceBase, int] = {} + ix_occurrences: Dict[OccurrenceBase, int] = {} for ev in log_events: - key = _dedupe_key(ev, log_pumpfun_counts) + key = _dedupe_key(ev, log_occurrences) if key is not None: index_by_key[key] = len(out) out.append(ev) for ev in instruction_events: - key = _dedupe_key(ev, ix_pumpfun_counts) + key = _dedupe_key(ev, ix_occurrences) if key is None: out.append(ev) continue diff --git a/sol_parser/merger.py b/sol_parser/merger.py index a51721c..8d56e2f 100644 --- a/sol_parser/merger.py +++ b/sol_parser/merger.py @@ -32,6 +32,14 @@ MeteoraDammV2CreatePositionEvent, MeteoraDammV2RemoveLiquidityEvent, MeteoraDammV2SwapEvent, + MeteoraDlmmAddLiquidityEvent, + MeteoraDlmmClaimFeeEvent, + MeteoraDlmmClosePositionEvent, + MeteoraDlmmCreatePositionEvent, + MeteoraDlmmInitializeBinArrayEvent, + MeteoraDlmmInitializePoolEvent, + MeteoraDlmmRemoveLiquidityEvent, + MeteoraDlmmSwapEvent, MeteoraPoolsAddLiquidityEvent, MeteoraPoolsRemoveLiquidityEvent, MeteoraPoolsSwapEvent, @@ -44,6 +52,40 @@ ZERO = "11111111111111111111111111111111" +GENERIC_MERGE_TYPES = ( + PumpSwapCreatePoolEvent, + PumpSwapLiquidityAddedEvent, + PumpSwapLiquidityRemovedEvent, + RaydiumClmmSwapEvent, + RaydiumClmmIncreaseLiquidityEvent, + RaydiumClmmDecreaseLiquidityEvent, + RaydiumClmmCreatePoolEvent, + RaydiumClmmCollectFeeEvent, + RaydiumCpmmSwapEvent, + RaydiumCpmmDepositEvent, + RaydiumCpmmWithdrawEvent, + RaydiumAmmV4SwapEvent, + RaydiumAmmV4DepositEvent, + RaydiumAmmV4WithdrawEvent, + OrcaWhirlpoolSwapEvent, + OrcaWhirlpoolLiquidityIncreasedEvent, + OrcaWhirlpoolLiquidityDecreasedEvent, + MeteoraPoolsSwapEvent, + MeteoraPoolsAddLiquidityEvent, + MeteoraPoolsRemoveLiquidityEvent, + MeteoraDammV2SwapEvent, + MeteoraDammV2AddLiquidityEvent, + MeteoraDammV2RemoveLiquidityEvent, + MeteoraDammV2CreatePositionEvent, + MeteoraDammV2ClosePositionEvent, + MeteoraDlmmSwapEvent, + MeteoraDlmmAddLiquidityEvent, + MeteoraDlmmRemoveLiquidityEvent, + MeteoraDlmmInitializeBinArrayEvent, + MeteoraDlmmClaimFeeEvent, + RaydiumLaunchlabTradeEvent, +) + def _merge_generic(base: Any, inner: Any) -> None: for f in dataclasses.fields(type(base)): @@ -258,6 +300,11 @@ def merge_pumpfun_migrate(base: PumpFunMigrateEvent, inner: PumpFunMigrateEvent) def merge_dex_events(base: DexEvent, inner: DexEvent) -> None: """将 ``inner`` 合并进 ``base``(就地修改 ``base.data``)。""" + try_merge_dex_events(base, inner) + + +def try_merge_dex_events(base: DexEvent, inner: DexEvent) -> bool: + """Merge compatible events and report success so unmatched events are retained.""" bd = base.data ind = inner.data @@ -274,108 +321,45 @@ def merge_dex_events(base: DexEvent, inner: DexEvent) -> None: EventType.PUMP_FUN_BUY_EXACT_SOL_IN, ): merge_pumpfun_trade(bd, ind) - return + return True + return False if isinstance(bd, PumpFunCreateEvent) and isinstance(ind, PumpFunCreateEvent): merge_pumpfun_create(bd, ind) - return + return True if isinstance(bd, PumpFunCreateV2TokenEvent) and isinstance(ind, PumpFunCreateV2TokenEvent): _merge_generic(bd, ind) - return + return True if isinstance(bd, PumpFunMigrateEvent) and isinstance(ind, PumpFunMigrateEvent): merge_pumpfun_migrate(bd, ind) - return + return True if isinstance(bd, PumpSwapBuyEvent) and isinstance(ind, PumpSwapBuyEvent): merge_pumpswap_buy(bd, ind) - return + return True if isinstance(bd, PumpSwapSellEvent) and isinstance(ind, PumpSwapSellEvent): merge_pumpswap_sell(bd, ind) - return - if isinstance(bd, PumpSwapCreatePoolEvent) and isinstance(ind, PumpSwapCreatePoolEvent): - _merge_generic(bd, ind) - return - if isinstance(bd, PumpSwapLiquidityAddedEvent) and isinstance(ind, PumpSwapLiquidityAddedEvent): - _merge_generic(bd, ind) - return - if isinstance(bd, PumpSwapLiquidityRemovedEvent) and isinstance(ind, PumpSwapLiquidityRemovedEvent): - _merge_generic(bd, ind) - return - - if isinstance(bd, RaydiumClmmSwapEvent) and isinstance(ind, RaydiumClmmSwapEvent): - _merge_generic(bd, ind) - return - if isinstance(bd, RaydiumClmmIncreaseLiquidityEvent) and isinstance(ind, RaydiumClmmIncreaseLiquidityEvent): - _merge_generic(bd, ind) - return - if isinstance(bd, RaydiumClmmDecreaseLiquidityEvent) and isinstance(ind, RaydiumClmmDecreaseLiquidityEvent): - _merge_generic(bd, ind) - return - if isinstance(bd, RaydiumClmmCreatePoolEvent) and isinstance(ind, RaydiumClmmCreatePoolEvent): - _merge_generic(bd, ind) - return - if isinstance(bd, RaydiumClmmCollectFeeEvent) and isinstance(ind, RaydiumClmmCollectFeeEvent): - _merge_generic(bd, ind) - return - - if isinstance(bd, RaydiumCpmmSwapEvent) and isinstance(ind, RaydiumCpmmSwapEvent): - _merge_generic(bd, ind) - return - if isinstance(bd, RaydiumCpmmDepositEvent) and isinstance(ind, RaydiumCpmmDepositEvent): - _merge_generic(bd, ind) - return - if isinstance(bd, RaydiumCpmmWithdrawEvent) and isinstance(ind, RaydiumCpmmWithdrawEvent): - _merge_generic(bd, ind) - return - - if isinstance(bd, RaydiumAmmV4SwapEvent) and isinstance(ind, RaydiumAmmV4SwapEvent): - _merge_generic(bd, ind) - return - if isinstance(bd, RaydiumAmmV4DepositEvent) and isinstance(ind, RaydiumAmmV4DepositEvent): - _merge_generic(bd, ind) - return - if isinstance(bd, RaydiumAmmV4WithdrawEvent) and isinstance(ind, RaydiumAmmV4WithdrawEvent): - _merge_generic(bd, ind) - return - - if isinstance(bd, OrcaWhirlpoolSwapEvent) and isinstance(ind, OrcaWhirlpoolSwapEvent): - _merge_generic(bd, ind) - return - if isinstance(bd, OrcaWhirlpoolLiquidityIncreasedEvent) and isinstance(ind, OrcaWhirlpoolLiquidityIncreasedEvent): - _merge_generic(bd, ind) - return - if isinstance(bd, OrcaWhirlpoolLiquidityDecreasedEvent) and isinstance(ind, OrcaWhirlpoolLiquidityDecreasedEvent): + return True + if isinstance(bd, MeteoraDlmmInitializePoolEvent) and isinstance(ind, MeteoraDlmmInitializePoolEvent): + creator, active_bin_id = bd.creator, bd.active_bin_id _merge_generic(bd, ind) - return - - if isinstance(bd, MeteoraPoolsSwapEvent) and isinstance(ind, MeteoraPoolsSwapEvent): - _merge_generic(bd, ind) - return - if isinstance(bd, MeteoraPoolsAddLiquidityEvent) and isinstance(ind, MeteoraPoolsAddLiquidityEvent): + bd.creator, bd.active_bin_id = creator, active_bin_id + return True + if isinstance(bd, MeteoraDlmmCreatePositionEvent) and isinstance(ind, MeteoraDlmmCreatePositionEvent): + lower_bin_id, width = bd.lower_bin_id, bd.width _merge_generic(bd, ind) - return - if isinstance(bd, MeteoraPoolsRemoveLiquidityEvent) and isinstance(ind, MeteoraPoolsRemoveLiquidityEvent): + bd.lower_bin_id, bd.width = lower_bin_id, width + return True + if isinstance(bd, MeteoraDlmmClosePositionEvent) and isinstance(ind, MeteoraDlmmClosePositionEvent): + pool = bd.pool _merge_generic(bd, ind) - return + bd.pool = pool + return True - if isinstance(bd, MeteoraDammV2SwapEvent) and isinstance(ind, MeteoraDammV2SwapEvent): - _merge_generic(bd, ind) - return - if isinstance(bd, MeteoraDammV2AddLiquidityEvent) and isinstance(ind, MeteoraDammV2AddLiquidityEvent): - _merge_generic(bd, ind) - return - if isinstance(bd, MeteoraDammV2RemoveLiquidityEvent) and isinstance(ind, MeteoraDammV2RemoveLiquidityEvent): + if base.type == inner.type and type(bd) is type(ind) and isinstance(bd, GENERIC_MERGE_TYPES): _merge_generic(bd, ind) - return - if isinstance(bd, MeteoraDammV2CreatePositionEvent) and isinstance(ind, MeteoraDammV2CreatePositionEvent): - _merge_generic(bd, ind) - return - if isinstance(bd, MeteoraDammV2ClosePositionEvent) and isinstance(ind, MeteoraDammV2ClosePositionEvent): - _merge_generic(bd, ind) - return + return True - if isinstance(bd, RaydiumLaunchlabTradeEvent) and isinstance(ind, RaydiumLaunchlabTradeEvent): - _merge_generic(bd, ind) - return + return False diff --git a/sol_parser/parser.py b/sol_parser/parser.py index 22cd069..79179e0 100644 --- a/sol_parser/parser.py +++ b/sol_parser/parser.py @@ -79,6 +79,17 @@ def parse_log_optimized( ) -> Optional[DexEvent]: """单次 base64 decode 后按 discriminator 做 early filter,再按实际事件类型二次过滤。""" grpc = int(time.time() * 1_000_000) if grpc_recv_us is None else grpc_recv_us + if program_id == "675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8" and "ray_log: " in log: + from .dex_parsers import parse_amm_ray_log_swap + from .grpc_types import EventType + + if event_type_filter is not None and not event_type_filter.should_include( + EventType.RAYDIUM_AMM_V4_SWAP + ): + return None + return parse_amm_ray_log_swap( + log, _meta(signature, slot, tx_index, block_time_us, grpc, recent_blockhash) + ) buf = decode_program_data_line(log) if not buf: return None diff --git a/sol_parser/rpc_parser.py b/sol_parser/rpc_parser.py index 3b4ab4e..462f71f 100644 --- a/sol_parser/rpc_parser.py +++ b/sol_parser/rpc_parser.py @@ -11,7 +11,11 @@ from .dex_parsers import DexEvent from .grpc_types import EventTypeFilter, EventType, event_type_filter_allows_instruction_parsing from .inner_instruction_parser import parse_inner_instruction -from .instructions import parse_inner_compiled_instruction_if_supported, parse_instruction_unified +from .instructions import ( + METEORA_DLMM_PROGRAM_ID, + parse_inner_compiled_instruction_if_supported, + parse_instruction_unified, +) from .log_instr_dedup import dedupe_log_instruction_events from .pumpfun_fee_enrich import enrich_pumpfun_same_tx_post_merge @@ -30,6 +34,7 @@ class RpcCompiledInstruction: program_id_index: int accounts: Union[List[int], bytes] data: bytes + stack_height: Optional[int] = None @dataclass @@ -215,8 +220,9 @@ def parse_rpc_transaction( block_tx_index = int(getattr(tx, "transaction_index", 0) or 0) full_account_keys = _merge_rpc_full_account_keys(msg.account_keys, meta) - indexed_instruction_events: List[Tuple[int, Optional[int], DexEvent]] = [] + indexed_instruction_events: List[Tuple[Any, ...]] = [] from .grpc_instruction_parser import ( + _is_dlmm_event_cpi, detect_pumpfun_create_from_logs, merge_instruction_events, ) @@ -238,7 +244,7 @@ def parse_rpc_transaction( is_created_buy=has_pumpfun_create_log, ) if ev: - indexed_instruction_events.append((outer_idx, None, ev)) + indexed_instruction_events.append((outer_idx, None, 1, False, ev)) # 解析内层指令 for group in meta.inner_instructions: @@ -256,7 +262,18 @@ def parse_rpc_transaction( is_created_buy=has_pumpfun_create_log, ) if ev: - indexed_instruction_events.append((int(group.index), inner_idx, ev)) + pid = ( + full_account_keys[ix.program_id_index] + if ix.program_id_index < len(full_account_keys) + else "" + ) + indexed_instruction_events.append(( + int(group.index), + inner_idx, + ix.stack_height, + pid == METEORA_DLMM_PROGRAM_ID and _is_dlmm_event_cpi(ix.data), + ev, + )) instruction_events = merge_instruction_events(indexed_instruction_events) enrich_pumpfun_same_tx_post_merge(instruction_events) @@ -525,6 +542,13 @@ def _parse_rpc_compiled_ix(ix: dict, account_keys: List[str]) -> RpcCompiledInst program_id_index=pidx, accounts=acc_list, data=_ix_data_from_rpc(ix), + stack_height=( + int(ix["stackHeight"]) + if ix.get("stackHeight") is not None + else int(ix["stack_height"]) + if ix.get("stack_height") is not None + else None + ), ) @@ -717,6 +741,8 @@ def rpc_response_to_solana_storage( acc = ix.accounts ii.accounts = bytes(acc) if not isinstance(acc, bytes) else acc ii.data = ix.data + if ix.stack_height is not None: + ii.stack_height = ix.stack_height if m.loaded_addresses: for w in m.loaded_addresses.writable: meta.loaded_writable_addresses.append(base58.b58decode(w)) diff --git a/tests/test_imports_and_inner.py b/tests/test_imports_and_inner.py index fee5547..edd9202 100644 --- a/tests/test_imports_and_inner.py +++ b/tests/test_imports_and_inner.py @@ -6,8 +6,12 @@ from sol_parser.dex_parsers import parse_ps_buy_from_data from sol_parser.inner_instruction_parser import parse_inner_instruction +from sol_parser.event_types import DexEvent, MeteoraDlmmCreatePositionEvent, MeteoraDlmmSwapEvent +from sol_parser.grpc_instruction_parser import merge_instruction_events +from sol_parser.merger import try_merge_dex_events from sol_parser.grpc_types import EventType, IncludeOnlyFilter from sol_parser.instructions import ( + METEORA_DLMM_PROGRAM_ID, METEORA_POOLS_PROGRAM_ID, PUMPFUN_PROGRAM_ID, PUMPSWAP_PROGRAM_ID, @@ -30,6 +34,8 @@ def test_parse_inner_pumpfun_short_data() -> None: METEORA_POOLS_INNER_SWAP = bytes( [81, 108, 227, 190, 205, 208, 10, 196, 155, 167, 108, 32, 122, 76, 173, 64] ) +EVENT_CPI_PREFIX = bytes([228, 69, 165, 46, 81, 203, 154, 29]) +METEORA_DLMM_SWAP2_EVENT = bytes([46, 116, 82, 215, 148, 27, 84, 77]) def test_parse_inner_none_filter_allows_pumpswap_buy() -> None: @@ -100,6 +106,64 @@ def test_parse_inner_meteora_pools_uses_protocol_prefilter() -> None: assert ev.type == EventType.METEORA_POOLS_SWAP +def test_parse_inner_meteora_dlmm_uses_current_anchor_event_cpi_layout() -> None: + payload = bytearray(147) + payload[72] = 1 + struct.pack_into(" None: + def swap(pool: str, amount_in: int, amount_out: int) -> DexEvent: + return DexEvent( + type=EventType.METEORA_DLMM_SWAP, + data=MeteoraDlmmSwapEvent(pool=pool, amount_in=amount_in, amount_out=amount_out), + ) + + events = merge_instruction_events([ + (0, 0, 2, False, swap("pool-1", 1, 0)), + (0, 1, 3, True, swap("pool-1", 10, 9)), + (0, 2, 2, False, swap("pool-2", 2, 0)), + (0, 3, 3, True, swap("pool-2", 20, 18)), + ]) + + assert [(event.data.amount_in, event.data.amount_out) for event in events] == [ + (10, 9), + (20, 18), + ] + + +def test_merge_preserves_unrelated_inner_event() -> None: + swap = DexEvent(type=EventType.METEORA_DLMM_SWAP, data=MeteoraDlmmSwapEvent()) + other = DexEvent(type=EventType.PUMP_FUN_TRADE, data=object()) + assert merge_instruction_events([(0, None, swap), (0, 0, other)]) == [swap, other] + + +def test_dlmm_position_merge_keeps_instruction_only_fields() -> None: + base = DexEvent( + type=EventType.METEORA_DLMM_CREATE_POSITION, + data=MeteoraDlmmCreatePositionEvent(lower_bin_id=-42, width=70), + ) + inner = DexEvent( + type=EventType.METEORA_DLMM_CREATE_POSITION, + data=MeteoraDlmmCreatePositionEvent(), + ) + assert try_merge_dex_events(base, inner) + assert (base.data.lower_bin_id, base.data.width) == (-42, 70) + + def test_parse_inner_raydium_launchlab_uses_real_cpi_discriminators() -> None: pool_create_payload = bytes(96) + bytes([6]) + struct.pack(" DexEvent: + return DexEvent( + type=EventType.RAYDIUM_CLMM_SWAP, + data=RaydiumClmmSwapEvent( + pool_state="clmm-pool", + zero_for_one=zero_for_one, + amount_0=amount_0, + ), + ) + + +def test_clmm_dedup_ignores_instruction_placeholder_direction() -> None: + out = dedupe_log_instruction_events( + [_clmm_swap(False, 123)], + [_clmm_swap(True, 0)], + ) + + assert len(out) == 1 + assert out[0].data.amount_0 == 123 + assert out[0].data.zero_for_one is False + + +def test_clmm_same_pool_occurrences_are_retained() -> None: + out = dedupe_log_instruction_events( + [_clmm_swap(False, 1), _clmm_swap(True, 2)], + [_clmm_swap(True, 0), _clmm_swap(False, 0)], + ) + + assert len(out) == 2 diff --git a/tests/test_orca_whirlpool_instruction_parity.py b/tests/test_orca_whirlpool_instruction_parity.py index 4085bc8..307ab5a 100644 --- a/tests/test_orca_whirlpool_instruction_parity.py +++ b/tests/test_orca_whirlpool_instruction_parity.py @@ -66,7 +66,7 @@ def test_parse_orca_swap_and_swap_v2_instruction_fields() -> None: assert ev is not None assert ev.type == EventType.ORCA_WHIRLPOOL_SWAP assert isinstance(ev.data, OrcaWhirlpoolSwapEvent) - assert ev.data.whirlpool == "account_1" + assert ev.data.whirlpool == "account_2" assert ev.data.a_to_b is False assert ev.data.pre_sqrt_price == str(sqrt_price_limit) assert ev.data.post_sqrt_price == "0" @@ -75,7 +75,7 @@ def test_parse_orca_swap_and_swap_v2_instruction_fields() -> None: swap_v2 = parse_instruction_unified( _swap_instruction(SWAP_V2_DISC, 333, 444, sqrt_price_limit + 1, False, True), - _accounts(4), + _accounts(5), "sig", 1, 0, @@ -87,7 +87,7 @@ def test_parse_orca_swap_and_swap_v2_instruction_fields() -> None: assert swap_v2 is not None assert swap_v2.type == EventType.ORCA_WHIRLPOOL_SWAP assert isinstance(swap_v2.data, OrcaWhirlpoolSwapEvent) - assert swap_v2.data.whirlpool == "account_1" + assert swap_v2.data.whirlpool == "account_4" assert swap_v2.data.a_to_b is True assert swap_v2.data.pre_sqrt_price == str(sqrt_price_limit + 1) assert swap_v2.data.input_amount == 0 diff --git a/tests/test_raydium_clmm_instruction_parity.py b/tests/test_raydium_clmm_instruction_parity.py index 5203b13..f4a9e9c 100644 --- a/tests/test_raydium_clmm_instruction_parity.py +++ b/tests/test_raydium_clmm_instruction_parity.py @@ -49,6 +49,7 @@ DLMM_SWAP_DISC = bytes([143, 190, 90, 218, 196, 30, 51, 222]) RAYDIUM_LAUNCHLAB_BUY_EXACT_IN_DISC = bytes([250, 234, 13, 123, 213, 156, 19, 236]) METEORA_POOLS_SWAP_DISC = bytes([248, 198, 158, 145, 225, 117, 135, 200]) +METEORA_DLMM_SWAP_DISC = bytes([248, 198, 158, 145, 225, 117, 135, 200]) def _accounts(n: int) -> list[str]: @@ -256,7 +257,7 @@ def test_parse_raydium_cpmm_normal_instruction_uses_rust_accounts_and_defaults() ) assert swap is not None assert swap.type == EventType.RAYDIUM_CPMM_SWAP - assert swap.data.pool_id == "11111111111111111111111111111111" + assert swap.data.pool_id == "account_3" assert swap.data.input_amount == 0 assert swap.data.output_amount == 0 assert swap.data.base_input is True @@ -339,8 +340,8 @@ def test_parse_meteora_pools_and_dlmm_outer_instructions_are_routed() -> None: assert event_type_filter_allows_instruction_parsing([EventType.METEORA_DLMM_SWAP]) dlmm = parse_instruction_unified( - bytes([11]) + struct.pack(" None: assert dlmm.type == EventType.METEORA_DLMM_SWAP assert isinstance(dlmm.data, MeteoraDlmmSwapEvent) assert dlmm.data.pool == "account_0" - assert dlmm.data.from_addr == "account_1" + assert dlmm.data.from_addr == "account_10" assert dlmm.data.amount_in == 333 diff --git a/tests/test_raydium_current_logs.py b/tests/test_raydium_current_logs.py new file mode 100644 index 0000000..cae49a5 --- /dev/null +++ b/tests/test_raydium_current_logs.py @@ -0,0 +1,41 @@ +import base64 +import struct + +from sol_parser.dex_parsers import RAYDIUM_AMM_V4_PROGRAM_ID, RAYDIUM_CPMM_PROGRAM_ID +from sol_parser.grpc_types import EventType +from sol_parser.parser import parse_log_optimized_with_program_id + + +def _program_data(discriminator: bytes, payload: bytes) -> str: + return "Program data: " + base64.b64encode(discriminator + payload).decode() + + +def test_current_cpmm_swap_event_layout() -> None: + pool = bytes(range(32)) + payload = pool + struct.pack(" None: + ray_log = bytes([3]) + struct.pack("