Program Derived Address
In this section, we'll walk through how to build a basic CRUD (Create, Read, Update, Delete) program.
We'll create a simple program where users can create, update, and delete a message. Each message will be stored in an account with a deterministic address derived from the program itself (Program Derived Address or PDA).
The purpose of this section is to guide you through building and testing a Solana program using the Anchor framework while demonstrating how to use Program Derived Addresses (PDAs). For more details, refer to the Program Derived Addresses page.
For reference, here is the final code after completing both the PDA and CPI sections.
Starter Code
Begin by opening this Solana Playground link with the starter code. Then click the "Import" button, which will add the program to your list of projects on Solana Playground.
Import
In the lib.rs
file, you'll find a program scaffolded with the create
,
update
, and delete
instructions we'll implement in the following steps.
use anchor_lang::prelude::*;declare_id!("8KPzbM2Cwn4Yjak7QYAEH9wyoQh86NcBicaLuzPaejdw");#[program]pub mod pda {use super::*;pub fn create(_ctx: Context<Create>) -> Result<()> {Ok(())}pub fn update(_ctx: Context<Update>) -> Result<()> {Ok(())}pub fn delete(_ctx: Context<Delete>) -> Result<()> {Ok(())}}#[derive(Accounts)]pub struct Create {}#[derive(Accounts)]pub struct Update {}#[derive(Accounts)]pub struct Delete {}#[account]pub struct MessageAccount {}
Before we begin, run build
in the Playground terminal to check the starter
program builds successfully.
build
Define Message Account Type
First, let's define the structure for the message account that our program will create. This is the data that we'll store in the account created by the program.
In lib.rs
, update the MessageAccount
struct with the following:
#[account]pub struct MessageAccount {pub user: Pubkey,pub message: String,pub bump: u8,}
Build the program again by running build
in the terminal.
build
We've just defined what data to store on the message account. Next, we'll implement the program instructions.
Implement Create Instruction
Now, let's implement the create
instruction to create and initialize the
MessageAccount
.
Start by defining the accounts required for the instruction by updating the
Create
struct with the following:
#[derive(Accounts)]#[instruction(message: String)]pub struct Create<'info> {#[account(mut)]pub user: Signer<'info>,#[account(init,seeds = [b"message", user.key().as_ref()],bump,payer = user,space = 8 + 32 + 4 + message.len() + 1)]pub message_account: Account<'info, MessageAccount>,pub system_program: Program<'info, System>,}
Next, implement the business logic for the create
instruction by updating the
create
function with the following:
pub fn create(ctx: Context<Create>, message: String) -> Result<()> {msg!("Create Message: {}", message);let account_data = &mut ctx.accounts.message_account;account_data.user = ctx.accounts.user.key();account_data.message = message;account_data.bump = ctx.bumps.message_account;Ok(())}
Rebuild the program.
build
Implement Update Instruction
Next, implement the update
instruction to update the MessageAccount
with a
new message.
Just as before, the first step is to specify the accounts required by the
update
instruction.
Update the Update
struct with the following:
#[derive(Accounts)]#[instruction(message: String)]pub struct Update<'info> {#[account(mut)]pub user: Signer<'info>,#[account(mut,seeds = [b"message", user.key().as_ref()],bump = message_account.bump,realloc = 8 + 32 + 4 + message.len() + 1,realloc::payer = user,realloc::zero = true,)]pub message_account: Account<'info, MessageAccount>,pub system_program: Program<'info, System>,}
Next, implement the logic for the update
instruction.
pub fn update(ctx: Context<Update>, message: String) -> Result<()> {msg!("Update Message: {}", message);let account_data = &mut ctx.accounts.message_account;account_data.message = message;Ok(())}
Rebuild the program
build
Implement Delete Instruction
Next, implement the delete
instruction to close the MessageAccount
.
Update the Delete
struct with the following:
#[derive(Accounts)]pub struct Delete<'info> {#[account(mut)]pub user: Signer<'info>,#[account(mut,seeds = [b"message", user.key().as_ref()],bump = message_account.bump,close= user,)]pub message_account: Account<'info, MessageAccount>,}
Next, implement the logic for the delete
instruction.
pub fn delete(_ctx: Context<Delete>) -> Result<()> {msg!("Delete Message");Ok(())}
Rebuild the program.
build
Deploy Program
The basic CRUD program is now complete. Deploy the program by running deploy
in the Playground terminal.
Ensure your Playground wallet has devnet SOL. Get devnet SOL from the Solana Faucet.
deploy
Set Up Test File
Included with the starter code is also a test file in anchor.test.ts
.
import { PublicKey } from "@solana/web3.js";describe("pda", () => {it("Create Message Account", async () => {});it("Update Message Account", async () => {});it("Delete Message Account", async () => {});});
Add the code below inside describe
, but before the it
sections.
const program = pg.program;const wallet = pg.wallet;const [messagePda, messageBump] = PublicKey.findProgramAddressSync([Buffer.from("message"), wallet.publicKey.toBuffer()],program.programId,);
Run the test file by running test
in the Playground terminal to check the file
runs as expected. We will implement the tests in the following steps.
test
Invoke Create Instruction
Update the first test with the following:
it("Create Message Account", async () => {const message = "Hello, World!";const transactionSignature = await program.methods.create(message).accounts({messageAccount: messagePda,}).rpc({ commitment: "confirmed" });const messageAccount = await program.account.messageAccount.fetch(messagePda,"confirmed",);console.log(JSON.stringify(messageAccount, null, 2));console.log("Transaction Signature:",`https://solana.fm/tx/${transactionSignature}?cluster=devnet-solana`,);});
Invoke Update Instruction
Update the second test with the following:
it("Update Message Account", async () => {const message = "Hello, Solana!";const transactionSignature = await program.methods.update(message).accounts({messageAccount: messagePda,}).rpc({ commitment: "confirmed" });const messageAccount = await program.account.messageAccount.fetch(messagePda,"confirmed",);console.log(JSON.stringify(messageAccount, null, 2));console.log("Transaction Signature:",`https://solana.fm/tx/${transactionSignature}?cluster=devnet-solana`,);});
Invoke Delete Instruction
Update the third test with the following:
it("Delete Message Account", async () => {const transactionSignature = await program.methods.delete().accounts({messageAccount: messagePda,}).rpc({ commitment: "confirmed" });const messageAccount = await program.account.messageAccount.fetchNullable(messagePda,"confirmed",);console.log("Expect Null:", JSON.stringify(messageAccount, null, 2));console.log("Transaction Signature:",`https://solana.fm/tx/${transactionSignature}?cluster=devnet-solana`,);});
Run Test
Once the tests are set up, run the test file by running test
in the Playground
terminal.
test
You can then inspect the SolanaFM links to view the transaction details.