Compare commits
339 Commits
Author | SHA1 | Date | |
---|---|---|---|
a6b92a0dd5 | |||
2f3238c65e | |||
65f5a38b4a | |||
271cbf682f | |||
a634593903 | |||
af94de93d1 | |||
35f669aa15 | |||
4795bd8108 | |||
29aed99fd1 | |||
aa4519ac30 | |||
752133b01c | |||
fe0c21ba08 | |||
90904a6b5e | |||
8e3f7ea68d | |||
a239104a28 | |||
3bc232e1da | |||
a1ee09cd85 | |||
b898cc030c | |||
0602353dd2 | |||
9d406923ae | |||
aa8565e3cc | |||
5de330b1f9 | |||
66597aed46 | |||
3071826f06 | |||
c3684eb064 | |||
335dd9e66d | |||
cd1611dbcd | |||
c17793aca9 | |||
01d898b618 | |||
17069c311b | |||
921d072942 | |||
6181e8b3e4 | |||
93fc12bb2e | |||
8e73c1a2f0 | |||
e97c15578d | |||
fd4f4e6aff | |||
cedf8f75e8 | |||
cd0a650df4 | |||
6d6b9e2ba6 | |||
fd915fdc5c | |||
59be813fe9 | |||
465fbdd47f | |||
f220abb716 | |||
db46ca87d7 | |||
d873a1a545 | |||
a464a8702b | |||
63722b932a | |||
698b3c46cd | |||
df81051d07 | |||
ac70a77361 | |||
59a2432af9 | |||
ea4fa8d5d4 | |||
ade3eff75c | |||
db2a2a2b6c | |||
579dcb5af8 | |||
69247dee8a | |||
7b9541b8e9 | |||
a12e4d7f64 | |||
897da9b07a | |||
293525d480 | |||
198e810355 | |||
fe25e00c94 | |||
8b129ab2e5 | |||
770bed54d1 | |||
774817d4ac | |||
b8068b2ae8 | |||
3007a6bbc8 | |||
1c0c8fece2 | |||
c52eee47f0 | |||
f88c98b9d9 | |||
b0e9e10f7e | |||
39d47e33f6 | |||
4b7b6c6327 | |||
a59edc5e8c | |||
5ba322ea6a | |||
b47b4b10cb | |||
c88f391935 | |||
26d3178e93 | |||
1ad27c7827 | |||
4200a8eed5 | |||
daceb7af8e | |||
f703b53bce | |||
2762224f0f | |||
86fc64d184 | |||
726cd6fd49 | |||
be1c4666e0 | |||
b75dfc4191 | |||
97815f8daf | |||
af16e1db69 | |||
5f6913b3a2 | |||
2b31af80cb | |||
3f9889d374 | |||
f8189c64a4 | |||
c9b5f89f17 | |||
ecb82f2cc9 | |||
f340c6eb7f | |||
ba0e080816 | |||
bb3d107309 | |||
8517b222bf | |||
aed32204b5 | |||
6b4eeff3f1 | |||
e3cc589ebb | |||
4a152e8ffc | |||
d54a9474d1 | |||
98472211fc | |||
099c9fa1f9 | |||
5226b77ffc | |||
290779ee39 | |||
4f39a8060c | |||
92caa98dfb | |||
df7bb9e2f8 | |||
02a039d695 | |||
b5e4c803aa | |||
2b7c70622f | |||
88779e7129 | |||
6beb7abfd2 | |||
a1ebedc0d1 | |||
d5ad0cdb39 | |||
39fb8dbb6a | |||
58194cb060 | |||
ef165e15bf | |||
bf871c46ec | |||
52331e057f | |||
b59021a0be | |||
8596e16feb | |||
223558c01d | |||
3a91965187 | |||
55d50af39d | |||
3ff293ab7f | |||
7bcf2b5472 | |||
983f34814f | |||
a33e20b46b | |||
bafdcb04ed | |||
cb4468d3b3 | |||
de6f0008a6 | |||
7618eacef1 | |||
842e083ebe | |||
1c510df3fc | |||
c80ffe396e | |||
401a31e5c2 | |||
2df60bd121 | |||
6d10c8a6c1 | |||
44898b5e23 | |||
c0f53db561 | |||
133fb96d28 | |||
98b7ad62af | |||
3069fe0dd9 | |||
729555b96f | |||
b4040ba7ad | |||
863752a471 | |||
6ae9d13c43 | |||
0c735f4e29 | |||
76d50b018b | |||
31672a2587 | |||
a048494f34 | |||
c513d6bd44 | |||
c3d37b1f78 | |||
5910644cda | |||
a16cd3e287 | |||
e3a0122eb3 | |||
1cda0eff16 | |||
6003aa4236 | |||
8753dd15de | |||
6ae6335c6d | |||
e3a1eed8b3 | |||
eb44203475 | |||
80e878c2f5 | |||
6cb1649fc2 | |||
63fceed5f4 | |||
781b2885cc | |||
2f9afda0ab | |||
108146ca92 | |||
bb46294a6d | |||
f8aad6ac80 | |||
658d1f1df0 | |||
ee3144f34a | |||
9a34fe46fb | |||
766d96c02d | |||
7445c89773 | |||
28ac5608a5 | |||
44c925a4ba | |||
51cd89f177 | |||
ab188ad54f | |||
513835ed0f | |||
a863812f90 | |||
a37fdde214 | |||
d5ef36fe50 | |||
7430ceb23d | |||
395b550c21 | |||
774565b121 | |||
72d1344002 | |||
b4ee5dcb0d | |||
a0f0ff0bf1 | |||
db2cc8f951 | |||
24007f1515 | |||
3d7445f359 | |||
34760afe77 | |||
417209b057 | |||
9026378b86 | |||
9b3dca1683 | |||
cde593a935 | |||
f0755260a6 | |||
582e1eb4f8 | |||
aaadda3e0f | |||
9d7f5b5b6e | |||
99040597dc | |||
d9794216dd | |||
84bb6056d3 | |||
dfed2daa8e | |||
1521ec8071 | |||
bf7ae178ef | |||
dc7f96c6da | |||
c6959bb0bc | |||
d4dd6c84bc | |||
e59678360c | |||
1b6fa0c7d8 | |||
95a5936daf | |||
477d4117ce | |||
444f119e50 | |||
fa13a2874e | |||
24ce325e31 | |||
a52a1901c4 | |||
45aee607e3 | |||
c263016939 | |||
741915b1f8 | |||
6f2534ba82 | |||
43635071d9 | |||
22f06ecd4e | |||
7efe83eba8 | |||
a5b732e197 | |||
f404aaf768 | |||
e1f8177834 | |||
cff391a7a9 | |||
9cd7608a53 | |||
6950a06532 | |||
0e6c2ec556 | |||
479fc50d9a | |||
a29a8f7ed9 | |||
83cf637f9d | |||
5dbb4bf6be | |||
f1f227b746 | |||
b96cac16c6 | |||
f58fdafdcd | |||
b7b39f8284 | |||
7a173a6692 | |||
b042f98f0f | |||
0bb260bec9 | |||
024ab8ff69 | |||
50cb61f3bd | |||
f2befb7c48 | |||
dd80263490 | |||
73c44f8726 | |||
ca85095273 | |||
b5196aa541 | |||
b649ae0a1e | |||
4deb7c3270 | |||
b10da976c7 | |||
27a2e614a5 | |||
e7a931e9ba | |||
979bc31868 | |||
33cf4066d8 | |||
009444951a | |||
9e27e14692 | |||
bc00633135 | |||
9a7aa181ed | |||
6cc90cbeb4 | |||
59de852484 | |||
c9d5e9ac05 | |||
59ce3b5fc0 | |||
b71f9d0a08 | |||
55a4c3c08d | |||
c79db1dc99 | |||
a5bd27661b | |||
9a0d0a7124 | |||
454939adad | |||
37c02d2539 | |||
5f8407b4b1 | |||
abeb10cc8c | |||
4dca81403b | |||
6ba6a34df2 | |||
6d14fe9c30 | |||
c0c4637c77 | |||
752d34f603 | |||
0009ed0921 | |||
953de22233 | |||
1477aef39c | |||
2cca8f82e1 | |||
9094af025d | |||
0ba608bd1e | |||
67f5d497d5 | |||
3cc614c024 | |||
7c20bab1c5 | |||
a2a3f43fd0 | |||
9f17e3e1f8 | |||
c1a2fc22f4 | |||
f1f19369a3 | |||
e49f25af09 | |||
bc28571174 | |||
2beae1dcd3 | |||
39a0b1e29e | |||
0f603ffb0a | |||
8fe5835e09 | |||
5fed7a3a0c | |||
28ea694791 | |||
45b0991841 | |||
505d9904af | |||
da385f9295 | |||
214de77a41 | |||
7b2db3755e | |||
44791cc9f3 | |||
a14b94c96f | |||
db1cf5c2ce | |||
1a060a6c7b | |||
ff719fbe2d | |||
94e9ab7f67 | |||
06a96e8b77 | |||
d43c3dc968 | |||
e3c5efa929 | |||
8db9d93d23 | |||
517bb94b8b | |||
c804f55d82 | |||
c96a25c9b9 | |||
4c57726be7 | |||
8f723d7131 | |||
a7e10c0fb9 | |||
15e73e1cad | |||
a17192ee99 | |||
27200d1fb0 | |||
9ddceae824 | |||
d1961e0938 | |||
033432d6fb | |||
c98f0ba55b | |||
76993d2532 | |||
62e3f2d8e1 | |||
9806cab090 | |||
0d9fbe2d41 | |||
016db76306 | |||
d469084596 | |||
bb4decd522 |
@ -121,3 +121,4 @@ bower_components
|
||||
output
|
||||
|
||||
.vs
|
||||
**/launchSettings.json
|
||||
|
149
.editorconfig
Normal file
149
.editorconfig
Normal 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
|
@ -4,12 +4,13 @@
|
||||
<TargetFramework>netcoreapp2.0</TargetFramework>
|
||||
|
||||
<IsPackable>false</IsPackable>
|
||||
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.3.0-preview-20170720-02" />
|
||||
<PackageReference Include="xunit" Version="2.2.0" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.2.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.5.0" />
|
||||
<PackageReference Include="xunit" Version="2.3.1" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.3.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -1,6 +1,6 @@
|
||||
using BTCPayServer.Configuration;
|
||||
using BTCPayServer.Hosting;
|
||||
using BTCPayServer.Servcices.Invoices;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Tests.Logging;
|
||||
using BTCPayServer.Tests.Mocks;
|
||||
@ -30,128 +30,137 @@ 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 ExtKey HDPrivateKey
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public Uri LTCNBXplorerUri { get; set; }
|
||||
|
||||
public Uri ServerUri
|
||||
{
|
||||
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);
|
||||
string chain = ChainType.Regtest.ToNetwork().Name;
|
||||
string chainDirectory = Path.Combine(_Directory, chain);
|
||||
if (!Directory.Exists(chainDirectory))
|
||||
Directory.CreateDirectory(chainDirectory);
|
||||
|
||||
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 + "/");
|
||||
StringBuilder config = new StringBuilder();
|
||||
config.AppendLine($"{chain.ToLowerInvariant()}=1");
|
||||
config.AppendLine($"port={Port}");
|
||||
config.AppendLine($"chains=btc,ltc");
|
||||
|
||||
var conf = new DefaultConfiguration() { Logger = Logs.LogProvider.CreateLogger("Console") }.CreateConfiguration(new[] { "--datadir", _Directory });
|
||||
config.AppendLine($"btc.explorer.url={NBXplorerUri.AbsoluteUri}");
|
||||
config.AppendLine($"btc.explorer.cookiefile=0");
|
||||
|
||||
_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(50);
|
||||
}
|
||||
config.AppendLine($"ltc.explorer.url={LTCNBXplorerUri.AbsoluteUri}");
|
||||
config.AppendLine($"ltc.explorer.cookiefile=0");
|
||||
|
||||
public BTCPayServerRuntime Runtime
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string HostName
|
||||
{
|
||||
get;
|
||||
internal set;
|
||||
}
|
||||
if (Postgres != null)
|
||||
config.AppendLine($"postgres=" + Postgres);
|
||||
var confPath = Path.Combine(chainDirectory, "settings.config");
|
||||
File.WriteAllText(confPath, config.ToString());
|
||||
|
||||
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;
|
||||
ServerUri = new Uri("http://" + HostName + ":" + Port + "/");
|
||||
|
||||
var httpAccessor = provider.GetRequiredService<IHttpContextAccessor>();
|
||||
httpAccessor.HttpContext = context;
|
||||
var conf = new DefaultConfiguration() { Logger = Logs.LogProvider.CreateLogger("Console") }.CreateConfiguration(new[] { "--datadir", _Directory, "--conf", confPath });
|
||||
|
||||
var controller = (T)ActivatorUtilities.CreateInstance(provider, typeof(T));
|
||||
_Host = new WebHostBuilder()
|
||||
.UseConfiguration(conf)
|
||||
.ConfigureServices(s =>
|
||||
{
|
||||
var mockRates = new MockRateProviderFactory();
|
||||
var btc = new MockRateProvider("BTC", new Rate("USD", 5000m));
|
||||
var ltc = new MockRateProvider("LTC", new Rate("USD", 500m));
|
||||
mockRates.AddMock(btc);
|
||||
mockRates.AddMock(ltc);
|
||||
s.AddSingleton<IRateProviderFactory>(mockRates);
|
||||
s.AddLogging(l =>
|
||||
{
|
||||
l.SetMinimumLevel(LogLevel.Information)
|
||||
.AddFilter("Microsoft", LogLevel.Error)
|
||||
.AddFilter("Hangfire", LogLevel.Error)
|
||||
.AddProvider(Logs.LogProvider);
|
||||
});
|
||||
})
|
||||
.UseKestrel()
|
||||
.UseStartup<Startup>()
|
||||
.Build();
|
||||
_Host.Start();
|
||||
InvoiceRepository = (InvoiceRepository)_Host.Services.GetService(typeof(InvoiceRepository));
|
||||
}
|
||||
|
||||
public string HostName
|
||||
{
|
||||
get;
|
||||
internal set;
|
||||
}
|
||||
public InvoiceRepository InvoiceRepository { get; private set; }
|
||||
|
||||
controller.Url = new UrlHelperMock(new Uri($"http://{HostName}:{Port}/"));
|
||||
controller.ControllerContext = new ControllerContext()
|
||||
{
|
||||
HttpContext = context
|
||||
};
|
||||
return controller;
|
||||
}
|
||||
public T GetService<T>()
|
||||
{
|
||||
return _Host.Services.GetRequiredService<T>();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if(_Host != null)
|
||||
_Host.Dispose();
|
||||
}
|
||||
}
|
||||
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 controller = (T)ActivatorUtilities.CreateInstance(provider, typeof(T));
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
FROM microsoft/dotnet:2.0.0-sdk
|
||||
FROM microsoft/dotnet:2.0.5-sdk-2.1.4
|
||||
WORKDIR /app
|
||||
# caches restore result by copying csproj file separately
|
||||
COPY BTCPayServer.Tests/BTCPayServer.Tests.csproj BTCPayServer.Tests/BTCPayServer.Tests.csproj
|
||||
|
37
BTCPayServer.Tests/EclairTester.cs
Normal file
37
BTCPayServer.Tests/EclairTester.cs
Normal file
@ -0,0 +1,37 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Eclair;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
public class EclairTester
|
||||
{
|
||||
ServerTester parent;
|
||||
public EclairTester(ServerTester parent, string environmentName, string defaultRPC, string defaultHost)
|
||||
{
|
||||
this.parent = parent;
|
||||
//RPC = new EclairRPCClient(new Uri(parent.GetEnvironment(environmentName, defaultRPC)), parent.Network);
|
||||
P2PHost = parent.GetEnvironment(environmentName + "_HOST", defaultHost);
|
||||
}
|
||||
|
||||
public EclairRPCClient RPC { get; }
|
||||
public string P2PHost { get; }
|
||||
|
||||
NodeInfo _NodeInfo;
|
||||
public async Task<NodeInfo> GetNodeInfoAsync()
|
||||
{
|
||||
if (_NodeInfo != null)
|
||||
return _NodeInfo;
|
||||
var info = await RPC.GetInfoAsync();
|
||||
_NodeInfo = new NodeInfo(info.NodeId, P2PHost, info.Port);
|
||||
return _NodeInfo;
|
||||
}
|
||||
|
||||
public NodeInfo GetNodeInfo()
|
||||
{
|
||||
return GetNodeInfoAsync().GetAwaiter().GetResult();
|
||||
}
|
||||
}
|
||||
}
|
@ -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(DateTimeOffset.UtcNow + " :" + Name + ": " + msg);
|
||||
}
|
||||
}
|
||||
public class Logs
|
||||
{
|
||||
public static ILog Tester
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public static XUnitLogProvider LogProvider
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"profiles": {
|
||||
"BTCPayServer.Tests": {
|
||||
"commandName": "Project"
|
||||
"profiles": {
|
||||
"BTCPayServer.Tests": {
|
||||
"commandName": "Project"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,14 @@
|
||||
# How to run the tests
|
||||
# How to be started for development
|
||||
|
||||
The tests depends on having a proper environment running with Postgres, Bitcoind, NBxplorer configured.
|
||||
BTCPay Server tests depend on having a proper environment running with Postgres, Bitcoind, NBxplorer configured.
|
||||
You can however use the `docker-compose.yml` of this folder to get it running.
|
||||
|
||||
In addition, when you run a debug session of BTCPay (Hitting F5 on Visual Studio Code or Visual Studio 2017), it will run the launch profile called `Docker-Regtest`. This launch profile depends on this `docker-compose` running.
|
||||
|
||||
This is running a bitcoind instance on regtest, a private bitcoin blockchain for testing on which you can generate blocks yourself.
|
||||
|
||||
```
|
||||
docker-compose up nbxplorer
|
||||
docker-compose up dev
|
||||
```
|
||||
|
||||
You can run the tests while it is running through your favorite IDE, or with
|
||||
@ -19,14 +23,26 @@ Once you want to stop
|
||||
docker-compose down
|
||||
```
|
||||
|
||||
If you want to stop, and remove all existing data
|
||||
|
||||
```
|
||||
docker-compose down -v
|
||||
```
|
||||
|
||||
You can run the tests inside a container by running
|
||||
|
||||
```
|
||||
docker-compose run --rm tests
|
||||
```
|
||||
|
||||
The Bitcoin RPC server is exposed to the host, for example, you can send 0.23111090 BTC to mohu16LH66ptoWGEL1GtP6KHTBJYXMWhEf.
|
||||
## Send commands to bitcoind
|
||||
|
||||
You can call bitcoin-cli inside the container with `docker exec`, for example, if you want to send `0.23111090` to `mohu16LH66ptoWGEL1GtP6KHTBJYXMWhEf`:
|
||||
```
|
||||
docker exec -ti btcpayserver_dev_bitcoind bitcoin-cli -regtest -conf="/data/bitcoin.conf" -datadir="/data" sendtoaddress "mohu16LH66ptoWGEL1GtP6KHTBJYXMWhEf" 0.23111090
|
||||
```
|
||||
|
||||
If you are using Powershell:
|
||||
```
|
||||
.\docker-bitcoin-cli.ps1 sendtoaddress "mohu16LH66ptoWGEL1GtP6KHTBJYXMWhEf" 0.23111090
|
||||
```
|
||||
bitcoin-cli -regtest -rpcport=43782 -rpcuser=ceiwHEbqWI83 -rpcpassword=DwubwWsoo3 sendtoaddress "mohu16LH66ptoWGEL1GtP6KHTBJYXMWhEf" 0.23111090
|
||||
```
|
@ -1,4 +1,5 @@
|
||||
using BTCPayServer.Controllers;
|
||||
using System.Linq;
|
||||
using BTCPayServer.Models.AccountViewModels;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
@ -16,221 +17,220 @@ using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading;
|
||||
using BTCPayServer.Eclair;
|
||||
using System.Globalization;
|
||||
|
||||
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();
|
||||
}
|
||||
NetworkProvider = new BTCPayNetworkProvider(ChainType.Regtest);
|
||||
ExplorerNode = new RPCClient(RPCCredentialString.Parse(GetEnvironment("TESTS_BTCRPCCONNECTION", "server=http://127.0.0.1:43782;ceiwHEbqWI83:DwubwWsoo3")), NetworkProvider.GetNetwork("BTC").NBitcoinNetwork);
|
||||
LTCExplorerNode = new RPCClient(RPCCredentialString.Parse(GetEnvironment("TESTS_LTCRPCCONNECTION", "server=http://127.0.0.1:43783;ceiwHEbqWI83:DwubwWsoo3")), NetworkProvider.GetNetwork("LTC").NBitcoinNetwork);
|
||||
|
||||
private string GetEnvironment(string variable, string defaultValue)
|
||||
{
|
||||
var var = Environment.GetEnvironmentVariable(variable);
|
||||
return String.IsNullOrEmpty(var) ? defaultValue : var;
|
||||
}
|
||||
ExplorerClient = new ExplorerClient(NetworkProvider.GetNetwork("BTC").NBXplorerNetwork, new Uri(GetEnvironment("TESTS_BTCNBXPLORERURL", "http://127.0.0.1:32838/")));
|
||||
LTCExplorerClient = new ExplorerClient(NetworkProvider.GetNetwork("LTC").NBXplorerNetwork, new Uri(GetEnvironment("TESTS_LTCNBXPLORERURL", "http://127.0.0.1:32838/")));
|
||||
|
||||
public TestAccount NewAccount()
|
||||
{
|
||||
return new TestAccount(this);
|
||||
}
|
||||
PayTester = new BTCPayServerTester(Path.Combine(_Directory, "pay"))
|
||||
{
|
||||
NBXplorerUri = ExplorerClient.Address,
|
||||
LTCNBXplorerUri = LTCExplorerClient.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(CultureInfo.InvariantCulture)), CultureInfo.InvariantCulture);
|
||||
PayTester.HostName = GetEnvironment("TESTS_HOSTNAME", "127.0.0.1");
|
||||
PayTester.Start();
|
||||
|
||||
public bool FakeCallback
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public RPCClient ExplorerNode
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public ExplorerClient ExplorerClient
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
HttpClient _Http = new HttpClient();
|
||||
|
||||
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 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 bool HasFormContentType => 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();
|
||||
}
|
||||
}
|
||||
|
||||
/// <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 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;
|
||||
|
||||
_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();
|
||||
}
|
||||
}
|
||||
MerchantEclair = new EclairTester(this, "TEST_ECLAIR1", "http://127.0.0.1:30992/", "eclair1");
|
||||
CustomerEclair = new EclairTester(this, "TEST_ECLAIR2", "http://127.0.0.1:30993/", "eclair2");
|
||||
}
|
||||
|
||||
|
||||
public BTCPayServerTester PayTester
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
/// <summary>
|
||||
/// This will setup a channel going from customer to merchant
|
||||
/// </summary>
|
||||
public void PrepareLightning()
|
||||
{
|
||||
PrepareLightningAsync().GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public Network Network
|
||||
{
|
||||
get;
|
||||
set;
|
||||
} = Network.RegTest;
|
||||
public async Task PrepareLightningAsync()
|
||||
{
|
||||
// Activate segwit
|
||||
var blockCount = ExplorerNode.GetBlockCountAsync();
|
||||
// Fetch node info, but that in cache
|
||||
var merchant = MerchantEclair.GetNodeInfoAsync();
|
||||
var customer = CustomerEclair.GetNodeInfoAsync();
|
||||
var channels = CustomerEclair.RPC.ChannelsAsync();
|
||||
var connect = CustomerEclair.RPC.ConnectAsync(merchant.Result);
|
||||
await Task.WhenAll(blockCount, merchant, customer, channels, connect);
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if(PayTester != null)
|
||||
PayTester.Dispose();
|
||||
}
|
||||
}
|
||||
// Mine until segwit is activated
|
||||
if (blockCount.Result <= 432)
|
||||
{
|
||||
ExplorerNode.Generate(433 - blockCount.Result);
|
||||
}
|
||||
}
|
||||
|
||||
public EclairTester MerchantEclair { get; set; }
|
||||
public EclairTester CustomerEclair { get; set; }
|
||||
|
||||
internal 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 BTCPayNetworkProvider NetworkProvider { get; private set; }
|
||||
public RPCClient ExplorerNode
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public RPCClient LTCExplorerNode
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public ExplorerClient ExplorerClient
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public ExplorerClient LTCExplorerClient { get; set; }
|
||||
|
||||
HttpClient _Http = new HttpClient();
|
||||
|
||||
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 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 bool HasFormContentType => 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 BTCPayServerTester PayTester
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (PayTester != null)
|
||||
PayTester.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Models.AccountViewModels;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Servcices.Invoices;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitcoin;
|
||||
using NBitpayClient;
|
||||
@ -10,83 +10,117 @@ using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Xunit;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
|
||||
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(string cryptoCode = null)
|
||||
{
|
||||
return CreateStoreAsync(cryptoCode).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
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 string CryptoCode { get; set; } = "BTC";
|
||||
public async Task<StoresController> CreateStoreAsync(string cryptoCode = null)
|
||||
{
|
||||
cryptoCode = cryptoCode ?? CryptoCode;
|
||||
SupportedNetwork = parent.NetworkProvider.GetNetwork(cryptoCode);
|
||||
ExtKey = new ExtKey().GetWif(SupportedNetwork.NBitcoinNetwork);
|
||||
var store = parent.PayTester.GetController<StoresController>(UserId);
|
||||
await store.CreateStore(new CreateStoreViewModel() { Name = "Test Store" });
|
||||
StoreId = store.CreatedStoreId;
|
||||
DerivationScheme = new DerivationStrategyFactory(SupportedNetwork.NBitcoinNetwork).Parse(ExtKey.Neuter().ToString() + "-[legacy]");
|
||||
var vm = (StoreViewModel)((ViewResult)await store.UpdateStore(StoreId)).Model;
|
||||
vm.SpeedPolicy = SpeedPolicy.MediumSpeed;
|
||||
await store.UpdateStore(StoreId, vm);
|
||||
|
||||
public Bitpay BitPay
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string UserId
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
await store.AddDerivationScheme(StoreId, new DerivationSchemeViewModel()
|
||||
{
|
||||
CryptoCurrency = cryptoCode,
|
||||
DerivationSchemeFormat = "BTCPay",
|
||||
DerivationScheme = DerivationScheme.ToString(),
|
||||
Confirmation = true
|
||||
}, "Save");
|
||||
return store;
|
||||
}
|
||||
|
||||
public string StoreId
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
}
|
||||
public BTCPayNetwork SupportedNetwork { get; set; }
|
||||
|
||||
public void RegisterDerivationScheme(string crytoCode)
|
||||
{
|
||||
RegisterDerivationSchemeAsync(crytoCode).GetAwaiter().GetResult();
|
||||
}
|
||||
public async Task RegisterDerivationSchemeAsync(string crytoCode)
|
||||
{
|
||||
var store = parent.PayTester.GetController<StoresController>(UserId);
|
||||
var networkProvider = parent.PayTester.GetService<BTCPayNetworkProvider>();
|
||||
var derivation = new DerivationStrategyFactory(networkProvider.GetNetwork(crytoCode).NBitcoinNetwork).Parse(ExtKey.Neuter().ToString() + "-[legacy]");
|
||||
await store.AddDerivationScheme(StoreId, new DerivationSchemeViewModel()
|
||||
{
|
||||
CryptoCurrency = crytoCode,
|
||||
DerivationSchemeFormat = crytoCode,
|
||||
DerivationScheme = derivation.ToString(),
|
||||
Confirmation = true
|
||||
}, "Save");
|
||||
}
|
||||
|
||||
public DerivationStrategyBase DerivationScheme { get; set; }
|
||||
|
||||
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 string StoreId
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
52
BTCPayServer.Tests/UnitTestPeusa.cs
Normal file
52
BTCPayServer.Tests/UnitTestPeusa.cs
Normal file
@ -0,0 +1,52 @@
|
||||
using NBitcoin;
|
||||
using NBitcoin.DataEncoders;
|
||||
using NBitpayClient;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using Xunit;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
// 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
|
||||
|
||||
// Testnet of Bitpay down
|
||||
//[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);
|
||||
|
||||
// // 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;
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
1
BTCPayServer.Tests/docker-bitcoin-cli.ps1
Normal file
1
BTCPayServer.Tests/docker-bitcoin-cli.ps1
Normal file
@ -0,0 +1 @@
|
||||
docker exec -ti btcpayserver_dev_bitcoind bitcoin-cli -regtest -conf="/data/bitcoin.conf" -datadir="/data" $args
|
@ -1,5 +1,8 @@
|
||||
version: "3"
|
||||
version: "3"
|
||||
|
||||
# Run `docker-compose up dev` for bootstrapping your development environment
|
||||
# Doing so will expose eclair API, NBXplorer, Bitcoind RPC and postgres port to the host so that tests can Run,
|
||||
# The Visual Studio launch setting `Docker-Regtest` is configured to use this environment.
|
||||
services:
|
||||
|
||||
tests:
|
||||
@ -7,51 +10,95 @@ services:
|
||||
context: ..
|
||||
dockerfile: BTCPayServer.Tests/Dockerfile
|
||||
environment:
|
||||
TESTS_RPCCONNECTION: server=http://bitcoind:43782;ceiwHEbqWI83:DwubwWsoo3
|
||||
TESTS_NBXPLORERURL: http://nbxplorer:32838/
|
||||
TESTS_BTCRPCCONNECTION: server=http://bitcoind:43782;ceiwHEbqWI83:DwubwWsoo3
|
||||
TESTS_LTCRPCCONNECTION: server=http://litecoind:43782;ceiwHEbqWI83:DwubwWsoo3
|
||||
TESTS_BTCNBXPLORERURL: http://nbxplorer:32838/
|
||||
TESTS_LTCNBXPLORERURL: http://nbxplorer:32838/
|
||||
TESTS_POSTGRES: User ID=postgres;Host=postgres;Port=5432;Database=btcpayserver
|
||||
TESTS_FAKECALLBACK: 'true'
|
||||
TESTS_PORT: 80
|
||||
TESTS_HOSTNAME: tests
|
||||
expose:
|
||||
- "80"
|
||||
links:
|
||||
- nbxplorer
|
||||
- postgres
|
||||
extra_hosts:
|
||||
- "tests:127.0.0.1"
|
||||
|
||||
# The dev container is not actually used, it is just handy to run `docker-compose up dev` to start all services
|
||||
dev:
|
||||
image: nicolasdorier/docker-bitcoin:0.15.0.1
|
||||
environment:
|
||||
BITCOIN_EXTRA_ARGS: |
|
||||
regtest=1
|
||||
connect=bitcoind:39388
|
||||
links:
|
||||
- nbxplorer
|
||||
- postgres
|
||||
|
||||
nbxplorer:
|
||||
image: nicolasdorier/nbxplorer:1.0.0.18
|
||||
image: nicolasdorier/nbxplorer:1.0.1.13
|
||||
ports:
|
||||
- "32838:32838"
|
||||
expose:
|
||||
- "32838"
|
||||
environment:
|
||||
NBXPLORER_NETWORK: regtest
|
||||
NBXPLORER_RPCURL: http://bitcoind:43782/
|
||||
NBXPLORER_RPCUSER: ceiwHEbqWI83
|
||||
NBXPLORER_RPCPASSWORD: DwubwWsoo3
|
||||
NBXPLORER_NODEENDPOINT: bitcoind:39388
|
||||
NBXPLORER_CHAINS: "btc,ltc"
|
||||
NBXPLORER_BTCRPCURL: http://bitcoind:43782/
|
||||
NBXPLORER_BTCNODEENDPOINT: bitcoind:39388
|
||||
NBXPLORER_BTCRPCUSER: ceiwHEbqWI83
|
||||
NBXPLORER_BTCRPCPASSWORD: DwubwWsoo3
|
||||
NBXPLORER_LTCRPCURL: http://litecoind:43782/
|
||||
NBXPLORER_LTCNODEENDPOINT: litecoind:39388
|
||||
NBXPLORER_LTCRPCUSER: ceiwHEbqWI83
|
||||
NBXPLORER_LTCRPCPASSWORD: DwubwWsoo3
|
||||
NBXPLORER_BIND: 0.0.0.0:32838
|
||||
NBXPLORER_VERBOSE: 1
|
||||
NBXPLORER_NOAUTH: 1
|
||||
links:
|
||||
- bitcoind
|
||||
- postgres
|
||||
- litecoind
|
||||
|
||||
bitcoind:
|
||||
container_name: btcpayserver_dev_bitcoind
|
||||
image: nicolasdorier/docker-bitcoin:0.15.0.1
|
||||
ports:
|
||||
- "43782:43782"
|
||||
- "39388:39388"
|
||||
environment:
|
||||
BITCOIN_EXTRA_ARGS: "regtest=1\nrpcport=43782\nport=39388\nwhitelist=0.0.0.0/0"
|
||||
BITCOIN_RPC_USER: ceiwHEbqWI83
|
||||
BITCOIN_RPC_PASSWORD: DwubwWsoo3
|
||||
BITCOIN_EXTRA_ARGS: |
|
||||
rpcuser=ceiwHEbqWI83
|
||||
rpcpassword=DwubwWsoo3
|
||||
regtest=1
|
||||
server=1
|
||||
rpcport=43782
|
||||
port=39388
|
||||
whitelist=0.0.0.0/0
|
||||
ports:
|
||||
- "43782:43782"
|
||||
expose:
|
||||
- "43782"
|
||||
- "39388"
|
||||
- "43782" # RPC
|
||||
- "39388" # P2P
|
||||
|
||||
litecoind:
|
||||
container_name: btcpayserver_dev_litecoind
|
||||
image: nicolasdorier/docker-litecoin:0.14.2
|
||||
environment:
|
||||
BITCOIN_EXTRA_ARGS: |
|
||||
rpcuser=ceiwHEbqWI83
|
||||
rpcpassword=DwubwWsoo3
|
||||
regtest=1
|
||||
server=1
|
||||
rpcport=43782
|
||||
port=39388
|
||||
whitelist=0.0.0.0/0
|
||||
ports:
|
||||
- "43783:43782"
|
||||
expose:
|
||||
- "43782" # RPC
|
||||
- "39388" # P2P
|
||||
|
||||
postgres:
|
||||
image: postgres:9.6.5
|
||||
ports:
|
||||
- "39372:5432"
|
||||
- "39372:5432"
|
||||
expose:
|
||||
- "5432"
|
||||
|
1
BTCPayServer.Tests/docker-litecoin-cli.ps1
Normal file
1
BTCPayServer.Tests/docker-litecoin-cli.ps1
Normal file
@ -0,0 +1 @@
|
||||
docker exec -ti btcpayserver_dev_litecoind litecoin-cli -regtest -conf="/data/litecoin.conf" -datadir="/data" $args
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -13,173 +13,196 @@ using System.Linq;
|
||||
|
||||
namespace BTCPayServer.Authentication
|
||||
{
|
||||
public class TokenRepository
|
||||
{
|
||||
ApplicationDbContextFactory _Factory;
|
||||
public TokenRepository(ApplicationDbContextFactory dbFactory)
|
||||
{
|
||||
if(dbFactory == null)
|
||||
throw new ArgumentNullException(nameof(dbFactory));
|
||||
_Factory = dbFactory;
|
||||
}
|
||||
public enum PairingResult
|
||||
{
|
||||
Partial,
|
||||
Complete,
|
||||
ReusedKey,
|
||||
Expired
|
||||
}
|
||||
|
||||
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 class TokenRepository
|
||||
{
|
||||
ApplicationDbContextFactory _Factory;
|
||||
public TokenRepository(ApplicationDbContextFactory dbFactory)
|
||||
{
|
||||
if (dbFactory == null)
|
||||
throw new ArgumentNullException(nameof(dbFactory));
|
||||
_Factory = dbFactory;
|
||||
}
|
||||
|
||||
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<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<string> CreatePairingCodeAsync()
|
||||
{
|
||||
string pairingCodeId = Encoders.Base58.EncodeData(RandomUtils.GetBytes(6));
|
||||
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;
|
||||
}
|
||||
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<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<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);
|
||||
ctx.PairingCodes.Add(new PairingCodeData()
|
||||
{
|
||||
Id = pairingCodeId,
|
||||
DateCreated = now,
|
||||
Expiration = expiration,
|
||||
TokenValue = Encoders.Base58.EncodeData(RandomUtils.GetBytes(32))
|
||||
});
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
return pairingCodeId;
|
||||
}
|
||||
|
||||
public async Task<bool> 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 false;
|
||||
pairingCode.StoreDataId = storeId;
|
||||
await ActivateIfComplete(ctx, pairingCode);
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
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<bool> 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 false;
|
||||
pairingCode.SIN = sin;
|
||||
await ActivateIfComplete(ctx, pairingCode);
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private async Task ActivateIfComplete(ApplicationDbContext ctx, PairingCodeData pairingCode)
|
||||
{
|
||||
if(!string.IsNullOrEmpty(pairingCode.SIN) && !string.IsNullOrEmpty(pairingCode.StoreDataId))
|
||||
{
|
||||
ctx.PairingCodes.Remove(pairingCode);
|
||||
await ctx.PairedSINData.AddAsync(new PairedSINData()
|
||||
{
|
||||
Id = pairingCode.TokenValue,
|
||||
PairingTime = DateTime.UtcNow,
|
||||
Facade = pairingCode.Facade,
|
||||
Label = pairingCode.Label,
|
||||
StoreDataId = pairingCode.StoreDataId,
|
||||
SIN = pairingCode.SIN
|
||||
});
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
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)
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
76
BTCPayServer/BTCPayNetwork.cs
Normal file
76
BTCPayServer/BTCPayNetwork.cs
Normal file
@ -0,0 +1,76 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using NBitcoin;
|
||||
using NBXplorer;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public class BTCPayDefaultSettings
|
||||
{
|
||||
static BTCPayDefaultSettings()
|
||||
{
|
||||
_Settings = new Dictionary<ChainType, BTCPayDefaultSettings>();
|
||||
foreach (var chainType in new[] { ChainType.Main, ChainType.Test, ChainType.Regtest })
|
||||
{
|
||||
var btcNetwork = (chainType == ChainType.Main ? Network.Main :
|
||||
chainType == ChainType.Regtest ? Network.RegTest :
|
||||
chainType == ChainType.Test ? Network.TestNet : throw new NotSupportedException(chainType.ToString()));
|
||||
|
||||
var settings = new BTCPayDefaultSettings();
|
||||
_Settings.Add(chainType, settings);
|
||||
settings.ChainType = chainType;
|
||||
settings.DefaultDataDirectory = StandardConfiguration.DefaultDataDirectory.GetDirectory("BTCPayServer", btcNetwork.Name);
|
||||
settings.DefaultConfigurationFile = Path.Combine(settings.DefaultDataDirectory, "settings.config");
|
||||
settings.DefaultPort = (chainType == ChainType.Main ? 23000 :
|
||||
chainType == ChainType.Regtest ? 23002 :
|
||||
chainType == ChainType.Test ? 23001 : throw new NotSupportedException(chainType.ToString()));
|
||||
}
|
||||
}
|
||||
|
||||
static Dictionary<ChainType, BTCPayDefaultSettings> _Settings;
|
||||
|
||||
public static BTCPayDefaultSettings GetDefaultSettings(ChainType chainType)
|
||||
{
|
||||
return _Settings[chainType];
|
||||
}
|
||||
|
||||
public string DefaultDataDirectory { get; set; }
|
||||
public string DefaultConfigurationFile { get; set; }
|
||||
public ChainType ChainType { get; internal set; }
|
||||
public int DefaultPort { get; set; }
|
||||
}
|
||||
public class BTCPayNetwork
|
||||
{
|
||||
public Network NBitcoinNetwork { get; set; }
|
||||
public string CryptoCode { get; internal set; }
|
||||
public string BlockExplorerLink { get; internal set; }
|
||||
public string UriScheme { get; internal set; }
|
||||
public IRateProvider DefaultRateProvider { get; set; }
|
||||
|
||||
[Obsolete("Should not be needed")]
|
||||
public bool IsBTC
|
||||
{
|
||||
get
|
||||
{
|
||||
return CryptoCode == "BTC";
|
||||
}
|
||||
}
|
||||
|
||||
public string CryptoImagePath { get; set; }
|
||||
public NBXplorer.NBXplorerNetwork NBXplorerNetwork { get; set; }
|
||||
|
||||
public BTCPayDefaultSettings DefaultSettings { get; set; }
|
||||
public KeyPath CoinType { get; internal set; }
|
||||
public int MaxTrackedConfirmation { get; internal set; } = 6;
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return CryptoCode;
|
||||
}
|
||||
}
|
||||
}
|
34
BTCPayServer/BTCPayNetworkProvider.Bitcoin.cs
Normal file
34
BTCPayServer/BTCPayNetworkProvider.Bitcoin.cs
Normal file
@ -0,0 +1,34 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using NBitcoin;
|
||||
using NBitpayClient;
|
||||
using NBXplorer;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public partial class BTCPayNetworkProvider
|
||||
{
|
||||
public void InitBitcoin()
|
||||
{
|
||||
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("BTC");
|
||||
var coinaverage = new CoinAverageRateProvider("BTC");
|
||||
var bitpay = new BitpayRateProvider(new Bitpay(new Key(), new Uri("https://bitpay.com/")));
|
||||
var btcRate = new FallbackRateProvider(new IRateProvider[] { coinaverage, bitpay });
|
||||
Add(new BTCPayNetwork()
|
||||
{
|
||||
CryptoCode = nbxplorerNetwork.CryptoCode,
|
||||
BlockExplorerLink = NBXplorerNetworkProvider.ChainType == ChainType.Main? "https://www.smartbit.com.au/tx/{0}" : "https://testnet.smartbit.com.au/tx/{0}",
|
||||
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
|
||||
NBXplorerNetwork = nbxplorerNetwork,
|
||||
UriScheme = "bitcoin",
|
||||
DefaultRateProvider = btcRate,
|
||||
CryptoImagePath = "imlegacy/bitcoin-symbol.svg",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NBXplorerNetworkProvider.ChainType),
|
||||
CoinType = NBXplorerNetworkProvider.ChainType == ChainType.Main ? new KeyPath("0'") : new KeyPath("1'")
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
33
BTCPayServer/BTCPayNetworkProvider.Litecoin.cs
Normal file
33
BTCPayServer/BTCPayNetworkProvider.Litecoin.cs
Normal file
@ -0,0 +1,33 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using NBitcoin;
|
||||
using NBXplorer;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public partial class BTCPayNetworkProvider
|
||||
{
|
||||
public void InitLitecoin()
|
||||
{
|
||||
NBXplorer.Altcoins.Litecoin.Networks.EnsureRegistered();
|
||||
var ltcRate = new CoinAverageRateProvider("LTC");
|
||||
|
||||
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("LTC");
|
||||
Add(new BTCPayNetwork()
|
||||
{
|
||||
CryptoCode = nbxplorerNetwork.CryptoCode,
|
||||
BlockExplorerLink = NBXplorerNetworkProvider.ChainType == ChainType.Main ? "https://live.blockcypher.com/ltc/tx/{0}/" : "http://explorer.litecointools.com/tx/{0}",
|
||||
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
|
||||
NBXplorerNetwork = nbxplorerNetwork,
|
||||
UriScheme = "litecoin",
|
||||
DefaultRateProvider = ltcRate,
|
||||
CryptoImagePath = "imlegacy/litecoin-symbol.svg",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NBXplorerNetworkProvider.ChainType),
|
||||
CoinType = NBXplorerNetworkProvider.ChainType == ChainType.Main ? new KeyPath("2'") : new KeyPath("3'")
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
60
BTCPayServer/BTCPayNetworkProvider.cs
Normal file
60
BTCPayServer/BTCPayNetworkProvider.cs
Normal file
@ -0,0 +1,60 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using NBitcoin;
|
||||
using NBitpayClient;
|
||||
using NBXplorer;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public partial class BTCPayNetworkProvider
|
||||
{
|
||||
Dictionary<string, BTCPayNetwork> _Networks = new Dictionary<string, BTCPayNetwork>();
|
||||
|
||||
|
||||
private readonly NBXplorerNetworkProvider _NBXplorerNetworkProvider;
|
||||
public NBXplorerNetworkProvider NBXplorerNetworkProvider
|
||||
{
|
||||
get
|
||||
{
|
||||
return _NBXplorerNetworkProvider;
|
||||
}
|
||||
}
|
||||
|
||||
public BTCPayNetworkProvider(ChainType chainType)
|
||||
{
|
||||
_NBXplorerNetworkProvider = new NBXplorerNetworkProvider(chainType);
|
||||
InitBitcoin();
|
||||
InitLitecoin();
|
||||
}
|
||||
|
||||
[Obsolete("To use only for legacy stuff")]
|
||||
public BTCPayNetwork BTC
|
||||
{
|
||||
get
|
||||
{
|
||||
return GetNetwork("BTC");
|
||||
}
|
||||
}
|
||||
|
||||
public void Add(BTCPayNetwork network)
|
||||
{
|
||||
_Networks.Add(network.CryptoCode, network);
|
||||
}
|
||||
|
||||
public IEnumerable<BTCPayNetwork> GetAll()
|
||||
{
|
||||
return _Networks.Values.ToArray();
|
||||
}
|
||||
|
||||
public BTCPayNetwork GetNetwork(string cryptoCode)
|
||||
{
|
||||
_Networks.TryGetValue(cryptoCode.ToUpperInvariant(), out BTCPayNetwork network);
|
||||
return network;
|
||||
}
|
||||
}
|
||||
}
|
@ -2,7 +2,8 @@
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>netcoreapp2.0</TargetFramework>
|
||||
<Version>1.0.0.8</Version>
|
||||
<Version>1.0.1.37</Version>
|
||||
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Compile Remove="Build\dockerfiles\**" />
|
||||
@ -18,25 +19,28 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Hangfire" Version="1.6.17" />
|
||||
<PackageReference Include="Hangfire.MemoryStorage" Version="1.5.1" />
|
||||
<PackageReference Include="Hangfire.MemoryStorage" Version="1.5.2" />
|
||||
<PackageReference Include="Hangfire.PostgreSql" Version="1.4.8.1" />
|
||||
<PackageReference Include="LedgerWallet" Version="1.0.1.32" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="2.6.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Filter" Version="1.1.2" />
|
||||
<PackageReference Include="NBitcoin" Version="4.0.0.38" />
|
||||
<PackageReference Include="NBitpayClient" Version="1.0.0.10" />
|
||||
<PackageReference Include="Microsoft.NetCore.Analyzers" Version="2.6.0" />
|
||||
<PackageReference Include="NBitcoin" Version="4.0.0.55" />
|
||||
<PackageReference Include="NBitpayClient" Version="1.0.0.17" />
|
||||
<PackageReference Include="DBreeze" Version="1.87.0" />
|
||||
<PackageReference Include="NBXplorer.Client" Version="1.0.0.17" />
|
||||
<PackageReference Include="NBXplorer.Client" Version="1.0.1.9" />
|
||||
<PackageReference Include="NicolasDorier.CommandLine" Version="1.0.0.1" />
|
||||
<PackageReference Include="NicolasDorier.CommandLine.Configuration" Version="1.0.0.2" />
|
||||
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="1.0.0.13" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="2.0.0" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="2.0.1" />
|
||||
<PackageReference Include="System.ValueTuple" Version="4.4.0" />
|
||||
<PackageReference Include="System.Xml.XmlSerializer" Version="4.0.11" />
|
||||
<PackageReference Include="Text.Analyzers" Version="2.6.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="2.0.0" PrivateAssets="All" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="2.0.0" PrivateAssets="All" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.5" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="2.0.1" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@ -46,7 +50,6 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="wwwroot\img\bitcoin-symbol.svg" />
|
||||
<None Include="wwwroot\js\core.js" />
|
||||
<None Include="wwwroot\js\creative.js" />
|
||||
<None Include="wwwroot\js\creative.min.js" />
|
||||
@ -100,4 +103,10 @@
|
||||
<ItemGroup>
|
||||
<Folder Include="Build\" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="devtest.pfx">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
72
BTCPayServer/BTCPayServer.ruleset
Normal file
72
BTCPayServer/BTCPayServer.ruleset
Normal file
@ -0,0 +1,72 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RuleSet Name="Microsoft Managed Recommended Rules" Description="These rules focus on the most critical problems in your code, including potential security holes, application crashes, and other important logic and design errors. It is recommended to include this rule set in any custom rule set you create for your projects." ToolsVersion="10.0">
|
||||
<Localization ResourceAssembly="Microsoft.VisualStudio.CodeAnalysis.RuleSets.Strings.dll" ResourceBaseName="Microsoft.VisualStudio.CodeAnalysis.RuleSets.Strings.Localized">
|
||||
<Name Resource="MinimumRecommendedRules_Name" />
|
||||
<Description Resource="MinimumRecommendedRules_Description" />
|
||||
</Localization>
|
||||
<Rules AnalyzerId="Microsoft.Analyzers.ManagedCodeAnalysis" RuleNamespace="Microsoft.Rules.Managed">
|
||||
<Rule Id="CA1001" Action="Warning" />
|
||||
<Rule Id="CA1009" Action="Warning" />
|
||||
<Rule Id="CA1016" Action="Warning" />
|
||||
<Rule Id="CA1033" Action="Warning" />
|
||||
<Rule Id="CA1049" Action="Warning" />
|
||||
<Rule Id="CA1060" Action="Warning" />
|
||||
<Rule Id="CA1061" Action="Warning" />
|
||||
<Rule Id="CA1063" Action="Warning" />
|
||||
<Rule Id="CA1065" Action="Warning" />
|
||||
<Rule Id="CA1301" Action="Warning" />
|
||||
<Rule Id="CA1400" Action="Warning" />
|
||||
<Rule Id="CA1401" Action="Warning" />
|
||||
<Rule Id="CA1403" Action="Warning" />
|
||||
<Rule Id="CA1404" Action="Warning" />
|
||||
<Rule Id="CA1405" Action="Warning" />
|
||||
<Rule Id="CA1410" Action="Warning" />
|
||||
<Rule Id="CA1415" Action="Warning" />
|
||||
<Rule Id="CA1821" Action="Warning" />
|
||||
<Rule Id="CA1900" Action="Warning" />
|
||||
<Rule Id="CA1901" Action="Warning" />
|
||||
<Rule Id="CA2002" Action="Warning" />
|
||||
<Rule Id="CA2100" Action="Warning" />
|
||||
<Rule Id="CA2101" Action="Warning" />
|
||||
<Rule Id="CA2108" Action="Warning" />
|
||||
<Rule Id="CA2111" Action="Warning" />
|
||||
<Rule Id="CA2112" Action="Warning" />
|
||||
<Rule Id="CA2114" Action="Warning" />
|
||||
<Rule Id="CA2116" Action="Warning" />
|
||||
<Rule Id="CA2117" Action="Warning" />
|
||||
<Rule Id="CA2122" Action="Warning" />
|
||||
<Rule Id="CA2123" Action="Warning" />
|
||||
<Rule Id="CA2124" Action="Warning" />
|
||||
<Rule Id="CA2126" Action="Warning" />
|
||||
<Rule Id="CA2131" Action="Warning" />
|
||||
<Rule Id="CA2132" Action="Warning" />
|
||||
<Rule Id="CA2133" Action="Warning" />
|
||||
<Rule Id="CA2134" Action="Warning" />
|
||||
<Rule Id="CA2137" Action="Warning" />
|
||||
<Rule Id="CA2138" Action="Warning" />
|
||||
<Rule Id="CA2140" Action="Warning" />
|
||||
<Rule Id="CA2141" Action="Warning" />
|
||||
<Rule Id="CA2146" Action="Warning" />
|
||||
<Rule Id="CA2147" Action="Warning" />
|
||||
<Rule Id="CA2149" Action="Warning" />
|
||||
<Rule Id="CA2200" Action="Warning" />
|
||||
<Rule Id="CA2202" Action="Warning" />
|
||||
<Rule Id="CA2207" Action="Warning" />
|
||||
<Rule Id="CA2212" Action="Warning" />
|
||||
<Rule Id="CA2213" Action="Warning" />
|
||||
<Rule Id="CA2214" Action="Warning" />
|
||||
<Rule Id="CA2216" Action="Warning" />
|
||||
<Rule Id="CA2220" Action="Warning" />
|
||||
<Rule Id="CA2229" Action="Warning" />
|
||||
<Rule Id="CA2231" Action="Warning" />
|
||||
<Rule Id="CA2232" Action="Warning" />
|
||||
<Rule Id="CA2235" Action="Warning" />
|
||||
<Rule Id="CA2236" Action="Warning" />
|
||||
<Rule Id="CA2237" Action="Warning" />
|
||||
<Rule Id="CA2238" Action="Warning" />
|
||||
<Rule Id="CA2240" Action="Warning" />
|
||||
<Rule Id="CA2241" Action="Warning" />
|
||||
<Rule Id="CA2242" Action="Warning" />
|
||||
</Rules>
|
||||
<Rules AnalyzerId="Microsoft.CodeAnalysis.CSharp.Analyzers" RuleNamespace="Microsoft.CodeAnalysis.CSharp.Analyzers" />
|
||||
</RuleSet>
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,64 +9,86 @@ using System.Net;
|
||||
using System.Text;
|
||||
using StandardConfiguration;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using NBXplorer;
|
||||
|
||||
namespace BTCPayServer.Configuration
|
||||
{
|
||||
public class BTCPayServerOptions
|
||||
{
|
||||
public Network Network
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public Uri Explorer
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public class NBXplorerConnectionSetting
|
||||
{
|
||||
public string CryptoCode { get; internal set; }
|
||||
public Uri ExplorerUri { get; internal set; }
|
||||
public string CookieFile { get; internal 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 class BTCPayServerOptions
|
||||
{
|
||||
public ChainType ChainType
|
||||
{
|
||||
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 List<NBXplorerConnectionSetting> NBXplorerConnectionSettings
|
||||
{
|
||||
get;
|
||||
set;
|
||||
} = new List<NBXplorerConnectionSetting>();
|
||||
|
||||
DataDir = conf.GetOrDefault<string>("datadir", networkInfo.DefaultDataDirectory);
|
||||
Logs.Configuration.LogInformation("Network: " + Network);
|
||||
public void LoadArgs(IConfiguration conf)
|
||||
{
|
||||
ChainType = DefaultConfiguration.GetChainType(conf);
|
||||
var defaultSettings = BTCPayDefaultSettings.GetDefaultSettings(ChainType);
|
||||
DataDir = conf.GetOrDefault<string>("datadir", defaultSettings.DefaultDataDirectory);
|
||||
Logs.Configuration.LogInformation("Network: " + ChainType.ToString());
|
||||
|
||||
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);
|
||||
}
|
||||
var supportedChains = conf.GetOrDefault<string>("chains", "btc")
|
||||
.Split(',', StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(t => t.ToUpperInvariant());
|
||||
var validChains = new List<string>();
|
||||
foreach (var net in new BTCPayNetworkProvider(ChainType).GetAll())
|
||||
{
|
||||
if (supportedChains.Contains(net.CryptoCode))
|
||||
{
|
||||
validChains.Add(net.CryptoCode);
|
||||
NBXplorerConnectionSetting setting = new NBXplorerConnectionSetting();
|
||||
setting.CryptoCode = net.CryptoCode;
|
||||
setting.ExplorerUri = conf.GetOrDefault<Uri>($"{net.CryptoCode}.explorer.url", net.NBXplorerNetwork.DefaultSettings.DefaultUrl);
|
||||
setting.CookieFile = conf.GetOrDefault<string>($"{net.CryptoCode}.explorer.cookiefile", net.NBXplorerNetwork.DefaultSettings.DefaultCookieFile);
|
||||
NBXplorerConnectionSettings.Add(setting);
|
||||
}
|
||||
}
|
||||
var invalidChains = String.Join(',', supportedChains.Where(s => !validChains.Contains(s)).ToArray());
|
||||
if(!string.IsNullOrEmpty(invalidChains))
|
||||
throw new ConfigException($"Invalid chains {invalidChains}");
|
||||
|
||||
public bool RequireHttps
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string PostgresConnectionString
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
}
|
||||
Logs.Configuration.LogInformation("Supported chains: " + String.Join(',', supportedChains.ToArray()));
|
||||
PostgresConnectionString = conf.GetOrDefault<string>("postgres", null);
|
||||
ExternalUrl = conf.GetOrDefault<Uri>("externalurl", null);
|
||||
}
|
||||
|
||||
public string PostgresConnectionString
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public Uri ExternalUrl
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,113 +0,0 @@
|
||||
using BTCPayServer.Authentication;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using BTCPayServer.Logging;
|
||||
using DBreeze;
|
||||
using NBitcoin;
|
||||
using NBXplorer;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Servcices.Invoices;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
|
||||
namespace BTCPayServer.Configuration
|
||||
{
|
||||
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);
|
||||
|
||||
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);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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>();
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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, StringComparison.InvariantCulture)];
|
||||
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(":", StringComparison.InvariantCulture);
|
||||
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, CultureInfo.InvariantCulture));
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -10,91 +10,108 @@ using System.Threading.Tasks;
|
||||
using NBitcoin;
|
||||
using System.Text;
|
||||
using CommandLine;
|
||||
using NBXplorer;
|
||||
|
||||
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()
|
||||
{
|
||||
var provider = new BTCPayNetworkProvider(ChainType.Main);
|
||||
var chains = string.Join(",", provider.GetAll().Select(n => n.CryptoCode.ToLowerInvariant()).ToArray());
|
||||
CommandLineApplication app = new CommandLineApplication(true)
|
||||
{
|
||||
FullName = "BTCPay\r\nOpen source, self-hosted payment processor.",
|
||||
Name = "BTCPay"
|
||||
};
|
||||
app.HelpOption("-? | -h | --help");
|
||||
app.Option("-n | --network", $"Set the network among (mainnet,testnet,regtest) (default: mainnet)", CommandOptionType.SingleValue);
|
||||
app.Option("--testnet | -testnet", $"Use testnet (Deprecated, use --network instead)", CommandOptionType.BoolValue);
|
||||
app.Option("--regtest | -regtest", $"Use regtest (Deprecated, use --network instead)", CommandOptionType.BoolValue);
|
||||
app.Option("--chains | -c", $"Chains to support comma separated (default: btc, available: {chains})", CommandOptionType.SingleValue);
|
||||
app.Option("--postgres", $"Connection string to postgres database (default: sqlite is used)", CommandOptionType.SingleValue);
|
||||
foreach (var network in provider.GetAll())
|
||||
{
|
||||
var crypto = network.CryptoCode.ToLowerInvariant();
|
||||
app.Option($"--{crypto}explorerurl", $"Url of the NBxplorer for {network.CryptoCode} (default: {network.NBXplorerNetwork.DefaultSettings.DefaultUrl})", CommandOptionType.SingleValue);
|
||||
app.Option($"--{crypto}explorercookiefile", $"Path to the cookie file (default: {network.NBXplorerNetwork.DefaultSettings.DefaultCookieFile})", CommandOptionType.SingleValue);
|
||||
}
|
||||
app.Option("--externalurl", $"The expected external url of this service, to use if BTCPay is behind a reverse proxy (default: empty, use the incoming HTTP request to figure out)", CommandOptionType.SingleValue);
|
||||
return app;
|
||||
}
|
||||
|
||||
return app;
|
||||
}
|
||||
public override string EnvironmentVariablePrefix => "BTCPAY_";
|
||||
|
||||
public override string EnvironmentVariablePrefix => "BTCPAY_";
|
||||
protected override string GetDefaultDataDir(IConfiguration conf)
|
||||
{
|
||||
return BTCPayDefaultSettings.GetDefaultSettings(GetChainType(conf)).DefaultDataDirectory;
|
||||
}
|
||||
|
||||
protected override string GetDefaultDataDir(IConfiguration conf)
|
||||
{
|
||||
return GetNetwork(conf).DefaultDataDirectory;
|
||||
}
|
||||
protected override string GetDefaultConfigurationFile(IConfiguration conf)
|
||||
{
|
||||
var network = BTCPayDefaultSettings.GetDefaultSettings(GetChainType(conf));
|
||||
var dataDir = conf["datadir"];
|
||||
if (dataDir == null)
|
||||
return network.DefaultConfigurationFile;
|
||||
var fileName = Path.GetFileName(network.DefaultConfigurationFile);
|
||||
var chainDir = Path.GetFileName(Path.GetDirectoryName(network.DefaultConfigurationFile));
|
||||
chainDir = Path.Combine(dataDir, chainDir);
|
||||
try
|
||||
{
|
||||
if (!Directory.Exists(chainDir))
|
||||
Directory.CreateDirectory(chainDir);
|
||||
}
|
||||
catch { }
|
||||
return Path.Combine(chainDir, 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 ChainType GetChainType(IConfiguration conf)
|
||||
{
|
||||
var network = conf.GetOrDefault<string>("network", null);
|
||||
if (network != null)
|
||||
{
|
||||
var n = Network.GetNetwork(network);
|
||||
if (n == null)
|
||||
{
|
||||
throw new ConfigException($"Invalid network parameter '{network}'");
|
||||
}
|
||||
return n.ToChainType();
|
||||
}
|
||||
var net = conf.GetOrDefault<bool>("regtest", false) ? ChainType.Regtest :
|
||||
conf.GetOrDefault<bool>("testnet", false) ? ChainType.Test : ChainType.Main;
|
||||
|
||||
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;
|
||||
}
|
||||
return net;
|
||||
}
|
||||
|
||||
var net = conf.GetOrDefault<bool>("regtest", false) ? Network.RegTest :
|
||||
conf.GetOrDefault<bool>("testnet", false) ? Network.TestNet : Network.Main;
|
||||
|
||||
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 defaultSettings = BTCPayDefaultSettings.GetDefaultSettings(GetChainType(conf));
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.AppendLine("### Global settings ###");
|
||||
builder.AppendLine("#network=mainnet");
|
||||
builder.AppendLine();
|
||||
builder.AppendLine("### Server settings ###");
|
||||
builder.AppendLine("#port=" + defaultSettings.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 ###");
|
||||
foreach (var n in new BTCPayNetworkProvider(defaultSettings.ChainType).GetAll())
|
||||
{
|
||||
builder.AppendLine($"#{n.CryptoCode}.explorer.url={n.NBXplorerNetwork.DefaultSettings.DefaultUrl}");
|
||||
builder.AppendLine($"#{n.CryptoCode}.explorer.cookiefile={ n.NBXplorerNetwork.DefaultSettings.DefaultCookieFile}");
|
||||
}
|
||||
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"), BTCPayDefaultSettings.GetDefaultSettings(GetChainType(conf)).DefaultPort);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,103 +0,0 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using NBitcoin;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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 static string ToStringAll()
|
||||
{
|
||||
return string.Join(", ", _Networks.Select(n => n.Key).ToArray());
|
||||
}
|
||||
}
|
||||
}
|
@ -12,69 +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);
|
||||
pairingEntity.SIN = sin;
|
||||
if(!await _TokenRepository.PairWithSINAsync(request.PairingCode, sin))
|
||||
throw new BitpayHttpException(400, "Unknown pairing code");
|
||||
|
||||
}
|
||||
pairingEntity = await _TokenRepository.GetPairingAsync(request.PairingCode);
|
||||
if (pairingEntity == null)
|
||||
throw new BitpayHttpException(404, "The specified pairingCode is not found");
|
||||
pairingEntity.SIN = sin;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
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 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -15,506 +15,508 @@ using BTCPayServer.Models.AccountViewModels;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Mails;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Logging;
|
||||
|
||||
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;
|
||||
StoreRepository storeRepository;
|
||||
RoleManager<IdentityRole> _RoleManager;
|
||||
SettingsRepository _SettingsRepository;
|
||||
ILogger _logger;
|
||||
|
||||
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)
|
||||
{
|
||||
this.storeRepository = storeRepository;
|
||||
_userManager = userManager;
|
||||
_signInManager = signInManager;
|
||||
_emailSender = emailSender;
|
||||
_RoleManager = roleManager;
|
||||
_SettingsRepository = settingsRepository;
|
||||
_logger = Logs.PayServer;
|
||||
}
|
||||
|
||||
[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, StringComparison.InvariantCulture).Replace("-", string.Empty, StringComparison.InvariantCulture);
|
||||
|
||||
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, StringComparison.InvariantCulture);
|
||||
|
||||
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 async Task<IActionResult> Register(string returnUrl = null)
|
||||
{
|
||||
var policies = await _SettingsRepository.GetSettingAsync<PoliciesSettings>() ?? new PoliciesSettings();
|
||||
if (policies.LockSubscription)
|
||||
return RedirectToAction(nameof(HomeController.Index), "Home");
|
||||
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;
|
||||
var policies = await _SettingsRepository.GetSettingAsync<PoliciesSettings>() ?? new PoliciesSettings();
|
||||
if (policies.LockSubscription)
|
||||
return RedirectToAction(nameof(HomeController.Index), "Home");
|
||||
if (ModelState.IsValid)
|
||||
{
|
||||
var user = new ApplicationUser { UserName = model.Email, Email = model.Email, RequiresEmailConfirmation = policies.RequiresConfirmedEmail };
|
||||
var result = await _userManager.CreateAsync(user, model.Password);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
var admin = await _userManager.GetUsersInRoleAsync(Roles.ServerAdmin);
|
||||
Logs.PayServer.LogInformation($"A new user just registered {user.Email} {(admin.Count == 0 ? "(admin)" : "")}");
|
||||
if (admin.Count == 0)
|
||||
{
|
||||
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);
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -1,116 +0,0 @@
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.Servcices.Invoices;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitcoin;
|
||||
using NBXplorer;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using NBXplorer.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
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 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);
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
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 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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
@ -10,112 +10,132 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Servcices.Invoices;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using BTCPayServer.Services.Stores;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public partial class InvoiceController
|
||||
[EnableCors("BitpayAPI")]
|
||||
[BitpayAPIConstraint]
|
||||
public class InvoiceControllerAPI : Controller
|
||||
{
|
||||
[HttpPost]
|
||||
[Route("invoices")]
|
||||
[MediaTypeConstraint("application/json")]
|
||||
[BitpayAPIConstraint]
|
||||
public async Task<DataWrapper<InvoiceResponse>> CreateInvoice([FromBody] Invoice invoice)
|
||||
{
|
||||
var bitToken = await CheckTokenPermissionAsync(Facade.Merchant, invoice.Token);
|
||||
var store = await FindStore(bitToken);
|
||||
return await CreateInvoiceCore(invoice, store);
|
||||
}
|
||||
private InvoiceController _InvoiceController;
|
||||
private InvoiceRepository _InvoiceRepository;
|
||||
private TokenRepository _TokenRepository;
|
||||
private StoreRepository _StoreRepository;
|
||||
private BTCPayNetworkProvider _NetworkProvider;
|
||||
|
||||
[HttpGet]
|
||||
[Route("invoices/{id}")]
|
||||
[BitpayAPIConstraint]
|
||||
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");
|
||||
public InvoiceControllerAPI(InvoiceController invoiceController,
|
||||
InvoiceRepository invoceRepository,
|
||||
TokenRepository tokenRepository,
|
||||
StoreRepository storeRepository,
|
||||
BTCPayNetworkProvider networkProvider)
|
||||
{
|
||||
this._InvoiceController = invoiceController;
|
||||
this._InvoiceRepository = invoceRepository;
|
||||
this._TokenRepository = tokenRepository;
|
||||
this._StoreRepository = storeRepository;
|
||||
this._NetworkProvider = networkProvider;
|
||||
}
|
||||
|
||||
var resp = invoice.EntityToDTO();
|
||||
return new DataWrapper<InvoiceResponse>(resp);
|
||||
}
|
||||
[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")]
|
||||
[BitpayAPIConstraint]
|
||||
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/{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(_NetworkProvider);
|
||||
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
|
||||
};
|
||||
|
||||
|
||||
var entities = (await _InvoiceRepository.GetInvoices(query))
|
||||
.Select((o) => o.EntityToDTO()).ToArray();
|
||||
var entities = (await _InvoiceRepository.GetInvoices(query))
|
||||
.Select((o) => o.EntityToDTO(_NetworkProvider)).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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -9,64 +9,78 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
|
||||
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}/{cryptoCode?}")]
|
||||
[AcceptMediaTypeConstraint("application/bitcoin-paymentrequest")]
|
||||
public async Task<IActionResult> GetInvoiceRequest(string invoiceId, string cryptoCode = null)
|
||||
{
|
||||
if (cryptoCode == null)
|
||||
cryptoCode = "BTC";
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
|
||||
var network = _NetworkProvider.GetNetwork(cryptoCode);
|
||||
var paymentMethodId = new PaymentMethodId(cryptoCode, Payments.PaymentTypes.BTCLike);
|
||||
if (invoice == null || invoice.IsExpired() || network == null || !invoice.Support(paymentMethodId))
|
||||
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(_NetworkProvider);
|
||||
var paymentMethod = dto.CryptoInfo.First(c => c.GetpaymentMethodId() == paymentMethodId);
|
||||
PaymentRequest request = new PaymentRequest
|
||||
{
|
||||
DetailsVersion = 1
|
||||
};
|
||||
request.Details.Expires = invoice.ExpirationTime;
|
||||
request.Details.Memo = invoice.ProductInformation.ItemDesc;
|
||||
request.Details.Network = network.NBitcoinNetwork;
|
||||
request.Details.Outputs.Add(new PaymentOutput() { Amount = paymentMethod.Due, Script = BitcoinAddress.Create(paymentMethod.Address, network.NBitcoinNetwork).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)]
|
||||
[Route("i/{invoiceId}/{cryptoCode}", Order = 99)]
|
||||
[MediaTypeConstraint("application/bitcoin-payment")]
|
||||
public async Task<IActionResult> PostPayment(string invoiceId, string cryptoCode = null)
|
||||
{
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
|
||||
if (cryptoCode == null)
|
||||
cryptoCode = "BTC";
|
||||
var network = _NetworkProvider.GetNetwork(cryptoCode);
|
||||
if (network == null || invoice == null || invoice.IsExpired() || !invoice.Support(new Services.Invoices.PaymentMethodId(cryptoCode, Payments.PaymentTypes.BTCLike)))
|
||||
return NotFound();
|
||||
|
||||
var wallet = _WalletProvider.GetWallet(network);
|
||||
if (wallet == null)
|
||||
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(), network.NBitcoinNetwork);
|
||||
return new PaymentAckActionResult(payment.CreateACK(invoiceId + " is currently processing, thanks for your purchase..."));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,9 @@
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Filters;
|
||||
using BTCPayServer.Models.InvoicingModels;
|
||||
using BTCPayServer.Servcices.Invoices;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using NBitcoin;
|
||||
@ -14,193 +15,437 @@ using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using System.Net.WebSockets;
|
||||
using System.Threading;
|
||||
using BTCPayServer.Events;
|
||||
using NBXplorer;
|
||||
using BTCPayServer.Payments;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public partial class InvoiceController
|
||||
{
|
||||
[HttpGet]
|
||||
[Route("invoices/{invoiceId}")]
|
||||
public async Task<IActionResult> Invoice(string invoiceId)
|
||||
{
|
||||
var invoice = (await _InvoiceRepository.GetInvoices(new InvoiceQuery()
|
||||
{
|
||||
UserId = GetUserId(),
|
||||
InvoiceId = invoiceId,
|
||||
IncludeAddresses = true,
|
||||
IncludeEvents = true
|
||||
})).FirstOrDefault();
|
||||
if (invoice == null)
|
||||
return NotFound();
|
||||
|
||||
[HttpGet]
|
||||
[Route("i/{invoiceId}")]
|
||||
[AcceptMediaTypeConstraint("application/bitcoin-paymentrequest", false)]
|
||||
public async Task<IActionResult> Checkout(string invoiceId)
|
||||
{
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
|
||||
if(invoice == null)
|
||||
return NotFound();
|
||||
var store = await _StoreRepository.FindStore(invoice.StoreId);
|
||||
var dto = invoice.EntityToDTO();
|
||||
var dto = invoice.EntityToDTO(_NetworkProvider);
|
||||
var store = await _StoreRepository.FindStore(invoice.StoreId);
|
||||
|
||||
var model = new PaymentModel()
|
||||
{
|
||||
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(),
|
||||
RedirectUrl = invoice.RedirectURL,
|
||||
StoreName = store.StoreName,
|
||||
TxFees = invoice.TxFee.ToString(),
|
||||
InvoiceBitcoinUrl = dto.PaymentUrls.BIP72,
|
||||
TxCount = invoice.GetTxCount(),
|
||||
BTCPaid = invoice.GetTotalPaid().ToString(),
|
||||
Status = invoice.Status
|
||||
};
|
||||
InvoiceDetailsModel model = new InvoiceDetailsModel()
|
||||
{
|
||||
StoreName = store.StoreName,
|
||||
StoreLink = Url.Action(nameof(StoresController.UpdateStore), "Stores", new { storeId = store.Id }),
|
||||
Id = invoice.Id,
|
||||
Status = invoice.Status,
|
||||
TransactionSpeed = invoice.SpeedPolicy == SpeedPolicy.HighSpeed ? "high" : invoice.SpeedPolicy == SpeedPolicy.MediumSpeed ? "medium" : "low",
|
||||
RefundEmail = invoice.RefundMail,
|
||||
CreatedDate = invoice.InvoiceTime,
|
||||
ExpirationDate = invoice.ExpirationTime,
|
||||
MonitoringDate = invoice.MonitoringExpiration,
|
||||
OrderId = invoice.OrderId,
|
||||
BuyerInformation = invoice.BuyerInformation,
|
||||
Fiat = FormatCurrency((decimal)dto.Price, dto.Currency),
|
||||
NotificationUrl = invoice.NotificationURL,
|
||||
RedirectUrl = invoice.RedirectURL,
|
||||
ProductInformation = invoice.ProductInformation,
|
||||
StatusException = invoice.ExceptionStatus,
|
||||
Events = invoice.Events
|
||||
};
|
||||
|
||||
var expiration = TimeSpan.FromSeconds((double)model.ExpirationSeconds);
|
||||
model.TimeLeft = PrettyPrint(expiration);
|
||||
return View(model);
|
||||
}
|
||||
foreach (var data in invoice.GetPaymentMethods(null))
|
||||
{
|
||||
var cryptoInfo = dto.CryptoInfo.First(o => o.GetpaymentMethodId() == data.GetId());
|
||||
var accounting = data.Calculate();
|
||||
var paymentNetwork = _NetworkProvider.GetNetwork(data.GetId().CryptoCode);
|
||||
var cryptoPayment = new InvoiceDetailsModel.CryptoPayment();
|
||||
cryptoPayment.CryptoCode = paymentNetwork.CryptoCode;
|
||||
cryptoPayment.Due = accounting.Due.ToString() + $" {paymentNetwork.CryptoCode}";
|
||||
cryptoPayment.Paid = accounting.CryptoPaid.ToString() + $" {paymentNetwork.CryptoCode}";
|
||||
|
||||
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();
|
||||
}
|
||||
var onchainMethod = data.GetPaymentMethodDetails() as Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod;
|
||||
if(onchainMethod != null)
|
||||
{
|
||||
cryptoPayment.Address = onchainMethod.DepositAddress.ToString();
|
||||
}
|
||||
cryptoPayment.Rate = FormatCurrency(data);
|
||||
cryptoPayment.PaymentUrl = cryptoInfo.PaymentUrls.BIP21;
|
||||
model.CryptoPayments.Add(cryptoPayment);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("i/{invoiceId}/status")]
|
||||
public async Task<IActionResult> GetStatus(string invoiceId)
|
||||
{
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
|
||||
if(invoice == null)
|
||||
return NotFound();
|
||||
return Content(invoice.Status);
|
||||
}
|
||||
var payments = invoice
|
||||
.GetPayments()
|
||||
.Where(p => p.GetpaymentMethodId().PaymentType == PaymentTypes.BTCLike)
|
||||
.Select(async payment =>
|
||||
{
|
||||
var paymentData = (Payments.Bitcoin.BitcoinLikePaymentData)payment.GetCryptoPaymentData();
|
||||
var m = new InvoiceDetailsModel.Payment();
|
||||
var paymentNetwork = _NetworkProvider.GetNetwork(payment.GetCryptoCode());
|
||||
m.CryptoCode = payment.GetCryptoCode();
|
||||
m.DepositAddress = paymentData.Output.ScriptPubKey.GetDestinationAddress(paymentNetwork.NBitcoinNetwork);
|
||||
|
||||
[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();
|
||||
}
|
||||
int confirmationCount = 0;
|
||||
if(paymentData.Legacy) // The confirmation count in the paymentData is not up to date
|
||||
{
|
||||
confirmationCount = (await _ExplorerClients.GetExplorerClient(payment.GetCryptoCode())?.GetTransactionAsync(paymentData.Outpoint.Hash))?.Confirmations ?? 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
confirmationCount = paymentData.ConfirmationCount;
|
||||
}
|
||||
if(confirmationCount >= paymentNetwork.MaxTrackedConfirmation)
|
||||
{
|
||||
m.Confirmations = "At least " + (paymentNetwork.MaxTrackedConfirmation);
|
||||
}
|
||||
else
|
||||
{
|
||||
m.Confirmations = confirmationCount.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
m.TransactionId = paymentData.Outpoint.Hash.ToString();
|
||||
m.ReceivedTime = payment.ReceivedTime;
|
||||
m.TransactionLink = string.Format(CultureInfo.InvariantCulture, paymentNetwork.BlockExplorerLink, m.TransactionId);
|
||||
m.Replaced = !payment.Accounted;
|
||||
return m;
|
||||
})
|
||||
.ToArray();
|
||||
await Task.WhenAll(payments);
|
||||
model.Addresses = invoice.HistoricalAddresses;
|
||||
model.Payments = payments.Select(p => p.GetAwaiter().GetResult()).ToList();
|
||||
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("i/{invoiceId}")]
|
||||
[Route("i/{invoiceId}/{paymentMethodId}")]
|
||||
[Route("invoice")]
|
||||
[AcceptMediaTypeConstraint("application/bitcoin-paymentrequest", false)]
|
||||
[XFrameOptionsAttribute(null)]
|
||||
public async Task<IActionResult> Checkout(string invoiceId, string id = null, string paymentMethodId = null)
|
||||
{
|
||||
//Keep compatibility with Bitpay
|
||||
invoiceId = invoiceId ?? id;
|
||||
id = invoiceId;
|
||||
////
|
||||
|
||||
[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 });
|
||||
}
|
||||
var model = await GetInvoiceModel(invoiceId, paymentMethodId);
|
||||
if (model == null)
|
||||
return NotFound();
|
||||
|
||||
[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);
|
||||
|
||||
StatusMessage = $"Invoice {result.Data.Id} just created!";
|
||||
return RedirectToAction(nameof(ListInvoices));
|
||||
}
|
||||
return View(nameof(Checkout), model);
|
||||
}
|
||||
|
||||
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<PaymentModel> GetInvoiceModel(string invoiceId, string paymentMethodIdStr)
|
||||
{
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
|
||||
if (invoice == null)
|
||||
return null;
|
||||
var store = await _StoreRepository.FindStore(invoice.StoreId);
|
||||
bool isDefaultCrypto = false;
|
||||
if (paymentMethodIdStr == null)
|
||||
{
|
||||
paymentMethodIdStr = store.GetDefaultCrypto();
|
||||
isDefaultCrypto = true;
|
||||
}
|
||||
|
||||
[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,
|
||||
});
|
||||
}
|
||||
var paymentMethodId = PaymentMethodId.Parse(paymentMethodIdStr);
|
||||
var network = _NetworkProvider.GetNetwork(paymentMethodId.CryptoCode);
|
||||
if (invoice == null || network == null)
|
||||
return null;
|
||||
if (!invoice.Support(paymentMethodId))
|
||||
{
|
||||
if(!isDefaultCrypto)
|
||||
return null;
|
||||
var paymentMethodTemp = invoice.GetPaymentMethods(_NetworkProvider).First();
|
||||
network = paymentMethodTemp.Network;
|
||||
paymentMethodId = paymentMethodTemp.GetId();
|
||||
}
|
||||
|
||||
[TempData]
|
||||
public string StatusMessage
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
var paymentMethod = invoice.GetPaymentMethod(paymentMethodId, _NetworkProvider);
|
||||
var paymentMethodDetails = paymentMethod.GetPaymentMethodDetails();
|
||||
var dto = invoice.EntityToDTO(_NetworkProvider);
|
||||
var cryptoInfo = dto.CryptoInfo.First(o => o.GetpaymentMethodId() == paymentMethodId);
|
||||
|
||||
private string GetUserId()
|
||||
{
|
||||
return _UserManager.GetUserId(User);
|
||||
}
|
||||
}
|
||||
var currency = invoice.ProductInformation.Currency;
|
||||
var accounting = paymentMethod.Calculate();
|
||||
var model = new PaymentModel()
|
||||
{
|
||||
CryptoCode = network.CryptoCode,
|
||||
PaymentMethodId = paymentMethodId.ToString(),
|
||||
ServerUrl = HttpContext.Request.GetAbsoluteRoot(),
|
||||
OrderId = invoice.OrderId,
|
||||
InvoiceId = invoice.Id,
|
||||
BtcAddress = paymentMethodDetails.GetPaymentDestination(),
|
||||
OrderAmount = (accounting.TotalDue - accounting.NetworkFee).ToString(),
|
||||
BtcDue = accounting.Due.ToString(),
|
||||
CustomerEmail = invoice.RefundMail,
|
||||
ExpirationSeconds = Math.Max(0, (int)(invoice.ExpirationTime - DateTimeOffset.UtcNow).TotalSeconds),
|
||||
MaxTimeSeconds = (int)(invoice.ExpirationTime - invoice.InvoiceTime).TotalSeconds,
|
||||
MaxTimeMinutes = (int)(invoice.ExpirationTime - invoice.InvoiceTime).TotalMinutes,
|
||||
ItemDesc = invoice.ProductInformation.ItemDesc,
|
||||
Rate = FormatCurrency(paymentMethod),
|
||||
MerchantRefLink = invoice.RedirectURL ?? "/",
|
||||
StoreName = store.StoreName,
|
||||
InvoiceBitcoinUrl = cryptoInfo.PaymentUrls.BIP21,
|
||||
TxCount = accounting.TxCount,
|
||||
BtcPaid = accounting.Paid.ToString(),
|
||||
Status = invoice.Status,
|
||||
CryptoImage = "/" + Url.Content(network.CryptoImagePath),
|
||||
NetworkFeeDescription = $"{accounting.TxCount} transaction{(accounting.TxCount > 1 ? "s" : "")} x {paymentMethodDetails.GetTxFee()} {network.CryptoCode}",
|
||||
AvailableCryptos = invoice.GetPaymentMethods(_NetworkProvider)
|
||||
.Where(i => i.Network != null)
|
||||
.Select(kv=> new PaymentModel.AvailableCrypto()
|
||||
{
|
||||
PaymentMethodId = kv.GetId().ToString(),
|
||||
CryptoImage = "/" + kv.Network.CryptoImagePath,
|
||||
Link = Url.Action(nameof(Checkout), new { invoiceId = invoiceId, paymentMethodId = kv.GetId().ToString() })
|
||||
}).Where(c => c.CryptoImage != "/")
|
||||
.ToList()
|
||||
};
|
||||
|
||||
var isMultiCurrency = invoice.GetPayments().Select(p=>p.GetCryptoCode()).Concat(new[] { network.CryptoCode }).Distinct().Count() > 1;
|
||||
if (isMultiCurrency)
|
||||
model.NetworkFeeDescription = $"{accounting.NetworkFee} {network.CryptoCode}";
|
||||
|
||||
var expiration = TimeSpan.FromSeconds(model.ExpirationSeconds);
|
||||
model.TimeLeft = PrettyPrint(expiration);
|
||||
return model;
|
||||
}
|
||||
|
||||
private string FormatCurrency(PaymentMethod paymentMethod)
|
||||
{
|
||||
string currency = paymentMethod.ParentEntity.ProductInformation.Currency;
|
||||
return FormatCurrency(paymentMethod.Rate, currency);
|
||||
}
|
||||
public string FormatCurrency(decimal price, string currency)
|
||||
{
|
||||
return price.ToString("C", _CurrencyNameTable.GetCurrencyProvider(currency)) + $" ({currency})";
|
||||
}
|
||||
|
||||
private string PrettyPrint(TimeSpan expiration)
|
||||
{
|
||||
StringBuilder builder = new StringBuilder();
|
||||
if (expiration.Days >= 1)
|
||||
builder.Append(expiration.Days.ToString(CultureInfo.InvariantCulture));
|
||||
if (expiration.Hours >= 1)
|
||||
builder.Append(expiration.Hours.ToString("00", CultureInfo.InvariantCulture));
|
||||
builder.Append($"{expiration.Minutes.ToString("00", CultureInfo.InvariantCulture)}:{expiration.Seconds.ToString("00", CultureInfo.InvariantCulture)}");
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("i/{invoiceId}/status")]
|
||||
[Route("i/{invoiceId}/{paymentMethodId}/status")]
|
||||
public async Task<IActionResult> GetStatus(string invoiceId, string paymentMethodId = null)
|
||||
{
|
||||
var model = await GetInvoiceModel(invoiceId, paymentMethodId);
|
||||
if (model == null)
|
||||
return NotFound();
|
||||
return Json(model);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("i/{invoiceId}/status/ws")]
|
||||
public async Task<IActionResult> GetStatusWebSocket(string invoiceId)
|
||||
{
|
||||
if (!HttpContext.WebSockets.IsWebSocketRequest)
|
||||
return NotFound();
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
|
||||
if (invoice == null || invoice.Status == "complete" || invoice.Status == "invalid" || invoice.Status == "expired")
|
||||
return NotFound();
|
||||
var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
|
||||
CompositeDisposable leases = new CompositeDisposable();
|
||||
try
|
||||
{
|
||||
leases.Add(_EventAggregator.Subscribe<Events.InvoiceDataChangedEvent>(async o => await NotifySocket(webSocket, o.InvoiceId, invoiceId)));
|
||||
leases.Add(_EventAggregator.Subscribe<Events.InvoiceNewAddressEvent>(async o => await NotifySocket(webSocket, o.InvoiceId, invoiceId)));
|
||||
leases.Add(_EventAggregator.Subscribe<Events.InvoiceEvent>(async o => await NotifySocket(webSocket, o.InvoiceId, invoiceId)));
|
||||
while (true)
|
||||
{
|
||||
var message = await webSocket.ReceiveAsync(DummyBuffer, default(CancellationToken));
|
||||
if (message.MessageType == WebSocketMessageType.Close)
|
||||
break;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
leases.Dispose();
|
||||
await webSocket.CloseSocket();
|
||||
}
|
||||
return new EmptyResult();
|
||||
}
|
||||
|
||||
ArraySegment<Byte> DummyBuffer = new ArraySegment<Byte>(new Byte[1]);
|
||||
private async Task NotifySocket(WebSocket webSocket, string invoiceId, string expectedId)
|
||||
{
|
||||
if (invoiceId != expectedId || webSocket.State != WebSocketState.Open)
|
||||
return;
|
||||
CancellationTokenSource cts = new CancellationTokenSource();
|
||||
cts.CancelAfter(5000);
|
||||
try
|
||||
{
|
||||
await webSocket.SendAsync(DummyBuffer, WebSocketMessageType.Binary, true, cts.Token);
|
||||
}
|
||||
catch { try { webSocket.Dispose(); } catch { } }
|
||||
}
|
||||
|
||||
[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();
|
||||
var filterString = new SearchString(searchTerm);
|
||||
foreach (var invoice in await _InvoiceRepository.GetInvoices(new InvoiceQuery()
|
||||
{
|
||||
TextSearch = filterString.TextSearch,
|
||||
Count = count,
|
||||
Skip = skip,
|
||||
UserId = GetUserId(),
|
||||
Status = filterString.Filters.TryGet("status"),
|
||||
StoreId = filterString.Filters.TryGet("storeid")
|
||||
}))
|
||||
{
|
||||
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 });
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("invoices/create")]
|
||||
[Authorize(AuthenticationSchemes = "Identity.Application")]
|
||||
[BitpayAPIConstraint(false)]
|
||||
public async Task<IActionResult> CreateInvoice(CreateInvoiceModel model)
|
||||
{
|
||||
model.Stores = await GetStores(GetUserId(), model.StoreId);
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(model);
|
||||
}
|
||||
var store = await _StoreRepository.FindStore(model.StoreId, GetUserId());
|
||||
if (store.GetDerivationStrategies(_NetworkProvider).Count() == 0)
|
||||
{
|
||||
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
|
||||
});
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var result = await CreateInvoiceCore(new Invoice()
|
||||
{
|
||||
Price = model.Amount.Value,
|
||||
Currency = model.Currency,
|
||||
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));
|
||||
}
|
||||
catch (RateUnavailableException)
|
||||
{
|
||||
ModelState.TryAddModelError(nameof(model.Currency), "Unsupported currency");
|
||||
return View(model);
|
||||
}
|
||||
}
|
||||
|
||||
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]
|
||||
[Route("invoices/invalidatepaid")]
|
||||
[Authorize(AuthenticationSchemes = "Identity.Application")]
|
||||
[BitpayAPIConstraint(false)]
|
||||
public async Task<IActionResult> InvalidatePaidInvoice(string invoiceId)
|
||||
{
|
||||
await _InvoiceRepository.UpdatePaidInvoiceToInvalid(invoiceId);
|
||||
_EventAggregator.Publish(new InvoiceEvent(invoiceId, 1008, "invoice_markedInvalid"));
|
||||
return RedirectToAction(nameof(ListInvoices));
|
||||
}
|
||||
|
||||
[TempData]
|
||||
public string StatusMessage
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
private string GetUserId()
|
||||
{
|
||||
return _UserManager.GetUserId(User);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -29,7 +29,7 @@ using BTCPayServer.Services;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.RegularExpressions;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Servcices.Invoices;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using BTCPayServer.Validations;
|
||||
@ -37,85 +37,195 @@ using BTCPayServer.Validations;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.AspNetCore.Mvc.Routing;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using NBXplorer;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Payments;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public partial class InvoiceController : Controller
|
||||
{
|
||||
TokenRepository _TokenRepository;
|
||||
InvoiceRepository _InvoiceRepository;
|
||||
BTCPayWallet _Wallet;
|
||||
IRateProvider _RateProvider;
|
||||
private InvoiceWatcher _Watcher;
|
||||
StoreRepository _StoreRepository;
|
||||
Network _Network;
|
||||
UserManager<ApplicationUser> _UserManager;
|
||||
IFeeProvider _FeeProvider;
|
||||
public partial class InvoiceController : Controller
|
||||
{
|
||||
InvoiceRepository _InvoiceRepository;
|
||||
BTCPayWalletProvider _WalletProvider;
|
||||
IRateProviderFactory _RateProviders;
|
||||
StoreRepository _StoreRepository;
|
||||
UserManager<ApplicationUser> _UserManager;
|
||||
IFeeProviderFactory _FeeProviderFactory;
|
||||
private CurrencyNameTable _CurrencyNameTable;
|
||||
EventAggregator _EventAggregator;
|
||||
BTCPayNetworkProvider _NetworkProvider;
|
||||
ExplorerClientProvider _ExplorerClients;
|
||||
public InvoiceController(InvoiceRepository invoiceRepository,
|
||||
CurrencyNameTable currencyNameTable,
|
||||
UserManager<ApplicationUser> userManager,
|
||||
BTCPayWalletProvider walletProvider,
|
||||
IRateProviderFactory rateProviders,
|
||||
StoreRepository storeRepository,
|
||||
EventAggregator eventAggregator,
|
||||
BTCPayNetworkProvider networkProvider,
|
||||
ExplorerClientProvider explorerClientProviders,
|
||||
IFeeProviderFactory feeProviderFactory)
|
||||
{
|
||||
_ExplorerClients = explorerClientProviders;
|
||||
_CurrencyNameTable = currencyNameTable ?? throw new ArgumentNullException(nameof(currencyNameTable));
|
||||
_StoreRepository = storeRepository ?? throw new ArgumentNullException(nameof(storeRepository));
|
||||
_InvoiceRepository = invoiceRepository ?? throw new ArgumentNullException(nameof(invoiceRepository));
|
||||
_WalletProvider = walletProvider ?? throw new ArgumentNullException(nameof(walletProvider));
|
||||
_RateProviders = rateProviders ?? throw new ArgumentNullException(nameof(rateProviders));
|
||||
_UserManager = userManager;
|
||||
_FeeProviderFactory = feeProviderFactory ?? throw new ArgumentNullException(nameof(feeProviderFactory));
|
||||
_EventAggregator = eventAggregator;
|
||||
_NetworkProvider = networkProvider;
|
||||
}
|
||||
|
||||
public InvoiceController(
|
||||
Network network,
|
||||
InvoiceRepository invoiceRepository,
|
||||
UserManager<ApplicationUser> userManager,
|
||||
TokenRepository tokenRepository,
|
||||
BTCPayWallet wallet,
|
||||
IRateProvider rateProvider,
|
||||
StoreRepository storeRepository,
|
||||
InvoiceWatcher watcher,
|
||||
IFeeProvider feeProvider)
|
||||
{
|
||||
_StoreRepository = storeRepository ?? throw new ArgumentNullException(nameof(storeRepository));
|
||||
_Network = network ?? throw new ArgumentNullException(nameof(network));
|
||||
_TokenRepository = tokenRepository ?? throw new ArgumentNullException(nameof(tokenRepository));
|
||||
_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));
|
||||
}
|
||||
|
||||
private async Task<DataWrapper<InvoiceResponse>> CreateInvoiceCore(Invoice invoice, StoreData store)
|
||||
{
|
||||
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 + TimeSpan.FromMinutes(15.0);
|
||||
entity.ServerUrl = HttpContext.Request.GetAbsoluteRoot();
|
||||
entity.FullNotifications = invoice.FullNotifications;
|
||||
entity.NotificationURL = notificationUri?.AbsoluteUri;
|
||||
entity.BuyerInformation = Map<Invoice, BuyerInformation>(invoice);
|
||||
entity.RefundMail = EmailValidator.IsEmail(entity?.BuyerInformation?.BuyerEmail) ? entity.BuyerInformation.BuyerEmail : null;
|
||||
entity.ProductInformation = Map<Invoice, ProductInformation>(invoice);
|
||||
entity.RedirectURL = invoice.RedirectURL ?? store.StoreWebsite;
|
||||
entity.Status = "new";
|
||||
entity.SpeedPolicy = store.SpeedPolicy;
|
||||
entity.TxFee = (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)
|
||||
{
|
||||
var derivationStrategies = store.GetDerivationStrategies(_NetworkProvider).Where(c => _ExplorerClients.IsAvailable(c.Network.CryptoCode)).ToList();
|
||||
if (derivationStrategies.Count == 0)
|
||||
throw new BitpayHttpException(400, "No derivation strategy are available now for this store");
|
||||
var entity = new InvoiceEntity
|
||||
{
|
||||
InvoiceTime = DateTimeOffset.UtcNow
|
||||
};
|
||||
entity.SetDerivationStrategies(derivationStrategies);
|
||||
|
||||
entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity);
|
||||
await _Wallet.MapAsync(entity.DepositAddress.ScriptPubKey, entity.Id);
|
||||
await _Watcher.WatchAsync(entity.Id);
|
||||
var resp = entity.EntityToDTO();
|
||||
return new DataWrapper<InvoiceResponse>(resp) { Facade = "pos/invoice" };
|
||||
}
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
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(storeBlob.InvoiceExpiration);
|
||||
entity.MonitoringExpiration = entity.ExpirationTime + TimeSpan.FromMinutes(storeBlob.MonitoringExpiration);
|
||||
entity.OrderId = invoice.OrderId;
|
||||
entity.ServerUrl = serverUrl;
|
||||
entity.FullNotifications = invoice.FullNotifications || invoice.ExtendedNotifications;
|
||||
entity.ExtendedNotifications = invoice.ExtendedNotifications;
|
||||
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);
|
||||
|
||||
private DerivationStrategyBase ParseDerivationStrategy(string derivationStrategy)
|
||||
{
|
||||
return new DerivationStrategyFactory(_Network).Parse(derivationStrategy);
|
||||
}
|
||||
var queries = derivationStrategies
|
||||
.Select(derivationStrategy => (Wallet: _WalletProvider.GetWallet(derivationStrategy.Network),
|
||||
DerivationStrategy: derivationStrategy.DerivationStrategyBase,
|
||||
Network: derivationStrategy.Network,
|
||||
RateProvider: _RateProviders.GetRateProvider(derivationStrategy.Network, false),
|
||||
FeeRateProvider: _FeeProviderFactory.CreateFeeProvider(derivationStrategy.Network)))
|
||||
.Where(_ => _.Wallet != null &&
|
||||
_.FeeRateProvider != null &&
|
||||
_.RateProvider != null)
|
||||
.Select(_ =>
|
||||
{
|
||||
return new
|
||||
{
|
||||
network = _.Network,
|
||||
getFeeRate = _.FeeRateProvider.GetFeeRateAsync(),
|
||||
getRate = storeBlob.ApplyRateRules(_.Network, _.RateProvider).GetRateAsync(invoice.Currency),
|
||||
getAddress = _.Wallet.ReserveAddressAsync(_.DerivationStrategy)
|
||||
};
|
||||
});
|
||||
|
||||
private TDest Map<TFrom, TDest>(TFrom data)
|
||||
{
|
||||
return JsonConvert.DeserializeObject<TDest>(JsonConvert.SerializeObject(data));
|
||||
}
|
||||
}
|
||||
bool legacyBTCisSet = false;
|
||||
var paymentMethods = new PaymentMethodDictionary();
|
||||
foreach (var q in queries)
|
||||
{
|
||||
PaymentMethod paymentMethod = new PaymentMethod();
|
||||
paymentMethod.SetId(new PaymentMethodId(q.network.CryptoCode, PaymentTypes.BTCLike));
|
||||
Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod onchainMethod = new Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod();
|
||||
onchainMethod.FeeRate = (await q.getFeeRate);
|
||||
onchainMethod.TxFee = GetTxFee(storeBlob, onchainMethod.FeeRate); // assume price for 100 bytes
|
||||
paymentMethod.Rate = await q.getRate;
|
||||
onchainMethod.DepositAddress = (await q.getAddress);
|
||||
paymentMethod.SetPaymentMethodDetails(onchainMethod);
|
||||
#pragma warning disable CS0618
|
||||
if (q.network.IsBTC)
|
||||
{
|
||||
legacyBTCisSet = true;
|
||||
entity.TxFee = paymentMethod.TxFee;
|
||||
entity.Rate = paymentMethod.Rate;
|
||||
entity.DepositAddress = paymentMethod.DepositAddress;
|
||||
}
|
||||
#pragma warning restore CS0618
|
||||
paymentMethods.Add(paymentMethod);
|
||||
}
|
||||
|
||||
if (!legacyBTCisSet)
|
||||
{
|
||||
// Legacy Bitpay clients expect information for BTC information, even if the store do not support it
|
||||
#pragma warning disable CS0618
|
||||
var btc = _NetworkProvider.BTC;
|
||||
var feeProvider = _FeeProviderFactory.CreateFeeProvider(btc);
|
||||
var rateProvider = storeBlob.ApplyRateRules(btc, _RateProviders.GetRateProvider(btc, false));
|
||||
if (feeProvider != null && rateProvider != null)
|
||||
{
|
||||
var gettingFee = feeProvider.GetFeeRateAsync();
|
||||
var gettingRate = rateProvider.GetRateAsync(invoice.Currency);
|
||||
entity.TxFee = GetTxFee(storeBlob, await gettingFee);
|
||||
entity.Rate = await gettingRate;
|
||||
}
|
||||
#pragma warning restore CS0618
|
||||
}
|
||||
|
||||
entity.SetPaymentMethods(paymentMethods);
|
||||
entity.PosData = invoice.PosData;
|
||||
entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity, _NetworkProvider);
|
||||
_EventAggregator.Publish(new Events.InvoiceEvent(entity, 1001, "invoice_created"));
|
||||
var resp = entity.EntityToDTO(_NetworkProvider);
|
||||
return new DataWrapper<InvoiceResponse>(resp) { Facade = "pos/invoice" };
|
||||
}
|
||||
|
||||
private static Money GetTxFee(StoreBlob storeBlob, FeeRate feeRate)
|
||||
{
|
||||
return storeBlob.NetworkFeeDisabled ? Money.Zero : feeRate.GetFee(100);
|
||||
}
|
||||
|
||||
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 DerivationStrategyBase ParseDerivationStrategy(string derivationStrategy, BTCPayNetwork network)
|
||||
{
|
||||
return new DerivationStrategyFactory(network.NBitcoinNetwork).Parse(derivationStrategy);
|
||||
}
|
||||
|
||||
private TDest Map<TFrom, TDest>(TFrom data)
|
||||
{
|
||||
return JsonConvert.DeserializeObject<TDest>(JsonConvert.SerializeObject(data));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,34 +7,68 @@ using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Filters;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
|
||||
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
|
||||
{
|
||||
IRateProviderFactory _RateProviderFactory;
|
||||
BTCPayNetworkProvider _NetworkProvider;
|
||||
CurrencyNameTable _CurrencyNameTable;
|
||||
StoreRepository _StoreRepo;
|
||||
public RateController(
|
||||
IRateProviderFactory rateProviderFactory,
|
||||
BTCPayNetworkProvider networkProvider,
|
||||
StoreRepository storeRepo,
|
||||
CurrencyNameTable currencyNameTable)
|
||||
{
|
||||
_RateProviderFactory = rateProviderFactory ?? throw new ArgumentNullException(nameof(rateProviderFactory));
|
||||
_NetworkProvider = networkProvider;
|
||||
_StoreRepo = storeRepo;
|
||||
_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<IActionResult> GetRates(string cryptoCode = null, string storeId = null)
|
||||
{
|
||||
var result = await GetRates2(cryptoCode, storeId);
|
||||
var rates = (result as JsonResult)?.Value as NBitpayClient.Rate[];
|
||||
if(rates == null)
|
||||
return result;
|
||||
return Json(new DataWrapper<NBitpayClient.Rate[]>(rates));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
[Route("api/rates")]
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GetRates2(string cryptoCode = null, string storeId = null)
|
||||
{
|
||||
cryptoCode = cryptoCode ?? "BTC";
|
||||
var network = _NetworkProvider.GetNetwork(cryptoCode);
|
||||
if (network == null)
|
||||
return NotFound();
|
||||
var rateProvider = _RateProviderFactory.GetRateProvider(network, true);
|
||||
if (rateProvider == null)
|
||||
return NotFound();
|
||||
|
||||
if (storeId != null)
|
||||
{
|
||||
var store = await _StoreRepo.FindStore(storeId);
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
rateProvider = store.GetStoreBlob().ApplyRateRules(network, rateProvider);
|
||||
}
|
||||
|
||||
var allRates = (await rateProvider.GetRatesAsync());
|
||||
return Json(allRates.Select(r =>
|
||||
new NBitpayClient.Rate()
|
||||
{
|
||||
Code = r.Currency,
|
||||
Name = _CurrencyNameTable.GetCurrencyData(r.Currency)?.Name,
|
||||
Value = r.Value
|
||||
}).Where(n => n.Name != null).ToArray());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -16,82 +16,117 @@ 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.StatusMessage = StatusMessage;
|
||||
users.Users
|
||||
= _UserManager.Users.Select(u => new UsersViewModel.UserViewModel()
|
||||
{
|
||||
Name = u.UserName,
|
||||
Email = u.Email,
|
||||
Id = u.Id
|
||||
}).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/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/users/{userId}/delete")]
|
||||
public async Task<IActionResult> DeleteUser(string userId)
|
||||
{
|
||||
var user = userId == null ? null : await _UserManager.FindByIdAsync(userId);
|
||||
if (user == null)
|
||||
return NotFound();
|
||||
return View("Confirm", new ConfirmModel()
|
||||
{
|
||||
Title = "Delete user " + user.Email,
|
||||
Description = "This user will be permanently deleted",
|
||||
Action = "Delete"
|
||||
});
|
||||
}
|
||||
|
||||
[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/users/{userId}/delete")]
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> DeleteUserPost(string userId)
|
||||
{
|
||||
var user = userId == null ? null : await _UserManager.FindByIdAsync(userId);
|
||||
if (user == null)
|
||||
return NotFound();
|
||||
await _UserManager.DeleteAsync(user);
|
||||
StatusMessage = "User deleted";
|
||||
return RedirectToAction(nameof(ListUsers));
|
||||
}
|
||||
|
||||
[TempData]
|
||||
public string StatusMessage
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
[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/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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
98
BTCPayServer/CustomThreadPool.cs
Normal file
98
BTCPayServer/CustomThreadPool.cs
Normal file
@ -0,0 +1,98 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
class CustomThreadPool : IDisposable
|
||||
{
|
||||
CancellationTokenSource _Cancel = new CancellationTokenSource();
|
||||
TaskCompletionSource<bool> _Exited;
|
||||
int _ExitedCount = 0;
|
||||
Thread[] _Threads;
|
||||
Exception _UnhandledException;
|
||||
BlockingCollection<(Action, TaskCompletionSource<object>)> _Actions = new BlockingCollection<(Action, TaskCompletionSource<object>)>(new ConcurrentQueue<(Action, TaskCompletionSource<object>)>());
|
||||
|
||||
public CustomThreadPool(int threadCount, string threadName)
|
||||
{
|
||||
if (threadCount <= 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(threadCount));
|
||||
_Exited = new TaskCompletionSource<bool>();
|
||||
_Threads = Enumerable.Range(0, threadCount).Select(_ => new Thread(RunLoop) { Name = threadName }).ToArray();
|
||||
foreach (var t in _Threads)
|
||||
t.Start();
|
||||
}
|
||||
|
||||
public void Do(Action act)
|
||||
{
|
||||
DoAsync(act).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public T Do<T>(Func<T> act)
|
||||
{
|
||||
return DoAsync(act).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public async Task<T> DoAsync<T>(Func<T> act)
|
||||
{
|
||||
TaskCompletionSource<object> done = new TaskCompletionSource<object>();
|
||||
_Actions.Add((() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
done.TrySetResult(act());
|
||||
}
|
||||
catch (Exception ex) { done.TrySetException(ex); }
|
||||
}
|
||||
, done));
|
||||
return (T)(await done.Task.ConfigureAwait(false));
|
||||
}
|
||||
|
||||
public Task DoAsync(Action act)
|
||||
{
|
||||
return DoAsync<object>(() =>
|
||||
{
|
||||
act();
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
void RunLoop()
|
||||
{
|
||||
try
|
||||
{
|
||||
foreach (var act in _Actions.GetConsumingEnumerable(_Cancel.Token))
|
||||
{
|
||||
act.Item1();
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) when (_Cancel.IsCancellationRequested) { }
|
||||
catch (Exception ex)
|
||||
{
|
||||
_Cancel.Cancel();
|
||||
_UnhandledException = ex;
|
||||
}
|
||||
if (Interlocked.Increment(ref _ExitedCount) == _Threads.Length)
|
||||
{
|
||||
foreach (var action in _Actions)
|
||||
{
|
||||
try
|
||||
{
|
||||
action.Item2.TrySetCanceled();
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
_Exited.TrySetResult(true);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_Cancel.Cancel();
|
||||
_Exited.Task.GetAwaiter().GetResult();
|
||||
}
|
||||
}
|
||||
}
|
@ -2,24 +2,66 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
{
|
||||
public class AddressInvoiceData
|
||||
{
|
||||
public string Address
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public class AddressInvoiceData
|
||||
{
|
||||
/// <summary>
|
||||
/// Some crypto currencies share same address prefix
|
||||
/// For not having exceptions thrown by two address on different network, we suffix by "#CRYPTOCODE"
|
||||
/// </summary>
|
||||
[Obsolete("Use GetHash instead")]
|
||||
public string Address
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public InvoiceData InvoiceData
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public string InvoiceDataId
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
}
|
||||
#pragma warning disable CS0618
|
||||
public string GetAddress()
|
||||
{
|
||||
if (Address == null)
|
||||
return null;
|
||||
var index = Address.LastIndexOf("#", StringComparison.InvariantCulture);
|
||||
if (index == -1)
|
||||
return Address;
|
||||
return Address.Substring(0, index);
|
||||
}
|
||||
public AddressInvoiceData Set(string address, PaymentMethodId paymentMethodId)
|
||||
{
|
||||
Address = address + "#" + paymentMethodId?.ToString();
|
||||
return this;
|
||||
}
|
||||
public PaymentMethodId GetpaymentMethodId()
|
||||
{
|
||||
if (Address == null)
|
||||
return null;
|
||||
var index = Address.LastIndexOf("#", StringComparison.InvariantCulture);
|
||||
// Legacy AddressInvoiceData does not have the paymentMethodId attached to the Address
|
||||
if (index == -1)
|
||||
return PaymentMethodId.Parse("BTC");
|
||||
/////////////////////////
|
||||
return PaymentMethodId.Parse(Address.Substring(index + 1));
|
||||
}
|
||||
#pragma warning restore CS0618
|
||||
|
||||
public InvoiceData InvoiceData
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public string InvoiceDataId
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public DateTimeOffset? CreatedTime
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -10,114 +10,144 @@ 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<PendingInvoiceData> PendingInvoices
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public DbSet<RefundAddressesData> RefundAddresses
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public DbSet<InvoiceEventData> InvoiceEvents
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public DbSet<PaymentData> Payments
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public DbSet<HistoricalAddressInvoiceData> HistoricalAddressInvoices
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public DbSet<StoreData> Stores
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public DbSet<PendingInvoiceData> PendingInvoices
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public DbSet<RefundAddressesData> RefundAddresses
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public DbSet<UserStore> UserStore
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public DbSet<PaymentData> Payments
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public DbSet<AddressInvoiceData> AddressInvoices
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public DbSet<StoreData> Stores
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public DbSet<SettingData> Settings
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public DbSet<UserStore> UserStore
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public DbSet<AddressInvoiceData> AddressInvoices
|
||||
{
|
||||
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>()
|
||||
#pragma warning disable CS0618
|
||||
.HasKey(o => o.Address);
|
||||
#pragma warning restore CS0618
|
||||
|
||||
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,
|
||||
#pragma warning disable CS0618
|
||||
o.Address
|
||||
#pragma warning restore CS0618
|
||||
});
|
||||
|
||||
builder.Entity<InvoiceEventData>()
|
||||
.HasKey(o => new
|
||||
{
|
||||
o.InvoiceDataId,
|
||||
#pragma warning disable CS0618
|
||||
o.UniqueId
|
||||
#pragma warning restore CS0618
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
61
BTCPayServer/Data/HistoricalAddressInvoiceData.cs
Normal file
61
BTCPayServer/Data/HistoricalAddressInvoiceData.cs
Normal file
@ -0,0 +1,61 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
{
|
||||
public class HistoricalAddressInvoiceData
|
||||
{
|
||||
public string InvoiceDataId
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Some crypto currencies share same address prefix
|
||||
/// For not having exceptions thrown by two address on different network, we suffix by "#CRYPTOCODE"
|
||||
/// </summary>
|
||||
[Obsolete("Use GetCryptoCode instead")]
|
||||
public string Address
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
|
||||
[Obsolete("Use GetCryptoCode instead")]
|
||||
public string CryptoCode { get; set; }
|
||||
|
||||
#pragma warning disable CS0618
|
||||
public string GetCryptoCode()
|
||||
{
|
||||
return string.IsNullOrEmpty(CryptoCode) ? "BTC" : CryptoCode;
|
||||
}
|
||||
public string GetAddress()
|
||||
{
|
||||
if (Address == null)
|
||||
return null;
|
||||
var index = Address.IndexOf("#", StringComparison.InvariantCulture);
|
||||
if (index == -1)
|
||||
return Address;
|
||||
return Address.Substring(0, index);
|
||||
}
|
||||
public HistoricalAddressInvoiceData SetAddress(string depositAddress, string cryptoCode)
|
||||
{
|
||||
Address = depositAddress + "#" + cryptoCode;
|
||||
CryptoCode = cryptoCode;
|
||||
return this;
|
||||
}
|
||||
#pragma warning restore CS0618
|
||||
|
||||
public DateTimeOffset Assigned
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public DateTimeOffset? UnAssigned
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
}
|
||||
}
|
@ -6,65 +6,79 @@ 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<InvoiceEventData> Events
|
||||
{
|
||||
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 List<RefundAddressesData> RefundAddresses
|
||||
{
|
||||
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 List<AddressInvoiceData> AddressInvoices
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
22
BTCPayServer/Data/InvoiceEventData.cs
Normal file
22
BTCPayServer/Data/InvoiceEventData.cs
Normal file
@ -0,0 +1,22 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
{
|
||||
public class InvoiceEventData
|
||||
{
|
||||
public string InvoiceDataId
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string UniqueId { get; internal set; }
|
||||
public DateTimeOffset Timestamp
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public string Message { get; set; }
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,25 +5,29 @@ 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;
|
||||
}
|
||||
public bool Accounted
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,9 +7,9 @@ namespace BTCPayServer.Data
|
||||
{
|
||||
public class PendingInvoiceData
|
||||
{
|
||||
public string Id
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
}
|
||||
public string Id
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,55 +1,266 @@
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Servcices.Invoices;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using NBitcoin;
|
||||
using NBXplorer;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.ComponentModel;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using BTCPayServer.Services.Rates;
|
||||
|
||||
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;
|
||||
}
|
||||
[Obsolete("Use GetDerivationStrategies instead")]
|
||||
public string DerivationStrategy
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public string StoreName
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
[Obsolete("Use GetDerivationStrategies instead")]
|
||||
public string DerivationStrategies
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public SpeedPolicy SpeedPolicy
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public IEnumerable<DerivationStrategy> GetDerivationStrategies(BTCPayNetworkProvider networks)
|
||||
{
|
||||
#pragma warning disable CS0618
|
||||
bool btcReturned = false;
|
||||
if (!string.IsNullOrEmpty(DerivationStrategy))
|
||||
{
|
||||
if (networks.BTC != null)
|
||||
{
|
||||
btcReturned = true;
|
||||
yield return BTCPayServer.DerivationStrategy.Parse(DerivationStrategy, networks.BTC);
|
||||
}
|
||||
}
|
||||
|
||||
public string StoreWebsite
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public byte[] StoreCertificate
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
if (!string.IsNullOrEmpty(DerivationStrategies))
|
||||
{
|
||||
JObject strategies = JObject.Parse(DerivationStrategies);
|
||||
foreach (var strat in strategies.Properties())
|
||||
{
|
||||
var network = networks.GetNetwork(strat.Name);
|
||||
if (network != null)
|
||||
{
|
||||
if (network == networks.BTC && btcReturned)
|
||||
continue;
|
||||
if (strat.Value.Type == JTokenType.Null)
|
||||
continue;
|
||||
yield return BTCPayServer.DerivationStrategy.Parse(strat.Value.Value<string>(), network);
|
||||
}
|
||||
}
|
||||
}
|
||||
#pragma warning restore CS0618
|
||||
}
|
||||
|
||||
[NotMapped]
|
||||
public string Role
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
}
|
||||
public void SetDerivationStrategy(BTCPayNetwork network, string derivationScheme)
|
||||
{
|
||||
#pragma warning disable CS0618
|
||||
JObject strategies = string.IsNullOrEmpty(DerivationStrategies) ? new JObject() : JObject.Parse(DerivationStrategies);
|
||||
bool existing = false;
|
||||
foreach (var strat in strategies.Properties().ToList())
|
||||
{
|
||||
if (strat.Name == network.CryptoCode)
|
||||
{
|
||||
if (network.IsBTC)
|
||||
DerivationStrategy = null;
|
||||
if (string.IsNullOrEmpty(derivationScheme))
|
||||
{
|
||||
strat.Remove();
|
||||
}
|
||||
else
|
||||
{
|
||||
strat.Value = new JValue(derivationScheme);
|
||||
}
|
||||
existing = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!existing && string.IsNullOrEmpty(derivationScheme))
|
||||
{
|
||||
if (network.IsBTC)
|
||||
DerivationStrategy = null;
|
||||
}
|
||||
else if (!existing)
|
||||
strategies.Add(new JProperty(network.CryptoCode, new JValue(derivationScheme)));
|
||||
// This is deprecated so we don't have to set anymore
|
||||
//if (network.IsBTC)
|
||||
// DerivationStrategy = derivationScheme;
|
||||
DerivationStrategies = strategies.ToString();
|
||||
#pragma warning restore CS0618
|
||||
}
|
||||
|
||||
public string StoreName
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public SpeedPolicy SpeedPolicy
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public string StoreWebsite
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public byte[] StoreCertificate
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
[NotMapped]
|
||||
public string Role
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public byte[] StoreBlob
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
[Obsolete("Use GetDefaultCrypto instead")]
|
||||
public string DefaultCrypto { get; set; }
|
||||
|
||||
#pragma warning disable CS0618
|
||||
public string GetDefaultCrypto()
|
||||
{
|
||||
return DefaultCrypto ?? "BTC";
|
||||
}
|
||||
public void SetDefaultCrypto(string defaultCryptoCurrency)
|
||||
{
|
||||
DefaultCrypto = defaultCryptoCurrency;
|
||||
}
|
||||
#pragma warning restore CS0618
|
||||
|
||||
static Network Dummy = Network.Main;
|
||||
|
||||
public StoreBlob GetStoreBlob()
|
||||
{
|
||||
return StoreBlob == null ? new StoreBlob() : new Serializer(Dummy).ToObject<StoreBlob>(Encoding.UTF8.GetString(StoreBlob));
|
||||
}
|
||||
|
||||
public bool SetStoreBlob(StoreBlob storeBlob)
|
||||
{
|
||||
var original = new Serializer(Dummy).ToString(GetStoreBlob());
|
||||
var newBlob = new Serializer(Dummy).ToString(storeBlob);
|
||||
if (original == newBlob)
|
||||
return false;
|
||||
StoreBlob = Encoding.UTF8.GetBytes(newBlob);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public class RateRule
|
||||
{
|
||||
public RateRule()
|
||||
{
|
||||
RuleName = "Multiplier";
|
||||
}
|
||||
public string RuleName { get; set; }
|
||||
|
||||
public double Multiplier { get; set; }
|
||||
|
||||
public decimal Apply(BTCPayNetwork network, decimal rate)
|
||||
{
|
||||
return rate * (decimal)Multiplier;
|
||||
}
|
||||
}
|
||||
|
||||
public class StoreBlob
|
||||
{
|
||||
public StoreBlob()
|
||||
{
|
||||
InvoiceExpiration = 15;
|
||||
MonitoringExpiration = 60;
|
||||
}
|
||||
public bool NetworkFeeDisabled
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
[DefaultValue(60)]
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
|
||||
public int MonitoringExpiration
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[DefaultValue(15)]
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
|
||||
public int InvoiceExpiration
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public void SetRateMultiplier(double rate)
|
||||
{
|
||||
RateRules = new List<RateRule>();
|
||||
RateRules.Add(new RateRule() { Multiplier = rate });
|
||||
}
|
||||
public decimal GetRateMultiplier()
|
||||
{
|
||||
decimal rate = 1.0m;
|
||||
if (RateRules == null || RateRules.Count == 0)
|
||||
return rate;
|
||||
foreach (var rule in RateRules)
|
||||
{
|
||||
rate = rule.Apply(null, rate);
|
||||
}
|
||||
return rate;
|
||||
}
|
||||
|
||||
public List<RateRule> RateRules { get; set; } = new List<RateRule>();
|
||||
public string PreferredExchange { get; set; }
|
||||
|
||||
public IRateProvider ApplyRateRules(BTCPayNetwork network, IRateProvider rateProvider)
|
||||
{
|
||||
if (!PreferredExchange.IsCoinAverage())
|
||||
{
|
||||
// If the original rateProvider is a cache, use the same inner provider as fallback, and same memory cache to wrap it all
|
||||
if (rateProvider is CachedRateProvider cachedRateProvider)
|
||||
{
|
||||
rateProvider = new FallbackRateProvider(new IRateProvider[] {
|
||||
new CoinAverageRateProvider(network.CryptoCode) { Exchange = PreferredExchange },
|
||||
cachedRateProvider.Inner
|
||||
});
|
||||
rateProvider = new CachedRateProvider(network.CryptoCode, rateProvider, cachedRateProvider.MemoryCache) { AdditionalScope = PreferredExchange };
|
||||
}
|
||||
else
|
||||
{
|
||||
rateProvider = new FallbackRateProvider(new IRateProvider[] {
|
||||
new CoinAverageRateProvider(network.CryptoCode) { Exchange = PreferredExchange },
|
||||
rateProvider
|
||||
});
|
||||
}
|
||||
}
|
||||
if (RateRules == null || RateRules.Count == 0)
|
||||
return rateProvider;
|
||||
return new TweakRateProvider(network, rateProvider, RateRules.ToList());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
40
BTCPayServer/DerivationStrategy.cs
Normal file
40
BTCPayServer/DerivationStrategy.cs
Normal file
@ -0,0 +1,40 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using NBitcoin;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public class DerivationStrategy
|
||||
{
|
||||
private DerivationStrategyBase _DerivationStrategy;
|
||||
private BTCPayNetwork _Network;
|
||||
|
||||
DerivationStrategy(DerivationStrategyBase result, BTCPayNetwork network)
|
||||
{
|
||||
this._DerivationStrategy = result;
|
||||
this._Network = network;
|
||||
}
|
||||
|
||||
public static DerivationStrategy Parse(string derivationStrategy, BTCPayNetwork network)
|
||||
{
|
||||
if (network == null)
|
||||
throw new ArgumentNullException(nameof(network));
|
||||
if (derivationStrategy == null)
|
||||
throw new ArgumentNullException(nameof(derivationStrategy));
|
||||
var result = new NBXplorer.DerivationStrategy.DerivationStrategyFactory(network.NBitcoinNetwork).Parse(derivationStrategy);
|
||||
return new DerivationStrategy(result, network);
|
||||
}
|
||||
|
||||
public BTCPayNetwork Network { get { return this._Network; } }
|
||||
|
||||
public DerivationStrategyBase DerivationStrategyBase { get { return this._DerivationStrategy; } }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return _DerivationStrategy.ToString();
|
||||
}
|
||||
}
|
||||
}
|
14
BTCPayServer/Eclair/AllChannelResponse.cs
Normal file
14
BTCPayServer/Eclair/AllChannelResponse.cs
Normal file
@ -0,0 +1,14 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Eclair
|
||||
{
|
||||
public class AllChannelResponse
|
||||
{
|
||||
public string ShortChannelId { get; set; }
|
||||
public string NodeId1 { get; set; }
|
||||
public string NodeId2 { get; set; }
|
||||
}
|
||||
}
|
21
BTCPayServer/Eclair/ChannelResponse.cs
Normal file
21
BTCPayServer/Eclair/ChannelResponse.cs
Normal file
@ -0,0 +1,21 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Eclair
|
||||
{
|
||||
public class ChannelResponse
|
||||
{
|
||||
|
||||
public string NodeId { get; set; }
|
||||
public string ChannelId { get; set; }
|
||||
public string State { get; set; }
|
||||
}
|
||||
public static class ChannelStates
|
||||
{
|
||||
public const string WAIT_FOR_FUNDING_CONFIRMED = "WAIT_FOR_FUNDING_CONFIRMED";
|
||||
|
||||
public const string NORMAL = "NORMAL";
|
||||
}
|
||||
}
|
230
BTCPayServer/Eclair/EclairRPCClient.cs
Normal file
230
BTCPayServer/Eclair/EclairRPCClient.cs
Normal file
@ -0,0 +1,230 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using NBitcoin;
|
||||
using NBitcoin.JsonConverters;
|
||||
using NBitcoin.RPC;
|
||||
|
||||
namespace BTCPayServer.Eclair
|
||||
{
|
||||
public class EclairRPCClient
|
||||
{
|
||||
public EclairRPCClient(Uri address, Network network)
|
||||
{
|
||||
if (address == null)
|
||||
throw new ArgumentNullException(nameof(address));
|
||||
if (network == null)
|
||||
throw new ArgumentNullException(nameof(network));
|
||||
Address = address;
|
||||
Network = network;
|
||||
}
|
||||
|
||||
public Network Network { get; private set; }
|
||||
|
||||
|
||||
public GetInfoResponse GetInfo()
|
||||
{
|
||||
return GetInfoAsync().GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public Task<GetInfoResponse> GetInfoAsync()
|
||||
{
|
||||
return SendCommandAsync<GetInfoResponse>(new RPCRequest("getinfo", Array.Empty<object>()));
|
||||
}
|
||||
|
||||
public async Task<T> SendCommandAsync<T>(RPCRequest request, bool throwIfRPCError = true)
|
||||
{
|
||||
var response = await SendCommandAsync(request, throwIfRPCError);
|
||||
return Serializer.ToObject<T>(response.ResultString, Network);
|
||||
}
|
||||
|
||||
public async Task<RPCResponse> SendCommandAsync(RPCRequest request, bool throwIfRPCError = true)
|
||||
{
|
||||
RPCResponse response = null;
|
||||
HttpWebRequest webRequest = response == null ? CreateWebRequest() : null;
|
||||
if (response == null)
|
||||
{
|
||||
var writer = new StringWriter();
|
||||
request.WriteJSON(writer);
|
||||
writer.Flush();
|
||||
var json = writer.ToString();
|
||||
var bytes = Encoding.UTF8.GetBytes(json);
|
||||
#if !(PORTABLE || NETCORE)
|
||||
webRequest.ContentLength = bytes.Length;
|
||||
#endif
|
||||
var dataStream = await webRequest.GetRequestStreamAsync().ConfigureAwait(false);
|
||||
await dataStream.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false);
|
||||
await dataStream.FlushAsync().ConfigureAwait(false);
|
||||
dataStream.Dispose();
|
||||
}
|
||||
WebResponse webResponse = null;
|
||||
WebResponse errorResponse = null;
|
||||
try
|
||||
{
|
||||
webResponse = response == null ? await webRequest.GetResponseAsync().ConfigureAwait(false) : null;
|
||||
response = response ?? RPCResponse.Load(await ToMemoryStreamAsync(webResponse.GetResponseStream()).ConfigureAwait(false));
|
||||
|
||||
if (throwIfRPCError)
|
||||
response.ThrowIfError();
|
||||
}
|
||||
catch (WebException ex)
|
||||
{
|
||||
if (ex.Response == null || ex.Response.ContentLength == 0 ||
|
||||
!ex.Response.ContentType.Equals("application/json", StringComparison.Ordinal))
|
||||
throw;
|
||||
errorResponse = ex.Response;
|
||||
response = RPCResponse.Load(await ToMemoryStreamAsync(errorResponse.GetResponseStream()).ConfigureAwait(false));
|
||||
if (throwIfRPCError)
|
||||
response.ThrowIfError();
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (errorResponse != null)
|
||||
{
|
||||
errorResponse.Dispose();
|
||||
errorResponse = null;
|
||||
}
|
||||
if (webResponse != null)
|
||||
{
|
||||
webResponse.Dispose();
|
||||
webResponse = null;
|
||||
}
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
public AllChannelResponse[] AllChannels()
|
||||
{
|
||||
return AllChannelsAsync().GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public async Task<AllChannelResponse[]> AllChannelsAsync()
|
||||
{
|
||||
return await SendCommandAsync<AllChannelResponse[]>(new RPCRequest("allchannels", Array.Empty<object>())).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public string[] Channels()
|
||||
{
|
||||
return ChannelsAsync().GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public async Task<string[]> ChannelsAsync()
|
||||
{
|
||||
return await SendCommandAsync<string[]>(new RPCRequest("channels", Array.Empty<object>())).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public void Close(string channelId)
|
||||
{
|
||||
CloseAsync(channelId).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public async Task CloseAsync(string channelId)
|
||||
{
|
||||
if (channelId == null)
|
||||
throw new ArgumentNullException(nameof(channelId));
|
||||
try
|
||||
{
|
||||
await SendCommandAsync(new RPCRequest("close", new object[] { channelId })).ConfigureAwait(false);
|
||||
}
|
||||
catch (RPCException ex) when (ex.Message == "closing already in progress")
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public ChannelResponse Channel(string channelId)
|
||||
{
|
||||
return ChannelAsync(channelId).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public async Task<ChannelResponse> ChannelAsync(string channelId)
|
||||
{
|
||||
if (channelId == null)
|
||||
throw new ArgumentNullException(nameof(channelId));
|
||||
return await SendCommandAsync<ChannelResponse>(new RPCRequest("channel", new object[] { channelId })).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public string[] AllNodes()
|
||||
{
|
||||
return AllNodesAsync().GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public async Task<string[]> AllNodesAsync()
|
||||
{
|
||||
return await SendCommandAsync<string[]>(new RPCRequest("allnodes", Array.Empty<object>())).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public Uri Address { get; private set; }
|
||||
|
||||
private HttpWebRequest CreateWebRequest()
|
||||
{
|
||||
var webRequest = (HttpWebRequest)WebRequest.Create(Address.AbsoluteUri);
|
||||
webRequest.ContentType = "application/json";
|
||||
webRequest.Method = "POST";
|
||||
return webRequest;
|
||||
}
|
||||
|
||||
|
||||
private async Task<Stream> ToMemoryStreamAsync(Stream stream)
|
||||
{
|
||||
MemoryStream ms = new MemoryStream();
|
||||
await stream.CopyToAsync(ms).ConfigureAwait(false);
|
||||
ms.Position = 0;
|
||||
return ms;
|
||||
}
|
||||
|
||||
public string Open(NodeInfo node, Money fundingSatoshi, LightMoney pushAmount = null)
|
||||
{
|
||||
return OpenAsync(node, fundingSatoshi, pushAmount).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public string Connect(NodeInfo node)
|
||||
{
|
||||
return ConnectAsync(node).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public async Task<string> ConnectAsync(NodeInfo node)
|
||||
{
|
||||
if (node == null)
|
||||
throw new ArgumentNullException(nameof(node));
|
||||
return (await SendCommandAsync(new RPCRequest("connect", new object[] { node.NodeId, node.Host, node.Port })).ConfigureAwait(false)).ResultString;
|
||||
}
|
||||
|
||||
public string Receive(LightMoney amount, string description = null)
|
||||
{
|
||||
return ReceiveAsync(amount, description).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public async Task<string> ReceiveAsync(LightMoney amount, string description = null)
|
||||
{
|
||||
if (amount == null)
|
||||
throw new ArgumentNullException(nameof(amount));
|
||||
List<object> args = new List<object>();
|
||||
args.Add(amount.MilliSatoshi);
|
||||
if(description != null)
|
||||
{
|
||||
args.Add(description);
|
||||
}
|
||||
return (await SendCommandAsync(new RPCRequest("receive", args.ToArray())).ConfigureAwait(false)).ResultString;
|
||||
}
|
||||
|
||||
public async Task<string> OpenAsync(NodeInfo node, Money fundingSatoshi, LightMoney pushAmount = null)
|
||||
{
|
||||
if (fundingSatoshi == null)
|
||||
throw new ArgumentNullException(nameof(fundingSatoshi));
|
||||
if (node == null)
|
||||
throw new ArgumentNullException(nameof(node));
|
||||
pushAmount = pushAmount ?? LightMoney.Zero;
|
||||
|
||||
var result = await SendCommandAsync(new RPCRequest("open", new object[] { node.NodeId, node.Host, node.Port, fundingSatoshi.Satoshi, pushAmount.MilliSatoshi }));
|
||||
|
||||
return result.ResultString;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
18
BTCPayServer/Eclair/GetInfoResponse.cs
Normal file
18
BTCPayServer/Eclair/GetInfoResponse.cs
Normal file
@ -0,0 +1,18 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using NBitcoin;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Eclair
|
||||
{
|
||||
public class GetInfoResponse
|
||||
{
|
||||
public string NodeId { get; set; }
|
||||
public string Alias { get; set; }
|
||||
public int Port { get; set; }
|
||||
public uint256 ChainHash { get; set; }
|
||||
public int BlockHeight { get; set; }
|
||||
}
|
||||
}
|
569
BTCPayServer/Eclair/LightMoney.cs
Normal file
569
BTCPayServer/Eclair/LightMoney.cs
Normal file
@ -0,0 +1,569 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Eclair
|
||||
{
|
||||
public enum LightMoneyUnit : ulong
|
||||
{
|
||||
BTC = 100000000000,
|
||||
MilliBTC = 100000000,
|
||||
Bit = 100000,
|
||||
Satoshi = 1000,
|
||||
MilliSatoshi = 1
|
||||
}
|
||||
|
||||
public class LightMoney : IComparable, IComparable<LightMoney>, IEquatable<LightMoney>
|
||||
{
|
||||
|
||||
|
||||
// for decimal.TryParse. None of the NumberStyles' composed values is useful for bitcoin style
|
||||
private const NumberStyles BitcoinStyle =
|
||||
NumberStyles.AllowLeadingWhite | NumberStyles.AllowTrailingWhite
|
||||
| NumberStyles.AllowLeadingSign | NumberStyles.AllowDecimalPoint;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Parse a bitcoin amount (Culture Invariant)
|
||||
/// </summary>
|
||||
/// <param name="bitcoin"></param>
|
||||
/// <param name="nRet"></param>
|
||||
/// <returns></returns>
|
||||
public static bool TryParse(string bitcoin, out LightMoney nRet)
|
||||
{
|
||||
nRet = null;
|
||||
|
||||
decimal value;
|
||||
if (!decimal.TryParse(bitcoin, BitcoinStyle, CultureInfo.InvariantCulture, out value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
nRet = new LightMoney(value, LightMoneyUnit.BTC);
|
||||
return true;
|
||||
}
|
||||
catch (OverflowException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a bitcoin amount (Culture Invariant)
|
||||
/// </summary>
|
||||
/// <param name="bitcoin"></param>
|
||||
/// <returns></returns>
|
||||
public static LightMoney Parse(string bitcoin)
|
||||
{
|
||||
LightMoney result;
|
||||
if (TryParse(bitcoin, out result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
throw new FormatException("Impossible to parse the string in a bitcoin amount");
|
||||
}
|
||||
|
||||
long _MilliSatoshis;
|
||||
public long MilliSatoshi
|
||||
{
|
||||
get
|
||||
{
|
||||
return _MilliSatoshis;
|
||||
}
|
||||
// used as a central point where long.MinValue checking can be enforced
|
||||
private set
|
||||
{
|
||||
CheckLongMinValue(value);
|
||||
_MilliSatoshis = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get absolute value of the instance
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public LightMoney Abs()
|
||||
{
|
||||
var a = this;
|
||||
if (a < LightMoney.Zero)
|
||||
a = -a;
|
||||
return a;
|
||||
}
|
||||
|
||||
public LightMoney(int satoshis)
|
||||
{
|
||||
MilliSatoshi = satoshis;
|
||||
}
|
||||
|
||||
public LightMoney(uint satoshis)
|
||||
{
|
||||
MilliSatoshi = satoshis;
|
||||
}
|
||||
|
||||
public LightMoney(long satoshis)
|
||||
{
|
||||
MilliSatoshi = satoshis;
|
||||
}
|
||||
|
||||
public LightMoney(ulong satoshis)
|
||||
{
|
||||
// overflow check.
|
||||
// ulong.MaxValue is greater than long.MaxValue
|
||||
checked
|
||||
{
|
||||
MilliSatoshi = (long)satoshis;
|
||||
}
|
||||
}
|
||||
|
||||
public LightMoney(decimal amount, LightMoneyUnit unit)
|
||||
{
|
||||
// sanity check. Only valid units are allowed
|
||||
CheckMoneyUnit(unit, "unit");
|
||||
checked
|
||||
{
|
||||
var satoshi = amount * (long)unit;
|
||||
MilliSatoshi = (long)satoshi;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Split the Money in parts without loss
|
||||
/// </summary>
|
||||
/// <param name="parts">The number of parts (must be more than 0)</param>
|
||||
/// <returns>The splitted money</returns>
|
||||
public IEnumerable<LightMoney> Split(int parts)
|
||||
{
|
||||
if (parts <= 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(parts), "Parts should be more than 0");
|
||||
long remain;
|
||||
long result = DivRem(_MilliSatoshis, parts, out remain);
|
||||
|
||||
for (int i = 0; i < parts; i++)
|
||||
{
|
||||
yield return LightMoney.Satoshis(result + (remain > 0 ? 1 : 0));
|
||||
remain--;
|
||||
}
|
||||
}
|
||||
|
||||
private static long DivRem(long a, long b, out long result)
|
||||
{
|
||||
result = a % b;
|
||||
return a / b;
|
||||
}
|
||||
|
||||
public static LightMoney FromUnit(decimal amount, LightMoneyUnit unit)
|
||||
{
|
||||
return new LightMoney(amount, unit);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert Money to decimal (same as ToDecimal)
|
||||
/// </summary>
|
||||
/// <param name="unit"></param>
|
||||
/// <returns></returns>
|
||||
public decimal ToUnit(LightMoneyUnit unit)
|
||||
{
|
||||
CheckMoneyUnit(unit, "unit");
|
||||
// overflow safe because (long / int) always fit in decimal
|
||||
// decimal operations are checked by default
|
||||
return (decimal)MilliSatoshi / (int)unit;
|
||||
}
|
||||
/// <summary>
|
||||
/// Convert Money to decimal (same as ToUnit)
|
||||
/// </summary>
|
||||
/// <param name="unit"></param>
|
||||
/// <returns></returns>
|
||||
public decimal ToDecimal(LightMoneyUnit unit)
|
||||
{
|
||||
return ToUnit(unit);
|
||||
}
|
||||
|
||||
public static LightMoney Coins(decimal coins)
|
||||
{
|
||||
// overflow safe.
|
||||
// decimal operations are checked by default
|
||||
return new LightMoney(coins * (ulong)LightMoneyUnit.BTC, LightMoneyUnit.MilliBTC);
|
||||
}
|
||||
|
||||
public static LightMoney Bits(decimal bits)
|
||||
{
|
||||
// overflow safe.
|
||||
// decimal operations are checked by default
|
||||
return new LightMoney(bits * (ulong)LightMoneyUnit.Bit, LightMoneyUnit.MilliBTC);
|
||||
}
|
||||
|
||||
public static LightMoney Cents(decimal cents)
|
||||
{
|
||||
// overflow safe.
|
||||
// decimal operations are checked by default
|
||||
return new LightMoney(cents * (ulong)LightMoneyUnit.Bit, LightMoneyUnit.MilliBTC);
|
||||
}
|
||||
|
||||
public static LightMoney Satoshis(decimal sats)
|
||||
{
|
||||
return new LightMoney(sats * (ulong)LightMoneyUnit.Satoshi, LightMoneyUnit.MilliBTC);
|
||||
}
|
||||
|
||||
public static LightMoney Satoshis(ulong sats)
|
||||
{
|
||||
return new LightMoney(sats);
|
||||
}
|
||||
|
||||
public static LightMoney Satoshis(long sats)
|
||||
{
|
||||
return new LightMoney(sats);
|
||||
}
|
||||
|
||||
public static LightMoney MilliSatoshis(long msats)
|
||||
{
|
||||
return new LightMoney(msats);
|
||||
}
|
||||
|
||||
public static LightMoney MilliSatoshis(ulong msats)
|
||||
{
|
||||
return new LightMoney(msats);
|
||||
}
|
||||
|
||||
#region IEquatable<Money> Members
|
||||
|
||||
public bool Equals(LightMoney other)
|
||||
{
|
||||
if (other == null)
|
||||
return false;
|
||||
return _MilliSatoshis.Equals(other._MilliSatoshis);
|
||||
}
|
||||
|
||||
public int CompareTo(LightMoney other)
|
||||
{
|
||||
if (other == null)
|
||||
return 1;
|
||||
return _MilliSatoshis.CompareTo(other._MilliSatoshis);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IComparable Members
|
||||
|
||||
public int CompareTo(object obj)
|
||||
{
|
||||
if (obj == null)
|
||||
return 1;
|
||||
LightMoney m = obj as LightMoney;
|
||||
if (m != null)
|
||||
return _MilliSatoshis.CompareTo(m._MilliSatoshis);
|
||||
#if !(PORTABLE || NETCORE)
|
||||
return _MilliSatoshis.CompareTo(obj);
|
||||
#else
|
||||
return _Satoshis.CompareTo((long)obj);
|
||||
#endif
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public static LightMoney operator -(LightMoney left, LightMoney right)
|
||||
{
|
||||
if (left == null)
|
||||
throw new ArgumentNullException("left");
|
||||
if (right == null)
|
||||
throw new ArgumentNullException("right");
|
||||
return new LightMoney(checked(left._MilliSatoshis - right._MilliSatoshis));
|
||||
}
|
||||
public static LightMoney operator -(LightMoney left)
|
||||
{
|
||||
if (left == null)
|
||||
throw new ArgumentNullException("left");
|
||||
return new LightMoney(checked(-left._MilliSatoshis));
|
||||
}
|
||||
public static LightMoney operator +(LightMoney left, LightMoney right)
|
||||
{
|
||||
if (left == null)
|
||||
throw new ArgumentNullException("left");
|
||||
if (right == null)
|
||||
throw new ArgumentNullException("right");
|
||||
return new LightMoney(checked(left._MilliSatoshis + right._MilliSatoshis));
|
||||
}
|
||||
public static LightMoney operator *(int left, LightMoney right)
|
||||
{
|
||||
if (right == null)
|
||||
throw new ArgumentNullException("right");
|
||||
return LightMoney.Satoshis(checked(left * right._MilliSatoshis));
|
||||
}
|
||||
|
||||
public static LightMoney operator *(LightMoney right, int left)
|
||||
{
|
||||
if (right == null)
|
||||
throw new ArgumentNullException("right");
|
||||
return LightMoney.Satoshis(checked(right._MilliSatoshis * left));
|
||||
}
|
||||
public static LightMoney operator *(long left, LightMoney right)
|
||||
{
|
||||
if (right == null)
|
||||
throw new ArgumentNullException("right");
|
||||
return LightMoney.Satoshis(checked(left * right._MilliSatoshis));
|
||||
}
|
||||
public static LightMoney operator *(LightMoney right, long left)
|
||||
{
|
||||
if (right == null)
|
||||
throw new ArgumentNullException("right");
|
||||
return LightMoney.Satoshis(checked(left * right._MilliSatoshis));
|
||||
}
|
||||
|
||||
public static LightMoney operator /(LightMoney left, long right)
|
||||
{
|
||||
if (left == null)
|
||||
throw new ArgumentNullException("left");
|
||||
return new LightMoney(checked(left._MilliSatoshis / right));
|
||||
}
|
||||
|
||||
public static bool operator <(LightMoney left, LightMoney right)
|
||||
{
|
||||
if (left == null)
|
||||
throw new ArgumentNullException("left");
|
||||
if (right == null)
|
||||
throw new ArgumentNullException("right");
|
||||
return left._MilliSatoshis < right._MilliSatoshis;
|
||||
}
|
||||
public static bool operator >(LightMoney left, LightMoney right)
|
||||
{
|
||||
if (left == null)
|
||||
throw new ArgumentNullException("left");
|
||||
if (right == null)
|
||||
throw new ArgumentNullException("right");
|
||||
return left._MilliSatoshis > right._MilliSatoshis;
|
||||
}
|
||||
public static bool operator <=(LightMoney left, LightMoney right)
|
||||
{
|
||||
if (left == null)
|
||||
throw new ArgumentNullException("left");
|
||||
if (right == null)
|
||||
throw new ArgumentNullException("right");
|
||||
return left._MilliSatoshis <= right._MilliSatoshis;
|
||||
}
|
||||
public static bool operator >=(LightMoney left, LightMoney right)
|
||||
{
|
||||
if (left == null)
|
||||
throw new ArgumentNullException("left");
|
||||
if (right == null)
|
||||
throw new ArgumentNullException("right");
|
||||
return left._MilliSatoshis >= right._MilliSatoshis;
|
||||
}
|
||||
|
||||
public static implicit operator LightMoney(long value)
|
||||
{
|
||||
return new LightMoney(value);
|
||||
}
|
||||
public static implicit operator LightMoney(int value)
|
||||
{
|
||||
return new LightMoney(value);
|
||||
}
|
||||
|
||||
public static implicit operator LightMoney(uint value)
|
||||
{
|
||||
return new LightMoney(value);
|
||||
}
|
||||
|
||||
public static implicit operator LightMoney(ulong value)
|
||||
{
|
||||
return new LightMoney(checked((long)value));
|
||||
}
|
||||
|
||||
public static implicit operator long(LightMoney value)
|
||||
{
|
||||
return value.MilliSatoshi;
|
||||
}
|
||||
|
||||
public static implicit operator ulong(LightMoney value)
|
||||
{
|
||||
return checked((ulong)value.MilliSatoshi);
|
||||
}
|
||||
|
||||
public static implicit operator LightMoney(string value)
|
||||
{
|
||||
return LightMoney.Parse(value);
|
||||
}
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
LightMoney item = obj as LightMoney;
|
||||
if (item == null)
|
||||
return false;
|
||||
return _MilliSatoshis.Equals(item._MilliSatoshis);
|
||||
}
|
||||
public static bool operator ==(LightMoney a, LightMoney b)
|
||||
{
|
||||
if (Object.ReferenceEquals(a, b))
|
||||
return true;
|
||||
if (((object)a == null) || ((object)b == null))
|
||||
return false;
|
||||
return a._MilliSatoshis == b._MilliSatoshis;
|
||||
}
|
||||
|
||||
public static bool operator !=(LightMoney a, LightMoney b)
|
||||
{
|
||||
return !(a == b);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return _MilliSatoshis.GetHashCode();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Returns a culture invariant string representation of Bitcoin amount
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public override string ToString()
|
||||
{
|
||||
return ToString(false, false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a culture invariant string representation of Bitcoin amount
|
||||
/// </summary>
|
||||
/// <param name="fplus">True if show + for a positive amount</param>
|
||||
/// <param name="trimExcessZero">True if trim excess zeroes</param>
|
||||
/// <returns></returns>
|
||||
public string ToString(bool fplus, bool trimExcessZero = true)
|
||||
{
|
||||
var fmt = string.Format(CultureInfo.InvariantCulture, "{{0:{0}{1}B}}",
|
||||
(fplus ? "+" : null),
|
||||
(trimExcessZero ? "2" : "11"));
|
||||
return string.Format(BitcoinFormatter.Formatter, fmt, _MilliSatoshis);
|
||||
}
|
||||
|
||||
|
||||
static LightMoney _Zero = new LightMoney(0);
|
||||
public static LightMoney Zero
|
||||
{
|
||||
get
|
||||
{
|
||||
return _Zero;
|
||||
}
|
||||
}
|
||||
|
||||
internal class BitcoinFormatter : IFormatProvider, ICustomFormatter
|
||||
{
|
||||
public static readonly BitcoinFormatter Formatter = new BitcoinFormatter();
|
||||
|
||||
public object GetFormat(Type formatType)
|
||||
{
|
||||
return formatType == typeof(ICustomFormatter) ? this : null;
|
||||
}
|
||||
|
||||
public string Format(string format, object arg, IFormatProvider formatProvider)
|
||||
{
|
||||
if (!this.Equals(formatProvider))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
var i = 0;
|
||||
var plus = format[i] == '+';
|
||||
if (plus)
|
||||
i++;
|
||||
int decPos = 0;
|
||||
if (int.TryParse(format.Substring(i, 1), out decPos))
|
||||
{
|
||||
i++;
|
||||
}
|
||||
var unit = format[i];
|
||||
var unitToUseInCalc = LightMoneyUnit.BTC;
|
||||
switch (unit)
|
||||
{
|
||||
case 'B':
|
||||
unitToUseInCalc = LightMoneyUnit.BTC;
|
||||
break;
|
||||
}
|
||||
var val = Convert.ToDecimal(arg, CultureInfo.InvariantCulture) / (long)unitToUseInCalc;
|
||||
var zeros = new string('0', decPos);
|
||||
var rest = new string('#', 11 - decPos);
|
||||
var fmt = plus && val > 0 ? "+" : string.Empty;
|
||||
|
||||
fmt += "{0:0" + (decPos > 0 ? "." + zeros + rest : string.Empty) + "}";
|
||||
return string.Format(CultureInfo.InvariantCulture, fmt, val);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tell if amount is almost equal to this instance
|
||||
/// </summary>
|
||||
/// <param name="amount"></param>
|
||||
/// <param name="dust">more or less amount</param>
|
||||
/// <returns>true if equals, else false</returns>
|
||||
public bool Almost(LightMoney amount, LightMoney dust)
|
||||
{
|
||||
if (amount == null)
|
||||
throw new ArgumentNullException("amount");
|
||||
if (dust == null)
|
||||
throw new ArgumentNullException("dust");
|
||||
return (amount - this).Abs() <= dust;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tell if amount is almost equal to this instance
|
||||
/// </summary>
|
||||
/// <param name="amount"></param>
|
||||
/// <param name="margin">error margin (between 0 and 1)</param>
|
||||
/// <returns>true if equals, else false</returns>
|
||||
public bool Almost(LightMoney amount, decimal margin)
|
||||
{
|
||||
if (amount == null)
|
||||
throw new ArgumentNullException("amount");
|
||||
if (margin < 0.0m || margin > 1.0m)
|
||||
throw new ArgumentOutOfRangeException("margin", "margin should be between 0 and 1");
|
||||
var dust = LightMoney.Satoshis((decimal)this.MilliSatoshi * margin);
|
||||
return Almost(amount, dust);
|
||||
}
|
||||
|
||||
public static LightMoney Min(LightMoney a, LightMoney b)
|
||||
{
|
||||
if (a == null)
|
||||
throw new ArgumentNullException("a");
|
||||
if (b == null)
|
||||
throw new ArgumentNullException("b");
|
||||
if (a <= b)
|
||||
return a;
|
||||
return b;
|
||||
}
|
||||
|
||||
public static LightMoney Max(LightMoney a, LightMoney b)
|
||||
{
|
||||
if (a == null)
|
||||
throw new ArgumentNullException("a");
|
||||
if (b == null)
|
||||
throw new ArgumentNullException("b");
|
||||
if (a >= b)
|
||||
return a;
|
||||
return b;
|
||||
}
|
||||
|
||||
private static void CheckLongMinValue(long value)
|
||||
{
|
||||
if (value == long.MinValue)
|
||||
throw new OverflowException("satoshis amount should be greater than long.MinValue");
|
||||
}
|
||||
|
||||
private static void CheckMoneyUnit(LightMoneyUnit value, string paramName)
|
||||
{
|
||||
var typeOfMoneyUnit = typeof(LightMoneyUnit);
|
||||
if (!Enum.IsDefined(typeOfMoneyUnit, value))
|
||||
{
|
||||
throw new ArgumentException("Invalid value for MoneyUnit", paramName);
|
||||
}
|
||||
}
|
||||
|
||||
#region IComparable Members
|
||||
|
||||
int IComparable.CompareTo(object obj)
|
||||
{
|
||||
return this.CompareTo(obj);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
24
BTCPayServer/Eclair/NodeInfo.cs
Normal file
24
BTCPayServer/Eclair/NodeInfo.cs
Normal file
@ -0,0 +1,24 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Eclair
|
||||
{
|
||||
public class NodeInfo
|
||||
{
|
||||
public NodeInfo(string nodeId, string host, int port)
|
||||
{
|
||||
if (host == null)
|
||||
throw new ArgumentNullException(nameof(host));
|
||||
if (nodeId == null)
|
||||
throw new ArgumentNullException(nameof(nodeId));
|
||||
Port = port;
|
||||
Host = host;
|
||||
NodeId = nodeId;
|
||||
}
|
||||
public string NodeId { get; private set; }
|
||||
public string Host { get; private set; }
|
||||
public int Port { get; private set; }
|
||||
}
|
||||
}
|
154
BTCPayServer/EventAggregator.cs
Normal file
154
BTCPayServer/EventAggregator.cs
Normal file
@ -0,0 +1,154 @@
|
||||
using System;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Logging;
|
||||
using System.Threading;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public interface IEventAggregatorSubscription : IDisposable
|
||||
{
|
||||
void Unsubscribe();
|
||||
void Resubscribe();
|
||||
}
|
||||
public class EventAggregator : IDisposable
|
||||
{
|
||||
class Subscription : IEventAggregatorSubscription
|
||||
{
|
||||
private EventAggregator aggregator;
|
||||
Type t;
|
||||
public Subscription(EventAggregator aggregator, Type t)
|
||||
{
|
||||
this.aggregator = aggregator;
|
||||
this.t = t;
|
||||
}
|
||||
|
||||
public Action<Object> Act { get; set; }
|
||||
|
||||
bool _Disposed;
|
||||
public void Dispose()
|
||||
{
|
||||
if (_Disposed)
|
||||
return;
|
||||
_Disposed = true;
|
||||
lock (this.aggregator._Subscriptions)
|
||||
{
|
||||
if (this.aggregator._Subscriptions.TryGetValue(t, out Dictionary<Subscription, Action<object>> actions))
|
||||
{
|
||||
if (actions.Remove(this))
|
||||
{
|
||||
if (actions.Count == 0)
|
||||
this.aggregator._Subscriptions.Remove(t);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Resubscribe()
|
||||
{
|
||||
aggregator.Subscribe(t, this);
|
||||
}
|
||||
|
||||
public void Unsubscribe()
|
||||
{
|
||||
Dispose();
|
||||
}
|
||||
}
|
||||
public Task<T> WaitNext<T>(CancellationToken cancellation = default(CancellationToken))
|
||||
{
|
||||
return WaitNext<T>(o => true, cancellation);
|
||||
}
|
||||
public async Task<T> WaitNext<T>(Func<T, bool> predicate, CancellationToken cancellation = default(CancellationToken))
|
||||
{
|
||||
TaskCompletionSource<T> tcs = new TaskCompletionSource<T>();
|
||||
var subscription = Subscribe<T>((a, b) => { if (predicate(b)) { tcs.TrySetResult(b); a.Unsubscribe(); } });
|
||||
using (cancellation.Register(() => { tcs.TrySetCanceled(); subscription.Unsubscribe(); }))
|
||||
{
|
||||
return await tcs.Task.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
public void Publish<T>(T evt) where T : class
|
||||
{
|
||||
Publish(evt, typeof(T));
|
||||
}
|
||||
|
||||
public void Publish(object evt, Type evtType)
|
||||
{
|
||||
if (evt == null)
|
||||
throw new ArgumentNullException(nameof(evt));
|
||||
List<Action<object>> actionList = new List<Action<object>>();
|
||||
lock (_Subscriptions)
|
||||
{
|
||||
if (_Subscriptions.TryGetValue(evtType, out Dictionary<Subscription, Action<object>> actions))
|
||||
{
|
||||
actionList = actions.Values.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
var log = evt.ToString();
|
||||
if(!String.IsNullOrEmpty(log))
|
||||
Logs.Events.LogInformation(log);
|
||||
foreach (var sub in actionList)
|
||||
{
|
||||
try
|
||||
{
|
||||
sub(evt);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logs.Events.LogError(ex, $"Error while calling event handler");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public IEventAggregatorSubscription Subscribe<T>(Action<IEventAggregatorSubscription, T> subscription)
|
||||
{
|
||||
var eventType = typeof(T);
|
||||
var s = new Subscription(this, eventType);
|
||||
s.Act = (o) => subscription(s, (T)o);
|
||||
return Subscribe(eventType, s);
|
||||
}
|
||||
|
||||
private IEventAggregatorSubscription Subscribe(Type eventType, Subscription subscription)
|
||||
{
|
||||
lock (_Subscriptions)
|
||||
{
|
||||
if (!_Subscriptions.TryGetValue(eventType, out Dictionary<Subscription, Action<object>> actions))
|
||||
{
|
||||
actions = new Dictionary<Subscription, Action<object>>();
|
||||
_Subscriptions.Add(eventType, actions);
|
||||
}
|
||||
actions.Add(subscription, subscription.Act);
|
||||
}
|
||||
return subscription;
|
||||
}
|
||||
|
||||
Dictionary<Type, Dictionary<Subscription, Action<object>>> _Subscriptions = new Dictionary<Type, Dictionary<Subscription, Action<object>>>();
|
||||
|
||||
public IEventAggregatorSubscription Subscribe<T, TReturn>(Func<T, TReturn> subscription)
|
||||
{
|
||||
return Subscribe(new Action<T>((t) => subscription(t)));
|
||||
}
|
||||
|
||||
public IEventAggregatorSubscription Subscribe<T, TReturn>(Func<IEventAggregatorSubscription, T, TReturn> subscription)
|
||||
{
|
||||
return Subscribe(new Action<IEventAggregatorSubscription, T>((sub, t) => subscription(sub, t)));
|
||||
}
|
||||
|
||||
public IEventAggregatorSubscription Subscribe<T>(Action<T> subscription)
|
||||
{
|
||||
return Subscribe(new Action<IEventAggregatorSubscription, T>((sub, t) => subscription(t)));
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
lock (_Subscriptions)
|
||||
{
|
||||
_Subscriptions.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
33
BTCPayServer/Events/InvoiceDataChangedEvent.cs
Normal file
33
BTCPayServer/Events/InvoiceDataChangedEvent.cs
Normal file
@ -0,0 +1,33 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
|
||||
namespace BTCPayServer.Events
|
||||
{
|
||||
public class InvoiceDataChangedEvent
|
||||
{
|
||||
public InvoiceDataChangedEvent(InvoiceEntity invoice)
|
||||
{
|
||||
InvoiceId = invoice.Id;
|
||||
Status = invoice.Status;
|
||||
ExceptionStatus = invoice.ExceptionStatus;
|
||||
}
|
||||
public string InvoiceId { get; set; }
|
||||
public string Status { get; internal set; }
|
||||
public string ExceptionStatus { get; internal set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
if (string.IsNullOrEmpty(ExceptionStatus) || ExceptionStatus == "false")
|
||||
{
|
||||
return $"Invoice status is {Status}";
|
||||
}
|
||||
else
|
||||
{
|
||||
return $"Invoice status is {Status} (Exception status: {ExceptionStatus})";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
31
BTCPayServer/Events/InvoiceEvent.cs
Normal file
31
BTCPayServer/Events/InvoiceEvent.cs
Normal file
@ -0,0 +1,31 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
|
||||
namespace BTCPayServer.Events
|
||||
{
|
||||
public class InvoiceEvent
|
||||
{
|
||||
public InvoiceEvent(InvoiceEntity invoice, int code, string name) : this(invoice.Id, code, name)
|
||||
{
|
||||
|
||||
}
|
||||
public InvoiceEvent(string invoiceId, int code, string name)
|
||||
{
|
||||
InvoiceId = invoiceId;
|
||||
EventCode = code;
|
||||
Name = name;
|
||||
}
|
||||
|
||||
public string InvoiceId { get; set; }
|
||||
public int EventCode { get; set; }
|
||||
public string Name { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"Invoice {InvoiceId} new event: {Name} ({EventCode})";
|
||||
}
|
||||
}
|
||||
}
|
35
BTCPayServer/Events/InvoiceIPNEvent.cs
Normal file
35
BTCPayServer/Events/InvoiceIPNEvent.cs
Normal file
@ -0,0 +1,35 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Events
|
||||
{
|
||||
public class InvoiceIPNEvent
|
||||
{
|
||||
public InvoiceIPNEvent(string invoiceId, int? eventCode, string name)
|
||||
{
|
||||
InvoiceId = invoiceId;
|
||||
EventCode = eventCode;
|
||||
Name = name;
|
||||
}
|
||||
|
||||
public int? EventCode { get; set; }
|
||||
public string Name { get; set; }
|
||||
|
||||
public string InvoiceId { get; set; }
|
||||
public string Error { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
string ipnType = "IPN";
|
||||
if(EventCode.HasValue)
|
||||
{
|
||||
ipnType = $"IPN ({EventCode.Value} {Name})";
|
||||
}
|
||||
if (Error == null)
|
||||
return $"{ipnType} sent for invoice {InvoiceId}";
|
||||
return $"Error while sending {ipnType}: {Error}";
|
||||
}
|
||||
}
|
||||
}
|
24
BTCPayServer/Events/InvoiceNeedUpdateEvent.cs
Normal file
24
BTCPayServer/Events/InvoiceNeedUpdateEvent.cs
Normal file
@ -0,0 +1,24 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Events
|
||||
{
|
||||
public class InvoiceNeedUpdateEvent
|
||||
{
|
||||
public InvoiceNeedUpdateEvent(string invoiceId)
|
||||
{
|
||||
if (invoiceId == null)
|
||||
throw new ArgumentNullException(nameof(invoiceId));
|
||||
InvoiceId = invoiceId;
|
||||
}
|
||||
|
||||
public string InvoiceId { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
25
BTCPayServer/Events/InvoiceNewAddressEvent.cs
Normal file
25
BTCPayServer/Events/InvoiceNewAddressEvent.cs
Normal file
@ -0,0 +1,25 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Events
|
||||
{
|
||||
public class InvoiceNewAddressEvent
|
||||
{
|
||||
public InvoiceNewAddressEvent(string invoiceId, string address, BTCPayNetwork network)
|
||||
{
|
||||
Address = address;
|
||||
InvoiceId = invoiceId;
|
||||
Network = network;
|
||||
}
|
||||
|
||||
public string Address { get; set; }
|
||||
public string InvoiceId { get; set; }
|
||||
public BTCPayNetwork Network { get; set; }
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{Network.CryptoCode}: New address {Address} for invoice {InvoiceId}";
|
||||
}
|
||||
}
|
||||
}
|
20
BTCPayServer/Events/InvoiceStopWatchedEvent.cs
Normal file
20
BTCPayServer/Events/InvoiceStopWatchedEvent.cs
Normal file
@ -0,0 +1,20 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Events
|
||||
{
|
||||
public class InvoiceStopWatchedEvent
|
||||
{
|
||||
public InvoiceStopWatchedEvent(string invoiceId)
|
||||
{
|
||||
this.InvoiceId = invoiceId;
|
||||
}
|
||||
public string InvoiceId { get; set; }
|
||||
public override string ToString()
|
||||
{
|
||||
return $"Invoice {InvoiceId} is not monitored anymore.";
|
||||
}
|
||||
}
|
||||
}
|
42
BTCPayServer/Events/NBXplorerStateChangedEvent.cs
Normal file
42
BTCPayServer/Events/NBXplorerStateChangedEvent.cs
Normal file
@ -0,0 +1,42 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.HostedServices;
|
||||
|
||||
namespace BTCPayServer.Events
|
||||
{
|
||||
public class NBXplorerErrorEvent
|
||||
{
|
||||
public NBXplorerErrorEvent(BTCPayNetwork network, string errorMessage)
|
||||
{
|
||||
Message = errorMessage;
|
||||
Network = network;
|
||||
}
|
||||
public string Message { get; set; }
|
||||
public BTCPayNetwork Network { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{Network.CryptoCode}: NBXplorer error `{Message}`";
|
||||
}
|
||||
}
|
||||
public class NBXplorerStateChangedEvent
|
||||
{
|
||||
public NBXplorerStateChangedEvent(BTCPayNetwork network, NBXplorerState old, NBXplorerState newState)
|
||||
{
|
||||
Network = network;
|
||||
NewState = newState;
|
||||
OldState = old;
|
||||
}
|
||||
|
||||
public BTCPayNetwork Network { get; set; }
|
||||
public NBXplorerState NewState { get; set; }
|
||||
public NBXplorerState OldState { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"NBXplorer {Network.CryptoCode}: {OldState} => {NewState}";
|
||||
}
|
||||
}
|
||||
}
|
16
BTCPayServer/Events/NewBlockEvent.cs
Normal file
16
BTCPayServer/Events/NewBlockEvent.cs
Normal file
@ -0,0 +1,16 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Events
|
||||
{
|
||||
public class NewBlockEvent
|
||||
{
|
||||
public string CryptoCode { get; set; }
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{CryptoCode}: New block";
|
||||
}
|
||||
}
|
||||
}
|
104
BTCPayServer/ExplorerClientProvider.cs
Normal file
104
BTCPayServer/ExplorerClientProvider.cs
Normal file
@ -0,0 +1,104 @@
|
||||
using System;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Configuration;
|
||||
using BTCPayServer.Logging;
|
||||
using NBXplorer;
|
||||
using BTCPayServer.HostedServices;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public class ExplorerClientProvider
|
||||
{
|
||||
BTCPayNetworkProvider _NetworkProviders;
|
||||
BTCPayServerOptions _Options;
|
||||
|
||||
public BTCPayNetworkProvider NetworkProviders => _NetworkProviders;
|
||||
NBXplorerDashboard _Dashboard;
|
||||
public ExplorerClientProvider(BTCPayNetworkProvider networkProviders, BTCPayServerOptions options, NBXplorerDashboard dashboard)
|
||||
{
|
||||
_Dashboard = dashboard;
|
||||
_NetworkProviders = networkProviders;
|
||||
_Options = options;
|
||||
|
||||
foreach (var setting in options.NBXplorerConnectionSettings)
|
||||
{
|
||||
var cookieFile = setting.CookieFile;
|
||||
if (cookieFile.Trim() == "0" || string.IsNullOrEmpty(cookieFile.Trim()))
|
||||
cookieFile = null;
|
||||
Logs.Configuration.LogInformation($"{setting.CryptoCode}: Explorer url is {(setting.ExplorerUri.AbsoluteUri ?? "not set")}");
|
||||
Logs.Configuration.LogInformation($"{setting.CryptoCode}: Cookie file is {(setting.CookieFile ?? "not set")}");
|
||||
if (setting.ExplorerUri != null)
|
||||
{
|
||||
_Clients.TryAdd(setting.CryptoCode, CreateExplorerClient(_NetworkProviders.GetNetwork(setting.CryptoCode), setting.ExplorerUri, setting.CookieFile));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static ExplorerClient CreateExplorerClient(BTCPayNetwork n, Uri uri, string cookieFile)
|
||||
{
|
||||
var explorer = new ExplorerClient(n.NBXplorerNetwork, uri);
|
||||
if (cookieFile == null)
|
||||
{
|
||||
Logs.Configuration.LogWarning($"{n.CryptoCode}: Not using cookie authentication");
|
||||
explorer.SetNoAuth();
|
||||
}
|
||||
if(!explorer.SetCookieAuth(cookieFile))
|
||||
{
|
||||
Logs.Configuration.LogWarning($"{n.CryptoCode}: Using cookie auth against NBXplorer, but {cookieFile} is not found");
|
||||
}
|
||||
return explorer;
|
||||
}
|
||||
|
||||
Dictionary<string, ExplorerClient> _Clients = new Dictionary<string, ExplorerClient>();
|
||||
|
||||
public ExplorerClient GetExplorerClient(string cryptoCode)
|
||||
{
|
||||
var network = _NetworkProviders.GetNetwork(cryptoCode);
|
||||
if (network == null)
|
||||
return null;
|
||||
_Clients.TryGetValue(network.CryptoCode, out ExplorerClient client);
|
||||
return client;
|
||||
}
|
||||
|
||||
public ExplorerClient GetExplorerClient(BTCPayNetwork network)
|
||||
{
|
||||
if (network == null)
|
||||
throw new ArgumentNullException(nameof(network));
|
||||
return GetExplorerClient(network.CryptoCode);
|
||||
}
|
||||
|
||||
public bool IsAvailable(BTCPayNetwork network)
|
||||
{
|
||||
return IsAvailable(network.CryptoCode);
|
||||
}
|
||||
|
||||
public bool IsAvailable(string cryptoCode)
|
||||
{
|
||||
return _Clients.ContainsKey(cryptoCode) && _Dashboard.IsFullySynched(cryptoCode);
|
||||
}
|
||||
|
||||
public BTCPayNetwork GetNetwork(string cryptoCode)
|
||||
{
|
||||
var network = _NetworkProviders.GetNetwork(cryptoCode);
|
||||
if (network == null)
|
||||
return null;
|
||||
if (_Clients.ContainsKey(network.CryptoCode))
|
||||
return network;
|
||||
return null;
|
||||
}
|
||||
|
||||
public IEnumerable<(BTCPayNetwork, ExplorerClient)> GetAll()
|
||||
{
|
||||
foreach (var net in _NetworkProviders.GetAll())
|
||||
{
|
||||
if (_Clients.TryGetValue(net.CryptoCode, out ExplorerClient explorer))
|
||||
{
|
||||
yield return (net, explorer);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,48 +1,119 @@
|
||||
using BTCPayServer.Authentication;
|
||||
using BTCPayServer.Configuration;
|
||||
using Microsoft.AspNetCore.Html;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Text.Encodings.Web;
|
||||
using NBitcoin;
|
||||
using System.Threading.Tasks;
|
||||
using NBXplorer;
|
||||
using NBXplorer.Models;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using System.IO;
|
||||
using BTCPayServer.Logging;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Net.WebSockets;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using NBitpayClient;
|
||||
using BTCPayServer.Payments;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public static class Extensions
|
||||
{
|
||||
public static string WithTrailingSlash(this string str)
|
||||
{
|
||||
if(str.EndsWith("/"))
|
||||
return str;
|
||||
return str + "/";
|
||||
}
|
||||
public static PaymentMethodId GetpaymentMethodId(this InvoiceCryptoInfo info)
|
||||
{
|
||||
return new PaymentMethodId(info.CryptoCode, Enum.Parse<PaymentTypes>(info.PaymentType));
|
||||
}
|
||||
public static async Task CloseSocket(this WebSocket webSocket)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (webSocket.State == WebSocketState.Open)
|
||||
{
|
||||
CancellationTokenSource cts = new CancellationTokenSource();
|
||||
cts.CancelAfter(5000);
|
||||
await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", cts.Token);
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
finally { try { webSocket.Dispose(); } catch { } }
|
||||
}
|
||||
public static bool SupportDropColumn(this Microsoft.EntityFrameworkCore.Migrations.Migration migration, string activeProvider)
|
||||
{
|
||||
return activeProvider != "Microsoft.EntityFrameworkCore.Sqlite";
|
||||
}
|
||||
|
||||
public static string GetAbsoluteRoot(this HttpRequest request)
|
||||
{
|
||||
return string.Concat(
|
||||
request.Scheme,
|
||||
"://",
|
||||
request.Host.ToUriComponent(),
|
||||
request.PathBase.ToUriComponent());
|
||||
}
|
||||
public static bool IsCoinAverage(this string exchangeName)
|
||||
{
|
||||
string[] coinAverages = new[] { "coinaverage", "bitcoinaverage" };
|
||||
return String.IsNullOrWhiteSpace(exchangeName) ? true : coinAverages.Contains(exchangeName, StringComparer.OrdinalIgnoreCase) ? true : false;
|
||||
}
|
||||
|
||||
public static IServiceCollection ConfigureBTCPayServer(this IServiceCollection services, IConfiguration conf)
|
||||
{
|
||||
services.Configure<BTCPayServerOptions>(o =>
|
||||
{
|
||||
o.LoadArgs(conf);
|
||||
});
|
||||
return services;
|
||||
}
|
||||
public static async Task<Dictionary<uint256, TransactionResult>> GetTransactions(this BTCPayWallet client, uint256[] hashes, CancellationToken cts = default(CancellationToken))
|
||||
{
|
||||
hashes = hashes.Distinct().ToArray();
|
||||
var transactions = hashes
|
||||
.Select(async o => await client.GetTransactionAsync(o, cts))
|
||||
.ToArray();
|
||||
await Task.WhenAll(transactions).ConfigureAwait(false);
|
||||
return transactions.Select(t => t.Result).Where(t => t != null).ToDictionary(o => o.Transaction.GetHash());
|
||||
}
|
||||
public static string WithTrailingSlash(this string str)
|
||||
{
|
||||
if (str.EndsWith("/", StringComparison.InvariantCulture))
|
||||
return str;
|
||||
return str + "/";
|
||||
}
|
||||
|
||||
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 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;
|
||||
}
|
||||
|
||||
public static HtmlString ToJSVariableModel(this object o, string variableName)
|
||||
{
|
||||
var encodedJson = JavaScriptEncoder.Default.Encode(o.ToJson());
|
||||
return new HtmlString($"var {variableName} = JSON.parse('" + encodedJson + "');");
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -7,70 +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)
|
||||
{
|
||||
return context.RouteContext.HttpContext.Request.Headers["x-accept-version"].Where(h => h == "2.0.0").Any() == 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 match = context.RouteContext.HttpContext.Request.Headers["Accept"].FirstOrDefault()?.StartsWith(MediaType, StringComparison.Ordinal);
|
||||
return (match.HasValue && match.Value) == ExpectedValue;
|
||||
}
|
||||
}
|
||||
public bool Accept(ActionConstraintContext context)
|
||||
{
|
||||
var hasHeader = context.RouteContext.HttpContext.Request.Headers["Accept"].Any(m => m.StartsWith(MediaType, StringComparison.Ordinal));
|
||||
return hasHeader == ExpectedValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
33
BTCPayServer/Filters/XFrameOptionsAttribute.cs
Normal file
33
BTCPayServer/Filters/XFrameOptionsAttribute.cs
Normal file
@ -0,0 +1,33 @@
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
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 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;
|
||||
}
|
||||
}
|
||||
}
|
363
BTCPayServer/HostedServices/InvoiceNotificationManager.cs
Normal file
363
BTCPayServer/HostedServices/InvoiceNotificationManager.cs
Normal file
@ -0,0 +1,363 @@
|
||||
using Hangfire;
|
||||
using Hangfire.Common;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Hangfire.Annotations;
|
||||
using System.Reflection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using NBitpayClient;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Newtonsoft.Json;
|
||||
using System.Collections.Concurrent;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using BTCPayServer.Events;
|
||||
using NBXplorer;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
|
||||
namespace BTCPayServer.HostedServices
|
||||
{
|
||||
public class InvoiceNotificationManager : IHostedService
|
||||
{
|
||||
public static HttpClient _Client = new HttpClient();
|
||||
|
||||
public class ScheduledJob
|
||||
{
|
||||
public int TryCount
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public InvoiceEntity Invoice
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public int? EventCode { get; set; }
|
||||
public string Message { get; set; }
|
||||
}
|
||||
|
||||
public ILogger Logger
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
IBackgroundJobClient _JobClient;
|
||||
EventAggregator _EventAggregator;
|
||||
InvoiceRepository _InvoiceRepository;
|
||||
BTCPayNetworkProvider _NetworkProvider;
|
||||
|
||||
public InvoiceNotificationManager(
|
||||
IBackgroundJobClient jobClient,
|
||||
EventAggregator eventAggregator,
|
||||
InvoiceRepository invoiceRepository,
|
||||
BTCPayNetworkProvider networkProvider,
|
||||
ILogger<InvoiceNotificationManager> logger)
|
||||
{
|
||||
Logger = logger as ILogger ?? NullLogger.Instance;
|
||||
_JobClient = jobClient;
|
||||
_EventAggregator = eventAggregator;
|
||||
_InvoiceRepository = invoiceRepository;
|
||||
_NetworkProvider = networkProvider;
|
||||
}
|
||||
|
||||
async Task Notify(InvoiceEntity invoice, int? eventCode = null, string name = null)
|
||||
{
|
||||
CancellationTokenSource cts = new CancellationTokenSource(10000);
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(invoice.NotificationURL))
|
||||
return;
|
||||
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(invoice.Id, eventCode, name));
|
||||
await SendNotification(invoice, eventCode, name, cts.Token);
|
||||
return;
|
||||
}
|
||||
catch (OperationCanceledException) when (cts.IsCancellationRequested)
|
||||
{
|
||||
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(invoice.Id, eventCode, name)
|
||||
{
|
||||
Error = "Timeout"
|
||||
});
|
||||
}
|
||||
catch (Exception ex) // It fails, it is OK because we try with hangfire after
|
||||
{
|
||||
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(invoice.Id, eventCode, name)
|
||||
{
|
||||
Error = ex.Message
|
||||
});
|
||||
}
|
||||
var invoiceStr = NBitcoin.JsonConverters.Serializer.ToString(new ScheduledJob() { TryCount = 0, Invoice = invoice, EventCode = eventCode, Message = name });
|
||||
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);
|
||||
|
||||
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
|
||||
{
|
||||
HttpResponseMessage response = await SendNotification(job.Invoice, job.EventCode, job.Message, cts.Token);
|
||||
reschedule = response.StatusCode != System.Net.HttpStatusCode.OK;
|
||||
Logger.LogInformation("Job " + jobId + " returned " + response.StatusCode);
|
||||
|
||||
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(job.Invoice.Id, job.EventCode, job.Message)
|
||||
{
|
||||
Error = reschedule ? $"Unexpected return code: {(int)response.StatusCode}" : null
|
||||
});
|
||||
}
|
||||
catch (OperationCanceledException) when (cts.IsCancellationRequested)
|
||||
{
|
||||
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(job.Invoice.Id, job.EventCode, job.Message)
|
||||
{
|
||||
Error = "Timeout"
|
||||
});
|
||||
reschedule = true;
|
||||
Logger.LogInformation("Job " + jobId + " timed out");
|
||||
}
|
||||
catch (Exception ex) // It fails, it is OK because we try with hangfire after
|
||||
{
|
||||
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(job.Invoice.Id, job.EventCode, job.Message)
|
||||
{
|
||||
Error = ex.Message
|
||||
});
|
||||
reschedule = true;
|
||||
|
||||
List<string> messages = new List<string>();
|
||||
while (ex != null)
|
||||
{
|
||||
messages.Add(ex.Message);
|
||||
ex = ex.InnerException;
|
||||
}
|
||||
string message = String.Join(',', messages.ToArray());
|
||||
Logger.LogInformation("Job " + jobId + " threw exception " + message);
|
||||
|
||||
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(job.Invoice.Id, job.EventCode, job.Message)
|
||||
{
|
||||
Error = $"Unexpected error: {message}"
|
||||
});
|
||||
}
|
||||
finally { cts.Dispose(); _Executing.TryRemove(jobId, out jobId); }
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
public class InvoicePaymentNotificationEvent
|
||||
{
|
||||
[JsonProperty("code")]
|
||||
public int Code { get; set; }
|
||||
[JsonProperty("name")]
|
||||
public string Name { get; set; }
|
||||
}
|
||||
public class InvoicePaymentNotificationEventWrapper
|
||||
{
|
||||
[JsonProperty("event")]
|
||||
public InvoicePaymentNotificationEvent Event { get; set; }
|
||||
[JsonProperty("data")]
|
||||
public InvoicePaymentNotification Data { get; set; }
|
||||
}
|
||||
|
||||
Encoding UTF8 = new UTF8Encoding(false);
|
||||
private async Task<HttpResponseMessage> SendNotification(InvoiceEntity invoice, int? eventCode, string name, CancellationToken cancellation)
|
||||
{
|
||||
var request = new HttpRequestMessage();
|
||||
request.Method = HttpMethod.Post;
|
||||
|
||||
var dto = invoice.EntityToDTO(_NetworkProvider);
|
||||
InvoicePaymentNotification notification = new InvoicePaymentNotification()
|
||||
{
|
||||
Id = dto.Id,
|
||||
Currency = dto.Currency,
|
||||
CurrentTime = dto.CurrentTime,
|
||||
ExceptionStatus = dto.ExceptionStatus,
|
||||
ExpirationTime = dto.ExpirationTime,
|
||||
InvoiceTime = dto.InvoiceTime,
|
||||
PosData = dto.PosData,
|
||||
Price = dto.Price,
|
||||
Status = dto.Status,
|
||||
BuyerFields = invoice.RefundMail == null ? null : new Newtonsoft.Json.Linq.JObject() { new JProperty("buyerEmail", invoice.RefundMail) }
|
||||
};
|
||||
|
||||
// We keep backward compatibility with bitpay by passing BTC info to the notification
|
||||
// we don't pass other info, as it is a bad idea to use IPN data for logic processing (can be faked)
|
||||
var btcCryptoInfo = dto.CryptoInfo.FirstOrDefault(c => c.GetpaymentMethodId() == new PaymentMethodId("BTC", Payments.PaymentTypes.BTCLike));
|
||||
if (btcCryptoInfo != null)
|
||||
{
|
||||
#pragma warning disable CS0618
|
||||
notification.Rate = (double)dto.Rate;
|
||||
notification.Url = dto.Url;
|
||||
notification.BTCDue = dto.BTCDue;
|
||||
notification.BTCPaid = dto.BTCPaid;
|
||||
notification.BTCPrice = dto.BTCPrice;
|
||||
#pragma warning restore CS0618
|
||||
}
|
||||
|
||||
string notificationString = null;
|
||||
if (eventCode.HasValue)
|
||||
{
|
||||
var wrapper = new InvoicePaymentNotificationEventWrapper();
|
||||
wrapper.Data = notification;
|
||||
wrapper.Event = new InvoicePaymentNotificationEvent() { Code = eventCode.Value, Name = name };
|
||||
notificationString = JsonConvert.SerializeObject(wrapper);
|
||||
}
|
||||
else
|
||||
{
|
||||
notificationString = JsonConvert.SerializeObject(notification);
|
||||
}
|
||||
|
||||
request.RequestUri = new Uri(invoice.NotificationURL, UriKind.Absolute);
|
||||
request.Content = new StringContent(notificationString, UTF8, "application/json");
|
||||
var response = await Enqueue(invoice.Id, async () => await _Client.SendAsync(request, cancellation));
|
||||
return response;
|
||||
}
|
||||
|
||||
Dictionary<string, Task> _SendingRequestsByInvoiceId = new Dictionary<string, Task>();
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Will make sure only one callback is called at once on the same invoiceId
|
||||
/// </summary>
|
||||
/// <param name="id"></param>
|
||||
/// <param name="sendRequest"></param>
|
||||
/// <returns></returns>
|
||||
private async Task<T> Enqueue<T>(string id, Func<Task<T>> sendRequest)
|
||||
{
|
||||
Task<T> sending = null;
|
||||
lock (_SendingRequestsByInvoiceId)
|
||||
{
|
||||
if (_SendingRequestsByInvoiceId.TryGetValue(id, out var executing))
|
||||
{
|
||||
var completion = new TaskCompletionSource<T>();
|
||||
sending = completion.Task;
|
||||
_SendingRequestsByInvoiceId.Remove(id);
|
||||
_SendingRequestsByInvoiceId.Add(id, sending);
|
||||
executing.ContinueWith(_ =>
|
||||
{
|
||||
sendRequest()
|
||||
.ContinueWith(t =>
|
||||
{
|
||||
if(t.Status == TaskStatus.RanToCompletion)
|
||||
{
|
||||
completion.TrySetResult(t.Result);
|
||||
}
|
||||
if(t.Status == TaskStatus.Faulted)
|
||||
{
|
||||
completion.TrySetException(t.Exception);
|
||||
}
|
||||
if(t.Status == TaskStatus.Canceled)
|
||||
{
|
||||
completion.TrySetCanceled();
|
||||
}
|
||||
}, TaskScheduler.Default);
|
||||
}, TaskScheduler.Default);
|
||||
}
|
||||
else
|
||||
{
|
||||
sending = sendRequest();
|
||||
_SendingRequestsByInvoiceId.Add(id, sending);
|
||||
}
|
||||
sending.ContinueWith(o =>
|
||||
{
|
||||
lock (_SendingRequestsByInvoiceId)
|
||||
{
|
||||
_SendingRequestsByInvoiceId.TryGetValue(id, out var executing2);
|
||||
if(executing2 == sending)
|
||||
_SendingRequestsByInvoiceId.Remove(id);
|
||||
}
|
||||
}, TaskScheduler.Default);
|
||||
}
|
||||
return await sending;
|
||||
}
|
||||
|
||||
int MaxTry = 6;
|
||||
|
||||
private static string GetHttpJobId(InvoiceEntity invoice)
|
||||
{
|
||||
return $"{invoice.Id}-{invoice.Status}-HTTP";
|
||||
}
|
||||
|
||||
CompositeDisposable leases = new CompositeDisposable();
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
leases.Add(_EventAggregator.Subscribe<InvoiceEvent>(async e =>
|
||||
{
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, e.InvoiceId);
|
||||
await SaveEvent(invoice.Id, e);
|
||||
|
||||
// we need to use the status in the event and not in the invoice. The invoice might now be in another status.
|
||||
if (invoice.FullNotifications)
|
||||
{
|
||||
if (e.Name == "invoice_expired" ||
|
||||
e.Name == "invoice_paidInFull" ||
|
||||
e.Name == "invoice_failedToConfirm" ||
|
||||
e.Name == "invoice_markedInvalid" ||
|
||||
e.Name == "invoice_failedToConfirm" ||
|
||||
e.Name == "invoice_completed"
|
||||
)
|
||||
await Notify(invoice);
|
||||
}
|
||||
|
||||
if (e.Name == "invoice_confirmed")
|
||||
{
|
||||
await Notify(invoice);
|
||||
}
|
||||
|
||||
if (invoice.ExtendedNotifications)
|
||||
{
|
||||
await Notify(invoice, e.EventCode, e.Name);
|
||||
}
|
||||
}));
|
||||
|
||||
|
||||
leases.Add(_EventAggregator.Subscribe<InvoiceDataChangedEvent>(async e =>
|
||||
{
|
||||
await SaveEvent(e.InvoiceId, e);
|
||||
}));
|
||||
|
||||
|
||||
leases.Add(_EventAggregator.Subscribe<InvoiceStopWatchedEvent>(async e =>
|
||||
{
|
||||
await SaveEvent(e.InvoiceId, e);
|
||||
}));
|
||||
|
||||
leases.Add(_EventAggregator.Subscribe<InvoiceIPNEvent>(async e =>
|
||||
{
|
||||
await SaveEvent(e.InvoiceId, e);
|
||||
}));
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private Task SaveEvent(string invoiceId, object evt)
|
||||
{
|
||||
return _InvoiceRepository.AddInvoiceEvent(invoiceId, evt);
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
leases.Dispose();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
342
BTCPayServer/HostedServices/InvoiceWatcher.cs
Normal file
342
BTCPayServer/HostedServices/InvoiceWatcher.cs
Normal file
@ -0,0 +1,342 @@
|
||||
using NBXplorer;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using NBXplorer.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using NBitcoin;
|
||||
using BTCPayServer.Logging;
|
||||
using System.Threading;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using System.Collections.Concurrent;
|
||||
using Hangfire;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Events;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services;
|
||||
|
||||
namespace BTCPayServer.HostedServices
|
||||
{
|
||||
public class InvoiceWatcher : IHostedService
|
||||
{
|
||||
class UpdateInvoiceContext
|
||||
{
|
||||
public UpdateInvoiceContext(InvoiceEntity invoice)
|
||||
{
|
||||
Invoice = invoice;
|
||||
}
|
||||
public InvoiceEntity Invoice { get; set; }
|
||||
public List<object> Events { get; set; } = new List<object>();
|
||||
|
||||
bool _Dirty = false;
|
||||
public void MarkDirty()
|
||||
{
|
||||
_Dirty = true;
|
||||
}
|
||||
|
||||
public bool Dirty => _Dirty;
|
||||
}
|
||||
|
||||
InvoiceRepository _InvoiceRepository;
|
||||
EventAggregator _EventAggregator;
|
||||
BTCPayNetworkProvider _NetworkProvider;
|
||||
|
||||
public InvoiceWatcher(
|
||||
BTCPayNetworkProvider networkProvider,
|
||||
InvoiceRepository invoiceRepository,
|
||||
EventAggregator eventAggregator)
|
||||
{
|
||||
PollInterval = TimeSpan.FromMinutes(1.0);
|
||||
_InvoiceRepository = invoiceRepository ?? throw new ArgumentNullException(nameof(invoiceRepository));
|
||||
_EventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator));
|
||||
_NetworkProvider = networkProvider;
|
||||
}
|
||||
CompositeDisposable leases = new CompositeDisposable();
|
||||
|
||||
|
||||
private async Task UpdateInvoice(UpdateInvoiceContext context)
|
||||
{
|
||||
var invoice = context.Invoice;
|
||||
if (invoice.Status == "new" && invoice.ExpirationTime < DateTimeOffset.UtcNow)
|
||||
{
|
||||
context.MarkDirty();
|
||||
await _InvoiceRepository.UnaffectAddress(invoice.Id);
|
||||
|
||||
context.Events.Add(new InvoiceEvent(invoice, 1004, "invoice_expired"));
|
||||
invoice.Status = "expired";
|
||||
}
|
||||
|
||||
var derivationStrategies = invoice.GetDerivationStrategies(_NetworkProvider).ToArray();
|
||||
var payments = invoice.GetPayments().Where(p => p.Accounted).ToArray();
|
||||
var allPaymentMethods = invoice.GetPaymentMethods(_NetworkProvider);
|
||||
var paymentMethod = GetNearestClearedPayment(allPaymentMethods, out var accounting, _NetworkProvider);
|
||||
if (paymentMethod == null)
|
||||
return;
|
||||
var network = _NetworkProvider.GetNetwork(paymentMethod.GetId().CryptoCode);
|
||||
if (invoice.Status == "new" || invoice.Status == "expired")
|
||||
{
|
||||
if (accounting.Paid >= accounting.TotalDue)
|
||||
{
|
||||
if (invoice.Status == "new")
|
||||
{
|
||||
context.Events.Add(new InvoiceEvent(invoice, 1003, "invoice_paidInFull"));
|
||||
invoice.Status = "paid";
|
||||
invoice.ExceptionStatus = accounting.Paid > accounting.TotalDue ? "paidOver" : null;
|
||||
await _InvoiceRepository.UnaffectAddress(invoice.Id);
|
||||
context.MarkDirty();
|
||||
}
|
||||
else if (invoice.Status == "expired" && invoice.ExceptionStatus != "paidLate")
|
||||
{
|
||||
invoice.ExceptionStatus = "paidLate";
|
||||
context.Events.Add(new InvoiceEvent(invoice, 1009, "invoice_paidAfterExpiration"));
|
||||
context.MarkDirty();
|
||||
}
|
||||
}
|
||||
|
||||
if (accounting.Paid < accounting.TotalDue && invoice.GetPayments().Count != 0 && invoice.ExceptionStatus != "paidPartial")
|
||||
{
|
||||
invoice.ExceptionStatus = "paidPartial";
|
||||
context.MarkDirty();
|
||||
}
|
||||
}
|
||||
|
||||
// Just make sure RBF did not cancelled a payment
|
||||
if (invoice.Status == "paid")
|
||||
{
|
||||
if (accounting.Paid == accounting.TotalDue && invoice.ExceptionStatus == "paidOver")
|
||||
{
|
||||
invoice.ExceptionStatus = null;
|
||||
context.MarkDirty();
|
||||
}
|
||||
|
||||
if (accounting.Paid > accounting.TotalDue && invoice.ExceptionStatus != "paidOver")
|
||||
{
|
||||
invoice.ExceptionStatus = "paidOver";
|
||||
context.MarkDirty();
|
||||
}
|
||||
|
||||
if (accounting.Paid < accounting.TotalDue)
|
||||
{
|
||||
invoice.Status = "new";
|
||||
invoice.ExceptionStatus = accounting.Paid == Money.Zero ? null : "paidPartial";
|
||||
context.MarkDirty();
|
||||
}
|
||||
}
|
||||
|
||||
if (invoice.Status == "paid")
|
||||
{
|
||||
var confirmedAccounting = paymentMethod.Calculate(p => p.GetCryptoPaymentData().PaymentConfirmed(p, invoice.SpeedPolicy, network));
|
||||
|
||||
if (// Is after the monitoring deadline
|
||||
(invoice.MonitoringExpiration < DateTimeOffset.UtcNow)
|
||||
&&
|
||||
// And not enough amount confirmed
|
||||
(confirmedAccounting.Paid < accounting.TotalDue))
|
||||
{
|
||||
await _InvoiceRepository.UnaffectAddress(invoice.Id);
|
||||
context.Events.Add(new InvoiceEvent(invoice, 1013, "invoice_failedToConfirm"));
|
||||
invoice.Status = "invalid";
|
||||
context.MarkDirty();
|
||||
}
|
||||
else if (confirmedAccounting.Paid >= accounting.TotalDue)
|
||||
{
|
||||
await _InvoiceRepository.UnaffectAddress(invoice.Id);
|
||||
context.Events.Add(new InvoiceEvent(invoice, 1005, "invoice_confirmed"));
|
||||
invoice.Status = "confirmed";
|
||||
context.MarkDirty();
|
||||
}
|
||||
}
|
||||
|
||||
if (invoice.Status == "confirmed")
|
||||
{
|
||||
var completedAccounting = paymentMethod.Calculate(p => p.GetCryptoPaymentData().PaymentCompleted(p, network));
|
||||
if (completedAccounting.Paid >= accounting.TotalDue)
|
||||
{
|
||||
context.Events.Add(new InvoiceEvent(invoice, 1006, "invoice_completed"));
|
||||
invoice.Status = "complete";
|
||||
context.MarkDirty();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static PaymentMethod GetNearestClearedPayment(PaymentMethodDictionary allPaymentMethods, out PaymentMethodAccounting accounting, BTCPayNetworkProvider networkProvider)
|
||||
{
|
||||
PaymentMethod result = null;
|
||||
accounting = null;
|
||||
decimal nearestToZero = 0.0m;
|
||||
foreach (var paymentMethod in allPaymentMethods)
|
||||
{
|
||||
if (networkProvider != null && networkProvider.GetNetwork(paymentMethod.GetId().CryptoCode) == null)
|
||||
continue;
|
||||
var currentAccounting = paymentMethod.Calculate();
|
||||
var distanceFromZero = Math.Abs(currentAccounting.DueUncapped.ToDecimal(MoneyUnit.BTC));
|
||||
if (result == null || distanceFromZero < nearestToZero)
|
||||
{
|
||||
result = paymentMethod;
|
||||
nearestToZero = distanceFromZero;
|
||||
accounting = currentAccounting;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
TimeSpan _PollInterval;
|
||||
public TimeSpan PollInterval
|
||||
{
|
||||
get
|
||||
{
|
||||
return _PollInterval;
|
||||
}
|
||||
set
|
||||
{
|
||||
_PollInterval = value;
|
||||
}
|
||||
}
|
||||
|
||||
private void Watch(string invoiceId)
|
||||
{
|
||||
if (invoiceId == null)
|
||||
throw new ArgumentNullException(nameof(invoiceId));
|
||||
_WatchRequests.Add(invoiceId);
|
||||
}
|
||||
|
||||
private async Task Wait(string invoiceId)
|
||||
{
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
|
||||
try
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
if (invoice.ExpirationTime > now)
|
||||
{
|
||||
await Task.Delay(invoice.ExpirationTime - now, _Cts.Token);
|
||||
}
|
||||
Watch(invoiceId);
|
||||
now = DateTimeOffset.UtcNow;
|
||||
if (invoice.MonitoringExpiration > now)
|
||||
{
|
||||
await Task.Delay(invoice.MonitoringExpiration - now, _Cts.Token);
|
||||
}
|
||||
Watch(invoiceId);
|
||||
}
|
||||
catch when (_Cts.IsCancellationRequested)
|
||||
{ }
|
||||
|
||||
}
|
||||
|
||||
BlockingCollection<string> _WatchRequests = new BlockingCollection<string>(new ConcurrentQueue<string>());
|
||||
|
||||
Task _Loop;
|
||||
Task _WaitingInvoices;
|
||||
CancellationTokenSource _Cts;
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_Cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
_Loop = StartLoop(_Cts.Token);
|
||||
_WaitingInvoices = WaitPendingInvoices();
|
||||
|
||||
leases.Add(_EventAggregator.Subscribe<Events.InvoiceNeedUpdateEvent>(b =>
|
||||
{
|
||||
Watch(b.InvoiceId);
|
||||
}));
|
||||
leases.Add(_EventAggregator.Subscribe<Events.InvoiceEvent>(async b =>
|
||||
{
|
||||
if (b.Name == "invoice_created")
|
||||
{
|
||||
Watch(b.InvoiceId);
|
||||
await Wait(b.InvoiceId);
|
||||
}
|
||||
|
||||
if (b.Name == "invoice_receivedPayment")
|
||||
{
|
||||
Watch(b.InvoiceId);
|
||||
}
|
||||
}));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task WaitPendingInvoices()
|
||||
{
|
||||
await Task.WhenAll((await _InvoiceRepository.GetPendingInvoices())
|
||||
.Select(id => Wait(id)).ToArray());
|
||||
_WaitingInvoices = null;
|
||||
}
|
||||
|
||||
async Task StartLoop(CancellationToken cancellation)
|
||||
{
|
||||
Logs.PayServer.LogInformation("Start watching invoices");
|
||||
await Task.Delay(1).ConfigureAwait(false); // Small hack so that the caller does not block on GetConsumingEnumerable
|
||||
try
|
||||
{
|
||||
foreach (var invoiceId in _WatchRequests.GetConsumingEnumerable(cancellation))
|
||||
{
|
||||
int maxLoop = 5;
|
||||
int loopCount = -1;
|
||||
while (!cancellation.IsCancellationRequested && loopCount < maxLoop)
|
||||
{
|
||||
loopCount++;
|
||||
try
|
||||
{
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId, true);
|
||||
if (invoice == null)
|
||||
break;
|
||||
var updateContext = new UpdateInvoiceContext(invoice);
|
||||
await UpdateInvoice(updateContext);
|
||||
if (updateContext.Dirty)
|
||||
{
|
||||
await _InvoiceRepository.UpdateInvoiceStatus(invoice.Id, invoice.Status, invoice.ExceptionStatus);
|
||||
updateContext.Events.Add(new InvoiceDataChangedEvent(invoice));
|
||||
}
|
||||
|
||||
foreach (var evt in updateContext.Events)
|
||||
{
|
||||
_EventAggregator.Publish(evt, evt.GetType());
|
||||
}
|
||||
|
||||
if (invoice.Status == "complete" ||
|
||||
((invoice.Status == "invalid" || invoice.Status == "expired") && invoice.MonitoringExpiration < DateTimeOffset.UtcNow))
|
||||
{
|
||||
if (await _InvoiceRepository.RemovePendingInvoice(invoice.Id))
|
||||
_EventAggregator.Publish(new InvoiceStopWatchedEvent(invoice.Id));
|
||||
break;
|
||||
}
|
||||
|
||||
if (updateContext.Events.Count == 0 || cancellation.IsCancellationRequested)
|
||||
break;
|
||||
}
|
||||
catch (OperationCanceledException) when (cancellation.IsCancellationRequested)
|
||||
{
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logs.PayServer.LogError(ex, "Unhandled error on watching invoice " + invoiceId);
|
||||
#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
|
||||
Task.Delay(10000, cancellation)
|
||||
.ContinueWith(t => _WatchRequests.Add(invoiceId), TaskScheduler.Default);
|
||||
#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch when (cancellation.IsCancellationRequested)
|
||||
{
|
||||
}
|
||||
Logs.PayServer.LogInformation("Stop watching invoices");
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
leases.Dispose();
|
||||
_Cts.Cancel();
|
||||
var waitingPendingInvoices = _WaitingInvoices ?? Task.CompletedTask;
|
||||
return Task.WhenAll(waitingPendingInvoices, _Loop);
|
||||
}
|
||||
}
|
||||
}
|
222
BTCPayServer/HostedServices/NBXplorerWaiter.cs
Normal file
222
BTCPayServer/HostedServices/NBXplorerWaiter.cs
Normal file
@ -0,0 +1,222 @@
|
||||
using System;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Logging;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using NBXplorer;
|
||||
using NBXplorer.Models;
|
||||
using System.Collections.Concurrent;
|
||||
using BTCPayServer.Events;
|
||||
|
||||
namespace BTCPayServer.HostedServices
|
||||
{
|
||||
public enum NBXplorerState
|
||||
{
|
||||
NotConnected,
|
||||
Synching,
|
||||
Ready
|
||||
}
|
||||
|
||||
public class NBXplorerDashboard
|
||||
{
|
||||
public class NBXplorerSummary
|
||||
{
|
||||
public BTCPayNetwork Network { get; set; }
|
||||
public NBXplorerState State { get; set; }
|
||||
public StatusResult Status { get; set; }
|
||||
public string Error { get; set; }
|
||||
}
|
||||
ConcurrentDictionary<string, NBXplorerSummary> _Summaries = new ConcurrentDictionary<string, NBXplorerSummary>();
|
||||
public void Publish(BTCPayNetwork network, NBXplorerState state, StatusResult status, string error)
|
||||
{
|
||||
var summary = new NBXplorerSummary() { Network = network, State = state, Status = status, Error = error };
|
||||
_Summaries.AddOrUpdate(network.CryptoCode, summary, (k, v) => summary);
|
||||
}
|
||||
|
||||
public bool IsFullySynched()
|
||||
{
|
||||
return _Summaries.All(s => s.Value.Status != null && s.Value.Status.IsFullySynched);
|
||||
}
|
||||
|
||||
public bool IsFullySynched(string cryptoCode)
|
||||
{
|
||||
return _Summaries.Any(s => s.Key.Equals(cryptoCode, StringComparison.OrdinalIgnoreCase) && s.Value.Status != null && s.Value.Status.IsFullySynched);
|
||||
}
|
||||
|
||||
public IEnumerable<NBXplorerSummary> GetAll()
|
||||
{
|
||||
return _Summaries.Values;
|
||||
}
|
||||
}
|
||||
|
||||
public class NBXplorerWaiters : IHostedService
|
||||
{
|
||||
List<NBXplorerWaiter> _Waiters = new List<NBXplorerWaiter>();
|
||||
public NBXplorerWaiters(NBXplorerDashboard dashboard, ExplorerClientProvider explorerClientProvider, EventAggregator eventAggregator)
|
||||
{
|
||||
foreach (var explorer in explorerClientProvider.GetAll())
|
||||
{
|
||||
_Waiters.Add(new NBXplorerWaiter(dashboard, explorer.Item1, explorer.Item2, eventAggregator));
|
||||
}
|
||||
}
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.WhenAll(_Waiters.Select(w => w.StartAsync(cancellationToken)).ToArray());
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.WhenAll(_Waiters.Select(w => w.StopAsync(cancellationToken)).ToArray());
|
||||
}
|
||||
}
|
||||
|
||||
public class NBXplorerWaiter : IHostedService
|
||||
{
|
||||
|
||||
public NBXplorerWaiter(NBXplorerDashboard dashboard, BTCPayNetwork network, ExplorerClient client, EventAggregator aggregator)
|
||||
{
|
||||
_Network = network;
|
||||
_Client = client;
|
||||
_Aggregator = aggregator;
|
||||
_Dashboard = dashboard;
|
||||
}
|
||||
|
||||
NBXplorerDashboard _Dashboard;
|
||||
BTCPayNetwork _Network;
|
||||
EventAggregator _Aggregator;
|
||||
ExplorerClient _Client;
|
||||
|
||||
CancellationTokenSource _Cts;
|
||||
Task _Loop;
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_Cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
_Loop = StartLoop(_Cts.Token);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task StartLoop(CancellationToken cancellation)
|
||||
{
|
||||
Logs.PayServer.LogInformation($"Starting listening NBXplorer ({_Network.CryptoCode})");
|
||||
try
|
||||
{
|
||||
while (!cancellation.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
while (await StepAsync(cancellation))
|
||||
{
|
||||
|
||||
}
|
||||
await Task.Delay(PollInterval, cancellation);
|
||||
}
|
||||
catch (Exception ex) when (!cancellation.IsCancellationRequested)
|
||||
{
|
||||
Logs.PayServer.LogError(ex, $"Unhandled exception in NBXplorerWaiter ({_Network.CryptoCode})");
|
||||
await Task.Delay(TimeSpan.FromSeconds(10), cancellation);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch when (cancellation.IsCancellationRequested) { }
|
||||
}
|
||||
|
||||
private async Task<bool> StepAsync(CancellationToken cancellation)
|
||||
{
|
||||
var oldState = State;
|
||||
string error = null;
|
||||
StatusResult status = null;
|
||||
try
|
||||
{
|
||||
switch (State)
|
||||
{
|
||||
case NBXplorerState.NotConnected:
|
||||
status = await _Client.GetStatusAsync(cancellation);
|
||||
if (status != null)
|
||||
{
|
||||
if (status.IsFullySynched)
|
||||
{
|
||||
State = NBXplorerState.Ready;
|
||||
}
|
||||
else
|
||||
{
|
||||
State = NBXplorerState.Synching;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case NBXplorerState.Synching:
|
||||
status = await _Client.GetStatusAsync(cancellation);
|
||||
if (status == null)
|
||||
{
|
||||
State = NBXplorerState.NotConnected;
|
||||
}
|
||||
else if (status.IsFullySynched)
|
||||
{
|
||||
State = NBXplorerState.Ready;
|
||||
}
|
||||
break;
|
||||
case NBXplorerState.Ready:
|
||||
status = await _Client.GetStatusAsync(cancellation);
|
||||
if (status == null)
|
||||
{
|
||||
State = NBXplorerState.NotConnected;
|
||||
}
|
||||
else if (!status.IsFullySynched)
|
||||
{
|
||||
State = NBXplorerState.Synching;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
catch (Exception ex) when (!cancellation.IsCancellationRequested)
|
||||
{
|
||||
error = ex.Message;
|
||||
}
|
||||
|
||||
|
||||
if(status == null && error == null)
|
||||
error = $"{_Network.CryptoCode}: NBXplorer does not support this cryptocurrency";
|
||||
|
||||
if(status != null && error == null)
|
||||
{
|
||||
if(status.ChainType != _Network.NBXplorerNetwork.DefaultSettings.ChainType)
|
||||
error = $"{_Network.CryptoCode}: NBXplorer is on a different ChainType (actual: {status.ChainType}, expected: {_Network.NBXplorerNetwork.DefaultSettings.ChainType})";
|
||||
}
|
||||
|
||||
if (error != null)
|
||||
{
|
||||
State = NBXplorerState.NotConnected;
|
||||
status = null;
|
||||
_Aggregator.Publish(new NBXplorerErrorEvent(_Network, error));
|
||||
}
|
||||
|
||||
_Dashboard.Publish(_Network, State, status, error);
|
||||
if (oldState != State)
|
||||
{
|
||||
if (State == NBXplorerState.Synching)
|
||||
{
|
||||
PollInterval = TimeSpan.FromSeconds(10);
|
||||
}
|
||||
else
|
||||
{
|
||||
PollInterval = TimeSpan.FromMinutes(1);
|
||||
}
|
||||
_Aggregator.Publish(new NBXplorerStateChangedEvent(_Network, oldState, State));
|
||||
}
|
||||
return oldState != State;
|
||||
}
|
||||
|
||||
public TimeSpan PollInterval { get; set; } = TimeSpan.FromMinutes(1.0);
|
||||
|
||||
public NBXplorerState State { get; private set; }
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_Cts.Cancel();
|
||||
return _Loop;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
using BTCPayServer.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
@ -16,7 +17,7 @@ using NBXplorer;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Servcices.Invoices;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Services.Fees;
|
||||
@ -33,172 +34,184 @@ using System.Threading.Tasks;
|
||||
using System.Threading;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using BTCPayServer.Authentication;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.HostedServices;
|
||||
|
||||
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
|
||||
{
|
||||
var store = await _StoreRepository.FindStore((string)storeId, _UserManager.GetUserId(((Microsoft.AspNetCore.Mvc.ActionContext)context.Resource).HttpContext.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.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, BitpayRateProvider>();
|
||||
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>();
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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<InvoiceRepository>(o =>
|
||||
{
|
||||
var opts = o.GetRequiredService<BTCPayServerOptions>();
|
||||
var dbContext = o.GetRequiredService<ApplicationDbContextFactory>();
|
||||
var dbpath = Path.Combine(opts.DataDir, "InvoiceDB");
|
||||
if (!Directory.Exists(dbpath))
|
||||
Directory.CreateDirectory(dbpath);
|
||||
return new InvoiceRepository(dbContext, dbpath);
|
||||
});
|
||||
services.AddSingleton<BTCPayServerEnvironment>();
|
||||
services.TryAddSingleton<TokenRepository>();
|
||||
services.TryAddSingleton<EventAggregator>();
|
||||
services.TryAddSingleton<ApplicationDbContextFactory>(o =>
|
||||
{
|
||||
var opts = o.GetRequiredService<BTCPayServerOptions>();
|
||||
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);
|
||||
}
|
||||
return dbContext;
|
||||
});
|
||||
|
||||
services.AddAuthorization(o =>
|
||||
{
|
||||
o.AddPolicy("CanAccessStore", builder =>
|
||||
{
|
||||
builder.AddRequirements(new OwnStoreAuthorizationRequirement());
|
||||
});
|
||||
services.TryAddSingleton<BTCPayNetworkProvider>(o =>
|
||||
{
|
||||
var opts = o.GetRequiredService<BTCPayServerOptions>();
|
||||
return new BTCPayNetworkProvider(opts.ChainType);
|
||||
});
|
||||
|
||||
o.AddPolicy("OwnStore", builder =>
|
||||
{
|
||||
builder.AddRequirements(new OwnStoreAuthorizationRequirement("Owner"));
|
||||
});
|
||||
});
|
||||
services.TryAddSingleton<NBXplorerDashboard>();
|
||||
services.TryAddSingleton<StoreRepository>();
|
||||
services.TryAddSingleton<BTCPayWalletProvider>();
|
||||
services.TryAddSingleton<CurrencyNameTable>();
|
||||
services.TryAddSingleton<IFeeProviderFactory>(o => new NBXplorerFeeProviderFactory(o.GetRequiredService<ExplorerClientProvider>())
|
||||
{
|
||||
Fallback = new FeeRate(100, 1),
|
||||
BlockTarget = 20
|
||||
});
|
||||
|
||||
return services;
|
||||
}
|
||||
services.AddSingleton<IHostedService, NBXplorerWaiters>();
|
||||
services.AddSingleton<IHostedService, Payments.Bitcoin.NBXplorerListener>();
|
||||
services.AddSingleton<IHostedService, InvoiceNotificationManager>();
|
||||
services.AddSingleton<IHostedService, InvoiceWatcher>();
|
||||
|
||||
public static IApplicationBuilder UsePayServer(this IApplicationBuilder app)
|
||||
{
|
||||
if(app.ApplicationServices.GetRequiredService<BTCPayServerOptions>().RequireHttps)
|
||||
{
|
||||
var options = new RewriteOptions().AddRedirectToHttps();
|
||||
app.UseRewriter(options);
|
||||
}
|
||||
services.TryAddSingleton<ExplorerClientProvider>();
|
||||
services.TryAddSingleton<Bitpay>(o =>
|
||||
{
|
||||
if (o.GetRequiredService<BTCPayServerOptions>().ChainType == ChainType.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<IRateProviderFactory, CachedDefaultRateProviderFactory>();
|
||||
|
||||
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;
|
||||
}
|
||||
services.TryAddScoped<IHttpContextAccessor, HttpContextAccessor>();
|
||||
services.TryAddSingleton<IAuthorizationHandler, OwnStoreHandler>();
|
||||
services.AddTransient<AccessTokenController>();
|
||||
services.AddTransient<InvoiceController>();
|
||||
// Add application services.
|
||||
services.AddTransient<IEmailSender, EmailSender>();
|
||||
|
||||
static void Retry(Action act)
|
||||
{
|
||||
CancellationTokenSource cts = new CancellationTokenSource(10000);
|
||||
while(true)
|
||||
{
|
||||
try
|
||||
{
|
||||
act();
|
||||
return;
|
||||
}
|
||||
catch
|
||||
{
|
||||
if(cts.IsCancellationRequested)
|
||||
throw;
|
||||
Thread.Sleep(1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
services.AddAuthorization(o =>
|
||||
{
|
||||
o.AddPolicy("CanAccessStore", builder =>
|
||||
{
|
||||
builder.AddRequirements(new OwnStoreAuthorizationRequirement());
|
||||
});
|
||||
|
||||
o.AddPolicy("OwnStore", builder =>
|
||||
{
|
||||
builder.AddRequirements(new OwnStoreAuthorizationRequirement("Owner"));
|
||||
});
|
||||
});
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
public static IApplicationBuilder UsePayServer(this IApplicationBuilder 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 when(!cts.IsCancellationRequested)
|
||||
{
|
||||
Thread.Sleep(100);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -22,97 +22,175 @@ using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Routing;
|
||||
using Microsoft.AspNetCore.Http.Extensions;
|
||||
using BTCPayServer.Controllers;
|
||||
using System.Net.WebSockets;
|
||||
|
||||
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;
|
||||
BTCPayServerOptions _Options;
|
||||
|
||||
public BTCPayMiddleware(RequestDelegate next,
|
||||
TokenRepository tokenRepo,
|
||||
BTCPayServerOptions options)
|
||||
{
|
||||
_TokenRepository = tokenRepo ?? throw new ArgumentNullException(nameof(tokenRepo));
|
||||
_Next = next ?? throw new ArgumentNullException(nameof(next));
|
||||
_Options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
}
|
||||
|
||||
|
||||
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;
|
||||
}
|
||||
public async Task Invoke(HttpContext httpContext)
|
||||
{
|
||||
RewriteHostIfNeeded(httpContext);
|
||||
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, Array.Empty<string>());
|
||||
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 (WebSocketException)
|
||||
{ }
|
||||
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 void RewriteHostIfNeeded(HttpContext httpContext)
|
||||
{
|
||||
string reverseProxyScheme = null;
|
||||
if (httpContext.Request.Headers.TryGetValue("X-Forwarded-Proto", out StringValues proto))
|
||||
{
|
||||
var scheme = proto.SingleOrDefault();
|
||||
if (scheme != null)
|
||||
{
|
||||
reverseProxyScheme = scheme;
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
ushort? reverseProxyPort = null;
|
||||
if (httpContext.Request.Headers.TryGetValue("X-Forwarded-Port", out StringValues port))
|
||||
{
|
||||
var portString = port.SingleOrDefault();
|
||||
if (portString != null && ushort.TryParse(portString, out ushort pp))
|
||||
{
|
||||
reverseProxyPort = pp;
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure that code executing after this point think that the external url has been hit.
|
||||
if (_Options.ExternalUrl != null)
|
||||
{
|
||||
if (reverseProxyScheme != null && _Options.ExternalUrl.Scheme != reverseProxyScheme)
|
||||
{
|
||||
if (reverseProxyScheme == "http" && _Options.ExternalUrl.Scheme == "https")
|
||||
Logs.PayServer.LogWarning($"BTCPay ExternalUrl setting expected to use scheme '{_Options.ExternalUrl.Scheme}' externally, but the reverse proxy uses scheme '{reverseProxyScheme}'");
|
||||
httpContext.Request.Scheme = reverseProxyScheme;
|
||||
}
|
||||
else
|
||||
{
|
||||
httpContext.Request.Scheme = _Options.ExternalUrl.Scheme;
|
||||
}
|
||||
if (_Options.ExternalUrl.IsDefaultPort)
|
||||
httpContext.Request.Host = new HostString(_Options.ExternalUrl.Host);
|
||||
else
|
||||
{
|
||||
if (reverseProxyPort != null && _Options.ExternalUrl.Port != reverseProxyPort.Value)
|
||||
{
|
||||
Logs.PayServer.LogWarning($"BTCPay ExternalUrl setting expected to use port '{_Options.ExternalUrl.Port}' externally, but the reverse proxy uses port '{reverseProxyPort.Value}'");
|
||||
httpContext.Request.Host = new HostString(_Options.ExternalUrl.Host, reverseProxyPort.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
httpContext.Request.Host = new HostString(_Options.ExternalUrl.Host, _Options.ExternalUrl.Port);
|
||||
}
|
||||
}
|
||||
}
|
||||
// NGINX pass X-Forwarded-Proto and X-Forwarded-Port, so let's use that to have better guess of the real domain
|
||||
else
|
||||
{
|
||||
ushort? p = null;
|
||||
if (reverseProxyScheme != null)
|
||||
{
|
||||
httpContext.Request.Scheme = reverseProxyScheme;
|
||||
if (reverseProxyScheme == "http")
|
||||
p = 80;
|
||||
if (reverseProxyScheme == "https")
|
||||
p = 443;
|
||||
}
|
||||
|
||||
|
||||
if (reverseProxyPort != null)
|
||||
{
|
||||
p = reverseProxyPort.Value;
|
||||
}
|
||||
|
||||
if (p.HasValue)
|
||||
{
|
||||
bool isDefault = httpContext.Request.Scheme == "http" && p.Value == 80;
|
||||
isDefault |= httpContext.Request.Scheme == "https" && p.Value == 443;
|
||||
if (isDefault)
|
||||
httpContext.Request.Host = new HostString(httpContext.Request.Host.Host);
|
||||
else
|
||||
httpContext.Request.Host = new HostString(httpContext.Request.Host.Host, p.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitpayClient;
|
||||
@ -34,99 +35,143 @@ using Hangfire.Annotations;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using System.Threading;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.ApplicationInsights.AspNetCore.Extensions;
|
||||
using Microsoft.AspNetCore.Mvc.Cors.Internal;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core;
|
||||
using System.Net;
|
||||
|
||||
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)
|
||||
{
|
||||
Configuration = conf;
|
||||
}
|
||||
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 IConfiguration Configuration
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
services.ConfigureBTCPayServer(Configuration);
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
services.ConfigureBTCPayServer(Configuration);
|
||||
services.AddMemoryCache();
|
||||
services.AddIdentity<ApplicationUser, IdentityRole>()
|
||||
.AddEntityFrameworkStores<ApplicationDbContext>()
|
||||
.AddDefaultTokenProviders();
|
||||
|
||||
services.AddIdentity<ApplicationUser, IdentityRole>()
|
||||
.AddEntityFrameworkStores<ApplicationDbContext>()
|
||||
.AddDefaultTokenProviders();
|
||||
// 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"));
|
||||
});
|
||||
|
||||
// Big hack, tests fails because Hangfire fail at initializing at the second test run
|
||||
AddHangfireFix(services);
|
||||
services.AddBTCPayServer();
|
||||
services.AddMvc();
|
||||
}
|
||||
services.Configure<IdentityOptions>(options =>
|
||||
{
|
||||
options.Password.RequireDigit = false;
|
||||
options.Password.RequiredLength = 7;
|
||||
options.Password.RequireLowercase = false;
|
||||
options.Password.RequireNonAlphanumeric = false;
|
||||
options.Password.RequireUppercase = false;
|
||||
});
|
||||
|
||||
// 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);
|
||||
};
|
||||
// Needed to debug U2F for ledger support
|
||||
//services.Configure<KestrelServerOptions>(kestrel =>
|
||||
//{
|
||||
// kestrel.Listen(IPAddress.Loopback, 5012, l =>
|
||||
// {
|
||||
// l.UseHttps("devtest.pfx", "toto");
|
||||
// });
|
||||
//});
|
||||
}
|
||||
|
||||
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.AddHangfire(configuration);
|
||||
}
|
||||
// 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);
|
||||
};
|
||||
|
||||
public void Configure(
|
||||
IApplicationBuilder app,
|
||||
IHostingEnvironment env,
|
||||
ILoggerFactory loggerFactory)
|
||||
{
|
||||
if(env.IsDevelopment())
|
||||
{
|
||||
app.UseDeveloperExceptionPage();
|
||||
app.UseBrowserLink();
|
||||
}
|
||||
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.AddHangfire(configuration);
|
||||
services.AddCors(o =>
|
||||
{
|
||||
o.AddPolicy("BitpayAPI", b =>
|
||||
{
|
||||
b.AllowAnyMethod().AllowAnyHeader().AllowAnyOrigin();
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
app.UsePayServer();
|
||||
app.UseStaticFiles();
|
||||
app.UseAuthentication();
|
||||
Logs.Configure(loggerFactory);
|
||||
|
||||
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 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.UseWebSockets();
|
||||
app.UseMvc(routes =>
|
||||
{
|
||||
routes.MapRoute(
|
||||
name: "default",
|
||||
template: "{controller=Home}/{action=Index}/{id?}");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,395 +6,401 @@ using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
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;
|
||||
public CustomConsoleLogProvider(ConsoleLoggerProcessor processor)
|
||||
{
|
||||
_Processor = processor;
|
||||
}
|
||||
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,
|
||||
default(CancellationToken),
|
||||
TaskCreationOptions.LongRunning, TaskScheduler.Default);
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -7,48 +7,55 @@ 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");
|
||||
Events = factory.CreateLogger("Events");
|
||||
}
|
||||
public static ILogger Configuration
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public static ILogger PayServer
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public class FuncLoggerFactory : ILoggerFactory
|
||||
{
|
||||
private Func<string, ILogger> createLogger;
|
||||
public FuncLoggerFactory(Func<string, ILogger> createLogger)
|
||||
{
|
||||
this.createLogger = createLogger;
|
||||
}
|
||||
public void AddProvider(ILoggerProvider provider)
|
||||
{
|
||||
public static ILogger Events
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
}
|
||||
public const int ColumnLength = 16;
|
||||
}
|
||||
|
||||
public ILogger CreateLogger(string categoryName)
|
||||
{
|
||||
return createLogger(categoryName);
|
||||
}
|
||||
public class FuncLoggerFactory : ILoggerFactory
|
||||
{
|
||||
private Func<string, ILogger> createLogger;
|
||||
public FuncLoggerFactory(Func<string, ILogger> createLogger)
|
||||
{
|
||||
this.createLogger = createLogger;
|
||||
}
|
||||
public void AddProvider(ILoggerProvider provider)
|
||||
{
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
public ILogger CreateLogger(string categoryName)
|
||||
{
|
||||
return createLogger(categoryName);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
// <auto-generated />
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Servcices.Invoices;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
|
@ -1,6 +1,6 @@
|
||||
// <auto-generated />
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Servcices.Invoices;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
|
@ -1,6 +1,6 @@
|
||||
// <auto-generated />
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Servcices.Invoices;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
|
@ -1,6 +1,6 @@
|
||||
// <auto-generated />
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Servcices.Invoices;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
|
@ -1,6 +1,6 @@
|
||||
// <auto-generated />
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Servcices.Invoices;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
|
@ -4,51 +4,46 @@ 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 (this.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";
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
452
BTCPayServer/Migrations/20171023101754_StoreBlob.Designer.cs
generated
Normal file
452
BTCPayServer/Migrations/20171023101754_StoreBlob.Designer.cs
generated
Normal file
@ -0,0 +1,452 @@
|
||||
// <auto-generated />
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage;
|
||||
using Microsoft.EntityFrameworkCore.Storage.Internal;
|
||||
using System;
|
||||
|
||||
namespace BTCPayServer.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20171023101754_StoreBlob")]
|
||||
partial class StoreBlob
|
||||
{
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "2.0.0-rtm-26452");
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
|
||||
{
|
||||
b.Property<string>("Address")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<string>("InvoiceDataId");
|
||||
|
||||
b.HasKey("Address");
|
||||
|
||||
b.HasIndex("InvoiceDataId");
|
||||
|
||||
b.ToTable("AddressInvoices");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<byte[]>("Blob");
|
||||
|
||||
b.Property<DateTimeOffset>("Created");
|
||||
|
||||
b.Property<string>("CustomerEmail");
|
||||
|
||||
b.Property<string>("ExceptionStatus");
|
||||
|
||||
b.Property<string>("ItemCode");
|
||||
|
||||
b.Property<string>("OrderId");
|
||||
|
||||
b.Property<string>("Status");
|
||||
|
||||
b.Property<string>("StoreDataId");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("StoreDataId");
|
||||
|
||||
b.ToTable("Invoices");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PairedSINData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<string>("Facade");
|
||||
|
||||
b.Property<string>("Label");
|
||||
|
||||
b.Property<DateTimeOffset>("PairingTime");
|
||||
|
||||
b.Property<string>("SIN");
|
||||
|
||||
b.Property<string>("StoreDataId");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("SIN");
|
||||
|
||||
b.HasIndex("StoreDataId");
|
||||
|
||||
b.ToTable("PairedSINData");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PairingCodeData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<DateTime>("DateCreated");
|
||||
|
||||
b.Property<DateTimeOffset>("Expiration");
|
||||
|
||||
b.Property<string>("Facade");
|
||||
|
||||
b.Property<string>("Label");
|
||||
|
||||
b.Property<string>("SIN");
|
||||
|
||||
b.Property<string>("StoreDataId");
|
||||
|
||||
b.Property<string>("TokenValue");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("PairingCodes");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PaymentData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<byte[]>("Blob");
|
||||
|
||||
b.Property<string>("InvoiceDataId");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("InvoiceDataId");
|
||||
|
||||
b.ToTable("Payments");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("PendingInvoices");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<byte[]>("Blob");
|
||||
|
||||
b.Property<string>("InvoiceDataId");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("InvoiceDataId");
|
||||
|
||||
b.ToTable("RefundAddresses");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.SettingData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<string>("Value");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Settings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.StoreData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<string>("DerivationStrategy");
|
||||
|
||||
b.Property<int>("SpeedPolicy");
|
||||
|
||||
b.Property<byte[]>("StoreBlob");
|
||||
|
||||
b.Property<byte[]>("StoreCertificate");
|
||||
|
||||
b.Property<string>("StoreName");
|
||||
|
||||
b.Property<string>("StoreWebsite");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Stores");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.UserStore", b =>
|
||||
{
|
||||
b.Property<string>("ApplicationUserId");
|
||||
|
||||
b.Property<string>("StoreDataId");
|
||||
|
||||
b.Property<string>("Role");
|
||||
|
||||
b.HasKey("ApplicationUserId", "StoreDataId");
|
||||
|
||||
b.HasIndex("StoreDataId");
|
||||
|
||||
b.ToTable("UserStore");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Models.ApplicationUser", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<int>("AccessFailedCount");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken();
|
||||
|
||||
b.Property<string>("Email")
|
||||
.HasMaxLength(256);
|
||||
|
||||
b.Property<bool>("EmailConfirmed");
|
||||
|
||||
b.Property<bool>("LockoutEnabled");
|
||||
|
||||
b.Property<DateTimeOffset?>("LockoutEnd");
|
||||
|
||||
b.Property<string>("NormalizedEmail")
|
||||
.HasMaxLength(256);
|
||||
|
||||
b.Property<string>("NormalizedUserName")
|
||||
.HasMaxLength(256);
|
||||
|
||||
b.Property<string>("PasswordHash");
|
||||
|
||||
b.Property<string>("PhoneNumber");
|
||||
|
||||
b.Property<bool>("PhoneNumberConfirmed");
|
||||
|
||||
b.Property<bool>("RequiresEmailConfirmation");
|
||||
|
||||
b.Property<string>("SecurityStamp");
|
||||
|
||||
b.Property<bool>("TwoFactorEnabled");
|
||||
|
||||
b.Property<string>("UserName")
|
||||
.HasMaxLength(256);
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NormalizedEmail")
|
||||
.HasName("EmailIndex");
|
||||
|
||||
b.HasIndex("NormalizedUserName")
|
||||
.IsUnique()
|
||||
.HasName("UserNameIndex");
|
||||
|
||||
b.ToTable("AspNetUsers");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken();
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(256);
|
||||
|
||||
b.Property<string>("NormalizedName")
|
||||
.HasMaxLength(256);
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NormalizedName")
|
||||
.IsUnique()
|
||||
.HasName("RoleNameIndex");
|
||||
|
||||
b.ToTable("AspNetRoles");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<string>("ClaimType");
|
||||
|
||||
b.Property<string>("ClaimValue");
|
||||
|
||||
b.Property<string>("RoleId")
|
||||
.IsRequired();
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.ToTable("AspNetRoleClaims");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<string>("ClaimType");
|
||||
|
||||
b.Property<string>("ClaimValue");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired();
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AspNetUserClaims");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||
{
|
||||
b.Property<string>("LoginProvider");
|
||||
|
||||
b.Property<string>("ProviderKey");
|
||||
|
||||
b.Property<string>("ProviderDisplayName");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired();
|
||||
|
||||
b.HasKey("LoginProvider", "ProviderKey");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AspNetUserLogins");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||
{
|
||||
b.Property<string>("UserId");
|
||||
|
||||
b.Property<string>("RoleId");
|
||||
|
||||
b.HasKey("UserId", "RoleId");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.ToTable("AspNetUserRoles");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||
{
|
||||
b.Property<string>("UserId");
|
||||
|
||||
b.Property<string>("LoginProvider");
|
||||
|
||||
b.Property<string>("Name");
|
||||
|
||||
b.Property<string>("Value");
|
||||
|
||||
b.HasKey("UserId", "LoginProvider", "Name");
|
||||
|
||||
b.ToTable("AspNetUserTokens");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
||||
.WithMany()
|
||||
.HasForeignKey("InvoiceDataId");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
|
||||
.WithMany()
|
||||
.HasForeignKey("StoreDataId");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PaymentData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
||||
.WithMany("Payments")
|
||||
.HasForeignKey("InvoiceDataId");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
||||
.WithMany("RefundAddresses")
|
||||
.HasForeignKey("InvoiceDataId");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.UserStore", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Models.ApplicationUser", "ApplicationUser")
|
||||
.WithMany("UserStores")
|
||||
.HasForeignKey("ApplicationUserId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
|
||||
.WithMany("UserStores")
|
||||
.HasForeignKey("StoreDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Models.ApplicationUser")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Models.ApplicationUser")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("BTCPayServer.Models.ApplicationUser")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Models.ApplicationUser")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
24
BTCPayServer/Migrations/20171023101754_StoreBlob.cs
Normal file
24
BTCPayServer/Migrations/20171023101754_StoreBlob.cs
Normal file
@ -0,0 +1,24 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace BTCPayServer.Migrations
|
||||
{
|
||||
public partial class StoreBlob : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<byte[]>(
|
||||
name: "StoreBlob",
|
||||
table: "Stores",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "StoreBlob",
|
||||
table: "Stores");
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user