import { Meta, StoryFn } from "@storybook/react-vite" import React, { FC, useEffect, useState } from "react" import { BitcoinConnectionProvider, useBitcoinConnectionContext, WalletSession, } from "./BitcoinConnectionProvider" import { WalletAdapter, WalletAdapterAddress, WalletAdapterBitcoinNetwork, WalletAdapterFactory, } from "./WalletAdapters.types" import { UnisatWalletAdapterFactory, XverseWalletAdapterFactory, OkxWalletAdapterFactory, LeatherWalletAdapterFactory, BitgetWalletAdapterFactory, MagicEdenWalletAdapterFactory, } from "./adapters" const adapterFactories: WalletAdapterFactory[] = [ UnisatWalletAdapterFactory(), XverseWalletAdapterFactory(), OkxWalletAdapterFactory(), LeatherWalletAdapterFactory(), BitgetWalletAdapterFactory(), MagicEdenWalletAdapterFactory({ network: WalletAdapterBitcoinNetwork.MAINNET, }), ] export default { title: "Components/BitcoinConnectionProvider", component: BitcoinConnectionProvider, } as Meta const WalletConnectionContent = (): React.ReactElement => { const [debugInfo, setDebugInfo] = useState([]) const [signMessageText, setSignMessageText] = useState("Hello Bitcoin!") const [selectedAddress, setSelectedAddress] = useState("") const [signResult, setSignResult] = useState("") const [isSigning, setIsSigning] = useState(false) // sendBitcoin state const [sendFromAddress, setSendFromAddress] = useState("") const [sendToAddress, setSendToAddress] = useState("") const [sendAmount, setSendAmount] = useState("1000") const [sendResult, setSendResult] = useState("") const [isSending, setIsSending] = useState(false) const ctx = useBitcoinConnectionContext() const addDebugInfo = (info: string): void => { setDebugInfo(prev => [ ...prev, `${new Date().toLocaleTimeString()}: ${info}`, ]) } const handleSignMessage = async (): Promise => { if (!ctx.walletSession || !selectedAddress) return setIsSigning(true) addDebugInfo(`Signing message with ${selectedAddress.slice(0, 10)}...`) try { const result = await ctx.walletSession.adapter.signMessage( selectedAddress, signMessageText, ) setSignResult(JSON.stringify(result, null, 2)) addDebugInfo(`Sign success: ${result.algorithm}`) } catch (err) { const message = err instanceof Error ? err.message : String(err) addDebugInfo(`Sign failed: ${message}`) setSignResult(`Error: ${message}`) } finally { setIsSigning(false) } } const handleSendBitcoin = async (): Promise => { if (!ctx.walletSession || !sendFromAddress || !sendToAddress || !sendAmount) return const satoshiAmount = BigInt(sendAmount) if (satoshiAmount <= 0n) { addDebugInfo("Invalid amount") return } setIsSending(true) addDebugInfo( `Sending ${sendAmount} sats from ${sendFromAddress.slice(0, 10)}... to ${sendToAddress.slice(0, 10)}...`, ) try { const result = await ctx.walletSession.adapter.sendBitcoin( sendFromAddress, sendToAddress, satoshiAmount, ) setSendResult(JSON.stringify(result, null, 2)) addDebugInfo(`Send success: txid ${result.txid.slice(0, 16)}...`) } catch (err) { const message = err instanceof Error ? err.message : String(err) addDebugInfo(`Send failed: ${message}`) setSendResult(`Error: ${message}`) } finally { setIsSending(false) } } return (

Bitcoin Connection Provider Demo

Note: Some features that depend on localStorage (e.g., Leather wallet connection persistence, auto-connect) may not work in Storybook's iframe mode. For full functionality, switch to{" "} isolation mode by clicking the share icon ( ↗) in the top-right corner and selecting "Open in isolation mode" .

Connection Status

Connected: {ctx.walletSession ? "Yes" : "No"}

Initializing:{" "} {ctx.isConnectionInitializing ? "Yes" : "No"}

{ctx.walletSession && ( <>

Adapter ID: {ctx.walletSession.adapterId}

Addresses:

    {ctx.walletSession.addresses.map((addr, i) => (
  • {addr.purposes.join(", ")}: {addr.address.slice(0, 20)}...
  • ))}
)}

Available Wallets ({ctx.availableAdapters.length})

{ctx.availableAdapters.map(([adapterId, adapter]) => { const factory = adapterFactories.find( factory => factory.adapterId === adapterId, )! return (
{ addDebugInfo(`Connecting to ${adapterId}...`) ctx .connect(adapterId, adapter) .then(() => { addDebugInfo(`Connected to ${adapterId}`) }) .catch(err => { addDebugInfo(`Failed to connect: ${err.message}`) }) }} style={{ display: "flex", alignItems: "center", justifyContent: "center", padding: "8px 16px", cursor: ctx.isConnectionInitializing ? "not-allowed" : "pointer", border: "1px solid #ccc", borderRadius: "4px", background: "#fff", }} > {ctx.isConnectionInitializing ? ( "Connecting..." ) : ( <> {factory.metadata.name} )}
) })}
{ctx.walletSession && ( )} {ctx.walletSession && (

Sign Message

setSignMessageText(e.target.value)} style={{ width: "100%", padding: "8px", borderRadius: "4px", border: "1px solid #ccc", boxSizing: "border-box", }} />
{signResult && (
                {signResult}
              
)}
)} {ctx.walletSession && (

Send Bitcoin

setSendToAddress(e.target.value)} placeholder="bc1q... or 3... or 1..." style={{ width: "100%", padding: "8px", borderRadius: "4px", border: "1px solid #ccc", boxSizing: "border-box", }} />
setSendAmount(e.target.value)} min="1" style={{ width: "100%", padding: "8px", borderRadius: "4px", border: "1px solid #ccc", boxSizing: "border-box", }} />
{sendResult && (
                {sendResult}
              
)}
)}

Registered Adapter Factories ({ctx.adapterFactories.length})

    {ctx.adapterFactories.map((factory, i) => (
  • {factory.metadata.name}
  • ))}
{debugInfo.length > 0 && (

Debug Info:

{debugInfo.map((info, index) => (
{info}
))}
)}
) } export const Default: StoryFn = () => { const [events, setEvents] = useState([]) const addEvent = (event: string): void => { setEvents(prev => [...prev, `${new Date().toLocaleTimeString()}: ${event}`]) } return ( { addEvent(`Wallet connected: ${session.adapterId}`) }} onWalletAddressesChanged={(addresses: WalletAdapterAddress[]) => { addEvent(`Addresses changed: ${addresses.length} addresses`) }} onWalletDisconnected={() => { addEvent("Wallet disconnected") }} > {events.length > 0 && (

Provider Events:

{events.map((event, index) => (
{event}
))}
)}
) } const WalletIcon: FC<{ adapterFactory: WalletAdapterFactory }> = ({ adapterFactory }) => { const [iconUrl, setIconUrl] = useState(null) useEffect(() => { const abortController = new AbortController() void adapterFactory.metadata .iconUrl() .then(url => { if (abortController.signal.aborted) return setIconUrl(url) }) .catch(err => { console.error( "Failed to load icon for ", adapterFactory.metadata.name, err, ) }) return () => { abortController.abort() } }, [adapterFactory.metadata]) if (!iconUrl) { return (
) } return ( {adapterFactory.metadata.name} ) }