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