Kheopskit is a library designed to simplify the development of multi-chain DApps (Polkadot, Ethereum, Solana). It provides tools to:
- List all installed wallets and connect/disconnect them.
- List all accounts from those wallets.
- Support Polkadot, Ethereum, and Solana wallets.
- Handle identical accounts injected by multiple wallets.
Try it on the interactive playground
See Kheopswap (polkadot/ethereum) and Taohop for production DApps built using Kheopskit.
Upgrading from v3? v4 moves platforms to plugins and makes platform SDKs (and WalletConnect) optional peer dependencies. See the v4 migration guide.
- Multi-wallet support: Easily interact with Polkadot, Ethereum, and Solana wallets.
- Account management: Manage accounts from all connected wallets in a single list.
- Modern tech stack: Designed for use with polkadot-api (PAPI), viem, and @solana/kit.
Install the required packages using pnpm:
pnpm add @kheopskit/core @kheopskit/reactEach platform lives behind its own entry point and brings its own (optional) peer dependencies. Install only the platforms you use:
| Platform | Entry point | Install |
|---|---|---|
| Polkadot | @kheopskit/core/polkadot |
pnpm add polkadot-api |
| Ethereum | @kheopskit/core/ethereum |
pnpm add viem mipd |
| Solana | @kheopskit/core/solana |
pnpm add @solana/kit @wallet-standard/app @wallet-standard/base |
WalletConnect support (via Reown AppKit, across any platform) is also optional — install it only if you pass config.walletConnect:
| Feature | Install |
|---|---|
| WalletConnect | pnpm add @reown/appkit |
rxjs is always required. Everything else is an optional peer dependency: a Polkadot-only dapp that doesn't use WalletConnect installs neither the Ethereum/Solana SDKs nor @reown/appkit, and none of their code is pulled into your bundle. If config.walletConnect is set but @reown/appkit isn't installed, WalletConnect is disabled with a console error while injected wallets keep working.
Platforms are enabled by passing plugins (imported from their entry points) to config.platforms. Bind them once with createKheopskit to get a KheopskitProvider plus useWallets / useAccounts hooks already typed to those platforms — no generic to repeat.
// kheopskit.ts
import { createKheopskit } from "@kheopskit/react";
import { polkadot } from "@kheopskit/core/polkadot";
import { ethereum } from "@kheopskit/core/ethereum";
export const { KheopskitProvider, useWallets, useAccounts } = createKheopskit({
platforms: [polkadot(), ethereum()],
autoReconnect: true,
});// app.tsx
import { KheopskitProvider, useWallets } from "./kheopskit";
const Wallets = () => {
const { wallets, accounts } = useWallets(); // platform-precise, no type argument
return (
<div>
<h1>Wallets</h1>
{wallets.map((wallet) => (
<div key={wallet.id}>
[{wallet.platform}] {wallet.name}
{wallet.isConnected ? (
<button onClick={() => wallet.disconnect()}>Disconnect</button>
) : (
<button onClick={() => wallet.connect()}>Connect</button>
)}
</div>
))}
<h1>Accounts</h1>
{accounts.map((account) => (
<p key={account.address}>
[{account.platform}] {account.name} - {account.address}
</p>
))}
</div>
);
};
export const App = () => (
<KheopskitProvider>
<Wallets />
</KheopskitProvider>
);Prefer the component directly? Use
<KheopskitProvider config={{ platforms }}>and recover platform-precise account types (e.g.account.signeron Solana,account.clienton Ethereum) with a type argument —useWallets<typeof platforms>(). React context can't be generic, so the bareuseWallets()returns the SDK-free base shapes. Pass a referentially stableconfig(module scope,useMemo, orcreateKheopskit); a new object each render recreates the store and re-subscribes.
Reading a wallet's SDK handle?
state.walletsmixes injected wallets with the single, platform-less WalletConnect connector. Narrow withisInjectedWallet(wallet)(the complement ofisWalletConnectWallet) to recoverwallet.platform,wallet.sourceId, and the injected SDK handle (provideron Ethereum,extensionon Polkadot,walleton Solana).
- Instantiate a Kheopskit observable with
getKheopskit$(config). - Subscribe to the observable to access wallets and accounts.
import { getKheopskit$ } from "@kheopskit/core";
import { polkadot } from "@kheopskit/core/polkadot";
import { ethereum } from "@kheopskit/core/ethereum";
const config = {
platforms: [polkadot(), ethereum()],
autoReconnect: true,
};
const kheopskit$ = getKheopskit$(config);
kheopskit$.subscribe(({ wallets, accounts }) => {
console.log("Wallets:", wallets);
console.log("Accounts:", accounts);
});Pass accountTypes to the polkadot() plugin to control which Polkadot account key types are exposed in accounts.
- Supported values:
"sr25519","ed25519","ecdsa","ethereum" - Default:
["sr25519", "ed25519", "ecdsa"] "ethereum"is excluded by default and must be opted in explicitly
Example including Ethereum-style Polkadot accounts:
import { polkadot } from "@kheopskit/core/polkadot";
const config = {
platforms: [
polkadot({ accountTypes: ["sr25519", "ed25519", "ecdsa", "ethereum"] }),
],
autoReconnect: true,
};Add the solana() plugin to platforms to surface Solana wallets. Injected wallets are discovered via the Wallet Standard; WalletConnect is supported through Reown AppKit.
Each Solana account exposes a signer built on @solana/kit's signer interfaces, so it plugs directly into kit's transaction pipeline (e.g. signAndSendTransactionMessageWithSigners):
import { solana } from "@kheopskit/core/solana";
const config = {
// `chain` is the cluster the account signers target. Default: "solana:mainnet"
platforms: [solana({ chain: "solana:mainnet" })],
autoReconnect: true,
};
// account.signer is bound to the plugin's `chain`
const [signed] = await account.signer.modifyAndSignMessages([message]);
// account.getSigner(chain) returns a signer bound to another cluster
const devnetSigner = account.getSigner("solana:devnet");- Supported
chainvalues:"solana:mainnet","solana:devnet","solana:testnet","solana:localnet" "solana:localnet"cannot be used over WalletConnect (no canonical CAIP-2 id)
Kheopskit supports SSR with frameworks like Next.js and TanStack Start. Pass the ssrCookies prop to enable cookie-based storage that works on the server.
When you pass ssrCookies:
- Storage switches from localStorage to cookies
- Server can read initial state from request headers
- No hydration mismatch between server and client
@kheopskit/react ships with a "use client" directive, so you can import KheopskitProvider straight into a Server Component (like the root layout below) — no manual client-boundary wrapper needed.
// app/layout.tsx
import { cookies } from "next/headers";
import { ethereum } from "@kheopskit/core/ethereum";
import { polkadot } from "@kheopskit/core/polkadot";
import { App } from "./app";
const config = {
platforms: [polkadot(), ethereum()],
autoReconnect: true,
};
export default async function RootLayout({ children }: { children: React.ReactNode }) {
const cookieStore = await cookies();
const ssrCookies = cookieStore.toString();
return (
<html>
<body>
<KheopskitProvider config={config} ssrCookies={ssrCookies}>
{children}
</KheopskitProvider>
</body>
</html>
);
}// routes/__root.tsx
import { createRootRoute, Outlet } from "@tanstack/react-router";
import { createServerFn, Meta, Scripts } from "@tanstack/start";
import { getRequest } from "@tanstack/start/server";
import { ethereum } from "@kheopskit/core/ethereum";
import { polkadot } from "@kheopskit/core/polkadot";
import { KheopskitProvider } from "@kheopskit/react";
const config = {
platforms: [polkadot(), ethereum()],
autoReconnect: true,
};
const getSSRCookies = createServerFn({ method: "GET" }).handler(async () => {
const request = getRequest();
return request?.headers.get("cookie") ?? undefined;
});
export const Route = createRootRoute({
loader: async () => ({ ssrCookies: await getSSRCookies() }),
component: RootComponent,
});
function RootComponent() {
const { ssrCookies } = Route.useLoaderData();
return (
<html>
<head><Meta /></head>
<body>
<KheopskitProvider config={config} ssrCookies={ssrCookies}>
<Outlet />
</KheopskitProvider>
<Scripts />
</body>
</html>
);
}| Feature | Client-Only | SSR (with ssrCookies) |
|---|---|---|
| Storage | localStorage | Cookies |
| Server access | ❌ | ✅ |
| Hydration match | ✅ No flash | |
| Size limit | ~5MB | ~4KB |
| Cross-tab sync | storage event |
BroadcastChannel |
Cookie attributes: Kheopskit uses SameSite=Lax, Secure (on HTTPS), path=/, and 1-year expiry.
Cookie size limit (compact format): Cookie storage uses a compact JSON schema to stay under the ~4KB limit. As a rule of thumb, with 6 connected wallets you can fit about 30-40 accounts when most accounts do not include a name. If many accounts have names, expect closer to 25-30. When the cookie grows too large, browsers may reject it.