BTC Flow
这份指南围绕 UKey Wallet 的 BTC 地址确认与 UTXO 交易签名展开。比特币流程里最容易踩坑的是路径、脚本类型、前序交易和找零输出不匹配,因此这里重点讲这些信息如何交给设备审阅并完成校验。
目录
原理
BTC 相关方法通过 SDK 入口发起,设备端会按照 UTXO 模型审阅输入、输出、找零和手续费。应用需要提供足够完整的交易上下文,设备才能确认用户看到的内容与最终交易一致。
- HD 路径必须与脚本 purpose 对应,例如 44、49、84、86。
- 每个输入都应能在
refTxs中找到完整前序交易。 - 找零输出必须使用正确路径和脚本类型,否则设备无法安全判断它是否属于用户。
- 用户确认后,SDK 返回签名、序列化交易或交易 ID 等结果。
部署
npm i @ukeyfe/hardware-core @ukeyfe/hardware-common-connect-sdk
启动
import ukeySdk from "@ukeyfe/hardware-common-connect-sdk";
await ukeySdk.init({ env: "webusb", fetchConfig: true, debug: false });
const [{ connectId }] = await ukeySdk.searchDevices();
const deviceId = (await ukeySdk.getFeatures(connectId)).payload?.device_id;
适用面
场景 1:读取地址
读取指定路径下的 BTC 地址。首次展示给用户、保存收款地址或配置找零路径时,建议开启设备端确认。
const path84 = "m/84'/0'/1'/0/0";
const opResult = await ukeySdk.btcGetAddress(connectId, deviceId, {
path: path84,
coin: "btc",
showOnUKey: true,
});
// 返回内容可参考:opResult.payload.address, publicKey?, path
入参清单
path/address_n: BIP44 风格路径,可用字符串或数字数组,需和脚本 purpose 对齐。coin: 网络选择值,例如btc或test。showOnUKey?: 开启后会先在设备端展示地址并等待用户核对。
返回
Promise<{ success; payload: { address; path; publicKey?; chainCode? } }>;
场景 2:交易签名
签名交易前,请先确认输入、输出、找零和手续费都能由应用端独立计算。设备端展示内容应与前端最终广播的交易保持一致。
const { success, payload } = await ukeySdk.btcSignTransaction(
connectId,
deviceId,
{
coin: "btc",
inputs,
outputs,
refTxs,
locktime: 0,
},
);
// 返回结构大致为: { serializedTx, signatures?, txid? }
入参清单
coin: 用来选择 Bitcoin 网络规则和地址前缀。inputs: 交易输入集合。需提供以下字段;SegWit/Taproot 必须带amount,Legacy 也建议填写:address_n(路径,与脚本匹配)prev_hash(UTXO txid,hex)prev_index(UTXO vout)amount(字符串 satoshi)script_type(SPENDADDRESS/SPENDP2SHWITNESS/SPENDWITNESS/SPENDTAPROOT)
outputs: 交易输出集合,同时描述收款输出和找零输出:- 收款地址输出:
address+amount+script_type(如PAYTOADDRESS) - 找零输出:
address_n+amount+script_type对应脚本
- 收款地址输出:
refTxs: 每个输入对应的完整前序交易;按原交易结构提供hash、version、lock_time、inputs、bin_outputs或outputs。locktime?: 交易需要锁定时间时传入数字值。
返回
Promise<{
success;
payload: { serializedTx; signatures?: string[]; txid?: string };
}>;
交互状态
- 方法通过 Promise 返回结果;解锁、打开 BTC App、确认输入/输出/找零/手续费等交互通过
UI_REQUEST事件触发。 - 同一台设备建议串行签名,避免多个请求同时等待用户确认。
- 签名前可用
showOnUKey让用户确认地址和路径,尤其是找零路径。
示范
示例写法:P2SH-P2WPKH 交易(嵌套 SegWit)
import ukeySdk from "@ukeyfe/hardware-common-connect-sdk";
await ukeySdk.init({ env: "webusb", debug: false });
const [{ connectId }] = await ukeySdk.searchDevices();
const deviceId = (await ukeySdk.getFeatures(connectId)).payload?.device_id;
const path49 = [
(49 | 0x80000000) >>> 0,
(0 | 0x80000000) >>> 0,
(0 | 0x80000000) >>> 0,
0,
0,
];
await ukeySdk.btcGetAddress(connectId, deviceId, {
path: path49,
coin: "btc",
showOnUKey: true,
});
const inputs = [
{
address_n: path49,
prev_index: 0,
prev_hash:
"d29e7c4b1a0f3d5e6c7b8a99112233445566778899aabbccddeeff0012345678",
amount: "3471185",
script_type: "SPENDWITNESS",
},
];
const outputs = [
{
address_n: [
(49 | 0x80000000) >>> 0,
(0 | 0x80000000) >>> 0,
(0 | 0x80000000) >>> 0,
1,
1,
],
amount: "3265985",
script_type: "PAYTOWITNESS", // 用作找零输出
},
{
address: "1FfmbHfnpaZjKFvyi1okTjJJusN455paPH",
amount: "205200",
script_type: "PAYTOADDRESS", // 对应收款方输出
},
];
const refTxs = [
{
hash: "d29e7c4b1a0f3d5e6c7b8a99112233445566778899aabbccddeeff0012345678",
version: 1,
lock_time: 0,
inputs: [
/* 这里填写前序交易的 inputs */
],
bin_outputs: [
/* 这里填写前序交易的 outputs */
],
},
];
const { success, payload } = await ukeySdk.btcSignTransaction(
connectId,
deviceId,
{
coin: "btc",
inputs,
outputs,
refTxs,
locktime: 0,
},
);
if (success) {
console.info("可广播的已签名交易:", payload.serializedTx); // 这时就能直接发出去了
}
末尾核对
| 核验项 | 建议处理 |
|---|---|
| 路径说明与脚本 | 44' 对应 P2PKH,49' 对应 P2SH-P2WPKH,84' 对应 P2WPKH,86' 对应 P2TR。 |
| 前序交易 | refTxs 要把每个输入的来源交易补全,否则设备会直接拒签。 |
| 找零地址 | 找零返回值里的 address_n 应留在同一账户路径说明内,避免和收款地址混淆。 |
| 屏幕内容 | 交易输入、返回值、找零和手续费摘要要和设备展示保持一致。 |
| 结果核对 | 可以用 bitcoinjs-lib 或 @scure/btc-signer 重建交易,再比对 txid 和 serializedTx。 |
| 常见卡点 | 输入过多时先拆分批次,手续费异常时调整返回值与找零,用户拒签或超时则提供重试和取消。 |
继续查看具体 API:btcSignTransaction · btcGetAddress。