Account Structure
Solana's account model is fundamentally different from Ethereum's. Understanding accounts is crucial for building on Solana—they're the foundation of all state storage.
The Account Mental Model
Think of Solana accounts like files in a filesystem:
Text
FILESYSTEM ANALOGY
══════════════════
Linux Filesystem Solana
─────────────────────────────────────────
/home/user/file.txt → Account (Public Key)
File contents → data field
File permissions → owner field
File size → data.length
Modified time → (implicit in slot)
Account Anatomy
Every account has the same structure:
TypeScript
interface Account {
// Identity
pubkey: PublicKey; // 32-byte address (not stored, derived)
// Economics
lamports: bigint; // Balance in lamports (1 SOL = 1B lamports)
// Data
data: Uint8Array; // Arbitrary bytes, program interprets
// Ownership
owner: PublicKey; // Program that owns this account
// Flags
executable: boolean; // Is this account a program?
rentEpoch: bigint; // Epoch for next rent collection
}
Visual Representation
Text
┌─────────────────────────────────────────────────────────────────┐
│ SOLANA ACCOUNT │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ pubkey (address): 7xKXt...abc123 │ │
│ │ (Not stored in account, computed from keypair) │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────┬──────────────────────────────────────────┐ │
│ │ lamports │ 1_000_000_000 (1 SOL) │ │
│ ├───────────────┼──────────────────────────────────────────┤ │
│ │ owner │ 11111111111111111111111111111111 │ │
│ │ │ (System Program) │ │
│ ├───────────────┼──────────────────────────────────────────┤ │
│ │ executable │ false │ │
│ ├───────────────┼──────────────────────────────────────────┤ │
│ │ rent_epoch │ 425 │ │
│ ├───────────────┼──────────────────────────────────────────┤ │
│ │ data │ [...arbitrary bytes...] │ │
│ │ │ Length: 0-10MB │ │
│ └───────────────┴──────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
Account Types
1. System Accounts (Wallets)
Basic accounts owned by the System Program:
TypeScript
// System account characteristics
interface SystemAccount {
lamports: bigint; // Just holds SOL
owner: PublicKey; // Always System Program: 11111...
data: Uint8Array; // Empty (length 0)
executable: false;
}
// Creating a system account
import {
Connection,
Keypair,
SystemProgram,
Transaction,
sendAndConfirmTransaction,
} from "@solana/web3.js";
async function createSystemAccount(
connection: Connection,
payer: Keypair,
): Promise<Keypair> {
const newAccount = Keypair.generate();
const transaction = new Transaction().add(
SystemProgram.transfer({
fromPubkey: payer.publicKey,
toPubkey: newAccount.publicKey,
lamports: 1_000_000_000, // 1 SOL
}),
);
await sendAndConfirmTransaction(connection, transaction, [payer]);
return newAccount;
}
2. Program Accounts
Executable accounts containing deployed programs:
TypeScript
// Program account characteristics
interface ProgramAccount {
lamports: bigint; // Rent-exempt balance
owner: PublicKey; // BPF Loader: BPFLoaderUpgradeab1e...
data: Uint8Array; // Compiled BPF bytecode
executable: true; // Must be true
}
// Program accounts are created by deploy, not directly
// The BPF Loader owns all program accounts
3. Data Accounts (PDAs and Program-owned)
Accounts created and owned by programs to store state:
TypeScript
// Program data account characteristics
interface DataAccount {
lamports: bigint; // Rent-exempt balance
owner: PublicKey; // The program that created it
data: Uint8Array; // Serialized program state
executable: false; // Never executable
}
// Example: Token account structure
interface TokenAccount {
mint: PublicKey; // Which token
owner: PublicKey; // Who owns tokens
amount: bigint; // Token balance
delegate: PublicKey; // Optional delegate
state: number; // Account state
// ... more fields
}
Reading Account Data
TypeScript
import { Connection, PublicKey } from "@solana/web3.js";
const connection = new Connection("https://api.mainnet-beta.solana.com");
// Get account info
async function inspectAccount(address: string) {
const pubkey = new PublicKey(address);
const accountInfo = await connection.getAccountInfo(pubkey);
if (!accountInfo) {
console.log("Account does not exist");
return;
}
console.log("Account Details:");
console.log("================");
console.log("Lamports:", accountInfo.lamports);
console.log("SOL:", accountInfo.lamports / 1e9);
console.log("Owner:", accountInfo.owner.toBase58());
console.log("Executable:", accountInfo.executable);
console.log("Rent Epoch:", accountInfo.rentEpoch);
console.log("Data Length:", accountInfo.data.length, "bytes");
// Preview data (first 32 bytes)
if (accountInfo.data.length > 0) {
console.log(
"Data Preview:",
Buffer.from(accountInfo.data.slice(0, 32)).toString("hex"),
);
}
}
// Example usage
await inspectAccount("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA");
Batch Reading
TypeScript
// Efficient batch reading
async function getMultipleAccountsInfo(
connection: Connection,
pubkeys: PublicKey[],
) {
// getMultipleAccountsInfo is more efficient than multiple getAccountInfo
const accounts = await connection.getMultipleAccountsInfo(pubkeys);
return pubkeys.map((pubkey, i) => ({
pubkey,
account: accounts[i],
}));
}
// With parsed data (for known program accounts)
async function getParsedTokenAccounts(
connection: Connection,
owner: PublicKey,
) {
const accounts = await connection.getParsedTokenAccountsByOwner(owner, {
programId: TOKEN_PROGRAM_ID,
});
return accounts.value.map(({ pubkey, account }) => ({
pubkey,
mint: account.data.parsed.info.mint,
owner: account.data.parsed.info.owner,
amount: account.data.parsed.info.tokenAmount.uiAmount,
}));
}
Account Data Layouts
Programs define their own data layouts. Here's how to interpret them:
Fixed Layout Example
TypeScript
import * as borsh from "borsh";
// Define schema
class CounterAccount {
count: bigint;
authority: Uint8Array;
bump: number;
constructor(fields: { count: bigint; authority: Uint8Array; bump: number }) {
this.count = fields.count;
this.authority = fields.authority;
this.bump = fields.bump;
}
}
// Borsh schema
const counterSchema = {
struct: {
count: "u64",
authority: { array: { type: "u8", len: 32 } },
bump: "u8",
},
};
// Deserialize account data
function deserializeCounter(data: Buffer): CounterAccount {
return borsh.deserialize(counterSchema, data) as CounterAccount;
}
// Calculate required size
const COUNTER_ACCOUNT_SIZE =
8 + // count: u64
32 + // authority: Pubkey
1; // bump: u8
// Total: 41 bytes
Dynamic Layout Example
TypeScript
// Account with variable-length data
class PostAccount {
author: Uint8Array; // 32 bytes
timestamp: bigint; // 8 bytes
title: string; // 4 + length bytes
content: string; // 4 + length bytes
static calculateSize(title: string, content: string): number {
return (
32 + // author
8 + // timestamp
4 +
Buffer.byteLength(title, "utf8") + // title
4 +
Buffer.byteLength(content, "utf8") // content
);
}
}
// When creating account, calculate exact size needed
const titleBytes = Buffer.byteLength(title, "utf8");
const contentBytes = Buffer.byteLength(content, "utf8");
const space = 32 + 8 + 4 + titleBytes + 4 + contentBytes;
Account Creation Deep Dive
Creating Data Accounts
TypeScript
import {
Connection,
Keypair,
PublicKey,
SystemProgram,
Transaction,
TransactionInstruction,
} from "@solana/web3.js";
// Method 1: Two-step creation (explicit)
async function createAccountExplicit(
connection: Connection,
payer: Keypair,
programId: PublicKey,
space: number,
): Promise<Keypair> {
const newAccount = Keypair.generate();
// Calculate rent-exempt balance
const rentExemptBalance =
await connection.getMinimumBalanceForRentExemption(space);
const createAccountIx = SystemProgram.createAccount({
fromPubkey: payer.publicKey,
newAccountPubkey: newAccount.publicKey,
lamports: rentExemptBalance,
space,
programId, // Sets the owner
});
const transaction = new Transaction().add(createAccountIx);
await sendAndConfirmTransaction(
connection,
transaction,
[payer, newAccount], // newAccount must sign
);
return newAccount;
}
// Method 2: Program creates account (CPI)
// The program calls System Program internally
// Common for PDAs
Account Ownership Rules
Text
OWNERSHIP RULES
═══════════════
1. Only the OWNER can modify account data
└── Programs check: account.owner == expected_program
2. Anyone can credit lamports
└── No permission needed to send SOL
3. Only owner or System Program can debit lamports
└── Programs can debit their own accounts
4. Only owner can change owner
└── Typically to System Program (close account)
5. Only System Program can change executable flag
└── During program deployment
Account Addresses
Regular Keypairs
TypeScript
import { Keypair, PublicKey } from '@solana/web3.js';
// Generate random keypair
const keypair = Keypair.generate();
console.log('Public Key:', keypair.publicKey.toBase58());
console.log('Secret Key:', Buffer.from(keypair.secretKey).toString('hex'));
// From seed (deterministic)
const seed = Buffer.from('my-seed-phrase-here', 'utf8');
const seedKeypair = Keypair.fromSeed(seed.slice(0, 32));
// From existing secret key
const secretKey = Uint8Array.from([...]); // 64 bytes
const existingKeypair = Keypair.fromSecretKey(secretKey);
Program Derived Addresses (PDAs)
TypeScript
import { PublicKey } from "@solana/web3.js";
// PDAs are derived deterministically
const [pda, bump] = PublicKey.findProgramAddressSync(
[Buffer.from("prefix"), userPubkey.toBuffer(), Buffer.from("suffix")],
programId,
);
// PDAs have NO private key
// Only the program can sign for them via CPI
console.log("PDA:", pda.toBase58());
console.log("Bump:", bump);
Account Discovery
Finding accounts without knowing addresses:
TypeScript
// Method 1: getProgramAccounts (expensive, use carefully)
const accounts = await connection.getProgramAccounts(programId, {
filters: [
{ dataSize: 165 }, // Exact size
{
memcmp: {
offset: 0, // Where to start comparing
bytes: mint.toBase58(), // Base58 encoded value
},
},
],
});
// Method 2: getTokenAccountsByOwner (for SPL tokens)
const tokenAccounts = await connection.getTokenAccountsByOwner(owner, { mint });
// Method 3: Derive PDAs (preferred when possible)
const [userAccount] = PublicKey.findProgramAddressSync(
[Buffer.from("user"), owner.toBuffer()],
programId,
);
Common Patterns
Account Initialization Pattern
Rust
// In Anchor/Rust program
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(
init, // Create account
payer = user, // Who pays
space = 8 + MyData::INIT_SPACE, // Size needed
seeds = [b"my-data", user.key().as_ref()],
bump,
)]
pub my_data: Account<'info, MyData>,
#[account(mut)]
pub user: Signer<'info>,
pub system_program: Program<'info, System>,
}
Account Validation Pattern
TypeScript
// Client-side validation before sending transaction
async function validateAccount(
connection: Connection,
address: PublicKey,
expectedOwner: PublicKey,
expectedSize: number,
): Promise<boolean> {
const account = await connection.getAccountInfo(address);
if (!account) {
console.error("Account does not exist");
return false;
}
if (!account.owner.equals(expectedOwner)) {
console.error("Wrong owner");
return false;
}
if (account.data.length !== expectedSize) {
console.error("Wrong size");
return false;
}
return true;
}
Key Takeaways
- Everything is an account - programs, wallets, data
- Owner determines who can modify data
- Data is just bytes - programs interpret meaning
- Accounts cost rent (or rent-exempt balance)
- PDAs enable deterministic addresses without keypairs
Next: Lamports & Rent - Understanding Solana's economic model for accounts.