I've compiled a list of Solana program snippets that I thought could be useful for any project. These snippets assume you're using anchor.
- Resources
- Basic Program
- Initializing An Account
- Updating An Account
- Transferring SOL
- Transferring SOL from a PDA
- Transferring SPL Tokens
- Transferring SPL Tokens from a PDA
- Checking Metaplex Metadata
remaining_accounts
msg!
- Errors
- Finding Help
Resources
Learn about the basics of how Solana works:
Here are some useful repos I referenced while learning:
- https://github.com/coral-xyz/anchor
- https://github.com/coral-xyz/anchor-by-example
- https://github.com/solana-labs/solana-program-library
- https://github.com/metaplex-foundation/metaplex-program-library
- https://github.com/metaplex-foundation/js
- https://github.com/jet-lab/jet-v2
Some other useful resources I found snippets from:
- https://spl.solana.com/
- https://examples.anchor-lang.com/
- https://solanacookbook.com/
- https://hackmd.io/@ironaddicteddog/solana-anchor-escrow
Dependencies
These are the dependencies I used when writing these snippets (these go in your Cargo.toml
inside your program directory):
[dependencies]
anchor-lang = "0.25.0"
anchor-spl = "0.25.0"
mpl-token-metadata = {version="1.3.6", features = ["no-entrypoint"]}
solana-program = "~1.10.29"
spl-token = "~3.3.0"
Use the
"no-entrypoint"
feature withmpl-token-metadata
to prevent "global allocator errors"
Basic Program
You can write a program by just creating a context and a function that uses the context. The context defines what accounts are expected as inputs to your function, and a function is an instruction (the things you put in a transaction) that your program can process. A minimal example:
#[program]
pub mod my_program {
use super::*;
// Instruction
pub fn do_something<'info>(ctx: Context<DoSomething>) -> Result<()> {
msg!("Doing something to {}", &ctx.accounts.account_to_do_something_with.key());
Ok(())
}
}
// Context
#[derive(Accounts)]
pub struct DoSomething<'info> {
#[account(mut)]
pub payer: Signer<'info>,
/// CHECK: Data held in account could be anything
pub account_to_do_something_with: AccountInfo<'info>
}
State
If your program contains state you define structs modeling your state and Anchor can automatically initialize and deserialize them in your instructions. By using the Account<T>
type in your context, you're telling Anchor that the data held in the given account matches the type used. If it doesn't match (eg. using a Metadata account when expecting YourAccount), the program will fail to process the instruction. More on accounts here.
Initializing An Account
In the example below we make use of Anchor's #[account]
attribute with the init
constraint to automatically initialize the given account. The space required for an account is 8 bytes for Anchor's account discriminator, then add the size of each field in the account struct. This value goes in the space
constraint. payer
is (surprise!) the wallet paying for the new account to be created (cost is determined by the amount of space needed). seeds
are used to distinguish between PDAs.
In this example only the
payer
is used in theseeds
, but IRL you'd want some sort of post ID too, so a user can have multiple posts.
#[program]
pub mod my_program {
use super::*;
pub fn create_post<'info>(ctx: Context<CreatePost>) -> Result<()> {
ctx.accounts.post.owner = ctx.accounts.payer.key();
Ok(())
}
}
#[derive(Accounts)]
pub struct CreatePost<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
seeds = [payer.key().as_ref(), b"post".as_ref()],
bump,
space = POST_SIZE,
payer = payer,
)]
pub post: Account<'info, Post>,
// Required to init the account
pub system_program: Program<'info, System>,
}
pub const POST_SIZE: usize = 8 + 32 + 4 + 4;
// State
#[account]
pub struct Post {
pub owner: Pubkey;
pub likes: u32,
pub replies: u32,
}
The
#[instruction]
attribute is another useful attribute that allows you to use instruction args in your constraints.
Updating An Account
When updating an account, you'll have to use the mut
constraint to mark the account as mutable. If you forget it, you'll see a build error complaining about mutability. The LikePost
instruction below just increments the number of likes
on the account, then computes the ratio
.
#[program]
pub mod my_program {
use super::*;
pub fn like_post<'info>(ctx: Context<LikePost>) -> Result<()> {
ctx.accounts.post.likes = ctx.accounts.post.likes + 1;
if ctx.accounts.post.replies > 0 {
ctx.accounts.post.ratio = ctx.accounts.post.likes.checked_div(ctx.accounts.post.replies)
.ok_or(Error::InvalidRatio)?;
}
Ok(())
}
}
#[derive(Accounts)]
pub struct LikePost<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(mut)]
pub post: Account<'info, Post>,
}
There are loads of useful constraints you can use to harden your code. Check out all the contraints here.
Transferring SOL
Transfer SOL from one account to another. lamports
is the SOL amount in lamports. account_a
and account_b
must be marked with mut
in your context. The System program will also need to be included in your context:
// In your instruction function
solana_program::program::invoke(
&solana_program::system_instruction::transfer(
&ctx.accounts.account_a.key(),
&ctx.accounts.account_b.key(),
lamports,
),
&[
ctx.accounts.account_a.to_account_info().clone(),
ctx.accounts.account_b.to_account_info().clone(),
]
)?;
// In your context
#[account(mut)]
pub account_a: Signer<'info>,
#[account(mut)]
pub account_b: AccountInfo<'info>,
pub system_program: Program<'info, System>
Transferring SOL from a PDA
PDA auth seeds
When transferring assets from a PDA account that isn't owned by your program (eg. owned by token program) you'll have to sign the instruction using the same seeds used to derive the PDA.
// If you use the PDA constraint:
// seeds = [seed_1.key().as_ref(), b"my_account_type".as_ref()]
// Get your signer_seeds with:
let signer_seeds = &[
&ctx.accounts.seed_1.key().to_bytes()[..],
&b"my_account_type"[..],
// pda_bump can be passed in as an instruction arg or derived again using Pubkey::find_program_address
&[pda_bump]
];
Now that you have your signer_seeds
, you can call invoke_signed
to do the transfer:
// In your instruction function
solana_program::program::invoke_signed(
&solana_program::system_instruction::transfer(
&ctx.accounts.pda_account.key(),
&ctx.accounts.account_b.key(),
lamports
),
&[
ctx.accounts.pda_account.to_account_info().clone(),
ctx.accounts.account_b.to_account_info().clone(),
],
&[&signer_seeds[..]]
)?;
However, if the PDA is owned by your program (eg. created using the init
account constraint), you can transfer lamports using:
let src = &mut ctx.accounts.pda_account.to_account_info();
**src.try_borrow_mut_lamports()? = src
.lamports()
.checked_sub(lamports)
.ok_or(ProgramError::InvalidArgument)?;
let dst = &mut ctx.accounts.account_b.to_account_info();
**dst.try_borrow_mut_lamports()? = dst
.lamports()
.checked_add(lamports)
.ok_or(ProgramError::InvalidArgument)?;
Transferring SPL Tokens
Transfer an SPL token (like FOXY or USDC) from one account to another. The from
and to
accounts are associated token accounts and must be marked with mut
in your context and the authority
account must be a signer. The Token program will also need to be included in your context:
// In your instruction function
anchor_spl::token::transfer(
CpiContext::new(ctx.accounts.token_program.to_account_info(), anchor_spl::token::Transfer {
from: ctx.accounts.token_account_a.to_account_info().clone(),
to: ctx.accounts.token_account_b.to_account_info().clone(),
authority: ctx.accounts.payer.to_account_info().clone(),
}),
amount
)?;
// In your context
#[account(
mut,
// check for a certain owner if needed: constraint = token_account_a.owner == payer.key(),
// check for a certain token mint if needed: token::mint = mint,
)]
pub token_account_a: Account<'info, TokenAccount>,
#[account(mut)]
pub token_account_b: Account<'info, TokenAccount>,
pub token_program: Program<'info, Token>
Transferring SPL Tokens from a PDA
Similar to the above example, except now with the added with_signer
method call:
anchor_spl::token::transfer(
CpiContext::new(ctx.accounts.token_program.to_account_info(), anchor_spl::token::Transfer {
from: ctx.accounts.pda_account.to_account_info().clone(),
to: ctx.accounts.token_account_b.to_account_info().clone(),
authority: ctx.accounts.pda_account.to_account_info().clone(),
}).with_signer(&[&signer_seeds[..]]),
amount
)?;
Checking Metaplex Metadata
At the time of writing the Anchor team is putting together an Anchor-friendly MetadataAccount
to deserialize and check Metaplex metadata automatically, but it wasn't ready yet so I dug around and found this:
// Imports
use mpl_token_metadata::state::{Metadata, TokenMetadataAccount};
// Instruction
// Verify the metadata account given is the expected one for the given mint
pub fn verify_metadata<'info>(ctx: Context<VerifyMetadata>) -> Result<()> {
let mint_key = &ctx.accounts.mint.key();
let metadata_program_id = &mpl_token_metadata::id();
let metadata_seeds = &[
mpl_token_metadata::state::PREFIX.as_bytes(),
metadata_program_id.as_ref(),
mint_key.as_ref()
];
// Get the metadata PDA for the given mint
let (expected_metadata_account, _) = Pubkey::find_program_address(
metadata_seeds,
metadata_program_id
);
assert_eq!(expected_metadata_account, ctx.accounts.metadata_account.key(),
"Invalid metadata account");
Ok(())
}
// Context
#[derive(Accounts)]
pub struct VerifyMetadata<'info> {
pub mint: Account<'info, Mint>,
pub metadata_account: AccountInfo<'info>
}
If the given metadata_account
doesn't match what we derive from known seed inputs, it's not the metadata for the NFT and can't be trusted.
Deserializing Metaplex Metadata
Now that you know you have the expected metadata account, you can deserialize it with this:
let metadata: Metadata = Metadata::from_account_info(&ctx.accounts.metadata_account)?;
Which gives you access to check royalties, creator shares, etc for the noble lads and lassies out there looking to disburse royalties.
remaining_accounts
When your instruction can accept a variable number of accounts (say you're disbursing royalties to n number of creators), use the ctx.remaining_accounts
property. These accounts are set up as generic AccountInfo
's so you'll have to be careful when reading data from them (always check keys with PDAs if possible).
On the front-end side of things you can populate these accounts like so: program.methods.doSomething().remainingAccounts([accounts])
.
msg!
There are deeper ways of debugging programs, but I found that simply logging using the msg!
method was sufficient for me. These messages are written to transaction logs when executed. While testing you can find these logs in <working-dir>/.anchor/program-logs/program-id.program-name.log
Errors
Here are some other generic errors I've run into and solutions that may work for you:
program failed to complete
An error that occurs when executing a transaction. They say it might have something to do with running out of heap space. I boxed all of my accounts to make it go away (add Box<>
around your Account<>
's in your context).
lifetime mismatch
Build error in Rust. Something that has to do with Rust lifetimes. I added lifetime qualifiers to my instruction's ctx arg to make it go away Context<'_, '_, '_, 'info, DoSomething<'info>>
instead of Context<DoSomething>
.
accounts not balanced after tx
That's not the exact phrase, but something along those lines. I got this when closing an account in the same instruction as transferring lamports. It should be possible, but may have been an issue with the mix of how I was transferring/closing. My fix was to split the transfer and close actions into separate instructions.
invalid account data for instruction
This may appear when trying to execute a transaction using an account that hasn't been initialized yet (empty data).
Finding Help
When searching for errors, I found that searching the Anchor Discord yielded better results than Google most of the time.
Fin
I'll keep jotting down notes and publishing more snippets as I learn and grow more in this ecosystem. Till next time, good luck out there SOLdier π«‘
Let's connect on Twitter: @meditatingsloth
Top comments (1)
hi, thank you for this. My problem with continuing with Solana development is the complexities of rustlang. I hope some day I will pick it back up.