Bitcoin 钱包开发流程|区块链技术系列课程 #10

  • Dapplink
  • 更新于 2024-06-18 16:03
  • 阅读 1571

Bitcoin 钱包开发流程

作者丨Seek image.png

地址生成


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 原生 Bitcoin 接口

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

2 Rosetta Api

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:通过查询比特币节点,返回指定地址的余额。

发送交易到区块链网络:

  • /construction/submit:通过比特币节点提交签名后的交易。

3 文档资料

中心化钱包开发 

1 离线地址生成

  • 调度签名机生成密钥对,签名机吐出公钥

  • 使用公钥匙导出地址

2 充值逻辑

  • 获得最新块高;更新到数据库

  • 从数据库中获取上次解析交易的块高做为起始块高,最新块高为截止块高,如果数据库中没有记录,说明需要从头开始扫,起始块高为 0;

  • 解析区块里面的交易,to 地址是系统内部的用户地址,说明用户充值,更新交易到数据库中,将交易的状态设置为待确认。

  • 所在块的交易过了确认位,将交易状态更新位充值成功并通知业务层。

  • 解析到的充值交易需要在钱包的数据库里面维护 UTXO

3 提现逻辑

  • 获取离线签名需要的参数,给合适的手续费

  • 构建未签名的交易消息摘要,将消息摘要递给签名机签名

  • 构建完整的交易并进行序列化

  • 发送交易到区块链网络

  • 扫链获取到交易之后更新交易状态并上报业务层

4 归集逻辑

  • 将用户地址上的资金转到归集地址,签名流程类似提现

  • 发送交易到区块链网络

  • 扫链获取到交易之后更新交易状态

5 转冷逻辑

  • 将热钱包地址上的资金转到冷钱包地址,签名流程类似提现

  • 发送交易到区块链网络

  • 扫链获取到交易之后更新交易状态

6 冷转热逻辑

  • 手动操作转账到热钱包地址

  • 扫链获取到交易之后更新交易状态

注意 👇

交费的学员需要完整的项目实战代码可寻求 The Web3 社区索取

HD 钱包开发

1 离线地生成和离线签名

参考上面的代码

2 和链上交互的接口

image.png

总结

HD 钱包和交易所钱包不同之处有以下几点:

1 密钥管理方式不同

  • HD 钱包私钥在本地设备,私钥用户自己控制

  • 交易所钱包中心化服务器(CloadHSM, TEE 等),私钥项目方控制

2 资金存在方式不同

  • HD 资金在用户钱包地址

  • 交易所钱包资金在交易所热钱包或者冷钱包里面

3 业务逻辑不一致

  • 中心化钱包:实时不断扫链更新交易数据和状态

  • HD 钱包:根据用户的操作通过请求接口实现业务逻辑

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
Dapplink
Dapplink
0xBdcb...f214
首个模块化、可组合的Layer3协议。