Summary
- Anchor provides a simplified way to create CPIs using a
CpiContext - Anchor’s
cpifeature 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
invokeandinvoke_signeddirectly - The
error_codeattribute macro is used to create custom Anchor Errors
Lesson
Anchor makes invoking other Nexis Native Chain programs easier, 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
CPIs allow programs to invoke instructions on other programs using theinvoke
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.
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 invokedremaining_accounts- any remaining accountsprogram- the program ID of the program being invokedsigner_seeds- if a PDA is signing, include the seeds required to derive the PDA
CpiContext::new to construct a new instance when passing along the
original transaction signature.
CpiContext::new_with_signer to construct a new instance when signing
on behalf of a PDA for the CPI.
CPI accounts
One of the main things aboutCpiContext 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’sCargo.toml file as follows:
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:
Invoke an instruction on a non-Anchor program
When the program you’re calling is not an Anchor program, there are two possible options:- 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_splcrate provides helper functions that are virtually identical from a call-site perspective to what you would get with thecpimodule of an Anchor program. E.g. you can mint using themint_tohelper function and use theMintToaccounts struct. - If there is no helper module for the program whose instruction(s) you need to
invoke, you can fall back to using
invokeandinvoke_signed. In fact, the source code of themint_tohelper function referenced above shows an example usinginvoke_signedwhen given aCpiContext. You can follow a similar pattern if you decide to use an accounts struct andCpiContextto organize and prepare your CPI.
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
- Anchor Internal Errors that the framework returns from inside its own code
- Custom errors that you the developer can create
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.
Lab
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 lab 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. We’ll be using thesolution-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.
3. Initialize reward token
Next, navigate tolib.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.
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.
To initialize the Mint account, we’ll need to include the token_program,
rent, and system_program in the list of accounts.
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 therating
passed to either the add_movie_review or update_movie_review instruction.
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 tokensmint- the mint account for the tokens that we’ll mint to users when they add a movie reviewtoken_account- the associated token account for the afforementionedmintand reviewerassociated_token_program- required because we’ll be using theassociated_tokenconstraint on thetoken_accountrent- required because we are using theinit-if-neededconstraint on thetoken_account
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
ratingis valid. If it is not a valid rating, return theInvalidRatingerror. - Make a CPI to the token program’s
mint_toinstruction 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 it10*10^6.
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.
Update your use statements to include:
add_movie_review function to:
6. Update update_movie_review instruction
Here we are only adding the check that rating is valid.
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 anddescribe function look like this:
npm install @nexis-network/spl-token --save-dev if you don’t have it
installed.
With that done, add a test for the initializeTokenMint instruction:
.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:
- To get the associated token address that needs to be passed into the instruction as an account that cannot be inferred
- Check at the end of the test that the associated token account has 10 tokens
updateMovieReview nor the test for
deleteMovieReview need any changes.
At this point, run anchor test and you should see the following output
solution-add-tokens branch.