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