Commit da338538 authored by Jeremy Bokobza's avatar Jeremy Bokobza

Added creatig receiving addresses for an account

parent c5fa0532
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
"variables": [], "variables": [],
"info": { "info": {
"name": "Wallet", "name": "Wallet",
"_postman_id": "25ac0bc5-23f5-e00d-932f-a8fde7787e7a", "_postman_id": "5eec0912-fcf0-50f5-05a2-0835fa13c670",
"description": "Requests relating to operations on the wallet", "description": "Requests relating to operations on the wallet",
"schema": "https://schema.getpostman.com/json/collection/v2.0.0/collection.json" "schema": "https://schema.getpostman.com/json/collection/v2.0.0/collection.json"
}, },
...@@ -206,6 +206,66 @@ ...@@ -206,6 +206,66 @@
"description": "Gets all the wallets files stored in the default folder" "description": "Gets all the wallets files stored in the default folder"
}, },
"response": [] "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",
"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\t\"password\": \"123456\"\n}"
},
"description": ""
},
"response": []
},
{
"name": "Create new address for wallet",
"request": {
"url": "http://localhost:5000/api/v1/wallet/address",
"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": []
} }
] ]
} }
\ No newline at end of file
...@@ -328,7 +328,33 @@ namespace Breeze.Wallet.Controllers ...@@ -328,7 +328,33 @@ namespace Breeze.Wallet.Controllers
try try
{ {
var result = this.walletManager.CreateNewAccount(request.WalletName, request.AccountName); var result = this.walletManager.CreateNewAccount(request.WalletName, request.AccountName, request.Password);
return this.Json(result);
}
catch (Exception e)
{
return ErrorHelpers.BuildErrorResponse(HttpStatusCode.BadRequest, e.Message, e.ToString());
}
}
/// <summary>
/// Creates a new address for a wallet.
/// </summary>
/// <returns>An address in Base58 format.</returns>
[Route("address")]
[HttpPost]
public IActionResult CreateNewAddress([FromBody]CreateAddressModel 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.CreateNewAddress(request.WalletName, request.AccountName);
return this.Json(result); return this.Json(result);
} }
catch (Exception e) catch (Exception e)
......
...@@ -55,12 +55,21 @@ namespace Breeze.Wallet ...@@ -55,12 +55,21 @@ namespace Breeze.Wallet
/// </summary> /// </summary>
/// <param name="walletName">The name of the wallet in which this account will be created.</param> /// <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> /// <param name="accountName">The name by which this account will be identified.</param>
/// <param name="password">The password used to decrypt the private key.</param>
/// <remarks> /// <remarks>
/// According to BIP44, an account at index (i) can only be created when the account /// According to BIP44, an account at index (i) can only be created when the account
/// at index (i - 1) contains transactions. /// at index (i - 1) contains transactions.
/// </remarks> /// </remarks>
/// <returns>The name of the new account.</returns> /// <returns>The name of the new account.</returns>
string CreateNewAccount(string walletName, string accountName); string CreateNewAccount(string walletName, string accountName, string password);
/// <summary>
/// Creates the new address.
/// </summary>
/// <param name="walletName">The name of the wallet in which this address will be created.</param>
/// <param name="accountName">The name of the account in which this address will be created.</param>
/// <returns>The new address, in Base58 format.</returns>
string CreateNewAddress(string walletName, string accountName);
WalletGeneralInfoModel GetGeneralInfo(string walletName); WalletGeneralInfoModel GetGeneralInfo(string walletName);
......
...@@ -18,5 +18,13 @@ namespace Breeze.Wallet.Models ...@@ -18,5 +18,13 @@ namespace Breeze.Wallet.Models
/// </summary> /// </summary>
[Required] [Required]
public string AccountName { get; set; } public string AccountName { get; set; }
/// <summary>
/// The password for this wallet.
/// </summary>
[Required]
public string Password { get; set; }
} }
} }
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Text;
namespace Breeze.Wallet.Models
{
public class CreateAddressModel
{
/// <summary>
/// The name of the wallet in which to create the address.
/// </summary>
[Required]
public string WalletName { get; set; }
/// <summary>
/// The name of the account in which to create the address.
/// </summary>
[Required]
public string AccountName { get; set; }
}
}
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using Breeze.Wallet.JsonConverters; using Breeze.Wallet.JsonConverters;
using NBitcoin; using NBitcoin;
using NBitcoin.JsonConverters;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace Breeze.Wallet namespace Breeze.Wallet
...@@ -93,6 +94,12 @@ namespace Breeze.Wallet ...@@ -93,6 +94,12 @@ namespace Breeze.Wallet
[JsonProperty(PropertyName = "name")] [JsonProperty(PropertyName = "name")]
public string Name { get; set; } public string Name { get; set; }
/// <summary>
/// An extended pub key used to generate addresses.
/// </summary>
[JsonProperty(PropertyName = "extPubKey")]
public string ExtendedPubKey { get; set; }
/// <summary> /// <summary>
/// Gets or sets the creation time. /// Gets or sets the creation time.
/// </summary> /// </summary>
...@@ -118,6 +125,12 @@ namespace Breeze.Wallet ...@@ -118,6 +125,12 @@ namespace Breeze.Wallet
/// </summary> /// </summary>
public class HdAddress public class HdAddress
{ {
/// <summary>
/// The index of the address.
/// </summary>
[JsonProperty(PropertyName = "index")]
public int Index { get; set; }
/// <summary> /// <summary>
/// Gets or sets the creation time. /// Gets or sets the creation time.
/// </summary> /// </summary>
...@@ -129,13 +142,14 @@ namespace Breeze.Wallet ...@@ -129,13 +142,14 @@ namespace Breeze.Wallet
/// The script pub key for this address. /// The script pub key for this address.
/// </summary> /// </summary>
[JsonProperty(PropertyName = "scriptPubKey")] [JsonProperty(PropertyName = "scriptPubKey")]
[JsonConverter(typeof(ScriptJsonConverter))]
public Script ScriptPubKey { get; set; } public Script ScriptPubKey { get; set; }
/// <summary> /// <summary>
/// The Base58 representation of this address. /// The Base58 representation of this address.
/// </summary> /// </summary>
[JsonProperty(PropertyName = "address")] [JsonProperty(PropertyName = "address")]
public BitcoinAddress Address { get; set; } public string Address { get; set; }
/// <summary> /// <summary>
/// A path to the address as defined in BIP44. /// A path to the address as defined in BIP44.
......
...@@ -6,6 +6,7 @@ using Breeze.Wallet.Helpers; ...@@ -6,6 +6,7 @@ using Breeze.Wallet.Helpers;
using Breeze.Wallet.Models; using Breeze.Wallet.Models;
using NBitcoin; using NBitcoin;
using Newtonsoft.Json; using Newtonsoft.Json;
using Stratis.Bitcoin.Utilities;
namespace Breeze.Wallet namespace Breeze.Wallet
{ {
...@@ -77,7 +78,7 @@ namespace Breeze.Wallet ...@@ -77,7 +78,7 @@ namespace Breeze.Wallet
} }
/// <inheritdoc /> /// <inheritdoc />
public string CreateNewAccount(string walletName, string accountName) public string CreateNewAccount(string walletName, string accountName, string password)
{ {
Wallet wallet = this.Wallets.SingleOrDefault(w => w.Name == walletName); Wallet wallet = this.Wallets.SingleOrDefault(w => w.Name == walletName);
if (wallet == null) if (wallet == null)
...@@ -85,7 +86,7 @@ namespace Breeze.Wallet ...@@ -85,7 +86,7 @@ namespace Breeze.Wallet
throw new Exception($"No wallet with name {walletName} could be found."); throw new Exception($"No wallet with name {walletName} could be found.");
} }
var lastAccountIndex = 0; int newAccountIndex = 0;
// validate account creation // validate account creation
if (wallet.Accounts.Any()) if (wallet.Accounts.Any())
...@@ -97,17 +98,27 @@ namespace Breeze.Wallet ...@@ -97,17 +98,27 @@ namespace Breeze.Wallet
} }
// check account at index i - 1 contains transactions. // check account at index i - 1 contains transactions.
lastAccountIndex = wallet.Accounts.Max(a => a.Index); int lastAccountIndex = wallet.Accounts.Max(a => a.Index);
HdAccount previousAccount = wallet.Accounts.Single(a => a.Index == lastAccountIndex); HdAccount previousAccount = wallet.Accounts.Single(a => a.Index == lastAccountIndex);
if (!previousAccount.ExternalAddresses.Any(addresses => addresses.Transactions.Any()) && !previousAccount.InternalAddresses.Any(addresses => addresses.Transactions.Any())) 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."); throw new Exception($"Cannot create new account '{accountName}' in '{walletName}' if the previous account '{previousAccount.Name}' has not been used.");
} }
newAccountIndex = lastAccountIndex + 1;
} }
// get the extended pub key used to generate addresses for this account
var privateKey = Key.Parse(wallet.EncryptedSeed, password, wallet.Network);
var seedExtKey = new ExtKey(privateKey, wallet.ChainCode);
KeyPath keyPath = new KeyPath($"m/44'/{(int)wallet.CoinType}'/{newAccountIndex}'");
var accountExtKey = seedExtKey.Derive(keyPath);
ExtPubKey accountExtPubKey = accountExtKey.Neuter();
wallet.Accounts = wallet.Accounts.Concat(new[] {new HdAccount wallet.Accounts = wallet.Accounts.Concat(new[] {new HdAccount
{ {
Index = lastAccountIndex + 1, Index = newAccountIndex,
ExtendedPubKey = accountExtPubKey.ToString(wallet.Network),
ExternalAddresses = new List<HdAddress>(), ExternalAddresses = new List<HdAddress>(),
InternalAddresses = new List<HdAddress>(), InternalAddresses = new List<HdAddress>(),
Name = accountName, Name = accountName,
...@@ -119,6 +130,56 @@ namespace Breeze.Wallet ...@@ -119,6 +130,56 @@ namespace Breeze.Wallet
return accountName; return accountName;
} }
/// <inheritdoc />
public string CreateNewAddress(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.");
}
HdAccount account = wallet.Accounts.SingleOrDefault(a => a.Name == accountName);
if (account == null)
{
throw new Exception($"No account with name {accountName} could be found.");
}
int newAddressIndex = 0;
// validate address creation
if (account.ExternalAddresses.Any())
{
// check last created address contains transactions.
int lastAddressIndex = account.ExternalAddresses.Max(a => a.Index);
var lastAddress = account.ExternalAddresses.SingleOrDefault(a => a.Index == lastAddressIndex);
if (lastAddress != null && !lastAddress.Transactions.Any())
{
throw new Exception($"Cannot create new address in account '{accountName}' if the previous address '{lastAddress.Address}' has not been used.");
}
newAddressIndex = lastAddressIndex + 1;
}
// generate new receiving address
BitcoinPubKeyAddress address = this.GenerateAddress(account.ExtendedPubKey, newAddressIndex, false, wallet.Network);
// add address details
account.ExternalAddresses = account.ExternalAddresses.Concat(new[] {new HdAddress
{
Index = newAddressIndex,
HdPath = CreateBip44Path(wallet.CoinType, account.Index, newAddressIndex, false),
ScriptPubKey = address.ScriptPubKey,
Address = address.ToString(),
Transactions = new List<TransactionData>(),
CreationTime = DateTimeOffset.Now
}});
this.SaveToFile(wallet);
return address.ToString();
}
public WalletGeneralInfoModel GetGeneralInfo(string name) public WalletGeneralInfoModel GetGeneralInfo(string name)
{ {
throw new System.NotImplementedException(); throw new System.NotImplementedException();
...@@ -215,5 +276,30 @@ namespace Breeze.Wallet ...@@ -215,5 +276,30 @@ namespace Breeze.Wallet
this.Wallets.Add(wallet); this.Wallets.Add(wallet);
} }
} }
private BitcoinPubKeyAddress GenerateAddress(string accountExtPubKey, int index, bool isChange, Network network)
{
int change = isChange ? 1 : 0;
KeyPath keyPath = new KeyPath($"{change}/{index}");
ExtPubKey extPubKey = ExtPubKey.Parse(accountExtPubKey).Derive(keyPath);
return extPubKey.PubKey.GetAddress(network);
}
/// <summary>
/// Creates the bip44 path.
/// </summary>
/// <param name="coinType">Type of the coin.</param>
/// <param name="accountIndex">Index of the account.</param>
/// <param name="addressIndex">Index of the address.</param>
/// <param name="isChange">if set to <c>true</c> [is change].</param>
/// <returns></returns>
public static string CreateBip44Path(CoinType coinType, int accountIndex, int addressIndex, bool isChange = false)
{
//// populate the items according to the BIP44 path
//// [m/purpose'/coin_type'/account'/change/address_index]
int change = isChange ? 1 : 0;
return $"m/44'/{(int)coinType}'/{accountIndex}'/{change}/{addressIndex}";
}
} }
} }
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