diff --git a/CHANGELOG.md b/CHANGELOG.md index 101f1849..d308b46d 100644 --- a/CHANGELOG.md +++ b/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 diff --git a/Cargo.lock b/Cargo.lock index db6773c0..735b72ec 100644 --- a/Cargo.lock +++ b/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", diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 09f87570..38b2bf1d 100644 --- a/cli/Cargo.toml +++ b/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" diff --git a/cli/src/config.rs b/cli/src/config.rs index bbc2961e..aa4a61bc 100644 --- a/cli/src/config.rs +++ b/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, } @@ -73,14 +76,24 @@ struct _Config { cluster: String, wallet: String, test: Option, + clusters: Option>>, } 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>, +) -> BTreeMap> { + 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::>(); + (cluster, programs) + }) + .collect::>>() +} + +fn deser_clusters( + clusters: BTreeMap>, +) -> Result>> { + 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::>>()?; + Ok((cluster, programs)) + }) + .collect::>>>() +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Test { pub genesis: Vec, @@ -177,4 +233,18 @@ impl Program { } } +pub type Clusters = BTreeMap>; + +#[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"); diff --git a/cli/src/main.rs b/cli/src/main.rs index a4d22dd8..c20a7e71 100644 --- a/cli/src/main.rs +++ b/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, + /// Local path to the wallet keypair file. + #[clap(short, long)] + wallet: Option, + }, } #[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, wallet: Option) -> 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 = 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::>(), + } + }; + 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(()) + }) +} diff --git a/cli/src/template.rs b/cli/src/template.rs index cd0eee5f..59853f48 100644 --- a/cli/src/template.rs +++ b/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, +) -> Result { + 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) +} diff --git a/client/Cargo.toml b/client/Cargo.toml index fc085ceb..13f3b32a 100644 --- a/client/Cargo.toml +++ b/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" diff --git a/client/src/cluster.rs b/client/src/cluster.rs index 5177cca1..534cbb13 100644 --- a/client/src/cluster.rs +++ b/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,