Init commit from ElementExplorer

This commit is contained in:
NicolasDorier 2017-08-17 16:49:14 +09:00
parent a9490f3566
commit 5ba0d16d76
30 changed files with 5884 additions and 5 deletions

View File

@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.0.0" />
<PackageReference Include="xunit" Version="2.2.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.3.0-beta3-build3705" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ElementsExplorer\ElementsExplorer.csproj" />
<ProjectReference Include="..\NElements.TestFramework\NBitcoin.TestFramework.csproj" />
<ProjectReference Include="..\NElements\NBitcoin.NETCore\NBitcoin.NETCore.csproj" />
</ItemGroup>
<ItemGroup>
<Service Include="{82a7f48d-3b50-4b1e-b82e-3ada8210c358}" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,44 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Text;
namespace ElementsExplorer.Tests
{
public class RepositoryTester : IDisposable
{
public static RepositoryTester Create(bool caching, [CallerMemberName]string name = null)
{
return new RepositoryTester(name, caching);
}
string _Name;
RepositoryTester(string name, bool caching)
{
_Name = name;
ServerTester.DeleteRecursivelyWithMagicDust(name);
_Repository = new Repository(name, caching);
}
public void Dispose()
{
_Repository.Dispose();
ServerTester.DeleteRecursivelyWithMagicDust(_Name);
}
public void ReloadRepository(bool caching)
{
_Repository.Dispose();
_Repository = new Repository(_Name, caching);
}
private Repository _Repository;
public Repository Repository
{
get
{
return _Repository;
}
}
}
}

View File

@ -0,0 +1,299 @@
using System.Linq;
using ElementsExplorer.Configuration;
using Microsoft.AspNetCore.Hosting;
using NBitcoin;
using NBitcoin.Tests;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Runtime.CompilerServices;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Server.Kestrel;
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Hosting.Server.Features;
namespace ElementsExplorer.Tests
{
public class ServerTester : IDisposable
{
private readonly string _Directory;
public static ServerTester Create([CallerMemberNameAttribute]string caller = null)
{
return new ServerTester(caller);
}
public void Dispose()
{
if(Host != null)
{
Host.Dispose();
Host = null;
}
if(Runtime != null)
{
Runtime.Dispose();
Runtime = null;
}
if(NodeBuilder != null)
{
NodeBuilder.Dispose();
NodeBuilder = null;
}
}
public ServerTester(string directory)
{
try
{
var rootTestData = "TestData";
var cachedNodes = "TestData/CachedNodes";
directory = rootTestData + "/" + directory;
_Directory = directory;
if(!Directory.Exists(rootTestData))
Directory.CreateDirectory(rootTestData);
if(!Directory.Exists(cachedNodes))
{
Directory.CreateDirectory(cachedNodes);
RunScenario(cachedNodes);
}
if(!TryDelete(directory, false))
{
foreach(var process in Process.GetProcessesByName("elementd"))
{
if(process.MainModule.FileName.Replace("\\", "/").StartsWith(Path.GetFullPath(rootTestData).Replace("\\", "/"), StringComparison.Ordinal))
{
process.Kill();
process.WaitForExit();
}
}
TryDelete(directory, true);
}
NodeBuilder = NodeBuilder.Create(directory);
NodeBuilder.CleanBeforeStartingNode = false;
Copy(cachedNodes, directory);
User1 = NodeBuilder.CreateNode();
User2 = NodeBuilder.CreateNode();
Explorer = NodeBuilder.CreateNode();
NodeBuilder.StartAll();
var creds = ExtractCredentials(File.ReadAllText(Explorer.Config));
var conf = new ExplorerConfiguration();
conf.DataDir = Path.Combine(directory, "explorer");
conf.Network = Network.RegTest;
conf.RPC = new RPCArgs()
{
User = creds.Item1,
Password = creds.Item2,
Url = Explorer.CreateRPCClient().Address,
NoTest = true
};
conf.NodeEndpoint = Explorer.Endpoint;
conf.Network = ExplorerConfiguration.CreateNetwork(conf.Network, Explorer.CreateRPCClient().GetBlock(0));
Runtime = conf.CreateRuntime();
Runtime.Repository.SetIndexProgress(new BlockLocator() { Blocks = { Runtime.RPC.GetBestBlockHash() } });
Runtime.StartNodeListener(conf.StartHeight);
Host = Runtime.CreateWebHost();
Host.Start();
}
catch
{
Dispose();
throw;
}
}
private void RunScenario(string directory)
{
NodeBuilder = NodeBuilder.Create(directory);
User1 = NodeBuilder.CreateNode();
User2 = NodeBuilder.CreateNode();
Explorer = NodeBuilder.CreateNode();
NodeBuilder.StartAll();
User1.CreateRPCClient().Generate(1);
Explorer.CreateRPCClient().Generate(1);
User2.CreateRPCClient().Generate(101);
User1.Sync(User2, true);
Explorer.Sync(User1, true);
var a = User1.CreateRPCClient().GetBlockCount();
var b = User1.CreateRPCClient().GetBlockCount();
var c = User1.CreateRPCClient().GetBlockCount();
Task.WaitAll(new Task[]
{
User1.CreateRPCClient().SendCommandAsync("stop"),
User2.CreateRPCClient().SendCommandAsync("stop"),
Explorer.CreateRPCClient().SendCommandAsync("stop")
}.ToArray());
User1.WaitForExit();
User2.WaitForExit();
Explorer.WaitForExit();
NodeBuilder = null;
}
private Tuple<string, string> ExtractCredentials(string config)
{
var user = Regex.Match(config, "rpcuser=([^\r\n]*)");
var pass = Regex.Match(config, "rpcpassword=([^\r\n]*)");
return Tuple.Create(user.Groups[1].Value, pass.Groups[1].Value);
}
public Uri Address
{
get
{
var address = ((KestrelServer)(Host.Services.GetService(typeof(IServer)))).Features.Get<IServerAddressesFeature>().Addresses.FirstOrDefault();
return new Uri(address);
}
}
ExplorerClient _Client;
public ExplorerClient Client
{
get
{
return _Client = _Client ?? new ExplorerClient(Runtime.Network, Address);
}
}
public CoreNode Explorer
{
get; set;
}
public CoreNode User1
{
get; set;
}
public CoreNode User2
{
get; set;
}
public NodeBuilder NodeBuilder
{
get; set;
}
public ExplorerRuntime Runtime
{
get; set;
}
public IWebHost Host
{
get; set;
}
public string BaseDirectory
{
get
{
return _Directory;
}
}
private static bool TryDelete(string directory, bool throws)
{
try
{
DeleteRecursivelyWithMagicDust(directory);
return true;
}
catch(DirectoryNotFoundException)
{
return true;
}
catch(Exception)
{
if(throws)
throw;
}
return false;
}
// http://stackoverflow.com/a/14933880/2061103
public static void DeleteRecursivelyWithMagicDust(string destinationDir)
{
const int magicDust = 10;
for(var gnomes = 1; gnomes <= magicDust; gnomes++)
{
try
{
Directory.Delete(destinationDir, true);
}
catch(DirectoryNotFoundException)
{
return; // good!
}
catch(IOException)
{
if(gnomes == magicDust)
throw;
// System.IO.IOException: The directory is not empty
System.Diagnostics.Debug.WriteLine("Gnomes prevent deletion of {0}! Applying magic dust, attempt #{1}.", destinationDir, gnomes);
// see http://stackoverflow.com/questions/329355/cannot-delete-directory-with-directory-deletepath-true for more magic
Thread.Sleep(100);
continue;
}
catch(UnauthorizedAccessException)
{
if(gnomes == magicDust)
throw;
// Wait, maybe another software make us authorized a little later
System.Diagnostics.Debug.WriteLine("Gnomes prevent deletion of {0}! Applying magic dust, attempt #{1}.", destinationDir, gnomes);
// see http://stackoverflow.com/questions/329355/cannot-delete-directory-with-directory-deletepath-true for more magic
Thread.Sleep(100);
continue;
}
return;
}
// depending on your use case, consider throwing an exception here
}
static void Copy(string sourceDirectory, string targetDirectory)
{
DirectoryInfo diSource = new DirectoryInfo(sourceDirectory);
DirectoryInfo diTarget = new DirectoryInfo(targetDirectory);
CopyAll(diSource, diTarget);
}
static void CopyAll(DirectoryInfo source, DirectoryInfo target)
{
Directory.CreateDirectory(target.FullName);
// Copy each file into the new directory.
foreach(FileInfo fi in source.GetFiles())
{
Console.WriteLine(@"Copying {0}\{1}", target.FullName, fi.Name);
fi.CopyTo(Path.Combine(target.FullName, fi.Name), true);
}
// Copy each subdirectory using recursion.
foreach(DirectoryInfo diSourceSubDir in source.GetDirectories())
{
DirectoryInfo nextTargetSubDir =
target.CreateSubdirectory(diSourceSubDir.Name);
CopyAll(diSourceSubDir, nextTargetSubDir);
}
}
}
}

View File

@ -0,0 +1,46 @@
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Text;
using Xunit.Abstractions;
namespace ElementsExplorer.Tests
{
public class TestOutputHelperFactory : ILoggerFactory, ILogger
{
public void AddProvider(ILoggerProvider provider)
{
}
ITestOutputHelper _Out;
public TestOutputHelperFactory(ITestOutputHelper output)
{
_Out = output;
}
public IDisposable BeginScope<TState>(TState state)
{
return this;
}
public ILogger CreateLogger(string categoryName)
{
return this;
}
public void Dispose()
{
}
public bool IsEnabled(LogLevel logLevel)
{
return true;
}
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
{
_Out.WriteLine(formatter(state, exception));
}
}
}

File diff suppressed because one or more lines are too long

40
ElementsExplorer.sln Normal file
View File

@ -0,0 +1,40 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.26430.6
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ElementsExplorer", "ElementsExplorer\ElementsExplorer.csproj", "{9C18B7C4-8FA8-4DBC-8E11-2C7C302D4CD9}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ElementsExplorer.Tests", "ElementsExplorer.Tests\ElementsExplorer.Tests.csproj", "{F1F294B4-C7B2-439E-90B6-9A90815B0D51}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NBitcoin.NETCore", "NElements\NBitcoin.NETCore\NBitcoin.NETCore.csproj", "{A1E918F1-4F8E-4833-95E4-8EBC45FAA734}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NBitcoin.TestFramework", "NElements.TestFramework\NBitcoin.TestFramework.csproj", "{74B03BA5-1FCA-4906-887C-3E9559394619}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{9C18B7C4-8FA8-4DBC-8E11-2C7C302D4CD9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9C18B7C4-8FA8-4DBC-8E11-2C7C302D4CD9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9C18B7C4-8FA8-4DBC-8E11-2C7C302D4CD9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9C18B7C4-8FA8-4DBC-8E11-2C7C302D4CD9}.Release|Any CPU.Build.0 = Release|Any CPU
{F1F294B4-C7B2-439E-90B6-9A90815B0D51}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F1F294B4-C7B2-439E-90B6-9A90815B0D51}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F1F294B4-C7B2-439E-90B6-9A90815B0D51}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F1F294B4-C7B2-439E-90B6-9A90815B0D51}.Release|Any CPU.Build.0 = Release|Any CPU
{A1E918F1-4F8E-4833-95E4-8EBC45FAA734}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A1E918F1-4F8E-4833-95E4-8EBC45FAA734}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A1E918F1-4F8E-4833-95E4-8EBC45FAA734}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A1E918F1-4F8E-4833-95E4-8EBC45FAA734}.Release|Any CPU.Build.0 = Release|Any CPU
{74B03BA5-1FCA-4906-887C-3E9559394619}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{74B03BA5-1FCA-4906-887C-3E9559394619}.Debug|Any CPU.Build.0 = Debug|Any CPU
{74B03BA5-1FCA-4906-887C-3E9559394619}.Release|Any CPU.ActiveCfg = Release|Any CPU
{74B03BA5-1FCA-4906-887C-3E9559394619}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal

View File

@ -0,0 +1,51 @@
using NBitcoin;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using System.IO;
using ElementsExplorer.Logging;
namespace ElementsExplorer.Configuration
{
public class DefaultDataDirectory
{
public static string GetDefaultDirectory(string appName, Network network)
{
string directory = null;
var home = Environment.GetEnvironmentVariable("HOME");
if(!string.IsNullOrEmpty(home))
{
Logs.Configuration.LogInformation("Using HOME environment variable for initializing application data");
directory = home;
directory = Path.Combine(directory, "." + appName.ToLowerInvariant());
}
else
{
var localAppData = Environment.GetEnvironmentVariable("APPDATA");
if(!string.IsNullOrEmpty(localAppData))
{
Logs.Configuration.LogInformation("Using APPDATA environment variable for initializing application data");
directory = localAppData;
directory = Path.Combine(directory, appName);
}
else
{
throw new DirectoryNotFoundException("Could not find suitable datadir");
}
}
if(!Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
directory = Path.Combine(directory, network?.Name ?? "elements");
if(!Directory.Exists(directory))
{
Logs.Configuration.LogInformation("Creating data directory");
Directory.CreateDirectory(directory);
}
return directory;
}
}
}

View File

@ -0,0 +1,229 @@
using System;
using Microsoft.Extensions.Logging;
using System.Linq;
using System.Collections.Generic;
using System.Text;
using NBitcoin;
using System.IO;
using System.Net;
using ElementsExplorer.Logging;
using NBitcoin.Protocol;
using NBitcoin.DataEncoders;
using NBitcoin.RPC;
namespace ElementsExplorer.Configuration
{
public class ExplorerConfiguration
{
public string ConfigurationFile
{
get;
set;
}
public string DataDir
{
get;
set;
}
public Network Network
{
get; set;
}
public List<IPEndPoint> Listen
{
get;
set;
} = new List<IPEndPoint>();
public RPCArgs RPC
{
get;
set;
}
public bool Rescan
{
get; set;
}
public ExplorerConfiguration LoadArgs(String[] args)
{
ConfigurationFile = args.Where(a => a.StartsWith("-conf=", StringComparison.Ordinal)).Select(a => a.Substring("-conf=".Length).Replace("\"", "")).FirstOrDefault();
DataDir = args.Where(a => a.StartsWith("-datadir=", StringComparison.Ordinal)).Select(a => a.Substring("-datadir=".Length).Replace("\"", "")).FirstOrDefault();
if(DataDir != null && ConfigurationFile != null)
{
var isRelativePath = Path.GetFullPath(ConfigurationFile).Length > ConfigurationFile.Length;
if(isRelativePath)
{
ConfigurationFile = Path.Combine(DataDir, ConfigurationFile);
}
}
Network = args.Contains("-testnet", StringComparer.OrdinalIgnoreCase) ? Network.TestNet :
args.Contains("-regtest", StringComparer.OrdinalIgnoreCase) ? Network.RegTest :
Network.DefaultMain;
if(ConfigurationFile != null)
{
AssetConfigFileExists();
var configTemp = TextFileConfiguration.Parse(File.ReadAllText(ConfigurationFile));
Network = configTemp.GetOrDefault<bool>("testnet", false) ? Network.TestNet :
configTemp.GetOrDefault<bool>("regtest", false) ? Network.RegTest :
Network.DefaultMain;
}
if(DataDir == null)
{
DataDir = DefaultDataDirectory.GetDefaultDirectory("ElementExplorer", Network);
}
if(ConfigurationFile == null)
{
ConfigurationFile = GetDefaultConfigurationFile();
}
if(!Directory.Exists(DataDir))
throw new ConfigurationException("Data directory does not exists");
var consoleConfig = new TextFileConfiguration(args);
var config = TextFileConfiguration.Parse(File.ReadAllText(ConfigurationFile));
consoleConfig.MergeInto(config, true);
if(Network == Network.DefaultMain || Network == Network.RegTest)
{
var rpc = RPCArgs.Parse(config, Network).ConfigureRPCClient(Network);
Network = CreateNetwork(Network, rpc.GetBlock(0));
RPCArgs.CheckNetwork(Network, rpc);
}
Logs.Configuration.LogInformation("Network: " + Network);
Logs.Configuration.LogInformation("Data directory set to " + DataDir);
Logs.Configuration.LogInformation("Configuration file set to " + ConfigurationFile);
Rescan = config.GetOrDefault<bool>("rescan", false);
var defaultPort = config.GetOrDefault<int>("port", 37123);
Listen = config
.GetAll("bind")
.Select(p => ConvertToEndpoint(p, defaultPort))
.ToList();
if(Listen.Count == 0)
{
Listen.Add(new IPEndPoint(IPAddress.Any, defaultPort));
}
RPC = RPCArgs.Parse(config, Network);
NodeEndpoint = ConvertToEndpoint(config.GetOrDefault<string>("node.endpoint", "127.0.0.1"), Network.DefaultPort);
CacheChain = config.GetOrDefault<bool>("cachechain", true);
StartHeight = config.GetOrDefault<int>("startheight", 0);
return this;
}
public int StartHeight
{
get; set;
}
public static Network CreateNetwork(Network parent, Block genesisblock)
{
if(genesisblock == null)
return null;
try
{
return parent.CreateNetwork("explorernetwork", parent, genesisblock);
}
catch
{
return Network.GetNetwork("explorernetwork");
}
}
public string[] GetUrls()
{
return Listen.Select(b => "http://" + b + "/").ToArray();
}
public IPEndPoint NodeEndpoint
{
get; set;
}
public bool CacheChain
{
get;
set;
}
public ExplorerRuntime CreateRuntime()
{
return new ExplorerRuntime(this);
}
public static IPEndPoint ConvertToEndpoint(string str, int defaultPort)
{
var portOut = defaultPort;
var hostOut = "";
int colon = str.LastIndexOf(':');
// if a : is found, and it either follows a [...], or no other : is in the string, treat it as port separator
bool fHaveColon = colon != -1;
bool fBracketed = fHaveColon && (str[0] == '[' && str[colon - 1] == ']'); // if there is a colon, and in[0]=='[', colon is not 0, so in[colon-1] is safe
bool fMultiColon = fHaveColon && (str.LastIndexOf(':', colon - 1) != -1);
if(fHaveColon && (colon == 0 || fBracketed || !fMultiColon))
{
int n;
if(int.TryParse(str.Substring(colon + 1), out n) && n > 0 && n < 0x10000)
{
str = str.Substring(0, colon);
portOut = n;
}
}
if(str.Length > 0 && str[0] == '[' && str[str.Length - 1] == ']')
hostOut = str.Substring(1, str.Length - 2);
else
hostOut = str;
return new IPEndPoint(IPAddress.Parse(hostOut), portOut);
}
private void AssetConfigFileExists()
{
if(!File.Exists(ConfigurationFile))
throw new ConfigurationException("Configuration file does not exists");
}
private string GetDefaultConfigurationFile()
{
var config = Path.Combine(DataDir, "settings.config");
Logs.Configuration.LogInformation("Configuration file set to " + config);
if(!File.Exists(config))
{
Logs.Configuration.LogInformation("Creating configuration file");
StringBuilder builder = new StringBuilder();
builder.AppendLine("####Common Commands####");
builder.AppendLine("#Connection to the node instance");
builder.AppendLine("#rpc.url=http://localhost:" + Network.RPCPort + "/");
builder.AppendLine("#rpc.user=bitcoinuser");
builder.AppendLine("#rpc.password=bitcoinpassword");
builder.AppendLine("#rpc.cookiefile=yourbitcoinfolder/.cookie");
builder.AppendLine("#node.endpoint=localhost:" + Network.DefaultPort);
builder.AppendLine();
builder.AppendLine();
builder.AppendLine("####Server Commands####");
builder.AppendLine("#port=37123");
builder.AppendLine("#listen=0.0.0.0");
File.WriteAllText(config, builder.ToString());
}
return config;
}
}
public class ConfigException : Exception
{
public ConfigException() : base("")
{
}
public ConfigException(string message) : base(message)
{
}
}
}

View File

@ -0,0 +1,162 @@
using NBitcoin;
using NBitcoin.RPC;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json.Linq;
using ElementsExplorer.Logging;
namespace ElementsExplorer.Configuration
{
public class RPCArgs
{
public Uri Url
{
get; set;
}
public string User
{
get; set;
}
public string Password
{
get; set;
}
public string CookieFile
{
get; set;
}
public bool NoTest
{
get;
set;
}
public RPCClient ConfigureRPCClient(Network network)
{
RPCClient rpcClient = null;
var url = Url;
var usr = User;
var pass = Password;
if(url != null && usr != null && pass != null)
rpcClient = new RPCClient(new System.Net.NetworkCredential(usr, pass), url, network);
if(rpcClient == null)
{
if(url != null && CookieFile != null)
{
try
{
rpcClient = new RPCClient(File.ReadAllText(CookieFile), url, network);
}
catch(IOException)
{
Logs.Configuration.LogWarning("RPC Cookie file not found at " + CookieFile);
}
}
if(rpcClient == null)
{
try
{
rpcClient = new RPCClient(network);
}
catch { }
if(rpcClient == null)
{
Logs.Configuration.LogError("RPC connection settings not configured");
throw new ConfigException();
}
}
}
if(NoTest)
return rpcClient;
Logs.Configuration.LogInformation("Testing RPC connection to " + rpcClient.Address.AbsoluteUri);
try
{
var address = new Key().PubKey.GetAddress(network);
var isValid = ((JObject)rpcClient.SendCommand("validateaddress", address.ToString()).Result)["isvalid"].Value<bool>();
if(!isValid)
{
Logs.Configuration.LogError("The RPC Server is on a different blockchain than the one configured for tumbling");
throw new ConfigException();
}
}
catch(ConfigException)
{
throw;
}
catch(RPCException ex)
{
Logs.Configuration.LogError("Invalid response from RPC server " + ex.Message);
throw new ConfigException();
}
catch(Exception ex)
{
Logs.Configuration.LogError("Error connecting to RPC server " + ex.Message);
throw new ConfigException();
}
Logs.Configuration.LogInformation("RPC connection successfull");
var getInfo = rpcClient.SendCommand(RPCOperations.getinfo);
var version = ((JObject)getInfo.Result)["version"].Value<int>();
if(version < MIN_CORE_VERSION)
{
Logs.Configuration.LogError($"The minimum Elements version required is {MIN_CORE_VERSION} (detected: {version})");
throw new ConfigException();
}
Logs.Configuration.LogInformation($"Elements version detected: {version}");
return rpcClient;
}
public static void CheckNetwork(Network network, RPCClient rpcClient)
{
if(network.GenesisHash != null && rpcClient.GetBlockHash(0) != network.GenesisHash)
{
Logs.Configuration.LogError("The RPC server is not using the chain " + network.Name);
throw new ConfigException();
}
}
public RPCClient ConfigureRPCClient(object network)
{
throw new NotImplementedException();
}
const int MIN_CORE_VERSION = 140100;
public static RPCClient ConfigureRPCClient(TextFileConfiguration confArgs, Network network, string prefix = null)
{
RPCArgs args = Parse(confArgs, network, prefix);
return args.ConfigureRPCClient(network);
}
public static RPCArgs Parse(TextFileConfiguration confArgs, Network network, string prefix = null)
{
prefix = prefix ?? "";
if(prefix != "")
{
if(!prefix.EndsWith("."))
prefix += ".";
}
try
{
var url = confArgs.GetOrDefault<string>(prefix + "rpc.url", network == null ? null : "http://localhost:" + network.RPCPort + "/");
return new RPCArgs()
{
User = confArgs.GetOrDefault<string>(prefix + "rpc.user", null),
Password = confArgs.GetOrDefault<string>(prefix + "rpc.password", null),
CookieFile = confArgs.GetOrDefault<string>(prefix + "rpc.cookiefile", null),
Url = url == null ? null : new Uri(url)
};
}
catch(FormatException)
{
throw new ConfigException("rpc.url is not an url");
}
}
}
}

View File

@ -0,0 +1,221 @@
using NBitcoin;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading.Tasks;
namespace ElementsExplorer.Configuration
{
public class ConfigurationException : Exception
{
public ConfigurationException(string message) : base(message)
{
}
}
public class TextFileConfiguration
{
private Dictionary<string, List<string>> _Args;
public TextFileConfiguration(string[] args)
{
_Args = new Dictionary<string, List<string>>();
string noValueParam = null;
Action flushNoValueParam = () =>
{
if(noValueParam != null)
{
Add(noValueParam, "1", false);
noValueParam = null;
}
};
foreach(var arg in args)
{
bool isParamName = arg.StartsWith("-", StringComparison.Ordinal);
if(isParamName)
{
var splitted = arg.Split('=');
if(splitted.Length > 1)
{
var value = String.Join("=", splitted.Skip(1).ToArray());
flushNoValueParam();
Add(splitted[0], value, false);
}
else
{
flushNoValueParam();
noValueParam = splitted[0];
}
}
else
{
if(noValueParam != null)
{
Add(noValueParam, arg, false);
noValueParam = null;
}
}
}
flushNoValueParam();
}
private void Add(string key, string value, bool sourcePriority)
{
key = NormalizeKey(key);
List<string> list;
if(!_Args.TryGetValue(key, out list))
{
list = new List<string>();
_Args.Add(key, list);
}
if(sourcePriority)
list.Insert(0, value);
else
list.Add(value);
}
private static string NormalizeKey(string key)
{
key = key.ToLowerInvariant();
while(key.Length > 0 && key[0] == '-')
{
key = key.Substring(1);
}
key = key.Replace(".", "");
return key;
}
public void MergeInto(TextFileConfiguration destination, bool sourcePriority)
{
foreach(var kv in _Args)
{
foreach(var v in kv.Value)
destination.Add(kv.Key, v, sourcePriority);
}
}
public TextFileConfiguration(Dictionary<string, List<string>> args)
{
_Args = args;
}
public static TextFileConfiguration Parse(string data)
{
Dictionary<string, List<string>> result = new Dictionary<string, List<string>>();
var lines = data.Split(new[] { "\r\n", "\n" }, StringSplitOptions.RemoveEmptyEntries);
int lineCount = -1;
foreach(var l in lines)
{
lineCount++;
var line = l.Trim();
if(line.StartsWith("#", StringComparison.Ordinal))
continue;
var split = line.Split('=');
if(split.Length == 0)
continue;
if(split.Length == 1)
throw new FormatException("Line " + lineCount + ": No value are set");
var key = split[0];
key = NormalizeKey(key);
List<string> values;
if(!result.TryGetValue(key, out values))
{
values = new List<string>();
result.Add(key, values);
}
var value = String.Join("=", split.Skip(1).ToArray());
values.Add(value);
}
return new TextFileConfiguration(result);
}
public bool Contains(string key)
{
List<string> values;
return _Args.TryGetValue(key, out values);
}
public string[] GetAll(string key)
{
List<string> values;
if(!_Args.TryGetValue(key, out values))
return new string[0];
return values.ToArray();
}
private List<Tuple<string, string>> _Aliases = new List<Tuple<string, string>>();
public void AddAlias(string from, string to)
{
from = NormalizeKey(from);
to = NormalizeKey(to);
_Aliases.Add(Tuple.Create(from, to));
}
public T GetOrDefault<T>(string key, T defaultValue)
{
key = NormalizeKey(key);
var aliases = _Aliases
.Where(a => a.Item1 == key || a.Item2 == key)
.Select(a => a.Item1 == key ? a.Item2 : a.Item1)
.ToList();
aliases.Insert(0, key);
foreach(var alias in aliases)
{
List<string> values;
if(!_Args.TryGetValue(alias, out values))
continue;
if(values.Count == 0)
continue;
try
{
return ConvertValue<T>(values[0]);
}
catch(FormatException) { throw new ConfigurationException("Key " + key + " should be of type " + typeof(T).Name); }
}
return defaultValue;
}
private T ConvertValue<T>(string str)
{
if(typeof(T) == typeof(bool))
{
var trueValues = new[] { "1", "true" };
var falseValues = new[] { "0", "false" };
if(trueValues.Contains(str, StringComparer.OrdinalIgnoreCase))
return (T)(object)true;
if(falseValues.Contains(str, StringComparer.OrdinalIgnoreCase))
return (T)(object)false;
throw new FormatException();
}
else if(typeof(T) == typeof(Uri))
return (T)(object)new Uri(str, UriKind.Absolute);
else if(typeof(T) == typeof(string))
return (T)(object)str;
else if(typeof(T) == typeof(IPEndPoint))
{
var separator = str.LastIndexOf(":");
if(separator == -1)
throw new FormatException();
var ip = str.Substring(0, separator);
var port = str.Substring(separator + 1);
return (T)(object)new IPEndPoint(IPAddress.Parse(ip), int.Parse(port));
}
else if(typeof(T) == typeof(int))
{
return (T)(object)int.Parse(str, CultureInfo.InvariantCulture);
}
else
{
throw new NotSupportedException("Configuration value does not support time " + typeof(T).Name);
}
}
}
}

View File

@ -0,0 +1,275 @@
using ElementsExplorer.Logging;
using ElementsExplorer.ModelBinders;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using NBitcoin;
using NBitcoin.DataEncoders;
using NBitcoin.RPC;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace ElementsExplorer.Controllers
{
[Route("v1")]
public class MainController : Controller
{
class AnnotatedTransaction
{
public int Height
{
get;
internal set;
}
public TrackedTransaction Record
{
get;
internal set;
}
}
public MainController(ExplorerRuntime runtime)
{
if(runtime == null)
throw new ArgumentNullException("runtime");
Runtime = runtime;
}
public ExplorerRuntime Runtime
{
get; set;
}
[HttpGet]
[Route("asset/{assetId}")]
public FileContentResult GetAssetName(
[ModelBinder(BinderType = typeof(UInt256ModelBinding))]
uint256 assetId)
{
var name = Runtime.Repository.GetAssetName(assetId) ?? "";
return new FileContentResult(Encoding.UTF8.GetBytes(name), "application/octet-stream");
}
[HttpGet]
[Route("sync/{extPubKey}")]
public async Task<FileContentResult> Sync(
[ModelBinder(BinderType = typeof(DestinationModelBinder))]
BitcoinExtPubKey extPubKey,
[ModelBinder(BinderType = typeof(UInt256ModelBinding))]
uint256 lastBlockHash = null,
[ModelBinder(BinderType = typeof(UInt256ModelBinding))]
uint256 unconfirmedHash = null,
bool noWait = false)
{
lastBlockHash = lastBlockHash ?? uint256.Zero;
var actualLastBlockHash = uint256.Zero;
var waitingTransaction = noWait ? Task.FromResult(false) : WaitingTransaction(extPubKey);
Runtime.Repository.MarkAsUsed(new KeyInformation(extPubKey));
UTXOChanges changes = null;
UTXOChanges previousChanges = null;
List<TrackedTransaction> cleanList = null;
var getKeyPath = GetKeyPaths(extPubKey);
while(true)
{
cleanList = new List<TrackedTransaction>();
HashSet<uint256> conflictedUnconf = new HashSet<uint256>();
changes = new UTXOChanges();
List<AnnotatedTransaction> transactions = GetAnnotatedTransactions(extPubKey);
var unconf = transactions.Where(tx => tx.Height == MempoolHeight);
var conf = transactions.Where(tx => tx.Height != MempoolHeight);
conf = conf.TopologicalSort(DependsOn(conf.ToList())).ToList();
unconf = unconf.TopologicalSort(DependsOn(unconf.ToList())).ToList();
foreach(var item in conf.Concat(unconf))
{
var record = item.Record;
if(record.BlockHash == null)
{
if( //A parent conflicted with the current utxo
record.Transaction.Inputs.Any(i => conflictedUnconf.Contains(i.PrevOut.Hash))
||
//Conflict with the confirmed utxo
changes.Confirmed.HasConflict(record.Transaction))
{
cleanList.Add(record);
conflictedUnconf.Add(record.Transaction.GetHash());
continue;
}
if(changes.Unconfirmed.HasConflict(record.Transaction))
{
Logs.Explorer.LogInformation($"Conflicts in the mempool. {record.Transaction.GetHash()} ignored");
continue;
}
changes.Unconfirmed.LoadChanges(record.Transaction, getKeyPath);
}
else
{
if(changes.Confirmed.HasConflict(record.Transaction))
{
Logs.Explorer.LogError("A conflict among confirmed transaction happened, this should be impossible");
throw new InvalidOperationException("The impossible happened");
}
changes.Unconfirmed.LoadChanges(record.Transaction, getKeyPath);
changes.Confirmed.LoadChanges(record.Transaction, getKeyPath);
changes.Confirmed.Hash = record.BlockHash;
actualLastBlockHash = record.BlockHash;
if(record.BlockHash == lastBlockHash)
previousChanges = changes.Clone();
}
}
changes.Unconfirmed = changes.Unconfirmed.Diff(changes.Confirmed);
changes.Unconfirmed.Hash = changes.Unconfirmed.GetHash();
if(changes.Unconfirmed.Hash == unconfirmedHash)
changes.Unconfirmed.Clear();
else
changes.Unconfirmed.Reset = true;
if(actualLastBlockHash == lastBlockHash)
changes.Confirmed.Clear();
else if(previousChanges != null)
{
changes.Confirmed.Reset = false;
changes.Confirmed = changes.Confirmed.Diff(previousChanges.Confirmed);
}
else
{
changes.Confirmed.Reset = true;
changes.Confirmed.SpentOutpoints.Clear();
}
if(changes.HasChanges || !(await waitingTransaction))
break;
waitingTransaction = Task.FromResult(false); //next time, will not wait
}
Runtime.Repository.CleanTransactions(extPubKey.ExtPubKey, cleanList);
return new FileContentResult(changes.ToBytes(), "application/octet-stream");
}
private List<AnnotatedTransaction> GetAnnotatedTransactions(BitcoinExtPubKey extPubKey)
{
return Runtime.Repository
.GetTransactions(extPubKey)
.Select(t =>
new AnnotatedTransaction
{
Height = GetHeight(t.BlockHash),
Record = t
})
.Where(u => u.Height != OrphanHeight)
.ToList();
}
Func<AnnotatedTransaction, IEnumerable<AnnotatedTransaction>> DependsOn(IEnumerable<AnnotatedTransaction> transactions)
{
return t =>
{
HashSet<uint256> dependsOn = new HashSet<uint256>(t.Record.Transaction.Inputs.Select(txin => txin.PrevOut.Hash));
return transactions.Where(u => dependsOn.Contains(u.Record.Transaction.GetHash()) || //Depends on parent transaction
((u.Height < t.Height))); //Depends on earlier transaction
};
}
private async Task<bool> WaitingTransaction(BitcoinExtPubKey extPubKey)
{
CancellationTokenSource cts = new CancellationTokenSource();
int timeout = 10000;
cts.CancelAfter(timeout);
try
{
if(!await Runtime.WaitFor(extPubKey.ExtPubKey, cts.Token))
{
await Task.Delay(timeout);
return false;
}
}
catch(OperationCanceledException) { return false; }
return true;
}
private Func<Script, KeyPath> GetKeyPaths(BitcoinExtPubKey extPubKey)
{
return (script) =>
{
return Runtime.Repository.GetKeyInformation(extPubKey.ExtPubKey, script)?.KeyPath;
};
}
const int MempoolHeight = int.MaxValue;
const int OrphanHeight = int.MaxValue - 1;
private int GetHeight(uint256 blockHash)
{
if(blockHash == null)
return MempoolHeight;
var header = Runtime.Chain.GetBlock(blockHash);
return header == null ? OrphanHeight : header.Height;
}
[HttpPost]
[Route("broadcast")]
public async Task<bool> Broadcast()
{
BitcoinExtPubKey extPubKey = null;
//Crazy hack to get extPubKey... For some reason it is impossible to pass it as argument without crashing on linux
if(Request.Query.ContainsKey("extPubKey"))
{
extPubKey = new BitcoinExtPubKey(Request.Query["extPubKey"], Runtime.Network);
}
////
var tx = new Transaction();
var stream = new BitcoinStream(Request.Body, false);
tx.ReadWrite(stream);
try
{
await Runtime.RPC.SendRawTransactionAsync(tx);
return true;
}
catch(RPCException ex)
{
Logs.Explorer.LogInformation($"Transaction {tx.GetHash()} failed to broadcast (Code: {ex.RPCCode}, Message: {ex.RPCCodeMessage}, Details: {ex.Message} )");
if(extPubKey != null && ex.Message.StartsWith("Missing inputs", StringComparison.OrdinalIgnoreCase))
{
Logs.Explorer.LogInformation("Trying to broadcast unconfirmed of the wallet");
var transactions = GetAnnotatedTransactions(extPubKey).Where(t => t.Height == MempoolHeight).ToList();
transactions = transactions.TopologicalSort(DependsOn(transactions)).ToList();
foreach(var existing in transactions)
{
try
{
await Runtime.RPC.SendRawTransactionAsync(existing.Record.Transaction);
}
catch { }
}
try
{
await Runtime.RPC.SendRawTransactionAsync(tx);
Logs.Explorer.LogInformation($"Broadcast success");
return true;
}
catch(RPCException)
{
Logs.Explorer.LogInformation($"Transaction {tx.GetHash()} failed to broadcast (Code: {ex.RPCCode}, Message: {ex.RPCCodeMessage}, Details: {ex.Message} )");
}
}
return false;
}
}
}
}

View File

@ -0,0 +1,34 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\NElements\NBitcoin.NETCore\NBitcoin.NETCore.csproj" />
<PackageReference Include="DBreeze" Version="1.87.0" />
<PackageReference Include="Microsoft.AspNetCore" Version="1.1.2" />
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="1.1.3" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="1.1.2" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="1.1.2" />
<PackageReference Include="Microsoft.Extensions.Logging.Filter" Version="1.1.2" />
<PackageReference Include="System.Xml.XmlSerializer" Version="4.0.11" />
</ItemGroup>
<ItemGroup>
<Compile Update="Properties\Resources.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Properties\Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
</Project>

View File

@ -0,0 +1,347 @@
using ElementsExplorer.Logging;
using System.Linq;
using Microsoft.Extensions.Logging;
using NBitcoin;
using NBitcoin.Protocol;
using NBitcoin.Protocol.Behaviors;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.Collections.Concurrent;
using System.Threading.Tasks;
using NBitcoin.Crypto;
using Completion = System.Threading.Tasks.TaskCompletionSource<bool>;
namespace ElementsExplorer
{
public class ExplorerBehavior : NodeBehavior
{
public ExplorerBehavior(ExplorerRuntime runtime, ConcurrentChain chain)
{
if(runtime == null)
throw new ArgumentNullException("runtime");
if(chain == null)
throw new ArgumentNullException(nameof(chain));
_Chain = chain;
_Runtime = runtime;
}
private readonly ConcurrentChain _Chain;
public ConcurrentChain Chain
{
get
{
return _Chain;
}
}
private readonly ExplorerRuntime _Runtime;
public ExplorerRuntime Runtime
{
get
{
return _Runtime;
}
}
public int StartHeight
{
get;
set;
}
public override object Clone()
{
return new ExplorerBehavior(Runtime, _Chain);
}
Timer _Timer;
protected override void AttachCore()
{
AttachedNode.StateChanged += AttachedNode_StateChanged;
AttachedNode.MessageReceived += AttachedNode_MessageReceived;
_CurrentLocation = Runtime.Repository.GetIndexProgress();
_Timer = new Timer(Tick, null, 0, 30);
}
public async Task WaitFor(ExtPubKey pubKey, CancellationToken cancellation = default(CancellationToken))
{
TaskCompletionSource<bool> completion = new TaskCompletionSource<bool>();
var key = Hashes.Hash160(pubKey.ToBytes());
lock(_WaitFor)
{
_WaitFor.Add(key, completion);
}
cancellation.Register(() =>
{
completion.TrySetCanceled();
});
try
{
await completion.Task;
}
finally
{
lock(_WaitFor)
{
_WaitFor.Remove(key, completion);
}
}
}
MultiValueDictionary<uint160, Completion> _WaitFor = new MultiValueDictionary<uint160, Completion>();
public void AskBlocks()
{
if(AttachedNode.State != NodeState.HandShaked)
return;
var pendingTip = AttachedNode.Behaviors.Find<ChainBehavior>().PendingTip;
if(pendingTip == null || pendingTip.Height < AttachedNode.PeerVersion.StartHeight)
return;
if(_InFlights.Count != 0)
return;
var currentLocation = _CurrentLocation ?? new BlockLocator() { Blocks = { Chain.GetBlock(StartHeight).HashBlock } }; ;
var currentBlock = Chain.FindFork(currentLocation);
if(currentBlock.Height < StartHeight)
currentBlock = Chain.GetBlock(StartHeight) ?? pendingTip;
//Up to date
if(pendingTip.HashBlock == currentBlock.HashBlock)
return;
var toDownload = pendingTip.EnumerateToGenesis().TakeWhile(b => b.HashBlock != currentBlock.HashBlock).ToArray();
Array.Reverse(toDownload);
var invs = toDownload.Take(10)
.Select(b => new InventoryVector(AttachedNode.AddSupportedOptions(InventoryType.MSG_BLOCK), b.HashBlock))
.Where(b => _InFlights.TryAdd(b.Hash, new Download()))
.ToArray();
if(invs.Length != 0)
{
AttachedNode.SendMessageAsync(new GetDataPayload(invs));
Runtime.Repository.SetIndexProgress(currentLocation);
}
}
class Download
{
}
ConcurrentDictionary<uint256, Download> _InFlights = new ConcurrentDictionary<uint256, Download>();
void Tick(object state)
{
try
{
AskBlocks();
}
catch(Exception ex)
{
if(AttachedNode == null)
return;
Logs.Explorer.LogError("Exception in ExplorerBehavior tick loop");
Logs.Explorer.LogError(ex.ToString());
}
}
BlockLocator _CurrentLocation;
protected override void DetachCore()
{
AttachedNode.StateChanged -= AttachedNode_StateChanged;
AttachedNode.MessageReceived -= AttachedNode_MessageReceived;
_Timer.Dispose();
_Timer = null;
}
private void AttachedNode_MessageReceived(Node node, IncomingMessage message)
{
message.Message.IfPayloadIs<InvPayload>(invs =>
{
var data = new GetDataPayload();
foreach(var inv in invs.Inventory)
{
inv.Type = node.AddSupportedOptions(inv.Type);
if(inv.Type.HasFlag(InventoryType.MSG_TX))
data.Inventory.Add(inv);
}
if(data.Inventory.Count != 0)
node.SendMessageAsync(data);
});
message.Message.IfPayloadIs<HeadersPayload>(headers =>
{
if(headers.Headers.Count == 0)
return;
AskBlocks();
});
message.Message.IfPayloadIs<BlockPayload>(block =>
{
block.Object.Header.CacheHashes();
Download o;
if(_InFlights.TryRemove(block.Object.GetHash(), out o))
{
HashSet<ExtPubKey> pubKeys = new HashSet<ExtPubKey>();
foreach(var tx in block.Object.Transactions)
tx.CacheHashes();
List<InsertTransaction> trackedTransactions = new List<InsertTransaction>();
foreach(var tx in block.Object.Transactions)
{
var pubKeys2 = GetInterestedWallets(tx);
foreach(var pubkey in pubKeys2)
{
pubKeys.Add(pubkey);
trackedTransactions.Add(
new InsertTransaction()
{
PubKey = pubkey,
TrackedTransaction = new TrackedTransaction()
{
BlockHash = block.Object.GetHash(),
Transaction = tx
}
});
}
}
Runtime.Repository.InsertTransactions(trackedTransactions.ToArray());
var blockHeader = Runtime.Chain.GetBlock(block.Object.GetHash());
if(blockHeader != null)
{
_CurrentLocation = blockHeader.GetLocator();
Logs.Explorer.LogInformation($"Processed block {block.Object.GetHash()}");
}
foreach(var tx in block.Object.Transactions)
ScanForAssetName(tx, false);
foreach(var pubkey in pubKeys)
{
Notify(pubkey, false);
}
}
if(_InFlights.Count == 0)
AskBlocks();
});
message.Message.IfPayloadIs<TxPayload>(txPayload =>
{
var pubKeys = GetInterestedWallets(txPayload.Object);
foreach(var pubkey in pubKeys)
{
Runtime.Repository.InsertTransactions(new[]
{
new InsertTransaction()
{
PubKey = pubkey,
TrackedTransaction = new TrackedTransaction()
{
Transaction = txPayload.Object
}
}
});
}
ScanForAssetName(txPayload.Object, true);
foreach(var pubkey in pubKeys)
{
Notify(pubkey, true);
}
});
}
private void ScanForAssetName(Transaction tx, bool logFailure)
{
var name = NamedIssuance.Extract(tx);
if(name != null)
{
var result = Runtime.Repository.SetAssetName(name);
if(result == Repository.SetNameResult.Success)
Logs.Explorer.LogInformation($"Name {name.Name} claimed by {name.AssetId}");
else
if(logFailure)
Logs.Explorer.LogInformation($"Name {name.Name} failed to be claimed by {name.AssetId}, cause: {result}");
}
}
private void Notify(ExtPubKey pubkey, bool log)
{
if(log)
Logs.Explorer.LogInformation($"A wallet received money");
var key = Hashes.Hash160(pubkey.ToBytes());
lock(_WaitFor)
{
IReadOnlyCollection<Completion> completions;
if(_WaitFor.TryGetValue(key, out completions))
{
foreach(var completion in completions.ToList())
{
completion.TrySetResult(true);
}
}
}
}
private HashSet<ExtPubKey> GetInterestedWallets(Transaction tx)
{
var pubKeys = new HashSet<ExtPubKey>();
tx.CacheHashes();
foreach(var input in tx.Inputs)
{
var signer = input.ScriptSig.GetSigner() ?? input.WitScript.ToScript().GetSigner();
if(signer != null)
{
var keyInfo = Runtime.Repository.GetKeyInformation(signer.ScriptPubKey);
if(keyInfo != null)
{
pubKeys.Add(new ExtPubKey(keyInfo.RootKey));
Runtime.Repository.MarkAsUsed(keyInfo);
}
}
}
foreach(var output in tx.Outputs)
{
var keyInfo = Runtime.Repository.GetKeyInformation(output.ScriptPubKey);
if(keyInfo != null)
{
pubKeys.Add(new ExtPubKey(keyInfo.RootKey));
Runtime.Repository.MarkAsUsed(keyInfo);
}
}
return pubKeys;
}
private void AttachedNode_StateChanged(Node node, NodeState oldState)
{
if(node.State == NodeState.HandShaked)
{
Logs.Explorer.LogInformation($"Handshaked Elements node");
node.SendMessageAsync(new SendHeadersPayload());
node.SendMessageAsync(new MempoolPayload());
AskBlocks();
}
if(node.State == NodeState.Offline)
Logs.Explorer.LogInformation($"Closed connection with Elements node");
if(node.State == NodeState.Failed)
Logs.Explorer.LogError($"Connection with Elements unexpectedly failed: {node.DisconnectReason.Reason}");
}
}
}

View File

@ -0,0 +1,136 @@
using NBitcoin;
using NBitcoin.JsonConverters;
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
namespace ElementsExplorer
{
public class ExplorerClient
{
public ExplorerClient(Network network, Uri serverAddress)
{
if(serverAddress == null)
throw new ArgumentNullException(nameof(serverAddress));
if(network == null)
throw new ArgumentNullException(nameof(network));
_Address = serverAddress;
_Network = network;
}
public UTXOChanges Sync(BitcoinExtPubKey extKey, UTXOChanges previousChange, bool noWait = false)
{
return SyncAsync(extKey, previousChange, noWait).GetAwaiter().GetResult();
}
public Task<UTXOChanges> SyncAsync(BitcoinExtPubKey extKey, UTXOChanges previousChange, bool noWait = false)
{
return SyncAsync(extKey, previousChange?.Confirmed?.Hash, previousChange?.Unconfirmed?.Hash, noWait);
}
public UTXOChanges Sync(BitcoinExtPubKey extKey, uint256 lastBlockHash, uint256 unconfirmedHash, bool noWait = false)
{
return SyncAsync(extKey, lastBlockHash, unconfirmedHash, noWait).GetAwaiter().GetResult();
}
public async Task<UTXOChanges> SyncAsync(BitcoinExtPubKey extKey, uint256 lastBlockHash, uint256 unconfirmedHash, bool noWait = false)
{
lastBlockHash = lastBlockHash ?? uint256.Zero;
unconfirmedHash = unconfirmedHash ?? uint256.Zero;
var bytes = await SendAsync<byte[]>(HttpMethod.Get, null, "v1/sync/{0}?lastBlockHash={1}&unconfirmedHash={2}&noWait={3}", extKey, lastBlockHash, unconfirmedHash, noWait).ConfigureAwait(false);
UTXOChanges changes = new UTXOChanges();
changes.FromBytes(bytes);
return changes;
}
public bool Broadcast(Transaction tx)
{
return BroadcastAsync(tx).GetAwaiter().GetResult();
}
public Task<bool> BroadcastAsync(Transaction tx)
{
return SendAsync<bool>(HttpMethod.Post, tx.ToBytes(), "v1/broadcast");
}
private static readonly HttpClient SharedClient = new HttpClient();
internal HttpClient Client = SharedClient;
private readonly Network _Network;
public Network Network
{
get
{
return _Network;
}
}
private readonly Uri _Address;
public Uri Address
{
get
{
return _Address;
}
}
private string GetFullUri(string relativePath, params object[] parameters)
{
relativePath = String.Format(relativePath, parameters ?? new object[0]);
var uri = Address.AbsoluteUri;
if(!uri.EndsWith("/", StringComparison.Ordinal))
uri += "/";
uri += relativePath;
return uri;
}
private Task<T> GetAsync<T>(string relativePath, params object[] parameters)
{
return SendAsync<T>(HttpMethod.Get, null, relativePath, parameters);
}
private async Task<T> SendAsync<T>(HttpMethod method, object body, string relativePath, params object[] parameters)
{
var uri = GetFullUri(relativePath, parameters);
var message = new HttpRequestMessage(method, uri);
if(body != null)
{
if(body is byte[])
message.Content = new ByteArrayContent((byte[])body);
else
message.Content = new StringContent(Serializer.ToString(body, Network), Encoding.UTF8, "application/json");
}
var result = await Client.SendAsync(message).ConfigureAwait(false);
if(result.StatusCode == HttpStatusCode.NotFound)
return default(T);
if(!result.IsSuccessStatusCode)
{
string error = await result.Content.ReadAsStringAsync().ConfigureAwait(false);
if(!string.IsNullOrEmpty(error))
{
throw new HttpRequestException(result.StatusCode + ": " + error);
}
}
result.EnsureSuccessStatusCode();
if(typeof(T) == typeof(byte[]))
return (T)(object)await result.Content.ReadAsByteArrayAsync().ConfigureAwait(false);
var str = await result.Content.ReadAsStringAsync().ConfigureAwait(false);
if(typeof(T) == typeof(string))
return (T)(object)str;
return Serializer.ToObject<T>(str, Network);
}
public async Task<string> GetAssetNameAsync(uint256 assetId)
{
var bytes = await SendAsync<byte[]>(HttpMethod.Get, null, "v1/asset/" + assetId).ConfigureAwait(false);
return Encoding.UTF8.GetString(bytes);
}
public string GetAssetName(uint256 assetId)
{
return GetAssetNameAsync(assetId).GetAwaiter().GetResult();
}
}
}

View File

@ -0,0 +1,201 @@
using ElementsExplorer.Configuration;
using System.Linq;
using Microsoft.Extensions.Logging;
using NBitcoin;
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Extensions.DependencyInjection;
using NBitcoin.RPC;
using NBitcoin.Protocol;
using System.Threading;
using ElementsExplorer.Logging;
using Microsoft.AspNetCore.Hosting;
using System.Net;
using NBitcoin.Protocol.Behaviors;
using System.IO;
using System.Threading.Tasks;
namespace ElementsExplorer
{
public class ExplorerRuntime : IDisposable
{
public ExplorerRuntime()
{
}
NodesGroup _Nodes;
public ExplorerRuntime(ExplorerConfiguration configuration)
{
if(configuration == null)
throw new ArgumentNullException("configuration");
Network = configuration.Network;
Chain = new ConcurrentChain(Network.GetGenesis().Header);
RPC = configuration.RPC.ConfigureRPCClient(configuration.Network);
NodeEndpoint = configuration.NodeEndpoint;
ServerUrls = configuration.GetUrls();
var cachePath = Path.Combine(configuration.DataDir, "chain.dat");
if(configuration.CacheChain)
{
Logs.Configuration.LogInformation($"Loading chain from cache...");
if(File.Exists(cachePath))
{
Chain.Load(File.ReadAllBytes(cachePath));
}
}
Logs.Configuration.LogInformation($"Loading chain from node...");
var heightBefore = Chain.Height;
try
{
if(!configuration.RPC.NoTest)
{
Logs.Configuration.LogInformation("Trying to connect to node: " + configuration.NodeEndpoint);
using(var node = Node.Connect(Network, configuration.NodeEndpoint))
{
var cts = new CancellationTokenSource();
cts.CancelAfter(5000);
node.VersionHandshake(cts.Token);
node.SynchronizeChain(Chain);
}
Logs.Configuration.LogInformation("Node connection successfull");
}
}
catch(Exception ex)
{
Logs.Configuration.LogError("Error while connecting to node: " + ex.Message);
throw new ConfigException();
}
Logs.Configuration.LogInformation($"Chain loaded from node");
if(configuration.CacheChain && heightBefore != Chain.Height)
{
Logs.Configuration.LogInformation($"Saving chain to cache...");
var ms = new MemoryStream();
Chain.WriteTo(ms);
File.WriteAllBytes(cachePath, ms.ToArray());
}
var dbPath = Path.Combine(configuration.DataDir, "db");
Repository = new Repository(dbPath, true);
if(configuration.Rescan)
{
Logs.Configuration.LogInformation("Rescanning...");
Repository.SetIndexProgress(null);
}
}
public void StartNodeListener(int startHeight)
{
_Nodes = CreateNodeGroup(Chain, startHeight);
while(_Nodes.ConnectedNodes.Count == 0)
Thread.Sleep(10);
}
public Repository Repository
{
get; set;
}
public async Task<bool> WaitFor(ExtPubKey extPubKey, CancellationToken token)
{
var node = _Nodes.ConnectedNodes.FirstOrDefault();
if(node == null)
return false;
await node.Behaviors.Find<ExplorerBehavior>().WaitFor(extPubKey, token).ConfigureAwait(false);
return true;
}
public ConcurrentChain Chain
{
get; set;
}
public string[] ServerUrls
{
get; set;
}
public IWebHost CreateWebHost()
{
return new WebHostBuilder()
.UseKestrel()
.UseStartup<Startup>()
.ConfigureServices(services =>
{
services.AddSingleton(provider =>
{
return this;
});
services.AddSingleton(Network);
})
.UseUrls(ServerUrls)
.Build();
}
NodesGroup CreateNodeGroup(ConcurrentChain chain, int startHeight)
{
AddressManager manager = new AddressManager();
manager.Add(new NetworkAddress(NodeEndpoint), IPAddress.Loopback);
NodesGroup group = new NodesGroup(Network, new NodeConnectionParameters()
{
Services = NodeServices.Nothing,
IsRelay = true,
TemplateBehaviors =
{
new AddressManagerBehavior(manager)
{
PeersToDiscover = 1,
Mode = AddressManagerBehaviorMode.None
},
new ExplorerBehavior(this, chain) { StartHeight = startHeight },
new ChainBehavior(chain)
{
CanRespondToGetHeaders = false
}
}
});
group.AllowSameGroup = true;
group.MaximumNodeConnection = 1;
group.Connect();
return group;
}
public Network Network
{
get; set;
}
public RPCClient RPC
{
get;
set;
}
public IPEndPoint NodeEndpoint
{
get;
set;
}
object l = new object();
public void Dispose()
{
lock(l)
{
if(_Nodes != null)
{
_Nodes.Disconnect();
_Nodes = null;
}
if(Repository != null)
{
Repository.Dispose();
Repository = null;
}
}
}
}
}

View File

@ -0,0 +1,406 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Console;
using Microsoft.Extensions.Logging.Console.Internal;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
namespace ElementsExplorer.Logging
{
/// <summary>
/// A variant of ASP.NET Core ConsoleLogger which does not make new line for the category
/// </summary>
public class CustomerConsoleLogger : ILogger
{
private static readonly string _loglevelPadding = ": ";
private static readonly string _messagePadding;
private static readonly string _newLineWithMessagePadding;
// ConsoleColor does not have a value to specify the 'Default' color
private readonly ConsoleColor? DefaultConsoleColor = null;
private readonly ConsoleLoggerProcessor _queueProcessor;
private Func<string, LogLevel, bool> _filter;
[ThreadStatic]
private static StringBuilder _logBuilder;
static CustomerConsoleLogger()
{
var logLevelString = GetLogLevelString(LogLevel.Information);
_messagePadding = new string(' ', logLevelString.Length + _loglevelPadding.Length);
_newLineWithMessagePadding = Environment.NewLine + _messagePadding;
}
public CustomerConsoleLogger(string name, Func<string, LogLevel, bool> filter, bool includeScopes)
: this(name, filter, includeScopes, new ConsoleLoggerProcessor())
{
}
internal CustomerConsoleLogger(string name, Func<string, LogLevel, bool> filter, bool includeScopes, ConsoleLoggerProcessor loggerProcessor)
{
if(name == null)
{
throw new ArgumentNullException(nameof(name));
}
Name = name;
Filter = filter ?? ((category, logLevel) => true);
IncludeScopes = includeScopes;
_queueProcessor = loggerProcessor;
if(RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
Console = new WindowsLogConsole();
}
else
{
Console = new AnsiLogConsole(new AnsiSystemConsole());
}
}
public IConsole Console
{
get
{
return _queueProcessor.Console;
}
set
{
if(value == null)
{
throw new ArgumentNullException(nameof(value));
}
_queueProcessor.Console = value;
}
}
public Func<string, LogLevel, bool> Filter
{
get
{
return _filter;
}
set
{
if(value == null)
{
throw new ArgumentNullException(nameof(value));
}
_filter = value;
}
}
public bool IncludeScopes
{
get; set;
}
public string Name
{
get;
}
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
{
if(!IsEnabled(logLevel))
{
return;
}
if(formatter == null)
{
throw new ArgumentNullException(nameof(formatter));
}
var message = formatter(state, exception);
if(!string.IsNullOrEmpty(message) || exception != null)
{
WriteMessage(logLevel, Name, eventId.Id, message, exception);
}
}
public virtual void WriteMessage(LogLevel logLevel, string logName, int eventId, string message, Exception exception)
{
var logBuilder = _logBuilder;
_logBuilder = null;
if(logBuilder == null)
{
logBuilder = new StringBuilder();
}
var logLevelColors = default(ConsoleColors);
var logLevelString = string.Empty;
// Example:
// INFO: ConsoleApp.Program[10]
// Request received
logLevelColors = GetLogLevelConsoleColors(logLevel);
logLevelString = GetLogLevelString(logLevel);
// category and event id
var lenBefore = logBuilder.ToString().Length;
logBuilder.Append(_loglevelPadding);
logBuilder.Append(logName);
logBuilder.Append(": ");
var lenAfter = logBuilder.ToString().Length;
while(lenAfter++ < 18)
logBuilder.Append(" ");
// scope information
if(IncludeScopes)
{
GetScopeInformation(logBuilder);
}
if(!string.IsNullOrEmpty(message))
{
// message
//logBuilder.Append(_messagePadding);
var len = logBuilder.Length;
logBuilder.AppendLine(message);
logBuilder.Replace(Environment.NewLine, _newLineWithMessagePadding, len, message.Length);
}
// Example:
// System.InvalidOperationException
// at Namespace.Class.Function() in File:line X
if(exception != null)
{
// exception message
logBuilder.AppendLine(exception.ToString());
}
if(logBuilder.Length > 0)
{
var hasLevel = !string.IsNullOrEmpty(logLevelString);
// Queue log message
_queueProcessor.EnqueueMessage(new LogMessageEntry()
{
Message = logBuilder.ToString(),
MessageColor = DefaultConsoleColor,
LevelString = hasLevel ? logLevelString : null,
LevelBackground = hasLevel ? logLevelColors.Background : null,
LevelForeground = hasLevel ? logLevelColors.Foreground : null
});
}
logBuilder.Clear();
if(logBuilder.Capacity > 1024)
{
logBuilder.Capacity = 1024;
}
_logBuilder = logBuilder;
}
public bool IsEnabled(LogLevel logLevel)
{
return Filter(Name, logLevel);
}
public IDisposable BeginScope<TState>(TState state)
{
if(state == null)
{
throw new ArgumentNullException(nameof(state));
}
return ConsoleLogScope.Push(Name, state);
}
private static string GetLogLevelString(LogLevel logLevel)
{
switch(logLevel)
{
case LogLevel.Trace:
return "trce";
case LogLevel.Debug:
return "dbug";
case LogLevel.Information:
return "info";
case LogLevel.Warning:
return "warn";
case LogLevel.Error:
return "fail";
case LogLevel.Critical:
return "crit";
default:
throw new ArgumentOutOfRangeException(nameof(logLevel));
}
}
private ConsoleColors GetLogLevelConsoleColors(LogLevel logLevel)
{
// We must explicitly set the background color if we are setting the foreground color,
// since just setting one can look bad on the users console.
switch(logLevel)
{
case LogLevel.Critical:
return new ConsoleColors(ConsoleColor.White, ConsoleColor.Red);
case LogLevel.Error:
return new ConsoleColors(ConsoleColor.Black, ConsoleColor.Red);
case LogLevel.Warning:
return new ConsoleColors(ConsoleColor.Yellow, ConsoleColor.Black);
case LogLevel.Information:
return new ConsoleColors(ConsoleColor.DarkGreen, ConsoleColor.Black);
case LogLevel.Debug:
return new ConsoleColors(ConsoleColor.Gray, ConsoleColor.Black);
case LogLevel.Trace:
return new ConsoleColors(ConsoleColor.Gray, ConsoleColor.Black);
default:
return new ConsoleColors(DefaultConsoleColor, DefaultConsoleColor);
}
}
private void GetScopeInformation(StringBuilder builder)
{
var current = ConsoleLogScope.Current;
string scopeLog = string.Empty;
var length = builder.Length;
while(current != null)
{
if(length == builder.Length)
{
scopeLog = $"=> {current}";
}
else
{
scopeLog = $"=> {current} ";
}
builder.Insert(length, scopeLog);
current = current.Parent;
}
if(builder.Length > length)
{
builder.Insert(length, _messagePadding);
builder.AppendLine();
}
}
private struct ConsoleColors
{
public ConsoleColors(ConsoleColor? foreground, ConsoleColor? background)
{
Foreground = foreground;
Background = background;
}
public ConsoleColor? Foreground
{
get;
}
public ConsoleColor? Background
{
get;
}
}
private class AnsiSystemConsole : IAnsiSystemConsole
{
public void Write(string message)
{
System.Console.Write(message);
}
public void WriteLine(string message)
{
System.Console.WriteLine(message);
}
}
}
public class ConsoleLoggerProcessor : IDisposable
{
private const int _maxQueuedMessages = 1024;
private readonly BlockingCollection<LogMessageEntry> _messageQueue = new BlockingCollection<LogMessageEntry>(_maxQueuedMessages);
private readonly Task _outputTask;
public IConsole Console;
public ConsoleLoggerProcessor()
{
// Start Console message queue processor
_outputTask = Task.Factory.StartNew(
ProcessLogQueue,
this,
TaskCreationOptions.LongRunning);
}
public virtual void EnqueueMessage(LogMessageEntry message)
{
if(!_messageQueue.IsAddingCompleted)
{
try
{
_messageQueue.Add(message);
return;
}
catch(InvalidOperationException) { }
}
// Adding is completed so just log the message
WriteMessage(message);
}
// for testing
internal virtual void WriteMessage(LogMessageEntry message)
{
if(message.LevelString != null)
{
Console.Write(message.LevelString, message.LevelBackground, message.LevelForeground);
}
Console.Write(message.Message, message.MessageColor, message.MessageColor);
Console.Flush();
}
private void ProcessLogQueue()
{
foreach(var message in _messageQueue.GetConsumingEnumerable())
{
WriteMessage(message);
}
}
private static void ProcessLogQueue(object state)
{
var consoleLogger = (ConsoleLoggerProcessor)state;
consoleLogger.ProcessLogQueue();
}
public void Dispose()
{
_messageQueue.CompleteAdding();
try
{
_outputTask.Wait(1500); // with timeout in-case Console is locked by user input
}
catch(TaskCanceledException) { }
catch(AggregateException ex) when(ex.InnerExceptions.Count == 1 && ex.InnerExceptions[0] is TaskCanceledException) { }
}
}
public struct LogMessageEntry
{
public string LevelString;
public ConsoleColor? LevelBackground;
public ConsoleColor? LevelForeground;
public ConsoleColor? MessageColor;
public string Message;
}
}

View File

@ -0,0 +1,54 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace ElementsExplorer.Logging
{
public class Logs
{
static Logs()
{
Configure(new FuncLoggerFactory(n => NullLogger.Instance));
}
public static void Configure(ILoggerFactory factory)
{
Configuration = factory.CreateLogger("Configuration");
Explorer = factory.CreateLogger("Explorer");
}
public static ILogger Configuration
{
get; set;
}
public static ILogger Explorer
{
get; set;
}
public const int ColumnLength = 16;
}
public class FuncLoggerFactory : ILoggerFactory
{
private Func<string, ILogger> createLogger;
public FuncLoggerFactory(Func<string, ILogger> createLogger)
{
this.createLogger = createLogger;
}
public void AddProvider(ILoggerProvider provider)
{
}
public ILogger CreateLogger(string categoryName)
{
return createLogger(categoryName);
}
public void Dispose()
{
}
}
}

View File

@ -0,0 +1,53 @@
using Microsoft.AspNetCore.Mvc.ModelBinding;
using NBitcoin;
using System.Reflection;
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Internal;
namespace ElementsExplorer.ModelBinders
{
public class DestinationModelBinder : IModelBinder
{
public DestinationModelBinder()
{
}
#region IModelBinder Members
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if(!
(typeof(Base58Data).GetTypeInfo().IsAssignableFrom(bindingContext.ModelType) ||
typeof(IDestination).GetTypeInfo().IsAssignableFrom(bindingContext.ModelType)))
{
return TaskCache.CompletedTask;
}
ValueProviderResult val = bindingContext.ValueProvider.GetValue(
bindingContext.ModelName);
if(val == null)
{
return TaskCache.CompletedTask;
}
string key = val.FirstValue as string;
if(key == null)
{
return TaskCache.CompletedTask;
}
var network = (Network)bindingContext.HttpContext.RequestServices.GetService(typeof(Network));
var data = Network.Parse(key, network);
if(!bindingContext.ModelType.IsInstanceOfType(data))
{
throw new FormatException("Invalid destination type");
}
bindingContext.Result = ModelBindingResult.Success(data);
return TaskCache.CompletedTask;
}
#endregion
}
}

View File

@ -0,0 +1,75 @@
using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using NBitcoin;
using System;
using System.Reflection;
using System.Threading.Tasks;
namespace ElementsExplorer.ModelBinders
{
public class UInt256ModelBinding : IModelBinder
{
#region IModelBinder Members
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if(!typeof(uint256).GetTypeInfo().IsAssignableFrom(bindingContext.ModelType))
{
return TaskCache.CompletedTask;
}
ValueProviderResult val = bindingContext.ValueProvider.GetValue(
bindingContext.ModelName);
if(val == null)
{
return TaskCache.CompletedTask;
}
string key = val.FirstValue as string;
if(key == null)
{
bindingContext.Result = ModelBindingResult.Success(null);
return TaskCache.CompletedTask;
}
var value = uint256.Parse(key);
bindingContext.Result = ModelBindingResult.Success(value);
return TaskCache.CompletedTask;
}
#endregion
}
public class UInt160ModelBinding : IModelBinder
{
#region IModelBinder Members
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if(!typeof(uint160).GetTypeInfo().IsAssignableFrom(bindingContext.ModelType))
{
return TaskCache.CompletedTask;
}
ValueProviderResult val = bindingContext.ValueProvider.GetValue(
bindingContext.ModelName);
if(val == null)
{
return TaskCache.CompletedTask;
}
string key = val.FirstValue as string;
if(key == null)
{
bindingContext.Model = null;
return TaskCache.CompletedTask;
}
var value = uint160.Parse(key);
if(value.ToString().StartsWith(uint160.Zero.ToString()))
throw new FormatException("Invalid hash format");
bindingContext.Result = ModelBindingResult.Success(value);
return TaskCache.CompletedTask;
}
#endregion
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,46 @@
using NBitcoin;
using System.Linq;
using System;
using System.Collections.Generic;
using System.Text;
using NBitcoin.DataEncoders;
namespace ElementsExplorer
{
public class NamedIssuance
{
public string Name
{
get;
set;
}
public uint256 AssetId
{
get;
set;
}
public static NamedIssuance Extract(Transaction tx)
{
var assetId = tx.Inputs
.Select(txin => txin.GetIssuedAssetId())
.FirstOrDefault(asset => asset != null);
if(assetId == null)
return null;
try
{
var name = tx.Outputs
.Select(txout => TxNullDataTemplate.Instance.ExtractScriptPubKeyParameters(txout.ScriptPubKey))
.Where(data => data != null && data.Length == 1)
.Select(data => Encoding.UTF8.GetString(data.First()))
.FirstOrDefault();
if(name == null)
return null;
return new NamedIssuance() { Name = name, AssetId = assetId };
}
catch { return null; }
}
}
}

View File

@ -0,0 +1,49 @@
using System;
using Microsoft.Extensions.Logging;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using ElementsExplorer.Configuration;
using ElementsExplorer.Logging;
using NBitcoin.Protocol;
namespace ElementsExplorer
{
public class Program
{
public static void Main(string[] args)
{
Logs.Configure(new FuncLoggerFactory(i => new CustomerConsoleLogger(i, (a, b) => true, false)));
IWebHost host = null;
try
{
var conf = new ExplorerConfiguration();
conf.LoadArgs(args);
using(var runtime = conf.CreateRuntime())
{
runtime.StartNodeListener(conf.StartHeight);
host = runtime.CreateWebHost();
host.Run();
}
}
catch(ConfigException ex)
{
if(!string.IsNullOrEmpty(ex.Message))
Logs.Configuration.LogError(ex.Message);
}
catch(Exception exception)
{
Logs.Explorer.LogError("Exception thrown while running the server");
Logs.Explorer.LogError(exception.ToString());
}
finally
{
if(host != null)
host.Dispose();
}
}
}
}

View File

@ -0,0 +1,424 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace ElementsExplorer.Properties {
using System;
using System.Reflection;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// This class was auto-generated by the StronglyTypedResourceBuilder
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Resources {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal Resources() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("ElementsExplorer.Properties.Resources", typeof(Resources).GetTypeInfo().Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
/// <summary>
/// Looks up a localized string similar to Array lengths must be the same..
/// </summary>
internal static string Arg_ArrayLengthsDiffer {
get {
return ResourceManager.GetString("Arg_ArrayLengthsDiffer", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Destination array is not long enough to copy all the items in the collection. Check array index and length..
/// </summary>
internal static string Arg_ArrayPlusOffTooSmall {
get {
return ResourceManager.GetString("Arg_ArrayPlusOffTooSmall", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Only supported array types for CopyTo on BitArrays are Boolean[], Int32[] and Byte[]..
/// </summary>
internal static string Arg_BitArrayTypeUnsupported {
get {
return ResourceManager.GetString("Arg_BitArrayTypeUnsupported", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to HashSet capacity is too big..
/// </summary>
internal static string Arg_HSCapacityOverflow {
get {
return ResourceManager.GetString("Arg_HSCapacityOverflow", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Hashtable&apos;s capacity overflowed and went negative. Check load factor, capacity and the current size of the table..
/// </summary>
internal static string Arg_HTCapacityOverflow {
get {
return ResourceManager.GetString("Arg_HTCapacityOverflow", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Insufficient space in the target location to copy the information..
/// </summary>
internal static string Arg_InsufficientSpace {
get {
return ResourceManager.GetString("Arg_InsufficientSpace", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Multi dimension array is not supported on this operation..
/// </summary>
internal static string Arg_MultiRank {
get {
return ResourceManager.GetString("Arg_MultiRank", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The lower bound of target array must be zero..
/// </summary>
internal static string Arg_NonZeroLowerBound {
get {
return ResourceManager.GetString("Arg_NonZeroLowerBound", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Only single dimensional arrays are supported for the requested action..
/// </summary>
internal static string Arg_RankMultiDimNotSupported {
get {
return ResourceManager.GetString("Arg_RankMultiDimNotSupported", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The value &apos;{0}&apos; is not of type &apos;{1}&apos; and cannot be used in this generic collection..
/// </summary>
internal static string Arg_WrongType {
get {
return ResourceManager.GetString("Arg_WrongType", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to An item with the same key has already been added..
/// </summary>
internal static string Argument_AddingDuplicate {
get {
return ResourceManager.GetString("Argument_AddingDuplicate", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The input array length must not exceed Int32.MaxValue / {0}. Otherwise BitArray.Length would exceed Int32.MaxValue..
/// </summary>
internal static string Argument_ArrayTooLarge {
get {
return ResourceManager.GetString("Argument_ArrayTooLarge", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to At least one object must implement IComparable..
/// </summary>
internal static string Argument_ImplementIComparable {
get {
return ResourceManager.GetString("Argument_ImplementIComparable", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Type of argument is not compatible with the generic comparer..
/// </summary>
internal static string Argument_InvalidArgumentForComparison {
get {
return ResourceManager.GetString("Argument_InvalidArgumentForComparison", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Target array type is not compatible with the type of items in the collection..
/// </summary>
internal static string Argument_InvalidArrayType {
get {
return ResourceManager.GetString("Argument_InvalidArrayType", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Offset and length were out of bounds for the array or count is greater than the number of elements from index to the end of the source collection..
/// </summary>
internal static string Argument_InvalidOffLen {
get {
return ResourceManager.GetString("Argument_InvalidOffLen", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Larger than collection size..
/// </summary>
internal static string ArgumentOutOfRange_BiggerThanCollection {
get {
return ResourceManager.GetString("ArgumentOutOfRange_BiggerThanCollection", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Count must be positive and count must refer to a location within the string/array/collection..
/// </summary>
internal static string ArgumentOutOfRange_Count {
get {
return ResourceManager.GetString("ArgumentOutOfRange_Count", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Index was out of range. Must be non-negative and less than the size of the collection..
/// </summary>
internal static string ArgumentOutOfRange_Index {
get {
return ResourceManager.GetString("ArgumentOutOfRange_Index", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Index must be within the bounds of the List..
/// </summary>
internal static string ArgumentOutOfRange_ListInsert {
get {
return ResourceManager.GetString("ArgumentOutOfRange_ListInsert", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Non-negative number required..
/// </summary>
internal static string ArgumentOutOfRange_NeedNonNegNum {
get {
return ResourceManager.GetString("ArgumentOutOfRange_NeedNonNegNum", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Non-negative number required..
/// </summary>
internal static string ArgumentOutOfRange_NeedNonNegNumRequired {
get {
return ResourceManager.GetString("ArgumentOutOfRange_NeedNonNegNumRequired", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to capacity was less than the current size..
/// </summary>
internal static string ArgumentOutOfRange_SmallCapacity {
get {
return ResourceManager.GetString("ArgumentOutOfRange_SmallCapacity", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Destination array is not long enough to copy all the items in the collection. Check array index and length..
/// </summary>
internal static string CopyTo_ArgumentsTooSmall {
get {
return ResourceManager.GetString("CopyTo_ArgumentsTooSmall", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The specified TValueCollection creates collections that have IsReadOnly set to true by default. TValueCollection must be a mutable ICollection..
/// </summary>
internal static string Create_TValueCollectionReadOnly {
get {
return ResourceManager.GetString("Create_TValueCollectionReadOnly", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The LinkedList node does not belong to current LinkedList..
/// </summary>
internal static string ExternalLinkedListNode {
get {
return ResourceManager.GetString("ExternalLinkedListNode", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Index {0} is out of range..
/// </summary>
internal static string IndexOutOfRange {
get {
return ResourceManager.GetString("IndexOutOfRange", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Target array type is not compatible with the type of items in the collection..
/// </summary>
internal static string Invalid_Array_Type {
get {
return ResourceManager.GetString("Invalid_Array_Type", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Queue empty..
/// </summary>
internal static string InvalidOperation_EmptyQueue {
get {
return ResourceManager.GetString("InvalidOperation_EmptyQueue", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Stack empty..
/// </summary>
internal static string InvalidOperation_EmptyStack {
get {
return ResourceManager.GetString("InvalidOperation_EmptyStack", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Enumeration already finished..
/// </summary>
internal static string InvalidOperation_EnumEnded {
get {
return ResourceManager.GetString("InvalidOperation_EnumEnded", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Collection was modified; enumeration operation may not execute..
/// </summary>
internal static string InvalidOperation_EnumFailedVersion {
get {
return ResourceManager.GetString("InvalidOperation_EnumFailedVersion", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Enumeration has not started. Call MoveNext..
/// </summary>
internal static string InvalidOperation_EnumNotStarted {
get {
return ResourceManager.GetString("InvalidOperation_EnumNotStarted", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Enumeration has either not started or has already finished..
/// </summary>
internal static string InvalidOperation_EnumOpCantHappen {
get {
return ResourceManager.GetString("InvalidOperation_EnumOpCantHappen", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The LinkedList is empty..
/// </summary>
internal static string LinkedListEmpty {
get {
return ResourceManager.GetString("LinkedListEmpty", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The LinkedList node already belongs to a LinkedList..
/// </summary>
internal static string LinkedListNodeIsAttached {
get {
return ResourceManager.GetString("LinkedListNodeIsAttached", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Mutating a key collection derived from a dictionary is not allowed..
/// </summary>
internal static string NotSupported_KeyCollectionSet {
get {
return ResourceManager.GetString("NotSupported_KeyCollectionSet", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to This operation is not supported on SortedList nested types because they require modifying the original SortedList..
/// </summary>
internal static string NotSupported_SortedListNestedWrite {
get {
return ResourceManager.GetString("NotSupported_SortedListNestedWrite", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Mutating a value collection derived from a dictionary is not allowed..
/// </summary>
internal static string NotSupported_ValueCollectionSet {
get {
return ResourceManager.GetString("NotSupported_ValueCollectionSet", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The collection is read-only.
/// </summary>
internal static string ReadOnly_Modification {
get {
return ResourceManager.GetString("ReadOnly_Modification", resourceCulture);
}
}
}
}

View File

@ -0,0 +1,239 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="ArgumentOutOfRange_BiggerThanCollection" xml:space="preserve">
<value>Larger than collection size.</value>
</data>
<data name="ArgumentOutOfRange_Count" xml:space="preserve">
<value>Count must be positive and count must refer to a location within the string/array/collection.</value>
</data>
<data name="ArgumentOutOfRange_Index" xml:space="preserve">
<value>Index was out of range. Must be non-negative and less than the size of the collection.</value>
</data>
<data name="ArgumentOutOfRange_ListInsert" xml:space="preserve">
<value>Index must be within the bounds of the List.</value>
</data>
<data name="ArgumentOutOfRange_NeedNonNegNum" xml:space="preserve">
<value>Non-negative number required.</value>
</data>
<data name="ArgumentOutOfRange_NeedNonNegNumRequired" xml:space="preserve">
<value>Non-negative number required.</value>
</data>
<data name="ArgumentOutOfRange_SmallCapacity" xml:space="preserve">
<value>capacity was less than the current size.</value>
</data>
<data name="Argument_AddingDuplicate" xml:space="preserve">
<value>An item with the same key has already been added.</value>
</data>
<data name="Argument_ArrayTooLarge" xml:space="preserve">
<value>The input array length must not exceed Int32.MaxValue / {0}. Otherwise BitArray.Length would exceed Int32.MaxValue.</value>
</data>
<data name="Argument_ImplementIComparable" xml:space="preserve">
<value>At least one object must implement IComparable.</value>
</data>
<data name="Argument_InvalidArgumentForComparison" xml:space="preserve">
<value>Type of argument is not compatible with the generic comparer.</value>
</data>
<data name="Argument_InvalidArrayType" xml:space="preserve">
<value>Target array type is not compatible with the type of items in the collection.</value>
</data>
<data name="Argument_InvalidOffLen" xml:space="preserve">
<value>Offset and length were out of bounds for the array or count is greater than the number of elements from index to the end of the source collection.</value>
</data>
<data name="Arg_ArrayLengthsDiffer" xml:space="preserve">
<value>Array lengths must be the same.</value>
</data>
<data name="Arg_ArrayPlusOffTooSmall" xml:space="preserve">
<value>Destination array is not long enough to copy all the items in the collection. Check array index and length.</value>
</data>
<data name="Arg_BitArrayTypeUnsupported" xml:space="preserve">
<value>Only supported array types for CopyTo on BitArrays are Boolean[], Int32[] and Byte[].</value>
</data>
<data name="Arg_HSCapacityOverflow" xml:space="preserve">
<value>HashSet capacity is too big.</value>
</data>
<data name="Arg_HTCapacityOverflow" xml:space="preserve">
<value>Hashtable's capacity overflowed and went negative. Check load factor, capacity and the current size of the table.</value>
</data>
<data name="Arg_InsufficientSpace" xml:space="preserve">
<value>Insufficient space in the target location to copy the information.</value>
</data>
<data name="Arg_MultiRank" xml:space="preserve">
<value>Multi dimension array is not supported on this operation.</value>
</data>
<data name="Arg_NonZeroLowerBound" xml:space="preserve">
<value>The lower bound of target array must be zero.</value>
</data>
<data name="Arg_RankMultiDimNotSupported" xml:space="preserve">
<value>Only single dimensional arrays are supported for the requested action.</value>
</data>
<data name="Arg_WrongType" xml:space="preserve">
<value>The value '{0}' is not of type '{1}' and cannot be used in this generic collection.</value>
</data>
<data name="CopyTo_ArgumentsTooSmall" xml:space="preserve">
<value>Destination array is not long enough to copy all the items in the collection. Check array index and length.</value>
</data>
<data name="Create_TValueCollectionReadOnly" xml:space="preserve">
<value>The specified TValueCollection creates collections that have IsReadOnly set to true by default. TValueCollection must be a mutable ICollection.</value>
</data>
<data name="ExternalLinkedListNode" xml:space="preserve">
<value>The LinkedList node does not belong to current LinkedList.</value>
</data>
<data name="IndexOutOfRange" xml:space="preserve">
<value>Index {0} is out of range.</value>
</data>
<data name="InvalidOperation_EmptyQueue" xml:space="preserve">
<value>Queue empty.</value>
</data>
<data name="InvalidOperation_EmptyStack" xml:space="preserve">
<value>Stack empty.</value>
</data>
<data name="InvalidOperation_EnumEnded" xml:space="preserve">
<value>Enumeration already finished.</value>
</data>
<data name="InvalidOperation_EnumFailedVersion" xml:space="preserve">
<value>Collection was modified; enumeration operation may not execute.</value>
</data>
<data name="InvalidOperation_EnumNotStarted" xml:space="preserve">
<value>Enumeration has not started. Call MoveNext.</value>
</data>
<data name="InvalidOperation_EnumOpCantHappen" xml:space="preserve">
<value>Enumeration has either not started or has already finished.</value>
</data>
<data name="Invalid_Array_Type" xml:space="preserve">
<value>Target array type is not compatible with the type of items in the collection.</value>
</data>
<data name="LinkedListEmpty" xml:space="preserve">
<value>The LinkedList is empty.</value>
</data>
<data name="LinkedListNodeIsAttached" xml:space="preserve">
<value>The LinkedList node already belongs to a LinkedList.</value>
</data>
<data name="NotSupported_KeyCollectionSet" xml:space="preserve">
<value>Mutating a key collection derived from a dictionary is not allowed.</value>
</data>
<data name="NotSupported_SortedListNestedWrite" xml:space="preserve">
<value>This operation is not supported on SortedList nested types because they require modifying the original SortedList.</value>
</data>
<data name="NotSupported_ValueCollectionSet" xml:space="preserve">
<value>Mutating a value collection derived from a dictionary is not allowed.</value>
</data>
<data name="ReadOnly_Modification" xml:space="preserve">
<value>The collection is read-only</value>
</data>
</root>

View File

@ -0,0 +1,346 @@
using DBreeze;
using System.Linq;
using NBitcoin;
using NBitcoin.Crypto;
using NBitcoin.JsonConverters;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Text;
namespace ElementsExplorer
{
public class TrackedTransaction
{
public uint256 BlockHash
{
get; set;
}
public Transaction Transaction
{
get; set;
}
internal string GetRowKey()
{
return $"{Transaction.GetHash()}:{BlockHash}";
}
}
public class InsertTransaction
{
public ExtPubKey PubKey
{
get; set;
}
public TrackedTransaction TrackedTransaction
{
get; set;
}
}
public class KeyInformation
{
public KeyInformation()
{
}
public KeyInformation(ExtPubKey pubKey) : this(pubKey, null)
{
}
public KeyInformation(ExtPubKey pubKey, KeyPath keyPath)
{
KeyPath = keyPath;
RootKey = pubKey.ToBytes();
}
public byte[] RootKey
{
get; set;
}
public KeyPath KeyPath
{
get; set;
}
}
public class Repository : IDisposable
{
DBreezeEngine _Engine;
public Repository(string directory, bool caching)
{
if(!Directory.Exists(directory))
Directory.CreateDirectory(directory);
_Engine = new DBreezeEngine(directory);
Caching = caching;
if(caching)
{
using(var tx = _Engine.GetTransaction())
{
tx.ValuesLazyLoadingIsOn = false;
foreach(var existingRow in tx.SelectForward<string, byte[]>("KeysByScript"))
{
if(existingRow == null || !existingRow.Exists)
continue;
_Cache.TryAdd(new ScriptId(existingRow.Key), Serializer.ToObject<KeyInformation>(Unzip(existingRow.Value)));
}
}
}
}
public BlockLocator GetIndexProgress()
{
using(var tx = _Engine.GetTransaction())
{
tx.ValuesLazyLoadingIsOn = false;
var existingRow = tx.Select<string, byte[]>("IndexProgress", "");
if(existingRow == null || !existingRow.Exists)
return null;
BlockLocator locator = new BlockLocator();
locator.FromBytes(existingRow.Value);
return locator;
}
}
public void SetIndexProgress(BlockLocator locator)
{
using(var tx = _Engine.GetTransaction())
{
if(locator == null)
tx.RemoveKey("IndexProgress", "");
else
tx.Insert("IndexProgress", "", locator.ToBytes());
tx.Commit();
}
}
public KeyInformation GetKeyInformation(ExtPubKey pubKey, Script script)
{
var info = GetKeyInformation(script);
if(info == null || !pubKey.ToBytes().SequenceEqual(info.RootKey))
return null;
return info;
}
public KeyInformation GetKeyInformation(Script script)
{
if(Caching)
{
KeyInformation v;
_Cache.TryGetValue(script.Hash, out v);
return v;
}
using(var tx = _Engine.GetTransaction())
{
tx.ValuesLazyLoadingIsOn = false;
var existingRow = tx.Select<string, byte[]>("KeysByScript", script.Hash.ToString());
if(existingRow == null || !existingRow.Exists)
return null;
var keyInfo = Serializer.ToObject<KeyInformation>(Unzip(existingRow.Value));
return keyInfo;
}
}
private byte[] Zip(string unzipped)
{
MemoryStream ms = new MemoryStream();
using(GZipStream gzip = new GZipStream(ms, CompressionMode.Compress))
{
StreamWriter writer = new StreamWriter(gzip, Encoding.UTF8);
writer.Write(unzipped);
writer.Flush();
}
return ms.ToArray();
}
private string Unzip(byte[] bytes)
{
MemoryStream ms = new MemoryStream(bytes);
using(GZipStream gzip = new GZipStream(ms, CompressionMode.Decompress))
{
StreamReader reader = new StreamReader(gzip, Encoding.UTF8);
var unzipped = reader.ReadToEnd();
return unzipped;
}
}
public const int MinGap = 20;
readonly KeyPath[] TrackedPathes = new KeyPath[] { new KeyPath("0"), new KeyPath("1") };
public void MarkAsUsed(KeyInformation info)
{
var tableName = $"U-{Hashes.Hash160(info.RootKey).ToString()}";
var highestUsedIndexes = new Dictionary<KeyPath, long>();
var highestUnusedIndexes = new Dictionary<KeyPath, long>();
using(var tx = _Engine.GetTransaction())
{
tx.ValuesLazyLoadingIsOn = false;
if(info.KeyPath != null)
tx.Insert(tableName, info.KeyPath.ToString(), true);
foreach(var row in tx.SelectForward<string, bool>(tableName))
{
if(info.KeyPath == null)
return; //Early exit, no need to create the first keys, it has already been done
var highestIndexes = row.Value ? highestUsedIndexes : highestUnusedIndexes;
KeyPath k = new KeyPath(row.Key);
long highestKey;
if(!highestIndexes.TryGetValue(k.Parent, out highestKey))
highestKey = -1;
highestKey = Math.Max(highestKey, k.Indexes.Last());
highestIndexes.AddOrReplace(k.Parent, highestKey);
}
foreach(var trackedPath in TrackedPathes)
{
ExtPubKey pathPubKey = null;
long highestUnused;
if(!highestUnusedIndexes.TryGetValue(trackedPath, out highestUnused))
highestUnused = -1;
long highestUsed;
if(!highestUsedIndexes.TryGetValue(trackedPath, out highestUsed))
highestUsed = -1;
KeyPath highestUnusedPath = null;
while(highestUnused - highestUsed < MinGap)
{
if(highestUnused == uint.MaxValue)
break;
highestUnused++;
highestUnusedPath = trackedPath.Derive((uint)highestUnused);
pathPubKey = pathPubKey ?? new ExtPubKey(info.RootKey).Derive(trackedPath);
var scriptPubKey = pathPubKey.Derive((uint)highestUnused).PubKey.Hash.ScriptPubKey;
InsertKeyInformation(tx, scriptPubKey, new KeyInformation()
{
KeyPath = trackedPath.Derive((uint)highestUnused),
RootKey = info.RootKey
});
}
if(highestUnusedPath != null)
{
byte[] inserted;
bool existed;
tx.Insert(tableName, highestUnusedPath.ToString(), false, out inserted, out existed, dontUpdateIfExists: true);
}
}
tx.Commit();
}
}
public string GetAssetName(uint256 assetId)
{
using(var tx = _Engine.GetTransaction())
{
var row = tx.Select<string, string>("AssetNameById", assetId.ToString());
if(row == null || !row.Exists)
return null;
return row.Value;
}
}
public enum SetNameResult
{
AssetNameAlreadyExist,
AssetIdAlreadyClaimedAName,
Success
}
public SetNameResult SetAssetName(NamedIssuance issuance)
{
using(var tx = _Engine.GetTransaction())
{
byte[] a;
bool b;
tx.Insert("AssetNameById", issuance.AssetId.ToString(), issuance.Name, out a, out b, true);
if(b)
return SetNameResult.AssetIdAlreadyClaimedAName;
tx.Insert("AssetIdByName", issuance.Name, issuance.AssetId.ToString(), out a, out b, true);
if(b)
return SetNameResult.AssetNameAlreadyExist;
tx.Commit();
return SetNameResult.Success;
}
}
public TrackedTransaction[] GetTransactions(BitcoinExtPubKey pubkey)
{
var tableName = $"T-{Hashes.Hash160(pubkey.ToBytes()).ToString()}";
var result = new List<TrackedTransaction>();
using(var tx = _Engine.GetTransaction())
{
foreach(var row in tx.SelectForward<string, byte[]>(tableName))
{
if(row == null || !row.Exists)
continue;
var transaction = new Transaction(row.Value);
transaction.CacheHashes();
var blockHash = row.Key.Split(':')[1];
var tracked = new TrackedTransaction();
if(blockHash.Length != 0)
tracked.BlockHash = new uint256(blockHash);
tracked.Transaction = transaction;
result.Add(tracked);
}
}
return result.ToArray();
}
public void InsertTransactions(InsertTransaction[] transactions)
{
if(transactions.Length == 0)
return;
var groups = transactions.GroupBy(i => $"T-{Hashes.Hash160(i.PubKey.ToBytes()).ToString()}");
using(var tx = _Engine.GetTransaction())
{
foreach(var group in groups)
{
foreach(var value in group)
tx.Insert(group.Key, value.TrackedTransaction.GetRowKey(), value.TrackedTransaction.Transaction.ToBytes());
}
tx.Commit();
}
}
public void CleanTransactions(ExtPubKey pubkey, List<TrackedTransaction> cleanList)
{
if(cleanList == null || cleanList.Count == 0)
return;
var tableName = $"T-{Hashes.Hash160(pubkey.ToBytes()).ToString()}";
using(var tx = _Engine.GetTransaction())
{
foreach(var tracked in cleanList)
{
tx.RemoveKey(tableName, tracked.GetRowKey());
}
tx.Commit();
}
}
public bool Caching
{
get;
private set;
}
ConcurrentDictionary<ScriptId, KeyInformation> _Cache = new ConcurrentDictionary<ScriptId, KeyInformation>();
private void InsertKeyInformation(DBreeze.Transactions.Transaction tx, Script scriptPubKey, KeyInformation info)
{
if(Caching)
_Cache.TryAdd(scriptPubKey.Hash, info);
tx.Insert("KeysByScript", scriptPubKey.Hash.ToString(), Zip(Serializer.ToString(info)));
}
public void Dispose()
{
_Engine.Dispose();
}
}
}

View File

@ -0,0 +1,75 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.AspNetCore.Mvc;
using NBitcoin.JsonConverters;
using Microsoft.Extensions.Options;
using Microsoft.Net.Http.Headers;
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.AspNetCore.Http.Features;
namespace ElementsExplorer
{
public class Startup
{
public Startup(IHostingEnvironment env)
{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddEnvironmentVariables();
Configuration = builder.Build();
}
public IConfigurationRoot Configuration
{
get;
}
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IObjectModelValidator, NoObjectModelValidator>();
services.AddMvcCore()
.AddJsonFormatters()
.AddFormatterMappings();
}
internal class NoObjectModelValidator : IObjectModelValidator
{
public void Validate(ActionContext actionContext, ValidationStateDictionary validationState, string prefix, object model)
{
}
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, IServiceProvider serviceProvider)
{
var logging = new FilterLoggerSettings();
logging.Add("Microsoft.AspNetCore.Hosting.Internal.WebHost", LogLevel.Error);
logging.Add("Microsoft.AspNetCore.Mvc", LogLevel.Error);
logging.Add("Microsoft.AspNetCore.Server.Kestrel", LogLevel.Error);
loggerFactory
.WithFilter(logging)
.AddConsole();
app.UseDeveloperExceptionPage();
app.UseMvc();
var config = serviceProvider.GetService<ExplorerRuntime>();
var options = GetMVCOptions(serviceProvider);
Serializer.RegisterFrontConverters(options.SerializerSettings, config.Network);
}
private static MvcJsonOptions GetMVCOptions(IServiceProvider serviceProvider)
{
return serviceProvider.GetRequiredService<IOptions<MvcJsonOptions>>().Value;
}
}
}

View File

@ -0,0 +1,371 @@
using NBitcoin;
using System.Linq;
using NBitcoin.Protocol;
using System;
using System.Collections.Generic;
using System.Text;
using NBitcoin.Crypto;
using System.IO;
namespace ElementsExplorer
{
public class UTXOChanges : IBitcoinSerializable
{
public void ReadWrite(BitcoinStream stream)
{
stream.ReadWrite(ref _Confirmed);
stream.ReadWrite(ref _Unconfirmed);
}
UTXOChange _Unconfirmed = new UTXOChange();
public UTXOChange Unconfirmed
{
get
{
return _Unconfirmed;
}
set
{
_Unconfirmed = value;
}
}
UTXOChange _Confirmed = new UTXOChange();
public UTXOChange Confirmed
{
get
{
return _Confirmed;
}
set
{
_Confirmed = value;
}
}
public bool HasChanges
{
get
{
return Confirmed.HasChanges || Unconfirmed.HasChanges;
}
}
}
public class UTXOChange : IBitcoinSerializable
{
byte _Reset;
public bool Reset
{
get
{
return _Reset == 1;
}
set
{
_Reset = (byte)(value ? 1 : 0);
}
}
uint256 _Hash = uint256.Zero;
public uint256 Hash
{
get
{
return _Hash;
}
set
{
_Hash = value;
}
}
List<UTXO> _UTXOs = new List<UTXO>();
public List<UTXO> UTXOs
{
get
{
return _UTXOs;
}
set
{
_UTXOs = value;
}
}
List<OutPoint> _SpentOutpoints = new List<OutPoint>();
public List<OutPoint> SpentOutpoints
{
get
{
return _SpentOutpoints;
}
set
{
_SpentOutpoints = value;
}
}
public bool HasChanges
{
get
{
return Reset || UTXOs.Count != 0 || SpentOutpoints.Count != 0;
}
}
public void ReadWrite(BitcoinStream stream)
{
stream.ReadWrite(ref _Reset);
stream.ReadWrite(ref _Hash);
stream.ReadWrite(ref _UTXOs);
stream.ReadWrite(ref _SpentOutpoints);
}
public void LoadChanges(Transaction tx, Func<Script, KeyPath> getKeyPath)
{
if(tx == null)
throw new ArgumentNullException("tx");
tx.CacheHashes();
var existingUTXOs = new HashSet<OutPoint>(UTXOs.Select(u => u.Outpoint));
var removedUTXOs = new HashSet<OutPoint>(SpentOutpoints);
foreach(var input in tx.Inputs)
{
if(existingUTXOs.Remove(input.PrevOut))
removedUTXOs.Add(input.PrevOut);
}
int index = -1;
foreach(var output in tx.Outputs)
{
index++;
if(!existingUTXOs.Contains(new OutPoint(tx.GetHash(), index)))
{
var keyPath = getKeyPath(output.ScriptPubKey);
if(keyPath != null)
{
var outpoint = new OutPoint(tx.GetHash(), index);
UTXOs.Add(new UTXO(outpoint, output, keyPath));
existingUTXOs.Add(outpoint);
}
}
}
UTXOs = UTXOs.Where(u => existingUTXOs.Contains(u.Outpoint)).ToList();
SpentOutpoints = removedUTXOs.ToList();
}
public bool HasConflict(Transaction tx)
{
var existingUTXOs = new HashSet<OutPoint>(UTXOs.Select(u => u.Outpoint));
var spentOutpoints = new HashSet<OutPoint>(SpentOutpoints);
//If there is double spending
foreach(var input in tx.Inputs)
{
if(spentOutpoints.Contains(input.PrevOut))
return true;
spentOutpoints.Add(input.PrevOut);
}
var index = -1;
foreach(var output in tx.Outputs)
{
index++;
var outpoint = new OutPoint(tx.GetHash(), index);
if(existingUTXOs.Contains(outpoint) || spentOutpoints.Contains(outpoint))
return true;
existingUTXOs.Add(outpoint);
}
return false;
}
public UTXOChange Diff(UTXOChange previousChange)
{
var previousUTXOs = previousChange.UTXOs.ToDictionary(u => u.Outpoint);
var currentUTXOs = UTXOs.ToDictionary(u => u.Outpoint);
var deletedUTXOs = previousChange.UTXOs.Where(utxo => !currentUTXOs.ContainsKey(utxo.Outpoint));
var addedUTXOs = UTXOs.Where(utxo => !previousUTXOs.ContainsKey(utxo.Outpoint));
var diff = new UTXOChange();
diff.Hash = this.Hash;
diff.Reset = Reset;
foreach(var deleted in deletedUTXOs)
{
diff.SpentOutpoints.Add(deleted.Outpoint);
}
foreach(var added in addedUTXOs)
{
diff.UTXOs.Add(added);
}
return diff;
}
internal void Clear()
{
Reset = false;
UTXOs.Clear();
SpentOutpoints.Clear();
}
public uint256 GetHash()
{
MemoryStream ms = new MemoryStream();
BitcoinStream bs = new BitcoinStream(ms, true);
bs.ReadWrite(ref _UTXOs);
bs.ReadWrite(ref _SpentOutpoints);
return Hashes.Hash256(ms.ToArray());
}
}
public class UTXO : IBitcoinSerializable
{
public UTXO()
{
}
OutPoint _Outpoint = new OutPoint();
public OutPoint Outpoint
{
get
{
return _Outpoint;
}
set
{
_Outpoint = value;
}
}
Script _ScriptPubKey;
public Script ScriptPubKey
{
get
{
return _ScriptPubKey;
}
set
{
_ScriptPubKey = value;
}
}
ConfidentialAsset _Asset;
public ConfidentialAsset Asset
{
get
{
return _Asset;
}
set
{
_Asset = value;
}
}
ConfidentialValue _Value;
public ConfidentialValue Value
{
get
{
return _Value;
}
set
{
_Value = value;
}
}
ConfidentialNonce _Nonce;
public ConfidentialNonce Nonce
{
get
{
return _Nonce;
}
set
{
_Nonce = value;
}
}
byte[] _RangeProof;
public byte[] RangeProof
{
get
{
return _RangeProof;
}
set
{
_RangeProof = value;
}
}
byte[] _SurjectionProof;
public byte[] SurjectionProof
{
get
{
return _SurjectionProof;
}
set
{
_SurjectionProof = value;
}
}
KeyPath _KeyPath;
public UTXO(OutPoint outPoint, TxOut output, KeyPath keyPath)
{
Outpoint = outPoint;
RangeProof = output.RangeProof;
SurjectionProof = output.SurjectionProof;
Nonce = output.Nonce;
Asset = output.Asset;
Value = output.ConfidentialValue;
ScriptPubKey = output.ScriptPubKey;
KeyPath = keyPath;
}
public KeyPath KeyPath
{
get
{
return _KeyPath;
}
set
{
_KeyPath = value;
}
}
public void ReadWrite(BitcoinStream stream)
{
stream.ReadWrite(ref _Outpoint);
stream.ReadWrite(ref _ScriptPubKey);
stream.ReadWrite(ref _Asset);
stream.ReadWrite(ref _Value);
stream.ReadWrite(ref _Nonce);
stream.ReadWriteAsVarString(ref _RangeProof);
stream.ReadWriteAsVarString(ref _SurjectionProof);
uint[] indexes = _KeyPath?.Indexes ?? new uint[0];
stream.ReadWrite(ref indexes);
if(!stream.Serializing)
_KeyPath = new KeyPath(indexes);
}
}
}

34
ElementsExplorer/Utils.cs Normal file
View File

@ -0,0 +1,34 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace ElementsExplorer
{
public static class Utils
{
public static IEnumerable<T> TopologicalSort<T>(this IEnumerable<T> nodes,
Func<T, IEnumerable<T>> dependsOn)
{
List<T> result = new List<T>();
var elems = nodes.ToDictionary(node => node,
node => new HashSet<T>(dependsOn(node)));
while(elems.Count > 0)
{
var elem = elems.FirstOrDefault(x => x.Value.Count == 0);
if(elem.Key == null)
{
//cycle detected can't order
return nodes;
}
elems.Remove(elem.Key);
foreach(var selem in elems)
{
selem.Value.Remove(elem.Key);
}
result.Add(elem.Key);
}
return result;
}
}
}

10
LICENSE
View File

@ -1,4 +1,4 @@
MIT License
The MIT License (MIT)
Copyright (c) 2017 Digital Garage
@ -9,13 +9,13 @@ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

20
README.md Normal file
View File

@ -0,0 +1,20 @@
# ElementsExplorer
A minimalist block explorer for Elements
## How to run?
* Install [.NET Core](https://www.microsoft.com/net/core)
* Have an Elements instance running in regtest
```
git clone https://github.com/dgarage/ElementsExplorer
cd ElementsExplorer
git submodule init
git submodule update
dotnet restore
cd ElementsExplorer
dotnet run -regtest
```
Adapt the configuration file if elements is not using default settings