learn.sol
Solana Kit Clients • Mini Capstone Kit Client For Anchor Program
Lesson 11 of 11

Mini Capstone: Kit Client for an Anchor Program

Ship one real client slice: connect a wallet, read state, send a real transaction, submit a real program action, and verify the outcome.

This capstone should feel smaller than a full product and more real than a toy.

That is the right target.

You are not building ten pages.

You are building one vertical slice that proves your client architecture is sound.

A good vertical slice does four things cleanly:

  • acquires signer access
  • reads chain state
  • submits a real transaction
  • proves the result after the write path finishes

If one of those is fake, the slice is incomplete.

Vertical SliceTap to reveal
One narrow but complete client flow that proves the architecture works from wallet connection through verification.
Vertical Slice
Shared AuthorityTap to reveal
Using the same actual wallet identity across browser and setup script so program actions and verification point at the same account.
Shared Authority
Capstone ConfigTap to reveal
The small deterministic config layer that stores the exact runtime values the final page needs instead of relying on placeholders.
Capstone Config
VerificationTap to reveal
The final step where the client or Explorer proves the write path really landed instead of stopping at a local success message.
Verification

What You Are Building

Build one page that composes the pieces from the earlier lessons:

  • wallet connection
  • account inspection
  • one real SOL transfer card
  • one real Anchor program action button

That is enough.

Do not add tabs, dashboards, or multi-program routing.

The point is correctness, not surface area.

Step 1: Prepare One Real Devnet Wallet And One Real Recipient

This page only becomes runnable if the browser wallet and the local setup script both use the same authority.

Use one dedicated devnet wallet for that.

The cleanest path is to use a mnemonic and derive the first Solana account from it in both places.

  1. create or import one dedicated devnet wallet in your browser extension
  2. keep that wallet connected to devnet
  3. store its mnemonic locally in .env.local
CAPSTONE_AUTHORITY_MNEMONIC="word1 word2 word3 ... word12"

Now install the one extra dependency needed to derive the same wallet locally:

pnpm add bip39 micro-ed25519-hdkey

Next, create one real recipient address you can inspect later:

mkdir -p keys
solana-keygen new --no-bip39-passphrase -o keys/kit-capstone-recipient.json
solana address -k keys/kit-capstone-recipient.json

Copy the printed recipient address.

You will use it for the SOL transfer card.

Step 2: Create One Real Counter Account With The Same Authority

Create scripts/create-capstone-counter.ts:

import fs from "node:fs";
import * as bip39 from "bip39";
import { HDKey } from "micro-ed25519-hdkey";
import {
  airdropFactory,
  address,
  appendTransactionMessageInstructions,
  createKeyPairSignerFromBytes,
  createKeyPairSignerFromPrivateKeyBytes,
  createSolanaRpc,
  createSolanaRpcSubscriptions,
  createTransactionMessage,
  generateKeyPairSigner,
  getSignatureFromTransaction,
  lamports,
  pipe,
  sendAndConfirmTransactionFactory,
  setTransactionMessageFeePayerSigner,
  setTransactionMessageLifetimeUsingBlockhash,
  signTransactionMessageWithSigners,
} from "@solana/kit";
import { getInitializeInstruction } from "../lib/generated/counter";

const RPC_URL = process.env.NEXT_PUBLIC_RPC_URL ?? "https://api.devnet.solana.com";
const RPC_WS_URL = process.env.NEXT_PUBLIC_RPC_WS_URL ?? "wss://api.devnet.solana.com";
const SYSTEM_PROGRAM_ADDRESS = address("11111111111111111111111111111111");

async function getAuthorityFromMnemonic(mnemonic: string) {
  const seed = bip39.mnemonicToSeedSync(mnemonic);
  const hd = HDKey.fromMasterSeed(seed.toString("hex"));
  const child = hd.derive("m/44'/501'/0'/0'");

  return createKeyPairSignerFromPrivateKeyBytes(new Uint8Array(child.privateKey));
}

async function loadRecipient() {
  const recipientBytes = Uint8Array.from(
    JSON.parse(fs.readFileSync("keys/kit-capstone-recipient.json", "utf8")),
  );

  return createKeyPairSignerFromBytes(recipientBytes);
}

async function main() {
  const mnemonic = process.env.CAPSTONE_AUTHORITY_MNEMONIC;

  if (!mnemonic) {
    throw new Error("Missing CAPSTONE_AUTHORITY_MNEMONIC in .env.local");
  }

  const rpc = createSolanaRpc(RPC_URL);
  const rpcSubscriptions = createSolanaRpcSubscriptions(RPC_WS_URL);
  const sendAndConfirmTransaction = sendAndConfirmTransactionFactory({
    rpc,
    rpcSubscriptions,
  });

  const authority = await getAuthorityFromMnemonic(mnemonic);
  const recipient = await loadRecipient();
  const counter = await generateKeyPairSigner();

  await airdropFactory({ rpc, rpcSubscriptions })({
    recipientAddress: authority.address,
    lamports: lamports(2_000_000_000n),
    commitment: "confirmed",
  });

  const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();

  const initializeInstruction = getInitializeInstruction({
    authority: authority.address,
    counter,
    systemProgram: SYSTEM_PROGRAM_ADDRESS,
  });

  const transactionMessage = pipe(
    createTransactionMessage({ version: 0 }),
    (tx) => setTransactionMessageFeePayerSigner(authority, tx),
    (tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx),
    (tx) => appendTransactionMessageInstructions([initializeInstruction], tx),
  );

  const signedTransaction = await signTransactionMessageWithSigners(
    transactionMessage,
  );

  await sendAndConfirmTransaction(signedTransaction, {
    commitment: "confirmed",
  });

  console.log("AUTHORITY_ADDRESS=", authority.address);
  console.log("TRANSFER_DESTINATION=", recipient.address);
  console.log("COUNTER_ACCOUNT=", counter.address);
  console.log(
    "INITIALIZE_SIGNATURE=",
    getSignatureFromTransaction(signedTransaction),
  );
}

main().catch((e) => {
  console.error(e);
  process.exit(1);
});

Run it with:

pnpm tsx scripts/create-capstone-counter.ts

This script does four important jobs:

  • derives the same authority that your browser wallet should be using
  • funds that authority on devnet
  • creates a brand new counter account
  • prints the exact addresses the capstone page needs

If the authority address printed here does not match the connected browser wallet, stop and fix that first.

Do not keep going with mismatched authorities.

Step 3: Save The Printed Values In One Config File

Create lib/solana/capstoneConfig.ts:

export const CAPSTONE_TRANSFER_DESTINATION = "PASTE_TRANSFER_DESTINATION_HERE";
export const CAPSTONE_COUNTER_ACCOUNT = "PASTE_COUNTER_ACCOUNT_HERE";

Paste the exact values printed by create-capstone-counter.ts.

That makes the page deterministic.

No fake placeholders. No guessing.

Step 4: Put The Capstone On A Semantic Route

Create app/kit-client-capstone/page.tsx:

"use client";

import React from "react";
import { WalletPanel } from "@/components/solana/WalletPanel";
import { AccountInspector } from "@/components/solana/AccountInspector";
import { TxActionCard } from "@/components/solana/TxActionCard";
import { CounterActionButton } from "@/components/solana/CounterActionButton";
import {
  CAPSTONE_COUNTER_ACCOUNT,
  CAPSTONE_TRANSFER_DESTINATION,
} from "@/lib/solana/capstoneConfig";

export default function KitClientCapstonePage() {
  return (
    <main className="max-w-3xl mx-auto p-6 space-y-6">
      <h1 className="text-2xl font-bold">Kit Client Capstone</h1>

      <section>
        <h2 className="text-lg font-semibold mb-2">Wallet</h2>
        <WalletPanel />
      </section>

      <section>
        <h2 className="text-lg font-semibold mb-2">Read Chain State</h2>
        <AccountInspector />
      </section>

      <section>
        <h2 className="text-lg font-semibold mb-2">Send A Real Transaction</h2>
        <TxActionCard destination={CAPSTONE_TRANSFER_DESTINATION} />
      </section>

      <section>
        <h2 className="text-lg font-semibold mb-2">Submit A Real Program Action</h2>
        <CounterActionButton counterAddress={CAPSTONE_COUNTER_ACCOUNT} />
      </section>
    </main>
  );
}

This route name matters.

The curriculum is not week-based anymore.

Your app structure should stop pretending it is.

Step 5: Verify The Slice End To End

A successful capstone run should let you prove all of these:

  1. the wallet panel shows a real connected session
  2. the account inspector fetches a real account from the configured cluster
  3. the transaction card returns a real signature after sending SOL
  4. the program action button returns a real signature for the Anchor instruction
  5. you can inspect both signatures in Explorer

That is the real checkpoint.

Not whether the page "looks complete".

The Main Failure Modes

The wallet connects but writes still fail

That usually means one of these:

  • wrong cluster
  • wrong destination address
  • not enough devnet SOL
  • the connected wallet does not match the mnemonic used by the setup script

The read panel works but the program action fails

That usually means your generated instruction builder or account wiring is wrong.

This is exactly why the service layer exists.

The failure should be isolated to the program action path, not spread through the page.

The signatures appear but you never verify them

Then the capstone is incomplete.

A real client flow ends with verification.

Use Explorer or your own read-back path and confirm that the state change actually happened.

What You Can Do After This

You can build one real client page that combines wallet access, reads, writes, program actions, and verification without collapsing into ad hoc code.

That is enough foundation to start a larger Solana frontend responsibly.

Quick Check

Quick Check
Single answer

Why does the capstone insist that the browser wallet and the setup script use the same authority?

Quick Check
Single answer

What makes the capstone incomplete even if the page appears to work?

Sources

Solana Assistant

AI-powered documentation helper

Welcome to Solana Assistant

Ask specific questions about Solana development:

Ask specific questions for better results400px