Adopt dotnet core editorconfig, big reformating

This commit is contained in:
nicolas.dorier
2017-10-27 17:53:04 +09:00
parent b10da976c7
commit 4deb7c3270
199 changed files with 66960 additions and 46532 deletions

149
.editorconfig Normal file
View File

@@ -0,0 +1,149 @@
# editorconfig.org
# top-most EditorConfig file
root = true
# Default settings:
# A newline ending every file
# Use 4 spaces as indentation
[*]
insert_final_newline = true
indent_style = space
indent_size = 4
[project.json]
indent_size = 2
# C# files
[*.cs]
# New line preferences
csharp_new_line_before_open_brace = all
csharp_new_line_before_else = true
csharp_new_line_before_catch = true
csharp_new_line_before_finally = true
csharp_new_line_before_members_in_object_initializers = true
csharp_new_line_before_members_in_anonymous_types = true
csharp_new_line_within_query_expression_clauses = true
# Indentation preferences
csharp_indent_block_contents = true
csharp_indent_braces = false
csharp_indent_case_contents = true
csharp_indent_switch_labels = true
csharp_indent_labels = flush_left
# avoid this. unless absolutely necessary
dotnet_style_qualification_for_field = false:suggestion
dotnet_style_qualification_for_property = false:suggestion
dotnet_style_qualification_for_method = false:suggestion
dotnet_style_qualification_for_event = false:suggestion
# only use var when it's obvious what the variable type is
csharp_style_var_for_built_in_types = false:none
csharp_style_var_when_type_is_apparent = false:none
csharp_style_var_elsewhere = false:suggestion
# use language keywords instead of BCL types
dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
dotnet_style_predefined_type_for_member_access = true:suggestion
# name all constant fields using PascalCase
dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields
dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style
dotnet_naming_symbols.constant_fields.applicable_kinds = field
dotnet_naming_symbols.constant_fields.required_modifiers = const
dotnet_naming_style.pascal_case_style.capitalization = pascal_case
# internal and private fields should be _camelCase
dotnet_naming_rule.camel_case_for_private_internal_fields.severity = suggestion
dotnet_naming_rule.camel_case_for_private_internal_fields.symbols = private_internal_fields
dotnet_naming_rule.camel_case_for_private_internal_fields.style = camel_case_underscore_style
dotnet_naming_symbols.private_internal_fields.applicable_kinds = field
dotnet_naming_symbols.private_internal_fields.applicable_accessibilities = private, internal
dotnet_naming_style.camel_case_underscore_style.required_prefix = _
dotnet_naming_style.camel_case_underscore_style.capitalization = camel_case
# Code style defaults
dotnet_sort_system_directives_first = true
csharp_preserve_single_line_blocks = true
csharp_preserve_single_line_statements = false
# Expression-level preferences
dotnet_style_object_initializer = true:suggestion
dotnet_style_collection_initializer = true:suggestion
dotnet_style_explicit_tuple_names = true:suggestion
dotnet_style_coalesce_expression = true:suggestion
dotnet_style_null_propagation = true:suggestion
# Expression-bodied members
csharp_style_expression_bodied_methods = false:none
csharp_style_expression_bodied_constructors = false:none
csharp_style_expression_bodied_operators = false:none
csharp_style_expression_bodied_properties = true:none
csharp_style_expression_bodied_indexers = true:none
csharp_style_expression_bodied_accessors = true:none
# Pattern matching
csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
csharp_style_inlined_variable_declaration = true:suggestion
# Null checking preferences
csharp_style_throw_expression = true:suggestion
csharp_style_conditional_delegate_call = true:suggestion
# Space preferences
csharp_space_after_cast = false
csharp_space_after_colon_in_inheritance_clause = true
csharp_space_after_comma = true
csharp_space_after_dot = false
csharp_space_after_keywords_in_control_flow_statements = true
csharp_space_after_semicolon_in_for_statement = true
csharp_space_around_binary_operators = before_and_after
csharp_space_around_declaration_statements = do_not_ignore
csharp_space_before_colon_in_inheritance_clause = true
csharp_space_before_comma = false
csharp_space_before_dot = false
csharp_space_before_open_square_brackets = false
csharp_space_before_semicolon_in_for_statement = false
csharp_space_between_empty_square_brackets = false
csharp_space_between_method_call_empty_parameter_list_parentheses = false
csharp_space_between_method_call_name_and_opening_parenthesis = false
csharp_space_between_method_call_parameter_list_parentheses = false
csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
csharp_space_between_method_declaration_name_and_open_parenthesis = false
csharp_space_between_method_declaration_parameter_list_parentheses = false
csharp_space_between_parentheses = false
csharp_space_between_square_brackets = false
# C++ Files
[*.{cpp,h,in}]
curly_bracket_next_line = true
indent_brace_style = Allman
# Xml project files
[*.{csproj,vcxproj,vcxproj.filters,proj,nativeproj,locproj}]
indent_size = 2
# Xml build files
[*.builds]
indent_size = 2
# Xml files
[*.{xml,stylecop,resx,ruleset}]
indent_size = 2
# Xml config files
[*.{props,targets,config,nuspec}]
indent_size = 2
# Shell scripts
[*.sh]
end_of_line = lf
[*.{cmd, bat}]
end_of_line = crlf

View File

@@ -30,133 +30,133 @@ using Xunit;
namespace BTCPayServer.Tests namespace BTCPayServer.Tests
{ {
public class BTCPayServerTester : IDisposable public class BTCPayServerTester : IDisposable
{ {
private string _Directory; private string _Directory;
public BTCPayServerTester(string scope) public BTCPayServerTester(string scope)
{ {
this._Directory = scope ?? throw new ArgumentNullException(nameof(scope)); this._Directory = scope ?? throw new ArgumentNullException(nameof(scope));
} }
public Uri NBXplorerUri public Uri NBXplorerUri
{ {
get; set; get; set;
} }
public string CookieFile public string CookieFile
{ {
get; set; get; set;
} }
public Uri ServerUri public Uri ServerUri
{ {
get; get;
set; set;
} }
public ExtKey HDPrivateKey public ExtKey HDPrivateKey
{ {
get; set; get; set;
} }
public string Postgres public string Postgres
{ {
get; set; get; set;
} }
IWebHost _Host; IWebHost _Host;
public int Port public int Port
{ {
get; set; get; set;
} }
public void Start() public void Start()
{ {
if(!Directory.Exists(_Directory)) if (!Directory.Exists(_Directory))
Directory.CreateDirectory(_Directory); Directory.CreateDirectory(_Directory);
HDPrivateKey = new ExtKey(); HDPrivateKey = new ExtKey();
StringBuilder config = new StringBuilder(); StringBuilder config = new StringBuilder();
config.AppendLine($"regtest=1"); config.AppendLine($"regtest=1");
config.AppendLine($"port={Port}"); config.AppendLine($"port={Port}");
config.AppendLine($"explorer.url={NBXplorerUri.AbsoluteUri}"); config.AppendLine($"explorer.url={NBXplorerUri.AbsoluteUri}");
config.AppendLine($"explorer.cookiefile={CookieFile}"); config.AppendLine($"explorer.cookiefile={CookieFile}");
config.AppendLine($"hdpubkey={HDPrivateKey.Neuter().ToString(Network.RegTest)}"); config.AppendLine($"hdpubkey={HDPrivateKey.Neuter().ToString(Network.RegTest)}");
if(Postgres != null) if (Postgres != null)
config.AppendLine($"postgres=" + Postgres); config.AppendLine($"postgres=" + Postgres);
File.WriteAllText(Path.Combine(_Directory, "settings.config"), config.ToString()); File.WriteAllText(Path.Combine(_Directory, "settings.config"), config.ToString());
ServerUri = new Uri("http://" + HostName + ":" + Port + "/"); ServerUri = new Uri("http://" + HostName + ":" + Port + "/");
var conf = new DefaultConfiguration() { Logger = Logs.LogProvider.CreateLogger("Console") }.CreateConfiguration(new[] { "--datadir", _Directory }); var conf = new DefaultConfiguration() { Logger = Logs.LogProvider.CreateLogger("Console") }.CreateConfiguration(new[] { "--datadir", _Directory });
_Host = new WebHostBuilder() _Host = new WebHostBuilder()
.UseConfiguration(conf) .UseConfiguration(conf)
.ConfigureServices(s => .ConfigureServices(s =>
{ {
s.AddSingleton<IRateProvider>(new MockRateProvider(new Rate("USD", 5000m))); s.AddSingleton<IRateProvider>(new MockRateProvider(new Rate("USD", 5000m)));
s.AddLogging(l => s.AddLogging(l =>
{ {
l.SetMinimumLevel(LogLevel.Information) l.SetMinimumLevel(LogLevel.Information)
.AddFilter("Microsoft", LogLevel.Error) .AddFilter("Microsoft", LogLevel.Error)
.AddFilter("Hangfire", LogLevel.Error) .AddFilter("Hangfire", LogLevel.Error)
.AddProvider(Logs.LogProvider); .AddProvider(Logs.LogProvider);
}); });
}) })
.UseKestrel() .UseKestrel()
.UseStartup<Startup>() .UseStartup<Startup>()
.Build(); .Build();
_Host.Start(); _Host.Start();
Runtime = (BTCPayServerRuntime)_Host.Services.GetService(typeof(BTCPayServerRuntime)); Runtime = (BTCPayServerRuntime)_Host.Services.GetService(typeof(BTCPayServerRuntime));
var watcher = (InvoiceWatcher)_Host.Services.GetService(typeof(InvoiceWatcher)); var watcher = (InvoiceWatcher)_Host.Services.GetService(typeof(InvoiceWatcher));
watcher.PollInterval = TimeSpan.FromMilliseconds(500); watcher.PollInterval = TimeSpan.FromMilliseconds(500);
} }
public BTCPayServerRuntime Runtime public BTCPayServerRuntime Runtime
{ {
get; set; get; set;
} }
public string HostName public string HostName
{ {
get; get;
internal set; internal set;
} }
public T GetService<T>() public T GetService<T>()
{ {
return _Host.Services.GetRequiredService<T>(); return _Host.Services.GetRequiredService<T>();
} }
public T GetController<T>(string userId = null) where T : Controller public T GetController<T>(string userId = null) where T : Controller
{ {
var context = new DefaultHttpContext(); var context = new DefaultHttpContext();
context.Request.Host = new HostString("127.0.0.1"); context.Request.Host = new HostString("127.0.0.1");
context.Request.Scheme = "http"; context.Request.Scheme = "http";
context.Request.Protocol = "http"; context.Request.Protocol = "http";
if(userId != null) if (userId != null)
{ {
context.User = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, userId) })); context.User = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, userId) }));
} }
var scope = (IServiceScopeFactory)_Host.Services.GetService(typeof(IServiceScopeFactory)); var scope = (IServiceScopeFactory)_Host.Services.GetService(typeof(IServiceScopeFactory));
var provider = scope.CreateScope().ServiceProvider; var provider = scope.CreateScope().ServiceProvider;
context.RequestServices = provider; context.RequestServices = provider;
var httpAccessor = provider.GetRequiredService<IHttpContextAccessor>(); var httpAccessor = provider.GetRequiredService<IHttpContextAccessor>();
httpAccessor.HttpContext = context; httpAccessor.HttpContext = context;
var controller = (T)ActivatorUtilities.CreateInstance(provider, typeof(T)); var controller = (T)ActivatorUtilities.CreateInstance(provider, typeof(T));
controller.Url = new UrlHelperMock(new Uri($"http://{HostName}:{Port}/")); controller.Url = new UrlHelperMock(new Uri($"http://{HostName}:{Port}/"));
controller.ControllerContext = new ControllerContext() controller.ControllerContext = new ControllerContext()
{ {
HttpContext = context HttpContext = context
}; };
return controller; return controller;
} }
public void Dispose() public void Dispose()
{ {
if(_Host != null) if (_Host != null)
_Host.Dispose(); _Host.Dispose();
} }
} }
} }

View File

@@ -11,65 +11,65 @@ using Microsoft.AspNetCore.Hosting.Server.Features;
namespace BTCPayServer.Tests namespace BTCPayServer.Tests
{ {
public class CustomServer : IDisposable public class CustomServer : IDisposable
{ {
TaskCompletionSource<bool> _Evt = null; TaskCompletionSource<bool> _Evt = null;
IWebHost _Host = null; IWebHost _Host = null;
CancellationTokenSource _Closed = new CancellationTokenSource(); CancellationTokenSource _Closed = new CancellationTokenSource();
public CustomServer() public CustomServer()
{ {
var port = Utils.FreeTcpPort(); var port = Utils.FreeTcpPort();
_Host = new WebHostBuilder() _Host = new WebHostBuilder()
.Configure(app => .Configure(app =>
{ {
app.Run(req => app.Run(req =>
{ {
while(_Act == null) while (_Act == null)
{ {
Thread.Sleep(10); Thread.Sleep(10);
_Closed.Token.ThrowIfCancellationRequested(); _Closed.Token.ThrowIfCancellationRequested();
} }
_Act(req); _Act(req);
_Act = null; _Act = null;
_Evt.TrySetResult(true); _Evt.TrySetResult(true);
req.Response.StatusCode = 200; req.Response.StatusCode = 200;
return Task.CompletedTask; return Task.CompletedTask;
}); });
}) })
.UseKestrel() .UseKestrel()
.UseUrls("http://127.0.0.1:" + port) .UseUrls("http://127.0.0.1:" + port)
.Build(); .Build();
_Host.Start(); _Host.Start();
} }
public Uri GetUri() public Uri GetUri()
{ {
return new Uri(_Host.ServerFeatures.Get<IServerAddressesFeature>().Addresses.First()); return new Uri(_Host.ServerFeatures.Get<IServerAddressesFeature>().Addresses.First());
} }
Action<HttpContext> _Act; Action<HttpContext> _Act;
public void ProcessNextRequest(Action<HttpContext> act) public void ProcessNextRequest(Action<HttpContext> act)
{ {
var source = new TaskCompletionSource<bool>(); var source = new TaskCompletionSource<bool>();
CancellationTokenSource cancellation = new CancellationTokenSource(20000); CancellationTokenSource cancellation = new CancellationTokenSource(20000);
cancellation.Token.Register(() => source.TrySetCanceled()); cancellation.Token.Register(() => source.TrySetCanceled());
source = new TaskCompletionSource<bool>(); source = new TaskCompletionSource<bool>();
_Evt = source; _Evt = source;
_Act = act; _Act = act;
try try
{ {
_Evt.Task.GetAwaiter().GetResult(); _Evt.Task.GetAwaiter().GetResult();
} }
catch(TaskCanceledException) catch (TaskCanceledException)
{ {
throw new Xunit.Sdk.XunitException("Callback to the webserver was expected, check if the callback url is accessible from internet"); throw new Xunit.Sdk.XunitException("Callback to the webserver was expected, check if the callback url is accessible from internet");
} }
} }
public void Dispose() public void Dispose()
{ {
_Closed.Cancel(); _Closed.Cancel();
_Host.Dispose(); _Host.Dispose();
} }
} }
} }

View File

@@ -6,84 +6,84 @@ using Xunit.Abstractions;
namespace BTCPayServer.Tests.Logging namespace BTCPayServer.Tests.Logging
{ {
public interface ILog public interface ILog
{ {
void LogInformation(string msg); void LogInformation(string msg);
} }
public class XUnitLogProvider : ILoggerProvider public class XUnitLogProvider : ILoggerProvider
{ {
ITestOutputHelper _Helper; ITestOutputHelper _Helper;
public XUnitLogProvider(ITestOutputHelper helper) public XUnitLogProvider(ITestOutputHelper helper)
{ {
_Helper = helper; _Helper = helper;
} }
public ILogger CreateLogger(string categoryName) public ILogger CreateLogger(string categoryName)
{ {
return new XUnitLog(_Helper) { Name = categoryName }; return new XUnitLog(_Helper) { Name = categoryName };
} }
public void Dispose() public void Dispose()
{ {
}
}
public class XUnitLog : ILog, ILogger, IDisposable
{
ITestOutputHelper _Helper;
public XUnitLog(ITestOutputHelper helper)
{
_Helper = helper;
}
public string Name }
{ }
get; set; public class XUnitLog : ILog, ILogger, IDisposable
} {
ITestOutputHelper _Helper;
public XUnitLog(ITestOutputHelper helper)
{
_Helper = helper;
}
public IDisposable BeginScope<TState>(TState state) public string Name
{ {
return this; get; set;
} }
public void Dispose() public IDisposable BeginScope<TState>(TState state)
{ {
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) public bool IsEnabled(LogLevel logLevel)
{ {
StringBuilder builder = new StringBuilder(); return true;
builder.Append(formatter(state, exception)); }
if(exception != null)
{
builder.AppendLine();
builder.Append(exception.ToString());
}
LogInformation(builder.ToString());
}
public void LogInformation(string msg) public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
{ {
if(msg != null) StringBuilder builder = new StringBuilder();
_Helper.WriteLine(Name + ": " + msg); builder.Append(formatter(state, exception));
} if (exception != null)
} {
public class Logs builder.AppendLine();
{ builder.Append(exception.ToString());
public static ILog Tester }
{ LogInformation(builder.ToString());
get; set; }
}
public static XUnitLogProvider LogProvider public void LogInformation(string msg)
{ {
get; if (msg != null)
set; _Helper.WriteLine(Name + ": " + msg);
} }
} }
public class Logs
{
public static ILog Tester
{
get; set;
}
public static XUnitLogProvider LogProvider
{
get;
set;
}
}
} }

View File

@@ -6,38 +6,38 @@ using Microsoft.AspNetCore.Mvc.Routing;
namespace BTCPayServer.Tests.Mocks namespace BTCPayServer.Tests.Mocks
{ {
public class UrlHelperMock : IUrlHelper public class UrlHelperMock : IUrlHelper
{ {
Uri _BaseUrl; Uri _BaseUrl;
public UrlHelperMock(Uri baseUrl) public UrlHelperMock(Uri baseUrl)
{ {
_BaseUrl = baseUrl; _BaseUrl = baseUrl;
} }
public ActionContext ActionContext => throw new NotImplementedException(); public ActionContext ActionContext => throw new NotImplementedException();
public string Action(UrlActionContext actionContext) public string Action(UrlActionContext actionContext)
{ {
return $"{_BaseUrl}mock"; return $"{_BaseUrl}mock";
} }
public string Content(string contentPath) public string Content(string contentPath)
{ {
return $"{_BaseUrl}{contentPath}"; return $"{_BaseUrl}{contentPath}";
} }
public bool IsLocalUrl(string url) public bool IsLocalUrl(string url)
{ {
return false; return false;
} }
public string Link(string routeName, object values) public string Link(string routeName, object values)
{ {
return _BaseUrl.AbsoluteUri; return _BaseUrl.AbsoluteUri;
} }
public string RouteUrl(UrlRouteContext routeContext) public string RouteUrl(UrlRouteContext routeContext)
{ {
return _BaseUrl.AbsoluteUri; return _BaseUrl.AbsoluteUri;
} }
} }
} }

View File

@@ -8,105 +8,105 @@ using Xunit;
namespace BTCPayServer.Tests namespace BTCPayServer.Tests
{ {
public class ProcessLauncher public class ProcessLauncher
{ {
string _CurrentDirectory; string _CurrentDirectory;
public string CurrentDirectory public string CurrentDirectory
{ {
get get
{ {
return _CurrentDirectory; return _CurrentDirectory;
} }
} }
public ProcessLauncher() public ProcessLauncher()
{ {
_CurrentDirectory = Directory.GetCurrentDirectory(); _CurrentDirectory = Directory.GetCurrentDirectory();
} }
public bool GoTo(string[] directories, bool createIfNotExists = false) public bool GoTo(string[] directories, bool createIfNotExists = false)
{ {
var original = _CurrentDirectory; var original = _CurrentDirectory;
foreach(var dir in directories) foreach (var dir in directories)
{ {
var newDirectory = Path.Combine(_CurrentDirectory, dir); var newDirectory = Path.Combine(_CurrentDirectory, dir);
if(!Directory.Exists(newDirectory)) if (!Directory.Exists(newDirectory))
{ {
if(createIfNotExists) if (createIfNotExists)
Directory.CreateDirectory(newDirectory); Directory.CreateDirectory(newDirectory);
else else
{ {
_CurrentDirectory = original; _CurrentDirectory = original;
return false; return false;
} }
} }
_CurrentDirectory = newDirectory; _CurrentDirectory = newDirectory;
} }
return true; return true;
} }
Stack<string> _Directories = new Stack<string>(); Stack<string> _Directories = new Stack<string>();
public void PushDirectory() public void PushDirectory()
{ {
_Directories.Push(_CurrentDirectory); _Directories.Push(_CurrentDirectory);
} }
public void PopDirectory() public void PopDirectory()
{ {
_CurrentDirectory = _Directories.Pop(); _CurrentDirectory = _Directories.Pop();
} }
public bool GoTo(string directory, bool createIfNotExists = false) public bool GoTo(string directory, bool createIfNotExists = false)
{ {
return GoTo(new[] { directory }, createIfNotExists); return GoTo(new[] { directory }, createIfNotExists);
} }
public void Run(string processName, string args) public void Run(string processName, string args)
{ {
Start(processName, args).WaitForExit(); Start(processName, args).WaitForExit();
} }
public Process Start(string processName, string args) public Process Start(string processName, string args)
{ {
Logs.Tester.LogInformation($"Running [{processName} {args}] from {_CurrentDirectory}"); Logs.Tester.LogInformation($"Running [{processName} {args}] from {_CurrentDirectory}");
StringBuilder builder = new StringBuilder(); StringBuilder builder = new StringBuilder();
var process = new Process() var process = new Process()
{ {
StartInfo = new ProcessStartInfo StartInfo = new ProcessStartInfo
{ {
WorkingDirectory = _CurrentDirectory, WorkingDirectory = _CurrentDirectory,
FileName = processName, FileName = processName,
Arguments = args, Arguments = args,
RedirectStandardOutput = true, RedirectStandardOutput = true,
RedirectStandardError = true RedirectStandardError = true
} }
}; };
try try
{ {
process.OutputDataReceived += (s, e) => process.OutputDataReceived += (s, e) =>
{ {
Logs.Tester.LogInformation(e.Data); Logs.Tester.LogInformation(e.Data);
}; };
process.ErrorDataReceived += (s, e) => process.ErrorDataReceived += (s, e) =>
{ {
Logs.Tester.LogInformation(e.Data); Logs.Tester.LogInformation(e.Data);
}; };
process.Start(); process.Start();
process.BeginErrorReadLine(); process.BeginErrorReadLine();
process.BeginOutputReadLine(); process.BeginOutputReadLine();
} }
catch(Exception ex) { throw new Exception($"You need to install {processName} for this test (info : {ex.Message})"); } catch (Exception ex) { throw new Exception($"You need to install {processName} for this test (info : {ex.Message})"); }
return process; return process;
} }
public void AssertExists(string file) public void AssertExists(string file)
{ {
var path = Path.Combine(_CurrentDirectory, file); var path = Path.Combine(_CurrentDirectory, file);
Assert.True(File.Exists(path), $"The file {path} should exist"); Assert.True(File.Exists(path), $"The file {path} should exist");
} }
public bool Exists(string file) public bool Exists(string file)
{ {
var path = Path.Combine(_CurrentDirectory, file); var path = Path.Combine(_CurrentDirectory, file);
return File.Exists(path); return File.Exists(path);
} }
} }
} }

View File

@@ -1,7 +1,7 @@
{ {
"profiles": { "profiles": {
"BTCPayServer.Tests": { "BTCPayServer.Tests": {
"commandName": "Project" "commandName": "Project"
}
} }
} }
}

View File

@@ -19,218 +19,218 @@ using System.Threading;
namespace BTCPayServer.Tests namespace BTCPayServer.Tests
{ {
public class ServerTester : IDisposable public class ServerTester : IDisposable
{ {
public static ServerTester Create([CallerMemberNameAttribute]string scope = null) public static ServerTester Create([CallerMemberNameAttribute]string scope = null)
{ {
return new ServerTester(scope); return new ServerTester(scope);
} }
string _Directory; string _Directory;
public ServerTester(string scope) public ServerTester(string scope)
{ {
_Directory = scope; _Directory = scope;
} }
public bool Dockerized public bool Dockerized
{ {
get; set; get; set;
} }
public void Start() public void Start()
{ {
if(Directory.Exists(_Directory)) if (Directory.Exists(_Directory))
Utils.DeleteDirectory(_Directory); Utils.DeleteDirectory(_Directory);
if(!Directory.Exists(_Directory)) if (!Directory.Exists(_Directory))
Directory.CreateDirectory(_Directory); Directory.CreateDirectory(_Directory);
FakeCallback = bool.Parse(GetEnvironment("TESTS_FAKECALLBACK", "true")); FakeCallback = bool.Parse(GetEnvironment("TESTS_FAKECALLBACK", "true"));
ExplorerNode = new RPCClient(RPCCredentialString.Parse(GetEnvironment("TESTS_RPCCONNECTION", "server=http://127.0.0.1:43782;ceiwHEbqWI83:DwubwWsoo3")), Network); ExplorerNode = new RPCClient(RPCCredentialString.Parse(GetEnvironment("TESTS_RPCCONNECTION", "server=http://127.0.0.1:43782;ceiwHEbqWI83:DwubwWsoo3")), Network);
ExplorerClient = new ExplorerClient(Network, new Uri(GetEnvironment("TESTS_NBXPLORERURL", "http://127.0.0.1:32838/"))); ExplorerClient = new ExplorerClient(Network, new Uri(GetEnvironment("TESTS_NBXPLORERURL", "http://127.0.0.1:32838/")));
PayTester = new BTCPayServerTester(Path.Combine(_Directory, "pay")) PayTester = new BTCPayServerTester(Path.Combine(_Directory, "pay"))
{ {
NBXplorerUri = ExplorerClient.Address, NBXplorerUri = ExplorerClient.Address,
Postgres = GetEnvironment("TESTS_POSTGRES", "User ID=postgres;Host=127.0.0.1;Port=39372;Database=btcpayserver") Postgres = GetEnvironment("TESTS_POSTGRES", "User ID=postgres;Host=127.0.0.1;Port=39372;Database=btcpayserver")
}; };
PayTester.Port = int.Parse(GetEnvironment("TESTS_PORT", Utils.FreeTcpPort().ToString())); PayTester.Port = int.Parse(GetEnvironment("TESTS_PORT", Utils.FreeTcpPort().ToString()));
PayTester.HostName = GetEnvironment("TESTS_HOSTNAME", "127.0.0.1"); PayTester.HostName = GetEnvironment("TESTS_HOSTNAME", "127.0.0.1");
PayTester.Start(); PayTester.Start();
} }
private string GetEnvironment(string variable, string defaultValue) private string GetEnvironment(string variable, string defaultValue)
{ {
var var = Environment.GetEnvironmentVariable(variable); var var = Environment.GetEnvironmentVariable(variable);
return String.IsNullOrEmpty(var) ? defaultValue : var; return String.IsNullOrEmpty(var) ? defaultValue : var;
} }
public TestAccount NewAccount() public TestAccount NewAccount()
{ {
return new TestAccount(this); return new TestAccount(this);
} }
public bool FakeCallback public bool FakeCallback
{ {
get; get;
set; set;
} }
public RPCClient ExplorerNode public RPCClient ExplorerNode
{ {
get; set; get; set;
} }
public ExplorerClient ExplorerClient public ExplorerClient ExplorerClient
{ {
get; set; get; set;
} }
HttpClient _Http = new HttpClient(); HttpClient _Http = new HttpClient();
class MockHttpRequest : HttpRequest class MockHttpRequest : HttpRequest
{ {
Uri serverUri; Uri serverUri;
public MockHttpRequest(Uri serverUri) public MockHttpRequest(Uri serverUri)
{ {
this.serverUri = serverUri; this.serverUri = serverUri;
} }
public override HttpContext HttpContext => throw new NotImplementedException(); public override HttpContext HttpContext => throw new NotImplementedException();
public override string Method public override string Method
{ {
get => throw new NotImplementedException(); get => throw new NotImplementedException();
set => throw new NotImplementedException(); set => throw new NotImplementedException();
} }
public override string Scheme public override string Scheme
{ {
get => serverUri.Scheme; get => serverUri.Scheme;
set => throw new NotImplementedException(); set => throw new NotImplementedException();
} }
public override bool IsHttps public override bool IsHttps
{ {
get => throw new NotImplementedException(); get => throw new NotImplementedException();
set => throw new NotImplementedException(); set => throw new NotImplementedException();
} }
public override HostString Host public override HostString Host
{ {
get => new HostString(serverUri.Host, serverUri.Port); get => new HostString(serverUri.Host, serverUri.Port);
set => throw new NotImplementedException(); set => throw new NotImplementedException();
} }
public override PathString PathBase public override PathString PathBase
{ {
get => ""; get => "";
set => throw new NotImplementedException(); set => throw new NotImplementedException();
} }
public override PathString Path public override PathString Path
{ {
get => throw new NotImplementedException(); get => throw new NotImplementedException();
set => throw new NotImplementedException(); set => throw new NotImplementedException();
} }
public override QueryString QueryString public override QueryString QueryString
{ {
get => throw new NotImplementedException(); get => throw new NotImplementedException();
set => throw new NotImplementedException(); set => throw new NotImplementedException();
} }
public override IQueryCollection Query public override IQueryCollection Query
{ {
get => throw new NotImplementedException(); get => throw new NotImplementedException();
set => throw new NotImplementedException(); set => throw new NotImplementedException();
} }
public override string Protocol public override string Protocol
{ {
get => throw new NotImplementedException(); get => throw new NotImplementedException();
set => throw new NotImplementedException(); set => throw new NotImplementedException();
} }
public override IHeaderDictionary Headers => throw new NotImplementedException(); public override IHeaderDictionary Headers => throw new NotImplementedException();
public override IRequestCookieCollection Cookies public override IRequestCookieCollection Cookies
{ {
get => throw new NotImplementedException(); get => throw new NotImplementedException();
set => throw new NotImplementedException(); set => throw new NotImplementedException();
} }
public override long? ContentLength public override long? ContentLength
{ {
get => throw new NotImplementedException(); get => throw new NotImplementedException();
set => throw new NotImplementedException(); set => throw new NotImplementedException();
} }
public override string ContentType public override string ContentType
{ {
get => throw new NotImplementedException(); get => throw new NotImplementedException();
set => throw new NotImplementedException(); set => throw new NotImplementedException();
} }
public override Stream Body public override Stream Body
{ {
get => throw new NotImplementedException(); get => throw new NotImplementedException();
set => throw new NotImplementedException(); set => throw new NotImplementedException();
} }
public override bool HasFormContentType => throw new NotImplementedException(); public override bool HasFormContentType => throw new NotImplementedException();
public override IFormCollection Form public override IFormCollection Form
{ {
get => throw new NotImplementedException(); get => throw new NotImplementedException();
set => throw new NotImplementedException(); set => throw new NotImplementedException();
} }
public override Task<IFormCollection> ReadFormAsync(CancellationToken cancellationToken = default(CancellationToken)) public override Task<IFormCollection> ReadFormAsync(CancellationToken cancellationToken = default(CancellationToken))
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
} }
/// <summary> /// <summary>
/// Simulating callback from NBXplorer. NBXplorer can't reach the host during tests as it is not running on localhost. /// Simulating callback from NBXplorer. NBXplorer can't reach the host during tests as it is not running on localhost.
/// </summary> /// </summary>
/// <param name="address"></param> /// <param name="address"></param>
public void SimulateCallback(BitcoinAddress address = null) public void SimulateCallback(BitcoinAddress address = null)
{ {
if(!FakeCallback) //The callback of NBXplorer should work if (!FakeCallback) //The callback of NBXplorer should work
return; return;
var req = new MockHttpRequest(PayTester.ServerUri); var req = new MockHttpRequest(PayTester.ServerUri);
var controller = PayTester.GetController<CallbackController>(); var controller = PayTester.GetController<CallbackController>();
if(address != null) if (address != null)
{ {
var match = new TransactionMatch(); var match = new TransactionMatch();
match.Outputs.Add(new KeyPathInformation() { ScriptPubKey = address.ScriptPubKey }); match.Outputs.Add(new KeyPathInformation() { ScriptPubKey = address.ScriptPubKey });
var content = new StringContent(new NBXplorer.Serializer(Network).ToString(match), new UTF8Encoding(false), "application/json"); var content = new StringContent(new NBXplorer.Serializer(Network).ToString(match), new UTF8Encoding(false), "application/json");
var uri = controller.GetCallbackUriAsync(req).GetAwaiter().GetResult(); var uri = controller.GetCallbackUriAsync(req).GetAwaiter().GetResult();
HttpRequestMessage message = new HttpRequestMessage(); HttpRequestMessage message = new HttpRequestMessage();
message.Method = HttpMethod.Post; message.Method = HttpMethod.Post;
message.RequestUri = uri; message.RequestUri = uri;
message.Content = content; message.Content = content;
_Http.SendAsync(message).GetAwaiter().GetResult(); _Http.SendAsync(message).GetAwaiter().GetResult();
} }
else else
{ {
var uri = controller.GetCallbackBlockUriAsync(req).GetAwaiter().GetResult(); var uri = controller.GetCallbackBlockUriAsync(req).GetAwaiter().GetResult();
HttpRequestMessage message = new HttpRequestMessage(); HttpRequestMessage message = new HttpRequestMessage();
message.Method = HttpMethod.Post; message.Method = HttpMethod.Post;
message.RequestUri = uri; message.RequestUri = uri;
_Http.SendAsync(message).GetAwaiter().GetResult(); _Http.SendAsync(message).GetAwaiter().GetResult();
} }
} }
public BTCPayServerTester PayTester public BTCPayServerTester PayTester
{ {
get; set; get; set;
} }
public Network Network public Network Network
{ {
get; get;
set; set;
} = Network.RegTest; } = Network.RegTest;
public void Dispose() public void Dispose()
{ {
if(PayTester != null) if (PayTester != null)
PayTester.Dispose(); PayTester.Dispose();
} }
} }
} }

View File

@@ -15,78 +15,78 @@ namespace BTCPayServer.Tests
{ {
public class TestAccount public class TestAccount
{ {
ServerTester parent; ServerTester parent;
public TestAccount(ServerTester parent) public TestAccount(ServerTester parent)
{ {
this.parent = parent; this.parent = parent;
BitPay = new Bitpay(new Key(), parent.PayTester.ServerUri); BitPay = new Bitpay(new Key(), parent.PayTester.ServerUri);
} }
public void GrantAccess() public void GrantAccess()
{ {
GrantAccessAsync().GetAwaiter().GetResult(); GrantAccessAsync().GetAwaiter().GetResult();
} }
public void Register() public void Register()
{ {
RegisterAsync().GetAwaiter().GetResult(); RegisterAsync().GetAwaiter().GetResult();
} }
public BitcoinExtKey ExtKey public BitcoinExtKey ExtKey
{ {
get; set; get; set;
} }
public async Task GrantAccessAsync() public async Task GrantAccessAsync()
{ {
await RegisterAsync(); await RegisterAsync();
var store = await CreateStoreAsync(); var store = await CreateStoreAsync();
var pairingCode = BitPay.RequestClientAuthorization("test", Facade.Merchant); var pairingCode = BitPay.RequestClientAuthorization("test", Facade.Merchant);
Assert.IsType<ViewResult>(await store.RequestPairing(pairingCode.ToString())); Assert.IsType<ViewResult>(await store.RequestPairing(pairingCode.ToString()));
await store.Pair(pairingCode.ToString(), StoreId); await store.Pair(pairingCode.ToString(), StoreId);
} }
public StoresController CreateStore() public StoresController CreateStore()
{ {
return CreateStoreAsync().GetAwaiter().GetResult(); return CreateStoreAsync().GetAwaiter().GetResult();
} }
public async Task<StoresController> CreateStoreAsync() public async Task<StoresController> CreateStoreAsync()
{ {
ExtKey = new ExtKey().GetWif(parent.Network); ExtKey = new ExtKey().GetWif(parent.Network);
var store = parent.PayTester.GetController<StoresController>(UserId); var store = parent.PayTester.GetController<StoresController>(UserId);
await store.CreateStore(new CreateStoreViewModel() { Name = "Test Store" }); await store.CreateStore(new CreateStoreViewModel() { Name = "Test Store" });
StoreId = store.CreatedStoreId; StoreId = store.CreatedStoreId;
await store.UpdateStore(StoreId, new StoreViewModel() await store.UpdateStore(StoreId, new StoreViewModel()
{ {
DerivationScheme = ExtKey.Neuter().ToString() + "-[legacy]", DerivationScheme = ExtKey.Neuter().ToString() + "-[legacy]",
SpeedPolicy = SpeedPolicy.MediumSpeed SpeedPolicy = SpeedPolicy.MediumSpeed
}, "Save"); }, "Save");
return store; return store;
} }
private async Task RegisterAsync() private async Task RegisterAsync()
{ {
var account = parent.PayTester.GetController<AccountController>(); var account = parent.PayTester.GetController<AccountController>();
await account.Register(new RegisterViewModel() await account.Register(new RegisterViewModel()
{ {
Email = Guid.NewGuid() + "@toto.com", Email = Guid.NewGuid() + "@toto.com",
ConfirmPassword = "Kitten0@", ConfirmPassword = "Kitten0@",
Password = "Kitten0@", Password = "Kitten0@",
}); });
UserId = account.RegisteredUserId; UserId = account.RegisteredUserId;
} }
public Bitpay BitPay public Bitpay BitPay
{ {
get; set; get; set;
} }
public string UserId public string UserId
{ {
get; set; get; set;
} }
public string StoreId public string StoreId
{ {
get; set; get; set;
} }
} }
} }

View File

@@ -25,369 +25,369 @@ using Microsoft.Extensions.Caching.Memory;
namespace BTCPayServer.Tests namespace BTCPayServer.Tests
{ {
public class UnitTest1 public class UnitTest1
{ {
public UnitTest1(ITestOutputHelper helper) public UnitTest1(ITestOutputHelper helper)
{ {
Logs.Tester = new XUnitLog(helper) { Name = "Tests" }; Logs.Tester = new XUnitLog(helper) { Name = "Tests" };
Logs.LogProvider = new XUnitLogProvider(helper); Logs.LogProvider = new XUnitLogProvider(helper);
} }
[Fact] [Fact]
public void CanCalculateCryptoDue() public void CanCalculateCryptoDue()
{ {
var entity = new InvoiceEntity(); var entity = new InvoiceEntity();
entity.TxFee = Money.Coins(0.1m); entity.TxFee = Money.Coins(0.1m);
entity.Rate = 5000; entity.Rate = 5000;
entity.Payments = new System.Collections.Generic.List<PaymentEntity>(); entity.Payments = new System.Collections.Generic.List<PaymentEntity>();
entity.ProductInformation = new ProductInformation() { Price = 5000 }; entity.ProductInformation = new ProductInformation() { Price = 5000 };
Assert.Equal(Money.Coins(1.1m), entity.GetCryptoDue()); Assert.Equal(Money.Coins(1.1m), entity.GetCryptoDue());
Assert.Equal(Money.Coins(1.1m), entity.GetTotalCryptoDue()); Assert.Equal(Money.Coins(1.1m), entity.GetTotalCryptoDue());
entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.5m), new Key()) }); entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.5m), new Key()) });
//Since we need to spend one more txout, it should be 1.1 - 0,5 + 0.1 //Since we need to spend one more txout, it should be 1.1 - 0,5 + 0.1
Assert.Equal(Money.Coins(0.7m), entity.GetCryptoDue()); Assert.Equal(Money.Coins(0.7m), entity.GetCryptoDue());
Assert.Equal(Money.Coins(1.2m), entity.GetTotalCryptoDue()); Assert.Equal(Money.Coins(1.2m), entity.GetTotalCryptoDue());
entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.2m), new Key()) }); entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.2m), new Key()) });
Assert.Equal(Money.Coins(0.6m), entity.GetCryptoDue()); Assert.Equal(Money.Coins(0.6m), entity.GetCryptoDue());
Assert.Equal(Money.Coins(1.3m), entity.GetTotalCryptoDue()); Assert.Equal(Money.Coins(1.3m), entity.GetTotalCryptoDue());
entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.6m), new Key()) }); entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.6m), new Key()) });
Assert.Equal(Money.Zero, entity.GetCryptoDue()); Assert.Equal(Money.Zero, entity.GetCryptoDue());
Assert.Equal(Money.Coins(1.3m), entity.GetTotalCryptoDue()); Assert.Equal(Money.Coins(1.3m), entity.GetTotalCryptoDue());
entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.2m), new Key()) }); entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.2m), new Key()) });
Assert.Equal(Money.Zero, entity.GetCryptoDue()); Assert.Equal(Money.Zero, entity.GetCryptoDue());
Assert.Equal(Money.Coins(1.3m), entity.GetTotalCryptoDue()); Assert.Equal(Money.Coins(1.3m), entity.GetTotalCryptoDue());
} }
[Fact] [Fact]
public void CanPayUsingBIP70() public void CanPayUsingBIP70()
{ {
using(var tester = ServerTester.Create()) using (var tester = ServerTester.Create())
{ {
tester.Start(); tester.Start();
var user = tester.NewAccount(); var user = tester.NewAccount();
user.GrantAccess(); user.GrantAccess();
var invoice = user.BitPay.CreateInvoice(new Invoice() var invoice = user.BitPay.CreateInvoice(new Invoice()
{ {
Buyer = new Buyer() { email = "test@fwf.com" }, Buyer = new Buyer() { email = "test@fwf.com" },
Price = 5000.0, Price = 5000.0,
Currency = "USD", Currency = "USD",
PosData = "posData", PosData = "posData",
OrderId = "orderId", OrderId = "orderId",
//RedirectURL = redirect + "redirect", //RedirectURL = redirect + "redirect",
//NotificationURL = CallbackUri + "/notification", //NotificationURL = CallbackUri + "/notification",
ItemDesc = "Some description", ItemDesc = "Some description",
FullNotifications = true FullNotifications = true
}, Facade.Merchant); }, Facade.Merchant);
Assert.False(invoice.Refundable); Assert.False(invoice.Refundable);
var url = new BitcoinUrlBuilder(invoice.PaymentUrls.BIP72); var url = new BitcoinUrlBuilder(invoice.PaymentUrls.BIP72);
var request = url.GetPaymentRequest(); var request = url.GetPaymentRequest();
var payment = request.CreatePayment(); var payment = request.CreatePayment();
Transaction tx = new Transaction(); Transaction tx = new Transaction();
tx.Outputs.AddRange(request.Details.Outputs.Select(o => new TxOut(o.Amount, o.Script))); tx.Outputs.AddRange(request.Details.Outputs.Select(o => new TxOut(o.Amount, o.Script)));
var cashCow = tester.ExplorerNode; var cashCow = tester.ExplorerNode;
tx = cashCow.FundRawTransaction(tx).Transaction; tx = cashCow.FundRawTransaction(tx).Transaction;
tx = cashCow.SignRawTransaction(tx); tx = cashCow.SignRawTransaction(tx);
payment.Transactions.Add(tx); payment.Transactions.Add(tx);
payment.RefundTo.Add(new PaymentOutput(Money.Coins(1.0m), new Key().ScriptPubKey)); payment.RefundTo.Add(new PaymentOutput(Money.Coins(1.0m), new Key().ScriptPubKey));
var ack = payment.SubmitPayment(); var ack = payment.SubmitPayment();
Assert.NotNull(ack); Assert.NotNull(ack);
Eventually(() => Eventually(() =>
{ {
tester.SimulateCallback(url.Address); tester.SimulateCallback(url.Address);
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant); var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
Assert.Equal("paid", localInvoice.Status); Assert.Equal("paid", localInvoice.Status);
Assert.True(localInvoice.Refundable); Assert.True(localInvoice.Refundable);
}); });
} }
} }
[Fact] [Fact]
public void CanUseServerInitiatedPairingCode() public void CanUseServerInitiatedPairingCode()
{ {
using(var tester = ServerTester.Create()) using (var tester = ServerTester.Create())
{ {
tester.Start(); tester.Start();
var acc = tester.NewAccount(); var acc = tester.NewAccount();
acc.Register(); acc.Register();
acc.CreateStore(); acc.CreateStore();
var controller = tester.PayTester.GetController<StoresController>(acc.UserId); var controller = tester.PayTester.GetController<StoresController>(acc.UserId);
var token = (RedirectToActionResult)controller.CreateToken(acc.StoreId, new Models.StoreViewModels.CreateTokenViewModel() var token = (RedirectToActionResult)controller.CreateToken(acc.StoreId, new Models.StoreViewModels.CreateTokenViewModel()
{ {
Facade = Facade.Merchant.ToString(), Facade = Facade.Merchant.ToString(),
Label = "bla", Label = "bla",
PublicKey = null PublicKey = null
}).GetAwaiter().GetResult(); }).GetAwaiter().GetResult();
var pairingCode = (string)token.RouteValues["pairingCode"]; var pairingCode = (string)token.RouteValues["pairingCode"];
acc.BitPay.AuthorizeClient(new PairingCode(pairingCode)).GetAwaiter().GetResult(); acc.BitPay.AuthorizeClient(new PairingCode(pairingCode)).GetAwaiter().GetResult();
Assert.True(acc.BitPay.TestAccess(Facade.Merchant)); Assert.True(acc.BitPay.TestAccess(Facade.Merchant));
} }
} }
[Fact] [Fact]
public void CanSendIPN() public void CanSendIPN()
{ {
using(var callbackServer = new CustomServer()) using (var callbackServer = new CustomServer())
{ {
using(var tester = ServerTester.Create()) using (var tester = ServerTester.Create())
{ {
tester.Start(); tester.Start();
var acc = tester.NewAccount(); var acc = tester.NewAccount();
acc.GrantAccess(); acc.GrantAccess();
var invoice = acc.BitPay.CreateInvoice(new Invoice() var invoice = acc.BitPay.CreateInvoice(new Invoice()
{ {
Price = 5.0, Price = 5.0,
Currency = "USD", Currency = "USD",
PosData = "posData", PosData = "posData",
OrderId = "orderId", OrderId = "orderId",
NotificationURL = callbackServer.GetUri().AbsoluteUri, NotificationURL = callbackServer.GetUri().AbsoluteUri,
ItemDesc = "Some description", ItemDesc = "Some description",
FullNotifications = true FullNotifications = true
}); });
BitcoinUrlBuilder url = new BitcoinUrlBuilder(invoice.PaymentUrls.BIP21); BitcoinUrlBuilder url = new BitcoinUrlBuilder(invoice.PaymentUrls.BIP21);
tester.ExplorerNode.SendToAddress(url.Address, url.Amount); tester.ExplorerNode.SendToAddress(url.Address, url.Amount);
Thread.Sleep(5000); Thread.Sleep(5000);
tester.SimulateCallback(url.Address); tester.SimulateCallback(url.Address);
callbackServer.ProcessNextRequest((ctx) => callbackServer.ProcessNextRequest((ctx) =>
{ {
var ipn = new StreamReader(ctx.Request.Body).ReadToEnd(); var ipn = new StreamReader(ctx.Request.Body).ReadToEnd();
JsonConvert.DeserializeObject<InvoicePaymentNotification>(ipn); //can deserialize JsonConvert.DeserializeObject<InvoicePaymentNotification>(ipn); //can deserialize
}); });
var invoice2 = acc.BitPay.GetInvoice(invoice.Id); var invoice2 = acc.BitPay.GetInvoice(invoice.Id);
Assert.NotNull(invoice2); Assert.NotNull(invoice2);
} }
} }
} }
[Fact] [Fact]
public void CantPairTwiceWithSamePubkey() public void CantPairTwiceWithSamePubkey()
{ {
using(var tester = ServerTester.Create()) using (var tester = ServerTester.Create())
{ {
tester.Start(); tester.Start();
var acc = tester.NewAccount(); var acc = tester.NewAccount();
acc.Register(); acc.Register();
var store = acc.CreateStore(); var store = acc.CreateStore();
var pairingCode = acc.BitPay.RequestClientAuthorization("test", Facade.Merchant); var pairingCode = acc.BitPay.RequestClientAuthorization("test", Facade.Merchant);
Assert.IsType<RedirectToActionResult>(store.Pair(pairingCode.ToString(), acc.StoreId).GetAwaiter().GetResult()); Assert.IsType<RedirectToActionResult>(store.Pair(pairingCode.ToString(), acc.StoreId).GetAwaiter().GetResult());
pairingCode = acc.BitPay.RequestClientAuthorization("test1", Facade.Merchant); pairingCode = acc.BitPay.RequestClientAuthorization("test1", Facade.Merchant);
var store2 = acc.CreateStore(); var store2 = acc.CreateStore();
store2.Pair(pairingCode.ToString(), store2.CreatedStoreId).GetAwaiter().GetResult(); store2.Pair(pairingCode.ToString(), store2.CreatedStoreId).GetAwaiter().GetResult();
Assert.Contains(nameof(PairingResult.ReusedKey), store2.StatusMessage); Assert.Contains(nameof(PairingResult.ReusedKey), store2.StatusMessage);
} }
} }
[Fact] [Fact]
public void InvoiceFlowThroughDifferentStatesCorrectly() public void InvoiceFlowThroughDifferentStatesCorrectly()
{ {
using(var tester = ServerTester.Create()) using (var tester = ServerTester.Create())
{ {
tester.Start(); tester.Start();
var user = tester.NewAccount(); var user = tester.NewAccount();
Assert.False(user.BitPay.TestAccess(Facade.Merchant)); Assert.False(user.BitPay.TestAccess(Facade.Merchant));
user.GrantAccess(); user.GrantAccess();
Assert.True(user.BitPay.TestAccess(Facade.Merchant)); Assert.True(user.BitPay.TestAccess(Facade.Merchant));
var invoice = user.BitPay.CreateInvoice(new Invoice() var invoice = user.BitPay.CreateInvoice(new Invoice()
{ {
Price = 5000.0, Price = 5000.0,
Currency = "USD", Currency = "USD",
PosData = "posData", PosData = "posData",
OrderId = "orderId", OrderId = "orderId",
//RedirectURL = redirect + "redirect", //RedirectURL = redirect + "redirect",
//NotificationURL = CallbackUri + "/notification", //NotificationURL = CallbackUri + "/notification",
ItemDesc = "Some description", ItemDesc = "Some description",
FullNotifications = true FullNotifications = true
}, Facade.Merchant); }, Facade.Merchant);
var repo = tester.PayTester.GetService<InvoiceRepository>(); var repo = tester.PayTester.GetService<InvoiceRepository>();
var ctx = tester.PayTester.GetService<ApplicationDbContextFactory>().CreateContext(); var ctx = tester.PayTester.GetService<ApplicationDbContextFactory>().CreateContext();
var textSearchResult = tester.PayTester.Runtime.InvoiceRepository.GetInvoices(new InvoiceQuery() var textSearchResult = tester.PayTester.Runtime.InvoiceRepository.GetInvoices(new InvoiceQuery()
{ {
StoreId = user.StoreId, StoreId = user.StoreId,
TextSearch = invoice.OrderId TextSearch = invoice.OrderId
}).GetAwaiter().GetResult(); }).GetAwaiter().GetResult();
Assert.Equal(1, textSearchResult.Length); Assert.Equal(1, textSearchResult.Length);
textSearchResult = tester.PayTester.Runtime.InvoiceRepository.GetInvoices(new InvoiceQuery() textSearchResult = tester.PayTester.Runtime.InvoiceRepository.GetInvoices(new InvoiceQuery()
{ {
StoreId = user.StoreId, StoreId = user.StoreId,
TextSearch = invoice.Id TextSearch = invoice.Id
}).GetAwaiter().GetResult(); }).GetAwaiter().GetResult();
Assert.Equal(1, textSearchResult.Length); Assert.Equal(1, textSearchResult.Length);
invoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant); invoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
Assert.Equal(Money.Coins(0), invoice.BtcPaid); Assert.Equal(Money.Coins(0), invoice.BtcPaid);
Assert.Equal("new", invoice.Status); Assert.Equal("new", invoice.Status);
Assert.Equal(false, (bool)((JValue)invoice.ExceptionStatus).Value); Assert.Equal(false, (bool)((JValue)invoice.ExceptionStatus).Value);
Assert.Equal(1, user.BitPay.GetInvoices(invoice.InvoiceTime.DateTime).Length); Assert.Equal(1, user.BitPay.GetInvoices(invoice.InvoiceTime.DateTime).Length);
Assert.Equal(0, user.BitPay.GetInvoices(invoice.InvoiceTime.DateTime + TimeSpan.FromDays(1)).Length); Assert.Equal(0, user.BitPay.GetInvoices(invoice.InvoiceTime.DateTime + TimeSpan.FromDays(1)).Length);
Assert.Equal(1, user.BitPay.GetInvoices(invoice.InvoiceTime.DateTime - TimeSpan.FromDays(5)).Length); Assert.Equal(1, user.BitPay.GetInvoices(invoice.InvoiceTime.DateTime - TimeSpan.FromDays(5)).Length);
Assert.Equal(1, user.BitPay.GetInvoices(invoice.InvoiceTime.DateTime - TimeSpan.FromDays(5), invoice.InvoiceTime.DateTime).Length); Assert.Equal(1, user.BitPay.GetInvoices(invoice.InvoiceTime.DateTime - TimeSpan.FromDays(5), invoice.InvoiceTime.DateTime).Length);
Assert.Equal(0, user.BitPay.GetInvoices(invoice.InvoiceTime.DateTime - TimeSpan.FromDays(5), invoice.InvoiceTime.DateTime - TimeSpan.FromDays(1)).Length); Assert.Equal(0, user.BitPay.GetInvoices(invoice.InvoiceTime.DateTime - TimeSpan.FromDays(5), invoice.InvoiceTime.DateTime - TimeSpan.FromDays(1)).Length);
var firstPayment = Money.Coins(0.04m); var firstPayment = Money.Coins(0.04m);
var txFee = Money.Zero; var txFee = Money.Zero;
var rate = user.BitPay.GetRates(); var rate = user.BitPay.GetRates();
var cashCow = tester.ExplorerNode; var cashCow = tester.ExplorerNode;
var invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network); var invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network);
var iii = ctx.AddressInvoices.ToArray(); var iii = ctx.AddressInvoices.ToArray();
Assert.True(IsMapped(invoice, ctx)); Assert.True(IsMapped(invoice, ctx));
cashCow.SendToAddress(invoiceAddress, firstPayment); cashCow.SendToAddress(invoiceAddress, firstPayment);
var invoiceEntity = repo.GetInvoice(null, invoice.Id, true).GetAwaiter().GetResult(); var invoiceEntity = repo.GetInvoice(null, invoice.Id, true).GetAwaiter().GetResult();
Assert.Equal(1, invoiceEntity.HistoricalAddresses.Length); Assert.Equal(1, invoiceEntity.HistoricalAddresses.Length);
Assert.Null(invoiceEntity.HistoricalAddresses[0].UnAssigned); Assert.Null(invoiceEntity.HistoricalAddresses[0].UnAssigned);
Money secondPayment = Money.Zero; Money secondPayment = Money.Zero;
Eventually(() => Eventually(() =>
{ {
tester.SimulateCallback(invoiceAddress); tester.SimulateCallback(invoiceAddress);
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant); var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
Assert.Equal("new", localInvoice.Status); Assert.Equal("new", localInvoice.Status);
Assert.Equal(firstPayment, localInvoice.BtcPaid); Assert.Equal(firstPayment, localInvoice.BtcPaid);
txFee = localInvoice.BtcDue - invoice.BtcDue; txFee = localInvoice.BtcDue - invoice.BtcDue;
Assert.Equal("paidPartial", localInvoice.ExceptionStatus); Assert.Equal("paidPartial", localInvoice.ExceptionStatus);
Assert.NotEqual(localInvoice.BitcoinAddress, invoice.BitcoinAddress); //New address Assert.NotEqual(localInvoice.BitcoinAddress, invoice.BitcoinAddress); //New address
Assert.True(IsMapped(invoice, ctx)); Assert.True(IsMapped(invoice, ctx));
Assert.True(IsMapped(localInvoice, ctx)); Assert.True(IsMapped(localInvoice, ctx));
invoiceEntity = repo.GetInvoice(null, invoice.Id, true).GetAwaiter().GetResult(); invoiceEntity = repo.GetInvoice(null, invoice.Id, true).GetAwaiter().GetResult();
var historical1 = invoiceEntity.HistoricalAddresses.FirstOrDefault(h => h.Address == invoice.BitcoinAddress.ToString()); var historical1 = invoiceEntity.HistoricalAddresses.FirstOrDefault(h => h.Address == invoice.BitcoinAddress.ToString());
Assert.NotNull(historical1.UnAssigned); Assert.NotNull(historical1.UnAssigned);
var historical2 = invoiceEntity.HistoricalAddresses.FirstOrDefault(h => h.Address == localInvoice.BitcoinAddress.ToString()); var historical2 = invoiceEntity.HistoricalAddresses.FirstOrDefault(h => h.Address == localInvoice.BitcoinAddress.ToString());
Assert.Null(historical2.UnAssigned); Assert.Null(historical2.UnAssigned);
invoiceAddress = BitcoinAddress.Create(localInvoice.BitcoinAddress, cashCow.Network); invoiceAddress = BitcoinAddress.Create(localInvoice.BitcoinAddress, cashCow.Network);
secondPayment = localInvoice.BtcDue; secondPayment = localInvoice.BtcDue;
}); });
cashCow.SendToAddress(invoiceAddress, secondPayment); cashCow.SendToAddress(invoiceAddress, secondPayment);
Eventually(() => Eventually(() =>
{ {
tester.SimulateCallback(invoiceAddress); tester.SimulateCallback(invoiceAddress);
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant); var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
Assert.Equal("paid", localInvoice.Status); Assert.Equal("paid", localInvoice.Status);
Assert.Equal(firstPayment + secondPayment, localInvoice.BtcPaid); Assert.Equal(firstPayment + secondPayment, localInvoice.BtcPaid);
Assert.Equal(Money.Zero, localInvoice.BtcDue); Assert.Equal(Money.Zero, localInvoice.BtcDue);
Assert.Equal(localInvoice.BitcoinAddress, invoiceAddress.ToString()); //no new address generated Assert.Equal(localInvoice.BitcoinAddress, invoiceAddress.ToString()); //no new address generated
Assert.True(IsMapped(localInvoice, ctx)); Assert.True(IsMapped(localInvoice, ctx));
Assert.Equal(false, (bool)((JValue)localInvoice.ExceptionStatus).Value); Assert.Equal(false, (bool)((JValue)localInvoice.ExceptionStatus).Value);
}); });
cashCow.Generate(1); //The user has medium speed settings, so 1 conf is enough to be confirmed cashCow.Generate(1); //The user has medium speed settings, so 1 conf is enough to be confirmed
Eventually(() => Eventually(() =>
{ {
tester.SimulateCallback(); tester.SimulateCallback();
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant); var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
Assert.Equal("confirmed", localInvoice.Status); Assert.Equal("confirmed", localInvoice.Status);
}); });
cashCow.Generate(5); //Now should be complete cashCow.Generate(5); //Now should be complete
Eventually(() => Eventually(() =>
{ {
tester.SimulateCallback(); tester.SimulateCallback();
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant); var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
Assert.Equal("complete", localInvoice.Status); Assert.Equal("complete", localInvoice.Status);
}); });
invoice = user.BitPay.CreateInvoice(new Invoice() invoice = user.BitPay.CreateInvoice(new Invoice()
{ {
Price = 5000.0, Price = 5000.0,
Currency = "USD", Currency = "USD",
PosData = "posData", PosData = "posData",
OrderId = "orderId", OrderId = "orderId",
//RedirectURL = redirect + "redirect", //RedirectURL = redirect + "redirect",
//NotificationURL = CallbackUri + "/notification", //NotificationURL = CallbackUri + "/notification",
ItemDesc = "Some description", ItemDesc = "Some description",
FullNotifications = true FullNotifications = true
}, Facade.Merchant); }, Facade.Merchant);
invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network); invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network);
cashCow.SendToAddress(invoiceAddress, invoice.BtcDue + Money.Coins(1)); cashCow.SendToAddress(invoiceAddress, invoice.BtcDue + Money.Coins(1));
Eventually(() => Eventually(() =>
{ {
tester.SimulateCallback(invoiceAddress); tester.SimulateCallback(invoiceAddress);
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant); var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
Assert.Equal("paid", localInvoice.Status); Assert.Equal("paid", localInvoice.Status);
Assert.Equal(Money.Zero, localInvoice.BtcDue); Assert.Equal(Money.Zero, localInvoice.BtcDue);
Assert.Equal("paidOver", (string)((JValue)localInvoice.ExceptionStatus).Value); Assert.Equal("paidOver", (string)((JValue)localInvoice.ExceptionStatus).Value);
}); });
cashCow.Generate(1); cashCow.Generate(1);
Eventually(() => Eventually(() =>
{ {
tester.SimulateCallback(); tester.SimulateCallback();
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant); var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
Assert.Equal("confirmed", localInvoice.Status); Assert.Equal("confirmed", localInvoice.Status);
Assert.Equal(Money.Zero, localInvoice.BtcDue); Assert.Equal(Money.Zero, localInvoice.BtcDue);
Assert.Equal("paidOver", (string)((JValue)localInvoice.ExceptionStatus).Value); Assert.Equal("paidOver", (string)((JValue)localInvoice.ExceptionStatus).Value);
}); });
} }
} }
[Fact] [Fact]
public void CheckRatesProvider() public void CheckRatesProvider()
{ {
var coinAverage = new CoinAverageRateProvider(); var coinAverage = new CoinAverageRateProvider();
var jpy = coinAverage.GetRateAsync("JPY").GetAwaiter().GetResult(); var jpy = coinAverage.GetRateAsync("JPY").GetAwaiter().GetResult();
var jpy2 = new BitpayRateProvider(new Bitpay(new Key(), new Uri("https://bitpay.com/"))).GetRateAsync("JPY").GetAwaiter().GetResult(); var jpy2 = new BitpayRateProvider(new Bitpay(new Key(), new Uri("https://bitpay.com/"))).GetRateAsync("JPY").GetAwaiter().GetResult();
var cached = new CachedRateProvider(coinAverage, new MemoryCache(new MemoryCacheOptions() { ExpirationScanFrequency = TimeSpan.FromSeconds(1.0) })); var cached = new CachedRateProvider(coinAverage, new MemoryCache(new MemoryCacheOptions() { ExpirationScanFrequency = TimeSpan.FromSeconds(1.0) }));
cached.CacheSpan = TimeSpan.FromSeconds(10); cached.CacheSpan = TimeSpan.FromSeconds(10);
var a = cached.GetRateAsync("JPY").GetAwaiter().GetResult(); var a = cached.GetRateAsync("JPY").GetAwaiter().GetResult();
var b = cached.GetRateAsync("JPY").GetAwaiter().GetResult(); var b = cached.GetRateAsync("JPY").GetAwaiter().GetResult();
//Manually check that cache get hit after 10 sec //Manually check that cache get hit after 10 sec
var c = cached.GetRateAsync("JPY").GetAwaiter().GetResult(); var c = cached.GetRateAsync("JPY").GetAwaiter().GetResult();
} }
private static bool IsMapped(Invoice invoice, ApplicationDbContext ctx) private static bool IsMapped(Invoice invoice, ApplicationDbContext ctx)
{ {
var h = BitcoinAddress.Create(invoice.BitcoinAddress).ScriptPubKey.Hash.ToString(); var h = BitcoinAddress.Create(invoice.BitcoinAddress).ScriptPubKey.Hash.ToString();
return ctx.AddressInvoices.FirstOrDefault(i => i.InvoiceDataId == invoice.Id && i.Address == h) != null; return ctx.AddressInvoices.FirstOrDefault(i => i.InvoiceDataId == invoice.Id && i.Address == h) != null;
} }
private void Eventually(Action act) private void Eventually(Action act)
{ {
CancellationTokenSource cts = new CancellationTokenSource(10000); CancellationTokenSource cts = new CancellationTokenSource(10000);
while(true) while (true)
{ {
try try
{ {
act(); act();
break; break;
} }
catch(XunitException) when(!cts.Token.IsCancellationRequested) catch (XunitException) when (!cts.Token.IsCancellationRequested)
{ {
cts.Token.WaitHandle.WaitOne(500); cts.Token.WaitHandle.WaitOne(500);
} }
} }
} }
} }
} }

View File

@@ -8,43 +8,43 @@ using Xunit;
namespace BTCPayServer.Tests namespace BTCPayServer.Tests
{ {
// Helper class for testing functionality and generating data needed during coding/debuging // Helper class for testing functionality and generating data needed during coding/debuging
public class UnitTestPeusa public class UnitTestPeusa
{ {
// Unit test that generates temorary checkout Bitpay page // Unit test that generates temorary checkout Bitpay page
// https://forkbitpay.slack.com/archives/C7M093Z55/p1508293682000217 // https://forkbitpay.slack.com/archives/C7M093Z55/p1508293682000217
[Fact] [Fact]
public void BitpayCheckout() public void BitpayCheckout()
{ {
var key = new Key(Encoders.Hex.DecodeData("7b70a06f35562873e3dcb46005ed0fe78e1991ad906e56adaaafa40ba861e056")); var key = new Key(Encoders.Hex.DecodeData("7b70a06f35562873e3dcb46005ed0fe78e1991ad906e56adaaafa40ba861e056"));
var url = new Uri("https://test.bitpay.com/"); var url = new Uri("https://test.bitpay.com/");
var btcpay = new Bitpay(key, url); var btcpay = new Bitpay(key, url);
var invoice = btcpay.CreateInvoice(new Invoice() var invoice = btcpay.CreateInvoice(new Invoice()
{ {
Price = 5.0, Price = 5.0,
Currency = "USD", Currency = "USD",
PosData = "posData", PosData = "posData",
OrderId = "cdfd8a5f-6928-4c3b-ba9b-ddf438029e73", OrderId = "cdfd8a5f-6928-4c3b-ba9b-ddf438029e73",
ItemDesc = "Hello from the otherside" ItemDesc = "Hello from the otherside"
}, Facade.Merchant); }, Facade.Merchant);
// go to invoice.Url // go to invoice.Url
Console.WriteLine(invoice.Url); Console.WriteLine(invoice.Url);
} }
// Generating Extended public key to use on http://localhost:14142/stores/{storeId} // Generating Extended public key to use on http://localhost:14142/stores/{storeId}
[Fact] [Fact]
public void GeneratePubkey() public void GeneratePubkey()
{ {
var network = Network.RegTest; var network = Network.RegTest;
ExtKey masterKey = new ExtKey(); ExtKey masterKey = new ExtKey();
Console.WriteLine("Master key : " + masterKey.ToString(network)); Console.WriteLine("Master key : " + masterKey.ToString(network));
ExtPubKey masterPubKey = masterKey.Neuter(); ExtPubKey masterPubKey = masterKey.Neuter();
ExtPubKey pubkey = masterPubKey.Derive(0); ExtPubKey pubkey = masterPubKey.Derive(0);
Console.WriteLine("PubKey " + 0 + " : " + pubkey.ToString(network)); Console.WriteLine("PubKey " + 0 + " : " + pubkey.ToString(network));
} }
} }
} }

View File

@@ -10,54 +10,54 @@ namespace BTCPayServer.Tests
{ {
public class Utils public class Utils
{ {
public static int FreeTcpPort() public static int FreeTcpPort()
{ {
TcpListener l = new TcpListener(IPAddress.Loopback, 0); TcpListener l = new TcpListener(IPAddress.Loopback, 0);
l.Start(); l.Start();
int port = ((IPEndPoint)l.LocalEndpoint).Port; int port = ((IPEndPoint)l.LocalEndpoint).Port;
l.Stop(); l.Stop();
return port; return port;
} }
// http://stackoverflow.com/a/14933880/2061103 // http://stackoverflow.com/a/14933880/2061103
public static void DeleteDirectory(string destinationDir) public static void DeleteDirectory(string destinationDir)
{ {
const int magicDust = 10; const int magicDust = 10;
for(var gnomes = 1; gnomes <= magicDust; gnomes++) for (var gnomes = 1; gnomes <= magicDust; gnomes++)
{ {
try try
{ {
Directory.Delete(destinationDir, true); Directory.Delete(destinationDir, true);
} }
catch(DirectoryNotFoundException) catch (DirectoryNotFoundException)
{ {
return; // good! return; // good!
} }
catch(IOException) catch (IOException)
{ {
if(gnomes == magicDust) if (gnomes == magicDust)
throw; throw;
// System.IO.IOException: The directory is not empty // System.IO.IOException: The directory is not empty
System.Diagnostics.Debug.WriteLine("Gnomes prevent deletion of {0}! Applying magic dust, attempt #{1}.", destinationDir, gnomes); 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 // see http://stackoverflow.com/questions/329355/cannot-delete-directory-with-directory-deletepath-true for more magic
Thread.Sleep(100); Thread.Sleep(100);
continue; continue;
} }
catch(UnauthorizedAccessException) catch (UnauthorizedAccessException)
{ {
if(gnomes == magicDust) if (gnomes == magicDust)
throw; throw;
// Wait, maybe another software make us authorized a little later // 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); 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 // see http://stackoverflow.com/questions/329355/cannot-delete-directory-with-directory-deletepath-true for more magic
Thread.Sleep(100); Thread.Sleep(100);
continue; continue;
} }
return; return;
} }
// depending on your use case, consider throwing an exception here // depending on your use case, consider throwing an exception here
} }
} }
} }

View File

@@ -9,27 +9,27 @@ namespace BTCPayServer.Authentication
{ {
public class BitIdentity : IIdentity public class BitIdentity : IIdentity
{ {
public BitIdentity(PubKey key) public BitIdentity(PubKey key)
{ {
PubKey = key; PubKey = key;
_Name = Encoders.Base58Check.EncodeData(Encoders.Hex.DecodeData("0f02" + key.Hash.ToString())); _Name = Encoders.Base58Check.EncodeData(Encoders.Hex.DecodeData("0f02" + key.Hash.ToString()));
SIN = NBitpayClient.Extensions.BitIdExtensions.GetBitIDSIN(key); SIN = NBitpayClient.Extensions.BitIdExtensions.GetBitIDSIN(key);
} }
string _Name; string _Name;
public string SIN public string SIN
{ {
get; get;
} }
public PubKey PubKey public PubKey PubKey
{ {
get; get;
} }
public string AuthenticationType => "BitID"; public string AuthenticationType => "BitID";
public bool IsAuthenticated => true; public bool IsAuthenticated => true;
public string Name => _Name; public string Name => _Name;
} }
} }

View File

@@ -8,43 +8,43 @@ namespace BTCPayServer.Authentication
{ {
public class BitTokenEntity public class BitTokenEntity
{ {
public string Facade public string Facade
{ {
get; set; get; set;
} }
public string Value public string Value
{ {
get; set; get; set;
} }
public string StoreId public string StoreId
{ {
get; set; get; set;
} }
public string Label public string Label
{ {
get; set; get; set;
} }
public DateTimeOffset PairingTime public DateTimeOffset PairingTime
{ {
get; set; get; set;
} }
public string SIN public string SIN
{ {
get; get;
set; set;
} }
public BitTokenEntity Clone(Facade facade) public BitTokenEntity Clone(Facade facade)
{ {
return new BitTokenEntity() return new BitTokenEntity()
{ {
Label = Label, Label = Label,
Facade = Facade, Facade = Facade,
StoreId = StoreId, StoreId = StoreId,
PairingTime = PairingTime, PairingTime = PairingTime,
SIN = SIN, SIN = SIN,
Value = Value Value = Value
}; };
} }
} }
} }

View File

@@ -4,47 +4,47 @@ using System.Text;
namespace BTCPayServer.Authentication namespace BTCPayServer.Authentication
{ {
public class PairingCodeEntity public class PairingCodeEntity
{ {
public string Id public string Id
{ {
get; get;
set; set;
} }
public string Facade public string Facade
{ {
get; get;
set; set;
} }
public string Label public string Label
{ {
get; get;
set; set;
} }
public string SIN public string SIN
{ {
get; get;
set; set;
} }
public DateTimeOffset CreatedTime public DateTimeOffset CreatedTime
{ {
get; get;
set; set;
} }
public DateTimeOffset Expiration public DateTimeOffset Expiration
{ {
get; get;
set; set;
} }
public string TokenValue public string TokenValue
{ {
get; get;
set; set;
} }
public bool IsExpired() public bool IsExpired()
{ {
return DateTimeOffset.UtcNow > Expiration; return DateTimeOffset.UtcNow > Expiration;
} }
} }
} }

View File

@@ -13,196 +13,196 @@ using System.Linq;
namespace BTCPayServer.Authentication namespace BTCPayServer.Authentication
{ {
public enum PairingResult public enum PairingResult
{ {
Partial, Partial,
Complete, Complete,
ReusedKey, ReusedKey,
Expired Expired
} }
public class TokenRepository public class TokenRepository
{ {
ApplicationDbContextFactory _Factory; ApplicationDbContextFactory _Factory;
public TokenRepository(ApplicationDbContextFactory dbFactory) public TokenRepository(ApplicationDbContextFactory dbFactory)
{ {
if(dbFactory == null) if (dbFactory == null)
throw new ArgumentNullException(nameof(dbFactory)); throw new ArgumentNullException(nameof(dbFactory));
_Factory = dbFactory; _Factory = dbFactory;
} }
public async Task<BitTokenEntity[]> GetTokens(string sin) public async Task<BitTokenEntity[]> GetTokens(string sin)
{ {
using(var ctx = _Factory.CreateContext()) using (var ctx = _Factory.CreateContext())
{ {
return (await ctx.PairedSINData return (await ctx.PairedSINData
.Where(p => p.SIN == sin) .Where(p => p.SIN == sin)
.ToListAsync()) .ToListAsync())
.Select(p => CreateTokenEntity(p)) .Select(p => CreateTokenEntity(p))
.ToArray(); .ToArray();
} }
} }
private BitTokenEntity CreateTokenEntity(PairedSINData data) private BitTokenEntity CreateTokenEntity(PairedSINData data)
{ {
return new BitTokenEntity() return new BitTokenEntity()
{ {
Label = data.Label, Label = data.Label,
Facade = data.Facade, Facade = data.Facade,
Value = data.Id, Value = data.Id,
SIN = data.SIN, SIN = data.SIN,
PairingTime = data.PairingTime, PairingTime = data.PairingTime,
StoreId = data.StoreDataId StoreId = data.StoreDataId
}; };
} }
public async Task<string> CreatePairingCodeAsync() public async Task<string> CreatePairingCodeAsync()
{ {
string pairingCodeId = null; string pairingCodeId = null;
while(true) while (true)
{ {
pairingCodeId = Encoders.Base58.EncodeData(RandomUtils.GetBytes(6)); pairingCodeId = Encoders.Base58.EncodeData(RandomUtils.GetBytes(6));
if(pairingCodeId.Length == 7) // woocommerce plugin check for exactly 7 digits if (pairingCodeId.Length == 7) // woocommerce plugin check for exactly 7 digits
break; break;
} }
using(var ctx = _Factory.CreateContext()) using (var ctx = _Factory.CreateContext())
{ {
var now = DateTime.UtcNow; var now = DateTime.UtcNow;
var expiration = DateTime.UtcNow + TimeSpan.FromMinutes(15); var expiration = DateTime.UtcNow + TimeSpan.FromMinutes(15);
await ctx.PairingCodes.AddAsync(new PairingCodeData() await ctx.PairingCodes.AddAsync(new PairingCodeData()
{ {
Id = pairingCodeId, Id = pairingCodeId,
DateCreated = now, DateCreated = now,
Expiration = expiration, Expiration = expiration,
TokenValue = Encoders.Base58.EncodeData(RandomUtils.GetBytes(32)) TokenValue = Encoders.Base58.EncodeData(RandomUtils.GetBytes(32))
}); });
await ctx.SaveChangesAsync(); await ctx.SaveChangesAsync();
} }
return pairingCodeId; return pairingCodeId;
} }
public async Task<PairingCodeEntity> UpdatePairingCode(PairingCodeEntity pairingCodeEntity) public async Task<PairingCodeEntity> UpdatePairingCode(PairingCodeEntity pairingCodeEntity)
{ {
using(var ctx = _Factory.CreateContext()) using (var ctx = _Factory.CreateContext())
{ {
var pairingCode = await ctx.PairingCodes.FindAsync(pairingCodeEntity.Id); var pairingCode = await ctx.PairingCodes.FindAsync(pairingCodeEntity.Id);
pairingCode.Label = pairingCodeEntity.Label; pairingCode.Label = pairingCodeEntity.Label;
pairingCode.Facade = pairingCodeEntity.Facade; pairingCode.Facade = pairingCodeEntity.Facade;
await ctx.SaveChangesAsync(); await ctx.SaveChangesAsync();
return CreatePairingCodeEntity(pairingCode); return CreatePairingCodeEntity(pairingCode);
} }
} }
public async Task<PairingResult> PairWithStoreAsync(string pairingCodeId, string storeId) public async Task<PairingResult> PairWithStoreAsync(string pairingCodeId, string storeId)
{ {
using(var ctx = _Factory.CreateContext()) using (var ctx = _Factory.CreateContext())
{ {
var pairingCode = await ctx.PairingCodes.FindAsync(pairingCodeId); var pairingCode = await ctx.PairingCodes.FindAsync(pairingCodeId);
if(pairingCode == null || pairingCode.Expiration < DateTimeOffset.UtcNow) if (pairingCode == null || pairingCode.Expiration < DateTimeOffset.UtcNow)
return PairingResult.Expired; return PairingResult.Expired;
pairingCode.StoreDataId = storeId; pairingCode.StoreDataId = storeId;
var result = await ActivateIfComplete(ctx, pairingCode); var result = await ActivateIfComplete(ctx, pairingCode);
await ctx.SaveChangesAsync(); await ctx.SaveChangesAsync();
return result; return result;
} }
} }
public async Task<PairingResult> PairWithSINAsync(string pairingCodeId, string sin) public async Task<PairingResult> PairWithSINAsync(string pairingCodeId, string sin)
{ {
using(var ctx = _Factory.CreateContext()) using (var ctx = _Factory.CreateContext())
{ {
var pairingCode = await ctx.PairingCodes.FindAsync(pairingCodeId); var pairingCode = await ctx.PairingCodes.FindAsync(pairingCodeId);
if(pairingCode == null || pairingCode.Expiration < DateTimeOffset.UtcNow) if (pairingCode == null || pairingCode.Expiration < DateTimeOffset.UtcNow)
return PairingResult.Expired; return PairingResult.Expired;
pairingCode.SIN = sin; pairingCode.SIN = sin;
var result = await ActivateIfComplete(ctx, pairingCode); var result = await ActivateIfComplete(ctx, pairingCode);
await ctx.SaveChangesAsync(); await ctx.SaveChangesAsync();
return result; return result;
} }
} }
private async Task<PairingResult> ActivateIfComplete(ApplicationDbContext ctx, PairingCodeData pairingCode) private async Task<PairingResult> ActivateIfComplete(ApplicationDbContext ctx, PairingCodeData pairingCode)
{ {
if(!string.IsNullOrEmpty(pairingCode.SIN) && !string.IsNullOrEmpty(pairingCode.StoreDataId)) if (!string.IsNullOrEmpty(pairingCode.SIN) && !string.IsNullOrEmpty(pairingCode.StoreDataId))
{ {
ctx.PairingCodes.Remove(pairingCode); ctx.PairingCodes.Remove(pairingCode);
// Can have concurrency issues... but no harm can be done // Can have concurrency issues... but no harm can be done
var alreadyUsed = await ctx.PairedSINData.Where(p => p.SIN == pairingCode.SIN && p.StoreDataId != pairingCode.StoreDataId).AnyAsync(); var alreadyUsed = await ctx.PairedSINData.Where(p => p.SIN == pairingCode.SIN && p.StoreDataId != pairingCode.StoreDataId).AnyAsync();
if(alreadyUsed) if (alreadyUsed)
return PairingResult.ReusedKey; return PairingResult.ReusedKey;
await ctx.PairedSINData.AddAsync(new PairedSINData() await ctx.PairedSINData.AddAsync(new PairedSINData()
{ {
Id = pairingCode.TokenValue, Id = pairingCode.TokenValue,
PairingTime = DateTime.UtcNow, PairingTime = DateTime.UtcNow,
Facade = pairingCode.Facade, Facade = pairingCode.Facade,
Label = pairingCode.Label, Label = pairingCode.Label,
StoreDataId = pairingCode.StoreDataId, StoreDataId = pairingCode.StoreDataId,
SIN = pairingCode.SIN SIN = pairingCode.SIN
}); });
return PairingResult.Complete; return PairingResult.Complete;
} }
return PairingResult.Partial; return PairingResult.Partial;
} }
public async Task<BitTokenEntity[]> GetTokensByStoreIdAsync(string storeId) public async Task<BitTokenEntity[]> GetTokensByStoreIdAsync(string storeId)
{ {
using(var ctx = _Factory.CreateContext()) using (var ctx = _Factory.CreateContext())
{ {
return (await ctx.PairedSINData.Where(p => p.StoreDataId == storeId).ToListAsync()) return (await ctx.PairedSINData.Where(p => p.StoreDataId == storeId).ToListAsync())
.Select(c => CreateTokenEntity(c)) .Select(c => CreateTokenEntity(c))
.ToArray(); .ToArray();
} }
} }
public async Task<PairingCodeEntity> GetPairingAsync(string pairingCode) public async Task<PairingCodeEntity> GetPairingAsync(string pairingCode)
{ {
using(var ctx = _Factory.CreateContext()) using (var ctx = _Factory.CreateContext())
{ {
return CreatePairingCodeEntity(await ctx.PairingCodes.FindAsync(pairingCode)); return CreatePairingCodeEntity(await ctx.PairingCodes.FindAsync(pairingCode));
} }
} }
private PairingCodeEntity CreatePairingCodeEntity(PairingCodeData data) private PairingCodeEntity CreatePairingCodeEntity(PairingCodeData data)
{ {
if(data == null) if (data == null)
return null; return null;
return new PairingCodeEntity() return new PairingCodeEntity()
{ {
Facade = data.Facade, Facade = data.Facade,
Id = data.Id, Id = data.Id,
Label = data.Label, Label = data.Label,
Expiration = data.Expiration, Expiration = data.Expiration,
CreatedTime = data.DateCreated, CreatedTime = data.DateCreated,
TokenValue = data.TokenValue, TokenValue = data.TokenValue,
SIN = data.SIN SIN = data.SIN
}; };
} }
public async Task<bool> DeleteToken(string tokenId) public async Task<bool> DeleteToken(string tokenId)
{ {
using(var ctx = _Factory.CreateContext()) using (var ctx = _Factory.CreateContext())
{ {
var token = await ctx.PairedSINData.FindAsync(tokenId); var token = await ctx.PairedSINData.FindAsync(tokenId);
if(token == null) if (token == null)
return false; return false;
ctx.PairedSINData.Remove(token); ctx.PairedSINData.Remove(token);
await ctx.SaveChangesAsync(); await ctx.SaveChangesAsync();
return true; return true;
} }
} }
public async Task<BitTokenEntity> GetToken(string tokenId) public async Task<BitTokenEntity> GetToken(string tokenId)
{ {
using(var ctx = _Factory.CreateContext()) using (var ctx = _Factory.CreateContext())
{ {
var token = await ctx.PairedSINData.FindAsync(tokenId); var token = await ctx.PairedSINData.FindAsync(tokenId);
return CreateTokenEntity(token); return CreateTokenEntity(token);
} }
} }
} }
} }

View File

@@ -4,15 +4,15 @@ using System.Text;
namespace BTCPayServer namespace BTCPayServer
{ {
public class BitpayHttpException : Exception public class BitpayHttpException : Exception
{ {
public BitpayHttpException(int code, string message) : base(message) public BitpayHttpException(int code, string message) : base(message)
{ {
StatusCode = code; StatusCode = code;
} }
public int StatusCode public int StatusCode
{ {
get; set; get; set;
} }
} }
} }

View File

@@ -12,61 +12,61 @@ using Microsoft.Extensions.Configuration;
namespace BTCPayServer.Configuration namespace BTCPayServer.Configuration
{ {
public class BTCPayServerOptions public class BTCPayServerOptions
{ {
public Network Network public Network Network
{ {
get; set; get; set;
} }
public Uri Explorer public Uri Explorer
{ {
get; set; get; set;
} }
public string CookieFile public string CookieFile
{ {
get; set; get; set;
} }
public string ConfigurationFile public string ConfigurationFile
{ {
get; get;
private set; private set;
} }
public string DataDir public string DataDir
{ {
get; get;
private set; private set;
} }
public List<IPEndPoint> Listen public List<IPEndPoint> Listen
{ {
get; get;
set; set;
} }
public void LoadArgs(IConfiguration conf) public void LoadArgs(IConfiguration conf)
{ {
var networkInfo = DefaultConfiguration.GetNetwork(conf); var networkInfo = DefaultConfiguration.GetNetwork(conf);
Network = networkInfo?.Network; Network = networkInfo?.Network;
if(Network == null) if (Network == null)
throw new ConfigException("Invalid network"); throw new ConfigException("Invalid network");
DataDir = conf.GetOrDefault<string>("datadir", networkInfo.DefaultDataDirectory); DataDir = conf.GetOrDefault<string>("datadir", networkInfo.DefaultDataDirectory);
Logs.Configuration.LogInformation("Network: " + Network); Logs.Configuration.LogInformation("Network: " + Network);
Explorer = conf.GetOrDefault<Uri>("explorer.url", networkInfo.DefaultExplorerUrl); Explorer = conf.GetOrDefault<Uri>("explorer.url", networkInfo.DefaultExplorerUrl);
CookieFile = conf.GetOrDefault<string>("explorer.cookiefile", networkInfo.DefaultExplorerCookieFile); CookieFile = conf.GetOrDefault<string>("explorer.cookiefile", networkInfo.DefaultExplorerCookieFile);
RequireHttps = conf.GetOrDefault<bool>("requirehttps", false); RequireHttps = conf.GetOrDefault<bool>("requirehttps", false);
PostgresConnectionString = conf.GetOrDefault<string>("postgres", null); PostgresConnectionString = conf.GetOrDefault<string>("postgres", null);
} }
public bool RequireHttps public bool RequireHttps
{ {
get; set; get; set;
} }
public string PostgresConnectionString public string PostgresConnectionString
{ {
get; get;
set; set;
} }
} }
} }

View File

@@ -18,96 +18,96 @@ using BTCPayServer.Services.Wallets;
namespace BTCPayServer.Configuration namespace BTCPayServer.Configuration
{ {
public class BTCPayServerRuntime : IDisposable public class BTCPayServerRuntime : IDisposable
{ {
public ExplorerClient Explorer public ExplorerClient Explorer
{ {
get; get;
private set; private set;
} }
public void Configure(BTCPayServerOptions opts) public void Configure(BTCPayServerOptions opts)
{ {
ConfigureAsync(opts).GetAwaiter().GetResult(); ConfigureAsync(opts).GetAwaiter().GetResult();
} }
public async Task ConfigureAsync(BTCPayServerOptions opts) public async Task ConfigureAsync(BTCPayServerOptions opts)
{ {
Network = opts.Network; Network = opts.Network;
Explorer = new ExplorerClient(opts.Network, opts.Explorer); Explorer = new ExplorerClient(opts.Network, opts.Explorer);
if(!Explorer.SetCookieAuth(opts.CookieFile)) if (!Explorer.SetCookieAuth(opts.CookieFile))
Explorer.SetNoAuth(); Explorer.SetNoAuth();
CancellationTokenSource cts = new CancellationTokenSource(30000); CancellationTokenSource cts = new CancellationTokenSource(30000);
try try
{ {
Logs.Configuration.LogInformation("Trying to connect to explorer " + Explorer.Address.AbsoluteUri); Logs.Configuration.LogInformation("Trying to connect to explorer " + Explorer.Address.AbsoluteUri);
await Explorer.WaitServerStartedAsync(cts.Token).ConfigureAwait(false); await Explorer.WaitServerStartedAsync(cts.Token).ConfigureAwait(false);
Logs.Configuration.LogInformation("Connection successfull"); Logs.Configuration.LogInformation("Connection successfull");
} }
catch(Exception ex) catch (Exception ex)
{ {
throw new ConfigException($"Could not connect to NBXplorer, {ex.Message}"); throw new ConfigException($"Could not connect to NBXplorer, {ex.Message}");
} }
DBreezeEngine db = new DBreezeEngine(CreateDBPath(opts, "TokensDB")); DBreezeEngine db = new DBreezeEngine(CreateDBPath(opts, "TokensDB"));
_Resources.Add(db); _Resources.Add(db);
db = new DBreezeEngine(CreateDBPath(opts, "InvoiceDB")); db = new DBreezeEngine(CreateDBPath(opts, "InvoiceDB"));
_Resources.Add(db); _Resources.Add(db);
ApplicationDbContextFactory dbContext = null; ApplicationDbContextFactory dbContext = null;
if(opts.PostgresConnectionString == null) if (opts.PostgresConnectionString == null)
{ {
var connStr = "Data Source=" + Path.Combine(opts.DataDir, "sqllite.db"); var connStr = "Data Source=" + Path.Combine(opts.DataDir, "sqllite.db");
Logs.Configuration.LogInformation($"SQLite DB used ({connStr})"); Logs.Configuration.LogInformation($"SQLite DB used ({connStr})");
dbContext = new ApplicationDbContextFactory(DatabaseType.Sqlite, connStr); dbContext = new ApplicationDbContextFactory(DatabaseType.Sqlite, connStr);
} }
else else
{ {
Logs.Configuration.LogInformation($"Postgres DB used ({opts.PostgresConnectionString})"); Logs.Configuration.LogInformation($"Postgres DB used ({opts.PostgresConnectionString})");
dbContext = new ApplicationDbContextFactory(DatabaseType.Postgres, opts.PostgresConnectionString); dbContext = new ApplicationDbContextFactory(DatabaseType.Postgres, opts.PostgresConnectionString);
} }
DBFactory = dbContext; DBFactory = dbContext;
InvoiceRepository = new InvoiceRepository(dbContext, db, Network); InvoiceRepository = new InvoiceRepository(dbContext, db, Network);
} }
private static string CreateDBPath(BTCPayServerOptions opts, string name) private static string CreateDBPath(BTCPayServerOptions opts, string name)
{ {
var dbpath = Path.Combine(opts.DataDir, name); var dbpath = Path.Combine(opts.DataDir, name);
if(!Directory.Exists(dbpath)) if (!Directory.Exists(dbpath))
Directory.CreateDirectory(dbpath); Directory.CreateDirectory(dbpath);
return dbpath; return dbpath;
} }
List<IDisposable> _Resources = new List<IDisposable>(); List<IDisposable> _Resources = new List<IDisposable>();
public void Dispose() public void Dispose()
{ {
lock(_Resources) lock (_Resources)
{ {
foreach(var r in _Resources) foreach (var r in _Resources)
{ {
r.Dispose(); r.Dispose();
} }
_Resources.Clear(); _Resources.Clear();
} }
} }
public Network Network public Network Network
{ {
get; get;
private set; private set;
} }
public InvoiceRepository InvoiceRepository public InvoiceRepository InvoiceRepository
{ {
get; get;
set; set;
} }
public ApplicationDbContextFactory DBFactory public ApplicationDbContextFactory DBFactory
{ {
get; get;
set; set;
} }
} }
} }

View File

@@ -5,11 +5,11 @@ using System.Threading.Tasks;
namespace BTCPayServer.Configuration namespace BTCPayServer.Configuration
{ {
public class ConfigException : Exception public class ConfigException : Exception
{ {
public ConfigException(string message) : base(message) public ConfigException(string message) : base(message)
{ {
} }
} }
} }

View File

@@ -9,44 +9,44 @@ using Microsoft.Extensions.Primitives;
namespace BTCPayServer.Configuration namespace BTCPayServer.Configuration
{ {
public static class ConfigurationExtensions public static class ConfigurationExtensions
{ {
public static T GetOrDefault<T>(this IConfiguration configuration, string key, T defaultValue) public static T GetOrDefault<T>(this IConfiguration configuration, string key, T defaultValue)
{ {
var str = configuration[key] ?? configuration[key.Replace(".", string.Empty)]; var str = configuration[key] ?? configuration[key.Replace(".", string.Empty)];
if(str == null) if (str == null)
return defaultValue; return defaultValue;
if(typeof(T) == typeof(bool)) if (typeof(T) == typeof(bool))
{ {
var trueValues = new[] { "1", "true" }; var trueValues = new[] { "1", "true" };
var falseValues = new[] { "0", "false" }; var falseValues = new[] { "0", "false" };
if(trueValues.Contains(str, StringComparer.OrdinalIgnoreCase)) if (trueValues.Contains(str, StringComparer.OrdinalIgnoreCase))
return (T)(object)true; return (T)(object)true;
if(falseValues.Contains(str, StringComparer.OrdinalIgnoreCase)) if (falseValues.Contains(str, StringComparer.OrdinalIgnoreCase))
return (T)(object)false; return (T)(object)false;
throw new FormatException(); throw new FormatException();
} }
else if(typeof(T) == typeof(Uri)) else if (typeof(T) == typeof(Uri))
return (T)(object)new Uri(str, UriKind.Absolute); return (T)(object)new Uri(str, UriKind.Absolute);
else if(typeof(T) == typeof(string)) else if (typeof(T) == typeof(string))
return (T)(object)str; return (T)(object)str;
else if(typeof(T) == typeof(IPEndPoint)) else if (typeof(T) == typeof(IPEndPoint))
{ {
var separator = str.LastIndexOf(":"); var separator = str.LastIndexOf(":");
if(separator == -1) if (separator == -1)
throw new FormatException(); throw new FormatException();
var ip = str.Substring(0, separator); var ip = str.Substring(0, separator);
var port = str.Substring(separator + 1); var port = str.Substring(separator + 1);
return (T)(object)new IPEndPoint(IPAddress.Parse(ip), int.Parse(port)); return (T)(object)new IPEndPoint(IPAddress.Parse(ip), int.Parse(port));
} }
else if(typeof(T) == typeof(int)) else if (typeof(T) == typeof(int))
{ {
return (T)(object)int.Parse(str, CultureInfo.InvariantCulture); return (T)(object)int.Parse(str, CultureInfo.InvariantCulture);
} }
else else
{ {
throw new NotSupportedException("Configuration value does not support time " + typeof(T).Name); throw new NotSupportedException("Configuration value does not support time " + typeof(T).Name);
} }
} }
} }
} }

View File

@@ -13,88 +13,88 @@ using CommandLine;
namespace BTCPayServer.Configuration namespace BTCPayServer.Configuration
{ {
public class DefaultConfiguration : StandardConfiguration.DefaultConfiguration public class DefaultConfiguration : StandardConfiguration.DefaultConfiguration
{ {
protected override CommandLineApplication CreateCommandLineApplicationCore() protected override CommandLineApplication CreateCommandLineApplicationCore()
{ {
CommandLineApplication app = new CommandLineApplication(true) CommandLineApplication app = new CommandLineApplication(true)
{ {
FullName = "NBXplorer\r\nLightweight block explorer for tracking HD wallets", FullName = "NBXplorer\r\nLightweight block explorer for tracking HD wallets",
Name = "NBXplorer" Name = "NBXplorer"
}; };
app.HelpOption("-? | -h | --help"); app.HelpOption("-? | -h | --help");
app.Option("-n | --network", $"Set the network among ({NetworkInformation.ToStringAll()}) (default: {Network.Main.ToString()})", CommandOptionType.SingleValue); app.Option("-n | --network", $"Set the network among ({NetworkInformation.ToStringAll()}) (default: {Network.Main.ToString()})", CommandOptionType.SingleValue);
app.Option("--testnet | -testnet", $"Use testnet", CommandOptionType.BoolValue); app.Option("--testnet | -testnet", $"Use testnet", CommandOptionType.BoolValue);
app.Option("--regtest | -regtest", $"Use regtest", CommandOptionType.BoolValue); app.Option("--regtest | -regtest", $"Use regtest", CommandOptionType.BoolValue);
app.Option("--requirehttps", $"Will redirect to https version of the website (default: false)", CommandOptionType.BoolValue); app.Option("--requirehttps", $"Will redirect to https version of the website (default: false)", CommandOptionType.BoolValue);
app.Option("--postgres", $"Connection string to postgres database (default: sqlite is used)", CommandOptionType.SingleValue); app.Option("--postgres", $"Connection string to postgres database (default: sqlite is used)", CommandOptionType.SingleValue);
app.Option("--explorerurl", $"Url of the NBxplorer (default: : Default setting of NBXplorer for the network)", CommandOptionType.SingleValue); app.Option("--explorerurl", $"Url of the NBxplorer (default: : Default setting of NBXplorer for the network)", CommandOptionType.SingleValue);
app.Option("--explorercookiefile", $"Path to the cookie file (default: Default setting of NBXplorer for the network)", CommandOptionType.SingleValue); app.Option("--explorercookiefile", $"Path to the cookie file (default: Default setting of NBXplorer for the network)", CommandOptionType.SingleValue);
return app; return app;
} }
public override string EnvironmentVariablePrefix => "BTCPAY_"; public override string EnvironmentVariablePrefix => "BTCPAY_";
protected override string GetDefaultDataDir(IConfiguration conf) protected override string GetDefaultDataDir(IConfiguration conf)
{ {
return GetNetwork(conf).DefaultDataDirectory; return GetNetwork(conf).DefaultDataDirectory;
} }
protected override string GetDefaultConfigurationFile(IConfiguration conf) protected override string GetDefaultConfigurationFile(IConfiguration conf)
{ {
var network = GetNetwork(conf); var network = GetNetwork(conf);
var dataDir = conf["datadir"]; var dataDir = conf["datadir"];
if(dataDir == null) if (dataDir == null)
return network.DefaultConfigurationFile; return network.DefaultConfigurationFile;
var fileName = Path.GetFileName(network.DefaultConfigurationFile); var fileName = Path.GetFileName(network.DefaultConfigurationFile);
return Path.Combine(dataDir, fileName); return Path.Combine(dataDir, fileName);
} }
public static NetworkInformation GetNetwork(IConfiguration conf) public static NetworkInformation GetNetwork(IConfiguration conf)
{ {
var network = conf.GetOrDefault<string>("network", null); var network = conf.GetOrDefault<string>("network", null);
if(network != null) if (network != null)
{ {
var info = NetworkInformation.GetNetworkByName(network); var info = NetworkInformation.GetNetworkByName(network);
if(info == null) if (info == null)
throw new ConfigException($"Invalid network name {network}"); throw new ConfigException($"Invalid network name {network}");
return info; return info;
} }
var net = conf.GetOrDefault<bool>("regtest", false) ? Network.RegTest : var net = conf.GetOrDefault<bool>("regtest", false) ? Network.RegTest :
conf.GetOrDefault<bool>("testnet", false) ? Network.TestNet : Network.Main; conf.GetOrDefault<bool>("testnet", false) ? Network.TestNet : Network.Main;
return NetworkInformation.GetNetworkByName(net.Name); return NetworkInformation.GetNetworkByName(net.Name);
} }
protected override string GetDefaultConfigurationFileTemplate(IConfiguration conf) protected override string GetDefaultConfigurationFileTemplate(IConfiguration conf)
{ {
var network = GetNetwork(conf); var network = GetNetwork(conf);
StringBuilder builder = new StringBuilder(); StringBuilder builder = new StringBuilder();
builder.AppendLine("### Global settings ###"); builder.AppendLine("### Global settings ###");
builder.AppendLine("#testnet=0"); builder.AppendLine("#testnet=0");
builder.AppendLine("#regtest=0"); builder.AppendLine("#regtest=0");
builder.AppendLine(); builder.AppendLine();
builder.AppendLine("### Server settings ###"); builder.AppendLine("### Server settings ###");
builder.AppendLine("#requirehttps=0"); builder.AppendLine("#requirehttps=0");
builder.AppendLine("#port=" + network.DefaultPort); builder.AppendLine("#port=" + network.DefaultPort);
builder.AppendLine("#bind=127.0.0.1"); builder.AppendLine("#bind=127.0.0.1");
builder.AppendLine(); builder.AppendLine();
builder.AppendLine("### Database ###"); builder.AppendLine("### Database ###");
builder.AppendLine("#postgres=User ID=root;Password=myPassword;Host=localhost;Port=5432;Database=myDataBase;"); builder.AppendLine("#postgres=User ID=root;Password=myPassword;Host=localhost;Port=5432;Database=myDataBase;");
builder.AppendLine(); builder.AppendLine();
builder.AppendLine("### NBXplorer settings ###"); builder.AppendLine("### NBXplorer settings ###");
builder.AppendLine("#explorer.url=" + network.DefaultExplorerUrl.AbsoluteUri); builder.AppendLine("#explorer.url=" + network.DefaultExplorerUrl.AbsoluteUri);
builder.AppendLine("#explorer.cookiefile=" + network.DefaultExplorerCookieFile); builder.AppendLine("#explorer.cookiefile=" + network.DefaultExplorerCookieFile);
return builder.ToString(); return builder.ToString();
} }
protected override IPEndPoint GetDefaultEndpoint(IConfiguration conf) protected override IPEndPoint GetDefaultEndpoint(IConfiguration conf)
{ {
return new IPEndPoint(IPAddress.Parse("127.0.0.1"), GetNetwork(conf).DefaultPort); return new IPEndPoint(IPAddress.Parse("127.0.0.1"), GetNetwork(conf).DefaultPort);
} }
} }
} }

View File

@@ -8,96 +8,96 @@ using System.Threading.Tasks;
namespace BTCPayServer.Configuration namespace BTCPayServer.Configuration
{ {
public class NetworkInformation public class NetworkInformation
{ {
static NetworkInformation() static NetworkInformation()
{ {
_Networks = new Dictionary<string, NetworkInformation>(); _Networks = new Dictionary<string, NetworkInformation>();
foreach(var network in Network.GetNetworks()) foreach (var network in Network.GetNetworks())
{ {
NetworkInformation info = new NetworkInformation(); NetworkInformation info = new NetworkInformation();
info.DefaultDataDirectory = StandardConfiguration.DefaultDataDirectory.GetDirectory("BTCPayServer", network.Name); info.DefaultDataDirectory = StandardConfiguration.DefaultDataDirectory.GetDirectory("BTCPayServer", network.Name);
info.DefaultConfigurationFile = Path.Combine(info.DefaultDataDirectory, "settings.config"); info.DefaultConfigurationFile = Path.Combine(info.DefaultDataDirectory, "settings.config");
info.DefaultExplorerCookieFile = Path.Combine(StandardConfiguration.DefaultDataDirectory.GetDirectory("NBXplorer", network.Name, false), ".cookie"); info.DefaultExplorerCookieFile = Path.Combine(StandardConfiguration.DefaultDataDirectory.GetDirectory("NBXplorer", network.Name, false), ".cookie");
info.Network = network; info.Network = network;
info.DefaultExplorerUrl = new Uri("http://127.0.0.1:24446", UriKind.Absolute); info.DefaultExplorerUrl = new Uri("http://127.0.0.1:24446", UriKind.Absolute);
info.DefaultPort = 23002; info.DefaultPort = 23002;
_Networks.Add(network.Name, info); _Networks.Add(network.Name, info);
if(network == Network.Main) if (network == Network.Main)
{ {
info.DefaultExplorerUrl = new Uri("http://127.0.0.1:24444", UriKind.Absolute); info.DefaultExplorerUrl = new Uri("http://127.0.0.1:24444", UriKind.Absolute);
Main = info; Main = info;
info.DefaultPort = 23000; info.DefaultPort = 23000;
} }
if(network == Network.TestNet) if (network == Network.TestNet)
{ {
info.DefaultExplorerUrl = new Uri("http://127.0.0.1:24445", UriKind.Absolute); info.DefaultExplorerUrl = new Uri("http://127.0.0.1:24445", UriKind.Absolute);
info.DefaultPort = 23001; info.DefaultPort = 23001;
} }
} }
} }
static Dictionary<string, NetworkInformation> _Networks; static Dictionary<string, NetworkInformation> _Networks;
public static NetworkInformation GetNetworkByName(string name) public static NetworkInformation GetNetworkByName(string name)
{ {
var value = _Networks.TryGet(name); var value = _Networks.TryGet(name);
if(value != null) if (value != null)
return value; return value;
//Maybe alias ? //Maybe alias ?
var network = Network.GetNetwork(name); var network = Network.GetNetwork(name);
if(network != null) if (network != null)
{ {
value = _Networks.TryGet(network.Name); value = _Networks.TryGet(network.Name);
if(value != null) if (value != null)
return value; return value;
} }
return null; return null;
} }
public static NetworkInformation Main public static NetworkInformation Main
{ {
get; get;
set; set;
} }
public Network Network public Network Network
{ {
get; set; get; set;
} }
public string DefaultConfigurationFile public string DefaultConfigurationFile
{ {
get; get;
set; set;
} }
public string DefaultDataDirectory public string DefaultDataDirectory
{ {
get; get;
set; set;
} }
public Uri DefaultExplorerUrl public Uri DefaultExplorerUrl
{ {
get; get;
internal set; internal set;
} }
public int DefaultPort public int DefaultPort
{ {
get; get;
private set; private set;
} }
public string DefaultExplorerCookieFile public string DefaultExplorerCookieFile
{ {
get; get;
internal set; internal set;
} }
public override string ToString() public override string ToString()
{ {
return Network.ToString(); return Network.ToString();
} }
public static string ToStringAll() public static string ToStringAll()
{ {
return string.Join(", ", _Networks.Select(n => n.Key).ToArray()); return string.Join(", ", _Networks.Select(n => n.Key).ToArray());
} }
} }
} }

View File

@@ -12,79 +12,79 @@ using System.Threading.Tasks;
namespace BTCPayServer.Controllers namespace BTCPayServer.Controllers
{ {
public class AccessTokenController : Controller public class AccessTokenController : Controller
{ {
TokenRepository _TokenRepository; TokenRepository _TokenRepository;
public AccessTokenController(TokenRepository tokenRepository) public AccessTokenController(TokenRepository tokenRepository)
{ {
_TokenRepository = tokenRepository ?? throw new ArgumentNullException(nameof(tokenRepository)); _TokenRepository = tokenRepository ?? throw new ArgumentNullException(nameof(tokenRepository));
} }
[HttpGet] [HttpGet]
[Route("tokens")] [Route("tokens")]
public async Task<GetTokensResponse> Tokens() public async Task<GetTokensResponse> Tokens()
{ {
var tokens = await _TokenRepository.GetTokens(this.GetBitIdentity().SIN); var tokens = await _TokenRepository.GetTokens(this.GetBitIdentity().SIN);
return new GetTokensResponse(tokens); return new GetTokensResponse(tokens);
} }
[HttpPost] [HttpPost]
[Route("tokens")] [Route("tokens")]
public async Task<DataWrapper<List<PairingCodeResponse>>> Tokens([FromBody] TokenRequest request) public async Task<DataWrapper<List<PairingCodeResponse>>> Tokens([FromBody] TokenRequest request)
{ {
PairingCodeEntity pairingEntity = null; PairingCodeEntity pairingEntity = null;
if(string.IsNullOrEmpty(request.PairingCode)) if (string.IsNullOrEmpty(request.PairingCode))
{ {
if(string.IsNullOrEmpty(request.Id) || !NBitpayClient.Extensions.BitIdExtensions.ValidateSIN(request.Id)) if (string.IsNullOrEmpty(request.Id) || !NBitpayClient.Extensions.BitIdExtensions.ValidateSIN(request.Id))
throw new BitpayHttpException(400, "'id' property is required"); throw new BitpayHttpException(400, "'id' property is required");
if(string.IsNullOrEmpty(request.Facade)) if (string.IsNullOrEmpty(request.Facade))
throw new BitpayHttpException(400, "'facade' property is required"); throw new BitpayHttpException(400, "'facade' property is required");
var pairingCode = await _TokenRepository.CreatePairingCodeAsync(); var pairingCode = await _TokenRepository.CreatePairingCodeAsync();
await _TokenRepository.PairWithSINAsync(pairingCode, request.Id); await _TokenRepository.PairWithSINAsync(pairingCode, request.Id);
pairingEntity = await _TokenRepository.UpdatePairingCode(new PairingCodeEntity() pairingEntity = await _TokenRepository.UpdatePairingCode(new PairingCodeEntity()
{ {
Id = pairingCode, Id = pairingCode,
Facade = request.Facade, Facade = request.Facade,
Label = request.Label Label = request.Label
}); });
} }
else else
{ {
var sin = this.GetBitIdentity(false)?.SIN ?? request.Id; var sin = this.GetBitIdentity(false)?.SIN ?? request.Id;
if(string.IsNullOrEmpty(request.Id) || !NBitpayClient.Extensions.BitIdExtensions.ValidateSIN(request.Id)) if (string.IsNullOrEmpty(request.Id) || !NBitpayClient.Extensions.BitIdExtensions.ValidateSIN(request.Id))
throw new BitpayHttpException(400, "'id' property is required, alternatively, use BitId"); throw new BitpayHttpException(400, "'id' property is required, alternatively, use BitId");
pairingEntity = await _TokenRepository.GetPairingAsync(request.PairingCode); pairingEntity = await _TokenRepository.GetPairingAsync(request.PairingCode);
if(pairingEntity == null) if (pairingEntity == null)
throw new BitpayHttpException(404, "The specified pairingCode is not found"); throw new BitpayHttpException(404, "The specified pairingCode is not found");
pairingEntity.SIN = sin; pairingEntity.SIN = sin;
if(string.IsNullOrEmpty(pairingEntity.Label) && !string.IsNullOrEmpty(request.Label)) if (string.IsNullOrEmpty(pairingEntity.Label) && !string.IsNullOrEmpty(request.Label))
{ {
pairingEntity.Label = request.Label; pairingEntity.Label = request.Label;
await _TokenRepository.UpdatePairingCode(pairingEntity); await _TokenRepository.UpdatePairingCode(pairingEntity);
} }
var result = await _TokenRepository.PairWithSINAsync(request.PairingCode, sin); var result = await _TokenRepository.PairWithSINAsync(request.PairingCode, sin);
if(result != PairingResult.Complete && result != PairingResult.Partial) if (result != PairingResult.Complete && result != PairingResult.Partial)
throw new BitpayHttpException(400, $"Error while pairing ({result})"); throw new BitpayHttpException(400, $"Error while pairing ({result})");
}
var pairingCodes = new List<PairingCodeResponse> }
{
new PairingCodeResponse() var pairingCodes = new List<PairingCodeResponse>
{ {
PairingCode = pairingEntity.Id, new PairingCodeResponse()
PairingExpiration = pairingEntity.Expiration, {
DateCreated = pairingEntity.CreatedTime, PairingCode = pairingEntity.Id,
Facade = pairingEntity.Facade, PairingExpiration = pairingEntity.Expiration,
Token = pairingEntity.TokenValue, DateCreated = pairingEntity.CreatedTime,
Label = pairingEntity.Label Facade = pairingEntity.Facade,
} Token = pairingEntity.TokenValue,
}; Label = pairingEntity.Label
return DataWrapper.Create(pairingCodes); }
} };
} return DataWrapper.Create(pairingCodes);
}
}
} }

View File

@@ -18,503 +18,503 @@ using BTCPayServer.Services.Stores;
namespace BTCPayServer.Controllers namespace BTCPayServer.Controllers
{ {
[Authorize] [Authorize]
[Route("[controller]/[action]")] [Route("[controller]/[action]")]
public class AccountController : Controller public class AccountController : Controller
{ {
private readonly UserManager<ApplicationUser> _userManager; private readonly UserManager<ApplicationUser> _userManager;
private readonly SignInManager<ApplicationUser> _signInManager; private readonly SignInManager<ApplicationUser> _signInManager;
private readonly IEmailSender _emailSender; private readonly IEmailSender _emailSender;
private readonly ILogger _logger; private readonly ILogger _logger;
StoreRepository storeRepository; StoreRepository storeRepository;
RoleManager<IdentityRole> _RoleManager; RoleManager<IdentityRole> _RoleManager;
SettingsRepository _SettingsRepository; SettingsRepository _SettingsRepository;
public AccountController( public AccountController(
UserManager<ApplicationUser> userManager, UserManager<ApplicationUser> userManager,
RoleManager<IdentityRole> roleManager, RoleManager<IdentityRole> roleManager,
StoreRepository storeRepository, StoreRepository storeRepository,
SignInManager<ApplicationUser> signInManager, SignInManager<ApplicationUser> signInManager,
IEmailSender emailSender, IEmailSender emailSender,
SettingsRepository settingsRepository, SettingsRepository settingsRepository,
ILogger<AccountController> logger) ILogger<AccountController> logger)
{ {
this.storeRepository = storeRepository; this.storeRepository = storeRepository;
_userManager = userManager; _userManager = userManager;
_signInManager = signInManager; _signInManager = signInManager;
_emailSender = emailSender; _emailSender = emailSender;
_logger = logger; _logger = logger;
_RoleManager = roleManager; _RoleManager = roleManager;
_SettingsRepository = settingsRepository; _SettingsRepository = settingsRepository;
} }
[TempData] [TempData]
public string ErrorMessage public string ErrorMessage
{ {
get; set; get; set;
} }
[HttpGet] [HttpGet]
[AllowAnonymous] [AllowAnonymous]
public async Task<IActionResult> Login(string returnUrl = null) public async Task<IActionResult> Login(string returnUrl = null)
{ {
// Clear the existing external cookie to ensure a clean login process // Clear the existing external cookie to ensure a clean login process
await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme); await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);
ViewData["ReturnUrl"] = returnUrl; ViewData["ReturnUrl"] = returnUrl;
return View(); return View();
} }
[HttpPost] [HttpPost]
[AllowAnonymous] [AllowAnonymous]
[ValidateAntiForgeryToken] [ValidateAntiForgeryToken]
public async Task<IActionResult> Login(LoginViewModel model, string returnUrl = null) public async Task<IActionResult> Login(LoginViewModel model, string returnUrl = null)
{ {
ViewData["ReturnUrl"] = returnUrl; ViewData["ReturnUrl"] = returnUrl;
if(ModelState.IsValid) if (ModelState.IsValid)
{ {
// Require the user to have a confirmed email before they can log on. // Require the user to have a confirmed email before they can log on.
var user = await _userManager.FindByEmailAsync(model.Email); var user = await _userManager.FindByEmailAsync(model.Email);
if(user != null) if (user != null)
{ {
if(user.RequiresEmailConfirmation && !await _userManager.IsEmailConfirmedAsync(user)) if (user.RequiresEmailConfirmation && !await _userManager.IsEmailConfirmedAsync(user))
{ {
ModelState.AddModelError(string.Empty, ModelState.AddModelError(string.Empty,
"You must have a confirmed email to log in."); "You must have a confirmed email to log in.");
return View(model); return View(model);
} }
} }
// This doesn't count login failures towards account lockout // This doesn't count login failures towards account lockout
// To enable password failures to trigger account lockout, set lockoutOnFailure: true // To enable password failures to trigger account lockout, set lockoutOnFailure: true
var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, lockoutOnFailure: false); var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, lockoutOnFailure: false);
if(result.Succeeded) if (result.Succeeded)
{ {
_logger.LogInformation("User logged in."); _logger.LogInformation("User logged in.");
return RedirectToLocal(returnUrl); return RedirectToLocal(returnUrl);
} }
if(result.RequiresTwoFactor) if (result.RequiresTwoFactor)
{ {
return RedirectToAction(nameof(LoginWith2fa), new return RedirectToAction(nameof(LoginWith2fa), new
{ {
returnUrl, returnUrl,
model.RememberMe model.RememberMe
}); });
} }
if(result.IsLockedOut) if (result.IsLockedOut)
{ {
_logger.LogWarning("User account locked out."); _logger.LogWarning("User account locked out.");
return RedirectToAction(nameof(Lockout)); return RedirectToAction(nameof(Lockout));
} }
else else
{ {
ModelState.AddModelError(string.Empty, "Invalid login attempt."); ModelState.AddModelError(string.Empty, "Invalid login attempt.");
return View(model); return View(model);
} }
} }
// If we got this far, something failed, redisplay form // If we got this far, something failed, redisplay form
return View(model); return View(model);
} }
[HttpGet] [HttpGet]
[AllowAnonymous] [AllowAnonymous]
public async Task<IActionResult> LoginWith2fa(bool rememberMe, string returnUrl = null) public async Task<IActionResult> LoginWith2fa(bool rememberMe, string returnUrl = null)
{ {
// Ensure the user has gone through the username & password screen first // Ensure the user has gone through the username & password screen first
var user = await _signInManager.GetTwoFactorAuthenticationUserAsync(); var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
if(user == null) if (user == null)
{ {
throw new ApplicationException($"Unable to load two-factor authentication user."); throw new ApplicationException($"Unable to load two-factor authentication user.");
} }
var model = new LoginWith2faViewModel { RememberMe = rememberMe }; var model = new LoginWith2faViewModel { RememberMe = rememberMe };
ViewData["ReturnUrl"] = returnUrl; ViewData["ReturnUrl"] = returnUrl;
return View(model); return View(model);
} }
[HttpPost] [HttpPost]
[AllowAnonymous] [AllowAnonymous]
[ValidateAntiForgeryToken] [ValidateAntiForgeryToken]
public async Task<IActionResult> LoginWith2fa(LoginWith2faViewModel model, bool rememberMe, string returnUrl = null) public async Task<IActionResult> LoginWith2fa(LoginWith2faViewModel model, bool rememberMe, string returnUrl = null)
{ {
if(!ModelState.IsValid) if (!ModelState.IsValid)
{ {
return View(model); return View(model);
} }
var user = await _signInManager.GetTwoFactorAuthenticationUserAsync(); var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
if(user == null) if (user == null)
{ {
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
} }
var authenticatorCode = model.TwoFactorCode.Replace(" ", string.Empty).Replace("-", string.Empty); var authenticatorCode = model.TwoFactorCode.Replace(" ", string.Empty).Replace("-", string.Empty);
var result = await _signInManager.TwoFactorAuthenticatorSignInAsync(authenticatorCode, rememberMe, model.RememberMachine); var result = await _signInManager.TwoFactorAuthenticatorSignInAsync(authenticatorCode, rememberMe, model.RememberMachine);
if(result.Succeeded) if (result.Succeeded)
{ {
_logger.LogInformation("User with ID {UserId} logged in with 2fa.", user.Id); _logger.LogInformation("User with ID {UserId} logged in with 2fa.", user.Id);
return RedirectToLocal(returnUrl); return RedirectToLocal(returnUrl);
} }
else if(result.IsLockedOut) else if (result.IsLockedOut)
{ {
_logger.LogWarning("User with ID {UserId} account locked out.", user.Id); _logger.LogWarning("User with ID {UserId} account locked out.", user.Id);
return RedirectToAction(nameof(Lockout)); return RedirectToAction(nameof(Lockout));
} }
else else
{ {
_logger.LogWarning("Invalid authenticator code entered for user with ID {UserId}.", user.Id); _logger.LogWarning("Invalid authenticator code entered for user with ID {UserId}.", user.Id);
ModelState.AddModelError(string.Empty, "Invalid authenticator code."); ModelState.AddModelError(string.Empty, "Invalid authenticator code.");
return View(); return View();
} }
} }
[HttpGet] [HttpGet]
[AllowAnonymous] [AllowAnonymous]
public async Task<IActionResult> LoginWithRecoveryCode(string returnUrl = null) public async Task<IActionResult> LoginWithRecoveryCode(string returnUrl = null)
{ {
// Ensure the user has gone through the username & password screen first // Ensure the user has gone through the username & password screen first
var user = await _signInManager.GetTwoFactorAuthenticationUserAsync(); var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
if(user == null) if (user == null)
{ {
throw new ApplicationException($"Unable to load two-factor authentication user."); throw new ApplicationException($"Unable to load two-factor authentication user.");
} }
ViewData["ReturnUrl"] = returnUrl; ViewData["ReturnUrl"] = returnUrl;
return View(); return View();
} }
[HttpPost] [HttpPost]
[AllowAnonymous] [AllowAnonymous]
[ValidateAntiForgeryToken] [ValidateAntiForgeryToken]
public async Task<IActionResult> LoginWithRecoveryCode(LoginWithRecoveryCodeViewModel model, string returnUrl = null) public async Task<IActionResult> LoginWithRecoveryCode(LoginWithRecoveryCodeViewModel model, string returnUrl = null)
{ {
if(!ModelState.IsValid) if (!ModelState.IsValid)
{ {
return View(model); return View(model);
} }
var user = await _signInManager.GetTwoFactorAuthenticationUserAsync(); var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
if(user == null) if (user == null)
{ {
throw new ApplicationException($"Unable to load two-factor authentication user."); throw new ApplicationException($"Unable to load two-factor authentication user.");
} }
var recoveryCode = model.RecoveryCode.Replace(" ", string.Empty); var recoveryCode = model.RecoveryCode.Replace(" ", string.Empty);
var result = await _signInManager.TwoFactorRecoveryCodeSignInAsync(recoveryCode); var result = await _signInManager.TwoFactorRecoveryCodeSignInAsync(recoveryCode);
if(result.Succeeded) if (result.Succeeded)
{ {
_logger.LogInformation("User with ID {UserId} logged in with a recovery code.", user.Id); _logger.LogInformation("User with ID {UserId} logged in with a recovery code.", user.Id);
return RedirectToLocal(returnUrl); return RedirectToLocal(returnUrl);
} }
if(result.IsLockedOut) if (result.IsLockedOut)
{ {
_logger.LogWarning("User with ID {UserId} account locked out.", user.Id); _logger.LogWarning("User with ID {UserId} account locked out.", user.Id);
return RedirectToAction(nameof(Lockout)); return RedirectToAction(nameof(Lockout));
} }
else else
{ {
_logger.LogWarning("Invalid recovery code entered for user with ID {UserId}", user.Id); _logger.LogWarning("Invalid recovery code entered for user with ID {UserId}", user.Id);
ModelState.AddModelError(string.Empty, "Invalid recovery code entered."); ModelState.AddModelError(string.Empty, "Invalid recovery code entered.");
return View(); return View();
} }
} }
[HttpGet] [HttpGet]
[AllowAnonymous] [AllowAnonymous]
public IActionResult Lockout() public IActionResult Lockout()
{ {
return View(); return View();
} }
[HttpGet] [HttpGet]
[AllowAnonymous] [AllowAnonymous]
public IActionResult Register(string returnUrl = null) public IActionResult Register(string returnUrl = null)
{ {
ViewData["ReturnUrl"] = returnUrl; ViewData["ReturnUrl"] = returnUrl;
return View(); return View();
} }
[HttpPost] [HttpPost]
[AllowAnonymous] [AllowAnonymous]
[ValidateAntiForgeryToken] [ValidateAntiForgeryToken]
public async Task<IActionResult> Register(RegisterViewModel model, string returnUrl = null) public async Task<IActionResult> Register(RegisterViewModel model, string returnUrl = null)
{ {
ViewData["ReturnUrl"] = returnUrl; ViewData["ReturnUrl"] = returnUrl;
if(ModelState.IsValid) if (ModelState.IsValid)
{ {
var policies = await _SettingsRepository.GetSettingAsync<PoliciesSettings>() ?? new PoliciesSettings(); var policies = await _SettingsRepository.GetSettingAsync<PoliciesSettings>() ?? new PoliciesSettings();
var user = new ApplicationUser { UserName = model.Email, Email = model.Email, RequiresEmailConfirmation = policies.RequiresConfirmedEmail }; var user = new ApplicationUser { UserName = model.Email, Email = model.Email, RequiresEmailConfirmation = policies.RequiresConfirmedEmail };
var result = await _userManager.CreateAsync(user, model.Password); var result = await _userManager.CreateAsync(user, model.Password);
if(result.Succeeded) if (result.Succeeded)
{ {
_logger.LogInformation("User created a new account with password."); _logger.LogInformation("User created a new account with password.");
var admin = await _userManager.GetUsersInRoleAsync(Roles.ServerAdmin); var admin = await _userManager.GetUsersInRoleAsync(Roles.ServerAdmin);
if(admin.Count == 0) if (admin.Count == 0)
{ {
_logger.LogInformation("Admin created."); _logger.LogInformation("Admin created.");
await _RoleManager.CreateAsync(new IdentityRole(Roles.ServerAdmin)); await _RoleManager.CreateAsync(new IdentityRole(Roles.ServerAdmin));
await _userManager.AddToRoleAsync(user, Roles.ServerAdmin); await _userManager.AddToRoleAsync(user, Roles.ServerAdmin);
} }
var code = await _userManager.GenerateEmailConfirmationTokenAsync(user); var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
var callbackUrl = Url.EmailConfirmationLink(user.Id, code, Request.Scheme); var callbackUrl = Url.EmailConfirmationLink(user.Id, code, Request.Scheme);
RegisteredUserId = user.Id; RegisteredUserId = user.Id;
await _emailSender.SendEmailConfirmationAsync(model.Email, callbackUrl); await _emailSender.SendEmailConfirmationAsync(model.Email, callbackUrl);
_logger.LogInformation("User created a new account with password."); _logger.LogInformation("User created a new account with password.");
if(!policies.RequiresConfirmedEmail) if (!policies.RequiresConfirmedEmail)
{ {
await _signInManager.SignInAsync(user, isPersistent: false); await _signInManager.SignInAsync(user, isPersistent: false);
return RedirectToLocal(returnUrl); return RedirectToLocal(returnUrl);
} }
else else
{ {
TempData["StatusMessage"] = "Account created, please confirm your email"; TempData["StatusMessage"] = "Account created, please confirm your email";
return View(); return View();
} }
} }
AddErrors(result); AddErrors(result);
} }
// If we got this far, something failed, redisplay form // If we got this far, something failed, redisplay form
return View(model); return View(model);
} }
/// <summary> /// <summary>
/// Test property /// Test property
/// </summary> /// </summary>
public string RegisteredUserId public string RegisteredUserId
{ {
get; set; get; set;
} }
[HttpGet] [HttpGet]
public async Task<IActionResult> Logout() public async Task<IActionResult> Logout()
{ {
await _signInManager.SignOutAsync(); await _signInManager.SignOutAsync();
_logger.LogInformation("User logged out."); _logger.LogInformation("User logged out.");
return RedirectToAction(nameof(HomeController.Index), "Home"); return RedirectToAction(nameof(HomeController.Index), "Home");
} }
[HttpPost] [HttpPost]
[AllowAnonymous] [AllowAnonymous]
[ValidateAntiForgeryToken] [ValidateAntiForgeryToken]
public IActionResult ExternalLogin(string provider, string returnUrl = null) public IActionResult ExternalLogin(string provider, string returnUrl = null)
{ {
// Request a redirect to the external login provider. // Request a redirect to the external login provider.
var redirectUrl = Url.Action(nameof(ExternalLoginCallback), "Account", new var redirectUrl = Url.Action(nameof(ExternalLoginCallback), "Account", new
{ {
returnUrl returnUrl
}); });
var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl); var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl);
return Challenge(properties, provider); return Challenge(properties, provider);
} }
[HttpGet] [HttpGet]
[AllowAnonymous] [AllowAnonymous]
public async Task<IActionResult> ExternalLoginCallback(string returnUrl = null, string remoteError = null) public async Task<IActionResult> ExternalLoginCallback(string returnUrl = null, string remoteError = null)
{ {
if(remoteError != null) if (remoteError != null)
{ {
ErrorMessage = $"Error from external provider: {remoteError}"; ErrorMessage = $"Error from external provider: {remoteError}";
return RedirectToAction(nameof(Login)); return RedirectToAction(nameof(Login));
} }
var info = await _signInManager.GetExternalLoginInfoAsync(); var info = await _signInManager.GetExternalLoginInfoAsync();
if(info == null) if (info == null)
{ {
return RedirectToAction(nameof(Login)); return RedirectToAction(nameof(Login));
} }
// Sign in the user with this external login provider if the user already has a login. // Sign in the user with this external login provider if the user already has a login.
var result = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false, bypassTwoFactor: true); var result = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false, bypassTwoFactor: true);
if(result.Succeeded) if (result.Succeeded)
{ {
_logger.LogInformation("User logged in with {Name} provider.", info.LoginProvider); _logger.LogInformation("User logged in with {Name} provider.", info.LoginProvider);
return RedirectToLocal(returnUrl); return RedirectToLocal(returnUrl);
} }
if(result.IsLockedOut) if (result.IsLockedOut)
{ {
return RedirectToAction(nameof(Lockout)); return RedirectToAction(nameof(Lockout));
} }
else else
{ {
// If the user does not have an account, then ask the user to create an account. // If the user does not have an account, then ask the user to create an account.
ViewData["ReturnUrl"] = returnUrl; ViewData["ReturnUrl"] = returnUrl;
ViewData["LoginProvider"] = info.LoginProvider; ViewData["LoginProvider"] = info.LoginProvider;
var email = info.Principal.FindFirstValue(ClaimTypes.Email); var email = info.Principal.FindFirstValue(ClaimTypes.Email);
return View("ExternalLogin", new ExternalLoginViewModel { Email = email }); return View("ExternalLogin", new ExternalLoginViewModel { Email = email });
} }
} }
[HttpPost] [HttpPost]
[AllowAnonymous] [AllowAnonymous]
[ValidateAntiForgeryToken] [ValidateAntiForgeryToken]
public async Task<IActionResult> ExternalLoginConfirmation(ExternalLoginViewModel model, string returnUrl = null) public async Task<IActionResult> ExternalLoginConfirmation(ExternalLoginViewModel model, string returnUrl = null)
{ {
if(ModelState.IsValid) if (ModelState.IsValid)
{ {
// Get the information about the user from the external login provider // Get the information about the user from the external login provider
var info = await _signInManager.GetExternalLoginInfoAsync(); var info = await _signInManager.GetExternalLoginInfoAsync();
if(info == null) if (info == null)
{ {
throw new ApplicationException("Error loading external login information during confirmation."); throw new ApplicationException("Error loading external login information during confirmation.");
} }
var user = new ApplicationUser { UserName = model.Email, Email = model.Email }; var user = new ApplicationUser { UserName = model.Email, Email = model.Email };
var result = await _userManager.CreateAsync(user); var result = await _userManager.CreateAsync(user);
if(result.Succeeded) if (result.Succeeded)
{ {
result = await _userManager.AddLoginAsync(user, info); result = await _userManager.AddLoginAsync(user, info);
if(result.Succeeded) if (result.Succeeded)
{ {
await _signInManager.SignInAsync(user, isPersistent: false); await _signInManager.SignInAsync(user, isPersistent: false);
_logger.LogInformation("User created an account using {Name} provider.", info.LoginProvider); _logger.LogInformation("User created an account using {Name} provider.", info.LoginProvider);
return RedirectToLocal(returnUrl); return RedirectToLocal(returnUrl);
} }
} }
AddErrors(result); AddErrors(result);
} }
ViewData["ReturnUrl"] = returnUrl; ViewData["ReturnUrl"] = returnUrl;
return View(nameof(ExternalLogin), model); return View(nameof(ExternalLogin), model);
} }
[HttpGet] [HttpGet]
[AllowAnonymous] [AllowAnonymous]
public async Task<IActionResult> ConfirmEmail(string userId, string code) public async Task<IActionResult> ConfirmEmail(string userId, string code)
{ {
if(userId == null || code == null) if (userId == null || code == null)
{ {
return RedirectToAction(nameof(HomeController.Index), "Home"); return RedirectToAction(nameof(HomeController.Index), "Home");
} }
var user = await _userManager.FindByIdAsync(userId); var user = await _userManager.FindByIdAsync(userId);
if(user == null) if (user == null)
{ {
throw new ApplicationException($"Unable to load user with ID '{userId}'."); throw new ApplicationException($"Unable to load user with ID '{userId}'.");
} }
var result = await _userManager.ConfirmEmailAsync(user, code); var result = await _userManager.ConfirmEmailAsync(user, code);
return View(result.Succeeded ? "ConfirmEmail" : "Error"); return View(result.Succeeded ? "ConfirmEmail" : "Error");
} }
[HttpGet] [HttpGet]
[AllowAnonymous] [AllowAnonymous]
public IActionResult ForgotPassword() public IActionResult ForgotPassword()
{ {
return View(); return View();
} }
[HttpPost] [HttpPost]
[AllowAnonymous] [AllowAnonymous]
[ValidateAntiForgeryToken] [ValidateAntiForgeryToken]
public async Task<IActionResult> ForgotPassword(ForgotPasswordViewModel model) public async Task<IActionResult> ForgotPassword(ForgotPasswordViewModel model)
{ {
if(ModelState.IsValid) if (ModelState.IsValid)
{ {
var user = await _userManager.FindByEmailAsync(model.Email); var user = await _userManager.FindByEmailAsync(model.Email);
if(user == null || !(await _userManager.IsEmailConfirmedAsync(user))) if (user == null || !(await _userManager.IsEmailConfirmedAsync(user)))
{ {
// Don't reveal that the user does not exist or is not confirmed // Don't reveal that the user does not exist or is not confirmed
return RedirectToAction(nameof(ForgotPasswordConfirmation)); return RedirectToAction(nameof(ForgotPasswordConfirmation));
} }
// For more information on how to enable account confirmation and password reset please // For more information on how to enable account confirmation and password reset please
// visit https://go.microsoft.com/fwlink/?LinkID=532713 // visit https://go.microsoft.com/fwlink/?LinkID=532713
var code = await _userManager.GeneratePasswordResetTokenAsync(user); var code = await _userManager.GeneratePasswordResetTokenAsync(user);
var callbackUrl = Url.ResetPasswordCallbackLink(user.Id, code, Request.Scheme); var callbackUrl = Url.ResetPasswordCallbackLink(user.Id, code, Request.Scheme);
await _emailSender.SendEmailAsync(model.Email, "Reset Password", await _emailSender.SendEmailAsync(model.Email, "Reset Password",
$"Please reset your password by clicking here: <a href='{callbackUrl}'>link</a>"); $"Please reset your password by clicking here: <a href='{callbackUrl}'>link</a>");
return RedirectToAction(nameof(ForgotPasswordConfirmation)); return RedirectToAction(nameof(ForgotPasswordConfirmation));
} }
// If we got this far, something failed, redisplay form // If we got this far, something failed, redisplay form
return View(model); return View(model);
} }
[HttpGet] [HttpGet]
[AllowAnonymous] [AllowAnonymous]
public IActionResult ForgotPasswordConfirmation() public IActionResult ForgotPasswordConfirmation()
{ {
return View(); return View();
} }
[HttpGet] [HttpGet]
[AllowAnonymous] [AllowAnonymous]
public IActionResult ResetPassword(string code = null) public IActionResult ResetPassword(string code = null)
{ {
if(code == null) if (code == null)
{ {
throw new ApplicationException("A code must be supplied for password reset."); throw new ApplicationException("A code must be supplied for password reset.");
} }
var model = new ResetPasswordViewModel { Code = code }; var model = new ResetPasswordViewModel { Code = code };
return View(model); return View(model);
} }
[HttpPost] [HttpPost]
[AllowAnonymous] [AllowAnonymous]
[ValidateAntiForgeryToken] [ValidateAntiForgeryToken]
public async Task<IActionResult> ResetPassword(ResetPasswordViewModel model) public async Task<IActionResult> ResetPassword(ResetPasswordViewModel model)
{ {
if(!ModelState.IsValid) if (!ModelState.IsValid)
{ {
return View(model); return View(model);
} }
var user = await _userManager.FindByEmailAsync(model.Email); var user = await _userManager.FindByEmailAsync(model.Email);
if(user == null) if (user == null)
{ {
// Don't reveal that the user does not exist // Don't reveal that the user does not exist
return RedirectToAction(nameof(ResetPasswordConfirmation)); return RedirectToAction(nameof(ResetPasswordConfirmation));
} }
var result = await _userManager.ResetPasswordAsync(user, model.Code, model.Password); var result = await _userManager.ResetPasswordAsync(user, model.Code, model.Password);
if(result.Succeeded) if (result.Succeeded)
{ {
return RedirectToAction(nameof(ResetPasswordConfirmation)); return RedirectToAction(nameof(ResetPasswordConfirmation));
} }
AddErrors(result); AddErrors(result);
return View(); return View();
} }
[HttpGet] [HttpGet]
[AllowAnonymous] [AllowAnonymous]
public IActionResult ResetPasswordConfirmation() public IActionResult ResetPasswordConfirmation()
{ {
return View(); return View();
} }
[HttpGet] [HttpGet]
public IActionResult AccessDenied() public IActionResult AccessDenied()
{ {
return View(); return View();
} }
#region Helpers #region Helpers
private void AddErrors(IdentityResult result) private void AddErrors(IdentityResult result)
{ {
foreach(var error in result.Errors) foreach (var error in result.Errors)
{ {
ModelState.AddModelError(string.Empty, error.Description); ModelState.AddModelError(string.Empty, error.Description);
} }
} }
private IActionResult RedirectToLocal(string returnUrl) private IActionResult RedirectToLocal(string returnUrl)
{ {
if(Url.IsLocalUrl(returnUrl)) if (Url.IsLocalUrl(returnUrl))
{ {
return Redirect(returnUrl); return Redirect(returnUrl);
} }
else else
{ {
return RedirectToAction(nameof(HomeController.Index), "Home"); return RedirectToAction(nameof(HomeController.Index), "Home");
} }
} }
#endregion #endregion
} }
} }

View File

@@ -18,99 +18,99 @@ using System.Threading.Tasks;
namespace BTCPayServer.Controllers namespace BTCPayServer.Controllers
{ {
public class CallbackController : Controller public class CallbackController : Controller
{ {
public class CallbackSettings public class CallbackSettings
{ {
public string Token public string Token
{ {
get; set; get; set;
} }
} }
SettingsRepository _Settings; SettingsRepository _Settings;
Network _Network; Network _Network;
InvoiceWatcher _Watcher; InvoiceWatcher _Watcher;
ExplorerClient _Explorer; ExplorerClient _Explorer;
public CallbackController(SettingsRepository repo, public CallbackController(SettingsRepository repo,
ExplorerClient explorer, ExplorerClient explorer,
InvoiceWatcher watcher, InvoiceWatcher watcher,
Network network) Network network)
{ {
_Settings = repo; _Settings = repo;
_Network = network; _Network = network;
_Watcher = watcher; _Watcher = watcher;
_Explorer = explorer; _Explorer = explorer;
} }
[Route("callbacks/transactions")] [Route("callbacks/transactions")]
[HttpPost] [HttpPost]
public async Task NewTransaction(string token) public async Task NewTransaction(string token)
{ {
await AssertToken(token); await AssertToken(token);
Logs.PayServer.LogInformation("New transaction callback"); Logs.PayServer.LogInformation("New transaction callback");
//We don't want to register all the json converter at MVC level, so we parse here //We don't want to register all the json converter at MVC level, so we parse here
var serializer = new NBXplorer.Serializer(_Network); var serializer = new NBXplorer.Serializer(_Network);
var content = await new StreamReader(Request.Body, new UTF8Encoding(false), false, 1024, true).ReadToEndAsync(); var content = await new StreamReader(Request.Body, new UTF8Encoding(false), false, 1024, true).ReadToEndAsync();
var match = serializer.ToObject<TransactionMatch>(content); var match = serializer.ToObject<TransactionMatch>(content);
foreach(var output in match.Outputs) foreach (var output in match.Outputs)
{ {
await _Watcher.NotifyReceived(output.ScriptPubKey); await _Watcher.NotifyReceived(output.ScriptPubKey);
} }
} }
[Route("callbacks/blocks")] [Route("callbacks/blocks")]
[HttpPost] [HttpPost]
public async Task NewBlock(string token) public async Task NewBlock(string token)
{ {
await AssertToken(token); await AssertToken(token);
Logs.PayServer.LogInformation("New block callback"); Logs.PayServer.LogInformation("New block callback");
await _Watcher.NotifyBlock(); await _Watcher.NotifyBlock();
} }
private async Task AssertToken(string token) private async Task AssertToken(string token)
{ {
var callback = await _Settings.GetSettingAsync<CallbackSettings>(); var callback = await _Settings.GetSettingAsync<CallbackSettings>();
if(await GetToken() != token) if (await GetToken() != token)
throw new BTCPayServer.BitpayHttpException(400, "invalid-callback-token"); throw new BTCPayServer.BitpayHttpException(400, "invalid-callback-token");
} }
public async Task<Uri> GetCallbackUriAsync(HttpRequest request) public async Task<Uri> GetCallbackUriAsync(HttpRequest request)
{ {
string token = await GetToken(); string token = await GetToken();
return new Uri(request.GetAbsoluteRoot() + "/callbacks/transactions?token=" + token); return new Uri(request.GetAbsoluteRoot() + "/callbacks/transactions?token=" + token);
} }
public async Task RegisterCallbackUriAsync(DerivationStrategyBase derivationScheme, HttpRequest request) public async Task RegisterCallbackUriAsync(DerivationStrategyBase derivationScheme, HttpRequest request)
{ {
var uri = await GetCallbackUriAsync(request); var uri = await GetCallbackUriAsync(request);
await _Explorer.SubscribeToWalletAsync(uri, derivationScheme); await _Explorer.SubscribeToWalletAsync(uri, derivationScheme);
} }
private async Task<string> GetToken() private async Task<string> GetToken()
{ {
var callback = await _Settings.GetSettingAsync<CallbackSettings>(); var callback = await _Settings.GetSettingAsync<CallbackSettings>();
if(callback == null) if (callback == null)
{ {
callback = new CallbackSettings() { Token = Guid.NewGuid().ToString() }; callback = new CallbackSettings() { Token = Guid.NewGuid().ToString() };
await _Settings.UpdateSetting(callback); await _Settings.UpdateSetting(callback);
} }
var token = callback.Token; var token = callback.Token;
return token; return token;
} }
public async Task<Uri> GetCallbackBlockUriAsync(HttpRequest request) public async Task<Uri> GetCallbackBlockUriAsync(HttpRequest request)
{ {
string token = await GetToken(); string token = await GetToken();
return new Uri(request.GetAbsoluteRoot() + "/callbacks/blocks?token=" + token); return new Uri(request.GetAbsoluteRoot() + "/callbacks/blocks?token=" + token);
} }
public async Task<Uri> RegisterCallbackBlockUriAsync(HttpRequest request) public async Task<Uri> RegisterCallbackBlockUriAsync(HttpRequest request)
{ {
var uri = await GetCallbackBlockUriAsync(request); var uri = await GetCallbackBlockUriAsync(request);
await _Explorer.SubscribeToBlocksAsync(uri); await _Explorer.SubscribeToBlocksAsync(uri);
return uri; return uri;
} }
} }
} }

View File

@@ -16,123 +16,123 @@ using BTCPayServer.Services.Stores;
namespace BTCPayServer.Controllers namespace BTCPayServer.Controllers
{ {
[EnableCors("BitpayAPI")] [EnableCors("BitpayAPI")]
[BitpayAPIConstraint] [BitpayAPIConstraint]
public class InvoiceControllerAPI : Controller public class InvoiceControllerAPI : Controller
{ {
private InvoiceController _InvoiceController; private InvoiceController _InvoiceController;
private InvoiceRepository _InvoiceRepository; private InvoiceRepository _InvoiceRepository;
private TokenRepository _TokenRepository; private TokenRepository _TokenRepository;
private StoreRepository _StoreRepository; private StoreRepository _StoreRepository;
public InvoiceControllerAPI(InvoiceController invoiceController, public InvoiceControllerAPI(InvoiceController invoiceController,
InvoiceRepository invoceRepository, InvoiceRepository invoceRepository,
TokenRepository tokenRepository, TokenRepository tokenRepository,
StoreRepository storeRepository) StoreRepository storeRepository)
{ {
this._InvoiceController = invoiceController; this._InvoiceController = invoiceController;
this._InvoiceRepository = invoceRepository; this._InvoiceRepository = invoceRepository;
this._TokenRepository = tokenRepository; this._TokenRepository = tokenRepository;
this._StoreRepository = storeRepository; this._StoreRepository = storeRepository;
} }
[HttpPost] [HttpPost]
[Route("invoices")] [Route("invoices")]
[MediaTypeConstraint("application/json")] [MediaTypeConstraint("application/json")]
public async Task<DataWrapper<InvoiceResponse>> CreateInvoice([FromBody] Invoice invoice) public async Task<DataWrapper<InvoiceResponse>> CreateInvoice([FromBody] Invoice invoice)
{ {
var bitToken = await CheckTokenPermissionAsync(Facade.Merchant, invoice.Token); var bitToken = await CheckTokenPermissionAsync(Facade.Merchant, invoice.Token);
var store = await FindStore(bitToken); var store = await FindStore(bitToken);
return await _InvoiceController.CreateInvoiceCore(invoice, store, HttpContext.Request.GetAbsoluteRoot()); return await _InvoiceController.CreateInvoiceCore(invoice, store, HttpContext.Request.GetAbsoluteRoot());
} }
[HttpGet] [HttpGet]
[Route("invoices/{id}")] [Route("invoices/{id}")]
public async Task<DataWrapper<InvoiceResponse>> GetInvoice(string id, string token) public async Task<DataWrapper<InvoiceResponse>> GetInvoice(string id, string token)
{ {
var bitToken = await CheckTokenPermissionAsync(Facade.Merchant, token); var bitToken = await CheckTokenPermissionAsync(Facade.Merchant, token);
var store = await FindStore(bitToken); var store = await FindStore(bitToken);
var invoice = await _InvoiceRepository.GetInvoice(store.Id, id); var invoice = await _InvoiceRepository.GetInvoice(store.Id, id);
if(invoice == null) if (invoice == null)
throw new BitpayHttpException(404, "Object not found"); throw new BitpayHttpException(404, "Object not found");
var resp = invoice.EntityToDTO(); var resp = invoice.EntityToDTO();
return new DataWrapper<InvoiceResponse>(resp); return new DataWrapper<InvoiceResponse>(resp);
} }
[HttpGet] [HttpGet]
[Route("invoices")] [Route("invoices")]
public async Task<DataWrapper<InvoiceResponse[]>> GetInvoices( public async Task<DataWrapper<InvoiceResponse[]>> GetInvoices(
string token, string token,
DateTimeOffset? dateStart = null, DateTimeOffset? dateStart = null,
DateTimeOffset? dateEnd = null, DateTimeOffset? dateEnd = null,
string orderId = null, string orderId = null,
string itemCode = null, string itemCode = null,
string status = null, string status = null,
int? limit = null, int? limit = null,
int? offset = null) int? offset = null)
{ {
if(dateEnd != null) if (dateEnd != null)
dateEnd = dateEnd.Value + TimeSpan.FromDays(1); //Should include the end day dateEnd = dateEnd.Value + TimeSpan.FromDays(1); //Should include the end day
var bitToken = await CheckTokenPermissionAsync(Facade.Merchant, token); var bitToken = await CheckTokenPermissionAsync(Facade.Merchant, token);
var store = await FindStore(bitToken); var store = await FindStore(bitToken);
var query = new InvoiceQuery() var query = new InvoiceQuery()
{ {
Count = limit, Count = limit,
Skip = offset, Skip = offset,
EndDate = dateEnd, EndDate = dateEnd,
StartDate = dateStart, StartDate = dateStart,
OrderId = orderId, OrderId = orderId,
ItemCode = itemCode, ItemCode = itemCode,
Status = status, Status = status,
StoreId = store.Id StoreId = store.Id
}; };
var entities = (await _InvoiceRepository.GetInvoices(query)) var entities = (await _InvoiceRepository.GetInvoices(query))
.Select((o) => o.EntityToDTO()).ToArray(); .Select((o) => o.EntityToDTO()).ToArray();
return DataWrapper.Create(entities); return DataWrapper.Create(entities);
} }
private async Task<BitTokenEntity> CheckTokenPermissionAsync(Facade facade, string expectedToken) private async Task<BitTokenEntity> CheckTokenPermissionAsync(Facade facade, string expectedToken)
{ {
if(facade == null) if (facade == null)
throw new ArgumentNullException(nameof(facade)); throw new ArgumentNullException(nameof(facade));
var actualTokens = (await _TokenRepository.GetTokens(this.GetBitIdentity().SIN)).ToArray(); var actualTokens = (await _TokenRepository.GetTokens(this.GetBitIdentity().SIN)).ToArray();
actualTokens = actualTokens.SelectMany(t => GetCompatibleTokens(t)).ToArray(); actualTokens = actualTokens.SelectMany(t => GetCompatibleTokens(t)).ToArray();
var actualToken = actualTokens.FirstOrDefault(a => a.Value.Equals(expectedToken, StringComparison.Ordinal));
if(expectedToken == null || actualToken == null)
{
Logs.PayServer.LogDebug($"No token found for facade {facade} for SIN {this.GetBitIdentity().SIN}");
throw new BitpayHttpException(401, $"This endpoint does not support the `{actualTokens.Select(a => a.Facade).Concat(new[] { "user" }).FirstOrDefault()}` facade");
}
return actualToken;
}
private IEnumerable<BitTokenEntity> GetCompatibleTokens(BitTokenEntity token) var actualToken = actualTokens.FirstOrDefault(a => a.Value.Equals(expectedToken, StringComparison.Ordinal));
{ if (expectedToken == null || actualToken == null)
if(token.Facade == Facade.Merchant.ToString()) {
{ Logs.PayServer.LogDebug($"No token found for facade {facade} for SIN {this.GetBitIdentity().SIN}");
yield return token.Clone(Facade.User); throw new BitpayHttpException(401, $"This endpoint does not support the `{actualTokens.Select(a => a.Facade).Concat(new[] { "user" }).FirstOrDefault()}` facade");
yield return token.Clone(Facade.PointOfSale); }
} return actualToken;
if(token.Facade == Facade.PointOfSale.ToString()) }
{
yield return token.Clone(Facade.User);
}
yield return token;
}
private async Task<StoreData> FindStore(BitTokenEntity bitToken) private IEnumerable<BitTokenEntity> GetCompatibleTokens(BitTokenEntity token)
{ {
var store = await _StoreRepository.FindStore(bitToken.StoreId); if (token.Facade == Facade.Merchant.ToString())
if(store == null) {
throw new BitpayHttpException(401, "Unknown store"); yield return token.Clone(Facade.User);
return store; yield return token.Clone(Facade.PointOfSale);
} }
if (token.Facade == Facade.PointOfSale.ToString())
{
yield return token.Clone(Facade.User);
}
yield return token;
}
} private async Task<StoreData> FindStore(BitTokenEntity bitToken)
{
var store = await _StoreRepository.FindStore(bitToken.StoreId);
if (store == null)
throw new BitpayHttpException(401, "Unknown store");
return store;
}
}
} }

View File

@@ -14,59 +14,59 @@ namespace BTCPayServer.Controllers
{ {
public partial class InvoiceController public partial class InvoiceController
{ {
[HttpGet] [HttpGet]
[Route("i/{invoiceId}")] [Route("i/{invoiceId}")]
[AcceptMediaTypeConstraint("application/bitcoin-paymentrequest")] [AcceptMediaTypeConstraint("application/bitcoin-paymentrequest")]
public async Task<IActionResult> GetInvoiceRequest(string invoiceId) public async Task<IActionResult> GetInvoiceRequest(string invoiceId)
{ {
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId); var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
if(invoice == null || invoice.IsExpired()) if (invoice == null || invoice.IsExpired())
return NotFound(); return NotFound();
var dto = invoice.EntityToDTO(); var dto = invoice.EntityToDTO();
PaymentRequest request = new PaymentRequest PaymentRequest request = new PaymentRequest
{ {
DetailsVersion = 1 DetailsVersion = 1
}; };
request.Details.Expires = invoice.ExpirationTime; request.Details.Expires = invoice.ExpirationTime;
request.Details.Memo = invoice.ProductInformation.ItemDesc; request.Details.Memo = invoice.ProductInformation.ItemDesc;
request.Details.Network = _Network; request.Details.Network = _Network;
request.Details.Outputs.Add(new PaymentOutput() { Amount = dto.BTCDue, Script = BitcoinAddress.Create(dto.BitcoinAddress, _Network).ScriptPubKey }); request.Details.Outputs.Add(new PaymentOutput() { Amount = dto.BTCDue, Script = BitcoinAddress.Create(dto.BitcoinAddress, _Network).ScriptPubKey });
request.Details.MerchantData = Encoding.UTF8.GetBytes(invoice.Id); request.Details.MerchantData = Encoding.UTF8.GetBytes(invoice.Id);
request.Details.Time = DateTimeOffset.UtcNow; request.Details.Time = DateTimeOffset.UtcNow;
request.Details.PaymentUrl = new Uri(invoice.ServerUrl.WithTrailingSlash() + ($"i/{invoice.Id}"), UriKind.Absolute); request.Details.PaymentUrl = new Uri(invoice.ServerUrl.WithTrailingSlash() + ($"i/{invoice.Id}"), UriKind.Absolute);
var store = await _StoreRepository.FindStore(invoice.StoreId); var store = await _StoreRepository.FindStore(invoice.StoreId);
if(store == null) if (store == null)
throw new BitpayHttpException(401, "Unknown store"); throw new BitpayHttpException(401, "Unknown store");
if(store.StoreCertificate != null) if (store.StoreCertificate != null)
{ {
try try
{ {
request.Sign(store.StoreCertificate, PKIType.X509SHA256); request.Sign(store.StoreCertificate, PKIType.X509SHA256);
} }
catch(Exception ex) catch (Exception ex)
{ {
Logs.PayServer.LogWarning(ex, "Error while signing payment request"); Logs.PayServer.LogWarning(ex, "Error while signing payment request");
} }
} }
return new PaymentRequestActionResult(request); return new PaymentRequestActionResult(request);
} }
[HttpPost] [HttpPost]
[Route("i/{invoiceId}", Order = 99)] [Route("i/{invoiceId}", Order = 99)]
[MediaTypeConstraint("application/bitcoin-payment")] [MediaTypeConstraint("application/bitcoin-payment")]
public async Task<IActionResult> PostPayment(string invoiceId) public async Task<IActionResult> PostPayment(string invoiceId)
{ {
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId); var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
if(invoice == null || invoice.IsExpired()) if (invoice == null || invoice.IsExpired())
return NotFound(); return NotFound();
var payment = PaymentMessage.Load(Request.Body); var payment = PaymentMessage.Load(Request.Body);
var unused = _Wallet.BroadcastTransactionsAsync(payment.Transactions); var unused = _Wallet.BroadcastTransactionsAsync(payment.Transactions);
await _InvoiceRepository.AddRefundsAsync(invoiceId, payment.RefundTo.Select(p => new TxOut(p.Amount, p.Script)).ToArray()); await _InvoiceRepository.AddRefundsAsync(invoiceId, payment.RefundTo.Select(p => new TxOut(p.Amount, p.Script)).ToArray());
return new PaymentAckActionResult(payment.CreateACK(invoiceId + " is currently processing, thanks for your purchase...")); return new PaymentAckActionResult(payment.CreateACK(invoiceId + " is currently processing, thanks for your purchase..."));
} }
} }
} }

View File

@@ -18,299 +18,299 @@ using System.Threading.Tasks;
namespace BTCPayServer.Controllers namespace BTCPayServer.Controllers
{ {
public partial class InvoiceController public partial class InvoiceController
{ {
[HttpPost] [HttpPost]
[Route("invoices/{invoiceId}")] [Route("invoices/{invoiceId}")]
public async Task<IActionResult> Invoice(string invoiceId, string command) public async Task<IActionResult> Invoice(string invoiceId, string command)
{ {
if(command == "refresh") if (command == "refresh")
{ {
await _Watcher.WatchAsync(invoiceId, true); await _Watcher.WatchAsync(invoiceId, true);
} }
StatusMessage = "Invoice is state is being refreshed, please refresh the page soon..."; StatusMessage = "Invoice is state is being refreshed, please refresh the page soon...";
return RedirectToAction(nameof(Invoice), new return RedirectToAction(nameof(Invoice), new
{ {
invoiceId = invoiceId invoiceId = invoiceId
}); });
} }
[HttpGet] [HttpGet]
[Route("invoices/{invoiceId}")] [Route("invoices/{invoiceId}")]
public async Task<IActionResult> Invoice(string invoiceId) public async Task<IActionResult> Invoice(string invoiceId)
{ {
var invoice = (await _InvoiceRepository.GetInvoices(new InvoiceQuery() var invoice = (await _InvoiceRepository.GetInvoices(new InvoiceQuery()
{ {
UserId = GetUserId(), UserId = GetUserId(),
InvoiceId = invoiceId InvoiceId = invoiceId
})).FirstOrDefault(); })).FirstOrDefault();
if(invoice == null) if (invoice == null)
return NotFound(); return NotFound();
var dto = invoice.EntityToDTO(); var dto = invoice.EntityToDTO();
var store = await _StoreRepository.FindStore(invoice.StoreId); var store = await _StoreRepository.FindStore(invoice.StoreId);
InvoiceDetailsModel model = new InvoiceDetailsModel() InvoiceDetailsModel model = new InvoiceDetailsModel()
{ {
StoreName = store.StoreName, StoreName = store.StoreName,
StoreLink = Url.Action(nameof(StoresController.UpdateStore), "Stores", new { storeId = store.Id }), StoreLink = Url.Action(nameof(StoresController.UpdateStore), "Stores", new { storeId = store.Id }),
Id = invoice.Id, Id = invoice.Id,
Status = invoice.Status, Status = invoice.Status,
RefundEmail = invoice.RefundMail, RefundEmail = invoice.RefundMail,
CreatedDate = invoice.InvoiceTime, CreatedDate = invoice.InvoiceTime,
ExpirationDate = invoice.ExpirationTime, ExpirationDate = invoice.ExpirationTime,
OrderId = invoice.OrderId, OrderId = invoice.OrderId,
BuyerInformation = invoice.BuyerInformation, BuyerInformation = invoice.BuyerInformation,
Rate = invoice.Rate, Rate = invoice.Rate,
Fiat = dto.Price + " " + dto.Currency, Fiat = dto.Price + " " + dto.Currency,
BTC = invoice.GetTotalCryptoDue().ToString() + " BTC", BTC = invoice.GetTotalCryptoDue().ToString() + " BTC",
BTCDue = invoice.GetCryptoDue().ToString() + " BTC", BTCDue = invoice.GetCryptoDue().ToString() + " BTC",
BTCPaid = invoice.GetTotalPaid().ToString() + " BTC", BTCPaid = invoice.GetTotalPaid().ToString() + " BTC",
NetworkFee = invoice.GetNetworkFee().ToString() + " BTC", NetworkFee = invoice.GetNetworkFee().ToString() + " BTC",
NotificationUrl = invoice.NotificationURL, NotificationUrl = invoice.NotificationURL,
ProductInformation = invoice.ProductInformation, ProductInformation = invoice.ProductInformation,
BitcoinAddress = invoice.DepositAddress, BitcoinAddress = invoice.DepositAddress,
PaymentUrl = dto.PaymentUrls.BIP72 PaymentUrl = dto.PaymentUrls.BIP72
}; };
var payments = invoice var payments = invoice
.Payments .Payments
.Select(async payment => .Select(async payment =>
{ {
var m = new InvoiceDetailsModel.Payment(); var m = new InvoiceDetailsModel.Payment();
m.DepositAddress = payment.Output.ScriptPubKey.GetDestinationAddress(_Network); m.DepositAddress = payment.Output.ScriptPubKey.GetDestinationAddress(_Network);
m.Confirmations = (await _Explorer.GetTransactionAsync(payment.Outpoint.Hash))?.Confirmations ?? 0; m.Confirmations = (await _Explorer.GetTransactionAsync(payment.Outpoint.Hash))?.Confirmations ?? 0;
m.TransactionId = payment.Outpoint.Hash.ToString(); m.TransactionId = payment.Outpoint.Hash.ToString();
m.ReceivedTime = payment.ReceivedTime; m.ReceivedTime = payment.ReceivedTime;
m.TransactionLink = _Network == Network.Main ? $"https://www.smartbit.com.au/tx/{m.TransactionId}" : $"https://testnet.smartbit.com.au/tx/{m.TransactionId}"; m.TransactionLink = _Network == Network.Main ? $"https://www.smartbit.com.au/tx/{m.TransactionId}" : $"https://testnet.smartbit.com.au/tx/{m.TransactionId}";
return m; return m;
}) })
.ToArray(); .ToArray();
await Task.WhenAll(payments); await Task.WhenAll(payments);
model.Payments = payments.Select(p => p.GetAwaiter().GetResult()).ToList(); model.Payments = payments.Select(p => p.GetAwaiter().GetResult()).ToList();
model.StatusMessage = StatusMessage; model.StatusMessage = StatusMessage;
return View(model); return View(model);
} }
static Dictionary<string, CultureInfo> _CurrencyProviders = new Dictionary<string, CultureInfo>(); static Dictionary<string, CultureInfo> _CurrencyProviders = new Dictionary<string, CultureInfo>();
private IFormatProvider GetCurrencyProvider(string currency) private IFormatProvider GetCurrencyProvider(string currency)
{ {
lock(_CurrencyProviders) lock (_CurrencyProviders)
{ {
if(_CurrencyProviders.Count == 0) if (_CurrencyProviders.Count == 0)
{ {
foreach(var culture in CultureInfo.GetCultures(CultureTypes.AllCultures).Where(c => !c.IsNeutralCulture)) foreach (var culture in CultureInfo.GetCultures(CultureTypes.AllCultures).Where(c => !c.IsNeutralCulture))
{ {
try try
{ {
_CurrencyProviders.TryAdd(new RegionInfo(culture.LCID).ISOCurrencySymbol, culture); _CurrencyProviders.TryAdd(new RegionInfo(culture.LCID).ISOCurrencySymbol, culture);
} }
catch { } catch { }
} }
} }
return _CurrencyProviders.TryGet(currency); return _CurrencyProviders.TryGet(currency);
} }
} }
[HttpGet] [HttpGet]
[Route("i/{invoiceId}")] [Route("i/{invoiceId}")]
[Route("invoice")] [Route("invoice")]
[AcceptMediaTypeConstraint("application/bitcoin-paymentrequest", false)] [AcceptMediaTypeConstraint("application/bitcoin-paymentrequest", false)]
[XFrameOptionsAttribute(null)] [XFrameOptionsAttribute(null)]
public async Task<IActionResult> Checkout(string invoiceId, string id = null) public async Task<IActionResult> Checkout(string invoiceId, string id = null)
{ {
//Keep compatibility with Bitpay //Keep compatibility with Bitpay
invoiceId = invoiceId ?? id; invoiceId = invoiceId ?? id;
id = invoiceId; id = invoiceId;
//// ////
var model = await GetInvoiceModel(invoiceId); var model = await GetInvoiceModel(invoiceId);
if (model == null) if (model == null)
return NotFound(); return NotFound();
return View(nameof(Checkout), model); return View(nameof(Checkout), model);
} }
private async Task<PaymentModel> GetInvoiceModel(string invoiceId) private async Task<PaymentModel> GetInvoiceModel(string invoiceId)
{ {
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId); var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
if (invoice == null) if (invoice == null)
return null; return null;
var store = await _StoreRepository.FindStore(invoice.StoreId); var store = await _StoreRepository.FindStore(invoice.StoreId);
var dto = invoice.EntityToDTO(); var dto = invoice.EntityToDTO();
var model = new PaymentModel() var model = new PaymentModel()
{ {
ServerUrl = HttpContext.Request.GetAbsoluteRoot(), ServerUrl = HttpContext.Request.GetAbsoluteRoot(),
OrderId = invoice.OrderId, OrderId = invoice.OrderId,
InvoiceId = invoice.Id, InvoiceId = invoice.Id,
BtcAddress = invoice.DepositAddress.ToString(), BtcAddress = invoice.DepositAddress.ToString(),
BtcAmount = (invoice.GetTotalCryptoDue() - invoice.TxFee).ToString(), BtcAmount = (invoice.GetTotalCryptoDue() - invoice.TxFee).ToString(),
BtcTotalDue = invoice.GetTotalCryptoDue().ToString(), BtcTotalDue = invoice.GetTotalCryptoDue().ToString(),
BtcDue = invoice.GetCryptoDue().ToString(), BtcDue = invoice.GetCryptoDue().ToString(),
CustomerEmail = invoice.RefundMail, CustomerEmail = invoice.RefundMail,
ExpirationSeconds = Math.Max(0, (int)(invoice.ExpirationTime - DateTimeOffset.UtcNow).TotalSeconds), ExpirationSeconds = Math.Max(0, (int)(invoice.ExpirationTime - DateTimeOffset.UtcNow).TotalSeconds),
MaxTimeSeconds = (int)(invoice.ExpirationTime - invoice.InvoiceTime).TotalSeconds, MaxTimeSeconds = (int)(invoice.ExpirationTime - invoice.InvoiceTime).TotalSeconds,
ItemDesc = invoice.ProductInformation.ItemDesc, ItemDesc = invoice.ProductInformation.ItemDesc,
Rate = invoice.Rate.ToString("C", GetCurrencyProvider(invoice.ProductInformation.Currency)), Rate = invoice.Rate.ToString("C", GetCurrencyProvider(invoice.ProductInformation.Currency)),
MerchantRefLink = invoice.RedirectURL ?? "/", MerchantRefLink = invoice.RedirectURL ?? "/",
StoreName = store.StoreName, StoreName = store.StoreName,
TxFees = invoice.TxFee.ToString(), TxFees = invoice.TxFee.ToString(),
InvoiceBitcoinUrl = dto.PaymentUrls.BIP72, InvoiceBitcoinUrl = dto.PaymentUrls.BIP72,
TxCount = invoice.GetTxCount(), TxCount = invoice.GetTxCount(),
BtcPaid = invoice.GetTotalPaid().ToString(), BtcPaid = invoice.GetTotalPaid().ToString(),
Status = invoice.Status Status = invoice.Status
}; };
var expiration = TimeSpan.FromSeconds(model.ExpirationSeconds); var expiration = TimeSpan.FromSeconds(model.ExpirationSeconds);
model.TimeLeft = PrettyPrint(expiration); model.TimeLeft = PrettyPrint(expiration);
return model; return model;
} }
private string PrettyPrint(TimeSpan expiration) private string PrettyPrint(TimeSpan expiration)
{ {
StringBuilder builder = new StringBuilder(); StringBuilder builder = new StringBuilder();
if(expiration.Days >= 1) if (expiration.Days >= 1)
builder.Append(expiration.Days.ToString()); builder.Append(expiration.Days.ToString());
if(expiration.Hours >= 1) if (expiration.Hours >= 1)
builder.Append(expiration.Hours.ToString("00")); builder.Append(expiration.Hours.ToString("00"));
builder.Append($"{expiration.Minutes.ToString("00")}:{expiration.Seconds.ToString("00")}"); builder.Append($"{expiration.Minutes.ToString("00")}:{expiration.Seconds.ToString("00")}");
return builder.ToString(); return builder.ToString();
} }
[HttpGet] [HttpGet]
[Route("i/{invoiceId}/status")] [Route("i/{invoiceId}/status")]
public async Task<IActionResult> GetStatus(string invoiceId) public async Task<IActionResult> GetStatus(string invoiceId)
{ {
var model = await GetInvoiceModel(invoiceId); var model = await GetInvoiceModel(invoiceId);
if(model == null) if (model == null)
return NotFound(); return NotFound();
return Json(model); return Json(model);
} }
[HttpPost] [HttpPost]
[Route("i/{invoiceId}/UpdateCustomer")] [Route("i/{invoiceId}/UpdateCustomer")]
public async Task<IActionResult> UpdateCustomer(string invoiceId, [FromBody]UpdateCustomerModel data) public async Task<IActionResult> UpdateCustomer(string invoiceId, [FromBody]UpdateCustomerModel data)
{ {
if(!ModelState.IsValid) if (!ModelState.IsValid)
{ {
return BadRequest(ModelState); return BadRequest(ModelState);
} }
await _InvoiceRepository.UpdateInvoice(invoiceId, data).ConfigureAwait(false); await _InvoiceRepository.UpdateInvoice(invoiceId, data).ConfigureAwait(false);
return Ok(); return Ok();
} }
[HttpGet] [HttpGet]
[Route("invoices")] [Route("invoices")]
[Authorize(AuthenticationSchemes = "Identity.Application")] [Authorize(AuthenticationSchemes = "Identity.Application")]
[BitpayAPIConstraint(false)] [BitpayAPIConstraint(false)]
public async Task<IActionResult> ListInvoices(string searchTerm = null, int skip = 0, int count = 20) public async Task<IActionResult> ListInvoices(string searchTerm = null, int skip = 0, int count = 20)
{ {
var model = new InvoicesModel(); var model = new InvoicesModel();
foreach(var invoice in await _InvoiceRepository.GetInvoices(new InvoiceQuery() foreach (var invoice in await _InvoiceRepository.GetInvoices(new InvoiceQuery()
{ {
TextSearch = searchTerm, TextSearch = searchTerm,
Count = count, Count = count,
Skip = skip, Skip = skip,
UserId = GetUserId() UserId = GetUserId()
})) }))
{ {
model.SearchTerm = searchTerm; model.SearchTerm = searchTerm;
model.Invoices.Add(new InvoiceModel() model.Invoices.Add(new InvoiceModel()
{ {
Status = invoice.Status, Status = invoice.Status,
Date = invoice.InvoiceTime, Date = invoice.InvoiceTime,
InvoiceId = invoice.Id, InvoiceId = invoice.Id,
AmountCurrency = $"{invoice.ProductInformation.Price.ToString(CultureInfo.InvariantCulture)} {invoice.ProductInformation.Currency}" AmountCurrency = $"{invoice.ProductInformation.Price.ToString(CultureInfo.InvariantCulture)} {invoice.ProductInformation.Currency}"
}); });
} }
model.Skip = skip; model.Skip = skip;
model.Count = count; model.Count = count;
model.StatusMessage = StatusMessage; model.StatusMessage = StatusMessage;
return View(model); return View(model);
} }
[HttpGet] [HttpGet]
[Route("invoices/create")] [Route("invoices/create")]
[Authorize(AuthenticationSchemes = "Identity.Application")] [Authorize(AuthenticationSchemes = "Identity.Application")]
[BitpayAPIConstraint(false)] [BitpayAPIConstraint(false)]
public async Task<IActionResult> CreateInvoice() public async Task<IActionResult> CreateInvoice()
{ {
var stores = await GetStores(GetUserId()); var stores = await GetStores(GetUserId());
if(stores.Count() == 0) if (stores.Count() == 0)
{ {
StatusMessage = "Error: You need to create at least one store before creating a transaction"; StatusMessage = "Error: You need to create at least one store before creating a transaction";
return RedirectToAction(nameof(StoresController.ListStores), "Stores"); return RedirectToAction(nameof(StoresController.ListStores), "Stores");
} }
return View(new CreateInvoiceModel() { Stores = stores }); return View(new CreateInvoiceModel() { Stores = stores });
} }
[HttpPost] [HttpPost]
[Route("invoices/create")] [Route("invoices/create")]
[Authorize(AuthenticationSchemes = "Identity.Application")] [Authorize(AuthenticationSchemes = "Identity.Application")]
[BitpayAPIConstraint(false)] [BitpayAPIConstraint(false)]
public async Task<IActionResult> CreateInvoice(CreateInvoiceModel model) public async Task<IActionResult> CreateInvoice(CreateInvoiceModel model)
{ {
if(!ModelState.IsValid) if (!ModelState.IsValid)
{ {
model.Stores = await GetStores(GetUserId(), model.StoreId); model.Stores = await GetStores(GetUserId(), model.StoreId);
return View(model); return View(model);
} }
var store = await _StoreRepository.FindStore(model.StoreId, GetUserId()); var store = await _StoreRepository.FindStore(model.StoreId, GetUserId());
if(string.IsNullOrEmpty(store.DerivationStrategy)) if (string.IsNullOrEmpty(store.DerivationStrategy))
{ {
StatusMessage = "Error: You need to configure the derivation scheme in order to create an invoice"; StatusMessage = "Error: You need to configure the derivation scheme in order to create an invoice";
return RedirectToAction(nameof(StoresController.UpdateStore), "Stores", new return RedirectToAction(nameof(StoresController.UpdateStore), "Stores", new
{ {
storeId = store.Id storeId = store.Id
}); });
} }
var result = await CreateInvoiceCore(new Invoice() var result = await CreateInvoiceCore(new Invoice()
{ {
Price = model.Amount.Value, Price = model.Amount.Value,
Currency = "USD", Currency = "USD",
PosData = model.PosData, PosData = model.PosData,
OrderId = model.OrderId, OrderId = model.OrderId,
//RedirectURL = redirect + "redirect", //RedirectURL = redirect + "redirect",
NotificationURL = model.NotificationUrl, NotificationURL = model.NotificationUrl,
ItemDesc = model.ItemDesc, ItemDesc = model.ItemDesc,
FullNotifications = true, FullNotifications = true,
BuyerEmail = model.BuyerEmail, BuyerEmail = model.BuyerEmail,
}, store, HttpContext.Request.GetAbsoluteRoot()); }, store, HttpContext.Request.GetAbsoluteRoot());
StatusMessage = $"Invoice {result.Data.Id} just created!"; StatusMessage = $"Invoice {result.Data.Id} just created!";
return RedirectToAction(nameof(ListInvoices)); return RedirectToAction(nameof(ListInvoices));
} }
private async Task<SelectList> GetStores(string userId, string storeId = null) private async Task<SelectList> GetStores(string userId, string storeId = null)
{ {
return new SelectList(await _StoreRepository.GetStoresByUserId(userId), nameof(StoreData.Id), nameof(StoreData.StoreName), storeId); return new SelectList(await _StoreRepository.GetStoresByUserId(userId), nameof(StoreData.Id), nameof(StoreData.StoreName), storeId);
} }
[HttpPost] [HttpPost]
[Authorize(AuthenticationSchemes = "Identity.Application")] [Authorize(AuthenticationSchemes = "Identity.Application")]
[BitpayAPIConstraint(false)] [BitpayAPIConstraint(false)]
public IActionResult SearchInvoice(InvoicesModel invoices) public IActionResult SearchInvoice(InvoicesModel invoices)
{ {
return RedirectToAction(nameof(ListInvoices), new return RedirectToAction(nameof(ListInvoices), new
{ {
searchTerm = invoices.SearchTerm, searchTerm = invoices.SearchTerm,
skip = invoices.Skip, skip = invoices.Skip,
count = invoices.Count, count = invoices.Count,
}); });
} }
[TempData] [TempData]
public string StatusMessage public string StatusMessage
{ {
get; get;
set; set;
} }
private string GetUserId() private string GetUserId()
{ {
return _UserManager.GetUserId(User); return _UserManager.GetUserId(User);
} }
} }
} }

View File

@@ -41,119 +41,119 @@ using NBXplorer;
namespace BTCPayServer.Controllers namespace BTCPayServer.Controllers
{ {
public partial class InvoiceController : Controller public partial class InvoiceController : Controller
{ {
InvoiceRepository _InvoiceRepository; InvoiceRepository _InvoiceRepository;
BTCPayWallet _Wallet; BTCPayWallet _Wallet;
IRateProvider _RateProvider; IRateProvider _RateProvider;
private InvoiceWatcher _Watcher; private InvoiceWatcher _Watcher;
StoreRepository _StoreRepository; StoreRepository _StoreRepository;
Network _Network; Network _Network;
UserManager<ApplicationUser> _UserManager; UserManager<ApplicationUser> _UserManager;
IFeeProvider _FeeProvider; IFeeProvider _FeeProvider;
ExplorerClient _Explorer; ExplorerClient _Explorer;
public InvoiceController( public InvoiceController(
Network network, Network network,
InvoiceRepository invoiceRepository, InvoiceRepository invoiceRepository,
UserManager<ApplicationUser> userManager, UserManager<ApplicationUser> userManager,
BTCPayWallet wallet, BTCPayWallet wallet,
IRateProvider rateProvider, IRateProvider rateProvider,
StoreRepository storeRepository, StoreRepository storeRepository,
InvoiceWatcher watcher, InvoiceWatcher watcher,
ExplorerClient explorerClient, ExplorerClient explorerClient,
IFeeProvider feeProvider) IFeeProvider feeProvider)
{ {
_Explorer = explorerClient ?? throw new ArgumentNullException(nameof(explorerClient)); _Explorer = explorerClient ?? throw new ArgumentNullException(nameof(explorerClient));
_StoreRepository = storeRepository ?? throw new ArgumentNullException(nameof(storeRepository)); _StoreRepository = storeRepository ?? throw new ArgumentNullException(nameof(storeRepository));
_Network = network ?? throw new ArgumentNullException(nameof(network)); _Network = network ?? throw new ArgumentNullException(nameof(network));
_InvoiceRepository = invoiceRepository ?? throw new ArgumentNullException(nameof(invoiceRepository)); _InvoiceRepository = invoiceRepository ?? throw new ArgumentNullException(nameof(invoiceRepository));
_Wallet = wallet ?? throw new ArgumentNullException(nameof(wallet)); _Wallet = wallet ?? throw new ArgumentNullException(nameof(wallet));
_RateProvider = rateProvider ?? throw new ArgumentNullException(nameof(rateProvider)); _RateProvider = rateProvider ?? throw new ArgumentNullException(nameof(rateProvider));
_Watcher = watcher ?? throw new ArgumentNullException(nameof(watcher)); _Watcher = watcher ?? throw new ArgumentNullException(nameof(watcher));
_UserManager = userManager; _UserManager = userManager;
_FeeProvider = feeProvider ?? throw new ArgumentNullException(nameof(feeProvider)); _FeeProvider = feeProvider ?? throw new ArgumentNullException(nameof(feeProvider));
} }
internal async Task<DataWrapper<InvoiceResponse>> CreateInvoiceCore(Invoice invoice, StoreData store, string serverUrl, double expiryMinutes = 15, double monitoringMinutes = 60) internal async Task<DataWrapper<InvoiceResponse>> CreateInvoiceCore(Invoice invoice, StoreData store, string serverUrl, double expiryMinutes = 15, double monitoringMinutes = 60)
{ {
//TODO: expiryMinutes (time before a new invoice can become paid) and monitoringMinutes (time before a paid invoice becomes invalid) should be configurable at store level //TODO: expiryMinutes (time before a new invoice can become paid) and monitoringMinutes (time before a paid invoice becomes invalid) should be configurable at store level
var derivationStrategy = store.DerivationStrategy; var derivationStrategy = store.DerivationStrategy;
var entity = new InvoiceEntity var entity = new InvoiceEntity
{ {
InvoiceTime = DateTimeOffset.UtcNow, InvoiceTime = DateTimeOffset.UtcNow,
DerivationStrategy = derivationStrategy ?? throw new BitpayHttpException(400, "This store has not configured the derivation strategy") DerivationStrategy = derivationStrategy ?? throw new BitpayHttpException(400, "This store has not configured the derivation strategy")
}; };
Uri notificationUri = Uri.IsWellFormedUriString(invoice.NotificationURL, UriKind.Absolute) ? new Uri(invoice.NotificationURL, UriKind.Absolute) : null; Uri notificationUri = Uri.IsWellFormedUriString(invoice.NotificationURL, UriKind.Absolute) ? new Uri(invoice.NotificationURL, UriKind.Absolute) : null;
if(notificationUri == null || (notificationUri.Scheme != "http" && notificationUri.Scheme != "https")) //TODO: Filer non routable addresses ? if (notificationUri == null || (notificationUri.Scheme != "http" && notificationUri.Scheme != "https")) //TODO: Filer non routable addresses ?
notificationUri = null; notificationUri = null;
EmailAddressAttribute emailValidator = new EmailAddressAttribute(); EmailAddressAttribute emailValidator = new EmailAddressAttribute();
entity.ExpirationTime = entity.InvoiceTime.AddMinutes(expiryMinutes); entity.ExpirationTime = entity.InvoiceTime.AddMinutes(expiryMinutes);
entity.MonitoringExpiration = entity.InvoiceTime.AddMinutes(monitoringMinutes); entity.MonitoringExpiration = entity.InvoiceTime.AddMinutes(monitoringMinutes);
entity.OrderId = invoice.OrderId; entity.OrderId = invoice.OrderId;
entity.ServerUrl = serverUrl; entity.ServerUrl = serverUrl;
entity.FullNotifications = invoice.FullNotifications; entity.FullNotifications = invoice.FullNotifications;
entity.NotificationURL = notificationUri?.AbsoluteUri; entity.NotificationURL = notificationUri?.AbsoluteUri;
entity.BuyerInformation = Map<Invoice, BuyerInformation>(invoice); entity.BuyerInformation = Map<Invoice, BuyerInformation>(invoice);
//Another way of passing buyer info to support //Another way of passing buyer info to support
FillBuyerInfo(invoice.Buyer, entity.BuyerInformation); FillBuyerInfo(invoice.Buyer, entity.BuyerInformation);
if(entity?.BuyerInformation?.BuyerEmail != null) if (entity?.BuyerInformation?.BuyerEmail != null)
{ {
if(!EmailValidator.IsEmail(entity.BuyerInformation.BuyerEmail)) if (!EmailValidator.IsEmail(entity.BuyerInformation.BuyerEmail))
throw new BitpayHttpException(400, "Invalid email"); throw new BitpayHttpException(400, "Invalid email");
entity.RefundMail = entity.BuyerInformation.BuyerEmail; entity.RefundMail = entity.BuyerInformation.BuyerEmail;
} }
entity.ProductInformation = Map<Invoice, ProductInformation>(invoice); entity.ProductInformation = Map<Invoice, ProductInformation>(invoice);
entity.RedirectURL = invoice.RedirectURL ?? store.StoreWebsite; entity.RedirectURL = invoice.RedirectURL ?? store.StoreWebsite;
entity.Status = "new"; entity.Status = "new";
entity.SpeedPolicy = ParseSpeedPolicy(invoice.TransactionSpeed, store.SpeedPolicy); entity.SpeedPolicy = ParseSpeedPolicy(invoice.TransactionSpeed, store.SpeedPolicy);
entity.TxFee = store.GetStoreBlob(_Network).NetworkFeeDisabled ? Money.Zero : (await _FeeProvider.GetFeeRateAsync()).GetFee(100); // assume price for 100 bytes entity.TxFee = store.GetStoreBlob(_Network).NetworkFeeDisabled ? Money.Zero : (await _FeeProvider.GetFeeRateAsync()).GetFee(100); // assume price for 100 bytes
entity.Rate = (double)await _RateProvider.GetRateAsync(invoice.Currency); entity.Rate = (double)await _RateProvider.GetRateAsync(invoice.Currency);
entity.PosData = invoice.PosData; entity.PosData = invoice.PosData;
entity.DepositAddress = await _Wallet.ReserveAddressAsync(ParseDerivationStrategy(derivationStrategy)); entity.DepositAddress = await _Wallet.ReserveAddressAsync(ParseDerivationStrategy(derivationStrategy));
entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity); entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity);
await _Watcher.WatchAsync(entity.Id); await _Watcher.WatchAsync(entity.Id);
var resp = entity.EntityToDTO(); var resp = entity.EntityToDTO();
return new DataWrapper<InvoiceResponse>(resp) { Facade = "pos/invoice" }; return new DataWrapper<InvoiceResponse>(resp) { Facade = "pos/invoice" };
} }
private SpeedPolicy ParseSpeedPolicy(string transactionSpeed, SpeedPolicy defaultPolicy) private SpeedPolicy ParseSpeedPolicy(string transactionSpeed, SpeedPolicy defaultPolicy)
{ {
if(transactionSpeed == null) if (transactionSpeed == null)
return defaultPolicy; return defaultPolicy;
var mappings = new Dictionary<string, SpeedPolicy>(); var mappings = new Dictionary<string, SpeedPolicy>();
mappings.Add("low", SpeedPolicy.LowSpeed); mappings.Add("low", SpeedPolicy.LowSpeed);
mappings.Add("medium", SpeedPolicy.MediumSpeed); mappings.Add("medium", SpeedPolicy.MediumSpeed);
mappings.Add("high", SpeedPolicy.HighSpeed); mappings.Add("high", SpeedPolicy.HighSpeed);
if(!mappings.TryGetValue(transactionSpeed, out SpeedPolicy policy)) if (!mappings.TryGetValue(transactionSpeed, out SpeedPolicy policy))
policy = defaultPolicy; policy = defaultPolicy;
return policy; return policy;
} }
private void FillBuyerInfo(Buyer buyer, BuyerInformation buyerInformation) private void FillBuyerInfo(Buyer buyer, BuyerInformation buyerInformation)
{ {
if(buyer == null) if (buyer == null)
return; return;
buyerInformation.BuyerAddress1 = buyerInformation.BuyerAddress1 ?? buyer.Address1; buyerInformation.BuyerAddress1 = buyerInformation.BuyerAddress1 ?? buyer.Address1;
buyerInformation.BuyerAddress2 = buyerInformation.BuyerAddress2 ?? buyer.Address2; buyerInformation.BuyerAddress2 = buyerInformation.BuyerAddress2 ?? buyer.Address2;
buyerInformation.BuyerCity = buyerInformation.BuyerCity ?? buyer.City; buyerInformation.BuyerCity = buyerInformation.BuyerCity ?? buyer.City;
buyerInformation.BuyerCountry = buyerInformation.BuyerCountry ?? buyer.country; buyerInformation.BuyerCountry = buyerInformation.BuyerCountry ?? buyer.country;
buyerInformation.BuyerEmail = buyerInformation.BuyerEmail ?? buyer.email; buyerInformation.BuyerEmail = buyerInformation.BuyerEmail ?? buyer.email;
buyerInformation.BuyerName = buyerInformation.BuyerName ?? buyer.Name; buyerInformation.BuyerName = buyerInformation.BuyerName ?? buyer.Name;
buyerInformation.BuyerPhone = buyerInformation.BuyerPhone ?? buyer.phone; buyerInformation.BuyerPhone = buyerInformation.BuyerPhone ?? buyer.phone;
buyerInformation.BuyerState = buyerInformation.BuyerState ?? buyer.State; buyerInformation.BuyerState = buyerInformation.BuyerState ?? buyer.State;
buyerInformation.BuyerZip = buyerInformation.BuyerZip ?? buyer.zip; buyerInformation.BuyerZip = buyerInformation.BuyerZip ?? buyer.zip;
} }
private DerivationStrategyBase ParseDerivationStrategy(string derivationStrategy) private DerivationStrategyBase ParseDerivationStrategy(string derivationStrategy)
{ {
return new DerivationStrategyFactory(_Network).Parse(derivationStrategy); return new DerivationStrategyFactory(_Network).Parse(derivationStrategy);
} }
private TDest Map<TFrom, TDest>(TFrom data) private TDest Map<TFrom, TDest>(TFrom data)
{ {
return JsonConvert.DeserializeObject<TDest>(JsonConvert.SerializeObject(data)); return JsonConvert.DeserializeObject<TDest>(JsonConvert.SerializeObject(data));
} }
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -7,34 +7,34 @@ using System.Threading.Tasks;
namespace BTCPayServer.Controllers namespace BTCPayServer.Controllers
{ {
public class PaymentRequestActionResult : IActionResult public class PaymentRequestActionResult : IActionResult
{ {
PaymentRequest req; PaymentRequest req;
public PaymentRequestActionResult(PaymentRequest req) public PaymentRequestActionResult(PaymentRequest req)
{ {
this.req = req; this.req = req;
} }
public Task ExecuteResultAsync(ActionContext context) public Task ExecuteResultAsync(ActionContext context)
{ {
context.HttpContext.Response.Headers["Content-Transfer-Encoding"] = "binary"; context.HttpContext.Response.Headers["Content-Transfer-Encoding"] = "binary";
context.HttpContext.Response.ContentType = "application/bitcoin-paymentrequest"; context.HttpContext.Response.ContentType = "application/bitcoin-paymentrequest";
req.WriteTo(context.HttpContext.Response.Body); req.WriteTo(context.HttpContext.Response.Body);
return Task.CompletedTask; return Task.CompletedTask;
} }
} }
public class PaymentAckActionResult : IActionResult public class PaymentAckActionResult : IActionResult
{ {
PaymentACK req; PaymentACK req;
public PaymentAckActionResult(PaymentACK req) public PaymentAckActionResult(PaymentACK req)
{ {
this.req = req; this.req = req;
} }
public Task ExecuteResultAsync(ActionContext context) public Task ExecuteResultAsync(ActionContext context)
{ {
context.HttpContext.Response.Headers["Content-Transfer-Encoding"] = "binary"; context.HttpContext.Response.Headers["Content-Transfer-Encoding"] = "binary";
context.HttpContext.Response.ContentType = "application/bitcoin-paymentack"; context.HttpContext.Response.ContentType = "application/bitcoin-paymentack";
req.WriteTo(context.HttpContext.Response.Body); req.WriteTo(context.HttpContext.Response.Body);
return Task.CompletedTask; return Task.CompletedTask;
} }
} }
} }

View File

@@ -10,31 +10,31 @@ using BTCPayServer.Services.Rates;
namespace BTCPayServer.Controllers namespace BTCPayServer.Controllers
{ {
public class RateController : Controller public class RateController : Controller
{ {
IRateProvider _RateProvider; IRateProvider _RateProvider;
CurrencyNameTable _CurrencyNameTable; CurrencyNameTable _CurrencyNameTable;
public RateController(IRateProvider rateProvider, CurrencyNameTable currencyNameTable) public RateController(IRateProvider rateProvider, CurrencyNameTable currencyNameTable)
{ {
_RateProvider = rateProvider ?? throw new ArgumentNullException(nameof(rateProvider)); _RateProvider = rateProvider ?? throw new ArgumentNullException(nameof(rateProvider));
_CurrencyNameTable = currencyNameTable ?? throw new ArgumentNullException(nameof(currencyNameTable)); _CurrencyNameTable = currencyNameTable ?? throw new ArgumentNullException(nameof(currencyNameTable));
} }
[Route("rates")] [Route("rates")]
[HttpGet] [HttpGet]
[BitpayAPIConstraint] [BitpayAPIConstraint]
public async Task<DataWrapper<NBitpayClient.Rate[]>> GetRates() public async Task<DataWrapper<NBitpayClient.Rate[]>> GetRates()
{ {
var allRates = (await _RateProvider.GetRatesAsync()); var allRates = (await _RateProvider.GetRatesAsync());
return new DataWrapper<NBitpayClient.Rate[]> return new DataWrapper<NBitpayClient.Rate[]>
(allRates.Select(r => (allRates.Select(r =>
new NBitpayClient.Rate() new NBitpayClient.Rate()
{ {
Code = r.Currency, Code = r.Currency,
Name = _CurrencyNameTable.GetCurrencyData(r.Currency)?.Name, Name = _CurrencyNameTable.GetCurrencyData(r.Currency)?.Name,
Value = r.Value Value = r.Value
}).Where(n => n.Name != null).ToArray()); }).Where(n => n.Name != null).ToArray());
} }
} }
} }

View File

@@ -16,82 +16,82 @@ using System.Threading.Tasks;
namespace BTCPayServer.Controllers namespace BTCPayServer.Controllers
{ {
[Authorize(Roles = Roles.ServerAdmin)] [Authorize(Roles = Roles.ServerAdmin)]
public class ServerController : Controller public class ServerController : Controller
{ {
private UserManager<ApplicationUser> _UserManager; private UserManager<ApplicationUser> _UserManager;
SettingsRepository _SettingsRepository; SettingsRepository _SettingsRepository;
public ServerController(UserManager<ApplicationUser> userManager, SettingsRepository settingsRepository) public ServerController(UserManager<ApplicationUser> userManager, SettingsRepository settingsRepository)
{ {
_UserManager = userManager; _UserManager = userManager;
_SettingsRepository = settingsRepository; _SettingsRepository = settingsRepository;
} }
[Route("server/users")] [Route("server/users")]
public IActionResult ListUsers() public IActionResult ListUsers()
{ {
var users = new UsersViewModel(); var users = new UsersViewModel();
users.Users users.Users
= _UserManager.Users.Select(u => new UsersViewModel.UserViewModel() = _UserManager.Users.Select(u => new UsersViewModel.UserViewModel()
{ {
Name = u.UserName, Name = u.UserName,
Email = u.Email Email = u.Email
}).ToList(); }).ToList();
return View(users); return View(users);
} }
[Route("server/emails")] [Route("server/emails")]
public async Task<IActionResult> Emails() public async Task<IActionResult> Emails()
{ {
var data = (await _SettingsRepository.GetSettingAsync<EmailSettings>()) ?? new EmailSettings(); var data = (await _SettingsRepository.GetSettingAsync<EmailSettings>()) ?? new EmailSettings();
return View(new EmailsViewModel() { Settings = data }); return View(new EmailsViewModel() { Settings = data });
} }
[Route("server/policies")] [Route("server/policies")]
public async Task<IActionResult> Policies() public async Task<IActionResult> Policies()
{ {
var data = (await _SettingsRepository.GetSettingAsync<PoliciesSettings>()) ?? new PoliciesSettings(); var data = (await _SettingsRepository.GetSettingAsync<PoliciesSettings>()) ?? new PoliciesSettings();
return View(data); return View(data);
} }
[Route("server/policies")] [Route("server/policies")]
[HttpPost] [HttpPost]
public async Task<IActionResult> Policies(PoliciesSettings settings) public async Task<IActionResult> Policies(PoliciesSettings settings)
{ {
await _SettingsRepository.UpdateSetting(settings); await _SettingsRepository.UpdateSetting(settings);
TempData["StatusMessage"] = "Policies upadated successfully"; TempData["StatusMessage"] = "Policies upadated successfully";
return View(settings); return View(settings);
} }
[Route("server/emails")] [Route("server/emails")]
[HttpPost] [HttpPost]
public async Task<IActionResult> Emails(EmailsViewModel model, string command) public async Task<IActionResult> Emails(EmailsViewModel model, string command)
{ {
if(command == "Test") if (command == "Test")
{ {
if(!ModelState.IsValid) if (!ModelState.IsValid)
return View(model); return View(model);
try try
{ {
var client = model.Settings.CreateSmtpClient(); var client = model.Settings.CreateSmtpClient();
await client.SendMailAsync(model.Settings.From, model.TestEmail, "BTCPay test", "BTCPay test"); await client.SendMailAsync(model.Settings.From, model.TestEmail, "BTCPay test", "BTCPay test");
model.StatusMessage = "Email sent to " + model.TestEmail + ", please, verify you received it"; model.StatusMessage = "Email sent to " + model.TestEmail + ", please, verify you received it";
} }
catch(Exception ex) catch (Exception ex)
{ {
model.StatusMessage = "Error: " + ex.Message; model.StatusMessage = "Error: " + ex.Message;
} }
return View(model); return View(model);
} }
else else
{ {
ModelState.Remove(nameof(model.TestEmail)); ModelState.Remove(nameof(model.TestEmail));
if(!ModelState.IsValid) if (!ModelState.IsValid)
return View(model); return View(model);
await _SettingsRepository.UpdateSetting(model.Settings); await _SettingsRepository.UpdateSetting(model.Settings);
model.StatusMessage = "Email settings saved"; model.StatusMessage = "Email settings saved";
return View(model); return View(model);
} }
} }
} }
} }

View File

@@ -20,399 +20,399 @@ using System.Threading.Tasks;
namespace BTCPayServer.Controllers namespace BTCPayServer.Controllers
{ {
[Route("stores")] [Route("stores")]
[Authorize(AuthenticationSchemes = "Identity.Application")] [Authorize(AuthenticationSchemes = "Identity.Application")]
[Authorize(Policy = "CanAccessStore")] [Authorize(Policy = "CanAccessStore")]
[AutoValidateAntiforgeryToken] [AutoValidateAntiforgeryToken]
public class StoresController : Controller public class StoresController : Controller
{ {
public StoresController( public StoresController(
StoreRepository repo, StoreRepository repo,
TokenRepository tokenRepo, TokenRepository tokenRepo,
CallbackController callbackController, CallbackController callbackController,
UserManager<ApplicationUser> userManager, UserManager<ApplicationUser> userManager,
AccessTokenController tokenController, AccessTokenController tokenController,
BTCPayWallet wallet, BTCPayWallet wallet,
Network network, Network network,
IHostingEnvironment env) IHostingEnvironment env)
{ {
_Repo = repo; _Repo = repo;
_TokenRepository = tokenRepo; _TokenRepository = tokenRepo;
_UserManager = userManager; _UserManager = userManager;
_TokenController = tokenController; _TokenController = tokenController;
_Wallet = wallet; _Wallet = wallet;
_Env = env; _Env = env;
_Network = network; _Network = network;
_CallbackController = callbackController; _CallbackController = callbackController;
} }
Network _Network; Network _Network;
CallbackController _CallbackController; CallbackController _CallbackController;
BTCPayWallet _Wallet; BTCPayWallet _Wallet;
AccessTokenController _TokenController; AccessTokenController _TokenController;
StoreRepository _Repo; StoreRepository _Repo;
TokenRepository _TokenRepository; TokenRepository _TokenRepository;
UserManager<ApplicationUser> _UserManager; UserManager<ApplicationUser> _UserManager;
IHostingEnvironment _Env; IHostingEnvironment _Env;
[TempData] [TempData]
public string StatusMessage public string StatusMessage
{ {
get; set; get; set;
} }
[HttpGet] [HttpGet]
[Route("create")] [Route("create")]
public IActionResult CreateStore() public IActionResult CreateStore()
{ {
return View(); return View();
} }
[HttpPost] [HttpPost]
[Route("create")] [Route("create")]
public async Task<IActionResult> CreateStore(CreateStoreViewModel vm) public async Task<IActionResult> CreateStore(CreateStoreViewModel vm)
{ {
if(!ModelState.IsValid) if (!ModelState.IsValid)
{ {
return View(vm); return View(vm);
} }
var store = await _Repo.CreateStore(GetUserId(), vm.Name); var store = await _Repo.CreateStore(GetUserId(), vm.Name);
CreatedStoreId = store.Id; CreatedStoreId = store.Id;
StatusMessage = "Store successfully created"; StatusMessage = "Store successfully created";
return RedirectToAction(nameof(ListStores)); return RedirectToAction(nameof(ListStores));
} }
public string CreatedStoreId public string CreatedStoreId
{ {
get; set; get; set;
} }
[HttpGet] [HttpGet]
public async Task<IActionResult> ListStores() public async Task<IActionResult> ListStores()
{ {
StoresViewModel result = new StoresViewModel(); StoresViewModel result = new StoresViewModel();
result.StatusMessage = StatusMessage; result.StatusMessage = StatusMessage;
var stores = await _Repo.GetStoresByUserId(GetUserId()); var stores = await _Repo.GetStoresByUserId(GetUserId());
var balances = stores.Select(async s => string.IsNullOrEmpty(s.DerivationStrategy) ? Money.Zero : await _Wallet.GetBalance(ParseDerivationStrategy(s.DerivationStrategy))).ToArray(); var balances = stores.Select(async s => string.IsNullOrEmpty(s.DerivationStrategy) ? Money.Zero : await _Wallet.GetBalance(ParseDerivationStrategy(s.DerivationStrategy))).ToArray();
for(int i = 0; i < stores.Length; i++) for (int i = 0; i < stores.Length; i++)
{ {
var store = stores[i]; var store = stores[i];
result.Stores.Add(new StoresViewModel.StoreViewModel() result.Stores.Add(new StoresViewModel.StoreViewModel()
{ {
Id = store.Id, Id = store.Id,
Name = store.StoreName, Name = store.StoreName,
WebSite = store.StoreWebsite, WebSite = store.StoreWebsite,
Balance = await balances[i] Balance = await balances[i]
}); });
} }
return View(result); return View(result);
} }
[HttpGet] [HttpGet]
[Route("{storeId}/delete")] [Route("{storeId}/delete")]
public async Task<IActionResult> DeleteStore(string storeId) public async Task<IActionResult> DeleteStore(string storeId)
{ {
var store = await _Repo.FindStore(storeId, GetUserId()); var store = await _Repo.FindStore(storeId, GetUserId());
if(store == null) if (store == null)
return NotFound(); return NotFound();
return View("Confirm", new ConfirmModel() return View("Confirm", new ConfirmModel()
{ {
Title = "Delete store " + store.StoreName, Title = "Delete store " + store.StoreName,
Description = "This store will still be accessible to users sharing it", Description = "This store will still be accessible to users sharing it",
Action = "Delete" Action = "Delete"
}); });
} }
[HttpPost] [HttpPost]
[Route("{storeId}/delete")] [Route("{storeId}/delete")]
public async Task<IActionResult> DeleteStorePost(string storeId) public async Task<IActionResult> DeleteStorePost(string storeId)
{ {
var userId = GetUserId(); var userId = GetUserId();
var store = await _Repo.FindStore(storeId, GetUserId()); var store = await _Repo.FindStore(storeId, GetUserId());
if(store == null) if (store == null)
return NotFound(); return NotFound();
await _Repo.RemoveStore(storeId, userId); await _Repo.RemoveStore(storeId, userId);
StatusMessage = "Store removed successfully"; StatusMessage = "Store removed successfully";
return RedirectToAction(nameof(ListStores)); return RedirectToAction(nameof(ListStores));
} }
[HttpGet] [HttpGet]
[Route("{storeId}")] [Route("{storeId}")]
public async Task<IActionResult> UpdateStore(string storeId) public async Task<IActionResult> UpdateStore(string storeId)
{ {
var store = await _Repo.FindStore(storeId, GetUserId()); var store = await _Repo.FindStore(storeId, GetUserId());
if(store == null) if (store == null)
return NotFound(); return NotFound();
var vm = new StoreViewModel(); var vm = new StoreViewModel();
vm.StoreName = store.StoreName; vm.StoreName = store.StoreName;
vm.StoreWebsite = store.StoreWebsite; vm.StoreWebsite = store.StoreWebsite;
vm.NetworkFee = !store.GetStoreBlob(_Network).NetworkFeeDisabled; vm.NetworkFee = !store.GetStoreBlob(_Network).NetworkFeeDisabled;
vm.SpeedPolicy = store.SpeedPolicy; vm.SpeedPolicy = store.SpeedPolicy;
vm.DerivationScheme = store.DerivationStrategy; vm.DerivationScheme = store.DerivationStrategy;
vm.StatusMessage = StatusMessage; vm.StatusMessage = StatusMessage;
return View(vm); return View(vm);
} }
[HttpPost] [HttpPost]
[Route("{storeId}")] [Route("{storeId}")]
public async Task<IActionResult> UpdateStore(string storeId, StoreViewModel model, string command) public async Task<IActionResult> UpdateStore(string storeId, StoreViewModel model, string command)
{ {
if(!ModelState.IsValid) if (!ModelState.IsValid)
{ {
return View(model); return View(model);
} }
var store = await _Repo.FindStore(storeId, GetUserId()); var store = await _Repo.FindStore(storeId, GetUserId());
if(store == null) if (store == null)
return NotFound(); return NotFound();
if(command == "Save") if (command == "Save")
{ {
bool needUpdate = false; bool needUpdate = false;
if(store.SpeedPolicy != model.SpeedPolicy) if (store.SpeedPolicy != model.SpeedPolicy)
{ {
needUpdate = true; needUpdate = true;
store.SpeedPolicy = model.SpeedPolicy; store.SpeedPolicy = model.SpeedPolicy;
} }
if(store.StoreName != model.StoreName) if (store.StoreName != model.StoreName)
{ {
needUpdate = true; needUpdate = true;
store.StoreName = model.StoreName; store.StoreName = model.StoreName;
} }
if(store.StoreWebsite != model.StoreWebsite) if (store.StoreWebsite != model.StoreWebsite)
{ {
needUpdate = true; needUpdate = true;
store.StoreWebsite = model.StoreWebsite; store.StoreWebsite = model.StoreWebsite;
} }
if(store.DerivationStrategy != model.DerivationScheme) if (store.DerivationStrategy != model.DerivationScheme)
{ {
needUpdate = true; needUpdate = true;
try try
{ {
var strategy = ParseDerivationStrategy(model.DerivationScheme); var strategy = ParseDerivationStrategy(model.DerivationScheme);
await _Wallet.TrackAsync(strategy); await _Wallet.TrackAsync(strategy);
await _CallbackController.RegisterCallbackUriAsync(strategy, Request); await _CallbackController.RegisterCallbackUriAsync(strategy, Request);
store.DerivationStrategy = model.DerivationScheme; store.DerivationStrategy = model.DerivationScheme;
} }
catch catch
{ {
ModelState.AddModelError(nameof(model.DerivationScheme), "Invalid Derivation Scheme"); ModelState.AddModelError(nameof(model.DerivationScheme), "Invalid Derivation Scheme");
return View(model); return View(model);
} }
} }
if(store.GetStoreBlob(_Network).NetworkFeeDisabled != !model.NetworkFee) if (store.GetStoreBlob(_Network).NetworkFeeDisabled != !model.NetworkFee)
{ {
var blob = store.GetStoreBlob(_Network); var blob = store.GetStoreBlob(_Network);
blob.NetworkFeeDisabled = !model.NetworkFee; blob.NetworkFeeDisabled = !model.NetworkFee;
store.SetStoreBlob(blob, _Network); store.SetStoreBlob(blob, _Network);
needUpdate = true; needUpdate = true;
} }
if(needUpdate) if (needUpdate)
{ {
await _Repo.UpdateStore(store); await _Repo.UpdateStore(store);
StatusMessage = "Store successfully updated"; StatusMessage = "Store successfully updated";
} }
return RedirectToAction(nameof(UpdateStore), new return RedirectToAction(nameof(UpdateStore), new
{ {
storeId = storeId storeId = storeId
}); });
} }
else else
{ {
var facto = new DerivationStrategyFactory(_Network); var facto = new DerivationStrategyFactory(_Network);
var scheme = facto.Parse(model.DerivationScheme); var scheme = facto.Parse(model.DerivationScheme);
var line = scheme.GetLineFor(DerivationFeature.Deposit); var line = scheme.GetLineFor(DerivationFeature.Deposit);
for(int i = 0; i < 10; i++) for (int i = 0; i < 10; i++)
{ {
var address = line.Derive((uint)i); var address = line.Derive((uint)i);
model.AddressSamples.Add((line.Path.Derive((uint)i).ToString(), address.ScriptPubKey.GetDestinationAddress(_Network).ToString())); model.AddressSamples.Add((line.Path.Derive((uint)i).ToString(), address.ScriptPubKey.GetDestinationAddress(_Network).ToString()));
} }
return View(model); return View(model);
} }
} }
private DerivationStrategyBase ParseDerivationStrategy(string derivationScheme) private DerivationStrategyBase ParseDerivationStrategy(string derivationScheme)
{ {
return new DerivationStrategyFactory(_Network).Parse(derivationScheme); return new DerivationStrategyFactory(_Network).Parse(derivationScheme);
} }
[HttpGet] [HttpGet]
[Route("{storeId}/Tokens")] [Route("{storeId}/Tokens")]
public async Task<IActionResult> ListTokens(string storeId) public async Task<IActionResult> ListTokens(string storeId)
{ {
var model = new TokensViewModel(); var model = new TokensViewModel();
var tokens = await _TokenRepository.GetTokensByStoreIdAsync(storeId); var tokens = await _TokenRepository.GetTokensByStoreIdAsync(storeId);
model.StatusMessage = StatusMessage; model.StatusMessage = StatusMessage;
model.Tokens = tokens.Select(t => new TokenViewModel() model.Tokens = tokens.Select(t => new TokenViewModel()
{ {
Facade = t.Facade, Facade = t.Facade,
Label = t.Label, Label = t.Label,
SIN = t.SIN, SIN = t.SIN,
Id = t.Value Id = t.Value
}).ToArray(); }).ToArray();
return View(model); return View(model);
} }
[HttpPost] [HttpPost]
[Route("/api-tokens")] [Route("/api-tokens")]
[Route("{storeId}/Tokens/Create")] [Route("{storeId}/Tokens/Create")]
public async Task<IActionResult> CreateToken(string storeId, CreateTokenViewModel model) public async Task<IActionResult> CreateToken(string storeId, CreateTokenViewModel model)
{ {
if(!ModelState.IsValid) if (!ModelState.IsValid)
{ {
return View(model); return View(model);
} }
model.Label = model.Label ?? String.Empty; model.Label = model.Label ?? String.Empty;
if(storeId == null) // Permissions are not checked by Policy if the storeId is not passed by url if (storeId == null) // Permissions are not checked by Policy if the storeId is not passed by url
{ {
storeId = model.StoreId; storeId = model.StoreId;
var userId = GetUserId(); var userId = GetUserId();
if(userId == null) if (userId == null)
return Unauthorized(); return Unauthorized();
var store = await _Repo.FindStore(storeId, userId); var store = await _Repo.FindStore(storeId, userId);
if(store == null) if (store == null)
return Unauthorized(); return Unauthorized();
} }
var tokenRequest = new TokenRequest() var tokenRequest = new TokenRequest()
{ {
Facade = model.Facade, Facade = model.Facade,
Label = model.Label, Label = model.Label,
Id = model.PublicKey == null ? null : NBitpayClient.Extensions.BitIdExtensions.GetBitIDSIN(new PubKey(model.PublicKey)) Id = model.PublicKey == null ? null : NBitpayClient.Extensions.BitIdExtensions.GetBitIDSIN(new PubKey(model.PublicKey))
}; };
string pairingCode = null; string pairingCode = null;
if(model.PublicKey == null) if (model.PublicKey == null)
{ {
tokenRequest.PairingCode = await _TokenRepository.CreatePairingCodeAsync(); tokenRequest.PairingCode = await _TokenRepository.CreatePairingCodeAsync();
await _TokenRepository.UpdatePairingCode(new PairingCodeEntity() await _TokenRepository.UpdatePairingCode(new PairingCodeEntity()
{ {
Id = tokenRequest.PairingCode, Id = tokenRequest.PairingCode,
Facade = model.Facade, Facade = model.Facade,
Label = model.Label, Label = model.Label,
}); });
await _TokenRepository.PairWithStoreAsync(tokenRequest.PairingCode, storeId); await _TokenRepository.PairWithStoreAsync(tokenRequest.PairingCode, storeId);
pairingCode = tokenRequest.PairingCode; pairingCode = tokenRequest.PairingCode;
} }
else else
{ {
pairingCode = ((DataWrapper<List<PairingCodeResponse>>)await _TokenController.Tokens(tokenRequest)).Data[0].PairingCode; pairingCode = ((DataWrapper<List<PairingCodeResponse>>)await _TokenController.Tokens(tokenRequest)).Data[0].PairingCode;
} }
return RedirectToAction(nameof(RequestPairing), new return RedirectToAction(nameof(RequestPairing), new
{ {
pairingCode = pairingCode, pairingCode = pairingCode,
selectedStore = storeId selectedStore = storeId
}); });
} }
[HttpGet] [HttpGet]
[Route("/api-tokens")] [Route("/api-tokens")]
[Route("{storeId}/Tokens/Create")] [Route("{storeId}/Tokens/Create")]
public async Task<IActionResult> CreateToken(string storeId) public async Task<IActionResult> CreateToken(string storeId)
{ {
var userId = GetUserId(); var userId = GetUserId();
if(string.IsNullOrWhiteSpace(userId)) if (string.IsNullOrWhiteSpace(userId))
return Unauthorized(); return Unauthorized();
var model = new CreateTokenViewModel(); var model = new CreateTokenViewModel();
model.Facade = "merchant"; model.Facade = "merchant";
ViewBag.HidePublicKey = storeId == null; ViewBag.HidePublicKey = storeId == null;
ViewBag.ShowStores = storeId == null; ViewBag.ShowStores = storeId == null;
ViewBag.ShowMenu = storeId != null; ViewBag.ShowMenu = storeId != null;
model.StoreId = storeId; model.StoreId = storeId;
if(storeId == null) if (storeId == null)
{ {
model.Stores = new SelectList(await _Repo.GetStoresByUserId(userId), nameof(StoreData.Id), nameof(StoreData.StoreName), storeId); model.Stores = new SelectList(await _Repo.GetStoresByUserId(userId), nameof(StoreData.Id), nameof(StoreData.StoreName), storeId);
} }
return View(model); return View(model);
} }
[HttpPost] [HttpPost]
[Route("{storeId}/Tokens/Delete")] [Route("{storeId}/Tokens/Delete")]
public async Task<IActionResult> DeleteToken(string storeId, string tokenId) public async Task<IActionResult> DeleteToken(string storeId, string tokenId)
{ {
var token = await _TokenRepository.GetToken(tokenId); var token = await _TokenRepository.GetToken(tokenId);
if(token == null || if (token == null ||
token.StoreId != storeId || token.StoreId != storeId ||
!await _TokenRepository.DeleteToken(tokenId)) !await _TokenRepository.DeleteToken(tokenId))
StatusMessage = "Failure to revoke this token"; StatusMessage = "Failure to revoke this token";
else else
StatusMessage = "Token revoked"; StatusMessage = "Token revoked";
return RedirectToAction(nameof(ListTokens)); return RedirectToAction(nameof(ListTokens));
} }
[HttpGet] [HttpGet]
[Route("/api-access-request")] [Route("/api-access-request")]
public async Task<IActionResult> RequestPairing(string pairingCode, string selectedStore = null) public async Task<IActionResult> RequestPairing(string pairingCode, string selectedStore = null)
{ {
var pairing = await _TokenRepository.GetPairingAsync(pairingCode); var pairing = await _TokenRepository.GetPairingAsync(pairingCode);
if(pairing == null) if (pairing == null)
{ {
StatusMessage = "Unknown pairing code"; StatusMessage = "Unknown pairing code";
return RedirectToAction(nameof(ListStores)); return RedirectToAction(nameof(ListStores));
} }
else else
{ {
var stores = await _Repo.GetStoresByUserId(GetUserId()); var stores = await _Repo.GetStoresByUserId(GetUserId());
return View(new PairingModel() return View(new PairingModel()
{ {
Id = pairing.Id, Id = pairing.Id,
Facade = pairing.Facade, Facade = pairing.Facade,
Label = pairing.Label, Label = pairing.Label,
SIN = pairing.SIN ?? "Server-Initiated Pairing", SIN = pairing.SIN ?? "Server-Initiated Pairing",
SelectedStore = selectedStore ?? stores.FirstOrDefault()?.Id, SelectedStore = selectedStore ?? stores.FirstOrDefault()?.Id,
Stores = stores.Select(s => new PairingModel.StoreViewModel() Stores = stores.Select(s => new PairingModel.StoreViewModel()
{ {
Id = s.Id, Id = s.Id,
Name = string.IsNullOrEmpty(s.StoreName) ? s.Id : s.StoreName Name = string.IsNullOrEmpty(s.StoreName) ? s.Id : s.StoreName
}).ToArray() }).ToArray()
}); });
} }
} }
[HttpPost] [HttpPost]
[Route("api-access-request")] [Route("api-access-request")]
public async Task<IActionResult> Pair(string pairingCode, string selectedStore) public async Task<IActionResult> Pair(string pairingCode, string selectedStore)
{ {
if(pairingCode == null) if (pairingCode == null)
return NotFound(); return NotFound();
var store = await _Repo.FindStore(selectedStore, GetUserId()); var store = await _Repo.FindStore(selectedStore, GetUserId());
var pairing = await _TokenRepository.GetPairingAsync(pairingCode); var pairing = await _TokenRepository.GetPairingAsync(pairingCode);
if(store == null || pairing == null) if (store == null || pairing == null)
return NotFound(); return NotFound();
var pairingResult = await _TokenRepository.PairWithStoreAsync(pairingCode, store.Id); var pairingResult = await _TokenRepository.PairWithStoreAsync(pairingCode, store.Id);
if(pairingResult == PairingResult.Complete || pairingResult == PairingResult.Partial) if (pairingResult == PairingResult.Complete || pairingResult == PairingResult.Partial)
{ {
StatusMessage = "Pairing is successfull"; StatusMessage = "Pairing is successfull";
if(pairingResult == PairingResult.Partial) if (pairingResult == PairingResult.Partial)
StatusMessage = "Server initiated pairing code: " + pairingCode; StatusMessage = "Server initiated pairing code: " + pairingCode;
return RedirectToAction(nameof(ListTokens), new return RedirectToAction(nameof(ListTokens), new
{ {
storeId = store.Id storeId = store.Id
}); });
} }
else else
{ {
StatusMessage = $"Pairing failed ({pairingResult})"; StatusMessage = $"Pairing failed ({pairingResult})";
return RedirectToAction(nameof(ListTokens), new return RedirectToAction(nameof(ListTokens), new
{ {
storeId = store.Id storeId = store.Id
}); });
} }
} }
private string GetUserId() private string GetUserId()
{ {
return _UserManager.GetUserId(User); return _UserManager.GetUserId(User);
} }
} }
} }

View File

@@ -5,26 +5,26 @@ using System.Threading.Tasks;
namespace BTCPayServer.Data namespace BTCPayServer.Data
{ {
public class AddressInvoiceData public class AddressInvoiceData
{ {
public string Address public string Address
{ {
get; set; get; set;
} }
public InvoiceData InvoiceData public InvoiceData InvoiceData
{ {
get; set; get; set;
} }
public string InvoiceDataId public string InvoiceDataId
{ {
get; set; get; set;
} }
public DateTimeOffset? CreatedTime public DateTimeOffset? CreatedTime
{ {
get; set; get; set;
} }
} }
} }

View File

@@ -10,126 +10,126 @@ using Microsoft.EntityFrameworkCore.Infrastructure;
namespace BTCPayServer.Data namespace BTCPayServer.Data
{ {
public class ApplicationDbContext : IdentityDbContext<ApplicationUser> public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{ {
public ApplicationDbContext() public ApplicationDbContext()
{ {
} }
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options) : base(options)
{ {
} }
public DbSet<InvoiceData> Invoices public DbSet<InvoiceData> Invoices
{ {
get; set; get; set;
} }
public DbSet<HistoricalAddressInvoiceData> HistoricalAddressInvoices public DbSet<HistoricalAddressInvoiceData> HistoricalAddressInvoices
{ {
get; set; get; set;
} }
public DbSet<PendingInvoiceData> PendingInvoices public DbSet<PendingInvoiceData> PendingInvoices
{ {
get; set; get; set;
} }
public DbSet<RefundAddressesData> RefundAddresses public DbSet<RefundAddressesData> RefundAddresses
{ {
get; set; get; set;
} }
public DbSet<PaymentData> Payments public DbSet<PaymentData> Payments
{ {
get; set; get; set;
} }
public DbSet<StoreData> Stores public DbSet<StoreData> Stores
{ {
get; set; get; set;
} }
public DbSet<UserStore> UserStore public DbSet<UserStore> UserStore
{ {
get; set; get; set;
} }
public DbSet<AddressInvoiceData> AddressInvoices public DbSet<AddressInvoiceData> AddressInvoices
{ {
get; set; get; set;
} }
public DbSet<SettingData> Settings public DbSet<SettingData> Settings
{ {
get; set; get; set;
} }
public DbSet<PairingCodeData> PairingCodes public DbSet<PairingCodeData> PairingCodes
{ {
get; set; get; set;
} }
public DbSet<PairedSINData> PairedSINData public DbSet<PairedSINData> PairedSINData
{ {
get; set; get; set;
} }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{ {
var isConfigured = optionsBuilder.Options.Extensions.OfType<RelationalOptionsExtension>().Any(); var isConfigured = optionsBuilder.Options.Extensions.OfType<RelationalOptionsExtension>().Any();
if(!isConfigured) if (!isConfigured)
optionsBuilder.UseSqlite("Data Source=temp.db"); optionsBuilder.UseSqlite("Data Source=temp.db");
} }
protected override void OnModelCreating(ModelBuilder builder) protected override void OnModelCreating(ModelBuilder builder)
{ {
base.OnModelCreating(builder); base.OnModelCreating(builder);
builder.Entity<InvoiceData>() builder.Entity<InvoiceData>()
.HasIndex(o => o.StoreDataId); .HasIndex(o => o.StoreDataId);
builder.Entity<PaymentData>() builder.Entity<PaymentData>()
.HasIndex(o => o.InvoiceDataId); .HasIndex(o => o.InvoiceDataId);
builder.Entity<RefundAddressesData>() builder.Entity<RefundAddressesData>()
.HasIndex(o => o.InvoiceDataId); .HasIndex(o => o.InvoiceDataId);
builder.Entity<UserStore>() builder.Entity<UserStore>()
.HasKey(t => new .HasKey(t => new
{ {
t.ApplicationUserId, t.ApplicationUserId,
t.StoreDataId t.StoreDataId
}); });
builder.Entity<UserStore>() builder.Entity<UserStore>()
.HasOne(pt => pt.ApplicationUser) .HasOne(pt => pt.ApplicationUser)
.WithMany(p => p.UserStores) .WithMany(p => p.UserStores)
.HasForeignKey(pt => pt.ApplicationUserId); .HasForeignKey(pt => pt.ApplicationUserId);
builder.Entity<UserStore>() builder.Entity<UserStore>()
.HasOne(pt => pt.StoreData) .HasOne(pt => pt.StoreData)
.WithMany(t => t.UserStores) .WithMany(t => t.UserStores)
.HasForeignKey(pt => pt.StoreDataId); .HasForeignKey(pt => pt.StoreDataId);
builder.Entity<AddressInvoiceData>() builder.Entity<AddressInvoiceData>()
.HasKey(o => o.Address); .HasKey(o => o.Address);
builder.Entity<PairingCodeData>() builder.Entity<PairingCodeData>()
.HasKey(o => o.Id); .HasKey(o => o.Id);
builder.Entity<PairedSINData>(b => builder.Entity<PairedSINData>(b =>
{ {
b.HasIndex(o => o.SIN); b.HasIndex(o => o.SIN);
b.HasIndex(o => o.StoreDataId); b.HasIndex(o => o.StoreDataId);
}); });
builder.Entity<HistoricalAddressInvoiceData>() builder.Entity<HistoricalAddressInvoiceData>()
.HasKey(o => new .HasKey(o => new
{ {
o.InvoiceDataId, o.InvoiceDataId,
o.Address o.Address
}); });
} }
} }
} }

View File

@@ -9,42 +9,42 @@ using Hangfire.PostgreSql;
namespace BTCPayServer.Data namespace BTCPayServer.Data
{ {
public enum DatabaseType public enum DatabaseType
{ {
Sqlite, Sqlite,
Postgres Postgres
} }
public class ApplicationDbContextFactory public class ApplicationDbContextFactory
{ {
string _ConnectionString; string _ConnectionString;
DatabaseType _Type; DatabaseType _Type;
public ApplicationDbContextFactory(DatabaseType type, string connectionString) public ApplicationDbContextFactory(DatabaseType type, string connectionString)
{ {
_ConnectionString = connectionString ?? throw new ArgumentNullException(nameof(connectionString)); _ConnectionString = connectionString ?? throw new ArgumentNullException(nameof(connectionString));
_Type = type; _Type = type;
} }
public ApplicationDbContext CreateContext() public ApplicationDbContext CreateContext()
{ {
var builder = new DbContextOptionsBuilder<ApplicationDbContext>(); var builder = new DbContextOptionsBuilder<ApplicationDbContext>();
ConfigureBuilder(builder); ConfigureBuilder(builder);
return new ApplicationDbContext(builder.Options); return new ApplicationDbContext(builder.Options);
} }
public void ConfigureBuilder(DbContextOptionsBuilder builder) public void ConfigureBuilder(DbContextOptionsBuilder builder)
{ {
if(_Type == DatabaseType.Sqlite) if (_Type == DatabaseType.Sqlite)
builder.UseSqlite(_ConnectionString); builder.UseSqlite(_ConnectionString);
else if(_Type == DatabaseType.Postgres) else if (_Type == DatabaseType.Postgres)
builder.UseNpgsql(_ConnectionString); builder.UseNpgsql(_ConnectionString);
} }
public void ConfigureHangfireBuilder(IGlobalConfiguration builder) public void ConfigureHangfireBuilder(IGlobalConfiguration builder)
{ {
if(_Type == DatabaseType.Sqlite) if (_Type == DatabaseType.Sqlite)
builder.UseMemoryStorage(); //Sql provider does not support multiple workers builder.UseMemoryStorage(); //Sql provider does not support multiple workers
else if(_Type == DatabaseType.Postgres) else if (_Type == DatabaseType.Postgres)
builder.UsePostgreSqlStorage(_ConnectionString); builder.UsePostgreSqlStorage(_ConnectionString);
} }
} }
} }

View File

@@ -7,24 +7,24 @@ namespace BTCPayServer.Data
{ {
public class HistoricalAddressInvoiceData public class HistoricalAddressInvoiceData
{ {
public string InvoiceDataId public string InvoiceDataId
{ {
get; set; get; set;
} }
public string Address public string Address
{ {
get; set; get; set;
} }
public DateTimeOffset Assigned public DateTimeOffset Assigned
{ {
get; set; get; set;
} }
public DateTimeOffset? UnAssigned public DateTimeOffset? UnAssigned
{ {
get; set; get; set;
} }
} }
} }

View File

@@ -6,70 +6,70 @@ using System.Threading.Tasks;
namespace BTCPayServer.Data namespace BTCPayServer.Data
{ {
public class InvoiceData public class InvoiceData
{ {
public string StoreDataId public string StoreDataId
{ {
get; set; get; set;
} }
public StoreData StoreData public StoreData StoreData
{ {
get; set; get; set;
} }
public string Id public string Id
{ {
get; set; get; set;
} }
public DateTimeOffset Created public DateTimeOffset Created
{ {
get; set; get; set;
} }
public List<PaymentData> Payments public List<PaymentData> Payments
{ {
get; set; get; set;
} }
public List<RefundAddressesData> RefundAddresses public List<RefundAddressesData> RefundAddresses
{ {
get; set; get; set;
} }
public List<HistoricalAddressInvoiceData> HistoricalAddressInvoices public List<HistoricalAddressInvoiceData> HistoricalAddressInvoices
{ {
get; set; get; set;
} }
public byte[] Blob public byte[] Blob
{ {
get; set; get; set;
} }
public string ItemCode public string ItemCode
{ {
get; get;
set; set;
} }
public string OrderId public string OrderId
{ {
get; get;
set; set;
} }
public string Status public string Status
{ {
get; get;
set; set;
} }
public string ExceptionStatus public string ExceptionStatus
{ {
get; get;
set; set;
} }
public string CustomerEmail public string CustomerEmail
{ {
get; get;
set; set;
} }
} }
} }

View File

@@ -7,33 +7,33 @@ namespace BTCPayServer.Data
{ {
public class PairedSINData public class PairedSINData
{ {
public string Id public string Id
{ {
get; set; get; set;
} }
public string Facade public string Facade
{ {
get; set; get; set;
} }
public string StoreDataId public string StoreDataId
{ {
get; set; get; set;
} }
public string Label public string Label
{ {
get; get;
set; set;
} }
public DateTimeOffset PairingTime public DateTimeOffset PairingTime
{ {
get; get;
set; set;
} }
public string SIN public string SIN
{ {
get; set; get; set;
} }
} }
} }

View File

@@ -7,44 +7,44 @@ namespace BTCPayServer.Data
{ {
public class PairingCodeData public class PairingCodeData
{ {
public string Id public string Id
{ {
get; set; get; set;
} }
public string Facade
{
get; set;
}
public string StoreDataId
{
get; set;
}
public DateTimeOffset Expiration
{
get;
set;
}
public string Label public string Facade
{ {
get; get; set;
set; }
} public string StoreDataId
public string SIN {
{ get; set;
get; }
set; public DateTimeOffset Expiration
} {
public DateTime DateCreated get;
{ set;
get; }
set;
} public string Label
public string TokenValue {
{ get;
get; set;
set; }
} public string SIN
} {
get;
set;
}
public DateTime DateCreated
{
get;
set;
}
public string TokenValue
{
get;
set;
}
}
} }

View File

@@ -5,25 +5,25 @@ using System.Threading.Tasks;
namespace BTCPayServer.Data namespace BTCPayServer.Data
{ {
public class PaymentData public class PaymentData
{ {
public string Id public string Id
{ {
get; set; get; set;
} }
public string InvoiceDataId public string InvoiceDataId
{ {
get; set; get; set;
} }
public InvoiceData InvoiceData public InvoiceData InvoiceData
{ {
get; set; get; set;
} }
public byte[] Blob public byte[] Blob
{ {
get; set; get; set;
} }
} }
} }

View File

@@ -7,9 +7,9 @@ namespace BTCPayServer.Data
{ {
public class PendingInvoiceData public class PendingInvoiceData
{ {
public string Id public string Id
{ {
get; set; get; set;
} }
} }
} }

View File

@@ -7,21 +7,21 @@ namespace BTCPayServer.Data
{ {
public class RefundAddressesData public class RefundAddressesData
{ {
public string Id public string Id
{ {
get; set; get; set;
} }
public string InvoiceDataId public string InvoiceDataId
{ {
get; set; get; set;
} }
public InvoiceData InvoiceData public InvoiceData InvoiceData
{ {
get; set; get; set;
} }
public byte[] Blob public byte[] Blob
{ {
get; set; get; set;
} }
} }
} }

View File

@@ -7,14 +7,14 @@ namespace BTCPayServer.Data
{ {
public class SettingData public class SettingData
{ {
public string Id public string Id
{ {
get; set; get; set;
} }
public string Value public string Value
{ {
get; set; get; set;
} }
} }
} }

View File

@@ -13,69 +13,69 @@ namespace BTCPayServer.Data
{ {
public class StoreData public class StoreData
{ {
public string Id public string Id
{ {
get; get;
set; set;
} }
public List<UserStore> UserStores public List<UserStore> UserStores
{ {
get; set; get; set;
} }
public string DerivationStrategy public string DerivationStrategy
{ {
get; set; get; set;
} }
public string StoreName public string StoreName
{ {
get; set; get; set;
} }
public SpeedPolicy SpeedPolicy public SpeedPolicy SpeedPolicy
{ {
get; set; get; set;
} }
public string StoreWebsite public string StoreWebsite
{ {
get; set; get; set;
} }
public byte[] StoreCertificate public byte[] StoreCertificate
{ {
get; set; get; set;
} }
[NotMapped] [NotMapped]
public string Role public string Role
{ {
get; set; get; set;
} }
public byte[] StoreBlob public byte[] StoreBlob
{ {
get; get;
set; set;
} }
public StoreBlob GetStoreBlob(Network network) public StoreBlob GetStoreBlob(Network network)
{ {
return StoreBlob == null ? new StoreBlob() : new Serializer(network).ToObject<StoreBlob>(Encoding.UTF8.GetString(StoreBlob)); return StoreBlob == null ? new StoreBlob() : new Serializer(network).ToObject<StoreBlob>(Encoding.UTF8.GetString(StoreBlob));
} }
public void SetStoreBlob(StoreBlob storeBlob, Network network) public void SetStoreBlob(StoreBlob storeBlob, Network network)
{ {
StoreBlob = Encoding.UTF8.GetBytes(new Serializer(network).ToString(storeBlob)); StoreBlob = Encoding.UTF8.GetBytes(new Serializer(network).ToString(storeBlob));
} }
} }
public class StoreBlob public class StoreBlob
{ {
public bool NetworkFeeDisabled public bool NetworkFeeDisabled
{ {
get; set; get; set;
} }
} }
} }

View File

@@ -6,29 +6,29 @@ using System.Threading.Tasks;
namespace BTCPayServer.Data namespace BTCPayServer.Data
{ {
public class UserStore public class UserStore
{ {
public string ApplicationUserId public string ApplicationUserId
{ {
get; set; get; set;
} }
public ApplicationUser ApplicationUser public ApplicationUser ApplicationUser
{ {
get; set; get; set;
} }
public string StoreDataId public string StoreDataId
{ {
get; set; get; set;
} }
public StoreData StoreData public StoreData StoreData
{ {
get; set; get; set;
} }
public string Role public string Role
{ {
get; get;
set; set;
} }
} }
} }

View File

@@ -14,54 +14,54 @@ using System.Text.Encodings.Web;
namespace BTCPayServer namespace BTCPayServer
{ {
public static class Extensions public static class Extensions
{ {
public static string WithTrailingSlash(this string str) public static string WithTrailingSlash(this string str)
{ {
if(str.EndsWith("/")) if (str.EndsWith("/"))
return str; return str;
return str + "/"; return str + "/";
} }
public static string GetAbsoluteRoot(this HttpRequest request) public static string GetAbsoluteRoot(this HttpRequest request)
{ {
return string.Concat( return string.Concat(
request.Scheme, request.Scheme,
"://", "://",
request.Host.ToUriComponent(), request.Host.ToUriComponent(),
request.PathBase.ToUriComponent()); request.PathBase.ToUriComponent());
} }
public static IServiceCollection ConfigureBTCPayServer(this IServiceCollection services, IConfiguration conf) public static IServiceCollection ConfigureBTCPayServer(this IServiceCollection services, IConfiguration conf)
{ {
services.Configure<BTCPayServerOptions>(o => services.Configure<BTCPayServerOptions>(o =>
{ {
o.LoadArgs(conf); o.LoadArgs(conf);
}); });
return services; return services;
} }
public static BitIdentity GetBitIdentity(this Controller controller, bool throws = true) public static BitIdentity GetBitIdentity(this Controller controller, bool throws = true)
{ {
if(!(controller.User.Identity is BitIdentity)) if (!(controller.User.Identity is BitIdentity))
return throws ? throw new UnauthorizedAccessException("no-bitid") : (BitIdentity)null; return throws ? throw new UnauthorizedAccessException("no-bitid") : (BitIdentity)null;
return (BitIdentity)controller.User.Identity; return (BitIdentity)controller.User.Identity;
} }
private static JsonSerializerSettings jsonSettings = new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() }; private static JsonSerializerSettings jsonSettings = new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() };
public static string ToJson(this object o) public static string ToJson(this object o)
{ {
var res = JsonConvert.SerializeObject(o, Formatting.None, jsonSettings); var res = JsonConvert.SerializeObject(o, Formatting.None, jsonSettings);
return res; return res;
} }
public static HtmlString ToSrvModel(this object o) public static HtmlString ToSrvModel(this object o)
{ {
var encodedJson = JavaScriptEncoder.Default.Encode(o.ToJson()); var encodedJson = JavaScriptEncoder.Default.Encode(o.ToJson());
return new HtmlString("var srvModel = JSON.parse('" + encodedJson + "');"); return new HtmlString("var srvModel = JSON.parse('" + encodedJson + "');");
} }
} }
} }

View File

@@ -7,72 +7,72 @@ using System.Text;
namespace BTCPayServer.Filters namespace BTCPayServer.Filters
{ {
public class MediaTypeConstraintAttribute : Attribute, IActionConstraint public class MediaTypeConstraintAttribute : Attribute, IActionConstraint
{ {
public MediaTypeConstraintAttribute(string mediaType) public MediaTypeConstraintAttribute(string mediaType)
{ {
MediaType = mediaType ?? throw new ArgumentNullException(nameof(mediaType)); MediaType = mediaType ?? throw new ArgumentNullException(nameof(mediaType));
} }
public string MediaType public string MediaType
{ {
get; set; get; set;
} }
public int Order => 100; public int Order => 100;
public bool Accept(ActionConstraintContext context) public bool Accept(ActionConstraintContext context)
{ {
var match = context.RouteContext.HttpContext.Request.ContentType?.StartsWith(MediaType, StringComparison.Ordinal); var match = context.RouteContext.HttpContext.Request.ContentType?.StartsWith(MediaType, StringComparison.Ordinal);
return match.HasValue && match.Value; return match.HasValue && match.Value;
} }
} }
public class BitpayAPIConstraintAttribute : Attribute, IActionConstraint public class BitpayAPIConstraintAttribute : Attribute, IActionConstraint
{ {
public BitpayAPIConstraintAttribute(bool isBitpayAPI = true) public BitpayAPIConstraintAttribute(bool isBitpayAPI = true)
{ {
IsBitpayAPI = isBitpayAPI; IsBitpayAPI = isBitpayAPI;
} }
public bool IsBitpayAPI public bool IsBitpayAPI
{ {
get; set; get; set;
} }
public int Order => 100; public int Order => 100;
public bool Accept(ActionConstraintContext context) public bool Accept(ActionConstraintContext context)
{ {
var hasVersion = context.RouteContext.HttpContext.Request.Headers["x-accept-version"].Where(h => h == "2.0.0").Any(); var hasVersion = context.RouteContext.HttpContext.Request.Headers["x-accept-version"].Where(h => h == "2.0.0").Any();
var hasIdentity = context.RouteContext.HttpContext.Request.Headers["x-identity"].Any(); var hasIdentity = context.RouteContext.HttpContext.Request.Headers["x-identity"].Any();
return (hasVersion || hasIdentity) == IsBitpayAPI; return (hasVersion || hasIdentity) == IsBitpayAPI;
} }
} }
public class AcceptMediaTypeConstraintAttribute : Attribute, IActionConstraint public class AcceptMediaTypeConstraintAttribute : Attribute, IActionConstraint
{ {
public AcceptMediaTypeConstraintAttribute(string mediaType, bool expectedValue = true) public AcceptMediaTypeConstraintAttribute(string mediaType, bool expectedValue = true)
{ {
MediaType = mediaType ?? throw new ArgumentNullException(nameof(mediaType)); MediaType = mediaType ?? throw new ArgumentNullException(nameof(mediaType));
ExpectedValue = expectedValue; ExpectedValue = expectedValue;
} }
public bool ExpectedValue public bool ExpectedValue
{ {
get; set; get; set;
} }
public string MediaType public string MediaType
{ {
get; set; get; set;
} }
public int Order => 100; public int Order => 100;
public bool Accept(ActionConstraintContext context) public bool Accept(ActionConstraintContext context)
{ {
var hasHeader = context.RouteContext.HttpContext.Request.Headers["Accept"].Any(m => m.StartsWith(MediaType, StringComparison.Ordinal)); var hasHeader = context.RouteContext.HttpContext.Request.Headers["Accept"].Any(m => m.StartsWith(MediaType, StringComparison.Ordinal));
return hasHeader == ExpectedValue; return hasHeader == ExpectedValue;
} }
} }
} }

View File

@@ -6,28 +6,28 @@ using System.Threading.Tasks;
namespace BTCPayServer.Filters namespace BTCPayServer.Filters
{ {
public class XFrameOptionsAttribute : Attribute, IActionFilter public class XFrameOptionsAttribute : Attribute, IActionFilter
{ {
public XFrameOptionsAttribute(string value) public XFrameOptionsAttribute(string value)
{ {
Value = value; Value = value;
} }
public string Value public string Value
{ {
get; set; get; set;
} }
public void OnActionExecuted(ActionExecutedContext context) public void OnActionExecuted(ActionExecutedContext context)
{ {
}
public void OnActionExecuting(ActionExecutingContext context) }
{
var existing = context.HttpContext.Response.Headers["x-frame-options"].FirstOrDefault(); public void OnActionExecuting(ActionExecutingContext context)
if(existing != null && Value == null) {
context.HttpContext.Response.Headers.Remove("x-frame-options"); var existing = context.HttpContext.Response.Headers["x-frame-options"].FirstOrDefault();
else if (existing != null && Value == null)
context.HttpContext.Response.Headers["x-frame-options"] = Value; context.HttpContext.Response.Headers.Remove("x-frame-options");
} else
} context.HttpContext.Response.Headers["x-frame-options"] = Value;
}
}
} }

View File

@@ -37,178 +37,178 @@ using Microsoft.Extensions.Caching.Memory;
namespace BTCPayServer.Hosting namespace BTCPayServer.Hosting
{ {
public static class BTCPayServerServices public static class BTCPayServerServices
{ {
public class OwnStoreAuthorizationRequirement : IAuthorizationRequirement public class OwnStoreAuthorizationRequirement : IAuthorizationRequirement
{ {
public OwnStoreAuthorizationRequirement() public OwnStoreAuthorizationRequirement()
{ {
} }
public OwnStoreAuthorizationRequirement(string role) public OwnStoreAuthorizationRequirement(string role)
{ {
Role = role; Role = role;
} }
public string Role public string Role
{ {
get; set; get; set;
} }
} }
public class OwnStoreHandler : AuthorizationHandler<OwnStoreAuthorizationRequirement> public class OwnStoreHandler : AuthorizationHandler<OwnStoreAuthorizationRequirement>
{ {
StoreRepository _StoreRepository; StoreRepository _StoreRepository;
UserManager<ApplicationUser> _UserManager; UserManager<ApplicationUser> _UserManager;
public OwnStoreHandler(StoreRepository storeRepository, UserManager<ApplicationUser> userManager) public OwnStoreHandler(StoreRepository storeRepository, UserManager<ApplicationUser> userManager)
{ {
_StoreRepository = storeRepository; _StoreRepository = storeRepository;
_UserManager = userManager; _UserManager = userManager;
} }
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, OwnStoreAuthorizationRequirement requirement) protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, OwnStoreAuthorizationRequirement requirement)
{ {
object storeId = null; object storeId = null;
if(!((Microsoft.AspNetCore.Mvc.ActionContext)context.Resource).RouteData.Values.TryGetValue("storeId", out storeId)) if (!((Microsoft.AspNetCore.Mvc.ActionContext)context.Resource).RouteData.Values.TryGetValue("storeId", out storeId))
context.Succeed(requirement); context.Succeed(requirement);
else if(storeId != null) else if (storeId != null)
{ {
var user = _UserManager.GetUserId(((Microsoft.AspNetCore.Mvc.ActionContext)context.Resource).HttpContext.User); var user = _UserManager.GetUserId(((Microsoft.AspNetCore.Mvc.ActionContext)context.Resource).HttpContext.User);
if(user != null) if (user != null)
{ {
var store = await _StoreRepository.FindStore((string)storeId, user); var store = await _StoreRepository.FindStore((string)storeId, user);
if(store != null) if (store != null)
if(requirement.Role == null || requirement.Role == store.Role) if (requirement.Role == null || requirement.Role == store.Role)
context.Succeed(requirement); context.Succeed(requirement);
} }
} }
} }
} }
class BTCPayServerConfigureOptions : IConfigureOptions<MvcOptions> class BTCPayServerConfigureOptions : IConfigureOptions<MvcOptions>
{ {
BTCPayServerOptions _Options; BTCPayServerOptions _Options;
public BTCPayServerConfigureOptions(BTCPayServerOptions options) public BTCPayServerConfigureOptions(BTCPayServerOptions options)
{ {
_Options = options; _Options = options;
} }
public void Configure(MvcOptions options) public void Configure(MvcOptions options)
{ {
if(_Options.RequireHttps) if (_Options.RequireHttps)
options.Filters.Add(new RequireHttpsAttribute()); options.Filters.Add(new RequireHttpsAttribute());
} }
} }
public static IServiceCollection AddBTCPayServer(this IServiceCollection services) public static IServiceCollection AddBTCPayServer(this IServiceCollection services)
{ {
services.AddDbContext<ApplicationDbContext>((provider, o) => services.AddDbContext<ApplicationDbContext>((provider, o) =>
{ {
var factory = provider.GetRequiredService<ApplicationDbContextFactory>(); var factory = provider.GetRequiredService<ApplicationDbContextFactory>();
factory.ConfigureBuilder(o); factory.ConfigureBuilder(o);
}); });
services.TryAddSingleton<SettingsRepository>(); services.TryAddSingleton<SettingsRepository>();
services.TryAddSingleton<InvoicePaymentNotification>(); services.TryAddSingleton<InvoicePaymentNotification>();
services.TryAddSingleton<BTCPayServerOptions>(o => o.GetRequiredService<IOptions<BTCPayServerOptions>>().Value); services.TryAddSingleton<BTCPayServerOptions>(o => o.GetRequiredService<IOptions<BTCPayServerOptions>>().Value);
services.TryAddSingleton<IConfigureOptions<MvcOptions>, BTCPayServerConfigureOptions>(); services.TryAddSingleton<IConfigureOptions<MvcOptions>, BTCPayServerConfigureOptions>();
services.TryAddSingleton(o => services.TryAddSingleton(o =>
{ {
var runtime = new BTCPayServerRuntime(); var runtime = new BTCPayServerRuntime();
runtime.Configure(o.GetRequiredService<BTCPayServerOptions>()); runtime.Configure(o.GetRequiredService<BTCPayServerOptions>());
return runtime; return runtime;
}); });
services.AddSingleton<BTCPayServerEnvironment>(); services.AddSingleton<BTCPayServerEnvironment>();
services.TryAddSingleton<TokenRepository>(); services.TryAddSingleton<TokenRepository>();
services.TryAddSingleton(o => o.GetRequiredService<BTCPayServerRuntime>().InvoiceRepository); services.TryAddSingleton(o => o.GetRequiredService<BTCPayServerRuntime>().InvoiceRepository);
services.TryAddSingleton<Network>(o => o.GetRequiredService<BTCPayServerOptions>().Network); services.TryAddSingleton<Network>(o => o.GetRequiredService<BTCPayServerOptions>().Network);
services.TryAddSingleton<ApplicationDbContextFactory>(o => o.GetRequiredService<BTCPayServerRuntime>().DBFactory); services.TryAddSingleton<ApplicationDbContextFactory>(o => o.GetRequiredService<BTCPayServerRuntime>().DBFactory);
services.TryAddSingleton<StoreRepository>(); services.TryAddSingleton<StoreRepository>();
services.TryAddSingleton<BTCPayWallet>(); services.TryAddSingleton<BTCPayWallet>();
services.TryAddSingleton<CurrencyNameTable>(); services.TryAddSingleton<CurrencyNameTable>();
services.TryAddSingleton<IFeeProvider>(o => new NBXplorerFeeProvider() services.TryAddSingleton<IFeeProvider>(o => new NBXplorerFeeProvider()
{ {
Fallback = new FeeRate(100, 1), Fallback = new FeeRate(100, 1),
BlockTarget = 20, BlockTarget = 20,
ExplorerClient = o.GetRequiredService<ExplorerClient>() ExplorerClient = o.GetRequiredService<ExplorerClient>()
}); });
services.TryAddSingleton<ExplorerClient>(o => services.TryAddSingleton<ExplorerClient>(o =>
{ {
var runtime = o.GetRequiredService<BTCPayServerRuntime>(); var runtime = o.GetRequiredService<BTCPayServerRuntime>();
return runtime.Explorer; return runtime.Explorer;
}); });
services.TryAddSingleton<Bitpay>(o => services.TryAddSingleton<Bitpay>(o =>
{ {
if(o.GetRequiredService<BTCPayServerOptions>().Network == Network.Main) if (o.GetRequiredService<BTCPayServerOptions>().Network == Network.Main)
return new Bitpay(new Key(), new Uri("https://bitpay.com/")); return new Bitpay(new Key(), new Uri("https://bitpay.com/"));
else else
return new Bitpay(new Key(), new Uri("https://test.bitpay.com/")); return new Bitpay(new Key(), new Uri("https://test.bitpay.com/"));
}); });
services.TryAddSingleton<IRateProvider>(o => services.TryAddSingleton<IRateProvider>(o =>
{ {
return new CachedRateProvider(new CoinAverageRateProvider(), o.GetRequiredService<IMemoryCache>()) { CacheSpan = TimeSpan.FromMinutes(1.0) }; return new CachedRateProvider(new CoinAverageRateProvider(), o.GetRequiredService<IMemoryCache>()) { CacheSpan = TimeSpan.FromMinutes(1.0) };
}); });
services.TryAddSingleton<InvoiceWatcher>(); services.TryAddSingleton<InvoiceWatcher>();
services.TryAddSingleton<InvoiceNotificationManager>(); services.TryAddSingleton<InvoiceNotificationManager>();
services.TryAddSingleton<IHostedService>(o => o.GetRequiredService<InvoiceWatcher>()); services.TryAddSingleton<IHostedService>(o => o.GetRequiredService<InvoiceWatcher>());
services.TryAddScoped<IHttpContextAccessor, HttpContextAccessor>(); services.TryAddScoped<IHttpContextAccessor, HttpContextAccessor>();
services.TryAddSingleton<IAuthorizationHandler, OwnStoreHandler>(); services.TryAddSingleton<IAuthorizationHandler, OwnStoreHandler>();
services.AddTransient<AccessTokenController>(); services.AddTransient<AccessTokenController>();
services.AddTransient<CallbackController>(); services.AddTransient<CallbackController>();
services.AddTransient<InvoiceController>(); services.AddTransient<InvoiceController>();
// Add application services. // Add application services.
services.AddTransient<IEmailSender, EmailSender>(); services.AddTransient<IEmailSender, EmailSender>();
services.AddAuthorization(o => services.AddAuthorization(o =>
{ {
o.AddPolicy("CanAccessStore", builder => o.AddPolicy("CanAccessStore", builder =>
{ {
builder.AddRequirements(new OwnStoreAuthorizationRequirement()); builder.AddRequirements(new OwnStoreAuthorizationRequirement());
}); });
o.AddPolicy("OwnStore", builder => o.AddPolicy("OwnStore", builder =>
{ {
builder.AddRequirements(new OwnStoreAuthorizationRequirement("Owner")); builder.AddRequirements(new OwnStoreAuthorizationRequirement("Owner"));
}); });
}); });
return services; return services;
} }
public static IApplicationBuilder UsePayServer(this IApplicationBuilder app) public static IApplicationBuilder UsePayServer(this IApplicationBuilder app)
{ {
if(app.ApplicationServices.GetRequiredService<BTCPayServerOptions>().RequireHttps) if (app.ApplicationServices.GetRequiredService<BTCPayServerOptions>().RequireHttps)
{ {
var options = new RewriteOptions().AddRedirectToHttps(); var options = new RewriteOptions().AddRedirectToHttps();
app.UseRewriter(options); app.UseRewriter(options);
} }
using(var scope = app.ApplicationServices.GetService<IServiceScopeFactory>().CreateScope()) using (var scope = app.ApplicationServices.GetService<IServiceScopeFactory>().CreateScope())
{ {
//Wait the DB is ready //Wait the DB is ready
Retry(() => Retry(() =>
{ {
scope.ServiceProvider.GetRequiredService<ApplicationDbContext>().Database.Migrate(); scope.ServiceProvider.GetRequiredService<ApplicationDbContext>().Database.Migrate();
}); });
} }
app.UseMiddleware<BTCPayMiddleware>(); app.UseMiddleware<BTCPayMiddleware>();
return app; return app;
} }
static void Retry(Action act) static void Retry(Action act)
{ {
CancellationTokenSource cts = new CancellationTokenSource(10000); CancellationTokenSource cts = new CancellationTokenSource(10000);
while(true) while (true)
{ {
try try
{ {
act(); act();
return; return;
} }
catch catch
{ {
if(cts.IsCancellationRequested) if (cts.IsCancellationRequested)
throw; throw;
Thread.Sleep(1000); Thread.Sleep(1000);
} }
} }
} }
} }
} }

View File

@@ -25,94 +25,94 @@ using BTCPayServer.Controllers;
namespace BTCPayServer.Hosting namespace BTCPayServer.Hosting
{ {
public class BTCPayMiddleware public class BTCPayMiddleware
{ {
TokenRepository _TokenRepository; TokenRepository _TokenRepository;
RequestDelegate _Next; RequestDelegate _Next;
CallbackController _CallbackController; CallbackController _CallbackController;
public BTCPayMiddleware(RequestDelegate next, public BTCPayMiddleware(RequestDelegate next,
TokenRepository tokenRepo, TokenRepository tokenRepo,
CallbackController callbackController) CallbackController callbackController)
{ {
_TokenRepository = tokenRepo ?? throw new ArgumentNullException(nameof(tokenRepo)); _TokenRepository = tokenRepo ?? throw new ArgumentNullException(nameof(tokenRepo));
_Next = next ?? throw new ArgumentNullException(nameof(next)); _Next = next ?? throw new ArgumentNullException(nameof(next));
_CallbackController = callbackController; _CallbackController = callbackController;
} }
bool _Registered; bool _Registered;
public async Task Invoke(HttpContext httpContext) public async Task Invoke(HttpContext httpContext)
{ {
if(!_Registered) if (!_Registered)
{ {
var callback = await _CallbackController.RegisterCallbackBlockUriAsync(httpContext.Request); var callback = await _CallbackController.RegisterCallbackBlockUriAsync(httpContext.Request);
Logs.PayServer.LogInformation($"Registering block callback to " + callback); Logs.PayServer.LogInformation($"Registering block callback to " + callback);
_Registered = true; _Registered = true;
} }
httpContext.Request.Headers.TryGetValue("x-signature", out StringValues values); httpContext.Request.Headers.TryGetValue("x-signature", out StringValues values);
var sig = values.FirstOrDefault(); var sig = values.FirstOrDefault();
httpContext.Request.Headers.TryGetValue("x-identity", out values); httpContext.Request.Headers.TryGetValue("x-identity", out values);
var id = values.FirstOrDefault(); var id = values.FirstOrDefault();
if(!string.IsNullOrEmpty(sig) && !string.IsNullOrEmpty(id)) if (!string.IsNullOrEmpty(sig) && !string.IsNullOrEmpty(id))
{ {
httpContext.Request.EnableRewind(); httpContext.Request.EnableRewind();
string body = string.Empty; string body = string.Empty;
if(httpContext.Request.ContentLength != 0 && httpContext.Request.Body != null) if (httpContext.Request.ContentLength != 0 && httpContext.Request.Body != null)
{ {
using(StreamReader reader = new StreamReader(httpContext.Request.Body, Encoding.UTF8, true, 1024, true)) using (StreamReader reader = new StreamReader(httpContext.Request.Body, Encoding.UTF8, true, 1024, true))
{ {
body = reader.ReadToEnd(); body = reader.ReadToEnd();
} }
httpContext.Request.Body.Position = 0; httpContext.Request.Body.Position = 0;
} }
var url = httpContext.Request.GetEncodedUrl(); var url = httpContext.Request.GetEncodedUrl();
try try
{ {
var key = new PubKey(id); var key = new PubKey(id);
if(BitIdExtensions.CheckBitIDSignature(key, sig, url, body)) if (BitIdExtensions.CheckBitIDSignature(key, sig, url, body))
{ {
var bitid = new BitIdentity(key); var bitid = new BitIdentity(key);
httpContext.User = new GenericPrincipal(bitid, new string[0]); httpContext.User = new GenericPrincipal(bitid, new string[0]);
Logs.PayServer.LogDebug($"BitId signature check success for SIN {bitid.SIN}"); Logs.PayServer.LogDebug($"BitId signature check success for SIN {bitid.SIN}");
} }
} }
catch(FormatException) { } catch (FormatException) { }
if(!(httpContext.User.Identity is BitIdentity)) if (!(httpContext.User.Identity is BitIdentity))
Logs.PayServer.LogDebug("BitId signature check failed"); Logs.PayServer.LogDebug("BitId signature check failed");
} }
try try
{ {
await _Next(httpContext); await _Next(httpContext);
} }
catch(UnauthorizedAccessException ex) catch (UnauthorizedAccessException ex)
{ {
await HandleBitpayHttpException(httpContext, new BitpayHttpException(401, ex.Message)); await HandleBitpayHttpException(httpContext, new BitpayHttpException(401, ex.Message));
} }
catch(BitpayHttpException ex) catch (BitpayHttpException ex)
{ {
await HandleBitpayHttpException(httpContext, ex); await HandleBitpayHttpException(httpContext, ex);
} }
catch(Exception ex) catch (Exception ex)
{ {
Logs.PayServer.LogCritical(new EventId(), ex, "Unhandled exception in BTCPayMiddleware"); Logs.PayServer.LogCritical(new EventId(), ex, "Unhandled exception in BTCPayMiddleware");
throw; throw;
} }
} }
private static async Task HandleBitpayHttpException(HttpContext httpContext, BitpayHttpException ex) private static async Task HandleBitpayHttpException(HttpContext httpContext, BitpayHttpException ex)
{ {
httpContext.Response.StatusCode = ex.StatusCode; httpContext.Response.StatusCode = ex.StatusCode;
using(var writer = new StreamWriter(httpContext.Response.Body, new UTF8Encoding(false), 1024, true)) using (var writer = new StreamWriter(httpContext.Response.Body, new UTF8Encoding(false), 1024, true))
{ {
httpContext.Response.ContentType = "application/json"; httpContext.Response.ContentType = "application/json";
var result = JsonConvert.SerializeObject(new BitpayErrorsModel(ex)); var result = JsonConvert.SerializeObject(new BitpayErrorsModel(ex));
writer.Write(result); writer.Write(result);
await writer.FlushAsync(); await writer.FlushAsync();
} }
} }
} }
} }

View File

@@ -39,117 +39,117 @@ using Microsoft.AspNetCore.Mvc.Cors.Internal;
namespace BTCPayServer.Hosting namespace BTCPayServer.Hosting
{ {
public class Startup public class Startup
{ {
class NeedRole : IDashboardAuthorizationFilter class NeedRole : IDashboardAuthorizationFilter
{ {
string _Role; string _Role;
public NeedRole(string role) public NeedRole(string role)
{ {
_Role = role; _Role = role;
} }
public bool Authorize([NotNull] DashboardContext context) public bool Authorize([NotNull] DashboardContext context)
{ {
return context.GetHttpContext().User.IsInRole(_Role); return context.GetHttpContext().User.IsInRole(_Role);
} }
} }
public Startup(IConfiguration conf, IHostingEnvironment env) public Startup(IConfiguration conf, IHostingEnvironment env)
{ {
Configuration = conf; Configuration = conf;
_Env = env; _Env = env;
} }
IHostingEnvironment _Env; IHostingEnvironment _Env;
public IConfiguration Configuration public IConfiguration Configuration
{ {
get; set; get; set;
} }
public void ConfigureServices(IServiceCollection services)
{
services.ConfigureBTCPayServer(Configuration);
services.AddMemoryCache();
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
// Big hack, tests fails because Hangfire fail at initializing at the second test run public void ConfigureServices(IServiceCollection services)
AddHangfireFix(services); {
services.AddBTCPayServer(); services.ConfigureBTCPayServer(Configuration);
services.AddMvc(o => services.AddMemoryCache();
{ services.AddIdentity<ApplicationUser, IdentityRole>()
o.Filters.Add(new XFrameOptionsAttribute("DENY")); .AddEntityFrameworkStores<ApplicationDbContext>()
}); .AddDefaultTokenProviders();
}
// Big hack, tests fails if only call AddHangfire because Hangfire fail at initializing at the second test run // Big hack, tests fails because Hangfire fail at initializing at the second test run
private void AddHangfireFix(IServiceCollection services) AddHangfireFix(services);
{ services.AddBTCPayServer();
Action<IGlobalConfiguration> configuration = o => services.AddMvc(o =>
{ {
var scope = AspNetCoreJobActivator.Current.BeginScope(null); o.Filters.Add(new XFrameOptionsAttribute("DENY"));
var options = (ApplicationDbContextFactory)scope.Resolve(typeof(ApplicationDbContextFactory)); });
options.ConfigureHangfireBuilder(o); }
};
ServiceCollectionDescriptorExtensions.TryAddSingleton<Action<IGlobalConfiguration>>(services, (IServiceProvider serviceProvider) => new Action<IGlobalConfiguration>((config) => // Big hack, tests fails if only call AddHangfire because Hangfire fail at initializing at the second test run
{ private void AddHangfireFix(IServiceCollection services)
ILoggerFactory service = ServiceProviderServiceExtensions.GetService<ILoggerFactory>(serviceProvider); {
if(service != null) Action<IGlobalConfiguration> configuration = o =>
{ {
Hangfire.GlobalConfigurationExtensions.UseLogProvider<AspNetCoreLogProvider>(config, new AspNetCoreLogProvider(service)); var scope = AspNetCoreJobActivator.Current.BeginScope(null);
} var options = (ApplicationDbContextFactory)scope.Resolve(typeof(ApplicationDbContextFactory));
IServiceScopeFactory service2 = ServiceProviderServiceExtensions.GetService<IServiceScopeFactory>(serviceProvider); options.ConfigureHangfireBuilder(o);
if(service2 != null) };
{
Hangfire.GlobalConfigurationExtensions.UseActivator<AspNetCoreJobActivator>(config, new AspNetCoreJobActivator(service2));
}
configuration(config);
}));
services.AddHangfire(configuration); ServiceCollectionDescriptorExtensions.TryAddSingleton<Action<IGlobalConfiguration>>(services, (IServiceProvider serviceProvider) => new Action<IGlobalConfiguration>((config) =>
services.AddCors(o => {
{ ILoggerFactory service = ServiceProviderServiceExtensions.GetService<ILoggerFactory>(serviceProvider);
o.AddPolicy("BitpayAPI", b => if (service != null)
{ {
b.AllowAnyMethod().AllowAnyHeader().AllowAnyOrigin(); Hangfire.GlobalConfigurationExtensions.UseLogProvider<AspNetCoreLogProvider>(config, new AspNetCoreLogProvider(service));
}); }
}); IServiceScopeFactory service2 = ServiceProviderServiceExtensions.GetService<IServiceScopeFactory>(serviceProvider);
if (service2 != null)
{
Hangfire.GlobalConfigurationExtensions.UseActivator<AspNetCoreJobActivator>(config, new AspNetCoreJobActivator(service2));
}
configuration(config);
}));
services.Configure<IOptions<ApplicationInsightsServiceOptions>>(o => services.AddHangfire(configuration);
{ services.AddCors(o =>
o.Value.DeveloperMode = _Env.IsDevelopment(); {
}); o.AddPolicy("BitpayAPI", b =>
} {
b.AllowAnyMethod().AllowAnyHeader().AllowAnyOrigin();
});
});
public void Configure( services.Configure<IOptions<ApplicationInsightsServiceOptions>>(o =>
IApplicationBuilder app, {
IHostingEnvironment env, o.Value.DeveloperMode = _Env.IsDevelopment();
IServiceProvider prov, });
ILoggerFactory loggerFactory) }
{
if(env.IsDevelopment()) public void Configure(
{ IApplicationBuilder app,
app.UseDeveloperExceptionPage(); IHostingEnvironment env,
app.UseBrowserLink(); IServiceProvider prov,
} ILoggerFactory loggerFactory)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseBrowserLink();
}
Logs.Configure(loggerFactory); Logs.Configure(loggerFactory);
//App insight do not that by itself... //App insight do not that by itself...
loggerFactory.AddApplicationInsights(prov, LogLevel.Information); loggerFactory.AddApplicationInsights(prov, LogLevel.Information);
app.UsePayServer(); app.UsePayServer();
app.UseStaticFiles(); app.UseStaticFiles();
app.UseAuthentication(); app.UseAuthentication();
app.UseHangfireServer(); app.UseHangfireServer();
app.UseHangfireDashboard("/hangfire", new DashboardOptions() { Authorization = new[] { new NeedRole(Roles.ServerAdmin) } }); app.UseHangfireDashboard("/hangfire", new DashboardOptions() { Authorization = new[] { new NeedRole(Roles.ServerAdmin) } });
app.UseMvc(routes => app.UseMvc(routes =>
{ {
routes.MapRoute( routes.MapRoute(
name: "default", name: "default",
template: "{controller=Home}/{action=Index}/{id?}"); template: "{controller=Home}/{action=Index}/{id?}");
}); });
} }
} }
} }

View File

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

View File

@@ -7,48 +7,48 @@ using System.Threading.Tasks;
namespace BTCPayServer.Logging namespace BTCPayServer.Logging
{ {
public class Logs public class Logs
{ {
static Logs() static Logs()
{ {
Configure(new FuncLoggerFactory(n => NullLogger.Instance)); Configure(new FuncLoggerFactory(n => NullLogger.Instance));
} }
public static void Configure(ILoggerFactory factory) public static void Configure(ILoggerFactory factory)
{ {
Configuration = factory.CreateLogger("Configuration"); Configuration = factory.CreateLogger("Configuration");
PayServer = factory.CreateLogger("PayServer"); PayServer = factory.CreateLogger("PayServer");
} }
public static ILogger Configuration public static ILogger Configuration
{ {
get; set; get; set;
} }
public static ILogger PayServer public static ILogger PayServer
{ {
get; set; get; set;
} }
public const int ColumnLength = 16; public const int ColumnLength = 16;
} }
public class FuncLoggerFactory : ILoggerFactory public class FuncLoggerFactory : ILoggerFactory
{ {
private Func<string, ILogger> createLogger; private Func<string, ILogger> createLogger;
public FuncLoggerFactory(Func<string, ILogger> createLogger) public FuncLoggerFactory(Func<string, ILogger> createLogger)
{ {
this.createLogger = createLogger; this.createLogger = createLogger;
} }
public void AddProvider(ILoggerProvider provider) public void AddProvider(ILoggerProvider provider)
{ {
} }
public ILogger CreateLogger(string categoryName) public ILogger CreateLogger(string categoryName)
{ {
return createLogger(categoryName); return createLogger(categoryName);
} }
public void Dispose() public void Dispose()
{ {
} }
} }
} }

View File

@@ -4,51 +4,51 @@ using System.Collections.Generic;
namespace BTCPayServer.Migrations namespace BTCPayServer.Migrations
{ {
public partial class PendingInvoices : Migration public partial class PendingInvoices : Migration
{ {
protected override void Up(MigrationBuilder migrationBuilder) protected override void Up(MigrationBuilder migrationBuilder)
{ {
if(SupportDropColumn(migrationBuilder.ActiveProvider)) if (SupportDropColumn(migrationBuilder.ActiveProvider))
{ {
migrationBuilder.DropColumn( migrationBuilder.DropColumn(
name: "Name", name: "Name",
table: "PairingCodes"); table: "PairingCodes");
migrationBuilder.DropColumn( migrationBuilder.DropColumn(
name: "Name", name: "Name",
table: "PairedSINData"); table: "PairedSINData");
} }
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "PendingInvoices", name: "PendingInvoices",
columns: table => new columns: table => new
{ {
Id = table.Column<string>(type: "TEXT", nullable: false) Id = table.Column<string>(type: "TEXT", nullable: false)
}, },
constraints: table => constraints: table =>
{ {
table.PrimaryKey("PK_PendingInvoices", x => x.Id); table.PrimaryKey("PK_PendingInvoices", x => x.Id);
}); });
} }
private bool SupportDropColumn(string activeProvider) private bool SupportDropColumn(string activeProvider)
{ {
return activeProvider != "Microsoft.EntityFrameworkCore.Sqlite"; return activeProvider != "Microsoft.EntityFrameworkCore.Sqlite";
} }
protected override void Down(MigrationBuilder migrationBuilder) protected override void Down(MigrationBuilder migrationBuilder)
{ {
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "PendingInvoices"); name: "PendingInvoices");
migrationBuilder.AddColumn<string>( migrationBuilder.AddColumn<string>(
name: "Name", name: "Name",
table: "PairingCodes", table: "PairingCodes",
nullable: true); nullable: true);
migrationBuilder.AddColumn<string>( migrationBuilder.AddColumn<string>(
name: "Name", name: "Name",
table: "PairedSINData", table: "PairedSINData",
nullable: true); nullable: true);
} }
} }
} }

View File

@@ -8,9 +8,9 @@ namespace BTCPayServer.Models.AccountViewModels
{ {
public class LoginWithRecoveryCodeViewModel public class LoginWithRecoveryCodeViewModel
{ {
[Required] [Required]
[DataType(DataType.Text)] [DataType(DataType.Text)]
[Display(Name = "Recovery Code")] [Display(Name = "Recovery Code")]
public string RecoveryCode { get; set; } public string RecoveryCode { get; set; }
} }
} }

View File

@@ -10,15 +10,15 @@ namespace BTCPayServer.Models
// Add profile data for application users by adding properties to the ApplicationUser class // Add profile data for application users by adding properties to the ApplicationUser class
public class ApplicationUser : IdentityUser public class ApplicationUser : IdentityUser
{ {
public List<UserStore> UserStores public List<UserStore> UserStores
{ {
get; get;
set; set;
} }
public bool RequiresEmailConfirmation public bool RequiresEmailConfirmation
{ {
get; set; get; set;
} }
} }
} }

View File

@@ -5,35 +5,35 @@ using System.Text;
namespace BTCPayServer.Models namespace BTCPayServer.Models
{ {
public class BitpayErrorsModel public class BitpayErrorsModel
{ {
public BitpayErrorsModel() public BitpayErrorsModel()
{ {
} }
public BitpayErrorsModel(BitpayHttpException ex) public BitpayErrorsModel(BitpayHttpException ex)
{ {
Error = ex.Message; Error = ex.Message;
} }
[JsonProperty("errors", DefaultValueHandling = DefaultValueHandling.Ignore)] [JsonProperty("errors", DefaultValueHandling = DefaultValueHandling.Ignore)]
public BitpayErrorModel[] Errors public BitpayErrorModel[] Errors
{ {
get; set; get; set;
} }
[JsonProperty("error", DefaultValueHandling = DefaultValueHandling.Ignore)] [JsonProperty("error", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string Error public string Error
{ {
get; set; get; set;
} }
} }
public class BitpayErrorModel public class BitpayErrorModel
{ {
[JsonProperty("error")] [JsonProperty("error")]
public string Error public string Error
{ {
get; set; get; set;
} }
} }
} }

View File

@@ -7,17 +7,17 @@ namespace BTCPayServer.Models
{ {
public class ConfirmModel public class ConfirmModel
{ {
public string Title public string Title
{ {
get; set; get; set;
} }
public string Description public string Description
{ {
get; set; get; set;
} }
public string Action public string Action
{ {
get; set; get; set;
} }
} }
} }

View File

@@ -5,33 +5,33 @@ using System.Text;
namespace BTCPayServer.Models namespace BTCPayServer.Models
{ {
public class DataWrapper public class DataWrapper
{ {
public static DataWrapper<T> Create<T>(T obj) public static DataWrapper<T> Create<T>(T obj)
{ {
return new DataWrapper<T>(obj); return new DataWrapper<T>(obj);
} }
} }
public class DataWrapper<T> public class DataWrapper<T>
{ {
public DataWrapper() public DataWrapper()
{ {
} }
public DataWrapper(T data) public DataWrapper(T data)
{ {
Data = data; Data = data;
} }
[JsonProperty("facade", NullValueHandling = NullValueHandling.Ignore)] [JsonProperty("facade", NullValueHandling = NullValueHandling.Ignore)]
public string Facade public string Facade
{ {
get; set; get; set;
} }
[JsonProperty("data")] [JsonProperty("data")]
public T Data public T Data
{ {
get; set; get; set;
} }
} }
} }

View File

@@ -12,43 +12,43 @@ using System.IO;
namespace BTCPayServer.Models namespace BTCPayServer.Models
{ {
//{"data":[{"pos":"FfZ6WCa8TunAvPCpQZXkdBsoH4Yo18FyPaJ5X5qjrVVY"},{"pos/invoice":"H1pwwh2tMeSCri9rh5VvHWEHokGdf2EGtghfZkUEbeZv"},{"merchant":"89zEBr9orAc6wgybAABp8ioGcjYeFrUaZgMzjxNuqYty"},{"merchant/invoice":"8e7ijDxGfJsWXWgJuKXjjNgxnX1xpsBM8cTZCFnU7ehj"}]} //{"data":[{"pos":"FfZ6WCa8TunAvPCpQZXkdBsoH4Yo18FyPaJ5X5qjrVVY"},{"pos/invoice":"H1pwwh2tMeSCri9rh5VvHWEHokGdf2EGtghfZkUEbeZv"},{"merchant":"89zEBr9orAc6wgybAABp8ioGcjYeFrUaZgMzjxNuqYty"},{"merchant/invoice":"8e7ijDxGfJsWXWgJuKXjjNgxnX1xpsBM8cTZCFnU7ehj"}]}
public class GetTokensResponse : IActionResult public class GetTokensResponse : IActionResult
{ {
BitTokenEntity[] _Tokens; BitTokenEntity[] _Tokens;
public GetTokensResponse(BitTokenEntity[] tokens) public GetTokensResponse(BitTokenEntity[] tokens)
{ {
if(tokens == null) if (tokens == null)
throw new ArgumentNullException(nameof(tokens)); throw new ArgumentNullException(nameof(tokens));
this._Tokens = tokens; this._Tokens = tokens;
} }
[JsonProperty(PropertyName = "data")] [JsonProperty(PropertyName = "data")]
//{"pos":"FfZ6WCa8TunAvPCpQZXkdBsoH4Yo18FyPaJ5X5qjrVVY"} //{"pos":"FfZ6WCa8TunAvPCpQZXkdBsoH4Yo18FyPaJ5X5qjrVVY"}
public JArray Data public JArray Data
{ {
get; set; get; set;
} }
public async Task ExecuteResultAsync(ActionContext context) public async Task ExecuteResultAsync(ActionContext context)
{ {
JObject jobj = new JObject(); JObject jobj = new JObject();
JArray jarray = new JArray(); JArray jarray = new JArray();
jobj.Add("data", jarray); jobj.Add("data", jarray);
foreach(var token in _Tokens) foreach (var token in _Tokens)
{ {
JObject item = new JObject(); JObject item = new JObject();
jarray.Add(item); jarray.Add(item);
JProperty jProp = new JProperty(token.Facade); JProperty jProp = new JProperty(token.Facade);
item.Add(jProp); item.Add(jProp);
jProp.Value = token.Value; jProp.Value = token.Value;
} }
context.HttpContext.Response.Headers.Add("Content-Type", new Microsoft.Extensions.Primitives.StringValues("application/json")); context.HttpContext.Response.Headers.Add("Content-Type", new Microsoft.Extensions.Primitives.StringValues("application/json"));
var str = JsonConvert.SerializeObject(jobj); var str = JsonConvert.SerializeObject(jobj);
using(var writer = new StreamWriter(context.HttpContext.Response.Body, new UTF8Encoding(false), 1024 * 10, true)) using (var writer = new StreamWriter(context.HttpContext.Response.Body, new UTF8Encoding(false), 1024 * 10, true))
{ {
await writer.WriteAsync(str); await writer.WriteAsync(str);
} }
} }
} }
} }

View File

@@ -6,220 +6,220 @@ using System.Text;
namespace BTCPayServer.Models namespace BTCPayServer.Models
{ {
class DateTimeJsonConverter : JsonConverter class DateTimeJsonConverter : JsonConverter
{ {
public override bool CanConvert(Type objectType) public override bool CanConvert(Type objectType)
{ {
return objectType == typeof(DateTimeOffset); return objectType == typeof(DateTimeOffset);
} }
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{ {
var v = (long)reader.Value; var v = (long)reader.Value;
Check(v); Check(v);
return unixRef + TimeSpan.FromMilliseconds((long)v); return unixRef + TimeSpan.FromMilliseconds((long)v);
} }
static DateTimeOffset unixRef = new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero); static DateTimeOffset unixRef = new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero);
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{ {
var date = ((DateTimeOffset)value).ToUniversalTime(); var date = ((DateTimeOffset)value).ToUniversalTime();
long v = (long)(date - unixRef).TotalMilliseconds; long v = (long)(date - unixRef).TotalMilliseconds;
Check(v); Check(v);
writer.WriteValue(v); writer.WriteValue(v);
} }
private static void Check(long v) private static void Check(long v)
{ {
if(v < 0) if (v < 0)
throw new FormatException("Invalid datetime (less than 1/1/1970)"); throw new FormatException("Invalid datetime (less than 1/1/1970)");
} }
} }
//{"facade":"pos/invoice","data":{,}} //{"facade":"pos/invoice","data":{,}}
public class InvoiceResponse public class InvoiceResponse
{ {
//"url":"https://test.bitpay.com/invoice?id=9saCHtp1zyPcNoi3rDdBu8" //"url":"https://test.bitpay.com/invoice?id=9saCHtp1zyPcNoi3rDdBu8"
[JsonProperty("url")] [JsonProperty("url")]
public string Url public string Url
{ {
get; set; get; set;
} }
//"posData":"posData" //"posData":"posData"
[JsonProperty("posData")] [JsonProperty("posData")]
public string PosData public string PosData
{ {
get; set; get; set;
} }
//status":"new" //status":"new"
[JsonProperty("status")] [JsonProperty("status")]
public string Status public string Status
{ {
get; set; get; set;
} }
//"btcPrice":"0.001157" //"btcPrice":"0.001157"
[JsonProperty("btcPrice")] [JsonProperty("btcPrice")]
public string BTCPrice public string BTCPrice
{ {
get; set; get; set;
} }
//"btcDue":"0.001160" //"btcDue":"0.001160"
[JsonProperty("btcDue")] [JsonProperty("btcDue")]
public string BTCDue public string BTCDue
{ {
get; set; get; set;
} }
//"price":5 //"price":5
[JsonProperty("price")] [JsonProperty("price")]
public double Price public double Price
{ {
get; set; get; set;
} }
//"currency":"USD" //"currency":"USD"
[JsonProperty("currency")] [JsonProperty("currency")]
public string Currency public string Currency
{ {
get; set; get; set;
} }
//"exRates":{"USD":4320.02} //"exRates":{"USD":4320.02}
[JsonProperty("exRates")] [JsonProperty("exRates")]
public Dictionary<string, double> ExRates public Dictionary<string, double> ExRates
{ {
get; set; get; set;
} }
//"buyerTotalBtcAmount":"0.001160" //"buyerTotalBtcAmount":"0.001160"
[JsonProperty("buyerTotalBtcAmount")] [JsonProperty("buyerTotalBtcAmount")]
public string BuyerTotalBtcAmount public string BuyerTotalBtcAmount
{ {
get; set; get; set;
} }
//"itemDesc":"Some description" //"itemDesc":"Some description"
[JsonProperty("itemDesc")] [JsonProperty("itemDesc")]
public string ItemDesc public string ItemDesc
{ {
get; set; get; set;
} }
//"orderId":"orderId" //"orderId":"orderId"
[JsonProperty("orderId")] [JsonProperty("orderId")]
public string OrderId public string OrderId
{ {
get; set; get; set;
} }
//"guid":"e238ce2a-06da-47e9-aefd-2588d4aa5f8d" //"guid":"e238ce2a-06da-47e9-aefd-2588d4aa5f8d"
[JsonProperty("guid")] [JsonProperty("guid")]
public string Guid public string Guid
{ {
get; set; get; set;
} }
//"id":"9saCHtp1zyPcNoi3rDdBu8" //"id":"9saCHtp1zyPcNoi3rDdBu8"
[JsonProperty("id")] [JsonProperty("id")]
public string Id public string Id
{ {
get; set; get; set;
} }
[JsonConverter(typeof(DateTimeJsonConverter))] [JsonConverter(typeof(DateTimeJsonConverter))]
[JsonProperty("invoiceTime")] [JsonProperty("invoiceTime")]
public DateTimeOffset InvoiceTime public DateTimeOffset InvoiceTime
{ {
get; set; get; set;
} }
[JsonConverter(typeof(DateTimeJsonConverter))] [JsonConverter(typeof(DateTimeJsonConverter))]
[JsonProperty("expirationTime")] [JsonProperty("expirationTime")]
public DateTimeOffset ExpirationTime public DateTimeOffset ExpirationTime
{ {
get; set; get; set;
} }
[JsonConverter(typeof(DateTimeJsonConverter))] [JsonConverter(typeof(DateTimeJsonConverter))]
[JsonProperty("currentTime")] [JsonProperty("currentTime")]
public DateTimeOffset CurrentTime public DateTimeOffset CurrentTime
{ {
get; set; get; set;
} }
//"lowFeeDetected":false //"lowFeeDetected":false
[JsonProperty("lowFeeDetected")] [JsonProperty("lowFeeDetected")]
public bool LowFeeDetected public bool LowFeeDetected
{ {
get; set; get; set;
} }
//"btcPaid":"0.000000" //"btcPaid":"0.000000"
[JsonProperty("btcPaid")] [JsonProperty("btcPaid")]
public string BTCPaid public string BTCPaid
{ {
get; set; get; set;
} }
//"rate":4320.02 //"rate":4320.02
[JsonProperty("rate")] [JsonProperty("rate")]
public double Rate public double Rate
{ {
get; set; get; set;
} }
//"exceptionStatus":false //"exceptionStatus":false
//Can be `paidPartial`, `paidOver`, or false //Can be `paidPartial`, `paidOver`, or false
[JsonProperty("exceptionStatus")] [JsonProperty("exceptionStatus")]
public JToken ExceptionStatus public JToken ExceptionStatus
{ {
get; set; get; set;
} }
//"paymentUrls":{"BIP21":"bitcoin:muFQCEbfRJohcds3bkfv1sRFj8uVTfv2wv?amount=0.001160","BIP72":"bitcoin:muFQCEbfRJohcds3bkfv1sRFj8uVTfv2wv?amount=0.001160&r=https://test.bitpay.com/i/9saCHtp1zyPcNoi3rDdBu8","BIP72b":"bitcoin:?r=https://test.bitpay.com/i/9saCHtp1zyPcNoi3rDdBu8","BIP73":"https://test.bitpay.com/i/9saCHtp1zyPcNoi3rDdBu8"} //"paymentUrls":{"BIP21":"bitcoin:muFQCEbfRJohcds3bkfv1sRFj8uVTfv2wv?amount=0.001160","BIP72":"bitcoin:muFQCEbfRJohcds3bkfv1sRFj8uVTfv2wv?amount=0.001160&r=https://test.bitpay.com/i/9saCHtp1zyPcNoi3rDdBu8","BIP72b":"bitcoin:?r=https://test.bitpay.com/i/9saCHtp1zyPcNoi3rDdBu8","BIP73":"https://test.bitpay.com/i/9saCHtp1zyPcNoi3rDdBu8"}
[JsonProperty("paymentUrls")] [JsonProperty("paymentUrls")]
public NBitpayClient.InvoicePaymentUrls PaymentUrls public NBitpayClient.InvoicePaymentUrls PaymentUrls
{ {
get; set; get; set;
} }
//"refundAddressRequestPending":false //"refundAddressRequestPending":false
[JsonProperty("refundAddressRequestPending")] [JsonProperty("refundAddressRequestPending")]
public bool RefundAddressRequestPending public bool RefundAddressRequestPending
{ {
get; set; get; set;
} }
//"buyerPaidBtcMinerFee":"0.000003" //"buyerPaidBtcMinerFee":"0.000003"
[JsonProperty("buyerPaidBtcMinerFee")] [JsonProperty("buyerPaidBtcMinerFee")]
public string BuyerPaidBtcMinerFee public string BuyerPaidBtcMinerFee
{ {
get; set; get; set;
} }
//"bitcoinAddress":"muFQCEbfRJohcds3bkfv1sRFj8uVTfv2wv" //"bitcoinAddress":"muFQCEbfRJohcds3bkfv1sRFj8uVTfv2wv"
[JsonProperty("bitcoinAddress")] [JsonProperty("bitcoinAddress")]
public string BitcoinAddress public string BitcoinAddress
{ {
get; set; get; set;
} }
//"token":"9jF3TU7A8inKHDRQXFrKcRnMkLXWGQ2yKf7pnjMKGHEfpwTNV35HytrD9FXDBy25Li" //"token":"9jF3TU7A8inKHDRQXFrKcRnMkLXWGQ2yKf7pnjMKGHEfpwTNV35HytrD9FXDBy25Li"
[JsonProperty("token")] [JsonProperty("token")]
public string Token public string Token
{ {
get; set; get; set;
} }
[JsonProperty("flags")] [JsonProperty("flags")]
public Flags Flags public Flags Flags
{ {
get; set; get; set;
} }
} }
public class Flags public class Flags
{ {
[JsonProperty("refundable")] [JsonProperty("refundable")]
public bool Refundable public bool Refundable
{ {
get; set; get; set;
} }
} }
} }

View File

@@ -7,52 +7,52 @@ using System.Threading.Tasks;
namespace BTCPayServer.Models.InvoicingModels namespace BTCPayServer.Models.InvoicingModels
{ {
public class CreateInvoiceModel public class CreateInvoiceModel
{ {
[Required] [Required]
public double? Amount public double? Amount
{ {
get; set; get; set;
} }
[Required] [Required]
public string StoreId public string StoreId
{ {
get; set; get; set;
} }
public string OrderId public string OrderId
{ {
get; set; get; set;
} }
public string ItemDesc public string ItemDesc
{ {
get; set; get; set;
} }
public string PosData public string PosData
{ {
get; set; get; set;
} }
[EmailAddress] [EmailAddress]
public string BuyerEmail public string BuyerEmail
{ {
get; set; get; set;
} }
[Url] [Url]
public string NotificationUrl public string NotificationUrl
{ {
get; set; get; set;
} }
public SelectList Stores public SelectList Stores
{ {
get; get;
set; set;
} }
} }
} }

View File

@@ -9,138 +9,138 @@ namespace BTCPayServer.Models.InvoicingModels
{ {
public class InvoiceDetailsModel public class InvoiceDetailsModel
{ {
public class Payment public class Payment
{ {
public int Confirmations public int Confirmations
{ {
get; set; get; set;
} }
public BitcoinAddress DepositAddress public BitcoinAddress DepositAddress
{ {
get; set; get; set;
} }
public string Amount public string Amount
{ {
get; set; get; set;
} }
public string TransactionId public string TransactionId
{ {
get; set; get; set;
} }
public DateTimeOffset ReceivedTime public DateTimeOffset ReceivedTime
{ {
get; get;
internal set; internal set;
} }
public string TransactionLink public string TransactionLink
{ {
get; get;
set; set;
} }
} }
public string StatusMessage public string StatusMessage
{ {
get; set; get; set;
} }
public String Id public String Id
{ {
get; set; get; set;
} }
public List<Payment> Payments public List<Payment> Payments
{ {
get; set; get; set;
} = new List<Payment>(); } = new List<Payment>();
public string Status public string Status
{ {
get; set; get; set;
} }
public DateTimeOffset CreatedDate public DateTimeOffset CreatedDate
{ {
get; set; get; set;
} }
public DateTimeOffset ExpirationDate public DateTimeOffset ExpirationDate
{ {
get; set; get; set;
} }
public string OrderId public string OrderId
{ {
get; set; get; set;
} }
public string RefundEmail public string RefundEmail
{ {
get; get;
set; set;
} }
public BuyerInformation BuyerInformation public BuyerInformation BuyerInformation
{ {
get; get;
set; set;
} }
public object StoreName public object StoreName
{ {
get; get;
internal set; internal set;
} }
public string StoreLink public string StoreLink
{ {
get; get;
set; set;
} }
public double Rate public double Rate
{ {
get; get;
internal set; internal set;
} }
public string NotificationUrl public string NotificationUrl
{ {
get; get;
internal set; internal set;
} }
public string Fiat public string Fiat
{ {
get; get;
set; set;
} }
public string BTC public string BTC
{ {
get; get;
set; set;
} }
public string BTCDue public string BTCDue
{ {
get; get;
set; set;
} }
public string BTCPaid public string BTCPaid
{ {
get; get;
internal set; internal set;
} }
public String NetworkFee public String NetworkFee
{ {
get; get;
internal set; internal set;
} }
public ProductInformation ProductInformation public ProductInformation ProductInformation
{ {
get; get;
internal set; internal set;
} }
public BitcoinAddress BitcoinAddress public BitcoinAddress BitcoinAddress
{ {
get; get;
internal set; internal set;
} }
public string PaymentUrl public string PaymentUrl
{ {
get; get;
set; set;
} }
} }
} }

View File

@@ -7,53 +7,53 @@ namespace BTCPayServer.Models.InvoicingModels
{ {
public class InvoicesModel public class InvoicesModel
{ {
public int Skip public int Skip
{ {
get; set; get; set;
} }
public int Count public int Count
{ {
get; set; get; set;
} }
public string SearchTerm public string SearchTerm
{ {
get; set; get; set;
} }
public List<InvoiceModel> Invoices public List<InvoiceModel> Invoices
{ {
get; set; get; set;
} = new List<InvoiceModel>(); } = new List<InvoiceModel>();
public string StatusMessage public string StatusMessage
{ {
get; get;
set; set;
} }
} }
public class InvoiceModel public class InvoiceModel
{ {
public DateTimeOffset Date public DateTimeOffset Date
{ {
get; set; get; set;
} }
public string InvoiceId public string InvoiceId
{ {
get; set; get; set;
} }
public string Status public string Status
{ {
get; set; get; set;
} }
public string AmountCurrency public string AmountCurrency
{ {
get; set; get; set;
} }
public string StatusMessage public string StatusMessage
{ {
get; set; get; set;
} }
} }
} }

View File

@@ -5,31 +5,31 @@ using System.Threading.Tasks;
namespace BTCPayServer.Models.InvoicingModels namespace BTCPayServer.Models.InvoicingModels
{ {
public class PaymentModel public class PaymentModel
{ {
public string ServerUrl { get; set; } public string ServerUrl { get; set; }
public string InvoiceId { get; set; } public string InvoiceId { get; set; }
public string BtcAddress { get; set; } public string BtcAddress { get; set; }
public string BtcDue { get; set; } public string BtcDue { get; set; }
public string CustomerEmail { get; set; } public string CustomerEmail { get; set; }
public int ExpirationSeconds { get; set; } public int ExpirationSeconds { get; set; }
public string Status { get; set; } public string Status { get; set; }
public string MerchantRefLink { get; set; } public string MerchantRefLink { get; set; }
public int MaxTimeSeconds { get; set; } public int MaxTimeSeconds { get; set; }
// These properties are not used in client side code // These properties are not used in client side code
public string StoreName { get; set; } public string StoreName { get; set; }
public string ItemDesc { get; set; } public string ItemDesc { get; set; }
public string TimeLeft { get; set; } public string TimeLeft { get; set; }
public string Rate { get; set; } public string Rate { get; set; }
public string BtcAmount { get; set; } public string BtcAmount { get; set; }
public string TxFees { get; set; } public string TxFees { get; set; }
public string InvoiceBitcoinUrl { get; set; } public string InvoiceBitcoinUrl { get; set; }
public string BtcTotalDue { get; set; } public string BtcTotalDue { get; set; }
public int TxCount { get; set; } public int TxCount { get; set; }
public string BtcPaid { get; set; } public string BtcPaid { get; set; }
public string StoreEmail { get; set; } public string StoreEmail { get; set; }
public string OrderId { get; set; } public string OrderId { get; set; }
} }
} }

View File

@@ -8,11 +8,11 @@ namespace BTCPayServer.Models.InvoicingModels
{ {
public class UpdateCustomerModel public class UpdateCustomerModel
{ {
[EmailAddress] [EmailAddress]
[Required] [Required]
public string Email public string Email
{ {
get; set; get; set;
} }
} }
} }

View File

@@ -9,15 +9,15 @@ namespace BTCPayServer.Models.ManageViewModels
{ {
public class EnableAuthenticatorViewModel public class EnableAuthenticatorViewModel
{ {
[Required] [Required]
[StringLength(7, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] [StringLength(7, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
[DataType(DataType.Text)] [DataType(DataType.Text)]
[Display(Name = "Verification Code")] [Display(Name = "Verification Code")]
public string Code { get; set; } public string Code { get; set; }
[ReadOnly(true)] [ReadOnly(true)]
public string SharedKey { get; set; } public string SharedKey { get; set; }
public string AuthenticatorUri { get; set; } public string AuthenticatorUri { get; set; }
} }
} }

View File

@@ -12,25 +12,25 @@ namespace BTCPayServer.Models.ManageViewModels
public string Username { get; set; } public string Username { get; set; }
[Required] [Required]
[EmailAddress] [EmailAddress]
[MaxLength(50)] [MaxLength(50)]
public string Email public string Email
{ {
get; set; get; set;
} }
public bool IsEmailConfirmed { get; set; } public bool IsEmailConfirmed { get; set; }
[Phone] [Phone]
[Display(Name = "Phone number")] [Display(Name = "Phone number")]
[MaxLength(50)] [MaxLength(50)]
public string PhoneNumber { get; set; } public string PhoneNumber { get; set; }
public string StatusMessage public string StatusMessage
{ {
get; set; get; set;
} }
} }
} }

View File

@@ -10,20 +10,20 @@ namespace BTCPayServer.Models.ServerViewModels
{ {
public class EmailsViewModel public class EmailsViewModel
{ {
public string StatusMessage public string StatusMessage
{ {
get; set; get; set;
} }
public EmailSettings Settings public EmailSettings Settings
{ {
get; set; get; set;
} }
[Required] [Required]
[EmailAddress] [EmailAddress]
public string TestEmail public string TestEmail
{ {
get; set; get; set;
} }
} }
} }

View File

@@ -7,27 +7,27 @@ namespace BTCPayServer.Models.ServerViewModels
{ {
public class UsersViewModel public class UsersViewModel
{ {
public class UserViewModel public class UserViewModel
{ {
public string Name public string Name
{ {
get; set; get; set;
} }
public string Email public string Email
{ {
get; set; get; set;
} }
} }
public string StatusMessage public string StatusMessage
{ {
get; set; get; set;
} }
public List<UserViewModel> Users public List<UserViewModel> Users
{ {
get; set; get; set;
} = new List<UserViewModel>(); } = new List<UserViewModel>();
} }
} }

View File

@@ -8,12 +8,12 @@ namespace BTCPayServer.Models.StoreViewModels
{ {
public class CreateStoreViewModel public class CreateStoreViewModel
{ {
[Required] [Required]
[MaxLength(50)] [MaxLength(50)]
[MinLength(1)] [MinLength(1)]
public string Name public string Name
{ {
get; set; get; set;
} }
} }
} }

View File

@@ -8,44 +8,44 @@ namespace BTCPayServer.Models.StoreViewModels
{ {
public class PairingModel public class PairingModel
{ {
public class StoreViewModel public class StoreViewModel
{ {
public string Name public string Name
{ {
get; set; get; set;
} }
public string Id public string Id
{ {
get; set; get; set;
} }
} }
public string Id public string Id
{ {
get; set; get; set;
} }
public string Label public string Label
{ {
get; set; get; set;
} }
public string Facade public string Facade
{ {
get; set; get; set;
} }
public string SIN public string SIN
{ {
get; set; get; set;
} }
public StoreViewModel[] Stores public StoreViewModel[] Stores
{ {
get; get;
set; set;
} }
[Display(Name = "Pair to")] [Display(Name = "Pair to")]
[Required] [Required]
public string SelectedStore public string SelectedStore
{ {
get; set; get; set;
} }
} }
} }

View File

@@ -10,50 +10,50 @@ namespace BTCPayServer.Models.StoreViewModels
{ {
public class StoreViewModel public class StoreViewModel
{ {
[Display(Name = "Store Name")] [Display(Name = "Store Name")]
[Required] [Required]
[MaxLength(50)] [MaxLength(50)]
[MinLength(1)] [MinLength(1)]
public string StoreName public string StoreName
{ {
get; set; get; set;
} }
[Url] [Url]
[Display(Name = "Store Website")] [Display(Name = "Store Website")]
[MaxLength(500)] [MaxLength(500)]
public string StoreWebsite public string StoreWebsite
{ {
get; get;
set; set;
} }
[DerivationStrategyValidator] [DerivationStrategyValidator]
public string DerivationScheme public string DerivationScheme
{ {
get; set; get; set;
} }
[Display(Name = "Consider the invoice confirmed when the payment transaction...")] [Display(Name = "Consider the invoice confirmed when the payment transaction...")]
public SpeedPolicy SpeedPolicy public SpeedPolicy SpeedPolicy
{ {
get; set; get; set;
} }
[Display(Name = "Add network fee to invoice (vary with mining fees)")] [Display(Name = "Add network fee to invoice (vary with mining fees)")]
public bool NetworkFee public bool NetworkFee
{ {
get; set; get; set;
} }
public List<(string KeyPath, string Address)> AddressSamples public List<(string KeyPath, string Address)> AddressSamples
{ {
get; set; get; set;
} = new List<(string KeyPath, string Address)>(); } = new List<(string KeyPath, string Address)>();
public string StatusMessage public string StatusMessage
{ {
get; set; get; set;
} }
} }
} }

View File

@@ -8,34 +8,34 @@ namespace BTCPayServer.Models.StoreViewModels
{ {
public class StoresViewModel public class StoresViewModel
{ {
public string StatusMessage public string StatusMessage
{ {
get; set; get; set;
} }
public List<StoreViewModel> Stores public List<StoreViewModel> Stores
{ {
get; set; get; set;
} = new List<StoreViewModel>(); } = new List<StoreViewModel>();
public class StoreViewModel public class StoreViewModel
{ {
public string Name public string Name
{ {
get; set; get; set;
} }
public string WebSite public string WebSite
{ {
get; set; get; set;
} }
public string Id public string Id
{ {
get; set; get; set;
} }
public Money Balance public Money Balance
{ {
get; set; get; set;
} }
} }
} }
} }

View File

@@ -8,65 +8,65 @@ using System.Threading.Tasks;
namespace BTCPayServer.Models.StoreViewModels namespace BTCPayServer.Models.StoreViewModels
{ {
public class CreateTokenViewModel public class CreateTokenViewModel
{
[PubKeyValidatorAttribute]
public string PublicKey
{
get; set;
}
public string Label
{
get; set;
}
[Required]
public string Facade
{
get; set;
}
[Required]
public string StoreId
{
get; set;
}
public SelectList Stores
{
get; set;
}
}
public class TokenViewModel
{
public string Id
{
get; set;
}
public string Label
{
get; set;
}
public string SIN
{
get; set;
}
public string Facade
{
get; set;
}
}
public class TokensViewModel
{ {
public TokenViewModel[] Tokens [PubKeyValidatorAttribute]
{ public string PublicKey
get; set; {
} get; set;
public string StatusMessage }
{
get; public string Label
set; {
} get; set;
} }
[Required]
public string Facade
{
get; set;
}
[Required]
public string StoreId
{
get; set;
}
public SelectList Stores
{
get; set;
}
}
public class TokenViewModel
{
public string Id
{
get; set;
}
public string Label
{
get; set;
}
public string SIN
{
get; set;
}
public string Facade
{
get; set;
}
}
public class TokensViewModel
{
public TokenViewModel[] Tokens
{
get; set;
}
public string StatusMessage
{
get;
set;
}
}
} }

View File

@@ -6,83 +6,83 @@ using NBitcoin;
namespace BTCPayServer.Models namespace BTCPayServer.Models
{ {
public class TokenRequest public class TokenRequest
{ {
[JsonProperty(PropertyName = "id")] [JsonProperty(PropertyName = "id")]
public string Id public string Id
{ {
get; set; get; set;
} }
[JsonProperty(PropertyName = "guid")] [JsonProperty(PropertyName = "guid")]
public string Guid public string Guid
{ {
get; set; get; set;
} }
[JsonProperty(PropertyName = "facade")] [JsonProperty(PropertyName = "facade")]
public string Facade public string Facade
{ {
get; set; get; set;
} }
[JsonProperty(PropertyName = "count")] [JsonProperty(PropertyName = "count")]
public int Count public int Count
{ {
get; set; get; set;
} }
[JsonProperty(PropertyName = "label")] [JsonProperty(PropertyName = "label")]
public string Label public string Label
{ {
get; set; get; set;
} }
[JsonProperty(PropertyName = "pairingCode")] [JsonProperty(PropertyName = "pairingCode")]
public string PairingCode public string PairingCode
{ {
get; set; get; set;
} }
} }
public class PairingCodeResponse public class PairingCodeResponse
{ {
[JsonProperty(PropertyName = "pairingCode")] [JsonProperty(PropertyName = "pairingCode")]
public string PairingCode public string PairingCode
{ {
get; set; get; set;
} }
[JsonProperty(PropertyName = "pairingExpiration")] [JsonProperty(PropertyName = "pairingExpiration")]
[JsonConverter(typeof(DateTimeJsonConverter))] [JsonConverter(typeof(DateTimeJsonConverter))]
public DateTimeOffset PairingExpiration public DateTimeOffset PairingExpiration
{ {
get; set; get; set;
} }
[JsonProperty(PropertyName = "dateCreated")] [JsonProperty(PropertyName = "dateCreated")]
[JsonConverter(typeof(DateTimeJsonConverter))] [JsonConverter(typeof(DateTimeJsonConverter))]
public DateTimeOffset DateCreated public DateTimeOffset DateCreated
{ {
get; set; get; set;
} }
[JsonProperty(PropertyName = "facade")] [JsonProperty(PropertyName = "facade")]
public string Facade public string Facade
{ {
get; get;
set; set;
} }
[JsonProperty(PropertyName = "token")] [JsonProperty(PropertyName = "token")]
public string Token public string Token
{ {
get; get;
set; set;
} }
[JsonProperty(PropertyName = "label")] [JsonProperty(PropertyName = "label")]
public string Label public string Label
{ {
get; get;
set; set;
} }
} }
} }

View File

@@ -18,60 +18,60 @@ using System.Threading;
namespace BTCPayServer namespace BTCPayServer
{ {
class Program class Program
{ {
static void Main(string[] args) static void Main(string[] args)
{ {
ServicePointManager.DefaultConnectionLimit = 100; ServicePointManager.DefaultConnectionLimit = 100;
IWebHost host = null; IWebHost host = null;
CustomConsoleLogProvider loggerProvider = new CustomConsoleLogProvider(); CustomConsoleLogProvider loggerProvider = new CustomConsoleLogProvider();
var loggerFactory = new LoggerFactory(); var loggerFactory = new LoggerFactory();
loggerFactory.AddProvider(loggerProvider); loggerFactory.AddProvider(loggerProvider);
var logger = loggerFactory.CreateLogger("Configuration"); var logger = loggerFactory.CreateLogger("Configuration");
try try
{ {
var conf = new DefaultConfiguration() { Logger = logger }.CreateConfiguration(args); var conf = new DefaultConfiguration() { Logger = logger }.CreateConfiguration(args);
if(conf == null) if (conf == null)
return; return;
host = new WebHostBuilder() host = new WebHostBuilder()
.UseKestrel() .UseKestrel()
.UseIISIntegration() .UseIISIntegration()
.UseContentRoot(Directory.GetCurrentDirectory()) .UseContentRoot(Directory.GetCurrentDirectory())
.UseConfiguration(conf) .UseConfiguration(conf)
.UseApplicationInsights() .UseApplicationInsights()
.ConfigureLogging(l => .ConfigureLogging(l =>
{ {
l.AddFilter("Microsoft", LogLevel.Error); l.AddFilter("Microsoft", LogLevel.Error);
l.AddProvider(new CustomConsoleLogProvider()); l.AddProvider(new CustomConsoleLogProvider());
}) })
.UseStartup<Startup>() .UseStartup<Startup>()
.Build(); .Build();
host.StartAsync().GetAwaiter().GetResult(); host.StartAsync().GetAwaiter().GetResult();
var urls = host.ServerFeatures.Get<IServerAddressesFeature>().Addresses; var urls = host.ServerFeatures.Get<IServerAddressesFeature>().Addresses;
foreach(var url in urls) foreach (var url in urls)
{ {
logger.LogInformation("Listening on " + url); logger.LogInformation("Listening on " + url);
} }
host.WaitForShutdown(); host.WaitForShutdown();
} }
catch(ConfigException ex) catch (ConfigException ex)
{ {
if(!string.IsNullOrEmpty(ex.Message)) if (!string.IsNullOrEmpty(ex.Message))
Logs.Configuration.LogError(ex.Message); Logs.Configuration.LogError(ex.Message);
} }
catch(Exception exception) catch (Exception exception)
{ {
logger.LogError("Exception thrown while running the server"); logger.LogError("Exception thrown while running the server");
logger.LogError(exception.ToString()); logger.LogError(exception.ToString());
} }
finally finally
{ {
if(host != null) if (host != null)
host.Dispose(); host.Dispose();
loggerProvider.Dispose(); loggerProvider.Dispose();
} }
} }
} }
} }

View File

@@ -7,6 +7,6 @@ namespace BTCPayServer
{ {
public class Roles public class Roles
{ {
public const string ServerAdmin = "ServerAdmin"; public const string ServerAdmin = "ServerAdmin";
} }
} }

View File

@@ -10,37 +10,37 @@ namespace BTCPayServer.Services
{ {
public class BTCPayServerEnvironment public class BTCPayServerEnvironment
{ {
public BTCPayServerEnvironment(IHostingEnvironment env) public BTCPayServerEnvironment(IHostingEnvironment env)
{ {
Version = typeof(BTCPayServerEnvironment).GetTypeInfo().Assembly.GetCustomAttribute<AssemblyFileVersionAttribute>().Version; Version = typeof(BTCPayServerEnvironment).GetTypeInfo().Assembly.GetCustomAttribute<AssemblyFileVersionAttribute>().Version;
#if DEBUG #if DEBUG
Build = "Debug"; Build = "Debug";
#else #else
Build = "Release"; Build = "Release";
#endif #endif
Environment = env; Environment = env;
} }
public IHostingEnvironment Environment public IHostingEnvironment Environment
{ {
get; set; get; set;
} }
public string Version public string Version
{ {
get; set; get; set;
} }
public string Build public string Build
{ {
get; set; get; set;
} }
public override string ToString() public override string ToString()
{ {
StringBuilder txt = new StringBuilder(); StringBuilder txt = new StringBuilder();
txt.Append($"@Copyright BTCPayServer v{Version}"); txt.Append($"@Copyright BTCPayServer v{Version}");
if(!Environment.IsProduction() || Build.Equals("Release", StringComparison.OrdinalIgnoreCase)) if (!Environment.IsProduction() || Build.Equals("Release", StringComparison.OrdinalIgnoreCase))
{ {
txt.Append($" Environment: {Environment.EnvironmentName} Build: {Build}"); txt.Append($" Environment: {Environment.EnvironmentName} Build: {Build}");
} }
return txt.ToString(); return txt.ToString();
} }
} }
} }

View File

@@ -6,21 +6,21 @@ using System.Threading.Tasks;
namespace BTCPayServer.Services.Fees namespace BTCPayServer.Services.Fees
{ {
public class FixedFeeProvider : IFeeProvider public class FixedFeeProvider : IFeeProvider
{ {
public FixedFeeProvider(FeeRate feeRate) public FixedFeeProvider(FeeRate feeRate)
{ {
FeeRate = feeRate; FeeRate = feeRate;
} }
public FeeRate FeeRate public FeeRate FeeRate
{ {
get; set; get; set;
} }
public Task<FeeRate> GetFeeRateAsync() public Task<FeeRate> GetFeeRateAsync()
{ {
return Task.FromResult(FeeRate); return Task.FromResult(FeeRate);
} }
} }
} }

View File

@@ -8,8 +8,8 @@ using System.Threading.Tasks;
namespace BTCPayServer.Services namespace BTCPayServer.Services
{ {
public interface IFeeProvider public interface IFeeProvider
{ {
Task<FeeRate> GetFeeRateAsync(); Task<FeeRate> GetFeeRateAsync();
} }
} }

View File

@@ -8,30 +8,30 @@ using System.Threading.Tasks;
namespace BTCPayServer.Services.Fees namespace BTCPayServer.Services.Fees
{ {
public class NBXplorerFeeProvider : IFeeProvider public class NBXplorerFeeProvider : IFeeProvider
{ {
public ExplorerClient ExplorerClient public ExplorerClient ExplorerClient
{ {
get; set; get; set;
} }
public FeeRate Fallback public FeeRate Fallback
{ {
get; set; get; set;
} }
public int BlockTarget public int BlockTarget
{ {
get; set; get; set;
} }
public async Task<FeeRate> GetFeeRateAsync() public async Task<FeeRate> GetFeeRateAsync()
{ {
try try
{ {
return (await ExplorerClient.GetFeeRateAsync(BlockTarget).ConfigureAwait(false)).FeeRate; return (await ExplorerClient.GetFeeRateAsync(BlockTarget).ConfigureAwait(false)).FeeRate;
} }
catch(NBXplorerException ex) when(ex.Error.HttpCode == 400 && ex.Error.Code == "fee-estimation-unavailable") catch (NBXplorerException ex) when (ex.Error.HttpCode == 400 && ex.Error.Code == "fee-estimation-unavailable")
{ {
return Fallback; return Fallback;
} }
} }
} }
} }

View File

@@ -12,328 +12,328 @@ using BTCPayServer.Data;
namespace BTCPayServer.Services.Invoices namespace BTCPayServer.Services.Invoices
{ {
public class BuyerInformation public class BuyerInformation
{ {
[JsonProperty(PropertyName = "buyerName")] [JsonProperty(PropertyName = "buyerName")]
public string BuyerName public string BuyerName
{ {
get; set; get; set;
} }
[JsonProperty(PropertyName = "buyerEmail")] [JsonProperty(PropertyName = "buyerEmail")]
public string BuyerEmail public string BuyerEmail
{ {
get; set; get; set;
} }
[JsonProperty(PropertyName = "buyerCountry")] [JsonProperty(PropertyName = "buyerCountry")]
public string BuyerCountry public string BuyerCountry
{ {
get; set; get; set;
} }
[JsonProperty(PropertyName = "buyerZip")] [JsonProperty(PropertyName = "buyerZip")]
public string BuyerZip public string BuyerZip
{ {
get; set; get; set;
} }
[JsonProperty(PropertyName = "buyerState")] [JsonProperty(PropertyName = "buyerState")]
public string BuyerState public string BuyerState
{ {
get; set; get; set;
} }
[JsonProperty(PropertyName = "buyerCity")] [JsonProperty(PropertyName = "buyerCity")]
public string BuyerCity public string BuyerCity
{ {
get; set; get; set;
} }
[JsonProperty(PropertyName = "buyerAddress2")] [JsonProperty(PropertyName = "buyerAddress2")]
public string BuyerAddress2 public string BuyerAddress2
{ {
get; set; get; set;
} }
[JsonProperty(PropertyName = "buyerAddress1")] [JsonProperty(PropertyName = "buyerAddress1")]
public string BuyerAddress1 public string BuyerAddress1
{ {
get; set; get; set;
} }
[JsonProperty(PropertyName = "buyerPhone")] [JsonProperty(PropertyName = "buyerPhone")]
public string BuyerPhone public string BuyerPhone
{ {
get; set; get; set;
} }
} }
public class ProductInformation public class ProductInformation
{ {
[JsonProperty(PropertyName = "itemDesc")] [JsonProperty(PropertyName = "itemDesc")]
public string ItemDesc public string ItemDesc
{ {
get; set; get; set;
} }
[JsonProperty(PropertyName = "itemCode")] [JsonProperty(PropertyName = "itemCode")]
public string ItemCode public string ItemCode
{ {
get; set; get; set;
} }
[JsonProperty(PropertyName = "physical")] [JsonProperty(PropertyName = "physical")]
public bool Physical public bool Physical
{ {
get; set; get; set;
} }
[JsonProperty(PropertyName = "price")] [JsonProperty(PropertyName = "price")]
public double Price public double Price
{ {
get; set; get; set;
} }
[JsonProperty(PropertyName = "currency")] [JsonProperty(PropertyName = "currency")]
public string Currency public string Currency
{ {
get; set; get; set;
} }
} }
public enum SpeedPolicy public enum SpeedPolicy
{ {
HighSpeed = 0, HighSpeed = 0,
MediumSpeed = 1, MediumSpeed = 1,
LowSpeed = 2 LowSpeed = 2
} }
public class InvoiceEntity public class InvoiceEntity
{ {
public string Id public string Id
{ {
get; set; get; set;
} }
public string StoreId public string StoreId
{ {
get; set; get; set;
} }
public int GetTxCount() public int GetTxCount()
{ {
return Calculate().TxCount; return Calculate().TxCount;
} }
public string OrderId public string OrderId
{ {
get; set; get; set;
} }
public Money GetTotalCryptoDue() public Money GetTotalCryptoDue()
{ {
return Calculate().TotalDue; return Calculate().TotalDue;
} }
private (Money TotalDue, Money Paid, int TxCount) Calculate() private (Money TotalDue, Money Paid, int TxCount) Calculate()
{ {
var totalDue = Money.Coins((decimal)(ProductInformation.Price / Rate)) + TxFee; var totalDue = Money.Coins((decimal)(ProductInformation.Price / Rate)) + TxFee;
var paid = Money.Zero; var paid = Money.Zero;
int txCount = 1; int txCount = 1;
var payments = var payments =
Payments Payments
.OrderByDescending(p => p.ReceivedTime) .OrderByDescending(p => p.ReceivedTime)
.Select(_ => .Select(_ =>
{ {
paid += _.Output.Value; paid += _.Output.Value;
return _; return _;
}) })
.TakeWhile(_ => .TakeWhile(_ =>
{ {
var paidEnough = totalDue <= paid; var paidEnough = totalDue <= paid;
if(!paidEnough) if (!paidEnough)
{ {
txCount++; txCount++;
totalDue += TxFee; totalDue += TxFee;
} }
return !paidEnough; return !paidEnough;
}) })
.ToArray(); .ToArray();
return (totalDue, paid, txCount); return (totalDue, paid, txCount);
} }
public Money GetTotalPaid() public Money GetTotalPaid()
{ {
return Calculate().Paid; return Calculate().Paid;
} }
public Money GetCryptoDue() public Money GetCryptoDue()
{ {
var o = Calculate(); var o = Calculate();
var v = o.TotalDue - o.Paid; var v = o.TotalDue - o.Paid;
return v < Money.Zero ? Money.Zero : v; return v < Money.Zero ? Money.Zero : v;
} }
public SpeedPolicy SpeedPolicy public SpeedPolicy SpeedPolicy
{ {
get; set; get; set;
} }
public double Rate public double Rate
{ {
get; set; get; set;
} }
public DateTimeOffset InvoiceTime public DateTimeOffset InvoiceTime
{ {
get; set; get; set;
} }
public DateTimeOffset ExpirationTime public DateTimeOffset ExpirationTime
{ {
get; set; get; set;
} }
public BitcoinAddress DepositAddress public BitcoinAddress DepositAddress
{ {
get; set; get; set;
} }
public ProductInformation ProductInformation public ProductInformation ProductInformation
{ {
get; set; get; set;
} }
public BuyerInformation BuyerInformation public BuyerInformation BuyerInformation
{ {
get; set; get; set;
} }
public string PosData public string PosData
{ {
get; get;
set; set;
} }
public string DerivationStrategy public string DerivationStrategy
{ {
get; get;
set; set;
} }
public string Status public string Status
{ {
get; get;
set; set;
} }
public string ExceptionStatus public string ExceptionStatus
{ {
get; set; get; set;
} }
public List<PaymentEntity> Payments public List<PaymentEntity> Payments
{ {
get; set; get; set;
} }
public bool Refundable public bool Refundable
{ {
get; get;
set; set;
} }
public string RefundMail public string RefundMail
{ {
get; get;
set; set;
} }
public string RedirectURL public string RedirectURL
{ {
get; get;
set; set;
} }
public Money TxFee public Money TxFee
{ {
get; get;
set; set;
} }
public bool FullNotifications public bool FullNotifications
{ {
get; get;
set; set;
} }
public string NotificationURL public string NotificationURL
{ {
get; get;
set; set;
} }
public string ServerUrl public string ServerUrl
{ {
get; get;
set; set;
} }
public DateTimeOffset? MonitoringExpiration public DateTimeOffset? MonitoringExpiration
{ {
get; get;
set; set;
} }
public HistoricalAddressInvoiceData[] HistoricalAddresses public HistoricalAddressInvoiceData[] HistoricalAddresses
{ {
get; get;
set; set;
} }
public bool IsExpired() public bool IsExpired()
{ {
return DateTimeOffset.UtcNow > ExpirationTime; return DateTimeOffset.UtcNow > ExpirationTime;
} }
public InvoiceResponse EntityToDTO() public InvoiceResponse EntityToDTO()
{ {
ServerUrl = ServerUrl ?? ""; ServerUrl = ServerUrl ?? "";
InvoiceResponse dto = new InvoiceResponse InvoiceResponse dto = new InvoiceResponse
{ {
Id = Id, Id = Id,
OrderId = OrderId, OrderId = OrderId,
PosData = PosData, PosData = PosData,
CurrentTime = DateTimeOffset.UtcNow, CurrentTime = DateTimeOffset.UtcNow,
InvoiceTime = InvoiceTime, InvoiceTime = InvoiceTime,
ExpirationTime = ExpirationTime, ExpirationTime = ExpirationTime,
BTCPrice = Money.Coins((decimal)(1.0 / Rate)).ToString(), BTCPrice = Money.Coins((decimal)(1.0 / Rate)).ToString(),
Status = Status, Status = Status,
Url = ServerUrl.WithTrailingSlash() + "invoice?id=" + Id, Url = ServerUrl.WithTrailingSlash() + "invoice?id=" + Id,
Currency = ProductInformation.Currency, Currency = ProductInformation.Currency,
Flags = new Flags() { Refundable = Refundable } Flags = new Flags() { Refundable = Refundable }
}; };
Populate(ProductInformation, dto); Populate(ProductInformation, dto);
Populate(BuyerInformation, dto); Populate(BuyerInformation, dto);
dto.ExRates = new Dictionary<string, double> dto.ExRates = new Dictionary<string, double>
{ {
{ ProductInformation.Currency, Rate } { ProductInformation.Currency, Rate }
}; };
dto.PaymentUrls = new InvoicePaymentUrls() dto.PaymentUrls = new InvoicePaymentUrls()
{ {
BIP72 = $"bitcoin:{DepositAddress}?amount={GetCryptoDue()}&r={ServerUrl.WithTrailingSlash() + ($"i/{Id}")}", BIP72 = $"bitcoin:{DepositAddress}?amount={GetCryptoDue()}&r={ServerUrl.WithTrailingSlash() + ($"i/{Id}")}",
BIP72b = $"bitcoin:?r={ServerUrl.WithTrailingSlash() + ($"i/{Id}")}", BIP72b = $"bitcoin:?r={ServerUrl.WithTrailingSlash() + ($"i/{Id}")}",
BIP73 = ServerUrl.WithTrailingSlash() + ($"i/{Id}"), BIP73 = ServerUrl.WithTrailingSlash() + ($"i/{Id}"),
BIP21 = $"bitcoin:{DepositAddress}?amount={GetCryptoDue()}", BIP21 = $"bitcoin:{DepositAddress}?amount={GetCryptoDue()}",
}; };
dto.BitcoinAddress = DepositAddress.ToString(); dto.BitcoinAddress = DepositAddress.ToString();
dto.Token = Encoders.Base58.EncodeData(RandomUtils.GetBytes(16)); //No idea what it is useful for dto.Token = Encoders.Base58.EncodeData(RandomUtils.GetBytes(16)); //No idea what it is useful for
dto.Guid = Guid.NewGuid().ToString(); dto.Guid = Guid.NewGuid().ToString();
var paid = Payments.Select(p => p.Output.Value).Sum(); var paid = Payments.Select(p => p.Output.Value).Sum();
dto.BTCPaid = paid.ToString(); dto.BTCPaid = paid.ToString();
dto.BTCDue = GetCryptoDue().ToString(); dto.BTCDue = GetCryptoDue().ToString();
dto.ExceptionStatus = ExceptionStatus == null ? new JValue(false) : new JValue(ExceptionStatus); dto.ExceptionStatus = ExceptionStatus == null ? new JValue(false) : new JValue(ExceptionStatus);
return dto; return dto;
} }
private void Populate<TFrom, TDest>(TFrom from, TDest dest) private void Populate<TFrom, TDest>(TFrom from, TDest dest)
{ {
var str = JsonConvert.SerializeObject(from); var str = JsonConvert.SerializeObject(from);
JsonConvert.PopulateObject(str, dest); JsonConvert.PopulateObject(str, dest);
} }
public Money GetNetworkFee() public Money GetNetworkFee()
{ {
var item = Calculate(); var item = Calculate();
return TxFee * item.TxCount; return TxFee * item.TxCount;
} }
} }
public class PaymentEntity public class PaymentEntity
{ {
public DateTimeOffset ReceivedTime public DateTimeOffset ReceivedTime
{ {
get; set; get; set;
} }
public OutPoint Outpoint public OutPoint Outpoint
{ {
get; set; get; set;
} }
public TxOut Output public TxOut Output
{ {
get; set; get; set;
} }
} }
} }

View File

@@ -18,109 +18,109 @@ using System.Collections.Concurrent;
namespace BTCPayServer.Services.Invoices namespace BTCPayServer.Services.Invoices
{ {
public class InvoiceNotificationManager public class InvoiceNotificationManager
{ {
public static HttpClient _Client = new HttpClient(); public static HttpClient _Client = new HttpClient();
public class ScheduledJob public class ScheduledJob
{ {
public int TryCount public int TryCount
{ {
get; set; get; set;
} }
public InvoiceEntity Invoice public InvoiceEntity Invoice
{ {
get; set; get; set;
} }
} }
public ILogger Logger public ILogger Logger
{ {
get; set; get; set;
} }
IBackgroundJobClient _JobClient; IBackgroundJobClient _JobClient;
public InvoiceNotificationManager( public InvoiceNotificationManager(
IBackgroundJobClient jobClient, IBackgroundJobClient jobClient,
ILogger<InvoiceNotificationManager> logger) ILogger<InvoiceNotificationManager> logger)
{ {
Logger = logger as ILogger ?? NullLogger.Instance; Logger = logger as ILogger ?? NullLogger.Instance;
_JobClient = jobClient; _JobClient = jobClient;
} }
public void Notify(InvoiceEntity invoice) public void Notify(InvoiceEntity invoice)
{ {
var invoiceStr = NBitcoin.JsonConverters.Serializer.ToString(new ScheduledJob() { TryCount = 0, Invoice = invoice }); var invoiceStr = NBitcoin.JsonConverters.Serializer.ToString(new ScheduledJob() { TryCount = 0, Invoice = invoice });
if(!string.IsNullOrEmpty(invoice.NotificationURL)) if (!string.IsNullOrEmpty(invoice.NotificationURL))
_JobClient.Schedule(() => NotifyHttp(invoiceStr), TimeSpan.Zero); _JobClient.Schedule(() => NotifyHttp(invoiceStr), TimeSpan.Zero);
} }
ConcurrentDictionary<string, string> _Executing = new ConcurrentDictionary<string, string>(); ConcurrentDictionary<string, string> _Executing = new ConcurrentDictionary<string, string>();
public async Task NotifyHttp(string invoiceData) public async Task NotifyHttp(string invoiceData)
{ {
var job = NBitcoin.JsonConverters.Serializer.ToObject<ScheduledJob>(invoiceData); var job = NBitcoin.JsonConverters.Serializer.ToObject<ScheduledJob>(invoiceData);
var jobId = GetHttpJobId(job.Invoice); var jobId = GetHttpJobId(job.Invoice);
if(!_Executing.TryAdd(jobId, jobId)) if (!_Executing.TryAdd(jobId, jobId))
return; //For some reason, Hangfire fire the job several time return; //For some reason, Hangfire fire the job several time
Logger.LogInformation("Running " + jobId); Logger.LogInformation("Running " + jobId);
bool reschedule = false; bool reschedule = false;
CancellationTokenSource cts = new CancellationTokenSource(10000); CancellationTokenSource cts = new CancellationTokenSource(10000);
try try
{ {
var request = new HttpRequestMessage(); var request = new HttpRequestMessage();
request.Method = HttpMethod.Post; request.Method = HttpMethod.Post;
var dto = job.Invoice.EntityToDTO(); var dto = job.Invoice.EntityToDTO();
InvoicePaymentNotification notification = new InvoicePaymentNotification() InvoicePaymentNotification notification = new InvoicePaymentNotification()
{ {
Id = dto.Id, Id = dto.Id,
Url = dto.Url, Url = dto.Url,
BTCDue = dto.BTCDue, BTCDue = dto.BTCDue,
BTCPaid = dto.BTCPaid, BTCPaid = dto.BTCPaid,
BTCPrice = dto.BTCPrice, BTCPrice = dto.BTCPrice,
Currency = dto.Currency, Currency = dto.Currency,
CurrentTime = dto.CurrentTime, CurrentTime = dto.CurrentTime,
ExceptionStatus = dto.ExceptionStatus, ExceptionStatus = dto.ExceptionStatus,
ExpirationTime = dto.ExpirationTime, ExpirationTime = dto.ExpirationTime,
InvoiceTime = dto.InvoiceTime, InvoiceTime = dto.InvoiceTime,
PosData = dto.PosData, PosData = dto.PosData,
Price = dto.Price, Price = dto.Price,
Rate = dto.Rate, Rate = dto.Rate,
Status = dto.Status, Status = dto.Status,
BuyerFields = job.Invoice.RefundMail == null ? null : new Newtonsoft.Json.Linq.JObject() { new JProperty("buyerEmail", job.Invoice.RefundMail) } BuyerFields = job.Invoice.RefundMail == null ? null : new Newtonsoft.Json.Linq.JObject() { new JProperty("buyerEmail", job.Invoice.RefundMail) }
}; };
request.RequestUri = new Uri(job.Invoice.NotificationURL, UriKind.Absolute); request.RequestUri = new Uri(job.Invoice.NotificationURL, UriKind.Absolute);
request.Content = new StringContent(JsonConvert.SerializeObject(notification), Encoding.UTF8, "application/json"); request.Content = new StringContent(JsonConvert.SerializeObject(notification), Encoding.UTF8, "application/json");
var response = await _Client.SendAsync(request, cts.Token); var response = await _Client.SendAsync(request, cts.Token);
reschedule = response.StatusCode != System.Net.HttpStatusCode.OK; reschedule = response.StatusCode != System.Net.HttpStatusCode.OK;
Logger.LogInformation("Job " + jobId + " returned " + response.StatusCode); Logger.LogInformation("Job " + jobId + " returned " + response.StatusCode);
} }
catch(Exception ex) catch (Exception ex)
{ {
reschedule = true; reschedule = true;
Logger.LogInformation("Job " + jobId + " threw exception " + ex.Message); Logger.LogInformation("Job " + jobId + " threw exception " + ex.Message);
} }
finally { cts.Dispose(); _Executing.TryRemove(jobId, out jobId); } finally { cts.Dispose(); _Executing.TryRemove(jobId, out jobId); }
job.TryCount++; job.TryCount++;
if(job.TryCount < MaxTry && reschedule) if (job.TryCount < MaxTry && reschedule)
{ {
Logger.LogInformation("Rescheduling " + jobId + " in 10 minutes, remaining try " + (MaxTry - job.TryCount)); Logger.LogInformation("Rescheduling " + jobId + " in 10 minutes, remaining try " + (MaxTry - job.TryCount));
invoiceData = NBitcoin.JsonConverters.Serializer.ToString(job); invoiceData = NBitcoin.JsonConverters.Serializer.ToString(job);
_JobClient.Schedule(() => NotifyHttp(invoiceData), TimeSpan.FromMinutes(10.0)); _JobClient.Schedule(() => NotifyHttp(invoiceData), TimeSpan.FromMinutes(10.0));
} }
} }
int MaxTry = 6; int MaxTry = 6;
private static string GetHttpJobId(InvoiceEntity invoice) private static string GetHttpJobId(InvoiceEntity invoice)
{ {
return $"{invoice.Id}-{invoice.Status}-HTTP"; return $"{invoice.Id}-{invoice.Status}-HTTP";
} }
} }
} }

View File

@@ -19,464 +19,464 @@ using BTCPayServer.Models.InvoicingModels;
namespace BTCPayServer.Services.Invoices namespace BTCPayServer.Services.Invoices
{ {
public class InvoiceRepository public class InvoiceRepository
{ {
private readonly DBreezeEngine _Engine; private readonly DBreezeEngine _Engine;
public DBreezeEngine Engine public DBreezeEngine Engine
{ {
get get
{ {
return _Engine; return _Engine;
} }
} }
Network _Network; Network _Network;
public Network Network public Network Network
{ {
get get
{ {
return _Network; return _Network;
} }
set set
{ {
_Network = value; _Network = value;
} }
} }
private ApplicationDbContextFactory _ContextFactory; private ApplicationDbContextFactory _ContextFactory;
public InvoiceRepository(ApplicationDbContextFactory contextFactory, DBreezeEngine engine, Network network) public InvoiceRepository(ApplicationDbContextFactory contextFactory, DBreezeEngine engine, Network network)
{ {
_Engine = engine; _Engine = engine;
_Network = network; _Network = network;
_ContextFactory = contextFactory; _ContextFactory = contextFactory;
} }
public async Task AddPendingInvoice(string invoiceId) public async Task AddPendingInvoice(string invoiceId)
{ {
using(var ctx = _ContextFactory.CreateContext()) using (var ctx = _ContextFactory.CreateContext())
{ {
ctx.PendingInvoices.Add(new PendingInvoiceData() { Id = invoiceId }); ctx.PendingInvoices.Add(new PendingInvoiceData() { Id = invoiceId });
await ctx.SaveChangesAsync(); await ctx.SaveChangesAsync();
} }
} }
public async Task<bool> RemovePendingInvoice(string invoiceId) public async Task<bool> RemovePendingInvoice(string invoiceId)
{ {
using(var ctx = _ContextFactory.CreateContext()) using (var ctx = _ContextFactory.CreateContext())
{ {
ctx.PendingInvoices.Remove(new PendingInvoiceData() { Id = invoiceId }); ctx.PendingInvoices.Remove(new PendingInvoiceData() { Id = invoiceId });
try try
{ {
await ctx.SaveChangesAsync(); await ctx.SaveChangesAsync();
return true; return true;
} }
catch(DbUpdateException) { return false; } catch (DbUpdateException) { return false; }
} }
} }
public async Task<string> GetInvoiceIdFromScriptPubKey(Script scriptPubKey) public async Task<string> GetInvoiceIdFromScriptPubKey(Script scriptPubKey)
{ {
using(var db = _ContextFactory.CreateContext()) using (var db = _ContextFactory.CreateContext())
{ {
var result = await db.AddressInvoices.FindAsync(scriptPubKey.Hash.ToString()); var result = await db.AddressInvoices.FindAsync(scriptPubKey.Hash.ToString());
return result?.InvoiceDataId; return result?.InvoiceDataId;
} }
} }
public async Task<string[]> GetPendingInvoices() public async Task<string[]> GetPendingInvoices()
{ {
using(var ctx = _ContextFactory.CreateContext()) using (var ctx = _ContextFactory.CreateContext())
{ {
return await ctx.PendingInvoices.Select(p => p.Id).ToArrayAsync(); return await ctx.PendingInvoices.Select(p => p.Id).ToArrayAsync();
} }
} }
public async Task UpdateInvoice(string invoiceId, UpdateCustomerModel data) public async Task UpdateInvoice(string invoiceId, UpdateCustomerModel data)
{ {
using(var ctx = _ContextFactory.CreateContext()) using (var ctx = _ContextFactory.CreateContext())
{ {
var invoiceData = await ctx.Invoices.FindAsync(invoiceId).ConfigureAwait(false); var invoiceData = await ctx.Invoices.FindAsync(invoiceId).ConfigureAwait(false);
if(invoiceData == null) if (invoiceData == null)
return; return;
if(invoiceData.CustomerEmail == null && data.Email != null) if (invoiceData.CustomerEmail == null && data.Email != null)
{ {
invoiceData.CustomerEmail = data.Email; invoiceData.CustomerEmail = data.Email;
} }
await ctx.SaveChangesAsync().ConfigureAwait(false); await ctx.SaveChangesAsync().ConfigureAwait(false);
} }
} }
public async Task<InvoiceEntity> CreateInvoiceAsync(string storeId, InvoiceEntity invoice) public async Task<InvoiceEntity> CreateInvoiceAsync(string storeId, InvoiceEntity invoice)
{ {
invoice = Clone(invoice); invoice = Clone(invoice);
invoice.Id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(16)); invoice.Id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(16));
invoice.Payments = new List<PaymentEntity>(); invoice.Payments = new List<PaymentEntity>();
invoice.StoreId = storeId; invoice.StoreId = storeId;
using(var context = _ContextFactory.CreateContext()) using (var context = _ContextFactory.CreateContext())
{ {
await context.AddAsync(new InvoiceData() await context.AddAsync(new InvoiceData()
{ {
StoreDataId = storeId, StoreDataId = storeId,
Id = invoice.Id, Id = invoice.Id,
Created = invoice.InvoiceTime, Created = invoice.InvoiceTime,
Blob = ToBytes(invoice), Blob = ToBytes(invoice),
OrderId = invoice.OrderId, OrderId = invoice.OrderId,
Status = invoice.Status, Status = invoice.Status,
ItemCode = invoice.ProductInformation.ItemCode, ItemCode = invoice.ProductInformation.ItemCode,
CustomerEmail = invoice.RefundMail CustomerEmail = invoice.RefundMail
}).ConfigureAwait(false); }).ConfigureAwait(false);
context.AddressInvoices.Add(new AddressInvoiceData() context.AddressInvoices.Add(new AddressInvoiceData()
{ {
Address = invoice.DepositAddress.ScriptPubKey.Hash.ToString(), Address = invoice.DepositAddress.ScriptPubKey.Hash.ToString(),
InvoiceDataId = invoice.Id, InvoiceDataId = invoice.Id,
CreatedTime = DateTimeOffset.UtcNow, CreatedTime = DateTimeOffset.UtcNow,
}); });
context.HistoricalAddressInvoices.Add(new HistoricalAddressInvoiceData() context.HistoricalAddressInvoices.Add(new HistoricalAddressInvoiceData()
{ {
InvoiceDataId = invoice.Id, InvoiceDataId = invoice.Id,
Address = invoice.DepositAddress.ToString(), Address = invoice.DepositAddress.ToString(),
Assigned = DateTimeOffset.UtcNow Assigned = DateTimeOffset.UtcNow
}); });
await context.SaveChangesAsync().ConfigureAwait(false); await context.SaveChangesAsync().ConfigureAwait(false);
} }
AddToTextSearch(invoice.Id, AddToTextSearch(invoice.Id,
invoice.Id, invoice.Id,
invoice.DepositAddress.ToString(), invoice.DepositAddress.ToString(),
invoice.InvoiceTime.ToString(CultureInfo.InvariantCulture), invoice.InvoiceTime.ToString(CultureInfo.InvariantCulture),
invoice.ProductInformation.Price.ToString(CultureInfo.InvariantCulture), invoice.ProductInformation.Price.ToString(CultureInfo.InvariantCulture),
invoice.GetTotalCryptoDue().ToString(), invoice.GetTotalCryptoDue().ToString(),
invoice.OrderId, invoice.OrderId,
ToString(invoice.BuyerInformation), ToString(invoice.BuyerInformation),
ToString(invoice.ProductInformation), ToString(invoice.ProductInformation),
invoice.StoreId invoice.StoreId
); );
return invoice; return invoice;
} }
public async Task<bool> NewAddress(string invoiceId, BitcoinAddress bitcoinAddress) public async Task<bool> NewAddress(string invoiceId, BitcoinAddress bitcoinAddress)
{ {
using(var context = _ContextFactory.CreateContext()) using (var context = _ContextFactory.CreateContext())
{ {
var invoice = await context.Invoices.FirstOrDefaultAsync(i => i.Id == invoiceId); var invoice = await context.Invoices.FirstOrDefaultAsync(i => i.Id == invoiceId);
if(invoice == null) if (invoice == null)
return false; return false;
var invoiceEntity = ToObject<InvoiceEntity>(invoice.Blob); var invoiceEntity = ToObject<InvoiceEntity>(invoice.Blob);
var old = invoiceEntity.DepositAddress; var old = invoiceEntity.DepositAddress;
invoiceEntity.DepositAddress = bitcoinAddress; invoiceEntity.DepositAddress = bitcoinAddress;
invoice.Blob = ToBytes(invoiceEntity); invoice.Blob = ToBytes(invoiceEntity);
if(old != null) if (old != null)
{ {
MarkUnassigned(invoiceId, old, context); MarkUnassigned(invoiceId, old, context);
} }
context.AddressInvoices.Add(new AddressInvoiceData() { Address = bitcoinAddress.ScriptPubKey.Hash.ToString(), InvoiceDataId = invoiceId, CreatedTime = DateTimeOffset.UtcNow }); context.AddressInvoices.Add(new AddressInvoiceData() { Address = bitcoinAddress.ScriptPubKey.Hash.ToString(), InvoiceDataId = invoiceId, CreatedTime = DateTimeOffset.UtcNow });
context.HistoricalAddressInvoices.Add(new HistoricalAddressInvoiceData() context.HistoricalAddressInvoices.Add(new HistoricalAddressInvoiceData()
{ {
InvoiceDataId = invoiceId, InvoiceDataId = invoiceId,
Address = bitcoinAddress.ToString(), Address = bitcoinAddress.ToString(),
Assigned = DateTimeOffset.UtcNow Assigned = DateTimeOffset.UtcNow
}); });
await context.SaveChangesAsync(); await context.SaveChangesAsync();
AddToTextSearch(invoice.Id, bitcoinAddress.ToString()); AddToTextSearch(invoice.Id, bitcoinAddress.ToString());
return true; return true;
} }
} }
private static void MarkUnassigned(string invoiceId, BitcoinAddress old, ApplicationDbContext context) private static void MarkUnassigned(string invoiceId, BitcoinAddress old, ApplicationDbContext context)
{ {
var historical = new HistoricalAddressInvoiceData(); var historical = new HistoricalAddressInvoiceData();
historical.InvoiceDataId = invoiceId; historical.InvoiceDataId = invoiceId;
historical.Address = old.ToString(); historical.Address = old.ToString();
historical.UnAssigned = DateTimeOffset.UtcNow; historical.UnAssigned = DateTimeOffset.UtcNow;
context.Attach(historical); context.Attach(historical);
context.Entry(historical).Property(o => o.UnAssigned).IsModified = true; context.Entry(historical).Property(o => o.UnAssigned).IsModified = true;
} }
public async Task UnaffectAddress(string invoiceId) public async Task UnaffectAddress(string invoiceId)
{ {
using(var context = _ContextFactory.CreateContext()) using (var context = _ContextFactory.CreateContext())
{ {
var invoiceData = await context.FindAsync<InvoiceData>(invoiceId).ConfigureAwait(false); var invoiceData = await context.FindAsync<InvoiceData>(invoiceId).ConfigureAwait(false);
if(invoiceData == null) if (invoiceData == null)
return; return;
var invoiceEntity = ToObject<InvoiceEntity>(invoiceData.Blob); var invoiceEntity = ToObject<InvoiceEntity>(invoiceData.Blob);
if(invoiceEntity.DepositAddress == null) if (invoiceEntity.DepositAddress == null)
return; return;
MarkUnassigned(invoiceId, invoiceEntity.DepositAddress, context); MarkUnassigned(invoiceId, invoiceEntity.DepositAddress, context);
try try
{ {
await context.SaveChangesAsync(); await context.SaveChangesAsync();
} }
catch(DbUpdateException) { } //Possibly, it was unassigned before catch (DbUpdateException) { } //Possibly, it was unassigned before
} }
} }
private string[] SearchInvoice(string searchTerms) private string[] SearchInvoice(string searchTerms)
{ {
using(var tx = _Engine.GetTransaction()) using (var tx = _Engine.GetTransaction())
{ {
return tx.TextSearch("InvoiceSearch").Block(searchTerms) return tx.TextSearch("InvoiceSearch").Block(searchTerms)
.GetDocumentIDs() .GetDocumentIDs()
.Select(id => Encoders.Base58.EncodeData(id)) .Select(id => Encoders.Base58.EncodeData(id))
.ToArray(); .ToArray();
} }
} }
void AddToTextSearch(string invoiceId, params string[] terms) void AddToTextSearch(string invoiceId, params string[] terms)
{ {
using(var tx = _Engine.GetTransaction()) using (var tx = _Engine.GetTransaction())
{ {
tx.TextInsert("InvoiceSearch", Encoders.Base58.DecodeData(invoiceId), string.Join(" ", terms.Where(t => !String.IsNullOrWhiteSpace(t)))); tx.TextInsert("InvoiceSearch", Encoders.Base58.DecodeData(invoiceId), string.Join(" ", terms.Where(t => !String.IsNullOrWhiteSpace(t))));
tx.Commit(); tx.Commit();
} }
} }
public async Task UpdateInvoiceStatus(string invoiceId, string status, string exceptionStatus) public async Task UpdateInvoiceStatus(string invoiceId, string status, string exceptionStatus)
{ {
using(var context = _ContextFactory.CreateContext()) using (var context = _ContextFactory.CreateContext())
{ {
var invoiceData = await context.FindAsync<InvoiceData>(invoiceId).ConfigureAwait(false); var invoiceData = await context.FindAsync<InvoiceData>(invoiceId).ConfigureAwait(false);
if(invoiceData == null) if (invoiceData == null)
return; return;
invoiceData.Status = status; invoiceData.Status = status;
invoiceData.ExceptionStatus = exceptionStatus; invoiceData.ExceptionStatus = exceptionStatus;
await context.SaveChangesAsync().ConfigureAwait(false); await context.SaveChangesAsync().ConfigureAwait(false);
} }
} }
public async Task<InvoiceEntity> GetInvoice(string storeId, string id, bool includeHistoricalAddresses = false) public async Task<InvoiceEntity> GetInvoice(string storeId, string id, bool includeHistoricalAddresses = false)
{ {
using(var context = _ContextFactory.CreateContext()) using (var context = _ContextFactory.CreateContext())
{ {
IQueryable<InvoiceData> query = IQueryable<InvoiceData> query =
context context
.Invoices .Invoices
.Include(o => o.Payments) .Include(o => o.Payments)
.Include(o => o.RefundAddresses); .Include(o => o.RefundAddresses);
if(includeHistoricalAddresses) if (includeHistoricalAddresses)
query = query.Include(o => o.HistoricalAddressInvoices); query = query.Include(o => o.HistoricalAddressInvoices);
query = query.Where(i => i.Id == id); query = query.Where(i => i.Id == id);
if(storeId != null) if (storeId != null)
query = query.Where(i => i.StoreDataId == storeId); query = query.Where(i => i.StoreDataId == storeId);
var invoice = await query.FirstOrDefaultAsync().ConfigureAwait(false); var invoice = await query.FirstOrDefaultAsync().ConfigureAwait(false);
if(invoice == null) if (invoice == null)
return null; return null;
return ToEntity(invoice); return ToEntity(invoice);
} }
} }
private InvoiceEntity ToEntity(InvoiceData invoice) private InvoiceEntity ToEntity(InvoiceData invoice)
{ {
var entity = ToObject<InvoiceEntity>(invoice.Blob); var entity = ToObject<InvoiceEntity>(invoice.Blob);
entity.Payments = invoice.Payments.Select(p => ToObject<PaymentEntity>(p.Blob)).ToList(); entity.Payments = invoice.Payments.Select(p => ToObject<PaymentEntity>(p.Blob)).ToList();
entity.ExceptionStatus = invoice.ExceptionStatus; entity.ExceptionStatus = invoice.ExceptionStatus;
entity.Status = invoice.Status; entity.Status = invoice.Status;
entity.RefundMail = invoice.CustomerEmail; entity.RefundMail = invoice.CustomerEmail;
entity.Refundable = invoice.RefundAddresses.Count != 0; entity.Refundable = invoice.RefundAddresses.Count != 0;
if(invoice.HistoricalAddressInvoices != null) if (invoice.HistoricalAddressInvoices != null)
{ {
entity.HistoricalAddresses = invoice.HistoricalAddressInvoices.ToArray(); entity.HistoricalAddresses = invoice.HistoricalAddressInvoices.ToArray();
} }
return entity; return entity;
} }
public async Task<InvoiceEntity[]> GetInvoices(InvoiceQuery queryObject) public async Task<InvoiceEntity[]> GetInvoices(InvoiceQuery queryObject)
{ {
using(var context = _ContextFactory.CreateContext()) using (var context = _ContextFactory.CreateContext())
{ {
IQueryable<InvoiceData> query = context IQueryable<InvoiceData> query = context
.Invoices .Invoices
.Include(o => o.Payments) .Include(o => o.Payments)
.Include(o => o.RefundAddresses); .Include(o => o.RefundAddresses);
if(!string.IsNullOrEmpty(queryObject.InvoiceId)) if (!string.IsNullOrEmpty(queryObject.InvoiceId))
{ {
query = query.Where(i => i.Id == queryObject.InvoiceId); query = query.Where(i => i.Id == queryObject.InvoiceId);
} }
if(!string.IsNullOrEmpty(queryObject.StoreId)) if (!string.IsNullOrEmpty(queryObject.StoreId))
{ {
query = query.Where(i => i.StoreDataId == queryObject.StoreId); query = query.Where(i => i.StoreDataId == queryObject.StoreId);
} }
if(queryObject.UserId != null) if (queryObject.UserId != null)
{ {
query = query.Where(i => i.StoreData.UserStores.Any(u => u.ApplicationUserId == queryObject.UserId)); query = query.Where(i => i.StoreData.UserStores.Any(u => u.ApplicationUserId == queryObject.UserId));
} }
if(!string.IsNullOrEmpty(queryObject.TextSearch)) if (!string.IsNullOrEmpty(queryObject.TextSearch))
{ {
var ids = new HashSet<string>(SearchInvoice(queryObject.TextSearch)); var ids = new HashSet<string>(SearchInvoice(queryObject.TextSearch));
if(ids.Count == 0) if (ids.Count == 0)
return new InvoiceEntity[0]; return new InvoiceEntity[0];
query = query.Where(i => ids.Contains(i.Id)); query = query.Where(i => ids.Contains(i.Id));
} }
if(queryObject.StartDate != null) if (queryObject.StartDate != null)
query = query.Where(i => queryObject.StartDate.Value <= i.Created); query = query.Where(i => queryObject.StartDate.Value <= i.Created);
if(queryObject.EndDate != null) if (queryObject.EndDate != null)
query = query.Where(i => i.Created <= queryObject.EndDate.Value); query = query.Where(i => i.Created <= queryObject.EndDate.Value);
if(queryObject.ItemCode != null) if (queryObject.ItemCode != null)
query = query.Where(i => i.ItemCode == queryObject.ItemCode); query = query.Where(i => i.ItemCode == queryObject.ItemCode);
if(queryObject.OrderId != null) if (queryObject.OrderId != null)
query = query.Where(i => i.OrderId == queryObject.OrderId); query = query.Where(i => i.OrderId == queryObject.OrderId);
if(queryObject.Status != null) if (queryObject.Status != null)
query = query.Where(i => i.Status == queryObject.Status); query = query.Where(i => i.Status == queryObject.Status);
query = query.OrderByDescending(q => q.Created); query = query.OrderByDescending(q => q.Created);
if(queryObject.Skip != null) if (queryObject.Skip != null)
query = query.Skip(queryObject.Skip.Value); query = query.Skip(queryObject.Skip.Value);
if(queryObject.Count != null) if (queryObject.Count != null)
query = query.Take(queryObject.Count.Value); query = query.Take(queryObject.Count.Value);
var data = await query.ToArrayAsync().ConfigureAwait(false); var data = await query.ToArrayAsync().ConfigureAwait(false);
return data.Select(ToEntity).ToArray(); return data.Select(ToEntity).ToArray();
} }
} }
public async Task AddRefundsAsync(string invoiceId, TxOut[] outputs) public async Task AddRefundsAsync(string invoiceId, TxOut[] outputs)
{ {
if(outputs.Length == 0) if (outputs.Length == 0)
return; return;
outputs = outputs.Take(10).ToArray(); outputs = outputs.Take(10).ToArray();
using(var context = _ContextFactory.CreateContext()) using (var context = _ContextFactory.CreateContext())
{ {
int i = 0; int i = 0;
foreach(var output in outputs) foreach (var output in outputs)
{ {
await context.RefundAddresses.AddAsync(new RefundAddressesData() await context.RefundAddresses.AddAsync(new RefundAddressesData()
{ {
Id = invoiceId + "-" + i, Id = invoiceId + "-" + i,
InvoiceDataId = invoiceId, InvoiceDataId = invoiceId,
Blob = ToBytes(output) Blob = ToBytes(output)
}).ConfigureAwait(false); }).ConfigureAwait(false);
i++; i++;
} }
await context.SaveChangesAsync().ConfigureAwait(false); await context.SaveChangesAsync().ConfigureAwait(false);
} }
var addresses = outputs.Select(o => o.ScriptPubKey.GetDestinationAddress(_Network)).Where(a => a != null).ToArray(); var addresses = outputs.Select(o => o.ScriptPubKey.GetDestinationAddress(_Network)).Where(a => a != null).ToArray();
AddToTextSearch(invoiceId, addresses.Select(a => a.ToString()).ToArray()); AddToTextSearch(invoiceId, addresses.Select(a => a.ToString()).ToArray());
} }
public async Task<PaymentEntity> AddPayment(string invoiceId, Coin receivedCoin) public async Task<PaymentEntity> AddPayment(string invoiceId, Coin receivedCoin)
{ {
using(var context = _ContextFactory.CreateContext()) using (var context = _ContextFactory.CreateContext())
{ {
PaymentEntity entity = new PaymentEntity PaymentEntity entity = new PaymentEntity
{ {
Outpoint = receivedCoin.Outpoint, Outpoint = receivedCoin.Outpoint,
Output = receivedCoin.TxOut, Output = receivedCoin.TxOut,
ReceivedTime = DateTime.UtcNow ReceivedTime = DateTime.UtcNow
}; };
PaymentData data = new PaymentData PaymentData data = new PaymentData
{ {
Id = receivedCoin.Outpoint.ToString(), Id = receivedCoin.Outpoint.ToString(),
Blob = ToBytes(entity), Blob = ToBytes(entity),
InvoiceDataId = invoiceId InvoiceDataId = invoiceId
}; };
await context.Payments.AddAsync(data).ConfigureAwait(false); await context.Payments.AddAsync(data).ConfigureAwait(false);
await context.SaveChangesAsync().ConfigureAwait(false); await context.SaveChangesAsync().ConfigureAwait(false);
return entity; return entity;
} }
} }
private T ToObject<T>(byte[] value) private T ToObject<T>(byte[] value)
{ {
return NBitcoin.JsonConverters.Serializer.ToObject<T>(ZipUtils.Unzip(value), Network); return NBitcoin.JsonConverters.Serializer.ToObject<T>(ZipUtils.Unzip(value), Network);
} }
private byte[] ToBytes<T>(T obj) private byte[] ToBytes<T>(T obj)
{ {
return ZipUtils.Zip(NBitcoin.JsonConverters.Serializer.ToString(obj)); return ZipUtils.Zip(NBitcoin.JsonConverters.Serializer.ToString(obj));
} }
private T Clone<T>(T invoice) private T Clone<T>(T invoice)
{ {
return NBitcoin.JsonConverters.Serializer.ToObject<T>(ToString(invoice), Network); return NBitcoin.JsonConverters.Serializer.ToObject<T>(ToString(invoice), Network);
} }
private string ToString<T>(T data) private string ToString<T>(T data)
{ {
return NBitcoin.JsonConverters.Serializer.ToString(data, Network); return NBitcoin.JsonConverters.Serializer.ToString(data, Network);
} }
} }
public class InvoiceQuery public class InvoiceQuery
{ {
public string StoreId public string StoreId
{ {
get; set; get; set;
} }
public string UserId public string UserId
{ {
get; set; get; set;
} }
public string TextSearch public string TextSearch
{ {
get; set; get; set;
} }
public DateTimeOffset? StartDate public DateTimeOffset? StartDate
{ {
get; set; get; set;
} }
public DateTimeOffset? EndDate public DateTimeOffset? EndDate
{ {
get; set; get; set;
} }
public int? Skip public int? Skip
{ {
get; set; get; set;
} }
public int? Count public int? Count
{ {
get; set; get; set;
} }
public string OrderId public string OrderId
{ {
get; set; get; set;
} }
public string ItemCode public string ItemCode
{ {
get; set; get; set;
} }
public string Status public string Status
{ {
get; set; get; set;
} }
public string InvoiceId public string InvoiceId
{ {
get; get;
set; set;
} }
} }
} }

View File

@@ -16,342 +16,342 @@ using BTCPayServer.Services.Wallets;
namespace BTCPayServer.Services.Invoices namespace BTCPayServer.Services.Invoices
{ {
public class InvoiceWatcher : IHostedService public class InvoiceWatcher : IHostedService
{ {
InvoiceRepository _InvoiceRepository; InvoiceRepository _InvoiceRepository;
ExplorerClient _ExplorerClient; ExplorerClient _ExplorerClient;
DerivationStrategyFactory _DerivationFactory; DerivationStrategyFactory _DerivationFactory;
InvoiceNotificationManager _NotificationManager; InvoiceNotificationManager _NotificationManager;
BTCPayWallet _Wallet; BTCPayWallet _Wallet;
public InvoiceWatcher(ExplorerClient explorerClient, public InvoiceWatcher(ExplorerClient explorerClient,
InvoiceRepository invoiceRepository, InvoiceRepository invoiceRepository,
BTCPayWallet wallet, BTCPayWallet wallet,
InvoiceNotificationManager notificationManager) InvoiceNotificationManager notificationManager)
{ {
LongPollingMode = explorerClient.Network == Network.RegTest; LongPollingMode = explorerClient.Network == Network.RegTest;
PollInterval = explorerClient.Network == Network.RegTest ? TimeSpan.FromSeconds(10.0) : TimeSpan.FromMinutes(1.0); PollInterval = explorerClient.Network == Network.RegTest ? TimeSpan.FromSeconds(10.0) : TimeSpan.FromMinutes(1.0);
_Wallet = wallet ?? throw new ArgumentNullException(nameof(wallet)); _Wallet = wallet ?? throw new ArgumentNullException(nameof(wallet));
_ExplorerClient = explorerClient ?? throw new ArgumentNullException(nameof(explorerClient)); _ExplorerClient = explorerClient ?? throw new ArgumentNullException(nameof(explorerClient));
_DerivationFactory = new DerivationStrategyFactory(_ExplorerClient.Network); _DerivationFactory = new DerivationStrategyFactory(_ExplorerClient.Network);
_InvoiceRepository = invoiceRepository ?? throw new ArgumentNullException(nameof(invoiceRepository)); _InvoiceRepository = invoiceRepository ?? throw new ArgumentNullException(nameof(invoiceRepository));
_NotificationManager = notificationManager ?? throw new ArgumentNullException(nameof(notificationManager)); _NotificationManager = notificationManager ?? throw new ArgumentNullException(nameof(notificationManager));
} }
public bool LongPollingMode public bool LongPollingMode
{ {
get; set; get; set;
} }
public async Task NotifyReceived(Script scriptPubKey) public async Task NotifyReceived(Script scriptPubKey)
{ {
var invoice = await _InvoiceRepository.GetInvoiceIdFromScriptPubKey(scriptPubKey); var invoice = await _InvoiceRepository.GetInvoiceIdFromScriptPubKey(scriptPubKey);
if(invoice != null) if (invoice != null)
_WatchRequests.Add(invoice); _WatchRequests.Add(invoice);
} }
public async Task NotifyBlock() public async Task NotifyBlock()
{ {
foreach(var invoice in await _InvoiceRepository.GetPendingInvoices()) foreach (var invoice in await _InvoiceRepository.GetPendingInvoices())
{ {
_WatchRequests.Add(invoice); _WatchRequests.Add(invoice);
} }
} }
private async Task UpdateInvoice(string invoiceId) private async Task UpdateInvoice(string invoiceId)
{ {
UTXOChanges changes = null; UTXOChanges changes = null;
while(true) while (true)
{ {
try try
{ {
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId).ConfigureAwait(false); var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId).ConfigureAwait(false);
if(invoice == null) if (invoice == null)
break; break;
var stateBefore = invoice.Status; var stateBefore = invoice.Status;
var result = await UpdateInvoice(changes, invoice).ConfigureAwait(false); var result = await UpdateInvoice(changes, invoice).ConfigureAwait(false);
changes = result.Changes; changes = result.Changes;
if(result.NeedSave) if (result.NeedSave)
await _InvoiceRepository.UpdateInvoiceStatus(invoice.Id, invoice.Status, invoice.ExceptionStatus).ConfigureAwait(false); await _InvoiceRepository.UpdateInvoiceStatus(invoice.Id, invoice.Status, invoice.ExceptionStatus).ConfigureAwait(false);
var changed = stateBefore != invoice.Status; var changed = stateBefore != invoice.Status;
if(changed) if (changed)
{ {
Logs.PayServer.LogInformation($"Invoice {invoice.Id}: {stateBefore} => {invoice.Status}"); Logs.PayServer.LogInformation($"Invoice {invoice.Id}: {stateBefore} => {invoice.Status}");
} }
var expirationMonitoring = invoice.MonitoringExpiration.HasValue ? invoice.MonitoringExpiration.Value : invoice.InvoiceTime + TimeSpan.FromMinutes(60); var expirationMonitoring = invoice.MonitoringExpiration.HasValue ? invoice.MonitoringExpiration.Value : invoice.InvoiceTime + TimeSpan.FromMinutes(60);
if(invoice.Status == "complete" || if (invoice.Status == "complete" ||
((invoice.Status == "invalid" || invoice.Status == "expired") && expirationMonitoring < DateTimeOffset.UtcNow)) ((invoice.Status == "invalid" || invoice.Status == "expired") && expirationMonitoring < DateTimeOffset.UtcNow))
{ {
if(await _InvoiceRepository.RemovePendingInvoice(invoice.Id).ConfigureAwait(false)) if (await _InvoiceRepository.RemovePendingInvoice(invoice.Id).ConfigureAwait(false))
Logs.PayServer.LogInformation("Stopped watching invoice " + invoiceId); Logs.PayServer.LogInformation("Stopped watching invoice " + invoiceId);
break; break;
} }
if(!changed || _Cts.Token.IsCancellationRequested) if (!changed || _Cts.Token.IsCancellationRequested)
break; break;
} }
catch(OperationCanceledException) when(_Cts.Token.IsCancellationRequested) catch (OperationCanceledException) when (_Cts.Token.IsCancellationRequested)
{ {
break; break;
} }
catch(Exception ex) catch (Exception ex)
{ {
Logs.PayServer.LogError(ex, "Unhandled error on watching invoice " + invoiceId); Logs.PayServer.LogError(ex, "Unhandled error on watching invoice " + invoiceId);
await Task.Delay(10000, _Cts.Token).ConfigureAwait(false); await Task.Delay(10000, _Cts.Token).ConfigureAwait(false);
} }
} }
} }
private async Task<(bool NeedSave, UTXOChanges Changes)> UpdateInvoice(UTXOChanges changes, InvoiceEntity invoice) private async Task<(bool NeedSave, UTXOChanges Changes)> UpdateInvoice(UTXOChanges changes, InvoiceEntity invoice)
{ {
bool needSave = false; bool needSave = false;
//Fetch unknown payments //Fetch unknown payments
var strategy = _DerivationFactory.Parse(invoice.DerivationStrategy); var strategy = _DerivationFactory.Parse(invoice.DerivationStrategy);
changes = await _ExplorerClient.SyncAsync(strategy, changes, !LongPollingMode, _Cts.Token).ConfigureAwait(false); changes = await _ExplorerClient.SyncAsync(strategy, changes, !LongPollingMode, _Cts.Token).ConfigureAwait(false);
var utxos = changes.Confirmed.UTXOs.Concat(changes.Unconfirmed.UTXOs).ToArray(); var utxos = changes.Confirmed.UTXOs.Concat(changes.Unconfirmed.UTXOs).ToArray();
var invoiceIds = utxos.Select(u => _InvoiceRepository.GetInvoiceIdFromScriptPubKey(u.Output.ScriptPubKey)).ToArray(); var invoiceIds = utxos.Select(u => _InvoiceRepository.GetInvoiceIdFromScriptPubKey(u.Output.ScriptPubKey)).ToArray();
utxos = utxos =
utxos utxos
.Where((u, i) => invoiceIds[i].GetAwaiter().GetResult() == invoice.Id) .Where((u, i) => invoiceIds[i].GetAwaiter().GetResult() == invoice.Id)
.ToArray(); .ToArray();
List<Coin> receivedCoins = new List<Coin>(); List<Coin> receivedCoins = new List<Coin>();
foreach(var received in utxos) foreach (var received in utxos)
if(received.Output.ScriptPubKey == invoice.DepositAddress.ScriptPubKey) if (received.Output.ScriptPubKey == invoice.DepositAddress.ScriptPubKey)
receivedCoins.Add(new Coin(received.Outpoint, received.Output)); receivedCoins.Add(new Coin(received.Outpoint, received.Output));
var alreadyAccounted = new HashSet<OutPoint>(invoice.Payments.Select(p => p.Outpoint)); var alreadyAccounted = new HashSet<OutPoint>(invoice.Payments.Select(p => p.Outpoint));
BitcoinAddress generatedAddress = null; BitcoinAddress generatedAddress = null;
bool dirtyAddress = false; bool dirtyAddress = false;
foreach(var coin in receivedCoins.Where(c => !alreadyAccounted.Contains(c.Outpoint))) foreach (var coin in receivedCoins.Where(c => !alreadyAccounted.Contains(c.Outpoint)))
{ {
var payment = await _InvoiceRepository.AddPayment(invoice.Id, coin).ConfigureAwait(false); var payment = await _InvoiceRepository.AddPayment(invoice.Id, coin).ConfigureAwait(false);
invoice.Payments.Add(payment); invoice.Payments.Add(payment);
if(coin.ScriptPubKey == invoice.DepositAddress.ScriptPubKey && generatedAddress == null) if (coin.ScriptPubKey == invoice.DepositAddress.ScriptPubKey && generatedAddress == null)
{ {
dirtyAddress = true; dirtyAddress = true;
} }
} }
////// //////
if(invoice.Status == "new" && invoice.ExpirationTime < DateTimeOffset.UtcNow) if (invoice.Status == "new" && invoice.ExpirationTime < DateTimeOffset.UtcNow)
{ {
needSave = true; needSave = true;
await _InvoiceRepository.UnaffectAddress(invoice.Id); await _InvoiceRepository.UnaffectAddress(invoice.Id);
invoice.Status = "expired"; invoice.Status = "expired";
} }
if(invoice.Status == "new" || invoice.Status == "expired") if (invoice.Status == "new" || invoice.Status == "expired")
{ {
var totalPaid = invoice.Payments.Select(p => p.Output.Value).Sum(); var totalPaid = invoice.Payments.Select(p => p.Output.Value).Sum();
if(totalPaid >= invoice.GetTotalCryptoDue()) if (totalPaid >= invoice.GetTotalCryptoDue())
{ {
if(invoice.Status == "new") if (invoice.Status == "new")
{ {
invoice.Status = "paid"; invoice.Status = "paid";
if(invoice.FullNotifications) if (invoice.FullNotifications)
{ {
_NotificationManager.Notify(invoice); _NotificationManager.Notify(invoice);
} }
invoice.ExceptionStatus = null; invoice.ExceptionStatus = null;
await _InvoiceRepository.UnaffectAddress(invoice.Id); await _InvoiceRepository.UnaffectAddress(invoice.Id);
needSave = true; needSave = true;
} }
else if(invoice.Status == "expired") else if (invoice.Status == "expired")
{ {
invoice.ExceptionStatus = "paidLate"; invoice.ExceptionStatus = "paidLate";
needSave = true; needSave = true;
} }
} }
if(totalPaid > invoice.GetTotalCryptoDue() && invoice.ExceptionStatus != "paidOver") if (totalPaid > invoice.GetTotalCryptoDue() && invoice.ExceptionStatus != "paidOver")
{ {
invoice.ExceptionStatus = "paidOver"; invoice.ExceptionStatus = "paidOver";
await _InvoiceRepository.UnaffectAddress(invoice.Id); await _InvoiceRepository.UnaffectAddress(invoice.Id);
needSave = true; needSave = true;
} }
if(totalPaid < invoice.GetTotalCryptoDue() && invoice.Payments.Count != 0 && invoice.ExceptionStatus != "paidPartial") if (totalPaid < invoice.GetTotalCryptoDue() && invoice.Payments.Count != 0 && invoice.ExceptionStatus != "paidPartial")
{ {
Logs.PayServer.LogInformation("Paid to " + invoice.DepositAddress); Logs.PayServer.LogInformation("Paid to " + invoice.DepositAddress);
invoice.ExceptionStatus = "paidPartial"; invoice.ExceptionStatus = "paidPartial";
needSave = true; needSave = true;
if(dirtyAddress) if (dirtyAddress)
{ {
var address = await _Wallet.ReserveAddressAsync(_DerivationFactory.Parse(invoice.DerivationStrategy)); var address = await _Wallet.ReserveAddressAsync(_DerivationFactory.Parse(invoice.DerivationStrategy));
Logs.PayServer.LogInformation("Generate new " + address); Logs.PayServer.LogInformation("Generate new " + address);
await _InvoiceRepository.NewAddress(invoice.Id, address); await _InvoiceRepository.NewAddress(invoice.Id, address);
} }
} }
} }
if(invoice.Status == "paid") if (invoice.Status == "paid")
{ {
if(!invoice.MonitoringExpiration.HasValue || invoice.MonitoringExpiration > DateTimeOffset.UtcNow) if (!invoice.MonitoringExpiration.HasValue || invoice.MonitoringExpiration > DateTimeOffset.UtcNow)
{ {
var transactions = await GetPaymentsWithTransaction(invoice); var transactions = await GetPaymentsWithTransaction(invoice);
if(invoice.SpeedPolicy == SpeedPolicy.HighSpeed) if (invoice.SpeedPolicy == SpeedPolicy.HighSpeed)
{ {
transactions = transactions.Where(t => !t.Transaction.Transaction.RBF); transactions = transactions.Where(t => !t.Transaction.Transaction.RBF);
} }
else if(invoice.SpeedPolicy == SpeedPolicy.MediumSpeed) else if (invoice.SpeedPolicy == SpeedPolicy.MediumSpeed)
{ {
transactions = transactions.Where(t => t.Transaction.Confirmations >= 1); transactions = transactions.Where(t => t.Transaction.Confirmations >= 1);
} }
else if(invoice.SpeedPolicy == SpeedPolicy.LowSpeed) else if (invoice.SpeedPolicy == SpeedPolicy.LowSpeed)
{ {
transactions = transactions.Where(t => t.Transaction.Confirmations >= 6); transactions = transactions.Where(t => t.Transaction.Confirmations >= 6);
} }
var totalConfirmed = transactions.Select(t => t.Payment.Output.Value).Sum(); var totalConfirmed = transactions.Select(t => t.Payment.Output.Value).Sum();
if(totalConfirmed >= invoice.GetTotalCryptoDue()) if (totalConfirmed >= invoice.GetTotalCryptoDue())
{ {
await _InvoiceRepository.UnaffectAddress(invoice.Id); await _InvoiceRepository.UnaffectAddress(invoice.Id);
invoice.Status = "confirmed"; invoice.Status = "confirmed";
_NotificationManager.Notify(invoice); _NotificationManager.Notify(invoice);
needSave = true; needSave = true;
} }
} }
else else
{ {
await _InvoiceRepository.UnaffectAddress(invoice.Id); await _InvoiceRepository.UnaffectAddress(invoice.Id);
invoice.Status = "invalid"; invoice.Status = "invalid";
needSave = true; needSave = true;
} }
} }
if(invoice.Status == "confirmed") if (invoice.Status == "confirmed")
{ {
var transactions = await GetPaymentsWithTransaction(invoice); var transactions = await GetPaymentsWithTransaction(invoice);
transactions = transactions.Where(t => t.Transaction.Confirmations >= 6); transactions = transactions.Where(t => t.Transaction.Confirmations >= 6);
var totalConfirmed = transactions.Select(t => t.Payment.Output.Value).Sum(); var totalConfirmed = transactions.Select(t => t.Payment.Output.Value).Sum();
if(totalConfirmed >= invoice.GetTotalCryptoDue()) if (totalConfirmed >= invoice.GetTotalCryptoDue())
{ {
invoice.Status = "complete"; invoice.Status = "complete";
if(invoice.FullNotifications) if (invoice.FullNotifications)
_NotificationManager.Notify(invoice); _NotificationManager.Notify(invoice);
needSave = true; needSave = true;
} }
} }
return (needSave, changes); return (needSave, changes);
} }
private async Task<IEnumerable<(PaymentEntity Payment, TransactionResult Transaction)>> GetPaymentsWithTransaction(InvoiceEntity invoice) private async Task<IEnumerable<(PaymentEntity Payment, TransactionResult Transaction)>> GetPaymentsWithTransaction(InvoiceEntity invoice)
{ {
var getPayments = invoice.Payments var getPayments = invoice.Payments
.Select(async o => (Payment: o, Transaction: await _ExplorerClient.GetTransactionAsync(o.Outpoint.Hash, _Cts.Token))) .Select(async o => (Payment: o, Transaction: await _ExplorerClient.GetTransactionAsync(o.Outpoint.Hash, _Cts.Token)))
.ToArray(); .ToArray();
await Task.WhenAll(getPayments).ConfigureAwait(false); await Task.WhenAll(getPayments).ConfigureAwait(false);
var transactions = getPayments.Select(c => (Payment: c.Result.Payment, Transaction: c.Result.Transaction)); var transactions = getPayments.Select(c => (Payment: c.Result.Payment, Transaction: c.Result.Transaction));
return transactions; return transactions;
} }
TimeSpan _PollInterval; TimeSpan _PollInterval;
public TimeSpan PollInterval public TimeSpan PollInterval
{ {
get get
{ {
return _PollInterval; return _PollInterval;
} }
set set
{ {
_PollInterval = value; _PollInterval = value;
if(_UpdatePendingInvoices != null) if (_UpdatePendingInvoices != null)
{ {
_UpdatePendingInvoices.Change(0, (int)value.TotalMilliseconds); _UpdatePendingInvoices.Change(0, (int)value.TotalMilliseconds);
} }
} }
} }
public async Task WatchAsync(string invoiceId, bool singleShot = false) public async Task WatchAsync(string invoiceId, bool singleShot = false)
{ {
if(invoiceId == null) if (invoiceId == null)
throw new ArgumentNullException(nameof(invoiceId)); throw new ArgumentNullException(nameof(invoiceId));
if(!singleShot) if (!singleShot)
await _InvoiceRepository.AddPendingInvoice(invoiceId).ConfigureAwait(false); await _InvoiceRepository.AddPendingInvoice(invoiceId).ConfigureAwait(false);
_WatchRequests.Add(invoiceId); _WatchRequests.Add(invoiceId);
} }
BlockingCollection<string> _WatchRequests = new BlockingCollection<string>(new ConcurrentQueue<string>()); BlockingCollection<string> _WatchRequests = new BlockingCollection<string>(new ConcurrentQueue<string>());
public void Dispose() public void Dispose()
{ {
_Cts.Cancel(); _Cts.Cancel();
} }
Thread _Thread; Thread _Thread;
TaskCompletionSource<bool> _RunningTask; TaskCompletionSource<bool> _RunningTask;
CancellationTokenSource _Cts; CancellationTokenSource _Cts;
Timer _UpdatePendingInvoices; Timer _UpdatePendingInvoices;
public Task StartAsync(CancellationToken cancellationToken) public Task StartAsync(CancellationToken cancellationToken)
{ {
_RunningTask = new TaskCompletionSource<bool>(); _RunningTask = new TaskCompletionSource<bool>();
_Cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); _Cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
_Thread = new Thread(Run) { Name = "InvoiceWatcher" }; _Thread = new Thread(Run) { Name = "InvoiceWatcher" };
_Thread.Start(); _Thread.Start();
_UpdatePendingInvoices = new Timer(async s => _UpdatePendingInvoices = new Timer(async s =>
{ {
foreach(var pending in await _InvoiceRepository.GetPendingInvoices()) foreach (var pending in await _InvoiceRepository.GetPendingInvoices())
{ {
_WatchRequests.Add(pending); _WatchRequests.Add(pending);
} }
}, null, 0, (int)PollInterval.TotalMilliseconds); }, null, 0, (int)PollInterval.TotalMilliseconds);
return Task.CompletedTask; return Task.CompletedTask;
} }
void Run() void Run()
{ {
Logs.PayServer.LogInformation("Start watching invoices"); Logs.PayServer.LogInformation("Start watching invoices");
ConcurrentDictionary<string, Lazy<Task>> updating = new ConcurrentDictionary<string, Lazy<Task>>(); ConcurrentDictionary<string, Lazy<Task>> updating = new ConcurrentDictionary<string, Lazy<Task>>();
try try
{ {
foreach(var item in _WatchRequests.GetConsumingEnumerable(_Cts.Token)) foreach (var item in _WatchRequests.GetConsumingEnumerable(_Cts.Token))
{ {
try try
{ {
_Cts.Token.ThrowIfCancellationRequested(); _Cts.Token.ThrowIfCancellationRequested();
var localItem = item; var localItem = item;
// If the invoice is already updating, ignore // If the invoice is already updating, ignore
Lazy<Task> updateInvoice = new Lazy<Task>(() => UpdateInvoice(localItem), false); Lazy<Task> updateInvoice = new Lazy<Task>(() => UpdateInvoice(localItem), false);
if(updating.TryAdd(item, updateInvoice)) if (updating.TryAdd(item, updateInvoice))
{ {
updateInvoice.Value.ContinueWith(i => updating.TryRemove(item, out updateInvoice)); updateInvoice.Value.ContinueWith(i => updating.TryRemove(item, out updateInvoice));
} }
} }
catch(Exception ex) when(!_Cts.Token.IsCancellationRequested) catch (Exception ex) when (!_Cts.Token.IsCancellationRequested)
{ {
Logs.PayServer.LogCritical(ex, $"Error in the InvoiceWatcher loop (Invoice {item})"); Logs.PayServer.LogCritical(ex, $"Error in the InvoiceWatcher loop (Invoice {item})");
_Cts.Token.WaitHandle.WaitOne(2000); _Cts.Token.WaitHandle.WaitOne(2000);
} }
} }
} }
catch(OperationCanceledException) catch (OperationCanceledException)
{ {
try try
{ {
Task.WaitAll(updating.Select(c => c.Value.Value).ToArray()); Task.WaitAll(updating.Select(c => c.Value.Value).ToArray());
} }
catch(AggregateException) { } catch (AggregateException) { }
_RunningTask.TrySetResult(true); _RunningTask.TrySetResult(true);
} }
finally finally
{ {
Logs.PayServer.LogInformation("Stop watching invoices"); Logs.PayServer.LogInformation("Stop watching invoices");
} }
} }
public Task StopAsync(CancellationToken cancellationToken) public Task StopAsync(CancellationToken cancellationToken)
{ {
_UpdatePendingInvoices.Dispose(); _UpdatePendingInvoices.Dispose();
_Cts.Cancel(); _Cts.Cancel();
return Task.WhenAny(_RunningTask.Task, Task.Delay(-1, cancellationToken)); return Task.WhenAny(_RunningTask.Task, Task.Delay(-1, cancellationToken));
} }
} }
} }

View File

@@ -7,34 +7,34 @@ using System.Threading.Tasks;
namespace BTCPayServer.Services.Mails namespace BTCPayServer.Services.Mails
{ {
// This class is used by the application to send email for account confirmation and password reset. // This class is used by the application to send email for account confirmation and password reset.
// For more details see https://go.microsoft.com/fwlink/?LinkID=532713 // For more details see https://go.microsoft.com/fwlink/?LinkID=532713
public class EmailSender : IEmailSender public class EmailSender : IEmailSender
{ {
IBackgroundJobClient _JobClient; IBackgroundJobClient _JobClient;
SettingsRepository _Repository; SettingsRepository _Repository;
public EmailSender(IBackgroundJobClient jobClient, SettingsRepository repository) public EmailSender(IBackgroundJobClient jobClient, SettingsRepository repository)
{ {
if(jobClient == null) if (jobClient == null)
throw new ArgumentNullException(nameof(jobClient)); throw new ArgumentNullException(nameof(jobClient));
_JobClient = jobClient; _JobClient = jobClient;
_Repository = repository; _Repository = repository;
} }
public Task SendEmailAsync(string email, string subject, string message) public Task SendEmailAsync(string email, string subject, string message)
{ {
_JobClient.Schedule(() => SendMailCore(email, subject, message), TimeSpan.Zero); _JobClient.Schedule(() => SendMailCore(email, subject, message), TimeSpan.Zero);
return Task.CompletedTask; return Task.CompletedTask;
} }
public async Task SendMailCore(string email, string subject, string message) public async Task SendMailCore(string email, string subject, string message)
{ {
var settings = await _Repository.GetSettingAsync<EmailSettings>(); var settings = await _Repository.GetSettingAsync<EmailSettings>();
if(settings == null) if (settings == null)
throw new InvalidOperationException("Email settings not configured"); throw new InvalidOperationException("Email settings not configured");
var smtp = settings.CreateSmtpClient(); var smtp = settings.CreateSmtpClient();
MailMessage mail = new MailMessage(settings.From, email, subject, message); MailMessage mail = new MailMessage(settings.From, email, subject, message);
mail.IsBodyHtml = true; mail.IsBodyHtml = true;
await smtp.SendMailAsync(mail); await smtp.SendMailAsync(mail);
} }
} }
} }

View File

@@ -10,50 +10,50 @@ namespace BTCPayServer.Services.Mails
{ {
public class EmailSettings public class EmailSettings
{ {
[Required] [Required]
public string Server public string Server
{ {
get; set; get; set;
} }
[Required] [Required]
public int? Port public int? Port
{ {
get; set; get; set;
} }
[Required] [Required]
public String Login public String Login
{ {
get; set; get; set;
} }
[Required] [Required]
public String Password public String Password
{ {
get; set; get; set;
} }
[EmailAddress] [EmailAddress]
public string From public string From
{ {
get; set; get; set;
} }
public bool EnableSSL public bool EnableSSL
{ {
get; set; get; set;
} }
public SmtpClient CreateSmtpClient() public SmtpClient CreateSmtpClient()
{ {
SmtpClient client = new SmtpClient(Server, Port.Value); SmtpClient client = new SmtpClient(Server, Port.Value);
client.EnableSsl = true; client.EnableSsl = true;
client.UseDefaultCredentials = false; client.UseDefaultCredentials = false;
client.Credentials = new NetworkCredential(Login, Password); client.Credentials = new NetworkCredential(Login, Password);
client.DeliveryMethod = SmtpDeliveryMethod.Network; client.DeliveryMethod = SmtpDeliveryMethod.Network;
client.Timeout = 10000; client.Timeout = 10000;
return client; return client;
} }
} }
} }

View File

@@ -7,9 +7,9 @@ namespace BTCPayServer.Services
{ {
public class PoliciesSettings public class PoliciesSettings
{ {
public bool RequiresConfirmedEmail public bool RequiresConfirmedEmail
{ {
get; set; get; set;
} }
} }
} }

View File

@@ -7,30 +7,30 @@ using System.Threading.Tasks;
namespace BTCPayServer.Services.Rates namespace BTCPayServer.Services.Rates
{ {
public class BitpayRateProvider : IRateProvider public class BitpayRateProvider : IRateProvider
{ {
Bitpay _Bitpay; Bitpay _Bitpay;
public BitpayRateProvider(Bitpay bitpay) public BitpayRateProvider(Bitpay bitpay)
{ {
if(bitpay == null) if (bitpay == null)
throw new ArgumentNullException(nameof(bitpay)); throw new ArgumentNullException(nameof(bitpay));
_Bitpay = bitpay; _Bitpay = bitpay;
} }
public async Task<decimal> GetRateAsync(string currency) public async Task<decimal> GetRateAsync(string currency)
{ {
var rates = await _Bitpay.GetRatesAsync().ConfigureAwait(false); var rates = await _Bitpay.GetRatesAsync().ConfigureAwait(false);
var rate = rates.GetRate(currency); var rate = rates.GetRate(currency);
if(rate == 0m) if (rate == 0m)
throw new RateUnavailableException(currency); throw new RateUnavailableException(currency);
return (decimal)rate; return (decimal)rate;
} }
public async Task<ICollection<Rate>> GetRatesAsync() public async Task<ICollection<Rate>> GetRatesAsync()
{ {
return (await _Bitpay.GetRatesAsync().ConfigureAwait(false)) return (await _Bitpay.GetRatesAsync().ConfigureAwait(false))
.AllRates .AllRates
.Select(r => new Rate() { Currency = r.Code, Value = r.Value }) .Select(r => new Rate() { Currency = r.Code, Value = r.Value })
.ToList(); .ToList();
} }
} }
} }

View File

@@ -6,49 +6,49 @@ using Microsoft.Extensions.Caching.Memory;
namespace BTCPayServer.Services.Rates namespace BTCPayServer.Services.Rates
{ {
public class CachedRateProvider : IRateProvider public class CachedRateProvider : IRateProvider
{ {
private IRateProvider _Inner; private IRateProvider _Inner;
private IMemoryCache _MemoryCache; private IMemoryCache _MemoryCache;
public CachedRateProvider(IRateProvider inner, IMemoryCache memoryCache) public CachedRateProvider(IRateProvider inner, IMemoryCache memoryCache)
{ {
if(inner == null) if (inner == null)
throw new ArgumentNullException(nameof(inner)); throw new ArgumentNullException(nameof(inner));
if(memoryCache == null) if (memoryCache == null)
throw new ArgumentNullException(nameof(memoryCache)); throw new ArgumentNullException(nameof(memoryCache));
this._Inner = inner; this._Inner = inner;
this._MemoryCache = memoryCache; this._MemoryCache = memoryCache;
} }
public TimeSpan CacheSpan public TimeSpan CacheSpan
{ {
get; get;
set; set;
} = TimeSpan.FromMinutes(1.0); } = TimeSpan.FromMinutes(1.0);
public Task<decimal> GetRateAsync(string currency) public Task<decimal> GetRateAsync(string currency)
{ {
return _MemoryCache.GetOrCreateAsync("CURR_" + currency, (ICacheEntry entry) => return _MemoryCache.GetOrCreateAsync("CURR_" + currency, (ICacheEntry entry) =>
{ {
entry.AbsoluteExpiration = DateTimeOffset.UtcNow + CacheSpan; entry.AbsoluteExpiration = DateTimeOffset.UtcNow + CacheSpan;
return _Inner.GetRateAsync(currency); return _Inner.GetRateAsync(currency);
}); });
} }
private bool TryGetFromCache(string key, out object obj) private bool TryGetFromCache(string key, out object obj)
{ {
obj = _MemoryCache.Get(key); obj = _MemoryCache.Get(key);
return obj != null; return obj != null;
} }
public Task<ICollection<Rate>> GetRatesAsync() public Task<ICollection<Rate>> GetRatesAsync()
{ {
return _MemoryCache.GetOrCreateAsync("GLOBAL_RATES", (ICacheEntry entry) => return _MemoryCache.GetOrCreateAsync("GLOBAL_RATES", (ICacheEntry entry) =>
{ {
entry.AbsoluteExpiration = DateTimeOffset.UtcNow + CacheSpan; entry.AbsoluteExpiration = DateTimeOffset.UtcNow + CacheSpan;
return _Inner.GetRatesAsync(); return _Inner.GetRatesAsync();
}); });
} }
} }
} }

View File

@@ -8,109 +8,109 @@ using System.Threading.Tasks;
namespace BTCPayServer.Services.Rates namespace BTCPayServer.Services.Rates
{ {
public class CoinAverageException : Exception public class CoinAverageException : Exception
{ {
public CoinAverageException(string message) : base(message) public CoinAverageException(string message) : base(message)
{ {
} }
} }
public class CoinAverageRateProvider : IRateProvider public class CoinAverageRateProvider : IRateProvider
{ {
public class RatesJson public class RatesJson
{ {
public class RateJson public class RateJson
{ {
public string Code public string Code
{ {
get; set; get; set;
} }
public decimal Rate public decimal Rate
{ {
get; set; get; set;
} }
} }
[JsonProperty("rates")] [JsonProperty("rates")]
public JObject RatesInternal public JObject RatesInternal
{ {
get; set; get; set;
} }
[JsonIgnore] [JsonIgnore]
public List<RateJson> Rates public List<RateJson> Rates
{ {
get; set; get; set;
} }
[JsonIgnore] [JsonIgnore]
public Dictionary<string, decimal> RatesByCurrency public Dictionary<string, decimal> RatesByCurrency
{ {
get; set; get; set;
} }
public decimal GetRate(string currency) public decimal GetRate(string currency)
{ {
if(!RatesByCurrency.TryGetValue(currency.ToUpperInvariant(), out decimal currUSD)) if (!RatesByCurrency.TryGetValue(currency.ToUpperInvariant(), out decimal currUSD))
throw new RateUnavailableException(currency); throw new RateUnavailableException(currency);
if(!RatesByCurrency.TryGetValue("BTC", out decimal btcUSD)) if (!RatesByCurrency.TryGetValue("BTC", out decimal btcUSD))
throw new RateUnavailableException(currency); throw new RateUnavailableException(currency);
return currUSD / btcUSD; return currUSD / btcUSD;
} }
public void CalculateDictionary() public void CalculateDictionary()
{ {
RatesByCurrency = new Dictionary<string, decimal>(); RatesByCurrency = new Dictionary<string, decimal>();
Rates = new List<RateJson>(); Rates = new List<RateJson>();
foreach(var rate in RatesInternal.OfType<JProperty>()) foreach (var rate in RatesInternal.OfType<JProperty>())
{ {
var rateJson = new RateJson(); var rateJson = new RateJson();
rateJson.Code = rate.Name; rateJson.Code = rate.Name;
rateJson.Rate = rate.Value["rate"].Value<decimal>(); rateJson.Rate = rate.Value["rate"].Value<decimal>();
RatesByCurrency.Add(rate.Name, rateJson.Rate); RatesByCurrency.Add(rate.Name, rateJson.Rate);
Rates.Add(rateJson); Rates.Add(rateJson);
} }
} }
} }
static HttpClient _Client = new HttpClient(); static HttpClient _Client = new HttpClient();
public string Market public string Market
{ {
get; set; get; set;
} = "global"; } = "global";
public async Task<decimal> GetRateAsync(string currency) public async Task<decimal> GetRateAsync(string currency)
{ {
RatesJson rates = await GetRatesCore(); RatesJson rates = await GetRatesCore();
return rates.GetRate(currency); return rates.GetRate(currency);
} }
private async Task<RatesJson> GetRatesCore() private async Task<RatesJson> GetRatesCore()
{ {
var resp = await _Client.GetAsync("https://apiv2.bitcoinaverage.com/constants/exchangerates/" + Market); var resp = await _Client.GetAsync("https://apiv2.bitcoinaverage.com/constants/exchangerates/" + Market);
using(resp) using (resp)
{ {
if((int)resp.StatusCode == 401) if ((int)resp.StatusCode == 401)
throw new CoinAverageException("Unauthorized access to the API"); throw new CoinAverageException("Unauthorized access to the API");
if((int)resp.StatusCode == 429) if ((int)resp.StatusCode == 429)
throw new CoinAverageException("Exceed API limits"); throw new CoinAverageException("Exceed API limits");
if((int)resp.StatusCode == 403) if ((int)resp.StatusCode == 403)
throw new CoinAverageException("Unauthorized access to the API, premium plan needed"); throw new CoinAverageException("Unauthorized access to the API, premium plan needed");
resp.EnsureSuccessStatusCode(); resp.EnsureSuccessStatusCode();
var rates = JsonConvert.DeserializeObject<RatesJson>(await resp.Content.ReadAsStringAsync()); var rates = JsonConvert.DeserializeObject<RatesJson>(await resp.Content.ReadAsStringAsync());
rates.CalculateDictionary(); rates.CalculateDictionary();
return rates; return rates;
} }
} }
public async Task<ICollection<Rate>> GetRatesAsync() public async Task<ICollection<Rate>> GetRatesAsync()
{ {
RatesJson rates = await GetRatesCore(); RatesJson rates = await GetRatesCore();
return rates.Rates.Select(o => new Rate() return rates.Rates.Select(o => new Rate()
{ {
Currency = o.Code, Currency = o.Code,
Value = rates.GetRate(o.Code) Value = rates.GetRate(o.Code)
}).ToList(); }).ToList();
} }
} }
} }

View File

@@ -7,77 +7,77 @@ using System.Text;
namespace BTCPayServer.Services.Rates namespace BTCPayServer.Services.Rates
{ {
public class CurrencyData public class CurrencyData
{
public string Name
{
get;
internal set;
}
public string Code
{
get;
internal set;
}
public int Divisibility
{
get;
internal set;
}
public string Symbol
{
get;
internal set;
}
}
public class CurrencyNameTable
{ {
public CurrencyNameTable() public string Name
{ {
_Currencies = LoadCurrency().ToDictionary(k => k.Code); get;
} internal set;
}
public string Code
{
get;
internal set;
}
public int Divisibility
{
get;
internal set;
}
public string Symbol
{
get;
internal set;
}
}
public class CurrencyNameTable
{
public CurrencyNameTable()
{
_Currencies = LoadCurrency().ToDictionary(k => k.Code);
}
Dictionary<string, CurrencyData> _Currencies; Dictionary<string, CurrencyData> _Currencies;
static CurrencyData[] LoadCurrency() static CurrencyData[] LoadCurrency()
{ {
var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("BTCPayServer.Currencies.txt"); var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("BTCPayServer.Currencies.txt");
string content = null; string content = null;
using(var reader = new StreamReader(stream, Encoding.UTF8)) using (var reader = new StreamReader(stream, Encoding.UTF8))
{ {
content = reader.ReadToEnd(); content = reader.ReadToEnd();
} }
var currencies = content.Split(new[] { "\r\n", "\n" }, StringSplitOptions.RemoveEmptyEntries); var currencies = content.Split(new[] { "\r\n", "\n" }, StringSplitOptions.RemoveEmptyEntries);
Dictionary<string, CurrencyData> dico = new Dictionary<string, CurrencyData>(); Dictionary<string, CurrencyData> dico = new Dictionary<string, CurrencyData>();
foreach(var currency in currencies) foreach (var currency in currencies)
{ {
var splitted = currency.Split(new[] { '\t' }, StringSplitOptions.RemoveEmptyEntries); var splitted = currency.Split(new[] { '\t' }, StringSplitOptions.RemoveEmptyEntries);
if(splitted.Length < 3) if (splitted.Length < 3)
continue; continue;
CurrencyData info = new CurrencyData(); CurrencyData info = new CurrencyData();
info.Name = splitted[0]; info.Name = splitted[0];
info.Code = splitted[1]; info.Code = splitted[1];
int divisibility; int divisibility;
if(!int.TryParse(splitted[2], out divisibility)) if (!int.TryParse(splitted[2], out divisibility))
continue; continue;
info.Divisibility = divisibility; info.Divisibility = divisibility;
if(!dico.ContainsKey(info.Code)) if (!dico.ContainsKey(info.Code))
dico.Add(info.Code, info); dico.Add(info.Code, info);
if(splitted.Length >= 4) if (splitted.Length >= 4)
{ {
info.Symbol = splitted[3]; info.Symbol = splitted[3];
} }
} }
return dico.Values.ToArray(); return dico.Values.ToArray();
} }
public CurrencyData GetCurrencyData(string currency) public CurrencyData GetCurrencyData(string currency)
{ {
CurrencyData result; CurrencyData result;
_Currencies.TryGetValue(currency.ToUpperInvariant(), out result); _Currencies.TryGetValue(currency.ToUpperInvariant(), out result);
return result; return result;
} }
} }
} }

View File

@@ -5,29 +5,29 @@ using System.Threading.Tasks;
namespace BTCPayServer.Services.Rates namespace BTCPayServer.Services.Rates
{ {
public class Rate public class Rate
{ {
public Rate() public Rate()
{ {
} }
public Rate(string currency, decimal value) public Rate(string currency, decimal value)
{ {
Value = value; Value = value;
Currency = currency; Currency = currency;
} }
public string Currency public string Currency
{ {
get; set; get; set;
} }
public decimal Value public decimal Value
{ {
get; set; get; set;
} }
} }
public interface IRateProvider public interface IRateProvider
{ {
Task<decimal> GetRateAsync(string currency); Task<decimal> GetRateAsync(string currency);
Task<ICollection<Rate>> GetRatesAsync(); Task<ICollection<Rate>> GetRatesAsync();
} }
} }

View File

@@ -6,30 +6,30 @@ using System.Threading.Tasks;
namespace BTCPayServer.Services.Rates namespace BTCPayServer.Services.Rates
{ {
public class MockRateProvider : IRateProvider public class MockRateProvider : IRateProvider
{ {
List<Rate> _Rates; List<Rate> _Rates;
public MockRateProvider(params Rate[] rates) public MockRateProvider(params Rate[] rates)
{ {
_Rates = new List<Rate>(rates); _Rates = new List<Rate>(rates);
} }
public MockRateProvider(List<Rate> rates) public MockRateProvider(List<Rate> rates)
{ {
_Rates = rates; _Rates = rates;
} }
public Task<decimal> GetRateAsync(string currency) public Task<decimal> GetRateAsync(string currency)
{ {
var rate = _Rates.FirstOrDefault(r => r.Currency.Equals(currency, StringComparison.OrdinalIgnoreCase)); var rate = _Rates.FirstOrDefault(r => r.Currency.Equals(currency, StringComparison.OrdinalIgnoreCase));
if(rate == null) if (rate == null)
throw new RateUnavailableException(currency); throw new RateUnavailableException(currency);
return Task.FromResult(rate.Value); return Task.FromResult(rate.Value);
} }
public Task<ICollection<Rate>> GetRatesAsync() public Task<ICollection<Rate>> GetRatesAsync()
{ {
ICollection<Rate> rates = _Rates; ICollection<Rate> rates = _Rates;
return Task.FromResult(rates); return Task.FromResult(rates);
} }
} }
} }

View File

@@ -6,16 +6,16 @@ namespace BTCPayServer.Services.Rates
{ {
public class RateUnavailableException : Exception public class RateUnavailableException : Exception
{ {
public RateUnavailableException(string currency) : base("Rate unavailable for currency " + currency) public RateUnavailableException(string currency) : base("Rate unavailable for currency " + currency)
{ {
if(currency == null) if (currency == null)
throw new ArgumentNullException(nameof(currency)); throw new ArgumentNullException(nameof(currency));
Currency = currency; Currency = currency;
} }
public string Currency public string Currency
{ {
get; set; get; set;
} }
} }
} }

View File

@@ -11,56 +11,56 @@ using Newtonsoft.Json;
namespace BTCPayServer.Services namespace BTCPayServer.Services
{ {
public class SettingsRepository public class SettingsRepository
{ {
private ApplicationDbContextFactory _ContextFactory; private ApplicationDbContextFactory _ContextFactory;
public SettingsRepository(ApplicationDbContextFactory contextFactory) public SettingsRepository(ApplicationDbContextFactory contextFactory)
{ {
_ContextFactory = contextFactory; _ContextFactory = contextFactory;
} }
public async Task<T> GetSettingAsync<T>() public async Task<T> GetSettingAsync<T>()
{ {
var name = typeof(T).FullName; var name = typeof(T).FullName;
using(var ctx = _ContextFactory.CreateContext()) using (var ctx = _ContextFactory.CreateContext())
{ {
var data = await ctx.Settings.Where(s => s.Id == name).FirstOrDefaultAsync(); var data = await ctx.Settings.Where(s => s.Id == name).FirstOrDefaultAsync();
if(data == null) if (data == null)
return default(T); return default(T);
return Deserialize<T>(data.Value); return Deserialize<T>(data.Value);
} }
} }
public async Task UpdateSetting<T>(T obj) public async Task UpdateSetting<T>(T obj)
{ {
var name = obj.GetType().FullName; var name = obj.GetType().FullName;
using(var ctx = _ContextFactory.CreateContext()) using (var ctx = _ContextFactory.CreateContext())
{ {
var settings = new SettingData(); var settings = new SettingData();
settings.Id = name; settings.Id = name;
settings.Value = Serialize(obj); settings.Value = Serialize(obj);
ctx.Attach(settings); ctx.Attach(settings);
ctx.Entry(settings).State = EntityState.Modified; ctx.Entry(settings).State = EntityState.Modified;
try try
{ {
await ctx.SaveChangesAsync(); await ctx.SaveChangesAsync();
} }
catch(DbUpdateException) catch (DbUpdateException)
{ {
ctx.Entry(settings).State = EntityState.Added; ctx.Entry(settings).State = EntityState.Added;
await ctx.SaveChangesAsync(); await ctx.SaveChangesAsync();
} }
} }
} }
private T Deserialize<T>(string value) private T Deserialize<T>(string value)
{ {
return JsonConvert.DeserializeObject<T>(value); return JsonConvert.DeserializeObject<T>(value);
} }
private string Serialize<T>(T obj) private string Serialize<T>(T obj)
{ {
return JsonConvert.SerializeObject(obj); return JsonConvert.SerializeObject(obj);
} }
} }
} }

Some files were not shown because too many files have changed in this diff Show More