@rialo/frost
v0.1.1
Published
React wallet integration library for Rialo DApps
Readme
Rialo Frost 🧊
The wallet integration toolkit for Rialo dApps — Connect wallets, sign transactions, and build Web3 apps in minutes.
pnpm add @rialo/frost30-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?
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/frostFor Non-React Apps
Use the framework-agnostic core package:
pnpm add @rialo/frost-coreStep-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 wallet discovery is complete |
| 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() | Mutation | 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, useSwitchChain) return both mutate and mutateAsync.
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 { mutateAsync: switchChainAsync } = useSwitchChain();
await switchChainAsync(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 lamports
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 { mutate: 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
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 */
}