Skip to main content
Version: Canary 🚧

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.

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
$ build
Building...
Build successful. Completed in 3.50s.

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,
}
- #[account]
- pub struct MessageAccount {}

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

The #[account] macro in an Anchor program is used to annotate structs that represent account data (data type to store in the AccountInfo's data field).

In this example, we're defining a MessageAccount struct to store a message created by users that contains three fields:

  • user - A Pubkey representing the user who created the message account.
  • message - A String containing the user's message.
  • bump - A u8 storing the "bump" seed used in deriving the program derived address (PDA). Storing this value saves compute by eliminating the need to rederive it for each use in subsequent instructions. When an account is created, the MessageAccount data will be serialized and stored in the new account's data field.

Later, when reading from the account, this data can be deserialized back into the MessageAccount data type. The process of creating and reading the account data will be demonstrated in the testing section.

Build the program again by running build in the 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:

#[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>,
}
- #[derive(Accounts)]
- pub struct Create {}

+ #[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>,
+ }

The #[derive(Accounts)] macro in an Anchor program is used to annotate structs that represent a list of accounts required by an instruction where each field in the struct is an account.

Each account (field) in the struct is annotated with an account type (ex. Signer<'info>) and can be further annotated with constraints (ex. #[account(mut)]). The account type along with account constraints are used to perform security checks on the accounts passed to the instruction.

The naming of each field is only for our understanding and has no effect on account validation, however, it is recommended to use descriptive account names.


The Create struct defines the accounts required for the create instruction.

  1. user: Signer<'info>

    • Represents the user creating the message account
    • Marked as mutable (#[account(mut)]) as it pays for the new account
    • Must be a signer to approve the transaction, as lamports are deducted from the account
  2. message_account: Account<'info, MessageAccount>

    • The new account created to store the user's message
    • init constraint indicates the account will be created in the instruction
    • seeds and bump constraints indicate the address of the account is a Program Derived Address (PDA)
    • payer = user specifies the account paying for the creation of the new account
    • space specifies the number of bytes allocated to the new account's data field
  3. system_program: Program<'info, System>

    • Required for creating new accounts
    • Under the hood, the init constraint invokes the System Program to create a new account allocated with the specified space and reassigns the program owner to the current program.

The #[instruction(message: String)] annotation enables the Create struct to access the message parameter from the create instruction.


The seeds and bump constraints are used together to specify that an account's address is a Program Derived Address (PDA).

seeds = [b"message", user.key().as_ref()],
bump,

The seeds constraint defines the optional inputs used to derive the PDA.

  • b"message" - A hardcoded string as the first seed.
  • user.key().as_ref() - The public key of the user account as the second seed.

The bump constraint tells Anchor to automatically find and use the correct bump seed. Anchor will use the seeds and bump to derive the PDA.


The space calculation (8 + 32 + 4 + message.len() + 1) allocates space for MessageAccount data type:

  • Anchor Account discriminator (identifier): 8 bytes
  • User Address (Pubkey): 32 bytes
  • User Message (String): 4 bytes for length + variable message length
  • PDA Bump seed (u8): 1 byte
#[account]
pub struct MessageAccount {
pub user: Pubkey,
pub message: String,
pub bump: u8,
}

All accounts created through an Anchor program require 8 bytes for an account discriminator, which is an identifier for the account type that is automatically generated when the account is created.

A String type requires 4 bytes to store the length of the string, and the remaining length is the actual data.

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(())
}
- pub fn create(_ctx: Context<Create>) -> Result<()> {
- Ok(())
- }

+ 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(())
+ }

The create function implements the logic for initializing a new message account's data. It takes two parameters:

  1. ctx: Context<Create> - Provides access to the accounts specified in the Create struct.
  2. message: String - The user's message to be stored.

The body of the function then performs the following logic:

  1. Print a message to program logs using the msg!() macro.

    msg!("Create Message: {}", message);
  2. Initializing Account Data:

    • Accesses the message_account from the context.
    let account_data = &mut ctx.accounts.message_account;
    • Sets the user field to the public key of the user account.
    account_data.user = ctx.accounts.user.key();
    • Sets the message field to the message from the function argument.
    account_data.message = message;
    • Sets the bump value used to derive the PDA, retrieved from ctx.bumps.message_account.
    account_data.bump = ctx.bumps.message_account;

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>,
}
- #[derive(Accounts)]
- pub struct Update {}

+ #[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>,
+ }

The Update struct defines the accounts required for the update instruction.

  1. user: Signer<'info>

    • Represents the user updating the message account
    • Marked as mutable (#[account(mut)]) as it may pay for additional space for the message_account if needed
    • Must be a signer to approve the transaction
  2. message_account: Account<'info, MessageAccount>

    • The existing account storing the user's message that will be updated
    • mut constraint indicates this account's data will be modified
    • realloc constraint allows for resizing the account's data
    • seeds and bump constraints ensure the account is the correct PDA
  3. system_program: Program<'info, System>

    • Required for potential reallocation of account space
    • The realloc constraint invokes the System Program to adjust the account's data size

Note that the bump = message_account.bump constraint uses the bump seed stored on the mesesage_account, rather than having Anchor recalculate it.


#[instruction(message: String)] annotation enables the Update struct to access the message parameter from the update instruction.

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(())
}
- pub fn update(_ctx: Context<Update>) -> Result<()> {
- Ok(())
- }

+ 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(())
+ }

The update function implements the logic for modifying an existing message account. It takes two parameters:

  1. ctx: Context<Update> - Provides access to the accounts specified in the Update struct.
  2. message: String - The new message to replace the existing one.

The body of the function then:

  1. Print a message to program logs using the msg!() macro.

  2. Updates Account Data:

    • Accesses the message_account from the context.
    • Sets the message field to the new message from the function argument.

Rebuld 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>,
}
- #[derive(Accounts)]
- pub struct Delete {}

+ #[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>,
+ }

The Delete struct defines the accounts required for the delete instruction:

  1. user: Signer<'info>

    • Represents the user closing the message account
    • Marked as mutable (#[account(mut)]) as it will receive the lamports from the closed account
    • Must be a signer to ensure only the correct user can close their message account
  2. message_account: Account<'info, MessageAccount>

    • The account being closed
    • mut constraint indicates this account will be modified
    • seeds and bump constraints ensure the account is the correct PDA
    • close = user constraint specifies that this account will be closed and its lamports transferred to the user account

Next, implement the logic for the update instruction.

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

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

The delete function takes one parameter:

  1. _ctx: Context<Delete> - Provides access to the accounts specified in the Delete struct. The _ctx syntax indicates we won't be using the Context in the body of the function.

The body of the function only prints a message to program logs using the msg!() macro. The function does not require any additional logic because the actual closing of the account is handled by the close constraint in the Delete struct.

Rebuild the program.

build

Deploy Program

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

deploy
$ deploy
Deploying... This could take a while depending on the program size and network conditions.
Deployment successful. Completed in 17s.

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,
);
  import { PublicKey } from "@solana/web3.js";

describe("pda", () => {
+ const program = pg.program;
+ const wallet = pg.wallet;
+
+ const [messagePda, messageBump] = PublicKey.findProgramAddressSync(
+ [Buffer.from("message"), wallet.publicKey.toBuffer()],
+ program.programId
+ );

it("Create Message Account", async () => {});

it("Update Message Account", async () => {});

it("Delete Message Account", async () => {});
});

In this section, we are simply setting up the test file.

Solana Playground removes some boilerplate setup where pg.program allows us to access the client library for interacting with the program, while pg.wallet is your playground wallet.

const program = pg.program;
const wallet = pg.wallet;

As part of the setup, we derive the message account PDA. This demonstrates how to derive the PDA in Javascript using the seeds specified in the program.

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
$ test
Running tests...
anchor.test.ts:
pda
✔ Create Message Account
✔ Update Message Account
✔ Delete Message Account
3 passing (4ms)

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`,
);
});
- it("Create Message Account", async () => {});

+ 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`
+ );
+ });

First, we send a transaction that invokes the create instruction, passing in "Hello, World!" as the message.

const message = "Hello, World!";
const transactionSignature = await program.methods
.create(message)
.accounts({
messageAccount: messagePda,
})
.rpc({ commitment: "confirmed" });

Once the transaction is sent and the account is created, we then fetch the account using its address (messagePda).

const messageAccount = await program.account.messageAccount.fetch(
messagePda,
"confirmed",
);

Lastly, we log the account data and a link to view the transaction details.

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`,
);
});
- it("Update Message Account", async () => {});

+ 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`
+ );
+ });

First, we send a transaction that invokes the update instruction, passing in "Hello, Solana!" as the new message.

const message = "Hello, Solana!";
const transactionSignature = await program.methods
.update(message)
.accounts({
messageAccount: messagePda,
})
.rpc({ commitment: "confirmed" });

Once the transaction is sent and the account is updated, we then fetch the account using its address (messagePda).

const messageAccount = await program.account.messageAccount.fetch(
messagePda,
"confirmed",
);

Lastly, we log the account data and a link to view the transaction details.

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`,
);
});
- it("Delete Message Account", async () => {});

+ 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`
+ );
+ });

First, we send a transaction that invokes the delete instruction to close the message account.

const transactionSignature = await program.methods
.delete()
.accounts({
messageAccount: messagePda,
})
.rpc({ commitment: "confirmed" });

Once the transaction is sent and the account is closed, we attempt to fetch the account using its address (messagePda) using fetchNullable since we expect the return value to be null because the account is closed.

const messageAccount = await program.account.messageAccount.fetchNullable(
messagePda,
"confirmed",
);

Lastly, we log the account data and a link to view the transaction details where the account data should be logged as null.

console.log(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
$ test
Running tests...
anchor.test.ts:
pda
{
"user": "3z9vL1zjN6qyAFHhHQdWYRTFAcy69pJydkZmSFBKHg1R",
"message": "Hello, World!",
"bump": 254
}
Transaction Signature: https://solana.fm/tx/5oBT4jEdUR6CRYsFNGoqvyMBTRDvFqRWTAAmCGM9rEvYRBWy3B2bkb6GVFpVPKBnkr714UCFUurBSDKSa7nLHo8e?cluster=devnet-solana
✔ Create Message Account (1025ms)
{
"user": "3z9vL1zjN6qyAFHhHQdWYRTFAcy69pJydkZmSFBKHg1R",
"message": "Hello, Solana!",
"bump": 254
}
Transaction Signature: https://solana.fm/tx/42veGAsQjHbJP1SxWBGcfYF7EdRN9X7bACNv23NSZNe4U7w2dmaYgSv8UUWXYzwgJPoNHejhtWdKZModHiMaTWYK?cluster=devnet-solana
✔ Update Message Account (713ms)
Expect Null: null
Transaction Signature: https://solana.fm/tx/Sseog2i2X7uDEn2DyDMMJKVHeZEzmuhnqUwicwGhnGhstZo8URNwUZgED8o6HANiojJkfQbhXVbGNLdhsFtWrd6?cluster=devnet-solana
✔ Delete Message Account (812ms)
3 passing (3s)