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