Program Derived Address

In this section, we'll walk through how to build a basic CRUD (Create, Read, Update, Delete) program. The program will store a user's message using a Program Derived Address (PDA) as the account's address.

The purpose of this section is to guide you through the steps for building and testing a Solana program using the Anchor framework and demonstrating how to use PDAs within a program. For more details, refer to the Programs Derived Address 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.

ImportImport

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.

lib.rs
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.

Terminal
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:

lib.rs
#[account]
pub struct MessageAccount {
    pub user: Pubkey,
    pub message: String,
    pub bump: u8,
}

Build the program again by running build in the terminal.

Terminal
build

We've defined what our message account will look like. 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:

lib.rs
#[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:

lib.rs
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.

Terminal
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:

lib.rs
#[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.

lib.rs
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

Terminal
build

Implement Delete Instruction #

Next, implement the delete instruction to close the MessageAccount.

Update the Delete struct with the following:

lib.rs
#[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.

lib.rs
pub fn delete(_ctx: Context<Delete>) -> Result<()> {
    msg!("Delete Message");
    Ok(())
}

Rebuild the program.

Terminal
build

Deploy Program #

The basic CRUD program is now complete. Deploy the program by running deploy in the Playground terminal.

Terminal
deploy

Set Up Test File #

Included with the starter code is also a test file in anchor.test.ts.

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.

anchor.test.ts
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.

Terminal
test

Invoke Create Instruction #

Update the first test with the following:

anchor.test.ts
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:

anchor.test.ts
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:

anchor.test.ts
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.

Terminal
test