bitcoin-wallet-connector: 可能是目前最好用的比特币钱包适配器
标题党一下 🤪
上一篇文章我吐槽了比特币钱包生态有多混乱:WBIPs 标准没人实现、sats-connect 的兼容性、钱包各有各的 API...
这篇文章介绍下我写的库:bitcoin-wallet-connector。
我来讲讲这个库关注什么
把那些莫名其妙的差异抹平
bitcoin-wallet-connector 提供了一套统一的 API,让你用同样的代码接入所有支持的钱包:
import {
BitcoinWalletConnector,
UnisatWalletAdapterFactory,
XverseWalletAdapterFactory,
LeatherWalletAdapterFactory,
} from "bitcoin-wallet-connector";
// 注册你想支持的钱包
const connector = new BitcoinWalletConnector([
UnisatWalletAdapterFactory(),
XverseWalletAdapterFactory(),
LeatherWalletAdapterFactory(),
]);
// 订阅可用钱包变化
// 注意:钱包扩展注入 API 的时机是不确定的(有些在 DOMContentLoaded,
// 有些在 load 事件后),所以推荐使用 subscribe 而不是 get
connector.subscribeAvailableAdapters((availableAdapters) => {
console.log(
"可用钱包:",
availableAdapters.map(([id]) => id)
);
// => ['unisat', 'xverse', ...]
});
// 连接钱包 - 所有钱包用同一套 API
const [adapterId, adapter] = availableAdapters[0];
await connector.connect(adapterId, adapter);
// 获取地址、签名、发送交易 - 统一接口
const addresses = await adapter.getAddresses();
const result = await adapter.signMessage(
addresses[0].address,
"Hello Bitcoin!"
);
就这样,你只需要写一遍代码,就能支持所有钱包。
目前支持的钱包:
| 钱包 | Adapter | 额外依赖 |
|---|---|---|
| UniSat | UnisatWalletAdapterFactory |
- |
| Xverse | XverseWalletAdapterFactory |
sats-connect |
| OKX | OkxWalletAdapterFactory |
- |
| Leather | LeatherWalletAdapterFactory |
@leather.io/rpc |
| Bitget | BitgetWalletAdapterFactory |
- |
| Magic Eden | MagicEdenWalletAdapterFactory |
sats-connect |
所有 adapter 都实现了相同的接口:
interface WalletAdapter {
// 连接/断开
connect(): Promise<void>;
disconnect(): Promise<void>;
// 获取地址
getAddresses(): Promise<WalletAdapterAddress[]>;
// 消息签名
signMessage(address: string, message: string): Promise<SignMessageResult>;
// 发送 BTC
sendBitcoin(
fromAddress: string,
receiverAddress: string,
satoshiAmount: bigint
): Promise<{ txid: string }>;
// PSBT 签名
signAndFinalizePsbt(
psbtHex: string,
signIndices: [address: string, signIndex: number][]
): Promise<{ signedPsbtHex: string }>;
// 监听地址变化
onAddressesChanged(callback): { unsubscribe: () => void };
}
无论用户选择哪个钱包,你的业务代码都是一样的。
关于 sendRunes/sendInscriptions/sendBRC20
目前这个库只支持 signMessage, sendBitcoin, 和 signPsbt,暂时不支持 sendRunes, sendInscriptions, sendBRC20。
因为这些都涉及到更复杂的依赖(比如需要一个 Ordinals Indexer, 还需要一个 BRC20 Indexer 等等)。这些会让一个 Connector 变的过于复杂。
在我看来,这应该是一个 Transaction Builder 的职责,由 Transaction Builder 负责构建交易,然后将交易交给 Connector 来签名和发送。
安全性很重要
在设计这个库的时候,我把依赖安全放在了很高的优先级。原因很简单:钱包库直接接触用户的资产,任何安全漏洞都可能造成真金白银的损失。
Peer Dependencies
我把一些重要的依赖声明为 peer dependencies,而不是打包进库里:
pnpm add bitcoin-wallet-connector @scure/base @scure/btc-signer
这意味着:
- 你可以直接控制这些依赖的版本
- 如果某个依赖爆出安全漏洞,你可以立即升级,不用等这个库发新版本
- 也不会出现最终的 bundle 里被打包了两个版本的
@scure/btc-signer的尴尬场景
可选依赖:按需安装
钱包 SDK(如 sats-connect、@leather.io/rpc)是可选的 peer dependencies:
# 只支持 UniSat 和 OKX?不需要安装任何额外依赖
# 需要支持 Xverse?
pnpm add sats-connect
# 需要支持 Leather?
pnpm add @leather.io/rpc
你只安装你需要的,减少你被恶意包脚本攻击的风险。
动态导入:延迟加载
这是另外一个重要的安全设计:钱包 SDK 通过 dynamic import() 延迟加载。
// 内部实现示意
const availability = createAvailability({
getPrecondition: () => window.unisat ?? null,
initializer: async () => {
// 只有用户真正要连接这个钱包时,才会加载对应的实现
const { UnisatWalletAdapterImpl } = await import(
"./UnisatWalletAdapter.impl"
);
return new UnisatWalletAdapterImpl();
},
});
假设 sats-connect 这个包被供应链攻击了(这在 npm 生态并不罕见)。如果你的用户只使用 UniSat 钱包,恶意代码不会被加载和执行,因为 sats-connect 只有在用户点击"连接 Xverse"时才会被 import。
这一条应该能降低用户被供应链攻击的风险。
框架集成
目前提供了 React 集成,开箱即用的 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>已连接: {walletSession.adapterId}</p>
<button onClick={() => disconnect()}>断开连接</button>
</div>
);
}
return (
<div>
{availableAdapters.map(([adapterId, adapter]) => (
<button key={adapterId} onClick={() => connect(adapterId, adapter)}>
连接 {adapterId}
</button>
))}
</div>
);
}
核心的 BitcoinWalletConnector 是框架无关的,如果你在用 Vue、Svelte、Solid 或其他框架,封装成对应的 hooks/composables 应该不难,当然也欢迎贡献!
欢迎使用和贡献
快速开始
pnpm add bitcoin-wallet-connector @scure/base @scure/btc-signer
# 按需安装钱包 SDK
pnpm add sats-connect # Xverse / Magic Eden
pnpm add @leather.io/rpc # Leather
或者 5 分钟跑通 Demo:
- Clone 仓库
pnpm installpnpm storybook- 打开 http://localhost:6006
你可以在 Storybook 里测试各种钱包的连接、签名等功能。
贡献
这是一个开源项目,非常欢迎社区贡献!
如果你想添加新钱包的支持,有一个小小的期望:尽量不要依赖钱包官方的 SDK。
为什么?因为很多钱包的 API 其实就是挂在 window 对象上的,直接调用就行,没必要引入一个额外的 SDK。比如 UniSat、OKX、Bitget 的 adapter 都是零外部依赖的。
引入 SDK 意味着多一个潜在的供应链攻击入口、用户需要多安装一个包、可能引入不必要的 bundle size。当然,如果某个钱包确实只能通过 SDK 接入(比如 Xverse 的 sats-connect),那也没问题,我们可以把它作为可选的 peer dependency。
详细的贡献指南请看 CONTRIBUTING.md。
项目完全开源(MIT),欢迎 Star 和贡献!