use crate::core::clock::now_us;
use crate::core::events::EventMetadata;
#[cfg(target_os = "windows")]
use crate::core::now_micros;
use base64::{engine::general_purpose, Engine as _};
use solana_sdk::{pubkey::Pubkey, signature::Signature};
#[inline]
pub fn extract_program_data(log: &str) -> Option<Vec<u8>> {
use memchr::memmem;
let log_bytes = log.as_bytes();
let pos = memmem::find(log_bytes, b"Program data: ")?;
let data_part = &log[pos + 14..];
general_purpose::STANDARD.decode(data_part.trim()).ok()
}
#[inline]
pub fn extract_discriminator_fast(log: &str) -> Option<[u8; 8]> {
let pos = memchr::memmem::find(log.as_bytes(), b"Program data: ")?;
let prefix = log.get(pos + 14..)?.trim().as_bytes().get(..12)?;
let mut bytes = [0u8; 9];
let len = general_purpose::STANDARD.decode_slice(prefix, &mut bytes).ok()?;
(len >= 8).then(|| bytes[..8].try_into().unwrap())
}
#[inline]
pub fn read_u64_le(data: &[u8], offset: usize) -> Option<u64> {
data.get(offset..offset.checked_add(8)?).map(|slice| u64::from_le_bytes(slice.try_into().unwrap()))
}
#[inline]
pub fn read_u32_le(data: &[u8], offset: usize) -> Option<u32> {
data.get(offset..offset.checked_add(4)?).map(|slice| u32::from_le_bytes(slice.try_into().unwrap()))
}
pub fn read_i64_le(data: &[u8], offset: usize) -> Option<i64> {
data.get(offset..offset.checked_add(8)?).map(|slice| i64::from_le_bytes(slice.try_into().unwrap()))
}
pub fn read_i32_le(data: &[u8], offset: usize) -> Option<i32> {
data.get(offset..offset.checked_add(4)?).map(|slice| i32::from_le_bytes(slice.try_into().unwrap()))
}
pub fn read_u128_le(data: &[u8], offset: usize) -> Option<u128> {
data.get(offset..offset.checked_add(16)?).map(|slice| u128::from_le_bytes(slice.try_into().unwrap()))
}
pub fn read_u16_le(data: &[u8], offset: usize) -> Option<u16> {
data.get(offset..offset.checked_add(2)?).map(|slice| u16::from_le_bytes(slice.try_into().unwrap()))
}
pub fn read_u8(data: &[u8], offset: usize) -> Option<u8> {
data.get(offset).copied()
}
#[inline]
pub fn read_pubkey(data: &[u8], offset: usize) -> Option<Pubkey> {
data.get(offset..offset.checked_add(32)?).and_then(|slice| {
let key_bytes: [u8; 32] = slice.try_into().ok()?;
Some(Pubkey::new_from_array(key_bytes))
})
}
pub fn read_string(data: &[u8], offset: usize) -> Option<(String, usize)> {
let (string_ref, consumed) = read_string_ref(data, offset)?;
Some((string_ref.to_string(), consumed))
}
#[inline(always)] pub fn read_string_ref(data: &[u8], offset: usize) -> Option<(&str, usize)> {
let tail = data.get(offset..)?;
let len = read_u32_le(tail, 0)? as usize;
let bytes = tail.get(4..)?.get(..len)?;
Some((std::str::from_utf8(bytes).ok()?, len.checked_add(4)?))
}
pub fn read_bool(data: &[u8], offset: usize) -> Option<bool> {
if data.len() <= offset {
return None;
}
Some(data[offset] == 1)
}
pub fn timestamp_to_microseconds(seconds: i64, nanos: i32) -> i128 {
seconds as i128 * 1_000_000 + (nanos as i128 / 1_000)
}
pub fn create_metadata_simple(
signature: Signature,
slot: u64,
tx_index: u64,
block_time_us: Option<i64>,
program_id: Pubkey,
grpc_recv_us: i64,
) -> EventMetadata {
EventMetadata {
signature,
slot,
tx_index,
block_time_us: block_time_us.unwrap_or(0),
grpc_recv_us,
recent_blockhash: None,
}
}
pub fn create_metadata_default(
signature: Signature,
slot: u64,
tx_index: u64,
block_time_us: Option<i64>,
) -> EventMetadata {
let current_time = now_us();
EventMetadata {
signature,
slot,
tx_index,
block_time_us: block_time_us.unwrap_or(0),
grpc_recv_us: current_time,
recent_blockhash: None,
}
}
pub mod text_parser {
pub fn extract_number_from_text(text: &str, field: &str) -> Option<u64> {
if let Some(start) = text.find(&format!("{}:", field)) {
let after_colon = &text[start + field.len() + 1..];
if let Some(end) = after_colon.find(' ').or_else(|| after_colon.find(',')) {
after_colon[..end].trim().parse().ok()
} else {
after_colon.trim().parse().ok()
}
} else {
None
}
}
pub fn extract_text_field(text: &str, field: &str) -> Option<String> {
extract_text_field_ref(text, field).map(|s| s.to_string())
}
#[inline(always)] pub fn extract_text_field_ref<'a>(text: &'a str, field: &str) -> Option<&'a str> {
let start = text.find(&format!("{}:", field))?;
let after_colon = &text[start + field.len() + 1..];
if let Some(end) = after_colon.find(',').or_else(|| after_colon.find(' ')) {
Some(after_colon[..end].trim())
} else {
Some(after_colon.trim())
}
}
pub fn detect_trade_type(log: &str) -> Option<bool> {
if log.contains("buy") || log.contains("Buy") {
Some(true)
} else if log.contains("sell") || log.contains("Sell") {
Some(false)
} else {
None
}
}
}
#[cfg(test)]
mod review_log_bounds_tests {
use super::*;
#[test]
fn discriminator_prefix_handles_short_and_non_ascii_logs_without_panicking() {
let discriminator = [1u8, 2, 3, 4, 5, 6, 7, 8];
let encoded = general_purpose::STANDARD.encode(discriminator);
assert_eq!(encoded.len(), 12);
assert_eq!(extract_discriminator_fast(&format!("Program data: {encoded}")), Some(discriminator));
for length in 0..encoded.len() {
assert_eq!(extract_discriminator_fast(&format!("Program data: {}", &encoded[..length])), None);
}
for prefix in ["AAAAAAAAAAAA", "AAAAAAAAAAAAA", "AAAAAAAAAAAAAA", "AAAAAAAAAAAAAAA", "AAAAAAAAAAAAAAAé", "éééééééé"] {
let log = format!("Program data: {prefix}");
let _ = extract_discriminator_fast(&log);
}
let mut payload = discriminator.to_vec();
payload.extend_from_slice(&[9; 128]);
assert_eq!(extract_discriminator_fast(&format!("Program data: {}", general_purpose::STANDARD.encode(payload))), Some(discriminator));
}
#[test]
fn log_readers_reject_overflowing_offsets_and_invalid_strings() {
let data = [0u8; 64];
for offset in [65, usize::MAX - 32, usize::MAX - 1, usize::MAX] {
assert_eq!(read_u16_le(&data, offset), None);
assert_eq!(read_u32_le(&data, offset), None);
assert_eq!(read_i32_le(&data, offset), None);
assert_eq!(read_u64_le(&data, offset), None);
assert_eq!(read_i64_le(&data, offset), None);
assert_eq!(read_u128_le(&data, offset), None);
assert_eq!(read_pubkey(&data, offset), None);
assert_eq!(read_string_ref(&data, offset), None);
}
assert_eq!(read_string_ref(&u32::MAX.to_le_bytes(), 0), None);
assert_eq!(read_string_ref(&[1, 0, 0, 0, 255], 0), None);
}
}