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() >= 15 {
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 {
642 use crate::instr::pump::discriminators::*;
643 let v2 = [BUY_V2, BUY_EXACT_QUOTE_IN_V2, SELL_V2].iter().any(|d| disc == Some(&d[..]));
644 let v3 = [BUY_V3, BUY_EXACT_QUOTE_IN_V3, SELL_V3].iter().any(|d| disc == Some(&d[..]));
645 let buy = disc != Some(&SELL_V3[..]) && disc != Some(&SELL_V2[..]);
646 let exact = disc != Some(&BUY_V3[..]) && disc != Some(&BUY_V2[..]);
647 if !(v2 || v3)
648 || ix.accounts.len()
649 < if v2 {
650 if buy {
651 27
652 } else {
653 26
654 }
655 } else {
656 17
657 }
658 {
659 return None;
660 }
661 let (pool_index, user_index, base_account, quote_account) =
662 if v2 { (10, 13, 14, 15) } else { (5, 8, 9, 10) };
663 let quote = a(2);
664 let source = if buy {
665 if quote == ROUTE_WSOL {
666 a(user_index)
667 } else {
668 a(quote_account)
669 }
670 } else {
671 a(base_account)
672 };
673 let dest = if buy {
674 a(base_account)
675 } else if quote == ROUTE_WSOL {
676 a(user_index)
677 } else {
678 a(quote_account)
679 };
680 (
681 SwapProtocol::PumpFun,
682 a(pool_index),
683 a(user_index),
684 source,
685 dest,
686 exact,
687 u64_at(ix.data, 8)?,
688 u64_at(ix.data, 16)?,
689 )
690 } else if ix.program == PUMPSWAP_PROGRAM_ID
691 && (ix.accounts.len() >= 21 || ix.accounts.len() == 17)
692 {
693 use crate::instr::pump_amm::discriminators::*;
694 let (buy, exact_in) =
695 if disc == Some(&BUY_EXACT_QUOTE_IN[..]) || disc == Some(&BUY_EXACT_QUOTE_IN_V2[..]) {
696 (true, true)
697 } else if disc == Some(&BUY[..]) || disc == Some(&BUY_V2[..]) {
698 (true, false)
699 } else if disc == Some(&SELL[..]) || disc == Some(&SELL_V2[..]) {
700 (false, true)
701 } else {
702 return None;
703 };
704 if buy && ix.data.len() > 24 {
705 crate::instr::utils::read_option_bool_idl(ix.data, 24)?;
706 }
707 (
708 SwapProtocol::PumpSwap,
709 a(0),
710 a(1),
711 a(if buy { 6 } else { 5 }),
712 a(if buy { 5 } else { 6 }),
713 exact_in,
714 u64_at(ix.data, 8)?,
715 u64_at(ix.data, 16)?,
716 )
717 } else if ix.program == RAYDIUM_AMM_V4_PROGRAM_ID
718 && matches!(ix.data.first(), Some(16 | 17))
719 && ix.accounts.len() >= 8
720 {
721 let exact_in = ix.data[0] == 16;
722 let first = u64_at(ix.data, 1)?;
723 let second = u64_at(ix.data, 9)?;
724 (
725 SwapProtocol::RaydiumAmmV4,
726 a(1),
727 a(7),
728 a(5),
729 a(6),
730 exact_in,
731 if exact_in { first } else { second },
732 if exact_in { second } else { first },
733 )
734 } else if ix.program == RAYDIUM_AMM_V4_PROGRAM_ID
735 && matches!(ix.data.first(), Some(9 | 11))
736 && ix.accounts.len() >= 17
737 {
738 let shift = usize::from(ix.accounts.len() == 17);
740 let exact_in = ix.data[0] == 9;
741 let first = u64_at(ix.data, 1)?;
742 let second = u64_at(ix.data, 9)?;
743 (
744 SwapProtocol::RaydiumAmmV4,
745 a(1),
746 a(17 - shift),
747 a(15 - shift),
748 a(16 - shift),
749 exact_in,
750 if exact_in { first } else { second },
751 if exact_in { second } else { first },
752 )
753 } else {
754 return None;
755 };
756 let explicit_pair = match protocol {
757 SwapProtocol::PumpFun => Some(
758 if disc == Some(&crate::instr::pump::discriminators::SELL_V3[..])
759 || disc == Some(&crate::instr::pump::discriminators::SELL_V2[..])
760 {
761 (a(1), a(2))
762 } else {
763 (a(2), a(1))
764 },
765 ),
766 SwapProtocol::LaunchLab => Some(if input == a(6) { (a(10), a(9)) } else { (a(9), a(10)) }),
767 SwapProtocol::RaydiumCpmm => Some((a(10), a(11))),
768 SwapProtocol::RaydiumClmm if disc == swap_v2 && ix.accounts.len() >= 13 => {
769 Some((a(11), a(12)))
770 }
771 SwapProtocol::OrcaWhirlpool if disc == swap_v2 => {
772 Some(if input == a(7) { (a(5), a(6)) } else { (a(6), a(5)) })
773 }
774 SwapProtocol::PumpSwap => Some(if input == a(6) { (a(4), a(3)) } else { (a(3), a(4)) }),
775 _ => None,
776 };
777 Some(RouteSwapLeg {
778 position: ix.position,
779 program: ix.program,
780 protocol,
781 pool,
782 trader,
783 input_account: input,
784 output_account: output,
785 input_mint: mints
786 .get(&input)
787 .copied()
788 .or_else(|| explicit_pair.map(|pair| pair.0).filter(|mint| *mint != Pubkey::default())),
789 output_mint: mints
790 .get(&output)
791 .copied()
792 .or_else(|| explicit_pair.map(|pair| pair.1).filter(|mint| *mint != Pubkey::default())),
793 amount_specified_is_input: exact_in,
794 specified_amount: amount,
795 other_amount_threshold: threshold,
796 actual_input_amount: None,
797 actual_output_amount: None,
798 stonkfun_mode: mode,
799 stonkfun_graduated: protocol == SwapProtocol::RaydiumCpmm && graduated.contains(&pool),
800 })
801}
802
803#[cfg(test)]
804mod tests {
805 use super::*;
806 #[test]
807 fn native_actions_require_wsol_identity_and_preserve_failure_status() {
808 use yellowstone_grpc_proto::prelude::{CompiledInstruction, Message, TransactionError};
809 let token = ROUTE_SPL_TOKEN;
810 let wsol = ROUTE_WSOL;
811 let keys = [Pubkey::default(), token, Pubkey::new_unique(), Pubkey::new_unique(), wsol];
812 let mut init = vec![18];
813 init.extend_from_slice(keys[2].as_ref());
814 let mut funding = 2u32.to_le_bytes().to_vec();
815 funding.extend_from_slice(&123u64.to_le_bytes());
816 let mut tx = Transaction {
817 signatures: vec![],
818 message: Some(Message {
819 account_keys: keys.iter().map(|k| k.to_bytes().to_vec()).collect(),
820 instructions: vec![
821 CompiledInstruction { program_id_index: 1, accounts: vec![3, 4], data: init },
822 CompiledInstruction {
823 program_id_index: 0,
824 accounts: vec![2, 3],
825 data: funding,
826 },
827 CompiledInstruction { program_id_index: 1, accounts: vec![3], data: vec![17] },
828 CompiledInstruction {
829 program_id_index: 1,
830 accounts: vec![3, 2, 2],
831 data: vec![9],
832 },
833 ],
834 ..Default::default()
835 }),
836 ..Default::default()
837 };
838 let mut meta = TransactionStatusMeta::default();
839 let route = analyze_yellowstone_transaction_routes(&tx, &meta, &[]);
840 assert!(route.succeeded);
841 assert_eq!(route.native_token_actions.len(), 3);
842 assert_eq!(
843 route.native_token_actions[0].action,
844 NativeTokenAction::Fund { source: keys[2], lamports: 123 }
845 );
846 meta.err = Some(TransactionError { err: vec![1] });
847 let failed = analyze_yellowstone_transaction_routes(&tx, &meta, &[]);
848 assert!(!failed.succeeded);
849 assert_eq!(failed.native_token_actions.len(), 3);
850 tx.message.as_mut().unwrap().account_keys[4] = Pubkey::new_unique().to_bytes().to_vec();
851 assert!(analyze_yellowstone_transaction_routes(&tx, &meta, &[])
852 .native_token_actions
853 .is_empty());
854 }
855 #[test]
856 fn ephemeral_initialized_accounts_propagate_mints_without_balance_snapshots() {
857 use yellowstone_grpc_proto::prelude::{CompiledInstruction, Message};
858 let program = ROUTE_SPL_TOKEN;
859 let keys = [
860 program,
861 Pubkey::new_unique(),
862 Pubkey::new_unique(),
863 Pubkey::new_unique(),
864 Pubkey::new_unique(),
865 ];
866 let mut initialize = vec![18];
867 initialize.extend_from_slice(keys[4].as_ref());
868 let mut plain = vec![3];
869 plain.extend_from_slice(&100u64.to_le_bytes());
870 let transaction = Transaction {
871 signatures: vec![],
872 message: Some(Message {
873 account_keys: keys.iter().map(|key| key.to_bytes().to_vec()).collect(),
874 instructions: vec![
875 CompiledInstruction {
876 program_id_index: 0,
877 accounts: vec![1, 2],
878 data: initialize,
879 },
880 CompiledInstruction {
881 program_id_index: 0,
882 accounts: vec![1, 3, 4],
883 data: plain,
884 },
885 ],
886 ..Default::default()
887 }),
888 };
889 let meta = TransactionStatusMeta::default();
890 let mints = transaction_token_mints(&transaction, &meta);
891 assert_eq!(mints.get(&keys[1]), Some(&keys[2]));
892 assert_eq!(mints.get(&keys[3]), Some(&keys[2]));
893 let route = analyze_yellowstone_transaction_routes(&transaction, &meta, &[]);
894 assert_eq!(route.transfers[0].mint, Some(keys[2]));
895 }
896 #[test]
897 fn malformed_checked_transfers_do_not_invent_amounts_or_mints() {
898 let keys: Vec<_> = (0..4).map(|_| Pubkey::new_unique()).collect();
899 let mut data = vec![12];
900 data.extend_from_slice(&123u64.to_le_bytes());
901 let mut ix = Invocation {
902 position: InstructionPosition {
903 outer_index: 0,
904 inner_index: None,
905 stack_height: Some(1),
906 },
907 program: ROUTE_SPL_TOKEN,
908 program_resolved: true,
909 accounts: &[0, 1, 2, 3],
910 data: &data,
911 };
912 assert!(!checked_transfer(&ix));
913 assert!(transfer(&ix, &keys, &HashMap::new()).is_none());
914 let mut complete_data = data.clone();
915 complete_data.push(6);
916 ix.data = &complete_data;
917 assert!(checked_transfer(&ix));
918 assert_eq!(transfer(&ix, &keys, &HashMap::new()).unwrap().amount, 123);
919 let mut fee_data = vec![26, 1];
920 fee_data.extend_from_slice(&123u64.to_le_bytes());
921 fee_data.push(6);
922 fee_data.extend_from_slice(&2u64.to_le_bytes());
923 ix.data = &fee_data;
924 assert!(transfer(&ix, &keys, &HashMap::new()).is_none());
925 ix.program = ROUTE_TOKEN_2022;
926 assert_eq!(transfer(&ix, &keys, &HashMap::new()).unwrap().withheld_fee, Some(2));
927 }
928}
929
930pub fn analyze_yellowstone_transaction_routes(
934 transaction: &Transaction,
935 meta: &TransactionStatusMeta,
936 graduated_stonkfun_pools: &[Pubkey],
937) -> TransactionRoute {
938 let keys = transaction_keys(transaction, meta);
939 let invocations = transaction_invocations(transaction, meta, &keys);
940 let mints = token_mints_from_invocations(meta, &keys, &invocations);
941 let mut legs = Vec::new();
942 let analysis =
943 invocation_analysis(&invocations, &keys, &mints, graduated_stonkfun_pools, &mut legs);
944 let succeeded = meta.err.is_none();
945 let mut unknown = Vec::new();
946 for (i, ix) in invocations.iter().enumerate() {
947 let row = &analysis[i];
948 if row.skip_route {
949 continue;
950 }
951 if let Some(index) = row.swap_index {
952 let leg = &mut legs[index];
953 if succeeded {
954 let mut input_sum = Some(0u64);
955 let mut output_sum = Some(0u64);
956 let mut has_input = false;
957 let mut has_output = false;
958 for transfer in analysis[i + 1..row.descendant_end]
959 .iter()
960 .filter_map(|row| row.transfer.as_ref())
961 {
962 if transfer.source == leg.input_account {
963 has_input = true;
964 input_sum = input_sum.and_then(|sum| sum.checked_add(transfer.amount));
965 }
966 if transfer.destination == leg.output_account {
967 has_output = true;
968 output_sum = output_sum.and_then(|sum| {
969 sum.checked_add(transfer.amount.checked_sub(transfer.withheld_fee?)?)
970 });
971 }
972 }
973 leg.actual_input_amount = has_input.then_some(input_sum).flatten();
974 leg.actual_output_amount = has_output.then_some(output_sum).flatten();
975 }
976 } else {
977 unknown.push(RouteUnknownInvocation {
979 position: ix.position,
980 program: ix.program,
981 has_token_transfers: row.has_token_transfers,
982 has_known_swap_descendants: row.has_known_swap_descendants,
983 });
984 }
985 }
986 let transfers = analysis.into_iter().filter_map(|row| row.transfer).collect();
987 TransactionRoute {
988 signature: transaction
989 .signatures
990 .first()
991 .and_then(|b| Signature::try_from(b.as_slice()).ok())
992 .unwrap_or_default(),
993 succeeded,
994 legs,
995 transfers,
996 native_token_actions: invocations
997 .iter()
998 .filter_map(|ix| native_action(ix, &keys, &mints))
999 .collect(),
1000 unknown_invocations: unknown,
1001 }
1002}
1003
1004pub fn analyze_rpc_transaction_routes(
1005 transaction: &EncodedConfirmedTransactionWithStatusMeta,
1006 graduated_stonkfun_pools: &[Pubkey],
1007) -> Result<TransactionRoute, ParseError> {
1008 let (meta, transaction) = convert_rpc_to_grpc(transaction)?;
1009 Ok(analyze_yellowstone_transaction_routes(&transaction, &meta, graduated_stonkfun_pools))
1010}
1011
1012#[cfg(test)]
1013mod review_regressions {
1014 use super::*;
1015 use yellowstone_grpc_proto::prelude::{
1016 CompiledInstruction, InnerInstruction, InnerInstructions, Message,
1017 };
1018
1019 #[test]
1020 fn idl_minimum_launchlab_and_pump_v2_accounts() {
1021 use crate::instr::{program_ids::*, pump::discriminators as pump};
1022 let keys: Vec<_> = (0..30).map(|_| Pubkey::new_unique()).collect();
1023 for (program, disc, count, input, output) in [
1024 (
1025 RAYDIUM_LAUNCHLAB_PROGRAM_ID,
1026 crate::instr::raydium_launchlab::discriminators::BUY_EXACT_IN,
1027 15,
1028 6,
1029 5,
1030 ),
1031 (PUMPFUN_PROGRAM_ID, pump::BUY_V2, 27, 15, 14),
1032 (PUMPFUN_PROGRAM_ID, pump::BUY_EXACT_QUOTE_IN_V2, 27, 15, 14),
1033 (PUMPFUN_PROGRAM_ID, pump::SELL_V2, 26, 14, 15),
1034 ] {
1035 let mut data = disc.to_vec();
1036 data.extend_from_slice(&100u64.to_le_bytes());
1037 data.extend_from_slice(&20u64.to_le_bytes());
1038 let accounts: Vec<u8> = (0..count).collect();
1039 let mut ix = Invocation {
1040 position: InstructionPosition {
1041 outer_index: 0,
1042 inner_index: None,
1043 stack_height: Some(1),
1044 },
1045 program,
1046 program_resolved: true,
1047 accounts: &accounts,
1048 data: &data,
1049 };
1050 let leg = swap(&ix, &keys, &HashMap::new(), &[]).unwrap();
1051 assert_eq!(leg.input_account, keys[input]);
1052 assert_eq!(leg.output_account, keys[output]);
1053 assert!(leg.input_mint.is_some() && leg.output_mint.is_some());
1054 ix.accounts = &accounts[..accounts.len() - 1];
1055 assert!(swap(&ix, &keys, &HashMap::new(), &[]).is_none());
1056 }
1057 }
1058
1059 fn cpmm_with_output(program: Pubkey, fee: Option<u64>) -> (Transaction, TransactionStatusMeta) {
1060 let mut keys: Vec<_> = (0..13).map(|_| Pubkey::new_unique()).collect();
1061 keys.push(crate::instr::program_ids::RAYDIUM_CPMM_PROGRAM_ID);
1062 keys.push(program);
1063 let mut swap_data = crate::instr::raydium_cpmm::discriminators::SWAP_BASE_IN.to_vec();
1064 swap_data.extend_from_slice(&100u64.to_le_bytes());
1065 swap_data.extend_from_slice(&50u64.to_le_bytes());
1066 let mut transfer_data = if fee.is_some() { vec![26, 1] } else { vec![3] };
1067 transfer_data.extend_from_slice(&80u64.to_le_bytes());
1068 if let Some(fee) = fee {
1069 transfer_data.push(6);
1070 transfer_data.extend_from_slice(&fee.to_le_bytes());
1071 }
1072 let tx = Transaction {
1073 message: Some(Message {
1074 account_keys: keys.iter().map(|k| k.to_bytes().to_vec()).collect(),
1075 instructions: vec![CompiledInstruction {
1076 program_id_index: 13,
1077 accounts: (0..13).collect(),
1078 data: swap_data,
1079 }],
1080 ..Default::default()
1081 }),
1082 ..Default::default()
1083 };
1084 let meta = TransactionStatusMeta {
1085 inner_instructions: vec![InnerInstructions {
1086 index: 0,
1087 instructions: vec![InnerInstruction {
1088 program_id_index: 14,
1089 accounts: if fee.is_some() { vec![7, 11, 5, 1] } else { vec![7, 5, 1] },
1090 data: transfer_data,
1091 stack_height: Some(2),
1092 }],
1093 }],
1094 ..Default::default()
1095 };
1096 (tx, meta)
1097 }
1098
1099 #[test]
1100 fn output_credit_requires_known_transfer_fee_and_failure_never_reports_credit() {
1101 let spl = ROUTE_SPL_TOKEN;
1102 let token22 = ROUTE_TOKEN_2022;
1103 for (program, fee, expected) in [
1104 (spl, None, Some(80)),
1105 (token22, None, None),
1106 (token22, Some(3), Some(77)),
1107 (token22, Some(81), None),
1108 ] {
1109 let (tx, mut meta) = cpmm_with_output(program, fee);
1110 let route = analyze_yellowstone_transaction_routes(&tx, &meta, &[]);
1111 assert_eq!(route.legs.len(), 1);
1112 assert_eq!(route.legs[0].actual_output_amount, expected);
1113 assert_eq!(route.transfers[0].amount, 80);
1114 meta.err = Some(yellowstone_grpc_proto::prelude::TransactionError { err: vec![1] });
1115 assert_eq!(
1116 analyze_yellowstone_transaction_routes(&tx, &meta, &[]).legs[0]
1117 .actual_output_amount,
1118 None
1119 );
1120 }
1121 }
1122
1123 #[test]
1124 fn amm_remaining_accounts_do_not_shift_user_accounts() {
1125 let keys: Vec<_> = (0..21).map(|_| Pubkey::new_unique()).collect();
1126 let mut data = vec![9];
1127 data.extend_from_slice(&100u64.to_le_bytes());
1128 data.extend_from_slice(&80u64.to_le_bytes());
1129 for count in [17, 18, 20] {
1130 let accounts: Vec<u8> = (0..count).collect();
1131 let ix = Invocation {
1132 position: InstructionPosition {
1133 outer_index: 0,
1134 inner_index: None,
1135 stack_height: Some(1),
1136 },
1137 program: crate::instr::program_ids::RAYDIUM_AMM_V4_PROGRAM_ID,
1138 program_resolved: true,
1139 accounts: &accounts,
1140 data: &data,
1141 };
1142 let leg = swap(&ix, &keys, &HashMap::new(), &[]).unwrap();
1143 let shift = usize::from(count == 17);
1144 assert_eq!(leg.input_account, keys[15 - shift]);
1145 assert_eq!(leg.output_account, keys[16 - shift]);
1146 assert_eq!(leg.trader, keys[17 - shift]);
1147 }
1148 }
1149
1150 #[test]
1151 #[ignore = "manual local timing; excludes transport and ALT resolution"]
1152 fn route_parser_local_timing() {
1153 let (tx, meta) = cpmm_with_output(ROUTE_SPL_TOKEN, None);
1154 for _ in 0..100 {
1155 std::hint::black_box(analyze_yellowstone_transaction_routes(&tx, &meta, &[]));
1156 }
1157 let mut samples = Vec::with_capacity(21);
1158 for _ in 0..21 {
1159 let start = std::time::Instant::now();
1160 for _ in 0..2000 {
1161 std::hint::black_box(analyze_yellowstone_transaction_routes(
1162 std::hint::black_box(&tx),
1163 std::hint::black_box(&meta),
1164 &[],
1165 ));
1166 }
1167 samples.push(start.elapsed().as_nanos() / 2000);
1168 }
1169 samples.sort_unstable();
1170 eprintln!(
1171 "CPMM route (one swap, one transfer): median={} ns/tx, max_batch_mean={} ns/tx",
1172 samples[10], samples[20]
1173 );
1174 }
1175}
1176
1177#[cfg(test)]
1178mod review_missing_mint_regressions {
1179 use super::*;
1180 use yellowstone_grpc_proto::prelude::{CompiledInstruction, Message, TokenBalance};
1181
1182 #[test]
1183 fn missing_accounts_never_seed_or_propagate_a_mint() {
1184 let keys =
1185 [Pubkey::new_unique(), Pubkey::new_unique(), Pubkey::new_unique(), ROUTE_SPL_TOKEN];
1186 let mint = Pubkey::new_unique();
1187 let mut data = vec![3];
1188 data.extend_from_slice(&10u64.to_le_bytes());
1189 let tx = Transaction {
1190 message: Some(Message {
1191 account_keys: keys.iter().map(|k| k.to_bytes().to_vec()).collect(),
1192 instructions: vec![
1193 CompiledInstruction {
1194 program_id_index: 3,
1195 accounts: vec![255, 0, 2],
1196 data: data.clone(),
1197 },
1198 CompiledInstruction { program_id_index: 3, accounts: vec![0, 1, 2], data },
1199 ],
1200 ..Default::default()
1201 }),
1202 ..Default::default()
1203 };
1204 let meta = TransactionStatusMeta {
1205 pre_token_balances: vec![TokenBalance {
1206 account_index: u32::MAX,
1207 mint: mint.to_string(),
1208 ..Default::default()
1209 }],
1210 ..Default::default()
1211 };
1212 assert!(transaction_token_mints(&tx, &meta).is_empty());
1213 let route = analyze_yellowstone_transaction_routes(&tx, &meta, &[]);
1214 assert_eq!(route.transfers.len(), 1);
1215 assert_eq!(route.transfers[0].source, keys[0]);
1216 assert_eq!(route.transfers[0].mint, None);
1217 }
1218}
1219
1220#[cfg(test)]
1221mod review_missing_program_regressions {
1222 use super::*;
1223 use yellowstone_grpc_proto::prelude::{CompiledInstruction, Message, TokenBalance};
1224
1225 #[test]
1226 fn invalid_program_or_source_index_cannot_masquerade_as_system_funding() {
1227 let keys = [Pubkey::new_unique(), Pubkey::new_unique(), Pubkey::default()];
1228 let mut data = 2u32.to_le_bytes().to_vec();
1229 data.extend_from_slice(&100u64.to_le_bytes());
1230 let mut tx = Transaction {
1231 message: Some(Message {
1232 account_keys: keys.iter().map(|key| key.to_bytes().to_vec()).collect(),
1233 instructions: vec![CompiledInstruction {
1234 program_id_index: 255,
1235 accounts: vec![0, 1],
1236 data,
1237 }],
1238 ..Default::default()
1239 }),
1240 ..Default::default()
1241 };
1242 let meta = TransactionStatusMeta {
1243 pre_token_balances: vec![TokenBalance {
1244 account_index: 1,
1245 mint: ROUTE_WSOL.to_string(),
1246 ..Default::default()
1247 }],
1248 ..Default::default()
1249 };
1250 assert!(analyze_yellowstone_transaction_routes(&tx, &meta, &[])
1251 .native_token_actions
1252 .is_empty());
1253 tx.message.as_mut().unwrap().instructions[0].program_id_index = 2;
1254 assert_eq!(
1255 analyze_yellowstone_transaction_routes(&tx, &meta, &[]).native_token_actions.len(),
1256 1
1257 );
1258 tx.message.as_mut().unwrap().instructions[0].accounts[0] = 255;
1259 assert!(analyze_yellowstone_transaction_routes(&tx, &meta, &[])
1260 .native_token_actions
1261 .is_empty());
1262 }
1263}
1264
1265#[cfg(test)]
1266mod review_route_scope_regressions {
1267 use super::*;
1268 use yellowstone_grpc_proto::prelude::{CompiledInstruction, Message, TokenBalance};
1269
1270 #[test]
1271 fn pumpswap_invalid_optional_bool_is_not_a_known_route_swap() {
1272 let mut keys: Vec<_> = (0..21).map(|_| Pubkey::new_unique()).collect();
1273 keys.push(crate::instr::program_ids::PUMPSWAP_PROGRAM_ID);
1274 for discriminator in [
1275 crate::instr::pump_amm::discriminators::BUY,
1276 crate::instr::pump_amm::discriminators::BUY_EXACT_QUOTE_IN,
1277 ] {
1278 let mut data = discriminator.to_vec();
1279 data.extend_from_slice(&100u64.to_le_bytes());
1280 data.extend_from_slice(&50u64.to_le_bytes());
1281 for flag in [None, Some(0), Some(1), Some(2), Some(255)] {
1282 let mut wire = data.clone();
1283 if let Some(flag) = flag {
1284 wire.push(flag);
1285 }
1286 let tx = Transaction {
1287 message: Some(Message {
1288 account_keys: keys.iter().map(|key| key.to_bytes().to_vec()).collect(),
1289 instructions: vec![CompiledInstruction {
1290 program_id_index: 21,
1291 accounts: (0..21).collect(),
1292 data: wire,
1293 }],
1294 ..Default::default()
1295 }),
1296 ..Default::default()
1297 };
1298 let meta = TransactionStatusMeta::default();
1299 let invocations = transaction_invocations(&tx, &meta, &keys);
1300 let mut legs = Vec::new();
1301 let rows =
1302 invocation_analysis(&invocations, &keys, &HashMap::new(), &[], &mut legs);
1303 assert_eq!(legs.len(), usize::from(!matches!(flag, Some(2 | 255))));
1304 assert_eq!(rows[0].swap_index.is_some(), !matches!(flag, Some(2 | 255)));
1305 }
1306 }
1307 }
1308
1309 #[test]
1310 fn dlmm_route_requires_v2_remaining_account_info() {
1311 let mut keys: Vec<_> = (0..16).map(|_| Pubkey::new_unique()).collect();
1312 keys.push(crate::instr::program_ids::METEORA_DLMM_PROGRAM_ID);
1313 let mut wire = crate::instr::meteora_dlmm::discriminators::SWAP2.to_vec();
1314 wire.extend_from_slice(&100u64.to_le_bytes());
1315 wire.extend_from_slice(&50u64.to_le_bytes());
1316 for tail in [vec![], vec![1, 0, 0, 0, 0, 1], vec![0, 0, 0, 0]] {
1317 let mut data = wire.clone();
1318 data.extend_from_slice(&tail);
1319 let tx = Transaction {
1320 message: Some(Message {
1321 account_keys: keys.iter().map(|k| k.to_bytes().to_vec()).collect(),
1322 instructions: vec![CompiledInstruction {
1323 program_id_index: 16,
1324 accounts: (0..16).collect(),
1325 data,
1326 }],
1327 ..Default::default()
1328 }),
1329 ..Default::default()
1330 };
1331 let route =
1332 analyze_yellowstone_transaction_routes(&tx, &TransactionStatusMeta::default(), &[]);
1333 assert_eq!(route.legs.len(), usize::from(tail == [0, 0, 0, 0]));
1334 assert_eq!(route.unknown_invocations.len(), usize::from(tail != [0, 0, 0, 0]));
1335 }
1336 }
1337
1338 #[test]
1339 fn conflicting_mint_evidence_is_sticky_and_cannot_spread_to_neighbours() {
1340 let keys = [
1341 Pubkey::new_unique(),
1342 Pubkey::new_unique(),
1343 Pubkey::new_unique(),
1344 Pubkey::new_unique(),
1345 Pubkey::new_unique(),
1346 ROUTE_SPL_TOKEN,
1347 ];
1348 let balance = |index, mint: Pubkey| TokenBalance {
1349 account_index: index,
1350 mint: mint.to_string(),
1351 ..Default::default()
1352 };
1353 let mut plain = vec![3];
1354 plain.extend_from_slice(&10u64.to_le_bytes());
1355 let mut checked = vec![12];
1356 checked.extend_from_slice(&10u64.to_le_bytes());
1357 checked.push(6);
1358 let transaction = |with_checked| {
1359 let mut instructions = vec![];
1360 if with_checked {
1361 instructions.push(CompiledInstruction {
1362 program_id_index: 5,
1363 accounts: vec![0, 4, 1, 2],
1364 data: checked.clone(),
1365 });
1366 }
1367 for accounts in [vec![0, 1, 2], vec![1, 2, 3]] {
1368 instructions.push(CompiledInstruction {
1369 program_id_index: 5,
1370 accounts,
1371 data: plain.clone(),
1372 });
1373 }
1374 Transaction {
1375 message: Some(Message {
1376 account_keys: keys.iter().map(|k| k.to_bytes().to_vec()).collect(),
1377 instructions,
1378 ..Default::default()
1379 }),
1380 ..Default::default()
1381 }
1382 };
1383 for with_checked in [false, true] {
1384 for reverse in [false, true] {
1385 let (before, after) = if reverse { (keys[4], keys[3]) } else { (keys[3], keys[4]) };
1386 let meta = TransactionStatusMeta {
1387 pre_token_balances: vec![balance(0, before), balance(1, keys[4])],
1388 post_token_balances: vec![balance(0, after)],
1389 ..Default::default()
1390 };
1391 let tx = transaction(with_checked);
1392 let mints = transaction_token_mints(&tx, &meta);
1393 assert_eq!(mints.get(&keys[0]), None);
1394 assert_eq!(mints.get(&keys[1]), Some(&keys[4]));
1395 assert_eq!(mints.get(&keys[2]), Some(&keys[4]));
1396 assert_eq!(
1397 analyze_yellowstone_transaction_routes(&tx, &meta, &[]).transfers[0].mint,
1398 None
1399 );
1400 }
1401 }
1402 let meta = TransactionStatusMeta {
1403 pre_token_balances: vec![balance(0, keys[3])],
1404 ..Default::default()
1405 };
1406 assert_eq!(transaction_token_mints(&transaction(true), &meta).get(&keys[0]), None);
1408 }
1409
1410 #[test]
1411 fn cached_subtree_boundaries_and_flags_match_exhaustive_scans() {
1412 let keys: Vec<_> = (0..13).map(|_| Pubkey::new_unique()).collect();
1413 let opaque = Pubkey::new_unique();
1414 let mut swap_data = crate::instr::raydium_cpmm::discriminators::SWAP_BASE_IN.to_vec();
1415 swap_data.extend_from_slice(&100u64.to_le_bytes());
1416 swap_data.extend_from_slice(&50u64.to_le_bytes());
1417 let swap_accounts: Vec<u8> = (0..13).collect();
1418 let mut transfer_data = vec![3];
1419 transfer_data.extend_from_slice(&70u64.to_le_bytes());
1420 let transfer_accounts = [4, 5, 0];
1421 let mut state = 0x1234_5678u64;
1422 for seed in 0..128 {
1423 let mut invocations = Vec::new();
1424 for outer in 0..3 {
1425 invocations.push(Invocation {
1426 position: InstructionPosition {
1427 outer_index: outer,
1428 inner_index: None,
1429 stack_height: Some(1),
1430 },
1431 program: opaque,
1432 program_resolved: true,
1433 accounts: &[],
1434 data: &[],
1435 });
1436 for j in 0..24 {
1437 state ^= state << 13;
1438 state ^= state >> 7;
1439 state ^= state << 17;
1440 let (program, accounts, data): (_, &[u8], &[u8]) = match state % 3 {
1441 0 => (opaque, &[], &[]),
1442 1 => (
1443 crate::instr::program_ids::RAYDIUM_CPMM_PROGRAM_ID,
1444 &swap_accounts,
1445 &swap_data,
1446 ),
1447 _ => (ROUTE_SPL_TOKEN, &transfer_accounts, &transfer_data),
1448 };
1449 invocations.push(Invocation {
1450 position: InstructionPosition {
1451 outer_index: outer,
1452 inner_index: Some(if seed % 7 == 0 { j / 2 } else { j }),
1453 stack_height: if state % 5 == 0 {
1454 None
1455 } else {
1456 Some(2 + (state % 6) as u32)
1457 },
1458 },
1459 program,
1460 program_resolved: true,
1461 accounts,
1462 data,
1463 });
1464 }
1465 }
1466 let mints = HashMap::new();
1467 let analysis = invocation_analysis(&invocations, &keys, &mints, &[], &mut Vec::new());
1468 for (i, row) in analysis.iter().enumerate() {
1469 let reference = descendants(i, &invocations);
1470 assert_eq!(row.descendant_end, i + 1 + reference.len());
1471 assert_eq!(
1472 row.has_token_transfers,
1473 reference.iter().any(|ix| transfer(ix, &keys, &mints).is_some())
1474 );
1475 assert_eq!(
1476 row.has_known_swap_descendants,
1477 reference.iter().any(|ix| swap(ix, &keys, &mints, &[]).is_some())
1478 );
1479 }
1480 }
1481 }
1482}
1483
1484#[cfg(test)]
1485mod review_invocation_join_regressions {
1486 use super::*;
1487 use yellowstone_grpc_proto::prelude::{
1488 CompiledInstruction, InnerInstruction, InnerInstructions, Message,
1489 };
1490 fn exhaustive_invocations<'a>(
1491 transaction: &'a Transaction,
1492 meta: &'a TransactionStatusMeta,
1493 keys: &[Pubkey],
1494 ) -> Vec<Invocation<'a>> {
1495 let mut result = Vec::new();
1496 if let Some(message) = &transaction.message {
1497 for (i, ix) in message.instructions.iter().enumerate() {
1498 result.push(Invocation {
1499 position: InstructionPosition {
1500 outer_index: i as u32,
1501 inner_index: None,
1502 stack_height: Some(1),
1503 },
1504 program: key(keys, ix.program_id_index),
1505 program_resolved: (ix.program_id_index as usize) < keys.len(),
1506 accounts: &ix.accounts,
1507 data: &ix.data,
1508 });
1509 for group in meta.inner_instructions.iter().filter(|g| g.index == i as u32) {
1510 for (j, ix) in group.instructions.iter().enumerate() {
1511 result.push(Invocation {
1512 position: InstructionPosition {
1513 outer_index: i as u32,
1514 inner_index: Some(j as u32),
1515 stack_height: ix.stack_height,
1516 },
1517 program: key(keys, ix.program_id_index),
1518 program_resolved: (ix.program_id_index as usize) < keys.len(),
1519 accounts: &ix.accounts,
1520 data: &ix.data,
1521 });
1522 }
1523 }
1524 }
1525 }
1526 result
1527 }
1528
1529 #[test]
1530 fn ordered_join_preserves_unsorted_duplicate_and_orphan_group_semantics() {
1531 let keys = [Pubkey::new_unique(), Pubkey::new_unique(), Pubkey::new_unique()];
1532 let tx = Transaction {
1533 message: Some(Message {
1534 account_keys: keys.iter().map(|k| k.to_bytes().to_vec()).collect(),
1535 instructions: (0..3)
1536 .map(|i| CompiledInstruction {
1537 program_id_index: i,
1538 data: vec![i as u8],
1539 accounts: vec![0, 1],
1540 })
1541 .collect(),
1542 ..Default::default()
1543 }),
1544 ..Default::default()
1545 };
1546 let indices = [2, 0, u32::MAX, 1, 0, 2];
1547 let mut groups: Vec<_> = indices
1548 .into_iter()
1549 .enumerate()
1550 .map(|(i, index)| InnerInstructions {
1551 index,
1552 instructions: vec![InnerInstruction {
1553 program_id_index: if i % 2 == 0 { 1 } else { 255 },
1554 accounts: vec![i as u8],
1555 data: vec![i as u8],
1556 stack_height: if i % 3 == 0 { None } else { Some(2) },
1557 }],
1558 })
1559 .collect();
1560 for permutation in 0..12 {
1561 if permutation % 2 == 0 {
1562 groups.rotate_left(1);
1563 } else {
1564 groups.reverse();
1565 }
1566 let meta =
1567 TransactionStatusMeta { inner_instructions: groups.clone(), ..Default::default() };
1568 let expected = exhaustive_invocations(&tx, &meta, &keys);
1569 let actual = transaction_invocations(&tx, &meta, &keys);
1570 assert_eq!(actual.len(), 8);
1571 assert_eq!(actual.len(), expected.len());
1572 for (actual, expected) in actual.iter().zip(expected.iter()) {
1573 assert_eq!(actual.position, expected.position);
1574 assert_eq!(actual.program, expected.program);
1575 assert_eq!(actual.program_resolved, expected.program_resolved);
1576 assert_eq!(actual.accounts, expected.accounts);
1577 assert_eq!(actual.data, expected.data);
1578 }
1579 }
1580 groups.sort_by_key(|g| g.index);
1581 let meta = TransactionStatusMeta { inner_instructions: groups, ..Default::default() };
1582 let expected = exhaustive_invocations(&tx, &meta, &keys);
1583 let actual = transaction_invocations(&tx, &meta, &keys);
1584 assert_eq!(
1585 actual.iter().map(|ix| (ix.position, ix.data)).collect::<Vec<_>>(),
1586 expected.iter().map(|ix| (ix.position, ix.data)).collect::<Vec<_>>()
1587 );
1588 assert!(transaction_invocations(&Transaction::default(), &meta, &keys).is_empty());
1589 }
1590}