Rialo Frost
The wallet integration toolkit for Rialo dApps — Connect wallets, sign transactions, and build Web3 apps in minutes.
pnpm add @rialo/frost
30-Second Setup
// 1. Create config (do this once, outside components)
import { createConfig, getDefaultRialoClientConfig } from "@rialo/frost";
export const config = createConfig({
clientConfig: getDefaultRialoClientConfig("devnet"),
});
// 2. Wrap your app
import { FrostProvider, ConnectButton } from "@rialo/frost";
function App() {
return (
<FrostProvider config={config}>
<ConnectButton />
</FrostProvider>
);
}
That's it. You now have a working wallet connection button.
Table of Contents
- New to Web3?
- Installation
- Step-by-Step Guide
- Hooks Reference
- Async/Await with mutateAsync
- Components
- Error Handling
- Advanced Usage
- Troubleshooting
- CSS Custom Properties
New to Web3?
Click here if you're new to blockchain development
What You Need to Know
Wallet = A browser extension (like MetaMask, but for Rialo) that stores the user's private keys and lets them approve transactions.
Account/Address = A public identifier (like 7xKXtg2CW87d97...) that represents a user on the blockchain. One wallet can have multiple accounts.
Signing = When a user cryptographically approves something using their wallet. This proves they own the account without exposing their private key.
Transaction = An operation that changes state on the blockchain (sending tokens, interacting with smart contracts, etc.).
How Frost Fits In
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Your App │ ───► │ Frost │ ───► │ Wallet │
│ │ │ │ │ Extension │
│ - UI │ │ - Discovers │ │ │
│ - Logic │ │ wallets │ │ - Stores │
│ - Hooks │ │ - Manages │ │ keys │
│ │ │ state │ │ - Signs │
└──────────────┘ └──────────────┘ └──────────────┘
Frost handles all the complex wallet communication so you can focus on building your app.
Installation
Requirements
- Node.js 18+
- React 18 or 19
- A Rialo wallet extension (for testing)
Install
# pnpm (recommended)
pnpm add @rialo/frost
# npm
npm install @rialo/frost
# yarn
yarn add @rialo/frost
For Non-React Apps
Use the framework-agnostic core package:
pnpm add @rialo/frost-core
Step-by-Step Guide
Step 1: Create Your Config
Create a file called frost.config.ts in your src folder:
// src/frost.config.ts
import { createConfig, getDefaultRialoClientConfig } from "@rialo/frost";
export const frostConfig = createConfig({
clientConfig: getDefaultRialoClientConfig("devnet"), // Use "mainnet" for production
autoConnect: true, // Reconnect automatically on page refresh
});
Important: Create the config outside of React components. This prevents it from being recreated on every render.
Step 2: Add the Provider
Wrap your app with FrostProvider:
// src/App.tsx
import { FrostProvider } from "@rialo/frost";
import { frostConfig } from "./frost.config";
function App() {
return (
<FrostProvider config={frostConfig}>
<YourApp />
</FrostProvider>
);
}
Step 3: Add a Connect Button
Option A: Use the built-in component
import { ConnectButton } from "@rialo/frost";
function Header() {
return (
<nav>
<h1>My dApp</h1>
<ConnectButton />
</nav>
);
}
Option B: Build your own
import {
useWallets,
useConnectWallet,
useDisconnectWallet,
useIsConnected,
useActiveAccount,
} from "@rialo/frost";
function CustomConnectButton() {
const wallets = useWallets();
const { mutate: connect, isPending } = useConnectWallet();
const { mutate: disconnect } = useDisconnectWallet();
const isConnected = useIsConnected();
const account = useActiveAccount();
if (isConnected && account) {
return (
<div>
<span>{account.address.slice(0, 6)}...{account.address.slice(-4)}</span>
<button onClick={() => disconnect()}>Disconnect</button>
</div>
);
}
return (
<div>
{wallets.map((wallet) => (
<button
key={wallet.name}
onClick={() => connect({ walletName: wallet.name })}
disabled={isPending}
>
{wallet.icon && <img src={wallet.icon} alt="" width={20} />}
Connect {wallet.name}
</button>
))}
</div>
);
}
Step 4: Display Balance
import { useNativeBalance, useIsConnected } from "@rialo/frost";
function Balance() {
const isConnected = useIsConnected();
const { formatted, isLoading, refetch } = useNativeBalance();
if (!isConnected) return null;
return (
<div>
{isLoading ? "Loading..." : `${formatted} RIA`}
<button onClick={() => refetch()}>↻</button>
</div>
);
}
Step 5: Sign Messages
import { useSignMessage, useIsConnected } from "@rialo/frost";
function SignDemo() {
const isConnected = useIsConnected();
const { mutate: signMessage, isPending } = useSignMessage({
onSuccess: (result) => {
console.log("Signature:", result.signature);
},
});
if (!isConnected) return <p>Connect wallet first</p>;
return (
<button
onClick={() => signMessage({ message: "Hello from my dApp!" })}
disabled={isPending}
>
{isPending ? "Signing..." : "Sign Message"}
</button>
);
}
Step 6: Send Transactions
import { useSendTransaction } from "@rialo/frost";
function SendTx({ transaction }: { transaction: Uint8Array }) {
const { mutate: send, isPending, isSuccess, data } = useSendTransaction({
onSuccess: (result) => {
console.log("Transaction sent:", result.signature);
},
onError: (error) => {
console.error("Failed:", error.message);
},
});
return (
<div>
<button onClick={() => send({ transaction })} disabled={isPending}>
{isPending ? "Sending..." : "Send Transaction"}
</button>
{isSuccess && <p>✓ Signature: {data.signature}</p>}
</div>
);
}
Hooks Reference
Connection
| Hook | Returns | Description |
|---|---|---|
useWallets() |
WalletEntity[] |
All discovered wallets |
useWalletsReady() |
boolean |
True when at least one wallet has been discovered |
useConnectWallet() |
Mutation | Connect to a wallet |
useDisconnectWallet() |
Mutation | Disconnect current wallet |
useConnectionStatus() |
ConnectionStatus |
"disconnected" | "connecting" | "connected" | "reconnecting" |
useIsConnected() |
boolean |
True if connected |
Account & Wallet
| Hook | Returns | Description |
|---|---|---|
useActiveWallet() |
WalletEntity | null |
Currently connected wallet |
useActiveAccount() |
AccountEntity | null |
Currently active account |
useAccounts() |
AccountEntity[] |
All accounts from connected wallet |
Chain & Client
| Hook | Returns | Description |
|---|---|---|
useChainId() |
string |
Current chain ID (e.g., "rialo:devnet") |
useSwitchChain() |
(clientConfig: RialoClientConfig) => void |
Switch to a different network |
useClient() |
RialoClient |
Direct RPC client access |
Balance
| Hook | Returns | Description |
|---|---|---|
useNativeBalance() |
{ formatted, balance, isLoading, refetch } |
Native token balance |
Signing & Transactions
| Hook | Description |
|---|---|
useSignMessage() |
Sign arbitrary messages |
useSignTransaction() |
Sign transaction without sending |
useSendTransaction() |
Sign and send via wallet |
useSignAndSendTransaction() |
Sign via wallet, send via Frost's RPC |
Async/Await with mutateAsync
All mutation hooks (useConnectWallet, useSignMessage, useSignTransaction, useSendTransaction, useSignAndSendTransaction, useDisconnectWallet) return both mutate and mutateAsync. Note: useSwitchChain is not a mutation hook — it returns a plain function directly.
Callback Pattern (mutate)
The mutate function is fire-and-forget with callbacks:
const { mutate: send } = useSendTransaction({
onSuccess: (result) => console.log("Sent:", result.signature),
onError: (error) => console.error("Failed:", error),
});
// Fire and forget - no await, no return value
send({ transaction });
Async/Await Pattern (mutateAsync)
The mutateAsync function returns a Promise you can await:
const { mutateAsync: sendAsync } = useSendTransaction();
async function handleSend() {
try {
const result = await sendAsync({ transaction });
console.log("Transaction signature:", result.signature);
// Continue with next steps...
} catch (error) {
console.error("Transaction failed:", error);
}
}
When to Use Each
| Pattern | Use When |
|---|---|
mutate |
Simple fire-and-forget, UI updates via hook state |
mutateAsync |
Sequential operations, custom error handling, chaining actions |
Real-World Example: Sign Then Send
function TransactionFlow() {
const { mutateAsync: signAsync } = useSignTransaction();
const { mutateAsync: sendAsync } = useSendTransaction();
const [status, setStatus] = useState<string>("");
async function handleTransaction(transaction: Uint8Array) {
try {
setStatus("Signing...");
const { signedTransaction } = await signAsync({ transaction });
setStatus("Sending...");
const { signature } = await sendAsync({ transaction: signedTransaction });
setStatus(`Success! Signature: ${signature}`);
} catch (error) {
setStatus(`Error: ${error.message}`);
}
}
return (
<div>
<button onClick={() => handleTransaction(myTx)}>Execute</button>
<p>{status}</p>
</div>
);
}
All Mutation Hooks Support Both Patterns
// Connection
const { mutateAsync: connectAsync } = useConnectWallet();
const result = await connectAsync({ walletName: "Rialo" });
// Signing
const { mutateAsync: signMessageAsync } = useSignMessage();
const { signature } = await signMessageAsync({ message: "Hello" });
// Transactions
const { mutateAsync: signTxAsync } = useSignTransaction();
const { signedTransaction } = await signTxAsync({ transaction });
const { mutateAsync: sendTxAsync } = useSendTransaction();
const { signature } = await sendTxAsync({ transaction });
const { mutateAsync: signAndSendAsync } = useSignAndSendTransaction();
const { signature } = await signAndSendAsync({ transaction });
// Disconnect
const { mutateAsync: disconnectAsync } = useDisconnectWallet();
await disconnectAsync();
// Switch chain
const switchChain = useSwitchChain();
switchChain(getDefaultRialoClientConfig("mainnet"));
Hooks in Detail
useConnectWallet
const {
mutate: connect, // Call to trigger connection
isPending, // True while connecting
isSuccess, // True after successful connection
isError, // True if connection failed
error, // Error object if failed
data, // ConnectResult if successful
} = useConnectWallet({
onSuccess: (result) => {
console.log("Connected:", result.walletName, result.accountAddress);
},
onError: (error) => {
console.error("Failed:", error.message);
},
});
// Usage
connect({ walletName: "Rialo" });
useSignMessage
const { mutate: signMessage, isPending } = useSignMessage({
onSuccess: (result) => {
// result.signature: Uint8Array
// result.signedMessage: Uint8Array
},
});
// Sign a string
signMessage({ message: "Hello World" });
// Sign raw bytes
signMessage({ message: new Uint8Array([1, 2, 3]) });
useNativeBalance
const {
balance, // bigint | undefined - Raw balance in kelvins
formatted, // string | undefined - Human readable (e.g., "1.5")
isLoading, // boolean
isFetching, // boolean - True during refetch
isError, // boolean
error, // Error | null
refetch, // () => void
} = useNativeBalance({
address: "optional-address", // Defaults to active account
decimals: 9, // Defaults to 9
});
useSwitchChain
import { getDefaultRialoClientConfig } from "@rialo/frost";
const switchChain = useSwitchChain();
// Switch to mainnet
switchChain(getDefaultRialoClientConfig("mainnet"));
// Switch to testnet
switchChain(getDefaultRialoClientConfig("testnet"));
Components
ConnectButton
A complete, styled connect button with dropdown:
<ConnectButton
label="Connect Wallet" // Button text when disconnected
connectingLabel="Connecting..." // Text while connecting
connectedLabel={(addr, wallet) => // Text when connected
`${addr.slice(0, 6)}...`
}
showDropdown={true} // Show dropdown when connected
dropdownClassName="my-dropdown" // CSS class for dropdown container
onConnect={(wallet, address) => {}} // Connection callback
onDisconnect={() => {}} // Disconnect callback
onError={(error) => {}} // Error callback
/>
WalletModal
A modal for wallet selection:
const [open, setOpen] = useState(false);
<WalletModal
open={open}
onClose={() => setOpen(false)}
onConnect={(walletName, address) => {
console.log(`Connected to ${walletName}`);
setOpen(false);
}}
onError={(error, walletName) => {
console.error(`${walletName} failed:`, error);
}}
title="Select a Wallet"
/>
Error Handling
Frost provides typed errors for precise handling:
import { isFrostError, type FrostError } from "@rialo/frost";
function handleError(error: Error) {
if (!isFrostError(error)) {
console.error("Unknown error:", error);
return;
}
switch (error.code) {
case "WALLET_NOT_FOUND":
alert(`Install ${error.walletName} to continue`);
break;
case "WALLET_DISCONNECTED":
alert("Please connect your wallet");
break;
case "UNSUPPORTED_CHAIN":
alert("Switch to a supported network");
break;
case "UNSUPPORTED_FEATURE":
alert("Your wallet doesn't support this feature");
break;
case "CONNECTION_FAILED":
alert("Connection rejected or failed");
break;
case "SESSION_EXPIRED":
alert("Session expired, please reconnect");
break;
case "TRANSACTION_FAILED":
alert(`Transaction failed: ${error.reason}`);
break;
case "WALLET_ERROR":
alert(error.message);
break;
}
}
Error Types
| Code | Class | When |
|---|---|---|
WALLET_NOT_FOUND |
WalletNotFoundError |
Wallet extension not installed |
WALLET_DISCONNECTED |
WalletDisconnectedError |
No wallet connected |
UNSUPPORTED_CHAIN |
UnsupportedChainError |
Wallet doesn't support current chain |
UNSUPPORTED_FEATURE |
UnsupportedFeatureError |
Missing wallet capability |
CONNECTION_FAILED |
ConnectionFailedError |
User rejected or wallet error |
SESSION_EXPIRED |
SessionExpiredError |
Saved session too old |
TRANSACTION_FAILED |
TransactionFailedError |
Transaction confirmed but execution failed on-chain |
WALLET_ERROR |
WalletError |
Generic wallet error |
Advanced Usage
Custom RPC URL
import { createConfig } from "@rialo/frost";
const config = createConfig({
clientConfig: {
chain: {
id: "rialo:mainnet",
name: "Mainnet",
rpcUrl: "https://my-custom-rpc.example.com",
},
transport: { timeout: 30000 },
},
});
Disable Session Persistence
const config = createConfig({
clientConfig: getDefaultRialoClientConfig("devnet"),
storage: null, // Won't remember connected wallet
});
Share TanStack Query Client
If your app already uses TanStack Query:
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
const queryClient = new QueryClient();
function App() {
return (
<QueryClientProvider client={queryClient}>
<FrostProvider config={frostConfig} queryClient={queryClient}>
<YourApp />
</FrostProvider>
</QueryClientProvider>
);
}
Direct Config Access
import { useFrostConfig } from "@rialo/frost";
function Advanced() {
const config = useFrostConfig();
// Access RPC client
const client = config.client;
// Get chain ID
const chainId = config.getChainId();
// Switch chain imperatively
config.switchChain(getDefaultRialoClientConfig("mainnet"));
}
Non-React Usage
import {
createConfig,
getDefaultRialoClientConfig,
connect,
disconnect,
signTransaction,
WalletRegistry,
WalletEventBridge,
initializeConfig,
} from "@rialo/frost-core";
// Setup
const config = createConfig({
clientConfig: getDefaultRialoClientConfig("devnet"),
});
const registry = new WalletRegistry(config);
const bridge = new WalletEventBridge(config);
initializeConfig(config, registry, bridge);
// Connect
const { accountAddress } = await connect(config, { walletName: "Rialo" });
// Sign
const { signedTransaction } = await signTransaction(config, { transaction });
// Disconnect
await disconnect(config);
// Cleanup
config.destroy();
Troubleshooting
"No wallets found"
- Install a Rialo wallet extension
- Make sure it's enabled for your site
- Refresh the page
Connection keeps failing
- Unlock your wallet
- Check if the wallet supports your network (devnet/mainnet)
- Try disabling and re-enabling the extension
- Clear localStorage and refresh
Hooks not updating
Wrong: Creating config inside component
function App() {
const config = createConfig({ ... }); // ❌ Recreated every render
return <FrostProvider config={config}>...</FrostProvider>;
}
Right: Creating config outside component
const config = createConfig({ ... }); // ✅ Created once
function App() {
return <FrostProvider config={config}>...</FrostProvider>;
}
"Session expired" on page load
This is expected behavior. The default session TTL is 7 days. Users just need to reconnect.
Configuration Options
interface CreateConfigOptions {
/** RPC client configuration (required) */
clientConfig: RialoClientConfig;
/** Auto-reconnect on page load (default: true) */
autoConnect?: boolean;
/** Session TTL in ms (default: 7 days) */
sessionTTL?: number;
/** Storage key prefix (default: "rialo-frost") */
storageKey?: string;
/** Custom storage (default: localStorage, null to disable) */
storage?: Storage | null;
}
Networks
| Network | Chain ID | Use Case |
|---|---|---|
| Devnet | rialo:devnet |
Development (free test tokens) |
| Testnet | rialo:testnet |
Pre-production testing |
| Mainnet | rialo:mainnet |
Production |
CSS Custom Properties
Frost components support theming via CSS custom properties. Set these variables to customize the appearance:
Colors
| Variable | Default | Description |
|---|---|---|
--frost-primary |
#3b82f6 |
Primary brand color (buttons, accents) |
--frost-text-color |
#111827 |
Main text color |
--frost-text-secondary |
#6b7280 |
Secondary/muted text |
--frost-border-color |
#e5e7eb |
Border and divider color |
--frost-danger |
#ef4444 |
Destructive action color (disconnect) |
Backgrounds
| Variable | Default | Description |
|---|---|---|
--frost-button-text |
#ffffff |
Button text color |
--frost-button-connected-bg |
#f3f4f6 |
Connected button background |
--frost-button-connected-text |
#111827 |
Connected button text |
--frost-dropdown-bg |
#ffffff |
Dropdown menu background |
--frost-hover-bg |
#f3f4f6 |
Hover state background |
--frost-modal-bg |
#ffffff |
Modal dialog background |
--frost-overlay-bg |
rgba(0, 0, 0, 0.5) |
Modal overlay background |
--frost-icon-bg |
#f3f4f6 |
Wallet icon background |
Layout
| Variable | Default | Description |
|---|---|---|
--frost-border-radius |
8px |
Default border radius |
--frost-border-radius-sm |
6px |
Small border radius |
--frost-modal-max-width |
400px |
Maximum modal width |
--frost-shadow |
0 4px 16px rgba(0, 0, 0, 0.12) |
Box shadow |
--frost-z-index |
9999 |
Z-index for overlays |
Example: Dark Theme
:root {
--frost-primary: #60a5fa;
--frost-text-color: #f9fafb;
--frost-text-secondary: #9ca3af;
--frost-border-color: #374151;
--frost-button-text: #ffffff;
--frost-button-connected-bg: #1f2937;
--frost-button-connected-text: #f9fafb;
--frost-dropdown-bg: #1f2937;
--frost-hover-bg: #374151;
--frost-modal-bg: #111827;
--frost-overlay-bg: rgba(0, 0, 0, 0.7);
--frost-icon-bg: #374151;
}
Example: Custom Brand Colors
:root {
--frost-primary: #8b5cf6; /* Purple primary */
--frost-border-radius: 12px; /* More rounded */
--frost-shadow: 0 8px 32px rgba(139, 92, 246, 0.2); /* Purple tinted shadow */
}