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