Commit 72c7a83c authored by Jeremy Bokobza's avatar Jeremy Bokobza

Added Safe recovery

parent cd4e3f8f
...@@ -8,3 +8,4 @@ ...@@ -8,3 +8,4 @@
/Breeze.Api/src/Breeze.Api/bin /Breeze.Api/src/Breeze.Api/bin
/Breeze.Api/src/Breeze.Api/obj /Breeze.Api/src/Breeze.Api/obj
*.user *.user
/Breeze.Api/src/Breeze.Api/Wallets
using Microsoft.AspNetCore.Mvc; using System.Collections.Generic;
using System.IO;
using Microsoft.AspNetCore.Mvc;
using Xunit; using Xunit;
using Moq; using Moq;
using Breeze.Api.Controllers; using Breeze.Api.Controllers;
...@@ -10,9 +12,9 @@ namespace Breeze.Api.Tests ...@@ -10,9 +12,9 @@ namespace Breeze.Api.Tests
public class ControllersTests public class ControllersTests
{ {
[Fact] [Fact]
public void CreateSafesuccessfullyReturnsMnemonic() public void CreateSafeSuccessfullyReturnsMnemonic()
{ {
var mockSafeCreate = new Mock<ISafeWrapper>(); var mockSafeCreate = new Mock<ISafeWrapper>();
mockSafeCreate.Setup(safe => safe.Create(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>())).Returns("mnemonic"); mockSafeCreate.Setup(safe => safe.Create(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>())).Returns("mnemonic");
var controller = new SafeController(mockSafeCreate.Object); var controller = new SafeController(mockSafeCreate.Object);
...@@ -27,9 +29,102 @@ namespace Breeze.Api.Tests ...@@ -27,9 +29,102 @@ namespace Breeze.Api.Tests
}); });
// Assert // Assert
mockSafeCreate.VerifyAll();
var viewResult = Assert.IsType<JsonResult>(result); var viewResult = Assert.IsType<JsonResult>(result);
Assert.Equal("mnemonic", viewResult.Value); Assert.Equal("mnemonic", viewResult.Value);
Assert.NotNull(result); Assert.NotNull(result);
} }
[Fact]
public void LoadSafeSuccessfullyReturnsSafeModel()
{
SafeModel safeModel = new SafeModel
{
FileName = "myWallet",
Network = "MainNet",
Addresses = new List<string> { "address1", "address2", "address3", "address4", "address5" }
};
var mockSafeWrapper = new Mock<ISafeWrapper>();
mockSafeWrapper.Setup(safe => safe.Recover(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>())).Returns(safeModel);
var controller = new SafeController(mockSafeWrapper.Object);
// Act
var result = controller.Recover(new SafeRecoveryModel
{
Name = "myName",
FolderPath = "",
Password = "",
Network = "",
Mnemonic = "mnemonic"
});
// Assert
mockSafeWrapper.VerifyAll();
var viewResult = Assert.IsType<JsonResult>(result);
Assert.NotNull(viewResult.Value);
Assert.IsType<SafeModel>(viewResult.Value);
var model = viewResult.Value as SafeModel;
Assert.Equal("myWallet", model.FileName);
}
[Fact]
public void RecoverSafeSuccessfullyReturnsSafeModel()
{
SafeModel safeModel = new SafeModel
{
FileName = "myWallet",
Network = "MainNet",
Addresses = new List<string> { "address1", "address2", "address3", "address4", "address5" }
};
var mockSafeWrapper = new Mock<ISafeWrapper>();
mockSafeWrapper.Setup(safe => safe.Load(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>())).Returns(safeModel);
var controller = new SafeController(mockSafeWrapper.Object);
// Act
var result = controller.Load(new SafeLoadModel
{
Name = "myName",
FolderPath = "",
Password = ""
});
// Assert
mockSafeWrapper.VerifyAll();
var viewResult = Assert.IsType<JsonResult>(result);
Assert.NotNull(viewResult.Value);
Assert.IsType<SafeModel>(viewResult.Value);
var model = viewResult.Value as SafeModel;
Assert.Equal("myWallet", model.FileName);
}
[Fact]
public void FileNotFoundExceptionandReturns404()
{
var mockSafeWrapper = new Mock<ISafeWrapper>();
mockSafeWrapper.Setup(safe => safe.Load(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>())).Throws<FileNotFoundException>();
var controller = new SafeController(mockSafeWrapper.Object);
// Act
var result = controller.Load(new SafeLoadModel
{
Name = "myName",
FolderPath = "",
Password = ""
});
// Assert
mockSafeWrapper.VerifyAll();
var viewResult = Assert.IsType<ObjectResult>(result);
Assert.NotNull(viewResult);
Assert.Equal(404, viewResult.StatusCode);
}
} }
} }
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
}, },
"item": [ "item": [
{ {
"name": "Create wallet - success", "name": "Create safe - success",
"request": { "request": {
"url": "http://localhost:5000/api/safe/", "url": "http://localhost:5000/api/safe/",
"method": "POST", "method": "POST",
...@@ -28,7 +28,7 @@ ...@@ -28,7 +28,7 @@
"response": [] "response": []
}, },
{ {
"name": "Create wallet - validation errors", "name": "Create safe - validation errors",
"request": { "request": {
"url": "http://localhost:5000/api/safe/", "url": "http://localhost:5000/api/safe/",
"method": "POST", "method": "POST",
...@@ -48,7 +48,7 @@ ...@@ -48,7 +48,7 @@
"response": [] "response": []
}, },
{ {
"name": "Load wallet", "name": "Load safe",
"request": { "request": {
"url": "http://localhost:5000/api/safe/?password=123456&folderPath=MyWallets&name=myFirstWallet", "url": "http://localhost:5000/api/safe/?password=123456&folderPath=MyWallets&name=myFirstWallet",
"method": "GET", "method": "GET",
...@@ -66,6 +66,26 @@ ...@@ -66,6 +66,26 @@
"description": "" "description": ""
}, },
"response": [] "response": []
},
{
"name": "Recover safe",
"request": {
"url": "http://localhost:5000/api/safe/recover",
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json",
"description": ""
}
],
"body": {
"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}"
},
"description": ""
},
"response": []
} }
] ]
} }
\ No newline at end of file
...@@ -84,5 +84,43 @@ namespace Breeze.Api.Controllers ...@@ -84,5 +84,43 @@ namespace Breeze.Api.Controllers
return this.StatusCode((int)HttpStatusCode.BadRequest, e.Message); return this.StatusCode((int)HttpStatusCode.BadRequest, e.Message);
} }
} }
[Route("recover")]
[HttpPost]
public IActionResult Recover([FromBody]SafeRecoveryModel safeRecovery)
{
// checks the request is valid
if (!this.ModelState.IsValid)
{
var errors = this.ModelState.Values.SelectMany(e => e.Errors.Select(m => m.ErrorMessage));
return this.BadRequest(string.Join(Environment.NewLine, errors));
}
try
{
var safe = this.safeWrapper.Recover(safeRecovery.Password, safeRecovery.FolderPath, safeRecovery.Name, safeRecovery.Network, safeRecovery.Mnemonic);
return this.Json(safe);
}
catch (FileNotFoundException e)
{
Console.WriteLine(e);
// indicates that this wallet does not exist
return this.StatusCode((int)HttpStatusCode.NotFound, "Wallet not found.");
}
catch (SecurityException e)
{
Console.WriteLine(e);
// indicates that the password is wrong
return this.StatusCode((int)HttpStatusCode.Forbidden, "Wrong password, please try again.");
}
catch (Exception e)
{
Console.WriteLine(e);
return this.StatusCode((int)HttpStatusCode.BadRequest, e.Message);
}
}
} }
} }
...@@ -4,41 +4,58 @@ using System.ComponentModel.DataAnnotations; ...@@ -4,41 +4,58 @@ using System.ComponentModel.DataAnnotations;
namespace Breeze.Api.Models namespace Breeze.Api.Models
{ {
/// <summary> /// <summary>
/// Object used to create a new wallet /// Object used to create a new wallet
/// </summary> /// </summary>
public class SafeCreationModel public class SafeCreationModel
{ {
[Required(ErrorMessage = "A password is required.")] [Required(ErrorMessage = "A password is required.")]
public string Password { get; set; } public string Password { get; set; }
public string Network { get; set; } public string Network { get; set; }
[Required(ErrorMessage = "The folder path where the wallet will be created is required.")] [Required(ErrorMessage = "The folder path where the safe will be created is required.")]
public string FolderPath { get; set; } public string FolderPath { get; set; }
[Required(ErrorMessage = "The name of the wallet to create is missing.")] [Required(ErrorMessage = "The name of the safe to create is missing.")]
public string Name { get; set; } public string Name { get; set; }
} }
public class SafeLoadModel public class SafeLoadModel
{ {
[Required(ErrorMessage = "A password is required.")] [Required(ErrorMessage = "A password is required.")]
public string Password { get; set; } public string Password { get; set; }
[Required(ErrorMessage = "The folder path is required.")] [Required(ErrorMessage = "The folder path is required.")]
public string FolderPath { get; set; } public string FolderPath { get; set; }
[Required(ErrorMessage = "The name of the wallet is missing.")] [Required(ErrorMessage = "The name of the safe is missing.")]
public string Name { get; set; } public string Name { get; set; }
} }
public class SafeModel public class SafeRecoveryModel
{ {
public string Network { get; set; } [Required(ErrorMessage = "A mnemonic is required.")]
public string Mnemonic { get; set; }
public string FileName { get; set; } [Required(ErrorMessage = "A password is required.")]
public string Password { get; set; }
public IEnumerable<string> Addresses { get; set; } [Required(ErrorMessage = "The folder path is required.")]
} public string FolderPath { get; set; }
[Required(ErrorMessage = "The name of the safe is missing.")]
public string Name { get; set; }
public string Network { get; set; }
}
public class SafeModel
{
public string Network { get; set; }
public string FileName { get; set; }
public IEnumerable<string> Addresses { get; set; }
}
} }
...@@ -2,13 +2,15 @@ ...@@ -2,13 +2,15 @@
namespace Breeze.Api.Wrappers namespace Breeze.Api.Wrappers
{ {
/// <summary> /// <summary>
/// An interface enabling wallet operations. /// An interface enabling wallet operations.
/// </summary> /// </summary>
public interface ISafeWrapper public interface ISafeWrapper
{ {
string Create(string password, string folderPath, string name, string network); string Create(string password, string folderPath, string name, string network);
SafeModel Load(string password, string folderPath, string name); SafeModel Load(string password, string folderPath, string name);
}
SafeModel Recover(string password, string folderPath, string name, string network, string mnemonic);
}
} }
...@@ -6,57 +6,79 @@ using Breeze.Api.Models; ...@@ -6,57 +6,79 @@ using Breeze.Api.Models;
namespace Breeze.Api.Wrappers namespace Breeze.Api.Wrappers
{ {
/// <summary> /// <summary>
/// An implementation of the <see cref="ISafeWrapper"/> interface. /// An implementation of the <see cref="ISafeWrapper"/> interface.
/// </summary> /// </summary>
public class SafeWrapper : ISafeWrapper public class SafeWrapper : ISafeWrapper
{ {
/// <summary> /// <summary>
/// Creates a safe on the local device. /// Creates a safe on the local device.
/// </summary> /// </summary>
/// <param name="password">The user's password.</param> /// <param name="password">The user's password.</param>
/// <param name="folderPath">The folder where the safe will be saved.</param> /// <param name="folderPath">The folder where the safe will be saved.</param>
/// <param name="name">The name of the safe.</param> /// <param name="name">The name of the safe.</param>
/// <param name="network">The network for which to create a safe.</param> /// <param name="network">The network for which to create a safe.</param>
/// <returns>A mnemonic allowing recovery of the safe.</returns> /// <returns>A mnemonic allowing recovery of the safe.</returns>
public string Create(string password, string folderPath, string name, string network) public string Create(string password, string folderPath, string name, string network)
{ {
// any network different than MainNet will default to TestNet Mnemonic mnemonic;
Network net; Safe safe = Safe.Create(out mnemonic, password, Path.Combine(folderPath, $"{name}.json"), this.GetNetwork(network));
switch (network.ToLowerInvariant()) return mnemonic.ToString();
{ }
case "main":
case "mainnet":
net = Network.Main;
break;
default:
net = Network.TestNet;
break;
}
Mnemonic mnemonic; /// <summary>
Safe safe = Safe.Create(out mnemonic, password, Path.Combine(folderPath, $"{name}.json"), net); /// Loads a safe from the local device.
return mnemonic.ToString(); /// </summary>
} /// <param name="password">The user's password.</param>
/// <param name="folderPath">The folder where the safe will be loaded.</param>
/// <param name="name">The name of the safe.</param>
/// <returns>The safe loaded from the local device</returns>
public SafeModel Load(string password, string folderPath, string name)
{
Safe safe = Safe.Load(password, Path.Combine(folderPath, $"{name}.json"));
/// <summary> //TODO review here which data should be returned
/// Loads a safe from the local device. return new SafeModel
/// </summary> {
/// <param name="password">The user's password.</param> Network = safe.Network.Name,
/// <param name="folderPath">The folder where the safe will be loaded.</param> Addresses = safe.GetFirstNAddresses(10).Select(a => a.ToWif()),
/// <param name="name">The name of the safe.</param> FileName = safe.WalletFilePath
/// <returns>The safe loaded from the local device</returns> };
public SafeModel Load(string password, string folderPath, string name) }
{
Safe safe = Safe.Load(password, Path.Combine(folderPath, $"{name}.json"));
//TODO review here which data should be returned /// <summary>
return new SafeModel /// Recovers a safe from the local device.
{ /// </summary>
Network = safe.Network.Name, /// <param name="password">The user's password.</param>
Addresses = safe.GetFirstNAddresses(10).Select(a => a.ToWif()), /// <param name="folderPath">The folder where the safe will be loaded.</param>
FileName = safe.WalletFilePath /// <param name="name">The name of the safe.</param>
}; /// <param name="network">The network in which to creae this wallet</param>
} /// <param name="mnemonic">The user's mnemonic for the safe.</param>
} /// <returns></returns>
public SafeModel Recover(string password, string folderPath, string name, string network, string mnemonic)
{
Safe safe = Safe.Recover(new Mnemonic(mnemonic), password, Path.Combine(folderPath, $"{name}.json"), this.GetNetwork(network));
//TODO review here which data should be returned
return new SafeModel
{
Network = safe.Network.Name,
Addresses = safe.GetFirstNAddresses(10).Select(a => a.ToWif()),
FileName = safe.WalletFilePath
};
}
private Network GetNetwork(string network)
{
// any network different than MainNet will default to TestNet
switch (network.ToLowerInvariant())
{
case "main":
case "mainnet":
return Network.Main;
default:
return Network.TestNet;
}
}
}
} }
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