diff --git a/.github/actions/setup-rust/action.yml b/.github/actions/setup-rust/action.yml index 9e45bc83..ae7f7358 100644 --- a/.github/actions/setup-rust/action.yml +++ b/.github/actions/setup-rust/action.yml @@ -18,7 +18,7 @@ runs: rustup component add rustfmt clippy shell: bash - name: Install Cargo Expand - run: cargo install cargo-expand + run: cargo install --locked cargo-expand shell: bash - name: Get rustc version id: rust-version diff --git a/Cargo.lock b/Cargo.lock index 0d221855..c91dd86a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -113,7 +113,7 @@ version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "faa5be5b72abea167f87c868379ba3c2be356bfca9e6f474fd055fa0f7eeb4f2" dependencies = [ - "anchor-syn", + "anchor-syn 0.28.0", "anyhow", "proc-macro2 1.0.66", "quote 1.0.32", @@ -121,13 +121,25 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "anchor-attribute-access-control" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5f619f1d04f53621925ba8a2e633ba5a6081f2ae14758cbb67f38fd823e0a3e" +dependencies = [ + "anchor-syn 0.29.0", + "proc-macro2 1.0.66", + "quote 1.0.32", + "syn 1.0.109", +] + [[package]] name = "anchor-attribute-account" version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f468970344c7c9f9d03b4da854fd7c54f21305059f53789d0045c1dd803f0018" dependencies = [ - "anchor-syn", + "anchor-syn 0.28.0", "anyhow", "bs58 0.5.0", "proc-macro2 1.0.66", @@ -136,62 +148,120 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "anchor-attribute-account" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7f2a3e1df4685f18d12a943a9f2a7456305401af21a07c9fe076ef9ecd6e400" +dependencies = [ + "anchor-syn 0.29.0", + "bs58 0.5.0", + "proc-macro2 1.0.66", + "quote 1.0.32", + "syn 1.0.109", +] + [[package]] name = "anchor-attribute-constant" version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59948e7f9ef8144c2aefb3f32a40c5fce2798baeec765ba038389e82301017ef" dependencies = [ - "anchor-syn", + "anchor-syn 0.28.0", "proc-macro2 1.0.66", "syn 1.0.109", ] +[[package]] +name = "anchor-attribute-constant" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9423945cb55627f0b30903288e78baf6f62c6c8ab28fb344b6b25f1ffee3dca7" +dependencies = [ + "anchor-syn 0.29.0", + "quote 1.0.32", + "syn 1.0.109", +] + [[package]] name = "anchor-attribute-error" version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc753c9d1c7981cb8948cf7e162fb0f64558999c0413058e2d43df1df5448086" dependencies = [ - "anchor-syn", + "anchor-syn 0.28.0", "proc-macro2 1.0.66", "quote 1.0.32", "syn 1.0.109", ] +[[package]] +name = "anchor-attribute-error" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93ed12720033cc3c3bf3cfa293349c2275cd5ab99936e33dd4bf283aaad3e241" +dependencies = [ + "anchor-syn 0.29.0", + "quote 1.0.32", + "syn 1.0.109", +] + [[package]] name = "anchor-attribute-event" version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f38b4e172ba1b52078f53fdc9f11e3dc0668ad27997838a0aad2d148afac8c97" dependencies = [ - "anchor-syn", + "anchor-syn 0.28.0", "anyhow", "proc-macro2 1.0.66", "quote 1.0.32", "syn 1.0.109", ] +[[package]] +name = "anchor-attribute-event" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eef4dc0371eba2d8c8b54794b0b0eb786a234a559b77593d6f80825b6d2c77a2" +dependencies = [ + "anchor-syn 0.29.0", + "proc-macro2 1.0.66", + "quote 1.0.32", + "syn 1.0.109", +] + [[package]] name = "anchor-attribute-program" version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4eebd21543606ab61e2d83d9da37d24d3886a49f390f9c43a1964735e8c0f0d5" dependencies = [ - "anchor-syn", + "anchor-syn 0.28.0", "anyhow", "proc-macro2 1.0.66", "quote 1.0.32", "syn 1.0.109", ] +[[package]] +name = "anchor-attribute-program" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b18c4f191331e078d4a6a080954d1576241c29c56638783322a18d308ab27e4f" +dependencies = [ + "anchor-syn 0.29.0", + "quote 1.0.32", + "syn 1.0.109", +] + [[package]] name = "anchor-client" version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8434a6bf33efba0c93157f7fa2fafac658cb26ab75396886dcedd87c2a8ad445" dependencies = [ - "anchor-lang", + "anchor-lang 0.28.0", "anyhow", "futures", "regex", @@ -210,13 +280,37 @@ version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec4720d899b3686396cced9508f23dab420f1308344456ec78ef76f98fda42af" dependencies = [ - "anchor-syn", + "anchor-syn 0.28.0", "anyhow", "proc-macro2 1.0.66", "quote 1.0.32", "syn 1.0.109", ] +[[package]] +name = "anchor-derive-accounts" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5de10d6e9620d3bcea56c56151cad83c5992f50d5960b3a9bebc4a50390ddc3c" +dependencies = [ + "anchor-syn 0.29.0", + "quote 1.0.32", + "syn 1.0.109", +] + +[[package]] +name = "anchor-derive-serde" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4e2e5be518ec6053d90a2a7f26843dbee607583c779e6c8395951b9739bdfbe" +dependencies = [ + "anchor-syn 0.29.0", + "borsh-derive-internal 0.10.3", + "proc-macro2 1.0.66", + "quote 1.0.32", + "syn 1.0.109", +] + [[package]] name = "anchor-derive-space" version = "0.28.0" @@ -228,20 +322,57 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "anchor-derive-space" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ecc31d19fa54840e74b7a979d44bcea49d70459de846088a1d71e87ba53c419" +dependencies = [ + "proc-macro2 1.0.66", + "quote 1.0.32", + "syn 1.0.109", +] + [[package]] name = "anchor-lang" version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d2d4b20100f1310a774aba3471ef268e5c4ba4d5c28c0bbe663c2658acbc414" dependencies = [ - "anchor-attribute-access-control", - "anchor-attribute-account", - "anchor-attribute-constant", - "anchor-attribute-error", - "anchor-attribute-event", - "anchor-attribute-program", - "anchor-derive-accounts", - "anchor-derive-space", + "anchor-attribute-access-control 0.28.0", + "anchor-attribute-account 0.28.0", + "anchor-attribute-constant 0.28.0", + "anchor-attribute-error 0.28.0", + "anchor-attribute-event 0.28.0", + "anchor-attribute-program 0.28.0", + "anchor-derive-accounts 0.28.0", + "anchor-derive-space 0.28.0", + "arrayref", + "base64 0.13.1", + "bincode", + "borsh 0.10.3", + "bytemuck", + "getrandom 0.2.10", + "solana-program", + "thiserror", +] + +[[package]] +name = "anchor-lang" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35da4785497388af0553586d55ebdc08054a8b1724720ef2749d313494f2b8ad" +dependencies = [ + "anchor-attribute-access-control 0.29.0", + "anchor-attribute-account 0.29.0", + "anchor-attribute-constant 0.29.0", + "anchor-attribute-error 0.29.0", + "anchor-attribute-event 0.29.0", + "anchor-attribute-program 0.29.0", + "anchor-derive-accounts 0.29.0", + "anchor-derive-serde", + "anchor-derive-space 0.29.0", + "anchor-syn 0.29.0", "arrayref", "base64 0.13.1", "bincode", @@ -270,6 +401,24 @@ dependencies = [ "thiserror", ] +[[package]] +name = "anchor-syn" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9101b84702fed2ea57bd22992f75065da5648017135b844283a2f6d74f27825" +dependencies = [ + "anyhow", + "bs58 0.5.0", + "heck 0.3.3", + "proc-macro2 1.0.66", + "quote 1.0.32", + "serde", + "serde_json", + "sha2 0.10.7", + "syn 1.0.109", + "thiserror", +] + [[package]] name = "android-tzdata" version = "0.1.1" @@ -2891,6 +3040,12 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" +[[package]] +name = "pathdiff" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" + [[package]] name = "pbkdf2" version = "0.4.0" @@ -5711,6 +5866,7 @@ name = "trdelnik-client" version = "0.5.0" dependencies = [ "anchor-client", + "anchor-lang 0.29.0", "anyhow", "arbitrary", "bincode", @@ -5723,6 +5879,8 @@ dependencies = [ "honggfuzz", "lazy_static", "log", + "macrotest", + "pathdiff", "pretty_assertions", "proc-macro2 1.0.66", "quinn-proto", diff --git a/crates/client/Cargo.toml b/crates/client/Cargo.toml index 59d9fb01..cfc45350 100644 --- a/crates/client/Cargo.toml +++ b/crates/client/Cargo.toml @@ -24,6 +24,7 @@ macrotest = "1.0.9" [dependencies] trdelnik-test = { workspace = true } +anchor-lang = { version = "0.29.0", features = ["idl-build"] } solana-sdk = { workspace = true } solana-cli-output = { workspace = true } solana-transaction-status = { workspace = true } diff --git a/crates/client/src/fuzzer/data_builder.rs b/crates/client/src/fuzzer/data_builder.rs index 30b53501..df892c9c 100644 --- a/crates/client/src/fuzzer/data_builder.rs +++ b/crates/client/src/fuzzer/data_builder.rs @@ -208,6 +208,7 @@ pub enum FuzzingError { CannotGetInstructionData, CannotDeserializeAccount, NotEnoughAccounts, // TODO add also custom error + AccountNotFound, } #[macro_export] diff --git a/crates/client/src/fuzzer/snapshot.rs b/crates/client/src/fuzzer/snapshot.rs index 99882892..c6c141dd 100644 --- a/crates/client/src/fuzzer/snapshot.rs +++ b/crates/client/src/fuzzer/snapshot.rs @@ -45,7 +45,25 @@ where Ok(accounts) } + fn set_missing_accounts_to_default(accounts: &mut [Option]) { + for acc in accounts.iter_mut() { + if acc.is_none() { + *acc = Some(solana_sdk::account::Account::default()); + } + } + } + pub fn get_snapshot(&'info mut self) -> Result<(T::Ix, T::Ix), FuzzingError> { + // When user passes an account that is not initialized, the runtime will provide + // a default empty account to the program. If the uninitialized account is of type + // AccountInfo, Signer or UncheckedAccount, Anchor will not return an error. However + // when we try to fetch "on-chain" accounts and an account is not initilized, this + // account simply does not exist and the get_account() method returns None. To prevent + // errors during deserialization due to missing accounts, we replace the missing accounts + // with default values similar as the runtime does. + Self::set_missing_accounts_to_default(&mut self.before); + Self::set_missing_accounts_to_default(&mut self.after); + let pre_ix = self.ix.deserialize_option(self.metas, &mut self.before)?; let post_ix = self.ix.deserialize_option(self.metas, &mut self.after)?; Ok((pre_ix, post_ix)) diff --git a/crates/client/src/fuzzer/snapshot_generator.rs b/crates/client/src/fuzzer/snapshot_generator.rs index 4423de5a..3a1e0bde 100644 --- a/crates/client/src/fuzzer/snapshot_generator.rs +++ b/crates/client/src/fuzzer/snapshot_generator.rs @@ -1,13 +1,18 @@ +// To generate the snapshot data types, we need to first find all context struct within the program and parse theirs accounts. +// The parsing of individual Anchor accounts is done using Anchor syn parser: +// https://github.com/coral-xyz/anchor/blob/master/lang/syn/src/parser/accounts/mod.rs + use std::{error::Error, fs::File, io::Read}; +use anchor_lang::anchor_syn::{AccountField, Ty}; use cargo_metadata::camino::Utf8PathBuf; -use proc_macro2::TokenStream; +use proc_macro2::{Ident, Span, TokenStream}; use quote::{format_ident, quote, ToTokens}; use syn::parse::{Error as ParseError, Result as ParseResult}; use syn::spanned::Spanned; -use syn::{ - parse_quote, Attribute, Fields, GenericArgument, Item, ItemStruct, PathArguments, TypePath, -}; +use syn::{parse_quote, Attribute, Fields, GenericArgument, Item, ItemStruct, PathArguments, Type}; + +use anchor_lang::anchor_syn::parser::accounts::parse_account_field; pub fn generate_snapshots_code(code_path: Vec<(String, Utf8PathBuf)>) -> Result { let code = code_path.iter().map(|(code, path)| { @@ -19,7 +24,7 @@ pub fn generate_snapshots_code(code_path: Vec<(String, Utf8PathBuf)>) -> Result< let parse_result = syn::parse_file(&content).map_err(|e| e.to_string())?; - // locate the program module to extract instructions and corresponding Context structs + // locate the program module to extract instructions and corresponding Context structs. for item in parse_result.items.iter() { if let Item::Mod(module) = item { // Check if the module has the #[program] attribute @@ -35,79 +40,105 @@ pub fn generate_snapshots_code(code_path: Vec<(String, Utf8PathBuf)>) -> Result< .content .ok_or("the content of program module is missing")?; - let mut ix_ctx_pairs = Vec::new(); - for item in items { - // Iterate through items in program module and find functions with the Context<_> parameter. Save the function name and the Context's inner type. - if let syn::Item::Fn(func) = item { - let func_name = &func.sig.ident; - let first_param_type = if let Some(param) = func.sig.inputs.into_iter().next() { - let mut ty = None::; - if let syn::FnArg::Typed(t) = param { - if let syn::Type::Path(tp) = *t.ty.clone() { - if let Some(seg) = tp.path.segments.into_iter().next() { - if let PathArguments::AngleBracketed(arg) = seg.arguments { - ty = arg.args.first().cloned(); - } - } - } - } - ty - } else { - None - }; - - let first_param_type = first_param_type.ok_or(format!( - "The function {} does not have the Context parameter and is malformed.", - func_name - ))?; + let ix_ctx_pairs = get_ix_ctx_pairs(&items)?; - ix_ctx_pairs.push((func_name.clone(), first_param_type)); - } - } + let (structs, impls) = get_snapshot_structs_and_impls(code, &ix_ctx_pairs)?; - // Find definition of each Context struct and create new struct with fields wrapped in Option<_> - let mut structs = String::new(); - let mut desers = String::new(); - let parse_result = syn::parse_file(code).map_err(|e| e.to_string())?; - for pair in ix_ctx_pairs { - let mut ty = None; - if let GenericArgument::Type(syn::Type::Path(tp)) = &pair.1 { - ty = tp.path.get_ident().cloned(); - // TODO add support for types with fully qualified path such as ix::Initialize - } - let ty = ty.ok_or(format!("malformed parameters of {} instruction", pair.0))?; - // println!("{} - {}", pair.0, ty); - - // recursively find the context struct and create a new version with wrapped field into Option - if let Some(ctx) = get_ctx_struct(&parse_result.items, &ty) { - let wrapped_struct = wrap_fields_in_option(ctx).unwrap(); - // println!("{}", wrapped_struct); - let deser_code = deserialize_ctx_struct(ctx).unwrap(); - // println!("{}", deser_code); - structs = format!("{}{}", structs, wrapped_struct.into_token_stream()); - desers = format!("{}{}", desers, deser_code.into_token_stream()); - } else { - return Err(format!("The Context struct {} was not found", ty)); - } - } let use_statements = quote! { use trdelnik_client::anchor_lang::{prelude::*, self}; use trdelnik_client::anchor_lang::solana_program::instruction::AccountMeta; use trdelnik_client::fuzzing::{get_account_infos_option, FuzzingError}; } .into_token_stream(); - Ok(format!("{}{}{}", use_statements, structs, desers)) + Ok(format!("{}{}{}", use_statements, structs, impls)) }); code.into_iter().collect() } +/// Creates new snapshot structs with fields wrapped in Option<_> if approriate and the +/// respective implementations with snapshot deserialization methods +fn get_snapshot_structs_and_impls( + code: &str, + ix_ctx_pairs: &[(Ident, GenericArgument)], +) -> Result<(String, String), String> { + let mut structs = String::new(); + let mut impls = String::new(); + let parse_result = syn::parse_file(code).map_err(|e| e.to_string())?; + for pair in ix_ctx_pairs { + let mut ty = None; + if let GenericArgument::Type(syn::Type::Path(tp)) = &pair.1 { + ty = tp.path.get_ident().cloned(); + // TODO add support for types with fully qualified path such as ix::Initialize + } + let ty = ty.ok_or(format!("malformed parameters of {} instruction", pair.0))?; + + // recursively find the context struct and create a new version with wrapped fields into Option + if let Some(ctx) = find_ctx_struct(&parse_result.items, &ty) { + let fields_parsed = if let Fields::Named(f) = ctx.fields.clone() { + let field_deser: ParseResult> = + f.named.iter().map(parse_account_field).collect(); + field_deser + } else { + Err(ParseError::new( + ctx.fields.span(), + "Context struct parse errror.", + )) + } + .map_err(|e| e.to_string())?; + + let wrapped_struct = create_snapshot_struct(ctx, &fields_parsed).unwrap(); + let deser_code = + deserialize_ctx_struct_anchor(ctx, &fields_parsed).map_err(|e| e.to_string())?; + // let deser_code = deserialize_ctx_struct(ctx).unwrap(); + structs = format!("{}{}", structs, wrapped_struct.into_token_stream()); + impls = format!("{}{}", impls, deser_code.into_token_stream()); + } else { + return Err(format!("The Context struct {} was not found", ty)); + } + } + + Ok((structs, impls)) +} + +/// Iterates through items and finds functions with the Context<_> parameter. Returns pairs with the function name and the Context's inner type. +fn get_ix_ctx_pairs(items: &[Item]) -> Result, String> { + let mut ix_ctx_pairs = Vec::new(); + for item in items { + if let syn::Item::Fn(func) = item { + let func_name = &func.sig.ident; + let first_param_type = if let Some(param) = func.sig.inputs.iter().next() { + let mut ty = None::; + if let syn::FnArg::Typed(t) = param { + if let syn::Type::Path(tp) = *t.ty.clone() { + if let Some(seg) = tp.path.segments.iter().next() { + if let PathArguments::AngleBracketed(arg) = &seg.arguments { + ty = arg.args.first().cloned(); + } + } + } + } + ty + } else { + None + }; + + let first_param_type = first_param_type.ok_or(format!( + "The function {} does not have the Context parameter and is malformed.", + func_name + ))?; + + ix_ctx_pairs.push((func_name.clone(), first_param_type)); + } + } + Ok(ix_ctx_pairs) +} + /// Recursively find a struct with a given `name` -fn get_ctx_struct<'a>(items: &'a Vec, name: &'a syn::Ident) -> Option<&'a ItemStruct> { +fn find_ctx_struct<'a>(items: &'a Vec, name: &'a syn::Ident) -> Option<&'a ItemStruct> { for item in items { if let Item::Struct(struct_item) = item { if struct_item.ident == *name { - // println!("we found the struct {}", name); return Some(struct_item); } } @@ -117,7 +148,7 @@ fn get_ctx_struct<'a>(items: &'a Vec, name: &'a syn::Ident) -> Option for item in items { if let Item::Mod(mod_item) = item { if let Some((_, items)) = &mod_item.content { - let r = get_ctx_struct(items, name); + let r = find_ctx_struct(items, name); if r.is_some() { return r; } @@ -128,18 +159,79 @@ fn get_ctx_struct<'a>(items: &'a Vec, name: &'a syn::Ident) -> Option None } -fn wrap_fields_in_option(orig_struct: &ItemStruct) -> Result> { +fn is_boxed(ty: &anchor_lang::anchor_syn::Ty) -> bool { + match ty { + Ty::Account(acc) => acc.boxed, + Ty::InterfaceAccount(acc) => acc.boxed, + _ => false, + } +} + +/// Determines if an Account should be wrapped into the `Option` type. +/// The function returns true if the account has the init or close constraints set +/// or if it is wrapped into the `Option` type. +fn is_optional(parsed_field: &AccountField) -> bool { + let is_optional = match parsed_field { + AccountField::Field(field) => field.is_optional, + AccountField::CompositeField(_) => false, + }; + let constraints = match parsed_field { + AccountField::Field(f) => &f.constraints, + AccountField::CompositeField(f) => &f.constraints, + }; + + constraints.init.is_some() || constraints.is_close() || is_optional +} + +/// Creates new Snapshot struct from the context struct. Removes Box<> types. +fn create_snapshot_struct( + orig_struct: &ItemStruct, + parsed_fields: &[AccountField], +) -> Result> { let struct_name = format_ident!("{}Snapshot", orig_struct.ident); let wrapped_fields = match orig_struct.fields.clone() { Fields::Named(named) => { - let field_wrappers = named.named.iter().map(|field| { - let field_name = &field.ident; - let field_type = &field.ty; - quote! { - pub #field_name: Option<#field_type>, - } - }); + let field_wrappers = + named + .named + .iter() + .zip(parsed_fields) + .map(|(field, parsed_field)| { + let field_name = &field.ident; + let mut field_type = &field.ty; + if let AccountField::Field(f) = parsed_field { + if f.is_optional { + // remove option + if let Some(ty) = extract_inner_type(field_type) { + field_type = ty; + } + } + if is_boxed(&f.ty) { + // remove box + if let Some(ty) = extract_inner_type(field_type) { + field_type = ty; + } + } + } else { + return Err( + "Composite field types in context structs not supported".into() + ); + } + if is_optional(parsed_field) { + Ok(quote! { + pub #field_name: Option<#field_type>, + }) + } else { + Ok(quote! { + pub #field_name: #field_type, + }) + } + }); + + let field_wrappers: Result, Box> = + field_wrappers.into_iter().collect(); + let field_wrappers = field_wrappers?; quote! { { #(#field_wrappers)* } } @@ -156,45 +248,45 @@ fn wrap_fields_in_option(orig_struct: &ItemStruct) -> Result Result> { - let impl_name = format_ident!("{}Snapshot", orig_struct.ident); - let names_deser_pairs = match orig_struct.fields.clone() { - Fields::Named(named) => { - let field_deser = named.named.iter().map(|field| { - let field_name = match &field.ident { - Some(name) => name, - None => { - return Err(ParseError::new( - field.ident.span(), - "invalid account name given", - )) - } - }; - let field_type = &field.ty; - - let path = match &field_type { - syn::Type::Path(ty_path) => ty_path.path.clone(), - _ => { - return Err(ParseError::new( - field_type.span(), - "invalid account type given", - )) - } - }; - let id = path.segments[0].clone(); - // println!("field name: {}, type: {}", field_name, id.ident); - let ty = match id.ident.to_string().as_str() { - "AccountInfo" => AnchorType::AccountInfo, - "Signer" => AnchorType::Signer, - "Account" => AnchorType::Account(parse_account_ty(&path)?), - "Program" => AnchorType::Program(parse_program_ty(&path)?), // TODO - _ => return Err(ParseError::new(id.span(), "invalid account type given")), - }; - let deser_tokens = match ty.to_tokens() { - Some((return_type, deser_method)) => { - deserialize_tokens(field_name, return_type, deser_method) +fn extract_inner_type(field_type: &Type) -> Option<&Type> { + if let syn::Type::Path(type_path) = field_type { + let segment = type_path.path.segments.last()?; + let ident = &segment.ident; + + if ident == "Box" || ident == "Option" { + if let PathArguments::AngleBracketed(args) = &segment.arguments { + if let Some(syn::GenericArgument::Type(inner_type)) = args.args.first() { + return Some(inner_type); + } + } + } + } + + None +} + +/// Generates code to deserialize the snapshot structs. +fn deserialize_ctx_struct_anchor( + snapshot_struct: &ItemStruct, + parsed_fields: &[AccountField], +) -> Result> { + let impl_name = format_ident!("{}Snapshot", snapshot_struct.ident); + let names_deser_pairs: Result, _> = parsed_fields + .iter() + .map(|parsed_f| match parsed_f { + AccountField::Field(f) => { + let field_name = f.ident.clone(); + let deser_tokens = match ty_to_tokens(&f.ty) { + Some((return_type, deser_method)) => deserialize_account_tokens( + &field_name, + is_optional(parsed_f), + return_type, + deser_method, + ), + None if matches!(&f.ty, Ty::UncheckedAccount) => { + acc_unchecked_tokens(&field_name) } - None => acc_info_tokens(field_name), + None => acc_info_tokens(&field_name), }; Ok(( quote! {#field_name}, @@ -202,16 +294,12 @@ fn deserialize_ctx_struct(orig_struct: &ItemStruct) -> Result, _> = - field_deser.into_iter().collect(); - result - } - - _ => return Err("Only structs with named fields are supported".into()), - }?; + } + AccountField::CompositeField(_) => Err("CompositeFields not supported!"), + }) + .collect(); - let (names, fields_deser): (Vec<_>, Vec<_>) = names_deser_pairs.iter().cloned().unzip(); + let (names, fields_deser): (Vec<_>, Vec<_>) = names_deser_pairs?.iter().cloned().unzip(); let generated_deser_impl: syn::Item = parse_quote! { impl<'info> #impl_name<'info> { @@ -236,157 +324,142 @@ fn deserialize_ctx_struct(orig_struct: &ItemStruct) -> Result Option<(TokenStream, TokenStream)> { - let (return_type, deser_method) = match self { - AnchorType::AccountInfo => return None, - AnchorType::Signer => ( - quote! { Signer<'_>}, - quote!(anchor_lang::accounts::signer::Signer::try_from(&acc)), - ), - AnchorType::Account(acc) => { - let path = &acc.account_type_path; - ( - quote! { anchor_lang::accounts::account::Account<#path>}, - quote! {anchor_lang::accounts::account::Account::try_from(&acc)}, - ) - } - AnchorType::Program(prog) => { - let path = &prog.account_type_path; - ( - quote! { anchor_lang::accounts::program::Program<#path>}, - quote!(anchor_lang::accounts::program::Program::try_from(&acc)), - ) - } - }; - Some((return_type, deser_method)) - } -} - -pub struct ProgramTy { - // The struct type of the account. - pub account_type_path: TypePath, +/// Get the identifier (name) of the passed sysvar type. +fn sysvar_to_ident(sysvar: &anchor_lang::anchor_syn::SysvarTy) -> String { + let str = match sysvar { + anchor_lang::anchor_syn::SysvarTy::Clock => "Clock", + anchor_lang::anchor_syn::SysvarTy::Rent => "Rent", + anchor_lang::anchor_syn::SysvarTy::EpochSchedule => "EpochSchedule", + anchor_lang::anchor_syn::SysvarTy::Fees => "Fees", + anchor_lang::anchor_syn::SysvarTy::RecentBlockhashes => "RecentBlockhashes", + anchor_lang::anchor_syn::SysvarTy::SlotHashes => "SlotHashes", + anchor_lang::anchor_syn::SysvarTy::SlotHistory => "SlotHistory", + anchor_lang::anchor_syn::SysvarTy::StakeHistory => "StakeHistory", + anchor_lang::anchor_syn::SysvarTy::Instructions => "Instructions", + anchor_lang::anchor_syn::SysvarTy::Rewards => "Rewards", + }; + str.into() } -pub struct AccountTy { - // The struct type of the account. - pub account_type_path: TypePath, - // True if the account has been boxed via `Box`. - pub boxed: bool, +/// Converts passed account type to token streams. The function returns a pair of streams where the first +/// variable in the pair is the type itself and the second is a fully qualified function to deserialize +/// the given type. +pub fn ty_to_tokens(ty: &anchor_lang::anchor_syn::Ty) -> Option<(TokenStream, TokenStream)> { + let (return_type, deser_method) = match ty { + Ty::AccountInfo | Ty::UncheckedAccount => return None, + Ty::SystemAccount => ( + quote! { SystemAccount<'_>}, + quote!(anchor_lang::accounts::system_account::SystemAccount::try_from(&acc)), + ), + Ty::Sysvar(sysvar) => { + let id = syn::Ident::new(&sysvar_to_ident(sysvar), Span::call_site()); + ( + quote! { Sysvar<#id>}, + quote!(anchor_lang::accounts::sysvar::Sysvar::from_account_info( + &acc + )), + ) + } + Ty::Signer => ( + quote! { Signer<'_>}, + quote!(anchor_lang::accounts::signer::Signer::try_from(&acc)), + ), + Ty::Account(acc) => { + let path = &acc.account_type_path; + ( + quote! { anchor_lang::accounts::account::Account<#path>}, + quote! {anchor_lang::accounts::account::Account::try_from(&acc)}, + ) + } + Ty::AccountLoader(acc) => { + let path = &acc.account_type_path; + ( + quote! { anchor_lang::accounts::account_loader::AccountLoader<#path>}, + quote! {anchor_lang::accounts::account_loader::AccountLoader::try_from(&acc)}, + ) + } + Ty::Program(prog) => { + let path = &prog.account_type_path; + ( + quote! { anchor_lang::accounts::program::Program<#path>}, + quote!(anchor_lang::accounts::program::Program::try_from(&acc)), + ) + } + Ty::Interface(interf) => { + let path = &interf.account_type_path; + ( + quote! { anchor_lang::accounts::interface::Interface<#path>}, + quote! {anchor_lang::accounts::interface::Interface::try_from(&acc)}, + ) + } + Ty::InterfaceAccount(interf_acc) => { + let path = &interf_acc.account_type_path; + ( + quote! { anchor_lang::accounts::interface_account::InterfaceAccount<#path>}, + quote! {anchor_lang::accounts::interface_account::InterfaceAccount::try_from(&acc)}, + ) + } + Ty::ProgramData => return None, + }; + Some((return_type, deser_method)) } -fn deserialize_tokens( +/// Generates the code necessary to deserialize an account +fn deserialize_account_tokens( name: &syn::Ident, + is_optional: bool, return_type: TokenStream, deser_method: TokenStream, ) -> TokenStream { - quote! { - let #name:Option<#return_type> = accounts_iter - .next() - .ok_or(FuzzingError::NotEnoughAccounts)? - .map(|acc| #deser_method) - .transpose() - .unwrap_or(None); + if is_optional { + quote! { + let #name:Option<#return_type> = accounts_iter + .next() + .ok_or(FuzzingError::NotEnoughAccounts)? + .map(|acc| { + if acc.key() != PROGRAM_ID { + #deser_method.map_err(|e| e.to_string()) + } else { + Err("Optional account not provided".to_string()) + } + }) + .transpose() + .unwrap_or(None); + } + } else { + quote! { + let #name: #return_type = accounts_iter + .next() + .ok_or(FuzzingError::NotEnoughAccounts)? + .map(|acc| #deser_method) + .ok_or(FuzzingError::AccountNotFound)? + .map_err(|_| FuzzingError::CannotDeserializeAccount)?; + } } } +/// Generates the code used with raw accounts as AccountInfo fn acc_info_tokens(name: &syn::Ident) -> TokenStream { quote! { let #name = accounts_iter .next() - .ok_or(FuzzingError::NotEnoughAccounts)?; + .ok_or(FuzzingError::NotEnoughAccounts)? + .ok_or(FuzzingError::AccountNotFound)?; } } -fn parse_account_ty(path: &syn::Path) -> ParseResult { - let account_type_path = parse_account(path)?; - let boxed = tts_to_string(path) - .replace(' ', "") - .starts_with("Box ParseResult { - let account_type_path = parse_account(path)?; - Ok(ProgramTy { account_type_path }) -} - -pub fn tts_to_string(item: T) -> String { - let mut tts = proc_macro2::TokenStream::new(); - item.to_tokens(&mut tts); - tts.to_string() -} - -fn parse_account(mut path: &syn::Path) -> ParseResult { - let path_str = tts_to_string(path).replace(' ', ""); - if path_str.starts_with("Box { - // Expected: <'info, MyType>. - if args.args.len() != 1 { - return Err(ParseError::new( - args.args.span(), - "bracket arguments must be the lifetime and type", - )); - } - match &args.args[0] { - syn::GenericArgument::Type(syn::Type::Path(ty_path)) => { - path = &ty_path.path; - } - _ => { - return Err(ParseError::new( - args.args[1].span(), - "first bracket argument must be a lifetime", - )) - } - } - } - _ => { - return Err(ParseError::new( - segments.arguments.span(), - "expected angle brackets with a lifetime and type", - )) - } - } - } - - let segments = &path.segments[0]; - match &segments.arguments { - syn::PathArguments::AngleBracketed(args) => { - // Expected: <'info, MyType>. - if args.args.len() != 2 { - return Err(ParseError::new( - args.args.span(), - "bracket arguments must be the lifetime and type", - )); - } - match &args.args[1] { - syn::GenericArgument::Type(syn::Type::Path(ty_path)) => Ok(ty_path.clone()), - _ => Err(ParseError::new( - args.args[1].span(), - "first bracket argument must be a lifetime", - )), - } - } - _ => Err(ParseError::new( - segments.arguments.span(), - "expected angle brackets with a lifetime and type", - )), +/// Generates the code used with Unchecked accounts +fn acc_unchecked_tokens(name: &syn::Ident) -> TokenStream { + quote! { + let #name = accounts_iter + .next() + .ok_or(FuzzingError::NotEnoughAccounts)? + .map(anchor_lang::accounts::unchecked_account::UncheckedAccount::try_from) + .ok_or(FuzzingError::AccountNotFound)?; } } +/// Checks if the program attribute is present fn has_program_attribute(attrs: &Vec) -> bool { for attr in attrs { if attr.path.is_ident("program") { diff --git a/crates/client/src/lib.rs b/crates/client/src/lib.rs index 08fdab61..da7301b9 100644 --- a/crates/client/src/lib.rs +++ b/crates/client/src/lib.rs @@ -19,6 +19,7 @@ pub use anyhow::{self, Error}; #[cfg(feature = "fuzzing")] pub mod fuzzing { + pub use self::anchor_lang::solana_program::instruction::AccountMeta; pub use super::{ anchor_lang, anchor_lang::system_program::ID as SYSTEM_PROGRAM_ID, anchor_lang::InstructionData, anchor_lang::ToAccountInfo, anchor_lang::ToAccountMetas, @@ -26,7 +27,6 @@ pub mod fuzzing { Keypair, Pubkey, Signer, TempClone, }; pub use anchor_client::anchor_lang::solana_program::hash::Hash; - pub use anchor_lang::solana_program::instruction::AccountMeta; pub use arbitrary; pub use arbitrary::Arbitrary; pub use honggfuzz::fuzz; diff --git a/crates/client/tests/test_data/expected_source_codes/expected_accounts_snapshots.rs b/crates/client/tests/test_data/expected_source_codes/expected_accounts_snapshots.rs index 851a6ce0..f21787bf 100644 --- a/crates/client/tests/test_data/expected_source_codes/expected_accounts_snapshots.rs +++ b/crates/client/tests/test_data/expected_source_codes/expected_accounts_snapshots.rs @@ -2,23 +2,23 @@ use trdelnik_client::anchor_lang::solana_program::instruction::AccountMeta; use trdelnik_client::anchor_lang::{self, prelude::*}; use trdelnik_client::fuzzing::{get_account_infos_option, FuzzingError}; pub struct InitVestingSnapshot<'info> { - pub sender: Option>, - pub sender_token_account: Option>, + pub sender: Signer<'info>, + pub sender_token_account: Account<'info, TokenAccount>, pub escrow: Option>, - pub escrow_token_account: Option>, - pub mint: Option>, - pub token_program: Option>, - pub system_program: Option>, + pub escrow_token_account: Account<'info, TokenAccount>, + pub mint: Account<'info, Mint>, + pub token_program: Program<'info, Token>, + pub system_program: Program<'info, System>, } pub struct WithdrawUnlockedSnapshot<'info> { - pub recipient: Option>, - pub recipient_token_account: Option>, + pub recipient: Signer<'info>, + pub recipient_token_account: Account<'info, TokenAccount>, pub escrow: Option>, - pub escrow_token_account: Option>, - pub escrow_pda_authority: Option>, - pub mint: Option>, - pub token_program: Option>, - pub system_program: Option>, + pub escrow_token_account: Account<'info, TokenAccount>, + pub escrow_pda_authority: AccountInfo<'info>, + pub mint: Account<'info, Mint>, + pub token_program: Program<'info, Token>, + pub system_program: Program<'info, System>, } impl<'info> InitVestingSnapshot<'info> { pub fn deserialize_option( @@ -28,50 +28,57 @@ impl<'info> InitVestingSnapshot<'info> { let accounts = get_account_infos_option(accounts, metas) .map_err(|_| FuzzingError::CannotGetAccounts)?; let mut accounts_iter = accounts.into_iter(); - let sender: Option> = accounts_iter + let sender: Signer<'_> = accounts_iter .next() .ok_or(FuzzingError::NotEnoughAccounts)? .map(|acc| anchor_lang::accounts::signer::Signer::try_from(&acc)) - .transpose() - .unwrap_or(None); - let sender_token_account: Option> = + .ok_or(FuzzingError::AccountNotFound)? + .map_err(|_| FuzzingError::CannotDeserializeAccount)?; + let sender_token_account: anchor_lang::accounts::account::Account = accounts_iter .next() .ok_or(FuzzingError::NotEnoughAccounts)? .map(|acc| anchor_lang::accounts::account::Account::try_from(&acc)) - .transpose() - .unwrap_or(None); + .ok_or(FuzzingError::AccountNotFound)? + .map_err(|_| FuzzingError::CannotDeserializeAccount)?; let escrow: Option> = accounts_iter .next() .ok_or(FuzzingError::NotEnoughAccounts)? - .map(|acc| anchor_lang::accounts::account::Account::try_from(&acc)) + .map(|acc| { + if acc.key() != PROGRAM_ID { + anchor_lang::accounts::account::Account::try_from(&acc) + .map_err(|e| e.to_string()) + } else { + Err("Optional account not provided".to_string()) + } + }) .transpose() .unwrap_or(None); - let escrow_token_account: Option> = + let escrow_token_account: anchor_lang::accounts::account::Account = accounts_iter .next() .ok_or(FuzzingError::NotEnoughAccounts)? .map(|acc| anchor_lang::accounts::account::Account::try_from(&acc)) - .transpose() - .unwrap_or(None); - let mint: Option> = accounts_iter + .ok_or(FuzzingError::AccountNotFound)? + .map_err(|_| FuzzingError::CannotDeserializeAccount)?; + let mint: anchor_lang::accounts::account::Account = accounts_iter .next() .ok_or(FuzzingError::NotEnoughAccounts)? .map(|acc| anchor_lang::accounts::account::Account::try_from(&acc)) - .transpose() - .unwrap_or(None); - let token_program: Option> = accounts_iter + .ok_or(FuzzingError::AccountNotFound)? + .map_err(|_| FuzzingError::CannotDeserializeAccount)?; + let token_program: anchor_lang::accounts::program::Program = accounts_iter .next() .ok_or(FuzzingError::NotEnoughAccounts)? .map(|acc| anchor_lang::accounts::program::Program::try_from(&acc)) - .transpose() - .unwrap_or(None); - let system_program: Option> = accounts_iter + .ok_or(FuzzingError::AccountNotFound)? + .map_err(|_| FuzzingError::CannotDeserializeAccount)?; + let system_program: anchor_lang::accounts::program::Program = accounts_iter .next() .ok_or(FuzzingError::NotEnoughAccounts)? .map(|acc| anchor_lang::accounts::program::Program::try_from(&acc)) - .transpose() - .unwrap_or(None); + .ok_or(FuzzingError::AccountNotFound)? + .map_err(|_| FuzzingError::CannotDeserializeAccount)?; Ok(Self { sender, sender_token_account, @@ -91,53 +98,61 @@ impl<'info> WithdrawUnlockedSnapshot<'info> { let accounts = get_account_infos_option(accounts, metas) .map_err(|_| FuzzingError::CannotGetAccounts)?; let mut accounts_iter = accounts.into_iter(); - let recipient: Option> = accounts_iter + let recipient: Signer<'_> = accounts_iter .next() .ok_or(FuzzingError::NotEnoughAccounts)? .map(|acc| anchor_lang::accounts::signer::Signer::try_from(&acc)) - .transpose() - .unwrap_or(None); - let recipient_token_account: Option> = + .ok_or(FuzzingError::AccountNotFound)? + .map_err(|_| FuzzingError::CannotDeserializeAccount)?; + let recipient_token_account: anchor_lang::accounts::account::Account = accounts_iter .next() .ok_or(FuzzingError::NotEnoughAccounts)? .map(|acc| anchor_lang::accounts::account::Account::try_from(&acc)) - .transpose() - .unwrap_or(None); + .ok_or(FuzzingError::AccountNotFound)? + .map_err(|_| FuzzingError::CannotDeserializeAccount)?; let escrow: Option> = accounts_iter .next() .ok_or(FuzzingError::NotEnoughAccounts)? - .map(|acc| anchor_lang::accounts::account::Account::try_from(&acc)) + .map(|acc| { + if acc.key() != PROGRAM_ID { + anchor_lang::accounts::account::Account::try_from(&acc) + .map_err(|e| e.to_string()) + } else { + Err("Optional account not provided".to_string()) + } + }) .transpose() .unwrap_or(None); - let escrow_token_account: Option> = + let escrow_token_account: anchor_lang::accounts::account::Account = accounts_iter .next() .ok_or(FuzzingError::NotEnoughAccounts)? .map(|acc| anchor_lang::accounts::account::Account::try_from(&acc)) - .transpose() - .unwrap_or(None); + .ok_or(FuzzingError::AccountNotFound)? + .map_err(|_| FuzzingError::CannotDeserializeAccount)?; let escrow_pda_authority = accounts_iter .next() - .ok_or(FuzzingError::NotEnoughAccounts)?; - let mint: Option> = accounts_iter + .ok_or(FuzzingError::NotEnoughAccounts)? + .ok_or(FuzzingError::AccountNotFound)?; + let mint: anchor_lang::accounts::account::Account = accounts_iter .next() .ok_or(FuzzingError::NotEnoughAccounts)? .map(|acc| anchor_lang::accounts::account::Account::try_from(&acc)) - .transpose() - .unwrap_or(None); - let token_program: Option> = accounts_iter + .ok_or(FuzzingError::AccountNotFound)? + .map_err(|_| FuzzingError::CannotDeserializeAccount)?; + let token_program: anchor_lang::accounts::program::Program = accounts_iter .next() .ok_or(FuzzingError::NotEnoughAccounts)? .map(|acc| anchor_lang::accounts::program::Program::try_from(&acc)) - .transpose() - .unwrap_or(None); - let system_program: Option> = accounts_iter + .ok_or(FuzzingError::AccountNotFound)? + .map_err(|_| FuzzingError::CannotDeserializeAccount)?; + let system_program: anchor_lang::accounts::program::Program = accounts_iter .next() .ok_or(FuzzingError::NotEnoughAccounts)? .map(|acc| anchor_lang::accounts::program::Program::try_from(&acc)) - .transpose() - .unwrap_or(None); + .ok_or(FuzzingError::AccountNotFound)? + .map_err(|_| FuzzingError::CannotDeserializeAccount)?; Ok(Self { recipient, recipient_token_account, diff --git a/rust-toolchain.toml b/rust-toolchain.toml index aa464261..743f7cd9 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,2 +1,2 @@ [toolchain] -channel = "1.71.0" +channel = "1.72.0"