bitcoin-wallet-adapter
v1.15.0
Published
Bitcoin wallet adapter using MUI components
Downloads
25
Maintainers
Readme
🌟 Bitcoin Wallet Adapter
A production-ready React library for Bitcoin wallet integration with professional UI/UX, comprehensive error handling, and support for 6+ popular Bitcoin wallets. Built with TypeScript, Redux, and modern React patterns. 🚀
✨ Latest Features
- 🎨 Professional UI/UX - High-contrast, accessible design with Tailwind CSS
- ⚡ Real-time Balance Tracking - Pending transaction awareness and accurate balance display
- 💸 BTC Payment Integration - Built-in
usePayBTChook with transaction ID capture - 🔒 Professional Error Handling - Comprehensive error management with detailed feedback
- 📱 6+ Wallet Support - Unisat, Xverse, Leather, MagicEden, Phantom, OKX
- 🌐 Mainnet Ready - Optimized for production Bitcoin mainnet usage
- 🔌 Auto-disconnect on Network Change - When you switch between mainnet/testnet, any connected wallet is automatically disconnected to prevent cross-network mismatches
- 🖊️ Sign-before-persist Sessions - Wallet connections are only persisted after a successful BIP-322 signature. Sessions are verified on restore (signature + network + TTL)
🧙♂️ Tech Stack
- ⚛️ Frontend: React 18, TypeScript, Material-UI
- 🔄 State Management: Redux Toolkit with RTK Query
- 🎨 Styling: Tailwind CSS, CSS-in-JS support
- 🔐 Security: Network isolation, error boundaries, input validation
- 📦 Build: Modern bundling with tree-shaking support
🚀 Quick Start
Installation
npm install bitcoin-wallet-adapter
# or
yarn add bitcoin-wallet-adapter
# or
pnpm add bitcoin-wallet-adapterComplete Working Example
Here's a complete, production-ready implementation you can copy and use immediately:
import React, { useState } from 'react';
import {
WalletProvider,
ConnectMultiButton,
useWalletAddress,
useWalletBalance,
usePayBTC,
useMessageSign,
Notification
} from 'bitcoin-wallet-adapter';
// 🎯 Main App Component
function App() {
return (
<WalletProvider
customAuthOptions={{
network: "mainnet", // Production ready
appDetails: {
name: "My Bitcoin App",
icon: "/logo.png"
}
}}
>
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-blue-50 p-6">
<header className="text-center mb-8">
<h1 className="text-4xl font-bold text-slate-800 mb-4">
🌟 My Bitcoin Wallet App
</h1>
{/* 🔌 Connect Button - Always visible for persistence */}
<ConnectMultiButton
buttonClassname="bg-blue-600 hover:bg-blue-700 text-white px-8 py-3 rounded-xl font-semibold shadow-lg transition-all"
modalContainerClass="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"
modalContentClass="bg-white rounded-2xl shadow-2xl max-w-md w-full mx-4 p-6"
supportedWallets={["unisat", "xverse", "leather", "magiceden", "phantom", "okx"]}
/>
</header>
{/* 📊 Wallet Dashboard */}
<WalletDashboard />
{/* 💸 Payment Interface */}
<PaymentInterface />
{/* 🔔 Notifications */}
<Notification />
</div>
</WalletProvider>
);
}
// 📊 Wallet Dashboard Component
function WalletDashboard() {
const walletDetails = useWalletAddress();
const { balance, btcPrice, refreshBalance, loading } = useWalletBalance();
if (!walletDetails.cardinal) {
return (
<div className="max-w-2xl mx-auto">
<div className="bg-white rounded-xl shadow-lg p-8 text-center">
<div className="text-6xl mb-4">🔐</div>
<h2 className="text-2xl font-bold text-slate-800 mb-2">
Wallet Not Connected
</h2>
<p className="text-slate-600">
Connect your Bitcoin wallet to see your balance and start transacting.
</p>
</div>
</div>
);
}
return (
<div className="max-w-4xl mx-auto mb-8">
<div className="grid md:grid-cols-2 gap-6">
{/* 💰 Balance Card */}
<div className="bg-white rounded-xl shadow-lg p-6">
<div className="flex items-center justify-between mb-4">
<h3 className="text-xl font-bold text-slate-800">💰 Balance</h3>
<button
onClick={refreshBalance}
disabled={loading}
className="text-blue-600 hover:text-blue-700 font-medium disabled:opacity-50"
>
{loading ? '⏳' : '🔄'} Refresh
</button>
</div>
<div className="space-y-3">
<div className="flex justify-between items-center">
<span className="text-slate-600">Confirmed:</span>
<span className="font-bold text-2xl text-emerald-600">
{(balance?.confirmed || 0).toFixed(8)} BTC
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-slate-600">USD Value:</span>
<span className="font-semibold text-slate-800">
${((balance?.confirmed || 0) * btcPrice).toFixed(2)}
</span>
</div>
{(balance?.unconfirmed || 0) > 0 && (
<div className="flex justify-between items-center">
<span className="text-slate-600">Pending:</span>
<span className="font-medium text-orange-600">
{balance.unconfirmed.toFixed(8)} BTC
</span>
</div>
)}
</div>
</div>
{/* 🔗 Wallet Info Card */}
<div className="bg-white rounded-xl shadow-lg p-6">
<h3 className="text-xl font-bold text-slate-800 mb-4">🔗 Wallet Info</h3>
<div className="space-y-3">
<div>
<span className="text-slate-600 text-sm">Connected Wallet:</span>
<p className="font-bold text-lg text-blue-600">
{walletDetails.wallet || 'Unknown'}
</p>
</div>
<div>
<span className="text-slate-600 text-sm">Cardinal Address:</span>
<p className="font-mono text-sm bg-slate-100 p-2 rounded truncate">
{walletDetails.cardinal}
</p>
</div>
{walletDetails.ordinal && (
<div>
<span className="text-slate-600 text-sm">Ordinal Address:</span>
<p className="font-mono text-sm bg-slate-100 p-2 rounded truncate">
{walletDetails.ordinal}
</p>
</div>
)}
</div>
</div>
</div>
</div>
);
}
// 💸 Payment Interface Component
function PaymentInterface() {
const walletDetails = useWalletAddress();
const { payBTC, loading, error } = usePayBTC();
const [recipient, setRecipient] = useState(
localStorage.getItem('btc-recipient') || ''
);
const [amount, setAmount] = useState(1000); // satoshis
const [lastTxId, setLastTxId] = useState<string | null>(null);
if (!walletDetails.cardinal) return null;
const handlePayment = async () => {
try {
const result = await payBTC({
address: recipient,
amount: amount
});
// Handle both string (txId) and object responses
const txId = typeof result === 'string' ? result : result.txid || result.transactionId;
setLastTxId(txId);
// Save recipient for next time
localStorage.setItem('btc-recipient', recipient);
alert(`✅ Payment sent! Transaction ID: ${txId}`);
} catch (err: any) {
console.error('Payment failed:', err);
alert(`❌ Payment failed: ${err.message}`);
}
};
return (
<div className="max-w-2xl mx-auto">
<div className="bg-white rounded-xl shadow-lg p-6">
<h3 className="text-xl font-bold text-slate-800 mb-6">💸 Send Bitcoin</h3>
<div className="space-y-4">
<div>
<label className="block text-slate-700 font-medium mb-2">
Recipient Address:
</label>
<input
type="text"
value={recipient}
onChange={(e) => setRecipient(e.target.value)}
placeholder="bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh"
className="w-full px-4 py-3 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
</div>
<div>
<label className="block text-slate-700 font-medium mb-2">
Amount (satoshis):
</label>
<input
type="number"
value={amount}
onChange={(e) => setAmount(parseInt(e.target.value) || 0)}
min="546"
className="w-full px-4 py-3 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
<p className="text-sm text-slate-500 mt-1">
≈ {(amount / 100000000).toFixed(8)} BTC
</p>
</div>
<button
onClick={handlePayment}
disabled={loading || !recipient || amount < 546}
className="w-full bg-emerald-600 hover:bg-emerald-700 disabled:bg-slate-400 text-white py-3 rounded-lg font-semibold transition-colors"
>
{loading ? '⏳ Processing...' : '💸 Send Bitcoin'}
</button>
{error && (
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
<p className="text-red-800 font-medium">❌ Error: {error.message}</p>
</div>
)}
{lastTxId && (
<div className="bg-emerald-50 border border-emerald-200 rounded-lg p-4">
<p className="text-emerald-800 font-medium">✅ Transaction sent!</p>
<p className="text-sm text-emerald-600 font-mono break-all">
{lastTxId}
</p>
</div>
)}
</div>
</div>
</div>
);
}
export default App;🎯 Key Implementation Features
- 🔌 Persistent Connection: ConnectMultiButton stays visible for wallet persistence after page refresh
- ⚡ Real-time Balance: Automatic balance updates with pending transaction awareness
- 💸 Payment Integration: Complete BTC payment flow with transaction ID capture
- 💾 Local Storage: Remembers recipient addresses between sessions
- 🎨 Professional UI: High-contrast, accessible design with Tailwind CSS
- 🔒 Error Handling: Comprehensive error management with user-friendly messages
- 📱 Multi-wallet Support: Works with Unisat, Xverse, Leather, MagicEden, Phantom, OKX
📋 Latest Project Status (v1.8.2)
✅ Completed Enhancements
- Demo Application Excellence: Professional UI/UX redesign with high-contrast accessibility
- Payment System: Full BTC payment integration with transaction ID capture and pending balance awareness
- Error Handling: Comprehensive error management with user-friendly feedback across all wallet operations
- Architecture Documentation: Complete technical analysis and network switching constraint documentation
- Multi-wallet Support: Production-ready support for 6+ major Bitcoin wallets
- Mainnet Optimization: Optimized for production Bitcoin mainnet usage with proper network isolation
🔍 Architectural Insights
- Network Management: Network is managed via the library's internal Redux store. Set an initial network with
WalletProvider'scustomAuthOptions.networkand change it at runtime by dispatching thesetNetwork('mainnet'|'testnet')action or using theuseNetwork()hook. - Hook Design: All hooks (
useWalletBalance,usePayBTC,useMessageSign,useWalletConnect) read the network from Redux and do not accept a network parameter. - Module Boundaries: Do not pass
networkprops to components. Use the Redux-managed network only. - Security: Proper network isolation prevents cross-network transaction issues
🚀 Production Ready Features
- Professional Error System: BWAError class with severity levels and detailed error tracking
- Real-time Balance: Accurate balance display with pending transaction awareness
- Transaction Management: Complete payment flow with proper transaction ID handling
- Persistent Sessions: Wallet connections persist across page refreshes
- Responsive Design: Mobile-friendly interface with Tailwind CSS integration
🛠 Usage
🏛 WalletProvider
Wrap your application with the WalletProvider to enable wallet functionalities:
import { WalletProvider } from "bitcoin-wallet-adapter";
function App() {
return (
<WalletProvider customAuthOptions={/* optional auth options */}>
<YourApp />
</WalletProvider>
);
}WalletProvider Props
| Prop | Type | Description | | ----------------- | -------------------------- | -------------------------------------------------- | | children | ReactNode | The child components to be wrapped by the provider | | customAuthOptions | AuthOptionsArgs (optional) | Custom authentication options |
The AuthOptionsArgs type includes:
interface AuthOptionsArgs {
manifestPath?: string;
redirectTo?: string;
appDetails?: {
name?: string;
icon?: string;
};
}📝 Note: The
appDetailsfield incustomAuthOptionsis primarily used by the Leather wallet.
🔗 ConnectMultiButton
Use the ConnectMultiButton component to render a multi-wallet connect button:
import { ConnectMultiButton } from "bitcoin-wallet-adapter";
function WalletConnect() {
return (
<ConnectMultiButton
buttonClassname="custom-button-class"
modalContainerClass="custom-modal-container"
modalContentClass="custom-modal-content"
closeButtonClass="custom-close-button"
headingClass="custom-heading"
walletItemClass="custom-wallet-item"
walletImageClass="custom-wallet-image"
walletLabelClass="custom-wallet-label"
InnerMenu={CustomInnerMenu}
icon="custom-icon.png"
iconClass="custom-icon-class"
balance={1000}
connectionMessage="Please sign this message to authenticate your wallet ownership"
fractal={true}
supportedWallets={["unisat", "xverse", "leather"]}
/>
);
}ConnectMultiButton Props
| Prop | Type | Description | | ------------------- | ---------------------------------------------- | ------------------------------------------------------------ | | buttonClassname | string (optional) | Custom class for the main button | | modalContainerClass | string (optional) | Custom class for the modal container | | modalContentClass | string (optional) | Custom class for the modal content | | closeButtonClass | string (optional) | Custom class for the close button | | headingClass | string (optional) | Custom class for the modal heading | | walletItemClass | string (optional) | Custom class for each wallet item in the list | | walletImageClass | string (optional) | Custom class for wallet logos | | walletLabelClass | string (optional) | Custom class for wallet labels | | InnerMenu | React.ComponentType (optional) | Custom component for the inner menu when wallet is connected | | icon | string (optional) | Custom logo URL of your application | | iconClass | string (optional) | Custom class for the icon | | balance | number (optional) | Wallet balance to display | | connectionMessage | string (optional) | Custom message for wallet connection/signing authentication | | fractal | boolean (optional) | Show only fractal supporting wallets (Unisat | OKX) | | supportedWallets | string[] (optional) | Array of wallet names to be supported in the dApp | | onSignatureCapture | function (optional) | Callback function to capture wallet signature data after successful connection |
The supportedWallets prop allows you to specify which wallets you want to support in your dApp. Pass an array of wallet names (in lowercase) to filter the available wallets. For example:
supportedWallets={["unisat", "xverse", "leather"]}This will only show the Unisat, Xverse, and Leather wallet options in the connect modal, even if other wallets are installed in the user's browser.
🧩 Build a Custom Connect Button (Headless)
Use the headless hook useWalletConnect() to design your own connect UI while using our logic.
// What happens in this file:
// - Implements a fully custom connect button using the headless `useWalletConnect` hook
// - Lists available wallets, connects/disconnects, shows basic errors
// - Uses Tailwind classes and includes null checks per best practices
import React from 'react';
import {
WalletProvider,
useWalletConnect,
useWalletAddress,
useNetwork,
Notification,
} from 'bitcoin-wallet-adapter';
function CustomConnectButton() {
const {
isConnected,
isLoading,
error,
currentWallet,
availableWallets,
connect,
disconnect,
checkAvailableWallets,
} = useWalletConnect();
const walletAddr = useWalletAddress();
React.useEffect(() => {
checkAvailableWallets();
}, [checkAvailableWallets]);
if (isConnected) {
return (
<div className="inline-flex items-center gap-3 bg-white border border-slate-200 rounded-xl px-4 py-2 shadow-sm">
<div className="text-sm">
<div className="font-semibold text-slate-800">
{currentWallet?.wallet || 'Wallet'}
</div>
<div className="font-mono text-xs text-slate-500 max-w-[220px] truncate">
{walletAddr?.cardinal ?? '—'}
</div>
</div>
<button
onClick={disconnect}
className="bg-slate-800 hover:bg-slate-900 text-white text-sm font-semibold px-3 py-2 rounded-lg disabled:opacity-50"
>
Disconnect
</button>
</div>
);
}
return (
<div className="relative inline-block">
<button
onClick={checkAvailableWallets}
disabled={isLoading}
className="bg-blue-600 hover:bg-blue-700 text-white text-sm font-semibold px-4 py-2 rounded-lg shadow disabled:opacity-50"
>
{isLoading ? 'Checking wallets…' : 'Connect Wallet'}
</button>
<div className="mt-2 w-64 bg-white border border-slate-200 rounded-xl shadow p-2">
{(availableWallets?.length ?? 0) > 0 ? (
<ul className="space-y-1">
{availableWallets!.map((w) => (
<li key={w.connector}>
<button
onClick={() => connect(w.connector)}
disabled={isLoading}
className="flex items-center gap-2 w-full text-left px-3 py-2 rounded-lg hover:bg-slate-50 disabled:opacity-50"
>
{w.logo && <img src={w.logo} alt={w.label} className="h-5 w-5" />}
<span className="text-sm text-slate-800">{w.label}</span>
</button>
</li>
))}
</ul>
) : (
<div className="text-sm text-slate-500 px-3 py-2">No supported wallets detected.</div>
)}
{error && (
<div className="mt-2 text-sm text-red-700 bg-red-50 border border-red-200 rounded-lg px-3 py-2">
{error.message}
</div>
)}
</div>
</div>
);
}
function NetworkSwitcher() {
const { network, setNetwork, toggle } = useNetwork();
return (
<div className="inline-flex rounded-lg overflow-hidden border border-slate-200 bg-slate-50">
<button
onClick={() => setNetwork('mainnet')}
className={`px-3 py-2 text-sm font-semibold ${
network === 'mainnet' ? 'bg-emerald-600 text-white' : 'bg-white text-slate-700'
}`}
>
Mainnet
</button>
<button
onClick={() => setNetwork('testnet')}
className={`px-3 py-2 text-sm font-semibold border-l border-slate-200 ${
network === 'testnet' ? 'bg-blue-600 text-white' : 'bg-white text-slate-700'
}`}
>
Testnet
</button>
<button onClick={toggle} className="px-3 py-2 text-xs text-blue-600 underline">
Toggle
</button>
</div>
);
}
export default function App() {
return (
<WalletProvider
customAuthOptions={{
network: 'mainnet',
appDetails: { name: 'My App', icon: '/logo.png' },
}}
>
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-blue-50 p-6 space-y-6">
<div className="flex items-center gap-4">
<CustomConnectButton />
<NetworkSwitcher />
</div>
<Notification />
</div>
</WalletProvider>
);
}🔐 Signature Handling & Capture
Wallet connections require a BIP-322 signature. After a successful sign-in:
- The library persists the session only after signature verification.
- The signature is stored in Redux and exposed in the headless hook return.
useWalletConnect() now returns signature so you can access it anywhere:
import { useWalletConnect } from 'bitcoin-wallet-adapter';
const { isConnected, signature, currentWallet } = useWalletConnect();
if (isConnected && signature) {
// send signature to your backend for auth, etc.
}ConnectMultiButton delegates connection logic to useWalletConnect() and uses the hook’s signature to invoke onSignatureCapture.
Signature Capture Example
<ConnectMultiButton
onSignatureCapture={(signatureData) => {
console.log('Signature captured:', signatureData);
// Process signature data for your application
// signatureData contains: signature, message, address, wallet, network
}}
/>Signature Data Structure
The callback receives a signature data object with the following properties:
interface SignatureData {
signature: string; // The BIP-322 signature string
message: string; // The signed message
address: string; // Bitcoin address used for signing
wallet: string; // Wallet type (e.g., "Unisat", "Xverse", "Leather")
network: string; // Network used ("mainnet" or "testnet")
}🔐 Session Security
- Sessions are saved only after successful signature.
- On restore, the adapter verifies:
- Signature matches the saved message and address
- Address matches the active Redux network
- Message TTL is fresh (≤ 24h)
🧼 Production Logging
- Non-error logs are disabled in production throughout the library. Dev-only logs are gated by
NODE_ENV !== 'production'.
Security Notes
- The signature is only captured after successful BIP-322 verification
- All signatures are verified before the callback is invoked
- The callback is only called once per successful wallet connection
Custom Sign-In Messages
The connectionMessage prop allows you to customize the message that users sign when connecting their wallet for authentication. This is particularly useful for branding or providing specific context about the sign-in process.
Default Message
If no connectionMessage is provided, a default message is generated that includes:
- The user's Bitcoin address
- Welcome text with your application name
- URI and version information
- Timestamp
Custom Message Example
<ConnectMultiButton
connectionMessage="Welcome to MyApp! Please sign this message to verify wallet ownership and authenticate your session."
/>Security Note
The connection message is used for wallet authentication and signature verification. Ensure your custom message is clear about its purpose and maintains security best practices.
📝 Note: If
supportedWalletsis not provided, all available wallets will be shown.
🌐 Network Switching (Mainnet/Testnet)
Use the useNetwork() hook to read and change the active network globally. All hooks automatically pick up the change from the Redux store.
import { useNetwork } from 'bitcoin-wallet-adapter';
export function NetworkSwitcher() {
const { network, setNetwork, toggle } = useNetwork();
return (
<div className="inline-flex rounded-lg overflow-hidden border border-slate-200 bg-slate-50">
<button
onClick={() => setNetwork('mainnet')}
className={`px-3 py-2 text-sm font-semibold ${network === 'mainnet' ? 'bg-emerald-600 text-white' : 'bg-white text-slate-700'}`}
>
Mainnet
</button>
<button
onClick={() => setNetwork('testnet')}
className={`px-3 py-2 text-sm font-semibold border-l border-slate-200 ${network === 'testnet' ? 'bg-blue-600 text-white' : 'bg-white text-slate-700'}`}
>
Testnet
</button>
<button onClick={toggle} className="px-3 py-2 text-xs text-blue-600 underline">
Toggle
</button>
</div>
);
}Recommended placement: near the wallet connect UI so users can switch before connecting. No network props are needed anywhere else.
Behavior on Network Change
- When the active network in Redux changes (via
useNetwork().setNetwork,toggle, or initialcustomAuthOptions.network), the adapter will automatically disconnect the currently connected wallet. - This prevents accidental cross-network actions (e.g., signing on testnet with a mainnet session) and ensures a clean, explicit reconnection on the new network.
Testnet Wallet Availability
- The Connect modal only shows wallets compatible with the active network and detected in the browser. Availability may vary by extension and version.
🔔 Notification
Add the Notification component to your app to display wallet-related notifications:
import { Notification } from "bitcoin-wallet-adapter";
function App() {
return (
<div>
<YourAppContent />
<Notification />
</div>
);
}🪝 API Reference
Core Hooks
import {
useWalletAddress, // Get wallet connection details
useWalletBalance, // Get balance and BTC price
useMessageSign, // Sign messages
usePayBTC // Send Bitcoin payments
} from "bitcoin-wallet-adapter";
// Get wallet info
const walletDetails = useWalletAddress();
console.log(walletDetails.cardinal); // Payment address
console.log(walletDetails.wallet); // Wallet name
// Get balance
const { balance, btcPrice } = useWalletBalance();
console.log(balance.confirmed); // Confirmed BTC
// Sign message
const { signMessage } = useMessageSign();
const signature = await signMessage({
message: "Hello Bitcoin!",
address: walletDetails.cardinal,
wallet: walletDetails.wallet
});
// Send payment
const { payBTC } = usePayBTC();
const txId = await payBTC({
address: "bc1q...",
amount: 1000, // satoshis
});🚨 Professional Error Handling System
Bitcoin Wallet Adapter features a comprehensive, professional-grade error handling system that provides consistent error management, automatic notifications, and detailed error tracking across all wallet operations.
🔧 Core Error Handling Components
BWAError Class
Custom error class with professional error codes, severity levels, and contextual information:
import { throwBWAError, BWAErrorCode, BWAErrorSeverity } from 'bitcoin-wallet-adapter';
// Professional error with all context
throwBWAError(
BWAErrorCode.WALLET_NOT_CONNECTED,
"No wallet is currently connected. Please connect a wallet first.",
{
severity: BWAErrorSeverity.HIGH,
context: {
operation: 'message_signing',
additionalData: { expectedWallet: 'Unisat' }
}
}
);Error Codes & Severity Levels
Error Codes:
WALLET_NOT_CONNECTED- No wallet connectedWALLET_NOT_SUPPORTED- Unsupported wallet typeTRANSACTION_SIGNING_FAILED- Transaction signing errorsMESSAGE_SIGNING_FAILED- Message signing errorsSIGNATURE_VERIFICATION_FAILED- Signature verification errorsPSBT_INVALID- Invalid PSBT formatNETWORK_MISMATCH- Network configuration mismatchVALIDATION_ERROR- Input validation errorsCONNECTION_FAILED- Wallet connection failuresINSUFFICIENT_BALANCE- Insufficient wallet balanceUSER_REJECTED- User canceled operation
Severity Levels:
LOW- Minor issues, recoverableMEDIUM- Moderate issues requiring attentionHIGH- Critical errors requiring immediate action
🛠️ Error Handling Functions
throwBWAError()
Primary function for throwing professional errors with automatic notification dispatch:
import { throwBWAError, BWAErrorCode, BWAErrorSeverity } from 'bitcoin-wallet-adapter';
// Throws error AND triggers snackbar notifications
throwBWAError(
BWAErrorCode.WALLET_NOT_SUPPORTED,
"Phantom wallet does not support this operation",
{
severity: BWAErrorSeverity.HIGH,
context: {
operation: 'btc_payment',
walletType: 'Phantom'
}
}
);wrapAndThrowError()
Wraps existing errors with professional context:
import { wrapAndThrowError, BWAErrorCode } from 'bitcoin-wallet-adapter';
try {
await someWalletOperation();
} catch (originalError) {
wrapAndThrowError(
originalError,
BWAErrorCode.TRANSACTION_SIGNING_FAILED,
"Failed to sign transaction with Unisat wallet",
{ operation: 'transaction_signing', walletType: 'Unisat' }
);
}useErrorHandler() Hook
React hook for handling errors with automatic UI notifications:
import { useErrorHandler, BWAErrorCode } from 'bitcoin-wallet-adapter';
function MyComponent() {
const { errors, clearErrors, hasError } = useErrorHandler({
onError: (error) => {
// Automatically dispatches snackbar notifications
console.log('Error handled:', error.code, error.message);
},
filterCodes: [BWAErrorCode.WALLET_NOT_CONNECTED], // Optional filtering
filterSeverity: [BWAErrorSeverity.HIGH], // Optional severity filtering
autoClearTimeout: 5000 // Auto-clear errors after 5 seconds
});
return (
<div>
{hasError(BWAErrorCode.WALLET_NOT_CONNECTED) && (
<p>Please connect your wallet first!</p>
)}
</div>
);
}✨ Benefits of Professional Error Handling
- 🎯 Consistent Experience: All errors follow the same professional format
- 📱 Automatic Notifications: Errors automatically trigger snackbar notifications
- 🔍 Rich Context: Detailed error context for debugging and user guidance
- ⚡ Type Safety: Full TypeScript support with professional error codes
- 📊 Error Tracking: Built-in error history and analytics
- 🎛️ Configurable: Filter by codes, severity, auto-clear options
- ♻️ Recoverable: Errors marked as recoverable allow retry mechanisms
🎨 Seamless Error Integration
Built-in error handling with automatic UI notifications:
import { useErrorHandler } from 'bitcoin-wallet-adapter';
function App() {
// Automatic error handling with professional snackbar notifications
useErrorHandler({
onError: (error) => {
// Errors automatically appear as styled notifications
// No additional setup required
}
});
return <YourAppComponents />;
}👛 Supported Wallets
Bitcoin Wallet Adapter currently supports the following wallets:
- 🦄 Unisat
- 🌌 Xverse
- 🐂 Leather/Hiro
- 🔮 MagicEden
- 👻 Phantom
- 🅾️ OKX
🎨 Headless Hooks (Advanced Usage)
For developers who want complete control over their wallet UI design, we provide headless hooks that separate logic from presentation. These hooks give you all the wallet functionality without any predefined UI components.
🚀 Why Use Headless Hooks?
- ✨ Complete Design Freedom: Build any UI you want
- 🎯 Logic Separation: Clean separation between wallet logic and presentation
- 🔧 Maximum Flexibility: Customize every aspect of the user experience
- 📱 Framework Agnostic: Logic works with any design system or component library
📖 Available Headless Hooks
🔌 useWalletConnect
The core hook for wallet connection management:
import { useWalletConnect } from 'bitcoin-wallet-adapter';
function CustomWalletConnect() {
const {
// Connection State
isConnected,
isLoading,
error,
// Wallet Information
currentWallet,
lastWallet,
availableWallets,
// Actions
connect,
disconnect,
// Utilities
checkAvailableWallets,
refreshBalance
} = useWalletConnect();
return (
<div className="my-custom-design">
{isConnected ? (
<div>
<h3>Connected to {currentWallet?.wallet}</h3>
<p>Cardinal: {currentWallet?.cardinal}</p>
<p>Ordinal: {currentWallet?.ordinal}</p>
<button onClick={disconnect}>Disconnect</button>
</div>
) : (
<div>
<h3>Choose a Wallet</h3>
{availableWallets.map((wallet) => (
<button
key={wallet.connector}
onClick={() => connect(wallet.connector)}
disabled={isLoading}
>
<img src={wallet.logo} alt={wallet.label} />
{wallet.label}
</button>
))}
{error && <p>Error: {error.message}</p>}
</div>
)}
</div>
);
}💰 useWalletBalance
Manage wallet balance and BTC price:
import { useWalletBalance } from 'bitcoin-wallet-adapter';
function CustomBalanceDisplay() {
const {
// Balance State
balance,
btcPrice,
isLoading,
error,
// Actions
refreshBalance,
// Utilities
formatBalance,
formatPrice
} = useWalletBalance();
return (
<div className="balance-container">
<div className="balance-card">
<h4>Wallet Balance</h4>
{isLoading ? (
<div>Loading balance...</div>
) : (
<>
<div className="btc-balance">
<span>{formatBalance(balance.btc || 0)} BTC</span>
<small>${formatPrice((balance.btc || 0) * btcPrice)}</small>
</div>
<div className="sats-balance">
<span>{balance.sats?.toLocaleString()} sats</span>
</div>
</>
)}
<button onClick={refreshBalance} disabled={isLoading}>
Refresh Balance
</button>
{error && <p className="error">{error.message}</p>}
</div>
</div>
);
}✍️ useWalletSigning
Handle message and transaction signing:
import { useWalletSigning } from 'bitcoin-wallet-adapter';
function CustomSigning() {
const {
// Signing State
isLoading,
error,
lastSignature,
// Actions
signMessage,
signTransaction,
signPSBT,
// Utilities
verifySignature,
clearError
} = useWalletSigning();
const handleSignMessage = async () => {
try {
const signature = await signMessage({
message: "Hello Bitcoin!",
address: "current-wallet-address"
});
console.log("Message signed:", signature);
} catch (err) {
console.error("Signing failed:", err);
}
};
const handleSignTransaction = async () => {
try {
const result = await signTransaction({
psbt: "your-psbt-here",
action: "sell"
});
console.log("Transaction signed:", result);
} catch (err) {
console.error("Transaction signing failed:", err);
}
};
return (
<div className="signing-interface">
<div className="signing-actions">
<button
onClick={handleSignMessage}
disabled={isLoading}
>
{isLoading ? 'Signing...' : 'Sign Message'}
</button>
<button
onClick={handleSignTransaction}
disabled={isLoading}
>
{isLoading ? 'Signing...' : 'Sign Transaction'}
</button>
</div>
{lastSignature && (
<div className="signature-result">
<h4>Last Signature:</h4>
<pre>{JSON.stringify(lastSignature, null, 2)}</pre>
</div>
)}
{error && (
<div className="error-message">
<p>Error: {error.message}</p>
<button onClick={clearError}>Clear Error</button>
</div>
)}
</div>
);
}🎨 Building Custom Components
You can combine these headless hooks to create sophisticated wallet interfaces:
import {
useWalletConnect,
useWalletBalance,
useWalletSigning
} from 'bitcoin-wallet-adapter';
function CompleteCustomWallet() {
const { isConnected, connect, disconnect, availableWallets, currentWallet } = useWalletConnect();
const { balance, formatBalance } = useWalletBalance();
const { signMessage, isLoading: isSigning } = useWalletSigning();
if (!isConnected) {
return (
<div className="connect-interface">
<h2>Connect Your Bitcoin Wallet</h2>
<div className="wallet-grid">
{availableWallets.map((wallet) => (
<div
key={wallet.connector}
className="wallet-option"
onClick={() => connect(wallet.connector)}
>
<img src={wallet.logo} alt={wallet.label} />
<span>{wallet.label}</span>
</div>
))}
</div>
</div>
);
}
return (
<div className="wallet-dashboard">
<header className="wallet-header">
<h2>{currentWallet?.wallet} Connected</h2>
<button onClick={disconnect} className="disconnect-btn">
Disconnect
</button>
</header>
<div className="wallet-content">
<div className="balance-section">
<h3>Balance: {formatBalance(balance.btc || 0)} BTC</h3>
<small>{balance.sats?.toLocaleString()} sats</small>
</div>
<div className="actions-section">
<button
onClick={() => signMessage({
message: "Custom message",
address: currentWallet?.cardinal || ""
})}
disabled={isSigning}
>
Sign Message
</button>
</div>
</div>
</div>
);
}🤝 Contributing
We welcome contributions to the Bitcoin Wallet Adapter! Please read our CONTRIBUTING.md for details on our code of conduct and the process for submitting pull requests. Let's build the future of Bitcoin wallets together! 🚀
📄 License
This project is licensed under the MIT License - see the LICENSE.md file for details.
🌐 Connect with Us
💻 Tech Stack
