0

打造属于自己的比特币钱包

腾讯云加社区 2018年03月26日 发布于区块链 cloud.tencent.com

大体上有三种方式可以和比特币网络进行通信:用一个完整节点,SPV节点或通过HTTP API。本教程将使用来自NBitcoin的创造者Nicolas Dorier的QBitNinja HTTP API,但我计划把它扩展成一个完整的通信节点。

打造属于自己的比特币钱包

2018年03月26日 发布,来源:cloud.tencent.com

打造属于自己的比特币钱包

背景

为了能够顺利地读懂本文,您需要有一点C#编程经验并且熟悉NBitcoin。当然如果你研究过Bitcoin C# book就更好了。

设计选择

我们希望打造一个跨平台的钱包,所以.NET Core是我们的首选。我们将使用NBitcoin比特币库,因为它是目前为止最流行的库。这个钱包没有使用图形界面的必要,因此使用命令行界面就够了。

大体上有三种方式可以和比特币网络进行通信:用一个完整节点,SPV节点或通过HTTP API。本教程将使用来自NBitcoin的创造者Nicolas Dorier的QBitNinja HTTP API,但我计划把它扩展成一个完整的通信节点。

在本文章发布的时候(2016.11.29)我还不清楚比特币网络是否会激活隔离见证技术,因此我暂时未将其写入本教程。

下面我会尽量说的通俗易懂,因此可能效率不会那么高。在阅读完本教程之后,您可以去看看这个钱包的应用版本HiddenWallet。这是个修复了BUG,性能也比较高,可以真正拿来用的比特币钱包。

命令行实现解析

这个钱包得具备以下命令:help, generate-wallet, recover-wallet, show-balances, show-history, receive, send.

help命令是干嘛的就不用我说了,这个命令没有其他参数。

generate-wallet, recover-wallet, show-balances, show-historyreceive命令后面可以加参数--指定钱包的文件名。例如wallet-file=wallet.dat。如果wallet-file=未指定参数的话,则应用程序将使用默认配置文件中指定的钱包文件。

send命令后面同样可以附加钱包文件名和一些其他参数,如:

  • btc=3.2
  • address=1F1tAaz5x1HUXrCNLbtMDqcw6o5GNn4xqX

几个例子:

  • dotnet run generate-wallet wallet-file=wallet.dat
  • dotnet run receive wallet-file=wallet.dat
  • dotnet run show-balances wallet-file=wallet.dat
  • dotnet run send wallet-file=wallet.dat btc=3.2 address=1F1tAaz5x1HUXrCNLbtMDqcw6o5GNn4x
  • dotnet run show-history wallet-file = wallet.dat

现在我们继续创建一个新的.NET Core命令行程序,你可以自己随喜好去实现这些命令,或者跟着我的代码来也行。

然后从NuGet管理器中添加NBitcoin和QBitNinja.Client。

创建配置文件

第一次运行程序时,它会生成带默认参数的配置文件:

{
  "DefaultWalletFileName": "Wallet.json",
  "Network": "Main",
  "ConnectionType": "Http",
  "CanSpendUnconfirmed": "False"
}

Config.json文件存储全局设置。

Network的值的可以是MainTestNet。当你在处于开发阶段时你可以把它设置为测试模式(TestNet)。CanSpendUnconfirmed也可以设置为TrueConnectionType可以是HttpFullNode,但如果设置为FullNode的话,程序会抛出异常

为了方便的设置配置文件,我创建了一个类:Config

public static class Config
{
    // 使用默认属性初始化
    public static string DefaultWalletFileName = @"Wallet.json";
    public static Network Network = Network.Main;
    ....
}

你可以用你喜欢的方式来管理这个配置文件,或者跟着我的代码来。

命令

generate-wallet

输出示例

Choose a password:

Confirm password:

Wallet is successfully created.
Wallet file: Wallets/Wallet.json

Write down the following mnemonic words.
With the mnemonic words AND your password you can recover this wallet by using the recover-wallet command.

-------
renew frog endless nature mango farm dash sing frog trip ritual voyage
-------

代码

首先要确定指定名字的钱包文件不存在,以免意外覆盖一个已经存在的钱包文件。

var walletFilePath =  GetWalletFilePath ( args ); 
AssertWalletNotExists ( walletFilePath );

那么要怎样怎样妥当地管理我们的钱包私钥呢?我写了一个HBitcoin(GitHubNuGet)的库,里面有一个类叫Safe类,我强烈建议你使用这个类,这样能确保你不会出什么差错。如果你想自己手动去实现密钥管理类的话,你得有十足的把握。不然一个小错误就可能会导致灾难性的后果,您的客户可能会损失掉钱包里的资金。

之前我很全面地写了一些关于这个类的使用方法:这个链接是高级版这个链接是简单版

在原始版本中,为了让那些Safe类的使用者们不被那些NBitcoin 的复杂引用搞的头晕,我把很多细节都隐藏起来了。但对于这篇文章,我对Safe做了稍许修改,因为本文章的读者应该水平更高一点。

工作流程:
  1. 用户输入密码
  2. 用户确认密码
  3. 创建钱包
  4. 显示助记符

首先用户输入密码并确认密码。如果您决定自己写,请在不同的系统上进行测试。相同的代码在不同的终端可能有不同的结果。

string pw;
string pwConf;
do
{
    // 1. 用户输入密码
    WriteLine("Choose a password:");
    pw = PasswordConsole.ReadPassword();
    // 2. 用户确认密码
    WriteLine("Confirm password:");
    pwConf = PasswordConsole.ReadPassword();

    if (pw != pwConf) WriteLine("Passwords do not match. Try again!");
} while (pw != pwConf);

接下来用我的修改后的Safe类创建一个钱包并显示助记符。

// 3. 创建钱包
string mnemonic;
Safe safe = Safe.Create(out mnemonic, pw, walletFilePath, Config.Network);
// 如果没有异常抛出的话,此时就会创建一个钱包
WriteLine();
WriteLine("Wallet is successfully created.");
WriteLine($"Wallet file: {walletFilePath}");

// 4. 显示助记符
WriteLine();
WriteLine("Write down the following mnemonic words.");
WriteLine("With the mnemonic words AND your password you can recover this wallet by using the recover-wallet command.");
WriteLine();
WriteLine("-------");
WriteLine(mnemonic);
WriteLine("-------");

recover-wallet

输出示例

Your software is configured using the Bitcoin TestNet network.
Provide your mnemonic words, separated by spaces:
renew frog endless nature mango farm dash sing frog trip ritual voyage
Provide your password. Please note the wallet cannot check if your password is correct or not. If you provide a wrong password a wallet will be recovered with your provided mnemonic AND password pair:

Wallet is successfully recovered.
Wallet file: Wallets/jojdsaoijds.json

代码

无需多解释,代码很简单,很容易理解:

var walletFilePath = GetWalletFilePath(args);
AssertWalletNotExists(walletFilePath);

WriteLine($"Your software is configured using the Bitcoin {Config.Network} network.");
WriteLine("Provide your mnemonic words, separated by spaces:");
var mnemonic = ReadLine();
AssertCorrectMnemonicFormat(mnemonic);

WriteLine("Provide your password. Please note the wallet cannot check if your password is correct or not. If you provide a wrong password a wallet will be recovered with your provided mnemonic AND password pair:");
var password = PasswordConsole.ReadPassword();

Safe safe = Safe.Recover(mnemonic, password, walletFilePath, Config.Network);
// 如果没有异常抛出,钱包会被顺利恢复
WriteLine();
WriteLine("Wallet is successfully recovered.");
WriteLine($"Wallet file: {walletFilePath}");

安全提示:

攻击者如果想破解一个比特币钱包,他必须知道(passwordmnemonicpassword钱包文件)。而其他钱包只要知道助记符就够了。

receive

输出示例

Type your password:

Wallets/Wallet.json wallet is decrypted.
7 Receive keys are processed.

---------------------------------------------------------------------------
Unused Receive Addresses
---------------------------------------------------------------------------
mxqP39byCjTtNYaJUFVZMRx6zebbY3QKYx
mzDPgvzs2Tbz5w3xdXn12hkSE46uMK2F8j
mnd9h6458WsoFxJEfxcgq4k3a2NuiuSxyV
n3SiVKs8fVBEecSZFP518mxbwSCnGNkw5s
mq95Cs3dpL2tW8YBt41Su4vXRK6xh39aGe
n39JHXvsUATXU5YEVQaLR3rLwuiNWBAp5d
mjHWeQa63GPmaMNExt14VnjJTKMWMPd7yZ

代码

到目前为止,我们都不必与比特币网络进行通信。下面就来了,正如我之前提到的,这个钱包有两种方法可以与比特币网络进行通信。通过HTTP API和使用完整节点。(稍后我会解释为什么我不实现完整节点的通信方式)。

我们现在有两种方式可以分别实现其余的命令,好让它们都能与区块链进行通信,。当然这些命令也需要访问Safe类:

var walletFilePath = GetWalletFilePath(args);
Safe safe = DecryptWalletByAskingForPassword(walletFilePath);

if (Config.ConnectionType == ConnectionType.Http)
{
    // 从现在开始,我们下面的工作都在这里进行
}
else if (Config.ConnectionType == ConnectionType.FullNode)
{
    throw new NotImplementedException();
}
else
{
    Exit("Invalid connection type.");
}

我们将使用QBitNinja.Client作为我们的HTTP API,您可以在NuGet中找到它。对于完整节点通信,我的想法是在本地运行QBitNinja.Server和bitcoind客户端。这样Client(客户端)就可以连上了,并且代码也会比较统一规整。只是有个问题,QBitNinja.Server目前还不能在.NET Core上运行。

receive命令是最直接的。我们只需向用户展示7个未使用的地址就行了,这样它就可以开始接收比特币了。

下面我们该做的就是用QBitNinja jutsu(QBit忍术)来查询一堆数据:

Dictionary<BitcoinAddress, List<BalanceOperation>> operationsPerReceiveAddresses = QueryOperationsPerSafeAddresses(safe, 7, HdPathType.Receive);

上面的语句可能有点难懂。不要逃避,那样你会什么都不懂得。它的基本功能是:给我们一个字典,其中键是我们的safe类(钱包)的地址,值是这些地址上的所有操作。操作列表的列表,换句话说就是:这些操作按地址就行分组。这样我们就有足够的信息来实现所有命令而不需要再去进一步查询区块链了。

public static Dictionary<BitcoinAddress, List<BalanceOperation>> QueryOperationsPerSafeAddresses(Safe safe, int minUnusedKeys = 7, HdPathType? hdPathType = null)
{
    if (hdPathType == null)
    {
        Dictionary<BitcoinAddress, List<BalanceOperation>> operationsPerReceiveAddresses = QueryOperationsPerSafeAddresses(safe, 7, HdPathType.Receive);
        Dictionary<BitcoinAddress, List<BalanceOperation>> operationsPerChangeAddresses = QueryOperationsPerSafeAddresses(safe, 7, HdPathType.Change);

        var operationsPerAllAddresses = new Dictionary<BitcoinAddress, List<BalanceOperation>>();
        foreach (var elem in operationsPerReceiveAddresses)
            operationsPerAllAddresses.Add(elem.Key, elem.Value);
        foreach (var elem in operationsPerChangeAddresses)
            operationsPerAllAddresses.Add(elem.Key, elem.Value);
        return operationsPerAllAddresses;
    }

    var addresses = safe.GetFirstNAddresses(minUnusedKeys, hdPathType.GetValueOrDefault());
    //var addresses = FakeData.FakeSafe.GetFirstNAddresses(minUnusedKeys);

    var operationsPerAddresses = new Dictionary<BitcoinAddress, List<BalanceOperation>>();
    var unusedKeyCount = 0;
    foreach (var elem in QueryOperationsPerAddresses(addresses))
    {
        operationsPerAddresses.Add(elem.Key, elem.Value);
        if (elem.Value.Count == 0) unusedKeyCount++;
    }
    WriteLine($"{operationsPerAddresses.Count} {hdPathType} keys are processed.");

    var startIndex = minUnusedKeys;
    while (unusedKeyCount < minUnusedKeys)
    {
        addresses = new HashSet<BitcoinAddress>();
        for (int i = startIndex; i < startIndex + minUnusedKeys; i++)
        {
            addresses.Add(safe.GetAddress(i, hdPathType.GetValueOrDefault()));
            //addresses.Add(FakeData.FakeSafe.GetAddress(i));
        }
        foreach (var elem in QueryOperationsPerAddresses(addresses))
        {
            operationsPerAddresses.Add(elem.Key, elem.Value);
            if (elem.Value.Count == 0) unusedKeyCount++;
        }
        WriteLine($"{operationsPerAddresses.Count} {hdPathType} keys are processed.");
        startIndex += minUnusedKeys;
    }

    return operationsPerAddresses;
}

这些代码做了很多事。基本上它所做的是查询我们指定的每个地址的所有操作。首先,如果safe类中的前7个地址不是全部未使用的,我们就进行查询,然后继续查询后面7个地址。如果在组合列表中,仍然没有找到7个未使用的地址,我们再查询7个,以此次类推完成查询。if ConnectionType.Http的结尾,我们完成了任何有关我们的钱包密钥的所有操作。而且,这些操作在与区块链沟通的其他命令中都是必不可少的,这样我们后面就轻松了。现在我们来学习如何用operationsPerAddresses来向用户输出相关信息。

receive命令是最简单的一个。它只是向向用户展示了所有未使用和正处于监控中的地址:

Dictionary<BitcoinAddress, List<BalanceOperation>> operationsPerReceiveAddresses = QueryOperationsPerSafeAddresses(safe, 7, HdPathType.Receive);

WriteLine("---------------------------------------------------------------------------");
WriteLine("Unused Receive Addresses");
WriteLine("---------------------------------------------------------------------------");
foreach (var elem in operationsPerReceiveAddresses)
    if (elem.Value.Count == 0)
        WriteLine($"{elem.Key.ToWif()}");

请注意elem.Key是比特币地址。

show-history

输出示例

Type your password:

Wallets/Wallet.json wallet is decrypted.
7 Receive keys are processed.
14 Receive keys are processed.
21 Receive keys are processed.
7 Change keys are processed.
14 Change keys are processed.
21 Change keys are processed.

---------------------------------------------------------------------------
Date            Amount        Confirmed    Transaction Id
---------------------------------------------------------------------------
12/2/16 10:39:59 AM    0.04100000    True        1a5d0e6ba8e57a02e9fe5162b0dc8190dc91857b7ace065e89a0f588ac2e7316
12/2/16 10:39:59 AM    -0.00025000    True        56d2073b712f12267dde533e828f554807e84fc7453e4a7e44e78e039267ff30
12/2/16 10:39:59 AM    0.04100000    True        3287896029429735dbedbac92712283000388b220483f96d73189e7370201043
12/2/16 10:39:59 AM    0.04100000    True        a20521c75a5960fcf82df8740f0bb67ee4f5da8bd074b248920b40d3cc1dba9f
12/2/16 10:39:59 AM    0.04000000    True        60da73a9903dbc94ca854e7b022ce7595ab706aca8ca43cb160f02dd36ece02f
12/2/16 10:39:59 AM    -0.00125000    True 

代码

跟着我来:

AssertArgumentsLenght(args.Length, 1, 2);
var walletFilePath = GetWalletFilePath(args);
Safe safe = DecryptWalletByAskingForPassword(walletFilePath);

if (Config.ConnectionType == ConnectionType.Http)
{
// 0.查询所有操作,把使用过的Safe地址(钱包地址)按组分类
Dictionary<BitcoinAddress, List<BalanceOperation>> operationsPerAddresses = QueryOperationsPerSafeAddresses(safe);

WriteLine();
WriteLine("---------------------------------------------------------------------------");
WriteLine("Date\t\t\tAmount\t\tConfirmed\tTransaction Id");
WriteLine("---------------------------------------------------------------------------");

Dictionary<uint256, List<BalanceOperation>> operationsPerTransactions = GetOperationsPerTransactions(operationsPerAddresses);

// 3. 记录交易历史
// 向用户展示历史记录信息这个功能是可选的
var txHistoryRecords = new List<Tuple<DateTimeOffset, Money, int, uint256>>();
foreach (var elem in operationsPerTransactions)
{
    var amount = Money.Zero;
    foreach (var op in elem.Value)
        amount += op.Amount;
    var firstOp = elem.Value.First();

    txHistoryRecords
        .Add(new Tuple<DateTimeOffset, Money, int, uint256>(
            firstOp.FirstSeen,
            amount,
            firstOp.Confirmations,
            elem.Key));
}

// 4. 把记录按时间或确认顺序排序(按时间排序是无效的, 因为QBitNinja有这么个bug)
var orderedTxHistoryRecords = txHistoryRecords
    .OrderByDescending(x => x.Item3) // 时间排序
    .ThenBy(x => x.Item1); // 首项
foreach (var record in orderedTxHistoryRecords)
{
    // Item2是总额
    if (record.Item2 > 0) ForegroundColor = ConsoleColor.Green;
    else if (record.Item2 < 0) ForegroundColor = ConsoleColor.Red;
    WriteLine($"{record.Item1.DateTime}\t{record.Item2}\t{record.Item3 > 0}\t\t{record.Item4}");
    ResetColor();
}

show-balances

输出示例

Type your password:

Wallets/test wallet is decrypted.
7 Receive keys are processed.
14 Receive keys are processed.
7 Change keys are processed.
14 Change keys are processed.

---------------------------------------------------------------------------
Address                    Confirmed    Unconfirmed
---------------------------------------------------------------------------
mk212H3T5Hm11rBpPAhfNcrg8ioL15zhYQ    0.0655        0
mpj1orB2HDp88shsotjsec2gdARnwmabug    0.09975        0

---------------------------------------------------------------------------
Confirmed Wallet Balance: 0.16525btc
Unconfirmed Wallet Balance: 0btc<code>
---------------------------------------------------------------------------</code>

代码

它与前一个类似,有点难懂。跟着我来:

// 0.查询所有操作,按地址分组 
Dictionary<BitcoinAddress, List<BalanceOperation>> operationsPerAddresses = QueryOperationsPerSafeAddresses(safe, 7);

//1.通过wrapper类取得所有地址历史记录
var addressHistoryRecords = new List<AddressHistoryRecord>();
foreach (var elem in operationsPerAddresses)
{
    foreach (var op in elem.Value)
    {
        addressHistoryRecords.Add(new AddressHistoryRecord(elem.Key, op));
    }
}

// 2. 计算钱包余额
Money confirmedWalletBalance;
Money unconfirmedWalletBalance;
GetBalances(addressHistoryRecords, out confirmedWalletBalance, out unconfirmedWalletBalance);

// 3. 把所有地址历史记录按地址分组
var addressHistoryRecordsPerAddresses = new Dictionary<BitcoinAddress, HashSet<AddressHistoryRecord>>();
foreach (var address in operationsPerAddresses.Keys)
{
    var recs = new HashSet<AddressHistoryRecord>();
    foreach(var record in addressHistoryRecords)
    {
        if (record.Address == address)
            recs.Add(record);
    }
    addressHistoryRecordsPerAddresses.Add(address, recs);
}

// 4. 计算地址的余额
WriteLine();
WriteLine("---------------------------------------------------------------------------");
WriteLine("Address\t\t\t\t\tConfirmed\tUnconfirmed");
WriteLine("---------------------------------------------------------------------------");
foreach (var elem in addressHistoryRecordsPerAddresses)
{
    Money confirmedBalance;
    Money unconfirmedBalance;
    GetBalances(elem.Value, out confirmedBalance, out unconfirmedBalance);
    if (confirmedBalance != Money.Zero || unconfirmedBalance != Money.Zero)
        WriteLine($"{elem.Key.ToWif()}\t{confirmedBalance.ToDecimal(MoneyUnit.BTC).ToString("0.#############################")}\t\t{unconfirmedBalance.ToDecimal(MoneyUnit.BTC).ToString("0.#############################")}");
}
WriteLine("---------------------------------------------------------------------------");
WriteLine($"Confirmed Wallet Balance: {confirmedWalletBalance.ToDecimal(MoneyUnit.BTC).ToString("0.#############################")}btc");
WriteLine($"Unconfirmed Wallet Balance: {unconfirmedWalletBalance.ToDecimal(MoneyUnit.BTC).ToString("0.#############################")}btc");
WriteLine("---------------------------------------------------------------------------");

send

输出示例

Type your password:

Wallets/test wallet is decrypted.
7 Receive keys are processed.
14 Receive keys are processed.
7 Change keys are processed.
14 Change keys are processed.
Finding not empty private keys...
Select change address...
1 Change keys are processed.
2 Change keys are processed.
3 Change keys are processed.
4 Change keys are processed.
5 Change keys are processed.
6 Change keys are processed.
Gathering unspent coins...
Calculating transaction fee...
Fee: 0.00025btc

The transaction fee is 2% of your transaction amount.
Sending:     0.01btc
Fee:         0.00025btc
Are you sure you want to proceed? (y/n)
y
Selecting coins...
Signing transaction...
Transaction Id: ad29443fee2e22460586ed0855799e32d6a3804d2df059c102877cc8cf1df2ad
Try broadcasting transaction... (1)

Transaction is successfully propagated on the network.

代码

从用户处获取指定的特比特金额和比特币地址。将他们解析成NBitcoin.Money

NBitcoin.BitcoinAddress

我们先找到所有非空的私钥,这样我们就知道有多少钱能花。

Dictionary<BitcoinAddress, List<BalanceOperation>> operationsPerAddresses = QueryOperationsPerSafeAddresses(safe, 7);

// 1. 收集所有非空的私钥
WriteLine("Finding not empty private keys...");
var operationsPerNotEmptyPrivateKeys = new Dictionary<BitcoinExtKey, List<BalanceOperation>>();
foreach (var elem in operationsPerAddresses)
{
    var balance = Money.Zero;
    foreach (var op in elem.Value) balance += op.Amount;
    if (balance > Money.Zero)
    {
        var secret = safe.FindPrivateKey(elem.Key);
        operationsPerNotEmptyPrivateKeys.Add(secret, elem.Value);
    }
}

下面我们得找个地方把更改发送出去。首先我们先得到changeScriptPubKey。这是第一个未使用的changeScriptPubKey,我使用了一种效率比较低的方式来完成它,因为突然间我不知道该怎么做才不会让我的代码变得乱七八糟:

// 2. 得到所有ScriptPubkey的变化
WriteLine("Select change address...");
Script changeScriptPubKey = null;
Dictionary<BitcoinAddress, List<BalanceOperation>> operationsPerChangeAddresses = QueryOperationsPerSafeAddresses(safe, minUnusedKeys: 1, hdPathType: HdPathType.Change);
foreach (var elem in operationsPerChangeAddresses)
{
    if (elem.Value.Count == 0)
        changeScriptPubKey = safe.FindPrivateKey(elem.Key).ScriptPubKey;
}
if (changeScriptPubKey == null)
    throw new ArgumentNullException();

一切搞定。现在让我们以同样低效的方式来收集未使用的比特币:

// 3. 获得花掉的比特币数目
WriteLine("Gathering unspent coins...");
Dictionary<Coin, bool> unspentCoins = GetUnspentCoins(operationsPerNotEmptyPrivateKeys.Keys);

还有功能:

/// <summary>
/// 
/// </summary>
/// <param name="secrets"></param>
/// <returns>dictionary with coins and if confirmed</returns>
public static Dictionary<Coin, bool> GetUnspentCoins(IEnumerable<ISecret> secrets)
{
    var unspentCoins = new Dictionary<Coin, bool>();
    foreach (var secret in secrets)
    {
        var destination = secret.PrivateKey.ScriptPubKey.GetDestinationAddress(Config.Network);

        var client = new QBitNinjaClient(Config.Network);
        var balanceModel = client.GetBalance(destination, unspentOnly: true).Result;
        foreach (var operation in balanceModel.Operations)
        {
            foreach (var elem in operation.ReceivedCoins.Select(coin => coin as Coin))
            {
                unspentCoins.Add(elem, operation.Confirmations > 0);
            }
        }
    }

    return unspentCoins;
}

下面我们来计算一下手续费。在比特币圈里这可是一个热门话题,里面有很多疑惑和错误信息。其实很简单,一笔交易只要是确定的,不是异世界里的,那么使用动态计算算出来的费用就99%是对的。但是当API出现问题时,我将使用HTTP API来查询费用并妥当的处理。这一点很重要,即使你用比特币核心中最可靠的方式来计算费用,你也不能指望它100%不出错。还记得 Mycelium 的16美元交易费用吗?这其实也不是钱包的错。

有一件事要注意:交易的数据包大小决定了交易费用。而交易数据包的大小又取决于输入和输出的数据大小。在这里阅读更多。一笔常规交易大概有1-2个输入和2个输出,数据白大小为250字节左右,这个大小应该够用了,因为交易的数据包大小变化不大。但是也有一些例外,例如当你有很多小的输入时。我在这个链接里说明了如何处理,但是我不会写在本教程中,因为它会使费用估计变得很复杂。

// 4. 取得手续费
WriteLine("Calculating transaction fee...");
Money fee;
try
{
    var txSizeInBytes = 250;
    using (var client = new HttpClient())
    {

        const string request = @"https://bitcoinfees.21.co/api/v1/fees/recommended";
        var result = client.GetAsync(request, HttpCompletionOption.ResponseContentRead).Result;
        var json = JObject.Parse(result.Content.ReadAsStringAsync().Result);
        var fastestSatoshiPerByteFee = json.Value<decimal>("fastestFee");
        fee = new Money(fastestSatoshiPerByteFee * txSizeInBytes, MoneyUnit.Satoshi);
    }
}
catch
{
    Exit("Couldn't calculate transaction fee, try it again later.");
    throw new Exception("Can't get tx fee");
}
WriteLine($"Fee: {fee.ToDecimal(MoneyUnit.BTC).ToString("0.#############################")}btc");

如你所见,我只发起了最快的交易请求。此外,我们希望检查费用是否高于了用户想要发送的资金的1%,如果超过了就要求客户亲自确认,但是这些将会在晚些时候完成。

现在我们来算算我们一共有多少钱可以花。尽管禁止用户花费未经确认的硬币是一个不错的主意,但由于我经常希望这样做,所以我会将它作为非默认选项添加到钱包中。

请注意,我们还会计算未确认的金额,这样就人性化多了:

// 5. 我们有多少钱能花?
Money availableAmount = Money.Zero;
Money unconfirmedAvailableAmount = Money.Zero;
foreach (var elem in unspentCoins)
{
    // 如果未确定的比特币可以使用,则全部加起来
    if (Config.CanSpendUnconfirmed)
    {
        availableAmount += elem.Key.Amount;
        if (!elem.Value)
            unconfirmedAvailableAmount += elem.Key.Amount;
    }
    //否则只相加已经确定的
    else
    {
        if (elem.Value)
        {
            availableAmount += elem.Key.Amount;
        }
    }
}

接下来我们要弄清楚有多少钱能用来发送。我可以很容易地通过参数来得到它,例如:

var amountToSend = new Money(GetAmountToSend(args), MoneyUnit.BTC);

但我想做得更好,能让用户指定一个特殊金额来发送钱包中的所有资金。这种需求总会有的。所以,用户可以直接输入btc=all而不是btc=2.918112来实现这个功能。经过一些重构,上面的代码变成了这样:

// 6. 能花多少?
Money amountToSend = null;
string amountString = GetArgumentValue(args, argName: "btc", required: true);
if (string.Equals(amountString, "all", StringComparison.OrdinalIgnoreCase))
{
    amountToSend = availableAmount;
    amountToSend -= fee;
}
else
{
    amountToSend = ParseBtcString(amountString);
}

然后检查下代码:

// 7. 做一些检查
if (amountToSend < Money.Zero || availableAmount < amountToSend + fee)
    Exit("Not enough coins.");

decimal feePc = Math.Round((100 * fee.ToDecimal(MoneyUnit.BTC)) / amountToSend.ToDecimal(MoneyUnit.BTC));
if (feePc > 1)
{
    WriteLine();
    WriteLine($"The transaction fee is {feePc.ToString("0.#")}% of your transaction amount.");
    WriteLine($"Sending:\t {amountToSend.ToDecimal(MoneyUnit.BTC).ToString("0.#############################")}btc");
    WriteLine($"Fee:\t\t {fee.ToDecimal(MoneyUnit.BTC).ToString("0.#############################")}btc");
    ConsoleKey response = GetYesNoAnswerFromUser();
    if (response == ConsoleKey.N)
    {
        Exit("User interruption.");
    }
}

var confirmedAvailableAmount = availableAmount - unconfirmedAvailableAmount;
var totalOutAmount = amountToSend + fee;
if (confirmedAvailableAmount < totalOutAmount)
{
    var unconfirmedToSend = totalOutAmount - confirmedAvailableAmount;
    WriteLine();
    WriteLine($"In order to complete this transaction you have to spend {unconfirmedToSend.ToDecimal(MoneyUnit.BTC).ToString("0.#############################")} unconfirmed btc.");
    ConsoleKey response = GetYesNoAnswerFromUser();
    if (response == ConsoleKey.N)
    {
        Exit("User interruption.");
    }
}

下面离创建交易只差最后一步了:选择要花的比特币。后面我会做一个面向隐私的比特币选择。现在只就用一个简单就行了的:

// 8. 选择比特币
WriteLine("Selecting coins...");
var coinsToSpend = new HashSet<Coin>();
var unspentConfirmedCoins = new List<Coin>();
var unspentUnconfirmedCoins = new List<Coin>();
foreach (var elem in unspentCoins)
    if (elem.Value) unspentConfirmedCoins.Add(elem.Key);
    else unspentUnconfirmedCoins.Add(elem.Key);

bool haveEnough = SelectCoins(ref coinsToSpend, totalOutAmount, unspentConfirmedCoins);
if (!haveEnough)
    haveEnough = SelectCoins(ref coinsToSpend, totalOutAmount, unspentUnconfirmedCoins);
if (!haveEnough)
    throw new Exception("Not enough funds.");

还有SelectCoins功能:

public static bool SelectCoins(ref HashSet<Coin> coinsToSpend, Money totalOutAmount, List<Coin> unspentCoins)
{
    var haveEnough = false;
    foreach (var coin in unspentCoins.OrderByDescending(x => x.Amount))
    {
        coinsToSpend.Add(coin);
        // if doesn't reach amount, continue adding next coin
        if (coinsToSpend.Sum(x => x.Amount) < totalOutAmount) continue;
        else
        {
            haveEnough = true;
            break;
        }
    }

    return haveEnough;
}

接下来获取签名密钥:

// 9. 获取签名私钥
var signingKeys = new HashSet<ISecret>();
foreach (var coin in coinsToSpend)
{
    foreach (var elem in operationsPerNotEmptyPrivateKeys)
    {
        if (elem.Key.ScriptPubKey == coin.ScriptPubKey)
            signingKeys.Add(elem.Key);
    }
}

建立交易。

// 10.建立交易
WriteLine("Signing transaction...");
var builder = new TransactionBuilder();
var tx = builder
    .AddCoins(coinsToSpend)
    .AddKeys(signingKeys.ToArray())
    .Send(addressToSend, amountToSend)
    .SetChange(changeScriptPubKey)
    .SendFees(fee)
    .BuildTransaction(true);

最后把它广播出去!注意这比理想的情况要多了些代码,因为QBitNinja的响应是错误的,所以我们做一些手动检查:

if (!builder.Verify(tx))
    Exit("Couldn't build the transaction.");

WriteLine($"Transaction Id: {tx.GetHash()}");

var qBitClient = new QBitNinjaClient(Config.Network);

// QBit's 的成功提示有点BUG,所以我们得手动检查一下结果
BroadcastResponse broadcastResponse;
var success = false;
var tried = 0;
var maxTry = 7;
do
{
    tried++;
    WriteLine($"Try broadcasting transaction... ({tried})");
    broadcastResponse = qBitClient.Broadcast(tx).Result;
    var getTxResp = qBitClient.GetTransaction(tx.GetHash()).Result;
    if (getTxResp == null)
    {
        Thread.Sleep(3000);
        continue;
    }
    else
    {
        success = true;
        break;
    }
} while (tried <= maxTry);
if (!success)
{
    if (broadcastResponse.Error != null)
    {
        WriteLine($"Error code: {broadcastResponse.Error.ErrorCode} Reason: {broadcastResponse.Error.Reason}");
    }
    Exit($"The transaction might not have been successfully broadcasted. Please check the Transaction ID in a block explorer.", ConsoleColor.Blue);
}
Exit("Transaction is successfully propagated on the network.", ConsoleColor.Green);

最后的话

恭喜你,你刚刚完成了你的第一个比特币钱包。你可能也会像我一样遇到一些难题,并且可能会更好地解决它们,即使你现在可能不太理解。此外,如果你已经略有小成了,我会欢迎你来修复我在这个比特币钱包中可能产生的数百万个错误。

本文的版权归 飞翔的猪脚粉 所有,如需转载请联系作者。

发表于 5天前
778 浏览 收藏 报告 阅读模式
载入中...