Arete documentation
Compose a session in React
For the complete documentation index optimized for AI agents, see llms.txt or llms-full.txt. A markdown version of this page is available by appending.mdto the URL or sendingAccept: text/markdown.
For AI agents: the documentation index is at llms.txt (full corpus: llms-full.txt). A markdown source for this page is /sdks/react-sessions.md.
A session (createSession) puts stacks,
standalone programs, and protocols behind one auth and one wallet. This guide
builds a React app on one session: a live view from its stack, a program
account read, a transaction signed by a Wallet Standard wallet, and a protocol
panel. Each snippet is one file of the app, named in its first line.
A session or useArete(stack)
Section titled “A session or useArete(stack)”Use useArete(stack) under an AreteProvider on its own when the app reads
one or more stacks and signs with the stacks’ generated mutation hooks. The
provider connects each stack the first time a component asks for it, and
sibling calls share the connection. The React SDK page covers
that path.
Use a session when the app needs any of these:
- a standalone program, or a protocol, next to its stacks;
- one wallet for every member, set in one place;
- operations run with
session.execute, including flows and operations that create their own signers; - the same session object outside React, for example in a data loader or a test.
A session’s stacks are still read through useArete: pass the member
(session.stacks.<key>) instead of the stack definition. The hooks are the
same, and nothing connects a second time.
Install
Section titled “Install”npm install @usearete/react @usearete/sdk zod# Wallet Standard signing:npm install @usearete/adapter-kit @solana/kit \ @wallet-standard/react @wallet-standard/ui-registry@usearete/adapter-kit and the Wallet Standard packages are optional peers of
@usearete/react. Only the @usearete/react/wallet-standard entry point needs
them. Keep @usearete/react, @usearete/sdk, and @usearete/adapter-kit on
the same release.
Define the session
Section titled “Define the session”Install the packages the app uses (Install a stack, Integrate a protocol), then name each one in a session definition. Declare it at module level, so its identity never changes between renders:
import { ORE_STREAM_STACK } from "./generated/ore-stack";import { SQUADS_V4_PROGRAM } from "./generated/squads-v4";import TOKEN_RAISE from "./generated/typescript/protocols/token-raise/token-raise";
// Module level, so its identity never changes between renders.export const APP = { stacks: { ore: ORE_STREAM_STACK }, programs: { squads: SQUADS_V4_PROGRAM }, protocols: { raise: TOKEN_RAISE },};The imports and keys are examples. Use the generated imports for the packages
you installed. The keys become session.stacks.ore, session.programs.squads,
and session.protocols.raise. Programs that a stack packages, such as ORE’s
ore program, are also promoted to session.programs. A protocol’s own stacks
and programs join session.stacks and session.programs too. See
Multiple stacks and programs
for what happens when two of them claim one key.
Providers
Section titled “Providers”AreteProvider supplies auth and fetch. AreteSessionProvider opens the
session with them and gives the session its wallet:
import { useState } from "react";import type { UiWalletAccount } from "@wallet-standard/react";import { AreteProvider, AreteSessionProvider } from "@usearete/react";import { useWalletStandardWallet } from "@usearete/react/wallet-standard";import { APP } from "./arete-session";import { ClaimSol } from "./ClaimSol";import { LatestRound } from "./LatestRound";import { MinerAccount } from "./MinerAccount";import { RaisePanel } from "./RaisePanel";import { SessionBanner } from "./SessionBanner";import { WalletPicker, WalletProblem } from "./WalletPicker";
const publishableKey = import.meta.env.VITE_ARETE_PUBLISHABLE_KEY;if (!publishableKey) throw new Error("VITE_ARETE_PUBLISHABLE_KEY is required");const auth = { publishableKey };
export function App({ raiseId }: { raiseId: string }) { const [account, setAccount] = useState<UiWalletAccount | null>(null); const { wallet, address, isPromptOpen, error } = useWalletStandardWallet( account, { chain: "solana:mainnet" }, );
return ( <AreteProvider auth={auth}> <AreteSessionProvider definition={APP} wallet={wallet}> <WalletPicker onSelect={setAccount} /> {error && <WalletProblem error={error} />} {isPromptOpen && <p>Approve the request in your wallet.</p>} <SessionBanner /> <LatestRound /> <MinerAccount authority={address} /> <ClaimSol wallet={wallet} authority={address} /> <RaisePanel instanceId={raiseId} wallet={address} /> </AreteSessionProvider> </AreteProvider> );}Browser code authenticates with an origin-bound publishable key; see Connect for how to create one.
How the session provider behaves:
- It owns the session. It opens the session when it mounts and closes it when it unmounts. Under React StrictMode in development it opens, closes, and reopens once, like any effect.
- Auth comes from
AreteProvider.options.authandoptions.fetchdefault to the enclosingAreteProvider’s, so oneauthdrives both. Passoptions(thecreateSessionoptions withoutwallet) for anything else, such as per-member overrides.AreteProvideris optional: without one, putauthinoptions. - A change reopens it. A new definition or new options close the old
session at once and open a new one.
sessionisnulluntil the new one is open. Options are compared by shape two levels deep, so an inlineoptions={{ auth: { publishableKey } }}does not reopen it on every render. Functions such asgetTokenandfetchcompare by identity, so keep them stable. - A shared session. Pass
session={existing}instead ofdefinitionto share a session the app opened. The provider never closes it, and sets the wallet on it only when you pass thewalletprop.
The wallet rules
Section titled “The wallet rules”- The session provider’s
walletprop is the session’s wallet. Changing it callssession.setWallet; it never reopens the session. AreteProvider’swalletprop does not reach session members. It is only for stack clients thatAreteProviderconnects itself, throughuseArete(stack). Set the session’s wallet onAreteSessionProvider.- Generated mutation hooks on a session stack sign with the session’s wallet.
useOperationMutationsigns only with the wallet you pass it. Without one, a run fails withnot-readybefore anything is prepared. SetallowExecutorWallet: trueto sign with the session’s wallet instead.
Connect a Wallet Standard wallet
Section titled “Connect a Wallet Standard wallet”useWalletStandardWallet(account, { chain }) turns the account a user
connected with @wallet-standard/react into the wallet Arete signs with. It is
sign-only: the wallet signs with solana:signTransaction, and Arete submits
the signed bytes through the session’s transaction transport. Wallets that
implement Wallet Standard register themselves, so the app never imports
wallet-specific adapters:
import { useConnect, useWallets, type UiWallet, type UiWalletAccount,} from "@wallet-standard/react";import type { WalletStandardSignerError } from "@usearete/react/wallet-standard";
export function WalletPicker({ onSelect,}: { onSelect: (account: UiWalletAccount) => void;}) { const wallets = useWallets().filter((wallet) => wallet.chains.includes("solana:mainnet"), ); return ( <ul> {wallets.map((wallet) => ( <li key={wallet.name}> <ConnectButton wallet={wallet} onSelect={onSelect} /> </li> ))} </ul> );}
function ConnectButton({ wallet, onSelect,}: { wallet: UiWallet; onSelect: (account: UiWalletAccount) => void;}) { const [isConnecting, connect] = useConnect(wallet); const choose = async () => { const [account] = await connect(); if (account) onSelect(account); }; return ( <button disabled={isConnecting} onClick={() => void choose()}> {wallet.name} </button> );}
export function WalletProblem({ error }: { error: WalletStandardSignerError }) { switch (error.code) { case "sign_transaction_unsupported": return <p>This wallet can't sign without sending. Choose another wallet.</p>; case "chain_unsupported": return <p>Switch your wallet to Solana mainnet.</p>; case "account_unavailable": return <p>The wallet account is no longer available. Connect it again.</p>; default: return <p>{error.message}</p>; }}The hook returns:
wallet: the sign-only adapter. It isundefineduntil an account is connected, and when the account cannot sign for Arete. It stays the same object while the account, chain, and transport stay the same.address: the connected account’s address, ornull.isPromptOpen: true while the wallet’s approval prompt is open for the current account.error: why the account cannot sign, as aWalletStandardSignerError.
error.code | Meaning |
|---|---|
sign_transaction_unsupported | The wallet lacks solana:signTransaction; it only signs and sends in one step. |
chain_unsupported | The account does not list chain, for example a devnet account. |
account_unavailable | The account could not be read, or its wallet is no longer registered. |
A declined prompt is not an error here. It fails that run with
wallet-rejected, and the wallet stays usable. useWalletStandardWallet also
accepts a raw { wallet, account } pair from the Wallet Standard registry.
Outside React, createWalletStandardWalletAdapter from @usearete/adapter-kit
does the same; see the
adapter README.
Session status
Section titled “Session status”useAreteSession() returns { session, status, error, ready, retry }:
statusisopening,open, orerror.sessionisnulluntil the session is open, after a failure, and while it reopens.ready()resolves with the session once it is open, waiting through a reopen, and rejects when opening fails. If the provider unmounts before the session opens, it rejects with anAreteSessionUnavailableError(code: "provider_unmounted"). StrictMode’s extra mount doesn’t count as an unmount. It is stable, so it is safe in callbacks and dependency arrays.retry()opens the session again after a failure.
import { useAreteSession } from "@usearete/react";
export function SessionBanner() { const { status, error, retry } = useAreteSession(); if (status === "opening") return <p>Connecting…</p>; if (status === "error") { return ( <p role="alert"> Could not connect: {error?.message}{" "} <button onClick={retry}>Try again</button> </p> ); } return null;}Pass the definition type for typed members: useAreteSession<typeof APP>().
useAreteSession throws outside an AreteSessionProvider.
Live views from a session stack
Section titled “Live views from a session stack”Pass the stack member to useArete. While the session opens, the member is
undefined, so pass the stack definition as stack too. It supplies the
placeholder views, programs, addresses, and constants until the member exists:
import { useArete, useAreteSession } from "@usearete/react";import { APP } from "./arete-session";
export function LatestRound() { const { session } = useAreteSession<typeof APP>(); // `session` is null while it opens; `stack` supplies the placeholders. const ore = useArete(session?.stacks.ore, { stack: APP.stacks.ore }); const round = ore.views.OreRound.latest.useOne();
if (ore.status === "error") { return <button onClick={() => void ore.retry()}>Reconnect</button>; } if (round.status !== "ready") return <p>Loading the round…</p>; if (round.isEmpty) return <p>No round yet.</p>; return <p>Round {round.data.id.roundId?.toString() ?? "unavailable"}</p>;}The result is the same as useArete(stack)’s: views, programs, read,
queries, chain, status, and retry(). The differences:
- One connection. The hooks use the member’s own connection and store.
An enclosing
AreteProvideropens nothing for it. - The session owns it. Unmounting never disconnects the member; closing the
session does. If the session fails to open,
statusis'error'andretry()reopens the session. - A reopen gives a new member. The hooks drop the old one and subscribe on the new one.
- No connection options. The hooks read the member’s
stackandtransport. Generated mutation hooks wait for the streamed slot they landed in only when the member’stransportis'ws'. To connect a member over HTTP, settransportin the session options. TypeScript requiresstackwhenever the member can benullorundefined, and leaves it optional for a member that always exists. - Attached programs need placeholders too. Programs attached to a member
through the session options (
stacks.ore.programs) are part of the member, but not of its stack definition. Pass them asprogramsso their addresses, PDAs and constants work while the session opens:useArete(session?.stacks.ore, { stack: APP.stacks.ore, programs: { memo } }). TypeScript asks for every attached program thatstacklacks.
Program reads with useAsyncRead
Section titled “Program reads with useAsyncRead”Account readers such as accounts.Miner.fetch are async functions, not hooks.
useAsyncRead(key, read, options) runs one for React: it reruns when the key
changes, never shows data from a previous key, and aborts a superseded request.
Read from session.programs, which holds standalone programs and the programs
that stacks package:
import { useAreteSession, useAsyncRead } from "@usearete/react";import { APP } from "./arete-session";
export function MinerAccount({ authority }: { authority: string | null }) { const { session } = useAreteSession<typeof APP>(); const ore = session?.programs.ore;
const miner = useAsyncRead( ["ore-miner", authority], async () => { if (!ore || !authority) return null; return ore.accounts.Miner.fetch(ore.addresses.miner(authority)); }, { enabled: Boolean(ore && authority), disabledStatus: ore ? "disabled" : "connecting", }, );
if (!authority) return <p>Connect a wallet to see your miner.</p>; if (miner.status === "error") return <p role="alert">{miner.error?.message}</p>; if (miner.isPending) return <p>Loading your miner…</p>; if (miner.isEmpty) return <p>No miner account yet.</p>; return <p>Unclaimed SOL (lamports): {miner.data?.rewardsSol.toString()}</p>;}enabled holds the read until the session is open and a wallet is connected.
disabledStatus: "connecting" reports a read that waits for the session as
connecting rather than disabled. The result has data, error, status,
isPending, isReady, isEmpty, and refresh(); call refresh() after a
transaction changes the account. Stack extension reads already have hooks:
ore.read.<name>.use(...) on the useArete result. See
Program Read for the read surface.
Transactions with useOperationMutation
Section titled “Transactions with useOperationMutation”useOperationMutation(prepare, options) runs any prepared instruction,
transaction, or flow with session.execute, and reports where the run is.
Inside an AreteSessionProvider it runs on the provider’s session, waiting
for it to open, so executor can be left out. Use ready() inside prepare
to reach the open session:
import { useAreteSession, useOperationMutation, type OperationMutationError, type WalletAdapter,} from "@usearete/react";import { APP } from "./arete-session";
export function ClaimSol({ wallet, authority,}: { wallet: WalletAdapter | undefined; authority: string | null;}) { const { ready } = useAreteSession<typeof APP>(); const claim = useOperationMutation( async (input: { authority: string }) => { const session = await ready(); return session.programs.ore.instructions.rewards.claimSol.prepare(input); }, { wallet }, );
if (!authority) return null; return ( <section> <button disabled={!wallet || claim.isBusy} onClick={() => claim.mutate({ authority })} > Claim SOL </button> {claim.isAwaitingWallet && <p>Approve the request in your wallet.</p>} {claim.status === "sending" && <p>Sending…</p>} {claim.status === "confirming" && <p>Confirming…</p>} {claim.status === "done" && <p>Claimed in {claim.signatures[0]}.</p>} {claim.error && <p role="alert">{describe(claim.error)}</p>} </section> );}
function describe(error: OperationMutationError): string { switch (error.kind) { case "not-ready": return "Connect a wallet first."; case "wallet-rejected": return "You declined the request."; case "wallet-feature-unsupported": return "This wallet can't sign without sending. Choose another wallet."; case "wallet-chain-unsupported": return "Switch your wallet to Solana mainnet."; case "wallet-version-unsupported": return "This wallet doesn't support this transaction version."; case "wallet-modified": return "The wallet changed the transaction, so it was not sent."; case "submitted-unknown": return `Sent, but not confirmed yet. Check ${error.signature} before trying again.`; case "chain-failed": return error.programError?.message ?? "The transaction failed on chain."; default: return error.message; }}The wallet is the one useWalletStandardWallet returned, passed down from
App. A run can also take one: claim.run(input, { wallet }).
status walks idle → preparing → signing → sending → confirming →
done, or ends in error. A flow walks signing → sending → confirming
once per transaction (transactionIndex of transactionCount). The
@usearete/adapter-kit adapters report sending and confirming; other
wallets stay signing until the transaction confirms. isAwaitingWallet is
true while a prompt can be open, and isBusy while a run is in progress. A
second run while one is in progress fails with busy, so a double click never
opens two prompts. A run ends when its status becomes done or error,
before onSuccess or onError is awaited, so isBusy and busy always
agree: while an async callback runs, a button disabled by isBusy is enabled
and a click starts a new run. Each hook follows only its own transactions, so
several hooks can share one wallet without moving each other’s status.
mutate() keeps the failure in error. run() returns the receipt and
rejects with the same error, for code that awaits the result. When prepare
creates signers, such as a create key, return { operation, signers }.
error.kind | Meaning |
|---|---|
busy | A run of this hook is already in progress; nothing was started. |
not-ready | No session or no wallet yet; nothing was prepared or signed. |
prepare-failed | prepare threw; nothing was signed. |
wallet-rejected | The user declined or closed the wallet prompt. |
wallet-feature-unsupported | The wallet cannot sign without sending (no solana:signTransaction). |
wallet-chain-unsupported | The wallet does not sign for the chain it was asked to. |
wallet-version-unsupported | The wallet signs, but not this transaction version. |
wallet-modified | The wallet changed the transaction, so it was not sent. |
not-submitted | Failed before anything reached the chain. |
submitted-unknown | Sent, but the confirmation did not arrive; check the chain before a retry. |
chain-failed | Failed on chain; see programError. |
The error also carries failure, signature, programError, and
completedSignatures, the signatures of a flow’s transactions that confirmed
before it failed. toOperationMutationError(error) classifies a failure from
an imperative session.execute the same way.
Generated mutation hooks on a session stack
Section titled “Generated mutation hooks on a session stack”useArete(session?.stacks.ore, { stack }) also returns the stack’s generated
.useMutation() hooks, for example
ore.programs.ore.transactions.rewards.claimSolWithCheckpoint.useMutation().
They sign with the session’s wallet and reconcile with the live views after
confirmation, as described in Mutations. Use them for
stack operations whose views should refresh. Use useOperationMutation for
standalone programs, flows, operations that create signers, or when you want
the typed error kinds above.
Protocols in the session
Section titled “Protocols in the session”Protocol hooks take a protocol client, and session.protocols.<key> is one. It
is undefined while the session opens, and the hooks wait until it exists:
import { useAllowedSteps, useAreteSession, useProtocolInstance,} from "@usearete/react";import { APP } from "./arete-session";
export function RaisePanel({ instanceId, wallet,}: { instanceId: string; wallet: string | null;}) { const { session } = useAreteSession<typeof APP>(); // Undefined while the session opens; the hooks wait for it. const raise = session?.protocols.raise; const { instance, live, isLoading, error } = useProtocolInstance( raise, instanceId, { wallet }, ); const steps = useAllowedSteps( raise, { instance: instanceId, wallet }, { refreshOnEvents: true }, );
if (error) return <p role="alert">{error.message}</p>; if (!raise || isLoading) return <p>Loading the raise…</p>; return ( <section> <p>State: {instance?.state}</p> {live !== undefined && <p>Live data connected.</p>} {steps.error ? ( <p role="alert">Could not load actions: {steps.error.message}</p> ) : ( <ul> {steps.data?.map((step) => ( <li key={step.step}> {step.step}: {step.allowed ? "available" : step.reason} </li> ))} </ul> )} </section> );}In a session, live holds the protocol stack’s row, because the session
connects that stack. Protocol steps run through the protocol client’s own
prepare, sign, and submit flow, not useOperationMutation; see
Execute and track a step. A
publishable-key session has only the protocol’s public steps. See
Build a protocol interface with React for
the other protocol hooks.
Next steps
Section titled “Next steps”- React SDK:
useArete, views, reads, and generated mutations - TypeScript SDK:
createSessionoptions and the session surface - Transactions: transports, inspection, and failure outcomes
- React SDK README: the full hook reference