Workspaces

This commit is contained in:
Armani Ferrante 2021-01-02 22:40:17 -08:00 committed by armaniferrante
parent 453c0d95de
commit 9570830b65
No known key found for this signature in database
GPG Key ID: 58BEF301E91F7828
24 changed files with 3905 additions and 169 deletions

3129
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -17,3 +17,10 @@ syn = { version = "1.0.54", features = ["full", "extra-traits"] }
anchor-syn = { path = "../syn", features = ["idl"] }
serde_json = "1.0"
shellexpand = "2.1.0"
serde_yaml = "0.8"
toml = "0.5.8"
serde = { version = "1.0", features = ["derive"] }
solana-sdk = "1.5.0"
serum-common = { git = "https://github.com/project-serum/serum-dex", features = ["client"] }
dirs = "3.0"
heck = "0.3.1"

158
cli/src/config.rs Normal file
View File

@ -0,0 +1,158 @@
use anchor_syn::idl::Idl;
use anyhow::{anyhow, Error, Result};
use serde::{Deserialize, Serialize};
use serum_common::client::Cluster;
use std::fs::{self, File};
use std::io::prelude::*;
use std::path::Path;
use std::path::PathBuf;
use std::str::FromStr;
#[derive(Default)]
pub struct Config {
cluster: Cluster,
wallet: WalletPath,
}
impl Config {
// Searches all parent directories for an Anchor.toml file.
pub fn discover() -> Result<Option<(Self, PathBuf, Option<PathBuf>)>> {
// Set to true if we ever see a Cargo.toml file when traversing the
// parent directories.
let mut cargo_toml = None;
let _cwd = std::env::current_dir()?;
let mut cwd_opt = Some(_cwd.as_path());
while let Some(cwd) = cwd_opt {
let files = fs::read_dir(cwd)?;
// Cargo.toml file for this directory level.
let mut cargo_toml_level = None;
let mut anchor_toml = None;
for f in files {
let p = f?.path();
if let Some(filename) = p.file_name() {
if filename.to_str() == Some("Cargo.toml") {
cargo_toml_level = Some(PathBuf::from(p));
} else if filename.to_str() == Some("Anchor.toml") {
let mut cfg_file = File::open(&p)?;
let mut cfg_contents = String::new();
cfg_file.read_to_string(&mut cfg_contents)?;
let cfg = cfg_contents.parse()?;
anchor_toml = Some((cfg, PathBuf::from(p)));
}
}
}
if let Some((cfg, parent)) = anchor_toml {
return Ok(Some((cfg, parent, cargo_toml)));
}
if cargo_toml.is_none() {
cargo_toml = cargo_toml_level;
}
cwd_opt = cwd.parent();
}
Ok(None)
}
}
// Pubkey serializes as a byte array so use this type a hack to serialize
// into base 58 strings.
#[derive(Serialize, Deserialize)]
struct _Config {
cluster: String,
wallet: String,
}
impl ToString for Config {
fn to_string(&self) -> String {
let cfg = _Config {
cluster: format!("{}", self.cluster),
wallet: self.wallet.to_string(),
};
toml::to_string(&cfg).expect("Must be well formed")
}
}
impl FromStr for Config {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let cfg: _Config = toml::from_str(s)
.map_err(|e| anyhow::format_err!("Unable to deserialize config: {}", e.to_string()))?;
Ok(Config {
cluster: cfg.cluster.parse()?,
wallet: cfg.wallet.parse()?,
})
}
}
pub fn find_cargo_toml() -> Result<Option<PathBuf>> {
let _cwd = std::env::current_dir()?;
let mut cwd_opt = Some(_cwd.as_path());
while let Some(cwd) = cwd_opt {
let files = fs::read_dir(cwd)?;
for f in files {
let p = f?.path();
if let Some(filename) = p.file_name() {
if filename.to_str() == Some("Cargo.toml") {
return Ok(Some(PathBuf::from(p)));
}
}
}
cwd_opt = cwd.parent();
}
Ok(None)
}
pub fn read_all_programs() -> Result<Vec<Program>> {
let files = fs::read_dir("programs")?;
let mut r = vec![];
for f in files {
let path = f?.path();
let idl = anchor_syn::parser::file::parse(path.join("src/lib.rs"))?;
let lib_name = extract_lib_name(&path.join("Cargo.toml"))?;
r.push(Program {
lib_name,
path,
idl,
});
}
Ok(r)
}
pub fn extract_lib_name(path: impl AsRef<Path>) -> Result<String> {
let mut toml = File::open(path)?;
let mut contents = String::new();
toml.read_to_string(&mut contents)?;
let cargo_toml: toml::Value = contents.parse()?;
match cargo_toml {
toml::Value::Table(t) => match t.get("lib") {
None => Err(anyhow!("lib not found in Cargo.toml")),
Some(lib) => match lib
.get("name")
.ok_or(anyhow!("lib name not found in Cargo.toml"))?
{
toml::Value::String(n) => Ok(n.to_string()),
_ => Err(anyhow!("lib name must be a string")),
},
},
_ => Err(anyhow!("Invalid Cargo.toml")),
}
}
#[derive(Debug)]
pub struct Program {
pub lib_name: String,
pub path: PathBuf,
pub idl: Idl,
}
serum_common::home_path!(WalletPath, ".config/solana/id.json");

View File

@ -1,5 +1,18 @@
use crate::config::{find_cargo_toml, read_all_programs, Config, Program};
use anchor_syn::idl::Idl;
use anyhow::Result;
use clap::Clap;
use serde::{Deserialize, Serialize};
use solana_sdk::pubkey::Pubkey;
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::string::ToString;
mod config;
mod template;
#[derive(Debug, Clap)]
pub struct Opts {
@ -9,6 +22,18 @@ pub struct Opts {
#[derive(Debug, Clap)]
pub enum Command {
/// Initializes a workspace.
Init { name: String },
/// Builds a Solana program.
Build {
/// Output directory for the IDL.
#[clap(short, long)]
idl: Option<String>,
},
/// Runs integration tests against a localnetwork.
Test,
/// Creates a new program.
New { name: String },
/// Outputs an interface definition file.
Idl {
/// Path to the program's interface definition.
@ -18,40 +43,349 @@ pub enum Command {
#[clap(short, long)]
out: Option<String>,
},
/// Generates a client module.
Gen {
/// Path to the program's interface definition.
#[clap(short, long, required_unless_present("idl"))]
file: Option<String>,
/// Output file (stdout if not specified).
#[clap(short, long)]
out: Option<String>,
#[clap(short, long)]
idl: Option<String>,
},
}
fn main() -> Result<()> {
let opts = Opts::parse();
match opts.command {
Command::Idl { file, out } => idl(file, out),
Command::Gen { file, out, idl } => gen(file, out, idl),
Command::Init { name } => init(name),
Command::Build { idl } => build(idl),
Command::Test => test(),
Command::New { name } => new(name),
Command::Idl { file, out } => {
if out.is_none() {
return idl(file, None);
}
idl(file, Some(&PathBuf::from(out.unwrap())))
}
}
}
fn idl(file: String, out: Option<String>) -> Result<()> {
let file = shellexpand::tilde(&file);
let idl = anchor_syn::parser::file::parse(&file)?;
let idl_json = serde_json::to_string_pretty(&idl)?;
if let Some(out) = out {
std::fs::write(out, idl_json)?;
return Ok(());
fn init(name: String) -> Result<()> {
let cfg = Config::discover()?;
if cfg.is_some() {
println!("Anchor workspace already initialized");
}
println!("{}", idl_json);
fs::create_dir(name.clone())?;
std::env::set_current_dir(&name)?;
fs::create_dir("app")?;
let cfg = Config::default();
let toml = cfg.to_string();
let mut file = File::create("Anchor.toml")?;
file.write_all(toml.as_bytes())?;
// Build virtual manifest.
let mut virt_manifest = File::create("Cargo.toml")?;
virt_manifest.write_all(template::virtual_manifest().as_bytes())?;
// Build the program.
fs::create_dir("programs")?;
new_program(&name)?;
// Build the test suite.
fs::create_dir("tests")?;
let mut mocha = File::create(&format!("tests/{}.js", name))?;
mocha.write_all(template::mocha(&name).as_bytes())?;
println!("{} initialized", name);
Ok(())
}
fn gen(file: Option<String>, out: Option<String>, idl: Option<String>) -> Result<()> {
// TODO. Generate clients in any language.
// Creates a new program crate in the `programs/<name>` directory.
fn new(name: String) -> Result<()> {
match Config::discover()? {
None => {
println!("Not in anchor workspace.");
std::process::exit(1);
}
Some((_cfg, cfg_path, _inside_cargo)) => {
match cfg_path.parent() {
None => {
println!("Unable to make new program");
}
Some(parent) => {
std::env::set_current_dir(&parent)?;
new_program(&name)?;
println!("Created new program.");
}
};
}
}
Ok(())
}
// 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/{}/src/", name))?;
let mut cargo_toml = File::create(&format!("programs/{}/Cargo.toml", name))?;
cargo_toml.write_all(template::cargo_toml(&name).as_bytes())?;
let mut xargo_toml = File::create(&format!("programs/{}/Xargo.toml", name))?;
xargo_toml.write_all(template::xargo_toml(&name).as_bytes())?;
let mut lib_rs = File::create(&format!("programs/{}/src/lib.rs", name))?;
lib_rs.write_all(template::lib_rs(&name).as_bytes())?;
Ok(())
}
fn build(idl: Option<String>) -> Result<()> {
match Config::discover()? {
None => build_cwd(idl),
Some((cfg, cfg_path, inside_cargo)) => build_ws(cfg, cfg_path, inside_cargo, idl),
}
}
// Runs the build inside a workspace.
//
// * Builds a single program if the current dir is within a Cargo subdirectory,
// e.g., `programs/my-program/src`.
// * Builds *all* programs if thje current dir is anywhere else in the workspace.
//
fn build_ws(
cfg: Config,
cfg_path: PathBuf,
cargo_toml: Option<PathBuf>,
idl: Option<String>,
) -> Result<()> {
let idl_out = match idl {
Some(idl) => Some(PathBuf::from(idl)),
None => {
let cfg_parent = match cfg_path.parent() {
None => return Err(anyhow::anyhow!("Invalid Anchor.toml")),
Some(parent) => parent,
};
fs::create_dir_all(cfg_parent.join("target/idl"))?;
Some(cfg_parent.join("target/idl"))
}
};
match cargo_toml {
None => build_all(cfg, cfg_path, idl_out),
Some(ct) => _build_cwd(ct, idl_out),
}
}
fn build_all(_cfg: Config, cfg_path: PathBuf, idl_out: Option<PathBuf>) -> Result<()> {
match cfg_path.parent() {
None => Err(anyhow::anyhow!(
"Invalid Anchor.toml at {}",
cfg_path.display()
)),
Some(parent) => {
let files = fs::read_dir(parent.join("programs"))?;
for f in files {
let p = f?.path();
_build_cwd(p.join("Cargo.toml"), idl_out.clone())?;
}
Ok(())
}
}
}
fn build_cwd(idl_out: Option<String>) -> Result<()> {
match find_cargo_toml()? {
None => {
println!("Cargo.toml not found");
std::process::exit(1);
}
Some(cargo_toml) => _build_cwd(cargo_toml, idl_out.map(PathBuf::from)),
}
}
// Runs the build command outside of a workspace.
fn _build_cwd(cargo_toml: PathBuf, idl_out: Option<PathBuf>) -> Result<()> {
match cargo_toml.parent() {
None => return Err(anyhow::anyhow!("Unable to find parent")),
Some(p) => std::env::set_current_dir(&p)?,
};
let exit = std::process::Command::new("cargo")
.arg("build-bpf")
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.output()
.map_err(|e| anyhow::format_err!("{}", e.to_string()))?;
if !exit.status.success() {
std::process::exit(exit.status.code().unwrap_or(1));
}
// Always assume idl is located ar src/lib.rs.
let idl = extract_idl("src/lib.rs")?;
let out = match idl_out {
None => PathBuf::from(".").join(&idl.name).with_extension("json"),
Some(o) => PathBuf::from(&o.join(&idl.name).with_extension("json")),
};
write_idl(&idl, Some(&out))
}
fn idl(file: String, out: Option<&Path>) -> Result<()> {
let idl = extract_idl(&file)?;
write_idl(&idl, out)
}
fn extract_idl(file: &str) -> Result<Idl> {
let file = shellexpand::tilde(file);
anchor_syn::parser::file::parse(&*file)
}
fn write_idl(idl: &Idl, out: Option<&Path>) -> Result<()> {
let idl_json = serde_json::to_string_pretty(idl)?;
match out.as_ref() {
None => println!("{}", idl_json),
Some(out) => std::fs::write(out, idl_json)?,
};
Ok(())
}
// Builds, deploys, and tests all workspace programs in a single command.
fn test() -> Result<()> {
// Switch directories to top level workspace.
set_workspace_dir_or_exit();
// Build everything.
build(None)?;
// Switch again (todo: restore cwd in `build` command).
set_workspace_dir_or_exit();
// Bootup validator.
let mut validator_handle = start_test_validator()?;
// Deploy all programs.
let programs = deploy_ws()?;
// Store deployed program addresses in IDL metadata (for consumption by
// client + tests).
for (program, address) in programs {
// Add metadata to the IDL.
let mut idl = program.idl;
idl.metadata = Some(serde_json::to_value(IdlTestMetadata {
address: address.to_string(),
})?);
// Persist it.
let idl_out = PathBuf::from("target/idl")
.join(&idl.name)
.with_extension("json");
write_idl(&idl, Some(&idl_out))?;
}
// Run the tests.
if let Err(e) = std::process::Command::new("mocha")
.arg("tests/")
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.output()
{
validator_handle.kill()?;
return Err(anyhow::format_err!("{}", e.to_string()));
}
validator_handle.kill()?;
Ok(())
}
#[derive(Debug, Serialize, Deserialize)]
pub struct IdlTestMetadata {
address: String,
}
fn start_test_validator() -> Result<Child> {
fs::create_dir_all(".anchor")?;
let test_ledger_filename = ".anchor/test-ledger";
let test_ledger_log_filename = ".anchor/test-ledger-log.txt";
if Path::new(test_ledger_filename).exists() {
std::fs::remove_dir_all(test_ledger_filename)?;
}
if Path::new(test_ledger_log_filename).exists() {
std::fs::remove_file(test_ledger_log_filename)?;
}
// Start a validator for testing.
let test_validator_stdout = File::create(test_ledger_log_filename)?;
let test_validator_stderr = test_validator_stdout.try_clone()?;
let validator_handle = std::process::Command::new("solana-test-validator")
.arg("--ledger")
.arg(test_ledger_filename)
.stdout(Stdio::from(test_validator_stdout))
.stderr(Stdio::from(test_validator_stderr))
.spawn()
.map_err(|e| anyhow::format_err!("{}", e.to_string()))?;
// TODO: do something more sensible than sleeping.
std::thread::sleep(std::time::Duration::from_millis(2000));
Ok(validator_handle)
}
fn deploy_ws() -> Result<Vec<(Program, Pubkey)>> {
let mut programs = vec![];
println!("Deploying workspace to http://localhost:8899...");
for program in read_all_programs()? {
let binary_path = format!(
"target/bpfel-unknown-unknown/release/{}.so",
program.lib_name
);
println!("Deploying {}...", binary_path);
let exit = std::process::Command::new("solana")
.arg("deploy")
.arg(&binary_path)
.arg("--url")
.arg("http://localhost:8899") // TODO: specify network via cli.
.arg("--keypair")
.arg(".anchor/test-ledger/faucet-keypair.json") // TODO: specify wallet.
.output()
.expect("Must deploy");
if !exit.status.success() {
println!("There was a problem deploying.");
std::process::exit(exit.status.code().unwrap_or(1));
}
let stdout: DeployStdout = serde_json::from_str(std::str::from_utf8(&exit.stdout)?)?;
programs.push((program, stdout.program_id.parse()?));
}
println!("Deploy success!");
Ok(programs)
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DeployStdout {
program_id: String,
}
fn set_workspace_dir_or_exit() {
let d = match Config::discover() {
Err(_) => {
println!("Not in anchor workspace.");
std::process::exit(1);
}
Ok(d) => d,
};
match d {
None => {
println!("Not in anchor workspace.");
std::process::exit(1);
}
Some((_cfg, cfg_path, _inside_cargo)) => {
match cfg_path.parent() {
None => {
println!("Unable to make new program");
}
Some(parent) => match std::env::set_current_dir(&parent) {
Err(_) => {
println!("Not in anchor workspace.");
std::process::exit(1);
}
Ok(_) => {}
},
};
}
}
}

83
cli/src/template.rs Normal file
View File

@ -0,0 +1,83 @@
use heck::CamelCase;
use heck::SnakeCase;
pub fn virtual_manifest() -> String {
r#"[workspace]
members = [
"programs/*"
]
"#
.to_string()
}
pub fn cargo_toml(name: &str) -> String {
format!(
r#"[package]
name = "{0}"
version = "0.1.0"
description = "Created with Anchor"
edition = "2018"
[lib]
crate-type = ["cdylib"]
name = "{1}"
[dependencies]
borsh = {{ git = "https://github.com/project-serum/borsh", branch = "serum", features = ["serum-program"] }}
solana-program = "1.4.3"
solana-sdk = {{ version = "1.3.14", default-features = false, features = ["program"] }}
# anchor = {{ git = "https://github.com/project-serum/anchor", features = ["derive"] }}
anchor = {{ path = "/home/armaniferrante/Documents/code/src/github.com/project-serum/anchor", features = ["derive"] }}
"#,
name,
name.to_snake_case(),
)
}
pub fn xargo_toml(name: &str) -> String {
r#"[target.bpfel-unknown-unknown.dependencies.std]
features = []"#
.to_string()
}
pub fn lib_rs(name: &str) -> String {
format!(
r#"#![feature(proc_macro_hygiene)]
use anchor::prelude::*;
#[program]
mod {} {{
use super::*;
pub fn initialize(ctx: Context<Initialize>) -> ProgramResult {{
Ok(())
}}
}}
#[derive(Accounts)]
pub struct Initialize {{}}"#,
name.to_snake_case(),
)
}
pub fn mocha(name: &str) -> String {
format!(
r#"const anchor = require('@project-serum/anchor');
describe('{}', () => {{
// Configure the client to use the local cluster.
anchor.setProvider(anchor.Provider.local());
it('Is initialized!', async () => {{
// Add your test here.
const program = anchor.workspace.{};
const tx = await program.rpc.initialize();
console.log("Your transaction signature", tx);
}});
}});
"#,
name,
name.to_camel_case(),
)
}

View File

@ -42,13 +42,19 @@ module.exports = {
title: "Getting Started",
children: [
"/getting-started/introduction",
"/getting-started/installation",
"/getting-started/installation",
"/getting-started/quick-start",
],
},
{
collapsable: false,
title: "Tutorials",
children: ["/tutorials/tutorial-0", "/tutorials/tutorial-1"],
children: [
"/tutorials/tutorial-0",
"/tutorials/tutorial-1",
"/tutorials/tutorial-2",
"/tutorials/tutorial-3",
],
},
],

View File

@ -1,32 +1,44 @@
# Install
# Installing Dependencies
To get started, make sure to setup all the prerequisite tools on your local machine.
To get started, make sure to setup all the prerequisite tools on your local machine
(an installer has not yet been developed).
## Install Rust
For an introduction to Rust, see the excellent Rust [book](https://doc.rust-lang.org/book/).
```bash
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source $HOME/.cargo/env
rustup component add rustfmt
```
For an introduction to Rust, see the excellent Rust [book](https://doc.rust-lang.org/book/).
## Install Solana
See the solana [docs](https://docs.solana.com/cli/install-solana-cli-tools) for installation instructions. On macOS and Linux,
```bash
sh -c "$(curl -sSfL https://release.solana.com/v1.5.0/install)"
```
## Install Mocha
Program integration tests are run using [Mocha](https://mochajs.org/).
```bash
npm install -g mocha
```
## Install Anchor
For now, we can use Cargo.
For now, we can use Cargo to install the CLI.
```bash
cargo install --git https://github.com/project-serum/anchor anchor-cli
```
## Install Solana
To install the JavaScript package.
```bash
curl -sSf https://raw.githubusercontent.com/solana-labs/solana/v1.4.14/install/solana-install-init.sh | sh -s - v1.4.14
export PATH="/home/ubuntu/.local/share/solana/install/active_release/bin:$PATH"
npm install -g @project-serum/anchor
```
## Setup a Localnet
The easiest way to run a local cluster is to run the docker container provided by Solana. Instructions can be found [here](https://solana-labs.github.io/solana-web3.js/). (Note: `solana-test-validator` is the new, preferred way to run a local validator, though I haven't tested it yet).

View File

@ -1,21 +1,17 @@
# Introduction
Anchor is a framework for Solana's [Sealevel](https://medium.com/solana-labs/sealevel-parallel-processing-thousands-of-smart-contracts-d814b378192) runtime, exposing a safer and more convenient programming model to the Solana developer by providing a
Anchor is a framework for Solana's [Sealevel](https://medium.com/solana-labs/sealevel-parallel-processing-thousands-of-smart-contracts-d814b378192) runtime, exposing a more convenient programming model by providing several different developer tools.
- Rust Crate for writing Solana programs
- Rust crates and DSL for writing Solana programs
- CLI for extracting an [IDL](https://en.wikipedia.org/wiki/Interface_description_language) from source
- TypeScript package for generating clients from IDL
- Workspace management for developing complete applications
If you're familiar with developing in Ethereum's [Solidity](https://docs.soliditylang.org/en/v0.7.4/) and [web3.js](https://github.com/ethereum/web3.js) or Parity's [Ink!](https://github.com/paritytech/ink), then the experience will be familiar. Although the DSL syntax and semantics are targeted at Solana, the high level flow of writing RPC request handlers, emitting an IDL, and generating clients from IDL is the same.
If you're familiar with developing in Ethereum's [Solidity](https://docs.soliditylang.org/en/v0.7.4/), [Truffle](https://www.trufflesuite.com/), [web3.js](https://github.com/ethereum/web3.js) or Parity's [Ink!](https://github.com/paritytech/ink), then the experience will be familiar. Although the DSL syntax and semantics are targeted at Solana, the high level flow of writing RPC request handlers, emitting an IDL, and generating clients from IDL is the same.
Here, we'll walkthrough a tutorial demonstrating how to use Anchor. To skip the tutorial and jump straight to a full example, go [here](https://github.com/project-serum/anchor/tree/master/examples/basic/src/lib.rs).
Here, we'll walkthrough several tutorials demonstrating how to use Anchor. To skip the tutorials and jump straight to a full example, checkout the
[quickstart](./quick-start.md) or go [here](https://github.com/project-serum/anchor/tree/master/examples/basic/src/lib.rs). For an introduction to Solana, see the [docs](https://docs.solana.com/developing/programming-model/overview).
## Contributing
It would be great to have clients generated for languages other than TypeScript. If you're
interested in developing a client generator, feel free to reach out, or go ahead and just
do it :P.
## Note
Anchor is in active development, so all APIs are subject to change. If you have feedback, please reach out by [filing an issue](https://github.com/project-serum/anchor/issues/new). This documentation is a work in progress and is expected to change dramatically as features continue to be built out. If you have any problems, consult the [source](https://github.com/project-serum/anchor) or feel free to ask questions on the [Serum Discord](https://discord.com/channels/739225212658122886/752530209848295555).
::: tip NOTE
Anchor is in active development, so all APIs are subject to change. If you are one of the early developers to try it out and have feedback, please reach out by [filing an issue](https://github.com/project-serum/anchor/issues/new). This documentation is a work in progress and is expected to change dramatically as features continue to be built out. If you have any problems, consult the [source](https://github.com/project-serum/anchor) or feel free to ask questions on the [Serum Discord](https://discord.com/channels/739225212658122886/752530209848295555).
:::

View File

@ -0,0 +1,52 @@
# Quick Start
The quick start provides a whirlwind tour through creating, deploying, and testing a project
using Anchor, targeted at developers who are familiar with blockchain development. For an in depth
guide of Anchor from the ground up, see the subequent tutorials.
## Initialize a project
Anchor follows the principle of "Convention is better than configuration".
To initialize your project workspace, run
```bash
anchor init my-project && cd my-project
```
Your repo will be laid out with the following structure
* `Anchor.toml`: Anchor configuration file.
* `programs/`: Directory for Solana program crates.
* `app/`: Directory for your application frontend.
* `tests/`: Directory for TypeScript integration tests.
## Build
To build your program targeting Solana's BPF runtime and emit an IDL that can be
consumed by clients, run
```bash
anchor build
```
## Test
It's [recommended](https://www.parity.io/paritys-checklist-for-secure-smart-contract-development/)
to test your program using integration tests in a language other
than Rust to make sure that bugs related to syntax misunderstandings
are coverable with tests and not just replicated in tests.
```
anchor test
```
You just built a program, deployed it to a local network, and
ran integration tests in one command. It's that easy. ;)
## Deploy
To deploy all programs in your workspace, run
```
anchor deploy
```

View File

@ -4,7 +4,7 @@ Here, we introduce a minimal example demonstrating the Anchor workflow and core
elements. This tutorial assumes all [prerequisites](./prerequisites.md) are installed and
a local network is running.
## Clone the repo
## Clone the Repo
To get started, clone the repo.
@ -12,71 +12,52 @@ To get started, clone the repo.
git clone https://github.com/project-serum/anchor
```
And change directories to the [example](https://github.com/project-serum/anchor/tree/master/examples/basic-0).
And change directories to the [example](https://github.com/project-serum/anchor/tree/master/examples/tutorial/basic-0).
```bash
cd anchor/examples/tutorial/basic-0
```
## Defining a program
## Defining a Program
We define the minimum viable program as follows.
<<< @/../examples/tutorial/basic-0/program/src/lib.rs
There are a couple of syntax elements to point out here.
### `#[program]`
First, notice that a program is defined with the `#[program]` attribute, where each
* `#[program]` First, notice that a program is defined with the `#[program]` attribute, where each
inner method defines an RPC request handler, or, in Solana parlance, an "instruction"
handler. These handlers are the entrypoints to your program that clients may invoke, as
we will see soon.
### `Context<Initialize>`
The first parameter of _every_ RPC handler is the `Context` struct, which is a simple
* `Context<Initialize>` The first parameter of _every_ RPC handler is the `Context` struct, which is a simple
container for the currently executing `program_id` generic over
`Accounts`--here, the `Initialize` struct.
### `#[derive(Accounts)]`
The `Accounts` derive macro marks a struct containing all the accounts that must be
* `#[derive(Accounts)]` The `Accounts` derive macro marks a struct containing all the accounts that must be
specified for a given instruction. To understand Accounts on Solana, see the
[docs](https://docs.solana.com/developing/programming-model/accounts).
In subsequent tutorials, we'll demonstrate how an `Accounts` struct can be used to
specify constraints on accounts given to your program. Since this example doesn't touch any
accounts, we skip this (important) detail.
## Building a program
## Building and Emitting an IDL
This program can be built in same way as any other Solana program.
```bash
cargo build-bpf
```
## Deploying a program
Similarly, we can deploy the program using the `solana deploy` command.
```bash
solana deploy <path-to-your-repo>/anchor/target/deploy/basic_program_0.so
```
Making sure to susbstitute paths to match your local filesystem. Now, save the address
the program was deployed with. It will be useful later.
## Emmiting an IDL
After creating a program, one can use the Anchor CLI to emit an IDL, from which clients
After creating a program, one can use the `anchor` CLI to build and emit an IDL, from which clients
can be generated.
```bash
anchor idl -f src/lib.rs -o idl.js
anchor build
```
Inspecting the contents of `idl.js` one should see
::: details
The `build` command is a convenience combining two steps.
1) `cargo build-bpf`
2) `anchor idl -f src/lib.rs -o basic.json`.
:::
Once run, you should see your build artifacts, as usual, in your `target/` directory. Additionally,
a `basic.json` file is created. Inspecting its contents you should see
```json
{
@ -92,7 +73,23 @@ Inspecting the contents of `idl.js` one should see
}
```
For experienced Ethereum developers, this is analogous to an `abi.json` file.
From which a client can be generated. Note that this file is created by parsing the `src/lib.rs`
file in your program's crate.
::: tip
If you've developed on Ethereum, the IDL is analogous to the `abi.json`.
:::
## Deploying a program
Once built, we can deploy the program using the `solana deploy` command.
```bash
solana deploy <path-to-your-repo>/anchor/target/deploy/basic_program_0.so
```
Making sure to susbstitute paths to match your local filesystem. Now, save the address
the program was deployed with. It will be useful later.
## Generating a Client

View File

@ -0,0 +1 @@
# Tutorial 2: Constraints and Access Control

View File

@ -0,0 +1 @@
# Tutorial 3: Workspaces

View File

@ -1,5 +1,3 @@
// TODO: replace path once the package is published.
//
// Before running this script, make sure to run `yarn && yarn build` inside
// the `ts` directory.
const anchor = require('../../../../ts');

View File

@ -0,0 +1,25 @@
// Before running this script, make sure to run `yarn && yarn build` inside
// the `ts` directory.
const anchor = require('../../../../ts');
const fs = require('fs');
// Configure the local cluster.
anchor.setProvider(anchor.Provider.local());
// #region main
async function main() {
// Read the generated IDL.
const idl = JSON.parse(fs.readFileSync('../idl.json', 'utf8'));
// Address of the deployed program.
const programId = new anchor.web3.PublicKey('<YOUR-PROGRAM-ID>');
// Generate the program client from IDL.
const program = new anchor.Program(idl, programId);
// Execute the RPC.
await program.rpc.initialize();
}
// #endregion main
main();

View File

@ -15,3 +15,4 @@ syn = { version = "1.0.54", features = ["full", "extra-traits", "parsing"] }
anyhow = "1.0.32"
heck = "0.3.1"
serde = { version = "1.0.118", features = ["derive"] }
serde_json = "1.0"

View File

@ -9,6 +9,8 @@ pub struct Idl {
pub accounts: Vec<IdlTypeDef>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub types: Vec<IdlTypeDef>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub metadata: Option<serde_json::Value>,
}
#[derive(Debug, Serialize, Deserialize)]

View File

@ -2,7 +2,6 @@ use crate::{
AccountsStruct, Constraint, ConstraintBelongsTo, ConstraintLiteral, ConstraintOwner,
ConstraintSigner, Field, ProgramAccountTy, Ty,
};
use quote::quote;
pub fn parse(strct: &syn::ItemStruct) -> AccountsStruct {
let fields = match &strct.fields {

View File

@ -3,17 +3,17 @@ use crate::parser::anchor;
use crate::parser::program;
use crate::AccountsStruct;
use anyhow::Result;
use heck::MixedCase;
use quote::ToTokens;
use std::collections::{HashMap, HashSet};
use std::fs::File;
use std::io::Read;
use heck::MixedCase;
use heck::CamelCase;
use std::path::Path;
static DERIVE_NAME: &'static str = "Accounts";
// Parse an entire interface file.
pub fn parse(filename: &str) -> Result<Idl> {
pub fn parse(filename: impl AsRef<Path>) -> Result<Idl> {
let mut file = File::open(&filename)?;
let mut src = String::new();
@ -91,6 +91,7 @@ pub fn parse(filename: &str) -> Result<Idl> {
instructions,
types,
accounts,
metadata: None,
})
}
@ -164,12 +165,7 @@ fn parse_ty_defs(f: &syn::File) -> Result<Vec<IdlTypeDef>> {
let mut tts = proc_macro2::TokenStream::new();
f.ty.to_tokens(&mut tts);
Ok(IdlField {
name: f
.ident
.as_ref()
.unwrap()
.to_string()
.to_mixed_case(),
name: f.ident.as_ref().unwrap().to_string().to_mixed_case(),
ty: tts.to_string().parse()?,
})
})

View File

@ -28,7 +28,8 @@
"@types/bn.js": "^4.11.6",
"bn.js": "^5.1.2",
"buffer-layout": "^1.2.0",
"camelcase": "^5.3.1"
"camelcase": "^5.3.1",
"find": "^0.3.0"
},
"devDependencies": {
"@commitlint/cli": "^8.2.0",

View File

@ -3,6 +3,7 @@ import * as web3 from "@solana/web3.js";
import { Provider } from "@project-serum/common";
import { Program } from "./program";
import Coder from "./coder";
import workspace from './workspace';
let _provider: Provider | null = null;
@ -14,4 +15,4 @@ function getProvider(): Provider {
return _provider;
}
export { Program, Coder, setProvider, getProvider, Provider, BN, web3 };
export { workspace, Program, Coder, setProvider, getProvider, Provider, BN, web3 };

57
ts/src/workspace.ts Normal file
View File

@ -0,0 +1,57 @@
import camelCase from "camelcase";
import { Program } from './program';
let _populatedWorkspace = false;
// Workspace program discovery only works for node environments.
export default new Proxy({} as any, {
get(
workspaceCache: { [key: string]: Program },
programName: string
) {
const find = require('find');
const fs = require('fs');
const process = require('process');
if (typeof window !== 'undefined') {
throw new Error(
'`anchor.workspace` is not available in the browser'
);
}
if (!_populatedWorkspace) {
const path = require('path');
let projectRoot = process.cwd();
while (!fs.existsSync(path.join(projectRoot, 'Anchor.toml'))) {
const parentDir = path.dirname(projectRoot);
if (parentDir === projectRoot) {
projectRoot = undefined;
}
projectRoot = parentDir;
}
if (projectRoot === undefined) {
throw new Error(
'Could not find workspace root. Perhaps set the `OASIS_WORKSPACE` env var?'
);
}
find
.fileSync(/target\/idl\/.*\.json/, projectRoot)
.reduce((programs: any, path: string) => {
const idlStr = fs.readFileSync(path);
const idl = JSON.parse(idlStr);
const name = camelCase(idl.name, { pascalCase: true });
programs[name] = new Program(idl, idl.metadata.address);
return programs;
}, workspaceCache);
_populatedWorkspace = true;
}
return workspaceCache[programName];
},
}
);

View File

@ -10,6 +10,10 @@ const WORKSPACE = {
};
describe('Constraints program tests', () => {
it('Parses a workspace', async () => {
});
it('Runs against a localnetwork', async () => {
// Configure the local cluster.
anchor.setProvider(WORKSPACE.provider);

View File

@ -2515,6 +2515,13 @@ find-versions@^3.2.0:
dependencies:
semver-regex "^2.0.0"
find@^0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/find/-/find-0.3.0.tgz#4082e8fc8d8320f1a382b5e4f521b9bc50775cb8"
integrity sha512-iSd+O4OEYV/I36Zl8MdYJO0xD82wH528SaCieTVHhclgiYNe9y+yPKSwK+A7/WsmHL1EZ+pYUJBXWTL5qofksw==
dependencies:
traverse-chain "~0.1.0"
flat-cache@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11"
@ -5667,6 +5674,11 @@ tr46@^2.0.2:
dependencies:
punycode "^2.1.1"
traverse-chain@~0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/traverse-chain/-/traverse-chain-0.1.0.tgz#61dbc2d53b69ff6091a12a168fd7d433107e40f1"
integrity sha1-YdvC1Ttp/2CRoSoWj9fUMxB+QPE=
trim-newlines@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-2.0.0.tgz#b403d0b91be50c331dfc4b82eeceb22c3de16d20"