Bläddra i källkod

Create Solidity interface file from Anchor idl

solang metadata --target solana <idl-file>

Signed-off-by: Sean Young <sean@mess.org>
Sean Young 3 år sedan
förälder
incheckning
3aaf579972
5 ändrade filer med 481 tillägg och 13 borttagningar
  1. 2 0
      Cargo.toml
  2. 5 0
      solang-parser/src/lexer.rs
  3. 2 0
      src/bin/doc/mod.rs
  4. 438 0
      src/bin/idl/mod.rs
  5. 34 13
      src/bin/solang.rs

+ 2 - 0
Cargo.toml

@@ -51,6 +51,8 @@ codespan-reporting = "0.11"
 phf = "0.10.1"
 rust-lapper = "1.0"
 bitflags = "1.3"
+anchor-syn = { version = "0.25", features = ["idl"] }
+convert_case = "0.5"
 
 [dev-dependencies]
 num-derive = "0.3"

+ 5 - 0
solang-parser/src/lexer.rs

@@ -384,6 +384,11 @@ impl CodeLocation for LexicalError {
     }
 }
 
+/// Is this word a keyword in Solidity
+pub fn is_keyword(word: &str) -> bool {
+    KEYWORDS.contains_key(word)
+}
+
 static KEYWORDS: phf::Map<&'static str, Token> = phf_map! {
     "address" => Token::Address,
     "anonymous" => Token::Anonymous,

+ 2 - 0
src/bin/doc/mod.rs

@@ -153,6 +153,8 @@ fn get_tag_no<'a>(name: &str, no: usize, tags: &'a [ast::Tag]) -> Option<&'a str
         .map(|e| &e.value as &str)
 }
 
+/// Generate documentation from the doccomments. This may be replaced with force-doc
+/// one day (once it exists)
 pub fn generate_docs(outdir: &str, files: &[ast::Namespace], verbose: bool) {
     let mut top = Top {
         contracts: Vec::new(),

+ 438 - 0
src/bin/idl/mod.rs

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

+ 34 - 13
src/bin/solang.rs

@@ -22,9 +22,11 @@ use std::{
     fs::{create_dir_all, File},
     io::prelude::*,
     path::{Path, PathBuf},
+    process::exit,
 };
 
 mod doc;
+mod idl;
 mod languageserver;
 
 fn main() {
@@ -272,6 +274,24 @@ fn main() {
                             .action(ArgAction::Append),
                     ),
             )
+            .subcommand(
+                Command::new("idl")
+                    .about("Generate Solidity interface files from Anchor IDL files")
+                    .arg(
+                        Arg::new("INPUT")
+                            .help("Convert IDL files")
+                            .required(true)
+                            .value_parser(ValueParser::os_string())
+                            .multiple_values(true),
+                    )
+                    .arg(
+                        Arg::new("OUTPUT")
+                            .help("output file")
+                            .short('o')
+                            .long("output")
+                            .takes_value(true),
+                    ),
+            )
             .subcommand(
                 Command::new("shell-complete")
                     .about("Print shell completion for various shells to STDOUT")
@@ -293,6 +313,7 @@ fn main() {
         }
         Some(("compile", matches)) => compile(matches),
         Some(("doc", matches)) => doc(matches),
+        Some(("idl", matches)) => idl::idl(matches),
         Some(("shell-complete", matches)) => shell_complete(app(), matches),
         _ => unreachable!(),
     }
@@ -391,16 +412,16 @@ fn compile(matches: &ArgMatches) {
     let namespaces = namespaces.iter().collect::<Vec<_>>();
 
     if let Some("ast-dot") = matches.get_one::<String>("EMIT").map(|v| v.as_str()) {
-        std::process::exit(0);
+        exit(0);
     }
 
     if errors {
         if matches.contains_id("STD-JSON") {
             println!("{}", serde_json::to_string(&json).unwrap());
-            std::process::exit(0);
+            exit(0);
         } else {
             eprintln!("error: not all contracts are valid");
-            std::process::exit(1);
+            exit(1);
         }
     }
 
@@ -543,7 +564,7 @@ fn process_file(
 
         if let Err(err) = file.write_all(dot.as_bytes()) {
             eprintln!("{}: error: {}", dot_filename.display(), err);
-            std::process::exit(1);
+            exit(1);
         }
 
         return Ok(ns);
@@ -700,7 +721,7 @@ fn save_intermediates(binary: &solang::emit::binary::Binary, matches: &ArgMatche
                 Ok(o) => o,
                 Err(s) => {
                     println!("error: {}", s);
-                    std::process::exit(1);
+                    exit(1);
                 }
             };
 
@@ -723,7 +744,7 @@ fn save_intermediates(binary: &solang::emit::binary::Binary, matches: &ArgMatche
                 Ok(o) => o,
                 Err(s) => {
                     println!("error: {}", s);
-                    std::process::exit(1);
+                    exit(1);
                 }
             };
 
@@ -755,7 +776,7 @@ fn create_file(path: &Path) -> File {
                 parent.display(),
                 err
             );
-            std::process::exit(1);
+            exit(1);
         }
     }
 
@@ -763,7 +784,7 @@ fn create_file(path: &Path) -> File {
         Ok(file) => file,
         Err(err) => {
             eprintln!("error: cannot create file '{}': {}", path.display(), err,);
-            std::process::exit(1);
+            exit(1);
         }
     }
 }
@@ -790,7 +811,7 @@ fn target_arg(matches: &ArgMatches) -> Target {
             "error: address length cannot be modified for target '{}'",
             target
         );
-        std::process::exit(1);
+        exit(1);
     }
 
     if !target.is_substrate()
@@ -800,7 +821,7 @@ fn target_arg(matches: &ArgMatches) -> Target {
             "error: value length cannot be modified for target '{}'",
             target
         );
-        std::process::exit(1);
+        exit(1);
     }
 
     target
@@ -817,14 +838,14 @@ fn imports_arg(matches: &ArgMatches) -> FileResolver {
 
     if let Err(e) = resolver.add_import_path(&PathBuf::from(".")) {
         eprintln!("error: cannot add current directory to import path: {}", e);
-        std::process::exit(1);
+        exit(1);
     }
 
     if let Some(paths) = matches.get_many::<PathBuf>("IMPORTPATH") {
         for path in paths {
             if let Err(e) = resolver.add_import_path(path) {
                 eprintln!("error: import path '{}': {}", path.to_string_lossy(), e);
-                std::process::exit(1);
+                exit(1);
             }
         }
     }
@@ -833,7 +854,7 @@ fn imports_arg(matches: &ArgMatches) -> FileResolver {
         for (map, path) in maps {
             if let Err(e) = resolver.add_import_map(OsString::from(map), path.clone()) {
                 eprintln!("error: import path '{}': {}", path.display(), e);
-                std::process::exit(1);
+                exit(1);
             }
         }
     }