"A zero-dependency, atomic, and surgical Solana wallet connection library powered by Utsutsu and the native Browser Wallet Standard."
In Japanese, Hirin (日輪) represents the Solar Disc or the radiant Sun Halo. In the Solana ecosystem, connecting a user's wallet is the fundamental gateway to on-chain experiences. Yet, traditional wallet adapters have historically cast long, heavy shadows over our codebases:
- Context Re-Render Cascades: Legacy adapters bind the wallet state to a monolithic React Context. When the user's connection status, active wallet, or public key changes, the entire virtual DOM tree down from the provider undergoes complete re-evaluation, introducing latency and UI micro-stutters.
- Hydration Crashes: Server-Side Rendered (SSR) setups (like Next.js App Router) frequently crash during builds or hydration due to eager checks on browser-only
windowvariables. - Obsolete Wallet Bloat: Importing outdated adapters forces bundle sizes to balloon, packaging giant SVG icon strings and legacy API wrappers for wallets your users may never use.
Hirin was built to eliminate these problems.
Hirin rejects the traditional React Context approach. Powered by the reactive state cell engine Utsutsu, Hirin exposes all wallet states as atomic Lenses. When the wallet connects or disconnects, only the specific DOM nodes subscribed to those exact lenses are updated. The rest of your React component tree remains perfectly static.
Hirin contains zero dependency packages in its final bundle. It relies entirely on the browser-native Wallet Standard (specifically @wallet-standard/base and @wallet-standard/core under the hood) which are standard interfaces injected directly by modern browser extensions.
Hirin is 100% server-safe. All client discovery mechanisms are lazily evaluated and guarded. You can import Hirin in Next.js React Server Components (RSC) and render custom headless shells without triggering hydration errors.
The primary purpose of Hirin is to act as a lightweight, headless bridge between your dApp UI and the native browser-injected wallets.
By offloading discovery, event listening, and session management to the browser's native capabilities, Hirin accomplishes three things:
- Future-Proof Discovery: As new wallets are created and adopted, they automatically register themselves on the user's browser via the Wallet Standard. Hirin detects them instantly—no library updates or new adapter installations required.
- Event-Driven Reactiveness: Utilizing push-based
standard:events, Hirin synchronizes account switches or locks inside the wallet extension instantly, without any background polling or interval queries. - Decoupled Headless Design: Hirin provides only the underlying state and interactions. You retain absolute control over your UI styling, transitions, and accessibility.
Install hirin along with its lightweight peer dependencies:
npm install hirin utsutsu @solana/kit reactutsutsu: Atomic, lens-based state manager.@solana/kit: Modern, unified Solana Web3 SDK (v2 / v6+ compliant).react: Supporting React 18 & React 19 natively.
Here are production-grade recipes showing how to harness Hirin for different standard dApp workflows.
To begin discovering browser-injected wallets and restore the user's last authorized connection, initialize Hirin once at the root entry point:
// main.tsx or layout.tsx (Client Component)
import { useEffect } from "react";
import { initializeHirin } from "hirin";
export function AppProvider({ children }: { children: React.ReactNode }) {
useEffect(() => {
// Begins dynamic wallet standard discovery.
// Setting autoConnect to true attempts to silently re-establish
// connection with the last used wallet from localStorage.
const cleanup = initializeHirin({ autoConnect: true });
return () => cleanup(); // React StrictMode safe: cleans up listeners on unmount
}, []);
return <>{children}</>;
}Combine Hirin with Radix UI Dialog (or Shadcn UI) to build a beautiful, modern glassmorphic wallet-selection modal:
import { useState } from "react";
import * as Dialog from "@radix-ui/react-dialog";
import { useValue } from "utsutsu";
import {
walletsLens,
publicKeyLens,
connectingLens,
connect,
disconnect,
POPULAR_WALLETS
} from "hirin";
export function WalletConnector() {
const wallets = useValue(walletsLens);
const publicKey = useValue(publicKeyLens);
const isConnecting = useValue(connectingLens);
const [open, setOpen] = useState(false);
// Merge discovered wallets with a static registry to show install links
const walletList = POPULAR_WALLETS.map((registry) => {
const installed = wallets.find((w) =>
w.name.toLowerCase().includes(registry.name.toLowerCase())
);
return {
name: registry.name,
icon: installed ? installed.icon : registry.icon,
url: registry.url,
isInstalled: !!installed,
instance: installed,
};
});
const handleConnect = async (walletInstance: any) => {
try {
await connect(walletInstance);
setOpen(false);
} catch (err) {
console.error("Connection failed", err);
}
};
if (publicKey) {
return (
<div className="connected-badge">
<span>{publicKey.slice(0, 4)}...{publicKey.slice(-4)}</span>
<button onClick={disconnect}>Disconnect</button>
</div>
);
}
return (
<Dialog.Root open={open} onOpenChange={setOpen}>
<Dialog.Trigger className="connect-btn">
{isConnecting ? "Connecting..." : "Connect Wallet"}
</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay className="modal-overlay" />
<Dialog.Content className="modal-content">
<Dialog.Title>Connect a Wallet</Dialog.Title>
<div className="wallet-grid">
{walletList.map((wallet) => (
wallet.isInstalled && wallet.instance ? (
<button key={wallet.name} onClick={() => handleConnect(wallet.instance)}>
<img src={wallet.icon} alt={wallet.name} />
<span>{wallet.name}</span>
<span className="badge">Detected</span>
</button>
) : (
<a key={wallet.name} href={wallet.url} target="_blank" rel="noopener">
<img src={wallet.icon} alt={wallet.name} className="grayscale" />
<span>{wallet.name}</span>
<span className="badge install">Install</span>
</a>
)
))}
</div>
{/* Extension Auto-Connect Tip */}
<div className="tip-box">
<p className="tip-title">💡 Whitelisting & Reconnections</p>
<p className="tip-text">
Once trusted, Phantom connects silently on subsequent clicks. To switch accounts,
simply select it directly inside your Phantom Extension—Hirin will sync your dApp instantly!
</p>
</div>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);
}Hirin integrates directly with standard:events to register reactive callback listeners. This means you do not need to prompt the user or disconnect/reconnect when they switch accounts.
When a user switches their active address directly in the Phantom or Solflare extension UI:
- The extension emits a native
changeevent. - Hirin's internal listener catches the updated accounts array.
- Hirin updates
publicKeyLensinstantly. - Only the components rendering the address re-render; no page reloads, no session losses.
Build, serialize, sign, and broadcast transactions using the modern @solana/kit (v2 / v6+) transaction model:
import { signAndSendTransaction, publicKeyLens } from "hirin";
import { store } from "hirin/store"; // or read via utsutsu
import {
address,
pipe,
createTransaction,
setTransactionFeePayer,
setTransactionLifetimeUsingBlockhash,
appendTransactionInstruction,
compileTransaction,
getBase64EncodedWireTransaction
} from "@solana/kit";
import { getTransferSolInstruction } from "@solana-program/system";
async function transferSol(recipientAddress: string, lamports: bigint, rpc: any) {
const senderPubKey = store.get().publicKey;
if (!senderPubKey) throw new Error("Wallet not connected!");
// 1. Fetch blockhash
const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();
// 2. Build the Versioned Transaction using pipes
const transaction = pipe(
createTransaction({ version: 0 }),
tx => setTransactionFeePayer(address(senderPubKey), tx),
tx => setTransactionLifetimeUsingBlockhash(latestBlockhash, tx),
tx => appendTransactionInstruction(
getTransferSolInstruction({
source: address(senderPubKey),
destination: address(recipientAddress),
amount: lamports,
}),
tx
)
);
// 3. Compile and serialize transaction bytes
const compiled = compileTransaction(transaction);
const wireBytes = getBase64EncodedWireTransaction(compiled);
const transactionBytes = new Uint8Array(Buffer.from(wireBytes, "base64"));
// 4. Sign and send to the cluster using Hirin
try {
const signatureBytes = await signAndSendTransaction(transactionBytes, {
commitment: "confirmed"
});
// Convert signature bytes to hex/base58 for Solana Explorer
const signature = Array.from(signatureBytes)
.map(b => b.toString(16).padStart(2, "0"))
.join("");
console.log("Transaction Broadcasted! Signature:", signature);
} catch (err) {
console.error("Transaction failed or was rejected:", err);
}
}Request cryptographically secure signatures for message verification or SIWS auth flows:
import { signMessage } from "hirin";
async function authenticateUser() {
const messageText = `dApp Auth Request:\nChallenge: ${crypto.randomUUID()}\nTimestamp: ${Date.now()}`;
const messageBytes = new TextEncoder().encode(messageText);
try {
// Prompt the extension native signMessage dialog
const signatureBytes = await signMessage(messageBytes);
// Convert signature bytes to hex for your backend auth API
const signatureHex = Array.from(signatureBytes)
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
console.log("Secure verification signature:", signatureHex);
// Send to server verify logic (e.g. nacl.sign.detached.verify)
} catch (error) {
console.warn("User rejected message signature", error);
}
}All lenses are reactive read-only inputs for Utsutsu's useValue() hook.
walletsLens: Discovered browser standard wallets (readonly Wallet[]).activeWalletLens: The currently connected active standard wallet (Wallet | null).publicKeyLens: The Base58-encoded active address of the user (string | null).connectingLens: Handshake loading status (boolean).disconnectingLens: Disconnection loading status (boolean).connectedLens: Derived helper state (boolean).errorLens: The last recorded connection or transaction error (string | null).
initializeHirin(options?: { autoConnect?: boolean }): Bootstraps the standard registry scanner and loads the active cached session. Returns a cleanup function.connect(wallet: Wallet): Establishes connection with a discovered standard wallet. Tries to clear stale caches to ensure native dialogs trigger correctly.disconnect(): Standard-compliant graceful disconnection. Guaranteed to reset all internal store states and clean local storage safely.signMessage(message: Uint8Array): Prompts active wallet to sign arbitrary byte arrays.signTransaction(transaction: Uint8Array): Prompts wallet to sign serialised transaction wire bytes without broadcasting.signAndSendTransaction(transaction: Uint8Array, options?: any): Prompts wallet to sign and broadcast transaction wire bytes to the network.
Hirin (日輪) is open-source software licensed under the MIT License.
Crafted with ☀️ and precision for high-performance Solana applications.