Complete documentation for integrating Stellar wallets into the Predictify frontend using the useWallet hook and wallet context layer.
- Overview
- Supported Wallets
- Architecture
- Network Configuration
- API Reference
- Usage Examples
- Persistence and Reconnect
- Known Limitations
- Troubleshooting
The wallet layer provides a streamlined interface for connecting to Stellar wallets and signing transactions. It consists of three core components:
getKit()(constants/wallet-kits.constant.ts) - Initializes and manages theStellarWalletsKitsingleton instance with SSR safetyuseWallethook (hooks/useWallet.hook.ts) - React hook exposing wallet connection, disconnection, and transaction signingWalletContext(context/WalletContext.tsx) - React context that manages wallet state persistence in localStorage and automatic reconnection on mount
The wallet layer supports the following Stellar wallets via @creit.tech/stellar-wallets-kit:
| Wallet | ID | Notes |
|---|---|---|
| Freighter | freighter |
Default wallet, requires Freighter browser extension |
| LOBSTR | lobstr |
LOBSTR wallet support |
| XBull | xbull |
XBull wallet support |
| Albedo | albedo |
Albedo wallet support |
| Rabet | rabet |
Rabet wallet support |
The wallet ID constants are imported from @creit.tech/stellar-wallets-kit:
import {
FREIGHTER_ID,
LOBSTR_ID,
XBULL_ID,
ALBEDO_ID,
RABET_ID,
} from "@creit.tech/stellar-wallets-kit";Component
↓ (uses)
useWallet Hook
↓ (reads state from & calls methods on)
WalletContext
↓ (uses)
getKit() / StellarWalletsKit
↓
User's Wallet (Browser Extension)
The getKit() function in constants/wallet-kits.constant.ts initializes the StellarWalletsKit singleton on the client side only:
import { StellarWalletsKit, WalletNetwork, allowAllModules } from "@creit.tech/stellar-wallets-kit";
import { getClientConfig } from "@/lib/config";
let kit: StellarWalletsKit | null = null;
export const getKit = (): StellarWalletsKit => {
if (typeof window === 'undefined') {
throw new Error('StellarWalletsKit can only be used on the client side');
}
if (!kit) {
const config = getClientConfig();
const network = config.stellar.network === 'mainnet'
? WalletNetwork.MAINNET
: WalletNetwork.TESTNET;
kit = new StellarWalletsKit({
network,
selectedWalletId: FREIGHTER_ID,
modules: allowAllModules(),
});
}
return kit;
};Key Points:
- Throws an error if called on the server (SSR guard)
- Lazy initializes the kit singleton on first client-side access
- Automatically selects the network based on
NEXT_PUBLIC_STELLAR_NETWORKenvironment variable - Enables all wallet modules via
allowAllModules()
Network configuration is controlled via the NEXT_PUBLIC_STELLAR_NETWORK environment variable:
# In your .env.local or deployment environment
NEXT_PUBLIC_STELLAR_NETWORK=testnet # or 'mainnet'The network is read and validated in lib/config.ts:
type Network = 'testnet' | 'mainnet';
function getStellarNetwork(): Network {
const network = env.NEXT_PUBLIC_STELLAR_NETWORK.toLowerCase();
if (network === 'mainnet' || network === 'testnet') {
return network as Network;
}
// Default to testnet for safety
return 'testnet';
}
export function getClientConfig() {
const network = env.NEXT_PUBLIC_STELLAR_NETWORK.toLowerCase();
return {
stellar: {
network: (network === 'mainnet' || network === 'testnet'
? network
: 'testnet') as Network,
},
};
}The network setting is used by getKit() to initialize the wallet kit with the appropriate WalletNetwork value.
The useWallet hook provides the main interface for wallet interactions. It must be called from a component within a WalletProvider.
interface UseWalletReturn {
// Methods
connectWallet: (walletId: string) => Promise<ConnectResult>;
disconnectWallet: () => Promise<DisconnectResult>;
signTransaction: (xdr: string) => Promise<SignResult>;
// State
isConnecting: boolean;
error: string | null;
isConnected: boolean;
walletAddress: string | null;
walletName: string | null;
}Initiates connection to a wallet.
Parameters:
walletId: One ofFREIGHTER_ID,LOBSTR_ID,XBULL_ID,ALBEDO_ID, orRABET_ID
Returns:
type ConnectResult =
| { success: true; address: string }
| { success: false; error: string };Example:
const result = await connectWallet(FREIGHTER_ID);
if (result.success) {
console.log('Connected wallet address:', result.address);
} else {
console.error('Connection failed:', result.error);
}Side Effects:
- Updates
WalletContextstate (address, name, connected) - Persists wallet state to
localStorageunder key'predictify_wallet_state' - Sets
isConnectingtotrueduring connection,falseafter
Disconnects the currently connected wallet.
Returns:
type DisconnectResult =
| { success: true }
| { success: false; error: string };Example:
const result = await disconnectWallet();
if (result.success) {
console.log('Wallet disconnected');
} else {
console.error('Disconnection failed:', result.error);
}Side Effects:
- Clears wallet state in
WalletContext - Removes wallet state from
localStorage - Calls the underlying
kit.disconnect()
Signs a transaction for the connected wallet.
Parameters:
xdr: The Stellar transaction envelope in XDR format
Returns:
type SignResult =
| { success: true; signedTxXdr: string }
| { success: false; error: string };Example:
const result = await signTransaction(txXdr);
if (result.success) {
console.log('Transaction signed:', result.signedTxXdr);
// Now you can submit to the Stellar network
} else {
console.error('Signing failed:', result.error);
}Requirements:
- Must have an active wallet connection (
isConnected === true) - Throws error:
"No wallet connected"if called without a connected wallet
- Currently hardcodes
WalletNetwork.TESTNETas the network passphrase, regardless of the configured network - This will be fixed in a future release to use the network configuration from
getClientConfig()
All state properties are reactive and update automatically:
isConnecting(boolean) -truewhile a connection request is in progresserror(string | null) - Most recent error message, ornullif no errorisConnected(boolean) -trueif a wallet is currently connectedwalletAddress(string | null) - Stellar public address of connected wallet, ornullwalletName(string | null) - Human-readable name of connected wallet (e.g., "Freighter"), ornull
"use client";
import { useWallet } from "@/hooks/useWallet.hook";
import { FREIGHTER_ID } from "@creit.tech/stellar-wallets-kit";
import { Button } from "@/components/ui/button";
export function WalletConnectButton() {
const { connectWallet, disconnectWallet, isConnected, walletName, isConnecting } = useWallet();
const handleConnect = async () => {
const result = await connectWallet(FREIGHTER_ID);
if (!result.success) {
alert(`Connection failed: ${result.error}`);
}
};
const handleDisconnect = async () => {
const result = await disconnectWallet();
if (!result.success) {
alert(`Disconnection failed: ${result.error}`);
}
};
return (
<div>
{isConnected ? (
<div>
<p>Connected: {walletName}</p>
<Button onClick={handleDisconnect}>Disconnect</Button>
</div>
) : (
<Button onClick={handleConnect} disabled={isConnecting}>
{isConnecting ? "Connecting..." : "Connect Wallet"}
</Button>
)}
</div>
);
}"use client";
import { useWallet } from "@/hooks/useWallet.hook";
import { FREIGHTER_ID, LOBSTR_ID, XBULL_ID, ALBEDO_ID, RABET_ID } from "@creit.tech/stellar-wallets-kit";
import { Button } from "@/components/ui/button";
const WALLET_OPTIONS = [
{ id: FREIGHTER_ID, name: "Freighter" },
{ id: LOBSTR_ID, name: "LOBSTR" },
{ id: XBULL_ID, name: "XBull" },
{ id: ALBEDO_ID, name: "Albedo" },
{ id: RABET_ID, name: "Rabet" },
];
export function WalletSelector() {
const { connectWallet, isConnecting, error } = useWallet();
const handleSelectWallet = async (walletId: string) => {
const result = await connectWallet(walletId);
if (!result.success) {
console.error(`Failed to connect: ${result.error}`);
}
};
return (
<div>
{error && <div className="text-red-600">{error}</div>}
<div className="grid grid-cols-2 gap-2">
{WALLET_OPTIONS.map((wallet) => (
<Button
key={wallet.id}
onClick={() => handleSelectWallet(wallet.id)}
disabled={isConnecting}
>
{wallet.name}
</Button>
))}
</div>
</div>
);
}"use client";
import { useWallet } from "@/hooks/useWallet.hook";
import { Button } from "@/components/ui/button";
export function SignTransactionButton({ txXdr }: { txXdr: string }) {
const { signTransaction, isConnected, error } = useWallet();
const handleSign = async () => {
const result = await signTransaction(txXdr);
if (result.success) {
console.log("Transaction signed:", result.signedTxXdr);
// Next step: submit to Stellar network (not yet implemented)
} else {
console.error("Signing failed:", result.error);
}
};
return (
<div>
{error && <div className="text-red-600">{error}</div>}
<Button onClick={handleSign} disabled={!isConnected}>
Sign Transaction
</Button>
</div>
);
}Wrap your app or page with WalletProvider:
import { WalletProvider } from "@/context/WalletContext";
import WalletConnectButton from "./wallet-connect-button";
export default function App() {
return (
<WalletProvider>
<WalletConnectButton />
</WalletProvider>
);
}The WalletContext manages wallet state persistence and automatic reconnection:
- On Mount: The context component reads the stored wallet state from
localStorageunder the key'predictify_wallet_state' - On Connection/Disconnection: State changes are immediately persisted to
localStorage - Automatic Reconnect: On subsequent page loads, if a previous connection was stored, the wallet state is restored automatically
The wallet state is stored as JSON in localStorage:
{
"address": "GXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
"name": "Freighter",
"connected": true
}From context/WalletContext.tsx:
const WALLET_STORAGE_KEY = 'predictify_wallet_state';
// Load on mount
useEffect(() => {
try {
const savedState = localStorage.getItem(WALLET_STORAGE_KEY);
if (savedState) {
const { address, name, connected } = JSON.parse(savedState);
if (connected && address && name) {
setAddress(address);
setName(name);
setConnected(true);
}
}
} catch (error) {
console.error('Error loading wallet state:', error);
} finally {
setIsLoading(false);
}
}, []);
// Save on change
useEffect(() => {
if (!isLoading) {
const state = { address, name, connected };
localStorage.setItem(WALLET_STORAGE_KEY, JSON.stringify(state));
}
}, [address, name, connected, isLoading]);- Users stay logged in after page refresh
- Connection state persists across browser sessions
- Disconnection clears all stored state
- Loading state (
isLoading) indicates when the context is initializing from storage
Issue: The signTransaction method currently hardcodes WalletNetwork.TESTNET regardless of the configured network:
const { signedTxXdr } = await kit.signTransaction(xdr, {
address: walletState.address,
networkPassphrase: WalletNetwork.TESTNET, // ← Always testnet
});Impact: Transactions will fail if the app is configured for mainnet, as the network passphrase must match the transaction's network.
Status: This will be fixed in a future release to respect getClientConfig().stellar.network.
Issue: The wallet layer does not provide a method to submit signed transactions to the Stellar network. The signTransaction return shape includes signedTxXdr, but no corresponding broadcast/submit method exists.
Current Approach: Applications must:
- Call
signTransaction(xdr)to get the signed XDR - Manually submit to a Stellar RPC endpoint (e.g., Horizon API)
Future Work: A submitTransaction() method may be added in a future release to streamline this process.
The top of hooks/useWallet.hook.ts contains a commented-out block showing an older, abandoned approach to wallet integration (lines 1-55). This code is not used and serves only as historical reference. It should be removed in a future cleanup.
Cause: You called getKit() or useWallet in a Server Component.
Solution: Ensure the component is marked with "use client":
"use client";
import { useWallet } from "@/hooks/useWallet.hook";
// ...Cause: You used useWallet in a component that is not wrapped by WalletProvider.
Solution: Wrap your component tree with WalletProvider:
import { WalletProvider } from "@/context/WalletContext";
export default function App() {
return (
<WalletProvider>
<YourComponent />
</WalletProvider>
);
}Cause: Likely due to the hardcoded testnet limitation described above.
Solution:
- Verify your app is configured for testnet via
NEXT_PUBLIC_STELLAR_NETWORK=testnet - Check that the transaction XDR was also created for testnet
- Ensure the wallet extension is also connected to testnet
Cause: localStorage may not be enabled in the browser, or consent was denied.
Solution:
- Check that browser storage is allowed
- Verify no privacy/incognito mode is interfering
- Check browser console for
localStorageaccess errors
# Network configuration
NEXT_PUBLIC_STELLAR_NETWORK=testnet# App branding
NEXT_PUBLIC_APP_NAME=Predictify
NEXT_PUBLIC_APP_URL=https://predictify.example.com
# API endpoint
NEXT_PUBLIC_API_URL=https://api.predictify.example.com# Install dependencies
pnpm install
# Run linting
pnpm lint
# Build the project
pnpm build
# Run tests
pnpm test- StellarWalletsKit Documentation: https://github.com/CreitTech/stellar-wallets-kit
- Stellar Documentation: https://developers.stellar.org/
- Hook Implementation:
hooks/useWallet.hook.ts - Context Implementation:
context/WalletContext.tsx - Configuration:
lib/config.ts - Wallet Kit Initialization:
constants/wallet-kits.constant.ts
Last Updated: June 2026
Status: Stable with Known Limitations
Version: 1.0