├── .github └── workflows │ └── build+test.yml ├── .gitignore ├── Cargo.toml ├── README.md ├── diagnostics └── pda │ ├── README.md │ └── dumps │ ├── 01_shank-orig.txt │ ├── 02_shank-no-seeds.txt │ ├── 03_shank-unused-seeds.txt │ ├── 04_shank-using-seeds-with-bump.txt │ ├── 05_shank-using-seeds-with-bump-only.txt │ ├── 06_shank-using-pda-additionally.txt │ └── 07_shank-using-pda-only.txt ├── release.toml ├── rustfmt.toml ├── shank-cli ├── Cargo.toml ├── README.md └── src │ ├── bin │ └── main.rs │ └── lib.rs ├── shank-idl ├── Cargo.toml ├── README.md ├── rustfmt.toml ├── src │ ├── file.rs │ ├── idl.rs │ ├── idl_error_code.rs │ ├── idl_field.rs │ ├── idl_instruction.rs │ ├── idl_metadata.rs │ ├── idl_type.rs │ ├── idl_type_definition.rs │ ├── idl_variant.rs │ ├── lib.rs │ └── manifest.rs └── tests │ ├── accounts.rs │ ├── errors.rs │ ├── fixtures │ ├── accounts │ │ ├── sample_crate │ │ │ ├── Cargo.toml │ │ │ ├── idl.json │ │ │ └── src │ │ │ │ ├── auctionhouse.rs │ │ │ │ ├── lib.rs │ │ │ │ └── metadata.rs │ │ └── single_file │ │ │ ├── account.json │ │ │ ├── account.rs │ │ │ ├── complex_types.json │ │ │ ├── complex_types.rs │ │ │ ├── idl_type.json │ │ │ ├── idl_type.rs │ │ │ ├── padding.json │ │ │ └── padding.rs │ ├── errors │ │ ├── this_error.json │ │ ├── this_error.rs │ │ ├── this_error_custom_codes.json │ │ └── this_error_custom_codes.rs │ ├── instructions │ │ └── single_file │ │ │ ├── create_idl_instructions.json │ │ │ ├── create_idl_instructions.rs │ │ │ ├── instruction_invalid_attr.rs │ │ │ ├── instruction_invalid_discriminant.rs │ │ │ ├── instruction_no_args.json │ │ │ ├── instruction_no_args.rs │ │ │ ├── instruction_with_args.json │ │ │ ├── instruction_with_args.rs │ │ │ ├── instruction_with_docs.json │ │ │ ├── instruction_with_docs.rs │ │ │ ├── instruction_with_multiple_args.json │ │ │ ├── instruction_with_multiple_args.rs │ │ │ ├── instruction_with_optional_account.json │ │ │ ├── instruction_with_optional_account.rs │ │ │ ├── instruction_with_optional_account_defaulting.json │ │ │ ├── instruction_with_optional_account_defaulting.rs │ │ │ ├── instruction_with_optional_signer_account.json │ │ │ ├── instruction_with_optional_signer_account.rs │ │ │ ├── instruction_with_struct_args.json │ │ │ └── instruction_with_struct_args.rs │ ├── macros │ │ ├── program_id_missing.json │ │ ├── program_id_missing.rs │ │ ├── program_id_valid.json │ │ └── program_id_valid.rs │ └── types │ │ ├── invalid_single.rs │ │ ├── valid_multiple.json │ │ ├── valid_multiple.rs │ │ ├── valid_multiple_maps.json │ │ ├── valid_multiple_maps.rs │ │ ├── valid_multiple_sets.json │ │ ├── valid_multiple_sets.rs │ │ ├── valid_multiple_tuples.json │ │ ├── valid_multiple_tuples.rs │ │ ├── valid_single_data_enum.json │ │ ├── valid_single_data_enum.rs │ │ ├── valid_single_enum.json │ │ ├── valid_single_enum.rs │ │ ├── valid_single_enum_shank_type.json │ │ ├── valid_single_enum_shank_type.rs │ │ ├── valid_single_struct.json │ │ ├── valid_single_struct.rs │ │ ├── valid_single_struct_shank_type.json │ │ └── valid_single_struct_shank_type.rs │ ├── instructions.rs │ ├── macros.rs │ └── types.rs ├── shank-macro-impl ├── Cargo.toml ├── README.md ├── rustfmt.toml └── src │ ├── account │ ├── extract_accounts.rs │ └── mod.rs │ ├── builder │ ├── argument.rs │ ├── argument_test.rs │ ├── builder.rs │ ├── builder_test.rs │ └── mod.rs │ ├── converters.rs │ ├── custom_type │ ├── custom_enum.rs │ ├── custom_struct.rs │ ├── custom_type_config.rs │ └── mod.rs │ ├── error │ ├── mod.rs │ ├── program_error.rs │ └── this_error.rs │ ├── instruction │ ├── account_attrs.rs │ ├── account_attrs_test.rs │ ├── extract_instructions.rs │ ├── idl_instruction_attrs.rs │ ├── instruction.rs │ ├── instruction_test.rs │ ├── mod.rs │ ├── strategy_attrs.rs │ └── strategy_attrs_test.rs │ ├── krate │ ├── crate_context.rs │ ├── mod.rs │ └── module_context.rs │ ├── lib.rs │ ├── macros │ ├── mod.rs │ └── program_id.rs │ ├── parsed_enum │ ├── mod.rs │ ├── parsed_enum.rs │ └── parsed_enum_variant.rs │ ├── parsed_macro │ ├── mod.rs │ └── parsed_macro.rs │ ├── parsed_struct │ ├── mod.rs │ ├── parsed_struct.rs │ ├── parsed_struct_test.rs │ ├── seed.rs │ ├── struct_attr.rs │ └── struct_field_attr.rs │ ├── parsers │ ├── attrs.rs │ └── mod.rs │ └── types │ ├── mod.rs │ ├── parsed_reference.rs │ ├── render_rust_ty.rs │ ├── resolve_rust_ty.rs │ └── type_kind.rs ├── shank-macro ├── Cargo.toml ├── README.md ├── rustfmt.toml └── src │ ├── account.rs │ ├── builder.rs │ ├── context.rs │ ├── instruction.rs │ └── lib.rs ├── shank-render ├── Cargo.toml ├── rustfmt.toml ├── src │ ├── builder │ │ ├── mod.rs │ │ └── render_builders.rs │ ├── consts.rs │ ├── context │ │ ├── mod.rs │ │ └── render_context.rs │ ├── lib.rs │ └── pda │ │ ├── mod.rs │ │ ├── pda_common.rs │ │ ├── render_pda.rs │ │ └── render_seeds.rs └── tests │ ├── mod.rs │ ├── pda │ ├── mod.rs │ ├── render_impl.rs │ ├── render_pda_fn.rs │ └── render_seeds_fn.rs │ └── utils │ └── mod.rs └── shank ├── Cargo.toml ├── README.md ├── assets └── shank-logo.gif └── src └── lib.rs /.github/workflows/build+test.yml: -------------------------------------------------------------------------------- 1 | name: Build+Test 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build_and_test: 14 | runs-on: ${{ matrix.os }} 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | os: [ubuntu-latest-16-cores, windows-latest-8-cores, macos-latest] 19 | include: 20 | - os: ubuntu-latest 21 | RUST: stable 22 | 23 | - os: windows-latest 24 | RUST: stable 25 | 26 | - os: macos-latest 27 | RUST: stable 28 | 29 | steps: 30 | - uses: hecrj/setup-rust-action@v1 31 | with: 32 | rust-version: ${{ matrix.RUST }} 33 | 34 | - uses: actions/checkout@master 35 | 36 | - name: Build and Run Tests 37 | run: cargo test 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/target 2 | **/Cargo.lock 3 | /.idea/ 4 | **/.DS_Store 5 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "shank", 4 | "shank-cli", 5 | "shank-idl", 6 | "shank-macro", 7 | "shank-macro-impl", 8 | "shank-render", 9 | ] 10 | exclude = ["./shank-idl/tests/fixtures/"] 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Shank [![Build+Test](https://github.com/metaplex-foundation/shank/actions/workflows/build+test.yml/badge.svg)](https://github.com/metaplex-foundation/shank/actions/workflows/build+test.yml) 2 | 3 | Collection of shank crates used to annotate Rust programs in order to extract IDL via the 4 | included CLI tool. This IDL is used by [solita](https://github.com/metaplex-foundation/solita) in order to generate program SDKs. 5 | 6 | ![shank-logo](./shank/assets/shank-logo.gif) 7 | 8 | ## Installation 9 | 10 | For _usage_ and _installation_ see the [shank-cli Readme](./shank-cli/README.md). 11 | 12 | ## Crates 13 | 14 | - [shank](./shank) top level crate to be installed and included in your library to add macro 15 | annotations 16 | - [shank_cli](./shank-cli) the CLI tool that extracts IDL from a specified crate into a file 17 | - [shank-macro](./shank-macro) provides the _derive_ macros shank uses 18 | - [shank-macro-impl](./shank-macro-impl) implements and tests the _derive_ macros 19 | - [shank-idl](./shank-idl) processes files of a crate in order to discover _shank_ macros 20 | annotations and convert annotated types into an [solita](https://github.com/metaplex-foundation/solita) compatible IDL 21 | - [shank-render](./shank-render) generates Rust `impl` blocks from specific annotations like 22 | account `seeds` 23 | 24 | ## Development 25 | 26 | Fork the repo makes some changes and make sure that all is dandy by running `cargo test`. Then 27 | provide a pull request. 28 | 29 | If you are a contributor with access to publish to crates.io do the below in order to publish a 30 | new version. NOTE that this only works from the _master_ branch and should be performed _after_ 31 | merging a PR into master. 32 | 33 | ```sh 34 | cargo test && cargo release 35 | ``` 36 | 37 | The above runs all tests and dry-runs the release process. You should verify closely what it is 38 | about to do and then re-run the release command as shown below. 39 | 40 | ```sh 41 | cargo release --execute 42 | ``` 43 | 44 | ## LICENSE 45 | 46 | Apache-2.0 47 | -------------------------------------------------------------------------------- /diagnostics/pda/README.md: -------------------------------------------------------------------------------- 1 | # Shank PDA/Seeds impl Overhead 2 | 3 | Measuring effects on `.so` artifact caused by `impl` functions generated by _shank_ to derive 4 | _seeds_ and _PDA_s. 5 | 6 | ## Seeds/PDA Function Effects 7 | 8 | This diagnosis was performed on the [tictactoe workshop](https://github.com/thlorenz/tictactoe) 9 | which was hand rolling pda + seeds derivation. 10 | 11 | TODO: add a link to the relevant PR in tictactoe that changed that. 12 | 13 | According to this analysis the following was shown: 14 | 15 | - adding `impl` methods that aren't used in the code **does not add to the size of the `.so`** 16 | artifact 17 | - only when `impl` methods are invoked are they included in the binary and add to its size 18 | - due to _shank's_ efficient function reuse invoking its generated methods instead of the 19 | hand-rolled versions resulted in a smaller `.so` size (at least in this example) 20 | - final size ends up being smaller since _shank_ reuses the seed array creation code, vs. the 21 | hand rolled implementation inlined that code in two places. 22 | 23 | ### Details 24 | 25 | For the following seeds specification: 26 | 27 | ```rs 28 | #[derive(Debug, BorshSerialize, BorshDeserialize, ShankAccount)] 29 | #[seeds("tictactoe", game("The key of the game"))] 30 | pub struct Game { 31 | pub player_x: Pubkey, 32 | // [ .. ] 33 | } 34 | ``` 35 | 36 | _shank_ generates the following code: 37 | 38 | ```rs 39 | impl Game { 40 | /// Derives the seeds for this account. 41 | /// 42 | /// * **game**: The key of the game | [Pubkey] 43 | #[allow(unused, clippy::needless_lifetimes)] 44 | pub fn shank_seeds<'a>( 45 | game: &'a ::solana_program::pubkey::Pubkey, 46 | ) -> [&'a [u8]; 2usize] { 47 | [b"tictactoe", game.as_ref()] 48 | } 49 | /// Derives the seeds for this account allowing to provide a bump seed. 50 | /// 51 | /// * **game**: The key of the game | [Pubkey] 52 | /// * **bump**: the bump seed to pass when deriving the PDA 53 | #[allow(unused, clippy::needless_lifetimes)] 54 | pub fn shank_seeds_with_bump<'a>( 55 | game: &'a ::solana_program::pubkey::Pubkey, 56 | bump: &'a [u8; 1], 57 | ) -> [&'a [u8]; 3usize] { 58 | [b"tictactoe", game.as_ref(), bump] 59 | } 60 | /// Derives the PDA for this account. 61 | /// 62 | /// * **program_id**: The id of the program 63 | /// * **game**: The key of the game | [Pubkey] 64 | #[allow(unused)] 65 | pub fn shank_pda( 66 | program_id: &::solana_program::pubkey::Pubkey, 67 | game: &::solana_program::pubkey::Pubkey, 68 | ) -> (::solana_program::pubkey::Pubkey, u8) { 69 | let seeds = Self::shank_seeds(game); 70 | ::solana_program::pubkey::Pubkey::find_program_address(&seeds, program_id) 71 | } 72 | /// Derives the PDA for this account allowing to provide a bump seed. 73 | /// 74 | /// * **program_id**: The id of the program 75 | /// * **game**: The key of the game | [Pubkey] 76 | /// * **bump**: the bump seed to pass when deriving the PDA 77 | #[allow(unused)] 78 | pub fn shank_pda_with_bump( 79 | program_id: &::solana_program::pubkey::Pubkey, 80 | game: &::solana_program::pubkey::Pubkey, 81 | bump: u8, 82 | ) -> (::solana_program::pubkey::Pubkey, u8) { 83 | let bump_arg = &[bump]; 84 | let seeds = Self::shank_seeds_with_bump(game, bump_arg); 85 | ::solana_program::pubkey::Pubkey::find_program_address(&seeds, program_id) 86 | } 87 | } 88 | ``` 89 | 90 | The `.so` size for the different options were recorded + `cargo build-bpf --dump` results 91 | included (differences between dumps are best viewed via `vim -d dump1.txt dump2.txt`): 92 | 93 | - `.so` size with shank that did not generate any code: `90.704K` ([./dumps/01_shank-orig.txt](./dumps/01_shank-orig.txt)) 94 | - `.so` size when not specifying seeds: `90.704K` ([./dumps/02_shank-no-seeds.txt](./dumps/02_shank-no-seeds.txt)) 95 | - `.so` size when specifying seeds without using them: `90.704K` ([./dumps/03_shank-unused-seeds.txt](./dumps/03_shank-unused-seeds.txt)) 96 | - `.so` size when specifying seeds using `seeds_with_bump`: `90.752K` ([./dumps/04_shank-using-seeds-with-bump.txt](./dumps/04_shank-using-seeds-with-bump.txt)) 97 | - `.so` size when specifying seeds using `seeds_with_bump` and removing use of hand-rolled 98 | seeds assignment: `90.704K` ([./dumps/05_shank-using-seeds-with-bump-only.txt](./dumps/05_shank-using-seeds-with-bump-only.txt)) 99 | - `.so` size when using shank pda impl, keeping hand rolled use `90.960K` ([./dumps/06_shank-using-pda-additionally.txt](./dumps/06_shank-using-pda-additionally.txt)) 100 | - `.so` size when using shank pda impl only `90.672K` ([./dumps/07_shank-using-pda-only.txt](./dumps/07_shank-using-pda-only.txt)) 101 | -------------------------------------------------------------------------------- /release.toml: -------------------------------------------------------------------------------- 1 | pre-release-commit-message = "chore: release" 2 | tag-message = "{{tag_name}}" 3 | tag-name = "{{crate_name}}@v{{version}}" 4 | consolidate-commits = true 5 | allow-branch = ["master"] 6 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | imports_granularity = "Crate" 2 | max_width = 80 3 | -------------------------------------------------------------------------------- /shank-cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "shank-cli" 3 | description = "Shank CLI that extracts solita compatible IDL from your Rust Solana program code annotated with shank macro attributes" 4 | authors = ["Metaplex Maintainers "] 5 | repository = "https://github.com/metaplex-foundation/shank" 6 | license = "Apache-2.0" 7 | version = "0.4.3" 8 | edition = "2018" 9 | 10 | [[bin]] 11 | name = "shank" 12 | path = "src/bin/main.rs" 13 | 14 | 15 | [dependencies] 16 | anyhow = "1.0.48" 17 | clap = { version = "3.0.14", features = ["derive"] } 18 | fern = { version = "0.6.0", features = ["colored"] } 19 | log = "0.4.14" 20 | shank_idl = { version= "0.4.3", path = "../shank-idl" } 21 | -------------------------------------------------------------------------------- /shank-cli/README.md: -------------------------------------------------------------------------------- 1 | # shank 2 | 3 | [Shank](https://github.com/metaplex-foundation/shank) CLI that extracts IDL from your Rust Solana program code annotated with [shank macro 4 | attributes](../shank-macro/README.md). This IDL can then be fed to 5 | [solita](https://github.com/metaplex-foundation/solita) in order to generate low level 6 | TypeScript SDK for that particular Rust program. 7 | 8 | ![shank-logo](../shank/assets/shank-logo.gif) 9 | 10 | ## Installation 11 | 12 | ### Via Cargo 13 | 14 | ```sh 15 | cargo install shank-cli 16 | ``` 17 | 18 | ### Via Yarn/Npm 19 | 20 | _Coming soon ... _ 21 | 22 | ## Overview 23 | 24 | ``` 25 | USAGE: 26 | shank 27 | 28 | OPTIONS: 29 | -h, --help Print help information 30 | 31 | SUBCOMMANDS: 32 | help Print this message or the help of the given subcommand(s) 33 | idl 34 | ``` 35 | 36 | ## IDL Extraction 37 | 38 | ``` 39 | USAGE: 40 | shank idl [OPTIONS] 41 | 42 | OPTIONS: 43 | -h, --help Print help information 44 | -o, --out-dir Output directory for the IDL JSON [default: idl] 45 | -r, --crate-root Directory of program crate for which to generate the IDL 46 | ``` 47 | 48 | ## LICENSE 49 | 50 | Apache-2.0 51 | -------------------------------------------------------------------------------- /shank-cli/src/bin/main.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use clap::StructOpt; 3 | use shank_cli::Opts; 4 | 5 | use fern::colors::{Color, ColoredLevelConfig}; 6 | 7 | fn main() -> Result<()> { 8 | setup_logging(); 9 | shank_cli::entry(Opts::parse()) 10 | } 11 | 12 | fn setup_logging() { 13 | let colors = ColoredLevelConfig::new() 14 | .debug(Color::BrightBlue) 15 | .info(Color::BrightYellow); 16 | 17 | fern::Dispatch::new() 18 | .chain(std::io::stdout()) 19 | .format(move |out, message, record| { 20 | out.finish(format_args!( 21 | "shank {} {}", 22 | colors.color(record.level()), 23 | message 24 | )) 25 | }) 26 | .level(log::LevelFilter::Debug) 27 | .apply() 28 | .unwrap(); 29 | } 30 | -------------------------------------------------------------------------------- /shank-cli/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fs::{self, File}, 3 | io::Write, 4 | path::{Path, PathBuf}, 5 | }; 6 | 7 | use anyhow::{anyhow, format_err, Result}; 8 | use clap::Parser; 9 | use log::{debug, info}; 10 | use shank_idl::{extract_idl, manifest::Manifest, ParseIdlOpts}; 11 | 12 | pub const VERSION: &str = env!("CARGO_PKG_VERSION"); 13 | 14 | #[derive(Debug, Parser)] 15 | #[clap(version = VERSION)] 16 | pub struct Opts { 17 | #[clap(subcommand)] 18 | pub command: Command, 19 | } 20 | 21 | #[derive(Debug, Parser)] 22 | pub enum Command { 23 | Idl { 24 | /// Output directory for the IDL JSON. 25 | #[clap(short, long, default_value = "idl")] 26 | out_dir: String, 27 | 28 | /// Output filename for the IDL JSON [default: .json, 29 | /// e.g. "my_program.json"] 30 | #[clap(long)] 31 | out_filename: Option, 32 | 33 | /// Directory of program crate for which to generate the IDL. 34 | #[clap(short = 'r', long)] 35 | crate_root: Option, 36 | 37 | /// Manually specify and override the address in the IDL 38 | #[clap(short = 'p', long)] 39 | program_id: Option, 40 | }, 41 | } 42 | 43 | pub fn entry(opts: Opts) -> Result<()> { 44 | match opts.command { 45 | Command::Idl { 46 | out_dir, 47 | out_filename, 48 | crate_root, 49 | program_id, 50 | } => idl(out_dir, out_filename, crate_root, program_id), 51 | } 52 | } 53 | 54 | pub fn try_resolve_path(p: Option, label: &str) -> Result { 55 | let p = match p { 56 | Some(crate_root) => Ok(Path::new(&crate_root).to_path_buf()), 57 | None => { 58 | debug!("No {} provided, assuming in current dir", label); 59 | std::env::current_dir() 60 | } 61 | }?; 62 | 63 | let p = if p.is_absolute() { 64 | Ok(p) 65 | } else { 66 | debug!("{} is relative, resolving from current dir", label); 67 | std::env::current_dir().map(|x| x.join(p)) 68 | }?; 69 | 70 | Ok(p) 71 | } 72 | 73 | pub fn idl( 74 | out_dir: String, 75 | out_filename: Option, 76 | crate_root: Option, 77 | program_id: Option, 78 | ) -> Result<()> { 79 | // Resolve input and output directories 80 | let crate_root = try_resolve_path(crate_root, "crate_root")?; 81 | let out_dir = try_resolve_path(Some(out_dir), "out_dir")?; 82 | fs::create_dir_all(&out_dir).map_err(|err| { 83 | format_err!( 84 | "Unable to create out_dir ({}), {}", 85 | &out_dir.display(), 86 | err 87 | ) 88 | })?; 89 | 90 | // Resolve info about lib for which we generate IDL 91 | let cargo_toml = crate_root.join("Cargo.toml"); 92 | if !cargo_toml.exists() { 93 | return Err(anyhow!( 94 | "Did not find Cargo.toml at the path: {}", 95 | crate_root.display() 96 | )); 97 | } 98 | let manifest = Manifest::from_path(&cargo_toml)?; 99 | let lib_rel_path = manifest 100 | .lib_rel_path() 101 | .ok_or(anyhow!("Program needs to be a lib"))?; 102 | 103 | let lib_full_path_str = crate_root.join(lib_rel_path); 104 | let lib_full_path = 105 | lib_full_path_str.to_str().ok_or(anyhow!("Invalid Path"))?; 106 | 107 | // Extract IDL and convert to JSON 108 | let opts = ParseIdlOpts { 109 | program_address_override: program_id, 110 | ..ParseIdlOpts::default() 111 | }; 112 | let idl = extract_idl(lib_full_path, opts)? 113 | .ok_or(anyhow!("No IDL could be extracted"))?; 114 | let idl_json = idl.try_into_json()?; 115 | 116 | // Write to JSON file 117 | let out_filename = if let Some(out_filename) = out_filename { 118 | out_filename 119 | } else { 120 | format!("{}.json", manifest.lib_name()?) 121 | }; 122 | let idl_json_path = out_dir.join(out_filename); 123 | let mut idl_json_file = File::create(&idl_json_path)?; 124 | info!("Writing IDL to {}", &idl_json_path.display()); 125 | 126 | idl_json_file.write_all(idl_json.as_bytes())?; 127 | 128 | Ok(()) 129 | } 130 | -------------------------------------------------------------------------------- /shank-idl/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "shank_idl" 3 | version = "0.4.3" 4 | description = "Converts information extracted via shank derive macros to a solita compatible IDL." 5 | authors = ["Metaplex Maintainers "] 6 | repository = "https://github.com/metaplex-foundation/shank" 7 | license = "Apache-2.0" 8 | edition = "2018" 9 | 10 | [dependencies] 11 | anyhow = "1.0.48" 12 | cargo_toml = "0.17" 13 | heck = "0.3.3" 14 | serde = { version = "1.0.130", features = ["derive"] } 15 | serde_json = "1.0.72" 16 | shank_macro_impl = { version = "0.4.3", path = "../shank-macro-impl" } 17 | shellexpand = "2.1.0" 18 | -------------------------------------------------------------------------------- /shank-idl/README.md: -------------------------------------------------------------------------------- 1 | # shank-macro-impl 2 | 3 | Converts information extracted via [shank](https://github.com/metaplex-foundation/shank) _derive_ macros to a [solita](https://github.com/metaplex-foundation/solita) compatible IDL. 4 | 5 | ## LICENSE 6 | 7 | Apache-2.0 8 | -------------------------------------------------------------------------------- /shank-idl/rustfmt.toml: -------------------------------------------------------------------------------- 1 | imports_granularity = "Crate" 2 | max_width = 80 3 | -------------------------------------------------------------------------------- /shank-idl/src/file.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | 3 | use std::{ 4 | convert::{TryFrom, TryInto}, 5 | path::Path, 6 | }; 7 | 8 | use crate::{ 9 | idl::{Idl, IdlConst, IdlEvent, IdlState}, 10 | idl_error_code::IdlErrorCode, 11 | idl_instruction::{IdlInstruction, IdlInstructions}, 12 | idl_metadata::IdlMetadata, 13 | idl_type_definition::IdlTypeDefinition, 14 | }; 15 | use shank_macro_impl::{ 16 | account::extract_account_structs, 17 | converters::parse_error_into, 18 | custom_type::{CustomEnum, CustomStruct, DetectCustomTypeConfig}, 19 | error::extract_this_errors, 20 | instruction::extract_instruction_enums, 21 | krate::CrateContext, 22 | macros::ProgramId, 23 | }; 24 | 25 | // ----------------- 26 | // ParseIdlConfig 27 | // ----------------- 28 | #[derive(Debug)] 29 | pub struct ParseIdlConfig { 30 | pub program_version: String, 31 | pub program_name: String, 32 | pub detect_custom_struct: DetectCustomTypeConfig, 33 | pub require_program_address: bool, 34 | pub program_address_override: Option, 35 | } 36 | 37 | impl Default for ParseIdlConfig { 38 | fn default() -> Self { 39 | Self { 40 | program_version: Default::default(), 41 | program_name: Default::default(), 42 | detect_custom_struct: Default::default(), 43 | require_program_address: true, 44 | program_address_override: None, 45 | } 46 | } 47 | } 48 | 49 | impl ParseIdlConfig { 50 | pub fn optional_program_address() -> Self { 51 | Self { 52 | require_program_address: false, 53 | ..Self::default() 54 | } 55 | } 56 | } 57 | 58 | // ----------------- 59 | // Parse File 60 | // ----------------- 61 | 62 | /// Parse an entire interface file. 63 | pub fn parse_file( 64 | filename: impl AsRef, 65 | config: &ParseIdlConfig, 66 | ) -> Result> { 67 | let ctx = CrateContext::parse(filename)?; 68 | 69 | let constants = constants(&ctx)?; 70 | let instructions = instructions(&ctx)?; 71 | let state = state(&ctx)?; 72 | let accounts = accounts(&ctx)?; 73 | let types = types(&ctx, &config.detect_custom_struct)?; 74 | let events = events(&ctx)?; 75 | let errors = errors(&ctx)?; 76 | let metadata = metadata( 77 | &ctx, 78 | config.require_program_address, 79 | config.program_address_override.as_ref(), 80 | )?; 81 | 82 | let idl = Idl { 83 | version: config.program_version.to_string(), 84 | name: config.program_name.to_string(), 85 | constants, 86 | instructions, 87 | state, 88 | accounts, 89 | types, 90 | events, 91 | errors, 92 | metadata, 93 | }; 94 | 95 | Ok(Some(idl)) 96 | } 97 | 98 | fn accounts(ctx: &CrateContext) -> Result> { 99 | let account_structs = extract_account_structs(ctx.structs())?; 100 | 101 | let mut accounts: Vec = Vec::new(); 102 | for strct in account_structs { 103 | let idl_def: IdlTypeDefinition = strct.try_into()?; 104 | accounts.push(idl_def); 105 | } 106 | Ok(accounts) 107 | } 108 | 109 | fn instructions(ctx: &CrateContext) -> Result> { 110 | let instruction_enums = 111 | extract_instruction_enums(ctx.enums()).map_err(parse_error_into)?; 112 | 113 | let mut instructions: Vec = Vec::new(); 114 | // TODO(thlorenz): Should we enforce only one Instruction Enum Arg? 115 | // TODO(thlorenz): Should unfold that only arg? 116 | // TODO(thlorenz): Better way to combine those if we don't do the above. 117 | 118 | for ix in instruction_enums { 119 | let idl_instructions: IdlInstructions = ix.try_into()?; 120 | for ix in idl_instructions.0 { 121 | instructions.push(ix); 122 | } 123 | } 124 | Ok(instructions) 125 | } 126 | 127 | fn constants(_ctx: &CrateContext) -> Result> { 128 | // TODO(thlorenz): Implement 129 | let constants: Vec = Vec::new(); 130 | Ok(constants) 131 | } 132 | 133 | fn state(_ctx: &CrateContext) -> Result> { 134 | // TODO(thlorenz): Implement 135 | Ok(None) 136 | } 137 | 138 | fn types( 139 | ctx: &CrateContext, 140 | detect_custom_type: &DetectCustomTypeConfig, 141 | ) -> Result> { 142 | let custom_structs = ctx 143 | .structs() 144 | .filter(|x| detect_custom_type.are_custom_type_attrs(&x.attrs)) 145 | .map(|x| CustomStruct::try_from(x).map_err(parse_error_into)) 146 | .collect::>>()?; 147 | 148 | let custom_enums = ctx 149 | .enums() 150 | .filter(|x| detect_custom_type.are_custom_type_attrs(&x.attrs)) 151 | .map(|x| CustomEnum::try_from(x).map_err(parse_error_into)) 152 | .collect::>>()?; 153 | 154 | let types = custom_structs 155 | .into_iter() 156 | .map(IdlTypeDefinition::try_from) 157 | .chain(custom_enums.into_iter().map(IdlTypeDefinition::try_from)) 158 | .collect::>>()?; 159 | 160 | Ok(types) 161 | } 162 | 163 | fn metadata( 164 | ctx: &CrateContext, 165 | require_program_address: bool, 166 | program_address_override: Option<&String>, 167 | ) -> Result { 168 | let macros: Vec<_> = ctx.macros().cloned().collect(); 169 | let address = if let Some(program_address) = program_address_override { 170 | Ok(Some(program_address.clone())) 171 | } else { 172 | match ProgramId::try_from(¯os[..]) { 173 | Ok(ProgramId { id }) => Ok(Some(id)), 174 | Err(err) if require_program_address => Err(err), 175 | Err(_) => Ok(None), 176 | } 177 | }?; 178 | Ok(IdlMetadata { 179 | origin: "shank".to_string(), 180 | address, 181 | }) 182 | } 183 | 184 | fn events(_ctx: &CrateContext) -> Result>> { 185 | // TODO(thlorenz): Implement 186 | Ok(None) 187 | } 188 | 189 | fn errors(ctx: &CrateContext) -> Result>> { 190 | let program_errors = extract_this_errors(ctx.enums())?; 191 | if program_errors.is_empty() { 192 | Ok(None) 193 | } else { 194 | let error_codes = program_errors 195 | .into_iter() 196 | .map(IdlErrorCode::from) 197 | .collect::>(); 198 | Ok(Some(error_codes)) 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /shank-idl/src/idl.rs: -------------------------------------------------------------------------------- 1 | /// NOTE: this was lifted from https://github.com/project-serum/anchor/blob/1a2fd38451b36a569287eb9794ec10e51675789e/lang/syn/src/idl/mod.rs 2 | /// We want to add non-anchor specific extensions to this IDL which is why we don't depend on the 3 | /// anchor crate instead. 4 | use serde::{Deserialize, Serialize}; 5 | 6 | use crate::{ 7 | idl_error_code::IdlErrorCode, idl_instruction::IdlInstruction, 8 | idl_metadata::IdlMetadata, 9 | }; 10 | 11 | use super::{idl_type::IdlType, idl_type_definition::IdlTypeDefinition}; 12 | use anyhow::{anyhow, Result}; 13 | 14 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] 15 | pub struct Idl { 16 | pub version: String, 17 | pub name: String, 18 | #[serde(skip_serializing_if = "Vec::is_empty", default)] 19 | pub constants: Vec, 20 | pub instructions: Vec, 21 | #[serde(skip_serializing_if = "Option::is_none", default)] 22 | pub state: Option, 23 | #[serde(skip_serializing_if = "Vec::is_empty", default)] 24 | pub accounts: Vec, 25 | #[serde(skip_serializing_if = "Vec::is_empty", default)] 26 | pub types: Vec, 27 | #[serde(skip_serializing_if = "Option::is_none", default)] 28 | pub events: Option>, 29 | #[serde(skip_serializing_if = "Option::is_none", default)] 30 | pub errors: Option>, 31 | pub metadata: IdlMetadata, 32 | } 33 | 34 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] 35 | pub struct IdlConst { 36 | pub name: String, 37 | #[serde(rename = "type")] 38 | pub ty: IdlType, 39 | pub value: String, 40 | } 41 | 42 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] 43 | pub struct IdlState { 44 | #[serde(rename = "struct")] 45 | pub strct: IdlTypeDefinition, 46 | pub methods: Vec, 47 | } 48 | 49 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] 50 | pub struct IdlEvent { 51 | pub name: String, 52 | pub fields: Vec, 53 | } 54 | 55 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] 56 | pub struct IdlEventField { 57 | pub name: String, 58 | #[serde(rename = "type")] 59 | pub ty: IdlType, 60 | pub index: bool, 61 | } 62 | 63 | impl Idl { 64 | pub fn try_into_json(&self) -> Result { 65 | serde_json::to_string_pretty(&self) 66 | .map_err(|err| anyhow!("Failed to convert to JSON. {}", err)) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /shank-idl/src/idl_error_code.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use shank_macro_impl::error::ProgramError; 3 | 4 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] 5 | pub struct IdlErrorCode { 6 | pub code: u32, 7 | pub name: String, 8 | #[serde(skip_serializing_if = "Option::is_none", default)] 9 | pub msg: Option, 10 | } 11 | 12 | impl From for IdlErrorCode { 13 | fn from(program_error: ProgramError) -> Self { 14 | let ProgramError { 15 | name, desc, code, .. 16 | } = program_error; 17 | Self { 18 | code, 19 | name, 20 | msg: Some(desc), 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /shank-idl/src/idl_field.rs: -------------------------------------------------------------------------------- 1 | use heck::MixedCase; 2 | use std::convert::{TryFrom, TryInto}; 3 | 4 | use serde::{Deserialize, Serialize}; 5 | use shank_macro_impl::parsed_struct::StructField; 6 | 7 | use crate::idl_type::IdlType; 8 | use anyhow::{Error, Result}; 9 | 10 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] 11 | pub struct IdlField { 12 | pub name: String, 13 | #[serde(rename = "type")] 14 | pub ty: IdlType, 15 | #[serde(skip_serializing_if = "Option::is_none")] 16 | pub attrs: Option>, 17 | } 18 | 19 | impl TryFrom for IdlField { 20 | type Error = Error; 21 | fn try_from(field: StructField) -> Result { 22 | let ty: IdlType = if let Some(override_type) = field.type_override() { 23 | override_type.clone().try_into()? 24 | } else { 25 | field.rust_type.try_into()? 26 | }; 27 | 28 | let attrs = field 29 | .attrs 30 | .iter() 31 | .map(Into::::into) 32 | .collect::>(); 33 | 34 | let attrs = if attrs.is_empty() { None } else { Some(attrs) }; 35 | Ok(Self { 36 | name: field.ident.to_string().to_mixed_case(), 37 | ty, 38 | attrs, 39 | }) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /shank-idl/src/idl_metadata.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] 4 | pub struct IdlMetadata { 5 | /// shank 6 | pub origin: String, 7 | #[serde(skip_serializing_if = "Option::is_none", default)] 8 | pub address: Option, 9 | } 10 | -------------------------------------------------------------------------------- /shank-idl/src/idl_type_definition.rs: -------------------------------------------------------------------------------- 1 | use std::convert::{TryFrom, TryInto}; 2 | 3 | use anyhow::{Error, Result}; 4 | use serde::{Deserialize, Serialize}; 5 | use shank_macro_impl::{ 6 | custom_type::{CustomEnum, CustomStruct}, 7 | parsed_enum::ParsedEnum, 8 | parsed_struct::ParsedStruct, 9 | }; 10 | 11 | use crate::{idl_field::IdlField, idl_variant::IdlEnumVariant}; 12 | 13 | // ----------------- 14 | // IdlTypeDefinitionTy 15 | // ----------------- 16 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] 17 | #[serde(rename_all = "lowercase", tag = "kind")] 18 | pub enum IdlTypeDefinitionTy { 19 | Struct { fields: Vec }, 20 | Enum { variants: Vec }, 21 | } 22 | 23 | impl TryFrom for IdlTypeDefinitionTy { 24 | type Error = Error; 25 | 26 | fn try_from(strct: ParsedStruct) -> Result { 27 | let fields = strct 28 | .fields 29 | .into_iter() 30 | .map(IdlField::try_from) 31 | .collect::>>()?; 32 | 33 | Ok(Self::Struct { fields }) 34 | } 35 | } 36 | 37 | impl TryFrom for IdlTypeDefinitionTy { 38 | type Error = Error; 39 | 40 | fn try_from(enm: ParsedEnum) -> Result { 41 | let variants = enm 42 | .variants 43 | .into_iter() 44 | .map(IdlEnumVariant::try_from) 45 | .collect::>>()?; 46 | 47 | Ok(Self::Enum { variants }) 48 | } 49 | } 50 | 51 | // ----------------- 52 | // IdlTypeDefinition 53 | // ----------------- 54 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] 55 | pub struct IdlTypeDefinition { 56 | pub name: String, 57 | #[serde(rename = "type")] 58 | pub ty: IdlTypeDefinitionTy, 59 | } 60 | 61 | impl TryFrom for IdlTypeDefinition { 62 | type Error = Error; 63 | 64 | fn try_from(strct: ParsedStruct) -> Result { 65 | let name = strct.ident.to_string(); 66 | let ty: IdlTypeDefinitionTy = strct.try_into()?; 67 | Ok(Self { ty, name }) 68 | } 69 | } 70 | 71 | impl TryFrom for IdlTypeDefinition { 72 | type Error = Error; 73 | 74 | fn try_from(strct: CustomStruct) -> Result { 75 | let name = strct.ident.to_string(); 76 | let ty: IdlTypeDefinitionTy = strct.0.try_into()?; 77 | Ok(Self { ty, name }) 78 | } 79 | } 80 | 81 | impl TryFrom for IdlTypeDefinition { 82 | type Error = Error; 83 | 84 | fn try_from(enm: CustomEnum) -> Result { 85 | let name = enm.ident.to_string(); 86 | let ty: IdlTypeDefinitionTy = enm.0.try_into()?; 87 | Ok(Self { ty, name }) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /shank-idl/src/idl_variant.rs: -------------------------------------------------------------------------------- 1 | use std::convert::{TryFrom, TryInto}; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | use shank_macro_impl::parsed_enum::{ 5 | ParsedEnumVariant, ParsedEnumVariantField, 6 | }; 7 | 8 | use crate::{idl_field::IdlField, idl_type::IdlType}; 9 | use anyhow::{Error, Result}; 10 | 11 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] 12 | #[serde(untagged)] 13 | pub enum EnumFields { 14 | Named(Vec), 15 | Tuple(Vec), 16 | } 17 | 18 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] 19 | pub struct IdlEnumVariant { 20 | pub name: String, 21 | #[serde(skip_serializing_if = "Option::is_none", default)] 22 | pub fields: Option, 23 | } 24 | 25 | impl TryFrom for IdlType { 26 | type Error = Error; 27 | fn try_from(field: ParsedEnumVariantField) -> Result { 28 | field.rust_type.try_into() 29 | } 30 | } 31 | 32 | impl TryFrom for IdlEnumVariant { 33 | type Error = Error; 34 | 35 | fn try_from(variant: ParsedEnumVariant) -> Result { 36 | let mut named_fields = Vec::new(); 37 | let mut tuple_fields = Vec::new(); 38 | 39 | for field in &variant.fields { 40 | let ty = IdlType::try_from(field.rust_type.clone())?; 41 | match &field.ident { 42 | Some(name) => named_fields.push(IdlField { 43 | name: name.to_string(), 44 | ty, 45 | attrs: None, 46 | }), 47 | None => tuple_fields.push(ty), 48 | } 49 | } 50 | 51 | assert!(named_fields.is_empty() || tuple_fields.is_empty(), "should either have named or tuple fields on a variant, but never both"); 52 | let fields = if !named_fields.is_empty() { 53 | Some(EnumFields::Named(named_fields)) 54 | } else if !tuple_fields.is_empty() { 55 | Some(EnumFields::Tuple(tuple_fields)) 56 | } else { 57 | None 58 | }; 59 | 60 | Ok(Self { 61 | name: variant.ident.to_string(), 62 | fields, 63 | }) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /shank-idl/src/lib.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Result}; 2 | use idl::Idl; 3 | use manifest::Manifest; 4 | use shank_macro_impl::custom_type::DetectCustomTypeConfig; 5 | 6 | use std::path::PathBuf; 7 | 8 | mod file; 9 | pub mod idl; 10 | mod idl_error_code; 11 | mod idl_field; 12 | mod idl_instruction; 13 | mod idl_metadata; 14 | mod idl_type; 15 | mod idl_type_definition; 16 | mod idl_variant; 17 | pub mod manifest; 18 | 19 | pub use file::*; 20 | 21 | // ----------------- 22 | // ParseIdlOpts 23 | // ----------------- 24 | pub struct ParseIdlOpts { 25 | pub detect_custom_struct: DetectCustomTypeConfig, 26 | pub require_program_address: bool, 27 | pub program_address_override: Option, 28 | } 29 | 30 | impl Default for ParseIdlOpts { 31 | fn default() -> Self { 32 | Self { 33 | detect_custom_struct: Default::default(), 34 | require_program_address: true, 35 | program_address_override: None, 36 | } 37 | } 38 | } 39 | 40 | // ----------------- 41 | // extract_idl 42 | // ----------------- 43 | pub fn extract_idl(file: &str, opts: ParseIdlOpts) -> Result> { 44 | let file = shellexpand::tilde(file); 45 | let manifest_from_path = 46 | std::env::current_dir()?.join(PathBuf::from(&*file).parent().unwrap()); 47 | let cargo = Manifest::discover_from_path(manifest_from_path)? 48 | .ok_or_else(|| anyhow!("Cargo.toml not found"))?; 49 | let program_name = cargo 50 | .lib_name() 51 | .map_err(|err| anyhow!("Cargo.toml is missing lib name. {}", err))?; 52 | file::parse_file( 53 | &*file, 54 | &ParseIdlConfig { 55 | program_name, 56 | program_version: cargo.version(), 57 | detect_custom_struct: opts.detect_custom_struct, 58 | require_program_address: opts.require_program_address, 59 | program_address_override: opts.program_address_override, 60 | }, 61 | ) 62 | } 63 | -------------------------------------------------------------------------------- /shank-idl/src/manifest.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Result}; 2 | use cargo_toml; 3 | use std::{ 4 | fs, 5 | ops::Deref, 6 | path::{Path, PathBuf}, 7 | }; 8 | 9 | use heck::SnakeCase; 10 | 11 | // ----------------- 12 | // WithPath 13 | // ----------------- 14 | pub struct WithPath { 15 | inner: T, 16 | path: PathBuf, 17 | } 18 | 19 | // TODO(thlorenz): figure out if we'll actually need this 20 | #[allow(unused)] 21 | impl WithPath { 22 | pub fn new(inner: T, path: PathBuf) -> Self { 23 | Self { inner, path } 24 | } 25 | 26 | pub fn path(&self) -> &PathBuf { 27 | &self.path 28 | } 29 | 30 | pub fn into_inner(self) -> T { 31 | self.inner 32 | } 33 | } 34 | 35 | impl std::convert::AsRef for WithPath { 36 | fn as_ref(&self) -> &T { 37 | &self.inner 38 | } 39 | } 40 | 41 | impl std::ops::Deref for WithPath { 42 | type Target = T; 43 | fn deref(&self) -> &Self::Target { 44 | &self.inner 45 | } 46 | } 47 | 48 | impl std::ops::DerefMut for WithPath { 49 | fn deref_mut(&mut self) -> &mut Self::Target { 50 | &mut self.inner 51 | } 52 | } 53 | 54 | // ----------------- 55 | // Manifest 56 | // ----------------- 57 | #[derive(Debug, Clone, PartialEq)] 58 | pub struct Manifest(cargo_toml::Manifest); 59 | 60 | impl Manifest { 61 | pub fn from_path(p: impl AsRef) -> Result { 62 | cargo_toml::Manifest::from_path(p) 63 | .map(Manifest) 64 | .map_err(Into::into) 65 | } 66 | 67 | pub fn lib_rel_path(&self) -> Option { 68 | self.lib.as_ref().and_then(|x| x.path.clone()) 69 | } 70 | 71 | pub fn lib_name(&self) -> Result { 72 | if self.lib.is_some() && self.lib.as_ref().unwrap().name.is_some() { 73 | Ok(self 74 | .lib 75 | .as_ref() 76 | .unwrap() 77 | .name 78 | .as_ref() 79 | .unwrap() 80 | .to_string() 81 | .to_snake_case()) 82 | } else { 83 | Ok(self 84 | .package 85 | .as_ref() 86 | .ok_or_else(|| anyhow!("package section not provided"))? 87 | .name 88 | .to_string() 89 | .to_snake_case()) 90 | } 91 | } 92 | 93 | pub fn version(&self) -> String { 94 | match &self.package { 95 | Some(package) => package.version.get().unwrap().clone(), 96 | _ => "0.0.0".to_string(), 97 | } 98 | } 99 | 100 | // Climbs each parent directory from a given starting directory until we find a Cargo.toml. 101 | pub fn discover_from_path( 102 | start_from: PathBuf, 103 | ) -> Result>> { 104 | let mut cwd_opt = Some(start_from.as_path()); 105 | 106 | while let Some(cwd) = cwd_opt { 107 | for f in fs::read_dir(cwd)? { 108 | let p = f?.path(); 109 | if let Some(filename) = p.file_name() { 110 | if filename.to_str() == Some("Cargo.toml") { 111 | let m = WithPath::new(Manifest::from_path(&p)?, p); 112 | return Ok(Some(m)); 113 | } 114 | } 115 | } 116 | 117 | // Not found. Go up a directory level. 118 | cwd_opt = cwd.parent(); 119 | } 120 | 121 | Ok(None) 122 | } 123 | } 124 | 125 | impl Deref for Manifest { 126 | type Target = cargo_toml::Manifest; 127 | 128 | fn deref(&self) -> &Self::Target { 129 | &self.0 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /shank-idl/tests/accounts.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fs::{read_to_string, File}, 3 | io::Write, 4 | path::{Path, PathBuf}, 5 | }; 6 | 7 | use shank_idl::{ 8 | extract_idl, idl::Idl, parse_file, ParseIdlConfig, ParseIdlOpts, 9 | }; 10 | 11 | fn fixtures_dir() -> PathBuf { 12 | let root_dir = Path::new(env!("CARGO_MANIFEST_DIR")); 13 | root_dir.join("tests").join("fixtures").join("accounts") 14 | } 15 | 16 | // TODO(thlorenz): Should live in test util crate 17 | pub fn check_or_update_idl(idl: &Idl, json_path: &str) { 18 | let expected_json_file = fixtures_dir().join(json_path); 19 | let expected_json = 20 | read_to_string(&expected_json_file).expect("Unable to read json file"); 21 | let expected_idl: Idl = serde_json::from_str(&expected_json) 22 | .expect("Unable to parse expected json"); 23 | 24 | if std::env::var("UPDATE_IDL").is_ok() { 25 | let idl_json = idl.try_into_json().unwrap(); 26 | 27 | let mut idl_json_file = File::create(&expected_json_file) 28 | .expect("Unable to create JSON file"); 29 | 30 | idl_json_file 31 | .write_all(idl_json.as_bytes()) 32 | .expect("Unable to write file"); 33 | } else { 34 | assert_eq!(idl, &expected_idl); 35 | } 36 | } 37 | 38 | #[test] 39 | fn account_from_single_file() { 40 | let file = fixtures_dir().join("single_file").join("account.rs"); 41 | let idl = parse_file(file, &ParseIdlConfig::optional_program_address()) 42 | .expect("Parsing should not fail") 43 | .expect("File contains IDL"); 44 | 45 | check_or_update_idl(&idl, "single_file/account.json"); 46 | } 47 | 48 | #[test] 49 | fn account_from_single_file_complex_types() { 50 | let file = fixtures_dir().join("single_file").join("complex_types.rs"); 51 | let idl = parse_file(file, &ParseIdlConfig::optional_program_address()) 52 | .expect("Parsing should not fail") 53 | .expect("File contains IDL"); 54 | 55 | // eprintln!("{}", idl.try_into_json().unwrap()); 56 | check_or_update_idl(&idl, "single_file/complex_types.json"); 57 | } 58 | 59 | #[test] 60 | fn account_from_single_file_padding() { 61 | let file = fixtures_dir().join("single_file").join("padding.rs"); 62 | let idl = parse_file(file, &ParseIdlConfig::optional_program_address()) 63 | .expect("Parsing should not fail") 64 | .expect("File contains IDL"); 65 | 66 | check_or_update_idl(&idl, "single_file/padding.json"); 67 | } 68 | 69 | #[test] 70 | fn account_from_single_file_idl_type() { 71 | let file = fixtures_dir().join("single_file").join("idl_type.rs"); 72 | let idl = parse_file(file, &ParseIdlConfig::optional_program_address()) 73 | .expect("Parsing should not fail") 74 | .expect("File contains IDL"); 75 | 76 | check_or_update_idl(&idl, "single_file/idl_type.json"); 77 | } 78 | 79 | #[test] 80 | fn account_from_crate() { 81 | let file = fixtures_dir() 82 | .join("sample_crate") 83 | .join("src") 84 | .join("lib.rs"); 85 | let idl = extract_idl( 86 | file.to_str().unwrap(), 87 | ParseIdlOpts { 88 | require_program_address: false, 89 | ..Default::default() 90 | }, 91 | ) 92 | .expect("Parsing should not fail") 93 | .expect("File contains IDL"); 94 | 95 | check_or_update_idl(&idl, "sample_crate/idl.json"); 96 | } 97 | -------------------------------------------------------------------------------- /shank-idl/tests/errors.rs: -------------------------------------------------------------------------------- 1 | use std::path::{Path, PathBuf}; 2 | 3 | use shank_idl::{idl::Idl, parse_file, ParseIdlConfig}; 4 | 5 | fn fixtures_dir() -> PathBuf { 6 | let root_dir = Path::new(env!("CARGO_MANIFEST_DIR")); 7 | root_dir.join("tests").join("fixtures").join("errors") 8 | } 9 | 10 | #[test] 11 | fn errors_this_error() { 12 | let file = fixtures_dir().join("this_error.rs"); 13 | let idl = parse_file(file, &ParseIdlConfig::optional_program_address()) 14 | .expect("Parsing should not fail") 15 | .expect("File contains IDL"); 16 | 17 | // eprintln!("{}", idl.try_into_json().unwrap()); 18 | 19 | let expected_idl: Idl = 20 | serde_json::from_str(include_str!("./fixtures/errors/this_error.json")) 21 | .unwrap(); 22 | 23 | assert_eq!(idl, expected_idl); 24 | } 25 | 26 | #[test] 27 | fn errors_this_error_custom_codes() { 28 | let file = fixtures_dir().join("this_error_custom_codes.rs"); 29 | let idl = parse_file(file, &ParseIdlConfig::optional_program_address()) 30 | .expect("Parsing should not fail") 31 | .expect("File contains IDL"); 32 | 33 | let expected_idl: Idl = serde_json::from_str(include_str!( 34 | "./fixtures/errors/this_error_custom_codes.json" 35 | )) 36 | .unwrap(); 37 | 38 | assert_eq!(idl, expected_idl); 39 | } 40 | -------------------------------------------------------------------------------- /shank-idl/tests/fixtures/accounts/sample_crate/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sample_crate" 3 | version = "0.1.0" 4 | edition = "2018" 5 | -------------------------------------------------------------------------------- /shank-idl/tests/fixtures/accounts/sample_crate/idl.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.1.0", 3 | "name": "sample_crate", 4 | "instructions": [], 5 | "accounts": [ 6 | { 7 | "name": "AuctionHouse", 8 | "type": { 9 | "kind": "struct", 10 | "fields": [ 11 | { 12 | "name": "auctionHouseFeeAccount", 13 | "type": "publicKey" 14 | }, 15 | { 16 | "name": "auctionHouseTreasury", 17 | "type": "publicKey" 18 | }, 19 | { 20 | "name": "treasuryWithdrawalDestination", 21 | "type": "publicKey" 22 | }, 23 | { 24 | "name": "feeWithdrawalDestination", 25 | "type": "publicKey" 26 | }, 27 | { 28 | "name": "treasuryMint", 29 | "type": "publicKey" 30 | }, 31 | { 32 | "name": "authority", 33 | "type": "publicKey" 34 | }, 35 | { 36 | "name": "creator", 37 | "type": "publicKey" 38 | }, 39 | { 40 | "name": "bump", 41 | "type": "u8" 42 | }, 43 | { 44 | "name": "treasuryBump", 45 | "type": "u8" 46 | }, 47 | { 48 | "name": "feePayerBump", 49 | "type": "u8" 50 | }, 51 | { 52 | "name": "sellerFeeBasisPoints", 53 | "type": "u16" 54 | }, 55 | { 56 | "name": "requiresSignOff", 57 | "type": "bool" 58 | }, 59 | { 60 | "name": "canChangeSalePrice", 61 | "type": "bool" 62 | } 63 | ] 64 | } 65 | }, 66 | { 67 | "name": "Metadata", 68 | "type": { 69 | "kind": "struct", 70 | "fields": [ 71 | { 72 | "name": "key", 73 | "type": { 74 | "defined": "Key" 75 | } 76 | }, 77 | { 78 | "name": "updateAuthority", 79 | "type": "publicKey" 80 | }, 81 | { 82 | "name": "mint", 83 | "type": "publicKey" 84 | }, 85 | { 86 | "name": "data", 87 | "type": { 88 | "defined": "Data" 89 | } 90 | }, 91 | { 92 | "name": "primarySaleHappened", 93 | "type": "bool" 94 | }, 95 | { 96 | "name": "isMutable", 97 | "type": "bool" 98 | }, 99 | { 100 | "name": "editionNonce", 101 | "type": { 102 | "option": "u8" 103 | } 104 | }, 105 | { 106 | "name": "tokenStandard", 107 | "type": { 108 | "option": { 109 | "defined": "TokenStandard" 110 | } 111 | } 112 | }, 113 | { 114 | "name": "collection", 115 | "type": { 116 | "option": { 117 | "defined": "Collection" 118 | } 119 | } 120 | }, 121 | { 122 | "name": "uses", 123 | "type": { 124 | "option": { 125 | "defined": "Uses" 126 | } 127 | } 128 | } 129 | ] 130 | } 131 | } 132 | ], 133 | "metadata": { 134 | "origin": "shank" 135 | } 136 | } 137 | 138 | -------------------------------------------------------------------------------- /shank-idl/tests/fixtures/accounts/sample_crate/src/auctionhouse.rs: -------------------------------------------------------------------------------- 1 | #[derive(ShankAccount)] 2 | pub struct AuctionHouse { 3 | pub auction_house_fee_account: Pubkey, 4 | pub auction_house_treasury: Pubkey, 5 | pub treasury_withdrawal_destination: Pubkey, 6 | pub fee_withdrawal_destination: Pubkey, 7 | pub treasury_mint: Pubkey, 8 | pub authority: Pubkey, 9 | pub creator: Pubkey, 10 | pub bump: u8, 11 | pub treasury_bump: u8, 12 | pub fee_payer_bump: u8, 13 | pub seller_fee_basis_points: u16, 14 | pub requires_sign_off: bool, 15 | pub can_change_sale_price: bool, 16 | } 17 | -------------------------------------------------------------------------------- /shank-idl/tests/fixtures/accounts/sample_crate/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod auctionhouse; 2 | mod metadata; 3 | -------------------------------------------------------------------------------- /shank-idl/tests/fixtures/accounts/sample_crate/src/metadata.rs: -------------------------------------------------------------------------------- 1 | #[derive(ShankAccount)] 2 | pub struct Metadata { 3 | pub key: Key, 4 | pub update_authority: Pubkey, 5 | pub mint: Pubkey, 6 | pub data: Data, 7 | // Immutable, once flipped, all sales of this metadata are considered secondary. 8 | pub primary_sale_happened: bool, 9 | // Whether or not the data struct is mutable, default is not 10 | pub is_mutable: bool, 11 | /// nonce for easy calculation of editions, if present 12 | pub edition_nonce: Option, 13 | /// Since we cannot easily change Metadata, we add the new DataV2 fields here at the end. 14 | pub token_standard: Option, 15 | /// Collection 16 | pub collection: Option, 17 | /// Uses 18 | pub uses: Option, 19 | } 20 | -------------------------------------------------------------------------------- /shank-idl/tests/fixtures/accounts/single_file/account.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "", 3 | "name": "", 4 | "instructions": [], 5 | "accounts": [ 6 | { 7 | "name": "Metadata", 8 | "type": { 9 | "kind": "struct", 10 | "fields": [ 11 | { 12 | "name": "key", 13 | "type": { 14 | "defined": "Key" 15 | } 16 | }, 17 | { 18 | "name": "updateAuthority", 19 | "type": "publicKey" 20 | }, 21 | { 22 | "name": "mint", 23 | "type": "publicKey" 24 | }, 25 | { 26 | "name": "data", 27 | "type": { 28 | "defined": "Data" 29 | } 30 | }, 31 | { 32 | "name": "primarySaleHappened", 33 | "type": "bool" 34 | }, 35 | { 36 | "name": "isMutable", 37 | "type": "bool" 38 | }, 39 | { 40 | "name": "editionNonce", 41 | "type": { 42 | "option": "u8" 43 | } 44 | }, 45 | { 46 | "name": "tokenStandard", 47 | "type": { 48 | "option": { 49 | "defined": "TokenStandard" 50 | } 51 | } 52 | }, 53 | { 54 | "name": "collection", 55 | "type": { 56 | "option": { 57 | "defined": "Collection" 58 | } 59 | } 60 | }, 61 | { 62 | "name": "uses", 63 | "type": { 64 | "option": { 65 | "defined": "Uses" 66 | } 67 | } 68 | } 69 | ] 70 | } 71 | } 72 | ], 73 | "metadata": { 74 | "origin": "shank" 75 | } 76 | } 77 | 78 | -------------------------------------------------------------------------------- /shank-idl/tests/fixtures/accounts/single_file/account.rs: -------------------------------------------------------------------------------- 1 | #[derive(ShankAccount)] 2 | pub struct Metadata { 3 | pub key: Key, 4 | pub update_authority: Pubkey, 5 | pub mint: Pubkey, 6 | pub data: Data, 7 | // Immutable, once flipped, all sales of this metadata are considered secondary. 8 | pub primary_sale_happened: bool, 9 | // Whether or not the data struct is mutable, default is not 10 | pub is_mutable: bool, 11 | /// nonce for easy calculation of editions, if present 12 | pub edition_nonce: Option, 13 | /// Since we cannot easily change Metadata, we add the new DataV2 fields here at the end. 14 | pub token_standard: Option, 15 | /// Collection 16 | pub collection: Option, 17 | /// Uses 18 | pub uses: Option, 19 | } 20 | -------------------------------------------------------------------------------- /shank-idl/tests/fixtures/accounts/single_file/complex_types.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "", 3 | "name": "", 4 | "instructions": [], 5 | "accounts": [ 6 | { 7 | "name": "StructAccount", 8 | "type": { 9 | "kind": "struct", 10 | "fields": [ 11 | { 12 | "name": "optVecOpt", 13 | "type": { 14 | "option": { 15 | "vec": { 16 | "option": "u8" 17 | } 18 | } 19 | } 20 | }, 21 | { 22 | "name": "vecOptPubkey", 23 | "type": { 24 | "vec": { 25 | "option": "publicKey" 26 | } 27 | } 28 | }, 29 | { 30 | "name": "optVecCustomTy", 31 | "type": { 32 | "option": { 33 | "vec": { 34 | "defined": "CustomType" 35 | } 36 | } 37 | } 38 | } 39 | ] 40 | } 41 | } 42 | ], 43 | "metadata": { 44 | "origin": "shank" 45 | } 46 | } 47 | 48 | -------------------------------------------------------------------------------- /shank-idl/tests/fixtures/accounts/single_file/complex_types.rs: -------------------------------------------------------------------------------- 1 | #[derive(ShankAccount)] 2 | pub struct StructAccount { 3 | pub opt_vec_opt: Option>>, 4 | pub vec_opt_pubkey: Vec>, 5 | pub opt_vec_custom_ty: Option>, 6 | } 7 | -------------------------------------------------------------------------------- /shank-idl/tests/fixtures/accounts/single_file/idl_type.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "", 3 | "name": "", 4 | "instructions": [], 5 | "accounts": [ 6 | { 7 | "name": "AccountWithIdlType", 8 | "type": { 9 | "kind": "struct", 10 | "fields": [ 11 | { 12 | "name": "regularField", 13 | "type": "u32" 14 | }, 15 | { 16 | "name": "enumAsByteStr", 17 | "type": { 18 | "defined": "TestEnum" 19 | }, 20 | "attrs": ["idl-type"] 21 | }, 22 | { 23 | "name": "wrappedU64Str", 24 | "type": "u64", 25 | "attrs": ["idl-type"] 26 | }, 27 | { 28 | "name": "enumAsByteDirect", 29 | "type": { 30 | "defined": "TestEnum" 31 | }, 32 | "attrs": ["idl-type"] 33 | }, 34 | { 35 | "name": "wrappedU32Direct", 36 | "type": "u32", 37 | "attrs": ["idl-type"] 38 | } 39 | ] 40 | } 41 | } 42 | ], 43 | "types": [ 44 | { 45 | "name": "TestEnum", 46 | "type": { 47 | "kind": "enum", 48 | "variants": [ 49 | { 50 | "name": "OptionA" 51 | }, 52 | { 53 | "name": "OptionB" 54 | }, 55 | { 56 | "name": "OptionC" 57 | } 58 | ] 59 | } 60 | } 61 | ], 62 | "metadata": { 63 | "origin": "shank" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /shank-idl/tests/fixtures/accounts/single_file/idl_type.rs: -------------------------------------------------------------------------------- 1 | use borsh::{BorshDeserialize, BorshSerialize}; 2 | use shank::{ShankAccount, ShankType}; 3 | 4 | /// An enum that will be used with the idl_type attribute 5 | #[derive(ShankType)] 6 | pub enum TestEnum { 7 | OptionA, 8 | OptionB, 9 | OptionC, 10 | } 11 | 12 | /// A wrapper type for u64 13 | pub struct CustomU64Wrapper(pub u64); 14 | 15 | /// Another wrapper type for u32 16 | pub struct CustomU32Wrapper(pub u32); 17 | 18 | /// Account with fields using the idl_type(...) attribute 19 | #[derive(ShankAccount)] 20 | pub struct AccountWithIdlType { 21 | /// A regular field without any attribute 22 | pub regular_field: u32, 23 | 24 | /// A field stored as u8 but representing an enum (using string literal format) 25 | #[idl_type("TestEnum")] 26 | pub enum_as_byte_str: u8, 27 | 28 | /// A field with a wrapper type that should be treated as a simpler type (using string literal format) 29 | #[idl_type("u64")] 30 | pub wrapped_u64_str: CustomU64Wrapper, 31 | 32 | /// A field stored as u8 but representing an enum (using direct type format) 33 | #[idl_type(TestEnum)] 34 | pub enum_as_byte_direct: u8, 35 | 36 | /// A field with a wrapper type that should be treated as a simpler type (using direct type format) 37 | #[idl_type(u32)] 38 | pub wrapped_u32_direct: CustomU32Wrapper, 39 | } 40 | 41 | // Notes: This test does not check: 42 | // - The ability to reference a path (like std::string::String) 43 | // - Parsing failure when the direct type is not found 44 | -------------------------------------------------------------------------------- /shank-idl/tests/fixtures/accounts/single_file/padding.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "", 3 | "name": "", 4 | "instructions": [], 5 | "accounts": [ 6 | { 7 | "name": "StructAccountWithPadding", 8 | "type": { 9 | "kind": "struct", 10 | "fields": [ 11 | { 12 | "name": "count", 13 | "type": "u8" 14 | }, 15 | { 16 | "name": "padding", 17 | "type": { 18 | "array": [ 19 | "u8", 20 | 3 21 | ] 22 | }, 23 | "attrs": [ 24 | "padding" 25 | ] 26 | } 27 | ] 28 | } 29 | } 30 | ], 31 | "metadata": { 32 | "origin": "shank" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /shank-idl/tests/fixtures/accounts/single_file/padding.rs: -------------------------------------------------------------------------------- 1 | #[derive(ShankAccount)] 2 | pub struct StructAccountWithPadding { 3 | count: u8, 4 | #[padding] 5 | _padding: [u8; 3], 6 | } 7 | -------------------------------------------------------------------------------- /shank-idl/tests/fixtures/errors/this_error.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "", 3 | "name": "", 4 | "instructions": [], 5 | "errors": [ 6 | { 7 | "code": 0, 8 | "name": "InstructionUnpackError", 9 | "msg": "Failed to unpack instruction data" 10 | }, 11 | { 12 | "code": 1, 13 | "name": "NotRentExempt", 14 | "msg": "Lamport balance below rent-exempt threshold" 15 | }, 16 | { 17 | "code": 2, 18 | "name": "AlreadyInitialized", 19 | "msg": "Already initialized" 20 | }, 21 | { 22 | "code": 3, 23 | "name": "Uninitialized", 24 | "msg": "Uninitialized" 25 | }, 26 | { 27 | "code": 4, 28 | "name": "IncorrectOwner", 29 | "msg": "Account does not have correct owner" 30 | }, 31 | { 32 | "code": 5, 33 | "name": "NumericalOverflowError", 34 | "msg": "NumericalOverflowError" 35 | } 36 | ], 37 | "metadata": { 38 | "origin": "shank" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /shank-idl/tests/fixtures/errors/this_error.rs: -------------------------------------------------------------------------------- 1 | /// Errors that may be returned by the Vault program. 2 | #[derive(Clone, Debug, Eq, Error, FromPrimitive, PartialEq)] 3 | pub enum VaultError { 4 | /// Invalid instruction data passed in. 5 | #[error("Failed to unpack instruction data")] 6 | InstructionUnpackError, 7 | 8 | /// Lamport balance below rent-exempt threshold. 9 | #[error("Lamport balance below rent-exempt threshold")] 10 | NotRentExempt, 11 | 12 | /// Already initialized 13 | #[error("Already initialized")] 14 | AlreadyInitialized, 15 | 16 | /// Uninitialized 17 | #[error("Uninitialized")] 18 | Uninitialized, 19 | 20 | /// Account does not have correct owner 21 | #[error("Account does not have correct owner")] 22 | IncorrectOwner, 23 | 24 | /// NumericalOverflowError 25 | #[error("NumericalOverflowError")] 26 | NumericalOverflowError, 27 | } 28 | -------------------------------------------------------------------------------- /shank-idl/tests/fixtures/errors/this_error_custom_codes.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "", 3 | "name": "", 4 | "instructions": [], 5 | "errors": [ 6 | { 7 | "code": 3000, 8 | "name": "InstructionUnpackError", 9 | "msg": "Failed to unpack instruction data" 10 | }, 11 | { 12 | "code": 3001, 13 | "name": "NotRentExempt", 14 | "msg": "Lamport balance below rent-exempt threshold" 15 | }, 16 | { 17 | "code": 3002, 18 | "name": "AlreadyInitialized", 19 | "msg": "Already initialized" 20 | }, 21 | { 22 | "code": 3003, 23 | "name": "Uninitialized", 24 | "msg": "Uninitialized" 25 | }, 26 | { 27 | "code": 4000, 28 | "name": "IncorrectOwner", 29 | "msg": "Account does not have correct owner" 30 | }, 31 | { 32 | "code": 4001, 33 | "name": "NumericalOverflowError", 34 | "msg": "NumericalOverflowError" 35 | } 36 | ], 37 | "metadata": { 38 | "origin": "shank" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /shank-idl/tests/fixtures/errors/this_error_custom_codes.rs: -------------------------------------------------------------------------------- 1 | /// Errors that may be returned by the Vault program. 2 | #[derive(Clone, Debug, Eq, Error, FromPrimitive, PartialEq)] 3 | pub enum VaultError { 4 | /// Invalid instruction data passed in. 5 | #[error("Failed to unpack instruction data")] 6 | InstructionUnpackError = 3000, 7 | 8 | /// Lamport balance below rent-exempt threshold. 9 | #[error("Lamport balance below rent-exempt threshold")] 10 | NotRentExempt, 11 | 12 | /// Already initialized 13 | #[error("Already initialized")] 14 | AlreadyInitialized, 15 | 16 | /// Uninitialized 17 | #[error("Uninitialized")] 18 | Uninitialized, 19 | 20 | /// Account does not have correct owner 21 | #[error("Account does not have correct owner")] 22 | IncorrectOwner = 4000, 23 | 24 | /// NumericalOverflowError 25 | #[error("NumericalOverflowError")] 26 | NumericalOverflowError, 27 | } 28 | -------------------------------------------------------------------------------- /shank-idl/tests/fixtures/instructions/single_file/create_idl_instructions.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "", 3 | "name": "", 4 | "instructions": [ 5 | { 6 | "name": "Create", 7 | "accounts": [ 8 | { 9 | "name": "from", 10 | "isMut": true, 11 | "isSigner": true, 12 | "docs": [ 13 | "Payer of the transaction" 14 | ] 15 | }, 16 | { 17 | "name": "to", 18 | "isMut": true, 19 | "isSigner": false, 20 | "docs": [ 21 | "The deterministically defined 'state' account being created via `create_account_with_seed`" 22 | ] 23 | }, 24 | { 25 | "name": "base", 26 | "isMut": false, 27 | "isSigner": false, 28 | "docs": [ 29 | "The program-derived-address signing off on the account creation. Seeds = &[] + bump seed." 30 | ] 31 | }, 32 | { 33 | "name": "systemProgram", 34 | "isMut": false, 35 | "isSigner": false, 36 | "docs": [ 37 | "The system program" 38 | ] 39 | }, 40 | { 41 | "name": "program", 42 | "isMut": false, 43 | "isSigner": false, 44 | "docs": [ 45 | "The program whose state is being constructed" 46 | ] 47 | } 48 | ], 49 | "args": [ 50 | { 51 | "name": "dataLen", 52 | "type": "u64" 53 | } 54 | ], 55 | "discriminant": { 56 | "type": "u8", 57 | "value": 0 58 | } 59 | }, 60 | { 61 | "name": "CreateBuffer", 62 | "accounts": [ 63 | { 64 | "name": "buffer", 65 | "isMut": true, 66 | "isSigner": false 67 | }, 68 | { 69 | "name": "authority", 70 | "isMut": false, 71 | "isSigner": true 72 | } 73 | ], 74 | "args": [], 75 | "discriminant": { 76 | "type": "u8", 77 | "value": 1 78 | } 79 | }, 80 | { 81 | "name": "SetBuffer", 82 | "accounts": [ 83 | { 84 | "name": "buffer", 85 | "isMut": true, 86 | "isSigner": false, 87 | "docs": [ 88 | "The buffer with the new idl data." 89 | ] 90 | }, 91 | { 92 | "name": "idl", 93 | "isMut": true, 94 | "isSigner": false, 95 | "docs": [ 96 | "The idl account to be updated with the buffer's data." 97 | ] 98 | }, 99 | { 100 | "name": "authority", 101 | "isMut": false, 102 | "isSigner": true 103 | } 104 | ], 105 | "args": [], 106 | "discriminant": { 107 | "type": "u8", 108 | "value": 2 109 | } 110 | }, 111 | { 112 | "name": "SetAuthority", 113 | "accounts": [ 114 | { 115 | "name": "idl", 116 | "isMut": true, 117 | "isSigner": false 118 | }, 119 | { 120 | "name": "authority", 121 | "isMut": false, 122 | "isSigner": true 123 | } 124 | ], 125 | "args": [ 126 | { 127 | "name": "newAuthority", 128 | "type": "publicKey" 129 | } 130 | ], 131 | "discriminant": { 132 | "type": "u8", 133 | "value": 3 134 | } 135 | }, 136 | { 137 | "name": "Write", 138 | "accounts": [ 139 | { 140 | "name": "idl", 141 | "isMut": true, 142 | "isSigner": false 143 | }, 144 | { 145 | "name": "authority", 146 | "isMut": false, 147 | "isSigner": true 148 | } 149 | ], 150 | "args": [ 151 | { 152 | "name": "idlData", 153 | "type": "bytes" 154 | } 155 | ], 156 | "discriminant": { 157 | "type": "u8", 158 | "value": 4 159 | } 160 | } 161 | ], 162 | "metadata": { 163 | "origin": "shank" 164 | } 165 | } -------------------------------------------------------------------------------- /shank-idl/tests/fixtures/instructions/single_file/create_idl_instructions.rs: -------------------------------------------------------------------------------- 1 | #[derive(ShankInstruction)] 2 | pub enum Instruction { 3 | #[idl_instruction(Create)] 4 | Create, 5 | #[idl_instruction(CreateBuffer)] 6 | CreateBuffer, 7 | #[idl_instruction(SetBuffer)] 8 | SetBuffer, 9 | #[idl_instruction(SetAuthority)] 10 | SetAuthority, 11 | #[idl_instruction(Write)] 12 | Write, 13 | } 14 | -------------------------------------------------------------------------------- /shank-idl/tests/fixtures/instructions/single_file/instruction_invalid_attr.rs: -------------------------------------------------------------------------------- 1 | #[derive(ShankInstruction)] 2 | pub enum Instruction { 3 | // Misspelled sig 4 | #[account(0, name = "creator", sg)] 5 | CreateThing, 6 | } 7 | -------------------------------------------------------------------------------- /shank-idl/tests/fixtures/instructions/single_file/instruction_invalid_discriminant.rs: -------------------------------------------------------------------------------- 1 | #[derive(ShankInstruction)] 2 | pub enum Instruction { 3 | #[account(0, name = "creator", sig)] 4 | CreateThing = 256, // u8::MAX + 1, 5 | } 6 | -------------------------------------------------------------------------------- /shank-idl/tests/fixtures/instructions/single_file/instruction_no_args.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "", 3 | "name": "", 4 | "instructions": [ 5 | { 6 | "name": "CreateThing", 7 | "accounts": [ 8 | { 9 | "name": "creator", 10 | "isMut": false, 11 | "isSigner": true 12 | }, 13 | { 14 | "name": "thing", 15 | "isMut": true, 16 | "isSigner": false 17 | } 18 | ], 19 | "args": [], 20 | "discriminant": { "type": "u8", "value": 0 } 21 | }, 22 | { 23 | "name": "CloseThing", 24 | "accounts": [ 25 | { 26 | "name": "originalCreator", 27 | "isMut": false, 28 | "isSigner": true 29 | } 30 | ], 31 | "args": [], 32 | "discriminant": { "type": "u8", "value": 1 } 33 | } 34 | ], 35 | "metadata": { 36 | "origin": "shank" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /shank-idl/tests/fixtures/instructions/single_file/instruction_no_args.rs: -------------------------------------------------------------------------------- 1 | #[derive(ShankInstruction)] 2 | pub enum Instruction { 3 | #[account(0, name = "creator", sig)] 4 | #[account(1, name = "thing", mut)] 5 | CreateThing, 6 | #[account(name = "original_creator", sig)] 7 | CloseThing, 8 | } 9 | -------------------------------------------------------------------------------- /shank-idl/tests/fixtures/instructions/single_file/instruction_with_args.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "", 3 | "name": "", 4 | "instructions": [ 5 | { 6 | "name": "CreateThing", 7 | "accounts": [ 8 | { 9 | "name": "creator", 10 | "isMut": false, 11 | "isSigner": true 12 | }, 13 | { 14 | "name": "thing", 15 | "isMut": true, 16 | "isSigner": false 17 | } 18 | ], 19 | "args": [ 20 | { 21 | "name": "someArgs", 22 | "type": { 23 | "defined": "SomeArgs" 24 | } 25 | } 26 | ], 27 | "discriminant": { "type": "u8", "value": 0 } 28 | }, 29 | { 30 | "name": "CloseThing", 31 | "accounts": [ 32 | { 33 | "name": "creator", 34 | "isMut": false, 35 | "isSigner": true 36 | } 37 | ], 38 | "args": [ 39 | { 40 | "name": "args", 41 | "type": { 42 | "option": "u8" 43 | } 44 | } 45 | ], 46 | "discriminant": { "type": "u8", "value": 1 } 47 | } 48 | ], 49 | "metadata": { 50 | "origin": "shank" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /shank-idl/tests/fixtures/instructions/single_file/instruction_with_args.rs: -------------------------------------------------------------------------------- 1 | #[derive(ShankInstruction)] 2 | pub enum Instruction { 3 | #[account(0, name = "creator", sig)] 4 | #[account(1, name = "thing", mut)] 5 | CreateThing(SomeArgs), 6 | #[account(name = "creator", sig)] 7 | CloseThing(Option), 8 | } 9 | -------------------------------------------------------------------------------- /shank-idl/tests/fixtures/instructions/single_file/instruction_with_docs.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "", 3 | "name": "", 4 | "instructions": [ 5 | { 6 | "name": "CreateThing", 7 | "accounts": [ 8 | { 9 | "name": "creator", 10 | "isMut": false, 11 | "isSigner": true, 12 | "docs": [ 13 | "The creator of the thing" 14 | ] 15 | }, 16 | { 17 | "name": "thing", 18 | "isMut": true, 19 | "isSigner": false, 20 | "docs": [ 21 | "The thing to create" 22 | ] 23 | } 24 | ], 25 | "args": [], 26 | "discriminant": { "type": "u8", "value": 0 } 27 | }, 28 | { 29 | "name": "CloseThing", 30 | "accounts": [ 31 | { 32 | "name": "originalCreator", 33 | "isMut": false, 34 | "isSigner": true, 35 | "docs": [ 36 | "The original creator of the thing" 37 | ] 38 | } 39 | ], 40 | "args": [], 41 | "discriminant": { "type": "u8", "value": 1 } 42 | } 43 | ], 44 | "metadata": { 45 | "origin": "shank" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /shank-idl/tests/fixtures/instructions/single_file/instruction_with_docs.rs: -------------------------------------------------------------------------------- 1 | #[derive(ShankInstruction)] 2 | pub enum Instruction { 3 | #[account(0, name = "creator", sig, desc = "The creator of the thing")] 4 | #[account(1, name = "thing", mut, desc = "The thing to create")] 5 | CreateThing, 6 | #[account( 7 | name = "original_creator", 8 | sig, 9 | docs = "The original creator of the thing" 10 | )] 11 | CloseThing, 12 | } 13 | -------------------------------------------------------------------------------- /shank-idl/tests/fixtures/instructions/single_file/instruction_with_multiple_args.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "", 3 | "name": "", 4 | "instructions": [ 5 | { 6 | "name": "CloseThing", 7 | "accounts": [ 8 | { 9 | "name": "creator", 10 | "isMut": false, 11 | "isSigner": true 12 | } 13 | ], 14 | "args": [ 15 | { 16 | "name": "arg0", 17 | "type": { 18 | "option": "u8" 19 | } 20 | }, 21 | { 22 | "name": "arg1", 23 | "type": { 24 | "defined": "ComplexArgs" 25 | } 26 | }, 27 | { 28 | "name": "arg2", 29 | "type": { 30 | "defined": "ComplexArgs" 31 | } 32 | } 33 | ], 34 | "discriminant": { 35 | "type": "u8", 36 | "value": 0 37 | } 38 | } 39 | ], 40 | "metadata": { 41 | "origin": "shank" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /shank-idl/tests/fixtures/instructions/single_file/instruction_with_multiple_args.rs: -------------------------------------------------------------------------------- 1 | #[derive(ShankInstruction)] 2 | pub enum Instruction { 3 | #[account(0, name = "creator", sig)] 4 | CloseThing(Option, ComplexArgs, ComplexArgs), 5 | } 6 | -------------------------------------------------------------------------------- /shank-idl/tests/fixtures/instructions/single_file/instruction_with_optional_account.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "", 3 | "name": "", 4 | "instructions": [ 5 | { 6 | "name": "CreateThing", 7 | "accounts": [ 8 | { 9 | "name": "creator", 10 | "isMut": false, 11 | "isSigner": true 12 | }, 13 | { 14 | "name": "thing", 15 | "isMut": true, 16 | "isSigner": false, 17 | "isOptional": true 18 | } 19 | ], 20 | "args": [ 21 | { 22 | "name": "someArgs", 23 | "type": { 24 | "defined": "SomeArgs" 25 | } 26 | } 27 | ], 28 | "discriminant": { 29 | "type": "u8", 30 | "value": 0 31 | } 32 | }, 33 | { 34 | "name": "CloseThing", 35 | "accounts": [ 36 | { 37 | "name": "creator", 38 | "isMut": false, 39 | "isSigner": true 40 | } 41 | ], 42 | "args": [], 43 | "discriminant": { 44 | "type": "u8", 45 | "value": 1 46 | } 47 | } 48 | ], 49 | "metadata": { 50 | "origin": "shank" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /shank-idl/tests/fixtures/instructions/single_file/instruction_with_optional_account.rs: -------------------------------------------------------------------------------- 1 | #[derive(ShankInstruction)] 2 | pub enum Instruction { 3 | #[account(0, name = "creator", sig)] 4 | #[account(1, name = "thing", mut, optional)] 5 | CreateThing(SomeArgs), 6 | #[account(name = "creator", sig)] 7 | CloseThing, 8 | } 9 | -------------------------------------------------------------------------------- /shank-idl/tests/fixtures/instructions/single_file/instruction_with_optional_account_defaulting.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "", 3 | "name": "", 4 | "instructions": [ 5 | { 6 | "name": "CreateThing", 7 | "accounts": [ 8 | { 9 | "name": "creator", 10 | "isMut": false, 11 | "isSigner": true 12 | }, 13 | { 14 | "name": "thing", 15 | "isMut": true, 16 | "isSigner": false, 17 | "isOptional": true 18 | } 19 | ], 20 | "args": [ 21 | { 22 | "name": "someArgs", 23 | "type": { 24 | "defined": "SomeArgs" 25 | } 26 | } 27 | ], 28 | "legacyOptionalAccountsStrategy": true, 29 | "discriminant": { 30 | "type": "u8", 31 | "value": 0 32 | } 33 | }, 34 | { 35 | "name": "CloseThing", 36 | "accounts": [ 37 | { 38 | "name": "creator", 39 | "isMut": false, 40 | "isSigner": true 41 | } 42 | ], 43 | "args": [], 44 | "discriminant": { 45 | "type": "u8", 46 | "value": 1 47 | } 48 | } 49 | ], 50 | "metadata": { 51 | "origin": "shank" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /shank-idl/tests/fixtures/instructions/single_file/instruction_with_optional_account_defaulting.rs: -------------------------------------------------------------------------------- 1 | #[derive(ShankInstruction)] 2 | pub enum Instruction { 3 | #[legacy_optional_accounts_strategy] 4 | #[account(0, name = "creator", sig)] 5 | #[account(1, name = "thing", mut, optional)] 6 | CreateThing(SomeArgs), 7 | #[account(name = "creator", sig)] 8 | CloseThing, 9 | } 10 | -------------------------------------------------------------------------------- /shank-idl/tests/fixtures/instructions/single_file/instruction_with_optional_signer_account.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "", 3 | "name": "", 4 | "instructions": [ 5 | { 6 | "name": "CreateThing", 7 | "accounts": [ 8 | { 9 | "name": "creator", 10 | "isMut": false, 11 | "isSigner": true 12 | }, 13 | { 14 | "name": "thing", 15 | "isMut": true, 16 | "isSigner": false 17 | } 18 | ], 19 | "args": [ 20 | { 21 | "name": "someArgs", 22 | "type": { 23 | "defined": "SomeArgs" 24 | } 25 | } 26 | ], 27 | "discriminant": { 28 | "type": "u8", 29 | "value": 0 30 | } 31 | }, 32 | { 33 | "name": "CloseThing", 34 | "accounts": [ 35 | { 36 | "name": "creator", 37 | "isMut": false, 38 | "isSigner": false, 39 | "isOptionalSigner": true 40 | } 41 | ], 42 | "args": [], 43 | "discriminant": { 44 | "type": "u8", 45 | "value": 1 46 | } 47 | } 48 | ], 49 | "metadata": { 50 | "origin": "shank" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /shank-idl/tests/fixtures/instructions/single_file/instruction_with_optional_signer_account.rs: -------------------------------------------------------------------------------- 1 | #[derive(ShankInstruction)] 2 | pub enum Instruction { 3 | #[account(0, name = "creator", signer)] 4 | #[account(1, name = "thing", writable)] 5 | CreateThing(SomeArgs), 6 | #[account(name = "creator", optional_signer)] 7 | CloseThing, 8 | } 9 | -------------------------------------------------------------------------------- /shank-idl/tests/fixtures/instructions/single_file/instruction_with_struct_args.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "", 3 | "name": "", 4 | "instructions": [ 5 | { 6 | "name": "CreateThing", 7 | "accounts": [ 8 | { 9 | "name": "creator", 10 | "isMut": false, 11 | "isSigner": true 12 | }, 13 | { 14 | "name": "thing", 15 | "isMut": true, 16 | "isSigner": false 17 | } 18 | ], 19 | "args": [ 20 | { 21 | "name": "someArgs", 22 | "type": { 23 | "defined": "SomeArgs" 24 | } 25 | }, 26 | { 27 | "name": "otherArgs", 28 | "type": { 29 | "defined": "OtherArgs" 30 | } 31 | } 32 | ], 33 | "discriminant": { 34 | "type": "u8", 35 | "value": 0 36 | } 37 | }, 38 | { 39 | "name": "CloseThing", 40 | "accounts": [ 41 | { 42 | "name": "creator", 43 | "isMut": false, 44 | "isSigner": true 45 | } 46 | ], 47 | "args": [ 48 | { 49 | "name": "args", 50 | "type": { 51 | "option": "u8" 52 | } 53 | } 54 | ], 55 | "discriminant": { 56 | "type": "u8", 57 | "value": 1 58 | } 59 | } 60 | ], 61 | "metadata": { 62 | "origin": "shank" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /shank-idl/tests/fixtures/instructions/single_file/instruction_with_struct_args.rs: -------------------------------------------------------------------------------- 1 | #[derive(ShankInstruction)] 2 | pub enum Instruction { 3 | #[account(0, name = "creator", sig)] 4 | #[account(1, name = "thing", mut)] 5 | CreateThing { 6 | some_args: SomeArgs, 7 | other_args: OtherArgs, 8 | }, 9 | #[account(0, name = "creator", sig)] 10 | CloseThing(Option), 11 | } 12 | -------------------------------------------------------------------------------- /shank-idl/tests/fixtures/macros/program_id_missing.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "", 3 | "name": "", 4 | "instructions": [], 5 | "metadata": { 6 | "origin": "shank" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /shank-idl/tests/fixtures/macros/program_id_missing.rs: -------------------------------------------------------------------------------- 1 | pub use solana_program; 2 | 3 | // solana_program::declare_id!("metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s"); 4 | -------------------------------------------------------------------------------- /shank-idl/tests/fixtures/macros/program_id_valid.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "", 3 | "name": "", 4 | "instructions": [], 5 | "metadata": { 6 | "address": "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s", 7 | "origin": "shank" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /shank-idl/tests/fixtures/macros/program_id_valid.rs: -------------------------------------------------------------------------------- 1 | pub use solana_program; 2 | 3 | solana_program::declare_id!("metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s"); 4 | -------------------------------------------------------------------------------- /shank-idl/tests/fixtures/types/invalid_single.rs: -------------------------------------------------------------------------------- 1 | #[derive(BorshSerialize)] 2 | pub struct MyType { 3 | pb field: u8, 4 | } 5 | -------------------------------------------------------------------------------- /shank-idl/tests/fixtures/types/valid_multiple.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "", 3 | "name": "", 4 | "instructions": [], 5 | "accounts": [ 6 | { 7 | "name": "AccountType", 8 | "type": { 9 | "kind": "struct", 10 | "fields": [ 11 | { 12 | "name": "field", 13 | "type": "u8" 14 | } 15 | ] 16 | } 17 | } 18 | ], 19 | "types": [ 20 | { 21 | "name": "OneCustomType", 22 | "type": { 23 | "kind": "struct", 24 | "fields": [ 25 | { 26 | "name": "field", 27 | "type": "u8" 28 | } 29 | ] 30 | } 31 | }, 32 | { 33 | "name": "OtherCustomType", 34 | "type": { 35 | "kind": "struct", 36 | "fields": [ 37 | { 38 | "name": "field", 39 | "type": { 40 | "option": "string" 41 | } 42 | } 43 | ] 44 | } 45 | }, 46 | { 47 | "name": "EnumCustomType", 48 | "type": { 49 | "kind": "enum", 50 | "variants": [ 51 | { 52 | "name": "Up", 53 | "fields": ["u8"] 54 | }, 55 | { 56 | "name": "Down", 57 | "fields": ["u8"] 58 | } 59 | ] 60 | } 61 | } 62 | ], 63 | "metadata": { 64 | "origin": "shank" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /shank-idl/tests/fixtures/types/valid_multiple.rs: -------------------------------------------------------------------------------- 1 | #[derive(BorshSerialize)] 2 | pub struct OneCustomType { 3 | pub field: u8, 4 | } 5 | 6 | #[derive(BorshDeserialize)] 7 | pub struct OtherCustomType { 8 | pub field: Option, 9 | } 10 | 11 | #[derive(BorshDeserialize)] 12 | pub enum EnumCustomType { 13 | Up(u8), 14 | Down(u8), 15 | } 16 | 17 | /// Misses serialization attrs 18 | pub struct NotCustomType { 19 | pub field: u8, 20 | } 21 | 22 | /// Has serialization attr, but also ShankInstruction 23 | #[derive(BorshDeserialize, ShankInstruction)] 24 | pub struct AlsoNotCustomType { 25 | pub field: u8, 26 | } 27 | 28 | /// Has serialization attr, but also ShankAccount 29 | #[derive(BorshDeserialize, ShankAccount)] 30 | pub struct AccountType { 31 | pub field: u8, 32 | } 33 | -------------------------------------------------------------------------------- /shank-idl/tests/fixtures/types/valid_multiple_maps.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "", 3 | "name": "", 4 | "instructions": [], 5 | "types": [ 6 | { 7 | "name": "OneHashMapStruct", 8 | "type": { 9 | "kind": "struct", 10 | "fields": [ 11 | { 12 | "name": "u8U8Map", 13 | "type": { 14 | "hashMap": ["u8", "u8"] 15 | } 16 | } 17 | ] 18 | } 19 | }, 20 | { 21 | "name": "MultipleHashMapsStruct", 22 | "type": { 23 | "kind": "struct", 24 | "fields": [ 25 | { 26 | "name": "u8StringMap", 27 | "type": { 28 | "hashMap": ["u8", "string"] 29 | } 30 | }, 31 | { 32 | "name": "stringOptionI128Map", 33 | "type": { 34 | "hashMap": [ 35 | "string", 36 | { 37 | "option": "i128" 38 | } 39 | ] 40 | } 41 | }, 42 | { 43 | "name": "optionStringVecCustomMap", 44 | "type": { 45 | "hashMap": [ 46 | { 47 | "option": "string" 48 | }, 49 | { 50 | "vec": { 51 | "defined": "Custom" 52 | } 53 | } 54 | ] 55 | } 56 | } 57 | ] 58 | } 59 | }, 60 | { 61 | "name": "OneBTreeMapStruct", 62 | "type": { 63 | "kind": "struct", 64 | "fields": [ 65 | { 66 | "name": "u8U8Map", 67 | "type": { 68 | "bTreeMap": ["u8", "u8"] 69 | } 70 | } 71 | ] 72 | } 73 | }, 74 | { 75 | "name": "MultipleMapsStruct", 76 | "type": { 77 | "kind": "struct", 78 | "fields": [ 79 | { 80 | "name": "u8StringBtreeMap", 81 | "type": { 82 | "bTreeMap": ["u8", "string"] 83 | } 84 | }, 85 | { 86 | "name": "optionStringVecCustomBtreeMap", 87 | "type": { 88 | "bTreeMap": [ 89 | { 90 | "option": "string" 91 | }, 92 | { 93 | "vec": { 94 | "defined": "Custom" 95 | } 96 | } 97 | ] 98 | } 99 | }, 100 | { 101 | "name": "i16OptionBoolHashMap", 102 | "type": { 103 | "hashMap": [ 104 | "i16", 105 | { 106 | "option": "bool" 107 | } 108 | ] 109 | } 110 | } 111 | ] 112 | } 113 | }, 114 | { 115 | "name": "NestedMapsStruct", 116 | "type": { 117 | "kind": "struct", 118 | "fields": [ 119 | { 120 | "name": "vecHashMapU8U8", 121 | "type": { 122 | "vec": { 123 | "hashMap": ["u8", "u8"] 124 | } 125 | } 126 | }, 127 | { 128 | "name": "optionBtreeMapU8U8", 129 | "type": { 130 | "option": { 131 | "bTreeMap": ["u8", "u8"] 132 | } 133 | } 134 | } 135 | ] 136 | } 137 | } 138 | ], 139 | "metadata": { 140 | "origin": "shank" 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /shank-idl/tests/fixtures/types/valid_multiple_maps.rs: -------------------------------------------------------------------------------- 1 | #[derive(BorshSerialize)] 2 | pub struct OneHashMapStruct { 3 | pub u8_u8_map: HashMap, 4 | } 5 | 6 | #[derive(BorshSerialize)] 7 | pub struct MultipleHashMapsStruct { 8 | pub u8_string_map: HashMap, 9 | pub string_option_i128_map: HashMap>, 10 | pub option_string_vec_custom_map: HashMap, Vec>, 11 | } 12 | 13 | #[derive(BorshSerialize)] 14 | pub struct OneBTreeMapStruct { 15 | pub u8_u8_map: BTreeMap, 16 | } 17 | 18 | #[derive(BorshSerialize)] 19 | pub struct MultipleMapsStruct { 20 | pub u8_string_btree_map: BTreeMap, 21 | pub option_string_vec_custom_btree_map: 22 | BTreeMap, Vec>, 23 | pub i16_option_bool_hash_map: HashMap>, 24 | } 25 | 26 | #[derive(BorshSerialize)] 27 | pub struct NestedMapsStruct { 28 | pub vec_hash_map_u8_u8: Vec>, 29 | pub option_btree_map_u8_u8: Option>, 30 | } 31 | -------------------------------------------------------------------------------- /shank-idl/tests/fixtures/types/valid_multiple_sets.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "", 3 | "name": "", 4 | "instructions": [], 5 | "types": [ 6 | { 7 | "name": "OneHashSetStruct", 8 | "type": { 9 | "kind": "struct", 10 | "fields": [ 11 | { 12 | "name": "u8Set", 13 | "type": { 14 | "hashSet": "u8" 15 | } 16 | } 17 | ] 18 | } 19 | }, 20 | { 21 | "name": "MultipleHashSetsStruct", 22 | "type": { 23 | "kind": "struct", 24 | "fields": [ 25 | { 26 | "name": "u8Set", 27 | "type": { 28 | "hashSet": "u8" 29 | } 30 | }, 31 | { 32 | "name": "stringSet", 33 | "type": { 34 | "hashSet": "string" 35 | } 36 | }, 37 | { 38 | "name": "optionI128Set", 39 | "type": { 40 | "hashSet": { 41 | "option": "i128" 42 | } 43 | } 44 | }, 45 | { 46 | "name": "vecCustomSet", 47 | "type": { 48 | "hashSet": { 49 | "vec": { 50 | "defined": "Custom" 51 | } 52 | } 53 | } 54 | } 55 | ] 56 | } 57 | }, 58 | { 59 | "name": "OneBTreeSetStruct", 60 | "type": { 61 | "kind": "struct", 62 | "fields": [ 63 | { 64 | "name": "u8Set", 65 | "type": { 66 | "bTreeSet": "u8" 67 | } 68 | } 69 | ] 70 | } 71 | }, 72 | { 73 | "name": "MultipleBTreeSetsStruct", 74 | "type": { 75 | "kind": "struct", 76 | "fields": [ 77 | { 78 | "name": "u8Set", 79 | "type": { 80 | "bTreeSet": "u8" 81 | } 82 | }, 83 | { 84 | "name": "stringSet", 85 | "type": { 86 | "bTreeSet": "string" 87 | } 88 | }, 89 | { 90 | "name": "optionI128Set", 91 | "type": { 92 | "bTreeSet": { 93 | "option": "i128" 94 | } 95 | } 96 | }, 97 | { 98 | "name": "vecCustomSet", 99 | "type": { 100 | "bTreeSet": { 101 | "vec": { 102 | "defined": "Custom" 103 | } 104 | } 105 | } 106 | } 107 | ] 108 | } 109 | }, 110 | { 111 | "name": "NestedSetsStruct", 112 | "type": { 113 | "kind": "struct", 114 | "fields": [ 115 | { 116 | "name": "vecHashMapU8", 117 | "type": { 118 | "vec": { 119 | "hashSet": "u8" 120 | } 121 | } 122 | }, 123 | { 124 | "name": "optionBtreeMapU8", 125 | "type": { 126 | "option": { 127 | "bTreeSet": "u8" 128 | } 129 | } 130 | } 131 | ] 132 | } 133 | } 134 | ], 135 | "metadata": { 136 | "origin": "shank" 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /shank-idl/tests/fixtures/types/valid_multiple_sets.rs: -------------------------------------------------------------------------------- 1 | #[derive(BorshSerialize)] 2 | pub struct OneHashSetStruct { 3 | pub u8_set: HashSet, 4 | } 5 | 6 | #[derive(BorshSerialize)] 7 | pub struct MultipleHashSetsStruct { 8 | pub u8_set: HashSet, 9 | pub string_set: HashSet, 10 | pub option_i128_set: HashSet>, 11 | pub vec_custom_set: HashSet>, 12 | } 13 | 14 | #[derive(BorshSerialize)] 15 | pub struct OneBTreeSetStruct { 16 | pub u8_set: BTreeSet, 17 | } 18 | 19 | #[derive(BorshSerialize)] 20 | pub struct MultipleBTreeSetsStruct { 21 | pub u8_set: BTreeSet, 22 | pub string_set: BTreeSet, 23 | pub option_i128_set: BTreeSet>, 24 | pub vec_custom_set: BTreeSet>, 25 | } 26 | 27 | #[derive(BorshSerialize)] 28 | pub struct NestedSetsStruct { 29 | pub vec_hash_map_u8: Vec>, 30 | pub option_btree_map_u8: Option>, 31 | } 32 | -------------------------------------------------------------------------------- /shank-idl/tests/fixtures/types/valid_multiple_tuples.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "", 3 | "name": "", 4 | "instructions": [], 5 | "types": [ 6 | { 7 | "name": "TwoElementTuples", 8 | "type": { 9 | "kind": "struct", 10 | "fields": [ 11 | { 12 | "name": "u8U8", 13 | "type": { 14 | "tuple": ["u8", "u8"] 15 | } 16 | }, 17 | { 18 | "name": "u8U16", 19 | "type": { 20 | "tuple": ["u8", "u16"] 21 | } 22 | }, 23 | { 24 | "name": "stringCustom", 25 | "type": { 26 | "tuple": [ 27 | "string", 28 | { 29 | "defined": "Custom" 30 | } 31 | ] 32 | } 33 | } 34 | ] 35 | } 36 | }, 37 | { 38 | "name": "NestedTwoElementTuples", 39 | "type": { 40 | "kind": "struct", 41 | "fields": [ 42 | { 43 | "name": "vecU8U8", 44 | "type": { 45 | "vec": { 46 | "tuple": ["u8", "u8"] 47 | } 48 | } 49 | }, 50 | { 51 | "name": "hashMapU8U16StringCustom", 52 | "type": { 53 | "hashMap": [ 54 | { 55 | "tuple": ["u8", "u16"] 56 | }, 57 | { 58 | "tuple": [ 59 | "string", 60 | { 61 | "defined": "Custom" 62 | } 63 | ] 64 | } 65 | ] 66 | } 67 | } 68 | ] 69 | } 70 | }, 71 | { 72 | "name": "MoreElementTuples", 73 | "type": { 74 | "kind": "struct", 75 | "fields": [ 76 | { 77 | "name": "u8U8U8", 78 | "type": { 79 | "tuple": ["u8", "u8", "u8"] 80 | } 81 | }, 82 | { 83 | "name": "u8U16I32Bool", 84 | "type": { 85 | "tuple": ["u8", "u16", "i32", "bool"] 86 | } 87 | }, 88 | { 89 | "name": "stringCustomOptionI128U8U16U32U64", 90 | "type": { 91 | "tuple": [ 92 | "string", 93 | { 94 | "defined": "Custom" 95 | }, 96 | { 97 | "option": "i128" 98 | }, 99 | "u8", 100 | "u16", 101 | "u32", 102 | "u64" 103 | ] 104 | } 105 | } 106 | ] 107 | } 108 | }, 109 | { 110 | "name": "NestedMultiElementTuples", 111 | "type": { 112 | "kind": "struct", 113 | "fields": [ 114 | { 115 | "name": "vecU8U8U16U16U32Uy32", 116 | "type": { 117 | "vec": { 118 | "tuple": ["u8", "u8", "u16", "u16", "u32", "u32"] 119 | } 120 | } 121 | }, 122 | { 123 | "name": "hashMapU8U16U32StringCustomU8U8", 124 | "type": { 125 | "hashMap": [ 126 | { 127 | "tuple": ["u8", "u16", "u32"] 128 | }, 129 | { 130 | "tuple": [ 131 | "string", 132 | { 133 | "defined": "Custom" 134 | }, 135 | "u8", 136 | "u8" 137 | ] 138 | } 139 | ] 140 | } 141 | } 142 | ] 143 | } 144 | } 145 | ], 146 | "metadata": { 147 | "origin": "shank" 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /shank-idl/tests/fixtures/types/valid_multiple_tuples.rs: -------------------------------------------------------------------------------- 1 | #[derive(BorshSerialize)] 2 | pub struct TwoElementTuples { 3 | pub u8_u8: (u8, u8), 4 | pub u8_u16: (u8, u16), 5 | pub string_custom: (String, Custom), 6 | } 7 | 8 | #[derive(BorshSerialize)] 9 | pub struct NestedTwoElementTuples { 10 | pub vec_u8_u8: Vec<(u8, u8)>, 11 | pub hash_map_u8_u16_string_custom: HashMap<(u8, u16), (String, Custom)>, 12 | } 13 | 14 | #[derive(BorshSerialize)] 15 | pub struct MoreElementTuples { 16 | pub u8_u8_u8: (u8, u8, u8), 17 | pub u8_u16_i32_bool: (u8, u16, i32, bool), 18 | pub string_custom_option_i128_u8_u16_u32_u64: 19 | (String, Custom, Option, u8, u16, u32, u64), 20 | } 21 | 22 | #[derive(BorshSerialize)] 23 | pub struct NestedMultiElementTuples { 24 | pub vec_u8_u8_u16_u16_u32_uy32: Vec<(u8, u8, u16, u16, u32, u32)>, 25 | pub hash_map_u8_u16_u32_string_custom_u8_u8: 26 | HashMap<(u8, u16, u32), (String, Custom, u8, u8)>, 27 | } 28 | -------------------------------------------------------------------------------- /shank-idl/tests/fixtures/types/valid_single_data_enum.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "", 3 | "name": "", 4 | "instructions": [], 5 | "types": [ 6 | { 7 | "name": "CollectionInfo", 8 | "type": { 9 | "kind": "enum", 10 | "variants": [ 11 | { 12 | "name": "V1", 13 | "fields": [ 14 | { 15 | "name": "symbol", 16 | "type": "string" 17 | }, 18 | { 19 | "name": "verified_creators", 20 | "type": { 21 | "vec": "publicKey" 22 | } 23 | }, 24 | { 25 | "name": "whitelist_root", 26 | "type": { 27 | "array": ["u8", 32] 28 | } 29 | } 30 | ] 31 | }, 32 | { 33 | "name": "V2", 34 | "fields": [ 35 | { 36 | "name": "collection_mint", 37 | "type": "publicKey" 38 | } 39 | ] 40 | } 41 | ] 42 | } 43 | } 44 | ], 45 | "metadata": { 46 | "origin": "shank" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /shank-idl/tests/fixtures/types/valid_single_data_enum.rs: -------------------------------------------------------------------------------- 1 | // https://github.com/metaplex-foundation/solita/issues/53#issuecomment-1133910360 2 | #[derive(BorshSerialize)] 3 | pub enum CollectionInfo { 4 | V1 { 5 | symbol: String, 6 | verified_creators: Vec, 7 | whitelist_root: [u8; 32], 8 | }, 9 | V2 { 10 | collection_mint: Pubkey, 11 | }, 12 | } 13 | -------------------------------------------------------------------------------- /shank-idl/tests/fixtures/types/valid_single_enum.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "", 3 | "name": "", 4 | "instructions": [], 5 | "types": [ 6 | { 7 | "name": "Color", 8 | "type": { 9 | "kind": "enum", 10 | "variants": [ 11 | { 12 | "name": "Red", 13 | "fields": ["u8"] 14 | }, 15 | { 16 | "name": "Green", 17 | "fields": ["u8"] 18 | }, 19 | { 20 | "name": "Blue", 21 | "fields": ["u8"] 22 | }, 23 | { 24 | "name": "White" 25 | } 26 | ] 27 | } 28 | } 29 | ], 30 | "metadata": { 31 | "origin": "shank" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /shank-idl/tests/fixtures/types/valid_single_enum.rs: -------------------------------------------------------------------------------- 1 | #[derive(BorshSerialize)] 2 | pub enum Color { 3 | Red(u8), 4 | Green(u8), 5 | Blue(u8), 6 | White, 7 | } 8 | -------------------------------------------------------------------------------- /shank-idl/tests/fixtures/types/valid_single_enum_shank_type.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "", 3 | "name": "", 4 | "instructions": [], 5 | "types": [ 6 | { 7 | "name": "Color", 8 | "type": { 9 | "kind": "enum", 10 | "variants": [ 11 | { 12 | "name": "Red", 13 | "fields": ["u8"] 14 | }, 15 | { 16 | "name": "Green", 17 | "fields": ["u8"] 18 | }, 19 | { 20 | "name": "Blue", 21 | "fields": ["u8"] 22 | }, 23 | { 24 | "name": "White" 25 | } 26 | ] 27 | } 28 | } 29 | ], 30 | "metadata": { 31 | "origin": "shank" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /shank-idl/tests/fixtures/types/valid_single_enum_shank_type.rs: -------------------------------------------------------------------------------- 1 | #[derive(ShankType)] 2 | pub enum Color { 3 | Red(u8), 4 | Green(u8), 5 | Blue(u8), 6 | White, 7 | } 8 | -------------------------------------------------------------------------------- /shank-idl/tests/fixtures/types/valid_single_struct.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "", 3 | "name": "", 4 | "instructions": [], 5 | "types": [ 6 | { 7 | "name": "MyType", 8 | "type": { 9 | "kind": "struct", 10 | "fields": [ 11 | { 12 | "name": "field", 13 | "type": "u8" 14 | } 15 | ] 16 | } 17 | } 18 | ], 19 | "metadata": { 20 | "origin": "shank" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /shank-idl/tests/fixtures/types/valid_single_struct.rs: -------------------------------------------------------------------------------- 1 | #[derive(BorshSerialize)] 2 | pub struct MyType { 3 | pub field: u8, 4 | } 5 | -------------------------------------------------------------------------------- /shank-idl/tests/fixtures/types/valid_single_struct_shank_type.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "", 3 | "name": "", 4 | "instructions": [], 5 | "types": [ 6 | { 7 | "name": "MyType", 8 | "type": { 9 | "kind": "struct", 10 | "fields": [ 11 | { 12 | "name": "field", 13 | "type": "u8" 14 | } 15 | ] 16 | } 17 | } 18 | ], 19 | "metadata": { 20 | "origin": "shank" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /shank-idl/tests/fixtures/types/valid_single_struct_shank_type.rs: -------------------------------------------------------------------------------- 1 | #[derive(ShankType)] 2 | pub struct MyType { 3 | pub field: u8, 4 | } 5 | -------------------------------------------------------------------------------- /shank-idl/tests/instructions.rs: -------------------------------------------------------------------------------- 1 | use std::path::{Path, PathBuf}; 2 | 3 | use shank_idl::{idl::Idl, parse_file, ParseIdlConfig}; 4 | 5 | fn fixtures_dir() -> PathBuf { 6 | let root_dir = Path::new(env!("CARGO_MANIFEST_DIR")); 7 | root_dir.join("tests").join("fixtures").join("instructions") 8 | } 9 | 10 | #[test] 11 | fn instruction_from_single_file_no_args() { 12 | let file = fixtures_dir() 13 | .join("single_file") 14 | .join("instruction_no_args.rs"); 15 | let idl = parse_file(file, &ParseIdlConfig::optional_program_address()) 16 | .expect("Parsing should not fail") 17 | .expect("File contains IDL"); 18 | 19 | // eprintln!("{}", idl.try_into_json().unwrap()); 20 | 21 | let expected_idl: Idl = serde_json::from_str(include_str!( 22 | "./fixtures/instructions/single_file/instruction_no_args.json" 23 | )) 24 | .unwrap(); 25 | 26 | assert_eq!(idl, expected_idl); 27 | } 28 | 29 | #[test] 30 | fn instruction_from_single_file_with_args() { 31 | let file = fixtures_dir() 32 | .join("single_file") 33 | .join("instruction_with_args.rs"); 34 | let idl = parse_file(file, &ParseIdlConfig::optional_program_address()) 35 | .expect("Parsing should not fail") 36 | .expect("File contains IDL"); 37 | 38 | let expected_idl: Idl = serde_json::from_str(include_str!( 39 | "./fixtures/instructions/single_file/instruction_with_args.json" 40 | )) 41 | .unwrap(); 42 | 43 | assert_eq!(idl, expected_idl); 44 | } 45 | 46 | #[test] 47 | fn instruction_from_single_file_with_struct_args() { 48 | let file = fixtures_dir() 49 | .join("single_file") 50 | .join("instruction_with_struct_args.rs"); 51 | let idl = parse_file(file, &ParseIdlConfig::optional_program_address()) 52 | .expect("Parsing should not fail") 53 | .expect("File contains IDL"); 54 | 55 | let expected_idl: Idl = serde_json::from_str(include_str!( 56 | "./fixtures/instructions/single_file/instruction_with_struct_args.json" 57 | )) 58 | .unwrap(); 59 | 60 | assert_eq!(idl, expected_idl); 61 | } 62 | 63 | #[test] 64 | fn instruction_from_single_file_with_multiple_args() { 65 | let file = fixtures_dir() 66 | .join("single_file") 67 | .join("instruction_with_multiple_args.rs"); 68 | let idl = parse_file(file, &ParseIdlConfig::optional_program_address()) 69 | .expect("Parsing should not fail") 70 | .expect("File contains IDL"); 71 | 72 | let expected_idl: Idl = serde_json::from_str(include_str!( 73 | "./fixtures/instructions/single_file/instruction_with_multiple_args.json" 74 | )) 75 | .unwrap(); 76 | 77 | assert_eq!(idl, expected_idl); 78 | } 79 | 80 | #[test] 81 | fn instruction_from_single_file_with_idl_instructions() { 82 | let file = fixtures_dir() 83 | .join("single_file") 84 | .join("create_idl_instructions.rs"); 85 | let idl = parse_file(file, &ParseIdlConfig::optional_program_address()) 86 | .expect("Parsing should not fail") 87 | .expect("File contains IDL"); 88 | 89 | let expected_idl: Idl = serde_json::from_str(include_str!( 90 | "./fixtures/instructions/single_file/create_idl_instructions.json" 91 | )) 92 | .unwrap(); 93 | 94 | println!("IDL: {}", idl.try_into_json().unwrap()); 95 | 96 | println!("Expected: {}", expected_idl.try_into_json().unwrap()); 97 | 98 | assert_eq!(idl, expected_idl); 99 | } 100 | 101 | #[test] 102 | fn instruction_from_single_file_with_optional_account() { 103 | let file = fixtures_dir() 104 | .join("single_file") 105 | .join("instruction_with_optional_account.rs"); 106 | let idl = parse_file(file, &ParseIdlConfig::optional_program_address()) 107 | .expect("Parsing should not fail") 108 | .expect("File contains IDL"); 109 | 110 | let expected_idl: Idl = serde_json::from_str(include_str!( 111 | "./fixtures/instructions/single_file/instruction_with_optional_account.json" 112 | )) 113 | .unwrap(); 114 | 115 | assert_eq!(idl, expected_idl); 116 | } 117 | 118 | #[test] 119 | fn instruction_from_single_file_with_optional_account_defaulting() { 120 | let file = fixtures_dir() 121 | .join("single_file") 122 | .join("instruction_with_optional_account_defaulting.rs"); 123 | let idl = parse_file(file, &ParseIdlConfig::optional_program_address()) 124 | .expect("Parsing should not fail") 125 | .expect("File contains IDL"); 126 | 127 | let expected_idl: Idl = serde_json::from_str(include_str!( 128 | "./fixtures/instructions/single_file/instruction_with_optional_account_defaulting.json" 129 | )) 130 | .unwrap(); 131 | 132 | assert_eq!(idl, expected_idl); 133 | } 134 | 135 | #[test] 136 | fn instruction_from_single_file_invalid_attr() { 137 | let file = fixtures_dir() 138 | .join("single_file") 139 | .join("instruction_invalid_attr.rs"); 140 | let res = parse_file(file, &ParseIdlConfig::optional_program_address()); 141 | 142 | let err = res.unwrap_err(); 143 | let source_string = err.source().unwrap().to_string(); 144 | assert!(source_string.contains("Invalid")); 145 | assert!(source_string.contains("account meta configuration")); 146 | } 147 | 148 | #[test] 149 | fn instruction_from_single_file_invalid_discriminant() { 150 | let file = fixtures_dir() 151 | .join("single_file") 152 | .join("instruction_invalid_discriminant.rs"); 153 | let res = parse_file(file, &ParseIdlConfig::optional_program_address()); 154 | 155 | let err = res.unwrap_err().to_string(); 156 | assert!(err.contains("discriminants have to be <= u8::MAX")); 157 | assert!(err.contains("discriminant of variant 'CreateThing' is 256")); 158 | } 159 | 160 | #[test] 161 | fn instruction_from_single_file_with_optional_signer_account() { 162 | let file = fixtures_dir() 163 | .join("single_file") 164 | .join("instruction_with_optional_signer_account.rs"); 165 | let idl = parse_file(file, &ParseIdlConfig::optional_program_address()) 166 | .expect("Parsing should not fail") 167 | .expect("File contains IDL"); 168 | 169 | let expected_idl: Idl = serde_json::from_str(include_str!( 170 | "./fixtures/instructions/single_file/instruction_with_optional_signer_account.json" 171 | )) 172 | .unwrap(); 173 | 174 | assert_eq!(idl, expected_idl); 175 | } 176 | 177 | #[test] 178 | fn instruction_from_single_file_with_docs() { 179 | let file = fixtures_dir() 180 | .join("single_file") 181 | .join("instruction_with_docs.rs"); 182 | let idl = parse_file(file, &ParseIdlConfig::optional_program_address()) 183 | .expect("Parsing should not fail") 184 | .expect("File contains IDL"); 185 | 186 | let expected_idl: Idl = serde_json::from_str(include_str!( 187 | "./fixtures/instructions/single_file/instruction_with_docs.json" 188 | )) 189 | .unwrap(); 190 | 191 | assert_eq!(idl, expected_idl); 192 | } 193 | -------------------------------------------------------------------------------- /shank-idl/tests/macros.rs: -------------------------------------------------------------------------------- 1 | use std::path::{Path, PathBuf}; 2 | 3 | use shank_idl::{idl::Idl, parse_file, ParseIdlConfig}; 4 | 5 | fn fixtures_dir() -> PathBuf { 6 | let root_dir = Path::new(env!("CARGO_MANIFEST_DIR")); 7 | root_dir.join("tests").join("fixtures").join("macros") 8 | } 9 | 10 | #[test] 11 | fn macro_valid_program_id() { 12 | let file = fixtures_dir().join("program_id_valid.rs"); 13 | let idl = parse_file(file, &ParseIdlConfig::default()) 14 | .expect("Parsing should not fail") 15 | .expect("File contains IDL"); 16 | 17 | let expected_idl: Idl = serde_json::from_str(include_str!( 18 | "./fixtures/macros/program_id_valid.json" 19 | )) 20 | .unwrap(); 21 | 22 | assert_eq!(idl, expected_idl); 23 | } 24 | 25 | #[test] 26 | fn macro_missing_program_id() { 27 | let file = fixtures_dir().join("program_id_missing.rs"); 28 | let err = parse_file(file, &ParseIdlConfig::default()) 29 | .expect_err("Should fail") 30 | .to_string(); 31 | assert!(err.contains("Could not find")); 32 | assert!(err.contains("declare_id")); 33 | } 34 | 35 | #[test] 36 | fn macro_missing_program_id_not_required() { 37 | let file = fixtures_dir().join("program_id_missing.rs"); 38 | let idl = parse_file(file, &ParseIdlConfig::optional_program_address()) 39 | .expect("Parsing should not fail") 40 | .expect("File contains IDL"); 41 | 42 | let expected_idl: Idl = serde_json::from_str(include_str!( 43 | "./fixtures/macros/program_id_missing.json" 44 | )) 45 | .unwrap(); 46 | 47 | assert_eq!(idl, expected_idl); 48 | } 49 | -------------------------------------------------------------------------------- /shank-idl/tests/types.rs: -------------------------------------------------------------------------------- 1 | use std::path::{Path, PathBuf}; 2 | 3 | use shank_idl::{idl::Idl, parse_file, ParseIdlConfig}; 4 | 5 | fn fixtures_dir() -> PathBuf { 6 | let root_dir = Path::new(env!("CARGO_MANIFEST_DIR")); 7 | root_dir.join("tests").join("fixtures").join("types") 8 | } 9 | 10 | #[test] 11 | fn type_valid_single_struct() { 12 | let file = fixtures_dir().join("valid_single_struct.rs"); 13 | let idl = parse_file(file, &ParseIdlConfig::optional_program_address()) 14 | .expect("Parsing should not fail") 15 | .expect("File contains IDL"); 16 | 17 | let expected_idl: Idl = serde_json::from_str(include_str!( 18 | "./fixtures/types/valid_single_struct.json" 19 | )) 20 | .unwrap(); 21 | 22 | assert_eq!(idl, expected_idl); 23 | } 24 | 25 | #[test] 26 | fn type_valid_single_emum() { 27 | let file = fixtures_dir().join("valid_single_enum.rs"); 28 | let idl = parse_file(file, &ParseIdlConfig::optional_program_address()) 29 | .expect("Parsing should not fail") 30 | .expect("File contains IDL"); 31 | 32 | let expected_idl: Idl = serde_json::from_str(include_str!( 33 | "./fixtures/types/valid_single_enum.json" 34 | )) 35 | .unwrap(); 36 | 37 | assert_eq!(idl, expected_idl); 38 | } 39 | 40 | #[test] 41 | fn type_valid_single_data_emum() { 42 | let file = fixtures_dir().join("valid_single_data_enum.rs"); 43 | let idl = parse_file(file, &ParseIdlConfig::optional_program_address()) 44 | .expect("Parsing should not fail") 45 | .expect("File contains IDL"); 46 | 47 | let expected_idl: Idl = serde_json::from_str(include_str!( 48 | "./fixtures/types/valid_single_data_enum.json" 49 | )) 50 | .unwrap(); 51 | 52 | assert_eq!(idl, expected_idl); 53 | } 54 | 55 | #[test] 56 | fn type_valid_multiple() { 57 | let file = fixtures_dir().join("valid_multiple.rs"); 58 | let idl = parse_file(file, &ParseIdlConfig::optional_program_address()) 59 | .expect("Parsing should not fail") 60 | .expect("File contains IDL"); 61 | // eprintln!("{}", serde_json::to_string_pretty(&idl).unwrap()); 62 | 63 | let expected_idl: Idl = serde_json::from_str(include_str!( 64 | "./fixtures/types/valid_multiple.json" 65 | )) 66 | .unwrap(); 67 | 68 | assert_eq!(idl, expected_idl); 69 | } 70 | 71 | #[test] 72 | fn type_invalid_single() { 73 | let file = fixtures_dir().join("invalid_single.rs"); 74 | assert!( 75 | parse_file(file, &ParseIdlConfig::optional_program_address()).is_err() 76 | ) 77 | } 78 | 79 | #[test] 80 | fn type_valid_maps() { 81 | let file = fixtures_dir().join("valid_multiple_maps.rs"); 82 | let idl = parse_file(file, &ParseIdlConfig::optional_program_address()) 83 | .expect("Parsing should not fail") 84 | .expect("File contains IDL"); 85 | // eprintln!("{}", serde_json::to_string_pretty(&idl).unwrap()); 86 | 87 | let expected_idl: Idl = serde_json::from_str(include_str!( 88 | "./fixtures/types/valid_multiple_maps.json" 89 | )) 90 | .unwrap(); 91 | 92 | assert_eq!(idl, expected_idl); 93 | } 94 | 95 | #[test] 96 | fn type_valid_sets() { 97 | let file = fixtures_dir().join("valid_multiple_sets.rs"); 98 | let idl = parse_file(file, &ParseIdlConfig::optional_program_address()) 99 | .expect("Parsing should not fail") 100 | .expect("File contains IDL"); 101 | // eprintln!("{}", serde_json::to_string_pretty(&idl).unwrap()); 102 | 103 | let expected_idl: Idl = serde_json::from_str(include_str!( 104 | "./fixtures/types/valid_multiple_sets.json" 105 | )) 106 | .unwrap(); 107 | 108 | assert_eq!(idl, expected_idl); 109 | } 110 | 111 | #[test] 112 | fn type_valid_tuples() { 113 | let file = fixtures_dir().join("valid_multiple_tuples.rs"); 114 | let idl = parse_file(file, &ParseIdlConfig::optional_program_address()) 115 | .expect("Parsing should not fail") 116 | .expect("File contains IDL"); 117 | // eprintln!("{}", serde_json::to_string_pretty(&idl).unwrap()); 118 | 119 | let expected_idl: Idl = serde_json::from_str(include_str!( 120 | "./fixtures/types/valid_multiple_tuples.json" 121 | )) 122 | .unwrap(); 123 | 124 | assert_eq!(idl, expected_idl); 125 | } 126 | 127 | #[test] 128 | fn type_valid_single_struct_shank_type() { 129 | let file = fixtures_dir().join("valid_single_struct_shank_type.rs"); 130 | let idl = parse_file(file, &ParseIdlConfig::optional_program_address()) 131 | .expect("Parsing should not fail") 132 | .expect("File contains IDL"); 133 | // eprintln!("{}", serde_json::to_string_pretty(&idl).unwrap()); 134 | 135 | let expected_idl: Idl = serde_json::from_str(include_str!( 136 | "./fixtures/types/valid_single_struct_shank_type.json" 137 | )) 138 | .unwrap(); 139 | 140 | assert_eq!(idl, expected_idl); 141 | } 142 | 143 | #[test] 144 | fn type_valid_single_enum_shank_type() { 145 | let file = fixtures_dir().join("valid_single_enum_shank_type.rs"); 146 | let idl = parse_file(file, &ParseIdlConfig::optional_program_address()) 147 | .expect("Parsing should not fail") 148 | .expect("File contains IDL"); 149 | // eprintln!("{}", serde_json::to_string_pretty(&idl).unwrap()); 150 | 151 | let expected_idl: Idl = serde_json::from_str(include_str!( 152 | "./fixtures/types/valid_single_enum_shank_type.json" 153 | )) 154 | .unwrap(); 155 | 156 | assert_eq!(idl, expected_idl); 157 | } 158 | -------------------------------------------------------------------------------- /shank-macro-impl/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "shank_macro_impl" 3 | version = "0.4.3" 4 | description = "Implements and tests shank derive macros" 5 | authors = ["Metaplex Maintainers "] 6 | repository = "https://github.com/metaplex-foundation/shank" 7 | license = "Apache-2.0" 8 | edition = "2018" 9 | 10 | [dependencies] 11 | anyhow = "1.0.48" 12 | proc-macro2 = "1.0.32" 13 | quote = "1.0.21" 14 | serde = { version = "1.0.130", features = ["derive"] } 15 | syn = { version = "1.0.82", features = ["extra-traits", "full"] } 16 | 17 | [dev-dependencies] 18 | assert_matches = "1.5.0" 19 | -------------------------------------------------------------------------------- /shank-macro-impl/README.md: -------------------------------------------------------------------------------- 1 | # shank-macro-impl 2 | 3 | Implements and tests [shank](https://github.com/metaplex-foundation/shank) _derive_ macros. 4 | 5 | ## LICENSE 6 | 7 | Apache-2.0 8 | -------------------------------------------------------------------------------- /shank-macro-impl/rustfmt.toml: -------------------------------------------------------------------------------- 1 | imports_granularity = "Crate" 2 | max_width = 80 3 | -------------------------------------------------------------------------------- /shank-macro-impl/src/account/mod.rs: -------------------------------------------------------------------------------- 1 | mod extract_accounts; 2 | pub use extract_accounts::*; 3 | -------------------------------------------------------------------------------- /shank-macro-impl/src/builder/argument.rs: -------------------------------------------------------------------------------- 1 | use std::convert::TryFrom; 2 | 3 | use syn::{ 4 | Attribute, Error as ParseError, Expr, ExprPath, ExprType, GenericArgument, 5 | Path, PathArguments, Result as ParseResult, Type, TypePath, 6 | }; 7 | 8 | const INSTRUCTION_ARGUMENT: &str = "args"; 9 | 10 | #[derive(Debug, PartialEq, Eq, Hash)] 11 | pub struct BuilderArgument { 12 | pub name: String, 13 | pub ty: String, 14 | pub generic_ty: Option, 15 | } 16 | 17 | #[derive(Debug, PartialEq, Eq)] 18 | pub struct BuilderArguments(pub Vec); 19 | 20 | impl BuilderArgument { 21 | fn is_argument_attr(attr: &Attribute) -> Option<&Attribute> { 22 | match attr 23 | .path 24 | .get_ident() 25 | .map(|x| x.to_string().as_str() == INSTRUCTION_ARGUMENT) 26 | { 27 | Some(true) => Some(attr), 28 | _ => None, 29 | } 30 | } 31 | 32 | pub fn from_argument_attr( 33 | attr: &Attribute, 34 | ) -> ParseResult { 35 | Self::parse_argument_tokens(attr.parse_args()?) 36 | } 37 | 38 | fn parse_argument_tokens(tokens: ExprType) -> ParseResult { 39 | let clone = tokens.clone(); 40 | // name 41 | let name = match *clone.expr { 42 | Expr::Path(ExprPath { 43 | path: Path { segments, .. }, 44 | .. 45 | }) => segments.first().unwrap().ident.to_string(), 46 | _ => { 47 | return Err(ParseError::new_spanned( 48 | tokens, 49 | "#[args] requires an expression 'name: type'", 50 | )) 51 | } 52 | }; 53 | // type 54 | match *clone.ty { 55 | Type::Path(TypePath { 56 | path: Path { segments, .. }, 57 | .. 58 | }) => { 59 | let segment = segments.first().unwrap(); 60 | 61 | // check whether we are dealing with a generic type 62 | let generic_ty = match &segment.arguments { 63 | PathArguments::AngleBracketed(arguments) => { 64 | if let Some(GenericArgument::Type(Type::Path(ty))) = 65 | arguments.args.first() 66 | { 67 | Some( 68 | ty.path 69 | .segments 70 | .first() 71 | .unwrap() 72 | .ident 73 | .to_string(), 74 | ) 75 | } else { 76 | None 77 | } 78 | } 79 | _ => None, 80 | }; 81 | 82 | Ok(BuilderArgument { 83 | name, 84 | ty: segment.ident.to_string(), 85 | generic_ty, 86 | }) 87 | } 88 | _ => Err(ParseError::new_spanned( 89 | tokens, 90 | "#[args] requires an expression 'name: type'", 91 | )), 92 | } 93 | } 94 | } 95 | 96 | impl TryFrom<&[Attribute]> for BuilderArguments { 97 | type Error = ParseError; 98 | 99 | fn try_from(attrs: &[Attribute]) -> ParseResult { 100 | let arguments = attrs 101 | .iter() 102 | .filter_map(BuilderArgument::is_argument_attr) 103 | .map(BuilderArgument::from_argument_attr) 104 | .collect::>>()?; 105 | 106 | Ok(BuilderArguments(arguments)) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /shank-macro-impl/src/builder/argument_test.rs: -------------------------------------------------------------------------------- 1 | use std::convert::TryInto; 2 | 3 | use proc_macro2::TokenStream; 4 | use quote::quote; 5 | use syn::ItemEnum; 6 | 7 | use syn::{Attribute, Result as ParseResult}; 8 | 9 | use crate::instruction::InstructionAccounts; 10 | 11 | use super::BuilderArguments; 12 | 13 | fn parse_first_enum_variant_attrs( 14 | code: TokenStream, 15 | ) -> ParseResult<(InstructionAccounts, BuilderArguments)> { 16 | let parsed = 17 | syn::parse2::(code).expect("Should parse successfully"); 18 | let attrs: &[Attribute] = parsed.variants.first().unwrap().attrs.as_ref(); 19 | let instruction_accounts = attrs.try_into()?; 20 | let instruction_arguments = attrs.try_into()?; 21 | Ok((instruction_accounts, instruction_arguments)) 22 | } 23 | 24 | #[test] 25 | fn instruction_with_args() { 26 | let (_, arguments) = parse_first_enum_variant_attrs(quote! { 27 | #[derive(ShankBuilder)] 28 | pub enum Instructions { 29 | #[account(name="authority")] 30 | #[args(authority_id: u64)] 31 | NonIndexed 32 | } 33 | }) 34 | .expect("Should parse fine"); 35 | 36 | assert_eq!(arguments.0.len(), 1, "includes one instruction argument"); 37 | assert!( 38 | arguments.0.first().unwrap().name == "authority_id", 39 | "to instruction argument" 40 | ); 41 | } 42 | 43 | #[test] 44 | fn instruction_with_multiple_args() { 45 | let (_, arguments) = parse_first_enum_variant_attrs(quote! { 46 | #[derive(ShankBuilder)] 47 | pub enum Instructions { 48 | #[account(name="authority")] 49 | #[args(id_1: u64)] 50 | #[args(id_2: u64)] 51 | #[args(id_3: u64)] 52 | NonIndexed 53 | } 54 | }) 55 | .expect("Should parse fine"); 56 | 57 | assert_eq!( 58 | arguments.0.len(), 59 | 3, 60 | "includes multuple instruction arguments" 61 | ); 62 | } 63 | 64 | #[test] 65 | fn instruction_without_args() { 66 | let (_, arguments) = parse_first_enum_variant_attrs(quote! { 67 | #[derive(ShankBuilder)] 68 | pub enum Instructions { 69 | #[account(name="authority")] 70 | NonIndexed 71 | } 72 | }) 73 | .expect("Should parse fine"); 74 | 75 | assert_eq!(arguments.0.len(), 0, "includes no instruction argument"); 76 | } 77 | -------------------------------------------------------------------------------- /shank-macro-impl/src/builder/builder.rs: -------------------------------------------------------------------------------- 1 | use std::convert::{TryFrom, TryInto}; 2 | use syn::{Attribute, Error as ParseError, ItemEnum, Result as ParseResult}; 3 | 4 | use syn::Ident; 5 | 6 | use crate::instruction::InstructionVariantFields; 7 | use crate::parsed_enum::ParsedEnum; 8 | use crate::parsers::get_derive_attr; 9 | use crate::DERIVE_BUILDER_ATTR; 10 | use crate::{ 11 | instruction::{InstructionAccount, InstructionAccounts}, 12 | parsed_enum::ParsedEnumVariant, 13 | }; 14 | 15 | use super::{BuilderArgument, BuilderArguments}; 16 | 17 | // ----------------- 18 | // Instruction 19 | // ----------------- 20 | #[derive(Debug)] 21 | pub struct Builder { 22 | pub ident: Ident, 23 | pub variants: Vec, 24 | } 25 | 26 | impl Builder { 27 | pub fn try_from_item_enum( 28 | item_enum: &ItemEnum, 29 | skip_derive_attr_check: bool, 30 | ) -> ParseResult> { 31 | if skip_derive_attr_check 32 | || get_derive_attr(&item_enum.attrs, DERIVE_BUILDER_ATTR).is_some() 33 | { 34 | let parsed_enum = ParsedEnum::try_from(item_enum)?; 35 | Builder::try_from(&parsed_enum).map(Some) 36 | } else { 37 | Ok(None) 38 | } 39 | } 40 | } 41 | 42 | impl TryFrom<&ParsedEnum> for Option { 43 | type Error = ParseError; 44 | 45 | fn try_from(parsed_enum: &ParsedEnum) -> ParseResult { 46 | match get_derive_attr(&parsed_enum.attrs, DERIVE_BUILDER_ATTR) 47 | .map(|_| parsed_enum) 48 | { 49 | Some(builder_enum) => builder_enum.try_into().map(Some), 50 | None => Ok(None), 51 | } 52 | } 53 | } 54 | 55 | impl TryFrom<&ParsedEnum> for Builder { 56 | type Error = ParseError; 57 | 58 | fn try_from(parsed_enum: &ParsedEnum) -> ParseResult { 59 | let ParsedEnum { 60 | ident, variants, .. 61 | } = parsed_enum; 62 | 63 | let variants = variants 64 | .iter() 65 | .map(BuilderVariant::try_from) 66 | .collect::>>()?; 67 | Ok(Self { 68 | ident: ident.clone(), 69 | variants, 70 | }) 71 | } 72 | } 73 | 74 | // ----------------- 75 | // Builder Variant 76 | // ----------------- 77 | #[derive(Debug)] 78 | pub struct BuilderVariant { 79 | pub ident: Ident, 80 | pub field_tys: InstructionVariantFields, 81 | pub accounts: Vec, 82 | pub arguments: Vec, 83 | pub discriminant: usize, 84 | } 85 | 86 | impl TryFrom<&ParsedEnumVariant> for BuilderVariant { 87 | type Error = ParseError; 88 | 89 | fn try_from(variant: &ParsedEnumVariant) -> ParseResult { 90 | let ParsedEnumVariant { 91 | ident, 92 | fields, 93 | discriminant, 94 | attrs, 95 | .. 96 | } = variant; 97 | 98 | let field_tys: InstructionVariantFields = if !fields.is_empty() { 99 | // Determine if the InstructionType is tuple or struct variant 100 | let field = fields.first().unwrap(); 101 | match &field.ident { 102 | Some(_) => InstructionVariantFields::Named( 103 | fields 104 | .iter() 105 | .map(|x| { 106 | ( 107 | x.ident.as_ref().unwrap().to_string(), 108 | x.rust_type.clone(), 109 | ) 110 | }) 111 | .collect(), 112 | ), 113 | None => InstructionVariantFields::Unnamed( 114 | fields.iter().map(|x| x.rust_type.clone()).collect(), 115 | ), 116 | } 117 | } else { 118 | InstructionVariantFields::Unnamed(vec![]) 119 | }; 120 | 121 | let attrs: &[Attribute] = attrs.as_ref(); 122 | let accounts: InstructionAccounts = attrs.try_into()?; 123 | let arguments: BuilderArguments = attrs.try_into()?; 124 | 125 | Ok(Self { 126 | ident: ident.clone(), 127 | field_tys, 128 | accounts: accounts.0, 129 | arguments: arguments.0, 130 | discriminant: *discriminant, 131 | }) 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /shank-macro-impl/src/builder/builder_test.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use quote::quote; 3 | use syn::{ItemEnum, Result as ParseResult}; 4 | 5 | use crate::{instruction::InstructionVariantFields, types::RustType}; 6 | 7 | use super::{ 8 | builder::{Builder, BuilderVariant}, 9 | BuilderArgument, 10 | }; 11 | 12 | fn parse_instruction(code: TokenStream) -> ParseResult> { 13 | let item_enum = syn::parse2::(code) 14 | .expect("Should parse ItemEnum successfully"); 15 | Builder::try_from_item_enum(&item_enum, false) 16 | } 17 | 18 | fn assert_context_variant( 19 | variant: &BuilderVariant, 20 | name: &str, 21 | expected_discriminant: usize, 22 | expected_field_tys: &Vec, 23 | expected_args: &Vec, 24 | accounts_len: usize, 25 | ) { 26 | let BuilderVariant { 27 | ident, 28 | field_tys, 29 | accounts, 30 | discriminant, 31 | arguments, 32 | .. 33 | } = variant; 34 | 35 | assert_eq!(ident.to_string(), name); 36 | assert_eq!(discriminant, &expected_discriminant, "discriminant"); 37 | assert_eq!(accounts.len(), accounts_len, "accounts"); 38 | match field_tys { 39 | InstructionVariantFields::Named(field_tys) => { 40 | assert_eq!( 41 | field_tys.len(), 42 | expected_field_tys.len(), 43 | "fields size" 44 | ); 45 | for field_idx in 0..expected_field_tys.len() { 46 | let (_field_name, field_ty) = field_tys.get(field_idx).unwrap(); 47 | let expected_field_ty = 48 | expected_field_tys.get(field_idx).unwrap(); 49 | assert_eq!(field_ty, expected_field_ty, "field type"); 50 | } 51 | } 52 | InstructionVariantFields::Unnamed(field_tys) => { 53 | assert_eq!( 54 | field_tys.len(), 55 | expected_field_tys.len(), 56 | "fields size" 57 | ); 58 | for field_idx in 0..expected_field_tys.len() { 59 | let field_ty = field_tys.get(field_idx).unwrap(); 60 | let expected_field_ty = 61 | expected_field_tys.get(field_idx).unwrap(); 62 | assert_eq!(field_ty, expected_field_ty, "field type"); 63 | } 64 | } 65 | } 66 | 67 | assert_eq!(arguments.len(), expected_args.len(), "arguments"); 68 | 69 | for argument_idx in 0..expected_args.len() { 70 | let BuilderArgument { 71 | name, 72 | ty, 73 | generic_ty, 74 | } = arguments.get(argument_idx).unwrap(); 75 | 76 | let BuilderArgument { 77 | name: expected_name, 78 | ty: expected_ty, 79 | generic_ty: expected_generic_ty, 80 | } = expected_args.get(argument_idx).unwrap(); 81 | 82 | assert_eq!(name, expected_name, "argument name"); 83 | assert_eq!(ty, expected_ty, "argument type"); 84 | assert_eq!(generic_ty, expected_generic_ty, "argument generic type"); 85 | } 86 | } 87 | 88 | #[test] 89 | fn parse_c_style_instruction_with_context() { 90 | let parsed = parse_instruction(quote! { 91 | #[derive(ShankInstruction, ShankBuilder)] 92 | pub enum Instruction { 93 | #[account(0, name = "creator", sig)] 94 | #[account(1, name = "thing", mut, optional)] 95 | #[args(first_arg: u64)] 96 | #[args(second_arg: u64)] 97 | CreateThing, 98 | #[account(name = "creator", sig)] 99 | #[args(composite_arg: Vec)] 100 | CloseThing, 101 | } 102 | }) 103 | .expect("Should parse fine") 104 | .expect("Should be instruction"); 105 | 106 | assert_eq!(parsed.ident.to_string(), "Instruction", "enum ident"); 107 | assert_eq!(parsed.variants.len(), 2, "variants"); 108 | assert!( 109 | !parsed.variants[0].accounts[0].optional, 110 | "non-optional account of first variant" 111 | ); 112 | assert!( 113 | parsed.variants[0].accounts[1].optional, 114 | "optional account of first variant" 115 | ); 116 | assert!( 117 | !parsed.variants[1].accounts[0].optional, 118 | "non-optional account of second variant" 119 | ); 120 | 121 | assert_context_variant( 122 | &parsed.variants[0], 123 | "CreateThing", 124 | 0, 125 | &vec![], 126 | &vec![ 127 | BuilderArgument { 128 | name: String::from("first_arg"), 129 | ty: String::from("u64"), 130 | generic_ty: None, 131 | }, 132 | BuilderArgument { 133 | name: String::from("second_arg"), 134 | ty: String::from("u64"), 135 | generic_ty: None, 136 | }, 137 | ], 138 | 2, 139 | ); 140 | assert_context_variant( 141 | &parsed.variants[1], 142 | "CloseThing", 143 | 1, 144 | &vec![], 145 | &vec![BuilderArgument { 146 | name: String::from("composite_arg"), 147 | ty: String::from("Vec"), 148 | generic_ty: Some(String::from("u64")), 149 | }], 150 | 1, 151 | ); 152 | } 153 | -------------------------------------------------------------------------------- /shank-macro-impl/src/builder/mod.rs: -------------------------------------------------------------------------------- 1 | mod argument; 2 | #[allow(clippy::module_inception)] 3 | mod builder; 4 | 5 | pub use argument::*; 6 | pub use builder::*; 7 | 8 | #[cfg(test)] 9 | mod argument_test; 10 | #[cfg(test)] 11 | mod builder_test; 12 | -------------------------------------------------------------------------------- /shank-macro-impl/src/converters.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | use syn::Error as ParseError; 3 | 4 | pub fn parse_error_into>(parse_err: T) -> Error { 5 | let parse_err: ParseError = parse_err.into(); 6 | Error::new(parse_err) 7 | .context("[ParseError] Run `cargo build` or `cargo check` in the program crate root for more details.".to_string()) 8 | } 9 | -------------------------------------------------------------------------------- /shank-macro-impl/src/custom_type/custom_enum.rs: -------------------------------------------------------------------------------- 1 | use std::{convert::TryFrom, ops::Deref}; 2 | 3 | use syn::{ 4 | parse::{Parse, ParseStream}, 5 | Error as ParseError, ItemEnum, Result as ParseResult, 6 | }; 7 | 8 | use crate::parsed_enum::ParsedEnum; 9 | 10 | use super::DetectCustomTypeConfig; 11 | 12 | pub struct CustomEnum(pub ParsedEnum); 13 | 14 | impl TryFrom<&ItemEnum> for CustomEnum { 15 | type Error = ParseError; 16 | 17 | fn try_from(item: &ItemEnum) -> ParseResult { 18 | Ok(Self(ParsedEnum::try_from(item)?)) 19 | } 20 | } 21 | 22 | impl Parse for CustomEnum { 23 | fn parse(input: ParseStream) -> ParseResult { 24 | let strct = ::parse(input)?; 25 | CustomEnum::try_from(&strct) 26 | } 27 | } 28 | 29 | impl Deref for CustomEnum { 30 | type Target = ParsedEnum; 31 | 32 | fn deref(&self) -> &Self::Target { 33 | &self.0 34 | } 35 | } 36 | 37 | impl CustomEnum { 38 | pub fn is_custom_enum(&self, config: &DetectCustomTypeConfig) -> bool { 39 | config.are_custom_type_attrs(&self.attrs) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /shank-macro-impl/src/custom_type/custom_struct.rs: -------------------------------------------------------------------------------- 1 | use std::{convert::TryFrom, ops::Deref}; 2 | use syn::{ 3 | parse::{Parse, ParseStream}, 4 | Error as ParseError, ItemStruct, Result as ParseResult, 5 | }; 6 | 7 | use crate::parsed_struct::ParsedStruct; 8 | 9 | use super::DetectCustomTypeConfig; 10 | 11 | // ----------------- 12 | // CustomStruct 13 | // ----------------- 14 | pub struct CustomStruct(pub ParsedStruct); 15 | 16 | impl TryFrom<&ItemStruct> for CustomStruct { 17 | type Error = ParseError; 18 | 19 | fn try_from(item: &ItemStruct) -> ParseResult { 20 | Ok(Self(ParsedStruct::try_from(item)?)) 21 | } 22 | } 23 | 24 | impl Parse for CustomStruct { 25 | fn parse(input: ParseStream) -> ParseResult { 26 | let strct = ::parse(input)?; 27 | CustomStruct::try_from(&strct) 28 | } 29 | } 30 | 31 | impl Deref for CustomStruct { 32 | type Target = ParsedStruct; 33 | 34 | fn deref(&self) -> &Self::Target { 35 | &self.0 36 | } 37 | } 38 | 39 | impl CustomStruct { 40 | pub fn is_custom_struct(&self, config: &DetectCustomTypeConfig) -> bool { 41 | config.are_custom_type_attrs(&self.attrs) 42 | } 43 | } 44 | 45 | // ----------------- 46 | // Tests 47 | // ----------------- 48 | #[cfg(test)] 49 | mod tests { 50 | use proc_macro2::TokenStream; 51 | use quote::quote; 52 | 53 | use crate::custom_type::parse_custom_struct; 54 | 55 | fn assert_is_custom(tokens: TokenStream) { 56 | assert!( 57 | parse_custom_struct(tokens).is_custom_struct(&Default::default()) 58 | ); 59 | } 60 | 61 | fn assert_is_not_custom(tokens: TokenStream) { 62 | assert!( 63 | !parse_custom_struct(tokens).is_custom_struct(&Default::default()) 64 | ); 65 | } 66 | 67 | #[test] 68 | fn is_custom_struct_missing_derive() { 69 | assert_is_not_custom(quote! { 70 | struct MyStruct {} 71 | }); 72 | 73 | assert_is_not_custom(quote! { 74 | #[BorshSerialize] 75 | struct MyStruct {} 76 | }); 77 | } 78 | 79 | #[test] 80 | fn is_custom_struct_including_derive() { 81 | assert_is_custom(quote! { 82 | #[derive(BorshSerialize)] 83 | struct MyStruct {} 84 | }); 85 | } 86 | 87 | #[test] 88 | fn is_custom_struct_including_borsh_derive_and_shank_derive() { 89 | assert_is_not_custom(quote! { 90 | #[derive(BorshSerialize, ShankInstruction)] 91 | struct MyStruct {} 92 | }); 93 | assert_is_not_custom(quote! { 94 | #[derive(BorshSerialize)] 95 | #[derive(ShankInstruction)] 96 | struct MyStruct {} 97 | }); 98 | } 99 | 100 | #[test] 101 | fn is_custom_struct_including_borsh_derive_and_shank_type() { 102 | assert_is_custom(quote! { 103 | #[derive(BorshSerialize, ShankType)] 104 | struct MyStruct {} 105 | }); 106 | assert_is_custom(quote! { 107 | #[derive(BorshSerialize)] 108 | #[derive(ShankType)] 109 | struct MyStruct {} 110 | }); 111 | } 112 | 113 | #[test] 114 | fn is_custom_struct_including_shank_type() { 115 | assert_is_custom(quote! { 116 | #[derive(ShankType)] 117 | struct MyStruct {} 118 | }); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /shank-macro-impl/src/custom_type/custom_type_config.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashSet, iter::FromIterator}; 2 | 3 | use syn::Attribute; 4 | 5 | use crate::{ 6 | parsers::get_derive_names, DERIVE_ACCOUNT_ATTR, DERIVE_INSTRUCTION_ATTR, 7 | }; 8 | 9 | #[derive(Debug)] 10 | pub struct DetectCustomTypeConfig { 11 | /// If any of those derives is detected that struct is considered a Custom Struct 12 | pub include_derives: HashSet, 13 | 14 | /// If any of those derives is detected that struct is NOT considered a Custom Struct 15 | pub skip_derives: HashSet, 16 | } 17 | 18 | impl Default for DetectCustomTypeConfig { 19 | fn default() -> Self { 20 | Self { 21 | include_derives: HashSet::from_iter( 22 | vec!["BorshSerialize", "BorshDeserialize", "ShankType"] 23 | .into_iter() 24 | .map(String::from), 25 | ), 26 | skip_derives: HashSet::from_iter( 27 | vec![DERIVE_ACCOUNT_ATTR, DERIVE_INSTRUCTION_ATTR] 28 | .into_iter() 29 | .map(String::from), 30 | ), 31 | } 32 | } 33 | } 34 | 35 | impl DetectCustomTypeConfig { 36 | pub fn are_custom_type_attrs(&self, attrs: &[Attribute]) -> bool { 37 | let derives = get_derive_names(attrs); 38 | let mut saw_include = false; 39 | for derive in derives { 40 | if self.skip_derives.contains(&derive) { 41 | return false; 42 | } 43 | if !saw_include { 44 | saw_include = self.include_derives.contains(&derive); 45 | } 46 | } 47 | saw_include 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /shank-macro-impl/src/custom_type/mod.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | 3 | mod custom_enum; 4 | mod custom_struct; 5 | mod custom_type_config; 6 | pub use custom_enum::*; 7 | pub use custom_struct::*; 8 | pub use custom_type_config::*; 9 | 10 | pub fn parse_custom_struct(item: TokenStream) -> CustomStruct { 11 | match syn::parse2::(item) { 12 | Ok(custom_struct) => custom_struct, 13 | Err(err) => panic!("{}", err), 14 | } 15 | } 16 | 17 | pub fn parse_custom_enum(item: TokenStream) -> CustomEnum { 18 | match syn::parse2::(item) { 19 | Ok(custom_enum) => custom_enum, 20 | Err(err) => panic!("{}", err), 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /shank-macro-impl/src/error/mod.rs: -------------------------------------------------------------------------------- 1 | mod program_error; 2 | mod this_error; 3 | 4 | pub use program_error::*; 5 | pub use this_error::*; 6 | 7 | pub const DERIVE_THIS_ERROR_ATTR: &str = "Error"; 8 | -------------------------------------------------------------------------------- /shank-macro-impl/src/error/this_error.rs: -------------------------------------------------------------------------------- 1 | use std::convert::TryFrom; 2 | 3 | use anyhow::{format_err, Result}; 4 | 5 | use crate::{parsed_enum::ParsedEnum, parsers::get_derive_attr}; 6 | use syn::Result as ParseResult; 7 | 8 | use super::{ProgramError, ProgramErrors, DERIVE_THIS_ERROR_ATTR}; 9 | 10 | fn filter_this_error_enums<'a>( 11 | enums: impl Iterator, 12 | ) -> Vec<&'a syn::ItemEnum> { 13 | enums 14 | .filter_map(|item_enum| { 15 | get_derive_attr(&item_enum.attrs, DERIVE_THIS_ERROR_ATTR) 16 | .map(|_| item_enum) 17 | }) 18 | .collect() 19 | } 20 | 21 | fn extract_this_error_enums<'a>( 22 | enums: impl Iterator, 23 | ) -> Result> { 24 | let mut error_enums = Vec::new(); 25 | 26 | for x in filter_this_error_enums(enums) { 27 | let enm = ParsedEnum::try_from(x).map_err(|err| { 28 | format_err!( 29 | "Encountered an error parsing {} this_error enum.\n{}", 30 | x.ident, 31 | err 32 | ) 33 | })?; 34 | error_enums.push(enm); 35 | } 36 | Ok(error_enums) 37 | } 38 | 39 | pub fn extract_this_errors<'a>( 40 | enums: impl Iterator, 41 | ) -> Result> { 42 | let this_error_enums = extract_this_error_enums(enums)?; 43 | let program_errors = this_error_enums 44 | .iter() 45 | .map(ProgramErrors::try_from) 46 | .collect::>>()? 47 | .into_iter() 48 | .flat_map(|x| x.0) 49 | .collect::>(); 50 | Ok(program_errors) 51 | } 52 | -------------------------------------------------------------------------------- /shank-macro-impl/src/instruction/extract_instructions.rs: -------------------------------------------------------------------------------- 1 | use super::Instruction; 2 | use syn::Result as ParseResult; 3 | 4 | pub fn extract_instruction_enums<'a>( 5 | enums: impl Iterator, 6 | ) -> ParseResult> { 7 | let ixs = enums 8 | .map(|x| Instruction::try_from_item_enum(x, false)) 9 | .collect::>>>()? 10 | .into_iter() 11 | .flatten() 12 | .collect::>(); 13 | 14 | Ok(ixs) 15 | } 16 | 17 | #[cfg(test)] 18 | mod tests { 19 | use proc_macro2::TokenStream; 20 | use quote::quote; 21 | use syn::ItemEnum; 22 | 23 | use super::*; 24 | 25 | fn parse_instructions( 26 | codes: Vec, 27 | ) -> ParseResult> { 28 | let item_enums = codes 29 | .into_iter() 30 | .map(syn::parse2::) 31 | .collect::>>() 32 | .expect("Should parse ItemEnum successfully"); 33 | 34 | extract_instruction_enums(item_enums.iter()) 35 | } 36 | 37 | fn instruction_valid() -> TokenStream { 38 | quote! { 39 | #[derive(ShankInstruction)] 40 | pub enum InstructionValid { 41 | #[account(0, name = "creator", sig)] 42 | CreateThing, 43 | CloseThing 44 | } 45 | } 46 | } 47 | fn instruction_missing_derive() -> TokenStream { 48 | quote! { 49 | pub enum InstructionMissingDerive { 50 | #[account(0, name = "creator", sig)] 51 | CreateThing, 52 | CloseThing 53 | } 54 | } 55 | } 56 | 57 | fn instruction_invalid_account_name() -> TokenStream { 58 | quote! { 59 | #[derive(ShankInstruction)] 60 | pub enum InstructionInvalidAccountName { 61 | #[account(naaame = "creator", sig)] 62 | CreateThing 63 | } 64 | } 65 | } 66 | 67 | fn instruction_unknown_account_attr() -> TokenStream { 68 | quote! { 69 | #[derive(ShankInstruction)] 70 | pub enum InstructionUnknownAccountAttr { 71 | #[account(name = "creator", unknown)] 72 | CreateThing 73 | } 74 | } 75 | } 76 | 77 | fn instruction_invalid_account_idx() -> TokenStream { 78 | quote! { 79 | #[derive(ShankInstruction)] 80 | pub enum InstructionUnknownAccountAttr { 81 | #[account(1, name = "creator")] 82 | CreateThing 83 | } 84 | } 85 | } 86 | 87 | #[test] 88 | fn extract_valid_instructions() { 89 | let ixs = parse_instructions(vec![ 90 | instruction_valid(), 91 | instruction_missing_derive(), 92 | ]) 93 | .expect("Should parse fine"); 94 | 95 | assert_eq!(ixs.len(), 1, "extracts the one valid instruction") 96 | } 97 | 98 | #[test] 99 | fn extract_valid_instruction_and_invalid_account_name() { 100 | let res = parse_instructions(vec![ 101 | instruction_valid(), 102 | instruction_invalid_account_name(), 103 | ]); 104 | assert!(res 105 | .unwrap_err() 106 | .to_string() 107 | .contains("Only desc/description or name")); 108 | } 109 | 110 | #[test] 111 | fn extract_valid_instruction_and_unknown_account_attr() { 112 | let res = parse_instructions(vec![ 113 | instruction_unknown_account_attr(), 114 | instruction_valid(), 115 | ]); 116 | assert!(res 117 | .unwrap_err() 118 | .to_string() 119 | .contains("Invalid/unknown account meta configuration")); 120 | } 121 | 122 | #[test] 123 | fn extract_valid_instruction_and_invalid_account_idx() { 124 | let res = parse_instructions(vec![ 125 | instruction_invalid_account_idx(), 126 | instruction_valid(), 127 | ]); 128 | assert!(res 129 | .unwrap_err() 130 | .to_string() 131 | .contains("Account index 1 does not match")); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /shank-macro-impl/src/instruction/instruction.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::HashSet, 3 | convert::{TryFrom, TryInto}, 4 | }; 5 | use syn::{Attribute, Error as ParseError, ItemEnum, Result as ParseResult}; 6 | 7 | use syn::Ident; 8 | 9 | use crate::{ 10 | parsed_enum::{ParsedEnum, ParsedEnumVariant}, 11 | parsers::get_derive_attr, 12 | types::RustType, 13 | DERIVE_INSTRUCTION_ATTR, 14 | }; 15 | 16 | use super::{ 17 | account_attrs::InstructionAccount, IdlInstruction, InstructionStrategies, 18 | InstructionStrategy, 19 | }; 20 | 21 | // ----------------- 22 | // Instruction 23 | // ----------------- 24 | #[derive(Debug)] 25 | pub struct Instruction { 26 | pub ident: Ident, 27 | pub variants: Vec, 28 | } 29 | 30 | impl Instruction { 31 | pub fn try_from_item_enum( 32 | item_enum: &ItemEnum, 33 | skip_derive_attr_check: bool, 34 | ) -> ParseResult> { 35 | if skip_derive_attr_check 36 | || get_derive_attr(&item_enum.attrs, DERIVE_INSTRUCTION_ATTR) 37 | .is_some() 38 | { 39 | let parsed_enum = ParsedEnum::try_from(item_enum)?; 40 | Instruction::try_from(&parsed_enum).map(Some) 41 | } else { 42 | Ok(None) 43 | } 44 | } 45 | } 46 | 47 | impl TryFrom<&ParsedEnum> for Option { 48 | type Error = ParseError; 49 | 50 | fn try_from(parsed_enum: &ParsedEnum) -> ParseResult { 51 | match get_derive_attr(&parsed_enum.attrs, DERIVE_INSTRUCTION_ATTR) 52 | .map(|_| parsed_enum) 53 | { 54 | Some(ix_enum) => ix_enum.try_into().map(Some), 55 | None => Ok(None), 56 | } 57 | } 58 | } 59 | 60 | impl TryFrom<&ParsedEnum> for Instruction { 61 | type Error = ParseError; 62 | 63 | fn try_from(parsed_enum: &ParsedEnum) -> ParseResult { 64 | let ParsedEnum { 65 | ident, variants, .. 66 | } = parsed_enum; 67 | 68 | let variants = variants 69 | .iter() 70 | .map(InstructionVariant::try_from) 71 | .collect::>>()?; 72 | Ok(Self { 73 | ident: ident.clone(), 74 | variants, 75 | }) 76 | } 77 | } 78 | 79 | #[derive(Debug)] 80 | pub enum InstructionVariantFields { 81 | Unnamed(Vec), 82 | Named(Vec<(String, RustType)>), 83 | } 84 | 85 | // ----------------- 86 | // Instruction Variant 87 | // ----------------- 88 | #[derive(Debug)] 89 | pub struct InstructionVariant { 90 | pub ident: Ident, 91 | pub field_tys: InstructionVariantFields, 92 | pub accounts: Vec, 93 | pub strategies: HashSet, 94 | pub discriminant: usize, 95 | } 96 | 97 | impl TryFrom<&ParsedEnumVariant> for InstructionVariant { 98 | type Error = ParseError; 99 | 100 | fn try_from(variant: &ParsedEnumVariant) -> ParseResult { 101 | let ParsedEnumVariant { 102 | ident, 103 | fields, 104 | discriminant, 105 | attrs, 106 | .. 107 | } = variant; 108 | 109 | let mut field_tys: InstructionVariantFields = if !fields.is_empty() { 110 | // Determine if the InstructionType is tuple or struct variant 111 | let field = fields.first().unwrap(); 112 | match &field.ident { 113 | Some(_) => InstructionVariantFields::Named( 114 | fields 115 | .iter() 116 | .map(|x| { 117 | ( 118 | x.ident.as_ref().unwrap().to_string(), 119 | x.rust_type.clone(), 120 | ) 121 | }) 122 | .collect(), 123 | ), 124 | None => InstructionVariantFields::Unnamed( 125 | fields.iter().map(|x| x.rust_type.clone()).collect(), 126 | ), 127 | } 128 | } else { 129 | InstructionVariantFields::Unnamed(vec![]) 130 | }; 131 | 132 | let attrs: &[Attribute] = attrs.as_ref(); 133 | let (accounts, strategies) = match IdlInstruction::try_from(attrs) { 134 | Ok(idl_ix) => { 135 | field_tys = idl_ix.to_instruction_fields(ident.clone()); 136 | ( 137 | idl_ix.to_accounts(ident.clone()), 138 | InstructionStrategies(HashSet::::new()), 139 | ) 140 | } 141 | Err(_) => (attrs.try_into()?, attrs.into()), 142 | }; 143 | 144 | Ok(Self { 145 | ident: ident.clone(), 146 | field_tys, 147 | accounts: accounts.0, 148 | strategies: strategies.0, 149 | discriminant: *discriminant, 150 | }) 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /shank-macro-impl/src/instruction/instruction_test.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use quote::quote; 3 | use syn::{ItemEnum, Result as ParseResult}; 4 | 5 | use crate::{ 6 | instruction::InstructionVariantFields, 7 | types::{Primitive, RustType}, 8 | }; 9 | 10 | use super::instruction::{Instruction, InstructionVariant}; 11 | 12 | fn parse_instruction(code: TokenStream) -> ParseResult> { 13 | let item_enum = syn::parse2::(code) 14 | .expect("Should parse ItemEnum successfully"); 15 | Instruction::try_from_item_enum(&item_enum, false) 16 | } 17 | 18 | fn assert_instruction_variant( 19 | variant: &InstructionVariant, 20 | name: &str, 21 | expected_discriminant: usize, 22 | expected_field_tys: &Vec, 23 | accounts_len: usize, 24 | ) { 25 | let InstructionVariant { 26 | ident, 27 | field_tys, 28 | accounts, 29 | discriminant, 30 | .. 31 | } = variant; 32 | 33 | assert_eq!(ident.to_string(), name); 34 | assert_eq!(discriminant, &expected_discriminant, "discriminant"); 35 | assert_eq!(accounts.len(), accounts_len, "accounts"); 36 | match field_tys { 37 | InstructionVariantFields::Named(field_tys) => { 38 | assert_eq!( 39 | field_tys.len(), 40 | expected_field_tys.len(), 41 | "fields size" 42 | ); 43 | for field_idx in 0..expected_field_tys.len() { 44 | let (_field_name, field_ty) = field_tys.get(field_idx).unwrap(); 45 | let expected_field_ty = 46 | expected_field_tys.get(field_idx).unwrap(); 47 | assert_eq!(field_ty, expected_field_ty, "field type"); 48 | } 49 | } 50 | InstructionVariantFields::Unnamed(field_tys) => { 51 | assert_eq!( 52 | field_tys.len(), 53 | expected_field_tys.len(), 54 | "fields size" 55 | ); 56 | for field_idx in 0..expected_field_tys.len() { 57 | let field_ty = field_tys.get(field_idx).unwrap(); 58 | let expected_field_ty = 59 | expected_field_tys.get(field_idx).unwrap(); 60 | assert_eq!(field_ty, expected_field_ty, "field type"); 61 | } 62 | } 63 | } 64 | } 65 | 66 | #[test] 67 | fn parse_c_style_instruction() { 68 | let parsed = parse_instruction(quote! { 69 | #[derive(ShankInstruction)] 70 | pub enum Instruction { 71 | #[account(0, name = "creator", sig)] 72 | #[account(1, name = "thing", mut, optional)] 73 | CreateThing, 74 | #[account(name = "creator", sig)] 75 | CloseThing 76 | } 77 | }) 78 | .expect("Should parse fine") 79 | .expect("Should be instruction"); 80 | 81 | assert_eq!(parsed.ident.to_string(), "Instruction", "enum ident"); 82 | assert_eq!(parsed.variants.len(), 2, "variants"); 83 | assert!( 84 | !parsed.variants[0].accounts[0].optional, 85 | "non-optional account of first variant" 86 | ); 87 | assert!( 88 | parsed.variants[0].accounts[1].optional, 89 | "optional account of first variant" 90 | ); 91 | assert!( 92 | !parsed.variants[1].accounts[0].optional, 93 | "non-optional account of second variant" 94 | ); 95 | 96 | assert_instruction_variant( 97 | &parsed.variants[0], 98 | "CreateThing", 99 | 0, 100 | &vec![], 101 | 2, 102 | ); 103 | assert_instruction_variant( 104 | &parsed.variants[1], 105 | "CloseThing", 106 | 1, 107 | &vec![], 108 | 1, 109 | ); 110 | } 111 | 112 | #[test] 113 | fn parse_custom_field_variant_instruction() { 114 | let parsed = parse_instruction(quote! { 115 | #[derive(ShankInstruction)] 116 | pub enum Instruction { 117 | CreateThing, 118 | #[account(name = "creator", sig)] 119 | CloseThing(CloseArgs) 120 | } 121 | }) 122 | .expect("Should parse fine") 123 | .expect("Should be instruction"); 124 | 125 | assert_eq!(parsed.ident.to_string(), "Instruction", "enum ident"); 126 | assert_eq!(parsed.variants.len(), 2, "variants"); 127 | 128 | assert_instruction_variant( 129 | &parsed.variants[0], 130 | "CreateThing", 131 | 0, 132 | &vec![], 133 | 0, 134 | ); 135 | assert_instruction_variant( 136 | &parsed.variants[1], 137 | "CloseThing", 138 | 1, 139 | &vec![RustType::owned_custom_value("CloseArgs", "CloseArgs")], 140 | 1, 141 | ); 142 | } 143 | 144 | #[test] 145 | fn parse_u8_field_variant_instruction() { 146 | let parsed = parse_instruction(quote! { 147 | #[derive(ShankInstruction)] 148 | pub enum Instruction { 149 | #[account(0, name = "creator", sig)] 150 | CreateThing, 151 | #[account(name = "creator", sig)] 152 | CloseThing(u8) 153 | } 154 | }) 155 | .expect("Should parse fine") 156 | .expect("Should be instruction"); 157 | 158 | assert_eq!(parsed.ident.to_string(), "Instruction", "enum ident"); 159 | assert_eq!(parsed.variants.len(), 2, "variants"); 160 | 161 | assert_instruction_variant( 162 | &parsed.variants[0], 163 | "CreateThing", 164 | 0, 165 | &vec![], 166 | 1, 167 | ); 168 | assert_instruction_variant( 169 | &parsed.variants[1], 170 | "CloseThing", 171 | 1, 172 | &vec![RustType::owned_primitive("u8", Primitive::U8)], 173 | 1, 174 | ); 175 | } 176 | 177 | #[test] 178 | fn parse_non_instruction_enum() { 179 | assert!( 180 | parse_instruction(quote! { 181 | pub enum Instruction { 182 | #[account(0, name = "creator", sig)] 183 | #[account(1, name = "thing", mut)] 184 | CreateThing, 185 | #[account(name = "creator", sig)] 186 | CloseThing 187 | } 188 | }) 189 | .expect("Should parse fine") 190 | .is_none(), 191 | "should be none" 192 | ); 193 | assert!( 194 | parse_instruction(quote! { 195 | #[derive(OtherDerive)] 196 | pub enum Instruction { 197 | #[account(0, name = "creator", sig)] 198 | #[account(1, name = "thing", mut)] 199 | CreateThing, 200 | #[account(name = "creator", sig)] 201 | CloseThing 202 | } 203 | }) 204 | .expect("Should parse fine") 205 | .is_none(), 206 | "should be none" 207 | ); 208 | } 209 | -------------------------------------------------------------------------------- /shank-macro-impl/src/instruction/mod.rs: -------------------------------------------------------------------------------- 1 | mod account_attrs; 2 | mod extract_instructions; 3 | mod idl_instruction_attrs; 4 | #[allow(clippy::module_inception)] 5 | mod instruction; 6 | mod strategy_attrs; 7 | 8 | pub use account_attrs::*; 9 | pub use extract_instructions::*; 10 | pub use idl_instruction_attrs::*; 11 | pub use instruction::*; 12 | pub use strategy_attrs::*; 13 | 14 | #[cfg(test)] 15 | mod account_attrs_test; 16 | #[cfg(test)] 17 | mod instruction_test; 18 | #[cfg(test)] 19 | mod strategy_attrs_test; 20 | -------------------------------------------------------------------------------- /shank-macro-impl/src/instruction/strategy_attrs.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | 3 | use syn::Attribute; 4 | 5 | const LEGACY_OPTIONAL_ACCOUNTS_STRATEGY: &str = 6 | "legacy_optional_accounts_strategy"; 7 | 8 | #[derive(Debug, PartialEq, Eq, Hash)] 9 | pub enum InstructionStrategy { 10 | LegacyOptionalAccounts, 11 | } 12 | 13 | #[derive(Debug, PartialEq, Eq)] 14 | pub struct InstructionStrategies(pub HashSet); 15 | 16 | impl InstructionStrategy { 17 | pub fn from_account_attr(attr: &Attribute) -> Option { 18 | match attr.path.get_ident().map(|x| { 19 | x.to_string().as_str() == LEGACY_OPTIONAL_ACCOUNTS_STRATEGY 20 | }) { 21 | Some(true) => Some(InstructionStrategy::LegacyOptionalAccounts), 22 | _ => None, 23 | } 24 | } 25 | } 26 | 27 | impl From<&[Attribute]> for InstructionStrategies { 28 | fn from(attrs: &[Attribute]) -> Self { 29 | let strategies = attrs 30 | .iter() 31 | .filter_map(InstructionStrategy::from_account_attr) 32 | .collect::>(); 33 | 34 | InstructionStrategies(strategies) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /shank-macro-impl/src/instruction/strategy_attrs_test.rs: -------------------------------------------------------------------------------- 1 | use std::convert::TryInto; 2 | 3 | use proc_macro2::TokenStream; 4 | use quote::quote; 5 | use syn::ItemEnum; 6 | 7 | use syn::{Attribute, Result as ParseResult}; 8 | 9 | use crate::instruction::{ 10 | account_attrs_test::{ 11 | assert_instruction_account_matches, InstructionAccountWithoutIdent, 12 | }, 13 | InstructionStrategy, 14 | }; 15 | 16 | use super::{account_attrs::InstructionAccounts, InstructionStrategies}; 17 | 18 | fn parse_first_enum_variant_attrs( 19 | code: TokenStream, 20 | ) -> ParseResult<(InstructionAccounts, InstructionStrategies)> { 21 | let parsed = 22 | syn::parse2::(code).expect("Should parse successfully"); 23 | let attrs: &[Attribute] = parsed.variants.first().unwrap().attrs.as_ref(); 24 | let instruction_accounts = attrs.try_into()?; 25 | let instruction_strategies = attrs.into(); 26 | Ok((instruction_accounts, instruction_strategies)) 27 | } 28 | 29 | #[test] 30 | fn instruction_with_legacy_optional_accounts_strategy() { 31 | let (accounts, strategies) = parse_first_enum_variant_attrs(quote! { 32 | #[derive(ShankInstruction)] 33 | pub enum Instructions { 34 | #[legacy_optional_accounts_strategy] 35 | #[account(name="authority")] 36 | NonIndexed 37 | } 38 | }) 39 | .expect("Should parse fine"); 40 | 41 | assert_instruction_account_matches( 42 | &accounts.0[0], 43 | InstructionAccountWithoutIdent { 44 | index: None, 45 | name: "authority".to_string(), 46 | writable: false, 47 | signer: false, 48 | desc: None, 49 | optional: false, 50 | }, 51 | ); 52 | 53 | assert_eq!(strategies.0.len(), 1, "includes one instruction strategy"); 54 | assert!( 55 | strategies 56 | .0 57 | .contains(&InstructionStrategy::LegacyOptionalAccounts), 58 | "to legacy optional accounts strategy" 59 | ); 60 | } 61 | 62 | #[test] 63 | fn instruction_without_legacy_optional_accounts_strategy() { 64 | let (accounts, strategies) = parse_first_enum_variant_attrs(quote! { 65 | #[derive(ShankInstruction)] 66 | pub enum Instructions { 67 | #[account(name="authority")] 68 | NonIndexed 69 | } 70 | }) 71 | .expect("Should parse fine"); 72 | 73 | assert_instruction_account_matches( 74 | &accounts.0[0], 75 | InstructionAccountWithoutIdent { 76 | index: None, 77 | name: "authority".to_string(), 78 | writable: false, 79 | signer: false, 80 | desc: None, 81 | optional: false, 82 | }, 83 | ); 84 | 85 | assert_eq!(strategies.0.len(), 0, "includes no instruction strategy"); 86 | } 87 | -------------------------------------------------------------------------------- /shank-macro-impl/src/krate/crate_context.rs: -------------------------------------------------------------------------------- 1 | /// Adapted from: https://github.com/project-serum/anchor/blob/d8d720067dd6e2a3bec50207b84008276c914732/lang/syn/src/parser/context.rs 2 | use std::{collections::BTreeMap, path::Path}; 3 | 4 | use super::module_context::{ModuleContext, ParsedModule}; 5 | 6 | /// Crate parse context 7 | /// 8 | /// Keeps track of modules defined within a crate. 9 | #[derive(Debug)] 10 | pub struct CrateContext { 11 | modules: BTreeMap, 12 | } 13 | 14 | impl CrateContext { 15 | pub fn consts(&self) -> impl Iterator { 16 | self.modules.iter().flat_map(|(_, ctx)| ctx.consts()) 17 | } 18 | 19 | pub fn structs(&self) -> impl Iterator { 20 | self.modules.iter().flat_map(|(_, ctx)| ctx.structs()) 21 | } 22 | 23 | pub fn enums(&self) -> impl Iterator { 24 | self.modules.iter().flat_map(|(_, ctx)| ctx.enums()) 25 | } 26 | 27 | pub fn macros(&self) -> impl Iterator { 28 | self.modules.iter().flat_map(|(_, ctx)| ctx.macros()) 29 | } 30 | 31 | pub fn modules(&self) -> impl Iterator { 32 | self.modules.values().map(|detail| ModuleContext { detail }) 33 | } 34 | 35 | pub fn root_module(&self) -> ModuleContext { 36 | ModuleContext { 37 | detail: self.modules.get("crate").unwrap(), 38 | } 39 | } 40 | 41 | pub fn all_items(&self) -> impl Iterator { 42 | self.modules.iter().flat_map(|(_, ctx)| ctx.all_items()) 43 | } 44 | 45 | pub fn all_items_vec(&self) -> Vec { 46 | self.modules 47 | .iter() 48 | .flat_map(|(_, ctx)| ctx.all_items()) 49 | .cloned() 50 | .collect() 51 | } 52 | 53 | pub fn parse(root: impl AsRef) -> Result { 54 | Ok(CrateContext { 55 | modules: ParsedModule::parse_recursive(root.as_ref())?, 56 | }) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /shank-macro-impl/src/krate/mod.rs: -------------------------------------------------------------------------------- 1 | mod crate_context; 2 | mod module_context; 3 | 4 | pub use crate_context::*; 5 | -------------------------------------------------------------------------------- /shank-macro-impl/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod account; 2 | pub mod builder; 3 | pub mod converters; 4 | pub mod custom_type; 5 | pub mod error; 6 | pub mod instruction; 7 | pub mod krate; 8 | pub mod macros; 9 | pub mod parsed_enum; 10 | pub mod parsed_macro; 11 | pub mod parsed_struct; 12 | pub mod parsers; 13 | pub mod types; 14 | 15 | pub const DERIVE_ACCOUNT_ATTR: &str = "ShankAccount"; 16 | pub const DERIVE_CONTEXT_ATTR: &str = "ShankContext"; 17 | pub const DERIVE_BUILDER_ATTR: &str = "ShankBuilder"; 18 | pub const DERIVE_INSTRUCTION_ATTR: &str = "ShankInstruction"; 19 | 20 | pub mod syn { 21 | pub use syn::*; 22 | } 23 | -------------------------------------------------------------------------------- /shank-macro-impl/src/macros/mod.rs: -------------------------------------------------------------------------------- 1 | mod program_id; 2 | 3 | pub use program_id::*; 4 | -------------------------------------------------------------------------------- /shank-macro-impl/src/macros/program_id.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::{Ident, Span}; 2 | use std::convert::TryFrom; 3 | use syn::{Error as ParseError, ItemMacro, Result as ParseResult}; 4 | 5 | use crate::parsed_macro::ParsedMacro; 6 | 7 | /// Declared program id, i.e. `solana_program::declare_id!("")` 8 | #[derive(Debug)] 9 | pub struct ProgramId { 10 | pub id: String, 11 | } 12 | 13 | impl TryFrom<&[ItemMacro]> for ProgramId { 14 | type Error = ParseError; 15 | 16 | fn try_from(macros: &[ItemMacro]) -> ParseResult { 17 | let matches: Vec<(Ident, String)> = macros 18 | .iter() 19 | .map(ParsedMacro::from) 20 | .filter_map( 21 | |ParsedMacro { 22 | path, 23 | literal, 24 | path_idents, 25 | }| { 26 | literal.and_then(|lit| { 27 | if path.ends_with("declare_id") { 28 | Some((path_idents[0].clone(), lit)) 29 | } else { 30 | None 31 | } 32 | }) 33 | }, 34 | ) 35 | .collect(); 36 | 37 | if matches.len() > 1 { 38 | Err(ParseError::new_spanned( 39 | &matches[0].0, 40 | format!( 41 | "Found more than one program id candidate: {:?}. You should either have exactly one `declare_id!` in your code or override the program id via -p.", 42 | matches.iter().map(|x| x.1.clone()).collect::>() 43 | ), 44 | )) 45 | } else if matches.is_empty() { 46 | Err(ParseError::new( 47 | Span::call_site(), 48 | "Could not find a `declare_id(\"\")` invocation in the program. If this is intentional provide a program address via the -p argument instead", 49 | )) 50 | } else { 51 | Ok(ProgramId { 52 | id: matches[0].1.clone(), 53 | }) 54 | } 55 | } 56 | } 57 | 58 | #[cfg(test)] 59 | mod tests { 60 | use super::*; 61 | 62 | use proc_macro2::TokenStream; 63 | use quote::quote; 64 | use syn::ItemMacro; 65 | 66 | fn parse_program_id(codes: Vec) -> ParseResult { 67 | let item_macros = codes 68 | .into_iter() 69 | .map(syn::parse2::) 70 | .collect::>>() 71 | .expect("Should parse ItemMacro successfully"); 72 | 73 | ProgramId::try_from(&item_macros[..]) 74 | } 75 | 76 | #[test] 77 | fn program_id_qualified_solana_program() { 78 | let parsed = parse_program_id(vec![ 79 | quote! { 80 | format!("Just another macro {}", s); 81 | }, 82 | quote! { 83 | solana_program::declare_id!("metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s"); 84 | }, 85 | ]) 86 | .expect("Should parse fine"); 87 | 88 | assert_eq!( 89 | parsed.id, 90 | "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s".to_string() 91 | ) 92 | } 93 | 94 | #[test] 95 | fn program_id_imported_solana_program() { 96 | let parsed = parse_program_id(vec![ 97 | quote! { 98 | format!("Just another macro {}", s); 99 | }, 100 | quote! { 101 | declare_id!("metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s"); 102 | }, 103 | ]) 104 | .expect("Should parse fine"); 105 | 106 | assert_eq!( 107 | parsed.id, 108 | "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s".to_string() 109 | ) 110 | } 111 | 112 | #[test] 113 | fn program_id_two_declarations() { 114 | let err = parse_program_id(vec![ 115 | quote! { 116 | declare_id!("otherid"); 117 | }, 118 | quote! { 119 | solana_program::declare_id!("metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s"); 120 | }, 121 | ]) 122 | .expect_err("Should error"); 123 | 124 | assert_eq!( 125 | err.to_string().as_str(), 126 | "Found more than one program id candidate: [\"otherid\", \"metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s\"]. You should either have exactly one `declare_id!` in your code or override the program id via -p." 127 | ); 128 | } 129 | 130 | #[test] 131 | fn program_id_no_declaration() { 132 | let err = parse_program_id(vec![ 133 | quote! { 134 | format!("Just another macro {}", s); 135 | }, 136 | quote! { 137 | declare_some_other_id!("metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s"); 138 | }, 139 | ]) 140 | .expect_err("Should error"); 141 | 142 | assert_eq!( 143 | err.to_string().as_str(), 144 | "Could not find a `declare_id(\"\")` invocation in the program. If this is intentional provide a program address via the -p argument instead" 145 | ); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /shank-macro-impl/src/parsed_enum/mod.rs: -------------------------------------------------------------------------------- 1 | #[allow(clippy::module_inception)] 2 | mod parsed_enum; 3 | mod parsed_enum_variant; 4 | 5 | pub use parsed_enum::*; 6 | pub use parsed_enum_variant::*; 7 | -------------------------------------------------------------------------------- /shank-macro-impl/src/parsed_enum/parsed_enum_variant.rs: -------------------------------------------------------------------------------- 1 | use std::convert::TryFrom; 2 | 3 | use syn::{ 4 | Attribute, Error as ParseError, Expr, ExprLit, Field, Ident, Lit, 5 | Result as ParseResult, Variant, 6 | }; 7 | 8 | use crate::types::RustType; 9 | 10 | // ----------------- 11 | // Enum Variant 12 | // ----------------- 13 | #[derive(Debug, PartialEq, Eq)] 14 | pub struct ParsedEnumVariant { 15 | /// The identifier name of the variant, i.e. Red 16 | pub ident: Ident, 17 | 18 | /// The fields of the variant, empty for c-like enums 19 | pub fields: Vec, 20 | 21 | /// The variant slot starting with 0 22 | pub slot: usize, 23 | 24 | /// The variant discriminator which defaults to the slot, 25 | /// but can be assigned other values, i.e. Red = 0xf00 26 | pub discriminant: usize, 27 | 28 | /// Attributes found on the enum variant 29 | pub attrs: Vec, 30 | } 31 | 32 | impl TryFrom<(usize, usize, &Variant)> for ParsedEnumVariant { 33 | type Error = ParseError; 34 | 35 | fn try_from( 36 | (slot, implicit_discriminant, variant): (usize, usize, &Variant), 37 | ) -> ParseResult { 38 | let fields = variant 39 | .fields 40 | .iter() 41 | .enumerate() 42 | .map(ParsedEnumVariantField::try_from) 43 | .collect::>>()?; 44 | 45 | let discriminant = match &variant.discriminant { 46 | Some((_, expr)) => match expr { 47 | Expr::Lit(ExprLit { lit, .. }) => { 48 | match lit { 49 | Lit::Int(lit) => { 50 | lit 51 | .base10_parse() 52 | .map_err(|err| ParseError::new_spanned( 53 | lit, 54 | format!("Invalid discriminant value, only `usize` literals supported. {}", err))) 55 | } , 56 | _ => Err( ParseError::new_spanned( 57 | expr, 58 | "Only literal integer enum variant discriminators supported", 59 | )), 60 | 61 | } 62 | } 63 | _ => Err(ParseError::new_spanned( 64 | expr, 65 | "Only literal enum variant discriminators supported", 66 | )), 67 | }, 68 | None => Ok(implicit_discriminant), 69 | }?; 70 | 71 | Ok(Self { 72 | ident: variant.ident.clone(), 73 | fields, 74 | slot, 75 | discriminant, 76 | attrs: variant.attrs.clone(), 77 | }) 78 | } 79 | } 80 | 81 | // ----------------- 82 | // Enum Variant Field 83 | // ----------------- 84 | #[derive(Debug, PartialEq, Eq)] 85 | pub struct ParsedEnumVariantField { 86 | /// The Rust type of the field 87 | pub rust_type: RustType, 88 | 89 | /// Name of the field, not present for tuple fields 90 | pub ident: Option, 91 | 92 | /// The slot (starting with 0) of the field 93 | pub slot: usize, 94 | } 95 | 96 | impl TryFrom<(usize, &Field)> for ParsedEnumVariantField { 97 | type Error = ParseError; 98 | 99 | fn try_from((slot, field): (usize, &Field)) -> ParseResult { 100 | let rust_type = RustType::try_from(&field.ty)?; 101 | Ok(ParsedEnumVariantField { 102 | rust_type, 103 | ident: field.ident.clone(), 104 | slot, 105 | }) 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /shank-macro-impl/src/parsed_macro/mod.rs: -------------------------------------------------------------------------------- 1 | #[allow(clippy::module_inception)] 2 | mod parsed_macro; 3 | 4 | pub use parsed_macro::*; 5 | -------------------------------------------------------------------------------- /shank-macro-impl/src/parsed_macro/parsed_macro.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::{Ident, Literal}; 2 | use syn::ItemMacro; 3 | 4 | /// A very simplified parsed macro which only supports the use case we need for now 5 | #[derive(Debug)] 6 | pub struct ParsedMacro { 7 | pub path_idents: Vec, 8 | pub path: String, 9 | pub literal: Option, 10 | } 11 | 12 | impl From<&ItemMacro> for ParsedMacro { 13 | fn from(item_macro: &ItemMacro) -> Self { 14 | let path_idents: Vec = item_macro 15 | .mac 16 | .path 17 | .segments 18 | .iter() 19 | .map(|x| x.ident.clone()) 20 | .collect(); 21 | 22 | let path = path_idents 23 | .iter() 24 | .map(|ident| ident.to_string()) 25 | .reduce(|acc, ident| format!("{}::{}", acc, ident)) 26 | .unwrap_or_default(); 27 | 28 | let literal = syn::parse2::(item_macro.mac.tokens.clone()) 29 | .map_or(None, |lit| { 30 | Some(lit.to_string().trim_matches('"').to_string()) 31 | }); 32 | 33 | Self { 34 | path_idents, 35 | path, 36 | literal, 37 | } 38 | } 39 | } 40 | 41 | #[cfg(test)] 42 | mod tests { 43 | use super::*; 44 | 45 | use proc_macro2::TokenStream; 46 | use quote::quote; 47 | use syn::ItemMacro; 48 | 49 | fn parse_macro(code: TokenStream) -> ParsedMacro { 50 | let item_macro = syn::parse2::(code) 51 | .expect("Should parse ItemMacro successfully"); 52 | (&item_macro).into() 53 | } 54 | 55 | #[test] 56 | fn macro_program_id_qualified_solana_program() { 57 | let parsed = parse_macro(quote! { 58 | solana_program::declare_id!("metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s"); 59 | }); 60 | 61 | assert_eq!(parsed.path, "solana_program::declare_id", "path"); 62 | assert_eq!(parsed.path_idents.len(), 2, "path idents"); 63 | assert_eq!( 64 | parsed.literal, 65 | Some("metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s".to_string()) 66 | ) 67 | } 68 | 69 | #[test] 70 | fn macro_program_id_imported_solana_program() { 71 | let parsed = parse_macro(quote! { 72 | declare_id!("metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s"); 73 | }); 74 | 75 | assert_eq!(parsed.path, "declare_id", "path"); 76 | assert_eq!(parsed.path_idents.len(), 1, "path idents"); 77 | assert_eq!( 78 | parsed.literal, 79 | Some("metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s".to_string()) 80 | ) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /shank-macro-impl/src/parsed_struct/mod.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | 3 | #[allow(clippy::module_inception)] 4 | mod parsed_struct; 5 | mod seed; 6 | mod struct_attr; 7 | mod struct_field_attr; 8 | 9 | pub use parsed_struct::*; 10 | pub use seed::*; 11 | pub use struct_attr::*; 12 | pub use struct_field_attr::StructFieldAttr; 13 | 14 | #[cfg(test)] 15 | mod parsed_struct_test; 16 | 17 | pub fn parse_struct(item: TokenStream) -> ParsedStruct { 18 | match syn::parse2::(item) { 19 | Ok(account_struct) => account_struct, 20 | Err(err) => panic!("{}", err), 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /shank-macro-impl/src/parsed_struct/parsed_struct.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::HashSet, 3 | convert::{TryFrom, TryInto}, 4 | fmt::Display, 5 | }; 6 | 7 | use syn::{ 8 | parse::{Parse, ParseStream}, 9 | Attribute, Error as ParseError, Field, Ident, ItemStruct, 10 | Result as ParseResult, 11 | }; 12 | 13 | use crate::{parsed_struct::struct_attr::StructAttrs, types::RustType}; 14 | 15 | use super::struct_field_attr::{StructFieldAttr, StructFieldAttrs}; 16 | 17 | #[derive(Debug, Clone)] 18 | pub struct StructField { 19 | pub ident: syn::Ident, 20 | pub rust_type: RustType, 21 | pub attrs: HashSet, 22 | } 23 | 24 | impl Display for StructField { 25 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 26 | write!( 27 | f, 28 | "{key}: {ty} ({kind:?})", 29 | key = self.ident, 30 | ty = self.rust_type.ident, 31 | kind = self.rust_type.kind 32 | ) 33 | } 34 | } 35 | 36 | impl StructField { 37 | /// Get the overridden type from the IdlType attribute if present 38 | pub fn type_override(&self) -> Option<&RustType> { 39 | self.attrs.iter().find_map(|attr| { 40 | if let StructFieldAttr::IdlType(rust_type) = attr { 41 | Some(rust_type) 42 | } else { 43 | None 44 | } 45 | }) 46 | } 47 | } 48 | 49 | impl TryFrom<&Field> for StructField { 50 | type Error = ParseError; 51 | 52 | fn try_from(f: &Field) -> ParseResult { 53 | let ident = f.ident.as_ref().unwrap().clone(); 54 | let attrs = match StructFieldAttrs::try_from(f.attrs.as_ref()) { 55 | Ok(field_attrs) => field_attrs.0, 56 | Err(err) => { 57 | return Err(ParseError::new_spanned( 58 | &f.ident, 59 | format!("Failed to parse field attributes: {}", err), 60 | )); 61 | } 62 | }; 63 | let rust_type: RustType = match (&f.ty).try_into() { 64 | Ok(ty) => ty, 65 | Err(err) => { 66 | return Err(ParseError::new_spanned(ident, err.to_string())) 67 | } 68 | }; 69 | 70 | Ok(Self { 71 | ident, 72 | rust_type, 73 | attrs, 74 | }) 75 | } 76 | } 77 | 78 | #[derive(Debug)] 79 | pub struct ParsedStruct { 80 | pub ident: Ident, 81 | pub fields: Vec, 82 | pub attrs: Vec, 83 | pub struct_attrs: StructAttrs, 84 | } 85 | 86 | impl Parse for ParsedStruct { 87 | fn parse(input: ParseStream) -> ParseResult { 88 | let strct = ::parse(input)?; 89 | ParsedStruct::try_from(&strct) 90 | } 91 | } 92 | 93 | impl TryFrom<&ItemStruct> for ParsedStruct { 94 | type Error = ParseError; 95 | 96 | fn try_from(item: &ItemStruct) -> ParseResult { 97 | let fields = match &item.fields { 98 | syn::Fields::Named(fields) => fields 99 | .named 100 | .iter() 101 | .map(StructField::try_from) 102 | .collect::>>()?, 103 | _ => { 104 | return Err(ParseError::new_spanned( 105 | &item.fields, 106 | "failed to parse fields make sure they are all named", 107 | )) 108 | } 109 | }; 110 | let struct_attrs = StructAttrs::try_from(item.attrs.as_slice())?; 111 | Ok(ParsedStruct { 112 | ident: item.ident.clone(), 113 | fields, 114 | attrs: item.attrs.clone(), 115 | struct_attrs, 116 | }) 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /shank-macro-impl/src/parsed_struct/struct_field_attr.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | use std::convert::TryFrom; 3 | 4 | use crate::types::RustType; 5 | use syn::{ 6 | Attribute, Error as ParseError, Lit, Meta, NestedMeta, 7 | Result as ParseResult, 8 | }; 9 | 10 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] 11 | pub enum StructFieldAttr { 12 | Padding, 13 | IdlType(RustType), 14 | } 15 | 16 | impl From<&StructFieldAttr> for String { 17 | fn from(attr: &StructFieldAttr) -> Self { 18 | match attr { 19 | StructFieldAttr::Padding => "padding".to_string(), 20 | StructFieldAttr::IdlType(_) => "idl-type".to_string(), 21 | } 22 | } 23 | } 24 | 25 | pub struct StructFieldAttrs(pub HashSet); 26 | 27 | impl TryFrom<&[Attribute]> for StructFieldAttrs { 28 | type Error = ParseError; 29 | 30 | fn try_from(attrs: &[Attribute]) -> ParseResult { 31 | let mut result = HashSet::new(); 32 | 33 | for attr in attrs { 34 | if attr.path.is_ident("padding") { 35 | result.insert(StructFieldAttr::Padding); 36 | } else if attr.path.is_ident("idl_type") { 37 | match attr.parse_meta() { 38 | Ok(Meta::List(meta_list)) => { 39 | let mut found_valid_type = false; 40 | 41 | for nested in meta_list.nested.iter() { 42 | let type_str = match nested { 43 | // Handle string literal format: #[idl_type("TypeName")] 44 | NestedMeta::Lit(Lit::Str(lit_str)) => { 45 | Some(lit_str.value()) 46 | } 47 | 48 | // Handle direct type format: #[idl_type(TypeName)] 49 | NestedMeta::Meta(meta) => { 50 | if let Some(ident) = meta.path().get_ident() 51 | { 52 | Some(ident.to_string()) 53 | } else { 54 | // Handle path with segments (like std::string::String) 55 | Some( 56 | meta.path() 57 | .segments 58 | .iter() 59 | .map(|seg| { 60 | seg.ident.to_string() 61 | }) 62 | .collect::>() 63 | .join("::"), 64 | ) 65 | } 66 | } 67 | _ => { 68 | return Err(ParseError::new_spanned( 69 | nested, 70 | "Invalid nested meta in idl_type attribute" 71 | )); 72 | } 73 | }; 74 | 75 | if let Some(type_str) = type_str { 76 | match RustType::try_from(type_str.as_str()) { 77 | Ok(rust_type) => { 78 | result.insert( 79 | StructFieldAttr::IdlType(rust_type), 80 | ); 81 | found_valid_type = true; 82 | break; 83 | } 84 | Err(err) => { 85 | return Err(ParseError::new_spanned( 86 | nested, 87 | format!("Invalid type override format in idl_type attribute: {}", err) 88 | )); 89 | } 90 | } 91 | } 92 | } 93 | 94 | if !found_valid_type { 95 | return Err(ParseError::new_spanned( 96 | &meta_list.nested, 97 | "No valid type found in idl_type attribute", 98 | )); 99 | } 100 | } 101 | Ok(_) => { 102 | return Err(ParseError::new_spanned( 103 | attr, 104 | "idl_type attribute must be a list, e.g., #[idl_type(TypeName)] or #[idl_type(\"TypeName\")]" 105 | )); 106 | } 107 | Err(err) => { 108 | return Err(ParseError::new_spanned( 109 | attr, 110 | format!( 111 | "Failed to parse idl_type attribute: {}", 112 | err 113 | ), 114 | )); 115 | } 116 | } 117 | } 118 | } 119 | 120 | Ok(Self(result)) 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /shank-macro-impl/src/parsers/attrs.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::Ident; 2 | use syn::{ 3 | punctuated::Punctuated, Attribute, Meta, MetaList, NestedMeta, Token, 4 | }; 5 | 6 | fn flattened_idents_from_nested_meta( 7 | nested: &Punctuated, 8 | ) -> Vec { 9 | nested 10 | .iter() 11 | .flat_map(|nested| match nested { 12 | NestedMeta::Meta(Meta::Path(path)) => { 13 | path.segments.iter().map(|x| x.ident.clone()).collect() 14 | } 15 | NestedMeta::Lit(_) => { 16 | todo!("Handle NestedMeta::Lit for derive nested") 17 | } 18 | _ => vec![], 19 | }) 20 | .collect() 21 | } 22 | 23 | pub fn get_derive_names(attrs: &[Attribute]) -> Vec { 24 | attrs 25 | .iter() 26 | .flat_map(|attr| { 27 | let meta = &attr.parse_meta(); 28 | match meta { 29 | Ok(Meta::List(MetaList { path, nested, .. })) => { 30 | let derive = path 31 | .segments 32 | .iter() 33 | .enumerate() 34 | .find(|(_, x)| x.ident == "derive"); 35 | 36 | match derive { 37 | Some(_) => flattened_idents_from_nested_meta(nested) 38 | .into_iter() 39 | .map(|x| x.to_string()) 40 | .collect::>(), 41 | None => vec![], 42 | } 43 | } 44 | Ok(_) => vec![], 45 | Err(_) => vec![], 46 | } 47 | }) 48 | .collect() 49 | } 50 | 51 | pub fn attr_is_derive(attr: &&Attribute, derive: &str) -> bool { 52 | let meta = &attr.parse_meta(); 53 | 54 | match meta { 55 | Ok(Meta::List(MetaList { path, nested, .. })) => { 56 | let found_derive = 57 | path.segments.iter().find(|x| x.ident == "derive"); 58 | 59 | match found_derive { 60 | Some(_) => flattened_idents_from_nested_meta(nested) 61 | .into_iter() 62 | .any(|ident| ident == derive), 63 | None => false, 64 | } 65 | } 66 | Ok(_) => false, 67 | Err(err) => { 68 | eprintln!("{:#?}", err); 69 | false 70 | } 71 | } 72 | } 73 | 74 | pub fn get_derive_attr<'a>( 75 | attrs: &'a [Attribute], 76 | derive: &str, 77 | ) -> Option<&'a Attribute> { 78 | attrs.iter().find(|attr| attr_is_derive(attr, derive)) 79 | } 80 | -------------------------------------------------------------------------------- /shank-macro-impl/src/parsers/mod.rs: -------------------------------------------------------------------------------- 1 | mod attrs; 2 | pub use attrs::*; 3 | -------------------------------------------------------------------------------- /shank-macro-impl/src/types/mod.rs: -------------------------------------------------------------------------------- 1 | mod parsed_reference; 2 | mod render_rust_ty; 3 | mod resolve_rust_ty; 4 | mod type_kind; 5 | pub use parsed_reference::*; 6 | pub use resolve_rust_ty::*; 7 | pub use type_kind::*; 8 | -------------------------------------------------------------------------------- /shank-macro-impl/src/types/parsed_reference.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Debug; 2 | use std::hash::{Hash, Hasher}; 3 | 4 | use syn::{Lifetime, TypeReference}; 5 | 6 | #[derive(Clone, PartialEq, Eq)] 7 | pub enum ParsedReference { 8 | Owned, 9 | Ref(Option), 10 | RefMut(Option), 11 | } 12 | 13 | impl Hash for ParsedReference { 14 | fn hash(&self, state: &mut H) { 15 | // Use discriminant to hash the enum variant 16 | std::mem::discriminant(self).hash(state); 17 | 18 | // Hash the inner lifetime if present 19 | match self { 20 | ParsedReference::Owned => {} 21 | ParsedReference::Ref(lifetime) => { 22 | if let Some(lt) = lifetime { 23 | lt.to_string().hash(state); 24 | } 25 | } 26 | ParsedReference::RefMut(lifetime) => { 27 | if let Some(lt) = lifetime { 28 | lt.to_string().hash(state); 29 | } 30 | } 31 | } 32 | } 33 | } 34 | 35 | impl Debug for ParsedReference { 36 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 37 | let r = match self { 38 | ParsedReference::Owned => "ParsedReference::Owned".to_string(), 39 | ParsedReference::Ref(ident) => { 40 | format!("ParsedReference::Ref({:?})", ident) 41 | } 42 | ParsedReference::RefMut(ident) => { 43 | format!("ParsedReference::RefMut({:?})", ident) 44 | } 45 | }; 46 | write!(f, "{}", r) 47 | } 48 | } 49 | 50 | impl From<&TypeReference> for ParsedReference { 51 | fn from(r: &TypeReference) -> Self { 52 | let TypeReference { 53 | lifetime, 54 | mutability, 55 | .. 56 | } = r; 57 | 58 | let lifetime_ident = lifetime 59 | .as_ref() 60 | .map(|Lifetime { ident, .. }| ident.clone()); 61 | 62 | match mutability.is_some() { 63 | true => ParsedReference::RefMut(lifetime_ident), 64 | false => ParsedReference::Ref(lifetime_ident), 65 | } 66 | } 67 | } 68 | 69 | impl ParsedReference { 70 | pub fn with_lifetime(self, lifetime: syn::Ident) -> Self { 71 | match self { 72 | ParsedReference::Owned => self, 73 | ParsedReference::Ref(_) => ParsedReference::Ref(Some(lifetime)), 74 | ParsedReference::RefMut(_) => { 75 | ParsedReference::RefMut(Some(lifetime)) 76 | } 77 | } 78 | } 79 | 80 | /** 81 | * Adds the provided lifetime if this is a Ref or RefMut and has no lifteime already. 82 | * Otherwise returns itself unchanged. 83 | */ 84 | pub fn ensured_lifetime(&self, lifetime: syn::Ident) -> Self { 85 | match self { 86 | ParsedReference::Ref(None) => ParsedReference::Ref(Some(lifetime)), 87 | ParsedReference::RefMut(None) => { 88 | ParsedReference::RefMut(Some(lifetime)) 89 | } 90 | _ => self.clone(), 91 | } 92 | } 93 | 94 | pub fn lifetime(&self) -> Option<&syn::Ident> { 95 | match self { 96 | ParsedReference::Owned => None, 97 | ParsedReference::Ref(lifetime) => lifetime.as_ref(), 98 | ParsedReference::RefMut(lifetime) => lifetime.as_ref(), 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /shank-macro/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "shank_macro" 3 | version = "0.4.3" 4 | description = "Provides macros used to annotate Solana Rust programs in order to extract an IDL with the shank CLI" 5 | authors = ["Metaplex Maintainers "] 6 | repository = "https://github.com/metaplex-foundation/shank" 7 | license = "Apache-2.0" 8 | edition = "2018" 9 | 10 | [lib] 11 | proc-macro = true 12 | doctest = false 13 | 14 | [dependencies] 15 | proc-macro2 = "1.0.32" 16 | quote = "1.0.10" 17 | shank_macro_impl = { version = "0.4.3", path = "../shank-macro-impl" } 18 | shank_render = { version = "0.4.3", path = "../shank-render" } 19 | syn = "1.0.82" 20 | -------------------------------------------------------------------------------- /shank-macro/README.md: -------------------------------------------------------------------------------- 1 | # shank_macro 2 | 3 | Provides macros used to annotate Solana Rust programs in order to extract an IDL with the shank 4 | CLI. 5 | 6 | ### ShankAccount 7 | 8 | Annotates a _struct_ that shank will consider an account containing de/serializable data. 9 | 10 | ```rs 11 | use shank::ShankAccount; 12 | use borsh::{BorshDeserialize, BorshSerialize}; 13 | 14 | #[derive(Clone, BorshSerialize, BorshDeserialize, ShankAccount)] 15 | pub struct Metadata { 16 | pub update_authority: Pubkey, 17 | pub mint: Pubkey, 18 | pub primary_sale_happened: bool, 19 | } 20 | ``` 21 | 22 | ### Field Attributes 23 | 24 | #### `#[idl_type(...)]` attribute 25 | 26 | This attribute allows you to override how Shank interprets a field's type when generating the IDL. This is useful for: 27 | 28 | 1. Fields with wrapper types that should be treated as their inner types in the IDL 29 | 2. Fields storing enum values as primitives (like `u8`) that should be recognized as enums 30 | 3. Fields with complex types that need simpler representations in the IDL 31 | 32 | The attribute supports two formats: 33 | 34 | 1. **String literal format**: `#idl_type("TypeName")]` 35 | 2. **Direct type format**: `#[idl_type(TypeName)]` 36 | 37 | The difference between these is that the direct type format will error at runtime if the referenced type cannot be found in the Rust type system. 38 | In the future, ideally the direct type format would perform checks on the given type. 39 | 40 | ```rs 41 | use shank::ShankAccount; 42 | use borsh::{BorshDeserialize, BorshSerialize}; 43 | 44 | #[derive(Clone, BorshSerialize, BorshDeserialize, ShankAccount)] 45 | pub struct MyAccount { 46 | // Regular field 47 | pub regular_field: u32, 48 | 49 | // Field stored as u8 but representing an enum (string literal format) 50 | #[idl_type("MyEnum")] 51 | pub enum_as_byte_str: u8, 52 | 53 | // Field with a wrapper type that should be treated as a simpler type (string literal format) 54 | #[idl_type("u64")] 55 | pub wrapped_u64_str: CustomU64Wrapper, 56 | 57 | // Field stored as u8 but representing an enum (direct type format) 58 | #[idl_type(MyEnum)] 59 | pub enum_as_byte_direct: u8, 60 | 61 | // Field with a wrapper type that should be treated as a simpler type (direct type format) 62 | #[idl_type(u32)] 63 | pub wrapped_u32_direct: CustomU32Wrapper, 64 | } 65 | ``` 66 | 67 | The type specified must be a valid Rust type that Shank can recognize. If the type is a custom type (like an enum), make sure it's defined in your codebase and is accessible to Shank during IDL generation (for example, via the ShankType trait). 68 | 69 | #### `#[padding]` attribute 70 | 71 | Indicates that a field is used for padding and should be marked as such in the IDL. 72 | 73 | ### Note 74 | 75 | The fields of a _ShankAccount_ struct can reference other types as long as they are annotated 76 | with `BorshSerialize`, `BorshDeserialize`, or `ShankType`. 77 | 78 | ## ShankInstruction 79 | 80 | Annotates the program _Instruction_ `Enum` in order to include `#[account]` attributes. 81 | 82 | The `#[account]` attributes indicate for each instruction _variant_ which accounts it expects 83 | and how they should be configured. 84 | 85 | ### `#[account]` attribute 86 | 87 | This attribute allows you to configure each account that is provided to the particular 88 | instruction. These annotations need to follow the order in which the accounts are provided. 89 | They take the following general form: 90 | 91 | ```rs 92 | #[account(index?, (writable|signer)?, name="", desc?="optional description")] 93 | ``` 94 | 95 | - `index`: optionally provides the account index in the provided accounts array which needs to 96 | match its position of `#[account]` attributes 97 | - `signer` | `sign` | `sig`: indicates that the account is _signer_ 98 | - `writable` | `write` | `writ` | `mut`: indicates that the account is _writable_ which means it may be 99 | mutated as part of processing the particular instruction 100 | - `name`: (required) provides the name for the account 101 | - `desc` | `description`: allows to provide a description of the account 102 | 103 | ### Known Accounts 104 | 105 | If an account `name` matches either of the a _known_ accounts indicated below then 106 | [solita](https://github.com/metaplex-foundation/solita) generated SDK code won't require providing 107 | it as the program id is known. 108 | 109 | - `token_program` uses `TOKEN_PROGRAM_ID` 110 | - `ata_program` uses `ASSOCIATED_TOKEN_PROGRAM_ID` 111 | - `system_program` uses `SystemProgram.programId` 112 | - `rent` uses `SYSVAR_RENT_PUBKEY` 113 | 114 | ```rs 115 | use borsh::{BorshDeserialize, BorshSerialize}; 116 | use shank::ShankInstruction; 117 | #[derive(Debug, Clone, ShankInstruction, BorshSerialize, BorshDeserialize)] 118 | #[rustfmt::skip] 119 | pub enum VaultInstruction { 120 | /// Initialize a token vault, starts inactivate. Add tokens in subsequent instructions, then activate. 121 | #[account(0, writable, name="fraction_mint", 122 | desc="Initialized fractional share mint with 0 tokens in supply, authority on mint must be pda of program with seed [prefix, programid]")] 123 | #[account(1, writable, name="redeem_treasury", 124 | desc = "Initialized redeem treasury token account with 0 tokens in supply, owner of account must be pda of program like above")] 125 | #[account(2, writable, name="fraction_treasury", 126 | desc = "Initialized fraction treasury token account with 0 tokens in supply, owner of account must be pda of program like above")] 127 | #[account(3, writable, name="vault", 128 | desc = "Uninitialized vault account")] 129 | #[account(4, name="authority", 130 | desc = "Authority on the vault")] 131 | #[account(5, name="pricing_lookup_address", 132 | desc = "Pricing Lookup Address")] 133 | #[account(6, name="token_program", 134 | desc = "Token program")] 135 | #[account(7, name="rent", 136 | desc = "Rent sysvar")] 137 | InitVault(InitVaultArgs), 138 | 139 | /// Activates the vault, distributing initial shares into the fraction treasury. 140 | /// Tokens can no longer be removed in this state until Combination. 141 | #[account(0, writable, name="vault", desc = "Initialized inactivated fractionalized token vault")] 142 | #[account(1, writable, name="fraction_mint", desc = "Fraction mint")] 143 | #[account(2, writable, name="fraction_treasury", desc = "Fraction treasury")] 144 | #[account(3, name="fraction_mint_authority", desc = "Fraction mint authority for the program - seed of [PREFIX, program_id]")] 145 | #[account(4, signer, name="vault_authority", desc = "Authority on the vault")] 146 | #[account(5, name="token_program", desc = "Token program")] 147 | ActivateVault(NumberOfShareArgs) 148 | } 149 | ``` 150 | 151 | ## LICENSE 152 | 153 | Apache-2.0 154 | -------------------------------------------------------------------------------- /shank-macro/rustfmt.toml: -------------------------------------------------------------------------------- 1 | imports_granularity = "Crate" 2 | max_width = 80 3 | -------------------------------------------------------------------------------- /shank-macro/src/account.rs: -------------------------------------------------------------------------------- 1 | use std::convert::TryFrom; 2 | 3 | use proc_macro2::TokenStream; 4 | use shank_macro_impl::{ 5 | parsed_struct::ParsedStruct, parsers::get_derive_attr, DERIVE_ACCOUNT_ATTR, 6 | }; 7 | use syn::{DeriveInput, Error as ParseError, Item, Result as ParseResult}; 8 | 9 | pub fn derive_account(input: DeriveInput) -> ParseResult { 10 | let attr = get_derive_attr(&input.attrs, DERIVE_ACCOUNT_ATTR).cloned(); 11 | let item = Item::from(input); 12 | match item { 13 | Item::Struct(struct_item) => { 14 | let parsed_struct = ParsedStruct::try_from(&struct_item)?; 15 | shank_render::pda::render_pda_and_seeds_impl( 16 | &parsed_struct.struct_attrs, 17 | &parsed_struct.ident, 18 | true, 19 | ) 20 | } 21 | _ => Err(ParseError::new_spanned( 22 | &attr, 23 | "ShankAccount can only be derived for structs", 24 | )), 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /shank-macro/src/builder.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use shank_macro_impl::{ 3 | builder::Builder, parsers::get_derive_attr, DERIVE_BUILDER_ATTR, 4 | }; 5 | use syn::{DeriveInput, Error as ParseError, Item, Result as ParseResult}; 6 | 7 | pub fn derive_builder(input: DeriveInput) -> ParseResult { 8 | let attr = get_derive_attr(&input.attrs, DERIVE_BUILDER_ATTR).cloned(); 9 | let item = Item::from(input); 10 | match item { 11 | Item::Enum(enum_item) => { 12 | if let Some(builder_item) = 13 | Builder::try_from_item_enum(&enum_item, true)? 14 | { 15 | shank_render::builder::render_builders_impl(&builder_item) 16 | } else { 17 | Err(ParseError::new_spanned( 18 | &attr, 19 | "ShankBuilder can only be derived for enums with variants that have a `#[ShankBuilder]` attribute", 20 | )) 21 | } 22 | } 23 | _ => Err(ParseError::new_spanned( 24 | &attr, 25 | "ShankBuilder can only be derived for enums", 26 | )), 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /shank-macro/src/context.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use shank_macro_impl::{ 3 | instruction::Instruction, parsers::get_derive_attr, DERIVE_CONTEXT_ATTR, 4 | }; 5 | use syn::{DeriveInput, Error as ParseError, Item, Result as ParseResult}; 6 | 7 | pub fn derive_context(input: DeriveInput) -> ParseResult { 8 | let attr = get_derive_attr(&input.attrs, DERIVE_CONTEXT_ATTR).cloned(); 9 | let item = Item::from(input); 10 | match item { 11 | Item::Enum(enum_item) => { 12 | if let Some(instruction) = 13 | Instruction::try_from_item_enum(&enum_item, true)? 14 | { 15 | shank_render::context::render_contexts_impl(&instruction) 16 | } else { 17 | Err(ParseError::new_spanned( 18 | &attr, 19 | "ShankContext can only be derived for enums with variants that have a `#[ShankContext]` attribute", 20 | )) 21 | } 22 | } 23 | _ => Err(ParseError::new_spanned( 24 | &attr, 25 | "ShankContext can only be derived for enums", 26 | )), 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /shank-macro/src/instruction.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use shank_macro_impl::{ 3 | instruction::Instruction, parsers::get_derive_attr, DERIVE_INSTRUCTION_ATTR, 4 | }; 5 | use syn::{DeriveInput, Error as ParseError, Item, Result as ParseResult}; 6 | 7 | pub fn derive_instruction(input: DeriveInput) -> ParseResult { 8 | let attr = get_derive_attr(&input.attrs, DERIVE_INSTRUCTION_ATTR).cloned(); 9 | let item = Item::from(input); 10 | match item { 11 | Item::Enum(enum_item) => { 12 | Instruction::try_from_item_enum(&enum_item, true) 13 | .map(|_| TokenStream::new()) 14 | } 15 | _ => Err(ParseError::new_spanned( 16 | &attr, 17 | "ShankInstruction can only be derived for enums", 18 | )), 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /shank-render/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "shank_render" 3 | version = "0.4.3" 4 | description = "Renders implementaions derived from shank macros" 5 | authors = ["Metaplex Maintainers "] 6 | repository = "https://github.com/metaplex-foundation/shank" 7 | license = "Apache-2.0" 8 | edition = "2018" 9 | 10 | [dependencies] 11 | proc-macro2 = "1.0.46" 12 | quote = "1.0.21" 13 | shank_macro_impl = { version = "0.4.3", path = "../shank-macro-impl" } 14 | 15 | [dev-dependencies] 16 | prettyplease = "0.1.21" 17 | -------------------------------------------------------------------------------- /shank-render/rustfmt.toml: -------------------------------------------------------------------------------- 1 | imports_granularity = "Crate" 2 | max_width = 80 3 | -------------------------------------------------------------------------------- /shank-render/src/builder/mod.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use quote::quote; 3 | use shank_macro_impl::builder::Builder; 4 | use shank_macro_impl::syn::Result as ParseResult; 5 | 6 | mod render_builders; 7 | use self::render_builders::generate_builders; 8 | 9 | pub fn render_builders_impl( 10 | builder_item: &Builder, 11 | ) -> ParseResult { 12 | let builders = builder_item 13 | .variants 14 | .iter() 15 | .map(|variant| generate_builders(&builder_item.ident, variant)) 16 | .collect::>(); 17 | 18 | Ok(quote! { 19 | pub mod builders { 20 | use super::*; 21 | 22 | /// Trait that defines the interface for creating an instruction. 23 | pub trait InstructionBuilder { 24 | fn instruction(&self) -> solana_program::instruction::Instruction; 25 | } 26 | 27 | #(#builders)* 28 | } 29 | }) 30 | } 31 | -------------------------------------------------------------------------------- /shank-render/src/consts.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use quote::quote; 3 | 4 | pub fn solana_program_pubkey() -> TokenStream { 5 | quote! { ::solana_program::pubkey::Pubkey } 6 | } 7 | -------------------------------------------------------------------------------- /shank-render/src/context/mod.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use quote::quote; 3 | use shank_macro_impl::instruction::Instruction; 4 | use shank_macro_impl::syn::Result as ParseResult; 5 | 6 | mod render_context; 7 | use self::render_context::generate_context; 8 | 9 | pub fn render_contexts_impl( 10 | instruction: &Instruction, 11 | ) -> ParseResult { 12 | let contexts = instruction 13 | .variants 14 | .iter() 15 | .map(generate_context) 16 | .collect::>(); 17 | 18 | Ok(quote! { 19 | pub mod accounts { 20 | use super::*; 21 | 22 | pub struct Context<'a, T> { 23 | pub accounts: T, 24 | pub remaining_accounts: &'a [solana_program::account_info::AccountInfo<'a>], 25 | } 26 | 27 | #(#contexts)* 28 | } 29 | }) 30 | } 31 | -------------------------------------------------------------------------------- /shank-render/src/context/render_context.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use quote::quote; 3 | use shank_macro_impl::{instruction::InstructionVariant, syn}; 4 | 5 | pub(crate) fn generate_context(variant: &InstructionVariant) -> TokenStream { 6 | // accounts fields 7 | let struct_fields = variant.accounts.iter().map(|account| { 8 | let account_name = syn::parse_str::(&account.name).unwrap(); 9 | if account.optional { 10 | quote! { 11 | pub #account_name: Option<&'a solana_program::account_info::AccountInfo<'a>> 12 | } 13 | } else { 14 | quote! { 15 | pub #account_name:&'a solana_program::account_info::AccountInfo<'a> 16 | } 17 | } 18 | }); 19 | 20 | // accounts initialization 21 | let account_fields = variant.accounts.iter().enumerate().map(|(index, account)| { 22 | let account_name = syn::parse_str::(&account.name).unwrap(); 23 | if account.optional { 24 | quote! { 25 | #account_name: if accounts[#index].key == &crate::ID { None } else { Some(&accounts[#index]) } 26 | } 27 | } else { 28 | quote! { 29 | #account_name: &accounts[#index] 30 | } 31 | } 32 | }); 33 | 34 | let expected = variant.accounts.len(); // number of expected accounts 35 | let name = 36 | syn::parse_str::(&format!("{}Accounts", variant.ident)) 37 | .unwrap(); 38 | 39 | quote! { 40 | pub struct #name<'a> { 41 | #(#struct_fields,)* 42 | } 43 | impl<'a> #name<'a> { 44 | pub fn context( 45 | accounts: &'a [solana_program::account_info::AccountInfo<'a>] 46 | ) -> Result, solana_program::sysvar::slot_history::ProgramError> { 47 | if accounts.len() < #expected { 48 | return Err(solana_program::sysvar::slot_history::ProgramError::NotEnoughAccountKeys); 49 | } 50 | 51 | Ok(Context { 52 | accounts: Self { #(#account_fields,)* }, 53 | remaining_accounts: &accounts[#expected..], 54 | }) 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /shank-render/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod builder; 2 | pub mod consts; 3 | pub mod context; 4 | pub mod pda; 5 | -------------------------------------------------------------------------------- /shank-render/src/pda/mod.rs: -------------------------------------------------------------------------------- 1 | mod pda_common; 2 | mod render_pda; 3 | mod render_seeds; 4 | 5 | pub use pda_common::*; 6 | use proc_macro2::{Span, TokenStream}; 7 | pub use render_pda::*; 8 | pub use render_seeds::*; 9 | 10 | use quote::quote; 11 | use shank_macro_impl::{ 12 | parsed_struct::StructAttrs, 13 | syn::{Ident, Result as ParseResult}, 14 | }; 15 | 16 | pub fn render_pda_and_seeds_impl( 17 | struct_attrs: &StructAttrs, 18 | account_type_ident: &Ident, 19 | include_comments: bool, 20 | ) -> ParseResult { 21 | let processed_seeds = try_process_seeds(struct_attrs)?; 22 | if processed_seeds.is_empty() { 23 | return Ok(TokenStream::new()); 24 | } 25 | 26 | let seeds_fn_ident = Ident::new("shank_seeds", Span::call_site()); 27 | let seeds_fn_with_bump_ident = 28 | Ident::new("shank_seeds_with_bump", Span::call_site()); 29 | let pda_fn_ident = Ident::new("shank_pda", Span::call_site()); 30 | let pda_fn_with_bump_ident = 31 | Ident::new("shank_pda_with_bump", Span::call_site()); 32 | 33 | let pub_seeds_fn = try_render_seeds_fn( 34 | &processed_seeds, 35 | &seeds_fn_ident, 36 | &seeds_fn_with_bump_ident, 37 | include_comments, 38 | )?; 39 | let pub_pda_fn = render_pda_fn( 40 | &processed_seeds, 41 | &seeds_fn_ident, 42 | &seeds_fn_with_bump_ident, 43 | &pda_fn_ident, 44 | &pda_fn_with_bump_ident, 45 | include_comments, 46 | ); 47 | 48 | if let (Some(pub_seeds_fn), Some(pub_pda_fn)) = (pub_seeds_fn, pub_pda_fn) { 49 | Ok(quote! { 50 | impl #account_type_ident { 51 | #pub_seeds_fn 52 | #pub_pda_fn 53 | } 54 | }) 55 | } else { 56 | Ok(TokenStream::new()) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /shank-render/src/pda/pda_common.rs: -------------------------------------------------------------------------------- 1 | use shank_macro_impl::{ 2 | parsed_struct::{ProcessedSeed, SeedArg, StructAttr, StructAttrs}, 3 | syn::Result as ParseResult, 4 | }; 5 | 6 | pub fn try_process_seeds( 7 | struct_attrs: &StructAttrs, 8 | ) -> ParseResult> { 9 | let all_seeds = struct_attrs 10 | .items_ref() 11 | .iter() 12 | .map(|attr| match attr { 13 | StructAttr::Seeds(seeds) => seeds, 14 | }) 15 | .collect::>(); 16 | 17 | assert!( 18 | all_seeds.len() <= 1, 19 | "Should only have one seed definition per account" 20 | ); 21 | 22 | if all_seeds.is_empty() { 23 | Ok(vec![]) 24 | } else { 25 | let seeds = all_seeds.first().unwrap(); 26 | seeds.process() 27 | } 28 | } 29 | 30 | pub fn render_args_comments( 31 | processed_seeds: &[ProcessedSeed], 32 | exclude_program_id: bool, 33 | ) -> Vec { 34 | processed_seeds 35 | .iter() 36 | .map(|x| x.arg.as_ref()) 37 | .filter(Option::is_some) 38 | .flatten() 39 | .filter(|x| !exclude_program_id || x.name != "program_id") 40 | .map(|SeedArg { name, desc, ty }| { 41 | format!("/// * **{}**: {} | [{}] ", name, desc, ty.ident) 42 | }) 43 | .collect() 44 | } 45 | -------------------------------------------------------------------------------- /shank-render/tests/mod.rs: -------------------------------------------------------------------------------- 1 | mod pda; 2 | mod utils; 3 | -------------------------------------------------------------------------------- /shank-render/tests/pda/mod.rs: -------------------------------------------------------------------------------- 1 | mod render_impl; 2 | mod render_pda_fn; 3 | mod render_seeds_fn; 4 | -------------------------------------------------------------------------------- /shank-render/tests/utils/mod.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use shank_macro_impl::{ 3 | account::extract_account_structs, 4 | parsed_struct::{ProcessedSeed, StructAttrs}, 5 | syn::{self, Ident, ItemStruct, Result as ParseResult}, 6 | }; 7 | use shank_render::pda::try_process_seeds; 8 | 9 | fn parse_struct(code: TokenStream) -> ItemStruct { 10 | syn::parse2::(code).expect("Should parse successfully") 11 | } 12 | 13 | pub fn parse_struct_attrs(code: TokenStream) -> (Ident, StructAttrs) { 14 | let account_struct = parse_struct(code); 15 | let all_structs = vec![&account_struct].into_iter(); 16 | let parsed_structs = extract_account_structs(all_structs) 17 | .expect("Should parse struct without error"); 18 | 19 | ( 20 | account_struct.ident, 21 | parsed_structs.first().unwrap().struct_attrs.clone(), 22 | ) 23 | } 24 | 25 | pub fn process_seeds(code: TokenStream) -> ParseResult> { 26 | let (_, struct_attrs) = parse_struct_attrs(code); 27 | try_process_seeds(&struct_attrs) 28 | } 29 | 30 | pub fn pretty_print(code: TokenStream) -> String { 31 | let syn_tree = syn::parse_file(code.to_string().as_str()).unwrap(); 32 | prettyplease::unparse(&syn_tree) 33 | } 34 | -------------------------------------------------------------------------------- /shank/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "shank" 3 | description = "Exposes macros to annotate Rust programs to extract solita compatible IDL in order to generate program SDKs" 4 | authors = ["Metaplex Maintainers "] 5 | repository = "https://github.com/metaplex-foundation/shank" 6 | license = "Apache-2.0" 7 | version = "0.4.3" 8 | edition = "2018" 9 | 10 | 11 | [dependencies] 12 | shank_macro = { version = "0.4.3", path = "../shank-macro" } 13 | -------------------------------------------------------------------------------- /shank/README.md: -------------------------------------------------------------------------------- 1 | # Shank 2 | 3 | Entry point to shank crates used to annotate Rust programs in order to extract IDL which is 4 | used by [solita](https://github.com/metaplex-foundation/solita) in order to generate program 5 | SDKs. 6 | 7 | ![shank-logo](./assets/shank-logo.gif) 8 | 9 | ## Shank Macro 10 | 11 | [Readme](../shank-macro/README.md) | [Docs](https://docs.rs/shank_macro) 12 | 13 | Provides macros used to annotate Solana Rust programs in order to extract an IDL with the shank 14 | CLI. 15 | 16 | ## LICENSE 17 | 18 | Apache-2.0 19 | -------------------------------------------------------------------------------- /shank/assets/shank-logo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/metaplex-foundation/shank/46cbde90d46296c5c31660a8f6220817444e3921/shank/assets/shank-logo.gif -------------------------------------------------------------------------------- /shank/src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate shank_macro; 2 | pub use shank_macro::*; 3 | --------------------------------------------------------------------------------