1#![deny(
2 clippy::disallowed_methods,
3 clippy::suspicious,
4 clippy::style,
5 clippy::clone_on_ref_ptr,
6 missing_debug_implementations,
7 missing_copy_implementations
8)]
9#![warn(clippy::pedantic, missing_docs)]
10#![allow(clippy::module_name_repetitions)]
11#![allow(missing_docs, clippy::missing_errors_doc, clippy::missing_panics_doc)]
13
14use std::{
15 fmt::{Debug, Display},
16 fs,
17 path::{Path, PathBuf},
18 str::FromStr,
19 sync::Arc,
20};
21
22pub use futures;
23use serde::{Deserialize, Serialize};
24use serde_json::json;
25use solana_client::{nonblocking::rpc_client::RpcClient, rpc_request::RpcRequest};
26use solana_rpc_client_api::client_error::Result as ClientResult;
27use solana_sdk::{account::Account, bs58, pubkey::Pubkey, signature::Signature};
28use solana_transaction_status::{
29 option_serializer::OptionSerializer, EncodedConfirmedTransactionWithStatusMeta,
30 EncodedTransaction, EncodedTransactionWithStatusMeta, UiCompiledInstruction,
31 UiInnerInstructions, UiInstruction, UiMessage,
32};
33use yellowstone_grpc_proto::geyser::{SubscribeUpdateAccount, SubscribeUpdateAccountInfo};
34use yellowstone_vixen_core::{
35 instruction::{InstructionShared, InstructionUpdate},
36 log_messages::split_logs_by_outer_ix,
37 KeyBytes, ProgramParser,
38};
39
40const DEFAULT_RPC_ENDPOINT: &str = "https://api.devnet.solana.com";
41
42const FIXTURES_PATH: &str = "./fixtures";
43const PUBKEY_REGEX: &str = r"^[1-9A-HJ-NP-Za-km-z]{32,44}$";
44const TX_SIGNATURE_REGEX: &str = r"^[1-9A-HJ-NP-Za-km-z]{64,90}$";
45
46#[derive(Debug, Serialize, Deserialize)]
47#[serde(rename_all = "camelCase")]
48pub struct AccountInfo {
49 pub data: Vec<u8>,
50 pub pubkey: Pubkey,
51 pub executable: bool,
52 pub lamports: u64,
53 pub owner: Pubkey,
54 pub rent_epoch: u64,
55 pub space: u64,
56}
57
58impl From<AccountInfo> for SubscribeUpdateAccount {
59 fn from(value: AccountInfo) -> Self {
60 Self {
61 is_startup: false,
62 slot: 0,
63 account: Some(SubscribeUpdateAccountInfo {
64 txn_signature: None,
65 write_version: 0,
66 pubkey: value.pubkey.to_bytes().to_vec(),
67 data: value.data.into(),
68 executable: value.executable,
69 lamports: value.lamports,
70 owner: value.owner.to_bytes().to_vec(),
71 rent_epoch: value.rent_epoch,
72 }),
73 }
74 }
75}
76
77impl TryFrom<SubscribeUpdateAccount> for AccountInfo {
78 type Error = &'static str;
79
80 fn try_from(value: SubscribeUpdateAccount) -> Result<Self, Self::Error> {
81 let account_info = value.account.ok_or("Missing account info")?;
82
83 let pubkey = Pubkey::new_from_array(
84 account_info
85 .pubkey
86 .try_into()
87 .map_err(|_| "Invalid pubkey length")?,
88 );
89
90 let owner = Pubkey::new_from_array(
91 account_info
92 .owner
93 .try_into()
94 .map_err(|_| "Invalid owner length")?,
95 );
96
97 Ok(Self {
98 pubkey,
99 data: account_info.data.to_vec(),
100 executable: account_info.executable,
101 lamports: account_info.lamports,
102 owner,
103 rent_epoch: account_info.rent_epoch,
104 space: account_info.data.len() as u64,
105 })
106 }
107}
108
109#[derive(Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
110pub struct SerializablePubkey(pub [u8; 32]);
111
112impl Debug for SerializablePubkey {
113 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { Display::fmt(self, f) }
114}
115
116impl Display for SerializablePubkey {
117 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
118 f.write_str(&bs58::encode(&self.0).into_string())
119 }
120}
121
122impl From<KeyBytes<32>> for SerializablePubkey {
123 fn from(value: KeyBytes<32>) -> Self { Self(value.into_bytes()) }
124}
125
126impl From<SerializablePubkey> for KeyBytes<32> {
127 fn from(value: SerializablePubkey) -> Self { Self::new(value.0) }
128}
129
130pub type IxIndex = [usize; 2]; #[derive(Clone, Serialize, Deserialize, Debug)]
133pub struct SerializableInstructionUpdate {
134 pub ix_index: IxIndex,
135 pub program: SerializablePubkey,
136 pub accounts: Vec<SerializablePubkey>,
137 pub data: Vec<u8>,
138 pub inner: Vec<SerializableInstructionUpdate>,
139 #[serde(default)]
140 pub log_messages: Vec<String>,
141}
142
143impl From<&InstructionUpdate> for SerializableInstructionUpdate {
144 fn from(value: &InstructionUpdate) -> Self {
145 Self {
146 ix_index: [0; 2],
147 program: SerializablePubkey(value.program.0),
148 accounts: value
149 .accounts
150 .iter()
151 .map(|x| SerializablePubkey(x.into_bytes()))
152 .collect(),
153 data: value.data.clone(),
154 inner: value.inner.iter().map(Into::into).collect(),
155 log_messages: value.log_messages().to_vec(),
156 }
157 }
158}
159
160impl From<&SerializableInstructionUpdate> for InstructionUpdate {
161 fn from(value: &SerializableInstructionUpdate) -> Self {
162 let shared = Arc::new(InstructionShared {
163 log_messages: value.log_messages.clone(),
164 ..InstructionShared::default()
165 });
166
167 convert_instruction(value, &shared)
168 }
169}
170
171#[allow(clippy::cast_possible_truncation)]
172fn convert_instruction(
173 value: &SerializableInstructionUpdate,
174 shared: &Arc<InstructionShared>,
175) -> InstructionUpdate {
176 let log_range = 0..shared.log_messages.len();
177
178 InstructionUpdate {
179 program: value.program.into(),
180 accounts: value.accounts.iter().copied().map(Into::into).collect(),
181 data: value.data.clone(),
182 shared: Arc::clone(shared),
183 inner: value
184 .inner
185 .iter()
186 .map(|inner| convert_instruction(inner, shared))
187 .collect(),
188 log_range,
189 path: value
190 .ix_index
191 .iter()
192 .map(|x| *x as u32)
193 .collect::<Vec<u32>>()
194 .into(),
195 }
196}
197
198pub fn get_account_pubkey_from_index(
199 index: usize,
200 accounts: &[String],
201) -> Result<SerializablePubkey, String> {
202 if accounts.is_empty() {
203 return Err("No accounts found".to_string());
204 }
205
206 accounts.get(index).map_or(
207 Err(format!(
208 "Account index {} out of bounds for {} accounts",
209 index,
210 accounts.len(),
211 )),
212 |account| {
213 Ok(KeyBytes::<32>::from_str(account)
214 .map_err(|e| e.to_string())?
215 .into())
216 },
217 )
218}
219
220fn try_from_ui_instructions(
221 ui_ixs: &[UiCompiledInstruction],
222 accounts: &[String],
223 program_id: &str,
224) -> Result<Vec<SerializableInstructionUpdate>, String> {
225 let mut ixs: Vec<SerializableInstructionUpdate> = Vec::new();
226 for (idx, ix) in ui_ixs.iter().enumerate() {
227 let accounts_out = ix
228 .accounts
229 .iter()
230 .map(|account| get_account_pubkey_from_index(*account as usize, accounts))
231 .collect::<Result<Vec<SerializablePubkey>, String>>()?;
232 let program = get_account_pubkey_from_index(ix.program_id_index as usize, accounts)?;
233
234 let ix = SerializableInstructionUpdate {
235 ix_index: [idx, 0],
236 data: decode_bs58_to_bytes(&ix.data)?,
237 accounts: accounts_out,
238 program,
239 inner: Vec::new(),
240 log_messages: vec![],
241 };
242
243 ixs.push(ix);
244 }
245 Ok(filter_ixs(ixs, program_id))
246}
247
248fn try_from_ui_inner_ixs(
249 ui_inner_ixs: &UiInnerInstructions,
250 accounts: &[String],
251 program_id: &str,
252) -> Result<Vec<SerializableInstructionUpdate>, String> {
253 let mut ixs: Vec<SerializableInstructionUpdate> = Vec::new();
254 for (idx, ix) in ui_inner_ixs.instructions.iter().enumerate() {
255 if let UiInstruction::Compiled(compiled_ix) = ix {
256 let accounts_out = compiled_ix
257 .accounts
258 .iter()
259 .map(|account| get_account_pubkey_from_index(*account as usize, accounts))
260 .collect::<Result<Vec<SerializablePubkey>, String>>()?;
261 let program =
262 get_account_pubkey_from_index(compiled_ix.program_id_index as usize, accounts)?;
263
264 let ix = SerializableInstructionUpdate {
265 ix_index: [ui_inner_ixs.index.into(), idx],
266 data: decode_bs58_to_bytes(&compiled_ix.data)?,
267 accounts: accounts_out,
268 program,
269 inner: Vec::new(),
270 log_messages: vec![],
271 };
272 ixs.push(ix);
273 } else {
274 return Err("Invalid inner instruction".into());
275 }
276 }
277 Ok(filter_ixs(ixs, program_id))
278}
279
280fn assign_logs_to_instructions(
281 logs: &[String],
282 instructions: &mut [SerializableInstructionUpdate],
283) {
284 let per_outer = split_logs_by_outer_ix(logs);
285
286 for ix in instructions.iter_mut() {
287 let outer_idx = ix.ix_index[0];
288
289 if let Some(ix_logs) = per_outer.get(outer_idx) {
290 ix.log_messages.clone_from(ix_logs);
291 }
292 }
293}
294
295fn filter_ixs(
296 ixs: Vec<SerializableInstructionUpdate>,
297 program_id: &str,
298) -> Vec<SerializableInstructionUpdate> {
299 ixs.into_iter()
301 .filter(|ix| ix.program.to_string().eq(program_id))
302 .collect::<Vec<SerializableInstructionUpdate>>()
303}
304
305fn try_from_tx_meta<P: ProgramParser>(
306 value: EncodedConfirmedTransactionWithStatusMeta,
307 parser: &P,
308) -> Result<SerializableTransactionFixture, String> {
309 let EncodedConfirmedTransactionWithStatusMeta {
310 transaction,
311 slot: _,
312 block_time: _,
313 } = value;
314 let EncodedTransactionWithStatusMeta {
315 transaction,
316 meta,
317 version: _,
318 } = transaction;
319 let mut inner_ixs: Option<Vec<UiInnerInstructions>> = None;
320 let mut log_messages: Vec<String> = Vec::new();
321
322 let mut account_keys: Vec<String> = Vec::new();
323 let program_id = parser.program_id().to_string();
324
325 if let EncodedTransaction::Json(tx_data) = transaction {
326 if let UiMessage::Raw(raw_message) = tx_data.message {
327 account_keys.extend(raw_message.account_keys);
328
329 if let Some(meta) = meta {
330 inner_ixs = meta.inner_instructions.map(Some).flatten();
331
332 if let OptionSerializer::Some(logs) = meta.log_messages {
333 log_messages = logs;
334 }
335
336 if let OptionSerializer::Some(loaded) = meta.loaded_addresses {
337 for address in loaded.writable {
338 account_keys.push(address);
339 }
340
341 for address in loaded.readonly {
342 account_keys.push(address);
343 }
344 }
345 }
346
347 let mut program_filtered_ixs =
349 try_from_ui_instructions(&raw_message.instructions, &account_keys, &program_id)?;
350
351 if let Some(inner_ixs) = inner_ixs
353 && !inner_ixs.is_empty()
354 {
355 for ixs in inner_ixs {
356 let inner_ixs = try_from_ui_inner_ixs(&ixs, &account_keys, &program_id)?;
357
358 if !inner_ixs.is_empty() {
359 program_filtered_ixs.extend(inner_ixs);
360 }
361 }
362 }
363
364 assign_logs_to_instructions(&log_messages, &mut program_filtered_ixs);
365
366 return Ok(SerializableTransactionFixture {
367 instructions: program_filtered_ixs,
368 log_messages,
369 });
370 }
371
372 return Err("Invalid transaction encoding".into());
373 }
374
375 Err("Invalid transaction encoding".into())
376}
377
378#[macro_export]
379macro_rules! account_fixture {
380 ($pubkey:expr, $parser:expr) => {
381 match $crate::load_fixture($pubkey, $parser).await.unwrap() {
382 $crate::FixtureData::Account(a) => {
383 $crate::run_account_parse!($parser, a)
384 },
385 f @ _ => panic!("Invalid account fixture {f:?}"),
386 }
387 };
388}
389
390#[macro_export]
391macro_rules! tx_fixture {
392 ($sig:expr, $parser:expr) => {
393 match $crate::load_fixture($sig, $parser).await.unwrap() {
394 $crate::FixtureData::Instructions(fixture) => {
395 let futures = fixture.instructions.iter().map(|ix| {
396 let parser = $parser.clone();
397 async move { $crate::run_ix_parse!(parser, ix) }
398 });
399 $crate::futures::future::join_all(futures).await
400 },
401 f @ _ => panic!("Invalid transaction fixture {f:?}"),
402 }
403 };
404}
405#[macro_export]
406macro_rules! run_account_parse {
407 ($parser:expr, $account:expr) => {
408 $parser.parse(&$account).await.unwrap()
409 };
410}
411
412#[macro_export]
413macro_rules! run_ix_parse {
414 ($parser:expr, $ix:expr) => {
415 match $parser.parse(&$ix.into()).await {
416 Ok(v) => Some(v),
417
418 Err(yellowstone_vixen_core::ParseError::Filtered) => None,
420 Err(e) => panic!("parse error: {e:?}"),
421 }
422 };
423}
424
425pub async fn load_fixture<P: ProgramParser>(
426 fixture: &str,
427 parser: &P,
428) -> Result<FixtureData, Box<dyn std::error::Error>> {
429 maybe_create_fixture_dir()?;
430 let path = fixture_path(fixture)?;
431
432 if path.is_file() {
433 read_fixture(&path)
434 } else {
435 fetch_fixture(fixture, parser)
436 .await
437 .and_then(write_fixture(path))
438 }
439}
440
441fn convert_account_info(pubkey: Pubkey) -> impl Fn(Account) -> ClientResult<AccountInfo> {
442 move |value: Account| {
443 Ok(AccountInfo {
444 data: value.data.clone(),
445 executable: value.executable,
446 lamports: value.lamports,
447 owner: value.owner,
448 rent_epoch: value.rent_epoch,
449 space: value.data.len() as u64,
450 pubkey,
451 })
452 }
453}
454
455#[must_use]
456pub fn get_rpc_client() -> RpcClient {
457 let endpoint =
458 std::env::var("RPC_ENDPOINT").unwrap_or_else(|_| DEFAULT_RPC_ENDPOINT.to_string());
459
460 RpcClient::new(endpoint)
461}
462
463#[derive(Debug, Clone, Serialize, Deserialize)]
468pub struct SerializableTransactionFixture {
469 pub instructions: Vec<SerializableInstructionUpdate>,
470 #[serde(default)]
471 pub log_messages: Vec<String>,
472}
473
474#[derive(Debug, Clone)]
475pub enum FixtureData {
476 Account(SubscribeUpdateAccount),
477 Instructions(SerializableTransactionFixture),
478}
479
480async fn fetch_fixture<P: ProgramParser>(
481 fixture: &str,
482 parser: &P,
483) -> Result<FixtureData, Box<dyn std::error::Error>> {
484 let fixture_type = get_fixture_type(fixture);
485
486 match fixture_type {
487 FixtureType::Pubkey => {
488 let pubkey = Pubkey::from_str(fixture)?;
489 let rpc_client = get_rpc_client();
490
491 let account_info = rpc_client
492 .get_account(&pubkey)
493 .await
494 .and_then(convert_account_info(pubkey))?;
495
496 Ok(FixtureData::Account(SubscribeUpdateAccount::from(
497 account_info,
498 )))
499 },
500 FixtureType::Signature => {
501 let signature = Signature::from_str(fixture)?;
502 let rpc_client = get_rpc_client();
503
504 let params = json!([signature.to_string(), {
505 "encoding": "json",
506 "maxSupportedTransactionVersion": 0
507 }]);
508
509 let tx = rpc_client
510 .send(RpcRequest::GetTransaction, params)
511 .await
512 .map_err(|e| format!("Error fetching tx: {e:?}"))?;
513
514 let fixture = try_from_tx_meta(tx, parser)?;
515
516 Ok(FixtureData::Instructions(fixture))
517 },
518 FixtureType::Invalid => Err("Invalid fixture".into()),
519 }
520}
521
522fn write_fixture(
523 path: PathBuf,
524) -> impl Fn(FixtureData) -> Result<FixtureData, Box<dyn std::error::Error>> {
525 move |data: FixtureData| {
526 match data.clone() {
527 FixtureData::Account(account) => {
528 let writable = AccountInfo::try_from(account.clone())?;
529 let data = serde_json::to_string(&writable)?;
530
531 fs::write(&path, data)?;
532 },
533 FixtureData::Instructions(fixture) => {
534 let data = serde_json::to_string(&fixture)?;
535 fs::write(&path, data)?;
536 },
537 }
538 Ok(data)
539 }
540}
541
542fn maybe_create_fixture_dir() -> std::io::Result<()> {
543 let dir_exists = Path::new(FIXTURES_PATH).is_dir();
544
545 if dir_exists {
546 return Ok(());
547 }
548
549 std::fs::create_dir(FIXTURES_PATH)
550}
551
552#[derive(Debug, Clone, Copy)]
553pub enum FixtureType {
554 Pubkey,
555 Signature,
556 Invalid,
557}
558
559#[must_use]
560pub fn get_fixture_type(fixture: &str) -> FixtureType {
561 if regex::Regex::new(TX_SIGNATURE_REGEX)
562 .unwrap()
563 .is_match(fixture)
564 {
565 FixtureType::Signature
566 } else if regex::Regex::new(PUBKEY_REGEX).unwrap().is_match(fixture) {
567 FixtureType::Pubkey
568 } else {
569 FixtureType::Invalid
570 }
571}
572
573pub fn fixture_path(fixture: &str) -> Result<PathBuf, String> {
574 let mut file_name = fixture.to_string();
575 let fixture_type = get_fixture_type(fixture);
576 match fixture_type {
577 FixtureType::Pubkey => file_name.push_str("_account"),
578 FixtureType::Signature => file_name.push_str("_tx"),
579 FixtureType::Invalid => return Err("Invalid fixture".to_string()),
580 }
581 file_name.push_str(".json");
582
583 Ok(Path::new(FIXTURES_PATH).join(file_name))
584}
585
586pub fn read_account_fixture(data: &[u8]) -> Result<FixtureData, Box<dyn std::error::Error>> {
587 let account_info: AccountInfo = serde_json::from_slice(data)?;
588
589 Ok(FixtureData::Account(SubscribeUpdateAccount::from(
590 account_info,
591 )))
592}
593
594pub fn read_instructions_fixture(data: &[u8]) -> Result<FixtureData, Box<dyn std::error::Error>> {
595 let fixture: SerializableTransactionFixture =
598 serde_json::from_slice::<SerializableTransactionFixture>(data).or_else(|_| {
599 let instructions: Vec<SerializableInstructionUpdate> = serde_json::from_slice(data)?;
600
601 Ok::<_, serde_json::Error>(SerializableTransactionFixture {
602 instructions,
603 log_messages: vec![],
604 })
605 })?;
606
607 Ok(FixtureData::Instructions(fixture))
608}
609
610pub fn read_fixture(path: &Path) -> Result<FixtureData, Box<dyn std::error::Error>> {
611 let data = std::fs::read(path)?;
612
613 let fixture_type = get_fixture_type(
614 path.file_stem()
615 .ok_or("Invalid fixture path")?
616 .to_str()
617 .ok_or("Invalid fixture path")?
618 .split('_')
619 .next()
620 .ok_or("Invalid fixture path")?,
621 );
622
623 match fixture_type {
624 FixtureType::Pubkey => read_account_fixture(&data),
625 FixtureType::Signature => read_instructions_fixture(&data),
626 FixtureType::Invalid => Err("Invalid fixture".into()),
627 }
628}
629
630pub fn decode_bs58_to_bytes(bs58: &str) -> Result<Vec<u8>, String> {
631 let bytes = bs58::decode(bs58)
632 .into_vec()
633 .map_err(|e| format!("Error decoding bs58: {e:?}"))?;
634 Ok(bytes)
635}