Skip to main content

sol_parser_sdk/core/
merger.rs

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