Skip to content

Commit c07ac70

Browse files
cli: Add shell command (otter-sec#303)
1 parent 4dfc99c commit c07ac70

8 files changed

Lines changed: 191 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ incremented for features.
1616
* ts: Add `program.simulate` namespace ([#266](https://github.com/project-serum/anchor/pull/266)).
1717
* cli: Add yarn flag to test command ([#267](https://github.com/project-serum/anchor/pull/267)).
1818
* cli: Add `--skip-build` flag to test command ([301](https://github.com/project-serum/anchor/pull/301)).
19+
* 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)).
1920

2021
## Breaking Changes
2122

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cli/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ clap = "3.0.0-beta.1"
1717
anyhow = "1.0.32"
1818
syn = { version = "1.0.60", features = ["full", "extra-traits"] }
1919
anchor-lang = { path = "../lang" }
20+
anchor-client = { path = "../client" }
2021
anchor-syn = { path = "../lang/syn", features = ["idl"] }
2122
serde_json = "1.0"
2223
shellexpand = "2.1.0"

cli/src/config.rs

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
use anchor_client::Cluster;
12
use anchor_syn::idl::Idl;
23
use anyhow::{anyhow, Error, Result};
34
use serde::{Deserialize, Serialize};
4-
use serum_common::client::Cluster;
5+
use solana_sdk::pubkey::Pubkey;
56
use solana_sdk::signature::Keypair;
7+
use std::collections::BTreeMap;
68
use std::fs::{self, File};
79
use std::io::prelude::*;
810
use std::path::Path;
@@ -12,6 +14,7 @@ use std::str::FromStr;
1214
#[derive(Debug, Default)]
1315
pub struct Config {
1416
pub cluster: Cluster,
17+
pub clusters: Clusters,
1518
pub wallet: WalletPath,
1619
pub test: Option<Test>,
1720
}
@@ -73,14 +76,24 @@ struct _Config {
7376
cluster: String,
7477
wallet: String,
7578
test: Option<Test>,
79+
clusters: Option<BTreeMap<String, BTreeMap<String, String>>>,
7680
}
7781

7882
impl ToString for Config {
7983
fn to_string(&self) -> String {
84+
let clusters = {
85+
let c = ser_clusters(&self.clusters);
86+
if c.len() == 0 {
87+
None
88+
} else {
89+
Some(c)
90+
}
91+
};
8092
let cfg = _Config {
8193
cluster: format!("{}", self.cluster),
8294
wallet: self.wallet.to_string(),
8395
test: self.test.clone(),
96+
clusters,
8497
};
8598

8699
toml::to_string(&cfg).expect("Must be well formed")
@@ -97,10 +110,53 @@ impl FromStr for Config {
97110
cluster: cfg.cluster.parse()?,
98111
wallet: shellexpand::tilde(&cfg.wallet).parse()?,
99112
test: cfg.test,
113+
clusters: cfg
114+
.clusters
115+
.map_or(Ok(BTreeMap::new()), |c| deser_clusters(c))?,
100116
})
101117
}
102118
}
103119

120+
fn ser_clusters(
121+
clusters: &BTreeMap<Cluster, BTreeMap<String, ProgramDeployment>>,
122+
) -> BTreeMap<String, BTreeMap<String, String>> {
123+
clusters
124+
.iter()
125+
.map(|(cluster, programs)| {
126+
let cluster = cluster.to_string();
127+
let programs = programs
128+
.iter()
129+
.map(|(name, deployment)| (name.clone(), deployment.program_id.to_string()))
130+
.collect::<BTreeMap<String, String>>();
131+
(cluster, programs)
132+
})
133+
.collect::<BTreeMap<String, BTreeMap<String, String>>>()
134+
}
135+
136+
fn deser_clusters(
137+
clusters: BTreeMap<String, BTreeMap<String, String>>,
138+
) -> Result<BTreeMap<Cluster, BTreeMap<String, ProgramDeployment>>> {
139+
clusters
140+
.iter()
141+
.map(|(cluster, programs)| {
142+
let cluster: Cluster = cluster.parse()?;
143+
let programs = programs
144+
.iter()
145+
.map(|(name, program_id)| {
146+
Ok((
147+
name.clone(),
148+
ProgramDeployment {
149+
name: name.clone(),
150+
program_id: program_id.parse()?,
151+
},
152+
))
153+
})
154+
.collect::<Result<BTreeMap<String, ProgramDeployment>>>()?;
155+
Ok((cluster, programs))
156+
})
157+
.collect::<Result<BTreeMap<Cluster, BTreeMap<String, ProgramDeployment>>>>()
158+
}
159+
104160
#[derive(Debug, Clone, Serialize, Deserialize)]
105161
pub struct Test {
106162
pub genesis: Vec<GenesisEntry>,
@@ -177,4 +233,18 @@ impl Program {
177233
}
178234
}
179235

236+
pub type Clusters = BTreeMap<Cluster, BTreeMap<String, ProgramDeployment>>;
237+
238+
#[derive(Debug, Default)]
239+
pub struct ProgramDeployment {
240+
pub name: String,
241+
pub program_id: Pubkey,
242+
}
243+
244+
pub struct ProgramWorkspace {
245+
pub name: String,
246+
pub program_id: Pubkey,
247+
pub idl: Idl,
248+
}
249+
180250
serum_common::home_path!(WalletPath, ".config/solana/id.json");

cli/src/main.rs

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
//! CLI for workspace management of anchor programs.
22
3-
use crate::config::{read_all_programs, Config, Program};
3+
use crate::config::{read_all_programs, Config, Program, ProgramWorkspace};
4+
use anchor_client::Cluster;
45
use anchor_lang::idl::{IdlAccount, IdlInstruction};
56
use anchor_lang::{AccountDeserialize, AnchorDeserialize, AnchorSerialize};
67
use anchor_syn::idl::Idl;
@@ -22,10 +23,12 @@ use solana_sdk::signature::Keypair;
2223
use solana_sdk::signature::Signer;
2324
use solana_sdk::sysvar;
2425
use solana_sdk::transaction::Transaction;
26+
use std::collections::HashMap;
2527
use std::fs::{self, File};
2628
use std::io::prelude::*;
2729
use std::path::{Path, PathBuf};
2830
use std::process::{Child, Stdio};
31+
use std::str::FromStr;
2932
use std::string::ToString;
3033

3134
mod config;
@@ -139,6 +142,16 @@ pub enum Command {
139142
#[clap(subcommand)]
140143
subcmd: ClusterCommand,
141144
},
145+
/// Starts a node shell with an Anchor client setup according to the local
146+
/// config.
147+
Shell {
148+
/// The cluster config to use.
149+
#[clap(short, long)]
150+
cluster: Option<String>,
151+
/// Local path to the wallet keypair file.
152+
#[clap(short, long)]
153+
wallet: Option<String>,
154+
},
142155
}
143156

144157
#[derive(Debug, Clap)]
@@ -253,6 +266,7 @@ fn main() -> Result<()> {
253266
#[cfg(feature = "dev")]
254267
Command::Airdrop { url } => airdrop(url),
255268
Command::Cluster { subcmd } => cluster(subcmd),
269+
Command::Shell { cluster, wallet } => shell(cluster, wallet),
256270
}
257271
}
258272

@@ -1589,3 +1603,52 @@ fn cluster(_cmd: ClusterCommand) -> Result<()> {
15891603
println!("* Testnet - https://testnet.solana.com");
15901604
Ok(())
15911605
}
1606+
1607+
fn shell(cluster: Option<String>, wallet: Option<String>) -> Result<()> {
1608+
with_workspace(|cfg, _path, _cargo| {
1609+
let cluster = match cluster {
1610+
None => cfg.cluster.clone(),
1611+
Some(c) => Cluster::from_str(&c)?,
1612+
};
1613+
let wallet = match wallet {
1614+
None => cfg.wallet.to_string(),
1615+
Some(c) => c,
1616+
};
1617+
let programs = {
1618+
let idls: HashMap<String, Idl> = read_all_programs()?
1619+
.iter()
1620+
.map(|program| (program.idl.name.clone(), program.idl.clone()))
1621+
.collect();
1622+
match cfg.clusters.get(&cluster) {
1623+
None => Vec::new(),
1624+
Some(programs) => programs
1625+
.iter()
1626+
.map(|(name, program_deployment)| ProgramWorkspace {
1627+
name: name.to_string(),
1628+
program_id: program_deployment.program_id,
1629+
idl: match idls.get(name) {
1630+
None => {
1631+
println!("Unable to find IDL for {}", name);
1632+
std::process::exit(1);
1633+
}
1634+
Some(idl) => idl.clone(),
1635+
},
1636+
})
1637+
.collect::<Vec<ProgramWorkspace>>(),
1638+
}
1639+
};
1640+
let js_code = template::node_shell(cluster.url(), &wallet, programs)?;
1641+
let mut child = std::process::Command::new("node")
1642+
.args(&["-e", &js_code, "-i", "--experimental-repl-await"])
1643+
.stdout(Stdio::inherit())
1644+
.stderr(Stdio::inherit())
1645+
.spawn()
1646+
.map_err(|e| anyhow::format_err!("{}", e.to_string()))?;
1647+
1648+
if !child.wait()?.success() {
1649+
println!("Error running node shell");
1650+
return Ok(());
1651+
}
1652+
Ok(())
1653+
})
1654+
}

cli/src/template.rs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
use crate::config::ProgramWorkspace;
12
use crate::VERSION;
3+
use anyhow::Result;
24
use heck::{CamelCase, SnakeCase};
35

46
pub fn virtual_manifest() -> &'static str {
@@ -190,3 +192,50 @@ target
190192
**/*.rs.bk
191193
"#
192194
}
195+
196+
pub fn node_shell(
197+
cluster_url: &str,
198+
wallet_path: &str,
199+
programs: Vec<ProgramWorkspace>,
200+
) -> Result<String> {
201+
let mut eval_string = format!(
202+
r#"
203+
const anchor = require('@project-serum/anchor');
204+
const web3 = anchor.web3;
205+
const PublicKey = anchor.web3.PublicKey;
206+
207+
const __wallet = new anchor.Wallet(
208+
Buffer.from(
209+
JSON.parse(
210+
require('fs').readFileSync(
211+
"{}",
212+
{{
213+
encoding: "utf-8",
214+
}},
215+
),
216+
),
217+
),
218+
);
219+
const __connection = new web3.Connection("{}", "processed");
220+
const provider = new anchor.Provider(__connection, __wallet, {{
221+
commitment: "processed",
222+
preflightcommitment: "processed",
223+
}});
224+
anchor.setProvider(provider);
225+
"#,
226+
wallet_path, cluster_url,
227+
);
228+
229+
for program in programs {
230+
eval_string.push_str(&format!(
231+
r#"
232+
anchor.workspace.{} = new anchor.Program({}, new PublicKey("{}"), provider);
233+
"#,
234+
program.name,
235+
serde_json::to_string(&program.idl)?,
236+
program.program_id.to_string()
237+
));
238+
}
239+
240+
Ok(eval_string)
241+
}

client/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ description = "Rust client for Anchor programs"
1010
anchor-lang = { path = "../lang", version = "0.5.0" }
1111
anyhow = "1.0.32"
1212
regex = "1.4.5"
13+
serde = { version = "1.0.122", features = ["derive"] }
1314
solana-client = "1.6.6"
1415
solana-sdk = "1.6.6"
1516
thiserror = "1.0.20"

client/src/cluster.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
use anyhow::Result;
2+
use serde::{Deserialize, Serialize};
23
use std::str::FromStr;
34

4-
#[derive(Clone, Debug, Eq, PartialEq)]
5+
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
56
pub enum Cluster {
67
Testnet,
78
Mainnet,

0 commit comments

Comments
 (0)