learn.sol
Solana Kit Clients • Rpc Reads And Account Decoding With Kit
Lesson 4 of 11

RPC Reads and Account Decoding with Kit

Fetch accounts safely with @solana/kit, normalize the result for UI code, and decode account data only after you know what you fetched.

Reading Solana state looks simple until the client starts guessing.

A string that is not a real address gets passed through the UI.

A missing account is treated like a crash.

Raw bytes get decoded before you even know which program owns them.

That is how read paths become brittle.

The correct model is:

  • fetch first
  • check existence
  • inspect ownership and shape
  • decode only when you know what you are looking at

That order matters.

Encoded AccountTap to reveal
The raw account facts returned by the chain, including existence, lamports, owner, flags, and bytes before program-specific decoding.
Encoded Account
MaybeEncodedAccountTap to reveal
The Kit result shape that makes account existence explicit instead of pretending every lookup succeeds.
MaybeEncodedAccount
NormalizationTap to reveal
The step where transport-heavy account data is converted into a simpler shape that UI code can reason about safely.
Normalization
Decode BoundaryTap to reveal
The point after existence and ownership checks where it finally makes sense to apply a program-specific decoder.
Decode Boundary

What A Read Path Actually Returns

At the RPC layer, an account is just facts:

  • address
  • lamports
  • owner
  • executable flag
  • raw data bytes

Those facts are useful on their own.

Decoding is a second step.

Do not merge them too early.

Step 1: Fetch One Account In A Unified Shape

The Kit docs recommend fetchEncodedAccount(...) when you want one consistent account shape instead of juggling raw RPC return variants.

Create lib/solana/rpc.ts:

import {
  address,
  assertAccountExists,
  createSolanaRpc,
  fetchEncodedAccount,
  type Address,
} from "@solana/kit";
import { SOLANA_RPC } from "@/lib/solana/config";

const rpc = createSolanaRpc(SOLANA_RPC);

export type AccountView =
  | { exists: false; address: string }
  | {
      exists: true;
      address: string;
      lamports: string;
      owner: string;
      executable: boolean;
      dataLength: number;
    };

export async function fetchAccountView(input: string): Promise<AccountView> {
  const accountAddress = address(input.trim()) as Address;
  const account = await fetchEncodedAccount(rpc, accountAddress);

  if (!account.exists) {
    return {
      exists: false,
      address: input.trim(),
    };
  }

  return {
    exists: true,
    address: account.address,
    lamports: account.lamports.toString(),
    owner: account.owner,
    executable: account.executable,
    dataLength: account.data.length,
  };
}

export async function fetchExistingEncodedAccount(input: string) {
  const accountAddress = address(input.trim()) as Address;
  const account = await fetchEncodedAccount(rpc, accountAddress);
  assertAccountExists(account);
  return account;
}

This file teaches the main split:

  • fetchAccountView(...) is for UI-safe inspection
  • fetchExistingEncodedAccount(...) is for flows where missing data should be treated as a real error

That is already a much stronger model than returning raw RPC blobs everywhere.

Why exists Matters

Kit returns a MaybeEncodedAccount shape on purpose.

That is not ceremony.

A missing account is normal on Solana.

Examples:

  • wrong cluster
  • address copied incorrectly
  • account not created yet
  • PDA derived correctly but not initialized yet

That should not explode your UI by default.

Step 2: Show The Read Result Without Guessing

Now create components/solana/AccountInspector.tsx:

"use client";

import React, { useState } from "react";
import { fetchAccountView, type AccountView } from "@/lib/solana/rpc";

export function AccountInspector() {
  const [input, setInput] = useState("");
  const [loading, setLoading] = useState(false);
  const [result, setResult] = useState<AccountView | null>(null);
  const [error, setError] = useState("");

  const onFetch = async () => {
    setLoading(true);
    setError("");

    try {
      setResult(await fetchAccountView(input));
    } catch (e: any) {
      setResult(null);
      setError(String(e?.message ?? e));
    } finally {
      setLoading(false);
    }
  };

  return (
    <div className="space-y-3">
      <input
        value={input}
        onChange={(e) => setInput(e.target.value)}
        className="w-full px-3 py-2 rounded bg-zinc-900"
        placeholder="Account address"
      />

      <button
        onClick={onFetch}
        disabled={loading}
        className="px-3 py-2 rounded bg-cyan-600 text-white"
      >
        {loading ? "Loading..." : "Fetch account"}
      </button>

      {error ? <div className="text-xs text-red-300">{error}</div> : null}
      {result ? (
        <pre className="text-xs bg-zinc-950 p-3 rounded">
          {JSON.stringify(result, null, 2)}
        </pre>
      ) : null}
    </div>
  );
}

This component is honest.

It does not pretend every lookup succeeds.

It does not decode program data blindly.

It just shows the chain facts cleanly.

That is the correct first read tool.

Step 3: Decode Only After You Know The Program

Once you know an account exists and belongs to the program you expect, then decoding makes sense.

For program-specific accounts, the Kit docs and program clients make this easier.

Example with the token program:

import { decodeMint } from "@solana-program/token";
import { fetchExistingEncodedAccount } from "@/lib/solana/rpc";

export async function fetchMintSummary(input: string) {
  const encodedAccount = await fetchExistingEncodedAccount(input);
  const mintAccount = decodeMint(encodedAccount);

  return {
    address: mintAccount.address,
    lamports: mintAccount.lamports.toString(),
    decimals: mintAccount.data.decimals,
    supply: mintAccount.data.supply.toString(),
  };
}

Read that in order:

  1. fetch the encoded account
  2. assert it exists
  3. decode with the program client helper
  4. normalize again for the UI

That is the safe decoding path.

The Failure Modes To Avoid

Decoding before checking ownership

If you decode arbitrary bytes with the wrong codec, your client logic becomes nonsense quickly.

The account may exist and still be the wrong thing.

Throwing on every missing account

That makes ordinary read states feel like fatal errors.

Use missing-account behavior deliberately.

Passing raw RPC data straight to components

That leaks transport details into UI code.

Normalize first.

Always.

What You Can Do After This

You can fetch account data with a stable Kit helper, distinguish missing accounts from real failures, and decode program data only after the read path proves you are looking at the right account.

That is the right base for the transaction-building lessons that come next.

Quick Check

Quick Check
Single answer

Why is `MaybeEncodedAccount` a useful shape instead of pointless ceremony?

Quick Check
Single answer

Why should decoding happen only after the client knows the account exists and belongs to the expected program?

Sources

Solana Assistant

AI-powered documentation helper

Welcome to Solana Assistant

Ask specific questions about Solana development:

Ask specific questions for better results400px