Hello again! Welcome back to our series on client-side Solana development.
In our last lesson, you successfully set up a TypeScript project with the modern @solana/web3.js v2 SDK and established a connection to the Solana Devnet. You verified this by fetching the current slot, which is like getting a heartbeat from the blockchain.
Today, we'll build directly on that foundation to achieve a core task for any dApp: reading and understanding on-chain data. Our goal is to fetch the data stored in a Solana account and transform it from a raw, unreadable binary format into a structured, usable JavaScript object. This process, known as deserialization, is fundamental for displaying on-chain state to users, such as their token balances or the details of an NFT.
1. Fetching Raw Account Data
As we've discussed, all data on Solana is stored in accounts. Each account has a unique address (a PublicKey) and contains data, lamports (for rent), and metadata like its owner program.
In the previous lesson, we used rpc.getSlot().send(). To fetch an account's full information, we use a similar method: rpc.getAccount(address).send(). This is the v2 equivalent of the older connection.getAccountInfo() method you might see in older tutorials.
Let's start by fetching the data for a well-known account: the Wrapped SOL (wSOL) mint account on Devnet. This account defines the properties of the wSOL token.
Update your src/index.ts file with the following code:
// src/index.ts
import { createSolanaRpc, address } from '@solana/web3.js';
const RPC_URL = 'https://api.devnet.solana.com';
// The address of the Wrapped SOL mint on Devnet
const wSolMintAddress = address('So11111111111111111111111111111111111111112');
async function main() {
console.log('Connecting to Solana Devnet...');
const rpc = createSolanaRpc(RPC_URL);
try {
// Fetch the account information.
const wSolMintAccount = await rpc.getAccount(wSolMintAddress).send();
if (!wSolMintAccount.value) {
console.error('Account not found!');
return;
}
console.log('✅ Account found!');
console.log('Owner Program:', wSolMintAccount.value.owner);
console.log('Is Executable?:', wSolMintAccount.value.executable);
console.log('Lamports (for rent):', wSolMintAccount.value.lamports);
// Log the raw data buffer
console.log('Raw Account Data:', wSolMintAccount.value.data);
} catch (error) {
console.error('❌ Failed to fetch account:', error);
}
}
main();
Now, run this script:npx esrun src/index.ts
You will see some metadata and then the Raw Account Data printed as a Uint8Array. It's just a sequence of numbers, which isn't very useful to us. This is because the data is serialized.
2. Why is the Data Serialized? Introducing Borsh
To understand why the data looks like this, we need to talk about Borsh (Binary Object Representation Serializer for Hashing). Borsh is a serialization format designed to be compact, fast, and deterministic, which are all critical properties for a high-performance blockchain. When a Solana program saves data to an account, it first serializes the data structure (e.g., a Rust struct) into this compact binary format.
Our job on the client side is to perform the reverse process: deserialization.
How to Deserialize Account Data on Solana
The following article, 'How to Deserialize Account Data on Solana' from QuickNode, provides a great introduction to Borsh and the problem we're trying to solve. Please read the introduction to solidify your understanding.
Read the 'Overview' and 'Why Borsh?' sections. This will clarify why we see a raw buffer instead of readable data. Note that the guide uses an older version of @solana/web3.js, but the concepts about Borsh are universal.
3. The Deserialization Process with buffer-layout
To deserialize the data, we need two things:
- The data's structure (schema): We need to know exactly how the data was organized when it was serialized. For example, "the first byte is a number, the next 32 bytes are a public key," etc.
- A tool to apply the schema: We need a library that can read our schema and use it to parse the raw binary buffer.
For this, we'll use the @solana/buffer-layout and @solana/buffer-layout-utils libraries. Let's install them:
npm install @solana/buffer-layout @solana/buffer-layout-utils
Now, let's walk through the process of deserializing the wSOL mint account data.
Step 1: Find the On-Chain Data Structure
This is the most crucial step. You must know the exact Rust struct that the on-chain program uses. For standard programs like the SPL Token Program (which governs tokens like wSOL), the source code is public.
If you look at the SPL Token program's source code, you'll find the Mint struct:
// From the SPL Token program source code
pub struct Mint {
pub mint_authority: COption<Pubkey>,
pub supply: u64,
pub decimals: u8,
pub is_initialized: bool,
pub freeze_authority: COption<Pubkey>,
}
This is our blueprint. It tells us the fields and their types.
Step 2: Define the Layout and TypeScript Interface
Now we translate this Rust struct into a schema using buffer-layout. We'll also create a TypeScript interface for type safety.
The QuickNode and Helius articles both provide excellent walkthroughs of this process. We'll use their logic but adapt the code to our project.
How to Deserialize Account Data on Solana
This section of the QuickNode guide demonstrates how to define a TypeScript interface and a corresponding buffer-layout structure for an SPL Mint account. This is exactly what we need to do.
Read 'Step 2 - Deserialize Account Data', focusing on the subsections 'Define TypeScript Interface' and 'Define Buffer layout'. This explains how the on-chain struct is mapped to a client-side schema. Again, disregard the specific code for fetching data, as it uses the old SDK.
Let's apply this to our index.ts file.
First, add the new imports at the top:
import { struct, bool, u8 } from '@solana/buffer-layout';
import { publicKey, u64 } from '@solana/buffer-layout-utils';
import { createSolanaRpc, address, Address } from '@solana/web3.js';
Next, define the interface and the layout. The COption<Pubkey> from Rust translates into an optional public key. A common way to represent this in buffer-layout is with a 4-byte flag (u32) indicating if the option is Some (1) or None (0), followed by the 32-byte public key itself.
Add this code below your imports:
// Interface to represent the deserialized mint account data
interface Mint {
mintAuthorityOption: 1 | 0;
mintAuthority: Address;
supply: bigint;
decimals: number;
isInitialized: boolean;
freezeAuthorityOption: 1 | 0;
freezeAuthority: Address;
}
// Layout for the mint account data
const MintLayout = struct<Mint>([
u32('mintAuthorityOption'),
publicKey('mintAuthority'),
u64('supply'),
u8('decimals'),
bool('isInitialized'),
u32('freezeAuthorityOption'),
publicKey('freezeAuthority'),
]);
Note: The u32 for the option flag is a quirk of the original SPL implementation. Newer programs might use a single byte.
Step 3: Decode the Data
Now we can use our MintLayout to decode the raw data buffer. The layout object has a decode() method that takes the Uint8Array and returns our structured Mint object.
Modify your main function to decode and log the data:
async function main() {
console.log('Connecting to Solana Devnet...');
const rpc = createSolanaRpc(RPC_URL);
try {
const wSolMintAccount = await rpc.getAccount(wSolMintAddress).send();
if (!wSolMintAccount.value) {
console.error('Account not found!');
return;
}
// Get the raw data buffer
const rawData = wSolMintAccount.value.data;
console.log('Raw data length:', rawData.length);
// Ensure the data is the expected size
if (rawData.length !== MintLayout.span) {
throw new Error(`Invalid account data length for Mint: expected ${MintLayout.span}, got ${rawData.length}`);
}
// Decode the data
const deserializedData = MintLayout.decode(rawData);
console.log('\n✅ Deserialized Mint Data:');
console.log({
...deserializedData,
mintAuthority: deserializedData.mintAuthority.toString(),
freezeAuthority: deserializedData.freezeAuthority.toString(),
});
} catch (error) {
console.error('❌ An error occurred:', error);
}
}
Run the script again: npx esrun src/index.ts.
This time, instead of a raw buffer, you should see a clean, structured object with properties like supply, decimals, and mintAuthority! You've successfully read and interpreted on-chain state.
Test your understanding!
The USDC token mint address on Mainnet is EPjFWdd5AufqSSqeM8qN1xzybapC8G4wEGGkZwyTDt1v. Modify your script to:
- Change the
RPC_URLto a mainnet endpoint (e.g.,https://api.mainnet-beta.solana.com). - Change the mint address to the USDC mint address.
- Run the script and observe the
decimalsandsupply. What are they?
Show answer
You would change the constants at the top of your file:
const RPC_URL = 'https://api.mainnet-beta.solana.com';
const mintAddress = address('EPjFWdd5AufqSSqeM8qN1xzybapC8G4wEGGkZwyTDt1v');
And use mintAddress in your main function.
When you run it, you should see that USDC has decimals: 6 and a very large supply. This is how you would programmatically verify token properties.
4. A Useful Tool: The Borsh Decoder
Writing code to deserialize every account you want to inspect can be tedious during development. Luckily, there are tools to help.
How to Deserialize Account Data on Solana
The QuickNode guide introduces a fantastic web-based tool for deserializing account data on the fly. This is incredibly useful for debugging and exploration.
Read the section 'Deserializing on the Fly' and visit the 'SOL/Borsh Decoder by M2' tool linked within. Try pasting the wSOL or USDC mint address into it. The tool has pre-defined layouts for common programs like the SPL Token program, making inspection easy.
This tool is a perfect example of how the community builds on core primitives. It essentially does what your script does—fetches account data and applies a known layout—but provides a convenient user interface.
Conclusion
In this lesson, you mastered a fundamental skill for any Solana developer. You learned how to bridge the gap between the on-chain world of serialized binary data and the off-chain world of structured JavaScript objects.
Here are the key takeaways:
- On-chain data is typically serialized using Borsh for efficiency.
- The
rpc.getAccount(address).send()method fetches the raw account information, including the serialized data buffer. - To deserialize data, you must first know the on-chain data structure (the Rust struct).
- The
@solana/buffer-layoutlibrary allows you to define a schema that matches the on-chain struct. - Calling
layout.decode(dataBuffer)applies this schema to parse the raw data into a usable JavaScript object.
You now have the power to read and understand the state of any account on Solana, provided you know its structure. This is the "read" part of the "read/write" equation.
In our next lesson, we will tackle the "write" part. You will learn how to construct a transaction with one or more instructions in TypeScript, preparing you to finally interact with and change the state of on-chain programs.
Can't find a good explanation? Sign up and we'll make it for you
Sign up