@rialo/frost-core
v0.1.1
Published
Framework-agnostic wallet integration library for Rialo DApps
Readme
Rialo Frost Core 🧊
Framework-agnostic wallet integration library for Rialo dApps — The foundation that powers @rialo/frost React bindings. Use this package directly when building with Vue, Svelte, Angular, or vanilla JavaScript.
pnpm add @rialo/frost-coreWhy Frost Core?
Frost Core provides the low-level wallet integration primitives without any framework dependencies:
- Zero Framework Lock-in: Works with any JavaScript framework or vanilla JS
- Wallet Standard Compliant: Built on @wallet-standard
- Reactive State Management: Powered by @tanstack/store for efficient updates
- Session Persistence: Automatic session management with configurable TTL
- Type-Safe: Full TypeScript support with comprehensive type definitions
- Tree-Shakeable: Only import what you need
Table of Contents
- Architecture Overview
- Installation
- Quick Start
- Configuration
- Actions Reference
- State Management
- Error Handling
- Testing
- Advanced Usage
- Troubleshooting
- API Reference
Architecture Overview
┌──────────────────────────────────────────────────────────────┐
│ Your Application │
├──────────────────────────────────────────────────────────────┤
│ Frost Core │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
│ │ Actions │ │ Store │ │ WalletRegistry │ │
│ │ connect() │ │ TanStack │ │ Auto-discovery │ │
│ │ sign*() │◄─┤ Store │◄─┤ Event handling │ │
│ │ send*() │ │ Reactive │ │ Wallet-standard │ │
│ └─────────────┘ └─────────────┘ └─────────────────────┘ │
├──────────────────────────────────────────────────────────────┤
│ @rialo/wallet-standard │
├──────────────────────────────────────────────────────────────┤
│ Wallet Extensions │
└──────────────────────────────────────────────────────────────┘Key Components:
- Actions: Pure functions for wallet operations (
connect,signMessage,sendTransaction, etc.) - Store: Reactive state container with automatic persistence
- WalletRegistry: Discovers and tracks available Rialo-compatible wallets
- WalletEventBridge: Handles wallet events (account changes, disconnections)
Installation
Requirements
- Node.js 18+
- A Rialo wallet extension (for testing)
Install
# pnpm (recommended)
pnpm add @rialo/frost-core
# npm
npm install @rialo/frost-core
# yarn
yarn add @rialo/frost-coreQuick Start
import {
createConfig,
getDefaultRialoClientConfig,
connect,
disconnect,
signMessage,
sendTransaction,
WalletRegistry,
WalletEventBridge,
initializeConfig,
} from "@rialo/frost-core";
// 1. Create configuration
const config = createConfig({
clientConfig: getDefaultRialoClientConfig("devnet"),
autoConnect: true,
});
// 2. Initialize wallet discovery (required for wallet operations)
const registry = new WalletRegistry(config);
const bridge = new WalletEventBridge(config);
initializeConfig(config, registry, bridge);
// 3. Wait for wallet discovery
await registry.ready;
// 4. Connect to a wallet
const { walletName, accountAddress } = await connect(config, {
walletName: "Rialo",
});
console.log(`Connected: ${accountAddress}`);
// 5. Sign a message
const { signature, signedMessage } = await signMessage(config, {
message: "Hello from my dApp!",
});
// 6. Send a transaction
const { signature: txSignature } = await sendTransaction(config, {
transaction: myTransactionBytes,
});
// 7. Cleanup when done
config.destroy();Configuration
createConfig(options)
Creates a Frost configuration object. Important: Create once and reuse throughout your application.
import { createConfig, getDefaultRialoClientConfig } from "@rialo/frost-core";
const config = createConfig({
// Required: RPC client configuration
clientConfig: getDefaultRialoClientConfig("devnet"),
// Optional settings (shown with defaults)
autoConnect: true, // Reconnect on page load
sessionTTL: 7 * 24 * 60 * 60 * 1000, // 7 days in milliseconds
storageKey: "rialo-frost", // localStorage key prefix
storage: localStorage, // Storage implementation (null to disable)
});Configuration Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| clientConfig | RialoClientConfig | Required | RPC client configuration |
| autoConnect | boolean | true | Auto-reconnect on page load |
| sessionTTL | number | 604800000 (7 days) | Session expiration in milliseconds |
| storageKey | string | "rialo-frost" | Storage key prefix |
| storage | Storage \| null | localStorage | Storage implementation |
Custom RPC URL
const config = createConfig({
clientConfig: {
chain: {
id: "rialo:mainnet",
name: "Mainnet",
rpcUrl: "https://my-custom-rpc.example.com",
},
transport: { timeout: 30000 },
},
});Networks
| Network | Chain ID | Getter |
|---------|----------|--------|
| Devnet | rialo:devnet | getDefaultRialoClientConfig("devnet") |
| Testnet | rialo:testnet | getDefaultRialoClientConfig("testnet") |
| Mainnet | rialo:mainnet | getDefaultRialoClientConfig("mainnet") |
| Localnet | rialo:localnet | getDefaultRialoClientConfig("localnet") |
Actions Reference
All actions are pure functions that take config as their first argument.
Connection Actions
connect(config, options)
Connects to a wallet.
interface ConnectOptions {
walletName: string; // Name of the wallet to connect
silent?: boolean; // Attempt without popup (for reconnection)
}
interface ConnectResult {
walletName: string;
accountAddress: string;
}
const result = await connect(config, { walletName: "Rialo" });Throws:
WalletNotFoundError— Wallet not discoveredUnsupportedFeatureError— Wallet doesn't support connectConnectionFailedError— User rejected or wallet error
disconnect(config)
Disconnects from the current wallet. Sets userDisconnected flag to prevent auto-reconnect.
await disconnect(config);reconnect(config)
Attempts silent reconnection to previously connected wallet.
const result = await reconnect(config);
// Returns ConnectResult | nullReturns null if:
- No previous connection exists
- User explicitly disconnected
- Session expired
getActiveConnection(config)
Gets current connection state synchronously.
interface ActiveConnection {
status: ConnectionStatus;
chainId: string;
walletName: string | null;
accountAddress: string | null;
connectedAt: number | null;
lastUsedAt: number;
}
const connection = getActiveConnection(config);Wallet Actions
getWallets(config)
Gets all discovered wallets.
const wallets: WalletEntity[] = getWallets(config);getSortedWallets(config)
Gets wallets sorted by priority (higher first), then by last connected time.
const wallets: WalletEntity[] = getSortedWallets(config);getWallet(config, walletName)
Gets a specific wallet by name.
const wallet: WalletEntity | undefined = getWallet(config, "Rialo");hasFeature(config, walletName, feature)
Checks if a wallet supports a specific feature.
const canSign = hasFeature(config, "Rialo", "rialo:signMessage");supportsChain(config, walletName)
Checks if a wallet supports the currently configured chain.
const supported = supportsChain(config, "Rialo");Account Actions
getAccounts(config)
Gets all known accounts.
const accounts: AccountEntity[] = getAccounts(config);getActiveAccount(config)
Gets the currently active (connected) account.
const account: AccountEntity | null = getActiveAccount(config);getAccount(config, address)
Gets a specific account by address.
const account: AccountEntity | undefined = getAccount(config, "7xKXtg...");Message Signing
signMessage(config, options)
Signs an arbitrary message.
interface SignMessageOptions {
message: string | Uint8Array; // String will be UTF-8 encoded
}
interface SignMessageResult {
signature: Uint8Array;
signedMessage: Uint8Array;
}
// Sign a string
const result = await signMessage(config, {
message: "Hello World",
});
// Sign raw bytes
const result = await signMessage(config, {
message: new Uint8Array([1, 2, 3]),
});Throws:
WalletDisconnectedError— No wallet connectedUnsupportedFeatureError— Wallet doesn't support signingWalletError— Wallet operation failed
Transaction Operations
signTransaction(config, options)
Signs a transaction without submitting it.
interface SignTransactionOptions {
transaction: Uint8Array | { serialize(): Uint8Array };
}
interface SignTransactionResult {
signedTransaction: Uint8Array;
}
const { signedTransaction } = await signTransaction(config, {
transaction: myTransaction,
});sendTransaction(config, options)
Signs and sends a transaction using the wallet's built-in send feature.
interface SendTransactionResult {
signature: Uint8Array;
}
const { signature } = await sendTransaction(config, {
transaction: myTransaction,
});signAndSendTransaction(config, options)
Signs via wallet, sends via Frost's RPC client with confirmation. Useful when you want:
- Custom RPC URL
- More control over retry logic
- To not trust the wallet's send implementation
interface SignAndSendTransactionOptions {
transaction: Uint8Array | { serialize(): Uint8Array };
sendOptions?: {
skipPreflight?: boolean;
maxRetries?: number;
retryDelay?: number;
};
}
interface SignAndSendTransactionResult {
signature: string; // Base58 encoded
}
const { signature } = await signAndSendTransaction(config, {
transaction: myTransaction,
sendOptions: { maxRetries: 3 },
});Throws:
WalletDisconnectedError— No wallet connectedUnsupportedFeatureError— Wallet doesn't support signingTransactionFailedError— Transaction confirmed but execution failed on-chain
State Management
Frost Core uses @tanstack/store for reactive state management.
FrostAppState
interface FrostAppState {
status: ConnectionStatus; // "disconnected" | "connecting" | "connected" | "reconnecting"
chainId: string; // Current chain ID
walletName: string | null; // Connected wallet name
accountAddress: string | null; // Active account address
connectedAt: number | null; // Connection timestamp
sessionExpiresAt: number | null; // Session expiration timestamp
lastUsedAt: number; // Last activity timestamp
userDisconnected: boolean; // Prevents auto-reconnect
wallets: Map<string, WalletEntity>; // Discovered wallets
accounts: Map<string, AccountEntity>; // Known accounts
}Subscribing to State Changes
// Subscribe to all state changes
const unsubscribe = config.store.subscribe(({ currentVal, prevVal }) => {
if (currentVal.status !== prevVal.status) {
console.log(`Status changed: ${prevVal.status} -> ${currentVal.status}`);
}
});
// Read current state
const state = config.store.state;
// Unsubscribe when done
unsubscribe();Entity Types
WalletEntity
interface WalletEntity {
name: string; // Display name
icon?: string; // Base64 data URI
chains: readonly string[]; // Supported chains
features: readonly string[]; // Feature identifiers
installedVersion?: string; // Wallet version
priority?: number; // UI sort priority
lastConnectedAt?: number; // Last successful connection
}AccountEntity
interface AccountEntity {
address: string; // Base58-encoded address
publicKey: Uint8Array; // Raw public key bytes
walletName: string; // Parent wallet name
chains: readonly string[]; // Valid chains
label?: string; // Optional user label
}Error Handling
Frost provides typed errors for precise handling.
Error Type Guard
import { isFrostError, type FrostError } from "@rialo/frost-core";
try {
await connect(config, { walletName: "Unknown" });
} catch (error) {
if (isFrostError(error)) {
handleFrostError(error);
} else {
console.error("Unknown error:", error);
}
}Error Types
| Code | Class | When |
|------|-------|------|
| WALLET_NOT_FOUND | WalletNotFoundError | Wallet extension not discovered |
| WALLET_DISCONNECTED | WalletDisconnectedError | Operation requires connected wallet |
| 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 | Persisted session too old |
| TRANSACTION_FAILED | TransactionFailedError | Transaction confirmed but failed on-chain |
| WALLET_ERROR | WalletError | Generic wallet error wrapper |
Error Properties
All errors extend FrostError and include:
class FrostError extends Error {
code: string; // Error code (e.g., "WALLET_NOT_FOUND")
walletName?: string; // Associated wallet (if applicable)
}Specific errors have additional properties:
// UnsupportedChainError
error.chainId; // Requested chain
error.supportedChains; // Array of supported chains
// UnsupportedFeatureError
error.feature; // Required feature identifier
// ConnectionFailedError
error.reason; // Failure reason string
// SessionExpiredError
error.expiredAt; // Expiration timestamp
// TransactionFailedError
error.signature; // Transaction signature
error.reason; // On-chain failure reason
// WalletError
error.originalError; // Original error from walletHandling Pattern
import {
isFrostError,
WalletNotFoundError,
WalletDisconnectedError,
UnsupportedChainError,
ConnectionFailedError,
} from "@rialo/frost-core";
function handleError(error: unknown) {
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 first");
break;
case "UNSUPPORTED_CHAIN":
const chainError = error as UnsupportedChainError;
alert(`Switch to: ${chainError.supportedChains.join(", ")}`);
break;
case "CONNECTION_FAILED":
alert("Connection rejected or failed");
break;
case "TRANSACTION_FAILED":
alert(`Transaction failed: ${error.message}`);
break;
default:
alert(error.message);
}
}Testing
Frost Core includes comprehensive testing utilities via the /testing export.
Installation
import {
createMockConfig,
createMockWallet,
createMockAccount,
createMockClient,
createMockStorage,
waitFor,
waitForState,
} from "@rialo/frost-core/testing";Creating a Mock Config
const { config, mockClient, mockStorage } = createMockConfig({
chainId: "rialo:devnet",
autoConnect: false,
initialState: {
status: "disconnected",
},
});Creating Mock Wallets
const mockWallet = createMockWallet({
name: "TestWallet",
autoApprove: true, // Auto-approve connections
connectDelay: 100, // Simulate network delay
failConnect: false, // Simulate connection failure
supportsEvents: true, // Include standard:events
supportsSignMessage: true, // Include rialo:signMessage
supportsSignTransaction: true,
supportsSignAndSend: true,
});
// Register the mock wallet
config.store.setState(state => ({
...state,
wallets: new Map([[mockWallet.name, createMockWalletEntity({ name: mockWallet.name })]]),
_walletRefs: new Map([[mockWallet.name, mockWallet]]),
}));Mock Wallet Test Helpers
// Trigger account change event
mockWallet._emitAccountChange([newAccount]);
// Check call counts
console.log(mockWallet._connectCallCount);
console.log(mockWallet._signMessageCallCount);
// Access last operations
console.log(mockWallet._lastSignedMessage);
console.log(mockWallet._lastSignedTransaction);
// Reset state between tests
mockWallet._reset();Mock RPC Client
const mockClient = createMockClient({
defaultBalance: 1_000_000_000n, // 1 SOL equivalent
requestDelay: 50, // Simulate latency
failRequests: false, // Simulate RPC errors
});
// Set specific balances
mockClient._setBalance("7xKXtg...", 5_000_000_000n);
// Simulate transaction failures
mockClient._setTransactionFails(true, "Insufficient funds");
// Check operations
console.log(mockClient._sendTransactionCallCount);
console.log(mockClient._lastSentTransaction);Wait Utilities
// Wait for a condition
await waitFor(() => config.store.state.status === "connected", {
timeout: 5000,
interval: 50,
});
// Wait for specific state
const state = await waitForState(
config,
(s) => s.wallets.size > 0,
{ timeout: 3000 }
);Example Test
import { describe, it, expect, beforeEach } from "vitest";
import { connect, disconnect } from "@rialo/frost-core";
import {
createMockConfig,
createMockWallet,
createMockWalletEntity,
waitForState,
} from "@rialo/frost-core/testing";
describe("connect", () => {
let config;
let mockWallet;
beforeEach(() => {
const { config: c } = createMockConfig();
config = c;
mockWallet = createMockWallet({ name: "TestWallet" });
// Register mock wallet
config.store.setState(state => ({
...state,
wallets: new Map([["TestWallet", createMockWalletEntity({ name: "TestWallet" })]]),
_walletRefs: new Map([["TestWallet", mockWallet]]),
}));
});
it("should connect to wallet", async () => {
const result = await connect(config, { walletName: "TestWallet" });
expect(result.walletName).toBe("TestWallet");
expect(config.store.state.status).toBe("connected");
expect(mockWallet._connectCallCount).toBe(1);
});
it("should throw on unknown wallet", async () => {
await expect(
connect(config, { walletName: "Unknown" })
).rejects.toThrow("WALLET_NOT_FOUND");
});
});Advanced Usage
Initializing Without Auto-Discovery
For custom wallet registration or SSR environments:
import {
createConfig,
getDefaultRialoClientConfig,
WalletRegistry,
WalletEventBridge,
initializeConfig,
} from "@rialo/frost-core";
const config = createConfig({
clientConfig: getDefaultRialoClientConfig("devnet"),
storage: null, // Disable persistence for SSR
});
// Initialize only in browser
if (typeof window !== "undefined") {
const registry = new WalletRegistry(config);
const bridge = new WalletEventBridge(config);
initializeConfig(config, registry, bridge);
}Switching Chains
import { getDefaultRialoClientConfig } from "@rialo/frost-core";
// Switch to mainnet
config.switchChain(getDefaultRialoClientConfig("mainnet"));
// Get current chain
const chainId = config.getChainId(); // "rialo:mainnet"Direct Store Access
// Read state
const { status, walletName, accountAddress } = config.store.state;
// Subscribe to changes
const unsubscribe = config.store.subscribe(({ currentVal }) => {
console.log("New status:", currentVal.status);
});
// Update state (internal use only)
config.store.setState(state => ({
...state,
lastUsedAt: Date.now(),
}));Custom Persistence
// In-memory storage
const memoryStorage = new Map<string, string>();
const customStorage: Storage = {
getItem: (key) => memoryStorage.get(key) ?? null,
setItem: (key, value) => memoryStorage.set(key, value),
removeItem: (key) => memoryStorage.delete(key),
clear: () => memoryStorage.clear(),
get length() { return memoryStorage.size; },
key: (index) => Array.from(memoryStorage.keys())[index] ?? null,
};
const config = createConfig({
clientConfig: getDefaultRialoClientConfig("devnet"),
storage: customStorage,
});Direct RPC Client Access
// Access the RialoClient for direct RPC calls
const client = config.client;
// Example: Get balance
const balance = await client.getBalance(publicKey);
// Example: Get latest blockhash
const { blockhash, lastValidBlockHeight } = await client.getLatestBlockhash();Cleanup
Always destroy the config when unmounting your app:
// Cleanup all subscriptions and resources
config.destroy();Troubleshooting
No wallets found
- Install a Rialo wallet extension
- Make sure the extension is enabled for your site
- Wait for
registry.readybefore checking wallets - 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
Session expired on page load
Expected behavior. The default session TTL is 7 days. Configure with sessionTTL option:
const config = createConfig({
clientConfig: getDefaultRialoClientConfig("devnet"),
sessionTTL: 30 * 24 * 60 * 60 * 1000, // 30 days
});State not updating
Ensure you're subscribing to the store correctly:
// Wrong: Reading state once
const status = config.store.state.status;
// Right: Subscribing to changes
config.store.subscribe(({ currentVal }) => {
updateUI(currentVal.status);
});Auto-reconnect not working
Check if userDisconnected flag is set:
const { userDisconnected } = config.store.state;
if (userDisconnected) {
// User explicitly disconnected, won't auto-reconnect
// Must call connect() again
}API Reference
Exports
// Configuration
export { createConfig, initializeConfig } from "./config";
export type { CreateConfigOptions, FrostConfig } from "./config";
// Chain definitions (re-exported from @rialo/ts-cdk)
export {
getDefaultRialoClientConfig,
RIALO_DEVNET_CHAIN,
RIALO_TESTNET_CHAIN,
RIALO_MAINNET_CHAIN,
RIALO_LOCALNET_CHAIN,
} from "@rialo/ts-cdk";
// Actions
export {
connect,
disconnect,
reconnect,
getActiveConnection,
getWallets,
getSortedWallets,
getWallet,
hasFeature,
supportsChain,
getAccounts,
getActiveAccount,
getAccount,
signMessage,
signTransaction,
sendTransaction,
signAndSendTransaction,
} from "./actions";
// Types
export type {
ConnectionStatus,
FrostAppState,
WalletEntity,
AccountEntity,
ConnectOptions,
ConnectResult,
ActiveConnection,
SignMessageOptions,
SignMessageResult,
SignTransactionOptions,
SignTransactionResult,
SendTransactionResult,
SignAndSendTransactionOptions,
SignAndSendTransactionResult,
} from "./types";
// Errors
export {
FrostError,
isFrostError,
WalletNotFoundError,
WalletDisconnectedError,
UnsupportedChainError,
UnsupportedFeatureError,
ConnectionFailedError,
SessionExpiredError,
TransactionFailedError,
WalletError,
} from "./errors";
// Internal (for framework bindings)
export { WalletRegistry } from "./registry/WalletRegistry";
export { WalletEventBridge } from "./bridge/WalletEventBridge";Testing Exports (@rialo/frost-core/testing)
export {
createMockConfig,
createMockWallet,
createMockAccount,
createMockClient,
createMockStorage,
createMockWalletEntity,
createMockAccountEntity,
waitFor,
waitForState,
} from "./testing";License
Apache-2.0
