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