JSPM

  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 671
  • Score
    100M100P100Q94746F
  • License Apache-2.0

Framework-agnostic wallet integration library for Rialo DApps

Package Exports

  • @rialo/frost-core
  • @rialo/frost-core/testing

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-core

Why 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

┌──────────────────────────────────────────────────────────────┐
│                       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-core

Quick 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 discovered
  • UnsupportedFeatureError — Wallet doesn't support connect
  • ConnectionFailedError — 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 | null

Returns 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 connected
  • UnsupportedFeatureError — Wallet doesn't support signing
  • WalletError — 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 connected
  • UnsupportedFeatureError — Wallet doesn't support signing
  • TransactionFailedError — 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 wallet

Handling 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

  1. Install a Rialo wallet extension
  2. Make sure the extension is enabled for your site
  3. Wait for registry.ready before checking wallets
  4. 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

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