Solana Anchor Program IDL
The IDL (Interface Definition Language) is a JSON file that describes how to interact with a Solana program. It is automatically generated by the Anchor framework.
There is nothing special about the function called “initialize” — it’s a name Anchor picks. What we will learn in this tutorial is how the typescript unit tests are able to “find” the appropriate function.
Let’s create a new project called anchor-function-tutorial
and change the name in the initialize function to boaty_mc_boatface
, keeping everything else the same.
pub fn boaty_mc_boatface(ctx: Context<Initialize>) -> Result<()> {
Ok(())
}
Now let’s change the test to the following
it("Call boaty mcboatface", async () => {
// Add your test here.
const tx = await program.methods.boatyMcBoatface().rpc();
console.log("Your transaction signature", tx);
});
Now run the tests with anchor test --skip-local-validator
It runs as expected. So how did this sorcery work?
How do the tests know about the initialize function?
When Anchor builds a Solana program, it creates an IDL (Interface Definition Language).
This is stored in target/idl/anchor_function_tutorial.json.
This file is called anchor_function_tutorial.json
because anchor_function_tutorial
is the name of the program. Note that anchor converted the dashes to underscores!
Let’s open it.
{
"version": "0.1.0",
"name": "anchor_function_tutorial",
"instructions": [
{
"name": "boatyMcBoatface",
"accounts": [],
"args": []
}
]
}
The list of “instructions” is the public facing functions that the program supports, roughly equivalent to the external and public functions on an Ethereum contract. An IDL file in Solana plays a similar role as the ABI file in Solidity, specifying how to interact with the program’s/contract’s.
We saw earlier that our function didn’t take any arguments, so that’s why the args
list is empty. We’ll explain later what “accounts” is.
One thing that stands out: functions in Rust are snake_cased, but Anchor formats them in JavaScript land as camelCased. This is to respect the conventions of the languages: Rust tends to use snake case, and JavaScript typically uses camel case.
This JSON file is how the “methods” object knows what functions to support.
When we run the test, we expect it to pass, which means the test is correctly calling the Solana program:
Exercise: Add an argument to the boaty_mc_boatface
function to receive a u64
. Run anchor build
again. Then open up the target/idl/anchor_function_tutorial.json
file again. How does it change?
Now let’s start to create a Solana program that has functions for basic addition and subtraction which print the result. Solana functions cannot return values the way Solidity does, so we will have to print them. (Solana has alternative ways of passing values we will discuss later). Let’s create two functions like so:
pub fn add(ctx: Context<Initialize>, a: u64, b: u64) -> Result<()> {
let sum = a + b;
msg!("Sum is {}", sum);
Ok(())
}
pub fn sub(ctx: Context<Initialize>, a: u64, b: u64) -> Result<()> {
let difference = a - b;
msg!("Difference is {}", difference);
Ok(())
}
And change our unit tests to the following:
it("Should add", async () => {
const tx = await program.methods.add(new anchor.BN(1), new anchor.BN(2)).rpc();
console.log("Your transaction signature", tx);
});
it("Should sub", async () => {
const tx = await program.methods.sub( new anchor.BN(10), new anchor.BN(3)).rpc();
console.log("Your transaction signature", tx);
});
Exercise: Implement similar functions for mul
, div
, and modulo
, and write a unit test to trigger each one.
What about the Initialize
struct?
Now there is another sneaky thing going on here. We’ve left the Initialize
struct untouched and are reusing it between functions. Again, the name does not matter. Let’s change the struct name to Empty
and re-run the test.
//...
// Change struct name here
pub fn add(ctx: Context<Empty>, a: u64, b: u64) -> Result<()> {
let sum = a + b;
msg!("Sum is {}", sum);
Ok(())
}
//...
// Change struct name here too
#[derive(Accounts)]
pub struct Empty {}
Again, the name Empty
is totally arbitrary here.
Exercise: Change the struct name Empty
to BoatyMcBoatface
and re-run the tests.
What is #[derive(Accounts)]
struct?
This #
syntax is a Rust attribute defined by the Anchor framework. We will explain this further in a later tutorial. For now, we want to pay attention to the accounts key in the IDL and how it relates to the struct defined in the program.
Accounts IDL key
Below we screenshot the IDL of our program above. So we can see the relationship between the “Accounts” in that Rust Attribute #[derive(Accounts)]
and the “accounts” key in the IDL:
In our example, the accounts
key in the JSON IDL above marked by the purple arrow is empty. But that is not the case for most useful Solana transactions as we will learn later.
Because our account struct for BoatyMcBoatface
is empty, the accounts list in the IDL is empty also.
Now let’s see what happens when the struct is non-empty. Copy the code below and replace the contents of lib.rs
.
use anchor_lang::prelude::*;
declare_id!("8PSAL9t1RMb7BcewhsSFrRQDq61Y7YXC5kHUxMk5b39Z");
#[program]
pub mod anchor_function_tutorial {
use super::*;
pub fn non_empty_account_example(ctx: Context<NonEmptyAccountExample>) -> Result<()> {
Ok(())
}
}
#[derive(Accounts)]
pub struct NonEmptyAccountExample<'info> {
signer: Signer<'info>,
another_signer: Signer<'info>,
}
Now run anchor build
– let’s see what we get back in the new IDL.
{
"version": "0.1.0",
"name": "anchor_function_tutorial",
"instructions": [
{
"name": "nonEmptyAccountExample",
"accounts": [
{
"name": "signer",
"isMut": false,
"isSigner": true
},
{
"name": "anotherSigner",
"isMut": false,
"isSigner": true
}
],
"args": []
}
],
"metadata": {
"address": "8PSAL9t1RMb7BcewhsSFrRQDq61Y7YXC5kHUxMk5b39Z"
}
}
Note that “accounts
” is no longer empty and is populated with the fields from the struct: “signer
” and “anotherSigner
” (note that another_signer
got transformed from snake case into camel case). The IDL has been updated to match the struct we just changed, specifically with the number of accounts we added.
We’ll dive further into the “Signer” in an upcoming tutorial, but for now you can think of it as analogous to tx.origin
in Ethereum.
A second example of a program and an IDL.
To summarize everything we’ve learned so far, let’s build another program with different functions and Account structs.
use anchor_lang::prelude::*;
declare_id!("8PSAL9t1RMb7BcewhsSFrRQDq61Y7YXC5kHUxMk5b39Z");
#[program]
pub mod anchor_function_tutorial {
use super::*;
pub fn function_a(ctx: Context<NonEmptyAccountExample>) -> Result<()> {
Ok(())
}
pub fn function_b(ctx: Context<Empty>, firstArg: u64) -> Result<()> {
Ok(())
}
}
#[derive(Accounts)]
pub struct NonEmptyAccountExample<'info> {
signer: Signer<'info>,
another_signer: Signer<'info>,
}
#[derive(Accounts)]
pub struct Empty {}
Now build it with anchor build
Let’s look again at the IDL file target/idl/anchor_function_tutorial.json
and place these files side by side:
Can you see the relationship between the IDL file and the program above?
The function function_a
has no arguments and this shows in the IDL as an empty array under the args
key.
Its Context
takes the NonEmptyAccountExample
struct. This struct NonEmptyAccountExample
has two signer fields: signer
and another_signer
. Note that these are repeated as elements in the account key in the IDL for function_a
. You can see that Anchor translated Rust’s snake case to camel case in the IDL.
Update for Anchor 0.30 Anchor no longer automatically performs this translation (release notes).
The function function_b
takes an u64 argument. Its context struct is empty, so the accounts
key in IDL for function_b
is an empty array.
In general, we expect the array of items in the IDL’s accounts
key to match the keys of the account struct that the function takes in its ctx
argument.
Summary
In this chapter:
- We learned Solana uses an IDL (iInterface Definition Language) to show how to interact with a Solana program and what fields appear in the IDL.
- We introduced the struct modified by
#[derive(Accounts)]
and how it relates to function arguments. - Anchor interprets snake_case functions in Rust as camelCase functions in the Typescript tests.
Originally Published February, 10, 2024