dApp Connect
Connect a dApp to an existing BRC-100 wallet (browser extension, desktop wallet, or Sigma Identity) using @1sat/connect (vanilla JS) and @1sat/react (React).
This skill is about connecting to a wallet a user already has. To create and run a wallet programmatically (local/remote storage, address sync, backups), see ../../../wallet/skills/wallet-setup.
Architecture
@1sat/react (React components + hooks)
└── @1sat/connect (connection core: connectWallet)
├── BRC-100 auto-detect — WalletClient('auto'): extensions, desktop (localhost), XDM, React Native
├── Sigma OAuth — browser redirect flow + CWI iframe
└── Custom providers — hosted CWI bridge (url) or a custom connect() fn
connectWallet races auto-detect and every configured provider with Promise.any; first to authenticate wins, and its provider type is saved to localStorage for warm reconnects.
Quick Start (React)
import { WalletProvider, ConnectButton, useWallet } from '@1sat/react'
function App() {
return (
<WalletProvider autoReconnect>
<ConnectButton />
<Dashboard />
</WalletProvider>
)
}
function Dashboard() {
const { wallet, status, identityKey } = useWallet()
if (status !== 'connected' || !wallet) return <p>Not connected</p>
return <p>Connected: {identityKey?.slice(0, 8)}...</p>
}
React Components
All components from @1sat/react are deliberately unstyled — they provide functional primitives with minimal inline styles. Themed UI wrappers are distributed via the BigBlocks registry as shadcn-compatible registry items.
WalletProvider
App-level context provider. Wrap the application root. Props (WalletProviderProps):
<WalletProvider
autoReconnect={false} // re-run last successful connection on mount (default false)
autoDetect={true} // include WalletClient('auto') in the connect race (default true)
providers={customProviders} // WalletProviderConfig[] — custom/Sigma providers (optional)
>
{children}
</WalletProvider>
There is no appName prop on WalletProvider. With autoReconnect, mount reads the stored provider type: brc100 re-runs auto-detect; a custom type must be present in providers; sigma is guarded against redirect loops via a sessionStorage flag.
ConnectButton
Unstyled button that triggers wallet connection. Supports render-prop children for full customization.
<ConnectButton
className="my-button"
style={{ /* inline styles */ }}
connectLabel="Connect" // default 'Connect Wallet'
connectingLabel="Connecting..." // default 'Connecting...'
connectedLabel={(key) => `${key.slice(0,6)}...${key.slice(-4)}`} // ReactNode | (identityKey) => ReactNode; defaults to a truncated key
=> console.log('connected')} // no args
=> console.log('disconnected')}
=> console.error(err)} // connect/disconnect errors
disconnectOnClick={true} // click to disconnect when connected (default true)
/>
Render-prop form receives { isConnected, isConnecting, identityKey, connect, disconnect }. isConnecting is true while status is detecting or connecting. When connected and no specific provider is configured, clicking calls the plain connect() which re-runs detection — there is no built-in provider picker on the button; pair it with ConnectDialogProvider (below) for selection.
ConnectDialog
Controlled dialog (native <dialog>) for wallet provider selection. Requires open and onOpenChange. Render-prop children is optional — omit it for a basic unstyled provider list.
<ConnectDialog
open={isOpen}
>
{({ providers, error, close }) => (
<div>
{providers.map(p => (
<button
key={p.type}
disabled={p.isConnecting}
// each provider carries its own bound connect()
>
{p.icon && <img src={p.icon} alt="" />}
{p.name}{p.isConnecting && ' ...'}
</button>
))}
{error && <p>{error.message}</p>}
<button
</div>
)}
</ConnectDialog>
Render props are { providers, error, close }. Each provider is { type, name, icon?, isConnecting, connect } — connect takes no arguments (it's pre-bound to that provider's type). There is no detected flag; the dialog lists the providers configured on WalletProvider.
ConnectDialogProvider + useConnectDialog
App-level provider that mounts a ConnectDialog and auto-opens it when status becomes 'selecting' (auto-detect found nothing). Place it inside WalletProvider. Pass renderDialog to customize the dialog content (same render props as ConnectDialog).
<WalletProvider autoReconnect>
<ConnectDialogProvider
renderDialog={({ providers, error, close }) => (/* custom UI */ null)} // optional
>
<App />
</ConnectDialogProvider>
</WalletProvider>
// In any child component:
function ConnectTrigger() {
const { openConnectDialog } = useConnectDialog()
return <button
}
useConnectDialog() returns { openConnectDialog } and throws if used outside ConnectDialogProvider.
WalletSelector
Headless, render-prop-only component (no default UI at all — children is required). Reads availableProviders, connect, and error from context; tracks a per-provider connecting state; calls onClose after a provider connects. Use it to build a bespoke selector when ConnectDialog's <dialog> shell doesn't fit.
<WalletSelector => setOpen(false)}>
{({ providers, error }) => (
<ul>
{providers.map(p => (
<li key={p.type}>
<button disabled={p.isConnecting}
{p.icon && <img src={p.icon} alt="" />}
{p.name}{p.isConnecting && ' (connecting...)'}
</button>
</li>
))}
{error && <p>{error.message}</p>}
</ul>
)}
</WalletSelector>
Render props: { providers, error }. Each provider is { type, name, icon?, isConnecting, connect } with connect pre-bound (no args). Unlike ConnectDialog, WalletSelector renders nothing but what children returns — no <dialog>, no open/onOpenChange.
SigmaCallback
Page component for the Sigma OAuth redirect. Place it at the OAuth callback route. It runs completeSigmaOAuth → connectSigmaWallet, applies the result to the wallet context, then redirects (or calls onComplete).
// app/auth/sigma/callback/page.tsx
import { SigmaCallback } from '@1sat/react'
export default function AuthCallback() {
return (
<SigmaCallback
redirectTo="/" // default '/'
=> router.push('/')} // no args; use for SPA nav instead of hard redirect
loadingContent={<p>Completing authentication...</p>}
renderError={(error, goBack) => <p>{error} <button
/>
)
}
onComplete takes no arguments (the connected wallet is already applied to context). renderError receives (error: string, goBack: () => void).
useWallet Hook
Primary hook for accessing wallet context:
interface WalletContextValue {
wallet: WalletInterface | null // @bsv/sdk WalletInterface
status: WalletStatus // 'disconnected'|'detecting'|'selecting'|'connecting'|'connected'
identityKey: string | null // Identity pubkey
providerType: string | null // 'brc100' (auto-detect) | 'sigma' | a custom provider's type
availableProviders: AvailableProvider[]
connect: (providerType?: string) => Promise<void>
applyResult: (result: ConnectWalletResult) => void
disconnect: () => void
error: Error | null
}
Vanilla JS
The public entry point is connectWallet(config). It races BRC-100 auto-detect and every configured provider, and returns a ConnectWalletResult — or null when nothing connected (treat null as "show the provider picker").
import { connectWallet } from '@1sat/connect'
const result = await connectWallet({
autoDetect: true, // include WalletClient('auto') in the race (default true)
providers: [ // optional custom/Sigma providers
{ type: 'my-wallet', name: 'My Wallet', url: 'https://wallet.example.com' },
],
})
if (!result) {
// no wallet connected — render your own provider picker
} else {
const { wallet, provider, identityKey, disconnect } = result
// `wallet` is a @bsv/sdk WalletInterface — drive BRC-100 ops directly:
const { publicKey } = await wallet.getPublicKey({ identityKey: true })
// ...wallet.createAction(...), wallet.createSignature(...), etc.
disconnect() // tear down the connection
}
ConnectWalletResult is { wallet: WalletInterface, provider: string, identityKey: string, disconnect: () => void }. All wallet operations go through the standard @bsv/sdk WalletInterface on result.wallet — @1sat/connect does not wrap them in its own method surface.
Provider config
Each entry in providers is a WalletProviderConfig:
{
type: string, // unique id, also the saved-reconnect key
name: string, // display name
icon?: string, // icon URL / data URI
url?: string, // Hosted CWI bridge (iframe, with popup fallback)
connect?: () => Promise<ConnectWalletResult>, // custom connector; overrides url
}
Resolution order per provider: custom connect → url (hosted CWI bridge via createWebCWI) → error. The hosted bridge uses an iframe when cross-site Storage Access is available and a top-level wallet window otherwise.
Helpers
import { connectWallet, getAvailableProviders, loadLastProvider } from '@1sat/connect'
// Build the provider list (each entry gets a bound connect()), sorted so the
// last successful provider is tried first.
const providers = getAvailableProviders({ providers: myProviders })
await providers[0].connect()
// The provider type saved on the last successful connectWallet() (localStorage).
const last = loadLastProvider() // string | null
connectWallet saves the winning provider type to localStorage and, on the next call, attempts that last provider first before the full race. There is no createOneSat factory, isOneSatInjected, waitForOneSat, saveConnection/loadConnection, or event emitter on the public @1sat/connect surface — those are internal to the popup OneSatBrowserProvider and not exported. OneSatProvider is exported as a type only.
Connection Flow
WalletProvider drives status transitions:
detecting—connectWalletraces BRC-100 auto-detect (WalletClient('auto'): extensions, desktop/localhost, XDM, React Native) and all configured providers.selecting— nothing auto-connected;connectWalletreturnednull.ConnectDialogProviderauto-opens the provider selector.connecting→connected— a specific provider was chosen (or won the race); on success the result is applied to context.- Sigma OAuth — redirect-based; completed by
SigmaCallback, which callsconnectSigmaWalletand applies the result.
Sigma OAuth (vanilla)
For non-React apps, the Sigma flow is three exported functions:
import { initiateSigmaOAuth, completeSigmaOAuth, connectSigmaWallet } from '@1sat/connect'
// 1. Kick off the redirect (never resolves — the page navigates away)
await initiateSigmaOAuth({ clientId: 'my-client', callbackURL: '/auth/sigma/callback' })
// 2. On the callback route, exchange the code
const { bapId, pubkey, user, accessToken } = await completeSigmaOAuth(
new URLSearchParams(window.location.search),
)
// 3. Open the CWI iframe and connect
const { wallet, identityKey, disconnect } = await connectSigmaWallet(bapId)
BigBlocks Registry Integration
@1sat/react components are unstyled primitives. The BigBlocks registry serves 30 shadcn-themed blocks installable via:
npx bigblocks add <block>
# or
bunx shadcn@latest add https://registry.bigblocks.dev/r/<block>.json
Connection and wallet blocks that wrap @1sat/react:
| Block | Description |
|---|---|
connect-wallet |
Wallet connection button with provider selection dialog and connected-state dropdown |
wallet-overview |
Dashboard card with balance, addresses, identity key, and send/receive actions |
send-bsv |
Send BSV dialog with sats/BSV toggle, fee estimate, and confirmation |
receive-address |
QR code and deposit address with copy and optional rotation |
transaction-history |
Transaction list with status indicators and pagination |
mnemonic-flow |
Multi-mode seed phrase create/display/import/verify |
unlock-wallet |
Passphrase and Touch ID unlock screen |
bigblocks-provider |
Context provider for web-mode (API) or custom-mode (desktop RPC) data fetching |
BigBlocks components wrap @1sat/react primitives in shadcn UI (Button, Dialog, Drawer, DropdownMenu) with full theme support. The source for @1sat/react stays in this repo — BigBlocks serves them without duplicating code. See the full 30-block registry at https://bigblocks.dev.
Requirements
# Vanilla JS
bun add @1sat/connect
# React
bun add @1sat/react # includes @1sat/connect
# Browser extensions
bun add @1sat/extension