Bitcoin 钱包开发流程
1.地址生成
import * as bitcoin from 'bitcoinjs-lib';
import * as ecc from 'tiny-secp256k1';
const { BIP32Factory } = require('bip32');
const bip32 = BIP32Factory(ecc);
export function createAddress (params: any): any {
const { seedHex, receiveOrChange, addressIndex, network, method } = params;
const root = bip32.fromSeed(Buffer.from(seedHex, 'hex'));
let path = "m/44'/0'/0'/0/" + addressIndex + '';
if (receiveOrChange === '1') {
path = "m/44'/0'/0'/1/" + addressIndex + '';
}
const child = root.derivePath(path);
let address: string;
switch (method) {
case 'p2pkh':
// eslint-disable-next-line no-case-declarations
const p2pkhAddress = bitcoin.payments.p2pkh({
pubkey: child.publicKey,
network: bitcoin.networks[network]
});
address = p2pkhAddress.address;
break;
case 'p2wpkh':
// eslint-disable-next-line no-case-declarations
const p2wpkhAddress = bitcoin.payments.p2wpkh({
pubkey: child.publicKey,
network: bitcoin.networks[network]
});
address = p2wpkhAddress.address;
break;
case 'p2sh':
// eslint-disable-next-line no-case-declarations
const p2shAddress = bitcoin.payments.p2sh({
redeem: bitcoin.payments.p2wpkh({
pubkey: child.publicKey,
network: bitcoin.networks[network]
})
});
address = p2shAddress.address;
break;
default:
console.log('This way can not support');
}
return {
privateKey: Buffer.from(child.privateKey).toString('hex'),
publicKey: Buffer.from(child.publicKey).toString('hex'),
address
};
}
export function createMultiSignAddress (params: any): string {
const { pubkeys, network, method, threshold } = params;
switch (method) {
case 'p2pkh':
return bitcoin.payments.p2sh({
redeem: bitcoin.payments.p2ms({
m: threshold,
network: bitcoin.networks[network],
pubkeys
})
}).address;
case 'p2wpkh':
return bitcoin.payments.p2wsh({
redeem: bitcoin.payments.p2ms({
m: threshold,
network: bitcoin.networks[network],
pubkeys
})
}).address;
case 'p2sh':
return bitcoin.payments.p2sh({
redeem: bitcoin.payments.p2wsh({
redeem: bitcoin.payments.p2ms({
m: threshold,
network: bitcoin.networks[network],
pubkeys
})
})
}).address;
default:
console.log('This way can not support');
return '0x00';
}
}
export function createSchnorrAddress (params: any): any {
bitcoin.initEccLib(ecc);
const { seedHex, receiveOrChange, addressIndex } = params;
const root = bip32.fromSeed(Buffer.from(seedHex, 'hex'));
let path = "m/44'/0'/0'/0/" + addressIndex + '';
if (receiveOrChange === '1') {
path = "m/44'/0'/0'/1/" + addressIndex + '';
}
const childKey = root.derivePath(path);
const privateKey = childKey.privateKey;
if (!privateKey) throw new Error('No private key found');
const publicKey = childKey.publicKey;
const tweak = bitcoin.crypto.taggedHash('TapTweak', publicKey.slice(1, 33));
const tweakedPublicKey = Buffer.from(publicKey);
for (let i = 0; i < 32; ++i) {
tweakedPublicKey[1 + i] ^= tweak[i];
}
const { address } = bitcoin.payments.p2tr({
internalPubkey: tweakedPublicKey.slice(1, 33)
});
return {
privateKey: Buffer.from(childKey.privateKey).toString('hex'),
publicKey: Buffer.from(childKey.publicKey).toString('hex'),
address
};
}
2. 离线签名
const ecc = require('tiny-secp256k1');
const { BIP32Factory } = require('bip32');
BIP32Factory(ecc);
const bitcoin = require('bitcoinjs-lib');
const bitcore = require('bitcore-lib');
/**
* @returns
* @param params
*/
export function buildAndSignTx (params: { privateKey: string; signObj: any; network: string; }): string {
const { privateKey, signObj, network } = params;
const net = bitcore.Networks[network];
const inputs = signObj.inputs.map(input => {
return {
address: input.address,
txId: input.txid,
outputIndex: input.vout,
// eslint-disable-next-line new-cap
script: new bitcore.Script.fromAddress(input.address).toHex(),
satoshis: input.amount
};
});
const outputs = signObj.outputs.map(output => {
return {
address: output.address,
satoshis: output.amount
};
});
const transaction = new bitcore.Transaction(net).from(inputs).to(outputs);
transaction.version = 2;
transaction.sign(privateKey);
return transaction.toString();
}
export function buildUnsignTxAndSign (params) {
const { keyPair, signObj, network } = params;
const psbt = new bitcoin.Psbt({ network });
const inputs = signObj.inputs.map(input => {
return {
address: input.address,
txId: input.txid,
outputIndex: input.vout,
// eslint-disable-next-line new-cap
script: new bitcore.Script.fromAddress(input.address).toHex(),
satoshis: input.amount
};
});
psbt.addInput(inputs);
const outputs = signObj.outputs.map(output => {
return {
address: output.address,
satoshis: output.amount
};
});
psbt.addOutput(outputs);
psbt.toBase64();
psbt.signInput(0, keyPair);
psbt.finalizeAllInputs();
const signedTransaction = psbt.extractTransaction().toHex();
console.log('signedTransaction==', signedTransaction);
}
3. 扫链接口
3.1.原生 Bitcoin 接口
获取活跃的最新区块
请求参数
curl --data-binary '{"jsonrpc": "1.0", "id": "curltest", "method": "getchaintips", "params": []}' -H 'content-type: text/plain;' https://thrilling-spring-bush.btc.quiknode.pro/c0d9254cfb049224abd0ece400635e62b791a388/
返回值
{
"result":[
{
"height":845198,
"hash":"00000000000000000000301d584ec5f1c16e89487c05baf035f01875cb763d75",
"branchlen":0,
"status":"active"
},
{
"height":841424,
"hash":"000000000000000000010998fc2714f8ae10ffb73f1986eecc58f5afc457ee07",
"branchlen":1,
"status":"valid-headers"
},
{
"height":838792,
"hash":"00000000000000000002af7214c8796e102b0e9074a5d469266d7afe5af2f087",
"branchlen":1,
"status":"headers-only"
},
{
"height":816358,
"hash":"00000000000000000001d5f92e2dbbfcbc1e859873117e7983dd574857da5e14",
"branchlen":1,
"status":"valid-headers"
},
{
"height":815202,
"hash":"0000000000000000000093917031004a140b6db5c6adec217f814db98d7f0bde",
"branchlen":1,
"status":"valid-fork"
},
],
"error":null,
"id":"curltest"
}
- “invalid” 该分支至少包含一个无效块
- “headers-only” 并非该分支的所有块都可用,但 headers 有效
- “valid-headers”所有块都可用于此分支,但它们从未经过完全验证
- “valid-fork” 该分支不是活动链的一部分,但经过充分验证
- “active”这是活跃主链的提示,这当然有效
获取区块信息
请求示范
curl --data-binary '{"jsonrpc": "1.0", "id": "curltest", "method": "getblockchaininfo", "params": []}' -H 'content-type: text/plain;' https://thrilling-spring-bush.btc.quiknode.pro/c0d9254cfb049224abd0ece400635e62b791a388/
返回值
{
"result":{
"chain":"main",
"blocks":845200,
"headers":845200,
"bestblockhash":"000000000000000000027a970865a12b12e4da473011e2033eeca871c957a747",
"difficulty":84381461788831.34,
"time":1716706327,
"mediantime":1716703878,
"verificationprogress":0.999998974207445,
"initialblockdownload":false,
"chainwork":"00000000000000000000000000000000000000007b695dedb46255cb840f5cb6",
"size_on_disk":652535688171,
"pruned":false,
"warnings":""
},
"error":null,
"id":"curltest"
}
列出未花费的输入输出
请求示范
curl --data-binary '{"jsonrpc": "1.0", "id": "curltest", "method": "listunspent", "params": [845000, 845200, [] , true, { "minimumAmount": 0.005 } ]}' -H 'content-type: text/plain;' https://thrilling-spring-bush.btc.quiknode.pro/c0d9254cfb049224abd0ece400635e62b791a388/
返回值
[
{
"txid" : "",
"vout" : 1,
"address" : "str",
"label" : "str",
"scriptPubKey" : "str",
"amount" : 10000,
"confirmations" : 12,
"redeemScript" : "hex",
"witnessScript" : "str",
"spendable" : false,
"solvable" : false,
"reused" : false,
"desc" : "str",
"safe" : true
},{
"txid" : "",
"vout" : 1,
"address" : "str",
"label" : "str",
"scriptPubKey" : "str",
"amount" : 10000,
"confirmations" : 12,
"redeemScript" : "hex",
"witnessScript" : "str",
"spendable" : false,
"solvable" : false,
"reused" : false,
"desc" : "str",
"safe" : true
},
]
发送交易到区块链网络
请求参数
curl --data-binary '{"jsonrpc": "1.0", "id": "curltest", "method": "sendrawtransaction", "params": ["signedhex"]}' -H 'content-type: text/plain;' https://thrilling-spring-bush.btc.quiknode.pro/c0d9254cfb049224abd0ece400635e62b791a388/
返回值
成功返回交易 Hash
3.2.Rosetta Api
Bitcoin Rosetta API 是由 Coinbase 提出的 Rosetta 标准的一部分,旨在为区块链和钱包提供一个统一的接口标准。这个标准化的接口使得与各种区块链的交互更加容易和一致,无论是对交易数据的读取还是写入。目前已经支持很多链,包含比特币,以太坊等主流链,也包含像 IoTex 和 Oasis 这样的非主流链。
3.2.1.Rosetta API 概述
Rosetta API 分为两部分:
- Data API:用于读取区块链数据。
- Construction API:用于构建和提交交易。
3.2.2. Data API
Data API 提供了一组端点,用于检索区块链数据,如区块、交易、余额等。主要端点包括:
- /network/list:返回支持的网络列表。
- /network/status:返回当前网络的状态信息。
- /network/options:返回支持的网络选项和版本信息。
- /block:返回指定区块的数据。
- /block/transaction:返回指定交易的数据。
- /account/balance:返回指定账户的余额。
- /mempool:返回当前未确认的交易池。
- /mempool/transaction:返回指定未确认交易的数据。
3.2.3. Construction API
Construction API 提供了一组端点,用于创建、签名和提交交易。主要端点包括:
- /construction/preprocess:分析交易需求并返回交易所需的元数据。
- /construction/metadata:返回构建交易所需的元数据。
- /construction/payloads:生成待签名的交易有效载荷。
- /construction/parse:解析交易并返回其操作。
- /construction/combine:将签名与待签名交易合并。
- /construction/hash:返回交易的唯一标识符(哈希)。
- /construction/submit:提交签名后的交易。
3.2.4.开发 BTC 钱包使用到的 Rosetta Api
为了具体实现 Rosetta API,开发者需要遵循 Rosetta 标准并根据比特币区块链的特性进行适配。以下是一些具体实现细节
数据结构:
- 区块:包含区块哈希、前一个区块哈希、区块高度、时间戳、交易列表等。
- 交易:包含交易哈希、输入输出列表、金额、地址等。
- 账户:包含账户地址和余额信息。
用到的接口
- /network/list:返回比特币主网和测试网信息。
- /network/status:返回当前最新区块、已同步区块高度、区块链处理器的状态等。
- /block 和 /block/transaction:返回区块和交易的详细信息,包括交易的输入输出、金额、地址等。
- /account/balance:通过查询比特币节点,返回指定地址的余额。
发送交易到区块链网络
- /construction/submit:通过比特币节点提交签名后的交易。
3.3. 文档资料
-
比特币开发文档:https://developer.bitcoin.org/reference/rpc/
-
Rosetta 开发文档:https://docs.cdp.coinbase.com/rosetta/reference/networklist/
-
Rosetta 开发文档:https://github.com/coinbase/mesh-ecosystem/blob/master/implementations.md
-
浏览器:https://btc.com/zh-CN
4.中心化钱包开发
4.1.离线地址生成
- 调度签名机生成密钥对,签名机吐出公钥
- 使用公钥匙导出地址
4.2.充值逻辑
- 获得最新块高;更新到数据库
- 从数据库中获取上次解析交易的块高做为起始块高,最新块高为截止块高,如果数据库中没有记录,说明需要从头开始扫,起始块高为 0;
- 解析区块里面的交易,to 地址是系统内部的用户地址,说明用户充值,更新交易到数据库中,将交易的状态设置为待确认。
- 所在块的交易过了确认位,将交易状态更新位充值成功并通知业务层。
- 解析到的充值交易需要在钱包的数据库里面维护 UTXO
4.2.提现逻辑
- 获取离线签名需要的参数,给合适的手续费
- 构建未签名的交易消息摘要,将消息摘要递给签名机签名
- 构建完整的交易并进行序列化
- 发送交易到区块链网络
- 扫链获取到交易之后更新交易状态并上报业务层
4.3.归集逻辑
- 将用户地址上的资金转到归集地址,签名流程类似提现
- 发送交易到区块链网络
- 扫链获取到交易之后更新交易状态
4.4.转冷逻辑
- 将热钱包地址上的资金转到冷钱包地址,签名流程类似提现
- 发送交易到区块链网络
- 扫链获取到交易之后更新交易状态
4.5.冷转热逻辑
- 手动操作转账到热钱包地址
- 扫链获取到交易之后更新交易状态
注意:交费的学员需要完整的项目实战代码可寻求 The Web3 社区索取
5.HD 钱包开发
5.1. 离线地生成和离线签名
参考上面的代码
5.2. 和链上交互的接口
-
获取账户余额
-
根据地址获取交易记录
-
根据交易 Hash 获取交易详情
-
获取未花费的输入输出
-
获取交易手续费
-
以上接口请参考代码库:https://github.com/the-web3/wallet-chain-node/tree/develop/wallet/bitcoin
6.总结
HD 钱包和交易所钱包不同之处有以下几点
6.1.密钥管理方式不同
- HD 钱包私钥在本地设备,私钥用户自己控制
- 交易所钱包中心化服务器(CloadHSM, TEE 等),私钥项目方控制
6.2.资金存在方式不同
- HD 资金在用户钱包地址
- 交易所钱包资金在交易所热钱包或者冷钱包里面
6.3.业务逻辑不一致
- 中心化钱包:实时不断扫链更新交易数据和状态
- HD 钱包:根据用户的操作通过请求接口实现业务逻辑