Bitcoin 钱包开发流程
作者丨Seek
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
};
}
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);
}
1、获取活跃的最新区块
请求参数
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”这是活跃主链的提示,这当然有效
2、获取区块信息
请求示范
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"
}
3、列出未花费的输入输出
请求示范
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
},
]
4、发送交易到区块链网络
请求参数
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
Bitcoin Rosetta API 是由 Coinbase 提出的 Rosetta 标准的一部分,旨在为区块链和钱包提供一个统一的接口标准。这个标准化的接口使得与各种区块链的交互更加容易和一致,无论是对交易数据的读取还是写入。目前已经支持很多链,包含比特币,以太坊等主流链,也包含像 IoTex 和 Oasis 这样的非主流链。
2.1.Rosetta API 概述
Rosetta API 分为两部分:
Data API:用于读取区块链数据。
Construction API:用于构建和提交交易。
2.2. Data API
Data API 提供了一组端点,用于检索区块链数据,如区块、交易、余额等。主要端点包括:
/network/list:返回支持的网络列表。
/network/status:返回当前网络的状态信息。
/network/options:返回支持的网络选项和版本信息。
/block:返回指定区块的数据。
/block/transaction:返回指定交易的数据。
/account/balance:返回指定账户的余额。
/mempool:返回当前未确认的交易池。
/mempool/transaction:返回指定未确认交易的数据。
2.3. Construction API
Construction API 提供了一组端点,用于创建、签名和提交交易。主要端点包括:
/construction/preprocess:分析交易需求并返回交易所需的元数据。
/construction/metadata:返回构建交易所需的元数据。
/construction/payloads:生成待签名的交易有效载荷。
/construction/parse:解析交易并返回其操作。
/construction/combine:将签名与待签名交易合并。
/construction/hash:返回交易的唯一标识符(哈希)。
/construction/submit:提交签名后的交易。
2.4. 开发 BTC 钱包使用到的 Rosetta Api
为了具体实现 Rosetta API,开发者需要遵循 Rosetta 标准并根据比特币区块链的特性进行适配。以下是一些具体实现细节
数据结构:
区块:包含区块哈希、前一个区块哈希、区块高度、时间戳、交易列表等。
交易:包含交易哈希、输入输出列表、金额、地址等。
账户:包含账户地址和余额信息。
用到的接口:
/network/list:返回比特币主网和测试网信息。
/network/status:返回当前最新区块、已同步区块高度、区块链处理器的状态等。
/block 和 /block/transaction:返回区块和交易的详细信息,包括交易的输入输出、金额、地址等。
/account/balance:通过查询比特币节点,返回指定地址的余额。
发送交易到区块链网络:
比特币开发文档:
Rosetta 开发文档:
https://docs.cdp.coinbase.com/rosetta/reference/networklist/
Rosetta 开发文档:
https://github.com/coinbase/mesh-ecosystem/blob/master/implementations.md
浏览器:
调度签名机生成密钥对,签名机吐出公钥
使用公钥匙导出地址
获得最新块高;更新到数据库
从数据库中获取上次解析交易的块高做为起始块高,最新块高为截止块高,如果数据库中没有记录,说明需要从头开始扫,起始块高为 0;
解析区块里面的交易,to 地址是系统内部的用户地址,说明用户充值,更新交易到数据库中,将交易的状态设置为待确认。
所在块的交易过了确认位,将交易状态更新位充值成功并通知业务层。
解析到的充值交易需要在钱包的数据库里面维护 UTXO
获取离线签名需要的参数,给合适的手续费
构建未签名的交易消息摘要,将消息摘要递给签名机签名
构建完整的交易并进行序列化
发送交易到区块链网络
扫链获取到交易之后更新交易状态并上报业务层
将用户地址上的资金转到归集地址,签名流程类似提现
发送交易到区块链网络
扫链获取到交易之后更新交易状态
将热钱包地址上的资金转到冷钱包地址,签名流程类似提现
发送交易到区块链网络
扫链获取到交易之后更新交易状态
手动操作转账到热钱包地址
扫链获取到交易之后更新交易状态
注意 👇
交费的学员需要完整的项目实战代码可寻求 The Web3 社区索取
参考上面的代码
获取账户余额
根据地址获取交易记录
根据交易 Hash 获取交易详情
获取未花费的输入输出
获取交易手续费
以上接口请参考代码库:
https://github.com/the-web3/wallet-chain-node/tree/develop/wallet/bitcoin
HD 钱包和交易所钱包不同之处有以下几点:
HD 钱包私钥在本地设备,私钥用户自己控制
交易所钱包中心化服务器(CloadHSM, TEE 等),私钥项目方控制
HD 资金在用户钱包地址
交易所钱包资金在交易所热钱包或者冷钱包里面
中心化钱包:实时不断扫链更新交易数据和状态
HD 钱包:根据用户的操作通过请求接口实现业务逻辑
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!