Background
In order to be able to follow along with this article, you need to know C# and be familiar with NBitcoin. Preferably, you have already been digging into the Bitcoin C# book before.
Design Choices
We want a cross-platform wallet and .NET Core is our platform of choice. NBitcoin
is the most popular C# Bitcoin library today, therefore we are going to use it. We don't need a GUI just yet, therefore it will be a CLI wallet.
There are roughly three ways to communicate with the Bitcoin network: as a full node, as an SPV node or through an HTTP API. This tutorial will use QBitNinja's HTTP API, from Nicolas Dorier, the creator of NBitcoin
, but I am planning to expand it with a full-node communication.
At this point (2016.11.29), it is unclear if Segregated Witness will activate on the Bitcoin network, therefore I am not incorporating it in this tutorial for now.
I kept the concepts simple, so you can understand them. This, of course, comes with inefficiencies. After this tutorial, you can take a look at HiddenWallet, the successor of this wallet. So you get a production ready version, with bug and efficiency fixes.
Implement Command Line Parsing
I want to implement the following commands: help
, generate-wallet
, recover-wallet
, show-balances
, show-history
, receive
, send
.
What the help
does is self-explanatory. The help
command is not followed by more arguments in my app.
The generate-wallet
, recover-wallet
, show-balances
, show-history
and receive
commands can be optionally followed by wallet filename specification, for example, wallet-file=wallet.dat
. If wallet-file=
is not specified, the app will use the default one, specified in the config file.
The send
command is followed by the same optional wallet file specification argument and some required arguments:
btc=3.2
address=1F1tAaz5x1HUXrCNLbtMDqcw6o5GNn4xqX
A few examples:
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
Now go ahead: create a new .NET Core CLI Application and implement the command line argument parsing with your favorite method, or just check out my code.
Then add NBitcoin
and QBitNinja.Client
from NuGet.
Create Config File
First time my app runs, it generates the config file with default parameters:
{
"DefaultWalletFileName": "Wallet.json",
"Network": "Main",
"ConnectionType": "Http",
"CanSpendUnconfirmed": "False"
}
This Config.json file stores global settings.
The values of Network
can be Main
, or TestNet
. You probably want to keep it on the test net while you are developing. Also, you will want to set CanSpendUnconfirmed
to True
.
ConnectionType
can be Http
or FullNode
. If you set FullNode
, it will keep throwing you exceptions.
We also want to access these settings easily, so I created a Config
class:
public static class Config
{
public static string DefaultWalletFileName = @"Wallet.json";
public static Network Network = Network.Main;
....
}
Now you can choose your favorite method on how to manage this config file or just check out my code.
Commands
generate-wallet
Output Example
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
-------
Code
First make sure the wallet file does not exist, so it won't get accidentally overwritten.
var walletFilePath = GetWalletFilePath(args);
AssertWalletNotExists(walletFilePath);
Then let's figure out how to properly manage our private keys. I am writing a library, called HBitcoin
(GitHub, NuGet), where I have a class, called Safe
that makes this job hard to get wrong. I would strongly recommend you use this class, unless you know what you are doing. If you try to do it manually, a small mistake can lead to catastrophic results and your customers can lose their funds.
Previously, I extensively documented its usage here at a high level and here at a low level.
Though I modified it for the shake of this project. In the original version, I was hiding every NBitcoin reference from the users of my Safe
class, so they don't get overwhelmed by the details. In this article, my audience is more advanced.
Workflow
- Get password from user
- Get password confirmation from user
- Create wallet
- Display mnemonic
First, get a password and password confirmation from the user. If you decide to write it yourself, test it on different systems. Different terminals are acting differently on the same code.
string pw;
string pwConf;
do
{
WriteLine("Choose a password:");
pw = PasswordConsole.ReadPassword();
WriteLine("Confirm password:");
pwConf = PasswordConsole.ReadPassword();
if (pw != pwConf) WriteLine("Passwords do not match. Try again!");
} while (pw != pwConf);
Next, create a wallet with my modified Safe
class and display the mnemonic.
string mnemonic;
Safe safe = Safe.Create(out mnemonic, pw, walletFilePath, Config.Network);
WriteLine();
WriteLine("Wallet is successfully created.");
WriteLine($"Wallet file: {walletFilePath}");
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
Output Example
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
Code
Not much to explain, the code is straightforward, easily understandable:
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}");
Sidenote on Security
To hack the wallet, an attacker must know (password
AND the mnemonic
) OR (the password
AND the wallet file.) It is not like most other wallets, where knowing the mnemonic is usually enough.
receive
Output Example
Type your password:
Wallets/Wallet.json wallet is decrypted.
7 Receive keys are processed.
---------------------------------------------------------------------------
Unused Receive Addresses
---------------------------------------------------------------------------
mxqP39byCjTtNYaJUFVZMRx6zebbY3QKYx
mzDPgvzs2Tbz5w3xdXn12hkSE46uMK2F8j
mnd9h6458WsoFxJEfxcgq4k3a2NuiuSxyV
n3SiVKs8fVBEecSZFP518mxbwSCnGNkw5s
mq95Cs3dpL2tW8YBt41Su4vXRK6xh39aGe
n39JHXvsUATXU5YEVQaLR3rLwuiNWBAp5d
mjHWeQa63GPmaMNExt14VnjJTKMWMPd7yZ
Code
So far, we did not have to communicate with the Bitcoin Network. This changes here. As I have mentioned previously, there are two ways in which this wallet is planned to be able to communicate with the Bitcoin network. Through an HTTP API and as a full node. (I'll explain later why I omit the implementation of the full node for now.)
The rest of the commands need to communicate with The Blockchain and will have now two ways to do it, those have to be implemented separately. Also, these commands need to access the a 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.");
}
We are going to use QBitNinja.Client
as our HTTP API, you can find it in NuGet.
For the full node, my idea is to run a QBitNinja.Server
locally, along with bitcoind
. This way, the Client
can connect to it and we would have nice, unified code. The problem is QBitNinja.Server
does not run on .NET Core yet.
The receive
command is the most straightforward. I just want to show the user 7 unused addresses, so it can start receiving bitcoins.
The first thing we'll always do is to query a bunch of data with the help of this QBitNinja jutsu:
Dictionary<BitcoinAddress, List<BalanceOperation>> operationsPerReceiveAddresses =
QueryOperationsPerSafeAddresses(safe, 7, HdPathType.Receive);
The above syntax might need some mental effort to understand. Don't be lazy, it'll just get worse. What it basically does is: it gives us a dictionary whose keys are the addresses of our safe and the values are all the operations on those addresses. A list of operation list. In other words: the operations are grouped by the addresses. This is sufficient information to successfully implement any commnad without any further querying of the blockchain.
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 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()));
}
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;
}
Many things are happening here. Basically, what it does is queries all the operations for each address we specify.
First, we query the first 7 address of our safe and if they are not all unused, then the next 7 address. If in the combined list we still cannot find 7 unused addresses, we query 7 more and so on. As an endresult in our if ConnectionType.Http
branch, we got every operation that has ever happened to any of our relevant wallet keys. And this will be needed in every other command that communicates with The Blockchain, so we are happy about it. Now all we have to figure out is how to work with the operationsPerAddresses
dictionary to present the relevant information to the user.
The receive
command is the simplest one. We just want to show all the unused and monitored addresses to the user:
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()}");
Note elem.Key
is the bitcoin address, elem.Value
are operations on that address.
show-history
Output Example
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 bcef7265f92f8b40dba0a40b706735daf9f05bde480b609adb96f4087442bbe8
Code
Follow on my comments:
AssertArgumentsLenght(args.Length, 1, 2);
var walletFilePath = GetWalletFilePath(args);
Safe safe = DecryptWalletByAskingForPassword(walletFilePath);
if (Config.ConnectionType == ConnectionType.Http)
{
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);
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));
}
var orderedTxHistoryRecords = txHistoryRecords
.OrderByDescending(x => x.Item3)
.ThenBy(x => x.Item1);
foreach (var record in orderedTxHistoryRecords)
{
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
Output Example
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
It is similar to the previous one, similarly confusing. Follow on my comments:
Dictionary<BitcoinAddress,
List<BalanceOperation>> operationsPerAddresses = QueryOperationsPerSafeAddresses(safe, 7);
var addressHistoryRecords = new List<AddressHistoryRecord>();
foreach (var elem in operationsPerAddresses)
{
foreach (var op in elem.Value)
{
addressHistoryRecords.Add(new AddressHistoryRecord(elem.Key, op));
}
}
Money confirmedWalletBalance;
Money unconfirmedWalletBalance;
GetBalances(addressHistoryRecords, out confirmedWalletBalance, out unconfirmedWalletBalance);
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);
}
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
Output Example
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.
Code
Get the specified btc amount and bitcoin address from the user. Parse them to NBitcoin.Money
and NBitcoin.BitcoinAddress
.
Let's find all our not empty private keys first, so we know what we can spend.
Dictionary<BitcoinAddress, List<BalanceOperation>>
operationsPerAddresses = QueryOperationsPerSafeAddresses(safe, 7);
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);
}
}
Next, figure out where to send our change. Let's get our changeScriptPubKey
. This is the first unused changeScriptPubKey
and I will totally do it in an inefficient way, because suddenly I don't know how should I do it in a way that wouldn't make my code much uglier:
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();
Hang in there, we are almost ready. Now let's gather the unspent coins in a similarly inefficient way:
WriteLine("Gathering unspent coins...");
Dictionary<Coin, bool> unspentCoins = GetUnspentCoins(operationsPerNotEmptyPrivateKeys.Keys);
And the function:
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;
}
Next, let's calculate our fee. This is a hot topic right now in Bitcoin world and there are a lot of FUD and misinformation out there. The truth is simple dynamic fee calculation for confirmed, not exotic transactions works 99% of the time. I will use an HTTP API to query what fee should be used and handle properly if there is something wrong with the API. This is important, even if you would calculate the fee with the most reliable way with bitcoin core, you should always expect it will have problems. Remember Mycelium $16 transaction fees? It was not the wallet's fault.
One thing to note: proper fee depends on transaction size. Transaction size depends on the number of inputs and outputs. Read more about it here. A regular transaction with 1-2 input and 2 output is about 250byte. Using this constant should be sufficient, since transaction sizes are not varying much.
However, there are some edge cases, for example, when you have many small inputs, I handled them here, but I will not include it in this tutorial, because it would complicate the fee estimation a lot.
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");
Yes, as you can see, I only send the fastest transactions possible for now. Furthermore, we want to do a check if the fee is higher than 1% of the money the user wants to send and ask for confirmation if so, but it will be done later on.
Now let's figure out how much is the total amount of money we can spend. While it is a good idea to not let your users spend unconfirmed coins, but since I very often want to, I will totally add this to the wallet as a not default option.
Note, we'll also count the unconfirmed amounts, will be good use later:
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;
}
}
}
Next, we have to figure out how much money to send. I could easily get it from the arguments, like this:
var amountToSend = new Money(GetAmountToSend(args), MoneyUnit.BTC);
But I want to do better and let the user specify a special amount that sends all the funds from the wallet. This would happen. So instead of btc=2.918112
, the user is able to do btc=all
. After a little refactoring, the above code became this:
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);
}
Then do some checks:
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.");
}
}
The last step before building our transactions is selecting coins to spend. I will want a privacy oriented coin selections to later. I'll just use a simple one for now:
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.");
And the SelectCoins
function:
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 (coinsToSpend.Sum(x => x.Amount) < totalOutAmount) continue;
else
{
haveEnough = true;
break;
}
}
return haveEnough;
}
Next, get the signing keys:
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);
}
}
Build the transaction.
WriteLine("Signing transaction...");
var builder = new TransactionBuilder();
var tx = builder
.AddCoins(coinsToSpend)
.AddKeys(signingKeys.ToArray())
.Send(addressToSend, amountToSend)
.SetChange(changeScriptPubKey)
.SendFees(fee)
.BuildTransaction(true);
Finally broadcast it! Note it is a little more lines of code, than ideally should be, because QBitNinja's response is buggy, so we do some manual checks:
if (!builder.Verify(tx))
Exit("Couldn't build the transaction.");
WriteLine($"Transaction Id: {tx.GetHash()}");
var qBitClient = new QBitNinjaClient(Config.Network);
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);
Final Words
Congratulations, you've just built your first Bitcoin wallet. Even if you didn't understand too much, you will face the same design decisions I faced and probably tackle them much better. Also if you got this far, I would welcome your PR to fix some of the millions of bugs I have probably made in this implementation.
Updates
- 21st February, 2017
- Add
HBitcoin
NuGet option to get the Safe
class - Add successor, called
HiddenWallet
to look for bug fixes and performance improvement
- 19th December, 2016
- Clarify transaction fee calculation part
- Fix some formatting mistakes