歡迎你們前往騰訊雲+社區,獲取更多騰訊海量技術實踐乾貨哦~git
爲了可以順利地讀懂本文,您須要有一點C#編程經驗而且熟悉NBitcoin。固然若是你研究過Bitcoin C# book就更好了。github
咱們但願打造一個跨平臺的錢包,因此.NET Core是咱們的首選。咱們將使用NBitcoin比特幣庫,由於它是目前爲止最流行的庫。這個錢包沒有使用圖形界面的必要,所以使用命令行界面就夠了。編程
大致上有三種方式能夠和比特幣網絡進行通訊:用一個完整節點,SPV節點或經過HTTP API。本教程將使用來自NBitcoin的創造者Nicolas Dorier的QBitNinja HTTP API,但我計劃把它擴展成一個完整的通訊節點。json
下面我會盡可能說的通俗易懂,所以可能效率不會那麼高。在閱讀完本教程以後,您能夠去看看這個錢包的應用版本HiddenWallet。這是個修復了BUG,性能也比較高,能夠真正拿來用的比特幣錢包。api
這個錢包得具有如下命令:help
, generate-wallet
, recover-wallet
, show-balances
, show-history
, receive
, send
安全
help
命令是沒有其餘參數的。generate-wallet
, recover-wallet
, show-balances
, show-history
和receive
命令後面能夠加參數--指定錢包的文件名。例如wallet-file=wallet.dat
。若是wallet-file=
未指定參數的話,則應用程序將使用默認配置文件中指定的錢包文件。網絡
send
命令後面一樣能夠附加錢包文件名和一些其餘參數,如:app
btc=3.2
address=1F1tAaz5x1HUXrCNLbtMDqcw6o5GNn4xqX
幾個例子:less
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命令行程序,你能夠本身隨喜愛去實現這些命令,或者跟着個人代碼來也行。ide
而後從NuGet管理器中添加NBitcoin和QBitNinja.Client。
第一次運行程序時,它會生成帶默認參數的配置文件:
{ "DefaultWalletFileName": "Wallet.json", "Network": "Main", "ConnectionType": "Http", "CanSpendUnconfirmed": "False" }
Config.json
文件存儲全局設置。
Network
的值的能夠是Main
或TestNet
。當你在處於開發階段時你能夠把它設置爲測試模式(TestNet
)。CanSpendUnconfirmed
也能夠設置爲True
。ConnectionType
能夠是Http
或FullNode
,但若是設置爲FullNode
的話,程序會拋出異常
爲了方便的設置配置文件,我建立了一個類:Config
public static class Config { // 使用默認屬性初始化 public static string DefaultWalletFileName = @"Wallet.json"; public static Network Network = Network.Main; .... }
你能夠用你喜歡的方式來管理這個配置文件,或者跟着個人代碼來。
輸出示例
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(GitHub,NuGet)的庫,裏面有一個類叫Safe
類,我強烈建議你使用這個類,這樣能確保你不會出什麼差錯。若是你想本身手動去實現密鑰管理類的話,你得有十足的把握。否則一個小錯誤就可能會致使災難性的後果,您的客戶可能會損失掉錢包裏的資金。
以前我很全面地寫了一些關於這個類的使用方法:這個連接是高級版,這個連接是簡單版。
在原始版本中,爲了讓那些Safe
類的使用者們不被那些NBitcoin 的複雜引用搞的頭暈,我把不少細節都隱藏起來了。但對於這篇文章,我對Safe作了稍許修改,由於本文章的讀者應該水平更高一點。
工做流程
首先用戶輸入密碼並確認密碼。若是您決定本身寫,請在不一樣的系統上進行測試。相同的代碼在不一樣的終端可能有不一樣的結果。
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("-------");
輸出示例
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}");
安全提示
攻擊者若是想破解一個比特幣錢包,他必須知道(password
和mnemonic
)或(password
和錢包文件)。而其餘錢包只要知道助記符就夠了。
輸出示例
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是
比特幣地址。
輸出示例
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(); }
輸出示例
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("---------------------------------------------------------------------------");
輸出示例
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);
恭喜你,你剛剛完成了你的第一個比特幣錢包。你可能也會像我同樣遇到一些難題,而且可能會更好地解決它們,即便你如今可能不太理解。此外,若是你已經略有小成了,我會歡迎你來修復我在這個比特幣錢包中可能產生的數百萬個錯誤。
問答
除了比特幣,區塊鏈還能夠應用到哪些技術場景?以及哪些公司在搞區塊鏈?
相關閱讀
此文已由做者受權騰訊雲+社區發佈,原文連接:https://cloud.tencent.com/developer/article/1066688?fromSource=waitui