1use crate::core::events::DexEvent;
7use crate::grpc::types::EventTypeFilter;
8use crate::instr::read_pubkey_fast;
9use base64::{engine::general_purpose, Engine as _};
10use solana_client::rpc_client::RpcClient;
11use solana_client::rpc_config::RpcTransactionConfig;
12use solana_sdk::pubkey::Pubkey;
13use solana_sdk::signature::Signature;
14use solana_transaction_status::{
15 EncodedConfirmedTransactionWithStatusMeta, EncodedTransaction, UiTransactionEncoding,
16};
17use std::collections::HashMap;
18use std::str::FromStr;
19use yellowstone_grpc_proto::prelude::{
20 CompiledInstruction, InnerInstruction, InnerInstructions, Message, MessageAddressTableLookup,
21 MessageHeader, Transaction, TransactionStatusMeta,
22};
23
24pub fn parse_transaction_from_rpc(
46 rpc_client: &RpcClient,
47 signature: &Signature,
48 filter: Option<&EventTypeFilter>,
49) -> Result<Vec<DexEvent>, ParseError> {
50 let config = RpcTransactionConfig {
52 encoding: Some(UiTransactionEncoding::Base64),
53 commitment: None,
54 max_supported_transaction_version: Some(0),
55 };
56
57 let rpc_tx = rpc_client.get_transaction_with_config(signature, config).map_err(|e| {
58 let msg = e.to_string();
59 if msg.contains("invalid type: null") && msg.contains("EncodedConfirmedTransactionWithStatusMeta") {
60 ParseError::RpcError(format!(
61 "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: {}",
62 msg
63 ))
64 } else {
65 ParseError::RpcError(msg)
66 }
67 })?;
68
69 parse_rpc_transaction(&rpc_tx, filter)
70}
71
72pub fn parse_rpc_transaction(
89 rpc_tx: &EncodedConfirmedTransactionWithStatusMeta,
90 filter: Option<&EventTypeFilter>,
91) -> Result<Vec<DexEvent>, ParseError> {
92 let (grpc_meta, grpc_tx) = convert_rpc_to_grpc(rpc_tx)?;
94
95 let signature = extract_signature(rpc_tx)?;
97 let slot = rpc_tx.slot;
98 let block_time_us = rpc_tx.block_time.map(|t| t * 1_000_000);
99 let grpc_recv_us =
100 std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_micros()
101 as i64;
102
103 let grpc_tx_opt = Some(grpc_tx);
105
106 let mut program_invokes: HashMap<Pubkey, Vec<(i32, i32)>> = HashMap::new();
107
108 if let Some(ref tx) = grpc_tx_opt {
109 if let Some(ref msg) = tx.message {
110 let keys_len = msg.account_keys.len();
111 let writable_len = grpc_meta.loaded_writable_addresses.len();
112 let get_key = |i: usize| -> Option<&Vec<u8>> {
113 if i < keys_len {
114 msg.account_keys.get(i)
115 } else if i < keys_len + writable_len {
116 grpc_meta.loaded_writable_addresses.get(i - keys_len)
117 } else {
118 grpc_meta.loaded_readonly_addresses.get(i - keys_len - writable_len)
119 }
120 };
121
122 for (i, ix) in msg.instructions.iter().enumerate() {
123 let pid = get_key(ix.program_id_index as usize)
124 .map_or(Pubkey::default(), |k| read_pubkey_fast(k));
125 if crate::grpc::program_ids::needs_invoke_context(&pid) {
126 program_invokes.entry(pid).or_default().push((i as i32, -1));
127 }
128 }
129
130 for inner in &grpc_meta.inner_instructions {
131 let outer_idx = inner.index as usize;
132 for (j, inner_ix) in inner.instructions.iter().enumerate() {
133 let pid = get_key(inner_ix.program_id_index as usize)
134 .map_or(Pubkey::default(), |k| read_pubkey_fast(k));
135 if crate::grpc::program_ids::needs_invoke_context(&pid) {
136 program_invokes.entry(pid).or_default().push((outer_idx as i32, j as i32));
137 }
138 }
139 }
140 }
141 }
142
143 let needs_pumpfun = filter.map(EventTypeFilter::includes_pumpfun).unwrap_or(true);
144 let is_created_buy = needs_pumpfun
145 && crate::logs::optimized_matcher::detect_pumpfun_create(&grpc_meta.log_messages);
146
147 let instr_events =
149 crate::grpc::instruction_parser::parse_instructions_enhanced_with_created_buy(
150 &grpc_meta,
151 &grpc_tx_opt,
152 signature,
153 slot,
154 0, block_time_us,
156 grpc_recv_us,
157 filter,
158 is_created_buy,
159 );
160
161 struct ActiveProgram<'a> {
163 encoded: &'a str,
164 pubkey: Pubkey,
165 }
166
167 let mut active_program_stack: Vec<ActiveProgram<'_>> = Vec::with_capacity(8);
168 let mut log_events = Vec::new();
169
170 for log in &grpc_meta.log_messages {
171 if let Some((pid, depth)) = crate::logs::optimized_matcher::parse_invoke_info(log) {
172 let program_id = crate::grpc::program_ids::known_program_id(pid)
173 .or_else(|| Pubkey::from_str(pid).ok());
174 if let Some(pk) = program_id {
175 active_program_stack.truncate(depth.saturating_sub(1));
176 active_program_stack.push(ActiveProgram { encoded: pid, pubkey: pk });
177 }
178 }
179
180 if let Some(mut event) = crate::logs::parse_log_with_program_id(
181 log,
182 signature,
183 slot,
184 0, block_time_us,
186 grpc_recv_us,
187 filter,
188 is_created_buy,
189 None,
190 active_program_stack.last().map(|active| &active.pubkey),
191 ) {
192 crate::core::account_dispatcher::fill_accounts_with_owned_keys(
194 &mut event,
195 &grpc_meta,
196 &grpc_tx_opt,
197 &program_invokes,
198 );
199
200 crate::core::common_filler::fill_data(
202 &mut event,
203 &grpc_meta,
204 &grpc_tx_opt,
205 &program_invokes,
206 );
207
208 log_events.push(event);
209 }
210
211 if let Some(pid) = crate::logs::optimized_matcher::parse_program_complete_info(log) {
212 if let Some(pos) = active_program_stack.iter().rposition(|active| active.encoded == pid)
213 {
214 active_program_stack.truncate(pos);
215 }
216 }
217 }
218
219 let mut events = merge_log_and_instruction_events(log_events, instr_events);
220 crate::grpc::transaction_meta::fill_recent_blockhash(&mut events, &grpc_tx_opt);
221 Ok(events)
222}
223
224fn merge_log_and_instruction_events(
225 log_events: Vec<DexEvent>,
226 instr_events: Vec<DexEvent>,
227) -> Vec<DexEvent> {
228 crate::grpc::log_instr_dedup::dedupe_log_instruction_events(log_events, instr_events)
229}
230
231#[derive(Debug)]
233pub enum ParseError {
234 RpcError(String),
235 ConversionError(String),
236 MissingField(String),
237}
238
239impl std::fmt::Display for ParseError {
240 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
241 match self {
242 ParseError::RpcError(msg) => write!(f, "RPC error: {}", msg),
243 ParseError::ConversionError(msg) => write!(f, "Conversion error: {}", msg),
244 ParseError::MissingField(msg) => write!(f, "Missing field: {}", msg),
245 }
246 }
247}
248
249impl std::error::Error for ParseError {}
250
251fn extract_signature(
256 rpc_tx: &EncodedConfirmedTransactionWithStatusMeta,
257) -> Result<Signature, ParseError> {
258 let ui_tx = &rpc_tx.transaction.transaction;
259
260 match ui_tx {
261 EncodedTransaction::Binary(data, _encoding) => {
262 let bytes = general_purpose::STANDARD.decode(data).map_err(|e| {
263 ParseError::ConversionError(format!("Failed to decode base64: {}", e))
264 })?;
265
266 let versioned_tx: solana_sdk::transaction::VersionedTransaction =
267 bincode::deserialize(&bytes).map_err(|e| {
268 ParseError::ConversionError(format!("Failed to deserialize transaction: {}", e))
269 })?;
270
271 Ok(versioned_tx.signatures[0])
272 }
273 _ => Err(ParseError::ConversionError("Unsupported transaction encoding".to_string())),
274 }
275}
276
277pub fn convert_rpc_to_grpc(
278 rpc_tx: &EncodedConfirmedTransactionWithStatusMeta,
279) -> Result<(TransactionStatusMeta, Transaction), ParseError> {
280 let rpc_meta = rpc_tx
281 .transaction
282 .meta
283 .as_ref()
284 .ok_or_else(|| ParseError::MissingField("meta".to_string()))?;
285
286 let mut grpc_meta = TransactionStatusMeta {
288 err: None,
289 fee: rpc_meta.fee,
290 pre_balances: rpc_meta.pre_balances.clone(),
291 post_balances: rpc_meta.post_balances.clone(),
292 inner_instructions: Vec::new(),
293 log_messages: {
294 let opt: Option<Vec<String>> = rpc_meta.log_messages.clone().into();
295 opt.unwrap_or_default()
296 },
297 pre_token_balances: Vec::new(),
298 post_token_balances: Vec::new(),
299 rewards: Vec::new(),
300 loaded_writable_addresses: {
301 let loaded_opt: Option<solana_transaction_status::UiLoadedAddresses> =
302 rpc_meta.loaded_addresses.clone().into();
303 loaded_opt
304 .map(|addrs| {
305 addrs
306 .writable
307 .iter()
308 .map(|pk_str| {
309 use std::str::FromStr;
310 solana_sdk::pubkey::Pubkey::from_str(pk_str)
311 .unwrap()
312 .to_bytes()
313 .to_vec()
314 })
315 .collect()
316 })
317 .unwrap_or_default()
318 },
319 loaded_readonly_addresses: {
320 let loaded_opt: Option<solana_transaction_status::UiLoadedAddresses> =
321 rpc_meta.loaded_addresses.clone().into();
322 loaded_opt
323 .map(|addrs| {
324 addrs
325 .readonly
326 .iter()
327 .map(|pk_str| {
328 use std::str::FromStr;
329 solana_sdk::pubkey::Pubkey::from_str(pk_str)
330 .unwrap()
331 .to_bytes()
332 .to_vec()
333 })
334 .collect()
335 })
336 .unwrap_or_default()
337 },
338 return_data: None,
339 compute_units_consumed: rpc_meta.compute_units_consumed.clone().into(),
340
341 inner_instructions_none: {
342 let opt: Option<Vec<_>> = rpc_meta.inner_instructions.clone().into();
343 opt.is_none()
344 },
345 log_messages_none: {
346 let opt: Option<Vec<String>> = rpc_meta.log_messages.clone().into();
347 opt.is_none()
348 },
349 return_data_none: {
350 let opt: Option<solana_transaction_status::UiTransactionReturnData> =
351 rpc_meta.return_data.clone().into();
352 opt.is_none()
353 },
354 cost_units: rpc_meta.compute_units_consumed.clone().into(),
355 };
356
357 let inner_instructions_opt: Option<Vec<_>> = rpc_meta.inner_instructions.clone().into();
359 if let Some(ref inner_instructions) = inner_instructions_opt {
360 for inner in inner_instructions {
361 let mut grpc_inner =
362 InnerInstructions { index: inner.index as u32, instructions: Vec::new() };
363
364 for ix in &inner.instructions {
365 if let solana_transaction_status::UiInstruction::Compiled(compiled) = ix {
366 let data = bs58::decode(&compiled.data).into_vec().map_err(|e| {
368 ParseError::ConversionError(format!(
369 "Failed to decode instruction data: {}",
370 e
371 ))
372 })?;
373
374 grpc_inner.instructions.push(InnerInstruction {
375 program_id_index: compiled.program_id_index as u32,
376 accounts: compiled.accounts.clone(),
377 data,
378 stack_height: compiled.stack_height,
379 });
380 }
381 }
382
383 grpc_meta.inner_instructions.push(grpc_inner);
384 }
385 }
386
387 let ui_tx = &rpc_tx.transaction.transaction;
389
390 let (message, signatures) = match ui_tx {
391 EncodedTransaction::Binary(data, _encoding) => {
392 let bytes = general_purpose::STANDARD.decode(data).map_err(|e| {
394 ParseError::ConversionError(format!("Failed to decode base64: {}", e))
395 })?;
396
397 let versioned_tx: solana_sdk::transaction::VersionedTransaction =
399 bincode::deserialize(&bytes).map_err(|e| {
400 ParseError::ConversionError(format!("Failed to deserialize transaction: {}", e))
401 })?;
402
403 let sigs: Vec<Vec<u8>> =
404 versioned_tx.signatures.iter().map(|s| s.as_ref().to_vec()).collect();
405
406 let message = match versioned_tx.message {
407 solana_sdk::message::VersionedMessage::Legacy(legacy_msg) => {
408 convert_legacy_message(&legacy_msg)?
409 }
410 solana_sdk::message::VersionedMessage::V0(v0_msg) => convert_v0_message(&v0_msg)?,
411 };
412
413 (message, sigs)
414 }
415 EncodedTransaction::Json(_) => {
416 return Err(ParseError::ConversionError(
417 "JSON encoded transactions not supported yet".to_string(),
418 ));
419 }
420 _ => {
421 return Err(ParseError::ConversionError(
422 "Unsupported transaction encoding".to_string(),
423 ));
424 }
425 };
426
427 let grpc_tx = Transaction { signatures, message: Some(message) };
428
429 Ok((grpc_meta, grpc_tx))
430}
431
432fn convert_legacy_message(
433 msg: &solana_sdk::message::legacy::Message,
434) -> Result<Message, ParseError> {
435 let account_keys: Vec<Vec<u8>> =
436 msg.account_keys.iter().map(|k| k.to_bytes().to_vec()).collect();
437
438 let instructions: Vec<CompiledInstruction> = msg
439 .instructions
440 .iter()
441 .map(|ix| CompiledInstruction {
442 program_id_index: ix.program_id_index as u32,
443 accounts: ix.accounts.clone(),
444 data: ix.data.clone(),
445 })
446 .collect();
447
448 Ok(Message {
449 header: Some(MessageHeader {
450 num_required_signatures: msg.header.num_required_signatures as u32,
451 num_readonly_signed_accounts: msg.header.num_readonly_signed_accounts as u32,
452 num_readonly_unsigned_accounts: msg.header.num_readonly_unsigned_accounts as u32,
453 }),
454 account_keys,
455 recent_blockhash: msg.recent_blockhash.to_bytes().to_vec(),
456 instructions,
457 versioned: false,
458 address_table_lookups: Vec::new(),
459 })
460}
461
462fn convert_v0_message(msg: &solana_sdk::message::v0::Message) -> Result<Message, ParseError> {
463 let account_keys: Vec<Vec<u8>> =
464 msg.account_keys.iter().map(|k| k.to_bytes().to_vec()).collect();
465
466 let instructions: Vec<CompiledInstruction> = msg
467 .instructions
468 .iter()
469 .map(|ix| CompiledInstruction {
470 program_id_index: ix.program_id_index as u32,
471 accounts: ix.accounts.clone(),
472 data: ix.data.clone(),
473 })
474 .collect();
475
476 Ok(Message {
477 header: Some(MessageHeader {
478 num_required_signatures: msg.header.num_required_signatures as u32,
479 num_readonly_signed_accounts: msg.header.num_readonly_signed_accounts as u32,
480 num_readonly_unsigned_accounts: msg.header.num_readonly_unsigned_accounts as u32,
481 }),
482 account_keys,
483 recent_blockhash: msg.recent_blockhash.to_bytes().to_vec(),
484 instructions,
485 versioned: true,
486 address_table_lookups: msg
487 .address_table_lookups
488 .iter()
489 .map(|lookup| MessageAddressTableLookup {
490 account_key: lookup.account_key.to_bytes().to_vec(),
491 writable_indexes: lookup.writable_indexes.clone(),
492 readonly_indexes: lookup.readonly_indexes.clone(),
493 })
494 .collect(),
495 })
496}
497
498#[cfg(test)]
499mod tests {
500 use super::*;
501 use crate::core::events::{DexEvent, EventMetadata, PumpSwapCreatePoolEvent};
502 use solana_sdk::{pubkey::Pubkey, signature::Signature};
503
504 fn dummy_meta() -> EventMetadata {
505 EventMetadata {
506 signature: Signature::default(),
507 slot: 1,
508 tx_index: 0,
509 block_time_us: 0,
510 grpc_recv_us: 0,
511 recent_blockhash: None,
512 }
513 }
514
515 #[test]
516 fn rpc_merge_keeps_instruction_cashback_for_log_only_pumpswap_create_pool() {
517 let pool = Pubkey::new_unique();
518 let base_mint = Pubkey::new_unique();
519 let quote_mint = Pubkey::new_unique();
520
521 let log_create = PumpSwapCreatePoolEvent {
522 metadata: dummy_meta(),
523 pool,
524 base_mint,
525 quote_mint,
526 is_cashback_coin: false,
527 ..Default::default()
528 };
529 let ix_create = PumpSwapCreatePoolEvent {
530 metadata: dummy_meta(),
531 pool,
532 base_mint,
533 quote_mint,
534 is_cashback_coin: true,
535 ..Default::default()
536 };
537
538 let merged = merge_log_and_instruction_events(
539 vec![DexEvent::PumpSwapCreatePool(log_create)],
540 vec![DexEvent::PumpSwapCreatePool(ix_create)],
541 );
542
543 assert_eq!(merged.len(), 1);
544 match &merged[0] {
545 DexEvent::PumpSwapCreatePool(e) => assert!(e.is_cashback_coin),
546 other => panic!("expected PumpSwapCreatePool, got {other:?}"),
547 }
548 }
549}