Commit 3e8b0a9f authored by Jeremy Bokobza's avatar Jeremy Bokobza

Merged from master

parents 1c4eaa2a 2279ba4a
...@@ -167,12 +167,12 @@ Cannot check if the password is good or not. If the password is wrong it'll reco ...@@ -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 ## DELETE /wallet - Deletes the wallet
Works as expected. 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 ### Parameters
``` ```
{ {
"walletName": "myFirstWallet", "walletName": "myFirstWallet",
"accountName": "account one",
"password": "123456", "password": "123456",
"coinType": 105 "coinType": 105
} }
...@@ -182,21 +182,20 @@ Works as expected. ...@@ -182,21 +182,20 @@ Works as expected.
"account one" "account one"
``` ```
## POST /wallet/address - Adds an address to an account ## GET /wallet/address - Gets an unused address
### Parameters
``` This endpoint will get the last address containing no transaction or will create a new address.
{ ### Query parameters
"walletName": "myFirstWallet", `walletName` (required) - the name of the wallet in which this address is contained.
"accountName": "account one",
"coinType": 0 `coinType` (required) - the type of coin for which to get the address, e.g 0 for bitcoin, 105 for stratis.
}
``` `accountName` (required) - the name of the account in which this address is contained.
### Responses ### Responses
``` ```
"1HDypWxXWZC5KXK259EHMnrWaa2youy7Mj" "1HDypWxXWZC5KXK259EHMnrWaa2youy7Mj"
``` ```
## GET /wallet/receive/[account1/account2] - Displays unused receive addresses of the specified wallet account ## GET /wallet/receive/[account1/account2] - Displays unused receive addresses of the specified wallet account
### Responses ### Responses
``` ```
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
"variables": [], "variables": [],
"info": { "info": {
"name": "Wallet", "name": "Wallet",
"_postman_id": "5eec0912-fcf0-50f5-05a2-0835fa13c670", "_postman_id": "57013f2c-02dc-df32-41e9-6e4aaa14ad5e",
"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"
}, },
...@@ -81,7 +81,7 @@ ...@@ -81,7 +81,7 @@
], ],
"body": { "body": {
"mode": "raw", "mode": "raw",
"raw": "{ \n\t\"password\": \"123456\",\n\t\"network\": \"Main\",\n\t\"folderPath\": \"Wallets\",\n\t\"name\": \"myFirstWalletRecovered\",\n\t\"mnemonic\": \"elbow scale error joke labor page beyond curve indicate exit brass laundry\"\n\t\n}" "raw": "{ \n\t\"password\": \"123456\",\n\t\"network\": \"Main\",\n\t\"folderPath\": \"Wallets\",\n\t\"name\": \"myRecoveredWallet\",\n\t\"mnemonic\": \"elbow scale error joke labor page beyond curve indicate exit brass laundry\",\n\t\"creationDate\": \"2016-02-25 16:20:33\"\n}"
}, },
"description": "" "description": ""
}, },
...@@ -208,7 +208,7 @@ ...@@ -208,7 +208,7 @@
"response": [] "response": []
}, },
{ {
"name": "New account for non-existant wallet fails", "name": "Get unused account in wallet",
"request": { "request": {
"url": "http://localhost:5000/api/v1/wallet/account", "url": "http://localhost:5000/api/v1/wallet/account",
"method": "POST", "method": "POST",
...@@ -221,37 +221,17 @@ ...@@ -221,37 +221,17 @@
], ],
"body": { "body": {
"mode": "raw", "mode": "raw",
"raw": "{\n\t\"walletName\": \"myFirstWallet\",\n\t\"accountName\": \"account one\"\n}" "raw": "{\n\t\"walletName\": \"myFirstWallet\",\n\t\"password\": \"123456\",\n\t\"coinType\": 0\n}"
}, },
"description": "" "description": ""
}, },
"response": [] "response": []
}, },
{ {
"name": "Create new account for wallet", "name": "Get unused address in wallet",
"request": { "request": {
"url": "http://localhost:5000/api/v1/wallet/account", "url": "http://localhost:5000/api/v1/wallet/address?walletName=wallet1&accountName=account 0&coinType=0",
"method": "POST", "method": "GET",
"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\t\"coinType\": 105\n}"
},
"description": ""
},
"response": []
},
{
"name": "Create new address for wallet",
"request": {
"url": "http://localhost:5000/api/v1/wallet/address",
"method": "POST",
"header": [ "header": [
{ {
"key": "Content-Type", "key": "Content-Type",
...@@ -261,7 +241,7 @@ ...@@ -261,7 +241,7 @@
], ],
"body": { "body": {
"mode": "raw", "mode": "raw",
"raw": "{\n\t\"walletName\": \"myFirstWallet\",\n\t\"accountName\": \"account one\",\n\t\"coinType\": 0\n}" "raw": ""
}, },
"description": "" "description": ""
}, },
......
...@@ -359,7 +359,7 @@ namespace Breeze.Wallet.Controllers ...@@ -359,7 +359,7 @@ namespace Breeze.Wallet.Controllers
/// <returns>An account name.</returns> /// <returns>An account name.</returns>
[Route("account")] [Route("account")]
[HttpPost] [HttpPost]
public IActionResult CreateNewAccount([FromBody]CreateAccountModel request) public IActionResult CreateNewAccount([FromBody]GetUnusedAccountModel request)
{ {
// checks the request is valid // checks the request is valid
if (!this.ModelState.IsValid) if (!this.ModelState.IsValid)
...@@ -370,8 +370,8 @@ namespace Breeze.Wallet.Controllers ...@@ -370,8 +370,8 @@ namespace Breeze.Wallet.Controllers
try try
{ {
var result = this.walletManager.CreateNewAccount(request.WalletName, request.CoinType, request.AccountName, request.Password); var result = this.walletManager.GetUnusedAccount(request.WalletName, request.CoinType, request.Password);
return this.Json(result); return this.Json(result.Name);
} }
catch (Exception e) catch (Exception e)
{ {
...@@ -380,12 +380,12 @@ namespace Breeze.Wallet.Controllers ...@@ -380,12 +380,12 @@ namespace Breeze.Wallet.Controllers
} }
/// <summary> /// <summary>
/// Creates a new address for a wallet. /// Gets an unused address.
/// </summary> /// </summary>
/// <returns>An address in Base58 format.</returns> /// <returns>The last created and unused address or creates a new address (in Base58 format).</returns>
[Route("address")] [Route("address")]
[HttpPost] [HttpGet]
public IActionResult CreateNewAddress([FromBody]CreateAddressModel request) public IActionResult GetUnusedAddress([FromQuery]GetUnusedAddressModel request)
{ {
// checks the request is valid // checks the request is valid
if (!this.ModelState.IsValid) if (!this.ModelState.IsValid)
...@@ -396,7 +396,7 @@ namespace Breeze.Wallet.Controllers ...@@ -396,7 +396,7 @@ namespace Breeze.Wallet.Controllers
try try
{ {
var result = this.walletManager.CreateNewAddress(request.WalletName, request.CoinType, request.AccountName); var result = this.walletManager.GetUnusedAddress(request.WalletName, request.CoinType, request.AccountName);
return this.Json(result); return this.Json(result);
} }
catch (Exception e) catch (Exception e)
......
...@@ -44,33 +44,58 @@ namespace Breeze.Wallet ...@@ -44,33 +44,58 @@ namespace Breeze.Wallet
Wallet RecoverWallet(string password, string folderPath, string name, string network, string mnemonic, string passphrase = null, DateTime? creationTime = null); Wallet RecoverWallet(string password, string folderPath, string name, string network, string mnemonic, string passphrase = null, DateTime? creationTime = null);
/// <summary> /// <summary>
/// Deleted a wallet. /// Deletes a wallet.
/// </summary> /// </summary>
/// <param name="walletFilePath">The location of the wallet file.</param> /// <param name="walletFilePath">The location of the wallet file.</param>
void DeleteWallet(string walletFilePath); void DeleteWallet(string walletFilePath);
/// <summary> /// <summary>
/// Creates a new account. /// 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> /// </summary>
/// <param name="walletName">The name of the wallet in which this account will be created.</param> /// <param name="wallet">The wallet from which to get an account.</param>
/// <param name="coinType">the type of coin for which to create an account.</param> /// <param name="coinType">The type of coin for which to get an account.</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> /// <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>An unused account.</returns>
string CreateNewAccount(string walletName, CoinType coinType, string accountName, string password); HdAccount GetUnusedAccount(Wallet wallet, CoinType coinType, string password);
/// <summary> /// <summary>
/// Creates the new address. /// Creates a new account.
/// </summary> /// </summary>
/// <param name="walletName">The name of the wallet in which this address will be created.</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="coinType">The type of coin for which to create an account.</param>
/// <param name="accountName">The name of the account in which this address will be created.</param> /// <param name="password">The password used to decrypt the private key.</param>
/// <returns>The new address, in Base58 format.</returns> /// <remarks>
string CreateNewAddress(string walletName, CoinType coinType, string accountName); /// According to BIP44, an account at index (i) can only be created when the account
/// at index (i - 1) contains transactions.
/// </remarks>
/// <returns>The new account.</returns>
HdAccount CreateNewAccount(Wallet wallet, CoinType coinType, string password);
/// <summary>
/// Gets an address that contains no transaction.
/// </summary>
/// <param name="walletName">The name of the wallet in which this address is contained.</param>
/// <param name="coinType">The type of coin for which to get the address.</param>
/// <param name="accountName">The name of the account in which this address is contained.</param>
/// <returns>An unused address or a newly created address, in Base58 format.</returns>
string GetUnusedAddress(string walletName, CoinType coinType, string accountName);
WalletGeneralInfoModel GetGeneralInfo(string walletName); WalletGeneralInfoModel GetGeneralInfo(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 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; }
}
}
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 type of coin this account contains.
/// </summary>
[Required]
public CoinType CoinType { get; set; }
/// <summary>
/// The name of the account in which to create the address.
/// </summary>
[Required]
public string AccountName { get; set; }
}
}
...@@ -100,4 +100,45 @@ namespace Breeze.Wallet.Models ...@@ -100,4 +100,45 @@ namespace Breeze.Wallet.Models
public string Hex { get; set; } public string Hex { get; set; }
} }
public class GetUnusedAddressModel
{
/// <summary>
/// The name of the wallet from which to get the address.
/// </summary>
[Required]
public string WalletName { get; set; }
/// <summary>
/// The type of coin this address is for.
/// </summary>
[Required]
public CoinType CoinType { get; set; }
/// <summary>
/// The name of the account for which to get the address.
/// </summary>
[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;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using Breeze.Wallet.JsonConverters; using Breeze.Wallet.JsonConverters;
using NBitcoin; using NBitcoin;
using NBitcoin.JsonConverters; using NBitcoin.JsonConverters;
...@@ -80,6 +81,23 @@ namespace Breeze.Wallet ...@@ -80,6 +81,23 @@ namespace Breeze.Wallet
/// </summary> /// </summary>
[JsonProperty(PropertyName = "accounts")] [JsonProperty(PropertyName = "accounts")]
public IEnumerable<HdAccount> Accounts { get; set; } 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> /// <summary>
...@@ -125,7 +143,6 @@ namespace Breeze.Wallet ...@@ -125,7 +143,6 @@ namespace Breeze.Wallet
[JsonProperty(PropertyName = "name")] [JsonProperty(PropertyName = "name")]
public string Name { get; set; } public string Name { get; set; }
/// <summary> /// <summary>
/// A path to the account as defined in BIP44. /// A path to the account as defined in BIP44.
/// </summary> /// </summary>
...@@ -156,6 +173,34 @@ namespace Breeze.Wallet ...@@ -156,6 +173,34 @@ namespace Breeze.Wallet
/// </summary> /// </summary>
[JsonProperty(PropertyName = "internalAddresses")] [JsonProperty(PropertyName = "internalAddresses")]
public IEnumerable<HdAddress> InternalAddresses { get; set; } public IEnumerable<HdAddress> InternalAddresses { get; set; }
/// <summary>
/// Gets the type of coin this account is for.
/// </summary>
/// <returns>A <see cref="CoinType"/>.</returns>
public CoinType GetCoinType()
{
string[] pathElements = this.HdPath.Split('/');
int coinType = int.Parse(pathElements[2].Replace("'", string.Empty));
return (CoinType)coinType;
}
/// <summary>
/// Gets the first receiving address that contains no transaction.
/// </summary>
/// <returns>An unused address</returns>
public HdAddress GetFirstUnusedExternalAddress()
{
var unusedAddresses = this.ExternalAddresses.Where(acc => !acc.Transactions.Any()).ToList();
if (!unusedAddresses.Any())
{
return null;
}
// gets the unused address with the lowest index
var index = unusedAddresses.Min(a => a.Index);
return unusedAddresses.Single(a => a.Index == index);
}
} }
/// <summary> /// <summary>
......
...@@ -18,10 +18,14 @@ namespace Breeze.Wallet ...@@ -18,10 +18,14 @@ namespace Breeze.Wallet
{ {
public List<Wallet> Wallets { get; } public List<Wallet> Wallets { get; }
public HashSet<Script> PubKeys { get; } public HashSet<Script> PubKeys { get; set; }
public HashSet<TransactionDetails> TrackedTransactions { get; } public HashSet<TransactionDetails> TrackedTransactions { get; }
private const int UnusedAddressesBuffer = 20;
private const int WalletRecoveryAccountsCreationCount = 3;
public WalletManager() public WalletManager()
{ {
this.Wallets = new List<Wallet>(); this.Wallets = new List<Wallet>();
...@@ -53,7 +57,6 @@ namespace Breeze.Wallet ...@@ -53,7 +57,6 @@ namespace Breeze.Wallet
ExtKey extendedKey = mnemonic.DeriveExtKey(passphrase); ExtKey extendedKey = mnemonic.DeriveExtKey(passphrase);
// create a wallet file // create a wallet file
Wallet wallet = this.GenerateWalletFile(password, folderPath, name, WalletHelpers.GetNetwork(network), extendedKey); Wallet wallet = this.GenerateWalletFile(password, folderPath, name, WalletHelpers.GetNetwork(network), extendedKey);
this.Load(wallet); this.Load(wallet);
...@@ -84,15 +87,28 @@ namespace Breeze.Wallet ...@@ -84,15 +87,28 @@ namespace Breeze.Wallet
// generate the root seed used to generate keys // generate the root seed used to generate keys
ExtKey extendedKey = (new Mnemonic(mnemonic)).DeriveExtKey(passphrase); ExtKey extendedKey = (new Mnemonic(mnemonic)).DeriveExtKey(passphrase);
Network coinNetwork = WalletHelpers.GetNetwork(network);
// create a wallet file // create a wallet file
Wallet wallet = this.GenerateWalletFile(password, folderPath, name, WalletHelpers.GetNetwork(network), extendedKey, creationTime); Wallet wallet = this.GenerateWalletFile(password, folderPath, name, coinNetwork, extendedKey, creationTime);
// generate multiple accounts and addresses from the get-go
for (int i = 0; i < WalletRecoveryAccountsCreationCount; i++)
{
HdAccount account = CreateNewAccount(wallet, CoinType.Bitcoin, password);
this.CreateAddressesInAccount(account, coinNetwork, UnusedAddressesBuffer);
this.CreateAddressesInAccount(account, coinNetwork, UnusedAddressesBuffer, true);
}
// save the changes to the file and add addresses to be tracked
this.SaveToFile(wallet);
this.PubKeys = this.LoadKeys(CoinType.Bitcoin);
this.Load(wallet); this.Load(wallet);
return wallet; return wallet;
} }
/// <inheritdoc /> /// <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); Wallet wallet = this.Wallets.SingleOrDefault(w => w.Name == walletName);
if (wallet == null) if (wallet == null)
...@@ -100,57 +116,73 @@ namespace Breeze.Wallet ...@@ -100,57 +116,73 @@ 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.");
} }
// get the accounts for this type of coin return this.GetUnusedAccount(wallet, coinType, password);
var accounts = wallet.AccountsRoot.Single(a => a.CoinType == coinType).Accounts.ToList(); }
int newAccountIndex = 0;
// validate account creation /// <inheritdoc />
if (accounts.Any()) public HdAccount GetUnusedAccount(Wallet wallet, CoinType coinType, string password)
{ {
// check account with same name doesn't already exists // get the accounts root for this type of coin
if (accounts.Any(a => a.Name == accountName)) var accountsRoot = wallet.AccountsRoot.Single(a => a.CoinType == coinType);
// check if an unused account exists
if (accountsRoot.Accounts.Any())
{
// 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. // all accounts contain transactions, create a new one
int lastAccountIndex = accounts.Max(a => a.Index); var newAccount = this.CreateNewAccount(wallet, coinType, password);
HdAccount previousAccount = accounts.Single(a => a.Index == lastAccountIndex);
if (!previousAccount.ExternalAddresses.Any(addresses => addresses.Transactions.Any()) && !previousAccount.InternalAddresses.Any(addresses => addresses.Transactions.Any())) // save the changes to the file
{ this.SaveToFile(wallet);
throw new Exception($"Cannot create new account '{accountName}' in '{walletName}' if the previous account '{previousAccount.Name}' has not been used."); 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 // get the extended pub key used to generate addresses for this account
var privateKey = Key.Parse(wallet.EncryptedSeed, password, wallet.Network); var privateKey = Key.Parse(wallet.EncryptedSeed, password, wallet.Network);
var seedExtKey = new ExtKey(privateKey, wallet.ChainCode); var seedExtKey = new ExtKey(privateKey, wallet.ChainCode);
var accountHdPath = $"m/44'/{(int) coinType}'/{newAccountIndex}'"; var accountHdPath = $"m/44'/{(int)coinType}'/{newAccountIndex}'";
KeyPath keyPath = new KeyPath(accountHdPath); KeyPath keyPath = new KeyPath(accountHdPath);
ExtKey accountExtKey = seedExtKey.Derive(keyPath); ExtKey accountExtKey = seedExtKey.Derive(keyPath);
ExtPubKey accountExtPubKey = accountExtKey.Neuter(); ExtPubKey accountExtPubKey = accountExtKey.Neuter();
accounts.Add(new HdAccount var newAccount = new HdAccount
{ {
Index = newAccountIndex, Index = newAccountIndex,
ExtendedPubKey = accountExtPubKey.ToString(wallet.Network), ExtendedPubKey = accountExtPubKey.ToString(wallet.Network),
ExternalAddresses = new List<HdAddress>(), ExternalAddresses = new List<HdAddress>(),
InternalAddresses = new List<HdAddress>(), InternalAddresses = new List<HdAddress>(),
Name = accountName, Name = $"account {newAccountIndex}",
HdPath = accountHdPath, HdPath = accountHdPath,
CreationTime = DateTimeOffset.Now CreationTime = DateTimeOffset.Now
}); };
accounts.Add(newAccount);
wallet.AccountsRoot.Single(a => a.CoinType == coinType).Accounts = accounts; wallet.AccountsRoot.Single(a => a.CoinType == coinType).Accounts = accounts;
this.SaveToFile(wallet);
return accountName; return newAccount;
} }
/// <inheritdoc /> /// <inheritdoc />
public string CreateNewAddress(string walletName, CoinType coinType, string accountName) public string GetUnusedAddress(string walletName, CoinType coinType, string accountName)
{ {
Wallet wallet = this.Wallets.SingleOrDefault(w => w.Name == walletName); Wallet wallet = this.Wallets.SingleOrDefault(w => w.Name == walletName);
if (wallet == null) if (wallet == null)
...@@ -165,42 +197,78 @@ namespace Breeze.Wallet ...@@ -165,42 +197,78 @@ namespace Breeze.Wallet
throw new Exception($"No account with name {accountName} could be found."); throw new Exception($"No account with name {accountName} could be found.");
} }
int newAddressIndex = 0;
// validate address creation // validate address creation
if (account.ExternalAddresses.Any()) if (account.ExternalAddresses.Any())
{ {
// check last created address contains transactions. // check last created address contains transactions.
int lastAddressIndex = account.ExternalAddresses.Max(a => a.Index); var firstUnusedExternalAddress = account.GetFirstUnusedExternalAddress();
var lastAddress = account.ExternalAddresses.SingleOrDefault(a => a.Index == lastAddressIndex); if (firstUnusedExternalAddress != null)
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."); return firstUnusedExternalAddress.Address;
}
}
// creates an address
this.CreateAddressesInAccount(account, wallet.Network, 1);
// persists the address to the wallet file
this.SaveToFile(wallet);
// adds the address to the list of tracked addresses
this.PubKeys = this.LoadKeys(coinType);
return account.GetFirstUnusedExternalAddress().Address;
} }
newAddressIndex = lastAddressIndex + 1; /// <summary>
/// Creates a number of addresses in the provided account.
/// </summary>
/// <param name="account">The account.</param>
/// <param name="network">The network.</param>
/// <param name="addressesQuantity">The number of addresses to create.</param>
/// <param name="isChange">Whether the addresses added are change (internal) addresses or receiving (external) addresses.</param>
/// <returns>A list of addresses in Base58.</returns>
private List<string> CreateAddressesInAccount(HdAccount account, Network network, int addressesQuantity, bool isChange = false)
{
List<string> addressesCreated = new List<string>();
var addresses = isChange ? account.InternalAddresses : account.ExternalAddresses;
// gets the index of the last address with transactions
int firstNewAddressIndex = 0;
if (addresses.Any())
{
firstNewAddressIndex = addresses.Max(add => add.Index) + 1;
} }
for (int i = firstNewAddressIndex; i < firstNewAddressIndex + addressesQuantity; i++)
{
// generate new receiving address // generate new receiving address
BitcoinPubKeyAddress address = this.GenerateAddress(account.ExtendedPubKey, newAddressIndex, false, wallet.Network); BitcoinPubKeyAddress address = this.GenerateAddress(account.ExtendedPubKey, i, false, network);
// add address details // add address details
account.ExternalAddresses = account.ExternalAddresses.Concat(new[] {new HdAddress addresses = addresses.Concat(new[] {new HdAddress
{ {
Index = newAddressIndex, Index = i,
HdPath = CreateBip44Path(coinType, account.Index, newAddressIndex, false), HdPath = CreateBip44Path(account.GetCoinType(), account.Index, i, isChange),
ScriptPubKey = address.ScriptPubKey, ScriptPubKey = address.ScriptPubKey,
Address = address.ToString(), Address = address.ToString(),
Transactions = new List<TransactionData>(), Transactions = new List<TransactionData>(),
CreationTime = DateTimeOffset.Now CreationTime = DateTimeOffset.Now
}}); }});
// persists the address to the wallet file addressesCreated.Add(address.ToString());
this.SaveToFile(wallet); }
// adds the address to the list of tracked addresses if (isChange)
this.PubKeys.Add(address.ScriptPubKey); {
return address.ToString(); account.InternalAddresses = addresses;
}
else
{
account.ExternalAddresses = addresses;
}
return addressesCreated;
} }
public WalletGeneralInfoModel GetGeneralInfo(string name) public WalletGeneralInfoModel GetGeneralInfo(string name)
...@@ -485,7 +553,7 @@ namespace Breeze.Wallet ...@@ -485,7 +553,7 @@ namespace Breeze.Wallet
SelectMany(a => a.ExternalAddresses). SelectMany(a => a.ExternalAddresses).
Select(s => s.ScriptPubKey)); Select(s => s.ScriptPubKey));
// uncomment the following for testing on a random address // 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> /// <summary>
......
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