The goal of this exercise is to understand how Accounts are stored on-chain on Solana and their structure.

  • No Anchor client
  • No special libraries
  • Just Buffer and raw byte parsing

Like Imagine you pulled account data from a Pubkey, and now you're reconstructing the Account struct from Solana manually with JavaScript.

For Example, an on-chain Rust account is:

#[account]
pub struct MyAccount {
    pub authority: Pubkey, // 32 bytes
    pub value: u64,        // 8 bytes
}

All the accounts on Solana are stored flat in memory, which takes exactly 48 bytes.

Bytes Range Content Size
0..8 Discriminator 8 bytes
8..40 Authority Pubkey 32 bytes
40..48 Value (u64) 8 bytes

The first 8 bytes are to identify which type of Account it is, or more like for safety, to make sure it is an "Account".

  • It's a unique identifier for your struct.
  • Prevents deserialising the wrong data into the wrong type.
  • Protects your program from bugs and hacks.

We use Borsh(Binary Object Representation Serializer for Hashing), which turns the Rust struct into bytes and when needed, bytes into the Account Struct, where:

  • Struct fields are stored in the order you declare them.
  • No gaps, no random alignment padding.
  • Everything is stored compactly.

Each field is serialised one after another in memory.

PubKey has 32 bytes, and the remaining data is 8 bytes in little-endian format.

If you ever change the order or the fields or add fields at the top, then you'll break old accounts on the mainnet because the byte layout will mismatch!
That's why in real projects, fields are always at the end!

Now you've got enough idea about the memory structure of accounts in Solana. Let's dive into decoding them in JS!

  1. First, we have to fetch the account info.
import { Connection, PublicKey } from '@solana/web3.js';

const connection = new Connection('https://api.mainnet-beta.solana.com');

const accountPublicKey = new PublicKey('YourAccountAddressHere');

const accountInfo = await connection.getAccountInfo(accountPublicKey);

if (!accountInfo) {
  throw new Error('Account not found!');
}

const data = accountInfo.data; // This is a Buffer (or Uint8Array)
  1. Now parse the data

We manually read the bytes:

// Skip the first 8 bytes (discriminator)
const authorityBytes = data.slice(8, 8 + 32); // bytes 8..40
const valueBytes = data.slice(40, 48);         // bytes 40..48

// Parse the pubkey
const authorityPubkey = new PublicKey(authorityBytes);

// Parse the u64 value
const value = Number(
  valueBytes.readBigUInt64LE(0) // read as little-endian
);

console.log('Authority:', authorityPubkey.toBase58());
console.log('Value:', value);

Visually step by step:

[0..8]    -> skip discriminator
[8..40]   -> authority pubkey (32 bytes) -> decode with PublicKey()
[40..48]  -> value (8 bytes) -> decode with readBigUInt64LE

The full code:

import { Connection, PublicKey } from '@solana/web3.js';

async function fetchAndDecodeMyAccount(addressStr) {
  const connection = new Connection('https://api.mainnet-beta.solana.com');
  const pubkey = new PublicKey(addressStr);

  const accountInfo = await connection.getAccountInfo(pubkey);

  if (!accountInfo) {
    throw new Error('Account not found');
  }

  const data = accountInfo.data;

  if (data.length !== 48) {
    throw new Error('Unexpected account size');
  }

  const authorityBytes = data.slice(8, 40);
  const valueBytes = data.slice(40, 48);

  const authority = new PublicKey(authorityBytes);
  const value = Number(valueBytes.readBigUInt64LE(0));

  return { authority: authority.toBase58(), value };
}

// Example usage
fetchAndDecodeMyAccount('YOUR_ACCOUNT_ADDRESS').then(console.log);

This way, you can read any Solana account manually in Javascript and you understand exactly how Solana memory layout works.