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

Added Safe recovery

parent cd4e3f8f
......@@ -8,3 +8,4 @@
/Breeze.Api/src/Breeze.Api/bin
/Breeze.Api/src/Breeze.Api/obj
*.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 Moq;
using Breeze.Api.Controllers;
......@@ -10,9 +12,9 @@ namespace Breeze.Api.Tests
public class ControllersTests
{
[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");
var controller = new SafeController(mockSafeCreate.Object);
......@@ -27,9 +29,102 @@ namespace Breeze.Api.Tests
});
// Assert
mockSafeCreate.VerifyAll();
var viewResult = Assert.IsType<JsonResult>(result);
Assert.Equal("mnemonic", viewResult.Value);
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 @@
},
"item": [
{
"name": "Create wallet - success",
"name": "Create safe - success",
"request": {
"url": "http://localhost:5000/api/safe/",
"method": "POST",
......@@ -28,7 +28,7 @@
"response": []
},
{
"name": "Create wallet - validation errors",
"name": "Create safe - validation errors",
"request": {
"url": "http://localhost:5000/api/safe/",
"method": "POST",
......@@ -48,7 +48,7 @@
"response": []
},
{
"name": "Load wallet",
"name": "Load safe",
"request": {
"url": "http://localhost:5000/api/safe/?password=123456&folderPath=MyWallets&name=myFirstWallet",
"method": "GET",
......@@ -66,6 +66,26 @@
"description": ""
},
"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
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;
namespace Breeze.Api.Models
{
/// <summary>
/// Object used to create a new wallet
/// </summary>
public class SafeCreationModel
{
[Required(ErrorMessage = "A password is required.")]
public string Password { get; set; }
/// <summary>
/// Object used to create a new wallet
/// </summary>
public class SafeCreationModel
{
[Required(ErrorMessage = "A password is required.")]
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.")]
public string FolderPath { get; set; }
[Required(ErrorMessage = "The folder path where the safe will be created is required.")]
public string FolderPath { get; set; }
[Required(ErrorMessage = "The name of the wallet to create is missing.")]
public string Name { get; set; }
}
[Required(ErrorMessage = "The name of the safe to create is missing.")]
public string Name { get; set; }
}
public class SafeLoadModel
{
[Required(ErrorMessage = "A password is required.")]
public string Password { get; set; }
public class SafeLoadModel
{
[Required(ErrorMessage = "A password is required.")]
public string Password { get; set; }
[Required(ErrorMessage = "The folder path is required.")]
public string FolderPath { get; set; }
[Required(ErrorMessage = "The folder path is required.")]
public string FolderPath { get; set; }
[Required(ErrorMessage = "The name of the wallet is missing.")]
public string Name { get; set; }
}
[Required(ErrorMessage = "The name of the safe is missing.")]
public string Name { get; set; }
}
public class SafeModel
{
public string Network { get; set; }
public class SafeRecoveryModel
{
[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 @@
namespace Breeze.Api.Wrappers
{
/// <summary>
/// An interface enabling wallet operations.
/// </summary>
public interface ISafeWrapper
{
string Create(string password, string folderPath, string name, string network);
/// <summary>
/// An interface enabling wallet operations.
/// </summary>
public interface ISafeWrapper
{
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;
namespace Breeze.Api.Wrappers
{
/// <summary>
/// An implementation of the <see cref="ISafeWrapper"/> interface.
/// </summary>
public class SafeWrapper : ISafeWrapper
{
/// <summary>
/// Creates a safe on the local device.
/// </summary>
/// <param name="password">The user's password.</param>
/// <param name="folderPath">The folder where the safe will be saved.</param>
/// <param name="name">The name of the safe.</param>
/// <param name="network">The network for which to create a safe.</param>
/// <returns>A mnemonic allowing recovery of the safe.</returns>
public string Create(string password, string folderPath, string name, string network)
{
// any network different than MainNet will default to TestNet
Network net;
switch (network.ToLowerInvariant())
{
case "main":
case "mainnet":
net = Network.Main;
break;
default:
net = Network.TestNet;
break;
}
/// <summary>
/// An implementation of the <see cref="ISafeWrapper"/> interface.
/// </summary>
public class SafeWrapper : ISafeWrapper
{
/// <summary>
/// Creates a safe on the local device.
/// </summary>
/// <param name="password">The user's password.</param>
/// <param name="folderPath">The folder where the safe will be saved.</param>
/// <param name="name">The name of the safe.</param>
/// <param name="network">The network for which to create a safe.</param>
/// <returns>A mnemonic allowing recovery of the safe.</returns>
public string Create(string password, string folderPath, string name, string network)
{
Mnemonic mnemonic;
Safe safe = Safe.Create(out mnemonic, password, Path.Combine(folderPath, $"{name}.json"), this.GetNetwork(network));
return mnemonic.ToString();
}
Mnemonic mnemonic;
Safe safe = Safe.Create(out mnemonic, password, Path.Combine(folderPath, $"{name}.json"), net);
return mnemonic.ToString();
}
/// <summary>
/// Loads a safe from the local device.
/// </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>
/// Loads a safe from the local device.
/// </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"));
//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
};
}
//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
};
}
}
/// <summary>
/// Recovers a safe from the local device.
/// </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>
/// <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