Skip to main content

sol_parser_sdk/core/
merger.rs

1//! 轻量级事件合并机制 - 零拷贝高性能实现
2//!
3//! 将 inner instruction 事件数据合并到主 instruction 事件中
4//! 设计原则:
5//! - 只合并必要的字段
6//! - 保持零拷贝特性
7//! - 内联优化,最小化开销
8//!
9//! **gRPC log + instruction 双路径**:见 [`merge_grpc_instruction_into_log`] —— **以程序日志为准**,
10//! 指令解析仅补充账户等日志侧缺失字段。
11
12use solana_sdk::pubkey::Pubkey;
13
14use crate::core::events::*;
15
16/// 合并 instruction 事件和 inner instruction 事件
17///
18/// # 设计
19/// - Inner instruction 包含完整的交易数据(来自程序日志)
20/// - Instruction 包含账户上下文(来自指令本身)
21/// - 合并后的事件包含两者的完整信息
22///
23/// # 性能
24/// - 内联优化,编译器会将其优化为直接赋值
25/// - 零堆分配
26/// - 预期开销 < 10ns
27#[inline(always)]
28pub fn merge_events(base: &mut DexEvent, inner: DexEvent) {
29    let mut unmerged = None;
30    let _ = try_merge_events(base, inner, &mut unmerged);
31}
32
33/// Try to merge `inner` into `base` without allocating or returning the large
34/// event enum by value. On mismatch, the untouched event is written to
35/// `unmerged` for the caller to recover.
36#[inline(always)]
37pub fn try_merge_events(
38    base: &mut DexEvent,
39    inner: DexEvent,
40    unmerged: &mut Option<DexEvent>,
41) -> bool {
42    use DexEvent::*;
43
44    match (base, inner) {
45        // ========== PumpFun 系列 ==========
46        (PumpFunTrade(b), PumpFunTrade(i))
47        | (PumpFunTrade(b), PumpFunBuy(i))
48        | (PumpFunTrade(b), PumpFunSell(i))
49        | (PumpFunTrade(b), PumpFunBuyExactSolIn(i))
50        | (PumpFunBuy(b), PumpFunTrade(i))
51        | (PumpFunBuy(b), PumpFunBuy(i))
52        | (PumpFunSell(b), PumpFunTrade(i))
53        | (PumpFunSell(b), PumpFunSell(i))
54        | (PumpFunBuyExactSolIn(b), PumpFunTrade(i))
55        | (PumpFunBuyExactSolIn(b), PumpFunBuyExactSolIn(i)) => merge_pumpfun_trade(b, i),
56
57        (PumpFunCreate(b), PumpFunCreate(i)) => merge_pumpfun_create(b, i),
58        (PumpFunCreateV2(b), PumpFunCreateV2(i)) => merge_pumpfun_create_v2(b, i),
59        (PumpFunMigrate(b), PumpFunMigrate(i)) => merge_pumpfun_migrate(b, i),
60        (PumpFunMigrateBondingCurveCreator(b), PumpFunMigrateBondingCurveCreator(i)) => {
61            merge_generic(b, i)
62        }
63
64        // ========== PumpFees 系列 ==========
65        (PumpFeesCreateFeeSharingConfig(b), PumpFeesCreateFeeSharingConfig(i)) => {
66            merge_generic(b, i)
67        }
68        (PumpFeesInitializeFeeConfig(b), PumpFeesInitializeFeeConfig(i)) => merge_generic(b, i),
69        (PumpFeesResetFeeSharingConfig(b), PumpFeesResetFeeSharingConfig(i)) => merge_generic(b, i),
70        (PumpFeesRevokeFeeSharingAuthority(b), PumpFeesRevokeFeeSharingAuthority(i)) => {
71            merge_generic(b, i)
72        }
73        (PumpFeesTransferFeeSharingAuthority(b), PumpFeesTransferFeeSharingAuthority(i)) => {
74            merge_generic(b, i)
75        }
76        (PumpFeesUpdateAdmin(b), PumpFeesUpdateAdmin(i)) => merge_generic(b, i),
77        (PumpFeesUpdateFeeConfig(b), PumpFeesUpdateFeeConfig(i)) => merge_generic(b, i),
78        (PumpFeesUpdateFeeShares(b), PumpFeesUpdateFeeShares(i)) => merge_generic(b, i),
79        (PumpFeesUpsertFeeTiers(b), PumpFeesUpsertFeeTiers(i)) => merge_generic(b, i),
80
81        // ========== PumpSwap 系列 ==========
82        (PumpSwapTrade(b), PumpSwapTrade(i)) => merge_generic(b, i),
83        (PumpSwapBuy(b), PumpSwapBuy(i)) => merge_pumpswap_buy(b, i),
84        (PumpSwapSell(b), PumpSwapSell(i)) => merge_pumpswap_sell(b, i),
85        (PumpSwapCreatePool(b), PumpSwapCreatePool(i)) => merge_generic(b, i),
86        (PumpSwapLiquidityAdded(b), PumpSwapLiquidityAdded(i)) => merge_generic(b, i),
87        (PumpSwapLiquidityRemoved(b), PumpSwapLiquidityRemoved(i)) => merge_generic(b, i),
88
89        // ========== Raydium CLMM 系列 ==========
90        (RaydiumClmmSwap(b), RaydiumClmmSwap(i)) => merge_generic(b, i),
91        (RaydiumClmmIncreaseLiquidity(b), RaydiumClmmIncreaseLiquidity(i)) => merge_generic(b, i),
92        (RaydiumClmmDecreaseLiquidity(b), RaydiumClmmDecreaseLiquidity(i)) => merge_generic(b, i),
93        (RaydiumClmmLiquidityChange(b), RaydiumClmmLiquidityChange(i)) => merge_generic(b, i),
94        (RaydiumClmmConfigChange(b), RaydiumClmmConfigChange(i)) => merge_generic(b, i),
95        (RaydiumClmmCreatePersonalPosition(b), RaydiumClmmCreatePersonalPosition(i)) => {
96            merge_generic(b, i)
97        }
98        (RaydiumClmmLiquidityCalculate(b), RaydiumClmmLiquidityCalculate(i)) => merge_generic(b, i),
99        (RaydiumClmmOpenLimitOrder(b), RaydiumClmmOpenLimitOrder(i)) => merge_generic(b, i),
100        (RaydiumClmmIncreaseLimitOrder(b), RaydiumClmmIncreaseLimitOrder(i)) => merge_generic(b, i),
101        (RaydiumClmmDecreaseLimitOrder(b), RaydiumClmmDecreaseLimitOrder(i)) => merge_generic(b, i),
102        (RaydiumClmmSettleLimitOrder(b), RaydiumClmmSettleLimitOrder(i)) => merge_generic(b, i),
103        (RaydiumClmmUpdateRewardInfos(b), RaydiumClmmUpdateRewardInfos(i)) => merge_generic(b, i),
104        (RaydiumClmmCreatePool(b), RaydiumClmmCreatePool(i)) => merge_generic(b, i),
105        (RaydiumClmmOpenPosition(b), RaydiumClmmOpenPosition(i)) => merge_generic(b, i),
106        (RaydiumClmmClosePosition(b), RaydiumClmmClosePosition(i)) => merge_generic(b, i),
107        (RaydiumClmmOpenPositionWithTokenExtNft(b), RaydiumClmmOpenPositionWithTokenExtNft(i)) => {
108            merge_generic(b, i)
109        }
110        (RaydiumClmmCollectFee(b), RaydiumClmmCollectFee(i)) => merge_generic(b, i),
111
112        // ========== Raydium CPMM 系列 ==========
113        (RaydiumCpmmSwap(b), RaydiumCpmmSwap(i)) => merge_generic(b, i),
114        (RaydiumCpmmDeposit(b), RaydiumCpmmDeposit(i)) => merge_generic(b, i),
115        (RaydiumCpmmWithdraw(b), RaydiumCpmmWithdraw(i)) => merge_generic(b, i),
116        (RaydiumCpmmInitialize(b), RaydiumCpmmInitialize(i)) => merge_generic(b, i),
117
118        // ========== Raydium AMM V4 系列 ==========
119        (RaydiumAmmV4Swap(b), RaydiumAmmV4Swap(i)) => merge_generic(b, i),
120        (RaydiumAmmV4Deposit(b), RaydiumAmmV4Deposit(i)) => merge_generic(b, i),
121        (RaydiumAmmV4Withdraw(b), RaydiumAmmV4Withdraw(i)) => merge_generic(b, i),
122        (RaydiumAmmV4Initialize2(b), RaydiumAmmV4Initialize2(i)) => merge_generic(b, i),
123        (RaydiumAmmV4WithdrawPnl(b), RaydiumAmmV4WithdrawPnl(i)) => merge_generic(b, i),
124
125        // ========== Orca Whirlpool 系列 ==========
126        (OrcaWhirlpoolSwap(b), OrcaWhirlpoolSwap(i)) => merge_generic(b, i),
127        (OrcaWhirlpoolLiquidityIncreased(b), OrcaWhirlpoolLiquidityIncreased(i)) => {
128            merge_generic(b, i)
129        }
130        (OrcaWhirlpoolLiquidityDecreased(b), OrcaWhirlpoolLiquidityDecreased(i)) => {
131            merge_generic(b, i)
132        }
133        (OrcaWhirlpoolPoolInitialized(b), OrcaWhirlpoolPoolInitialized(i)) => merge_generic(b, i),
134
135        // ========== Meteora Pools (AMM) 系列 ==========
136        (MeteoraPoolsSwap(b), MeteoraPoolsSwap(i)) => merge_generic(b, i),
137        (MeteoraPoolsAddLiquidity(b), MeteoraPoolsAddLiquidity(i)) => merge_generic(b, i),
138        (MeteoraPoolsRemoveLiquidity(b), MeteoraPoolsRemoveLiquidity(i)) => merge_generic(b, i),
139        (MeteoraPoolsBootstrapLiquidity(b), MeteoraPoolsBootstrapLiquidity(i)) => {
140            merge_generic(b, i)
141        }
142        (MeteoraPoolsPoolCreated(b), MeteoraPoolsPoolCreated(i)) => merge_generic(b, i),
143        (MeteoraPoolsSetPoolFees(b), MeteoraPoolsSetPoolFees(i)) => merge_generic(b, i),
144
145        // ========== Meteora DAMM V2 系列 ==========
146        (MeteoraDammV2Swap(b), MeteoraDammV2Swap(i)) => merge_generic(b, i),
147        (MeteoraDammV2AddLiquidity(b), MeteoraDammV2AddLiquidity(i)) => merge_generic(b, i),
148        (MeteoraDammV2RemoveLiquidity(b), MeteoraDammV2RemoveLiquidity(i)) => merge_generic(b, i),
149        (MeteoraDammV2InitializePool(b), MeteoraDammV2InitializePool(i)) => merge_generic(b, i),
150        (MeteoraDammV2CreatePosition(b), MeteoraDammV2CreatePosition(i)) => merge_generic(b, i),
151        (MeteoraDammV2ClosePosition(b), MeteoraDammV2ClosePosition(i)) => merge_generic(b, i),
152        (MeteoraDammV2UpdateDelegatePermission(b), MeteoraDammV2UpdateDelegatePermission(i)) => {
153            merge_generic(b, i)
154        }
155        (
156            MeteoraDammV2WithdrawDeadLiquidityReward(b),
157            MeteoraDammV2WithdrawDeadLiquidityReward(i),
158        ) => merge_generic(b, i),
159        (MeteoraDammV2CreateConfig(b), MeteoraDammV2CreateConfig(i)) => merge_generic(b, i),
160        (MeteoraDammV2CreateDynamicConfig(b), MeteoraDammV2CreateDynamicConfig(i)) => {
161            merge_generic(b, i)
162        }
163
164        // ========== Meteora DLMM 系列 ==========
165        (MeteoraDlmmSwap(b), MeteoraDlmmSwap(i)) => merge_dlmm_swap(b, i),
166        (MeteoraDlmmAddLiquidity(b), MeteoraDlmmAddLiquidity(i)) => merge_generic(b, i),
167        (MeteoraDlmmRemoveLiquidity(b), MeteoraDlmmRemoveLiquidity(i)) => merge_generic(b, i),
168        (MeteoraDlmmInitializePool(b), MeteoraDlmmInitializePool(i)) => {
169            merge_dlmm_initialize_pool(b, i)
170        }
171        (MeteoraDlmmInitializeBinArray(b), MeteoraDlmmInitializeBinArray(i)) => merge_generic(b, i),
172        (MeteoraDlmmCreatePosition(b), MeteoraDlmmCreatePosition(i)) => {
173            merge_dlmm_create_position(b, i)
174        }
175        (MeteoraDlmmClosePosition(b), MeteoraDlmmClosePosition(i)) => {
176            merge_dlmm_close_position(b, i)
177        }
178        (MeteoraDlmmClaimFee(b), MeteoraDlmmClaimFee(i)) => merge_generic(b, i),
179
180        // ========== RaydiumLaunchlab 系列 ==========
181        (RaydiumLaunchlabTrade(b), RaydiumLaunchlabTrade(i)) => merge_generic(b, i),
182        (RaydiumLaunchlabPoolCreate(b), RaydiumLaunchlabPoolCreate(i)) => merge_generic(b, i),
183        (RaydiumLaunchlabMigrateAmm(b), RaydiumLaunchlabMigrateAmm(i)) => merge_generic(b, i),
184
185        // 其他组合不需要合并(类型不匹配)
186        (_, event) => {
187            *unmerged = Some(event);
188            return false;
189        }
190    }
191
192    true
193}
194
195/// 通用合并函数 - 对于大多数事件,inner instruction 包含完整数据
196///
197/// 这个函数简单地用 inner 的数据覆盖 base,因为:
198/// - Inner instruction 来自程序日志,包含完整的交易数据
199/// - Instruction 主要提供账户上下文
200/// - 对于大多数协议,inner instruction 的数据已经足够完整
201#[inline(always)]
202fn merge_generic<T>(base: &mut T, inner: T) {
203    *base = inner;
204}
205
206#[inline(always)]
207fn merge_dlmm_swap(base: &mut MeteoraDlmmSwapEvent, inner: MeteoraDlmmSwapEvent) {
208    let min_amount_out =
209        if inner.min_amount_out != 0 { inner.min_amount_out } else { base.min_amount_out };
210    let user_token_in = if inner.user_token_in != Pubkey::default() {
211        inner.user_token_in
212    } else {
213        base.user_token_in
214    };
215    let user_token_out = if inner.user_token_out != Pubkey::default() {
216        inner.user_token_out
217    } else {
218        base.user_token_out
219    };
220    *base = inner;
221    base.min_amount_out = min_amount_out;
222    base.user_token_in = user_token_in;
223    base.user_token_out = user_token_out;
224}
225
226#[inline(always)]
227fn merge_dlmm_initialize_pool(
228    base: &mut MeteoraDlmmInitializePoolEvent,
229    inner: MeteoraDlmmInitializePoolEvent,
230) {
231    let creator = base.creator;
232    let active_bin_id = base.active_bin_id;
233    *base = inner;
234    base.creator = creator;
235    base.active_bin_id = active_bin_id;
236}
237
238#[inline(always)]
239fn merge_dlmm_create_position(
240    base: &mut MeteoraDlmmCreatePositionEvent,
241    inner: MeteoraDlmmCreatePositionEvent,
242) {
243    let lower_bin_id = base.lower_bin_id;
244    let width = base.width;
245    *base = inner;
246    base.lower_bin_id = lower_bin_id;
247    base.width = width;
248}
249
250#[inline(always)]
251fn merge_dlmm_close_position(
252    base: &mut MeteoraDlmmClosePositionEvent,
253    inner: MeteoraDlmmClosePositionEvent,
254) {
255    let pool = base.pool;
256    *base = inner;
257    base.pool = pool;
258}
259
260// ============================================================================
261// PumpFun 事件合并实现
262// ============================================================================
263
264#[inline(always)]
265fn put_pk_if_set(to: &mut Pubkey, from: Pubkey) {
266    if from != Pubkey::default() {
267        *to = from;
268    }
269}
270
271#[inline(always)]
272fn put_pumpfun_quote_mint_if_set(to: &mut Pubkey, from: Pubkey) {
273    let from = normalize_pumpfun_quote_mint(from);
274    if from != Pubkey::default()
275        && (*to == Pubkey::default()
276            || is_pumpfun_solscan_sol_quote_mint(*to)
277            || !is_pumpfun_solscan_sol_quote_mint(from))
278    {
279        *to = from;
280    }
281}
282
283#[inline(always)]
284fn put_u64_if_nonzero(to: &mut u64, from: u64) {
285    if from != 0 {
286        *to = from;
287    }
288}
289
290#[inline(always)]
291fn put_i64_if_nonzero(to: &mut i64, from: i64) {
292    if from != 0 {
293        *to = from;
294    }
295}
296
297/// 合并 PumpFun Trade 事件
298///
299/// 合并策略:
300/// - Inner instruction 提供: 交易数据(amount, reserves, fees 等)
301/// - Instruction 提供: 账户上下文(bonding_curve, associated_bonding_curve 等)
302/// - 合并后: 完整的交易事件
303///
304/// 同一 outer 下多段 inner 链式合并时:若某段 inner 未带成交量(`sol_amount`/`token_amount` 均为 0),
305/// 则不再用其覆盖金额与储备,避免把前一段已合并好的数据清空。
306#[inline(always)]
307fn merge_pumpfun_trade(base: &mut PumpFunTradeEvent, inner: PumpFunTradeEvent) {
308    let leg = inner.sol_amount != 0 || inner.token_amount != 0;
309
310    put_pk_if_set(&mut base.mint, inner.mint);
311    put_pk_if_set(&mut base.user, inner.user);
312    put_pk_if_set(&mut base.fee_recipient, inner.fee_recipient);
313    put_pk_if_set(&mut base.creator, inner.creator);
314
315    if leg {
316        base.sol_amount = inner.sol_amount;
317        base.token_amount = inner.token_amount;
318        base.is_buy = inner.is_buy;
319        base.timestamp = inner.timestamp;
320        base.virtual_sol_reserves = inner.virtual_sol_reserves;
321        base.virtual_token_reserves = inner.virtual_token_reserves;
322        base.real_sol_reserves = inner.real_sol_reserves;
323        base.real_token_reserves = inner.real_token_reserves;
324        base.fee_basis_points = inner.fee_basis_points;
325        base.fee = inner.fee;
326        base.creator_fee_basis_points = inner.creator_fee_basis_points;
327        base.creator_fee = inner.creator_fee;
328        base.track_volume |= inner.track_volume;
329        base.total_unclaimed_tokens = inner.total_unclaimed_tokens;
330        base.total_claimed_tokens = inner.total_claimed_tokens;
331        base.current_sol_volume = inner.current_sol_volume;
332        base.last_update_timestamp = inner.last_update_timestamp;
333        if !inner.ix_name.is_empty() {
334            base.ix_name = inner.ix_name;
335        }
336        base.mayhem_mode |= inner.mayhem_mode;
337        put_u64_if_nonzero(&mut base.cashback_fee_basis_points, inner.cashback_fee_basis_points);
338        put_u64_if_nonzero(&mut base.cashback, inner.cashback);
339        put_u64_if_nonzero(&mut base.buyback_fee_basis_points, inner.buyback_fee_basis_points);
340        put_u64_if_nonzero(&mut base.buyback_fee, inner.buyback_fee);
341        if base.shareholders.is_empty() && !inner.shareholders.is_empty() {
342            base.shareholders = inner.shareholders;
343        }
344        put_pumpfun_quote_mint_if_set(&mut base.quote_mint, inner.quote_mint);
345        put_u64_if_nonzero(&mut base.quote_amount, inner.quote_amount);
346        put_u64_if_nonzero(&mut base.virtual_quote_reserves, inner.virtual_quote_reserves);
347        put_u64_if_nonzero(&mut base.real_quote_reserves, inner.real_quote_reserves);
348        base.is_cashback_coin |= inner.is_cashback_coin;
349        base.holder_rewards_bps = inner.holder_rewards_bps;
350        base.holder_rewards = inner.holder_rewards;
351    } else {
352        put_u64_if_nonzero(&mut base.fee, inner.fee);
353        put_u64_if_nonzero(&mut base.creator_fee, inner.creator_fee);
354        put_u64_if_nonzero(&mut base.fee_basis_points, inner.fee_basis_points);
355        put_u64_if_nonzero(&mut base.creator_fee_basis_points, inner.creator_fee_basis_points);
356        put_u64_if_nonzero(&mut base.virtual_sol_reserves, inner.virtual_sol_reserves);
357        put_u64_if_nonzero(&mut base.virtual_token_reserves, inner.virtual_token_reserves);
358        put_u64_if_nonzero(&mut base.real_sol_reserves, inner.real_sol_reserves);
359        put_u64_if_nonzero(&mut base.real_token_reserves, inner.real_token_reserves);
360        put_u64_if_nonzero(&mut base.total_unclaimed_tokens, inner.total_unclaimed_tokens);
361        put_u64_if_nonzero(&mut base.total_claimed_tokens, inner.total_claimed_tokens);
362        put_u64_if_nonzero(&mut base.current_sol_volume, inner.current_sol_volume);
363        put_u64_if_nonzero(&mut base.cashback_fee_basis_points, inner.cashback_fee_basis_points);
364        put_u64_if_nonzero(&mut base.cashback, inner.cashback);
365        put_u64_if_nonzero(&mut base.buyback_fee_basis_points, inner.buyback_fee_basis_points);
366        put_u64_if_nonzero(&mut base.buyback_fee, inner.buyback_fee);
367        if base.shareholders.is_empty() && !inner.shareholders.is_empty() {
368            base.shareholders = inner.shareholders;
369        }
370        put_pumpfun_quote_mint_if_set(&mut base.quote_mint, inner.quote_mint);
371        put_u64_if_nonzero(&mut base.quote_amount, inner.quote_amount);
372        put_u64_if_nonzero(&mut base.virtual_quote_reserves, inner.virtual_quote_reserves);
373        put_u64_if_nonzero(&mut base.real_quote_reserves, inner.real_quote_reserves);
374        put_u64_if_nonzero(&mut base.holder_rewards_bps, inner.holder_rewards_bps);
375        put_u64_if_nonzero(&mut base.holder_rewards, inner.holder_rewards);
376        put_i64_if_nonzero(&mut base.timestamp, inner.timestamp);
377        put_i64_if_nonzero(&mut base.last_update_timestamp, inner.last_update_timestamp);
378        if !inner.ix_name.is_empty() {
379            base.ix_name = inner.ix_name;
380        }
381        base.track_volume |= inner.track_volume;
382        base.mayhem_mode |= inner.mayhem_mode;
383        base.is_cashback_coin |= inner.is_cashback_coin;
384    }
385    put_u64_if_nonzero(&mut base.amount, inner.amount);
386    put_u64_if_nonzero(&mut base.max_sol_cost, inner.max_sol_cost);
387    put_u64_if_nonzero(&mut base.min_sol_output, inner.min_sol_output);
388    put_u64_if_nonzero(&mut base.spendable_sol_in, inner.spendable_sol_in);
389    put_u64_if_nonzero(&mut base.spendable_quote_in, inner.spendable_quote_in);
390    put_u64_if_nonzero(&mut base.min_tokens_out, inner.min_tokens_out);
391    put_pk_if_set(&mut base.global, inner.global);
392    put_pk_if_set(&mut base.bonding_curve, inner.bonding_curve);
393    put_pk_if_set(&mut base.bonding_curve_v2, inner.bonding_curve_v2);
394    put_pk_if_set(&mut base.associated_bonding_curve, inner.associated_bonding_curve);
395    put_pk_if_set(&mut base.associated_user, inner.associated_user);
396    put_pk_if_set(&mut base.system_program, inner.system_program);
397    put_pk_if_set(&mut base.token_program, inner.token_program);
398    put_pk_if_set(&mut base.quote_token_program, inner.quote_token_program);
399    put_pk_if_set(&mut base.associated_token_program, inner.associated_token_program);
400    put_pk_if_set(&mut base.creator_vault, inner.creator_vault);
401    put_pk_if_set(&mut base.associated_quote_fee_recipient, inner.associated_quote_fee_recipient);
402    put_pk_if_set(&mut base.buyback_fee_recipient, inner.buyback_fee_recipient);
403    put_pk_if_set(
404        &mut base.associated_quote_buyback_fee_recipient,
405        inner.associated_quote_buyback_fee_recipient,
406    );
407    put_pk_if_set(&mut base.associated_quote_bonding_curve, inner.associated_quote_bonding_curve);
408    put_pk_if_set(&mut base.associated_quote_user, inner.associated_quote_user);
409    put_pk_if_set(&mut base.associated_creator_vault, inner.associated_creator_vault);
410    put_pk_if_set(&mut base.sharing_config, inner.sharing_config);
411    put_pk_if_set(&mut base.event_authority, inner.event_authority);
412    put_pk_if_set(&mut base.program, inner.program);
413    put_pk_if_set(&mut base.global_volume_accumulator, inner.global_volume_accumulator);
414    put_pk_if_set(&mut base.user_volume_accumulator, inner.user_volume_accumulator);
415    put_pk_if_set(
416        &mut base.associated_user_volume_accumulator,
417        inner.associated_user_volume_accumulator,
418    );
419    put_pk_if_set(&mut base.fee_config, inner.fee_config);
420    put_pk_if_set(&mut base.fee_program, inner.fee_program);
421    if base.account.is_none() {
422        base.account = inner.account;
423    }
424
425    base.is_created_buy |= inner.is_created_buy;
426    // 保留 base 的账户上下文字段(bonding_curve, associated_bonding_curve 等)
427}
428
429/// 合并 PumpFun Create 事件
430#[inline(always)]
431fn merge_pumpfun_create(base: &mut PumpFunCreateTokenEvent, inner: PumpFunCreateTokenEvent) {
432    // Inner instruction 包含完整的 create 数据
433    base.name = inner.name;
434    base.symbol = inner.symbol;
435    base.uri = inner.uri;
436    base.mint = inner.mint;
437    base.bonding_curve = inner.bonding_curve;
438    base.user = inner.user;
439    base.creator = inner.creator;
440    base.timestamp = inner.timestamp;
441    base.virtual_token_reserves = inner.virtual_token_reserves;
442    base.virtual_sol_reserves = inner.virtual_sol_reserves;
443    base.real_token_reserves = inner.real_token_reserves;
444    base.token_total_supply = inner.token_total_supply;
445    base.token_program = inner.token_program;
446    base.is_mayhem_mode = inner.is_mayhem_mode;
447    base.is_cashback_enabled = inner.is_cashback_enabled;
448    put_pumpfun_quote_mint_if_set(&mut base.quote_mint, inner.quote_mint);
449    put_pk_if_set(&mut base.quote_vault, inner.quote_vault);
450    put_pk_if_set(&mut base.quote_token_program, inner.quote_token_program);
451    put_u64_if_nonzero(&mut base.virtual_quote_reserves, inner.virtual_quote_reserves);
452    put_u64_if_nonzero(&mut base.creator_fee_bps, inner.creator_fee_bps);
453    base.is_holder_reward |= inner.is_holder_reward;
454}
455
456/// 合并 PumpFun CreateV2 事件
457#[inline(always)]
458fn merge_pumpfun_create_v2(base: &mut PumpFunCreateV2TokenEvent, inner: PumpFunCreateV2TokenEvent) {
459    fill_str_if_empty(&mut base.name, &inner.name);
460    fill_str_if_empty(&mut base.symbol, &inner.symbol);
461    fill_str_if_empty(&mut base.uri, &inner.uri);
462    put_pk_if_set(&mut base.mint, inner.mint);
463    put_pk_if_set(&mut base.bonding_curve, inner.bonding_curve);
464    put_pk_if_set(&mut base.user, inner.user);
465    put_pk_if_set(&mut base.creator, inner.creator);
466    put_i64_if_nonzero(&mut base.timestamp, inner.timestamp);
467    put_u64_if_nonzero(&mut base.virtual_token_reserves, inner.virtual_token_reserves);
468    put_u64_if_nonzero(&mut base.virtual_sol_reserves, inner.virtual_sol_reserves);
469    put_u64_if_nonzero(&mut base.real_token_reserves, inner.real_token_reserves);
470    put_u64_if_nonzero(&mut base.token_total_supply, inner.token_total_supply);
471    put_pk_if_set(&mut base.token_program, inner.token_program);
472    base.is_mayhem_mode |= inner.is_mayhem_mode;
473    base.is_cashback_enabled |= inner.is_cashback_enabled;
474    put_pumpfun_quote_mint_if_set(&mut base.quote_mint, inner.quote_mint);
475    put_pk_if_set(&mut base.quote_vault, inner.quote_vault);
476    put_pk_if_set(&mut base.quote_token_program, inner.quote_token_program);
477    put_u64_if_nonzero(&mut base.virtual_quote_reserves, inner.virtual_quote_reserves);
478    put_pk_if_set(&mut base.mint_authority, inner.mint_authority);
479    put_pk_if_set(&mut base.associated_bonding_curve, inner.associated_bonding_curve);
480    put_pk_if_set(&mut base.global, inner.global);
481    put_pk_if_set(&mut base.system_program, inner.system_program);
482    put_pk_if_set(&mut base.associated_token_program, inner.associated_token_program);
483    put_pk_if_set(&mut base.mayhem_program_id, inner.mayhem_program_id);
484    put_pk_if_set(&mut base.global_params, inner.global_params);
485    put_pk_if_set(&mut base.sol_vault, inner.sol_vault);
486    put_pk_if_set(&mut base.mayhem_state, inner.mayhem_state);
487    put_pk_if_set(&mut base.mayhem_token_vault, inner.mayhem_token_vault);
488    put_pk_if_set(&mut base.event_authority, inner.event_authority);
489    put_pk_if_set(&mut base.program, inner.program);
490    put_pk_if_set(&mut base.observed_fee_recipient, inner.observed_fee_recipient);
491    put_u64_if_nonzero(&mut base.creator_fee_bps, inner.creator_fee_bps);
492    base.is_holder_reward |= inner.is_holder_reward;
493}
494
495/// 合并 PumpFun Migrate 事件
496#[inline(always)]
497fn merge_pumpfun_migrate(base: &mut PumpFunMigrateEvent, inner: PumpFunMigrateEvent) {
498    // Inner instruction 包含完整的 migrate 数据
499    base.user = inner.user;
500    base.mint = inner.mint;
501    base.mint_amount = inner.mint_amount;
502    base.sol_amount = inner.sol_amount;
503    base.pool_migration_fee = inner.pool_migration_fee;
504    base.bonding_curve = inner.bonding_curve;
505    base.timestamp = inner.timestamp;
506    base.pool = inner.pool;
507}
508
509#[inline(always)]
510fn merge_pumpswap_buy(base: &mut PumpSwapBuyEvent, inner: PumpSwapBuyEvent) {
511    let ix = std::mem::take(base);
512    *base = inner;
513    merge_pumpswap_buy_log_preferred(base, ix);
514}
515
516#[inline(always)]
517fn merge_pumpswap_sell(base: &mut PumpSwapSellEvent, inner: PumpSwapSellEvent) {
518    let ix = std::mem::take(base);
519    *base = inner;
520    merge_pumpswap_sell_log_preferred(base, ix);
521}
522
523// ============================================================================
524// 工具函数
525// ============================================================================
526
527/// 判断两个事件是否可以合并
528///
529/// 合并条件:
530/// 1. 都是同一个协议的事件
531/// 2. 事件类型兼容(例如 Trade 和 Buy 可以合并)
532/// 3. 来自同一个交易(signature 相同)
533#[inline(always)]
534pub fn can_merge(base: &DexEvent, inner: &DexEvent) -> bool {
535    // 检查 signature 是否相同
536    if base.metadata().signature != inner.metadata().signature {
537        return false;
538    }
539
540    // 检查事件类型是否兼容
541    match (base, inner) {
542        // PumpFun Trade 系列事件可以互相合并
543        (DexEvent::PumpFunTrade(_), DexEvent::PumpFunTrade(_))
544        | (DexEvent::PumpFunTrade(_), DexEvent::PumpFunBuy(_))
545        | (DexEvent::PumpFunTrade(_), DexEvent::PumpFunSell(_))
546        | (DexEvent::PumpFunTrade(_), DexEvent::PumpFunBuyExactSolIn(_))
547        | (DexEvent::PumpFunBuy(_), DexEvent::PumpFunTrade(_))
548        | (DexEvent::PumpFunBuy(_), DexEvent::PumpFunBuy(_))
549        | (DexEvent::PumpFunSell(_), DexEvent::PumpFunTrade(_))
550        | (DexEvent::PumpFunSell(_), DexEvent::PumpFunSell(_))
551        | (DexEvent::PumpFunBuyExactSolIn(_), DexEvent::PumpFunTrade(_))
552        | (DexEvent::PumpFunBuyExactSolIn(_), DexEvent::PumpFunBuyExactSolIn(_)) => true,
553
554        // PumpFun Create / CreateV2 可以合并
555        (DexEvent::PumpFunCreate(_), DexEvent::PumpFunCreate(_)) => true,
556        (DexEvent::PumpFunCreateV2(_), DexEvent::PumpFunCreateV2(_)) => true,
557
558        // PumpFun Migrate 可以合并
559        (DexEvent::PumpFunMigrate(_), DexEvent::PumpFunMigrate(_)) => true,
560
561        // 其他组合不支持合并
562        _ => false,
563    }
564}
565
566// ============================================================================
567// gRPC:日志优先 + 指令补充(Yellowstone 并行解析 log / ix)
568// ============================================================================
569
570#[inline(always)]
571fn fill_pk(to: &mut Pubkey, from: Pubkey) {
572    if *to == Pubkey::default() && from != Pubkey::default() {
573        *to = from;
574    }
575}
576
577#[inline(always)]
578fn fill_pumpfun_quote_mint(to: &mut Pubkey, from: Pubkey) {
579    let from = normalize_pumpfun_quote_mint(from);
580    if (*to == Pubkey::default() || is_pumpfun_solscan_sol_quote_mint(*to))
581        && from != Pubkey::default()
582    {
583        *to = from;
584    }
585}
586
587#[inline(always)]
588fn fill_str_if_empty(to: &mut String, from: &str) {
589    if to.is_empty() && !from.is_empty() {
590        to.push_str(from);
591    }
592}
593
594/// PumpFun Trade:**保留 `log` 侧全部链上事件数值与标志**(与 `TradeEvent` 日志一致),
595/// 仅用 `ix` 补齐默认的账户类字段;`is_created_buy` 若仅 ix 侧为 true 则置位(创建首买标记)。
596#[inline]
597fn merge_pumpfun_trade_log_preferred(log: &mut PumpFunTradeEvent, ix: PumpFunTradeEvent) {
598    fill_pk(&mut log.global, ix.global);
599    fill_pk(&mut log.bonding_curve, ix.bonding_curve);
600    fill_pk(&mut log.bonding_curve_v2, ix.bonding_curve_v2);
601    fill_pk(&mut log.associated_bonding_curve, ix.associated_bonding_curve);
602    fill_pk(&mut log.associated_user, ix.associated_user);
603    fill_pk(&mut log.system_program, ix.system_program);
604    fill_pk(&mut log.token_program, ix.token_program);
605    fill_pk(&mut log.quote_token_program, ix.quote_token_program);
606    fill_pk(&mut log.associated_token_program, ix.associated_token_program);
607    fill_pk(&mut log.creator_vault, ix.creator_vault);
608    fill_pk(&mut log.fee_recipient, ix.fee_recipient);
609    fill_pk(&mut log.creator, ix.creator);
610    fill_pumpfun_quote_mint(&mut log.quote_mint, ix.quote_mint);
611    fill_pk(&mut log.associated_quote_fee_recipient, ix.associated_quote_fee_recipient);
612    fill_pk(&mut log.buyback_fee_recipient, ix.buyback_fee_recipient);
613    fill_pk(
614        &mut log.associated_quote_buyback_fee_recipient,
615        ix.associated_quote_buyback_fee_recipient,
616    );
617    fill_pk(&mut log.associated_quote_bonding_curve, ix.associated_quote_bonding_curve);
618    fill_pk(&mut log.associated_quote_user, ix.associated_quote_user);
619    fill_pk(&mut log.associated_creator_vault, ix.associated_creator_vault);
620    fill_pk(&mut log.sharing_config, ix.sharing_config);
621    fill_pk(&mut log.event_authority, ix.event_authority);
622    fill_pk(&mut log.program, ix.program);
623    fill_pk(&mut log.global_volume_accumulator, ix.global_volume_accumulator);
624    fill_pk(&mut log.user_volume_accumulator, ix.user_volume_accumulator);
625    fill_pk(&mut log.associated_user_volume_accumulator, ix.associated_user_volume_accumulator);
626    fill_pk(&mut log.fee_config, ix.fee_config);
627    fill_pk(&mut log.fee_program, ix.fee_program);
628    if log.account.is_none() {
629        log.account = ix.account;
630    }
631    if log.ix_name.is_empty() && !ix.ix_name.is_empty() {
632        log.ix_name = ix.ix_name;
633    }
634    put_u64_if_nonzero(&mut log.amount, ix.amount);
635    put_u64_if_nonzero(&mut log.max_sol_cost, ix.max_sol_cost);
636    put_u64_if_nonzero(&mut log.min_sol_output, ix.min_sol_output);
637    put_u64_if_nonzero(&mut log.spendable_sol_in, ix.spendable_sol_in);
638    put_u64_if_nonzero(&mut log.spendable_quote_in, ix.spendable_quote_in);
639    put_u64_if_nonzero(&mut log.min_tokens_out, ix.min_tokens_out);
640    put_u64_if_nonzero(&mut log.quote_amount, ix.quote_amount);
641    put_u64_if_nonzero(&mut log.virtual_quote_reserves, ix.virtual_quote_reserves);
642    put_u64_if_nonzero(&mut log.real_quote_reserves, ix.real_quote_reserves);
643    if !log.is_created_buy && ix.is_created_buy {
644        log.is_created_buy = true;
645    }
646}
647
648#[inline]
649fn merge_pumpfun_create_log_preferred(
650    log: &mut PumpFunCreateTokenEvent,
651    ix: PumpFunCreateTokenEvent,
652) {
653    fill_str_if_empty(&mut log.name, &ix.name);
654    fill_str_if_empty(&mut log.symbol, &ix.symbol);
655    fill_str_if_empty(&mut log.uri, &ix.uri);
656    fill_pk(&mut log.bonding_curve, ix.bonding_curve);
657    fill_pk(&mut log.user, ix.user);
658    fill_pk(&mut log.creator, ix.creator);
659    fill_pk(&mut log.token_program, ix.token_program);
660    fill_pumpfun_quote_mint(&mut log.quote_mint, ix.quote_mint);
661    fill_pk(&mut log.quote_vault, ix.quote_vault);
662    fill_pk(&mut log.quote_token_program, ix.quote_token_program);
663    put_u64_if_nonzero(&mut log.virtual_quote_reserves, ix.virtual_quote_reserves);
664    if log.creator_fee_bps == 0 {
665        log.creator_fee_bps = ix.creator_fee_bps;
666    }
667    if log.ix_name.is_empty() && !ix.ix_name.is_empty() {
668        log.ix_name = ix.ix_name;
669    }
670    log.is_mayhem_mode |= ix.is_mayhem_mode;
671    log.is_cashback_enabled |= ix.is_cashback_enabled;
672    log.is_holder_reward |= ix.is_holder_reward;
673}
674
675#[inline]
676fn merge_pumpfun_create_v2_into_create_log_preferred(
677    log: &mut PumpFunCreateTokenEvent,
678    ix: PumpFunCreateV2TokenEvent,
679) {
680    fill_str_if_empty(&mut log.name, &ix.name);
681    fill_str_if_empty(&mut log.symbol, &ix.symbol);
682    fill_str_if_empty(&mut log.uri, &ix.uri);
683    fill_pk(&mut log.mint, ix.mint);
684    fill_pk(&mut log.bonding_curve, ix.bonding_curve);
685    fill_pk(&mut log.user, ix.user);
686    fill_pk(&mut log.creator, ix.creator);
687    if log.timestamp == 0 && ix.timestamp != 0 {
688        log.timestamp = ix.timestamp;
689    }
690    put_u64_if_nonzero(&mut log.virtual_token_reserves, ix.virtual_token_reserves);
691    put_u64_if_nonzero(&mut log.virtual_sol_reserves, ix.virtual_sol_reserves);
692    put_u64_if_nonzero(&mut log.real_token_reserves, ix.real_token_reserves);
693    put_u64_if_nonzero(&mut log.token_total_supply, ix.token_total_supply);
694    fill_pk(&mut log.token_program, ix.token_program);
695    fill_pumpfun_quote_mint(&mut log.quote_mint, ix.quote_mint);
696    fill_pk(&mut log.quote_vault, ix.quote_vault);
697    fill_pk(&mut log.quote_token_program, ix.quote_token_program);
698    put_u64_if_nonzero(&mut log.virtual_quote_reserves, ix.virtual_quote_reserves);
699    if log.creator_fee_bps == 0 {
700        log.creator_fee_bps = ix.creator_fee_bps;
701    }
702    if log.ix_name.is_empty() && !ix.ix_name.is_empty() {
703        log.ix_name = ix.ix_name;
704    }
705    log.is_mayhem_mode |= ix.is_mayhem_mode;
706    log.is_cashback_enabled |= ix.is_cashback_enabled;
707    log.is_holder_reward |= ix.is_holder_reward;
708}
709
710#[inline]
711fn merge_pumpfun_create_into_create_v2_log_preferred(
712    log: &mut PumpFunCreateV2TokenEvent,
713    ix: PumpFunCreateTokenEvent,
714) {
715    fill_str_if_empty(&mut log.name, &ix.name);
716    fill_str_if_empty(&mut log.symbol, &ix.symbol);
717    fill_str_if_empty(&mut log.uri, &ix.uri);
718    fill_pk(&mut log.mint, ix.mint);
719    fill_pk(&mut log.bonding_curve, ix.bonding_curve);
720    fill_pk(&mut log.user, ix.user);
721    fill_pk(&mut log.creator, ix.creator);
722    if log.timestamp == 0 && ix.timestamp != 0 {
723        log.timestamp = ix.timestamp;
724    }
725    put_u64_if_nonzero(&mut log.virtual_token_reserves, ix.virtual_token_reserves);
726    put_u64_if_nonzero(&mut log.virtual_sol_reserves, ix.virtual_sol_reserves);
727    put_u64_if_nonzero(&mut log.real_token_reserves, ix.real_token_reserves);
728    put_u64_if_nonzero(&mut log.token_total_supply, ix.token_total_supply);
729    fill_pk(&mut log.token_program, ix.token_program);
730    fill_pumpfun_quote_mint(&mut log.quote_mint, ix.quote_mint);
731    fill_pk(&mut log.quote_vault, ix.quote_vault);
732    fill_pk(&mut log.quote_token_program, ix.quote_token_program);
733    put_u64_if_nonzero(&mut log.virtual_quote_reserves, ix.virtual_quote_reserves);
734    if log.creator_fee_bps == 0 {
735        log.creator_fee_bps = ix.creator_fee_bps;
736    }
737    if log.ix_name.is_empty() && !ix.ix_name.is_empty() {
738        log.ix_name = ix.ix_name;
739    }
740    log.is_mayhem_mode |= ix.is_mayhem_mode;
741    log.is_cashback_enabled |= ix.is_cashback_enabled;
742    log.is_holder_reward |= ix.is_holder_reward;
743}
744
745#[inline]
746fn merge_pumpfun_create_v2_log_preferred(
747    log: &mut PumpFunCreateV2TokenEvent,
748    ix: PumpFunCreateV2TokenEvent,
749) {
750    fill_str_if_empty(&mut log.name, &ix.name);
751    fill_str_if_empty(&mut log.symbol, &ix.symbol);
752    fill_str_if_empty(&mut log.uri, &ix.uri);
753    fill_pk(&mut log.bonding_curve, ix.bonding_curve);
754    fill_pk(&mut log.user, ix.user);
755    fill_pk(&mut log.creator, ix.creator);
756    fill_pk(&mut log.token_program, ix.token_program);
757    fill_pumpfun_quote_mint(&mut log.quote_mint, ix.quote_mint);
758    fill_pk(&mut log.quote_vault, ix.quote_vault);
759    fill_pk(&mut log.quote_token_program, ix.quote_token_program);
760    put_u64_if_nonzero(&mut log.virtual_quote_reserves, ix.virtual_quote_reserves);
761    if log.creator_fee_bps == 0 {
762        log.creator_fee_bps = ix.creator_fee_bps;
763    }
764    if log.ix_name.is_empty() && !ix.ix_name.is_empty() {
765        log.ix_name = ix.ix_name;
766    }
767    fill_pk(&mut log.mint_authority, ix.mint_authority);
768    fill_pk(&mut log.associated_bonding_curve, ix.associated_bonding_curve);
769    fill_pk(&mut log.global, ix.global);
770    fill_pk(&mut log.system_program, ix.system_program);
771    fill_pk(&mut log.associated_token_program, ix.associated_token_program);
772    fill_pk(&mut log.mayhem_program_id, ix.mayhem_program_id);
773    fill_pk(&mut log.global_params, ix.global_params);
774    fill_pk(&mut log.sol_vault, ix.sol_vault);
775    fill_pk(&mut log.mayhem_state, ix.mayhem_state);
776    fill_pk(&mut log.mayhem_token_vault, ix.mayhem_token_vault);
777    fill_pk(&mut log.event_authority, ix.event_authority);
778    fill_pk(&mut log.program, ix.program);
779    fill_pk(&mut log.observed_fee_recipient, ix.observed_fee_recipient);
780    log.is_holder_reward |= ix.is_holder_reward;
781}
782
783#[inline]
784fn merge_pumpfun_migrate_log_preferred(log: &mut PumpFunMigrateEvent, ix: PumpFunMigrateEvent) {
785    fill_pk(&mut log.bonding_curve, ix.bonding_curve);
786    fill_pk(&mut log.pool, ix.pool);
787    fill_pk(&mut log.user, ix.user);
788}
789
790#[inline]
791fn merge_pumpswap_trade_log_preferred(log: &mut PumpSwapTradeEvent, ix: PumpSwapTradeEvent) {
792    if log.ix_name.is_empty() && !ix.ix_name.is_empty() {
793        log.ix_name = ix.ix_name;
794    }
795}
796
797#[inline]
798fn merge_pumpswap_buy_log_preferred(log: &mut PumpSwapBuyEvent, ix: PumpSwapBuyEvent) {
799    fill_pk(&mut log.user_base_token_account, ix.user_base_token_account);
800    fill_pk(&mut log.user_quote_token_account, ix.user_quote_token_account);
801    fill_pk(&mut log.protocol_fee_recipient, ix.protocol_fee_recipient);
802    fill_pk(&mut log.protocol_fee_recipient_token_account, ix.protocol_fee_recipient_token_account);
803    fill_pk(&mut log.coin_creator, ix.coin_creator);
804    fill_pk(&mut log.base_mint, ix.base_mint);
805    fill_pk(&mut log.quote_mint, ix.quote_mint);
806    fill_pk(&mut log.pool_base_token_account, ix.pool_base_token_account);
807    fill_pk(&mut log.pool_quote_token_account, ix.pool_quote_token_account);
808    fill_pk(&mut log.coin_creator_vault_ata, ix.coin_creator_vault_ata);
809    fill_pk(&mut log.coin_creator_vault_authority, ix.coin_creator_vault_authority);
810    fill_pk(&mut log.base_token_program, ix.base_token_program);
811    fill_pk(&mut log.quote_token_program, ix.quote_token_program);
812    fill_pk(&mut log.pool_v2, ix.pool_v2);
813    fill_pk(&mut log.fee_recipient, ix.fee_recipient);
814    fill_pk(&mut log.fee_recipient_quote_token_account, ix.fee_recipient_quote_token_account);
815    if log.ix_name.is_empty() && !ix.ix_name.is_empty() {
816        log.ix_name = ix.ix_name;
817    }
818}
819
820#[inline]
821fn merge_pumpswap_sell_log_preferred(log: &mut PumpSwapSellEvent, ix: PumpSwapSellEvent) {
822    fill_pk(&mut log.user_base_token_account, ix.user_base_token_account);
823    fill_pk(&mut log.user_quote_token_account, ix.user_quote_token_account);
824    fill_pk(&mut log.protocol_fee_recipient, ix.protocol_fee_recipient);
825    fill_pk(&mut log.protocol_fee_recipient_token_account, ix.protocol_fee_recipient_token_account);
826    fill_pk(&mut log.coin_creator, ix.coin_creator);
827    fill_pk(&mut log.base_mint, ix.base_mint);
828    fill_pk(&mut log.quote_mint, ix.quote_mint);
829    fill_pk(&mut log.pool_base_token_account, ix.pool_base_token_account);
830    fill_pk(&mut log.pool_quote_token_account, ix.pool_quote_token_account);
831    fill_pk(&mut log.coin_creator_vault_ata, ix.coin_creator_vault_ata);
832    fill_pk(&mut log.coin_creator_vault_authority, ix.coin_creator_vault_authority);
833    fill_pk(&mut log.base_token_program, ix.base_token_program);
834    fill_pk(&mut log.quote_token_program, ix.quote_token_program);
835    fill_pk(&mut log.pool_v2, ix.pool_v2);
836    fill_pk(&mut log.fee_recipient, ix.fee_recipient);
837    fill_pk(&mut log.fee_recipient_quote_token_account, ix.fee_recipient_quote_token_account);
838}
839
840#[inline]
841fn merge_raydium_clmm_swap_log_preferred(log: &mut RaydiumClmmSwapEvent, ix: RaydiumClmmSwapEvent) {
842    fill_pk(&mut log.token_account_0, ix.token_account_0);
843    fill_pk(&mut log.token_account_1, ix.token_account_1);
844    fill_pk(&mut log.sender, ix.sender);
845    fill_pk(&mut log.amm_config, ix.amm_config);
846    fill_pk(&mut log.input_vault, ix.input_vault);
847    fill_pk(&mut log.output_vault, ix.output_vault);
848    fill_pk(&mut log.observation_state, ix.observation_state);
849    fill_pk(&mut log.input_mint, ix.input_mint);
850    fill_pk(&mut log.output_mint, ix.output_mint);
851    if log.tick_array_bitmap_extension.is_none() {
852        log.tick_array_bitmap_extension = ix.tick_array_bitmap_extension;
853    }
854    if log.tick_arrays.is_empty() && !ix.tick_arrays.is_empty() {
855        log.tick_arrays = ix.tick_arrays;
856    }
857}
858
859#[inline]
860fn merge_raydium_amm_v4_swap_log_preferred(
861    log: &mut RaydiumAmmV4SwapEvent,
862    ix: RaydiumAmmV4SwapEvent,
863) {
864    fill_pk(&mut log.token_program, ix.token_program);
865    fill_pk(&mut log.amm_authority, ix.amm_authority);
866    fill_pk(&mut log.amm_open_orders, ix.amm_open_orders);
867    if let Some(ref o) = ix.amm_target_orders {
868        if log.amm_target_orders.is_none() {
869            log.amm_target_orders = Some(*o);
870        }
871    }
872    fill_pk(&mut log.pool_coin_token_account, ix.pool_coin_token_account);
873    fill_pk(&mut log.pool_pc_token_account, ix.pool_pc_token_account);
874    fill_pk(&mut log.serum_program, ix.serum_program);
875    fill_pk(&mut log.serum_market, ix.serum_market);
876    fill_pk(&mut log.serum_bids, ix.serum_bids);
877    fill_pk(&mut log.serum_asks, ix.serum_asks);
878    fill_pk(&mut log.serum_event_queue, ix.serum_event_queue);
879    fill_pk(&mut log.serum_coin_vault_account, ix.serum_coin_vault_account);
880    fill_pk(&mut log.serum_pc_vault_account, ix.serum_pc_vault_account);
881    fill_pk(&mut log.serum_vault_signer, ix.serum_vault_signer);
882    fill_pk(&mut log.user_source_token_account, ix.user_source_token_account);
883    fill_pk(&mut log.user_destination_token_account, ix.user_destination_token_account);
884    fill_pk(&mut log.user_source_owner, ix.user_source_owner);
885    fill_pk(&mut log.amm, ix.amm);
886}
887
888#[inline]
889fn merge_pumpswap_create_pool_log_preferred(
890    log: &mut PumpSwapCreatePoolEvent,
891    ix: PumpSwapCreatePoolEvent,
892) {
893    fill_pk(&mut log.creator, ix.creator);
894    fill_pk(&mut log.pool, ix.pool);
895    fill_pk(&mut log.lp_mint, ix.lp_mint);
896    fill_pk(&mut log.user_base_token_account, ix.user_base_token_account);
897    fill_pk(&mut log.user_quote_token_account, ix.user_quote_token_account);
898    fill_pk(&mut log.coin_creator, ix.coin_creator);
899    log.is_mayhem_mode |= ix.is_mayhem_mode;
900    log.is_cashback_coin |= ix.is_cashback_coin;
901    if log.creator_fee_bps == 0 {
902        log.creator_fee_bps = ix.creator_fee_bps;
903    }
904    log.can_edit_creator_fee |= ix.can_edit_creator_fee;
905    log.is_holder_reward |= ix.is_holder_reward;
906}
907
908#[inline]
909fn merge_pumpswap_liquidity_added_log_preferred(
910    log: &mut PumpSwapLiquidityAdded,
911    ix: PumpSwapLiquidityAdded,
912) {
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.user_pool_token_account, ix.user_pool_token_account);
916}
917
918#[inline]
919fn merge_pumpswap_liquidity_removed_log_preferred(
920    log: &mut PumpSwapLiquidityRemoved,
921    ix: PumpSwapLiquidityRemoved,
922) {
923    fill_pk(&mut log.user_base_token_account, ix.user_base_token_account);
924    fill_pk(&mut log.user_quote_token_account, ix.user_quote_token_account);
925    fill_pk(&mut log.user_pool_token_account, ix.user_pool_token_account);
926}
927
928#[inline]
929fn merge_raydium_launchlab_pool_create_log_preferred(
930    log: &mut RaydiumLaunchlabPoolCreateEvent,
931    ix: RaydiumLaunchlabPoolCreateEvent,
932) {
933    fill_pk(&mut log.payer, ix.payer);
934    fill_pk(&mut log.creator, ix.creator);
935    fill_pk(&mut log.global_config, ix.global_config);
936    fill_pk(&mut log.platform_config, ix.platform_config);
937    fill_pk(&mut log.base_mint, ix.base_mint);
938    fill_pk(&mut log.quote_mint, ix.quote_mint);
939    fill_pk(&mut log.base_vault, ix.base_vault);
940    fill_pk(&mut log.quote_vault, ix.quote_vault);
941    fill_pk(&mut log.base_token_program, ix.base_token_program);
942    fill_pk(&mut log.quote_token_program, ix.quote_token_program);
943    fill_str_if_empty(&mut log.base_mint_param.name, &ix.base_mint_param.name);
944    fill_str_if_empty(&mut log.base_mint_param.symbol, &ix.base_mint_param.symbol);
945    fill_str_if_empty(&mut log.base_mint_param.uri, &ix.base_mint_param.uri);
946}
947
948#[inline]
949fn merge_raydium_launchlab_migrate_amm_log_preferred(
950    log: &mut RaydiumLaunchlabMigrateAmmEvent,
951    ix: RaydiumLaunchlabMigrateAmmEvent,
952) {
953    fill_pk(&mut log.old_pool, ix.old_pool);
954    fill_pk(&mut log.new_pool, ix.new_pool);
955    fill_pk(&mut log.user, ix.user);
956}
957
958#[inline]
959fn merge_raydium_launchlab_trade_log_preferred(
960    log: &mut RaydiumLaunchlabTradeEvent,
961    ix: RaydiumLaunchlabTradeEvent,
962) {
963    fill_pk(&mut log.user, ix.user);
964    fill_pk(&mut log.global_config, ix.global_config);
965    fill_pk(&mut log.platform_config, ix.platform_config);
966    fill_pk(&mut log.user_base_token, ix.user_base_token);
967    fill_pk(&mut log.user_quote_token, ix.user_quote_token);
968    fill_pk(&mut log.base_vault, ix.base_vault);
969    fill_pk(&mut log.quote_vault, ix.quote_vault);
970    fill_pk(&mut log.base_mint, ix.base_mint);
971    fill_pk(&mut log.quote_mint, ix.quote_mint);
972    fill_pk(&mut log.base_token_program, ix.base_token_program);
973    fill_pk(&mut log.quote_token_program, ix.quote_token_program);
974    fill_pk(&mut log.system_program, ix.system_program);
975    fill_pk(&mut log.platform_associated_account, ix.platform_associated_account);
976    fill_pk(&mut log.creator_associated_account, ix.creator_associated_account);
977}
978
979#[inline]
980fn merge_meteora_dlmm_swap_log_preferred(log: &mut MeteoraDlmmSwapEvent, ix: MeteoraDlmmSwapEvent) {
981    fill_pk(&mut log.user_token_in, ix.user_token_in);
982    fill_pk(&mut log.user_token_out, ix.user_token_out);
983    if log.min_amount_out == 0 {
984        log.min_amount_out = ix.min_amount_out;
985    }
986}
987
988/// 将 **instruction 路径**解析结果合并进 **log 路径**事件:`log` 保留链上日志权威数值,
989/// `ix` 仅填补 `log` 中为默认值的账户等字段。**不替换** `log` 外层枚举变体。
990///
991/// 已覆盖与 [`crate::grpc::log_instr_dedup`] 去重键一致的主要类型:PumpFun 全系、PumpSwap
992///(Trade/Buy/Sell/CreatePool/加减流动性)、RaydiumLaunchlab(Trade/PoolCreate/Migrate)、Raydium CLMM/AMM V4 Swap、Meteora DLMM Swap。
993pub fn merge_grpc_instruction_into_log(log: &mut DexEvent, ix: DexEvent) {
994    use DexEvent::*;
995    match log {
996        PumpFunTrade(l) => {
997            if let Some(i) = pumpfun_trade_from_ix_variant(ix) {
998                merge_pumpfun_trade_log_preferred(l, i);
999            }
1000        }
1001        PumpFunBuy(l) => {
1002            if let Some(i) = pumpfun_trade_from_ix_variant(ix) {
1003                merge_pumpfun_trade_log_preferred(l, i);
1004            }
1005        }
1006        PumpFunSell(l) => {
1007            if let Some(i) = pumpfun_trade_from_ix_variant(ix) {
1008                merge_pumpfun_trade_log_preferred(l, i);
1009            }
1010        }
1011        PumpFunBuyExactSolIn(l) => {
1012            if let Some(i) = pumpfun_trade_from_ix_variant(ix) {
1013                merge_pumpfun_trade_log_preferred(l, i);
1014            }
1015        }
1016        PumpFunCreate(l) => match ix {
1017            DexEvent::PumpFunCreate(i) => merge_pumpfun_create_log_preferred(l, i),
1018            DexEvent::PumpFunCreateV2(i) => merge_pumpfun_create_v2_into_create_log_preferred(l, i),
1019            _ => {}
1020        },
1021        PumpFunCreateV2(l) => match ix {
1022            DexEvent::PumpFunCreate(i) => merge_pumpfun_create_into_create_v2_log_preferred(l, i),
1023            DexEvent::PumpFunCreateV2(i) => merge_pumpfun_create_v2_log_preferred(l, i),
1024            _ => {}
1025        },
1026        PumpFunMigrate(l) => {
1027            if let DexEvent::PumpFunMigrate(i) = ix {
1028                merge_pumpfun_migrate_log_preferred(l, i);
1029            }
1030        }
1031        PumpSwapTrade(l) => {
1032            if let PumpSwapTrade(i) = ix {
1033                merge_pumpswap_trade_log_preferred(l, i);
1034            }
1035        }
1036        PumpSwapBuy(l) => {
1037            if let PumpSwapBuy(i) = ix {
1038                merge_pumpswap_buy_log_preferred(l, i);
1039            }
1040        }
1041        PumpSwapSell(l) => {
1042            if let PumpSwapSell(i) = ix {
1043                merge_pumpswap_sell_log_preferred(l, i);
1044            }
1045        }
1046        RaydiumClmmSwap(l) => {
1047            if let RaydiumClmmSwap(i) = ix {
1048                merge_raydium_clmm_swap_log_preferred(l, i);
1049            }
1050        }
1051        RaydiumAmmV4Swap(l) => {
1052            if let RaydiumAmmV4Swap(i) = ix {
1053                merge_raydium_amm_v4_swap_log_preferred(l, i);
1054            }
1055        }
1056        RaydiumLaunchlabTrade(l) => {
1057            if let RaydiumLaunchlabTrade(i) = ix {
1058                merge_raydium_launchlab_trade_log_preferred(l, i);
1059            }
1060        }
1061        RaydiumLaunchlabPoolCreate(l) => {
1062            if let RaydiumLaunchlabPoolCreate(i) = ix {
1063                merge_raydium_launchlab_pool_create_log_preferred(l, i);
1064            }
1065        }
1066        RaydiumLaunchlabMigrateAmm(l) => {
1067            if let RaydiumLaunchlabMigrateAmm(i) = ix {
1068                merge_raydium_launchlab_migrate_amm_log_preferred(l, i);
1069            }
1070        }
1071        PumpSwapCreatePool(l) => {
1072            if let PumpSwapCreatePool(i) = ix {
1073                merge_pumpswap_create_pool_log_preferred(l, i);
1074            }
1075        }
1076        PumpSwapLiquidityAdded(l) => {
1077            if let PumpSwapLiquidityAdded(i) = ix {
1078                merge_pumpswap_liquidity_added_log_preferred(l, i);
1079            }
1080        }
1081        PumpSwapLiquidityRemoved(l) => {
1082            if let PumpSwapLiquidityRemoved(i) = ix {
1083                merge_pumpswap_liquidity_removed_log_preferred(l, i);
1084            }
1085        }
1086        MeteoraDlmmSwap(l) => {
1087            if let MeteoraDlmmSwap(i) = ix {
1088                merge_meteora_dlmm_swap_log_preferred(l, i);
1089            }
1090        }
1091        _ => {}
1092    }
1093}
1094
1095#[inline]
1096fn pumpfun_trade_from_ix_variant(ix: DexEvent) -> Option<PumpFunTradeEvent> {
1097    match ix {
1098        DexEvent::PumpFunTrade(t)
1099        | DexEvent::PumpFunBuy(t)
1100        | DexEvent::PumpFunSell(t)
1101        | DexEvent::PumpFunBuyExactSolIn(t) => Some(t),
1102        _ => None,
1103    }
1104}
1105
1106#[cfg(test)]
1107mod tests {
1108    use super::*;
1109
1110    #[test]
1111    fn incompatible_merge_leaves_both_events_unchanged() {
1112        let mut base = DexEvent::Error("base".to_string());
1113        let inner = DexEvent::Error("inner".to_string());
1114        let mut unmerged = None;
1115
1116        assert!(!try_merge_events(&mut base, inner, &mut unmerged));
1117        assert!(matches!(base, DexEvent::Error(ref message) if message == "base"));
1118        assert!(matches!(unmerged, Some(DexEvent::Error(ref message)) if message == "inner"));
1119    }
1120    use solana_sdk::{pubkey::Pubkey, signature::Signature};
1121
1122    fn dlmm_swap(min_amount_out: u64, amount_out: u64) -> MeteoraDlmmSwapEvent {
1123        MeteoraDlmmSwapEvent {
1124            metadata: EventMetadata::default(),
1125            token_x_mint: Pubkey::default(),
1126            token_y_mint: Pubkey::default(),
1127            user_token_in: Pubkey::new_unique(),
1128            user_token_out: Pubkey::new_unique(),
1129            min_amount_out,
1130            pool: Pubkey::new_unique(),
1131            from: Pubkey::new_unique(),
1132            start_bin_id: 0,
1133            end_bin_id: 0,
1134            amount_in: 200,
1135            amount_out,
1136            swap_for_y: true,
1137            fee: 1,
1138            protocol_fee: 0,
1139            fee_bps: 25,
1140            host_fee: 0,
1141        ..Default::default()
1142        }
1143    }
1144
1145    #[test]
1146    fn dlmm_event_merge_keeps_instruction_threshold_and_executed_output() {
1147        let base_event = dlmm_swap(100, 0);
1148        let expected_user_token_in = base_event.user_token_in;
1149        let expected_user_token_out = base_event.user_token_out;
1150        let mut inner_event = dlmm_swap(0, 125);
1151        inner_event.user_token_in = Pubkey::default();
1152        inner_event.user_token_out = Pubkey::default();
1153        let mut base = DexEvent::MeteoraDlmmSwap(base_event);
1154        let inner = DexEvent::MeteoraDlmmSwap(inner_event);
1155
1156        assert!(try_merge_events(&mut base, inner, &mut None));
1157        let DexEvent::MeteoraDlmmSwap(event) = base else { panic!("swap") };
1158        assert_eq!(event.min_amount_out, 100);
1159        assert_eq!(event.amount_out, 125);
1160        assert_eq!(event.user_token_in, expected_user_token_in);
1161        assert_eq!(event.user_token_out, expected_user_token_out);
1162    }
1163
1164    #[test]
1165    fn grpc_dlmm_merge_keeps_log_output_and_adds_instruction_threshold() {
1166        let mut log_event = dlmm_swap(0, 125);
1167        log_event.user_token_in = Pubkey::default();
1168        log_event.user_token_out = Pubkey::default();
1169        let instruction_event = dlmm_swap(100, 0);
1170        let expected_user_token_in = instruction_event.user_token_in;
1171        let expected_user_token_out = instruction_event.user_token_out;
1172        let mut log = DexEvent::MeteoraDlmmSwap(log_event);
1173        let instruction = DexEvent::MeteoraDlmmSwap(instruction_event);
1174
1175        merge_grpc_instruction_into_log(&mut log, instruction);
1176        let DexEvent::MeteoraDlmmSwap(event) = log else { panic!("swap") };
1177        assert_eq!(event.min_amount_out, 100);
1178        assert_eq!(event.amount_out, 125);
1179        assert_eq!(event.user_token_in, expected_user_token_in);
1180        assert_eq!(event.user_token_out, expected_user_token_out);
1181    }
1182
1183    #[test]
1184    fn test_merge_pumpfun_trade() {
1185        let metadata = EventMetadata {
1186            signature: Signature::default(),
1187            slot: 100,
1188            tx_index: 1,
1189            block_time_us: 1000,
1190            grpc_recv_us: 2000,
1191            recent_blockhash: None,
1192        };
1193
1194        // Base event 来自 instruction(包含账户上下文)
1195        let mut base = DexEvent::PumpFunTrade(PumpFunTradeEvent {
1196            metadata: metadata.clone(),
1197            bonding_curve: Pubkey::new_unique(),
1198            associated_bonding_curve: Pubkey::new_unique(),
1199            ..Default::default()
1200        });
1201
1202        // Inner event 来自 inner instruction(包含交易数据)
1203        let inner = DexEvent::PumpFunTrade(PumpFunTradeEvent {
1204            metadata: metadata.clone(),
1205            mint: Pubkey::new_unique(),
1206            sol_amount: 1000,
1207            token_amount: 2000,
1208            is_buy: true,
1209            user: Pubkey::new_unique(),
1210            holder_rewards_bps: 300,
1211            holder_rewards: 400,
1212            ..Default::default()
1213        });
1214
1215        // 合并
1216        merge_events(&mut base, inner);
1217
1218        // 验证合并结果
1219        if let DexEvent::PumpFunTrade(trade) = base {
1220            assert_eq!(trade.sol_amount, 1000);
1221            assert_eq!(trade.token_amount, 2000);
1222            assert!(trade.is_buy);
1223            assert_eq!(trade.holder_rewards_bps, 300);
1224            assert_eq!(trade.holder_rewards, 400);
1225            // 账户上下文保留
1226            assert_ne!(trade.bonding_curve, Pubkey::default());
1227            assert_ne!(trade.associated_bonding_curve, Pubkey::default());
1228        } else {
1229            panic!("Expected PumpFunTrade event");
1230        }
1231    }
1232
1233    #[test]
1234    fn merge_pumpfun_trade_non_leg_propagates_holder_rewards() {
1235        let mut base = DexEvent::PumpFunTrade(PumpFunTradeEvent::default());
1236        let inner = DexEvent::PumpFunTrade(PumpFunTradeEvent {
1237            holder_rewards_bps: 300,
1238            holder_rewards: 400,
1239            ..Default::default()
1240        });
1241
1242        merge_events(&mut base, inner);
1243
1244        let DexEvent::PumpFunTrade(trade) = base else {
1245            panic!("expected PumpFunTrade event");
1246        };
1247        assert_eq!(trade.holder_rewards_bps, 300);
1248        assert_eq!(trade.holder_rewards, 400);
1249    }
1250
1251    #[test]
1252    fn merge_preserves_instruction_context_when_log_tail_is_absent() {
1253        let metadata = EventMetadata {
1254            signature: Signature::default(),
1255            slot: 100,
1256            tx_index: 1,
1257            block_time_us: 1000,
1258            grpc_recv_us: 2000,
1259            recent_blockhash: None,
1260        };
1261        let quote_mint = Pubkey::new_unique();
1262        let associated_quote_user = Pubkey::new_unique();
1263
1264        let mut base = DexEvent::PumpFunTrade(PumpFunTradeEvent {
1265            metadata: metadata.clone(),
1266            ix_name: "buy_exact_quote_in".to_string(),
1267            quote_mint,
1268            spendable_quote_in: 1_000,
1269            min_tokens_out: 2_000,
1270            associated_quote_user,
1271            ..Default::default()
1272        });
1273
1274        let inner = DexEvent::PumpFunBuy(PumpFunTradeEvent {
1275            metadata,
1276            sol_amount: 1_000,
1277            token_amount: 2_000,
1278            is_buy: true,
1279            ..Default::default()
1280        });
1281
1282        merge_events(&mut base, inner);
1283
1284        if let DexEvent::PumpFunTrade(t) = base {
1285            assert_eq!(t.sol_amount, 1_000);
1286            assert_eq!(t.token_amount, 2_000);
1287            assert_eq!(t.ix_name, "buy_exact_quote_in");
1288            assert_eq!(t.quote_mint, quote_mint);
1289            assert_eq!(t.spendable_quote_in, 1_000);
1290            assert_eq!(t.min_tokens_out, 2_000);
1291            assert_eq!(t.associated_quote_user, associated_quote_user);
1292        } else {
1293            panic!("Expected PumpFunTrade event");
1294        }
1295    }
1296
1297    #[test]
1298    fn merge_replaces_sol_quote_sentinel_with_real_quote_mint() {
1299        let quote_mint = Pubkey::new_unique();
1300        let mut base = DexEvent::PumpFunTrade(PumpFunTradeEvent {
1301            quote_mint: PUMPFUN_SOLSCAN_SOL_QUOTE_MINT,
1302            ..Default::default()
1303        });
1304
1305        let inner = DexEvent::PumpFunBuy(PumpFunTradeEvent { quote_mint, ..Default::default() });
1306
1307        merge_events(&mut base, inner);
1308
1309        if let DexEvent::PumpFunTrade(t) = base {
1310            assert_eq!(t.quote_mint, quote_mint);
1311        } else {
1312            panic!("Expected PumpFunTrade event");
1313        }
1314    }
1315
1316    #[test]
1317    fn merge_replaces_sol_quote_sentinel_with_wsol_mint() {
1318        let mut base = DexEvent::PumpFunTrade(PumpFunTradeEvent {
1319            quote_mint: PUMPFUN_SOLSCAN_SOL_QUOTE_MINT,
1320            ..Default::default()
1321        });
1322
1323        let inner = DexEvent::PumpFunBuy(PumpFunTradeEvent {
1324            quote_mint: PUMPFUN_WSOL_QUOTE_MINT,
1325            ..Default::default()
1326        });
1327
1328        merge_events(&mut base, inner);
1329
1330        if let DexEvent::PumpFunTrade(t) = base {
1331            assert_eq!(t.quote_mint, PUMPFUN_WSOL_QUOTE_MINT);
1332        } else {
1333            panic!("Expected PumpFunTrade event");
1334        }
1335    }
1336
1337    #[test]
1338    fn test_can_merge() {
1339        let metadata = EventMetadata {
1340            signature: Signature::default(),
1341            slot: 100,
1342            tx_index: 1,
1343            block_time_us: 1000,
1344            grpc_recv_us: 2000,
1345            recent_blockhash: None,
1346        };
1347
1348        let base = DexEvent::PumpFunTrade(PumpFunTradeEvent {
1349            metadata: metadata.clone(),
1350            ..Default::default()
1351        });
1352
1353        let inner = DexEvent::PumpFunBuy(PumpFunTradeEvent {
1354            metadata: metadata.clone(),
1355            ..Default::default()
1356        });
1357
1358        // 应该可以合并(同一个 signature,兼容类型)
1359        assert!(can_merge(&base, &inner));
1360
1361        // 不同 signature 不能合并
1362        let different_sig = DexEvent::PumpFunTrade(PumpFunTradeEvent {
1363            metadata: EventMetadata { signature: Signature::new_unique(), ..metadata },
1364            ..Default::default()
1365        });
1366
1367        assert!(!can_merge(&base, &different_sig));
1368    }
1369
1370    #[test]
1371    fn dlmm_position_event_keeps_instruction_only_fields() {
1372        let pool = Pubkey::new_unique();
1373        let position = Pubkey::new_unique();
1374        let owner = Pubkey::new_unique();
1375        let mut base = DexEvent::MeteoraDlmmCreatePosition(MeteoraDlmmCreatePositionEvent {
1376            metadata: EventMetadata::default(),
1377            pool,
1378            position,
1379            owner,
1380            lower_bin_id: -42,
1381            width: 70,
1382        });
1383        let inner = DexEvent::MeteoraDlmmCreatePosition(MeteoraDlmmCreatePositionEvent {
1384            metadata: EventMetadata::default(),
1385            pool,
1386            position,
1387            owner,
1388            lower_bin_id: 0,
1389            width: 0,
1390        });
1391
1392        assert!(try_merge_events(&mut base, inner, &mut None));
1393        let DexEvent::MeteoraDlmmCreatePosition(event) = base else { panic!("position") };
1394        assert_eq!(event.lower_bin_id, -42);
1395        assert_eq!(event.width, 70);
1396    }
1397
1398    #[test]
1399    fn grpc_merge_fills_fee_recipient_from_ix_when_log_default() {
1400        let metadata = EventMetadata {
1401            signature: Signature::default(),
1402            slot: 1,
1403            tx_index: 0,
1404            block_time_us: 0,
1405            grpc_recv_us: 0,
1406            recent_blockhash: None,
1407        };
1408        let fr = Pubkey::new_unique();
1409        let log_t =
1410            PumpFunTradeEvent { metadata: metadata.clone(), sol_amount: 50, ..Default::default() };
1411        let mut ix_t = log_t.clone();
1412        ix_t.fee_recipient = fr;
1413        ix_t.sol_amount = 777;
1414        let mut log_ev = DexEvent::PumpFunTrade(log_t);
1415        merge_grpc_instruction_into_log(&mut log_ev, DexEvent::PumpFunBuy(ix_t));
1416        match log_ev {
1417            DexEvent::PumpFunTrade(t) => {
1418                assert_eq!(t.fee_recipient, fr);
1419                assert_eq!(t.sol_amount, 50);
1420            }
1421            _ => panic!("expected trade"),
1422        }
1423    }
1424
1425    #[test]
1426    fn grpc_merge_keeps_log_trade_fields() {
1427        let metadata = EventMetadata {
1428            signature: Signature::default(),
1429            slot: 1,
1430            tx_index: 0,
1431            block_time_us: 0,
1432            grpc_recv_us: 0,
1433            recent_blockhash: None,
1434        };
1435        let log_t = PumpFunTradeEvent {
1436            metadata: metadata.clone(),
1437            mayhem_mode: true,
1438            sol_amount: 100,
1439            ..Default::default()
1440        };
1441        let mut ix_t = log_t.clone();
1442        ix_t.mayhem_mode = false;
1443        ix_t.sol_amount = 999;
1444
1445        let mut log_ev = DexEvent::PumpFunTrade(log_t);
1446        merge_grpc_instruction_into_log(&mut log_ev, DexEvent::PumpFunBuy(ix_t));
1447        match log_ev {
1448            DexEvent::PumpFunTrade(t) => {
1449                assert!(t.mayhem_mode);
1450                assert_eq!(t.sol_amount, 100);
1451            }
1452            _ => panic!("variant preserved"),
1453        }
1454    }
1455
1456    #[test]
1457    fn pumpfun_create_merge_propagates_holder_rewards_fields() {
1458        let mut base = DexEvent::PumpFunCreate(PumpFunCreateTokenEvent {
1459            creator_fee_bps: 125,
1460            ..Default::default()
1461        });
1462        let inner = DexEvent::PumpFunCreate(PumpFunCreateTokenEvent {
1463            creator_fee_bps: 300,
1464            is_holder_reward: true,
1465            ..Default::default()
1466        });
1467
1468        merge_events(&mut base, inner);
1469
1470        let DexEvent::PumpFunCreate(event) = base else { panic!("create") };
1471        assert_eq!(event.creator_fee_bps, 300);
1472        assert!(event.is_holder_reward);
1473    }
1474
1475    #[test]
1476    fn grpc_create_merge_keeps_log_fee_and_adds_instruction_holder_reward() {
1477        let mut log = DexEvent::PumpFunCreate(PumpFunCreateTokenEvent {
1478            creator_fee_bps: 300,
1479            ..Default::default()
1480        });
1481        let instruction = DexEvent::PumpFunCreateV2(PumpFunCreateV2TokenEvent {
1482            creator_fee_bps: 500,
1483            is_holder_reward: true,
1484            ..Default::default()
1485        });
1486
1487        merge_grpc_instruction_into_log(&mut log, instruction);
1488
1489        let DexEvent::PumpFunCreate(event) = log else { panic!("create") };
1490        assert_eq!(event.creator_fee_bps, 300);
1491        assert!(event.is_holder_reward);
1492    }
1493}