diff --git a/.github/actions/setup/action.yaml b/.github/actions/setup/action.yaml index 1ac983c81f..fdaf61824e 100644 --- a/.github/actions/setup/action.yaml +++ b/.github/actions/setup/action.yaml @@ -3,9 +3,18 @@ description: "Setup" runs: using: "composite" steps: - - run: sudo apt-get update && sudo apt-get install -y pkg-config build-essential libudev-dev - shell: bash - - run: echo "ANCHOR_VERSION=$(cat ./VERSION)" >> $GITHUB_ENV - shell: bash - - run: git submodule update --init --recursive --depth 1 - shell: bash + - run: sudo apt-get update && sudo apt-get install -y pkg-config build-essential libudev-dev + shell: bash + - run: echo "ANCHOR_VERSION=$(cat ./VERSION)" >> $GITHUB_ENV + shell: bash + - run: git submodule update --init --recursive --depth 1 + shell: bash + # `nightly` toolchain is currently required for building the IDL. + # + # Pinning the toolchain to an older date in order to fix + # `error[E0635]: unknown feature stdsimd` error from `ahash`. + # See: https://github.com/tkaitchuck/aHash/issues/200 + # + # TODO: Unpin `nightly` release after upgrading Solana to `1.18`. + - run: rustup toolchain install nightly-2024-01-30 + shell: bash diff --git a/.github/workflows/reusable-tests.yaml b/.github/workflows/reusable-tests.yaml index 47fcb067de..769e46df40 100644 --- a/.github/workflows/reusable-tests.yaml +++ b/.github/workflows/reusable-tests.yaml @@ -452,8 +452,9 @@ jobs: path: tests/bench - cmd: cd tests/idl && ./test.sh path: tests/idl - - cmd: cd tests/solang && anchor test - path: tests/solang + # TODO: Enable when `solang` becomes compatible with the new IDL spec + # - cmd: cd tests/solang && anchor test + # path: tests/solang steps: - uses: actions/checkout@v3 - uses: ./.github/actions/setup/ diff --git a/CHANGELOG.md b/CHANGELOG.md index fba3d08d95..6476f31ddf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,13 @@ The minor version will be incremented upon a breaking change and the patch versi - cli: Check `@coral-xyz/anchor` package and CLI version compatibility ([#2813](https://github.com/coral-xyz/anchor/pull/2813)). - cli: Accept package name as program name ([#2816](https://github.com/coral-xyz/anchor/pull/2816)). - cli: Add ability to build and test only a specified program ([#2823](https://github.com/coral-xyz/anchor/pull/2823)). +- idl: Add new IDL spec ([#2824](https://github.com/coral-xyz/anchor/pull/2824)). +- idl: Add support for `repr`s ([#2824](https://github.com/coral-xyz/anchor/pull/2824)). +- idl: Add support for expression evaluation ([#2824](https://github.com/coral-xyz/anchor/pull/2824)). +- idl: Add support for using external types when generating the IDL ([#2824](https://github.com/coral-xyz/anchor/pull/2824)). +- idl, ts: Add unit and tuple struct support ([#2824](https://github.com/coral-xyz/anchor/pull/2824)). +- idl, ts: Add generics support ([#2824](https://github.com/coral-xyz/anchor/pull/2824)). +- ts: Add `accountsPartial` method to keep the old `accounts` method behavior ([#2824](https://github.com/coral-xyz/anchor/pull/2824)). ### Fixes @@ -46,6 +53,9 @@ The minor version will be incremented upon a breaking change and the patch versi - cli: Fix `migrate` command not working without global `ts-node` installation ([#2767](https://github.com/coral-xyz/anchor/pull/2767)). - client, lang, spl, syn: Enable all features for docs.rs build ([#2774](https://github.com/coral-xyz/anchor/pull/2774)). - ts: Fix construction of field layouts for type aliased instruction arguments ([#2821](https://github.com/coral-xyz/anchor/pull/2821)) +- idl: Fix IDL ([#2824](https://github.com/coral-xyz/anchor/pull/2824)). +- idl, ts: Make casing consistent ([#2824](https://github.com/coral-xyz/anchor/pull/2824)). +- ts: Fix not being able to use numbers in instruction, account, or event names in some cases due to case conversion ([#2824](https://github.com/coral-xyz/anchor/pull/2824)). ### Breaking @@ -58,6 +68,14 @@ The minor version will be incremented upon a breaking change and the patch versi - ts: Remove `associated`, `account.associated` and `account.associatedAddress` methods ([#2749](https://github.com/coral-xyz/anchor/pull/2749)). - cli: `idl upgrade` command closes the IDL buffer account ([#2760](https://github.com/coral-xyz/anchor/pull/2760)). - cli: Remove `--jest` option from the `init` command ([#2805](https://github.com/coral-xyz/anchor/pull/2805)). +- cli: Require `idl-build` feature in program `Cargo.toml` ([#2824](https://github.com/coral-xyz/anchor/pull/2824)). +- cli: Rename `seeds` feature to `resolution` and make it enabled by default ([#2824](https://github.com/coral-xyz/anchor/pull/2824)). +- cli: Remove `idl parse` command ([#2824](https://github.com/coral-xyz/anchor/pull/2824)). +- idl: Change IDL spec ([#2824](https://github.com/coral-xyz/anchor/pull/2824)). +- syn: Remove `idl-parse` and `seeds` features ([#2824](https://github.com/coral-xyz/anchor/pull/2824)). +- ts: Change `accounts` method to no longer accept resolvable accounts ([#2824](https://github.com/coral-xyz/anchor/pull/2824)). +- ts: `Program` instances use camelCase for everything ([#2824](https://github.com/coral-xyz/anchor/pull/2824)). +- ts: Remove discriminator functions ([#2824](https://github.com/coral-xyz/anchor/pull/2824)). ## [0.29.0] - 2023-10-16 diff --git a/Cargo.lock b/Cargo.lock index 1b49a4f819..5b7eb80f61 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -180,8 +180,8 @@ name = "anchor-cli" version = "0.29.0" dependencies = [ "anchor-client", + "anchor-idl", "anchor-lang", - "anchor-syn", "anyhow", "base64 0.21.4", "bincode", @@ -257,6 +257,17 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "anchor-idl" +version = "0.29.0" +dependencies = [ + "anchor-syn", + "anyhow", + "regex", + "serde", + "serde_json", +] + [[package]] name = "anchor-lang" version = "0.29.0" @@ -304,6 +315,7 @@ version = "0.29.0" dependencies = [ "anyhow", "bs58 0.5.0", + "cargo_toml", "heck 0.3.3", "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 78a2c739fd..a6abc075cc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ members = [ "avm", "cli", "client", + "idl", "lang", "lang/attribute/*", "lang/derive/*", diff --git a/cli/Cargo.toml b/cli/Cargo.toml index cca6c18d95..914efd9a74 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -17,8 +17,8 @@ dev = [] [dependencies] anchor-client = { path = "../client", version = "0.29.0" } +anchor-idl = { path = "../idl", features = ["build"], version = "0.29.0" } anchor-lang = { path = "../lang", version = "0.29.0" } -anchor-syn = { path = "../lang/syn", features = ["event-cpi", "idl-parse", "init-if-needed"], version = "0.29.0" } anyhow = "1.0.32" base64 = "0.21" bincode = "1.3.3" diff --git a/cli/src/config.rs b/cli/src/config.rs index efce7fd0ad..e0efff2d51 100644 --- a/cli/src/config.rs +++ b/cli/src/config.rs @@ -1,6 +1,6 @@ use crate::is_hidden; use anchor_client::Cluster; -use anchor_syn::idl::types::Idl; +use anchor_idl::types::Idl; use anyhow::{anyhow, bail, Context, Error, Result}; use clap::{Parser, ValueEnum}; use dirs::home_dir; @@ -375,14 +375,33 @@ pub struct ToolchainConfig { pub solana_version: Option, } -#[derive(Default, Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct FeaturesConfig { - #[serde(default)] - pub seeds: bool, + /// Enable account resolution. + /// + /// Not able to specify default bool value: https://github.com/serde-rs/serde/issues/368 + #[serde(default = "FeaturesConfig::get_default_resolution")] + pub resolution: bool, + /// Disable safety comment checks #[serde(default, rename = "skip-lint")] pub skip_lint: bool, } +impl FeaturesConfig { + fn get_default_resolution() -> bool { + true + } +} + +impl Default for FeaturesConfig { + fn default() -> Self { + Self { + resolution: Self::get_default_resolution(), + skip_lint: false, + } + } +} + #[derive(Clone, Debug, Serialize, Deserialize)] pub struct RegistryConfig { pub url: String, @@ -619,8 +638,8 @@ impl FromStr for Config { type Err = Error; fn from_str(s: &str) -> Result { - let cfg: _Config = toml::from_str(s) - .map_err(|e| anyhow::format_err!("Unable to deserialize config: {}", e.to_string()))?; + let cfg: _Config = + toml::from_str(s).map_err(|e| anyhow!("Unable to deserialize config: {e}"))?; Ok(Config { toolchain: cfg.toolchain.unwrap_or_default(), features: cfg.features.unwrap_or_default(), diff --git a/cli/src/lib.rs b/cli/src/lib.rs index 4ceafd954a..b3c19c49f4 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -1,15 +1,14 @@ +#![cfg_attr(nightly, feature(proc_macro_span))] + use crate::config::{ AnchorPackage, BootstrapMode, BuildConfig, Config, ConfigOverride, Manifest, ProgramArch, ProgramDeployment, ProgramWorkspace, ScriptsConfig, TestValidator, WithPath, SHUTDOWN_WAIT, STARTUP_WAIT, }; use anchor_client::Cluster; +use anchor_idl::types::{Idl, IdlArrayLen, IdlDefinedFields, IdlType, IdlTypeDefTy}; use anchor_lang::idl::{IdlAccount, IdlInstruction, ERASED_AUTHORITY}; use anchor_lang::{AccountDeserialize, AnchorDeserialize, AnchorSerialize}; -use anchor_syn::idl::types::{ - EnumFields, Idl, IdlConst, IdlErrorCode, IdlEvent, IdlType, IdlTypeDefinition, - IdlTypeDefinitionTy, -}; use anyhow::{anyhow, Context, Result}; use checks::{check_anchor_version, check_overflow}; use clap::Parser; @@ -24,7 +23,7 @@ use reqwest::blocking::multipart::{Form, Part}; use reqwest::blocking::Client; use rust_template::{ProgramTemplate, TestTemplate}; use semver::{Version, VersionReq}; -use serde::{Deserialize, Serialize}; +use serde::Deserialize; use serde_json::{json, Map, Value as JsonValue}; use solana_client::rpc_client::RpcClient; use solana_program::instruction::{AccountMeta, Instruction}; @@ -440,23 +439,11 @@ pub enum IdlCommand { /// The program to view. program_id: Pubkey, }, - /// Parses an IDL from source. - Parse { - /// Path to the program's interface definition. - #[clap(short, long)] - file: String, - /// Output file for the IDL (stdout if not specified). - #[clap(short, long)] - out: Option, - /// Output file for the TypeScript IDL. - #[clap(short = 't', long)] - out_ts: Option, - /// Suppress doc strings in output - #[clap(long)] - no_docs: bool, - }, /// Generates the IDL for the program using the compilation method. Build { + // Program name to build the IDL of(current dir's program if not specified) + #[clap(short, long)] + program_name: Option, /// Output file for the IDL (stdout if not specified) #[clap(short, long)] out: Option, @@ -466,6 +453,9 @@ pub enum IdlCommand { /// Suppress doc strings in output #[clap(long)] no_docs: bool, + /// Do not check for safety comments + #[clap(long)] + skip_lint: bool, }, /// Fetches an IDL for the given address from a cluster. /// The address can be a program, IDL account, or IDL buffer. @@ -1464,12 +1454,12 @@ fn build_cwd_verifiable( let idl = generate_idl(cfg, skip_lint, no_docs)?; // Write out the JSON file. println!("Writing the IDL file"); - let out_file = workspace_dir.join(format!("target/idl/{}.json", idl.name)); + let out_file = workspace_dir.join(format!("target/idl/{}.json", idl.metadata.name)); write_idl(&idl, OutFile::File(out_file))?; // Write out the TypeScript type. println!("Writing the .ts file"); - let ts_file = workspace_dir.join(format!("target/types/{}.ts", idl.name)); + let ts_file = workspace_dir.join(format!("target/types/{}.ts", idl.metadata.name)); fs::write(&ts_file, rust_template::idl_ts(&idl)?)?; // Copy out the TypeScript type. @@ -1478,7 +1468,7 @@ fn build_cwd_verifiable( ts_file, workspace_dir .join(&cfg.workspace.types) - .join(idl.name) + .join(idl.metadata.name) .with_extension("ts"), )?; } @@ -1758,13 +1748,17 @@ fn _build_rust_cwd( let idl = generate_idl(cfg, skip_lint, no_docs)?; // JSON out path. let out = match idl_out { - None => PathBuf::from(".").join(&idl.name).with_extension("json"), - Some(o) => PathBuf::from(&o.join(&idl.name).with_extension("json")), + None => PathBuf::from(".") + .join(&idl.metadata.name) + .with_extension("json"), + Some(o) => PathBuf::from(&o.join(&idl.metadata.name).with_extension("json")), }; // TS out path. let ts_out = match idl_ts_out { - None => PathBuf::from(".").join(&idl.name).with_extension("ts"), - Some(o) => PathBuf::from(&o.join(&idl.name).with_extension("ts")), + None => PathBuf::from(".") + .join(&idl.metadata.name) + .with_extension("ts"), + Some(o) => PathBuf::from(&o.join(&idl.metadata.name).with_extension("ts")), }; // Write out the JSON file. @@ -1778,7 +1772,7 @@ fn _build_rust_cwd( &ts_out, cfg_parent .join(&cfg.workspace.types) - .join(&idl.name) + .join(&idl.metadata.name) .with_extension("ts"), )?; } @@ -1839,8 +1833,10 @@ fn _build_solidity_cwd( // TS out path. let ts_out = match idl_ts_out { - None => PathBuf::from(".").join(&idl.name).with_extension("ts"), - Some(o) => PathBuf::from(&o.join(&idl.name).with_extension("ts")), + None => PathBuf::from(".") + .join(&idl.metadata.name) + .with_extension("ts"), + Some(o) => PathBuf::from(&o.join(&idl.metadata.name).with_extension("ts")), }; // Write out the TypeScript type. @@ -1852,7 +1848,7 @@ fn _build_solidity_cwd( &ts_out, cfg_parent .join(&cfg.workspace.types) - .join(&idl.name) + .join(&idl.metadata.name) .with_extension("ts"), )?; } @@ -2101,17 +2097,13 @@ fn idl(cfg_override: &ConfigOverride, subcmd: IdlCommand) -> Result<()> { } => idl_set_authority(cfg_override, program_id, address, new_authority, print_only), IdlCommand::EraseAuthority { program_id } => idl_erase_authority(cfg_override, program_id), IdlCommand::Authority { program_id } => idl_authority(cfg_override, program_id), - IdlCommand::Parse { - file, - out, - out_ts, - no_docs, - } => idl_parse(cfg_override, file, out, out_ts, no_docs), IdlCommand::Build { + program_name, out, out_ts, no_docs, - } => idl_build(out, out_ts, no_docs), + skip_lint, + } => idl_build(cfg_override, program_name, out, out_ts, no_docs, skip_lint), IdlCommand::Fetch { address, out } => idl_fetch(cfg_override, address, out), } } @@ -2419,10 +2411,6 @@ fn idl_close_account( // and sending multiple transactions in the event the IDL doesn't fit into // a single transaction. fn idl_write(cfg: &Config, program_id: &Pubkey, idl: &Idl, idl_address: Pubkey) -> Result<()> { - // Remove the metadata before deploy. - let mut idl = idl.clone(); - idl.metadata = None; - // Misc. let keypair = solana_sdk::signature::read_keypair_file(&cfg.provider.wallet.to_string()) .map_err(|_| anyhow!("Unable to read keypair file"))?; @@ -2431,7 +2419,7 @@ fn idl_write(cfg: &Config, program_id: &Pubkey, idl: &Idl, idl_address: Pubkey) // Serialize and compress the idl. let idl_data = { - let json_bytes = serde_json::to_vec(&idl)?; + let json_bytes = serde_json::to_vec(idl)?; let mut e = ZlibEncoder::new(Vec::new(), Compression::default()); e.write_all(&json_bytes)?; e.finish()? @@ -2473,287 +2461,78 @@ fn idl_write(cfg: &Config, program_id: &Pubkey, idl: &Idl, idl_address: Pubkey) Ok(()) } -fn idl_parse( +fn idl_build( cfg_override: &ConfigOverride, - file: String, + program_name: Option, out: Option, out_ts: Option, no_docs: bool, + skip_lint: bool, ) -> Result<()> { - let cfg = Config::discover(cfg_override)?.expect("Not in workspace."); - let file = shellexpand::tilde(&file); - let manifest_path = std::env::current_dir()?.join(PathBuf::from(&*file).parent().unwrap()); - let manifest = Manifest::discover_from_path(manifest_path)? - .ok_or_else(|| anyhow!("Cargo.toml not found"))?; - let idl = generate_idl_parse( - &*file, - manifest.version(), - cfg.features.seeds, + let cfg = Config::discover(cfg_override)?.expect("Not in workspace"); + let program_path = match program_name { + Some(name) => cfg.get_program(&name)?.path, + None => { + let current_dir = std::env::current_dir()?; + cfg.read_all_programs()? + .into_iter() + .find(|program| program.path == current_dir) + .ok_or_else(|| anyhow!("Not in a program directory"))? + .path + } + }; + let idl = anchor_idl::build::build_idl( + program_path, + cfg.features.resolution, + cfg.features.skip_lint || skip_lint, no_docs, - !cfg.features.skip_lint, )?; - let out = match out { + Some(path) => OutFile::File(PathBuf::from(path)), None => OutFile::Stdout, - Some(out) => OutFile::File(PathBuf::from(out)), }; write_idl(&idl, out)?; - // Write out the TypeScript IDL. - if let Some(out) = out_ts { - fs::write(out, rust_template::idl_ts(&idl)?)?; + if let Some(path) = out_ts { + fs::write(path, rust_template::idl_ts(&idl)?)?; } Ok(()) } -fn idl_build(out: Option, out_ts: Option, no_docs: bool) -> Result<()> { - let idls = generate_idl_build(no_docs)?; - if idls.len() == 1 { - let idl = &idls[0]; - let out = match out { - None => OutFile::Stdout, - Some(path) => OutFile::File(PathBuf::from(path)), - }; - write_idl(idl, out)?; - - if let Some(path) = out_ts { - fs::write(path, rust_template::idl_ts(idl)?)?; - } - } else { - println!("{}", serde_json::to_string_pretty(&idls)?); - }; - - Ok(()) -} - /// Generate IDL with method decided by whether manifest file has `idl-build` feature or not. fn generate_idl(cfg: &WithPath, skip_lint: bool, no_docs: bool) -> Result { - let manifest = Manifest::discover()?.ok_or_else(|| anyhow!("Cargo.toml not found"))?; - // Check whether the manifest has `idl-build` feature + let manifest = Manifest::discover()?.ok_or_else(|| anyhow!("Cargo.toml not found"))?; let is_idl_build = manifest .features .iter() .any(|(feature, _)| feature == "idl-build"); - if is_idl_build { - generate_idl_build(no_docs)? - .into_iter() - .next() - .ok_or_else(|| anyhow!("Could not build IDL")) - } else { - generate_idl_parse( - "src/lib.rs", - manifest.version(), - cfg.features.seeds, - no_docs, - !(cfg.features.skip_lint || skip_lint), - ) - } -} - -/// Generate IDL with the parsing method(default). -fn generate_idl_parse( - path: impl AsRef, - version: String, - seeds_feature: bool, - no_docs: bool, - safety_checks: bool, -) -> Result { - anchor_syn::idl::parse::file::parse(path, version, seeds_feature, no_docs, safety_checks) -} - -/// Generate IDL with the build method. -fn generate_idl_build(no_docs: bool) -> Result> { - let no_docs = if no_docs { "TRUE" } else { "FALSE" }; - - let cfg = Config::discover(&ConfigOverride::default())?.expect("Not in workspace."); - let seeds_feature = if cfg.features.seeds { "TRUE" } else { "FALSE" }; - - let exit = std::process::Command::new("cargo") - .args([ - "test", - "__anchor_private_print_idl", - "--features", - "idl-build", - "--", - "--show-output", - "--quiet", - ]) - .env("ANCHOR_IDL_BUILD_NO_DOCS", no_docs) - .env("ANCHOR_IDL_BUILD_SEEDS_FEATURE", seeds_feature) - .stderr(Stdio::inherit()) - .output() - .map_err(|e| anyhow::format_err!("{}", e.to_string()))?; - if !exit.status.success() { - std::process::exit(exit.status.code().unwrap_or(1)); - } - - enum State { - Pass, - ConstLines(Vec), - EventLines(Vec), - ErrorsLines(Vec), - ProgramLines(Vec), - } - - #[derive(Serialize, Deserialize)] - struct IdlBuildEventPrint { - event: IdlEvent, - defined_types: Vec, - } - - let mut state = State::Pass; - - let mut events: Vec = vec![]; - let mut error_codes: Option> = None; - let mut constants: Vec = vec![]; - let mut defined_types: BTreeMap = BTreeMap::new(); - let mut curr_idl: Option = None; - - let mut idls: Vec = vec![]; - - let output = String::from_utf8_lossy(&exit.stdout); - for line in output.lines() { - match &mut state { - State::Pass => { - if line == "---- IDL begin const ----" { - state = State::ConstLines(vec![]); - continue; - } else if line == "---- IDL begin event ----" { - state = State::EventLines(vec![]); - continue; - } else if line == "---- IDL begin errors ----" { - state = State::ErrorsLines(vec![]); - continue; - } else if line == "---- IDL begin program ----" { - state = State::ProgramLines(vec![]); - continue; - } else if line.starts_with("test result: ok") { - let events = std::mem::take(&mut events); - let error_codes = error_codes.take(); - let constants = std::mem::take(&mut constants); - let mut defined_types = std::mem::take(&mut defined_types); - let curr_idl = curr_idl.take(); - - let events = if !events.is_empty() { - Some(events) - } else { - None - }; - - let mut idl = match curr_idl { - Some(idl) => idl, - None => continue, - }; - - idl.events = events; - idl.errors = error_codes; - idl.constants = constants; - - idl.constants.sort_by(|a, b| a.name.cmp(&b.name)); - idl.accounts.sort_by(|a, b| a.name.cmp(&b.name)); - if let Some(e) = idl.events.as_mut() { - e.sort_by(|a, b| a.name.cmp(&b.name)) - } - - let prog_ty = std::mem::take(&mut idl.types); - defined_types.extend(prog_ty.into_iter().map(|ty| (ty.name.clone(), ty))); - idl.types = defined_types.into_values().collect::>(); - - idls.push(idl); - continue; - } - } - State::ConstLines(lines) => { - if line == "---- IDL end const ----" { - let constant: IdlConst = serde_json::from_str(&lines.join("\n"))?; - constants.push(constant); - state = State::Pass; - continue; - } - lines.push(line.to_string()); - } - State::EventLines(lines) => { - if line == "---- IDL end event ----" { - let event: IdlBuildEventPrint = serde_json::from_str(&lines.join("\n"))?; - events.push(event.event); - defined_types.extend( - event - .defined_types - .into_iter() - .map(|ty| (ty.name.clone(), ty)), - ); - state = State::Pass; - continue; - } - lines.push(line.to_string()); - } - State::ErrorsLines(lines) => { - if line == "---- IDL end errors ----" { - let errs: Vec = serde_json::from_str(&lines.join("\n"))?; - error_codes = Some(errs); - state = State::Pass; - continue; - } - lines.push(line.to_string()); - } - State::ProgramLines(lines) => { - if line == "---- IDL end program ----" { - let idl: Idl = serde_json::from_str(&lines.join("\n"))?; - curr_idl = Some(idl); - state = State::Pass; - continue; - } - lines.push(line.to_string()); - } - } - } - - // Convert path to name if there are no conflicts - let path_regex = Regex::new(r#""((\w+::)+)(\w+)""#).unwrap(); - let idls = idls - .into_iter() - .filter_map(|idl| { - let mut modified_idl = serde_json::to_string(&idl).unwrap(); - - // TODO: Remove. False positive https://github.com/rust-lang/rust-clippy/issues/10577 - #[allow(clippy::redundant_clone)] - for captures in path_regex.captures_iter(&modified_idl.clone()) { - let path = captures.get(0).unwrap().as_str(); - let name = captures.get(3).unwrap().as_str(); - - // Replace path with name - let replaced_idl = modified_idl.replace(path, &format!(r#""{name}""#)); - - // Check whether there is a conflict - let has_conflict = replaced_idl.contains(&format!(r#"::{name}""#)); - if !has_conflict { - modified_idl = replaced_idl; - } - } + if !is_idl_build { + let path = manifest.path().display(); + let anchor_spl_idl_build = manifest + .dependencies + .iter() + .any(|dep| dep.0 == "anchor-spl") + .then_some(r#", "anchor-spl/idl-build""#) + .unwrap_or_default(); - serde_json::from_str::(&modified_idl).ok() - }) - .collect::>(); + return Err(anyhow!( + r#"`idl-build` feature is missing. To solve, add - // Verify IDLs are valid - for idl in &idls { - let full_path_account = idl - .accounts - .iter() - .find(|account| account.name.contains("::")); +[features] +idl-build = ["anchor-lang/idl-build"{anchor_spl_idl_build}] - if let Some(account) = full_path_account { - return Err(anyhow!( - "Conflicting accounts names are not allowed.\nProgram: {}\nAccount: {}", - idl.name, - account.name - )); - } +in `{path}`."# + )); } - Ok(idls) + anchor_idl::build::build_idl( + std::env::current_dir()?, + cfg.features.resolution, + cfg.features.skip_lint || skip_lint, + no_docs, + ) } fn idl_fetch(cfg_override: &ConfigOverride, address: Pubkey, out: Option) -> Result<()> { @@ -2841,7 +2620,7 @@ fn account( let bytes = fs::read(idl_path).expect("Unable to read IDL."); let idl: Idl = serde_json::from_reader(&*bytes).expect("Invalid IDL format."); - if idl.name != program_name { + if idl.metadata.name != program_name { panic!("IDL does not match program {program_name}."); } @@ -2882,25 +2661,40 @@ fn deserialize_idl_defined_type_to_json( data: &mut &[u8], ) -> Result { let defined_type = &idl - .types + .accounts .iter() - .chain(idl.accounts.iter()) - .find(|defined_type| defined_type.name == defined_type_name) + .find(|acc| acc.name == defined_type_name) + .and_then(|acc| idl.types.iter().find(|ty| ty.name == acc.name)) + .or_else(|| idl.types.iter().find(|ty| ty.name == defined_type_name)) .ok_or_else(|| anyhow!("Type `{}` not found in IDL.", defined_type_name))? .ty; let mut deserialized_fields = Map::new(); match defined_type { - IdlTypeDefinitionTy::Struct { fields } => { - for field in fields { - deserialized_fields.insert( - field.name.clone(), - deserialize_idl_type_to_json(&field.ty, data, idl)?, - ); + IdlTypeDefTy::Struct { fields } => { + if let Some(fields) = fields { + match fields { + IdlDefinedFields::Named(fields) => { + for field in fields { + deserialized_fields.insert( + field.name.clone(), + deserialize_idl_type_to_json(&field.ty, data, idl)?, + ); + } + } + IdlDefinedFields::Tuple(fields) => { + let mut values = Vec::new(); + for field in fields { + values.push(deserialize_idl_type_to_json(field, data, idl)?); + } + deserialized_fields + .insert(defined_type_name.to_owned(), JsonValue::Array(values)); + } + } } } - IdlTypeDefinitionTy::Enum { variants } => { + IdlTypeDefTy::Enum { variants } => { let repr = ::deserialize(data)?; let variant = variants @@ -2911,25 +2705,21 @@ fn deserialize_idl_defined_type_to_json( if let Some(enum_field) = &variant.fields { match enum_field { - EnumFields::Named(fields) => { + IdlDefinedFields::Named(fields) => { let mut values = Map::new(); - for field in fields { values.insert( field.name.clone(), deserialize_idl_type_to_json(&field.ty, data, idl)?, ); } - value = JsonValue::Object(values); } - EnumFields::Tuple(fields) => { + IdlDefinedFields::Tuple(fields) => { let mut values = Vec::new(); - for field in fields { values.push(deserialize_idl_type_to_json(field, data, idl)?); } - value = JsonValue::Array(values); } } @@ -2937,8 +2727,8 @@ fn deserialize_idl_defined_type_to_json( deserialized_fields.insert(variant.name.clone(), value); } - IdlTypeDefinitionTy::Alias { value } => { - return deserialize_idl_type_to_json(value, data, idl); + IdlTypeDefTy::Type { alias } => { + return deserialize_idl_type_to_json(alias, data, idl); } } @@ -3000,12 +2790,22 @@ fn deserialize_idl_type_to_json( .collect(), ), IdlType::String => json!(::deserialize(data)?), - IdlType::PublicKey => { + IdlType::Pubkey => { json!(::deserialize(data)?.to_string()) } - IdlType::Defined(type_name) => { - deserialize_idl_defined_type_to_json(parent_idl, type_name, data)? - } + IdlType::Array(ty, size) => match size { + IdlArrayLen::Value(size) => { + let mut array_data: Vec = Vec::with_capacity(*size); + + for _ in 0..*size { + array_data.push(deserialize_idl_type_to_json(ty, data, parent_idl)?); + } + + JsonValue::Array(array_data) + } + // TODO: + IdlArrayLen::Generic(_) => unimplemented!("Generic array length is not yet supported"), + }, IdlType::Option(ty) => { let is_present = ::deserialize(data)?; @@ -3028,20 +2828,14 @@ fn deserialize_idl_type_to_json( JsonValue::Array(vec_data) } - IdlType::Array(ty, size) => { - let mut array_data: Vec = Vec::with_capacity(*size); - - for _ in 0..*size { - array_data.push(deserialize_idl_type_to_json(ty, data, parent_idl)?); - } - - JsonValue::Array(array_data) - } - IdlType::GenericLenArray(_, _) => todo!("Generic length arrays are not yet supported"), - IdlType::Generic(_) => todo!("Generic types are not yet supported"), - IdlType::DefinedWithTypeArgs { name: _, args: _ } => { - todo!("Defined types with type args are not yet supported") + IdlType::Defined { + name, + generics: _generics, + } => { + // TODO: Generics + deserialize_idl_defined_type_to_json(parent_idl, name, data)? } + IdlType::Generic(generic) => json!(generic), }) } @@ -3313,11 +3107,11 @@ fn validator_flags( if let Some(idl) = program.idl.as_mut() { // Add program address to the IDL. - idl.metadata = Some(serde_json::to_value(IdlTestMetadata { address })?); + idl.address = address; // Persist it. let idl_out = PathBuf::from("target/idl") - .join(&idl.name) + .join(&idl.metadata.name) .with_extension("json"); write_idl(idl, OutFile::File(idl_out))?; } @@ -3442,22 +3236,15 @@ fn stream_logs(config: &WithPath, rpc_url: &str) -> Result, rpc_url: &str) -> Result, @@ -3717,13 +3499,11 @@ fn deploy( if let Some(idl) = program.idl.as_mut() { // Add program address to the IDL. - idl.metadata = Some(serde_json::to_value(IdlTestMetadata { - address: program_id.to_string(), - })?); + idl.address = program_id.to_string(); // Persist it. let idl_out = PathBuf::from("target/idl") - .join(&idl.name) + .join(&idl.metadata.name) .with_extension("json"); write_idl(idl, OutFile::File(idl_out))?; } @@ -4043,7 +3823,7 @@ fn shell(cfg_override: &ConfigOverride) -> Result<()> { .filter(|program| program.idl.is_some()) .map(|program| { ( - program.idl.as_ref().unwrap().name.clone(), + program.idl.as_ref().unwrap().metadata.name.clone(), program.idl.clone().unwrap(), ) }) diff --git a/cli/src/rust_template.rs b/cli/src/rust_template.rs index ed7a87c4f3..abbf2c359f 100644 --- a/cli/src/rust_template.rs +++ b/cli/src/rust_template.rs @@ -2,10 +2,11 @@ use crate::{ config::ProgramWorkspace, create_files, override_or_create_files, solidity_template, Files, VERSION, }; -use anchor_syn::idl::types::Idl; +use anchor_idl::types::Idl; use anyhow::Result; use clap::{Parser, ValueEnum}; -use heck::{ToLowerCamelCase, ToSnakeCase, ToUpperCamelCase}; +use heck::{ToLowerCamelCase, ToPascalCase, ToSnakeCase}; +use regex::Regex; use solana_sdk::{ pubkey::Pubkey, signature::{read_keypair_file, write_keypair_file, Keypair}, @@ -17,6 +18,7 @@ use std::{ io::Write as _, path::Path, process::Stdio, + str::FromStr, }; /// Program initialization template @@ -183,11 +185,12 @@ crate-type = ["cdylib", "lib"] name = "{1}" [features] +default = [] +cpi = ["no-entrypoint"] no-entrypoint = [] no-idl = [] no-log-ix-name = [] -cpi = ["no-entrypoint"] -default = [] +idl-build = ["anchor-lang/idl-build"] [dependencies] anchor-lang = "{2}" @@ -228,20 +231,37 @@ token = "{token}" } pub fn idl_ts(idl: &Idl) -> Result { - let mut idl = idl.clone(); - for acc in idl.accounts.iter_mut() { - acc.name = acc.name.to_lower_camel_case(); - } - let idl_json = serde_json::to_string_pretty(&idl)?; - Ok(format!( - r#"export type {} = {}; + let idl_name = &idl.metadata.name; + let type_name = idl_name.to_pascal_case(); + let idl = serde_json::to_string(idl)?; + + // Convert every field of the IDL to camelCase + let camel_idl = Regex::new(r#""\w+":"([\w\d]+)""#)? + .captures_iter(&idl) + .fold(idl.clone(), |acc, cur| { + let name = cur.get(1).unwrap().as_str(); + + // Do not modify pubkeys + if Pubkey::from_str(name).is_ok() { + return acc; + } -export const IDL: {} = {}; -"#, - idl.name.to_upper_camel_case(), - idl_json, - idl.name.to_upper_camel_case(), - idl_json + let camel_name = name.to_lower_camel_case(); + acc.replace(&format!(r#""{name}""#), &format!(r#""{camel_name}""#)) + }); + + // Pretty format + let camel_idl = serde_json::to_string_pretty(&serde_json::from_str::(&camel_idl)?)?; + + Ok(format!( + r#"/** + * Program IDL in camelCase format in order to be used in JS/TS. + * + * Note that this is only a type helper and is not the actual IDL. The original + * IDL can be found at `target/idl/{idl_name}.json`. + */ +export type {type_name} = {camel_idl}; +"# )) } @@ -347,7 +367,7 @@ describe("{}", () => {{ }}); "#, name, - name.to_upper_camel_case(), + name.to_pascal_case(), ) } @@ -368,7 +388,7 @@ describe("{}", () => {{ }}); "#, name, - name.to_upper_camel_case(), + name.to_pascal_case(), ) } @@ -478,11 +498,11 @@ describe("{}", () => {{ }}); }}); "#, - name.to_upper_camel_case(), + name.to_pascal_case(), name.to_snake_case(), name, - name.to_upper_camel_case(), - name.to_upper_camel_case(), + name.to_pascal_case(), + name.to_pascal_case(), ) } @@ -505,11 +525,11 @@ describe("{}", () => {{ }}); }}); "#, - name.to_upper_camel_case(), + name.to_pascal_case(), name.to_snake_case(), name, - name.to_upper_camel_case(), - name.to_upper_camel_case(), + name.to_pascal_case(), + name.to_pascal_case(), ) } @@ -606,7 +626,7 @@ anchor.setProvider(provider); r#" anchor.workspace.{} = new anchor.Program({}, new PublicKey("{}"), provider); "#, - program.name.to_upper_camel_case(), + program.name.to_pascal_case(), serde_json::to_string(&program.idl)?, program.program_id )?; diff --git a/examples/tutorial/basic-0/programs/basic-0/Cargo.toml b/examples/tutorial/basic-0/programs/basic-0/Cargo.toml index c980c51854..0456471796 100644 --- a/examples/tutorial/basic-0/programs/basic-0/Cargo.toml +++ b/examples/tutorial/basic-0/programs/basic-0/Cargo.toml @@ -12,6 +12,7 @@ name = "basic_0" [features] no-entrypoint = [] cpi = ["no-entrypoint"] +idl-build = ["anchor-lang/idl-build"] [dependencies] anchor-lang = { path = "../../../../../lang" } diff --git a/examples/tutorial/basic-1/programs/basic-1/Cargo.toml b/examples/tutorial/basic-1/programs/basic-1/Cargo.toml index e207669643..ac64592f7a 100644 --- a/examples/tutorial/basic-1/programs/basic-1/Cargo.toml +++ b/examples/tutorial/basic-1/programs/basic-1/Cargo.toml @@ -12,6 +12,7 @@ name = "basic_1" [features] no-entrypoint = [] cpi = ["no-entrypoint"] +idl-build = ["anchor-lang/idl-build"] [dependencies] anchor-lang = { path = "../../../../../lang" } diff --git a/examples/tutorial/basic-2/programs/basic-2/Cargo.toml b/examples/tutorial/basic-2/programs/basic-2/Cargo.toml index dae5ea4345..1a41066e7e 100644 --- a/examples/tutorial/basic-2/programs/basic-2/Cargo.toml +++ b/examples/tutorial/basic-2/programs/basic-2/Cargo.toml @@ -12,6 +12,7 @@ name = "basic_2" [features] no-entrypoint = [] cpi = ["no-entrypoint"] +idl-build = ["anchor-lang/idl-build"] [dependencies] anchor-lang = { path = "../../../../../lang" } diff --git a/examples/tutorial/basic-3/programs/puppet-master/Cargo.toml b/examples/tutorial/basic-3/programs/puppet-master/Cargo.toml index 04b3e044d3..569c62145e 100644 --- a/examples/tutorial/basic-3/programs/puppet-master/Cargo.toml +++ b/examples/tutorial/basic-3/programs/puppet-master/Cargo.toml @@ -12,6 +12,7 @@ name = "puppet_master" [features] no-entrypoint = [] cpi = ["no-entrypoint"] +idl-build = ["anchor-lang/idl-build"] [dependencies] anchor-lang = { path = "../../../../../lang" } diff --git a/examples/tutorial/basic-3/programs/puppet/Cargo.toml b/examples/tutorial/basic-3/programs/puppet/Cargo.toml index 862457316a..384b3817e0 100644 --- a/examples/tutorial/basic-3/programs/puppet/Cargo.toml +++ b/examples/tutorial/basic-3/programs/puppet/Cargo.toml @@ -12,6 +12,7 @@ name = "puppet" [features] no-entrypoint = [] cpi = ["no-entrypoint"] +idl-build = ["anchor-lang/idl-build"] [dependencies] anchor-lang = { path = "../../../../../lang" } diff --git a/examples/tutorial/basic-4/programs/basic-4/Cargo.toml b/examples/tutorial/basic-4/programs/basic-4/Cargo.toml index 3a143035cc..87b2bd6cc5 100644 --- a/examples/tutorial/basic-4/programs/basic-4/Cargo.toml +++ b/examples/tutorial/basic-4/programs/basic-4/Cargo.toml @@ -12,6 +12,7 @@ name = "basic_4" [features] no-entrypoint = [] cpi = ["no-entrypoint"] +idl-build = ["anchor-lang/idl-build"] [dependencies] anchor-lang = { path = "../../../../../lang" } diff --git a/examples/tutorial/basic-5/programs/basic-5/Cargo.toml b/examples/tutorial/basic-5/programs/basic-5/Cargo.toml index 8b5ee1f8e3..5abbb7fed3 100644 --- a/examples/tutorial/basic-5/programs/basic-5/Cargo.toml +++ b/examples/tutorial/basic-5/programs/basic-5/Cargo.toml @@ -12,6 +12,7 @@ name = "basic_5" [features] no-entrypoint = [] cpi = ["no-entrypoint"] +idl-build = ["anchor-lang/idl-build"] [dependencies] anchor-lang = { path = "../../../../../lang" } \ No newline at end of file diff --git a/idl/Cargo.toml b/idl/Cargo.toml new file mode 100644 index 0000000000..1447e31bcc --- /dev/null +++ b/idl/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "anchor-idl" +version = "0.29.0" +authors = ["Anchor Maintainers "] +repository = "https://github.com/coral-xyz/anchor" +rust-version = "1.60" +edition = "2021" +license = "Apache-2.0" +description = "Anchor framework IDL" + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + +[features] +build = [ + "anyhow", + "regex", + "serde", + "serde_json", +] + +[dependencies] +anchor-syn = { path = "../lang/syn", version = "0.29.0", features = ["idl-types"] } + +# `build` feature only +anyhow = { version = "1", optional = true } +regex = { version = "1", optional = true } +serde = { version = "1", features = ["derive"], optional = true } +serde_json = { version = "1", optional = true } diff --git a/idl/src/build.rs b/idl/src/build.rs new file mode 100644 index 0000000000..4758011a86 --- /dev/null +++ b/idl/src/build.rs @@ -0,0 +1,252 @@ +use std::{ + collections::BTreeMap, + env, mem, + path::Path, + process::{Command, Stdio}, +}; + +use anchor_syn::parser::context::CrateContext; +use anyhow::{anyhow, Result}; +use regex::Regex; +use serde::Deserialize; + +use crate::types::{Idl, IdlEvent, IdlTypeDef}; + +/// Generate IDL via compilation. +pub fn build_idl( + program_path: impl AsRef, + resolution: bool, + skip_lint: bool, + no_docs: bool, +) -> Result { + // Check safety comments + let program_path = program_path.as_ref(); + let lib_path = program_path.join("src").join("lib.rs"); + let ctx = CrateContext::parse(lib_path)?; + if !skip_lint { + ctx.safety_checks()?; + } + + let idl = build(program_path, resolution, no_docs)?; + let idl = convert_module_paths(idl); + let idl = sort(idl); + verify(&idl)?; + + Ok(idl) +} + +// Build IDL. +fn build(program_path: &Path, resolution: bool, no_docs: bool) -> Result { + // `nightly` toolchain is currently required for building the IDL. + // + // Pinning the toolchain to an older date in order to fix + // `error[E0635]: unknown feature stdsimd` error from `ahash`. + // See: https://github.com/tkaitchuck/aHash/issues/200 + // + // There is also another error when using a date after 2024-01-30 + // `error[E0412]: cannot find type `T` in this scope`` + // + // TODO: Unpin `nightly` release after upgrading Solana to `1.18`. + const TOOLCHAIN: &str = "+nightly-2024-01-30"; + install_toolchain_if_needed(TOOLCHAIN)?; + + let output = Command::new("cargo") + .args([ + TOOLCHAIN, + "test", + "__anchor_private_print_idl", + "--features", + "idl-build", + "--", + "--show-output", + "--quiet", + ]) + .env( + "ANCHOR_IDL_BUILD_NO_DOCS", + if no_docs { "TRUE" } else { "FALSE" }, + ) + .env( + "ANCHOR_IDL_BUILD_RESOLUTION", + if resolution { "TRUE" } else { "FALSE" }, + ) + .env("RUSTFLAGS", "--cfg procmacro2_semver_exempt") + .current_dir(program_path) + .stderr(Stdio::inherit()) + .output()?; + if !output.status.success() { + return Err(anyhow!("Building IDL failed")); + } + + enum State { + Pass, + Address, + Constants(Vec), + Events(Vec), + Errors(Vec), + Program(Vec), + } + + let mut address = String::new(); + let mut events = vec![]; + let mut error_codes = vec![]; + let mut constants = vec![]; + let mut types = BTreeMap::new(); + let mut idl: Option = None; + + let output = String::from_utf8_lossy(&output.stdout); + if env::var("ANCHOR_LOG").is_ok() { + println!("{}", output); + } + + let mut state = State::Pass; + for line in output.lines() { + match &mut state { + State::Pass => match line { + "--- IDL begin address ---" => state = State::Address, + "--- IDL begin const ---" => state = State::Constants(vec![]), + "--- IDL begin event ---" => state = State::Events(vec![]), + "--- IDL begin errors ---" => state = State::Errors(vec![]), + "--- IDL begin program ---" => state = State::Program(vec![]), + _ => { + if line.starts_with("test result: ok") { + if let Some(idl) = idl.as_mut() { + idl.address = mem::take(&mut address); + idl.constants = mem::take(&mut constants); + idl.events = mem::take(&mut events); + idl.errors = mem::take(&mut error_codes); + idl.types = { + let prog_ty = mem::take(&mut idl.types); + let mut types = mem::take(&mut types); + types.extend(prog_ty.into_iter().map(|ty| (ty.name.clone(), ty))); + types.into_values().collect() + }; + } + } + } + }, + State::Address => { + address = line.replace(|c: char| !c.is_alphanumeric(), ""); + state = State::Pass; + continue; + } + State::Constants(lines) => { + if line == "--- IDL end const ---" { + let constant = serde_json::from_str(&lines.join("\n"))?; + constants.push(constant); + state = State::Pass; + continue; + } + + lines.push(line.to_owned()); + } + State::Events(lines) => { + if line == "--- IDL end event ---" { + #[derive(Deserialize)] + struct IdlBuildEventPrint { + event: IdlEvent, + types: Vec, + } + + let event = serde_json::from_str::(&lines.join("\n"))?; + events.push(event.event); + types.extend(event.types.into_iter().map(|ty| (ty.name.clone(), ty))); + state = State::Pass; + continue; + } + + lines.push(line.to_owned()); + } + State::Errors(lines) => { + if line == "--- IDL end errors ---" { + error_codes = serde_json::from_str(&lines.join("\n"))?; + state = State::Pass; + continue; + } + + lines.push(line.to_owned()); + } + State::Program(lines) => { + if line == "--- IDL end program ---" { + idl = Some(serde_json::from_str(&lines.join("\n"))?); + state = State::Pass; + continue; + } + + lines.push(line.to_owned()); + } + } + } + + idl.ok_or_else(|| anyhow!("IDL doesn't exist")) +} + +/// Install the given toolchain if it's not already installed. +fn install_toolchain_if_needed(toolchain: &str) -> Result<()> { + let is_installed = Command::new("cargo") + .arg(toolchain) + .output()? + .status + .success(); + if !is_installed { + Command::new("rustup") + .args(["toolchain", "install", toolchain.trim_start_matches('+')]) + .spawn()? + .wait()?; + } + + Ok(()) +} + +/// Convert paths to name if there are no conflicts. +fn convert_module_paths(idl: Idl) -> Idl { + let idl = serde_json::to_string(&idl).unwrap(); + let idl = Regex::new(r#""((\w+::)+)(\w+)""#) + .unwrap() + .captures_iter(&idl.clone()) + .fold(idl, |acc, cur| { + let path = cur.get(0).unwrap().as_str(); + let name = cur.get(3).unwrap().as_str(); + + // Replace path with name + let replaced_idl = acc.replace(path, &format!(r#""{name}""#)); + + // Check whether there is a conflict + let has_conflict = replaced_idl.contains(&format!(r#"::{name}""#)); + if has_conflict { + acc + } else { + replaced_idl + } + }); + + serde_json::from_str(&idl).expect("Invalid IDL") +} + +/// Alphabetically sort fields for consistency. +fn sort(mut idl: Idl) -> Idl { + idl.accounts.sort_by(|a, b| a.name.cmp(&b.name)); + idl.constants.sort_by(|a, b| a.name.cmp(&b.name)); + idl.events.sort_by(|a, b| a.name.cmp(&b.name)); + idl.instructions.sort_by(|a, b| a.name.cmp(&b.name)); + idl.types.sort_by(|a, b| a.name.cmp(&b.name)); + + idl +} + +/// Verify IDL is valid. +fn verify(idl: &Idl) -> Result<()> { + // Check full path accounts + if let Some(account) = idl + .accounts + .iter() + .find(|account| account.name.contains("::")) + { + return Err(anyhow!( + "Conflicting accounts names are not allowed.\nProgram: `{}`\nAccount: `{}`", + idl.metadata.name, + account.name + )); + } + + Ok(()) +} diff --git a/idl/src/lib.rs b/idl/src/lib.rs new file mode 100644 index 0000000000..2a713d9e4b --- /dev/null +++ b/idl/src/lib.rs @@ -0,0 +1,6 @@ +//! Anchor IDL. + +pub mod types; + +#[cfg(feature = "build")] +pub mod build; diff --git a/idl/src/types.rs b/idl/src/types.rs new file mode 100644 index 0000000000..5579096aae --- /dev/null +++ b/idl/src/types.rs @@ -0,0 +1,3 @@ +//! IDL types are re-exported from [`anchor_syn`]. + +pub use anchor_syn::idl::types::*; diff --git a/lang/attribute/account/src/lib.rs b/lang/attribute/account/src/lib.rs index 125b208d20..72f567809f 100644 --- a/lang/attribute/account/src/lib.rs +++ b/lang/attribute/account/src/lib.rs @@ -1,7 +1,5 @@ extern crate proc_macro; -#[cfg(feature = "idl-build")] -use anchor_syn::idl::build::*; use quote::quote; use syn::parse_macro_input; @@ -404,8 +402,18 @@ pub fn zero_copy( #[cfg(feature = "idl-build")] { - let no_docs = get_no_docs(); - let idl_build_impl = gen_idl_build_impl_for_struct(&account_strct, no_docs); + let derive_unsafe = if is_unsafe { + // Not a real proc-macro but exists in order to pass the serialization info + quote! { #[derive(bytemuck::Unsafe)] } + } else { + quote! {} + }; + let zc_struct = syn::parse2(quote! { + #derive_unsafe + #ret + }) + .unwrap(); + let idl_build_impl = anchor_syn::idl::build::impl_idl_build_struct(&zc_struct); return proc_macro::TokenStream::from(quote! { #ret #idl_build_impl @@ -420,6 +428,21 @@ pub fn zero_copy( /// based programs. #[proc_macro] pub fn declare_id(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + #[cfg(feature = "idl-build")] + let address = input.clone().to_string(); + let id = parse_macro_input!(input as id::Id); - proc_macro::TokenStream::from(quote! {#id}) + let ret = quote! { #id }; + + #[cfg(feature = "idl-build")] + { + let idl_print = anchor_syn::idl::build::gen_idl_print_fn_address(address); + return proc_macro::TokenStream::from(quote! { + #ret + #idl_print + }); + } + + #[allow(unreachable_code)] + proc_macro::TokenStream::from(ret) } diff --git a/lang/attribute/constant/src/lib.rs b/lang/attribute/constant/src/lib.rs index fd1f4ad680..edf15c39d8 100644 --- a/lang/attribute/constant/src/lib.rs +++ b/lang/attribute/constant/src/lib.rs @@ -1,8 +1,5 @@ extern crate proc_macro; -#[cfg(feature = "idl-build")] -use {anchor_syn::idl::build::gen_idl_print_function_for_constant, quote::quote, syn}; - /// A marker attribute used to mark const values that should be included in the /// generated IDL but functionally does nothing. #[proc_macro_attribute] @@ -12,9 +9,11 @@ pub fn constant( ) -> proc_macro::TokenStream { #[cfg(feature = "idl-build")] { + use quote::quote; + let ts = match syn::parse(input).unwrap() { syn::Item::Const(item) => { - let idl_print = gen_idl_print_function_for_constant(&item); + let idl_print = anchor_syn::idl::build::gen_idl_print_fn_constant(&item); quote! { #item #idl_print diff --git a/lang/attribute/event/src/lib.rs b/lang/attribute/event/src/lib.rs index a8c1837054..8edf1cf2df 100644 --- a/lang/attribute/event/src/lib.rs +++ b/lang/attribute/event/src/lib.rs @@ -46,7 +46,7 @@ pub fn event( #[cfg(feature = "idl-build")] { - let idl_build = anchor_syn::idl::build::gen_idl_print_function_for_event(&event_strct); + let idl_build = anchor_syn::idl::build::gen_idl_print_fn_event(&event_strct); return proc_macro::TokenStream::from(quote! { #ret #idl_build diff --git a/lang/derive/accounts/src/lib.rs b/lang/derive/accounts/src/lib.rs index cb47c0ee40..7884afb559 100644 --- a/lang/derive/accounts/src/lib.rs +++ b/lang/derive/accounts/src/lib.rs @@ -628,7 +628,7 @@ use syn::parse_macro_input; /// /// #[proc_macro_derive(Accounts, attributes(account, instruction))] -pub fn derive_anchor_deserialize(item: TokenStream) -> TokenStream { +pub fn derive_accounts(item: TokenStream) -> TokenStream { parse_macro_input!(item as anchor_syn::AccountsStruct) .to_token_stream() .into() diff --git a/lang/derive/serde/src/lib.rs b/lang/derive/serde/src/lib.rs index 248191fecd..75996f1e5c 100644 --- a/lang/derive/serde/src/lib.rs +++ b/lang/derive/serde/src/lib.rs @@ -5,9 +5,6 @@ use proc_macro::TokenStream; use proc_macro2::{Span, TokenStream as TokenStream2}; use syn::{Ident, Item}; -#[cfg(feature = "idl-build")] -use {anchor_syn::idl::build::*, quote::quote}; - fn gen_borsh_serialize(input: TokenStream) -> TokenStream2 { let cratename = Ident::new("borsh", Span::call_site()); @@ -35,15 +32,13 @@ pub fn anchor_serialize(input: TokenStream) -> TokenStream { #[cfg(feature = "idl-build")] { - let no_docs = get_no_docs(); + use anchor_syn::idl::build::*; + use quote::quote; let idl_build_impl = match syn::parse(input).unwrap() { - Item::Struct(item) => gen_idl_build_impl_for_struct(&item, no_docs), - Item::Enum(item) => gen_idl_build_impl_for_enum(item, no_docs), - Item::Union(item) => { - // unions are not included in the IDL - TODO print a warning - idl_build_impl_skeleton(quote! {None}, quote! {}, &item.ident, &item.generics) - } + Item::Struct(item) => impl_idl_build_struct(&item), + Item::Enum(item) => impl_idl_build_enum(&item), + Item::Union(item) => impl_idl_build_union(&item), // Derive macros can only be defined on structs, enums, and unions. _ => unreachable!(), }; diff --git a/lang/src/bpf_upgradeable_state.rs b/lang/src/bpf_upgradeable_state.rs index 9da8385619..845f391487 100644 --- a/lang/src/bpf_upgradeable_state.rs +++ b/lang/src/bpf_upgradeable_state.rs @@ -72,3 +72,13 @@ impl AccountDeserialize for UpgradeableLoaderState { bincode::deserialize(buf).map_err(|_| ProgramError::InvalidAccountData.into()) } } + +#[cfg(feature = "idl-build")] +mod idl_build { + use super::*; + + impl crate::IdlBuild for ProgramData {} + impl crate::Discriminator for ProgramData { + const DISCRIMINATOR: [u8; 8] = [u8::MAX; 8]; + } +} diff --git a/lang/syn/Cargo.toml b/lang/syn/Cargo.toml index 3a9dca8360..fb2684741a 100644 --- a/lang/syn/Cargo.toml +++ b/lang/syn/Cargo.toml @@ -17,12 +17,10 @@ allow-missing-optionals = [] anchor-debug = [] event-cpi = [] hash = [] -idl-build = ["idl-parse", "idl-types"] -idl-parse = ["idl-types"] +idl-build = ["idl-types", "cargo_toml"] idl-types = [] init-if-needed = [] interface-instructions = [] -seeds = [] [dependencies] anyhow = "1" @@ -35,3 +33,6 @@ serde_json = "1" sha2 = "0.10" syn = { version = "1", features = ["full", "extra-traits", "parsing"] } thiserror = "1" + +# `idl-build` feature only +cargo_toml = { version = "0.15", optional = true } diff --git a/lang/syn/src/codegen/accounts/constraints.rs b/lang/syn/src/codegen/accounts/constraints.rs index 0418501df8..e02995699e 100644 --- a/lang/syn/src/codegen/accounts/constraints.rs +++ b/lang/syn/src/codegen/accounts/constraints.rs @@ -1098,7 +1098,7 @@ impl<'a> OptionalCheckScope<'a> { check_scope } pub fn generate_check(&mut self, field: impl ToTokens) -> TokenStream { - let field_name = tts_to_string(&field); + let field_name = parser::tts_to_string(&field); if self.seen.contains(&field_name) { quote! {} } else { diff --git a/lang/syn/src/codegen/accounts/mod.rs b/lang/syn/src/codegen/accounts/mod.rs index 2e0c3d5b33..fec6265602 100644 --- a/lang/syn/src/codegen/accounts/mod.rs +++ b/lang/syn/src/codegen/accounts/mod.rs @@ -37,10 +37,7 @@ pub fn generate(accs: &AccountsStruct) -> proc_macro2::TokenStream { #[cfg(feature = "idl-build")] { - #![allow(warnings)] - let no_docs = crate::idl::build::get_no_docs(); - let idl_build_impl = - crate::idl::build::gen_idl_build_impl_for_accounts_struct(&accs, no_docs); + let idl_build_impl = crate::idl::build::gen_idl_build_impl_accounts_struct(accs); return quote! { #ret #idl_build_impl diff --git a/lang/syn/src/codegen/error.rs b/lang/syn/src/codegen/error.rs index 2eedd7b489..f61212e3b8 100644 --- a/lang/syn/src/codegen/error.rs +++ b/lang/syn/src/codegen/error.rs @@ -1,9 +1,6 @@ use crate::Error; use quote::quote; -#[cfg(feature = "idl-build")] -use crate::idl::build::gen_idl_print_function_for_error; - pub fn generate(error: Error) -> proc_macro2::TokenStream { let error_enum = &error.raw_enum; let enum_name = &error.ident; @@ -103,10 +100,10 @@ pub fn generate(error: Error) -> proc_macro2::TokenStream { #[cfg(feature = "idl-build")] { - let idl_build = gen_idl_print_function_for_error(&error); + let idl_print = crate::idl::build::gen_idl_print_fn_error(&error); return quote! { #ret - #idl_build + #idl_print }; }; diff --git a/lang/syn/src/codegen/program/mod.rs b/lang/syn/src/codegen/program/mod.rs index c8f96ae104..6c456e2c17 100644 --- a/lang/syn/src/codegen/program/mod.rs +++ b/lang/syn/src/codegen/program/mod.rs @@ -39,12 +39,10 @@ pub fn generate(program: &Program) -> proc_macro2::TokenStream { #[cfg(feature = "idl-build")] { - let no_docs = crate::idl::build::get_no_docs(); - let idl_build = crate::idl::build::gen_idl_print_function_for_program(program, no_docs); - + let idl_build_impl = crate::idl::build::gen_idl_print_fn_program(program); return quote! { #ret - #idl_build + #idl_build_impl }; }; diff --git a/lang/syn/src/idl/build.rs b/lang/syn/src/idl/build.rs deleted file mode 100644 index 1aea742b85..0000000000 --- a/lang/syn/src/idl/build.rs +++ /dev/null @@ -1,948 +0,0 @@ -pub use serde_json; - -use crate::{parser::docs, AccountField, AccountsStruct, Error, Program}; -use heck::MixedCase; -use proc_macro2::TokenStream; -use quote::{format_ident, quote}; -use syn::{Ident, ItemEnum, ItemStruct}; - -/// A trait that types must implement in order to generate the IDL via compilation. -/// -/// This trait is automatically implemented for Anchor all types that use the `AnchorSerialize` -/// proc macro. Note that manually implementing the `AnchorSerialize` trait will **NOT** have the -/// same effect. -/// -/// Types that don't implement this trait will cause a compile error during the IDL generation. -/// -/// The methods have default implementation that allows the program to compile but the type will -/// **NOT** be included in the IDL. -pub trait IdlBuild { - /// Returns the full module path of the type. - fn __anchor_private_full_path() -> String { - String::default() - } - - /// Returns the IDL type definition of the type or `None` if it doesn't exist. - fn __anchor_private_gen_idl_type() -> Option { - None - } - - /// Insert the type definition to the defined types hashmap. - fn __anchor_private_insert_idl_defined( - _defined_types: &mut std::collections::HashMap, - ) { - } -} - -#[inline(always)] -fn get_module_paths() -> (TokenStream, TokenStream) { - ( - quote!(anchor_lang::anchor_syn::idl::types), - quote!(anchor_lang::anchor_syn::idl::build::serde_json), - ) -} - -#[inline(always)] -pub fn get_no_docs() -> bool { - std::option_env!("ANCHOR_IDL_BUILD_NO_DOCS") - .map(|val| val == "TRUE") - .unwrap_or(false) -} - -#[inline(always)] -pub fn get_seeds_feature() -> bool { - std::option_env!("ANCHOR_IDL_BUILD_SEEDS_FEATURE") - .map(|val| val == "TRUE") - .unwrap_or(false) -} - -// Returns TokenStream for IdlType enum and the syn::TypePath for the defined -// type if any. -// Returns Err when the type wasn't parsed successfully. -#[allow(clippy::result_unit_err)] -pub fn idl_type_ts_from_syn_type( - ty: &syn::Type, - type_params: &Vec, -) -> Result<(TokenStream, Vec), ()> { - let (idl, _) = get_module_paths(); - - fn the_only_segment_is(path: &syn::TypePath, cmp: &str) -> bool { - if path.path.segments.len() != 1 { - return false; - }; - return path.path.segments.first().unwrap().ident == cmp; - } - - // Foo -> first::path - fn get_first_angle_bracketed_path_arg(segment: &syn::PathSegment) -> Option<&syn::Type> { - match &segment.arguments { - syn::PathArguments::AngleBracketed(arguments) => match arguments.args.first() { - Some(syn::GenericArgument::Type(ty)) => Some(ty), - _ => None, - }, - _ => None, - } - } - - match ty { - syn::Type::Path(path) if the_only_segment_is(path, "bool") => { - Ok((quote! { #idl::IdlType::Bool }, vec![])) - } - syn::Type::Path(path) if the_only_segment_is(path, "u8") => { - Ok((quote! { #idl::IdlType::U8 }, vec![])) - } - syn::Type::Path(path) if the_only_segment_is(path, "i8") => { - Ok((quote! { #idl::IdlType::I8 }, vec![])) - } - syn::Type::Path(path) if the_only_segment_is(path, "u16") => { - Ok((quote! { #idl::IdlType::U16 }, vec![])) - } - syn::Type::Path(path) if the_only_segment_is(path, "i16") => { - Ok((quote! { #idl::IdlType::I16 }, vec![])) - } - syn::Type::Path(path) if the_only_segment_is(path, "u32") => { - Ok((quote! { #idl::IdlType::U32 }, vec![])) - } - syn::Type::Path(path) if the_only_segment_is(path, "i32") => { - Ok((quote! { #idl::IdlType::I32 }, vec![])) - } - syn::Type::Path(path) if the_only_segment_is(path, "f32") => { - Ok((quote! { #idl::IdlType::F32 }, vec![])) - } - syn::Type::Path(path) if the_only_segment_is(path, "u64") => { - Ok((quote! { #idl::IdlType::U64 }, vec![])) - } - syn::Type::Path(path) if the_only_segment_is(path, "i64") => { - Ok((quote! { #idl::IdlType::I64 }, vec![])) - } - syn::Type::Path(path) if the_only_segment_is(path, "f64") => { - Ok((quote! { #idl::IdlType::F64 }, vec![])) - } - syn::Type::Path(path) if the_only_segment_is(path, "u128") => { - Ok((quote! { #idl::IdlType::U128 }, vec![])) - } - syn::Type::Path(path) if the_only_segment_is(path, "i128") => { - Ok((quote! { #idl::IdlType::I128 }, vec![])) - } - syn::Type::Path(path) - if the_only_segment_is(path, "String") || the_only_segment_is(path, "&str") => - { - Ok((quote! { #idl::IdlType::String }, vec![])) - } - syn::Type::Path(path) if the_only_segment_is(path, "Pubkey") => { - Ok((quote! { #idl::IdlType::PublicKey }, vec![])) - } - syn::Type::Path(path) if the_only_segment_is(path, "Vec") => { - let segment = path.path.segments.first().unwrap(); - let arg = match get_first_angle_bracketed_path_arg(segment) { - Some(arg) => arg, - None => unreachable!("Vec arguments can only be of AngleBracketed variant"), - }; - match arg { - syn::Type::Path(path) if the_only_segment_is(path, "u8") => { - return Ok((quote! {#idl::IdlType::Bytes}, vec![])); - } - _ => (), - }; - let (inner, defined) = idl_type_ts_from_syn_type(arg, type_params)?; - Ok((quote! { #idl::IdlType::Vec(Box::new(#inner)) }, defined)) - } - syn::Type::Path(path) if the_only_segment_is(path, "Option") => { - let segment = path.path.segments.first().unwrap(); - let arg = match get_first_angle_bracketed_path_arg(segment) { - Some(arg) => arg, - None => unreachable!("Option arguments can only be of AngleBracketed variant"), - }; - let (inner, defined) = idl_type_ts_from_syn_type(arg, type_params)?; - Ok((quote! { #idl::IdlType::Option(Box::new(#inner)) }, defined)) - } - syn::Type::Path(path) if the_only_segment_is(path, "Box") => { - let segment = path.path.segments.first().unwrap(); - let arg = match get_first_angle_bracketed_path_arg(segment) { - Some(arg) => arg, - None => unreachable!("Box arguments can only be of AngleBracketed variant"), - }; - let (ts, defined) = idl_type_ts_from_syn_type(arg, type_params)?; - Ok((quote! { #ts }, defined)) - } - syn::Type::Array(arr) => { - let len = arr.len.clone(); - let len_is_generic = type_params.iter().any(|param| match len { - syn::Expr::Path(ref path) => path.path.is_ident(param), - _ => false, - }); - - let (inner, defined) = idl_type_ts_from_syn_type(&arr.elem, type_params)?; - - if len_is_generic { - match len { - syn::Expr::Path(ref len) => { - let len = len.path.get_ident().unwrap().to_string(); - Ok(( - quote! { #idl::IdlType::GenericLenArray(Box::new(#inner), #len.into()) }, - defined, - )) - } - _ => unreachable!("Array length can only be a generic parameter"), - } - } else { - Ok(( - quote! { #idl::IdlType::Array(Box::new(#inner), #len) }, - defined, - )) - } - } - syn::Type::Path(path) => { - let is_generic_param = type_params.iter().any(|param| path.path.is_ident(param)); - - if is_generic_param { - let generic = format!("{}", path.path.get_ident().unwrap()); - Ok((quote! { #idl::IdlType::Generic(#generic.into()) }, vec![])) - } else { - let mut params = vec![]; - let mut defined = vec![path.clone()]; - - if let Some(segment) = &path.path.segments.last() { - if let syn::PathArguments::AngleBracketed(ref args) = segment.arguments { - for arg in &args.args { - match arg { - syn::GenericArgument::Type(ty) => { - let (ts, def) = idl_type_ts_from_syn_type(ty, type_params)?; - params.push(quote! { #idl::IdlDefinedTypeArg::Type(#ts) }); - defined.extend(def); - } - syn::GenericArgument::Const(c) => params.push( - quote! { #idl::IdlDefinedTypeArg::Value(format!("{}", #c))}, - ), - _ => (), - } - } - } - } - - if !params.is_empty() { - let params = quote! { vec![#(#params),*] }; - Ok(( - quote! { #idl::IdlType::DefinedWithTypeArgs { - name: <#path>::__anchor_private_full_path(), - args: #params - } }, - defined, - )) - } else { - Ok(( - quote! { #idl::IdlType::Defined(<#path>::__anchor_private_full_path()) }, - vec![path.clone()], - )) - } - } - } - syn::Type::Reference(reference) => match reference.elem.as_ref() { - syn::Type::Slice(slice) if matches!(&*slice.elem, syn::Type::Path(path) if the_only_segment_is(path, "u8")) => - { - return Ok((quote! {#idl::IdlType::Bytes}, vec![])); - } - _ => panic!("Reference types other than byte slice(`&[u8]`) are not allowed"), - }, - _ => Err(()), - } -} - -// Returns TokenStream for IdlField struct and the syn::TypePath for the defined -// type if any. -// Returns Err when the type wasn't parsed successfully -#[allow(clippy::result_unit_err)] -pub fn idl_field_ts_from_syn_field( - field: &syn::Field, - no_docs: bool, - type_params: &Vec, -) -> Result<(TokenStream, Vec), ()> { - let (idl, _) = get_module_paths(); - - let name = field.ident.as_ref().unwrap().to_string().to_mixed_case(); - let docs = match docs::parse(&field.attrs) { - Some(docs) if !no_docs => quote! {Some(vec![#(#docs.into()),*])}, - _ => quote! {None}, - }; - let (ty, defined) = idl_type_ts_from_syn_type(&field.ty, type_params)?; - - Ok(( - quote! { - #idl::IdlField { - name: #name.into(), - docs: #docs, - ty: #ty, - } - }, - defined, - )) -} - -// Returns TokenStream for IdlEventField struct and the syn::TypePath for the defined -// type if any. -// Returns Err when the type wasn't parsed successfully -#[allow(clippy::result_unit_err)] -pub fn idl_event_field_ts_from_syn_field( - field: &syn::Field, -) -> Result<(TokenStream, Vec), ()> { - let (idl, _) = get_module_paths(); - - let name = field.ident.as_ref().unwrap().to_string().to_mixed_case(); - let (ty, defined) = idl_type_ts_from_syn_type(&field.ty, &vec![])?; - - let index: bool = field - .attrs - .get(0) - .and_then(|attr| attr.path.segments.first()) - .map(|segment| segment.ident == "index") - .unwrap_or(false); - - Ok(( - quote! { - #idl::IdlEventField { - name: #name.into(), - ty: #ty, - index: #index, - } - }, - defined, - )) -} - -// Returns TokenStream for IdlTypeDefinitionTy::Struct and Vec<&syn::TypePath> -// for the defined types if any. -// Returns Err if any of the fields weren't parsed successfully. -#[allow(clippy::result_unit_err)] -pub fn idl_type_definition_ts_from_syn_struct( - item_strct: &syn::ItemStruct, - no_docs: bool, -) -> Result<(TokenStream, Vec), ()> { - let (idl, _) = get_module_paths(); - - let docs = match docs::parse(&item_strct.attrs) { - Some(docs) if !no_docs => quote! {Some(vec![#(#docs.into()),*])}, - _ => quote! {None}, - }; - - let type_params = item_strct - .generics - .params - .iter() - .filter_map(|p| match p { - syn::GenericParam::Type(ty) => Some(ty.ident.clone()), - syn::GenericParam::Const(c) => Some(c.ident.clone()), - _ => None, - }) - .collect::>(); - let (fields, defined): (Vec, Vec>) = match &item_strct.fields { - syn::Fields::Named(fields) => fields - .named - .iter() - .map(|f: &syn::Field| idl_field_ts_from_syn_field(f, no_docs, &type_params)) - .collect::, _>>()? - .into_iter() - .unzip::<_, _, Vec<_>, Vec<_>>(), - _ => return Err(()), - }; - let defined = defined - .into_iter() - .flatten() - .collect::>(); - - let generics = if !type_params.is_empty() { - let g: Vec = type_params.iter().map(|id| id.to_string()).collect(); - quote! { Some(vec![#(#g.into()),*]) } - } else { - quote! { None } - }; - - Ok(( - quote! { - #idl::IdlTypeDefinition { - name: Self::__anchor_private_full_path(), - generics: #generics, - docs: #docs, - ty: #idl::IdlTypeDefinitionTy::Struct{ - fields: vec![ - #(#fields),* - ] - } - }, - }, - defined, - )) -} - -// Returns TokenStream for IdlTypeDefinitionTy::Enum and the Vec<&syn::TypePath> -// for the defined types if any. -// Returns Err if any of the fields didn't parse successfully. -#[allow(clippy::result_unit_err)] -pub fn idl_type_definition_ts_from_syn_enum( - enum_item: &syn::ItemEnum, - no_docs: bool, -) -> Result<(TokenStream, Vec), ()> { - let (idl, _) = get_module_paths(); - - let docs = match docs::parse(&enum_item.attrs) { - Some(docs) if !no_docs => quote! {Some(vec![#(#docs.into()),*])}, - _ => quote! {None}, - }; - - let type_params = enum_item - .generics - .params - .iter() - .filter_map(|p| match p { - syn::GenericParam::Type(ty) => Some(ty.ident.clone()), - syn::GenericParam::Const(c) => Some(c.ident.clone()), - _ => None, - }) - .collect::>(); - - let (variants, defined): (Vec, Vec>) = enum_item.variants.iter().map(|variant: &syn::Variant| { - let name = variant.ident.to_string(); - let (fields, defined): (TokenStream, Vec) = match &variant.fields { - syn::Fields::Unit => (quote!{None}, vec![]), - syn::Fields::Unnamed(fields) => { - let (types, defined) = fields.unnamed - .iter() - .map(|f| idl_type_ts_from_syn_type(&f.ty, &type_params)) - .collect::, _>>()? - .into_iter() - .unzip::, Vec, Vec>>(); - let defined = defined - .into_iter() - .flatten() - .collect::>(); - - (quote!{ Some(#idl::EnumFields::Tuple(vec![#(#types),*]))}, defined) - } - syn::Fields::Named(fields) => { - let (fields, defined) = fields.named - .iter() - .map(|f| idl_field_ts_from_syn_field(f, no_docs, &type_params)) - .collect::, _>>()? - .into_iter() - .unzip::, Vec, Vec>>(); - let defined = defined - .into_iter() - .flatten() - .collect::>(); - - (quote!{ Some(#idl::EnumFields::Named(vec![#(#fields),*]))}, defined) - } - }; - - Ok((quote!{ #idl::IdlEnumVariant{ name: #name.into(), fields: #fields }}, defined)) - }) - .collect::, _>>()? - .into_iter() - .unzip::, Vec, Vec>>(); - - let defined = defined - .into_iter() - .flatten() - .collect::>(); - - let generics = if !type_params.is_empty() { - let g: Vec = type_params.iter().map(|id| id.to_string()).collect(); - quote! { Some(vec![#(#g.into()),*]) } - } else { - quote! { None } - }; - - Ok(( - quote! { - #idl::IdlTypeDefinition { - name: Self::__anchor_private_full_path(), - generics: #generics, - docs: #docs, - ty: #idl::IdlTypeDefinitionTy::Enum{ - variants: vec![ - #(#variants),* - ] - } - } - }, - defined, - )) -} - -pub fn idl_build_impl_skeleton( - idl_type_definition_ts: TokenStream, - insert_defined_ts: TokenStream, - ident: &Ident, - input_generics: &syn::Generics, -) -> TokenStream { - let (idl, _) = get_module_paths(); - let name = ident.to_string(); - let (impl_generics, ty_generics, where_clause) = input_generics.split_for_impl(); - let idl_build_trait = quote! {anchor_lang::anchor_syn::idl::build::IdlBuild}; - - quote! { - impl #impl_generics #idl_build_trait for #ident #ty_generics #where_clause { - fn __anchor_private_full_path() -> String { - format!("{}::{}", std::module_path!(), #name) - } - - fn __anchor_private_gen_idl_type() -> Option<#idl::IdlTypeDefinition> { - #idl_type_definition_ts - } - - fn __anchor_private_insert_idl_defined( - defined_types: &mut std::collections::HashMap - ) { - #insert_defined_ts - } - } - } -} - -// generates the IDL generation impl for for a struct -pub fn gen_idl_build_impl_for_struct(strct: &ItemStruct, no_docs: bool) -> TokenStream { - let idl_type_definition_ts: TokenStream; - let insert_defined_ts: TokenStream; - - if let Ok((ts, defined)) = idl_type_definition_ts_from_syn_struct(strct, no_docs) { - idl_type_definition_ts = quote! {Some(#ts)}; - insert_defined_ts = quote! { - #({ - <#defined>::__anchor_private_insert_idl_defined(defined_types); - - let path = <#defined>::__anchor_private_full_path(); - <#defined>::__anchor_private_gen_idl_type() - .and_then(|ty| defined_types.insert(path, ty)); - });* - }; - } else { - idl_type_definition_ts = quote! {None}; - insert_defined_ts = quote! {}; - } - - let ident = &strct.ident; - let input_generics = &strct.generics; - - idl_build_impl_skeleton( - idl_type_definition_ts, - insert_defined_ts, - ident, - input_generics, - ) -} - -// generates the IDL generation impl for for an enum -pub fn gen_idl_build_impl_for_enum(enm: ItemEnum, no_docs: bool) -> TokenStream { - let idl_type_definition_ts: TokenStream; - let insert_defined_ts: TokenStream; - - if let Ok((ts, defined)) = idl_type_definition_ts_from_syn_enum(&enm, no_docs) { - idl_type_definition_ts = quote! {Some(#ts)}; - insert_defined_ts = quote! { - #({ - <#defined>::__anchor_private_insert_idl_defined(defined_types); - - let path = <#defined>::__anchor_private_full_path(); - <#defined>::__anchor_private_gen_idl_type() - .and_then(|ty| defined_types.insert(path, ty)); - });* - }; - } else { - idl_type_definition_ts = quote! {None}; - insert_defined_ts = quote! {}; - } - - let ident = &enm.ident; - let input_generics = &enm.generics; - - idl_build_impl_skeleton( - idl_type_definition_ts, - insert_defined_ts, - ident, - input_generics, - ) -} - -// generates the IDL generation impl for for an event -pub fn gen_idl_build_impl_for_event(event_strct: &ItemStruct) -> TokenStream { - fn parse_fields( - fields: &syn::FieldsNamed, - ) -> Result<(Vec, Vec), ()> { - let (fields, defined) = fields - .named - .iter() - .map(idl_event_field_ts_from_syn_field) - .collect::, _>>()? - .into_iter() - .unzip::<_, _, Vec<_>, Vec<_>>(); - let defined = defined - .into_iter() - .flatten() - .collect::>(); - - Ok((fields, defined)) - } - - let res = match &event_strct.fields { - syn::Fields::Named(fields) => parse_fields(fields), - _ => Err(()), - }; - - let (idl, _) = get_module_paths(); - let name = event_strct.ident.to_string(); - - let (ret_ts, types_ts) = match res { - Ok((fields, defined)) => { - let ret_ts = quote! { - Some( - #idl::IdlEvent { - name: #name.into(), - fields: vec![#(#fields),*], - } - ) - }; - let types_ts = quote! { - #({ - <#defined>::__anchor_private_insert_idl_defined(defined_types); - - let path = <#defined>::__anchor_private_full_path(); - <#defined>::__anchor_private_gen_idl_type() - .and_then(|ty| defined_types.insert(path, ty)); - });* - }; - (ret_ts, types_ts) - } - Err(()) => (quote! { None }, quote! {}), - }; - - let ident = &event_strct.ident; - let input_generics = &event_strct.generics; - let (impl_generics, ty_generics, where_clause) = input_generics.split_for_impl(); - - quote! { - impl #impl_generics #ident #ty_generics #where_clause { - pub fn __anchor_private_gen_idl_event( - defined_types: &mut std::collections::HashMap, - ) -> Option<#idl::IdlEvent> { - #types_ts - #ret_ts - } - } - } -} - -// generates the IDL generation impl for the Accounts struct -pub fn gen_idl_build_impl_for_accounts_struct( - accs_strct: &AccountsStruct, - no_docs: bool, -) -> TokenStream { - let (idl, _) = get_module_paths(); - - let ident = &accs_strct.ident; - let (impl_generics, ty_generics, where_clause) = accs_strct.generics.split_for_impl(); - - let (accounts, acc_types): (Vec, Vec>) = accs_strct - .fields - .iter() - .map(|acc: &AccountField| match acc { - AccountField::CompositeField(comp_f) => { - let ty = if let syn::Type::Path(path) = &comp_f.raw_field.ty { - // some::path::Foo<'info> -> some::path::Foo - let mut res = syn::Path { - leading_colon: path.path.leading_colon, - segments: syn::punctuated::Punctuated::new(), - }; - for segment in &path.path.segments { - let s = syn::PathSegment { - ident: segment.ident.clone(), - arguments: syn::PathArguments::None, - }; - res.segments.push(s); - }; - res - } else { - panic!("expecting path") - }; - let name = comp_f.ident.to_string().to_mixed_case(); - (quote!{ - #idl::IdlAccountItem::IdlAccounts(#idl::IdlAccounts { - name: #name.into(), - accounts: <#ty>::__anchor_private_gen_idl_accounts(accounts, defined_types), - }) - }, None) - } - AccountField::Field(acc) => { - let name = acc.ident.to_string().to_mixed_case(); - let is_mut = acc.constraints.is_mutable(); - let is_signer = match acc.ty { - crate::Ty::Signer => true, - _ => acc.constraints.is_signer() - }; - let is_optional = if acc.is_optional { quote!{Some(true)} } else { quote!{None} }; - let docs = match &acc.docs { - Some(docs) if !no_docs => quote! {Some(vec![#(#docs.into()),*])}, - _ => quote! {None}, - }; - let pda = quote!{None}; // TODO - let relations = super::parse::relations::parse(acc, get_seeds_feature()); - - let acc_type_path = match &acc.ty { - crate::Ty::Account(ty) => Some(&ty.account_type_path), - crate::Ty::AccountLoader(ty) => Some(&ty.account_type_path), - crate::Ty::InterfaceAccount(ty) => Some(&ty.account_type_path), - _ => None, - }; - - (quote!{ - #idl::IdlAccountItem::IdlAccount(#idl::IdlAccount{ - name: #name.into(), - is_mut: #is_mut, - is_signer: #is_signer, - is_optional: #is_optional, - docs: #docs, - pda: #pda, - relations: vec![#(#relations.into()),*], - }) - }, acc_type_path) - } - }) - .unzip::, Vec, Vec>>(); - let acc_types = acc_types - .into_iter() - .flatten() - .collect::>(); - - quote! { - impl #impl_generics #ident #ty_generics #where_clause { - pub fn __anchor_private_gen_idl_accounts( - accounts: &mut std::collections::HashMap, - defined_types: &mut std::collections::HashMap, - ) -> Vec<#idl::IdlAccountItem> { - #({ - <#acc_types>::__anchor_private_insert_idl_defined(defined_types); - - let path = <#acc_types>::__anchor_private_full_path(); - <#acc_types>::__anchor_private_gen_idl_type() - .and_then(|ty| accounts.insert(path, ty)); - - });* - - vec![#(#accounts),*] - } - } - } -} - -// generates the IDL generation print function for the program module -pub fn gen_idl_print_function_for_program(program: &Program, no_docs: bool) -> TokenStream { - let (idl, serde_json) = get_module_paths(); - - let (instructions, defined) = program - .ixs - .iter() - .flat_map(|ix| -> Result<_, ()> { - let name = ix.ident.to_string().to_mixed_case(); - let docs = match &ix.docs { - Some(docs) if !no_docs => quote! {Some(vec![#(#docs.into()),*])}, - _ => quote! {None}, - }; - let ctx_ident = &ix.anchor_ident; - - let (args, mut defined) = ix - .args - .iter() - .map(|arg| { - let arg_name = arg.name.to_string().to_mixed_case(); - let docs = match docs::parse(&arg.raw_arg.attrs) { - Some(docs) if !no_docs => quote! {Some(vec![#(#docs.into()),*])}, - _ => quote! {None}, - }; - let (ty, defined) = idl_type_ts_from_syn_type(&arg.raw_arg.ty, &vec![])?; - - Ok((quote! { - #idl::IdlField { - name: #arg_name.into(), - docs: #docs, - ty: #ty, - } - }, defined)) - }) - .collect::, ()>>()? - .into_iter() - .unzip::, Vec, Vec>>(); - - let returns = match idl_type_ts_from_syn_type(&ix.returns.ty, &vec![]) { - Ok((ty, def)) => { - defined.push(def); - quote!{ Some(#ty) } - }, - Err(()) => quote!{ None } - }; - - Ok((quote! { - #idl::IdlInstruction { - name: #name.into(), - docs: #docs, - accounts: #ctx_ident::__anchor_private_gen_idl_accounts( - &mut accounts, - &mut defined_types, - ), - args: vec![#(#args),*], - returns: #returns, - } - }, defined)) - }) - .unzip::>, Vec, Vec>>>(); - let defined = defined - .into_iter() - .flatten() - .flatten() - .collect::>(); - - let name = program.name.to_string(); - let docs = match &program.docs { - Some(docs) if !no_docs => quote! {Some(vec![#(#docs.into()),*])}, - _ => quote! {None}, - }; - - quote! { - #[test] - pub fn __anchor_private_print_idl_program() { - let mut accounts: std::collections::HashMap = - std::collections::HashMap::new(); - let mut defined_types: std::collections::HashMap = - std::collections::HashMap::new(); - - #({ - <#defined>::__anchor_private_insert_idl_defined(&mut defined_types); - - let path = <#defined>::__anchor_private_full_path(); - <#defined>::__anchor_private_gen_idl_type() - .and_then(|ty| defined_types.insert(path, ty)); - });* - - let instructions = vec![#(#instructions),*]; - - let idl = #idl::Idl { - version: env!("CARGO_PKG_VERSION").into(), - name: #name.into(), - docs: #docs, - constants: vec![], - instructions, - accounts: accounts.into_values().collect(), - types: defined_types.into_values().collect(), - events: None, - errors: None, - metadata: None, - }; - - println!("---- IDL begin program ----"); - println!("{}", #serde_json::to_string_pretty(&idl).unwrap()); - println!("---- IDL end program ----"); - } - } -} - -pub fn gen_idl_print_function_for_event(event: &ItemStruct) -> TokenStream { - let (idl, serde_json) = get_module_paths(); - - let ident = &event.ident; - let fn_name = format_ident!("__anchor_private_print_idl_event_{}", ident.to_string()); - let impl_gen = gen_idl_build_impl_for_event(event); - - quote! { - #impl_gen - - #[test] - pub fn #fn_name() { - let mut defined_types: std::collections::HashMap = std::collections::HashMap::new(); - let event = #ident::__anchor_private_gen_idl_event(&mut defined_types); - - if let Some(event) = event { - let json = #serde_json::json!({ - "event": event, - "defined_types": defined_types.into_values().collect::>() - }); - - println!("---- IDL begin event ----"); - println!("{}", #serde_json::to_string_pretty(&json).unwrap()); - println!("---- IDL end event ----"); - } - } - } -} - -pub fn gen_idl_print_function_for_constant(item: &syn::ItemConst) -> TokenStream { - let fn_name = format_ident!( - "__anchor_private_print_idl_const_{}", - item.ident.to_string() - ); - let (idl, serde_json) = get_module_paths(); - - let name = item.ident.to_string(); - let expr = &item.expr; - - let impl_ts = match idl_type_ts_from_syn_type(&item.ty, &vec![]) { - Ok((ty, _)) => quote! { - let value = format!("{:?}", #expr); - - let idl = #idl::IdlConst { - name: #name.into(), - ty: #ty, - value, - }; - - println!("---- IDL begin const ----"); - println!("{}", #serde_json::to_string_pretty(&idl).unwrap()); - println!("---- IDL end const ----"); - }, - Err(()) => quote! {}, - }; - - quote! { - #[test] - pub fn #fn_name() { - #impl_ts - } - } -} - -pub fn gen_idl_print_function_for_error(error: &Error) -> TokenStream { - let fn_name = format_ident!( - "__anchor_private_print_idl_error_{}", - error.ident.to_string() - ); - let (idl, serde_json) = get_module_paths(); - - let error_codes = error - .codes - .iter() - .map(|code| { - let id = code.id; - let name = code.ident.to_string(); - - let msg = match code.msg.clone() { - Some(msg) => quote! { Some(#msg.to_string()) }, - None => quote! { None }, - }; - - quote! { - #idl::IdlErrorCode { - code: anchor_lang::error::ERROR_CODE_OFFSET + #id, - name: #name.into(), - msg: #msg, - } - } - }) - .collect::>(); - - quote! { - #[test] - pub fn #fn_name() { - let errors = vec![#(#error_codes),*]; - - println!("---- IDL begin errors ----"); - println!("{}", #serde_json::to_string_pretty(&errors).unwrap()); - println!("---- IDL end errors ----"); - } - } -} diff --git a/lang/syn/src/idl/build/accounts.rs b/lang/syn/src/idl/build/accounts.rs new file mode 100644 index 0000000000..a282fbafe1 --- /dev/null +++ b/lang/syn/src/idl/build/accounts.rs @@ -0,0 +1,392 @@ +use anyhow::{anyhow, Result}; +use proc_macro2::TokenStream; +use quote::{quote, ToTokens}; + +use super::common::{get_idl_module_path, get_no_docs}; +use crate::{AccountField, AccountsStruct, Field, Ty}; + +/// Generate the IDL build impl for the Accounts struct. +pub fn gen_idl_build_impl_accounts_struct(accounts: &AccountsStruct) -> TokenStream { + let resolution = option_env!("ANCHOR_IDL_BUILD_RESOLUTION") + .map(|val| val == "TRUE") + .unwrap_or_default(); + let no_docs = get_no_docs(); + let idl = get_idl_module_path(); + + let ident = &accounts.ident; + let (impl_generics, ty_generics, where_clause) = accounts.generics.split_for_impl(); + + let (accounts, defined) = accounts + .fields + .iter() + .map(|acc| match acc { + AccountField::Field(acc) => { + let name = acc.ident.to_string(); + let writable = acc.constraints.is_mutable(); + let signer = match acc.ty { + Ty::Signer => true, + _ => acc.constraints.is_signer(), + }; + let optional = acc.is_optional; + let docs = match &acc.docs { + Some(docs) if !no_docs => quote! { vec![#(#docs.into()),*] }, + _ => quote! { vec![] }, + }; + + let (address, pda, relations) = if resolution { + ( + get_address(acc), + get_pda(acc, accounts), + get_relations(acc, accounts), + ) + } else { + (quote! { None }, quote! { None }, quote! { vec![] }) + }; + + let acc_type_path = match &acc.ty { + Ty::Account(ty) + // Skip `UpgradeableLoaderState` type for now until `bincode` serialization + // is supported. + // + // TODO: Remove this once either `bincode` serialization is supported or + // we wrap the type in order to implement `IdlBuild` in `anchor-lang`. + if !ty + .account_type_path + .path + .to_token_stream() + .to_string() + .contains("UpgradeableLoaderState") => + { + Some(&ty.account_type_path) + } + Ty::AccountLoader(ty) => Some(&ty.account_type_path), + Ty::InterfaceAccount(ty) => Some(&ty.account_type_path), + _ => None, + }; + + ( + quote! { + #idl::IdlInstructionAccountItem::Single(#idl::IdlInstructionAccount { + name: #name.into(), + docs: #docs, + writable: #writable, + signer: #signer, + optional: #optional, + address: #address, + pda: #pda, + relations: #relations, + }) + }, + acc_type_path, + ) + } + AccountField::CompositeField(comp_f) => { + let ty = if let syn::Type::Path(path) = &comp_f.raw_field.ty { + // some::path::Foo<'info> -> some::path::Foo + let mut res = syn::Path { + leading_colon: path.path.leading_colon, + segments: syn::punctuated::Punctuated::new(), + }; + for segment in &path.path.segments { + let s = syn::PathSegment { + ident: segment.ident.clone(), + arguments: syn::PathArguments::None, + }; + res.segments.push(s); + } + res + } else { + panic!( + "Compose field type must be a path but received: {:?}", + comp_f.raw_field.ty + ) + }; + let name = comp_f.ident.to_string(); + + ( + quote! { + #idl::IdlInstructionAccountItem::Composite(#idl::IdlInstructionAccounts { + name: #name.into(), + accounts: <#ty>::__anchor_private_gen_idl_accounts(accounts, types), + }) + }, + None, + ) + } + }) + .unzip::<_, _, Vec<_>, Vec<_>>(); + let defined = defined.into_iter().flatten().collect::>(); + + quote! { + impl #impl_generics #ident #ty_generics #where_clause { + pub fn __anchor_private_gen_idl_accounts( + accounts: &mut std::collections::BTreeMap, + types: &mut std::collections::BTreeMap, + ) -> Vec<#idl::IdlInstructionAccountItem> { + #( + if let Some(ty) = <#defined>::create_type() { + let account = #idl::IdlAccount { + name: ty.name.clone(), + discriminator: <#defined as anchor_lang::Discriminator>::DISCRIMINATOR.into(), + }; + accounts.insert(account.name.clone(), account); + types.insert(ty.name.clone(), ty); + <#defined>::insert_types(types); + } + );* + + vec![#(#accounts),*] + } + } + } +} + +fn get_address(acc: &Field) -> TokenStream { + match &acc.ty { + Ty::Program(ty) => ty + .account_type_path + .path + .segments + .last() + .map(|seg| &seg.ident) + .map(|ident| quote! { Some(#ident::id().to_string()) }) + .unwrap_or_else(|| quote! { None }), + Ty::Sysvar(_) => { + let ty = acc.account_ty(); + let sysvar_id_trait = quote!(anchor_lang::solana_program::sysvar::SysvarId); + quote! { Some(<#ty as #sysvar_id_trait>::id().to_string()) } + } + _ => acc + .constraints + .address + .as_ref() + .map(|constraint| &constraint.address) + .map(|address| quote! { Some(#address.to_string()) }) + .unwrap_or_else(|| quote! { None }), + } +} + +fn get_pda(acc: &Field, accounts: &AccountsStruct) -> TokenStream { + let idl = get_idl_module_path(); + let seed_constraints = acc.constraints.seeds.as_ref(); + let seeds = seed_constraints + .map(|seed| seed.seeds.iter().map(|seed| parse_seed(seed, accounts))) + .and_then(|seeds| seeds.collect::>>().ok()); + let program = seed_constraints + .and_then(|seed| seed.program_seed.as_ref()) + .and_then(|program| parse_seed(program, accounts).ok()) + .map(|program| quote! { Some(#program) }) + .unwrap_or_else(|| quote! { None }); + match seeds { + Some(seeds) => quote! { + Some( + #idl::IdlPda { + seeds: vec![#(#seeds),*], + program: #program, + } + ) + }, + _ => quote! { None }, + } +} + +/// Parse a seeds constraint, extracting the `IdlSeed` types. +/// +/// Note: This implementation makes assumptions about the types that can be used (e.g., no +/// program-defined function calls in seeds). +/// +/// This probably doesn't cover all cases. If you see a warning log, you can add a new case here. +/// In the worst case, we miss a seed and the parser will treat the given seeds as empty and so +/// clients will simply fail to automatically populate the PDA accounts. +/// +/// # Seed assumptions +/// +/// Seeds must be of one of the following forms: +/// +/// - Constant +/// - Instruction argument +/// - Account key or field +fn parse_seed(seed: &syn::Expr, accounts: &AccountsStruct) -> Result { + let idl = get_idl_module_path(); + let args = accounts.instruction_args().unwrap_or_default(); + match seed { + syn::Expr::MethodCall(_) => { + let seed_path = SeedPath::new(seed)?; + + if args.contains_key(&seed_path.name) { + let path = seed_path.path(); + + Ok(quote! { + #idl::IdlSeed::Arg( + #idl::IdlSeedArg { + path: #path.into(), + } + ) + }) + } else if let Some(account_field) = accounts + .fields + .iter() + .find(|field| *field.ident() == seed_path.name) + { + let path = seed_path.path(); + let account = match account_field.ty_name() { + Some(name) if !seed_path.subfields.is_empty() => { + quote! { Some(#name.into()) } + } + _ => quote! { None }, + }; + + Ok(quote! { + #idl::IdlSeed::Account( + #idl::IdlSeedAccount { + path: #path.into(), + account: #account, + } + ) + }) + } else if seed_path.name.contains('"') { + let seed = seed_path.name.trim_start_matches("b\"").trim_matches('"'); + Ok(quote! { + #idl::IdlSeed::Const( + #idl::IdlSeedConst { + value: #seed.into(), + } + ) + }) + } else { + Ok(quote! { + #idl::IdlSeed::Const( + #idl::IdlSeedConst { + value: #seed.into(), + } + ) + }) + } + } + syn::Expr::Path(path) => { + let seed = path + .path + .get_ident() + .map(|ident| ident.to_string()) + .filter(|ident| args.contains_key(ident)) + .map(|path| { + quote! { + #idl::IdlSeed::Arg( + #idl::IdlSeedArg { + path: #path.into(), + } + ) + } + }) + .unwrap_or_else(|| { + // Not all types can be converted to `Vec` with `.into` call e.g. `Pubkey`. + // This is problematic for `seeds::program` but a hacky way to handle this + // scenerio is to check whether the last segment of the path ends with `ID`. + let seed = path + .path + .segments + .last() + .filter(|seg| seg.ident.to_string().ends_with("ID")) + .map(|_| quote! { #seed.as_ref() }) + .unwrap_or_else(|| quote! { #seed }); + quote! { + #idl::IdlSeed::Const( + #idl::IdlSeedConst { + value: #seed.into(), + } + ) + } + }); + Ok(seed) + } + syn::Expr::Lit(_) => Ok(quote! { + #idl::IdlSeed::Const( + #idl::IdlSeedConst { + value: #seed.into(), + } + ) + }), + syn::Expr::Reference(rf) => parse_seed(&rf.expr, accounts), + _ => Err(anyhow!("Unexpected seed: {seed:?}")), + } +} + +/// SeedPath represents the deconstructed syntax of a single pda seed, +/// consisting of a variable name and a vec of all the sub fields accessed +/// on that variable name. For example, if a seed is `my_field.my_data.as_ref()`, +/// then the field name is `my_field` and the vec of sub fields is `[my_data]`. +struct SeedPath { + /// Seed name + name: String, + /// All path components for the subfields accessed on this seed + subfields: Vec, +} + +impl SeedPath { + /// Extract the seed path from a single seed expression. + fn new(seed: &syn::Expr) -> Result { + // Convert the seed into the raw string representation. + let seed_str = seed.to_token_stream().to_string(); + + // Break up the seed into each subfield component. + let mut components = seed_str.split('.').collect::>(); + if components.len() <= 1 { + return Err(anyhow!("Seed is in unexpected format: {seed:#?}")); + } + + // The name of the variable (or field). + let name = components.remove(0).to_owned(); + + // The path to the seed (only if the `name` type is a struct). + let mut path = Vec::new(); + while !components.is_empty() { + let subfield = components.remove(0); + if subfield.contains("()") { + break; + } + path.push(subfield.into()); + } + if path.len() == 1 && (path[0] == "key" || path[0] == "key()") { + path = Vec::new(); + } + + Ok(SeedPath { + name, + subfields: path, + }) + } + + /// Get the full path to the data this seed represents. + fn path(&self) -> String { + match self.subfields.len() { + 0 => self.name.to_owned(), + _ => format!("{}.{}", self.name, self.subfields.join(".")), + } + } +} + +fn get_relations(acc: &Field, accounts: &AccountsStruct) -> TokenStream { + let relations = accounts + .fields + .iter() + .filter_map(|af| match af { + AccountField::Field(f) => f + .constraints + .has_one + .iter() + .filter_map(|c| match &c.join_target { + syn::Expr::Path(path) => path + .path + .segments + .first() + .filter(|seg| seg.ident == acc.ident) + .map(|_| Some(f.ident.to_string())), + _ => None, + }) + .collect::>>(), + _ => None, + }) + .flatten() + .collect::>(); + quote! { vec![#(#relations.into()),*] } +} diff --git a/lang/syn/src/idl/build/address.rs b/lang/syn/src/idl/build/address.rs new file mode 100644 index 0000000000..092647a1fe --- /dev/null +++ b/lang/syn/src/idl/build/address.rs @@ -0,0 +1,15 @@ +use proc_macro2::TokenStream; +use quote::quote; + +use super::common::gen_print_section; + +pub fn gen_idl_print_fn_address(address: String) -> TokenStream { + let fn_body = gen_print_section("address", quote! { #address }); + + quote! { + #[test] + pub fn __anchor_private_print_idl_address() { + #fn_body + } + } +} diff --git a/lang/syn/src/idl/build/common.rs b/lang/syn/src/idl/build/common.rs new file mode 100644 index 0000000000..e3de92ed39 --- /dev/null +++ b/lang/syn/src/idl/build/common.rs @@ -0,0 +1,43 @@ +use std::{ + fs, + path::{Path, PathBuf}, +}; + +use anyhow::Result; +use proc_macro2::TokenStream; +use quote::{quote, ToTokens}; + +pub fn find_path(name: &str, path: impl AsRef) -> Result { + let parent_path = path.as_ref().parent().unwrap(); + for entry in fs::read_dir(parent_path)? { + let entry = entry?; + if entry.file_name().to_string_lossy() == name { + return entry.path().canonicalize().map_err(Into::into); + } + } + + find_path(name, parent_path) +} + +pub fn get_no_docs() -> bool { + option_env!("ANCHOR_IDL_BUILD_NO_DOCS") + .map(|val| val == "TRUE") + .unwrap_or_default() +} + +pub fn get_idl_module_path() -> TokenStream { + quote!(anchor_lang::anchor_syn::idl::types) +} + +pub fn get_serde_json_module_path() -> TokenStream { + quote!(anchor_lang::anchor_syn::idl::build::serde_json) +} + +pub fn gen_print_section(name: &str, value: impl ToTokens) -> TokenStream { + let serde_json = get_serde_json_module_path(); + quote! { + println!("--- IDL begin {} ---", #name); + println!("{}", #serde_json::to_string_pretty(&{ #value }).unwrap()); + println!("--- IDL end {} ---", #name); + } +} diff --git a/lang/syn/src/idl/build/constant.rs b/lang/syn/src/idl/build/constant.rs new file mode 100644 index 0000000000..b2a1b21fb8 --- /dev/null +++ b/lang/syn/src/idl/build/constant.rs @@ -0,0 +1,37 @@ +use heck::SnakeCase; +use proc_macro2::TokenStream; +use quote::{format_ident, quote}; + +use super::{ + common::{gen_print_section, get_idl_module_path}, + defined::gen_idl_type, +}; + +pub fn gen_idl_print_fn_constant(item: &syn::ItemConst) -> TokenStream { + let idl = get_idl_module_path(); + + let name = item.ident.to_string(); + let expr = &item.expr; + let fn_name = format_ident!("__anchor_private_print_idl_const_{}", name.to_snake_case()); + + let fn_body = match gen_idl_type(&item.ty, &[]) { + Ok((ty, _)) => gen_print_section( + "const", + quote! { + #idl::IdlConst { + name: #name.into(), + ty: #ty, + value: format!("{:?}", #expr), + } + }, + ), + _ => quote! {}, + }; + + quote! { + #[test] + pub fn #fn_name() { + #fn_body + } + } +} diff --git a/lang/syn/src/idl/build/defined.rs b/lang/syn/src/idl/build/defined.rs new file mode 100644 index 0000000000..0bdf3e9134 --- /dev/null +++ b/lang/syn/src/idl/build/defined.rs @@ -0,0 +1,654 @@ +use std::collections::BTreeMap; + +use anyhow::{anyhow, Result}; +use proc_macro2::TokenStream; +use quote::quote; + +use super::common::{get_idl_module_path, get_no_docs}; +use crate::{idl::types::IdlTypeDef, parser::docs}; + +/// A trait that types must implement in order to include the type in the IDL definition. +/// +/// This trait is automatically implemented for Anchor all types that use the `AnchorSerialize` +/// proc macro. Note that manually implementing the `AnchorSerialize` trait does **NOT** have the +/// same effect. +/// +/// Types that don't implement this trait will cause a compile error during the IDL generation. +/// +/// The default implementation of the trait allows the program to compile but the type does **NOT** +/// get included in the IDL. +pub trait IdlBuild { + /// Create an IDL type definition for the type. + /// + /// The type is only included in the IDL if this method returns `Some`. + fn create_type() -> Option { + None + } + + /// Insert all types that are included in the current type definition to the given map. + fn insert_types(_types: &mut BTreeMap) {} + + /// Get the full module path of the type. + /// + /// The full path will be used in the case of a conflicting type definition, e.g. when there + /// are multiple structs with the same name. + /// + /// The default implementation covers most cases. + fn get_full_path() -> String { + std::any::type_name::().into() + } +} + +/// Generate [`IdlBuild`] impl for a struct. +pub fn impl_idl_build_struct(item: &syn::ItemStruct) -> TokenStream { + impl_idl_build(&item.ident, &item.generics, gen_idl_type_def_struct(item)) +} + +/// Generate [`IdlBuild`] impl for an enum. +pub fn impl_idl_build_enum(item: &syn::ItemEnum) -> TokenStream { + impl_idl_build(&item.ident, &item.generics, gen_idl_type_def_enum(item)) +} + +/// Generate [`IdlBuild`] impl for a union. +/// +/// Unions are not currently supported in the IDL. +pub fn impl_idl_build_union(item: &syn::ItemUnion) -> TokenStream { + impl_idl_build( + &item.ident, + &item.generics, + Err(anyhow!("Unions are not supported")), + ) +} + +/// Generate [`IdlBuild`] implementation. +fn impl_idl_build( + ident: &syn::Ident, + generics: &syn::Generics, + type_def: Result<(TokenStream, Vec)>, +) -> TokenStream { + let idl = get_idl_module_path(); + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + let idl_build_trait = quote!(anchor_lang::anchor_syn::idl::build::IdlBuild); + + let (idl_type_def, insert_defined) = match type_def { + Ok((ts, defined)) => ( + quote! { Some(#ts) }, + quote! { + #( + if let Some(ty) = <#defined>::create_type() { + types.insert(<#defined>::get_full_path(), ty); + <#defined>::insert_types(types); + } + );* + }, + ), + _ => (quote! { None }, quote! {}), + }; + + quote! { + impl #impl_generics #idl_build_trait for #ident #ty_generics #where_clause { + fn create_type() -> Option<#idl::IdlTypeDef> { + #idl_type_def + } + + fn insert_types( + types: &mut std::collections::BTreeMap + ) { + #insert_defined + } + + fn get_full_path() -> String { + format!("{}::{}", module_path!(), stringify!(#ident)) + } + } + } +} + +pub fn gen_idl_type_def_struct( + strct: &syn::ItemStruct, +) -> Result<(TokenStream, Vec)> { + gen_idl_type_def(&strct.attrs, &strct.generics, |generic_params| { + let no_docs = get_no_docs(); + let idl = get_idl_module_path(); + + let (fields, defined) = match &strct.fields { + syn::Fields::Unit => (quote! { None }, vec![]), + syn::Fields::Named(fields) => { + let (fields, defined) = fields + .named + .iter() + .map(|f| gen_idl_field(f, generic_params, no_docs)) + .collect::>>()? + .into_iter() + .unzip::<_, _, Vec<_>, Vec<_>>(); + + ( + quote! { Some(#idl::IdlDefinedFields::Named(vec![#(#fields),*])) }, + defined, + ) + } + syn::Fields::Unnamed(fields) => { + let (types, defined) = fields + .unnamed + .iter() + .map(|f| gen_idl_type(&f.ty, generic_params)) + .collect::>>()? + .into_iter() + .unzip::<_, Vec<_>, Vec<_>, Vec<_>>(); + + ( + quote! { Some(#idl::IdlDefinedFields::Tuple(vec![#(#types),*])) }, + defined, + ) + } + }; + let defined = defined.into_iter().flatten().collect::>(); + + Ok(( + quote! { + #idl::IdlTypeDefTy::Struct { + fields: #fields, + } + }, + defined, + )) + }) +} + +fn gen_idl_type_def_enum(enm: &syn::ItemEnum) -> Result<(TokenStream, Vec)> { + gen_idl_type_def(&enm.attrs, &enm.generics, |generic_params| { + let no_docs = get_no_docs(); + let idl = get_idl_module_path(); + + let (variants, defined) = enm + .variants + .iter() + .map(|variant| { + let name = variant.ident.to_string(); + let (fields, defined) = match &variant.fields { + syn::Fields::Unit => (quote! { None }, vec![]), + syn::Fields::Named(fields) => { + let (fields, defined) = fields + .named + .iter() + .map(|f| gen_idl_field(f, generic_params, no_docs)) + .collect::>>()? + .into_iter() + .unzip::<_, Vec<_>, Vec<_>, Vec<_>>(); + let defined = defined.into_iter().flatten().collect::>(); + + ( + quote! { Some(#idl::IdlDefinedFields::Named(vec![#(#fields),*])) }, + defined, + ) + } + syn::Fields::Unnamed(fields) => { + let (types, defined) = fields + .unnamed + .iter() + .map(|f| gen_idl_type(&f.ty, generic_params)) + .collect::>>()? + .into_iter() + .unzip::<_, Vec<_>, Vec<_>, Vec<_>>(); + let defined = defined.into_iter().flatten().collect::>(); + + ( + quote! { Some(#idl::IdlDefinedFields::Tuple(vec![#(#types),*])) }, + defined, + ) + } + }; + + Ok(( + quote! { #idl::IdlEnumVariant { name: #name.into(), fields: #fields } }, + defined, + )) + }) + .collect::>>()? + .into_iter() + .unzip::<_, _, Vec<_>, Vec<_>>(); + let defined = defined.into_iter().flatten().collect::>(); + + Ok(( + quote! { + #idl::IdlTypeDefTy::Enum { + variants: vec![#(#variants),*], + } + }, + defined, + )) + }) +} + +fn gen_idl_type_def( + attrs: &[syn::Attribute], + generics: &syn::Generics, + create_fields: F, +) -> Result<(TokenStream, Vec)> +where + F: Fn(&[syn::Ident]) -> Result<(TokenStream, Vec)>, +{ + let no_docs = get_no_docs(); + let idl = get_idl_module_path(); + + let docs = match docs::parse(attrs) { + Some(docs) if !no_docs => quote! { vec![#(#docs.into()),*] }, + _ => quote! { vec![] }, + }; + + let serialization = get_attr_str("derive", attrs) + .and_then(|derive| { + if derive.contains("bytemuck") { + if derive.to_lowercase().contains("unsafe") { + Some(quote! { #idl::IdlSerialization::BytemuckUnsafe }) + } else { + Some(quote! { #idl::IdlSerialization::Bytemuck }) + } + } else { + None + } + }) + .unwrap_or_else(|| quote! { #idl::IdlSerialization::default() }); + + let repr = get_attr_str("repr", attrs) + .map(|repr| { + let packed = repr.contains("packed"); + let align = repr + .find("align") + .and_then(|i| repr.get(i..)) + .and_then(|align| { + align + .find('(') + .and_then(|start| align.find(')').and_then(|end| align.get(start + 1..end))) + }) + .and_then(|size| size.parse::().ok()) + .map(|size| quote! { Some(#size) }) + .unwrap_or_else(|| quote! { None }); + let modifier = quote! { + #idl::IdlReprModifier { + packed: #packed, + align: #align, + } + }; + + if repr.contains("transparent") { + quote! { #idl::IdlRepr::Transparent } + } else if repr.contains('C') { + quote! { #idl::IdlRepr::C(#modifier) } + } else { + quote! { #idl::IdlRepr::Rust(#modifier) } + } + }) + .map(|repr| quote! { Some(#repr) }) + .unwrap_or_else(|| quote! { None }); + + let generic_params = generics + .params + .iter() + .filter_map(|p| match p { + syn::GenericParam::Type(ty) => Some(ty.ident.clone()), + syn::GenericParam::Const(c) => Some(c.ident.clone()), + _ => None, + }) + .collect::>(); + let (ty, defined) = create_fields(&generic_params)?; + + let generics = generics + .params + .iter() + .filter_map(|p| match p { + syn::GenericParam::Type(ty) => { + let name = ty.ident.to_string(); + Some(quote! { + #idl::IdlTypeDefGeneric::Type { + name: #name.into(), + } + }) + } + syn::GenericParam::Const(c) => { + let name = c.ident.to_string(); + let ty = match &c.ty { + syn::Type::Path(path) => get_first_segment(path).ident.to_string(), + _ => unreachable!("Const generic type can only be path"), + }; + Some(quote! { + #idl::IdlTypeDefGeneric::Const { + name: #name.into(), + ty: #ty.into(), + } + }) + } + _ => None, + }) + .collect::>(); + + Ok(( + quote! { + #idl::IdlTypeDef { + name: Self::get_full_path(), + docs: #docs, + serialization: #serialization, + repr: #repr, + generics: vec![#(#generics.into()),*], + ty: #ty, + } + }, + defined, + )) +} + +fn get_attr_str(name: impl AsRef, attrs: &[syn::Attribute]) -> Option { + attrs + .iter() + .filter(|attr| { + attr.path + .segments + .first() + .filter(|seg| seg.ident == name) + .is_some() + }) + .map(|attr| attr.tokens.to_string()) + .reduce(|acc, cur| { + format!( + "{} , {}", + acc.get(..acc.len() - 1).unwrap(), + cur.get(1..).unwrap() + ) + }) +} + +fn gen_idl_field( + field: &syn::Field, + generic_params: &[syn::Ident], + no_docs: bool, +) -> Result<(TokenStream, Vec)> { + let idl = get_idl_module_path(); + + let name = field.ident.as_ref().unwrap().to_string(); + let docs = match docs::parse(&field.attrs) { + Some(docs) if !no_docs => quote! { vec![#(#docs.into()),*] }, + _ => quote! { vec![] }, + }; + let (ty, defined) = gen_idl_type(&field.ty, generic_params)?; + + Ok(( + quote! { + #idl::IdlField { + name: #name.into(), + docs: #docs, + ty: #ty, + } + }, + defined, + )) +} + +pub fn gen_idl_type( + ty: &syn::Type, + generic_params: &[syn::Ident], +) -> Result<(TokenStream, Vec)> { + let idl = get_idl_module_path(); + + fn the_only_segment_is(path: &syn::TypePath, cmp: &str) -> bool { + if path.path.segments.len() != 1 { + return false; + }; + return get_first_segment(path).ident == cmp; + } + + fn get_angle_bracketed_type_args(seg: &syn::PathSegment) -> Vec<&syn::Type> { + match &seg.arguments { + syn::PathArguments::AngleBracketed(ab) => ab + .args + .iter() + .filter_map(|arg| match arg { + syn::GenericArgument::Type(ty) => Some(ty), + _ => None, + }) + .collect(), + _ => panic!("No angle bracket for {seg:#?}"), + } + } + + match ty { + syn::Type::Path(path) if the_only_segment_is(path, "bool") => { + Ok((quote! { #idl::IdlType::Bool }, vec![])) + } + syn::Type::Path(path) if the_only_segment_is(path, "u8") => { + Ok((quote! { #idl::IdlType::U8 }, vec![])) + } + syn::Type::Path(path) if the_only_segment_is(path, "i8") => { + Ok((quote! { #idl::IdlType::I8 }, vec![])) + } + syn::Type::Path(path) if the_only_segment_is(path, "u16") => { + Ok((quote! { #idl::IdlType::U16 }, vec![])) + } + syn::Type::Path(path) if the_only_segment_is(path, "i16") => { + Ok((quote! { #idl::IdlType::I16 }, vec![])) + } + syn::Type::Path(path) if the_only_segment_is(path, "u32") => { + Ok((quote! { #idl::IdlType::U32 }, vec![])) + } + syn::Type::Path(path) if the_only_segment_is(path, "i32") => { + Ok((quote! { #idl::IdlType::I32 }, vec![])) + } + syn::Type::Path(path) if the_only_segment_is(path, "f32") => { + Ok((quote! { #idl::IdlType::F32 }, vec![])) + } + syn::Type::Path(path) if the_only_segment_is(path, "u64") => { + Ok((quote! { #idl::IdlType::U64 }, vec![])) + } + syn::Type::Path(path) if the_only_segment_is(path, "i64") => { + Ok((quote! { #idl::IdlType::I64 }, vec![])) + } + syn::Type::Path(path) if the_only_segment_is(path, "f64") => { + Ok((quote! { #idl::IdlType::F64 }, vec![])) + } + syn::Type::Path(path) if the_only_segment_is(path, "u128") => { + Ok((quote! { #idl::IdlType::U128 }, vec![])) + } + syn::Type::Path(path) if the_only_segment_is(path, "i128") => { + Ok((quote! { #idl::IdlType::I128 }, vec![])) + } + syn::Type::Path(path) + if the_only_segment_is(path, "String") || the_only_segment_is(path, "str") => + { + Ok((quote! { #idl::IdlType::String }, vec![])) + } + syn::Type::Path(path) if the_only_segment_is(path, "Pubkey") => { + Ok((quote! { #idl::IdlType::Pubkey }, vec![])) + } + syn::Type::Path(path) if the_only_segment_is(path, "Option") => { + let segment = get_first_segment(path); + let arg = get_angle_bracketed_type_args(segment) + .into_iter() + .next() + .unwrap(); + let (inner, defined) = gen_idl_type(arg, generic_params)?; + Ok((quote! { #idl::IdlType::Option(Box::new(#inner)) }, defined)) + } + syn::Type::Path(path) if the_only_segment_is(path, "Vec") => { + let segment = get_first_segment(path); + let arg = get_angle_bracketed_type_args(segment) + .into_iter() + .next() + .unwrap(); + match arg { + syn::Type::Path(path) if the_only_segment_is(path, "u8") => { + return Ok((quote! {#idl::IdlType::Bytes}, vec![])); + } + _ => (), + }; + let (inner, defined) = gen_idl_type(arg, generic_params)?; + Ok((quote! { #idl::IdlType::Vec(Box::new(#inner)) }, defined)) + } + syn::Type::Path(path) if the_only_segment_is(path, "Box") => { + let segment = get_first_segment(path); + let arg = get_angle_bracketed_type_args(segment) + .into_iter() + .next() + .unwrap(); + gen_idl_type(arg, generic_params) + } + syn::Type::Array(arr) => { + let len = &arr.len; + let is_generic = generic_params.iter().any(|param| match len { + syn::Expr::Path(path) => path.path.is_ident(param), + _ => false, + }); + + let len = if is_generic { + match len { + syn::Expr::Path(len) => { + let len = len.path.get_ident().unwrap().to_string(); + quote! { #idl::IdlArrayLen::Generic(#len.into()) } + } + _ => unreachable!("Array length can only be a generic parameter"), + } + } else { + quote! { #idl::IdlArrayLen::Value(#len) } + }; + + let (inner, defined) = gen_idl_type(&arr.elem, generic_params)?; + Ok(( + quote! { #idl::IdlType::Array(Box::new(#inner), #len) }, + defined, + )) + } + // Defined + syn::Type::Path(path) => { + let is_generic_param = generic_params.iter().any(|param| path.path.is_ident(param)); + if is_generic_param { + let generic = get_first_segment(path).ident.to_string(); + return Ok((quote! { #idl::IdlType::Generic(#generic.into()) }, vec![])); + } + + // Handle type aliases and external types + #[cfg(procmacro2_semver_exempt)] + { + use super::{common::find_path, external::get_external_type}; + use crate::parser::context::CrateContext; + use quote::ToTokens; + + let source_path = proc_macro2::Span::call_site().source_file().path(); + let lib_path = find_path("lib.rs", &source_path).expect("lib.rs should exist"); + + if let Ok(ctx) = CrateContext::parse(lib_path) { + let name = path.path.segments.last().unwrap().ident.to_string(); + let alias = ctx.type_aliases().find(|ty| ty.ident == name); + if let Some(alias) = alias { + if let Some(segment) = path.path.segments.last() { + if let syn::PathArguments::AngleBracketed(args) = &segment.arguments { + let inners = args + .args + .iter() + .map(|arg| match arg { + syn::GenericArgument::Type(ty) => match ty { + syn::Type::Path(inner_ty) => { + inner_ty.path.to_token_stream().to_string() + } + _ => { + unimplemented!("Inner type not implemented: {ty:?}") + } + }, + syn::GenericArgument::Const(c) => { + c.to_token_stream().to_string() + } + _ => unimplemented!("Arg not implemented: {arg:?}"), + }) + .collect::>(); + + let outer = match &*alias.ty { + syn::Type::Path(outer_ty) => outer_ty.path.to_token_stream(), + syn::Type::Array(outer_ty) => outer_ty.to_token_stream(), + _ => unimplemented!("Type not implemented: {:?}", alias.ty), + } + .to_string(); + + let resolved_alias = alias + .generics + .params + .iter() + .map(|param| match param { + syn::GenericParam::Const(param) => param.ident.to_string(), + syn::GenericParam::Type(param) => param.ident.to_string(), + _ => panic!("Lifetime parameters are not allowed"), + }) + .enumerate() + .fold(outer, |acc, (i, cur)| { + let inner = &inners[i]; + acc.replace(&format!(" {cur}"), &format!(" {inner} ")) + .replace(&format!(" {cur},"), &format!(" {inner},")) + .replace(&format!("[{cur} "), &format!("[{inner} ",)) + .replace(&format!(" {cur}]"), &format!(" {inner}]")) + }); + if let Ok(ty) = syn::parse_str(&resolved_alias) { + return gen_idl_type(&ty, generic_params); + } + } + }; + + // Non-generic type alias e.g. `type UnixTimestamp = i64` + return gen_idl_type(&*alias.ty, generic_params); + } + + // Handle external types + let is_external = ctx + .structs() + .map(|s| s.ident.to_string()) + .chain(ctx.enums().map(|e| e.ident.to_string())) + .find(|defined| defined == &name) + .is_none(); + if is_external { + if let Ok(Some(ty)) = get_external_type(&name, source_path) { + return gen_idl_type(&ty, generic_params); + } + } + } + } + + // Defined in crate + let mut generics = vec![]; + let mut defined = vec![path.clone()]; + + if let Some(segment) = path.path.segments.last() { + if let syn::PathArguments::AngleBracketed(args) = &segment.arguments { + for arg in &args.args { + match arg { + syn::GenericArgument::Type(ty) => { + let (ty, def) = gen_idl_type(ty, generic_params)?; + generics.push(quote! { #idl::IdlGenericArg::Type { ty: #ty } }); + defined.extend(def); + } + syn::GenericArgument::Const(c) => generics.push( + quote! { #idl::IdlGenericArg::Const { value: #c.to_string() } }, + ), + _ => (), + } + } + } + } + + Ok(( + quote! { + #idl::IdlType::Defined { + name: <#path>::get_full_path(), + generics: vec![#(#generics),*], + } + }, + defined, + )) + } + syn::Type::Reference(reference) => match reference.elem.as_ref() { + syn::Type::Slice(slice) if matches!(&*slice.elem, syn::Type::Path(path) if the_only_segment_is(path, "u8")) => { + Ok((quote! {#idl::IdlType::Bytes}, vec![])) + } + _ => gen_idl_type(&reference.elem, generic_params), + }, + _ => Err(anyhow!("Unknown type: {ty:#?}")), + } +} + +fn get_first_segment(type_path: &syn::TypePath) -> &syn::PathSegment { + type_path.path.segments.first().unwrap() +} diff --git a/lang/syn/src/idl/build/error.rs b/lang/syn/src/idl/build/error.rs new file mode 100644 index 0000000000..dcff0f5e27 --- /dev/null +++ b/lang/syn/src/idl/build/error.rs @@ -0,0 +1,44 @@ +use heck::SnakeCase; +use proc_macro2::TokenStream; +use quote::{format_ident, quote}; + +use super::common::{gen_print_section, get_idl_module_path}; +use crate::Error; + +pub fn gen_idl_print_fn_error(error: &Error) -> TokenStream { + let idl = get_idl_module_path(); + + let fn_name = format_ident!( + "__anchor_private_print_idl_error_{}", + error.ident.to_string().to_snake_case() + ); + + let error_codes = error + .codes + .iter() + .map(|code| { + let id = code.id; + let name = code.ident.to_string(); + let msg = match &code.msg { + Some(msg) => quote! { Some(#msg.into()) }, + None => quote! { None }, + }; + + quote! { + #idl::IdlErrorCode { + code: anchor_lang::error::ERROR_CODE_OFFSET + #id, + name: #name.into(), + msg: #msg, + } + } + }) + .collect::>(); + let fn_body = gen_print_section("errors", quote! { vec![#(#error_codes),*] }); + + quote! { + #[test] + pub fn #fn_name() { + #fn_body + } + } +} diff --git a/lang/syn/src/idl/build/event.rs b/lang/syn/src/idl/build/event.rs new file mode 100644 index 0000000000..49d1ef7129 --- /dev/null +++ b/lang/syn/src/idl/build/event.rs @@ -0,0 +1,81 @@ +use heck::SnakeCase; +use proc_macro2::TokenStream; +use quote::{format_ident, quote}; + +use super::{ + common::{gen_print_section, get_idl_module_path, get_serde_json_module_path}, + defined::gen_idl_type_def_struct, +}; + +pub fn gen_idl_print_fn_event(event_struct: &syn::ItemStruct) -> TokenStream { + let idl = get_idl_module_path(); + let serde_json = get_serde_json_module_path(); + + let ident = &event_struct.ident; + let fn_name = format_ident!( + "__anchor_private_print_idl_event_{}", + ident.to_string().to_snake_case() + ); + let idl_build_impl = impl_idl_build_event(event_struct); + + let print_ts = gen_print_section( + "event", + quote! { + #serde_json::json!({ + "event": event, + "types": types.into_values().collect::>() + }) + }, + ); + + quote! { + #idl_build_impl + + #[test] + pub fn #fn_name() { + let mut types: std::collections::BTreeMap = + std::collections::BTreeMap::new(); + if let Some(event) = #ident::__anchor_private_gen_idl_event(&mut types) { + #print_ts + } + } + } +} + +/// Generate IDL build impl for an event. +fn impl_idl_build_event(event_struct: &syn::ItemStruct) -> TokenStream { + let idl = get_idl_module_path(); + + let ident = &event_struct.ident; + let (impl_generics, ty_generics, where_clause) = event_struct.generics.split_for_impl(); + + let fn_body = match gen_idl_type_def_struct(event_struct) { + Ok((ts, defined)) => quote! { + #( + if let Some(ty) = <#defined>::create_type() { + types.insert(<#defined>::get_full_path(), ty); + <#defined>::insert_types(types); + } + );* + + let ty = #ts; + let event = #idl::IdlEvent { + name: ty.name.clone(), + discriminator: ::DISCRIMINATOR.into(), + }; + types.insert(ty.name.clone(), ty); + Some(event) + }, + _ => quote! { None }, + }; + + quote! { + impl #impl_generics #ident #ty_generics #where_clause { + pub fn __anchor_private_gen_idl_event( + types: &mut std::collections::BTreeMap, + ) -> Option<#idl::IdlEvent> { + #fn_body + } + } + } +} diff --git a/lang/syn/src/idl/build/external.rs b/lang/syn/src/idl/build/external.rs new file mode 100644 index 0000000000..2b8b48f4c6 --- /dev/null +++ b/lang/syn/src/idl/build/external.rs @@ -0,0 +1,143 @@ +use std::{ + env, fs, + path::{Path, PathBuf}, +}; + +use anyhow::{anyhow, Result}; +use cargo_toml::Manifest; +use quote::ToTokens; + +use super::common::find_path; +use crate::parser::context::CrateContext; + +pub fn get_external_type(name: &str, path: impl AsRef) -> Result> { + let use_path = get_uses(path.as_ref())? + .into_iter() + .find(|u| u.split("::").last().unwrap() == name) + .ok_or_else(|| anyhow!("`{name}` not found in use statements"))?; + + // Get crate name and version from lock file + let lib_path = find_path("lib.rs", path)?; + let lock_path = find_path("Cargo.lock", lib_path)?; + let lock_file = parse_lock_file(lock_path)?; + let registry_path = get_registry_path()?; + + recursively_find_type(name, &use_path, ®istry_path, &lock_file) +} + +fn recursively_find_type( + defined_name: &str, + use_path: &str, + registry_path: &Path, + lock_file: &[(String, String)], +) -> Result> { + let crate_name = use_path.split("::").next().unwrap(); + let (crate_name, version) = lock_file + .iter() + .find(|(name, _)| name == crate_name || name == &crate_name.replace('_', "-")) + .ok_or_else(|| anyhow!("Crate should exist in the lock file"))?; + + let crate_path = registry_path.join(format!("{crate_name}-{version}")); + let lib_path = crate_path.join("src").join("lib.rs"); + let ctx = CrateContext::parse(&lib_path)?; + + // TODO: Struct and enum + + let alias = ctx.type_aliases().find(|item| item.ident == defined_name); + match alias { + Some(alias) => Ok(Some(*alias.ty.to_owned())), + None => { + // Check re-exported deps e.g. `anchor_lang::solana_program::...` + let cargo_toml_path = find_path("Cargo.toml", &lib_path)?; + let deps = Manifest::from_path(cargo_toml_path)?.dependencies; + let paths = use_path.split("::").skip(1).collect::>(); + let paths = paths.iter().enumerate().filter_map(|(i, path)| { + if deps.contains_key(*path) || deps.contains_key(&path.replace('_', "-")) { + Some(paths.iter().skip(i).copied().collect::>().join("::")) + } else { + None + } + }); + for path in paths { + let result = recursively_find_type(defined_name, &path, registry_path, lock_file); + if result.is_ok() { + return result; + } + } + + Ok(None) + } + } +} + +fn get_registry_path() -> Result { + #[allow(deprecated)] + let path = env::home_dir() + .unwrap() + .join(".cargo") + .join("registry") + .join("src"); + fs::read_dir(&path)? + .filter_map(|entry| entry.ok()) + .find_map(|entry| { + let file_name = entry.file_name(); + if file_name.to_string_lossy().starts_with("index.crates.io") { + Some(file_name) + } else { + None + } + }) + .map(|name| path.join(name)) + .ok_or_else(|| anyhow!("crates.io registry not found")) +} + +fn parse_lock_file(path: impl AsRef) -> Result> { + let parsed = fs::read_to_string(path.as_ref())? + .split("[[package]]") + .skip(1) + .map(|pkg| { + let get_value = |key: &str| -> String { + pkg.lines() + .find(|line| line.starts_with(key)) + .expect(&format!("`{key}` line not found")) + .split('"') + .nth(1) + .unwrap() + .to_owned() + }; + let name = get_value("name"); + let version = get_value("version"); + (name, version) + }) + .collect::>(); + Ok(parsed) +} + +fn get_uses(path: impl AsRef) -> Result> { + let content = fs::read_to_string(path.as_ref())?; + let uses = syn::parse_file(&content)? + .items + .into_iter() + .filter_map(|item| match item { + syn::Item::Use(u) => Some(flatten_uses(&u.tree)), + _ => None, + }) + .flatten() + .collect::>(); + Ok(uses) +} + +fn flatten_uses(tree: &syn::UseTree) -> Vec { + match tree { + syn::UseTree::Group(group) => group.items.iter().flat_map(flatten_uses).collect(), + syn::UseTree::Path(path) => flatten_uses(&path.tree) + .into_iter() + .map(|item| format!("{}::{}", path.ident, item)) + .collect(), + syn::UseTree::Glob(glob) => { + vec![format!("{}", glob.star_token.to_token_stream().to_string())] + } + syn::UseTree::Name(name) => vec![name.ident.to_string()], + syn::UseTree::Rename(rename) => vec![rename.ident.to_string()], + } +} diff --git a/lang/syn/src/idl/build/mod.rs b/lang/syn/src/idl/build/mod.rs new file mode 100644 index 0000000000..e2974da54e --- /dev/null +++ b/lang/syn/src/idl/build/mod.rs @@ -0,0 +1,21 @@ +#![allow(dead_code)] + +mod accounts; +mod address; +mod common; +mod constant; +mod defined; +mod error; +mod event; +mod external; +mod program; + +pub use accounts::gen_idl_build_impl_accounts_struct; +pub use address::gen_idl_print_fn_address; +pub use constant::gen_idl_print_fn_constant; +pub use defined::{impl_idl_build_enum, impl_idl_build_struct, impl_idl_build_union, IdlBuild}; +pub use error::gen_idl_print_fn_error; +pub use event::gen_idl_print_fn_event; +pub use program::gen_idl_print_fn_program; + +pub use serde_json; diff --git a/lang/syn/src/idl/build/program.rs b/lang/syn/src/idl/build/program.rs new file mode 100644 index 0000000000..6eb284cdc4 --- /dev/null +++ b/lang/syn/src/idl/build/program.rs @@ -0,0 +1,140 @@ +use anyhow::Result; +use heck::CamelCase; +use proc_macro2::TokenStream; +use quote::{format_ident, quote}; + +use super::{ + common::{gen_print_section, get_idl_module_path, get_no_docs}, + defined::gen_idl_type, +}; +use crate::{parser::docs, Program}; + +/// Generate the IDL build print function for the program module. +pub fn gen_idl_print_fn_program(program: &Program) -> TokenStream { + let idl = get_idl_module_path(); + let no_docs = get_no_docs(); + + let name = program.name.to_string(); + let docs = match &program.docs { + Some(docs) if !no_docs => quote! { vec![#(#docs.into()),*] }, + _ => quote! { vec![] }, + }; + + let (instructions, defined) = program + .ixs + .iter() + .flat_map(|ix| -> Result<_> { + let name = ix.ident.to_string(); + let name_pascal = format_ident!("{}", name.to_camel_case()); + let ctx_ident = &ix.anchor_ident; + let discriminator = quote! { + ::DISCRIMINATOR + }; + + let docs = match &ix.docs { + Some(docs) if !no_docs => quote! { vec![#(#docs.into()),*] }, + _ => quote! { vec![] }, + }; + + let (args, mut defined) = ix + .args + .iter() + .map(|arg| { + let name = arg.name.to_string(); + let docs = match docs::parse(&arg.raw_arg.attrs) { + Some(docs) if !no_docs => quote! { vec![#(#docs.into()),*] }, + _ => quote! { vec![] }, + }; + let (ty, defined) = gen_idl_type(&arg.raw_arg.ty, &[])?; + + Ok(( + quote! { + #idl::IdlField { + name: #name.into(), + docs: #docs, + ty: #ty, + } + }, + defined, + )) + }) + .collect::>>()? + .into_iter() + .unzip::<_, Vec<_>, Vec<_>, Vec<_>>(); + + let returns = match gen_idl_type(&ix.returns.ty, &[]) { + Ok((ty, def)) => { + defined.push(def); + quote! { Some(#ty) } + } + _ => quote! { None }, + }; + + Ok(( + quote! { + #idl::IdlInstruction { + name: #name.into(), + docs: #docs, + discriminator: #discriminator.into(), + accounts: #ctx_ident::__anchor_private_gen_idl_accounts( + &mut accounts, + &mut types, + ), + args: vec![#(#args),*], + returns: #returns, + } + }, + defined, + )) + }) + .unzip::<_, Vec<_>, Vec<_>, Vec<_>>(); + let defined = defined.into_iter().flatten().flatten().collect::>(); + + let fn_body = gen_print_section( + "program", + quote! { + let mut accounts: std::collections::BTreeMap = + std::collections::BTreeMap::new(); + let mut types: std::collections::BTreeMap = + std::collections::BTreeMap::new(); + + #( + if let Some(ty) = <#defined>::create_type() { + types.insert(<#defined>::get_full_path(), ty); + <#defined>::insert_types(&mut types); + } + );* + + #idl::Idl { + address: Default::default(), + metadata: #idl::IdlMetadata { + name: #name.into(), + version: env!("CARGO_PKG_VERSION").into(), + spec: #idl::IDL_SPEC.into(), + description: option_env!("CARGO_PKG_DESCRIPTION") + .filter(|d| !d.is_empty()) + .map(|d| d.into()), + repository: option_env!("CARGO_PKG_REPOSITORY") + .filter(|r| !r.is_empty()) + .map(|r| r.into()), + dependencies: Default::default(), + contact: Default::default(), + }, + docs: #docs, + instructions: vec![#(#instructions),*], + accounts: accounts.into_values().collect(), + events: Default::default(), + errors: Default::default(), + types: types.into_values().collect(), + constants: Default::default(), + } + }, + ); + + quote! { + #[test] + pub fn __anchor_private_print_idl_program() { + #fn_body + } + } +} diff --git a/lang/syn/src/idl/mod.rs b/lang/syn/src/idl/mod.rs index e2979e8b15..8a3bb7a883 100644 --- a/lang/syn/src/idl/mod.rs +++ b/lang/syn/src/idl/mod.rs @@ -2,6 +2,3 @@ pub mod types; #[cfg(feature = "idl-build")] pub mod build; - -#[cfg(feature = "idl-parse")] -pub mod parse; diff --git a/lang/syn/src/idl/parse/file.rs b/lang/syn/src/idl/parse/file.rs deleted file mode 100644 index 14ebdf2018..0000000000 --- a/lang/syn/src/idl/parse/file.rs +++ /dev/null @@ -1,590 +0,0 @@ -use crate::idl::types::*; -use crate::parser::context::CrateContext; -use crate::parser::{self, accounts, docs, error, program}; -use crate::Ty; -use crate::{AccountField, AccountsStruct}; -use anyhow::anyhow; -use anyhow::Result; -use heck::MixedCase; -use quote::ToTokens; -use std::collections::{HashMap, HashSet}; -use std::path::Path; -use std::str::FromStr; -use syn::{ - Expr, ExprLit, ItemConst, - Lit::{Byte, ByteStr}, -}; - -use super::relations; - -const DERIVE_NAME: &str = "Accounts"; -// TODO: share this with `anchor_lang` crate. -const ERROR_CODE_OFFSET: u32 = 6000; - -/// Parse an entire interface file. -pub fn parse( - path: impl AsRef, - version: String, - seeds_feature: bool, - no_docs: bool, - safety_checks: bool, -) -> Result { - let ctx = CrateContext::parse(path)?; - if safety_checks { - ctx.safety_checks()?; - } - - let program_mod = parse_program_mod(&ctx)?; - let mut program = program::parse(program_mod)?; - - if no_docs { - program.docs = None; - for ix in &mut program.ixs { - ix.docs = None; - } - } - - let accs = parse_account_derives(&ctx); - - let error = parse_error_enum(&ctx).map(|mut e| error::parse(&mut e, None)); - let error_codes = error.as_ref().map(|e| { - e.codes - .iter() - .map(|code| IdlErrorCode { - code: ERROR_CODE_OFFSET + code.id, - name: code.ident.to_string(), - msg: code.msg.clone(), - }) - .collect::>() - }); - - let instructions = program - .ixs - .iter() - .map(|ix| { - let args = ix - .args - .iter() - .map(|arg| { - let doc = if !no_docs { - docs::parse(&arg.raw_arg.attrs) - } else { - None - }; - IdlField { - name: arg.name.to_string().to_mixed_case(), - docs: doc, - ty: to_idl_type(&ctx, &arg.raw_arg.ty), - } - }) - .collect::>(); - // todo: don't unwrap - let accounts_strct = accs.get(&ix.anchor_ident.to_string()).unwrap(); - let accounts = idl_accounts(&ctx, accounts_strct, &accs, seeds_feature, no_docs); - let ret_type_str = ix.returns.ty.to_token_stream().to_string(); - let returns = match ret_type_str.as_str() { - "()" => None, - _ => Some(ret_type_str.parse().unwrap()), - }; - IdlInstruction { - name: ix.ident.to_string().to_mixed_case(), - docs: ix.docs.clone(), - accounts, - args, - returns, - } - }) - .collect::>(); - - let events = parse_events(&ctx) - .iter() - .map(|e: &&syn::ItemStruct| { - let fields = match &e.fields { - syn::Fields::Named(n) => n, - _ => panic!("Event fields must be named"), - }; - let fields = fields - .named - .iter() - .map(|f: &syn::Field| { - let index = match f.attrs.first() { - None => false, - Some(i) => parser::tts_to_string(&i.path) == "index", - }; - IdlEventField { - name: f.ident.clone().unwrap().to_string().to_mixed_case(), - ty: to_idl_type(&ctx, &f.ty), - index, - } - }) - .collect::>(); - - IdlEvent { - name: e.ident.to_string(), - fields, - } - }) - .collect::>(); - - // All user defined types. - let mut accounts = vec![]; - let mut types = vec![]; - let ty_defs = parse_ty_defs(&ctx, no_docs)?; - - let account_structs = parse_accounts(&ctx); - let account_names: HashSet = account_structs - .iter() - .map(|a| a.ident.to_string()) - .collect::>(); - - let error_name = error.map(|e| e.name).unwrap_or_default(); - - // All types that aren't in the accounts section, are in the types section. - for ty_def in ty_defs { - // Don't add the error type to the types or accounts sections. - if ty_def.name != error_name { - if account_names.contains(&ty_def.name) { - accounts.push(ty_def); - } else if !events.iter().any(|e| e.name == ty_def.name) { - types.push(ty_def); - } - } - } - - let constants = parse_consts(&ctx) - .iter() - .map(|c: &&syn::ItemConst| to_idl_const(c)) - .collect::>(); - - Ok(Idl { - version, - name: program.name.to_string(), - docs: program.docs.clone(), - instructions, - types, - accounts, - events: if events.is_empty() { - None - } else { - Some(events) - }, - errors: error_codes, - metadata: None, - constants, - }) -} - -/// Parse the main program mod. -fn parse_program_mod(ctx: &CrateContext) -> Result { - let root = ctx.root_module(); - let mods = root - .items() - .filter_map(|i| match i { - syn::Item::Mod(item_mod) => { - let mod_count = item_mod - .attrs - .iter() - .filter(|attr| attr.path.segments.last().unwrap().ident == "program") - .count(); - if mod_count != 1 { - return None; - } - Some(item_mod) - } - _ => None, - }) - .collect::>(); - - match mods.len() { - 0 => Err(anyhow!("Program module not found")), - 1 => Ok(mods[0].clone()), - _ => Err(anyhow!("Multiple program modules are not allowed")), - } -} - -fn parse_error_enum(ctx: &CrateContext) -> Option { - ctx.enums() - .find(|item_enum| { - let attrs_count = item_enum - .attrs - .iter() - .filter(|attr| { - let segment = attr.path.segments.last().unwrap(); - segment.ident == "error_code" - }) - .count(); - match attrs_count { - 0 => false, - 1 => true, - _ => panic!("Invalid syntax: one error attribute allowed"), - } - }) - .cloned() -} - -fn parse_events(ctx: &CrateContext) -> Vec<&syn::ItemStruct> { - ctx.structs() - .filter(|item_strct| { - let attrs_count = item_strct - .attrs - .iter() - .filter(|attr| { - let segment = attr.path.segments.last().unwrap(); - segment.ident == "event" - }) - .count(); - match attrs_count { - 0 => false, - 1 => true, - _ => panic!("Invalid syntax: one event attribute allowed"), - } - }) - .collect() -} - -fn parse_accounts(ctx: &CrateContext) -> Vec<&syn::ItemStruct> { - ctx.structs() - .filter(|item_strct| { - let attrs_count = item_strct - .attrs - .iter() - .filter(|attr| { - let segment = attr.path.segments.last().unwrap(); - segment.ident == "account" || segment.ident == "associated" - }) - .count(); - match attrs_count { - 0 => false, - 1 => true, - _ => panic!("Invalid syntax: one account attribute allowed"), - } - }) - .collect() -} - -// Parse all structs implementing the `Accounts` trait. -fn parse_account_derives(ctx: &CrateContext) -> HashMap { - // TODO: parse manual implementations. Currently we only look - // for derives. - ctx.structs() - .filter_map(|i_strct| { - for attr in &i_strct.attrs { - if attr.path.is_ident("derive") && attr.tokens.to_string().contains(DERIVE_NAME) { - let strct = accounts::parse(i_strct).expect("Code not parseable"); - return Some((strct.ident.to_string(), strct)); - } - } - None - }) - .collect() -} - -fn parse_consts(ctx: &CrateContext) -> Vec<&syn::ItemConst> { - ctx.consts() - .filter(|item_strct| { - for attr in &item_strct.attrs { - if attr.path.segments.last().unwrap().ident == "constant" { - return true; - } - } - false - }) - .collect() -} - -// Parse all user defined types in the file. -fn parse_ty_defs(ctx: &CrateContext, no_docs: bool) -> Result> { - ctx.structs() - .filter_map(|item_strct| { - // Only take public types - if !matches!(&item_strct.vis, syn::Visibility::Public(_)) { - return None; - } - - // Only take serializable types - let serializable = item_strct.attrs.iter().any(|attr| { - let attr_string = attr.tokens.to_string(); - let attr_name = attr.path.segments.last().unwrap().ident.to_string(); - let attr_serializable = ["account", "associated", "event", "zero_copy"]; - - let derived_serializable = attr_name == "derive" - && attr_string.contains("AnchorSerialize") - && attr_string.contains("AnchorDeserialize"); - - attr_serializable.iter().any(|a| *a == attr_name) || derived_serializable - }); - - if !serializable { - return None; - } - - let name = item_strct.ident.to_string(); - let doc = if !no_docs { - docs::parse(&item_strct.attrs) - } else { - None - }; - let fields = match &item_strct.fields { - syn::Fields::Named(fields) => fields - .named - .iter() - .map(|f: &syn::Field| { - let doc = if !no_docs { - docs::parse(&f.attrs) - } else { - None - }; - Ok(IdlField { - name: f.ident.as_ref().unwrap().to_string().to_mixed_case(), - docs: doc, - ty: to_idl_type(ctx, &f.ty), - }) - }) - .collect::>>(), - syn::Fields::Unnamed(_) => return None, - _ => panic!("Empty structs are not allowed."), - }; - - Some(fields.map(|fields| IdlTypeDefinition { - name, - generics: None, - docs: doc, - ty: IdlTypeDefinitionTy::Struct { fields }, - })) - }) - .chain(ctx.enums().filter_map(|enm| { - // Only take public types - if !matches!(&enm.vis, syn::Visibility::Public(_)) { - return None; - } - - let name = enm.ident.to_string(); - let doc = if !no_docs { - docs::parse(&enm.attrs) - } else { - None - }; - let variants = enm - .variants - .iter() - .map(|variant: &syn::Variant| { - let name = variant.ident.to_string(); - let fields = match &variant.fields { - syn::Fields::Unit => None, - syn::Fields::Unnamed(fields) => { - let fields: Vec = fields - .unnamed - .iter() - .map(|f| to_idl_type(ctx, &f.ty)) - .collect(); - Some(EnumFields::Tuple(fields)) - } - syn::Fields::Named(fields) => { - let fields: Vec = fields - .named - .iter() - .map(|f: &syn::Field| { - let name = - f.ident.as_ref().unwrap().to_string().to_mixed_case(); - let doc = if !no_docs { - docs::parse(&f.attrs) - } else { - None - }; - let ty = to_idl_type(ctx, &f.ty); - IdlField { - name, - docs: doc, - ty, - } - }) - .collect(); - Some(EnumFields::Named(fields)) - } - }; - IdlEnumVariant { name, fields } - }) - .collect::>(); - - Some(Ok(IdlTypeDefinition { - name, - generics: None, - docs: doc, - ty: IdlTypeDefinitionTy::Enum { variants }, - })) - })) - .chain(ctx.type_aliases().filter_map(|alias| { - // Only take public types - if !matches!(&alias.vis, syn::Visibility::Public(_)) { - return None; - } - // Generic type aliases are not currently supported - if alias.generics.lt_token.is_some() { - return None; - } - - let name = alias.ident.to_string(); - let doc = if !no_docs { - docs::parse(&alias.attrs) - } else { - None - }; - let result = IdlType::from_str(&alias.ty.to_token_stream().to_string()).map(|value| { - IdlTypeDefinition { - name, - generics: None, - docs: doc, - ty: IdlTypeDefinitionTy::Alias { value }, - } - }); - - match &result { - Ok(_) => Some(result), - Err(_) => None, - } - })) - .collect() -} - -// Replace variable array lengths with values -fn resolve_variable_array_lengths(ctx: &CrateContext, mut tts_string: String) -> String { - for constant in ctx.consts().filter(|c| match *c.ty { - // Filter to only those consts that are of type usize or could be cast to usize - syn::Type::Path(ref p) => { - let segment = p.path.segments.last().unwrap(); - matches!( - segment.ident.to_string().as_str(), - "usize" - | "u8" - | "u16" - | "u32" - | "u64" - | "u128" - | "isize" - | "i8" - | "i16" - | "i32" - | "i64" - | "i128" - ) - } - _ => false, - }) { - let mut check_string = tts_string.clone(); - // Strip whitespace to handle accidental double whitespaces - check_string.retain(|c| !c.is_whitespace()); - let size_string = format!("{}]", &constant.ident.to_string()); - let cast_size_string = format!("{}asusize]", &constant.ident.to_string()); - // Check for something to replace - let mut replacement_string = None; - if check_string.contains(cast_size_string.as_str()) { - replacement_string = Some(cast_size_string); - } else if check_string.contains(size_string.as_str()) { - replacement_string = Some(size_string); - } - if let Some(replacement_string) = replacement_string { - // Check for the existence of consts existing elsewhere in the - // crate which have the same name, are usize, and have a - // different value. We can't know which was intended for the - // array size from ctx. - if ctx.consts().any(|c| { - c != constant - && c.ident == constant.ident - && c.ty == constant.ty - && c.expr != constant.expr - }) { - panic!("Crate wide unique name required for array size const."); - } - // Replace the match, don't break because there might be multiple replacements to be - // made in the case of multidimensional arrays - tts_string = check_string.replace( - &replacement_string, - format!("{}]", &constant.expr.to_token_stream()).as_str(), - ); - } - } - tts_string -} - -fn to_idl_type(ctx: &CrateContext, ty: &syn::Type) -> IdlType { - let mut tts_string = parser::tts_to_string(ty); - if tts_string.starts_with('[') { - tts_string = resolve_variable_array_lengths(ctx, tts_string); - } - // Box -> FooType - tts_string = tts_string - .strip_prefix("Box < ") - .and_then(|t| t.strip_suffix(" >")) - .unwrap_or(&tts_string) - .into(); - - tts_string.parse().unwrap() -} - -// TODO parse other issues -fn to_idl_const(item: &ItemConst) -> IdlConst { - let name = item.ident.to_string(); - - if let Expr::Lit(ExprLit { lit, .. }) = &*item.expr { - match lit { - ByteStr(lit_byte_str) => { - return IdlConst { - name, - ty: IdlType::Bytes, - value: format!("{:?}", lit_byte_str.value()), - } - } - Byte(lit_byte) => { - return IdlConst { - name, - ty: IdlType::U8, - value: lit_byte.value().to_string(), - } - } - _ => (), - } - } - - IdlConst { - name, - ty: item.ty.to_token_stream().to_string().parse().unwrap(), - value: item.expr.to_token_stream().to_string().parse().unwrap(), - } -} - -fn idl_accounts( - ctx: &CrateContext, - accounts: &AccountsStruct, - global_accs: &HashMap, - seeds_feature: bool, - no_docs: bool, -) -> Vec { - accounts - .fields - .iter() - .map(|acc: &AccountField| match acc { - AccountField::CompositeField(comp_f) => { - let accs_strct = global_accs.get(&comp_f.symbol).unwrap_or_else(|| { - panic!("Could not resolve Accounts symbol {}", comp_f.symbol) - }); - let accounts = idl_accounts(ctx, accs_strct, global_accs, seeds_feature, no_docs); - IdlAccountItem::IdlAccounts(IdlAccounts { - name: comp_f.ident.to_string().to_mixed_case(), - accounts, - }) - } - AccountField::Field(acc) => IdlAccountItem::IdlAccount(IdlAccount { - name: acc.ident.to_string().to_mixed_case(), - is_mut: acc.constraints.is_mutable(), - is_signer: match acc.ty { - Ty::Signer => true, - _ => acc.constraints.is_signer(), - }, - is_optional: if acc.is_optional { Some(true) } else { None }, - docs: if !no_docs { acc.docs.clone() } else { None }, - pda: super::pda::parse(ctx, accounts, acc, seeds_feature), - relations: relations::parse(acc, seeds_feature), - }), - }) - .collect::>() -} diff --git a/lang/syn/src/idl/parse/mod.rs b/lang/syn/src/idl/parse/mod.rs deleted file mode 100644 index 88dc4e7ba7..0000000000 --- a/lang/syn/src/idl/parse/mod.rs +++ /dev/null @@ -1,120 +0,0 @@ -use crate::idl::types::*; - -pub mod file; -pub mod pda; -pub mod relations; - -impl std::str::FromStr for IdlType { - type Err = anyhow::Error; - - fn from_str(s: &str) -> Result { - let mut s = s.to_string(); - fn array_from_str(inner: &str) -> IdlType { - match inner.strip_suffix(']') { - None => { - let (raw_type, raw_length) = inner.rsplit_once(';').unwrap(); - let ty = IdlType::from_str(raw_type).unwrap(); - let len = raw_length.replace('_', "").parse::().unwrap(); - IdlType::Array(Box::new(ty), len) - } - Some(nested_inner) => array_from_str(&nested_inner[1..]), - } - } - s.retain(|c| !c.is_whitespace()); - - let r = match s.as_str() { - "bool" => IdlType::Bool, - "u8" => IdlType::U8, - "i8" => IdlType::I8, - "u16" => IdlType::U16, - "i16" => IdlType::I16, - "u32" => IdlType::U32, - "i32" => IdlType::I32, - "f32" => IdlType::F32, - "u64" => IdlType::U64, - "i64" => IdlType::I64, - "f64" => IdlType::F64, - "u128" => IdlType::U128, - "i128" => IdlType::I128, - "u256" => IdlType::U256, - "i256" => IdlType::I256, - "Vec" => IdlType::Bytes, - "String" | "&str" | "&'staticstr" => IdlType::String, - "Pubkey" => IdlType::PublicKey, - _ => match s.to_string().strip_prefix("Option<") { - None => match s.to_string().strip_prefix("Vec<") { - None => { - if s.to_string().starts_with('[') { - array_from_str(&s) - } else { - IdlType::Defined(s.to_string()) - } - } - Some(inner) => { - let inner_ty = Self::from_str( - inner - .strip_suffix('>') - .ok_or_else(|| anyhow::anyhow!("Invalid option"))?, - )?; - IdlType::Vec(Box::new(inner_ty)) - } - }, - Some(inner) => { - let inner_ty = Self::from_str( - inner - .strip_suffix('>') - .ok_or_else(|| anyhow::anyhow!("Invalid option"))?, - )?; - IdlType::Option(Box::new(inner_ty)) - } - }, - }; - Ok(r) - } -} - -#[cfg(test)] -mod tests { - use crate::idl::types::IdlType; - use std::str::FromStr; - - #[test] - fn multidimensional_array() { - assert_eq!( - IdlType::from_str("[[u8;16];32]").unwrap(), - IdlType::Array(Box::new(IdlType::Array(Box::new(IdlType::U8), 16)), 32) - ); - } - - #[test] - fn array() { - assert_eq!( - IdlType::from_str("[Pubkey;16]").unwrap(), - IdlType::Array(Box::new(IdlType::PublicKey), 16) - ); - } - - #[test] - fn array_with_underscored_length() { - assert_eq!( - IdlType::from_str("[u8;50_000]").unwrap(), - IdlType::Array(Box::new(IdlType::U8), 50000) - ); - } - - #[test] - fn option() { - assert_eq!( - IdlType::from_str("Option").unwrap(), - IdlType::Option(Box::new(IdlType::Bool)) - ) - } - - #[test] - fn vector() { - assert_eq!( - IdlType::from_str("Vec").unwrap(), - IdlType::Vec(Box::new(IdlType::Bool)) - ) - } -} diff --git a/lang/syn/src/idl/parse/pda.rs b/lang/syn/src/idl/parse/pda.rs deleted file mode 100644 index d9561c29c1..0000000000 --- a/lang/syn/src/idl/parse/pda.rs +++ /dev/null @@ -1,389 +0,0 @@ -use crate::idl::types::*; -use crate::parser; -use crate::parser::context::CrateContext; -use crate::ConstraintSeedsGroup; -use crate::{AccountsStruct, Field}; -use std::collections::HashMap; -use std::str::FromStr; -use syn::{Expr, ExprLit, Lit, Path}; - -// Parses a seeds constraint, extracting the IdlSeed types. -// -// Note: This implementation makes assumptions about the types that can be used -// (e.g., no program-defined function calls in seeds). -// -// This probably doesn't cover all cases. If you see a warning log, you -// can add a new case here. In the worst case, we miss a seed and -// the parser will treat the given seeds as empty and so clients will -// simply fail to automatically populate the PDA accounts. -// -// Seed Assumptions: Seeds must be of one of the following forms: -// -// - instruction argument. -// - account context field pubkey. -// - account data, where the account is defined in the current program. -// We make an exception for the SPL token program, since it is so common -// and sometimes convenient to use fields as a seed (e.g. Auction house -// program). In the case of nested structs/account data, all nested structs -// must be defined in the current program as well. -// - byte string literal (e.g. b"MY_SEED"). -// - byte string literal constant (e.g. `pub const MY_SEED: [u8; 2] = *b"hi";`). -// - array constants. -// -pub fn parse( - ctx: &CrateContext, - accounts: &AccountsStruct, - acc: &Field, - seeds_feature: bool, -) -> Option { - if !seeds_feature { - return None; - } - let pda_parser = PdaParser::new(ctx, accounts); - acc.constraints - .seeds - .as_ref() - .map(|s| pda_parser.parse(s)) - .unwrap_or(None) -} - -struct PdaParser<'a> { - ctx: &'a CrateContext, - // Accounts context. - accounts: &'a AccountsStruct, - // Maps var name to var type. These are the instruction arguments in a - // given accounts context. - ix_args: HashMap, - // Constants available in the crate. - const_names: Vec, - // Constants declared in impl blocks available in the crate - impl_const_names: Vec, - // All field names of the accounts in the accounts context. - account_field_names: Vec, -} - -impl<'a> PdaParser<'a> { - fn new(ctx: &'a CrateContext, accounts: &'a AccountsStruct) -> Self { - // All the available sources of seeds. - let ix_args = accounts.instruction_args().unwrap_or_default(); - let const_names: Vec = ctx.consts().map(|c| c.ident.to_string()).collect(); - - let impl_const_names: Vec = ctx - .impl_consts() - .map(|(ident, item)| format!("{} :: {}", ident, item.ident)) - .collect(); - - let account_field_names = accounts.field_names(); - - Self { - ctx, - accounts, - ix_args, - const_names, - impl_const_names, - account_field_names, - } - } - - fn parse(&self, seeds_grp: &ConstraintSeedsGroup) -> Option { - // Extract the idl seed types from the constraints. - let seeds = seeds_grp - .seeds - .iter() - .map(|s| self.parse_seed(s)) - .collect::>>()?; - // Parse the program id from the constraints. - let program_id = seeds_grp - .program_seed - .as_ref() - .map(|pid| self.parse_seed(pid)) - .unwrap_or_default(); - - // Done. - Some(IdlPda { seeds, program_id }) - } - - fn parse_seed(&self, seed: &Expr) -> Option { - match seed { - Expr::MethodCall(_) => { - let seed_path = parse_seed_path(seed)?; - - if self.is_instruction(&seed_path) { - self.parse_instruction(&seed_path) - } else if self.is_const(&seed_path) { - self.parse_const(&seed_path) - } else if self.is_impl_const(&seed_path) { - self.parse_impl_const(&seed_path) - } else if self.is_account(&seed_path) { - self.parse_account(&seed_path) - } else if self.is_str_literal(&seed_path) { - self.parse_str_literal(&seed_path) - } else { - println!("WARNING: unexpected seed category for var: {seed_path:?}"); - None - } - } - Expr::Reference(expr_reference) => self.parse_seed(&expr_reference.expr), - Expr::Index(_) => { - println!("WARNING: auto pda derivation not currently supported for slice literals"); - None - } - Expr::Lit(ExprLit { - lit: Lit::ByteStr(lit_byte_str), - .. - }) => { - let seed_path: SeedPath = SeedPath(lit_byte_str.token().to_string(), Vec::new()); - self.parse_str_literal(&seed_path) - } - Expr::Path(expr_path) => self.parse_const_path(&expr_path.path), - // Unknown type. Please file an issue. - _ => { - println!("WARNING: unexpected seed: {seed:?}"); - None - } - } - } - - fn parse_instruction(&self, seed_path: &SeedPath) -> Option { - let idl_ty = IdlType::from_str(self.ix_args.get(&seed_path.name()).unwrap()).ok()?; - Some(IdlSeed::Arg(IdlSeedArg { - ty: idl_ty, - path: seed_path.path(), - })) - } - - fn parse_const_path(&self, path: &Path) -> Option { - let ident = &path.segments.first().unwrap().ident; - - let const_item = self.ctx.consts().find(|c| c.ident == *ident).unwrap(); - let idl_ty = IdlType::from_str(&parser::tts_to_string(&const_item.ty)).ok()?; - - let idl_ty_value = parser::tts_to_string(&const_item.expr); - let idl_ty_value: String = str_lit_to_array(&idl_ty, &idl_ty_value); - - let seed_path: SeedPath = SeedPath(idl_ty_value, Vec::new()); - - if self.is_str_literal(&seed_path) { - self.parse_str_literal(&seed_path) - } else { - println!("WARNING: unexpected constant path value: {seed_path:?}"); - None - } - } - - fn parse_const(&self, seed_path: &SeedPath) -> Option { - // Pull in the constant value directly into the IDL. - assert!(seed_path.components().is_empty()); - let const_item = self - .ctx - .consts() - .find(|c| c.ident == seed_path.name()) - .unwrap(); - let idl_ty = IdlType::from_str(&parser::tts_to_string(&const_item.ty)).ok()?; - - let idl_ty_value = parser::tts_to_string(&const_item.expr); - let idl_ty_value = str_lit_to_array(&idl_ty, &idl_ty_value); - - Some(IdlSeed::Const(IdlSeedConst { - ty: idl_ty, - value: serde_json::from_str(&idl_ty_value).unwrap(), - })) - } - - fn parse_impl_const(&self, seed_path: &SeedPath) -> Option { - // Pull in the constant value directly into the IDL. - assert!(seed_path.components().is_empty()); - let static_item = self - .ctx - .impl_consts() - .find(|(ident, item)| format!("{} :: {}", ident, item.ident) == seed_path.name()) - .unwrap() - .1; - - let idl_ty = IdlType::from_str(&parser::tts_to_string(&static_item.ty)).ok()?; - - let idl_ty_value = parser::tts_to_string(&static_item.expr); - let idl_ty_value = str_lit_to_array(&idl_ty, &idl_ty_value); - - Some(IdlSeed::Const(IdlSeedConst { - ty: idl_ty, - value: serde_json::from_str(&idl_ty_value).unwrap(), - })) - } - - fn parse_account(&self, seed_path: &SeedPath) -> Option { - // Get the anchor account field from the derive accounts struct. - let account_field = self - .accounts - .fields - .iter() - .find(|field| *field.ident() == seed_path.name()) - .unwrap(); - - // Follow the path to find the seed type. - let ty = { - let mut path = seed_path.components(); - match path.len() { - 0 => IdlType::PublicKey, - 1 => { - // Name of the account struct. - let account = account_field.ty_name()?; - if account == "TokenAccount" { - assert!(path.len() == 1); - match path[0].as_str() { - "mint" => IdlType::PublicKey, - "amount" => IdlType::U64, - "authority" => IdlType::PublicKey, - "delegated_amount" => IdlType::U64, - _ => { - println!("WARNING: token field isn't supported: {}", &path[0]); - return None; - } - } - } else { - // Get the rust representation of the field's struct. - let strct = self.ctx.structs().find(|s| s.ident == account).unwrap(); - parse_field_path(self.ctx, strct, &mut path) - } - } - _ => panic!("invariant violation"), - } - }; - - Some(IdlSeed::Account(IdlSeedAccount { - ty, - account: account_field.ty_name(), - path: seed_path.path(), - })) - } - - fn parse_str_literal(&self, seed_path: &SeedPath) -> Option { - let mut var_name = seed_path.name(); - // Remove the byte `b` prefix if the string is of the form `b"seed". - if var_name.starts_with("b\"") { - var_name.remove(0); - } - let value_string: String = var_name.chars().filter(|c| *c != '"').collect(); - Some(IdlSeed::Const(IdlSeedConst { - value: serde_json::Value::String(value_string), - ty: IdlType::String, - })) - } - - fn is_instruction(&self, seed_path: &SeedPath) -> bool { - self.ix_args.contains_key(&seed_path.name()) - } - - fn is_const(&self, seed_path: &SeedPath) -> bool { - self.const_names.contains(&seed_path.name()) - } - - fn is_impl_const(&self, seed_path: &SeedPath) -> bool { - self.impl_const_names.contains(&seed_path.name()) - } - - fn is_account(&self, seed_path: &SeedPath) -> bool { - self.account_field_names.contains(&seed_path.name()) - } - - fn is_str_literal(&self, seed_path: &SeedPath) -> bool { - seed_path.components().is_empty() && seed_path.name().contains('"') - } -} - -// SeedPath represents the deconstructed syntax of a single pda seed, -// consisting of a variable name and a vec of all the sub fields accessed -// on that variable name. For example, if a seed is `my_field.my_data.as_ref()`, -// then the field name is `my_field` and the vec of sub fields is `[my_data]`. -#[derive(Debug)] -struct SeedPath(String, Vec); - -impl SeedPath { - fn name(&self) -> String { - self.0.clone() - } - - // Full path to the data this seed represents. - fn path(&self) -> String { - match self.1.len() { - 0 => self.0.clone(), - _ => format!("{}.{}", self.name(), self.components().join(".")), - } - } - - // All path components for the subfields accessed on this seed. - fn components(&self) -> &[String] { - &self.1 - } -} - -// Extracts the seed path from a single seed expression. -fn parse_seed_path(seed: &Expr) -> Option { - // Convert the seed into the raw string representation. - let seed_str = parser::tts_to_string(seed); - - // Break up the seed into each sub field component. - let mut components: Vec<&str> = seed_str.split(" . ").collect(); - if components.len() <= 1 { - println!("WARNING: seeds are in an unexpected format: {seed:?}"); - return None; - } - - // The name of the variable (or field). - let name = components.remove(0).to_string(); - - // The path to the seed (only if the `name` type is a struct). - let mut path = Vec::new(); - while !components.is_empty() { - let c = components.remove(0); - if c.contains("()") { - break; - } - path.push(c.to_string()); - } - if path.len() == 1 && (path[0] == "key" || path[0] == "key()") { - path = Vec::new(); - } - - Some(SeedPath(name, path)) -} - -fn parse_field_path(ctx: &CrateContext, strct: &syn::ItemStruct, path: &mut &[String]) -> IdlType { - let field_name = &path[0]; - *path = &path[1..]; - - // Get the type name for the field. - let next_field = strct - .fields - .iter() - .find(|f| &f.ident.clone().unwrap().to_string() == field_name) - .unwrap(); - let next_field_ty_str = parser::tts_to_string(&next_field.ty); - - // The path is empty so this must be a primitive type. - if path.is_empty() { - return next_field_ty_str.parse().unwrap(); - } - - // Get the rust representation of hte field's struct. - let strct = ctx - .structs() - .find(|s| s.ident == next_field_ty_str) - .unwrap(); - - parse_field_path(ctx, strct, path) -} - -fn str_lit_to_array(idl_ty: &IdlType, idl_ty_value: &String) -> String { - if let IdlType::Array(_ty, _size) = &idl_ty { - // Convert str literal to array. - if idl_ty_value.contains("b\"") { - let components: Vec<&str> = idl_ty_value.split('b').collect(); - assert_eq!(components.len(), 2); - let mut str_lit = components[1].to_string(); - str_lit.retain(|c| c != '"'); - return format!("{:?}", str_lit.as_bytes()); - } - } - idl_ty_value.to_string() -} diff --git a/lang/syn/src/idl/parse/relations.rs b/lang/syn/src/idl/parse/relations.rs deleted file mode 100644 index b7276c72a2..0000000000 --- a/lang/syn/src/idl/parse/relations.rs +++ /dev/null @@ -1,19 +0,0 @@ -use crate::Field; -use syn::Expr; - -pub fn parse(acc: &Field, seeds_feature: bool) -> Vec { - if !seeds_feature { - return vec![]; - } - acc.constraints - .has_one - .iter() - .flat_map(|s| match &s.join_target { - Expr::Path(path) => path.path.segments.first().map(|l| l.ident.to_string()), - _ => { - println!("WARNING: unexpected seed: {s:?}"); - None - } - }) - .collect() -} diff --git a/lang/syn/src/idl/types.rs b/lang/syn/src/idl/types.rs index 0aa2956997..a145f8261d 100644 --- a/lang/syn/src/idl/types.rs +++ b/lang/syn/src/idl/types.rs @@ -1,92 +1,104 @@ +use std::str::FromStr; + +use anyhow::anyhow; use serde::{Deserialize, Serialize}; +/// IDL specification Semantic Version +pub const IDL_SPEC: &str = "0.1.0"; + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct Idl { - pub version: String, - pub name: String, - #[serde(skip_serializing_if = "Option::is_none", default)] - pub docs: Option>, - #[serde(skip_serializing_if = "Vec::is_empty", default)] - pub constants: Vec, + pub address: String, + pub metadata: IdlMetadata, + #[serde(default, skip_serializing_if = "is_default")] + pub docs: Vec, pub instructions: Vec, - #[serde(skip_serializing_if = "Vec::is_empty", default)] - pub accounts: Vec, - #[serde(skip_serializing_if = "Vec::is_empty", default)] - pub types: Vec, - #[serde(skip_serializing_if = "Option::is_none", default)] - pub events: Option>, - #[serde(skip_serializing_if = "Option::is_none", default)] - pub errors: Option>, - #[serde(skip_serializing_if = "Option::is_none", default)] - pub metadata: Option, + #[serde(default, skip_serializing_if = "is_default")] + pub accounts: Vec, + #[serde(default, skip_serializing_if = "is_default")] + pub events: Vec, + #[serde(default, skip_serializing_if = "is_default")] + pub errors: Vec, + #[serde(default, skip_serializing_if = "is_default")] + pub types: Vec, + #[serde(default, skip_serializing_if = "is_default")] + pub constants: Vec, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub struct IdlConst { +pub struct IdlMetadata { pub name: String, - #[serde(rename = "type")] - pub ty: IdlType, - pub value: String, + pub version: String, + pub spec: String, + #[serde(skip_serializing_if = "is_default")] + pub description: Option, + #[serde(skip_serializing_if = "is_default")] + pub repository: Option, + #[serde(default, skip_serializing_if = "is_default")] + pub dependencies: Vec, + #[serde(skip_serializing_if = "is_default")] + pub contact: Option, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub struct IdlState { - #[serde(rename = "struct")] - pub strct: IdlTypeDefinition, - pub methods: Vec, +pub struct IdlDependency { + pub name: String, + pub version: String, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct IdlInstruction { pub name: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub docs: Option>, - pub accounts: Vec, + #[serde(default, skip_serializing_if = "is_default")] + pub docs: Vec, + pub discriminator: IdlDiscriminator, + pub accounts: Vec, pub args: Vec, - #[serde(skip_serializing_if = "Option::is_none")] + #[serde(skip_serializing_if = "is_default")] pub returns: Option, } -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -#[serde(rename_all = "camelCase")] -pub struct IdlAccounts { - pub name: String, - pub accounts: Vec, -} - #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(untagged)] -pub enum IdlAccountItem { - IdlAccount(IdlAccount), - IdlAccounts(IdlAccounts), +pub enum IdlInstructionAccountItem { + Composite(IdlInstructionAccounts), + Single(IdlInstructionAccount), } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -#[serde(rename_all = "camelCase")] -pub struct IdlAccount { +pub struct IdlInstructionAccount { pub name: String, - pub is_mut: bool, - pub is_signer: bool, - #[serde(skip_serializing_if = "Option::is_none")] - pub is_optional: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub docs: Option>, - #[serde(skip_serializing_if = "Option::is_none", default)] + #[serde(default, skip_serializing_if = "is_default")] + pub docs: Vec, + #[serde(default, skip_serializing_if = "is_default")] + pub writable: bool, + #[serde(default, skip_serializing_if = "is_default")] + pub signer: bool, + #[serde(default, skip_serializing_if = "is_default")] + pub optional: bool, + #[serde(skip_serializing_if = "is_default")] + pub address: Option, + #[serde(skip_serializing_if = "is_default")] pub pda: Option, - #[serde(skip_serializing_if = "Vec::is_empty", default)] + #[serde(default, skip_serializing_if = "is_default")] pub relations: Vec, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -#[serde(rename_all = "camelCase")] +pub struct IdlInstructionAccounts { + pub name: String, + pub accounts: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct IdlPda { pub seeds: Vec, - #[serde(skip_serializing_if = "Option::is_none", default)] - pub program_id: Option, + #[serde(skip_serializing_if = "is_default")] + pub program: Option, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -#[serde(rename_all = "camelCase", tag = "kind")] +#[serde(tag = "kind", rename_all = "lowercase")] pub enum IdlSeed { Const(IdlSeedConst), Arg(IdlSeedArg), @@ -94,96 +106,164 @@ pub enum IdlSeed { } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -#[serde(rename_all = "camelCase")] -pub struct IdlSeedAccount { - #[serde(rename = "type")] - pub ty: IdlType, - // account_ty points to the entry in the "accounts" section. - // Some only if the `Account` type is used. - #[serde(skip_serializing_if = "Option::is_none")] - pub account: Option, - pub path: String, +pub struct IdlSeedConst { + pub value: Vec, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -#[serde(rename_all = "camelCase")] pub struct IdlSeedArg { - #[serde(rename = "type")] - pub ty: IdlType, pub path: String, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -#[serde(rename_all = "camelCase")] -pub struct IdlSeedConst { - #[serde(rename = "type")] - pub ty: IdlType, - pub value: serde_json::Value, +pub struct IdlSeedAccount { + pub path: String, + #[serde(skip_serializing_if = "is_default")] + pub account: Option, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub struct IdlField { +pub struct IdlAccount { pub name: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub docs: Option>, - #[serde(rename = "type")] - pub ty: IdlType, + pub discriminator: IdlDiscriminator, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct IdlEvent { pub name: String, - pub fields: Vec, + pub discriminator: IdlDiscriminator, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub struct IdlEventField { +pub struct IdlConst { pub name: String, #[serde(rename = "type")] pub ty: IdlType, - pub index: bool, + pub value: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct IdlErrorCode { + pub code: u32, + pub name: String, + #[serde(skip_serializing_if = "is_default")] + pub msg: Option, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub struct IdlTypeDefinition { - /// - `idl-parse`: always the name of the type - /// - `idl-build`: full path if there is a name conflict, otherwise the name of the type +pub struct IdlField { pub name: String, - /// Documentation comments - #[serde(skip_serializing_if = "Option::is_none")] - pub docs: Option>, - /// Generics, only supported with `idl-build` - #[serde(skip_serializing_if = "Option::is_none")] - pub generics: Option>, - /// Type definition, `struct` or `enum` + #[serde(default, skip_serializing_if = "is_default")] + pub docs: Vec, #[serde(rename = "type")] - pub ty: IdlTypeDefinitionTy, + pub ty: IdlType, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -#[serde(rename_all = "lowercase", tag = "kind")] -pub enum IdlTypeDefinitionTy { - Struct { fields: Vec }, - Enum { variants: Vec }, - Alias { value: IdlType }, +pub struct IdlTypeDef { + pub name: String, + #[serde(default, skip_serializing_if = "is_default")] + pub docs: Vec, + #[serde(default, skip_serializing_if = "is_default")] + pub serialization: IdlSerialization, + #[serde(skip_serializing_if = "is_default")] + pub repr: Option, + #[serde(default, skip_serializing_if = "is_default")] + pub generics: Vec, + #[serde(rename = "type")] + pub ty: IdlTypeDefTy, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] +#[serde(rename_all = "lowercase")] +pub enum IdlSerialization { + #[default] + Borsh, + Bytemuck, + BytemuckUnsafe, + Custom(String), +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(tag = "kind", rename_all = "lowercase")] +pub enum IdlRepr { + Rust(IdlReprModifier), + C(IdlReprModifier), + Transparent, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct IdlReprModifier { + #[serde(default, skip_serializing_if = "is_default")] + pub packed: bool, + #[serde(skip_serializing_if = "is_default")] + pub align: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(tag = "kind", rename_all = "lowercase")] +pub enum IdlTypeDefGeneric { + Type { + name: String, + }, + Const { + name: String, + #[serde(rename = "type")] + ty: String, + }, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(tag = "kind", rename_all = "lowercase")] +pub enum IdlTypeDefTy { + Struct { + #[serde(skip_serializing_if = "is_default")] + fields: Option, + }, + Enum { + variants: Vec, + }, + Type { + alias: IdlType, + }, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct IdlEnumVariant { pub name: String, - #[serde(skip_serializing_if = "Option::is_none", default)] - pub fields: Option, + #[serde(skip_serializing_if = "is_default")] + pub fields: Option, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(untagged)] -pub enum EnumFields { +pub enum IdlDefinedFields { Named(Vec), Tuple(Vec), } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -#[serde(rename_all = "camelCase")] +#[serde(rename_all = "lowercase")] +pub enum IdlArrayLen { + Generic(String), + #[serde(untagged)] + Value(usize), +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(tag = "kind", rename_all = "lowercase")] +pub enum IdlGenericArg { + Type { + #[serde(rename = "type")] + ty: IdlType, + }, + Const { + value: String, + }, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "lowercase")] pub enum IdlType { Bool, U8, @@ -202,31 +282,205 @@ pub enum IdlType { I256, Bytes, String, - PublicKey, - Defined(String), + Pubkey, Option(Box), Vec(Box), - Array(Box, usize), - GenericLenArray(Box, String), - Generic(String), - DefinedWithTypeArgs { + Array(Box, IdlArrayLen), + Defined { name: String, - args: Vec, + #[serde(default, skip_serializing_if = "is_default")] + generics: Vec, }, + Generic(String), } -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -#[serde(rename_all = "camelCase")] -pub enum IdlDefinedTypeArg { - Generic(String), - Value(String), - Type(IdlType), +impl FromStr for IdlType { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + let mut s = s.to_owned(); + s.retain(|c| !c.is_whitespace()); + + let r = match s.as_str() { + "bool" => IdlType::Bool, + "u8" => IdlType::U8, + "i8" => IdlType::I8, + "u16" => IdlType::U16, + "i16" => IdlType::I16, + "u32" => IdlType::U32, + "i32" => IdlType::I32, + "f32" => IdlType::F32, + "u64" => IdlType::U64, + "i64" => IdlType::I64, + "f64" => IdlType::F64, + "u128" => IdlType::U128, + "i128" => IdlType::I128, + "u256" => IdlType::U256, + "i256" => IdlType::I256, + "Vec" => IdlType::Bytes, + "String" | "&str" | "&'staticstr" => IdlType::String, + "Pubkey" => IdlType::Pubkey, + _ => { + if let Some(inner) = s.strip_prefix("Option<") { + let inner_ty = Self::from_str( + inner + .strip_suffix('>') + .ok_or_else(|| anyhow!("Invalid Option"))?, + )?; + return Ok(IdlType::Option(Box::new(inner_ty))); + } + + if let Some(inner) = s.strip_prefix("Vec<") { + let inner_ty = Self::from_str( + inner + .strip_suffix('>') + .ok_or_else(|| anyhow!("Invalid Vec"))?, + )?; + return Ok(IdlType::Vec(Box::new(inner_ty))); + } + + if s.starts_with('[') { + fn array_from_str(inner: &str) -> IdlType { + match inner.strip_suffix(']') { + Some(nested_inner) => array_from_str(&nested_inner[1..]), + None => { + let (raw_type, raw_length) = inner.rsplit_once(';').unwrap(); + let ty = IdlType::from_str(raw_type).unwrap(); + let len = match raw_length.replace('_', "").parse::() { + Ok(len) => IdlArrayLen::Value(len), + Err(_) => IdlArrayLen::Generic(raw_length.to_owned()), + }; + IdlType::Array(Box::new(ty), len) + } + } + } + return Ok(array_from_str(&s)); + } + + // Defined + let (name, generics) = if let Some(i) = s.find('<') { + ( + s.get(..i).unwrap().to_owned(), + s.get(i + 1..) + .unwrap() + .strip_suffix('>') + .unwrap() + .split(',') + .map(|g| g.trim().to_owned()) + .map(|g| { + if g.parse::().is_ok() + || g.parse::().is_ok() + || g.parse::().is_ok() + || g.parse::().is_ok() + { + Ok(IdlGenericArg::Const { value: g }) + } else { + Self::from_str(&g).map(|ty| IdlGenericArg::Type { ty }) + } + }) + .collect::, _>>()?, + ) + } else { + (s.to_owned(), vec![]) + }; + + IdlType::Defined { name, generics } + } + }; + Ok(r) + } } -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct IdlErrorCode { - pub code: u32, - pub name: String, - #[serde(skip_serializing_if = "Option::is_none", default)] - pub msg: Option, +pub type IdlDiscriminator = Vec; + +/// Get whether the given data is the default of its type. +fn is_default(it: &T) -> bool { + *it == T::default() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn option() { + assert_eq!( + IdlType::from_str("Option").unwrap(), + IdlType::Option(Box::new(IdlType::Bool)) + ) + } + + #[test] + fn vector() { + assert_eq!( + IdlType::from_str("Vec").unwrap(), + IdlType::Vec(Box::new(IdlType::Bool)) + ) + } + + #[test] + fn array() { + assert_eq!( + IdlType::from_str("[Pubkey; 16]").unwrap(), + IdlType::Array(Box::new(IdlType::Pubkey), IdlArrayLen::Value(16)) + ); + } + + #[test] + fn array_with_underscored_length() { + assert_eq!( + IdlType::from_str("[u8; 50_000]").unwrap(), + IdlType::Array(Box::new(IdlType::U8), IdlArrayLen::Value(50000)) + ); + } + + #[test] + fn multidimensional_array() { + assert_eq!( + IdlType::from_str("[[u8; 16]; 32]").unwrap(), + IdlType::Array( + Box::new(IdlType::Array( + Box::new(IdlType::U8), + IdlArrayLen::Value(16) + )), + IdlArrayLen::Value(32) + ) + ); + } + + #[test] + fn generic_array() { + assert_eq!( + IdlType::from_str("[u64; T]").unwrap(), + IdlType::Array(Box::new(IdlType::U64), IdlArrayLen::Generic("T".into())) + ); + } + + #[test] + fn defined() { + assert_eq!( + IdlType::from_str("MyStruct").unwrap(), + IdlType::Defined { + name: "MyStruct".into(), + generics: vec![] + } + ) + } + + #[test] + fn defined_with_generics() { + assert_eq!( + IdlType::from_str("MyStruct").unwrap(), + IdlType::Defined { + name: "MyStruct".into(), + generics: vec![ + IdlGenericArg::Type { + ty: IdlType::Pubkey + }, + IdlGenericArg::Type { ty: IdlType::U64 }, + IdlGenericArg::Const { value: "8".into() }, + ], + } + ) + } } diff --git a/lang/syn/src/lib.rs b/lang/syn/src/lib.rs index 7249d4dc3c..4fc1182453 100644 --- a/lang/syn/src/lib.rs +++ b/lang/syn/src/lib.rs @@ -11,7 +11,6 @@ pub mod hash; #[cfg(not(feature = "hash"))] pub(crate) mod hash; -use crate::parser::tts_to_string; use codegen::accounts as accounts_codegen; use codegen::program as program_codegen; use parser::accounts as accounts_parser; @@ -175,7 +174,7 @@ impl AccountsStruct { let matching_field = self .fields .iter() - .find(|f| *f.ident() == tts_to_string(field)); + .find(|f| *f.ident() == parser::tts_to_string(field)); if let Some(matching_field) = matching_field { matching_field.is_optional() } else { diff --git a/lang/syn/src/parser/context.rs b/lang/syn/src/parser/context.rs index 502c71a429..ea0e4ee077 100644 --- a/lang/syn/src/parser/context.rs +++ b/lang/syn/src/parser/context.rs @@ -1,4 +1,4 @@ -use anyhow::anyhow; +use anyhow::{anyhow, Result}; use std::collections::BTreeMap; use std::path::{Path, PathBuf}; use syn::parse::{Error as ParseError, Result as ParseResult}; @@ -12,6 +12,12 @@ pub struct CrateContext { } impl CrateContext { + pub fn parse(root: impl AsRef) -> Result { + Ok(CrateContext { + modules: ParsedModule::parse_recursive(root.as_ref())?, + }) + } + pub fn consts(&self) -> impl Iterator { self.modules.iter().flat_map(|(_, ctx)| ctx.consts()) } @@ -42,16 +48,10 @@ impl CrateContext { } } - pub fn parse(root: impl AsRef) -> Result { - Ok(CrateContext { - modules: ParsedModule::parse_recursive(root.as_ref())?, - }) - } - // Perform Anchor safety checks on the parsed create - pub fn safety_checks(&self) -> Result<(), anyhow::Error> { + pub fn safety_checks(&self) -> Result<()> { // Check all structs for unsafe field types, i.e. AccountInfo and UncheckedAccount. - for (_, ctx) in self.modules.iter() { + for ctx in self.modules.values() { for unsafe_field in ctx.unsafe_struct_fields() { // Check if unsafe field type has been documented with a /// SAFETY: doc string. let is_documented = unsafe_field.attrs.iter().any(|attr| { @@ -104,8 +104,15 @@ struct ParsedModule { items: Vec, } +struct UnparsedModule { + file: PathBuf, + path: String, + name: String, + item: syn::ItemMod, +} + impl ParsedModule { - fn parse_recursive(root: &Path) -> Result, anyhow::Error> { + fn parse_recursive(root: &Path) -> Result> { let mut modules = BTreeMap::new(); let root_content = std::fs::read_to_string(root)?; @@ -117,35 +124,13 @@ impl ParsedModule { root_file.items, ); - struct UnparsedModule { - file: PathBuf, - path: String, - name: String, - item: syn::ItemMod, - } - - let mut unparsed = root_mod - .submodules() - .map(|item| UnparsedModule { - file: root_mod.file.clone(), - path: root_mod.path.clone(), - name: item.ident.to_string(), - item: item.clone(), - }) - .collect::>(); - + let mut unparsed = root_mod.unparsed_submodules(); while let Some(to_parse) = unparsed.pop() { let path = format!("{}::{}", to_parse.path, to_parse.name); - let name = to_parse.name; let module = Self::from_item_mod(&to_parse.file, &path, to_parse.item)?; - unparsed.extend(module.submodules().map(|item| UnparsedModule { - item: item.clone(), - file: module.file.clone(), - path: module.path.clone(), - name: item.ident.to_string(), - })); - modules.insert(format!("{}{}", module.path.clone(), name.clone()), module); + unparsed.extend(module.unparsed_submodules()); + modules.insert(format!("{}{}", module.path, to_parse.name), module); } modules.insert(root_mod.name.clone(), root_mod); @@ -209,6 +194,17 @@ impl ParsedModule { } } + fn unparsed_submodules(&self) -> Vec { + self.submodules() + .map(|item| UnparsedModule { + file: self.file.clone(), + path: self.path.clone(), + name: item.ident.to_string(), + item: item.clone(), + }) + .collect() + } + fn submodules(&self) -> impl Iterator { self.items.iter().filter_map(|i| match i { syn::Item::Mod(item) => Some(item), @@ -259,6 +255,13 @@ impl ParsedModule { }) } + fn type_aliases(&self) -> impl Iterator { + self.items.iter().filter_map(|i| match i { + syn::Item::Type(item) => Some(item), + _ => None, + }) + } + fn consts(&self) -> impl Iterator { self.items.iter().filter_map(|i| match i { syn::Item::Const(item) => Some(item), @@ -297,11 +300,4 @@ impl ParsedModule { }) .flatten() } - - fn type_aliases(&self) -> impl Iterator { - self.items.iter().filter_map(|i| match i { - syn::Item::Type(item) => Some(item), - _ => None, - }) - } } diff --git a/lang/syn/src/parser/mod.rs b/lang/syn/src/parser/mod.rs index b586eaf819..6106d1175f 100644 --- a/lang/syn/src/parser/mod.rs +++ b/lang/syn/src/parser/mod.rs @@ -6,7 +6,5 @@ pub mod program; pub mod spl_interface; pub fn tts_to_string(item: T) -> String { - let mut tts = proc_macro2::TokenStream::new(); - item.to_tokens(&mut tts); - tts.to_string() + item.to_token_stream().to_string() } diff --git a/spl/src/governance.rs b/spl/src/governance.rs index 351351529d..1e5f6eac16 100644 --- a/spl/src/governance.rs +++ b/spl/src/governance.rs @@ -57,5 +57,10 @@ macro_rules! vote_weight_record { #[cfg(feature = "idl-build")] impl anchor_lang::IdlBuild for VoterWeightRecord {} + + #[cfg(feature = "idl-build")] + impl anchor_lang::Discriminator for VoterWeightRecord { + const DISCRIMINATOR: [u8; 8] = [0; 8]; + } }; } diff --git a/spl/src/idl_build.rs b/spl/src/idl_build.rs new file mode 100644 index 0000000000..993f747a4b --- /dev/null +++ b/spl/src/idl_build.rs @@ -0,0 +1,32 @@ +/// Crate a default [`anchor_lang::IdlBuild`] implementation for the given type. +/// +/// This is used in order to make wrapper accounts of `anchor-spl` work with `idl-build` feature. +macro_rules! impl_idl_build { + ($ty: ty) => { + impl anchor_lang::IdlBuild for $ty {} + + // This is not used for the IDL generation since default `IdlBuild` impl doesn't include + // the type in the IDL but it stil needs to be added in order to make compilation work. + // + // TODO: Find a better way to handle discriminators of wrapped external accounts. + impl anchor_lang::Discriminator for $ty { + const DISCRIMINATOR: [u8; 8] = [0; 8]; + } + }; +} + +#[cfg(feature = "metadata")] +impl_idl_build!(crate::metadata::MetadataAccount); +#[cfg(feature = "metadata")] +impl_idl_build!(crate::metadata::MasterEditionAccount); +#[cfg(feature = "metadata")] +impl_idl_build!(crate::metadata::TokenRecordAccount); + +#[cfg(feature = "stake")] +impl_idl_build!(crate::stake::StakeAccount); + +impl_idl_build!(crate::token::Mint); +impl_idl_build!(crate::token::TokenAccount); + +impl_idl_build!(crate::token_interface::Mint); +impl_idl_build!(crate::token_interface::TokenAccount); diff --git a/spl/src/lib.rs b/spl/src/lib.rs index 5e2a01c610..a4e2afec23 100644 --- a/spl/src/lib.rs +++ b/spl/src/lib.rs @@ -31,3 +31,6 @@ pub mod metadata; #[cfg(feature = "memo")] pub mod memo; + +#[cfg(feature = "idl-build")] +mod idl_build; diff --git a/spl/src/metadata.rs b/spl/src/metadata.rs index 8be64cff1b..94d54c1fc4 100644 --- a/spl/src/metadata.rs +++ b/spl/src/metadata.rs @@ -795,9 +795,6 @@ impl Deref for MetadataAccount { } } -#[cfg(feature = "idl-build")] -impl anchor_lang::IdlBuild for MetadataAccount {} - #[derive(Clone, Debug, PartialEq)] pub struct MasterEditionAccount(mpl_token_metadata::accounts::MasterEdition); @@ -831,9 +828,6 @@ impl anchor_lang::Owner for MasterEditionAccount { } } -#[cfg(feature = "idl-build")] -impl anchor_lang::IdlBuild for MasterEditionAccount {} - #[derive(Clone, Debug, PartialEq)] pub struct TokenRecordAccount(mpl_token_metadata::accounts::TokenRecord); @@ -870,9 +864,6 @@ impl Deref for TokenRecordAccount { } } -#[cfg(feature = "idl-build")] -impl anchor_lang::IdlBuild for TokenRecordAccount {} - #[derive(Clone)] pub struct Metadata; diff --git a/spl/src/stake.rs b/spl/src/stake.rs index 0bdc6d32ed..ce0659c48e 100644 --- a/spl/src/stake.rs +++ b/spl/src/stake.rs @@ -156,9 +156,6 @@ impl Deref for StakeAccount { } } -#[cfg(feature = "idl-build")] -impl anchor_lang::IdlBuild for StakeAccount {} - #[derive(Clone)] pub struct Stake; diff --git a/spl/src/token.rs b/spl/src/token.rs index 072401f5f5..3e8fe0efde 100644 --- a/spl/src/token.rs +++ b/spl/src/token.rs @@ -471,9 +471,6 @@ impl Deref for TokenAccount { } } -#[cfg(feature = "idl-build")] -impl anchor_lang::IdlBuild for TokenAccount {} - #[derive(Clone, Debug, Default, PartialEq)] pub struct Mint(spl_token::state::Mint); @@ -505,9 +502,6 @@ impl Deref for Mint { } } -#[cfg(feature = "idl-build")] -impl anchor_lang::IdlBuild for Mint {} - #[derive(Clone)] pub struct Token; diff --git a/spl/src/token_interface.rs b/spl/src/token_interface.rs index 59675e13aa..977f0c68e0 100644 --- a/spl/src/token_interface.rs +++ b/spl/src/token_interface.rs @@ -1,6 +1,8 @@ use anchor_lang::solana_program::pubkey::Pubkey; use std::ops::Deref; +pub use crate::token_2022::*; + static IDS: [Pubkey; 2] = [spl_token::ID, spl_token_2022::ID]; #[derive(Clone, Debug, Default, PartialEq)] @@ -32,9 +34,6 @@ impl Deref for TokenAccount { } } -#[cfg(feature = "idl-build")] -impl anchor_lang::IdlBuild for TokenAccount {} - #[derive(Clone, Debug, Default, PartialEq)] pub struct Mint(spl_token_2022::state::Mint); @@ -62,9 +61,6 @@ impl Deref for Mint { } } -#[cfg(feature = "idl-build")] -impl anchor_lang::IdlBuild for Mint {} - #[derive(Clone)] pub struct TokenInterface; @@ -73,5 +69,3 @@ impl anchor_lang::Ids for TokenInterface { &IDS } } - -pub use crate::token_2022::*; diff --git a/tests/anchor-cli-account/programs/account-command/Cargo.toml b/tests/anchor-cli-account/programs/account-command/Cargo.toml index a078db6a90..9ea040e518 100644 --- a/tests/anchor-cli-account/programs/account-command/Cargo.toml +++ b/tests/anchor-cli-account/programs/account-command/Cargo.toml @@ -14,6 +14,7 @@ no-idl = [] no-log-ix-name = [] cpi = ["no-entrypoint"] default = [] +idl-build = ["anchor-lang/idl-build"] [dependencies] anchor-lang = { path = "../../../../lang" } diff --git a/tests/anchor-cli-account/tests/account.ts b/tests/anchor-cli-account/tests/account.ts index 913d8f5d14..c6754cc546 100644 --- a/tests/anchor-cli-account/tests/account.ts +++ b/tests/anchor-cli-account/tests/account.ts @@ -55,9 +55,9 @@ describe("Test CLI account commands", () => { await sleep(5000); } - assert(output.balance == balance, "Balance deserialized incorrectly"); + assert(output.balance === balance, "Balance deserialized incorrectly"); assert( - output.delegatePubkey == provider.wallet.publicKey, + output.delegate_pubkey === provider.wallet.publicKey.toBase58(), "delegatePubkey deserialized incorrectly" ); assert( diff --git a/tests/anchor-cli-idl/programs/idl-commands-one/Cargo.toml b/tests/anchor-cli-idl/programs/idl-commands-one/Cargo.toml index 0da2c05f9c..6d54188882 100644 --- a/tests/anchor-cli-idl/programs/idl-commands-one/Cargo.toml +++ b/tests/anchor-cli-idl/programs/idl-commands-one/Cargo.toml @@ -14,6 +14,7 @@ no-idl = [] no-log-ix-name = [] cpi = ["no-entrypoint"] default = [] +idl-build = ["anchor-lang/idl-build"] [dependencies] anchor-lang = { path = "../../../../lang" } diff --git a/tests/anchor-cli-idl/programs/idl-commands-two/Cargo.toml b/tests/anchor-cli-idl/programs/idl-commands-two/Cargo.toml index 94a8ea6c9d..55e6cb8965 100644 --- a/tests/anchor-cli-idl/programs/idl-commands-two/Cargo.toml +++ b/tests/anchor-cli-idl/programs/idl-commands-two/Cargo.toml @@ -14,6 +14,7 @@ no-idl = [] no-log-ix-name = [] cpi = ["no-entrypoint"] default = [] +idl-build = ["anchor-lang/idl-build"] [dependencies] anchor-lang = { path = "../../../../lang" } diff --git a/tests/anchor-cli-idl/test.sh b/tests/anchor-cli-idl/test.sh index 3a351dcf5b..1bf535253b 100755 --- a/tests/anchor-cli-idl/test.sh +++ b/tests/anchor-cli-idl/test.sh @@ -8,8 +8,12 @@ RANDOM_DATA=$(openssl rand -base64 $((10*1680)) | sed 's/.*/"&",/') # Create the JSON object with the "docs" field containing random data echo '{ - "version": "0.1.0", - "name": "idl_commands_one", + "address": "2uA3amp95zsEHUpo8qnLMhcFAUsiKVEcKHXS1JetFjU5", + "metadata": { + "name": "idl_commands_one", + "version": "0.1.0", + "spec": "0.1.0" + }, "instructions": [ { "name": "initialize", @@ -17,6 +21,7 @@ echo '{ '"$RANDOM_DATA"' "trailing comma begone" ], + "discriminator": [], "accounts": [], "args": [] } diff --git a/tests/anchor-cli-idl/tests/idl.ts b/tests/anchor-cli-idl/tests/idl.ts index b50e6d506e..9c7f56f501 100644 --- a/tests/anchor-cli-idl/tests/idl.ts +++ b/tests/anchor-cli-idl/tests/idl.ts @@ -24,12 +24,12 @@ describe("Test CLI IDL commands", () => { it("Can fetch an IDL using the TypeScript client", async () => { const idl = await anchor.Program.fetchIdl(programOne.programId, provider); - assert.deepEqual(idl, programOne.idl); + assert.deepEqual(idl, programOne.rawIdl); }); it("Can fetch an IDL via the CLI", async () => { const idl = execSync(`anchor idl fetch ${programOne.programId}`).toString(); - assert.deepEqual(JSON.parse(idl), programOne.idl); + assert.deepEqual(JSON.parse(idl), programOne.rawIdl); }); it("Can write a new IDL using the upgrade command", async () => { @@ -39,7 +39,7 @@ describe("Test CLI IDL commands", () => { { stdio: "inherit" } ); const idl = await anchor.Program.fetchIdl(programOne.programId, provider); - assert.deepEqual(idl, programTwo.idl); + assert.deepEqual(idl, programTwo.rawIdl); }); it("Can write a new IDL using write-buffer and set-buffer", async () => { @@ -53,7 +53,7 @@ describe("Test CLI IDL commands", () => { { stdio: "inherit" } ); const idl = await anchor.Program.fetchIdl(programOne.programId, provider); - assert.deepEqual(idl, programOne.idl); + assert.deepEqual(idl, programOne.rawIdl); }); it("Can fetch an IDL authority via the CLI", async () => { diff --git a/tests/auction-house/programs/auction-house/Cargo.toml b/tests/auction-house/programs/auction-house/Cargo.toml index d04d6803de..2029f1932c 100644 --- a/tests/auction-house/programs/auction-house/Cargo.toml +++ b/tests/auction-house/programs/auction-house/Cargo.toml @@ -14,6 +14,7 @@ no-entrypoint = [] no-idl = [] cpi = ["no-entrypoint"] default = [] +idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] [dependencies] anchor-lang = { path = "../../../../lang" } diff --git a/tests/auction-house/tests/auction-house.ts b/tests/auction-house/tests/auction-house.ts index f7bdf1dcca..f6bb150740 100644 --- a/tests/auction-house/tests/auction-house.ts +++ b/tests/auction-house/tests/auction-house.ts @@ -15,7 +15,9 @@ import { import { u64, Token, TOKEN_PROGRAM_ID } from "@solana/spl-token"; import * as metaplex from "@metaplex/js"; import * as assert from "assert"; -import { IDL, AuctionHouse } from "../target/types/auction_house"; +import { AuctionHouse } from "../target/types/auction_house"; + +const IDL = require("../target/idl/auction_house.json"); const MetadataDataData = metaplex.programs.metadata.MetadataDataData; const CreateMetadata = metaplex.programs.metadata.CreateMetadata; diff --git a/tests/bench/programs/bench/Cargo.toml b/tests/bench/programs/bench/Cargo.toml index d7b162620b..7a349521ae 100644 --- a/tests/bench/programs/bench/Cargo.toml +++ b/tests/bench/programs/bench/Cargo.toml @@ -10,6 +10,7 @@ crate-type = ["cdylib", "lib"] [features] no-entrypoint = [] cpi = ["no-entrypoint"] +idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] [dependencies] anchor-lang = { path = "../../../../lang" } diff --git a/tests/bench/tests/binary-size.ts b/tests/bench/tests/binary-size.ts index 0e60a56697..00f2c081cf 100644 --- a/tests/bench/tests/binary-size.ts +++ b/tests/bench/tests/binary-size.ts @@ -1,15 +1,18 @@ import * as fs from "fs/promises"; import path from "path"; -import { IDL } from "../target/types/bench"; import { BenchData, BinarySize } from "../scripts/utils"; +const IDL = require("../target/idl/bench.json"); + describe("Binary size", () => { const binarySize: BinarySize = {}; it("Measure binary size", async () => { - const stat = await fs.stat(path.join("target", "deploy", `${IDL.name}.so`)); - binarySize[IDL.name] = stat.size; + const stat = await fs.stat( + path.join("target", "deploy", `${IDL.metadata.name}.so`) + ); + binarySize[IDL.metadata.name] = stat.size; }); after(async () => { diff --git a/tests/bench/tests/compute-units.ts b/tests/bench/tests/compute-units.ts index 3465bcf1b8..2a964fdaef 100644 --- a/tests/bench/tests/compute-units.ts +++ b/tests/bench/tests/compute-units.ts @@ -1,7 +1,7 @@ import * as anchor from "@coral-xyz/anchor"; import * as token from "@coral-xyz/spl-token"; -import { Bench, IDL } from "../target/types/bench"; +import { Bench } from "../target/types/bench"; import { BenchData, ComputeUnits } from "../scripts/utils"; describe("Compute units", () => { @@ -31,7 +31,7 @@ describe("Compute units", () => { for (const accountCount of options.accountCounts) { // Check whether the init version of the instruction exists const ixNameInit = `${ixName}Init`; - const hasInitVersion = IDL.instructions.some((ix) => + const hasInitVersion = program.idl.instructions.some((ix) => ix.name.startsWith(ixNameInit) ); @@ -53,7 +53,7 @@ describe("Compute units", () => { signers.splice(0); } - for (const ix of IDL.instructions) { + for (const ix of program.idl.instructions) { if (ix.name !== method) continue; for (const account of ix.accounts) { @@ -80,7 +80,7 @@ describe("Compute units", () => { const keypair = options.generateKeypair(account.name); accounts[account.name] = keypair.publicKey; - if (account.isSigner) { + if (account.signer) { signers.push(keypair); } } diff --git a/tests/bench/tests/stack-memory.ts b/tests/bench/tests/stack-memory.ts index caad67b643..9605540fbe 100644 --- a/tests/bench/tests/stack-memory.ts +++ b/tests/bench/tests/stack-memory.ts @@ -2,7 +2,8 @@ import path from "path"; import fs from "fs/promises"; import { BenchData, StackMemory, spawn } from "../scripts/utils"; -import { IDL } from "../target/types/bench"; + +const IDL = require("../target/idl/bench.json"); describe("Stack memory", () => { const stackMemory: StackMemory = {}; @@ -15,7 +16,7 @@ describe("Stack memory", () => { ].join("\n\t\t"); it("Measure stack memory usage", async () => { - const libPath = path.join("programs", IDL.name, "src", "lib.rs"); + const libPath = path.join("programs", IDL.metadata.name, "src", "lib.rs"); const lib = await fs.readFile(libPath, "utf8"); const indices = [...lib.matchAll(/fn\s[\w\d]+\(/g)] .map((match) => match.index) diff --git a/tests/bpf-upgradeable-state/programs/bpf-upgradeable-state/Cargo.toml b/tests/bpf-upgradeable-state/programs/bpf-upgradeable-state/Cargo.toml index 4a81349c7c..8b45db41c7 100644 --- a/tests/bpf-upgradeable-state/programs/bpf-upgradeable-state/Cargo.toml +++ b/tests/bpf-upgradeable-state/programs/bpf-upgradeable-state/Cargo.toml @@ -14,6 +14,7 @@ no-entrypoint = [] no-idl = [] cpi = ["no-entrypoint"] default = [] +idl-build = ["anchor-lang/idl-build"] [dependencies] anchor-lang = { path = "../../../../lang" } diff --git a/tests/bpf-upgradeable-state/programs/bpf-upgradeable-state/src/lib.rs b/tests/bpf-upgradeable-state/programs/bpf-upgradeable-state/src/lib.rs index 53fdf43592..9734695285 100644 --- a/tests/bpf-upgradeable-state/programs/bpf-upgradeable-state/src/lib.rs +++ b/tests/bpf-upgradeable-state/programs/bpf-upgradeable-state/src/lib.rs @@ -1,5 +1,7 @@ use anchor_lang::prelude::*; +use crate::program::BpfUpgradeableState; + declare_id!("Cum9tTyj5HwcEiAmhgaS7Bbj4UczCwsucrCkxRECzM4e"); #[program] @@ -73,7 +75,7 @@ pub struct SetAdminSettingsUseProgramState<'info> { #[account(mut)] pub authority: Signer<'info>, #[account(constraint = program.programdata_address()? == Some(program_data.key()))] - pub program: Program<'info, crate::program::BpfUpgradeableState>, + pub program: Program<'info, BpfUpgradeableState>, #[account(constraint = program_data.upgrade_authority_address == Some(authority.key()))] pub program_data: Account<'info, ProgramData>, pub system_program: Program<'info, System>, diff --git a/tests/cashiers-check/programs/cashiers-check/Cargo.toml b/tests/cashiers-check/programs/cashiers-check/Cargo.toml index da6bb2d3cb..b23df1672b 100644 --- a/tests/cashiers-check/programs/cashiers-check/Cargo.toml +++ b/tests/cashiers-check/programs/cashiers-check/Cargo.toml @@ -14,6 +14,7 @@ no-entrypoint = [] no-idl = [] cpi = ["no-entrypoint"] default = [] +idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] [dependencies] anchor-lang = { path = "../../../../lang" } diff --git a/tests/chat/programs/chat/Cargo.toml b/tests/chat/programs/chat/Cargo.toml index 93da0ea8f7..a6a9d7f965 100644 --- a/tests/chat/programs/chat/Cargo.toml +++ b/tests/chat/programs/chat/Cargo.toml @@ -14,6 +14,7 @@ no-entrypoint = [] no-idl = [] cpi = ["no-entrypoint"] default = [] +idl-build = ["anchor-lang/idl-build"] [dependencies] anchor-lang = { path = "../../../../lang" } diff --git a/tests/composite/programs/composite/Cargo.toml b/tests/composite/programs/composite/Cargo.toml index 211064b84f..c01c3755a4 100644 --- a/tests/composite/programs/composite/Cargo.toml +++ b/tests/composite/programs/composite/Cargo.toml @@ -12,6 +12,7 @@ name = "composite" [features] no-entrypoint = [] cpi = ["no-entrypoint"] +idl-build = ["anchor-lang/idl-build"] [dependencies] anchor-lang = { path = "../../../../lang" } diff --git a/tests/cpi-returns/programs/callee/Cargo.toml b/tests/cpi-returns/programs/callee/Cargo.toml index c964a4aa4d..6e98dbefdf 100644 --- a/tests/cpi-returns/programs/callee/Cargo.toml +++ b/tests/cpi-returns/programs/callee/Cargo.toml @@ -14,6 +14,7 @@ no-idl = [] no-log-ix-name = [] cpi = ["no-entrypoint"] default = [] +idl-build = ["anchor-lang/idl-build"] [dependencies] anchor-lang = { path = "../../../../lang", features = ["init-if-needed"] } diff --git a/tests/cpi-returns/programs/caller/Cargo.toml b/tests/cpi-returns/programs/caller/Cargo.toml index 2aaa551085..b898da71bd 100644 --- a/tests/cpi-returns/programs/caller/Cargo.toml +++ b/tests/cpi-returns/programs/caller/Cargo.toml @@ -14,6 +14,7 @@ no-idl = [] no-log-ix-name = [] cpi = ["no-entrypoint"] default = [] +idl-build = ["anchor-lang/idl-build"] [dependencies] anchor-lang = { path = "../../../../lang", features = ["init-if-needed"] } diff --git a/tests/cpi-returns/tests/cpi-return.ts b/tests/cpi-returns/tests/cpi-return.ts index 22217577d4..0099ef45a2 100644 --- a/tests/cpi-returns/tests/cpi-return.ts +++ b/tests/cpi-returns/tests/cpi-return.ts @@ -159,7 +159,7 @@ describe("CPI return", () => { (f) => f.name == "returnStruct" ); assert.deepStrictEqual(returnStructInstruction.returns, { - defined: "StructReturn", + defined: { name: "structReturn" }, }); }); diff --git a/tests/custom-coder/programs/native-system/Cargo.toml b/tests/custom-coder/programs/native-system/Cargo.toml index ec46d25869..627a5567de 100644 --- a/tests/custom-coder/programs/native-system/Cargo.toml +++ b/tests/custom-coder/programs/native-system/Cargo.toml @@ -15,6 +15,7 @@ no-idl = [] no-log-ix-name = [] cpi = ["no-entrypoint"] default = [] +idl-build = ["anchor-lang/idl-build"] [dependencies] anchor-lang = { path = "../../../../lang" } diff --git a/tests/custom-coder/programs/spl-associated-token/Cargo.toml b/tests/custom-coder/programs/spl-associated-token/Cargo.toml index 3d58f79d80..bab72f0f1c 100644 --- a/tests/custom-coder/programs/spl-associated-token/Cargo.toml +++ b/tests/custom-coder/programs/spl-associated-token/Cargo.toml @@ -14,6 +14,7 @@ no-idl = [] no-log-ix-name = [] cpi = ["no-entrypoint"] default = [] +idl-build = ["anchor-lang/idl-build"] [profile.release] overflow-checks = true diff --git a/tests/custom-coder/programs/spl-token/Cargo.toml b/tests/custom-coder/programs/spl-token/Cargo.toml index 39ee2c7839..06a9b1543c 100644 --- a/tests/custom-coder/programs/spl-token/Cargo.toml +++ b/tests/custom-coder/programs/spl-token/Cargo.toml @@ -15,6 +15,7 @@ no-idl = [] no-log-ix-name = [] cpi = ["no-entrypoint"] default = [] +idl-build = ["anchor-lang/idl-build"] [dependencies] anchor-lang = { path = "../../../../lang" } diff --git a/tests/declare-id/programs/declare-id/Cargo.toml b/tests/declare-id/programs/declare-id/Cargo.toml index 0bbbab6119..a22cb4601b 100644 --- a/tests/declare-id/programs/declare-id/Cargo.toml +++ b/tests/declare-id/programs/declare-id/Cargo.toml @@ -11,6 +11,7 @@ name = "declare_id" [features] no-entrypoint = [] cpi = ["no-entrypoint"] +idl-build = ["anchor-lang/idl-build"] [dependencies] anchor-lang = { path = "../../../../lang" } diff --git a/tests/docs/programs/docs/Cargo.toml b/tests/docs/programs/docs/Cargo.toml index debe6017ed..b04efa0770 100644 --- a/tests/docs/programs/docs/Cargo.toml +++ b/tests/docs/programs/docs/Cargo.toml @@ -11,6 +11,7 @@ name = "errors" [features] no-entrypoint = [] cpi = ["no-entrypoint"] +idl-build = ["anchor-lang/idl-build"] [dependencies] anchor-lang = { path = "../../../../lang" } diff --git a/tests/errors/programs/errors/Cargo.toml b/tests/errors/programs/errors/Cargo.toml index de51fd93e3..f0f5df0fe7 100644 --- a/tests/errors/programs/errors/Cargo.toml +++ b/tests/errors/programs/errors/Cargo.toml @@ -12,6 +12,7 @@ name = "errors" [features] no-entrypoint = [] cpi = ["no-entrypoint"] +idl-build = ["anchor-lang/idl-build"] [dependencies] anchor-lang = { path = "../../../../lang" } diff --git a/tests/errors/tests/errors.ts b/tests/errors/tests/errors.ts index b7066d7d17..9598bcc0a6 100644 --- a/tests/errors/tests/errors.ts +++ b/tests/errors/tests/errors.ts @@ -258,7 +258,7 @@ describe("errors", () => { }, ], programId: program.programId, - data: program.coder.instruction.encode("signer_error", {}), + data: program.coder.instruction.encode("signerError", {}), }) ); await program.provider.sendAndConfirm(tx); diff --git a/tests/escrow/programs/escrow/Cargo.toml b/tests/escrow/programs/escrow/Cargo.toml index 1a75dd327c..2fcf3aa297 100644 --- a/tests/escrow/programs/escrow/Cargo.toml +++ b/tests/escrow/programs/escrow/Cargo.toml @@ -14,6 +14,7 @@ no-entrypoint = [] no-idl = [] cpi = ["no-entrypoint"] default = [] +idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] [dependencies] anchor-lang = { path = "../../../../lang" } diff --git a/tests/events/programs/events/Cargo.toml b/tests/events/programs/events/Cargo.toml index 7b00b1a898..ce53979d1b 100644 --- a/tests/events/programs/events/Cargo.toml +++ b/tests/events/programs/events/Cargo.toml @@ -14,6 +14,7 @@ no-entrypoint = [] no-idl = [] cpi = ["no-entrypoint"] default = [] +idl-build = ["anchor-lang/idl-build"] [dependencies] anchor-lang = { path = "../../../../lang", features = ["event-cpi"] } diff --git a/tests/events/tests/events.ts b/tests/events/tests/events.ts index d342948d88..7cd2333a78 100644 --- a/tests/events/tests/events.ts +++ b/tests/events/tests/events.ts @@ -27,15 +27,15 @@ describe("Events", () => { describe("Normal event", () => { it("Single event works", async () => { - const event = await getEvent("MyEvent", "initialize"); + const event = await getEvent("myEvent", "initialize"); assert.strictEqual(event.data.toNumber(), 5); assert.strictEqual(event.label, "hello"); }); it("Multiple events work", async () => { - const eventOne = await getEvent("MyEvent", "initialize"); - const eventTwo = await getEvent("MyOtherEvent", "testEvent"); + const eventOne = await getEvent("myEvent", "initialize"); + const eventTwo = await getEvent("myOtherEvent", "testEvent"); assert.strictEqual(eventOne.data.toNumber(), 5); assert.strictEqual(eventOne.label, "hello"); @@ -61,7 +61,7 @@ describe("Events", () => { const eventData = anchor.utils.bytes.base64.encode(ixData.slice(8)); const event = program.coder.events.decode(eventData); - assert.strictEqual(event.name, "MyOtherEvent"); + assert.strictEqual(event.name, "myOtherEvent"); assert.strictEqual(event.data.label, "cpi"); assert.strictEqual((event.data.data as anchor.BN).toNumber(), 7); }); diff --git a/tests/floats/programs/floats/Cargo.toml b/tests/floats/programs/floats/Cargo.toml index c9a4a2b4ab..77d8ca37c1 100644 --- a/tests/floats/programs/floats/Cargo.toml +++ b/tests/floats/programs/floats/Cargo.toml @@ -14,6 +14,7 @@ no-idl = [] no-log-ix-name = [] cpi = ["no-entrypoint"] default = [] +idl-build = ["anchor-lang/idl-build"] [dependencies] anchor-lang = { path = "../../../../lang" } diff --git a/tests/idl/Anchor.toml b/tests/idl/Anchor.toml index eed2cd3684..d5f7fcc491 100644 --- a/tests/idl/Anchor.toml +++ b/tests/idl/Anchor.toml @@ -1,16 +1,15 @@ [features] -seeds = true +skip-lint = true [programs.localnet] -client_interactions = "C1ient1nteractions1111111111111111111111111" docs = "Docs111111111111111111111111111111111111111" external = "Externa1111111111111111111111111111111111111" generics = "Generics111111111111111111111111111111111111" idl = "id11111111111111111111111111111111111111111" -idl_build_features = "id1Bui1dFeatures111111111111111111111111111" relations_derivation = "Re1ationsDerivation111111111111111111111111" non_existent = { address = "NonExistent11111111111111111111111111111111", idl = "non-existent.json" } -numbers_123 = { address = "Numbers111111111111111111111111111111111111", idl = "idls/relations_build.json" } +numbers_123 = { address = "Numbers111111111111111111111111111111111111", idl = "idls/relations.json" } +new_idl = "Newid11111111111111111111111111111111111111" [provider] cluster = "localnet" diff --git a/tests/idl/generate.sh b/tests/idl/generate.sh index 99c792d2e9..075da596fb 100755 --- a/tests/idl/generate.sh +++ b/tests/idl/generate.sh @@ -8,11 +8,10 @@ else fi cd programs/idl -anchor idl parse --file src/lib.rs -o $dir/parse.json -anchor idl build -o $dir/build.json +anchor idl build -o $dir/new.json cd ../generics -anchor idl build -o $dir/generics_build.json +anchor idl build -o $dir/generics.json cd ../relations-derivation -anchor idl build -o $dir/relations_build.json \ No newline at end of file +anchor idl build -o $dir/relations.json diff --git a/tests/idl/idls/generics_build.json b/tests/idl/idls/generics.json similarity index 66% rename from tests/idl/idls/generics_build.json rename to tests/idl/idls/generics.json index a3fda000c3..8fb229925e 100644 --- a/tests/idl/idls/generics_build.json +++ b/tests/idl/idls/generics.json @@ -1,40 +1,55 @@ { - "version": "0.1.0", - "name": "generics", + "address": "Generics111111111111111111111111111111111111", + "metadata": { + "name": "generics", + "version": "0.1.0", + "spec": "0.1.0", + "description": "Created with Anchor" + }, "instructions": [ { "name": "generic", + "discriminator": [ + 63, + 235, + 150, + 148, + 7, + 255, + 185, + 159 + ], "accounts": [ { - "name": "genericAcc", - "isMut": false, - "isSigner": false + "name": "generic_acc" }, { "name": "payer", - "isMut": true, - "isSigner": true + "writable": true, + "signer": true }, { - "name": "systemProgram", - "isMut": false, - "isSigner": false + "name": "system_program", + "address": "11111111111111111111111111111111" } ], "args": [ { - "name": "genericField", + "name": "generic_field", "type": { - "definedWithTypeArgs": { + "defined": { "name": "GenericType", - "args": [ + "generics": [ { + "kind": "type", "type": "u32" }, { + "kind": "type", "type": "u64" }, { + "kind": "const", "value": "10" } ] @@ -45,6 +60,21 @@ } ], "accounts": [ + { + "name": "GenericAccount", + "discriminator": [ + 10, + 71, + 68, + 49, + 51, + 72, + 147, + 245 + ] + } + ], + "types": [ { "name": "GenericAccount", "type": { @@ -53,16 +83,19 @@ { "name": "data", "type": { - "definedWithTypeArgs": { + "defined": { "name": "GenericType", - "args": [ + "generics": [ { + "kind": "type", "type": "u32" }, { + "kind": "type", "type": "u64" }, { + "kind": "const", "value": "10" } ] @@ -71,27 +104,23 @@ } ] } - } - ], - "types": [ - { - "name": "Baz", - "type": { - "kind": "struct", - "fields": [ - { - "name": "someField", - "type": "u8" - } - ] - } }, { "name": "GenericEnum", "generics": [ - "T", - "U", - "N" + { + "kind": "type", + "name": "T" + }, + { + "kind": "type", + "name": "U" + }, + { + "kind": "const", + "name": "N", + "type": "usize" + } ], "type": { "kind": "enum", @@ -128,15 +157,17 @@ "name": "Struct", "fields": [ { - "definedWithTypeArgs": { + "defined": { "name": "GenericNested", - "args": [ + "generics": [ { + "kind": "type", "type": { "generic": "T" } }, { + "kind": "type", "type": { "generic": "U" } @@ -150,11 +181,13 @@ "name": "Arr", "fields": [ { - "genericLenArray": [ + "array": [ { "generic": "T" }, - "N" + { + "generic": "N" + } ] } ] @@ -165,8 +198,14 @@ { "name": "GenericNested", "generics": [ - "V", - "Z" + { + "kind": "type", + "name": "V" + }, + { + "kind": "type", + "name": "Z" + } ], "type": { "kind": "struct", @@ -189,9 +228,19 @@ { "name": "GenericType", "generics": [ - "T", - "U", - "N" + { + "kind": "type", + "name": "T" + }, + { + "kind": "type", + "name": "U" + }, + { + "kind": "const", + "name": "N", + "type": "usize" + } ], "type": { "kind": "struct", @@ -211,13 +260,15 @@ { "name": "gen3", "type": { - "definedWithTypeArgs": { + "defined": { "name": "GenericNested", - "args": [ + "generics": [ { + "kind": "type", "type": "u32" }, { + "kind": "type", "type": { "generic": "U" } @@ -229,17 +280,21 @@ { "name": "gen4", "type": { - "definedWithTypeArgs": { + "defined": { "name": "GenericNested", - "args": [ + "generics": [ { + "kind": "type", "type": { "generic": "T" } }, { + "kind": "type", "type": { - "defined": "Baz" + "defined": { + "name": "MyStruct" + } } } ] @@ -249,15 +304,17 @@ { "name": "gen5", "type": { - "definedWithTypeArgs": { + "defined": { "name": "GenericNested", - "args": [ + "generics": [ { + "kind": "type", "type": { "generic": "T" } }, { + "kind": "type", "type": { "generic": "U" } @@ -269,13 +326,15 @@ { "name": "gen6", "type": { - "definedWithTypeArgs": { + "defined": { "name": "GenericNested", - "args": [ + "generics": [ { + "kind": "type", "type": "u32" }, { + "kind": "type", "type": "u64" } ] @@ -285,25 +344,29 @@ { "name": "gen7", "type": { - "definedWithTypeArgs": { + "defined": { "name": "GenericNested", - "args": [ + "generics": [ { + "kind": "type", "type": { "generic": "T" } }, { + "kind": "type", "type": { - "definedWithTypeArgs": { + "defined": { "name": "GenericNested", - "args": [ + "generics": [ { + "kind": "type", "type": { "generic": "T" } }, { + "kind": "type", "type": { "generic": "U" } @@ -319,19 +382,22 @@ { "name": "arr", "type": { - "genericLenArray": [ + "array": [ "u8", - "N" + { + "generic": "N" + } ] } }, { "name": "warr", "type": { - "definedWithTypeArgs": { + "defined": { "name": "WrappedU8Array", - "args": [ + "generics": [ { + "kind": "type", "type": { "generic": "N" } @@ -343,10 +409,11 @@ { "name": "warrval", "type": { - "definedWithTypeArgs": { + "defined": { "name": "WrappedU8Array", - "args": [ + "generics": [ { + "kind": "const", "value": "10" } ] @@ -356,20 +423,23 @@ { "name": "enm1", "type": { - "definedWithTypeArgs": { + "defined": { "name": "GenericEnum", - "args": [ + "generics": [ { + "kind": "type", "type": { "generic": "T" } }, { + "kind": "type", "type": { "generic": "U" } }, { + "kind": "type", "type": { "generic": "N" } @@ -381,20 +451,23 @@ { "name": "enm2", "type": { - "definedWithTypeArgs": { + "defined": { "name": "GenericEnum", - "args": [ + "generics": [ { + "kind": "type", "type": { - "definedWithTypeArgs": { + "defined": { "name": "GenericNested", - "args": [ + "generics": [ { + "kind": "type", "type": { "generic": "T" } }, { + "kind": "type", "type": "u64" } ] @@ -402,9 +475,11 @@ } }, { + "kind": "type", "type": "u32" }, { + "kind": "const", "value": "30" } ] @@ -413,6 +488,34 @@ } ] } + }, + { + "name": "MyStruct", + "type": { + "kind": "struct", + "fields": [ + { + "name": "some_field", + "type": "u8" + } + ] + } + }, + { + "name": "WrappedU8Array", + "generics": [ + { + "kind": "const", + "name": "N", + "type": "usize" + } + ], + "type": { + "kind": "struct", + "fields": [ + "u8" + ] + } } ] } \ No newline at end of file diff --git a/tests/idl/idls/build.json b/tests/idl/idls/new.json similarity index 54% rename from tests/idl/idls/build.json rename to tests/idl/idls/new.json index 0ced23b634..40ff4f1443 100644 --- a/tests/idl/idls/build.json +++ b/tests/idl/idls/new.json @@ -1,268 +1,266 @@ { - "version": "0.1.0", - "name": "idl", + "address": "id11111111111111111111111111111111111111111", + "metadata": { + "name": "idl", + "version": "0.1.0", + "spec": "0.1.0", + "description": "Created with Anchor" + }, "docs": [ "IDL test program documentation." ], - "constants": [ - { - "name": "BYTES_STR", - "type": "bytes", - "value": "[116, 101, 115, 116]" - }, - { - "name": "BYTE_STR", - "type": "u8", - "value": "116" - }, + "instructions": [ { - "name": "I128", - "type": "i128", - "value": "1000000" + "name": "cause_error", + "discriminator": [ + 67, + 104, + 37, + 17, + 2, + 155, + 68, + 17 + ], + "accounts": [], + "args": [] }, - { - "name": "U8", - "type": "u8", - "value": "6" - } - ], - "instructions": [ { "name": "initialize", + "discriminator": [ + 175, + 175, + 109, + 31, + 13, + 152, + 155, + 237 + ], "accounts": [ { "name": "state", - "isMut": true, - "isSigner": true, "docs": [ "State account" - ] + ], + "writable": true, + "signer": true }, { "name": "nested", "accounts": [ { "name": "clock", - "isMut": false, - "isSigner": false, "docs": [ "Sysvar clock" - ] + ], + "address": "SysvarC1ock11111111111111111111111111111111" }, { "name": "rent", - "isMut": false, - "isSigner": false + "address": "SysvarRent111111111111111111111111111111111" } ] }, { - "name": "zcAccount", - "isMut": false, - "isSigner": false + "name": "zc_account" }, { - "name": "tokenAccount", - "isMut": false, - "isSigner": false + "name": "token_account" }, { - "name": "mintAccount", - "isMut": false, - "isSigner": false + "name": "mint_account" }, { - "name": "tokenInterfaceAccount", - "isMut": false, - "isSigner": false + "name": "token_interface_account" }, { - "name": "mintInterfaceAccount", - "isMut": false, - "isSigner": false + "name": "mint_interface_account" }, { "name": "payer", - "isMut": true, - "isSigner": true + "writable": true, + "signer": true }, { - "name": "systemProgram", - "isMut": false, - "isSigner": false + "name": "system_program", + "address": "11111111111111111111111111111111" } ], "args": [] }, { - "name": "initializeWithValues", + "name": "initialize_with_values", "docs": [ "Initializes an account with specified values" ], + "discriminator": [ + 220, + 73, + 8, + 213, + 178, + 69, + 181, + 141 + ], "accounts": [ { "name": "state", - "isMut": true, - "isSigner": true, "docs": [ "State account" - ] + ], + "writable": true, + "signer": true }, { "name": "nested", "accounts": [ { "name": "clock", - "isMut": false, - "isSigner": false, "docs": [ "Sysvar clock" - ] + ], + "address": "SysvarC1ock11111111111111111111111111111111" }, { "name": "rent", - "isMut": false, - "isSigner": false + "address": "SysvarRent111111111111111111111111111111111" } ] }, { - "name": "zcAccount", - "isMut": false, - "isSigner": false + "name": "zc_account" }, { - "name": "tokenAccount", - "isMut": false, - "isSigner": false + "name": "token_account" }, { - "name": "mintAccount", - "isMut": false, - "isSigner": false + "name": "mint_account" }, { - "name": "tokenInterfaceAccount", - "isMut": false, - "isSigner": false + "name": "token_interface_account" }, { - "name": "mintInterfaceAccount", - "isMut": false, - "isSigner": false + "name": "mint_interface_account" }, { "name": "payer", - "isMut": true, - "isSigner": true + "writable": true, + "signer": true }, { - "name": "systemProgram", - "isMut": false, - "isSigner": false + "name": "system_program", + "address": "11111111111111111111111111111111" } ], "args": [ { - "name": "boolField", + "name": "bool_field", "type": "bool" }, { - "name": "u8Field", + "name": "u8_field", "type": "u8" }, { - "name": "i8Field", + "name": "i8_field", "type": "i8" }, { - "name": "u16Field", + "name": "u16_field", "type": "u16" }, { - "name": "i16Field", + "name": "i16_field", "type": "i16" }, { - "name": "u32Field", + "name": "u32_field", "type": "u32" }, { - "name": "i32Field", + "name": "i32_field", "type": "i32" }, { - "name": "f32Field", + "name": "f32_field", "type": "f32" }, { - "name": "u64Field", + "name": "u64_field", "type": "u64" }, { - "name": "i64Field", + "name": "i64_field", "type": "i64" }, { - "name": "f64Field", + "name": "f64_field", "type": "f64" }, { - "name": "u128Field", + "name": "u128_field", "type": "u128" }, { - "name": "i128Field", + "name": "i128_field", "type": "i128" }, { - "name": "bytesField", + "name": "bytes_field", "type": "bytes" }, { - "name": "stringField", + "name": "string_field", "type": "string" }, { - "name": "pubkeyField", - "type": "publicKey" + "name": "pubkey_field", + "type": "pubkey" }, { - "name": "vecField", + "name": "vec_field", "type": { "vec": "u64" } }, { - "name": "vecStructField", + "name": "vec_struct_field", "type": { "vec": { - "defined": "FooStruct" + "defined": { + "name": "FooStruct" + } } } }, { - "name": "optionField", + "name": "option_field", "type": { "option": "bool" } }, { - "name": "optionStructField", + "name": "option_struct_field", "type": { "option": { - "defined": "FooStruct" + "defined": { + "name": "FooStruct" + } } } }, { - "name": "structField", + "name": "struct_field", "type": { - "defined": "FooStruct" + "defined": { + "name": "FooStruct" + } } }, { - "name": "arrayField", + "name": "array_field", "type": { "array": [ "bool", @@ -271,57 +269,74 @@ } }, { - "name": "enumField1", + "name": "enum_field_1", "type": { - "defined": "FooEnum" + "defined": { + "name": "FooEnum" + } } }, { - "name": "enumField2", + "name": "enum_field_2", "type": { - "defined": "FooEnum" + "defined": { + "name": "FooEnum" + } } }, { - "name": "enumField3", + "name": "enum_field_3", "type": { - "defined": "FooEnum" + "defined": { + "name": "FooEnum" + } } }, { - "name": "enumField4", + "name": "enum_field_4", "type": { - "defined": "FooEnum" + "defined": { + "name": "FooEnum" + } } } ] }, { - "name": "initializeWithValues2", + "name": "initialize_with_values2", "docs": [ "a separate instruction due to initialize_with_values having too many arguments", "https://github.com/solana-labs/solana/issues/23978" ], + "discriminator": [ + 248, + 190, + 21, + 97, + 239, + 148, + 39, + 181 + ], "accounts": [ { "name": "state", - "isMut": true, - "isSigner": true + "writable": true, + "signer": true }, { "name": "payer", - "isMut": true, - "isSigner": true + "writable": true, + "signer": true }, { - "name": "systemProgram", - "isMut": false, - "isSigner": false + "name": "system_program", + "address": "11111111111111111111111111111111" } ], "args": [ { - "name": "vecOfOption", + "name": "vec_of_option", "type": { "vec": { "option": "u64" @@ -329,214 +344,90 @@ } }, { - "name": "boxField", + "name": "box_field", "type": "bool" } ], "returns": { - "defined": "SomeRetStruct" + "defined": { + "name": "SomeRetStruct" + } } - }, - { - "name": "causeError", - "accounts": [], - "args": [] } ], "accounts": [ { "name": "SomeZcAccount", - "type": { - "kind": "struct", - "fields": [ - { - "name": "field", - "type": { - "defined": "ZcStruct" - } - } - ] - } + "discriminator": [ + 56, + 72, + 82, + 194, + 210, + 35, + 17, + 191 + ] }, { "name": "State", - "docs": [ - "An account containing various fields" - ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "boolField", - "docs": [ - "A boolean field" - ], - "type": "bool" - }, - { - "name": "u8Field", - "type": "u8" - }, - { - "name": "i8Field", - "type": "i8" - }, - { - "name": "u16Field", - "type": "u16" - }, - { - "name": "i16Field", - "type": "i16" - }, - { - "name": "u32Field", - "type": "u32" - }, - { - "name": "i32Field", - "type": "i32" - }, - { - "name": "f32Field", - "type": "f32" - }, - { - "name": "u64Field", - "type": "u64" - }, - { - "name": "i64Field", - "type": "i64" - }, - { - "name": "f64Field", - "type": "f64" - }, - { - "name": "u128Field", - "type": "u128" - }, - { - "name": "i128Field", - "type": "i128" - }, - { - "name": "bytesField", - "type": "bytes" - }, - { - "name": "stringField", - "type": "string" - }, - { - "name": "pubkeyField", - "type": "publicKey" - }, - { - "name": "vecField", - "type": { - "vec": "u64" - } - }, - { - "name": "vecStructField", - "type": { - "vec": { - "defined": "FooStruct" - } - } - }, - { - "name": "optionField", - "type": { - "option": "bool" - } - }, - { - "name": "optionStructField", - "type": { - "option": { - "defined": "FooStruct" - } - } - }, - { - "name": "structField", - "type": { - "defined": "FooStruct" - } - }, - { - "name": "arrayField", - "type": { - "array": [ - "bool", - 3 - ] - } - }, - { - "name": "enumField1", - "type": { - "defined": "FooEnum" - } - }, - { - "name": "enumField2", - "type": { - "defined": "FooEnum" - } - }, - { - "name": "enumField3", - "type": { - "defined": "FooEnum" - } - }, - { - "name": "enumField4", - "type": { - "defined": "FooEnum" - } - } - ] - } + "discriminator": [ + 216, + 146, + 107, + 94, + 104, + 75, + 182, + 177 + ] }, { "name": "State2", - "type": { - "kind": "struct", - "fields": [ - { - "name": "vecOfOption", - "type": { - "vec": { - "option": "u64" - } - } - }, - { - "name": "boxField", - "type": "bool" - } - ] - } + "discriminator": [ + 106, + 97, + 255, + 161, + 250, + 205, + 185, + 192 + ] } ], - "types": [ + "events": [ { - "name": "external::Baz", - "type": { - "kind": "struct", - "fields": [ - { - "name": "someField", - "type": "u8" - } - ] - } + "name": "SomeEvent", + "discriminator": [ + 39, + 221, + 150, + 148, + 91, + 206, + 29, + 93 + ] + } + ], + "errors": [ + { + "code": 6000, + "name": "SomeError", + "msg": "Example error." }, + { + "code": 6001, + "name": "OtherError", + "msg": "Another error." + }, + { + "code": 6002, + "name": "ErrorWithoutMsg" + } + ], + "types": [ { "name": "BarStruct", "docs": [ @@ -546,14 +437,14 @@ "kind": "struct", "fields": [ { - "name": "someField", + "name": "some_field", "docs": [ "Some field" ], "type": "bool" }, { - "name": "otherField", + "name": "other_field", "type": "u8" } ] @@ -573,7 +464,9 @@ "bool", "u8", { - "defined": "BarStruct" + "defined": { + "name": "BarStruct" + } } ] }, @@ -581,7 +474,9 @@ "name": "UnnamedSingle", "fields": [ { - "defined": "BarStruct" + "defined": { + "name": "BarStruct" + } } ] }, @@ -589,20 +484,22 @@ "name": "Named", "fields": [ { - "name": "boolField", + "name": "bool_field", "docs": [ "A bool field inside a struct tuple kind" ], "type": "bool" }, { - "name": "u8Field", + "name": "u8_field", "type": "u8" }, { "name": "nested", "type": { - "defined": "BarStruct" + "defined": { + "name": "BarStruct" + } } } ] @@ -611,7 +508,9 @@ "name": "Struct", "fields": [ { - "defined": "BarStruct" + "defined": { + "name": "BarStruct" + } } ] }, @@ -620,7 +519,9 @@ "fields": [ { "option": { - "defined": "BarStruct" + "defined": { + "name": "BarStruct" + } } } ] @@ -630,7 +531,9 @@ "fields": [ { "vec": { - "defined": "BarStruct" + "defined": { + "name": "BarStruct" + } } } ] @@ -657,29 +560,65 @@ { "name": "nested", "type": { - "defined": "BarStruct" + "defined": { + "name": "BarStruct" + } } }, { - "name": "vecNested", + "name": "vec_nested", "type": { "vec": { - "defined": "BarStruct" + "defined": { + "name": "BarStruct" + } } } }, { - "name": "optionNested", + "name": "option_nested", "type": { "option": { - "defined": "BarStruct" + "defined": { + "name": "BarStruct" + } + } + } + }, + { + "name": "enum_field", + "type": { + "defined": { + "name": "FooEnum" + } + } + } + ] + } + }, + { + "name": "SomeEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "bool_field", + "type": "bool" + }, + { + "name": "external_my_struct", + "type": { + "defined": { + "name": "external::MyStruct" } } }, { - "name": "enumField", + "name": "other_module_my_struct", "type": { - "defined": "FooEnum" + "defined": { + "name": "idl::some_other_module::MyStruct" + } } } ] @@ -691,77 +630,272 @@ "kind": "struct", "fields": [ { - "name": "someField", + "name": "some_field", "type": "u8" } ] } }, + { + "name": "SomeZcAccount", + "serialization": "bytemuck", + "repr": { + "kind": "c" + }, + "type": { + "kind": "struct", + "fields": [ + { + "name": "field", + "type": { + "defined": { + "name": "ZcStruct" + } + } + } + ] + } + }, + { + "name": "State", + "docs": [ + "An account containing various fields" + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "bool_field", + "docs": [ + "A boolean field" + ], + "type": "bool" + }, + { + "name": "u8_field", + "type": "u8" + }, + { + "name": "i8_field", + "type": "i8" + }, + { + "name": "u16_field", + "type": "u16" + }, + { + "name": "i16_field", + "type": "i16" + }, + { + "name": "u32_field", + "type": "u32" + }, + { + "name": "i32_field", + "type": "i32" + }, + { + "name": "f32_field", + "type": "f32" + }, + { + "name": "u64_field", + "type": "u64" + }, + { + "name": "i64_field", + "type": "i64" + }, + { + "name": "f64_field", + "type": "f64" + }, + { + "name": "u128_field", + "type": "u128" + }, + { + "name": "i128_field", + "type": "i128" + }, + { + "name": "bytes_field", + "type": "bytes" + }, + { + "name": "string_field", + "type": "string" + }, + { + "name": "pubkey_field", + "type": "pubkey" + }, + { + "name": "vec_field", + "type": { + "vec": "u64" + } + }, + { + "name": "vec_struct_field", + "type": { + "vec": { + "defined": { + "name": "FooStruct" + } + } + } + }, + { + "name": "option_field", + "type": { + "option": "bool" + } + }, + { + "name": "option_struct_field", + "type": { + "option": { + "defined": { + "name": "FooStruct" + } + } + } + }, + { + "name": "struct_field", + "type": { + "defined": { + "name": "FooStruct" + } + } + }, + { + "name": "array_field", + "type": { + "array": [ + "bool", + 3 + ] + } + }, + { + "name": "enum_field_1", + "type": { + "defined": { + "name": "FooEnum" + } + } + }, + { + "name": "enum_field_2", + "type": { + "defined": { + "name": "FooEnum" + } + } + }, + { + "name": "enum_field_3", + "type": { + "defined": { + "name": "FooEnum" + } + } + }, + { + "name": "enum_field_4", + "type": { + "defined": { + "name": "FooEnum" + } + } + } + ] + } + }, + { + "name": "State2", + "type": { + "kind": "struct", + "fields": [ + { + "name": "vec_of_option", + "type": { + "vec": { + "option": "u64" + } + } + }, + { + "name": "box_field", + "type": "bool" + } + ] + } + }, { "name": "ZcStruct", + "serialization": "bytemuck", + "repr": { + "kind": "c" + }, "type": { "kind": "struct", "fields": [ { - "name": "someField", + "name": "some_field", "type": "u16" } ] } }, { - "name": "idl::some_other_module::Baz", + "name": "external::MyStruct", "type": { "kind": "struct", "fields": [ { - "name": "someU8", + "name": "some_field", "type": "u8" } ] } - } - ], - "events": [ + }, { - "name": "SomeEvent", - "fields": [ - { - "name": "boolField", - "type": "bool", - "index": false - }, - { - "name": "externalBaz", - "type": { - "defined": "external::Baz" - }, - "index": false - }, - { - "name": "otherModuleBaz", - "type": { - "defined": "idl::some_other_module::Baz" - }, - "index": false - } - ] + "name": "idl::some_other_module::MyStruct", + "type": { + "kind": "struct", + "fields": [ + { + "name": "some_u8", + "type": "u8" + } + ] + } } ], - "errors": [ + "constants": [ { - "code": 6000, - "name": "SomeError", - "msg": "Example error." + "name": "BYTES_STR", + "type": "bytes", + "value": "[116, 101, 115, 116]" }, { - "code": 6001, - "name": "OtherError", - "msg": "Another error." + "name": "BYTE_STR", + "type": "u8", + "value": "116" }, { - "code": 6002, - "name": "ErrorWithoutMsg" + "name": "I128", + "type": "i128", + "value": "1000000" + }, + { + "name": "U8", + "type": "u8", + "value": "6" } ] } \ No newline at end of file diff --git a/tests/idl/idls/parse.json b/tests/idl/idls/old.json similarity index 98% rename from tests/idl/idls/parse.json rename to tests/idl/idls/old.json index 3648844023..dac25cd03c 100644 --- a/tests/idl/idls/parse.json +++ b/tests/idl/idls/old.json @@ -526,7 +526,7 @@ ], "types": [ { - "name": "Baz", + "name": "MyStruct", "type": { "kind": "struct", "fields": [ @@ -720,16 +720,16 @@ "index": false }, { - "name": "externalBaz", + "name": "externalMyStruct", "type": { - "defined": "external::Baz" + "defined": "external::MyStruct" }, "index": false }, { - "name": "otherModuleBaz", + "name": "otherModuleMyStruct", "type": { - "defined": "some_other_module::Baz" + "defined": "some_other_module::MyStruct" }, "index": false } diff --git a/tests/idl/idls/relations.json b/tests/idl/idls/relations.json new file mode 100644 index 0000000000..6418651649 --- /dev/null +++ b/tests/idl/idls/relations.json @@ -0,0 +1,151 @@ +{ + "address": "Re1ationsDerivation111111111111111111111111", + "metadata": { + "name": "relations_derivation", + "version": "0.1.0", + "spec": "0.1.0", + "description": "Created with Anchor" + }, + "instructions": [ + { + "name": "init_base", + "discriminator": [ + 85, + 87, + 185, + 141, + 241, + 191, + 213, + 88 + ], + "accounts": [ + { + "name": "my_account", + "writable": true, + "signer": true + }, + { + "name": "account", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 115, + 101, + 101, + 100 + ] + } + ] + } + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + } + ], + "args": [] + }, + { + "name": "test_relation", + "discriminator": [ + 247, + 199, + 255, + 202, + 7, + 0, + 197, + 158 + ], + "accounts": [ + { + "name": "my_account", + "relations": [ + "account" + ] + }, + { + "name": "account", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 115, + 101, + 101, + 100 + ] + } + ] + } + }, + { + "name": "nested", + "accounts": [ + { + "name": "my_account", + "relations": [ + "account" + ] + }, + { + "name": "account", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 115, + 101, + 101, + 100 + ] + } + ] + } + } + ] + } + ], + "args": [] + } + ], + "accounts": [ + { + "name": "MyAccount", + "discriminator": [ + 246, + 28, + 6, + 87, + 251, + 45, + 50, + 42 + ] + } + ], + "types": [ + { + "name": "MyAccount", + "type": { + "kind": "struct", + "fields": [ + { + "name": "my_account", + "type": "pubkey" + }, + { + "name": "bump", + "type": "u8" + } + ] + } + } + ] +} \ No newline at end of file diff --git a/tests/idl/idls/relations_build.json b/tests/idl/idls/relations_build.json deleted file mode 100644 index bf70c7fc1d..0000000000 --- a/tests/idl/idls/relations_build.json +++ /dev/null @@ -1,82 +0,0 @@ -{ - "version": "0.1.0", - "name": "relations_derivation", - "instructions": [ - { - "name": "initBase", - "accounts": [ - { - "name": "myAccount", - "isMut": true, - "isSigner": true - }, - { - "name": "account", - "isMut": true, - "isSigner": false - }, - { - "name": "systemProgram", - "isMut": false, - "isSigner": false - } - ], - "args": [] - }, - { - "name": "testRelation", - "accounts": [ - { - "name": "myAccount", - "isMut": false, - "isSigner": false - }, - { - "name": "account", - "isMut": false, - "isSigner": false, - "relations": [ - "my_account" - ] - }, - { - "name": "nested", - "accounts": [ - { - "name": "myAccount", - "isMut": false, - "isSigner": false - }, - { - "name": "account", - "isMut": false, - "isSigner": false, - "relations": [ - "my_account" - ] - } - ] - } - ], - "args": [] - } - ], - "accounts": [ - { - "name": "MyAccount", - "type": { - "kind": "struct", - "fields": [ - { - "name": "myAccount", - "type": "publicKey" - }, - { - "name": "bump", - "type": "u8" - } - ] - } - } - ] -} \ No newline at end of file diff --git a/tests/idl/programs/client-interactions/Cargo.toml b/tests/idl/programs/client-interactions/Cargo.toml deleted file mode 100644 index e96129ddf7..0000000000 --- a/tests/idl/programs/client-interactions/Cargo.toml +++ /dev/null @@ -1,19 +0,0 @@ -[package] -name = "client-interactions" -version = "0.1.0" -description = "Created with Anchor" -rust-version = "1.60" -edition = "2021" - -[lib] -crate-type = ["cdylib", "lib"] -name = "client_interactions" - -[features] -no-entrypoint = [] -no-idl = [] -cpi = ["no-entrypoint"] -default = [] - -[dependencies] -anchor-lang = { path = "../../../../lang" } diff --git a/tests/idl/programs/client-interactions/src/lib.rs b/tests/idl/programs/client-interactions/src/lib.rs deleted file mode 100644 index 08d2bee1f6..0000000000 --- a/tests/idl/programs/client-interactions/src/lib.rs +++ /dev/null @@ -1,131 +0,0 @@ -use anchor_lang::prelude::*; - -declare_id!("C1ient1nteractions1111111111111111111111111"); - -#[program] -pub mod client_interactions { - use super::*; - - pub fn int(ctx: Context, i8: i8, i16: i16, i32: i32, i64: i64, i128: i128) -> Result<()> { - ctx.accounts.account.i8 = i8; - ctx.accounts.account.i16 = i16; - ctx.accounts.account.i32 = i32; - ctx.accounts.account.i64 = i64; - ctx.accounts.account.i128 = i128; - Ok(()) - } - - pub fn uint( - ctx: Context, - u8: u8, - u16: u16, - u32: u32, - u64: u64, - u128: u128, - ) -> Result<()> { - ctx.accounts.account.u8 = u8; - ctx.accounts.account.u16 = u16; - ctx.accounts.account.u32 = u32; - ctx.accounts.account.u64 = u64; - ctx.accounts.account.u128 = u128; - Ok(()) - } - - pub fn enm(ctx: Context, enum_arg: MyEnum) -> Result<()> { - ctx.accounts.account.enum_field = enum_arg; - Ok(()) - } - - pub fn type_alias( - ctx: Context, - type_alias_u8: TypeAliasU8, - type_alias_u8_array: TypeAliasU8Array, - type_alias_struct: TypeAliasStruct, - ) -> Result<()> { - ctx.accounts.account.type_alias_u8 = type_alias_u8; - ctx.accounts.account.type_alias_u8_array = type_alias_u8_array; - ctx.accounts.account.type_alias_struct = type_alias_struct; - Ok(()) - } -} - -#[derive(Accounts)] -pub struct Int<'info> { - #[account(zero)] - pub account: Account<'info, IntAccount>, -} - -#[account] -pub struct IntAccount { - pub i8: i8, - pub i16: i16, - pub i32: i32, - pub i64: i64, - pub i128: i128, -} - -#[derive(Accounts)] -pub struct UnsignedInt<'info> { - #[account(zero)] - pub account: Account<'info, UnsignedIntAccount>, -} - -#[account] -pub struct UnsignedIntAccount { - pub u8: u8, - pub u16: u16, - pub u32: u32, - pub u64: u64, - pub u128: u128, -} - -#[derive(Accounts)] -pub struct Enum<'info> { - #[account(zero)] - pub account: Account<'info, EnumAccount>, -} - -#[account] -pub struct EnumAccount { - pub enum_field: MyEnum, -} - -#[derive(AnchorSerialize, AnchorDeserialize, Clone, Copy, Debug, Eq, PartialEq)] -pub enum MyEnum { - Unit, - Named { point_x: u64, point_y: u64 }, - Unnamed(u8, u8, u16, u16), - UnnamedStruct(MyStruct), -} - -#[derive(AnchorSerialize, AnchorDeserialize, Clone, Copy, Debug, Eq, PartialEq)] -pub struct MyStruct { - pub u8: u8, - pub u16: u16, - pub u32: u32, - pub u64: u64, -} - -#[derive(Accounts)] -pub struct TypeAlias<'info> { - #[account(zero)] - pub account: Account<'info, TypeAliasAccount>, -} - -#[account] -pub struct TypeAliasAccount { - pub type_alias_u8: TypeAliasU8, - pub type_alias_u8_array: TypeAliasU8Array, - pub type_alias_struct: TypeAliasStruct, -} - -pub type TypeAliasU8 = u8; -pub type TypeAliasU8Array = [TypeAliasU8; 8]; -pub type TypeAliasStruct = MyStruct; - -/// Generic type aliases should not get included in the IDL -pub type TypeAliasNotSupported<'a, T> = NotSupported<'a, T>; -pub struct NotSupported<'a, T> { - _t: T, - _s: &'a str, -} diff --git a/tests/idl/programs/docs/Cargo.toml b/tests/idl/programs/docs/Cargo.toml index cfff4e5bfc..0fea54d950 100644 --- a/tests/idl/programs/docs/Cargo.toml +++ b/tests/idl/programs/docs/Cargo.toml @@ -14,6 +14,7 @@ no-entrypoint = [] no-idl = [] cpi = ["no-entrypoint"] default = [] +idl-build = ["anchor-lang/idl-build"] [dependencies] anchor-lang = { path = "../../../../lang" } diff --git a/tests/idl/programs/external/src/lib.rs b/tests/idl/programs/external/src/lib.rs index 93cefe9414..7ce4cf49c5 100644 --- a/tests/idl/programs/external/src/lib.rs +++ b/tests/idl/programs/external/src/lib.rs @@ -6,15 +6,26 @@ declare_id!("Externa1111111111111111111111111111111111111"); pub mod external { use super::*; - pub fn initialize(_ctx: Context, _baz: Baz) -> Result<()> { + pub fn initialize(_ctx: Context) -> Result<()> { Ok(()) } } #[derive(AnchorSerialize, AnchorDeserialize, Clone)] -pub struct Baz { +pub struct MyStruct { some_field: u8, } +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub enum MyEnum { + Unit, + Named { name: String }, + Tuple(String), +} + +pub struct NonBorshStruct { + pub data: i32, +} + #[derive(Accounts)] pub struct Initialize {} diff --git a/tests/idl/programs/generics/src/lib.rs b/tests/idl/programs/generics/src/lib.rs index 40ca903bba..4e4b740b26 100644 --- a/tests/idl/programs/generics/src/lib.rs +++ b/tests/idl/programs/generics/src/lib.rs @@ -38,7 +38,7 @@ where pub gen1: T, pub gen2: U, pub gen3: GenericNested, - pub gen4: GenericNested, + pub gen4: GenericNested, pub gen5: GenericNested, pub gen6: GenericNested, pub gen7: GenericNested>, diff --git a/tests/idl/programs/idl-build-features/Xargo.toml b/tests/idl/programs/idl-build-features/Xargo.toml deleted file mode 100644 index 1744f098ae..0000000000 --- a/tests/idl/programs/idl-build-features/Xargo.toml +++ /dev/null @@ -1,2 +0,0 @@ -[target.bpfel-unknown-unknown.dependencies.std] -features = [] \ No newline at end of file diff --git a/tests/idl/programs/idl-build-features/src/lib.rs b/tests/idl/programs/idl-build-features/src/lib.rs deleted file mode 100644 index 939df83eb8..0000000000 --- a/tests/idl/programs/idl-build-features/src/lib.rs +++ /dev/null @@ -1,47 +0,0 @@ -use anchor_lang::prelude::*; - -declare_id!("id1Bui1dFeatures111111111111111111111111111"); - -#[program] -pub mod idl_build_features { - use super::*; - - pub fn full_path( - ctx: Context, - my_struct: MyStruct, - some_module_my_struct: some_module::MyStruct, - ) -> Result<()> { - ctx.accounts.account.my_struct = my_struct; - ctx.accounts.account.some_module_my_struct = some_module_my_struct; - Ok(()) - } -} - -#[derive(Accounts)] -pub struct FullPath<'info> { - #[account(zero)] - pub account: Account<'info, FullPathAccount>, -} - -#[account] -pub struct FullPathAccount { - pub my_struct: MyStruct, - pub some_module_my_struct: some_module::MyStruct, -} - -mod some_module { - use super::*; - - #[derive(AnchorSerialize, AnchorDeserialize, Clone)] - pub struct MyStruct { - pub data: u8, - } -} - -#[derive(AnchorSerialize, AnchorDeserialize, Clone)] -pub struct MyStruct { - pub u8: u8, - pub u16: u16, - pub u32: u32, - pub u64: u64, -} diff --git a/tests/idl/programs/idl/src/lib.rs b/tests/idl/programs/idl/src/lib.rs index d436c3ab69..6642428f0f 100644 --- a/tests/idl/programs/idl/src/lib.rs +++ b/tests/idl/programs/idl/src/lib.rs @@ -105,7 +105,7 @@ pub mod idl { } pub fn cause_error(_ctx: Context) -> Result<()> { - return Err(error!(ErrorCode::SomeError)); + Err(error!(ErrorCode::SomeError)) } } @@ -137,10 +137,10 @@ pub struct BarStruct { impl Default for BarStruct { fn default() -> Self { - return BarStruct { + Self { some_field: true, other_field: 10, - }; + } } } @@ -156,7 +156,7 @@ pub struct FooStruct { impl Default for FooStruct { fn default() -> Self { - return FooStruct { + Self { field1: 123, field2: 999, nested: BarStruct::default(), @@ -167,7 +167,7 @@ impl Default for FooStruct { u8_field: 15, nested: BarStruct::default(), }, - }; + } } } @@ -205,8 +205,7 @@ pub struct State { impl Default for State { fn default() -> Self { - // some arbitrary default values - return State { + Self { bool_field: true, u8_field: 234, i8_field: -123, @@ -237,7 +236,7 @@ impl Default for State { }, enum_field_3: FooEnum::Struct(BarStruct::default()), enum_field_4: FooEnum::NoFields, - }; + } } } @@ -248,10 +247,10 @@ pub struct State2 { } impl Default for State2 { fn default() -> Self { - return State2 { + Self { vec_of_option: vec![None, Some(10)], box_field: Box::new(true), - }; + } } } @@ -267,7 +266,7 @@ pub struct Initialize<'info> { /// State account #[account( init, - space = 8 + 1000, // TODO: use exact space required + space = 8 + 1000, payer = payer, )] state: Account<'info, State>, @@ -289,7 +288,7 @@ pub struct Initialize<'info> { pub struct Initialize2<'info> { #[account( init, - space = 8 + 1000, // TODO: use exact space required + space = 8 + 1000, payer = payer, )] state: Account<'info, State2>, @@ -315,7 +314,7 @@ mod some_other_module { use super::*; #[derive(AnchorSerialize, AnchorDeserialize, Clone)] - pub struct Baz { + pub struct MyStruct { some_u8: u8, } } @@ -323,8 +322,8 @@ mod some_other_module { #[event] pub struct SomeEvent { bool_field: bool, - external_baz: external::Baz, - other_module_baz: some_other_module::Baz, + external_my_struct: external::MyStruct, + other_module_my_struct: some_other_module::MyStruct, } #[zero_copy] diff --git a/tests/idl/programs/idl-build-features/Cargo.toml b/tests/idl/programs/new-idl/Cargo.toml similarity index 76% rename from tests/idl/programs/idl-build-features/Cargo.toml rename to tests/idl/programs/new-idl/Cargo.toml index a8577df4cd..ac8124e6a0 100644 --- a/tests/idl/programs/idl-build-features/Cargo.toml +++ b/tests/idl/programs/new-idl/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "idl-build-features" +name = "new-idl" version = "0.1.0" description = "Created with Anchor" rust-version = "1.60" @@ -7,14 +7,15 @@ edition = "2021" [lib] crate-type = ["cdylib", "lib"] -name = "idl_build_features" +name = "new_idl" [features] no-entrypoint = [] no-idl = [] cpi = ["no-entrypoint"] -idl-build = ["anchor-lang/idl-build"] default = [] +idl-build = ["anchor-lang/idl-build"] [dependencies] anchor-lang = { path = "../../../../lang" } +external = { path = "../external", features = ["no-entrypoint"] } diff --git a/tests/idl/programs/client-interactions/Xargo.toml b/tests/idl/programs/new-idl/Xargo.toml similarity index 100% rename from tests/idl/programs/client-interactions/Xargo.toml rename to tests/idl/programs/new-idl/Xargo.toml diff --git a/tests/idl/programs/new-idl/src/lib.rs b/tests/idl/programs/new-idl/src/lib.rs new file mode 100644 index 0000000000..9624f6e08b --- /dev/null +++ b/tests/idl/programs/new-idl/src/lib.rs @@ -0,0 +1,415 @@ +use anchor_lang::{prelude::*, solana_program::clock::UnixTimestamp}; + +declare_id!("Newid11111111111111111111111111111111111111"); + +#[program] +pub mod new_idl { + use super::*; + + pub fn no_case_conversion(ctx: Context, field_name: u8) -> Result<()> { + ctx.accounts.case_conversion_account.field_name = field_name; + emit!(SimpleEvent { field_name }); + Ok(()) + } + + pub fn empty(_ctx: Context) -> Result<()> { + Ok(()) + } + + pub fn primitive_types( + ctx: Context, + bool: bool, + i8: i8, + i16: i16, + i32: i32, + i64: i64, + i128: i128, + u8: u8, + u16: u16, + u32: u32, + u64: u64, + u128: u128, + f32: f32, + f64: f64, + pubkey: Pubkey, + ) -> Result<()> { + ctx.accounts.account.bool = bool; + + ctx.accounts.account.i8 = i8; + ctx.accounts.account.i16 = i16; + ctx.accounts.account.i32 = i32; + ctx.accounts.account.i64 = i64; + ctx.accounts.account.i128 = i128; + + ctx.accounts.account.u8 = u8; + ctx.accounts.account.u16 = u16; + ctx.accounts.account.u32 = u32; + ctx.accounts.account.u64 = u64; + ctx.accounts.account.u128 = u128; + + ctx.accounts.account.f32 = f32; + ctx.accounts.account.f64 = f64; + + ctx.accounts.account.pubkey = pubkey; + Ok(()) + } + + pub fn unsized_types(ctx: Context, string: String, bytes: Vec) -> Result<()> { + ctx.accounts.account.string = string; + ctx.accounts.account.bytes = bytes; + Ok(()) + } + + pub fn strct( + ctx: Context, + unit: UnitStruct, + named: NamedStruct, + tuple: TupleStruct, + ) -> Result<()> { + ctx.accounts.account.unit = unit; + ctx.accounts.account.named = named; + ctx.accounts.account.tuple = tuple; + Ok(()) + } + + pub fn enm(ctx: Context, full_enum: FullEnum) -> Result<()> { + ctx.accounts.account.full_enum = full_enum; + Ok(()) + } + + pub fn type_alias( + ctx: Context, + alias_u8: AliasU8, + alias_u8_array: AliasU8Array, + alias_struct: AliasStruct, + alias_vec_string: AliasVec, + alias_option_vec_pubkey: AliasOptionVec, + alias_generic_const: AliasGenericConst<4>, + alias_multiple_generics_mixed: AliasMultipleGenericMixed, + alias_external: UnixTimestamp, + ) -> Result<()> { + ctx.accounts.account.alias_u8 = alias_u8; + ctx.accounts.account.alias_u8_array = alias_u8_array; + ctx.accounts.account.alias_struct = alias_struct; + ctx.accounts.account.alias_vec_string = alias_vec_string; + ctx.accounts.account.alias_option_vec_pubkey = alias_option_vec_pubkey; + ctx.accounts.account.alias_generic_const = alias_generic_const; + ctx.accounts.account.alias_multiple_generics_mixed = alias_multiple_generics_mixed; + ctx.accounts.account.alias_external = alias_external; + Ok(()) + } + + pub fn account_and_event_arg_and_field( + ctx: Context, + account: AccountAndEventFieldAccount, + ) -> Result<()> { + *ctx.accounts.account = account; + Ok(()) + } + + pub fn generic(ctx: Context, generic_arg: GenericStruct) -> Result<()> { + ctx.accounts.my_account.field = generic_arg; + Ok(()) + } + + pub fn full_path( + ctx: Context, + named_struct: NamedStruct, + some_module_named_struct: some_module::NamedStruct, + ) -> Result<()> { + ctx.accounts.account.named_struct = named_struct; + ctx.accounts.account.some_module_named_struct = some_module_named_struct; + Ok(()) + } + + pub fn external(ctx: Context, my_struct: external::MyStruct) -> Result<()> { + ctx.accounts.account.my_struct = my_struct; + Ok(()) + } + + pub fn external_non_anchor( + ctx: Context, + feature: wrapped::Feature, + ) -> Result<()> { + ctx.accounts.account.feature = feature; + Ok(()) + } +} + +#[account] +#[derive(InitSpace)] +pub struct SimpleAccount { + pub field_name: u8, +} + +#[event] +#[derive(Clone)] +pub struct SimpleEvent { + pub field_name: u8, +} + +#[derive(Accounts)] +pub struct NoCaseConversion<'info> { + #[account(init, payer = payer, space = 8 + SimpleAccount::INIT_SPACE)] + pub case_conversion_account: Account<'info, SimpleAccount>, + #[account(mut)] + pub payer: Signer<'info>, + pub system_program: Program<'info, System>, +} +#[derive(Accounts)] +pub struct Empty {} + +#[derive(Accounts)] +pub struct PrimitiveTypes<'info> { + #[account(zero)] + pub account: Account<'info, PrimitiveAccount>, +} + +#[account] +pub struct PrimitiveAccount { + pub bool: bool, + pub i8: i8, + pub i16: i16, + pub i32: i32, + pub i64: i64, + pub i128: i128, + pub u8: u8, + pub u16: u16, + pub u32: u32, + pub u64: u64, + pub u128: u128, + pub f32: f32, + pub f64: f64, + pub pubkey: Pubkey, +} + +#[derive(Accounts)] +pub struct UnsizedTypes<'info> { + #[account(zero)] + pub account: Account<'info, UnsizedAccount>, +} + +#[account] +pub struct UnsizedAccount { + pub string: String, + pub bytes: Vec, +} + +#[derive(Accounts)] +pub struct Struct<'info> { + #[account(zero)] + pub account: Account<'info, StructAccount>, +} + +#[account] +pub struct StructAccount { + pub unit: UnitStruct, + pub named: NamedStruct, + pub tuple: TupleStruct, +} + +#[derive(AnchorDeserialize, AnchorSerialize, Clone)] +pub struct UnitStruct; + +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Copy, Debug, Eq, PartialEq)] +pub struct NamedStruct { + pub u8: u8, + pub u16: u16, + pub u32: u32, + pub u64: u64, +} + +#[derive(AnchorDeserialize, AnchorSerialize, Clone)] +pub struct TupleStruct(u64, String); + +#[derive(Accounts)] +pub struct Enum<'info> { + #[account(zero)] + pub account: Account<'info, EnumAccount>, +} + +#[account] +pub struct EnumAccount { + pub full_enum: FullEnum, +} + +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Copy, Debug, Eq, PartialEq)] +pub enum FullEnum { + Unit, + Named { point_x: u64, point_y: u64 }, + Unnamed(u8, u8, u16, u16), + UnnamedStruct(NamedStruct), +} + +#[derive(Accounts)] +pub struct TypeAlias<'info> { + #[account(zero)] + pub account: Account<'info, AliasAccount>, +} + +#[account] +pub struct AliasAccount { + pub alias_u8: AliasU8, + pub alias_u8_array: AliasU8Array, + pub alias_struct: AliasStruct, + pub alias_vec_string: AliasVec, + pub alias_option_vec_pubkey: AliasOptionVec, + pub alias_generic_const: AliasGenericConst<4>, + pub alias_multiple_generics_mixed: AliasMultipleGenericMixed, + pub alias_external: UnixTimestamp, +} + +pub type AliasU8 = u8; +pub type AliasU8Array = [AliasU8; 8]; +pub type AliasStruct = NamedStruct; +pub type AliasVec = Vec; +pub type AliasOptionVec = Vec>; +pub type AliasGenericConst = [u32; N]; +pub type AliasMultipleGenericMixed = Vec<[T; N]>; + +#[derive(Accounts)] +pub struct AccountAndEventArgAndField<'info> { + #[account(zero)] + pub account: Account<'info, AccountAndEventFieldAccount>, +} + +#[account] +pub struct AccountAndEventFieldAccount { + pub simple_account: SimpleAccount, + pub simple_event: SimpleEvent, +} + +#[derive(Accounts)] +pub struct FullPath<'info> { + #[account(zero)] + pub account: Account<'info, FullPathAccount>, +} + +#[account] +pub struct FullPathAccount { + pub named_struct: NamedStruct, + pub some_module_named_struct: some_module::NamedStruct, +} + +mod some_module { + use super::*; + + #[derive(AnchorSerialize, AnchorDeserialize, Clone)] + pub struct NamedStruct { + pub data: u8, + } +} + +#[derive(Accounts)] +pub struct Generic<'info> { + #[account(mut)] + pub signer: Signer<'info>, + #[account( + init, + payer = signer, + space = 1024, + seeds = [b"generic", signer.key.as_ref()], + bump + )] + pub my_account: Account<'info, GenericAccount>, + pub system_program: Program<'info, System>, +} + +#[account] +pub struct GenericAccount { + pub field: GenericStruct, +} + +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] +pub struct GenericStruct { + arr: [T; N], + sub_field: SubGenericStruct<8, T, Vec>>, +} + +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] +pub struct SubGenericStruct { + sub_arr: [T; N], + another: U, +} + +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] +pub enum GenericEnum { + Unit, + Named { x: T }, + Tuple(Vec), +} + +#[derive(Accounts)] +pub struct External<'info> { + #[account(zero)] + pub account: Account<'info, AccountWithExternalField>, +} + +#[account] +pub struct AccountWithExternalField { + pub my_struct: external::MyStruct, +} + +#[derive(Accounts)] +pub struct ExternalNonAnchor<'info> { + #[account(zero)] + pub account: Account<'info, AccountWithNonAnchorExternalField>, +} + +#[account] +pub struct AccountWithNonAnchorExternalField { + pub feature: wrapped::Feature, +} + +/// An example of wrapping a non-Anchor external type in order to include it in the IDL +mod wrapped { + use super::*; + + #[cfg(feature = "idl-build")] + use anchor_lang::anchor_syn::idl::types::*; + + pub struct Feature(anchor_lang::solana_program::feature::Feature); + + impl AnchorSerialize for Feature { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + self.0.activated_at.serialize(writer)?; + Ok(()) + } + } + + impl AnchorDeserialize for Feature { + fn deserialize_reader(reader: &mut R) -> std::io::Result { + Ok(Self(anchor_lang::solana_program::feature::Feature { + activated_at: AnchorDeserialize::deserialize_reader(reader)?, + })) + } + } + + impl Clone for Feature { + fn clone(&self) -> Self { + Self(anchor_lang::solana_program::feature::Feature { + activated_at: self.0.activated_at.clone(), + }) + } + } + + #[cfg(feature = "idl-build")] + impl IdlBuild for Feature { + fn create_type() -> Option { + Some(IdlTypeDef { + name: "Feature".into(), + ty: IdlTypeDefTy::Struct { + fields: Some(IdlDefinedFields::Named(vec![IdlField { + name: "activated_at".into(), + ty: IdlType::Option(Box::new(IdlType::U64)), + docs: Default::default(), + }])), + }, + docs: Default::default(), + generics: Default::default(), + serialization: Default::default(), + repr: Default::default(), + }) + } + } +} diff --git a/tests/idl/test.sh b/tests/idl/test.sh index 7b0db254f0..3c23d5cc81 100755 --- a/tests/idl/test.sh +++ b/tests/idl/test.sh @@ -1,11 +1,22 @@ #!/usr/bin/env bash set -e +# Generate temp directory +tmp_dir=$(mktemp -d) + +# Fix external type resolution not working in CI due to missing `anchor-lang` +# crates.io entry in runner machine. +pushd $tmp_dir +cargo new external-ci +pushd external-ci +cargo add anchor-lang +cargo b +popd +popd + # Run anchor test anchor test --skip-lint -tmp_dir=$(mktemp -d) - # Generate IDLs ./generate.sh $tmp_dir @@ -27,9 +38,8 @@ compare() { echo "" } -compare "parse" -compare "build" -compare "generics_build" -compare "relations_build" +compare "new" +compare "generics" +compare "relations" exit $ret diff --git a/tests/idl/tests/client-interactions.ts b/tests/idl/tests/client-interactions.ts deleted file mode 100644 index 25473fbec7..0000000000 --- a/tests/idl/tests/client-interactions.ts +++ /dev/null @@ -1,149 +0,0 @@ -import * as anchor from "@coral-xyz/anchor"; -import { assert } from "chai"; - -import { ClientInteractions } from "../target/types/client_interactions"; - -describe("Client interactions", () => { - anchor.setProvider(anchor.AnchorProvider.env()); - const program = anchor.workspace - .clientInteractions as anchor.Program; - - it("Can use integers", async () => { - const kp = anchor.web3.Keypair.generate(); - - const i8 = -3; - const i16 = 1; - const i32 = -5555551; - const i64 = new anchor.BN("384535471"); - const i128 = new anchor.BN(-8342491); - - await program.methods - .int(i8, i16, i32, i64, i128) - .accounts({ account: kp.publicKey }) - .signers([kp]) - .preInstructions([await program.account.intAccount.createInstruction(kp)]) - .rpc(); - - const account = await program.account.intAccount.fetch(kp.publicKey); - assert.strictEqual(account.i8, i8); - assert.strictEqual(account.i16, i16); - assert.strictEqual(account.i32, i32); - assert(account.i64.eq(i64)); - assert(account.i128.eq(i128)); - }); - - it("Can use unsigned integers", async () => { - const kp = anchor.web3.Keypair.generate(); - - const u8 = 123; - const u16 = 7888; - const u32 = 5555551; - const u64 = new anchor.BN("384535471"); - const u128 = new anchor.BN(8888888); - - await program.methods - .uint(u8, u16, u32, u64, u128) - .accounts({ account: kp.publicKey }) - .signers([kp]) - .preInstructions([ - await program.account.unsignedIntAccount.createInstruction(kp), - ]) - .rpc(); - - const account = await program.account.unsignedIntAccount.fetch( - kp.publicKey - ); - assert.strictEqual(account.u8, u8); - assert.strictEqual(account.u16, u16); - assert.strictEqual(account.u32, u32); - assert(account.u64.eq(u64)); - assert(account.u128.eq(u128)); - }); - - it("Can use enum", async () => { - const testAccountEnum = async ( - ...args: Parameters - ) => { - const kp = anchor.web3.Keypair.generate(); - await program.methods - .enm(...(args as any)) - .accounts({ account: kp.publicKey }) - .signers([kp]) - .preInstructions([ - await program.account.enumAccount.createInstruction(kp), - ]) - .rpc(); - return await program.account.enumAccount.fetch(kp.publicKey); - }; - - // Unit - const unit = await testAccountEnum({ unit: {} }); - assert.deepEqual(unit.enumField.unit, {}); - - // Named - const pointX = new anchor.BN(1); - const pointY = new anchor.BN(2); - const named = await testAccountEnum({ named: { pointX, pointY } }); - assert(named.enumField.named.pointX.eq(pointX)); - assert(named.enumField.named.pointY.eq(pointY)); - - // Unnamed - const tupleArg = [1, 2, 3, 4] as const; - const unnamed = await testAccountEnum({ unnamed: tupleArg }); - assert.strictEqual(unnamed.enumField.unnamed[0], tupleArg[0]); - assert.strictEqual(unnamed.enumField.unnamed[1], tupleArg[1]); - assert.strictEqual(unnamed.enumField.unnamed[2], tupleArg[2]); - assert.strictEqual(unnamed.enumField.unnamed[3], tupleArg[3]); - - // Unnamed struct - const tupleStructArg = [ - { u8: 1, u16: 11, u32: 111, u64: new anchor.BN(1111) }, - ] as const; - const unnamedStruct = await testAccountEnum({ - unnamedStruct: tupleStructArg, - }); - assert.strictEqual( - unnamedStruct.enumField.unnamedStruct[0].u8, - tupleStructArg[0].u8 - ); - assert.strictEqual( - unnamedStruct.enumField.unnamedStruct[0].u16, - tupleStructArg[0].u16 - ); - assert.strictEqual( - unnamedStruct.enumField.unnamedStruct[0].u32, - tupleStructArg[0].u32 - ); - assert( - unnamedStruct.enumField.unnamedStruct[0].u64.eq(tupleStructArg[0].u64) - ); - }); - - it("Can use type aliases", async () => { - const kp = anchor.web3.Keypair.generate(); - - const typeAliasU8 = 42; - const typeAliasU8Array = [1, 2, 3, 4, 5, 6, 7, 8]; - const typeAliasStruct = { - u8: 1, - u16: 2, - u32: 3, - u64: new anchor.BN(4), - }; - - await program.methods - .typeAlias(typeAliasU8, typeAliasU8Array, typeAliasStruct) - .accounts({ account: kp.publicKey }) - .signers([kp]) - .preInstructions([await program.account.intAccount.createInstruction(kp)]) - .rpc(); - - const account = await program.account.typeAliasAccount.fetch(kp.publicKey); - assert.strictEqual(account.typeAliasU8, typeAliasU8); - assert.deepEqual(account.typeAliasU8Array, typeAliasU8Array); - assert.strictEqual(account.typeAliasStruct.u8, typeAliasStruct.u8); - assert.strictEqual(account.typeAliasStruct.u16, typeAliasStruct.u16); - assert.strictEqual(account.typeAliasStruct.u32, typeAliasStruct.u32); - assert(account.typeAliasStruct.u64.eq(typeAliasStruct.u64)); - }); -}); diff --git a/tests/idl/tests/docs.ts b/tests/idl/tests/docs.ts index 401888d7b4..af0dc88fd7 100644 --- a/tests/idl/tests/docs.ts +++ b/tests/idl/tests/docs.ts @@ -10,7 +10,7 @@ describe("Docs", () => { const instruction = program.idl.instructions.find( (i) => i.name === "testIdlDocParse" - ); + )!; it("includes instruction doc comment", () => { assert.deepEqual(instruction.docs, [ @@ -19,26 +19,25 @@ describe("Docs", () => { }); it("includes account doc comment", () => { - const act = instruction.accounts.find((i) => i.name === "act"); + const act = instruction.accounts.find((i) => i.name === "act")!; assert.deepEqual(act.docs, [ "This account doc comment should appear in the IDL", "This is a multi-line comment", ]); }); - const dataWithDoc = program.idl.accounts.find( - // @ts-expect-error - (acc) => acc.name === "DataWithDoc" - ); + const dataWithDoc = program.idl.types.find( + (ty) => ty.name === "dataWithDoc" + )!; - it("includes accounts doc comment", () => { + it("includes type doc comment", () => { assert.deepEqual(dataWithDoc.docs, [ "Custom account doc comment should appear in the IDL", ]); }); it("includes account attribute doc comment", () => { - const dataField = dataWithDoc.type.fields.find((i) => i.name === "data"); + const dataField = dataWithDoc.type.fields.find((i) => i.name === "data")!; assert.deepEqual(dataField.docs, [ "Account attribute doc comment should appear in the IDL", ]); diff --git a/tests/idl/tests/idl-build-features.ts b/tests/idl/tests/idl-build-features.ts deleted file mode 100644 index 4abdb13d34..0000000000 --- a/tests/idl/tests/idl-build-features.ts +++ /dev/null @@ -1,35 +0,0 @@ -import * as anchor from "@coral-xyz/anchor"; -import { assert } from "chai"; - -import { IdlBuildFeatures } from "../target/types/idl_build_features"; - -describe("idl-build features", () => { - anchor.setProvider(anchor.AnchorProvider.env()); - const program = anchor.workspace - .idlBuildFeatures as anchor.Program; - - it("Can use full module path types", async () => { - const kp = anchor.web3.Keypair.generate(); - - const outerMyStructArg = { u8: 1, u16: 2, u32: 3, u64: new anchor.BN(4) }; - const someModuleMyStructArg = { data: 5 }; - - await program.methods - .fullPath(outerMyStructArg, someModuleMyStructArg) - .accounts({ account: kp.publicKey }) - .preInstructions([ - await program.account.fullPathAccount.createInstruction(kp), - ]) - .signers([kp]) - .rpc(); - - const fullPathAccount = await program.account.fullPathAccount.fetch( - kp.publicKey - ); - assert.strictEqual(fullPathAccount.myStruct.u8, outerMyStructArg.u8); - assert.strictEqual(fullPathAccount.myStruct.u16, outerMyStructArg.u16); - assert.strictEqual(fullPathAccount.myStruct.u32, outerMyStructArg.u32); - assert(fullPathAccount.myStruct.u64.eq(outerMyStructArg.u64)); - assert.deepEqual(fullPathAccount.someModuleMyStruct, someModuleMyStructArg); - }); -}); diff --git a/tests/idl/tests/idl.ts b/tests/idl/tests/idl.ts index 4aa7e4ddc2..a0bae6127f 100644 --- a/tests/idl/tests/idl.ts +++ b/tests/idl/tests/idl.ts @@ -12,19 +12,20 @@ describe("IDL", () => { const checkDefined = ( cb: (constant: typeof program["idl"]["constants"][number]) => boolean ) => { - program.idl.constants.find((c) => cb(c)); + const constant = program.idl.constants.find(cb); + if (!constant) throw new Error("Constant not found"); }; - checkDefined((c) => c.name === "U8" && c.type === "u8" && c.value === "6"); + checkDefined((c) => c.name === "u8" && c.type === "u8" && c.value === "6"); checkDefined( - (c) => c.name === "I128" && c.type === "i128" && c.value === "1000000" + (c) => c.name === "i128" && c.type === "i128" && c.value === "1000000" ); checkDefined( - (c) => c.name === "BYTE_STR" && c.type === "u8" && c.value === "116" + (c) => c.name === "byteStr" && c.type === "u8" && c.value === "116" ); checkDefined( (c) => - c.name === "BYTES_STR" && + c.name === "bytesStr" && c.type === "bytes" && c.value === "[116, 101, 115, 116]" ); @@ -32,6 +33,6 @@ describe("IDL", () => { it("Does not include constants that does not use `#[constant]` macro ", () => { // @ts-expect-error - assert.isUndefined(program.idl.constants.find((c) => c.name === "NO_IDL")); + assert.isUndefined(program.idl.constants.find((c) => c.name === "noIdl")); }); }); diff --git a/tests/idl/tests/new-idl.ts b/tests/idl/tests/new-idl.ts new file mode 100644 index 0000000000..443dcb3e6c --- /dev/null +++ b/tests/idl/tests/new-idl.ts @@ -0,0 +1,467 @@ +import * as anchor from "@coral-xyz/anchor"; +import { assert } from "chai"; + +import type { NewIdl } from "../target/types/new_idl"; + +describe("New IDL", () => { + anchor.setProvider(anchor.AnchorProvider.env()); + const program = anchor.workspace.newIdl as anchor.Program; + + describe("Case conversion", () => { + const caseConversionAccountKp = anchor.web3.Keypair.generate(); + const FIELD_NAME = 5; + + it("Works when instructions have no case conversion in IDL", async () => { + const ixName = "no_case_conversion"; + const ix = program.rawIdl.instructions.find((ix) => ix.name === ixName); + if (!ix) throw new Error(`Instruction \`${ixName}\` not found`); + + await program.methods + .noCaseConversion(FIELD_NAME) + .accounts({ caseConversionAccount: caseConversionAccountKp.publicKey }) + .signers([caseConversionAccountKp]) + .rpc(); + }); + + it("Works when accounts have no case conversion in IDL", async () => { + const accName = "SimpleAccount"; + const acc = program.rawIdl.accounts!.find((acc) => acc.name === accName); + if (!acc) throw new Error(`Account \`${accName}\` not found`); + + const caseConversionAccount = await program.account.simpleAccount.fetch( + caseConversionAccountKp.publicKey + ); + assert.strictEqual(caseConversionAccount.fieldName, FIELD_NAME); + }); + + it("Works when events have no case conversion in IDL", async () => { + const eventName = "SimpleEvent"; + const event = program.rawIdl.events!.find((ev) => ev.name === eventName); + if (!event) throw new Error(`Event \`${eventName}\` not found`); + + await new Promise(async (res) => { + const id = program.addEventListener("simpleEvent", (ev) => { + program.removeEventListener(id); + assert.strictEqual(ev.fieldName, FIELD_NAME); + res(); + }); + + const caseConversionAccountKp = anchor.web3.Keypair.generate(); + await program.methods + .noCaseConversion(FIELD_NAME) + .accounts({ + caseConversionAccount: caseConversionAccountKp.publicKey, + }) + .signers([caseConversionAccountKp]) + .rpc(); + }); + }); + }); + + describe("Client interaction", () => { + it("Can send empty ix(no arg, no account)", async () => { + await program.methods.empty().rpc(); + }); + + it("Can use primitive types", async () => { + const kp = anchor.web3.Keypair.generate(); + + const bool = true; + + const i8 = -3; + const i16 = 1; + const i32 = -5555551; + const i64 = new anchor.BN("384535471"); + const i128 = new anchor.BN(-8342491); + + const u8 = 123; + const u16 = 7888; + const u32 = 5555551; + const u64 = new anchor.BN("384535471"); + const u128 = new anchor.BN(8888888); + + const f32 = 1.0; + const f64 = 0.618; + + const pubkey = anchor.web3.PublicKey.default; + + await program.methods + .primitiveTypes( + bool, + i8, + i16, + i32, + i64, + i128, + u8, + u16, + u32, + u64, + u128, + f32, + f64, + pubkey + ) + .accounts({ account: kp.publicKey }) + .signers([kp]) + .preInstructions([ + await program.account.primitiveAccount.createInstruction(kp), + ]) + .rpc(); + + const account = await program.account.primitiveAccount.fetch( + kp.publicKey + ); + assert.strictEqual(account.bool, bool); + + assert.strictEqual(account.i8, i8); + assert.strictEqual(account.i16, i16); + assert.strictEqual(account.i32, i32); + assert(account.i64.eq(i64)); + assert(account.i128.eq(i128)); + + assert.strictEqual(account.u8, u8); + assert.strictEqual(account.u16, u16); + assert.strictEqual(account.u32, u32); + assert(account.u64.eq(u64)); + assert(account.u128.eq(u128)); + + assert.strictEqual(account.f32, f32); + assert.strictEqual(account.f64, f64); + + assert(account.pubkey.equals(pubkey)); + }); + + it("Can use unsized types", async () => { + const kp = anchor.web3.Keypair.generate(); + + const string = "anchor"; + const bytes = Buffer.from([1, 2, 3, 4]); + await program.methods + .unsizedTypes(string, bytes) + .accounts({ account: kp.publicKey }) + .signers([kp]) + .preInstructions([ + await program.account.primitiveAccount.createInstruction(kp), + ]) + .rpc(); + + const account = await program.account.unsizedAccount.fetch(kp.publicKey); + assert.strictEqual(account.string, string); + assert(account.bytes.equals(bytes)); + }); + + it("Can use struct", async () => { + const unitStructArg = {} as const; + const namedStructArg = { + u8: 1, + u16: 11, + u32: 111, + u64: new anchor.BN(1111), + } as const; + const tupleStructArg = [new anchor.BN(23), "tuple"] as const; + + const kp = anchor.web3.Keypair.generate(); + await program.methods + .strct(unitStructArg, namedStructArg, tupleStructArg) + .accounts({ account: kp.publicKey }) + .signers([kp]) + .preInstructions([ + await program.account.structAccount.createInstruction(kp, 1024), + ]) + .rpc(); + + const struct = await program.account.structAccount.fetch(kp.publicKey); + + // Unit + assert.deepEqual(struct.unit, unitStructArg); + + // Named + assert.strictEqual(struct.named.u8, namedStructArg.u8); + assert.strictEqual(struct.named.u16, namedStructArg.u16); + assert.strictEqual(struct.named.u32, namedStructArg.u32); + assert(struct.named.u64.eq(namedStructArg.u64)); + + // Tuple + assert(struct.tuple[0].eq(tupleStructArg[0])); + assert.strictEqual(struct.tuple[1], tupleStructArg[1]); + }); + + it("Can use enum", async () => { + const testAccountEnum = async ( + ...args: Parameters + ) => { + const kp = anchor.web3.Keypair.generate(); + await program.methods + .enm(...(args as any)) + .accounts({ account: kp.publicKey }) + .signers([kp]) + .preInstructions([ + await program.account.enumAccount.createInstruction(kp), + ]) + .rpc(); + return await program.account.enumAccount.fetch(kp.publicKey); + }; + + // Unit + const unit = await testAccountEnum({ unit: {} }); + assert.deepEqual(unit.fullEnum.unit, {}); + + // Named + const pointX = new anchor.BN(1); + const pointY = new anchor.BN(2); + const named = await testAccountEnum({ named: { pointX, pointY } }); + if (!named.fullEnum.named) throw new Error("Named not crated"); + assert(named.fullEnum.named.pointX.eq(pointX)); + assert(named.fullEnum.named.pointY.eq(pointY)); + + // Unnamed + const tupleArg = [1, 2, 3, 4] as const; + const unnamed = await testAccountEnum({ unnamed: tupleArg }); + if (!unnamed.fullEnum.unnamed) throw new Error("Unnamed not crated"); + assert( + Object.entries(unnamed.fullEnum.unnamed).every( + ([key, value]) => value === tupleArg[key as keyof typeof tupleArg] + ) + ); + + // Unnamed struct + const tupleStructArg = [ + { u8: 1, u16: 11, u32: 111, u64: new anchor.BN(1111) }, + ] as const; + const unnamedStruct = await testAccountEnum({ + unnamedStruct: tupleStructArg, + }); + if (!unnamedStruct.fullEnum.unnamedStruct) { + throw new Error("Unnamed struct not crated"); + } + assert.strictEqual( + unnamedStruct.fullEnum.unnamedStruct[0].u8, + tupleStructArg[0].u8 + ); + assert.strictEqual( + unnamedStruct.fullEnum.unnamedStruct[0].u16, + tupleStructArg[0].u16 + ); + assert.strictEqual( + unnamedStruct.fullEnum.unnamedStruct[0].u32, + tupleStructArg[0].u32 + ); + assert( + unnamedStruct.fullEnum.unnamedStruct[0].u64.eq(tupleStructArg[0].u64) + ); + }); + + it("Can use type aliases", async () => { + const kp = anchor.web3.Keypair.generate(); + + const aliasU8 = 42; + const aliasU8Array = [1, 2, 3, 4, 5, 6, 7, 8]; + const aliasStruct = { + u8: 1, + u16: 2, + u32: 3, + u64: new anchor.BN(4), + }; + const aliasVecString = ["first", "second"]; + const aliasOptionVecPubkey = [anchor.web3.Keypair.generate().publicKey]; + const aliasGenericConst = [1, 23045, 32, 4]; + const aliasMultipleGenericsMixed = [ + [true, false], + [false, true], + ]; + const aliasExternal = new anchor.BN(1708705033); + + await program.methods + .typeAlias( + aliasU8, + aliasU8Array, + aliasStruct, + aliasVecString, + aliasOptionVecPubkey, + aliasGenericConst, + aliasMultipleGenericsMixed, + aliasExternal + ) + .accounts({ account: kp.publicKey }) + .signers([kp]) + .preInstructions([ + await program.account.aliasAccount.createInstruction(kp, 1024), + ]) + .rpc(); + + const account = await program.account.aliasAccount.fetch(kp.publicKey); + + assert.strictEqual(account.aliasU8, aliasU8); + assert.deepEqual(account.aliasU8Array, aliasU8Array); + assert.strictEqual(account.aliasStruct.u8, aliasStruct.u8); + assert.strictEqual(account.aliasStruct.u16, aliasStruct.u16); + assert.strictEqual(account.aliasStruct.u32, aliasStruct.u32); + assert(account.aliasStruct.u64.eq(aliasStruct.u64)); + assert.deepEqual(account.aliasVecString, aliasVecString); + assert.deepEqual(account.aliasOptionVecPubkey, aliasOptionVecPubkey); + assert.deepEqual(account.aliasGenericConst, aliasGenericConst); + assert.deepEqual( + account.aliasMultipleGenericsMixed, + aliasMultipleGenericsMixed + ); + assert(account.aliasExternal.eq(aliasExternal)); + }); + + it("Can use accounts and events as arguments and fields", async () => { + const kp = anchor.web3.Keypair.generate(); + + const accountArg = { + simpleAccount: { fieldName: 2 }, + simpleEvent: { fieldName: 4 }, + }; + await program.methods + .accountAndEventArgAndField(accountArg) + .accounts({ account: kp.publicKey }) + .signers([kp]) + .preInstructions([ + await program.account.accountAndEventFieldAccount.createInstruction( + kp + ), + ]) + .rpc(); + + const account = await program.account.accountAndEventFieldAccount.fetch( + kp.publicKey + ); + assert.deepEqual(account, accountArg); + }); + + it("Can use generics", async () => { + const arg = { + arr: [6, 123, 2, 3], + subField: { + subArr: new Array(8).fill(null).map((_, i) => i + 33), + another: [30211, 65050, 21, 441], + }, + }; + const { pubkeys } = await program.methods.generic(arg).rpcAndKeys(); + const myAccount = await program.account.genericAccount.fetch( + pubkeys.myAccount + ); + assert.deepEqual(myAccount.field, arg); + }); + + it("Can use full module path types", async () => { + const kp = anchor.web3.Keypair.generate(); + + const namedStructArg = { u8: 1, u16: 2, u32: 3, u64: new anchor.BN(4) }; + const someModuleNamedStructArg = { data: 5 }; + + await program.methods + .fullPath(namedStructArg, someModuleNamedStructArg) + .accounts({ account: kp.publicKey }) + .preInstructions([ + await program.account.fullPathAccount.createInstruction(kp), + ]) + .signers([kp]) + .rpc(); + + const fullPathAccount = await program.account.fullPathAccount.fetch( + kp.publicKey + ); + assert.strictEqual(fullPathAccount.namedStruct.u8, namedStructArg.u8); + assert.strictEqual(fullPathAccount.namedStruct.u16, namedStructArg.u16); + assert.strictEqual(fullPathAccount.namedStruct.u32, namedStructArg.u32); + assert(fullPathAccount.namedStruct.u64.eq(namedStructArg.u64)); + assert.deepEqual( + fullPathAccount.someModuleNamedStruct, + someModuleNamedStructArg + ); + }); + + it("Can use external types", async () => { + const externalArg = { someField: 5 }; + + const kp = anchor.web3.Keypair.generate(); + await program.methods + .external(externalArg) + .accounts({ account: kp.publicKey }) + .signers([kp]) + .preInstructions([ + await program.account.accountWithExternalField.createInstruction(kp), + ]) + .rpc(); + + const account = await program.account.accountWithExternalField.fetch( + kp.publicKey + ); + + assert.deepEqual(account.myStruct, externalArg); + }); + + it("Can use non-Anchor external types", async () => { + const feature = { activatedAt: new anchor.BN(42) }; + + const kp = anchor.web3.Keypair.generate(); + await program.methods + .externalNonAnchor(feature) + .accounts({ account: kp.publicKey }) + .signers([kp]) + .preInstructions([ + await program.account.accountWithNonAnchorExternalField.createInstruction( + kp + ), + ]) + .rpc(); + + const account = + await program.account.accountWithNonAnchorExternalField.fetch( + kp.publicKey + ); + + assert(account.feature.activatedAt?.eq(feature.activatedAt)); + }); + }); + + describe("Format", () => { + const ixCoder = new anchor.BorshInstructionCoder(program.idl); + + const formatEnum = async (argName: string, data: any, expected: string) => { + const typeName = Object.keys(data)[0]; + const arg = data[typeName]; + const ix = await program.methods + .enm(arg) + .accounts({ account: anchor.web3.PublicKey.default }) + .instruction(); + + const formattedIx = ixCoder.format({ name: "enm", data }, ix.keys); + if (!formattedIx) throw new Error("Failed to format"); + + assert.deepEqual(formattedIx.args[0], { + name: argName, + type: typeName, + data: expected, + }); + }; + + it("Can format unit enum", async () => { + await formatEnum("fullEnum", { fullEnum: { unit: {} } }, "unit"); + }); + + it("Can format named enum", async () => { + await formatEnum( + "fullEnum", + { + fullEnum: { + named: { pointX: new anchor.BN(1), pointY: new anchor.BN(2) }, + }, + }, + "named { pointX: 1, pointY: 2 }" + ); + }); + + it("Can format tuple enum", async () => { + await formatEnum( + "fullEnum", + { fullEnum: { unnamed: [2, 10, 200, 49] } }, + "unnamed { 0: 2, 1: 10, 2: 200, 3: 49 }" + ); + }); + }); +}); diff --git a/tests/idl/tests/workspace.ts b/tests/idl/tests/workspace.ts index c240bdd9b6..458a2c6673 100644 --- a/tests/idl/tests/workspace.ts +++ b/tests/idl/tests/workspace.ts @@ -25,7 +25,7 @@ describe("Workspace", () => { const compareProgramNames = (...programs: anchor.Program[]) => { return programs.every( - (program) => program.idl.name === "relations_derivation" + (program) => program.rawIdl.metadata.name === "relations_derivation" ); }; diff --git a/tests/idl/tsconfig.json b/tests/idl/tsconfig.json index 774260253f..feba6ca17f 100644 --- a/tests/idl/tsconfig.json +++ b/tests/idl/tsconfig.json @@ -4,6 +4,7 @@ "lib": ["es2015"], "module": "commonjs", "target": "es6", - "esModuleInterop": true + "esModuleInterop": true, + "strict": true } } diff --git a/tests/ido-pool/programs/ido-pool/Cargo.toml b/tests/ido-pool/programs/ido-pool/Cargo.toml index d7558bf745..6e555311ab 100644 --- a/tests/ido-pool/programs/ido-pool/Cargo.toml +++ b/tests/ido-pool/programs/ido-pool/Cargo.toml @@ -14,6 +14,7 @@ no-entrypoint = [] no-idl = [] cpi = ["no-entrypoint"] default = [] +idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] [dependencies] anchor-lang = { path = "../../../../lang" } diff --git a/tests/misc/programs/init-if-needed/Cargo.toml b/tests/misc/programs/init-if-needed/Cargo.toml index 8843138b0b..0a8f5b70fa 100644 --- a/tests/misc/programs/init-if-needed/Cargo.toml +++ b/tests/misc/programs/init-if-needed/Cargo.toml @@ -14,6 +14,7 @@ no-idl = [] no-log-ix-name = [] cpi = ["no-entrypoint"] default = [] +idl-build = ["anchor-lang/idl-build"] [dependencies] anchor-lang = { path = "../../../../lang", features = ["init-if-needed"] } diff --git a/tests/misc/programs/lamports/Cargo.toml b/tests/misc/programs/lamports/Cargo.toml index ea99d59e5b..341b34880f 100644 --- a/tests/misc/programs/lamports/Cargo.toml +++ b/tests/misc/programs/lamports/Cargo.toml @@ -10,6 +10,7 @@ crate-type = ["cdylib", "lib"] [features] no-entrypoint = [] cpi = ["no-entrypoint"] +idl-build = ["anchor-lang/idl-build"] [dependencies] anchor-lang = { path = "../../../../lang" } diff --git a/tests/misc/programs/misc-optional/Cargo.toml b/tests/misc/programs/misc-optional/Cargo.toml index 366cc8d1c9..694f8ef200 100644 --- a/tests/misc/programs/misc-optional/Cargo.toml +++ b/tests/misc/programs/misc-optional/Cargo.toml @@ -14,6 +14,7 @@ no-entrypoint = [] no-idl = [] cpi = ["no-entrypoint"] default = [] +idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] [dependencies] anchor-lang = { path = "../../../../lang", features = ["init-if-needed"] } diff --git a/tests/misc/programs/misc/Cargo.toml b/tests/misc/programs/misc/Cargo.toml index caa7507bac..7b5a77c685 100644 --- a/tests/misc/programs/misc/Cargo.toml +++ b/tests/misc/programs/misc/Cargo.toml @@ -14,6 +14,7 @@ no-entrypoint = [] no-idl = [] cpi = ["no-entrypoint"] default = [] +idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] [dependencies] anchor-lang = { path = "../../../../lang", features = ["init-if-needed"] } diff --git a/tests/misc/programs/overflow-checks/Cargo.toml b/tests/misc/programs/overflow-checks/Cargo.toml index 52a1c5d5cf..252c84c13e 100644 --- a/tests/misc/programs/overflow-checks/Cargo.toml +++ b/tests/misc/programs/overflow-checks/Cargo.toml @@ -14,6 +14,7 @@ no-idl = [] no-log-ix-name = [] cpi = ["no-entrypoint"] default = [] +idl-build = ["anchor-lang/idl-build"] [dependencies] anchor-lang = { path = "../../../../lang" } diff --git a/tests/misc/programs/remaining-accounts/Cargo.toml b/tests/misc/programs/remaining-accounts/Cargo.toml index 19f2010d5f..c436170e0a 100644 --- a/tests/misc/programs/remaining-accounts/Cargo.toml +++ b/tests/misc/programs/remaining-accounts/Cargo.toml @@ -14,6 +14,7 @@ no-entrypoint = [] no-idl = [] cpi = ["no-entrypoint"] default = [] +idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] [dependencies] anchor-lang = { path = "../../../../lang", features = ["init-if-needed"] } diff --git a/tests/misc/tests/lamports/lamports.ts b/tests/misc/tests/lamports/lamports.ts index 678ed5c795..e5c54dadbd 100644 --- a/tests/misc/tests/lamports/lamports.ts +++ b/tests/misc/tests/lamports/lamports.ts @@ -1,8 +1,8 @@ import * as anchor from "@coral-xyz/anchor"; -import { Lamports, IDL } from "../../target/types/lamports"; +import { Lamports } from "../../target/types/lamports"; -describe(IDL.name, () => { +describe("lamports", () => { // Configure the client to use the local cluster anchor.setProvider(anchor.AnchorProvider.env()); diff --git a/tests/misc/tests/misc/misc.ts b/tests/misc/tests/misc/misc.ts index 3567d928bb..413f637ed3 100644 --- a/tests/misc/tests/misc/misc.ts +++ b/tests/misc/tests/misc/misc.ts @@ -438,18 +438,18 @@ const miscTest = ( ]; assert.deepStrictEqual(expectedRaw, resp.raw.slice(0, -2)); - assert.strictEqual(resp.events[0].name, "E1"); + assert.strictEqual(resp.events[0].name, "e1"); assert.strictEqual(resp.events[0].data.data, 44); - assert.strictEqual(resp.events[1].name, "E2"); + assert.strictEqual(resp.events[1].name, "e2"); assert.strictEqual(resp.events[1].data.data, 1234); - assert.strictEqual(resp.events[2].name, "E3"); + assert.strictEqual(resp.events[2].name, "e3"); assert.strictEqual(resp.events[2].data.data, 9); - assert.strictEqual(resp.events[3].name, "E5"); + assert.strictEqual(resp.events[3].name, "e5"); assert.deepStrictEqual( resp.events[3].data.data, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] ); - assert.strictEqual(resp.events[4].name, "E6"); + assert.strictEqual(resp.events[4].name, "e6"); assert.deepStrictEqual( resp.events[4].data.data, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] diff --git a/tests/misc/tests/overflow-checks/overflow-checks.ts b/tests/misc/tests/overflow-checks/overflow-checks.ts index 8d08c271d2..615ee32258 100644 --- a/tests/misc/tests/overflow-checks/overflow-checks.ts +++ b/tests/misc/tests/overflow-checks/overflow-checks.ts @@ -1,8 +1,8 @@ import * as anchor from "@coral-xyz/anchor"; -import { OverflowChecks, IDL } from "../../target/types/overflow_checks"; +import { OverflowChecks } from "../../target/types/overflow_checks"; -describe(IDL.name, () => { +describe("overflow-checks", () => { anchor.setProvider(anchor.AnchorProvider.env()); const program = anchor.workspace diff --git a/tests/misc/tests/remaining-accounts/remaining-accounts.ts b/tests/misc/tests/remaining-accounts/remaining-accounts.ts index 7a9fc9009e..b3160fc60c 100644 --- a/tests/misc/tests/remaining-accounts/remaining-accounts.ts +++ b/tests/misc/tests/remaining-accounts/remaining-accounts.ts @@ -1,11 +1,11 @@ import * as anchor from "@coral-xyz/anchor"; +import NodeWallet from "@coral-xyz/anchor/dist/cjs/nodewallet"; import { TOKEN_PROGRAM_ID, Token } from "@solana/spl-token"; import { assert } from "chai"; -import { RemainingAccounts, IDL } from "../../target/types/remaining_accounts"; -import NodeWallet from "@coral-xyz/anchor/dist/cjs/nodewallet"; +import { RemainingAccounts } from "../../target/types/remaining_accounts"; -describe(IDL.name, () => { +describe("remaining-accounts", () => { // Configure the client to use the local cluster anchor.setProvider(anchor.AnchorProvider.env()); const payer = NodeWallet.local().payer; diff --git a/tests/multiple-suites-run-single/programs/multiple-suites-run-single/Cargo.toml b/tests/multiple-suites-run-single/programs/multiple-suites-run-single/Cargo.toml index 5b5e102925..c995e42aa9 100644 --- a/tests/multiple-suites-run-single/programs/multiple-suites-run-single/Cargo.toml +++ b/tests/multiple-suites-run-single/programs/multiple-suites-run-single/Cargo.toml @@ -14,6 +14,7 @@ no-idl = [] no-log-ix-name = [] cpi = ["no-entrypoint"] default = [] +idl-build = ["anchor-lang/idl-build"] [dependencies] anchor-lang = { path = "../../../../lang" } diff --git a/tests/multiple-suites/programs/multiple-suites/Cargo.toml b/tests/multiple-suites/programs/multiple-suites/Cargo.toml index fca9cd19fa..cd84759c4e 100644 --- a/tests/multiple-suites/programs/multiple-suites/Cargo.toml +++ b/tests/multiple-suites/programs/multiple-suites/Cargo.toml @@ -14,6 +14,7 @@ no-idl = [] no-log-ix-name = [] cpi = ["no-entrypoint"] default = [] +idl-build = ["anchor-lang/idl-build"] [dependencies] anchor-lang = { path = "../../../../lang" } diff --git a/tests/multisig/programs/multisig/Cargo.toml b/tests/multisig/programs/multisig/Cargo.toml index 1293910f33..330a5cc4a5 100644 --- a/tests/multisig/programs/multisig/Cargo.toml +++ b/tests/multisig/programs/multisig/Cargo.toml @@ -14,6 +14,7 @@ no-entrypoint = [] no-idl = [] cpi = ["no-entrypoint"] default = [] +idl-build = ["anchor-lang/idl-build"] [dependencies] anchor-lang = { path = "../../../../lang" } diff --git a/tests/multisig/tests/multisig.js b/tests/multisig/tests/multisig.js index f65e7d1822..f3581ed159 100644 --- a/tests/multisig/tests/multisig.js +++ b/tests/multisig/tests/multisig.js @@ -59,7 +59,7 @@ describe("multisig", () => { }, ]; const newOwners = [ownerA.publicKey, ownerB.publicKey, ownerD.publicKey]; - const data = program.coder.instruction.encode("set_owners", { + const data = program.coder.instruction.encode("setOwners", { owners: newOwners, }); diff --git a/tests/optional/programs/allow-missing-optionals/Cargo.toml b/tests/optional/programs/allow-missing-optionals/Cargo.toml index 90ae32316b..dddf7fd0db 100644 --- a/tests/optional/programs/allow-missing-optionals/Cargo.toml +++ b/tests/optional/programs/allow-missing-optionals/Cargo.toml @@ -11,6 +11,7 @@ name = "allow_missing_optionals" [features] no-entrypoint = [] cpi = ["no-entrypoint"] +idl-build = ["anchor-lang/idl-build"] [dependencies] anchor-lang = { path = "../../../../lang", features = ["allow-missing-optionals"] } diff --git a/tests/optional/programs/optional/Cargo.toml b/tests/optional/programs/optional/Cargo.toml index 090174508c..3711f52f87 100644 --- a/tests/optional/programs/optional/Cargo.toml +++ b/tests/optional/programs/optional/Cargo.toml @@ -11,6 +11,7 @@ name = "optional" [features] no-entrypoint = [] cpi = ["no-entrypoint"] +idl-build = ["anchor-lang/idl-build"] [dependencies] anchor-lang = { path = "../../../../lang" } diff --git a/tests/optional/programs/optional/src/context.rs b/tests/optional/programs/optional/src/context.rs index 34303963a7..4509e45250 100644 --- a/tests/optional/programs/optional/src/context.rs +++ b/tests/optional/programs/optional/src/context.rs @@ -10,7 +10,7 @@ pub struct Initialize<'info> { pub system_program: Option>, #[account(zero)] pub required: Account<'info, DataAccount>, - #[account(init, seeds=[DataPda::PREFIX.as_ref(), optional_account.as_ref().unwrap().key().as_ref()], bump, payer=payer, space=DataPda::LEN)] + #[account(init, seeds=[DataPda::PREFIX.as_bytes(), optional_account.as_ref().unwrap().key().as_ref()], bump, payer=payer, space=DataPda::LEN)] pub optional_pda: Option>, } @@ -19,22 +19,22 @@ pub struct Initialize<'info> { pub struct Update<'info> { #[account(mut)] pub payer: Option>, - #[account(mut, seeds=[DataPda::PREFIX.as_ref(), optional_account.as_ref().unwrap().key().as_ref()], bump = pda_bump)] + #[account(mut, seeds=[DataPda::PREFIX.as_bytes(), optional_account.as_ref().unwrap().key().as_ref()], bump = pda_bump)] pub optional_pda: Option>, #[account(mut, signer, constraint = payer.is_some())] pub optional_account: Option>>, } #[derive(Accounts)] -#[instruction(new_size: usize)] +#[instruction(new_size: u64)] pub struct Realloc<'info> { #[account(mut)] pub payer: Option>, - #[account(mut, realloc = new_size, realloc::payer = payer, realloc::zero = false)] + #[account(mut, realloc = new_size as usize, realloc::payer = payer, realloc::zero = false)] pub optional_pda: Option>, pub required: Account<'info, DataAccount>, pub system_program: Option>, - #[account(mut, signer, realloc = new_size, realloc::payer = payer, realloc::zero = true)] + #[account(mut, signer, realloc = new_size as usize, realloc::payer = payer, realloc::zero = true)] pub optional_account: Option>, } diff --git a/tests/optional/tests/optional.ts b/tests/optional/tests/optional.ts index 19aeeb7f71..dc4bf4ad30 100644 --- a/tests/optional/tests/optional.ts +++ b/tests/optional/tests/optional.ts @@ -75,7 +75,7 @@ describe("Optional", () => { const [requiredKeypair, createRequiredIx] = await createRequired(); const initializeIx = await program.methods .initialize(initializeValue1, initializeKey) - .accounts({ + .accountsPartial({ payer: null, optionalAccount: null, systemProgram, @@ -115,7 +115,7 @@ describe("Optional", () => { .AllowMissingOptionals as Program; const doStuffIx = await allowMissingOptionals.methods .doStuff() - .accounts({ + .accountsPartial({ payer, systemProgram, optional2: null, @@ -135,7 +135,7 @@ describe("Optional", () => { await program.methods .initialize(initializeValue1, initializeKey) .preInstructions([createRequiredIx]) - .accounts({ + .accountsPartial({ payer, systemProgram, // @ts-ignore @@ -149,7 +149,7 @@ describe("Optional", () => { "Unexpected success in creating a transaction that should have failed at the client level" ); } catch (e) { - const errMsg = "Invalid arguments: required not provided"; + const errMsg = "Account `required` not provided"; // @ts-ignore let error: string = e.toString(); assert(error.includes(errMsg), `Unexpected error: ${e}`); @@ -161,7 +161,7 @@ describe("Optional", () => { await program.methods .initialize(initializeValue1, initializeKey) .preInstructions([createRequiredIx]) - .accounts({ + .accountsPartial({ payer: null, systemProgram, required: requiredKeypair.publicKey, @@ -182,7 +182,7 @@ describe("Optional", () => { await program.methods .initialize(initializeValue1, initializeKey) .preInstructions([createRequiredIx]) - .accounts({ + .accountsPartial({ payer: null, systemProgram: null, required: requiredKeypair.publicKey, @@ -205,7 +205,7 @@ describe("Optional", () => { await program.methods .initialize(initializeValue1, initializeKey) .preInstructions([createRequiredIx]) - .accounts({ + .accountsStrict({ payer, systemProgram: null, required: requiredKeypair.publicKey, @@ -238,7 +238,7 @@ describe("Optional", () => { await program.methods .initialize(initializeValue1, initializeKey) .preInstructions([createRequiredIx]) - .accounts({ + .accountsPartial({ payer, systemProgram, required: requiredKeypair.publicKey, @@ -262,7 +262,7 @@ describe("Optional", () => { await program.methods .initialize(initializeValue1, initializeKey) .preInstructions([createRequiredIx1]) - .accounts({ + .accountsPartial({ payer, systemProgram, required: requiredKeypair1.publicKey, @@ -290,7 +290,7 @@ describe("Optional", () => { await program.methods .initialize(initializeValue2, initializeKey) .preInstructions([createRequiredIx2]) - .accounts({ + .accountsPartial({ payer, systemProgram, required: requiredKeypair2.publicKey, @@ -319,7 +319,7 @@ describe("Optional", () => { await program.methods .initialize(initializeValue2, initializeKey) .preInstructions([createRequiredIx2]) - .accounts({ + .accountsPartial({ payer, systemProgram, required: requiredKeypair2.publicKey, @@ -352,7 +352,7 @@ describe("Optional", () => { it("Can update with invalid explicit pda bump with no pda", async () => { await program.methods .update(initializeValue2, initializeKey, dataPda2[1] - 1) - .accounts({ + .accountsPartial({ payer, optionalPda: null, optionalAccount: null, @@ -364,7 +364,7 @@ describe("Optional", () => { try { await program.methods .update(initializeValue2, initializeKey, dataPda2[1] - 1) - .accounts({ + .accountsPartial({ payer, optionalPda: dataPda2[0], optionalAccount: dataAccountKeypair2.publicKey, @@ -391,7 +391,7 @@ describe("Optional", () => { try { let txn = await program.methods .update(initializeValue2, initializeKey, dataPda2[1]) - .accounts({ + .accountsPartial({ payer, optionalPda: dataPda2[0], optionalAccount: dataAccountKeypair2.publicKey, @@ -424,7 +424,7 @@ describe("Optional", () => { try { await program.methods .update(initializeValue2, initializeKey, dataPda2[1]) - .accounts({ + .accountsPartial({ payer: null, optionalPda: dataPda2[0], optionalAccount: dataAccountKeypair2.publicKey, @@ -450,7 +450,7 @@ describe("Optional", () => { it("Can update an optional account", async () => { await program.methods .update(initializeValue2.muln(3), initializeKey, dataPda2[1]) - .accounts({ + .accountsPartial({ payer, optionalPda: null, optionalAccount: dataAccountKeypair2.publicKey, @@ -470,7 +470,7 @@ describe("Optional", () => { const newKey = web3.PublicKey.unique(); await program.methods .update(initializeValue2, newKey, dataPda2[1]) - .accounts({ + .accountsPartial({ payer, optionalPda: dataPda2[0], optionalAccount: dataAccountKeypair2.publicKey, @@ -493,7 +493,7 @@ describe("Optional", () => { try { await program.methods .realloc(new BN(100)) - .accounts({ + .accountsPartial({ payer: null, required: dataAccountKeypair1.publicKey, optionalPda: null, @@ -522,7 +522,7 @@ describe("Optional", () => { try { await program.methods .realloc(new BN(100)) - .accounts({ + .accountsStrict({ payer, required: dataAccountKeypair1.publicKey, optionalPda: null, @@ -551,7 +551,7 @@ describe("Optional", () => { try { await program.methods .realloc(new BN(100)) - .accounts({ + .accountsPartial({ payer, required: dataAccountKeypair1.publicKey, optionalPda: dataAccountKeypair2.publicKey, @@ -579,7 +579,7 @@ describe("Optional", () => { const newLength = 100; await program.methods .realloc(new BN(newLength)) - .accounts({ + .accountsPartial({ payer, required: dataAccountKeypair1.publicKey, optionalPda: null, @@ -599,7 +599,7 @@ describe("Optional", () => { const newLength = program.account.dataAccount.size; await program.methods .realloc(new BN(newLength)) - .accounts({ + .accountsPartial({ payer, required: dataAccountKeypair1.publicKey, optionalPda: null, @@ -619,7 +619,7 @@ describe("Optional", () => { const newLength = 100; await program.methods .realloc(new BN(newLength)) - .accounts({ + .accountsPartial({ payer, required: dataAccountKeypair1.publicKey, optionalPda: dataPda2[0], @@ -690,7 +690,7 @@ describe("Optional", () => { await program.methods .initialize(initializeValue3, initializeKey) .preInstructions([createRequiredIx3]) - .accounts({ + .accountsPartial({ payer, systemProgram, required: requiredKeypair3.publicKey, @@ -708,7 +708,7 @@ describe("Optional", () => { await program.methods .initialize(initializeValue4, initializeKey) .preInstructions([createRequiredIx4]) - .accounts({ + .accountsPartial({ payer, systemProgram, required: requiredKeypair4.publicKey, @@ -726,7 +726,7 @@ describe("Optional", () => { await program.methods .update(initializeValue3, dataAccountKeypair3.publicKey, dataPda3[1]) - .accounts({ + .accountsPartial({ payer, optionalPda: dataPda3[0], optionalAccount: dataAccountKeypair3.publicKey, @@ -739,7 +739,7 @@ describe("Optional", () => { ); await program.methods .update(initializeValue4, dataAccountKeypair4.publicKey, dataPda4[1]) - .accounts({ + .accountsPartial({ payer, optionalPda: dataPda4[0], optionalAccount: dataAccountKeypair4.publicKey, @@ -756,7 +756,7 @@ describe("Optional", () => { try { await program.methods .close() - .accounts({ + .accountsPartial({ payer: null, optionalPda: null, dataAccount: dataAccountKeypair3.publicKey, @@ -784,7 +784,7 @@ describe("Optional", () => { try { await program.methods .close() - .accounts({ + .accountsPartial({ payer, optionalPda: dataPda4[0], dataAccount: dataAccountKeypair3.publicKey, @@ -811,7 +811,7 @@ describe("Optional", () => { it("Can close an optional account", async () => { await program.methods .close() - .accounts({ + .accountsPartial({ payer, optionalPda: null, dataAccount: dataAccountKeypair3.publicKey, @@ -828,7 +828,7 @@ describe("Optional", () => { it("Can close multiple optional accounts", async () => { await program.methods .close() - .accounts({ + .accountsPartial({ payer, optionalPda: dataPda4[0], dataAccount: dataAccountKeypair4.publicKey, diff --git a/tests/pda-derivation/programs/pda-derivation/Cargo.toml b/tests/pda-derivation/programs/pda-derivation/Cargo.toml index afaebd9588..f210e4febe 100644 --- a/tests/pda-derivation/programs/pda-derivation/Cargo.toml +++ b/tests/pda-derivation/programs/pda-derivation/Cargo.toml @@ -14,6 +14,7 @@ no-entrypoint = [] no-idl = [] cpi = ["no-entrypoint"] default = [] +idl-build = ["anchor-lang/idl-build"] [dependencies] anchor-lang = { path = "../../../../lang" } diff --git a/tests/pyth/programs/pyth/Cargo.toml b/tests/pyth/programs/pyth/Cargo.toml index e3ab5484ae..2e77b55134 100644 --- a/tests/pyth/programs/pyth/Cargo.toml +++ b/tests/pyth/programs/pyth/Cargo.toml @@ -14,6 +14,7 @@ no-entrypoint = [] no-idl = [] cpi = ["no-entrypoint"] default = [] +idl-build = ["anchor-lang/idl-build"] [dependencies] anchor-lang = { path = "../../../../lang" } diff --git a/tests/realloc/programs/realloc/Cargo.toml b/tests/realloc/programs/realloc/Cargo.toml index 4f014303e2..7cf118ddc1 100644 --- a/tests/realloc/programs/realloc/Cargo.toml +++ b/tests/realloc/programs/realloc/Cargo.toml @@ -14,6 +14,7 @@ no-idl = [] no-log-ix-name = [] cpi = ["no-entrypoint"] default = [] +idl-build = ["anchor-lang/idl-build"] [profile.release] overflow-checks = true diff --git a/tests/relations-derivation/programs/relations-derivation/Cargo.toml b/tests/relations-derivation/programs/relations-derivation/Cargo.toml index 3e373f6a26..d97e6116e8 100644 --- a/tests/relations-derivation/programs/relations-derivation/Cargo.toml +++ b/tests/relations-derivation/programs/relations-derivation/Cargo.toml @@ -14,6 +14,7 @@ no-entrypoint = [] no-idl = [] cpi = ["no-entrypoint"] default = [] +idl-build = ["anchor-lang/idl-build"] [dependencies] anchor-lang = { path = "../../../../lang" } diff --git a/tests/safety-checks/programs/account-info/Cargo.toml b/tests/safety-checks/programs/account-info/Cargo.toml index e222f06ed7..495db50692 100644 --- a/tests/safety-checks/programs/account-info/Cargo.toml +++ b/tests/safety-checks/programs/account-info/Cargo.toml @@ -14,6 +14,7 @@ no-idl = [] no-log-ix-name = [] cpi = ["no-entrypoint"] default = [] +idl-build = ["anchor-lang/idl-build"] [dependencies] anchor-lang = { path = "../../../../lang" } diff --git a/tests/safety-checks/programs/ignore-non-accounts/Cargo.toml b/tests/safety-checks/programs/ignore-non-accounts/Cargo.toml index e8de691a5d..284046b1ae 100644 --- a/tests/safety-checks/programs/ignore-non-accounts/Cargo.toml +++ b/tests/safety-checks/programs/ignore-non-accounts/Cargo.toml @@ -14,6 +14,7 @@ no-idl = [] no-log-ix-name = [] cpi = ["no-entrypoint"] default = [] +idl-build = ["anchor-lang/idl-build"] [dependencies] anchor-lang = { path = "../../../../lang" } diff --git a/tests/safety-checks/programs/unchecked-account/Cargo.toml b/tests/safety-checks/programs/unchecked-account/Cargo.toml index 69443051c2..c5f455db83 100644 --- a/tests/safety-checks/programs/unchecked-account/Cargo.toml +++ b/tests/safety-checks/programs/unchecked-account/Cargo.toml @@ -14,6 +14,7 @@ no-idl = [] no-log-ix-name = [] cpi = ["no-entrypoint"] default = [] +idl-build = ["anchor-lang/idl-build"] [dependencies] anchor-lang = { path = "../../../../lang" } diff --git a/tests/spl/metadata/programs/metadata/Cargo.toml b/tests/spl/metadata/programs/metadata/Cargo.toml index c38e0fb52f..cf26afec98 100644 --- a/tests/spl/metadata/programs/metadata/Cargo.toml +++ b/tests/spl/metadata/programs/metadata/Cargo.toml @@ -14,6 +14,7 @@ no-entrypoint = [] no-idl = [] cpi = ["no-entrypoint"] default = [] +idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] [dependencies] anchor-lang = { path = "../../../../../lang" } diff --git a/tests/spl/token-proxy/programs/token-proxy/Cargo.toml b/tests/spl/token-proxy/programs/token-proxy/Cargo.toml index 122355d369..5a5799e935 100644 --- a/tests/spl/token-proxy/programs/token-proxy/Cargo.toml +++ b/tests/spl/token-proxy/programs/token-proxy/Cargo.toml @@ -12,6 +12,7 @@ name = "token_proxy" [features] no-entrypoint = [] cpi = ["no-entrypoint"] +idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] [dependencies] anchor-lang = { path = "../../../../../lang" } diff --git a/tests/spl/token-wrapper/programs/token-wrapper/Cargo.toml b/tests/spl/token-wrapper/programs/token-wrapper/Cargo.toml index e83d329891..33ea951d85 100644 --- a/tests/spl/token-wrapper/programs/token-wrapper/Cargo.toml +++ b/tests/spl/token-wrapper/programs/token-wrapper/Cargo.toml @@ -14,6 +14,7 @@ no-entrypoint = [] no-idl = [] cpi = ["no-entrypoint"] default = [] +idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] [dependencies] anchor-lang = { path = "../../../../../lang" } diff --git a/tests/spl/transfer-hook/programs/transfer-hook/Cargo.toml b/tests/spl/transfer-hook/programs/transfer-hook/Cargo.toml index 1c76665c7d..d41e798fa9 100644 --- a/tests/spl/transfer-hook/programs/transfer-hook/Cargo.toml +++ b/tests/spl/transfer-hook/programs/transfer-hook/Cargo.toml @@ -14,6 +14,7 @@ no-entrypoint = [] no-idl = [] cpi = ["no-entrypoint"] default = [] +idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] [dependencies] anchor-lang = { path = "../../../../../lang", features = ["interface-instructions"] } diff --git a/tests/spl/transfer-hook/tests/transfer-hook.ts b/tests/spl/transfer-hook/tests/transfer-hook.ts index 6fca5350a2..249cded332 100644 --- a/tests/spl/transfer-hook/tests/transfer-hook.ts +++ b/tests/spl/transfer-hook/tests/transfer-hook.ts @@ -146,7 +146,6 @@ describe("transfer hook", () => { it("can create an `InitializeExtraAccountMetaList` instruction with the proper discriminator", async () => { const ix = await program.methods .initialize(extraMetas as any[]) - .interface("spl_transfer_hook_interface::initialize_extra_account_metas") .accounts({ extraMetasAccount: extraMetasAddress, mint: mint.publicKey, @@ -162,8 +161,7 @@ describe("transfer hook", () => { ); const { name, data } = new anchor.BorshInstructionCoder(program.idl).decode( ix.data, - "hex", - "initialize" + "hex" ); assert.equal(name, "initialize"); assert.property(data, "metas"); @@ -174,7 +172,6 @@ describe("transfer hook", () => { it("can create an `Execute` instruction with the proper discriminator", async () => { const ix = await program.methods .execute(new anchor.BN(transferAmount)) - .interface("spl_transfer_hook_interface::execute") .accounts({ sourceAccount: source, mint: mint.publicKey, @@ -193,8 +190,7 @@ describe("transfer hook", () => { ); const { name, data } = new anchor.BorshInstructionCoder(program.idl).decode( ix.data, - "hex", - "execute" + "hex" ); assert.equal(name, "execute"); assert.property(data, "amount"); @@ -206,7 +202,6 @@ describe("transfer hook", () => { // Initialize the extra metas await program.methods .initialize(extraMetas as any[]) - .interface("spl_transfer_hook_interface::initialize_extra_account_metas") .accounts({ extraMetasAccount: extraMetasAddress, mint: mint.publicKey, diff --git a/tests/swap/programs/swap/Cargo.toml b/tests/swap/programs/swap/Cargo.toml index e45242d9e9..ac43344e3d 100644 --- a/tests/swap/programs/swap/Cargo.toml +++ b/tests/swap/programs/swap/Cargo.toml @@ -14,6 +14,7 @@ no-entrypoint = [] no-idl = [] cpi = ["no-entrypoint"] default = [] +idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] [dependencies] anchor-lang = { path = "../../../../lang" } diff --git a/tests/system-accounts/programs/system-accounts/Cargo.toml b/tests/system-accounts/programs/system-accounts/Cargo.toml index f50c6b6686..474dd452cf 100644 --- a/tests/system-accounts/programs/system-accounts/Cargo.toml +++ b/tests/system-accounts/programs/system-accounts/Cargo.toml @@ -12,6 +12,7 @@ name = "system_accounts" [features] no-entrypoint = [] cpi = ["no-entrypoint"] +idl-build = ["anchor-lang/idl-build"] [dependencies] anchor-lang = { path = "../../../../lang" } diff --git a/tests/sysvars/programs/sysvars/Cargo.toml b/tests/sysvars/programs/sysvars/Cargo.toml index e0249a20f1..f1696f5248 100644 --- a/tests/sysvars/programs/sysvars/Cargo.toml +++ b/tests/sysvars/programs/sysvars/Cargo.toml @@ -12,6 +12,7 @@ name = "sysvars" [features] no-entrypoint = [] cpi = ["no-entrypoint"] +idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] [dependencies] anchor-lang = { path = "../../../../lang" } diff --git a/tests/tictactoe/programs/tictactoe/Cargo.toml b/tests/tictactoe/programs/tictactoe/Cargo.toml index b79004546e..c84bf71737 100644 --- a/tests/tictactoe/programs/tictactoe/Cargo.toml +++ b/tests/tictactoe/programs/tictactoe/Cargo.toml @@ -14,6 +14,7 @@ no-entrypoint = [] no-idl = [] cpi = ["no-entrypoint"] default = [] +idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] [dependencies] anchor-lang = { path = "../../../../lang" } diff --git a/tests/typescript/programs/typescript/Cargo.toml b/tests/typescript/programs/typescript/Cargo.toml index ddaaf1e5f1..1e5c86f459 100644 --- a/tests/typescript/programs/typescript/Cargo.toml +++ b/tests/typescript/programs/typescript/Cargo.toml @@ -14,6 +14,7 @@ no-entrypoint = [] no-idl = [] cpi = ["no-entrypoint"] default = [] +idl-build = ["anchor-lang/idl-build"] [dependencies] anchor-lang = { path = "../../../../lang" } diff --git a/tests/validator-clone/programs/validator-clone/Cargo.toml b/tests/validator-clone/programs/validator-clone/Cargo.toml index d246a8c83b..7d21e4f46f 100644 --- a/tests/validator-clone/programs/validator-clone/Cargo.toml +++ b/tests/validator-clone/programs/validator-clone/Cargo.toml @@ -14,6 +14,7 @@ no-idl = [] no-log-ix-name = [] cpi = ["no-entrypoint"] default = [] +idl-build = ["anchor-lang/idl-build"] [dependencies] anchor-lang = { path = "../../../../lang" } diff --git a/tests/zero-copy/programs/zero-copy/Cargo.toml b/tests/zero-copy/programs/zero-copy/Cargo.toml index 1c03914cc5..f83168914f 100644 --- a/tests/zero-copy/programs/zero-copy/Cargo.toml +++ b/tests/zero-copy/programs/zero-copy/Cargo.toml @@ -15,6 +15,7 @@ no-idl = [] cpi = ["no-entrypoint"] default = [] test-sbf = [] +idl-build = ["anchor-lang/idl-build"] [dependencies] anchor-lang = { path = "../../../../lang" } diff --git a/tests/zero-copy/programs/zero-cpi/Cargo.toml b/tests/zero-copy/programs/zero-cpi/Cargo.toml index c3f74ad346..b1ef481cb4 100644 --- a/tests/zero-copy/programs/zero-cpi/Cargo.toml +++ b/tests/zero-copy/programs/zero-cpi/Cargo.toml @@ -14,6 +14,7 @@ no-entrypoint = [] no-idl = [] cpi = ["no-entrypoint"] default = [] +idl-build = ["anchor-lang/idl-build"] [dependencies] anchor-lang = { path = "../../../../lang" } diff --git a/ts/packages/anchor/src/coder/borsh/accounts.ts b/ts/packages/anchor/src/coder/borsh/accounts.ts index 742c5bc826..9998133800 100644 --- a/ts/packages/anchor/src/coder/borsh/accounts.ts +++ b/ts/packages/anchor/src/coder/borsh/accounts.ts @@ -1,12 +1,10 @@ import bs58 from "bs58"; import { Buffer } from "buffer"; import { Layout } from "buffer-layout"; -import camelcase from "camelcase"; -import { Idl, IdlTypeDef } from "../../idl.js"; +import { Idl } from "../../idl.js"; import { IdlCoder } from "./idl.js"; import { AccountsCoder } from "../index.js"; -import { accountSize } from "../common.js"; -import { DISCRIMINATOR_SIZE, discriminator } from "./discriminator.js"; +import { DISCRIMINATOR_SIZE } from "./discriminator.js"; /** * Encodes and decodes account objects. @@ -19,22 +17,26 @@ export class BorshAccountsCoder */ private accountLayouts: Map; - /** - * IDL whose acconts will be coded. - */ - private idl: Idl; - - public constructor(idl: Idl) { - if (idl.accounts === undefined) { + public constructor(private idl: Idl) { + if (!idl.accounts) { this.accountLayouts = new Map(); return; } + + const types = idl.types; + if (!types) { + throw new Error("Accounts require `idl.types`"); + } + const layouts: [A, Layout][] = idl.accounts.map((acc) => { - return [acc.name as A, IdlCoder.typeDefLayout(acc, idl.types)]; + const typeDef = types.find((ty) => ty.name === acc.name); + if (!typeDef) { + throw new Error(`Account not found: ${acc.name}`); + } + return [acc.name as A, IdlCoder.typeDefLayout({ typeDef, types })]; }); this.accountLayouts = new Map(layouts); - this.idl = idl; } public async encode(accountName: A, account: T): Promise { @@ -44,35 +46,35 @@ export class BorshAccountsCoder throw new Error(`Unknown account: ${accountName}`); } const len = layout.encode(account, buffer); - let accountData = buffer.slice(0, len); - let discriminator = BorshAccountsCoder.accountDiscriminator(accountName); + const accountData = buffer.slice(0, len); + const discriminator = this.accountDiscriminator(accountName); return Buffer.concat([discriminator, accountData]); } public decode(accountName: A, data: Buffer): T { // Assert the account discriminator is correct. - const discriminator = BorshAccountsCoder.accountDiscriminator(accountName); - if (discriminator.compare(data.slice(0, 8))) { + const discriminator = this.accountDiscriminator(accountName); + if (discriminator.compare(data.slice(0, DISCRIMINATOR_SIZE))) { throw new Error("Invalid account discriminator"); } return this.decodeUnchecked(accountName, data); } public decodeAny(data: Buffer): T { - const accountDescriminator = data.slice(0, 8); + const discriminator = data.slice(0, DISCRIMINATOR_SIZE); const accountName = Array.from(this.accountLayouts.keys()).find((key) => - BorshAccountsCoder.accountDiscriminator(key).equals(accountDescriminator) + this.accountDiscriminator(key).equals(discriminator) ); if (!accountName) { - throw new Error("Account descriminator not found"); + throw new Error("Account not found"); } - return this.decodeUnchecked(accountName as any, data); + return this.decodeUnchecked(accountName, data); } - public decodeUnchecked(accountName: A, ix: Buffer): T { + public decodeUnchecked(accountName: A, acc: Buffer): T { // Chop off the discriminator before decoding. - const data = ix.subarray(DISCRIMINATOR_SIZE); + const data = acc.subarray(DISCRIMINATOR_SIZE); const layout = this.accountLayouts.get(accountName); if (!layout) { throw new Error(`Unknown account: ${accountName}`); @@ -81,7 +83,7 @@ export class BorshAccountsCoder } public memcmp(accountName: A, appendData?: Buffer): any { - const discriminator = BorshAccountsCoder.accountDiscriminator(accountName); + const discriminator = this.accountDiscriminator(accountName); return { offset: 0, bytes: bs58.encode( @@ -90,8 +92,11 @@ export class BorshAccountsCoder }; } - public size(idlAccount: IdlTypeDef): number { - return DISCRIMINATOR_SIZE + (accountSize(this.idl, idlAccount) ?? 0); + public size(accountName: A): number { + return ( + DISCRIMINATOR_SIZE + + IdlCoder.typeSize({ defined: { name: accountName } }, this.idl) + ); } /** @@ -99,11 +104,12 @@ export class BorshAccountsCoder * * @param name The name of the account to calculate the discriminator. */ - public static accountDiscriminator(name: string): Buffer { - const discriminatorPreimage = `account:${camelcase(name, { - pascalCase: true, - preserveConsecutiveUppercase: true, - })}`; - return discriminator(discriminatorPreimage); + public accountDiscriminator(name: string): Buffer { + const account = this.idl.accounts?.find((acc) => acc.name === name); + if (!account) { + throw new Error(`Account not found: ${name}`); + } + + return Buffer.from(account.discriminator); } } diff --git a/ts/packages/anchor/src/coder/borsh/discriminator.ts b/ts/packages/anchor/src/coder/borsh/discriminator.ts index ec0d24bce6..effc15a918 100644 --- a/ts/packages/anchor/src/coder/borsh/discriminator.ts +++ b/ts/packages/anchor/src/coder/borsh/discriminator.ts @@ -1,10 +1,4 @@ -import { sha256 } from "@noble/hashes/sha256"; - /** * Number of bytes in anchor discriminators */ export const DISCRIMINATOR_SIZE = 8; - -export function discriminator(preimage: string): Buffer { - return Buffer.from(sha256(preimage).slice(0, DISCRIMINATOR_SIZE)); -} diff --git a/ts/packages/anchor/src/coder/borsh/event.ts b/ts/packages/anchor/src/coder/borsh/event.ts index 09e9c72e6f..0ce5c0971e 100644 --- a/ts/packages/anchor/src/coder/borsh/event.ts +++ b/ts/packages/anchor/src/coder/borsh/event.ts @@ -1,11 +1,9 @@ import { Buffer } from "buffer"; import { Layout } from "buffer-layout"; import * as base64 from "../../utils/bytes/base64.js"; -import { Idl, IdlEvent, IdlTypeDef } from "../../idl.js"; -import { Event, EventData } from "../../program/event.js"; +import { Idl } from "../../idl.js"; import { IdlCoder } from "./idl.js"; import { EventCoder } from "../index.js"; -import { discriminator } from "./discriminator.js"; export class BorshEventCoder implements EventCoder { /** @@ -19,37 +17,37 @@ export class BorshEventCoder implements EventCoder { private discriminators: Map; public constructor(idl: Idl) { - if (idl.events === undefined) { + if (!idl.events) { this.layouts = new Map(); return; } - const layouts: [string, Layout][] = idl.events.map((event) => { - let eventTypeDef: IdlTypeDef = { - name: event.name, - type: { - kind: "struct", - fields: event.fields.map((f) => { - return { name: f.name, type: f.type }; - }), - }, - }; - return [event.name, IdlCoder.typeDefLayout(eventTypeDef, idl.types)]; + + const types = idl.types; + if (!types) { + throw new Error("Events require `idl.types`"); + } + + const layouts: [string, Layout][] = idl.events.map((ev) => { + const typeDef = types.find((ty) => ty.name === ev.name); + if (!typeDef) { + throw new Error(`Event not found: ${ev.name}`); + } + return [ev.name, IdlCoder.typeDefLayout({ typeDef, types })]; }); this.layouts = new Map(layouts); this.discriminators = new Map( - idl.events === undefined - ? [] - : idl.events.map((e) => [ - base64.encode(eventDiscriminator(e.name)), - e.name, - ]) + (idl.events ?? []).map((ev) => [ + base64.encode(Buffer.from(ev.discriminator)), + ev.name, + ]) ); } - public decode>( - log: string - ): Event | null { + public decode(log: string): { + name: string; + data: any; + } | null { let logArr: Buffer; // This will throw if log length is not a multiple of 4. try { @@ -61,7 +59,7 @@ export class BorshEventCoder implements EventCoder { // Only deserialize if the discriminator implies a proper event. const eventName = this.discriminators.get(disc); - if (eventName === undefined) { + if (!eventName) { return null; } @@ -69,14 +67,7 @@ export class BorshEventCoder implements EventCoder { if (!layout) { throw new Error(`Unknown event: ${eventName}`); } - const data = layout.decode(logArr.slice(8)) as EventData< - E["fields"][number], - T - >; + const data = layout.decode(logArr.slice(8)); return { data, name: eventName }; } } - -export function eventDiscriminator(name: string): Buffer { - return discriminator(`event:${name}`); -} diff --git a/ts/packages/anchor/src/coder/borsh/idl.ts b/ts/packages/anchor/src/coder/borsh/idl.ts index 2f33de082d..e682f2dcd5 100644 --- a/ts/packages/anchor/src/coder/borsh/idl.ts +++ b/ts/packages/anchor/src/coder/borsh/idl.ts @@ -1,16 +1,25 @@ -import camelCase from "camelcase"; import { Layout } from "buffer-layout"; import * as borsh from "@coral-xyz/borsh"; -import { IdlField, IdlTypeDef, IdlEnumVariant, IdlType } from "../../idl.js"; +import { + IdlField, + IdlTypeDef, + IdlType, + IdlGenericArg, + Idl, + handleDefinedFields, + IdlArrayLen, +} from "../../idl.js"; import { IdlError } from "../../error.js"; +type PartialField = { name?: string } & Pick; + export class IdlCoder { public static fieldLayout( - field: { name?: string } & Pick, - types?: IdlTypeDef[] + field: PartialField, + types: IdlTypeDef[] = [], + genericArgs?: IdlGenericArg[] | null ): Layout { - const fieldName = - field.name !== undefined ? camelCase(field.name) : undefined; + const fieldName = field.name; switch (field.type) { case "bool": { return borsh.bool(fieldName); @@ -63,95 +72,154 @@ export class IdlCoder { case "string": { return borsh.str(fieldName); } - case "publicKey": { + case "pubkey": { return borsh.publicKey(fieldName); } default: { - if ("vec" in field.type) { - return borsh.vec( + if ("option" in field.type) { + return borsh.option( IdlCoder.fieldLayout( - { - name: undefined, - type: field.type.vec, - }, - types + { type: field.type.option }, + types, + genericArgs ), fieldName ); - } else if ("option" in field.type) { - return borsh.option( - IdlCoder.fieldLayout( - { - name: undefined, - type: field.type.option, - }, - types - ), + } + if ("vec" in field.type) { + return borsh.vec( + IdlCoder.fieldLayout({ type: field.type.vec }, types, genericArgs), fieldName ); - } else if ("defined" in field.type) { - // User defined type. + } + if ("array" in field.type) { + let [type, len] = field.type.array; + len = IdlCoder.resolveArrayLen(len, genericArgs); + + return borsh.array( + IdlCoder.fieldLayout({ type }, types, genericArgs), + len, + fieldName + ); + } + if ("defined" in field.type) { if (!types) { throw new IdlError("User defined types not provided"); } - const defined = field.type.defined; - const filtered = types.filter((t) => t.name === defined); - if (filtered.length !== 1) { - throw new IdlError(`Type not found: ${JSON.stringify(field)}`); + const definedName = field.type.defined.name; + const typeDef = types.find((t) => t.name === definedName); + if (!typeDef) { + throw new IdlError(`Type not found: ${field.name}`); } - return IdlCoder.typeDefLayout(filtered[0], types, fieldName); - } else if ("array" in field.type) { - let arrayTy = field.type.array[0]; - let arrayLen = field.type.array[1]; - let innerLayout = IdlCoder.fieldLayout( - { - name: undefined, - type: arrayTy, - }, - types - ); - return borsh.array(innerLayout, arrayLen, fieldName); - } else { - throw new Error(`Not yet implemented: ${field}`); + + return IdlCoder.typeDefLayout({ + typeDef, + types, + genericArgs: genericArgs ?? field.type.defined.generics, + name: fieldName, + }); } + if ("generic" in field.type) { + const genericArg = genericArgs?.at(0); + if (genericArg?.kind !== "type") { + throw new IdlError(`Invalid generic field: ${field.name}`); + } + + return IdlCoder.fieldLayout({ ...field, type: genericArg.type }); + } + + throw new IdlError( + `Not yet implemented: ${JSON.stringify(field.type)}` + ); } } } - public static typeDefLayout( - typeDef: IdlTypeDef, - types: IdlTypeDef[] = [], - name?: string - ): Layout { + /** + * Get the type layout of the given defined type(struct or enum). + */ + public static typeDefLayout({ + typeDef, + types, + name, + genericArgs, + }: { + typeDef: IdlTypeDef; + types: IdlTypeDef[]; + genericArgs?: IdlGenericArg[] | null; + name?: string; + }): Layout { switch (typeDef.type.kind) { case "struct": { - const fieldLayouts = typeDef.type.fields.map((field) => { - return IdlCoder.fieldLayout(field, types); - }); + const fieldLayouts = handleDefinedFields( + typeDef.type.fields, + () => [], + (fields) => + fields.map((f) => { + const genArgs = genericArgs + ? IdlCoder.resolveGenericArgs({ + type: f.type, + typeDef, + genericArgs, + }) + : genericArgs; + return IdlCoder.fieldLayout(f, types, genArgs); + }), + (fields) => + fields.map((f, i) => { + const genArgs = genericArgs + ? IdlCoder.resolveGenericArgs({ + type: f, + typeDef, + genericArgs, + }) + : genericArgs; + return IdlCoder.fieldLayout( + { name: i.toString(), type: f }, + types, + genArgs + ); + }) + ); + return borsh.struct(fieldLayouts, name); } case "enum": { - let variants = typeDef.type.variants.map((variant: IdlEnumVariant) => { - const name = camelCase(variant.name); - if (!variant.fields) { - return borsh.struct([], name); - } - - const fieldLayouts = variant.fields.map( - (f: IdlField | IdlType, i: number) => { - if ((f as IdlField)?.name) { - return IdlCoder.fieldLayout(f as IdlField, types); - } - - return IdlCoder.fieldLayout( - { type: f as IdlType, name: i.toString() }, - types - ); - } + const variants = typeDef.type.variants.map((variant) => { + const fieldLayouts = handleDefinedFields( + variant.fields, + () => [], + (fields) => + fields.map((f) => { + const genArgs = genericArgs + ? IdlCoder.resolveGenericArgs({ + type: f.type, + typeDef, + genericArgs, + }) + : genericArgs; + return IdlCoder.fieldLayout(f, types, genArgs); + }), + (fields) => + fields.map((f, i) => { + const genArgs = genericArgs + ? IdlCoder.resolveGenericArgs({ + type: f, + typeDef, + genericArgs, + }) + : genericArgs; + return IdlCoder.fieldLayout( + { name: i.toString(), type: f }, + types, + genArgs + ); + }) ); - return borsh.struct(fieldLayouts, name); + + return borsh.struct(fieldLayouts, variant.name); }); if (name !== undefined) { @@ -163,9 +231,289 @@ export class IdlCoder { return borsh.rustEnum(variants, name); } - case "alias": { - return IdlCoder.fieldLayout({ type: typeDef.type.value, name }, types); + case "type": { + return IdlCoder.fieldLayout({ type: typeDef.type.alias, name }, types); } } } + + /** + * Get the type of the size in bytes. Returns `1` for variable length types. + */ + public static typeSize( + ty: IdlType, + idl: Idl, + genericArgs?: IdlGenericArg[] | null + ): number { + switch (ty) { + case "bool": + return 1; + case "u8": + return 1; + case "i8": + return 1; + case "i16": + return 2; + case "u16": + return 2; + case "u32": + return 4; + case "i32": + return 4; + case "f32": + return 4; + case "u64": + return 8; + case "i64": + return 8; + case "f64": + return 8; + case "u128": + return 16; + case "i128": + return 16; + case "u256": + return 32; + case "i256": + return 32; + case "bytes": + return 1; + case "string": + return 1; + case "pubkey": + return 32; + default: + if ("option" in ty) { + return 1 + IdlCoder.typeSize(ty.option, idl, genericArgs); + } + if ("coption" in ty) { + return 4 + IdlCoder.typeSize(ty.coption, idl, genericArgs); + } + if ("vec" in ty) { + return 1; + } + if ("array" in ty) { + let [type, len] = ty.array; + len = IdlCoder.resolveArrayLen(len, genericArgs); + return IdlCoder.typeSize(type, idl, genericArgs) * len; + } + if ("defined" in ty) { + const typeDef = idl.types?.find((t) => t.name === ty.defined.name); + if (!typeDef) { + throw new IdlError(`Type not found: ${JSON.stringify(ty)}`); + } + + const typeSize = (type: IdlType) => { + const genArgs = genericArgs ?? ty.defined.generics; + const args = genArgs + ? IdlCoder.resolveGenericArgs({ + type, + typeDef, + genericArgs: genArgs, + }) + : genArgs; + + return IdlCoder.typeSize(type, idl, args); + }; + + switch (typeDef.type.kind) { + case "struct": { + return handleDefinedFields( + typeDef.type.fields, + () => [0], + (fields) => fields.map((f) => typeSize(f.type)), + (fields) => fields.map((f) => typeSize(f)) + ).reduce((acc, size) => acc + size, 0); + } + + case "enum": { + const variantSizes = typeDef.type.variants.map((variant) => { + return handleDefinedFields( + variant.fields, + () => [0], + (fields) => fields.map((f) => typeSize(f.type)), + (fields) => fields.map((f) => typeSize(f)) + ).reduce((acc, size) => acc + size, 0); + }); + + return Math.max(...variantSizes) + 1; + } + + case "type": { + return IdlCoder.typeSize(typeDef.type.alias, idl, genericArgs); + } + } + } + if ("generic" in ty) { + const genericArg = genericArgs?.at(0); + if (genericArg?.kind !== "type") { + throw new IdlError(`Invalid generic: ${ty.generic}`); + } + + return IdlCoder.typeSize(genericArg.type, idl, genericArgs); + } + + throw new Error(`Invalid type ${JSON.stringify(ty)}`); + } + } + + /** + * Resolve the generic array length or return the constant-sized array length. + */ + private static resolveArrayLen( + len: IdlArrayLen, + genericArgs?: IdlGenericArg[] | null + ): number { + if (typeof len === "number") return len; + + if (genericArgs) { + const genericLen = genericArgs.find((g) => g.kind === "const"); + if (genericLen?.kind === "const") { + len = +genericLen.value; + } + } + + if (typeof len !== "number") { + throw new IdlError("Generic array length did not resolve"); + } + + return len; + } + + /** + * Recursively resolve generic arguments i.e. replace all generics with the + * actual type that they hold based on the initial `genericArgs` given. + */ + private static resolveGenericArgs({ + type, + typeDef, + genericArgs, + isDefined, + }: { + type: IdlType; + typeDef: IdlTypeDef; + genericArgs: IdlGenericArg[]; + isDefined?: boolean; + }): IdlGenericArg[] | null { + if (typeof type !== "object") return null; + + for (const index in typeDef.generics) { + const defGeneric = typeDef.generics[index]; + + if ("generic" in type && defGeneric.name === type.generic) { + return [genericArgs[index]]; + } + + if ("option" in type) { + const args = IdlCoder.resolveGenericArgs({ + type: type.option, + typeDef, + genericArgs, + isDefined, + }); + if (!args || !isDefined) return args; + + if (args[0].kind === "type") { + return [ + { + kind: "type", + type: { option: args[0].type }, + }, + ]; + } + } + + if ("vec" in type) { + const args = IdlCoder.resolveGenericArgs({ + type: type.vec, + typeDef, + genericArgs, + isDefined, + }); + if (!args || !isDefined) return args; + + if (args[0].kind === "type") { + return [ + { + kind: "type", + type: { vec: args[0].type }, + }, + ]; + } + } + + if ("array" in type) { + const [elTy, len] = type.array; + const isGenericLen = typeof len === "object"; + + const args = IdlCoder.resolveGenericArgs({ + type: elTy, + typeDef, + genericArgs, + isDefined, + }); + if (args) { + // Has generic type, also check for generic length + for (const i in typeDef.generics.slice(+index)) { + const curIndex = +index + +i; + if ( + isGenericLen && + typeDef.generics[curIndex].name === len.generic + ) { + args.push(genericArgs[curIndex]); + } + } + + if (!isDefined) return args; + + if (args[0].kind === "type" && args[1].kind === "const") { + return [ + { + kind: "type", + type: { array: [args[0].type, +args[1].value] }, + }, + ]; + } + } + + // Only generic len + if (isGenericLen && defGeneric.name === len.generic) { + const arg = genericArgs[index]; + if (!isDefined) return [arg]; + + return [ + { + kind: "type", + type: { array: [elTy, +arg.value] }, + }, + ]; + } + + // Non-generic + return null; + } + + if ("defined" in type) { + if (!type.defined.generics) return null; + + return type.defined.generics + .flatMap((g) => { + switch (g.kind) { + case "type": + return IdlCoder.resolveGenericArgs({ + type: g.type, + typeDef, + genericArgs, + isDefined: true, + }); + case "const": + return [g]; + } + }) + .filter((g) => g !== null) as IdlGenericArg[]; + } + } + + return null; + } } diff --git a/ts/packages/anchor/src/coder/borsh/index.ts b/ts/packages/anchor/src/coder/borsh/index.ts index daf6cb737d..e3e9435316 100644 --- a/ts/packages/anchor/src/coder/borsh/index.ts +++ b/ts/packages/anchor/src/coder/borsh/index.ts @@ -8,7 +8,7 @@ import { Coder } from "../index.js"; export { BorshInstructionCoder } from "./instruction.js"; export { BorshAccountsCoder } from "./accounts.js"; export { DISCRIMINATOR_SIZE } from "./discriminator.js"; -export { BorshEventCoder, eventDiscriminator } from "./event.js"; +export { BorshEventCoder } from "./event.js"; /** * BorshCoder is the default Coder for Anchor programs implementing the diff --git a/ts/packages/anchor/src/coder/borsh/instruction.ts b/ts/packages/anchor/src/coder/borsh/instruction.ts index 480a1191a5..5d45e3319e 100644 --- a/ts/packages/anchor/src/coder/borsh/instruction.ts +++ b/ts/packages/anchor/src/coder/borsh/instruction.ts @@ -1,95 +1,69 @@ import bs58 from "bs58"; import { Buffer } from "buffer"; import { Layout } from "buffer-layout"; -import camelCase from "camelcase"; -import { snakeCase } from "snake-case"; import * as borsh from "@coral-xyz/borsh"; import { AccountMeta, PublicKey } from "@solana/web3.js"; import { + handleDefinedFields, Idl, IdlField, IdlType, IdlTypeDef, IdlAccount, - IdlAccountItem, - IdlTypeDefTyStruct, + IdlInstructionAccountItem, IdlTypeVec, - IdlTypeOption, - IdlTypeDefined, - IdlAccounts, - IdlEnumFieldsNamed, + IdlInstructionAccounts, + IdlDiscriminator, } from "../../idl.js"; import { IdlCoder } from "./idl.js"; -import { InstructionCoder } from "../index.js"; -import { sha256 } from "@noble/hashes/sha256"; - -/** - * Namespace for global instruction function signatures (i.e. functions - * that aren't namespaced by the state or any of its trait implementations). - */ -export const SIGHASH_GLOBAL_NAMESPACE = "global"; +import { DISCRIMINATOR_SIZE, InstructionCoder } from "../index.js"; /** * Encodes and decodes program instructions. */ export class BorshInstructionCoder implements InstructionCoder { // Instruction args layout. Maps namespaced method - private ixLayout: Map; + private ixLayouts: Map< + string, + { discriminator: IdlDiscriminator; layout: Layout } + >; // Base58 encoded sighash to instruction layout. - private sighashLayouts: Map; + private sighashLayouts: Map; public constructor(private idl: Idl) { - this.ixLayout = BorshInstructionCoder.parseIxLayout(idl); - - const sighashLayouts = new Map(); - idl.instructions.forEach((ix) => { - const sh = sighash(SIGHASH_GLOBAL_NAMESPACE, ix.name); - sighashLayouts.set(bs58.encode(sh), { - layout: this.ixLayout.get(ix.name), - name: ix.name, - }); + const ixLayouts = idl.instructions.map((ix) => { + const name = ix.name; + const fieldLayouts = ix.args.map((arg) => + IdlCoder.fieldLayout(arg, idl.types) + ); + const layout = borsh.struct(fieldLayouts, name); + return [name, { discriminator: ix.discriminator, layout }] as const; }); + this.ixLayouts = new Map(ixLayouts); - this.sighashLayouts = sighashLayouts; + const sighashLayouts = ixLayouts.map( + ([name, { discriminator, layout }]) => { + return [bs58.encode(discriminator), { name, layout }] as const; + } + ); + this.sighashLayouts = new Map(sighashLayouts); } /** * Encodes a program instruction. */ - public encode(ixName: string, ix: any, discriminator?: Buffer): Buffer { - return this._encode( - ixName, - ix, - discriminator ?? sighash(SIGHASH_GLOBAL_NAMESPACE, ixName) - ); - } - - private _encode(ixName: string, ix: any, discriminator: Buffer): Buffer { + public encode(ixName: string, ix: any): Buffer { const buffer = Buffer.alloc(1000); // TODO: use a tighter buffer. - const methodName = camelCase(ixName); - const layout = this.ixLayout.get(methodName); - if (!layout) { - throw new Error(`Unknown method: ${methodName}`); + const encoder = this.ixLayouts.get(ixName); + if (!encoder) { + throw new Error(`Unknown method: ${ixName}`); } - const len = layout.encode(ix, buffer); - const data = buffer.slice(0, len); - return Buffer.concat([discriminator, data]); - } - private static parseIxLayout(idl: Idl): Map { - const ixLayouts = idl.instructions.map((ix): [string, Layout] => { - let fieldLayouts = ix.args.map((arg: IdlField) => - IdlCoder.fieldLayout( - arg, - Array.from([...(idl.accounts ?? []), ...(idl.types ?? [])]) - ) - ); - const name = camelCase(ix.name); - return [name, borsh.struct(fieldLayouts, name)]; - }); + const len = encoder.layout.encode(ix, buffer); + const data = buffer.slice(0, len); - return new Map(ixLayouts); + return Buffer.concat([Buffer.from(encoder.discriminator), data]); } /** @@ -97,27 +71,22 @@ export class BorshInstructionCoder implements InstructionCoder { */ public decode( ix: Buffer | string, - encoding: "hex" | "base58" = "hex", - ixName?: string + encoding: "hex" | "base58" = "hex" ): Instruction | null { if (typeof ix === "string") { ix = encoding === "hex" ? Buffer.from(ix, "hex") : bs58.decode(ix); } - // Use the provided method name to get the sighash, ignoring the - // discriminator in the instruction data. - // This is useful for decoding instructions that have been encoded with a - // different namespace, such as an SPL interface. - let sighashKey = bs58.encode( - ixName ? sighash(SIGHASH_GLOBAL_NAMESPACE, ixName) : ix.slice(0, 8) - ); - let data = ix.slice(8); - const decoder = this.sighashLayouts.get(sighashKey); + + const disc = ix.slice(0, DISCRIMINATOR_SIZE); + const data = ix.slice(DISCRIMINATOR_SIZE); + const decoder = this.sighashLayouts.get(bs58.encode(disc)); if (!decoder) { return null; } + return { - data: decoder.layout.decode(data), name: decoder.name, + data: decoder.layout.decode(data), }; } @@ -153,8 +122,8 @@ class InstructionFormatter { accountMetas: AccountMeta[], idl: Idl ): InstructionDisplay | null { - const idlIx = idl.instructions.filter((i) => ix.name === i.name)[0]; - if (idlIx === undefined) { + const idlIx = idl.instructions.find((i) => ix.name === i.name); + if (!idlIx) { console.error("Invalid instruction given"); return null; } @@ -199,21 +168,40 @@ class InstructionFormatter { private static formatIdlType(idlType: IdlType): string { if (typeof idlType === "string") { - return idlType as string; + return idlType; } - if ("vec" in idlType) { - return `Vec<${this.formatIdlType(idlType.vec)}>`; - } if ("option" in idlType) { return `Option<${this.formatIdlType(idlType.option)}>`; } - if ("defined" in idlType) { - return idlType.defined; + if ("coption" in idlType) { + return `COption<${this.formatIdlType(idlType.coption)}>`; + } + if ("vec" in idlType) { + return `Vec<${this.formatIdlType(idlType.vec)}>`; } if ("array" in idlType) { return `Array<${idlType.array[0]}; ${idlType.array[1]}>`; } + if ("defined" in idlType) { + const name = idlType.defined.name; + if (idlType.defined.generics) { + const generics = idlType.defined.generics + .map((g) => { + switch (g.kind) { + case "type": + return InstructionFormatter.formatIdlType(g.type); + case "const": + return g.value; + } + }) + .join(", "); + + return `${name}<${generics}>`; + } + + return name; + } throw new Error(`Unknown IDL type: ${idlType}`); } @@ -226,11 +214,11 @@ class InstructionFormatter { if (typeof idlField.type === "string") { return data.toString(); } - if (idlField.type.hasOwnProperty("vec")) { + if ("vec" in idlField.type) { return ( "[" + (>data) - .map((d: IdlField) => + .map((d) => this.formatIdlData( { name: "", type: (idlField.type).vec }, d, @@ -241,32 +229,27 @@ class InstructionFormatter { "]" ); } - if (idlField.type.hasOwnProperty("option")) { + if ("option" in idlField.type) { return data === null ? "null" : this.formatIdlData( - { name: "", type: (idlField.type).option }, + { name: "", type: idlField.type.option }, data, types ); } - if (idlField.type.hasOwnProperty("defined")) { - if (types === undefined) { + if ("defined" in idlField.type) { + if (!types) { throw new Error("User defined types not provided"); } - const filtered = types.filter( - (t) => t.name === (idlField.type).defined - ); - if (filtered.length !== 1) { - throw new Error( - `Type not found: ${(idlField.type).defined}` - ); + + const definedName = idlField.type.defined.name; + const typeDef = types.find((t) => t.name === definedName); + if (!typeDef) { + throw new Error(`Type not found: ${definedName}`); } - return InstructionFormatter.formatIdlDataDefined( - filtered[0], - data, - types - ); + + return InstructionFormatter.formatIdlDataDefined(typeDef, data, types); } return "unknown"; @@ -279,76 +262,106 @@ class InstructionFormatter { ): string { switch (typeDef.type.kind) { case "struct": { - const struct: IdlTypeDefTyStruct = typeDef.type; - const fields = Object.keys(data) - .map((k) => { - const field = struct.fields.find((f) => f.name === k); - if (!field) { - throw new Error("Unable to find type"); + return ( + "{ " + + handleDefinedFields( + typeDef.type.fields, + () => "", + (fields) => { + return Object.entries(data) + .map(([key, val]) => { + const field = fields.find((f) => f.name === key); + if (!field) { + throw new Error(`Field not found: ${key}`); + } + return ( + key + + ": " + + InstructionFormatter.formatIdlData(field, val, types) + ); + }) + .join(", "); + }, + (fields) => { + return Object.entries(data) + .map(([key, val]) => { + return ( + key + + ": " + + InstructionFormatter.formatIdlData( + { name: "", type: fields[key] }, + val, + types + ) + ); + }) + .join(", "); } - return ( - k + - ": " + - InstructionFormatter.formatIdlData(field, data[k], types) - ); - }) - .join(", "); - return "{ " + fields + " }"; + ) + + " }" + ); } case "enum": { - if (typeDef.type.variants.length === 0) { - return "{}"; + const variantName = Object.keys(data)[0]; + const variant = typeDef.type.variants.find( + (v) => v.name === variantName + ); + if (!variant) { + throw new Error(`Unable to find variant: ${variantName}`); } - // Struct enum. - if (typeDef.type.variants[0].name) { - const variants = typeDef.type.variants; - const variant = Object.keys(data)[0]; - const enumType = data[variant]; - const enumVariant = variants.find( - (v) => camelCase(v.name) === variant - ); - if (!enumVariant) { - throw new Error(`Unable to find variant \`${variant}\``); - } - const fields = enumVariant.fields as IdlEnumFieldsNamed; - const namedFields = Object.keys(enumType) - .map((f) => { - const fieldData = enumType[f]; - const idlField = fields.find((v) => v.name === f); - if (!idlField) { - throw new Error(`Unable to find field \`${f}\``); - } - - return ( - f + - ": " + - InstructionFormatter.formatIdlData(idlField, fieldData, types) - ); - }) - .join(", "); - - const variantName = camelCase(variant, { pascalCase: true }); - if (namedFields.length === 0) { - return variantName; + + const enumValue = data[variantName]; + return handleDefinedFields( + variant.fields, + () => variantName, + (fields) => { + const namedFields = Object.keys(enumValue) + .map((f) => { + const fieldData = enumValue[f]; + const idlField = fields.find((v) => v.name === f); + if (!idlField) { + throw new Error(`Field not found: ${f}`); + } + + return ( + f + + ": " + + InstructionFormatter.formatIdlData(idlField, fieldData, types) + ); + }) + .join(", "); + + return `${variantName} { ${namedFields} }`; + }, + (fields) => { + const tupleFields = Object.entries(enumValue) + .map(([key, val]) => { + return ( + key + + ": " + + InstructionFormatter.formatIdlData( + { name: "", type: fields[key] }, + val as any, + types + ) + ); + }) + .join(", "); + + return `${variantName} { ${tupleFields} }`; } - return `${variantName} { ${namedFields} }`; - } - // Tuple enum. - else { - // TODO. - return "Tuple formatting not yet implemented"; - } + ); } - case "alias": { - return InstructionFormatter.formatIdlType(typeDef.type.value); + case "type": { + return InstructionFormatter.formatIdlType(typeDef.type.alias); } } } private static flattenIdlAccounts( - accounts: IdlAccountItem[], + accounts: IdlInstructionAccountItem[], prefix?: string ): IdlAccount[] { return accounts @@ -357,7 +370,7 @@ class InstructionFormatter { if (account.hasOwnProperty("accounts")) { const newPrefix = prefix ? `${prefix} > ${accName}` : accName; return InstructionFormatter.flattenIdlAccounts( - (account).accounts, + (account).accounts, newPrefix ); } else { @@ -375,11 +388,3 @@ function sentenceCase(field: string): string { const result = field.replace(/([A-Z])/g, " $1"); return result.charAt(0).toUpperCase() + result.slice(1); } - -// Not technically sighash, since we don't include the arguments, as Rust -// doesn't allow function overloading. -function sighash(nameSpace: string, ixName: string): Buffer { - let name = snakeCase(ixName); - let preimage = `${nameSpace}:${name}`; - return Buffer.from(sha256(preimage).slice(0, 8)); -} diff --git a/ts/packages/anchor/src/coder/borsh/types.ts b/ts/packages/anchor/src/coder/borsh/types.ts index 848e4faa3d..4087d5a52d 100644 --- a/ts/packages/anchor/src/coder/borsh/types.ts +++ b/ts/packages/anchor/src/coder/borsh/types.ts @@ -13,40 +13,38 @@ export class BorshTypesCoder implements TypesCoder { */ private typeLayouts: Map; - /** - * IDL whose types will be coded. - */ - private idl: Idl; - public constructor(idl: Idl) { - if (idl.types === undefined) { + const types = idl.types; + if (!types) { this.typeLayouts = new Map(); return; } - const layouts: [N, Layout][] = idl.types.map((acc) => { - return [acc.name as N, IdlCoder.typeDefLayout(acc, idl.types)]; - }); + const layouts: [N, Layout][] = types + .filter((ty) => !ty.generics) + .map((ty) => [ + ty.name as N, + IdlCoder.typeDefLayout({ typeDef: ty, types }), + ]); this.typeLayouts = new Map(layouts); - this.idl = idl; } - public encode(typeName: N, type: T): Buffer { + public encode(name: N, type: T): Buffer { const buffer = Buffer.alloc(1000); // TODO: use a tighter buffer. - const layout = this.typeLayouts.get(typeName); + const layout = this.typeLayouts.get(name); if (!layout) { - throw new Error(`Unknown type: ${typeName}`); + throw new Error(`Unknown type: ${name}`); } const len = layout.encode(type, buffer); return buffer.slice(0, len); } - public decode(typeName: N, typeData: Buffer): T { - const layout = this.typeLayouts.get(typeName); + public decode(name: N, data: Buffer): T { + const layout = this.typeLayouts.get(name); if (!layout) { - throw new Error(`Unknown type: ${typeName}`); + throw new Error(`Unknown type: ${name}`); } - return layout.decode(typeData); + return layout.decode(data); } } diff --git a/ts/packages/anchor/src/coder/common.ts b/ts/packages/anchor/src/coder/common.ts deleted file mode 100644 index 6df3b82bdd..0000000000 --- a/ts/packages/anchor/src/coder/common.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { Idl, IdlField, IdlTypeDef, IdlType } from "../idl.js"; -import { IdlError } from "../error.js"; - -export function accountSize(idl: Idl, idlAccount: IdlTypeDef) { - switch (idlAccount.type.kind) { - case "struct": { - return idlAccount.type.fields - .map((f) => typeSize(idl, f.type)) - .reduce((acc, size) => acc + size, 0); - } - - case "enum": { - const variantSizes = idlAccount.type.variants.map((variant) => { - if (!variant.fields) { - return 0; - } - - return variant.fields - .map((f: IdlField | IdlType) => { - // Unnamed enum variant - if (!(typeof f === "object" && "name" in f)) { - return typeSize(idl, f); - } - - // Named enum variant - return typeSize(idl, f.type); - }) - .reduce((acc, size) => acc + size, 0); - }); - - return Math.max(...variantSizes) + 1; - } - - case "alias": { - return typeSize(idl, idlAccount.type.value); - } - } -} - -// Returns the size of the type in bytes. For variable length types, just return -// 1. Users should override this value in such cases. -function typeSize(idl: Idl, ty: IdlType): number { - switch (ty) { - case "bool": - return 1; - case "u8": - return 1; - case "i8": - return 1; - case "i16": - return 2; - case "u16": - return 2; - case "u32": - return 4; - case "i32": - return 4; - case "f32": - return 4; - case "u64": - return 8; - case "i64": - return 8; - case "f64": - return 8; - case "u128": - return 16; - case "i128": - return 16; - case "u256": - return 32; - case "i256": - return 32; - case "bytes": - return 1; - case "string": - return 1; - case "publicKey": - return 32; - default: - if ("vec" in ty) { - return 1; - } - if ("option" in ty) { - return 1 + typeSize(idl, ty.option); - } - if ("coption" in ty) { - return 4 + typeSize(idl, ty.coption); - } - if ("defined" in ty) { - const filtered = idl.types?.filter((t) => t.name === ty.defined) ?? []; - if (filtered.length !== 1) { - throw new IdlError(`Type not found: ${JSON.stringify(ty)}`); - } - let typeDef = filtered[0]; - - return accountSize(idl, typeDef); - } - if ("array" in ty) { - let arrayTy = ty.array[0]; - let arraySize = ty.array[1]; - return typeSize(idl, arrayTy) * arraySize; - } - throw new Error(`Invalid type ${JSON.stringify(ty)}`); - } -} diff --git a/ts/packages/anchor/src/coder/index.ts b/ts/packages/anchor/src/coder/index.ts index 10567ebae1..6d0d482d64 100644 --- a/ts/packages/anchor/src/coder/index.ts +++ b/ts/packages/anchor/src/coder/index.ts @@ -1,4 +1,4 @@ -import { IdlEvent, IdlTypeDef } from "../idl.js"; +import { IdlEvent } from "../idl.js"; import { Event } from "../program/event.js"; export * from "./borsh/index.js"; @@ -36,14 +36,14 @@ export interface StateCoder { export interface AccountsCoder { encode(accountName: A, account: T): Promise; - decode(accountName: A, ix: Buffer): T; - decodeUnchecked(accountName: A, ix: Buffer): T; + decode(accountName: A, acc: Buffer): T; + decodeUnchecked(accountName: A, acc: Buffer): T; memcmp(accountName: A, appendData?: Buffer): any; - size(idlAccount: IdlTypeDef): number; + size(accountName: A): number; } export interface InstructionCoder { - encode(ixName: string, ix: any, discriminator?: Buffer): Buffer; + encode(ixName: string, ix: any): Buffer; } export interface EventCoder { diff --git a/ts/packages/anchor/src/coder/system/accounts.ts b/ts/packages/anchor/src/coder/system/accounts.ts index c5749dc3c0..9cab25b901 100644 --- a/ts/packages/anchor/src/coder/system/accounts.ts +++ b/ts/packages/anchor/src/coder/system/accounts.ts @@ -1,8 +1,8 @@ import { AccountsCoder } from "../index.js"; -import { Idl, IdlTypeDef } from "../../idl.js"; +import { Idl } from "../../idl.js"; import * as BufferLayout from "buffer-layout"; import { NONCE_ACCOUNT_LENGTH, PublicKey } from "@solana/web3.js"; -import { accountSize } from "../common.js"; +import { IdlCoder } from "../borsh/idl.js"; export class SystemAccountsCoder implements AccountsCoder @@ -51,8 +51,8 @@ export class SystemAccountsCoder } } - public size(idlAccount: IdlTypeDef): number { - return accountSize(this.idl, idlAccount) ?? 0; + public size(accountName: A): number { + return IdlCoder.typeSize({ defined: { name: accountName } }, this.idl); } } diff --git a/ts/packages/anchor/src/coder/system/instruction.ts b/ts/packages/anchor/src/coder/system/instruction.ts index 894cdf12c6..5a6dfa0d78 100644 --- a/ts/packages/anchor/src/coder/system/instruction.ts +++ b/ts/packages/anchor/src/coder/system/instruction.ts @@ -1,6 +1,5 @@ import BN from "bn.js"; import * as BufferLayout from "buffer-layout"; -import camelCase from "camelcase"; import { Idl } from "../../idl.js"; import { InstructionCoder } from "../index.js"; @@ -9,7 +8,7 @@ export class SystemInstructionCoder implements InstructionCoder { constructor(_: Idl) {} encode(ixName: string, ix: any): Buffer { - switch (camelCase(ixName)) { + switch (ixName) { case "createAccount": { return encodeCreateAccount(ix); } diff --git a/ts/packages/anchor/src/idl.ts b/ts/packages/anchor/src/idl.ts index 4a6c71fb72..c651e5852b 100644 --- a/ts/packages/anchor/src/idl.ts +++ b/ts/packages/anchor/src/idl.ts @@ -1,98 +1,107 @@ +import camelCase from "camelcase"; import { Buffer } from "buffer"; import { PublicKey } from "@solana/web3.js"; import * as borsh from "@coral-xyz/borsh"; export type Idl = { - version: string; - name: string; + address: string; + metadata: IdlMetadata; docs?: string[]; instructions: IdlInstruction[]; - accounts?: IdlAccountDef[]; - types?: IdlTypeDef[]; + accounts?: IdlAccount[]; events?: IdlEvent[]; errors?: IdlErrorCode[]; - constants?: IdlConstant[]; - metadata?: IdlMetadata; -}; - -export type IdlMetadata = any; - -export type IdlConstant = { - name: string; - type: IdlType; - value: string; + types?: IdlTypeDef[]; + constants?: IdlConst[]; }; -export type IdlEvent = { +export type IdlMetadata = { name: string; - fields: IdlEventField[]; + version: string; + spec: string; + description?: string; + repository?: string; + dependencies?: IdlDependency[]; + contact?: string; }; -export type IdlEventField = { +export type IdlDependency = { name: string; - type: IdlType; - index: boolean; + version: string; }; export type IdlInstruction = { name: string; docs?: string[]; - accounts: IdlAccountItem[]; + discriminator: IdlDiscriminator; + accounts: IdlInstructionAccountItem[]; args: IdlField[]; returns?: IdlType; }; -export type IdlStateMethod = IdlInstruction; - -export type IdlAccountItem = IdlAccount | IdlAccounts; - -export function isIdlAccounts( - accountItem: IdlAccountItem -): accountItem is IdlAccounts { - return "accounts" in accountItem; -} +export type IdlInstructionAccountItem = + | IdlInstructionAccount + | IdlInstructionAccounts; -export type IdlAccount = { +export type IdlInstructionAccount = { name: string; - isMut: boolean; - isSigner: boolean; - isOptional?: boolean; docs?: string[]; - relations?: string[]; + writable?: boolean; + signer?: boolean; + optional?: boolean; + address?: string; pda?: IdlPda; + relations?: string[]; +}; + +export type IdlInstructionAccounts = { + name: string; + accounts: IdlInstructionAccount[]; }; export type IdlPda = { seeds: IdlSeed[]; - programId?: IdlSeed; + program?: IdlSeed; }; export type IdlSeed = IdlSeedConst | IdlSeedArg | IdlSeedAccount; export type IdlSeedConst = { kind: "const"; - type: IdlType; - value: any; + value: number[]; }; export type IdlSeedArg = { kind: "arg"; - type: IdlType; path: string; }; export type IdlSeedAccount = { kind: "account"; - type: IdlType; - account?: string; path: string; + account?: string; }; -// A nested/recursive version of IdlAccount. -export type IdlAccounts = { +export type IdlAccount = { name: string; - docs?: string[]; - accounts: IdlAccountItem[]; + discriminator: IdlDiscriminator; +}; + +export type IdlEvent = { + name: string; + discriminator: IdlDiscriminator; +}; + +export type IdlConst = { + name: string; + type: IdlType; + value: string; +}; + +export type IdlErrorCode = { + name: string; + code: number; + msg?: string; }; export type IdlField = { @@ -104,18 +113,58 @@ export type IdlField = { export type IdlTypeDef = { name: string; docs?: string[]; + serialization?: IdlSerialization; + repr?: IdlRepr; + generics?: IdlTypeDefGeneric[]; type: IdlTypeDefTy; }; -export type IdlAccountDef = { +export type IdlSerialization = + | "borsh" + | "bytemuck" + | "bytemuckunsafe" + | { custom: string }; + +export type IdlRepr = IdlReprRust | IdlReprC | IdlReprTransparent; + +export type IdlReprRust = { + kind: "rust"; +} & IdlReprModifier; + +export type IdlReprC = { + kind: "c"; +} & IdlReprModifier; + +export type IdlReprTransparent = { + kind: "transparent"; +}; + +export type IdlReprModifier = { + packed?: boolean; + align?: number; +}; + +export type IdlTypeDefGeneric = IdlTypeDefGenericType | IdlTypeDefGenericConst; + +export type IdlTypeDefGenericType = { + kind: "type"; name: string; - docs?: string[]; - type: IdlTypeDefTyStruct; }; +export type IdlTypeDefGenericConst = { + kind: "const"; + name: string; + type: string; +}; + +export type IdlTypeDefTy = + | IdlTypeDefTyEnum + | IdlTypeDefTyStruct + | IdlTypeDefTyType; + export type IdlTypeDefTyStruct = { kind: "struct"; - fields: IdlTypeDefStruct; + fields?: IdlDefinedFields; }; export type IdlTypeDefTyEnum = { @@ -123,17 +172,35 @@ export type IdlTypeDefTyEnum = { variants: IdlEnumVariant[]; }; -export type IdlTypeDefTyAlias = { - kind: "alias"; - value: IdlType; +export type IdlTypeDefTyType = { + kind: "type"; + alias: IdlType; }; -export type IdlTypeDefTy = - | IdlTypeDefTyEnum - | IdlTypeDefTyStruct - | IdlTypeDefTyAlias; +export type IdlEnumVariant = { + name: string; + fields?: IdlDefinedFields; +}; + +export type IdlDefinedFields = IdlDefinedFieldsNamed | IdlDefinedFieldsTuple; + +export type IdlDefinedFieldsNamed = IdlField[]; + +export type IdlDefinedFieldsTuple = IdlType[]; + +export type IdlArrayLen = IdlArrayLenGeneric | IdlArrayLenValue; + +export type IdlArrayLenGeneric = { + generic: string; +}; + +export type IdlArrayLenValue = number; + +export type IdlGenericArg = IdlGenericArgType | IdlGenericArgConst; + +export type IdlGenericArgType = { kind: "type"; type: IdlType }; -export type IdlTypeDefStruct = Array; +export type IdlGenericArgConst = { kind: "const"; value: string }; export type IdlType = | "bool" @@ -153,17 +220,13 @@ export type IdlType = | "i256" | "bytes" | "string" - | "publicKey" - | IdlTypeDefined + | "pubkey" | IdlTypeOption | IdlTypeCOption | IdlTypeVec - | IdlTypeArray; - -// User defined type. -export type IdlTypeDefined = { - defined: string; -}; + | IdlTypeArray + | IdlTypeDefined + | IdlTypeGeneric; export type IdlTypeOption = { option: IdlType; @@ -178,25 +241,27 @@ export type IdlTypeVec = { }; export type IdlTypeArray = { - array: [idlType: IdlType, size: number]; + array: [idlType: IdlType, size: IdlArrayLen]; }; -export type IdlEnumVariant = { - name: string; - fields?: IdlEnumFields; +export type IdlTypeDefined = { + defined: { + name: string; + generics?: IdlGenericArg[]; + }; }; -export type IdlEnumFields = IdlEnumFieldsNamed | IdlEnumFieldsTuple; - -export type IdlEnumFieldsNamed = IdlField[]; +export type IdlTypeGeneric = { + generic: string; +}; -export type IdlEnumFieldsTuple = IdlType[]; +export type IdlDiscriminator = number[]; -export type IdlErrorCode = { - code: number; - name: string; - msg?: string; -}; +export function isCompositeAccounts( + accountItem: IdlInstructionAccountItem +): accountItem is IdlInstructionAccounts { + return "accounts" in accountItem; +} // Deterministic IDL address as a function of the program id. export async function idlAddress(programId: PublicKey): Promise { @@ -229,3 +294,58 @@ export function encodeIdlAccount(acc: IdlProgramAccount): Buffer { const len = IDL_ACCOUNT_LAYOUT.encode(acc, buffer); return buffer.slice(0, len); } + +/** + * Convert the given IDL to camelCase. + * + * The IDL is generated from Rust which has different conventions compared to + * JS/TS, e.g. instruction names in Rust are snake_case. + * + * The conversion happens automatically for programs, however, if you are using + * internals such as `BorshInstructionCoder` and you only have the original + * (not camelCase) IDL, you might need to use this function. + * + * @param idl IDL to convert to camelCase + * @returns camelCase version of the IDL + */ +export function convertIdlToCamelCase(idl: I) { + const KEYS_TO_CONVERT = ["name", "path", "account", "relations", "generic"]; + + // `my_account.field` is getting converted to `myAccountField` but we + // need `myAccount.field`. + const toCamelCase = (s: any) => s.split(".").map(camelCase).join("."); + + const recursivelyConvertNamesToCamelCase = (obj: Record) => { + for (const key in obj) { + const val = obj[key]; + if (KEYS_TO_CONVERT.includes(key)) { + obj[key] = Array.isArray(val) ? val.map(toCamelCase) : toCamelCase(val); + } else if (typeof val === "object") { + recursivelyConvertNamesToCamelCase(val); + } + } + }; + + const camelCasedIdl = structuredClone(idl); + recursivelyConvertNamesToCamelCase(camelCasedIdl); + return camelCasedIdl; +} + +/** Conveniently handle all defined field kinds with proper type support. */ +export function handleDefinedFields( + fields: IdlDefinedFields | undefined, + unitCb: () => U, + namedCb: (fields: IdlDefinedFieldsNamed) => N, + tupleCb: (fields: IdlDefinedFieldsTuple) => T +) { + // Unit + if (!fields?.length) return unitCb(); + + // Named + if ((fields as IdlDefinedFieldsNamed)[0].name) { + return namedCb(fields as IdlDefinedFieldsNamed); + } + + // Tuple + return tupleCb(fields as IdlDefinedFieldsTuple); +} diff --git a/ts/packages/anchor/src/native/system.ts b/ts/packages/anchor/src/native/system.ts index e42ead4dc9..76f1a49a71 100644 --- a/ts/packages/anchor/src/native/system.ts +++ b/ts/packages/anchor/src/native/system.ts @@ -17,321 +17,281 @@ export function coder(): SystemCoder { * System IDL. */ export type SystemProgram = { - version: "0.1.0"; - name: "system_program"; + address: "11111111111111111111111111111111"; + metadata: { + name: "systemProgram"; + version: "0.1.0"; + spec: "0.1.0"; + }; instructions: [ { - name: "createAccount"; + name: "advanceNonceAccount"; + discriminator: [4, 0, 0, 0]; accounts: [ { - name: "from"; - isMut: true; - isSigner: true; - }, - { - name: "to"; - isMut: true; - isSigner: true; - } - ]; - args: [ - { - name: "lamports"; - type: "u64"; + name: "nonce"; + writable: true; }, { - name: "space"; - type: "u64"; + name: "recentBlockhashes"; }, { - name: "owner"; - type: "publicKey"; - } - ]; - }, - { - name: "assign"; - accounts: [ - { - name: "pubkey"; - isMut: true; - isSigner: true; + name: "authorized"; + signer: true; } ]; args: [ { - name: "owner"; - type: "publicKey"; + name: "authorized"; + type: "pubkey"; } ]; }, { - name: "transfer"; + name: "allocate"; + discriminator: [8, 0, 0, 0]; accounts: [ { - name: "from"; - isMut: true; - isSigner: true; - }, - { - name: "to"; - isMut: true; - isSigner: false; + name: "pubkey"; + writable: true; + signer: true; } ]; args: [ { - name: "lamports"; + name: "space"; type: "u64"; } ]; }, { - name: "createAccountWithSeed"; + name: "allocateWithSeed"; + discriminator: [9, 0, 0, 0]; accounts: [ { - name: "from"; - isMut: true; - isSigner: true; - }, - { - name: "to"; - isMut: true; - isSigner: false; + name: "account"; + writable: true; }, { name: "base"; - isMut: false; - isSigner: true; + signer: true; } ]; args: [ { name: "base"; - type: "publicKey"; + type: "pubkey"; }, { name: "seed"; type: "string"; }, - { - name: "lamports"; - type: "u64"; - }, { name: "space"; type: "u64"; }, { name: "owner"; - type: "publicKey"; + type: "pubkey"; } ]; }, { - name: "advanceNonceAccount"; + name: "assign"; + discriminator: [1, 0, 0, 0]; accounts: [ { - name: "nonce"; - isMut: true; - isSigner: false; - }, - { - name: "recentBlockhashes"; - isMut: false; - isSigner: false; - }, - { - name: "authorized"; - isMut: false; - isSigner: true; + name: "pubkey"; + writable: true; + signer: true; } ]; args: [ { - name: "authorized"; - type: "publicKey"; + name: "owner"; + type: "pubkey"; } ]; }, { - name: "withdrawNonceAccount"; + name: "assignWithSeed"; + discriminator: [10, 0, 0, 0]; accounts: [ { - name: "nonce"; - isMut: true; - isSigner: false; - }, - { - name: "to"; - isMut: true; - isSigner: false; - }, - { - name: "recentBlockhashes"; - isMut: false; - isSigner: false; - }, - { - name: "rent"; - isMut: false; - isSigner: false; + name: "account"; + writable: true; }, { - name: "authorized"; - isMut: false; - isSigner: true; + name: "base"; + signer: true; } ]; args: [ { - name: "lamports"; - type: "u64"; - } - ]; - }, - { - name: "initializeNonceAccount"; - accounts: [ - { - name: "nonce"; - isMut: true; - isSigner: true; + name: "base"; + type: "pubkey"; }, { - name: "recentBlockhashes"; - isMut: false; - isSigner: false; + name: "seed"; + type: "string"; }, { - name: "rent"; - isMut: false; - isSigner: false; - } - ]; - args: [ - { - name: "authorized"; - type: "publicKey"; + name: "owner"; + type: "pubkey"; } ]; }, { name: "authorizeNonceAccount"; + discriminator: [7, 0, 0, 0]; accounts: [ { name: "nonce"; - isMut: true; - isSigner: false; + writable: true; }, { name: "authorized"; - isMut: false; - isSigner: true; + signer: true; } ]; args: [ { name: "authorized"; - type: "publicKey"; + type: "pubkey"; } ]; }, { - name: "allocate"; + name: "createAccount"; + discriminator: [0, 0, 0, 0]; accounts: [ { - name: "pubkey"; - isMut: true; - isSigner: true; + name: "from"; + writable: true; + signer: true; + }, + { + name: "to"; + writable: true; + signer: true; } ]; args: [ + { + name: "lamports"; + type: "u64"; + }, { name: "space"; type: "u64"; + }, + { + name: "owner"; + type: "pubkey"; } ]; }, { - name: "allocateWithSeed"; + name: "createAccountWithSeed"; + discriminator: [3, 0, 0, 0]; accounts: [ { - name: "account"; - isMut: true; - isSigner: false; + name: "from"; + writable: true; + signer: true; + }, + { + name: "to"; + writable: true; }, { name: "base"; - isMut: false; - isSigner: true; + signer: true; } ]; args: [ { name: "base"; - type: "publicKey"; + type: "pubkey"; }, { name: "seed"; type: "string"; }, + { + name: "lamports"; + type: "u64"; + }, { name: "space"; type: "u64"; }, { name: "owner"; - type: "publicKey"; + type: "pubkey"; } ]; }, { - name: "assignWithSeed"; + name: "initializeNonceAccount"; + discriminator: [6, 0, 0, 0]; accounts: [ { - name: "account"; - isMut: true; - isSigner: false; + name: "nonce"; + writable: true; + signer: true; }, { - name: "base"; - isMut: false; - isSigner: true; + name: "recentBlockhashes"; + }, + { + name: "rent"; + address: "SysvarRent111111111111111111111111111111111"; } ]; args: [ { - name: "base"; - type: "publicKey"; - }, + name: "authorized"; + type: "pubkey"; + } + ]; + }, + { + name: "transfer"; + discriminator: [2, 0, 0, 0]; + accounts: [ { - name: "seed"; - type: "string"; + name: "from"; + writable: true; + signer: true; }, { - name: "owner"; - type: "publicKey"; + name: "to"; + writable: true; + } + ]; + args: [ + { + name: "lamports"; + type: "u64"; } ]; }, { name: "transferWithSeed"; + discriminator: [11, 0, 0, 0]; accounts: [ { name: "from"; - isMut: true; - isSigner: false; + writable: true; }, { name: "base"; - isMut: false; - isSigner: true; + signer: true; }, { name: "to"; - isMut: true; - isSigner: false; + writable: true; } ]; args: [ @@ -345,12 +305,61 @@ export type SystemProgram = { }, { name: "owner"; - type: "publicKey"; + type: "pubkey"; + } + ]; + }, + { + name: "withdrawNonceAccount"; + discriminator: [5, 0, 0, 0]; + accounts: [ + { + name: "nonce"; + writable: true; + }, + { + name: "to"; + writable: true; + }, + { + name: "recentBlockhashes"; + }, + { + name: "rent"; + address: "SysvarRent111111111111111111111111111111111"; + }, + { + name: "authorized"; + signer: true; + } + ]; + args: [ + { + name: "lamports"; + type: "u64"; } ]; } ]; accounts: [ + { + name: "nonce"; + discriminator: []; + } + ]; + types: [ + { + name: "feeCalculator"; + type: { + kind: "struct"; + fields: [ + { + name: "lamportsPerSignature"; + type: "u64"; + } + ]; + }; + }, { name: "nonce"; type: { @@ -366,354 +375,302 @@ export type SystemProgram = { }, { name: "authorizedPubkey"; - type: "publicKey"; + type: "pubkey"; }, { name: "nonce"; - type: "publicKey"; + type: "pubkey"; }, { name: "feeCalculator"; type: { - defined: "FeeCalculator"; + defined: { + name: "feeCalculator"; + }; }; } ]; }; } ]; - types: [ - { - name: "FeeCalculator"; - type: { - kind: "struct"; - fields: [ - { - name: "lamportsPerSignature"; - type: "u64"; - } - ]; - }; - } - ]; }; export const IDL: SystemProgram = { - version: "0.1.0", - name: "system_program", + address: "11111111111111111111111111111111", + metadata: { + name: "systemProgram", + version: "0.1.0", + spec: "0.1.0", + }, instructions: [ { - name: "createAccount", + name: "advanceNonceAccount", + discriminator: [4, 0, 0, 0], accounts: [ { - name: "from", - isMut: true, - isSigner: true, - }, - { - name: "to", - isMut: true, - isSigner: true, - }, - ], - args: [ - { - name: "lamports", - type: "u64", - }, - { - name: "space", - type: "u64", + name: "nonce", + writable: true, }, { - name: "owner", - type: "publicKey", + name: "recentBlockhashes", }, - ], - }, - { - name: "assign", - accounts: [ { - name: "pubkey", - isMut: true, - isSigner: true, + name: "authorized", + signer: true, }, ], args: [ { - name: "owner", - type: "publicKey", + name: "authorized", + type: "pubkey", }, ], }, { - name: "transfer", + name: "allocate", + discriminator: [8, 0, 0, 0], accounts: [ { - name: "from", - isMut: true, - isSigner: true, - }, - { - name: "to", - isMut: true, - isSigner: false, + name: "pubkey", + writable: true, + signer: true, }, ], args: [ { - name: "lamports", + name: "space", type: "u64", }, ], }, { - name: "createAccountWithSeed", + name: "allocateWithSeed", + discriminator: [9, 0, 0, 0], accounts: [ { - name: "from", - isMut: true, - isSigner: true, - }, - { - name: "to", - isMut: true, - isSigner: false, + name: "account", + writable: true, }, { name: "base", - isMut: false, - isSigner: true, + signer: true, }, ], args: [ { name: "base", - type: "publicKey", + type: "pubkey", }, { name: "seed", type: "string", }, - { - name: "lamports", - type: "u64", - }, { name: "space", type: "u64", }, { name: "owner", - type: "publicKey", + type: "pubkey", }, ], }, { - name: "advanceNonceAccount", + name: "assign", + discriminator: [1, 0, 0, 0], accounts: [ { - name: "nonce", - isMut: true, - isSigner: false, - }, - { - name: "recentBlockhashes", - isMut: false, - isSigner: false, - }, - { - name: "authorized", - isMut: false, - isSigner: true, + name: "pubkey", + writable: true, + signer: true, }, ], args: [ { - name: "authorized", - type: "publicKey", + name: "owner", + type: "pubkey", }, ], }, { - name: "withdrawNonceAccount", + name: "assignWithSeed", + discriminator: [10, 0, 0, 0], accounts: [ { - name: "nonce", - isMut: true, - isSigner: false, - }, - { - name: "to", - isMut: true, - isSigner: false, - }, - { - name: "recentBlockhashes", - isMut: false, - isSigner: false, - }, - { - name: "rent", - isMut: false, - isSigner: false, + name: "account", + writable: true, }, { - name: "authorized", - isMut: false, - isSigner: true, + name: "base", + signer: true, }, ], args: [ { - name: "lamports", - type: "u64", - }, - ], - }, - { - name: "initializeNonceAccount", - accounts: [ - { - name: "nonce", - isMut: true, - isSigner: true, - }, - { - name: "recentBlockhashes", - isMut: false, - isSigner: false, + name: "base", + type: "pubkey", }, { - name: "rent", - isMut: false, - isSigner: false, + name: "seed", + type: "string", }, - ], - args: [ { - name: "authorized", - type: "publicKey", + name: "owner", + type: "pubkey", }, ], }, { name: "authorizeNonceAccount", + discriminator: [7, 0, 0, 0], accounts: [ { name: "nonce", - isMut: true, - isSigner: false, + writable: true, }, { name: "authorized", - isMut: false, - isSigner: true, + signer: true, }, ], args: [ { name: "authorized", - type: "publicKey", + type: "pubkey", }, ], }, { - name: "allocate", + name: "createAccount", + discriminator: [0, 0, 0, 0], accounts: [ { - name: "pubkey", - isMut: true, - isSigner: true, + name: "from", + writable: true, + signer: true, + }, + { + name: "to", + writable: true, + signer: true, }, ], args: [ + { + name: "lamports", + type: "u64", + }, { name: "space", type: "u64", }, + { + name: "owner", + type: "pubkey", + }, ], }, { - name: "allocateWithSeed", + name: "createAccountWithSeed", + discriminator: [3, 0, 0, 0], accounts: [ { - name: "account", - isMut: true, - isSigner: false, + name: "from", + writable: true, + signer: true, + }, + { + name: "to", + writable: true, }, { name: "base", - isMut: false, - isSigner: true, + signer: true, }, ], args: [ { name: "base", - type: "publicKey", + type: "pubkey", }, { name: "seed", type: "string", }, + { + name: "lamports", + type: "u64", + }, { name: "space", type: "u64", }, { name: "owner", - type: "publicKey", + type: "pubkey", }, ], }, { - name: "assignWithSeed", + name: "initializeNonceAccount", + discriminator: [6, 0, 0, 0], accounts: [ { - name: "account", - isMut: true, - isSigner: false, + name: "nonce", + writable: true, + signer: true, }, { - name: "base", - isMut: false, - isSigner: true, + name: "recentBlockhashes", + }, + { + name: "rent", + address: "SysvarRent111111111111111111111111111111111", }, ], args: [ { - name: "base", - type: "publicKey", + name: "authorized", + type: "pubkey", }, + ], + }, + { + name: "transfer", + discriminator: [2, 0, 0, 0], + accounts: [ { - name: "seed", - type: "string", + name: "from", + writable: true, + signer: true, }, { - name: "owner", - type: "publicKey", + name: "to", + writable: true, + }, + ], + args: [ + { + name: "lamports", + type: "u64", }, ], }, { name: "transferWithSeed", + discriminator: [11, 0, 0, 0], accounts: [ { name: "from", - isMut: true, - isSigner: false, + writable: true, }, { name: "base", - isMut: false, - isSigner: true, + signer: true, }, { name: "to", - isMut: true, - isSigner: false, + writable: true, }, ], args: [ @@ -727,12 +684,61 @@ export const IDL: SystemProgram = { }, { name: "owner", - type: "publicKey", + type: "pubkey", + }, + ], + }, + { + name: "withdrawNonceAccount", + discriminator: [5, 0, 0, 0], + accounts: [ + { + name: "nonce", + writable: true, + }, + { + name: "to", + writable: true, + }, + { + name: "recentBlockhashes", + }, + { + name: "rent", + address: "SysvarRent111111111111111111111111111111111", + }, + { + name: "authorized", + signer: true, + }, + ], + args: [ + { + name: "lamports", + type: "u64", }, ], }, ], accounts: [ + { + name: "nonce", + discriminator: [], + }, + ], + types: [ + { + name: "feeCalculator", + type: { + kind: "struct", + fields: [ + { + name: "lamportsPerSignature", + type: "u64", + }, + ], + }, + }, { name: "nonce", type: { @@ -748,34 +754,22 @@ export const IDL: SystemProgram = { }, { name: "authorizedPubkey", - type: "publicKey", + type: "pubkey", }, { name: "nonce", - type: "publicKey", + type: "pubkey", }, { name: "feeCalculator", type: { - defined: "FeeCalculator", + defined: { + name: "feeCalculator", + }, }, }, ], }, }, ], - types: [ - { - name: "FeeCalculator", - type: { - kind: "struct", - fields: [ - { - name: "lamportsPerSignature", - type: "u64", - }, - ], - }, - }, - ], }; diff --git a/ts/packages/anchor/src/program/accounts-resolver.ts b/ts/packages/anchor/src/program/accounts-resolver.ts index 0bf4be87b1..836af62e1a 100644 --- a/ts/packages/anchor/src/program/accounts-resolver.ts +++ b/ts/packages/anchor/src/program/accounts-resolver.ts @@ -1,36 +1,30 @@ -import camelCase from "camelcase"; -import { - PublicKey, - SystemProgram, - SYSVAR_CLOCK_PUBKEY, - SYSVAR_RENT_PUBKEY, -} from "@solana/web3.js"; +import BN from "bn.js"; +import { PublicKey } from "@solana/web3.js"; import { Idl, IdlSeed, - IdlAccount, - IdlAccountItem, - IdlAccounts, + IdlInstructionAccountItem, + IdlInstructionAccount, IdlTypeDef, IdlTypeDefTyStruct, IdlType, - isIdlAccounts, + isCompositeAccounts, IdlSeedConst, IdlSeedArg, IdlSeedAccount, + IdlTypeDefined, + IdlDefinedFieldsNamed, } from "../idl.js"; -import * as utf8 from "../utils/bytes/utf8.js"; -import { TOKEN_PROGRAM_ID, ASSOCIATED_PROGRAM_ID } from "../utils/token.js"; import { AllInstructions } from "./namespace/types.js"; import Provider from "../provider.js"; import { AccountNamespace } from "./namespace/account.js"; import { BorshAccountsCoder } from "src/coder/index.js"; import { decodeTokenAccount } from "./token-account-layout"; -import { Program, translateAddress } from "./index.js"; +import { Address, Program, translateAddress } from "./index.js"; import { + PartialAccounts, flattenPartialAccounts, isPartialAccounts, - PartialAccounts, } from "./namespace/methods"; export type AccountsGeneric = { @@ -53,88 +47,101 @@ export type CustomAccountResolver = (params: { // Populates a given accounts context with PDAs and common missing accounts. export class AccountsResolver { - _args: Array; - static readonly CONST_ACCOUNTS = { - associatedTokenProgram: ASSOCIATED_PROGRAM_ID, - rent: SYSVAR_RENT_PUBKEY, - systemProgram: SystemProgram.programId, - tokenProgram: TOKEN_PROGRAM_ID, - clock: SYSVAR_CLOCK_PUBKEY, - }; - private _accountStore: AccountStore; constructor( - _args: Array, + private _args: any[], private _accounts: AccountsGeneric, private _provider: Provider, private _programId: PublicKey, private _idlIx: AllInstructions, - _accountNamespace: AccountNamespace, + accountNamespace: AccountNamespace, private _idlTypes: IdlTypeDef[], private _customResolver?: CustomAccountResolver ) { - this._args = _args; this._accountStore = new AccountStore( _provider, - _accountNamespace, - this._programId + accountNamespace, + _programId ); } - public args(_args: Array): void { - this._args = _args; + public args(args: Array): void { + this._args = args; } // Note: We serially resolve PDAs one by one rather than doing them // in parallel because there can be dependencies between // addresses. That is, one PDA can be used as a seed in another. public async resolve() { - await this.resolveConst(this._idlIx.accounts); - this._resolveEventCpi(this._idlIx.accounts); + this.resolveEventCpi(this._idlIx.accounts); + this.resolveConst(this._idlIx.accounts); // Auto populate pdas and relations until we stop finding new accounts + let depth = 0; while ( - (await this.resolvePdas(this._idlIx.accounts)) + - (await this.resolveRelations(this._idlIx.accounts)) + + (await this.resolvePdasAndRelations(this._idlIx.accounts)) + (await this.resolveCustom()) > 0 - ) {} + ) { + depth++; + if (depth === 16) { + throw new Error("Reached maximum depth for account resolution"); + } + } } - private async resolveCustom(): Promise { - if (this._customResolver) { - const { accounts, resolved } = await this._customResolver({ - args: this._args, - accounts: this._accounts, - provider: this._provider, - programId: this._programId, - idlIx: this._idlIx, - }); - this._accounts = accounts; - return resolved; + public resolveOptionals(accounts: PartialAccounts) { + Object.assign( + this._accounts, + this.resolveOptionalsHelper(accounts, this._idlIx.accounts) + ); + } + + private get(path: string[]): PublicKey | undefined { + // Only return if pubkey + const ret = path.reduce( + (acc, subPath) => acc && acc[subPath], + this._accounts + ); + + if (ret && ret.toBase58) { + return ret as PublicKey; } - return 0; + } + + private set(path: string[], value: PublicKey): void { + let cur = this._accounts; + path.forEach((p, i) => { + const isLast = i === path.length - 1; + if (isLast) { + cur[p] = value; + } + + cur[p] = cur[p] ?? {}; + cur = cur[p] as AccountsGeneric; + }); } private resolveOptionalsHelper( partialAccounts: PartialAccounts, - accountItems: IdlAccountItem[] + accounts: IdlInstructionAccountItem[] ): AccountsGeneric { const nestedAccountsGeneric: AccountsGeneric = {}; // Looping through accountItem array instead of on partialAccounts, so // we only traverse array once - for (const accountItem of accountItems) { + for (const accountItem of accounts) { const accountName = accountItem.name; const partialAccount = partialAccounts[accountName]; // Skip if the account isn't included (thus would be undefined) if (partialAccount === undefined) continue; + if (isPartialAccounts(partialAccount)) { // is compound accounts, recurse one level deeper - if (isIdlAccounts(accountItem)) { + if (isCompositeAccounts(accountItem)) { nestedAccountsGeneric[accountName] = this.resolveOptionalsHelper( partialAccount, - accountItem["accounts"] as IdlAccountItem[] + accountItem["accounts"] ); } else { // Here we try our best to recover gracefully. If there are optionals we can't check, we will fail then. @@ -146,8 +153,10 @@ export class AccountsResolver { } else { // if not compound accounts, do null/optional check and proceed if (partialAccount !== null) { - nestedAccountsGeneric[accountName] = translateAddress(partialAccount); - } else if (accountItem["isOptional"]) { + nestedAccountsGeneric[accountName] = translateAddress( + partialAccount as Address + ); + } else if (accountItem["optional"]) { nestedAccountsGeneric[accountName] = this._programId; } } @@ -155,78 +164,20 @@ export class AccountsResolver { return nestedAccountsGeneric; } - public resolveOptionals(accounts: PartialAccounts) { - Object.assign( - this._accounts, - this.resolveOptionalsHelper(accounts, this._idlIx.accounts) - ); - } - - private get(path: string[]): PublicKey | undefined { - // Only return if pubkey - const ret = path.reduce( - (acc, subPath) => acc && acc[subPath], - this._accounts - ); - - if (ret && ret.toBase58) { - return ret as PublicKey; + private async resolveCustom() { + if (this._customResolver) { + const { accounts, resolved } = await this._customResolver({ + args: this._args, + accounts: this._accounts, + provider: this._provider, + programId: this._programId, + idlIx: this._idlIx, + }); + this._accounts = accounts; + return resolved; } - } - private set(path: string[], value: PublicKey): void { - let curr = this._accounts; - path.forEach((p, idx) => { - const isLast = idx == path.length - 1; - if (isLast) { - curr[p] = value; - } - - curr[p] = curr[p] || {}; - curr = curr[p] as AccountsGeneric; - }); - } - - private async resolveConst( - accounts: IdlAccountItem[], - path: string[] = [] - ): Promise { - for (let k = 0; k < accounts.length; k += 1) { - const accountDescOrAccounts = accounts[k]; - const subAccounts = (accountDescOrAccounts as IdlAccounts).accounts; - if (subAccounts) { - await this.resolveConst(subAccounts, [ - ...path, - camelCase(accountDescOrAccounts.name), - ]); - } - - const accountDesc = accountDescOrAccounts as IdlAccount; - const accountDescName = camelCase(accountDescOrAccounts.name); - - // Signers default to the provider. - if (accountDesc.isSigner && !this.get([...path, accountDescName])) { - // @ts-expect-error - if (this._provider.wallet === undefined) { - throw new Error( - "This function requires the Provider interface implementor to have a 'wallet' field." - ); - } - // @ts-expect-error - this.set([...path, accountDescName], this._provider.wallet.publicKey); - } - - // Common accounts are auto populated with magic names by convention. - if ( - Reflect.has(AccountsResolver.CONST_ACCOUNTS, accountDescName) && - !this.get([...path, accountDescName]) - ) { - this.set( - [...path, accountDescName], - AccountsResolver.CONST_ACCOUNTS[accountDescName] - ); - } - } + return 0; } /** @@ -235,17 +186,16 @@ export class AccountsResolver { * Accounts will only be resolved if they are declared next to each other to * reduce the chance of name collision. */ - private _resolveEventCpi( - accounts: IdlAccountItem[], + private resolveEventCpi( + accounts: IdlInstructionAccountItem[], path: string[] = [] ): void { for (const i in accounts) { - const accountDescOrAccounts = accounts[i]; - const subAccounts = (accountDescOrAccounts as IdlAccounts).accounts; - if (subAccounts) { - this._resolveEventCpi(subAccounts, [ + const accountOrAccounts = accounts[i]; + if (isCompositeAccounts(accountOrAccounts)) { + this.resolveEventCpi(accountOrAccounts.accounts, [ ...path, - camelCase(accountDescOrAccounts.name), + accountOrAccounts.name, ]); } @@ -253,8 +203,8 @@ export class AccountsResolver { const nextIndex = +i + 1; if (nextIndex === accounts.length) return; - const currentName = camelCase(accounts[i].name); - const nextName = camelCase(accounts[nextIndex].name); + const currentName = accounts[i].name; + const nextName = accounts[nextIndex].name; // Populate event CPI accounts if they exist if (currentName === "eventAuthority" && nextName === "program") { @@ -279,364 +229,340 @@ export class AccountsResolver { } } - private async resolvePdas( - accounts: IdlAccountItem[], + private resolveConst( + accounts: IdlInstructionAccountItem[], path: string[] = [] - ): Promise { - let found = 0; - for (let k = 0; k < accounts.length; k += 1) { - const accountDesc = accounts[k]; - const subAccounts = (accountDesc as IdlAccounts).accounts; - if (subAccounts) { - found += await this.resolvePdas(subAccounts, [ - ...path, - camelCase(accountDesc.name), - ]); - } + ) { + for (const accountOrAccounts of accounts) { + const name = accountOrAccounts.name; + if (isCompositeAccounts(accountOrAccounts)) { + this.resolveConst(accountOrAccounts.accounts, [...path, name]); + } else { + const account = accountOrAccounts; + + if ((account.signer || account.address) && !this.get([...path, name])) { + // Default signers to the provider + if (account.signer) { + // @ts-expect-error + if (!this._provider.wallet) { + throw new Error( + "This function requires the `Provider` interface implementor to have a `wallet` field." + ); + } + // @ts-expect-error + this.set([...path, name], this._provider.wallet.publicKey); + } - const accountDescCasted: IdlAccount = accountDesc as IdlAccount; - const accountDescName = camelCase(accountDesc.name); - - // PDA derived from IDL seeds. - if ( - accountDescCasted.pda && - accountDescCasted.pda.seeds.length > 0 && - !this.get([...path, accountDescName]) - ) { - if (Boolean(await this.autoPopulatePda(accountDescCasted, path))) { - found += 1; + // Set based on `address` field + if (account.address) { + this.set([...path, name], translateAddress(account.address)); + } } } } - return found; } - private async resolveRelations( - accounts: IdlAccountItem[], + private async resolvePdasAndRelations( + accounts: IdlInstructionAccountItem[], path: string[] = [] ): Promise { let found = 0; - for (let k = 0; k < accounts.length; k += 1) { - const accountDesc = accounts[k]; - const subAccounts = (accountDesc as IdlAccounts).accounts; - if (subAccounts) { - found += await this.resolveRelations(subAccounts, [ - ...path, - camelCase(accountDesc.name), - ]); - } - const relations = (accountDesc as IdlAccount).relations || []; - const accountDescName = camelCase(accountDesc.name); - const newPath = [...path, accountDescName]; - - // If we have this account and there's some missing accounts that are relations to this account, fetch them - const accountKey = this.get(newPath); - if (accountKey) { - const matching = relations.filter( - (rel) => !this.get([...path, camelCase(rel)]) + for (const accountOrAccounts of accounts) { + const name = accountOrAccounts.name; + if (isCompositeAccounts(accountOrAccounts)) { + found += await this.resolvePdasAndRelations( + accountOrAccounts.accounts, + [...path, name] ); - - found += matching.length; - if (matching.length > 0) { - const account = await this._accountStore.fetchAccount({ - publicKey: accountKey, - }); - await Promise.all( - matching.map(async (rel) => { - const relName = camelCase(rel); - - this.set([...path, relName], account[relName]); - return account[relName]; - }) - ); + } else { + const account = accountOrAccounts; + if ((account.pda || account.relations) && !this.get([...path, name])) { + found++; + + // Accounts might not get resolved successfully if a seed depends on + // another seed to be resolved *and* the accounts for resolution are + // out of order. In this case, skip the accounts that throw in order + // to resolve those accounts later. + try { + if (account.pda) { + const seeds = await Promise.all( + account.pda.seeds.map((seed) => this.toBuffer(seed, path)) + ); + if (seeds.some((seed) => !seed)) { + continue; + } + + const programId = await this.parseProgramId(account, path); + const [pubkey] = PublicKey.findProgramAddressSync( + seeds as Buffer[], + programId + ); + + this.set([...path, name], pubkey); + } + } catch {} + + try { + if (account.relations) { + const accountKey = this.get([...path, account.relations[0]]); + if (accountKey) { + const account = await this._accountStore.fetchAccount({ + publicKey: accountKey, + }); + this.set([...path, name], account[name]); + } + } + } catch {} } } } - return found; - } - - private async autoPopulatePda(accountDesc: IdlAccount, path: string[] = []) { - if (!accountDesc.pda || !accountDesc.pda.seeds) - throw new Error("Must have seeds"); - - const seeds: (Buffer | undefined)[] = await Promise.all( - accountDesc.pda.seeds.map((seedDesc: IdlSeed) => - this.toBuffer(seedDesc, path) - ) - ); - - if (seeds.some((seed) => typeof seed == "undefined")) { - return; - } - - const programId = await this.parseProgramId(accountDesc, path); - if (!programId) { - return; - } - const [pubkey] = await PublicKey.findProgramAddress( - seeds as Buffer[], - programId - ); - this.set([...path, camelCase(accountDesc.name)], pubkey); + return found; } private async parseProgramId( - accountDesc: IdlAccount, + account: IdlInstructionAccount, path: string[] = [] ): Promise { - if (!accountDesc.pda?.programId) { + if (!account.pda?.program) { return this._programId; } - switch (accountDesc.pda.programId.kind) { - case "const": - return new PublicKey( - this.toBufferConst(accountDesc.pda.programId.value) - ); - case "arg": - return this.argValue(accountDesc.pda.programId); - case "account": - return await this.accountValue(accountDesc.pda.programId, path); - default: - throw new Error( - `Unexpected program seed: ${accountDesc.pda.programId}` - ); + const buf = await this.toBuffer(account.pda.program, path); + if (!buf) { + throw new Error(`Program seed not resolved: ${account.name}`); } + + return new PublicKey(buf); } private async toBuffer( - seedDesc: IdlSeed, + seed: IdlSeed, path: string[] = [] ): Promise { - switch (seedDesc.kind) { + switch (seed.kind) { case "const": - return this.toBufferConst(seedDesc); + return this.toBufferConst(seed); case "arg": - return await this.toBufferArg(seedDesc); + return await this.toBufferArg(seed); case "account": - return await this.toBufferAccount(seedDesc, path); + return await this.toBufferAccount(seed, path); default: - throw new Error(`Unexpected seed: ${seedDesc}`); + throw new Error(`Unexpected seed: ${seed}`); } } - /** - * Recursively get the type at some path of either a primitive or a user defined struct. - */ - private getType(type: IdlType, path: string[] = []): string { - if (path.length > 0 && (type as any).defined) { - const subType = this._idlTypes.find( - (t) => t.name === (type as any).defined - ); - if (!subType) { - throw new Error(`Cannot find type ${(type as any).defined}`); - } - - const structType = subType.type as IdlTypeDefTyStruct; // enum not supported yet - const field = structType.fields.find((field) => field.name === path[0]); - - return this.getType(field!.type, path.slice(1)); - } - - return type as string; + private toBufferConst(seed: IdlSeedConst): Buffer { + return this.toBufferValue("bytes", seed.value); } - private toBufferConst(seedDesc: IdlSeedConst): Buffer { - return this.toBufferValue(this.getType(seedDesc.type), seedDesc.value); - } + private async toBufferArg(seed: IdlSeedArg): Promise { + const [name, ...path] = seed.path.split("."); - private async toBufferArg(seedDesc: IdlSeedArg): Promise { - const argValue = this.argValue(seedDesc); - if (typeof argValue === "undefined") { - return; + const index = this._idlIx.args.findIndex((arg) => arg.name === name); + if (index === -1) { + throw new Error(`Unable to find argument for seed: ${name}`); } - return this.toBufferValue( - this.getType(seedDesc.type, (seedDesc.path || "").split(".").slice(1)), - argValue - ); - } - private argValue(seedDesc: IdlSeedArg): any { - const split = seedDesc.path.split("."); - const seedArgName = camelCase(split[0]); - - const idlArgPosition = this._idlIx.args.findIndex( - (argDesc: any) => argDesc.name === seedArgName + const value = path.reduce( + (acc, path) => (acc ?? {})[path], + this._args[index] ); - if (idlArgPosition === -1) { - throw new Error(`Unable to find argument for seed: ${seedArgName}`); + if (value === undefined) { + return; } - return split - .slice(1) - .reduce((curr, path) => (curr || {})[path], this._args[idlArgPosition]); + const type = this.getType(this._idlIx.args[index].type, path); + return this.toBufferValue(type, value); } private async toBufferAccount( - seedDesc: IdlSeedAccount, + seed: IdlSeedAccount, path: string[] = [] ): Promise { - const accountValue = await this.accountValue(seedDesc, path); - if (!accountValue) { - return; - } - return this.toBufferValue(seedDesc.type, accountValue); - } - - private async accountValue( - seedDesc: IdlSeedAccount, - path: string[] = [] - ): Promise { - const pathComponents = seedDesc.path.split("."); - - const fieldName = pathComponents[0]; - const fieldPubkey = this.get([...path, camelCase(fieldName)]); + const [name, ...paths] = seed.path.split("."); + const fieldPubkey = this.get([...path, name]); + if (!fieldPubkey) return; - if (fieldPubkey === null) { - throw new Error(`fieldPubkey is null`); + // The seed is a pubkey of the account. + if (!paths.length) { + return this.toBufferValue("pubkey", fieldPubkey); } - // The seed is a pubkey of the account. - if (pathComponents.length === 1) { - return fieldPubkey; + if (!seed.account) { + throw new Error( + `Seed account is required in order to resolve type: ${seed.path}` + ); } // The key is account data. // // Fetch and deserialize it. const account = await this._accountStore.fetchAccount({ - publicKey: fieldPubkey as PublicKey, - name: seedDesc.account, + publicKey: fieldPubkey, + name: seed.account, }); // Dereference all fields in the path to get the field value // used in the seed. - const fieldValue = this.parseAccountValue(account, pathComponents.slice(1)); - return fieldValue; - } - - private parseAccountValue(account: T, path: Array): any { - let accountField: any; - while (path.length > 0) { - accountField = account[camelCase(path[0])]; - path = path.slice(1); + let accountValue = account; + let currentPaths = paths; + while (currentPaths.length > 0) { + accountValue = accountValue[currentPaths[0]]; + currentPaths = currentPaths.slice(1); } - return accountField; + if (accountValue === undefined) return; + + const type = this.getType({ defined: { name: seed.account } }, paths); + return this.toBufferValue(type, accountValue); } - // Converts the given idl valaue into a Buffer. The values here must be - // primitives. E.g. no structs. - // - // TODO: add more types here as needed. - private toBufferValue(type: string | any, value: any): Buffer { + /** + * Converts the given idl valaue into a Buffer. The values here must be + * primitives, e.g. no structs. + */ + private toBufferValue(type: any, value: any): Buffer { switch (type) { case "u8": + case "i8": return Buffer.from([value]); case "u16": - let b = Buffer.alloc(2); - b.writeUInt16LE(value); - return b; + case "i16": + return new BN(value).toArrayLike(Buffer, "le", 2); case "u32": - let buf = Buffer.alloc(4); - buf.writeUInt32LE(value); - return buf; + case "i32": + return new BN(value).toArrayLike(Buffer, "le", 4); case "u64": - let bU64 = Buffer.alloc(8); - bU64.writeBigUInt64LE(BigInt(value)); - return bU64; + case "i64": + return new BN(value).toArrayLike(Buffer, "le", 8); + case "u128": + case "i128": + return new BN(value).toArrayLike(Buffer, "le", 16); + case "u256": + case "i256": + return new BN(value).toArrayLike(Buffer, "le", 32); case "string": - return Buffer.from(utf8.encode(value)); - case "publicKey": + return Buffer.from(value); + case "pubkey": return value.toBuffer(); + case "bytes": + return Buffer.from(value); default: - if (type.array) { + if (type?.array) { return Buffer.from(value); } + throw new Error(`Unexpected seed type: ${type}`); } } + + /** + * Recursively get the type at some path of either a primitive or a user + * defined struct. + */ + private getType( + type: IdlType, + path: string[] = [] + ): Extract { + const typeName = (type as IdlTypeDefined)?.defined?.name; + if (typeName) { + // Handle token account separately + if (typeName === "tokenAccount") { + switch (path.at(0)) { + case "mint": + case "owner": + return "pubkey"; + case "amount": + case "delagatedAmount": + return "u64"; + default: + throw new Error(`Unknown token account path: ${path}`); + } + } + + const definedType = this._idlTypes.find((t) => t.name === typeName); + if (!definedType) { + throw new Error(`Type not found: ${typeName}`); + } + + // Only named structs are supported + const [fieldName, ...subPath] = path; + const fields = (definedType.type as IdlTypeDefTyStruct) + .fields as IdlDefinedFieldsNamed; + const field = fields.find((field) => field.name === fieldName); + if (!field) { + throw new Error(`Field not found: ${fieldName}`); + } + + return this.getType(field.type, subPath); + } + + return type as Extract; + } } // TODO: this should be configureable to avoid unnecessary requests. -export class AccountStore { +class AccountStore { private _cache = new Map(); private _idls: Record> = {}; - // todo: don't use the progrma use the account namespace. constructor( private _provider: Provider, - _accounts: AccountNamespace, - private _programId: PublicKey - ) { - this._idls[_programId.toBase58()] = _accounts; - } - - private async ensureIdl( + accounts: AccountNamespace, programId: PublicKey - ): Promise | undefined> { - if (!this._idls[programId.toBase58()]) { - const idl = await Program.fetchIdl(programId, this._provider); - if (idl) { - const program = new Program(idl, programId, this._provider); - this._idls[programId.toBase58()] = program.account; - } - } - - return this._idls[programId.toBase58()]; + ) { + this._idls[programId.toBase58()] = accounts; } public async fetchAccount({ publicKey, name, - programId = this._programId, }: { publicKey: PublicKey; name?: string; programId?: PublicKey; }): Promise { - const address = publicKey.toString(); + const address = publicKey.toBase58(); if (!this._cache.has(address)) { - if (name === "TokenAccount") { - const accountInfo = await this._provider.connection.getAccountInfo( - publicKey - ); - if (accountInfo === null) { - throw new Error(`invalid account info for ${address}`); - } - const data = decodeTokenAccount(accountInfo.data); - this._cache.set(address, data); - } else if (name) { - const accounts = await this.ensureIdl(programId); + const accountInfo = await this._provider.connection.getAccountInfo( + publicKey + ); + if (accountInfo === null) { + throw new Error(`Account not found: ${address}`); + } + + if (name === "tokenAccount") { + const account = decodeTokenAccount(accountInfo.data); + this._cache.set(address, account); + } else { + const accounts = await this.getAccountsNs(accountInfo.owner); if (accounts) { - const accountFetcher = accounts[camelCase(name)]; - if (accountFetcher) { - const account = await accountFetcher.fetch(publicKey); + const accountNs = Object.values(accounts)[0] as any; + if (accountNs) { + const account = ( + accountNs.coder.accounts as BorshAccountsCoder + ).decodeAny(accountInfo.data); this._cache.set(address, account); } } - } else { - const account = await this._provider.connection.getAccountInfo( - publicKey - ); - if (account === null) { - throw new Error(`invalid account info for ${address}`); - } - const data = account.data; - const accounts = await this.ensureIdl(account.owner); - if (accounts) { - const firstAccountLayout = Object.values(accounts)[0] as any; - if (!firstAccountLayout) { - throw new Error("No accounts for this program"); - } - - const result = ( - firstAccountLayout.coder.accounts as BorshAccountsCoder - ).decodeAny(data); - this._cache.set(address, result); - } } } + return this._cache.get(address); } + + private async getAccountsNs( + programId: PublicKey + ): Promise | undefined> { + const programIdStr = programId.toBase58(); + if (!this._idls[programIdStr]) { + const idl = await Program.fetchIdl(programId, this._provider); + if (idl) { + const program = new Program(idl, programId, this._provider); + this._idls[programIdStr] = program.account; + } + } + + return this._idls[programIdStr]; + } } diff --git a/ts/packages/anchor/src/program/common.ts b/ts/packages/anchor/src/program/common.ts index 0023ce3f79..48460ab76f 100644 --- a/ts/packages/anchor/src/program/common.ts +++ b/ts/packages/anchor/src/program/common.ts @@ -1,6 +1,11 @@ import EventEmitter from "eventemitter3"; import { PublicKey } from "@solana/web3.js"; -import { Idl, IdlInstruction, IdlAccountItem, IdlStateMethod } from "../idl.js"; +import { + Idl, + IdlInstruction, + IdlInstructionAccountItem, + isCompositeAccounts, +} from "../idl.js"; import { Accounts } from "./context.js"; export type Subscription = { @@ -19,11 +24,7 @@ export function parseIdlErrors(idl: Idl): Map { return errors; } -// Allow either IdLInstruction or IdlStateMethod since the types share fields. -export function toInstruction( - idlIx: IdlInstruction | IdlStateMethod, - ...args: any[] -) { +export function toInstruction(idlIx: IdlInstruction, ...args: any[]) { if (idlIx.args.length != args.length) { throw new Error("Invalid argument length"); } @@ -39,15 +40,15 @@ export function toInstruction( // Throws error if any account required for the `ix` is not given. export function validateAccounts( - ixAccounts: IdlAccountItem[], + ixAccounts: IdlInstructionAccountItem[], accounts: Accounts = {} ) { ixAccounts.forEach((acc) => { - if ("accounts" in acc) { + if (isCompositeAccounts(acc)) { validateAccounts(acc.accounts, accounts[acc.name] as Accounts); } else { - if (accounts[acc.name] === undefined) { - throw new Error(`Invalid arguments: ${acc.name} not provided.`); + if (!accounts[acc.name]) { + throw new Error(`Account \`${acc.name}\` not provided.`); } } }); diff --git a/ts/packages/anchor/src/program/context.ts b/ts/packages/anchor/src/program/context.ts index ef1d37fdbb..f7b975cd1d 100644 --- a/ts/packages/anchor/src/program/context.ts +++ b/ts/packages/anchor/src/program/context.ts @@ -5,7 +5,11 @@ import { TransactionInstruction, } from "@solana/web3.js"; import { Address } from "./common.js"; -import { IdlAccountItem, IdlAccounts, IdlInstruction } from "../idl.js"; +import { + IdlInstructionAccountItem, + IdlInstructionAccounts, + IdlInstruction, +} from "../idl.js"; /** * Context provides all non-argument inputs for generating Anchor transactions. @@ -50,11 +54,6 @@ export type Context = { * Commitment parameters to use for a transaction. */ options?: ConfirmOptions; - - /** - * An optional override for the default instruction discriminator. - */ - discriminator?: Buffer; }; /** @@ -66,15 +65,20 @@ export type Context = { * If multiple accounts are nested in the rust program, then they should be * nested here. */ -export type Accounts = { +export type Accounts< + A extends IdlInstructionAccountItem = IdlInstructionAccountItem +> = { [N in A["name"]]: Account; }; -type Account = A extends IdlAccounts - ? Accounts - : A extends { isOptional: true } - ? Address | null - : Address; +type Account = + A extends IdlInstructionAccounts + ? Accounts + : A extends { optional: true } + ? Address | null + : A extends { signer: true } + ? Address | undefined + : Address; export function splitArgsAndCtx( idlIx: IdlInstruction, diff --git a/ts/packages/anchor/src/program/event.ts b/ts/packages/anchor/src/program/event.ts index 6c8888627a..42b41d5dbd 100644 --- a/ts/packages/anchor/src/program/event.ts +++ b/ts/packages/anchor/src/program/event.ts @@ -1,5 +1,5 @@ import { PublicKey } from "@solana/web3.js"; -import { IdlEvent, IdlEventField } from "../idl.js"; +import { IdlEvent, IdlField } from "../idl.js"; import { Coder } from "../coder/index.js"; import { DecodeType } from "./namespace/types.js"; import Provider from "../provider.js"; @@ -15,10 +15,12 @@ export type Event< Defined = Record > = { name: E["name"]; - data: EventData; + // TODO: + // data: EventData; + data: any; }; -export type EventData = { +export type EventData = { [N in T["name"]]: DecodeType<(T & { name: N })["type"], Defined>; }; diff --git a/ts/packages/anchor/src/program/index.ts b/ts/packages/anchor/src/program/index.ts index d9722a0aee..e8c3398adc 100644 --- a/ts/packages/anchor/src/program/index.ts +++ b/ts/packages/anchor/src/program/index.ts @@ -1,7 +1,13 @@ import { inflate } from "pako"; import { PublicKey } from "@solana/web3.js"; import Provider, { getProvider } from "../provider.js"; -import { Idl, idlAddress, decodeIdlAccount, IdlInstruction } from "../idl.js"; +import { + Idl, + idlAddress, + decodeIdlAccount, + IdlInstruction, + convertIdlToCamelCase, +} from "../idl.js"; import { Coder, BorshCoder } from "../coder/index.js"; import NamespaceFactory, { RpcNamespace, @@ -224,13 +230,25 @@ export class Program { private _programId: PublicKey; /** - * IDL defining the program's interface. + * IDL in camelCase format to work in TypeScript. + * + * See {@link rawIdl} field if you need the original IDL. */ public get idl(): IDL { return this._idl; } private _idl: IDL; + /** + * Raw IDL i.e. the original IDL without camelCase conversion. + * + * See {@link idl} field if you need the camelCased version of the IDL. + */ + public get rawIdl(): Idl { + return this._rawIdl; + } + private _rawIdl: Idl; + /** * Coder for serializing requests. */ @@ -264,7 +282,7 @@ export class Program { public constructor( idl: IDL, programId: Address, - provider?: Provider, + provider: Provider = getProvider(), coder?: Coder, getCustomResolver?: ( instruction: IdlInstruction @@ -272,25 +290,24 @@ export class Program { ) { programId = translateAddress(programId); - if (!provider) { - provider = getProvider(); - } + const camelCasedIdl = convertIdlToCamelCase(idl); // Fields. - this._idl = idl; + this._idl = camelCasedIdl; + this._rawIdl = idl; this._provider = provider; this._programId = programId; - this._coder = coder ?? new BorshCoder(idl); + this._coder = coder ?? new BorshCoder(camelCasedIdl); this._events = new EventManager(this._programId, provider, this._coder); // Dynamic namespaces. const [rpc, instruction, transaction, account, simulate, methods, views] = NamespaceFactory.build( - idl, + camelCasedIdl, this._coder, programId, provider, - getCustomResolver ?? (() => undefined) + getCustomResolver ); this.rpc = rpc; this.instruction = instruction; @@ -359,7 +376,7 @@ export class Program { * program logs. */ public addEventListener>( - eventName: E, + eventName: E & string, callback: ( event: IdlEvents[E], slot: number, diff --git a/ts/packages/anchor/src/program/namespace/account.ts b/ts/packages/anchor/src/program/namespace/account.ts index 2adf3f90e2..87e53e155c 100644 --- a/ts/packages/anchor/src/program/namespace/account.ts +++ b/ts/packages/anchor/src/program/namespace/account.ts @@ -1,4 +1,3 @@ -import camelCase from "camelcase"; import EventEmitter from "eventemitter3"; import { Signer, @@ -12,7 +11,7 @@ import { Context, } from "@solana/web3.js"; import Provider, { getProvider } from "../../provider.js"; -import { Idl, IdlAccountDef } from "../../idl.js"; +import { Idl, IdlAccount } from "../../idl.js"; import { Coder, BorshCoder } from "../../coder/index.js"; import { Subscription, Address, translateAddress } from "../common.js"; import { AllAccountsMap, IdlAccounts } from "./types.js"; @@ -25,25 +24,21 @@ export default class AccountFactory { programId: PublicKey, provider?: Provider ): AccountNamespace { - const accountFns = {} as AccountNamespace; - - idl.accounts?.forEach((idlAccount) => { - const name = camelCase(idlAccount.name); - accountFns[name] = new AccountClient( + return (idl.accounts ?? []).reduce((accountFns, acc) => { + accountFns[acc.name] = new AccountClient( idl, - idlAccount, + acc, programId, provider, coder ); - }); - - return accountFns; + return accountFns; + }, {}) as AccountNamespace; } } type NullableIdlAccount = IDL["accounts"] extends undefined - ? IdlAccountDef + ? IdlAccount : NonNullable[number]; /** @@ -66,15 +61,17 @@ type NullableIdlAccount = IDL["accounts"] extends undefined * * For the full API, see the [[AccountClient]] reference. */ -export type AccountNamespace = { - [N in keyof AllAccountsMap]: AccountClient; +export type AccountNamespace = { + [A in keyof AllAccountsMap]: AccountClient; }; export class AccountClient< IDL extends Idl = Idl, - N extends keyof IdlAccounts = keyof IdlAccounts, - A extends NullableIdlAccount = NullableIdlAccount, - T = IdlAccounts[N] + A extends keyof IdlAccounts = keyof IdlAccounts, + N extends NullableIdlAccount = NullableIdlAccount, + T = IdlAccounts[A] extends Record + ? IdlAccounts[A] + : never > { /** * Returns the number of bytes in this account. @@ -108,17 +105,11 @@ export class AccountClient< } private _coder: Coder; - /** - * Returns the idl account. - */ - get idlAccount(): A { - return this._idlAccount; - } - private _idlAccount: A; + private _idlAccount: N; constructor( idl: IDL, - idlAccount: A, + idlAccount: N, programId: PublicKey, provider?: Provider, coder?: Coder @@ -127,7 +118,7 @@ export class AccountClient< this._programId = programId; this._provider = provider ?? getProvider(); this._coder = coder ?? new BorshCoder(idl); - this._size = this._coder.accounts.size(idlAccount); + this._size = this._coder.accounts.size(idlAccount.name); } /** diff --git a/ts/packages/anchor/src/program/namespace/index.ts b/ts/packages/anchor/src/program/namespace/index.ts index 7cd23d8af6..b294282c61 100644 --- a/ts/packages/anchor/src/program/namespace/index.ts +++ b/ts/packages/anchor/src/program/namespace/index.ts @@ -1,4 +1,3 @@ -import camelCase from "camelcase"; import { PublicKey } from "@solana/web3.js"; import { Coder } from "../../coder/index.js"; import Provider from "../../provider.js"; @@ -60,8 +59,7 @@ export default class NamespaceFactory { idl.instructions.forEach((idlIx) => { const ixItem = InstructionFactory.build( idlIx, - (ixName, ix, discriminator) => - coder.instruction.encode(ixName, ix, discriminator), + (ixName, ix) => coder.instruction.encode(ixName, ix), programId ); const txItem = TransactionFactory.build(idlIx, ixItem); @@ -87,9 +85,9 @@ export default class NamespaceFactory { viewItem, account, idl.types || [], - getCustomResolver && getCustomResolver(idlIx) + getCustomResolver?.(idlIx) ); - const name = camelCase(idlIx.name); + const name = idlIx.name; instruction[name] = ixItem; transaction[name] = txItem; diff --git a/ts/packages/anchor/src/program/namespace/instruction.ts b/ts/packages/anchor/src/program/namespace/instruction.ts index 4d19930ac7..fe0a28d03c 100644 --- a/ts/packages/anchor/src/program/namespace/instruction.ts +++ b/ts/packages/anchor/src/program/namespace/instruction.ts @@ -5,10 +5,10 @@ import { } from "@solana/web3.js"; import { Idl, - IdlAccount, - IdlAccountItem, - IdlAccounts, + IdlInstructionAccountItem, + IdlInstructionAccounts, IdlInstruction, + isCompositeAccounts, } from "../../idl.js"; import { IdlError } from "../../error.js"; import { @@ -41,7 +41,6 @@ export default class InstructionNamespaceFactory { ...args: InstructionContextFnArgs ): TransactionInstruction => { const [ixArgs, ctx] = splitArgsAndCtx(idlIx, [...args]); - const { discriminator } = ctx; validateAccounts(idlIx.accounts, ctx.accounts); validateInstruction(idlIx, ...args); @@ -58,11 +57,7 @@ export default class InstructionNamespaceFactory { return new TransactionInstruction({ keys, programId, - data: encodeFn( - idlIx.name, - toInstruction(idlIx, ...ixArgs), - discriminator - ), + data: encodeFn(idlIx.name, toInstruction(idlIx, ...ixArgs)), }); }; @@ -81,7 +76,7 @@ export default class InstructionNamespaceFactory { public static accountsArray( ctx: Accounts | undefined, - accounts: readonly IdlAccountItem[], + accounts: readonly IdlInstructionAccountItem[], programId: PublicKey, ixName?: string ): AccountMeta[] { @@ -90,42 +85,38 @@ export default class InstructionNamespaceFactory { } return accounts - .map((acc: IdlAccountItem) => { - // Nested accounts. - const nestedAccounts: IdlAccountItem[] | undefined = - "accounts" in acc ? acc.accounts : undefined; - if (nestedAccounts !== undefined) { + .map((acc) => { + if (isCompositeAccounts(acc)) { const rpcAccs = ctx[acc.name] as Accounts; return InstructionNamespaceFactory.accountsArray( rpcAccs, - (acc as IdlAccounts).accounts, + (acc as IdlInstructionAccounts).accounts, programId, ixName ).flat(); - } else { - const account: IdlAccount = acc as IdlAccount; - let pubkey: PublicKey; - try { - pubkey = translateAddress(ctx[acc.name] as Address); - } catch (err) { - throw new Error( - `Wrong input type for account "${ - acc.name - }" in the instruction accounts object${ - ixName !== undefined ? ' for instruction "' + ixName + '"' : "" - }. Expected PublicKey or string.` - ); - } - - const optional = account.isOptional && pubkey.equals(programId); - const isWritable = account.isMut && !optional; - const isSigner = account.isSigner && !optional; - return { - pubkey, - isWritable, - isSigner, - }; } + + let pubkey: PublicKey; + try { + pubkey = translateAddress(ctx[acc.name] as Address); + } catch (err) { + throw new Error( + `Wrong input type for account "${ + acc.name + }" in the instruction accounts object${ + ixName !== undefined ? ' for instruction "' + ixName + '"' : "" + }. Expected PublicKey or string.` + ); + } + + const isOptional = acc.optional && pubkey.equals(programId); + const isWritable = Boolean(acc.writable && !isOptional); + const isSigner = Boolean(acc.signer && !isOptional); + return { + pubkey, + isWritable, + isSigner, + }; }) .flat(); } @@ -162,7 +153,7 @@ export default class InstructionNamespaceFactory { */ export type InstructionNamespace< IDL extends Idl = Idl, - I extends IdlInstruction = IDL["instructions"][number] + I extends IdlInstruction = AllInstructions > = MakeInstructionsNamespace< IDL, I, @@ -196,8 +187,7 @@ type IxProps = { export type InstructionEncodeFn = ( ixName: I["name"], - ix: any, - discriminator?: Buffer + ix: any ) => Buffer; // Throws error if any argument required for the `ix` is not given. diff --git a/ts/packages/anchor/src/program/namespace/methods.ts b/ts/packages/anchor/src/program/namespace/methods.ts index 7bd5c861fe..6e8d6185b0 100644 --- a/ts/packages/anchor/src/program/namespace/methods.ts +++ b/ts/packages/anchor/src/program/namespace/methods.ts @@ -7,7 +7,13 @@ import { TransactionInstruction, TransactionSignature, } from "@solana/web3.js"; -import { Idl, IdlAccountItem, IdlAccounts, IdlTypeDef } from "../../idl.js"; +import { + Idl, + IdlInstructionAccount, + IdlInstructionAccountItem, + IdlInstructionAccounts, + IdlTypeDef, +} from "../../idl.js"; import Provider from "../../provider.js"; import { AccountsGeneric, @@ -66,19 +72,57 @@ export class MethodsBuilderFactory { } } -export type PartialAccounts = - Partial<{ - [N in A["name"]]: PartialAccount; - }>; +type ResolvedAccounts< + A extends IdlInstructionAccountItem = IdlInstructionAccountItem +> = PartialUndefined>; + +type ResolvedAccountsRecursive< + A extends IdlInstructionAccountItem = IdlInstructionAccountItem +> = OmitNever<{ + [N in A["name"]]: ResolvedAccount; +}>; + +type ResolvedAccount< + A extends IdlInstructionAccountItem = IdlInstructionAccountItem +> = A extends IdlInstructionAccounts + ? ResolvedAccountsRecursive + : A extends NonNullable> + ? never + : A extends NonNullable> + ? never + : A extends NonNullable> + ? never + : A extends { signer: true } + ? Address | undefined + : PartialAccount; + +type PartialUndefined< + T, + P extends keyof T = { + [K in keyof T]: undefined extends T[K] ? K : never; + }[keyof T] +> = Partial> & Pick>; + +type OmitNever> = { + [K in keyof T as T[K] extends never ? never : K]: T[K]; +}; + +export type PartialAccounts< + A extends IdlInstructionAccountItem = IdlInstructionAccountItem +> = Partial<{ + [N in A["name"]]: PartialAccount; +}>; -type PartialAccount = A extends IdlAccounts +type PartialAccount< + A extends IdlInstructionAccountItem = IdlInstructionAccountItem +> = A extends IdlInstructionAccounts ? PartialAccounts - : A extends { isOptional: true } + : A extends { optional: true } ? Address | null : Address; export function isPartialAccounts( - partialAccount: PartialAccount + partialAccount: any ): partialAccount is PartialAccounts { return ( typeof partialAccount === "object" && @@ -87,7 +131,7 @@ export function isPartialAccounts( ); } -export function flattenPartialAccounts( +export function flattenPartialAccounts( partialAccounts: PartialAccounts, throwOnNull: boolean ): AccountsGeneric { @@ -108,122 +152,132 @@ export function flattenPartialAccounts( return toReturn; } -type SplInterface = - | "spl_transfer_hook_interface::initialize_extra_account_metas" - | "spl_transfer_hook_interface::execute"; - -export class MethodsBuilder> { - private readonly _accounts: AccountsGeneric = {}; +export class MethodsBuilder< + IDL extends Idl, + I extends AllInstructions, + A extends I["accounts"][number] = I["accounts"][number] +> { + private _accounts: AccountsGeneric = {}; private _remainingAccounts: Array = []; private _signers: Array = []; private _preInstructions: Array = []; private _postInstructions: Array = []; private _accountsResolver: AccountsResolver; - private _autoResolveAccounts: boolean = true; - private _args: Array; - private _discriminator?: Buffer; + private _resolveAccounts: boolean = true; constructor( - _args: Array, + private _args: Array, private _ixFn: InstructionFn, private _txFn: TransactionFn, private _rpcFn: RpcFn, private _simulateFn: SimulateFn, private _viewFn: ViewFn | undefined, - _provider: Provider, - private _programId: PublicKey, - _idlIx: AllInstructions, - _accountNamespace: AccountNamespace, - _idlTypes: IdlTypeDef[], - _customResolver?: CustomAccountResolver + provider: Provider, + programId: PublicKey, + idlIx: AllInstructions, + accountNamespace: AccountNamespace, + idlTypes: IdlTypeDef[], + customResolver?: CustomAccountResolver ) { - this._args = _args; this._accountsResolver = new AccountsResolver( _args, this._accounts, - _provider, - _programId, - _idlIx, - _accountNamespace, - _idlTypes, - _customResolver + provider, + programId, + idlIx, + accountNamespace, + idlTypes, + customResolver ); } - public args(_args: Array): void { - this._args = _args; - this._accountsResolver.args(_args); - } - - public async pubkeys(): Promise< - Partial> - > { - if (this._autoResolveAccounts) { - await this._accountsResolver.resolve(); - } - return this._accounts as unknown as Partial< - InstructionAccountAddresses - >; + public args(args: Array): void { + this._args = args; + this._accountsResolver.args(args); } - public interface(splInterface: SplInterface): MethodsBuilder { - if ( - splInterface === - "spl_transfer_hook_interface::initialize_extra_account_metas" - ) { - this._discriminator = Buffer.from([43, 34, 13, 49, 167, 88, 235, 235]); - } else if (splInterface === "spl_transfer_hook_interface::execute") { - this._discriminator = Buffer.from([105, 37, 101, 197, 75, 251, 102, 26]); - } else { - throw new Error(`Unsupported interface: ${splInterface}`); - } - return this; + /** + * Set instruction accounts with account resolution. + * + * This method only accepts accounts that cannot be resolved. + * + * See {@link accountsPartial} for overriding the account resolution or + * {@link accountsStrict} for strictly specifying all accounts. + */ + public accounts(accounts: ResolvedAccounts) { + // @ts-ignore + return this.accountsPartial(accounts); } - public accounts( - accounts: PartialAccounts - ): MethodsBuilder { - this._autoResolveAccounts = true; + /** + * Set instruction accounts with account resolution. + * + * There is no functional difference between this method and {@link accounts} + * method, the only difference is this method allows specifying all accounts + * even if they can be resolved. On the other hand, {@link accounts} method + * doesn't accept accounts that can be resolved. + */ + public accountsPartial(accounts: PartialAccounts) { + this._resolveAccounts = true; this._accountsResolver.resolveOptionals(accounts); return this; } - public accountsStrict( - accounts: Accounts - ): MethodsBuilder { - this._autoResolveAccounts = false; + /** + * Set instruction accounts without account resolution. + * + * All accounts strictly need to be specified when this method is used. + * + * See {@link accounts} and {@link accountsPartial} methods for automatically + * resolving accounts. + */ + public accountsStrict(accounts: Accounts) { + this._resolveAccounts = false; this._accountsResolver.resolveOptionals(accounts); return this; } - public signers(signers: Array): MethodsBuilder { + public signers(signers: Array) { this._signers = this._signers.concat(signers); return this; } - public remainingAccounts( - accounts: Array - ): MethodsBuilder { + public remainingAccounts(accounts: Array) { this._remainingAccounts = this._remainingAccounts.concat(accounts); return this; } - public preInstructions( - ixs: Array - ): MethodsBuilder { + public preInstructions(ixs: Array) { this._preInstructions = this._preInstructions.concat(ixs); return this; } - public postInstructions( - ixs: Array - ): MethodsBuilder { + public postInstructions(ixs: Array) { this._postInstructions = this._postInstructions.concat(ixs); return this; } + /** + * Get the public keys of the instruction accounts. + * + * The return type is an object with account names as keys and their public + * keys as their values. + * + * Note that an account key is `undefined` if the account hasn't yet been + * specified or resolved. + */ + public async pubkeys(): Promise< + Partial> + > { + if (this._resolveAccounts) { + await this._accountsResolver.resolve(); + } + // @ts-ignore + return this._accounts; + } + public async rpc(options?: ConfirmOptions): Promise { - if (this._autoResolveAccounts) { + if (this._resolveAccounts) { await this._accountsResolver.resolve(); } @@ -234,24 +288,23 @@ export class MethodsBuilder> { remainingAccounts: this._remainingAccounts, preInstructions: this._preInstructions, postInstructions: this._postInstructions, - options: options, - discriminator: this._discriminator, + options, }); } public async rpcAndKeys(options?: ConfirmOptions): Promise<{ - pubkeys: Partial>; + pubkeys: InstructionAccountAddresses; signature: TransactionSignature; }> { const pubkeys = await this.pubkeys(); return { - pubkeys, + pubkeys: pubkeys as Required>, signature: await this.rpc(options), }; } public async view(options?: ConfirmOptions): Promise { - if (this._autoResolveAccounts) { + if (this._resolveAccounts) { await this._accountsResolver.resolve(); } @@ -266,14 +319,14 @@ export class MethodsBuilder> { remainingAccounts: this._remainingAccounts, preInstructions: this._preInstructions, postInstructions: this._postInstructions, - options: options, + options, }); } public async simulate( options?: ConfirmOptions ): Promise> { - if (this._autoResolveAccounts) { + if (this._resolveAccounts) { await this._accountsResolver.resolve(); } @@ -284,12 +337,12 @@ export class MethodsBuilder> { remainingAccounts: this._remainingAccounts, preInstructions: this._preInstructions, postInstructions: this._postInstructions, - options: options, + options, }); } public async instruction(): Promise { - if (this._autoResolveAccounts) { + if (this._resolveAccounts) { await this._accountsResolver.resolve(); } @@ -300,13 +353,15 @@ export class MethodsBuilder> { remainingAccounts: this._remainingAccounts, preInstructions: this._preInstructions, postInstructions: this._postInstructions, - discriminator: this._discriminator, }); } /** - * Convenient shortcut to get instructions and pubkeys via - * const { pubkeys, instructions } = await prepare(); + * Convenient shortcut to get instructions and pubkeys via: + * + * ```ts + * const { pubkeys, instructions } = await method.prepare(); + * ``` */ public async prepare(): Promise<{ pubkeys: Partial>; @@ -316,12 +371,12 @@ export class MethodsBuilder> { return { instruction: await this.instruction(), pubkeys: await this.pubkeys(), - signers: await this._signers, + signers: this._signers, }; } public async transaction(): Promise { - if (this._autoResolveAccounts) { + if (this._resolveAccounts) { await this._accountsResolver.resolve(); } @@ -332,7 +387,6 @@ export class MethodsBuilder> { remainingAccounts: this._remainingAccounts, preInstructions: this._preInstructions, postInstructions: this._postInstructions, - discriminator: this._discriminator, }); } } diff --git a/ts/packages/anchor/src/program/namespace/simulate.ts b/ts/packages/anchor/src/program/namespace/simulate.ts index f73a821511..8735fa26e9 100644 --- a/ts/packages/anchor/src/program/namespace/simulate.ts +++ b/ts/packages/anchor/src/program/namespace/simulate.ts @@ -8,6 +8,7 @@ import { Coder } from "../../coder/index.js"; import { Idl, IdlEvent } from "../../idl.js"; import { translateError } from "../../error.js"; import { + AllEvents, AllInstructions, IdlTypes, InstructionContextFn, @@ -50,11 +51,11 @@ export default class SimulateFactory { throw new Error("Simulated logs not found"); } - const events: Event>[] = []; + const events = []; if (idl.events) { let parser = new EventParser(programId, coder); for (const event of parser.parseLogs(logs)) { - events.push(event); + events.push(event as AllEvents[number]); } } return { events, raw: logs }; @@ -104,13 +105,9 @@ export type SimulateNamespace< > = MakeInstructionsNamespace< IDL, I, - Promise, IdlTypes>> + Promise[number], IdlTypes>> >; -type NullableEvents = IDL["events"] extends undefined - ? IdlEvent - : NonNullable[number]; - /** * SimulateFn is a single method generated from an IDL. It simulates a method * against a cluster configured by the provider, returning a list of all the @@ -123,7 +120,7 @@ export type SimulateFn< > = InstructionContextFn< IDL, I, - Promise, IdlTypes>> + Promise[number], IdlTypes>> >; export type SimulateResponse = { diff --git a/ts/packages/anchor/src/program/namespace/types.ts b/ts/packages/anchor/src/program/namespace/types.ts index 52a21fe520..932e5d0c87 100644 --- a/ts/packages/anchor/src/program/namespace/types.ts +++ b/ts/packages/anchor/src/program/namespace/types.ts @@ -1,19 +1,20 @@ import { PublicKey } from "@solana/web3.js"; import BN from "bn.js"; -import { Idl } from "../../"; import { - IdlAccounts as IdlIdlAccounts, - IdlAccountItem, - IdlEnumFields, - IdlEnumFieldsNamed, - IdlEnumFieldsTuple, + Idl, + IdlInstructionAccounts as IdlInstructionAccounts, + IdlInstructionAccountItem, IdlField, IdlInstruction, IdlType, IdlTypeDef, - IdlTypeDefTyAlias, IdlTypeDefTyEnum, IdlTypeDefTyStruct, + IdlTypeDefTyType, + IdlDefinedFields, + IdlDefinedFieldsNamed, + IdlDefinedFieldsTuple, + IdlArrayLen, } from "../../idl"; import { Accounts, Context } from "../context"; import { MethodsBuilder } from "./methods"; @@ -21,40 +22,43 @@ import { MethodsBuilder } from "./methods"; /** * All instructions for an IDL. */ -export type AllInstructions = IDL["instructions"][number]; +export type AllInstructions = I["instructions"][number]; /** * Returns a type of instruction name to the IdlInstruction. */ -export type InstructionMap = { +type InstructionMap = { [K in I["name"]]: I & { name: K }; }; /** * Returns a type of instruction name to the IdlInstruction. */ -export type AllInstructionsMap = InstructionMap< - AllInstructions +export type AllInstructionsMap = InstructionMap< + AllInstructions >; /** * All accounts for an IDL. */ -export type AllAccounts = IDL["accounts"] extends undefined - ? IdlTypeDef - : NonNullable[number]; +export type AllAccounts = ResolveIdlTypePointer; /** * Returns a type of instruction name to the IdlInstruction. */ -export type AccountMap = { - [K in I["name"]]: I & { name: K }; +type AccountMap = { + [K in I[number]["name"]]: I & { name: K }; }; /** * Returns a type of instruction name to the IdlInstruction. */ -export type AllAccountsMap = AccountMap>; +export type AllAccountsMap = AccountMap>; + +/** + * All events for an IDL. + */ +export type AllEvents = ResolveIdlTypePointer; export type MakeInstructionsNamespace< IDL extends Idl, @@ -99,12 +103,14 @@ export type InstructionAccountAddresses< I extends AllInstructions > = InstructionAccountsAddresses; -type InstructionAccountsAddresses = { +type InstructionAccountsAddresses< + A extends IdlInstructionAccountItem = IdlInstructionAccountItem +> = { [N in A["name"]]: InstructionAccountsAddress; }; -type InstructionAccountsAddress = - A extends IdlIdlAccounts +type InstructionAccountsAddress = + A extends IdlInstructionAccounts ? InstructionAccountsAddresses : PublicKey; @@ -115,7 +121,7 @@ export type MethodsFn< > = (...args: ArgsTuple>) => Ret; type TypeMap = { - publicKey: PublicKey; + pubkey: PublicKey; bool: boolean; string: string; bytes: Buffer; @@ -129,15 +135,15 @@ export type DecodeType = IdlType extends T ? unknown : T extends keyof TypeMap ? TypeMap[T] - : T extends { defined: keyof Defined } - ? Defined[T["defined"]] + : T extends { defined: { name: keyof Defined } } + ? Defined[T["defined"]["name"]] : T extends { option: IdlType } ? DecodeType | null : T extends { coption: IdlType } ? DecodeType | null : T extends { vec: IdlType } ? DecodeType[] - : T extends { array: [defined: IdlType, size: number] } + : T extends { array: [defined: IdlType, size: IdlArrayLen] } ? DecodeType[] : unknown; @@ -160,33 +166,23 @@ type UnboxToUnion = T extends (infer U)[] ? UnboxToUnion : T; -type SnakeToCamelCase = S extends `${infer T}_${infer U}` - ? `${T}${Capitalize>}` - : S; - -/** - * decode single enum.field - */ -declare type DecodeEnumField = F extends IdlType +type DecodeDefinedField = F extends IdlType ? DecodeType : never; /** * decode enum variant: named or tuple */ -declare type DecodeEnumFields< - F extends IdlEnumFields, +type DecodeDefinedFields< + F extends IdlDefinedFields, Defined -> = F extends IdlEnumFieldsNamed +> = F extends IdlDefinedFieldsNamed ? { - [F2 in F[number] as SnakeToCamelCase]: DecodeEnumField< - F2["type"], - Defined - >; + [F2 in F[number] as F2["name"]]: DecodeDefinedField; } - : F extends IdlEnumFieldsTuple + : F extends IdlDefinedFieldsTuple ? { - [F3 in keyof F as Exclude]: DecodeEnumField< + [F3 in keyof F as Exclude]: DecodeDefinedField< F[F3], Defined >; @@ -194,7 +190,7 @@ declare type DecodeEnumFields< : Record; type DecodeEnumVariants = { - [V in I["variants"][number] as Uncapitalize]: DecodeEnumFields< + [V in I["variants"][number] as V["name"]]: DecodeDefinedFields< NonNullable, Defined >; @@ -211,12 +207,13 @@ type DecodeEnum = XorEnumVariants< DecodeEnumVariants >; -type DecodeStruct = { - [F in I["fields"][number] as F["name"]]: DecodeType; -}; +type DecodeStruct = DecodeDefinedFields< + NonNullable, + Defined +>; -type DecodeAlias = DecodeType< - I["value"], +type DecodeAlias = DecodeType< + I["alias"], Defined >; @@ -227,7 +224,7 @@ export type TypeDef< ? DecodeEnum : I["type"] extends IdlTypeDefTyStruct ? DecodeStruct - : I["type"] extends IdlTypeDefTyAlias + : I["type"] extends IdlTypeDefTyType ? DecodeAlias : never; @@ -241,8 +238,9 @@ type DecodedHelper = { type UnknownType = "__unknown_defined_type__"; /** - * empty "defined" object to produce UnknownType instead of never/unknown during idl types decoding - * */ + * Empty "defined" object to produce `UnknownType` instead of never/unknown + * during IDL types decoding. + */ type EmptyDefined = Record; type RecursiveDepth2< @@ -267,8 +265,8 @@ type RecursiveDepth4< > = DecodedHelper; /** - * typescript can't handle truly recursive type (RecursiveTypes instead of RecursiveDepth2). - * Hence we're doing "recursion" of depth=4 manually + * TypeScript can't handle truly recursive type (RecursiveTypes instead of RecursiveDepth2). + * Hence we're doing recursion of depth=4 manually * */ type RecursiveTypes< T extends IdlTypeDef[], @@ -280,27 +278,35 @@ type RecursiveTypes< ? RecursiveDepth2> : Decoded; -export type IdlTypes = RecursiveTypes>; +export type IdlTypes = RecursiveTypes>; -type IdlEventType< - I extends Idl, - Event extends NonNullable[number], - Defined -> = { - [F in Event["fields"][number] as F["name"]]: DecodeType; -}; +export type IdlErrors = NonNullable[number]; -export type IdlEvents> = { - [E in NonNullable[number] as E["name"]]: IdlEventType< - I, - E, - Defined - >; -}; +export type IdlAccounts = ResolveIdlPointerSection< + I, + "accounts" +>; + +export type IdlEvents = ResolveIdlPointerSection; + +type IdlPointerSection = keyof Pick; -export type IdlAccounts = TypeDefDictionary< - NonNullable, - IdlTypes +type ResolveIdlPointerSection< + I extends Idl, + K extends IdlPointerSection, + T extends ResolveIdlTypePointer = ResolveIdlTypePointer +> = TypeDefDictionary>; + +type ResolveIdlTypePointer< + I extends Idl, + Key extends IdlPointerSection +> = FilterTuple< + NonNullable, + { name: NonNullable[number]["name"] } >; -export type IdlErrorInfo = NonNullable[number]; +type FilterTuple = T extends [infer Head, ...infer Tail] + ? [Head] extends [F] + ? [Head, ...FilterTuple] + : FilterTuple + : []; diff --git a/ts/packages/anchor/src/program/namespace/views.ts b/ts/packages/anchor/src/program/namespace/views.ts index da8ffb42a2..d39cc0b44b 100644 --- a/ts/packages/anchor/src/program/namespace/views.ts +++ b/ts/packages/anchor/src/program/namespace/views.ts @@ -1,5 +1,5 @@ import { PublicKey } from "@solana/web3.js"; -import { Idl, IdlAccount } from "../../idl.js"; +import { Idl, IdlInstructionAccount } from "../../idl.js"; import { SimulateFn } from "./simulate.js"; import { AllInstructions, @@ -16,9 +16,11 @@ export default class ViewFactory { simulateFn: SimulateFn, idl: IDL ): ViewFn | undefined { - const isMut = idlIx.accounts.find((a: IdlAccount) => a.isMut); + const isWritable = idlIx.accounts.find( + (a: IdlInstructionAccount) => a.writable + ); const hasReturn = !!idlIx.returns; - if (isMut || !hasReturn) return; + if (isWritable || !hasReturn) return; const view: ViewFn = async (...args) => { let simulationResult = await simulateFn(...args); @@ -29,15 +31,14 @@ export default class ViewFactory { if (!returnLog) { throw new Error("View expected return log"); } + let returnData = decode(returnLog.slice(returnPrefix.length)); let returnType = idlIx.returns; if (!returnType) { throw new Error("View expected return type"); } - const coder = IdlCoder.fieldLayout( - { type: returnType }, - Array.from([...(idl.accounts ?? []), ...(idl.types ?? [])]) - ); + + const coder = IdlCoder.fieldLayout({ type: returnType }, idl.types); return coder.decode(returnData); }; return view; diff --git a/ts/packages/anchor/src/workspace.ts b/ts/packages/anchor/src/workspace.ts index fe890ce655..a0dd46a4cb 100644 --- a/ts/packages/anchor/src/workspace.ts +++ b/ts/packages/anchor/src/workspace.ts @@ -2,6 +2,7 @@ import * as toml from "toml"; import { snakeCase } from "snake-case"; import { Program } from "./program/index.js"; import { isBrowser } from "./utils/common.js"; +import { Idl } from "./idl.js"; /** * The `workspace` namespace provides a convenience API to automatically @@ -66,15 +67,14 @@ const workspace = new Proxy( ); } - const idl = JSON.parse(fs.readFileSync(idlPath)); + const idl: Idl = JSON.parse(fs.readFileSync(idlPath)); if (!programId) { - if (!idl.metadata?.address) { + if (!idl.address) { throw new Error( - `IDL for program \`${programName}\` does not have \`metadata.address\` field.\n` + - "To add the missing field, run `anchor deploy` or `anchor test`." + `IDL for program \`${programName}\` does not have \`address\` field.` ); } - programId = idl.metadata.address; + programId = idl.address; } workspaceCache[programName] = new Program(idl, programId); diff --git a/ts/packages/anchor/tests/coder-accounts.spec.ts b/ts/packages/anchor/tests/coder-accounts.spec.ts index c3da53324a..7432515b28 100644 --- a/ts/packages/anchor/tests/coder-accounts.spec.ts +++ b/ts/packages/anchor/tests/coder-accounts.spec.ts @@ -1,29 +1,40 @@ import * as assert from "assert"; -import { BorshCoder } from "../src"; +import { BorshCoder, Idl } from "../src"; import { DISCRIMINATOR_SIZE } from "../src/coder/borsh/discriminator"; import { sha256 } from "@noble/hashes/sha256"; describe("coder.accounts", () => { test("Can encode and decode user-defined accounts, including those with consecutive capital letters", () => { - const idl = { - version: "0.0.0", - name: "basic_0", + const idl: Idl = { + address: "Test111111111111111111111111111111111111111", + metadata: { + name: "basic_0", + version: "0.0.0", + spec: "0.1.0", + }, instructions: [ { name: "initialize", + discriminator: [], accounts: [], args: [], }, ], accounts: [ + { + name: "MemberDAO", + discriminator: [0, 1, 2, 3, 4, 5, 6, 7], + }, + ], + types: [ { name: "MemberDAO", type: { - kind: "struct" as const, + kind: "struct", fields: [ { name: "name", - type: "string" as const, + type: "string", }, ], }, @@ -37,11 +48,6 @@ describe("coder.accounts", () => { }; coder.accounts.encode("MemberDAO", memberDAO).then((encoded) => { - // start of encoded account = account discriminator - assert.deepEqual( - encoded.subarray(0, DISCRIMINATOR_SIZE), - Buffer.from(sha256("account:MemberDAO").slice(0, DISCRIMINATOR_SIZE)) - ); assert.deepEqual(coder.accounts.decode("MemberDAO", encoded), memberDAO); }); }); diff --git a/ts/packages/anchor/tests/coder-instructions.spec.ts b/ts/packages/anchor/tests/coder-instructions.spec.ts index a49bd13071..a87519c3c5 100644 --- a/ts/packages/anchor/tests/coder-instructions.spec.ts +++ b/ts/packages/anchor/tests/coder-instructions.spec.ts @@ -1,22 +1,29 @@ import * as assert from "assert"; import { BorshCoder } from "../src"; -import { IdlType } from "../src/idl"; +import { Idl, IdlType } from "../src/idl"; import { toInstruction } from "../src/program/common"; describe("coder.instructions", () => { test("Can encode and decode type aliased instruction arguments (byte array)", () => { - const idl = { - version: "0.1.0", - name: "test", + const idl: Idl = { + address: "Test111111111111111111111111111111111111111", + metadata: { + name: "test", + version: "0.0.0", + spec: "0.1.0", + }, instructions: [ { name: "initialize", + discriminator: [0, 1, 2, 3, 4, 5, 6, 7], accounts: [], args: [ { name: "arg", type: { - defined: "AliasTest", + defined: { + name: "AliasTest", + }, }, }, ], @@ -26,8 +33,8 @@ describe("coder.instructions", () => { { name: "AliasTest", type: { - kind: "alias" as const, - value: { + kind: "type", + alias: { array: ["u8", 3] as [IdlType, number], }, }, @@ -42,7 +49,7 @@ describe("coder.instructions", () => { const ix = toInstruction(idlIx, expected); const encoded = coder.instruction.encode(idlIx.name, ix); - const decoded = coder.instruction.decode(encoded, "hex", idlIx.name); + const decoded = coder.instruction.decode(encoded); assert.deepStrictEqual(decoded?.data[idlIx.args[0].name], expected); }); diff --git a/ts/packages/anchor/tests/coder-types.spec.ts b/ts/packages/anchor/tests/coder-types.spec.ts index 533ac24b8c..9c36b3ca9a 100644 --- a/ts/packages/anchor/tests/coder-types.spec.ts +++ b/ts/packages/anchor/tests/coder-types.spec.ts @@ -1,32 +1,37 @@ import * as assert from "assert"; -import { BorshCoder } from "../src"; +import { BorshCoder, Idl } from "../src"; import BN from "bn.js"; describe("coder.types", () => { test("Can encode and decode user-defined types", () => { - const idl = { - version: "0.0.0", - name: "basic_0", + const idl: Idl = { + address: "Test111111111111111111111111111111111111111", + metadata: { + name: "basic_0", + version: "0.0.0", + spec: "0.1.0", + }, instructions: [ { name: "initialize", accounts: [], args: [], + discriminator: [], }, ], types: [ { name: "MintInfo", type: { - kind: "struct" as const, + kind: "struct", fields: [ { name: "minted", - type: "bool" as const, + type: "bool", }, { name: "metadataUrl", - type: "string" as const, + type: "string", }, ], }, @@ -45,29 +50,34 @@ describe("coder.types", () => { }); test("Can encode and decode 256-bit integers", () => { - const idl = { - version: "0.0.0", - name: "basic_0", + const idl: Idl = { + address: "Test111111111111111111111111111111111111111", + metadata: { + name: "basic_0", + version: "0.0.0", + spec: "0.1.0", + }, instructions: [ { name: "initialize", accounts: [], args: [], + discriminator: [], }, ], types: [ { name: "IntegerTest", type: { - kind: "struct" as const, + kind: "struct", fields: [ { name: "unsigned", - type: "u256" as const, + type: "u256", }, { name: "signed", - type: "i256" as const, + type: "i256", }, ], }, diff --git a/ts/packages/anchor/tests/events.spec.ts b/ts/packages/anchor/tests/events.spec.ts index f02fc00ad4..09441b1b75 100644 --- a/ts/packages/anchor/tests/events.spec.ts +++ b/ts/packages/anchor/tests/events.spec.ts @@ -1,6 +1,6 @@ import { PublicKey } from "@solana/web3.js"; import { EventParser } from "../src/program/event"; -import { BorshCoder } from "../src"; +import { BorshCoder, Idl } from "../src"; describe("Events", () => { it("Parses multiple instructions", () => { @@ -11,14 +11,19 @@ describe("Events", () => { "Program J2XMGdW2qQLx7rAdwWtSZpTXDgAQ988BLP9QTgUZvm54 consumed 17867 of 200000 compute units", "Program J2XMGdW2qQLx7rAdwWtSZpTXDgAQ988BLP9QTgUZvm54 success", ]; - const idl = { - version: "0.0.0", - name: "basic_0", + const idl: Idl = { + address: "Test111111111111111111111111111111111111111", + metadata: { + name: "basic_0", + version: "0.0.0", + spec: "0.1.0", + }, instructions: [ { name: "initialize", accounts: [], args: [], + discriminator: [], }, ], }; @@ -39,14 +44,19 @@ describe("Events", () => { "Program J2XMGdW2qQLx7rAdwWtSZpTXDgAQ988BLP9QTgUZvm54 consumed 17867 of 200000 compute units", "Program J2XMGdW2qQLx7rAdwWtSZpTXDgAQ988BLP9QTgUZvm54 success", ]; - const idl = { - version: "0.0.0", - name: "basic_0", + const idl: Idl = { + address: "Test111111111111111111111111111111111111111", + metadata: { + name: "basic_0", + version: "0.0.0", + spec: "0.1.0", + }, instructions: [ { name: "initialize", accounts: [], args: [], + discriminator: [], }, ], }; @@ -83,31 +93,43 @@ describe("Events", () => { "Program J2XMGdW2qQLx7rAdwWtSZpTXDgAQ988BLP9QTgUZvm54 success", ]; - const idl = { - version: "0.0.0", - name: "basic_1", + const idl: Idl = { + address: "Test111111111111111111111111111111111111111", + metadata: { + name: "basic_1", + version: "0.0.0", + spec: "0.1.0", + }, instructions: [ { name: "initialize", accounts: [], args: [], + discriminator: [], }, ], events: [ { name: "NftSold", - fields: [ - { - name: "nftMintAddress", - type: "publicKey" as "publicKey", - index: false, - }, - { - name: "accountAddress", - type: "publicKey" as "publicKey", - index: false, - }, - ], + discriminator: [82, 21, 49, 86, 87, 54, 132, 103], + }, + ], + types: [ + { + name: "NftSold", + type: { + kind: "struct", + fields: [ + { + name: "nftMintAddress", + type: "pubkey", + }, + { + name: "accountAddress", + type: "pubkey", + }, + ], + }, }, ], }; @@ -116,6 +138,7 @@ describe("Events", () => { const programId = new PublicKey( "J2XMGdW2qQLx7rAdwWtSZpTXDgAQ988BLP9QTgUZvm54" ); + const eventParser = new EventParser(programId, coder); const gen = eventParser.parseLogs(logs); @@ -143,42 +166,41 @@ describe("Events", () => { "Program J2XMGdW2qQLx7rAdwWtSZpTXDgAQ988BLP9QTgUZvm54 success", ]; - const idl = { - version: "0.0.0", - name: "basic_2", + const idl: Idl = { + address: "Test111111111111111111111111111111111111111", + metadata: { + name: "basic_2", + version: "0.0.0", + spec: "0.1.0", + }, instructions: [ { name: "cancelListing", + discriminator: [], accounts: [ { name: "globalState", - isMut: true, - isSigner: false, + writable: true, }, { name: "nftHolderAccount", - isMut: true, - isSigner: false, + writable: true, }, { name: "listingAccount", - isMut: true, - isSigner: false, + writable: true, }, { name: "nftAssociatedAccount", - isMut: true, - isSigner: false, + writable: true, }, { name: "signer", - isMut: true, - isSigner: true, + writable: true, + signer: true, }, { name: "tokenProgram", - isMut: false, - isSigner: false, }, ], args: [], @@ -187,23 +209,29 @@ describe("Events", () => { events: [ { name: "ListingClosed", - fields: [ - { - name: "initializer", - type: "publicKey" as "publicKey", - index: false, - }, - { - name: "nftMintAddress", - type: "publicKey" as "publicKey", - index: false, - }, - { - name: "accountAddress", - type: "publicKey" as "publicKey", - index: false, - }, - ], + discriminator: [86, 219, 253, 196, 184, 194, 176, 78], + }, + ], + types: [ + { + name: "ListingClosed", + type: { + kind: "struct", + fields: [ + { + name: "initializer", + type: "pubkey", + }, + { + name: "nftMintAddress", + type: "pubkey", + }, + { + name: "accountAddress", + type: "pubkey", + }, + ], + }, }, ], }; @@ -239,30 +267,40 @@ describe("Events", () => { "Program 5VcVB7jEjdWJBkriXxayCrUUkwfhrPK3rXtnkxxUvMFP success", ]; - const idl = { - version: "0.0.0", - name: "basic_2", + const idl: Idl = { + address: "Test111111111111111111111111111111111111111", + metadata: { + name: "basic_2", + version: "0.0.0", + spec: "0.1.0", + }, instructions: [], events: [ { name: "ListingClosed", - fields: [ - { - name: "initializer", - type: "publicKey" as "publicKey", - index: false, - }, - { - name: "nftMintAddress", - type: "publicKey" as "publicKey", - index: false, - }, - { - name: "accountAddress", - type: "publicKey" as "publicKey", - index: false, - }, - ], + discriminator: [], + }, + ], + types: [ + { + name: "ListingClosed", + type: { + kind: "struct", + fields: [ + { + name: "initializer", + type: "pubkey", + }, + { + name: "nftMintAddress", + type: "pubkey", + }, + { + name: "accountAddress", + type: "pubkey", + }, + ], + }, }, ], }; diff --git a/ts/packages/anchor/tests/transaction.spec.ts b/ts/packages/anchor/tests/transaction.spec.ts index 4a3e69aea1..afc2cf5218 100644 --- a/ts/packages/anchor/tests/transaction.spec.ts +++ b/ts/packages/anchor/tests/transaction.spec.ts @@ -1,6 +1,6 @@ import TransactionFactory from "../src/program/namespace/transaction"; import InstructionFactory from "../src/program/namespace/instruction"; -import { BorshCoder } from "../src"; +import { BorshCoder, Idl } from "../src"; import { PublicKey, TransactionInstruction } from "@solana/web3.js"; describe("Transaction", () => { @@ -14,14 +14,19 @@ describe("Transaction", () => { programId: PublicKey.default, data: Buffer.from("post"), }); - const idl = { - version: "0.0.0", - name: "basic_0", + const idl: Idl = { + address: "Test111111111111111111111111111111111111111", + metadata: { + name: "basic_0", + version: "0.0.0", + spec: "0.1.0", + }, instructions: [ { name: "initialize", accounts: [], args: [], + discriminator: [], }, ], }; diff --git a/ts/packages/spl-associated-token-account/src/coder/accounts.ts b/ts/packages/spl-associated-token-account/src/coder/accounts.ts index 40a0ea8563..aa0fe2a8ff 100644 --- a/ts/packages/spl-associated-token-account/src/coder/accounts.ts +++ b/ts/packages/spl-associated-token-account/src/coder/accounts.ts @@ -39,10 +39,10 @@ export class SplAssociatedTokenAccountAccountsCoder } } - public size(idlAccount: IdlTypeDef): number { - switch (idlAccount.name) { + public size(accountName: A): number { + switch (accountName) { default: { - throw new Error(`Invalid account name: ${idlAccount.name}`); + throw new Error(`Invalid account name: ${accountName}`); } } } diff --git a/ts/packages/spl-associated-token-account/src/program.ts b/ts/packages/spl-associated-token-account/src/program.ts index feaec79b18..b54e0a1d0b 100644 --- a/ts/packages/spl-associated-token-account/src/program.ts +++ b/ts/packages/spl-associated-token-account/src/program.ts @@ -24,118 +24,102 @@ export function splAssociatedTokenAccountProgram( } type SplAssociatedTokenAccount = { - version: "1.1.1"; - name: "spl_associated_token_account"; + address: "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"; + metadata: { + name: "splAssociatedTokenAccount"; + version: "1.1.1"; + spec: "0.1.0"; + }; instructions: [ { name: "create"; + discriminator: [0]; accounts: [ { name: "fundingAddress"; - isMut: true; - isSigner: true; + writable: true; + signer: true; }, { name: "associatedAccountAddress"; - isMut: true; - isSigner: false; + writable: true; }, { name: "walletAddress"; - isMut: false; - isSigner: false; }, { name: "tokenMintAddress"; - isMut: false; - isSigner: false; }, { name: "systemProgram"; - isMut: false; - isSigner: false; + address: "11111111111111111111111111111111"; }, { name: "tokenProgram"; - isMut: false; - isSigner: false; + address: "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"; } ]; args: []; }, { name: "createIdempotent"; + discriminator: [1]; accounts: [ { name: "fundingAddress"; - isMut: true; - isSigner: true; + writable: true; + signer: true; }, { name: "associatedAccountAddress"; - isMut: true; - isSigner: false; + writable: true; }, { name: "walletAddress"; - isMut: false; - isSigner: false; }, { name: "tokenMintAddress"; - isMut: false; - isSigner: false; }, { name: "systemProgram"; - isMut: false; - isSigner: false; + address: "11111111111111111111111111111111"; }, { name: "tokenProgram"; - isMut: false; - isSigner: false; + address: "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"; } ]; args: []; }, { name: "recoverNested"; + discriminator: [2]; accounts: [ { name: "nestedAssociatedAccountAddress"; - isMut: true; - isSigner: false; + writable: true; }, { name: "nestedTokenMintAddress"; - isMut: false; - isSigner: false; }, { name: "destinationAssociatedAccountAddress"; - isMut: true; - isSigner: false; + writable: true; }, { name: "ownerAssociatedAccountAddress"; - isMut: false; - isSigner: false; }, { name: "ownerTokenMintAddress"; - isMut: false; - isSigner: false; }, { name: "walletAddress"; - isMut: true; - isSigner: true; + writable: true; + signer: true; }, { name: "tokenProgram"; - isMut: false; - isSigner: false; + address: "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"; } ]; args: []; @@ -144,125 +128,109 @@ type SplAssociatedTokenAccount = { errors: [ { code: 0; - name: "InvalidOwner"; + name: "invalidOwner"; msg: "Associated token account owner does not match address derivation"; } ]; }; const IDL: SplAssociatedTokenAccount = { - version: "1.1.1", - name: "spl_associated_token_account", + address: "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL", + metadata: { + name: "splAssociatedTokenAccount", + version: "1.1.1", + spec: "0.1.0", + }, instructions: [ { name: "create", + discriminator: [0], accounts: [ { name: "fundingAddress", - isMut: true, - isSigner: true, + writable: true, + signer: true, }, { name: "associatedAccountAddress", - isMut: true, - isSigner: false, + writable: true, }, { name: "walletAddress", - isMut: false, - isSigner: false, }, { name: "tokenMintAddress", - isMut: false, - isSigner: false, }, { name: "systemProgram", - isMut: false, - isSigner: false, + address: "11111111111111111111111111111111", }, { name: "tokenProgram", - isMut: false, - isSigner: false, + address: "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", }, ], args: [], }, { name: "createIdempotent", + discriminator: [1], accounts: [ { name: "fundingAddress", - isMut: true, - isSigner: true, + writable: true, + signer: true, }, { name: "associatedAccountAddress", - isMut: true, - isSigner: false, + writable: true, }, { name: "walletAddress", - isMut: false, - isSigner: false, }, { name: "tokenMintAddress", - isMut: false, - isSigner: false, }, { name: "systemProgram", - isMut: false, - isSigner: false, + address: "11111111111111111111111111111111", }, { name: "tokenProgram", - isMut: false, - isSigner: false, + address: "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", }, ], args: [], }, { name: "recoverNested", + discriminator: [2], accounts: [ { name: "nestedAssociatedAccountAddress", - isMut: true, - isSigner: false, + writable: true, }, { name: "nestedTokenMintAddress", - isMut: false, - isSigner: false, }, { name: "destinationAssociatedAccountAddress", - isMut: true, - isSigner: false, + writable: true, }, { name: "ownerAssociatedAccountAddress", - isMut: false, - isSigner: false, }, { name: "ownerTokenMintAddress", - isMut: false, - isSigner: false, }, { name: "walletAddress", - isMut: true, - isSigner: true, + writable: true, + signer: true, }, { name: "tokenProgram", - isMut: false, - isSigner: false, + address: "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", }, ], args: [], @@ -271,7 +239,7 @@ const IDL: SplAssociatedTokenAccount = { errors: [ { code: 0, - name: "InvalidOwner", + name: "invalidOwner", msg: "Associated token account owner does not match address derivation", }, ], diff --git a/ts/packages/spl-token/src/coder/accounts.ts b/ts/packages/spl-token/src/coder/accounts.ts index 176879c77f..5e0d1fcd29 100644 --- a/ts/packages/spl-token/src/coder/accounts.ts +++ b/ts/packages/spl-token/src/coder/accounts.ts @@ -78,8 +78,8 @@ export class SplTokenAccountsCoder } } - public size(idlAccount: IdlTypeDef): number { - switch (idlAccount.name) { + public size(accountName: A): number { + switch (accountName) { case "mint": { return 82; } @@ -90,7 +90,7 @@ export class SplTokenAccountsCoder return 355; } default: { - throw new Error(`Invalid account name: ${idlAccount.name}`); + throw new Error(`Invalid account name: ${accountName}`); } } } diff --git a/ts/packages/spl-token/src/program.ts b/ts/packages/spl-token/src/program.ts index 18fdf16999..9107bbb9c0 100644 --- a/ts/packages/spl-token/src/program.ts +++ b/ts/packages/spl-token/src/program.ts @@ -22,104 +22,96 @@ export function splTokenProgram(params?: GetProgramParams): Program { } type SplToken = { - version: "3.3.0"; - name: "spl_token"; + address: "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"; + metadata: { + name: "splToken"; + version: "3.3.0"; + spec: "0.1.0"; + }; instructions: [ { - name: "initializeMint"; + name: "amountToUiAmount"; + discriminator: [23]; accounts: [ { name: "mint"; - isMut: true; - isSigner: false; - }, - { - name: "rent"; - isMut: false; - isSigner: false; } ]; args: [ { - name: "decimals"; - type: "u8"; - }, - { - name: "mintAuthority"; - type: "publicKey"; - }, - { - name: "freezeAuthority"; - type: { - defined: "COption"; - }; + name: "amount"; + type: "u64"; } ]; }, { - name: "initializeAccount"; + name: "approve"; + discriminator: [4]; accounts: [ { - name: "account"; - isMut: true; - isSigner: false; + name: "source"; + writable: true; }, { - name: "mint"; - isMut: false; - isSigner: false; + name: "delegate"; }, { name: "owner"; - isMut: false; - isSigner: false; - }, + signer: true; + } + ]; + args: [ { - name: "rent"; - isMut: false; - isSigner: false; + name: "amount"; + type: "u64"; } ]; - args: []; }, { - name: "initializeMultisig"; + name: "approveChecked"; + discriminator: [13]; accounts: [ { - name: "multisig"; - isMut: true; - isSigner: false; + name: "source"; + writable: true; }, { - name: "rent"; - isMut: false; - isSigner: false; + name: "mint"; + }, + { + name: "delegate"; + }, + { + name: "owner"; + signer: true; } ]; args: [ { - name: "m"; + name: "amount"; + type: "u64"; + }, + { + name: "decimals"; type: "u8"; } ]; }, { - name: "transfer"; + name: "burn"; + discriminator: [8]; accounts: [ { - name: "source"; - isMut: true; - isSigner: false; + name: "account"; + writable: true; }, { - name: "destination"; - isMut: true; - isSigner: false; + name: "mint"; + writable: true; }, { name: "authority"; - isMut: false; - isSigner: true; + signer: true; } ]; args: [ @@ -130,313 +122,288 @@ type SplToken = { ]; }, { - name: "approve"; + name: "burnChecked"; + discriminator: [15]; accounts: [ { - name: "source"; - isMut: true; - isSigner: false; + name: "account"; + writable: true; }, { - name: "delegate"; - isMut: false; - isSigner: false; + name: "mint"; + writable: true; }, { - name: "owner"; - isMut: false; - isSigner: true; + name: "authority"; + signer: true; } ]; args: [ { name: "amount"; type: "u64"; + }, + { + name: "decimals"; + type: "u8"; } ]; }, { - name: "revoke"; + name: "closeAccount"; + discriminator: [9]; accounts: [ { - name: "source"; - isMut: true; - isSigner: false; + name: "account"; + writable: true; + }, + { + name: "destination"; + writable: true; }, { name: "owner"; - isMut: false; - isSigner: true; + signer: true; } ]; args: []; }, { - name: "setAuthority"; + name: "freezeAccount"; + discriminator: [10]; accounts: [ { - name: "owned"; - isMut: true; - isSigner: false; + name: "account"; + writable: true; }, { - name: "owner"; - isMut: false; - isSigner: true; + name: "mint"; }, { - name: "signer"; - isMut: false; - isSigner: true; + name: "owner"; + signer: true; } ]; - args: [ - { - name: "authorityType"; - type: { - defined: "AuthorityType"; - }; - }, + args: []; + }, + { + name: "getAccountDataSize"; + discriminator: [21]; + accounts: [ { - name: "newAuthority"; - type: { - defined: "COption"; - }; + name: "mint"; } ]; + args: []; }, { - name: "mintTo"; + name: "initializeAccount"; + discriminator: [1]; accounts: [ { - name: "mint"; - isMut: true; - isSigner: false; + name: "account"; + writable: true; }, { - name: "account"; - isMut: true; - isSigner: false; + name: "mint"; }, { name: "owner"; - isMut: false; - isSigner: true; - } - ]; - args: [ + }, { - name: "amount"; - type: "u64"; + name: "rent"; + address: "SysvarRent111111111111111111111111111111111"; } ]; + args: []; }, { - name: "burn"; + name: "initializeAccount2"; + discriminator: [16]; accounts: [ { name: "account"; - isMut: true; - isSigner: false; + writable: true; }, { name: "mint"; - isMut: true; - isSigner: false; }, { - name: "authority"; - isMut: false; - isSigner: true; + name: "rent"; + address: "SysvarRent111111111111111111111111111111111"; } ]; args: [ { - name: "amount"; - type: "u64"; + name: "owner"; + type: "pubkey"; } ]; }, { - name: "closeAccount"; + name: "initializeAccount3"; + discriminator: [18]; accounts: [ { name: "account"; - isMut: true; - isSigner: false; + writable: true; }, { - name: "destination"; - isMut: true; - isSigner: false; - }, + name: "mint"; + } + ]; + args: [ { name: "owner"; - isMut: false; - isSigner: true; + type: "pubkey"; } ]; - args: []; }, { - name: "freezeAccount"; + name: "initializeImmutableOwner"; + discriminator: [22]; accounts: [ { name: "account"; - isMut: true; - isSigner: false; - }, - { - name: "mint"; - isMut: false; - isSigner: false; - }, - { - name: "owner"; - isMut: false; - isSigner: true; + writable: true; } ]; args: []; }, { - name: "thawAccount"; + name: "initializeMint"; + discriminator: [0]; accounts: [ { - name: "account"; - isMut: true; - isSigner: false; + name: "mint"; + writable: true; }, { - name: "mint"; - isMut: false; - isSigner: false; + name: "rent"; + address: "SysvarRent111111111111111111111111111111111"; + } + ]; + args: [ + { + name: "decimals"; + type: "u8"; }, { - name: "owner"; - isMut: false; - isSigner: true; + name: "mintAuthority"; + type: "pubkey"; + }, + { + name: "freezeAuthority"; + type: { + coption: "pubkey"; + }; } ]; - args: []; }, { - name: "transferChecked"; + name: "initializeMint2"; + discriminator: [20]; accounts: [ - { - name: "source"; - isMut: true; - isSigner: false; - }, { name: "mint"; - isMut: false; - isSigner: false; + writable: true; + } + ]; + args: [ + { + name: "decimals"; + type: "u8"; }, { - name: "destination"; - isMut: true; - isSigner: false; + name: "mintAuthority"; + type: "pubkey"; }, { - name: "authority"; - isMut: false; - isSigner: true; + name: "freezeAuthority"; + type: { + coption: "pubkey"; + }; } ]; - args: [ + }, + { + name: "initializeMultisig"; + discriminator: [2]; + accounts: [ { - name: "amount"; - type: "u64"; + name: "multisig"; + writable: true; }, { - name: "decimals"; + name: "rent"; + address: "SysvarRent111111111111111111111111111111111"; + } + ]; + args: [ + { + name: "m"; type: "u8"; } ]; }, { - name: "approveChecked"; + name: "initializeMultisig2"; + discriminator: [19]; accounts: [ { - name: "source"; - isMut: true; - isSigner: false; - }, - { - name: "mint"; - isMut: false; - isSigner: false; - }, - { - name: "delegate"; - isMut: false; - isSigner: false; + name: "multisig"; + writable: true; }, { - name: "owner"; - isMut: false; - isSigner: true; + name: "signer"; } ]; args: [ { - name: "amount"; - type: "u64"; - }, - { - name: "decimals"; + name: "m"; type: "u8"; } ]; }, { - name: "mintToChecked"; + name: "mintTo"; + discriminator: [7]; accounts: [ { name: "mint"; - isMut: true; - isSigner: false; + writable: true; }, { name: "account"; - isMut: true; - isSigner: false; + writable: true; }, { name: "owner"; - isMut: false; - isSigner: true; + signer: true; } ]; args: [ { name: "amount"; type: "u64"; - }, - { - name: "decimals"; - type: "u8"; } ]; }, { - name: "burnChecked"; + name: "mintToChecked"; + discriminator: [14]; accounts: [ { - name: "account"; - isMut: true; - isSigner: false; + name: "mint"; + writable: true; }, { - name: "mint"; - isMut: true; - isSigner: false; + name: "account"; + writable: true; }, { - name: "authority"; - isMut: false; - isSigner: true; + name: "owner"; + signer: true; } ]; args: [ @@ -451,163 +418,150 @@ type SplToken = { ]; }, { - name: "initializeAccount2"; + name: "revoke"; + discriminator: [5]; accounts: [ { - name: "account"; - isMut: true; - isSigner: false; + name: "source"; + writable: true; }, { - name: "mint"; - isMut: false; - isSigner: false; + name: "owner"; + signer: true; + } + ]; + args: []; + }, + { + name: "setAuthority"; + discriminator: [6]; + accounts: [ + { + name: "owned"; + writable: true; }, { - name: "rent"; - isMut: false; - isSigner: false; + name: "owner"; + signer: true; + }, + { + name: "signer"; + signer: true; } ]; args: [ { - name: "owner"; - type: "publicKey"; + name: "authorityType"; + type: { + defined: { + name: "authorityType"; + }; + }; + }, + { + name: "newAuthority"; + type: { + coption: "pubkey"; + }; } ]; }, { name: "syncNative"; + discriminator: [17]; accounts: [ { name: "account"; - isMut: true; - isSigner: false; + writable: true; } ]; args: []; }, { - name: "initializeAccount3"; + name: "thawAccount"; + discriminator: [11]; accounts: [ { name: "account"; - isMut: true; - isSigner: false; + writable: true; }, { name: "mint"; - isMut: false; - isSigner: false; - } - ]; - args: [ + }, { name: "owner"; - type: "publicKey"; + signer: true; } ]; + args: []; }, { - name: "initializeMultisig2"; + name: "transfer"; + discriminator: [3]; accounts: [ { - name: "multisig"; - isMut: true; - isSigner: false; + name: "source"; + writable: true; }, { - name: "signer"; - isMut: false; - isSigner: false; + name: "destination"; + writable: true; + }, + { + name: "authority"; + signer: true; } ]; args: [ { - name: "m"; - type: "u8"; + name: "amount"; + type: "u64"; } ]; }, { - name: "initializeMint2"; + name: "transferChecked"; + discriminator: [12]; accounts: [ { - name: "mint"; - isMut: true; - isSigner: false; - } - ]; - args: [ - { - name: "decimals"; - type: "u8"; - }, - { - name: "mintAuthority"; - type: "publicKey"; + name: "source"; + writable: true; }, - { - name: "freezeAuthority"; - type: { - defined: "COption"; - }; - } - ]; - }, - { - name: "getAccountDataSize"; - accounts: [ { name: "mint"; - isMut: false; - isSigner: false; - } - ]; - args: []; - }, - { - name: "initializeImmutableOwner"; - accounts: [ + }, { - name: "account"; - isMut: true; - isSigner: false; - } - ]; - args: []; - }, - { - name: "amountToUiAmount"; - accounts: [ + name: "destination"; + writable: true; + }, { - name: "mint"; - isMut: false; - isSigner: false; + name: "authority"; + signer: true; } ]; args: [ { name: "amount"; type: "u64"; + }, + { + name: "decimals"; + type: "u8"; } ]; }, { name: "uiAmountToAmount"; + discriminator: [24]; accounts: [ { name: "mint"; - isMut: false; - isSigner: false; } ]; args: [ { name: "uiAmount"; - type: { - defined: "&'astr"; - }; + type: "string"; } ]; } @@ -615,353 +569,312 @@ type SplToken = { accounts: [ { name: "mint"; - type: { - kind: "struct"; - fields: [ - { - name: "mintAuthority"; - type: { - defined: "COption"; - }; - }, - { - name: "supply"; - type: "u64"; - }, - { - name: "decimals"; - type: "u8"; - }, - { - name: "isInitialized"; - type: "bool"; - }, - { - name: "freezeAuthority"; - type: { - defined: "COption"; - }; - } - ]; - }; - }, - { - name: "account"; - type: { - kind: "struct"; - fields: [ - { - name: "mint"; - type: "publicKey"; - }, - { - name: "owner"; - type: "publicKey"; - }, - { - name: "amount"; - type: "u64"; - }, - { - name: "delegate"; - type: { - defined: "COption"; - }; - }, - { - name: "state"; - type: { - defined: "AccountState"; - }; - }, - { - name: "isNative"; - type: { - defined: "COption"; - }; - }, - { - name: "delegatedAmount"; - type: "u64"; - }, - { - name: "closeAuthority"; - type: { - defined: "COption"; - }; - } - ]; - }; + discriminator: []; }, { name: "multisig"; - type: { - kind: "struct"; - fields: [ - { - name: "m"; - type: "u8"; - }, - { - name: "n"; - type: "u8"; - }, - { - name: "isInitialized"; - type: "bool"; - }, - { - name: "signers"; - type: { - array: ["publicKey", 11]; - }; - } - ]; - }; - } - ]; - types: [ - { - name: "AccountState"; - type: { - kind: "enum"; - variants: [ - { - name: "Uninitialized"; - }, - { - name: "Initialized"; - }, - { - name: "Frozen"; - } - ]; - }; + discriminator: []; }, { - name: "AuthorityType"; - type: { - kind: "enum"; - variants: [ - { - name: "MintTokens"; - }, - { - name: "FreezeAccount"; - }, - { - name: "AccountOwner"; - }, - { - name: "CloseAccount"; - } - ]; - }; + name: "account"; + discriminator: []; } ]; errors: [ { code: 0; - name: "NotRentExempt"; + name: "notRentExempt"; msg: "Lamport balance below rent-exempt threshold"; }, { code: 1; - name: "InsufficientFunds"; + name: "insufficientFunds"; msg: "Insufficient funds"; }, { code: 2; - name: "InvalidMint"; + name: "invalidMint"; msg: "Invalid Mint"; }, { code: 3; - name: "MintMismatch"; + name: "mintMismatch"; msg: "Account not associated with this Mint"; }, { code: 4; - name: "OwnerMismatch"; + name: "ownerMismatch"; msg: "Owner does not match"; }, { code: 5; - name: "FixedSupply"; + name: "fixedSupply"; msg: "Fixed supply"; }, { code: 6; - name: "AlreadyInUse"; + name: "alreadyInUse"; msg: "Already in use"; }, { code: 7; - name: "InvalidNumberOfProvidedSigners"; + name: "invalidNumberOfProvidedSigners"; msg: "Invalid number of provided signers"; }, { code: 8; - name: "InvalidNumberOfRequiredSigners"; + name: "invalidNumberOfRequiredSigners"; msg: "Invalid number of required signers"; }, { code: 9; - name: "UninitializedState"; + name: "uninitializedState"; msg: "State is unititialized"; }, { code: 10; - name: "NativeNotSupported"; + name: "nativeNotSupported"; msg: "Instruction does not support native tokens"; }, { code: 11; - name: "NonNativeHasBalance"; + name: "nonNativeHasBalance"; msg: "Non-native account can only be closed if its balance is zero"; }, { code: 12; - name: "InvalidInstruction"; + name: "invalidInstruction"; msg: "Invalid instruction"; }, { code: 13; - name: "InvalidState"; + name: "invalidState"; msg: "State is invalid for requested operation"; }, { code: 14; - name: "Overflow"; + name: "overflow"; msg: "Operation overflowed"; }, { code: 15; - name: "AuthorityTypeNotSupported"; + name: "authorityTypeNotSupported"; msg: "Account does not support specified authority type"; }, { code: 16; - name: "MintCannotFreeze"; + name: "mintCannotFreeze"; msg: "This token mint cannot freeze accounts"; }, { code: 17; - name: "AccountFrozen"; + name: "accountFrozen"; msg: "Account is frozen"; }, { code: 18; - name: "MintDecimalsMismatch"; + name: "mintDecimalsMismatch"; msg: "The provided decimals value different from the Mint decimals"; }, { code: 19; - name: "NonNativeNotSupported"; + name: "nonNativeNotSupported"; msg: "Instruction does not support non-native tokens"; } ]; -}; - -const IDL: SplToken = { - version: "3.3.0", - name: "spl_token", - instructions: [ + types: [ { - name: "initializeMint", - accounts: [ - { - name: "mint", - isMut: true, - isSigner: false, - }, - { - name: "rent", - isMut: false, - isSigner: false, - }, - ], - args: [ - { - name: "decimals", - type: "u8", - }, - { - name: "mintAuthority", - type: "publicKey", - }, - { - name: "freezeAuthority", - type: { - defined: "COption", + name: "accountState"; + type: { + kind: "enum"; + variants: [ + { + name: "uninitialized"; }, - }, - ], + { + name: "initialized"; + }, + { + name: "frozen"; + } + ]; + }; }, { - name: "initializeAccount", - accounts: [ - { - name: "account", - isMut: true, - isSigner: false, - }, - { - name: "mint", - isMut: false, - isSigner: false, - }, - { - name: "owner", - isMut: false, - isSigner: false, - }, - { - name: "rent", - isMut: false, - isSigner: false, - }, - ], - args: [], + name: "authorityType"; + type: { + kind: "enum"; + variants: [ + { + name: "mintTokens"; + }, + { + name: "freezeAccount"; + }, + { + name: "accountOwner"; + }, + { + name: "closeAccount"; + } + ]; + }; }, { - name: "initializeMultisig", - accounts: [ - { - name: "multisig", - isMut: true, - isSigner: false, - }, - { - name: "rent", - isMut: false, - isSigner: false, - }, - ], - args: [ - { - name: "m", - type: "u8", - }, - ], + name: "mint"; + type: { + kind: "struct"; + fields: [ + { + name: "mintAuthority"; + docs: [ + "Optional authority used to mint new tokens. The mint authority may only be provided during", + "mint creation. If no mint authority is present then the mint has a fixed supply and no", + "further tokens may be minted." + ]; + type: { + coption: "pubkey"; + }; + }, + { + name: "supply"; + docs: ["Total supply of tokens."]; + type: "u64"; + }, + { + name: "decimals"; + docs: [ + "Number of base 10 digits to the right of the decimal place." + ]; + type: "u8"; + }, + { + name: "isInitialized"; + docs: ["Is `true` if this structure has been initialized"]; + type: "bool"; + }, + { + name: "freezeAuthority"; + docs: ["Optional authority to freeze token accounts."]; + type: { + coption: "pubkey"; + }; + } + ]; + }; }, { - name: "transfer", + name: "multisig"; + type: { + kind: "struct"; + fields: [ + { + name: "m"; + docs: ["Number of signers required"]; + type: "u8"; + }, + { + name: "n"; + docs: ["Number of valid signers"]; + type: "u8"; + }, + { + name: "isInitialized"; + docs: ["Is `true` if this structure has been initialized"]; + type: "bool"; + }, + { + name: "signers"; + docs: ["Signer public keys"]; + type: { + array: ["pubkey", 11]; + }; + } + ]; + }; + }, + { + name: "account"; + type: { + kind: "struct"; + fields: [ + { + name: "mint"; + docs: ["The mint associated with this account"]; + type: "pubkey"; + }, + { + name: "owner"; + docs: ["The owner of this account."]; + type: "pubkey"; + }, + { + name: "amount"; + docs: ["The amount of tokens this account holds."]; + type: "u64"; + }, + { + name: "delegate"; + docs: [ + "If `delegate` is `Some` then `delegated_amount` represents", + "the amount authorized by the delegate" + ]; + type: { + coption: "pubkey"; + }; + }, + { + name: "state"; + docs: ["The account's state"]; + type: { + defined: { + name: "accountState"; + }; + }; + }, + { + name: "isNative"; + docs: [ + "If is_native.is_some, this is a native token, and the value logs the rent-exempt reserve. An", + "Account is required to be rent-exempt, so the value is used by the Processor to ensure that", + "wrapped SOL accounts do not drop below this threshold." + ]; + type: { + coption: "u64"; + }; + }, + { + name: "delegatedAmount"; + docs: ["The amount delegated"]; + type: "u64"; + }, + { + name: "closeAuthority"; + docs: ["Optional authority to close the account."]; + type: { + coption: "pubkey"; + }; + } + ]; + }; + } + ]; +}; + +const IDL: SplToken = { + address: "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", + metadata: { + name: "splToken", + version: "3.3.0", + spec: "0.1.0", + }, + instructions: [ + { + name: "amountToUiAmount", + discriminator: [23], accounts: [ { - name: "source", - isMut: true, - isSigner: false, - }, - { - name: "destination", - isMut: true, - isSigner: false, - }, - { - name: "authority", - isMut: false, - isSigner: true, + name: "mint", }, ], args: [ @@ -973,21 +886,18 @@ const IDL: SplToken = { }, { name: "approve", + discriminator: [4], accounts: [ { name: "source", - isMut: true, - isSigner: false, + writable: true, }, { name: "delegate", - isMut: false, - isSigner: false, }, { name: "owner", - isMut: false, - isSigner: true, + signer: true, }, ], args: [ @@ -998,72 +908,50 @@ const IDL: SplToken = { ], }, { - name: "revoke", + name: "approveChecked", + discriminator: [13], accounts: [ { name: "source", - isMut: true, - isSigner: false, + writable: true, }, { - name: "owner", - isMut: false, - isSigner: true, + name: "mint", }, - ], - args: [], - }, - { - name: "setAuthority", - accounts: [ { - name: "owned", - isMut: true, - isSigner: false, + name: "delegate", }, { name: "owner", - isMut: false, - isSigner: true, - }, - { - name: "signer", - isMut: false, - isSigner: true, + signer: true, }, ], args: [ { - name: "authorityType", - type: { - defined: "AuthorityType", - }, + name: "amount", + type: "u64", }, { - name: "newAuthority", - type: { - defined: "COption", - }, + name: "decimals", + type: "u8", }, ], }, { - name: "mintTo", + name: "burn", + discriminator: [8], accounts: [ { - name: "mint", - isMut: true, - isSigner: false, + name: "account", + writable: true, }, { - name: "account", - isMut: true, - isSigner: false, + name: "mint", + writable: true, }, { - name: "owner", - isMut: false, - isSigner: true, + name: "authority", + signer: true, }, ], args: [ @@ -1074,22 +962,20 @@ const IDL: SplToken = { ], }, { - name: "burn", + name: "burnChecked", + discriminator: [15], accounts: [ { name: "account", - isMut: true, - isSigner: false, + writable: true, }, { name: "mint", - isMut: true, - isSigner: false, + writable: true, }, { name: "authority", - isMut: false, - isSigner: true, + signer: true, }, ], args: [ @@ -1097,335 +983,400 @@ const IDL: SplToken = { name: "amount", type: "u64", }, + { + name: "decimals", + type: "u8", + }, ], }, { name: "closeAccount", + discriminator: [9], accounts: [ { name: "account", - isMut: true, - isSigner: false, + writable: true, }, { name: "destination", - isMut: true, - isSigner: false, + writable: true, }, { name: "owner", - isMut: false, - isSigner: true, + signer: true, }, ], args: [], }, { name: "freezeAccount", + discriminator: [10], accounts: [ { name: "account", - isMut: true, - isSigner: false, + writable: true, }, { name: "mint", - isMut: false, - isSigner: false, }, { name: "owner", - isMut: false, - isSigner: true, + signer: true, }, ], args: [], }, { - name: "thawAccount", + name: "getAccountDataSize", + discriminator: [21], accounts: [ - { - name: "account", - isMut: true, - isSigner: false, - }, { name: "mint", - isMut: false, - isSigner: false, - }, - { - name: "owner", - isMut: false, - isSigner: true, }, ], args: [], }, { - name: "transferChecked", + name: "initializeAccount", + discriminator: [1], accounts: [ { - name: "source", - isMut: true, - isSigner: false, + name: "account", + writable: true, }, { name: "mint", - isMut: false, - isSigner: false, - }, - { - name: "destination", - isMut: true, - isSigner: false, }, { - name: "authority", - isMut: false, - isSigner: true, - }, - ], - args: [ - { - name: "amount", - type: "u64", + name: "owner", }, { - name: "decimals", - type: "u8", + name: "rent", + address: "SysvarRent111111111111111111111111111111111", }, ], + args: [], }, { - name: "approveChecked", + name: "initializeAccount2", + discriminator: [16], accounts: [ { - name: "source", - isMut: true, - isSigner: false, + name: "account", + writable: true, }, { name: "mint", - isMut: false, - isSigner: false, }, { - name: "delegate", - isMut: false, - isSigner: false, - }, - { - name: "owner", - isMut: false, - isSigner: true, + name: "rent", + address: "SysvarRent111111111111111111111111111111111", }, ], args: [ { - name: "amount", - type: "u64", - }, - { - name: "decimals", - type: "u8", + name: "owner", + type: "pubkey", }, ], }, { - name: "mintToChecked", + name: "initializeAccount3", + discriminator: [18], accounts: [ - { - name: "mint", - isMut: true, - isSigner: false, - }, { name: "account", - isMut: true, - isSigner: false, + writable: true, }, { - name: "owner", - isMut: false, - isSigner: true, + name: "mint", }, ], args: [ { - name: "amount", - type: "u64", - }, - { - name: "decimals", - type: "u8", + name: "owner", + type: "pubkey", }, ], }, { - name: "burnChecked", + name: "initializeImmutableOwner", + discriminator: [22], accounts: [ { name: "account", - isMut: true, - isSigner: false, + writable: true, }, + ], + args: [], + }, + { + name: "initializeMint", + discriminator: [0], + accounts: [ { name: "mint", - isMut: true, - isSigner: false, + writable: true, }, { - name: "authority", - isMut: false, - isSigner: true, + name: "rent", + address: "SysvarRent111111111111111111111111111111111", }, ], args: [ - { - name: "amount", - type: "u64", - }, { name: "decimals", type: "u8", }, + { + name: "mintAuthority", + type: "pubkey", + }, + { + name: "freezeAuthority", + type: { + coption: "pubkey", + }, + }, ], }, { - name: "initializeAccount2", + name: "initializeMint2", + discriminator: [20], accounts: [ { - name: "account", - isMut: true, - isSigner: false, + name: "mint", + writable: true, }, + ], + args: [ { - name: "mint", - isMut: false, - isSigner: false, + name: "decimals", + type: "u8", }, { - name: "rent", - isMut: false, - isSigner: false, + name: "mintAuthority", + type: "pubkey", }, - ], - args: [ { - name: "owner", - type: "publicKey", + name: "freezeAuthority", + type: { + coption: "pubkey", + }, }, ], }, { - name: "syncNative", + name: "initializeMultisig", + discriminator: [2], accounts: [ { - name: "account", - isMut: true, - isSigner: false, + name: "multisig", + writable: true, + }, + { + name: "rent", + address: "SysvarRent111111111111111111111111111111111", + }, + ], + args: [ + { + name: "m", + type: "u8", }, ], - args: [], }, { - name: "initializeAccount3", + name: "initializeMultisig2", + discriminator: [19], accounts: [ { - name: "account", - isMut: true, - isSigner: false, + name: "multisig", + writable: true, }, { - name: "mint", - isMut: false, - isSigner: false, + name: "signer", }, ], args: [ { - name: "owner", - type: "publicKey", + name: "m", + type: "u8", }, ], }, { - name: "initializeMultisig2", + name: "mintTo", + discriminator: [7], accounts: [ { - name: "multisig", - isMut: true, - isSigner: false, + name: "mint", + writable: true, }, { - name: "signer", - isMut: false, - isSigner: false, + name: "account", + writable: true, + }, + { + name: "owner", + signer: true, }, ], args: [ { - name: "m", - type: "u8", + name: "amount", + type: "u64", }, ], }, { - name: "initializeMint2", + name: "mintToChecked", + discriminator: [14], accounts: [ { name: "mint", - isMut: true, - isSigner: false, + writable: true, + }, + { + name: "account", + writable: true, + }, + { + name: "owner", + signer: true, }, ], args: [ + { + name: "amount", + type: "u64", + }, { name: "decimals", type: "u8", }, + ], + }, + { + name: "revoke", + discriminator: [5], + accounts: [ + { + name: "source", + writable: true, + }, { - name: "mintAuthority", - type: "publicKey", + name: "owner", + signer: true, + }, + ], + args: [], + }, + { + name: "setAuthority", + discriminator: [6], + accounts: [ + { + name: "owned", + writable: true, }, { - name: "freezeAuthority", + name: "owner", + signer: true, + }, + { + name: "signer", + signer: true, + }, + ], + args: [ + { + name: "authorityType", + type: { + defined: { + name: "authorityType", + }, + }, + }, + { + name: "newAuthority", type: { - defined: "COption", + coption: "pubkey", }, }, ], }, { - name: "getAccountDataSize", + name: "syncNative", + discriminator: [17], accounts: [ { - name: "mint", - isMut: false, - isSigner: false, + name: "account", + writable: true, }, ], args: [], }, { - name: "initializeImmutableOwner", + name: "thawAccount", + discriminator: [11], accounts: [ { name: "account", - isMut: true, - isSigner: false, + writable: true, + }, + { + name: "mint", + }, + { + name: "owner", + signer: true, }, ], args: [], }, { - name: "amountToUiAmount", + name: "transfer", + discriminator: [3], accounts: [ + { + name: "source", + writable: true, + }, + { + name: "destination", + writable: true, + }, + { + name: "authority", + signer: true, + }, + ], + args: [ + { + name: "amount", + type: "u64", + }, + ], + }, + { + name: "transferChecked", + discriminator: [12], + accounts: [ + { + name: "source", + writable: true, + }, { name: "mint", - isMut: false, - isSigner: false, + }, + { + name: "destination", + writable: true, + }, + { + name: "authority", + signer: true, }, ], args: [ @@ -1433,23 +1384,24 @@ const IDL: SplToken = { name: "amount", type: "u64", }, + { + name: "decimals", + type: "u8", + }, ], }, { name: "uiAmountToAmount", + discriminator: [24], accounts: [ { name: "mint", - isMut: false, - isSigner: false, }, ], args: [ { name: "uiAmount", - type: { - defined: "&'astr", - }, + type: "string", }, ], }, @@ -1457,250 +1409,294 @@ const IDL: SplToken = { accounts: [ { name: "mint", - type: { - kind: "struct", - fields: [ - { - name: "mintAuthority", - type: { - defined: "COption", - }, - }, - { - name: "supply", - type: "u64", - }, - { - name: "decimals", - type: "u8", - }, - { - name: "isInitialized", - type: "bool", - }, - { - name: "freezeAuthority", - type: { - defined: "COption", - }, - }, - ], - }, - }, - { - name: "account", - type: { - kind: "struct", - fields: [ - { - name: "mint", - type: "publicKey", - }, - { - name: "owner", - type: "publicKey", - }, - { - name: "amount", - type: "u64", - }, - { - name: "delegate", - type: { - defined: "COption", - }, - }, - { - name: "state", - type: { - defined: "AccountState", - }, - }, - { - name: "isNative", - type: { - defined: "COption", - }, - }, - { - name: "delegatedAmount", - type: "u64", - }, - { - name: "closeAuthority", - type: { - defined: "COption", - }, - }, - ], - }, + discriminator: [], }, { name: "multisig", - type: { - kind: "struct", - fields: [ - { - name: "m", - type: "u8", - }, - { - name: "n", - type: "u8", - }, - { - name: "isInitialized", - type: "bool", - }, - { - name: "signers", - type: { - array: ["publicKey", 11], - }, - }, - ], - }, - }, - ], - types: [ - { - name: "AccountState", - type: { - kind: "enum", - variants: [ - { - name: "Uninitialized", - }, - { - name: "Initialized", - }, - { - name: "Frozen", - }, - ], - }, + discriminator: [], }, { - name: "AuthorityType", - type: { - kind: "enum", - variants: [ - { - name: "MintTokens", - }, - { - name: "FreezeAccount", - }, - { - name: "AccountOwner", - }, - { - name: "CloseAccount", - }, - ], - }, + name: "account", + discriminator: [], }, ], errors: [ { code: 0, - name: "NotRentExempt", + name: "notRentExempt", msg: "Lamport balance below rent-exempt threshold", }, { code: 1, - name: "InsufficientFunds", + name: "insufficientFunds", msg: "Insufficient funds", }, { code: 2, - name: "InvalidMint", + name: "invalidMint", msg: "Invalid Mint", }, { code: 3, - name: "MintMismatch", + name: "mintMismatch", msg: "Account not associated with this Mint", }, { code: 4, - name: "OwnerMismatch", + name: "ownerMismatch", msg: "Owner does not match", }, { code: 5, - name: "FixedSupply", + name: "fixedSupply", msg: "Fixed supply", }, { code: 6, - name: "AlreadyInUse", + name: "alreadyInUse", msg: "Already in use", }, { code: 7, - name: "InvalidNumberOfProvidedSigners", + name: "invalidNumberOfProvidedSigners", msg: "Invalid number of provided signers", }, { code: 8, - name: "InvalidNumberOfRequiredSigners", + name: "invalidNumberOfRequiredSigners", msg: "Invalid number of required signers", }, { code: 9, - name: "UninitializedState", + name: "uninitializedState", msg: "State is unititialized", }, { code: 10, - name: "NativeNotSupported", + name: "nativeNotSupported", msg: "Instruction does not support native tokens", }, { code: 11, - name: "NonNativeHasBalance", + name: "nonNativeHasBalance", msg: "Non-native account can only be closed if its balance is zero", }, { code: 12, - name: "InvalidInstruction", + name: "invalidInstruction", msg: "Invalid instruction", }, { code: 13, - name: "InvalidState", + name: "invalidState", msg: "State is invalid for requested operation", }, { code: 14, - name: "Overflow", + name: "overflow", msg: "Operation overflowed", }, { code: 15, - name: "AuthorityTypeNotSupported", + name: "authorityTypeNotSupported", msg: "Account does not support specified authority type", }, { code: 16, - name: "MintCannotFreeze", + name: "mintCannotFreeze", msg: "This token mint cannot freeze accounts", }, { code: 17, - name: "AccountFrozen", + name: "accountFrozen", msg: "Account is frozen", }, { code: 18, - name: "MintDecimalsMismatch", + name: "mintDecimalsMismatch", msg: "The provided decimals value different from the Mint decimals", }, { code: 19, - name: "NonNativeNotSupported", + name: "nonNativeNotSupported", msg: "Instruction does not support non-native tokens", }, ], + types: [ + { + name: "accountState", + type: { + kind: "enum", + variants: [ + { + name: "uninitialized", + }, + { + name: "initialized", + }, + { + name: "frozen", + }, + ], + }, + }, + { + name: "authorityType", + type: { + kind: "enum", + variants: [ + { + name: "mintTokens", + }, + { + name: "freezeAccount", + }, + { + name: "accountOwner", + }, + { + name: "closeAccount", + }, + ], + }, + }, + { + name: "mint", + type: { + kind: "struct", + fields: [ + { + name: "mintAuthority", + docs: [ + "Optional authority used to mint new tokens. The mint authority may only be provided during", + "mint creation. If no mint authority is present then the mint has a fixed supply and no", + "further tokens may be minted.", + ], + type: { + coption: "pubkey", + }, + }, + { + name: "supply", + docs: ["Total supply of tokens."], + type: "u64", + }, + { + name: "decimals", + docs: [ + "Number of base 10 digits to the right of the decimal place.", + ], + type: "u8", + }, + { + name: "isInitialized", + docs: ["Is `true` if this structure has been initialized"], + type: "bool", + }, + { + name: "freezeAuthority", + docs: ["Optional authority to freeze token accounts."], + type: { + coption: "pubkey", + }, + }, + ], + }, + }, + { + name: "multisig", + type: { + kind: "struct", + fields: [ + { + name: "m", + docs: ["Number of signers required"], + type: "u8", + }, + { + name: "n", + docs: ["Number of valid signers"], + type: "u8", + }, + { + name: "isInitialized", + docs: ["Is `true` if this structure has been initialized"], + type: "bool", + }, + { + name: "signers", + docs: ["Signer public keys"], + type: { + array: ["pubkey", 11], + }, + }, + ], + }, + }, + { + name: "account", + type: { + kind: "struct", + fields: [ + { + name: "mint", + docs: ["The mint associated with this account"], + type: "pubkey", + }, + { + name: "owner", + docs: ["The owner of this account."], + type: "pubkey", + }, + { + name: "amount", + docs: ["The amount of tokens this account holds."], + type: "u64", + }, + { + name: "delegate", + docs: [ + "If `delegate` is `Some` then `delegated_amount` represents", + "the amount authorized by the delegate", + ], + type: { + coption: "pubkey", + }, + }, + { + name: "state", + docs: ["The account's state"], + type: { + defined: { + name: "accountState", + }, + }, + }, + { + name: "isNative", + docs: [ + "If is_native.is_some, this is a native token, and the value logs the rent-exempt reserve. An", + "Account is required to be rent-exempt, so the value is used by the Processor to ensure that", + "wrapped SOL accounts do not drop below this threshold.", + ], + type: { + coption: "u64", + }, + }, + { + name: "delegatedAmount", + docs: ["The amount delegated"], + type: "u64", + }, + { + name: "closeAuthority", + docs: ["Optional authority to close the account."], + type: { + coption: "pubkey", + }, + }, + ], + }, + }, + ], };