In the previous article, I ranted about how chaotic the Bitcoin wallet ecosystem is: WBIPs standards that nobody implements, sats-connect compatibility issues, wallets each doing their own thing with APIs...
This article introduces the library I built: bitcoin-wallet-connector.
Try the Live Demo first, or check out the Storybook.
What This Library Focuses On
Smoothing Out the Ridiculous Differences
bitcoin-wallet-connector provides a unified API that lets you connect to all supported wallets with the same code:
import {
BitcoinWalletConnector,
UnisatWalletAdapterFactory,
XverseWalletAdapterFactory,
LeatherWalletAdapterFactory,
} from "bitcoin-wallet-connector"
// Register the wallets you want to support
const connector = new BitcoinWalletConnector([
UnisatWalletAdapterFactory(),
XverseWalletAdapterFactory(),
LeatherWalletAdapterFactory(),
])
// Subscribe to available wallet changes
// Note: The timing of wallet extension API injection is unpredictable
// (some inject at DOMContentLoaded, others after the load event),
// so subscribing is recommended over getting
connector.subscribeAvailableAdapters(availableAdapters => {
console.log(
"Available wallets:",
availableAdapters.map(([id]) => id),
)
// => ['unisat', 'xverse', ...]
})
// Connect wallet - same API for all wallets
const [adapterId, adapter] = availableAdapters[0]
await connector.connect(adapterId, adapter)
// Get addresses, sign, send transactions - unified interface
const addresses = await adapter.getAddresses()
const result = await adapter.signMessage(addresses[0].address, "Hello Bitcoin!")
That's it. Write the code once, support all wallets.
Currently supported wallets:
| Wallet | Adapter | Extra Dependencies |
|---|---|---|
| UniSat | UnisatWalletAdapterFactory |
- |
| Xverse | XverseWalletAdapterFactory |
sats-connect |
| OKX | OkxWalletAdapterFactory |
- |
| Leather | LeatherWalletAdapterFactory |
@leather.io/rpc |
| Bitget | BitgetWalletAdapterFactory |
- |
| Magic Eden | MagicEdenWalletAdapterFactory |
sats-connect |
All adapters implement the same interface:
interface WalletAdapter {
// Connect/Disconnect
connect(): Promise<void>
disconnect(): Promise<void>
// Get addresses
getAddresses(): Promise<WalletAdapterAddress[]>
// Message signing
signMessage(address: string, message: string): Promise<SignMessageResult>
// Send BTC
sendBitcoin(
fromAddress: string,
receiverAddress: string,
satoshiAmount: bigint,
): Promise<{ txid: string }>
// PSBT signing
signAndFinalizePsbt(
psbtHex: string,
signIndices: [address: string, signIndex: number][],
): Promise<{ signedPsbtHex: string }>
// Listen for address changes
onAddressesChanged(callback): { unsubscribe: () => void }
}
No matter which wallet users choose, your business logic stays the same.
About sendRunes/sendInscriptions/sendBRC20
Currently, this library only supports signMessage, sendBitcoin, and signPsbt. It doesn't support sendRunes, sendInscriptions, or sendBRC20.
Because these involve more complex dependencies (like needing an Ordinals Indexer, a BRC20 Indexer, etc.). These would make a Connector overly complicated.
In my view, this should be the responsibility of a Transaction Builder. The Transaction Builder handles transaction construction, then passes it to the Connector for signing and broadcasting.
Security Matters
When designing this library, I prioritized dependency security. The reason is simple: wallet libraries directly handle user assets, and any security vulnerability could result in real financial losses.
Peer Dependencies
I declared important dependencies as peer dependencies rather than bundling them:
pnpm add bitcoin-wallet-connector @scure/base @scure/btc-signer
This means:
- You directly control the versions of these dependencies
- If a dependency has a security vulnerability, you can upgrade immediately without waiting for this library to release a new version
- You won't end up with two versions of
@scure/btc-signerbundled in your final build
Optional Dependencies: Install Only What You Need
Wallet SDKs (like sats-connect, @leather.io/rpc) are optional peer dependencies:
# Only supporting UniSat and OKX? No extra dependencies needed
# Need Xverse support?
pnpm add sats-connect
# Need Leather support?
pnpm add @leather.io/rpc
You only install what you need, reducing your exposure to malicious package scripts.
Dynamic Imports: Lazy Loading
This is another important security design: Wallet SDKs are lazy-loaded via dynamic import().
// Internal implementation sketch
const availability = createAvailability({
getPrecondition: () => window.unisat ?? null,
initializer: async () => {
// Only when user actually wants to connect this wallet
// will the corresponding implementation be loaded
const { UnisatWalletAdapterImpl } =
await import("./UnisatWalletAdapter.impl")
return new UnisatWalletAdapterImpl()
},
})
Suppose sats-connect gets compromised in a supply chain attack (not uncommon in the npm ecosystem). If your users only use UniSat wallet, the malicious code won't be loaded or executed, because sats-connect is only imported when users click "Connect Xverse".
This should reduce users' exposure to supply chain attacks.
Framework Integration
Currently React integration is provided with an out-of-the-box Context Provider:
import {
BitcoinConnectionProvider,
useBitcoinConnectionContext,
} from "bitcoin-wallet-connector/react"
import {
UnisatWalletAdapterFactory,
XverseWalletAdapterFactory,
} from "bitcoin-wallet-connector/adapters"
const adapterFactories = [
UnisatWalletAdapterFactory(),
XverseWalletAdapterFactory(),
]
function App() {
return (
<BitcoinConnectionProvider
adapterFactories={adapterFactories}
onWalletConnected={session => console.log("Connected:", session)}
onWalletDisconnected={() => console.log("Disconnected")}
>
<WalletUI />
</BitcoinConnectionProvider>
)
}
function WalletUI() {
const { walletSession, availableAdapters, connect, disconnect } =
useBitcoinConnectionContext()
if (walletSession) {
return (
<div>
<p>Connected: {walletSession.adapterId}</p>
<button onClick={() => disconnect()}>Disconnect</button>
</div>
)
}
return (
<div>
{availableAdapters.map(([adapterId, adapter]) => (
<button key={adapterId} onClick={() => connect(adapterId, adapter)}>
Connect {adapterId}
</button>
))}
</div>
)
}
The core BitcoinWalletConnector is framework-agnostic. If you're using Vue, Svelte, Solid, or other frameworks, wrapping it into corresponding hooks/composables should be straightforward. Contributions are welcome!
Get Started and Contribute
Quick Start
pnpm add bitcoin-wallet-connector @scure/base @scure/btc-signer
# Install wallet SDKs as needed
pnpm add sats-connect # Xverse / Magic Eden
pnpm add @leather.io/rpc # Leather
Or get the demo running in 5 minutes:
- Clone the repo
pnpm installpnpm storybook- Open http://localhost:6006
You can test wallet connections, signing, and other features in Storybook.
Contributing
This is an open source project, and community contributions are very welcome!
If you want to add support for a new wallet, there's one small preference: try to avoid depending on the wallet's official SDK.
Why? Because many wallet APIs are just mounted on the window object and can be called directly without introducing an additional SDK. For example, the UniSat, OKX, and Bitget adapters have zero external dependencies.
Introducing an SDK means one more potential supply chain attack vector, one more package users need to install, and potentially unnecessary bundle size. Of course, if a wallet can only be accessed through its SDK (like Xverse's sats-connect), that's fine — we can make it an optional peer dependency.
See CONTRIBUTING.md for detailed contribution guidelines.
The project is fully open source (MIT). Stars and contributions are welcome!
Top comments (0)