* Rewrite IDL type spec

* Rewrite IDL generation

* Partially rewrite the TS package with the new IDL, improved account resolution and types
This commit is contained in:
acheron 2024-03-10 14:22:23 +01:00 committed by GitHub
parent 23028334f3
commit d9a9f19394
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
189 changed files with 7997 additions and 6804 deletions

View File

@ -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

View File

@ -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/

View File

@ -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

14
Cargo.lock generated
View File

@ -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",

View File

@ -9,6 +9,7 @@ members = [
"avm",
"cli",
"client",
"idl",
"lang",
"lang/attribute/*",
"lang/derive/*",

View File

@ -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"

View File

@ -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<String>,
}
#[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<Self, Self::Err> {
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(),

View File

@ -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,
DEFAULT_LEDGER_PATH, 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<String>,
/// Output file for the TypeScript IDL.
#[clap(short = 't', long)]
out_ts: Option<String>,
/// 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<String>,
/// Output file for the IDL (stdout if not specified)
#[clap(short, long)]
out: Option<String>,
@ -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<String>,
out: Option<String>,
out_ts: Option<String>,
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<String>, out_ts: Option<String>, 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<Config>, skip_lint: bool, no_docs: bool) -> Result<Idl> {
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<Path>,
version: String,
seeds_feature: bool,
no_docs: bool,
safety_checks: bool,
) -> Result<Idl> {
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<Vec<Idl>> {
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<String>),
EventLines(Vec<String>),
ErrorsLines(Vec<String>),
ProgramLines(Vec<String>),
}
#[derive(Serialize, Deserialize)]
struct IdlBuildEventPrint {
event: IdlEvent,
defined_types: Vec<IdlTypeDefinition>,
}
let mut state = State::Pass;
let mut events: Vec<IdlEvent> = vec![];
let mut error_codes: Option<Vec<IdlErrorCode>> = None;
let mut constants: Vec<IdlConst> = vec![];
let mut defined_types: BTreeMap<String, IdlTypeDefinition> = BTreeMap::new();
let mut curr_idl: Option<Idl> = None;
let mut idls: Vec<Idl> = 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::<Vec<_>>();
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<IdlErrorCode> = 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;
}
}
serde_json::from_str::<Idl>(&modified_idl).ok()
})
.collect::<Vec<_>>();
// Verify IDLs are valid
for idl in &idls {
let full_path_account = idl
.accounts
if !is_idl_build {
let path = manifest.path().display();
let anchor_spl_idl_build = manifest
.dependencies
.iter()
.find(|account| account.name.contains("::"));
.any(|dep| dep.0 == "anchor-spl")
.then_some(r#", "anchor-spl/idl-build""#)
.unwrap_or_default();
if let Some(account) = full_path_account {
return Err(anyhow!(
"Conflicting accounts names are not allowed.\nProgram: {}\nAccount: {}",
idl.name,
account.name
));
}
return Err(anyhow!(
r#"`idl-build` feature is missing. To solve, add
[features]
idl-build = ["anchor-lang/idl-build"{anchor_spl_idl_build}]
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<String>) -> 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<JsonValue, anyhow::Error> {
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 = <u8 as AnchorDeserialize>::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!(<String as AnchorDeserialize>::deserialize(data)?),
IdlType::PublicKey => {
IdlType::Pubkey => {
json!(<Pubkey as AnchorDeserialize>::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<JsonValue> = 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 = <u8 as AnchorDeserialize>::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<JsonValue> = 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),
})
}
@ -3314,11 +3108,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))?;
}
@ -3443,22 +3237,15 @@ fn stream_logs(config: &WithPath<Config>, rpc_url: &str) -> Result<Vec<std::proc
let mut contents = vec![];
file.read_to_end(&mut contents)?;
let idl: Idl = serde_json::from_slice(&contents)?;
let metadata = idl.metadata.ok_or_else(|| {
anyhow!(
"Metadata property not found in IDL of program: {}",
program.lib_name
)
})?;
let metadata: IdlTestMetadata = serde_json::from_value(metadata)?;
let log_file = File::create(format!(
"{}/{}.{}.log",
program_logs_dir, metadata.address, program.lib_name,
program_logs_dir, idl.address, program.lib_name,
))?;
let stdio = std::process::Stdio::from(log_file);
let child = std::process::Command::new("solana")
.arg("logs")
.arg(metadata.address)
.arg(idl.address)
.arg("--url")
.arg(rpc_url)
.stdout(stdio)
@ -3485,11 +3272,6 @@ fn stream_logs(config: &WithPath<Config>, rpc_url: &str) -> Result<Vec<std::proc
Ok(handles)
}
#[derive(Debug, Serialize, Deserialize)]
pub struct IdlTestMetadata {
address: String,
}
fn start_test_validator(
cfg: &Config,
test_validator: &Option<TestValidator>,
@ -3720,13 +3502,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))?;
}
@ -4046,7 +3826,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(),
)
})

View File

@ -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<String> {
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)?;
export const IDL: {} = {};
"#,
idl.name.to_upper_camel_case(),
idl_json,
idl.name.to_upper_camel_case(),
idl_json
// 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;
}
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::<Idl>(&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
)?;

View File

@ -12,6 +12,7 @@ name = "basic_0"
[features]
no-entrypoint = []
cpi = ["no-entrypoint"]
idl-build = ["anchor-lang/idl-build"]
[dependencies]
anchor-lang = { path = "../../../../../lang" }

View File

@ -12,6 +12,7 @@ name = "basic_1"
[features]
no-entrypoint = []
cpi = ["no-entrypoint"]
idl-build = ["anchor-lang/idl-build"]
[dependencies]
anchor-lang = { path = "../../../../../lang" }

View File

@ -12,6 +12,7 @@ name = "basic_2"
[features]
no-entrypoint = []
cpi = ["no-entrypoint"]
idl-build = ["anchor-lang/idl-build"]
[dependencies]
anchor-lang = { path = "../../../../../lang" }

View File

@ -12,6 +12,7 @@ name = "puppet_master"
[features]
no-entrypoint = []
cpi = ["no-entrypoint"]
idl-build = ["anchor-lang/idl-build"]
[dependencies]
anchor-lang = { path = "../../../../../lang" }

View File

@ -12,6 +12,7 @@ name = "puppet"
[features]
no-entrypoint = []
cpi = ["no-entrypoint"]
idl-build = ["anchor-lang/idl-build"]
[dependencies]
anchor-lang = { path = "../../../../../lang" }

View File

@ -12,6 +12,7 @@ name = "basic_4"
[features]
no-entrypoint = []
cpi = ["no-entrypoint"]
idl-build = ["anchor-lang/idl-build"]
[dependencies]
anchor-lang = { path = "../../../../../lang" }

View File

@ -12,6 +12,7 @@ name = "basic_5"
[features]
no-entrypoint = []
cpi = ["no-entrypoint"]
idl-build = ["anchor-lang/idl-build"]
[dependencies]
anchor-lang = { path = "../../../../../lang" }

30
idl/Cargo.toml Normal file
View File

@ -0,0 +1,30 @@
[package]
name = "anchor-idl"
version = "0.29.0"
authors = ["Anchor Maintainers <accounts@200ms.io>"]
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 }

252
idl/src/build.rs Normal file
View File

@ -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<Path>,
resolution: bool,
skip_lint: bool,
no_docs: bool,
) -> Result<Idl> {
// 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<Idl> {
// `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<String>),
Events(Vec<String>),
Errors(Vec<String>),
Program(Vec<String>),
}
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<Idl> = 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<IdlTypeDef>,
}
let event = serde_json::from_str::<IdlBuildEventPrint>(&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(())
}

6
idl/src/lib.rs Normal file
View File

@ -0,0 +1,6 @@
//! Anchor IDL.
pub mod types;
#[cfg(feature = "build")]
pub mod build;

3
idl/src/types.rs Normal file
View File

@ -0,0 +1,3 @@
//! IDL types are re-exported from [`anchor_syn`].
pub use anchor_syn::idl::types::*;

View File

@ -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)
}

View File

@ -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

View File

@ -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

View File

@ -628,7 +628,7 @@ use syn::parse_macro_input;
/// <tbody>
/// </table>
#[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()

View File

@ -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!(),
};

View File

@ -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];
}
}

View File

@ -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 }

View File

@ -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 {

View File

@ -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

View File

@ -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
};
};

View File

@ -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
};
};

View File

@ -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<super::types::IdlTypeDefinition> {
None
}
/// Insert the type definition to the defined types hashmap.
fn __anchor_private_insert_idl_defined(
_defined_types: &mut std::collections::HashMap<String, super::types::IdlTypeDefinition>,
) {
}
}
#[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<Ident>,
) -> Result<(TokenStream, Vec<syn::TypePath>), ()> {
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, second::path> -> 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<syn::Ident>,
) -> Result<(TokenStream, Vec<syn::TypePath>), ()> {
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<syn::TypePath>), ()> {
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<syn::TypePath>), ()> {
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::<Vec<_>>();
let (fields, defined): (Vec<TokenStream>, Vec<Vec<syn::TypePath>>) = 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::<Result<Vec<_>, _>>()?
.into_iter()
.unzip::<_, _, Vec<_>, Vec<_>>(),
_ => return Err(()),
};
let defined = defined
.into_iter()
.flatten()
.collect::<Vec<syn::TypePath>>();
let generics = if !type_params.is_empty() {
let g: Vec<String> = 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<syn::TypePath>), ()> {
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::<Vec<_>>();
let (variants, defined): (Vec<TokenStream>, Vec<Vec<syn::TypePath>>) = enum_item.variants.iter().map(|variant: &syn::Variant| {
let name = variant.ident.to_string();
let (fields, defined): (TokenStream, Vec<syn::TypePath>) = 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::<Result<Vec<_>, _>>()?
.into_iter()
.unzip::<TokenStream, Vec<syn::TypePath>, Vec<TokenStream>, Vec<Vec<syn::TypePath>>>();
let defined = defined
.into_iter()
.flatten()
.collect::<Vec<_>>();
(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::<Result<Vec<_>, _>>()?
.into_iter()
.unzip::<TokenStream, Vec<syn::TypePath>, Vec<TokenStream>, Vec<Vec<syn::TypePath>>>();
let defined = defined
.into_iter()
.flatten()
.collect::<Vec<_>>();
(quote!{ Some(#idl::EnumFields::Named(vec![#(#fields),*]))}, defined)
}
};
Ok((quote!{ #idl::IdlEnumVariant{ name: #name.into(), fields: #fields }}, defined))
})
.collect::<Result<Vec<_>, _>>()?
.into_iter()
.unzip::<TokenStream, Vec<syn::TypePath>, Vec<TokenStream>, Vec<Vec<syn::TypePath>>>();
let defined = defined
.into_iter()
.flatten()
.collect::<Vec<syn::TypePath>>();
let generics = if !type_params.is_empty() {
let g: Vec<String> = 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<String, #idl::IdlTypeDefinition>
) {
#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<TokenStream>, Vec<syn::TypePath>), ()> {
let (fields, defined) = fields
.named
.iter()
.map(idl_event_field_ts_from_syn_field)
.collect::<Result<Vec<_>, _>>()?
.into_iter()
.unzip::<_, _, Vec<_>, Vec<_>>();
let defined = defined
.into_iter()
.flatten()
.collect::<Vec<syn::TypePath>>();
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<String, #idl::IdlTypeDefinition>,
) -> 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<TokenStream>, Vec<Option<&syn::TypePath>>) = 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::<TokenStream, Option<&syn::TypePath>, Vec<TokenStream>, Vec<Option<&syn::TypePath>>>();
let acc_types = acc_types
.into_iter()
.flatten()
.collect::<Vec<&syn::TypePath>>();
quote! {
impl #impl_generics #ident #ty_generics #where_clause {
pub fn __anchor_private_gen_idl_accounts(
accounts: &mut std::collections::HashMap<String, #idl::IdlTypeDefinition>,
defined_types: &mut std::collections::HashMap<String, #idl::IdlTypeDefinition>,
) -> 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::<Result<Vec<_>, ()>>()?
.into_iter()
.unzip::<TokenStream, Vec<syn::TypePath>, Vec<TokenStream>, Vec<Vec<syn::TypePath>>>();
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::<TokenStream, Vec<Vec<syn::TypePath>>, Vec<TokenStream>, Vec<Vec<Vec<syn::TypePath>>>>();
let defined = defined
.into_iter()
.flatten()
.flatten()
.collect::<Vec<syn::TypePath>>();
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<String, #idl::IdlTypeDefinition> =
std::collections::HashMap::new();
let mut defined_types: std::collections::HashMap<String, #idl::IdlTypeDefinition> =
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<String, #idl::IdlTypeDefinition> = 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::<Vec<#idl::IdlTypeDefinition>>()
});
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::<Vec<TokenStream>>();
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 ----");
}
}
}

View File

@ -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::<Vec<_>>();
quote! {
impl #impl_generics #ident #ty_generics #where_clause {
pub fn __anchor_private_gen_idl_accounts(
accounts: &mut std::collections::BTreeMap<String, #idl::IdlAccount>,
types: &mut std::collections::BTreeMap<String, #idl::IdlTypeDef>,
) -> 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::<Result<Vec<_>>>().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<TokenStream> {
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<u8>` 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<String>,
}
impl SeedPath {
/// Extract the seed path from a single seed expression.
fn new(seed: &syn::Expr) -> Result<Self> {
// 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::<Vec<_>>();
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::<Option<Vec<_>>>(),
_ => None,
})
.flatten()
.collect::<Vec<_>>();
quote! { vec![#(#relations.into()),*] }
}

View File

@ -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
}
}
}

View File

@ -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<Path>) -> Result<PathBuf> {
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);
}
}

View File

@ -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
}
}
}

View File

@ -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<IdlTypeDef> {
None
}
/// Insert all types that are included in the current type definition to the given map.
fn insert_types(_types: &mut BTreeMap<String, IdlTypeDef>) {}
/// 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::<Self>().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<syn::TypePath>)>,
) -> 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<String, #idl::IdlTypeDef>
) {
#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<syn::TypePath>)> {
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::<Result<Vec<_>>>()?
.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::<Result<Vec<_>>>()?
.into_iter()
.unzip::<_, Vec<_>, Vec<_>, Vec<_>>();
(
quote! { Some(#idl::IdlDefinedFields::Tuple(vec![#(#types),*])) },
defined,
)
}
};
let defined = defined.into_iter().flatten().collect::<Vec<_>>();
Ok((
quote! {
#idl::IdlTypeDefTy::Struct {
fields: #fields,
}
},
defined,
))
})
}
fn gen_idl_type_def_enum(enm: &syn::ItemEnum) -> Result<(TokenStream, Vec<syn::TypePath>)> {
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::<Result<Vec<_>>>()?
.into_iter()
.unzip::<_, Vec<_>, Vec<_>, Vec<_>>();
let defined = defined.into_iter().flatten().collect::<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::<Result<Vec<_>>>()?
.into_iter()
.unzip::<_, Vec<_>, Vec<_>, Vec<_>>();
let defined = defined.into_iter().flatten().collect::<Vec<_>>();
(
quote! { Some(#idl::IdlDefinedFields::Tuple(vec![#(#types),*])) },
defined,
)
}
};
Ok((
quote! { #idl::IdlEnumVariant { name: #name.into(), fields: #fields } },
defined,
))
})
.collect::<Result<Vec<_>>>()?
.into_iter()
.unzip::<_, _, Vec<_>, Vec<_>>();
let defined = defined.into_iter().flatten().collect::<Vec<_>>();
Ok((
quote! {
#idl::IdlTypeDefTy::Enum {
variants: vec![#(#variants),*],
}
},
defined,
))
})
}
fn gen_idl_type_def<F>(
attrs: &[syn::Attribute],
generics: &syn::Generics,
create_fields: F,
) -> Result<(TokenStream, Vec<syn::TypePath>)>
where
F: Fn(&[syn::Ident]) -> Result<(TokenStream, Vec<syn::TypePath>)>,
{
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::<usize>().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::<Vec<_>>();
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::<Vec<_>>();
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<str>, attrs: &[syn::Attribute]) -> Option<String> {
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<syn::TypePath>)> {
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<syn::TypePath>)> {
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::<Vec<_>>();
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()
}

View File

@ -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::<Vec<_>>();
let fn_body = gen_print_section("errors", quote! { vec![#(#error_codes),*] });
quote! {
#[test]
pub fn #fn_name() {
#fn_body
}
}
}

View File

@ -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::<Vec<_>>()
})
},
);
quote! {
#idl_build_impl
#[test]
pub fn #fn_name() {
let mut types: std::collections::BTreeMap<String, #idl::IdlTypeDef> =
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: <Self as anchor_lang::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<String, #idl::IdlTypeDef>,
) -> Option<#idl::IdlEvent> {
#fn_body
}
}
}
}

View File

@ -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<Path>) -> Result<Option<syn::Type>> {
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, &registry_path, &lock_file)
}
fn recursively_find_type(
defined_name: &str,
use_path: &str,
registry_path: &Path,
lock_file: &[(String, String)],
) -> Result<Option<syn::Type>> {
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::<Vec<_>>();
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::<Vec<_>>().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<PathBuf> {
#[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<Path>) -> Result<Vec<(String, String)>> {
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::<Vec<_>>();
Ok(parsed)
}
fn get_uses(path: impl AsRef<Path>) -> Result<Vec<String>> {
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::<Vec<_>>();
Ok(uses)
}
fn flatten_uses(tree: &syn::UseTree) -> Vec<String> {
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()],
}
}

View File

@ -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;

View File

@ -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! {
<crate::instruction::#name_pascal as anchor_lang::Discriminator>::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::<Result<Vec<_>>>()?
.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::<Vec<_>>();
let fn_body = gen_print_section(
"program",
quote! {
let mut accounts: std::collections::BTreeMap<String, #idl::IdlAccount> =
std::collections::BTreeMap::new();
let mut types: std::collections::BTreeMap<String, #idl::IdlTypeDef> =
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
}
}
}

View File

@ -2,6 +2,3 @@ pub mod types;
#[cfg(feature = "idl-build")]
pub mod build;
#[cfg(feature = "idl-parse")]
pub mod parse;

View File

@ -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<Path>,
version: String,
seeds_feature: bool,
no_docs: bool,
safety_checks: bool,
) -> Result<Idl> {
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::<Vec<IdlErrorCode>>()
});
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::<Vec<_>>();
// 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::<Vec<_>>();
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::<Vec<IdlEventField>>();
IdlEvent {
name: e.ident.to_string(),
fields,
}
})
.collect::<Vec<IdlEvent>>();
// 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<String> = account_structs
.iter()
.map(|a| a.ident.to_string())
.collect::<HashSet<_>>();
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::<Vec<IdlConst>>();
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<syn::ItemMod> {
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::<Vec<_>>();
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<syn::ItemEnum> {
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<String, AccountsStruct> {
// 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<Vec<IdlTypeDefinition>> {
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::<Result<Vec<IdlField>>>(),
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<IdlType> = fields
.unnamed
.iter()
.map(|f| to_idl_type(ctx, &f.ty))
.collect();
Some(EnumFields::Tuple(fields))
}
syn::Fields::Named(fields) => {
let fields: Vec<IdlField> = 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::<Vec<IdlEnumVariant>>();
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> -> 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<String, AccountsStruct>,
seeds_feature: bool,
no_docs: bool,
) -> Vec<IdlAccountItem> {
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::<Vec<_>>()
}

View File

@ -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<Self, Self::Err> {
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::<usize>().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<u8>" => 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<bool>").unwrap(),
IdlType::Option(Box::new(IdlType::Bool))
)
}
#[test]
fn vector() {
assert_eq!(
IdlType::from_str("Vec<bool>").unwrap(),
IdlType::Vec(Box::new(IdlType::Bool))
)
}
}

View File

@ -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<IdlPda> {
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<String, String>,
// Constants available in the crate.
const_names: Vec<String>,
// Constants declared in impl blocks available in the crate
impl_const_names: Vec<String>,
// All field names of the accounts in the accounts context.
account_field_names: Vec<String>,
}
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<String> = ctx.consts().map(|c| c.ident.to_string()).collect();
let impl_const_names: Vec<String> = 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<IdlPda> {
// Extract the idl seed types from the constraints.
let seeds = seeds_grp
.seeds
.iter()
.map(|s| self.parse_seed(s))
.collect::<Option<Vec<_>>>()?;
// 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<IdlSeed> {
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<IdlSeed> {
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<IdlSeed> {
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<IdlSeed> {
// 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<IdlSeed> {
// 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<IdlSeed> {
// 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<IdlSeed> {
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<String>);
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<SeedPath> {
// 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()
}

View File

@ -1,19 +0,0 @@
use crate::Field;
use syn::Expr;
pub fn parse(acc: &Field, seeds_feature: bool) -> Vec<String> {
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()
}

View File

@ -1,24 +1,137 @@
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<Vec<String>>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub constants: Vec<IdlConst>,
pub address: String,
pub metadata: IdlMetadata,
#[serde(default, skip_serializing_if = "is_default")]
pub docs: Vec<String>,
pub instructions: Vec<IdlInstruction>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub accounts: Vec<IdlTypeDefinition>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub types: Vec<IdlTypeDefinition>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub events: Option<Vec<IdlEvent>>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub errors: Option<Vec<IdlErrorCode>>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub metadata: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "is_default")]
pub accounts: Vec<IdlAccount>,
#[serde(default, skip_serializing_if = "is_default")]
pub events: Vec<IdlEvent>,
#[serde(default, skip_serializing_if = "is_default")]
pub errors: Vec<IdlErrorCode>,
#[serde(default, skip_serializing_if = "is_default")]
pub types: Vec<IdlTypeDef>,
#[serde(default, skip_serializing_if = "is_default")]
pub constants: Vec<IdlConst>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct IdlMetadata {
pub name: String,
pub version: String,
pub spec: String,
#[serde(skip_serializing_if = "is_default")]
pub description: Option<String>,
#[serde(skip_serializing_if = "is_default")]
pub repository: Option<String>,
#[serde(default, skip_serializing_if = "is_default")]
pub dependencies: Vec<IdlDependency>,
#[serde(skip_serializing_if = "is_default")]
pub contact: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct IdlDependency {
pub name: String,
pub version: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct IdlInstruction {
pub name: String,
#[serde(default, skip_serializing_if = "is_default")]
pub docs: Vec<String>,
pub discriminator: IdlDiscriminator,
pub accounts: Vec<IdlInstructionAccountItem>,
pub args: Vec<IdlField>,
#[serde(skip_serializing_if = "is_default")]
pub returns: Option<IdlType>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(untagged)]
pub enum IdlInstructionAccountItem {
Composite(IdlInstructionAccounts),
Single(IdlInstructionAccount),
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct IdlInstructionAccount {
pub name: String,
#[serde(default, skip_serializing_if = "is_default")]
pub docs: Vec<String>,
#[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<String>,
#[serde(skip_serializing_if = "is_default")]
pub pda: Option<IdlPda>,
#[serde(default, skip_serializing_if = "is_default")]
pub relations: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct IdlInstructionAccounts {
pub name: String,
pub accounts: Vec<IdlInstructionAccountItem>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct IdlPda {
pub seeds: Vec<IdlSeed>,
#[serde(skip_serializing_if = "is_default")]
pub program: Option<IdlSeed>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "kind", rename_all = "lowercase")]
pub enum IdlSeed {
Const(IdlSeedConst),
Arg(IdlSeedArg),
Account(IdlSeedAccount),
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct IdlSeedConst {
pub value: Vec<u8>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct IdlSeedArg {
pub path: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct IdlSeedAccount {
pub path: String,
#[serde(skip_serializing_if = "is_default")]
pub account: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct IdlAccount {
pub name: String,
pub discriminator: IdlDiscriminator,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct IdlEvent {
pub name: String,
pub discriminator: IdlDiscriminator,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
@ -29,161 +142,128 @@ pub struct IdlConst {
pub value: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct IdlState {
#[serde(rename = "struct")]
pub strct: IdlTypeDefinition,
pub methods: Vec<IdlInstruction>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct IdlInstruction {
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct IdlErrorCode {
pub code: u32,
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub docs: Option<Vec<String>>,
pub accounts: Vec<IdlAccountItem>,
pub args: Vec<IdlField>,
#[serde(skip_serializing_if = "Option::is_none")]
pub returns: Option<IdlType>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct IdlAccounts {
pub name: String,
pub accounts: Vec<IdlAccountItem>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(untagged)]
pub enum IdlAccountItem {
IdlAccount(IdlAccount),
IdlAccounts(IdlAccounts),
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct IdlAccount {
pub name: String,
pub is_mut: bool,
pub is_signer: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub is_optional: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub docs: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub pda: Option<IdlPda>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub relations: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct IdlPda {
pub seeds: Vec<IdlSeed>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub program_id: Option<IdlSeed>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase", tag = "kind")]
pub enum IdlSeed {
Const(IdlSeedConst),
Arg(IdlSeedArg),
Account(IdlSeedAccount),
}
#[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<T>` type is used.
#[serde(skip_serializing_if = "Option::is_none")]
pub account: Option<String>,
pub path: String,
}
#[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,
#[serde(skip_serializing_if = "is_default")]
pub msg: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct IdlField {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub docs: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "is_default")]
pub docs: Vec<String>,
#[serde(rename = "type")]
pub ty: IdlType,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct IdlEvent {
pub name: String,
pub fields: Vec<IdlEventField>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct IdlEventField {
pub struct IdlTypeDef {
pub name: String,
#[serde(default, skip_serializing_if = "is_default")]
pub docs: Vec<String>,
#[serde(default, skip_serializing_if = "is_default")]
pub serialization: IdlSerialization,
#[serde(skip_serializing_if = "is_default")]
pub repr: Option<IdlRepr>,
#[serde(default, skip_serializing_if = "is_default")]
pub generics: Vec<IdlTypeDefGeneric>,
#[serde(rename = "type")]
pub ty: IdlType,
pub index: bool,
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)]
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 name: String,
/// Documentation comments
#[serde(skip_serializing_if = "Option::is_none")]
pub docs: Option<Vec<String>>,
/// Generics, only supported with `idl-build`
#[serde(skip_serializing_if = "Option::is_none")]
pub generics: Option<Vec<String>>,
/// Type definition, `struct` or `enum`
#[serde(rename = "type")]
pub ty: IdlTypeDefinitionTy,
#[serde(tag = "kind", rename_all = "lowercase")]
pub enum IdlRepr {
Rust(IdlReprModifier),
C(IdlReprModifier),
Transparent,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "lowercase", tag = "kind")]
pub enum IdlTypeDefinitionTy {
Struct { fields: Vec<IdlField> },
Enum { variants: Vec<IdlEnumVariant> },
Alias { value: IdlType },
pub struct IdlReprModifier {
#[serde(default, skip_serializing_if = "is_default")]
pub packed: bool,
#[serde(skip_serializing_if = "is_default")]
pub align: Option<usize>,
}
#[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<IdlDefinedFields>,
},
Enum {
variants: Vec<IdlEnumVariant>,
},
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<EnumFields>,
#[serde(skip_serializing_if = "is_default")]
pub fields: Option<IdlDefinedFields>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(untagged)]
pub enum EnumFields {
pub enum IdlDefinedFields {
Named(Vec<IdlField>),
Tuple(Vec<IdlType>),
}
#[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<IdlType>),
Vec(Box<IdlType>),
Array(Box<IdlType>, usize),
GenericLenArray(Box<IdlType>, String),
Generic(String),
DefinedWithTypeArgs {
Array(Box<IdlType>, IdlArrayLen),
Defined {
name: String,
args: Vec<IdlDefinedTypeArg>,
#[serde(default, skip_serializing_if = "is_default")]
generics: Vec<IdlGenericArg>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub enum IdlDefinedTypeArg {
Generic(String),
Value(String),
Type(IdlType),
}
#[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<String>,
impl FromStr for IdlType {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
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<u8>" => 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::<usize>() {
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::<bool>().is_ok()
|| g.parse::<u128>().is_ok()
|| g.parse::<i128>().is_ok()
|| g.parse::<char>().is_ok()
{
Ok(IdlGenericArg::Const { value: g })
} else {
Self::from_str(&g).map(|ty| IdlGenericArg::Type { ty })
}
})
.collect::<Result<Vec<_>, _>>()?,
)
} else {
(s.to_owned(), vec![])
};
IdlType::Defined { name, generics }
}
};
Ok(r)
}
}
pub type IdlDiscriminator = Vec<u8>;
/// Get whether the given data is the default of its type.
fn is_default<T: Default + PartialEq>(it: &T) -> bool {
*it == T::default()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn option() {
assert_eq!(
IdlType::from_str("Option<bool>").unwrap(),
IdlType::Option(Box::new(IdlType::Bool))
)
}
#[test]
fn vector() {
assert_eq!(
IdlType::from_str("Vec<bool>").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<Pubkey, u64, 8>").unwrap(),
IdlType::Defined {
name: "MyStruct".into(),
generics: vec![
IdlGenericArg::Type {
ty: IdlType::Pubkey
},
IdlGenericArg::Type { ty: IdlType::U64 },
IdlGenericArg::Const { value: "8".into() },
],
}
)
}
}

View File

@ -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 {

View File

@ -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<Path>) -> Result<Self> {
Ok(CrateContext {
modules: ParsedModule::parse_recursive(root.as_ref())?,
})
}
pub fn consts(&self) -> impl Iterator<Item = &syn::ItemConst> {
self.modules.iter().flat_map(|(_, ctx)| ctx.consts())
}
@ -42,16 +48,10 @@ impl CrateContext {
}
}
pub fn parse(root: impl AsRef<Path>) -> Result<Self, anyhow::Error> {
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<syn::Item>,
}
struct UnparsedModule {
file: PathBuf,
path: String,
name: String,
item: syn::ItemMod,
}
impl ParsedModule {
fn parse_recursive(root: &Path) -> Result<BTreeMap<String, ParsedModule>, anyhow::Error> {
fn parse_recursive(root: &Path) -> Result<BTreeMap<String, ParsedModule>> {
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::<Vec<_>>();
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<UnparsedModule> {
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<Item = &syn::ItemMod> {
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<Item = &syn::ItemType> {
self.items.iter().filter_map(|i| match i {
syn::Item::Type(item) => Some(item),
_ => None,
})
}
fn consts(&self) -> impl Iterator<Item = &syn::ItemConst> {
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<Item = &syn::ItemType> {
self.items.iter().filter_map(|i| match i {
syn::Item::Type(item) => Some(item),
_ => None,
})
}
}

View File

@ -6,7 +6,5 @@ pub mod program;
pub mod spl_interface;
pub fn tts_to_string<T: quote::ToTokens>(item: T) -> String {
let mut tts = proc_macro2::TokenStream::new();
item.to_tokens(&mut tts);
tts.to_string()
item.to_token_stream().to_string()
}

View File

@ -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];
}
};
}

32
spl/src/idl_build.rs Normal file
View File

@ -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);

View File

@ -31,3 +31,6 @@ pub mod metadata;
#[cfg(feature = "memo")]
pub mod memo;
#[cfg(feature = "idl-build")]
mod idl_build;

View File

@ -810,9 +810,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);
@ -846,9 +843,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);
@ -885,9 +879,6 @@ impl Deref for TokenRecordAccount {
}
}
#[cfg(feature = "idl-build")]
impl anchor_lang::IdlBuild for TokenRecordAccount {}
#[derive(Clone)]
pub struct Metadata;

View File

@ -156,9 +156,6 @@ impl Deref for StakeAccount {
}
}
#[cfg(feature = "idl-build")]
impl anchor_lang::IdlBuild for StakeAccount {}
#[derive(Clone)]
pub struct Stake;

View File

@ -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;

View File

@ -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::*;

View File

@ -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" }

View File

@ -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(

View File

@ -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" }

View File

@ -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" }

View File

@ -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": []
}

View File

@ -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 () => {

View File

@ -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" }

View File

@ -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;

View File

@ -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" }

View File

@ -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 () => {

View File

@ -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);
}
}

View File

@ -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)

View File

@ -14,6 +14,7 @@ no-entrypoint = []
no-idl = []
cpi = ["no-entrypoint"]
default = []
idl-build = ["anchor-lang/idl-build"]
[dependencies]
anchor-lang = { path = "../../../../lang" }

View File

@ -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>,

View File

@ -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" }

View File

@ -14,6 +14,7 @@ no-entrypoint = []
no-idl = []
cpi = ["no-entrypoint"]
default = []
idl-build = ["anchor-lang/idl-build"]
[dependencies]
anchor-lang = { path = "../../../../lang" }

View File

@ -12,6 +12,7 @@ name = "composite"
[features]
no-entrypoint = []
cpi = ["no-entrypoint"]
idl-build = ["anchor-lang/idl-build"]
[dependencies]
anchor-lang = { path = "../../../../lang" }

View File

@ -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"] }

View File

@ -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"] }

View File

@ -159,7 +159,7 @@ describe("CPI return", () => {
(f) => f.name == "returnStruct"
);
assert.deepStrictEqual(returnStructInstruction.returns, {
defined: "StructReturn",
defined: { name: "structReturn" },
});
});

View File

@ -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" }

View File

@ -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

View File

@ -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" }

View File

@ -11,6 +11,7 @@ name = "declare_id"
[features]
no-entrypoint = []
cpi = ["no-entrypoint"]
idl-build = ["anchor-lang/idl-build"]
[dependencies]
anchor-lang = { path = "../../../../lang" }

View File

@ -11,6 +11,7 @@ name = "errors"
[features]
no-entrypoint = []
cpi = ["no-entrypoint"]
idl-build = ["anchor-lang/idl-build"]
[dependencies]
anchor-lang = { path = "../../../../lang" }

View File

@ -12,6 +12,7 @@ name = "errors"
[features]
no-entrypoint = []
cpi = ["no-entrypoint"]
idl-build = ["anchor-lang/idl-build"]
[dependencies]
anchor-lang = { path = "../../../../lang" }

View File

@ -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);

View File

@ -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" }

View File

@ -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"] }

View File

@ -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);
});

View File

@ -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" }

View File

@ -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"

View File

@ -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
anchor idl build -o $dir/relations.json

View File

@ -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"
]
}
}
]
}

File diff suppressed because it is too large Load Diff

View File

@ -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
}

View File

@ -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"
}
]
}
}
]
}

View File

@ -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"
}
]
}
}
]
}

View File

@ -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" }

View File

@ -1,131 +0,0 @@
use anchor_lang::prelude::*;
declare_id!("C1ient1nteractions1111111111111111111111111");
#[program]
pub mod client_interactions {
use super::*;
pub fn int(ctx: Context<Int>, 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<UnsignedInt>,
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>, enum_arg: MyEnum) -> Result<()> {
ctx.accounts.account.enum_field = enum_arg;
Ok(())
}
pub fn type_alias(
ctx: Context<TypeAlias>,
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,
}

View File

@ -14,6 +14,7 @@ no-entrypoint = []
no-idl = []
cpi = ["no-entrypoint"]
default = []
idl-build = ["anchor-lang/idl-build"]
[dependencies]
anchor-lang = { path = "../../../../lang" }

View File

@ -6,15 +6,26 @@ declare_id!("Externa1111111111111111111111111111111111111");
pub mod external {
use super::*;
pub fn initialize(_ctx: Context<Initialize>, _baz: Baz) -> Result<()> {
pub fn initialize(_ctx: Context<Initialize>) -> 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 {}

Some files were not shown because too many files have changed in this diff Show More