Commit 1563efa9 authored by Pieterjan Vanhoof's avatar Pieterjan Vanhoof Committed by GitHub

Created a WalletManager to manage the user's wallets (#32).

Created a WalletManager to manage the user's wallets.
parents f4a62d20 d6b9e6fe

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.26403.0
VisualStudioVersion = 15.0.26403.7
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{807563C4-7259-434D-B604-A14C3DCF8E30}"
EndProject
......
......@@ -6,6 +6,7 @@ using Moq;
using Breeze.Wallet.Wrappers;
using Breeze.Wallet;
using Breeze.Wallet.Controllers;
using Breeze.Wallet.Errors;
using Breeze.Wallet.Models;
namespace Breeze.Api.Tests
......@@ -122,7 +123,7 @@ namespace Breeze.Api.Tests
// Assert
mockWalletWrapper.VerifyAll();
var viewResult = Assert.IsType<ObjectResult>(result);
var viewResult = Assert.IsType<ErrorResult>(result);
Assert.NotNull(viewResult);
Assert.Equal(404, viewResult.StatusCode);
}
......
......@@ -48,7 +48,7 @@ namespace Breeze.Wallet.Controllers
var mnemonic = this.walletWrapper.Create(request.Password, walletFolder.FullName, request.Name, request.Network);
return this.Json(mnemonic);
}
catch (NotSupportedException e)
catch (InvalidOperationException e)
{
// indicates that this wallet already exists
return ErrorHelpers.BuildErrorResponse(HttpStatusCode.Conflict, "This wallet already exists.", e.ToString());
......@@ -119,6 +119,11 @@ namespace Breeze.Wallet.Controllers
var wallet = this.walletWrapper.Recover(request.Password, walletFolder.FullName, request.Name, request.Network, request.Mnemonic);
return this.Json(wallet);
}
catch (InvalidOperationException e)
{
// indicates that this wallet already exists
return ErrorHelpers.BuildErrorResponse(HttpStatusCode.Conflict, "This wallet already exists.", e.ToString());
}
catch (FileNotFoundException e)
{
// indicates that this wallet does not exist
......
using System;
using NBitcoin;
namespace Breeze.Wallet
{
/// <summary>
/// Interface for a manager providing operations on wallets.
/// </summary>
public interface IWalletManager : IDisposable
{
/// <summary>
/// Creates a wallet and persist it as a file on the local system.
/// </summary>
/// <param name="password">The password used to encrypt sensitive info.</param>
/// <param name="passphrase">The passphrase used in the seed.</param>
/// <param name="walletFilePath">The path where the wallet file will be created.</param>
/// <param name="network">The network this wallet is for.</param>
/// <returns>A mnemonic defining the wallet's seed used to generate addresses.</returns>
Mnemonic CreateWallet(string password, string walletFilePath, Network network, string passphrase = null);
/// <summary>
/// Loads a wallet from a file.
/// </summary>
/// <param name="password">The password used to encrypt sensitive info.</param>
/// <param name="walletFilePath">The location of the wallet file.</param>
/// <returns>The wallet.</returns>
Wallet LoadWallet(string password, string walletFilePath);
/// <summary>
/// Recovers a wallet.
/// </summary>
/// <param name="mnemonic">A mnemonic defining the wallet's seed used to generate addresses.</param>
/// <param name="password">The password used to encrypt sensitive info.</param>
/// <param name="walletFilePath">The location of the wallet file.</param>
/// <param name="network">The network this wallet is for.</param>
/// <param name="passphrase">The passphrase used in the seed.</param>
/// <param name="creationTime">The time this wallet was created.</param>
/// <returns>The recovered wallet.</returns>
Wallet RecoverWallet(Mnemonic mnemonic, string password, string walletFilePath, Network network, string passphrase = null, DateTimeOffset? creationTime = null);
/// <summary>
/// Deleted a wallet.
/// </summary>
/// <param name="walletFilePath">The location of the wallet file.</param>
void DeleteWallet(string walletFilePath);
}
}
using System;
using System.Collections.Generic;
using System.Text;
using Newtonsoft.Json;
namespace Breeze.Wallet.JsonConverters
{
/// <summary>
/// Converter used to convert <see cref="byte"/> arrays to and from JSON.
/// </summary>
/// <seealso cref="Newtonsoft.Json.JsonConverter" />
public class ByteArrayConverter : JsonConverter
{
/// <inheritdoc />
public override bool CanConvert(Type objectType)
{
return objectType == typeof(byte[]);
}
/// <inheritdoc />
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
return Convert.FromBase64String((string)reader.Value);
}
/// <inheritdoc />
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
writer.WriteValue(Convert.ToBase64String((byte[])value));
}
}
}
using Newtonsoft.Json;
using System;
namespace Breeze.Wallet.JsonConverters
{
/// <summary>
/// Converter used to convert <see cref="DateTimeOffset"/> to and from Unix time.
/// </summary>
/// <seealso cref="Newtonsoft.Json.JsonConverter" />
public class DateTimeOffsetConverter : JsonConverter
{
/// <inheritdoc />
public override bool CanConvert(Type objectType)
{
return objectType == typeof(DateTimeOffset);
}
/// <inheritdoc />
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
return DateTimeOffset.FromUnixTimeMilliseconds(long.Parse((string)reader.Value));
}
/// <inheritdoc />
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
writer.WriteValue(((DateTimeOffset)value).ToUnixTimeMilliseconds().ToString());
}
}
}
using System;
using Breeze.Wallet.Helpers;
using NBitcoin;
using Newtonsoft.Json;
namespace Breeze.Wallet.JsonConverters
{
/// <summary>
/// Converter used to convert <see cref="Network"/> to and from JSON.
/// </summary>
/// <seealso cref="Newtonsoft.Json.JsonConverter" />
public class NetworkConverter : JsonConverter
{
/// <inheritdoc />
public override bool CanConvert(Type objectType)
{
return objectType == typeof(Network);
}
/// <inheritdoc />
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
return WalletHelpers.GetNetwork((string)reader.Value);
}
/// <inheritdoc />
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
writer.WriteValue(((Network)value).ToString());
}
}
}
using System;
using NBitcoin;
namespace Breeze.Wallet
{
/// <summary>
/// A wallet
/// </summary>
public class Wallet
{
/// <summary>
/// The chain code.
/// </summary>
public byte[] ChainCode { get; set; }
/// <summary>
/// The network this wallet is for.
/// </summary>
public Network Network { get; set; }
/// <summary>
/// The time this wallet was created.
/// </summary>
public DateTimeOffset CreationTime { get; set; }
/// <summary>
/// The location of the wallet file on the local system.
/// </summary>
public string WalletFilePath { get; set; }
/// <summary>
/// The key used to generate keys.
/// </summary>
public ExtKey ExtendedKey { get; set; }
}
}
\ No newline at end of file
......@@ -41,6 +41,7 @@ namespace Breeze.Wallet
{
services.AddTransient<IWalletWrapper, WalletWrapper>();
services.AddTransient<ITrackerWrapper, TrackerWrapper>();
services.AddSingleton<IWalletManager, WalletManager>();
services.AddSingleton<WalletController>();
});
});
......
using System;
using NBitcoin;
using Newtonsoft.Json;
using Breeze.Wallet.JsonConverters;
namespace Breeze.Wallet
{
/// <summary>
/// An object representing a wallet on a local file system.
/// </summary>
public class WalletFile
{
/// <summary>
/// The seed for this wallet, password encrypted.
/// </summary>
[JsonProperty(PropertyName = "encryptedSeed")]
public string EncryptedSeed { get; set; }
/// <summary>
/// The chain code.
/// </summary>
[JsonProperty(PropertyName = "chainCode")]
[JsonConverter(typeof(ByteArrayConverter))]
public byte[] ChainCode { get; set; }
/// <summary>
/// The network this wallet is for.
/// </summary>
[JsonProperty(PropertyName = "network")]
[JsonConverter(typeof(NetworkConverter))]
public Network Network { get; set; }
/// <summary>
/// The time this wallet was created.
/// </summary>
[JsonProperty(PropertyName = "creationTime")]
[JsonConverter(typeof(DateTimeOffsetConverter))]
public DateTimeOffset CreationTime { get; set; }
}
}
using System;
using System.IO;
using NBitcoin;
using Newtonsoft.Json;
namespace Breeze.Wallet
{
/// <summary>
/// A manager providing operations on wallets.
/// </summary>
public class WalletManager : IWalletManager
{
/// <inheritdoc />
public Mnemonic CreateWallet(string password, string walletFilePath, Network network, string passphrase = null)
{
// generate the root seed used to generate keys from a mnemonic picked at random
// and a passphrase optionally provided by the user
Mnemonic mnemonic = new Mnemonic(Wordlist.English, WordCount.Twelve);
ExtKey extendedKey = mnemonic.DeriveExtKey(passphrase);
// create a wallet file
this.GenerateWalletFile(password, walletFilePath, network, extendedKey);
return mnemonic;
}
/// <inheritdoc />
public Wallet LoadWallet(string password, string walletFilePath)
{
if (!File.Exists(walletFilePath))
throw new FileNotFoundException($"No wallet file found at {walletFilePath}");
// load the file from the local system
WalletFile walletFile = JsonConvert.DeserializeObject<WalletFile>(File.ReadAllText(walletFilePath));
// decrypt the private key and use it to regenerate the seed
var privateKey = Key.Parse(walletFile.EncryptedSeed, password, walletFile.Network);
var seedExtKey = new ExtKey(privateKey, walletFile.ChainCode);
Wallet wallet = new Wallet
{
ChainCode = walletFile.ChainCode,
CreationTime = walletFile.CreationTime,
Network = walletFile.Network,
WalletFilePath = walletFilePath,
ExtendedKey = seedExtKey
};
return wallet;
}
/// <inheritdoc />
public Wallet RecoverWallet(Mnemonic mnemonic, string password, string walletFilePath, Network network, string passphrase = null, DateTimeOffset? creationTime = null)
{
// generate the root seed used to generate keys
ExtKey extendedKey = mnemonic.DeriveExtKey(passphrase);
// create a wallet file
WalletFile walletFile = this.GenerateWalletFile(password, walletFilePath, network, extendedKey, creationTime);
Wallet wallet = new Wallet
{
ChainCode = walletFile.ChainCode,
CreationTime = walletFile.CreationTime,
Network = walletFile.Network,
WalletFilePath = walletFilePath,
ExtendedKey = extendedKey
};
return wallet;
}
/// <inheritdoc />
public void DeleteWallet(string walletFilePath)
{
File.Delete(walletFilePath);
}
/// <inheritdoc />
public void Dispose()
{
// TODO Safely persist the wallet before disposing
}
/// <summary>
/// Generates the wallet file.
/// </summary>
/// <param name="password">The password used to encrypt sensitive info.</param>
/// <param name="walletFilePath">The location of the wallet file.</param>
/// <param name="network">The network this wallet is for.</param>
/// <param name="extendedKey">The root key used to generate keys.</param>
/// <param name="creationTime">The time this wallet was created.</param>
/// <returns></returns>
/// <exception cref="System.NotSupportedException"></exception>
private WalletFile GenerateWalletFile(string password, string walletFilePath, Network network, ExtKey extendedKey, DateTimeOffset? creationTime = null)
{
if (File.Exists(walletFilePath))
throw new InvalidOperationException($"Wallet already exists at {walletFilePath}");
WalletFile walletFile = new WalletFile
{
EncryptedSeed = extendedKey.PrivateKey.GetEncryptedBitcoinSecret(password, network).ToWif(),
ChainCode = extendedKey.ChainCode,
CreationTime = creationTime ?? DateTimeOffset.Now,
Network = network
};
// create a folder if none exists and persist the file
Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(walletFilePath)));
File.WriteAllText(walletFilePath, JsonConvert.SerializeObject(walletFile, Formatting.Indented));
return walletFile;
}
}
}
......@@ -13,6 +13,13 @@ namespace Breeze.Wallet.Wrappers
/// </summary>
public class WalletWrapper : IWalletWrapper
{
private readonly IWalletManager walletManager;
public WalletWrapper(IWalletManager walletManager)
{
this.walletManager = walletManager;
}
/// <summary>
/// Creates a wallet on the local device.
/// </summary>
......@@ -23,8 +30,7 @@ namespace Breeze.Wallet.Wrappers
/// <returns>A mnemonic allowing recovery of the wallet.</returns>
public string Create(string password, string folderPath, string name, string network)
{
Mnemonic mnemonic;
Safe wallet = Safe.Create(out mnemonic, password, Path.Combine(folderPath, $"{name}.json"), WalletHelpers.GetNetwork(network));
Mnemonic mnemonic = this.walletManager.CreateWallet(password, Path.Combine(folderPath, $"{name}.json"), WalletHelpers.GetNetwork(network), password);
return mnemonic.ToString();
}
......@@ -37,13 +43,13 @@ namespace Breeze.Wallet.Wrappers
/// <returns>The wallet loaded from the local device</returns>
public WalletModel Load(string password, string folderPath, string name)
{
Safe wallet = Safe.Load(password, Path.Combine(folderPath, $"{name}.json"));
Wallet wallet = this.walletManager.LoadWallet(password, Path.Combine(folderPath, $"{name}.json"));
//TODO review here which data should be returned
return new WalletModel
{
Network = wallet.Network.Name,
Addresses = wallet.GetFirstNAddresses(10).Select(a => a.ToWif()),
// Addresses = wallet.GetFirstNAddresses(10).Select(a => a.ToWif()),
FileName = wallet.WalletFilePath
};
}
......@@ -59,13 +65,13 @@ namespace Breeze.Wallet.Wrappers
/// <returns></returns>
public WalletModel Recover(string password, string folderPath, string name, string network, string mnemonic)
{
Safe wallet = Safe.Recover(new Mnemonic(mnemonic), password, Path.Combine(folderPath, $"{name}.json"), WalletHelpers.GetNetwork(network));
Wallet wallet = this.walletManager.RecoverWallet(new Mnemonic(mnemonic), password, Path.Combine(folderPath, $"{name}.json"), WalletHelpers.GetNetwork(network), password);
//TODO review here which data should be returned
return new WalletModel
{
Network = wallet.Network.Name,
Addresses = wallet.GetFirstNAddresses(10).Select(a => a.ToWif()),
// Addresses = wallet.GetFirstNAddresses(10).Select(a => a.ToWif()),
FileName = wallet.WalletFilePath
};
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment