Import
solana_sdk::signer::{keypair::Keypair, Signer}Generate a new keypair
let keypair = Keypair.generate();This is used to create a brand-new Solana keypair.
-
The
pubkey!macro takes in a base58 string and converts it into aPubkey. -
Example:
pubkey!("1" * 32)
This converts the System Program ID into a
Pubkey.
- Seeds are used to derive PDAs.
- Seeds must be in bytes, not strings.
Example:
let seeds = [b"helloWorld".as_ref()];-
The
bprefix indicates that the value is byte-encoded, not an&str(which is commonly assumed). -
PDA seeds must be of type:
&[&[u8]]
-
This is why
.as_ref()is used β to convert the byte array into a byte slice.
-
Pubkey::find_program_address:- Takes in the seeds and the program ID
- Hashes them
- Finds a PDA that does not have a private key
-
This is required because PDAs CANNOT HAVE PRIVATE KEYS.
-
Create a connection.
-
Generate a keypair to pay for the transaction.
-
Request an airdrop to fund the keypair.
-
Generate a keypair for the mint account.
- The public key will be used as the mint accountβs address.
-
Calculate the minimum lamports required for a mint account.
getMinimumBalanceForRentExemptMintcalculates how many lamports must be allocated for the mint accountβs data.
This instruction:
- Allocates the number of bytes needed to store the mint data
- Transfers lamports from the wallet to fund the new account
- Assigns ownership of the account to the Token Extensions Program
This instruction initializes the mint account with:
-
2 decimals
-
Wallet as both:
- Mint authority
- Freeze authority
Two signatures are required:
-
Wallet account
- Signs as the payer for transaction fees
- Signs for account creation
day 2:
This is a simple Solana program built using Anchor that demonstrates how to:
- Create a Program Derived Account (PDA)
- Store user-specific data on-chain
- Update dynamically sized data (
String) - Close accounts and reclaim lamports
- Interact with the program using an Anchor test (TypeScript)
This project was built as a learning exercise to understand:
- PDAs
- Anchor
Context - Account constraints
- How client code talks to on-chain Rust programs
use anchor_lang::prelude::*;
declare_id!("AKL2d5ktgDVn6p2LYrKY3DPU3ZdoMC7SJ6AaQyomfAXw");
#[program]
pub mod pda {
use super::*;
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 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 delete(_ctx: Context<Delete>) -> Result<()> {
msg!("Delete Message");
Ok(())
}
#[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)]
#[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 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>,
}
#[account]
pub struct MessageAccount {
pub user: Pubkey,
pub message: String,
pub bump: u8,
}Each wallet (user) can have one message account, stored as a PDA.
The PDA address is derived using:
["message", user_public_key]
This guarantees:
- One message per user
- Deterministic account address
- No collisions between users
-
Creates a PDA for the user
-
Stores:
- the userβs public key
- the message string
- the PDA bump
-
User pays for account creation
- Updates the existing message
- Uses
reallocto resize account if message length changes - User pays extra lamports if account grows
This is where dynamic data on Solana really shows up.
- Closes the PDA
- Refunds remaining lamports back to the user
- Frees on-chain storage
pub struct MessageAccount {
pub user: Pubkey, // Owner of the message
pub message: String, // Stored message
pub bump: u8, // PDA bump
}8 // Anchor discriminator
32 // Pubkey
4 // String length prefix
N // Message bytes
1 // bumpSolana accounts have fixed size, so this calculation must be exact.
import { PublicKey } from "@solana/web3.js";
describe("pda", () => {
const program = pg.program;
const wallet = pg.wallet;
const [pda, bump] = PublicKey.findProgramAddressSync(
[Buffer.from("message"), wallet.publicKey.toBuffer()],
program.programId
);
it("Update Message", async () => {
const message = "I will win!";
const transactionSignature = await program.methods
.update(message)
.accounts({ messageAccount: pda })
.rpc({ commitment: "confirmed" });
const messageAccount = await program.account.messageAccount.fetch(
pda,
"confirmed"
);
console.log(JSON.stringify(messageAccount, null, 2));
console.log(
"Transaction Signature:",
`https://solana.fm/tx/${transactionSignature}?cluster=devnet-solana`
);
});
});pg.walletacts as the user- PDA is derived off-chain using the same seeds as Rust
.accounts({ messageAccount: pda })tells Anchor which address to use.update(message)calls the on-chain instruction.fetch(pda)reads the on-chain account state
This test simulates a real user updating their on-chain data.
-
What a PDA is and why it exists
-
Difference between:
- instruction execution (
program.methods) - account reading (
program.account)
- instruction execution (
-
How Anchor maps:
message_account(Rust) βmessageAccount(TS)
-
Why accounts must be explicitly passed
-
Why realloc is needed for strings
-
How Solana enforces correctness via constraints
The wallet is the user, the PDA is the userβs storage, and the program is just the rulebook.
This pattern is used in real protocols for:
- profiles
- posts
- vaults
- escrows
- configs
- counters
- NFTs
- governance
Day 3