Browse Source

Add support for Solidity using the Solang compiler (#2421)

anchor init and anchor new have a new option `-s` (or `--solidity`) which
creates an example using Solidity in the soldity directory of the
anchor workspace.

anchor deploy/build/test work accordingly.

solang is required to be in the path, this can be downloaded from the
release at https://github.com/hyperledger/solang

anchor build/deploy finds all the solidity contracts by using the
solidity parser to find all contract declarations.
Sean Young 2 years ago
parent
commit
9e7c33eec1
7 changed files with 1060 additions and 68 deletions
  1. 1 0
      CHANGELOG.md
  2. 283 2
      Cargo.lock
  3. 1 0
      cli/Cargo.toml
  4. 79 10
      cli/src/config.rs
  5. 223 56
      cli/src/lib.rs
  6. 0 0
      cli/src/rust_template.rs
  7. 473 0
      cli/src/solidity_template.rs

+ 1 - 0
CHANGELOG.md

@@ -13,6 +13,7 @@ The minor version will be incremented upon a breaking change and the patch versi
 ### Features
 
 - spl: Add metadata wrappers `approve_collection_authority`, `bubblegum_set_collection_size`, `burn_edition_nft`, `burn_nft`, `revoke_collection_authority`, `set_token_standard`, `utilize`, `unverify_sized_collection_item`, `unverify_collection` ([#2430](https://github.com/coral-xyz/anchor/pull/2430))
+- cli: Add support for Solidity programs. `anchor init` and `anchor new` take an option `--solidity` which creates solidity code rather than rust. `anchor build` and `anchor test` work accordingly ([#2421](https://github.com/coral-xyz/anchor/pull/2421))
 
 ### Fixes
 - ts: Narrowed `AccountClient` type to it's appropriate account type ([#2440](https://github.com/coral-xyz/anchor/pull/2440))

+ 283 - 2
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"

+ 1 - 0
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"

+ 79 - 10
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<Config> {
-    pub fn get_program_list(&self) -> Result<Vec<PathBuf>> {
+    pub fn get_rust_program_list(&self) -> Result<Vec<PathBuf>> {
         // Canonicalize the workspace filepaths to compare with relative paths.
         let (members, exclude) = self.canonicalize_workspace()?;
 
@@ -156,12 +158,16 @@ impl WithPath<Config> {
         let program_paths: Vec<PathBuf> = {
             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::<Vec<Result<PathBuf, std::io::Error>>>()
-                    .into_iter()
-                    .collect::<Result<Vec<PathBuf>, 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::<Vec<Result<PathBuf, std::io::Error>>>()
+                        .into_iter()
+                        .collect::<Result<Vec<PathBuf>, std::io::Error>>()?
+                } else {
+                    Vec::new()
+                }
             } else {
                 members
             }
@@ -174,9 +180,56 @@ impl WithPath<Config> {
             .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<Vec<(String, PathBuf)>> {
+        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<Vec<Program>> {
         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<Config> {
 
             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<Idl>,
 }

+ 223 - 56
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<String>,
     },
     /// 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<String>,
         /// Keypair of the program (filepath) (requires program-name)
-        #[clap(long, requires = "program-name")]
+        #[clap(long, requires = "program_name")]
         program_keypair: Option<String>,
     },
     /// 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<std::process::Output> {
 }
 
 // Creates a new program crate in the `programs/<name>` 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<Config>,
     cargo_toml: PathBuf,
     idl_out: Option<PathBuf>,
@@ -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<Config>,
+    name: String,
+    path: PathBuf,
+    idl_out: Option<PathBuf>,
+    idl_ts_out: Option<PathBuf>,
+    build_config: &BuildConfig,
+    stdout: Option<File>,
+    stderr: Option<File>,
+    cargo_args: Vec<String>,
+) -> 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<Config>,
     idl_out: Option<PathBuf>,
     idl_ts_out: Option<PathBuf>,
@@ -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<Config>,
+    name: &str,
+    path: &Path,
+    idl_out: Option<PathBuf>,
+    idl_ts_out: Option<PathBuf>,
+    stdout: Option<File>,
+    stderr: Option<File>,
+    solang_args: Vec<String>,
+) -> 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();
     }

+ 0 - 0
cli/src/template.rs → cli/src/rust_template.rs


+ 473 - 0
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<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 {} = {};
+
+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<ProgramWorkspace>,
+) -> Result<String> {
+    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)
+}