打造屬於自己的比特幣錢包


歡迎大家前往騰訊雲+社區,獲取更多騰訊海量技術實踐干貨哦~

背景

為了能夠順利地讀懂本文,您需要有一點C#編程經驗並且熟悉NBitcoin。當然如果你研究過Bitcoin C# book就更好了。

設計選擇

我們希望打造一個跨平台的錢包,所以.NET Core是我們的首選。我們將使用NBitcoin比特幣庫,因為它是目前為止最流行的庫。這個錢包沒有使用圖形界面的必要,因此使用命令行界面就夠了。

大體上有三種方式可以和比特幣網絡進行通信:用一個完整節點,SPV節點或通過HTTP API。本教程將使用來自NBitcoin的創造者Nicolas Dorier的QBitNinja HTTP API,但我計划把它擴展成一個完整的通信節點。

下面我會盡量說的通俗易懂,因此可能效率不會那么高。在閱讀完本教程之后,您可以去看看這個錢包的應用版本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}");

安全提示

攻擊者如果想破解一個比特幣錢包,他必須知道(passwordmnemonic)或(password和錢包文件)。而其他錢包只要知道助記符就夠了。

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.MoneyNBitcoin.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);

最后的話

恭喜你,你剛剛完成了你的第一個比特幣錢包。你可能也會像我一樣遇到一些難題,並且可能會更好地解決它們,即使你現在可能不太理解。此外,如果你已經略有小成了,我會歡迎你來修復我在這個比特幣錢包中可能產生的數百萬個錯誤。

 

問答

除了比特幣,區塊鏈還可以應用到哪些技術場景?以及哪些公司在搞區塊鏈?

相關閱讀

比特幣的成與敗:中本聰式的自由

區塊鏈共識機制的思考

如何正確投資比特幣、區塊鏈和其他加密貨幣?

 

此文已由作者授權騰訊雲+社區發布,原文鏈接:https://cloud.tencent.com/developer/article/1066688?fromSource=waitui


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM