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