Program Derived Address

In this section, you'll learn how to build a basic Create, Read, Update, Delete (CRUD) program.

This guide demonstrates a simple program where users can create, update, and delete a message. Each message exists in an account with a deterministic address derived from the program itself (Program Derived Address or PDA).

This guide walks you through building and testing a Solana program using the Anchor framework while demonstrating Program Derived Addresses (PDAs). For more details, refer to the Program Derived Addresses page.

For reference, you can view the final code after completing both the PDA and Cross-Program Invocation (CPI) sections.

Starter Code

Start by opening this Solana Playground link with the starter code. Then click the "Import" button to add the program to your Solana Playground projects.

ImportImport

In the lib.rs file, you'll find a program with the create, update, and delete instructions to add 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 beginning, run build in the Playground terminal to check the starter program builds successfully.

Terminal
$
build

Define Message Account Type

First, define the structure for the message account that the program creates. This structure defines the data to 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

This code defines what data to store on the message account. Next, you'll add the program instructions.

Add Create Instruction

Now, add the create instruction that creates and initializes 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, add 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

Add Update Instruction

Next, add the update instruction to change the MessageAccount with a new message.

Like the previous step, first 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, add 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

Add Delete Instruction

Next, add 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, add 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

You've now completed the basic CRUD program. Deploy the program by running deploy in the Playground terminal.

Ensure your Playground wallet has devnet SOL. Get devnet SOL from the Solana Faucet.

Terminal
$
deploy

Set Up Test File

The starter code also includes 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 that it runs as expected. The next steps add the actual tests.

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

After preparing your tests, run the test file with test in the Playground terminal.

Terminal
$
test

Inspect the SolanaFM links to view the transaction details.

Is this page helpful?

Sisällysluettelo

Muokkaa sivua