Commit c5fa0532 authored by Jeremy Bokobza's avatar Jeremy Bokobza

Added creation of accounts for a wallet

parent bbec71c8
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.IO;
using Microsoft.AspNetCore.Mvc;
using Xunit;
using Moq;
using Breeze.Wallet.Wrappers;
using Breeze.Wallet;
using Breeze.Wallet.Controllers;
using Breeze.Wallet.Errors;
using Breeze.Wallet.Helpers;
using Breeze.Wallet.Models;
using NBitcoin;
namespace Breeze.Api.Tests
{
......@@ -16,8 +18,9 @@ namespace Breeze.Api.Tests
[Fact]
public void CreateWalletSuccessfullyReturnsMnemonic()
{
var mockWalletCreate = new Mock<IWalletWrapper>();
mockWalletCreate.Setup(wallet => wallet.Create(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>())).Returns("mnemonic");
Mnemonic mnemonic = new Mnemonic(Wordlist.English, WordCount.Twelve);
var mockWalletCreate = new Mock<IWalletManager>();
mockWalletCreate.Setup(wallet => wallet.CreateWallet(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), null, CoinType.Bitcoin)).Returns(mnemonic);
var controller = new WalletController(mockWalletCreate.Object);
......@@ -33,32 +36,31 @@ namespace Breeze.Api.Tests
// Assert
mockWalletCreate.VerifyAll();
var viewResult = Assert.IsType<JsonResult>(result);
Assert.Equal("mnemonic", viewResult.Value);
Assert.Equal(mnemonic.ToString(), viewResult.Value);
Assert.NotNull(result);
}
[Fact]
public void LoadWalletSuccessfullyReturnsWalletModel()
{
WalletModel walletModel = new WalletModel
Wallet.Wallet wallet = new Wallet.Wallet
{
FileName = "myWallet",
Network = "MainNet",
Addresses = new List<string> { "address1", "address2", "address3", "address4", "address5" }
Name = "myWallet",
Network = WalletHelpers.GetNetwork("mainnet")
};
var mockWalletWrapper = new Mock<IWalletWrapper>();
mockWalletWrapper.Setup(wallet => wallet.Recover(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>())).Returns(walletModel);
var mockWalletWrapper = new Mock<IWalletManager>();
mockWalletWrapper.Setup(w => w.RecoverWallet(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), null, CoinType.Bitcoin, null)).Returns(wallet);
var controller = new WalletController(mockWalletWrapper.Object);
// Act
var result = controller.Recover(new WalletRecoveryRequest
{
Name = "myName",
Name = "myWallet",
FolderPath = "",
Password = "",
Network = "",
Network = "MainNet",
Mnemonic = "mnemonic"
});
......@@ -69,28 +71,26 @@ namespace Breeze.Api.Tests
Assert.IsType<WalletModel>(viewResult.Value);
var model = viewResult.Value as WalletModel;
Assert.Equal("myWallet", model.FileName);
Assert.Equal("Main", model.Network);
}
[Fact]
public void RecoverWalletSuccessfullyReturnsWalletModel()
{
WalletModel walletModel = new WalletModel
Wallet.Wallet wallet = new Wallet.Wallet
{
FileName = "myWallet",
Network = "MainNet",
Addresses = new List<string> { "address1", "address2", "address3", "address4", "address5" }
Name = "myWallet",
Network = WalletHelpers.GetNetwork("mainnet")
};
var mockWalletWrapper = new Mock<IWalletWrapper>();
mockWalletWrapper.Setup(wallet => wallet.Load(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>())).Returns(walletModel);
var mockWalletWrapper = new Mock<IWalletManager>();
mockWalletWrapper.Setup(w => w.LoadWallet(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>())).Returns(wallet);
var controller = new WalletController(mockWalletWrapper.Object);
// Act
var result = controller.Load(new WalletLoadRequest
{
Name = "myName",
Name = "myWallet",
FolderPath = "",
Password = ""
});
......@@ -102,14 +102,14 @@ namespace Breeze.Api.Tests
Assert.IsType<WalletModel>(viewResult.Value);
var model = viewResult.Value as WalletModel;
Assert.Equal("myWallet", model.FileName);
Assert.Equal("Main", model.Network);
}
[Fact]
public void FileNotFoundExceptionandReturns404()
{
var mockWalletWrapper = new Mock<IWalletWrapper>();
mockWalletWrapper.Setup(wallet => wallet.Load(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>())).Throws<FileNotFoundException>();
var mockWalletWrapper = new Mock<IWalletManager>();
mockWalletWrapper.Setup(wallet => wallet.LoadWallet(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>())).Throws<FileNotFoundException>();
var controller = new WalletController(mockWalletWrapper.Object);
......
......@@ -311,6 +311,32 @@ namespace Breeze.Wallet.Controllers
}
}
/// <summary>
/// Creates a new account for a wallet.
/// </summary>
/// <returns>An account name.</returns>
[Route("account")]
[HttpPost]
public IActionResult CreateNewAccount([FromBody]CreateAccountModel request)
{
// checks the request is valid
if (!this.ModelState.IsValid)
{
var errors = this.ModelState.Values.SelectMany(e => e.Errors.Select(m => m.ErrorMessage));
return ErrorHelpers.BuildErrorResponse(HttpStatusCode.BadRequest, "Formatting error", string.Join(Environment.NewLine, errors));
}
try
{
var result = this.walletManager.CreateNewAccount(request.WalletName, request.AccountName);
return this.Json(result);
}
catch (Exception e)
{
return ErrorHelpers.BuildErrorResponse(HttpStatusCode.BadRequest, e.Message, e.ToString());
}
}
/// <summary>
/// Gets a folder.
/// </summary>
......
......@@ -17,8 +17,9 @@ namespace Breeze.Wallet
/// <param name="name">The name of the wallet.</param>
/// <param name="network">The network this wallet is for.</param>
/// <param name="passphrase">The passphrase used in the seed.</param>
/// <param name="coinType">The type of coin this wallet will contain.</param>
/// <returns>A mnemonic defining the wallet's seed used to generate addresses.</returns>
Mnemonic CreateWallet(string password, string folderPath, string name, string network, string passphrase = null);
Mnemonic CreateWallet(string password, string folderPath, string name, string network, string passphrase = null, CoinType coinType = CoinType.Bitcoin);
/// <summary>
/// Loads a wallet from a file.
......@@ -38,9 +39,10 @@ namespace Breeze.Wallet
/// <param name="network">The network in which to creae this wallet</param>
/// <param name="mnemonic">The user's mnemonic for the wallet.</param>
/// <param name="passphrase">The passphrase used in the seed.</param>
/// <param name="coinType">The type of coin this wallet will contain.</param>
/// <param name="creationTime">The time this wallet was created.</param>
/// <returns>The recovered wallet.</returns>
Wallet RecoverWallet(string password, string folderPath, string name, string network, string mnemonic, string passphrase = null, DateTimeOffset? creationTime = null);
Wallet RecoverWallet(string password, string folderPath, string name, string network, string mnemonic, string passphrase = null, CoinType coinType = CoinType.Bitcoin, DateTimeOffset? creationTime = null);
/// <summary>
/// Deleted a wallet.
......@@ -48,6 +50,18 @@ namespace Breeze.Wallet
/// <param name="walletFilePath">The location of the wallet file.</param>
void DeleteWallet(string walletFilePath);
/// <summary>
/// Creates a new account.
/// </summary>
/// <param name="walletName">The name of the wallet in which this account will be created.</param>
/// <param name="accountName">The name by which this account will be identified.</param>
/// <remarks>
/// According to BIP44, an account at index (i) can only be created when the account
/// at index (i - 1) contains transactions.
/// </remarks>
/// <returns>The name of the new account.</returns>
string CreateNewAccount(string walletName, string accountName);
WalletGeneralInfoModel GetGeneralInfo(string walletName);
WalletBalanceModel GetBalance(string walletName);
......
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Text;
namespace Breeze.Wallet.Models
{
public class CreateAccountModel
{
/// <summary>
/// The name of the wallet in which to create the account.
/// </summary>
[Required]
public string WalletName { get; set; }
/// <summary>
/// The name of the account.
/// </summary>
[Required]
public string AccountName { get; set; }
}
}
using System;
using System.Collections.Generic;
using Breeze.Wallet.JsonConverters;
using NBitcoin;
using Newtonsoft.Json;
......@@ -10,6 +11,11 @@ namespace Breeze.Wallet
/// </summary>
public class Wallet
{
/// <summary>
/// The name of this wallet.
/// </summary>
[JsonProperty(PropertyName = "name")]
public string Name { get; set; }
/// <summary>
/// The seed for this wallet, password encrypted.
......@@ -41,13 +47,149 @@ namespace Breeze.Wallet
/// <summary>
/// The location of the wallet file on the local system.
/// </summary>
[JsonIgnore]
[JsonProperty(PropertyName = "walletFilePath")]
public string WalletFilePath { get; set; }
/// <summary>
/// The hierarchy of the wallet's accounts and addresses.
/// The type of coin, Bitcoin or Stratis.
/// </summary>
[JsonProperty(PropertyName = "coinType")]
public CoinType CoinType { get; set; }
/// <summary>
/// The accounts used in the wallet.
/// </summary>
[JsonProperty(PropertyName = "accounts")]
public IEnumerable<HdAccount> Accounts { get; set; }
}
/// <summary>
/// The type of coin, as specified in BIP44.
/// </summary>
public enum CoinType
{
Bitcoin = 0,
Stratis = 105
}
/// <summary>
/// An Hd account's details.
/// </summary>
public class HdAccount
{
/// <summary>
/// The index of the account.
/// </summary>
/// <remarks>
/// According to BIP44, an account at index (i) can only be created when the account
/// at index (i - 1) contains transactions.
/// </remarks>
[JsonProperty(PropertyName = "index")]
public int Index { get; set; }
/// <summary>
/// The name of this account.
/// </summary>
[JsonProperty(PropertyName = "hierarchy")]
public WalletHierarchy Hierarchy { get; set; }
[JsonProperty(PropertyName = "name")]
public string Name { get; set; }
/// <summary>
/// Gets or sets the creation time.
/// </summary>
[JsonProperty(PropertyName = "creationTime")]
[JsonConverter(typeof(DateTimeOffsetConverter))]
public DateTimeOffset CreationTime { get; set; }
/// <summary>
/// The list of external addresses, typically used for receiving money.
/// </summary>
[JsonProperty(PropertyName = "externalAddresses")]
public IEnumerable<HdAddress> ExternalAddresses { get; set; }
/// <summary>
/// The list of internal addresses, typically used to receive change.
/// </summary>
[JsonProperty(PropertyName = "internalAddresses")]
public IEnumerable<HdAddress> InternalAddresses { get; set; }
}
/// <summary>
/// An Hd address.
/// </summary>
public class HdAddress
{
/// <summary>
/// Gets or sets the creation time.
/// </summary>
[JsonProperty(PropertyName = "creationTime")]
[JsonConverter(typeof(DateTimeOffsetConverter))]
public DateTimeOffset CreationTime { get; set; }
/// <summary>
/// The script pub key for this address.
/// </summary>
[JsonProperty(PropertyName = "scriptPubKey")]
public Script ScriptPubKey { get; set; }
/// <summary>
/// The Base58 representation of this address.
/// </summary>
[JsonProperty(PropertyName = "address")]
public BitcoinAddress Address { get; set; }
/// <summary>
/// A path to the address as defined in BIP44.
/// </summary>
[JsonProperty(PropertyName = "hdPath")]
public string HdPath { get; set; }
/// <summary>
/// A list detailing which blocks have been scanned for this address.
/// </summary>
[JsonIgnore]
public SortedList<int, int> BlocksScanned { get; set; }
/// <summary>
/// A list of transactions involving this address.
/// </summary>
[JsonProperty(PropertyName = "transactions")]
public IEnumerable<TransactionData> Transactions { get; set; }
}
/// <summary>
/// An object containing transaction data.
/// </summary>
public class TransactionData
{
/// <summary>
/// Transaction id.
/// </summary>
[JsonProperty(PropertyName = "id")]
public string Id { get; set; }
/// <summary>
/// The transaction amount.
/// </summary>
[JsonProperty(PropertyName = "amount")]
public Money Amount { get; set; }
/// <summary>
/// The height of the block including this transaction.
/// </summary>
[JsonProperty(PropertyName = "blockHeight")]
public int BlockHeight { get; set; }
/// <summary>
/// Whether this transaction has been confirmed or not.
/// </summary>
[JsonProperty(PropertyName = "confirmed")]
public bool Confirmed { get; set; }
/// <summary>
/// Gets or sets the creation time.
/// </summary>
[JsonProperty(PropertyName = "creationTime")]
[JsonConverter(typeof(DateTimeOffsetConverter))]
public DateTimeOffset CreationTime { get; set; }
}
}
\ No newline at end of file
using System;
using System.Collections.Generic;
using Breeze.Wallet.JsonConverters;
using NBitcoin;
using Newtonsoft.Json;
namespace Breeze.Wallet
{
/// <summary>
/// Represents the root of the user's wallet's addresses and transactions.
/// </summary>
public class WalletHierarchy
{
/// <summary>
/// The type of coin, Bitcoin or Stratis.
/// </summary>
[JsonProperty(PropertyName = "coinType")]
public CoinType CoinType { get; set; }
/// <summary>
/// The accounts used in the wallet.
/// </summary>
[JsonProperty(PropertyName = "accounts")]
public IEnumerable<HdAccount> Accounts { get; set; }
}
/// <summary>
/// The type of coin, as specified in BIP44.
/// </summary>
public enum CoinType
{
Bitcoin = 0,
Stratis = 105
}
/// <summary>
/// An Hd account's details.
/// </summary>
public class HdAccount
{
/// <summary>
/// The index of the account.
/// </summary>
/// <remarks>
/// According to BIP44, an account at index (i) can only be created when the account
/// at index (i - 1) contains transactions.
/// </remarks>
[JsonProperty(PropertyName = "index")]
public int Index { get; set; }
/// <summary>
/// The list of external addresses, typically used for receiving money.
/// </summary>
[JsonProperty(PropertyName = "externalAddresses")]
public IEnumerable<HdAddress> ExternalAddresses { get; set; }
/// <summary>
/// The list of internal addresses, typically used to receive change.
/// </summary>
[JsonProperty(PropertyName = "internalAddresses")]
public IEnumerable<HdAddress> InternalAddresses { get; set; }
}
/// <summary>
/// An Hd address.
/// </summary>
public class HdAddress
{
/// <summary>
/// Gets or sets the creation time.
/// </summary>
[JsonProperty(PropertyName = "creationTime")]
[JsonConverter(typeof(DateTimeOffsetConverter))]
public DateTimeOffset CreationTime { get; set; }
/// <summary>
/// The script pub key for this address.
/// </summary>
[JsonProperty(PropertyName = "scriptPubKey")]
public Script ScriptPubKey { get; set; }
/// <summary>
/// The Base58 representation of this address.
/// </summary>
[JsonProperty(PropertyName = "address")]
public BitcoinAddress Address { get; set; }
/// <summary>
/// A path to the address as defined in BIP44.
/// </summary>
[JsonProperty(PropertyName = "hdPath")]
public string HdPath { get; set; }
/// <summary>
/// A list detailing which blocks have been scanned for this address.
/// </summary>
[JsonIgnore]
public SortedList<int, int> BlocksScanned { get; set; }
/// <summary>
/// A list of transactions involving this address.
/// </summary>
[JsonProperty(PropertyName = "transactions")]
public IEnumerable<TransactionData> Transactions { get; set; }
}
/// <summary>
/// An object containing transaction data.
/// </summary>
public class TransactionData
{
/// <summary>
/// Transaction id.
/// </summary>
[JsonProperty(PropertyName = "id")]
public string Id { get; set; }
/// <summary>
/// The transaction amount.
/// </summary>
[JsonProperty(PropertyName = "amount")]
public Money Amount { get; set; }
/// <summary>
/// The height of the block including this transaction.
/// </summary>
[JsonProperty(PropertyName = "blockHeight")]
public int BlockHeight { get; set; }
/// <summary>
/// Whether this transaction has been confirmed or not.
/// </summary>
[JsonProperty(PropertyName = "confirmed")]
public bool Confirmed { get; set; }
/// <summary>
/// Gets or sets the creation time.
/// </summary>
[JsonProperty(PropertyName = "creationTime")]
[JsonConverter(typeof(DateTimeOffsetConverter))]
public DateTimeOffset CreationTime { get; set; }
}
}
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Breeze.Wallet.Helpers;
using Breeze.Wallet.Models;
using NBitcoin;
......@@ -12,11 +14,16 @@ namespace Breeze.Wallet
/// </summary>
public class WalletManager : IWalletManager
{
/// <inheritdoc />
public Mnemonic CreateWallet(string password, string folderPath, string name, string network, string passphrase = null)
public List<Wallet> Wallets { get; }
public WalletManager()
{
string walletFilePath = Path.Combine(folderPath, $"{name}.json");
this.Wallets = new List<Wallet>();
}
/// <inheritdoc />
public Mnemonic CreateWallet(string password, string folderPath, string name, string network, string passphrase = null, CoinType coinType = CoinType.Bitcoin)
{
// for now the passphrase is set to be the password by default.
if (passphrase == null)
{
......@@ -29,7 +36,9 @@ namespace Breeze.Wallet
ExtKey extendedKey = mnemonic.DeriveExtKey(passphrase);
// create a wallet file
Wallet wallet = this.GenerateWalletFile(password, walletFilePath, WalletHelpers.GetNetwork(network), extendedKey);
Wallet wallet = this.GenerateWalletFile(password, folderPath, name, WalletHelpers.GetNetwork(network), extendedKey, coinType);
this.Load(wallet);
return mnemonic;
}
......@@ -43,11 +52,13 @@ namespace Breeze.Wallet
// load the file from the local system
Wallet wallet = JsonConvert.DeserializeObject<Wallet>(File.ReadAllText(walletFilePath));
this.Load(wallet);
return wallet;
}
/// <inheritdoc />
public Wallet RecoverWallet(string password, string folderPath, string name, string network, string mnemonic, string passphrase = null, DateTimeOffset? creationTime = null)
public Wallet RecoverWallet(string password, string folderPath, string name, string network, string mnemonic, string passphrase = null, CoinType coinType = CoinType.Bitcoin, DateTimeOffset? creationTime = null)
{
// for now the passphrase is set to be the password by default.
if (passphrase == null)
......@@ -59,9 +70,54 @@ namespace Breeze.Wallet
ExtKey extendedKey = (new Mnemonic(mnemonic)).DeriveExtKey(passphrase);
// create a wallet file
Wallet wallet = this.GenerateWalletFile(password, Path.Combine(folderPath, $"{name}.json"), WalletHelpers.GetNetwork(network), extendedKey, creationTime);
Wallet wallet = this.GenerateWalletFile(password, folderPath, name, WalletHelpers.GetNetwork(network), extendedKey, coinType, creationTime);
this.Load(wallet);
return wallet;
}
/// <inheritdoc />
public string CreateNewAccount(string walletName, string accountName)
{
Wallet wallet = this.Wallets.SingleOrDefault(w => w.Name == walletName);
if (wallet == null)
{
throw new Exception($"No wallet with name {walletName} could be found.");
}
var lastAccountIndex = 0;
// validate account creation
if (wallet.Accounts.Any())
{
// check account with same name doesn't already exists
if (wallet.Accounts.Any(a => a.Name == accountName))
{
throw new Exception($"Account with name '{accountName}' already exists in '{walletName}'.");
}
// check account at index i - 1 contains transactions.
lastAccountIndex = wallet.Accounts.Max(a => a.Index);
HdAccount previousAccount = wallet.Accounts.Single(a => a.Index == lastAccountIndex);
if (!previousAccount.ExternalAddresses.Any(addresses => addresses.Transactions.Any()) && !previousAccount.InternalAddresses.Any(addresses => addresses.Transactions.Any()))
{
throw new Exception($"Cannot create new account '{accountName}' in '{walletName}' if the previous account '{previousAccount.Name}' has not been used.");
}
}
wallet.Accounts = wallet.Accounts.Concat(new[] {new HdAccount
{
Index = lastAccountIndex + 1,
ExternalAddresses = new List<HdAddress>(),
InternalAddresses = new List<HdAddress>(),
Name = accountName,
CreationTime = DateTimeOffset.Now
}});
this.SaveToFile(wallet);
return accountName;
}
public WalletGeneralInfoModel GetGeneralInfo(string name)
{
......@@ -105,23 +161,31 @@ namespace Breeze.Wallet
/// 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="folderPath">The folder where the wallet will be generated.</param>
/// <param name="name">The name of the wallet.</param>
/// <param name="network">The network this wallet is for.</param>
/// <param name="extendedKey">The root key used to generate keys.</param>
/// <param name="coinType">The type of coin for which this wallet is created.</param>
/// <param name="creationTime">The time this wallet was created.</param>
/// <returns></returns>
/// <exception cref="System.NotSupportedException"></exception>
private Wallet GenerateWalletFile(string password, string walletFilePath, Network network, ExtKey extendedKey, DateTimeOffset? creationTime = null)
private Wallet GenerateWalletFile(string password, string folderPath, string name, Network network, ExtKey extendedKey, CoinType coinType = CoinType.Bitcoin, DateTimeOffset? creationTime = null)
{
string walletFilePath = Path.Combine(folderPath, $"{name}.json");
if (File.Exists(walletFilePath))
throw new InvalidOperationException($"Wallet already exists at {walletFilePath}");
Wallet walletFile = new Wallet
{
Name = name,
EncryptedSeed = extendedKey.PrivateKey.GetEncryptedBitcoinSecret(password, network).ToWif(),
ChainCode = extendedKey.ChainCode,
CreationTime = creationTime ?? DateTimeOffset.Now,
Network = network
Network = network,
Accounts = new List<HdAccount>(),
CoinType = coinType,
WalletFilePath = walletFilePath
};
// create a folder if none exists and persist the file
......@@ -130,5 +194,26 @@ namespace Breeze.Wallet
return walletFile;
}
/// <summary>
/// Saves the wallet into the file system.
/// </summary>
/// <param name="wallet">The wallet to save.</param>
private void SaveToFile(Wallet wallet)
{
File.WriteAllText(wallet.WalletFilePath, JsonConvert.SerializeObject(wallet, Formatting.Indented));
}
/// <summary>
/// Loads the wallet to be used by the manager.
/// </summary>
/// <param name="wallet">The wallet to load.</param>
private void Load(Wallet wallet)
{
if (this.Wallets.All(w => w.Name != wallet.Name))
{
this.Wallets.Add(wallet);
}
}
}
}
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