├── .gitignore ├── src ├── commands │ ├── mod.rs │ ├── init.rs │ ├── status.rs │ └── var.rs ├── instrument │ ├── custom_types │ │ ├── mod.rs │ │ ├── structs.rs │ │ └── enums.rs │ ├── mod.rs │ ├── ast.rs │ ├── ast_custom_types.rs │ ├── source.rs │ ├── project.rs │ ├── ast_general.rs │ └── fixed_serialization.rs ├── compile │ ├── mod.rs │ ├── project.rs │ ├── correct_file.rs │ └── sbf_with_errors.rs ├── utils │ ├── mod.rs │ ├── debugee_project_info.rs │ ├── debugger_cache.rs │ └── program_input.rs ├── output │ ├── mod.rs │ ├── print_node.rs │ ├── generate.rs │ └── parse.rs └── main.rs ├── screenshot.png ├── inline-screenshot.png ├── Cargo.toml ├── README.md └── tutorial.md /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /.idea 3 | -------------------------------------------------------------------------------- /src/commands/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod init; 2 | pub mod var; 3 | pub mod status; -------------------------------------------------------------------------------- /src/instrument/custom_types/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod structs; 2 | pub mod enums; -------------------------------------------------------------------------------- /src/compile/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod project; 2 | pub mod sbf_with_errors; 3 | pub mod correct_file; -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Solana-Debugger/solana-debugger-cli/HEAD/screenshot.png -------------------------------------------------------------------------------- /src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod debugger_cache; 2 | pub mod program_input; 3 | pub mod debugee_project_info; -------------------------------------------------------------------------------- /inline-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Solana-Debugger/solana-debugger-cli/HEAD/inline-screenshot.png -------------------------------------------------------------------------------- /src/output/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod generate; 2 | pub mod parse; 3 | pub mod print_node; 4 | 5 | pub use generate::*; 6 | pub use parse::*; 7 | pub use print_node::*; 8 | -------------------------------------------------------------------------------- /src/commands/init.rs: -------------------------------------------------------------------------------- 1 | use crate::utils::debugger_cache::*; 2 | 3 | pub(crate) fn process_init(program_path: &str, input_path: &str) -> Result<(), Box> { 4 | 5 | ensure_cache_dir(); 6 | 7 | let config = DebuggerConfig::new_from_input(program_path, input_path)?; 8 | 9 | //dbg!(&config); 10 | 11 | config.write_to_file(&get_config_path())?; 12 | 13 | rm_target_dir(); 14 | 15 | Ok(()) 16 | } -------------------------------------------------------------------------------- /src/commands/status.rs: -------------------------------------------------------------------------------- 1 | use crate::utils::debugger_cache::*; 2 | 3 | pub(crate) fn process_status() -> Result<(), Box> { 4 | if !get_cache_dir().is_dir() { 5 | Err("Cache directory does not exist. Run 'init' to create it")? 6 | } 7 | let config: DebuggerConfig = DebuggerConfig::load_from_file(&get_config_path())?; 8 | config.validate()?; 9 | println!("{:#?}", config); 10 | Ok(()) 11 | } -------------------------------------------------------------------------------- /src/instrument/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod project; 2 | pub mod source; 3 | pub mod fixed_serialization; 4 | pub mod ast; 5 | pub mod ast_general; 6 | pub mod ast_custom_types; 7 | pub mod custom_types; 8 | 9 | pub use project::*; 10 | pub use source::*; 11 | pub use fixed_serialization::*; 12 | pub use ast::*; 13 | pub use ast_general::*; 14 | pub use ast_custom_types::*; 15 | 16 | pub fn is_hidden_path(path: &std::ffi::OsStr) -> bool { 17 | path.to_str().map_or(false, |s| s.starts_with('.')) 18 | } -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "solana-debugger-cli" 3 | version = "0.1.0" 4 | edition = "2021" 5 | description = "Solana Debugger CLI" 6 | authors = ["Maxim Schmidt "] 7 | 8 | [dependencies] 9 | clap = { version = "4.5.26" , features = ["cargo"]} 10 | solana-sdk = "=2.1.9" 11 | solana-program = "=2.1.9" 12 | solana-program-test = "=2.1.9" 13 | tokio = "1.43.0" 14 | log = "0.4.25" 15 | prettyplease = "0.2.29" 16 | quote = "1.0.38" 17 | syn = { version = "2.0.96", features = ["full", "parsing", "extra-traits", "visit", "visit-mut", "derive", "fold"] } 18 | proc-macro2 = { version = "=1.0.93", features = ["span-locations"] } 19 | cargo_metadata = "=0.19.1" 20 | base64 = "0.22.1" 21 | solana-account = "=2.1.9" 22 | serde_json = "1.0.135" 23 | serde = { version = "1.0.217", features = ["derive"] } 24 | solana-account-decoder-client-types = "=2.1.9" 25 | solana-rpc-client-api = "=2.1.9" 26 | colored = "3.0.0" 27 | dirs = "=6.0.0" 28 | -------------------------------------------------------------------------------- /src/instrument/ast.rs: -------------------------------------------------------------------------------- 1 | use syn::{parse_quote, File, Item}; 2 | use crate::instrument::{inst_ast_general, inst_ast_custom_types}; 3 | 4 | #[derive(Debug)] 5 | pub struct InstAstSpec { 6 | pub mod_fixed_serialization: bool, 7 | pub feature_min_specialization: bool, 8 | #[allow(dead_code)] 9 | pub debugee_file_path: String, 10 | pub line_inst: Option, 11 | pub custom_type_serialization: bool 12 | } 13 | 14 | pub fn inst_ast(mut input: File, spec: &InstAstSpec) -> File { 15 | if let Some(line) = &spec.line_inst { 16 | input = inst_ast_general(input, *line); 17 | } 18 | if spec.custom_type_serialization { 19 | input = inst_ast_custom_types(input); 20 | } 21 | if spec.mod_fixed_serialization { 22 | input.items.insert(0, Item::Mod(parse_quote! { 23 | mod _solana_debugger_serialize; 24 | })); 25 | } 26 | if spec.feature_min_specialization { 27 | input.attrs.insert(0, parse_quote! { 28 | #![feature(min_specialization)] 29 | }); 30 | } 31 | input 32 | } -------------------------------------------------------------------------------- /src/instrument/ast_custom_types.rs: -------------------------------------------------------------------------------- 1 | use syn::{File, Item, ItemMod}; 2 | use syn::fold::Fold; 3 | 4 | #[derive(Clone, Debug)] 5 | struct InstContext { 6 | } 7 | 8 | pub fn inst_ast_custom_types(file: File) -> File { 9 | let mut ctx = InstContext {}; 10 | ctx.fold_file(file) 11 | } 12 | 13 | impl Fold for InstContext { 14 | fn fold_file(&mut self, mut node: File) -> File { 15 | insert_serialize_impl(&mut node.items); 16 | syn::fold::fold_file(self, node) 17 | } 18 | 19 | fn fold_item_mod(&mut self, mut node: ItemMod) -> ItemMod { 20 | match &mut node.content { 21 | Some((_, items)) => { 22 | insert_serialize_impl(items) 23 | } 24 | None => {}, 25 | } 26 | syn::fold::fold_item_mod(self, node) 27 | } 28 | } 29 | 30 | fn insert_serialize_impl(items: &mut Vec) { 31 | let mut i: usize = 0; 32 | while i < items.len() { 33 | match &items[i] { 34 | Item::Struct(val) => { 35 | items.insert(i+1, syn::Item::Impl(crate::instrument::custom_types::structs::get_serialize_impl(val))); 36 | i += 2 37 | }, 38 | Item::Enum(val) => { 39 | items.insert(i+1, syn::Item::Impl(crate::instrument::custom_types::enums::get_serialize_impl(val))); 40 | i += 2 41 | }, 42 | _ => { 43 | i += 1 44 | } 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /src/compile/project.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::path::PathBuf; 3 | use crate::compile::correct_file::correct_file; 4 | use crate::compile::sbf_with_errors::{compile_sbf_with_errors, CompileError}; 5 | 6 | #[derive(Debug)] 7 | pub struct CompileProjectArgs { 8 | /// The path to the program to be compiled 9 | pub program_path: PathBuf, 10 | 11 | /// The path to the workspace of the program to be compiled (needed to find the files in which an error occurs) 12 | pub workspace_root: PathBuf, 13 | 14 | /// Custom `target` dir for cargo build 15 | pub target_dir: Option, 16 | } 17 | 18 | pub async fn compile_project(args: CompileProjectArgs) -> Result<(), Box> { 19 | let CompileProjectArgs { program_path, workspace_root, target_dir } = args; 20 | let target_dir = target_dir.as_ref().map(|dir| dir.as_path()); 21 | 22 | // Compile and correct approach 23 | // If the compiler returns an error, correct the respective files. Try to compile again. Do this until it compiles. 24 | loop { 25 | let compile_errors = compile_sbf_with_errors(&program_path, target_dir).await?; 26 | //dbg!(&compile_errors); 27 | 28 | if compile_errors.is_empty() { 29 | return Ok(()) 30 | } 31 | 32 | let files_map = files_to_errors(compile_errors); 33 | //dbg!(&files_map); 34 | 35 | for (file_path, errors) in files_map { 36 | let full_path = workspace_root.join(file_path); 37 | correct_file(&full_path, errors)?; 38 | } 39 | } 40 | } 41 | 42 | fn files_to_errors(errs: Vec) -> HashMap>{ 43 | let mut result: HashMap> = HashMap::new(); 44 | for err in errs { 45 | result.entry(err.file_path.clone()).or_default().push(err); 46 | } 47 | result 48 | } -------------------------------------------------------------------------------- /src/output/print_node.rs: -------------------------------------------------------------------------------- 1 | use colored::*; 2 | use crate::output::parse::{DebugNode, DebugNodeType}; 3 | 4 | const PRINT_MAX_CHILDREN: usize = 15; 5 | 6 | /* 7 | pub fn print_debug_node(node: &DebugNode, indent: usize) { 8 | let node_type = match node.node_type { 9 | DebugNodeType::Complex => "-", 10 | DebugNodeType::Primitive => "-", 11 | }; 12 | 13 | let indent_str = " ".repeat(indent); 14 | 15 | let value_str = match node.value.len() { 16 | 0 => "".to_string(), 17 | _ => format!("({})", node.value) 18 | }; 19 | 20 | println!("{}{} {}: {} {}", indent_str, node_type, node.name, node.full_type, value_str); 21 | //println!("{}{} {}: {}", indent_str, node_type, node.name, node.full_type); 22 | 23 | for child in node.children.iter() { 24 | print_debug_node(child, indent + 1); 25 | } 26 | } 27 | */ 28 | 29 | pub fn print_debug_node_colored(node: &DebugNode, indent: usize) { 30 | 31 | let node_type = match node.node_type { 32 | DebugNodeType::Complex => "▶".bright_blue(), 33 | DebugNodeType::Primitive => "•".green(), 34 | }; 35 | 36 | let indent_str = " ".repeat(indent); 37 | 38 | let name_str = node.name.bold().bright_yellow(); 39 | 40 | let type_str = match node.full_type.len() { 41 | 0 => "".to_string(), 42 | _ => format!("({})", node.full_type) 43 | }.italic().cyan(); 44 | 45 | let value_str = match node.value.len() { 46 | 0 => "".to_string(), 47 | _ => node.value.clone() 48 | }.bright_purple(); 49 | 50 | let gap_str = if value_str.len() > 0 { " ".to_string() } else { "".to_string() }; 51 | 52 | println!("{}{} {}: {}{}{}", indent_str, node_type, name_str, value_str, gap_str, type_str); 53 | 54 | for (i, child) in node.children.iter().enumerate() { 55 | if i < PRINT_MAX_CHILDREN { 56 | print_debug_node_colored(child, indent + 1); 57 | } else { 58 | let indent_str = " ".repeat(indent + 1); 59 | let node_type = "•".green(); 60 | println!("{}{} [...]", indent_str, node_type); 61 | break 62 | } 63 | } 64 | } -------------------------------------------------------------------------------- /src/instrument/custom_types/structs.rs: -------------------------------------------------------------------------------- 1 | use quote::quote; 2 | use syn::{parse_quote, ItemImpl, ItemStruct}; 3 | 4 | pub fn get_serialize_impl(node: &ItemStruct) -> ItemImpl { 5 | 6 | let name = &node.ident; 7 | let fields = &node.fields; 8 | 9 | let serialize_fields = match fields { 10 | syn::Fields::Named(fields_named) => { 11 | let field_statements = fields_named.named.iter().map(|field| { 12 | let field_name = field.ident.as_ref().unwrap(); 13 | let field_name_str = field_name.to_string(); 14 | quote! { 15 | crate::_solana_debugger_serialize::_SolanaDebuggerSerialize::_solana_debugger_serialize(&self.#field_name, #field_name_str); 16 | } 17 | }); 18 | quote! { 19 | #(#field_statements)* 20 | } 21 | }, 22 | syn::Fields::Unnamed(fields_unnamed) => { 23 | let field_statements = fields_unnamed.unnamed.iter().enumerate().map(|(i, _)| { 24 | let index = syn::Index::from(i); 25 | let index_str = format!("{}", i); 26 | quote! { 27 | crate::_solana_debugger_serialize::_SolanaDebuggerSerialize::_solana_debugger_serialize(&self.#index, #index_str); 28 | } 29 | }); 30 | quote! { 31 | #(#field_statements)* 32 | } 33 | }, 34 | syn::Fields::Unit => { 35 | quote! {} 36 | } 37 | }; 38 | 39 | parse_quote! { 40 | impl crate::_solana_debugger_serialize::_SolanaDebuggerSerialize for #name { 41 | fn _solana_debugger_serialize(&self, name: &str) { 42 | solana_program::log::sol_log("START_NODE"); 43 | solana_program::log::sol_log("complex"); 44 | solana_program::log::sol_log(name); 45 | solana_program::log::sol_log(std::any::type_name_of_val(self)); 46 | solana_program::log::sol_log("no_data"); 47 | 48 | #serialize_fields 49 | 50 | solana_program::log::sol_log("END_NODE"); 51 | } 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /src/utils/debugee_project_info.rs: -------------------------------------------------------------------------------- 1 | use std::path::{Path, PathBuf}; 2 | use cargo_metadata::{MetadataCommand, TargetKind}; 3 | 4 | #[derive(Debug)] 5 | pub struct DebugeeProjectInfo { 6 | pub program_path: PathBuf, 7 | pub workspace_root: PathBuf, 8 | pub is_workspace: bool, 9 | pub target_directory: PathBuf, 10 | pub target_name: String, 11 | } 12 | 13 | pub fn get_program_info(program_path: &Path) -> Result> { 14 | if !program_path.is_dir() { 15 | Err("program_path is not a directory")?; 16 | } 17 | 18 | // Run cargo metadata 19 | // This will create a Cargo.lock file if it doesn't exist yet (there's no option to disable this) 20 | let program_path = program_path.canonicalize()?; 21 | let manifest_path = program_path.join("Cargo.toml").canonicalize()?; 22 | let metadata = MetadataCommand::new() 23 | .manifest_path(&manifest_path) 24 | .exec()?; 25 | let program_package = metadata.packages.iter().find(|package| { 26 | PathBuf::from(&package.manifest_path) == manifest_path 27 | }).ok_or("Could not find debug program package in cargo metadata output")?; 28 | let find_target = program_package.targets.iter().find(|&t| 29 | t.kind.contains(&TargetKind::CDyLib) && t.kind.contains(&TargetKind::Lib) 30 | ); 31 | if find_target.is_none() { 32 | dbg!(&program_package.targets); 33 | Err(format!("Failed to find a cdylib + lib target in package {}", program_package.name))?; 34 | } 35 | let target = find_target.unwrap(); 36 | // For a single Cargo package, this will be it's root folder, i.e. it will be equal to program_path 37 | let workspace_root = PathBuf::from(metadata.workspace_root); 38 | let is_workspace = workspace_root != program_path; 39 | // For a workspace, this is usually $workspace_root/target 40 | let target_directory = PathBuf::from(metadata.target_directory); 41 | let target_name = target.name.clone(); 42 | 43 | Ok( 44 | DebugeeProjectInfo { 45 | program_path, 46 | workspace_root, 47 | is_workspace, 48 | target_directory, 49 | target_name, 50 | } 51 | ) 52 | } -------------------------------------------------------------------------------- /src/output/generate.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | use std::sync::{Arc, RwLock}; 3 | use solana_program_test::{find_file, ProgramTest}; 4 | use crate::utils::program_input::ProgramInput; 5 | 6 | struct OutputLogger { 7 | output: Arc>> 8 | } 9 | 10 | impl log::Log for OutputLogger { 11 | fn enabled(&self, metadata: &log::Metadata) -> bool { 12 | metadata.level() == log::Level::Debug && metadata.target() == "solana_runtime::message_processor::stable_log" 13 | } 14 | 15 | fn log(&self, record: &log::Record) { 16 | if self.enabled(record.metadata()) { 17 | self.output.write().unwrap().push(format!("{}", record.args())); 18 | } 19 | } 20 | 21 | fn flush(&self) {} 22 | } 23 | 24 | pub fn set_output_logger() -> Result>>, Box> { 25 | let output_logger = OutputLogger { output: Arc::new(RwLock::new(Vec::new())) }; 26 | let output_clone = Arc::clone(&output_logger.output); 27 | let logger = Box::new(output_logger); 28 | log::set_boxed_logger(logger)?; 29 | log::set_max_level(log::LevelFilter::Debug); 30 | 31 | Ok(output_clone) 32 | } 33 | 34 | pub async fn generate_program_output( 35 | program_dir: &Path, 36 | program_name: &str, 37 | input: ProgramInput, 38 | output_log: Arc>> 39 | ) -> Result, Box> { 40 | 41 | std::env::set_var("BPF_OUT_DIR", program_dir.to_str().unwrap()); 42 | let program_so_filename = format!("{program_name}.so"); 43 | if !find_file(&program_so_filename).is_some() { 44 | Err(format!("No shared object {program_so_filename} found"))?; 45 | } 46 | 47 | let mut program_test = ProgramTest::default(); 48 | program_test.set_compute_max_units(std::i64::MAX as u64); 49 | 50 | let program_name_static: &'static str = program_name.to_string().leak(); 51 | program_test.add_program(program_name_static, input.program_id, None); 52 | for (pubkey, account) in input.accounts { 53 | program_test.add_account(pubkey, account); 54 | } 55 | 56 | let (banks_client, _payer, recent_blockhash) = program_test.start().await; 57 | let mut transaction = input.transaction; 58 | transaction.sign(&input.keypairs, recent_blockhash); 59 | let _tx_result = banks_client.process_transaction(transaction).await?; 60 | //dbg!(&tx_result); 61 | 62 | let output = output_log.read().unwrap().clone(); 63 | Ok(output) 64 | } -------------------------------------------------------------------------------- /src/utils/debugger_cache.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Debug; 2 | use std::fs; 3 | use std::fs::File; 4 | use std::io::{BufReader, BufWriter}; 5 | use std::path::{Path, PathBuf}; 6 | use serde::{Deserialize, Serialize}; 7 | 8 | /// Represents `~/.cache/solana_debugger/config.json` 9 | #[derive(Debug, Serialize, Deserialize)] 10 | pub struct DebuggerConfig { 11 | /// May panic on non-UTF-8 characters 12 | pub program_path: PathBuf, 13 | pub input_path: PathBuf 14 | } 15 | 16 | impl DebuggerConfig { 17 | 18 | /// Try to create a new config from (possibly relative) paths 19 | pub fn new_from_input(program_path: &str, input_path: &str) -> Result> { 20 | 21 | if ! PathBuf::from(&program_path).is_dir() { 22 | Err(format!("program_path is not a directory: {}", program_path))?; 23 | } 24 | if ! PathBuf::from(&input_path).is_dir() { 25 | Err(format!("input_path is not a directory: {}", input_path))?; 26 | } 27 | 28 | let program_path: PathBuf = fs::canonicalize(program_path)?; 29 | let input_path: PathBuf = fs::canonicalize(input_path)?; 30 | 31 | Ok( 32 | DebuggerConfig { 33 | program_path, 34 | input_path, 35 | } 36 | ) 37 | } 38 | 39 | pub fn validate(&self) -> Result<(), String> { 40 | if ! self.program_path.is_dir() { 41 | Err(format!("program_path is not a directory: {}", self.program_path.display()))?; 42 | } 43 | if ! self.input_path.is_dir() { 44 | Err(format!("input_path is not a directory: {}", self.input_path.display()))?; 45 | } 46 | Ok(()) 47 | } 48 | 49 | pub fn write_to_file(&self, path: &Path) -> std::io::Result<()> { 50 | let file = File::create(path)?; 51 | let writer = BufWriter::new(file); 52 | serde_json::to_writer_pretty(writer, self)?; 53 | Ok(()) 54 | } 55 | 56 | pub fn load_from_file(path: &Path) -> std::io::Result { 57 | let file = File::open(path)?; 58 | let reader = BufReader::new(file); 59 | let config = serde_json::from_reader(reader)?; 60 | Ok(config) 61 | } 62 | } 63 | 64 | /// Solana Debugger's cache directory is `~/.cache/solana_debugger` 65 | pub fn get_cache_dir() -> PathBuf { 66 | let home_dir = dirs::home_dir().expect("Could not find home directory"); 67 | home_dir.join(".cache").join("solana_debugger") 68 | } 69 | 70 | pub fn get_build_dir() -> PathBuf { 71 | get_cache_dir().join("build") 72 | } 73 | 74 | /// Custom target dir, outside of `build` 75 | pub fn get_target_dir() -> PathBuf { 76 | get_cache_dir().join("target") 77 | } 78 | 79 | pub fn get_target_so_dir() -> PathBuf { 80 | get_target_dir().join("sbf-solana-solana").join("release") 81 | } 82 | 83 | pub fn rm_target_dir() { 84 | let target_dir = get_target_dir(); 85 | if target_dir.is_dir() { 86 | fs::remove_dir_all(get_target_dir()).unwrap() 87 | } 88 | } 89 | 90 | pub fn get_config_path() -> PathBuf { 91 | get_cache_dir().join("config.json") 92 | } 93 | 94 | pub fn ensure_cache_dir() { 95 | let cache_dir = get_cache_dir(); 96 | fs::create_dir_all(&cache_dir).expect("Failed to create cache dir"); 97 | } -------------------------------------------------------------------------------- /src/instrument/source.rs: -------------------------------------------------------------------------------- 1 | use std::collections::VecDeque; 2 | use std::fs; 3 | use std::fs::File; 4 | use std::io::Write; 5 | use std::path::{Path, PathBuf}; 6 | use crate::instrument::*; 7 | 8 | #[allow(irrefutable_let_patterns)] 9 | pub fn inst_source(input_path: &Path, output_path: &Path, inst_spec: &InstProjectSpec) -> Result<(), Box> { 10 | if let InstProjectSpec::SingleLine { .. } = inst_spec { 11 | write_fixed_serialization_file(&output_path.join("_solana_debugger_serialize.rs"))?; 12 | } 13 | 14 | let mut queue = VecDeque::<(PathBuf, PathBuf)>::new(); 15 | queue.push_back((input_path.into(), output_path.into())); 16 | 17 | while let Some((input_dir, output_dir)) = queue.pop_front() { 18 | for entry in fs::read_dir(&input_dir)? { 19 | let entry = entry?; 20 | let path = entry.path(); 21 | let file_name = path.file_name().unwrap(); 22 | 23 | if is_hidden_path(file_name) { 24 | continue; 25 | } 26 | 27 | if path.is_dir() { 28 | let new_output_dir = output_dir.join(file_name); 29 | fs::create_dir(&new_output_dir)?; 30 | queue.push_back((path, new_output_dir)); 31 | } else if path.is_file() { 32 | let new_output_file = output_dir.join(file_name); 33 | 34 | if let InstProjectSpec::SingleLine { file, line } = &inst_spec { 35 | 36 | // TODO: path should be dynamically obtained 37 | let is_main_module = new_output_file.ends_with("src/lib.rs"); 38 | 39 | // TODO: should be something like "src/lib.rs" 40 | let file_path_str = "".to_string(); 41 | 42 | let line_inst = if path == file.to_owned() { Some(*line) } else { None }; 43 | 44 | let ast_spec = InstAstSpec { 45 | mod_fixed_serialization: is_main_module, 46 | feature_min_specialization: is_main_module, 47 | debugee_file_path: file_path_str, 48 | line_inst, 49 | custom_type_serialization: true 50 | }; 51 | 52 | inst_source_file(&path, &new_output_file, &ast_spec)?; 53 | } 54 | } 55 | } 56 | } 57 | 58 | Ok(()) 59 | } 60 | 61 | fn inst_source_file(input_file_path: &Path, output_file_path: &Path, spec: &InstAstSpec) -> Result<(), Box> { 62 | //eprintln!("Process {}", input_file_path.display()); 63 | let input_file_contents = fs::read_to_string(input_file_path)?; 64 | let input_ast = syn::parse_file(&input_file_contents)?; 65 | let output_ast = inst_ast(input_ast, spec); 66 | let output_file_contents = prettyplease::unparse(&output_ast); 67 | 68 | //eprintln!("Write {}", output_file_path.display()); 69 | let mut output_file = File::create(output_file_path)?; 70 | output_file.write_all(output_file_contents.as_bytes())?; 71 | 72 | Ok(()) 73 | } 74 | 75 | fn write_fixed_serialization_file(path: &Path) -> Result<(), Box> { 76 | let mut output_file = File::create(&path)?; 77 | let trait_code = crate::instrument::get_fixed_serialization(); 78 | let contents = prettyplease::unparse(&trait_code); 79 | output_file.write_all(contents.as_bytes())?; 80 | Ok(()) 81 | } 82 | -------------------------------------------------------------------------------- /src/compile/correct_file.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | use std::fs::File; 3 | use std::io::Write; 4 | use std::path::Path; 5 | use proc_macro2::TokenStream; 6 | use quote::quote; 7 | use syn::{Expr, ExprPath, Item, Stmt}; 8 | use syn::spanned::Spanned; 9 | use syn::visit_mut::VisitMut; 10 | use crate::compile::sbf_with_errors::CompileError; 11 | 12 | struct CorrectContext { 13 | errors: Vec, 14 | } 15 | 16 | pub fn correct_file(path: &Path, errors: Vec) -> Result<(), Box> { 17 | if errors.is_empty() { 18 | unreachable!(); 19 | } 20 | 21 | //eprintln!("Process file: {}", path.display()); 22 | //eprintln!("Error length: {}", errors.len()); 23 | 24 | let input = fs::read_to_string(path)?; 25 | let mut ast = syn::parse_file(&input)?; 26 | 27 | let mut ctx = CorrectContext { errors }; 28 | 29 | ctx.visit_file_mut(&mut ast); 30 | 31 | if !ctx.errors.is_empty() { 32 | eprintln!("Some errors were not corrected:"); 33 | dbg!(&ctx.errors); 34 | Err("Unrecoverable compile error")? 35 | } 36 | 37 | let output = prettyplease::unparse(&ast); 38 | //eprintln!("Write file: {}", path.display()); 39 | let mut output_file = File::create(path)?; 40 | output_file.write_all(output.as_bytes())?; 41 | 42 | Ok(()) 43 | } 44 | 45 | impl VisitMut for CorrectContext { 46 | fn visit_stmt_mut(&mut self, stmt: &mut Stmt) { 47 | if let Stmt::Expr(expr, _) = stmt { 48 | if is_solana_debugger_serialize_call(expr) { 49 | //dbg!(&expr); 50 | let stmt_span = stmt.span(); 51 | let mut err_cov = vec![]; 52 | let mut err_uncov = vec![]; 53 | for err in self.errors.clone() { 54 | let cov_source_span = err.source_spans.iter().find(|source_span| 55 | stmt_span.byte_range().contains(&source_span.start) && 56 | stmt_span.byte_range().contains(&source_span.end) 57 | ); 58 | if cov_source_span.is_some() { 59 | err_cov.push(err); 60 | } else { 61 | err_uncov.push(err); 62 | } 63 | } 64 | 65 | if !err_cov.is_empty() { 66 | /* 67 | eprintln!("Remove serialize statement at line {}", stmt_span.start().line); 68 | eprintln!("{}", quote!(#stmt)); 69 | for err in err_cov { 70 | eprintln!("{}", err.error_message); 71 | } 72 | */ 73 | *stmt = Stmt::Item(Item::Verbatim(TokenStream::new())); 74 | } 75 | self.errors = err_uncov 76 | } 77 | } 78 | syn::visit_mut::visit_stmt_mut(self, stmt); 79 | } 80 | } 81 | 82 | fn is_solana_debugger_serialize_call(expr: &Expr) -> bool { 83 | let serialize_path: ExprPath = syn::parse2::( 84 | quote!(crate::_solana_debugger_serialize::_SolanaDebuggerSerialize::_solana_debugger_serialize) 85 | ).unwrap(); 86 | 87 | match expr { 88 | Expr::Call(call) => { 89 | match &*call.func { 90 | Expr::Path(path) if path.eq(&serialize_path) => true, 91 | _ => false 92 | } 93 | } 94 | _ => false 95 | } 96 | } -------------------------------------------------------------------------------- /src/compile/sbf_with_errors.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Range; 2 | use std::path::Path; 3 | use std::process::{Command, Stdio}; 4 | use cargo_metadata::diagnostic::DiagnosticLevel; 5 | use cargo_metadata::Message; 6 | 7 | #[derive(Debug, Clone)] 8 | pub struct CompileError { 9 | pub file_path: String, 10 | #[allow(dead_code)] 11 | pub error_code: String, 12 | pub source_spans: Vec>, 13 | #[allow(dead_code)] 14 | pub error_message: String, 15 | } 16 | 17 | /// Try to compile to SBF, but expect compile errors 18 | /// The Ok value is Vec since compiling with errors is considered expected behavior in our case 19 | pub async fn compile_sbf_with_errors(program_path: &Path, target_dir: Option<&Path>) -> Result, Box> { 20 | 21 | //eprintln!("Compile SBF: {}", program_path.display()); 22 | 23 | // This is from cargo-build-sbf's `build_solana_package` 24 | let mut cargo_build_args = vec![ 25 | // select Solana toolchain 26 | "+solana", 27 | "build", 28 | // Do NOT remove this even if it's faster! 29 | // Without this, you get compiler warnings like that: "[...] The function call may cause undefined behavior during execution." 30 | "--release", 31 | "--target", 32 | "sbf-solana-solana" 33 | ]; 34 | 35 | // Make sure we only compile the single lib target 36 | //cargo_build_args.extend(["--lib"]); 37 | 38 | // Enable debug output 39 | cargo_build_args.extend(["--message-format", "json,json-diagnostic-short"]); 40 | 41 | if let Some(target_dir) = target_dir { 42 | cargo_build_args.extend(["--target-dir", target_dir.to_str().unwrap()]); 43 | } 44 | 45 | let mut command = Command::new("cargo") 46 | .args(&cargo_build_args) 47 | .current_dir(&program_path) 48 | .stdout(Stdio::piped()) 49 | .stderr(Stdio::null()) 50 | // Do NOT set this (it may be faster, but it causes compiler warnings) 51 | // .env("RUSTFLAGS", "-C opt-level=0") 52 | .spawn() 53 | .map_err(|err| format!("Failed to run cargo: {}", err))?; 54 | 55 | let reader = std::io::BufReader::new(command.stdout.take().unwrap()); 56 | 57 | let mut errs: Vec = vec![]; 58 | 59 | for message in cargo_metadata::Message::parse_stream(reader) { 60 | match message.unwrap() { 61 | Message::CompilerMessage(msg) => { 62 | let msg = msg.message; 63 | 64 | // Ignore warnings etc. 65 | if msg.level != DiagnosticLevel::Error { 66 | continue; 67 | } 68 | // This is usually something like "aborting due to ..." 69 | // We can ignore this 70 | if msg.code.is_none() { 71 | continue; 72 | } 73 | //dbg!(&msg); 74 | 75 | let error_code = msg.code.clone().unwrap().code; 76 | if msg.spans.is_empty() { 77 | Err("Cargo returned empty span")?; 78 | } 79 | let prim_span = msg.spans.iter().find(|x| x.is_primary).ok_or("No primary span found")?; 80 | let file_path = prim_span.file_name.clone(); 81 | let source_spans = msg.spans.iter().map(|x| x.byte_start as usize..x.byte_end as usize).collect::>(); 82 | let error_message = msg.rendered.unwrap_or("N/A".into()).trim().to_string(); 83 | 84 | errs.push(CompileError { 85 | file_path, 86 | error_code, 87 | source_spans, 88 | error_message 89 | }) 90 | } 91 | _ => {} 92 | } 93 | } 94 | 95 | let output = command.wait_with_output().map_err(|err| format!("Failed to get output of cargo: {}", err))?; 96 | //dbg!(&output); 97 | //dbg!(&errs); 98 | 99 | if !output.status.success() && errs.is_empty() { 100 | Err("Compilation failed, but cargo build didn't return compile errors")?; 101 | } 102 | 103 | Ok(errs) 104 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Solana Debugger CLI 2 | 3 | This is a CLI tool to debug Solana programs. 4 | 5 | It will support the following features: 6 | - Inspect the value of a variable at a breakpoint 7 | - Display the call stack 8 | - Evaluate arbitrary Rust expressions at specified locations 9 | - It can be used as debugger backend of another system (e.g. an IDE plugin) 10 | 11 | ## Demo 12 | 13 | ![Solana Debugger CLI screenshot](inline-screenshot.png) 14 | 15 | [Video](https://x.com/maximschmidt94/status/1914802590568562965) 16 | 17 | [Screenshot](screenshot.png) 18 | 19 | ## Quick Test 20 | 21 | If you just want to see that it works: 22 | 23 | ``` 24 | $ agave-install init 2.1.9 25 | $ mkdir tmp 26 | $ cd tmp 27 | $ git clone https://github.com/Solana-Debugger/solana-debugger-cli 28 | $ cd solana-debugger-cli 29 | $ cargo build 30 | $ cd .. 31 | $ git clone https://github.com/Solana-Debugger/delta-counter-program-example 32 | $ cd delta-counter-program-example 33 | # to force the installation of platform-tools (skip if not needed) 34 | $ cd delta-counter; cargo-build-sbf; cd .. 35 | $ ../solana-debugger-cli/target/debug/solana-debugger-cli init delta-counter debug_input/create_counter 36 | $ ../solana-debugger-cli/target/debug/solana-debugger-cli entrypoint.rs:18 37 | ``` 38 | 39 | This should print `program_id`, `accounts` and `instruction_data`. 40 | 41 | For details, see the [tutorial](tutorial.md). 42 | 43 | ## Tutorial 44 | 45 | For a demonstration of all features, check out the [tutorial](tutorial.md). 46 | 47 | It uses two example programs specifically made to be tested with Solana Debugger: 48 | 49 | * [Delta counter program](https://github.com/Solana-Debugger/delta-counter-program-example) 50 | * [Governance program](https://github.com/Solana-Debugger/governance-program-example) 51 | 52 | ## Status 53 | 54 | Current stage of development: pre-alpha 55 | 56 | This project is under active development. Only variable inspection works right now. 57 | 58 | ## Program input 59 | 60 | To run the debugger, you must specify the entire input to the Solana program (accounts, signers, transaction). 61 | 62 | We use a format that is compatible with other Solana tools and should be familiar to Solana devs. [Here is an example](https://github.com/Solana-Debugger/delta-counter-program-example/tree/main/debug_input/increase_counter_from_0_by_100). 63 | 64 | Since creating this input can be hard, we provide a method to [generate inputs from tests](https://github.com/Solana-Debugger/save-input). 65 | 66 | ## Installation 67 | 68 | ``` 69 | $ git clone https://github.com/Solana-Debugger/solana-debugger-cli 70 | $ cd solana-debugger-cli 71 | $ cargo build 72 | $ ln -s `realpath target/debug/solana-debugger-cli` ~/bin/solana-debugger 73 | ``` 74 | 75 | ## Usage 76 | 77 | Before you can start debugging, you need to initialize a debugger session: 78 | ``` 79 | $ solana-debugger init path_to_program program_input 80 | ``` 81 | 82 | To inspect variables, use this: 83 | ``` 84 | $ solana-debugger file_path:line [variable, ...] 85 | ``` 86 | 87 | `file_path` is relative to the program's `src` folder 88 | 89 | Example: 90 | ``` 91 | $ solana-debugger init token/program input/transfer_tokens 92 | $ solana-debugger lib.rs:33 var1 var2 93 | ``` 94 | 95 | ## Internals 96 | 97 | What's so cool about this debugger? 98 | 99 | Debuggers usually work by interrupting the execution of a program and allowing the user to inspect its memory via some source mapping like DWARF. This is not what we do here. 100 | 101 | Instead, we do this: We instrument the program in clever ways (i.e. add log statements), run it through the SVM, capture its output and present it to the user. Think of it as automated printf debugging. 102 | 103 | This means: 100% reliable outputs, you can set breakpoints at any line, you have access to any variable that you'd have access to in the Rust program, compiler optimization never gets in the way, you can get other traces like compute unit consumption, it can deal with frameworks that use code generation (Anchor!), CPIs can be debugged as you would expect. 104 | 105 | While this is an unconventional approach, it allows for robust and reliable source-level debugging. 106 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod commands; 2 | mod utils; 3 | mod instrument; 4 | mod compile; 5 | mod output; 6 | 7 | use clap::*; 8 | use crate::commands::var::VariableFilter; 9 | use crate::commands::var::VariableFilter::*; 10 | 11 | #[tokio::main] 12 | async fn main() -> Result<(), Box> { 13 | 14 | let mut cli = clap::Command::new(crate_name!()) 15 | .about(crate_description!()) 16 | .version(crate_version!()) 17 | .subcommand( 18 | Command::new("init") 19 | .about("Set a debug configuration") 20 | .arg(Arg::new("program_path") 21 | .help("Path to the Solana program to be debugged") 22 | .required(true)) 23 | .arg(Arg::new("input_path") 24 | .help("Path to a folder containing the input to the program") 25 | .required(true)) 26 | ) 27 | .subcommand( 28 | Command::new("status") 29 | .about("Show debug configuration") 30 | ) 31 | .subcommand( 32 | Command::new("var") 33 | .about("Inspect the value of variables") 34 | .arg(Arg::new("location") 35 | .help("Location to inspect. Format: FILE:LINE, e.g. lib.rs:33 (without `src/`)") 36 | .required(true)) 37 | .arg(Arg::new("variable_names") 38 | .help("Name of variables to inspect. Leave empty to show all") 39 | .required(false) 40 | .action(ArgAction::Append)) 41 | ); 42 | 43 | let processed_args = get_processed_args(); 44 | 45 | let matches = cli.try_get_matches_from_mut(processed_args).unwrap_or_else(|e| e.exit()); 46 | 47 | match matches.subcommand() { 48 | Some(("init", sub_m)) => subcommand_init(sub_m), 49 | Some(("status", sub_m)) => subcommand_status(sub_m), 50 | Some(("var", sub_m)) => subcommand_var(sub_m).await, 51 | _ => { 52 | eprintln!("Invalid subcommand. Help:"); 53 | eprintln!(); 54 | cli.print_help().unwrap(); 55 | std::process::exit(1) 56 | // Don't need to return Err 57 | } 58 | } 59 | } 60 | 61 | fn get_processed_args() -> Vec { 62 | let mut args: Vec = std::env::args().collect(); 63 | 64 | if args.len() > 1 && try_get_file_line_format(&args[1]).is_ok() { 65 | args.insert(1, "var".to_string()); 66 | } 67 | 68 | args 69 | } 70 | 71 | fn subcommand_init(matches: &ArgMatches) -> Result<(), Box> { 72 | let program_path = matches.get_one::("program_path").unwrap(); 73 | let input_path = matches.get_one::("input_path").unwrap(); 74 | 75 | commands::init::process_init(program_path, input_path)?; 76 | 77 | Ok(()) 78 | } 79 | 80 | fn subcommand_status(_matches: &ArgMatches) -> Result<(), Box> { 81 | commands::status::process_status()?; 82 | Ok(()) 83 | } 84 | 85 | async fn subcommand_var(matches: &ArgMatches) -> Result<(), Box> { 86 | let location_str = matches.get_one::("location").unwrap(); 87 | 88 | let (file_path, line_number) = try_get_file_line_format(&location_str)?; 89 | 90 | // We assume that the source files are stored in `src` 91 | // We prepend `src/` for convenience 92 | let file_path = "src/".to_string() + file_path.as_str(); 93 | 94 | let variable_filter = match matches.get_many::("variable_names") { 95 | None => VariableFilter::All, 96 | Some(v) => Select(v.map(|s| s.clone()).collect()) 97 | }; 98 | //dbg!(&variable_filter); 99 | 100 | commands::var::process_var(&file_path, line_number, variable_filter).await?; 101 | 102 | Ok(()) 103 | } 104 | 105 | fn try_get_file_line_format(input: &str) -> Result<(String, usize), String> { 106 | 107 | let split: Vec = input.rsplitn(2, ':').map(|v| v.to_string()).collect(); 108 | 109 | if split.len() != 2 { 110 | Err("Invalid format of location")?; 111 | } 112 | 113 | let line_number = split[0].parse::() 114 | .map_err(|_| format!("Invalid line number: {}", split[0]))?; 115 | 116 | let file_path = split[1].clone(); 117 | 118 | Ok((file_path, line_number)) 119 | } -------------------------------------------------------------------------------- /src/instrument/custom_types/enums.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use quote::quote; 3 | use syn::{parse_quote, ItemEnum, ItemImpl, Variant}; 4 | 5 | pub fn get_serialize_impl(node: &ItemEnum) -> ItemImpl { 6 | let name = &node.ident; 7 | 8 | let variant_str_arms = node.variants.iter().map(get_variant_str_arm); 9 | 10 | let variant_content_arms = node.variants.iter().map(get_variant_content_arm); 11 | 12 | parse_quote! { 13 | impl crate::_solana_debugger_serialize::_SolanaDebuggerSerialize for #name { 14 | fn _solana_debugger_serialize(&self, name: &str) { 15 | solana_program::log::sol_log("START_NODE"); 16 | solana_program::log::sol_log("complex"); 17 | solana_program::log::sol_log(name); 18 | solana_program::log::sol_log(std::any::type_name_of_val(self)); 19 | solana_program::log::sol_log("str_ident"); 20 | 21 | let variant_str = match self { 22 | #(#variant_str_arms),* 23 | }; 24 | solana_program::log::sol_log(variant_str); 25 | 26 | match self { 27 | #(#variant_content_arms)* 28 | } 29 | 30 | solana_program::log::sol_log("END_NODE"); 31 | } 32 | } 33 | } 34 | } 35 | 36 | /// Example 37 | /// 38 | /// ``` 39 | /// let variant_str = match self { 40 | /// Ok(_) => "Ok", 41 | /// Err(_) => "Err" 42 | /// }; 43 | /// ``` 44 | fn get_variant_str_arm(variant: &Variant) -> TokenStream { 45 | let variant_name = &variant.ident; 46 | let variant_name_str = &variant.ident.to_string(); 47 | 48 | match &variant.fields { 49 | syn::Fields::Named(_) => { 50 | quote! { 51 | Self::#variant_name { .. } => #variant_name_str 52 | } 53 | } 54 | syn::Fields::Unnamed(_) => { 55 | quote! { 56 | Self::#variant_name(..) => #variant_name_str 57 | } 58 | } 59 | syn::Fields::Unit => { 60 | quote! { 61 | Self::#variant_name => #variant_name_str 62 | } 63 | } 64 | } 65 | } 66 | 67 | /// Example 68 | /// 69 | /// ``` 70 | /// match self { 71 | /// Ok(v) => { 72 | /// crate::_solana_debugger_serialize::_SolanaDebuggerSerialize::_solana_debugger_serialize(&v, "0"); 73 | /// }, 74 | /// Err(v) => { 75 | /// crate::_solana_debugger_serialize::_SolanaDebuggerSerialize::_solana_debugger_serialize(&v, "0"); 76 | /// } 77 | /// } 78 | /// ``` 79 | 80 | fn get_variant_content_arm(variant: &Variant) -> TokenStream { 81 | let variant_name = &variant.ident; 82 | 83 | match &variant.fields { 84 | syn::Fields::Named(fields) => { 85 | 86 | let field_names = fields.named.iter().map(|field| &field.ident); 87 | 88 | let field_stmts = fields.named.iter().map(|field| { 89 | let field_name = field.ident.as_ref().unwrap(); 90 | let field_name_str = field_name.to_string(); 91 | quote! { 92 | crate::_solana_debugger_serialize::_SolanaDebuggerSerialize::_solana_debugger_serialize(&#field_name, #field_name_str); 93 | } 94 | }); 95 | 96 | quote! { 97 | Self::#variant_name { #(#field_names),* } => { 98 | #(#field_stmts)* 99 | } 100 | } 101 | } 102 | syn::Fields::Unnamed(fields) => { 103 | 104 | let field_names = (0..fields.unnamed.len()).map(|i| { 105 | syn::Ident::new(&format!("f{}", i), variant.ident.span()) 106 | }).collect::>(); 107 | 108 | let field_stmts = field_names.iter().enumerate().map(|(i, var)| { 109 | let index_str = format!("{}", i); 110 | quote! { 111 | crate::_solana_debugger_serialize::_SolanaDebuggerSerialize::_solana_debugger_serialize(&*#var, #index_str); 112 | } 113 | }); 114 | 115 | quote! { 116 | Self::#variant_name(#(#field_names),*) => { 117 | #(#field_stmts)* 118 | } 119 | } 120 | } 121 | syn::Fields::Unit => { 122 | quote! { 123 | Self::#variant_name => {} 124 | } 125 | } 126 | } 127 | } -------------------------------------------------------------------------------- /src/commands/var.rs: -------------------------------------------------------------------------------- 1 | use crate::compile::project::{compile_project, CompileProjectArgs}; 2 | use crate::utils::debugger_cache::*; 3 | use crate::utils::debugee_project_info::get_program_info; 4 | use crate::utils::program_input::*; 5 | use crate::instrument::*; 6 | use crate::output::*; 7 | 8 | #[derive(Debug)] 9 | pub enum VariableFilter { 10 | All, 11 | Select(Vec) 12 | } 13 | 14 | pub(crate) async fn process_var(location: &str, line: usize, variable_filter: VariableFilter) -> Result<(), Box> { 15 | 16 | // 17 | // Input Validation 18 | // 19 | 20 | if !get_cache_dir().is_dir() { 21 | Err("Cache directory does not exist. Run 'init' to create it")? 22 | } 23 | let config: DebuggerConfig = DebuggerConfig::load_from_file(&get_config_path())?; 24 | //dbg!(&config); 25 | config.validate()?; 26 | 27 | // Validate location 28 | let location_path = config.program_path.join(location); 29 | if !location_path.is_file() { 30 | Err(format!("Debug location {} does not exist", location_path.display()))? 31 | } 32 | 33 | // Must be set before load_input_from_folder 34 | let output_log = set_output_logger()?; 35 | 36 | let program_input = load_input_from_folder(&config.input_path).await?; 37 | //dbg!(&program_input); 38 | 39 | let debugee_project_info = get_program_info(&config.program_path)?; 40 | //dbg!(&debugee_project_info); 41 | 42 | // 43 | // Instrument 44 | // 45 | 46 | eprintln!("Instrument..."); 47 | 48 | let project_type = match debugee_project_info.is_workspace { 49 | false => InstInputProjectType::Package { program_path: debugee_project_info.program_path.clone() }, 50 | true => InstInputProjectType::Workspace { 51 | root_path: debugee_project_info.workspace_root.clone(), 52 | program_path: debugee_project_info.program_path.clone(), 53 | } 54 | }; 55 | 56 | let inst_args = InstProjectArgs { 57 | output_dir: get_build_dir(), 58 | input_project: InstInputProject { 59 | project_type, 60 | target_dir: debugee_project_info.target_directory.clone(), 61 | }, 62 | inst_spec: InstProjectSpec::SingleLine { file: location_path, line }, 63 | }; 64 | 65 | let inst_info = inst_project(inst_args)?; 66 | 67 | //dbg!(&inst_info); 68 | 69 | //return Ok(()); 70 | 71 | // 72 | // Compile 73 | // 74 | 75 | //rm_target_dir(); 76 | 77 | eprintln!("Compile..."); 78 | 79 | let compile_args = CompileProjectArgs { 80 | program_path: inst_info.program_path, 81 | workspace_root: inst_info.workspace_root, 82 | target_dir: Some(get_target_dir()) 83 | }; 84 | 85 | compile_project(compile_args).await?; 86 | 87 | // 88 | // Output 89 | // 90 | 91 | eprintln!("Output..."); 92 | 93 | let program_output = generate_program_output( 94 | &get_target_so_dir(), 95 | &debugee_project_info.target_name, 96 | program_input, 97 | output_log 98 | ).await?; 99 | 100 | //dbg!(&program_output); 101 | 102 | let line_vars = parse_program_output(program_output)?; 103 | 104 | if line_vars.is_empty() { 105 | eprintln!("No variables data (location was never hit)"); 106 | return Ok(()); 107 | } 108 | 109 | println!(); 110 | for (j, item) in line_vars.iter().enumerate() { 111 | if line_vars.len() > 1 { 112 | println!("{}:{} ({})", location, item.line_num, j + 1); 113 | println!(); 114 | } else { 115 | println!("{}:{}", location, item.line_num); 116 | println!(); 117 | } 118 | 119 | match &variable_filter { 120 | VariableFilter::All => { 121 | for (i, node) in item.nodes.iter().enumerate() { 122 | print_debug_node_colored(node, 0); 123 | if i < item.nodes.len() - 1 { 124 | println!(); 125 | } 126 | } 127 | } 128 | VariableFilter::Select(vars) => { 129 | let vars_len = vars.len(); 130 | for (i, var) in vars.iter().enumerate() { 131 | match item.nodes.iter().find(|&n| *var == n.name) { 132 | Some(node) => { 133 | print_debug_node_colored(node, 0); 134 | } 135 | None => { 136 | println!("Variable {} not available", var); 137 | } 138 | } 139 | if i < vars_len - 1 { 140 | println!(); 141 | } 142 | } 143 | } 144 | } 145 | 146 | if j < line_vars.len() - 1 { 147 | println!(); 148 | } 149 | } 150 | 151 | Ok(()) 152 | } -------------------------------------------------------------------------------- /tutorial.md: -------------------------------------------------------------------------------- 1 | # Tutorial 2 | 3 | ## Installation 4 | 5 | This was tested under Solana v2.1.9. You may need to change your version: 6 | ``` 7 | $ agave-install init 2.1.9 8 | ``` 9 | 10 | Ensure that you have the right `platform-tools` version installed. To verify, check that you get this output: 11 | ``` 12 | $ rustc +solana --version 13 | rustc 1.79.0-dev 14 | ``` 15 | 16 | If you don't get this output, you can force it to be installed like this: 17 | ``` 18 | $ git clone https://github.com/Solana-Debugger/delta-counter-program-example 19 | $ cd delta-counter-program-example/delta-counter 20 | $ cargo-build-sbf 21 | ``` 22 | 23 | Make sure that your system Rust is at least v1.84.0: 24 | ``` 25 | $ rustc --version 26 | rustc 1.84.0 (9fc6b4312 2025-01-07) 27 | ``` 28 | 29 | Install Solana Debugger CLI: 30 | ``` 31 | $ git clone https://github.com/Solana-Debugger/solana-debugger-cli 32 | $ cd solana-debugger-cli 33 | $ cargo build 34 | $ ln -s `realpath target/debug/solana-debugger-cli` ~/bin/solana-debugger 35 | ``` 36 | 37 | ## Delta counter program 38 | 39 | ### Setup 40 | 41 | Pull the example program: 42 | ``` 43 | $ git clone https://github.com/Solana-Debugger/delta-counter-program-example 44 | ``` 45 | 46 | Initialize the debugger with it: 47 | ``` 48 | $ cd delta-counter-program-example 49 | $ solana-debugger init delta-counter debug_input/create_counter 50 | ``` 51 | 52 | Verify that it works by displaying the entrypoint arguments (compiling may take a while): 53 | ``` 54 | $ solana-debugger entrypoint.rs:18 55 | ``` 56 | This should print `program_id`, `accounts` and `instruction_data`. 57 | 58 | ### Create counter 59 | 60 | Show a variable inside a function: 61 | ``` 62 | $ solana-debugger processor/process_create_counter.rs:42 counter_info 63 | ``` 64 | Note that `counter_info` is the PDA we will write to. Using the debugger's output, confirm that `is_signer` is `false` and `is_writable` is `true`, as you'd expect here. 65 | 66 | Show multiple variables: 67 | ``` 68 | $ solana-debugger processor/process_create_counter.rs:60 rent space rent_lamports 69 | ``` 70 | 71 | Show a custom struct: 72 | ``` 73 | $ solana-debugger processor/process_create_counter.rs:78 counter 74 | ``` 75 | `count` should be zero since that's the counter's initial value. 76 | 77 | ### Increase counter 78 | 79 | Now we will switch to a different program input. This time, we will debug an instruction that increases an existing counter's value: 80 | ``` 81 | $ solana-debugger init delta-counter debug_input/increase_counter_from_100_by_155 82 | ``` 83 | 84 | Show all available variables at the beginning of the `increase_counter` function (compiling may take a while) 85 | ``` 86 | $ solana-debugger processor/process_increase_counter.rs:28 87 | ``` 88 | Try to find `delta` among them! It should be `155` since we want to increase the counter by 155 89 | 90 | Show the counter struct before and after the increase: 91 | ``` 92 | $ solana-debugger processor/process_increase_counter.rs:55 counter 93 | $ solana-debugger processor/process_increase_counter.rs:63 counter 94 | ``` 95 | Verify that `count` goes from `100` to `255` 96 | 97 | ## Governance program 98 | 99 | ### Setup 100 | 101 | Pull the test program: 102 | ``` 103 | $ git clone https://github.com/Solana-Debugger/governance-program-example 104 | ``` 105 | 106 | Initialize the debugger with it: 107 | ``` 108 | $ cd governance-program-example 109 | $ solana-debugger init solana-program-library/governance/program debug_input/create_realm 110 | ``` 111 | 112 | Verify that it works (compiling may take a while): 113 | ``` 114 | $ solana-debugger processor/process_create_realm.rs:48 115 | ``` 116 | This should print various variables holding `AccountInfo`s, such as `realm_info`. 117 | 118 | Try to find `name: "Realm #0"` in this output! 119 | 120 | ### Custom types 121 | 122 | Print some crate-specific structs: 123 | ``` 124 | $ solana-debugger processor/process_create_realm.rs:116 realm_config_data 125 | $ solana-debugger processor/process_create_realm.rs:149 realm_data 126 | ``` 127 | 128 | ### Multiple hits 129 | 130 | Let's look at a line that will be executed multiple times. 131 | 132 | We will look at the function `create_and_serialize_account_signed` from `tools/spl_token.rs`. It's used to create new tokens (1 call = 1 new token). To create a new token, it executes two CPIs: one to make an account using the System Program and one to initialize it using the Token Program. 133 | 134 | Note that `process_create_realm` calls `create_and_serialize_account_signed` twice (i.e. it creates two tokens). What happens if we try to inspect it? 135 | 136 | Run this: 137 | ``` 138 | $ solana-debugger tools/spl_token.rs:80 create_account_instruction initialize_account_instruction 139 | ``` 140 | 141 | This should show two line hits. Each of them should print two variables (i.e. show both instructions). 142 | 143 | So, for each of the two tokens created, this command shows both instructions needed to do that. 144 | 145 | Note that the `program_id`s are indeed `11111111111111111111111111111111` and `TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA`. 146 | -------------------------------------------------------------------------------- /src/instrument/project.rs: -------------------------------------------------------------------------------- 1 | use std::collections::VecDeque; 2 | use std::fs; 3 | use std::path::{Path, PathBuf}; 4 | use crate::instrument::*; 5 | 6 | #[derive(Debug)] 7 | pub struct InstProjectArgs { 8 | /// The contents of this folder will be overwritten 9 | pub output_dir: PathBuf, 10 | 11 | /// Info on the project to be instrumented 12 | pub input_project: InstInputProject, 13 | 14 | /// Which kind of instrumentation to perform 15 | pub inst_spec: InstProjectSpec, 16 | } 17 | 18 | #[derive(Debug)] 19 | pub struct InstInputProject { 20 | pub project_type: InstInputProjectType, 21 | pub target_dir: PathBuf, 22 | } 23 | 24 | #[derive(Debug)] 25 | pub enum InstInputProjectType { 26 | Workspace { 27 | program_path: PathBuf, 28 | root_path: PathBuf, 29 | }, 30 | Package { 31 | program_path: PathBuf, 32 | }, 33 | } 34 | 35 | #[derive(Debug)] 36 | pub enum InstProjectSpec { 37 | SingleLine { 38 | file: PathBuf, 39 | line: usize, 40 | } 41 | } 42 | 43 | /// Information on the project that is the instrumented copy of the input project 44 | #[derive(Debug)] 45 | pub struct InstProjectInfo { 46 | pub program_path: PathBuf, 47 | pub workspace_root: PathBuf, 48 | #[allow(dead_code)] 49 | pub is_workspace: bool, 50 | } 51 | 52 | pub fn inst_project(args: InstProjectArgs) -> Result> { 53 | 54 | // Prepare output dir 55 | let output_dir = args.output_dir; 56 | if output_dir.exists() { 57 | fs::remove_dir_all(&output_dir).map_err(|_| "inst_project: Failed to remove output dir")?; 58 | } 59 | fs::create_dir_all(&output_dir).map_err(|_| "inst_project: Failed to create output dir")?; 60 | 61 | match args.input_project.project_type { 62 | InstInputProjectType::Package { program_path } => { 63 | inst_project_package(&program_path, &output_dir, &args.inst_spec)?; 64 | Ok( 65 | InstProjectInfo { 66 | program_path: output_dir.clone(), 67 | workspace_root: output_dir.clone(), 68 | is_workspace: false, 69 | } 70 | ) 71 | } 72 | InstInputProjectType::Workspace { program_path, root_path } => { 73 | 74 | if !program_path.starts_with(&root_path) { 75 | Err("inst_project: Invalid workspace root")?; 76 | } 77 | 78 | inst_project_workspace(&root_path, &output_dir, &program_path, &args.input_project.target_dir, &args.inst_spec)?; 79 | 80 | let relative_program_path = program_path.strip_prefix(&root_path).unwrap(); 81 | let output_program_path = output_dir.join(relative_program_path); 82 | 83 | Ok( 84 | InstProjectInfo { 85 | program_path: output_program_path, 86 | workspace_root: output_dir.clone(), 87 | is_workspace: true, 88 | } 89 | ) 90 | } 91 | } 92 | } 93 | 94 | fn inst_project_package(input_path: &Path, output_path: &Path, inst_spec: &InstProjectSpec) -> Result<(), Box> { 95 | let cargo_config_path = input_path.join("Cargo.toml"); 96 | if !cargo_config_path.exists() { 97 | Err("Cargo.toml not found")? 98 | } 99 | let cargo_config_output_path = output_path.join("Cargo.toml"); 100 | //eprintln!("Copy {} to {}", cargo_config_path.display(), cargo_config_output_path.display()); 101 | fs::copy(cargo_config_path, cargo_config_output_path)?; 102 | 103 | let cargo_lock_path = input_path.join("Cargo.lock"); 104 | if cargo_lock_path.exists() { 105 | let cargo_lock_output_path = output_path.join("Cargo.lock"); 106 | //eprintln!("Copy {} to {}", cargo_lock_path.display(), cargo_lock_output_path.display()); 107 | fs::copy(cargo_lock_path, cargo_lock_output_path)?; 108 | } 109 | 110 | let source_path = input_path.join("src"); 111 | if !source_path.is_dir() { 112 | Err(format!("Source directory {} doesn't exist", source_path.display()))?; 113 | } 114 | 115 | let source_path_out = output_path.join("src"); 116 | fs::create_dir(&source_path_out)?; 117 | 118 | inst_source(&source_path, &source_path_out, inst_spec)?; 119 | 120 | Ok(()) 121 | } 122 | 123 | fn inst_project_workspace( 124 | workspace_path: &Path, 125 | output_path: &Path, 126 | debugee_path: &Path, 127 | input_target_dir: &Path, 128 | inst_spec: &InstProjectSpec 129 | ) -> Result<(), Box> { 130 | 131 | let mut queue = VecDeque::<(PathBuf, PathBuf)>::new(); 132 | queue.push_back((workspace_path.into(), output_path.into())); 133 | 134 | while let Some((input_dir, output_dir)) = queue.pop_front() { 135 | for entry in fs::read_dir(&input_dir)? { 136 | let entry = entry?; 137 | let path = entry.path(); 138 | let file_name = path.file_name().unwrap(); 139 | 140 | // .git etc. 141 | if is_hidden_path(file_name) { 142 | continue; 143 | } 144 | 145 | if path.is_dir() { 146 | if path == input_target_dir { 147 | continue; 148 | } 149 | 150 | let new_output_dir = output_dir.join(file_name); 151 | fs::create_dir(&new_output_dir)?; 152 | 153 | if path != debugee_path { 154 | queue.push_back((path, new_output_dir)); 155 | } else { 156 | inst_project_package(&path, &new_output_dir, inst_spec)?; 157 | } 158 | } else if path.is_file() { 159 | let new_output_file = output_dir.join(file_name); 160 | fs::copy(&path, &new_output_file)?; 161 | } 162 | } 163 | } 164 | 165 | Ok(()) 166 | } -------------------------------------------------------------------------------- /src/utils/program_input.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | use std::path::Path; 3 | use std::str::FromStr; 4 | use serde::Deserialize; 5 | use solana_account::Account; 6 | use solana_program::instruction::{AccountMeta, Instruction}; 7 | use solana_program_test::ProgramTest; 8 | use solana_sdk::pubkey::{ParsePubkeyError, Pubkey}; 9 | use solana_sdk::signature::Keypair; 10 | use solana_sdk::signer::EncodableKey; 11 | use solana_sdk::transaction::Transaction; 12 | use solana_rpc_client_api::response::RpcKeyedAccount; 13 | 14 | #[derive(Debug)] 15 | pub struct ProgramInput { 16 | pub accounts: Vec<(Pubkey, Account)>, 17 | pub transaction: Transaction, 18 | pub keypairs: Vec, 19 | pub program_id: Pubkey, 20 | } 21 | 22 | #[derive(Deserialize, Debug)] 23 | struct TransactionInput { 24 | payer: String, 25 | instructions: Vec, 26 | } 27 | 28 | #[derive(Deserialize, Debug)] 29 | struct InstructionInput { 30 | program_id: String, 31 | accounts: Vec, 32 | data: Vec, 33 | } 34 | 35 | #[derive(Deserialize, Debug)] 36 | struct AccountMetaInput { 37 | pubkey: String, 38 | is_signer: bool, 39 | is_writable: bool, 40 | } 41 | 42 | impl TryFrom for AccountMeta { 43 | type Error = ParsePubkeyError; 44 | fn try_from(value: AccountMetaInput) -> Result { 45 | Ok( 46 | AccountMeta { 47 | pubkey: Pubkey::from_str(&value.pubkey)?, 48 | is_signer: value.is_signer, 49 | is_writable: value.is_writable, 50 | } 51 | ) 52 | } 53 | } 54 | 55 | impl TryFrom for Instruction { 56 | type Error = ParsePubkeyError; 57 | fn try_from(value: InstructionInput) -> Result { 58 | Ok( 59 | Instruction { 60 | program_id: Pubkey::from_str(&value.program_id)?, 61 | accounts: value.accounts.into_iter().map(|x| x.try_into()).collect::, _>>()?, 62 | data: value.data, 63 | } 64 | ) 65 | } 66 | } 67 | 68 | impl TryFrom for Transaction { 69 | type Error = ParsePubkeyError; 70 | fn try_from(value: TransactionInput) -> Result { 71 | let payer = Pubkey::from_str(&value.payer)?; 72 | let ixs = value.instructions.into_iter().map(|x| x.try_into()).collect::, _>>()?; 73 | 74 | Ok( 75 | Transaction::new_with_payer(&ixs, Some(&payer)) 76 | ) 77 | } 78 | } 79 | 80 | pub async fn load_input_from_folder(path: &Path) -> Result> { 81 | let accounts_dir = path.join("accounts"); 82 | let keypairs_dir = path.join("keypairs"); 83 | 84 | let accounts = parse_accounts(&accounts_dir)?; 85 | 86 | let keypairs = parse_keypairs(&keypairs_dir)?; 87 | 88 | let transaction_path = path.join("transaction.json"); 89 | let transaction = parse_transaction(&transaction_path)?; 90 | 91 | let program_id = get_debugee_id(&transaction).await.ok_or("No debugee program id found")?; 92 | 93 | Ok( 94 | ProgramInput { 95 | accounts, 96 | transaction, 97 | keypairs, 98 | program_id, 99 | } 100 | ) 101 | } 102 | 103 | fn parse_accounts(accounts_dir: &Path) -> Result, Box> { 104 | let mut accounts = Vec::new(); 105 | 106 | for entry in fs::read_dir(accounts_dir).map_err(|_| "Failed to read accounts directory")? { 107 | if let Ok(entry) = entry { 108 | let path = entry.path(); 109 | 110 | if !path.is_file() || path.extension().unwrap_or_default() != "json" { 111 | continue; 112 | } 113 | 114 | let file_contents = fs::read_to_string(path).map_err(|_| "Failed to read account file")?; 115 | 116 | let rpc_keyed_account = serde_json::from_str::(&file_contents).map_err(|_| "Failed to deserialize account file")?; 117 | 118 | let pubkey = Pubkey::from_str(&rpc_keyed_account.pubkey).unwrap(); 119 | let ui_account = rpc_keyed_account.account; 120 | 121 | accounts.push( 122 | (pubkey, Account { 123 | lamports: ui_account.lamports, 124 | data: ui_account.data.decode().unwrap(), 125 | owner: Pubkey::from_str(&ui_account.owner).unwrap(), 126 | executable: ui_account.executable, 127 | rent_epoch: ui_account.rent_epoch, 128 | })); 129 | } 130 | } 131 | 132 | Ok(accounts) 133 | } 134 | 135 | fn parse_keypairs(keypairs_dir: &Path) -> Result, Box> { 136 | let mut keypairs = Vec::new(); 137 | 138 | for entry in fs::read_dir(keypairs_dir).map_err(|_| "Failed to read keypairs directory")? { 139 | if let Ok(entry) = entry { 140 | let path = entry.path(); 141 | 142 | if !path.is_file() || path.extension().unwrap_or_default() != "json" { 143 | continue; 144 | } 145 | 146 | let keypair = Keypair::read_from_file(path).map_err(|_| "Failed to read keypair file")?; 147 | 148 | keypairs.push(keypair); 149 | } 150 | } 151 | 152 | Ok(keypairs) 153 | } 154 | 155 | fn parse_transaction(tx_path: &Path) -> Result> { 156 | let file_contents = fs::read_to_string(tx_path).map_err(|_| "Failed to read transaction file")?; 157 | 158 | let transaction_input: TransactionInput = serde_json::from_str(&file_contents).map_err(|_| "Failed to deserialize transaction file")?; 159 | 160 | Ok(transaction_input.try_into().map_err(|_| "Failed to parse public key")?) 161 | } 162 | 163 | async fn get_debugee_id(transaction: &Transaction) -> Option { 164 | let empty_program_test = ProgramTest::default(); 165 | let (empty_banks_client, _, _) = empty_program_test.start().await; 166 | 167 | for ix in transaction.message.instructions.iter() { 168 | let program_id = transaction.message.account_keys[ix.program_id_index as usize]; 169 | 170 | let get_acc = empty_banks_client.get_account(program_id.clone()).await.unwrap(); 171 | 172 | if get_acc.is_none() { 173 | return Some(program_id); 174 | } 175 | } 176 | None 177 | } -------------------------------------------------------------------------------- /src/output/parse.rs: -------------------------------------------------------------------------------- 1 | use std::collections::VecDeque; 2 | use std::fmt::Debug; 3 | use base64::Engine; 4 | use base64::engine::general_purpose; 5 | use solana_sdk::pubkey::Pubkey; 6 | 7 | #[derive(Debug)] 8 | pub enum DebugNodeType { 9 | Primitive, 10 | Complex, 11 | } 12 | 13 | #[derive(Debug)] 14 | pub struct DebugNode { 15 | pub node_type: DebugNodeType, 16 | pub name: String, 17 | pub full_type: String, 18 | pub value: String, 19 | pub children: Vec, 20 | } 21 | 22 | #[derive(Debug)] 23 | pub struct LineVars { 24 | pub line_num: usize, 25 | // Use a Vec to retain the order 26 | pub nodes: Vec, 27 | } 28 | 29 | #[derive(Debug, Clone)] 30 | struct OutputParseError(String); 31 | impl std::fmt::Display for OutputParseError { 32 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 33 | write!(f, "Failed to parse output: {}", self.0) 34 | } 35 | } 36 | impl std::error::Error for OutputParseError {} 37 | 38 | static BASE64_ENGINE: general_purpose::GeneralPurpose = general_purpose::STANDARD; 39 | 40 | pub fn parse_program_output(output: Vec) -> Result, Box> { 41 | let cleaned = clean_program_output(output); 42 | let mut result: Vec = Vec::new(); 43 | let mut it = cleaned.into_iter(); 44 | while let Some(line) = it.next() { 45 | if !line.starts_with("-.!;LINE_START") { 46 | continue; 47 | } 48 | let split: Vec<&str> = line.split(';').collect(); 49 | if split.len() != 3 { 50 | Err(OutputParseError(format!("Invalid line: {}", line)))? 51 | } 52 | let line_num: usize = split[2].parse()?; 53 | let mut line_block: VecDeque = VecDeque::new(); 54 | loop { 55 | match it.next() { 56 | Some(line_2) => { 57 | if line_2 == "-.!;LINE_END" { 58 | break; 59 | } 60 | line_block.push_back(line_2); 61 | } 62 | _ => Err(OutputParseError("LINE_END not found".into()))? 63 | } 64 | } 65 | let line_nodes = parse_line_vars_nodes(line_block)?; 66 | result.push(LineVars { 67 | line_num, 68 | nodes: line_nodes, 69 | }) 70 | } 71 | /* 72 | for r in &result { 73 | eprintln!("LineVars: line_num: {}, nodes length: {}", r.line_num, r.nodes.len()); 74 | } 75 | */ 76 | 77 | Ok(result) 78 | } 79 | 80 | fn clean_program_output(output: Vec) -> Vec { 81 | let mut result = vec![]; 82 | for line in output { 83 | if !(line.starts_with("Program log:") || line.starts_with("Program data:")) { 84 | continue; 85 | } 86 | let line = line.replacen("Program log: ", "", 1).replacen("Program data: ", "", 1); 87 | result.push(line); 88 | } 89 | result 90 | } 91 | 92 | fn parse_line_vars_nodes(mut input: VecDeque) -> Result, OutputParseError> { 93 | //dbg!(&input); 94 | let mut result = Vec::new(); 95 | while !input.is_empty() { 96 | result.push(consume_debug_node(&mut input)?); 97 | } 98 | Ok(result) 99 | } 100 | 101 | fn consume_debug_node(lines: &mut VecDeque) -> Result { 102 | match lines.pop_front() { 103 | None => Err(OutputParseError("Not enough lines".into()))?, 104 | Some(v) => { 105 | if v != "START_NODE" { 106 | Err(OutputParseError(format!("Invalid value: {}", v)))? 107 | } 108 | } 109 | } 110 | 111 | let node_type = lines.pop_front().ok_or(OutputParseError("Not enough lines".into()))?; 112 | let node_type = match node_type.as_str() { 113 | "complex" => DebugNodeType::Complex, 114 | "primitive" => DebugNodeType::Primitive, 115 | _ => Err(OutputParseError(format!("Invalid node type: {}", node_type)))?, 116 | }; 117 | let name = lines.pop_front().ok_or(OutputParseError("Not enough lines".into()))?; 118 | let full_type = lines.pop_front().ok_or(OutputParseError("Not enough lines".into()))?; 119 | let ser_type = lines.pop_front().ok_or(OutputParseError("Not enough lines".into()))?; 120 | 121 | let value = match ser_type.as_str() { 122 | "not_implemented" => "[not implemented]".to_string(), 123 | "int" => { 124 | let data_line = lines.pop_front().ok_or(OutputParseError("Not enough lines".into()))?; 125 | let decoded = BASE64_ENGINE.decode(data_line).map_err(|_| OutputParseError("Decode error".into()))?; 126 | let byte_arr: [u8; 16] = decoded.try_into().map_err(|_| OutputParseError("Decode error".into()))?; 127 | let integer = i128::from_le_bytes(byte_arr); 128 | integer.to_string() 129 | } 130 | "uint" => { 131 | let data_line = lines.pop_front().ok_or(OutputParseError("Not enough lines".into()))?; 132 | let decoded = BASE64_ENGINE.decode(data_line).map_err(|_| OutputParseError("Decode error".into()))?; 133 | let byte_arr: [u8; 16] = decoded.try_into().map_err(|_| OutputParseError("Decode error".into()))?; 134 | let integer = u128::from_le_bytes(byte_arr); 135 | integer.to_string() 136 | } 137 | "bool" => { 138 | let data_line = lines.pop_front().ok_or(OutputParseError("Not enough lines".into()))?; 139 | let decoded = BASE64_ENGINE.decode(data_line).map_err(|_| OutputParseError("Decode error".into()))?; 140 | if decoded.len() != 1 { 141 | Err(OutputParseError("Decode error: Invalid length".into()))? 142 | } 143 | let byte = decoded[0]; 144 | let bool_val: bool = byte == 1; 145 | bool_val.to_string() 146 | } 147 | "str" => { 148 | let data_line = lines.pop_front().ok_or(OutputParseError("Not enough lines".into()))?; 149 | format!(r#""{}""#, data_line) 150 | } 151 | "str_ident" => { 152 | let data_line = lines.pop_front().ok_or(OutputParseError("Not enough lines".into()))?; 153 | data_line 154 | } 155 | "error_str" => { 156 | let data_line = lines.pop_front().ok_or(OutputParseError("Not enough lines".into()))?; 157 | format!(r#"Error: {}"#, data_line) 158 | } 159 | "no_data" => { 160 | "".to_string() 161 | //"[empty]".to_string() 162 | } 163 | "rc_meta" => { 164 | let data_line = lines.pop_front().ok_or(OutputParseError("Not enough lines".into()))?; 165 | let decoded = BASE64_ENGINE.decode(data_line).map_err(|_| OutputParseError("Decode error".into()))?; 166 | let byte_arr: [u8; 16] = decoded.try_into().map_err(|_| OutputParseError("Decode error".into()))?; 167 | let integer = u128::from_le_bytes(byte_arr); 168 | let strong_count = integer.to_string(); 169 | 170 | let data_line = lines.pop_front().ok_or(OutputParseError("Not enough lines".into()))?; 171 | let decoded = BASE64_ENGINE.decode(data_line).map_err(|_| OutputParseError("Decode error".into()))?; 172 | let byte_arr: [u8; 16] = decoded.try_into().map_err(|_| OutputParseError("Decode error".into()))?; 173 | let integer = u128::from_le_bytes(byte_arr); 174 | let weak_count = integer.to_string(); 175 | 176 | format!("strong_count={}, weak_count={}", strong_count, weak_count) 177 | } 178 | "array_len" => { 179 | let data_line = lines.pop_front().ok_or(OutputParseError("Not enough lines".into()))?; 180 | let decoded = BASE64_ENGINE.decode(data_line).map_err(|_| OutputParseError("Decode error".into()))?; 181 | let byte_arr: [u8; 16] = decoded.try_into().map_err(|_| OutputParseError("Decode error".into()))?; 182 | let integer = u128::from_le_bytes(byte_arr); 183 | let len = integer.to_string(); 184 | 185 | format!("len={}", len) 186 | } 187 | "pubkey" => { 188 | let data_line = lines.pop_front().ok_or(OutputParseError("Not enough lines".into()))?; 189 | let decoded = BASE64_ENGINE.decode(data_line).map_err(|_| OutputParseError("Decode error".into()))?; 190 | let byte_arr: [u8; 32] = decoded.try_into().map_err(|_| OutputParseError("Decode error".into()))?; 191 | let pubkey = Pubkey::from(byte_arr); 192 | 193 | pubkey.to_string() 194 | } 195 | x => { 196 | Err(OutputParseError(format!("Unimplemented: {}", x)))? 197 | } 198 | }; 199 | 200 | let mut children = Vec::::new(); 201 | let mut inc_index = 0; 202 | while lines[0] == "START_NODE" { 203 | let mut child = consume_debug_node(lines)?; 204 | if child.name == "-inc-index" { 205 | child.name = inc_index.to_string(); 206 | inc_index += 1 207 | } 208 | children.push(child); 209 | } 210 | 211 | match lines.pop_front() { 212 | None => Err(OutputParseError("Not enough lines".into()))?, 213 | Some(v) => { 214 | if v != "END_NODE" { 215 | Err(OutputParseError(format!("Invalid value: {}", v)))? 216 | } 217 | } 218 | } 219 | 220 | let node = DebugNode { 221 | node_type, 222 | name, 223 | full_type, 224 | value, 225 | children, 226 | }; 227 | Ok(node) 228 | } 229 | -------------------------------------------------------------------------------- /src/instrument/ast_general.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | use proc_macro2::Ident; 3 | use quote::quote; 4 | use syn::fold::Fold; 5 | use syn::*; 6 | use syn::spanned::Spanned; 7 | 8 | #[derive(Clone, Debug)] 9 | struct InstContext { 10 | // We use a Vec instead of a HashSet to keep the order in which Idents are added. This makes debugging easier 11 | bindings: Vec, 12 | line: usize 13 | //file_path: String 14 | } 15 | 16 | /// TODO: should support execution path, a set of lines etc. 17 | pub fn inst_ast_general(file: File, line: usize) -> File { 18 | let mut ctx = InstContext { 19 | bindings: Vec::new(), 20 | line 21 | }; 22 | ctx.fold_file(file) 23 | } 24 | 25 | impl Fold for InstContext { 26 | fn fold_arm(&mut self, node: Arm) -> Arm 27 | { 28 | let mut ctx = self.clone(); 29 | // Get the bindings introduced by the arm's pattern 30 | match &node.pat { 31 | // Heuristic: 32 | // If the match arm only consists of a single Pat::Ident, this means it's likely a unit variant and not a new binding 33 | // So, we ignore it. 34 | Pat::Ident(_ident) => {}, 35 | _ => { 36 | ctx.bindings.extend(get_bindings_from_pat(&node.pat)); 37 | } 38 | } 39 | 40 | syn::fold::fold_arm(&mut ctx, node) 41 | } 42 | 43 | fn fold_block(&mut self, mut node: Block) -> Block { 44 | let mut stmts: Vec = vec![]; 45 | for stmt in node.stmts { 46 | let line_number = stmt.span().start().line; 47 | 48 | // Instrumentation statements that come before stmt (but only if we're at the right line) 49 | if line_number == self.line { 50 | let mut inst_stmts: Vec = vec![]; 51 | let line_start_str = format!("-.!;LINE_START;{}", line_number); 52 | inst_stmts.push(parse_quote! { 53 | solana_program::log::sol_log(#line_start_str); 54 | }); 55 | for ident in &self.bindings { 56 | let ident_str = ident.to_string(); 57 | let print_var: Stmt = parse_quote! { 58 | crate::_solana_debugger_serialize::_SolanaDebuggerSerialize::_solana_debugger_serialize(&#ident, #ident_str); 59 | }; 60 | inst_stmts.push(print_var); 61 | } 62 | inst_stmts.push(parse_quote! { 63 | solana_program::log::sol_log("-.!;LINE_END"); 64 | }); 65 | let inst_block = Block { 66 | brace_token: syn::token::Brace::default(), 67 | stmts: inst_stmts 68 | }; 69 | stmts.push(parse2::(quote!(#inst_block)).unwrap()); 70 | } 71 | 72 | // Get new local bindings introduced by this statement 73 | let in_scope_bindings = get_in_scope_bindings_from_stmt(&stmt); 74 | 75 | // self.clone() so that fold_stmt doesn't add its local bindings to ours 76 | stmts.push(self.clone().fold_stmt(stmt)); 77 | 78 | // Add the new bindings to print them before the next statements 79 | self.bindings.extend(in_scope_bindings); 80 | }; 81 | node.stmts = stmts; 82 | node 83 | } 84 | 85 | fn fold_expr_if(&mut self, mut node: ExprIf) -> ExprIf { 86 | let mut then_ctx = self.clone(); 87 | let mut else_ctx = self.clone(); 88 | 89 | if let Expr::Let(ref expr) = *node.cond { 90 | let let_bindings = get_bindings_from_pat(&*expr.pat); 91 | //dbg!(&let_bindings); 92 | then_ctx.bindings.extend(let_bindings); 93 | } 94 | 95 | let then_branch = node.then_branch.clone(); 96 | node.then_branch = parse_quote!({}); 97 | node = syn::fold::fold_expr_if(&mut else_ctx, node); 98 | node.then_branch = then_ctx.fold_block(then_branch); 99 | node 100 | } 101 | 102 | fn fold_impl_item_fn(&mut self, node: ImplItemFn) -> ImplItemFn 103 | { 104 | self.bindings = get_bindings_from_fn_sig(&node.sig); 105 | syn::fold::fold_impl_item_fn(self, node) 106 | 107 | /* 108 | let inst_block = inst_fn_block(ctx.fold_block(node.block), &node.sig.output, &node.sig.ident, &self.file_path); 109 | ImplItemFn { 110 | attrs: self.fold_attributes(node.attrs), 111 | vis: self.fold_visibility(node.vis), 112 | defaultness: node.defaultness, 113 | sig: self.fold_signature(node.sig), 114 | block: inst_block, 115 | } 116 | */ 117 | // Idea: first do fold_block, THEN add header and footer inst for the fn 118 | } 119 | 120 | /* 121 | We can skip this since the matchee doesn't introduce new bindings 122 | We can copy the context in the arms instead 123 | 124 | fn fold_expr_match(&mut self, mut node: ExprMatch) -> ExprMatch 125 | { 126 | node = syn::fold::fold_expr_match(self, node); 127 | node.arms = node.arms.into_iter().map(|arm| 128 | // Use one copy of self for each arm 129 | self.clone().fold_arm(arm) 130 | ).collect(); 131 | node 132 | } 133 | */ 134 | 135 | fn fold_item_fn(&mut self, node: ItemFn) -> ItemFn 136 | { 137 | self.bindings = get_bindings_from_fn_sig(&node.sig); 138 | syn::fold::fold_item_fn(self, node) 139 | 140 | /* 141 | let inst_block = inst_fn_block(ctx.fold_block(*node.block), &node.sig.output, &node.sig.ident, &self.file_path); 142 | ItemFn { 143 | attrs: self.fold_attributes(node.attrs), 144 | vis: self.fold_visibility(node.vis), 145 | sig: self.fold_signature(node.sig), 146 | block: Box::new(inst_block), 147 | } 148 | */ 149 | } 150 | } 151 | 152 | fn get_bindings_from_fn_sig(sig: &Signature) -> Vec { 153 | let mut bindings = Vec::new(); 154 | for arg in sig.inputs.iter() { 155 | match arg { 156 | FnArg::Receiver(receiver) => { 157 | bindings.push(receiver.self_token.into()); 158 | }, 159 | FnArg::Typed(PatType { pat, .. }) => { 160 | bindings.extend(get_bindings_from_pat(pat)); 161 | } 162 | } 163 | } 164 | bindings 165 | } 166 | 167 | fn get_bindings_from_pat(p: &Pat) -> Vec { 168 | let mut bindings = Vec::new(); 169 | match p { 170 | Pat::Ident(PatIdent { ident, .. }) => { 171 | bindings.push(ident.clone()); 172 | }, 173 | Pat::TupleStruct(PatTupleStruct { elems, .. }) => { 174 | for el in elems { 175 | bindings.extend(get_bindings_from_pat(el)); 176 | } 177 | }, 178 | Pat::Type(PatType { pat, .. }) => { 179 | bindings.extend(get_bindings_from_pat(&*pat)); 180 | }, 181 | Pat::Struct(PatStruct { fields, .. }) => { 182 | for field in fields { 183 | bindings.extend(get_bindings_from_pat(&field.pat)); 184 | } 185 | }, 186 | Pat::Tuple(PatTuple { elems, .. }) => { 187 | for el in elems { 188 | bindings.extend(get_bindings_from_pat(el)); 189 | } 190 | }, 191 | // TODO: other cases 192 | _ => {} 193 | } 194 | bindings 195 | 196 | /* 197 | match p { 198 | Pat::Ident(PatIdent { ident, subpat, .. }) => { 199 | bindings.push(ident.clone()); 200 | // Handle optional sub-pattern (occurs in `binding @ SUBPATTERN`) 201 | // Untested 202 | if let Some((_, subpat)) = subpat { 203 | bindings.extend(get_bindings_from_pat(&*subpat)); 204 | } 205 | }, 206 | Pat::TupleStruct(PatTupleStruct { elems, .. }) => { 207 | for el in elems { 208 | bindings.extend(get_bindings_from_pat(el)); 209 | } 210 | }, 211 | Pat::Type(PatType { pat, .. }) => { 212 | bindings.extend(get_bindings_from_pat(&*pat)); 213 | }, 214 | Pat::Const(_) => { 215 | // Const patterns don't bind variables 216 | }, 217 | Pat::Lit(_) => { 218 | // Literal patterns don't bind variables 219 | }, 220 | Pat::Macro(_) => { 221 | // Can't inspect inside a macro pattern 222 | }, 223 | Pat::Or(PatOr { cases, .. }) => { 224 | // For alternations like `a | b`, collect bindings from all cases 225 | for case in cases { 226 | bindings.extend(get_bindings_from_pat(case)); 227 | } 228 | }, 229 | Pat::Paren(PatParen { pat, .. }) => { 230 | bindings.extend(get_bindings_from_pat(&*pat)); 231 | }, 232 | Pat::Path(_) => { 233 | // Path patterns (like enum variants) don't bind variables 234 | }, 235 | Pat::Range(_) => { 236 | // Range patterns don't bind variables 237 | }, 238 | Pat::Reference(PatReference { pat, .. }) => { 239 | bindings.extend(get_bindings_from_pat(&*pat)); 240 | }, 241 | Pat::Rest(_) => { 242 | // Rest patterns (..) don't bind variables 243 | }, 244 | Pat::Slice(PatSlice { elems, .. }) => { 245 | for el in elems { 246 | bindings.extend(get_bindings_from_pat(el)); 247 | } 248 | }, 249 | Pat::Struct(PatStruct { fields, .. }) => { 250 | for field in fields { 251 | bindings.extend(get_bindings_from_pat(&field.pat)); 252 | } 253 | }, 254 | Pat::Tuple(PatTuple { elems, .. }) => { 255 | for el in elems { 256 | bindings.extend(get_bindings_from_pat(el)); 257 | } 258 | }, 259 | Pat::Verbatim(_) => { 260 | // Can't inspect inside verbatim tokens 261 | }, 262 | Pat::Wild(_) => { 263 | // Wildcard patterns (_) don't bind variables 264 | }, 265 | } 266 | */ 267 | } 268 | 269 | // TODO: include this in the inst 270 | #[allow(dead_code)] 271 | fn inst_fn_block(block: Block, return_type: &ReturnType, fn_name: &Ident, file_path: &str) -> Block { 272 | 273 | let var_type = match return_type { 274 | ReturnType::Default => parse_quote! { () }, 275 | ReturnType::Type(_, ty) => (**ty).clone(), 276 | }; 277 | 278 | let fn_name_str = fn_name.to_string(); 279 | 280 | parse_quote! {{ 281 | sol_log("-.!;FN_START"); 282 | sol_log(#fn_name_str); 283 | sol_log(#file_path); 284 | let ret: #var_type = #block; 285 | sol_log("-.!;FN_END"); 286 | ret 287 | }} 288 | 289 | /* 290 | // Doesn't work since it changes the function to -> () 291 | block.stmts.insert(0, parse_quote!( 292 | sol_log("-.!;FN_START"); 293 | )); 294 | 295 | block.stmts.push(parse_quote!( 296 | sol_log("-.!;FN_END"); 297 | )); 298 | 299 | block 300 | */ 301 | } 302 | 303 | 304 | /* 305 | TODO 306 | handle: if let Some(x) = y 307 | Expr::If 308 | */ 309 | 310 | /// Get new bindings introduced by stmt that are valid in its parent scope 311 | fn get_in_scope_bindings_from_stmt(stmt: &Stmt) -> HashSet { 312 | let mut bindings = HashSet::new(); 313 | match stmt { 314 | Stmt::Local(local) => { 315 | bindings.extend(get_bindings_from_pat(&local.pat)); 316 | } 317 | _ => {} 318 | } 319 | bindings 320 | } -------------------------------------------------------------------------------- /src/instrument/fixed_serialization.rs: -------------------------------------------------------------------------------- 1 | use syn::{parse_quote, File}; 2 | 3 | pub fn get_fixed_serialization() -> File { 4 | parse_quote! { 5 | 6 | use solana_program::log::{sol_log, sol_log_data}; 7 | use std::any::type_name_of_val; 8 | use std::cell::RefCell; 9 | use std::rc::Rc; 10 | use solana_program::account_info::AccountInfo; 11 | use solana_program::pubkey::Pubkey; 12 | 13 | pub trait _SolanaDebuggerSerialize { 14 | fn _solana_debugger_serialize(&self, name: &str); 15 | } 16 | 17 | impl _SolanaDebuggerSerialize for T { 18 | default fn _solana_debugger_serialize(&self, name: &str) { 19 | sol_log("START_NODE"); 20 | sol_log("complex"); 21 | sol_log(name); 22 | sol_log(type_name_of_val(self)); 23 | sol_log("not_implemented"); 24 | sol_log("END_NODE"); 25 | } 26 | } 27 | 28 | macro_rules! impl_serialize { 29 | ($type:ty, $is_complex:expr, $ser_type:expr, $data_ser:expr) => { 30 | impl _SolanaDebuggerSerialize for $type { 31 | fn _solana_debugger_serialize(&self, name: &str) { 32 | sol_log("START_NODE"); 33 | sol_log(if $is_complex { "complex" } else { "primitive" }); 34 | sol_log(name); 35 | sol_log(type_name_of_val(self)); 36 | sol_log($ser_type); 37 | ($data_ser)(self); 38 | sol_log("END_NODE"); 39 | } 40 | } 41 | } 42 | } 43 | 44 | macro_rules! impl_serialize_int { 45 | ($type:ty) => { 46 | impl_serialize!( 47 | $type, 48 | false, 49 | "int", 50 | |s: &$type| { 51 | let bytes = (*s as i128).to_le_bytes(); 52 | sol_log_data(&[bytes.as_slice()]); 53 | } 54 | ); 55 | } 56 | } 57 | 58 | impl_serialize_int!(i8); 59 | impl_serialize_int!(i16); 60 | impl_serialize_int!(i32); 61 | impl_serialize_int!(i64); 62 | impl_serialize_int!(i128); 63 | impl_serialize_int!(isize); 64 | 65 | macro_rules! impl_serialize_uint { 66 | ($type:ty) => { 67 | impl_serialize!( 68 | $type, 69 | false, 70 | "uint", 71 | |s: &$type| { 72 | let bytes = (*s as u128).to_le_bytes(); 73 | sol_log_data(&[bytes.as_slice()]); 74 | } 75 | ); 76 | } 77 | } 78 | 79 | impl_serialize_uint!(u8); 80 | impl_serialize_uint!(u16); 81 | impl_serialize_uint!(u32); 82 | impl_serialize_uint!(u64); 83 | impl_serialize_uint!(u128); 84 | impl_serialize_uint!(usize); 85 | 86 | impl _SolanaDebuggerSerialize for bool { 87 | fn _solana_debugger_serialize(&self, name: &str) { 88 | sol_log("START_NODE"); 89 | sol_log("primitive"); 90 | sol_log(name); 91 | sol_log(type_name_of_val(self)); 92 | sol_log("bool"); 93 | sol_log_data(&[&[*self as u8]]); 94 | sol_log("END_NODE"); 95 | } 96 | } 97 | 98 | impl _SolanaDebuggerSerialize for &str { 99 | fn _solana_debugger_serialize(&self, name: &str) { 100 | sol_log("START_NODE"); 101 | sol_log("primitive"); 102 | sol_log(name); 103 | sol_log(type_name_of_val(self)); 104 | sol_log("str"); 105 | sol_log(self); 106 | sol_log("END_NODE"); 107 | } 108 | } 109 | 110 | impl _SolanaDebuggerSerialize for String { 111 | fn _solana_debugger_serialize(&self, name: &str) { 112 | sol_log("START_NODE"); 113 | sol_log("primitive"); 114 | sol_log(name); 115 | sol_log(type_name_of_val(self)); 116 | sol_log("str"); 117 | sol_log(self.as_str()); 118 | sol_log("END_NODE"); 119 | } 120 | } 121 | 122 | impl _SolanaDebuggerSerialize for Option { 123 | fn _solana_debugger_serialize(&self, name: &str) { 124 | sol_log("START_NODE"); 125 | sol_log("complex"); 126 | sol_log(name); 127 | sol_log(type_name_of_val(self)); 128 | sol_log("str_ident"); 129 | let variant_str = match self { 130 | None => "None", 131 | Some(_) => "Some" 132 | }; 133 | sol_log(variant_str); 134 | if let Some(v) = self { 135 | v._solana_debugger_serialize("0"); 136 | } 137 | sol_log("END_NODE"); 138 | } 139 | } 140 | 141 | impl _SolanaDebuggerSerialize for Result { 142 | fn _solana_debugger_serialize(&self, name: &str) { 143 | sol_log("START_NODE"); 144 | sol_log("complex"); 145 | sol_log(name); 146 | sol_log(type_name_of_val(self)); 147 | sol_log("str_ident"); 148 | let variant_str = match self { 149 | Ok(_) => "Ok", 150 | Err(_) => "Err" 151 | }; 152 | sol_log(variant_str); 153 | match self { 154 | Ok(v) => { 155 | v._solana_debugger_serialize("0"); 156 | }, 157 | Err(v) => { 158 | v._solana_debugger_serialize("0"); 159 | } 160 | } 161 | sol_log("END_NODE"); 162 | } 163 | } 164 | 165 | impl _SolanaDebuggerSerialize for (T1, T2) { 166 | fn _solana_debugger_serialize(&self, name: &str) { 167 | sol_log("START_NODE"); 168 | sol_log("complex"); 169 | sol_log(name); 170 | sol_log(type_name_of_val(self)); 171 | sol_log("no_data"); 172 | self.0._solana_debugger_serialize("0"); 173 | self.1._solana_debugger_serialize("1"); 174 | sol_log("END_NODE"); 175 | } 176 | } 177 | 178 | impl _SolanaDebuggerSerialize for Box { 179 | fn _solana_debugger_serialize(&self, name: &str) { 180 | sol_log("START_NODE"); 181 | sol_log("complex"); 182 | sol_log(name); 183 | sol_log(type_name_of_val(self)); 184 | sol_log("no_data"); 185 | (**self)._solana_debugger_serialize("value"); 186 | sol_log("END_NODE"); 187 | } 188 | } 189 | 190 | impl _SolanaDebuggerSerialize for Rc { 191 | fn _solana_debugger_serialize(&self, name: &str) { 192 | sol_log("START_NODE"); 193 | sol_log("complex"); 194 | sol_log(name); 195 | sol_log(type_name_of_val(self)); 196 | sol_log("rc_meta"); 197 | let strong_count = (Rc::strong_count(self) as u128).to_le_bytes(); 198 | sol_log_data(&[strong_count.as_slice()]); 199 | let weak_count = (Rc::weak_count(self) as u128).to_le_bytes(); 200 | sol_log_data(&[weak_count.as_slice()]); 201 | (**self)._solana_debugger_serialize("value"); 202 | sol_log("END_NODE"); 203 | } 204 | } 205 | 206 | impl _SolanaDebuggerSerialize for RefCell { 207 | fn _solana_debugger_serialize(&self, name: &str) { 208 | sol_log("START_NODE"); 209 | sol_log("complex"); 210 | sol_log(name); 211 | sol_log(type_name_of_val(self)); 212 | 213 | if let Ok(v) = self.try_borrow() { 214 | sol_log("no_data"); 215 | (*v)._solana_debugger_serialize("value"); 216 | } else { 217 | sol_log("error_str"); 218 | sol_log("Failed to borrow"); 219 | } 220 | 221 | sol_log("END_NODE"); 222 | } 223 | } 224 | 225 | impl _SolanaDebuggerSerialize for Vec { 226 | fn _solana_debugger_serialize(&self, name: &str) { 227 | sol_log("START_NODE"); 228 | sol_log("complex"); 229 | sol_log(name); 230 | sol_log(type_name_of_val(self)); 231 | 232 | sol_log("array_len"); 233 | 234 | let len = (self.len() as u128).to_le_bytes(); 235 | sol_log_data(&[len.as_slice()]); 236 | 237 | for el in self { 238 | el._solana_debugger_serialize("-inc-index"); 239 | } 240 | 241 | sol_log("END_NODE"); 242 | } 243 | } 244 | 245 | impl _SolanaDebuggerSerialize for &[T] { 246 | fn _solana_debugger_serialize(&self, name: &str) { 247 | sol_log("START_NODE"); 248 | sol_log("complex"); 249 | sol_log(name); 250 | sol_log(type_name_of_val(self)); 251 | 252 | sol_log("array_len"); 253 | 254 | let len = (self.len() as u128).to_le_bytes(); 255 | sol_log_data(&[len.as_slice()]); 256 | 257 | for el in (*self).iter() { 258 | el._solana_debugger_serialize("-inc-index"); 259 | } 260 | 261 | sol_log("END_NODE"); 262 | } 263 | } 264 | 265 | impl _SolanaDebuggerSerialize for &mut [T] { 266 | fn _solana_debugger_serialize(&self, name: &str) { 267 | sol_log("START_NODE"); 268 | sol_log("complex"); 269 | sol_log(name); 270 | sol_log(type_name_of_val(self)); 271 | 272 | sol_log("array_len"); 273 | 274 | let len = (self.len() as u128).to_le_bytes(); 275 | sol_log_data(&[len.as_slice()]); 276 | 277 | for el in (*self).iter() { 278 | el._solana_debugger_serialize("-inc-index"); 279 | } 280 | 281 | sol_log("END_NODE"); 282 | } 283 | } 284 | impl _SolanaDebuggerSerialize for &T { 285 | fn _solana_debugger_serialize(&self, name: &str) { 286 | sol_log("START_NODE"); 287 | sol_log("complex"); 288 | sol_log(name); 289 | sol_log(type_name_of_val(self)); 290 | sol_log("no_data"); 291 | 292 | (**self)._solana_debugger_serialize("value"); 293 | 294 | sol_log("END_NODE"); 295 | } 296 | } 297 | 298 | impl _SolanaDebuggerSerialize for &mut T { 299 | fn _solana_debugger_serialize(&self, name: &str) { 300 | sol_log("START_NODE"); 301 | sol_log("complex"); 302 | sol_log(name); 303 | sol_log(type_name_of_val(self)); 304 | sol_log("no_data"); 305 | 306 | (**self)._solana_debugger_serialize("value"); 307 | 308 | sol_log("END_NODE"); 309 | } 310 | } 311 | 312 | impl<'a, T> _SolanaDebuggerSerialize for core::cell::Ref<'a, T> { 313 | fn _solana_debugger_serialize(&self, name: &str) { 314 | sol_log("START_NODE"); 315 | sol_log("complex"); 316 | sol_log(name); 317 | sol_log(type_name_of_val(self)); 318 | sol_log("no_data"); 319 | 320 | crate::_solana_debugger_serialize::_SolanaDebuggerSerialize::_solana_debugger_serialize(&**self, "value"); 321 | 322 | sol_log("END_NODE"); 323 | } 324 | } 325 | 326 | impl<'a, T> _SolanaDebuggerSerialize for core::cell::RefMut<'a, T> { 327 | fn _solana_debugger_serialize(&self, name: &str) { 328 | sol_log("START_NODE"); 329 | sol_log("complex"); 330 | sol_log(name); 331 | sol_log(type_name_of_val(self)); 332 | sol_log("no_data"); 333 | 334 | crate::_solana_debugger_serialize::_SolanaDebuggerSerialize::_solana_debugger_serialize(&**self, "value"); 335 | 336 | sol_log("END_NODE"); 337 | } 338 | } 339 | 340 | impl _SolanaDebuggerSerialize for [T; N] { 341 | fn _solana_debugger_serialize(&self, name: &str) { 342 | sol_log("START_NODE"); 343 | sol_log("complex"); 344 | sol_log(name); 345 | sol_log(type_name_of_val(self)); 346 | 347 | sol_log("array_len"); 348 | 349 | let len = (N as u128).to_le_bytes(); 350 | sol_log_data(&[len.as_slice()]); 351 | 352 | for el in self { 353 | el._solana_debugger_serialize("-inc-index"); 354 | } 355 | 356 | sol_log("END_NODE"); 357 | } 358 | } 359 | 360 | impl<'a> _SolanaDebuggerSerialize for AccountInfo<'a> { 361 | fn _solana_debugger_serialize(&self, name: &str) { 362 | sol_log("START_NODE"); 363 | sol_log("complex"); 364 | sol_log(name); 365 | sol_log(type_name_of_val(self)); 366 | sol_log("no_data"); 367 | 368 | self.key._solana_debugger_serialize("key"); 369 | self.lamports._solana_debugger_serialize("lamports"); 370 | self.data._solana_debugger_serialize("data"); 371 | self.owner._solana_debugger_serialize("owner"); 372 | self.rent_epoch._solana_debugger_serialize("rent_epoch"); 373 | self.is_signer._solana_debugger_serialize("is_signer"); 374 | self.is_writable._solana_debugger_serialize("is_writable"); 375 | self.executable._solana_debugger_serialize("executable"); 376 | 377 | sol_log("END_NODE"); 378 | } 379 | } 380 | 381 | impl _SolanaDebuggerSerialize for Pubkey { 382 | fn _solana_debugger_serialize(&self, name: &str) { 383 | sol_log("START_NODE"); 384 | sol_log("complex"); 385 | sol_log(name); 386 | sol_log(type_name_of_val(self)); 387 | 388 | sol_log("pubkey"); 389 | 390 | sol_log_data(&[self.as_ref()]); 391 | 392 | sol_log("END_NODE"); 393 | } 394 | } 395 | 396 | impl _SolanaDebuggerSerialize for solana_program::sysvar::rent::Rent { 397 | fn _solana_debugger_serialize(&self, name: &str) { 398 | sol_log("START_NODE"); 399 | sol_log("complex"); 400 | sol_log(name); 401 | sol_log(type_name_of_val(self)); 402 | 403 | sol_log("no_data"); 404 | 405 | self.lamports_per_byte_year._solana_debugger_serialize("lamports_per_byte_year"); 406 | self.exemption_threshold._solana_debugger_serialize("exemption_threshold"); 407 | self.burn_percent._solana_debugger_serialize("burn_percent"); 408 | 409 | sol_log("END_NODE"); 410 | } 411 | } 412 | 413 | impl _SolanaDebuggerSerialize for solana_program::instruction::Instruction { 414 | fn _solana_debugger_serialize(&self, name: &str) { 415 | sol_log("START_NODE"); 416 | sol_log("complex"); 417 | sol_log(name); 418 | sol_log(type_name_of_val(self)); 419 | 420 | sol_log("no_data"); 421 | 422 | self.program_id._solana_debugger_serialize("program_id"); 423 | self.accounts._solana_debugger_serialize("accounts"); 424 | self.data._solana_debugger_serialize("data"); 425 | 426 | sol_log("END_NODE"); 427 | } 428 | } 429 | 430 | impl _SolanaDebuggerSerialize for solana_program::instruction::AccountMeta { 431 | fn _solana_debugger_serialize(&self, name: &str) { 432 | sol_log("START_NODE"); 433 | sol_log("complex"); 434 | sol_log(name); 435 | sol_log(type_name_of_val(self)); 436 | 437 | sol_log("no_data"); 438 | 439 | self.pubkey._solana_debugger_serialize("pubkey"); 440 | self.is_signer._solana_debugger_serialize("is_signer"); 441 | self.is_writable._solana_debugger_serialize("is_writable"); 442 | 443 | sol_log("END_NODE"); 444 | } 445 | } 446 | 447 | } 448 | } 449 | --------------------------------------------------------------------------------