Commit 8e6ab15d authored by Dan Gershony's avatar Dan Gershony

Moving the wallet feature to the full node

parent 3435ae71
 
Microsoft Visual Studio Solution File, Format Version 12.00 Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15 # Visual Studio 15
VisualStudioVersion = 15.0.26430.6 VisualStudioVersion = 15.0.26430.12
MinimumVisualStudioVersion = 10.0.40219.1 MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{807563C4-7259-434D-B604-A14C3DCF8E30}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{807563C4-7259-434D-B604-A14C3DCF8E30}"
EndProject EndProject
...@@ -17,16 +17,18 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Breeze.Api.Tests", "src\Bre ...@@ -17,16 +17,18 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Breeze.Api.Tests", "src\Bre
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Breeze.Wallet", "src\Breeze.Wallet\Breeze.Wallet.csproj", "{D16CD478-9D1E-4C69-91AD-43539E94A215}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Breeze.Wallet", "src\Breeze.Wallet\Breeze.Wallet.csproj", "{D16CD478-9D1E-4C69-91AD-43539E94A215}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Breeze.Daemon", "src\Breeze.Daemon\Breeze.Daemon.csproj", "{1B598E33-667F-496D-BC0D-88276E8E7632}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Breeze.TumbleBit.Client", "src\Breeze.TumbleBit.Client\Breeze.TumbleBit.Client.csproj", "{2490DD1A-6C14-47F2-A9C6-56761A52E2D9}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Breeze.TumbleBit.Client", "src\Breeze.TumbleBit.Client\Breeze.TumbleBit.Client.csproj", "{2490DD1A-6C14-47F2-A9C6-56761A52E2D9}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Breeze.Common", "src\Breeze.Common\Breeze.Common.csproj", "{C726817D-9E2F-4DDC-90A4-B6895CF5309B}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "TumbleBit", "TumbleBit", "{1B724678-2B73-483E-B981-3A6733C2194E}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "TumbleBit", "TumbleBit", "{1B724678-2B73-483E-B981-3A6733C2194E}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NTumbleBit", "src\NTumbleBit\NTumbleBit.csproj", "{29E411B1-5687-43EE-A71B-6CCEC2289129}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NTumbleBit", "src\NTumbleBit\NTumbleBit.csproj", "{29E411B1-5687-43EE-A71B-6CCEC2289129}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Stratis.Bitcoin", "..\..\StratisBitcoinFullNode\Stratis.Bitcoin\Stratis.Bitcoin.csproj", "{6702A093-B42C-44DA-86BB-CD8F87E9A665}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Stratis.Bitcoin.Common", "..\..\StratisBitcoinFullNode\Stratis.Bitcoin.Common\Stratis.Bitcoin.Common.csproj", "{4F9F7CF7-326C-4FC0-9EFB-209536A42030}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Breeze.Daemon", "src\Breeze.Daemon\Breeze.Daemon.csproj", "{3F0937A2-3182-42D9-866F-3DEDEE28EC5A}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
...@@ -45,22 +47,26 @@ Global ...@@ -45,22 +47,26 @@ Global
{D16CD478-9D1E-4C69-91AD-43539E94A215}.Debug|Any CPU.Build.0 = Debug|Any CPU {D16CD478-9D1E-4C69-91AD-43539E94A215}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D16CD478-9D1E-4C69-91AD-43539E94A215}.Release|Any CPU.ActiveCfg = Release|Any CPU {D16CD478-9D1E-4C69-91AD-43539E94A215}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D16CD478-9D1E-4C69-91AD-43539E94A215}.Release|Any CPU.Build.0 = Release|Any CPU {D16CD478-9D1E-4C69-91AD-43539E94A215}.Release|Any CPU.Build.0 = Release|Any CPU
{1B598E33-667F-496D-BC0D-88276E8E7632}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1B598E33-667F-496D-BC0D-88276E8E7632}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1B598E33-667F-496D-BC0D-88276E8E7632}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1B598E33-667F-496D-BC0D-88276E8E7632}.Release|Any CPU.Build.0 = Release|Any CPU
{2490DD1A-6C14-47F2-A9C6-56761A52E2D9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {2490DD1A-6C14-47F2-A9C6-56761A52E2D9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2490DD1A-6C14-47F2-A9C6-56761A52E2D9}.Debug|Any CPU.Build.0 = Debug|Any CPU {2490DD1A-6C14-47F2-A9C6-56761A52E2D9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2490DD1A-6C14-47F2-A9C6-56761A52E2D9}.Release|Any CPU.ActiveCfg = Release|Any CPU {2490DD1A-6C14-47F2-A9C6-56761A52E2D9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2490DD1A-6C14-47F2-A9C6-56761A52E2D9}.Release|Any CPU.Build.0 = Release|Any CPU {2490DD1A-6C14-47F2-A9C6-56761A52E2D9}.Release|Any CPU.Build.0 = Release|Any CPU
{C726817D-9E2F-4DDC-90A4-B6895CF5309B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C726817D-9E2F-4DDC-90A4-B6895CF5309B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C726817D-9E2F-4DDC-90A4-B6895CF5309B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C726817D-9E2F-4DDC-90A4-B6895CF5309B}.Release|Any CPU.Build.0 = Release|Any CPU
{29E411B1-5687-43EE-A71B-6CCEC2289129}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {29E411B1-5687-43EE-A71B-6CCEC2289129}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{29E411B1-5687-43EE-A71B-6CCEC2289129}.Debug|Any CPU.Build.0 = Debug|Any CPU {29E411B1-5687-43EE-A71B-6CCEC2289129}.Debug|Any CPU.Build.0 = Debug|Any CPU
{29E411B1-5687-43EE-A71B-6CCEC2289129}.Release|Any CPU.ActiveCfg = Release|Any CPU {29E411B1-5687-43EE-A71B-6CCEC2289129}.Release|Any CPU.ActiveCfg = Release|Any CPU
{29E411B1-5687-43EE-A71B-6CCEC2289129}.Release|Any CPU.Build.0 = Release|Any CPU {29E411B1-5687-43EE-A71B-6CCEC2289129}.Release|Any CPU.Build.0 = Release|Any CPU
{6702A093-B42C-44DA-86BB-CD8F87E9A665}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6702A093-B42C-44DA-86BB-CD8F87E9A665}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6702A093-B42C-44DA-86BB-CD8F87E9A665}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6702A093-B42C-44DA-86BB-CD8F87E9A665}.Release|Any CPU.Build.0 = Release|Any CPU
{4F9F7CF7-326C-4FC0-9EFB-209536A42030}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4F9F7CF7-326C-4FC0-9EFB-209536A42030}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4F9F7CF7-326C-4FC0-9EFB-209536A42030}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4F9F7CF7-326C-4FC0-9EFB-209536A42030}.Release|Any CPU.Build.0 = Release|Any CPU
{3F0937A2-3182-42D9-866F-3DEDEE28EC5A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3F0937A2-3182-42D9-866F-3DEDEE28EC5A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3F0937A2-3182-42D9-866F-3DEDEE28EC5A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3F0937A2-3182-42D9-866F-3DEDEE28EC5A}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
...@@ -69,9 +75,8 @@ Global ...@@ -69,9 +75,8 @@ Global
{E7B3E9EB-34E8-4B10-B296-4D5270E314A4} = {807563C4-7259-434D-B604-A14C3DCF8E30} {E7B3E9EB-34E8-4B10-B296-4D5270E314A4} = {807563C4-7259-434D-B604-A14C3DCF8E30}
{BD5174B4-DCE8-4594-9A16-B83E56767770} = {807563C4-7259-434D-B604-A14C3DCF8E30} {BD5174B4-DCE8-4594-9A16-B83E56767770} = {807563C4-7259-434D-B604-A14C3DCF8E30}
{D16CD478-9D1E-4C69-91AD-43539E94A215} = {807563C4-7259-434D-B604-A14C3DCF8E30} {D16CD478-9D1E-4C69-91AD-43539E94A215} = {807563C4-7259-434D-B604-A14C3DCF8E30}
{1B598E33-667F-496D-BC0D-88276E8E7632} = {807563C4-7259-434D-B604-A14C3DCF8E30}
{2490DD1A-6C14-47F2-A9C6-56761A52E2D9} = {807563C4-7259-434D-B604-A14C3DCF8E30} {2490DD1A-6C14-47F2-A9C6-56761A52E2D9} = {807563C4-7259-434D-B604-A14C3DCF8E30}
{C726817D-9E2F-4DDC-90A4-B6895CF5309B} = {807563C4-7259-434D-B604-A14C3DCF8E30}
{29E411B1-5687-43EE-A71B-6CCEC2289129} = {1B724678-2B73-483E-B981-3A6733C2194E} {29E411B1-5687-43EE-A71B-6CCEC2289129} = {1B724678-2B73-483E-B981-3A6733C2194E}
{3F0937A2-3182-42D9-866F-3DEDEE28EC5A} = {807563C4-7259-434D-B604-A14C3DCF8E30}
EndGlobalSection EndGlobalSection
EndGlobal EndGlobal
...@@ -10,6 +10,7 @@ ...@@ -10,6 +10,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\..\..\StratisBitcoinFullNode\Stratis.Bitcoin\Stratis.Bitcoin.csproj" />
<ProjectReference Include="..\Breeze.Api\Breeze.Api.csproj" /> <ProjectReference Include="..\Breeze.Api\Breeze.Api.csproj" />
<ProjectReference Include="..\Breeze.Wallet\Breeze.Wallet.csproj" /> <ProjectReference Include="..\Breeze.Wallet\Breeze.Wallet.csproj" />
</ItemGroup> </ItemGroup>
......
using System;
using System.Collections.Generic;
using System.IO;
using Microsoft.AspNetCore.Mvc;
using Xunit;
using Moq;
using Breeze.Wallet;
using Breeze.Wallet.Controllers;
using Breeze.Common.JsonErrors;
using Breeze.Wallet.Helpers;
using Breeze.Wallet.Models;
using NBitcoin;
using NBitcoin.Protocol;
using Stratis.Bitcoin.Configuration;
using Stratis.Bitcoin.Connection;
namespace Breeze.Api.Tests
{
public class ControllersTests
{
[Fact]
public void CreateWalletSuccessfullyReturnsMnemonic()
{
Mnemonic mnemonic = new Mnemonic(Wordlist.English, WordCount.Twelve);
var mockWalletCreate = new Mock<IWalletManager>();
mockWalletCreate.Setup(wallet => wallet.CreateWallet(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), null)).Returns(mnemonic);
var controller = new WalletController(mockWalletCreate.Object, new Mock<ITracker>().Object, It.IsAny<ConnectionManager>(), Network.Main, new Mock<ConcurrentChain>().Object);
// Act
var result = controller.Create(new WalletCreationRequest
{
Name = "myName",
FolderPath = "",
Password = "",
Network = ""
});
// Assert
mockWalletCreate.VerifyAll();
var viewResult = Assert.IsType<JsonResult>(result);
Assert.Equal(mnemonic.ToString(), viewResult.Value);
Assert.NotNull(result);
}
[Fact]
public void LoadWalletSuccessfullyReturnsWalletModel()
{
Wallet.Wallet wallet = new Wallet.Wallet
{
Name = "myWallet",
Network = WalletHelpers.GetNetwork("mainnet")
};
var mockWalletWrapper = new Mock<IWalletManager>();
mockWalletWrapper.Setup(w => w.RecoverWallet(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<DateTime>(), null)).Returns(wallet);
var controller = new WalletController(mockWalletWrapper.Object, new Mock<ITracker>().Object, It.IsAny<ConnectionManager>(), Network.Main, new Mock<ConcurrentChain>().Object);
// Act
var result = controller.Recover(new WalletRecoveryRequest
{
Name = "myWallet",
FolderPath = "",
Password = "",
Network = "MainNet",
Mnemonic = "mnemonic"
});
// Assert
mockWalletWrapper.VerifyAll();
var viewResult = Assert.IsType<OkResult>(result);
Assert.Equal(200, viewResult.StatusCode);
}
[Fact]
public void RecoverWalletSuccessfullyReturnsWalletModel()
{
Wallet.Wallet wallet = new Wallet.Wallet
{
Name = "myWallet",
Network = WalletHelpers.GetNetwork("mainnet")
};
var mockWalletWrapper = new Mock<IWalletManager>();
mockWalletWrapper.Setup(w => w.LoadWallet(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>())).Returns(wallet);
var controller = new WalletController(mockWalletWrapper.Object, new Mock<ITracker>().Object, It.IsAny<ConnectionManager>(), Network.Main, new Mock<ConcurrentChain>().Object);
// Act
var result = controller.Load(new WalletLoadRequest
{
Name = "myWallet",
FolderPath = "",
Password = ""
});
// Assert
mockWalletWrapper.VerifyAll();
var viewResult = Assert.IsType<OkResult>(result);
Assert.Equal(200, viewResult.StatusCode);
}
[Fact]
public void FileNotFoundExceptionandReturns404()
{
var mockWalletWrapper = new Mock<IWalletManager>();
mockWalletWrapper.Setup(wallet => wallet.LoadWallet(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>())).Throws<FileNotFoundException>();
var controller = new WalletController(mockWalletWrapper.Object, new Mock<ITracker>().Object, It.IsAny<ConnectionManager>(), Network.Main, new Mock<ConcurrentChain>().Object);
// Act
var result = controller.Load(new WalletLoadRequest
{
Name = "myName",
FolderPath = "",
Password = ""
});
// Assert
mockWalletWrapper.VerifyAll();
var viewResult = Assert.IsType<ErrorResult>(result);
Assert.NotNull(viewResult);
Assert.Equal(404, viewResult.StatusCode);
}
}
}
using System;
using System.Collections.Generic;
using System.Text;
using Breeze.Wallet.Helpers;
using NBitcoin;
using Xunit;
namespace Breeze.Api.Tests
{
public class WalletHelpersTest
{
[Fact]
public void GetMainNetworkRetuirnsNetworkMain()
{
Network network = WalletHelpers.GetNetwork("main");
Assert.Equal(Network.Main, network);
}
[Fact]
public void GetMainNetNetworkRetuirnsNetworkMain()
{
Network network = WalletHelpers.GetNetwork("mainnet");
Assert.Equal(Network.Main, network);
}
[Fact]
public void GetTestNetworkRetuirnsNetworkTest()
{
Network network = WalletHelpers.GetNetwork("test");
Assert.Equal(Network.TestNet, network);
}
[Fact]
public void GetTestNetNetworkRetuirnsNetworkTest()
{
Network network = WalletHelpers.GetNetwork("testnet");
Assert.Equal(Network.TestNet, network);
}
[Fact]
public void GetNetworkIsCaseInsensitive()
{
Network testNetwork = WalletHelpers.GetNetwork("Test");
Assert.Equal(Network.TestNet, testNetwork);
Network mainNetwork = WalletHelpers.GetNetwork("MainNet");
Assert.Equal(Network.Main, mainNetwork);
}
[Fact]
public void WrongNetworkThrowsArgumentException()
{
var exception = Record.Exception(() => WalletHelpers.GetNetwork("myNetwork"));
Assert.NotNull(exception);
Assert.IsType<ArgumentException>(exception);
}
}
}
...@@ -29,10 +29,13 @@ ...@@ -29,10 +29,13 @@
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="1.1.2" /> <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="1.1.2" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="1.1.2" /> <PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="1.1.2" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="1.1.2" /> <PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="1.1.2" />
<PackageReference Include="Stratis.Bitcoin" Version="1.0.1.8-alpha" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="1.0.0" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="1.0.0" />
<PackageReference Include="System.Reactive" Version="3.1.1" /> <PackageReference Include="System.Reactive" Version="3.1.1" />
<PackageReference Include="System.Runtime.Loader" Version="4.3.0" /> <PackageReference Include="System.Runtime.Loader" Version="4.3.0" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\StratisBitcoinFullNode\Stratis.Bitcoin\Stratis.Bitcoin.csproj" />
</ItemGroup>
</Project> </Project>
...@@ -33,7 +33,7 @@ namespace Breeze.Api ...@@ -33,7 +33,7 @@ namespace Breeze.Api
body = string.Join(Environment.NewLine, arguments.Values); body = string.Join(Environment.NewLine, arguments.Values);
} }
this.logger.LogDebug($"Received {request.Method} {request.GetDisplayUrl()}. Body: '{body}'"); this.logger.LogInformation($"Received {request.Method} {request.GetDisplayUrl()}. Body: '{body}'");
await next(); await next();
} }
} }
......
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard1.6</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore" Version="1.1.2" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Abstractions" Version="1.1.3" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Core" Version="1.1.3" />
<PackageReference Include="Newtonsoft.Json" Version="10.0.2" />
</ItemGroup>
</Project>
\ No newline at end of file
using System.Collections.Generic;
using System.Net;
namespace Breeze.Common.JsonErrors
{
public static class ErrorHelpers
{
public static ErrorResult BuildErrorResponse(HttpStatusCode statusCode, string message, string description)
{
ErrorResponse errorResponse = new ErrorResponse
{
Errors = new List<ErrorModel>
{
new ErrorModel
{
Status = (int) statusCode,
Message = message,
Description = description
}
}
};
return new ErrorResult((int)statusCode, errorResponse);
}
}
}
using System.Collections.Generic;
using Newtonsoft.Json;
namespace Breeze.Common.JsonErrors
{
public class ErrorResponse
{
[JsonProperty(PropertyName = "errors")]
public List<ErrorModel> Errors { get; set; }
}
public class ErrorModel
{
[JsonProperty(PropertyName = "status")]
public int Status { get; set; }
[JsonProperty(PropertyName = "message")]
public string Message { get; set; }
[JsonProperty(PropertyName = "description")]
public string Description { get; set; }
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Breeze.Common.JsonErrors;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace Breeze.Common.JsonErrors
{
public class ErrorResult : ObjectResult
{
public ErrorResult(int statusCode, ErrorResponse value) : base(value)
{
StatusCode = statusCode;
}
}
}
...@@ -13,6 +13,7 @@ ...@@ -13,6 +13,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\..\..\StratisBitcoinFullNode\Stratis.Bitcoin\Stratis.Bitcoin.csproj" />
<ProjectReference Include="..\Breeze.Api\Breeze.Api.csproj" /> <ProjectReference Include="..\Breeze.Api\Breeze.Api.csproj" />
<ProjectReference Include="..\Breeze.TumbleBit.Client\Breeze.TumbleBit.Client.csproj" /> <ProjectReference Include="..\Breeze.TumbleBit.Client\Breeze.TumbleBit.Client.csproj" />
<ProjectReference Include="..\Breeze.Wallet\Breeze.Wallet.csproj" /> <ProjectReference Include="..\Breeze.Wallet\Breeze.Wallet.csproj" />
...@@ -21,7 +22,6 @@ ...@@ -21,7 +22,6 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="1.1.2" /> <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="1.1.2" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="1.1.2" /> <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="1.1.2" />
<PackageReference Include="Stratis.Bitcoin" Version="1.0.1.8-alpha" />
</ItemGroup> </ItemGroup>
</Project> </Project>
...@@ -10,6 +10,7 @@ using Stratis.Bitcoin.Configuration; ...@@ -10,6 +10,7 @@ using Stratis.Bitcoin.Configuration;
using Stratis.Bitcoin.Logging; using Stratis.Bitcoin.Logging;
using Breeze.Wallet; using Breeze.Wallet;
using Stratis.Bitcoin.Notifications; using Stratis.Bitcoin.Notifications;
using Stratis.Bitcoin.Utilities;
namespace Breeze.Daemon namespace Breeze.Daemon
{ {
...@@ -17,13 +18,13 @@ namespace Breeze.Daemon ...@@ -17,13 +18,13 @@ namespace Breeze.Daemon
{ {
public static void Main(string[] args) public static void Main(string[] args)
{ {
// configure Full Node // configure Full Node
Logs.Configure(new LoggerFactory().AddConsole(LogLevel.Trace, false)); Logs.Configure(Logs.GetLoggerFactory(args));
NodeSettings nodeSettings = NodeSettings.FromArguments(args); NodeSettings nodeSettings = NodeSettings.FromArguments(args);
var fullNodeBuilder = new FullNodeBuilder() var fullNodeBuilder = new FullNodeBuilder()
.UseNodeSettings(nodeSettings) .UseNodeSettings(nodeSettings)
.UseWallet() .UseLightWallet()
.UseBlockNotification() .UseBlockNotification()
.UseTransactionNotification() .UseTransactionNotification()
.UseApi(); .UseApi();
...@@ -36,13 +37,10 @@ namespace Breeze.Daemon ...@@ -36,13 +37,10 @@ namespace Breeze.Daemon
fullNodeBuilder.UseTumbleBit(new Uri(tumblerAddress)); fullNodeBuilder.UseTumbleBit(new Uri(tumblerAddress));
} }
var node = (FullNode)fullNodeBuilder.Build(); var node = fullNodeBuilder.Build();
// start Full Node - this will also start the API // start Full Node - this will also start the API
node.Start(); node.Run();
Console.WriteLine("Press any key to stop");
Console.ReadLine();
node.Dispose();
} }
} }
} }
...@@ -21,11 +21,11 @@ ...@@ -21,11 +21,11 @@
<PackageReference Include="Microsoft.AspNetCore.Mvc.Core" Version="1.1.3" /> <PackageReference Include="Microsoft.AspNetCore.Mvc.Core" Version="1.1.3" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Versioning" Version="1.0.3" /> <PackageReference Include="Microsoft.AspNetCore.Mvc.Versioning" Version="1.0.3" />
<PackageReference Include="Serilog.Extensions.Logging.File" Version="1.0.1" /> <PackageReference Include="Serilog.Extensions.Logging.File" Version="1.0.1" />
<PackageReference Include="Stratis.Bitcoin" Version="1.0.1.8-alpha" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Breeze.Common\Breeze.Common.csproj" /> <ProjectReference Include="..\..\..\..\StratisBitcoinFullNode\Stratis.Bitcoin.Common\Stratis.Bitcoin.Common.csproj" />
<ProjectReference Include="..\..\..\..\StratisBitcoinFullNode\Stratis.Bitcoin\Stratis.Bitcoin.csproj" />
<ProjectReference Include="..\NTumbleBit\NTumbleBit.csproj" /> <ProjectReference Include="..\NTumbleBit\NTumbleBit.csproj" />
</ItemGroup> </ItemGroup>
......
...@@ -2,9 +2,9 @@ ...@@ -2,9 +2,9 @@
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Threading.Tasks; using System.Threading.Tasks;
using Breeze.Common.JsonErrors;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Breeze.TumbleBit.Client; using Breeze.TumbleBit.Client;
using Stratis.Bitcoin.Common.JsonErrors;
namespace Breeze.TumbleBit.Controllers namespace Breeze.TumbleBit.Controllers
{ {
......
...@@ -20,11 +20,10 @@ ...@@ -20,11 +20,10 @@
<PackageReference Include="Microsoft.AspNetCore.Mvc.Versioning" Version="1.0.3" /> <PackageReference Include="Microsoft.AspNetCore.Mvc.Versioning" Version="1.0.3" />
<PackageReference Include="Serilog.Extensions.Logging.File" Version="1.0.1" /> <PackageReference Include="Serilog.Extensions.Logging.File" Version="1.0.1" />
<PackageReference Include="System.ValueTuple" Version="4.3.1" /> <PackageReference Include="System.ValueTuple" Version="4.3.1" />
<PackageReference Include="Stratis.Bitcoin" Version="1.0.1.8-alpha" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Breeze.Common\Breeze.Common.csproj" /> <ProjectReference Include="..\..\..\..\StratisBitcoinFullNode\Stratis.Bitcoin\Stratis.Bitcoin.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>
using System;
using System.Collections.Generic;
using System.Text;
using NBitcoin;
namespace Breeze.Wallet
{
public static class ChainExtensions
{
/// <summary>
/// Determines whether the chain is downloaded and up to date.
/// </summary>
/// <param name="chain">The chain.</param>
public static bool IsDownloaded(this ConcurrentChain chain)
{
return chain.Tip.Header.BlockTime.ToUnixTimeSeconds() > (DateTimeOffset.Now.ToUnixTimeSeconds() - TimeSpan.FromHours(1).TotalSeconds);
}
/// <summary>
/// Gets the height of the first block created after this date.
/// </summary>
/// <param name="chain">The chain of blocks.</param>
/// <param name="date">The date.</param>
/// <returns>The height of the first block created after the date.</returns>
public static int GetHeightAtTime(this ConcurrentChain chain, DateTime date)
{
int blockSyncStart = 0;
int upperLimit = chain.Tip.Height;
int lowerLimit = 0;
bool found = false;
while (!found)
{
int check = lowerLimit + (upperLimit - lowerLimit) / 2;
if (chain.GetBlock(check).Header.BlockTime >= date)
{
upperLimit = check;
}
else if (chain.GetBlock(check).Header.BlockTime < date)
{
lowerLimit = check;
}
if (upperLimit - lowerLimit <= 1)
{
blockSyncStart = upperLimit;
found = true;
}
}
return blockSyncStart;
}
}
}
\ No newline at end of file
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Security;
using Breeze.Common.JsonErrors;
using Microsoft.AspNetCore.Mvc;
using Breeze.Wallet.Models;
using NBitcoin;
using Stratis.Bitcoin.Connection;
namespace Breeze.Wallet.Controllers
{
/// <summary>
/// Controller providing operations on a wallet.
/// </summary>
[Route("api/v{version:apiVersion}/[controller]")]
public class WalletController : Controller
{
private readonly IWalletManager walletManager;
private readonly ITracker tracker;
private readonly CoinType coinType;
private readonly Network network;
private readonly ConnectionManager connectionManager;
private readonly ConcurrentChain chain;
public WalletController(IWalletManager walletManager, ITracker tracker, ConnectionManager connectionManager, Network network, ConcurrentChain chain)
{
this.walletManager = walletManager;
this.tracker = tracker;
this.connectionManager = connectionManager;
this.network = network;
this.coinType = (CoinType)network.Consensus.CoinType;
this.chain = chain;
}
/// <summary>
/// Creates a new wallet on the local machine.
/// </summary>
/// <param name="request">The object containing the parameters used to create the wallet.</param>
/// <returns>A JSON object containing the mnemonic created for the new wallet.</returns>
[Route("create")]
[HttpPost]
public IActionResult Create([FromBody]WalletCreationRequest 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
{
// get the wallet folder
DirectoryInfo walletFolder = GetWalletFolder(request.FolderPath);
Mnemonic mnemonic = this.walletManager.CreateWallet(request.Password, walletFolder.FullName, request.Name, request.Network);
return this.Json(mnemonic.ToString());
}
catch (InvalidOperationException e)
{
// indicates that this wallet already exists
return ErrorHelpers.BuildErrorResponse(HttpStatusCode.Conflict, "This wallet already exists.", e.ToString());
}
}
/// <summary>
/// Loads a wallet previously created by the user.
/// </summary>
/// <param name="request">The name of the wallet to load.</param>
/// <returns></returns>
[Route("load")]
[HttpPost]
public IActionResult Load([FromBody]WalletLoadRequest 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
{
// get the wallet folder
DirectoryInfo walletFolder = GetWalletFolder(request.FolderPath);
Wallet wallet = this.walletManager.LoadWallet(request.Password, walletFolder.FullName, request.Name);
return this.Ok();
}
catch (FileNotFoundException e)
{
return ErrorHelpers.BuildErrorResponse(HttpStatusCode.NotFound, "This wallet was not found at the specified location.", e.ToString());
}
catch (SecurityException e)
{
// indicates that the password is wrong
return ErrorHelpers.BuildErrorResponse(HttpStatusCode.Forbidden, "Wrong password, please try again.", e.ToString());
}
catch (Exception e)
{
return ErrorHelpers.BuildErrorResponse(HttpStatusCode.BadRequest, e.Message, e.ToString());
}
}
/// <summary>
/// Recovers a wallet.
/// </summary>
/// <param name="request">The object containing the parameters used to recover a wallet.</param>
/// <returns></returns>
[Route("recover")]
[HttpPost]
public IActionResult Recover([FromBody]WalletRecoveryRequest 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
{
// get the wallet folder
DirectoryInfo walletFolder = GetWalletFolder(request.FolderPath);
Wallet wallet = this.walletManager.RecoverWallet(request.Password, walletFolder.FullName, request.Name, request.Network, request.Mnemonic, request.CreationDate, null);
// start syncing the wallet from the creation date
this.tracker.SyncFrom(request.CreationDate);
return this.Ok();
}
catch (InvalidOperationException e)
{
// indicates that this wallet already exists
return ErrorHelpers.BuildErrorResponse(HttpStatusCode.Conflict, "This wallet already exists.", e.ToString());
}
catch (FileNotFoundException e)
{
// indicates that this wallet does not exist
return ErrorHelpers.BuildErrorResponse(HttpStatusCode.NotFound, "Wallet not found.", e.ToString());
}
catch (Exception e)
{
return ErrorHelpers.BuildErrorResponse(HttpStatusCode.BadRequest, e.Message, e.ToString());
}
}
/// <summary>
/// Get some general info about a wallet.
/// </summary>
/// <param name="request">The name of the wallet.</param>
/// <returns></returns>
[Route("general-info")]
[HttpGet]
public IActionResult GetGeneralInfo([FromQuery] WalletName 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
{
Wallet wallet = this.walletManager.GetWallet(request.Name);
var model = new WalletGeneralInfoModel
{
Network = wallet.Network,
WalletFilePath = wallet.WalletFilePath,
CreationTime = wallet.CreationTime,
LastBlockSyncedHeight = wallet.AccountsRoot.Single(a => a.CoinType == this.coinType).LastBlockSyncedHeight,
ConnectedNodes = this.connectionManager.ConnectedNodes.Count(),
ChainTip = this.chain.Tip.Height,
IsDecrypted = true
};
return this.Json(model);
}
catch (Exception e)
{
return ErrorHelpers.BuildErrorResponse(HttpStatusCode.BadRequest, e.Message, e.ToString());
}
}
/// <summary>
/// Retrieves the history of a wallet.
/// </summary>
/// <param name="request">The request parameters.</param>
/// <returns></returns>
[Route("history")]
[HttpGet]
public IActionResult GetHistory([FromQuery] WalletHistoryRequest 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
{
WalletHistoryModel model = new WalletHistoryModel { TransactionsHistory = new List<TransactionItemModel>() };
// get transactions contained in the wallet
var addresses = this.walletManager.GetHistoryByCoinType(request.WalletName, request.CoinType);
foreach (var address in addresses.Where(a => !a.IsChangeAddress()))
{
foreach (var transaction in address.Transactions)
{
TransactionItemModel item = new TransactionItemModel();
if (transaction.Amount > Money.Zero)
{
item.Type = TransactionItemType.Received;
item.ToAddress = address.Address;
item.Amount = transaction.Amount;
}
else
{
item.Type = TransactionItemType.Send;
item.Amount = Money.Zero;
if (transaction.Payments != null)
{
item.Payments = new List<PaymentDetailModel>();
foreach (var payment in transaction.Payments)
{
item.Payments.Add(new PaymentDetailModel
{
DestinationAddress = payment.DestinationAddress,
Amount = payment.Amount
});
item.Amount += payment.Amount;
}
}
var changeAddress = addresses.Single(a => a.IsChangeAddress() && a.Transactions.Any(t => t.Id == transaction.Id));
item.Fee = transaction.Amount.Abs() - item.Amount - changeAddress.Transactions.First(t => t.Id == transaction.Id).Amount;
}
item.Id = transaction.Id;
item.Timestamp = transaction.CreationTime;
item.ConfirmedInBlock = transaction.BlockHeight;
model.TransactionsHistory.Add(item);
}
}
model.TransactionsHistory = model.TransactionsHistory.OrderByDescending(t => t.Timestamp).ToList();
return this.Json(model);
}
catch (Exception e)
{
return ErrorHelpers.BuildErrorResponse(HttpStatusCode.BadRequest, e.Message, e.ToString());
}
}
/// <summary>
/// Gets the balance of a wallet.
/// </summary>
/// <param name="request">The request parameters.</param>
/// <returns></returns>
[Route("balance")]
[HttpGet]
public IActionResult GetBalance([FromQuery] WalletBalanceRequest 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
{
WalletBalanceModel model = new WalletBalanceModel { AccountsBalances = new List<AccountBalance>() };
var accounts = this.walletManager.GetAccountsByCoinType(request.WalletName, request.CoinType).ToList();
foreach (var account in accounts)
{
var allTransactions = account.ExternalAddresses.SelectMany(a => a.Transactions)
.Concat(account.InternalAddresses.SelectMany(i => i.Transactions)).ToList();
AccountBalance balance = new AccountBalance
{
CoinType = request.CoinType,
Name = account.Name,
HdPath = account.HdPath,
AmountConfirmed = allTransactions.Where(t => t.IsConfirmed()).Sum(t => t.Amount),
AmountUnconfirmed = allTransactions.Where(t => !t.IsConfirmed()).Sum(t => t.Amount)
};
model.AccountsBalances.Add(balance);
}
return this.Json(model);
}
catch (Exception e)
{
return ErrorHelpers.BuildErrorResponse(HttpStatusCode.BadRequest, e.Message, e.ToString());
}
}
/// <summary>
/// Builds a transaction.
/// </summary>
/// <param name="request">The transaction parameters.</param>
/// <returns>All the details of the transaction, including the hex used to execute it.</returns>
[Route("build-transaction")]
[HttpPost]
public IActionResult BuildTransaction([FromBody] BuildTransactionRequest 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 transactionResult = this.walletManager.BuildTransaction(request.WalletName, request.AccountName, request.CoinType, request.Password, request.DestinationAddress, request.Amount, request.FeeType, request.AllowUnconfirmed);
var model = new WalletBuildTransactionModel
{
Hex = transactionResult.hex,
Fee = transactionResult.fee,
TransactionId = transactionResult.transactionId
};
return this.Json(model);
}
catch (Exception e)
{
return ErrorHelpers.BuildErrorResponse(HttpStatusCode.BadRequest, e.Message, e.ToString());
}
}
/// <summary>
/// Sends a transaction.
/// </summary>
/// <param name="request">The hex representing the transaction.</param>
/// <returns></returns>
[Route("send-transaction")]
[HttpPost]
public IActionResult SendTransaction([FromBody] SendTransactionRequest 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.SendTransaction(request.Hex);
if (result)
{
return this.Ok();
}
return this.StatusCode((int)HttpStatusCode.BadRequest);
}
catch (Exception e)
{
return ErrorHelpers.BuildErrorResponse(HttpStatusCode.BadRequest, e.Message, e.ToString());
}
}
/// <summary>
/// Lists all the wallet files found under the default folder.
/// </summary>
/// <returns>A list of the wallets files found.</returns>
[Route("files")]
[HttpGet]
public IActionResult ListWalletsFiles()
{
try
{
DirectoryInfo walletsFolder = GetWalletFolder();
WalletFileModel model = new WalletFileModel
{
WalletsPath = walletsFolder.FullName,
WalletsFiles = Directory.EnumerateFiles(walletsFolder.FullName, "*.json", SearchOption.TopDirectoryOnly).Select(p => Path.GetFileName(p))
};
return this.Json(model);
}
catch (Exception e)
{
return ErrorHelpers.BuildErrorResponse(HttpStatusCode.BadRequest, e.Message, e.ToString());
}
}
/// <summary>
/// Creates a new account for a wallet.
/// </summary>
/// <returns>An account name.</returns>
[Route("account")]
[HttpPost]
public IActionResult CreateNewAccount([FromBody]GetUnusedAccountModel 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.GetUnusedAccount(request.WalletName, request.CoinType, request.Password);
return this.Json(result.Name);
}
catch (Exception e)
{
return ErrorHelpers.BuildErrorResponse(HttpStatusCode.BadRequest, e.Message, e.ToString());
}
}
/// <summary>
/// Gets an unused address.
/// </summary>
/// <returns>The last created and unused address or creates a new address (in Base58 format).</returns>
[Route("address")]
[HttpGet]
public IActionResult GetUnusedAddress([FromQuery]GetUnusedAddressModel 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.GetUnusedAddress(request.WalletName, request.CoinType, request.AccountName);
return this.Json(result);
}
catch (Exception e)
{
return ErrorHelpers.BuildErrorResponse(HttpStatusCode.BadRequest, e.Message, e.ToString());
}
}
/// <summary>
/// Gets a folder.
/// </summary>
/// <returns>The path folder of the folder.</returns>
/// <remarks>The folder is created if it doesn't exist.</remarks>
private static DirectoryInfo GetWalletFolder(string folderPath = null)
{
if (string.IsNullOrEmpty(folderPath))
{
folderPath = WalletManager.GetDefaultWalletFolderPath();
}
return Directory.CreateDirectory(folderPath);
}
}
}
using System;
using System.Collections.Generic;
using System.Text;
using NBitcoin;
using Stratis.Bitcoin.Utilities;
namespace Breeze.Wallet.Helpers
{
/// <summary>
/// Contains a collection of helpers methods.
/// </summary>
public static class WalletHelpers
{
/// <summary>
/// Get the network on which to operate.
/// </summary>
/// <param name="network">The network</param>
/// <returns>A <see cref="Network"/> object.</returns>
public static Network GetNetwork(string network)
{
Guard.NotEmpty(network, nameof(network));
switch (network.ToLowerInvariant())
{
case "main":
case "mainnet":
return Network.Main;
case "test":
case "testnet":
return Network.TestNet;
default:
throw new ArgumentException($"Network '{network}' is not a valid network.");
}
}
}
}
using System;
using System.Threading.Tasks;
using NBitcoin;
namespace Breeze.Wallet
{
public interface ITracker
{
/// <summary>
/// Initializes the tracker.
/// </summary>
/// <returns></returns>
Task Initialize();
/// <summary>
/// Waits for the chain to download.
/// </summary>
/// <returns></returns>
Task WaitForChainDownloadAsync();
/// <summary>
/// Synchronize the wallet starting from the date passed as a parameter.
/// </summary>
/// <param name="date">The date from which to start the sync process.</param>
/// <returns>The height of the block sync will start from</returns>
void SyncFrom(DateTime date);
/// <summary>
/// Synchronize the wallet starting from the height passed as a parameter.
/// </summary>
/// <param name="height">The height from which to start the sync process.</param>
/// <returns>The height of the block sync will start from</returns>
void SyncFrom(int height);
}
}
using System;
using System.Collections.Generic;
using Breeze.Wallet.Models;
using NBitcoin;
namespace Breeze.Wallet
{
/// <summary>
/// Interface for a manager providing operations on wallets.
/// </summary>
public interface IWalletManager : IDisposable
{
/// <summary>
/// Creates a wallet and persist it as a file on the local system.
/// </summary>
/// <param name="password">The password used to encrypt sensitive info.</param>
/// <param name="folderPath">The folder where the wallet will be saved.</param>
/// <param name="name">The name of the wallet.</param>
/// <param name="network">The network this wallet is for.</param>
/// <param name="passphrase">The passphrase used in the seed.</param>
/// <returns>A mnemonic defining the wallet's seed used to generate addresses.</returns>
Mnemonic CreateWallet(string password, string folderPath, string name, string network, string passphrase = null);
/// <summary>
/// Loads a wallet from a file.
/// </summary>
/// <param name="password">The user's password.</param>
/// <param name="folderPath">The folder where the wallet will be loaded.</param>
/// <param name="name">The name of the wallet.</param>
/// <returns>The wallet.</returns>
Wallet LoadWallet(string password, string folderPath, string name);
/// <summary>
/// Recovers a wallet.
/// </summary>
/// <param name="password">The user's password.</param>
/// <param name="folderPath">The folder where the wallet will be loaded.</param>
/// <param name="name">The name of the wallet.</param>
/// <param name="network">The network in which to creae this wallet</param>
/// <param name="mnemonic">The user's mnemonic for the wallet.</param>
/// <param name="passphrase">The passphrase used in the seed.</param>
/// <param name="creationTime">The date and time this wallet was created.</param>
/// <returns>The recovered wallet.</returns>
Wallet RecoverWallet(string password, string folderPath, string name, string network, string mnemonic, DateTime creationTime, string passphrase = null);
/// <summary>
/// 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="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 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);
/// <summary>
/// Gets a collection of addresses containing transactions for this coin.
/// </summary>
/// <param name="walletName">The name of the wallet to get history from.</param>
/// <param name="coinType">Type of the coin.</param>
/// <returns></returns>
IEnumerable<HdAddress> GetHistoryByCoinType(string walletName, CoinType coinType);
/// <summary>
/// Gets a collection of addresses containing transactions for this coin.
/// </summary>
/// <param name="wallet">The wallet to get history from.</param>
/// <param name="coinType">Type of the coin.</param>
/// <returns></returns>
IEnumerable<HdAddress> GetHistoryByCoinType(Wallet wallet, CoinType coinType);
/// <summary>
/// Gets some general information about a wallet.
/// </summary>
/// <param name="walletName">The name of the wallet.</param>
/// <returns></returns>
Wallet GetWallet(string walletName);
/// <summary>
/// Gets a list of accounts filtered by coin type.
/// </summary>
/// <param name="walletName">The name of the wallet to look into.</param>
/// <param name="coinType">The type of coin to filter by.</param>
/// <returns></returns>
IEnumerable<HdAccount> GetAccountsByCoinType(string walletName, CoinType coinType);
/// <summary>
/// Builds a transaction to be sent to the network.
/// </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>
/// <param name="password">The password used to decrypt the private key.</param>
/// <param name="destinationAddress">The destination address to send the funds to.</param>
/// <param name="amount">The amount of funds to be sent.</param>
/// <param name="feeType">The type of fee to be included.</param>
/// <param name="allowUnconfirmed">Whether or not we allow this transaction to rely on unconfirmed outputs.</param>
/// <returns></returns>
(string hex, uint256 transactionId, Money fee) BuildTransaction(string walletName, string accountName, CoinType coinType, string password, string destinationAddress, Money amount, string feeType, bool allowUnconfirmed);
/// <summary>
/// Sends a transaction to the network.
/// </summary>
/// <param name="transactionHex">The hex of the transaction.</param>
/// <returns></returns>
bool SendTransaction(string transactionHex);
/// <summary>
/// Processes a block received from the network.
/// </summary>
/// <param name="height">The height of the block in the blockchain.</param>
/// <param name="block">The block.</param>
void ProcessBlock(int height, Block block);
/// <summary>
/// Processes a transaction received from the network.
/// </summary>
/// <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="block">The block in which this transaction was included.</param>
void ProcessTransaction(Transaction transaction, int? blockHeight = null, Block block = null);
/// <summary>
/// Saves the wallet into the file system.
/// </summary>
/// <param name="wallet">The wallet to save.</param>
void SaveToFile(Wallet wallet);
/// <summary>
/// Saves all the loaded wallets into the file system.
/// </summary>
void SaveToFile();
/// <summary>
/// Updates the wallet with the height of the last block synced.
/// </summary>
/// <param name="wallet">The wallet to update.</param>
/// <param name="height">The height of the last block synced.</param>
void UpdateLastBlockSyncedHeight(Wallet wallet, int height);
/// <summary>
/// Updates all the loaded wallets with the height of the last block synced.
/// </summary>
/// <param name="height">The height of the last block synced.</param>
void UpdateLastBlockSyncedHeight(int height);
}
}
\ No newline at end of file
using System;
using System.Collections.Generic;
using System.Text;
using Newtonsoft.Json;
namespace Breeze.Wallet.JsonConverters
{
/// <summary>
/// Converter used to convert <see cref="byte"/> arrays to and from JSON.
/// </summary>
/// <seealso cref="Newtonsoft.Json.JsonConverter" />
public class ByteArrayConverter : JsonConverter
{
/// <inheritdoc />
public override bool CanConvert(Type objectType)
{
return objectType == typeof(byte[]);
}
/// <inheritdoc />
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
return Convert.FromBase64String((string)reader.Value);
}
/// <inheritdoc />
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
writer.WriteValue(Convert.ToBase64String((byte[])value));
}
}
}
using Newtonsoft.Json;
using System;
namespace Breeze.Wallet.JsonConverters
{
/// <summary>
/// Converter used to convert <see cref="DateTimeOffset"/> to and from Unix time.
/// </summary>
/// <seealso cref="Newtonsoft.Json.JsonConverter" />
public class DateTimeOffsetConverter : JsonConverter
{
/// <inheritdoc />
public override bool CanConvert(Type objectType)
{
return objectType == typeof(DateTimeOffset);
}
/// <inheritdoc />
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
return DateTimeOffset.FromUnixTimeSeconds(long.Parse((string)reader.Value));
}
/// <inheritdoc />
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
writer.WriteValue(((DateTimeOffset)value).ToUnixTimeSeconds().ToString());
}
}
}
using System;
using Breeze.Wallet.Helpers;
using NBitcoin;
using Newtonsoft.Json;
namespace Breeze.Wallet.JsonConverters
{
/// <summary>
/// Converter used to convert <see cref="Network"/> to and from JSON.
/// </summary>
/// <seealso cref="Newtonsoft.Json.JsonConverter" />
public class NetworkConverter : JsonConverter
{
/// <inheritdoc />
public override bool CanConvert(Type objectType)
{
return objectType == typeof(Network);
}
/// <inheritdoc />
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
return WalletHelpers.GetNetwork((string)reader.Value);
}
/// <inheritdoc />
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
writer.WriteValue(((Network)value).ToString());
}
}
}
using Stratis.Bitcoin.Builder.Feature; using Stratis.Bitcoin.Builder.Feature;
using Breeze.Wallet.Controllers;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Stratis.Bitcoin.Builder; using Stratis.Bitcoin.Builder;
using Stratis.Bitcoin.Logging; using Stratis.Bitcoin.Logging;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Serilog; using Serilog;
using Stratis.Bitcoin.Wallet;
namespace Breeze.Wallet namespace Breeze.Wallet
{ {
public class WalletFeature : FullNodeFeature public class LightWalletFeature : FullNodeFeature
{ {
private readonly ITracker tracker; private readonly TrackNotifier trackNotifier;
private readonly IWalletManager walletManager;
public WalletFeature(ITracker tracker, IWalletManager walletManager) public LightWalletFeature(TrackNotifier trackNotifier)
{ {
this.tracker = tracker; this.trackNotifier = trackNotifier;
this.walletManager = walletManager; }
}
public override void Start() public override void Start()
{ {
this.tracker.Initialize(); this.trackNotifier.Initialize().GetAwaiter().GetResult();
} }
public override void Stop() public override void Stop()
{ {
this.walletManager.Dispose();
base.Stop(); base.Stop();
} }
} }
public static class WalletFeatureExtension public static class WalletFeatureExtension
{ {
public static IFullNodeBuilder UseWallet(this IFullNodeBuilder fullNodeBuilder) public static IFullNodeBuilder UseLightWallet(this IFullNodeBuilder fullNodeBuilder)
{ {
// use the wallet and on top of that start to notifier
fullNodeBuilder.UseWallet();
fullNodeBuilder.ConfigureFeature(features => fullNodeBuilder.ConfigureFeature(features =>
{ {
features features
.AddFeature<WalletFeature>() .AddFeature<LightWalletFeature>()
.FeatureServices(services => .FeatureServices(services =>
{ {
var loggerFactory = Logs.LoggerFactory; services.AddSingleton<TrackNotifier>();
loggerFactory.AddFile("Logs/Breeze-{Date}.json", isJson: true, minimumLevel:LogLevel.Debug, fileSizeLimitBytes: 10000000);
services.AddSingleton<ITracker, Tracker>();
services.AddSingleton<ILoggerFactory>(loggerFactory);
services.AddSingleton<IWalletManager, WalletManager>();
services.AddSingleton<WalletController>();
}); });
}); });
......
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
namespace Breeze.Wallet.Models
{
public class RequestModel
{
public override string ToString()
{
return JsonConvert.SerializeObject(this, Formatting.Indented);
}
}
/// <summary>
/// Object used to create a new wallet
/// </summary>
public class WalletCreationRequest : RequestModel
{
[Required(ErrorMessage = "A password is required.")]
public string Password { get; set; }
public string Network { get; set; }
public string FolderPath { get; set; }
[Required(ErrorMessage = "The name of the wallet to create is missing.")]
public string Name { get; set; }
}
public class WalletLoadRequest : RequestModel
{
[Required(ErrorMessage = "A password is required.")]
public string Password { get; set; }
public string FolderPath { get; set; }
[Required(ErrorMessage = "The name of the wallet is missing.")]
public string Name { get; set; }
}
public class WalletRecoveryRequest : RequestModel
{
[Required(ErrorMessage = "A mnemonic is required.")]
public string Mnemonic { get; set; }
[Required(ErrorMessage = "A password is required.")]
public string Password { get; set; }
public string FolderPath { get; set; }
[Required(ErrorMessage = "The name of the wallet is missing.")]
public string Name { get; set; }
public string Network { get; set; }
[JsonConverter(typeof(IsoDateTimeConverter))]
public DateTime CreationDate { get; set; }
}
public class WalletHistoryRequest : RequestModel
{
[Required(ErrorMessage = "The name of the wallet is missing.")]
public string WalletName { get; set; }
[Required(ErrorMessage = "The type of coin for which history is requested is missing.")]
public CoinType CoinType { get; set; }
}
public class WalletBalanceRequest : RequestModel
{
[Required(ErrorMessage = "The name of the wallet is missing.")]
public string WalletName { get; set; }
[Required(ErrorMessage = "The type of coin for which history is requested is missing.")]
public CoinType CoinType { get; set; }
}
public class WalletName : RequestModel
{
[Required(ErrorMessage = "The name of the wallet is missing.")]
public string Name { get; set; }
}
public class BuildTransactionRequest : RequestModel
{
[Required(ErrorMessage = "The name of the wallet is missing.")]
public string WalletName { get; set; }
[Required]
public CoinType CoinType { get; set; }
[Required(ErrorMessage = "The name of the account is missing.")]
public string AccountName { get; set; }
[Required(ErrorMessage = "A password is required.")]
public string Password { get; set; }
[Required(ErrorMessage = "A destination address is required.")]
public string DestinationAddress { get; set; }
[Required(ErrorMessage = "An amount is required.")]
public string Amount { get; set; }
[Required(ErrorMessage = "A fee type is required. It can be 'low', 'medium' or 'high'.")]
public string FeeType { get; set; }
public bool AllowUnconfirmed { get; set; }
}
public class SendTransactionRequest : RequestModel
{
[Required(ErrorMessage = "A transaction in hexadecimal format is required.")]
public string Hex { get; set; }
}
public class GetUnusedAddressModel : RequestModel
{
/// <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 : RequestModel
{
/// <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 System.Threading.Tasks;
using NBitcoin;
using Newtonsoft.Json;
namespace Breeze.Wallet.Models
{
public class WalletBalanceModel
{
[JsonProperty(PropertyName = "balances")]
public List<AccountBalance> AccountsBalances { get; set; }
}
public class AccountBalance
{
[JsonProperty(PropertyName = "accountName")]
public string Name { get; set; }
[JsonProperty(PropertyName = "accountHdPath")]
public string HdPath { get; set; }
[JsonProperty(PropertyName = "coinType")]
public CoinType CoinType { get; set; }
[JsonProperty(PropertyName = "amountConfirmed")]
public Money AmountConfirmed { get; set; }
[JsonProperty(PropertyName = "amountUnconfirmed")]
public Money AmountUnconfirmed { get; set; }
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NBitcoin;
using Newtonsoft.Json;
namespace Breeze.Wallet.Models
{
public class WalletBuildTransactionModel
{
[JsonProperty(PropertyName = "fee")]
public Money Fee { get; set; }
[JsonProperty(PropertyName = "hex")]
public string Hex { get; set; }
[JsonProperty(PropertyName = "transactionId")]
public uint256 TransactionId { get; set; }
}
}
using System;
using System.Collections.Generic;
using System.Text;
using Newtonsoft.Json;
namespace Breeze.Wallet.Models
{
public class WalletFileModel
{
[JsonProperty(PropertyName = "walletsPath")]
public string WalletsPath { get; set; }
[JsonProperty(PropertyName = "walletsFiles")]
public IEnumerable<string> WalletsFiles { get; set; }
}
}
using System;
using Breeze.Wallet.JsonConverters;
using NBitcoin;
using Newtonsoft.Json;
namespace Breeze.Wallet.Models
{
public class WalletGeneralInfoModel
{
[JsonProperty(PropertyName = "walletFilePath")]
public string WalletFilePath { get; set; }
[JsonProperty(PropertyName = "network")]
[JsonConverter(typeof(NetworkConverter))]
public Network Network { get; set; }
/// <summary>
/// The time this wallet was created.
/// </summary>
[JsonProperty(PropertyName = "creationTime")]
[JsonConverter(typeof(DateTimeOffsetConverter))]
public DateTimeOffset CreationTime { get; set; }
[JsonProperty(PropertyName = "isDecrypted")]
public bool IsDecrypted { get; set; }
/// <summary>
/// The height of the last block that was synced.
/// </summary>
[JsonProperty(PropertyName = "lastBlockSyncedHeight")]
public int? LastBlockSyncedHeight { get; set; }
/// <summary>
/// The total number of blocks.
/// </summary>
[JsonProperty(PropertyName = "chainTip")]
public int? ChainTip { get; set; }
/// <summary>
/// The total number of nodes that we're connected to.
/// </summary>
[JsonProperty(PropertyName = "connectedNodes")]
public int ConnectedNodes { get; set; }
}
}
using System;
using System.Collections.Generic;
using Breeze.Wallet.JsonConverters;
using NBitcoin;
using NBitcoin.JsonConverters;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Serialization;
namespace Breeze.Wallet.Models
{
public class WalletHistoryModel
{
[JsonProperty(PropertyName = "transactionsHistory")]
public List<TransactionItemModel> TransactionsHistory { get; set; }
}
public class TransactionItemModel
{
[JsonProperty(PropertyName = "type")]
[JsonConverter(typeof(StringEnumConverter), true)]
public TransactionItemType Type { get; set; }
/// <summary>
/// The Base58 representation of this address.
/// </summary>
[JsonProperty(PropertyName = "toAddress", NullValueHandling = NullValueHandling.Ignore)]
public string ToAddress { get; set; }
[JsonProperty(PropertyName = "id")]
[JsonConverter(typeof(UInt256JsonConverter))]
public uint256 Id { get; set; }
[JsonProperty(PropertyName = "amount")]
public Money Amount { get; set; }
/// <summary>
/// A list of payments made out in this transaction.
/// </summary>
[JsonProperty(PropertyName = "payments", NullValueHandling = NullValueHandling.Ignore)]
public ICollection<PaymentDetailModel> Payments { get; set; }
[JsonProperty(PropertyName = "fee", NullValueHandling = NullValueHandling.Ignore)]
public Money Fee { get; set; }
/// <summary>
/// The height of the block in which this transaction was confirmed.
/// </summary>
[JsonProperty(PropertyName = "confirmedInBlock", NullValueHandling = NullValueHandling.Ignore)]
public int? ConfirmedInBlock { get; set; }
[JsonProperty(PropertyName = "timestamp")]
[JsonConverter(typeof(DateTimeOffsetConverter))]
public DateTimeOffset Timestamp { get; set; }
}
public class PaymentDetailModel
{
/// <summary>
/// The Base58 representation of the destination address.
/// </summary>
[JsonProperty(PropertyName = "destinationAddress")]
public string DestinationAddress { get; set; }
/// <summary>
/// The transaction amount.
/// </summary>
[JsonProperty(PropertyName = "amount")]
[JsonConverter(typeof(MoneyJsonConverter))]
public Money Amount { get; set; }
}
public enum TransactionItemType
{
Received,
Send
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Newtonsoft.Json;
namespace Breeze.Wallet.Models
{
public class WalletModel
{
[JsonProperty(PropertyName = "network")]
public string Network { get; set; }
[JsonProperty(PropertyName = "fileName")]
public string FileName { get; set; }
[JsonProperty(PropertyName = "addresses")]
public IEnumerable<string> Addresses { get; set; }
}
}
using NBitcoin;
using Stratis.Bitcoin;
namespace Breeze.Wallet.Notifications
{
/// <summary>
/// Observer that receives notifications about the arrival of new <see cref="Block"/>s.
/// </summary>
public class BlockObserver : SignalObserver<Block>
{
private readonly ConcurrentChain chain;
private readonly IWalletManager walletManager;
public BlockObserver(ConcurrentChain chain, IWalletManager walletManager)
{
this.chain = chain;
this.walletManager = walletManager;
}
/// <summary>
/// Manages what happens when a new block is received.
/// </summary>
/// <param name="block">The new block</param>
protected override void OnNextCore(Block block)
{
var hash = block.Header.GetHash();
var height = this.chain.GetBlock(hash).Height;
this.walletManager.ProcessBlock(height, block);
}
}
}
using Stratis.Bitcoin;
using System;
using NBitcoin;
namespace Breeze.Wallet.Notifications
{
/// <summary>
/// Manages the subscription of the block observer to the block signaler.
/// </summary>
public class BlockSubscriber
{
private readonly ISignaler<Block> signaler;
private readonly BlockObserver observer;
public BlockSubscriber(ISignaler<Block> signaler, BlockObserver observer)
{
this.signaler = signaler;
this.observer = observer;
}
/// <summary>
/// Subscribes the block observer to the block signaler.
/// </summary>
/// <returns>An <see cref="IDisposable"/></returns>
public IDisposable Subscribe()
{
return this.signaler.Subscribe(this.observer);
}
}
}
using NBitcoin;
using Stratis.Bitcoin;
namespace Breeze.Wallet.Notifications
{
/// <summary>
/// Observer that receives notifications about the arrival of new <see cref="Transaction"/>s.
/// </summary>
public class TransactionObserver : SignalObserver<Transaction>
{
private readonly IWalletManager walletManager;
public TransactionObserver(IWalletManager walletManager)
{
this.walletManager = walletManager;
}
/// <summary>
/// Manages what happens when a new transaction is received.
/// </summary>
/// <param name="transaction">The new transaction</param>
protected override void OnNextCore(Transaction transaction)
{
this.walletManager.ProcessTransaction(transaction);
}
}
}
using Stratis.Bitcoin;
using System;
using NBitcoin;
namespace Breeze.Wallet.Notifications
{
/// <summary>
/// Manages the subscription of the transaction observer to the transaction signaler.
/// </summary>
public class TransactionSubscriber
{
private readonly ISignaler<Transaction> signaler;
private readonly TransactionObserver observer;
public TransactionSubscriber(ISignaler<Transaction> signaler, TransactionObserver observer)
{
this.signaler = signaler;
this.observer = observer;
}
/// <summary>
/// Subscribes the transaction observer to the transaction signaler.
/// </summary>
/// <returns>An <see cref="IDisposable"/></returns>
public IDisposable Subscribe()
{
return this.signaler.Subscribe(this.observer);
}
}
}
...@@ -6,50 +6,44 @@ using System.Reactive.Linq; ...@@ -6,50 +6,44 @@ using System.Reactive.Linq;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Breeze.Wallet.Notifications;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using NBitcoin; using NBitcoin;
using Stratis.Bitcoin; using Stratis.Bitcoin;
using Stratis.Bitcoin.Notifications; using Stratis.Bitcoin.Notifications;
using Stratis.Bitcoin.Utilities; using Stratis.Bitcoin.Utilities;
using Stratis.Bitcoin.Wallet;
using Stratis.Bitcoin.Wallet.Notifications;
namespace Breeze.Wallet namespace Breeze.Wallet
{ {
public class Tracker : ITracker public class TrackNotifier
{ {
private readonly WalletManager walletManager; private readonly WalletManager walletManager;
private readonly ConcurrentChain chain; private readonly ConcurrentChain chain;
private readonly Signals signals;
private readonly BlockNotification blockNotification; private readonly BlockNotification blockNotification;
private readonly CoinType coinType; private readonly CoinType coinType;
private readonly ILogger logger; private readonly ILogger logger;
public Tracker(ILoggerFactory loggerFactory, IWalletManager walletManager, ConcurrentChain chain, Signals signals, BlockNotification blockNotification, Network network) public TrackNotifier(ILoggerFactory loggerFactory, IWalletManager walletManager,
ConcurrentChain chain, BlockNotification blockNotification, Network network)
{ {
this.walletManager = walletManager as WalletManager; this.walletManager = walletManager as WalletManager;
this.chain = chain; this.chain = chain;
this.signals = signals;
this.blockNotification = blockNotification; this.blockNotification = blockNotification;
this.coinType = (CoinType)network.Consensus.CoinType; this.coinType = (CoinType)network.Consensus.CoinType;
this.logger = loggerFactory.CreateLogger(this.GetType().FullName); this.logger = loggerFactory.CreateLogger(this.GetType().FullName);
} }
/// <inheritdoc /> /// <inheritdoc />
public async Task Initialize() public Task Initialize()
{ {
// get the chain headers. This needs to be up-to-date before we really do anything // get the chain headers. This needs to be up-to-date before we really do anything
await this.WaitForChainDownloadAsync(); //await this.WaitForChainDownloadAsync();
// subscribe to receiving blocks and transactions
BlockSubscriber sub = new BlockSubscriber(this.signals.Blocks, new BlockObserver(this.chain, this.walletManager));
sub.Subscribe();
TransactionSubscriber txSub = new TransactionSubscriber(this.signals.Transactions, new TransactionObserver(this.walletManager));
txSub.Subscribe();
// start syncing blocks // start syncing blocks
var bestHeightForSyncing = this.FindBestHeightForSyncing(); var bestHeightForSyncing = this.FindBestHeightForSyncing();
this.SyncFrom(bestHeightForSyncing); this.SyncFrom(bestHeightForSyncing);
this.logger.LogInformation($"Tracker initialized. Syncing from {bestHeightForSyncing}."); return Task.CompletedTask;
} }
private int FindBestHeightForSyncing() private int FindBestHeightForSyncing()
......
using System;
using System.Collections.Generic;
using System.Linq;
using Breeze.Wallet.JsonConverters;
using NBitcoin;
using NBitcoin.JsonConverters;
using Newtonsoft.Json;
namespace Breeze.Wallet
{
/// <summary>
/// A wallet
/// </summary>
public class Wallet
{
/// <summary>
/// The name of this wallet.
/// </summary>
[JsonProperty(PropertyName = "name")]
public string Name { get; set; }
/// <summary>
/// The seed for this wallet, password encrypted.
/// </summary>
[JsonProperty(PropertyName = "encryptedSeed")]
public string EncryptedSeed { get; set; }
/// <summary>
/// The chain code.
/// </summary>
[JsonProperty(PropertyName = "chainCode")]
[JsonConverter(typeof(ByteArrayConverter))]
public byte[] ChainCode { get; set; }
/// <summary>
/// The network this wallet is for.
/// </summary>
[JsonProperty(PropertyName = "network")]
[JsonConverter(typeof(NetworkConverter))]
public Network Network { get; set; }
/// <summary>
/// The time this wallet was created.
/// </summary>
[JsonProperty(PropertyName = "creationTime")]
[JsonConverter(typeof(DateTimeOffsetConverter))]
public DateTimeOffset CreationTime { get; set; }
/// <summary>
/// The location of the wallet file on the local system.
/// </summary>
[JsonProperty(PropertyName = "walletFilePath")]
public string WalletFilePath { get; set; }
/// <summary>
/// The root of the accounts tree.
/// </summary>
[JsonProperty(PropertyName = "accountsRoot")]
public IEnumerable<AccountRoot> AccountsRoot { get; set; }
/// <summary>
/// Gets the type of the accounts by coin.
/// </summary>
/// <param name="coinType">Type of the coin.</param>
/// <returns></returns>
public IEnumerable<HdAccount> GetAccountsByCoinType(CoinType coinType)
{
return this.AccountsRoot.Where(a => a.CoinType == coinType).SelectMany(a => a.Accounts);
}
/// <summary>
/// Gets all the transactions by coin type.
/// </summary>
/// <param name="coinType">Type of the coin.</param>
/// <returns></returns>
public IEnumerable<TransactionData> GetAllTransactionsByCoinType(CoinType coinType)
{
List<TransactionData> result = new List<TransactionData>();
var accounts = this.GetAccountsByCoinType(coinType).ToList();
foreach (var address in accounts.SelectMany(a => a.ExternalAddresses).Concat(accounts.SelectMany(a => a.InternalAddresses)))
{
result.AddRange(address.Transactions);
}
return result;
}
/// <summary>
/// Gets all the pub keys conatined in this wallet.
/// </summary>
/// <param name="coinType">Type of the coin.</param>
/// <returns></returns>
public IEnumerable<Script> GetAllPubKeysByCoinType(CoinType coinType)
{
var accounts = this.GetAccountsByCoinType(coinType).ToList();
foreach (var address in accounts.SelectMany(a => a.ExternalAddresses).Concat(accounts.SelectMany(a => a.InternalAddresses)))
{
yield return address.ScriptPubKey;
}
}
}
/// <summary>
/// The root for the accounts for any type of coins.
/// </summary>
public class AccountRoot
{
/// <summary>
/// The type of coin, Bitcoin or Stratis.
/// </summary>
[JsonProperty(PropertyName = "coinType")]
public CoinType CoinType { get; set; }
/// <summary>
/// The height of the last block that was synced.
/// </summary>
[JsonProperty(PropertyName = "lastBlockSyncedHeight", NullValueHandling = NullValueHandling.Ignore)]
public int? LastBlockSyncedHeight { get; set; }
/// <summary>
/// The accounts used in the wallet.
/// </summary>
[JsonProperty(PropertyName = "accounts")]
public ICollection<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>
/// Gets the account matching the name passed as a parameter.
/// </summary>
/// <param name="accountName">The name of the account to get.</param>
/// <returns></returns>
/// <exception cref="System.Exception"></exception>
public HdAccount GetAccountByName(string accountName)
{
// get the account
HdAccount account = this.Accounts.SingleOrDefault(a => a.Name == accountName);
if (account == null)
{
throw new Exception($"No account with name {accountName} could be found.");
}
return account;
}
}
/// <summary>
/// The type of coin, as specified in BIP44.
/// </summary>
/// <remarks>For more, see https://github.com/satoshilabs/slips/blob/master/slip-0044.md</remarks>
public enum CoinType
{
/// <summary>
/// Bitcoin
/// </summary>
Bitcoin = 0,
/// <summary>
/// Testnet (all coins)
/// </summary>
Testnet = 1,
/// <summary>
/// Stratis
/// </summary>
Stratis = 105
}
/// <summary>
/// An Hd account's details.
/// </summary>
public class HdAccount
{
/// <summary>
/// The index of the account.
/// </summary>
/// <remarks>
/// According to BIP44, an account at index (i) can only be created when the account
/// at index (i - 1) contains transactions.
/// </remarks>
[JsonProperty(PropertyName = "index")]
public int Index { get; set; }
/// <summary>
/// The name of this account.
/// </summary>
[JsonProperty(PropertyName = "name")]
public string Name { get; set; }
/// <summary>
/// A path to the account as defined in BIP44.
/// </summary>
[JsonProperty(PropertyName = "hdPath")]
public string HdPath { get; set; }
/// <summary>
/// An extended pub key used to generate addresses.
/// </summary>
[JsonProperty(PropertyName = "extPubKey")]
public string ExtendedPubKey { get; set; }
/// <summary>
/// Gets or sets the creation time.
/// </summary>
[JsonProperty(PropertyName = "creationTime")]
[JsonConverter(typeof(DateTimeOffsetConverter))]
public DateTimeOffset CreationTime { get; set; }
/// <summary>
/// The list of external addresses, typically used for receiving money.
/// </summary>
[JsonProperty(PropertyName = "externalAddresses")]
public ICollection<HdAddress> ExternalAddresses { get; set; }
/// <summary>
/// The list of internal addresses, typically used to receive change.
/// </summary>
[JsonProperty(PropertyName = "internalAddresses")]
public ICollection<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 GetFirstUnusedReceivingAddress()
{
return this.GetFirstUnusedAddress(false);
}
/// <summary>
/// Gets the first change address that contains no transaction.
/// </summary>
/// <returns>An unused address</returns>
public HdAddress GetFirstUnusedChangeAddress()
{
return this.GetFirstUnusedAddress(true);
}
/// <summary>
/// Gets the first receiving address that contains no transaction.
/// </summary>
/// <returns>An unused address</returns>
private HdAddress GetFirstUnusedAddress(bool isChange)
{
IEnumerable<HdAddress> addresses = isChange ? this.InternalAddresses : this.ExternalAddresses;
var unusedAddresses = addresses.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>
/// Gets the last address that contains transactions.
/// </summary>
/// <param name="isChange">Whether the address is a change (internal) address or receiving (external) address.</param>
/// <returns></returns>
public HdAddress GetLastUsedAddress(bool isChange)
{
IEnumerable<HdAddress> addresses = isChange ? this.InternalAddresses : this.ExternalAddresses;
var usedAddresses = addresses.Where(acc => acc.Transactions.Any()).ToList();
if (!usedAddresses.Any())
{
return null;
}
// gets the used address with the highest index
var index = usedAddresses.Max(a => a.Index);
return usedAddresses.Single(a => a.Index == index);
}
/// <summary>
/// Gets a collection of transactions by id.
/// </summary>
/// <param name="id">The identifier.</param>
/// <returns></returns>
public IEnumerable<TransactionData> GetTransactionsById(uint256 id)
{
var addresses = this.ExternalAddresses.Concat(this.InternalAddresses);
return addresses.SelectMany(a => a.Transactions.Where(t => t.Id == id));
}
/// <summary>
/// Gets a collection of transactions with spendable outputs.
/// </summary>
/// <returns></returns>
public IEnumerable<TransactionData> GetSpendableTransactions()
{
var addresses = this.ExternalAddresses.Concat(this.InternalAddresses);
return addresses.SelectMany(a => a.Transactions.Where(t => t.SpentInTransaction == null && t.Amount > Money.Zero));
}
/// <summary>
/// Finds the addresses in which a transaction is contained.
/// </summary>
/// <remarks>
/// Returns a collection because a transaction can be contained in a change address as well as in a receive address (as a spend).
/// </remarks>
/// <param name="predicate">A predicate by which to filter the transactions.</param>
/// <returns></returns>
public IEnumerable<HdAddress> FindAddressesForTransaction(Func<TransactionData, bool> predicate)
{
var addresses = this.ExternalAddresses.Concat(this.InternalAddresses);
return addresses.Where(a => a.Transactions.Any(predicate));
}
}
/// <summary>
/// An Hd address.
/// </summary>
public class HdAddress
{
/// <summary>
/// The index of the address.
/// </summary>
[JsonProperty(PropertyName = "index")]
public int Index { get; set; }
/// <summary>
/// The script pub key for this address.
/// </summary>
[JsonProperty(PropertyName = "scriptPubKey")]
[JsonConverter(typeof(ScriptJsonConverter))]
public Script ScriptPubKey { get; set; }
/// <summary>
/// The Base58 representation of this address.
/// </summary>
[JsonProperty(PropertyName = "address")]
public string Address { get; set; }
/// <summary>
/// A path to the address as defined in BIP44.
/// </summary>
[JsonProperty(PropertyName = "hdPath")]
public string HdPath { get; set; }
/// <summary>
/// A list detailing which blocks have been scanned for this address.
/// </summary>
[JsonIgnore]
public SortedList<int, int> BlocksScanned { get; set; }
/// <summary>
/// A list of transactions involving this address.
/// </summary>
[JsonProperty(PropertyName = "transactions")]
public ICollection<TransactionData> Transactions { get; set; }
/// <summary>
/// Determines whether this is a change address or a receive address.
/// </summary>
/// <returns>
/// <c>true</c> if it is a change address; otherwise, <c>false</c>.
/// </returns>
public bool IsChangeAddress()
{
return int.Parse(this.HdPath.Split('/')[4]) == 1;
}
}
/// <summary>
/// An object containing transaction data.
/// </summary>
public class TransactionData
{
/// <summary>
/// Transaction id.
/// </summary>
[JsonProperty(PropertyName = "id")]
[JsonConverter(typeof(UInt256JsonConverter))]
public uint256 Id { get; set; }
/// <summary>
/// The id of the transaction in which the output referenced in this transaction is spent.
/// </summary>
[JsonProperty(PropertyName = "spentIn", NullValueHandling = NullValueHandling.Ignore)]
[JsonConverter(typeof(UInt256JsonConverter))]
public uint256 SpentInTransaction { get; set; }
/// <summary>
/// The transaction amount.
/// </summary>
[JsonProperty(PropertyName = "amount")]
[JsonConverter(typeof(MoneyJsonConverter))]
public Money Amount { get; set; }
/// <summary>
/// A list of payments made out in this transaction.
/// </summary>
[JsonProperty(PropertyName = "payments", NullValueHandling = NullValueHandling.Ignore)]
public ICollection<PaymentDetails> Payments { get; set; }
/// <summary>
/// The index of this scriptPubKey in the transaction it is contained.
/// </summary>
[JsonProperty(PropertyName = "index", NullValueHandling = NullValueHandling.Ignore)]
public int? Index { get; set; }
/// <summary>
/// The height of the block including this transaction.
/// </summary>
[JsonProperty(PropertyName = "blockHeight", NullValueHandling = NullValueHandling.Ignore)]
public int? BlockHeight { get; set; }
/// <summary>
/// Gets or sets the creation time.
/// </summary>
[JsonProperty(PropertyName = "creationTime")]
[JsonConverter(typeof(DateTimeOffsetConverter))]
public DateTimeOffset CreationTime { get; set; }
/// <summary>
/// Gets or sets the Merkle proof for this transaction.
/// </summary>
[JsonProperty(PropertyName = "merkleProof", NullValueHandling = NullValueHandling.Ignore)]
public MerkleProof MerkleProof { get; set; }
/// <summary>
/// Determines whether this transaction is confirmed.
/// </summary>
public bool IsConfirmed()
{
return this.BlockHeight != null;
}
}
/// <summary>
/// An object representing a payment.
/// </summary>
public class PaymentDetails
{
/// <summary>
/// The script pub key of the destination address.
/// </summary>
[JsonProperty(PropertyName = "destinationScriptPubKey")]
[JsonConverter(typeof(ScriptJsonConverter))]
public Script DestinationScriptPubKey { get; set; }
/// <summary>
/// The Base58 representation of the destination address.
/// </summary>
[JsonProperty(PropertyName = "destinationAddress")]
public string DestinationAddress { get; set; }
/// <summary>
/// The transaction amount.
/// </summary>
[JsonProperty(PropertyName = "amount")]
[JsonConverter(typeof(MoneyJsonConverter))]
public Money Amount { get; set; }
}
/// <summary>
/// An object representing a Merkle proof
/// </summary>
public class MerkleProof
{
/// <summary>
/// Gets or sets the merkle root.
/// </summary>
[JsonProperty(PropertyName = "merkleRoot")]
[JsonConverter(typeof(UInt256JsonConverter))]
public uint256 MerkleRoot { get; set; }
/// <summary>
/// Gets or sets the merkle path.
/// </summary>
[JsonProperty(PropertyName = "merklePath", ItemConverterType = typeof(UInt256JsonConverter))]
public ICollection<uint256> MerklePath { get; set; }
}
}
\ No newline at end of file
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using Breeze.Wallet.Helpers;
using Breeze.Wallet.Models;
using Microsoft.Extensions.Logging;
using NBitcoin;
using NBitcoin.DataEncoders;
using NBitcoin.Protocol;
using Newtonsoft.Json;
using Stratis.Bitcoin.Connection;
using Transaction = NBitcoin.Transaction;
namespace Breeze.Wallet
{
/// <summary>
/// A manager providing operations on wallets.
/// </summary>
public class WalletManager : IWalletManager
{
public List<Wallet> Wallets { get; }
private const int UnusedAddressesBuffer = 20;
private const int WalletRecoveryAccountsCount = 3;
private const int WalletCreationAccountsCount = 2;
private readonly CoinType coinType;
private readonly Network network;
private readonly ConnectionManager connectionManager;
private readonly ConcurrentChain chain;
private Dictionary<Script, HdAddress> keysLookup;
private readonly ILogger logger;
/// <summary>
/// Occurs when a transaction is found.
/// </summary>
public event EventHandler<TransactionFoundEventArgs> TransactionFound;
public WalletManager(ILoggerFactory loggerFactory, ConnectionManager connectionManager, Network network, ConcurrentChain chain)
{
this.logger = loggerFactory.CreateLogger(this.GetType().FullName);
this.Wallets = new List<Wallet>();
// find wallets and load them in memory
foreach (var path in this.GetWalletFilesPaths())
{
this.Load(this.DeserializeWallet(path));
}
this.connectionManager = connectionManager;
this.network = network;
this.coinType = (CoinType)network.Consensus.CoinType;
this.chain = chain;
// load data in memory for faster lookups
this.LoadKeysLookup();
// register events
this.TransactionFound += this.OnTransactionFound;
}
/// <inheritdoc />
public Mnemonic CreateWallet(string password, string folderPath, string name, string network, string passphrase = null)
{
// for now the passphrase is set to be the password by default.
if (passphrase == null)
{
passphrase = password;
}
// generate the root seed used to generate keys from a mnemonic picked at random
// and a passphrase optionally provided by the user
Mnemonic mnemonic = new Mnemonic(Wordlist.English, WordCount.Twelve);
ExtKey extendedKey = mnemonic.DeriveExtKey(passphrase);
Network coinNetwork = WalletHelpers.GetNetwork(network);
// create a wallet file
Wallet wallet = this.GenerateWalletFile(password, folderPath, name, coinNetwork, extendedKey);
// generate multiple accounts and addresses from the get-go
for (int i = 0; i < WalletCreationAccountsCount; i++)
{
HdAccount account = CreateNewAccount(wallet, this.coinType, password);
this.CreateAddressesInAccount(account, coinNetwork, UnusedAddressesBuffer);
this.CreateAddressesInAccount(account, coinNetwork, UnusedAddressesBuffer, true);
}
// update the height of the we start syncing from
this.UpdateLastBlockSyncedHeight(wallet, this.chain.Tip.Height);
// save the changes to the file and add addresses to be tracked
this.SaveToFile(wallet);
this.Load(wallet);
this.LoadKeysLookup();
return mnemonic;
}
/// <inheritdoc />
public Wallet LoadWallet(string password, string folderPath, string name)
{
string walletFilePath = Path.Combine(folderPath, $"{name}.json");
// load the file from the local system
Wallet wallet = this.DeserializeWallet(walletFilePath);
this.Load(wallet);
return wallet;
}
/// <inheritdoc />
public Wallet RecoverWallet(string password, string folderPath, string name, string network, string mnemonic, DateTime creationTime, string passphrase = null)
{
// for now the passphrase is set to be the password by default.
if (passphrase == null)
{
passphrase = password;
}
// generate the root seed used to generate keys
ExtKey extendedKey = (new Mnemonic(mnemonic)).DeriveExtKey(passphrase);
Network coinNetwork = WalletHelpers.GetNetwork(network);
// create a wallet file
Wallet wallet = this.GenerateWalletFile(password, folderPath, name, coinNetwork, extendedKey, creationTime);
// generate multiple accounts and addresses from the get-go
for (int i = 0; i < WalletRecoveryAccountsCount; i++)
{
HdAccount account = CreateNewAccount(wallet, this.coinType, password);
this.CreateAddressesInAccount(account, coinNetwork, UnusedAddressesBuffer);
this.CreateAddressesInAccount(account, coinNetwork, UnusedAddressesBuffer, true);
}
int blockSyncStart = this.chain.GetHeightAtTime(creationTime);
this.UpdateLastBlockSyncedHeight(wallet, blockSyncStart);
// save the changes to the file and add addresses to be tracked
this.SaveToFile(wallet);
this.Load(wallet);
this.LoadKeysLookup();
return wallet;
}
/// <inheritdoc />
public HdAccount GetUnusedAccount(string walletName, CoinType coinType, string password)
{
Wallet wallet = this.GetWalletByName(walletName);
return this.GetUnusedAccount(wallet, coinType, password);
}
/// <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())
{
// gets an unused account
var firstUnusedAccount = accountsRoot.GetFirstUnusedAccount();
if (firstUnusedAccount != null)
{
return firstUnusedAccount;
}
}
// 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;
}
/// <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);
var seedExtKey = new ExtKey(privateKey, wallet.ChainCode);
var accountHdPath = $"m/44'/{(int)coinType}'/{newAccountIndex}'";
KeyPath keyPath = new KeyPath(accountHdPath);
ExtKey accountExtKey = seedExtKey.Derive(keyPath);
ExtPubKey accountExtPubKey = accountExtKey.Neuter();
var newAccount = new HdAccount
{
Index = newAccountIndex,
ExtendedPubKey = accountExtPubKey.ToString(wallet.Network),
ExternalAddresses = new List<HdAddress>(),
InternalAddresses = new List<HdAddress>(),
Name = $"account {newAccountIndex}",
HdPath = accountHdPath,
CreationTime = DateTimeOffset.Now
};
accounts.Add(newAccount);
wallet.AccountsRoot.Single(a => a.CoinType == coinType).Accounts = accounts;
return newAccount;
}
/// <inheritdoc />
public string GetUnusedAddress(string walletName, CoinType coinType, string accountName)
{
Wallet wallet = this.GetWalletByName(walletName);
// get the account
HdAccount account = wallet.AccountsRoot.Single(a => a.CoinType == coinType).GetAccountByName(accountName);
// validate address creation
if (account.ExternalAddresses.Any())
{
// check last created address contains transactions.
var firstUnusedExternalAddress = account.GetFirstUnusedReceivingAddress();
if (firstUnusedExternalAddress != null)
{
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.LoadKeysLookup();
return account.GetFirstUnusedReceivingAddress().Address;
}
/// <inheritdoc />
public IEnumerable<HdAddress> GetHistoryByCoinType(string walletName, CoinType coinType)
{
Wallet wallet = this.GetWalletByName(walletName);
return this.GetHistoryByCoinType(wallet, coinType);
}
/// <inheritdoc />
public IEnumerable<HdAddress> GetHistoryByCoinType(Wallet wallet, CoinType coinType)
{
var accounts = wallet.GetAccountsByCoinType(coinType).ToList();
foreach (var address in accounts.SelectMany(a => a.ExternalAddresses).Concat(accounts.SelectMany(a => a.InternalAddresses)))
{
if (address.Transactions.Any())
{
yield return address;
}
}
}
/// <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
BitcoinPubKeyAddress address = this.GenerateAddress(account.ExtendedPubKey, i, isChange, network);
// add address details
addresses.Add(new HdAddress
{
Index = i,
HdPath = CreateBip44Path(account.GetCoinType(), account.Index, i, isChange),
ScriptPubKey = address.ScriptPubKey,
Address = address.ToString(),
Transactions = new List<TransactionData>()
});
addressesCreated.Add(address.ToString());
}
if (isChange)
{
account.InternalAddresses = addresses;
}
else
{
account.ExternalAddresses = addresses;
}
return addressesCreated;
}
/// <inheritdoc />
public Wallet GetWallet(string walletName)
{
Wallet wallet = this.GetWalletByName(walletName);
return wallet;
}
/// <inheritdoc />
public IEnumerable<HdAccount> GetAccountsByCoinType(string walletName, CoinType coinType)
{
Wallet wallet = this.GetWalletByName(walletName);
return wallet.GetAccountsByCoinType(coinType);
}
/// <inheritdoc />
public (string hex, uint256 transactionId, Money fee) BuildTransaction(string walletName, string accountName, CoinType coinType, string password, string destinationAddress, Money amount, string feeType, bool allowUnconfirmed)
{
if (amount == Money.Zero)
{
throw new Exception($"Cannot send transaction with 0 {this.coinType}");
}
// get the wallet and the account
Wallet wallet = this.GetWalletByName(walletName);
HdAccount account = wallet.AccountsRoot.Single(a => a.CoinType == coinType).GetAccountByName(accountName);
// get a list of transactions outputs that have not been spent
IEnumerable<TransactionData> spendableTransactions = account.GetSpendableTransactions();
// get total spendable balance in the account.
var balance = spendableTransactions.Sum(t => t.Amount);
// make sure we have enough funds
if (balance < amount)
{
throw new Exception("Not enough funds.");
}
// calculate which addresses needs to be used as well as the fee to be charged
var calculationResult = this.CalculateFees(spendableTransactions, amount);
// get extended private key
var privateKey = Key.Parse(wallet.EncryptedSeed, password, wallet.Network);
var seedExtKey = new ExtKey(privateKey, wallet.ChainCode);
var signingKeys = new HashSet<ISecret>();
var coins = new List<Coin>();
foreach (var transactionToUse in calculationResult.transactionsToUse)
{
var address = account.FindAddressesForTransaction(t => t.Id == transactionToUse.Id && t.Amount > 0).Single();
ExtKey addressExtKey = seedExtKey.Derive(new KeyPath(address.HdPath));
BitcoinExtKey addressPrivateKey = addressExtKey.GetWif(wallet.Network);
signingKeys.Add(addressPrivateKey);
coins.Add(new Coin(transactionToUse.Id, (uint)transactionToUse.Index, transactionToUse.Amount, address.ScriptPubKey));
}
// get address to send the change to
var changeAddress = account.GetFirstUnusedChangeAddress();
// get script destination address
Script destinationScript = PayToPubkeyHashTemplate.Instance.GenerateScriptPubKey(new BitcoinPubKeyAddress(destinationAddress, wallet.Network));
// build transaction
var builder = new TransactionBuilder();
Transaction tx = builder
.AddCoins(coins)
.AddKeys(signingKeys.ToArray())
.Send(destinationScript, amount)
.SetChange(changeAddress.ScriptPubKey)
.SendFees(calculationResult.fee)
.BuildTransaction(true);
if (!builder.Verify(tx))
{
throw new Exception("Could not build transaction, please make sure you entered the correct data.");
}
return (tx.ToHex(), tx.GetHash(), calculationResult.fee);
}
/// <summary>
/// Calculates which outputs are to be used in the transaction, as well as the fees that will be charged.
/// </summary>
/// <param name="spendableTransactions">The transactions with unspent funds.</param>
/// <param name="amount">The amount to be sent.</param>
/// <returns>The collection of transactions to be used and the fee to be charged</returns>
private (List<TransactionData> transactionsToUse, Money fee) CalculateFees(IEnumerable<TransactionData> spendableTransactions, Money amount)
{
// TODO make this a bit smarter!
List<TransactionData> transactionsToUse = new List<TransactionData>();
foreach (var transaction in spendableTransactions)
{
transactionsToUse.Add(transaction);
if (transactionsToUse.Sum(t => t.Amount) >= amount)
{
break;
}
}
Money fee = new Money(new decimal(0.001), MoneyUnit.BTC);
return (transactionsToUse, fee);
}
/// <inheritdoc />
public bool SendTransaction(string transactionHex)
{
// TODO move this to a behavior on the full node
// parse transaction
Transaction transaction = Transaction.Parse(transactionHex);
TxPayload payload = new TxPayload(transaction);
foreach (var node in this.connectionManager.ConnectedNodes)
{
node.SendMessage(payload);
}
return true;
}
/// <inheritdoc />
public void ProcessBlock(int height, Block block)
{
this.logger.LogDebug($"block notification - height: {height}, hash: {block.Header.GetHash()}, coin: {this.coinType}");
foreach (Transaction transaction in block.Transactions)
{
this.ProcessTransaction(transaction, height, block);
}
// update the wallets with the last processed block height
this.UpdateLastBlockSyncedHeight(height);
}
/// <inheritdoc />
public void ProcessTransaction(Transaction transaction, int? blockHeight = null, Block block = null)
{
this.logger.LogDebug($"transaction received - hash: {transaction.GetHash()}, coin: {this.coinType}");
// check the outputs
foreach (var pubKey in this.keysLookup.Keys)
{
// check if the outputs contain one of our addresses
var utxo = transaction.Outputs.SingleOrDefault(o => pubKey == o.ScriptPubKey);
if (utxo != null)
{
AddTransactionToWallet(transaction.GetHash(), transaction.Time, transaction.Outputs.IndexOf(utxo), utxo.Value, pubKey, blockHeight, block);
}
}
// check the inputs - include those that have a reference to a transaction containing one of our scripts and the same index
foreach (TxIn input in transaction.Inputs.Where(txIn => this.keysLookup.Values.SelectMany(v => v.Transactions).Any(trackedTx => trackedTx.Id == txIn.PrevOut.Hash && trackedTx.Index == txIn.PrevOut.N)))
{
TransactionData tTx = this.keysLookup.Values.SelectMany(v => v.Transactions).Single(trackedTx => trackedTx.Id == input.PrevOut.Hash && trackedTx.Index == input.PrevOut.N);
// find the script this input references
var keyToSpend = this.keysLookup.Single(v => v.Value.Transactions.Contains(tTx)).Key;
// get the details of the outputs paid out.
// We first include the keys we don't hold and then we include the keys we do hold but that are for receiving addresses (which would mean the user paid itself).
IEnumerable<TxOut> paidoutto = transaction.Outputs.Where(o => !this.keysLookup.Keys.Contains(o.ScriptPubKey) || (this.keysLookup.ContainsKey(o.ScriptPubKey) && !this.keysLookup[o.ScriptPubKey].IsChangeAddress()));
AddTransactionToWallet(transaction.GetHash(), transaction.Time, null, -tTx.Amount, keyToSpend, blockHeight, block, tTx.Id, tTx.Index, paidoutto);
}
}
/// <summary>
/// Adds the transaction to the wallet.
/// </summary>
/// <param name="transactionHash">The transaction hash.</param>
/// <param name="time">The time.</param>
/// <param name="index">The index.</param>
/// <param name="amount">The amount.</param>
/// <param name="script">The script.</param>
/// <param name="blockHeight">Height of the block.</param>
/// <param name="block">The block containing the transaction to add.</param>
/// <param name="spendingTransactionId">The id of the transaction containing the output being spent, if this is a spending transaction.</param>
/// <param name="spendingTransactionIndex">The index of the output in the transaction being referenced, if this is a spending transaction.</param>
private void AddTransactionToWallet(uint256 transactionHash, uint time, int? index, Money amount, Script script, int? blockHeight = null, Block block = null, uint256 spendingTransactionId = null, int? spendingTransactionIndex = null, IEnumerable<TxOut> paidToOutputs = null)
{
// get the collection of transactions to add to.
this.keysLookup.TryGetValue(script, out HdAddress address);
var isSpendingTransaction = paidToOutputs != null && paidToOutputs.Any();
var trans = address.Transactions;
// if it's the first time we see this transaction
if (trans.All(t => t.Id != transactionHash))
{
var newTransaction = new TransactionData
{
Amount = amount,
BlockHeight = blockHeight,
Id = transactionHash,
CreationTime = DateTimeOffset.FromUnixTimeSeconds(block?.Header.Time ?? time),
Index = index
};
// add the Merkle proof to the (non-spending) transaction
if (block != null && !isSpendingTransaction)
{
newTransaction.MerkleProof = this.CreateMerkleProof(block, transactionHash);
}
// if this is a spending transaction, keep a record of the payments made out to other scripts.
if (isSpendingTransaction)
{
List<PaymentDetails> payments = new List<PaymentDetails>();
foreach (var paidToOutput in paidToOutputs)
{
payments.Add(new PaymentDetails
{
DestinationScriptPubKey = paidToOutput.ScriptPubKey,
DestinationAddress = paidToOutput.ScriptPubKey.GetDestinationAddress(this.network).ToString(),
Amount = paidToOutput.Value
});
}
newTransaction.Payments = payments;
// mark the transaction spent by this transaction as such
var transactions = this.keysLookup.Values.SelectMany(v => v.Transactions).Where(t => t.Id == spendingTransactionId);
if (transactions.Any())
{
var spentTransaction = transactions.Single(t => t.Index == spendingTransactionIndex);
spentTransaction.SpentInTransaction = transactionHash;
spentTransaction.MerkleProof = null;
}
}
trans.Add(newTransaction);
}
else if (trans.Any(t => t.Id == transactionHash)) // if this is an unconfirmed transaction now received in a block
{
var foundTransaction = trans.Single(t => t.Id == transactionHash);
// update the block height
if (foundTransaction.BlockHeight == null && blockHeight != null)
{
foundTransaction.BlockHeight = blockHeight;
}
// update the block time
if (block != null)
{
foundTransaction.CreationTime = DateTimeOffset.FromUnixTimeSeconds(block.Header.Time);
}
// add the Merkle proof now that the transaction is confirmed in a block
if (!isSpendingTransaction && foundTransaction.MerkleProof == null)
{
foundTransaction.MerkleProof = this.CreateMerkleProof(block, transactionHash);
}
}
// notify a transaction has been found
this.TransactionFound?.Invoke(this, new TransactionFoundEventArgs(script, transactionHash));
}
private MerkleProof CreateMerkleProof(Block block, uint256 transactionHash)
{
MerkleBlock merkleBlock = new MerkleBlock(block, new[] { transactionHash });
return new MerkleProof
{
MerkleRoot = block.Header.HashMerkleRoot,
MerklePath = merkleBlock.PartialMerkleTree.Hashes
};
}
private void OnTransactionFound(object sender, TransactionFoundEventArgs a)
{
foreach (Wallet wallet in this.Wallets)
{
foreach (var account in wallet.GetAccountsByCoinType(this.coinType))
{
bool isChange;
if (account.ExternalAddresses.Any(address => address.ScriptPubKey == a.Script))
{
isChange = false;
}
else if (account.InternalAddresses.Any(address => address.ScriptPubKey == a.Script))
{
isChange = true;
}
else
{
continue;
}
// calculate how many accounts to add to keep a buffer of 20 unused addresses
int lastUsedAddressIndex = account.GetLastUsedAddress(isChange).Index;
int addressesCount = isChange ? account.InternalAddresses.Count() : account.ExternalAddresses.Count();
int emptyAddressesCount = addressesCount - lastUsedAddressIndex - 1;
int accountsToAdd = UnusedAddressesBuffer - emptyAddressesCount;
this.CreateAddressesInAccount(account, wallet.Network, accountsToAdd, isChange);
// persists the address to the wallet file
this.SaveToFile(wallet);
}
}
this.LoadKeysLookup();
}
/// <inheritdoc />
public void DeleteWallet(string walletFilePath)
{
File.Delete(walletFilePath);
}
/// <inheritdoc />
public void SaveToFile(Wallet wallet)
{
File.WriteAllText(wallet.WalletFilePath, JsonConvert.SerializeObject(wallet, Formatting.Indented));
}
/// <inheritdoc />
public void UpdateLastBlockSyncedHeight(int height)
{
// update the wallets with the last processed block height
foreach (var wallet in this.Wallets)
{
this.UpdateLastBlockSyncedHeight(wallet, height);
}
}
/// <inheritdoc />
public void UpdateLastBlockSyncedHeight(Wallet wallet, int height)
{
// update the wallets with the last processed block height
foreach (var accountRoot in wallet.AccountsRoot.Where(a => a.CoinType == this.coinType))
{
accountRoot.LastBlockSyncedHeight = height;
}
}
/// <inheritdoc />
public void SaveToFile()
{
foreach (var wallet in this.Wallets)
{
this.SaveToFile(wallet);
}
}
/// <inheritdoc />
public void Dispose()
{
// safely persist the wallets to the file system before disposing
foreach (var wallet in this.Wallets)
{
this.SaveToFile(wallet);
}
}
/// <summary>
/// Generates the wallet file.
/// </summary>
/// <param name="password">The password used to encrypt sensitive info.</param>
/// <param name="folderPath">The folder where the wallet will be generated.</param>
/// <param name="name">The name of the wallet.</param>
/// <param name="network">The network this wallet is for.</param>
/// <param name="extendedKey">The root key used to generate keys.</param>
/// <param name="creationTime">The time this wallet was created.</param>
/// <returns></returns>
/// <exception cref="System.NotSupportedException"></exception>
private Wallet GenerateWalletFile(string password, string folderPath, string name, Network network, ExtKey extendedKey, DateTimeOffset? creationTime = null)
{
string walletFilePath = Path.Combine(folderPath, $"{name}.json");
if (File.Exists(walletFilePath))
throw new InvalidOperationException($"Wallet already exists at {walletFilePath}");
Wallet walletFile = new Wallet
{
Name = name,
EncryptedSeed = extendedKey.PrivateKey.GetEncryptedBitcoinSecret(password, network).ToWif(),
ChainCode = extendedKey.ChainCode,
CreationTime = creationTime ?? DateTimeOffset.Now,
Network = network,
AccountsRoot = new List<AccountRoot> {
new AccountRoot { Accounts = new List<HdAccount>(), CoinType = CoinType.Bitcoin },
new AccountRoot { Accounts = new List<HdAccount>(), CoinType = CoinType.Testnet },
new AccountRoot { Accounts = new List<HdAccount>(), CoinType = CoinType.Stratis} },
WalletFilePath = walletFilePath,
};
// create a folder if none exists and persist the file
Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(walletFilePath)));
File.WriteAllText(walletFilePath, JsonConvert.SerializeObject(walletFile, Formatting.Indented));
return walletFile;
}
/// <summary>
/// Gets the wallet located at the specified path.
/// </summary>
/// <param name="walletFilePath">The wallet file path.</param>
/// <returns></returns>
/// <exception cref="System.IO.FileNotFoundException"></exception>
private Wallet DeserializeWallet(string walletFilePath)
{
if (!File.Exists(walletFilePath))
throw new FileNotFoundException($"No wallet file found at {walletFilePath}");
// load the file from the local system
return JsonConvert.DeserializeObject<Wallet>(File.ReadAllText(walletFilePath));
}
/// <summary>
/// Loads the wallet to be used by the manager.
/// </summary>
/// <param name="wallet">The wallet to load.</param>
private void Load(Wallet wallet)
{
if (this.Wallets.Any(w => w.Name == wallet.Name))
{
return;
}
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);
}
private IEnumerable<string> GetWalletFilesPaths()
{
// TODO look in user-chosen folder as well.
// maybe the api can maintain a list of wallet paths it knows about
var defaultFolderPath = GetDefaultWalletFolderPath();
// create the directory if it doesn't exist
Directory.CreateDirectory(defaultFolderPath);
return Directory.EnumerateFiles(defaultFolderPath, "*.json", SearchOption.TopDirectoryOnly);
}
/// <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}";
}
/// <summary>
/// Gets the path of the default folder in which the wallets will be stored.
/// </summary>
/// <returns>The folder path for Windows, Linux or OSX systems.</returns>
public static string GetDefaultWalletFolderPath()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return $@"{Environment.GetEnvironmentVariable("AppData")}\Breeze";
}
return $"{Environment.GetEnvironmentVariable("HOME")}/.breeze";
}
/// <summary>
/// Loads the keys and transactions we're tracking in memory for faster lookups.
/// </summary>
/// <returns></returns>
private void LoadKeysLookup()
{
this.keysLookup = new Dictionary<Script, HdAddress>();
foreach (var wallet in this.Wallets)
{
var accounts = wallet.GetAccountsByCoinType(this.coinType);
foreach (var account in accounts)
{
var addresses = account.ExternalAddresses.Concat(account.InternalAddresses);
foreach (var address in addresses)
{
this.keysLookup.Add(address.ScriptPubKey, address);
}
}
}
}
/// <summary>
/// Gets a wallet given its name.
/// </summary>
/// <param name="walletName">The name of the wallet to get.</param>
/// <returns>A wallet or null if it doesn't exist</returns>
private Wallet GetWalletByName(string walletName)
{
Wallet wallet = this.Wallets.SingleOrDefault(w => w.Name == walletName);
if (wallet == null)
{
throw new Exception($"No wallet with name {walletName} could be found.");
}
return wallet;
}
}
public class TransactionDetails
{
public uint256 Hash { get; set; }
public int? Index { get; set; }
public Money Amount { get; internal set; }
}
public class TransactionFoundEventArgs : EventArgs
{
public Script Script { get; set; }
public uint256 TransactionHash { get; set; }
public TransactionFoundEventArgs(Script script, uint256 transactionHash)
{
this.Script = script;
this.TransactionHash = transactionHash;
}
}
}
\ No newline at end of file
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