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