Anchor - Tutorial 1
Tutorial 0 here.
Let's start with the project scaffold again
anchor init --javascript base1
cd base1
First, note the --javascript.. anchor defaults to typescript for the scaffold, so adding this option generates javascript files instead. The examples in the tutorial are done in javascript, so this helps.
let's run test
anchor test
and it runs fine. However, if you try node repl, you won't be able to replicate the test harness so easily. What is anchor test doing?
fn test(
cfg_override: &ConfigOverride,
skip_deploy: bool,
skip_local_validator: bool,
skip_build: bool,
detach: bool,
extra_args: Vec<String>,
cargo_args: Vec<String>,
) -> Result<()> {
with_workspace(cfg_override, |cfg| {
// Build if needed.
if !skip_build {
build(
cfg_override,
None,
None,
false,
None,
None,
None,
BootstrapMode::None,
None,
None,
cargo_args,
)?;
}
// Run the deploy against the cluster in two cases:
//
// 1. The cluster is not localnet.
// 2. The cluster is localnet, but we're not booting a local validator.
//
// In either case, skip the deploy if the user specifies.
let is_localnet = cfg.provider.cluster == Cluster::Localnet;
if (!is_localnet || skip_local_validator) && !skip_deploy {
deploy(cfg_override, None)?;
}
// Start local test validator, if needed.
let mut validator_handle = None;
if is_localnet && (!skip_local_validator) {
let flags = match skip_deploy {
true => None,
false => Some(validator_flags(cfg)?),
};
validator_handle = Some(start_test_validator(cfg, flags, true)?);
}
let url = cluster_url(cfg);
let node_options = format!(
"{} {}",
match std::env::var_os("NODE_OPTIONS") {
Some(value) => value
.into_string()
.map_err(std::env::VarError::NotUnicode)?,
None => "".to_owned(),
},
get_node_dns_option()?,
);
// Setup log reader.
let log_streams = stream_logs(cfg, &url);
// Run the tests.
let test_result: Result<_> = {
let cmd = cfg
.scripts
.get("test")
.expect("Not able to find command for `test`")
.clone();
let mut args: Vec<&str> = cmd
.split(' ')
.chain(extra_args.iter().map(|arg| arg.as_str()))
.collect();
let program = args.remove(0);
std::process::Command::new(program)
.args(args)
.env("ANCHOR_PROVIDER_URL", url)
.env("ANCHOR_WALLET", cfg.provider.wallet.to_string())
.env("NODE_OPTIONS", node_options)
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.output()
.map_err(anyhow::Error::from)
.context(cmd)
};
// Keep validator running if needed.
if test_result.is_ok() && detach {
println!("Local validator still running. Press Ctrl + C quit.");
std::io::stdin().lock().lines().next().unwrap().unwrap();
}
// Check all errors and shut down.
if let Some(mut child) = validator_handle {
if let Err(err) = child.kill() {
println!("Failed to kill subprocess {}: {}", child.id(), err);
}
}
for mut child in log_streams? {
if let Err(err) = child.kill() {
println!("Failed to kill subprocess {}: {}", child.id(), err);
}
}
// Must exist *after* shutting down the validator and log streams.
match test_result {
Ok(exit) => {
if !exit.status.success() {
std::process::exit(exit.status.code().unwrap());
}
}
Err(err) => {
println!("Failed to run test: {:#}", err)
}
}
Ok(())
})
}
In pseudocode:
- builds if not already built
- runs deploy
- starts local test validator if necessary, if not uses cluster/local cluster
- runs the program and tests
- shuts down and cleans up
In running the program and tests it relies on having access to a cluster or test_validator.
The great thing is that the anchor test harness basically preps the entire test environment for you, even if its opaque. The bad thing is that when you go into production you're going to have to sort out how to handle this in a CI/CD environment.
Looking through the test, this program does two things:
- creates and initializes an account in a single atomic transaction
- updates a previously created account
Got that? Accounts are like files, so this is basically the creation, read and update of data to the solana blockchain. And of course there is no real delete for blockchains, just overwrite. So this solves CRUD. Yay! That was quick.
Just to start off with I copied and refactored the tests for this tutorial.
Having done that, how would we tackle this?
First we need to look at programs/base1/src/lib.rs
use anchor_lang::prelude::*;
declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");
#[program]
pub mod base1 {
use super::*;
pub fn initialize(ctx: Context<Initialize>) -> ProgramResult {
Ok(())
}
}
#[derive(Accounts)]
pub struct Initialize {}
This is the scaffold start, same as tutorial 0. Now looking at the test case, we need to initialize this program with some data, and we need to sign the transaction so that it actually happens.
Following along in the basic1 tutorial, just note that you have to refer to several places
- anchor/docs/src/tutorials in the github
- https://project-serum.github.io/anchor/tutorials/tutorial-1.html#defining-a-program
- https://project-serum.github.io/anchor/tutorials/tutorial-2.html#clone-the-repo
Yes, in order to truly understand tutorial 1... one must refer to tutorial 2. I have the fully commented code below.
One thing to note is that anchor basically uses rust to abstract away a lot of validation, serialization and deserialization. In short, solana_program is close to the chain, and anchor provides a rust "safe" interface which takes care of a bunch of assumptions. These take the form of constraints on inputs encapsulated by structs, and loading a bunch of things from the environment rather than having them passed to the program.
This makes the code more concise, but also more dense. For a beginner, every character of code is meaningful... and I ended up taking apart the whole thing to try and understand what's going on.
Below is lib.rs with comments integrated from the tutorial 1.
use anchor_lang::prelude::*;
declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");
// Program definition block
/*
* #[program] : attribute : anchor_lang : 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.
*/
#[program]
mod basic_1 {
use super::*;
/*
* In this function, take a mutable reference to my_account and
* assign data to it
*
* data : argument: here : any valid Rust types can be passed
* as inputs
*
* ProgramResult : type : solana_program : type that defines the
* generic result/error of a solana program
*/
pub fn initialize(ctx: Context<Initialize>, data:u64) -> ProgramResult {
let my_account = &mut ctx.accounts.my_account;
my_account.data = data;
Ok(())
}
/*
* almost identical function to Initialize but with different
* input struct Update
*/
pub fn update(ctx: Context<Update>, data: u64) -> ProgramResult {
let my_account = &mut ctx.accounts.my_account;
my_account.data = data;
Ok(())
}
}
// Struct definition block
/*
* The Initalize and Update structs are used to define the inputs of
* the RPC request handlers, used by the program.
*/
/*
* #[derive(Accounts)] : macro: anchor_lang: derive macro
* implementing the Accounts trait, allowing a struct to transform
* from the untrusted &[AccountInfo] slice given to a Solana program
* into a validated struct of deserialized account types.
*
* 'info : lifetime parameter
*/
#[derive(Accounts)]
pub struct Initialize <'info>{
/*
* In effect we are creating a user field, assigning it a type of
* Signer, then using the info to populate the value when invoked.
* The validation of the Signer account is performed by type
* checking. Because we are changing the data upon the initialize,
* we mark this field mutable.
*
* Signer : type: anchor_lang : Type validating that the account
* signed the transaction. No other ownership or type checks are
* done. If this is used, one should not try to access the
* underlying account data.
*
* #[account(mut)] : attribute macro : anchor_lang : Marking an
* account as mut persists any changes made upon exiting the
* program.
*/
#[account(mut)]
pub user: Signer<'info>,
/*
* my_account : field : here : field is of type
* Account<'info, MyAccount> and the deserialized data structure
* is MyAccount. The my_account field is marked with the init
* attribute. This will create a new account owned by the current
* program, zero initialized. When using init, we must provide:
* payer: which will fund the account creation
* space: defines how large the account should be
* system_program: required by the runtime to create the account.
*/
#[account(init, payer = user, space = 8 + 8)]
pub my_account: Account<'info, MyAccount>,
pub system_program: Program<'info, System>,
}
#[derive(Accounts)]
pub struct Update<'info> {
#[account(mut)]
pub my_account: Account<'info, MyAccount>,
}
/*
* #[account] : attribute macro : anchor_lang : attribute macro
* implementing AccountSerialize and AccountDeserialize,
* automatically prepending a unique 8 byte discriminator to the
* account array. The discriminator is defined by the first 8 bytes
* of the Sha256 hash of the account's Rust identifier --i.e., the
* struct type name --and ensures no account can be substituted for
* another.
*/
#[account]
pub struct MyAccount {
pub data: u64,
}
Test cases are passing! Yay, but I think the tests can be refactored a little. Let's do so.
const assert = require("assert");
const anchor = require("@project-serum/anchor");
const { SystemProgram } = anchor.web3;
describe("basic-1", () => {
// Use a local provider.
const provider = anchor.Provider.local();
// Configure the client to use the local cluster.
anchor.setProvider(provider);
// The program to execute.
const program = anchor.workspace.Basic1;
// The Account to create.
const myAccount = anchor.web3.Keypair.generate();
it("Creates and initializes an account in a single atomic transaction", async () => {
// Create the new account and initialize it with the program.
await program.rpc.initialize(new anchor.BN(1234), {
accounts: {
myAccount: myAccount.publicKey,
user: provider.wallet.publicKey,
systemProgram: SystemProgram.programId,
},
signers: [myAccount],
});
// Fetch the newly created account from the cluster.
const account = await program.account.myAccount.fetch(myAccount.publicKey);
// Check it's state was initialized.
assert.ok(account.data.eq(new anchor.BN(1234)));
});
it("Updates a previously created account", async () => {
// Invoke the update rpc.
await program.rpc.update(new anchor.BN(4321), {
accounts: {
myAccount: myAccount.publicKey,
},
});
// Fetch the newly updated account.
const account = await program.account.myAccount.fetch(myAccount.publicKey);
// Check it's state was mutated.
assert.ok(account.data.eq(new anchor.BN(4321)));
});
});
Refactoring the test cases gives us a simple suite with setup at the top, and two tests, one to initialize (create) and one to update. The read is implicit in both tests.
Client code in this case will mirror the test cases.. so we don't need to build a client.
Notes:
- Lars pointed out that the parameters passed in js are cased different that parameters in the rust code.. and he is correct. After some testing I determined, that there are some assumptions being made about casing by anchor
Identifiers in rust must be upper camel case
pub struct XAccount {
pub data: u64,
}
and can be accessed in js with lower camel case
// Fetch the newly created account from the cluster.
const account = await program.account.xAccount.fetch(myAccount.publicKey);
Similarly
field in snake case in rust
#[derive(Accounts)]
pub struct Update<'info> {
#[account(mut)]
pub y_account: Account<'info, XAccount>,
}
can be accessed in lower camel case in js
// Invoke the update rpc.
await program.rpc.update(new anchor.BN(4321), {
accounts: {
yAccount: myAccount.publicKey,
},
});
I updated the tutorial git with these changes... just so that we can see the differences.