Click here to Skip to main content
13,556,447 members
Click here to Skip to main content
Add your own
alternative version

Stats

48.9K views
24 bookmarked
Posted 2 Dec 2016
Licenced Public Domain

Build your own Bitcoin wallet

, 20 Feb 2017
Rate this:
Please Sign up or sign in to vote.
Tutorial and template for building a basic, cross-platform Bitcoin wallet in .NET Core.

Clone the project from GitHub: DotNetWallet.

Background

In order to be able to follow on this article you need to know C# and need to 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 populat 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 way 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-walletrecover-walletshow-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-walletrecover-walletshow-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
{
    // Initialized with default attributes
    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 to 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 have extensively documented its usage here at a high level and here at a low level.
Though I modified it 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:
  1. Get password from user
  2. Get password confirmation from user
  3. Create wallet
  4. 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
{
    // 1. Get password from user
    WriteLine("Choose a password:");
    pw = PasswordConsole.ReadPassword();
    // 2. Get password confirmation from user
    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. 

// 3. Create wallet
string mnemonic;
Safe safe = Safe.Create(out mnemonic, pw, walletFilePath, Config.Network);
// If no exception thrown the wallet is successfully created.
WriteLine();
WriteLine("Wallet is successfully created.");
WriteLine($"Wallet file: {walletFilePath}");

// 4. Display mnemonic
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);
// If no exception thrown the wallet is successfully recovered.
WriteLine();
WriteLine("Wallet is successfully recovered.");
WriteLine($"Wallet file: {walletFilePath}");

Sidenote on security:

To hack the wallet an attacker must know (password AND the mnemonicOR (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 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)
{
    // From now on we'll only work here
}
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 will 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 thats 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 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;
}

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)
{
// 0. Query all operations, grouped our used safe addresses
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. Create history records from the transactions
// History records is arbitrary data we want to show to the user
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. Order the records by confirmations and time (Simply time does not work, because of a QBitNinja bug)
var orderedTxHistoryRecords = txHistoryRecords
    .OrderByDescending(x => x.Item3) // Confirmations
    .ThenBy(x => x.Item1); // FirstSeen
foreach (var record in orderedTxHistoryRecords)
{
    // Item2 is the Amount
    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>

Code

It is similar to the previous one, similarly confusing. Follow on my comments:

// 0. Query all operations, grouped by addresses
Dictionary<BitcoinAddress, List<BalanceOperation>> operationsPerAddresses = QueryOperationsPerSafeAddresses(safe, 7);

// 1. Get all address history record with a wrapper class
var addressHistoryRecords = new List<AddressHistoryRecord>();
foreach (var elem in operationsPerAddresses)
{
    foreach (var op in elem.Value)
    {
        addressHistoryRecords.Add(new AddressHistoryRecord(elem.Key, op));
    }
}

// 2. Calculate wallet balances
Money confirmedWalletBalance;
Money unconfirmedWalletBalance;
GetBalances(addressHistoryRecords, out confirmedWalletBalance, out unconfirmedWalletBalance);

// 3. Group all address history records by addresses
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. Calculate address balances
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);

// 1. Gather all the not empty private keys
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:

// 2. Get the script pubkey of the change.
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:

// 3. Gather coins can be spend
WriteLine("Gathering unspent coins...");
Dictionary<Coin, bool> unspentCoins = GetUnspentCoins(operationsPerNotEmptyPrivateKeys.Keys);

And the function:

/// <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;
}

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.

// 4. Get the fee
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:

// 5. How much money we can spend?
Money availableAmount = Money.Zero;
Money unconfirmedAvailableAmount = Money.Zero;
foreach (var elem in unspentCoins)
{
    // If can spend unconfirmed add all
    if (Config.CanSpendUnconfirmed)
    {
        availableAmount += elem.Key.Amount;
        if (!elem.Value)
            unconfirmedAvailableAmount += elem.Key.Amount;
    }
    // else only add confirmed ones
    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:  

// 6. How much to spend?
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:

// 7. 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:

// 8. Select coins
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 doesn't reach amount, continue adding next coin
        if (coinsToSpend.Sum(x => x.Amount) < totalOutAmount) continue;
        else
        {
            haveEnough = true;
            break;
        }
    }

    return haveEnough;
}

Next get the signing keys:

// 9. Get 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.

// 10. 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);

// QBit's success response is buggy so let's check manually, too        
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

  • 2017.02.21
    • Add HBitcoin NuGet option to get the Safe class.
    • Add successor, called HiddenWallet to look for bug fixes and performance improvement.
  • 2016.12.19
    • Clarify transaction fee calculation part.
    • Fix some formatting mistakes.

License

This article, along with any associated source code and files, is licensed under A Public Domain dedication

Share

About the Author

Ádám Ficsór
CEO of Bitcoin
Hungary Hungary
No Biography provided

You may also be interested in...

Pro
Pro

Comments and Discussions

 
QuestionHBitCoin Pin
Member 1383052422-May-18 5:22
memberMember 1383052422-May-18 5:22 
QuestionSource Code of this project. Pin
sisilchandana6-May-18 2:09
membersisilchandana6-May-18 2:09 
QuestionNeed help implementing your wallet Pin
Venkatesh S J19-Dec-17 2:23
memberVenkatesh S J19-Dec-17 2:23 
AnswerRe: Need help implementing your wallet Pin
Ádám Ficsór19-Dec-17 14:37
memberÁdám Ficsór19-Dec-17 14:37 
PraiseRe: Need help implementing your wallet Pin
Venkatesh S J20-Dec-17 11:57
professionalVenkatesh S J20-Dec-17 11:57 
Questionreference Pin
Huy Quoc3-Dec-17 21:46
memberHuy Quoc3-Dec-17 21:46 
AnswerRe: reference Pin
Ádám Ficsór19-Dec-17 14:25
memberÁdám Ficsór19-Dec-17 14:25 
QuestionReferencing DotNetWallet (which targets .NETCoreApp) in a .Net Framewok application Pin
H Braasch27-Jul-17 13:33
memberH Braasch27-Jul-17 13:33 
AnswerRe: Referencing DotNetWallet (which targets .NETCoreApp) in a .Net Framewok application Pin
Ádám Ficsór2-Aug-17 0:21
memberÁdám Ficsór2-Aug-17 0:21 
GeneralRe: Referencing DotNetWallet (which targets .NETCoreApp) in a .Net Framewok application Pin
H Braasch3-Aug-17 11:06
memberH Braasch3-Aug-17 11:06 
PraiseMessage Closed Pin
7-May-17 14:19
memberPonnalagu Sankaiah7-May-17 14:19 
QuestionWhat is Bitcoin Pin
Tridip Bhattacharjee20-Feb-17 20:49
professionalTridip Bhattacharjee20-Feb-17 20:49 
AnswerRe: What is Bitcoin Pin
Ádám Ficsór20-Feb-17 21:13
memberÁdám Ficsór20-Feb-17 21:13 
GeneralMessage Closed Pin
21-Feb-17 13:13
membertallinna21-Feb-17 13:13 
GeneralMy vote of 5 Pin
Gun Gun Febrianza9-Jan-17 16:08
member Gun Gun Febrianza9-Jan-17 16:08 
QuestionCEO of Bitcoin? Pin
Slacker0073-Dec-16 0:13
professionalSlacker0073-Dec-16 0:13 
AnswerRe: CEO of Bitcoin? Pin
Ádám Ficsór3-Dec-16 5:22
memberÁdám Ficsór3-Dec-16 5:22 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Permalink | Advertise | Privacy | Terms of Use | Mobile
Web02 | 2.8.180515.1 | Last Updated 21 Feb 2017
Article Copyright 2016 by Ádám Ficsór
Everything else Copyright © CodeProject, 1999-2018
Layout: fixed | fluid