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
13const ROUTE_SPL_TOKEN: Pubkey = solana_sdk::pubkey!("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA");
16const ROUTE_TOKEN_2022: Pubkey = solana_sdk::pubkey!("TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb");
17const ROUTE_WSOL: Pubkey = solana_sdk::pubkey!("So11111111111111111111111111111111111111112");
18const ROUTE_COMPUTE_BUDGET: Pubkey = solana_sdk::pubkey!("ComputeBudget111111111111111111111111111111");
19const ROUTE_ASSOCIATED_TOKEN: Pubkey = solana_sdk::pubkey!("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL");
20const ROUTE_MEMO: Pubkey = solana_sdk::pubkey!("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr");
21
22#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
23pub struct InstructionPosition {
24 pub outer_index: u32,
25 pub inner_index: Option<u32>,
26 pub stack_height: Option<u32>,
27}
28
29#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
30pub enum SwapProtocol {
31 PumpSwap,
32 LaunchLab,
33 RaydiumCpmm,
34 RaydiumAmmV4,
35 RaydiumClmm,
36 OrcaWhirlpool,
37 MeteoraDlmm,
38}
39
40#[derive(Clone, Debug, Serialize, Deserialize)]
41pub struct RouteSwapLeg {
42 pub position: InstructionPosition,
43 pub program: Pubkey,
44 pub protocol: SwapProtocol,
45 pub pool: Pubkey,
46 pub trader: Pubkey,
47 pub input_account: Pubkey,
48 pub output_account: Pubkey,
49 pub input_mint: Option<Pubkey>,
50 pub output_mint: Option<Pubkey>,
51 pub amount_specified_is_input: bool,
53 pub specified_amount: u64,
54 pub other_amount_threshold: u64,
55 pub actual_input_amount: Option<u64>,
58 pub actual_output_amount: Option<u64>,
59 pub stonkfun_mode: Option<crate::core::events::StonkFunMode>,
60 pub stonkfun_graduated: bool,
62}
63
64#[derive(Clone, Debug, Serialize, Deserialize)]
65pub struct RouteTokenTransfer {
66 pub position: InstructionPosition,
67 pub program: Pubkey,
68 pub source: Pubkey,
69 pub destination: Pubkey,
70 pub mint: Option<Pubkey>,
71 pub amount: u64,
73 pub withheld_fee: Option<u64>,
74}
75
76#[derive(Clone, Debug, Serialize, Deserialize)]
77pub struct RouteUnknownInvocation {
78 pub position: InstructionPosition,
79 pub program: Pubkey,
80 pub has_token_transfers: bool,
82 pub has_known_swap_descendants: bool,
85}
86
87#[derive(Clone, Debug, Serialize, Deserialize)]
88pub struct TransactionRoute {
89 pub signature: Signature,
90 pub succeeded: bool,
91 pub legs: Vec<RouteSwapLeg>,
92 pub transfers: Vec<RouteTokenTransfer>,
93 #[serde(default)]
96 pub native_token_actions: Vec<RouteNativeTokenAction>,
97 pub unknown_invocations: Vec<RouteUnknownInvocation>,
100}
101
102#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
103pub enum NativeTokenAction {
104 Fund {
106 source: Pubkey,
107 lamports: u64,
108 },
109 SyncNative,
110 Close {
113 destination: Pubkey,
114 authority: Pubkey,
115 },
116}
117
118#[derive(Clone, Debug, Serialize, Deserialize)]
119pub struct RouteNativeTokenAction {
120 pub position: InstructionPosition,
121 pub account: Pubkey,
122 pub action: NativeTokenAction,
123}
124
125fn native_action(
126 ix: &Invocation<'_>,
127 keys: &[Pubkey],
128 mints: &HashMap<Pubkey, Pubkey>,
129) -> Option<RouteNativeTokenAction> {
130 if !ix.program_resolved {
131 return None;
132 }
133 let wsol = ROUTE_WSOL;
134 let (target, action) = if ix.program == Pubkey::default()
135 && ix.data.get(..4) == Some(&2u32.to_le_bytes())
136 && ix.data.len() == 12
137 && ix.accounts.len() >= 2
138 {
139 (
140 account(ix, keys, 1),
141 NativeTokenAction::Fund { source: account(ix, keys, 0), lamports: u64_at(ix.data, 4)? },
142 )
143 } else if token_program(ix.program) && ix.data == [17] && !ix.accounts.is_empty() {
144 (account(ix, keys, 0), NativeTokenAction::SyncNative)
145 } else if token_program(ix.program) && ix.data == [9] && ix.accounts.len() >= 3 {
146 (
147 account(ix, keys, 0),
148 NativeTokenAction::Close {
149 destination: account(ix, keys, 1),
150 authority: account(ix, keys, 2),
151 },
152 )
153 } else {
154 return None;
155 };
156 if ix.accounts.iter().any(|&index| index as usize >= keys.len())
157 || mints.get(&target) != Some(&wsol)
158 {
159 return None;
160 }
161 Some(RouteNativeTokenAction { position: ix.position, account: target, action })
162}
163
164struct Invocation<'a> {
165 position: InstructionPosition,
166 program: Pubkey,
167 program_resolved: bool,
168 accounts: &'a [u8],
169 data: &'a [u8],
170}
171
172fn key(keys: &[Pubkey], index: u32) -> Pubkey {
173 keys.get(index as usize).copied().unwrap_or_default()
174}
175fn account(ix: &Invocation<'_>, keys: &[Pubkey], index: usize) -> Pubkey {
176 ix.accounts.get(index).map(|i| key(keys, u32::from(*i))).unwrap_or_default()
177}
178fn u64_at(data: &[u8], offset: usize) -> Option<u64> {
179 Some(u64::from_le_bytes(data.get(offset..offset + 8)?.try_into().ok()?))
180}
181fn token_program(program: Pubkey) -> bool {
182 program == ROUTE_SPL_TOKEN
183 || program == ROUTE_TOKEN_2022
184}
185fn checked_transfer(ix: &Invocation<'_>) -> bool {
186 token_program(ix.program)
187 && ix.data.first() == Some(&12)
188 && ix.data.len() >= 10
189 && ix.accounts.len() >= 4
190}
191fn checked_transfer_with_fee(ix: &Invocation<'_>) -> bool {
192 ix.program == ROUTE_TOKEN_2022
193 && ix.data.get(..2) == Some(&[26, 1])
194 && ix.data.len() >= 19
195 && ix.accounts.len() >= 4
196}
197fn descendant(parent: InstructionPosition, child: InstructionPosition) -> bool {
198 if parent.outer_index != child.outer_index {
199 return false;
200 }
201 match (parent.inner_index, child.inner_index) {
202 (None, Some(_)) => true,
203 (Some(a), Some(b)) if b > a => match (parent.stack_height, child.stack_height) {
204 (Some(a), Some(b)) => b > a,
205 _ => false,
206 },
207 _ => false,
208 }
209}
210#[cfg(test)]
211fn descendants<'a>(index: usize, invocations: &'a [Invocation<'_>]) -> &'a [Invocation<'a>] {
212 let parent = invocations[index].position;
213 let end = (index + 1..invocations.len())
214 .find(|&i| !descendant(parent, invocations[i].position))
215 .unwrap_or(invocations.len());
216 &invocations[index + 1..end]
217}
218
219struct InvocationAnalysis {
222 transfer: Option<RouteTokenTransfer>,
223 swap_index: Option<usize>,
224 descendant_end: usize,
225 has_token_transfers: bool,
226 has_known_swap_descendants: bool,
227 skip_route: bool,
228}
229
230fn invocation_analysis(
231 invocations: &[Invocation<'_>],
232 keys: &[Pubkey],
233 mints: &HashMap<Pubkey, Pubkey>,
234 graduated: &[Pubkey],
235 legs: &mut Vec<RouteSwapLeg>,
236) -> Vec<InvocationAnalysis> {
237 let mut analysis: Vec<_> = invocations.iter().enumerate().map(|(i, ix)| {
238 let skip_route = token_program(ix.program)
239 || ix.program == Pubkey::default()
240 || ix.program == ROUTE_COMPUTE_BUDGET
241 || ix.program == ROUTE_ASSOCIATED_TOKEN
242 || ix.program == ROUTE_MEMO;
243 let swap_index = if skip_route { None } else {
244 swap(ix, keys, mints, graduated).map(|leg| {
245 let index = legs.len();
246 legs.push(leg);
247 index
248 })
249 };
250 InvocationAnalysis {
251 transfer: transfer(ix, keys, mints), swap_index,
252 descendant_end: i + 1,
253 has_token_transfers: false,
254 has_known_swap_descendants: false,
255 skip_route,
256 }
257 }).collect();
258 for i in (0..invocations.len()).rev() {
259 let mut end = i + 1;
260 let mut has_transfers = false;
261 let mut has_swaps = false;
262 while end < invocations.len() && descendant(invocations[i].position, invocations[end].position) {
263 let child = &analysis[end];
264 has_transfers |= child.transfer.is_some() || child.has_token_transfers;
265 has_swaps |= child.swap_index.is_some() || child.has_known_swap_descendants;
266 end = child.descendant_end;
267 }
268 analysis[i].descendant_end = end;
269 analysis[i].has_token_transfers = has_transfers;
270 analysis[i].has_known_swap_descendants = has_swaps;
271 }
272 analysis
273}
274
275#[cfg(test)]
279fn transaction_token_mints(
280 transaction: &Transaction,
281 meta: &TransactionStatusMeta,
282) -> HashMap<Pubkey, Pubkey> {
283 let keys = transaction_keys(transaction, meta);
284 let invocations = transaction_invocations(transaction, meta, &keys);
285 token_mints_from_invocations(meta, &keys, &invocations)
286}
287
288fn token_mints_from_invocations(
289 meta: &TransactionStatusMeta,
290 keys: &[Pubkey],
291 invocations: &[Invocation<'_>],
292) -> HashMap<Pubkey, Pubkey> {
293 let mut mints = HashMap::new();
294 let record = |mints: &mut HashMap<Pubkey, Pubkey>, account, mint| {
295 if account != Pubkey::default() && mint != Pubkey::default() {
297 use std::collections::hash_map::Entry;
298 match mints.entry(account) {
299 Entry::Vacant(entry) => { entry.insert(mint); }
300 Entry::Occupied(mut entry) if *entry.get() != mint => {
301 entry.insert(Pubkey::default());
304 }
305 _ => {}
306 }
307 }
308 };
309 for balance in meta
310 .pre_token_balances
311 .iter()
312 .chain(&meta.post_token_balances)
313 {
314 if let Ok(mint) = balance.mint.parse() {
315 record(&mut mints, key(&keys, balance.account_index), mint);
316 }
317 }
318 for ix in invocations {
319 if checked_transfer(ix) {
320 let mint = account(ix, &keys, 1);
321 record(&mut mints, account(ix, &keys, 0), mint);
322 record(&mut mints, account(ix, &keys, 2), mint);
323 }
324 if checked_transfer_with_fee(ix) {
326 let mint = account(ix, &keys, 1);
327 record(&mut mints, account(ix, &keys, 0), mint);
328 record(&mut mints, account(ix, &keys, 2), mint);
329 }
330 if token_program(ix.program)
333 && ix.accounts.len() >= 2
334 && matches!(
335 (ix.data.first(), ix.data.len()),
336 (Some(1), 1) | (Some(16 | 18), 33)
337 )
338 {
339 record(&mut mints, account(ix, &keys, 0), account(ix, &keys, 1));
340 }
341 }
342 if mints.is_empty() {
343 return mints;
344 }
345 loop {
347 let before = mints.len();
348 for ix in invocations {
349 if token_program(ix.program)
350 && ix.data.first() == Some(&3)
351 && ix.data.len() >= 9
352 && ix.accounts.len() >= 3
353 {
354 let source = account(ix, &keys, 0);
355 let destination = account(ix, &keys, 1);
356 if source == Pubkey::default() || destination == Pubkey::default() {
357 continue;
358 }
359 let source_mint = mints.get(&source).copied();
360 let destination_mint = mints.get(&destination).copied();
361 if source_mint == Some(Pubkey::default())
362 || destination_mint == Some(Pubkey::default())
363 || matches!((source_mint, destination_mint), (Some(a), Some(b)) if a != b)
364 {
365 continue;
366 }
367 if let Some(mint) = source_mint.or(destination_mint) {
368 mints.entry(source).or_insert(mint);
369 mints.entry(destination).or_insert(mint);
370 }
371 }
372 }
373 if mints.len() == before {
374 break;
375 }
376 }
377 mints.retain(|_, mint| *mint != Pubkey::default());
378 mints
379}
380fn transaction_keys(transaction: &Transaction, meta: &TransactionStatusMeta) -> Vec<Pubkey> {
381 transaction
382 .message
383 .iter()
384 .flat_map(|m| &m.account_keys)
385 .chain(&meta.loaded_writable_addresses)
386 .chain(&meta.loaded_readonly_addresses)
387 .map(|bytes| crate::instr::read_pubkey_fast(bytes))
388 .collect()
389}
390fn transaction_invocations<'a>(
391 transaction: &'a Transaction,
392 meta: &'a TransactionStatusMeta,
393 keys: &[Pubkey],
394) -> Vec<Invocation<'a>> {
395 let Some(message) = &transaction.message else { return Vec::new(); };
396 if message.instructions.is_empty() { return Vec::new(); }
397 let count = message.instructions.len() + meta.inner_instructions.iter()
398 .filter(|group| (group.index as usize) < message.instructions.len())
399 .map(|group| group.instructions.len()).sum::<usize>();
400 let mut result = Vec::with_capacity(count);
401 let order = if meta.inner_instructions.windows(2).all(|pair| pair[0].index <= pair[1].index) {
404 None
405 } else {
406 let mut order: Vec<_> = (0..meta.inner_instructions.len()).collect();
407 order.sort_unstable_by_key(|&i| (meta.inner_instructions[i].index, i));
408 Some(order)
409 };
410 let group_at = |i: usize| {
411 let index = if let Some(order) = &order { *order.get(i)? } else { i };
412 meta.inner_instructions.get(index)
413 };
414 let mut cursor = 0;
415 for (i, ix) in message.instructions.iter().enumerate() {
416 result.push(Invocation {
417 position: InstructionPosition { outer_index: i as u32, inner_index: None, stack_height: Some(1) },
418 program: key(keys, ix.program_id_index),
419 program_resolved: (ix.program_id_index as usize) < keys.len(),
420 accounts: &ix.accounts, data: &ix.data,
421 });
422 while let Some(group) = group_at(cursor) {
423 if group.index > i as u32 { break; }
424 cursor += 1;
425 if group.index != i as u32 { continue; }
426 for (j, ix) in group.instructions.iter().enumerate() {
427 result.push(Invocation {
428 position: InstructionPosition { outer_index: i as u32, inner_index: Some(j as u32), stack_height: ix.stack_height },
429 program: key(keys, ix.program_id_index),
430 program_resolved: (ix.program_id_index as usize) < keys.len(),
431 accounts: &ix.accounts, data: &ix.data,
432 });
433 }
434 }
435 }
436 result
437}
438fn transfer(
439 ix: &Invocation<'_>,
440 keys: &[Pubkey],
441 mints: &HashMap<Pubkey, Pubkey>,
442) -> Option<RouteTokenTransfer> {
443 if !token_program(ix.program) {
444 return None;
445 }
446 let (destination_index, amount_offset, fee) = match ix.data.first()? {
447 3 if ix.accounts.len() >= 3 && ix.data.len() >= 9 => (
448 1,
449 1,
450 if ix.program == ROUTE_SPL_TOKEN {
451 Some(0)
452 } else {
453 None
454 },
455 ),
456 12 if checked_transfer(ix) => (
457 2,
458 1,
459 if ix.program == ROUTE_SPL_TOKEN {
460 Some(0)
461 } else {
462 None
463 },
464 ),
465 26 if checked_transfer_with_fee(ix) => (2, 2, Some(u64_at(ix.data, 11)?)),
466 _ => return None,
467 };
468 let source = account(ix, keys, 0);
469 let destination = account(ix, keys, destination_index);
470 if source == Pubkey::default() || destination == Pubkey::default()
471 || (destination_index == 2 && account(ix, keys, 1) == Pubkey::default())
472 {
473 return None;
474 }
475 Some(RouteTokenTransfer {
476 position: ix.position,
477 program: ix.program,
478 source,
479 destination,
480 mint: mints.get(&source).copied(),
481 amount: u64_at(ix.data, amount_offset)?,
482 withheld_fee: fee,
483 })
484}
485
486fn swap(
487 ix: &Invocation<'_>,
488 keys: &[Pubkey],
489 mints: &HashMap<Pubkey, Pubkey>,
490 graduated: &[Pubkey],
491) -> Option<RouteSwapLeg> {
492 use crate::instr::program_ids::*;
493 let a = |i| account(ix, keys, i);
494 let disc = ix.data.get(..8);
495 let swap_disc = Some(&[248, 198, 158, 145, 225, 117, 135, 200][..]);
496 let swap_v2 = Some(&[43, 4, 237, 11, 26, 201, 30, 98][..]);
497 let mut mode = None;
498 let (protocol, pool, trader, input, output, exact_in, amount, threshold) = if ix.program
499 == RAYDIUM_CLMM_PROGRAM_ID
500 && (disc == swap_disc || disc == swap_v2)
501 && ix.accounts.len() >= if disc == swap_v2 { 13 } else { 10 }
502 {
503 (
504 SwapProtocol::RaydiumClmm,
505 a(2),
506 a(0),
507 a(3),
508 a(4),
509 crate::instr::utils::read_option_bool_idl(ix.data, 40)?,
510 u64_at(ix.data, 8)?,
511 u64_at(ix.data, 16)?,
512 )
513 } else if ix.program == ORCA_WHIRLPOOL_PROGRAM_ID && (disc == swap_disc || disc == swap_v2) {
514 let v2 = disc == swap_v2;
515 let direction = crate::instr::utils::read_option_bool_idl(ix.data, 41)?;
516 let (input, output) = if v2 {
517 if ix.accounts.len() < 15 {
518 return None;
519 }
520 (
521 a(if direction { 7 } else { 9 }),
522 a(if direction { 9 } else { 7 }),
523 )
524 } else {
525 if ix.accounts.len() < 11 {
526 return None;
527 }
528 (
529 a(if direction { 3 } else { 5 }),
530 a(if direction { 5 } else { 3 }),
531 )
532 };
533 (
534 SwapProtocol::OrcaWhirlpool,
535 a(if v2 { 4 } else { 2 }),
536 a(if v2 { 3 } else { 1 }),
537 input,
538 output,
539 crate::instr::utils::read_option_bool_idl(ix.data, 40)?,
540 u64_at(ix.data, 8)?,
541 u64_at(ix.data, 16)?,
542 )
543 } else if ix.program == RAYDIUM_CPMM_PROGRAM_ID && ix.accounts.len() >= 13 {
544 use crate::instr::raydium_cpmm::discriminators::*;
545 let exact_in = if disc == Some(&SWAP_BASE_IN[..]) {
546 true
547 } else if disc == Some(&SWAP_BASE_OUT[..]) {
548 false
549 } else {
550 return None;
551 };
552 let first = u64_at(ix.data, 8)?;
553 let second = u64_at(ix.data, 16)?;
554 (
555 SwapProtocol::RaydiumCpmm,
556 a(3),
557 a(0),
558 a(4),
559 a(5),
560 exact_in,
561 if exact_in { first } else { second },
562 if exact_in { second } else { first },
563 )
564 } else if ix.program == METEORA_DLMM_PROGRAM_ID {
565 crate::instr::meteora_dlmm::validate_swap_layout(ix.data, ix.accounts.len())?;
566 use crate::instr::meteora_dlmm::discriminators::*;
567 let exact_in = if disc == Some(&SWAP[..]) || disc == Some(&SWAP2[..]) {
568 true
569 } else if disc == Some(&SWAP_EXACT_OUT[..]) || disc == Some(&SWAP_EXACT_OUT2[..]) {
570 false
571 } else {
572 return None;
573 };
574 let first = u64_at(ix.data, 8)?;
575 let second = u64_at(ix.data, 16)?;
576 (
577 SwapProtocol::MeteoraDlmm,
578 a(0),
579 a(10),
580 a(4),
581 a(5),
582 exact_in,
583 if exact_in { first } else { second },
584 if exact_in { second } else { first },
585 )
586 } else if ix.program == RAYDIUM_LAUNCHLAB_PROGRAM_ID && ix.accounts.len() >= 18 {
587 use crate::instr::raydium_launchlab::discriminators::*;
588 let (buy, exact_in) = if disc == Some(&BUY_EXACT_IN[..]) {
589 (true, true)
590 } else if disc == Some(&SELL_EXACT_IN[..]) {
591 (false, true)
592 } else if disc == Some(&BUY_EXACT_OUT[..]) {
593 (true, false)
594 } else if disc == Some(&SELL_EXACT_OUT[..]) {
595 (false, false)
596 } else {
597 return None;
598 };
599 mode = crate::core::events::stonkfun_mode_from_platform_config(a(3));
600 (
601 SwapProtocol::LaunchLab,
602 a(4),
603 a(0),
604 a(if buy { 6 } else { 5 }),
605 a(if buy { 5 } else { 6 }),
606 exact_in,
607 u64_at(ix.data, 8)?,
608 u64_at(ix.data, 16)?,
609 )
610 } else if ix.program == PUMPSWAP_PROGRAM_ID && ix.accounts.len() >= 21 {
611 use crate::instr::pump_amm::discriminators::*;
612 let (buy, exact_in) = if disc == Some(&BUY_EXACT_QUOTE_IN[..]) {
613 (true, true)
614 } else if disc == Some(&BUY[..]) {
615 (true, false)
616 } else if disc == Some(&SELL[..]) {
617 (false, true)
618 } else {
619 return None;
620 };
621 if buy && ix.data.len() > 24 {
622 crate::instr::utils::read_option_bool_idl(ix.data, 24)?;
623 }
624 (
625 SwapProtocol::PumpSwap,
626 a(0),
627 a(1),
628 a(if buy { 6 } else { 5 }),
629 a(if buy { 5 } else { 6 }),
630 exact_in,
631 u64_at(ix.data, 8)?,
632 u64_at(ix.data, 16)?,
633 )
634 } else if ix.program == RAYDIUM_AMM_V4_PROGRAM_ID
635 && matches!(ix.data.first(), Some(16 | 17))
636 && ix.accounts.len() >= 8
637 {
638 let exact_in = ix.data[0] == 16;
639 let first = u64_at(ix.data, 1)?;
640 let second = u64_at(ix.data, 9)?;
641 (
642 SwapProtocol::RaydiumAmmV4,
643 a(1),
644 a(7),
645 a(5),
646 a(6),
647 exact_in,
648 if exact_in { first } else { second },
649 if exact_in { second } else { first },
650 )
651 } else if ix.program == RAYDIUM_AMM_V4_PROGRAM_ID
652 && matches!(ix.data.first(), Some(9 | 11))
653 && ix.accounts.len() >= 17
654 {
655 let shift = usize::from(ix.accounts.len() == 17);
657 let exact_in = ix.data[0] == 9;
658 let first = u64_at(ix.data, 1)?;
659 let second = u64_at(ix.data, 9)?;
660 (
661 SwapProtocol::RaydiumAmmV4,
662 a(1),
663 a(17 - shift),
664 a(15 - shift),
665 a(16 - shift),
666 exact_in,
667 if exact_in { first } else { second },
668 if exact_in { second } else { first },
669 )
670 } else {
671 return None;
672 };
673 let explicit_pair = match protocol {
674 SwapProtocol::LaunchLab => Some(if input == a(6) {
675 (a(10), a(9))
676 } else {
677 (a(9), a(10))
678 }),
679 SwapProtocol::RaydiumCpmm => Some((a(10), a(11))),
680 SwapProtocol::RaydiumClmm if disc == swap_v2 && ix.accounts.len() >= 13 => {
681 Some((a(11), a(12)))
682 }
683 SwapProtocol::OrcaWhirlpool if disc == swap_v2 => Some(if input == a(7) {
684 (a(5), a(6))
685 } else {
686 (a(6), a(5))
687 }),
688 SwapProtocol::PumpSwap => Some(if input == a(6) {
689 (a(4), a(3))
690 } else {
691 (a(3), a(4))
692 }),
693 _ => None,
694 };
695 Some(RouteSwapLeg {
696 position: ix.position,
697 program: ix.program,
698 protocol,
699 pool,
700 trader,
701 input_account: input,
702 output_account: output,
703 input_mint: mints.get(&input).copied().or_else(|| {
704 explicit_pair
705 .map(|pair| pair.0)
706 .filter(|mint| *mint != Pubkey::default())
707 }),
708 output_mint: mints.get(&output).copied().or_else(|| {
709 explicit_pair
710 .map(|pair| pair.1)
711 .filter(|mint| *mint != Pubkey::default())
712 }),
713 amount_specified_is_input: exact_in,
714 specified_amount: amount,
715 other_amount_threshold: threshold,
716 actual_input_amount: None,
717 actual_output_amount: None,
718 stonkfun_mode: mode,
719 stonkfun_graduated: protocol == SwapProtocol::RaydiumCpmm && graduated.contains(&pool),
720 })
721}
722
723#[cfg(test)]
724mod tests {
725 use super::*;
726 #[test]
727 fn native_actions_require_wsol_identity_and_preserve_failure_status() {
728 use yellowstone_grpc_proto::prelude::{CompiledInstruction, Message, TransactionError};
729 let token = ROUTE_SPL_TOKEN;
730 let wsol = ROUTE_WSOL;
731 let keys = [Pubkey::default(), token, Pubkey::new_unique(), Pubkey::new_unique(), wsol];
732 let mut init = vec![18];
733 init.extend_from_slice(keys[2].as_ref());
734 let mut funding = 2u32.to_le_bytes().to_vec();
735 funding.extend_from_slice(&123u64.to_le_bytes());
736 let mut tx = Transaction {
737 signatures: vec![],
738 message: Some(Message {
739 account_keys: keys.iter().map(|k| k.to_bytes().to_vec()).collect(),
740 instructions: vec![
741 CompiledInstruction { program_id_index: 1, accounts: vec![3, 4], data: init },
742 CompiledInstruction {
743 program_id_index: 0,
744 accounts: vec![2, 3],
745 data: funding,
746 },
747 CompiledInstruction { program_id_index: 1, accounts: vec![3], data: vec![17] },
748 CompiledInstruction {
749 program_id_index: 1,
750 accounts: vec![3, 2, 2],
751 data: vec![9],
752 },
753 ],
754 ..Default::default()
755 }),
756 ..Default::default()
757 };
758 let mut meta = TransactionStatusMeta::default();
759 let route = analyze_yellowstone_transaction_routes(&tx, &meta, &[]);
760 assert!(route.succeeded);
761 assert_eq!(route.native_token_actions.len(), 3);
762 assert_eq!(
763 route.native_token_actions[0].action,
764 NativeTokenAction::Fund { source: keys[2], lamports: 123 }
765 );
766 meta.err = Some(TransactionError { err: vec![1] });
767 let failed = analyze_yellowstone_transaction_routes(&tx, &meta, &[]);
768 assert!(!failed.succeeded);
769 assert_eq!(failed.native_token_actions.len(), 3);
770 tx.message.as_mut().unwrap().account_keys[4] = Pubkey::new_unique().to_bytes().to_vec();
771 assert!(analyze_yellowstone_transaction_routes(&tx, &meta, &[])
772 .native_token_actions
773 .is_empty());
774 }
775 #[test]
776 fn ephemeral_initialized_accounts_propagate_mints_without_balance_snapshots() {
777 use yellowstone_grpc_proto::prelude::{CompiledInstruction, Message};
778 let program = ROUTE_SPL_TOKEN;
779 let keys = [
780 program,
781 Pubkey::new_unique(),
782 Pubkey::new_unique(),
783 Pubkey::new_unique(),
784 Pubkey::new_unique(),
785 ];
786 let mut initialize = vec![18];
787 initialize.extend_from_slice(keys[4].as_ref());
788 let mut plain = vec![3];
789 plain.extend_from_slice(&100u64.to_le_bytes());
790 let transaction = Transaction {
791 signatures: vec![],
792 message: Some(Message {
793 account_keys: keys.iter().map(|key| key.to_bytes().to_vec()).collect(),
794 instructions: vec![
795 CompiledInstruction {
796 program_id_index: 0,
797 accounts: vec![1, 2],
798 data: initialize,
799 },
800 CompiledInstruction {
801 program_id_index: 0,
802 accounts: vec![1, 3, 4],
803 data: plain,
804 },
805 ],
806 ..Default::default()
807 }),
808 };
809 let meta = TransactionStatusMeta::default();
810 let mints = transaction_token_mints(&transaction, &meta);
811 assert_eq!(mints.get(&keys[1]), Some(&keys[2]));
812 assert_eq!(mints.get(&keys[3]), Some(&keys[2]));
813 let route = analyze_yellowstone_transaction_routes(&transaction, &meta, &[]);
814 assert_eq!(route.transfers[0].mint, Some(keys[2]));
815 }
816 #[test]
817 fn malformed_checked_transfers_do_not_invent_amounts_or_mints() {
818 let keys: Vec<_> = (0..4).map(|_| Pubkey::new_unique()).collect();
819 let mut data = vec![12];
820 data.extend_from_slice(&123u64.to_le_bytes());
821 let mut ix = Invocation {
822 position: InstructionPosition {
823 outer_index: 0,
824 inner_index: None,
825 stack_height: Some(1),
826 },
827 program: ROUTE_SPL_TOKEN,
828 program_resolved: true,
829 accounts: &[0, 1, 2, 3],
830 data: &data,
831 };
832 assert!(!checked_transfer(&ix));
833 assert!(transfer(&ix, &keys, &HashMap::new()).is_none());
834 let mut complete_data = data.clone();
835 complete_data.push(6);
836 ix.data = &complete_data;
837 assert!(checked_transfer(&ix));
838 assert_eq!(transfer(&ix, &keys, &HashMap::new()).unwrap().amount, 123);
839 let mut fee_data = vec![26, 1];
840 fee_data.extend_from_slice(&123u64.to_le_bytes());
841 fee_data.push(6);
842 fee_data.extend_from_slice(&2u64.to_le_bytes());
843 ix.data = &fee_data;
844 assert!(transfer(&ix, &keys, &HashMap::new()).is_none());
845 ix.program = ROUTE_TOKEN_2022;
846 assert_eq!(transfer(&ix, &keys, &HashMap::new()).unwrap().withheld_fee, Some(2));
847 }
848}
849
850pub fn analyze_yellowstone_transaction_routes(
854 transaction: &Transaction,
855 meta: &TransactionStatusMeta,
856 graduated_stonkfun_pools: &[Pubkey],
857) -> TransactionRoute {
858 let keys = transaction_keys(transaction, meta);
859 let invocations = transaction_invocations(transaction, meta, &keys);
860 let mints = token_mints_from_invocations(meta, &keys, &invocations);
861 let mut legs = Vec::new();
862 let analysis = invocation_analysis(&invocations, &keys, &mints, graduated_stonkfun_pools, &mut legs);
863 let succeeded = meta.err.is_none();
864 let mut unknown = Vec::new();
865 for (i, ix) in invocations.iter().enumerate() {
866 let row = &analysis[i];
867 if row.skip_route { continue; }
868 if let Some(index) = row.swap_index {
869 let leg = &mut legs[index];
870 if succeeded {
871 let mut input_sum = Some(0u64);
872 let mut output_sum = Some(0u64);
873 let mut has_input = false;
874 let mut has_output = false;
875 for transfer in analysis[i + 1..row.descendant_end].iter().filter_map(|row| row.transfer.as_ref()) {
876 if transfer.source == leg.input_account {
877 has_input = true;
878 input_sum = input_sum.and_then(|sum| sum.checked_add(transfer.amount));
879 }
880 if transfer.destination == leg.output_account {
881 has_output = true;
882 output_sum = output_sum.and_then(|sum| {
883 sum.checked_add(transfer.amount.checked_sub(transfer.withheld_fee?)?)
884 });
885 }
886 }
887 leg.actual_input_amount = has_input.then_some(input_sum).flatten();
888 leg.actual_output_amount = has_output.then_some(output_sum).flatten();
889 }
890 } else {
891 unknown.push(RouteUnknownInvocation {
893 position: ix.position,
894 program: ix.program,
895 has_token_transfers: row.has_token_transfers,
896 has_known_swap_descendants: row.has_known_swap_descendants,
897 });
898 }
899 }
900 let transfers = analysis.into_iter().filter_map(|row| row.transfer).collect();
901 TransactionRoute {
902 signature: transaction
903 .signatures
904 .first()
905 .and_then(|b| Signature::try_from(b.as_slice()).ok())
906 .unwrap_or_default(),
907 succeeded,
908 legs,
909 transfers,
910 native_token_actions: invocations
911 .iter()
912 .filter_map(|ix| native_action(ix, &keys, &mints))
913 .collect(),
914 unknown_invocations: unknown,
915 }
916}
917
918pub fn analyze_rpc_transaction_routes(
919 transaction: &EncodedConfirmedTransactionWithStatusMeta,
920 graduated_stonkfun_pools: &[Pubkey],
921) -> Result<TransactionRoute, ParseError> {
922 let (meta, transaction) = convert_rpc_to_grpc(transaction)?;
923 Ok(analyze_yellowstone_transaction_routes(&transaction, &meta, graduated_stonkfun_pools))
924}
925
926#[cfg(test)]
927mod review_regressions {
928 use super::*;
929 use yellowstone_grpc_proto::prelude::{
930 CompiledInstruction, InnerInstruction, InnerInstructions, Message,
931 };
932
933 fn cpmm_with_output(program: Pubkey, fee: Option<u64>) -> (Transaction, TransactionStatusMeta) {
934 let mut keys: Vec<_> = (0..13).map(|_| Pubkey::new_unique()).collect();
935 keys.push(crate::instr::program_ids::RAYDIUM_CPMM_PROGRAM_ID);
936 keys.push(program);
937 let mut swap_data = crate::instr::raydium_cpmm::discriminators::SWAP_BASE_IN.to_vec();
938 swap_data.extend_from_slice(&100u64.to_le_bytes());
939 swap_data.extend_from_slice(&50u64.to_le_bytes());
940 let mut transfer_data = if fee.is_some() { vec![26, 1] } else { vec![3] };
941 transfer_data.extend_from_slice(&80u64.to_le_bytes());
942 if let Some(fee) = fee {
943 transfer_data.push(6);
944 transfer_data.extend_from_slice(&fee.to_le_bytes());
945 }
946 let tx = Transaction {
947 message: Some(Message {
948 account_keys: keys.iter().map(|k| k.to_bytes().to_vec()).collect(),
949 instructions: vec![CompiledInstruction {
950 program_id_index: 13,
951 accounts: (0..13).collect(),
952 data: swap_data,
953 }],
954 ..Default::default()
955 }),
956 ..Default::default()
957 };
958 let meta = TransactionStatusMeta {
959 inner_instructions: vec![InnerInstructions {
960 index: 0,
961 instructions: vec![InnerInstruction {
962 program_id_index: 14,
963 accounts: if fee.is_some() {
964 vec![7, 11, 5, 1]
965 } else {
966 vec![7, 5, 1]
967 },
968 data: transfer_data,
969 stack_height: Some(2),
970 }],
971 }],
972 ..Default::default()
973 };
974 (tx, meta)
975 }
976
977 #[test]
978 fn output_credit_requires_known_transfer_fee_and_failure_never_reports_credit() {
979 let spl = ROUTE_SPL_TOKEN;
980 let token22 = ROUTE_TOKEN_2022;
981 for (program, fee, expected) in [
982 (spl, None, Some(80)),
983 (token22, None, None),
984 (token22, Some(3), Some(77)),
985 (token22, Some(81), None),
986 ] {
987 let (tx, mut meta) = cpmm_with_output(program, fee);
988 let route = analyze_yellowstone_transaction_routes(&tx, &meta, &[]);
989 assert_eq!(route.legs.len(), 1);
990 assert_eq!(route.legs[0].actual_output_amount, expected);
991 assert_eq!(route.transfers[0].amount, 80);
992 meta.err = Some(yellowstone_grpc_proto::prelude::TransactionError { err: vec![1] });
993 assert_eq!(
994 analyze_yellowstone_transaction_routes(&tx, &meta, &[]).legs[0]
995 .actual_output_amount,
996 None
997 );
998 }
999 }
1000
1001 #[test]
1002 fn amm_remaining_accounts_do_not_shift_user_accounts() {
1003 let keys: Vec<_> = (0..21).map(|_| Pubkey::new_unique()).collect();
1004 let mut data = vec![9];
1005 data.extend_from_slice(&100u64.to_le_bytes());
1006 data.extend_from_slice(&80u64.to_le_bytes());
1007 for count in [17, 18, 20] {
1008 let accounts: Vec<u8> = (0..count).collect();
1009 let ix = Invocation {
1010 position: InstructionPosition {
1011 outer_index: 0,
1012 inner_index: None,
1013 stack_height: Some(1),
1014 },
1015 program: crate::instr::program_ids::RAYDIUM_AMM_V4_PROGRAM_ID,
1016 program_resolved: true,
1017 accounts: &accounts,
1018 data: &data,
1019 };
1020 let leg = swap(&ix, &keys, &HashMap::new(), &[]).unwrap();
1021 let shift = usize::from(count == 17);
1022 assert_eq!(leg.input_account, keys[15 - shift]);
1023 assert_eq!(leg.output_account, keys[16 - shift]);
1024 assert_eq!(leg.trader, keys[17 - shift]);
1025 }
1026 }
1027
1028 #[test]
1029 #[ignore = "manual local timing; excludes transport and ALT resolution"]
1030 fn route_parser_local_timing() {
1031 let (tx, meta) = cpmm_with_output(
1032 ROUTE_SPL_TOKEN,
1033 None,
1034 );
1035 for _ in 0..100 {
1036 std::hint::black_box(analyze_yellowstone_transaction_routes(&tx, &meta, &[]));
1037 }
1038 let mut samples = Vec::with_capacity(21);
1039 for _ in 0..21 {
1040 let start = std::time::Instant::now();
1041 for _ in 0..2000 {
1042 std::hint::black_box(analyze_yellowstone_transaction_routes(
1043 std::hint::black_box(&tx),
1044 std::hint::black_box(&meta),
1045 &[],
1046 ));
1047 }
1048 samples.push(start.elapsed().as_nanos() / 2000);
1049 }
1050 samples.sort_unstable();
1051 eprintln!(
1052 "CPMM route (one swap, one transfer): median={} ns/tx, max_batch_mean={} ns/tx",
1053 samples[10], samples[20]
1054 );
1055 }
1056}
1057
1058#[cfg(test)]
1059mod review_missing_mint_regressions {
1060 use super::*;
1061 use yellowstone_grpc_proto::prelude::{CompiledInstruction, Message, TokenBalance};
1062
1063 #[test]
1064 fn missing_accounts_never_seed_or_propagate_a_mint() {
1065 let keys = [Pubkey::new_unique(), Pubkey::new_unique(), Pubkey::new_unique(), ROUTE_SPL_TOKEN];
1066 let mint = Pubkey::new_unique();
1067 let mut data = vec![3]; data.extend_from_slice(&10u64.to_le_bytes());
1068 let tx = Transaction { message: Some(Message {
1069 account_keys: keys.iter().map(|k| k.to_bytes().to_vec()).collect(),
1070 instructions: vec![
1071 CompiledInstruction { program_id_index: 3, accounts: vec![255, 0, 2], data: data.clone() },
1072 CompiledInstruction { program_id_index: 3, accounts: vec![0, 1, 2], data },
1073 ], ..Default::default()
1074 }), ..Default::default() };
1075 let meta = TransactionStatusMeta { pre_token_balances: vec![TokenBalance {
1076 account_index: u32::MAX, mint: mint.to_string(), ..Default::default()
1077 }], ..Default::default() };
1078 assert!(transaction_token_mints(&tx, &meta).is_empty());
1079 let route = analyze_yellowstone_transaction_routes(&tx, &meta, &[]);
1080 assert_eq!(route.transfers.len(), 1);
1081 assert_eq!(route.transfers[0].source, keys[0]);
1082 assert_eq!(route.transfers[0].mint, None);
1083 }
1084}
1085
1086#[cfg(test)]
1087mod review_missing_program_regressions {
1088 use super::*;
1089 use yellowstone_grpc_proto::prelude::{CompiledInstruction, Message, TokenBalance};
1090
1091 #[test]
1092 fn invalid_program_or_source_index_cannot_masquerade_as_system_funding() {
1093 let keys = [Pubkey::new_unique(), Pubkey::new_unique(), Pubkey::default()];
1094 let mut data = 2u32.to_le_bytes().to_vec();
1095 data.extend_from_slice(&100u64.to_le_bytes());
1096 let mut tx = Transaction { message: Some(Message {
1097 account_keys: keys.iter().map(|key| key.to_bytes().to_vec()).collect(),
1098 instructions: vec![CompiledInstruction { program_id_index: 255, accounts: vec![0, 1], data }],
1099 ..Default::default()
1100 }), ..Default::default() };
1101 let meta = TransactionStatusMeta { pre_token_balances: vec![TokenBalance {
1102 account_index: 1, mint: ROUTE_WSOL.to_string(), ..Default::default()
1103 }], ..Default::default() };
1104 assert!(analyze_yellowstone_transaction_routes(&tx, &meta, &[]).native_token_actions.is_empty());
1105 tx.message.as_mut().unwrap().instructions[0].program_id_index = 2;
1106 assert_eq!(analyze_yellowstone_transaction_routes(&tx, &meta, &[]).native_token_actions.len(), 1);
1107 tx.message.as_mut().unwrap().instructions[0].accounts[0] = 255;
1108 assert!(analyze_yellowstone_transaction_routes(&tx, &meta, &[]).native_token_actions.is_empty());
1109 }
1110}
1111
1112#[cfg(test)]
1113mod review_route_scope_regressions {
1114 use super::*;
1115 use yellowstone_grpc_proto::prelude::{CompiledInstruction, Message, TokenBalance};
1116
1117 #[test]
1118 fn pumpswap_invalid_optional_bool_is_not_a_known_route_swap() {
1119 let mut keys: Vec<_> = (0..21).map(|_| Pubkey::new_unique()).collect();
1120 keys.push(crate::instr::program_ids::PUMPSWAP_PROGRAM_ID);
1121 for discriminator in [crate::instr::pump_amm::discriminators::BUY,
1122 crate::instr::pump_amm::discriminators::BUY_EXACT_QUOTE_IN] {
1123 let mut data = discriminator.to_vec();
1124 data.extend_from_slice(&100u64.to_le_bytes());
1125 data.extend_from_slice(&50u64.to_le_bytes());
1126 for flag in [None, Some(0), Some(1), Some(2), Some(255)] {
1127 let mut wire = data.clone();
1128 if let Some(flag) = flag { wire.push(flag); }
1129 let tx = Transaction {
1130 message: Some(Message {
1131 instructions: vec![CompiledInstruction {
1132 program_id_index: 21, accounts: (0..21).collect(), data: wire,
1133 }], ..Default::default()
1134 }), ..Default::default()
1135 };
1136 let meta = TransactionStatusMeta::default();
1137 let invocations = transaction_invocations(&tx, &meta, &keys);
1138 let mut legs = Vec::new();
1139 let rows = invocation_analysis(&invocations, &keys, &HashMap::new(), &[], &mut legs);
1140 assert_eq!(legs.len(), usize::from(!matches!(flag, Some(2 | 255))));
1141 assert_eq!(rows[0].swap_index.is_some(), !matches!(flag, Some(2 | 255)));
1142 }
1143 }
1144 }
1145
1146 #[test]
1147 fn dlmm_route_requires_v2_remaining_account_info() {
1148 let mut keys: Vec<_> = (0..16).map(|_| Pubkey::new_unique()).collect();
1149 keys.push(crate::instr::program_ids::METEORA_DLMM_PROGRAM_ID);
1150 let mut wire = crate::instr::meteora_dlmm::discriminators::SWAP2.to_vec();
1151 wire.extend_from_slice(&100u64.to_le_bytes());
1152 wire.extend_from_slice(&50u64.to_le_bytes());
1153 for tail in [vec![], vec![1,0,0,0,0,1], vec![0,0,0,0]] {
1154 let mut data = wire.clone(); data.extend_from_slice(&tail);
1155 let tx = Transaction { message: Some(Message {
1156 account_keys: keys.iter().map(|k| k.to_bytes().to_vec()).collect(),
1157 instructions: vec![CompiledInstruction { program_id_index: 16, accounts: (0..16).collect(), data }],
1158 ..Default::default()
1159 }), ..Default::default() };
1160 let route = analyze_yellowstone_transaction_routes(&tx, &TransactionStatusMeta::default(), &[]);
1161 assert_eq!(route.legs.len(), usize::from(tail == [0,0,0,0]));
1162 assert_eq!(route.unknown_invocations.len(), usize::from(tail != [0,0,0,0]));
1163 }
1164 }
1165
1166 #[test]
1167 fn conflicting_mint_evidence_is_sticky_and_cannot_spread_to_neighbours() {
1168 let keys = [Pubkey::new_unique(), Pubkey::new_unique(), Pubkey::new_unique(), Pubkey::new_unique(), Pubkey::new_unique(), ROUTE_SPL_TOKEN];
1169 let balance = |index, mint: Pubkey| TokenBalance { account_index: index, mint: mint.to_string(), ..Default::default() };
1170 let mut plain = vec![3]; plain.extend_from_slice(&10u64.to_le_bytes());
1171 let mut checked = vec![12]; checked.extend_from_slice(&10u64.to_le_bytes()); checked.push(6);
1172 let transaction = |with_checked| {
1173 let mut instructions = vec![];
1174 if with_checked { instructions.push(CompiledInstruction { program_id_index: 5, accounts: vec![0, 4, 1, 2], data: checked.clone() }); }
1175 for accounts in [vec![0, 1, 2], vec![1, 2, 3]] {
1176 instructions.push(CompiledInstruction { program_id_index: 5, accounts, data: plain.clone() });
1177 }
1178 Transaction { message: Some(Message { account_keys: keys.iter().map(|k| k.to_bytes().to_vec()).collect(), instructions, ..Default::default() }), ..Default::default() }
1179 };
1180 for with_checked in [false, true] {
1181 for reverse in [false, true] {
1182 let (before, after) = if reverse { (keys[4], keys[3]) } else { (keys[3], keys[4]) };
1183 let meta = TransactionStatusMeta {
1184 pre_token_balances: vec![balance(0, before), balance(1, keys[4])],
1185 post_token_balances: vec![balance(0, after)],
1186 ..Default::default()
1187 };
1188 let tx = transaction(with_checked);
1189 let mints = transaction_token_mints(&tx, &meta);
1190 assert_eq!(mints.get(&keys[0]), None);
1191 assert_eq!(mints.get(&keys[1]), Some(&keys[4]));
1192 assert_eq!(mints.get(&keys[2]), Some(&keys[4]));
1193 assert_eq!(analyze_yellowstone_transaction_routes(&tx, &meta, &[]).transfers[0].mint, None);
1194 }
1195 }
1196 let meta = TransactionStatusMeta { pre_token_balances: vec![balance(0, keys[3])], ..Default::default() };
1197 assert_eq!(transaction_token_mints(&transaction(true), &meta).get(&keys[0]), None);
1199 }
1200
1201 #[test]
1202 fn cached_subtree_boundaries_and_flags_match_exhaustive_scans() {
1203 let keys: Vec<_> = (0..13).map(|_| Pubkey::new_unique()).collect();
1204 let opaque = Pubkey::new_unique();
1205 let mut swap_data = crate::instr::raydium_cpmm::discriminators::SWAP_BASE_IN.to_vec();
1206 swap_data.extend_from_slice(&100u64.to_le_bytes()); swap_data.extend_from_slice(&50u64.to_le_bytes());
1207 let swap_accounts: Vec<u8> = (0..13).collect();
1208 let mut transfer_data = vec![3]; transfer_data.extend_from_slice(&70u64.to_le_bytes());
1209 let transfer_accounts = [4, 5, 0];
1210 let mut state = 0x1234_5678u64;
1211 for seed in 0..128 {
1212 let mut invocations = Vec::new();
1213 for outer in 0..3 {
1214 invocations.push(Invocation {
1215 position: InstructionPosition { outer_index: outer, inner_index: None, stack_height: Some(1) },
1216 program: opaque, program_resolved: true, accounts: &[], data: &[],
1217 });
1218 for j in 0..24 {
1219 state ^= state << 13; state ^= state >> 7; state ^= state << 17;
1220 let (program, accounts, data): (_, &[u8], &[u8]) = match state % 3 {
1221 0 => (opaque, &[], &[]),
1222 1 => (crate::instr::program_ids::RAYDIUM_CPMM_PROGRAM_ID, &swap_accounts, &swap_data),
1223 _ => (ROUTE_SPL_TOKEN, &transfer_accounts, &transfer_data),
1224 };
1225 invocations.push(Invocation {
1226 position: InstructionPosition { outer_index: outer,
1227 inner_index: Some(if seed % 7 == 0 { j / 2 } else { j }),
1228 stack_height: if state % 5 == 0 { None } else { Some(2 + (state % 6) as u32) },
1229 }, program, program_resolved: true, accounts, data,
1230 });
1231 }
1232 }
1233 let mints = HashMap::new();
1234 let analysis = invocation_analysis(&invocations, &keys, &mints, &[], &mut Vec::new());
1235 for (i, row) in analysis.iter().enumerate() {
1236 let reference = descendants(i, &invocations);
1237 assert_eq!(row.descendant_end, i + 1 + reference.len());
1238 assert_eq!(row.has_token_transfers, reference.iter().any(|ix| transfer(ix, &keys, &mints).is_some()));
1239 assert_eq!(row.has_known_swap_descendants, reference.iter().any(|ix| swap(ix, &keys, &mints, &[]).is_some()));
1240 }
1241 }
1242 }
1243}
1244
1245#[cfg(test)]
1246mod review_invocation_join_regressions {
1247 use super::*;
1248 use yellowstone_grpc_proto::prelude::{CompiledInstruction, InnerInstruction, InnerInstructions, Message};
1249fn exhaustive_invocations<'a>(
1250 transaction: &'a Transaction,
1251 meta: &'a TransactionStatusMeta,
1252 keys: &[Pubkey],
1253) -> Vec<Invocation<'a>> {
1254 let mut result = Vec::new();
1255 if let Some(message) = &transaction.message {
1256 for (i, ix) in message.instructions.iter().enumerate() {
1257 result.push(Invocation {
1258 position: InstructionPosition {
1259 outer_index: i as u32,
1260 inner_index: None,
1261 stack_height: Some(1),
1262 },
1263 program: key(keys, ix.program_id_index),
1264 program_resolved: (ix.program_id_index as usize) < keys.len(),
1265 accounts: &ix.accounts,
1266 data: &ix.data,
1267 });
1268 for group in meta
1269 .inner_instructions
1270 .iter()
1271 .filter(|g| g.index == i as u32)
1272 {
1273 for (j, ix) in group.instructions.iter().enumerate() {
1274 result.push(Invocation {
1275 position: InstructionPosition {
1276 outer_index: i as u32,
1277 inner_index: Some(j as u32),
1278 stack_height: ix.stack_height,
1279 },
1280 program: key(keys, ix.program_id_index),
1281 program_resolved: (ix.program_id_index as usize) < keys.len(),
1282 accounts: &ix.accounts,
1283 data: &ix.data,
1284 });
1285 }
1286 }
1287 }
1288 }
1289 result
1290}
1291
1292 #[test]
1293 fn ordered_join_preserves_unsorted_duplicate_and_orphan_group_semantics() {
1294 let keys = [Pubkey::new_unique(), Pubkey::new_unique(), Pubkey::new_unique()];
1295 let tx = Transaction { message: Some(Message {
1296 account_keys: keys.iter().map(|k| k.to_bytes().to_vec()).collect(),
1297 instructions: (0..3).map(|i| CompiledInstruction { program_id_index: i, data: vec![i as u8], accounts: vec![0, 1] }).collect(),
1298 ..Default::default()
1299 }), ..Default::default() };
1300 let indices = [2, 0, u32::MAX, 1, 0, 2];
1301 let mut groups: Vec<_> = indices.into_iter().enumerate().map(|(i, index)| InnerInstructions {
1302 index, instructions: vec![InnerInstruction { program_id_index: if i % 2 == 0 { 1 } else { 255 },
1303 accounts: vec![i as u8], data: vec![i as u8],
1304 stack_height: if i % 3 == 0 { None } else { Some(2) },
1305 }],
1306 }).collect();
1307 for permutation in 0..12 {
1308 if permutation % 2 == 0 { groups.rotate_left(1); } else { groups.reverse(); }
1309 let meta = TransactionStatusMeta { inner_instructions: groups.clone(), ..Default::default() };
1310 let expected = exhaustive_invocations(&tx, &meta, &keys);
1311 let actual = transaction_invocations(&tx, &meta, &keys);
1312 assert_eq!(actual.len(), 8);
1313 assert_eq!(actual.len(), expected.len());
1314 for (actual, expected) in actual.iter().zip(expected.iter()) {
1315 assert_eq!(actual.position, expected.position);
1316 assert_eq!(actual.program, expected.program);
1317 assert_eq!(actual.program_resolved, expected.program_resolved);
1318 assert_eq!(actual.accounts, expected.accounts);
1319 assert_eq!(actual.data, expected.data);
1320 }
1321 }
1322 groups.sort_by_key(|g| g.index);
1323 let meta = TransactionStatusMeta { inner_instructions: groups, ..Default::default() };
1324 let expected = exhaustive_invocations(&tx, &meta, &keys);
1325 let actual = transaction_invocations(&tx, &meta, &keys);
1326 assert_eq!(actual.iter().map(|ix| (ix.position, ix.data)).collect::<Vec<_>>(), expected.iter().map(|ix| (ix.position, ix.data)).collect::<Vec<_>>());
1327 assert!(transaction_invocations(&Transaction::default(), &meta, &keys).is_empty());
1328 }
1329}