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

  1. Everything is an account - programs, wallets, data
  2. Owner determines who can modify data
  3. Data is just bytes - programs interpret meaning
  4. Accounts cost rent (or rent-exempt balance)
  5. PDAs enable deterministic addresses without keypairs

Next: Lamports & Rent - Understanding Solana's economic model for accounts.