decane-connect-kit
v1.0.3
Published
Wallet connection for EVM, Solana, Tron, and Bitcoin dApps. Detects installed wallets via EIP-6963, the Solana Wallet Standard, TronLink, and Unisat/OKX/MetaMask Snap. Manages sessions with CAIP-25 and ships a polished wallet selector UI. Works with React
Readme
decane-connect-kit
Wallet connection for EVM, Solana, Tron, and Bitcoin dApps. Detects installed wallets via EIP-6963, the Solana Wallet Standard, TronLink, and Unisat/OKX/MetaMask Snap. Manages sessions with CAIP-25 and ships a polished wallet selector UI. Works with React 18+ in any framework.
Install
npm install decane-connect-kitReact 18 or later is required as a peer dependency.
Quick start
1. Wrap your app
// main.tsx / _app.tsx / layout.tsx
import { DecaneKit } from "decane-connect-kit";
<DecaneKit config={{ theme: "auto" }}>
<App />
</DecaneKit>That's the entire setup. Wallet discovery starts immediately. If the user was previously connected, the session is restored from localStorage automatically.
2. Add a connect button
import { useDecane, useWalletSelector } from "decane-connect-kit";
function ConnectButton() {
const { isConnected, connectedWallet, disconnect } = useDecane();
const { open } = useWalletSelector();
if (isConnected) {
return (
<>
<span>{connectedWallet!.name}</span>
<button onClick={disconnect}>Disconnect</button>
</>
);
}
return <button onClick={open}>Connect Wallet</button>;
}The modal renders itself — there is no <WalletSelector> component to place anywhere.
3. Sign a message
import { useSignMessage } from "decane-connect-kit";
function SignMessage() {
const { signMessage, signature, loading, error } = useSignMessage();
return (
<>
<button onClick={() => signMessage("Hello world")} disabled={loading}>
{loading ? "Signing…" : "Sign"}
</button>
{signature && <p>{signature}</p>}
{error && <p style={{ color: "red" }}>{error.message}</p>}
</>
);
}Chain and account are resolved automatically from the active session. Pass options.chainId to target a specific chain explicitly.
Framework setup
Next.js (App Router)
Client components are required for all hooks. Wrap DecaneKit in a "use client" boundary so the server layout stays a server component.
// src/components/Providers.tsx
"use client";
import { DecaneKit } from "decane-connect-kit";
import type { ReactNode } from "react";
export function Providers({ children }: { children: ReactNode }) {
return (
<DecaneKit config={{ theme: "auto" }}>
{children}
</DecaneKit>
);
}// src/app/layout.tsx
import { Providers } from "@/components/Providers";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<Providers>{children}</Providers>
</body>
</html>
);
}Add "use client" to every component that uses a decane hook.
Add transpilePackages to next.config.ts:
const nextConfig = {
transpilePackages: ["decane-connect-kit"],
};Vite / Create React App
No extra config needed. Wrap your root component with <DecaneKit> and use hooks anywhere inside it.
Configuration
All options are optional.
<DecaneKit config={{
// Which wallet types to show and connect
// "evm" | "solana" | "tron" | "bitcoin" | "all" — default: "all"
networks: "all",
// Restrict which built-in chains are available
// Uses CAIP-2 IDs. Custom chains (via `chains`) are always included.
supportedChainIds: [CHAINS.ETHEREUM, CHAINS.POLYGON, CHAINS.ARBITRUM],
// Custom chains to register at startup
chains: [
{
id: "eip155:31337",
type: "evm",
chainId: 31337,
name: "Hardhat Local",
nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 },
rpcUrl: "http://localhost:8545",
isCustom: true,
},
],
// UI theme — "light" | "dark" | "auto" — default: "auto"
theme: "auto",
// Default CAIP-2 chain ID to request when connecting
defaultChain: "eip155:1",
}}>networks
Controls which wallets appear in the selector and which chain types are offered.
// EVM-only dApp — all other wallets hidden
<DecaneKit config={{ networks: "evm" }}>
// Solana-only dApp
<DecaneKit config={{ networks: "solana" }}>
// Tron-only dApp
<DecaneKit config={{ networks: "tron" }}>
// Bitcoin-only dApp
<DecaneKit config={{ networks: "bitcoin" }}>
// All chains (default)
<DecaneKit config={{ networks: "all" }}>supportedChainIds
Restricts the chain list used throughout the SDK — the wallet selector, provider.chains, and wallet_requestPermissions all respect it.
import { CHAINS } from "decane-connect-kit";
<DecaneKit config={{
supportedChainIds: [CHAINS.ETHEREUM, CHAINS.BASE, CHAINS.SOLANA, CHAINS.TRON, CHAINS.BITCOIN],
}}>Hooks
useDecane()
Reactive state hook. Re-renders on any wallet or session change.
const {
// Detected wallets
wallets, // all wallets
evmWallets, // EVM wallets
solanaWallets, // Solana wallets
tronWallets, // Tron wallets (TronLink)
bitcoinWallets, // Bitcoin wallets (Unisat, OKX, MetaMask Snap)
multiChainWallets, // wallets supporting 2+ chains
// Connection state
isConnected,
connectedWallet, // UnifiedWalletAccount | null
activeSession, // ActiveSession | null
activeChain, // CAIP-2 string | null
// Registered chains
chains, // ChainDescriptor[]
// Theme
theme, // "light" | "dark" | "auto"
setTheme, // (t: Theme) => void
// Actions
connect, // (wallet, preference?) => Promise<void>
disconnect, // () => Promise<void>
addChain, // (chain: ChainDescriptor) => void
removeChain, // (chainId: string) => void
} = useDecane();useWalletSelector()
Controls the wallet modal.
const { isOpen, open, close } = useWalletSelector();
<button onClick={open}>Connect Wallet</button>useSignMessage()
Works on EVM, Solana, Tron, and Bitcoin. The active chain determines the signing method used.
const { signMessage, signature, loading, error, reset } = useSignMessage();
// Uses active chain and account automatically
await signMessage("Hello world");
// Override chain or account
await signMessage("Hello world", { chainId: CHAINS.POLYGON, account: "0xAbc…" });
await signMessage("Hello world", { chainId: CHAINS.TRON });
await signMessage("Hello world", { chainId: CHAINS.BITCOIN });useSendTransaction()
Works on EVM, Solana, Tron, and Bitcoin.
const { sendTransaction, txHash, loading, error, reset } = useSendTransaction();
// EVM — value in wei
await sendTransaction({
to: "0xRecipient",
value: "1000000000000000", // decimal string or hex
data: "0x", // optional calldata
gas: "21000", // optional
});
// Tron — value in SUN (1 TRX = 1,000,000 SUN)
await sendTransaction({ to: "TRecipient…", value: "1000000" }, { chainId: CHAINS.TRON });
// Bitcoin — value in satoshis
await sendTransaction({ to: "bc1q…", value: "10000" }, { chainId: CHAINS.BITCOIN });
// Override chain
await sendTransaction({ to: "0xRecipient" }, { chainId: CHAINS.ARBITRUM });useSignTypedData()
EVM only. Throws DecaneErrorCode.EVM_ONLY_METHOD if called on a non-EVM chain.
const { signTypedData, signature, loading, error, reset } = useSignTypedData();
await signTypedData({
domain: { name: "MyApp", version: "1", chainId: 1 },
types: { Permit: [{ name: "owner", type: "address" }, …] },
message: { owner: "0xAbc…", spender: "0xDef…", value: "1000" },
});useChainSwitch()
Switch the active chain within the current session.
const { switchChain, activeChain, grantedChains, loading, error } = useChainSwitch();
// grantedChains — CAIP-2 IDs granted in the current session
await switchChain(CHAINS.POLYGON);
await switchChain(CHAINS.TRON);
await switchChain(CHAINS.BITCOIN);Wallet detection
EVM
Detected via EIP-6963 — standard event-based discovery. All compliant wallets (MetaMask, Rabby, Coinbase Wallet, Brave, OKX, and others) are discovered automatically. window.ethereum is never used.
Solana
Detected via the Wallet Standard (@wallet-standard/app). Wallets must expose standard:connect, solana:signTransaction, and solana:signMessage. Multi-chain wallets like Phantom and Backpack are merged into a single entry when detected on both EVM and Solana.
Tron
Detected via TronLink's injected provider (window.tronLink). The SDK listens for tronLink#initialized and polls for up to 3 seconds to handle extensions that load after the page. Transactions are built via window.tronWeb.transactionBuilder when available, falling back to the TronGrid HTTP API.
Bitcoin
Three detection paths run in parallel:
| Wallet | Detection |
|--------|-----------|
| Unisat | window.unisat injected provider |
| OKX Wallet | window.okxwallet.bitcoin — shares rdns with OKX EVM, auto-merges to give EVM + Bitcoin support |
| MetaMask Bitcoin Snap | After each EVM wallet is discovered, wallet_getSnaps is called silently. If npm:@metamask/bitcoin-snap or npm:@consensys/btc-snap is enabled, the wallet gains Bitcoin support with no user interaction |
Chain constants
Use CHAINS instead of bare strings to get autocomplete and catch typos at compile time.
import { CHAINS, type ChainId } from "decane-connect-kit";
// Tier-1 EVM
CHAINS.ETHEREUM // "eip155:1"
CHAINS.BASE // "eip155:8453"
CHAINS.ARBITRUM // "eip155:42161"
CHAINS.OPTIMISM // "eip155:10"
CHAINS.BNB // "eip155:56"
CHAINS.POLYGON // "eip155:137"
// Extended EVM (selected)
CHAINS.ABSTRACT // "eip155:2741"
CHAINS.APE_CHAIN // "eip155:33139"
CHAINS.AVALANCHE // "eip155:43114"
CHAINS.BERACHAIN // "eip155:80094"
CHAINS.BLAST // "eip155:81457"
CHAINS.CELO // "eip155:42220"
CHAINS.GNOSIS // "eip155:100"
CHAINS.LINEA // "eip155:59144"
CHAINS.MANTLE // "eip155:5000"
CHAINS.SCROLL // "eip155:534352"
CHAINS.SONIC // "eip155:146"
CHAINS.TAIKO // "eip155:167000"
CHAINS.UNICHAIN // "eip155:130"
CHAINS.WORLD_CHAIN // "eip155:480"
CHAINS.ZKSYNC_ERA // "eip155:324"
CHAINS.ZORA // "eip155:7777777"
// …58 chains total — see Built-in chains table below
// Dev
CHAINS.LOCALHOST // "eip155:31337"
// Solana
CHAINS.SOLANA // "solana:mainnet"
CHAINS.SOLANA_DEVNET // "solana:devnet"
// Tron
CHAINS.TRON // "tron:mainnet"
CHAINS.TRON_SHASTA // "tron:shasta"
// Bitcoin
CHAINS.BITCOIN // "bip122:mainnet"ChainId is the union type of all built-in chain ID strings.
Custom chains
Add any EVM chain (local Hardhat, private L2) or Solana network at runtime.
const { addChain, removeChain } = useDecane();
// Add a local Hardhat node
addChain({
id: "eip155:31337",
type: "evm",
chainId: 31337,
name: "Hardhat Local",
nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 },
rpcUrl: "http://localhost:8545",
isCustom: true,
});
// Add a custom Solana network
addChain({
id: "solana:localnet",
type: "solana",
name: "Solana Localnet",
nativeCurrency: { name: "SOL", symbol: "SOL", decimals: 9 },
rpcUrl: "http://localhost:8899",
isCustom: true,
});
// Remove a custom chain (built-in chains cannot be removed)
removeChain("eip155:31337");Custom chains can also be registered at init via config.chains. When addChain is called while a wallet is connected, it automatically calls wallet_addEthereumChain on the wallet so it knows about the network.
Error handling
All hooks return error: DecaneError | null. Branch on error.code instead of parsing error strings.
import { DecaneError, DecaneErrorCode } from "decane-connect-kit";
const { signMessage, error } = useSignMessage();
if (error) {
switch (error.code) {
case DecaneErrorCode.USER_REJECTED:
// User dismissed the wallet prompt — not an error, just reset
break;
case DecaneErrorCode.NO_ACTIVE_SESSION:
// Prompt the user to connect a wallet first
break;
case DecaneErrorCode.CHAIN_NOT_IN_SCOPE:
// Call switchChain() to add this chain to the session
break;
case DecaneErrorCode.SCOPE_MISMATCH:
// The account isn't in the current session
break;
default:
console.error(error.message);
}
}Error codes
| Code | Value | When thrown |
|------|-------|-------------|
| USER_REJECTED | 4001 | User dismissed the wallet prompt |
| UNAUTHORIZED | 4100 | Wallet refused the request |
| NO_ACTIVE_SESSION | 5001 | Signing called before connecting |
| SCOPE_MISMATCH | 5002 | Account not in session scope |
| SESSION_EXPIRED | 5003 | Session is no longer valid |
| UNSUPPORTED_CHAIN | 5010 | Chain ID not in registry |
| CHAIN_NOT_IN_SCOPE | 5011 | Chain not granted in session |
| INVALID_CHAIN_CONFIG | 5012 | Custom chain descriptor is malformed |
| NO_WALLET_CONNECTED | 5020 | Action requires a connected wallet |
| WALLET_DISCONNECTED | 5021 | Wallet disconnected mid-session |
| PROVIDER_ERROR | 5022 | Underlying wallet threw an error |
| METHOD_NOT_SUPPORTED | 5030 | Method not recognised |
| EVM_ONLY_METHOD | 5031 | wallet_signTypedData on a non-EVM chain |
Theme
The modal theme is controlled through DecaneKit config and toggled at runtime via useDecane.
// Follow system preference (default)
<DecaneKit config={{ theme: "auto" }}>
// Force dark
<DecaneKit config={{ theme: "dark" }}>// Toggle at runtime — the modal follows automatically
const { theme, setTheme } = useDecane();
<button onClick={() => setTheme(theme === "dark" ? "light" : "dark")}>
Toggle theme
</button>To apply the same tokens to your own app UI, the SDK injects CSS custom properties into document.head whenever the modal opens:
var(--dc-bg)
var(--dc-surface)
var(--dc-border)
var(--dc-text)
var(--dc-text-secondary)
var(--dc-text-muted)
var(--dc-accent) /* gold */
var(--dc-accent-hover)
var(--dc-accent-text)
var(--dc-danger)
var(--dc-success)TypeScript
The package ships full type declarations. Key types:
import type {
DecaneProvider, // full provider interface
DecaneConfig, // config passed to <DecaneKit>
UnifiedWalletAccount, // a detected wallet entry
ChainDescriptor, // chain definition object
ActiveSession, // CAIP-25 session state
ChainScope, // methods + accounts for one chain
ChainType, // "evm" | "solana" | "tron" | "bitcoin"
ChainId, // union of all built-in CAIP-2 chain IDs
Theme, // "light" | "dark" | "auto"
TxParams, // params for useSendTransaction
CAIP25Method, // all supported method strings
} from "decane-connect-kit";What is not included
- Social sign-in or email login
- Any backend, server, or API calls (except the TronGrid fallback when TronWeb is unavailable)
window.ethereum— detection uses EIP-6963 exclusively- Private key or seed phrase management
- State management libraries (Redux, Zustand) — pure React context
Built-in chains
The following chains are available out of the box. Pass supportedChainIds in config to restrict to a subset.
Tier-1 EVM
| Name | CAIP-2 ID | CHAINS constant |
|------|-----------|-------------------|
| Ethereum | eip155:1 | CHAINS.ETHEREUM |
| Base | eip155:8453 | CHAINS.BASE |
| Arbitrum One | eip155:42161 | CHAINS.ARBITRUM |
| Optimism | eip155:10 | CHAINS.OPTIMISM |
| BNB Chain | eip155:56 | CHAINS.BNB |
| Polygon | eip155:137 | CHAINS.POLYGON |
Extended EVM
| Name | CAIP-2 ID | CHAINS constant |
|------|-----------|-------------------|
| Abstract | eip155:2741 | CHAINS.ABSTRACT |
| Ancient8 | eip155:888888888 | CHAINS.ANCIENT8 |
| Animechain | eip155:69000 | CHAINS.ANIMECHAIN |
| ApeChain | eip155:33139 | CHAINS.APE_CHAIN |
| Arbitrum Nova | eip155:42170 | CHAINS.ARBITRUM_NOVA |
| Avalanche | eip155:43114 | CHAINS.AVALANCHE |
| B3 | eip155:8333 | CHAINS.B3 |
| Berachain | eip155:80094 | CHAINS.BERACHAIN |
| Blast | eip155:81457 | CHAINS.BLAST |
| BOB | eip155:60808 | CHAINS.BOB |
| Boba Network | eip155:288 | CHAINS.BOBA |
| Celo | eip155:42220 | CHAINS.CELO |
| Corn | eip155:21000000 | CHAINS.CORN |
| Cronos | eip155:25 | CHAINS.CRONOS |
| Cyber | eip155:7560 | CHAINS.CYBER |
| Degen | eip155:666666666 | CHAINS.DEGEN |
| Flow EVM | eip155:747 | CHAINS.FLOW_EVM |
| Gnosis | eip155:100 | CHAINS.GNOSIS |
| Gunz | eip155:49321 | CHAINS.GUNZ |
| Hemi | eip155:43111 | CHAINS.HEMI |
| HyperEVM | eip155:999 | CHAINS.HYPER_EVM |
| Ink | eip155:57073 | CHAINS.INK |
| Linea | eip155:59144 | CHAINS.LINEA |
| Lisk | eip155:1135 | CHAINS.LISK |
| Manta Pacific | eip155:169 | CHAINS.MANTA_PACIFIC |
| Mantle | eip155:5000 | CHAINS.MANTLE |
| MegaETH | eip155:6342 | CHAINS.MEGA_ETH |
| Metis | eip155:1088 | CHAINS.METIS |
| Mode | eip155:34443 | CHAINS.MODE |
| Monad | eip155:10143 | CHAINS.MONAD |
| Morph | eip155:2818 | CHAINS.MORPH |
| Plume | eip155:98866 | CHAINS.PLUME |
| RARI | eip155:1380012617 | CHAINS.RARI |
| Redstone | eip155:690 | CHAINS.REDSTONE |
| Ronin | eip155:2020 | CHAINS.RONIN |
| Scroll | eip155:534352 | CHAINS.SCROLL |
| Sei | eip155:1329 | CHAINS.SEI |
| Shape | eip155:360 | CHAINS.SHAPE |
| Somnia | eip155:50312 | CHAINS.SOMNIA |
| Soneium | eip155:1868 | CHAINS.SONEIUM |
| Sonic | eip155:146 | CHAINS.SONIC |
| Story | eip155:1514 | CHAINS.STORY |
| Superposition | eip155:55244 | CHAINS.SUPERPOSITION |
| Superseed | eip155:5330 | CHAINS.SUPERSEED |
| SwellChain | eip155:1923 | CHAINS.SWELL_CHAIN |
| Taiko | eip155:167000 | CHAINS.TAIKO |
| Unichain | eip155:130 | CHAINS.UNICHAIN |
| World Chain | eip155:480 | CHAINS.WORLD_CHAIN |
| ZERO | eip155:543210 | CHAINS.ZERO |
| Zircuit | eip155:48900 | CHAINS.ZIRCUIT |
| zkSync Era | eip155:324 | CHAINS.ZKSYNC_ERA |
| Zora | eip155:7777777 | CHAINS.ZORA |
Dev / local
| Name | CAIP-2 ID | CHAINS constant |
|------|-----------|-------------------|
| Localhost | eip155:31337 | CHAINS.LOCALHOST |
Solana
| Name | CAIP-2 ID | CHAINS constant |
|------|-----------|-------------------|
| Solana | solana:mainnet | CHAINS.SOLANA |
| Solana Devnet | solana:devnet | CHAINS.SOLANA_DEVNET |
Tron
| Name | CAIP-2 ID | CHAINS constant |
|------|-----------|-------------------|
| Tron | tron:mainnet | CHAINS.TRON |
| Tron Shasta Testnet | tron:shasta | CHAINS.TRON_SHASTA |
Bitcoin
| Name | CAIP-2 ID | CHAINS constant |
|------|-----------|-------------------|
| Bitcoin | bip122:mainnet | CHAINS.BITCOIN |
