npm.io
0.18.1 • Published 5d ago

@rialo/frost

Licence
Apache-2.0
Version
0.18.1
Deps
4
Size
289 kB
Vulns
0
Weekly
0

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?

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"
  1. Install a Rialo wallet extension
  2. Make sure it's enabled for your site
  3. Refresh the page
Connection keeps failing
  1. Unlock your wallet
  2. Check if the wallet supports your network (devnet/mainnet)
  3. Try disabling and re-enabling the extension
  4. 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 */
}

Keywords