Commit e125985c authored by Jeremy Bokobza's avatar Jeremy Bokobza

Replace method CreatenewAccount by GetUnusedAccount

parent 8e626ee6
......@@ -167,12 +167,12 @@ Cannot check if the password is good or not. If the password is wrong it'll reco
## DELETE /wallet - Deletes the wallet
Works as expected.
## POST /wallet/account - Adds an account to the wallet
## POST /wallet/account - Gets an unused account from the wallet
This endpoint will get the first account containing no transaction or will create a new account.
### Parameters
```
{
"walletName": "myFirstWallet",
"accountName": "account one",
"walletName": "myFirstWallet",
"password": "123456",
"coinType": 105
}
......
......@@ -206,29 +206,9 @@
"description": "Gets all the wallets files stored in the default folder"
},
"response": []
},
{
"name": "New account for non-existant wallet fails",
"request": {
"url": "http://localhost:5000/api/v1/wallet/account",
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json",
"description": ""
}
],
"body": {
"mode": "raw",
"raw": "{\n\t\"walletName\": \"myFirstWallet\",\n\t\"accountName\": \"account one\"\n}"
},
"description": ""
},
"response": []
},
},
{
"name": "Create new account for wallet",
"name": "Get unused account in wallet",
"request": {
"url": "http://localhost:5000/api/v1/wallet/account",
"method": "POST",
......@@ -241,7 +221,7 @@
],
"body": {
"mode": "raw",
"raw": "{\n\t\"walletName\": \"myFirstWallet\",\n\t\"accountName\": \"account one\",\n\t\"password\": \"123456\",\n\t\"coinType\": 105\n}"
"raw": "{\n\t\"walletName\": \"myFirstWallet\",\n\t\"password\": \"123456\",\n\t\"coinType\": 0\n}"
},
"description": ""
},
......@@ -250,7 +230,7 @@
{
"name": "Get unused address in wallet",
"request": {
"url": "http://localhost:5000/api/v1/wallet/address?walletName=wallet1&accountName=account one&coinType=0",
"url": "http://localhost:5000/api/v1/wallet/address?walletName=wallet1&accountName=account 0&coinType=0",
"method": "GET",
"header": [
{
......
......@@ -121,7 +121,7 @@ namespace Breeze.Wallet.Controllers
DirectoryInfo walletFolder = GetWalletFolder(request.FolderPath);
Wallet wallet = this.walletManager.RecoverWallet(request.Password, walletFolder.FullName, request.Name, request.Network, request.Mnemonic);
// TODO give the tracker the date at which this wallet was originally created so that it can start syncing blocks for it
return this.Json(new WalletModel
......@@ -355,7 +355,7 @@ namespace Breeze.Wallet.Controllers
/// <returns>An account name.</returns>
[Route("account")]
[HttpPost]
public IActionResult CreateNewAccount([FromBody]CreateAccountModel request)
public IActionResult CreateNewAccount([FromBody]GetUnusedAccountModel request)
{
// checks the request is valid
if (!this.ModelState.IsValid)
......@@ -366,8 +366,8 @@ namespace Breeze.Wallet.Controllers
try
{
var result = this.walletManager.CreateNewAccount(request.WalletName, request.CoinType, request.AccountName, request.Password);
return this.Json(result);
var result = this.walletManager.GetUnusedAccount(request.WalletName, request.CoinType, request.Password);
return this.Json(result.Name);
}
catch (Exception e)
{
......
......@@ -44,24 +44,49 @@ namespace Breeze.Wallet
Wallet RecoverWallet(string password, string folderPath, string name, string network, string mnemonic, string passphrase = null, DateTimeOffset? creationTime = null);
/// <summary>
/// Deleted a wallet.
/// Deletes a wallet.
/// </summary>
/// <param name="walletFilePath">The location of the wallet file.</param>
void DeleteWallet(string walletFilePath);
/// <summary>
/// Gets an account that contains no transactions.
/// </summary>
/// <param name="walletName">The name of the wallet from which to get an account.</param>
/// <param name="coinType">The type of coin for which to get an account.</param>
/// <param name="password">The password used to decrypt the private key.</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>An unused account.</returns>
HdAccount GetUnusedAccount(string walletName, CoinType coinType, string password);
/// <summary>
/// Gets an account that contains no transactions.
/// </summary>
/// <param name="wallet">The wallet from which to get an account.</param>
/// <param name="coinType">The type of coin for which to get an account.</param>
/// <param name="password">The password used to decrypt the private key.</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>An unused account.</returns>
HdAccount GetUnusedAccount(Wallet wallet, CoinType coinType, string password);
/// <summary>
/// Creates a new account.
/// </summary>
/// <param name="walletName">The name of the wallet in which this account will be created.</param>
/// <param name="coinType">the type of coin for which to create an account.</param>
/// <param name="accountName">The name by which this account will be identified.</param>
/// <param name="wallet">The wallet in which this account will be created.</param>
/// <param name="coinType">The type of coin for which to create an account.</param>
/// <param name="password">The password used to decrypt the private key.</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, CoinType coinType, string accountName, string password);
/// <returns>The new account.</returns>
HdAccount CreateNewAccount(Wallet wallet, CoinType coinType, string password);
/// <summary>
/// Gets an address that contains no transaction.
......@@ -101,6 +126,6 @@ namespace Breeze.Wallet
/// <param name="transaction">The transaction.</param>
/// <param name="blockHeight">The height of the block this transaction came from. Null if it was not a transaction included in a block.</param>
/// <param name="blockTime">The block time.</param>
void ProcessTransaction(CoinType coinType, NBitcoin.Transaction transaction, int? blockHeight = null, uint? blockTime = null);
void ProcessTransaction(CoinType coinType, NBitcoin.Transaction transaction, int? blockHeight = null, uint? blockTime = null);
}
}
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 type of coin this account contains.
/// </summary>
[Required]
public CoinType CoinType { get; set; }
/// <summary>
/// The name of the account.
/// </summary>
[Required]
public string AccountName { get; set; }
/// <summary>
/// The password for this wallet.
/// </summary>
[Required]
public string Password { get; set; }
}
}
......@@ -114,4 +114,25 @@ namespace Breeze.Wallet.Models
[Required]
public string AccountName { get; set; }
}
public class GetUnusedAccountModel
{
/// <summary>
/// The name of the wallet in which to create the account.
/// </summary>
[Required]
public string WalletName { get; set; }
/// <summary>
/// The type of coin this account contains.
/// </summary>
[Required]
public CoinType CoinType { get; set; }
/// <summary>
/// The password for this wallet.
/// </summary>
[Required]
public string Password { get; set; }
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using Breeze.Wallet.JsonConverters;
using NBitcoin;
using NBitcoin.JsonConverters;
......@@ -80,6 +81,23 @@ namespace Breeze.Wallet
/// </summary>
[JsonProperty(PropertyName = "accounts")]
public IEnumerable<HdAccount> Accounts { get; set; }
/// <summary>
/// Gets the first account that contains no transaction.
/// </summary>
/// <returns>An unused account</returns>
public HdAccount GetFirstUnusedAccount()
{
var unusedAccounts = this.Accounts.Where(acc => !acc.ExternalAddresses.Any() && !acc.InternalAddresses.Any()).ToList();
if (!unusedAccounts.Any())
{
return null;
}
// gets the unused account with the lowest index
var index = unusedAccounts.Min(a => a.Index);
return unusedAccounts.Single(a => a.Index == index);
}
}
/// <summary>
......@@ -125,7 +143,6 @@ namespace Breeze.Wallet
[JsonProperty(PropertyName = "name")]
public string Name { get; set; }
/// <summary>
/// A path to the account as defined in BIP44.
/// </summary>
......
......@@ -15,7 +15,7 @@ namespace Breeze.Wallet
/// A manager providing operations on wallets.
/// </summary>
public class WalletManager : IWalletManager
{
{
public List<Wallet> Wallets { get; }
public HashSet<Script> PubKeys { get; }
......@@ -53,9 +53,8 @@ namespace Breeze.Wallet
ExtKey extendedKey = mnemonic.DeriveExtKey(passphrase);
// create a wallet file
Wallet wallet = this.GenerateWalletFile(password, folderPath, name, WalletHelpers.GetNetwork(network), extendedKey);
this.Load(wallet);
return mnemonic;
}
......@@ -92,7 +91,7 @@ namespace Breeze.Wallet
}
/// <inheritdoc />
public string CreateNewAccount(string walletName, CoinType coinType, string accountName, string password)
public HdAccount GetUnusedAccount(string walletName, CoinType coinType, string password)
{
Wallet wallet = this.Wallets.SingleOrDefault(w => w.Name == walletName);
if (wallet == null)
......@@ -100,29 +99,45 @@ namespace Breeze.Wallet
throw new Exception($"No wallet with name {walletName} could be found.");
}
// get the accounts for this type of coin
var accounts = wallet.AccountsRoot.Single(a => a.CoinType == coinType).Accounts.ToList();
int newAccountIndex = 0;
return this.GetUnusedAccount(wallet, coinType, password);
}
// validate account creation
if (accounts.Any())
/// <inheritdoc />
public HdAccount GetUnusedAccount(Wallet wallet, CoinType coinType, string password)
{
// get the accounts root for this type of coin
var accountsRoot = wallet.AccountsRoot.Single(a => a.CoinType == coinType);
// check if an unused account exists
if (accountsRoot.Accounts.Any())
{
// check account with same name doesn't already exists
if (accounts.Any(a => a.Name == accountName))
// gets an unused account
var firstUnusedAccount = accountsRoot.GetFirstUnusedAccount();
if (firstUnusedAccount != null)
{
throw new Exception($"Account with name '{accountName}' already exists in '{walletName}'.");
return firstUnusedAccount;
}
}
// check account at index i - 1 contains transactions.
int lastAccountIndex = accounts.Max(a => a.Index);
HdAccount previousAccount = 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.");
}
// all accounts contain transactions, create a new one
var newAccount = this.CreateNewAccount(wallet, coinType, password);
// save the changes to the file
this.SaveToFile(wallet);
return newAccount;
}
newAccountIndex = lastAccountIndex + 1;
}
/// <inheritdoc />
public HdAccount CreateNewAccount(Wallet wallet, CoinType coinType, string password)
{
// get the accounts for this type of coin
var accounts = wallet.AccountsRoot.Single(a => a.CoinType == coinType).Accounts.ToList();
int newAccountIndex = 0;
if (accounts.Any())
{
newAccountIndex = accounts.Max(a => a.Index) + 1;
}
// get the extended pub key used to generate addresses for this account
var privateKey = Key.Parse(wallet.EncryptedSeed, password, wallet.Network);
......@@ -132,21 +147,21 @@ namespace Breeze.Wallet
ExtKey accountExtKey = seedExtKey.Derive(keyPath);
ExtPubKey accountExtPubKey = accountExtKey.Neuter();
accounts.Add(new HdAccount
var newAccount = new HdAccount
{
Index = newAccountIndex,
ExtendedPubKey = accountExtPubKey.ToString(wallet.Network),
ExternalAddresses = new List<HdAddress>(),
InternalAddresses = new List<HdAddress>(),
Name = accountName,
Name = $"account {newAccountIndex}",
HdPath = accountHdPath,
CreationTime = DateTimeOffset.Now
});
};
accounts.Add(newAccount);
wallet.AccountsRoot.Single(a => a.CoinType == coinType).Accounts = accounts;
this.SaveToFile(wallet);
return accountName;
return newAccount;
}
/// <inheritdoc />
......@@ -264,7 +279,7 @@ namespace Breeze.Wallet
foreach (TxIn input in transaction.Inputs.Where(txIn => this.TrackedTransactions.Any(trackedTx => trackedTx.Hash == txIn.PrevOut.Hash)))
{
TransactionDetails tTx = this.TrackedTransactions.Single(trackedTx => trackedTx.Hash == input.PrevOut.Hash);
// compare the index of the output in its original transaction and the index references in the input
if (input.PrevOut.N == tTx.Index)
{
......@@ -272,7 +287,7 @@ namespace Breeze.Wallet
}
}
}
}
}
/// <summary>
/// Adds the transaction to the wallet.
......@@ -303,7 +318,7 @@ namespace Breeze.Wallet
{
foreach (var account in accountRoot.Accounts)
{
foreach (var address in account.ExternalAddresses.Where(a => a.ScriptPubKey == script))
foreach (var address in account.ExternalAddresses)//.Where(a => a.ScriptPubKey == script))
{
address.Transactions = address.Transactions.Concat(new[]
{
......@@ -316,7 +331,7 @@ namespace Breeze.Wallet
CreationTime = DateTimeOffset.FromUnixTimeMilliseconds(blockTime ?? time),
Index = index
}
});
});
}
}
}
......@@ -328,7 +343,7 @@ namespace Breeze.Wallet
Index = index,
Amount = amount
});
}
}
}
/// <inheritdoc />
......@@ -483,9 +498,9 @@ namespace Breeze.Wallet
SelectMany(w => w.AccountsRoot.Where(a => a.CoinType == coinType)).
SelectMany(a => a.Accounts).
SelectMany(a => a.ExternalAddresses).
Select(s => s.ScriptPubKey));
//Select(s => s.ScriptPubKey));
// uncomment the following for testing on a random address
// Select(t => (new BitcoinPubKeyAddress(t.Address, Network.Main)).ScriptPubKey));
Select(t => (new BitcoinPubKeyAddress(t.Address, Network.Main)).ScriptPubKey));
}
/// <summary>
......@@ -506,7 +521,7 @@ namespace Breeze.Wallet
Index = t.Index,
Amount = t.Amount
}));
}
}
}
public class TransactionDetails
......
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