Skip to content

Commit

Permalink
cli: Add idl type command (#3017)
Browse files Browse the repository at this point in the history
  • Loading branch information
acheroncrypto authored Jun 10, 2024
1 parent 29a6558 commit 8528092
Show file tree
Hide file tree
Showing 3 changed files with 64 additions and 46 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ The minor version will be incremented upon a breaking change and the patch versi
- spl: Export `spl-associated-token-account` crate ([#2999](https://github.com/coral-xyz/anchor/pull/2999)).
- lang: Support legacy IDLs with `declare_program!` ([#2997](https://github.com/coral-xyz/anchor/pull/2997)).
- cli: Add `idl convert` command ([#3009](https://github.com/coral-xyz/anchor/pull/3009)).
- cli: Add `idl type` command ([#3017](https://github.com/coral-xyz/anchor/pull/3017)).

### Fixes

Expand Down
69 changes: 62 additions & 7 deletions cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use flate2::read::GzDecoder;
use flate2::read::ZlibDecoder;
use flate2::write::{GzEncoder, ZlibEncoder};
use flate2::Compression;
use heck::{ToKebabCase, ToSnakeCase};
use heck::{ToKebabCase, ToLowerCamelCase, ToPascalCase, ToSnakeCase};
use regex::{Regex, RegexBuilder};
use reqwest::blocking::multipart::{Form, Part};
use reqwest::blocking::Client;
Expand Down Expand Up @@ -484,15 +484,23 @@ pub enum IdlCommand {
/// The address can be a program, IDL account, or IDL buffer.
Fetch {
address: Pubkey,
/// Output file for the idl (stdout if not specified).
/// Output file for the IDL (stdout if not specified).
#[clap(short, long)]
out: Option<String>,
},
/// Convert legacy IDLs (pre Anchor 0.30) to the new IDL spec
Convert {
/// Path to the IDL file
path: String,
/// Output file for the idl (stdout if not specified)
/// Output file for the IDL (stdout if not specified)
#[clap(short, long)]
out: Option<String>,
},
/// Generate TypeScript type for the IDL
Type {
/// Path to the IDL file
path: String,
/// Output file for the IDL (stdout if not specified)
#[clap(short, long)]
out: Option<String>,
},
Expand Down Expand Up @@ -1534,7 +1542,7 @@ fn build_cwd_verifiable(
// Write out the TypeScript type.
println!("Writing the .ts file");
let ts_file = workspace_dir.join(format!("target/types/{}.ts", idl.metadata.name));
fs::write(&ts_file, rust_template::idl_ts(&idl)?)?;
fs::write(&ts_file, idl_ts(&idl)?)?;

// Copy out the TypeScript type.
if !&cfg.workspace.types.is_empty() {
Expand Down Expand Up @@ -1842,7 +1850,7 @@ fn _build_rust_cwd(
// Write out the JSON file.
write_idl(&idl, OutFile::File(out))?;
// Write out the TypeScript type.
fs::write(&ts_out, rust_template::idl_ts(&idl)?)?;
fs::write(&ts_out, idl_ts(&idl)?)?;

// Copy out the TypeScript type.
let cfg_parent = cfg.path().parent().expect("Invalid Anchor.toml");
Expand Down Expand Up @@ -1920,7 +1928,7 @@ fn _build_solidity_cwd(
};

// Write out the TypeScript type.
fs::write(&ts_out, rust_template::idl_ts(&idl)?)?;
fs::write(&ts_out, idl_ts(&idl)?)?;
// Copy out the TypeScript type.
let cfg_parent = cfg.path().parent().expect("Invalid Anchor.toml");
if !&cfg.workspace.types.is_empty() {
Expand Down Expand Up @@ -2209,6 +2217,7 @@ fn idl(cfg_override: &ConfigOverride, subcmd: IdlCommand) -> Result<()> {
} => idl_build(cfg_override, program_name, out, out_ts, no_docs, skip_lint),
IdlCommand::Fetch { address, out } => idl_fetch(cfg_override, address, out),
IdlCommand::Convert { path, out } => idl_convert(path, out),
IdlCommand::Type { path, out } => idl_type(path, out),
}
}

Expand Down Expand Up @@ -2673,7 +2682,7 @@ fn idl_build(
write_idl(&idl, out)?;

if let Some(path) = out_ts {
fs::write(path, rust_template::idl_ts(&idl)?)?;
fs::write(path, idl_ts(&idl)?)?;
}

Ok(())
Expand Down Expand Up @@ -2733,6 +2742,52 @@ fn idl_convert(path: String, out: Option<String>) -> Result<()> {
write_idl(&idl, out)
}

fn idl_type(path: String, out: Option<String>) -> Result<()> {
let idl = fs::read(path)?;
let idl = Idl::from_slice_with_conversion(&idl)?;
let types = idl_ts(&idl)?;
match out {
Some(out) => fs::write(out, types)?,
_ => println!("{types}"),
};
Ok(())
}

fn idl_ts(idl: &Idl) -> Result<String> {
let idl_name = &idl.metadata.name;
let type_name = idl_name.to_pascal_case();
let idl = serde_json::to_string(idl)?;

// Convert every field of the IDL to camelCase
let camel_idl = Regex::new(r#""\w+":"([\w\d]+)""#)?
.captures_iter(&idl)
.fold(idl.clone(), |acc, cur| {
let name = cur.get(1).unwrap().as_str();

// Do not modify pubkeys
if Pubkey::from_str(name).is_ok() {
return acc;
}

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};
"#
))
}

fn write_idl(idl: &Idl, out: OutFile) -> Result<()> {
let idl_json = serde_json::to_string_pretty(idl)?;
match out {
Expand Down
40 changes: 1 addition & 39 deletions cli/src/rust_template.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,9 @@ use crate::{
config::ProgramWorkspace, create_files, override_or_create_files, solidity_template, Files,
VERSION,
};
use anchor_lang_idl::types::Idl;
use anyhow::Result;
use clap::{Parser, ValueEnum};
use heck::{ToLowerCamelCase, ToPascalCase, ToSnakeCase};
use regex::Regex;
use heck::{ToPascalCase, ToSnakeCase};
use solana_sdk::{
pubkey::Pubkey,
signature::{read_keypair_file, write_keypair_file, Keypair},
Expand All @@ -18,7 +16,6 @@ use std::{
io::Write as _,
path::Path,
process::Stdio,
str::FromStr,
};

/// Program initialization template
Expand Down Expand Up @@ -232,41 +229,6 @@ token = "{token}"
)
}

pub fn idl_ts(idl: &Idl) -> Result<String> {
let idl_name = &idl.metadata.name;
let type_name = idl_name.to_pascal_case();
let idl = serde_json::to_string(idl)?;

// Convert every field of the IDL to camelCase
let camel_idl = Regex::new(r#""\w+":"([\w\d]+)""#)?
.captures_iter(&idl)
.fold(idl.clone(), |acc, cur| {
let name = cur.get(1).unwrap().as_str();

// Do not modify pubkeys
if Pubkey::from_str(name).is_ok() {
return acc;
}

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};
"#
))
}

pub fn deploy_js_script_host(cluster_url: &str, script_path: &str) -> String {
format!(
r#"
Expand Down

0 comments on commit 8528092

Please sign in to comment.