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
Optional reading history is saved only in this browser. It is not a scored checkpoint or exam result.
In this lesson8 field notes · practice · sources · checkpoint
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.
- SELECT CLUSTER + WALLET
- DISCOVER CAPABILITIES
- CHECK CHAIN + ACCOUNT
- PREVIEW EXACT REQUEST
- LEARNER APPROVES OR DECLINES
- RECONCILE RESULT BEFORE RETRY
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.
- SERVER: issue domain-bound challenge
- APP: inspect signIn feature and version
- WALLET: learner reviews and signs message
- SERVER: verify exact challenge and signature
- 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 workbookAll 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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
// 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.
- Read the official Kit client and React wallet hooks guides; identify the selected chain and RPC endpoint in the application state.
- Implement connect and disconnect with Wallet Standard discovery; test that account changes clear stale account-scoped data.
- Add a wallet-free mock test for missing signing capability and a Devnet/Mainnet mismatch.
- Implement a one-time domain-bound sign-in challenge and test replay rejection; do not send a transaction for login.
- For a fictional state change, render program, destination, instruction, fee payer, transaction fee, and account funding before opening approval.
- 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?
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?
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?
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.
YOUR PROGRESS, SAVED
Ready for the checkpoint?
Make a free account to save your progress and unlock the quizzes.
Create free account