#2 - Introduction to Anchor

Author: @ironaddicteddog

[Updated at 2022.12.18]

You can find the full code base here

What is Anchor?

There is a comprehensive explanation on the official website. Let me just quote relative paragraphs here:

Anchor is a framework for Solana's Sealevel runtime providing several convenient developer tools.

If you're familiar with developing in Ethereum's Solidity, Truffle, web3.js, 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.

In short, Anchor gives you the following handy tools for developing Solana programs:

  • Rust crates and eDSL for writing Solana programs

  • IDL specification

  • TypeScript package for generating clients from IDL

  • CLI and workspace management for developing complete applications

You can watch this awesome talk given by Armani Ferrante at Breakpoint 2021 to feel the power of Anchor.

Workflow

  1. Develop the program (Smart Contract)

  2. Build the program and export the IDL

  3. Generate the client representation of program from the IDL to interact with the program

Why Anchor?

  • Productivity

    • Make Solana program more intuitive to understand

    • More clear buisness Logic

    • Remove a ton of biolderplate code

  • Security

    • Customized Account Validation

      • Singer

      • Mut

      • ...

    • Discriminator

      • Discriminator is generated and inserted into the first 8 bytes of account data. Ex: sha256("account:<MyAccountName>")[..8] || borsh(account_struct)

      • Used for more secure account validation and function dispatch

      • See this Twitter thread for more details

      • See here and here for the actual implementation

Before We Start

Why Rust? Why Solana?

You can refer to this doc for the motivations.

Prerequisites

Installation

Install avm:

Install latest anchor version:

If you haven't installed cargo, please refer to this doc for installation steps.

Extra Dependencies on Linux (Optional)

You may have to install some extra dependencies on Linux (ex. Ubuntu):

Verify the Installation

Check if Anchor is successfully installed:

Escrow Program

Reminder: you can find the full code base for this example here. However, I would strongly recommend you to go through the copy-paste with me to get familiar with the flow.

Next, let's develop an escrow program using Anchor. I strongly recommend you to go through this tutorial if you are not familiar with escrow program yet.

Overview

Since this program is extended from the original Escrow Program, I assumed you have gone through the original blog post at least once.

However, there is one major difference between this exmaple and the original Escrow program: Instead of letting initializer create a token account to be reset to a PDA authority, we create a token account Vault that has both a PDA key and a PDA authority.

Initialize

Initializer can send a transaction to the escrow program to initialize the Vault. In this transaction, two new accounts: Vault and EscrowState, will be created and tokens (Token A) to be exchanged will be transfered from Initializer to Vault.

Cancel

Initializer can also send a transaction to the escrow program to cancel the demand of escrow. The tokens will be transfered back to the Initialzer and both Vault and EscrowState will be closed in this case.

Exchange

Taker can send a transaction to the escrow to exchange Token B for Token A. First, tokens (Token B) will be transfered from Taker to Initializer. Afterward, the tokens (Token A) kept in the Vault will be transfered to Taker. Finally, both Vault and EscrowState will be closed.

Initialize the Program

First, let's start a fresh Anchor project:

This handy command will populate a project folder including the following files:

  • Cargo.toml

  • Anchor.toml

  • package.json

  • tsconfig.json

  • ...

Program Architecture

There are 3 main parts in the program:

  • Processor: Main buisiness logic locates in processor

  • Account Context (Instructions): Instruction data packing/unpacking and account constraints and access control locate in Instruction handling part

  • Account: Declaration of account owned by program locates in account part

Dependencies

Before we dive into the program, we need add the missing dependencies in Cargo.toml:

Update program_id (Optional)

There is a default program_id defined by declare_id! macro in lib.rs:

Although we can use the default value just fine, I would strongly recommend to replace this with the actual program_id, which is the public key of the deploy key.

Get the public key of the deploy key:

Replace the default value of program_id with this new value:

Processor (Part 1)

Let's scaffold the processor first. There should be 3 functions corresponding 3 tasks listed above:

The #[program] keyword is what makes the magic happen. In argument ctx, notice that we have to use a type Initialize for Context<T> generic. Initialize can be considered as a wrapper for instructions. This wrapper is enhanced by Anchor via derived macro (#[derive(account)]). We will see how it works real quick.

Each function has a corresponding instruction. As a result, there will be 3 instruction wrappers.

Instructions (Part 1)

From the processor section, we know that each function defined needs a corresponding instruction. So let's define those in instruction section:

Depending on the program functions, the instructions should bring in the accounts that are needed for operations.

To see what are accounts needed for initializing escrow account, we have to consider what data stored in escrow account first.

Program Account

Accounts that are owned and managed by the program are defined in the #[account] section.

EscrowAccount

Field
Type
Description

initializer_key

Pubkey

To authorize the actions properly

initializer_deposit_token_account

Pubkey

To record the deposit account of initialzer

initializer_receive_token_account

Pubkey

To record the receiving account of initializer

initializer_amount

u64

To record how much token should the initializer transfer to taker

taker_amount

u64

To record how much token should the initializer receive from the taker

As a result, we should design an account that stores the minimum information to validate the escrow state and keep the integrity of the program:

Instructions (Part 2)

According to what we have in EscrowAccount, we need the following accounts to initialize it.

Initialize

Field
Type
Description

initializer

AccountInfo

Signer of InitialEscrow instruction. To be stored in EscrowAccount

initializer_deposit_token_account

Account<TokenAccount>

The account of token account for token exchange. To be stored in EscrowAccount

initializer_receive_token_account

Account<TokenAccount>

The account of token account for token exchange. To be stored in EscrowAccount

token_program

AccountInfo

The account of TokenProgram

escrow_account

Box<Account<EscrowAccount>>

The account of EscrowAccount

vault_account

Account<TokenAccount>

The account of Vault, which is created by Anchor via constraints. (Will be explained in part 3)

mint

Account<Mint>

-

system_program

AccountInfo

-

rent

Sysvar<Rent>

-

Cancel

Field
Type
Description

initializer

AccountInfo

The initializer of EscrowAccount

initializer_deposit_token_account

Account<TokenAccount>

The address of token account for token exchange

vault_account

Account<TokenAccount>

The program derived address

vault_authority

AccountInfo

The program derived address

escrow_account

Box<Account<EscrowAccount>>

The address of EscrowAccount. Have to check if the EscrowAccount follows certain constraints.

token_program

AccountInfo

The address of TokenProgram

Exchange

Field
Type
Description

taker

AccountInfo

Singer of Exchange instruction

taker_deposit_token_account

Account<TokenAccount>

Token account for token exchange

taker_receive_token_account

Account<TokenAccount>

Token account for token exchange

initializer_deposit_token_account

Account<TokenAccount>

Token account for token exchange

initializer_receive_token_account

Account<TokenAccount>

Token account for token exchange

initializer

AccountInfo

To be used in constraints. (Will explain in part 3)

escrow_account

Box<Account<EscrowAccount>>

The address of EscrowAccount. Have to check if the EscrowAccount follows certain constraints.

vault_account

Account<TokenAccount>

The program derived address

vault_authority

AccountInfo

The program derived address

token_program

AccountInfo

The address of TokenProgram

You can tell this is a very long list of inputs since Solana programs are stateless.

Notice the lifetime anotation used in generic

You can see there are 2 different types for account: AccountInfo and Account. So what is the difference? I suppose it's proper to use Account over AccountInfo when you want Anchor to deserialize the data for convenience. In that case, you can access the account data via a trivial method call. For example: ctx.accounts.vault_account.mint

Processor (Part 2)

With necessary accounts, we can implement the business logic inside processor without bothering:

Now the business logic is simple, straightforward, and clear to understand.

  • In initialize, what happens is that the input accounts are assigned to EscrowAccount fields one by one. Then, a program derived address, or PDA, is derived to be going to become new authority of initializer_deposit_token_account.

  • In cancel, it just simply reset the authority from PDA back to the initializer.

  • In exchange, 3 things happen:

    • First, token A gets transfered from pda_deposit_token_account to taker_receive_token_account.

    • Next, token B gets transfered from taker_deposit_token_account to initializer_receive_token_account.

    • Finally, the authority of pda_deposit_token_account gets set back to the initializer.

Utils

There are some util functions used for wrapping the data to be passed in tokens::transfer, token::close_account and token::set_authority. It might look a bit overwhelmed in the first place. However, the purpose behind these functions are clear and simple:

Instructions (Part 3)

Finally, let's talk about the account constraints. Here comes a very handy funcionality that Anchor provides: Account Constraints.

Constraints are useful for basic checkings such as whether the initializer is the signer of instruction.

If you are familiar of Solidity, you can map this concept to solidity modifier.

Here, we can see a few new attributes, such as:

Attribute
Description

#[account(signer)]

Checks the given account signed the transaction

#[account(mut)]

Marks the account as mutable and persists the state transition

#[account(constraint = <expression\>)]

Executes the given code as a constraint. The expression should evaluate to a boolean

#[account(close = <target\>)]

Marks the account as being closed at the end of the instruction’s execution, sending the rent exemption lamports to the specified

Notice that we used a rather complex constraint to create an token account that has a PDA key (See this code snippet for more details). Let's take a closer look of it:

Check the official document for more constraints.

Now, the program should compile again successfully:

Build and Test

So far we have only accomplished the first part of the workflow. Let's write some client side test for it.

Interface Description Language (IDL)

First, you can access the IDL via the following path:

This will print the full IDL on the terminal:

As you can see, the IDL basically defines everything needed for a client representation.

You can think of IDL as ABI if you are familiar with Ethereum and Solidity.

Next, lets move to tests/anchor-escrow.ts to implement the tests.

Setup

Before we dive into the actual test cases, let's first setup the boilerlate for the tests:

Note: target/types/anchor_escrow is generated by running anchor build. Make sure you build the program first.

You can see there are 4 test cases to be completed. However, the first test case Initialize program state is used for program state setup such as minting tokens. As a result, there should be only 3 test cases corresponding to 3 functions of the program.

Let's finish the program state initialization:

We should be able to pass the first test case at this point:

Implement Tests for initialize, exchange and cancel

Next, we add the test case for initialize:

We should see 2 implemented test cases passed at this point:

Similarly, let's implement the rest of the tests real quick:

We should see all test cases passed at this moment:

And that's it!

References

  • https://github.com/ironaddicteddog/anchor-escrow

  • https://hackmd.io/@ironaddicteddog/solana-starter-kit

  • https://www.youtube.com/watch?v=cvW8EwGHw8U

  • https://github.com/project-serum/anchor/blob/master/CHANGELOG.md

  • https://paulx.dev/blog/2021/01/14/programming-on-solana-an-introduction/

  • https://project-serum.github.io/anchor/getting-started/introduction.html

  • https://docs.rs/anchor-lang/0.18.2/anchor_lang/derive.Accounts.html

  • https://anchor.projectserum.com/

  • https://blog.soteria.dev/?p=ef42d944f086

Last updated