DEV Community

Cover image for Solana Program Snippets
Meditating Sloth
Meditating Sloth

Posted on • Edited on

Solana Program Snippets

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

Learn about the basics of how Solana works:

Here are some useful repos I referenced while learning:

Some other useful resources I found snippets from:

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"
Enter fullscreen mode Exit fullscreen mode

Use the "no-entrypoint" feature with mpl-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>
}
Enter fullscreen mode Exit fullscreen mode

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 the seeds, 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,
}
Enter fullscreen mode Exit fullscreen mode

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>,
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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]
];
Enter fullscreen mode Exit fullscreen mode

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[..]]
)?;
Enter fullscreen mode Exit fullscreen mode

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)?;
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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
)?;
Enter fullscreen mode Exit fullscreen mode

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>
}
Enter fullscreen mode Exit fullscreen mode

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)?;
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
ayodejii profile image
Isaac Ayodeji Ikusika

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.