区块链开发之BTC钱包APP

目录

  • 基础知识
  • 开发准备
  • 实战开发
  • 总结
  • 参考文献

基础知识

1、什么是比特币?

  • 比特币是一种创新的支付网络和一种新型的货币。
  • 比特币本质上就是一个分布式的P2P网络系统。它由一系列电脑(或其它计算设备)相互连通构成一个P2P网络。每个电脑上都装有一系列的软件,这些软件就构成一个分布式计算系统,用于协调这些电脑完成相互链接、相互传递消息和通信、协调各自的任务和分工。最终这些电脑彼此交互要实现一个共同的目标——维护一套数据库的完整和更新。
  • 从技术层面来看,比特币是最早和最成功的区块链应用,它可以被看作一个由加密算法,共识机制,p2p网络等技术组合而成的系统。

2、比特币钱包又是什么?

  • 比特币钱包相当于与比特币交易的实体钱包。不同平台有不同的钱包。要开始使用比特币(比特币),首先你需要一个比特币钱包。它允许您进行交易,即买卖加密货币。比特币钱包的主要任务是存储访问比特币地址所需的密钥,以及相应的资金。
  • 从技术角度来看,比特币本身不存储在任何地方,只存储秘密数字密钥,这可以访问公共比特币地址和“签署”交易的能力。这是为了这个信息,需要一个比特币钱包。钱包是不同的,取决于它们所针对的设备,您甚至可以不使用计算机并将密钥写在纸上。当然,钱包有备份副本并且受到保护以防止未经授权的访问是非常重要的。

3、比特币交易又是什么?

  • 比特币的交易(Transation,缩写Tx),并不是通常意义的交易,例如一手交钱一手交货,而是转账。交易由N个输入和M个输出两部分组成。交易的每个输入便是前向交易的某个输出,那么追踪到源头,必然出现一个没有输入的交易,此类交易称为CoinBase Tx。CoinBase类交易是奖励挖矿者而产生的交易,该交易总是位于Block块的第一笔。


    区块链开发之BTC钱包APP_第1张图片
    image

    注:此图来自比特币白皮书

开发准备

1、bitcoinj基础:

bitcoinj是用于处理比特币协议的库。它可以维护一个钱包,发送/接收交易,而无需本地的Bitcoin Core副本,并具有许多其他高级功能。它用Java实现。

2、bitcoinj特征:

  • 高度优化的轻量级简化付款验证(SPV)模式。在这种模式下,仅下载了一小部分区块链,从而使比特币适合在受约束的设备(如智能手机或廉价的虚拟专用服务器)上使用。
  • 实验性完整验证模式,其验证工作与Bitcoin Core相同。在这种模式下,将计算未使用的事务输出集(UTXO集),并且由于有PostgreSQL存储,因此可以将其索引到数据库中,从而可以按地址快速查找余额。
  • 带有加密,费用计算,多重签名,确定性密钥派生,可插入硬币选择/硬币控制,扩展支持和事件监听器的钱包类,可让您随时了解余额的变化。
  • 支持小额支付通道,使您可以在客户端和服务器之间建立多签名合同,然后在该通道上进行协商,从而实现快速的小额支付,从而避免了矿工费用。
  • 为网络IO 提供异步和每连接线程数,从而允许您在可伸缩性和仅阻止功能(例如SOCKS代理)之间进行选择。
  • 轻松实现使用比特币合约功能的应用程序。
  • 一个简单的GUI钱包应用程序,您可以将其用作自己的应用程序的基础。观看或阅读有关如何对其进行自定义并构建不需要Java的本机安装程序的教程。
  • 用于处理钱包和链式文件,支付协议,网络等的命令行工具。

3、我们怎么使用bitcoinj:

  1. 在Github下载源码,自己编译。
gradle clean build
gradle clean assemble
  1. 使用Maven或Gradle依赖。
 compile  'org.bitcoinj:bitcoinj-core:0.15.6'
 for Android
 implementation  'org.bitcoinj:bitcoinj-core:0.15.6'

实战开发

生成账号:
1、随机生成账号

String mnemonic = ChainUtil.genMnemonic(Words.TWELVE);
ECKey ecKey = ChainUtil.genECKey(mnemonic, "m/44'/0'/0'/0/0");
Address address = Address.fromKey(MainNetParams.get(), ecKey, Script.ScriptType.P2PKH);
System.out.println("mnemonic:" + mnemonic);
System.out.println("privateKey:" +ecKey.getPrivateKeyAsWiF(MainNetParams.get()));
System.out.println("publicKey:" + ecKey.getPublicKeyAsHex());
System.out.println("address:" + address.toString());
输出:
mnemonic:across slow mechanic portion hospital perfect zone best capable champion pond exhaust
privateKey:L1QVhXZ4bBnQGwAWYDRCNQrkyeSLCiBjKhp5ewLKdXwDpxYVoifj 
publicKey:020afa4282079e6898fe04da1ba2c0eb6eec6e0c04d691c281869672ea212f1fe1
address:17TBeyxjCpTcZb3TUg8b1GRR6DCJ5mi1UU

2、助记词导入

String mnemonic = "across slow mechanic portion hospital perfect zone best capable champion pond exhaust";
ECKey ecKey = ChainUtil.genECKey(mnemonic, "m/44'/0'/0'/0/0");
Address address = Address.fromKey(MainNetParams.get(), ecKey, Script.ScriptType.P2PKH);
System.out.println("mnemonic:" + mnemonic);
System.out.println("privateKey:" + ecKey.getPrivateKeyAsWiF(MainNetParams.get()));
System.out.println("publicKey:" + ecKey.getPublicKeyAsHex());
System.out.println("address:" + address.toString());
输出:
mnemonic:across slow mechanic portion hospital perfect zone best capable champion pond exhaust
privateKey:L1QVhXZ4bBnQGwAWYDRCNQrkyeSLCiBjKhp5ewLKdXwDpxYVoifj 
publicKey:020afa4282079e6898fe04da1ba2c0eb6eec6e0c04d691c281869672ea212f1fe1
address:17TBeyxjCpTcZb3TUg8b1GRR6DCJ5mi1UU

3、私钥导入

String privateKey = "L1QVhXZ4bBnQGwAWYDRCNQrkyeSLCiBjKhp5ewLKdXwDpxYVoifj";
ECKey ecKey = DumpedPrivateKey.fromBase58(MainNetParams.get(), privateKey).getKey();
Address address = Address.fromKey(MainNetParams.get(), ecKey, Script.ScriptType.P2PKH);
System.out.println("privateKey:" +ecKey.getPrivateKeyAsWiF(MainNetParams.get()));
System.out.println("publicKey:" + ecKey.getPublicKeyAsHex());
System.out.println("address:" + address.toString());
输出:
privateKey:L1QVhXZ4bBnQGwAWYDRCNQrkyeSLCiBjKhp5ewLKdXwDpxYVoifj
publicKey:020afa4282079e6898fe04da1ba2c0eb6eec6e0c04d691c281869672ea212f1fe1
address:17TBeyxjCpTcZb3TUg8b1GRR6DCJ5mi1UU

4、获取余额:
https://blockchain.info/balance?active=$address

curl --location --request GET 'https://blockchain.info/balance?active=39rgKZGLDfNcQ9nLrNEujpm53c6SAqfM8M'
结果:
{
    "39rgKZGLDfNcQ9nLrNEujpm53c6SAqfM8M": {
        "final_balance": 0,
        "n_tx": 2,
        "total_received": 712880
    }
}
地址:39rgKZGLDfNcQ9nLrNEujpm53c6SAqfM8M
解析final_balance字段就可以拿到余额了

5、获取未花费的交易输出(Unspent outputs)
https://blockchain.info/unspent?active=$address

curl --location --request GET 'https://blockchain.info/unspent?&active=1KFHE7w8BhaENAswwryaoccDb6qcT6DbYY'
结果:
{
    "notice": "",
    "unspent_outputs": [
        {
            "tx_hash": "9b14b25798b431d2d0f749e958fd8cec0bbe109e16d890754671aec3a0602628",
            "tx_hash_big_endian": "282660a0c3ae71467590d8169e10be0bec8cfd58e949f7d0d231b49857b2149b",
            "tx_output_n": 0,
            "script": "76a914c825a1ecf2a6830c4401620c3a16f1995057c2ab88ac",
            "value": 1274264286,
            "value_hex": "4bf3bade",
            "confirmations": 138,
            "tx_index": 0
        }
    ]
}

说明:unspent_outputs就是你未花费的列表。

6、费用估算API

curl --location --request GET 'https://bitcoinfees.earn.com/api/v1/fees/recommended' \
--header 'Cookie: __cfduid=d402b8e2522ca17f6b6cbb1392842166b1572919642'
结果:
{
    "fastestFee": 8,
    "halfHourFee": 4,
    "hourFee": 2
}

7、手续费计算。

  • size 的计算公式
size = 148 x inputNum + 34 x outputNum + 10
算出字节数后,再乘以rate(Satoshi/byte),rate通过费用估算API获取
提示:所以为了转账少花手续费,最好把utxo列表根据余额从大到小做个排序
  • 手续费计算:
unSpentBTCList
value
rate sta/byte
-1发送的value超出了你的余额
public static long getFee(@NonNull List unSpentBTCList, long value, int rate) {
    long fee = 0L;
    int inputNum = 0;
    long totalMoney = 0;
    for (UnSpentBTC us : unSpentBTCList) {
        inputNum++;
        totalMoney += us.getSatoshis();
        if (totalMoney > value) {
            fee = (148 * inputNum + 34 * 1 + 10) * rate;
            if (totalMoney == (value + fee))
                return fee;
            else if (totalMoney > (value + fee)) {
                fee = (148 * inputNum + 34 * 2 + 10) * rate;
                if (totalMoney >= (value + fee))
                    return fee;
            }
        }
    }
     return -1;
}

有的朋友可能还会需要算出,最大能够发送的钱数,我这里也给一下代码,仅做参考:

public static long getMaxSendValue(List unSpentBTCList, long totalMoney, int rate) {

        while (true) {
            long fee = getFee(unSpentBTCList, totalMoney, rate);
            if (fee == -1)
                totalMoney -= 100;
            else
                break;
        }
       /* long fee = getFee(unSpentBTCList, totalMoney, rate);
        if (fee == -1) {
            totalMoney -= 100;
            getMaxSendValue(unSpentBTCList, totalMoney, rate);
        }*/
        return totalMoney;
    }

8、签名交易

NetworkParameters params = MainNetParams.get();
        String formAddr = "你的地址";
        String privateKey = "你的私钥";
        String recevieAddr = "接收地址";
        long amount = 1000; // "你要转多少"
        List unUtxos = getUnspentOutputs(formAddr);
        int sat = 10;//手续费比列
        if (null != unUtxos && !unUtxos.isEmpty()) {
            Collections.sort(unUtxos, (o1, o2) -> o2.getValue() - o1.getValue());
            List utxos = new ArrayList<>();
            DumpedPrivateKey dumpedPrivateKey = null;
            ECKey key = null;
            if (!TextUtils.isEmpty(privateKey)) {
                dumpedPrivateKey = DumpedPrivateKey.fromBase58(params, privateKey);
                key = dumpedPrivateKey.getKey();
            }
            // 接收地址
            Address receiveAddress = Address.fromString(params, recevieAddr);
            // 构建交易
            Transaction tx = new Transaction(params);
            tx.addOutput(Coin.valueOf(amount), receiveAddress); // 转出
            // 如果需要找零 消费列表总金额 - 已经转账的金额 - 手续费
            long value = 0;
            int fee = getFee(unUtxos, amount, sat);
            int size = fee / sat;
            for (UnspentOutputsBean.UnspentOutputsBean unUtxo : unUtxos) {
                value += unUtxo.getValue();
                utxos.add(new UTXO(Sha256Hash.wrap(unUtxo.getTx_hash_big_endian()),
                        unUtxo.getTx_output_n(),
                        Coin.valueOf(unUtxo.getValue()),
                        unUtxo.getConfirmations(),
                        false,
                        new Script(HexUtils.toBytes(unUtxo.getScript()))));
                if (value > fee + amount) {
                    break;
                }
            }
            Address toAddress = Address.fromString(params, formAddr);
            tx.addOutput(Coin.valueOf(value - amount - fee), toAddress); //找零地址

            for (UTXO utxo : utxos) {
                TransactionOutPoint outPoint = new TransactionOutPoint(params, utxo.getIndex(), utxo.getHash());
                // YOU HAVE TO CHANGE THIS
                if (key != null) {
                    tx.addSignedInput(outPoint, utxo.getScript(), key, Transaction.SigHash.ALL, true);
                }
            }
            org.bitcoinj.core.Context context = new org.bitcoinj.core.Context(params);
            tx.getConfidence().setSource(TransactionConfidence.Source.NETWORK);
            tx.setPurpose(Transaction.Purpose.USER_PAYMENT);
 
            System.out.println("size: " + size);
            System.out.println("sat: " + sat);
            System.out.println("fee: " + fee);
            System.out.println("hash: " + tx.getTxId().toString());
            System.out.println("serializeHex: " + Hex.toHexString(tx.bitcoinSerialize()));
        }

9、广播
在上一步拿到了序列化的16进制交易,我们拿到了后直接广播就可以了。

curl --location --request POST 'https://blockchain.info/pushtx' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'tx=你的16进制交易数据'

返回状态码200就广播成功了,接下来就等着被确认了。

10、获取交易详情。
在第7步的时候已经拿到了交易hash,并在8步交易成功后,就可以查询交易详情就,具体操作如下。
https://blockchain.info/rawtx/$tx_hash

curl --location --request GET 'https://blockchain.info/rawtx/e56b59f60f45842e43e4f3cf418d0e090b358b3c7433bde6cecb26f04f3b1b3a'

结果:
{
   "ver": 2,
   "inputs": [
      {
         "sequence": 4294967295,
         "witness": "0000000000000000000000000000000000000000000000000000000000000000",
         "script": "032c800904eb7e785e45552f48756f42692ffabe6d6d177b70cf4446e4298c1c2d14e8eec6d95846065d0e6fc12cd1a9b0726bdf16400800000090fed2180358dcf2a44b140000000000"
      }
   ],
   "weight": 1048,
   "block_height": 622636,
   "relayed_by": "0.0.0.0",
   "out": [
      {
         "spent": true,
         "spending_outpoints": [
            {
               "tx_index": 0,
               "n": 13
            }
         ],
         "tx_index": 0,
         "type": 0,
         "addr": "1MvYASoHjqynMaMnP7SBmenyEWiLsTqoU6",
         "value": 1261606981,
         "n": 0,
         "script": "76a914e582933875bedfdc448473c00b474f8f053a467588ac"
      },
      {
         "spent": false,
         "tx_index": 0,
         "type": 0,
         "value": 0,
         "n": 1,
         "script": "6a24aa21a9edf8ce97a293f7d1ba0a8e5634d3a12080bbfdf22a3392005c52a2ca9ad6ea65ca"
      },
      {
         "spent": false,
         "tx_index": 0,
         "type": 0,
         "value": 0,
         "n": 2,
         "script": "6a24b9e11b6d8d12ceae709815f42c2aedf406aa65d9e25b4c6075a7ce5f8505c0ff18c946ef"
      }
   ],
   "lock_time": 0,
   "size": 289,
   "block_index": 0,
   "time": 1584955126,
   "tx_index": 0,
   "vin_sz": 1,
   "hash": "e56b59f60f45842e43e4f3cf418d0e090b358b3c7433bde6cecb26f04f3b1b3a",
   "vout_sz": 3
}

总结

1、生成比特币账号相对简单点,都提供了相关的API,调用即可。
2、交易就相对麻烦一点,要知道比特币的交易模型,添加输入输出,计算手续费,并构建16进制的交易数据,广播即可。
3、在创建交易的时候一定要添加找零地址,不然的话后果就是,你没用完的比特币会全部交给矿工,切记 切记 切记。

参考文献

[1] 比特币白皮书:https://bitcoin.org/files/bitcoin-paper/bitcoin_zh_cn.pdf
[2] BitcoinJ Github:https://github.com/bitcoinj/bitcoinj
[3] 比特币官网:https://bitcoin.org/zh_CN/
[4]感谢获取手续费文章:https://blog.csdn.net/wypeng2010/article/details/81357684
[5]blockchain:https://www.blockchain.com/zh-cn/api/blockchain_api

你可能感兴趣的:(区块链开发之BTC钱包APP)