|
@@ -0,0 +1,438 @@
|
|
|
|
|
+// SPDX-License-Identifier: Apache-2.0
|
|
|
|
|
+
|
|
|
|
|
+use anchor_syn::idl::{Idl, IdlInstruction, IdlType, IdlTypeDefinitionTy};
|
|
|
|
|
+use clap::ArgMatches;
|
|
|
|
|
+use convert_case::{Boundary, Case, Casing};
|
|
|
|
|
+use serde_json::Value as JsonValue;
|
|
|
|
|
+use sha2::{Digest, Sha256};
|
|
|
|
|
+use solang_parser::lexer::is_keyword;
|
|
|
|
|
+use std::{
|
|
|
|
|
+ ffi::{OsStr, OsString},
|
|
|
|
|
+ fs::File,
|
|
|
|
|
+ io::Write,
|
|
|
|
|
+ path::PathBuf,
|
|
|
|
|
+ process::exit,
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+/// This subcommand generates a Solidity interface file from Anchor IDL file.
|
|
|
|
|
+/// The IDL file is json and lists all the instructions, events, structs, enums,
|
|
|
|
|
+/// etc. We have to avoid the numerous Solidity keywords, and retain any documentation.
|
|
|
|
|
+pub fn idl(matches: &ArgMatches) {
|
|
|
|
|
+ let files = matches.get_many::<OsString>("INPUT").unwrap();
|
|
|
|
|
+
|
|
|
|
|
+ let output = matches.get_one::<OsString>("OUTPUT").map(PathBuf::from);
|
|
|
|
|
+
|
|
|
|
|
+ for file in files {
|
|
|
|
|
+ idl_file(file, &output);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+fn idl_file(file: &OsStr, output: &Option<PathBuf>) {
|
|
|
|
|
+ let f = match File::open(file) {
|
|
|
|
|
+ Ok(s) => s,
|
|
|
|
|
+ Err(e) => {
|
|
|
|
|
+ eprintln!("{}: error: {}", file.to_string_lossy(), e);
|
|
|
|
|
+ exit(1);
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ let idl: Idl = match serde_json::from_reader(f) {
|
|
|
|
|
+ Ok(idl) => idl,
|
|
|
|
|
+ Err(e) => {
|
|
|
|
|
+ eprintln!("{}: error: {}", file.to_string_lossy(), e);
|
|
|
|
|
+ exit(1);
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ let filename = format!("{}.sol", idl.name);
|
|
|
|
|
+
|
|
|
|
|
+ let path = if let Some(base) = output {
|
|
|
|
|
+ base.join(filename)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ PathBuf::from(filename)
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ println!(
|
|
|
|
|
+ "{}: info: creating '{}'",
|
|
|
|
|
+ file.to_string_lossy(),
|
|
|
|
|
+ path.display()
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ let f = match File::create(&path) {
|
|
|
|
|
+ Ok(f) => f,
|
|
|
|
|
+ Err(e) => {
|
|
|
|
|
+ eprintln!("{}: error: {}", path.display(), e);
|
|
|
|
|
+ exit(1);
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ if let Err(e) = write_solidity(&idl, f) {
|
|
|
|
|
+ eprintln!("{}: error: {}", path.display(), e);
|
|
|
|
|
+ exit(1);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+fn write_solidity(idl: &Idl, mut f: File) -> Result<(), std::io::Error> {
|
|
|
|
|
+ if let Some(program_id) = program_id(idl) {
|
|
|
|
|
+ writeln!(
|
|
|
|
|
+ f,
|
|
|
|
|
+ "anchor_{} constant {} = anchor_{}(address'{}');\n",
|
|
|
|
|
+ idl.name, idl.name, idl.name, program_id
|
|
|
|
|
+ )?;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ let mut ty_names = idl
|
|
|
|
|
+ .types
|
|
|
|
|
+ .iter()
|
|
|
|
|
+ .map(|ty| (ty.name.to_string(), ty.name.to_string()))
|
|
|
|
|
+ .collect::<Vec<(String, String)>>();
|
|
|
|
|
+
|
|
|
|
|
+ if let Some(events) = &idl.events {
|
|
|
|
|
+ events
|
|
|
|
|
+ .iter()
|
|
|
|
|
+ .for_each(|event| ty_names.push((event.name.to_string(), event.name.to_string())));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ rename_keywords(&mut ty_names);
|
|
|
|
|
+
|
|
|
|
|
+ for ty_def in &idl.types {
|
|
|
|
|
+ if let IdlTypeDefinitionTy::Enum { variants } = &ty_def.ty {
|
|
|
|
|
+ if variants.iter().any(|variant| variant.fields.is_some()) {
|
|
|
|
|
+ eprintln!(
|
|
|
|
|
+ "enum {} has variants with fields, not supported in Solidity\n",
|
|
|
|
|
+ ty_def.name
|
|
|
|
|
+ );
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ let mut name_map = variants
|
|
|
|
|
+ .iter()
|
|
|
|
|
+ .map(|variant| (variant.name.to_string(), variant.name.to_string()))
|
|
|
|
|
+ .collect::<Vec<(String, String)>>();
|
|
|
|
|
+
|
|
|
|
|
+ rename_keywords(&mut name_map);
|
|
|
|
|
+
|
|
|
|
|
+ docs(&mut f, 0, &ty_def.docs)?;
|
|
|
|
|
+
|
|
|
|
|
+ let name = &ty_names.iter().find(|e| *e.0 == ty_def.name).unwrap().1;
|
|
|
|
|
+
|
|
|
|
|
+ writeln!(f, "enum {} {{", name)?;
|
|
|
|
|
+ let mut iter = variants.iter().enumerate();
|
|
|
|
|
+ let mut next = iter.next();
|
|
|
|
|
+ while let Some((no, _)) = next {
|
|
|
|
|
+ next = iter.next();
|
|
|
|
|
+
|
|
|
|
|
+ writeln!(
|
|
|
|
|
+ f,
|
|
|
|
|
+ "\t{}{}",
|
|
|
|
|
+ name_map[no].1,
|
|
|
|
|
+ if next.is_some() { "," } else { "" }
|
|
|
|
|
+ )?;
|
|
|
|
|
+ }
|
|
|
|
|
+ writeln!(f, "}}")?;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ for ty_def in &idl.types {
|
|
|
|
|
+ if let IdlTypeDefinitionTy::Struct { fields } = &ty_def.ty {
|
|
|
|
|
+ let badtys: Vec<String> = fields
|
|
|
|
|
+ .iter()
|
|
|
|
|
+ .filter_map(|field| idltype_to_solidity(&field.ty, &ty_names).err())
|
|
|
|
|
+ .collect();
|
|
|
|
|
+
|
|
|
|
|
+ if badtys.is_empty() {
|
|
|
|
|
+ let mut name_map = fields
|
|
|
|
|
+ .iter()
|
|
|
|
|
+ .map(|field| (field.name.to_string(), field.name.to_string()))
|
|
|
|
|
+ .collect::<Vec<(String, String)>>();
|
|
|
|
|
+
|
|
|
|
|
+ rename_keywords(&mut name_map);
|
|
|
|
|
+
|
|
|
|
|
+ docs(&mut f, 0, &ty_def.docs)?;
|
|
|
|
|
+
|
|
|
|
|
+ let name = &ty_names.iter().find(|e| *e.0 == ty_def.name).unwrap().1;
|
|
|
|
|
+
|
|
|
|
|
+ writeln!(f, "struct {} {{", name)?;
|
|
|
|
|
+
|
|
|
|
|
+ for (no, field) in fields.iter().enumerate() {
|
|
|
|
|
+ docs(&mut f, 1, &field.docs)?;
|
|
|
|
|
+
|
|
|
|
|
+ writeln!(
|
|
|
|
|
+ f,
|
|
|
|
|
+ "\t{}\t{};",
|
|
|
|
|
+ idltype_to_solidity(&field.ty, &ty_names).unwrap(),
|
|
|
|
|
+ name_map[no].1
|
|
|
|
|
+ )?;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ writeln!(f, "}}")?;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ eprintln!(
|
|
|
|
|
+ "struct {} has fields of type {} which is not supported on Solidity",
|
|
|
|
|
+ ty_def.name,
|
|
|
|
|
+ badtys.join(", ")
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if let Some(events) = &idl.events {
|
|
|
|
|
+ for event in events {
|
|
|
|
|
+ let badtys: Vec<String> = event
|
|
|
|
|
+ .fields
|
|
|
|
|
+ .iter()
|
|
|
|
|
+ .filter_map(|field| idltype_to_solidity(&field.ty, &ty_names).err())
|
|
|
|
|
+ .collect();
|
|
|
|
|
+
|
|
|
|
|
+ if badtys.is_empty() {
|
|
|
|
|
+ let mut name_map = event
|
|
|
|
|
+ .fields
|
|
|
|
|
+ .iter()
|
|
|
|
|
+ .map(|field| (field.name.to_string(), field.name.to_string()))
|
|
|
|
|
+ .collect::<Vec<(String, String)>>();
|
|
|
|
|
+
|
|
|
|
|
+ rename_keywords(&mut name_map);
|
|
|
|
|
+
|
|
|
|
|
+ let name = &ty_names.iter().find(|e| *e.0 == event.name).unwrap().1;
|
|
|
|
|
+
|
|
|
|
|
+ writeln!(f, "event {} {{", name)?;
|
|
|
|
|
+ let mut iter = event.fields.iter().enumerate();
|
|
|
|
|
+ let mut next = iter.next();
|
|
|
|
|
+ while let Some((no, e)) = next {
|
|
|
|
|
+ next = iter.next();
|
|
|
|
|
+
|
|
|
|
|
+ writeln!(
|
|
|
|
|
+ f,
|
|
|
|
|
+ "\t{}\t{}{}{}",
|
|
|
|
|
+ idltype_to_solidity(&e.ty, &ty_names).unwrap(),
|
|
|
|
|
+ if e.index { " indexed " } else { " " },
|
|
|
|
|
+ name_map[no].1,
|
|
|
|
|
+ if next.is_some() { "," } else { "" }
|
|
|
|
|
+ )?;
|
|
|
|
|
+ }
|
|
|
|
|
+ writeln!(f, "}}")?;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ eprintln!(
|
|
|
|
|
+ "event {} has fields of type {} which is not supported on Solidity",
|
|
|
|
|
+ event.name,
|
|
|
|
|
+ badtys.join(", ")
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ docs(&mut f, 0, &idl.docs)?;
|
|
|
|
|
+
|
|
|
|
|
+ writeln!(f, "interface anchor_{} {{", idl.name)?;
|
|
|
|
|
+
|
|
|
|
|
+ let mut instruction_names = idl
|
|
|
|
|
+ .instructions
|
|
|
|
|
+ .iter()
|
|
|
|
|
+ .map(|instr| (instr.name.to_string(), instr.name.to_string()))
|
|
|
|
|
+ .collect::<Vec<(String, String)>>();
|
|
|
|
|
+
|
|
|
|
|
+ if let Some(state) = &idl.state {
|
|
|
|
|
+ state.methods.iter().for_each(|instr| {
|
|
|
|
|
+ instruction_names.push((instr.name.to_string(), instr.name.to_string()))
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ rename_keywords(&mut instruction_names);
|
|
|
|
|
+
|
|
|
|
|
+ if let Some(state) = &idl.state {
|
|
|
|
|
+ for instr in &state.methods {
|
|
|
|
|
+ instruction(&mut f, instr, true, &instruction_names, &ty_names)?;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ for instr in &idl.instructions {
|
|
|
|
|
+ instruction(&mut f, instr, false, &instruction_names, &ty_names)?;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ writeln!(f, "}}")?;
|
|
|
|
|
+
|
|
|
|
|
+ Ok(())
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+fn instruction(
|
|
|
|
|
+ f: &mut File,
|
|
|
|
|
+ instr: &IdlInstruction,
|
|
|
|
|
+ state: bool,
|
|
|
|
|
+ instruction_names: &[(String, String)],
|
|
|
|
|
+ ty_names: &[(String, String)],
|
|
|
|
|
+) -> std::io::Result<()> {
|
|
|
|
|
+ let mut badtys: Vec<String> = instr
|
|
|
|
|
+ .args
|
|
|
|
|
+ .iter()
|
|
|
|
|
+ .filter_map(|field| idltype_to_solidity(&field.ty, ty_names).err())
|
|
|
|
|
+ .collect();
|
|
|
|
|
+
|
|
|
|
|
+ if let Some(ty) = &instr.returns {
|
|
|
|
|
+ if let Err(s) = idltype_to_solidity(ty, ty_names) {
|
|
|
|
|
+ badtys.push(s);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if badtys.is_empty() {
|
|
|
|
|
+ docs(f, 1, &instr.docs)?;
|
|
|
|
|
+
|
|
|
|
|
+ let name = &instruction_names
|
|
|
|
|
+ .iter()
|
|
|
|
|
+ .find(|e| *e.0 == instr.name)
|
|
|
|
|
+ .unwrap()
|
|
|
|
|
+ .1;
|
|
|
|
|
+
|
|
|
|
|
+ write!(
|
|
|
|
|
+ f,
|
|
|
|
|
+ "\tfunction {}(",
|
|
|
|
|
+ if instr.name == "new" {
|
|
|
|
|
+ "initialize"
|
|
|
|
|
+ } else {
|
|
|
|
|
+ name
|
|
|
|
|
+ }
|
|
|
|
|
+ )?;
|
|
|
|
|
+
|
|
|
|
|
+ let mut iter = instr.args.iter();
|
|
|
|
|
+ let mut next = iter.next();
|
|
|
|
|
+
|
|
|
|
|
+ while let Some(e) = next {
|
|
|
|
|
+ next = iter.next();
|
|
|
|
|
+
|
|
|
|
|
+ write!(
|
|
|
|
|
+ f,
|
|
|
|
|
+ "{} {}{}",
|
|
|
|
|
+ idltype_to_solidity(&e.ty, ty_names).unwrap(),
|
|
|
|
|
+ e.name,
|
|
|
|
|
+ if next.is_some() { "," } else { "" }
|
|
|
|
|
+ )?;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // The anchor discriminator is what Solidity calls a selector
|
|
|
|
|
+ let selector = discriminator(if state { "state" } else { "global" }, &instr.name);
|
|
|
|
|
+
|
|
|
|
|
+ write!(
|
|
|
|
|
+ f,
|
|
|
|
|
+ ") selector=hex\"{}\" {}external",
|
|
|
|
|
+ hex::encode(selector),
|
|
|
|
|
+ if state { "" } else { "view " }
|
|
|
|
|
+ )?;
|
|
|
|
|
+
|
|
|
|
|
+ if let Some(ty) = &instr.returns {
|
|
|
|
|
+ writeln!(
|
|
|
|
|
+ f,
|
|
|
|
|
+ " returns ({});",
|
|
|
|
|
+ idltype_to_solidity(ty, ty_names).unwrap()
|
|
|
|
|
+ )?;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ writeln!(f, ";")?;
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ eprintln!(
|
|
|
|
|
+ "instructions {} has arguments of type {} which is not supported on Solidity",
|
|
|
|
|
+ instr.name,
|
|
|
|
|
+ badtys.join(", ")
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ Ok(())
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+fn docs(f: &mut File, indent: usize, docs: &Option<Vec<String>>) -> std::io::Result<()> {
|
|
|
|
|
+ if let Some(docs) = docs {
|
|
|
|
|
+ for doc in docs {
|
|
|
|
|
+ for _ in 0..indent {
|
|
|
|
|
+ write!(f, "\t")?;
|
|
|
|
|
+ }
|
|
|
|
|
+ writeln!(f, "/// {}", doc)?;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ Ok(())
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/// Generate discriminator based on the name of the function. This is the 8 byte
|
|
|
|
|
+/// value anchor uses to dispatch function calls on. This should match
|
|
|
|
|
+/// anchor's behaviour - we need to match the discriminator exactly
|
|
|
|
|
+fn discriminator(namespace: &'static str, name: &str) -> Vec<u8> {
|
|
|
|
|
+ let mut hasher = Sha256::new();
|
|
|
|
|
+ // must match snake-case npm library, see
|
|
|
|
|
+ // https://github.com/coral-xyz/anchor/blob/master/ts/packages/anchor/src/coder/borsh/instruction.ts#L389
|
|
|
|
|
+ let normalized = name
|
|
|
|
|
+ .from_case(Case::Camel)
|
|
|
|
|
+ .without_boundaries(&[Boundary::LowerDigit])
|
|
|
|
|
+ .to_case(Case::Snake);
|
|
|
|
|
+ hasher.update(format!("{}:{}", namespace, normalized));
|
|
|
|
|
+ hasher.finalize()[..8].to_vec()
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+fn idltype_to_solidity(ty: &IdlType, ty_names: &[(String, String)]) -> Result<String, String> {
|
|
|
|
|
+ match ty {
|
|
|
|
|
+ IdlType::Bool => Ok("bool".to_string()),
|
|
|
|
|
+ IdlType::U8 => Ok("uint8".to_string()),
|
|
|
|
|
+ IdlType::I8 => Ok("int8".to_string()),
|
|
|
|
|
+ IdlType::U16 => Ok("uint16".to_string()),
|
|
|
|
|
+ IdlType::I16 => Ok("int16".to_string()),
|
|
|
|
|
+ IdlType::U32 => Ok("uint32".to_string()),
|
|
|
|
|
+ IdlType::I32 => Ok("int32".to_string()),
|
|
|
|
|
+ IdlType::U64 => Ok("uint64".to_string()),
|
|
|
|
|
+ IdlType::I64 => Ok("int64".to_string()),
|
|
|
|
|
+ IdlType::U128 => Ok("uint128".to_string()),
|
|
|
|
|
+ IdlType::I128 => Ok("int128".to_string()),
|
|
|
|
|
+ IdlType::F32 => Err("f32".to_string()),
|
|
|
|
|
+ IdlType::F64 => Err("f64".to_string()),
|
|
|
|
|
+ IdlType::Bytes => Ok("bytes".to_string()),
|
|
|
|
|
+ IdlType::String => Ok("string".to_string()),
|
|
|
|
|
+ IdlType::PublicKey => Ok("address".to_string()),
|
|
|
|
|
+ IdlType::Option(ty) => Err(format!(
|
|
|
|
|
+ "Option({})",
|
|
|
|
|
+ match idltype_to_solidity(ty, ty_names) {
|
|
|
|
|
+ Ok(ty) => ty,
|
|
|
|
|
+ Err(ty) => ty,
|
|
|
|
|
+ }
|
|
|
|
|
+ )),
|
|
|
|
|
+ IdlType::Defined(ty) => {
|
|
|
|
|
+ if let Some(e) = ty_names.iter().find(|rename| rename.0 == *ty) {
|
|
|
|
|
+ Ok(e.1.to_owned())
|
|
|
|
|
+ } else {
|
|
|
|
|
+ Ok(ty.into())
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ IdlType::Vec(ty) => match idltype_to_solidity(ty, ty_names) {
|
|
|
|
|
+ Ok(ty) => Ok(format!("{}[]", ty)),
|
|
|
|
|
+ Err(ty) => Err(format!("{}[]", ty)),
|
|
|
|
|
+ },
|
|
|
|
|
+ IdlType::Array(ty, size) => match idltype_to_solidity(ty, ty_names) {
|
|
|
|
|
+ Ok(ty) => Ok(format!("{}[{}]", ty, size)),
|
|
|
|
|
+ Err(ty) => Err(format!("{}[{}]", ty, size)),
|
|
|
|
|
+ },
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+fn program_id(idl: &Idl) -> Option<&String> {
|
|
|
|
|
+ if let Some(JsonValue::Object(metadata)) = &idl.metadata {
|
|
|
|
|
+ if let Some(JsonValue::String(address)) = metadata.get("address") {
|
|
|
|
|
+ return Some(address);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ None
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/// There are many keywords in Solidity which are not keywords in Rust, so they may
|
|
|
|
|
+/// occur as field name, function name, etc. Rename those fields by prepending
|
|
|
|
|
+/// underscores until unique
|
|
|
|
|
+fn rename_keywords(name_map: &mut Vec<(String, String)>) {
|
|
|
|
|
+ for i in 0..name_map.len() {
|
|
|
|
|
+ let name = &name_map[i].0;
|
|
|
|
|
+
|
|
|
|
|
+ if is_keyword(name) {
|
|
|
|
|
+ let mut name = name.to_owned();
|
|
|
|
|
+ loop {
|
|
|
|
|
+ name = format!("_{}", name);
|
|
|
|
|
+ if name_map.iter().all(|(_, n)| *n != name) {
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ name_map[i].1 = name;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|