Skip to main content

sol_parser_sdk/core/
merger.rs

1//! 轻量级事件合并机制 - 零拷贝高性能实现
2//!
3//! 将 inner instruction 事件数据合并到主 instruction 事件中
4//! 设计原则:
5//! - 只合并必要的字段
6//! - 保持零拷贝特性
7//! - 内联优化,最小化开销
8//!
9//! **gRPC log + instruction 双路径**:见 [`merge_grpc_instruction_into_log`] —— **以程序日志为准**,
10//! 指令解析仅补充账户等日志侧缺失字段。
11
12use solana_sdk::pubkey::Pubkey;
13
14use crate::core::events::*;
15
16/// 合并 instruction 事件和 inner instruction 事件
17///
18/// # 设计
19/// - Inner instruction 包含完整的交易数据(来自程序日志)
20/// - Instruction 包含账户上下文(来自指令本身)
21/// - 合并后的事件包含两者的完整信息
22///
23/// # 性能
24/// - 内联优化,编译器会将其优化为直接赋值
25/// - 零堆分配
26/// - 预期开销 < 10ns
27#[inline(always)]
28pub fn merge_events(base: &mut DexEvent, inner: DexEvent) {
29    let mut unmerged = None;
30    let _ = try_merge_events(base, inner, &mut unmerged);
31}
32
33/// Try to merge `inner` into `base` without allocating or returning the large
34/// event enum by value. On mismatch, the untouched event is written to
35/// `unmerged` for the caller to recover.
36#[inline(always)]
37pub fn try_merge_events(
38    base: &mut DexEvent,
39    inner: DexEvent,
40    unmerged: &mut Option<DexEvent>,
41) -> bool {
42    if pump_trade_identity_conflicts(base, &inner) {
43        *unmerged = Some(inner);
44        return false;
45    }
46    use DexEvent::*;
47
48    match (base, inner) {
49        // ========== PumpFun 系列 ==========
50        (PumpFunTrade(b), PumpFunTrade(i))
51        | (PumpFunTrade(b), PumpFunBuy(i))
52        | (PumpFunTrade(b), PumpFunSell(i))
53        | (PumpFunTrade(b), PumpFunBuyExactSolIn(i))
54        | (PumpFunBuy(b), PumpFunTrade(i))
55        | (PumpFunBuy(b), PumpFunBuy(i))
56        | (PumpFunSell(b), PumpFunTrade(i))
57        | (PumpFunSell(b), PumpFunSell(i))
58        | (PumpFunBuyExactSolIn(b), PumpFunTrade(i))
59        | (PumpFunBuyExactSolIn(b), PumpFunBuyExactSolIn(i)) => merge_pumpfun_trade(b, i),
60
61        (PumpFunCreate(b), PumpFunCreate(i)) => merge_pumpfun_create(b, i),
62        (PumpFunCreateV2(b), PumpFunCreateV2(i)) => merge_pumpfun_create_v2(b, i),
63        (PumpFunMigrate(b), PumpFunMigrate(i)) => merge_pumpfun_migrate(b, i),
64        (PumpFunMigrateBondingCurveCreator(b), PumpFunMigrateBondingCurveCreator(i)) => {
65            merge_generic(b, i)
66        }
67
68        // ========== PumpFees 系列 ==========
69        (PumpFeesCreateFeeSharingConfig(b), PumpFeesCreateFeeSharingConfig(i)) => {
70            merge_generic(b, i)
71        }
72        (PumpFeesInitializeFeeConfig(b), PumpFeesInitializeFeeConfig(i)) => merge_generic(b, i),
73        (PumpFeesResetFeeSharingConfig(b), PumpFeesResetFeeSharingConfig(i)) => merge_generic(b, i),
74        (PumpFeesRevokeFeeSharingAuthority(b), PumpFeesRevokeFeeSharingAuthority(i)) => {
75            merge_generic(b, i)
76        }
77        (PumpFeesTransferFeeSharingAuthority(b), PumpFeesTransferFeeSharingAuthority(i)) => {
78            merge_generic(b, i)
79        }
80        (PumpFeesUpdateAdmin(b), PumpFeesUpdateAdmin(i)) => merge_generic(b, i),
81        (PumpFeesUpdateFeeConfig(b), PumpFeesUpdateFeeConfig(i)) => merge_generic(b, i),
82        (PumpFeesUpdateFeeShares(b), PumpFeesUpdateFeeShares(i)) => merge_generic(b, i),
83        (PumpFeesUpsertFeeTiers(b), PumpFeesUpsertFeeTiers(i)) => merge_generic(b, i),
84
85        // ========== PumpSwap 系列 ==========
86        (PumpSwapTrade(b), PumpSwapTrade(i)) => merge_generic(b, i),
87        (PumpSwapBuy(b), PumpSwapBuy(i)) => merge_pumpswap_buy(b, i),
88        (PumpSwapSell(b), PumpSwapSell(i)) => merge_pumpswap_sell(b, i),
89        (PumpSwapCreatePool(b), PumpSwapCreatePool(i)) => merge_generic(b, i),
90        (PumpSwapLiquidityAdded(b), PumpSwapLiquidityAdded(i)) => merge_generic(b, i),
91        (PumpSwapLiquidityRemoved(b), PumpSwapLiquidityRemoved(i)) => merge_generic(b, i),
92
93        // ========== Raydium CLMM 系列 ==========
94        (RaydiumClmmSwap(b), RaydiumClmmSwap(i)) => merge_clmm_swap(b, i),
95        (RaydiumClmmIncreaseLiquidity(b), RaydiumClmmIncreaseLiquidity(i)) => merge_generic(b, i),
96        (RaydiumClmmDecreaseLiquidity(b), RaydiumClmmDecreaseLiquidity(i)) => merge_generic(b, i),
97        (RaydiumClmmLiquidityChange(b), RaydiumClmmLiquidityChange(i)) => merge_generic(b, i),
98        (RaydiumClmmConfigChange(b), RaydiumClmmConfigChange(i)) => merge_generic(b, i),
99        (RaydiumClmmCreatePersonalPosition(b), RaydiumClmmCreatePersonalPosition(i)) => {
100            merge_generic(b, i)
101        }
102        (RaydiumClmmLiquidityCalculate(b), RaydiumClmmLiquidityCalculate(i)) => merge_generic(b, i),
103        (RaydiumClmmOpenLimitOrder(b), RaydiumClmmOpenLimitOrder(i)) => merge_generic(b, i),
104        (RaydiumClmmIncreaseLimitOrder(b), RaydiumClmmIncreaseLimitOrder(i)) => merge_generic(b, i),
105        (RaydiumClmmDecreaseLimitOrder(b), RaydiumClmmDecreaseLimitOrder(i)) => merge_generic(b, i),
106        (RaydiumClmmSettleLimitOrder(b), RaydiumClmmSettleLimitOrder(i)) => merge_generic(b, i),
107        (RaydiumClmmUpdateRewardInfos(b), RaydiumClmmUpdateRewardInfos(i)) => merge_generic(b, i),
108        (RaydiumClmmCreatePool(b), RaydiumClmmCreatePool(i)) => merge_generic(b, i),
109        (RaydiumClmmOpenPosition(b), RaydiumClmmOpenPosition(i)) => merge_generic(b, i),
110        (RaydiumClmmClosePosition(b), RaydiumClmmClosePosition(i)) => merge_generic(b, i),
111        (RaydiumClmmOpenPositionWithTokenExtNft(b), RaydiumClmmOpenPositionWithTokenExtNft(i)) => {
112            merge_generic(b, i)
113        }
114        (RaydiumClmmCollectFee(b), RaydiumClmmCollectFee(i)) => merge_generic(b, i),
115
116        // ========== Raydium CPMM 系列 ==========
117        (RaydiumCpmmSwap(b), RaydiumCpmmSwap(i)) => merge_cpmm_swap(b, i),
118        (RaydiumCpmmDeposit(b), RaydiumCpmmDeposit(i)) => merge_generic(b, i),
119        (RaydiumCpmmWithdraw(b), RaydiumCpmmWithdraw(i)) => merge_generic(b, i),
120        (RaydiumCpmmInitialize(b), RaydiumCpmmInitialize(i)) => merge_generic(b, i),
121
122        // ========== Raydium AMM V4 系列 ==========
123        (RaydiumAmmV4Swap(b), RaydiumAmmV4Swap(i)) => {
124            let instruction = std::mem::replace(b, i);
125            merge_raydium_amm_v4_swap_log_preferred(b, instruction);
126        }
127        (RaydiumAmmV4Deposit(b), RaydiumAmmV4Deposit(i)) => merge_generic(b, i),
128        (RaydiumAmmV4Withdraw(b), RaydiumAmmV4Withdraw(i)) => merge_generic(b, i),
129        (RaydiumAmmV4Initialize2(b), RaydiumAmmV4Initialize2(i)) => merge_generic(b, i),
130        (RaydiumAmmV4WithdrawPnl(b), RaydiumAmmV4WithdrawPnl(i)) => merge_generic(b, i),
131
132        // ========== Orca Whirlpool 系列 ==========
133        (OrcaWhirlpoolSwap(b), OrcaWhirlpoolSwap(i)) => {
134            let instruction = std::mem::replace(b, i);
135            merge_orca_swap_context(b, instruction);
136        }
137        (OrcaWhirlpoolLiquidityIncreased(b), OrcaWhirlpoolLiquidityIncreased(i)) => {
138            merge_generic(b, i)
139        }
140        (OrcaWhirlpoolLiquidityDecreased(b), OrcaWhirlpoolLiquidityDecreased(i)) => {
141            merge_generic(b, i)
142        }
143        (OrcaWhirlpoolPoolInitialized(b), OrcaWhirlpoolPoolInitialized(i)) => merge_generic(b, i),
144
145        // ========== Meteora Pools (AMM) 系列 ==========
146        (MeteoraPoolsSwap(b), MeteoraPoolsSwap(i)) => {
147            let instruction = std::mem::replace(b, i);
148            merge_meteora_pools_swap_context(b, instruction);
149        }
150        (MeteoraPoolsAddLiquidity(b), MeteoraPoolsAddLiquidity(i)) => {
151            let instruction = std::mem::replace(b, i);
152            merge_meteora_pools_add_context(b, instruction);
153        }
154        (MeteoraPoolsRemoveLiquidity(b), MeteoraPoolsRemoveLiquidity(i)) => {
155            let instruction = std::mem::replace(b, i);
156            merge_meteora_pools_remove_context(b, instruction);
157        }
158        (MeteoraPoolsBootstrapLiquidity(b), MeteoraPoolsBootstrapLiquidity(i)) => {
159            let instruction = std::mem::replace(b, i);
160            merge_meteora_pools_bootstrap_context(b, instruction);
161        }
162        (MeteoraPoolsPoolCreated(b), MeteoraPoolsPoolCreated(i)) => {
163            let instruction = std::mem::replace(b, i);
164            merge_meteora_pools_poolcreated_context(b, instruction);
165        }
166        (MeteoraPoolsSetPoolFees(b), MeteoraPoolsSetPoolFees(i)) => {
167            let instruction = std::mem::replace(b, i);
168            merge_meteora_pools_setpoolfees_context(b, instruction);
169        }
170
171        // ========== Meteora DAMM V2 系列 ==========
172        (MeteoraDammV2Swap(b), MeteoraDammV2Swap(i)) => merge_generic(b, i),
173        (MeteoraDammV2AddLiquidity(b), MeteoraDammV2AddLiquidity(i)) => merge_generic(b, i),
174        (MeteoraDammV2RemoveLiquidity(b), MeteoraDammV2RemoveLiquidity(i)) => merge_generic(b, i),
175        (MeteoraDammV2InitializePool(b), MeteoraDammV2InitializePool(i)) => merge_generic(b, i),
176        (MeteoraDammV2ClaimPositionFee(b), MeteoraDammV2ClaimPositionFee(i)) => merge_generic(b, i),
177        (MeteoraDammV2ClaimReward(b), MeteoraDammV2ClaimReward(i)) => merge_generic(b, i),
178        (MeteoraDlmmClaimReward(b), MeteoraDlmmClaimReward(i)) => merge_generic(b, i),
179        (MeteoraDammV2CreatePosition(b), MeteoraDammV2CreatePosition(i)) => merge_generic(b, i),
180        (MeteoraDammV2ClosePosition(b), MeteoraDammV2ClosePosition(i)) => merge_generic(b, i),
181        (MeteoraDammV2UpdateDelegatePermission(b), MeteoraDammV2UpdateDelegatePermission(i)) => {
182            merge_generic(b, i)
183        }
184        (
185            MeteoraDammV2WithdrawDeadLiquidityReward(b),
186            MeteoraDammV2WithdrawDeadLiquidityReward(i),
187        ) => merge_generic(b, i),
188        (MeteoraDammV2WithdrawIneligibleReward(b), MeteoraDammV2WithdrawIneligibleReward(i)) => {
189            merge_generic(b, i)
190        }
191        (MeteoraDammV2UpdateRewardFunder(b), MeteoraDammV2UpdateRewardFunder(i)) => {
192            merge_generic(b, i)
193        }
194        (MeteoraDammV2UpdateRewardDuration(b), MeteoraDammV2UpdateRewardDuration(i)) => {
195            merge_generic(b, i)
196        }
197        (MeteoraDammV2InitializeReward(b), MeteoraDammV2InitializeReward(i)) => merge_generic(b, i),
198        (MeteoraDammV2FundReward(b), MeteoraDammV2FundReward(i)) => merge_generic(b, i),
199        (MeteoraDammV2CreateConfig(b), MeteoraDammV2CreateConfig(i)) => merge_generic(b, i),
200        (MeteoraDammV2CreateDynamicConfig(b), MeteoraDammV2CreateDynamicConfig(i)) => {
201            merge_generic(b, i)
202        }
203
204        // ========== Meteora DLMM 系列 ==========
205        (MeteoraDlmmSwap(b), MeteoraDlmmSwap(i)) => merge_dlmm_swap(b, i),
206        (MeteoraDlmmAddLiquidity(b), MeteoraDlmmAddLiquidity(i)) => merge_generic(b, i),
207        (MeteoraDlmmRemoveLiquidity(b), MeteoraDlmmRemoveLiquidity(i)) => merge_generic(b, i),
208        (MeteoraDlmmInitializePool(b), MeteoraDlmmInitializePool(i)) => {
209            merge_dlmm_initialize_pool(b, i)
210        }
211        (MeteoraDlmmInitializeBinArray(b), MeteoraDlmmInitializeBinArray(i)) => merge_generic(b, i),
212        (MeteoraDlmmCreatePosition(b), MeteoraDlmmCreatePosition(i)) => {
213            merge_dlmm_create_position(b, i)
214        }
215        (MeteoraDlmmClosePosition(b), MeteoraDlmmClosePosition(i)) => {
216            merge_dlmm_close_position(b, i)
217        }
218        (MeteoraDlmmClaimFee(b), MeteoraDlmmClaimFee(i)) => merge_generic(b, i),
219
220        // ========== RaydiumLaunchlab 系列 ==========
221        (RaydiumLaunchlabTrade(b), RaydiumLaunchlabTrade(i)) => merge_generic(b, i),
222        (RaydiumLaunchlabPoolCreate(b), RaydiumLaunchlabPoolCreate(i)) => merge_generic(b, i),
223        (RaydiumLaunchlabMigrateAmm(b), RaydiumLaunchlabMigrateAmm(i)) => merge_generic(b, i),
224
225        // 其他组合不需要合并(类型不匹配)
226        (_, event) => {
227            *unmerged = Some(event);
228            return false;
229        }
230    }
231
232    true
233}
234
235/// 通用合并函数 - 对于大多数事件,inner instruction 包含完整数据
236///
237/// 这个函数简单地用 inner 的数据覆盖 base,因为:
238/// - Inner instruction 来自程序日志,包含完整的交易数据
239/// - Instruction 主要提供账户上下文
240/// - 对于大多数协议,inner instruction 的数据已经足够完整
241#[inline(always)]
242fn merge_cpmm_swap(base: &mut RaydiumCpmmSwapEvent, inner: RaydiumCpmmSwapEvent) {
243    let instruction = std::mem::replace(base, inner);
244    merge_raydium_cpmm_swap_log_preferred(base, instruction);
245}
246
247#[inline]
248fn merge_raydium_cpmm_swap_log_preferred(log: &mut RaydiumCpmmSwapEvent, ix: RaydiumCpmmSwapEvent) {
249    if !ix.ix_name.is_empty() {
250        log.ix_name = ix.ix_name;
251        log.amount_in = ix.amount_in;
252        log.minimum_amount_out = ix.minimum_amount_out;
253        log.max_amount_in = ix.max_amount_in;
254        log.amount_out = ix.amount_out;
255    }
256    fill_pk(&mut log.payer, ix.payer);
257    fill_pk(&mut log.authority, ix.authority);
258    fill_pk(&mut log.input_token_account, ix.input_token_account);
259    fill_pk(&mut log.output_token_account, ix.output_token_account);
260    fill_pk(&mut log.amm_config, ix.amm_config);
261    fill_pk(&mut log.input_vault, ix.input_vault);
262    fill_pk(&mut log.output_vault, ix.output_vault);
263    fill_pk(&mut log.input_token_program, ix.input_token_program);
264    fill_pk(&mut log.output_token_program, ix.output_token_program);
265    fill_pk(&mut log.input_token_mint, ix.input_token_mint);
266    fill_pk(&mut log.output_token_mint, ix.output_token_mint);
267    fill_pk(&mut log.observation_state, ix.observation_state);
268}
269
270#[inline(always)]
271fn merge_clmm_swap(base: &mut RaydiumClmmSwapEvent, inner: RaydiumClmmSwapEvent) {
272    let instruction = std::mem::replace(base, inner);
273    merge_raydium_clmm_swap_log_preferred(base, instruction);
274}
275
276#[inline(always)]
277fn merge_generic<T>(base: &mut T, inner: T) {
278    *base = inner;
279}
280
281#[inline(always)]
282fn merge_dlmm_swap(base: &mut MeteoraDlmmSwapEvent, inner: MeteoraDlmmSwapEvent) {
283    let min_amount_out =
284        if inner.min_amount_out != 0 { inner.min_amount_out } else { base.min_amount_out };
285    let user_token_in = if inner.user_token_in != Pubkey::default() {
286        inner.user_token_in
287    } else {
288        base.user_token_in
289    };
290    let user_token_out = if inner.user_token_out != Pubkey::default() {
291        inner.user_token_out
292    } else {
293        base.user_token_out
294    };
295    *base = inner;
296    base.min_amount_out = min_amount_out;
297    base.user_token_in = user_token_in;
298    base.user_token_out = user_token_out;
299}
300
301#[inline(always)]
302fn merge_dlmm_initialize_pool(
303    base: &mut MeteoraDlmmInitializePoolEvent,
304    inner: MeteoraDlmmInitializePoolEvent,
305) {
306    let creator = base.creator;
307    let active_bin_id = base.active_bin_id;
308    *base = inner;
309    base.creator = creator;
310    base.active_bin_id = active_bin_id;
311}
312
313#[inline(always)]
314fn merge_dlmm_create_position(
315    base: &mut MeteoraDlmmCreatePositionEvent,
316    inner: MeteoraDlmmCreatePositionEvent,
317) {
318    let lower_bin_id = base.lower_bin_id;
319    let width = base.width;
320    *base = inner;
321    base.lower_bin_id = lower_bin_id;
322    base.width = width;
323}
324
325#[inline(always)]
326fn merge_dlmm_close_position(
327    base: &mut MeteoraDlmmClosePositionEvent,
328    inner: MeteoraDlmmClosePositionEvent,
329) {
330    let pool = base.pool;
331    *base = inner;
332    base.pool = pool;
333}
334
335// ============================================================================
336// PumpFun 事件合并实现
337// ============================================================================
338
339#[inline(always)]
340fn put_pk_if_set(to: &mut Pubkey, from: Pubkey) {
341    if from != Pubkey::default() {
342        *to = from;
343    }
344}
345
346#[inline(always)]
347fn put_pumpfun_quote_mint_if_set(to: &mut Pubkey, from: Pubkey) {
348    let from = normalize_pumpfun_quote_mint(from);
349    if from != Pubkey::default()
350        && (*to == Pubkey::default()
351            || is_pumpfun_solscan_sol_quote_mint(*to)
352            || !is_pumpfun_solscan_sol_quote_mint(from))
353    {
354        *to = from;
355    }
356}
357
358#[inline(always)]
359fn put_u64_if_nonzero(to: &mut u64, from: u64) {
360    if from != 0 {
361        *to = from;
362    }
363}
364
365#[inline(always)]
366fn put_i64_if_nonzero(to: &mut i64, from: i64) {
367    if from != 0 {
368        *to = from;
369    }
370}
371
372/// 合并 PumpFun Trade 事件
373///
374/// 合并策略:
375/// - Inner instruction 提供: 交易数据(amount, reserves, fees 等)
376/// - Instruction 提供: 账户上下文(bonding_curve, associated_bonding_curve 等)
377/// - 合并后: 完整的交易事件
378///
379/// 同一 outer 下多段 inner 链式合并时:若某段 inner 未带成交量(`sol_amount`/`token_amount` 均为 0),
380/// 则不再用其覆盖金额与储备,避免把前一段已合并好的数据清空。
381#[inline(always)]
382fn merge_pumpfun_trade(base: &mut PumpFunTradeEvent, inner: PumpFunTradeEvent) {
383    let leg = inner.sol_amount != 0 || inner.token_amount != 0;
384
385    put_pk_if_set(&mut base.mint, inner.mint);
386    put_pk_if_set(&mut base.user, inner.user);
387    put_pk_if_set(&mut base.fee_recipient, inner.fee_recipient);
388    put_pk_if_set(&mut base.creator, inner.creator);
389
390    if leg {
391        base.sol_amount = inner.sol_amount;
392        base.token_amount = inner.token_amount;
393        base.is_buy = inner.is_buy;
394        base.timestamp = inner.timestamp;
395        base.virtual_sol_reserves = inner.virtual_sol_reserves;
396        base.virtual_token_reserves = inner.virtual_token_reserves;
397        base.real_sol_reserves = inner.real_sol_reserves;
398        base.real_token_reserves = inner.real_token_reserves;
399        base.fee_basis_points = inner.fee_basis_points;
400        base.fee = inner.fee;
401        base.creator_fee_basis_points = inner.creator_fee_basis_points;
402        base.creator_fee = inner.creator_fee;
403        base.track_volume |= inner.track_volume;
404        base.total_unclaimed_tokens = inner.total_unclaimed_tokens;
405        base.total_claimed_tokens = inner.total_claimed_tokens;
406        base.current_sol_volume = inner.current_sol_volume;
407        base.last_update_timestamp = inner.last_update_timestamp;
408        if !inner.ix_name.is_empty() {
409            base.ix_name = inner.ix_name;
410        }
411        base.mayhem_mode |= inner.mayhem_mode;
412        put_u64_if_nonzero(&mut base.cashback_fee_basis_points, inner.cashback_fee_basis_points);
413        put_u64_if_nonzero(&mut base.cashback, inner.cashback);
414        put_u64_if_nonzero(&mut base.buyback_fee_basis_points, inner.buyback_fee_basis_points);
415        put_u64_if_nonzero(&mut base.buyback_fee, inner.buyback_fee);
416        if base.shareholders.is_empty() && !inner.shareholders.is_empty() {
417            base.shareholders = inner.shareholders;
418        }
419        put_pumpfun_quote_mint_if_set(&mut base.quote_mint, inner.quote_mint);
420        put_u64_if_nonzero(&mut base.quote_amount, inner.quote_amount);
421        put_u64_if_nonzero(&mut base.virtual_quote_reserves, inner.virtual_quote_reserves);
422        put_u64_if_nonzero(&mut base.real_quote_reserves, inner.real_quote_reserves);
423        base.is_cashback_coin |= inner.is_cashback_coin;
424        base.holder_rewards_bps = inner.holder_rewards_bps;
425        base.holder_rewards = inner.holder_rewards;
426        base.creator_fee_unclaimed = inner.creator_fee_unclaimed;
427    } else {
428        put_u64_if_nonzero(&mut base.fee, inner.fee);
429        put_u64_if_nonzero(&mut base.creator_fee, inner.creator_fee);
430        put_u64_if_nonzero(&mut base.fee_basis_points, inner.fee_basis_points);
431        put_u64_if_nonzero(&mut base.creator_fee_basis_points, inner.creator_fee_basis_points);
432        put_u64_if_nonzero(&mut base.virtual_sol_reserves, inner.virtual_sol_reserves);
433        put_u64_if_nonzero(&mut base.virtual_token_reserves, inner.virtual_token_reserves);
434        put_u64_if_nonzero(&mut base.real_sol_reserves, inner.real_sol_reserves);
435        put_u64_if_nonzero(&mut base.real_token_reserves, inner.real_token_reserves);
436        put_u64_if_nonzero(&mut base.total_unclaimed_tokens, inner.total_unclaimed_tokens);
437        put_u64_if_nonzero(&mut base.total_claimed_tokens, inner.total_claimed_tokens);
438        put_u64_if_nonzero(&mut base.current_sol_volume, inner.current_sol_volume);
439        put_u64_if_nonzero(&mut base.cashback_fee_basis_points, inner.cashback_fee_basis_points);
440        put_u64_if_nonzero(&mut base.cashback, inner.cashback);
441        put_u64_if_nonzero(&mut base.buyback_fee_basis_points, inner.buyback_fee_basis_points);
442        put_u64_if_nonzero(&mut base.buyback_fee, inner.buyback_fee);
443        if base.shareholders.is_empty() && !inner.shareholders.is_empty() {
444            base.shareholders = inner.shareholders;
445        }
446        put_pumpfun_quote_mint_if_set(&mut base.quote_mint, inner.quote_mint);
447        put_u64_if_nonzero(&mut base.quote_amount, inner.quote_amount);
448        put_u64_if_nonzero(&mut base.virtual_quote_reserves, inner.virtual_quote_reserves);
449        put_u64_if_nonzero(&mut base.real_quote_reserves, inner.real_quote_reserves);
450        put_u64_if_nonzero(&mut base.holder_rewards_bps, inner.holder_rewards_bps);
451        put_u64_if_nonzero(&mut base.holder_rewards, inner.holder_rewards);
452        put_i64_if_nonzero(&mut base.timestamp, inner.timestamp);
453        put_i64_if_nonzero(&mut base.last_update_timestamp, inner.last_update_timestamp);
454        if !inner.ix_name.is_empty() {
455            base.ix_name = inner.ix_name;
456        }
457        base.track_volume |= inner.track_volume;
458        base.mayhem_mode |= inner.mayhem_mode;
459        base.is_cashback_coin |= inner.is_cashback_coin;
460    }
461    put_u64_if_nonzero(&mut base.amount, inner.amount);
462    put_u64_if_nonzero(&mut base.max_sol_cost, inner.max_sol_cost);
463    put_u64_if_nonzero(&mut base.min_sol_output, inner.min_sol_output);
464    put_u64_if_nonzero(&mut base.spendable_sol_in, inner.spendable_sol_in);
465    put_u64_if_nonzero(&mut base.spendable_quote_in, inner.spendable_quote_in);
466    put_u64_if_nonzero(&mut base.min_tokens_out, inner.min_tokens_out);
467    put_pk_if_set(&mut base.global, inner.global);
468    put_pk_if_set(&mut base.bonding_curve, inner.bonding_curve);
469    put_pk_if_set(&mut base.bonding_curve_v2, inner.bonding_curve_v2);
470    put_pk_if_set(&mut base.associated_bonding_curve, inner.associated_bonding_curve);
471    put_pk_if_set(&mut base.associated_user, inner.associated_user);
472    put_pk_if_set(&mut base.system_program, inner.system_program);
473    put_pk_if_set(&mut base.token_program, inner.token_program);
474    put_pk_if_set(&mut base.quote_token_program, inner.quote_token_program);
475    put_pk_if_set(&mut base.associated_token_program, inner.associated_token_program);
476    put_pk_if_set(&mut base.creator_vault, inner.creator_vault);
477    put_pk_if_set(&mut base.associated_quote_fee_recipient, inner.associated_quote_fee_recipient);
478    put_pk_if_set(&mut base.buyback_fee_recipient, inner.buyback_fee_recipient);
479    put_pk_if_set(
480        &mut base.associated_quote_buyback_fee_recipient,
481        inner.associated_quote_buyback_fee_recipient,
482    );
483    put_pk_if_set(&mut base.associated_quote_bonding_curve, inner.associated_quote_bonding_curve);
484    put_pk_if_set(&mut base.associated_quote_user, inner.associated_quote_user);
485    put_pk_if_set(&mut base.associated_creator_vault, inner.associated_creator_vault);
486    put_pk_if_set(&mut base.sharing_config, inner.sharing_config);
487    put_pk_if_set(&mut base.event_authority, inner.event_authority);
488    put_pk_if_set(&mut base.program, inner.program);
489    put_pk_if_set(&mut base.global_volume_accumulator, inner.global_volume_accumulator);
490    put_pk_if_set(&mut base.user_volume_accumulator, inner.user_volume_accumulator);
491    put_pk_if_set(
492        &mut base.associated_user_volume_accumulator,
493        inner.associated_user_volume_accumulator,
494    );
495    put_pk_if_set(&mut base.fee_config, inner.fee_config);
496    put_pk_if_set(&mut base.fee_program, inner.fee_program);
497    if base.account.is_none() {
498        base.account = inner.account;
499    }
500
501    base.is_created_buy |= inner.is_created_buy;
502    // 保留 base 的账户上下文字段(bonding_curve, associated_bonding_curve 等)
503}
504
505/// 合并 PumpFun Create 事件
506#[inline(always)]
507fn merge_pumpfun_create(base: &mut PumpFunCreateTokenEvent, inner: PumpFunCreateTokenEvent) {
508    // Inner instruction 包含完整的 create 数据
509    base.name = inner.name;
510    base.symbol = inner.symbol;
511    base.uri = inner.uri;
512    base.mint = inner.mint;
513    base.bonding_curve = inner.bonding_curve;
514    base.user = inner.user;
515    base.creator = inner.creator;
516    base.timestamp = inner.timestamp;
517    base.virtual_token_reserves = inner.virtual_token_reserves;
518    base.virtual_sol_reserves = inner.virtual_sol_reserves;
519    base.real_token_reserves = inner.real_token_reserves;
520    base.token_total_supply = inner.token_total_supply;
521    base.token_program = inner.token_program;
522    base.is_mayhem_mode = inner.is_mayhem_mode;
523    base.is_cashback_enabled = inner.is_cashback_enabled;
524    put_pumpfun_quote_mint_if_set(&mut base.quote_mint, inner.quote_mint);
525    put_pk_if_set(&mut base.quote_vault, inner.quote_vault);
526    put_pk_if_set(&mut base.quote_token_program, inner.quote_token_program);
527    put_u64_if_nonzero(&mut base.virtual_quote_reserves, inner.virtual_quote_reserves);
528    put_u64_if_nonzero(&mut base.creator_fee_bps, inner.creator_fee_bps);
529    base.is_holder_reward |= inner.is_holder_reward;
530}
531
532/// 合并 PumpFun CreateV2 事件
533#[inline(always)]
534fn merge_pumpfun_create_v2(base: &mut PumpFunCreateV2TokenEvent, inner: PumpFunCreateV2TokenEvent) {
535    fill_str_if_empty(&mut base.name, &inner.name);
536    fill_str_if_empty(&mut base.symbol, &inner.symbol);
537    fill_str_if_empty(&mut base.uri, &inner.uri);
538    put_pk_if_set(&mut base.mint, inner.mint);
539    put_pk_if_set(&mut base.bonding_curve, inner.bonding_curve);
540    put_pk_if_set(&mut base.user, inner.user);
541    put_pk_if_set(&mut base.creator, inner.creator);
542    put_i64_if_nonzero(&mut base.timestamp, inner.timestamp);
543    put_u64_if_nonzero(&mut base.virtual_token_reserves, inner.virtual_token_reserves);
544    put_u64_if_nonzero(&mut base.virtual_sol_reserves, inner.virtual_sol_reserves);
545    put_u64_if_nonzero(&mut base.real_token_reserves, inner.real_token_reserves);
546    put_u64_if_nonzero(&mut base.token_total_supply, inner.token_total_supply);
547    put_pk_if_set(&mut base.token_program, inner.token_program);
548    base.is_mayhem_mode |= inner.is_mayhem_mode;
549    base.is_cashback_enabled |= inner.is_cashback_enabled;
550    put_pumpfun_quote_mint_if_set(&mut base.quote_mint, inner.quote_mint);
551    put_pk_if_set(&mut base.quote_vault, inner.quote_vault);
552    put_pk_if_set(&mut base.quote_token_program, inner.quote_token_program);
553    put_u64_if_nonzero(&mut base.virtual_quote_reserves, inner.virtual_quote_reserves);
554    put_pk_if_set(&mut base.mint_authority, inner.mint_authority);
555    put_pk_if_set(&mut base.associated_bonding_curve, inner.associated_bonding_curve);
556    put_pk_if_set(&mut base.global, inner.global);
557    put_pk_if_set(&mut base.system_program, inner.system_program);
558    put_pk_if_set(&mut base.associated_token_program, inner.associated_token_program);
559    put_pk_if_set(&mut base.mayhem_program_id, inner.mayhem_program_id);
560    put_pk_if_set(&mut base.global_params, inner.global_params);
561    put_pk_if_set(&mut base.sol_vault, inner.sol_vault);
562    put_pk_if_set(&mut base.mayhem_state, inner.mayhem_state);
563    put_pk_if_set(&mut base.mayhem_token_vault, inner.mayhem_token_vault);
564    put_pk_if_set(&mut base.event_authority, inner.event_authority);
565    put_pk_if_set(&mut base.program, inner.program);
566    put_pk_if_set(&mut base.observed_fee_recipient, inner.observed_fee_recipient);
567    put_u64_if_nonzero(&mut base.creator_fee_bps, inner.creator_fee_bps);
568    base.is_holder_reward |= inner.is_holder_reward;
569}
570
571/// 合并 PumpFun Migrate 事件
572#[inline(always)]
573fn merge_pumpfun_migrate(base: &mut PumpFunMigrateEvent, inner: PumpFunMigrateEvent) {
574    // Inner instruction 包含完整的 migrate 数据
575    base.user = inner.user;
576    base.mint = inner.mint;
577    base.mint_amount = inner.mint_amount;
578    base.sol_amount = inner.sol_amount;
579    base.pool_migration_fee = inner.pool_migration_fee;
580    base.bonding_curve = inner.bonding_curve;
581    base.timestamp = inner.timestamp;
582    base.pool = inner.pool;
583}
584
585#[inline(always)]
586fn merge_pumpswap_buy(base: &mut PumpSwapBuyEvent, inner: PumpSwapBuyEvent) {
587    let ix = std::mem::take(base);
588    *base = inner;
589    merge_pumpswap_buy_log_preferred(base, ix);
590}
591
592#[inline(always)]
593fn merge_pumpswap_sell(base: &mut PumpSwapSellEvent, inner: PumpSwapSellEvent) {
594    let ix = std::mem::take(base);
595    *base = inner;
596    merge_pumpswap_sell_log_preferred(base, ix);
597}
598
599// ============================================================================
600// 工具函数
601// ============================================================================
602
603/// 判断两个事件是否可以合并
604///
605/// 合并条件:
606/// 1. 都是同一个协议的事件
607/// 2. 事件类型兼容(例如 Trade 和 Buy 可以合并)
608/// 3. 来自同一个交易(signature 相同)
609#[inline(always)]
610pub fn can_merge(base: &DexEvent, inner: &DexEvent) -> bool {
611    if pump_trade_identity_conflicts(base, inner) {
612        return false;
613    }
614    // 检查 signature 是否相同
615    if base.metadata().signature != inner.metadata().signature {
616        return false;
617    }
618
619    // 检查事件类型是否兼容
620    match (base, inner) {
621        // PumpFun Trade 系列事件可以互相合并
622        (DexEvent::PumpFunTrade(_), DexEvent::PumpFunTrade(_))
623        | (DexEvent::PumpFunTrade(_), DexEvent::PumpFunBuy(_))
624        | (DexEvent::PumpFunTrade(_), DexEvent::PumpFunSell(_))
625        | (DexEvent::PumpFunTrade(_), DexEvent::PumpFunBuyExactSolIn(_))
626        | (DexEvent::PumpFunBuy(_), DexEvent::PumpFunTrade(_))
627        | (DexEvent::PumpFunBuy(_), DexEvent::PumpFunBuy(_))
628        | (DexEvent::PumpFunSell(_), DexEvent::PumpFunTrade(_))
629        | (DexEvent::PumpFunSell(_), DexEvent::PumpFunSell(_))
630        | (DexEvent::PumpFunBuyExactSolIn(_), DexEvent::PumpFunTrade(_))
631        | (DexEvent::PumpFunBuyExactSolIn(_), DexEvent::PumpFunBuyExactSolIn(_)) => true,
632
633        // PumpFun Create / CreateV2 可以合并
634        (DexEvent::PumpFunCreate(_), DexEvent::PumpFunCreate(_)) => true,
635        (DexEvent::PumpFunCreateV2(_), DexEvent::PumpFunCreateV2(_)) => true,
636
637        // PumpFun Migrate 可以合并
638        (DexEvent::PumpFunMigrate(_), DexEvent::PumpFunMigrate(_)) => true,
639
640        (DexEvent::PumpSwapBuy(_), DexEvent::PumpSwapBuy(_))
641        | (DexEvent::PumpSwapSell(_), DexEvent::PumpSwapSell(_)) => true,
642
643        // 其他组合不支持合并
644        _ => false,
645    }
646}
647
648// ============================================================================
649// gRPC:日志优先 + 指令补充(Yellowstone 并行解析 log / ix)
650// ============================================================================
651
652#[inline(always)]
653fn fill_pk(to: &mut Pubkey, from: Pubkey) {
654    if *to == Pubkey::default() && from != Pubkey::default() {
655        *to = from;
656    }
657}
658
659#[inline(always)]
660fn fill_pumpfun_quote_mint(to: &mut Pubkey, from: Pubkey) {
661    let from = normalize_pumpfun_quote_mint(from);
662    if (*to == Pubkey::default() || is_pumpfun_solscan_sol_quote_mint(*to))
663        && from != Pubkey::default()
664    {
665        *to = from;
666    }
667}
668
669#[inline(always)]
670fn fill_str_if_empty(to: &mut String, from: &str) {
671    if to.is_empty() && !from.is_empty() {
672        to.push_str(from);
673    }
674}
675
676/// PumpFun Trade:**保留 `log` 侧全部链上事件数值与标志**(与 `TradeEvent` 日志一致),
677/// 仅用 `ix` 补齐默认的账户类字段;`is_created_buy` 若仅 ix 侧为 true 则置位(创建首买标记)。
678#[inline]
679fn merge_pumpfun_trade_log_preferred(log: &mut PumpFunTradeEvent, ix: PumpFunTradeEvent) {
680    fill_pk(&mut log.global, ix.global);
681    fill_pk(&mut log.bonding_curve, ix.bonding_curve);
682    fill_pk(&mut log.bonding_curve_v2, ix.bonding_curve_v2);
683    fill_pk(&mut log.associated_bonding_curve, ix.associated_bonding_curve);
684    fill_pk(&mut log.associated_user, ix.associated_user);
685    fill_pk(&mut log.system_program, ix.system_program);
686    fill_pk(&mut log.token_program, ix.token_program);
687    fill_pk(&mut log.quote_token_program, ix.quote_token_program);
688    fill_pk(&mut log.associated_token_program, ix.associated_token_program);
689    fill_pk(&mut log.creator_vault, ix.creator_vault);
690    fill_pk(&mut log.fee_recipient, ix.fee_recipient);
691    fill_pk(&mut log.creator, ix.creator);
692    fill_pumpfun_quote_mint(&mut log.quote_mint, ix.quote_mint);
693    fill_pk(&mut log.associated_quote_fee_recipient, ix.associated_quote_fee_recipient);
694    fill_pk(&mut log.buyback_fee_recipient, ix.buyback_fee_recipient);
695    fill_pk(
696        &mut log.associated_quote_buyback_fee_recipient,
697        ix.associated_quote_buyback_fee_recipient,
698    );
699    fill_pk(&mut log.associated_quote_bonding_curve, ix.associated_quote_bonding_curve);
700    fill_pk(&mut log.associated_quote_user, ix.associated_quote_user);
701    fill_pk(&mut log.associated_creator_vault, ix.associated_creator_vault);
702    fill_pk(&mut log.sharing_config, ix.sharing_config);
703    fill_pk(&mut log.event_authority, ix.event_authority);
704    fill_pk(&mut log.program, ix.program);
705    fill_pk(&mut log.global_volume_accumulator, ix.global_volume_accumulator);
706    fill_pk(&mut log.user_volume_accumulator, ix.user_volume_accumulator);
707    fill_pk(&mut log.associated_user_volume_accumulator, ix.associated_user_volume_accumulator);
708    fill_pk(&mut log.fee_config, ix.fee_config);
709    fill_pk(&mut log.fee_program, ix.fee_program);
710    if log.account.is_none() {
711        log.account = ix.account;
712    }
713    if log.ix_name.is_empty() && !ix.ix_name.is_empty() {
714        log.ix_name = ix.ix_name;
715    }
716    put_u64_if_nonzero(&mut log.amount, ix.amount);
717    put_u64_if_nonzero(&mut log.max_sol_cost, ix.max_sol_cost);
718    put_u64_if_nonzero(&mut log.min_sol_output, ix.min_sol_output);
719    put_u64_if_nonzero(&mut log.spendable_sol_in, ix.spendable_sol_in);
720    put_u64_if_nonzero(&mut log.spendable_quote_in, ix.spendable_quote_in);
721    put_u64_if_nonzero(&mut log.min_tokens_out, ix.min_tokens_out);
722    if log.quote_amount == 0 {
723        put_u64_if_nonzero(&mut log.quote_amount, ix.quote_amount);
724    }
725    if log.virtual_quote_reserves == 0 {
726        put_u64_if_nonzero(&mut log.virtual_quote_reserves, ix.virtual_quote_reserves);
727    }
728    if log.real_quote_reserves == 0 {
729        put_u64_if_nonzero(&mut log.real_quote_reserves, ix.real_quote_reserves);
730    }
731    if !log.is_created_buy && ix.is_created_buy {
732        log.is_created_buy = true;
733    }
734}
735
736#[inline]
737fn merge_pumpfun_create_log_preferred(
738    log: &mut PumpFunCreateTokenEvent,
739    ix: PumpFunCreateTokenEvent,
740) {
741    fill_str_if_empty(&mut log.name, &ix.name);
742    fill_str_if_empty(&mut log.symbol, &ix.symbol);
743    fill_str_if_empty(&mut log.uri, &ix.uri);
744    fill_pk(&mut log.bonding_curve, ix.bonding_curve);
745    fill_pk(&mut log.user, ix.user);
746    fill_pk(&mut log.creator, ix.creator);
747    fill_pk(&mut log.token_program, ix.token_program);
748    fill_pumpfun_quote_mint(&mut log.quote_mint, ix.quote_mint);
749    fill_pk(&mut log.quote_vault, ix.quote_vault);
750    fill_pk(&mut log.quote_token_program, ix.quote_token_program);
751    put_u64_if_nonzero(&mut log.virtual_quote_reserves, ix.virtual_quote_reserves);
752    if log.creator_fee_bps == 0 {
753        log.creator_fee_bps = ix.creator_fee_bps;
754    }
755    if log.ix_name.is_empty() && !ix.ix_name.is_empty() {
756        log.ix_name = ix.ix_name;
757    }
758    log.is_mayhem_mode |= ix.is_mayhem_mode;
759    log.is_cashback_enabled |= ix.is_cashback_enabled;
760    log.is_holder_reward |= ix.is_holder_reward;
761}
762
763#[inline]
764fn merge_pumpfun_create_v2_into_create_log_preferred(
765    log: &mut PumpFunCreateTokenEvent,
766    ix: PumpFunCreateV2TokenEvent,
767) {
768    fill_str_if_empty(&mut log.name, &ix.name);
769    fill_str_if_empty(&mut log.symbol, &ix.symbol);
770    fill_str_if_empty(&mut log.uri, &ix.uri);
771    fill_pk(&mut log.mint, ix.mint);
772    fill_pk(&mut log.bonding_curve, ix.bonding_curve);
773    fill_pk(&mut log.user, ix.user);
774    fill_pk(&mut log.creator, ix.creator);
775    if log.timestamp == 0 && ix.timestamp != 0 {
776        log.timestamp = ix.timestamp;
777    }
778    put_u64_if_nonzero(&mut log.virtual_token_reserves, ix.virtual_token_reserves);
779    put_u64_if_nonzero(&mut log.virtual_sol_reserves, ix.virtual_sol_reserves);
780    put_u64_if_nonzero(&mut log.real_token_reserves, ix.real_token_reserves);
781    put_u64_if_nonzero(&mut log.token_total_supply, ix.token_total_supply);
782    fill_pk(&mut log.token_program, ix.token_program);
783    fill_pumpfun_quote_mint(&mut log.quote_mint, ix.quote_mint);
784    fill_pk(&mut log.quote_vault, ix.quote_vault);
785    fill_pk(&mut log.quote_token_program, ix.quote_token_program);
786    put_u64_if_nonzero(&mut log.virtual_quote_reserves, ix.virtual_quote_reserves);
787    if log.creator_fee_bps == 0 {
788        log.creator_fee_bps = ix.creator_fee_bps;
789    }
790    if log.ix_name.is_empty() && !ix.ix_name.is_empty() {
791        log.ix_name = ix.ix_name;
792    }
793    log.is_mayhem_mode |= ix.is_mayhem_mode;
794    log.is_cashback_enabled |= ix.is_cashback_enabled;
795    log.is_holder_reward |= ix.is_holder_reward;
796}
797
798#[inline]
799fn merge_pumpfun_create_into_create_v2_log_preferred(
800    log: &mut PumpFunCreateV2TokenEvent,
801    ix: PumpFunCreateTokenEvent,
802) {
803    fill_str_if_empty(&mut log.name, &ix.name);
804    fill_str_if_empty(&mut log.symbol, &ix.symbol);
805    fill_str_if_empty(&mut log.uri, &ix.uri);
806    fill_pk(&mut log.mint, ix.mint);
807    fill_pk(&mut log.bonding_curve, ix.bonding_curve);
808    fill_pk(&mut log.user, ix.user);
809    fill_pk(&mut log.creator, ix.creator);
810    if log.timestamp == 0 && ix.timestamp != 0 {
811        log.timestamp = ix.timestamp;
812    }
813    put_u64_if_nonzero(&mut log.virtual_token_reserves, ix.virtual_token_reserves);
814    put_u64_if_nonzero(&mut log.virtual_sol_reserves, ix.virtual_sol_reserves);
815    put_u64_if_nonzero(&mut log.real_token_reserves, ix.real_token_reserves);
816    put_u64_if_nonzero(&mut log.token_total_supply, ix.token_total_supply);
817    fill_pk(&mut log.token_program, ix.token_program);
818    fill_pumpfun_quote_mint(&mut log.quote_mint, ix.quote_mint);
819    fill_pk(&mut log.quote_vault, ix.quote_vault);
820    fill_pk(&mut log.quote_token_program, ix.quote_token_program);
821    put_u64_if_nonzero(&mut log.virtual_quote_reserves, ix.virtual_quote_reserves);
822    if log.creator_fee_bps == 0 {
823        log.creator_fee_bps = ix.creator_fee_bps;
824    }
825    if log.ix_name.is_empty() && !ix.ix_name.is_empty() {
826        log.ix_name = ix.ix_name;
827    }
828    log.is_mayhem_mode |= ix.is_mayhem_mode;
829    log.is_cashback_enabled |= ix.is_cashback_enabled;
830    log.is_holder_reward |= ix.is_holder_reward;
831}
832
833#[inline]
834fn merge_pumpfun_create_v2_log_preferred(
835    log: &mut PumpFunCreateV2TokenEvent,
836    ix: PumpFunCreateV2TokenEvent,
837) {
838    fill_str_if_empty(&mut log.name, &ix.name);
839    fill_str_if_empty(&mut log.symbol, &ix.symbol);
840    fill_str_if_empty(&mut log.uri, &ix.uri);
841    fill_pk(&mut log.bonding_curve, ix.bonding_curve);
842    fill_pk(&mut log.user, ix.user);
843    fill_pk(&mut log.creator, ix.creator);
844    fill_pk(&mut log.token_program, ix.token_program);
845    fill_pumpfun_quote_mint(&mut log.quote_mint, ix.quote_mint);
846    fill_pk(&mut log.quote_vault, ix.quote_vault);
847    fill_pk(&mut log.quote_token_program, ix.quote_token_program);
848    put_u64_if_nonzero(&mut log.virtual_quote_reserves, ix.virtual_quote_reserves);
849    if log.creator_fee_bps == 0 {
850        log.creator_fee_bps = ix.creator_fee_bps;
851    }
852    if log.ix_name.is_empty() && !ix.ix_name.is_empty() {
853        log.ix_name = ix.ix_name;
854    }
855    fill_pk(&mut log.mint_authority, ix.mint_authority);
856    fill_pk(&mut log.associated_bonding_curve, ix.associated_bonding_curve);
857    fill_pk(&mut log.global, ix.global);
858    fill_pk(&mut log.system_program, ix.system_program);
859    fill_pk(&mut log.associated_token_program, ix.associated_token_program);
860    fill_pk(&mut log.mayhem_program_id, ix.mayhem_program_id);
861    fill_pk(&mut log.global_params, ix.global_params);
862    fill_pk(&mut log.sol_vault, ix.sol_vault);
863    fill_pk(&mut log.mayhem_state, ix.mayhem_state);
864    fill_pk(&mut log.mayhem_token_vault, ix.mayhem_token_vault);
865    fill_pk(&mut log.event_authority, ix.event_authority);
866    fill_pk(&mut log.program, ix.program);
867    fill_pk(&mut log.observed_fee_recipient, ix.observed_fee_recipient);
868    log.is_holder_reward |= ix.is_holder_reward;
869}
870
871#[inline]
872fn merge_pumpfun_migrate_log_preferred(log: &mut PumpFunMigrateEvent, ix: PumpFunMigrateEvent) {
873    fill_pk(&mut log.bonding_curve, ix.bonding_curve);
874    fill_pk(&mut log.pool, ix.pool);
875    fill_pk(&mut log.user, ix.user);
876}
877
878#[inline]
879fn merge_pumpswap_trade_log_preferred(log: &mut PumpSwapTradeEvent, ix: PumpSwapTradeEvent) {
880    if log.ix_name.is_empty() && !ix.ix_name.is_empty() {
881        log.ix_name = ix.ix_name;
882    }
883}
884
885#[inline]
886fn merge_pumpswap_buy_log_preferred(log: &mut PumpSwapBuyEvent, ix: PumpSwapBuyEvent) {
887    fill_pk(&mut log.user_base_token_account, ix.user_base_token_account);
888    fill_pk(&mut log.user_quote_token_account, ix.user_quote_token_account);
889    fill_pk(&mut log.protocol_fee_recipient, ix.protocol_fee_recipient);
890    fill_pk(&mut log.protocol_fee_recipient_token_account, ix.protocol_fee_recipient_token_account);
891    fill_pk(&mut log.coin_creator, ix.coin_creator);
892    fill_pk(&mut log.base_mint, ix.base_mint);
893    fill_pk(&mut log.quote_mint, ix.quote_mint);
894    fill_pk(&mut log.pool_base_token_account, ix.pool_base_token_account);
895    fill_pk(&mut log.pool_quote_token_account, ix.pool_quote_token_account);
896    fill_pk(&mut log.coin_creator_vault_ata, ix.coin_creator_vault_ata);
897    fill_pk(&mut log.coin_creator_vault_authority, ix.coin_creator_vault_authority);
898    fill_pk(&mut log.base_token_program, ix.base_token_program);
899    fill_pk(&mut log.quote_token_program, ix.quote_token_program);
900    fill_pk(&mut log.pool_v2, ix.pool_v2);
901    fill_pk(&mut log.fee_recipient, ix.fee_recipient);
902    fill_pk(&mut log.fee_recipient_quote_token_account, ix.fee_recipient_quote_token_account);
903    if log.ix_name.is_empty() && !ix.ix_name.is_empty() {
904        log.ix_name = ix.ix_name;
905    }
906}
907
908#[inline]
909fn merge_pumpswap_sell_log_preferred(log: &mut PumpSwapSellEvent, ix: PumpSwapSellEvent) {
910    fill_pk(&mut log.user_base_token_account, ix.user_base_token_account);
911    fill_pk(&mut log.user_quote_token_account, ix.user_quote_token_account);
912    fill_pk(&mut log.protocol_fee_recipient, ix.protocol_fee_recipient);
913    fill_pk(&mut log.protocol_fee_recipient_token_account, ix.protocol_fee_recipient_token_account);
914    fill_pk(&mut log.coin_creator, ix.coin_creator);
915    fill_pk(&mut log.base_mint, ix.base_mint);
916    fill_pk(&mut log.quote_mint, ix.quote_mint);
917    fill_pk(&mut log.pool_base_token_account, ix.pool_base_token_account);
918    fill_pk(&mut log.pool_quote_token_account, ix.pool_quote_token_account);
919    fill_pk(&mut log.coin_creator_vault_ata, ix.coin_creator_vault_ata);
920    fill_pk(&mut log.coin_creator_vault_authority, ix.coin_creator_vault_authority);
921    fill_pk(&mut log.base_token_program, ix.base_token_program);
922    fill_pk(&mut log.quote_token_program, ix.quote_token_program);
923    fill_pk(&mut log.pool_v2, ix.pool_v2);
924    fill_pk(&mut log.fee_recipient, ix.fee_recipient);
925    fill_pk(&mut log.fee_recipient_quote_token_account, ix.fee_recipient_quote_token_account);
926}
927
928#[inline]
929fn merge_raydium_clmm_swap_log_preferred(log: &mut RaydiumClmmSwapEvent, ix: RaydiumClmmSwapEvent) {
930    if !ix.ix_name.is_empty() {
931        log.ix_name = ix.ix_name;
932        log.amount = ix.amount;
933        log.other_amount_threshold = ix.other_amount_threshold;
934        log.sqrt_price_limit_x64 = ix.sqrt_price_limit_x64;
935        log.is_base_input = ix.is_base_input;
936    }
937    fill_pk(&mut log.token_account_0, ix.token_account_0);
938    fill_pk(&mut log.token_account_1, ix.token_account_1);
939    fill_pk(&mut log.input_token_account, ix.input_token_account);
940    fill_pk(&mut log.output_token_account, ix.output_token_account);
941    fill_pk(&mut log.sender, ix.sender);
942    fill_pk(&mut log.amm_config, ix.amm_config);
943    fill_pk(&mut log.input_vault, ix.input_vault);
944    fill_pk(&mut log.output_vault, ix.output_vault);
945    fill_pk(&mut log.observation_state, ix.observation_state);
946    fill_pk(&mut log.input_mint, ix.input_mint);
947    fill_pk(&mut log.output_mint, ix.output_mint);
948    if log.tick_array_bitmap_extension.is_none() {
949        log.tick_array_bitmap_extension = ix.tick_array_bitmap_extension;
950    }
951    if log.tick_arrays.is_empty() && !ix.tick_arrays.is_empty() {
952        log.tick_arrays = ix.tick_arrays;
953    }
954}
955
956#[inline]
957fn merge_raydium_amm_v4_swap_log_preferred(
958    log: &mut RaydiumAmmV4SwapEvent,
959    ix: RaydiumAmmV4SwapEvent,
960) {
961    if !ix.ix_name.is_empty() {
962        log.ix_name = ix.ix_name;
963        log.instruction_amount_in = ix.instruction_amount_in;
964        log.instruction_amount_out = ix.instruction_amount_out;
965        log.minimum_amount_out = ix.minimum_amount_out;
966        log.max_amount_in = ix.max_amount_in;
967    }
968    fill_pk(&mut log.token_program, ix.token_program);
969    fill_pk(&mut log.amm_authority, ix.amm_authority);
970    fill_pk(&mut log.amm_open_orders, ix.amm_open_orders);
971    if let Some(ref o) = ix.amm_target_orders {
972        if log.amm_target_orders.is_none() {
973            log.amm_target_orders = Some(*o);
974        }
975    }
976    fill_pk(&mut log.pool_coin_token_account, ix.pool_coin_token_account);
977    fill_pk(&mut log.pool_pc_token_account, ix.pool_pc_token_account);
978    fill_pk(&mut log.serum_program, ix.serum_program);
979    fill_pk(&mut log.serum_market, ix.serum_market);
980    fill_pk(&mut log.serum_bids, ix.serum_bids);
981    fill_pk(&mut log.serum_asks, ix.serum_asks);
982    fill_pk(&mut log.serum_event_queue, ix.serum_event_queue);
983    fill_pk(&mut log.serum_coin_vault_account, ix.serum_coin_vault_account);
984    fill_pk(&mut log.serum_pc_vault_account, ix.serum_pc_vault_account);
985    fill_pk(&mut log.serum_vault_signer, ix.serum_vault_signer);
986    fill_pk(&mut log.user_source_token_account, ix.user_source_token_account);
987    fill_pk(&mut log.user_destination_token_account, ix.user_destination_token_account);
988    fill_pk(&mut log.user_source_owner, ix.user_source_owner);
989    fill_pk(&mut log.amm, ix.amm);
990}
991
992#[inline]
993fn merge_pumpswap_create_pool_log_preferred(
994    log: &mut PumpSwapCreatePoolEvent,
995    ix: PumpSwapCreatePoolEvent,
996) {
997    fill_pk(&mut log.creator, ix.creator);
998    fill_pk(&mut log.pool, ix.pool);
999    fill_pk(&mut log.lp_mint, ix.lp_mint);
1000    fill_pk(&mut log.user_base_token_account, ix.user_base_token_account);
1001    fill_pk(&mut log.user_quote_token_account, ix.user_quote_token_account);
1002    fill_pk(&mut log.coin_creator, ix.coin_creator);
1003    log.is_mayhem_mode |= ix.is_mayhem_mode;
1004    log.is_cashback_coin |= ix.is_cashback_coin;
1005    if log.creator_fee_bps == 0 {
1006        log.creator_fee_bps = ix.creator_fee_bps;
1007    }
1008    log.can_edit_creator_fee |= ix.can_edit_creator_fee;
1009    log.is_holder_reward |= ix.is_holder_reward;
1010}
1011
1012#[inline]
1013fn merge_pumpswap_liquidity_added_log_preferred(
1014    log: &mut PumpSwapLiquidityAdded,
1015    ix: PumpSwapLiquidityAdded,
1016) {
1017    fill_pk(&mut log.user_base_token_account, ix.user_base_token_account);
1018    fill_pk(&mut log.user_quote_token_account, ix.user_quote_token_account);
1019    fill_pk(&mut log.user_pool_token_account, ix.user_pool_token_account);
1020}
1021
1022#[inline]
1023fn merge_pumpswap_liquidity_removed_log_preferred(
1024    log: &mut PumpSwapLiquidityRemoved,
1025    ix: PumpSwapLiquidityRemoved,
1026) {
1027    fill_pk(&mut log.user_base_token_account, ix.user_base_token_account);
1028    fill_pk(&mut log.user_quote_token_account, ix.user_quote_token_account);
1029    fill_pk(&mut log.user_pool_token_account, ix.user_pool_token_account);
1030}
1031
1032#[inline]
1033fn merge_raydium_launchlab_pool_create_log_preferred(
1034    log: &mut RaydiumLaunchlabPoolCreateEvent,
1035    ix: RaydiumLaunchlabPoolCreateEvent,
1036) {
1037    fill_pk(&mut log.payer, ix.payer);
1038    fill_pk(&mut log.creator, ix.creator);
1039    fill_pk(&mut log.global_config, ix.global_config);
1040    fill_pk(&mut log.platform_config, ix.platform_config);
1041    fill_pk(&mut log.base_mint, ix.base_mint);
1042    fill_pk(&mut log.quote_mint, ix.quote_mint);
1043    fill_pk(&mut log.base_vault, ix.base_vault);
1044    fill_pk(&mut log.quote_vault, ix.quote_vault);
1045    fill_pk(&mut log.base_token_program, ix.base_token_program);
1046    fill_pk(&mut log.quote_token_program, ix.quote_token_program);
1047    fill_str_if_empty(&mut log.base_mint_param.name, &ix.base_mint_param.name);
1048    fill_str_if_empty(&mut log.base_mint_param.symbol, &ix.base_mint_param.symbol);
1049    fill_str_if_empty(&mut log.base_mint_param.uri, &ix.base_mint_param.uri);
1050}
1051
1052#[inline]
1053fn merge_raydium_launchlab_migrate_amm_log_preferred(
1054    log: &mut RaydiumLaunchlabMigrateAmmEvent,
1055    ix: RaydiumLaunchlabMigrateAmmEvent,
1056) {
1057    fill_pk(&mut log.old_pool, ix.old_pool);
1058    fill_pk(&mut log.new_pool, ix.new_pool);
1059    fill_pk(&mut log.user, ix.user);
1060}
1061
1062#[inline]
1063fn merge_raydium_launchlab_trade_log_preferred(
1064    log: &mut RaydiumLaunchlabTradeEvent,
1065    ix: RaydiumLaunchlabTradeEvent,
1066) {
1067    fill_pk(&mut log.user, ix.user);
1068    fill_pk(&mut log.global_config, ix.global_config);
1069    fill_pk(&mut log.platform_config, ix.platform_config);
1070    fill_pk(&mut log.user_base_token, ix.user_base_token);
1071    fill_pk(&mut log.user_quote_token, ix.user_quote_token);
1072    fill_pk(&mut log.base_vault, ix.base_vault);
1073    fill_pk(&mut log.quote_vault, ix.quote_vault);
1074    fill_pk(&mut log.base_mint, ix.base_mint);
1075    fill_pk(&mut log.quote_mint, ix.quote_mint);
1076    fill_pk(&mut log.base_token_program, ix.base_token_program);
1077    fill_pk(&mut log.quote_token_program, ix.quote_token_program);
1078    fill_pk(&mut log.system_program, ix.system_program);
1079    fill_pk(&mut log.platform_associated_account, ix.platform_associated_account);
1080    fill_pk(&mut log.creator_associated_account, ix.creator_associated_account);
1081}
1082
1083#[inline]
1084fn merge_meteora_dlmm_swap_log_preferred(log: &mut MeteoraDlmmSwapEvent, ix: MeteoraDlmmSwapEvent) {
1085    fill_pk(&mut log.user_token_in, ix.user_token_in);
1086    fill_pk(&mut log.user_token_out, ix.user_token_out);
1087    if log.min_amount_out == 0 {
1088        log.min_amount_out = ix.min_amount_out;
1089    }
1090}
1091
1092/// 将 **instruction 路径**解析结果合并进 **log 路径**事件:`log` 保留链上日志权威数值,
1093/// `ix` 仅填补 `log` 中为默认值的账户等字段。**不替换** `log` 外层枚举变体。
1094///
1095/// 已覆盖与 [`crate::grpc::log_instr_dedup`] 去重键一致的主要类型:PumpFun 全系、PumpSwap
1096///(Trade/Buy/Sell/CreatePool/加减流动性)、RaydiumLaunchlab(Trade/PoolCreate/Migrate)、Raydium CLMM/AMM V4 Swap、Meteora DLMM Swap。
1097fn merge_meteora_pools_add_context(
1098    log: &mut MeteoraPoolsAddLiquidityEvent,
1099    ix: MeteoraPoolsAddLiquidityEvent,
1100) {
1101    if !ix.ix_name.is_empty() {
1102        log.ix_name = ix.ix_name;
1103        log.pool_token_amount = ix.pool_token_amount;
1104        log.maximum_token_a_amount = ix.maximum_token_a_amount;
1105        log.maximum_token_b_amount = ix.maximum_token_b_amount;
1106        log.minimum_pool_token_amount = ix.minimum_pool_token_amount;
1107        log.token_a_in_amount = ix.token_a_in_amount;
1108        log.token_b_in_amount = ix.token_b_in_amount;
1109    }
1110    fill_pk(&mut log.pool, ix.pool);
1111    fill_pk(&mut log.lp_mint, ix.lp_mint);
1112    fill_pk(&mut log.user_pool_lp, ix.user_pool_lp);
1113    fill_pk(&mut log.a_vault_lp, ix.a_vault_lp);
1114    fill_pk(&mut log.b_vault_lp, ix.b_vault_lp);
1115    fill_pk(&mut log.a_vault, ix.a_vault);
1116    fill_pk(&mut log.b_vault, ix.b_vault);
1117    fill_pk(&mut log.a_vault_lp_mint, ix.a_vault_lp_mint);
1118    fill_pk(&mut log.b_vault_lp_mint, ix.b_vault_lp_mint);
1119    fill_pk(&mut log.a_token_vault, ix.a_token_vault);
1120    fill_pk(&mut log.b_token_vault, ix.b_token_vault);
1121    fill_pk(&mut log.user_a_token, ix.user_a_token);
1122    fill_pk(&mut log.user_b_token, ix.user_b_token);
1123    fill_pk(&mut log.user, ix.user);
1124    fill_pk(&mut log.vault_program, ix.vault_program);
1125    fill_pk(&mut log.token_program, ix.token_program);
1126}
1127
1128fn merge_meteora_pools_bootstrap_context(
1129    log: &mut MeteoraPoolsBootstrapLiquidityEvent,
1130    ix: MeteoraPoolsBootstrapLiquidityEvent,
1131) {
1132    if !ix.ix_name.is_empty() {
1133        log.ix_name = ix.ix_name;
1134        log.token_a_in_amount = ix.token_a_in_amount;
1135        log.token_b_in_amount = ix.token_b_in_amount;
1136    }
1137    fill_pk(&mut log.pool, ix.pool);
1138    fill_pk(&mut log.lp_mint, ix.lp_mint);
1139    fill_pk(&mut log.user_pool_lp, ix.user_pool_lp);
1140    fill_pk(&mut log.a_vault_lp, ix.a_vault_lp);
1141    fill_pk(&mut log.b_vault_lp, ix.b_vault_lp);
1142    fill_pk(&mut log.a_vault, ix.a_vault);
1143    fill_pk(&mut log.b_vault, ix.b_vault);
1144    fill_pk(&mut log.a_vault_lp_mint, ix.a_vault_lp_mint);
1145    fill_pk(&mut log.b_vault_lp_mint, ix.b_vault_lp_mint);
1146    fill_pk(&mut log.a_token_vault, ix.a_token_vault);
1147    fill_pk(&mut log.b_token_vault, ix.b_token_vault);
1148    fill_pk(&mut log.user_a_token, ix.user_a_token);
1149    fill_pk(&mut log.user_b_token, ix.user_b_token);
1150    fill_pk(&mut log.user, ix.user);
1151    fill_pk(&mut log.vault_program, ix.vault_program);
1152    fill_pk(&mut log.token_program, ix.token_program);
1153}
1154
1155fn merge_meteora_pools_poolcreated_context(
1156    log: &mut MeteoraPoolsPoolCreatedEvent,
1157    ix: MeteoraPoolsPoolCreatedEvent,
1158) {
1159    if !ix.ix_name.is_empty() {
1160        log.ix_name = ix.ix_name;
1161        log.token_a_in_amount = ix.token_a_in_amount;
1162        log.token_b_in_amount = ix.token_b_in_amount;
1163        log.activation_point = ix.activation_point;
1164        log.stable_curve = ix.stable_curve;
1165        log.trade_fee_bps = ix.trade_fee_bps;
1166        log.customizable_params = ix.customizable_params;
1167    }
1168    fill_pk(&mut log.pool, ix.pool);
1169    fill_pk(&mut log.config, ix.config);
1170    fill_pk(&mut log.lp_mint, ix.lp_mint);
1171    fill_pk(&mut log.token_a_mint, ix.token_a_mint);
1172    fill_pk(&mut log.token_b_mint, ix.token_b_mint);
1173    fill_pk(&mut log.a_vault, ix.a_vault);
1174    fill_pk(&mut log.b_vault, ix.b_vault);
1175    fill_pk(&mut log.a_token_vault, ix.a_token_vault);
1176    fill_pk(&mut log.b_token_vault, ix.b_token_vault);
1177    fill_pk(&mut log.a_vault_lp_mint, ix.a_vault_lp_mint);
1178    fill_pk(&mut log.b_vault_lp_mint, ix.b_vault_lp_mint);
1179    fill_pk(&mut log.a_vault_lp, ix.a_vault_lp);
1180    fill_pk(&mut log.b_vault_lp, ix.b_vault_lp);
1181    fill_pk(&mut log.payer_token_a, ix.payer_token_a);
1182    fill_pk(&mut log.payer_token_b, ix.payer_token_b);
1183    fill_pk(&mut log.payer_pool_lp, ix.payer_pool_lp);
1184    fill_pk(&mut log.protocol_token_a_fee, ix.protocol_token_a_fee);
1185    fill_pk(&mut log.protocol_token_b_fee, ix.protocol_token_b_fee);
1186    fill_pk(&mut log.payer, ix.payer);
1187    fill_pk(&mut log.rent, ix.rent);
1188    fill_pk(&mut log.mint_metadata, ix.mint_metadata);
1189    fill_pk(&mut log.metadata_program, ix.metadata_program);
1190    fill_pk(&mut log.vault_program, ix.vault_program);
1191    fill_pk(&mut log.token_program, ix.token_program);
1192    fill_pk(&mut log.associated_token_program, ix.associated_token_program);
1193    fill_pk(&mut log.system_program, ix.system_program);
1194    fill_pk(&mut log.admin_token_a, ix.admin_token_a);
1195    fill_pk(&mut log.admin_token_b, ix.admin_token_b);
1196    fill_pk(&mut log.admin_pool_lp, ix.admin_pool_lp);
1197    fill_pk(&mut log.admin, ix.admin);
1198    fill_pk(&mut log.fee_owner, ix.fee_owner);
1199}
1200
1201fn merge_meteora_pools_setpoolfees_context(
1202    log: &mut MeteoraPoolsSetPoolFeesEvent,
1203    ix: MeteoraPoolsSetPoolFeesEvent,
1204) {
1205    if !ix.ix_name.is_empty() {
1206        log.ix_name = ix.ix_name;
1207        log.new_partner_fee_numerator = ix.new_partner_fee_numerator;
1208    }
1209    fill_pk(&mut log.pool, ix.pool);
1210    fill_pk(&mut log.fee_operator, ix.fee_operator);
1211}
1212
1213fn merge_meteora_pools_remove_context(
1214    log: &mut MeteoraPoolsRemoveLiquidityEvent,
1215    ix: MeteoraPoolsRemoveLiquidityEvent,
1216) {
1217    if !ix.ix_name.is_empty() {
1218        log.ix_name = ix.ix_name;
1219        log.pool_token_amount = ix.pool_token_amount;
1220        log.minimum_out_amount = ix.minimum_out_amount;
1221        log.minimum_a_token_out = ix.minimum_a_token_out;
1222        log.minimum_b_token_out = ix.minimum_b_token_out;
1223    }
1224    fill_pk(&mut log.pool, ix.pool);
1225    fill_pk(&mut log.lp_mint, ix.lp_mint);
1226    fill_pk(&mut log.user_pool_lp, ix.user_pool_lp);
1227    fill_pk(&mut log.a_vault_lp, ix.a_vault_lp);
1228    fill_pk(&mut log.b_vault_lp, ix.b_vault_lp);
1229    fill_pk(&mut log.a_vault, ix.a_vault);
1230    fill_pk(&mut log.b_vault, ix.b_vault);
1231    fill_pk(&mut log.a_vault_lp_mint, ix.a_vault_lp_mint);
1232    fill_pk(&mut log.b_vault_lp_mint, ix.b_vault_lp_mint);
1233    fill_pk(&mut log.a_token_vault, ix.a_token_vault);
1234    fill_pk(&mut log.b_token_vault, ix.b_token_vault);
1235    fill_pk(&mut log.user_a_token, ix.user_a_token);
1236    fill_pk(&mut log.user_b_token, ix.user_b_token);
1237    fill_pk(&mut log.user, ix.user);
1238    fill_pk(&mut log.vault_program, ix.vault_program);
1239    fill_pk(&mut log.token_program, ix.token_program);
1240    fill_pk(&mut log.user_destination_token, ix.user_destination_token);
1241}
1242
1243fn merge_meteora_pools_swap_context(log: &mut MeteoraPoolsSwapEvent, ix: MeteoraPoolsSwapEvent) {
1244    if !ix.ix_name.is_empty() {
1245        log.ix_name = ix.ix_name;
1246        log.amount_in = ix.amount_in;
1247        log.minimum_out_amount = ix.minimum_out_amount;
1248    }
1249    fill_pk(&mut log.pool, ix.pool);
1250    fill_pk(&mut log.user_source_token, ix.user_source_token);
1251    fill_pk(&mut log.user_destination_token, ix.user_destination_token);
1252    fill_pk(&mut log.a_vault, ix.a_vault);
1253    fill_pk(&mut log.b_vault, ix.b_vault);
1254    fill_pk(&mut log.a_token_vault, ix.a_token_vault);
1255    fill_pk(&mut log.b_token_vault, ix.b_token_vault);
1256    fill_pk(&mut log.a_vault_lp_mint, ix.a_vault_lp_mint);
1257    fill_pk(&mut log.b_vault_lp_mint, ix.b_vault_lp_mint);
1258    fill_pk(&mut log.a_vault_lp, ix.a_vault_lp);
1259    fill_pk(&mut log.b_vault_lp, ix.b_vault_lp);
1260    fill_pk(&mut log.protocol_token_fee, ix.protocol_token_fee);
1261    fill_pk(&mut log.user, ix.user);
1262    fill_pk(&mut log.vault_program, ix.vault_program);
1263    fill_pk(&mut log.token_program, ix.token_program);
1264}
1265
1266fn merge_orca_swap_context(log: &mut OrcaWhirlpoolSwapEvent, ix: OrcaWhirlpoolSwapEvent) {
1267    if !ix.ix_name.is_empty() {
1268        log.ix_name = ix.ix_name;
1269        log.amount = ix.amount;
1270        log.other_amount_threshold = ix.other_amount_threshold;
1271        log.sqrt_price_limit = ix.sqrt_price_limit;
1272        log.amount_specified_is_input = ix.amount_specified_is_input;
1273    }
1274    fill_pk(&mut log.token_authority, ix.token_authority);
1275    fill_pk(&mut log.token_owner_account_a, ix.token_owner_account_a);
1276    fill_pk(&mut log.token_owner_account_b, ix.token_owner_account_b);
1277    fill_pk(&mut log.token_program_a, ix.token_program_a);
1278    fill_pk(&mut log.token_program_b, ix.token_program_b);
1279    fill_pk(&mut log.token_mint_a, ix.token_mint_a);
1280    fill_pk(&mut log.token_mint_b, ix.token_mint_b);
1281    fill_pk(&mut log.token_vault_a, ix.token_vault_a);
1282    fill_pk(&mut log.token_vault_b, ix.token_vault_b);
1283    fill_pk(&mut log.tick_array_0, ix.tick_array_0);
1284    fill_pk(&mut log.tick_array_1, ix.tick_array_1);
1285    fill_pk(&mut log.tick_array_2, ix.tick_array_2);
1286    fill_pk(&mut log.oracle, ix.oracle);
1287}
1288
1289pub fn merge_grpc_instruction_into_log(log: &mut DexEvent, ix: DexEvent) {
1290    use DexEvent::*;
1291    match log {
1292        PumpFunTrade(l) => {
1293            if let Some(i) = pumpfun_trade_from_ix_variant(ix) {
1294                merge_pumpfun_trade_log_preferred(l, i);
1295            }
1296        }
1297        PumpFunBuy(l) => {
1298            if let Some(i) = pumpfun_trade_from_ix_variant(ix) {
1299                merge_pumpfun_trade_log_preferred(l, i);
1300            }
1301        }
1302        PumpFunSell(l) => {
1303            if let Some(i) = pumpfun_trade_from_ix_variant(ix) {
1304                merge_pumpfun_trade_log_preferred(l, i);
1305            }
1306        }
1307        PumpFunBuyExactSolIn(l) => {
1308            if let Some(i) = pumpfun_trade_from_ix_variant(ix) {
1309                merge_pumpfun_trade_log_preferred(l, i);
1310            }
1311        }
1312        PumpFunCreate(l) => match ix {
1313            DexEvent::PumpFunCreate(i) => merge_pumpfun_create_log_preferred(l, i),
1314            DexEvent::PumpFunCreateV2(i) => merge_pumpfun_create_v2_into_create_log_preferred(l, i),
1315            _ => {}
1316        },
1317        PumpFunCreateV2(l) => match ix {
1318            DexEvent::PumpFunCreate(i) => merge_pumpfun_create_into_create_v2_log_preferred(l, i),
1319            DexEvent::PumpFunCreateV2(i) => merge_pumpfun_create_v2_log_preferred(l, i),
1320            _ => {}
1321        },
1322        PumpFunMigrate(l) => {
1323            if let DexEvent::PumpFunMigrate(i) = ix {
1324                merge_pumpfun_migrate_log_preferred(l, i);
1325            }
1326        }
1327        PumpSwapTrade(l) => {
1328            if let PumpSwapTrade(i) = ix {
1329                merge_pumpswap_trade_log_preferred(l, i);
1330            }
1331        }
1332        PumpSwapBuy(l) => {
1333            if let PumpSwapBuy(i) = ix {
1334                merge_pumpswap_buy_log_preferred(l, i);
1335            }
1336        }
1337        PumpSwapSell(l) => {
1338            if let PumpSwapSell(i) = ix {
1339                merge_pumpswap_sell_log_preferred(l, i);
1340            }
1341        }
1342        RaydiumCpmmSwap(l) => {
1343            if let RaydiumCpmmSwap(i) = ix {
1344                merge_raydium_cpmm_swap_log_preferred(l, i);
1345            }
1346        }
1347        MeteoraPoolsAddLiquidity(l) => {
1348            if let MeteoraPoolsAddLiquidity(i) = ix {
1349                merge_meteora_pools_add_context(l, i);
1350            }
1351        }
1352        MeteoraPoolsRemoveLiquidity(l) => {
1353            if let MeteoraPoolsRemoveLiquidity(i) = ix {
1354                merge_meteora_pools_remove_context(l, i);
1355            }
1356        }
1357        MeteoraPoolsBootstrapLiquidity(l) => {
1358            if let MeteoraPoolsBootstrapLiquidity(i) = ix {
1359                merge_meteora_pools_bootstrap_context(l, i);
1360            }
1361        }
1362        MeteoraPoolsPoolCreated(l) => {
1363            if let MeteoraPoolsPoolCreated(i) = ix {
1364                merge_meteora_pools_poolcreated_context(l, i);
1365            }
1366        }
1367        MeteoraPoolsSetPoolFees(l) => {
1368            if let MeteoraPoolsSetPoolFees(i) = ix {
1369                merge_meteora_pools_setpoolfees_context(l, i);
1370            }
1371        }
1372        MeteoraPoolsSwap(l) => {
1373            if let MeteoraPoolsSwap(i) = ix {
1374                merge_meteora_pools_swap_context(l, i);
1375            }
1376        }
1377        OrcaWhirlpoolSwap(l) => {
1378            if let OrcaWhirlpoolSwap(i) = ix {
1379                merge_orca_swap_context(l, i);
1380            }
1381        }
1382        RaydiumClmmIncreaseLiquidity(l) => {
1383            if let RaydiumClmmIncreaseLiquidity(i) = ix {
1384                fill_pk(&mut l.pool, i.pool);
1385                fill_pk(&mut l.user, i.user);
1386                fill_pk(&mut l.personal_position, i.personal_position);
1387                l.amount0_max = i.amount0_max;
1388                l.amount1_max = i.amount1_max;
1389            }
1390        }
1391        RaydiumClmmDecreaseLiquidity(l) => {
1392            if let RaydiumClmmDecreaseLiquidity(i) = ix {
1393                fill_pk(&mut l.pool, i.pool);
1394                fill_pk(&mut l.user, i.user);
1395                fill_pk(&mut l.personal_position, i.personal_position);
1396                l.amount0_min = i.amount0_min;
1397                l.amount1_min = i.amount1_min;
1398            }
1399        }
1400        RaydiumClmmSwap(l) => {
1401            if let RaydiumClmmSwap(i) = ix {
1402                merge_raydium_clmm_swap_log_preferred(l, i);
1403            }
1404        }
1405        RaydiumAmmV4Swap(l) => {
1406            if let RaydiumAmmV4Swap(i) = ix {
1407                merge_raydium_amm_v4_swap_log_preferred(l, i);
1408            }
1409        }
1410        RaydiumLaunchlabTrade(l) => {
1411            if let RaydiumLaunchlabTrade(i) = ix {
1412                merge_raydium_launchlab_trade_log_preferred(l, i);
1413            }
1414        }
1415        RaydiumLaunchlabPoolCreate(l) => {
1416            if let RaydiumLaunchlabPoolCreate(i) = ix {
1417                merge_raydium_launchlab_pool_create_log_preferred(l, i);
1418            }
1419        }
1420        RaydiumLaunchlabMigrateAmm(l) => {
1421            if let RaydiumLaunchlabMigrateAmm(i) = ix {
1422                merge_raydium_launchlab_migrate_amm_log_preferred(l, i);
1423            }
1424        }
1425        PumpSwapCreatePool(l) => {
1426            if let PumpSwapCreatePool(i) = ix {
1427                merge_pumpswap_create_pool_log_preferred(l, i);
1428            }
1429        }
1430        PumpSwapLiquidityAdded(l) => {
1431            if let PumpSwapLiquidityAdded(i) = ix {
1432                merge_pumpswap_liquidity_added_log_preferred(l, i);
1433            }
1434        }
1435        PumpSwapLiquidityRemoved(l) => {
1436            if let PumpSwapLiquidityRemoved(i) = ix {
1437                merge_pumpswap_liquidity_removed_log_preferred(l, i);
1438            }
1439        }
1440        MeteoraDlmmSwap(l) => {
1441            if let MeteoraDlmmSwap(i) = ix {
1442                merge_meteora_dlmm_swap_log_preferred(l, i);
1443            }
1444        }
1445        _ => {}
1446    }
1447}
1448
1449#[inline]
1450fn pumpfun_trade_from_ix_variant(ix: DexEvent) -> Option<PumpFunTradeEvent> {
1451    match ix {
1452        DexEvent::PumpFunTrade(t)
1453        | DexEvent::PumpFunBuy(t)
1454        | DexEvent::PumpFunSell(t)
1455        | DexEvent::PumpFunBuyExactSolIn(t) => Some(t),
1456        _ => None,
1457    }
1458}
1459
1460#[cfg(test)]
1461mod tests {
1462
1463    #[test]
1464    fn clmm_merge_retains_limits_and_mode_without_overwriting_executed_price_or_direction() {
1465        use crate::core::events::{DexEvent, RaydiumClmmSwapEvent};
1466        for input_mode in [false, true] {
1467            let instruction = RaydiumClmmSwapEvent {
1468                ix_name: "swap_v2".to_string(),
1469                amount: 0,
1470                other_amount_threshold: u64::MAX,
1471                sqrt_price_limit_x64: 0,
1472                is_base_input: input_mode,
1473                input_mint: solana_sdk::pubkey::Pubkey::new_unique(),
1474                tick_arrays: vec![solana_sdk::pubkey::Pubkey::new_unique()],
1475                ..Default::default()
1476            };
1477            let log = RaydiumClmmSwapEvent {
1478                amount_0: 456,
1479                amount_1: 123,
1480                sqrt_price_x64: 789,
1481                zero_for_one: !input_mode,
1482                tick: -7,
1483                liquidity: 1011,
1484                amount: 99,
1485                sqrt_price_limit_x64: 99,
1486                ..Default::default()
1487            };
1488            for log_first in [false, true] {
1489                let mut merged = DexEvent::RaydiumClmmSwap(if log_first {
1490                    log.clone()
1491                } else {
1492                    instruction.clone()
1493                });
1494                if log_first {
1495                    super::merge_grpc_instruction_into_log(
1496                        &mut merged,
1497                        DexEvent::RaydiumClmmSwap(instruction.clone()),
1498                    );
1499                } else {
1500                    super::merge_events(&mut merged, DexEvent::RaydiumClmmSwap(log.clone()));
1501                }
1502                let DexEvent::RaydiumClmmSwap(e) = merged else { panic!("swap") };
1503                assert_eq!(
1504                    (e.amount_0, e.amount_1, e.sqrt_price_x64, e.tick, e.liquidity),
1505                    (456, 123, 789, -7, 1011)
1506                );
1507                assert_eq!(e.zero_for_one, !input_mode);
1508                assert_eq!(
1509                    (e.amount, e.other_amount_threshold, e.sqrt_price_limit_x64),
1510                    (0, u64::MAX, 0)
1511                );
1512                assert_eq!(e.is_base_input, input_mode);
1513                assert_eq!(e.ix_name, "swap_v2");
1514                assert_eq!(e.input_mint, instruction.input_mint);
1515                assert_eq!(e.tick_arrays, instruction.tick_arrays);
1516            }
1517        }
1518    }
1519
1520    #[test]
1521    fn cpmm_merge_preserves_zero_limits_and_execution_values_in_both_paths() {
1522        use crate::core::events::{DexEvent, RaydiumCpmmSwapEvent};
1523        for exact_input in [false, true] {
1524            let payer = solana_sdk::pubkey::Pubkey::new_unique();
1525            let input_account = solana_sdk::pubkey::Pubkey::new_unique();
1526            let instruction = RaydiumCpmmSwapEvent {
1527                ix_name: if exact_input { "swap_base_input" } else { "swap_base_output" }
1528                    .to_string(),
1529                base_input: exact_input,
1530                amount_in: 0,
1531                minimum_amount_out: if exact_input { u64::MAX } else { 0 },
1532                max_amount_in: if exact_input { 0 } else { u64::MAX },
1533                amount_out: 0,
1534                payer,
1535                input_token_account: input_account,
1536                ..Default::default()
1537            };
1538            let log = RaydiumCpmmSwapEvent {
1539                base_input: exact_input,
1540                input_amount: 456,
1541                output_amount: 123,
1542                input_transfer_fee: 7,
1543                amount_in: 99,
1544                amount_out: 99,
1545                ..Default::default()
1546            };
1547            for log_first in [false, true] {
1548                let mut merged = if log_first {
1549                    DexEvent::RaydiumCpmmSwap(log.clone())
1550                } else {
1551                    DexEvent::RaydiumCpmmSwap(instruction.clone())
1552                };
1553                if log_first {
1554                    super::merge_grpc_instruction_into_log(
1555                        &mut merged,
1556                        DexEvent::RaydiumCpmmSwap(instruction.clone()),
1557                    );
1558                } else {
1559                    super::merge_events(&mut merged, DexEvent::RaydiumCpmmSwap(log.clone()));
1560                }
1561                let DexEvent::RaydiumCpmmSwap(e) = merged else { panic!("swap") };
1562                assert_eq!((e.input_amount, e.output_amount, e.input_transfer_fee), (456, 123, 7));
1563                assert_eq!(e.ix_name, instruction.ix_name);
1564                assert_eq!(
1565                    (e.amount_in, e.minimum_amount_out, e.max_amount_in, e.amount_out),
1566                    (
1567                        instruction.amount_in,
1568                        instruction.minimum_amount_out,
1569                        instruction.max_amount_in,
1570                        0
1571                    )
1572                );
1573                assert_eq!(e.payer, payer);
1574                assert_eq!(e.input_token_account, input_account);
1575                assert_eq!(e.base_input, exact_input);
1576            }
1577        }
1578    }
1579    use super::*;
1580
1581    #[test]
1582    fn incompatible_merge_leaves_both_events_unchanged() {
1583        let mut base = DexEvent::Error("base".to_string());
1584        let inner = DexEvent::Error("inner".to_string());
1585        let mut unmerged = None;
1586
1587        assert!(!try_merge_events(&mut base, inner, &mut unmerged));
1588        assert!(matches!(base, DexEvent::Error(ref message) if message == "base"));
1589        assert!(matches!(unmerged, Some(DexEvent::Error(ref message)) if message == "inner"));
1590    }
1591    use solana_sdk::{pubkey::Pubkey, signature::Signature};
1592
1593    fn dlmm_swap(min_amount_out: u64, amount_out: u64) -> MeteoraDlmmSwapEvent {
1594        MeteoraDlmmSwapEvent {
1595            metadata: EventMetadata::default(),
1596            token_x_mint: Pubkey::default(),
1597            token_y_mint: Pubkey::default(),
1598            user_token_in: Pubkey::new_unique(),
1599            user_token_out: Pubkey::new_unique(),
1600            min_amount_out,
1601            pool: Pubkey::new_unique(),
1602            from: Pubkey::new_unique(),
1603            start_bin_id: 0,
1604            end_bin_id: 0,
1605            amount_in: 200,
1606            amount_out,
1607            swap_for_y: true,
1608            fee: 1,
1609            protocol_fee: 0,
1610            fee_bps: 25,
1611            host_fee: 0,
1612            ..Default::default()
1613        }
1614    }
1615
1616    #[test]
1617    fn dlmm_event_merge_keeps_instruction_threshold_and_executed_output() {
1618        let base_event = dlmm_swap(100, 0);
1619        let expected_user_token_in = base_event.user_token_in;
1620        let expected_user_token_out = base_event.user_token_out;
1621        let mut inner_event = dlmm_swap(0, 125);
1622        inner_event.user_token_in = Pubkey::default();
1623        inner_event.user_token_out = Pubkey::default();
1624        let mut base = DexEvent::MeteoraDlmmSwap(base_event);
1625        let inner = DexEvent::MeteoraDlmmSwap(inner_event);
1626
1627        assert!(try_merge_events(&mut base, inner, &mut None));
1628        let DexEvent::MeteoraDlmmSwap(event) = base else { panic!("swap") };
1629        assert_eq!(event.min_amount_out, 100);
1630        assert_eq!(event.amount_out, 125);
1631        assert_eq!(event.user_token_in, expected_user_token_in);
1632        assert_eq!(event.user_token_out, expected_user_token_out);
1633    }
1634
1635    #[test]
1636    fn grpc_dlmm_merge_keeps_log_output_and_adds_instruction_threshold() {
1637        let mut log_event = dlmm_swap(0, 125);
1638        log_event.user_token_in = Pubkey::default();
1639        log_event.user_token_out = Pubkey::default();
1640        let instruction_event = dlmm_swap(100, 0);
1641        let expected_user_token_in = instruction_event.user_token_in;
1642        let expected_user_token_out = instruction_event.user_token_out;
1643        let mut log = DexEvent::MeteoraDlmmSwap(log_event);
1644        let instruction = DexEvent::MeteoraDlmmSwap(instruction_event);
1645
1646        merge_grpc_instruction_into_log(&mut log, instruction);
1647        let DexEvent::MeteoraDlmmSwap(event) = log else { panic!("swap") };
1648        assert_eq!(event.min_amount_out, 100);
1649        assert_eq!(event.amount_out, 125);
1650        assert_eq!(event.user_token_in, expected_user_token_in);
1651        assert_eq!(event.user_token_out, expected_user_token_out);
1652    }
1653
1654    #[test]
1655    fn test_merge_pumpfun_trade() {
1656        let metadata = EventMetadata {
1657            signature: Signature::default(),
1658            slot: 100,
1659            tx_index: 1,
1660            block_time_us: 1000,
1661            grpc_recv_us: 2000,
1662            recent_blockhash: None,
1663        };
1664
1665        // Base event 来自 instruction(包含账户上下文)
1666        let mut base = DexEvent::PumpFunTrade(PumpFunTradeEvent {
1667            metadata: metadata.clone(),
1668            bonding_curve: Pubkey::new_unique(),
1669            associated_bonding_curve: Pubkey::new_unique(),
1670            ..Default::default()
1671        });
1672
1673        // Inner event 来自 inner instruction(包含交易数据)
1674        let inner = DexEvent::PumpFunTrade(PumpFunTradeEvent {
1675            metadata: metadata.clone(),
1676            mint: Pubkey::new_unique(),
1677            sol_amount: 1000,
1678            token_amount: 2000,
1679            is_buy: true,
1680            user: Pubkey::new_unique(),
1681            holder_rewards_bps: 300,
1682            holder_rewards: 400,
1683            ..Default::default()
1684        });
1685
1686        // 合并
1687        merge_events(&mut base, inner);
1688
1689        // 验证合并结果
1690        if let DexEvent::PumpFunTrade(trade) = base {
1691            assert_eq!(trade.sol_amount, 1000);
1692            assert_eq!(trade.token_amount, 2000);
1693            assert!(trade.is_buy);
1694            assert_eq!(trade.holder_rewards_bps, 300);
1695            assert_eq!(trade.holder_rewards, 400);
1696            // 账户上下文保留
1697            assert_ne!(trade.bonding_curve, Pubkey::default());
1698            assert_ne!(trade.associated_bonding_curve, Pubkey::default());
1699        } else {
1700            panic!("Expected PumpFunTrade event");
1701        }
1702    }
1703
1704    #[test]
1705    fn merge_pumpfun_trade_non_leg_propagates_holder_rewards() {
1706        let mut base = DexEvent::PumpFunTrade(PumpFunTradeEvent::default());
1707        let inner = DexEvent::PumpFunTrade(PumpFunTradeEvent {
1708            holder_rewards_bps: 300,
1709            holder_rewards: 400,
1710            ..Default::default()
1711        });
1712
1713        merge_events(&mut base, inner);
1714
1715        let DexEvent::PumpFunTrade(trade) = base else {
1716            panic!("expected PumpFunTrade event");
1717        };
1718        assert_eq!(trade.holder_rewards_bps, 300);
1719        assert_eq!(trade.holder_rewards, 400);
1720    }
1721
1722    #[test]
1723    fn merge_preserves_instruction_context_when_log_tail_is_absent() {
1724        let metadata = EventMetadata {
1725            signature: Signature::default(),
1726            slot: 100,
1727            tx_index: 1,
1728            block_time_us: 1000,
1729            grpc_recv_us: 2000,
1730            recent_blockhash: None,
1731        };
1732        let quote_mint = Pubkey::new_unique();
1733        let associated_quote_user = Pubkey::new_unique();
1734
1735        let mut base = DexEvent::PumpFunTrade(PumpFunTradeEvent {
1736            metadata: metadata.clone(),
1737            ix_name: "buy_exact_quote_in".to_string(),
1738            is_buy: true,
1739            quote_mint,
1740            spendable_quote_in: 1_000,
1741            min_tokens_out: 2_000,
1742            associated_quote_user,
1743            ..Default::default()
1744        });
1745
1746        let inner = DexEvent::PumpFunBuy(PumpFunTradeEvent {
1747            metadata,
1748            sol_amount: 1_000,
1749            token_amount: 2_000,
1750            is_buy: true,
1751            ..Default::default()
1752        });
1753
1754        merge_events(&mut base, inner);
1755
1756        if let DexEvent::PumpFunTrade(t) = base {
1757            assert_eq!(t.sol_amount, 1_000);
1758            assert_eq!(t.token_amount, 2_000);
1759            assert_eq!(t.ix_name, "buy_exact_quote_in");
1760            assert_eq!(t.quote_mint, quote_mint);
1761            assert_eq!(t.spendable_quote_in, 1_000);
1762            assert_eq!(t.min_tokens_out, 2_000);
1763            assert_eq!(t.associated_quote_user, associated_quote_user);
1764        } else {
1765            panic!("Expected PumpFunTrade event");
1766        }
1767    }
1768
1769    #[test]
1770    fn merge_replaces_sol_quote_sentinel_with_real_quote_mint() {
1771        let quote_mint = Pubkey::new_unique();
1772        let mut base = DexEvent::PumpFunTrade(PumpFunTradeEvent {
1773            quote_mint: PUMPFUN_SOLSCAN_SOL_QUOTE_MINT,
1774            ..Default::default()
1775        });
1776
1777        let inner = DexEvent::PumpFunBuy(PumpFunTradeEvent { quote_mint, ..Default::default() });
1778
1779        merge_events(&mut base, inner);
1780
1781        if let DexEvent::PumpFunTrade(t) = base {
1782            assert_eq!(t.quote_mint, quote_mint);
1783        } else {
1784            panic!("Expected PumpFunTrade event");
1785        }
1786    }
1787
1788    #[test]
1789    fn merge_replaces_sol_quote_sentinel_with_wsol_mint() {
1790        let mut base = DexEvent::PumpFunTrade(PumpFunTradeEvent {
1791            quote_mint: PUMPFUN_SOLSCAN_SOL_QUOTE_MINT,
1792            ..Default::default()
1793        });
1794
1795        let inner = DexEvent::PumpFunBuy(PumpFunTradeEvent {
1796            quote_mint: PUMPFUN_WSOL_QUOTE_MINT,
1797            ..Default::default()
1798        });
1799
1800        merge_events(&mut base, inner);
1801
1802        if let DexEvent::PumpFunTrade(t) = base {
1803            assert_eq!(t.quote_mint, PUMPFUN_WSOL_QUOTE_MINT);
1804        } else {
1805            panic!("Expected PumpFunTrade event");
1806        }
1807    }
1808
1809    #[test]
1810    fn test_can_merge() {
1811        let metadata = EventMetadata {
1812            signature: Signature::default(),
1813            slot: 100,
1814            tx_index: 1,
1815            block_time_us: 1000,
1816            grpc_recv_us: 2000,
1817            recent_blockhash: None,
1818        };
1819
1820        let base = DexEvent::PumpFunTrade(PumpFunTradeEvent {
1821            metadata: metadata.clone(),
1822            ..Default::default()
1823        });
1824
1825        let inner = DexEvent::PumpFunBuy(PumpFunTradeEvent {
1826            metadata: metadata.clone(),
1827            ..Default::default()
1828        });
1829
1830        // 应该可以合并(同一个 signature,兼容类型)
1831        assert!(can_merge(&base, &inner));
1832
1833        // 不同 signature 不能合并
1834        let different_sig = DexEvent::PumpFunTrade(PumpFunTradeEvent {
1835            metadata: EventMetadata { signature: Signature::new_unique(), ..metadata },
1836            ..Default::default()
1837        });
1838
1839        assert!(!can_merge(&base, &different_sig));
1840    }
1841
1842    #[test]
1843    fn dlmm_position_event_keeps_instruction_only_fields() {
1844        let pool = Pubkey::new_unique();
1845        let position = Pubkey::new_unique();
1846        let owner = Pubkey::new_unique();
1847        let mut base = DexEvent::MeteoraDlmmCreatePosition(MeteoraDlmmCreatePositionEvent {
1848            metadata: EventMetadata::default(),
1849            pool,
1850            position,
1851            owner,
1852            lower_bin_id: -42,
1853            width: 70,
1854        });
1855        let inner = DexEvent::MeteoraDlmmCreatePosition(MeteoraDlmmCreatePositionEvent {
1856            metadata: EventMetadata::default(),
1857            pool,
1858            position,
1859            owner,
1860            lower_bin_id: 0,
1861            width: 0,
1862        });
1863
1864        assert!(try_merge_events(&mut base, inner, &mut None));
1865        let DexEvent::MeteoraDlmmCreatePosition(event) = base else { panic!("position") };
1866        assert_eq!(event.lower_bin_id, -42);
1867        assert_eq!(event.width, 70);
1868    }
1869
1870    #[test]
1871    fn grpc_merge_fills_fee_recipient_from_ix_when_log_default() {
1872        let metadata = EventMetadata {
1873            signature: Signature::default(),
1874            slot: 1,
1875            tx_index: 0,
1876            block_time_us: 0,
1877            grpc_recv_us: 0,
1878            recent_blockhash: None,
1879        };
1880        let fr = Pubkey::new_unique();
1881        let log_t =
1882            PumpFunTradeEvent { metadata: metadata.clone(), sol_amount: 50, ..Default::default() };
1883        let mut ix_t = log_t.clone();
1884        ix_t.fee_recipient = fr;
1885        ix_t.sol_amount = 777;
1886        let mut log_ev = DexEvent::PumpFunTrade(log_t);
1887        merge_grpc_instruction_into_log(&mut log_ev, DexEvent::PumpFunBuy(ix_t));
1888        match log_ev {
1889            DexEvent::PumpFunTrade(t) => {
1890                assert_eq!(t.fee_recipient, fr);
1891                assert_eq!(t.sol_amount, 50);
1892            }
1893            _ => panic!("expected trade"),
1894        }
1895    }
1896
1897    #[test]
1898    fn grpc_merge_keeps_log_trade_fields() {
1899        let metadata = EventMetadata {
1900            signature: Signature::default(),
1901            slot: 1,
1902            tx_index: 0,
1903            block_time_us: 0,
1904            grpc_recv_us: 0,
1905            recent_blockhash: None,
1906        };
1907        let log_t = PumpFunTradeEvent {
1908            metadata: metadata.clone(),
1909            mayhem_mode: true,
1910            sol_amount: 100,
1911            ..Default::default()
1912        };
1913        let mut ix_t = log_t.clone();
1914        ix_t.mayhem_mode = false;
1915        ix_t.sol_amount = 999;
1916
1917        let mut log_ev = DexEvent::PumpFunTrade(log_t);
1918        merge_grpc_instruction_into_log(&mut log_ev, DexEvent::PumpFunBuy(ix_t));
1919        match log_ev {
1920            DexEvent::PumpFunTrade(t) => {
1921                assert!(t.mayhem_mode);
1922                assert_eq!(t.sol_amount, 100);
1923            }
1924            _ => panic!("variant preserved"),
1925        }
1926    }
1927
1928    #[test]
1929    fn pumpfun_create_merge_propagates_holder_rewards_fields() {
1930        let mut base = DexEvent::PumpFunCreate(PumpFunCreateTokenEvent {
1931            creator_fee_bps: 125,
1932            ..Default::default()
1933        });
1934        let inner = DexEvent::PumpFunCreate(PumpFunCreateTokenEvent {
1935            creator_fee_bps: 300,
1936            is_holder_reward: true,
1937            ..Default::default()
1938        });
1939
1940        merge_events(&mut base, inner);
1941
1942        let DexEvent::PumpFunCreate(event) = base else { panic!("create") };
1943        assert_eq!(event.creator_fee_bps, 300);
1944        assert!(event.is_holder_reward);
1945    }
1946
1947    #[test]
1948    fn grpc_create_merge_keeps_log_fee_and_adds_instruction_holder_reward() {
1949        let mut log = DexEvent::PumpFunCreate(PumpFunCreateTokenEvent {
1950            creator_fee_bps: 300,
1951            ..Default::default()
1952        });
1953        let instruction = DexEvent::PumpFunCreateV2(PumpFunCreateV2TokenEvent {
1954            creator_fee_bps: 500,
1955            is_holder_reward: true,
1956            ..Default::default()
1957        });
1958
1959        merge_grpc_instruction_into_log(&mut log, instruction);
1960
1961        let DexEvent::PumpFunCreate(event) = log else { panic!("create") };
1962        assert_eq!(event.creator_fee_bps, 300);
1963        assert!(event.is_holder_reward);
1964    }
1965}
1966
1967#[cfg(test)]
1968mod orca_swap_context_tests {
1969    use super::*;
1970
1971    #[test]
1972    fn both_merge_paths_preserve_zero_wire_values_and_authoritative_execution() {
1973        for input_mode in [false, true] {
1974            let ix = OrcaWhirlpoolSwapEvent {
1975                ix_name: "swap_v2".into(),
1976                amount: 0,
1977                other_amount_threshold: u64::MAX,
1978                sqrt_price_limit: 0,
1979                amount_specified_is_input: input_mode,
1980                token_authority: Pubkey::new_unique(),
1981                token_owner_account_a: Pubkey::new_unique(),
1982                token_owner_account_b: Pubkey::new_unique(),
1983                ..Default::default()
1984            };
1985            let log = OrcaWhirlpoolSwapEvent {
1986                amount: 99,
1987                sqrt_price_limit: 99,
1988                input_amount: 123,
1989                output_amount: 456,
1990                pre_sqrt_price: 789,
1991                post_sqrt_price: 987,
1992                input_transfer_fee: 1,
1993                output_transfer_fee: 2,
1994                lp_fee: 3,
1995                protocol_fee: 4,
1996                a_to_b: true,
1997                ..Default::default()
1998            };
1999            for log_first in [false, true] {
2000                let mut result = if log_first {
2001                    DexEvent::OrcaWhirlpoolSwap(log.clone())
2002                } else {
2003                    DexEvent::OrcaWhirlpoolSwap(ix.clone())
2004                };
2005                if log_first {
2006                    merge_grpc_instruction_into_log(
2007                        &mut result,
2008                        DexEvent::OrcaWhirlpoolSwap(ix.clone()),
2009                    );
2010                } else {
2011                    merge_events(&mut result, DexEvent::OrcaWhirlpoolSwap(log.clone()));
2012                }
2013                let DexEvent::OrcaWhirlpoolSwap(e) = result else { panic!("swap") };
2014                assert_eq!(
2015                    (e.amount, e.other_amount_threshold, e.sqrt_price_limit),
2016                    (0, u64::MAX, 0)
2017                );
2018                assert_eq!(e.amount_specified_is_input, input_mode);
2019                assert_eq!(e.ix_name, "swap_v2");
2020                assert_eq!(
2021                    (e.input_amount, e.output_amount, e.pre_sqrt_price, e.post_sqrt_price),
2022                    (123, 456, 789, 987)
2023                );
2024                assert_eq!(
2025                    (e.input_transfer_fee, e.output_transfer_fee, e.lp_fee, e.protocol_fee),
2026                    (1, 2, 3, 4)
2027                );
2028                assert!(e.a_to_b);
2029                assert_eq!(
2030                    (e.token_authority, e.token_owner_account_a, e.token_owner_account_b),
2031                    (ix.token_authority, ix.token_owner_account_a, ix.token_owner_account_b)
2032                );
2033            }
2034        }
2035    }
2036}
2037
2038#[cfg(test)]
2039mod pools_swap_merge_tests {
2040    use super::*;
2041
2042    #[test]
2043    fn both_paths_keep_instruction_zero_values_and_log_execution_values() {
2044        let ix = MeteoraPoolsSwapEvent {
2045            ix_name: "swap".into(),
2046            amount_in: 0,
2047            minimum_out_amount: u64::MAX,
2048            pool: Pubkey::new_unique(),
2049            user_source_token: Pubkey::new_unique(),
2050            user_destination_token: Pubkey::new_unique(),
2051            a_vault: Pubkey::new_unique(),
2052            b_vault: Pubkey::new_unique(),
2053            a_token_vault: Pubkey::new_unique(),
2054            b_token_vault: Pubkey::new_unique(),
2055            a_vault_lp_mint: Pubkey::new_unique(),
2056            b_vault_lp_mint: Pubkey::new_unique(),
2057            a_vault_lp: Pubkey::new_unique(),
2058            b_vault_lp: Pubkey::new_unique(),
2059            protocol_token_fee: Pubkey::new_unique(),
2060            user: Pubkey::new_unique(),
2061            vault_program: Pubkey::new_unique(),
2062            token_program: Pubkey::new_unique(),
2063            ..Default::default()
2064        };
2065        let log = MeteoraPoolsSwapEvent {
2066            amount_in: 99,
2067            minimum_out_amount: 99,
2068            in_amount: 123,
2069            out_amount: 456,
2070            trade_fee: 1,
2071            admin_fee: 2,
2072            host_fee: 3,
2073            token_program: Pubkey::new_unique(),
2074            ..Default::default()
2075        };
2076        for log_first in [false, true] {
2077            let mut result =
2078                DexEvent::MeteoraPoolsSwap(if log_first { log.clone() } else { ix.clone() });
2079            if log_first {
2080                merge_grpc_instruction_into_log(
2081                    &mut result,
2082                    DexEvent::MeteoraPoolsSwap(ix.clone()),
2083                );
2084            } else {
2085                merge_events(&mut result, DexEvent::MeteoraPoolsSwap(log.clone()));
2086            }
2087            let DexEvent::MeteoraPoolsSwap(e) = result else { panic!("swap") };
2088            assert_eq!(e.ix_name, "swap");
2089            assert_eq!((e.amount_in, e.minimum_out_amount), (0, u64::MAX));
2090            assert_eq!(
2091                (e.in_amount, e.out_amount, e.trade_fee, e.admin_fee, e.host_fee),
2092                (123, 456, 1, 2, 3)
2093            );
2094            assert_eq!(e.pool, ix.pool);
2095            assert_eq!(e.user_source_token, ix.user_source_token);
2096            assert_eq!(e.user_destination_token, ix.user_destination_token);
2097            assert_eq!(e.a_vault, ix.a_vault);
2098            assert_eq!(e.b_vault, ix.b_vault);
2099            assert_eq!(e.a_token_vault, ix.a_token_vault);
2100            assert_eq!(e.b_token_vault, ix.b_token_vault);
2101            assert_eq!(e.a_vault_lp_mint, ix.a_vault_lp_mint);
2102            assert_eq!(e.b_vault_lp_mint, ix.b_vault_lp_mint);
2103            assert_eq!(e.a_vault_lp, ix.a_vault_lp);
2104            assert_eq!(e.b_vault_lp, ix.b_vault_lp);
2105            assert_eq!(e.protocol_token_fee, ix.protocol_token_fee);
2106            assert_eq!(e.user, ix.user);
2107            assert_eq!(e.vault_program, ix.vault_program);
2108            assert_eq!(e.token_program, log.token_program);
2109        }
2110    }
2111}
2112
2113#[cfg(test)]
2114mod pools_liquidity_merge_tests {
2115    use super::*;
2116    #[test]
2117    fn add_balance_liquidity_both_merge_paths_preserve_limits_and_execution() {
2118        let ix = MeteoraPoolsAddLiquidityEvent {
2119            ix_name: "add_balance_liquidity".into(),
2120            pool_token_amount: 0,
2121            maximum_token_a_amount: u64::MAX,
2122            maximum_token_b_amount: 0,
2123            pool: Pubkey::new_unique(),
2124            lp_mint: Pubkey::new_unique(),
2125            user_pool_lp: Pubkey::new_unique(),
2126            a_vault_lp: Pubkey::new_unique(),
2127            b_vault_lp: Pubkey::new_unique(),
2128            a_vault: Pubkey::new_unique(),
2129            b_vault: Pubkey::new_unique(),
2130            a_vault_lp_mint: Pubkey::new_unique(),
2131            b_vault_lp_mint: Pubkey::new_unique(),
2132            a_token_vault: Pubkey::new_unique(),
2133            b_token_vault: Pubkey::new_unique(),
2134            user_a_token: Pubkey::new_unique(),
2135            user_b_token: Pubkey::new_unique(),
2136            user: Pubkey::new_unique(),
2137            vault_program: Pubkey::new_unique(),
2138            token_program: Pubkey::new_unique(),
2139            ..Default::default()
2140        };
2141        let log = MeteoraPoolsAddLiquidityEvent {
2142            pool_token_amount: 99,
2143            maximum_token_a_amount: 99,
2144            maximum_token_b_amount: 99,
2145            lp_mint_amount: 123,
2146            token_a_amount: 124,
2147            token_b_amount: 125,
2148            token_program: Pubkey::new_unique(),
2149            ..Default::default()
2150        };
2151        for log_first in [false, true] {
2152            let mut result = DexEvent::MeteoraPoolsAddLiquidity(if log_first {
2153                log.clone()
2154            } else {
2155                ix.clone()
2156            });
2157            if log_first {
2158                merge_grpc_instruction_into_log(
2159                    &mut result,
2160                    DexEvent::MeteoraPoolsAddLiquidity(ix.clone()),
2161                );
2162            } else {
2163                merge_events(&mut result, DexEvent::MeteoraPoolsAddLiquidity(log.clone()));
2164            }
2165            let DexEvent::MeteoraPoolsAddLiquidity(e) = result else { panic!("liquidity") };
2166            assert_eq!(e.ix_name, "add_balance_liquidity");
2167            assert_eq!(e.pool_token_amount, ix.pool_token_amount);
2168            assert_eq!(e.maximum_token_a_amount, ix.maximum_token_a_amount);
2169            assert_eq!(e.maximum_token_b_amount, ix.maximum_token_b_amount);
2170            assert_eq!(e.pool, ix.pool);
2171            assert_eq!(e.lp_mint, ix.lp_mint);
2172            assert_eq!(e.user_pool_lp, ix.user_pool_lp);
2173            assert_eq!(e.a_vault_lp, ix.a_vault_lp);
2174            assert_eq!(e.b_vault_lp, ix.b_vault_lp);
2175            assert_eq!(e.a_vault, ix.a_vault);
2176            assert_eq!(e.b_vault, ix.b_vault);
2177            assert_eq!(e.a_vault_lp_mint, ix.a_vault_lp_mint);
2178            assert_eq!(e.b_vault_lp_mint, ix.b_vault_lp_mint);
2179            assert_eq!(e.a_token_vault, ix.a_token_vault);
2180            assert_eq!(e.b_token_vault, ix.b_token_vault);
2181            assert_eq!(e.user_a_token, ix.user_a_token);
2182            assert_eq!(e.user_b_token, ix.user_b_token);
2183            assert_eq!(e.user, ix.user);
2184            assert_eq!(e.vault_program, ix.vault_program);
2185            assert_eq!(e.lp_mint_amount, log.lp_mint_amount);
2186            assert_eq!(e.token_a_amount, log.token_a_amount);
2187            assert_eq!(e.token_b_amount, log.token_b_amount);
2188            assert_eq!(e.token_program, log.token_program);
2189        }
2190    }
2191    #[test]
2192    fn add_imbalance_liquidity_both_merge_paths_preserve_limits_and_execution() {
2193        let ix = MeteoraPoolsAddLiquidityEvent {
2194            ix_name: "add_imbalance_liquidity".into(),
2195            minimum_pool_token_amount: 0,
2196            token_a_in_amount: u64::MAX,
2197            token_b_in_amount: 0,
2198            pool: Pubkey::new_unique(),
2199            lp_mint: Pubkey::new_unique(),
2200            user_pool_lp: Pubkey::new_unique(),
2201            a_vault_lp: Pubkey::new_unique(),
2202            b_vault_lp: Pubkey::new_unique(),
2203            a_vault: Pubkey::new_unique(),
2204            b_vault: Pubkey::new_unique(),
2205            a_vault_lp_mint: Pubkey::new_unique(),
2206            b_vault_lp_mint: Pubkey::new_unique(),
2207            a_token_vault: Pubkey::new_unique(),
2208            b_token_vault: Pubkey::new_unique(),
2209            user_a_token: Pubkey::new_unique(),
2210            user_b_token: Pubkey::new_unique(),
2211            user: Pubkey::new_unique(),
2212            vault_program: Pubkey::new_unique(),
2213            token_program: Pubkey::new_unique(),
2214            ..Default::default()
2215        };
2216        let log = MeteoraPoolsAddLiquidityEvent {
2217            minimum_pool_token_amount: 99,
2218            token_a_in_amount: 99,
2219            token_b_in_amount: 99,
2220            lp_mint_amount: 123,
2221            token_a_amount: 124,
2222            token_b_amount: 125,
2223            token_program: Pubkey::new_unique(),
2224            ..Default::default()
2225        };
2226        for log_first in [false, true] {
2227            let mut result = DexEvent::MeteoraPoolsAddLiquidity(if log_first {
2228                log.clone()
2229            } else {
2230                ix.clone()
2231            });
2232            if log_first {
2233                merge_grpc_instruction_into_log(
2234                    &mut result,
2235                    DexEvent::MeteoraPoolsAddLiquidity(ix.clone()),
2236                );
2237            } else {
2238                merge_events(&mut result, DexEvent::MeteoraPoolsAddLiquidity(log.clone()));
2239            }
2240            let DexEvent::MeteoraPoolsAddLiquidity(e) = result else { panic!("liquidity") };
2241            assert_eq!(e.ix_name, "add_imbalance_liquidity");
2242            assert_eq!(e.minimum_pool_token_amount, ix.minimum_pool_token_amount);
2243            assert_eq!(e.token_a_in_amount, ix.token_a_in_amount);
2244            assert_eq!(e.token_b_in_amount, ix.token_b_in_amount);
2245            assert_eq!(e.pool, ix.pool);
2246            assert_eq!(e.lp_mint, ix.lp_mint);
2247            assert_eq!(e.user_pool_lp, ix.user_pool_lp);
2248            assert_eq!(e.a_vault_lp, ix.a_vault_lp);
2249            assert_eq!(e.b_vault_lp, ix.b_vault_lp);
2250            assert_eq!(e.a_vault, ix.a_vault);
2251            assert_eq!(e.b_vault, ix.b_vault);
2252            assert_eq!(e.a_vault_lp_mint, ix.a_vault_lp_mint);
2253            assert_eq!(e.b_vault_lp_mint, ix.b_vault_lp_mint);
2254            assert_eq!(e.a_token_vault, ix.a_token_vault);
2255            assert_eq!(e.b_token_vault, ix.b_token_vault);
2256            assert_eq!(e.user_a_token, ix.user_a_token);
2257            assert_eq!(e.user_b_token, ix.user_b_token);
2258            assert_eq!(e.user, ix.user);
2259            assert_eq!(e.vault_program, ix.vault_program);
2260            assert_eq!(e.lp_mint_amount, log.lp_mint_amount);
2261            assert_eq!(e.token_a_amount, log.token_a_amount);
2262            assert_eq!(e.token_b_amount, log.token_b_amount);
2263            assert_eq!(e.token_program, log.token_program);
2264        }
2265    }
2266    #[test]
2267    fn remove_balance_liquidity_both_merge_paths_preserve_limits_and_execution() {
2268        let ix = MeteoraPoolsRemoveLiquidityEvent {
2269            ix_name: "remove_balance_liquidity".into(),
2270            pool_token_amount: 0,
2271            minimum_a_token_out: u64::MAX,
2272            minimum_b_token_out: 0,
2273            pool: Pubkey::new_unique(),
2274            lp_mint: Pubkey::new_unique(),
2275            user_pool_lp: Pubkey::new_unique(),
2276            a_vault_lp: Pubkey::new_unique(),
2277            b_vault_lp: Pubkey::new_unique(),
2278            a_vault: Pubkey::new_unique(),
2279            b_vault: Pubkey::new_unique(),
2280            a_vault_lp_mint: Pubkey::new_unique(),
2281            b_vault_lp_mint: Pubkey::new_unique(),
2282            a_token_vault: Pubkey::new_unique(),
2283            b_token_vault: Pubkey::new_unique(),
2284            user_a_token: Pubkey::new_unique(),
2285            user_b_token: Pubkey::new_unique(),
2286            user: Pubkey::new_unique(),
2287            vault_program: Pubkey::new_unique(),
2288            token_program: Pubkey::new_unique(),
2289            ..Default::default()
2290        };
2291        let log = MeteoraPoolsRemoveLiquidityEvent {
2292            pool_token_amount: 99,
2293            minimum_a_token_out: 99,
2294            minimum_b_token_out: 99,
2295            lp_unmint_amount: 123,
2296            token_a_out_amount: 124,
2297            token_b_out_amount: 125,
2298            token_program: Pubkey::new_unique(),
2299            ..Default::default()
2300        };
2301        for log_first in [false, true] {
2302            let mut result = DexEvent::MeteoraPoolsRemoveLiquidity(if log_first {
2303                log.clone()
2304            } else {
2305                ix.clone()
2306            });
2307            if log_first {
2308                merge_grpc_instruction_into_log(
2309                    &mut result,
2310                    DexEvent::MeteoraPoolsRemoveLiquidity(ix.clone()),
2311                );
2312            } else {
2313                merge_events(&mut result, DexEvent::MeteoraPoolsRemoveLiquidity(log.clone()));
2314            }
2315            let DexEvent::MeteoraPoolsRemoveLiquidity(e) = result else { panic!("liquidity") };
2316            assert_eq!(e.ix_name, "remove_balance_liquidity");
2317            assert_eq!(e.pool_token_amount, ix.pool_token_amount);
2318            assert_eq!(e.minimum_a_token_out, ix.minimum_a_token_out);
2319            assert_eq!(e.minimum_b_token_out, ix.minimum_b_token_out);
2320            assert_eq!(e.pool, ix.pool);
2321            assert_eq!(e.lp_mint, ix.lp_mint);
2322            assert_eq!(e.user_pool_lp, ix.user_pool_lp);
2323            assert_eq!(e.a_vault_lp, ix.a_vault_lp);
2324            assert_eq!(e.b_vault_lp, ix.b_vault_lp);
2325            assert_eq!(e.a_vault, ix.a_vault);
2326            assert_eq!(e.b_vault, ix.b_vault);
2327            assert_eq!(e.a_vault_lp_mint, ix.a_vault_lp_mint);
2328            assert_eq!(e.b_vault_lp_mint, ix.b_vault_lp_mint);
2329            assert_eq!(e.a_token_vault, ix.a_token_vault);
2330            assert_eq!(e.b_token_vault, ix.b_token_vault);
2331            assert_eq!(e.user_a_token, ix.user_a_token);
2332            assert_eq!(e.user_b_token, ix.user_b_token);
2333            assert_eq!(e.user, ix.user);
2334            assert_eq!(e.vault_program, ix.vault_program);
2335            assert_eq!(e.lp_unmint_amount, log.lp_unmint_amount);
2336            assert_eq!(e.token_a_out_amount, log.token_a_out_amount);
2337            assert_eq!(e.token_b_out_amount, log.token_b_out_amount);
2338            assert_eq!(e.token_program, log.token_program);
2339        }
2340    }
2341}
2342
2343#[cfg(test)]
2344mod remaining_liquidity_merge_tests {
2345    use super::*;
2346    #[test]
2347    fn remove_liquidity_single_side_both_mergers_keep_zero_inputs_and_log_execution() {
2348        let ix = MeteoraPoolsRemoveLiquidityEvent {
2349            ix_name: "remove_liquidity_single_side".into(),
2350            pool_token_amount: 0,
2351            minimum_out_amount: u64::MAX,
2352            pool: Pubkey::new_unique(),
2353            lp_mint: Pubkey::new_unique(),
2354            user_pool_lp: Pubkey::new_unique(),
2355            a_vault_lp: Pubkey::new_unique(),
2356            b_vault_lp: Pubkey::new_unique(),
2357            a_vault: Pubkey::new_unique(),
2358            b_vault: Pubkey::new_unique(),
2359            a_vault_lp_mint: Pubkey::new_unique(),
2360            b_vault_lp_mint: Pubkey::new_unique(),
2361            a_token_vault: Pubkey::new_unique(),
2362            b_token_vault: Pubkey::new_unique(),
2363            user_destination_token: Pubkey::new_unique(),
2364            user: Pubkey::new_unique(),
2365            vault_program: Pubkey::new_unique(),
2366            token_program: Pubkey::new_unique(),
2367            ..Default::default()
2368        };
2369        let log = MeteoraPoolsRemoveLiquidityEvent {
2370            pool_token_amount: 99,
2371            minimum_out_amount: 99,
2372            lp_unmint_amount: 123,
2373            token_a_out_amount: 124,
2374            token_b_out_amount: 125,
2375            token_program: Pubkey::new_unique(),
2376            ..Default::default()
2377        };
2378        for log_first in [false, true] {
2379            let mut result = DexEvent::MeteoraPoolsRemoveLiquidity(if log_first {
2380                log.clone()
2381            } else {
2382                ix.clone()
2383            });
2384            if log_first {
2385                merge_grpc_instruction_into_log(
2386                    &mut result,
2387                    DexEvent::MeteoraPoolsRemoveLiquidity(ix.clone()),
2388                );
2389            } else {
2390                merge_events(&mut result, DexEvent::MeteoraPoolsRemoveLiquidity(log.clone()));
2391            }
2392            let DexEvent::MeteoraPoolsRemoveLiquidity(e) = result else { panic!("liquidity") };
2393            assert_eq!(e.ix_name, "remove_liquidity_single_side");
2394            assert_eq!(e.pool_token_amount, ix.pool_token_amount);
2395            assert_eq!(e.minimum_out_amount, ix.minimum_out_amount);
2396            assert_eq!(e.pool, ix.pool);
2397            assert_eq!(e.lp_mint, ix.lp_mint);
2398            assert_eq!(e.user_pool_lp, ix.user_pool_lp);
2399            assert_eq!(e.a_vault_lp, ix.a_vault_lp);
2400            assert_eq!(e.b_vault_lp, ix.b_vault_lp);
2401            assert_eq!(e.a_vault, ix.a_vault);
2402            assert_eq!(e.b_vault, ix.b_vault);
2403            assert_eq!(e.a_vault_lp_mint, ix.a_vault_lp_mint);
2404            assert_eq!(e.b_vault_lp_mint, ix.b_vault_lp_mint);
2405            assert_eq!(e.a_token_vault, ix.a_token_vault);
2406            assert_eq!(e.b_token_vault, ix.b_token_vault);
2407            assert_eq!(e.user_destination_token, ix.user_destination_token);
2408            assert_eq!(e.user, ix.user);
2409            assert_eq!(e.vault_program, ix.vault_program);
2410            assert_eq!(e.lp_unmint_amount, log.lp_unmint_amount);
2411            assert_eq!(e.token_a_out_amount, log.token_a_out_amount);
2412            assert_eq!(e.token_b_out_amount, log.token_b_out_amount);
2413            assert_eq!(e.token_program, log.token_program);
2414        }
2415    }
2416    #[test]
2417    fn bootstrap_liquidity_both_mergers_keep_zero_inputs_and_log_execution() {
2418        let ix = MeteoraPoolsBootstrapLiquidityEvent {
2419            ix_name: "bootstrap_liquidity".into(),
2420            token_a_in_amount: 0,
2421            token_b_in_amount: u64::MAX,
2422            pool: Pubkey::new_unique(),
2423            lp_mint: Pubkey::new_unique(),
2424            user_pool_lp: Pubkey::new_unique(),
2425            a_vault_lp: Pubkey::new_unique(),
2426            b_vault_lp: Pubkey::new_unique(),
2427            a_vault: Pubkey::new_unique(),
2428            b_vault: Pubkey::new_unique(),
2429            a_vault_lp_mint: Pubkey::new_unique(),
2430            b_vault_lp_mint: Pubkey::new_unique(),
2431            a_token_vault: Pubkey::new_unique(),
2432            b_token_vault: Pubkey::new_unique(),
2433            user_a_token: Pubkey::new_unique(),
2434            user_b_token: Pubkey::new_unique(),
2435            user: Pubkey::new_unique(),
2436            vault_program: Pubkey::new_unique(),
2437            token_program: Pubkey::new_unique(),
2438            ..Default::default()
2439        };
2440        let log = MeteoraPoolsBootstrapLiquidityEvent {
2441            token_a_in_amount: 99,
2442            token_b_in_amount: 99,
2443            lp_mint_amount: 123,
2444            token_a_amount: 124,
2445            token_b_amount: 125,
2446            token_program: Pubkey::new_unique(),
2447            ..Default::default()
2448        };
2449        for log_first in [false, true] {
2450            let mut result = DexEvent::MeteoraPoolsBootstrapLiquidity(if log_first {
2451                log.clone()
2452            } else {
2453                ix.clone()
2454            });
2455            if log_first {
2456                merge_grpc_instruction_into_log(
2457                    &mut result,
2458                    DexEvent::MeteoraPoolsBootstrapLiquidity(ix.clone()),
2459                );
2460            } else {
2461                merge_events(&mut result, DexEvent::MeteoraPoolsBootstrapLiquidity(log.clone()));
2462            }
2463            let DexEvent::MeteoraPoolsBootstrapLiquidity(e) = result else { panic!("liquidity") };
2464            assert_eq!(e.ix_name, "bootstrap_liquidity");
2465            assert_eq!(e.token_a_in_amount, ix.token_a_in_amount);
2466            assert_eq!(e.token_b_in_amount, ix.token_b_in_amount);
2467            assert_eq!(e.pool, ix.pool);
2468            assert_eq!(e.lp_mint, ix.lp_mint);
2469            assert_eq!(e.user_pool_lp, ix.user_pool_lp);
2470            assert_eq!(e.a_vault_lp, ix.a_vault_lp);
2471            assert_eq!(e.b_vault_lp, ix.b_vault_lp);
2472            assert_eq!(e.a_vault, ix.a_vault);
2473            assert_eq!(e.b_vault, ix.b_vault);
2474            assert_eq!(e.a_vault_lp_mint, ix.a_vault_lp_mint);
2475            assert_eq!(e.b_vault_lp_mint, ix.b_vault_lp_mint);
2476            assert_eq!(e.a_token_vault, ix.a_token_vault);
2477            assert_eq!(e.b_token_vault, ix.b_token_vault);
2478            assert_eq!(e.user_a_token, ix.user_a_token);
2479            assert_eq!(e.user_b_token, ix.user_b_token);
2480            assert_eq!(e.user, ix.user);
2481            assert_eq!(e.vault_program, ix.vault_program);
2482            assert_eq!(e.lp_mint_amount, log.lp_mint_amount);
2483            assert_eq!(e.token_a_amount, log.token_a_amount);
2484            assert_eq!(e.token_b_amount, log.token_b_amount);
2485            assert_eq!(e.token_program, log.token_program);
2486        }
2487    }
2488}
2489
2490#[cfg(test)]
2491mod pools_management_merge_tests {
2492    use super::*;
2493    #[test]
2494    fn creation_both_paths_keep_zero_inputs_optional_activation_and_accounts() {
2495        for activation_point in [None, Some(0), Some(u64::MAX)] {
2496            let ix = MeteoraPoolsPoolCreatedEvent {
2497                ix_name: "initialize_permissionless_constant_product_pool_with_config2".into(),
2498                token_a_in_amount: 0,
2499                token_b_in_amount: u64::MAX,
2500                activation_point,
2501                pool: Pubkey::new_unique(),
2502                config: Pubkey::new_unique(),
2503                lp_mint: Pubkey::new_unique(),
2504                token_a_mint: Pubkey::new_unique(),
2505                token_b_mint: Pubkey::new_unique(),
2506                a_vault: Pubkey::new_unique(),
2507                b_vault: Pubkey::new_unique(),
2508                a_token_vault: Pubkey::new_unique(),
2509                b_token_vault: Pubkey::new_unique(),
2510                a_vault_lp_mint: Pubkey::new_unique(),
2511                b_vault_lp_mint: Pubkey::new_unique(),
2512                a_vault_lp: Pubkey::new_unique(),
2513                b_vault_lp: Pubkey::new_unique(),
2514                payer_token_a: Pubkey::new_unique(),
2515                payer_token_b: Pubkey::new_unique(),
2516                payer_pool_lp: Pubkey::new_unique(),
2517                protocol_token_a_fee: Pubkey::new_unique(),
2518                protocol_token_b_fee: Pubkey::new_unique(),
2519                payer: Pubkey::new_unique(),
2520                rent: Pubkey::new_unique(),
2521                mint_metadata: Pubkey::new_unique(),
2522                metadata_program: Pubkey::new_unique(),
2523                vault_program: Pubkey::new_unique(),
2524                token_program: Pubkey::new_unique(),
2525                associated_token_program: Pubkey::new_unique(),
2526                system_program: Pubkey::new_unique(),
2527                ..Default::default()
2528            };
2529            let log = MeteoraPoolsPoolCreatedEvent {
2530                token_a_in_amount: 99,
2531                activation_point: Some(99),
2532                pool_type: 1,
2533                token_program: Pubkey::new_unique(),
2534                ..Default::default()
2535            };
2536            for log_first in [false, true] {
2537                let mut result = DexEvent::MeteoraPoolsPoolCreated(if log_first {
2538                    log.clone()
2539                } else {
2540                    ix.clone()
2541                });
2542                if log_first {
2543                    merge_grpc_instruction_into_log(
2544                        &mut result,
2545                        DexEvent::MeteoraPoolsPoolCreated(ix.clone()),
2546                    );
2547                } else {
2548                    merge_events(&mut result, DexEvent::MeteoraPoolsPoolCreated(log.clone()));
2549                }
2550                let DexEvent::MeteoraPoolsPoolCreated(e) = result else { panic!("create") };
2551                assert_eq!(
2552                    (e.token_a_in_amount, e.token_b_in_amount, e.activation_point),
2553                    (0, u64::MAX, activation_point)
2554                );
2555                assert_eq!(e.pool_type, 1);
2556                assert_eq!(e.pool, ix.pool);
2557                assert_eq!(e.config, ix.config);
2558                assert_eq!(e.lp_mint, ix.lp_mint);
2559                assert_eq!(e.token_a_mint, ix.token_a_mint);
2560                assert_eq!(e.token_b_mint, ix.token_b_mint);
2561                assert_eq!(e.a_vault, ix.a_vault);
2562                assert_eq!(e.b_vault, ix.b_vault);
2563                assert_eq!(e.a_token_vault, ix.a_token_vault);
2564                assert_eq!(e.b_token_vault, ix.b_token_vault);
2565                assert_eq!(e.a_vault_lp_mint, ix.a_vault_lp_mint);
2566                assert_eq!(e.b_vault_lp_mint, ix.b_vault_lp_mint);
2567                assert_eq!(e.a_vault_lp, ix.a_vault_lp);
2568                assert_eq!(e.b_vault_lp, ix.b_vault_lp);
2569                assert_eq!(e.payer_token_a, ix.payer_token_a);
2570                assert_eq!(e.payer_token_b, ix.payer_token_b);
2571                assert_eq!(e.payer_pool_lp, ix.payer_pool_lp);
2572                assert_eq!(e.protocol_token_a_fee, ix.protocol_token_a_fee);
2573                assert_eq!(e.protocol_token_b_fee, ix.protocol_token_b_fee);
2574                assert_eq!(e.payer, ix.payer);
2575                assert_eq!(e.rent, ix.rent);
2576                assert_eq!(e.mint_metadata, ix.mint_metadata);
2577                assert_eq!(e.metadata_program, ix.metadata_program);
2578                assert_eq!(e.vault_program, ix.vault_program);
2579                assert_eq!(e.associated_token_program, ix.associated_token_program);
2580                assert_eq!(e.system_program, ix.system_program);
2581                assert_eq!(e.token_program, log.token_program);
2582            }
2583        }
2584    }
2585    #[test]
2586    fn fees_both_paths_keep_partner_parameter_and_log_fee_values() {
2587        let ix = MeteoraPoolsSetPoolFeesEvent {
2588            ix_name: "set_pool_fees".into(),
2589            new_partner_fee_numerator: 0,
2590            fee_operator: Pubkey::new_unique(),
2591            pool: Pubkey::new_unique(),
2592            ..Default::default()
2593        };
2594        let log = MeteoraPoolsSetPoolFeesEvent {
2595            new_partner_fee_numerator: 99,
2596            trade_fee_numerator: 1,
2597            trade_fee_denominator: 2,
2598            protocol_trade_fee_numerator: 3,
2599            protocol_trade_fee_denominator: 4,
2600            owner_trade_fee_numerator: 3,
2601            owner_trade_fee_denominator: 4,
2602            ..Default::default()
2603        };
2604        for log_first in [false, true] {
2605            let mut result =
2606                DexEvent::MeteoraPoolsSetPoolFees(if log_first { log.clone() } else { ix.clone() });
2607            if log_first {
2608                merge_grpc_instruction_into_log(
2609                    &mut result,
2610                    DexEvent::MeteoraPoolsSetPoolFees(ix.clone()),
2611                );
2612            } else {
2613                merge_events(&mut result, DexEvent::MeteoraPoolsSetPoolFees(log.clone()));
2614            }
2615            let DexEvent::MeteoraPoolsSetPoolFees(e) = result else { panic!("fees") };
2616            assert_eq!(e.new_partner_fee_numerator, 0);
2617            assert_eq!((e.pool, e.fee_operator), (ix.pool, ix.fee_operator));
2618            assert_eq!(
2619                (
2620                    e.trade_fee_numerator,
2621                    e.trade_fee_denominator,
2622                    e.protocol_trade_fee_numerator,
2623                    e.protocol_trade_fee_denominator
2624                ),
2625                (1, 2, 3, 4)
2626            );
2627        }
2628    }
2629}
2630
2631#[cfg(test)]
2632mod remaining_creation_merge_tests {
2633    use super::*;
2634    #[test]
2635    fn both_paths_preserve_curve_custom_parameters_and_clear_absent_options() {
2636        for populated in [false, true] {
2637            let ix = MeteoraPoolsPoolCreatedEvent {
2638                ix_name: "initialize_customizable_permissionless_constant_product_pool".into(),
2639                stable_curve: populated
2640                    .then(|| MeteoraPoolsStableCurveParams { amp: u64::MAX, ..Default::default() }),
2641                trade_fee_bps: populated.then_some(0),
2642                customizable_params: populated.then(|| MeteoraPoolsCustomizableParams {
2643                    trade_fee_numerator: u32::MAX,
2644                    padding: vec![255; 90],
2645                    ..Default::default()
2646                }),
2647                admin: Pubkey::new_unique(),
2648                admin_token_a: Pubkey::new_unique(),
2649                admin_token_b: Pubkey::new_unique(),
2650                admin_pool_lp: Pubkey::new_unique(),
2651                fee_owner: Pubkey::new_unique(),
2652                ..Default::default()
2653            };
2654            let log = MeteoraPoolsPoolCreatedEvent {
2655                pool_type: 1,
2656                stable_curve: Some(MeteoraPoolsStableCurveParams::default()),
2657                trade_fee_bps: Some(99),
2658                customizable_params: Some(MeteoraPoolsCustomizableParams::default()),
2659                ..Default::default()
2660            };
2661            for log_first in [false, true] {
2662                let mut merged = DexEvent::MeteoraPoolsPoolCreated(if log_first {
2663                    log.clone()
2664                } else {
2665                    ix.clone()
2666                });
2667                if log_first {
2668                    merge_grpc_instruction_into_log(
2669                        &mut merged,
2670                        DexEvent::MeteoraPoolsPoolCreated(ix.clone()),
2671                    );
2672                } else {
2673                    merge_events(&mut merged, DexEvent::MeteoraPoolsPoolCreated(log.clone()));
2674                }
2675                let DexEvent::MeteoraPoolsPoolCreated(e) = merged else { panic!("create") };
2676                assert_eq!(e.stable_curve, ix.stable_curve);
2677                assert_eq!(e.trade_fee_bps, ix.trade_fee_bps);
2678                assert_eq!(e.customizable_params, ix.customizable_params);
2679                assert_eq!(
2680                    (e.admin, e.admin_token_a, e.admin_token_b, e.admin_pool_lp, e.fee_owner),
2681                    (ix.admin, ix.admin_token_a, ix.admin_token_b, ix.admin_pool_lp, ix.fee_owner)
2682                );
2683                assert_eq!(e.pool_type, 1);
2684            }
2685        }
2686    }
2687}
2688
2689#[cfg(test)]
2690mod amm_swap_parameter_merge_tests {
2691    use super::*;
2692    #[test]
2693    fn both_paths_preserve_zero_parameters_execution_and_account_context() {
2694        for name in ["swap_base_in", "swap_base_out", "swap_base_in_v2", "swap_base_out_v2"] {
2695            let input = name.starts_with("swap_base_in");
2696            let ix = RaydiumAmmV4SwapEvent {
2697                ix_name: name.into(),
2698                instruction_amount_in: 0,
2699                instruction_amount_out: 0,
2700                minimum_amount_out: if input { u64::MAX } else { 0 },
2701                max_amount_in: if input { 0 } else { u64::MAX },
2702                user_source_owner: Pubkey::new_unique(),
2703                user_source_token_account: Pubkey::new_unique(),
2704                user_destination_token_account: Pubkey::new_unique(),
2705                amm_target_orders: Some(Pubkey::new_unique()),
2706                ..Default::default()
2707            };
2708            let log = RaydiumAmmV4SwapEvent {
2709                amount_in: 123,
2710                amount_out: 456,
2711                instruction_amount_in: 99,
2712                instruction_amount_out: 99,
2713                minimum_amount_out: 99,
2714                max_amount_in: 99,
2715                token_program: Pubkey::new_unique(),
2716                ..Default::default()
2717            };
2718            for log_first in [false, true] {
2719                let mut merged =
2720                    DexEvent::RaydiumAmmV4Swap(if log_first { log.clone() } else { ix.clone() });
2721                if log_first {
2722                    merge_grpc_instruction_into_log(
2723                        &mut merged,
2724                        DexEvent::RaydiumAmmV4Swap(ix.clone()),
2725                    );
2726                } else {
2727                    merge_events(&mut merged, DexEvent::RaydiumAmmV4Swap(log.clone()));
2728                }
2729                let DexEvent::RaydiumAmmV4Swap(e) = merged else { panic!("swap") };
2730                assert_eq!(e.ix_name, name);
2731                assert_eq!((e.amount_in, e.amount_out), (123, 456));
2732                assert_eq!((e.instruction_amount_in, e.instruction_amount_out), (0, 0));
2733                assert_eq!(
2734                    (e.minimum_amount_out, e.max_amount_in),
2735                    (ix.minimum_amount_out, ix.max_amount_in)
2736                );
2737                assert_eq!(
2738                    (
2739                        e.user_source_owner,
2740                        e.user_source_token_account,
2741                        e.user_destination_token_account
2742                    ),
2743                    (
2744                        ix.user_source_owner,
2745                        ix.user_source_token_account,
2746                        ix.user_destination_token_account
2747                    )
2748                );
2749                assert_eq!(e.amm_target_orders, ix.amm_target_orders);
2750                assert_eq!(e.token_program, log.token_program);
2751            }
2752        }
2753    }
2754}
2755
2756// Instruction/CPI events sharing an outer index may still be distinct trades.
2757fn pump_trade_identity(event: &DexEvent) -> Option<(u8, Pubkey, Pubkey, Option<bool>)> {
2758    use DexEvent::*;
2759    match event {
2760        PumpFunTrade(e) | PumpFunBuy(e) | PumpFunSell(e) | PumpFunBuyExactSolIn(e) => Some((
2761            0,
2762            e.mint,
2763            e.user,
2764            (e.mint != Pubkey::default()
2765                || !e.ix_name.is_empty()
2766                || e.sol_amount != 0
2767                || e.token_amount != 0)
2768                .then_some(e.is_buy),
2769        )),
2770        PumpSwapBuy(e) => Some((1, e.pool, e.user, Some(true))),
2771        PumpSwapSell(e) => Some((1, e.pool, e.user, Some(false))),
2772        _ => None,
2773    }
2774}
2775
2776fn pump_trade_identity_conflicts(base: &DexEvent, inner: &DexEvent) -> bool {
2777    let (Some((kind, venue, user, buy)), Some((other, other_venue, other_user, other_buy))) =
2778        (pump_trade_identity(base), pump_trade_identity(inner))
2779    else {
2780        return false;
2781    };
2782    kind == other
2783        && ((buy.is_some() && other_buy.is_some() && buy != other_buy)
2784            || (venue != Pubkey::default()
2785                && other_venue != Pubkey::default()
2786                && venue != other_venue)
2787            || (user != Pubkey::default() && other_user != Pubkey::default() && user != other_user))
2788}