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