@orangecheck/wallet-adapter
Every Bitcoin browser wallet exposes a different signing API. This package hides that behind one SignFn = (message) => Promise<string>.
yarn add @orangecheck/wallet-adapter
React components are under a subpath to keep the core library zero-dep:
import { detectWallets, getSigner } from '@orangecheck/wallet-adapter';
import { OcWalletButton } from '@orangecheck/wallet-adapter/react';
detectWallets()
Returns every supported wallet with a detected flag.
detectWallets();
// [
// { id: 'unisat', name: 'UniSat', detected: true, installUrl: '…' },
// { id: 'xverse', name: 'Xverse', detected: false, installUrl: '…' },
// { id: 'leather', name: 'Leather', detected: false, installUrl: '…' },
// { id: 'alby', name: 'Alby', detected: true, installUrl: '…' },
// { id: 'manual', name: 'Paste signature', detected: true, isManual: true }
// ]
getSigner(id, { address })
Returns a SignFn bound to a particular wallet.
import { getSigner } from '@orangecheck/wallet-adapter';
const sign = getSigner('unisat', { address: userBtcAddress });
const signature = await sign(canonicalMessage);
Throws when the wallet is unavailable or the user cancels.
<OcWalletButton />
Pre-built wallet picker. Detects installed wallets, renders install prompts for missing ones, calls onSigned with the signature.
import { OcWalletButton } from '@orangecheck/wallet-adapter/react';
<OcWalletButton
address={userBtcAddress}
message={challenge.message}
onSigned={(sig, walletId) => {
console.log(`${walletId} returned`, sig);
postVerify({ signature: sig });
}}
onError={(err) => console.error(err)}
/>
| Prop | Type | Notes |
|---|---|---|
address | string | Required by Xverse and Leather. |
message | string | Canonical message to sign. |
onSigned | (sig, walletId) => void | Success callback. |
onError | (err, walletId) => void | Failure callback. |
hideUninstalled | boolean | Default false — uninstalled wallets render as install prompts. |
heading | ReactNode | Header text; default "Sign with your wallet". |
Wallet details
| Wallet | Global | Style |
|---|---|---|
| UniSat | window.unisat.signMessage(msg, 'bip322-simple') | Simple BIP-322 |
| Xverse | window.BitcoinProvider.request('signMessage', {...}) | Full BIP-322 |
| Leather | window.LeatherProvider.request('signMessage', {...}) | Full BIP-322 |
| Alby | window.webln.signMessage(msg) | Legacy-ish (best for 1… addrs) |
| Manual | window.prompt() | User pastes sig from hardware / Sparrow / Core |
The shims are duck-typed — we check the shape of window.*, not just the presence of a global. Spoofing wrappers don't produce false positives.
End-to-end sign-in example
import { useState } from 'react';
import { OcWalletButton } from '@orangecheck/wallet-adapter/react';
export function SignIn({ address }: { address: string }) {
const [step, setStep] = useState<'idle' | 'ready' | 'done'>('idle');
const [message, setMessage] = useState('');
const [proven, setProven] = useState('');
async function start() {
const r = await fetch(`/api/challenge?addr=${address}`);
const { message } = await r.json();
setMessage(message);
setStep('ready');
}
async function handleSigned(signature: string) {
const r = await fetch('/api/challenge', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message, signature }),
});
const body = await r.json();
if (body.ok) {
setProven(body.address);
setStep('done');
}
}
if (step === 'idle') return <button onClick={start}>Start sign-in</button>;
if (step === 'ready') {
return <OcWalletButton address={address} message={message} onSigned={handleSigned} />;
}
return <p>Signed in as {proven}</p>;
}
License
MIT.