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

View file

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

View file

@ -6,84 +6,84 @@ using Xunit.Abstractions;
namespace BTCPayServer.Tests.Logging
{
public interface ILog
{
void LogInformation(string msg);
}
public interface ILog
{
void LogInformation(string msg);
}
public class XUnitLogProvider : ILoggerProvider
{
ITestOutputHelper _Helper;
public XUnitLogProvider(ITestOutputHelper helper)
{
_Helper = helper;
}
public ILogger CreateLogger(string categoryName)
{
return new XUnitLog(_Helper) { Name = categoryName };
}
public class XUnitLogProvider : ILoggerProvider
{
ITestOutputHelper _Helper;
public XUnitLogProvider(ITestOutputHelper helper)
{
_Helper = helper;
}
public ILogger CreateLogger(string categoryName)
{
return new XUnitLog(_Helper) { Name = categoryName };
}
public void Dispose()
{
}
}
public class XUnitLog : ILog, ILogger, IDisposable
{
ITestOutputHelper _Helper;
public XUnitLog(ITestOutputHelper helper)
{
_Helper = helper;
}
public void Dispose()
{
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)
{
return this;
}
public string Name
{
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)
{
StringBuilder builder = new StringBuilder();
builder.Append(formatter(state, exception));
if(exception != null)
{
builder.AppendLine();
builder.Append(exception.ToString());
}
LogInformation(builder.ToString());
}
public bool IsEnabled(LogLevel logLevel)
{
return true;
}
public void LogInformation(string msg)
{
if(msg != null)
_Helper.WriteLine(Name + ": " + msg);
}
}
public class Logs
{
public static ILog Tester
{
get; set;
}
public static XUnitLogProvider LogProvider
{
get;
set;
}
}
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
{
StringBuilder builder = new StringBuilder();
builder.Append(formatter(state, exception));
if (exception != null)
{
builder.AppendLine();
builder.Append(exception.ToString());
}
LogInformation(builder.ToString());
}
public void LogInformation(string msg)
{
if (msg != null)
_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
{
public class UrlHelperMock : IUrlHelper
{
Uri _BaseUrl;
public UrlHelperMock(Uri baseUrl)
{
_BaseUrl = baseUrl;
}
public ActionContext ActionContext => throw new NotImplementedException();
public class UrlHelperMock : IUrlHelper
{
Uri _BaseUrl;
public UrlHelperMock(Uri baseUrl)
{
_BaseUrl = baseUrl;
}
public ActionContext ActionContext => throw new NotImplementedException();
public string Action(UrlActionContext actionContext)
{
return $"{_BaseUrl}mock";
}
public string Action(UrlActionContext actionContext)
{
return $"{_BaseUrl}mock";
}
public string Content(string contentPath)
{
return $"{_BaseUrl}{contentPath}";
}
public string Content(string contentPath)
{
return $"{_BaseUrl}{contentPath}";
}
public bool IsLocalUrl(string url)
{
return false;
}
public bool IsLocalUrl(string url)
{
return false;
}
public string Link(string routeName, object values)
{
return _BaseUrl.AbsoluteUri;
}
public string Link(string routeName, object values)
{
return _BaseUrl.AbsoluteUri;
}
public string RouteUrl(UrlRouteContext routeContext)
{
return _BaseUrl.AbsoluteUri;
}
}
public string RouteUrl(UrlRouteContext routeContext)
{
return _BaseUrl.AbsoluteUri;
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -9,27 +9,27 @@ namespace BTCPayServer.Authentication
{
public class BitIdentity : IIdentity
{
public BitIdentity(PubKey key)
{
PubKey = key;
_Name = Encoders.Base58Check.EncodeData(Encoders.Hex.DecodeData("0f02" + key.Hash.ToString()));
SIN = NBitpayClient.Extensions.BitIdExtensions.GetBitIDSIN(key);
}
string _Name;
public BitIdentity(PubKey key)
{
PubKey = key;
_Name = Encoders.Base58Check.EncodeData(Encoders.Hex.DecodeData("0f02" + key.Hash.ToString()));
SIN = NBitpayClient.Extensions.BitIdExtensions.GetBitIDSIN(key);
}
string _Name;
public string SIN
{
get;
}
public PubKey PubKey
{
get;
}
public string SIN
{
get;
}
public PubKey PubKey
{
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 string Facade
{
get; set;
}
public string Value
{
get; set;
}
public string StoreId
{
get; set;
}
public string Label
{
get; set;
}
public DateTimeOffset PairingTime
{
get; set;
}
public string SIN
{
get;
set;
}
public string Facade
{
get; set;
}
public string Value
{
get; set;
}
public string StoreId
{
get; set;
}
public string Label
{
get; set;
}
public DateTimeOffset PairingTime
{
get; set;
}
public string SIN
{
get;
set;
}
public BitTokenEntity Clone(Facade facade)
{
return new BitTokenEntity()
{
Label = Label,
Facade = Facade,
StoreId = StoreId,
PairingTime = PairingTime,
SIN = SIN,
Value = Value
};
}
}
public BitTokenEntity Clone(Facade facade)
{
return new BitTokenEntity()
{
Label = Label,
Facade = Facade,
StoreId = StoreId,
PairingTime = PairingTime,
SIN = SIN,
Value = Value
};
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -13,88 +13,88 @@ using CommandLine;
namespace BTCPayServer.Configuration
{
public class DefaultConfiguration : StandardConfiguration.DefaultConfiguration
{
protected override CommandLineApplication CreateCommandLineApplicationCore()
{
CommandLineApplication app = new CommandLineApplication(true)
{
FullName = "NBXplorer\r\nLightweight block explorer for tracking HD wallets",
Name = "NBXplorer"
};
app.HelpOption("-? | -h | --help");
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("--regtest | -regtest", $"Use regtest", 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("--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);
public class DefaultConfiguration : StandardConfiguration.DefaultConfiguration
{
protected override CommandLineApplication CreateCommandLineApplicationCore()
{
CommandLineApplication app = new CommandLineApplication(true)
{
FullName = "NBXplorer\r\nLightweight block explorer for tracking HD wallets",
Name = "NBXplorer"
};
app.HelpOption("-? | -h | --help");
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("--regtest | -regtest", $"Use regtest", 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("--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);
return app;
}
return app;
}
public override string EnvironmentVariablePrefix => "BTCPAY_";
public override string EnvironmentVariablePrefix => "BTCPAY_";
protected override string GetDefaultDataDir(IConfiguration conf)
{
return GetNetwork(conf).DefaultDataDirectory;
}
protected override string GetDefaultDataDir(IConfiguration conf)
{
return GetNetwork(conf).DefaultDataDirectory;
}
protected override string GetDefaultConfigurationFile(IConfiguration conf)
{
var network = GetNetwork(conf);
var dataDir = conf["datadir"];
if(dataDir == null)
return network.DefaultConfigurationFile;
var fileName = Path.GetFileName(network.DefaultConfigurationFile);
return Path.Combine(dataDir, fileName);
}
protected override string GetDefaultConfigurationFile(IConfiguration conf)
{
var network = GetNetwork(conf);
var dataDir = conf["datadir"];
if (dataDir == null)
return network.DefaultConfigurationFile;
var fileName = Path.GetFileName(network.DefaultConfigurationFile);
return Path.Combine(dataDir, fileName);
}
public static NetworkInformation GetNetwork(IConfiguration conf)
{
var network = conf.GetOrDefault<string>("network", null);
if(network != null)
{
var info = NetworkInformation.GetNetworkByName(network);
if(info == null)
throw new ConfigException($"Invalid network name {network}");
return info;
}
public static NetworkInformation GetNetwork(IConfiguration conf)
{
var network = conf.GetOrDefault<string>("network", null);
if (network != null)
{
var info = NetworkInformation.GetNetworkByName(network);
if (info == null)
throw new ConfigException($"Invalid network name {network}");
return info;
}
var net = conf.GetOrDefault<bool>("regtest", false) ? Network.RegTest :
conf.GetOrDefault<bool>("testnet", false) ? Network.TestNet : Network.Main;
var net = conf.GetOrDefault<bool>("regtest", false) ? Network.RegTest :
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)
{
var network = GetNetwork(conf);
StringBuilder builder = new StringBuilder();
builder.AppendLine("### Global settings ###");
builder.AppendLine("#testnet=0");
builder.AppendLine("#regtest=0");
builder.AppendLine();
builder.AppendLine("### Server settings ###");
builder.AppendLine("#requirehttps=0");
builder.AppendLine("#port=" + network.DefaultPort);
builder.AppendLine("#bind=127.0.0.1");
builder.AppendLine();
builder.AppendLine("### Database ###");
builder.AppendLine("#postgres=User ID=root;Password=myPassword;Host=localhost;Port=5432;Database=myDataBase;");
builder.AppendLine();
builder.AppendLine("### NBXplorer settings ###");
builder.AppendLine("#explorer.url=" + network.DefaultExplorerUrl.AbsoluteUri);
builder.AppendLine("#explorer.cookiefile=" + network.DefaultExplorerCookieFile);
return builder.ToString();
}
protected override string GetDefaultConfigurationFileTemplate(IConfiguration conf)
{
var network = GetNetwork(conf);
StringBuilder builder = new StringBuilder();
builder.AppendLine("### Global settings ###");
builder.AppendLine("#testnet=0");
builder.AppendLine("#regtest=0");
builder.AppendLine();
builder.AppendLine("### Server settings ###");
builder.AppendLine("#requirehttps=0");
builder.AppendLine("#port=" + network.DefaultPort);
builder.AppendLine("#bind=127.0.0.1");
builder.AppendLine();
builder.AppendLine("### Database ###");
builder.AppendLine("#postgres=User ID=root;Password=myPassword;Host=localhost;Port=5432;Database=myDataBase;");
builder.AppendLine();
builder.AppendLine("### NBXplorer settings ###");
builder.AppendLine("#explorer.url=" + network.DefaultExplorerUrl.AbsoluteUri);
builder.AppendLine("#explorer.cookiefile=" + network.DefaultExplorerCookieFile);
return builder.ToString();
}
protected override IPEndPoint GetDefaultEndpoint(IConfiguration conf)
{
return new IPEndPoint(IPAddress.Parse("127.0.0.1"), GetNetwork(conf).DefaultPort);
}
}
protected override IPEndPoint GetDefaultEndpoint(IConfiguration conf)
{
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
{
public class NetworkInformation
{
static NetworkInformation()
{
_Networks = new Dictionary<string, NetworkInformation>();
foreach(var network in Network.GetNetworks())
{
NetworkInformation info = new NetworkInformation();
info.DefaultDataDirectory = StandardConfiguration.DefaultDataDirectory.GetDirectory("BTCPayServer", network.Name);
info.DefaultConfigurationFile = Path.Combine(info.DefaultDataDirectory, "settings.config");
info.DefaultExplorerCookieFile = Path.Combine(StandardConfiguration.DefaultDataDirectory.GetDirectory("NBXplorer", network.Name, false), ".cookie");
info.Network = network;
info.DefaultExplorerUrl = new Uri("http://127.0.0.1:24446", UriKind.Absolute);
info.DefaultPort = 23002;
_Networks.Add(network.Name, info);
if(network == Network.Main)
{
info.DefaultExplorerUrl = new Uri("http://127.0.0.1:24444", UriKind.Absolute);
Main = info;
info.DefaultPort = 23000;
}
if(network == Network.TestNet)
{
info.DefaultExplorerUrl = new Uri("http://127.0.0.1:24445", UriKind.Absolute);
info.DefaultPort = 23001;
}
}
}
public class NetworkInformation
{
static NetworkInformation()
{
_Networks = new Dictionary<string, NetworkInformation>();
foreach (var network in Network.GetNetworks())
{
NetworkInformation info = new NetworkInformation();
info.DefaultDataDirectory = StandardConfiguration.DefaultDataDirectory.GetDirectory("BTCPayServer", network.Name);
info.DefaultConfigurationFile = Path.Combine(info.DefaultDataDirectory, "settings.config");
info.DefaultExplorerCookieFile = Path.Combine(StandardConfiguration.DefaultDataDirectory.GetDirectory("NBXplorer", network.Name, false), ".cookie");
info.Network = network;
info.DefaultExplorerUrl = new Uri("http://127.0.0.1:24446", UriKind.Absolute);
info.DefaultPort = 23002;
_Networks.Add(network.Name, info);
if (network == Network.Main)
{
info.DefaultExplorerUrl = new Uri("http://127.0.0.1:24444", UriKind.Absolute);
Main = info;
info.DefaultPort = 23000;
}
if (network == Network.TestNet)
{
info.DefaultExplorerUrl = new Uri("http://127.0.0.1:24445", UriKind.Absolute);
info.DefaultPort = 23001;
}
}
}
static Dictionary<string, NetworkInformation> _Networks;
public static NetworkInformation GetNetworkByName(string name)
{
var value = _Networks.TryGet(name);
if(value != null)
return value;
static Dictionary<string, NetworkInformation> _Networks;
public static NetworkInformation GetNetworkByName(string name)
{
var value = _Networks.TryGet(name);
if (value != null)
return value;
//Maybe alias ?
var network = Network.GetNetwork(name);
if(network != null)
{
value = _Networks.TryGet(network.Name);
if(value != null)
return value;
}
return null;
}
//Maybe alias ?
var network = Network.GetNetwork(name);
if (network != null)
{
value = _Networks.TryGet(network.Name);
if (value != null)
return value;
}
return null;
}
public static NetworkInformation Main
{
get;
set;
}
public Network Network
{
get; set;
}
public string DefaultConfigurationFile
{
get;
set;
}
public string DefaultDataDirectory
{
get;
set;
}
public Uri DefaultExplorerUrl
{
get;
internal set;
}
public int DefaultPort
{
get;
private set;
}
public string DefaultExplorerCookieFile
{
get;
internal set;
}
public static NetworkInformation Main
{
get;
set;
}
public Network Network
{
get; set;
}
public string DefaultConfigurationFile
{
get;
set;
}
public string DefaultDataDirectory
{
get;
set;
}
public Uri DefaultExplorerUrl
{
get;
internal set;
}
public int DefaultPort
{
get;
private set;
}
public string DefaultExplorerCookieFile
{
get;
internal set;
}
public override string ToString()
{
return Network.ToString();
}
public override string ToString()
{
return Network.ToString();
}
public static string ToStringAll()
{
return string.Join(", ", _Networks.Select(n => n.Key).ToArray());
}
}
public static string ToStringAll()
{
return string.Join(", ", _Networks.Select(n => n.Key).ToArray());
}
}
}

View file

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

View file

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

View file

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

View file

@ -16,123 +16,123 @@ using BTCPayServer.Services.Stores;
namespace BTCPayServer.Controllers
{
[EnableCors("BitpayAPI")]
[BitpayAPIConstraint]
public class InvoiceControllerAPI : Controller
[EnableCors("BitpayAPI")]
[BitpayAPIConstraint]
public class InvoiceControllerAPI : Controller
{
private InvoiceController _InvoiceController;
private InvoiceRepository _InvoiceRepository;
private TokenRepository _TokenRepository;
private StoreRepository _StoreRepository;
private InvoiceController _InvoiceController;
private InvoiceRepository _InvoiceRepository;
private TokenRepository _TokenRepository;
private StoreRepository _StoreRepository;
public InvoiceControllerAPI(InvoiceController invoiceController,
InvoiceRepository invoceRepository,
TokenRepository tokenRepository,
StoreRepository storeRepository)
{
this._InvoiceController = invoiceController;
this._InvoiceRepository = invoceRepository;
this._TokenRepository = tokenRepository;
this._StoreRepository = storeRepository;
}
public InvoiceControllerAPI(InvoiceController invoiceController,
InvoiceRepository invoceRepository,
TokenRepository tokenRepository,
StoreRepository storeRepository)
{
this._InvoiceController = invoiceController;
this._InvoiceRepository = invoceRepository;
this._TokenRepository = tokenRepository;
this._StoreRepository = storeRepository;
}
[HttpPost]
[Route("invoices")]
[MediaTypeConstraint("application/json")]
public async Task<DataWrapper<InvoiceResponse>> CreateInvoice([FromBody] Invoice invoice)
{
var bitToken = await CheckTokenPermissionAsync(Facade.Merchant, invoice.Token);
var store = await FindStore(bitToken);
return await _InvoiceController.CreateInvoiceCore(invoice, store, HttpContext.Request.GetAbsoluteRoot());
}
[HttpPost]
[Route("invoices")]
[MediaTypeConstraint("application/json")]
public async Task<DataWrapper<InvoiceResponse>> CreateInvoice([FromBody] Invoice invoice)
{
var bitToken = await CheckTokenPermissionAsync(Facade.Merchant, invoice.Token);
var store = await FindStore(bitToken);
return await _InvoiceController.CreateInvoiceCore(invoice, store, HttpContext.Request.GetAbsoluteRoot());
}
[HttpGet]
[Route("invoices/{id}")]
public async Task<DataWrapper<InvoiceResponse>> GetInvoice(string id, string token)
{
var bitToken = await CheckTokenPermissionAsync(Facade.Merchant, token);
var store = await FindStore(bitToken);
var invoice = await _InvoiceRepository.GetInvoice(store.Id, id);
if(invoice == null)
throw new BitpayHttpException(404, "Object not found");
[HttpGet]
[Route("invoices/{id}")]
public async Task<DataWrapper<InvoiceResponse>> GetInvoice(string id, string token)
{
var bitToken = await CheckTokenPermissionAsync(Facade.Merchant, token);
var store = await FindStore(bitToken);
var invoice = await _InvoiceRepository.GetInvoice(store.Id, id);
if (invoice == null)
throw new BitpayHttpException(404, "Object not found");
var resp = invoice.EntityToDTO();
return new DataWrapper<InvoiceResponse>(resp);
}
var resp = invoice.EntityToDTO();
return new DataWrapper<InvoiceResponse>(resp);
}
[HttpGet]
[Route("invoices")]
public async Task<DataWrapper<InvoiceResponse[]>> GetInvoices(
string token,
DateTimeOffset? dateStart = null,
DateTimeOffset? dateEnd = null,
string orderId = null,
string itemCode = null,
string status = null,
int? limit = null,
int? offset = null)
{
if(dateEnd != null)
dateEnd = dateEnd.Value + TimeSpan.FromDays(1); //Should include the end day
var bitToken = await CheckTokenPermissionAsync(Facade.Merchant, token);
var store = await FindStore(bitToken);
var query = new InvoiceQuery()
{
Count = limit,
Skip = offset,
EndDate = dateEnd,
StartDate = dateStart,
OrderId = orderId,
ItemCode = itemCode,
Status = status,
StoreId = store.Id
};
[HttpGet]
[Route("invoices")]
public async Task<DataWrapper<InvoiceResponse[]>> GetInvoices(
string token,
DateTimeOffset? dateStart = null,
DateTimeOffset? dateEnd = null,
string orderId = null,
string itemCode = null,
string status = null,
int? limit = null,
int? offset = null)
{
if (dateEnd != null)
dateEnd = dateEnd.Value + TimeSpan.FromDays(1); //Should include the end day
var bitToken = await CheckTokenPermissionAsync(Facade.Merchant, token);
var store = await FindStore(bitToken);
var query = new InvoiceQuery()
{
Count = limit,
Skip = offset,
EndDate = dateEnd,
StartDate = dateStart,
OrderId = orderId,
ItemCode = itemCode,
Status = status,
StoreId = store.Id
};
var entities = (await _InvoiceRepository.GetInvoices(query))
.Select((o) => o.EntityToDTO()).ToArray();
var entities = (await _InvoiceRepository.GetInvoices(query))
.Select((o) => o.EntityToDTO()).ToArray();
return DataWrapper.Create(entities);
}
return DataWrapper.Create(entities);
}
private async Task<BitTokenEntity> CheckTokenPermissionAsync(Facade facade, string expectedToken)
{
if(facade == null)
throw new ArgumentNullException(nameof(facade));
private async Task<BitTokenEntity> CheckTokenPermissionAsync(Facade facade, string expectedToken)
{
if (facade == null)
throw new ArgumentNullException(nameof(facade));
var actualTokens = (await _TokenRepository.GetTokens(this.GetBitIdentity().SIN)).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;
}
var actualTokens = (await _TokenRepository.GetTokens(this.GetBitIdentity().SIN)).ToArray();
actualTokens = actualTokens.SelectMany(t => GetCompatibleTokens(t)).ToArray();
private IEnumerable<BitTokenEntity> GetCompatibleTokens(BitTokenEntity token)
{
if(token.Facade == Facade.Merchant.ToString())
{
yield return token.Clone(Facade.User);
yield return token.Clone(Facade.PointOfSale);
}
if(token.Facade == Facade.PointOfSale.ToString())
{
yield return token.Clone(Facade.User);
}
yield return token;
}
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 async Task<StoreData> FindStore(BitTokenEntity bitToken)
{
var store = await _StoreRepository.FindStore(bitToken.StoreId);
if(store == null)
throw new BitpayHttpException(401, "Unknown store");
return store;
}
private IEnumerable<BitTokenEntity> GetCompatibleTokens(BitTokenEntity token)
{
if (token.Facade == Facade.Merchant.ToString())
{
yield return token.Clone(Facade.User);
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
{
[HttpGet]
[Route("i/{invoiceId}")]
[AcceptMediaTypeConstraint("application/bitcoin-paymentrequest")]
public async Task<IActionResult> GetInvoiceRequest(string invoiceId)
{
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
if(invoice == null || invoice.IsExpired())
return NotFound();
[HttpGet]
[Route("i/{invoiceId}")]
[AcceptMediaTypeConstraint("application/bitcoin-paymentrequest")]
public async Task<IActionResult> GetInvoiceRequest(string invoiceId)
{
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
if (invoice == null || invoice.IsExpired())
return NotFound();
var dto = invoice.EntityToDTO();
PaymentRequest request = new PaymentRequest
{
DetailsVersion = 1
};
request.Details.Expires = invoice.ExpirationTime;
request.Details.Memo = invoice.ProductInformation.ItemDesc;
request.Details.Network = _Network;
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.Time = DateTimeOffset.UtcNow;
request.Details.PaymentUrl = new Uri(invoice.ServerUrl.WithTrailingSlash() + ($"i/{invoice.Id}"), UriKind.Absolute);
var dto = invoice.EntityToDTO();
PaymentRequest request = new PaymentRequest
{
DetailsVersion = 1
};
request.Details.Expires = invoice.ExpirationTime;
request.Details.Memo = invoice.ProductInformation.ItemDesc;
request.Details.Network = _Network;
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.Time = DateTimeOffset.UtcNow;
request.Details.PaymentUrl = new Uri(invoice.ServerUrl.WithTrailingSlash() + ($"i/{invoice.Id}"), UriKind.Absolute);
var store = await _StoreRepository.FindStore(invoice.StoreId);
if(store == null)
throw new BitpayHttpException(401, "Unknown store");
var store = await _StoreRepository.FindStore(invoice.StoreId);
if (store == null)
throw new BitpayHttpException(401, "Unknown store");
if(store.StoreCertificate != null)
{
try
{
request.Sign(store.StoreCertificate, PKIType.X509SHA256);
}
catch(Exception ex)
{
Logs.PayServer.LogWarning(ex, "Error while signing payment request");
}
}
if (store.StoreCertificate != null)
{
try
{
request.Sign(store.StoreCertificate, PKIType.X509SHA256);
}
catch (Exception ex)
{
Logs.PayServer.LogWarning(ex, "Error while signing payment request");
}
}
return new PaymentRequestActionResult(request);
}
return new PaymentRequestActionResult(request);
}
[HttpPost]
[Route("i/{invoiceId}", Order = 99)]
[MediaTypeConstraint("application/bitcoin-payment")]
public async Task<IActionResult> PostPayment(string invoiceId)
{
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
if(invoice == null || invoice.IsExpired())
return NotFound();
var payment = PaymentMessage.Load(Request.Body);
var unused = _Wallet.BroadcastTransactionsAsync(payment.Transactions);
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..."));
}
}
[HttpPost]
[Route("i/{invoiceId}", Order = 99)]
[MediaTypeConstraint("application/bitcoin-payment")]
public async Task<IActionResult> PostPayment(string invoiceId)
{
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
if (invoice == null || invoice.IsExpired())
return NotFound();
var payment = PaymentMessage.Load(Request.Body);
var unused = _Wallet.BroadcastTransactionsAsync(payment.Transactions);
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..."));
}
}
}

View file

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

View file

@ -41,119 +41,119 @@ using NBXplorer;
namespace BTCPayServer.Controllers
{
public partial class InvoiceController : Controller
{
InvoiceRepository _InvoiceRepository;
BTCPayWallet _Wallet;
IRateProvider _RateProvider;
private InvoiceWatcher _Watcher;
StoreRepository _StoreRepository;
Network _Network;
UserManager<ApplicationUser> _UserManager;
IFeeProvider _FeeProvider;
ExplorerClient _Explorer;
public partial class InvoiceController : Controller
{
InvoiceRepository _InvoiceRepository;
BTCPayWallet _Wallet;
IRateProvider _RateProvider;
private InvoiceWatcher _Watcher;
StoreRepository _StoreRepository;
Network _Network;
UserManager<ApplicationUser> _UserManager;
IFeeProvider _FeeProvider;
ExplorerClient _Explorer;
public InvoiceController(
Network network,
InvoiceRepository invoiceRepository,
UserManager<ApplicationUser> userManager,
BTCPayWallet wallet,
IRateProvider rateProvider,
StoreRepository storeRepository,
InvoiceWatcher watcher,
ExplorerClient explorerClient,
IFeeProvider feeProvider)
{
_Explorer = explorerClient ?? throw new ArgumentNullException(nameof(explorerClient));
_StoreRepository = storeRepository ?? throw new ArgumentNullException(nameof(storeRepository));
_Network = network ?? throw new ArgumentNullException(nameof(network));
_InvoiceRepository = invoiceRepository ?? throw new ArgumentNullException(nameof(invoiceRepository));
_Wallet = wallet ?? throw new ArgumentNullException(nameof(wallet));
_RateProvider = rateProvider ?? throw new ArgumentNullException(nameof(rateProvider));
_Watcher = watcher ?? throw new ArgumentNullException(nameof(watcher));
_UserManager = userManager;
_FeeProvider = feeProvider ?? throw new ArgumentNullException(nameof(feeProvider));
}
public InvoiceController(
Network network,
InvoiceRepository invoiceRepository,
UserManager<ApplicationUser> userManager,
BTCPayWallet wallet,
IRateProvider rateProvider,
StoreRepository storeRepository,
InvoiceWatcher watcher,
ExplorerClient explorerClient,
IFeeProvider feeProvider)
{
_Explorer = explorerClient ?? throw new ArgumentNullException(nameof(explorerClient));
_StoreRepository = storeRepository ?? throw new ArgumentNullException(nameof(storeRepository));
_Network = network ?? throw new ArgumentNullException(nameof(network));
_InvoiceRepository = invoiceRepository ?? throw new ArgumentNullException(nameof(invoiceRepository));
_Wallet = wallet ?? throw new ArgumentNullException(nameof(wallet));
_RateProvider = rateProvider ?? throw new ArgumentNullException(nameof(rateProvider));
_Watcher = watcher ?? throw new ArgumentNullException(nameof(watcher));
_UserManager = userManager;
_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)
{
//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 entity = new InvoiceEntity
{
InvoiceTime = DateTimeOffset.UtcNow,
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;
if(notificationUri == null || (notificationUri.Scheme != "http" && notificationUri.Scheme != "https")) //TODO: Filer non routable addresses ?
notificationUri = null;
EmailAddressAttribute emailValidator = new EmailAddressAttribute();
entity.ExpirationTime = entity.InvoiceTime.AddMinutes(expiryMinutes);
entity.MonitoringExpiration = entity.InvoiceTime.AddMinutes(monitoringMinutes);
entity.OrderId = invoice.OrderId;
entity.ServerUrl = serverUrl;
entity.FullNotifications = invoice.FullNotifications;
entity.NotificationURL = notificationUri?.AbsoluteUri;
entity.BuyerInformation = Map<Invoice, BuyerInformation>(invoice);
//Another way of passing buyer info to support
FillBuyerInfo(invoice.Buyer, entity.BuyerInformation);
if(entity?.BuyerInformation?.BuyerEmail != null)
{
if(!EmailValidator.IsEmail(entity.BuyerInformation.BuyerEmail))
throw new BitpayHttpException(400, "Invalid email");
entity.RefundMail = entity.BuyerInformation.BuyerEmail;
}
entity.ProductInformation = Map<Invoice, ProductInformation>(invoice);
entity.RedirectURL = invoice.RedirectURL ?? store.StoreWebsite;
entity.Status = "new";
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.Rate = (double)await _RateProvider.GetRateAsync(invoice.Currency);
entity.PosData = invoice.PosData;
entity.DepositAddress = await _Wallet.ReserveAddressAsync(ParseDerivationStrategy(derivationStrategy));
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
var derivationStrategy = store.DerivationStrategy;
var entity = new InvoiceEntity
{
InvoiceTime = DateTimeOffset.UtcNow,
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;
if (notificationUri == null || (notificationUri.Scheme != "http" && notificationUri.Scheme != "https")) //TODO: Filer non routable addresses ?
notificationUri = null;
EmailAddressAttribute emailValidator = new EmailAddressAttribute();
entity.ExpirationTime = entity.InvoiceTime.AddMinutes(expiryMinutes);
entity.MonitoringExpiration = entity.InvoiceTime.AddMinutes(monitoringMinutes);
entity.OrderId = invoice.OrderId;
entity.ServerUrl = serverUrl;
entity.FullNotifications = invoice.FullNotifications;
entity.NotificationURL = notificationUri?.AbsoluteUri;
entity.BuyerInformation = Map<Invoice, BuyerInformation>(invoice);
//Another way of passing buyer info to support
FillBuyerInfo(invoice.Buyer, entity.BuyerInformation);
if (entity?.BuyerInformation?.BuyerEmail != null)
{
if (!EmailValidator.IsEmail(entity.BuyerInformation.BuyerEmail))
throw new BitpayHttpException(400, "Invalid email");
entity.RefundMail = entity.BuyerInformation.BuyerEmail;
}
entity.ProductInformation = Map<Invoice, ProductInformation>(invoice);
entity.RedirectURL = invoice.RedirectURL ?? store.StoreWebsite;
entity.Status = "new";
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.Rate = (double)await _RateProvider.GetRateAsync(invoice.Currency);
entity.PosData = invoice.PosData;
entity.DepositAddress = await _Wallet.ReserveAddressAsync(ParseDerivationStrategy(derivationStrategy));
entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity);
await _Watcher.WatchAsync(entity.Id);
var resp = entity.EntityToDTO();
return new DataWrapper<InvoiceResponse>(resp) { Facade = "pos/invoice" };
}
entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity);
await _Watcher.WatchAsync(entity.Id);
var resp = entity.EntityToDTO();
return new DataWrapper<InvoiceResponse>(resp) { Facade = "pos/invoice" };
}
private SpeedPolicy ParseSpeedPolicy(string transactionSpeed, SpeedPolicy defaultPolicy)
{
if(transactionSpeed == null)
return defaultPolicy;
var mappings = new Dictionary<string, SpeedPolicy>();
mappings.Add("low", SpeedPolicy.LowSpeed);
mappings.Add("medium", SpeedPolicy.MediumSpeed);
mappings.Add("high", SpeedPolicy.HighSpeed);
if(!mappings.TryGetValue(transactionSpeed, out SpeedPolicy policy))
policy = defaultPolicy;
return policy;
}
private SpeedPolicy ParseSpeedPolicy(string transactionSpeed, SpeedPolicy defaultPolicy)
{
if (transactionSpeed == null)
return defaultPolicy;
var mappings = new Dictionary<string, SpeedPolicy>();
mappings.Add("low", SpeedPolicy.LowSpeed);
mappings.Add("medium", SpeedPolicy.MediumSpeed);
mappings.Add("high", SpeedPolicy.HighSpeed);
if (!mappings.TryGetValue(transactionSpeed, out SpeedPolicy policy))
policy = defaultPolicy;
return policy;
}
private void FillBuyerInfo(Buyer buyer, BuyerInformation buyerInformation)
{
if(buyer == null)
return;
buyerInformation.BuyerAddress1 = buyerInformation.BuyerAddress1 ?? buyer.Address1;
buyerInformation.BuyerAddress2 = buyerInformation.BuyerAddress2 ?? buyer.Address2;
buyerInformation.BuyerCity = buyerInformation.BuyerCity ?? buyer.City;
buyerInformation.BuyerCountry = buyerInformation.BuyerCountry ?? buyer.country;
buyerInformation.BuyerEmail = buyerInformation.BuyerEmail ?? buyer.email;
buyerInformation.BuyerName = buyerInformation.BuyerName ?? buyer.Name;
buyerInformation.BuyerPhone = buyerInformation.BuyerPhone ?? buyer.phone;
buyerInformation.BuyerState = buyerInformation.BuyerState ?? buyer.State;
buyerInformation.BuyerZip = buyerInformation.BuyerZip ?? buyer.zip;
}
private void FillBuyerInfo(Buyer buyer, BuyerInformation buyerInformation)
{
if (buyer == null)
return;
buyerInformation.BuyerAddress1 = buyerInformation.BuyerAddress1 ?? buyer.Address1;
buyerInformation.BuyerAddress2 = buyerInformation.BuyerAddress2 ?? buyer.Address2;
buyerInformation.BuyerCity = buyerInformation.BuyerCity ?? buyer.City;
buyerInformation.BuyerCountry = buyerInformation.BuyerCountry ?? buyer.country;
buyerInformation.BuyerEmail = buyerInformation.BuyerEmail ?? buyer.email;
buyerInformation.BuyerName = buyerInformation.BuyerName ?? buyer.Name;
buyerInformation.BuyerPhone = buyerInformation.BuyerPhone ?? buyer.phone;
buyerInformation.BuyerState = buyerInformation.BuyerState ?? buyer.State;
buyerInformation.BuyerZip = buyerInformation.BuyerZip ?? buyer.zip;
}
private DerivationStrategyBase ParseDerivationStrategy(string derivationStrategy)
{
return new DerivationStrategyFactory(_Network).Parse(derivationStrategy);
}
private DerivationStrategyBase ParseDerivationStrategy(string derivationStrategy)
{
return new DerivationStrategyFactory(_Network).Parse(derivationStrategy);
}
private TDest Map<TFrom, TDest>(TFrom data)
{
return JsonConvert.DeserializeObject<TDest>(JsonConvert.SerializeObject(data));
}
}
private TDest Map<TFrom, TDest>(TFrom 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
{
public class PaymentRequestActionResult : IActionResult
{
PaymentRequest req;
public PaymentRequestActionResult(PaymentRequest req)
{
this.req = req;
}
public Task ExecuteResultAsync(ActionContext context)
{
context.HttpContext.Response.Headers["Content-Transfer-Encoding"] = "binary";
context.HttpContext.Response.ContentType = "application/bitcoin-paymentrequest";
req.WriteTo(context.HttpContext.Response.Body);
return Task.CompletedTask;
}
}
public class PaymentAckActionResult : IActionResult
{
PaymentACK req;
public PaymentAckActionResult(PaymentACK req)
{
this.req = req;
}
public Task ExecuteResultAsync(ActionContext context)
{
context.HttpContext.Response.Headers["Content-Transfer-Encoding"] = "binary";
context.HttpContext.Response.ContentType = "application/bitcoin-paymentack";
req.WriteTo(context.HttpContext.Response.Body);
return Task.CompletedTask;
}
}
public class PaymentRequestActionResult : IActionResult
{
PaymentRequest req;
public PaymentRequestActionResult(PaymentRequest req)
{
this.req = req;
}
public Task ExecuteResultAsync(ActionContext context)
{
context.HttpContext.Response.Headers["Content-Transfer-Encoding"] = "binary";
context.HttpContext.Response.ContentType = "application/bitcoin-paymentrequest";
req.WriteTo(context.HttpContext.Response.Body);
return Task.CompletedTask;
}
}
public class PaymentAckActionResult : IActionResult
{
PaymentACK req;
public PaymentAckActionResult(PaymentACK req)
{
this.req = req;
}
public Task ExecuteResultAsync(ActionContext context)
{
context.HttpContext.Response.Headers["Content-Transfer-Encoding"] = "binary";
context.HttpContext.Response.ContentType = "application/bitcoin-paymentack";
req.WriteTo(context.HttpContext.Response.Body);
return Task.CompletedTask;
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -7,44 +7,44 @@ namespace BTCPayServer.Data
{
public class PairingCodeData
{
public string Id
{
get; set;
}
public string Facade
{
get; set;
}
public string StoreDataId
{
get; set;
}
public DateTimeOffset Expiration
{
get;
set;
}
public string Id
{
get; set;
}
public string Label
{
get;
set;
}
public string SIN
{
get;
set;
}
public DateTime DateCreated
{
get;
set;
}
public string TokenValue
{
get;
set;
}
}
public string Facade
{
get; set;
}
public string StoreDataId
{
get; set;
}
public DateTimeOffset Expiration
{
get;
set;
}
public string Label
{
get;
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
{
public class PaymentData
{
public string Id
{
get; set;
}
public class PaymentData
{
public string Id
{
get; set;
}
public string InvoiceDataId
{
get; set;
}
public InvoiceData InvoiceData
{
get; set;
}
public string InvoiceDataId
{
get; set;
}
public InvoiceData InvoiceData
{
get; set;
}
public byte[] Blob
{
get; set;
}
}
public byte[] Blob
{
get; set;
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -8,9 +8,9 @@ namespace BTCPayServer.Models.AccountViewModels
{
public class LoginWithRecoveryCodeViewModel
{
[Required]
[DataType(DataType.Text)]
[Display(Name = "Recovery Code")]
public string RecoveryCode { get; set; }
[Required]
[DataType(DataType.Text)]
[Display(Name = "Recovery Code")]
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
public class ApplicationUser : IdentityUser
{
public List<UserStore> UserStores
{
get;
set;
}
public List<UserStore> UserStores
{
get;
set;
}
public bool RequiresEmailConfirmation
{
get; set;
}
}
public bool RequiresEmailConfirmation
{
get; set;
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -9,15 +9,15 @@ namespace BTCPayServer.Models.ManageViewModels
{
public class EnableAuthenticatorViewModel
{
[Required]
[StringLength(7, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
[DataType(DataType.Text)]
[Display(Name = "Verification Code")]
public string Code { get; set; }
[Required]
[StringLength(7, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
[DataType(DataType.Text)]
[Display(Name = "Verification Code")]
public string Code { get; set; }
[ReadOnly(true)]
public string SharedKey { get; set; }
[ReadOnly(true)]
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; }
[Required]
[EmailAddress]
[MaxLength(50)]
public string Email
{
get; set;
}
[Required]
[EmailAddress]
[MaxLength(50)]
public string Email
{
get; set;
}
public bool IsEmailConfirmed { get; set; }
public bool IsEmailConfirmed { get; set; }
[Phone]
[Phone]
[Display(Name = "Phone number")]
[MaxLength(50)]
public string PhoneNumber { get; set; }
[MaxLength(50)]
public string PhoneNumber { get; set; }
public string StatusMessage
{
get; set;
}
public string StatusMessage
{
get; set;
}
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -8,65 +8,65 @@ using System.Threading.Tasks;
namespace BTCPayServer.Models.StoreViewModels
{
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 class CreateTokenViewModel
{
public TokenViewModel[] Tokens
{
get; set;
}
public string StatusMessage
{
get;
set;
}
}
[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
{
get; set;
}
public string StatusMessage
{
get;
set;
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -7,34 +7,34 @@ using System.Threading.Tasks;
namespace BTCPayServer.Services.Mails
{
// 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
public class EmailSender : IEmailSender
{
IBackgroundJobClient _JobClient;
SettingsRepository _Repository;
public EmailSender(IBackgroundJobClient jobClient, SettingsRepository repository)
{
if(jobClient == null)
throw new ArgumentNullException(nameof(jobClient));
_JobClient = jobClient;
_Repository = repository;
}
public Task SendEmailAsync(string email, string subject, string message)
{
_JobClient.Schedule(() => SendMailCore(email, subject, message), TimeSpan.Zero);
return Task.CompletedTask;
}
// 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
public class EmailSender : IEmailSender
{
IBackgroundJobClient _JobClient;
SettingsRepository _Repository;
public EmailSender(IBackgroundJobClient jobClient, SettingsRepository repository)
{
if (jobClient == null)
throw new ArgumentNullException(nameof(jobClient));
_JobClient = jobClient;
_Repository = repository;
}
public Task SendEmailAsync(string email, string subject, string message)
{
_JobClient.Schedule(() => SendMailCore(email, subject, message), TimeSpan.Zero);
return Task.CompletedTask;
}
public async Task SendMailCore(string email, string subject, string message)
{
var settings = await _Repository.GetSettingAsync<EmailSettings>();
if(settings == null)
throw new InvalidOperationException("Email settings not configured");
var smtp = settings.CreateSmtpClient();
MailMessage mail = new MailMessage(settings.From, email, subject, message);
mail.IsBodyHtml = true;
await smtp.SendMailAsync(mail);
}
}
public async Task SendMailCore(string email, string subject, string message)
{
var settings = await _Repository.GetSettingAsync<EmailSettings>();
if (settings == null)
throw new InvalidOperationException("Email settings not configured");
var smtp = settings.CreateSmtpClient();
MailMessage mail = new MailMessage(settings.From, email, subject, message);
mail.IsBodyHtml = true;
await smtp.SendMailAsync(mail);
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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