← Solana Builder

Intermediate · LESSON 04 OF 12

4. Connect browser wallets safely with Solana Kit

By the end, you can build a cluster-aware browser wallet flow that separates public reads, off-chain sign-in, and user-approved transactions.

Look up Solana terms in the glossary

In this lesson8 field notes · practice · sources · checkpoint
The learner selects a cluster and wallet; the app checks chain and signing capabilities, previews the exact request and costs, then handles approval or decline and reconciles submitted status before retrying.
The learner selects a cluster and wallet; the app checks chain and signing capabilities, previews the exact request and costs, then handles approval or decline and reconciles submitted status before retrying.Open full-size visual ↗
Read a text version of this diagram

The sequence separates the learner’s wallet from the Academy client and the issuer or RPC. The client discovers a wallet only after the learner chooses to connect, then checks the account and selected chain against its own RPC cluster. An off-chain sign-in message uses a one-time domain-bound nonce and does not pay a transaction fee. A state-changing transaction gets an exact action, destination, fee payer, and account-funding preview before the wallet requests approval. Decline stops the flow. After submission, the client keeps the signature and checks status on the same cluster; an uncertain timeout is reconciled before a retry.

  1. SELECT CLUSTER + WALLET
  2. DISCOVER CAPABILITIES
  3. CHECK CHAIN + ACCOUNT
  4. PREVIEW EXACT REQUEST
  5. LEARNER APPROVES OR DECLINES
  6. RECONCILE RESULT BEFORE RETRY
The server issues a one-use sign-in challenge, the app checks supported wallet features, and the server verifies the exact signed message before opening an Academy session.
Sign In With Solana: challenge to verified session. The server issues a one-use sign-in challenge, the app checks supported wallet features, and the server verifies the exact signed message before opening an Academy session.Open full-size visual ↗
Read a text version: Sign In With Solana: challenge to verified session

The server creates and stores a Sign In With Solana challenge with the expected domain, statement, nonce, and issue time. The application checks the wallet-level solana:signIn feature version; version 1.1.0 can request off-chain message version 1. If that feature is unavailable, the connected account may support solana:signOffchainMessage version 1, followed by raw solana:signMessage as a clearly explained fallback. The learner sees and signs a message. The server compares the returned content with its stored challenge, verifies the address and signature, enforces expiry, consumes the nonce exactly once, and only then creates the session. This flow has no transaction, transaction fee, or RPC call.

  1. SERVER: issue domain-bound challenge
  2. APP: inspect signIn feature and version
  3. WALLET: learner reviews and signs message
  4. SERVER: verify exact challenge and signature
  5. Consume nonce once; create session

FREE INTERACTIVE CASEBOOK · ONLINE OR OFFLINE

Connect carefully. Review before signing.

Work through six fictional browser wallet flows covering connection, off-chain sign-in, network mismatch, capabilities, transaction costs, rejection, and uncertain submission. This local casebook uses no wallet, RPC, network request, transaction, or saved answers.

Open the Wallet Integration Casebook Download the offline casebook Download the Wallet Standard coding workbook

All accounts and requests are fictional. A wallet connection or web sign-in is not approval for a transaction. The workbook uses Wallet Standard discovery locally and produces synthetic signatures that must never be used as proof of wallet control.

FIELD NOTE 01

Use one explicit client for one selected cluster

A browser app usually needs a read client and a wallet signer, but those are different capabilities. With the current Solana Kit path, compose the client with the intended RPC endpoint and a `walletSigner` for the selected chain, then provide that client through the React `ClientProvider`. Keep the cluster decision visible and consistent: the RPC endpoint, wallet account chain, explorer links, and transaction preview must refer to the same cluster. The app may perform a read without a wallet signer; only add signing capability when a real user action needs it.

FIELD NOTE 02

Discover and connect by user choice

Use Wallet Standard discovery through the current `@solana/kit-plugin-wallet/react` hooks. The current React integration publishes one Kit client with `ClientProvider`; get that same typed client using `useClient<AppClient>()` and pass it explicitly to `useWallets(client)`, `useWalletStatus(client)`, `useConnectedWallet(client)`, `useConnect(client)`, and `useDisconnect(client)`. Let the learner choose from the discovered wallets, show pending reconnection state, and provide a disconnect control. `useConnectedWallet` can return `null`, and its `signer` can also be `null` for a read-only account; handle both before offering a signing action. A restored or connected account does not approve a transaction or sign the learner into the Academy. Older `useSignIn` and `useSignMessage` hooks also exist under `@solana/react` with different arguments; follow the Kit wallet plugin versions for this client pattern and do not mix the two hook families. Always refresh account-scoped views when the selected address changes.

FIELD NOTE 03

Check chain and account capabilities before acting

Before a wallet action, compare the selected account’s chain with the client’s configured cluster. If they differ, stop and explain the mismatch; do not silently change the RPC endpoint or guess which network the learner intended. Inspect the account and wallet’s advertised features before calling a sign-message, sign-transaction, or sign-and-send method. Capabilities vary across wallets, and a missing feature should lead to a clear safe fallback or no action. Do not request, import, or store a recovery phrase or private key in a web application.

FIELD NOTE 04

Separate connection, sign-in, and transaction approval

Connection lets the app learn the approved public address and use supported wallet features. If the app needs proof that a learner controls that address for an Academy web session, it can request an off-chain signed message instead of writing a transaction to Solana. The server should issue a fresh unpredictable nonce and bind the challenge to the expected domain, purpose, issue time, and expiry; it verifies the signature and consumes the nonce once. Explain the message before asking for a signature. A signed message is not a transaction, and neither connection nor a course login authorizes a later transfer. Solana documents off-chain message signing as the preferred format when the wallet supports it: version 1 messages are explicitly distinguishable from transactions, are readable, and require no transaction fee, RPC request, or network call. Check account features and supported versions. A raw `signMessage` fallback signs the exact bytes without a transaction prefix, so clearly label its contents; never present either message path as an asset transfer. Hardware-wallet users without message support should receive an explicit opt-in before any transaction-based fallback.

FIELD NOTE 05

Preview exactly what a transaction will do

For a state-changing action, build a transaction from the selected network and current application state, then show a human-readable summary before asking the wallet to sign. Identify the program or destination, the accounts and value involved, the selected cluster, the transaction fee payer, and any account-creation or storage funding separately. Do not promise success merely because a wallet signed or an RPC accepted the request. The learner decides whether to approve in their wallet and pays the applicable network fee and any account funding for their own credential mint.

FIELD NOTE 06

Handle rejection and uncertain submission honestly

A wallet rejection means stop and report that the learner declined; never retry automatically or mark the task complete. After a transaction is submitted, keep its signature, intended cluster, and request context. A browser or RPC timeout after submission leaves the outcome unknown, not failed. Query status with bounded retries and distinguish not-yet-visible, confirmed, failed, and unavailable observations. Reconcile the known signature before offering another submission so a slow response does not create a duplicate action.

FIELD NOTE 07

Test every boundary without a live wallet

Use an injected mock client, wallet, and RPC to exercise connect, disconnect, account change, missing feature, wrong chain, message rejection, transaction rejection, preflight failure, and timeout after submission. Assert the exact domain and one-time nonce for sign-in; assert the exact fee payer, destination, instruction set, network, and funding summary for a transaction. Keep automated tests deterministic and wallet-free. A successful mock test proves the app handled the simulated case; it does not prove an external wallet or RPC service behaved correctly.

FIELD NOTE 08

Ship a small, auditable integration

Pin compatible Kit and React package versions and follow their official current APIs; older web3.js and wallet-adapter integrations may be appropriate for an existing application, but do not mix examples from different generations without a migration plan. Put RPC URLs and server secrets in their intended configuration boundary. Log safe request identifiers and public transaction signatures only when necessary; never log signing payloads containing private data. On production builds, verify the shipped bundle contains no private key material, and rehearse failure and rollback paths on a non-production cluster.

MAKE THE CONNECTION

A detail worth keeping.

Treat wallet integrations as capability boundaries, not button wiring. Model selected wallet, selected account, configured RPC cluster, advertised signing features, and request state independently. A change in one should invalidate any UI assumptions that were based on the previous selection.

Work through an example

Off-chain authentication still needs server discipline: issue a nonce with sufficient entropy, bind the signed bytes to the exact domain and purpose, enforce an expiry, verify the signature for the claimed public key, and consume the nonce atomically. A valid signature alone does not make a reusable challenge safe.

Current Solana React wallet flow in three files: build one Kit client, publish it once with ClientProvider, then discover and connect a learner-selected Wallet Standard wallet through the Kit wallet React hooks. A connected wallet may be read-only (signer is null). Connection is not consent to sign a message or transaction. The walletSigner client makes the connected wallet the fee payer and identity in this baseline; explain that cost before requesting approval. Pin compatible package versions before shipping.
// client.ts — build one client outside the React tree.
import { createClient } from "@solana/kit";
import { solanaRpc } from "@solana/kit-plugin-rpc";
import { walletSigner } from "@solana/kit-plugin-wallet";

export const client = createClient()
  .use(walletSigner({ chain: "solana:devnet" }))
  .use(solanaRpc({ rpcUrl: "https://api.devnet.solana.com" }));
export type AppClient = Awaited<typeof client>;

// providers.tsx — publish the same client once.
import { ClientProvider } from "@solana/react";
import { client } from "./client";

export function Providers({ children }: { children: React.ReactNode }) {
  return <ClientProvider client={client}>{children}</ClientProvider>;
}

// wallet-panel.tsx — call wallet hooks only from a client component.
"use client";
import {
  useConnect, useConnectedWallet, useDisconnect, useWallets, useWalletStatus,
} from "@solana/kit-plugin-wallet/react";
import { useClient } from "@solana/react";
import type { AppClient } from "./client";

export function WalletPanel() {
  const client = useClient<AppClient>();
  const status = useWalletStatus(client);
  const wallets = useWallets(client);
  const connected = useConnectedWallet(client);
  const connect = useConnect(client);
  const disconnect = useDisconnect(client);

  if (status === "pending") return <p>Restoring wallet connection…</p>;
  if (connected) {
    return (
      <section>
        <p>Connected address: {connected.account.address}</p>
        {!connected.signer && <p>This account is read-only; it cannot approve transactions.</p>}
        <button onClick={() => disconnect.dispatch()}>Disconnect</button>
      </section>
    );
  }
  return (
    <section aria-label="Choose a wallet">
      {wallets.map((wallet) => (
        <button key={wallet.name} disabled={connect.isRunning}
          onClick={() => connect.dispatch(wallet)}>
          Connect {wallet.name}
        </button>
      ))}
    </section>
  );
}

Draw the request sequence for connect, off-chain sign-in, and transaction approval. Mark what is free, what the wallet must approve, who pays each cost, and how a timeout after submission is reconciled.

GUIDED PRACTICE · INTERMEDIATE

Do the work, one step at a time.

Use fictional addresses and disposable test data. Do not enter a recovery phrase or private key. Most exercises work on paper or with a local test; only a lesson that clearly says “read-only Devnet” makes a public network request.

  1. Read the official Kit client and React wallet hooks guides; identify the selected chain and RPC endpoint in the application state.
  2. Implement connect and disconnect with Wallet Standard discovery; test that account changes clear stale account-scoped data.
  3. Add a wallet-free mock test for missing signing capability and a Devnet/Mainnet mismatch.
  4. Implement a one-time domain-bound sign-in challenge and test replay rejection; do not send a transaction for login.
  5. For a fictional state change, render program, destination, instruction, fee payer, transaction fee, and account funding before opening approval.
  6. Simulate learner rejection and a lost RPC response after submission; retain and reconcile the signature instead of blindly resubmitting.

Before you move on, check your reasoning

  • Mock each wallet and RPC transition, including late responses after account switches.
  • Review the exact signing payload and decoded transaction in tests before any deployment.
  • Test cost language separately for transaction fees and created-account funding.

OPEN SELF-CHECK · UNGRADED

Pause and test your reasoning

Practice 1 of 3

A learner connects a wallet, then chooses “sign in.” What extra step proves control of the address for this web session without submitting a transaction?

  1. Issue a one-time domain-bound message challenge and verify its signature server-side.
  2. Treat a connected public account as proof that the server has authenticated the learner and the wallet approved any future transaction.
  3. Skip a fresh message challenge and treat automatic reconnect as permanent consent for every state-changing Academy action.
Reveal the answer and explanation

One best answer: Issue a one-time domain-bound message challenge and verify its signature server-side.

Connection and authentication are separate. A scoped off-chain signature can prove control of a key without paying a transaction fee or making an RPC call; the server must verify the challenge, enforce its expiry, and consume its fresh nonce exactly once.

Sources: Sign In With Solana · React Hooks

Practice 2 of 3

The connected account is on Mainnet, while the application RPC and transaction preview are set to Devnet. What should happen?

  1. Silently change the app to Mainnet.
  2. Stop and explain the mismatch, then require a deliberate supported network selection before continuing.
  3. Ask the learner to approve a transaction in the wallet and trust it to select Mainnet even while the Academy RPC and constructed transaction still target Devnet.
Reveal the answer and explanation

One best answer: Stop and explain the mismatch, then require a deliberate supported network selection before continuing.

Wallet chain and application RPC context must agree. Stop while they differ, explain which networks are selected, and require a deliberate supported network choice before rebuilding the transaction preview.

Sources: Kit Client · React Hooks

Practice 3 of 3

The wallet signed a transaction, but the submit response timed out. What is the safest next step?

  1. Immediately send a newly built transaction.
  2. Mark the action failed because the page timed out.
  3. Keep the result unknown, retain the signature if available, and reconcile status on the intended cluster before retrying.
Reveal the answer and explanation

One best answer: Keep the result unknown, retain the signature if available, and reconcile status on the intended cluster before retrying.

A lost response does not prove that a transaction was not submitted. Retain the known signature and request context, then reconcile status on the same intended cluster before offering a deliberate retry.

Sources: Kit Client · React Hooks

This practice is not scored, saved, or part of a certificate. Account-based lesson checkpoints and level exams remain separate.

Never enter a recovery phrase or private key into a learning site or lesson exercise.

READ THE SOURCE

Go deeper in the official docs

Reviewed on 2026-10-03. Protocol documentation can change; check the linked page’s current version as you use this material.

  1. Kit Client ↗
  2. React Hooks ↗
  3. Migrating to Kit ↗
  4. Sign In With Solana ↗

Report an issue with this lesson

YOUR PROGRESS, SAVED

Ready for the checkpoint?

Make a free account to save your progress and unlock the quizzes.

Create free account