diff --git a/Cargo.lock b/Cargo.lock index f19cab2344..a8fb158eb4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -187,6 +187,7 @@ dependencies = [ "solana-faucet", "solana-program", "solana-sdk", + "solang-parser", "syn 1.0.109", "tar", "tokio", @@ -413,6 +414,15 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eab1c04a571841102f5345a8fc0f6bb3d31c315dec879b5c6e42e40ce7ffa34e" +[[package]] +name = "ascii-canvas" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8824ecca2e851cec16968d54a01dd372ef8f95b244fb84b84e70128be347c3c6" +dependencies = [ + "term", +] + [[package]] name = "asn1-rs" version = "0.5.1" @@ -498,7 +508,7 @@ version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" dependencies = [ - "hermit-abi", + "hermit-abi 0.1.19", "libc", "winapi", ] @@ -536,6 +546,21 @@ dependencies = [ "serde", ] +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + [[package]] name = "bitflags" version = "1.3.2" @@ -1194,6 +1219,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + [[package]] name = "digest" version = "0.9.0" @@ -1336,6 +1367,15 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" +[[package]] +name = "ena" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c533630cf40e9caa44bd91aadc88a75d75a4c3a12b4cfde353cbed41daa1e1f1" +dependencies = [ + "log", +] + [[package]] name = "encode_unicode" version = "0.3.6" @@ -1416,6 +1456,27 @@ dependencies = [ "termcolor", ] +[[package]] +name = "errno" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" +dependencies = [ + "errno-dragonfly", + "libc", + "winapi", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + [[package]] name = "event-listener" version = "2.5.3" @@ -1459,6 +1520,12 @@ dependencies = [ "windows-sys 0.42.0", ] +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + [[package]] name = "flate2" version = "1.0.24" @@ -1701,6 +1768,15 @@ dependencies = [ "libc", ] +[[package]] +name = "hermit-abi" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" +dependencies = [ + "libc", +] + [[package]] name = "histogram" version = "0.6.9" @@ -1910,12 +1986,34 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "io-lifetimes" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7d6c6f8c91b4b9ed43484ad1a938e393caf35960fce7f82a040497207bd8e9e" +dependencies = [ + "libc", + "windows-sys 0.42.0", +] + [[package]] name = "ipnet" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "879d54834c8c76457ef4293a689b2a8c59b076067ad77b15efafbb05f92a592b" +[[package]] +name = "is-terminal" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dfb6c8100ccc63462345b67d1bbc3679177c75ee4bf59bf29c8b1d110b8189" +dependencies = [ + "hermit-abi 0.2.6", + "io-lifetimes", + "rustix", + "windows-sys 0.42.0", +] + [[package]] name = "itertools" version = "0.9.0" @@ -1979,6 +2077,38 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9b7d56ba4a8344d6be9729995e6b06f928af29998cdf79fe390cbf6b1fee838" +[[package]] +name = "lalrpop" +version = "0.19.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f34313ec00c2eb5c3c87ca6732ea02dcf3af99c3ff7a8fb622ffb99c9d860a87" +dependencies = [ + "ascii-canvas", + "bit-set", + "diff", + "ena", + "is-terminal", + "itertools 0.10.5", + "lalrpop-util", + "petgraph", + "pico-args", + "regex", + "regex-syntax", + "string_cache", + "term", + "tiny-keccak", + "unicode-xid 0.2.4", +] + +[[package]] +name = "lalrpop-util" +version = "0.19.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5c1f7869c94d214466c5fd432dfed12c379fd87786768d36455892d46b18edd" +dependencies = [ + "regex", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -2058,6 +2188,12 @@ dependencies = [ "cc", ] +[[package]] +name = "linux-raw-sys" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" + [[package]] name = "lock_api" version = "0.4.9" @@ -2225,6 +2361,12 @@ dependencies = [ "spl-token", ] +[[package]] +name = "new_debug_unreachable" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" + [[package]] name = "nix" version = "0.25.1" @@ -2354,7 +2496,7 @@ version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" dependencies = [ - "hermit-abi", + "hermit-abi 0.1.19", "libc", ] @@ -2514,6 +2656,73 @@ dependencies = [ "ucd-trie", ] +[[package]] +name = "petgraph" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dd7d28ee937e54fe3080c91faa1c3a46c06de6252988a7f4592ba2310ef22a4" +dependencies = [ + "fixedbitset", + "indexmap", +] + +[[package]] +name = "phf" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928c6535de93548188ef63bb7c4036bd415cd8f36ad25af44b9789b2ee72a48c" +dependencies = [ + "phf_macros", + "phf_shared 0.11.1", +] + +[[package]] +name = "phf_generator" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1181c94580fa345f50f19d738aaa39c0ed30a600d95cb2d3e23f94266f14fbf" +dependencies = [ + "phf_shared 0.11.1", + "rand 0.8.5", +] + +[[package]] +name = "phf_macros" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92aacdc5f16768709a569e913f7451034034178b05bdc8acda226659a3dccc66" +dependencies = [ + "phf_generator", + "phf_shared 0.11.1", + "proc-macro2 1.0.47", + "quote 1.0.21", + "syn 1.0.109", +] + +[[package]] +name = "phf_shared" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" +dependencies = [ + "siphasher", +] + +[[package]] +name = "phf_shared" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1fb5f6f826b772a8d4c0394209441e7d37cbbb967ae9c7e0e8134365c9ee676" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pico-args" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db8bcd96cb740d03149cbad5518db9fd87126a10ab519c011893b1754134c468" + [[package]] name = "pin-project-lite" version = "0.2.9" @@ -2576,6 +2785,12 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + [[package]] name = "proc-macro-crate" version = "0.1.5" @@ -3016,6 +3231,20 @@ dependencies = [ "nom", ] +[[package]] +name = "rustix" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fdebc4b395b7fbb9ab11e462e20ed9051e7b16e42d24042c776eca0ac81b03" +dependencies = [ + "bitflags", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys", + "windows-sys 0.42.0", +] + [[package]] name = "rustls" version = "0.20.7" @@ -3400,6 +3629,12 @@ version = "1.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" +[[package]] +name = "siphasher" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de" + [[package]] name = "sized-chunks" version = "0.6.5" @@ -4232,6 +4467,19 @@ dependencies = [ "winapi", ] +[[package]] +name = "solang-parser" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff87dae6cdccacdbf3b19e99b271083556e808de0f59c74a01482f64fdbc61fc" +dependencies = [ + "itertools 0.10.5", + "lalrpop", + "lalrpop-util", + "phf", + "unicode-xid 0.2.4", +] + [[package]] name = "spin" version = "0.5.2" @@ -4312,6 +4560,19 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "string_cache" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f91138e76242f575eb1d3b38b4f1362f10d3a43f47d182a5b359af488a02293b" +dependencies = [ + "new_debug_unreachable", + "once_cell", + "parking_lot", + "phf_shared 0.10.0", + "precomputed-hash", +] + [[package]] name = "strsim" version = "0.8.0" @@ -4389,6 +4650,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "term" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" +dependencies = [ + "dirs-next", + "rustversion", + "winapi", +] + [[package]] name = "termcolor" version = "1.1.3" @@ -4502,6 +4774,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + [[package]] name = "tinyvec" version = "1.6.0" diff --git a/cli/Cargo.toml b/cli/Cargo.toml index ff0942fd01..6daa019eca 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -26,6 +26,7 @@ anchor-syn = { path = "../lang/syn", features = ["idl", "init-if-needed"], versi serde_json = "1.0" shellexpand = "2.1.0" toml = "0.5.8" +solang-parser = "=0.2.3" semver = "1.0.4" serde = { version = "1.0.122", features = ["derive"] } solana-sdk = "1.14.16" diff --git a/cli/src/config.rs b/cli/src/config.rs index 7014e27b19..1b4ae8a4d4 100644 --- a/cli/src/config.rs +++ b/cli/src/config.rs @@ -1,7 +1,7 @@ use crate::is_hidden; use anchor_client::Cluster; use anchor_syn::idl::Idl; -use anyhow::{anyhow, Context, Error, Result}; +use anyhow::{anyhow, bail, Context, Error, Result}; use clap::{Parser, ValueEnum}; use heck::ToSnakeCase; use reqwest::Url; @@ -10,8 +10,10 @@ use serde::{Deserialize, Deserializer, Serialize}; use solana_cli_config::{Config as SolanaConfig, CONFIG_FILE}; use solana_sdk::pubkey::Pubkey; use solana_sdk::signature::{Keypair, Signer}; +use solang_parser::pt::{ContractTy, SourceUnitPart}; use std::collections::{BTreeMap, HashMap}; use std::convert::TryFrom; +use std::ffi::OsStr; use std::fs::{self, File}; use std::io::prelude::*; use std::marker::PhantomData; @@ -145,7 +147,7 @@ impl Deref for Manifest { } impl WithPath { - pub fn get_program_list(&self) -> Result> { + pub fn get_rust_program_list(&self) -> Result> { // Canonicalize the workspace filepaths to compare with relative paths. let (members, exclude) = self.canonicalize_workspace()?; @@ -156,12 +158,16 @@ impl WithPath { let program_paths: Vec = { if members.is_empty() { let path = self.path().parent().unwrap().join("programs"); - fs::read_dir(path)? - .filter(|entry| entry.as_ref().map(|e| e.path().is_dir()).unwrap_or(false)) - .map(|dir| dir.map(|d| d.path().canonicalize().unwrap())) - .collect::>>() - .into_iter() - .collect::, std::io::Error>>()? + if let Ok(entries) = fs::read_dir(path) { + entries + .filter(|entry| entry.as_ref().map(|e| e.path().is_dir()).unwrap_or(false)) + .map(|dir| dir.map(|d| d.path().canonicalize().unwrap())) + .collect::>>() + .into_iter() + .collect::, std::io::Error>>()? + } else { + Vec::new() + } } else { members } @@ -174,9 +180,56 @@ impl WithPath { .collect()) } + /// Parse all the files with the .sol extension, and get a list of the all + /// contracts defined in them along with their path. One Solidity file may + /// define multiple contracts. + pub fn get_solidity_program_list(&self) -> Result> { + let path = self.path().parent().unwrap().join("solidity"); + let mut res = Vec::new(); + + if let Ok(entries) = fs::read_dir(path) { + for entry in entries { + let path = entry?.path(); + + if !path.is_file() || path.extension() != Some(OsStr::new("sol")) { + continue; + } + + let source = fs::read_to_string(&path)?; + + let tree = match solang_parser::parse(&source, 0) { + Ok((tree, _)) => tree, + Err(diag) => { + // The parser can return multiple errors, however this is exceedingly rare. + // Just use the first one, else the formatting will be a mess. + bail!( + "{}: {}: {}", + path.display(), + diag[0].level.to_string(), + diag[0].message + ); + } + }; + + tree.0.iter().for_each(|part| { + if let SourceUnitPart::ContractDefinition(contract) = part { + // Must be a contract, not library/interface/abstract contract + if matches!(&contract.ty, ContractTy::Contract(..)) { + if let Some(name) = &contract.name { + res.push((name.name.clone(), path.clone())); + } + } + } + }); + } + } + + Ok(res) + } + pub fn read_all_programs(&self) -> Result> { let mut r = vec![]; - for path in self.get_program_list()? { + for path in self.get_rust_program_list()? { let cargo = Manifest::from_path(path.join("Cargo.toml"))?; let lib_name = cargo.lib_name()?; @@ -188,6 +241,21 @@ impl WithPath { r.push(Program { lib_name, + solidity: false, + path, + idl, + }); + } + for (lib_name, path) in self.get_solidity_program_list()? { + let idl_filepath = format!("target/idl/{lib_name}.json"); + let idl = fs::read(idl_filepath) + .ok() + .map(|bytes| serde_json::from_reader(&*bytes)) + .transpose()?; + + r.push(Program { + lib_name, + solidity: true, path, idl, }); @@ -1126,7 +1194,8 @@ impl Merge for _Validator { #[derive(Debug, Clone)] pub struct Program { pub lib_name: String, - // Canonicalized path to the program directory. + pub solidity: bool, + // Canonicalized path to the program directory or Solidity source file pub path: PathBuf, pub idl: Option, } diff --git a/cli/src/lib.rs b/cli/src/lib.rs index 75475189e1..0f70c8fcf2 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -46,7 +46,8 @@ use tar::Archive; pub mod config; mod path; -pub mod template; +pub mod rust_template; +pub mod solidity_template; // Version of the docker image. pub const VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -68,6 +69,8 @@ pub enum Command { name: String, #[clap(short, long)] javascript: bool, + #[clap(short, long)] + solidity: bool, #[clap(long)] no_git: bool, #[clap(long)] @@ -200,7 +203,11 @@ pub enum Command { cargo_args: Vec, }, /// Creates a new program. - New { name: String }, + New { + #[clap(short, long)] + solidity: bool, + name: String, + }, /// Commands for interacting with interface definitions. Idl { #[clap(subcommand)] @@ -214,7 +221,7 @@ pub enum Command { #[clap(short, long)] program_name: Option, /// Keypair of the program (filepath) (requires program-name) - #[clap(long, requires = "program-name")] + #[clap(long, requires = "program_name")] program_keypair: Option, }, /// Runs the deploy migration script. @@ -412,10 +419,11 @@ pub fn entry(opts: Opts) -> Result<()> { Command::Init { name, javascript, + solidity, no_git, jest, - } => init(&opts.cfg_override, name, javascript, no_git, jest), - Command::New { name } => new(&opts.cfg_override, name), + } => init(&opts.cfg_override, name, javascript, solidity, no_git, jest), + Command::New { solidity, name } => new(&opts.cfg_override, solidity, name), Command::Build { idl, idl_ts, @@ -559,6 +567,7 @@ fn init( cfg_override: &ConfigOverride, name: String, javascript: bool, + solidity: bool, no_git: bool, jest: bool, ) -> Result<()> { @@ -617,7 +626,11 @@ fn init( localnet.insert( rust_name, ProgramDeployment { - address: template::default_program_id(), + address: if solidity { + solidity_template::default_program_id() + } else { + rust_template::default_program_id() + }, path: None, idl: None, }, @@ -626,20 +639,25 @@ fn init( let toml = cfg.to_string(); fs::write("Anchor.toml", toml)?; - // Build virtual manifest. - fs::write("Cargo.toml", template::virtual_manifest())?; - // Initialize .gitignore file - fs::write(".gitignore", template::git_ignore())?; + fs::write(".gitignore", rust_template::git_ignore())?; // Initialize .prettierignore file - fs::write(".prettierignore", template::prettier_ignore())?; + fs::write(".prettierignore", rust_template::prettier_ignore())?; // Build the program. - fs::create_dir("programs")?; + if solidity { + fs::create_dir("solidity")?; - new_program(&project_name)?; + new_solidity_program(&project_name)?; + } else { + // Build virtual manifest for rust programs + fs::write("Cargo.toml", rust_template::virtual_manifest())?; + + fs::create_dir("programs")?; + new_rust_program(&project_name)?; + } // Build the test suite. fs::create_dir("tests")?; // Build the migrations directory. @@ -648,31 +666,44 @@ fn init( if javascript { // Build javascript config let mut package_json = File::create("package.json")?; - package_json.write_all(template::package_json(jest).as_bytes())?; + package_json.write_all(rust_template::package_json(jest).as_bytes())?; if jest { let mut test = File::create(format!("tests/{}.test.js", &project_name))?; - test.write_all(template::jest(&project_name).as_bytes())?; + if solidity { + test.write_all(solidity_template::jest(&project_name).as_bytes())?; + } else { + test.write_all(rust_template::jest(&project_name).as_bytes())?; + } } else { let mut test = File::create(format!("tests/{}.js", &project_name))?; - test.write_all(template::mocha(&project_name).as_bytes())?; + if solidity { + test.write_all(solidity_template::mocha(&project_name).as_bytes())?; + } else { + test.write_all(rust_template::mocha(&project_name).as_bytes())?; + } } let mut deploy = File::create("migrations/deploy.js")?; - deploy.write_all(template::deploy_script().as_bytes())?; + + deploy.write_all(rust_template::deploy_script().as_bytes())?; } else { // Build typescript config let mut ts_config = File::create("tsconfig.json")?; - ts_config.write_all(template::ts_config(jest).as_bytes())?; + ts_config.write_all(rust_template::ts_config(jest).as_bytes())?; let mut ts_package_json = File::create("package.json")?; - ts_package_json.write_all(template::ts_package_json(jest).as_bytes())?; + ts_package_json.write_all(rust_template::ts_package_json(jest).as_bytes())?; let mut deploy = File::create("migrations/deploy.ts")?; - deploy.write_all(template::ts_deploy_script().as_bytes())?; + deploy.write_all(rust_template::ts_deploy_script().as_bytes())?; let mut mocha = File::create(format!("tests/{}.ts", &project_name))?; - mocha.write_all(template::ts_mocha(&project_name).as_bytes())?; + if solidity { + mocha.write_all(solidity_template::ts_mocha(&project_name).as_bytes())?; + } else { + mocha.write_all(rust_template::ts_mocha(&project_name).as_bytes())?; + } } let yarn_result = install_node_modules("yarn")?; @@ -717,7 +748,7 @@ fn install_node_modules(cmd: &str) -> Result { } // Creates a new program crate in the `programs/` directory. -fn new(cfg_override: &ConfigOverride, name: String) -> Result<()> { +fn new(cfg_override: &ConfigOverride, solidity: bool, name: String) -> Result<()> { with_workspace(cfg_override, |cfg| { match cfg.path().parent() { None => { @@ -725,7 +756,11 @@ fn new(cfg_override: &ConfigOverride, name: String) -> Result<()> { } Some(parent) => { std::env::set_current_dir(parent)?; - new_program(&name)?; + if solidity { + new_solidity_program(&name)?; + } else { + new_rust_program(&name)?; + } println!("Created new program."); } }; @@ -733,16 +768,26 @@ fn new(cfg_override: &ConfigOverride, name: String) -> Result<()> { }) } -// Creates a new program crate in the current directory with `name`. -fn new_program(name: &str) -> Result<()> { - fs::create_dir(format!("programs/{name}"))?; - fs::create_dir(format!("programs/{name}/src/"))?; +// Creates a new rust program crate in the current directory with `name`. +fn new_rust_program(name: &str) -> Result<()> { + if !PathBuf::from("Cargo.toml").exists() { + fs::write("Cargo.toml", rust_template::virtual_manifest())?; + } + fs::create_dir_all(format!("programs/{name}/src/"))?; let mut cargo_toml = File::create(format!("programs/{name}/Cargo.toml"))?; - cargo_toml.write_all(template::cargo_toml(name).as_bytes())?; + cargo_toml.write_all(rust_template::cargo_toml(name).as_bytes())?; let mut xargo_toml = File::create(format!("programs/{name}/Xargo.toml"))?; - xargo_toml.write_all(template::xargo_toml().as_bytes())?; + xargo_toml.write_all(rust_template::xargo_toml().as_bytes())?; let mut lib_rs = File::create(format!("programs/{name}/src/lib.rs"))?; - lib_rs.write_all(template::lib_rs(name).as_bytes())?; + lib_rs.write_all(rust_template::lib_rs(name).as_bytes())?; + Ok(()) +} + +// Creates a new solidity program in the current directory with `name`. +fn new_solidity_program(name: &str) -> Result<()> { + fs::create_dir_all("solidity")?; + let mut lib_rs = File::create(format!("solidity/{name}.sol"))?; + lib_rs.write_all(solidity_template::solidity(name).as_bytes())?; Ok(()) } @@ -786,7 +831,7 @@ fn expand_all( cargo_args: &[String], ) -> Result<()> { let cur_dir = std::env::current_dir()?; - for p in workspace_cfg.get_program_list()? { + for p in workspace_cfg.get_rust_program_list()? { expand_program(p, expansions_path.clone(), cargo_args)?; } std::env::set_current_dir(cur_dir)?; @@ -923,7 +968,7 @@ pub fn build( arch, )?, // Cargo.toml represents a single package. Build it. - Some(cargo) => build_cwd( + Some(cargo) => build_rust_cwd( &cfg, cargo.path().to_path_buf(), idl_out, @@ -963,8 +1008,8 @@ fn build_all( let r = match cfg_path.parent() { None => Err(anyhow!("Invalid Anchor.toml at {}", cfg_path.display())), Some(_parent) => { - for p in cfg.get_program_list()? { - build_cwd( + for p in cfg.get_rust_program_list()? { + build_rust_cwd( cfg, p.join("Cargo.toml"), idl_out.clone(), @@ -979,6 +1024,19 @@ fn build_all( &arch, )?; } + for (name, path) in cfg.get_solidity_program_list()? { + build_solidity_cwd( + cfg, + name, + path, + idl_out.clone(), + idl_ts_out.clone(), + build_config, + stdout.as_ref().map(|f| f.try_clone()).transpose()?, + stderr.as_ref().map(|f| f.try_clone()).transpose()?, + cargo_args.clone(), + )?; + } Ok(()) } }; @@ -988,7 +1046,7 @@ fn build_all( // Runs the build command outside of a workspace. #[allow(clippy::too_many_arguments)] -fn build_cwd( +fn build_rust_cwd( cfg: &WithPath, cargo_toml: PathBuf, idl_out: Option, @@ -1007,7 +1065,7 @@ fn build_cwd( Some(p) => std::env::set_current_dir(p)?, }; match build_config.verifiable { - false => _build_cwd(cfg, idl_out, idl_ts_out, skip_lint, arch, cargo_args), + false => _build_rust_cwd(cfg, idl_out, idl_ts_out, skip_lint, arch, cargo_args), true => build_cwd_verifiable( cfg, cargo_toml, @@ -1023,6 +1081,31 @@ fn build_cwd( } } +// Runs the build command outside of a workspace. +#[allow(clippy::too_many_arguments)] +fn build_solidity_cwd( + cfg: &WithPath, + name: String, + path: PathBuf, + idl_out: Option, + idl_ts_out: Option, + build_config: &BuildConfig, + stdout: Option, + stderr: Option, + cargo_args: Vec, +) -> Result<()> { + match path.parent() { + None => return Err(anyhow!("Unable to find parent")), + Some(p) => std::env::set_current_dir(p)?, + }; + match build_config.verifiable { + false => _build_solidity_cwd( + cfg, &name, &path, idl_out, idl_ts_out, stdout, stderr, cargo_args, + ), + true => panic!("verifiable solidity not supported"), + } +} + // Builds an anchor program in a docker image and copies the build artifacts // into the `target/` directory. #[allow(clippy::too_many_arguments)] @@ -1078,7 +1161,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.name)); - fs::write(&ts_file, template::idl_ts(&idl)?)?; + fs::write(&ts_file, rust_template::idl_ts(&idl)?)?; // Copy out the TypeScript type. if !&cfg.workspace.types.is_empty() { @@ -1341,7 +1424,7 @@ fn docker_exec(container_name: &str, args: &[&str]) -> Result<()> { } } -fn _build_cwd( +fn _build_rust_cwd( cfg: &WithPath, idl_out: Option, idl_ts_out: Option, @@ -1377,7 +1460,7 @@ fn _build_cwd( // Write out the JSON file. write_idl(&idl, OutFile::File(out))?; // Write out the TypeScript type. - fs::write(&ts_out, template::idl_ts(&idl)?)?; + fs::write(&ts_out, rust_template::idl_ts(&idl)?)?; // Copy out the TypeScript type. let cfg_parent = cfg.path().parent().expect("Invalid Anchor.toml"); if !&cfg.workspace.types.is_empty() { @@ -1394,6 +1477,80 @@ fn _build_cwd( Ok(()) } +#[allow(clippy::too_many_arguments)] +fn _build_solidity_cwd( + cfg: &WithPath, + name: &str, + path: &Path, + idl_out: Option, + idl_ts_out: Option, + stdout: Option, + stderr: Option, + solang_args: Vec, +) -> Result<()> { + let mut cmd = std::process::Command::new("solang"); + let cmd = cmd.args(["compile", "--target", "solana", "--contract", name]); + + if let Some(idl_out) = &idl_out { + cmd.arg("--output-meta"); + cmd.arg(idl_out); + } + + let target_bin = cfg.path().parent().unwrap().join("target").join("deploy"); + + cmd.arg("--output"); + cmd.arg(target_bin); + cmd.arg("--verbose"); + cmd.arg(path); + + let exit = cmd + .args(solang_args) + .stdout(match stdout { + None => Stdio::inherit(), + Some(f) => f.into(), + }) + .stderr(match stderr { + None => Stdio::inherit(), + Some(f) => f.into(), + }) + .output() + .map_err(|e| anyhow::format_err!("{}", e.to_string()))?; + if !exit.status.success() { + std::process::exit(exit.status.code().unwrap_or(1)); + } + + // idl is written to idl_out or . + let idl_path = idl_out + .unwrap_or(PathBuf::from(".")) + .join(format!("{}.json", name)); + + let idl = fs::read_to_string(idl_path)?; + + let idl: Idl = serde_json::from_str(&idl)?; + + // 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")), + }; + + // Write out the TypeScript type. + fs::write(&ts_out, rust_template::idl_ts(&idl)?)?; + // Copy out the TypeScript type. + let cfg_parent = cfg.path().parent().expect("Invalid Anchor.toml"); + if !&cfg.workspace.types.is_empty() { + fs::copy( + &ts_out, + cfg_parent + .join(&cfg.workspace.types) + .join(&idl.name) + .with_extension("ts"), + )?; + } + + Ok(()) +} + #[allow(clippy::too_many_arguments)] fn verify( cfg_override: &ConfigOverride, @@ -1476,17 +1633,24 @@ fn cd_member(cfg_override: &ConfigOverride, program_name: &str) -> Result<()> { let cfg = Config::discover(cfg_override)?.expect("Not in workspace."); for program in cfg.read_all_programs()? { - let cargo_toml = program.path.join("Cargo.toml"); - if !cargo_toml.exists() { - return Err(anyhow!( - "Did not find Cargo.toml at the path: {}", - program.path.display() - )); - } - let p_lib_name = Manifest::from_path(&cargo_toml)?.lib_name()?; - if program_name == p_lib_name { - std::env::set_current_dir(&program.path)?; - return Ok(()); + if program.solidity { + if let Some(path) = program.path.parent() { + std::env::set_current_dir(path)?; + return Ok(()); + } + } else { + let cargo_toml = program.path.join("Cargo.toml"); + if !cargo_toml.exists() { + return Err(anyhow!( + "Did not find Cargo.toml at the path: {}", + program.path.display() + )); + } + let p_lib_name = Manifest::from_path(&cargo_toml)?.lib_name()?; + if program_name == p_lib_name { + std::env::set_current_dir(&program.path)?; + return Ok(()); + } } } Err(anyhow!("{} is not part of the workspace", program_name,)) @@ -2007,7 +2171,7 @@ fn idl_parse( // Write out the TypeScript IDL. if let Some(out) = out_ts { - fs::write(out, template::idl_ts(&idl)?)?; + fs::write(out, rust_template::idl_ts(&idl)?)?; } Ok(()) @@ -3116,7 +3280,7 @@ fn migrate(cfg_override: &ConfigOverride) -> Result<()> { let exit = if use_ts { let module_path = cur_dir.join("migrations/deploy.ts"); let deploy_script_host_str = - template::deploy_ts_script_host(&url, &module_path.display().to_string()); + rust_template::deploy_ts_script_host(&url, &module_path.display().to_string()); fs::write("deploy.ts", deploy_script_host_str)?; std::process::Command::new("ts-node") .arg("deploy.ts") @@ -3127,7 +3291,7 @@ fn migrate(cfg_override: &ConfigOverride) -> Result<()> { } else { let module_path = cur_dir.join("migrations/deploy.js"); let deploy_script_host_str = - template::deploy_js_script_host(&url, &module_path.display().to_string()); + rust_template::deploy_js_script_host(&url, &module_path.display().to_string()); fs::write("deploy.js", deploy_script_host_str)?; std::process::Command::new("node") .arg("deploy.js") @@ -3259,7 +3423,7 @@ fn shell(cfg_override: &ConfigOverride) -> Result<()> { } }; let url = cluster_url(cfg, &cfg.test_validator); - let js_code = template::node_shell(&url, &cfg.provider.wallet.to_string(), programs)?; + let js_code = rust_template::node_shell(&url, &cfg.provider.wallet.to_string(), programs)?; let mut child = std::process::Command::new("node") .args(["-e", &js_code, "-i", "--experimental-repl-await"]) .stdout(Stdio::inherit()) @@ -3309,7 +3473,7 @@ fn login(_cfg_override: &ConfigOverride, token: String) -> Result<()> { // Freely overwrite the entire file since it's not used for anything else. let mut file = File::create("credentials")?; - file.write_all(template::credentials(&token).as_bytes())?; + file.write_all(rust_template::credentials(&token).as_bytes())?; Ok(()) } @@ -3388,7 +3552,7 @@ fn publish( } // All workspace programs. - for path in cfg.get_program_list()? { + for path in cfg.get_rust_program_list()? { let mut dirs = walkdir::WalkDir::new(path) .into_iter() .filter_entry(|e| !is_hidden(e)); @@ -3674,6 +3838,7 @@ mod tests { true, false, false, + false, ) .unwrap(); } @@ -3690,6 +3855,7 @@ mod tests { true, false, false, + false, ) .unwrap(); } @@ -3706,6 +3872,7 @@ mod tests { true, false, false, + false, ) .unwrap(); } diff --git a/cli/src/template.rs b/cli/src/rust_template.rs similarity index 100% rename from cli/src/template.rs rename to cli/src/rust_template.rs diff --git a/cli/src/solidity_template.rs b/cli/src/solidity_template.rs new file mode 100644 index 0000000000..238b2ab62a --- /dev/null +++ b/cli/src/solidity_template.rs @@ -0,0 +1,473 @@ +use crate::config::ProgramWorkspace; +use crate::VERSION; +use anchor_syn::idl::Idl; +use anyhow::Result; +use heck::{ToLowerCamelCase, ToSnakeCase, ToUpperCamelCase}; +use solana_sdk::pubkey::Pubkey; +use std::fmt::Write; + +pub fn default_program_id() -> Pubkey { + "F1ipperKF9EfD821ZbbYjS319LXYiBmjhzkkf5a26rC" + .parse() + .unwrap() +} + +pub fn idl_ts(idl: &Idl) -> Result { + let mut idl = idl.clone(); + for acc in idl.accounts.iter_mut() { + acc.name = acc.name.to_lower_camel_case(); + } + let idl_json = serde_json::to_string_pretty(&idl)?; + Ok(format!( + r#"export type {} = {}; + +export const IDL: {} = {}; +"#, + idl.name.to_upper_camel_case(), + idl_json, + idl.name.to_upper_camel_case(), + idl_json + )) +} + +pub fn deploy_js_script_host(cluster_url: &str, script_path: &str) -> String { + format!( + r#" +const anchor = require('@coral-xyz/anchor'); + +// Deploy script defined by the user. +const userScript = require("{script_path}"); + +async function main() {{ + const url = "{cluster_url}"; + const preflightCommitment = 'recent'; + const connection = new anchor.web3.Connection(url, preflightCommitment); + const wallet = anchor.Wallet.local(); + + const provider = new anchor.AnchorProvider(connection, wallet, {{ + preflightCommitment, + commitment: 'recent', + }}); + + // Run the user's deploy script. + userScript(provider); +}} +main(); +"# + ) +} + +pub fn deploy_ts_script_host(cluster_url: &str, script_path: &str) -> String { + format!( + r#"import * as anchor from '@coral-xyz/anchor'; + +// Deploy script defined by the user. +const userScript = require("{script_path}"); + +async function main() {{ + const url = "{cluster_url}"; + const preflightCommitment = 'recent'; + const connection = new anchor.web3.Connection(url, preflightCommitment); + const wallet = anchor.Wallet.local(); + + const provider = new anchor.AnchorProvider(connection, wallet, {{ + preflightCommitment, + commitment: 'recent', + }}); + + // Run the user's deploy script. + userScript(provider); +}} +main(); +"# + ) +} + +pub fn deploy_script() -> &'static str { + r#"// Migrations are an early feature. Currently, they're nothing more than this +// single deploy script that's invoked from the CLI, injecting a provider +// configured from the workspace's Anchor.toml. + +const anchor = require("@coral-xyz/anchor"); + +module.exports = async function (provider) { + // Configure client to use the provider. + anchor.setProvider(provider); + + // Add your deploy script here. +}; +"# +} + +pub fn ts_deploy_script() -> &'static str { + r#"// Migrations are an early feature. Currently, they're nothing more than this +// single deploy script that's invoked from the CLI, injecting a provider +// configured from the workspace's Anchor.toml. + +const anchor = require("@coral-xyz/anchor"); + +module.exports = async function (provider) { + // Configure client to use the provider. + anchor.setProvider(provider); + + // Add your deploy script here. +}; +"# +} + +pub fn solidity(name: &str) -> String { + format!( + r#" +@program_id("{}") +contract {} {{ + bool private value = true; + + @payer(payer) + constructor(address payer) {{ + print("Hello, World!"); + }} + + /// A message that can be called on instantiated contracts. + /// This one flips the value of the stored `bool` from `true` + /// to `false` and vice versa. + function flip() public {{ + value = !value; + }} + + /// Simply returns the current value of our `bool`. + function get() public view returns (bool) {{ + return value; + }} +}} +"#, + default_program_id(), + name.to_snake_case(), + ) +} + +pub fn mocha(name: &str) -> String { + format!( + r#"const anchor = require("@coral-xyz/anchor"); + +describe("{}", () => {{ + // Configure the client to use the local cluster. + anchor.setProvider(anchor.AnchorProvider.env()); + + it("Is initialized!", async () => {{ + // Add your test here. + const program = anchor.workspace.{}; + const tx = await program.methods.initialize().rpc(); + console.log("Your transaction signature", tx); + + const val1 = await program.methods.get() + .accounts({{ dataAccount: dataAccount.publicKey }}) + .view(); + + console.log("state", val1); + + await program.methods.flip() + .accounts({{ dataAccount: dataAccount.publicKey }}) + .rpc(); + + const val2 = await program.methods.get() + .accounts({{ dataAccount: dataAccount.publicKey }}) + .view(); + + console.log("state", val2); + }}); +}}); +"#, + name, + name.to_upper_camel_case(), + ) +} + +pub fn jest(name: &str) -> String { + format!( + r#"const anchor = require("@coral-xyz/anchor"); + +describe("{}", () => {{ + // Configure the client to use the local cluster. + anchor.setProvider(anchor.AnchorProvider.env()); + + it("Is initialized!", async () => {{ + // Add your test here. + const program = anchor.workspace.{}; + const tx = await program.methods.initialize().rpc(); + console.log("Your transaction signature", tx); + }}); +}}); +"#, + name, + name.to_upper_camel_case(), + ) +} + +pub fn package_json(jest: bool) -> String { + if jest { + format!( + r#"{{ + "scripts": {{ + "lint:fix": "prettier */*.js \"*/**/*{{.js,.ts}}\" -w", + "lint": "prettier */*.js \"*/**/*{{.js,.ts}}\" --check" + }}, + "dependencies": {{ + "@coral-xyz/anchor": "^{VERSION}" + }}, + "devDependencies": {{ + "jest": "^29.0.3", + "prettier": "^2.6.2" + }} + }} + "# + ) + } else { + format!( + r#"{{ + "scripts": {{ + "lint:fix": "prettier */*.js \"*/**/*{{.js,.ts}}\" -w", + "lint": "prettier */*.js \"*/**/*{{.js,.ts}}\" --check" + }}, + "dependencies": {{ + "@coral-xyz/anchor": "^{VERSION}" + }}, + "devDependencies": {{ + "chai": "^4.3.4", + "mocha": "^9.0.3", + "prettier": "^2.6.2" + }} +}} +"# + ) + } +} + +pub fn ts_package_json(jest: bool) -> String { + if jest { + format!( + r#"{{ + "scripts": {{ + "lint:fix": "prettier */*.js \"*/**/*{{.js,.ts}}\" -w", + "lint": "prettier */*.js \"*/**/*{{.js,.ts}}\" --check" + }}, + "dependencies": {{ + "@coral-xyz/anchor": "^{VERSION}" + }}, + "devDependencies": {{ + "@types/bn.js": "^5.1.0", + "@types/jest": "^29.0.3", + "jest": "^29.0.3", + "prettier": "^2.6.2", + "ts-jest": "^29.0.2", + "typescript": "^4.3.5" + }} + }} + "# + ) + } else { + format!( + r#"{{ + "scripts": {{ + "lint:fix": "prettier */*.js \"*/**/*{{.js,.ts}}\" -w", + "lint": "prettier */*.js \"*/**/*{{.js,.ts}}\" --check" + }}, + "dependencies": {{ + "@coral-xyz/anchor": "^{VERSION}" + }}, + "devDependencies": {{ + "chai": "^4.3.4", + "mocha": "^9.0.3", + "ts-mocha": "^10.0.0", + "@types/bn.js": "^5.1.0", + "@types/chai": "^4.3.0", + "@types/mocha": "^9.0.0", + "typescript": "^4.3.5", + "prettier": "^2.6.2" + }} +}} +"# + ) + } +} + +pub fn ts_mocha(name: &str) -> String { + format!( + r#"import * as anchor from "@coral-xyz/anchor"; +import {{ Program }} from "@coral-xyz/anchor"; +import {{ {} }} from "../target/types/{}"; + +describe("{}", () => {{ + // Configure the client to use the local cluster. + const provider = anchor.AnchorProvider.env(); + anchor.setProvider(provider); + + const dataAccount = anchor.web3.Keypair.generate(); + const wallet = provider.wallet; + + const program = anchor.workspace.{} as Program<{}>; + + it("Is initialized!", async () => {{ + // Add your test here. + const tx = await program.methods.new(wallet.publicKey) + .accounts({{ dataAccount: dataAccount.publicKey }}) + .signers([dataAccount]).rpc(); + console.log("Your transaction signature", tx); + + const val1 = await program.methods.get() + .accounts({{ dataAccount: dataAccount.publicKey }}) + .view(); + + console.log("state", val1); + + await program.methods.flip() + .accounts({{ dataAccount: dataAccount.publicKey }}) + .rpc(); + + const val2 = await program.methods.get() + .accounts({{ dataAccount: dataAccount.publicKey }}) + .view(); + + console.log("state", val2); }}); +}}); +"#, + name.to_upper_camel_case(), + name.to_snake_case(), + name, + name.to_upper_camel_case(), + name.to_upper_camel_case(), + ) +} + +pub fn ts_jest(name: &str) -> String { + format!( + r#"import * as anchor from "@coral-xyz/anchor"; +import {{ Program }} from "@coral-xyz/anchor"; +import {{ {} }} from "../target/types/{}"; + +describe("{}", () => {{ + // Configure the client to use the local cluster. + const provider = anchor.AnchorProvider.env(); + anchor.setProvider(provider); + + const dataAccount = anchor.web3.Keypair.generate(); + const wallet = provider.wallet; + + const program = anchor.workspace.{} as Program<{}>; + + it("Is initialized!", async () => {{ + // Add your test here. + const tx = await program.methods.new(wallet.publicKey) + .accounts({{ dataAccount: dataAccount.publicKey }}) + .signers([dataAccount]).rpc(); + console.log("Your transaction signature", tx); + }}); +}}); +"#, + name.to_upper_camel_case(), + name.to_snake_case(), + name, + name.to_upper_camel_case(), + name.to_upper_camel_case(), + ) +} + +pub fn ts_config(jest: bool) -> &'static str { + if jest { + r#"{ + "compilerOptions": { + "types": ["jest"], + "typeRoots": ["./node_modules/@types"], + "lib": ["es2015"], + "module": "commonjs", + "target": "es6", + "esModuleInterop": true + } + } + "# + } else { + r#"{ + "compilerOptions": { + "types": ["mocha", "chai"], + "typeRoots": ["./node_modules/@types"], + "lib": ["es2015"], + "module": "commonjs", + "target": "es6", + "esModuleInterop": true + } + } + "# + } +} + +pub fn git_ignore() -> &'static str { + r#" +.anchor +.DS_Store +target +**/*.rs.bk +node_modules +test-ledger +"# +} + +pub fn prettier_ignore() -> &'static str { + r#" +.anchor +.DS_Store +target +node_modules +dist +build +test-ledger +"# +} + +pub fn node_shell( + cluster_url: &str, + wallet_path: &str, + programs: Vec, +) -> Result { + let mut eval_string = format!( + r#" +const anchor = require('@coral-xyz/anchor'); +const web3 = anchor.web3; +const PublicKey = anchor.web3.PublicKey; +const Keypair = anchor.web3.Keypair; + +const __wallet = new anchor.Wallet( + Keypair.fromSecretKey( + Buffer.from( + JSON.parse( + require('fs').readFileSync( + "{wallet_path}", + {{ + encoding: "utf-8", + }}, + ), + ), + ), + ), +); +const __connection = new web3.Connection("{cluster_url}", "processed"); +const provider = new anchor.AnchorProvider(__connection, __wallet, {{ + commitment: "processed", + preflightcommitment: "processed", +}}); +anchor.setProvider(provider); +"# + ); + + for program in programs { + write!( + &mut eval_string, + r#" +anchor.workspace.{} = new anchor.Program({}, new PublicKey("{}"), provider); +"#, + program.name.to_upper_camel_case(), + serde_json::to_string(&program.idl)?, + program.program_id + )?; + } + + Ok(eval_string) +}