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
# Visual Studio 15
VisualStudioVersion = 15.0.26430.6
VisualStudioVersion = 15.0.26430.12
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{807563C4-7259-434D-B604-A14C3DCF8E30}"
EndProject
......@@ -17,16 +17,18 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Breeze.Api.Tests", "src\Bre
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Breeze.Wallet", "src\Breeze.Wallet\Breeze.Wallet.csproj", "{D16CD478-9D1E-4C69-91AD-43539E94A215}"
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}"
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}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NTumbleBit", "src\NTumbleBit\NTumbleBit.csproj", "{29E411B1-5687-43EE-A71B-6CCEC2289129}"
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
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
......@@ -45,22 +47,26 @@ Global
{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.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.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.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.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.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
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
......@@ -69,9 +75,8 @@ Global
{E7B3E9EB-34E8-4B10-B296-4D5270E314A4} = {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}
{1B598E33-667F-496D-BC0D-88276E8E7632} = {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}
{3F0937A2-3182-42D9-866F-3DEDEE28EC5A} = {807563C4-7259-434D-B604-A14C3DCF8E30}
EndGlobalSection
EndGlobal
......@@ -10,6 +10,7 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\StratisBitcoinFullNode\Stratis.Bitcoin\Stratis.Bitcoin.csproj" />
<ProjectReference Include="..\Breeze.Api\Breeze.Api.csproj" />
<ProjectReference Include="..\Breeze.Wallet\Breeze.Wallet.csproj" />
</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 @@
<PackageReference Include="Microsoft.Extensions.Logging.Console" 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="Stratis.Bitcoin" Version="1.0.1.8-alpha" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="1.0.0" />
<PackageReference Include="System.Reactive" Version="3.1.1" />
<PackageReference Include="System.Runtime.Loader" Version="4.3.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\StratisBitcoinFullNode\Stratis.Bitcoin\Stratis.Bitcoin.csproj" />
</ItemGroup>
</Project>
......@@ -33,7 +33,7 @@ namespace Breeze.Api
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();
}
}
......
<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 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\StratisBitcoinFullNode\Stratis.Bitcoin\Stratis.Bitcoin.csproj" />
<ProjectReference Include="..\Breeze.Api\Breeze.Api.csproj" />
<ProjectReference Include="..\Breeze.TumbleBit.Client\Breeze.TumbleBit.Client.csproj" />
<ProjectReference Include="..\Breeze.Wallet\Breeze.Wallet.csproj" />
......@@ -21,7 +22,6 @@
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" 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>
</Project>
......@@ -10,6 +10,7 @@ using Stratis.Bitcoin.Configuration;
using Stratis.Bitcoin.Logging;
using Breeze.Wallet;
using Stratis.Bitcoin.Notifications;
using Stratis.Bitcoin.Utilities;
namespace Breeze.Daemon
{
......@@ -17,13 +18,13 @@ namespace Breeze.Daemon
{
public static void Main(string[] args)
{
// configure Full Node
Logs.Configure(new LoggerFactory().AddConsole(LogLevel.Trace, false));
// configure Full Node
Logs.Configure(Logs.GetLoggerFactory(args));
NodeSettings nodeSettings = NodeSettings.FromArguments(args);
var fullNodeBuilder = new FullNodeBuilder()
.UseNodeSettings(nodeSettings)
.UseWallet()
.UseLightWallet()
.UseBlockNotification()
.UseTransactionNotification()
.UseApi();
......@@ -36,13 +37,10 @@ namespace Breeze.Daemon
fullNodeBuilder.UseTumbleBit(new Uri(tumblerAddress));
}
var node = (FullNode)fullNodeBuilder.Build();
// start Full Node - this will also start the API
node.Start();
Console.WriteLine("Press any key to stop");
Console.ReadLine();
node.Dispose();
var node = fullNodeBuilder.Build();
// start Full Node - this will also start the API
node.Run();
}
}
}
......@@ -21,11 +21,11 @@
<PackageReference Include="Microsoft.AspNetCore.Mvc.Core" Version="1.1.3" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Versioning" Version="1.0.3" />
<PackageReference Include="Serilog.Extensions.Logging.File" Version="1.0.1" />
<PackageReference Include="Stratis.Bitcoin" Version="1.0.1.8-alpha" />
</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" />
</ItemGroup>
......
......@@ -2,9 +2,9 @@
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using Breeze.Common.JsonErrors;
using Microsoft.AspNetCore.Mvc;
using Breeze.TumbleBit.Client;
using Stratis.Bitcoin.Common.JsonErrors;
namespace Breeze.TumbleBit.Controllers
{
......
......@@ -20,11 +20,10 @@
<PackageReference Include="Microsoft.AspNetCore.Mvc.Versioning" Version="1.0.3" />
<PackageReference Include="Serilog.Extensions.Logging.File" Version="1.0.1" />
<PackageReference Include="System.ValueTuple" Version="4.3.1" />
<PackageReference Include="Stratis.Bitcoin" Version="1.0.1.8-alpha" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Breeze.Common\Breeze.Common.csproj" />
<ProjectReference Include="..\..\..\..\StratisBitcoinFullNode\Stratis.Bitcoin\Stratis.Bitcoin.csproj" />
</ItemGroup>
</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 Breeze.Wallet.Controllers;
using Microsoft.Extensions.DependencyInjection;
using Stratis.Bitcoin.Builder;
using Stratis.Bitcoin.Logging;
using Microsoft.Extensions.Logging;
using Serilog;
using Stratis.Bitcoin.Wallet;
namespace Breeze.Wallet
{
public class WalletFeature : FullNodeFeature
public class LightWalletFeature : FullNodeFeature
{
private readonly ITracker tracker;
private readonly IWalletManager walletManager;
private readonly TrackNotifier trackNotifier;
public WalletFeature(ITracker tracker, IWalletManager walletManager)
{
this.tracker = tracker;
this.walletManager = walletManager;
}
public LightWalletFeature(TrackNotifier trackNotifier)
{
this.trackNotifier = trackNotifier;
}
public override void Start()
{
this.tracker.Initialize();
this.trackNotifier.Initialize().GetAwaiter().GetResult();
}
public override void Stop()
{
this.walletManager.Dispose();
base.Stop();
}
}
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 =>
{
features
.AddFeature<WalletFeature>()
.AddFeature<LightWalletFeature>()
.FeatureServices(services =>
{
var loggerFactory = Logs.LoggerFactory;
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>();
services.AddSingleton<TrackNotifier>();
});
});
......
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;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Breeze.Wallet.Notifications;
using Microsoft.Extensions.Logging;
using NBitcoin;
using Stratis.Bitcoin;
using Stratis.Bitcoin.Notifications;
using Stratis.Bitcoin.Utilities;
using Stratis.Bitcoin.Wallet;
using Stratis.Bitcoin.Wallet.Notifications;
namespace Breeze.Wallet
{
public class Tracker : ITracker
public class TrackNotifier
{
private readonly WalletManager walletManager;
private readonly ConcurrentChain chain;
private readonly Signals signals;
private readonly BlockNotification blockNotification;
private readonly CoinType coinType;
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.chain = chain;
this.signals = signals;
this.blockNotification = blockNotification;
this.coinType = (CoinType)network.Consensus.CoinType;
this.logger = loggerFactory.CreateLogger(this.GetType().FullName);
}
/// <inheritdoc />
public async Task Initialize()
public Task Initialize()
{
// get the chain headers. This needs to be up-to-date before we really do anything
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();
//await this.WaitForChainDownloadAsync();
// start syncing blocks
var bestHeightForSyncing = this.FindBestHeightForSyncing();
this.SyncFrom(bestHeightForSyncing);
this.logger.LogInformation($"Tracker initialized. Syncing from {bestHeightForSyncing}.");
return Task.CompletedTask;
}
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