Wallets
One page for everything wallet-related in Caatinga: the adapter contract, the bundled Freighter and Stellar Wallets Kit adapters, the framework-agnostic wallet session, the React hooks, and the bundler workarounds Stellar Wallets Kit needs.
The adapter contract
Everything builds on one minimal interface:
export interface CaatingaWalletAdapter {
getPublicKey(): Promise<string>;
signTransaction(input: { xdr: string; networkPassphrase: string }): Promise<string>;
}Rules every adapter must follow:
- Reject on dismissal:
getPublicKeyandsignTransactionmust reject when the user cancels or dismisses the wallet UI. Never leave the promise pending indefinitely. - Adapter timeouts: an adapter may apply its own timeout before rejecting.
- Caatinga timeout: Caatinga imposes no default timeout. Pass
walletTimeout(milliseconds) onCaatingaClientConfig— ortimeoutoncreateWalletSession— to cap wallet calls; on expiry the client throwsCAATINGA_WALLET_TIMEOUT.
Bundled adapters
Stellar Wallets Kit (multi-wallet)
“Multi-wallet” here means choosing among wallet providers (Freighter, xBull, etc.) via Stellar Wallets Kit — not Soroban multi-invoker / signAuthEntry orchestration. Browser invoke is single-invoker only until v1.0 (Client scope).
npm install @caatinga/client @creit.tech/stellar-wallets-kitimport {
createStellarWalletsKitAdapter,
WalletNetwork,
} from "@caatinga/client/stellar-wallets-kit";
export const stellarWalletAdapter = createStellarWalletsKitAdapter({
network: WalletNetwork.TESTNET,
});The adapter wraps SWK 2.x static methods and adds:
openModal()— opens the wallet-selection modal (only installed/available wallets), sets the chosen wallet active, resolves with the connected address, rejects on dismissal.setWallet(walletId)/getWalletId()— select a wallet programmatically / read the currently selected wallet id (undefinedbefore any selection).getSupportedWallets()anddisconnect().kit— exposes the underlying staticStellarWalletsKitclass for advanced usage.- Optional WalletConnect support via
walletConnectMetadata(needs a WalletConnectprojectId).
Exported types from @caatinga/client/stellar-wallets-kit:
| Type | Purpose |
|---|---|
StellarWalletsKitAdapter | Return type of createStellarWalletsKitAdapter |
StellarWalletsKitAdapterOptions | Options for adapter creation (network, metadata) |
StellarWalletsKitMetadata | WalletConnect metadata shape |
StellarWalletsKitOpenModalOptions | Options for openModal() |
WalletNetwork | Enum of supported network passphrases |
Freighter (single wallet)
npm install @caatinga/client @stellar/freighter-apiimport { freighterWalletAdapter } from "@caatinga/client/freighter";Both peer dependencies are optional: install only the one your app uses.
Wallet session (framework-agnostic)
createWalletSession wraps any adapter with connection state, events, and optional persistence. It works in plain TypeScript, Vue, Svelte, or React — no framework required.
import { createWalletSession } from "@caatinga/client";
import { stellarWalletAdapter } from "./wallet.js";
const session = createWalletSession(stellarWalletAdapter, { persist: true });
const unsubscribe = session.subscribe(() => {
console.log(session.getState()); // { status, publicKey, error }
});
await session.connect(); // modal when available, else getPublicKey()
await session.restore(); // silent reconnect from persisted state
await session.disconnect(); // resets state and clears persistenceStates: disconnected → connecting → connected. A failed connect() returns to disconnected with error set and rethrows; UI can use either signal.
Persistence and restore
With persist: true, a successful connect stores { v: 1, walletId? } under the caatinga:wallet-session localStorage key (injectable via storage/storageKey options). restore() then reconnects silently on page load:
- resolves the public key on success;
- resolves
nullwhen nothing was persisted or reconnection failed; - never rejects and never sets
state.error— no error toast on page load; - clears stale persistence after a failed attempt.
Capabilities
The session detects optional adapter methods and uses them when present:
export interface CaatingaWalletCapabilities {
openModal?(): Promise<string>; // connect() prefers the modal
disconnect?(): Promise<void>; // disconnect() calls through
setWallet?(walletId: string): void; // restore() re-selects the persisted wallet
getWalletId?(): string | undefined; // persisted so restore can re-select
}The Stellar Wallets Kit adapter implements all four. A minimal custom adapter implements none and still works — connect() falls back to getPublicKey().
React hooks (@caatinga/client/react)
The react subpath ships WalletProvider + useWallet so apps stop hand-rolling a wallet context. React >=18 is an optional peer dependency — non-React consumers pull nothing extra.
import { WalletProvider, useWallet } from "@caatinga/client/react";
import { stellarWalletAdapter } from "./wallet.js";
export default function App() {
return (
<WalletProvider adapter={stellarWalletAdapter} options={{ persist: true }}>
<Body />
</WalletProvider>
);
}
function Body() {
const { publicKey, connected, connecting, error, connect, disconnect } = useWallet();
if (connecting) return <p>Connecting…</p>;
if (!connected) {
return (
<>
<button onClick={() => void connect().catch(() => {})}>Connect</button>
{error ? <p role="alert">{error.message}</p> : null}
</>
);
}
return <button onClick={() => void disconnect()}>{publicKey}</button>;
}useWallet()returns{ status, publicKey, connected, connecting, error, connect, disconnect, session }backed byuseSyncExternalStore— no tearing, no manual subscriptions.WalletProvideraccepts eitheradapter(it creates the session) or a pre-builtsessionshared with non-React code.autoConnectdefaults to true whenpersistis enabled: the provider runssession.restore()once on mount, silently.useWalletSession()exposes the underlying session for advanced flows.
The react-vite-counter template and examples/counter-web both use this provider.
Custom adapters
Implement the two-method contract and pass the object anywhere an adapter is accepted:
import type { CaatingaWalletAdapter } from "@caatinga/client";
export const myAdapter: CaatingaWalletAdapter = {
async getPublicKey() {
return myWallet.requestAccess(); // must reject on user dismissal
},
async signTransaction({ xdr, networkPassphrase }) {
return myWallet.sign(xdr, networkPassphrase); // resolve signed XDR
},
};Add any of the capability methods above to opt into modal connect, disconnect, or wallet-id persistence in sessions and hooks.
Stellar Wallets Kit bundler workarounds
SWK pulls optional wallet SDKs that misbehave in browser bundles. The official react-vite-counter template ships these workarounds preconfigured; copy them if you wire SWK into your own app.
Adding SWK to a custom Vite app
@caatinga/client/vite exports reusable helpers so you do not copy 15+ lines of overrides by hand:
import {
walletStubViteAliases,
walletStubOverrides,
walletStubPnpmWorkspaceYaml,
} from "@caatinga/client/vite";
import { fileURLToPath } from "node:url";
const stubsDir = fileURLToPath(new URL("./src/stubs", import.meta.url));
// vite.config.ts
export default defineConfig({
resolve: { alias: walletStubViteAliases(stubsDir) },
});
// package.json → overrides (npm)
// walletStubOverrides("./src/stubs")
// pnpm-workspace.yaml
// walletStubPnpmWorkspaceYaml()Copy the stub files from react-vite-counter/src/stubs/ (hot-wallet.ts, empty-wallet-dep/, hot-wallet-sdk/). Projects created with caatinga init --minimal do not need wallet stubs until you add @creit.tech/stellar-wallets-kit.
HOT Wallet stub
SWK's HOT Wallet module (NEAR-based) pulls @hot-wallet/sdk → @near-js/crypto → randombytes, which references the Node global and breaks in the browser. The Caatinga adapter already filters the module out of the wallet list; additionally alias @hot-wallet/sdk to a stub in your bundler so the NEAR chain is never bundled:
// vite.config.ts
resolve: {
alias: {
"@hot-wallet/sdk": path.resolve(__dirname, "src/stubs/hot-wallet.ts")
}
}Trezor / HOT npm overrides
SWK lists @trezor/connect-web and @hot-wallet/sdk as direct dependencies but does not register them in defaultModules(). Replace them with local stubs to avoid critical protobufjs advisories (Trezor) and NEAR/elliptic noise (HOT):
- npm:
overridesinpackage.jsonpointing at stub packages (seepackages/templates/react-vite-counter/src/stubs/). - pnpm:
"-"path overrides inpnpm-workspace.yaml.
Freighter, LOBSTR, WalletConnect, and the other modules are unaffected. Caatinga also filters Trezor and HOT out of the wallet modal (createStellarWalletsKitAdapter()).
ws npm override (Reown / viem audit findings)
SWK 2.x → Reown AppKit → viem → ws. Versions of ws below 8.21.0 trigger high-severity DoS advisories (GHSA-96hv-2xvq-fx4p). npm audit typically reports ~14 duplicated findings across Reown packages — this is not Trezor/protobufjs.
Pin at the top level of npm overrides (and in pnpm-workspace.yaml):
"overrides": {
"ws": "^8.21.0"
}Use walletStubOverrides() from @caatinga/client/vite so the pin stays aligned with official templates. Do not remove without running npm audit on a scaffolded project and updating scripts/consumer-isolation-test.sh. Details: Templates — Install override contract.
Safe / uuid overrides
Reown AppKit (transitive via SWK) pulls optional EVM/Safe packages and a deprecated uuid@8. The template blocks them with ignoredOptionalDependencies + overrides (pnpm) and nested npm overrides. Details and the exact YAML block: Templates.
Monorepo .pnpmfile.cjs
This repository's own .pnpmfile.cjs strips the same unused SWK dependencies at install time so workspace installs stay clean. App projects do not need it — the template overrides above cover the generated-app case.