Skip to main content

sol_parser_sdk/
rpc_parser.rs

1//! RPC Transaction Parser
2//!
3//! 提供独立的 RPC 交易解析功能,不依赖 gRPC streaming
4//! 可以用于测试验证和离线分析
5
6use crate::core::events::DexEvent;
7use crate::grpc::types::EventTypeFilter;
8use crate::instr::read_pubkey_fast;
9use crate::transaction_cost::{parse_yellowstone_transaction_cost, TransactionCost};
10use smallvec::SmallVec;
11use solana_client::rpc_client::RpcClient;
12use solana_client::rpc_config::{RpcTransactionConfig, UiTransactionEncoding};
13use solana_client::rpc_response::{
14    EncodedTransaction, UiInstruction, UiTransactionStatusMeta, UiTransactionTokenBalance,
15};
16use solana_sdk::pubkey::Pubkey;
17use solana_sdk::signature::Signature;
18use solana_transaction_status::{
19    option_serializer::OptionSerializer, EncodedConfirmedTransactionWithStatusMeta,
20};
21use std::collections::HashMap;
22use yellowstone_grpc_proto::prelude::{
23    CompiledInstruction, InnerInstruction, InnerInstructions, Message, MessageAddressTableLookup,
24    MessageHeader, TokenBalance, Transaction, TransactionError, TransactionStatusMeta,
25    UiTokenAmount,
26};
27
28/// Parse a transaction from RPC by signature
29///
30/// # Arguments
31/// * `rpc_client` - RPC client to fetch the transaction
32/// * `signature` - Transaction signature
33/// * `filter` - Optional event type filter
34///
35/// # Returns
36/// Vector of parsed DEX events
37///
38/// # Example
39/// ```no_run
40/// use solana_client::rpc_client::RpcClient;
41/// use solana_sdk::signature::Signature;
42/// use sol_parser_sdk::parse_transaction_from_rpc;
43/// use std::str::FromStr;
44///
45/// let client = RpcClient::new("https://api.mainnet-beta.solana.com".to_string());
46/// let sig = Signature::from_str("your-signature-here").unwrap();
47/// let events = parse_transaction_from_rpc(&client, &sig, None).unwrap();
48/// ```
49pub fn parse_transaction_from_rpc(
50    rpc_client: &RpcClient,
51    signature: &Signature,
52    filter: Option<&EventTypeFilter>,
53) -> Result<Vec<DexEvent>, ParseError> {
54    // Fetch transaction from RPC with V1 transaction support.
55    let config = RpcTransactionConfig {
56        encoding: Some(UiTransactionEncoding::Base64),
57        commitment: None,
58        max_supported_transaction_version: Some(1),
59    };
60
61    let rpc_tx = rpc_client.get_transaction_with_config(signature, config).map_err(|e| {
62        let msg = e.to_string();
63        if msg.contains("invalid type: null") && msg.contains("EncodedConfirmedTransactionWithStatusMeta") {
64            ParseError::RpcError(format!(
65                "Transaction not found (RPC returned null). Common causes: 1) Transaction is too old and pruned (use an archive RPC). 2) Wrong network or invalid signature. Try SOLANA_RPC_URL with an archive endpoint (e.g. Helius, QuickNode) or a more recent tx. Original: {}",
66                msg
67            ))
68        } else {
69            ParseError::RpcError(msg)
70        }
71    })?;
72
73    parse_rpc_transaction(&rpc_tx, filter)
74}
75
76/// Parse a RPC transaction structure
77///
78/// # Arguments
79/// * `rpc_tx` - RPC transaction to parse
80/// * `filter` - Optional event type filter
81///
82/// # Returns
83/// Vector of parsed DEX events
84///
85/// # Example
86/// ```no_run
87/// use sol_parser_sdk::parse_rpc_transaction;
88///
89/// // Assuming you have an rpc_tx from RPC
90/// // let events = parse_rpc_transaction(&rpc_tx, None).unwrap();
91/// ```
92pub fn parse_rpc_transaction(
93    rpc_tx: &EncodedConfirmedTransactionWithStatusMeta,
94    filter: Option<&EventTypeFilter>,
95) -> Result<Vec<DexEvent>, ParseError> {
96    // RPC logs from failed transactions describe rolled-back work. Match the
97    // Yellowstone event API and reject before decoding/account allocation.
98    if rpc_tx.transaction.meta.as_ref().is_some_and(|meta| meta.err.is_some()) {
99        return Ok(Vec::new());
100    }
101    let (grpc_meta, grpc_tx) = convert_rpc_to_grpc_for_parsing(rpc_tx)?;
102    let signature = extract_grpc_signature(&grpc_tx)?;
103    parse_converted_rpc_transaction(rpc_tx, grpc_meta, grpc_tx, signature, filter)
104}
105
106/// Result of parsing RPC events and transaction costs from one shared decode.
107#[derive(Debug)]
108pub struct ParsedRpcTransaction {
109    pub events: Vec<DexEvent>,
110    pub cost: TransactionCost,
111    pub signature: Signature,
112}
113
114/// Parses events and transaction costs while decoding the RPC payload once.
115pub fn parse_rpc_transaction_with_cost(
116    rpc_tx: &EncodedConfirmedTransactionWithStatusMeta,
117    filter: Option<&EventTypeFilter>,
118) -> Result<ParsedRpcTransaction, ParseError> {
119    let (grpc_meta, grpc_tx) = convert_rpc_to_grpc_for_parsing(rpc_tx)?;
120    let signature = extract_grpc_signature(&grpc_tx)?;
121    let cost = parse_yellowstone_transaction_cost(&grpc_tx, &grpc_meta)
122        .ok_or_else(|| ParseError::MissingField("transaction.message".to_string()))?;
123    let events = parse_converted_rpc_transaction(rpc_tx, grpc_meta, grpc_tx, signature, filter)?;
124    Ok(ParsedRpcTransaction { events, cost, signature })
125}
126
127/// Parses only transaction cost and signature from one shared RPC decode.
128pub fn parse_rpc_transaction_cost_with_signature(
129    rpc_tx: &EncodedConfirmedTransactionWithStatusMeta,
130) -> Result<(TransactionCost, Signature), ParseError> {
131    let (grpc_meta, grpc_tx) = convert_rpc_to_grpc_for_parsing(rpc_tx)?;
132    let signature = extract_grpc_signature(&grpc_tx)?;
133    let cost = parse_yellowstone_transaction_cost(&grpc_tx, &grpc_meta)
134        .ok_or_else(|| ParseError::MissingField("transaction.message".to_string()))?;
135    Ok((cost, signature))
136}
137
138fn extract_grpc_signature(transaction: &Transaction) -> Result<Signature, ParseError> {
139    transaction
140        .signatures
141        .first()
142        .ok_or_else(|| ParseError::MissingField("transaction.signatures[0]".to_string()))
143        .and_then(|bytes| {
144            Signature::try_from(bytes.as_slice()).map_err(|error| {
145                ParseError::ConversionError(format!("Invalid transaction signature: {error}"))
146            })
147        })
148}
149
150fn parse_converted_rpc_transaction(
151    rpc_tx: &EncodedConfirmedTransactionWithStatusMeta,
152    grpc_meta: TransactionStatusMeta,
153    grpc_tx: Transaction,
154    signature: Signature,
155    filter: Option<&EventTypeFilter>,
156) -> Result<Vec<DexEvent>, ParseError> {
157    // The combined API still decodes costs/signature for failed transactions,
158    // but must not interpret their logs as successful DEX activity.
159    if grpc_meta.err.is_some() {
160        return Ok(Vec::new());
161    }
162    // Extract metadata
163    let slot = rpc_tx.slot;
164    let block_time_us = rpc_tx.block_time.map(|t| t * 1_000_000);
165    let grpc_recv_us = std::time::SystemTime::now()
166        .duration_since(std::time::UNIX_EPOCH)
167        .unwrap_or_default()
168        .as_micros() as i64;
169
170    // Wrap grpc_tx in Option for reuse
171    let grpc_tx_opt = Some(grpc_tx);
172
173    let mut program_invokes: HashMap<Pubkey, Vec<(i32, i32)>> = HashMap::new();
174
175    if let Some(ref tx) = grpc_tx_opt {
176        if let Some(ref msg) = tx.message {
177            let keys_len = msg.account_keys.len();
178            let writable_len = grpc_meta.loaded_writable_addresses.len();
179            let get_key = |i: usize| -> Option<&Vec<u8>> {
180                if i < keys_len {
181                    msg.account_keys.get(i)
182                } else if i < keys_len + writable_len {
183                    grpc_meta.loaded_writable_addresses.get(i - keys_len)
184                } else {
185                    grpc_meta.loaded_readonly_addresses.get(i - keys_len - writable_len)
186                }
187            };
188
189            for (i, ix) in msg.instructions.iter().enumerate() {
190                let pid = get_key(ix.program_id_index as usize)
191                    .map_or(Pubkey::default(), |k| read_pubkey_fast(k));
192                if crate::grpc::program_ids::needs_invoke_context(&pid) {
193                    program_invokes.entry(pid).or_default().push((i as i32, -1));
194                }
195            }
196
197            for inner in &grpc_meta.inner_instructions {
198                let outer_idx = inner.index as usize;
199                for (j, inner_ix) in inner.instructions.iter().enumerate() {
200                    let pid = get_key(inner_ix.program_id_index as usize)
201                        .map_or(Pubkey::default(), |k| read_pubkey_fast(k));
202                    if crate::grpc::program_ids::needs_invoke_context(&pid) {
203                        program_invokes.entry(pid).or_default().push((outer_idx as i32, j as i32));
204                    }
205                }
206            }
207        }
208    }
209
210    let needs_pumpfun = filter.map(EventTypeFilter::includes_pumpfun).unwrap_or(true);
211    let log_messages = rpc_log_messages(rpc_tx);
212    let is_created_buy =
213        needs_pumpfun && crate::logs::optimized_matcher::detect_pumpfun_create(log_messages);
214
215    // Parse instructions
216    let instr_events =
217        crate::grpc::instruction_parser::parse_instructions_enhanced_with_created_buy(
218            &grpc_meta,
219            &grpc_tx_opt,
220            signature,
221            slot,
222            0, // tx_idx
223            block_time_us,
224            grpc_recv_us,
225            filter,
226            is_created_buy,
227        );
228
229    // Parse logs (for protocols like PumpFun that emit events in logs)
230    struct ActiveProgram<'a> {
231        encoded: &'a str,
232        pubkey: Pubkey,
233        position: (i32, i32),
234    }
235
236    let mut active_program_stack: SmallVec<[ActiveProgram<'_>; 8]> = SmallVec::new();
237    let mut log_events = Vec::new();
238    let mut outer_index = -1;
239    let mut inner_index = -1;
240
241    for log in log_messages {
242        if let Some((pid, depth)) = crate::logs::optimized_matcher::parse_invoke_info(log) {
243            let pk = crate::grpc::program_ids::known_program_id(pid).unwrap_or_default();
244            active_program_stack.truncate(depth - 1);
245            if depth == 1 {
246                outer_index = crate::grpc::yellowstone_tx_parse::next_logged_outer_index(
247                    &grpc_tx_opt,
248                    outer_index,
249                );
250                inner_index = -1;
251            } else {
252                inner_index += 1;
253            }
254            active_program_stack.push(ActiveProgram {
255                encoded: pid,
256                pubkey: pk,
257                position: (outer_index, inner_index),
258            });
259        }
260
261        if let Some(mut event) = crate::logs::parse_log_with_program_id(
262            log,
263            signature,
264            slot,
265            0, // tx_index
266            block_time_us,
267            grpc_recv_us,
268            filter,
269            is_created_buy,
270            None,
271            active_program_stack.last().map(|active| &active.pubkey),
272        ) {
273            // Fill account fields - use same function as gRPC parsing
274            if matches!(&event, DexEvent::RaydiumAmmV4Swap(_)) {
275                let mut scoped = crate::core::invoke_context::InvokeContext::default();
276                if let Some(active) = active_program_stack.last() {
277                    scoped.push(active.pubkey, active.position);
278                }
279                crate::core::account_dispatcher::fill_accounts_with_invoke_context(
280                    &mut event,
281                    &grpc_meta,
282                    &grpc_tx_opt,
283                    &scoped,
284                );
285            } else {
286                crate::core::account_dispatcher::fill_accounts_with_owned_keys(
287                    &mut event,
288                    &grpc_meta,
289                    &grpc_tx_opt,
290                    &program_invokes,
291                );
292            }
293
294            // Fill additional data fields (e.g., PumpSwap is_pump_pool)
295            crate::core::common_filler::fill_data(
296                &mut event,
297                &grpc_meta,
298                &grpc_tx_opt,
299                &program_invokes,
300            );
301
302            log_events.push(event);
303        }
304
305        if let Some(pid) = crate::logs::optimized_matcher::parse_program_complete_info(log) {
306            if let Some(pos) = active_program_stack.iter().rposition(|active| active.encoded == pid)
307            {
308                active_program_stack.truncate(pos);
309            }
310        }
311    }
312
313    let mut events = merge_log_and_instruction_events(log_events, instr_events);
314    crate::core::common_filler::fill_pump_creation_context(&mut events, &grpc_meta, &grpc_tx_opt);
315    fill_rpc_event_metadata(&mut events, rpc_tx, &grpc_meta, &grpc_tx_opt);
316    Ok(events)
317}
318
319fn fill_rpc_event_metadata(
320    events: &mut [DexEvent],
321    rpc_tx: &EncodedConfirmedTransactionWithStatusMeta,
322    meta: &TransactionStatusMeta,
323    transaction: &Option<Transaction>,
324) {
325    if let Some(rpc_meta) = rpc_tx.transaction.meta.as_ref() {
326        for event in events.iter_mut() {
327            fill_rpc_token_balances(event, rpc_meta, meta, transaction);
328        }
329    }
330    crate::grpc::transaction_meta::fill_recent_blockhash(events, transaction);
331}
332
333#[inline]
334fn fill_rpc_token_balances(
335    event: &mut DexEvent,
336    rpc_meta: &UiTransactionStatusMeta,
337    meta: &TransactionStatusMeta,
338    transaction: &Option<Transaction>,
339) {
340    let trade = match event {
341        DexEvent::PumpFunTrade(event)
342        | DexEvent::PumpFunBuy(event)
343        | DexEvent::PumpFunSell(event)
344        | DexEvent::PumpFunBuyExactSolIn(event) => event,
345        _ => return,
346    };
347
348    if let Some(user_index) = rpc_account_index(transaction, meta, &trade.user) {
349        trade.sol_balance = rpc_meta.post_balances.get(user_index).copied();
350    }
351
352    if trade.associated_user == Pubkey::default() {
353        return;
354    }
355
356    let matches_account = |balance: &UiTransactionTokenBalance| {
357        rpc_account_key(transaction, meta, balance.account_index as usize)
358            .is_some_and(|key| key.as_slice() == trade.associated_user.as_ref())
359    };
360
361    if let OptionSerializer::Some(balances) = &rpc_meta.post_token_balances {
362        if let Some(balance) = balances.iter().find(|balance| matches_account(balance)) {
363            trade.token_balance = balance.ui_token_amount.amount.parse().ok();
364            return;
365        }
366    }
367
368    if let OptionSerializer::Some(balances) = &rpc_meta.pre_token_balances {
369        if balances.iter().any(matches_account) {
370            trade.token_balance = Some(0);
371        }
372    }
373}
374
375#[inline]
376fn rpc_account_index(
377    transaction: &Option<Transaction>,
378    meta: &TransactionStatusMeta,
379    account: &Pubkey,
380) -> Option<usize> {
381    if *account == Pubkey::default() {
382        return None;
383    }
384
385    let message = transaction.as_ref()?.message.as_ref()?;
386    message
387        .account_keys
388        .iter()
389        .chain(&meta.loaded_writable_addresses)
390        .chain(&meta.loaded_readonly_addresses)
391        .position(|key| key.as_slice() == account.as_ref())
392}
393
394#[inline]
395fn rpc_account_key<'a>(
396    transaction: &'a Option<Transaction>,
397    meta: &'a TransactionStatusMeta,
398    index: usize,
399) -> Option<&'a Vec<u8>> {
400    let message = transaction.as_ref()?.message.as_ref()?;
401    let static_len = message.account_keys.len();
402    let writable_len = meta.loaded_writable_addresses.len();
403
404    if index < static_len {
405        message.account_keys.get(index)
406    } else if index < static_len + writable_len {
407        meta.loaded_writable_addresses.get(index - static_len)
408    } else {
409        meta.loaded_readonly_addresses.get(index - static_len - writable_len)
410    }
411}
412
413#[inline]
414fn rpc_log_messages(rpc_tx: &EncodedConfirmedTransactionWithStatusMeta) -> &[String] {
415    let Some(meta) = rpc_tx.transaction.meta.as_ref() else { return &[] };
416    match &meta.log_messages {
417        OptionSerializer::Some(messages) => messages,
418        _ => &[],
419    }
420}
421
422fn merge_log_and_instruction_events(
423    log_events: Vec<DexEvent>,
424    instr_events: Vec<DexEvent>,
425) -> Vec<DexEvent> {
426    crate::grpc::log_instr_dedup::dedupe_log_instruction_events(log_events, instr_events)
427}
428
429/// Parse error types
430#[derive(Debug)]
431pub enum ParseError {
432    RpcError(String),
433    ConversionError(String),
434    MissingField(String),
435}
436
437impl std::fmt::Display for ParseError {
438    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
439        match self {
440            ParseError::RpcError(msg) => write!(f, "RPC error: {}", msg),
441            ParseError::ConversionError(msg) => write!(f, "Conversion error: {}", msg),
442            ParseError::MissingField(msg) => write!(f, "Missing field: {}", msg),
443        }
444    }
445}
446
447impl std::error::Error for ParseError {}
448
449// ============================================================================
450// Internal conversion functions
451// ============================================================================
452
453pub fn convert_rpc_to_grpc(
454    rpc_tx: &EncodedConfirmedTransactionWithStatusMeta,
455) -> Result<(TransactionStatusMeta, Transaction), ParseError> {
456    convert_rpc_to_grpc_impl(rpc_tx, true)
457}
458
459/// Converts only metadata that parsing and cost calculation must own; logs and balances stay
460/// borrowed from the original RPC response on these internal paths.
461#[inline]
462pub(crate) fn convert_rpc_to_grpc_for_parsing(
463    rpc_tx: &EncodedConfirmedTransactionWithStatusMeta,
464) -> Result<(TransactionStatusMeta, Transaction), ParseError> {
465    convert_rpc_to_grpc_impl(rpc_tx, false)
466}
467
468fn convert_rpc_to_grpc_impl(
469    rpc_tx: &EncodedConfirmedTransactionWithStatusMeta,
470    include_borrowed_metadata: bool,
471) -> Result<(TransactionStatusMeta, Transaction), ParseError> {
472    let rpc_meta = rpc_tx
473        .transaction
474        .meta
475        .as_ref()
476        .ok_or_else(|| ParseError::MissingField("meta".to_string()))?;
477
478    let (loaded_writable_addresses, loaded_readonly_addresses) = rpc_meta
479        .loaded_addresses
480        .as_ref()
481        .map(|addresses| {
482            let writable = addresses
483                .writable
484                .iter()
485                .map(|address| parse_loaded_address(address))
486                .collect::<Result<Vec<_>, _>>()?;
487            let readonly = addresses
488                .readonly
489                .iter()
490                .map(|address| parse_loaded_address(address))
491                .collect::<Result<Vec<_>, _>>()?;
492            Ok((writable, readonly))
493        })
494        .transpose()?
495        .unwrap_or_default();
496
497    let err = rpc_meta
498        .err
499        .clone()
500        .map(|error| {
501            let error: solana_sdk::transaction::TransactionError = error.into();
502            wincode::serialize(&error).map(|err| TransactionError { err }).map_err(|error| {
503                ParseError::ConversionError(format!(
504                    "Failed to serialize transaction error: {error}"
505                ))
506            })
507        })
508        .transpose()?;
509
510    // Convert meta
511    let mut grpc_meta = TransactionStatusMeta {
512        err,
513        fee: rpc_meta.fee,
514        pre_balances: if include_borrowed_metadata {
515            rpc_meta.pre_balances.clone()
516        } else {
517            Vec::new()
518        },
519        post_balances: if include_borrowed_metadata {
520            rpc_meta.post_balances.clone()
521        } else {
522            Vec::new()
523        },
524        inner_instructions: Vec::new(),
525        log_messages: if include_borrowed_metadata {
526            rpc_meta.log_messages.as_ref().map(|messages| messages.clone()).unwrap_or_default()
527        } else {
528            Vec::new()
529        },
530        pre_token_balances: if include_borrowed_metadata {
531            rpc_meta
532                .pre_token_balances
533                .as_ref()
534                .map(|balances| convert_token_balances(balances))
535                .unwrap_or_default()
536        } else {
537            Vec::new()
538        },
539        post_token_balances: if include_borrowed_metadata {
540            rpc_meta
541                .post_token_balances
542                .as_ref()
543                .map(|balances| convert_token_balances(balances))
544                .unwrap_or_default()
545        } else {
546            Vec::new()
547        },
548        rewards: Vec::new(),
549        loaded_writable_addresses,
550        loaded_readonly_addresses,
551        return_data: None,
552        compute_units_consumed: rpc_meta.compute_units_consumed.clone().into(),
553
554        inner_instructions_none: !rpc_meta.inner_instructions.is_some(),
555        log_messages_none: !rpc_meta.log_messages.is_some(),
556        return_data_none: !rpc_meta.return_data.is_some(),
557        cost_units: rpc_meta.cost_units.clone().into(),
558    };
559
560    // Convert inner instructions
561    if let solana_transaction_status::option_serializer::OptionSerializer::Some(
562        inner_instructions,
563    ) = rpc_meta.inner_instructions.as_ref()
564    {
565        for inner in inner_instructions {
566            let mut grpc_inner =
567                InnerInstructions { index: inner.index as u32, instructions: Vec::new() };
568
569            for ix in &inner.instructions {
570                if let UiInstruction::Compiled(compiled) = ix {
571                    // Decode base58 data
572                    let data = base58_turbo::BITCOIN.decode(&compiled.data).map_err(|e| {
573                        ParseError::ConversionError(format!(
574                            "Failed to decode instruction data: {}",
575                            e
576                        ))
577                    })?;
578
579                    grpc_inner.instructions.push(InnerInstruction {
580                        program_id_index: compiled.program_id_index as u32,
581                        accounts: compiled.accounts.clone(),
582                        data,
583                        stack_height: compiled.stack_height,
584                    });
585                }
586            }
587
588            grpc_meta.inner_instructions.push(grpc_inner);
589        }
590    }
591
592    // Convert transaction
593    let ui_tx = &rpc_tx.transaction.transaction;
594
595    let (message, signatures) = match ui_tx {
596        EncodedTransaction::Binary(_, _) | EncodedTransaction::LegacyBinary(_) => {
597            // Solana's decoder handles Base58/Base64, wincode deserialization and sanitization.
598            let versioned_tx = ui_tx.decode().ok_or_else(|| {
599                ParseError::ConversionError(
600                    "Failed to decode or sanitize binary transaction".to_string(),
601                )
602            })?;
603
604            let sigs: Vec<Vec<u8>> =
605                versioned_tx.signatures.iter().map(|s| s.as_ref().to_vec()).collect();
606
607            let message = match versioned_tx.message {
608                solana_sdk::message::VersionedMessage::Legacy(legacy_msg) => {
609                    convert_legacy_message(legacy_msg)?
610                }
611                solana_sdk::message::VersionedMessage::V0(v0_msg) => convert_v0_message(v0_msg)?,
612                solana_sdk::message::VersionedMessage::V1(v1_msg) => convert_v1_message(v1_msg)?,
613            };
614
615            (message, sigs)
616        }
617        EncodedTransaction::Json(_) => {
618            return Err(ParseError::ConversionError(
619                "JSON encoded transactions not supported yet".to_string(),
620            ));
621        }
622        _ => {
623            return Err(ParseError::ConversionError(
624                "Unsupported transaction encoding".to_string(),
625            ));
626        }
627    };
628
629    let grpc_tx = Transaction { signatures, message: Some(message) };
630
631    Ok((grpc_meta, grpc_tx))
632}
633
634fn parse_loaded_address(address: &str) -> Result<Vec<u8>, ParseError> {
635    address.parse::<Pubkey>().map(|pubkey| pubkey.to_bytes().to_vec()).map_err(|error| {
636        ParseError::ConversionError(format!("Invalid loaded address {address}: {error}"))
637    })
638}
639
640fn convert_token_balances(balances: &[UiTransactionTokenBalance]) -> Vec<TokenBalance> {
641    balances
642        .iter()
643        .map(|balance| TokenBalance {
644            account_index: balance.account_index as u32,
645            mint: balance.mint.clone(),
646            ui_token_amount: Some(UiTokenAmount {
647                ui_amount: balance.ui_token_amount.ui_amount.unwrap_or_default(),
648                decimals: balance.ui_token_amount.decimals as u32,
649                amount: balance.ui_token_amount.amount.clone(),
650                ui_amount_string: balance.ui_token_amount.ui_amount_string.clone(),
651            }),
652            owner: balance.owner.as_ref().map(|owner| owner.clone()).unwrap_or_default(),
653            program_id: balance
654                .program_id
655                .as_ref()
656                .map(|program_id| program_id.clone())
657                .unwrap_or_default(),
658        })
659        .collect()
660}
661
662fn convert_legacy_message(
663    msg: solana_sdk::message::legacy::Message,
664) -> Result<Message, ParseError> {
665    let account_keys: Vec<Vec<u8>> =
666        msg.account_keys.iter().map(|k| k.to_bytes().to_vec()).collect();
667
668    let instructions: Vec<CompiledInstruction> = msg
669        .instructions
670        .into_iter()
671        .map(|ix| CompiledInstruction {
672            program_id_index: ix.program_id_index as u32,
673            accounts: ix.accounts,
674            data: ix.data,
675        })
676        .collect();
677
678    Ok(Message {
679        header: Some(MessageHeader {
680            num_required_signatures: msg.header.num_required_signatures as u32,
681            num_readonly_signed_accounts: msg.header.num_readonly_signed_accounts as u32,
682            num_readonly_unsigned_accounts: msg.header.num_readonly_unsigned_accounts as u32,
683        }),
684        account_keys,
685        recent_blockhash: msg.recent_blockhash.to_bytes().to_vec(),
686        instructions,
687        versioned: false,
688        address_table_lookups: Vec::new(),
689        config: None,
690    })
691}
692
693fn convert_v0_message(msg: solana_sdk::message::v0::Message) -> Result<Message, ParseError> {
694    let account_keys: Vec<Vec<u8>> =
695        msg.account_keys.iter().map(|k| k.to_bytes().to_vec()).collect();
696
697    let instructions: Vec<CompiledInstruction> = msg
698        .instructions
699        .into_iter()
700        .map(|ix| CompiledInstruction {
701            program_id_index: ix.program_id_index as u32,
702            accounts: ix.accounts,
703            data: ix.data,
704        })
705        .collect();
706
707    Ok(Message {
708        header: Some(MessageHeader {
709            num_required_signatures: msg.header.num_required_signatures as u32,
710            num_readonly_signed_accounts: msg.header.num_readonly_signed_accounts as u32,
711            num_readonly_unsigned_accounts: msg.header.num_readonly_unsigned_accounts as u32,
712        }),
713        account_keys,
714        recent_blockhash: msg.recent_blockhash.to_bytes().to_vec(),
715        instructions,
716        versioned: true,
717        address_table_lookups: msg
718            .address_table_lookups
719            .into_iter()
720            .map(|lookup| MessageAddressTableLookup {
721                account_key: lookup.account_key.to_bytes().to_vec(),
722                writable_indexes: lookup.writable_indexes,
723                readonly_indexes: lookup.readonly_indexes,
724            })
725            .collect(),
726        config: None,
727    })
728}
729
730fn convert_v1_message(msg: solana_sdk::message::v1::Message) -> Result<Message, ParseError> {
731    let account_keys = msg.account_keys.iter().map(|key| key.to_bytes().to_vec()).collect();
732    let instructions = msg
733        .instructions
734        .into_iter()
735        .map(|ix| CompiledInstruction {
736            program_id_index: ix.program_id_index as u32,
737            accounts: ix.accounts,
738            data: ix.data,
739        })
740        .collect();
741
742    Ok(Message {
743        header: Some(MessageHeader {
744            num_required_signatures: msg.header.num_required_signatures as u32,
745            num_readonly_signed_accounts: msg.header.num_readonly_signed_accounts as u32,
746            num_readonly_unsigned_accounts: msg.header.num_readonly_unsigned_accounts as u32,
747        }),
748        account_keys,
749        recent_blockhash: msg.lifetime_specifier.to_bytes().to_vec(),
750        instructions,
751        versioned: true,
752        address_table_lookups: Vec::new(),
753        config: Some(yellowstone_grpc_proto::prelude::TransactionConfig {
754            priority_fee: msg.config.priority_fee,
755            compute_unit_limit: msg.config.compute_unit_limit,
756            loaded_accounts_data_size_limit: msg.config.loaded_accounts_data_size_limit,
757            heap_size: msg.config.heap_size,
758        }),
759    })
760}
761
762#[cfg(test)]
763mod tests {
764    use super::*;
765    use crate::core::events::{
766        DexEvent, EventMetadata, PumpFunTradeEvent, PumpSwapCreatePoolEvent,
767    };
768    use base64::{engine::general_purpose, Engine as _};
769    use solana_client::rpc_response::{
770        UiLoadedAddresses, UiTokenAmount as RpcUiTokenAmount, UiTransactionStatusMeta,
771        UiTransactionTokenBalance,
772    };
773    use solana_sdk::{
774        hash::Hash,
775        message::{legacy, MessageHeader, VersionedMessage},
776        pubkey::Pubkey,
777        signature::Signature,
778        transaction::VersionedTransaction,
779    };
780    use solana_transaction_status::{
781        option_serializer::OptionSerializer, EncodedTransactionWithStatusMeta,
782        TransactionBinaryEncoding,
783    };
784
785    fn rpc_fixture(
786        user: Pubkey,
787        token_account: Pubkey,
788    ) -> EncodedConfirmedTransactionWithStatusMeta {
789        let transaction = VersionedTransaction {
790            signatures: vec![Signature::from([7; 64])],
791            message: VersionedMessage::Legacy(legacy::Message {
792                header: MessageHeader { num_required_signatures: 1, ..Default::default() },
793                account_keys: vec![user, token_account],
794                recent_blockhash: Hash::new_unique(),
795                instructions: Vec::new(),
796            }),
797        };
798        let bytes = wincode::serialize(&transaction).expect("serialize RPC fixture");
799        let token_balance = |amount: &str| UiTransactionTokenBalance {
800            account_index: 1,
801            mint: Pubkey::new_unique().to_string(),
802            ui_token_amount: RpcUiTokenAmount {
803                ui_amount: None,
804                decimals: 6,
805                amount: amount.to_string(),
806                ui_amount_string: amount.to_string(),
807            },
808            owner: OptionSerializer::Some(user.to_string()),
809            program_id: OptionSerializer::None,
810        };
811
812        EncodedConfirmedTransactionWithStatusMeta {
813            slot: 42,
814            transaction: EncodedTransactionWithStatusMeta {
815                transaction: EncodedTransaction::Binary(
816                    general_purpose::STANDARD.encode(bytes),
817                    TransactionBinaryEncoding::Base64,
818                ),
819                meta: Some(UiTransactionStatusMeta {
820                    err: None,
821                    status: Ok(()),
822                    fee: 5_000,
823                    pre_balances: vec![50_000, 2_039_280],
824                    post_balances: vec![40_000, 2_039_280],
825                    inner_instructions: OptionSerializer::None,
826                    log_messages: OptionSerializer::None,
827                    pre_token_balances: OptionSerializer::Some(vec![token_balance("10")]),
828                    post_token_balances: OptionSerializer::Some(vec![token_balance("35")]),
829                    rewards: OptionSerializer::None,
830                    loaded_addresses: OptionSerializer::None,
831                    return_data: OptionSerializer::None,
832                    compute_units_consumed: OptionSerializer::Some(123),
833                    cost_units: OptionSerializer::Some(456),
834                }),
835                version: None,
836            },
837            block_time: None,
838            transaction_index: None,
839        }
840    }
841
842    fn dummy_meta() -> EventMetadata {
843        EventMetadata {
844            signature: Signature::default(),
845            slot: 1,
846            tx_index: 0,
847            block_time_us: 0,
848            grpc_recv_us: 0,
849            recent_blockhash: None,
850        }
851    }
852
853    #[test]
854    fn rpc_merge_keeps_instruction_cashback_for_log_only_pumpswap_create_pool() {
855        let pool = Pubkey::new_unique();
856        let base_mint = Pubkey::new_unique();
857        let quote_mint = Pubkey::new_unique();
858
859        let log_create = PumpSwapCreatePoolEvent {
860            metadata: dummy_meta(),
861            pool,
862            base_mint,
863            quote_mint,
864            is_cashback_coin: false,
865            ..Default::default()
866        };
867        let ix_create = PumpSwapCreatePoolEvent {
868            metadata: dummy_meta(),
869            pool,
870            base_mint,
871            quote_mint,
872            is_cashback_coin: true,
873            ..Default::default()
874        };
875
876        let merged = merge_log_and_instruction_events(
877            vec![DexEvent::PumpSwapCreatePool(log_create)],
878            vec![DexEvent::PumpSwapCreatePool(ix_create)],
879        );
880
881        assert_eq!(merged.len(), 1);
882        match &merged[0] {
883            DexEvent::PumpSwapCreatePool(e) => assert!(e.is_cashback_coin),
884            other => panic!("expected PumpSwapCreatePool, got {other:?}"),
885        }
886    }
887
888    #[test]
889    fn optimized_rpc_parsing_skips_borrowed_metadata_clones_but_fills_pumpfun_trade() {
890        let user = Pubkey::new_unique();
891        let token_account = Pubkey::new_unique();
892        let rpc_tx = rpc_fixture(user, token_account);
893        let (public_meta, _) = convert_rpc_to_grpc(&rpc_tx).expect("convert RPC fixture");
894
895        assert_eq!(
896            public_meta.pre_token_balances[0].ui_token_amount.as_ref().unwrap().amount,
897            "10"
898        );
899        assert_eq!(
900            public_meta.post_token_balances[0].ui_token_amount.as_ref().unwrap().amount,
901            "35"
902        );
903        assert_eq!(public_meta.pre_balances, [50_000, 2_039_280]);
904        assert_eq!(public_meta.post_balances, [40_000, 2_039_280]);
905        let (meta, transaction) =
906            convert_rpc_to_grpc_for_parsing(&rpc_tx).expect("convert RPC fixture for parsing");
907        assert!(meta.pre_balances.is_empty());
908        assert!(meta.post_balances.is_empty());
909        assert!(meta.log_messages.is_empty());
910        assert!(meta.pre_token_balances.is_empty());
911        assert!(meta.post_token_balances.is_empty());
912        assert_eq!(meta.compute_units_consumed, Some(123));
913        assert_eq!(meta.cost_units, Some(456));
914
915        let mut events = vec![DexEvent::PumpFunTrade(PumpFunTradeEvent {
916            user,
917            associated_user: token_account,
918            ..Default::default()
919        })];
920        fill_rpc_event_metadata(&mut events, &rpc_tx, &meta, &Some(transaction));
921
922        let DexEvent::PumpFunTrade(trade) = &events[0] else {
923            panic!("expected PumpFun trade");
924        };
925        assert_eq!(trade.token_balance, Some(35));
926        assert_eq!(trade.sol_balance, Some(40_000));
927    }
928
929    #[test]
930    fn optimized_rpc_balance_fill_handles_closed_token_accounts() {
931        let user = Pubkey::new_unique();
932        let token_account = Pubkey::new_unique();
933        let mut rpc_tx = rpc_fixture(user, token_account);
934        rpc_tx.transaction.meta.as_mut().unwrap().post_token_balances = OptionSerializer::None;
935        let (meta, transaction) =
936            convert_rpc_to_grpc_for_parsing(&rpc_tx).expect("convert RPC fixture for parsing");
937        let mut events = vec![DexEvent::PumpFunSell(PumpFunTradeEvent {
938            user,
939            associated_user: token_account,
940            ..Default::default()
941        })];
942
943        fill_rpc_event_metadata(&mut events, &rpc_tx, &meta, &Some(transaction));
944
945        let DexEvent::PumpFunSell(trade) = &events[0] else {
946            panic!("expected PumpFun sell");
947        };
948        assert_eq!(trade.token_balance, Some(0));
949        assert_eq!(trade.sol_balance, Some(40_000));
950    }
951
952    #[test]
953    fn optimized_rpc_balance_fill_keeps_malformed_amount_unknown() {
954        let user = Pubkey::new_unique();
955        let token_account = Pubkey::new_unique();
956        let mut rpc_tx = rpc_fixture(user, token_account);
957        let rpc_meta = rpc_tx.transaction.meta.as_mut().unwrap();
958        let OptionSerializer::Some(balances) = &mut rpc_meta.post_token_balances else {
959            panic!("post token balances");
960        };
961        balances[0].ui_token_amount.amount = "invalid".to_string();
962        let (meta, transaction) =
963            convert_rpc_to_grpc_for_parsing(&rpc_tx).expect("convert RPC fixture for parsing");
964        let mut events = vec![DexEvent::PumpFunBuy(PumpFunTradeEvent {
965            user,
966            associated_user: token_account,
967            ..Default::default()
968        })];
969
970        fill_rpc_event_metadata(&mut events, &rpc_tx, &meta, &Some(transaction));
971
972        let DexEvent::PumpFunBuy(trade) = &events[0] else {
973            panic!("expected PumpFun buy");
974        };
975        assert_eq!(trade.token_balance, None);
976        assert_eq!(trade.sol_balance, Some(40_000));
977    }
978
979    #[test]
980    fn invalid_rpc_loaded_address_returns_error_instead_of_panicking() {
981        let mut rpc_tx = rpc_fixture(Pubkey::new_unique(), Pubkey::new_unique());
982        rpc_tx.transaction.meta.as_mut().unwrap().loaded_addresses =
983            OptionSerializer::Some(UiLoadedAddresses {
984                writable: vec!["not-a-pubkey".to_string()],
985                readonly: Vec::new(),
986            });
987
988        let error = convert_rpc_to_grpc(&rpc_tx).expect_err("invalid address must fail");
989        assert!(
990            matches!(error, ParseError::ConversionError(message) if message.contains("Invalid loaded address"))
991        );
992    }
993
994    #[test]
995    fn base58_rpc_transaction_is_supported() {
996        let mut rpc_tx = rpc_fixture(Pubkey::new_unique(), Pubkey::new_unique());
997        let EncodedTransaction::Binary(data, TransactionBinaryEncoding::Base64) =
998            &rpc_tx.transaction.transaction
999        else {
1000            panic!("expected base64 fixture");
1001        };
1002        let bytes = general_purpose::STANDARD.decode(data).expect("decode fixture");
1003        rpc_tx.transaction.transaction = EncodedTransaction::Binary(
1004            bs58::encode(bytes).into_string(),
1005            TransactionBinaryEncoding::Base58,
1006        );
1007
1008        convert_rpc_to_grpc(&rpc_tx).expect("base58 binary transaction must decode");
1009    }
1010
1011    #[test]
1012    fn unsanitized_rpc_transaction_is_rejected() {
1013        let mut rpc_tx = rpc_fixture(Pubkey::new_unique(), Pubkey::new_unique());
1014        let invalid = VersionedTransaction {
1015            signatures: vec![Signature::from([7; 64])],
1016            message: VersionedMessage::Legacy(legacy::Message {
1017                header: MessageHeader { num_required_signatures: 1, ..Default::default() },
1018                account_keys: vec![Pubkey::new_unique()],
1019                recent_blockhash: Hash::new_unique(),
1020                instructions: vec![
1021                    solana_sdk::message::compiled_instruction::CompiledInstruction {
1022                        program_id_index: 9,
1023                        accounts: Vec::new(),
1024                        data: Vec::new(),
1025                    },
1026                ],
1027            }),
1028        };
1029        let bytes = wincode::serialize(&invalid).expect("serialize invalid fixture");
1030        rpc_tx.transaction.transaction = EncodedTransaction::Binary(
1031            general_purpose::STANDARD.encode(bytes),
1032            TransactionBinaryEncoding::Base64,
1033        );
1034
1035        let error = convert_rpc_to_grpc(&rpc_tx).expect_err("unsanitized transaction must fail");
1036        assert!(matches!(
1037            error,
1038            ParseError::ConversionError(message) if message.contains("decode or sanitize")
1039        ));
1040    }
1041}