Anchor CPIs and Errors

Lesson Objectives

By the end of this lesson, you will be able to:

  • Make Cross Program Invocations (CPIs) from an Anchor program
  • Use the cpi feature to generate helper functions for invoking instructions on existing Anchor programs
  • Use invoke and invoke_signed to make CPIs where CPI helper functions are unavailable
  • Create and return custom Anchor errors

TL;DR

  • Anchor provides a simplified way to create CPIs using a CpiContext
  • Anchor's cpi feature generates CPI helper functions for invoking instructions on existing Anchor programs
  • If you do not have access to CPI helper functions, you can still use invoke and invoke_signed directly
  • The error_code attribute macro is used to create custom Anchor Errors

Overview

If you think back to the first CPI lesson, you'll remember that constructing CPIs can get tricky with vanilla Rust. Anchor makes it a bit simpler though, especially if the program you're invoking is also an Anchor program whose crate you can access.

In this lesson, you'll learn how to construct an Anchor CPI. You'll also learn how to throw custom errors from an Anchor program so that you can start to write more sophisticated Anchor programs.

Cross Program Invocations (CPIs) with Anchor

As a refresher, CPIs allow programs to invoke instructions on other programs using the invoke or invoke_signed functions. This allows new programs to build on top of existing programs (we call that composability).

While making CPIs directly using invoke or invoke_signed is still an option, Anchor also provides a simplified way to make CPIs by using a CpiContext.

In this lesson, you'll use the anchor_spl crate to make CPIs to the SPL Token Program. You can explore what's available in the anchor_spl crate here.

CpiContext

The first step in making a CPI is to create an instance of CpiContext. CpiContext is very similar to Context, the first argument type required by Anchor instruction functions. They are both declared in the same module and share similar functionality.

The CpiContext type specifies non-argument inputs for cross program invocations:

  • accounts - the list of accounts required for the instruction being invoked
  • remaining_accounts - any remaining accounts
  • program - the program ID of the program being invoked
  • signer_seeds - if a PDA is signing, include the seeds required to derived the PDA
1pub struct CpiContext<'a, 'b, 'c, 'info, T>
2where
3    T: ToAccountMetas + ToAccountInfos<'info>,
4{
5    pub accounts: T,
6    pub remaining_accounts: Vec<AccountInfo<'info>>,
7    pub program: AccountInfo<'info>,
8    pub signer_seeds: &'a [&'b [&'c [u8]]],
9}

You use CpiContext::new to construct a new instance when passing along the original transaction signature.

1CpiContext::new(cpi_program, cpi_accounts)
1pub fn new(
2        program: AccountInfo<'info>,
3        accounts: T
4    ) -> Self {
5    Self {
6        accounts,
7        program,
8        remaining_accounts: Vec::new(),
9        signer_seeds: &[],
10    }
11}

You use CpiContext::new_with_signer to construct a new instance when signing on behalf of a PDA for the CPI.

1CpiContext::new_with_signer(cpi_program, cpi_accounts, seeds)
1pub fn new_with_signer(
2    program: AccountInfo<'info>,
3    accounts: T,
4    signer_seeds: &'a [&'b [&'c [u8]]],
5) -> Self {
6    Self {
7        accounts,
8        program,
9        signer_seeds,
10        remaining_accounts: Vec::new(),
11    }
12}

CPI accounts

One of the main things about CpiContext that simplifies cross-program invocations is that the accounts argument is a generic type that lets you pass in any object that adopts the ToAccountMetas and ToAccountInfos<'info> traits.

These traits are added by the #[derive(Accounts)] attribute macro that you've used before when creating structs to represent instruction accounts. That means you can use similar structs with CpiContext.

This helps with code organization and type safety.

Invoke an instruction on another Anchor program

When the program you're calling is an Anchor program with a published crate, Anchor can generate instruction builders and CPI helper functions for you.

Simply declare your program's dependency on the program you're calling in your program's Cargo.toml file as follows:

1[dependencies]
2callee = { path = "../callee", features = ["cpi"]}

By adding features = ["cpi"], you enable the cpi feature and your program gains access to the callee::cpi module.

The cpi module exposes callee's instructions as a Rust function that takes as arguments a CpiContext and any additional instruction data. These functions use the same format as the instruction functions in your Anchor programs, only with CpiContext instead of Context. The cpi module also exposes the accounts structs required for calling the instructions.

For example, if callee has the instruction do_something that requires the accounts defined in the DoSomething struct, you could invoke do_something as follows:

1use anchor_lang::prelude::*;
2use callee;
3...
4
5#[program]
6pub mod lootbox_program {
7    use super::*;
8
9    pub fn call_another_program(ctx: Context<CallAnotherProgram>, params: InitUserParams) -> Result<()> {
10        callee::cpi::do_something(
11            CpiContext::new(
12                ctx.accounts.callee.to_account_info(),
13                callee::DoSomething {
14                    user: ctx.accounts.user.to_account_info()
15                }
16            )
17        )
18        Ok(())
19    }
20}
21...

Invoke an instruction on a non-Anchor program

When the program you're calling is not an Anchor program, there are two possible options:

  1. It's possible that the program maintainers have published a crate with their own helper functions for calling into their program. For example, the anchor_spl crate provides helper functions that are virtually identical from a call-site perspective to what you would get with the cpi module of an Anchor program. E.g. you can mint using the mint_to helper function and use the MintTo accounts struct.
    1token::mint_to(
    2    CpiContext::new_with_signer(
    3        ctx.accounts.token_program.to_account_info(),
    4        token::MintTo {
    5            mint: ctx.accounts.mint_account.to_account_info(),
    6            to: ctx.accounts.token_account.to_account_info(),
    7            authority: ctx.accounts.mint_authority.to_account_info(),
    8        },
    9        &[&[
    10            "mint".as_bytes(),
    11            &[*ctx.bumps.get("mint_authority").unwrap()],
    12        ]]
    13    ),
    14    amount,
    15)?;
  2. If there is no helper module for the program whose instruction(s) you need to invoke, you can fall back to using invoke and invoke_signed. In fact, the source code of the mint_to helper function referenced above shows an example us using invoke_signed when given a CpiContext. You can follow a similar pattern if you decide to use an accounts struct and CpiContext to organize and prepare your CPI.
    1pub fn mint_to<'a, 'b, 'c, 'info>(
    2    ctx: CpiContext<'a, 'b, 'c, 'info, MintTo<'info>>,
    3    amount: u64,
    4) -> Result<()> {
    5    let ix = spl_token::instruction::mint_to(
    6        &spl_token::ID,
    7        ctx.accounts.mint.key,
    8        ctx.accounts.to.key,
    9        ctx.accounts.authority.key,
    10        &[],
    11        amount,
    12    )?;
    13    solana_program::program::invoke_signed(
    14        &ix,
    15        &[
    16            ctx.accounts.to.clone(),
    17            ctx.accounts.mint.clone(),
    18            ctx.accounts.authority.clone(),
    19        ],
    20        ctx.signer_seeds,
    21    )
    22    .map_err(Into::into)
    23}

Throw errors in Anchor

We're deep enough into Anchor at this point that it's important to know how to create custom errors.

Ultimately, all programs return the same error type: ProgramError. However, when writing a program using Anchor you can use AnchorError as an abstraction on top of ProgramError. This abstraction provides additional information when a program fails, including:

  • The error name and number
  • Location in the code where the error was thrown
  • The account that violated a constraint
1pub struct AnchorError {
2    pub error_name: String,
3    pub error_code_number: u32,
4    pub error_msg: String,
5    pub error_origin: Option<ErrorOrigin>,
6    pub compared_values: Option<ComparedValues>,
7}

Anchor Errors can be divided into:

  • Anchor Internal Errors that the framework returns from inside its own code
  • Custom errors that you the developer can create

You can add errors unique to your program by using the error_code attribute. Simply add this attribute to a custom enum type. You can then use the variants of the enum as errors in your program. Additionally, you can add an error message to each variant using the msg attribute. Clients can then display this error message if the error occurs.

1#[error_code]
2pub enum MyError {
3    #[msg("MyAccount may only hold data below 100")]
4    DataTooLarge
5}

To return a custom error you can use the err or the error macro from an instruction function. These add file and line information to the error that is then logged by Anchor to help you with debugging.

1#[program]
2mod hello_anchor {
3    use super::*;
4    pub fn set_data(ctx: Context<SetData>, data: MyAccount) -> Result<()> {
5        if data.data >= 100 {
6            return err!(MyError::DataTooLarge);
7        }
8        ctx.accounts.my_account.set_inner(data);
9        Ok(())
10    }
11}
12
13#[error_code]
14pub enum MyError {
15    #[msg("MyAccount may only hold data below 100")]
16    DataTooLarge
17}

Alternatively, you can use the require macro to simplify returning errors. The code above can be refactored to the following:

1#[program]
2mod hello_anchor {
3    use super::*;
4    pub fn set_data(ctx: Context<SetData>, data: MyAccount) -> Result<()> {
5        require!(data.data < 100, MyError::DataTooLarge);
6        ctx.accounts.my_account.set_inner(data);
7        Ok(())
8    }
9}
10
11#[error_code]
12pub enum MyError {
13    #[msg("MyAccount may only hold data below 100")]
14    DataTooLarge
15}

Demo

Let’s practice the concepts we’ve gone over in this lesson by building on top of the Movie Review program from previous lessons.

In this demo we’ll update the program to mint tokens to users when they submit a new movie review.

1. Starter

To get started, we will be using the final state of the Anchor Movie Review program from the previous lesson. So, if you just completed that lesson then you’re all set and ready to go. If you are just jumping in here, no worries, you can download the starter code here. We'll be using the solution-pdas branch as our starting point.

2. Add dependencies to Cargo.toml

Before we get started we need enable the init-if-needed feature and add the anchor-spl crate to the dependencies in Cargo.toml. If you need to brush up on the init-if-needed feature take a look at the Anchor PDAs and Accounts lesson.

1[dependencies]
2anchor-lang = { version = "0.25.0", features = ["init-if-needed"] }
3anchor-spl = "0.25.0"

3. Initialize reward token

Next, navigate to lib.rs and create an instruction to initialize a new token mint. This will be the token that is minted each time a user leaves a review. Note that we don't need to include any custom instruction logic since the initialization can be handled entirely through Anchor constraints.

1pub fn initialize_token_mint(_ctx: Context<InitializeMint>) -> Result<()> {
2    msg!("Token mint initialized");
3    Ok(())
4}

Now, implement the InitializeMint context type and list the accounts and constraints the instruction requires. Here we initialize a new Mint account using a PDA with the string "mint" as a seed. Note that we can use the same PDA for both the address of the Mint account and the mint authority. Using a PDA as the mint authority enables our program to sign for the minting of the tokens.

In order to initialize the Mint account, we'll need to include the token_program, rent, and system_program in the list of accounts.

1#[derive(Accounts)]
2pub struct InitializeMint<'info> {
3    #[account(
4        init,
5        seeds = ["mint".as_bytes()],
6        bump,
7        payer = user,
8        mint::decimals = 6,
9        mint::authority = mint,
10    )]
11    pub mint: Account<'info, Mint>,
12    #[account(mut)]
13    pub user: Signer<'info>,
14    pub token_program: Program<'info, Token>,
15    pub rent: Sysvar<'info, Rent>,
16    pub system_program: Program<'info, System>
17}

There may be some constraints above that you haven't seen yet. Adding mint::decimals and mint::authority along with init ensures that the account is initialized as a new token mint with the appropriate decimals and mint authority set.

4. Anchor Error

Next, let’s create an Anchor Error that we’ll use when validating the rating passed to either the add_movie_review or update_movie_review instruction.

1#[error_code]
2enum MovieReviewError {
3    #[msg("Rating must be between 1 and 5")]
4    InvalidRating
5}

5. Update add_movie_review instruction

Now that we've done some setup, let’s update the add_movie_review instruction and AddMovieReview context type to mint tokens to the reviewer.

Next, update the AddMovieReview context type to add the following accounts:

  • token_program - we'll be using the Token Program to mint tokens
  • mint - the mint account for the tokens that we'll mint to users when they add a movie review
  • token_account - the associated token account for the afforementioned mint and reviewer
  • associated_token_program - required because we'll be using the associated_token constraint on the token_account
  • rent - required because we are using the init-if-needed constraint on the token_account
1#[derive(Accounts)]
2#[instruction(title: String, description: String)]
3pub struct AddMovieReview<'info> {
4    #[account(
5        init,
6        seeds=[title.as_bytes(), initializer.key().as_ref()],
7        bump,
8        payer = initializer,
9        space = 8 + 32 + 1 + 4 + title.len() + 4 + description.len()
10    )]
11    pub movie_review: Account<'info, MovieAccountState>,
12    #[account(mut)]
13    pub initializer: Signer<'info>,
14    pub system_program: Program<'info, System>,
15    // ADDED ACCOUNTS BELOW
16    pub token_program: Program<'info, Token>,
17    #[account(
18        seeds = ["mint".as_bytes()]
19        bump,
20        mut
21    )]
22    pub mint: Account<'info, Mint>,
23    #[account(
24        init_if_needed,
25        payer = initializer,
26        associated_token::mint = mint,
27        associated_token::authority = initializer
28    )]
29    pub token_account: Account<'info, TokenAccount>,
30    pub associated_token_program: Program<'info, AssociatedToken>,
31    pub rent: Sysvar<'info, Rent>
32}

Again, some of the above constraints may be unfamiliar to you. The associated_token::mint and associated_token::authority constraints along with the init_if_needed constraint ensures that if the account has not already been initialized, it will be initialized as an associated token account for the specified mint and authority.

Next, let’s update the add_movie_review instruction to do the following:

  • Check that rating is valid. If it is not a valid rating, return the InvalidRating error.
  • Make a CPI to the token program’s mint_to instruction using the mint authority PDA as a signer. Note that we'll mint 10 tokens to the user but need to adjust for the mint decimals by making it 10*10^6.

Fortunately, we can use the anchor_spl crate to access helper functions and types like mint_to and MintTo for constructing our CPI to the Token Program. mint_to takes a CpiContext and integer as arguments, where the integer represents the number of tokens to mint. MintTo can be used for the list of accounts that the mint instruction needs.

1pub fn add_movie_review(ctx: Context<AddMovieReview>, title: String, description: String, rating: u8) -> Result<()> {
2    msg!("Movie review account created");
3    msg!("Title: {}", title);
4    msg!("Description: {}", description);
5    msg!("Rating: {}", rating);
6
7    require!(rating >= 1 && rating <= 5, MovieReviewError::InvalidRating);
8
9    let movie_review = &mut ctx.accounts.movie_review;
10    movie_review.reviewer = ctx.accounts.initializer.key();
11    movie_review.title = title;
12    movie_review.description = description;
13    movie_review.rating = rating;
14
15    mint_to(
16        CpiContext::new_with_signer(
17            ctx.accounts.token_program.to_account_info(),
18            MintTo {
19                authority: ctx.accounts.mint.to_account_info(),
20                to: ctx.accounts.token_account.to_account_info(),
21                mint: ctx.accounts.mint.to_account_info()
22            },
23            &[&[
24                "mint".as_bytes(),
25                &[*ctx.bumps.get("mint").unwrap()]
26            ]]
27        ),
28        10*10^6
29    )?;
30
31    msg!("Minted tokens");
32
33    Ok(())
34}

6. Update update_movie_review instruction

Here we are only adding the check that rating is valid.

1pub fn update_movie_review(ctx: Context<UpdateMovieReview>, title: String, description: String, rating: u8) -> Result<()> {
2    msg!("Movie review account space reallocated");
3    msg!("Title: {}", title);
4    msg!("Description: {}", description);
5    msg!("Rating: {}", rating);
6
7    require!(rating >= 1 && rating <= 5, MovieReviewError::InvalidRating);
8
9    let movie_review = &mut ctx.accounts.movie_review;
10    movie_review.description = description;
11    movie_review.rating = rating;
12
13    Ok(())
14}

7. Test

Those are all of the changes we need to make to the program! Now, let’s update our tests.

Start by making sure your imports nad describe function look like this:

1import * as anchor from "@project-serum/anchor"
2import { Program } from "@project-serum/anchor"
3import { expect } from "chai"
4import { getAssociatedTokenAddress, getAccount } from "@solana/spl-token"
5import { AnchorMovieReviewProgram } from "../target/types/anchor_movie_review_program"
6
7describe("anchor-movie-review-program", () => {
8  // Configure the client to use the local cluster.
9  const provider = anchor.AnchorProvider.env()
10  anchor.setProvider(provider)
11
12  const program = anchor.workspace
13    .AnchorMovieReviewProgram as Program<AnchorMovieReviewProgram>
14
15  const movie = {
16    title: "Just a test movie",
17    description: "Wow what a good movie it was real great",
18    rating: 5,
19  }
20
21  const [movie_pda] = anchor.web3.PublicKey.findProgramAddressSync(
22    [Buffer.from(movie.title), provider.wallet.publicKey.toBuffer()],
23    program.programId
24  )
25
26  const [mint] = anchor.web3.PublicKey.findProgramAddressSync(
27    [Buffer.from("mint")],
28    program.programId
29  )
30...
31}

With that done, add a test for the initializeTokenMint instruction:

1it("Initializes the reward token", async () => {
2    const tx = await program.methods.initializeTokenMint().rpc()
3})

Notice that we didn't have to add .accounts because they call be inferred, including the mint account (assuming you have seed inference enabled).

Next, update the test for the addMovieReview instruction. The primary additions are:

  1. To get the associated token address that needs to be passed into the instruction as an account that cannot be inferred
  2. Check at the end of the test that the associated token account has 10 tokens
1it("Movie review is added`", async () => {
2  const tokenAccount = await getAssociatedTokenAddress(
3    mint,
4    provider.wallet.publicKey
5  )
6  
7  const tx = await program.methods
8    .addMovieReview(movie.title, movie.description, movie.rating)
9    .accounts({
10      tokenAccount: tokenAccount,
11    })
12    .rpc()
13  
14  const account = await program.account.movieAccountState.fetch(movie_pda)
15  expect(movie.title === account.title)
16  expect(movie.rating === account.rating)
17  expect(movie.description === account.description)
18  expect(account.reviewer === provider.wallet.publicKey)
19
20  const userAta = await getAccount(provider.connection, tokenAccount)
21  expect(Number(userAta.amount)).to.equal((10 * 10) ^ 6)
22})

After that, neither the test for updateMovieReview nor the test for deleteMovieReview need any changes.

At this point, run anchor test and you should see the following output

1anchor-movie-review-program
2    ✔ Initializes the reward token (458ms)
3    ✔ Movie review is added (410ms)
4    ✔ Movie review is updated (402ms)
5    ✔ Deletes a movie review (405ms)
6
7  5 passing (2s)

If you need more time with the concepts from this lesson or got stuck along the way, feel free to take a look at the solution code. Note that the solution to this demo is on the solution-add-tokens branch.

Challenge

To apply what you've learned about CPIs in this lesson, think about how you could incorporate them into the Student Intro program. You could do something similar to what we did in the demo here and add some functionality to mint tokens to users when they introduce themselves.

Try to do this independently if you can! But if you get stuck, feel free to reference this solution code. Note that your code may look slightly different than the solution code depending on your implementation.

Table of Contents