1use crate::core::events::DexEvent;
7use crate::grpc::instruction_parser::parse_instructions_enhanced;
8use crate::grpc::types::EventTypeFilter;
9use crate::instr::read_pubkey_fast;
10use base64::{engine::general_purpose, Engine as _};
11use solana_client::rpc_client::RpcClient;
12use solana_client::rpc_config::RpcTransactionConfig;
13use solana_sdk::pubkey::Pubkey;
14use solana_sdk::signature::Signature;
15use solana_transaction_status::{
16 EncodedConfirmedTransactionWithStatusMeta, EncodedTransaction, UiTransactionEncoding,
17};
18use std::collections::HashMap;
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 recent_blockhash = grpc_tx_opt.as_ref().and_then(|t| t.message.as_ref()).and_then(|m| {
107 if m.recent_blockhash.is_empty() {
108 None
109 } else {
110 Some(m.recent_blockhash.clone())
111 }
112 });
113
114 let mut program_invokes: HashMap<Pubkey, Vec<(i32, i32)>> = HashMap::new();
115
116 if let Some(ref tx) = grpc_tx_opt {
117 if let Some(ref msg) = tx.message {
118 let keys_len = msg.account_keys.len();
119 let writable_len = grpc_meta.loaded_writable_addresses.len();
120 let get_key = |i: usize| -> Option<&Vec<u8>> {
121 if i < keys_len {
122 msg.account_keys.get(i)
123 } else if i < keys_len + writable_len {
124 grpc_meta.loaded_writable_addresses.get(i - keys_len)
125 } else {
126 grpc_meta.loaded_readonly_addresses.get(i - keys_len - writable_len)
127 }
128 };
129
130 for (i, ix) in msg.instructions.iter().enumerate() {
131 let pid = get_key(ix.program_id_index as usize)
132 .map_or(Pubkey::default(), |k| read_pubkey_fast(k));
133 program_invokes.entry(pid).or_default().push((i as i32, -1));
134 }
135
136 for inner in &grpc_meta.inner_instructions {
137 let outer_idx = inner.index as usize;
138 for (j, inner_ix) in inner.instructions.iter().enumerate() {
139 let pid = get_key(inner_ix.program_id_index as usize)
140 .map_or(Pubkey::default(), |k| read_pubkey_fast(k));
141 program_invokes.entry(pid).or_default().push((outer_idx as i32, j as i32));
142 }
143 }
144 }
145 }
146
147 let mut events = parse_instructions_enhanced(
149 &grpc_meta,
150 &grpc_tx_opt,
151 signature,
152 slot,
153 0, block_time_us,
155 grpc_recv_us,
156 filter,
157 );
158
159 let mut is_created_buy = false;
161
162 for log in &grpc_meta.log_messages {
163 if let Some(mut event) = crate::logs::parse_log(
164 log,
165 signature,
166 slot,
167 0, block_time_us,
169 grpc_recv_us,
170 filter,
171 is_created_buy,
172 recent_blockhash.as_deref(),
173 ) {
174 if matches!(event, DexEvent::PumpFunCreate(_) | DexEvent::PumpFunCreateV2(_)) {
176 is_created_buy = true;
177 }
178
179 crate::core::account_dispatcher::fill_accounts_with_owned_keys(
181 &mut event,
182 &grpc_meta,
183 &grpc_tx_opt,
184 &program_invokes,
185 );
186
187 crate::core::common_filler::fill_data(
189 &mut event,
190 &grpc_meta,
191 &grpc_tx_opt,
192 &program_invokes,
193 );
194
195 events.push(event);
196 }
197 }
198
199 Ok(events)
200}
201
202#[derive(Debug)]
204pub enum ParseError {
205 RpcError(String),
206 ConversionError(String),
207 MissingField(String),
208}
209
210impl std::fmt::Display for ParseError {
211 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
212 match self {
213 ParseError::RpcError(msg) => write!(f, "RPC error: {}", msg),
214 ParseError::ConversionError(msg) => write!(f, "Conversion error: {}", msg),
215 ParseError::MissingField(msg) => write!(f, "Missing field: {}", msg),
216 }
217 }
218}
219
220impl std::error::Error for ParseError {}
221
222fn extract_signature(
227 rpc_tx: &EncodedConfirmedTransactionWithStatusMeta,
228) -> Result<Signature, ParseError> {
229 let ui_tx = &rpc_tx.transaction.transaction;
230
231 match ui_tx {
232 EncodedTransaction::Binary(data, _encoding) => {
233 let bytes = general_purpose::STANDARD.decode(data).map_err(|e| {
234 ParseError::ConversionError(format!("Failed to decode base64: {}", e))
235 })?;
236
237 let versioned_tx: solana_sdk::transaction::VersionedTransaction =
238 bincode::deserialize(&bytes).map_err(|e| {
239 ParseError::ConversionError(format!("Failed to deserialize transaction: {}", e))
240 })?;
241
242 Ok(versioned_tx.signatures[0])
243 }
244 _ => Err(ParseError::ConversionError("Unsupported transaction encoding".to_string())),
245 }
246}
247
248pub fn convert_rpc_to_grpc(
249 rpc_tx: &EncodedConfirmedTransactionWithStatusMeta,
250) -> Result<(TransactionStatusMeta, Transaction), ParseError> {
251 let rpc_meta = rpc_tx
252 .transaction
253 .meta
254 .as_ref()
255 .ok_or_else(|| ParseError::MissingField("meta".to_string()))?;
256
257 let mut grpc_meta = TransactionStatusMeta {
259 err: None,
260 fee: rpc_meta.fee,
261 pre_balances: rpc_meta.pre_balances.clone(),
262 post_balances: rpc_meta.post_balances.clone(),
263 inner_instructions: Vec::new(),
264 log_messages: {
265 let opt: Option<Vec<String>> = rpc_meta.log_messages.clone().into();
266 opt.unwrap_or_default()
267 },
268 pre_token_balances: Vec::new(),
269 post_token_balances: Vec::new(),
270 rewards: Vec::new(),
271 loaded_writable_addresses: {
272 let loaded_opt: Option<solana_transaction_status::UiLoadedAddresses> =
273 rpc_meta.loaded_addresses.clone().into();
274 loaded_opt
275 .map(|addrs| {
276 addrs
277 .writable
278 .iter()
279 .map(|pk_str| {
280 use std::str::FromStr;
281 solana_sdk::pubkey::Pubkey::from_str(pk_str)
282 .unwrap()
283 .to_bytes()
284 .to_vec()
285 })
286 .collect()
287 })
288 .unwrap_or_default()
289 },
290 loaded_readonly_addresses: {
291 let loaded_opt: Option<solana_transaction_status::UiLoadedAddresses> =
292 rpc_meta.loaded_addresses.clone().into();
293 loaded_opt
294 .map(|addrs| {
295 addrs
296 .readonly
297 .iter()
298 .map(|pk_str| {
299 use std::str::FromStr;
300 solana_sdk::pubkey::Pubkey::from_str(pk_str)
301 .unwrap()
302 .to_bytes()
303 .to_vec()
304 })
305 .collect()
306 })
307 .unwrap_or_default()
308 },
309 return_data: None,
310 compute_units_consumed: rpc_meta.compute_units_consumed.clone().into(),
311
312 inner_instructions_none: {
313 let opt: Option<Vec<_>> = rpc_meta.inner_instructions.clone().into();
314 opt.is_none()
315 },
316 log_messages_none: {
317 let opt: Option<Vec<String>> = rpc_meta.log_messages.clone().into();
318 opt.is_none()
319 },
320 return_data_none: {
321 let opt: Option<solana_transaction_status::UiTransactionReturnData> =
322 rpc_meta.return_data.clone().into();
323 opt.is_none()
324 },
325 cost_units: rpc_meta.compute_units_consumed.clone().into(),
326 };
327
328 let inner_instructions_opt: Option<Vec<_>> = rpc_meta.inner_instructions.clone().into();
330 if let Some(ref inner_instructions) = inner_instructions_opt {
331 for inner in inner_instructions {
332 let mut grpc_inner =
333 InnerInstructions { index: inner.index as u32, instructions: Vec::new() };
334
335 for ix in &inner.instructions {
336 if let solana_transaction_status::UiInstruction::Compiled(compiled) = ix {
337 let data = bs58::decode(&compiled.data).into_vec().map_err(|e| {
339 ParseError::ConversionError(format!(
340 "Failed to decode instruction data: {}",
341 e
342 ))
343 })?;
344
345 grpc_inner.instructions.push(InnerInstruction {
346 program_id_index: compiled.program_id_index as u32,
347 accounts: compiled.accounts.clone(),
348 data,
349 stack_height: compiled.stack_height.map(|h| h as u32),
350 });
351 }
352 }
353
354 grpc_meta.inner_instructions.push(grpc_inner);
355 }
356 }
357
358 let ui_tx = &rpc_tx.transaction.transaction;
360
361 let (message, signatures) = match ui_tx {
362 EncodedTransaction::Binary(data, _encoding) => {
363 let bytes = general_purpose::STANDARD.decode(data).map_err(|e| {
365 ParseError::ConversionError(format!("Failed to decode base64: {}", e))
366 })?;
367
368 let versioned_tx: solana_sdk::transaction::VersionedTransaction =
370 bincode::deserialize(&bytes).map_err(|e| {
371 ParseError::ConversionError(format!("Failed to deserialize transaction: {}", e))
372 })?;
373
374 let sigs: Vec<Vec<u8>> =
375 versioned_tx.signatures.iter().map(|s| s.as_ref().to_vec()).collect();
376
377 let message = match versioned_tx.message {
378 solana_sdk::message::VersionedMessage::Legacy(legacy_msg) => {
379 convert_legacy_message(&legacy_msg)?
380 }
381 solana_sdk::message::VersionedMessage::V0(v0_msg) => convert_v0_message(&v0_msg)?,
382 };
383
384 (message, sigs)
385 }
386 EncodedTransaction::Json(_) => {
387 return Err(ParseError::ConversionError(
388 "JSON encoded transactions not supported yet".to_string(),
389 ));
390 }
391 _ => {
392 return Err(ParseError::ConversionError(
393 "Unsupported transaction encoding".to_string(),
394 ));
395 }
396 };
397
398 let grpc_tx = Transaction { signatures, message: Some(message) };
399
400 Ok((grpc_meta, grpc_tx))
401}
402
403fn convert_legacy_message(
404 msg: &solana_sdk::message::legacy::Message,
405) -> Result<Message, ParseError> {
406 let account_keys: Vec<Vec<u8>> =
407 msg.account_keys.iter().map(|k| k.to_bytes().to_vec()).collect();
408
409 let instructions: Vec<CompiledInstruction> = msg
410 .instructions
411 .iter()
412 .map(|ix| CompiledInstruction {
413 program_id_index: ix.program_id_index as u32,
414 accounts: ix.accounts.clone(),
415 data: ix.data.clone(),
416 })
417 .collect();
418
419 Ok(Message {
420 header: Some(MessageHeader {
421 num_required_signatures: msg.header.num_required_signatures as u32,
422 num_readonly_signed_accounts: msg.header.num_readonly_signed_accounts as u32,
423 num_readonly_unsigned_accounts: msg.header.num_readonly_unsigned_accounts as u32,
424 }),
425 account_keys,
426 recent_blockhash: msg.recent_blockhash.to_bytes().to_vec(),
427 instructions,
428 versioned: false,
429 address_table_lookups: Vec::new(),
430 })
431}
432
433fn convert_v0_message(msg: &solana_sdk::message::v0::Message) -> Result<Message, ParseError> {
434 let account_keys: Vec<Vec<u8>> =
435 msg.account_keys.iter().map(|k| k.to_bytes().to_vec()).collect();
436
437 let instructions: Vec<CompiledInstruction> = msg
438 .instructions
439 .iter()
440 .map(|ix| CompiledInstruction {
441 program_id_index: ix.program_id_index as u32,
442 accounts: ix.accounts.clone(),
443 data: ix.data.clone(),
444 })
445 .collect();
446
447 Ok(Message {
448 header: Some(MessageHeader {
449 num_required_signatures: msg.header.num_required_signatures as u32,
450 num_readonly_signed_accounts: msg.header.num_readonly_signed_accounts as u32,
451 num_readonly_unsigned_accounts: msg.header.num_readonly_unsigned_accounts as u32,
452 }),
453 account_keys,
454 recent_blockhash: msg.recent_blockhash.to_bytes().to_vec(),
455 instructions,
456 versioned: true,
457 address_table_lookups: msg
458 .address_table_lookups
459 .iter()
460 .map(|lookup| MessageAddressTableLookup {
461 account_key: lookup.account_key.to_bytes().to_vec(),
462 writable_indexes: lookup.writable_indexes.clone(),
463 readonly_indexes: lookup.readonly_indexes.clone(),
464 })
465 .collect(),
466 })
467}