1use crate::{convert_rpc_to_grpc, ParseError};
7use serde::{Deserialize, Serialize};
8use solana_sdk::{pubkey::Pubkey, signature::Signature};
9use solana_transaction_status::EncodedConfirmedTransactionWithStatusMeta;
10use std::collections::HashMap;
11use yellowstone_grpc_proto::prelude::{Transaction, TransactionStatusMeta};
12
13#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
14pub struct InstructionPosition {
15 pub outer_index: u32,
16 pub inner_index: Option<u32>,
17 pub stack_height: Option<u32>,
18}
19
20#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
21pub enum SwapProtocol {
22 PumpSwap,
23 LaunchLab,
24 RaydiumCpmm,
25 RaydiumAmmV4,
26 RaydiumClmm,
27 OrcaWhirlpool,
28 MeteoraDlmm,
29}
30
31#[derive(Clone, Debug, Serialize, Deserialize)]
32pub struct RouteSwapLeg {
33 pub position: InstructionPosition,
34 pub program: Pubkey,
35 pub protocol: SwapProtocol,
36 pub pool: Pubkey,
37 pub trader: Pubkey,
38 pub input_account: Pubkey,
39 pub output_account: Pubkey,
40 pub input_mint: Option<Pubkey>,
41 pub output_mint: Option<Pubkey>,
42 pub amount_specified_is_input: bool,
44 pub specified_amount: u64,
45 pub other_amount_threshold: u64,
46 pub actual_input_amount: Option<u64>,
49 pub actual_output_amount: Option<u64>,
50 pub stonkfun_mode: Option<crate::core::events::StonkFunMode>,
51 pub stonkfun_graduated: bool,
53}
54
55#[derive(Clone, Debug, Serialize, Deserialize)]
56pub struct RouteTokenTransfer {
57 pub position: InstructionPosition,
58 pub program: Pubkey,
59 pub source: Pubkey,
60 pub destination: Pubkey,
61 pub mint: Option<Pubkey>,
62 pub amount: u64,
64 pub withheld_fee: Option<u64>,
65}
66
67#[derive(Clone, Debug, Serialize, Deserialize)]
68pub struct RouteUnknownInvocation {
69 pub position: InstructionPosition,
70 pub program: Pubkey,
71 pub has_token_transfers: bool,
73 pub has_known_swap_descendants: bool,
76}
77
78#[derive(Clone, Debug, Serialize, Deserialize)]
79pub struct TransactionRoute {
80 pub signature: Signature,
81 pub succeeded: bool,
82 pub legs: Vec<RouteSwapLeg>,
83 pub transfers: Vec<RouteTokenTransfer>,
84 #[serde(default)]
87 pub native_token_actions: Vec<RouteNativeTokenAction>,
88 pub unknown_invocations: Vec<RouteUnknownInvocation>,
91}
92
93#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
94pub enum NativeTokenAction {
95 Fund {
97 source: Pubkey,
98 lamports: u64,
99 },
100 SyncNative,
101 Close {
104 destination: Pubkey,
105 authority: Pubkey,
106 },
107}
108
109#[derive(Clone, Debug, Serialize, Deserialize)]
110pub struct RouteNativeTokenAction {
111 pub position: InstructionPosition,
112 pub account: Pubkey,
113 pub action: NativeTokenAction,
114}
115
116fn native_action(
117 ix: &Invocation<'_>,
118 keys: &[Pubkey],
119 mints: &HashMap<Pubkey, Pubkey>,
120) -> Option<RouteNativeTokenAction> {
121 let wsol = solana_sdk::pubkey!("So11111111111111111111111111111111111111112");
122 let (target, action) = if ix.program == Pubkey::default()
123 && ix.data.get(..4) == Some(&2u32.to_le_bytes())
124 && ix.data.len() == 12
125 && ix.accounts.len() >= 2
126 {
127 (
128 account(ix, keys, 1),
129 NativeTokenAction::Fund { source: account(ix, keys, 0), lamports: u64_at(ix.data, 4)? },
130 )
131 } else if token_program(ix.program) && ix.data == [17] && !ix.accounts.is_empty() {
132 (account(ix, keys, 0), NativeTokenAction::SyncNative)
133 } else if token_program(ix.program) && ix.data == [9] && ix.accounts.len() >= 3 {
134 (
135 account(ix, keys, 0),
136 NativeTokenAction::Close {
137 destination: account(ix, keys, 1),
138 authority: account(ix, keys, 2),
139 },
140 )
141 } else {
142 return None;
143 };
144 if mints.get(&target) != Some(&wsol) {
145 return None;
146 }
147 Some(RouteNativeTokenAction { position: ix.position, account: target, action })
148}
149
150struct Invocation<'a> {
151 position: InstructionPosition,
152 program: Pubkey,
153 accounts: &'a [u8],
154 data: &'a [u8],
155}
156
157fn key(keys: &[Pubkey], index: u32) -> Pubkey {
158 keys.get(index as usize).copied().unwrap_or_default()
159}
160fn account(ix: &Invocation<'_>, keys: &[Pubkey], index: usize) -> Pubkey {
161 ix.accounts.get(index).map(|i| key(keys, u32::from(*i))).unwrap_or_default()
162}
163fn u64_at(data: &[u8], offset: usize) -> Option<u64> {
164 Some(u64::from_le_bytes(data.get(offset..offset + 8)?.try_into().ok()?))
165}
166fn token_program(program: Pubkey) -> bool {
167 program == solana_sdk::pubkey!("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA")
168 || program == solana_sdk::pubkey!("TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb")
169}
170fn checked_transfer(ix: &Invocation<'_>) -> bool {
171 token_program(ix.program)
172 && ix.data.first() == Some(&12)
173 && ix.data.len() >= 10
174 && ix.accounts.len() >= 4
175}
176fn checked_transfer_with_fee(ix: &Invocation<'_>) -> bool {
177 ix.program == solana_sdk::pubkey!("TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb")
178 && ix.data.get(..2) == Some(&[26, 1])
179 && ix.data.len() >= 19
180 && ix.accounts.len() >= 4
181}
182fn descendant(parent: InstructionPosition, child: InstructionPosition) -> bool {
183 if parent.outer_index != child.outer_index {
184 return false;
185 }
186 match (parent.inner_index, child.inner_index) {
187 (None, Some(_)) => true,
188 (Some(a), Some(b)) if b > a => match (parent.stack_height, child.stack_height) {
189 (Some(a), Some(b)) => b > a,
190 _ => false,
191 },
192 _ => false,
193 }
194}
195fn descendants<'a>(index: usize, invocations: &'a [Invocation<'_>]) -> &'a [Invocation<'a>] {
196 let parent = invocations[index].position;
197 let end = (index + 1..invocations.len())
198 .find(|&i| !descendant(parent, invocations[i].position))
199 .unwrap_or(invocations.len());
200 &invocations[index + 1..end]
201}
202
203pub(crate) fn transaction_token_mints(
207 transaction: &Transaction,
208 meta: &TransactionStatusMeta,
209) -> HashMap<Pubkey, Pubkey> {
210 let keys = transaction_keys(transaction, meta);
211 let invocations = transaction_invocations(transaction, meta, &keys);
212 let mut mints = HashMap::new();
213 for balance in meta.pre_token_balances.iter().chain(&meta.post_token_balances) {
214 if let Ok(mint) = balance.mint.parse() {
215 mints.insert(key(&keys, balance.account_index), mint);
216 }
217 }
218 for ix in &invocations {
219 if checked_transfer(ix) {
220 let mint = account(ix, &keys, 1);
221 mints.insert(account(ix, &keys, 0), mint);
222 mints.insert(account(ix, &keys, 2), mint);
223 }
224 if checked_transfer_with_fee(ix) {
226 let mint = account(ix, &keys, 1);
227 mints.insert(account(ix, &keys, 0), mint);
228 mints.insert(account(ix, &keys, 2), mint);
229 }
230 if token_program(ix.program)
233 && ix.accounts.len() >= 2
234 && matches!((ix.data.first(), ix.data.len()), (Some(1), 1) | (Some(16 | 18), 33))
235 {
236 mints.insert(account(ix, &keys, 0), account(ix, &keys, 1));
237 }
238 }
239 loop {
241 let before = mints.len();
242 for ix in &invocations {
243 if token_program(ix.program)
244 && ix.data.first() == Some(&3)
245 && ix.data.len() >= 9
246 && ix.accounts.len() >= 3
247 {
248 let source = account(ix, &keys, 0);
249 let destination = account(ix, &keys, 1);
250 if let Some(mint) = mints.get(&source).or_else(|| mints.get(&destination)).copied()
251 {
252 mints.entry(source).or_insert(mint);
253 mints.entry(destination).or_insert(mint);
254 }
255 }
256 }
257 if mints.len() == before {
258 break;
259 }
260 }
261 mints.remove(&Pubkey::default());
262 mints
263}
264fn transaction_keys(transaction: &Transaction, meta: &TransactionStatusMeta) -> Vec<Pubkey> {
265 transaction
266 .message
267 .iter()
268 .flat_map(|m| &m.account_keys)
269 .chain(&meta.loaded_writable_addresses)
270 .chain(&meta.loaded_readonly_addresses)
271 .map(|bytes| crate::instr::read_pubkey_fast(bytes))
272 .collect()
273}
274fn transaction_invocations<'a>(
275 transaction: &'a Transaction,
276 meta: &'a TransactionStatusMeta,
277 keys: &[Pubkey],
278) -> Vec<Invocation<'a>> {
279 let mut result = Vec::new();
280 if let Some(message) = &transaction.message {
281 for (i, ix) in message.instructions.iter().enumerate() {
282 result.push(Invocation {
283 position: InstructionPosition {
284 outer_index: i as u32,
285 inner_index: None,
286 stack_height: Some(1),
287 },
288 program: key(keys, ix.program_id_index),
289 accounts: &ix.accounts,
290 data: &ix.data,
291 });
292 for group in meta.inner_instructions.iter().filter(|g| g.index == i as u32) {
293 for (j, ix) in group.instructions.iter().enumerate() {
294 result.push(Invocation {
295 position: InstructionPosition {
296 outer_index: i as u32,
297 inner_index: Some(j as u32),
298 stack_height: ix.stack_height,
299 },
300 program: key(keys, ix.program_id_index),
301 accounts: &ix.accounts,
302 data: &ix.data,
303 });
304 }
305 }
306 }
307 }
308 result
309}
310fn transfer(
311 ix: &Invocation<'_>,
312 keys: &[Pubkey],
313 mints: &HashMap<Pubkey, Pubkey>,
314) -> Option<RouteTokenTransfer> {
315 if !token_program(ix.program) {
316 return None;
317 }
318 let (destination_index, amount_offset, fee) = match ix.data.first()? {
319 3 if ix.accounts.len() >= 3 && ix.data.len() >= 9 => (1, 1, Some(0)),
320 12 if checked_transfer(ix) => (
321 2,
322 1,
323 if ix.program == solana_sdk::pubkey!("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA") {
324 Some(0)
325 } else {
326 None
327 },
328 ),
329 26 if checked_transfer_with_fee(ix) => (2, 2, Some(u64_at(ix.data, 11)?)),
330 _ => return None,
331 };
332 let source = account(ix, keys, 0);
333 Some(RouteTokenTransfer {
334 position: ix.position,
335 program: ix.program,
336 source,
337 destination: account(ix, keys, destination_index),
338 mint: mints.get(&source).copied(),
339 amount: u64_at(ix.data, amount_offset)?,
340 withheld_fee: fee,
341 })
342}
343
344fn swap(
345 ix: &Invocation<'_>,
346 keys: &[Pubkey],
347 mints: &HashMap<Pubkey, Pubkey>,
348 graduated: &[Pubkey],
349) -> Option<RouteSwapLeg> {
350 use crate::instr::program_ids::*;
351 let a = |i| account(ix, keys, i);
352 let disc = ix.data.get(..8);
353 let swap_disc = Some(&[248, 198, 158, 145, 225, 117, 135, 200][..]);
354 let swap_v2 = Some(&[43, 4, 237, 11, 26, 201, 30, 98][..]);
355 let mut mode = None;
356 let (protocol, pool, trader, input, output, exact_in, amount, threshold) = if ix.program
357 == RAYDIUM_CLMM_PROGRAM_ID
358 && (disc == swap_disc || disc == swap_v2)
359 && ix.accounts.len() >= 10
360 {
361 (
362 SwapProtocol::RaydiumClmm,
363 a(2),
364 a(0),
365 a(3),
366 a(4),
367 *ix.data.get(40)? != 0,
368 u64_at(ix.data, 8)?,
369 u64_at(ix.data, 16)?,
370 )
371 } else if ix.program == ORCA_WHIRLPOOL_PROGRAM_ID && (disc == swap_disc || disc == swap_v2) {
372 let v2 = disc == swap_v2;
373 let direction = *ix.data.get(41)? != 0;
374 let (input, output) = if v2 {
375 if ix.accounts.len() < 15 {
376 return None;
377 }
378 (a(if direction { 7 } else { 9 }), a(if direction { 9 } else { 7 }))
379 } else {
380 if ix.accounts.len() < 11 {
381 return None;
382 }
383 (a(if direction { 3 } else { 5 }), a(if direction { 5 } else { 3 }))
384 };
385 (
386 SwapProtocol::OrcaWhirlpool,
387 a(if v2 { 4 } else { 2 }),
388 a(if v2 { 3 } else { 1 }),
389 input,
390 output,
391 *ix.data.get(40)? != 0,
392 u64_at(ix.data, 8)?,
393 u64_at(ix.data, 16)?,
394 )
395 } else if ix.program == RAYDIUM_CPMM_PROGRAM_ID && ix.accounts.len() >= 13 {
396 use crate::instr::raydium_cpmm::discriminators::*;
397 let exact_in = if disc == Some(&SWAP_BASE_IN[..]) {
398 true
399 } else if disc == Some(&SWAP_BASE_OUT[..]) {
400 false
401 } else {
402 return None;
403 };
404 let first = u64_at(ix.data, 8)?;
405 let second = u64_at(ix.data, 16)?;
406 (
407 SwapProtocol::RaydiumCpmm,
408 a(3),
409 a(0),
410 a(4),
411 a(5),
412 exact_in,
413 if exact_in { first } else { second },
414 if exact_in { second } else { first },
415 )
416 } else if ix.program == METEORA_DLMM_PROGRAM_ID && ix.accounts.len() >= 11 {
417 use crate::instr::meteora_dlmm::discriminators::*;
418 let exact_in = if disc == Some(&SWAP[..]) || disc == Some(&SWAP2[..]) {
419 true
420 } else if disc == Some(&SWAP_EXACT_OUT[..]) || disc == Some(&SWAP_EXACT_OUT2[..]) {
421 false
422 } else {
423 return None;
424 };
425 let first = u64_at(ix.data, 8)?;
426 let second = u64_at(ix.data, 16)?;
427 (
428 SwapProtocol::MeteoraDlmm,
429 a(0),
430 a(10),
431 a(4),
432 a(5),
433 exact_in,
434 if exact_in { first } else { second },
435 if exact_in { second } else { first },
436 )
437 } else if ix.program == RAYDIUM_LAUNCHLAB_PROGRAM_ID && ix.accounts.len() >= 18 {
438 use crate::instr::raydium_launchlab::discriminators::*;
439 let (buy, exact_in) = if disc == Some(&BUY_EXACT_IN[..]) {
440 (true, true)
441 } else if disc == Some(&SELL_EXACT_IN[..]) {
442 (false, true)
443 } else if disc == Some(&BUY_EXACT_OUT[..]) {
444 (true, false)
445 } else if disc == Some(&SELL_EXACT_OUT[..]) {
446 (false, false)
447 } else {
448 return None;
449 };
450 mode = crate::core::events::stonkfun_mode_from_platform_config(a(3));
451 (
452 SwapProtocol::LaunchLab,
453 a(4),
454 a(0),
455 a(if buy { 6 } else { 5 }),
456 a(if buy { 5 } else { 6 }),
457 exact_in,
458 u64_at(ix.data, 8)?,
459 u64_at(ix.data, 16)?,
460 )
461 } else if ix.program == PUMPSWAP_PROGRAM_ID && ix.accounts.len() >= 21 {
462 use crate::instr::pump_amm::discriminators::*;
463 let (buy, exact_in) = if disc == Some(&BUY_EXACT_QUOTE_IN[..]) {
464 (true, true)
465 } else if disc == Some(&BUY[..]) {
466 (true, false)
467 } else if disc == Some(&SELL[..]) {
468 (false, true)
469 } else {
470 return None;
471 };
472 (
473 SwapProtocol::PumpSwap,
474 a(0),
475 a(1),
476 a(if buy { 6 } else { 5 }),
477 a(if buy { 5 } else { 6 }),
478 exact_in,
479 u64_at(ix.data, 8)?,
480 u64_at(ix.data, 16)?,
481 )
482 } else if ix.program == RAYDIUM_AMM_V4_PROGRAM_ID
483 && matches!(ix.data.first(), Some(16 | 17))
484 && ix.accounts.len() >= 8
485 {
486 let exact_in = ix.data[0] == 16;
487 let first = u64_at(ix.data, 1)?;
488 let second = u64_at(ix.data, 9)?;
489 (
490 SwapProtocol::RaydiumAmmV4,
491 a(1),
492 a(7),
493 a(5),
494 a(6),
495 exact_in,
496 if exact_in { first } else { second },
497 if exact_in { second } else { first },
498 )
499 } else if ix.program == RAYDIUM_AMM_V4_PROGRAM_ID
500 && matches!(ix.data.first(), Some(9 | 11))
501 && ix.accounts.len() >= 17
502 {
503 let n = ix.accounts.len();
505 let exact_in = ix.data[0] == 9;
506 let first = u64_at(ix.data, 1)?;
507 let second = u64_at(ix.data, 9)?;
508 (
509 SwapProtocol::RaydiumAmmV4,
510 a(1),
511 a(n - 1),
512 a(n - 3),
513 a(n - 2),
514 exact_in,
515 if exact_in { first } else { second },
516 if exact_in { second } else { first },
517 )
518 } else {
519 return None;
520 };
521 let explicit_pair = match protocol {
522 SwapProtocol::LaunchLab => Some(if input == a(6) { (a(10), a(9)) } else { (a(9), a(10)) }),
523 SwapProtocol::RaydiumCpmm => Some((a(10), a(11))),
524 SwapProtocol::RaydiumClmm if disc == swap_v2 && ix.accounts.len() >= 13 => {
525 Some((a(11), a(12)))
526 }
527 SwapProtocol::OrcaWhirlpool if disc == swap_v2 => {
528 Some(if input == a(7) { (a(5), a(6)) } else { (a(6), a(5)) })
529 }
530 SwapProtocol::PumpSwap => Some(if input == a(6) { (a(4), a(3)) } else { (a(3), a(4)) }),
531 _ => None,
532 };
533 Some(RouteSwapLeg {
534 position: ix.position,
535 program: ix.program,
536 protocol,
537 pool,
538 trader,
539 input_account: input,
540 output_account: output,
541 input_mint: mints
542 .get(&input)
543 .copied()
544 .or_else(|| explicit_pair.map(|pair| pair.0).filter(|mint| *mint != Pubkey::default())),
545 output_mint: mints
546 .get(&output)
547 .copied()
548 .or_else(|| explicit_pair.map(|pair| pair.1).filter(|mint| *mint != Pubkey::default())),
549 amount_specified_is_input: exact_in,
550 specified_amount: amount,
551 other_amount_threshold: threshold,
552 actual_input_amount: None,
553 actual_output_amount: None,
554 stonkfun_mode: mode,
555 stonkfun_graduated: protocol == SwapProtocol::RaydiumCpmm && graduated.contains(&pool),
556 })
557}
558
559#[cfg(test)]
560mod tests {
561 use super::*;
562 #[test]
563 fn native_actions_require_wsol_identity_and_preserve_failure_status() {
564 use yellowstone_grpc_proto::prelude::{CompiledInstruction, Message, TransactionError};
565 let token = solana_sdk::pubkey!("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA");
566 let wsol = solana_sdk::pubkey!("So11111111111111111111111111111111111111112");
567 let keys = [Pubkey::default(), token, Pubkey::new_unique(), Pubkey::new_unique(), wsol];
568 let mut init = vec![18];
569 init.extend_from_slice(keys[2].as_ref());
570 let mut funding = 2u32.to_le_bytes().to_vec();
571 funding.extend_from_slice(&123u64.to_le_bytes());
572 let mut tx = Transaction {
573 signatures: vec![],
574 message: Some(Message {
575 account_keys: keys.iter().map(|k| k.to_bytes().to_vec()).collect(),
576 instructions: vec![
577 CompiledInstruction { program_id_index: 1, accounts: vec![3, 4], data: init },
578 CompiledInstruction {
579 program_id_index: 0,
580 accounts: vec![2, 3],
581 data: funding,
582 },
583 CompiledInstruction { program_id_index: 1, accounts: vec![3], data: vec![17] },
584 CompiledInstruction {
585 program_id_index: 1,
586 accounts: vec![3, 2, 2],
587 data: vec![9],
588 },
589 ],
590 ..Default::default()
591 }),
592 ..Default::default()
593 };
594 let mut meta = TransactionStatusMeta::default();
595 let route = analyze_yellowstone_transaction_routes(&tx, &meta, &[]);
596 assert!(route.succeeded);
597 assert_eq!(route.native_token_actions.len(), 3);
598 assert_eq!(
599 route.native_token_actions[0].action,
600 NativeTokenAction::Fund { source: keys[2], lamports: 123 }
601 );
602 meta.err = Some(TransactionError { err: vec![1] });
603 let failed = analyze_yellowstone_transaction_routes(&tx, &meta, &[]);
604 assert!(!failed.succeeded);
605 assert_eq!(failed.native_token_actions.len(), 3);
606 tx.message.as_mut().unwrap().account_keys[4] = Pubkey::new_unique().to_bytes().to_vec();
607 assert!(analyze_yellowstone_transaction_routes(&tx, &meta, &[])
608 .native_token_actions
609 .is_empty());
610 }
611 #[test]
612 fn ephemeral_initialized_accounts_propagate_mints_without_balance_snapshots() {
613 use yellowstone_grpc_proto::prelude::{CompiledInstruction, Message};
614 let program = solana_sdk::pubkey!("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA");
615 let keys = [
616 program,
617 Pubkey::new_unique(),
618 Pubkey::new_unique(),
619 Pubkey::new_unique(),
620 Pubkey::new_unique(),
621 ];
622 let mut initialize = vec![18];
623 initialize.extend_from_slice(keys[4].as_ref());
624 let mut plain = vec![3];
625 plain.extend_from_slice(&100u64.to_le_bytes());
626 let transaction = Transaction {
627 signatures: vec![],
628 message: Some(Message {
629 account_keys: keys.iter().map(|key| key.to_bytes().to_vec()).collect(),
630 instructions: vec![
631 CompiledInstruction {
632 program_id_index: 0,
633 accounts: vec![1, 2],
634 data: initialize,
635 },
636 CompiledInstruction {
637 program_id_index: 0,
638 accounts: vec![1, 3, 4],
639 data: plain,
640 },
641 ],
642 ..Default::default()
643 }),
644 };
645 let meta = TransactionStatusMeta::default();
646 let mints = transaction_token_mints(&transaction, &meta);
647 assert_eq!(mints.get(&keys[1]), Some(&keys[2]));
648 assert_eq!(mints.get(&keys[3]), Some(&keys[2]));
649 let route = analyze_yellowstone_transaction_routes(&transaction, &meta, &[]);
650 assert_eq!(route.transfers[0].mint, Some(keys[2]));
651 }
652 #[test]
653 fn malformed_checked_transfers_do_not_invent_amounts_or_mints() {
654 let keys: Vec<_> = (0..4).map(|_| Pubkey::new_unique()).collect();
655 let mut data = vec![12];
656 data.extend_from_slice(&123u64.to_le_bytes());
657 let mut ix = Invocation {
658 position: InstructionPosition {
659 outer_index: 0,
660 inner_index: None,
661 stack_height: Some(1),
662 },
663 program: solana_sdk::pubkey!("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"),
664 accounts: &[0, 1, 2, 3],
665 data: &data,
666 };
667 assert!(!checked_transfer(&ix));
668 assert!(transfer(&ix, &keys, &HashMap::new()).is_none());
669 let mut complete_data = data.clone();
670 complete_data.push(6);
671 ix.data = &complete_data;
672 assert!(checked_transfer(&ix));
673 assert_eq!(transfer(&ix, &keys, &HashMap::new()).unwrap().amount, 123);
674 let mut fee_data = vec![26, 1];
675 fee_data.extend_from_slice(&123u64.to_le_bytes());
676 fee_data.push(6);
677 fee_data.extend_from_slice(&2u64.to_le_bytes());
678 ix.data = &fee_data;
679 assert!(transfer(&ix, &keys, &HashMap::new()).is_none());
680 ix.program = solana_sdk::pubkey!("TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb");
681 assert_eq!(transfer(&ix, &keys, &HashMap::new()).unwrap().withheld_fee, Some(2));
682 }
683}
684
685pub fn analyze_yellowstone_transaction_routes(
689 transaction: &Transaction,
690 meta: &TransactionStatusMeta,
691 graduated_stonkfun_pools: &[Pubkey],
692) -> TransactionRoute {
693 let keys = transaction_keys(transaction, meta);
694 let invocations = transaction_invocations(transaction, meta, &keys);
695 let mints = transaction_token_mints(transaction, meta);
696 let transfers: Vec<_> =
697 invocations.iter().filter_map(|ix| transfer(ix, &keys, &mints)).collect();
698 let succeeded = meta.err.is_none();
699 let mut legs = Vec::new();
700 let mut unknown = Vec::new();
701 for (i, ix) in invocations.iter().enumerate() {
702 let children = descendants(i, &invocations);
703 let nested: Vec<_> =
704 children.iter().filter_map(|child| transfer(child, &keys, &mints)).collect();
705 if let Some(mut leg) = swap(ix, &keys, &mints, graduated_stonkfun_pools) {
706 if succeeded && !nested.is_empty() {
707 let inputs: Vec<_> =
708 nested.iter().filter(|t| t.source == leg.input_account).collect();
709 let outputs: Vec<_> =
710 nested.iter().filter(|t| t.destination == leg.output_account).collect();
711 if !inputs.is_empty() {
712 leg.actual_input_amount =
713 inputs.iter().try_fold(0u64, |sum, t| sum.checked_add(t.amount));
714 }
715 if !outputs.is_empty() {
716 leg.actual_output_amount = outputs.iter().try_fold(0u64, |sum, t| {
717 sum.checked_add(t.amount.checked_sub(t.withheld_fee?)?)
718 });
719 }
720 }
721 legs.push(leg);
722 } else if !token_program(ix.program)
723 && ix.program != Pubkey::default()
724 && ix.program != solana_sdk::pubkey!("ComputeBudget111111111111111111111111111111")
725 && ix.program != solana_sdk::pubkey!("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL")
726 && ix.program != solana_sdk::pubkey!("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr")
727 {
728 unknown.push(RouteUnknownInvocation {
730 position: ix.position,
731 program: ix.program,
732 has_token_transfers: !nested.is_empty(),
733 has_known_swap_descendants: children
734 .iter()
735 .any(|child| swap(child, &keys, &mints, graduated_stonkfun_pools).is_some()),
736 });
737 }
738 }
739 TransactionRoute {
740 signature: transaction
741 .signatures
742 .first()
743 .and_then(|b| Signature::try_from(b.as_slice()).ok())
744 .unwrap_or_default(),
745 succeeded,
746 legs,
747 transfers,
748 native_token_actions: invocations
749 .iter()
750 .filter_map(|ix| native_action(ix, &keys, &mints))
751 .collect(),
752 unknown_invocations: unknown,
753 }
754}
755
756pub fn analyze_rpc_transaction_routes(
757 transaction: &EncodedConfirmedTransactionWithStatusMeta,
758 graduated_stonkfun_pools: &[Pubkey],
759) -> Result<TransactionRoute, ParseError> {
760 let (meta, transaction) = convert_rpc_to_grpc(transaction)?;
761 Ok(analyze_yellowstone_transaction_routes(&transaction, &meta, graduated_stonkfun_pools))
762}