1use crate::core::events::EventMetadata;
4use solana_sdk::{pubkey::Pubkey, signature::Signature};
5use yellowstone_grpc_proto::prelude::{Transaction, TransactionStatusMeta};
6
7pub fn create_metadata(
9 signature: Signature,
10 slot: u64,
11 tx_index: u64,
12 block_time_us: i64,
13 grpc_recv_us: i64,
14) -> EventMetadata {
15 EventMetadata { signature, slot, tx_index, block_time_us, grpc_recv_us, recent_blockhash: None }
16}
17
18#[inline(always)]
20pub fn create_metadata_simple(
21 signature: Signature,
22 slot: u64,
23 tx_index: u64,
24 block_time_us: Option<i64>,
25 _program_id: Pubkey,
26) -> EventMetadata {
27 let current_time = now_us();
28
29 EventMetadata {
30 signature,
31 slot,
32 tx_index,
33 block_time_us: block_time_us.unwrap_or(0),
34 grpc_recv_us: current_time,
35 recent_blockhash: None,
36 }
37}
38
39#[inline(always)]
41pub fn read_u64_le(data: &[u8], offset: usize) -> Option<u64> {
42 data.get(offset..offset.checked_add(8)?)
43 .map(|slice| u64::from_le_bytes(slice.try_into().unwrap()))
44}
45
46#[inline(always)]
48pub fn read_u32_le(data: &[u8], offset: usize) -> Option<u32> {
49 data.get(offset..offset.checked_add(4)?)
50 .map(|slice| u32::from_le_bytes(slice.try_into().unwrap()))
51}
52
53#[inline(always)]
55pub fn read_i64_le(data: &[u8], offset: usize) -> Option<i64> {
56 data.get(offset..offset.checked_add(8)?)
57 .map(|slice| i64::from_le_bytes(slice.try_into().unwrap()))
58}
59
60#[inline(always)]
62pub fn read_u16_le(data: &[u8], offset: usize) -> Option<u16> {
63 data.get(offset..offset.checked_add(2)?)
64 .map(|slice| u16::from_le_bytes(slice.try_into().unwrap()))
65}
66
67#[inline(always)]
69pub fn read_u8(data: &[u8], offset: usize) -> Option<u8> {
70 data.get(offset).copied()
71}
72
73#[inline(always)]
75pub fn read_i32_le(data: &[u8], offset: usize) -> Option<i32> {
76 data.get(offset..offset.checked_add(4)?)
77 .map(|slice| i32::from_le_bytes(slice.try_into().unwrap()))
78}
79
80#[inline(always)]
82pub fn read_u128_le(data: &[u8], offset: usize) -> Option<u128> {
83 data.get(offset..offset.checked_add(16)?)
84 .map(|slice| u128::from_le_bytes(slice.try_into().unwrap()))
85}
86
87#[inline(always)]
89pub fn read_bool(data: &[u8], offset: usize) -> Option<bool> {
90 read_option_bool_idl(data, offset)
91}
92
93#[inline(always)]
96pub fn read_option_bool_idl(data: &[u8], offset: usize) -> Option<bool> {
97 match data.get(offset).copied()? {
98 0 => Some(false),
99 1 => Some(true),
100 _ => None,
101 }
102}
103
104#[inline(always)]
107pub fn read_option_u64_idl(data: &[u8], offset: usize) -> Option<u64> {
108 read_u64_le(data, offset)
109}
110
111#[inline(always)]
113pub fn read_pubkey(data: &[u8], offset: usize) -> Option<Pubkey> {
114 data.get(offset..offset.checked_add(32)?).and_then(|slice| Pubkey::try_from(slice).ok())
115}
116
117#[inline(always)]
119pub fn get_account(accounts: &[Pubkey], index: usize) -> Option<Pubkey> {
120 accounts.get(index).copied()
121}
122
123pub fn calculate_slippage_bps(amount_in: u64, amount_out_min: u64) -> u16 {
126 if amount_in == 0 {
127 return 0;
128 }
129
130 let slippage =
132 (u128::from(amount_in.saturating_sub(amount_out_min)) * 10000) / u128::from(amount_in);
133 slippage.min(10000) as u16
134}
135
136pub fn calculate_price_impact_bps(_amount_in: u64, amount_out: u64, expected_out: u64) -> u16 {
138 if expected_out == 0 {
139 return 0;
140 }
141
142 let impact =
143 (u128::from(expected_out.saturating_sub(amount_out)) * 10000) / u128::from(expected_out);
144 impact.min(10000) as u16
145}
146
147pub fn read_bytes(data: &[u8], offset: usize, length: usize) -> Option<&[u8]> {
149 data.get(offset..offset.checked_add(length)?)
150}
151
152#[inline]
156pub fn parse_create_v2_tail_fields(
157 data_after_discriminator: &[u8],
158) -> Option<(Pubkey, bool, bool, u64, bool)> {
159 let mut offset = 0usize;
160 let (_, l) = read_str_unchecked(data_after_discriminator, offset)?;
161 offset += l;
162 let (_, l) = read_str_unchecked(data_after_discriminator, offset)?;
163 offset += l;
164 let (_, l) = read_str_unchecked(data_after_discriminator, offset)?;
165 offset += l;
166 if data_after_discriminator.len() < offset + 32 + 1 {
167 return None;
168 }
169 let creator = read_pubkey(data_after_discriminator, offset)?;
170 offset += 32;
171 let is_mayhem_mode = read_option_bool_idl(data_after_discriminator, offset)?;
172 offset += 1;
173 let (is_cashback_enabled, creator_fee_bps, is_holder_reward) =
174 parse_create_v2_optional_tail(&data_after_discriminator[offset..])?;
175 Some((creator, is_mayhem_mode, is_cashback_enabled, creator_fee_bps, is_holder_reward))
176}
177
178#[inline]
181pub(crate) fn parse_create_v2_optional_tail(tail: &[u8]) -> Option<(bool, u64, bool)> {
182 if tail.is_empty() {
183 return Some((false, 0, false));
184 }
185 let cashback = read_option_bool_idl(tail, 0)?;
186 if tail.len() == 1 {
187 return Some((cashback, 0, false));
188 }
189 let fee = read_u64_le(tail, 1)?;
190 let holder_reward = if tail.len() == 9 { false } else { read_option_bool_idl(tail, 9)? };
191 Some((cashback, fee, holder_reward))
192}
193
194#[inline]
197pub fn read_str_unchecked(data: &[u8], offset: usize) -> Option<(&str, usize)> {
198 let tail = data.get(offset..)?;
199 let len = read_u32_le(tail, 0)? as usize;
200 let bytes = tail.get(4..)?.get(..len)?;
201 Some((std::str::from_utf8(bytes).ok()?, len.checked_add(4)?))
202}
203
204pub fn read_vec_u64(data: &[u8], offset: usize) -> Option<Vec<u64>> {
207 let tail = data.get(offset..)?;
208 let count = read_u32_le(tail, 0)? as usize;
209 let bytes = tail.get(4..)?.get(..count.checked_mul(8)?)?;
210 Some(bytes.chunks_exact(8).map(|value| u64::from_le_bytes(value.try_into().unwrap())).collect())
211}
212
213#[inline(always)]
215pub fn read_pubkey_fast(bytes: &[u8]) -> Pubkey {
216 crate::logs::utils::read_pubkey(bytes, 0).unwrap_or_default()
217}
218
219pub fn get_instruction_account_getter<'a>(
222 meta: &'a TransactionStatusMeta,
223 transaction: &'a Option<Transaction>,
224 account_keys: Option<&'a Vec<Vec<u8>>>,
225 loaded_writable_addresses: &'a [Vec<u8>],
227 loaded_readonly_addresses: &'a [Vec<u8>],
228 index: &(i32, i32), ) -> Option<impl Fn(usize) -> Pubkey + 'a> {
230 let accounts = if index.1 >= 0 {
232 let outer_idx = index.0 as u32;
234 meta.inner_instructions
235 .binary_search_by_key(&outer_idx, |i| i.index)
236 .ok()
237 .and_then(|pos| meta.inner_instructions.get(pos))
238 .or_else(|| {
239 meta.inner_instructions.iter().find(|i| i.index == outer_idx)
241 })?
242 .instructions
243 .get(index.1 as usize)?
244 .accounts
245 .as_slice()
246 } else {
247 transaction
249 .as_ref()?
250 .message
251 .as_ref()?
252 .instructions
253 .get(index.0 as usize)?
254 .accounts
255 .as_slice()
256 };
257
258 Some(move |acc_index: usize| -> Pubkey {
260 let account_index = match accounts.get(acc_index) {
262 Some(&idx) => idx as usize,
263 None => return Pubkey::default(),
264 };
265 let Some(keys) = account_keys else {
267 return Pubkey::default();
268 };
269 if let Some(key_bytes) = keys.get(account_index) {
271 return Pubkey::try_from(key_bytes.as_slice()).unwrap_or_default();
272 }
273 let writable_offset = account_index.saturating_sub(keys.len());
275 if let Some(key_bytes) = loaded_writable_addresses.get(writable_offset) {
276 return Pubkey::try_from(key_bytes.as_slice()).unwrap_or_default();
277 }
278 let readonly_offset = writable_offset.saturating_sub(loaded_writable_addresses.len());
280 if let Some(key_bytes) = loaded_readonly_addresses.get(readonly_offset) {
281 return Pubkey::try_from(key_bytes.as_slice()).unwrap_or_default();
282 }
283 Pubkey::default()
284 })
285}
286
287use crate::core::clock::now_us;
288use std::collections::HashMap;
290
291pub struct InnerInstructionsIndex<'a> {
293 index_map: HashMap<u32, &'a yellowstone_grpc_proto::prelude::InnerInstructions>,
295}
296
297impl<'a> InnerInstructionsIndex<'a> {
298 #[inline]
300 pub fn new(meta: &'a TransactionStatusMeta) -> Self {
301 let mut index_map = HashMap::with_capacity(meta.inner_instructions.len());
302 for inner in &meta.inner_instructions {
303 index_map.insert(inner.index, inner);
304 }
305 Self { index_map }
306 }
307
308 #[inline]
310 pub fn get(
311 &self,
312 outer_index: u32,
313 ) -> Option<&'a yellowstone_grpc_proto::prelude::InnerInstructions> {
314 self.index_map.get(&outer_index).copied()
315 }
316}
317
318pub fn get_instruction_account_getter_indexed<'a>(
320 inner_index: &InnerInstructionsIndex<'a>,
321 transaction: &'a Option<Transaction>,
322 account_keys: Option<&'a Vec<Vec<u8>>>,
323 loaded_writable_addresses: &'a [Vec<u8>],
324 loaded_readonly_addresses: &'a [Vec<u8>],
325 index: &(i32, i32),
326) -> Option<impl Fn(usize) -> Pubkey + 'a> {
327 let accounts = if index.1 >= 0 {
328 inner_index.get(index.0 as u32)?.instructions.get(index.1 as usize)?.accounts.as_slice()
330 } else {
331 transaction
332 .as_ref()?
333 .message
334 .as_ref()?
335 .instructions
336 .get(index.0 as usize)?
337 .accounts
338 .as_slice()
339 };
340
341 Some(move |acc_index: usize| -> Pubkey {
342 let account_index = match accounts.get(acc_index) {
343 Some(&idx) => idx as usize,
344 None => return Pubkey::default(),
345 };
346 let Some(keys) = account_keys else {
347 return Pubkey::default();
348 };
349 if let Some(key_bytes) = keys.get(account_index) {
350 return Pubkey::try_from(key_bytes.as_slice()).unwrap_or_default();
351 }
352 let writable_offset = account_index.saturating_sub(keys.len());
353 if let Some(key_bytes) = loaded_writable_addresses.get(writable_offset) {
354 return Pubkey::try_from(key_bytes.as_slice()).unwrap_or_default();
355 }
356 let readonly_offset = writable_offset.saturating_sub(loaded_writable_addresses.len());
357 if let Some(key_bytes) = loaded_readonly_addresses.get(readonly_offset) {
358 return Pubkey::try_from(key_bytes.as_slice()).unwrap_or_default();
359 }
360 Pubkey::default()
361 })
362}
363
364#[cfg(test)]
365mod option_bool_tests {
366 use super::*;
367
368 #[test]
369 fn read_option_bool_idl_strict() {
370 assert_eq!(read_option_bool_idl(&[0], 0), Some(false));
371 assert_eq!(read_option_bool_idl(&[1], 0), Some(true));
372 assert_eq!(read_option_bool_idl(&[2], 0), None);
373 }
374
375 #[test]
376 fn parse_create_v2_tail_matches_anchor_len() {
377 let mut p = Vec::new();
379 p.extend_from_slice(&(1u32.to_le_bytes()));
380 p.push(b'a');
381 p.extend_from_slice(&(1u32.to_le_bytes()));
382 p.push(b'b');
383 p.extend_from_slice(&(1u32.to_le_bytes()));
384 p.push(b'c');
385 p.extend_from_slice(&[0u8; 32]);
386 p.push(1u8); p.push(1u8); assert_eq!(p.len(), 49);
389 let (creator, mayhem, cb, creator_fee_bps, holder_reward) =
390 parse_create_v2_tail_fields(&p).expect("parse");
391 assert_eq!(creator, Pubkey::default());
392 assert!(mayhem);
393 assert!(cb);
394 assert_eq!(creator_fee_bps, 0);
395 assert!(!holder_reward);
396 }
397
398 #[test]
399 fn parse_create_v2_tail_reads_holder_rewards_fields() {
400 let mut p = Vec::new();
401 for value in ["a", "b", "c"] {
402 p.extend_from_slice(&(value.len() as u32).to_le_bytes());
403 p.extend_from_slice(value.as_bytes());
404 }
405 p.extend_from_slice(&[0u8; 32]);
406 p.push(0); p.push(0); p.extend_from_slice(&250u64.to_le_bytes());
409 p.push(1); let (_, mayhem, cashback, creator_fee_bps, holder_reward) =
412 parse_create_v2_tail_fields(&p).expect("parse");
413 assert!(!mayhem);
414 assert!(!cashback);
415 assert_eq!(creator_fee_bps, 250);
416 assert!(holder_reward);
417 }
418}
419
420#[cfg(test)]
421mod review_tail_regressions {
422 use super::*;
423 #[test]
424 fn optional_create_tail_rejects_partial_fee_and_invalid_boolean() {
425 for n in 1..8 {
426 let mut tail = vec![0];
427 tail.extend(std::iter::repeat_n(1, n));
428 assert!(parse_create_v2_optional_tail(&tail).is_none());
429 }
430 assert_eq!(parse_create_v2_optional_tail(&[]), Some((false, 0, false)));
431 assert_eq!(parse_create_v2_optional_tail(&[1]), Some((true, 0, false)));
432 let mut tail = vec![0];
433 tail.extend_from_slice(&1u64.to_le_bytes());
434 assert_eq!(parse_create_v2_optional_tail(&tail), Some((false, 1, false)));
435 tail.push(2);
436 assert!(parse_create_v2_optional_tail(&tail).is_none());
437 tail[9] = 1;
438 assert_eq!(parse_create_v2_optional_tail(&tail), Some((false, 1, true)));
439 tail[0] = 2;
440 assert!(parse_create_v2_optional_tail(&tail).is_none());
441 }
442}
443
444#[cfg(test)]
445mod review_numeric_bounds_tests {
446 use super::*;
447
448 #[test]
449 fn oversized_offsets_and_lengths_return_none_without_panicking() {
450 let data = [0u8; 64];
451 for offset in [65, usize::MAX - 32, usize::MAX - 1, usize::MAX] {
452 assert_eq!(read_u16_le(&data, offset), None);
453 assert_eq!(read_u32_le(&data, offset), None);
454 assert_eq!(read_i32_le(&data, offset), None);
455 assert_eq!(read_u64_le(&data, offset), None);
456 assert_eq!(read_i64_le(&data, offset), None);
457 assert_eq!(read_u128_le(&data, offset), None);
458 assert_eq!(read_pubkey(&data, offset), None);
459 assert_eq!(read_str_unchecked(&data, offset), None);
460 assert_eq!(read_bytes(&data, offset, 8), None);
461 assert_eq!(read_vec_u64(&data, offset), None);
462 }
463 assert_eq!(read_bytes(&data, 1, usize::MAX), None);
464 assert_eq!(read_str_unchecked(&u32::MAX.to_le_bytes(), 0), None);
465 assert_eq!(read_str_unchecked(&[1, 0, 0, 0, 255], 0), None);
466 assert_eq!(read_str_unchecked(&[1, 0, 0, 0, b'x'], 0), Some(("x", 5)));
467 }
468
469 #[test]
470 fn borsh_vector_reads_values_and_rejects_truncation_before_allocation() {
471 let values = [0u64, 7, u64::MAX];
472 let mut data = vec![99, 99];
473 data.extend_from_slice(&3u32.to_le_bytes());
474 for value in values {
475 data.extend_from_slice(&value.to_le_bytes());
476 }
477 assert_eq!(read_vec_u64(&data, 2), Some(values.to_vec()));
478 for end in 0..data.len() {
479 assert_eq!(read_vec_u64(&data[..end], 2), None);
480 }
481 assert_eq!(read_vec_u64(&0u32.to_le_bytes(), 0), Some(Vec::new()));
482 assert_eq!(read_vec_u64(&u32::MAX.to_le_bytes(), 0), None);
483 }
484
485 #[test]
486 fn bps_ratio_is_bounded_and_correct_for_full_u64_range() {
487 for denominator in [0, 1, 10_000, u64::MAX / 2, u64::MAX] {
488 for amount in [0, 1, 10_000, u64::MAX / 2, u64::MAX] {
489 let expected = if denominator == 0 {
490 0
491 } else {
492 (u128::from(denominator.saturating_sub(amount)) * 10_000
493 / u128::from(denominator)) as u16
494 };
495 assert_eq!(calculate_slippage_bps(denominator, amount), expected);
496 assert_eq!(calculate_price_impact_bps(0, amount, denominator), expected);
497 }
498 }
499 }
500}
501
502#[cfg(test)]
503mod transaction_account_key_length_regressions {
504 use super::*;
505 use yellowstone_grpc_proto::prelude::{CompiledInstruction, Message};
506
507 #[test]
508 fn both_getters_keep_malformed_static_and_alt_keys_unresolved() {
509 let expected = Pubkey::new_unique();
510 for source in 0..3 {
511 for length in [0, 31, 32, 33, 64] {
512 let mut bytes = expected.to_bytes().to_vec();
513 bytes.resize(length, 0);
514 let keys =
515 vec![if source == 0 { bytes.clone() } else { expected.to_bytes().to_vec() }];
516 let writable =
517 vec![if source == 1 { bytes.clone() } else { expected.to_bytes().to_vec() }];
518 let readonly = vec![if source == 2 { bytes } else { expected.to_bytes().to_vec() }];
519 let tx = Some(Transaction {
520 message: Some(Message {
521 account_keys: keys.clone(),
522 instructions: vec![CompiledInstruction {
523 accounts: vec![0, 1, 2],
524 ..Default::default()
525 }],
526 ..Default::default()
527 }),
528 ..Default::default()
529 });
530 let meta = TransactionStatusMeta::default();
531 let index = InnerInstructionsIndex::new(&meta);
532 let position = (0, -1);
533 let get = get_instruction_account_getter(
534 &meta,
535 &tx,
536 Some(&keys),
537 &writable,
538 &readonly,
539 &position,
540 )
541 .unwrap();
542 let indexed = get_instruction_account_getter_indexed(
543 &index,
544 &tx,
545 Some(&keys),
546 &writable,
547 &readonly,
548 &position,
549 )
550 .unwrap();
551 let actual = if length == 32 { expected } else { Pubkey::default() };
552 assert_eq!(get(source), actual);
553 assert_eq!(indexed(source), actual);
554 assert_eq!(get(3), Pubkey::default());
555 }
556 }
557 }
558}
559
560#[cfg(test)]
561mod review_canonical_bool_regressions {
562 use super::*;
563 #[test]
564 fn bool_readers_accept_only_borsh_zero_and_one() {
565 for byte in 0..=u8::MAX {
566 let expected = match byte { 0 => Some(false), 1 => Some(true), _ => None };
567 assert_eq!(read_bool(&[byte],0),expected);
568 assert_eq!(read_option_bool_idl(&[byte],0),expected);
569 }
570 assert_eq!(read_bool(&[],0),None);
571 assert_eq!(read_bool(&[0],usize::MAX),None);
572 }
573}