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_generic(b, i),
46        (PumpFunMigrate(b), PumpFunMigrate(i)) => merge_pumpfun_migrate(b, i),
47
48        // ========== PumpSwap 系列 ==========
49        (PumpSwapBuy(b), PumpSwapBuy(i)) => merge_generic(b, i),
50        (PumpSwapSell(b), PumpSwapSell(i)) => merge_generic(b, i),
51        (PumpSwapCreatePool(b), PumpSwapCreatePool(i)) => merge_generic(b, i),
52        (PumpSwapLiquidityAdded(b), PumpSwapLiquidityAdded(i)) => merge_generic(b, i),
53        (PumpSwapLiquidityRemoved(b), PumpSwapLiquidityRemoved(i)) => merge_generic(b, i),
54
55        // ========== Raydium CLMM 系列 ==========
56        (RaydiumClmmSwap(b), RaydiumClmmSwap(i)) => merge_generic(b, i),
57        (RaydiumClmmIncreaseLiquidity(b), RaydiumClmmIncreaseLiquidity(i)) => merge_generic(b, i),
58        (RaydiumClmmDecreaseLiquidity(b), RaydiumClmmDecreaseLiquidity(i)) => merge_generic(b, i),
59        (RaydiumClmmCreatePool(b), RaydiumClmmCreatePool(i)) => merge_generic(b, i),
60        (RaydiumClmmCollectFee(b), RaydiumClmmCollectFee(i)) => merge_generic(b, i),
61
62        // ========== Raydium CPMM 系列 ==========
63        (RaydiumCpmmSwap(b), RaydiumCpmmSwap(i)) => merge_generic(b, i),
64        (RaydiumCpmmDeposit(b), RaydiumCpmmDeposit(i)) => merge_generic(b, i),
65        (RaydiumCpmmWithdraw(b), RaydiumCpmmWithdraw(i)) => merge_generic(b, i),
66
67        // ========== Raydium AMM V4 系列 ==========
68        (RaydiumAmmV4Swap(b), RaydiumAmmV4Swap(i)) => merge_generic(b, i),
69        (RaydiumAmmV4Deposit(b), RaydiumAmmV4Deposit(i)) => merge_generic(b, i),
70        (RaydiumAmmV4Withdraw(b), RaydiumAmmV4Withdraw(i)) => merge_generic(b, i),
71
72        // ========== Orca Whirlpool 系列 ==========
73        (OrcaWhirlpoolSwap(b), OrcaWhirlpoolSwap(i)) => merge_generic(b, i),
74        (OrcaWhirlpoolLiquidityIncreased(b), OrcaWhirlpoolLiquidityIncreased(i)) => {
75            merge_generic(b, i)
76        }
77        (OrcaWhirlpoolLiquidityDecreased(b), OrcaWhirlpoolLiquidityDecreased(i)) => {
78            merge_generic(b, i)
79        }
80
81        // ========== Meteora Pools (AMM) 系列 ==========
82        (MeteoraPoolsSwap(b), MeteoraPoolsSwap(i)) => merge_generic(b, i),
83        (MeteoraPoolsAddLiquidity(b), MeteoraPoolsAddLiquidity(i)) => merge_generic(b, i),
84        (MeteoraPoolsRemoveLiquidity(b), MeteoraPoolsRemoveLiquidity(i)) => merge_generic(b, i),
85
86        // ========== Meteora DAMM V2 系列 ==========
87        (MeteoraDammV2Swap(b), MeteoraDammV2Swap(i)) => merge_generic(b, i),
88        (MeteoraDammV2AddLiquidity(b), MeteoraDammV2AddLiquidity(i)) => merge_generic(b, i),
89        (MeteoraDammV2RemoveLiquidity(b), MeteoraDammV2RemoveLiquidity(i)) => merge_generic(b, i),
90        (MeteoraDammV2CreatePosition(b), MeteoraDammV2CreatePosition(i)) => merge_generic(b, i),
91        (MeteoraDammV2ClosePosition(b), MeteoraDammV2ClosePosition(i)) => merge_generic(b, i),
92
93        // ========== Bonk 系列 ==========
94        (BonkTrade(b), BonkTrade(i)) => merge_generic(b, i),
95
96        // 其他组合不需要合并(类型不匹配)
97        _ => {}
98    }
99}
100
101/// 通用合并函数 - 对于大多数事件,inner instruction 包含完整数据
102///
103/// 这个函数简单地用 inner 的数据覆盖 base,因为:
104/// - Inner instruction 来自程序日志,包含完整的交易数据
105/// - Instruction 主要提供账户上下文
106/// - 对于大多数协议,inner instruction 的数据已经足够完整
107#[inline(always)]
108fn merge_generic<T>(base: &mut T, inner: T) {
109    *base = inner;
110}
111
112// ============================================================================
113// PumpFun 事件合并实现
114// ============================================================================
115
116#[inline(always)]
117fn put_pk_if_set(to: &mut Pubkey, from: Pubkey) {
118    if from != Pubkey::default() {
119        *to = from;
120    }
121}
122
123#[inline(always)]
124fn put_u64_if_nonzero(to: &mut u64, from: u64) {
125    if from != 0 {
126        *to = from;
127    }
128}
129
130#[inline(always)]
131fn put_i64_if_nonzero(to: &mut i64, from: i64) {
132    if from != 0 {
133        *to = from;
134    }
135}
136
137/// 合并 PumpFun Trade 事件
138///
139/// 合并策略:
140/// - Inner instruction 提供: 交易数据(amount, reserves, fees 等)
141/// - Instruction 提供: 账户上下文(bonding_curve, associated_bonding_curve 等)
142/// - 合并后: 完整的交易事件
143///
144/// 同一 outer 下多段 inner 链式合并时:若某段 inner 未带成交量(`sol_amount`/`token_amount` 均为 0),
145/// 则不再用其覆盖金额与储备,避免把前一段已合并好的数据清空。
146#[inline(always)]
147fn merge_pumpfun_trade(base: &mut PumpFunTradeEvent, inner: PumpFunTradeEvent) {
148    let leg = inner.sol_amount != 0 || inner.token_amount != 0;
149
150    put_pk_if_set(&mut base.mint, inner.mint);
151    put_pk_if_set(&mut base.user, inner.user);
152    put_pk_if_set(&mut base.fee_recipient, inner.fee_recipient);
153    put_pk_if_set(&mut base.creator, inner.creator);
154
155    if leg {
156        base.sol_amount = inner.sol_amount;
157        base.token_amount = inner.token_amount;
158        base.is_buy = inner.is_buy;
159        base.timestamp = inner.timestamp;
160        base.virtual_sol_reserves = inner.virtual_sol_reserves;
161        base.virtual_token_reserves = inner.virtual_token_reserves;
162        base.real_sol_reserves = inner.real_sol_reserves;
163        base.real_token_reserves = inner.real_token_reserves;
164        base.fee_basis_points = inner.fee_basis_points;
165        base.fee = inner.fee;
166        base.creator_fee_basis_points = inner.creator_fee_basis_points;
167        base.creator_fee = inner.creator_fee;
168        base.track_volume = inner.track_volume;
169        base.total_unclaimed_tokens = inner.total_unclaimed_tokens;
170        base.total_claimed_tokens = inner.total_claimed_tokens;
171        base.current_sol_volume = inner.current_sol_volume;
172        base.last_update_timestamp = inner.last_update_timestamp;
173        base.ix_name = inner.ix_name;
174        base.mayhem_mode = inner.mayhem_mode;
175        base.cashback_fee_basis_points = inner.cashback_fee_basis_points;
176        base.cashback = inner.cashback;
177        base.is_cashback_coin = inner.is_cashback_coin;
178    } else {
179        put_u64_if_nonzero(&mut base.fee, inner.fee);
180        put_u64_if_nonzero(&mut base.creator_fee, inner.creator_fee);
181        put_u64_if_nonzero(&mut base.fee_basis_points, inner.fee_basis_points);
182        put_u64_if_nonzero(&mut base.creator_fee_basis_points, inner.creator_fee_basis_points);
183        put_u64_if_nonzero(&mut base.virtual_sol_reserves, inner.virtual_sol_reserves);
184        put_u64_if_nonzero(&mut base.virtual_token_reserves, inner.virtual_token_reserves);
185        put_u64_if_nonzero(&mut base.real_sol_reserves, inner.real_sol_reserves);
186        put_u64_if_nonzero(&mut base.real_token_reserves, inner.real_token_reserves);
187        put_u64_if_nonzero(&mut base.total_unclaimed_tokens, inner.total_unclaimed_tokens);
188        put_u64_if_nonzero(&mut base.total_claimed_tokens, inner.total_claimed_tokens);
189        put_u64_if_nonzero(&mut base.current_sol_volume, inner.current_sol_volume);
190        put_u64_if_nonzero(&mut base.cashback_fee_basis_points, inner.cashback_fee_basis_points);
191        put_u64_if_nonzero(&mut base.cashback, inner.cashback);
192        put_i64_if_nonzero(&mut base.timestamp, inner.timestamp);
193        put_i64_if_nonzero(&mut base.last_update_timestamp, inner.last_update_timestamp);
194        if !inner.ix_name.is_empty() {
195            base.ix_name = inner.ix_name;
196        }
197        base.track_volume |= inner.track_volume;
198        base.mayhem_mode |= inner.mayhem_mode;
199        base.is_cashback_coin |= inner.is_cashback_coin;
200    }
201
202    base.is_created_buy |= inner.is_created_buy;
203    // 保留 base 的账户上下文字段(bonding_curve, associated_bonding_curve 等)
204}
205
206/// 合并 PumpFun Create 事件
207#[inline(always)]
208fn merge_pumpfun_create(base: &mut PumpFunCreateTokenEvent, inner: PumpFunCreateTokenEvent) {
209    // Inner instruction 包含完整的 create 数据
210    base.name = inner.name;
211    base.symbol = inner.symbol;
212    base.uri = inner.uri;
213    base.mint = inner.mint;
214    base.bonding_curve = inner.bonding_curve;
215    base.user = inner.user;
216    base.creator = inner.creator;
217    base.timestamp = inner.timestamp;
218    base.virtual_token_reserves = inner.virtual_token_reserves;
219    base.virtual_sol_reserves = inner.virtual_sol_reserves;
220    base.real_token_reserves = inner.real_token_reserves;
221    base.token_total_supply = inner.token_total_supply;
222    base.token_program = inner.token_program;
223    base.is_mayhem_mode = inner.is_mayhem_mode;
224}
225
226/// 合并 PumpFun Migrate 事件
227#[inline(always)]
228fn merge_pumpfun_migrate(base: &mut PumpFunMigrateEvent, inner: PumpFunMigrateEvent) {
229    // Inner instruction 包含完整的 migrate 数据
230    base.user = inner.user;
231    base.mint = inner.mint;
232    base.mint_amount = inner.mint_amount;
233    base.sol_amount = inner.sol_amount;
234    base.pool_migration_fee = inner.pool_migration_fee;
235    base.bonding_curve = inner.bonding_curve;
236    base.timestamp = inner.timestamp;
237    base.pool = inner.pool;
238}
239
240// ============================================================================
241// 工具函数
242// ============================================================================
243
244/// 判断两个事件是否可以合并
245///
246/// 合并条件:
247/// 1. 都是同一个协议的事件
248/// 2. 事件类型兼容(例如 Trade 和 Buy 可以合并)
249/// 3. 来自同一个交易(signature 相同)
250#[inline(always)]
251pub fn can_merge(base: &DexEvent, inner: &DexEvent) -> bool {
252    // 检查 signature 是否相同
253    if base.metadata().signature != inner.metadata().signature {
254        return false;
255    }
256
257    // 检查事件类型是否兼容
258    match (base, inner) {
259        // PumpFun Trade 系列事件可以互相合并
260        (DexEvent::PumpFunTrade(_), DexEvent::PumpFunTrade(_))
261        | (DexEvent::PumpFunTrade(_), DexEvent::PumpFunBuy(_))
262        | (DexEvent::PumpFunTrade(_), DexEvent::PumpFunSell(_))
263        | (DexEvent::PumpFunTrade(_), DexEvent::PumpFunBuyExactSolIn(_))
264        | (DexEvent::PumpFunBuy(_), DexEvent::PumpFunTrade(_))
265        | (DexEvent::PumpFunBuy(_), DexEvent::PumpFunBuy(_))
266        | (DexEvent::PumpFunSell(_), DexEvent::PumpFunTrade(_))
267        | (DexEvent::PumpFunSell(_), DexEvent::PumpFunSell(_))
268        | (DexEvent::PumpFunBuyExactSolIn(_), DexEvent::PumpFunTrade(_))
269        | (DexEvent::PumpFunBuyExactSolIn(_), DexEvent::PumpFunBuyExactSolIn(_)) => true,
270
271        // PumpFun Create / CreateV2 可以合并
272        (DexEvent::PumpFunCreate(_), DexEvent::PumpFunCreate(_)) => true,
273        (DexEvent::PumpFunCreateV2(_), DexEvent::PumpFunCreateV2(_)) => true,
274
275        // PumpFun Migrate 可以合并
276        (DexEvent::PumpFunMigrate(_), DexEvent::PumpFunMigrate(_)) => true,
277
278        // 其他组合不支持合并
279        _ => false,
280    }
281}
282
283// ============================================================================
284// gRPC:日志优先 + 指令补充(Yellowstone 并行解析 log / ix)
285// ============================================================================
286
287#[inline(always)]
288fn fill_pk(to: &mut Pubkey, from: Pubkey) {
289    if *to == Pubkey::default() && from != Pubkey::default() {
290        *to = from;
291    }
292}
293
294#[inline(always)]
295fn fill_str_if_empty(to: &mut String, from: &str) {
296    if to.is_empty() && !from.is_empty() {
297        to.push_str(from);
298    }
299}
300
301/// PumpFun Trade:**保留 `log` 侧全部链上事件数值与标志**(与 `TradeEvent` 日志一致),
302/// 仅用 `ix` 补齐默认的账户类字段;`is_created_buy` 若仅 ix 侧为 true 则置位(创建首买标记)。
303#[inline]
304fn merge_pumpfun_trade_log_preferred(log: &mut PumpFunTradeEvent, ix: PumpFunTradeEvent) {
305    fill_pk(&mut log.bonding_curve, ix.bonding_curve);
306    fill_pk(&mut log.associated_bonding_curve, ix.associated_bonding_curve);
307    fill_pk(&mut log.token_program, ix.token_program);
308    fill_pk(&mut log.creator_vault, ix.creator_vault);
309    fill_pk(&mut log.fee_recipient, ix.fee_recipient);
310    fill_pk(&mut log.creator, ix.creator);
311    if log.account.is_none() {
312        log.account = ix.account;
313    }
314    if log.ix_name.is_empty() && !ix.ix_name.is_empty() {
315        log.ix_name = ix.ix_name;
316    }
317    if !log.is_created_buy && ix.is_created_buy {
318        log.is_created_buy = true;
319    }
320}
321
322#[inline]
323fn merge_pumpfun_create_log_preferred(log: &mut PumpFunCreateTokenEvent, ix: PumpFunCreateTokenEvent) {
324    fill_str_if_empty(&mut log.name, &ix.name);
325    fill_str_if_empty(&mut log.symbol, &ix.symbol);
326    fill_str_if_empty(&mut log.uri, &ix.uri);
327    fill_pk(&mut log.bonding_curve, ix.bonding_curve);
328    fill_pk(&mut log.user, ix.user);
329    fill_pk(&mut log.creator, ix.creator);
330    fill_pk(&mut log.token_program, ix.token_program);
331}
332
333#[inline]
334fn merge_pumpfun_create_v2_log_preferred(
335    log: &mut PumpFunCreateV2TokenEvent,
336    ix: PumpFunCreateV2TokenEvent,
337) {
338    fill_str_if_empty(&mut log.name, &ix.name);
339    fill_str_if_empty(&mut log.symbol, &ix.symbol);
340    fill_str_if_empty(&mut log.uri, &ix.uri);
341    fill_pk(&mut log.bonding_curve, ix.bonding_curve);
342    fill_pk(&mut log.user, ix.user);
343    fill_pk(&mut log.creator, ix.creator);
344    fill_pk(&mut log.token_program, ix.token_program);
345    fill_pk(&mut log.mint_authority, ix.mint_authority);
346    fill_pk(&mut log.associated_bonding_curve, ix.associated_bonding_curve);
347    fill_pk(&mut log.global, ix.global);
348    fill_pk(&mut log.system_program, ix.system_program);
349    fill_pk(&mut log.associated_token_program, ix.associated_token_program);
350    fill_pk(&mut log.mayhem_program_id, ix.mayhem_program_id);
351    fill_pk(&mut log.global_params, ix.global_params);
352    fill_pk(&mut log.sol_vault, ix.sol_vault);
353    fill_pk(&mut log.mayhem_state, ix.mayhem_state);
354    fill_pk(&mut log.mayhem_token_vault, ix.mayhem_token_vault);
355    fill_pk(&mut log.event_authority, ix.event_authority);
356    fill_pk(&mut log.program, ix.program);
357    fill_pk(&mut log.observed_fee_recipient, ix.observed_fee_recipient);
358}
359
360#[inline]
361fn merge_pumpfun_migrate_log_preferred(log: &mut PumpFunMigrateEvent, ix: PumpFunMigrateEvent) {
362    fill_pk(&mut log.bonding_curve, ix.bonding_curve);
363    fill_pk(&mut log.pool, ix.pool);
364    fill_pk(&mut log.user, ix.user);
365}
366
367#[inline]
368fn merge_pumpswap_trade_log_preferred(log: &mut PumpSwapTradeEvent, ix: PumpSwapTradeEvent) {
369    if log.ix_name.is_empty() && !ix.ix_name.is_empty() {
370        log.ix_name = ix.ix_name;
371    }
372}
373
374#[inline]
375fn merge_pumpswap_buy_log_preferred(log: &mut PumpSwapBuyEvent, ix: PumpSwapBuyEvent) {
376    fill_pk(&mut log.user_base_token_account, ix.user_base_token_account);
377    fill_pk(&mut log.user_quote_token_account, ix.user_quote_token_account);
378    fill_pk(&mut log.protocol_fee_recipient, ix.protocol_fee_recipient);
379    fill_pk(
380        &mut log.protocol_fee_recipient_token_account,
381        ix.protocol_fee_recipient_token_account,
382    );
383    fill_pk(&mut log.coin_creator, ix.coin_creator);
384    fill_pk(&mut log.base_mint, ix.base_mint);
385    fill_pk(&mut log.quote_mint, ix.quote_mint);
386    fill_pk(&mut log.pool_base_token_account, ix.pool_base_token_account);
387    fill_pk(&mut log.pool_quote_token_account, ix.pool_quote_token_account);
388    fill_pk(&mut log.coin_creator_vault_ata, ix.coin_creator_vault_ata);
389    fill_pk(
390        &mut log.coin_creator_vault_authority,
391        ix.coin_creator_vault_authority,
392    );
393    fill_pk(&mut log.base_token_program, ix.base_token_program);
394    fill_pk(&mut log.quote_token_program, ix.quote_token_program);
395    if log.ix_name.is_empty() && !ix.ix_name.is_empty() {
396        log.ix_name = ix.ix_name;
397    }
398}
399
400#[inline]
401fn merge_pumpswap_sell_log_preferred(log: &mut PumpSwapSellEvent, ix: PumpSwapSellEvent) {
402    fill_pk(&mut log.user_base_token_account, ix.user_base_token_account);
403    fill_pk(&mut log.user_quote_token_account, ix.user_quote_token_account);
404    fill_pk(&mut log.protocol_fee_recipient, ix.protocol_fee_recipient);
405    fill_pk(
406        &mut log.protocol_fee_recipient_token_account,
407        ix.protocol_fee_recipient_token_account,
408    );
409    fill_pk(&mut log.coin_creator, ix.coin_creator);
410    fill_pk(&mut log.base_mint, ix.base_mint);
411    fill_pk(&mut log.quote_mint, ix.quote_mint);
412    fill_pk(&mut log.pool_base_token_account, ix.pool_base_token_account);
413    fill_pk(&mut log.pool_quote_token_account, ix.pool_quote_token_account);
414    fill_pk(&mut log.coin_creator_vault_ata, ix.coin_creator_vault_ata);
415    fill_pk(
416        &mut log.coin_creator_vault_authority,
417        ix.coin_creator_vault_authority,
418    );
419    fill_pk(&mut log.base_token_program, ix.base_token_program);
420    fill_pk(&mut log.quote_token_program, ix.quote_token_program);
421}
422
423#[inline]
424fn merge_raydium_clmm_swap_log_preferred(log: &mut RaydiumClmmSwapEvent, ix: RaydiumClmmSwapEvent) {
425    fill_pk(&mut log.token_account_0, ix.token_account_0);
426    fill_pk(&mut log.token_account_1, ix.token_account_1);
427    fill_pk(&mut log.sender, ix.sender);
428}
429
430#[inline]
431fn merge_raydium_amm_v4_swap_log_preferred(log: &mut RaydiumAmmV4SwapEvent, ix: RaydiumAmmV4SwapEvent) {
432    fill_pk(&mut log.token_program, ix.token_program);
433    fill_pk(&mut log.amm_authority, ix.amm_authority);
434    fill_pk(&mut log.amm_open_orders, ix.amm_open_orders);
435    if let Some(ref o) = ix.amm_target_orders {
436        if log.amm_target_orders.is_none() {
437            log.amm_target_orders = Some(*o);
438        }
439    }
440    fill_pk(&mut log.pool_coin_token_account, ix.pool_coin_token_account);
441    fill_pk(&mut log.pool_pc_token_account, ix.pool_pc_token_account);
442    fill_pk(&mut log.serum_program, ix.serum_program);
443    fill_pk(&mut log.serum_market, ix.serum_market);
444    fill_pk(&mut log.serum_bids, ix.serum_bids);
445    fill_pk(&mut log.serum_asks, ix.serum_asks);
446    fill_pk(&mut log.serum_event_queue, ix.serum_event_queue);
447    fill_pk(
448        &mut log.serum_coin_vault_account,
449        ix.serum_coin_vault_account,
450    );
451    fill_pk(&mut log.serum_pc_vault_account, ix.serum_pc_vault_account);
452    fill_pk(&mut log.serum_vault_signer, ix.serum_vault_signer);
453    fill_pk(
454        &mut log.user_source_token_account,
455        ix.user_source_token_account,
456    );
457    fill_pk(
458        &mut log.user_destination_token_account,
459        ix.user_destination_token_account,
460    );
461}
462
463#[inline]
464fn merge_pumpswap_create_pool_log_preferred(
465    log: &mut PumpSwapCreatePoolEvent,
466    ix: PumpSwapCreatePoolEvent,
467) {
468    fill_pk(&mut log.creator, ix.creator);
469    fill_pk(&mut log.pool, ix.pool);
470    fill_pk(&mut log.lp_mint, ix.lp_mint);
471    fill_pk(&mut log.user_base_token_account, ix.user_base_token_account);
472    fill_pk(&mut log.user_quote_token_account, ix.user_quote_token_account);
473    fill_pk(&mut log.coin_creator, ix.coin_creator);
474}
475
476#[inline]
477fn merge_pumpswap_liquidity_added_log_preferred(
478    log: &mut PumpSwapLiquidityAdded,
479    ix: PumpSwapLiquidityAdded,
480) {
481    fill_pk(&mut log.user_base_token_account, ix.user_base_token_account);
482    fill_pk(&mut log.user_quote_token_account, ix.user_quote_token_account);
483    fill_pk(&mut log.user_pool_token_account, ix.user_pool_token_account);
484}
485
486#[inline]
487fn merge_pumpswap_liquidity_removed_log_preferred(
488    log: &mut PumpSwapLiquidityRemoved,
489    ix: PumpSwapLiquidityRemoved,
490) {
491    fill_pk(&mut log.user_base_token_account, ix.user_base_token_account);
492    fill_pk(&mut log.user_quote_token_account, ix.user_quote_token_account);
493    fill_pk(&mut log.user_pool_token_account, ix.user_pool_token_account);
494}
495
496#[inline]
497fn merge_bonk_pool_create_log_preferred(log: &mut BonkPoolCreateEvent, ix: BonkPoolCreateEvent) {
498    fill_pk(&mut log.creator, ix.creator);
499    fill_str_if_empty(&mut log.base_mint_param.name, &ix.base_mint_param.name);
500    fill_str_if_empty(&mut log.base_mint_param.symbol, &ix.base_mint_param.symbol);
501    fill_str_if_empty(&mut log.base_mint_param.uri, &ix.base_mint_param.uri);
502}
503
504#[inline]
505fn merge_bonk_migrate_amm_log_preferred(log: &mut BonkMigrateAmmEvent, ix: BonkMigrateAmmEvent) {
506    fill_pk(&mut log.old_pool, ix.old_pool);
507    fill_pk(&mut log.new_pool, ix.new_pool);
508    fill_pk(&mut log.user, ix.user);
509}
510
511/// BonkTrade 当前无独立「仅 ix 账户」字段;保留占位以便与 dedup 对齐,日后扩展。
512#[inline]
513fn merge_bonk_trade_log_preferred(_log: &mut BonkTradeEvent, _ix: BonkTradeEvent) {}
514
515#[inline]
516fn merge_meteora_dlmm_swap_log_preferred(_log: &mut MeteoraDlmmSwapEvent, _ix: MeteoraDlmmSwapEvent) {}
517
518/// 将 **instruction 路径**解析结果合并进 **log 路径**事件:`log` 保留链上日志权威数值,
519/// `ix` 仅填补 `log` 中为默认值的账户等字段。**不替换** `log` 外层枚举变体。
520///
521/// 已覆盖与 [`crate::grpc::log_instr_dedup`] 去重键一致的主要类型:PumpFun 全系、PumpSwap
522///(Trade/Buy/Sell/CreatePool/加减流动性)、Bonk(Trade/PoolCreate/Migrate)、Raydium CLMM/AMM V4 Swap、Meteora DLMM Swap。
523pub fn merge_grpc_instruction_into_log(log: &mut DexEvent, ix: DexEvent) {
524    use DexEvent::*;
525    match log {
526        PumpFunTrade(l) => {
527            if let Some(i) = pumpfun_trade_from_ix_variant(ix) {
528                merge_pumpfun_trade_log_preferred(l, i);
529            }
530        }
531        PumpFunBuy(l) => {
532            if let Some(i) = pumpfun_trade_from_ix_variant(ix) {
533                merge_pumpfun_trade_log_preferred(l, i);
534            }
535        }
536        PumpFunSell(l) => {
537            if let Some(i) = pumpfun_trade_from_ix_variant(ix) {
538                merge_pumpfun_trade_log_preferred(l, i);
539            }
540        }
541        PumpFunBuyExactSolIn(l) => {
542            if let Some(i) = pumpfun_trade_from_ix_variant(ix) {
543                merge_pumpfun_trade_log_preferred(l, i);
544            }
545        }
546        PumpFunCreate(l) => {
547            if let DexEvent::PumpFunCreate(i) = ix {
548                merge_pumpfun_create_log_preferred(l, i);
549            }
550        }
551        PumpFunCreateV2(l) => {
552            if let DexEvent::PumpFunCreateV2(i) = ix {
553                merge_pumpfun_create_v2_log_preferred(l, i);
554            }
555        }
556        PumpFunMigrate(l) => {
557            if let DexEvent::PumpFunMigrate(i) = ix {
558                merge_pumpfun_migrate_log_preferred(l, i);
559            }
560        }
561        PumpSwapTrade(l) => {
562            if let PumpSwapTrade(i) = ix {
563                merge_pumpswap_trade_log_preferred(l, i);
564            }
565        }
566        PumpSwapBuy(l) => {
567            if let PumpSwapBuy(i) = ix {
568                merge_pumpswap_buy_log_preferred(l, i);
569            }
570        }
571        PumpSwapSell(l) => {
572            if let PumpSwapSell(i) = ix {
573                merge_pumpswap_sell_log_preferred(l, i);
574            }
575        }
576        RaydiumClmmSwap(l) => {
577            if let RaydiumClmmSwap(i) = ix {
578                merge_raydium_clmm_swap_log_preferred(l, i);
579            }
580        }
581        RaydiumAmmV4Swap(l) => {
582            if let RaydiumAmmV4Swap(i) = ix {
583                merge_raydium_amm_v4_swap_log_preferred(l, i);
584            }
585        }
586        BonkTrade(l) => {
587            if let BonkTrade(i) = ix {
588                merge_bonk_trade_log_preferred(l, i);
589            }
590        }
591        BonkPoolCreate(l) => {
592            if let BonkPoolCreate(i) = ix {
593                merge_bonk_pool_create_log_preferred(l, i);
594            }
595        }
596        BonkMigrateAmm(l) => {
597            if let BonkMigrateAmm(i) = ix {
598                merge_bonk_migrate_amm_log_preferred(l, i);
599            }
600        }
601        PumpSwapCreatePool(l) => {
602            if let PumpSwapCreatePool(i) = ix {
603                merge_pumpswap_create_pool_log_preferred(l, i);
604            }
605        }
606        PumpSwapLiquidityAdded(l) => {
607            if let PumpSwapLiquidityAdded(i) = ix {
608                merge_pumpswap_liquidity_added_log_preferred(l, i);
609            }
610        }
611        PumpSwapLiquidityRemoved(l) => {
612            if let PumpSwapLiquidityRemoved(i) = ix {
613                merge_pumpswap_liquidity_removed_log_preferred(l, i);
614            }
615        }
616        MeteoraDlmmSwap(l) => {
617            if let MeteoraDlmmSwap(i) = ix {
618                merge_meteora_dlmm_swap_log_preferred(l, i);
619            }
620        }
621        _ => {}
622    }
623}
624
625#[inline]
626fn pumpfun_trade_from_ix_variant(ix: DexEvent) -> Option<PumpFunTradeEvent> {
627    match ix {
628        DexEvent::PumpFunTrade(t)
629        | DexEvent::PumpFunBuy(t)
630        | DexEvent::PumpFunSell(t)
631        | DexEvent::PumpFunBuyExactSolIn(t) => Some(t),
632        _ => None,
633    }
634}
635
636#[cfg(test)]
637mod tests {
638    use super::*;
639    use solana_sdk::{pubkey::Pubkey, signature::Signature};
640
641    #[test]
642    fn test_merge_pumpfun_trade() {
643        let metadata = EventMetadata {
644            signature: Signature::default(),
645            slot: 100,
646            tx_index: 1,
647            block_time_us: 1000,
648            grpc_recv_us: 2000,
649            recent_blockhash: None,
650        };
651
652        // Base event 来自 instruction(包含账户上下文)
653        let mut base = DexEvent::PumpFunTrade(PumpFunTradeEvent {
654            metadata: metadata.clone(),
655            bonding_curve: Pubkey::new_unique(),
656            associated_bonding_curve: Pubkey::new_unique(),
657            ..Default::default()
658        });
659
660        // Inner event 来自 inner instruction(包含交易数据)
661        let inner = DexEvent::PumpFunTrade(PumpFunTradeEvent {
662            metadata: metadata.clone(),
663            mint: Pubkey::new_unique(),
664            sol_amount: 1000,
665            token_amount: 2000,
666            is_buy: true,
667            user: Pubkey::new_unique(),
668            ..Default::default()
669        });
670
671        // 合并
672        merge_events(&mut base, inner);
673
674        // 验证合并结果
675        if let DexEvent::PumpFunTrade(trade) = base {
676            assert_eq!(trade.sol_amount, 1000);
677            assert_eq!(trade.token_amount, 2000);
678            assert!(trade.is_buy);
679            // 账户上下文保留
680            assert_ne!(trade.bonding_curve, Pubkey::default());
681            assert_ne!(trade.associated_bonding_curve, Pubkey::default());
682        } else {
683            panic!("Expected PumpFunTrade event");
684        }
685    }
686
687    #[test]
688    fn test_can_merge() {
689        let metadata = EventMetadata {
690            signature: Signature::default(),
691            slot: 100,
692            tx_index: 1,
693            block_time_us: 1000,
694            grpc_recv_us: 2000,
695            recent_blockhash: None,
696        };
697
698        let base = DexEvent::PumpFunTrade(PumpFunTradeEvent {
699            metadata: metadata.clone(),
700            ..Default::default()
701        });
702
703        let inner = DexEvent::PumpFunBuy(PumpFunTradeEvent {
704            metadata: metadata.clone(),
705            ..Default::default()
706        });
707
708        // 应该可以合并(同一个 signature,兼容类型)
709        assert!(can_merge(&base, &inner));
710
711        // 不同 signature 不能合并
712        let different_sig = DexEvent::PumpFunTrade(PumpFunTradeEvent {
713            metadata: EventMetadata { signature: Signature::new_unique(), ..metadata },
714            ..Default::default()
715        });
716
717        assert!(!can_merge(&base, &different_sig));
718    }
719
720    #[test]
721    fn grpc_merge_fills_fee_recipient_from_ix_when_log_default() {
722        let metadata = EventMetadata {
723            signature: Signature::default(),
724            slot: 1,
725            tx_index: 0,
726            block_time_us: 0,
727            grpc_recv_us: 0,
728            recent_blockhash: None,
729        };
730        let fr = Pubkey::new_unique();
731        let log_t = PumpFunTradeEvent {
732            metadata: metadata.clone(),
733            sol_amount: 50,
734            ..Default::default()
735        };
736        let mut ix_t = log_t.clone();
737        ix_t.fee_recipient = fr;
738        ix_t.sol_amount = 777;
739        let mut log_ev = DexEvent::PumpFunTrade(log_t);
740        merge_grpc_instruction_into_log(&mut log_ev, DexEvent::PumpFunBuy(ix_t));
741        match log_ev {
742            DexEvent::PumpFunTrade(t) => {
743                assert_eq!(t.fee_recipient, fr);
744                assert_eq!(t.sol_amount, 50);
745            }
746            _ => panic!("expected trade"),
747        }
748    }
749
750    #[test]
751    fn grpc_merge_keeps_log_trade_fields() {
752        let metadata = EventMetadata {
753            signature: Signature::default(),
754            slot: 1,
755            tx_index: 0,
756            block_time_us: 0,
757            grpc_recv_us: 0,
758            recent_blockhash: None,
759        };
760        let log_t = PumpFunTradeEvent {
761            metadata: metadata.clone(),
762            mayhem_mode: true,
763            sol_amount: 100,
764            ..Default::default()
765        };
766        let mut ix_t = log_t.clone();
767        ix_t.mayhem_mode = false;
768        ix_t.sol_amount = 999;
769
770        let mut log_ev = DexEvent::PumpFunTrade(log_t);
771        merge_grpc_instruction_into_log(&mut log_ev, DexEvent::PumpFunBuy(ix_t));
772        match log_ev {
773            DexEvent::PumpFunTrade(t) => {
774                assert!(t.mayhem_mode);
775                assert_eq!(t.sol_amount, 100);
776            }
777            _ => panic!("variant preserved"),
778        }
779    }
780}