Browse Source

cli: Add shell command (#303)

Armani Ferrante 4 years ago
parent
commit
ccf18557f9
8 changed files with 191 additions and 3 deletions
  1. 1 0
      CHANGELOG.md
  2. 2 0
      Cargo.lock
  3. 1 0
      cli/Cargo.toml
  4. 71 1
      cli/src/config.rs
  5. 64 1
      cli/src/main.rs
  6. 49 0
      cli/src/template.rs
  7. 1 0
      client/Cargo.toml
  8. 2 1
      client/src/cluster.rs

+ 1 - 0
CHANGELOG.md

@@ -16,6 +16,7 @@ incremented for features.
 * ts: Add `program.simulate` namespace ([#266](https://github.com/project-serum/anchor/pull/266)).
 * cli: Add yarn flag to test command ([#267](https://github.com/project-serum/anchor/pull/267)).
 * cli: Add `--skip-build` flag to test command ([301](https://github.com/project-serum/anchor/pull/301)).
+* cli: Add `anchor shell` command to spawn a node shell populated with an Anchor.toml based environment ([#303](https://github.com/project-serum/anchor/pull/303)).
 
 ## Breaking Changes
 

+ 2 - 0
Cargo.lock

@@ -130,6 +130,7 @@ dependencies = [
 name = "anchor-cli"
 version = "0.5.0"
 dependencies = [
+ "anchor-client",
  "anchor-lang",
  "anchor-syn",
  "anyhow",
@@ -158,6 +159,7 @@ dependencies = [
  "anchor-lang",
  "anyhow",
  "regex",
+ "serde",
  "solana-client",
  "solana-sdk",
  "thiserror",

+ 1 - 0
cli/Cargo.toml

@@ -17,6 +17,7 @@ clap = "3.0.0-beta.1"
 anyhow = "1.0.32"
 syn = { version = "1.0.60", features = ["full", "extra-traits"] }
 anchor-lang = { path = "../lang" }
+anchor-client = { path = "../client" }
 anchor-syn = { path = "../lang/syn", features = ["idl"] }
 serde_json = "1.0"
 shellexpand = "2.1.0"

+ 71 - 1
cli/src/config.rs

@@ -1,8 +1,10 @@
+use anchor_client::Cluster;
 use anchor_syn::idl::Idl;
 use anyhow::{anyhow, Error, Result};
 use serde::{Deserialize, Serialize};
-use serum_common::client::Cluster;
+use solana_sdk::pubkey::Pubkey;
 use solana_sdk::signature::Keypair;
+use std::collections::BTreeMap;
 use std::fs::{self, File};
 use std::io::prelude::*;
 use std::path::Path;
@@ -12,6 +14,7 @@ use std::str::FromStr;
 #[derive(Debug, Default)]
 pub struct Config {
     pub cluster: Cluster,
+    pub clusters: Clusters,
     pub wallet: WalletPath,
     pub test: Option<Test>,
 }
@@ -73,14 +76,24 @@ struct _Config {
     cluster: String,
     wallet: String,
     test: Option<Test>,
+    clusters: Option<BTreeMap<String, BTreeMap<String, String>>>,
 }
 
 impl ToString for Config {
     fn to_string(&self) -> String {
+        let clusters = {
+            let c = ser_clusters(&self.clusters);
+            if c.len() == 0 {
+                None
+            } else {
+                Some(c)
+            }
+        };
         let cfg = _Config {
             cluster: format!("{}", self.cluster),
             wallet: self.wallet.to_string(),
             test: self.test.clone(),
+            clusters,
         };
 
         toml::to_string(&cfg).expect("Must be well formed")
@@ -97,10 +110,53 @@ impl FromStr for Config {
             cluster: cfg.cluster.parse()?,
             wallet: shellexpand::tilde(&cfg.wallet).parse()?,
             test: cfg.test,
+            clusters: cfg
+                .clusters
+                .map_or(Ok(BTreeMap::new()), |c| deser_clusters(c))?,
         })
     }
 }
 
+fn ser_clusters(
+    clusters: &BTreeMap<Cluster, BTreeMap<String, ProgramDeployment>>,
+) -> BTreeMap<String, BTreeMap<String, String>> {
+    clusters
+        .iter()
+        .map(|(cluster, programs)| {
+            let cluster = cluster.to_string();
+            let programs = programs
+                .iter()
+                .map(|(name, deployment)| (name.clone(), deployment.program_id.to_string()))
+                .collect::<BTreeMap<String, String>>();
+            (cluster, programs)
+        })
+        .collect::<BTreeMap<String, BTreeMap<String, String>>>()
+}
+
+fn deser_clusters(
+    clusters: BTreeMap<String, BTreeMap<String, String>>,
+) -> Result<BTreeMap<Cluster, BTreeMap<String, ProgramDeployment>>> {
+    clusters
+        .iter()
+        .map(|(cluster, programs)| {
+            let cluster: Cluster = cluster.parse()?;
+            let programs = programs
+                .iter()
+                .map(|(name, program_id)| {
+                    Ok((
+                        name.clone(),
+                        ProgramDeployment {
+                            name: name.clone(),
+                            program_id: program_id.parse()?,
+                        },
+                    ))
+                })
+                .collect::<Result<BTreeMap<String, ProgramDeployment>>>()?;
+            Ok((cluster, programs))
+        })
+        .collect::<Result<BTreeMap<Cluster, BTreeMap<String, ProgramDeployment>>>>()
+}
+
 #[derive(Debug, Clone, Serialize, Deserialize)]
 pub struct Test {
     pub genesis: Vec<GenesisEntry>,
@@ -177,4 +233,18 @@ impl Program {
     }
 }
 
+pub type Clusters = BTreeMap<Cluster, BTreeMap<String, ProgramDeployment>>;
+
+#[derive(Debug, Default)]
+pub struct ProgramDeployment {
+    pub name: String,
+    pub program_id: Pubkey,
+}
+
+pub struct ProgramWorkspace {
+    pub name: String,
+    pub program_id: Pubkey,
+    pub idl: Idl,
+}
+
 serum_common::home_path!(WalletPath, ".config/solana/id.json");

+ 64 - 1
cli/src/main.rs

@@ -1,6 +1,7 @@
 //! CLI for workspace management of anchor programs.
 
-use crate::config::{read_all_programs, Config, Program};
+use crate::config::{read_all_programs, Config, Program, ProgramWorkspace};
+use anchor_client::Cluster;
 use anchor_lang::idl::{IdlAccount, IdlInstruction};
 use anchor_lang::{AccountDeserialize, AnchorDeserialize, AnchorSerialize};
 use anchor_syn::idl::Idl;
@@ -22,10 +23,12 @@ use solana_sdk::signature::Keypair;
 use solana_sdk::signature::Signer;
 use solana_sdk::sysvar;
 use solana_sdk::transaction::Transaction;
+use std::collections::HashMap;
 use std::fs::{self, File};
 use std::io::prelude::*;
 use std::path::{Path, PathBuf};
 use std::process::{Child, Stdio};
+use std::str::FromStr;
 use std::string::ToString;
 
 mod config;
@@ -139,6 +142,16 @@ pub enum Command {
         #[clap(subcommand)]
         subcmd: ClusterCommand,
     },
+    /// Starts a node shell with an Anchor client setup according to the local
+    /// config.
+    Shell {
+        /// The cluster config to use.
+        #[clap(short, long)]
+        cluster: Option<String>,
+        /// Local path to the wallet keypair file.
+        #[clap(short, long)]
+        wallet: Option<String>,
+    },
 }
 
 #[derive(Debug, Clap)]
@@ -253,6 +266,7 @@ fn main() -> Result<()> {
         #[cfg(feature = "dev")]
         Command::Airdrop { url } => airdrop(url),
         Command::Cluster { subcmd } => cluster(subcmd),
+        Command::Shell { cluster, wallet } => shell(cluster, wallet),
     }
 }
 
@@ -1589,3 +1603,52 @@ fn cluster(_cmd: ClusterCommand) -> Result<()> {
     println!("* Testnet - https://testnet.solana.com");
     Ok(())
 }
+
+fn shell(cluster: Option<String>, wallet: Option<String>) -> Result<()> {
+    with_workspace(|cfg, _path, _cargo| {
+        let cluster = match cluster {
+            None => cfg.cluster.clone(),
+            Some(c) => Cluster::from_str(&c)?,
+        };
+        let wallet = match wallet {
+            None => cfg.wallet.to_string(),
+            Some(c) => c,
+        };
+        let programs = {
+            let idls: HashMap<String, Idl> = read_all_programs()?
+                .iter()
+                .map(|program| (program.idl.name.clone(), program.idl.clone()))
+                .collect();
+            match cfg.clusters.get(&cluster) {
+                None => Vec::new(),
+                Some(programs) => programs
+                    .iter()
+                    .map(|(name, program_deployment)| ProgramWorkspace {
+                        name: name.to_string(),
+                        program_id: program_deployment.program_id,
+                        idl: match idls.get(name) {
+                            None => {
+                                println!("Unable to find IDL for {}", name);
+                                std::process::exit(1);
+                            }
+                            Some(idl) => idl.clone(),
+                        },
+                    })
+                    .collect::<Vec<ProgramWorkspace>>(),
+            }
+        };
+        let js_code = template::node_shell(cluster.url(), &wallet, programs)?;
+        let mut child = std::process::Command::new("node")
+            .args(&["-e", &js_code, "-i", "--experimental-repl-await"])
+            .stdout(Stdio::inherit())
+            .stderr(Stdio::inherit())
+            .spawn()
+            .map_err(|e| anyhow::format_err!("{}", e.to_string()))?;
+
+        if !child.wait()?.success() {
+            println!("Error running node shell");
+            return Ok(());
+        }
+        Ok(())
+    })
+}

+ 49 - 0
cli/src/template.rs

@@ -1,4 +1,6 @@
+use crate::config::ProgramWorkspace;
 use crate::VERSION;
+use anyhow::Result;
 use heck::{CamelCase, SnakeCase};
 
 pub fn virtual_manifest() -> &'static str {
@@ -190,3 +192,50 @@ target
 **/*.rs.bk
     "#
 }
+
+pub fn node_shell(
+    cluster_url: &str,
+    wallet_path: &str,
+    programs: Vec<ProgramWorkspace>,
+) -> Result<String> {
+    let mut eval_string = format!(
+        r#"
+const anchor = require('@project-serum/anchor');
+const web3 = anchor.web3;
+const PublicKey = anchor.web3.PublicKey;
+
+const __wallet = new anchor.Wallet(
+  Buffer.from(
+    JSON.parse(
+      require('fs').readFileSync(
+        "{}",
+        {{
+          encoding: "utf-8",
+        }},
+      ),
+    ),
+  ),
+);
+const __connection = new web3.Connection("{}", "processed");
+const provider = new anchor.Provider(__connection, __wallet, {{
+  commitment: "processed",
+  preflightcommitment: "processed",
+}});
+anchor.setProvider(provider);
+"#,
+        wallet_path, cluster_url,
+    );
+
+    for program in programs {
+        eval_string.push_str(&format!(
+            r#"
+anchor.workspace.{} = new anchor.Program({}, new PublicKey("{}"), provider);
+"#,
+            program.name,
+            serde_json::to_string(&program.idl)?,
+            program.program_id.to_string()
+        ));
+    }
+
+    Ok(eval_string)
+}

+ 1 - 0
client/Cargo.toml

@@ -10,6 +10,7 @@ description = "Rust client for Anchor programs"
 anchor-lang = { path = "../lang", version = "0.5.0" }
 anyhow = "1.0.32"
 regex = "1.4.5"
+serde = { version = "1.0.122", features = ["derive"] }
 solana-client = "1.6.6"
 solana-sdk = "1.6.6"
 thiserror = "1.0.20"

+ 2 - 1
client/src/cluster.rs

@@ -1,7 +1,8 @@
 use anyhow::Result;
+use serde::{Deserialize, Serialize};
 use std::str::FromStr;
 
-#[derive(Clone, Debug, Eq, PartialEq)]
+#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
 pub enum Cluster {
     Testnet,
     Mainnet,