1use crate::core::clock::now_us;
6use crate::core::events::EventMetadata;
7#[cfg(target_os = "windows")]
8use crate::core::now_micros;
9use base64::{engine::general_purpose, Engine as _};
10use solana_sdk::{pubkey::Pubkey, signature::Signature};
11
12#[inline]
14pub fn extract_program_data(log: &str) -> Option<Vec<u8>> {
15 use memchr::memmem;
16
17 let log_bytes = log.as_bytes();
18 let pos = memmem::find(log_bytes, b"Program data: ")?;
19
20 let data_part = &log[pos + 14..];
21 general_purpose::STANDARD.decode(data_part.trim()).ok()
22}
23
24#[inline]
26pub fn extract_discriminator_fast(log: &str) -> Option<[u8; 8]> {
27 let pos = memchr::memmem::find(log.as_bytes(), b"Program data: ")?;
28 let prefix = log.get(pos + 14..)?.trim().as_bytes().get(..12)?;
29 let mut bytes = [0u8; 9];
30 let len = general_purpose::STANDARD.decode_slice(prefix, &mut bytes).ok()?;
31 (len >= 8).then(|| bytes[..8].try_into().unwrap())
32}
33
34#[inline]
36pub fn read_u64_le(data: &[u8], offset: usize) -> Option<u64> {
37 data.get(offset..offset.checked_add(8)?).map(|slice| u64::from_le_bytes(slice.try_into().unwrap()))
38}
39
40#[inline]
42pub fn read_u32_le(data: &[u8], offset: usize) -> Option<u32> {
43 data.get(offset..offset.checked_add(4)?).map(|slice| u32::from_le_bytes(slice.try_into().unwrap()))
44}
45
46pub fn read_i64_le(data: &[u8], offset: usize) -> Option<i64> {
48 data.get(offset..offset.checked_add(8)?).map(|slice| i64::from_le_bytes(slice.try_into().unwrap()))
49}
50
51pub fn read_i32_le(data: &[u8], offset: usize) -> Option<i32> {
53 data.get(offset..offset.checked_add(4)?).map(|slice| i32::from_le_bytes(slice.try_into().unwrap()))
54}
55
56pub fn read_u128_le(data: &[u8], offset: usize) -> Option<u128> {
58 data.get(offset..offset.checked_add(16)?).map(|slice| u128::from_le_bytes(slice.try_into().unwrap()))
59}
60
61pub fn read_u16_le(data: &[u8], offset: usize) -> Option<u16> {
63 data.get(offset..offset.checked_add(2)?).map(|slice| u16::from_le_bytes(slice.try_into().unwrap()))
64}
65
66pub fn read_u8(data: &[u8], offset: usize) -> Option<u8> {
68 data.get(offset).copied()
69}
70
71#[inline]
73pub fn read_pubkey(data: &[u8], offset: usize) -> Option<Pubkey> {
74 data.get(offset..offset.checked_add(32)?).and_then(|slice| {
75 let key_bytes: [u8; 32] = slice.try_into().ok()?;
76 Some(Pubkey::new_from_array(key_bytes))
77 })
78}
79
80pub fn read_string(data: &[u8], offset: usize) -> Option<(String, usize)> {
82 let (string_ref, consumed) = read_string_ref(data, offset)?;
83 Some((string_ref.to_string(), consumed))
84}
85
86#[inline(always)] pub fn read_string_ref(data: &[u8], offset: usize) -> Option<(&str, usize)> {
99 let tail = data.get(offset..)?;
100 let len = read_u32_le(tail, 0)? as usize;
101 let bytes = tail.get(4..)?.get(..len)?;
102 Some((std::str::from_utf8(bytes).ok()?, len.checked_add(4)?))
103}
104
105pub fn read_bool(data: &[u8], offset: usize) -> Option<bool> {
107 if data.len() <= offset {
108 return None;
109 }
110 Some(data[offset] == 1)
111}
112
113pub fn timestamp_to_microseconds(seconds: i64, nanos: i32) -> i128 {
115 seconds as i128 * 1_000_000 + (nanos as i128 / 1_000)
117}
118
119pub fn create_metadata_simple(
121 signature: Signature,
122 slot: u64,
123 tx_index: u64,
124 block_time_us: Option<i64>,
125 program_id: Pubkey,
126 grpc_recv_us: i64,
127) -> EventMetadata {
128 EventMetadata {
129 signature,
130 slot,
131 tx_index,
132 block_time_us: block_time_us.unwrap_or(0),
133 grpc_recv_us,
134 recent_blockhash: None,
135 }
136}
137
138pub fn create_metadata_default(
140 signature: Signature,
141 slot: u64,
142 tx_index: u64,
143 block_time_us: Option<i64>,
144) -> EventMetadata {
145 let current_time = now_us();
146 EventMetadata {
147 signature,
148 slot,
149 tx_index,
150 block_time_us: block_time_us.unwrap_or(0),
151 grpc_recv_us: current_time,
152 recent_blockhash: None,
153 }
154}
155
156pub mod text_parser {
158
159 pub fn extract_number_from_text(text: &str, field: &str) -> Option<u64> {
161 if let Some(start) = text.find(&format!("{}:", field)) {
162 let after_colon = &text[start + field.len() + 1..];
163 if let Some(end) = after_colon.find(' ').or_else(|| after_colon.find(',')) {
164 after_colon[..end].trim().parse().ok()
165 } else {
166 after_colon.trim().parse().ok()
167 }
168 } else {
169 None
170 }
171 }
172
173 pub fn extract_text_field(text: &str, field: &str) -> Option<String> {
175 extract_text_field_ref(text, field).map(|s| s.to_string())
176 }
177
178 #[inline(always)] pub fn extract_text_field_ref<'a>(text: &'a str, field: &str) -> Option<&'a str> {
190 let start = text.find(&format!("{}:", field))?;
191 let after_colon = &text[start + field.len() + 1..];
192 if let Some(end) = after_colon.find(',').or_else(|| after_colon.find(' ')) {
193 Some(after_colon[..end].trim())
194 } else {
195 Some(after_colon.trim())
196 }
197 }
198
199 pub fn detect_trade_type(log: &str) -> Option<bool> {
201 if log.contains("buy") || log.contains("Buy") {
202 Some(true)
203 } else if log.contains("sell") || log.contains("Sell") {
204 Some(false)
205 } else {
206 None
207 }
208 }
209}
210
211#[cfg(test)]
212mod review_log_bounds_tests {
213 use super::*;
214
215 #[test]
216 fn discriminator_prefix_handles_short_and_non_ascii_logs_without_panicking() {
217 let discriminator = [1u8, 2, 3, 4, 5, 6, 7, 8];
218 let encoded = general_purpose::STANDARD.encode(discriminator);
219 assert_eq!(encoded.len(), 12);
220 assert_eq!(extract_discriminator_fast(&format!("Program data: {encoded}")), Some(discriminator));
221 for length in 0..encoded.len() {
222 assert_eq!(extract_discriminator_fast(&format!("Program data: {}", &encoded[..length])), None);
223 }
224 for prefix in ["AAAAAAAAAAAA", "AAAAAAAAAAAAA", "AAAAAAAAAAAAAA", "AAAAAAAAAAAAAAA", "AAAAAAAAAAAAAAAé", "éééééééé"] {
225 let log = format!("Program data: {prefix}");
226 let _ = extract_discriminator_fast(&log);
227 }
228 let mut payload = discriminator.to_vec();
229 payload.extend_from_slice(&[9; 128]);
230 assert_eq!(extract_discriminator_fast(&format!("Program data: {}", general_purpose::STANDARD.encode(payload))), Some(discriminator));
231 }
232
233 #[test]
234 fn log_readers_reject_overflowing_offsets_and_invalid_strings() {
235 let data = [0u8; 64];
236 for offset in [65, usize::MAX - 32, usize::MAX - 1, usize::MAX] {
237 assert_eq!(read_u16_le(&data, offset), None);
238 assert_eq!(read_u32_le(&data, offset), None);
239 assert_eq!(read_i32_le(&data, offset), None);
240 assert_eq!(read_u64_le(&data, offset), None);
241 assert_eq!(read_i64_le(&data, offset), None);
242 assert_eq!(read_u128_le(&data, offset), None);
243 assert_eq!(read_pubkey(&data, offset), None);
244 assert_eq!(read_string_ref(&data, offset), None);
245 }
246 assert_eq!(read_string_ref(&u32::MAX.to_le_bytes(), 0), None);
247 assert_eq!(read_string_ref(&[1, 0, 0, 0, 255], 0), None);
248 }
249}