Compare commits
469 Commits
Author | SHA1 | Date | |
---|---|---|---|
322518e9dc | |||
ea2dd536b4 | |||
6a1eca760a | |||
29513d4ded | |||
86ca081030 | |||
14841ad7c9 | |||
2c6aa12aab | |||
ef4d39db3c | |||
7a566c477d | |||
85c40aef23 | |||
d00fa42553 | |||
e9e94f5e99 | |||
5e2d4407ca | |||
846bd08e20 | |||
a1a4eed860 | |||
1e582625f3 | |||
39b018fdf3 | |||
83304de1c6 | |||
5c8e03dcbf | |||
4dddc539f6 | |||
9950b781b4 | |||
7a32f692d1 | |||
d480be925b | |||
3775317047 | |||
500bdd9bf1 | |||
57bda24664 | |||
6401af00fe | |||
3b3a18bbbc | |||
b4e9dfeeaf | |||
16f5def245 | |||
26e7de534b | |||
a3ae694048 | |||
b04d70f141 | |||
101d6131c7 | |||
faabd68f6f | |||
aa72b814da | |||
0dcda0f289 | |||
a2b039f983 | |||
1a54f2d01a | |||
4276994265 | |||
2c6133b4f7 | |||
64181d1a93 | |||
25e9a27a78 | |||
f3edaf5160 | |||
7bfdf2d11d | |||
d2808cf662 | |||
b68fcec692 | |||
86644d38d7 | |||
52f60b0457 | |||
638b58ab48 | |||
1606f43609 | |||
ad1307746c | |||
22307b8a2b | |||
f1c244bf6a | |||
c425e0439d | |||
cdf78f0463 | |||
7d997f7d60 | |||
0edbbe6ae3 | |||
4f7bfbf451 | |||
cd416dac60 | |||
b58173967d | |||
4b743bb998 | |||
0af1adcfb8 | |||
c048e4eeaf | |||
64194896ba | |||
1d4472cc08 | |||
d8bc7ef4b8 | |||
27cea81cb2 | |||
b0d6216ffc | |||
060876d07f | |||
96b7373d88 | |||
c2f282f85c | |||
7afa726ddc | |||
6eb7bbf853 | |||
11a26c940d | |||
624252e4ad | |||
57bb980526 | |||
e0c718f4ba | |||
c58e015bfb | |||
648829644a | |||
1b0f8c7aca | |||
4f8e0b0393 | |||
466f65d6cd | |||
022b4f115d | |||
71f6aaabbd | |||
79b06bce42 | |||
480afebcd9 | |||
96721e95a2 | |||
883cd41232 | |||
3f48a478af | |||
8d3b45bdec | |||
bbd19a96ec | |||
ce17e3212a | |||
c3ea63c6ce | |||
1ee4bd9c92 | |||
e6bb6619e5 | |||
cc29d863d7 | |||
8cb2c93abd | |||
2187e05a10 | |||
4c07483383 | |||
65916755b6 | |||
3cefbc89e4 | |||
c40b47b1dd | |||
a2d17bfa7e | |||
cdf0c6d27d | |||
8154986102 | |||
203494e809 | |||
d49bbc95af | |||
c44132fd35 | |||
c75512303d | |||
97d50df13e | |||
0e1ef78af1 | |||
464ab30fea | |||
3ee1c05646 | |||
c54c39926b | |||
33d18a3278 | |||
97e564901e | |||
832069dd44 | |||
1ac17e96c3 | |||
d907031ec7 | |||
4b5af9cb5c | |||
0057146fee | |||
0c8925d2a2 | |||
b9e5b0d56e | |||
eb6dbd1247 | |||
fe8428b8b0 | |||
f2aa15310a | |||
1814cb2d6e | |||
94a6f20a05 | |||
22e700a53e | |||
cd78e559cf | |||
f0257fb8f7 | |||
34cdbf73f0 | |||
b291a6d25a | |||
fa7e974e73 | |||
976d9d0cda | |||
6ea2d9175d | |||
10ceddc709 | |||
5dd57c8064 | |||
a256dd3277 | |||
5ee9a92f1e | |||
65c7c85c14 | |||
27b686095c | |||
cd2fef0dab | |||
743288fa47 | |||
270ebead49 | |||
145e3bec83 | |||
563e931468 | |||
3113097c4f | |||
cdbbad1694 | |||
c9c2730409 | |||
310a9a6d59 | |||
1a1078782e | |||
73cb3dc4ee | |||
9eb36a8b40 | |||
6307aa8665 | |||
b9e8408db5 | |||
0879895678 | |||
a4ecf070b0 | |||
162d76206e | |||
5af14ef2ec | |||
7210eebeca | |||
25dbf6445f | |||
0828c60537 | |||
34deb17f3d | |||
06b02b8691 | |||
b7abc08c27 | |||
399ae2cd9e | |||
63fe0f6612 | |||
42d60ef84b | |||
1784c30787 | |||
ac8feceaf2 | |||
3d8c5195ae | |||
9a5259510b | |||
caecb26420 | |||
ecc8b3d9ed | |||
d313395751 | |||
9e698a8004 | |||
3c4c99ee42 | |||
d34ffc0d9a | |||
039303bfaa | |||
273cf1adc9 | |||
5feb520843 | |||
17e914778d | |||
db24ab792f | |||
42475ec7b7 | |||
4972f0ab7b | |||
07e13747cf | |||
2465eb7e36 | |||
4ddcd7a4c8 | |||
89d9658e82 | |||
66ecb32538 | |||
a22576da0a | |||
69bd888bab | |||
9b540273fc | |||
cfd083bed5 | |||
55c9314cdd | |||
8cafa8a483 | |||
448cc06a11 | |||
0780df4fd7 | |||
04174b7431 | |||
b7c58c2083 | |||
cd75fd6842 | |||
370951a3bd | |||
2c08b0137b | |||
724af44e41 | |||
1eee31e9f1 | |||
01cf579530 | |||
f72705935a | |||
a29ab6b6b0 | |||
4784518235 | |||
f8c88bd44f | |||
0d1d0d57f4 | |||
2bd1238668 | |||
d1fb51b412 | |||
279de1b869 | |||
ce9189caf8 | |||
431147784e | |||
0697b8bf86 | |||
5050b59014 | |||
665cf4c3b1 | |||
bac9ef4f93 | |||
98e81ab0fd | |||
6ce70237fc | |||
4f23fc99a1 | |||
d7fccae452 | |||
d7a5021ed2 | |||
63ec832667 | |||
8d95b9fa04 | |||
ada6f3b844 | |||
c8a26ce952 | |||
6cf80b7533 | |||
79df523bb2 | |||
e921f9757a | |||
b497d1871e | |||
c7cd029482 | |||
68f2cba60d | |||
5c4200b036 | |||
bc06114023 | |||
bed9737d64 | |||
556082c4c9 | |||
6a46d02fc6 | |||
d75e5b8b12 | |||
d293bc3947 | |||
e634700913 | |||
ce81136c88 | |||
a97ef2eee8 | |||
be33ebc168 | |||
789193a0c8 | |||
01792cf299 | |||
ff9265f721 | |||
8d61314852 | |||
1ce6ae8727 | |||
dec5dbc0d2 | |||
4e32dad1ea | |||
127ca7582f | |||
b98993f84b | |||
e35f074b66 | |||
ba3d13d56c | |||
ead67887ab | |||
437f27f107 | |||
8d41a8e98d | |||
7e6ab015a6 | |||
2583eb15ec | |||
1879ea55e8 | |||
f8bc3a5081 | |||
dd1a93ee0e | |||
093ae39e61 | |||
cac58808f0 | |||
a063f10778 | |||
3cf3aa63f6 | |||
011dd5574f | |||
365911286b | |||
fe5347aa86 | |||
f22c8a72cd | |||
eeb522fe7d | |||
f9e40b209a | |||
20635ea3d6 | |||
6cefd9c3e7 | |||
7062705d6f | |||
58b994e043 | |||
640ff36fa2 | |||
39ec5242d7 | |||
1c50210e61 | |||
a1ffda0151 | |||
fd15348551 | |||
989c99c550 | |||
bcf97b1474 | |||
67abbed66a | |||
eb01e91e13 | |||
12ceb9e0bc | |||
ecf03f90aa | |||
1747414a57 | |||
3a02f16c6e | |||
a6ee337ed0 | |||
559f535257 | |||
2952ccc7fd | |||
a0243fa569 | |||
789b9168ad | |||
7c29cb62ef | |||
f81ca1888d | |||
ed02e0f4d6 | |||
0a83f21af5 | |||
23a3c145ed | |||
4184c6c208 | |||
29c28b1841 | |||
de48fb4077 | |||
bcd79c5882 | |||
b8c513aa2b | |||
ad67f4ef18 | |||
2c0bcfc0ec | |||
0ba1072d54 | |||
f7fe855274 | |||
449738414b | |||
a34842585d | |||
eb882c2c46 | |||
ca65c6bd8f | |||
f97173e9e7 | |||
8fc1b0c856 | |||
cabd7c4e64 | |||
f8540dc78c | |||
b03d271f85 | |||
3770adb7d3 | |||
7fdf19ca22 | |||
4e776adb03 | |||
26db946392 | |||
d102c142b9 | |||
f7989541b9 | |||
b7f0ce18b3 | |||
e1dfbfe3b0 | |||
786d129452 | |||
a37a8e8fcd | |||
355989c278 | |||
af3dee95de | |||
70a6bd6a01 | |||
4afb0acc84 | |||
9afc143801 | |||
8e4943df65 | |||
9b3bd8343d | |||
ee4f83ddba | |||
c326998381 | |||
239a011e60 | |||
5ffe118159 | |||
6f07849e1d | |||
dbe5c62d11 | |||
199db01eaf | |||
a3c46c8f67 | |||
66a68d6180 | |||
be1128a886 | |||
d41a5a65a2 | |||
d5cab938ee | |||
9dddfb65f0 | |||
6bd5976d90 | |||
b3385bf901 | |||
bba268b5e2 | |||
70c98b6901 | |||
2d3b7fea2e | |||
3bdf1c9a00 | |||
a52665ea80 | |||
3d943d49e6 | |||
6ca8ba9231 | |||
75d685ae6c | |||
7b2ef9aec2 | |||
efe666b284 | |||
ca8af5047a | |||
cdc0b0d628 | |||
87e28b70fd | |||
b96f464e39 | |||
bca68986f3 | |||
272ac49872 | |||
5f05ca5ac6 | |||
7872b3ec55 | |||
27a0aebd12 | |||
366490516e | |||
9a92646d4d | |||
b002c49dac | |||
3f4ec9ba80 | |||
0290a5eacd | |||
744734a6a1 | |||
29f662f87c | |||
af21f9f10c | |||
efdc99b9d1 | |||
4458e63c1a | |||
3225745115 | |||
a325592106 | |||
01069ed583 | |||
0fc770bbb1 | |||
dfb79ef96e | |||
4ebffc8d43 | |||
c2dad08fef | |||
7c3ddf904c | |||
c3d73236e0 | |||
8a4da361fd | |||
57effe318b | |||
9325441693 | |||
180341576b | |||
e2533a93e3 | |||
14360bde78 | |||
d793265bed | |||
0a449e1e8e | |||
74ccc34c9c | |||
674cd1486d | |||
ce12e87b70 | |||
8f1324fdf3 | |||
3ab69046b0 | |||
6dc4bfaefe | |||
f460837f96 | |||
34d0d3e011 | |||
e57a488371 | |||
43be1e191f | |||
cfbcf0947a | |||
fcfba7f5e1 | |||
f4f9fabfd3 | |||
25208915eb | |||
eb975bf8fc | |||
21bbf49640 | |||
9339c7dff2 | |||
af0eb831a2 | |||
1fc9a1a54b | |||
3954ce2137 | |||
271de362cb | |||
d41474ebc8 | |||
5b0b3e30f4 | |||
48a95457b6 | |||
7c0b26174f | |||
f0145142a4 | |||
2848caff2e | |||
75f4a39ef2 | |||
f9f4d93191 | |||
9e05ad787f | |||
69050f7a56 | |||
de39fa0aea | |||
94ff77f2b2 | |||
bb7dc1ed4a | |||
c5e833ee79 | |||
4397591134 | |||
986c7b94f4 | |||
a6ef7387cf | |||
1743919cd4 | |||
131328b42c | |||
ad3b605148 | |||
f32e225fa6 | |||
95bdeacd93 | |||
07c2f6b810 | |||
8ff81f1648 | |||
c3ee43c228 | |||
d85da28ca7 | |||
042142396d | |||
fbc4ca89aa | |||
2e5d29064b | |||
ef0b8376d3 | |||
1fa1b74261 | |||
4f9e4116a2 | |||
82d8fda05f | |||
d4935263da | |||
e158d909fb | |||
de8147d5dd | |||
16f1791a9a | |||
8745c3f8c6 | |||
ec5b45cff6 | |||
1348197295 | |||
f2516854d8 | |||
062ca6e743 | |||
44b6997bb5 | |||
78b544f9ca | |||
81926b4450 | |||
52e0845fc5 | |||
daf1a0a4bc | |||
bc8978182e |
@ -1,14 +1,15 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp2.0</TargetFramework>
|
||||
<TargetFramework>netcoreapp2.1</TargetFramework>
|
||||
|
||||
<IsPackable>false</IsPackable>
|
||||
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
|
||||
<LangVersion>7.2</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.7.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.7.2" />
|
||||
<PackageReference Include="xunit" Version="2.3.1" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.3.1" />
|
||||
</ItemGroup>
|
||||
|
@ -2,8 +2,11 @@
|
||||
using BTCPayServer.Hosting;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Tests.Logging;
|
||||
using BTCPayServer.Tests.Mocks;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
@ -104,15 +107,6 @@ namespace BTCPayServer.Tests
|
||||
.UseConfiguration(conf)
|
||||
.ConfigureServices(s =>
|
||||
{
|
||||
if (MockRates)
|
||||
{
|
||||
var mockRates = new MockRateProviderFactory();
|
||||
var btc = new MockRateProvider("BTC", new Rate("USD", 5000m), new Rate("CAD", 4500m));
|
||||
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)
|
||||
@ -126,6 +120,36 @@ namespace BTCPayServer.Tests
|
||||
.Build();
|
||||
_Host.Start();
|
||||
InvoiceRepository = (InvoiceRepository)_Host.Services.GetService(typeof(InvoiceRepository));
|
||||
StoreRepository = (StoreRepository)_Host.Services.GetService(typeof(StoreRepository));
|
||||
var rateProvider = (BTCPayRateProviderFactory)_Host.Services.GetService(typeof(BTCPayRateProviderFactory));
|
||||
rateProvider.DirectProviders.Clear();
|
||||
|
||||
var coinAverageMock = new MockRateProvider();
|
||||
coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
|
||||
{
|
||||
Exchange = "coinaverage",
|
||||
CurrencyPair = CurrencyPair.Parse("BTC_USD"),
|
||||
BidAsk = new BidAsk(5000m)
|
||||
});
|
||||
coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
|
||||
{
|
||||
Exchange = "coinaverage",
|
||||
CurrencyPair = CurrencyPair.Parse("BTC_CAD"),
|
||||
BidAsk = new BidAsk(4500m)
|
||||
});
|
||||
coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
|
||||
{
|
||||
Exchange = "coinaverage",
|
||||
CurrencyPair = CurrencyPair.Parse("LTC_BTC"),
|
||||
BidAsk = new BidAsk(0.001m)
|
||||
});
|
||||
coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
|
||||
{
|
||||
Exchange = "coinaverage",
|
||||
CurrencyPair = CurrencyPair.Parse("LTC_USD"),
|
||||
BidAsk = new BidAsk(500m)
|
||||
});
|
||||
rateProvider.DirectProviders.Add("coinaverage", coinAverageMock);
|
||||
}
|
||||
|
||||
public string HostName
|
||||
@ -134,6 +158,7 @@ namespace BTCPayServer.Tests
|
||||
internal set;
|
||||
}
|
||||
public InvoiceRepository InvoiceRepository { get; private set; }
|
||||
public StoreRepository StoreRepository { get; private set; }
|
||||
public Uri IntegratedLightning { get; internal set; }
|
||||
public bool InContainer { get; internal set; }
|
||||
|
||||
@ -142,7 +167,7 @@ namespace BTCPayServer.Tests
|
||||
return _Host.Services.GetRequiredService<T>();
|
||||
}
|
||||
|
||||
public T GetController<T>(string userId = null) where T : Controller
|
||||
public T GetController<T>(string userId = null, string storeId = null) where T : Controller
|
||||
{
|
||||
var context = new DefaultHttpContext();
|
||||
context.Request.Host = new HostString("127.0.0.1");
|
||||
@ -150,7 +175,11 @@ namespace BTCPayServer.Tests
|
||||
context.Request.Protocol = "http";
|
||||
if (userId != null)
|
||||
{
|
||||
context.User = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, userId) }));
|
||||
context.User = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, userId) }, Policies.CookieAuthentication));
|
||||
}
|
||||
if(storeId != null)
|
||||
{
|
||||
context.SetStoreData(GetService<StoreRepository>().FindStore(storeId, userId).GetAwaiter().GetResult());
|
||||
}
|
||||
var scope = (IServiceScopeFactory)_Host.Services.GetService(typeof(IServiceScopeFactory));
|
||||
var provider = scope.CreateScope().ServiceProvider;
|
||||
|
@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Payments.Lightning.Charge;
|
||||
using BTCPayServer.Payments.Lightning.CLightning;
|
||||
using NBitcoin;
|
||||
@ -15,7 +16,7 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
this._Parent = serverTester;
|
||||
var url = serverTester.GetEnvironment(environmentName, defaultValue);
|
||||
Client = new ChargeClient(new Uri(url), network);
|
||||
Client = (ChargeClient)LightningClientFactory.CreateClient(url, network);
|
||||
P2PHost = _Parent.GetEnvironment(environmentName + "_HOST", defaultHost);
|
||||
}
|
||||
public ChargeClient Client { get; set; }
|
||||
|
@ -1,4 +1,4 @@
|
||||
FROM microsoft/dotnet:2.0.6-sdk-2.1.101-stretch
|
||||
FROM microsoft/dotnet:2.1.300-sdk-alpine3.7
|
||||
WORKDIR /app
|
||||
# caches restore result by copying csproj file separately
|
||||
COPY BTCPayServer.Tests/BTCPayServer.Tests.csproj BTCPayServer.Tests/BTCPayServer.Tests.csproj
|
||||
|
27
BTCPayServer.Tests/Lnd/LndMockTester.cs
Normal file
27
BTCPayServer.Tests/Lnd/LndMockTester.cs
Normal file
@ -0,0 +1,27 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using BTCPayServer.Payments.Lightning.Lnd;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer.Tests.Lnd
|
||||
{
|
||||
public class LndMockTester
|
||||
{
|
||||
private ServerTester _Parent;
|
||||
|
||||
public LndMockTester(ServerTester serverTester, string environmentName, string defaultValue, string defaultHost, Network network)
|
||||
{
|
||||
this._Parent = serverTester;
|
||||
var url = serverTester.GetEnvironment(environmentName, defaultValue);
|
||||
|
||||
Swagger = new LndSwaggerClient(new LndRestSettings(new Uri(url)) { AllowInsecure = true });
|
||||
Client = new LndInvoiceClient(Swagger);
|
||||
P2PHost = _Parent.GetEnvironment(environmentName + "_HOST", defaultHost);
|
||||
}
|
||||
|
||||
public LndSwaggerClient Swagger { get; set; }
|
||||
public LndInvoiceClient Client { get; set; }
|
||||
public string P2PHost { get; }
|
||||
}
|
||||
}
|
246
BTCPayServer.Tests/Lnd/UnitTests.cs
Normal file
246
BTCPayServer.Tests/Lnd/UnitTests.cs
Normal file
@ -0,0 +1,246 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Payments.Lightning.Lnd;
|
||||
using NBitcoin;
|
||||
using NBitcoin.RPC;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using NBitpayClient;
|
||||
using System.Globalization;
|
||||
using Xunit.Sdk;
|
||||
|
||||
namespace BTCPayServer.Tests.Lnd
|
||||
{
|
||||
// this depends for now on `docker-compose up devlnd`
|
||||
public class UnitTests
|
||||
{
|
||||
private readonly ITestOutputHelper output;
|
||||
|
||||
public UnitTests(ITestOutputHelper output)
|
||||
{
|
||||
this.output = output;
|
||||
initializeEnvironment();
|
||||
|
||||
MerchantLnd = new LndSwaggerClient(new LndRestSettings(new Uri("https://127.0.0.1:53280")) { AllowInsecure = true });
|
||||
InvoiceClient = new LndInvoiceClient(MerchantLnd);
|
||||
|
||||
CustomerLnd = new LndSwaggerClient(new LndRestSettings(new Uri("https://127.0.0.1:53281")) { AllowInsecure = true });
|
||||
}
|
||||
|
||||
private LndSwaggerClient MerchantLnd { get; set; }
|
||||
private LndInvoiceClient InvoiceClient { get; set; }
|
||||
|
||||
private LndSwaggerClient CustomerLnd { get; set; }
|
||||
|
||||
[Fact]
|
||||
public async Task GetInfo()
|
||||
{
|
||||
var res = await InvoiceClient.GetInfo();
|
||||
output.WriteLine("Result: " + res.ToJson());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateInvoice()
|
||||
{
|
||||
var res = await InvoiceClient.CreateInvoice(10000, "Hello world", TimeSpan.FromSeconds(3600));
|
||||
output.WriteLine("Result: " + res.ToJson());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetInvoice()
|
||||
{
|
||||
var createInvoice = await InvoiceClient.CreateInvoice(10000, "Hello world", TimeSpan.FromSeconds(3600));
|
||||
var getInvoice = await InvoiceClient.GetInvoice(createInvoice.Id);
|
||||
|
||||
Assert.Equal(createInvoice.BOLT11, getInvoice.BOLT11);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Play()
|
||||
{
|
||||
var seq = new System.Buffers.ReadOnlySequence<byte>(new ReadOnlyMemory<byte>(new byte[1000]));
|
||||
var seq2 = seq.Slice(3);
|
||||
var pos = seq2.GetPosition(0);
|
||||
}
|
||||
|
||||
// integration tests
|
||||
[Fact]
|
||||
public async Task TestWaitListenInvoice()
|
||||
{
|
||||
var merchantInvoice = await InvoiceClient.CreateInvoice(10000, "Hello world", TimeSpan.FromSeconds(3600));
|
||||
var merchantInvoice2 = await InvoiceClient.CreateInvoice(10000, "Hello world", TimeSpan.FromSeconds(3600));
|
||||
|
||||
var waitToken = default(CancellationToken);
|
||||
var listener = await InvoiceClient.Listen(waitToken);
|
||||
var waitTask = listener.WaitInvoice(waitToken);
|
||||
|
||||
await EnsureLightningChannelAsync();
|
||||
var payResponse = await CustomerLnd.SendPaymentSyncAsync(new LnrpcSendRequest
|
||||
{
|
||||
Payment_request = merchantInvoice.BOLT11
|
||||
});
|
||||
|
||||
var invoice = await waitTask;
|
||||
Assert.True(invoice.PaidAt.HasValue);
|
||||
|
||||
var waitTask2 = listener.WaitInvoice(waitToken);
|
||||
|
||||
payResponse = await CustomerLnd.SendPaymentSyncAsync(new LnrpcSendRequest
|
||||
{
|
||||
Payment_request = merchantInvoice2.BOLT11
|
||||
});
|
||||
|
||||
invoice = await waitTask2;
|
||||
Assert.True(invoice.PaidAt.HasValue);
|
||||
|
||||
var waitTask3 = listener.WaitInvoice(waitToken);
|
||||
await Task.Delay(100);
|
||||
listener.Dispose();
|
||||
Assert.Throws<TaskCanceledException>(() => waitTask3.GetAwaiter().GetResult());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateLndInvoiceAndPay()
|
||||
{
|
||||
var merchantInvoice = await InvoiceClient.CreateInvoice(10000, "Hello world", TimeSpan.FromSeconds(3600));
|
||||
|
||||
await EnsureLightningChannelAsync();
|
||||
|
||||
await EventuallyAsync(async () =>
|
||||
{
|
||||
var payResponse = await CustomerLnd.SendPaymentSyncAsync(new LnrpcSendRequest
|
||||
{
|
||||
Payment_request = merchantInvoice.BOLT11
|
||||
});
|
||||
|
||||
var invoice = await InvoiceClient.GetInvoice(merchantInvoice.Id);
|
||||
Assert.True(invoice.PaidAt.HasValue);
|
||||
});
|
||||
}
|
||||
|
||||
private async Task EventuallyAsync(Func<Task> act)
|
||||
{
|
||||
CancellationTokenSource cts = new CancellationTokenSource(20000);
|
||||
while (true)
|
||||
{
|
||||
try
|
||||
{
|
||||
await act();
|
||||
break;
|
||||
}
|
||||
catch (XunitException) when (!cts.Token.IsCancellationRequested)
|
||||
{
|
||||
await Task.Delay(500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<LnrpcChannel> EnsureLightningChannelAsync()
|
||||
{
|
||||
var merchantInfo = await WaitLNSynched();
|
||||
var merchantNodeAddress = new LnrpcLightningAddress
|
||||
{
|
||||
Pubkey = merchantInfo.NodeId,
|
||||
Host = "merchant_lnd:9735"
|
||||
};
|
||||
|
||||
while (true)
|
||||
{
|
||||
// if channel is pending generate blocks until confirmed
|
||||
var pendingResponse = await CustomerLnd.PendingChannelsAsync();
|
||||
if (pendingResponse.Pending_open_channels?
|
||||
.Any(a => a.Channel?.Remote_node_pub == merchantNodeAddress.Pubkey) == true)
|
||||
{
|
||||
ExplorerNode.Generate(1);
|
||||
await WaitLNSynched();
|
||||
continue;
|
||||
}
|
||||
|
||||
// check if channel is established
|
||||
var chanResponse = await CustomerLnd.ListChannelsAsync(null, null, null, null);
|
||||
LnrpcChannel channelToMerchant = null;
|
||||
if (chanResponse != null && chanResponse.Channels != null)
|
||||
{
|
||||
channelToMerchant = chanResponse.Channels
|
||||
.Where(a => a.Remote_pubkey == merchantNodeAddress.Pubkey)
|
||||
.FirstOrDefault();
|
||||
}
|
||||
|
||||
if (channelToMerchant == null)
|
||||
{
|
||||
// create new channel
|
||||
var isConnected = await CustomerLnd.ListPeersAsync();
|
||||
if (isConnected.Peers == null ||
|
||||
!isConnected.Peers.Any(a => a.Pub_key == merchantInfo.NodeId))
|
||||
{
|
||||
var connectResp = await CustomerLnd.ConnectPeerAsync(new LnrpcConnectPeerRequest
|
||||
{
|
||||
Addr = merchantNodeAddress
|
||||
});
|
||||
}
|
||||
|
||||
var addressResponse = await CustomerLnd.NewWitnessAddressAsync();
|
||||
var address = BitcoinAddress.Create(addressResponse.Address, Network.RegTest);
|
||||
await ExplorerNode.SendToAddressAsync(address, Money.Coins(0.2m));
|
||||
ExplorerNode.Generate(1);
|
||||
await WaitLNSynched();
|
||||
|
||||
var channelReq = new LnrpcOpenChannelRequest
|
||||
{
|
||||
Local_funding_amount = 16777215.ToString(CultureInfo.InvariantCulture),
|
||||
Node_pubkey_string = merchantInfo.NodeId
|
||||
};
|
||||
var channelResp = await CustomerLnd.OpenChannelSyncAsync(channelReq);
|
||||
}
|
||||
else
|
||||
{
|
||||
// channel exists, return it
|
||||
ExplorerNode.Generate(1);
|
||||
await WaitLNSynched();
|
||||
return channelToMerchant;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<LightningNodeInformation> WaitLNSynched()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
var merchantInfo = await InvoiceClient.GetInfo();
|
||||
var blockCount = await ExplorerNode.GetBlockCountAsync();
|
||||
if (merchantInfo.BlockHeight != blockCount)
|
||||
{
|
||||
await Task.Delay(500);
|
||||
}
|
||||
else
|
||||
{
|
||||
return merchantInfo;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
//
|
||||
private void initializeEnvironment()
|
||||
{
|
||||
NetworkProvider = new BTCPayNetworkProvider(NetworkType.Regtest);
|
||||
ExplorerNode = new RPCClient(RPCCredentialString.Parse(GetEnvironment("TESTS_BTCRPCCONNECTION", "server=http://127.0.0.1:43782;ceiwHEbqWI83:DwubwWsoo3")), NetworkProvider.GetNetwork("BTC").NBitcoinNetwork);
|
||||
}
|
||||
|
||||
public BTCPayNetworkProvider NetworkProvider { get; private set; }
|
||||
public RPCClient ExplorerNode { get; set; }
|
||||
|
||||
internal string GetEnvironment(string variable, string defaultValue)
|
||||
{
|
||||
var var = Environment.GetEnvironmentVariable(variable);
|
||||
return String.IsNullOrEmpty(var) ? defaultValue : var;
|
||||
}
|
||||
}
|
||||
}
|
18
BTCPayServer.Tests/Mocks/MockRateProvider.cs
Normal file
18
BTCPayServer.Tests/Mocks/MockRateProvider.cs
Normal file
@ -0,0 +1,18 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Services.Rates;
|
||||
|
||||
namespace BTCPayServer.Tests.Mocks
|
||||
{
|
||||
public class MockRateProvider : IRateProvider
|
||||
{
|
||||
public ExchangeRates ExchangeRates { get; set; } = new ExchangeRates();
|
||||
public Task<ExchangeRates> GetRatesAsync()
|
||||
{
|
||||
return Task.FromResult(ExchangeRates);
|
||||
}
|
||||
}
|
||||
}
|
179
BTCPayServer.Tests/RateRulesTest.cs
Normal file
179
BTCPayServer.Tests/RateRulesTest.cs
Normal file
@ -0,0 +1,179 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using BTCPayServer.Rating;
|
||||
using Xunit;
|
||||
using System.Globalization;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
public class RateRulesTest
|
||||
{
|
||||
[Fact]
|
||||
public void CanParseRateRules()
|
||||
{
|
||||
// Check happy path
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.AppendLine("// Some cool comments");
|
||||
builder.AppendLine("DOGE_X = DOGE_BTC * BTC_X * 1.1");
|
||||
builder.AppendLine("DOGE_BTC = Bittrex(DOGE_BTC)");
|
||||
builder.AppendLine("// Some other cool comments");
|
||||
builder.AppendLine("BTC_usd = GDax(BTC_USD)");
|
||||
builder.AppendLine("BTC_X = Coinbase(BTC_X);");
|
||||
builder.AppendLine("X_X = CoinAverage(X_X) * 1.02");
|
||||
|
||||
Assert.False(RateRules.TryParse("DPW*&W&#hdi&#&3JJD", out var rules));
|
||||
Assert.True(RateRules.TryParse(builder.ToString(), out rules));
|
||||
Assert.Equal(
|
||||
"// Some cool comments\n" +
|
||||
"DOGE_X = DOGE_BTC * BTC_X * 1.1;\n" +
|
||||
"DOGE_BTC = bittrex(DOGE_BTC);\n" +
|
||||
"// Some other cool comments\n" +
|
||||
"BTC_USD = gdax(BTC_USD);\n" +
|
||||
"BTC_X = coinbase(BTC_X);\n" +
|
||||
"X_X = coinaverage(X_X) * 1.02;",
|
||||
rules.ToString());
|
||||
var tests = new[]
|
||||
{
|
||||
(Pair: "DOGE_USD", Expected: "bittrex(DOGE_BTC) * gdax(BTC_USD) * 1.1"),
|
||||
(Pair: "BTC_USD", Expected: "gdax(BTC_USD)"),
|
||||
(Pair: "BTC_CAD", Expected: "coinbase(BTC_CAD)"),
|
||||
(Pair: "DOGE_CAD", Expected: "bittrex(DOGE_BTC) * coinbase(BTC_CAD) * 1.1"),
|
||||
(Pair: "LTC_CAD", Expected: "coinaverage(LTC_CAD) * 1.02"),
|
||||
};
|
||||
foreach (var test in tests)
|
||||
{
|
||||
Assert.Equal(test.Expected, rules.GetRuleFor(CurrencyPair.Parse(test.Pair)).ToString());
|
||||
}
|
||||
rules.Spread = 0.2m;
|
||||
Assert.Equal("(bittrex(DOGE_BTC) * gdax(BTC_USD) * 1.1) * (0.8, 1.2)", rules.GetRuleFor(CurrencyPair.Parse("DOGE_USD")).ToString());
|
||||
////////////////
|
||||
|
||||
// Check errors conditions
|
||||
builder = new StringBuilder();
|
||||
builder.AppendLine("DOGE_X = LTC_CAD * BTC_X * 1.1");
|
||||
builder.AppendLine("DOGE_BTC = Bittrex(DOGE_BTC)");
|
||||
builder.AppendLine("BTC_usd = GDax(BTC_USD)");
|
||||
builder.AppendLine("LTC_CHF = LTC_CHF * 1.01");
|
||||
builder.AppendLine("BTC_X = Coinbase(BTC_X)");
|
||||
Assert.True(RateRules.TryParse(builder.ToString(), out rules));
|
||||
|
||||
tests = new[]
|
||||
{
|
||||
(Pair: "LTC_CAD", Expected: "ERR_NO_RULE_MATCH(LTC_CAD)"),
|
||||
(Pair: "DOGE_USD", Expected: "ERR_NO_RULE_MATCH(LTC_CAD) * gdax(BTC_USD) * 1.1"),
|
||||
(Pair: "LTC_CHF", Expected: "ERR_TOO_MUCH_NESTED_CALLS(LTC_CHF) * 1.01"),
|
||||
};
|
||||
foreach (var test in tests)
|
||||
{
|
||||
Assert.Equal(test.Expected, rules.GetRuleFor(CurrencyPair.Parse(test.Pair)).ToString());
|
||||
}
|
||||
//////////////////
|
||||
|
||||
// Check if we can resolve exchange rates
|
||||
builder = new StringBuilder();
|
||||
builder.AppendLine("DOGE_X = DOGE_BTC * BTC_X * 1.1");
|
||||
builder.AppendLine("DOGE_BTC = Bittrex(DOGE_BTC)");
|
||||
builder.AppendLine("BTC_usd = GDax(BTC_USD)");
|
||||
builder.AppendLine("BTC_X = Coinbase(BTC_X)");
|
||||
builder.AppendLine("X_X = CoinAverage(X_X) * 1.02");
|
||||
Assert.True(RateRules.TryParse(builder.ToString(), out rules));
|
||||
|
||||
var tests2 = new[]
|
||||
{
|
||||
(Pair: "DOGE_USD", Expected: "bittrex(DOGE_BTC) * gdax(BTC_USD) * 1.1", ExpectedExchangeRates: "bittrex(DOGE_BTC),gdax(BTC_USD)"),
|
||||
(Pair: "BTC_USD", Expected: "gdax(BTC_USD)", ExpectedExchangeRates: "gdax(BTC_USD)"),
|
||||
(Pair: "BTC_CAD", Expected: "coinbase(BTC_CAD)", ExpectedExchangeRates: "coinbase(BTC_CAD)"),
|
||||
(Pair: "DOGE_CAD", Expected: "bittrex(DOGE_BTC) * coinbase(BTC_CAD) * 1.1", ExpectedExchangeRates: "bittrex(DOGE_BTC),coinbase(BTC_CAD)"),
|
||||
(Pair: "LTC_CAD", Expected: "coinaverage(LTC_CAD) * 1.02", ExpectedExchangeRates: "coinaverage(LTC_CAD)"),
|
||||
};
|
||||
foreach (var test in tests2)
|
||||
{
|
||||
var rule = rules.GetRuleFor(CurrencyPair.Parse(test.Pair));
|
||||
Assert.Equal(test.Expected, rule.ToString());
|
||||
Assert.Equal(test.ExpectedExchangeRates, string.Join(',', rule.ExchangeRates.OfType<object>().ToArray()));
|
||||
}
|
||||
var rule2 = rules.GetRuleFor(CurrencyPair.Parse("DOGE_CAD"));
|
||||
rule2.ExchangeRates.SetRate("bittrex", CurrencyPair.Parse("DOGE_BTC"), new BidAsk(5000m));
|
||||
rule2.Reevaluate();
|
||||
Assert.True(rule2.HasError);
|
||||
Assert.Equal("5000 * ERR_RATE_UNAVAILABLE(coinbase, BTC_CAD) * 1.1", rule2.ToString(true));
|
||||
Assert.Equal("bittrex(DOGE_BTC) * coinbase(BTC_CAD) * 1.1", rule2.ToString(false));
|
||||
rule2.ExchangeRates.SetRate("coinbase", CurrencyPair.Parse("BTC_CAD"), new BidAsk(2000.4m));
|
||||
rule2.Reevaluate();
|
||||
Assert.False(rule2.HasError);
|
||||
Assert.Equal("5000 * 2000.4 * 1.1", rule2.ToString(true));
|
||||
Assert.Equal(rule2.BidAsk.Bid, 5000m * 2000.4m * 1.1m);
|
||||
////////
|
||||
|
||||
// Make sure parenthesis are correctly calculated
|
||||
builder = new StringBuilder();
|
||||
builder.AppendLine("DOGE_X = DOGE_BTC * BTC_X");
|
||||
builder.AppendLine("BTC_USD = -3 + coinbase(BTC_CAD) + 50 - 5");
|
||||
builder.AppendLine("DOGE_BTC = 2000");
|
||||
Assert.True(RateRules.TryParse(builder.ToString(), out rules));
|
||||
rules.Spread = 0.1m;
|
||||
|
||||
rule2 = rules.GetRuleFor(CurrencyPair.Parse("DOGE_USD"));
|
||||
Assert.Equal("(2000 * (-3 + coinbase(BTC_CAD) + 50 - 5)) * (0.9, 1.1)", rule2.ToString());
|
||||
rule2.ExchangeRates.SetRate("coinbase", CurrencyPair.Parse("BTC_CAD"), new BidAsk(1000m));
|
||||
Assert.True(rule2.Reevaluate());
|
||||
Assert.Equal("(2000 * (-3 + 1000 + 50 - 5)) * (0.9, 1.1)", rule2.ToString(true));
|
||||
Assert.Equal((2000m * (-3m + 1000m + 50m - 5m)) * 0.9m, rule2.BidAsk.Bid);
|
||||
|
||||
// Test inverse
|
||||
rule2 = rules.GetRuleFor(CurrencyPair.Parse("USD_DOGE"));
|
||||
Assert.Equal("(1 / (2000 * (-3 + coinbase(BTC_CAD) + 50 - 5))) * (0.9, 1.1)", rule2.ToString());
|
||||
rule2.ExchangeRates.SetRate("coinbase", CurrencyPair.Parse("BTC_CAD"), new BidAsk(1000m));
|
||||
Assert.True(rule2.Reevaluate());
|
||||
Assert.Equal("(1 / (2000 * (-3 + 1000 + 50 - 5))) * (0.9, 1.1)", rule2.ToString(true));
|
||||
Assert.Equal((1.0m / (2000m * (-3m + 1000m + 50m - 5m))) * 0.9m, rule2.BidAsk.Bid);
|
||||
////////
|
||||
|
||||
// Make sure kraken is not converted to CurrencyPair
|
||||
builder = new StringBuilder();
|
||||
builder.AppendLine("BTC_USD = kraken(BTC_USD)");
|
||||
Assert.True(RateRules.TryParse(builder.ToString(), out rules));
|
||||
rule2 = rules.GetRuleFor(CurrencyPair.Parse("BTC_USD"));
|
||||
rule2.ExchangeRates.SetRate("kraken", CurrencyPair.Parse("BTC_USD"), new BidAsk(1000m));
|
||||
Assert.True(rule2.Reevaluate());
|
||||
|
||||
// Make sure can handle pairs
|
||||
builder = new StringBuilder();
|
||||
builder.AppendLine("BTC_USD = kraken(BTC_USD)");
|
||||
Assert.True(RateRules.TryParse(builder.ToString(), out rules));
|
||||
rule2 = rules.GetRuleFor(CurrencyPair.Parse("BTC_USD"));
|
||||
rule2.ExchangeRates.SetRate("kraken", CurrencyPair.Parse("BTC_USD"), new BidAsk(6000m, 6100m));
|
||||
Assert.True(rule2.Reevaluate());
|
||||
Assert.Equal("(6000, 6100)", rule2.ToString(true));
|
||||
Assert.Equal(6000m, rule2.BidAsk.Bid);
|
||||
rule2 = rules.GetRuleFor(CurrencyPair.Parse("USD_BTC"));
|
||||
rule2.ExchangeRates.SetRate("kraken", CurrencyPair.Parse("BTC_USD"), new BidAsk(6000m, 6100m));
|
||||
Assert.True(rule2.Reevaluate());
|
||||
Assert.Equal("1 / (6000, 6100)", rule2.ToString(true));
|
||||
Assert.Equal(1m / 6100m, rule2.BidAsk.Bid);
|
||||
|
||||
// Make sure the inverse has more priority than X_X or CDNT_X
|
||||
builder = new StringBuilder();
|
||||
builder.AppendLine("EUR_CDNT = 10");
|
||||
builder.AppendLine("CDNT_BTC = CDNT_EUR * EUR_BTC;");
|
||||
builder.AppendLine("CDNT_X = CDNT_BTC * BTC_X;");
|
||||
builder.AppendLine("X_X = coinaverage(X_X);");
|
||||
Assert.True(RateRules.TryParse(builder.ToString(), out rules));
|
||||
rule2 = rules.GetRuleFor(CurrencyPair.Parse("CDNT_EUR"));
|
||||
rule2.ExchangeRates.SetRate("coinaverage", CurrencyPair.Parse("BTC_USD"), new BidAsk(6000m, 6100m));
|
||||
Assert.True(rule2.Reevaluate());
|
||||
Assert.Equal("1 / 10", rule2.ToString(false));
|
||||
|
||||
// Make sure an inverse can be solved on an exchange
|
||||
builder = new StringBuilder();
|
||||
builder.AppendLine("X_X = coinaverage(X_X);");
|
||||
Assert.True(RateRules.TryParse(builder.ToString(), out rules));
|
||||
rule2 = rules.GetRuleFor(CurrencyPair.Parse("USD_BTC"));
|
||||
rule2.ExchangeRates.SetRate("coinaverage", CurrencyPair.Parse("BTC_USD"), new BidAsk(6000m, 6100m));
|
||||
Assert.True(rule2.Reevaluate());
|
||||
Assert.Equal($"({(1m / 6100m).ToString(CultureInfo.InvariantCulture)}, {(1m / 6000m).ToString(CultureInfo.InvariantCulture)})", rule2.ToString(true));
|
||||
}
|
||||
}
|
||||
}
|
@ -20,6 +20,8 @@ using System.Threading;
|
||||
using System.Globalization;
|
||||
using BTCPayServer.Payments.Lightning.CLightning;
|
||||
using BTCPayServer.Payments.Lightning.Charge;
|
||||
using BTCPayServer.Tests.Lnd;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
@ -47,10 +49,12 @@ namespace BTCPayServer.Tests
|
||||
LTCExplorerClient = new ExplorerClient(NetworkProvider.GetNetwork("LTC").NBXplorerNetwork, new Uri(GetEnvironment("TESTS_LTCNBXPLORERURL", "http://127.0.0.1:32838/")));
|
||||
|
||||
var btc = NetworkProvider.GetNetwork("BTC").NBitcoinNetwork;
|
||||
CustomerLightningD = new CLightningRPCClient(new Uri(GetEnvironment("TEST_CUSTOMERLIGHTNINGD", "tcp://127.0.0.1:30992/")), btc);
|
||||
MerchantLightningD = new CLightningRPCClient(new Uri(GetEnvironment("TEST_MERCHANTLIGHTNINGD", "tcp://127.0.0.1:30993/")), btc);
|
||||
CustomerLightningD = (CLightningRPCClient)LightningClientFactory.CreateClient(GetEnvironment("TEST_CUSTOMERLIGHTNINGD", "type=clightning;server=tcp://127.0.0.1:30992/"), btc);
|
||||
MerchantLightningD = (CLightningRPCClient)LightningClientFactory.CreateClient(GetEnvironment("TEST_MERCHANTLIGHTNINGD", "type=clightning;server=tcp://127.0.0.1:30993/"), btc);
|
||||
|
||||
MerchantCharge = new ChargeTester(this, "TEST_MERCHANTCHARGE", "http://api-token:foiewnccewuify@127.0.0.1:54938/", "merchant_lightningd", btc);
|
||||
MerchantCharge = new ChargeTester(this, "TEST_MERCHANTCHARGE", "type=charge;server=http://127.0.0.1:54938/;api-token=foiewnccewuify", "merchant_lightningd", btc);
|
||||
|
||||
MerchantLnd = new LndMockTester(this, "TEST_MERCHANTLND", "https://lnd:lnd@127.0.0.1:53280/", "merchant_lnd", btc);
|
||||
|
||||
PayTester = new BTCPayServerTester(Path.Combine(_Directory, "pay"))
|
||||
{
|
||||
@ -78,41 +82,54 @@ namespace BTCPayServer.Tests
|
||||
/// <summary>
|
||||
/// Connect a customer LN node to the merchant LN node
|
||||
/// </summary>
|
||||
public void PrepareLightning()
|
||||
public void PrepareLightning(LightningConnectionType lndBackend)
|
||||
{
|
||||
PrepareLightningAsync().GetAwaiter().GetResult();
|
||||
ILightningInvoiceClient client = MerchantCharge.Client;
|
||||
if (lndBackend == LightningConnectionType.LndREST)
|
||||
client = MerchantLnd.Client;
|
||||
|
||||
PrepareLightningAsync(client).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
|
||||
private static readonly string[] SKIPPED_STATES =
|
||||
{ "ONCHAIN", "CHANNELD_SHUTTING_DOWN", "CLOSINGD_SIGEXCHANGE", "CLOSINGD_COMPLETE", "FUNDING_SPEND_SEEN" };
|
||||
|
||||
/// <summary>
|
||||
/// Connect a customer LN node to the merchant LN node
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public async Task PrepareLightningAsync()
|
||||
private async Task PrepareLightningAsync(ILightningInvoiceClient client)
|
||||
{
|
||||
bool awaitingLocking = false;
|
||||
while (true)
|
||||
{
|
||||
var skippedStates = new[] { "ONCHAIN", "CHANNELD_SHUTTING_DOWN", "CLOSINGD_SIGEXCHANGE", "CLOSINGD_COMPLETE", "FUNDING_SPEND_SEEN" };
|
||||
var channel = (await CustomerLightningD.ListPeersAsync())
|
||||
var merchantInfo = await WaitLNSynched(client, CustomerLightningD, MerchantLightningD);
|
||||
|
||||
var peers = await CustomerLightningD.ListPeersAsync();
|
||||
var filteringToTargetedPeers = peers.Where(a => a.Id == merchantInfo.NodeId);
|
||||
var channel = filteringToTargetedPeers
|
||||
.SelectMany(p => p.Channels)
|
||||
.Where(c => !skippedStates.Contains(c.State ?? ""))
|
||||
.Where(c => !SKIPPED_STATES.Contains(c.State ?? ""))
|
||||
.FirstOrDefault();
|
||||
|
||||
switch (channel?.State)
|
||||
{
|
||||
case null:
|
||||
var merchantInfo = await WaitLNSynched();
|
||||
var clightning = new NodeInfo(merchantInfo.Id, MerchantCharge.P2PHost, merchantInfo.Port);
|
||||
await CustomerLightningD.ConnectAsync(clightning);
|
||||
var address = await CustomerLightningD.NewAddressAsync();
|
||||
await ExplorerNode.SendToAddressAsync(address, Money.Coins(0.2m));
|
||||
await ExplorerNode.SendToAddressAsync(address, Money.Coins(0.5m));
|
||||
ExplorerNode.Generate(1);
|
||||
await WaitLNSynched();
|
||||
await WaitLNSynched(client, CustomerLightningD, MerchantLightningD);
|
||||
await Task.Delay(1000);
|
||||
await CustomerLightningD.FundChannelAsync(clightning, Money.Satoshis(16777215));
|
||||
|
||||
var merchantNodeInfo = new NodeInfo(merchantInfo.NodeId, merchantInfo.Address, merchantInfo.P2PPort);
|
||||
await CustomerLightningD.ConnectAsync(merchantNodeInfo);
|
||||
await CustomerLightningD.FundChannelAsync(merchantNodeInfo, Money.Satoshis(16777215));
|
||||
break;
|
||||
case "CHANNELD_AWAITING_LOCKIN":
|
||||
ExplorerNode.Generate(1);
|
||||
await WaitLNSynched();
|
||||
ExplorerNode.Generate(awaitingLocking ? 1 : 10);
|
||||
await WaitLNSynched(client, CustomerLightningD, MerchantLightningD);
|
||||
awaitingLocking = true;
|
||||
break;
|
||||
case "CHANNELD_NORMAL":
|
||||
return;
|
||||
@ -122,23 +139,29 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<GetInfoResponse> WaitLNSynched()
|
||||
private async Task<LightningNodeInformation> WaitLNSynched(params ILightningInvoiceClient[] clients)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
var merchantInfo = await MerchantCharge.Client.GetInfoAsync();
|
||||
var blockCount = await ExplorerNode.GetBlockCountAsync();
|
||||
if (merchantInfo.BlockHeight != blockCount)
|
||||
{
|
||||
await Task.Delay(1000);
|
||||
}
|
||||
else
|
||||
{
|
||||
return merchantInfo;
|
||||
}
|
||||
var synching = clients.Select(c => WaitLNSynchedCore(blockCount, c)).ToArray();
|
||||
await Task.WhenAll(synching);
|
||||
if (synching.All(c => c.Result != null))
|
||||
return synching[0].Result;
|
||||
await Task.Delay(1000);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<LightningNodeInformation> WaitLNSynchedCore(int blockCount, ILightningInvoiceClient client)
|
||||
{
|
||||
var merchantInfo = await client.GetInfo();
|
||||
if (merchantInfo.BlockHeight == blockCount)
|
||||
{
|
||||
return merchantInfo;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void SendLightningPayment(Invoice invoice)
|
||||
{
|
||||
SendLightningPaymentAsync(invoice).GetAwaiter().GetResult();
|
||||
@ -152,8 +175,10 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
|
||||
public CLightningRPCClient CustomerLightningD { get; set; }
|
||||
|
||||
public CLightningRPCClient MerchantLightningD { get; private set; }
|
||||
public ChargeTester MerchantCharge { get; private set; }
|
||||
public LndMockTester MerchantLnd { get; set; }
|
||||
|
||||
internal string GetEnvironment(string variable, string defaultValue)
|
||||
{
|
||||
@ -185,106 +210,18 @@ namespace BTCPayServer.Tests
|
||||
|
||||
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 List<string> Stores { get; internal set; } = new List<string>();
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
foreach(var store in Stores)
|
||||
{
|
||||
Xunit.Assert.True(PayTester.StoreRepository.DeleteStore(store).GetAwaiter().GetResult());
|
||||
}
|
||||
if (PayTester != null)
|
||||
PayTester.Dispose();
|
||||
}
|
||||
|
@ -44,29 +44,28 @@ namespace BTCPayServer.Tests
|
||||
public async Task GrantAccessAsync()
|
||||
{
|
||||
await RegisterAsync();
|
||||
var store = await CreateStoreAsync();
|
||||
await CreateStoreAsync();
|
||||
var store = this.GetController<StoresController>();
|
||||
var pairingCode = BitPay.RequestClientAuthorization("test", Facade.Merchant);
|
||||
Assert.IsType<ViewResult>(await store.RequestPairing(pairingCode.ToString()));
|
||||
await store.Pair(pairingCode.ToString(), StoreId);
|
||||
}
|
||||
public StoresController CreateStore()
|
||||
public void CreateStore()
|
||||
{
|
||||
return CreateStoreAsync().GetAwaiter().GetResult();
|
||||
CreateStoreAsync().GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public T GetController<T>() where T : Controller
|
||||
public T GetController<T>(bool setImplicitStore = true) where T : Controller
|
||||
{
|
||||
return parent.PayTester.GetController<T>(UserId);
|
||||
return parent.PayTester.GetController<T>(UserId, setImplicitStore ? StoreId : null);
|
||||
}
|
||||
|
||||
public async Task<StoresController> CreateStoreAsync()
|
||||
public async Task CreateStoreAsync()
|
||||
{
|
||||
var store = parent.PayTester.GetController<UserStoresController>(UserId);
|
||||
var store = this.GetController<UserStoresController>();
|
||||
await store.CreateStore(new CreateStoreViewModel() { Name = "Test Store" });
|
||||
StoreId = store.CreatedStoreId;
|
||||
var store2 = parent.PayTester.GetController<StoresController>(UserId);
|
||||
store2.CreatedStoreId = store.CreatedStoreId;
|
||||
return store2;
|
||||
parent.Stores.Add(StoreId);
|
||||
}
|
||||
|
||||
public BTCPayNetwork SupportedNetwork { get; set; }
|
||||
@ -78,12 +77,12 @@ namespace BTCPayServer.Tests
|
||||
public async Task RegisterDerivationSchemeAsync(string cryptoCode)
|
||||
{
|
||||
SupportedNetwork = parent.NetworkProvider.GetNetwork(cryptoCode);
|
||||
var store = parent.PayTester.GetController<StoresController>(UserId);
|
||||
var store = parent.PayTester.GetController<StoresController>(UserId, StoreId);
|
||||
ExtKey = new ExtKey().GetWif(SupportedNetwork.NBitcoinNetwork);
|
||||
DerivationScheme = new DerivationStrategyFactory(SupportedNetwork.NBitcoinNetwork).Parse(ExtKey.Neuter().ToString() + "-[legacy]");
|
||||
var vm = (StoreViewModel)((ViewResult)await store.UpdateStore(StoreId)).Model;
|
||||
var vm = (StoreViewModel)((ViewResult)store.UpdateStore()).Model;
|
||||
vm.SpeedPolicy = SpeedPolicy.MediumSpeed;
|
||||
await store.UpdateStore(StoreId, vm);
|
||||
await store.UpdateStore(vm);
|
||||
|
||||
await store.AddDerivationScheme(StoreId, new DerivationSchemeViewModel()
|
||||
{
|
||||
@ -119,7 +118,7 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
|
||||
public void RegisterLightningNode(string cryptoCode, LightningConnectionType connectionType)
|
||||
{
|
||||
RegisterLightningNodeAsync(cryptoCode, connectionType).GetAwaiter().GetResult();
|
||||
@ -127,16 +126,25 @@ namespace BTCPayServer.Tests
|
||||
|
||||
public async Task RegisterLightningNodeAsync(string cryptoCode, LightningConnectionType connectionType)
|
||||
{
|
||||
var storeController = parent.PayTester.GetController<StoresController>(UserId);
|
||||
var storeController = this.GetController<StoresController>();
|
||||
|
||||
string connectionString = null;
|
||||
if (connectionType == LightningConnectionType.Charge)
|
||||
connectionString = "type=charge;server=" + parent.MerchantCharge.Client.Uri.AbsoluteUri;
|
||||
else if (connectionType == LightningConnectionType.CLightning)
|
||||
connectionString = "type=clightning;server=" + parent.MerchantLightningD.Address.AbsoluteUri;
|
||||
else if (connectionType == LightningConnectionType.LndREST)
|
||||
connectionString = $"type=lnd-rest;server={parent.MerchantLnd.Swagger.BaseUrl};allowinsecure=true";
|
||||
else
|
||||
throw new NotSupportedException(connectionType.ToString());
|
||||
|
||||
await storeController.AddLightningNode(StoreId, new LightningNodeViewModel()
|
||||
{
|
||||
Url = connectionType == LightningConnectionType.Charge ? parent.MerchantCharge.Client.Uri.AbsoluteUri :
|
||||
connectionType == LightningConnectionType.CLightning ? parent.MerchantLightningD.Address.AbsoluteUri
|
||||
: throw new NotSupportedException(connectionType.ToString()),
|
||||
ConnectionString = connectionString,
|
||||
SkipPortTest = true
|
||||
}, "save", "BTC");
|
||||
if (storeController.ModelState.ErrorCount != 0)
|
||||
Assert.False(true, storeController.ModelState.FirstOrDefault().Value.Errors[0].ErrorMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,9 +1,5 @@
|
||||
using NBitcoin;
|
||||
using NBitcoin.DataEncoders;
|
||||
using NBitpayClient;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System;
|
||||
using NBitcoin;
|
||||
using Xunit;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
|
@ -17,9 +17,10 @@ services:
|
||||
TESTS_POSTGRES: User ID=postgres;Host=postgres;Port=5432;Database=btcpayserver
|
||||
TESTS_PORT: 80
|
||||
TESTS_HOSTNAME: tests
|
||||
TEST_MERCHANTLIGHTNINGD: "/etc/merchant_lightningd_datadir/lightning-rpc"
|
||||
TEST_CUSTOMERLIGHTNINGD: "/etc/customer_lightningd_datadir/lightning-rpc"
|
||||
TEST_MERCHANTCHARGE: http://api-token:foiewnccewuify@lightning-charged:9112/
|
||||
TEST_MERCHANTLIGHTNINGD: "type=clightning;server=/etc/merchant_lightningd_datadir/lightning-rpc"
|
||||
TEST_CUSTOMERLIGHTNINGD: "type=clightning;server=/etc/customer_lightningd_datadir/lightning-rpc"
|
||||
TEST_MERCHANTCHARGE: "type=charge;server=https://lightning-charged:9112/;api-token=foiewnccewuify;allowinsecure=true"
|
||||
TEST_MERCHANTLND: "type=lnd-rest;server=https://lnd:lnd@127.0.0.1:53280/;allowinsecure=true"
|
||||
TESTS_INCONTAINER: "true"
|
||||
expose:
|
||||
- "80"
|
||||
@ -44,9 +45,25 @@ services:
|
||||
- customer_lightningd
|
||||
- merchant_lightningd
|
||||
- lightning-charged
|
||||
- customer_lnd
|
||||
- merchant_lnd
|
||||
|
||||
devlnd:
|
||||
image: nicolasdorier/docker-bitcoin:0.16.0
|
||||
environment:
|
||||
BITCOIN_EXTRA_ARGS: |
|
||||
regtest=1
|
||||
connect=bitcoind:39388
|
||||
links:
|
||||
- nbxplorer
|
||||
- postgres
|
||||
- customer_lnd
|
||||
- merchant_lnd
|
||||
|
||||
|
||||
|
||||
nbxplorer:
|
||||
image: nicolasdorier/nbxplorer:1.0.2.0
|
||||
image: nicolasdorier/nbxplorer:1.0.2.14
|
||||
ports:
|
||||
- "32838:32838"
|
||||
expose:
|
||||
@ -80,8 +97,13 @@ services:
|
||||
rpcport=43782
|
||||
port=39388
|
||||
whitelist=0.0.0.0/0
|
||||
zmqpubrawtx=tcp://0.0.0.0:28332
|
||||
zmqpubrawblock=tcp://0.0.0.0:28332
|
||||
zmqpubrawtxlock=tcp://0.0.0.0:28332
|
||||
zmqpubhashblock=tcp://0.0.0.0:28332
|
||||
ports:
|
||||
- "43782:43782"
|
||||
- "28332:28332"
|
||||
expose:
|
||||
- "43782" # RPC
|
||||
- "39388" # P2P
|
||||
@ -89,15 +111,17 @@ services:
|
||||
- "bitcoin_datadir:/data"
|
||||
|
||||
customer_lightningd:
|
||||
image: nicolasdorier/clightning:0.0.0.3
|
||||
image: nicolasdorier/clightning:v0.6-dev
|
||||
environment:
|
||||
EXPOSE_TCP: "true"
|
||||
LIGHTNINGD_OPT: |
|
||||
bitcoin-datadir=/etc/bitcoin
|
||||
bitcoin-rpcconnect=bitcoind
|
||||
network=regtest
|
||||
ipaddr=customer_lightningd
|
||||
bind-addr=0.0.0.0
|
||||
announce-addr=customer_lightningd
|
||||
log-level=debug
|
||||
dev-broadcast-interval=1000
|
||||
ports:
|
||||
- "30992:9835" # api port
|
||||
expose:
|
||||
@ -110,7 +134,7 @@ services:
|
||||
- bitcoind
|
||||
|
||||
lightning-charged:
|
||||
image: shesek/lightning-charge:0.3.9
|
||||
image: shesek/lightning-charge:0.3.15
|
||||
environment:
|
||||
NETWORK: regtest
|
||||
API_TOKEN: foiewnccewuify
|
||||
@ -129,13 +153,14 @@ services:
|
||||
- merchant_lightningd
|
||||
|
||||
merchant_lightningd:
|
||||
image: nicolasdorier/clightning:0.0.0.5-dev
|
||||
image: nicolasdorier/clightning:v0.6-dev
|
||||
environment:
|
||||
EXPOSE_TCP: "true"
|
||||
LIGHTNINGD_OPT: |
|
||||
bitcoin-datadir=/etc/bitcoin
|
||||
bitcoin-rpcconnect=bitcoind
|
||||
ipaddr=merchant_lightningd
|
||||
bind-addr=0.0.0.0
|
||||
announce-addr=merchant_lightningd
|
||||
network=regtest
|
||||
log-level=debug
|
||||
dev-broadcast-interval=1000
|
||||
@ -174,8 +199,59 @@ services:
|
||||
expose:
|
||||
- "5432"
|
||||
|
||||
merchant_lnd:
|
||||
image: btcpayserver/lnd:0.4.2.0
|
||||
environment:
|
||||
LND_CHAIN: "btc"
|
||||
LND_ENVIRONMENT: "regtest"
|
||||
LND_EXTRA_ARGS: |
|
||||
restlisten=0.0.0.0:8080
|
||||
bitcoin.node=bitcoind
|
||||
bitcoind.rpchost=bitcoind:43782
|
||||
bitcoind.zmqpath=tcp://bitcoind:28332
|
||||
externalip=merchant_lnd:9735
|
||||
no-macaroons=1
|
||||
debuglevel=debug
|
||||
noencryptwallet=1
|
||||
ports:
|
||||
- "53280:8080"
|
||||
expose:
|
||||
- "9735"
|
||||
volumes:
|
||||
- "merchant_lnd_datadir:/data"
|
||||
- "bitcoin_datadir:/deps/.bitcoin"
|
||||
links:
|
||||
- bitcoind
|
||||
|
||||
customer_lnd:
|
||||
image: btcpayserver/lnd:0.4.2.0
|
||||
environment:
|
||||
LND_CHAIN: "btc"
|
||||
LND_ENVIRONMENT: "regtest"
|
||||
LND_EXTRA_ARGS: |
|
||||
restlisten=0.0.0.0:8080
|
||||
bitcoin.node=bitcoind
|
||||
bitcoind.rpchost=bitcoind:43782
|
||||
bitcoind.zmqpath=tcp://bitcoind:28332
|
||||
externalip=customer_lnd:10009
|
||||
no-macaroons=1
|
||||
debuglevel=debug
|
||||
noencryptwallet=1
|
||||
ports:
|
||||
- "53281:8080"
|
||||
expose:
|
||||
- "8080"
|
||||
- "10009"
|
||||
volumes:
|
||||
- "customer_lnd_datadir:/root/.lnd"
|
||||
- "bitcoin_datadir:/deps/.bitcoin"
|
||||
links:
|
||||
- bitcoind
|
||||
|
||||
volumes:
|
||||
bitcoin_datadir:
|
||||
customer_lightningd_datadir:
|
||||
merchant_lightningd_datadir:
|
||||
lightning_charge_datadir:
|
||||
customer_lnd_datadir:
|
||||
merchant_lnd_datadir:
|
||||
|
@ -1,35 +0,0 @@
|
||||
using NBitcoin;
|
||||
using NBitcoin.DataEncoders;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Security.Principal;
|
||||
using System.Text;
|
||||
|
||||
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 string SIN
|
||||
{
|
||||
get;
|
||||
}
|
||||
public PubKey PubKey
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
public string AuthenticationType => "BitID";
|
||||
|
||||
public bool IsAuthenticated => true;
|
||||
|
||||
public string Name => _Name;
|
||||
}
|
||||
}
|
@ -33,6 +33,8 @@ namespace BTCPayServer.Authentication
|
||||
|
||||
public async Task<BitTokenEntity[]> GetTokens(string sin)
|
||||
{
|
||||
if (sin == null)
|
||||
return Array.Empty<BitTokenEntity>();
|
||||
using (var ctx = _Factory.CreateContext())
|
||||
{
|
||||
return (await ctx.PairedSINData
|
||||
@ -43,6 +45,46 @@ namespace BTCPayServer.Authentication
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<String> GetStoreIdFromAPIKey(string apiKey)
|
||||
{
|
||||
using (var ctx = _Factory.CreateContext())
|
||||
{
|
||||
return await ctx.ApiKeys.Where(o => o.Id == apiKey).Select(o => o.StoreId).FirstOrDefaultAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task GenerateLegacyAPIKey(string storeId)
|
||||
{
|
||||
// It is legacy support and Bitpay generate string of unknown format, trying to replicate them
|
||||
// as good as possible. The string below got generated for me.
|
||||
var chars = "ERo0vkBMOYhyU0ZHvirCplbLDIGWPdi1ok77VnW7QdE";
|
||||
var rand = new Random(Math.Abs(RandomUtils.GetInt32()));
|
||||
var generated = new char[chars.Length];
|
||||
for (int i = 0; i < generated.Length; i++)
|
||||
{
|
||||
generated[i] = chars[rand.Next(0, generated.Length)];
|
||||
}
|
||||
|
||||
using (var ctx = _Factory.CreateContext())
|
||||
{
|
||||
var existing = await ctx.ApiKeys.Where(o => o.StoreId == storeId).FirstOrDefaultAsync();
|
||||
if (existing != null)
|
||||
{
|
||||
ctx.ApiKeys.Remove(existing);
|
||||
}
|
||||
ctx.ApiKeys.Add(new APIKeyData() { Id = new string(generated), StoreId = storeId });
|
||||
await ctx.SaveChangesAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<string[]> GetLegacyAPIKeys(string storeId)
|
||||
{
|
||||
using (var ctx = _Factory.CreateContext())
|
||||
{
|
||||
return await ctx.ApiKeys.Where(o => o.StoreId == storeId).Select(c => c.Id).ToArrayAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private BitTokenEntity CreateTokenEntity(PairedSINData data)
|
||||
{
|
||||
return new BitTokenEntity()
|
||||
|
@ -44,7 +44,8 @@ namespace BTCPayServer
|
||||
public string CryptoCode { get; internal set; }
|
||||
public string BlockExplorerLink { get; internal set; }
|
||||
public string UriScheme { get; internal set; }
|
||||
public RateProviderDescription DefaultRateProvider { get; set; }
|
||||
public Money MinFee { get; internal set; }
|
||||
public string DisplayName { get; set; }
|
||||
|
||||
[Obsolete("Should not be needed")]
|
||||
public bool IsBTC
|
||||
@ -62,6 +63,7 @@ namespace BTCPayServer
|
||||
public BTCPayDefaultSettings DefaultSettings { get; set; }
|
||||
public KeyPath CoinType { get; internal set; }
|
||||
public int MaxTrackedConfirmation { get; internal set; } = 6;
|
||||
public string[] DefaultRateRules { get; internal set; } = Array.Empty<string>();
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
|
@ -14,19 +14,16 @@ namespace BTCPayServer
|
||||
public void InitBitcoin()
|
||||
{
|
||||
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("BTC");
|
||||
var coinaverage = new CoinAverageRateProviderDescription("BTC");
|
||||
var bitpay = new BitpayRateProviderDescription();
|
||||
var btcRate = new FallbackRateProviderDescription(new RateProviderDescription[] { coinaverage, bitpay });
|
||||
Add(new BTCPayNetwork()
|
||||
{
|
||||
CryptoCode = nbxplorerNetwork.CryptoCode,
|
||||
DisplayName = "Bitcoin",
|
||||
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "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",
|
||||
LightningImagePath = "imlegacy/btc-lightning.svg",
|
||||
CryptoImagePath = "imlegacy/bitcoin.svg",
|
||||
LightningImagePath = "imlegacy/bitcoin-lightning.svg",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("0'") : new KeyPath("1'")
|
||||
});
|
||||
|
30
BTCPayServer/BTCPayNetworkProvider.BitcoinGold.cs
Normal file
30
BTCPayServer/BTCPayNetworkProvider.BitcoinGold.cs
Normal file
@ -0,0 +1,30 @@
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public partial class BTCPayNetworkProvider
|
||||
{
|
||||
public void InitBitcoinGold()
|
||||
{
|
||||
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("BTG");
|
||||
Add(new BTCPayNetwork()
|
||||
{
|
||||
CryptoCode = nbxplorerNetwork.CryptoCode,
|
||||
DisplayName = "BGold",
|
||||
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://explorer.bitcoingold.org/insight/tx/{0}/" : "https://test-explorer.bitcoingold.org/insight/tx/{0}",
|
||||
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
|
||||
NBXplorerNetwork = nbxplorerNetwork,
|
||||
UriScheme = "bitcoingold",
|
||||
DefaultRateRules = new[]
|
||||
{
|
||||
"BTG_X = BTG_BTC * BTC_X",
|
||||
"BTG_BTC = bitfinex(BTG_BTC)",
|
||||
},
|
||||
CryptoImagePath = "imlegacy/btg.svg",
|
||||
LightningImagePath = "imlegacy/btg-lightning.svg",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("156'") : new KeyPath("1'")
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
35
BTCPayServer/BTCPayNetworkProvider.Dash.cs
Normal file
35
BTCPayServer/BTCPayNetworkProvider.Dash.cs
Normal file
@ -0,0 +1,35 @@
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public partial class BTCPayNetworkProvider
|
||||
{
|
||||
public void InitDash()
|
||||
{
|
||||
//not needed: NBitcoin.Altcoins.Dash.Instance.EnsureRegistered();
|
||||
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("DASH");
|
||||
Add(new BTCPayNetwork()
|
||||
{
|
||||
CryptoCode = nbxplorerNetwork.CryptoCode,
|
||||
DisplayName = "Dash",
|
||||
BlockExplorerLink = NetworkType == NetworkType.Mainnet
|
||||
? "https://insight.dash.org/insight/tx/{0}"
|
||||
: "https://testnet-insight.dashevo.org/insight/tx/{0}",
|
||||
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
|
||||
NBXplorerNetwork = nbxplorerNetwork,
|
||||
UriScheme = "dash",
|
||||
DefaultRateRules = new[]
|
||||
{
|
||||
"DASH_X = DASH_BTC * BTC_X",
|
||||
"DASH_BTC = bittrex(DASH_BTC)"
|
||||
},
|
||||
CryptoImagePath = "imlegacy/dash.png",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||
//https://github.com/satoshilabs/slips/blob/master/slip-0044.md
|
||||
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("5'")
|
||||
: new KeyPath("1'"),
|
||||
MinFee = Money.Satoshis(1m)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -16,14 +16,20 @@ namespace BTCPayServer
|
||||
Add(new BTCPayNetwork()
|
||||
{
|
||||
CryptoCode = nbxplorerNetwork.CryptoCode,
|
||||
DisplayName = "Dogecoin",
|
||||
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://dogechain.info/tx/{0}" : "https://dogechain.info/tx/{0}",
|
||||
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
|
||||
NBXplorerNetwork = nbxplorerNetwork,
|
||||
UriScheme = "dogecoin",
|
||||
DefaultRateProvider = new CoinAverageRateProviderDescription("DOGE"),
|
||||
DefaultRateRules = new[]
|
||||
{
|
||||
"DOGE_X = DOGE_BTC * BTC_X",
|
||||
"DOGE_BTC = bittrex(DOGE_BTC)"
|
||||
},
|
||||
CryptoImagePath = "imlegacy/dogecoin.png",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("3'") : new KeyPath("1'")
|
||||
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("3'") : new KeyPath("1'"),
|
||||
MinFee = Money.Coins(1m)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
35
BTCPayServer/BTCPayNetworkProvider.Feathercoin.cs
Normal file
35
BTCPayServer/BTCPayNetworkProvider.Feathercoin.cs
Normal file
@ -0,0 +1,35 @@
|
||||
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 InitFeathercoin()
|
||||
{
|
||||
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("FTC");
|
||||
Add(new BTCPayNetwork()
|
||||
{
|
||||
CryptoCode = nbxplorerNetwork.CryptoCode,
|
||||
DisplayName = "Feathercoin",
|
||||
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://explorer.feathercoin.com/tx/{0}" : "https://explorer.feathercoin.com/tx/{0}",
|
||||
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
|
||||
NBXplorerNetwork = nbxplorerNetwork,
|
||||
UriScheme = "feathercoin",
|
||||
DefaultRateRules = new[]
|
||||
{
|
||||
"FTC_X = FTC_BTC * BTC_X",
|
||||
"FTC_BTC = bittrex(FTC_BTC)"
|
||||
},
|
||||
CryptoImagePath = "imlegacy/feathercoin.png",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("8'") : new KeyPath("1'")
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
34
BTCPayServer/BTCPayNetworkProvider.Groestlcoin.cs
Normal file
34
BTCPayServer/BTCPayNetworkProvider.Groestlcoin.cs
Normal file
@ -0,0 +1,34 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public partial class BTCPayNetworkProvider
|
||||
{
|
||||
public void InitGroestlcoin()
|
||||
{
|
||||
|
||||
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("GRS");
|
||||
Add(new BTCPayNetwork()
|
||||
{
|
||||
CryptoCode = nbxplorerNetwork.CryptoCode,
|
||||
DisplayName = "Groestlcoin",
|
||||
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://chainz.cryptoid.info/grs/tx.dws?{0}.htm" : "https://chainz.cryptoid.info/grs-test/tx.dws?{0}.htm",
|
||||
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
|
||||
NBXplorerNetwork = nbxplorerNetwork,
|
||||
UriScheme = "groestlcoin",
|
||||
DefaultRateRules = new[]
|
||||
{
|
||||
"GRS_X = GRS_BTC * BTC_X",
|
||||
"GRS_BTC = bittrex(GRS_BTC)"
|
||||
},
|
||||
CryptoImagePath = "imlegacy/groestlcoin.png",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("17'") : new KeyPath("1'")
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -16,13 +16,13 @@ namespace BTCPayServer
|
||||
Add(new BTCPayNetwork()
|
||||
{
|
||||
CryptoCode = nbxplorerNetwork.CryptoCode,
|
||||
DisplayName = "Litecoin",
|
||||
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://live.blockcypher.com/ltc/tx/{0}/" : "http://explorer.litecointools.com/tx/{0}",
|
||||
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
|
||||
NBXplorerNetwork = nbxplorerNetwork,
|
||||
UriScheme = "litecoin",
|
||||
DefaultRateProvider = new CoinAverageRateProviderDescription("LTC"),
|
||||
CryptoImagePath = "imlegacy/litecoin-symbol.svg",
|
||||
LightningImagePath = "imlegacy/ltc-lightning.svg",
|
||||
CryptoImagePath = "imlegacy/litecoin.svg",
|
||||
LightningImagePath = "imlegacy/litecoin-lightning.svg",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("2'") : new KeyPath("1'")
|
||||
});
|
||||
|
36
BTCPayServer/BTCPayNetworkProvider.Monacoin.cs
Normal file
36
BTCPayServer/BTCPayNetworkProvider.Monacoin.cs
Normal file
@ -0,0 +1,36 @@
|
||||
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 InitMonacoin()
|
||||
{
|
||||
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("MONA");
|
||||
Add(new BTCPayNetwork()
|
||||
{
|
||||
CryptoCode = nbxplorerNetwork.CryptoCode,
|
||||
DisplayName = "Monacoin",
|
||||
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://mona.insight.monaco-ex.org/insight/tx/{0}" : "https://testnet-mona.insight.monaco-ex.org/insight/tx/{0}",
|
||||
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
|
||||
NBXplorerNetwork = nbxplorerNetwork,
|
||||
UriScheme = "monacoin",
|
||||
DefaultRateRules = new[]
|
||||
{
|
||||
"MONA_X = MONA_BTC * BTC_X",
|
||||
"MONA_BTC = zaif(MONA_BTC)"
|
||||
},
|
||||
CryptoImagePath = "imlegacy/monacoin.png",
|
||||
LightningImagePath = "imlegacy/mona-lightning.svg",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("22'") : new KeyPath("1'")
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
35
BTCPayServer/BTCPayNetworkProvider.Polis.cs
Normal file
35
BTCPayServer/BTCPayNetworkProvider.Polis.cs
Normal file
@ -0,0 +1,35 @@
|
||||
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 InitPolis()
|
||||
{
|
||||
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("POLIS");
|
||||
Add(new BTCPayNetwork()
|
||||
{
|
||||
CryptoCode = nbxplorerNetwork.CryptoCode,
|
||||
DisplayName = "Polis",
|
||||
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://insight.polispay.org/tx/{0}" : "https://insight.polispay.org/tx/{0}",
|
||||
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
|
||||
NBXplorerNetwork = nbxplorerNetwork,
|
||||
UriScheme = "polis",
|
||||
DefaultRateRules = new[]
|
||||
{
|
||||
"POLIS_X = POLIS_BTC * BTC_X",
|
||||
"POLIS_BTC = cryptopia(POLIS_BTC)"
|
||||
},
|
||||
CryptoImagePath = "imlegacy/polis.png",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("1997'") : new KeyPath("1'")
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
35
BTCPayServer/BTCPayNetworkProvider.Ufo.cs
Normal file
35
BTCPayServer/BTCPayNetworkProvider.Ufo.cs
Normal file
@ -0,0 +1,35 @@
|
||||
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 InitUfo()
|
||||
{
|
||||
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("UFO");
|
||||
Add(new BTCPayNetwork()
|
||||
{
|
||||
CryptoCode = nbxplorerNetwork.CryptoCode,
|
||||
DisplayName = "Ufo",
|
||||
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://chainz.cryptoid.info/ufo/tx.dws?{0}" : "https://chainz.cryptoid.info/ufo/tx.dws?{0}",
|
||||
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
|
||||
NBXplorerNetwork = nbxplorerNetwork,
|
||||
UriScheme = "ufo",
|
||||
DefaultRateRules = new[]
|
||||
{
|
||||
"UFO_X = UFO_BTC * BTC_X",
|
||||
"UFO_BTC = coinexchange(UFO_BTC)"
|
||||
},
|
||||
CryptoImagePath = "imlegacy/ufo.png",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("202'") : new KeyPath("1'")
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
35
BTCPayServer/BTCPayNetworkProvider.Viacoin.cs
Normal file
35
BTCPayServer/BTCPayNetworkProvider.Viacoin.cs
Normal file
@ -0,0 +1,35 @@
|
||||
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 InitViacoin()
|
||||
{
|
||||
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("VIA");
|
||||
Add(new BTCPayNetwork()
|
||||
{
|
||||
CryptoCode = nbxplorerNetwork.CryptoCode,
|
||||
DisplayName = "Viacoin",
|
||||
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://explorer.viacoin.org/tx/{0}" : "https://explorer.viacoin.org/tx/{0}",
|
||||
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
|
||||
NBXplorerNetwork = nbxplorerNetwork,
|
||||
UriScheme = "viacoin",
|
||||
DefaultRateRules = new[]
|
||||
{
|
||||
"VIA_X = VIA_BTC * BTC_X",
|
||||
"VIA_BTC = bittrex(VIA_BTC)"
|
||||
},
|
||||
CryptoImagePath = "imlegacy/viacoin.png",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("14'") : new KeyPath("1'")
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -48,6 +48,14 @@ namespace BTCPayServer
|
||||
InitBitcoin();
|
||||
InitLitecoin();
|
||||
InitDogecoin();
|
||||
InitBitcoinGold();
|
||||
InitMonacoin();
|
||||
InitDash();
|
||||
InitPolis();
|
||||
InitFeathercoin();
|
||||
InitGroestlcoin();
|
||||
InitViacoin();
|
||||
//InitUfo();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -86,7 +94,11 @@ namespace BTCPayServer
|
||||
|
||||
public BTCPayNetwork GetNetwork(string cryptoCode)
|
||||
{
|
||||
_Networks.TryGetValue(cryptoCode.ToUpperInvariant(), out BTCPayNetwork network);
|
||||
if(!_Networks.TryGetValue(cryptoCode.ToUpperInvariant(), out BTCPayNetwork network))
|
||||
{
|
||||
if (cryptoCode == "XBT")
|
||||
return GetNetwork("BTC");
|
||||
}
|
||||
return network;
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>netcoreapp2.0</TargetFramework>
|
||||
<Version>1.0.1.91</Version>
|
||||
<TargetFramework>netcoreapp2.1</TargetFramework>
|
||||
<Version>1.0.2.89</Version>
|
||||
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
@ -30,37 +30,36 @@
|
||||
<EmbeddedResource Include="Currencies.txt" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BuildBundlerMinifier" Version="2.6.375" />
|
||||
<PackageReference Include="BuildBundlerMinifier" Version="2.7.385" />
|
||||
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="0.4.1" />
|
||||
<PackageReference Include="Hangfire" Version="1.6.19" />
|
||||
<PackageReference Include="Hangfire.MemoryStorage" Version="1.5.2" />
|
||||
<PackageReference Include="Hangfire.PostgreSql" Version="1.4.8.1" />
|
||||
<PackageReference Include="LedgerWallet" Version="1.0.1.35" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="2.6.1" />
|
||||
<PackageReference Include="Meziantou.AspNetCore.BundleTagHelpers" Version="1.0.1" />
|
||||
<PackageReference Include="Hangfire.PostgreSql" Version="1.4.8.2" />
|
||||
<PackageReference Include="LedgerWallet" Version="2.0.0" />
|
||||
<PackageReference Include="Meziantou.AspNetCore.BundleTagHelpers" Version="2.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Filter" Version="1.1.2" />
|
||||
<PackageReference Include="Microsoft.NetCore.Analyzers" Version="2.6.0" />
|
||||
<PackageReference Include="NBitcoin" Version="4.1.1" />
|
||||
<PackageReference Include="NBitpayClient" Version="1.0.0.18" />
|
||||
<PackageReference Include="NBitcoin" Version="4.1.1.32" />
|
||||
<PackageReference Include="NBitpayClient" Version="1.0.0.29" />
|
||||
<PackageReference Include="DBreeze" Version="1.87.0" />
|
||||
<PackageReference Include="NBXplorer.Client" Version="1.0.2" />
|
||||
<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.1" />
|
||||
<PackageReference Include="System.ValueTuple" Version="4.4.0" />
|
||||
<PackageReference Include="System.Xml.XmlSerializer" Version="4.0.11" />
|
||||
<PackageReference Include="NBXplorer.Client" Version="1.0.2.15" />
|
||||
<PackageReference Include="NicolasDorier.CommandLine" Version="1.0.0.2" />
|
||||
<PackageReference Include="NicolasDorier.CommandLine.Configuration" Version="1.0.0.3" />
|
||||
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="1.0.0.16" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="2.1.0" />
|
||||
<PackageReference Include="SSH.NET" Version="2016.1.0" />
|
||||
<PackageReference Include="System.Xml.XmlSerializer" Version="4.3.0" />
|
||||
<PackageReference Include="Text.Analyzers" Version="2.6.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.6" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="2.0.2" PrivateAssets="All" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.App" Version="2.1.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version=" 2.1.0" PrivateAssets="All" />
|
||||
<PackageReference Include="YamlDotNet" Version="4.3.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="2.0.0" />
|
||||
<DotNetCliToolReference Include="Microsoft.Extensions.SecretManager.Tools" Version="2.0.0" />
|
||||
<DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="2.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
@ -113,6 +112,37 @@
|
||||
<ItemGroup>
|
||||
<Folder Include="Build\" />
|
||||
<Folder Include="wwwroot\vendor\clipboard.js\" />
|
||||
<Folder Include="wwwroot\vendor\highlightjs\" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Update="Views\Server\SSHService.cshtml">
|
||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||
</Content>
|
||||
<Content Update="Views\Server\LNDGRPCServices.cshtml">
|
||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||
</Content>
|
||||
<Content Update="Views\Server\Maintenance.cshtml">
|
||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||
</Content>
|
||||
<Content Update="Views\Server\Services.cshtml">
|
||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||
</Content>
|
||||
<Content Update="Views\Wallets\ListWallets.cshtml">
|
||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||
</Content>
|
||||
<Content Update="Views\Wallets\WalletTransactions.cshtml">
|
||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||
</Content>
|
||||
<Content Update="Views\Wallets\_Nav.cshtml">
|
||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||
</Content>
|
||||
<Content Update="Views\Wallets\_ViewImports.cshtml">
|
||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||
</Content>
|
||||
<Content Update="Views\Wallets\_ViewStart.cshtml">
|
||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -11,6 +11,8 @@ using StandardConfiguration;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using NBXplorer;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using Renci.SshNet;
|
||||
using NBitcoin.DataEncoders;
|
||||
|
||||
namespace BTCPayServer.Configuration
|
||||
{
|
||||
@ -21,6 +23,69 @@ namespace BTCPayServer.Configuration
|
||||
public string CookieFile { get; internal set; }
|
||||
}
|
||||
|
||||
public class SSHSettings
|
||||
{
|
||||
public string Server { get; set; }
|
||||
public int Port { get; set; } = 22;
|
||||
public string KeyFile { get; set; }
|
||||
public string KeyFilePassword { get; set; }
|
||||
public string Username { get; set; }
|
||||
public string Password { get; set; }
|
||||
|
||||
public ConnectionInfo CreateConnectionInfo()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(KeyFile))
|
||||
{
|
||||
return new ConnectionInfo(Server, Port, Username, new[] { new PrivateKeyAuthenticationMethod(Username, new PrivateKeyFile(KeyFile, KeyFilePassword)) });
|
||||
}
|
||||
else
|
||||
{
|
||||
return new ConnectionInfo(Server, Port, Username, new[] { new PasswordAuthenticationMethod(Username, Password) });
|
||||
}
|
||||
}
|
||||
|
||||
public static SSHSettings ParseConfiguration(IConfiguration conf)
|
||||
{
|
||||
var externalUrl = conf.GetOrDefault<Uri>("externalurl", null);
|
||||
var settings = new SSHSettings();
|
||||
settings.Server = conf.GetOrDefault<string>("sshconnection", null);
|
||||
if (settings.Server != null)
|
||||
{
|
||||
var parts = settings.Server.Split(':');
|
||||
if (parts.Length == 2 && int.TryParse(parts[1], out int port))
|
||||
{
|
||||
settings.Port = port;
|
||||
settings.Server = parts[0];
|
||||
}
|
||||
else
|
||||
{
|
||||
settings.Port = 22;
|
||||
}
|
||||
|
||||
parts = settings.Server.Split('@');
|
||||
if (parts.Length == 2)
|
||||
{
|
||||
settings.Username = parts[0];
|
||||
settings.Server = parts[1];
|
||||
}
|
||||
else
|
||||
{
|
||||
settings.Username = "root";
|
||||
}
|
||||
}
|
||||
else if (externalUrl != null)
|
||||
{
|
||||
settings.Port = 22;
|
||||
settings.Username = "root";
|
||||
settings.Server = externalUrl.DnsSafeHost;
|
||||
}
|
||||
settings.Password = conf.GetOrDefault<string>("sshpassword", "");
|
||||
settings.KeyFile = conf.GetOrDefault<string>("sshkeyfile", "");
|
||||
settings.KeyFilePassword = conf.GetOrDefault<string>("sshkeyfilepassword", "");
|
||||
return settings;
|
||||
}
|
||||
}
|
||||
|
||||
public class BTCPayServerOptions
|
||||
{
|
||||
public NetworkType NetworkType
|
||||
@ -74,16 +139,40 @@ namespace BTCPayServer.Configuration
|
||||
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 lightning = conf.GetOrDefault<string>($"{net.CryptoCode}.lightning", string.Empty);
|
||||
if(lightning.Length != 0)
|
||||
{
|
||||
if(!LightningConnectionString.TryParse(lightning, out var connectionString, out var error))
|
||||
var lightning = conf.GetOrDefault<string>($"{net.CryptoCode}.lightning", string.Empty);
|
||||
if (lightning.Length != 0)
|
||||
{
|
||||
throw new ConfigException($"Invalid setting {net.CryptoCode}.lightning, you need to pass either " +
|
||||
$"the absolute path to the unix socket of a running CLightning instance (eg. /root/.lightning/lightning-rpc), " +
|
||||
$"or the url to a charge server with crendetials (eg. https://apitoken@API_TOKEN_SECRET:charge.example.com/)");
|
||||
if (!LightningConnectionString.TryParse(lightning, true, out var connectionString, out var error))
|
||||
{
|
||||
throw new ConfigException($"Invalid setting {net.CryptoCode}.lightning, " + Environment.NewLine +
|
||||
$"If you have a lightning server use: 'type=clightning;server=/root/.lightning/lightning-rpc', " + Environment.NewLine +
|
||||
$"If you have a lightning charge server: 'type=charge;server=https://charge.example.com;api-token=yourapitoken'" + Environment.NewLine +
|
||||
$"If you have a lnd server: 'type=lnd-rest;server=https://lnd:lnd@lnd.example.com;macaroon=abf239...;certthumbprint=2abdf302...'" + Environment.NewLine +
|
||||
$" lnd server: 'type=lnd-rest;server=https://lnd:lnd@lnd.example.com;macaroonfilepath=/root/.lnd/admin.macaroon;certthumbprint=2abdf302...'" + Environment.NewLine +
|
||||
error);
|
||||
}
|
||||
if (connectionString.IsLegacy)
|
||||
{
|
||||
Logs.Configuration.LogWarning($"Setting {net.CryptoCode}.lightning will work but use an deprecated format, please replace it by '{connectionString.ToString()}'");
|
||||
}
|
||||
InternalLightningByCryptoCode.Add(net.CryptoCode, connectionString);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
var lightning = conf.GetOrDefault<string>($"{net.CryptoCode}.external.lnd.grpc", string.Empty);
|
||||
if (lightning.Length != 0)
|
||||
{
|
||||
if (!LightningConnectionString.TryParse(lightning, false, out var connectionString, out var error))
|
||||
{
|
||||
throw new ConfigException($"Invalid setting {net.CryptoCode}.external.lnd.grpc, " + Environment.NewLine +
|
||||
$"lnd server: 'type=lnd-grpc;server=https://lnd.example.com;macaroon=abf239...;certthumbprint=2abdf302...'" + Environment.NewLine +
|
||||
$"lnd server: 'type=lnd-grpc;server=https://lnd.example.com;macaroonfilepath=/root/.lnd/admin.macaroon;certthumbprint=2abdf302...'" + Environment.NewLine +
|
||||
error);
|
||||
}
|
||||
ExternalServicesByCryptoCode.Add(net.CryptoCode, new ExternalLNDGRPC(connectionString));
|
||||
}
|
||||
InternalLightningByCryptoCode.Add(net.CryptoCode, connectionString);
|
||||
}
|
||||
}
|
||||
|
||||
@ -93,15 +182,86 @@ namespace BTCPayServer.Configuration
|
||||
BundleJsCss = conf.GetOrDefault<bool>("bundlejscss", true);
|
||||
ExternalUrl = conf.GetOrDefault<Uri>("externalurl", null);
|
||||
|
||||
var sshSettings = SSHSettings.ParseConfiguration(conf);
|
||||
if ((!string.IsNullOrEmpty(sshSettings.Password) || !string.IsNullOrEmpty(sshSettings.KeyFile)) && !string.IsNullOrEmpty(sshSettings.Server))
|
||||
{
|
||||
if (!string.IsNullOrEmpty(sshSettings.KeyFile) && !File.Exists(sshSettings.KeyFile))
|
||||
throw new ConfigException($"sshkeyfile does not exist");
|
||||
if (sshSettings.Port > ushort.MaxValue ||
|
||||
sshSettings.Port < ushort.MinValue)
|
||||
throw new ConfigException($"ssh port is invalid");
|
||||
if (!string.IsNullOrEmpty(sshSettings.Password) && !string.IsNullOrEmpty(sshSettings.KeyFile))
|
||||
throw new ConfigException($"sshpassword or sshkeyfile should be provided, but not both");
|
||||
try
|
||||
{
|
||||
sshSettings.CreateConnectionInfo();
|
||||
}
|
||||
catch
|
||||
{
|
||||
throw new ConfigException($"sshkeyfilepassword is invalid");
|
||||
}
|
||||
SSHSettings = sshSettings;
|
||||
}
|
||||
|
||||
var fingerPrints = conf.GetOrDefault<string>("sshtrustedfingerprints", "");
|
||||
if (!string.IsNullOrEmpty(fingerPrints))
|
||||
{
|
||||
foreach (var fingerprint in fingerPrints.Split(';', StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(str => str.Replace(":", "", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
TrustedFingerprints.Add(DecodeFingerprint(fingerprint));
|
||||
}
|
||||
}
|
||||
|
||||
RootPath = conf.GetOrDefault<string>("rootpath", "/");
|
||||
if (!RootPath.StartsWith("/", StringComparison.InvariantCultureIgnoreCase))
|
||||
RootPath = "/" + RootPath;
|
||||
var old = conf.GetOrDefault<Uri>("internallightningnode", null);
|
||||
if(old != null)
|
||||
if (old != null)
|
||||
throw new ConfigException($"internallightningnode should not be used anymore, use btclightning instead");
|
||||
}
|
||||
|
||||
private static byte[] DecodeFingerprint(string fingerprint)
|
||||
{
|
||||
try
|
||||
{
|
||||
return Encoders.Hex.DecodeData(fingerprint.Trim());
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
var localFingerprint = fingerprint;
|
||||
if (localFingerprint.StartsWith("SHA256", StringComparison.OrdinalIgnoreCase))
|
||||
localFingerprint = localFingerprint.Substring("SHA256".Length).Trim();
|
||||
try
|
||||
{
|
||||
return Encoders.Base64.DecodeData(localFingerprint);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
if (!localFingerprint.EndsWith('='))
|
||||
localFingerprint = localFingerprint + "=";
|
||||
try
|
||||
{
|
||||
return Encoders.Base64.DecodeData(localFingerprint);
|
||||
}
|
||||
catch
|
||||
{
|
||||
throw new ConfigException($"sshtrustedfingerprints is invalid");
|
||||
}
|
||||
}
|
||||
|
||||
internal bool IsTrustedFingerprint(byte[] fingerPrint)
|
||||
{
|
||||
return TrustedFingerprints.Any(f => Utils.ArrayEqual(f, fingerPrint));
|
||||
}
|
||||
|
||||
public string RootPath { get; set; }
|
||||
public Dictionary<string, LightningConnectionString> InternalLightningByCryptoCode { get; set; } = new Dictionary<string, LightningConnectionString>();
|
||||
public ExternalServices ExternalServicesByCryptoCode { get; set; } = new ExternalServices();
|
||||
|
||||
public BTCPayNetworkProvider NetworkProvider { get; set; }
|
||||
public string PostgresConnectionString
|
||||
@ -119,6 +279,12 @@ namespace BTCPayServer.Configuration
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public List<byte[]> TrustedFingerprints { get; set; } = new List<byte[]>();
|
||||
public SSHSettings SSHSettings
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
internal string GetRootUri()
|
||||
{
|
||||
@ -129,4 +295,29 @@ namespace BTCPayServer.Configuration
|
||||
return builder.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
public class ExternalServices : MultiValueDictionary<string, ExternalService>
|
||||
{
|
||||
public IEnumerable<T> GetServices<T>(string cryptoCode) where T : ExternalService
|
||||
{
|
||||
if (!this.TryGetValue(cryptoCode.ToUpperInvariant(), out var services))
|
||||
return Array.Empty<T>();
|
||||
return services.OfType<T>();
|
||||
}
|
||||
}
|
||||
|
||||
public class ExternalService
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public class ExternalLNDGRPC : ExternalService
|
||||
{
|
||||
public ExternalLNDGRPC(LightningConnectionString connectionString)
|
||||
{
|
||||
ConnectionString = connectionString;
|
||||
}
|
||||
|
||||
public LightningConnectionString ConnectionString { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -27,19 +27,25 @@ namespace BTCPayServer.Configuration
|
||||
};
|
||||
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);
|
||||
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);
|
||||
app.Option("--bundlejscss", $"Bundle javascript and css files for better performance (default: true)", 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 as a comma separated (default: btc; available: {chains})", CommandOptionType.SingleValue);
|
||||
app.Option("--postgres", $"Connection string to a PostgreSQL database (default: SQLite)", 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);
|
||||
app.Option("--bundlejscss", $"Bundle JavaScript and CSS files for better performance (default: true)", CommandOptionType.SingleValue);
|
||||
app.Option("--rootpath", "The root path in the URL to access BTCPay (default: /)", CommandOptionType.SingleValue);
|
||||
app.Option("--sshconnection", "SSH server to manage BTCPay under the form user@server:port (default: root@externalhost or empty)", CommandOptionType.SingleValue);
|
||||
app.Option("--sshpassword", "SSH password to manage BTCPay (default: empty)", CommandOptionType.SingleValue);
|
||||
app.Option("--sshkeyfile", "SSH private key file to manage BTCPay (default: empty)", CommandOptionType.SingleValue);
|
||||
app.Option("--sshkeyfilepassword", "Password of the SSH keyfile (default: empty)", CommandOptionType.SingleValue);
|
||||
app.Option("--sshtrustedfingerprints", "SSH Host SHA256 rsa fingerprint in base64 or hex (default: empty, it will allow untrusted connections)", 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}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($"--{crypto}lightning", $"Easy configuration of lightning for the server adnistrator: Must be a unix socket of CLightning (lightning-rpc) or URL to a charge server (default: empty)", CommandOptionType.SingleValue);
|
||||
app.Option($"--{crypto}lightning", $"Easy configuration of lightning for the server administrator: Must be a UNIX socket of c-lightning (lightning-rpc) or URL to a charge server (default: empty)", CommandOptionType.SingleValue);
|
||||
app.Option($"--{crypto}externallndgrpc", $"The LND gRPC configuration BTCPay will expose to easily connect to the internal lnd wallet from Zap wallet (default: empty)", CommandOptionType.SingleValue);
|
||||
}
|
||||
return app;
|
||||
}
|
||||
|
@ -12,6 +12,8 @@ using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
[Authorize(AuthenticationSchemes = Security.Policies.BitpayAuthentication)]
|
||||
[BitpayAPIConstraint(true)]
|
||||
public class AccessTokenController : Controller
|
||||
{
|
||||
TokenRepository _TokenRepository;
|
||||
@ -23,12 +25,13 @@ namespace BTCPayServer.Controllers
|
||||
[Route("tokens")]
|
||||
public async Task<GetTokensResponse> Tokens()
|
||||
{
|
||||
var tokens = await _TokenRepository.GetTokens(this.GetBitIdentity().SIN);
|
||||
var tokens = await _TokenRepository.GetTokens(this.User.GetSIN());
|
||||
return new GetTokensResponse(tokens);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("tokens")]
|
||||
[AllowAnonymous]
|
||||
public async Task<DataWrapper<List<PairingCodeResponse>>> Tokens([FromBody] TokenRequest request)
|
||||
{
|
||||
PairingCodeEntity pairingEntity = null;
|
||||
@ -51,8 +54,8 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
else
|
||||
{
|
||||
var sin = this.GetBitIdentity(false)?.SIN ?? request.Id;
|
||||
if (string.IsNullOrEmpty(request.Id) || !NBitpayClient.Extensions.BitIdExtensions.ValidateSIN(request.Id))
|
||||
var sin = this.User.GetSIN() ?? request.Id;
|
||||
if (string.IsNullOrEmpty(sin) || !NBitpayClient.Extensions.BitIdExtensions.ValidateSIN(sin))
|
||||
throw new BitpayHttpException(400, "'id' property is required, alternatively, use BitId");
|
||||
|
||||
pairingEntity = await _TokenRepository.GetPairingAsync(request.PairingCode);
|
||||
@ -76,6 +79,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
new PairingCodeResponse()
|
||||
{
|
||||
Policies = new Newtonsoft.Json.Linq.JArray(),
|
||||
PairingCode = pairingEntity.Id,
|
||||
PairingExpiration = pairingEntity.Expiration,
|
||||
DateCreated = pairingEntity.CreatedTime,
|
||||
|
@ -16,10 +16,12 @@ using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Mails;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.Security;
|
||||
using System.Globalization;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
[Authorize]
|
||||
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
||||
[Route("[controller]/[action]")]
|
||||
public class AccountController : Controller
|
||||
{
|
||||
@ -235,23 +237,25 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
[HttpGet]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> Register(string returnUrl = null)
|
||||
public async Task<IActionResult> Register(string returnUrl = null, bool logon = true)
|
||||
{
|
||||
var policies = await _SettingsRepository.GetSettingAsync<PoliciesSettings>() ?? new PoliciesSettings();
|
||||
if (policies.LockSubscription)
|
||||
if (policies.LockSubscription && !User.IsInRole(Roles.ServerAdmin))
|
||||
return RedirectToAction(nameof(HomeController.Index), "Home");
|
||||
ViewData["ReturnUrl"] = returnUrl;
|
||||
ViewData["Logon"] = logon.ToString(CultureInfo.InvariantCulture).ToLowerInvariant();
|
||||
return View();
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[AllowAnonymous]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Register(RegisterViewModel model, string returnUrl = null)
|
||||
public async Task<IActionResult> Register(RegisterViewModel model, string returnUrl = null, bool logon = true)
|
||||
{
|
||||
ViewData["ReturnUrl"] = returnUrl;
|
||||
ViewData["Logon"] = logon.ToString(CultureInfo.InvariantCulture).ToLowerInvariant();
|
||||
var policies = await _SettingsRepository.GetSettingAsync<PoliciesSettings>() ?? new PoliciesSettings();
|
||||
if (policies.LockSubscription)
|
||||
if (policies.LockSubscription && !User.IsInRole(Roles.ServerAdmin))
|
||||
return RedirectToAction(nameof(HomeController.Index), "Home");
|
||||
if (ModelState.IsValid)
|
||||
{
|
||||
@ -273,7 +277,8 @@ namespace BTCPayServer.Controllers
|
||||
await _emailSender.SendEmailConfirmationAsync(model.Email, callbackUrl);
|
||||
if (!policies.RequiresConfirmedEmail)
|
||||
{
|
||||
await _signInManager.SignInAsync(user, isPersistent: false);
|
||||
if(logon)
|
||||
await _signInManager.SignInAsync(user, isPersistent: false);
|
||||
return RedirectToLocal(returnUrl);
|
||||
}
|
||||
else
|
||||
|
@ -17,6 +17,9 @@ using YamlDotNet.RepresentationModel;
|
||||
using System.IO;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using System.Text.Encodings.Web;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
@ -42,10 +45,12 @@ namespace BTCPayServer.Controllers
|
||||
" price: 15\n\n" +
|
||||
"tshirt:\n" +
|
||||
" price: 25";
|
||||
ShowCustomAmount = true;
|
||||
}
|
||||
public string Title { get; set; }
|
||||
public string Currency { get; set; }
|
||||
public string Template { get; set; }
|
||||
public bool ShowCustomAmount { get; set; }
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
@ -55,15 +60,56 @@ namespace BTCPayServer.Controllers
|
||||
var app = await GetOwnedApp(appId, AppType.PointOfSale);
|
||||
if (app == null)
|
||||
return NotFound();
|
||||
|
||||
var settings = app.GetSettings<PointOfSaleSettings>();
|
||||
return View(new UpdatePointOfSaleViewModel() { Title = settings.Title, Currency = settings.Currency, Template = settings.Template });
|
||||
var vm = new UpdatePointOfSaleViewModel()
|
||||
{
|
||||
Title = settings.Title,
|
||||
ShowCustomAmount = settings.ShowCustomAmount,
|
||||
Currency = settings.Currency,
|
||||
Template = settings.Template
|
||||
};
|
||||
if (HttpContext?.Request != null)
|
||||
{
|
||||
var appUrl = HttpContext.Request.GetAbsoluteRoot().WithTrailingSlash() + $"apps/{appId}/pos";
|
||||
var encoder = HtmlEncoder.Default;
|
||||
if (settings.ShowCustomAmount)
|
||||
{
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.AppendLine($"<form method=\"POST\" action=\"{encoder.Encode(appUrl)}\">");
|
||||
builder.AppendLine($" <input type=\"hidden\" name=\"amount\" value=\"100\" />");
|
||||
builder.AppendLine($" <input type=\"hidden\" name=\"email\" value=\"customer@example.com\" />");
|
||||
builder.AppendLine($" <input type=\"hidden\" name=\"orderId\" value=\"CustomOrderId\" />");
|
||||
builder.AppendLine($" <input type=\"hidden\" name=\"notificationUrl\" value=\"https://example.com/callbacks\" />");
|
||||
builder.AppendLine($" <input type=\"hidden\" name=\"redirectUrl\" value=\"https://example.com/thanksyou\" />");
|
||||
builder.AppendLine($" <button type=\"submit\">Buy now</button>");
|
||||
builder.AppendLine($"</form>");
|
||||
vm.Example1 = builder.ToString();
|
||||
}
|
||||
try
|
||||
{
|
||||
var items = Parse(settings.Template, settings.Currency);
|
||||
var builder = new StringBuilder();
|
||||
builder.AppendLine($"<form method=\"POST\" action=\"{encoder.Encode(appUrl)}\">");
|
||||
builder.AppendLine($" <input type=\"hidden\" name=\"email\" value=\"customer@example.com\" />");
|
||||
builder.AppendLine($" <input type=\"hidden\" name=\"orderId\" value=\"CustomOrderId\" />");
|
||||
builder.AppendLine($" <input type=\"hidden\" name=\"notificationUrl\" value=\"https://example.com/callbacks\" />");
|
||||
builder.AppendLine($" <input type=\"hidden\" name=\"redirectUrl\" value=\"https://example.com/thanksyou\" />");
|
||||
builder.AppendLine($" <button type=\"submit\" name=\"choiceKey\" value=\"{items[0].Id}\">Buy now</button>");
|
||||
builder.AppendLine($"</form>");
|
||||
vm.Example2 = builder.ToString();
|
||||
}
|
||||
catch { }
|
||||
vm.InvoiceUrl = appUrl + "invoices/SkdsDghkdP3D3qkj7bLq3";
|
||||
}
|
||||
|
||||
vm.ExampleCallback = "{\n \"id\":\"SkdsDghkdP3D3qkj7bLq3\",\n \"url\":\"https://btcpay.example.com/invoice?id=SkdsDghkdP3D3qkj7bLq3\",\n \"status\":\"paid\",\n \"price\":10,\n \"currency\":\"EUR\",\n \"invoiceTime\":1520373130312,\n \"expirationTime\":1520374030312,\n \"currentTime\":1520373179327,\n \"exceptionStatus\":false,\n \"buyerFields\":{\n \"buyerEmail\":\"customer@example.com\",\n \"buyerNotify\":false\n },\n \"paymentSubtotals\": {\n \"BTC\":114700\n },\n \"paymentTotals\": {\n \"BTC\":118400\n },\n \"transactionCurrency\": \"BTC\",\n \"amountPaid\": \"1025900\",\n \"exchangeRates\": {\n \"BTC\": {\n \"EUR\": 8721.690715789999,\n \"USD\": 10817.99\n }\n }\n}";
|
||||
return View(vm);
|
||||
}
|
||||
[HttpPost]
|
||||
[Route("{appId}/settings/pos")]
|
||||
public async Task<IActionResult> UpdatePointOfSale(string appId, UpdatePointOfSaleViewModel vm)
|
||||
{
|
||||
if (_Currencies.GetCurrencyData(vm.Currency) == null)
|
||||
if (_Currencies.GetCurrencyData(vm.Currency, false) == null)
|
||||
ModelState.AddModelError(nameof(vm.Currency), "Invalid currency");
|
||||
try
|
||||
{
|
||||
@ -83,12 +129,13 @@ namespace BTCPayServer.Controllers
|
||||
app.SetSettings(new PointOfSaleSettings()
|
||||
{
|
||||
Title = vm.Title,
|
||||
ShowCustomAmount = vm.ShowCustomAmount,
|
||||
Currency = vm.Currency.ToUpperInvariant(),
|
||||
Template = vm.Template
|
||||
});
|
||||
await UpdateAppSettings(app);
|
||||
StatusMessage = "App updated";
|
||||
return RedirectToAction(nameof(UpdatePointOfSale));
|
||||
return RedirectToAction(nameof(ListApps));
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
@ -99,9 +146,14 @@ namespace BTCPayServer.Controllers
|
||||
if (app == null)
|
||||
return NotFound();
|
||||
var settings = app.GetSettings<PointOfSaleSettings>();
|
||||
var currency = _Currencies.GetCurrencyData(settings.Currency, false);
|
||||
double step = currency == null ? 1 : Math.Pow(10, -(currency.Divisibility));
|
||||
|
||||
return View(new ViewPointOfSaleViewModel()
|
||||
{
|
||||
Title = settings.Title,
|
||||
Step = step.ToString(CultureInfo.InvariantCulture),
|
||||
ShowCustomAmount = settings.ShowCustomAmount,
|
||||
Items = Parse(settings.Template, settings.Currency)
|
||||
});
|
||||
}
|
||||
@ -111,7 +163,8 @@ namespace BTCPayServer.Controllers
|
||||
using (var ctx = _ContextFactory.CreateContext())
|
||||
{
|
||||
return await ctx.Apps
|
||||
.Where(us => us.Id == appId && us.AppType == appType.ToString())
|
||||
.Where(us => us.Id == appId &&
|
||||
us.AppType == appType.ToString())
|
||||
.FirstOrDefaultAsync();
|
||||
}
|
||||
}
|
||||
@ -155,23 +208,57 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
[HttpPost]
|
||||
[Route("{appId}/pos")]
|
||||
public async Task<IActionResult> ViewPointOfSale(string appId, string choiceKey)
|
||||
[IgnoreAntiforgeryToken]
|
||||
[EnableCors(CorsPolicies.All)]
|
||||
public async Task<IActionResult> ViewPointOfSale(string appId,
|
||||
decimal amount,
|
||||
string email,
|
||||
string orderId,
|
||||
string notificationUrl,
|
||||
string redirectUrl,
|
||||
string choiceKey)
|
||||
{
|
||||
var app = await GetApp(appId, AppType.PointOfSale);
|
||||
if (string.IsNullOrEmpty(choiceKey) && amount <= 0)
|
||||
{
|
||||
return RedirectToAction(nameof(ViewPointOfSale), new { appId = appId });
|
||||
}
|
||||
if (app == null)
|
||||
return NotFound();
|
||||
var settings = app.GetSettings<PointOfSaleSettings>();
|
||||
var choices = Parse(settings.Template, settings.Currency);
|
||||
var choice = choices.FirstOrDefault(c => c.Id == choiceKey);
|
||||
if (choice == null)
|
||||
return NotFound();
|
||||
|
||||
if (string.IsNullOrEmpty(choiceKey) && !settings.ShowCustomAmount)
|
||||
{
|
||||
return RedirectToAction(nameof(ViewPointOfSale), new { appId = appId });
|
||||
}
|
||||
string title = null;
|
||||
var price = 0.0m;
|
||||
if (!string.IsNullOrEmpty(choiceKey))
|
||||
{
|
||||
var choices = Parse(settings.Template, settings.Currency);
|
||||
var choice = choices.FirstOrDefault(c => c.Id == choiceKey);
|
||||
if (choice == null)
|
||||
return NotFound();
|
||||
title = choice.Title;
|
||||
price = choice.Price.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!settings.ShowCustomAmount)
|
||||
return NotFound();
|
||||
price = amount;
|
||||
title = settings.Title;
|
||||
}
|
||||
var store = await GetStore(app);
|
||||
var invoice = await _InvoiceController.CreateInvoiceCore(new NBitpayClient.Invoice()
|
||||
{
|
||||
ItemDesc = choice.Title,
|
||||
ItemDesc = title,
|
||||
Currency = settings.Currency,
|
||||
Price = (double)choice.Price.Value,
|
||||
Price = price,
|
||||
BuyerEmail = email,
|
||||
OrderId = orderId,
|
||||
NotificationURL = notificationUrl,
|
||||
RedirectURL = redirectUrl,
|
||||
FullNotifications = true
|
||||
}, store, HttpContext.Request.GetAbsoluteRoot());
|
||||
return Redirect(invoice.Data.Url);
|
||||
}
|
||||
|
@ -102,9 +102,9 @@ namespace BTCPayServer.Controllers
|
||||
StatusMessage = "Error: You are not owner of this store";
|
||||
return RedirectToAction(nameof(ListApps));
|
||||
}
|
||||
var id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(32));
|
||||
using (var ctx = _ContextFactory.CreateContext())
|
||||
{
|
||||
var id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(32));
|
||||
var appData = new AppData() { Id = id };
|
||||
appData.StoreDataId = selectedStore;
|
||||
appData.Name = vm.Name;
|
||||
@ -113,6 +113,9 @@ namespace BTCPayServer.Controllers
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
StatusMessage = "App successfully created";
|
||||
|
||||
if (appType == AppType.PointOfSale)
|
||||
return RedirectToAction(nameof(UpdatePointOfSale), new { appId = id });
|
||||
return RedirectToAction(nameof(ListApps));
|
||||
}
|
||||
|
||||
@ -140,6 +143,8 @@ namespace BTCPayServer.Controllers
|
||||
.Where(us => us.ApplicationUserId == userId && us.Role == StoreRoles.Owner)
|
||||
.SelectMany(us => us.StoreData.Apps.Where(a => a.Id == appId))
|
||||
.FirstOrDefaultAsync();
|
||||
if (app == null)
|
||||
return null;
|
||||
if (type != null && type.Value.ToString() != app.AppType)
|
||||
return null;
|
||||
return app;
|
||||
@ -174,24 +179,19 @@ namespace BTCPayServer.Controllers
|
||||
using (var ctx = _ContextFactory.CreateContext())
|
||||
{
|
||||
return await ctx.UserStore
|
||||
.Where(us => us.ApplicationUserId == userId)
|
||||
.Select(us => new
|
||||
{
|
||||
IsOwner = us.Role == StoreRoles.Owner,
|
||||
StoreId = us.StoreDataId,
|
||||
StoreName = us.StoreData.StoreName,
|
||||
Apps = us.StoreData.Apps
|
||||
})
|
||||
.SelectMany(us => us.Apps.Select(app => new ListAppsViewModel.ListAppViewModel()
|
||||
{
|
||||
IsOwner = us.IsOwner,
|
||||
AppName = app.Name,
|
||||
AppType = app.AppType,
|
||||
Id = app.Id,
|
||||
StoreId = us.StoreId,
|
||||
StoreName = us.StoreName
|
||||
}))
|
||||
.ToArrayAsync();
|
||||
.Where(us => us.ApplicationUserId == userId)
|
||||
.Join(ctx.Apps, us => us.StoreDataId, app => app.StoreDataId,
|
||||
(us, app) =>
|
||||
new ListAppsViewModel.ListAppViewModel()
|
||||
{
|
||||
IsOwner = us.Role == StoreRoles.Owner,
|
||||
StoreId = us.StoreDataId,
|
||||
StoreName = us.StoreData.StoreName,
|
||||
AppName = app.Name,
|
||||
AppType = app.AppType,
|
||||
Id = app.Id
|
||||
})
|
||||
.ToArrayAsync();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -5,6 +5,7 @@ using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using BTCPayServer.Models;
|
||||
using NBitcoin.DataEncoders;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
|
@ -13,29 +13,26 @@ using BTCPayServer.Data;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using BTCPayServer.Security;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
[EnableCors("BitpayAPI")]
|
||||
[BitpayAPIConstraint]
|
||||
[Authorize(Policies.CanUseStore.Key, AuthenticationSchemes = Policies.BitpayAuthentication)]
|
||||
public class InvoiceControllerAPI : Controller
|
||||
{
|
||||
private InvoiceController _InvoiceController;
|
||||
private InvoiceRepository _InvoiceRepository;
|
||||
private TokenRepository _TokenRepository;
|
||||
private StoreRepository _StoreRepository;
|
||||
private BTCPayNetworkProvider _NetworkProvider;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@ -44,21 +41,17 @@ namespace BTCPayServer.Controllers
|
||||
[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());
|
||||
return await _InvoiceController.CreateInvoiceCore(invoice, HttpContext.GetStoreData(), HttpContext.Request.GetAbsoluteRoot());
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("invoices/{id}")]
|
||||
[AllowAnonymous]
|
||||
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);
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, id);
|
||||
if (invoice == null)
|
||||
throw new BitpayHttpException(404, "Object not found");
|
||||
|
||||
var resp = invoice.EntityToDTO(_NetworkProvider);
|
||||
return new DataWrapper<InvoiceResponse>(resp);
|
||||
}
|
||||
@ -77,8 +70,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
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,
|
||||
@ -87,55 +79,14 @@ namespace BTCPayServer.Controllers
|
||||
StartDate = dateStart,
|
||||
OrderId = orderId,
|
||||
ItemCode = itemCode,
|
||||
Status = status,
|
||||
StoreId = store.Id
|
||||
Status = status == null ? null : new[] { status },
|
||||
StoreId = new[] { this.HttpContext.GetStoreData().Id }
|
||||
};
|
||||
|
||||
|
||||
var entities = (await _InvoiceRepository.GetInvoices(query))
|
||||
.Select((o) => o.EntityToDTO(_NetworkProvider)).ToArray();
|
||||
|
||||
return DataWrapper.Create(entities);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -78,7 +78,7 @@ namespace BTCPayServer.Controllers
|
||||
var wallet = _WalletProvider.GetWallet(network);
|
||||
if (wallet == null)
|
||||
return NotFound();
|
||||
var payment = PaymentMessage.Load(Request.Body);
|
||||
var payment = PaymentMessage.Load(Request.Body, network.NBitcoinNetwork);
|
||||
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..."));
|
||||
|
@ -22,6 +22,7 @@ using BTCPayServer.Events;
|
||||
using NBXplorer;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Security;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
@ -50,14 +51,17 @@ namespace BTCPayServer.Controllers
|
||||
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",
|
||||
TransactionSpeed = invoice.SpeedPolicy == SpeedPolicy.HighSpeed ? "high" :
|
||||
invoice.SpeedPolicy == SpeedPolicy.MediumSpeed ? "medium" :
|
||||
invoice.SpeedPolicy == SpeedPolicy.LowMediumSpeed ? "low-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),
|
||||
Fiat = FormatCurrency((decimal)dto.Price, dto.Currency, _CurrencyNameTable),
|
||||
NotificationUrl = invoice.NotificationURL,
|
||||
RedirectUrl = invoice.RedirectURL,
|
||||
ProductInformation = invoice.ProductInformation,
|
||||
@ -74,13 +78,14 @@ namespace BTCPayServer.Controllers
|
||||
cryptoPayment.PaymentMethod = ToString(paymentMethodId);
|
||||
cryptoPayment.Due = accounting.Due.ToString() + $" {paymentMethodId.CryptoCode}";
|
||||
cryptoPayment.Paid = accounting.CryptoPaid.ToString() + $" {paymentMethodId.CryptoCode}";
|
||||
cryptoPayment.Overpaid = (accounting.DueUncapped > Money.Zero ? Money.Zero : -accounting.DueUncapped).ToString() + $" {paymentMethodId.CryptoCode}";
|
||||
|
||||
var onchainMethod = data.GetPaymentMethodDetails() as Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod;
|
||||
if (onchainMethod != null)
|
||||
{
|
||||
cryptoPayment.Address = onchainMethod.DepositAddress;
|
||||
}
|
||||
cryptoPayment.Rate = FormatCurrency(data);
|
||||
cryptoPayment.Rate = ExchangeRate(data);
|
||||
cryptoPayment.PaymentUrl = cryptoInfo.PaymentUrls.BIP21;
|
||||
model.CryptoPayments.Add(cryptoPayment);
|
||||
}
|
||||
@ -170,6 +175,7 @@ namespace BTCPayServer.Controllers
|
||||
[Route("invoice")]
|
||||
[AcceptMediaTypeConstraint("application/bitcoin-paymentrequest", false)]
|
||||
[XFrameOptionsAttribute(null)]
|
||||
[ReferrerPolicyAttribute("origin")]
|
||||
public async Task<IActionResult> Checkout(string invoiceId, string id = null, string paymentMethodId = null)
|
||||
{
|
||||
//Keep compatibility with Bitpay
|
||||
@ -181,6 +187,20 @@ namespace BTCPayServer.Controllers
|
||||
if (model == null)
|
||||
return NotFound();
|
||||
|
||||
|
||||
_CSP.Add(new ConsentSecurityPolicy("script-src", "'unsafe-eval'")); // Needed by Vue
|
||||
if (!string.IsNullOrEmpty(model.CustomCSSLink) &&
|
||||
Uri.TryCreate(model.CustomCSSLink, UriKind.Absolute, out var uri))
|
||||
{
|
||||
_CSP.Clear();
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(model.CustomLogoLink) &&
|
||||
Uri.TryCreate(model.CustomLogoLink, UriKind.Absolute, out uri))
|
||||
{
|
||||
_CSP.Clear();
|
||||
}
|
||||
|
||||
return View(nameof(Checkout), model);
|
||||
}
|
||||
|
||||
@ -193,21 +213,32 @@ namespace BTCPayServer.Controllers
|
||||
bool isDefaultCrypto = false;
|
||||
if (paymentMethodIdStr == null)
|
||||
{
|
||||
paymentMethodIdStr = store.GetDefaultCrypto();
|
||||
paymentMethodIdStr = store.GetDefaultCrypto(_NetworkProvider);
|
||||
isDefaultCrypto = true;
|
||||
}
|
||||
|
||||
var paymentMethodId = PaymentMethodId.Parse(paymentMethodIdStr);
|
||||
var network = _NetworkProvider.GetNetwork(paymentMethodId.CryptoCode);
|
||||
if (network == null && isDefaultCrypto)
|
||||
{
|
||||
network = _NetworkProvider.GetAll().FirstOrDefault();
|
||||
paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike);
|
||||
paymentMethodIdStr = paymentMethodId.ToString();
|
||||
}
|
||||
if (invoice == null || network == null)
|
||||
return null;
|
||||
if (!invoice.Support(paymentMethodId))
|
||||
{
|
||||
if (!isDefaultCrypto)
|
||||
return null;
|
||||
var paymentMethodTemp = invoice.GetPaymentMethods(_NetworkProvider).First();
|
||||
var paymentMethodTemp = invoice.GetPaymentMethods(_NetworkProvider)
|
||||
.Where(c=> paymentMethodId.CryptoCode == c.GetId().CryptoCode)
|
||||
.FirstOrDefault();
|
||||
if (paymentMethodTemp == null)
|
||||
paymentMethodTemp = invoice.GetPaymentMethods(_NetworkProvider).First();
|
||||
network = paymentMethodTemp.Network;
|
||||
paymentMethodId = paymentMethodTemp.GetId();
|
||||
paymentMethodIdStr = paymentMethodId.ToString();
|
||||
}
|
||||
|
||||
var paymentMethod = invoice.GetPaymentMethod(paymentMethodId, _NetworkProvider);
|
||||
@ -221,22 +252,27 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
CryptoCode = network.CryptoCode,
|
||||
PaymentMethodId = paymentMethodId.ToString(),
|
||||
PaymentMethodName = GetDisplayName(paymentMethodId, network),
|
||||
CryptoImage = GetImage(paymentMethodId, network),
|
||||
IsLightning = paymentMethodId.PaymentType == PaymentTypes.LightningLike,
|
||||
ServerUrl = HttpContext.Request.GetAbsoluteRoot(),
|
||||
OrderId = invoice.OrderId,
|
||||
InvoiceId = invoice.Id,
|
||||
DefaultLang = storeBlob.DefaultLang ?? "en-US",
|
||||
HtmlTitle = storeBlob.HtmlTitle ?? "BTCPay Invoice",
|
||||
CustomCSSLink = storeBlob.CustomCSS?.AbsoluteUri,
|
||||
CustomLogoLink = storeBlob.CustomLogo?.AbsoluteUri,
|
||||
BtcAddress = paymentMethodDetails.GetPaymentDestination(),
|
||||
OrderAmount = (accounting.TotalDue - accounting.NetworkFee).ToString(),
|
||||
BtcDue = accounting.Due.ToString(),
|
||||
OrderAmount = (accounting.TotalDue - accounting.NetworkFee).ToString(),
|
||||
OrderAmountFiat = OrderAmountFromInvoice(network.CryptoCode, invoice.ProductInformation),
|
||||
CustomerEmail = invoice.RefundMail,
|
||||
RequiresRefundEmail = storeBlob.RequiresRefundEmail,
|
||||
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),
|
||||
Rate = ExchangeRate(paymentMethod),
|
||||
MerchantRefLink = invoice.RedirectURL ?? "/",
|
||||
StoreName = store.StoreName,
|
||||
InvoiceBitcoinUrl = paymentMethodId.PaymentType == PaymentTypes.BTCLike ? cryptoInfo.PaymentUrls.BIP21 :
|
||||
@ -249,7 +285,6 @@ namespace BTCPayServer.Controllers
|
||||
TxCount = accounting.TxRequired,
|
||||
BtcPaid = accounting.Paid.ToString(),
|
||||
Status = invoice.Status,
|
||||
CryptoImage = "/" + GetImage(paymentMethodId, network),
|
||||
NetworkFee = paymentMethodDetails.GetTxFee(),
|
||||
IsMultiCurrency = invoice.GetPayments().Select(p => p.GetPaymentMethodId()).Concat(new[] { paymentMethod.GetId() }).Distinct().Count() > 1,
|
||||
AllowCoinConversion = storeBlob.AllowCoinConversion,
|
||||
@ -258,10 +293,14 @@ namespace BTCPayServer.Controllers
|
||||
.Select(kv => new PaymentModel.AvailableCrypto()
|
||||
{
|
||||
PaymentMethodId = kv.GetId().ToString(),
|
||||
CryptoImage = "/" + GetImage(kv.GetId(), kv.Network),
|
||||
CryptoCode = kv.GetId().CryptoCode,
|
||||
PaymentMethodName = GetDisplayName(kv.GetId(), kv.Network),
|
||||
IsLightning = kv.GetId().PaymentType == PaymentTypes.LightningLike,
|
||||
CryptoImage = GetImage(kv.GetId(), kv.Network),
|
||||
Link = Url.Action(nameof(Checkout), new { invoiceId = invoiceId, paymentMethodId = kv.GetId().ToString() })
|
||||
}).Where(c => c.CryptoImage != "/")
|
||||
.ToList()
|
||||
.OrderByDescending(a => a.CryptoCode == "BTC").ThenBy(a => a.PaymentMethodName).ThenBy(a => a.IsLightning ? 1 : 0)
|
||||
.ToList()
|
||||
};
|
||||
|
||||
var expiration = TimeSpan.FromSeconds(model.ExpirationSeconds);
|
||||
@ -269,19 +308,58 @@ namespace BTCPayServer.Controllers
|
||||
return model;
|
||||
}
|
||||
|
||||
private string GetImage(PaymentMethodId paymentMethodId, BTCPayNetwork network)
|
||||
private string GetDisplayName(PaymentMethodId paymentMethodId, BTCPayNetwork network)
|
||||
{
|
||||
return (paymentMethodId.PaymentType == PaymentTypes.BTCLike ? Url.Content(network.CryptoImagePath) : Url.Content(network.LightningImagePath));
|
||||
return paymentMethodId.PaymentType == PaymentTypes.BTCLike ?
|
||||
network.DisplayName : network.DisplayName + " (via Lightning)";
|
||||
}
|
||||
|
||||
private string FormatCurrency(PaymentMethod paymentMethod)
|
||||
private string GetImage(PaymentMethodId paymentMethodId, BTCPayNetwork network)
|
||||
{
|
||||
var res = paymentMethodId.PaymentType == PaymentTypes.BTCLike ?
|
||||
Url.Content(network.CryptoImagePath) : Url.Content(network.LightningImagePath);
|
||||
return "/" + res;
|
||||
}
|
||||
|
||||
private string OrderAmountFromInvoice(string cryptoCode, ProductInformation productInformation)
|
||||
{
|
||||
// if invoice source currency is the same as currently display currency, no need for "order amount from invoice"
|
||||
if (cryptoCode == productInformation.Currency)
|
||||
return null;
|
||||
|
||||
return FormatCurrency(productInformation.Price, productInformation.Currency, _CurrencyNameTable);
|
||||
}
|
||||
private string ExchangeRate(PaymentMethod paymentMethod)
|
||||
{
|
||||
string currency = paymentMethod.ParentEntity.ProductInformation.Currency;
|
||||
return FormatCurrency(paymentMethod.Rate, currency);
|
||||
return FormatCurrency(paymentMethod.Rate, currency, _CurrencyNameTable);
|
||||
}
|
||||
public string FormatCurrency(decimal price, string currency)
|
||||
|
||||
public static string FormatCurrency(decimal price, string currency, CurrencyNameTable currencies)
|
||||
{
|
||||
return price.ToString("C", _CurrencyNameTable.GetCurrencyProvider(currency)) + $" ({currency})";
|
||||
var provider = currencies.GetNumberFormatInfo(currency, true);
|
||||
var currencyData = currencies.GetCurrencyData(currency, true);
|
||||
var divisibility = currencyData.Divisibility;
|
||||
while (true)
|
||||
{
|
||||
var rounded = decimal.Round(price, divisibility, MidpointRounding.AwayFromZero);
|
||||
if ((Math.Abs(rounded - price) / price) < 0.001m)
|
||||
{
|
||||
price = rounded;
|
||||
break;
|
||||
}
|
||||
divisibility++;
|
||||
}
|
||||
if (divisibility != provider.CurrencyDecimalDigits)
|
||||
{
|
||||
provider = (NumberFormatInfo)provider.Clone();
|
||||
provider.CurrencyDecimalDigits = divisibility;
|
||||
}
|
||||
|
||||
if (currencyData.Crypto)
|
||||
return price.ToString("C", provider);
|
||||
else
|
||||
return price.ToString("C", provider) + $" ({currency})";
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
@ -310,7 +388,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
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)));
|
||||
leases.Add(_EventAggregator.Subscribe<Events.InvoiceEvent>(async o => await NotifySocket(webSocket, o.Invoice.Id, invoiceId)));
|
||||
while (true)
|
||||
{
|
||||
var message = await webSocket.ReceiveAsync(DummyBuffer, default(CancellationToken));
|
||||
@ -354,7 +432,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
[HttpGet]
|
||||
[Route("invoices")]
|
||||
[Authorize(AuthenticationSchemes = "Identity.Application")]
|
||||
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
||||
[BitpayAPIConstraint(false)]
|
||||
public async Task<IActionResult> ListInvoices(string searchTerm = null, int skip = 0, int count = 50)
|
||||
{
|
||||
@ -366,15 +444,20 @@ namespace BTCPayServer.Controllers
|
||||
Count = count,
|
||||
Skip = skip,
|
||||
UserId = GetUserId(),
|
||||
Status = filterString.Filters.TryGet("status"),
|
||||
StoreId = filterString.Filters.TryGet("storeid")
|
||||
Unusual = !filterString.Filters.ContainsKey("unusual") ? null
|
||||
: !bool.TryParse(filterString.Filters["unusual"].First(), out var r) ? (bool?)null
|
||||
: r,
|
||||
Status = filterString.Filters.ContainsKey("status") ? filterString.Filters["status"].ToArray() : null,
|
||||
ExceptionStatus = filterString.Filters.ContainsKey("exceptionstatus") ? filterString.Filters["exceptionstatus"].ToArray() : null,
|
||||
StoreId = filterString.Filters.ContainsKey("storeid") ? filterString.Filters["storeid"].ToArray() : null
|
||||
}))
|
||||
{
|
||||
model.SearchTerm = searchTerm;
|
||||
model.Invoices.Add(new InvoiceModel()
|
||||
{
|
||||
Status = invoice.Status,
|
||||
Date = (DateTimeOffset.UtcNow - invoice.InvoiceTime).Prettify() + " ago",
|
||||
Status = invoice.Status + (invoice.ExceptionStatus == null ? string.Empty : $" ({invoice.ExceptionStatus})"),
|
||||
ShowCheckout = invoice.Status == "new",
|
||||
Date = invoice.InvoiceTime,
|
||||
InvoiceId = invoice.Id,
|
||||
OrderId = invoice.OrderId ?? string.Empty,
|
||||
RedirectUrl = invoice.RedirectURL ?? string.Empty,
|
||||
@ -389,11 +472,11 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
[HttpGet]
|
||||
[Route("invoices/create")]
|
||||
[Authorize(AuthenticationSchemes = "Identity.Application")]
|
||||
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
||||
[BitpayAPIConstraint(false)]
|
||||
public async Task<IActionResult> CreateInvoice()
|
||||
{
|
||||
var stores = await GetStores(GetUserId());
|
||||
var stores = new SelectList(await _StoreRepository.GetStoresByUserId(GetUserId()), nameof(StoreData.Id), nameof(StoreData.StoreName), null);
|
||||
if (stores.Count() == 0)
|
||||
{
|
||||
StatusMessage = "Error: You need to create at least one store before creating a transaction";
|
||||
@ -404,18 +487,23 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
[HttpPost]
|
||||
[Route("invoices/create")]
|
||||
[Authorize(AuthenticationSchemes = "Identity.Application")]
|
||||
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
||||
[BitpayAPIConstraint(false)]
|
||||
public async Task<IActionResult> CreateInvoice(CreateInvoiceModel model)
|
||||
{
|
||||
model.Stores = await GetStores(GetUserId(), model.StoreId);
|
||||
var stores = await _StoreRepository.GetStoresByUserId(GetUserId());
|
||||
model.Stores = new SelectList(stores, nameof(StoreData.Id), nameof(StoreData.StoreName), model.StoreId);
|
||||
var store = stores.FirstOrDefault(s => s.Id == model.StoreId);
|
||||
if (store == null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.StoreId), "Store not found");
|
||||
}
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(model);
|
||||
}
|
||||
var store = await _StoreRepository.FindStore(model.StoreId, GetUserId());
|
||||
StatusMessage = null;
|
||||
if (store.Role != StoreRoles.Owner)
|
||||
if (!store.HasClaim(Policies.CanModifyStoreSettings.Key))
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.StoreId), "You need to be owner of this store to create an invoice");
|
||||
return View(model);
|
||||
@ -453,20 +541,15 @@ namespace BTCPayServer.Controllers
|
||||
StatusMessage = $"Invoice {result.Data.Id} just created!";
|
||||
return RedirectToAction(nameof(ListInvoices));
|
||||
}
|
||||
catch (RateUnavailableException)
|
||||
catch (BitpayHttpException ex)
|
||||
{
|
||||
ModelState.TryAddModelError(nameof(model.Currency), "Unsupported currency");
|
||||
ModelState.TryAddModelError(nameof(model.Currency), $"Error: {ex.Message}");
|
||||
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")]
|
||||
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
||||
[BitpayAPIConstraint(false)]
|
||||
public IActionResult SearchInvoice(InvoicesModel invoices)
|
||||
{
|
||||
@ -480,12 +563,15 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
[HttpPost]
|
||||
[Route("invoices/invalidatepaid")]
|
||||
[Authorize(AuthenticationSchemes = "Identity.Application")]
|
||||
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
||||
[BitpayAPIConstraint(false)]
|
||||
public async Task<IActionResult> InvalidatePaidInvoice(string invoiceId)
|
||||
{
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
|
||||
if (invoice == null)
|
||||
return NotFound();
|
||||
await _InvoiceRepository.UpdatePaidInvoiceToInvalid(invoiceId);
|
||||
_EventAggregator.Publish(new InvoiceEvent(invoiceId, 1008, "invoice_markedInvalid"));
|
||||
_EventAggregator.Publish(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1008, "invoice_markedInvalid"));
|
||||
return RedirectToAction(nameof(ListInvoices));
|
||||
}
|
||||
|
||||
|
@ -40,13 +40,16 @@ using NBXplorer.DerivationStrategy;
|
||||
using NBXplorer;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Security;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public partial class InvoiceController : Controller
|
||||
{
|
||||
InvoiceRepository _InvoiceRepository;
|
||||
IRateProviderFactory _RateProviders;
|
||||
ContentSecurityPolicies _CSP;
|
||||
BTCPayRateProviderFactory _RateProvider;
|
||||
StoreRepository _StoreRepository;
|
||||
UserManager<ApplicationUser> _UserManager;
|
||||
private CurrencyNameTable _CurrencyNameTable;
|
||||
@ -59,26 +62,30 @@ namespace BTCPayServer.Controllers
|
||||
InvoiceRepository invoiceRepository,
|
||||
CurrencyNameTable currencyNameTable,
|
||||
UserManager<ApplicationUser> userManager,
|
||||
IRateProviderFactory rateProviders,
|
||||
BTCPayRateProviderFactory rateProvider,
|
||||
StoreRepository storeRepository,
|
||||
EventAggregator eventAggregator,
|
||||
BTCPayWalletProvider walletProvider,
|
||||
ContentSecurityPolicies csp,
|
||||
BTCPayNetworkProvider networkProvider)
|
||||
{
|
||||
_ServiceProvider = serviceProvider;
|
||||
_CurrencyNameTable = currencyNameTable ?? throw new ArgumentNullException(nameof(currencyNameTable));
|
||||
_StoreRepository = storeRepository ?? throw new ArgumentNullException(nameof(storeRepository));
|
||||
_InvoiceRepository = invoiceRepository ?? throw new ArgumentNullException(nameof(invoiceRepository));
|
||||
_RateProviders = rateProviders ?? throw new ArgumentNullException(nameof(rateProviders));
|
||||
_RateProvider = rateProvider ?? throw new ArgumentNullException(nameof(rateProvider));
|
||||
_UserManager = userManager;
|
||||
_EventAggregator = eventAggregator;
|
||||
_NetworkProvider = networkProvider;
|
||||
_WalletProvider = walletProvider;
|
||||
_CSP = csp;
|
||||
}
|
||||
|
||||
|
||||
internal async Task<DataWrapper<InvoiceResponse>> CreateInvoiceCore(Invoice invoice, StoreData store, string serverUrl)
|
||||
{
|
||||
InvoiceLogs logs = new InvoiceLogs();
|
||||
logs.Write("Creation of invoice starting");
|
||||
var entity = new InvoiceEntity
|
||||
{
|
||||
InvoiceTime = DateTimeOffset.UtcNow
|
||||
@ -97,6 +104,7 @@ namespace BTCPayServer.Controllers
|
||||
entity.ExtendedNotifications = invoice.ExtendedNotifications;
|
||||
entity.NotificationURL = notificationUri?.AbsoluteUri;
|
||||
entity.BuyerInformation = Map<Invoice, BuyerInformation>(invoice);
|
||||
entity.PaymentTolerance = storeBlob.PaymentTolerance;
|
||||
//Another way of passing buyer info to support
|
||||
FillBuyerInfo(invoice.Buyer, entity.BuyerInformation);
|
||||
if (entity?.BuyerInformation?.BuyerEmail != null)
|
||||
@ -107,11 +115,33 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
entity.ProductInformation = Map<Invoice, ProductInformation>(invoice);
|
||||
entity.RedirectURL = invoice.RedirectURL ?? store.StoreWebsite;
|
||||
if (!Uri.IsWellFormedUriString(entity.RedirectURL, UriKind.Absolute))
|
||||
entity.RedirectURL = null;
|
||||
|
||||
entity.Status = "new";
|
||||
entity.SpeedPolicy = ParseSpeedPolicy(invoice.TransactionSpeed, store.SpeedPolicy);
|
||||
|
||||
HashSet<CurrencyPair> currencyPairsToFetch = new HashSet<CurrencyPair>();
|
||||
var rules = storeBlob.GetRateRules(_NetworkProvider);
|
||||
var excludeFilter = storeBlob.GetExcludedPaymentMethods(); // Here we can compose filters from other origin with PaymentFilter.Any()
|
||||
foreach (var network in store.GetSupportedPaymentMethods(_NetworkProvider)
|
||||
.Where(s => !excludeFilter.Match(s.PaymentId))
|
||||
.Select(c => _NetworkProvider.GetNetwork(c.PaymentId.CryptoCode))
|
||||
.Where(c => c != null))
|
||||
{
|
||||
currencyPairsToFetch.Add(new CurrencyPair(network.CryptoCode, invoice.Currency));
|
||||
if (storeBlob.LightningMaxValue != null)
|
||||
currencyPairsToFetch.Add(new CurrencyPair(network.CryptoCode, storeBlob.LightningMaxValue.Currency));
|
||||
if (storeBlob.OnChainMinValue != null)
|
||||
currencyPairsToFetch.Add(new CurrencyPair(network.CryptoCode, storeBlob.OnChainMinValue.Currency));
|
||||
}
|
||||
|
||||
var rateRules = storeBlob.GetRateRules(_NetworkProvider);
|
||||
var fetchingByCurrencyPair = _RateProvider.FetchRates(currencyPairsToFetch, rateRules);
|
||||
|
||||
var fetchingAll = WhenAllFetched(logs, fetchingByCurrencyPair);
|
||||
var supportedPaymentMethods = store.GetSupportedPaymentMethods(_NetworkProvider)
|
||||
.Where(s => !excludeFilter.Match(s.PaymentId))
|
||||
.Select(c =>
|
||||
(Handler: (IPaymentMethodHandler)_ServiceProvider.GetService(typeof(IPaymentMethodHandler<>).MakeGenericType(c.GetType())),
|
||||
SupportedPaymentMethod: c,
|
||||
@ -119,136 +149,136 @@ namespace BTCPayServer.Controllers
|
||||
.Where(c => c.Network != null)
|
||||
.Select(o =>
|
||||
(SupportedPaymentMethod: o.SupportedPaymentMethod,
|
||||
PaymentMethod: CreatePaymentMethodAsync(o.Handler, o.SupportedPaymentMethod, o.Network, entity, store)))
|
||||
PaymentMethod: CreatePaymentMethodAsync(fetchingByCurrencyPair, o.Handler, o.SupportedPaymentMethod, o.Network, entity, store, logs)))
|
||||
.ToList();
|
||||
|
||||
List<string> paymentMethodErrors = new List<string>();
|
||||
List<ISupportedPaymentMethod> supported = new List<ISupportedPaymentMethod>();
|
||||
var paymentMethods = new PaymentMethodDictionary();
|
||||
foreach (var o in supportedPaymentMethods)
|
||||
{
|
||||
try
|
||||
{
|
||||
var paymentMethod = await o.PaymentMethod;
|
||||
if (paymentMethod == null)
|
||||
throw new PaymentMethodUnavailableException("Payment method unavailable (The handler returned null)");
|
||||
supported.Add(o.SupportedPaymentMethod);
|
||||
paymentMethods.Add(paymentMethod);
|
||||
}
|
||||
catch (PaymentMethodUnavailableException ex)
|
||||
{
|
||||
paymentMethodErrors.Add($"{o.SupportedPaymentMethod.PaymentId.CryptoCode} ({o.SupportedPaymentMethod.PaymentId.PaymentType}): Payment method unavailable ({ex.Message})");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
paymentMethodErrors.Add($"{o.SupportedPaymentMethod.PaymentId.CryptoCode} ({o.SupportedPaymentMethod.PaymentId.PaymentType}): Unexpected exception ({ex.ToString()})");
|
||||
}
|
||||
var paymentMethod = await o.PaymentMethod;
|
||||
if (paymentMethod == null)
|
||||
continue;
|
||||
supported.Add(o.SupportedPaymentMethod);
|
||||
paymentMethods.Add(paymentMethod);
|
||||
}
|
||||
|
||||
if (supported.Count == 0)
|
||||
{
|
||||
StringBuilder errors = new StringBuilder();
|
||||
errors.AppendLine("No payment method available for this store");
|
||||
foreach (var error in paymentMethodErrors)
|
||||
foreach (var error in logs.ToList())
|
||||
{
|
||||
errors.AppendLine(error);
|
||||
errors.AppendLine(error.ToString());
|
||||
}
|
||||
throw new BitpayHttpException(400, errors.ToString());
|
||||
}
|
||||
|
||||
entity.SetSupportedPaymentMethods(supported);
|
||||
entity.SetPaymentMethods(paymentMethods);
|
||||
#pragma warning disable CS0618
|
||||
// Legacy Bitpay clients expect information for BTC information, even if the store do not support it
|
||||
var legacyBTCisSet = paymentMethods.Any(p => p.GetId().IsBTCOnChain);
|
||||
if (!legacyBTCisSet && _NetworkProvider.BTC != null)
|
||||
{
|
||||
var btc = _NetworkProvider.BTC;
|
||||
var feeProvider = ((IFeeProviderFactory)_ServiceProvider.GetService(typeof(IFeeProviderFactory))).CreateFeeProvider(btc);
|
||||
var rateProvider = _RateProviders.GetRateProvider(btc, storeBlob.GetRateRules());
|
||||
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.PosData = invoice.PosData;
|
||||
entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity, paymentMethodErrors, _NetworkProvider);
|
||||
|
||||
_EventAggregator.Publish(new Events.InvoiceEvent(entity, 1001, "invoice_created"));
|
||||
entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity, logs, _NetworkProvider);
|
||||
await fetchingAll;
|
||||
_EventAggregator.Publish(new Events.InvoiceEvent(entity.EntityToDTO(_NetworkProvider), 1001, "invoice_created"));
|
||||
var resp = entity.EntityToDTO(_NetworkProvider);
|
||||
return new DataWrapper<InvoiceResponse>(resp) { Facade = "pos/invoice" };
|
||||
}
|
||||
|
||||
private async Task<PaymentMethod> CreatePaymentMethodAsync(IPaymentMethodHandler handler, ISupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network, InvoiceEntity entity, StoreData store)
|
||||
private Task WhenAllFetched(InvoiceLogs logs, Dictionary<CurrencyPair, Task<RateResult>> fetchingByCurrencyPair)
|
||||
{
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
var rate = await _RateProviders.GetRateProvider(network, storeBlob.GetRateRules()).GetRateAsync(entity.ProductInformation.Currency);
|
||||
PaymentMethod paymentMethod = new PaymentMethod();
|
||||
paymentMethod.ParentEntity = entity;
|
||||
paymentMethod.Network = network;
|
||||
paymentMethod.SetId(supportedPaymentMethod.PaymentId);
|
||||
paymentMethod.Rate = rate;
|
||||
var paymentDetails = await handler.CreatePaymentMethodDetails(supportedPaymentMethod, paymentMethod, store, network);
|
||||
if (storeBlob.NetworkFeeDisabled)
|
||||
paymentDetails.SetNoTxFee();
|
||||
paymentMethod.SetPaymentMethodDetails(paymentDetails);
|
||||
|
||||
Func<Money, Money, bool> compare = null;
|
||||
CurrencyValue limitValue = null;
|
||||
string errorMessage = null;
|
||||
if (supportedPaymentMethod.PaymentId.PaymentType == PaymentTypes.LightningLike &&
|
||||
storeBlob.LightningMaxValue != null)
|
||||
return Task.WhenAll(fetchingByCurrencyPair.Select(async pair =>
|
||||
{
|
||||
compare = (a, b) => a > b;
|
||||
limitValue = storeBlob.LightningMaxValue;
|
||||
errorMessage = "The amount of the invoice is too high to be paid with lightning";
|
||||
}
|
||||
else if (supportedPaymentMethod.PaymentId.PaymentType == PaymentTypes.BTCLike &&
|
||||
storeBlob.OnChainMinValue != null)
|
||||
{
|
||||
compare = (a, b) => a < b;
|
||||
limitValue = storeBlob.OnChainMinValue;
|
||||
errorMessage = "The amount of the invoice is too low to be paid on chain";
|
||||
}
|
||||
|
||||
if (compare != null)
|
||||
{
|
||||
var limitValueRate = 0.0m;
|
||||
if (limitValue.Currency == entity.ProductInformation.Currency)
|
||||
limitValueRate = paymentMethod.Rate;
|
||||
else
|
||||
limitValueRate = await _RateProviders.GetRateProvider(network, storeBlob.GetRateRules()).GetRateAsync(limitValue.Currency);
|
||||
|
||||
var limitValueCrypto = Money.Coins(limitValue.Value / limitValueRate);
|
||||
if (compare(paymentMethod.Calculate().Due, limitValueCrypto))
|
||||
var rateResult = await pair.Value;
|
||||
logs.Write($"{pair.Key}: The rating rule is {rateResult.Rule}");
|
||||
logs.Write($"{pair.Key}: The evaluated rating rule is {rateResult.EvaluatedRule}");
|
||||
if (rateResult.Errors.Count != 0)
|
||||
{
|
||||
throw new PaymentMethodUnavailableException(errorMessage);
|
||||
var allRateRuleErrors = string.Join(", ", rateResult.Errors.ToArray());
|
||||
logs.Write($"{pair.Key}: Rate rule error ({allRateRuleErrors})");
|
||||
}
|
||||
}
|
||||
///////////////
|
||||
|
||||
|
||||
#pragma warning disable CS0618
|
||||
if (paymentMethod.GetId().IsBTCOnChain)
|
||||
{
|
||||
entity.TxFee = paymentMethod.TxFee;
|
||||
entity.Rate = paymentMethod.Rate;
|
||||
entity.DepositAddress = paymentMethod.DepositAddress;
|
||||
}
|
||||
#pragma warning restore CS0618
|
||||
return paymentMethod;
|
||||
if (rateResult.ExchangeExceptions.Count != 0)
|
||||
{
|
||||
foreach (var ex in rateResult.ExchangeExceptions)
|
||||
{
|
||||
logs.Write($"{pair.Key}: Exception reaching exchange {ex.ExchangeName} ({ex.Exception.Message})");
|
||||
}
|
||||
}
|
||||
}).ToArray());
|
||||
}
|
||||
|
||||
#pragma warning disable CS0618
|
||||
private static Money GetTxFee(StoreBlob storeBlob, FeeRate feeRate)
|
||||
private async Task<PaymentMethod> CreatePaymentMethodAsync(Dictionary<CurrencyPair, Task<RateResult>> fetchingByCurrencyPair, IPaymentMethodHandler handler, ISupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network, InvoiceEntity entity, StoreData store, InvoiceLogs logs)
|
||||
{
|
||||
return storeBlob.NetworkFeeDisabled ? Money.Zero : feeRate.GetFee(100);
|
||||
}
|
||||
try
|
||||
{
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
var rate = await fetchingByCurrencyPair[new CurrencyPair(network.CryptoCode, entity.ProductInformation.Currency)];
|
||||
if (rate.BidAsk == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
PaymentMethod paymentMethod = new PaymentMethod();
|
||||
paymentMethod.ParentEntity = entity;
|
||||
paymentMethod.Network = network;
|
||||
paymentMethod.SetId(supportedPaymentMethod.PaymentId);
|
||||
paymentMethod.Rate = rate.BidAsk.Bid;
|
||||
var paymentDetails = await handler.CreatePaymentMethodDetails(supportedPaymentMethod, paymentMethod, store, network);
|
||||
if (storeBlob.NetworkFeeDisabled)
|
||||
paymentDetails.SetNoTxFee();
|
||||
paymentMethod.SetPaymentMethodDetails(paymentDetails);
|
||||
|
||||
Func<Money, Money, bool> compare = null;
|
||||
CurrencyValue limitValue = null;
|
||||
string errorMessage = null;
|
||||
if (supportedPaymentMethod.PaymentId.PaymentType == PaymentTypes.LightningLike &&
|
||||
storeBlob.LightningMaxValue != null)
|
||||
{
|
||||
compare = (a, b) => a > b;
|
||||
limitValue = storeBlob.LightningMaxValue;
|
||||
errorMessage = "The amount of the invoice is too high to be paid with lightning";
|
||||
}
|
||||
else if (supportedPaymentMethod.PaymentId.PaymentType == PaymentTypes.BTCLike &&
|
||||
storeBlob.OnChainMinValue != null)
|
||||
{
|
||||
compare = (a, b) => a < b;
|
||||
limitValue = storeBlob.OnChainMinValue;
|
||||
errorMessage = "The amount of the invoice is too low to be paid on chain";
|
||||
}
|
||||
|
||||
if (compare != null)
|
||||
{
|
||||
var limitValueRate = await fetchingByCurrencyPair[new CurrencyPair(network.CryptoCode, limitValue.Currency)];
|
||||
if (limitValueRate.BidAsk != null)
|
||||
{
|
||||
var limitValueCrypto = Money.Coins(limitValue.Value / limitValueRate.BidAsk.Bid);
|
||||
if (compare(paymentMethod.Calculate().Due, limitValueCrypto))
|
||||
{
|
||||
logs.Write($"{supportedPaymentMethod.PaymentId.CryptoCode}: {errorMessage}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
///////////////
|
||||
|
||||
|
||||
#pragma warning disable CS0618
|
||||
if (paymentMethod.GetId().IsBTCOnChain)
|
||||
{
|
||||
entity.TxFee = paymentMethod.TxFee;
|
||||
entity.Rate = paymentMethod.Rate;
|
||||
entity.DepositAddress = paymentMethod.DepositAddress;
|
||||
}
|
||||
#pragma warning restore CS0618
|
||||
return paymentMethod;
|
||||
}
|
||||
catch (PaymentMethodUnavailableException ex)
|
||||
{
|
||||
logs.Write($"{supportedPaymentMethod.PaymentId.CryptoCode}: Payment method unavailable ({ex.Message})");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logs.Write($"{supportedPaymentMethod.PaymentId.CryptoCode}: Unexpected exception ({ex.ToString()})");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private SpeedPolicy ParseSpeedPolicy(string transactionSpeed, SpeedPolicy defaultPolicy)
|
||||
{
|
||||
@ -256,6 +286,7 @@ namespace BTCPayServer.Controllers
|
||||
return defaultPolicy;
|
||||
var mappings = new Dictionary<string, SpeedPolicy>();
|
||||
mappings.Add("low", SpeedPolicy.LowSpeed);
|
||||
mappings.Add("low-medium", SpeedPolicy.LowMediumSpeed);
|
||||
mappings.Add("medium", SpeedPolicy.MediumSpeed);
|
||||
mappings.Add("high", SpeedPolicy.HighSpeed);
|
||||
if (!mappings.TryGetValue(transactionSpeed, out SpeedPolicy policy))
|
||||
|
@ -21,10 +21,11 @@ using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using BTCPayServer.Services.Mails;
|
||||
using System.Globalization;
|
||||
using BTCPayServer.Security;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
[Authorize]
|
||||
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
||||
[Route("[controller]/[action]")]
|
||||
public class ManageController : Controller
|
||||
{
|
||||
|
@ -8,17 +8,19 @@ using System.Threading.Tasks;
|
||||
using BTCPayServer.Filters;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Rating;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public class RateController : Controller
|
||||
{
|
||||
IRateProviderFactory _RateProviderFactory;
|
||||
BTCPayRateProviderFactory _RateProviderFactory;
|
||||
BTCPayNetworkProvider _NetworkProvider;
|
||||
CurrencyNameTable _CurrencyNameTable;
|
||||
StoreRepository _StoreRepo;
|
||||
public RateController(
|
||||
IRateProviderFactory rateProviderFactory,
|
||||
BTCPayRateProviderFactory rateProviderFactory,
|
||||
BTCPayNetworkProvider networkProvider,
|
||||
StoreRepository storeRepo,
|
||||
CurrencyNameTable currencyNameTable)
|
||||
@ -29,48 +31,179 @@ namespace BTCPayServer.Controllers
|
||||
_CurrencyNameTable = currencyNameTable ?? throw new ArgumentNullException(nameof(currencyNameTable));
|
||||
}
|
||||
|
||||
[Route("rates/{baseCurrency}")]
|
||||
[HttpGet]
|
||||
[BitpayAPIConstraint]
|
||||
public async Task<IActionResult> GetBaseCurrencyRates(string baseCurrency, string storeId)
|
||||
{
|
||||
storeId = storeId ?? this.HttpContext.GetStoreData()?.Id;
|
||||
var store = this.HttpContext.GetStoreData();
|
||||
if (store == null || store.Id != storeId)
|
||||
store = await _StoreRepo.FindStore(storeId);
|
||||
if (store == null)
|
||||
{
|
||||
var err = Json(new BitpayErrorsModel() { Error = "Store not found" });
|
||||
err.StatusCode = 404;
|
||||
return err;
|
||||
}
|
||||
var supportedMethods = store.GetSupportedPaymentMethods(_NetworkProvider);
|
||||
|
||||
var currencyCodes = supportedMethods.Where(method => !string.IsNullOrEmpty(method.PaymentId.CryptoCode))
|
||||
.Select(method => method.PaymentId.CryptoCode).Distinct();
|
||||
|
||||
var currencypairs = BuildCurrencyPairs(currencyCodes, baseCurrency);
|
||||
|
||||
var result = await GetRates2(currencypairs, store.Id);
|
||||
var rates = (result as JsonResult)?.Value as Rate[];
|
||||
if (rates == null)
|
||||
return result;
|
||||
return Json(new DataWrapper<Rate[]>(rates));
|
||||
}
|
||||
|
||||
|
||||
[Route("rates/{baseCurrency}/{currency}")]
|
||||
[HttpGet]
|
||||
[BitpayAPIConstraint]
|
||||
public async Task<IActionResult> GetCurrencyPairRate(string baseCurrency, string currency, string storeId)
|
||||
{
|
||||
storeId = storeId ?? this.HttpContext.GetStoreData()?.Id;
|
||||
var result = await GetRates2($"{baseCurrency}_{currency}", storeId);
|
||||
var rates = (result as JsonResult)?.Value as Rate[];
|
||||
if (rates == null)
|
||||
return result;
|
||||
return Json(new DataWrapper<Rate>(rates.First()));
|
||||
}
|
||||
|
||||
[Route("rates")]
|
||||
[HttpGet]
|
||||
[BitpayAPIConstraint]
|
||||
public async Task<IActionResult> GetRates(string cryptoCode = null, string storeId = null)
|
||||
public async Task<IActionResult> GetRates(string currencyPairs, string storeId)
|
||||
{
|
||||
var result = await GetRates2(cryptoCode, storeId);
|
||||
var rates = (result as JsonResult)?.Value as NBitpayClient.Rate[];
|
||||
if(rates == null)
|
||||
storeId = storeId ?? this.HttpContext.GetStoreData()?.Id;
|
||||
var result = await GetRates2(currencyPairs, storeId);
|
||||
var rates = (result as JsonResult)?.Value as Rate[];
|
||||
if (rates == null)
|
||||
return result;
|
||||
return Json(new DataWrapper<NBitpayClient.Rate[]>(rates));
|
||||
return Json(new DataWrapper<Rate[]>(rates));
|
||||
}
|
||||
|
||||
|
||||
[Route("api/rates")]
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GetRates2(string cryptoCode = null, string storeId = null)
|
||||
public async Task<IActionResult> GetRates2(string currencyPairs, string storeId)
|
||||
{
|
||||
cryptoCode = cryptoCode ?? "BTC";
|
||||
var network = _NetworkProvider.GetNetwork(cryptoCode);
|
||||
if (network == null)
|
||||
return NotFound();
|
||||
|
||||
RateRules rules = null;
|
||||
if (storeId != null)
|
||||
if (storeId == null)
|
||||
{
|
||||
var store = await _StoreRepo.FindStore(storeId);
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
rules = store.GetStoreBlob().GetRateRules();
|
||||
var result = Json(new BitpayErrorsModel() { Error = "You need to specify storeId (in your store settings)" });
|
||||
result.StatusCode = 400;
|
||||
return result;
|
||||
}
|
||||
var store = this.HttpContext.GetStoreData();
|
||||
if (store == null || store.Id != storeId)
|
||||
store = await _StoreRepo.FindStore(storeId);
|
||||
if (store == null)
|
||||
{
|
||||
var result = Json(new BitpayErrorsModel() { Error = "Store not found" });
|
||||
result.StatusCode = 404;
|
||||
return result;
|
||||
}
|
||||
|
||||
var rateProvider = _RateProviderFactory.GetRateProvider(network, rules);
|
||||
if (rateProvider == null)
|
||||
return NotFound();
|
||||
if (currencyPairs == null)
|
||||
{
|
||||
var supportedMethods = store.GetSupportedPaymentMethods(_NetworkProvider);
|
||||
var currencyCodes = supportedMethods.Select(method => method.PaymentId.CryptoCode).Distinct();
|
||||
var defaultCrypto = store.GetDefaultCrypto(_NetworkProvider);
|
||||
|
||||
var allRates = (await rateProvider.GetRatesAsync());
|
||||
return Json(allRates.Select(r =>
|
||||
new NBitpayClient.Rate()
|
||||
currencyPairs = BuildCurrencyPairs(currencyCodes, defaultCrypto);
|
||||
|
||||
if (string.IsNullOrEmpty(currencyPairs))
|
||||
{
|
||||
var result = Json(new BitpayErrorsModel() { Error = "You need to specify currencyPairs (eg. BTC_USD,LTC_CAD)" });
|
||||
result.StatusCode = 400;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var rules = store.GetStoreBlob().GetRateRules(_NetworkProvider);
|
||||
|
||||
HashSet<CurrencyPair> pairs = new HashSet<CurrencyPair>();
|
||||
foreach (var currency in currencyPairs.Split(','))
|
||||
{
|
||||
if (!CurrencyPair.TryParse(currency, out var pair))
|
||||
{
|
||||
var result = Json(new BitpayErrorsModel() { Error = $"Currency pair {currency} uncorrectly formatted" });
|
||||
result.StatusCode = 400;
|
||||
return result;
|
||||
}
|
||||
pairs.Add(pair);
|
||||
}
|
||||
|
||||
var fetching = _RateProviderFactory.FetchRates(pairs, rules);
|
||||
await Task.WhenAll(fetching.Select(f => f.Value).ToArray());
|
||||
return Json(pairs
|
||||
.Select(r => (Pair: r, Value: fetching[r].GetAwaiter().GetResult().BidAsk?.Bid))
|
||||
.Where(r => r.Value.HasValue)
|
||||
.Select(r =>
|
||||
new Rate()
|
||||
{
|
||||
Code = r.Currency,
|
||||
Name = _CurrencyNameTable.GetCurrencyData(r.Currency)?.Name,
|
||||
Value = r.Value
|
||||
CryptoCode = r.Pair.Left,
|
||||
Code = r.Pair.Right,
|
||||
CurrencyPair = r.Pair.ToString(),
|
||||
Name = _CurrencyNameTable.GetCurrencyData(r.Pair.Right, true).Name,
|
||||
Value = r.Value.Value
|
||||
}).Where(n => n.Name != null).ToArray());
|
||||
}
|
||||
|
||||
private static string BuildCurrencyPairs(IEnumerable<string> currencyCodes, string baseCrypto)
|
||||
{
|
||||
StringBuilder currencyPairsBuilder = new StringBuilder();
|
||||
bool first = true;
|
||||
foreach (var currencyCode in currencyCodes)
|
||||
{
|
||||
if(!first)
|
||||
currencyPairsBuilder.Append(",");
|
||||
first = false;
|
||||
currencyPairsBuilder.Append($"{baseCrypto}_{currencyCode}");
|
||||
}
|
||||
return currencyPairsBuilder.ToString();
|
||||
}
|
||||
|
||||
public class Rate
|
||||
{
|
||||
|
||||
[JsonProperty(PropertyName = "name")]
|
||||
public string Name
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
[JsonProperty(PropertyName = "cryptoCode")]
|
||||
public string CryptoCode
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[JsonProperty(PropertyName = "currencyPair")]
|
||||
public string CurrencyPair
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[JsonProperty(PropertyName = "code")]
|
||||
public string Code
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
[JsonProperty(PropertyName = "rate")]
|
||||
public decimal Value
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,13 +1,19 @@
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.ServerViewModels;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Mails;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Validations;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitcoin;
|
||||
using NBitcoin.DataEncoders;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
@ -16,26 +22,37 @@ using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Mail;
|
||||
using System.Threading.Tasks;
|
||||
using Renci.SshNet;
|
||||
using BTCPayServer.Logging;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
[Authorize(Roles = Roles.ServerAdmin)]
|
||||
[Authorize(Policy = BTCPayServer.Security.Policies.CanModifyServerSettings.Key)]
|
||||
public class ServerController : Controller
|
||||
{
|
||||
private UserManager<ApplicationUser> _UserManager;
|
||||
SettingsRepository _SettingsRepository;
|
||||
private IRateProviderFactory _RateProviderFactory;
|
||||
private CssThemeManager _CssThemeManager;
|
||||
private readonly NBXplorerDashboard _dashBoard;
|
||||
private BTCPayRateProviderFactory _RateProviderFactory;
|
||||
private StoreRepository _StoreRepository;
|
||||
LightningConfigurationProvider _LnConfigProvider;
|
||||
BTCPayServerOptions _Options;
|
||||
|
||||
public ServerController(UserManager<ApplicationUser> userManager,
|
||||
IRateProviderFactory rateProviderFactory,
|
||||
SettingsRepository settingsRepository,
|
||||
CssThemeManager cssThemeManager)
|
||||
Configuration.BTCPayServerOptions options,
|
||||
BTCPayRateProviderFactory rateProviderFactory,
|
||||
SettingsRepository settingsRepository,
|
||||
NBXplorerDashboard dashBoard,
|
||||
LightningConfigurationProvider lnConfigProvider,
|
||||
Services.Stores.StoreRepository storeRepository)
|
||||
{
|
||||
_Options = options;
|
||||
_UserManager = userManager;
|
||||
_SettingsRepository = settingsRepository;
|
||||
_dashBoard = dashBoard;
|
||||
_RateProviderFactory = rateProviderFactory;
|
||||
_CssThemeManager = cssThemeManager;
|
||||
_StoreRepository = storeRepository;
|
||||
_LnConfigProvider = lnConfigProvider;
|
||||
}
|
||||
|
||||
[Route("server/rates")]
|
||||
@ -77,7 +94,7 @@ namespace BTCPayServer.Controllers
|
||||
try
|
||||
{
|
||||
var service = GetCoinaverageService(vm, true);
|
||||
if(service != null)
|
||||
if (service != null)
|
||||
await service.TestAuthAsync();
|
||||
}
|
||||
catch
|
||||
@ -102,7 +119,7 @@ namespace BTCPayServer.Controllers
|
||||
};
|
||||
if (!withAuth || settings.GetCoinAverageSignature() != null)
|
||||
{
|
||||
return new CoinAverageRateProvider("BTC")
|
||||
return new CoinAverageRateProvider()
|
||||
{ Authenticator = settings };
|
||||
}
|
||||
return null;
|
||||
@ -137,6 +154,176 @@ namespace BTCPayServer.Controllers
|
||||
return View(userVM);
|
||||
}
|
||||
|
||||
[Route("server/maintenance")]
|
||||
public IActionResult Maintenance()
|
||||
{
|
||||
MaintenanceViewModel vm = new MaintenanceViewModel();
|
||||
vm.UserName = "btcpayserver";
|
||||
vm.DNSDomain = this.Request.Host.Host;
|
||||
vm.SetConfiguredSSH(_Options.SSHSettings);
|
||||
if (IPAddress.TryParse(vm.DNSDomain, out var unused))
|
||||
vm.DNSDomain = null;
|
||||
return View(vm);
|
||||
}
|
||||
[Route("server/maintenance")]
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> Maintenance(MaintenanceViewModel vm, string command)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
return View(vm);
|
||||
vm.SetConfiguredSSH(_Options.SSHSettings);
|
||||
if (command == "changedomain")
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(vm.DNSDomain))
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.DNSDomain), $"Required field");
|
||||
return View(vm);
|
||||
}
|
||||
vm.DNSDomain = vm.DNSDomain.Trim().ToLowerInvariant();
|
||||
if (IPAddress.TryParse(vm.DNSDomain, out var unused))
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.DNSDomain), $"This should be a domain name");
|
||||
return View(vm);
|
||||
}
|
||||
if (vm.DNSDomain.Equals(this.Request.Host.Host, StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.DNSDomain), $"The server is already set to use this domain");
|
||||
return View(vm);
|
||||
}
|
||||
var builder = new UriBuilder();
|
||||
using (var client = new HttpClient(new HttpClientHandler()
|
||||
{
|
||||
ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
|
||||
}))
|
||||
{
|
||||
try
|
||||
{
|
||||
builder.Scheme = this.Request.Scheme;
|
||||
builder.Host = vm.DNSDomain;
|
||||
if (this.Request.Host.Port != null)
|
||||
builder.Port = this.Request.Host.Port.Value;
|
||||
builder.Path = "runid";
|
||||
builder.Query = $"expected={RunId}";
|
||||
var response = await client.GetAsync(builder.Uri);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.DNSDomain), $"Invalid host ({vm.DNSDomain} is not pointing to this BTCPay instance)");
|
||||
return View(vm);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var messages = new List<object>();
|
||||
messages.Add(ex.Message);
|
||||
if (ex.InnerException != null)
|
||||
messages.Add(ex.InnerException.Message);
|
||||
ModelState.AddModelError(nameof(vm.DNSDomain), $"Invalid domain ({string.Join(", ", messages.ToArray())})");
|
||||
return View(vm);
|
||||
}
|
||||
}
|
||||
|
||||
var error = RunSSH(vm, $"changedomain.sh {vm.DNSDomain}");
|
||||
if (error != null)
|
||||
return error;
|
||||
|
||||
builder.Path = null;
|
||||
builder.Query = null;
|
||||
StatusMessage = $"Domain name changing... the server will restart, please use \"{builder.Uri.AbsoluteUri}\"";
|
||||
}
|
||||
else if (command == "update")
|
||||
{
|
||||
var error = RunSSH(vm, $"btcpay-update.sh");
|
||||
if (error != null)
|
||||
return error;
|
||||
StatusMessage = $"The server might restart soon if an update is available...";
|
||||
}
|
||||
else
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
return RedirectToAction(nameof(Maintenance));
|
||||
}
|
||||
|
||||
public static string RunId = Encoders.Hex.EncodeData(NBitcoin.RandomUtils.GetBytes(32));
|
||||
[HttpGet]
|
||||
[Route("runid")]
|
||||
[AllowAnonymous]
|
||||
public IActionResult SeeRunId(string expected = null)
|
||||
{
|
||||
if (expected == RunId)
|
||||
return Ok();
|
||||
return BadRequest();
|
||||
}
|
||||
|
||||
private IActionResult RunSSH(MaintenanceViewModel vm, string ssh)
|
||||
{
|
||||
ssh = $"sudo bash -c '. /etc/profile.d/btcpay-env.sh && nohup {ssh} > /dev/null 2>&1 & disown'";
|
||||
var sshClient = _Options.SSHSettings == null ? vm.CreateSSHClient(this.Request.Host.Host)
|
||||
: new SshClient(_Options.SSHSettings.CreateConnectionInfo());
|
||||
|
||||
if (_Options.TrustedFingerprints.Count != 0)
|
||||
{
|
||||
sshClient.HostKeyReceived += (object sender, Renci.SshNet.Common.HostKeyEventArgs e) =>
|
||||
{
|
||||
if (_Options.TrustedFingerprints.Count == 0)
|
||||
{
|
||||
Logs.Configuration.LogWarning($"SSH host fingerprint for {e.HostKey} is untrusted, start BTCPay with -sshtrustedfingerprints \"{Encoders.Hex.EncodeData(e.FingerPrint)}\"");
|
||||
e.CanTrust = true; // Not a typo, we want the connection to succeed with a warning
|
||||
}
|
||||
else
|
||||
{
|
||||
e.CanTrust = _Options.IsTrustedFingerprint(e.FingerPrint);
|
||||
if(!e.CanTrust)
|
||||
Logs.Configuration.LogError($"SSH host fingerprint for {e.HostKey} is untrusted, start BTCPay with -sshtrustedfingerprints \"{Encoders.Hex.EncodeData(e.FingerPrint)}\"");
|
||||
}
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
sshClient.Connect();
|
||||
}
|
||||
catch (Renci.SshNet.Common.SshAuthenticationException)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.Password), "Invalid credentials");
|
||||
sshClient.Dispose();
|
||||
return View(vm);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var message = ex.Message;
|
||||
if (ex is AggregateException aggrEx && aggrEx.InnerException?.Message != null)
|
||||
{
|
||||
message = aggrEx.InnerException.Message;
|
||||
}
|
||||
ModelState.AddModelError(nameof(vm.UserName), $"Connection problem ({message})");
|
||||
sshClient.Dispose();
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
var sshCommand = sshClient.CreateCommand(ssh);
|
||||
sshCommand.CommandTimeout = TimeSpan.FromMinutes(1.0);
|
||||
sshCommand.BeginExecute(ar =>
|
||||
{
|
||||
try
|
||||
{
|
||||
Logs.PayServer.LogInformation("Running SSH command: " + ssh);
|
||||
var result = sshCommand.EndExecute(ar);
|
||||
Logs.PayServer.LogInformation("SSH command executed: " + result);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logs.PayServer.LogWarning("Error while executing SSH command: " + ex.Message);
|
||||
}
|
||||
sshClient.Dispose();
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
private static bool IsAdmin(IList<string> roles)
|
||||
{
|
||||
return roles.Contains(Roles.ServerAdmin, StringComparer.Ordinal);
|
||||
@ -191,6 +378,7 @@ namespace BTCPayServer.Controllers
|
||||
if (user == null)
|
||||
return NotFound();
|
||||
await _UserManager.DeleteAsync(user);
|
||||
await _StoreRepository.CleanUnreachableStores();
|
||||
StatusMessage = "User deleted";
|
||||
return RedirectToAction(nameof(ListUsers));
|
||||
}
|
||||
@ -223,6 +411,150 @@ namespace BTCPayServer.Controllers
|
||||
return View(settings);
|
||||
}
|
||||
|
||||
[Route("server/services")]
|
||||
public IActionResult Services()
|
||||
{
|
||||
var result = new ServicesViewModel();
|
||||
foreach (var cryptoCode in _Options.ExternalServicesByCryptoCode.Keys)
|
||||
{
|
||||
{
|
||||
int i = 0;
|
||||
foreach (var grpcService in _Options.ExternalServicesByCryptoCode.GetServices<ExternalLNDGRPC>(cryptoCode))
|
||||
{
|
||||
result.LNDServices.Add(new ServicesViewModel.LNDServiceViewModel()
|
||||
{
|
||||
Crypto = cryptoCode,
|
||||
Type = "gRPC",
|
||||
Index = i++,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
result.HasSSH = _Options.SSHSettings != null;
|
||||
return View(result);
|
||||
}
|
||||
|
||||
[Route("server/services/lnd-grpc/{cryptoCode}/{index}")]
|
||||
public IActionResult LNDGRPCServices(string cryptoCode, int index, uint? nonce)
|
||||
{
|
||||
if (!_dashBoard.IsFullySynched(cryptoCode, out var unusud))
|
||||
{
|
||||
StatusMessage = $"Error: {cryptoCode} is not fully synched";
|
||||
return RedirectToAction(nameof(Services));
|
||||
}
|
||||
var external = GetExternalLNDConnectionString(cryptoCode, index);
|
||||
if (external == null)
|
||||
return NotFound();
|
||||
var model = new LNDGRPCServicesViewModel();
|
||||
|
||||
model.Host = $"{external.BaseUri.DnsSafeHost}:{external.BaseUri.Port}";
|
||||
model.SSL = external.BaseUri.Scheme == "https";
|
||||
if (external.CertificateThumbprint != null)
|
||||
{
|
||||
model.CertificateThumbprint = Encoders.Hex.EncodeData(external.CertificateThumbprint);
|
||||
}
|
||||
if (external.Macaroon != null)
|
||||
{
|
||||
model.Macaroon = Encoders.Hex.EncodeData(external.Macaroon);
|
||||
}
|
||||
|
||||
if (nonce != null)
|
||||
{
|
||||
var configKey = GetConfigKey("lnd-grpc", cryptoCode, index, nonce.Value);
|
||||
var lnConfig = _LnConfigProvider.GetConfig(configKey);
|
||||
if (lnConfig != null)
|
||||
{
|
||||
model.QRCodeLink = $"{this.Request.GetAbsoluteRoot().WithTrailingSlash()}lnd-config/{configKey}/lnd.config";
|
||||
model.QRCode = $"config={model.QRCodeLink}";
|
||||
}
|
||||
}
|
||||
|
||||
return View(model);
|
||||
}
|
||||
|
||||
private static uint GetConfigKey(string type, string cryptoCode, int index, uint nonce)
|
||||
{
|
||||
return (uint)HashCode.Combine(type, cryptoCode, index, nonce);
|
||||
}
|
||||
|
||||
[Route("lnd-config/{configKey}/lnd.config")]
|
||||
[AllowAnonymous]
|
||||
public IActionResult GetLNDConfig(uint configKey)
|
||||
{
|
||||
var conf = _LnConfigProvider.GetConfig(configKey);
|
||||
if (conf == null)
|
||||
return NotFound();
|
||||
return Json(conf);
|
||||
}
|
||||
|
||||
[Route("server/services/lnd-grpc/{cryptoCode}/{index}")]
|
||||
[HttpPost]
|
||||
public IActionResult LNDGRPCServicesPOST(string cryptoCode, int index)
|
||||
{
|
||||
var external = GetExternalLNDConnectionString(cryptoCode, index);
|
||||
if (external == null)
|
||||
return NotFound();
|
||||
LightningConfigurations confs = new LightningConfigurations();
|
||||
LightningConfiguration conf = new LightningConfiguration();
|
||||
conf.Type = "grpc";
|
||||
conf.ChainType = _Options.NetworkType.ToString();
|
||||
conf.CryptoCode = cryptoCode;
|
||||
conf.Host = external.BaseUri.DnsSafeHost;
|
||||
conf.Port = external.BaseUri.Port;
|
||||
conf.SSL = external.BaseUri.Scheme == "https";
|
||||
conf.Macaroon = external.Macaroon == null ? null : Encoders.Hex.EncodeData(external.Macaroon);
|
||||
conf.CertificateThumbprint = external.CertificateThumbprint == null ? null : Encoders.Hex.EncodeData(external.CertificateThumbprint);
|
||||
confs.Configurations.Add(conf);
|
||||
|
||||
var nonce = RandomUtils.GetUInt32();
|
||||
var configKey = GetConfigKey("lnd-grpc", cryptoCode, index, nonce);
|
||||
_LnConfigProvider.KeepConfig(configKey, confs);
|
||||
return RedirectToAction(nameof(LNDGRPCServices), new { cryptoCode = cryptoCode, nonce = nonce });
|
||||
}
|
||||
|
||||
private LightningConnectionString GetExternalLNDConnectionString(string cryptoCode, int index)
|
||||
{
|
||||
var connectionString = _Options.ExternalServicesByCryptoCode.GetServices<ExternalLNDGRPC>(cryptoCode).Skip(index).Select(c => c.ConnectionString).FirstOrDefault();
|
||||
if (connectionString == null)
|
||||
return null;
|
||||
connectionString = connectionString.Clone();
|
||||
if (connectionString.MacaroonFilePath != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
connectionString.Macaroon = System.IO.File.ReadAllBytes(connectionString.MacaroonFilePath);
|
||||
connectionString.MacaroonFilePath = null;
|
||||
}
|
||||
catch
|
||||
{
|
||||
Logging.Logs.Configuration.LogWarning($"{cryptoCode}: The macaroon file path of the external LND grpc config was not found ({connectionString.MacaroonFilePath})");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return connectionString;
|
||||
}
|
||||
|
||||
[Route("server/services/ssh")]
|
||||
public IActionResult SSHService(bool downloadKeyFile = false)
|
||||
{
|
||||
var settings = _Options.SSHSettings;
|
||||
if (settings == null)
|
||||
return NotFound();
|
||||
if (downloadKeyFile)
|
||||
{
|
||||
if (!System.IO.File.Exists(settings.KeyFile))
|
||||
return NotFound();
|
||||
return File(System.IO.File.ReadAllBytes(settings.KeyFile), "application/octet-stream", "id_rsa");
|
||||
}
|
||||
SSHServiceViewModel vm = new SSHServiceViewModel();
|
||||
string port = settings.Port == 22 ? "" : $" -p {settings.Port}";
|
||||
vm.CommandLine = $"ssh {settings.Username}@{settings.Server}{port}";
|
||||
vm.Password = settings.Password;
|
||||
vm.KeyFilePassword = settings.KeyFilePassword;
|
||||
vm.HasKeyFile = !string.IsNullOrEmpty(settings.KeyFile);
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
[Route("server/theme")]
|
||||
public async Task<IActionResult> Theme()
|
||||
{
|
||||
@ -234,9 +566,6 @@ namespace BTCPayServer.Controllers
|
||||
public async Task<IActionResult> Theme(ThemeSettings settings)
|
||||
{
|
||||
await _SettingsRepository.UpdateSetting(settings);
|
||||
// TODO: remove controller/class-level property and have only reference to
|
||||
// CssThemeManager here in this method
|
||||
_CssThemeManager.Update(settings);
|
||||
TempData["StatusMessage"] = "Theme settings updated successfully";
|
||||
return View(settings);
|
||||
}
|
||||
@ -247,10 +576,13 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
if (command == "Test")
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
return View(model);
|
||||
try
|
||||
{
|
||||
if (!model.Settings.IsComplete())
|
||||
{
|
||||
model.StatusMessage = "Error: Required fields missing";
|
||||
return View(model);
|
||||
}
|
||||
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";
|
||||
@ -261,11 +593,8 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
return View(model);
|
||||
}
|
||||
else
|
||||
else // if(command == "Save")
|
||||
{
|
||||
ModelState.Remove(nameof(model.TestEmail));
|
||||
if (!ModelState.IsValid)
|
||||
return View(model);
|
||||
await _SettingsRepository.UpdateSetting(model.Settings);
|
||||
model.StatusMessage = "Email settings saved";
|
||||
return View(model);
|
||||
|
@ -10,6 +10,7 @@ using BTCPayServer.Data;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Services;
|
||||
using LedgerWallet;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitcoin;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
@ -21,9 +22,9 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
[HttpGet]
|
||||
[Route("{storeId}/derivations/{cryptoCode}")]
|
||||
public async Task<IActionResult> AddDerivationScheme(string storeId, string cryptoCode)
|
||||
public IActionResult AddDerivationScheme(string storeId, string cryptoCode)
|
||||
{
|
||||
var store = await _Repo.FindStore(storeId, GetUserId());
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
var network = cryptoCode == null ? null : _ExplorerProvider.GetNetwork(cryptoCode);
|
||||
@ -33,7 +34,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
DerivationSchemeViewModel vm = new DerivationSchemeViewModel();
|
||||
vm.ServerUrl = GetStoreUrl(storeId);
|
||||
vm.ServerUrl = WalletsController.GetLedgerWebsocketUrl(this.HttpContext, cryptoCode, null);
|
||||
vm.CryptoCode = cryptoCode;
|
||||
vm.RootKeyPath = network.GetRootKeyPath();
|
||||
SetExistingValues(store, vm);
|
||||
@ -43,6 +44,7 @@ namespace BTCPayServer.Controllers
|
||||
private void SetExistingValues(StoreData store, DerivationSchemeViewModel vm)
|
||||
{
|
||||
vm.DerivationScheme = GetExistingDerivationStrategy(vm.CryptoCode, store)?.DerivationStrategyBase.ToString();
|
||||
vm.Enabled = !store.GetStoreBlob().IsExcluded(new PaymentMethodId(vm.CryptoCode, PaymentTypes.BTCLike));
|
||||
}
|
||||
|
||||
private DerivationStrategy GetExistingDerivationStrategy(string cryptoCode, StoreData store)
|
||||
@ -58,9 +60,9 @@ namespace BTCPayServer.Controllers
|
||||
[Route("{storeId}/derivations/{cryptoCode}")]
|
||||
public async Task<IActionResult> AddDerivationScheme(string storeId, DerivationSchemeViewModel vm, string cryptoCode)
|
||||
{
|
||||
vm.ServerUrl = GetStoreUrl(storeId);
|
||||
vm.ServerUrl = WalletsController.GetLedgerWebsocketUrl(this.HttpContext, cryptoCode, null);
|
||||
vm.CryptoCode = cryptoCode;
|
||||
var store = await _Repo.FindStore(storeId, GetUserId());
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
|
||||
@ -77,6 +79,11 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
PaymentMethodId paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike);
|
||||
var exisingStrategy = store.GetSupportedPaymentMethods(_NetworkProvider)
|
||||
.Where(c => c.PaymentId == paymentMethodId)
|
||||
.OfType<DerivationStrategy>()
|
||||
.Select(c => c.DerivationStrategyBase.ToString())
|
||||
.FirstOrDefault();
|
||||
DerivationStrategy strategy = null;
|
||||
try
|
||||
{
|
||||
@ -93,10 +100,32 @@ namespace BTCPayServer.Controllers
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
if (!vm.Confirmation && strategy != null)
|
||||
return ShowAddresses(vm, strategy);
|
||||
var showAddress = (vm.Confirmation && !string.IsNullOrWhiteSpace(vm.HintAddress)) || // Testing hint address
|
||||
(!vm.Confirmation && strategy != null && exisingStrategy != strategy.DerivationStrategyBase.ToString()); // Checking addresses after setting xpub
|
||||
|
||||
if (vm.Confirmation && !string.IsNullOrWhiteSpace(vm.HintAddress))
|
||||
if (!showAddress)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (strategy != null)
|
||||
await wallet.TrackAsync(strategy.DerivationStrategyBase);
|
||||
store.SetSupportedPaymentMethod(paymentMethodId, strategy);
|
||||
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
storeBlob.SetExcluded(paymentMethodId, !vm.Enabled);
|
||||
store.SetStoreBlob(storeBlob);
|
||||
}
|
||||
catch
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.DerivationScheme), "Invalid Derivation Scheme");
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
await _Repo.UpdateStore(store);
|
||||
StatusMessage = $"Derivation scheme for {network.CryptoCode} has been modified.";
|
||||
return RedirectToAction(nameof(UpdateStore), new { storeId = storeId });
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(vm.HintAddress))
|
||||
{
|
||||
BitcoinAddress address = null;
|
||||
try
|
||||
@ -122,26 +151,8 @@ namespace BTCPayServer.Controllers
|
||||
vm.StatusMessage = "Address successfully found, please verify that the rest is correct and click on \"Confirm\"";
|
||||
ModelState.Remove(nameof(vm.HintAddress));
|
||||
ModelState.Remove(nameof(vm.DerivationScheme));
|
||||
return ShowAddresses(vm, strategy);
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
if (strategy != null)
|
||||
await wallet.TrackAsync(strategy.DerivationStrategyBase);
|
||||
store.SetSupportedPaymentMethod(paymentMethodId, strategy);
|
||||
}
|
||||
catch
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.DerivationScheme), "Invalid Derivation Scheme");
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
await _Repo.UpdateStore(store);
|
||||
StatusMessage = $"Derivation scheme for {network.CryptoCode} has been modified.";
|
||||
return RedirectToAction(nameof(UpdateStore), new { storeId = storeId });
|
||||
}
|
||||
return ShowAddresses(vm, strategy);
|
||||
}
|
||||
|
||||
private IActionResult ShowAddresses(DerivationSchemeViewModel vm, DerivationStrategy strategy)
|
||||
@ -160,199 +171,5 @@ namespace BTCPayServer.Controllers
|
||||
vm.Confirmation = true;
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
|
||||
public class GetInfoResult
|
||||
{
|
||||
public int RecommendedSatoshiPerByte { get; set; }
|
||||
public double Balance { get; set; }
|
||||
}
|
||||
|
||||
public class SendToAddressResult
|
||||
{
|
||||
public string TransactionId { get; set; }
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{storeId}/ws/ledger")]
|
||||
public async Task<IActionResult> LedgerConnection(
|
||||
string storeId,
|
||||
string command,
|
||||
// getinfo
|
||||
string cryptoCode = null,
|
||||
// getxpub
|
||||
int account = 0,
|
||||
// sendtoaddress
|
||||
string destination = null, string amount = null, string feeRate = null, string substractFees = null
|
||||
)
|
||||
{
|
||||
if (!HttpContext.WebSockets.IsWebSocketRequest)
|
||||
return NotFound();
|
||||
var store = await _Repo.FindStore(storeId, GetUserId());
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
|
||||
var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
|
||||
|
||||
var hw = new HardwareWalletService(webSocket);
|
||||
object result = null;
|
||||
try
|
||||
{
|
||||
BTCPayNetwork network = null;
|
||||
if (cryptoCode != null)
|
||||
{
|
||||
network = _NetworkProvider.GetNetwork(cryptoCode);
|
||||
if (network == null)
|
||||
throw new FormatException("Invalid value for crypto code");
|
||||
}
|
||||
|
||||
BitcoinAddress destinationAddress = null;
|
||||
if (destination != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
destinationAddress = BitcoinAddress.Create(destination, network.NBitcoinNetwork);
|
||||
}
|
||||
catch { }
|
||||
if (destinationAddress == null)
|
||||
throw new FormatException("Invalid value for destination");
|
||||
}
|
||||
|
||||
FeeRate feeRateValue = null;
|
||||
if (feeRate != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
feeRateValue = new FeeRate(Money.Satoshis(int.Parse(feeRate, CultureInfo.InvariantCulture)), 1);
|
||||
}
|
||||
catch { }
|
||||
if (feeRateValue == null || feeRateValue.FeePerK <= Money.Zero)
|
||||
throw new FormatException("Invalid value for fee rate");
|
||||
}
|
||||
|
||||
Money amountBTC = null;
|
||||
if (amount != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
amountBTC = Money.Parse(amount);
|
||||
}
|
||||
catch { }
|
||||
if (amountBTC == null || amountBTC <= Money.Zero)
|
||||
throw new FormatException("Invalid value for amount");
|
||||
}
|
||||
|
||||
bool subsctractFeesValue = false;
|
||||
if (substractFees != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
subsctractFeesValue = bool.Parse(substractFees);
|
||||
}
|
||||
catch { throw new FormatException("Invalid value for subtract fees"); }
|
||||
}
|
||||
if (command == "test")
|
||||
{
|
||||
result = await hw.Test();
|
||||
}
|
||||
if (command == "getxpub")
|
||||
{
|
||||
var getxpubResult = await hw.GetExtPubKey(network, account);
|
||||
result = getxpubResult;
|
||||
}
|
||||
if (command == "getinfo")
|
||||
{
|
||||
var strategy = GetDirectDerivationStrategy(store, network);
|
||||
var strategyBase = GetDerivationStrategy(store, network);
|
||||
if (strategy == null || !await hw.SupportDerivation(network, strategy))
|
||||
{
|
||||
throw new Exception($"This store is not configured to use this ledger");
|
||||
}
|
||||
|
||||
var feeProvider = _FeeRateProvider.CreateFeeProvider(network);
|
||||
var recommendedFees = feeProvider.GetFeeRateAsync();
|
||||
var balance = _WalletProvider.GetWallet(network).GetBalance(strategyBase);
|
||||
result = new GetInfoResult() { Balance = (double)(await balance).ToDecimal(MoneyUnit.BTC), RecommendedSatoshiPerByte = (int)(await recommendedFees).GetFee(1).Satoshi };
|
||||
}
|
||||
|
||||
if (command == "sendtoaddress")
|
||||
{
|
||||
if (!_Dashboard.IsFullySynched(network.CryptoCode, out var summary))
|
||||
throw new Exception($"{network.CryptoCode}: not started or fully synched");
|
||||
var strategy = GetDirectDerivationStrategy(store, network);
|
||||
var strategyBase = GetDerivationStrategy(store, network);
|
||||
var wallet = _WalletProvider.GetWallet(network);
|
||||
var change = wallet.GetChangeAddressAsync(strategyBase);
|
||||
|
||||
var unspentCoins = await wallet.GetUnspentCoins(strategyBase);
|
||||
var changeAddress = await change;
|
||||
var transaction = await hw.SendToAddress(strategy, unspentCoins, network,
|
||||
new[] { (destinationAddress as IDestination, amountBTC, subsctractFeesValue) },
|
||||
feeRateValue,
|
||||
changeAddress.Item1,
|
||||
changeAddress.Item2, summary.Status.BitcoinStatus.MinRelayTxFee);
|
||||
try
|
||||
{
|
||||
var broadcastResult = await wallet.BroadcastTransactionsAsync(new List<Transaction>() { transaction });
|
||||
if (!broadcastResult[0].Success)
|
||||
{
|
||||
throw new Exception($"RPC Error while broadcasting: {broadcastResult[0].RPCCode} {broadcastResult[0].RPCCodeMessage} {broadcastResult[0].RPCMessage}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception("Error while broadcasting: " + ex.Message);
|
||||
}
|
||||
wallet.InvalidateCache(strategyBase);
|
||||
result = new SendToAddressResult() { TransactionId = transaction.GetHash().ToString() };
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{ result = new LedgerTestResult() { Success = false, Error = "Timeout" }; }
|
||||
catch (Exception ex)
|
||||
{ result = new LedgerTestResult() { Success = false, Error = ex.Message }; }
|
||||
|
||||
try
|
||||
{
|
||||
if (result != null)
|
||||
{
|
||||
UTF8Encoding UTF8NOBOM = new UTF8Encoding(false);
|
||||
var bytes = UTF8NOBOM.GetBytes(JsonConvert.SerializeObject(result, _MvcJsonOptions.SerializerSettings));
|
||||
await webSocket.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Text, true, new CancellationTokenSource(2000).Token);
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
finally
|
||||
{
|
||||
await webSocket.CloseSocket();
|
||||
}
|
||||
|
||||
return new EmptyResult();
|
||||
}
|
||||
|
||||
private DirectDerivationStrategy GetDirectDerivationStrategy(StoreData store, BTCPayNetwork network)
|
||||
{
|
||||
var strategy = GetDerivationStrategy(store, network);
|
||||
var directStrategy = strategy as DirectDerivationStrategy;
|
||||
if (directStrategy == null)
|
||||
directStrategy = (strategy as P2SHDerivationStrategy).Inner as DirectDerivationStrategy;
|
||||
if (!directStrategy.Segwit)
|
||||
return null;
|
||||
return directStrategy;
|
||||
}
|
||||
|
||||
private DerivationStrategyBase GetDerivationStrategy(StoreData store, BTCPayNetwork network)
|
||||
{
|
||||
var strategy = store
|
||||
.GetSupportedPaymentMethods(_NetworkProvider)
|
||||
.OfType<DerivationStrategy>()
|
||||
.FirstOrDefault(s => s.Network.NBitcoinNetwork == network.NBitcoinNetwork);
|
||||
if (strategy == null)
|
||||
{
|
||||
throw new Exception($"Derivation strategy for {network.CryptoCode} is not set");
|
||||
}
|
||||
|
||||
return strategy.DerivationStrategyBase;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -19,23 +19,25 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
[HttpGet]
|
||||
[Route("{storeId}/lightning/{cryptoCode}")]
|
||||
public async Task<IActionResult> AddLightningNode(string storeId, string cryptoCode)
|
||||
public IActionResult AddLightningNode(string storeId, string cryptoCode)
|
||||
{
|
||||
var store = await _Repo.FindStore(storeId, GetUserId());
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
LightningNodeViewModel vm = new LightningNodeViewModel();
|
||||
vm.CryptoCode = cryptoCode;
|
||||
vm.InternalLightningNode = GetInternalLighningNode(cryptoCode)?.ToUri(true)?.AbsoluteUri;
|
||||
LightningNodeViewModel vm = new LightningNodeViewModel
|
||||
{
|
||||
CryptoCode = cryptoCode,
|
||||
InternalLightningNode = GetInternalLighningNode(cryptoCode)?.ToString()
|
||||
};
|
||||
SetExistingValues(store, vm);
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
private void SetExistingValues(StoreData store, LightningNodeViewModel vm)
|
||||
{
|
||||
vm.Url = GetExistingLightningSupportedPaymentMethod(vm.CryptoCode, store)?.GetLightningUrl()?.ToString();
|
||||
vm.ConnectionString = GetExistingLightningSupportedPaymentMethod(vm.CryptoCode, store)?.GetLightningUrl()?.ToString();
|
||||
vm.Enabled = !store.GetStoreBlob().IsExcluded(new PaymentMethodId(vm.CryptoCode, PaymentTypes.LightningLike));
|
||||
}
|
||||
|
||||
private LightningSupportedPaymentMethod GetExistingLightningSupportedPaymentMethod(string cryptoCode, StoreData store)
|
||||
{
|
||||
var id = new PaymentMethodId(cryptoCode, PaymentTypes.LightningLike);
|
||||
@ -59,13 +61,13 @@ namespace BTCPayServer.Controllers
|
||||
public async Task<IActionResult> AddLightningNode(string storeId, LightningNodeViewModel vm, string command, string cryptoCode)
|
||||
{
|
||||
vm.CryptoCode = cryptoCode;
|
||||
var store = await _Repo.FindStore(storeId, GetUserId());
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
var network = vm.CryptoCode == null ? null : _ExplorerProvider.GetNetwork(vm.CryptoCode);
|
||||
|
||||
var internalLightning = GetInternalLighningNode(network.CryptoCode);
|
||||
vm.InternalLightningNode = internalLightning?.ToUri(true)?.AbsoluteUri;
|
||||
vm.InternalLightningNode = internalLightning?.ToString();
|
||||
if (network == null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.CryptoCode), "Invalid network");
|
||||
@ -74,33 +76,57 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
PaymentMethodId paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.LightningLike);
|
||||
Payments.Lightning.LightningSupportedPaymentMethod paymentMethod = null;
|
||||
if (!string.IsNullOrEmpty(vm.Url))
|
||||
if (!string.IsNullOrEmpty(vm.ConnectionString))
|
||||
{
|
||||
if (!LightningConnectionString.TryParse(vm.Url, out var connectionString, out var error))
|
||||
if (!LightningConnectionString.TryParse(vm.ConnectionString, false, out var connectionString, out var error))
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.Url), $"Invalid URL ({error})");
|
||||
ModelState.AddModelError(nameof(vm.ConnectionString), $"Invalid URL ({error})");
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
var internalDomain = internalLightning?.ToUri(false)?.DnsSafeHost;
|
||||
bool isLocal = (internalDomain == "127.0.0.1" || internalDomain == "localhost");
|
||||
if(connectionString.ConnectionType == LightningConnectionType.LndGRPC)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.ConnectionString), $"BTCPay does not support gRPC connections");
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
var internalDomain = internalLightning?.BaseUri?.DnsSafeHost;
|
||||
|
||||
bool isInternalNode = connectionString.ConnectionType == LightningConnectionType.CLightning ||
|
||||
connectionString.BaseUri.DnsSafeHost == internalDomain ||
|
||||
isLocal;
|
||||
(internalDomain == "127.0.0.1" || internalDomain == "localhost");
|
||||
|
||||
if (connectionString.BaseUri.Scheme == "http" && !isLocal)
|
||||
if (connectionString.BaseUri.Scheme == "http")
|
||||
{
|
||||
if (!isInternalNode || (isInternalNode && !CanUseInternalLightning()))
|
||||
if (!isInternalNode)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.Url), "The url must be HTTPS");
|
||||
ModelState.AddModelError(nameof(vm.ConnectionString), "The url must be HTTPS");
|
||||
return View(vm);
|
||||
}
|
||||
}
|
||||
|
||||
if(connectionString.MacaroonFilePath != null)
|
||||
{
|
||||
if(!CanUseInternalLightning())
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.ConnectionString), "You are not authorized to use macaroonfilepath");
|
||||
return View(vm);
|
||||
}
|
||||
if(!System.IO.File.Exists(connectionString.MacaroonFilePath))
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.ConnectionString), "The macaroonfilepath file does exist");
|
||||
return View(vm);
|
||||
}
|
||||
if(!System.IO.Path.IsPathRooted(connectionString.MacaroonFilePath))
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.ConnectionString), "The macaroonfilepath should be fully rooted");
|
||||
return View(vm);
|
||||
}
|
||||
}
|
||||
|
||||
if (isInternalNode && !CanUseInternalLightning())
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.Url), "Unauthorized url");
|
||||
ModelState.AddModelError(nameof(vm.ConnectionString), "Unauthorized url");
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
@ -110,39 +136,42 @@ namespace BTCPayServer.Controllers
|
||||
};
|
||||
paymentMethod.SetLightningUrl(connectionString);
|
||||
}
|
||||
if (command == "save")
|
||||
|
||||
switch (command)
|
||||
{
|
||||
store.SetSupportedPaymentMethod(paymentMethodId, paymentMethod);
|
||||
await _Repo.UpdateStore(store);
|
||||
StatusMessage = $"Lightning node modified ({network.CryptoCode})";
|
||||
return RedirectToAction(nameof(UpdateStore), new { storeId = storeId });
|
||||
}
|
||||
else // if(command == "test")
|
||||
{
|
||||
if (paymentMethod == null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.Url), "Missing url parameter");
|
||||
case "save":
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
storeBlob.SetExcluded(paymentMethodId, !vm.Enabled);
|
||||
store.SetStoreBlob(storeBlob);
|
||||
store.SetSupportedPaymentMethod(paymentMethodId, paymentMethod);
|
||||
await _Repo.UpdateStore(store);
|
||||
StatusMessage = $"Lightning node modified ({network.CryptoCode})";
|
||||
return RedirectToAction(nameof(UpdateStore), new { storeId = storeId });
|
||||
case "test" when paymentMethod == null:
|
||||
ModelState.AddModelError(nameof(vm.ConnectionString), "Missing url parameter");
|
||||
return View(vm);
|
||||
}
|
||||
var handler = (LightningLikePaymentHandler)_ServiceProvider.GetRequiredService<IPaymentMethodHandler<Payments.Lightning.LightningSupportedPaymentMethod>>();
|
||||
try
|
||||
{
|
||||
var info = await handler.Test(paymentMethod, network);
|
||||
if (!vm.SkipPortTest)
|
||||
case "test":
|
||||
var handler = (LightningLikePaymentHandler)_ServiceProvider.GetRequiredService<IPaymentMethodHandler<Payments.Lightning.LightningSupportedPaymentMethod>>();
|
||||
try
|
||||
{
|
||||
using (CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(20)))
|
||||
var info = await handler.Test(paymentMethod, network);
|
||||
if (!vm.SkipPortTest)
|
||||
{
|
||||
await handler.TestConnection(info, cts.Token);
|
||||
using (CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(20)))
|
||||
{
|
||||
await handler.TestConnection(info, cts.Token);
|
||||
}
|
||||
}
|
||||
vm.StatusMessage = $"Connection to the lightning node succeeded ({info})";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
vm.StatusMessage = $"Error: {ex.Message}";
|
||||
return View(vm);
|
||||
}
|
||||
vm.StatusMessage = $"Connection to the lightning node succeed ({info})";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
vm.StatusMessage = $"Error: {ex.Message}";
|
||||
return View(vm);
|
||||
}
|
||||
return View(vm);
|
||||
default:
|
||||
return View(vm);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4,6 +4,8 @@ using BTCPayServer.Data;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
@ -19,6 +21,7 @@ using NBitcoin.DataEncoders;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
@ -27,31 +30,30 @@ using System.Threading.Tasks;
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
[Route("stores")]
|
||||
[Authorize(AuthenticationSchemes = "Identity.Application")]
|
||||
[Authorize(Policy = StorePolicies.OwnStore)]
|
||||
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings.Key)]
|
||||
[AutoValidateAntiforgeryToken]
|
||||
public partial class StoresController : Controller
|
||||
{
|
||||
BTCPayRateProviderFactory _RateFactory;
|
||||
public string CreatedStoreId { get; set; }
|
||||
public StoresController(
|
||||
NBXplorerDashboard dashboard,
|
||||
IServiceProvider serviceProvider,
|
||||
BTCPayServerOptions btcpayServerOptions,
|
||||
BTCPayServerEnvironment btcpayEnv,
|
||||
IOptions<MvcJsonOptions> mvcJsonOptions,
|
||||
StoreRepository repo,
|
||||
TokenRepository tokenRepo,
|
||||
UserManager<ApplicationUser> userManager,
|
||||
AccessTokenController tokenController,
|
||||
BTCPayWalletProvider walletProvider,
|
||||
BTCPayNetworkProvider networkProvider,
|
||||
BTCPayRateProviderFactory rateFactory,
|
||||
ExplorerClientProvider explorerProvider,
|
||||
IFeeProviderFactory feeRateProvider,
|
||||
LanguageService langService,
|
||||
IHostingEnvironment env,
|
||||
CoinAverageSettings coinAverage)
|
||||
IHostingEnvironment env)
|
||||
{
|
||||
_Dashboard = dashboard;
|
||||
_RateFactory = rateFactory;
|
||||
_Repo = repo;
|
||||
_TokenRepository = tokenRepo;
|
||||
_UserManager = userManager;
|
||||
@ -61,21 +63,16 @@ namespace BTCPayServer.Controllers
|
||||
_Env = env;
|
||||
_NetworkProvider = networkProvider;
|
||||
_ExplorerProvider = explorerProvider;
|
||||
_MvcJsonOptions = mvcJsonOptions.Value;
|
||||
_FeeRateProvider = feeRateProvider;
|
||||
_ServiceProvider = serviceProvider;
|
||||
_BtcpayServerOptions = btcpayServerOptions;
|
||||
_BTCPayEnv = btcpayEnv;
|
||||
_CoinAverage = coinAverage;
|
||||
}
|
||||
CoinAverageSettings _CoinAverage;
|
||||
NBXplorerDashboard _Dashboard;
|
||||
BTCPayServerOptions _BtcpayServerOptions;
|
||||
BTCPayServerEnvironment _BTCPayEnv;
|
||||
IServiceProvider _ServiceProvider;
|
||||
BTCPayNetworkProvider _NetworkProvider;
|
||||
private ExplorerClientProvider _ExplorerProvider;
|
||||
private MvcJsonOptions _MvcJsonOptions;
|
||||
private IFeeProviderFactory _FeeRateProvider;
|
||||
BTCPayWalletProvider _WalletProvider;
|
||||
AccessTokenController _TokenController;
|
||||
@ -91,37 +88,19 @@ namespace BTCPayServer.Controllers
|
||||
get; set;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{storeId}/wallet/{cryptoCode}")]
|
||||
public async Task<IActionResult> Wallet(string storeId, string cryptoCode)
|
||||
{
|
||||
var store = await _Repo.FindStore(storeId, GetUserId());
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
WalletModel model = new WalletModel();
|
||||
model.ServerUrl = GetStoreUrl(storeId);
|
||||
model.CryptoCurrency = cryptoCode;
|
||||
return View(model);
|
||||
}
|
||||
|
||||
private string GetStoreUrl(string storeId)
|
||||
{
|
||||
return HttpContext.Request.GetAbsoluteRoot() + "/stores/" + storeId + "/";
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{storeId}/users")]
|
||||
public async Task<IActionResult> StoreUsers(string storeId)
|
||||
public async Task<IActionResult> StoreUsers()
|
||||
{
|
||||
StoreUsersViewModel vm = new StoreUsersViewModel();
|
||||
await FillUsers(storeId, vm);
|
||||
await FillUsers(vm);
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
private async Task FillUsers(string storeId, StoreUsersViewModel vm)
|
||||
private async Task FillUsers(StoreUsersViewModel vm)
|
||||
{
|
||||
var users = await _Repo.GetStoreUsers(storeId);
|
||||
vm.StoreId = storeId;
|
||||
var users = await _Repo.GetStoreUsers(StoreData.Id);
|
||||
vm.StoreId = StoreData.Id;
|
||||
vm.Users = users.Select(u => new StoreUsersViewModel.StoreUserViewModel()
|
||||
{
|
||||
Email = u.Email,
|
||||
@ -130,11 +109,20 @@ namespace BTCPayServer.Controllers
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
public StoreData StoreData
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.HttpContext.GetStoreData();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[HttpPost]
|
||||
[Route("{storeId}/users")]
|
||||
public async Task<IActionResult> StoreUsers(string storeId, StoreUsersViewModel vm)
|
||||
public async Task<IActionResult> StoreUsers(StoreUsersViewModel vm)
|
||||
{
|
||||
await FillUsers(storeId, vm);
|
||||
await FillUsers(vm);
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(vm);
|
||||
@ -150,7 +138,7 @@ namespace BTCPayServer.Controllers
|
||||
ModelState.AddModelError(nameof(vm.Role), "Invalid role");
|
||||
return View(vm);
|
||||
}
|
||||
if (!await _Repo.AddStoreUser(storeId, user.Id, vm.Role))
|
||||
if (!await _Repo.AddStoreUser(StoreData.Id, user.Id, vm.Role))
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.Email), "The user already has access to this store");
|
||||
return View(vm);
|
||||
@ -161,19 +149,16 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
[HttpGet]
|
||||
[Route("{storeId}/users/{userId}/delete")]
|
||||
public async Task<IActionResult> DeleteStoreUser(string storeId, string userId)
|
||||
public async Task<IActionResult> DeleteStoreUser(string userId)
|
||||
{
|
||||
StoreUsersViewModel vm = new StoreUsersViewModel();
|
||||
var store = await _Repo.FindStore(storeId, userId);
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
var user = await _UserManager.FindByIdAsync(userId);
|
||||
if (user == null)
|
||||
return NotFound();
|
||||
return View("Confirm", new ConfirmModel()
|
||||
{
|
||||
Title = $"Remove store user",
|
||||
Description = $"Are you sure to remove access to remove {store.Role} access to {user.Email}?",
|
||||
Description = $"Are you sure to remove access to remove access to {user.Email}?",
|
||||
Action = "Delete"
|
||||
});
|
||||
}
|
||||
@ -188,27 +173,165 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{storeId}/checkout")]
|
||||
public async Task<IActionResult> CheckoutExperience(string storeId)
|
||||
[Route("{storeId}/rates")]
|
||||
public IActionResult Rates()
|
||||
{
|
||||
var store = await _Repo.FindStore(storeId, GetUserId());
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
var storeBlob = StoreData.GetStoreBlob();
|
||||
var vm = new RatesViewModel();
|
||||
vm.SetExchangeRates(GetSupportedExchanges(), storeBlob.PreferredExchange ?? CoinAverageRateProvider.CoinAverageName);
|
||||
vm.Spread = (double)(storeBlob.Spread * 100m);
|
||||
vm.Script = storeBlob.GetRateRules(_NetworkProvider).ToString();
|
||||
vm.DefaultScript = storeBlob.GetDefaultRateRules(_NetworkProvider).ToString();
|
||||
vm.AvailableExchanges = GetSupportedExchanges();
|
||||
vm.ShowScripting = storeBlob.RateScripting;
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("{storeId}/rates")]
|
||||
public async Task<IActionResult> Rates(RatesViewModel model, string command = null)
|
||||
{
|
||||
model.SetExchangeRates(GetSupportedExchanges(), model.PreferredExchange);
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(model);
|
||||
}
|
||||
if (model.PreferredExchange != null)
|
||||
model.PreferredExchange = model.PreferredExchange.Trim().ToLowerInvariant();
|
||||
|
||||
var blob = StoreData.GetStoreBlob();
|
||||
model.DefaultScript = blob.GetDefaultRateRules(_NetworkProvider).ToString();
|
||||
model.AvailableExchanges = GetSupportedExchanges();
|
||||
|
||||
blob.PreferredExchange = model.PreferredExchange;
|
||||
blob.Spread = (decimal)model.Spread / 100.0m;
|
||||
|
||||
if (!model.ShowScripting)
|
||||
{
|
||||
if (!GetSupportedExchanges().Select(c => c.Name).Contains(blob.PreferredExchange, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.PreferredExchange), $"Unsupported exchange ({model.RateSource})");
|
||||
return View(model);
|
||||
}
|
||||
}
|
||||
RateRules rules = null;
|
||||
if (model.ShowScripting)
|
||||
{
|
||||
if (!RateRules.TryParse(model.Script, out rules, out var errors))
|
||||
{
|
||||
errors = errors ?? new List<RateRulesErrors>();
|
||||
var errorString = String.Join(", ", errors.ToArray());
|
||||
ModelState.AddModelError(nameof(model.Script), $"Parsing error ({errorString})");
|
||||
return View(model);
|
||||
}
|
||||
else
|
||||
{
|
||||
blob.RateScript = rules.ToString();
|
||||
ModelState.Remove(nameof(model.Script));
|
||||
model.Script = blob.RateScript;
|
||||
}
|
||||
}
|
||||
rules = blob.GetRateRules(_NetworkProvider);
|
||||
|
||||
if (command == "Test")
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(model.ScriptTest))
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.ScriptTest), "Fill out currency pair to test for (like BTC_USD,BTC_CAD)");
|
||||
return View(model);
|
||||
}
|
||||
var splitted = model.ScriptTest.Split(',', StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
var pairs = new List<CurrencyPair>();
|
||||
foreach (var pair in splitted)
|
||||
{
|
||||
if (!CurrencyPair.TryParse(pair, out var currencyPair))
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.ScriptTest), $"Invalid currency pair '{pair}' (it should be formatted like BTC_USD,BTC_CAD)");
|
||||
return View(model);
|
||||
}
|
||||
pairs.Add(currencyPair);
|
||||
}
|
||||
|
||||
var fetchs = _RateFactory.FetchRates(pairs.ToHashSet(), rules);
|
||||
var testResults = new List<RatesViewModel.TestResultViewModel>();
|
||||
foreach (var fetch in fetchs)
|
||||
{
|
||||
var testResult = await (fetch.Value);
|
||||
testResults.Add(new RatesViewModel.TestResultViewModel()
|
||||
{
|
||||
CurrencyPair = fetch.Key.ToString(),
|
||||
Error = testResult.Errors.Count != 0,
|
||||
Rule = testResult.Errors.Count == 0 ? testResult.Rule + " = " + testResult.BidAsk.Bid.ToString(CultureInfo.InvariantCulture)
|
||||
: testResult.EvaluatedRule
|
||||
});
|
||||
}
|
||||
model.TestRateRules = testResults;
|
||||
return View(model);
|
||||
}
|
||||
else // command == Save
|
||||
{
|
||||
if (StoreData.SetStoreBlob(blob))
|
||||
{
|
||||
await _Repo.UpdateStore(StoreData);
|
||||
StatusMessage = "Rate settings updated";
|
||||
}
|
||||
return RedirectToAction(nameof(Rates), new
|
||||
{
|
||||
storeId = StoreData.Id
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{storeId}/rates/confirm")]
|
||||
public IActionResult ShowRateRules(bool scripting)
|
||||
{
|
||||
return View("Confirm", new ConfirmModel()
|
||||
{
|
||||
Action = "Continue",
|
||||
Title = "Rate rule scripting",
|
||||
Description = scripting ?
|
||||
"This action will modify your current rate sources. Are you sure to turn on rate rules scripting? (Advanced users)"
|
||||
: "This action will delete your rate script. Are you sure to turn off rate rules scripting?",
|
||||
ButtonClass = "btn-primary"
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("{storeId}/rates/confirm")]
|
||||
public async Task<IActionResult> ShowRateRulesPost(bool scripting)
|
||||
{
|
||||
var blob = StoreData.GetStoreBlob();
|
||||
blob.RateScripting = scripting;
|
||||
blob.RateScript = blob.GetDefaultRateRules(_NetworkProvider).ToString();
|
||||
StoreData.SetStoreBlob(blob);
|
||||
await _Repo.UpdateStore(StoreData);
|
||||
StatusMessage = "Rate rules scripting activated";
|
||||
return RedirectToAction(nameof(Rates), new { storeId = StoreData.Id });
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{storeId}/checkout")]
|
||||
public IActionResult CheckoutExperience()
|
||||
{
|
||||
var storeBlob = StoreData.GetStoreBlob();
|
||||
var vm = new CheckoutExperienceViewModel();
|
||||
vm.SetCryptoCurrencies(_ExplorerProvider, store.GetDefaultCrypto());
|
||||
vm.SetCryptoCurrencies(_ExplorerProvider, StoreData.GetDefaultCrypto(_NetworkProvider));
|
||||
vm.SetLanguages(_LangService, storeBlob.DefaultLang);
|
||||
vm.LightningMaxValue = storeBlob.LightningMaxValue?.ToString() ?? "";
|
||||
vm.OnChainMinValue = storeBlob.OnChainMinValue?.ToString() ?? "";
|
||||
vm.AllowCoinConversion = storeBlob.AllowCoinConversion;
|
||||
vm.RequiresRefundEmail = storeBlob.RequiresRefundEmail;
|
||||
vm.CustomCSS = storeBlob.CustomCSS?.AbsoluteUri;
|
||||
vm.CustomLogo = storeBlob.CustomLogo?.AbsoluteUri;
|
||||
vm.HtmlTitle = storeBlob.HtmlTitle;
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("{storeId}/checkout")]
|
||||
public async Task<IActionResult> CheckoutExperience(string storeId, CheckoutExperienceViewModel model)
|
||||
public async Task<IActionResult> CheckoutExperience(CheckoutExperienceViewModel model)
|
||||
{
|
||||
CurrencyValue lightningMaxValue = null;
|
||||
if (!string.IsNullOrWhiteSpace(model.LightningMaxValue))
|
||||
@ -227,16 +350,12 @@ namespace BTCPayServer.Controllers
|
||||
ModelState.AddModelError(nameof(model.OnChainMinValue), "Invalid on chain min value");
|
||||
}
|
||||
}
|
||||
|
||||
var store = await _Repo.FindStore(storeId, GetUserId());
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
bool needUpdate = false;
|
||||
var blob = store.GetStoreBlob();
|
||||
if (store.GetDefaultCrypto() != model.DefaultCryptoCurrency)
|
||||
var blob = StoreData.GetStoreBlob();
|
||||
if (StoreData.GetDefaultCrypto(_NetworkProvider) != model.DefaultCryptoCurrency)
|
||||
{
|
||||
needUpdate = true;
|
||||
store.SetDefaultCrypto(model.DefaultCryptoCurrency);
|
||||
StoreData.SetDefaultCrypto(model.DefaultCryptoCurrency);
|
||||
}
|
||||
model.SetCryptoCurrencies(_ExplorerProvider, model.DefaultCryptoCurrency);
|
||||
model.SetLanguages(_LangService, model.DefaultLang);
|
||||
@ -247,53 +366,56 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
blob.DefaultLang = model.DefaultLang;
|
||||
blob.AllowCoinConversion = model.AllowCoinConversion;
|
||||
blob.RequiresRefundEmail = model.RequiresRefundEmail;
|
||||
blob.LightningMaxValue = lightningMaxValue;
|
||||
blob.OnChainMinValue = onchainMinValue;
|
||||
blob.CustomLogo = string.IsNullOrWhiteSpace(model.CustomLogo) ? null : new Uri(model.CustomLogo, UriKind.Absolute);
|
||||
blob.CustomCSS = string.IsNullOrWhiteSpace(model.CustomCSS) ? null : new Uri(model.CustomCSS, UriKind.Absolute);
|
||||
if (store.SetStoreBlob(blob))
|
||||
blob.HtmlTitle = string.IsNullOrWhiteSpace(model.HtmlTitle) ? null : model.HtmlTitle;
|
||||
if (StoreData.SetStoreBlob(blob))
|
||||
{
|
||||
needUpdate = true;
|
||||
}
|
||||
if (needUpdate)
|
||||
{
|
||||
await _Repo.UpdateStore(store);
|
||||
await _Repo.UpdateStore(StoreData);
|
||||
StatusMessage = "Store successfully updated";
|
||||
}
|
||||
|
||||
return RedirectToAction(nameof(CheckoutExperience), new
|
||||
{
|
||||
storeId = storeId
|
||||
storeId = StoreData.Id
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{storeId}")]
|
||||
public async Task<IActionResult> UpdateStore(string storeId)
|
||||
public IActionResult UpdateStore()
|
||||
{
|
||||
var store = await _Repo.FindStore(storeId, GetUserId());
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
var vm = new StoreViewModel();
|
||||
vm.SetExchangeRates(GetSupportedExchanges(), storeBlob.PreferredExchange.IsCoinAverage() ? "coinaverage" : storeBlob.PreferredExchange);
|
||||
vm.Id = store.Id;
|
||||
vm.StoreName = store.StoreName;
|
||||
vm.StoreWebsite = store.StoreWebsite;
|
||||
vm.NetworkFee = !storeBlob.NetworkFeeDisabled;
|
||||
vm.SpeedPolicy = store.SpeedPolicy;
|
||||
AddPaymentMethods(store, vm);
|
||||
vm.CanDelete = _Repo.CanDeleteStores();
|
||||
AddPaymentMethods(store, storeBlob, vm);
|
||||
vm.MonitoringExpiration = storeBlob.MonitoringExpiration;
|
||||
vm.InvoiceExpiration = storeBlob.InvoiceExpiration;
|
||||
vm.RateMultiplier = (double)storeBlob.GetRateMultiplier();
|
||||
vm.LightningDescriptionTemplate = storeBlob.LightningDescriptionTemplate;
|
||||
vm.PaymentTolerance = storeBlob.PaymentTolerance;
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
|
||||
private void AddPaymentMethods(StoreData store, StoreViewModel vm)
|
||||
private void AddPaymentMethods(StoreData store, StoreBlob storeBlob, StoreViewModel vm)
|
||||
{
|
||||
var excludeFilters = storeBlob.GetExcludedPaymentMethods();
|
||||
var derivationByCryptoCode =
|
||||
store
|
||||
.GetSupportedPaymentMethods(_NetworkProvider)
|
||||
@ -305,7 +427,9 @@ namespace BTCPayServer.Controllers
|
||||
vm.DerivationSchemes.Add(new StoreViewModel.DerivationScheme()
|
||||
{
|
||||
Crypto = network.CryptoCode,
|
||||
Value = strategy?.DerivationStrategyBase?.ToString() ?? string.Empty
|
||||
Value = strategy?.DerivationStrategyBase?.ToString() ?? string.Empty,
|
||||
WalletId = new WalletId(store.Id, network.CryptoCode),
|
||||
Enabled = !excludeFilters.Match(new Payments.PaymentMethodId(network.CryptoCode, Payments.PaymentTypes.BTCLike))
|
||||
});
|
||||
}
|
||||
|
||||
@ -317,88 +441,90 @@ namespace BTCPayServer.Controllers
|
||||
foreach (var network in _NetworkProvider.GetAll())
|
||||
{
|
||||
var lightning = lightningByCryptoCode.TryGet(network.CryptoCode);
|
||||
var paymentId = new Payments.PaymentMethodId(network.CryptoCode, Payments.PaymentTypes.LightningLike);
|
||||
vm.LightningNodes.Add(new StoreViewModel.LightningNode()
|
||||
{
|
||||
CryptoCode = network.CryptoCode,
|
||||
Address = lightning?.GetLightningUrl()?.BaseUri.AbsoluteUri ?? string.Empty
|
||||
Address = lightning?.GetLightningUrl()?.BaseUri.AbsoluteUri ?? string.Empty,
|
||||
Enabled = !excludeFilters.Match(paymentId)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("{storeId}")]
|
||||
public async Task<IActionResult> UpdateStore(string storeId, StoreViewModel model)
|
||||
public async Task<IActionResult> UpdateStore(StoreViewModel model, string command = null)
|
||||
{
|
||||
model.SetExchangeRates(GetSupportedExchanges(), model.PreferredExchange);
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(model);
|
||||
}
|
||||
if (model.PreferredExchange != null)
|
||||
model.PreferredExchange = model.PreferredExchange.Trim().ToLowerInvariant();
|
||||
var store = await _Repo.FindStore(storeId, GetUserId());
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
AddPaymentMethods(store, model);
|
||||
|
||||
bool needUpdate = false;
|
||||
if (store.SpeedPolicy != model.SpeedPolicy)
|
||||
if (StoreData.SpeedPolicy != model.SpeedPolicy)
|
||||
{
|
||||
needUpdate = true;
|
||||
store.SpeedPolicy = model.SpeedPolicy;
|
||||
StoreData.SpeedPolicy = model.SpeedPolicy;
|
||||
}
|
||||
if (store.StoreName != model.StoreName)
|
||||
if (StoreData.StoreName != model.StoreName)
|
||||
{
|
||||
needUpdate = true;
|
||||
store.StoreName = model.StoreName;
|
||||
StoreData.StoreName = model.StoreName;
|
||||
}
|
||||
if (store.StoreWebsite != model.StoreWebsite)
|
||||
if (StoreData.StoreWebsite != model.StoreWebsite)
|
||||
{
|
||||
needUpdate = true;
|
||||
store.StoreWebsite = model.StoreWebsite;
|
||||
StoreData.StoreWebsite = model.StoreWebsite;
|
||||
}
|
||||
|
||||
var blob = store.GetStoreBlob();
|
||||
var blob = StoreData.GetStoreBlob();
|
||||
blob.NetworkFeeDisabled = !model.NetworkFee;
|
||||
blob.MonitoringExpiration = model.MonitoringExpiration;
|
||||
blob.InvoiceExpiration = model.InvoiceExpiration;
|
||||
blob.LightningDescriptionTemplate = model.LightningDescriptionTemplate ?? string.Empty;
|
||||
blob.PaymentTolerance = model.PaymentTolerance;
|
||||
|
||||
bool newExchange = blob.PreferredExchange != model.PreferredExchange;
|
||||
blob.PreferredExchange = model.PreferredExchange;
|
||||
|
||||
blob.SetRateMultiplier(model.RateMultiplier);
|
||||
|
||||
if (store.SetStoreBlob(blob))
|
||||
if (StoreData.SetStoreBlob(blob))
|
||||
{
|
||||
needUpdate = true;
|
||||
}
|
||||
|
||||
if (!blob.PreferredExchange.IsCoinAverage() && newExchange)
|
||||
{
|
||||
|
||||
if (!GetSupportedExchanges().Select(c => c.Name).Contains(blob.PreferredExchange, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.PreferredExchange), $"Unsupported exchange ({model.RateSource})");
|
||||
return View(model);
|
||||
}
|
||||
}
|
||||
|
||||
if (needUpdate)
|
||||
{
|
||||
await _Repo.UpdateStore(store);
|
||||
await _Repo.UpdateStore(StoreData);
|
||||
StatusMessage = "Store successfully updated";
|
||||
}
|
||||
|
||||
return RedirectToAction(nameof(UpdateStore), new
|
||||
{
|
||||
storeId = storeId
|
||||
storeId = StoreData.Id
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{storeId}/delete")]
|
||||
public IActionResult DeleteStore(string storeId)
|
||||
{
|
||||
return View("Confirm", new ConfirmModel()
|
||||
{
|
||||
Action = "Delete this store",
|
||||
Title = "Delete this store",
|
||||
Description = "This action is irreversible and will remove all information related to this store. (Invoices, Apps etc...)",
|
||||
ButtonClass = "btn-danger"
|
||||
});
|
||||
}
|
||||
|
||||
private (String DisplayName, String Name)[] GetSupportedExchanges()
|
||||
[HttpPost]
|
||||
[Route("{storeId}/delete")]
|
||||
public async Task<IActionResult> DeleteStorePost(string storeId)
|
||||
{
|
||||
return new[] { ("Coin Average", "coinaverage") }.Concat(_CoinAverage.AvailableExchanges).ToArray();
|
||||
await _Repo.DeleteStore(StoreData.Id);
|
||||
StatusMessage = "Success: Store successfully deleted";
|
||||
return RedirectToAction(nameof(UserStoresController.ListStores), "UserStores");
|
||||
}
|
||||
|
||||
private CoinAverageExchange[] GetSupportedExchanges()
|
||||
{
|
||||
return _RateFactory.GetSupportedExchanges()
|
||||
.Select(c => c.Value)
|
||||
.OrderBy(s => s.Name, StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private DerivationStrategy ParseDerivationStrategy(string derivationScheme, Script hint, BTCPayNetwork network)
|
||||
@ -410,10 +536,10 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
[HttpGet]
|
||||
[Route("{storeId}/Tokens")]
|
||||
public async Task<IActionResult> ListTokens(string storeId)
|
||||
public async Task<IActionResult> ListTokens()
|
||||
{
|
||||
var model = new TokensViewModel();
|
||||
var tokens = await _TokenRepository.GetTokensByStoreIdAsync(storeId);
|
||||
var tokens = await _TokenRepository.GetTokensByStoreIdAsync(StoreData.Id);
|
||||
model.StatusMessage = StatusMessage;
|
||||
model.Tokens = tokens.Select(t => new TokenViewModel()
|
||||
{
|
||||
@ -422,30 +548,43 @@ namespace BTCPayServer.Controllers
|
||||
SIN = t.SIN,
|
||||
Id = t.Value
|
||||
}).ToArray();
|
||||
|
||||
model.ApiKey = (await _TokenRepository.GetLegacyAPIKeys(StoreData.Id)).FirstOrDefault();
|
||||
if (model.ApiKey == null)
|
||||
model.EncodedApiKey = "*API Key*";
|
||||
else
|
||||
model.EncodedApiKey = Encoders.Base64.EncodeData(Encoders.ASCII.DecodeData(model.ApiKey));
|
||||
return View(model);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("/api-tokens")]
|
||||
[Route("{storeId}/Tokens/Create")]
|
||||
public async Task<IActionResult> CreateToken(string storeId, CreateTokenViewModel model)
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> CreateToken(CreateTokenViewModel model)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(model);
|
||||
}
|
||||
model.Label = model.Label ?? String.Empty;
|
||||
storeId = model.StoreId ?? storeId;
|
||||
var userId = GetUserId();
|
||||
if (userId == null)
|
||||
return Unauthorized();
|
||||
var store = await _Repo.FindStore(storeId, userId);
|
||||
if (store == null)
|
||||
return Unauthorized();
|
||||
if (store.Role != StoreRoles.Owner)
|
||||
return Challenge(Policies.CookieAuthentication);
|
||||
|
||||
var store = StoreData;
|
||||
var storeId = StoreData?.Id;
|
||||
if (storeId == null)
|
||||
{
|
||||
StatusMessage = "Error: You need to be owner of this store to request pairing codes";
|
||||
return RedirectToAction(nameof(UserStoresController.ListStores), "UserStores");
|
||||
storeId = model.StoreId;
|
||||
store = await _Repo.FindStore(storeId, userId);
|
||||
if (store == null)
|
||||
return Challenge(Policies.CookieAuthentication);
|
||||
}
|
||||
|
||||
if (!store.HasClaim(Policies.CanModifyStoreSettings.Key))
|
||||
{
|
||||
return Challenge(Policies.CookieAuthentication);
|
||||
}
|
||||
|
||||
var tokenRequest = new TokenRequest()
|
||||
@ -486,11 +625,20 @@ namespace BTCPayServer.Controllers
|
||||
[HttpGet]
|
||||
[Route("/api-tokens")]
|
||||
[Route("{storeId}/Tokens/Create")]
|
||||
public async Task<IActionResult> CreateToken(string storeId)
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> CreateToken()
|
||||
{
|
||||
var userId = GetUserId();
|
||||
if (string.IsNullOrWhiteSpace(userId))
|
||||
return Unauthorized();
|
||||
return Challenge(Policies.CookieAuthentication);
|
||||
var storeId = StoreData?.Id;
|
||||
if (StoreData != null)
|
||||
{
|
||||
if (!StoreData.HasClaim(Policies.CanModifyStoreSettings.Key))
|
||||
{
|
||||
return Challenge(Policies.CookieAuthentication);
|
||||
}
|
||||
}
|
||||
var model = new CreateTokenViewModel();
|
||||
model.Facade = "merchant";
|
||||
ViewBag.HidePublicKey = storeId == null;
|
||||
@ -499,20 +647,25 @@ namespace BTCPayServer.Controllers
|
||||
model.StoreId = storeId;
|
||||
if (storeId == null)
|
||||
{
|
||||
model.Stores = new SelectList(await _Repo.GetStoresByUserId(userId), nameof(StoreData.Id), nameof(StoreData.StoreName), storeId);
|
||||
var stores = await _Repo.GetStoresByUserId(userId);
|
||||
model.Stores = new SelectList(stores.Where(s => s.HasClaim(Policies.CanModifyStoreSettings.Key)), nameof(StoreData.Id), nameof(StoreData.StoreName), storeId);
|
||||
if (model.Stores.Count() == 0)
|
||||
{
|
||||
StatusMessage = "Error: You need to be owner of at least one store before pairing";
|
||||
return RedirectToAction(nameof(UserStoresController.ListStores), "UserStores");
|
||||
}
|
||||
}
|
||||
|
||||
return View(model);
|
||||
}
|
||||
|
||||
|
||||
[HttpPost]
|
||||
[Route("{storeId}/Tokens/Delete")]
|
||||
public async Task<IActionResult> DeleteToken(string storeId, string tokenId)
|
||||
public async Task<IActionResult> DeleteToken(string tokenId)
|
||||
{
|
||||
var token = await _TokenRepository.GetToken(tokenId);
|
||||
if (token == null ||
|
||||
token.StoreId != storeId ||
|
||||
token.StoreId != StoreData.Id ||
|
||||
!await _TokenRepository.DeleteToken(tokenId))
|
||||
StatusMessage = "Failure to revoke this token";
|
||||
else
|
||||
@ -520,11 +673,26 @@ namespace BTCPayServer.Controllers
|
||||
return RedirectToAction(nameof(ListTokens));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("{storeId}/tokens/apikey")]
|
||||
public async Task<IActionResult> GenerateAPIKey()
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
await _TokenRepository.GenerateLegacyAPIKey(StoreData.Id);
|
||||
StatusMessage = "API Key re-generated";
|
||||
return RedirectToAction(nameof(ListTokens));
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("/api-access-request")]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> RequestPairing(string pairingCode, string selectedStore = null)
|
||||
{
|
||||
var userId = GetUserId();
|
||||
if (userId == null)
|
||||
return Challenge(Policies.CookieAuthentication);
|
||||
if (pairingCode == null)
|
||||
return NotFound();
|
||||
var pairing = await _TokenRepository.GetPairingAsync(pairingCode);
|
||||
@ -535,7 +703,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
else
|
||||
{
|
||||
var stores = await _Repo.GetStoresByUserId(GetUserId());
|
||||
var stores = await _Repo.GetStoresByUserId(userId);
|
||||
return View(new PairingModel()
|
||||
{
|
||||
Id = pairing.Id,
|
||||
@ -543,7 +711,7 @@ namespace BTCPayServer.Controllers
|
||||
Label = pairing.Label,
|
||||
SIN = pairing.SIN ?? "Server-Initiated Pairing",
|
||||
SelectedStore = selectedStore ?? stores.FirstOrDefault()?.Id,
|
||||
Stores = stores.Select(s => new PairingModel.StoreViewModel()
|
||||
Stores = stores.Where(u => u.HasClaim(Policies.CanModifyStoreSettings.Key)).Select(s => new PairingModel.StoreViewModel()
|
||||
{
|
||||
Id = s.Id,
|
||||
Name = string.IsNullOrEmpty(s.StoreName) ? s.Id : s.StoreName
|
||||
@ -554,19 +722,22 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
[HttpPost]
|
||||
[Route("/api-access-request")]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> Pair(string pairingCode, string selectedStore)
|
||||
{
|
||||
if (pairingCode == null)
|
||||
return NotFound();
|
||||
var store = await _Repo.FindStore(selectedStore, GetUserId());
|
||||
var userId = GetUserId();
|
||||
if (userId == null)
|
||||
return Challenge(Policies.CookieAuthentication);
|
||||
var store = await _Repo.FindStore(selectedStore, userId);
|
||||
var pairing = await _TokenRepository.GetPairingAsync(pairingCode);
|
||||
if (store == null || pairing == null)
|
||||
return NotFound();
|
||||
|
||||
if (store.Role != StoreRoles.Owner)
|
||||
if (!store.HasClaim(Policies.CanModifyStoreSettings.Key))
|
||||
{
|
||||
StatusMessage = "Error: You can't approve a pairing without being owner of the store";
|
||||
return RedirectToAction(nameof(UserStoresController.ListStores), "UserStores");
|
||||
return Challenge(Policies.CookieAuthentication);
|
||||
}
|
||||
|
||||
var pairingResult = await _TokenRepository.PairWithStoreAsync(pairingCode, store.Id);
|
||||
@ -592,6 +763,8 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
private string GetUserId()
|
||||
{
|
||||
if (User.Identity.AuthenticationType != Policies.CookieAuthentication)
|
||||
return null;
|
||||
return _UserManager.GetUserId(User);
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
@ -15,40 +16,23 @@ using NBXplorer.DerivationStrategy;
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
[Route("stores")]
|
||||
[Authorize(AuthenticationSchemes = "Identity.Application")]
|
||||
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
||||
[AutoValidateAntiforgeryToken]
|
||||
public partial class UserStoresController : Controller
|
||||
{
|
||||
private StoreRepository _Repo;
|
||||
private BTCPayNetworkProvider _NetworkProvider;
|
||||
private UserManager<ApplicationUser> _UserManager;
|
||||
private BTCPayWalletProvider _WalletProvider;
|
||||
|
||||
public UserStoresController(
|
||||
UserManager<ApplicationUser> userManager,
|
||||
BTCPayNetworkProvider networkProvider,
|
||||
BTCPayWalletProvider walletProvider,
|
||||
StoreRepository storeRepository)
|
||||
{
|
||||
_Repo = storeRepository;
|
||||
_NetworkProvider = networkProvider;
|
||||
_UserManager = userManager;
|
||||
_WalletProvider = walletProvider;
|
||||
}
|
||||
[HttpGet]
|
||||
[Route("{storeId}/delete")]
|
||||
public async Task<IActionResult> DeleteStore(string storeId)
|
||||
{
|
||||
var store = await _Repo.FindStore(storeId, GetUserId());
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
return View("Confirm", new ConfirmModel()
|
||||
{
|
||||
Title = "Delete store " + store.StoreName,
|
||||
Description = "This store will still be accessible to users sharing it",
|
||||
Action = "Delete"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("create")]
|
||||
@ -62,12 +46,27 @@ namespace BTCPayServer.Controllers
|
||||
get; set;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{storeId}/me/delete")]
|
||||
public IActionResult DeleteStore(string storeId)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
return View("Confirm", new ConfirmModel()
|
||||
{
|
||||
Title = "Delete store " + store.StoreName,
|
||||
Description = "This store will still be accessible to users sharing it",
|
||||
Action = "Delete"
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("{storeId}/delete")]
|
||||
[Route("{storeId}/me/delete")]
|
||||
public async Task<IActionResult> DeleteStorePost(string storeId)
|
||||
{
|
||||
var userId = GetUserId();
|
||||
var store = await _Repo.FindStore(storeId, GetUserId());
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
await _Repo.RemoveStore(storeId, userId);
|
||||
@ -83,17 +82,6 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
StoresViewModel result = new StoresViewModel();
|
||||
var stores = await _Repo.GetStoresByUserId(GetUserId());
|
||||
|
||||
var balances = stores
|
||||
.Select(s => s.GetSupportedPaymentMethods(_NetworkProvider)
|
||||
.OfType<DerivationStrategy>()
|
||||
.Select(d => ((Wallet: _WalletProvider.GetWallet(d.Network),
|
||||
DerivationStrategy: d.DerivationStrategyBase)))
|
||||
.Where(_ => _.Wallet != null)
|
||||
.Select(async _ => (await GetBalanceString(_)) + " " + _.Wallet.Network.CryptoCode))
|
||||
.ToArray();
|
||||
|
||||
await Task.WhenAll(balances.SelectMany(_ => _));
|
||||
for (int i = 0; i < stores.Length; i++)
|
||||
{
|
||||
var store = stores[i];
|
||||
@ -102,8 +90,7 @@ namespace BTCPayServer.Controllers
|
||||
Id = store.Id,
|
||||
Name = store.StoreName,
|
||||
WebSite = store.StoreWebsite,
|
||||
IsOwner = store.Role == StoreRoles.Owner,
|
||||
Balances = store.Role == StoreRoles.Owner ? balances[i].Select(t => t.Result).ToArray() : Array.Empty<string>()
|
||||
IsOwner = store.HasClaim(Policies.CanModifyStoreSettings.Key)
|
||||
});
|
||||
}
|
||||
return View(result);
|
||||
@ -120,25 +107,12 @@ namespace BTCPayServer.Controllers
|
||||
var store = await _Repo.CreateStore(GetUserId(), vm.Name);
|
||||
CreatedStoreId = store.Id;
|
||||
StatusMessage = "Store successfully created";
|
||||
return RedirectToAction(nameof(ListStores));
|
||||
}
|
||||
|
||||
private static async Task<string> GetBalanceString((BTCPayWallet Wallet, DerivationStrategyBase DerivationStrategy) _)
|
||||
{
|
||||
using (CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)))
|
||||
return RedirectToAction(nameof(StoresController.UpdateStore), "Stores", new
|
||||
{
|
||||
try
|
||||
{
|
||||
return (await _.Wallet.GetBalance(_.DerivationStrategy, cts.Token)).ToString();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return "--";
|
||||
}
|
||||
}
|
||||
storeId = store.Id
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
private string GetUserId()
|
||||
{
|
||||
return _UserManager.GetUserId(User);
|
||||
|
487
BTCPayServer/Controllers/WalletsController.cs
Normal file
487
BTCPayServer/Controllers/WalletsController.cs
Normal file
@ -0,0 +1,487 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net.WebSockets;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.ModelBinders;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.WalletViewModels;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using LedgerWallet;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
using NBitcoin;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using Newtonsoft.Json;
|
||||
using static BTCPayServer.Controllers.StoresController;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
[Route("wallets")]
|
||||
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
||||
[AutoValidateAntiforgeryToken]
|
||||
public class WalletsController : Controller
|
||||
{
|
||||
private StoreRepository _Repo;
|
||||
private BTCPayNetworkProvider _NetworkProvider;
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
private readonly IOptions<MvcJsonOptions> _mvcJsonOptions;
|
||||
private readonly NBXplorerDashboard _dashboard;
|
||||
private readonly ExplorerClientProvider _explorerProvider;
|
||||
private readonly IFeeProviderFactory _feeRateProvider;
|
||||
private readonly BTCPayWalletProvider _walletProvider;
|
||||
BTCPayRateProviderFactory _RateProvider;
|
||||
CurrencyNameTable _currencyTable;
|
||||
public WalletsController(StoreRepository repo,
|
||||
CurrencyNameTable currencyTable,
|
||||
BTCPayNetworkProvider networkProvider,
|
||||
UserManager<ApplicationUser> userManager,
|
||||
IOptions<MvcJsonOptions> mvcJsonOptions,
|
||||
NBXplorerDashboard dashboard,
|
||||
BTCPayRateProviderFactory rateProvider,
|
||||
ExplorerClientProvider explorerProvider,
|
||||
IFeeProviderFactory feeRateProvider,
|
||||
BTCPayWalletProvider walletProvider)
|
||||
{
|
||||
_currencyTable = currencyTable;
|
||||
_Repo = repo;
|
||||
_RateProvider = rateProvider;
|
||||
_NetworkProvider = networkProvider;
|
||||
_userManager = userManager;
|
||||
_mvcJsonOptions = mvcJsonOptions;
|
||||
_dashboard = dashboard;
|
||||
_explorerProvider = explorerProvider;
|
||||
_feeRateProvider = feeRateProvider;
|
||||
_walletProvider = walletProvider;
|
||||
}
|
||||
|
||||
public async Task<IActionResult> ListWallets()
|
||||
{
|
||||
var wallets = new ListWalletsViewModel();
|
||||
var stores = await _Repo.GetStoresByUserId(GetUserId());
|
||||
|
||||
var onChainWallets = stores
|
||||
.SelectMany(s => s.GetSupportedPaymentMethods(_NetworkProvider)
|
||||
.OfType<DerivationStrategy>()
|
||||
.Select(d => ((Wallet: _walletProvider.GetWallet(d.Network),
|
||||
DerivationStrategy: d.DerivationStrategyBase,
|
||||
Network: d.Network)))
|
||||
.Where(_ => _.Wallet != null)
|
||||
.Select(_ => (Wallet: _.Wallet,
|
||||
Store: s,
|
||||
Balance: GetBalanceString(_.Wallet, _.DerivationStrategy),
|
||||
DerivationStrategy: _.DerivationStrategy,
|
||||
Network: _.Network)))
|
||||
.ToList();
|
||||
|
||||
foreach (var wallet in onChainWallets)
|
||||
{
|
||||
ListWalletsViewModel.WalletViewModel walletVm = new ListWalletsViewModel.WalletViewModel();
|
||||
wallets.Wallets.Add(walletVm);
|
||||
walletVm.Balance = await wallet.Balance + " " + wallet.Wallet.Network.CryptoCode;
|
||||
if (!wallet.Store.HasClaim(Policies.CanModifyStoreSettings.Key))
|
||||
{
|
||||
walletVm.Balance = "";
|
||||
}
|
||||
walletVm.CryptoCode = wallet.Network.CryptoCode;
|
||||
walletVm.StoreId = wallet.Store.Id;
|
||||
walletVm.Id = new WalletId(wallet.Store.Id, wallet.Network.CryptoCode);
|
||||
walletVm.StoreName = wallet.Store.StoreName;
|
||||
walletVm.IsOwner = wallet.Store.HasClaim(Policies.CanModifyStoreSettings.Key);
|
||||
}
|
||||
|
||||
return View(wallets);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{walletId}")]
|
||||
public async Task<IActionResult> WalletTransactions(
|
||||
[ModelBinder(typeof(WalletIdModelBinder))]
|
||||
WalletId walletId)
|
||||
{
|
||||
var store = await _Repo.FindStore(walletId.StoreId, GetUserId());
|
||||
DerivationStrategy paymentMethod = GetPaymentMethod(walletId, store);
|
||||
if (paymentMethod == null)
|
||||
return NotFound();
|
||||
|
||||
var wallet = _walletProvider.GetWallet(paymentMethod.Network);
|
||||
var transactions = await wallet.FetchTransactions(paymentMethod.DerivationStrategyBase);
|
||||
|
||||
var model = new ListTransactionsViewModel();
|
||||
foreach(var tx in transactions.UnconfirmedTransactions.Transactions.Concat(transactions.ConfirmedTransactions.Transactions))
|
||||
{
|
||||
var vm = new ListTransactionsViewModel.TransactionViewModel();
|
||||
model.Transactions.Add(vm);
|
||||
vm.Id = tx.TransactionId.ToString();
|
||||
vm.Link = string.Format(CultureInfo.InvariantCulture, paymentMethod.Network.BlockExplorerLink, vm.Id);
|
||||
vm.Timestamp = tx.Timestamp;
|
||||
vm.Positive = tx.BalanceChange >= Money.Zero;
|
||||
vm.Balance = tx.BalanceChange.ToString();
|
||||
}
|
||||
model.Transactions = model.Transactions.OrderByDescending(t => t.Timestamp).ToList();
|
||||
return View(model);
|
||||
}
|
||||
|
||||
|
||||
[HttpGet]
|
||||
[Route("{walletId}/send")]
|
||||
public async Task<IActionResult> WalletSend(
|
||||
[ModelBinder(typeof(WalletIdModelBinder))]
|
||||
WalletId walletId)
|
||||
{
|
||||
if (walletId?.StoreId == null)
|
||||
return NotFound();
|
||||
var store = await _Repo.FindStore(walletId.StoreId, GetUserId());
|
||||
DerivationStrategy paymentMethod = GetPaymentMethod(walletId, store);
|
||||
if (paymentMethod == null)
|
||||
return NotFound();
|
||||
|
||||
var storeData = store.GetStoreBlob();
|
||||
var rateRules = store.GetStoreBlob().GetRateRules(_NetworkProvider);
|
||||
rateRules.Spread = 0.0m;
|
||||
var currencyPair = new Rating.CurrencyPair(paymentMethod.PaymentId.CryptoCode, GetCurrencyCode(storeData.DefaultLang) ?? "USD");
|
||||
WalletModel model = new WalletModel();
|
||||
model.ServerUrl = GetLedgerWebsocketUrl(this.HttpContext, walletId.CryptoCode, paymentMethod.DerivationStrategyBase);
|
||||
model.CryptoCurrency = walletId.CryptoCode;
|
||||
|
||||
using (CancellationTokenSource cts = new CancellationTokenSource())
|
||||
{
|
||||
try
|
||||
{
|
||||
cts.CancelAfter(TimeSpan.FromSeconds(5));
|
||||
var result = await _RateProvider.FetchRate(currencyPair, rateRules).WithCancellation(cts.Token);
|
||||
if (result.BidAsk != null)
|
||||
{
|
||||
model.Rate = result.BidAsk.Center;
|
||||
model.Divisibility = _currencyTable.GetNumberFormatInfo(currencyPair.Right, true).CurrencyDecimalDigits;
|
||||
model.Fiat = currencyPair.Right;
|
||||
}
|
||||
else
|
||||
{
|
||||
model.RateError = $"{result.EvaluatedRule} ({string.Join(", ", result.Errors.OfType<object>().ToArray())})";
|
||||
}
|
||||
}
|
||||
catch(Exception ex) { model.RateError = ex.Message; }
|
||||
}
|
||||
return View(model);
|
||||
}
|
||||
|
||||
private string GetCurrencyCode(string defaultLang)
|
||||
{
|
||||
if (defaultLang == null)
|
||||
return null;
|
||||
try
|
||||
{
|
||||
var ri = new RegionInfo(defaultLang);
|
||||
return ri.ISOCurrencySymbol;
|
||||
}
|
||||
catch(ArgumentException) { }
|
||||
return null;
|
||||
}
|
||||
|
||||
private DerivationStrategy GetPaymentMethod(WalletId walletId, StoreData store)
|
||||
{
|
||||
if (store == null || !store.HasClaim(Policies.CanModifyStoreSettings.Key))
|
||||
return null;
|
||||
|
||||
var paymentMethod = store
|
||||
.GetSupportedPaymentMethods(_NetworkProvider)
|
||||
.OfType<DerivationStrategy>()
|
||||
.FirstOrDefault(p => p.PaymentId.PaymentType == Payments.PaymentTypes.BTCLike && p.PaymentId.CryptoCode == walletId.CryptoCode);
|
||||
return paymentMethod;
|
||||
}
|
||||
|
||||
private static async Task<string> GetBalanceString(BTCPayWallet wallet, DerivationStrategyBase derivationStrategy)
|
||||
{
|
||||
using (CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)))
|
||||
{
|
||||
try
|
||||
{
|
||||
return (await wallet.GetBalance(derivationStrategy, cts.Token)).ToString();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return "--";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private string GetUserId()
|
||||
{
|
||||
return _userManager.GetUserId(User);
|
||||
}
|
||||
|
||||
public static string GetLedgerWebsocketUrl(HttpContext httpContext, string cryptoCode, DerivationStrategyBase derivationStrategy)
|
||||
{
|
||||
return $"{httpContext.Request.GetAbsoluteRoot().WithTrailingSlash()}ws/ledger/{cryptoCode}/{derivationStrategy?.ToString() ?? string.Empty}";
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("/ws/ledger/{cryptoCode}/{derivationScheme?}")]
|
||||
public async Task<IActionResult> LedgerConnection(
|
||||
string command,
|
||||
// getinfo
|
||||
string cryptoCode = null,
|
||||
// getxpub
|
||||
[ModelBinder(typeof(ModelBinders.DerivationSchemeModelBinder))]
|
||||
DerivationStrategyBase derivationScheme = null,
|
||||
int account = 0,
|
||||
// sendtoaddress
|
||||
string destination = null, string amount = null, string feeRate = null, string substractFees = null
|
||||
)
|
||||
{
|
||||
if (!HttpContext.WebSockets.IsWebSocketRequest)
|
||||
return NotFound();
|
||||
var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
|
||||
|
||||
using (var normalOperationTimeout = new CancellationTokenSource())
|
||||
using (var signTimeout = new CancellationTokenSource())
|
||||
{
|
||||
normalOperationTimeout.CancelAfter(TimeSpan.FromMinutes(30));
|
||||
var hw = new HardwareWalletService(webSocket);
|
||||
object result = null;
|
||||
try
|
||||
{
|
||||
BTCPayNetwork network = null;
|
||||
if (cryptoCode != null)
|
||||
{
|
||||
network = _NetworkProvider.GetNetwork(cryptoCode);
|
||||
if (network == null)
|
||||
throw new FormatException("Invalid value for crypto code");
|
||||
}
|
||||
|
||||
BitcoinAddress destinationAddress = null;
|
||||
if (destination != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
destinationAddress = BitcoinAddress.Create(destination, network.NBitcoinNetwork);
|
||||
}
|
||||
catch { }
|
||||
if (destinationAddress == null)
|
||||
throw new FormatException("Invalid value for destination");
|
||||
}
|
||||
|
||||
FeeRate feeRateValue = null;
|
||||
if (feeRate != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
feeRateValue = new FeeRate(Money.Satoshis(int.Parse(feeRate, CultureInfo.InvariantCulture)), 1);
|
||||
}
|
||||
catch { }
|
||||
if (feeRateValue == null || feeRateValue.FeePerK <= Money.Zero)
|
||||
throw new FormatException("Invalid value for fee rate");
|
||||
}
|
||||
|
||||
Money amountBTC = null;
|
||||
if (amount != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
amountBTC = Money.Parse(amount);
|
||||
}
|
||||
catch { }
|
||||
if (amountBTC == null || amountBTC <= Money.Zero)
|
||||
throw new FormatException("Invalid value for amount");
|
||||
}
|
||||
|
||||
bool subsctractFeesValue = false;
|
||||
if (substractFees != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
subsctractFeesValue = bool.Parse(substractFees);
|
||||
}
|
||||
catch { throw new FormatException("Invalid value for subtract fees"); }
|
||||
}
|
||||
if (command == "test")
|
||||
{
|
||||
result = await hw.Test(normalOperationTimeout.Token);
|
||||
}
|
||||
if (command == "getxpub")
|
||||
{
|
||||
var getxpubResult = await hw.GetExtPubKey(network, account, normalOperationTimeout.Token);
|
||||
result = getxpubResult;
|
||||
}
|
||||
if (command == "getinfo")
|
||||
{
|
||||
var strategy = GetDirectDerivationStrategy(derivationScheme);
|
||||
if (strategy == null || await hw.GetKeyPath(network, strategy, normalOperationTimeout.Token) == null)
|
||||
{
|
||||
throw new Exception($"This store is not configured to use this ledger");
|
||||
}
|
||||
|
||||
var feeProvider = _feeRateProvider.CreateFeeProvider(network);
|
||||
var recommendedFees = feeProvider.GetFeeRateAsync();
|
||||
var balance = _walletProvider.GetWallet(network).GetBalance(derivationScheme);
|
||||
result = new GetInfoResult() { Balance = (double)(await balance).ToDecimal(MoneyUnit.BTC), RecommendedSatoshiPerByte = (int)(await recommendedFees).GetFee(1).Satoshi };
|
||||
}
|
||||
|
||||
if (command == "sendtoaddress")
|
||||
{
|
||||
if (!_dashboard.IsFullySynched(network.CryptoCode, out var summary))
|
||||
throw new Exception($"{network.CryptoCode}: not started or fully synched");
|
||||
var strategy = GetDirectDerivationStrategy(derivationScheme);
|
||||
var wallet = _walletProvider.GetWallet(network);
|
||||
var change = wallet.GetChangeAddressAsync(derivationScheme);
|
||||
|
||||
var unspentCoins = await wallet.GetUnspentCoins(derivationScheme);
|
||||
var changeAddress = await change;
|
||||
var send = new[] { (
|
||||
destination: destinationAddress as IDestination,
|
||||
amount: amountBTC,
|
||||
substractFees: subsctractFeesValue) };
|
||||
|
||||
foreach (var element in send)
|
||||
{
|
||||
if (element.destination == null)
|
||||
throw new ArgumentNullException(nameof(element.destination));
|
||||
if (element.amount == null)
|
||||
throw new ArgumentNullException(nameof(element.amount));
|
||||
if (element.amount <= Money.Zero)
|
||||
throw new ArgumentOutOfRangeException(nameof(element.amount), "The amount should be above zero");
|
||||
}
|
||||
|
||||
var foundKeyPath = await hw.GetKeyPath(network, strategy, normalOperationTimeout.Token);
|
||||
if (foundKeyPath == null)
|
||||
{
|
||||
throw new HardwareWalletException($"This store is not configured to use this ledger");
|
||||
}
|
||||
|
||||
TransactionBuilder builder = new TransactionBuilder();
|
||||
builder.StandardTransactionPolicy.MinRelayTxFee = summary.Status.BitcoinStatus.MinRelayTxFee;
|
||||
builder.SetConsensusFactory(network.NBitcoinNetwork);
|
||||
builder.AddCoins(unspentCoins.Select(c => c.Coin).ToArray());
|
||||
|
||||
foreach (var element in send)
|
||||
{
|
||||
builder.Send(element.destination, element.amount);
|
||||
if (element.substractFees)
|
||||
builder.SubtractFees();
|
||||
}
|
||||
builder.SetChange(changeAddress.Item1);
|
||||
|
||||
if (network.MinFee == null)
|
||||
{
|
||||
builder.SendEstimatedFees(feeRateValue);
|
||||
}
|
||||
else
|
||||
{
|
||||
var estimatedFee = builder.EstimateFees(feeRateValue);
|
||||
if (network.MinFee > estimatedFee)
|
||||
builder.SendFees(network.MinFee);
|
||||
else
|
||||
builder.SendEstimatedFees(feeRateValue);
|
||||
}
|
||||
builder.Shuffle();
|
||||
var unsigned = builder.BuildTransaction(false);
|
||||
|
||||
var keypaths = new Dictionary<Script, KeyPath>();
|
||||
foreach (var c in unspentCoins)
|
||||
{
|
||||
keypaths.TryAdd(c.Coin.ScriptPubKey, c.KeyPath);
|
||||
}
|
||||
|
||||
var hasChange = unsigned.Outputs.Count == 2;
|
||||
var usedCoins = builder.FindSpentCoins(unsigned);
|
||||
|
||||
Dictionary<uint256, Transaction> parentTransactions = new Dictionary<uint256, Transaction>();
|
||||
|
||||
if (!strategy.Segwit)
|
||||
{
|
||||
var parentHashes = usedCoins.Select(c => c.Outpoint.Hash).ToHashSet();
|
||||
var explorer = _explorerProvider.GetExplorerClient(network);
|
||||
var getTransactionAsyncs = parentHashes.Select(h => (Op: explorer.GetTransactionAsync(h), Hash: h)).ToList();
|
||||
foreach (var getTransactionAsync in getTransactionAsyncs)
|
||||
{
|
||||
var tx = (await getTransactionAsync.Op);
|
||||
if (tx == null)
|
||||
throw new Exception($"Parent transaction {getTransactionAsync.Hash} not found");
|
||||
parentTransactions.Add(tx.Transaction.GetHash(), tx.Transaction);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
signTimeout.CancelAfter(TimeSpan.FromMinutes(5));
|
||||
var transaction = await hw.SignTransactionAsync(usedCoins.Select(c => new SignatureRequest
|
||||
{
|
||||
InputTransaction = parentTransactions.TryGet(c.Outpoint.Hash),
|
||||
InputCoin = c,
|
||||
KeyPath = foundKeyPath.Derive(keypaths[c.TxOut.ScriptPubKey]),
|
||||
PubKey = strategy.Root.Derive(keypaths[c.TxOut.ScriptPubKey]).PubKey
|
||||
}).ToArray(), unsigned, hasChange ? foundKeyPath.Derive(changeAddress.Item2) : null, signTimeout.Token);
|
||||
try
|
||||
{
|
||||
var broadcastResult = await wallet.BroadcastTransactionsAsync(new List<Transaction>() { transaction });
|
||||
if (!broadcastResult[0].Success)
|
||||
{
|
||||
throw new Exception($"RPC Error while broadcasting: {broadcastResult[0].RPCCode} {broadcastResult[0].RPCCodeMessage} {broadcastResult[0].RPCMessage}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception("Error while broadcasting: " + ex.Message);
|
||||
}
|
||||
wallet.InvalidateCache(derivationScheme);
|
||||
result = new SendToAddressResult() { TransactionId = transaction.GetHash().ToString() };
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{ result = new LedgerTestResult() { Success = false, Error = "Timeout" }; }
|
||||
catch (Exception ex)
|
||||
{ result = new LedgerTestResult() { Success = false, Error = ex.Message }; }
|
||||
finally { hw.Dispose(); }
|
||||
try
|
||||
{
|
||||
if (result != null)
|
||||
{
|
||||
UTF8Encoding UTF8NOBOM = new UTF8Encoding(false);
|
||||
var bytes = UTF8NOBOM.GetBytes(JsonConvert.SerializeObject(result, _mvcJsonOptions.Value.SerializerSettings));
|
||||
await webSocket.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Text, true, new CancellationTokenSource(2000).Token);
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
finally
|
||||
{
|
||||
await webSocket.CloseSocket();
|
||||
}
|
||||
}
|
||||
return new EmptyResult();
|
||||
}
|
||||
|
||||
private DirectDerivationStrategy GetDirectDerivationStrategy(DerivationStrategyBase strategy)
|
||||
{
|
||||
if (strategy == null)
|
||||
throw new Exception("The derivation scheme is not provided");
|
||||
var directStrategy = strategy as DirectDerivationStrategy;
|
||||
if (directStrategy == null)
|
||||
directStrategy = (strategy as P2SHDerivationStrategy).Inner as DirectDerivationStrategy;
|
||||
return directStrategy;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public class GetInfoResult
|
||||
{
|
||||
public int RecommendedSatoshiPerByte { get; set; }
|
||||
public double Balance { get; set; }
|
||||
}
|
||||
|
||||
public class SendToAddressResult
|
||||
{
|
||||
public string TransactionId { get; set; }
|
||||
}
|
||||
}
|
12
BTCPayServer/CorsPolicies.cs
Normal file
12
BTCPayServer/CorsPolicies.cs
Normal file
@ -0,0 +1,12 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public static class CorsPolicies
|
||||
{
|
||||
public const string All = "BTCPAY_ALL";
|
||||
}
|
||||
}
|
@ -21,7 +21,7 @@ namespace BTCPayServer
|
||||
return false;
|
||||
|
||||
var currency = match.Groups.Last().Value.ToUpperInvariant();
|
||||
var currencyData = _CurrencyTable.GetCurrencyData(currency);
|
||||
var currencyData = _CurrencyTable.GetCurrencyData(currency, false);
|
||||
if (currencyData == null)
|
||||
return false;
|
||||
v = Math.Round(v, currencyData.Divisibility);
|
||||
|
25
BTCPayServer/Data/APIKeyData.cs
Normal file
25
BTCPayServer/Data/APIKeyData.cs
Normal file
@ -0,0 +1,25 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
{
|
||||
public class APIKeyData
|
||||
{
|
||||
[MaxLength(50)]
|
||||
public string Id
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
[MaxLength(50)]
|
||||
public string StoreId
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public StoreData StoreData { get; set; }
|
||||
}
|
||||
}
|
@ -86,6 +86,11 @@ namespace BTCPayServer.Data
|
||||
get; set;
|
||||
}
|
||||
|
||||
public DbSet<APIKeyData> ApiKeys
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||
{
|
||||
var isConfigured = optionsBuilder.Options.Extensions.OfType<RelationalOptionsExtension>().Any();
|
||||
@ -97,14 +102,27 @@ namespace BTCPayServer.Data
|
||||
{
|
||||
base.OnModelCreating(builder);
|
||||
builder.Entity<InvoiceData>()
|
||||
.HasIndex(o => o.StoreDataId);
|
||||
.HasOne(o => o.StoreData)
|
||||
.WithMany(a => a.Invoices).OnDelete(DeleteBehavior.Cascade);
|
||||
builder.Entity<InvoiceData>().HasIndex(o => o.StoreDataId);
|
||||
|
||||
|
||||
builder.Entity<PaymentData>()
|
||||
.HasIndex(o => o.InvoiceDataId);
|
||||
.HasOne(o => o.InvoiceData)
|
||||
.WithMany(i => i.Payments).OnDelete(DeleteBehavior.Cascade);
|
||||
builder.Entity<PaymentData>()
|
||||
.HasIndex(o => o.InvoiceDataId);
|
||||
|
||||
|
||||
builder.Entity<RefundAddressesData>()
|
||||
.HasOne(o => o.InvoiceData)
|
||||
.WithMany(i => i.RefundAddresses).OnDelete(DeleteBehavior.Cascade);
|
||||
builder.Entity<RefundAddressesData>()
|
||||
.HasIndex(o => o.InvoiceDataId);
|
||||
|
||||
builder.Entity<UserStore>()
|
||||
.HasOne(o => o.StoreData)
|
||||
.WithMany(i => i.UserStores).OnDelete(DeleteBehavior.Cascade);
|
||||
builder.Entity<UserStore>()
|
||||
.HasKey(t => new
|
||||
{
|
||||
@ -112,7 +130,16 @@ namespace BTCPayServer.Data
|
||||
t.StoreDataId
|
||||
});
|
||||
|
||||
builder.Entity<APIKeyData>()
|
||||
.HasOne(o => o.StoreData)
|
||||
.WithMany(i => i.APIKeys)
|
||||
.HasForeignKey(i => i.StoreId).OnDelete(DeleteBehavior.Cascade);
|
||||
builder.Entity<APIKeyData>()
|
||||
.HasIndex(o => o.StoreId);
|
||||
|
||||
builder.Entity<AppData>()
|
||||
.HasOne(o => o.StoreData)
|
||||
.WithMany(i => i.Apps).OnDelete(DeleteBehavior.Cascade);
|
||||
builder.Entity<AppData>()
|
||||
.HasOne(a => a.StoreData);
|
||||
|
||||
@ -126,6 +153,10 @@ namespace BTCPayServer.Data
|
||||
.WithMany(t => t.UserStores)
|
||||
.HasForeignKey(pt => pt.StoreDataId);
|
||||
|
||||
|
||||
builder.Entity<AddressInvoiceData>()
|
||||
.HasOne(o => o.InvoiceData)
|
||||
.WithMany(i => i.AddressInvoices).OnDelete(DeleteBehavior.Cascade);
|
||||
builder.Entity<AddressInvoiceData>()
|
||||
#pragma warning disable CS0618
|
||||
.HasKey(o => o.Address);
|
||||
@ -134,12 +165,24 @@ namespace BTCPayServer.Data
|
||||
builder.Entity<PairingCodeData>()
|
||||
.HasKey(o => o.Id);
|
||||
|
||||
builder.Entity<PendingInvoiceData>()
|
||||
.HasOne(o => o.InvoiceData)
|
||||
.WithMany(o => o.PendingInvoices)
|
||||
.HasForeignKey(o => o.Id).OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
|
||||
builder.Entity<PairedSINData>()
|
||||
.HasOne(o => o.StoreData)
|
||||
.WithMany(i => i.PairedSINs).OnDelete(DeleteBehavior.Cascade);
|
||||
builder.Entity<PairedSINData>(b =>
|
||||
{
|
||||
b.HasIndex(o => o.SIN);
|
||||
b.HasIndex(o => o.StoreDataId);
|
||||
});
|
||||
|
||||
builder.Entity<HistoricalAddressInvoiceData>()
|
||||
.HasOne(o => o.InvoiceData)
|
||||
.WithMany(i => i.HistoricalAddressInvoices).OnDelete(DeleteBehavior.Cascade);
|
||||
builder.Entity<HistoricalAddressInvoiceData>()
|
||||
.HasKey(o => new
|
||||
{
|
||||
@ -149,6 +192,10 @@ namespace BTCPayServer.Data
|
||||
#pragma warning restore CS0618
|
||||
});
|
||||
|
||||
|
||||
builder.Entity<InvoiceEventData>()
|
||||
.HasOne(o => o.InvoiceData)
|
||||
.WithMany(i => i.Events).OnDelete(DeleteBehavior.Cascade);
|
||||
builder.Entity<InvoiceEventData>()
|
||||
.HasKey(o => new
|
||||
{
|
||||
|
@ -6,6 +6,11 @@ using System.Threading.Tasks;
|
||||
using Hangfire;
|
||||
using Hangfire.MemoryStorage;
|
||||
using Hangfire.PostgreSql;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Migrations;
|
||||
using JetBrains.Annotations;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Migrations.Operations;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
{
|
||||
@ -24,6 +29,15 @@ namespace BTCPayServer.Data
|
||||
_Type = type;
|
||||
}
|
||||
|
||||
|
||||
public DatabaseType Type
|
||||
{
|
||||
get
|
||||
{
|
||||
return _Type;
|
||||
}
|
||||
}
|
||||
|
||||
public ApplicationDbContext CreateContext()
|
||||
{
|
||||
var builder = new DbContextOptionsBuilder<ApplicationDbContext>();
|
||||
@ -31,20 +45,66 @@ namespace BTCPayServer.Data
|
||||
return new ApplicationDbContext(builder.Options);
|
||||
}
|
||||
|
||||
class CustomNpgsqlMigrationsSqlGenerator : NpgsqlMigrationsSqlGenerator
|
||||
{
|
||||
public CustomNpgsqlMigrationsSqlGenerator(MigrationsSqlGeneratorDependencies dependencies) : base(dependencies)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void Generate(NpgsqlCreateDatabaseOperation operation, IModel model, MigrationCommandListBuilder builder)
|
||||
{
|
||||
builder
|
||||
.Append("CREATE DATABASE ")
|
||||
.Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Name));
|
||||
|
||||
// POSTGRES gotcha: Indexed Text column (even if PK) are not used if we are not using C locale
|
||||
builder
|
||||
.Append(" TEMPLATE ")
|
||||
.Append(Dependencies.SqlGenerationHelper.DelimitIdentifier("template0"));
|
||||
|
||||
builder
|
||||
.Append(" LC_CTYPE ")
|
||||
.Append(Dependencies.SqlGenerationHelper.DelimitIdentifier("C"));
|
||||
|
||||
builder
|
||||
.Append(" LC_COLLATE ")
|
||||
.Append(Dependencies.SqlGenerationHelper.DelimitIdentifier("C"));
|
||||
|
||||
builder
|
||||
.Append(" ENCODING ")
|
||||
.Append(Dependencies.SqlGenerationHelper.DelimitIdentifier("UTF8"));
|
||||
|
||||
if (operation.Tablespace != null)
|
||||
{
|
||||
builder
|
||||
.Append(" TABLESPACE ")
|
||||
.Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Tablespace));
|
||||
}
|
||||
|
||||
builder.AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator);
|
||||
|
||||
EndStatement(builder, suppressTransaction: true);
|
||||
}
|
||||
}
|
||||
|
||||
public void ConfigureBuilder(DbContextOptionsBuilder builder)
|
||||
{
|
||||
if (_Type == DatabaseType.Sqlite)
|
||||
builder.UseSqlite(_ConnectionString);
|
||||
else if (_Type == DatabaseType.Postgres)
|
||||
builder.UseNpgsql(_ConnectionString);
|
||||
builder
|
||||
.UseNpgsql(_ConnectionString)
|
||||
.ReplaceService<IMigrationsSqlGenerator, CustomNpgsqlMigrationsSqlGenerator>();
|
||||
}
|
||||
|
||||
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);
|
||||
builder.UseMemoryStorage();
|
||||
//We always use memory storage because of incompatibilities with the latest postgres in 2.1
|
||||
//if (_Type == DatabaseType.Sqlite)
|
||||
// builder.UseMemoryStorage(); //Sqlite provider does not support multiple workers
|
||||
//else if (_Type == DatabaseType.Postgres)
|
||||
// builder.UsePostgreSqlStorage(_ConnectionString);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -12,6 +12,11 @@ namespace BTCPayServer.Data
|
||||
get; set;
|
||||
}
|
||||
|
||||
public InvoiceData InvoiceData
|
||||
{
|
||||
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"
|
||||
|
@ -80,5 +80,6 @@ namespace BTCPayServer.Data
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public List<PendingInvoiceData> PendingInvoices { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,10 @@ namespace BTCPayServer.Data
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public InvoiceData InvoiceData
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string UniqueId { get; internal set; }
|
||||
public DateTimeOffset Timestamp
|
||||
{
|
||||
|
@ -21,6 +21,9 @@ namespace BTCPayServer.Data
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public StoreData StoreData { get; set; }
|
||||
|
||||
public string Label
|
||||
{
|
||||
get;
|
||||
|
@ -11,5 +11,6 @@ namespace BTCPayServer.Data
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public InvoiceData InvoiceData { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -14,6 +14,11 @@ using Newtonsoft.Json.Linq;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.JsonConverters;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using BTCPayServer.Services;
|
||||
using System.Security.Claims;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Rating;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
{
|
||||
@ -29,12 +34,13 @@ namespace BTCPayServer.Data
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public List<AppData> Apps
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public List<InvoiceData> Invoices { get; set; }
|
||||
|
||||
[Obsolete("Use GetDerivationStrategies instead")]
|
||||
public string DerivationStrategy
|
||||
{
|
||||
@ -120,7 +126,7 @@ namespace BTCPayServer.Data
|
||||
}
|
||||
}
|
||||
|
||||
if(!existing && supportedPaymentMethod == null && paymentMethodId.IsBTCOnChain)
|
||||
if (!existing && supportedPaymentMethod == null && paymentMethodId.IsBTCOnChain)
|
||||
{
|
||||
DerivationStrategy = null;
|
||||
}
|
||||
@ -151,10 +157,35 @@ namespace BTCPayServer.Data
|
||||
}
|
||||
|
||||
[NotMapped]
|
||||
[Obsolete]
|
||||
public string Role
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public Claim[] GetClaims()
|
||||
{
|
||||
List<Claim> claims = new List<Claim>();
|
||||
#pragma warning disable CS0612 // Type or member is obsolete
|
||||
var role = Role;
|
||||
#pragma warning restore CS0612 // Type or member is obsolete
|
||||
if (role == StoreRoles.Owner)
|
||||
{
|
||||
claims.Add(new Claim(Policies.CanModifyStoreSettings.Key, Id));
|
||||
claims.Add(new Claim(Policies.CanUseStore.Key, Id));
|
||||
}
|
||||
if (role == StoreRoles.Guest)
|
||||
{
|
||||
claims.Add(new Claim(Policies.CanUseStore.Key, Id));
|
||||
}
|
||||
return claims.ToArray();
|
||||
}
|
||||
|
||||
public bool HasClaim(string claim)
|
||||
{
|
||||
return GetClaims().Any(c => c.Type == claim);
|
||||
}
|
||||
|
||||
public byte[] StoreBlob
|
||||
{
|
||||
get;
|
||||
@ -162,11 +193,13 @@ namespace BTCPayServer.Data
|
||||
}
|
||||
[Obsolete("Use GetDefaultCrypto instead")]
|
||||
public string DefaultCrypto { get; set; }
|
||||
public List<PairedSINData> PairedSINs { get; set; }
|
||||
public IEnumerable<APIKeyData> APIKeys { get; set; }
|
||||
|
||||
#pragma warning disable CS0618
|
||||
public string GetDefaultCrypto()
|
||||
public string GetDefaultCrypto(BTCPayNetworkProvider networkProvider = null)
|
||||
{
|
||||
return DefaultCrypto ?? "BTC";
|
||||
return DefaultCrypto ?? (networkProvider == null ? "BTC" : GetSupportedPaymentMethods(networkProvider).First().PaymentId.CryptoCode);
|
||||
}
|
||||
public void SetDefaultCrypto(string defaultCryptoCurrency)
|
||||
{
|
||||
@ -178,7 +211,10 @@ namespace BTCPayServer.Data
|
||||
|
||||
public StoreBlob GetStoreBlob()
|
||||
{
|
||||
return StoreBlob == null ? new StoreBlob() : new Serializer(Dummy).ToObject<StoreBlob>(Encoding.UTF8.GetString(StoreBlob));
|
||||
var result = StoreBlob == null ? new StoreBlob() : new Serializer(Dummy).ToObject<StoreBlob>(Encoding.UTF8.GetString(StoreBlob));
|
||||
if (result.PreferredExchange == null)
|
||||
result.PreferredExchange = CoinAverageRateProvider.CoinAverageName;
|
||||
return result;
|
||||
}
|
||||
|
||||
public bool SetStoreBlob(StoreBlob storeBlob)
|
||||
@ -192,9 +228,9 @@ namespace BTCPayServer.Data
|
||||
}
|
||||
}
|
||||
|
||||
public class RateRule
|
||||
public class RateRule_Obsolete
|
||||
{
|
||||
public RateRule()
|
||||
public RateRule_Obsolete()
|
||||
{
|
||||
RuleName = "Multiplier";
|
||||
}
|
||||
@ -214,6 +250,8 @@ namespace BTCPayServer.Data
|
||||
{
|
||||
InvoiceExpiration = 15;
|
||||
MonitoringExpiration = 60;
|
||||
PaymentTolerance = 0;
|
||||
RequiresRefundEmail = true;
|
||||
}
|
||||
public bool NetworkFeeDisabled
|
||||
{
|
||||
@ -223,6 +261,9 @@ namespace BTCPayServer.Data
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public bool RequiresRefundEmail { get; set; }
|
||||
|
||||
public string DefaultLang { get; set; }
|
||||
[DefaultValue(60)]
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
|
||||
@ -240,24 +281,10 @@ namespace BTCPayServer.Data
|
||||
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 decimal Spread { get; set; } = 0.0m;
|
||||
|
||||
public List<RateRule> RateRules { get; set; } = new List<RateRule>();
|
||||
[Obsolete]
|
||||
public List<RateRule_Obsolete> RateRules { get; set; } = new List<RateRule_Obsolete>();
|
||||
public string PreferredExchange { get; set; }
|
||||
|
||||
[JsonConverter(typeof(CurrencyValueJsonConverter))]
|
||||
@ -269,6 +296,11 @@ namespace BTCPayServer.Data
|
||||
public Uri CustomLogo { get; set; }
|
||||
[JsonConverter(typeof(UriJsonConverter))]
|
||||
public Uri CustomCSS { get; set; }
|
||||
public string HtmlTitle { get; set; }
|
||||
|
||||
public bool RateScripting { get; set; }
|
||||
|
||||
public string RateScript { get; set; }
|
||||
|
||||
|
||||
string _LightningDescriptionTemplate;
|
||||
@ -284,12 +316,77 @@ namespace BTCPayServer.Data
|
||||
}
|
||||
}
|
||||
|
||||
public RateRules GetRateRules()
|
||||
[DefaultValue(0)]
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
|
||||
public double PaymentTolerance { get; set; }
|
||||
|
||||
public BTCPayServer.Rating.RateRules GetRateRules(BTCPayNetworkProvider networkProvider)
|
||||
{
|
||||
return new RateRules(RateRules)
|
||||
if (!RateScripting ||
|
||||
string.IsNullOrEmpty(RateScript) ||
|
||||
!BTCPayServer.Rating.RateRules.TryParse(RateScript, out var rules))
|
||||
{
|
||||
PreferredExchange = PreferredExchange
|
||||
};
|
||||
return GetDefaultRateRules(networkProvider);
|
||||
}
|
||||
else
|
||||
{
|
||||
rules.Spread = Spread;
|
||||
return rules;
|
||||
}
|
||||
}
|
||||
|
||||
public RateRules GetDefaultRateRules(BTCPayNetworkProvider networkProvider)
|
||||
{
|
||||
StringBuilder builder = new StringBuilder();
|
||||
foreach (var network in networkProvider.GetAll())
|
||||
{
|
||||
if (network.DefaultRateRules.Length != 0)
|
||||
{
|
||||
builder.AppendLine($"// Default rate rules for {network.CryptoCode}");
|
||||
foreach (var line in network.DefaultRateRules)
|
||||
{
|
||||
builder.AppendLine(line);
|
||||
}
|
||||
builder.AppendLine($"////////");
|
||||
builder.AppendLine();
|
||||
}
|
||||
}
|
||||
|
||||
var preferredExchange = string.IsNullOrEmpty(PreferredExchange) ? "coinaverage" : PreferredExchange;
|
||||
builder.AppendLine($"X_X = {preferredExchange}(X_X);");
|
||||
|
||||
BTCPayServer.Rating.RateRules.TryParse(builder.ToString(), out var rules);
|
||||
rules.Spread = Spread;
|
||||
return rules;
|
||||
}
|
||||
|
||||
[Obsolete("Use GetExcludedPaymentMethods instead")]
|
||||
public string[] ExcludedPaymentMethods { get; set; }
|
||||
|
||||
public IPaymentFilter GetExcludedPaymentMethods()
|
||||
{
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
if (ExcludedPaymentMethods == null || ExcludedPaymentMethods.Length == 0)
|
||||
return PaymentFilter.Never();
|
||||
return PaymentFilter.Any(ExcludedPaymentMethods.Select(p => PaymentFilter.WhereIs(PaymentMethodId.Parse(p))).ToArray());
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
}
|
||||
|
||||
public bool IsExcluded(PaymentMethodId paymentMethodId)
|
||||
{
|
||||
return GetExcludedPaymentMethods().Match(paymentMethodId);
|
||||
}
|
||||
|
||||
public void SetExcluded(PaymentMethodId paymentMethodId, bool value)
|
||||
{
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
var methods = new HashSet<string>(ExcludedPaymentMethods ?? Array.Empty<string>());
|
||||
if (value)
|
||||
methods.Add(paymentMethodId.ToString());
|
||||
else
|
||||
methods.Remove(paymentMethodId.ToString());
|
||||
ExcludedPaymentMethods = methods.ToArray();
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -72,7 +72,7 @@ namespace BTCPayServer
|
||||
}
|
||||
try
|
||||
{
|
||||
var data = Encoders.Base58Check.DecodeData(parts[i]);
|
||||
var data = Network.GetBase58CheckEncoder().DecodeData(parts[i]);
|
||||
if (data.Length < 4)
|
||||
continue;
|
||||
var prefix = Utils.ToUInt32(data, false);
|
||||
@ -80,7 +80,7 @@ namespace BTCPayServer
|
||||
for (int ii = 0; ii < 4; ii++)
|
||||
data[ii] = standardPrefix[ii];
|
||||
|
||||
var derivationScheme = new BitcoinExtPubKey(Encoders.Base58Check.EncodeData(data), Network).ToString();
|
||||
var derivationScheme = new BitcoinExtPubKey(Network.GetBase58CheckEncoder().EncodeData(data), Network).ToString();
|
||||
electrumMapping.TryGetValue(prefix, out string[] labels);
|
||||
if (labels != null)
|
||||
{
|
||||
|
@ -32,7 +32,7 @@ namespace BTCPayServer
|
||||
|
||||
public BTCPayNetwork Network { get { return this._Network; } }
|
||||
|
||||
public DerivationStrategyBase DerivationStrategyBase { get { return this._DerivationStrategy; } }
|
||||
public DerivationStrategyBase DerivationStrategyBase => this._DerivationStrategy;
|
||||
|
||||
public PaymentMethodId PaymentId => new PaymentMethodId(Network.CryptoCode, PaymentTypes.BTCLike);
|
||||
|
||||
|
@ -8,24 +8,20 @@ namespace BTCPayServer.Events
|
||||
{
|
||||
public class InvoiceEvent
|
||||
{
|
||||
public InvoiceEvent(InvoiceEntity invoice, int code, string name) : this(invoice.Id, code, name)
|
||||
public InvoiceEvent(Models.InvoiceResponse invoice, int code, string name)
|
||||
{
|
||||
|
||||
}
|
||||
public InvoiceEvent(string invoiceId, int code, string name)
|
||||
{
|
||||
InvoiceId = invoiceId;
|
||||
Invoice = invoice;
|
||||
EventCode = code;
|
||||
Name = name;
|
||||
}
|
||||
|
||||
public string InvoiceId { get; set; }
|
||||
public Models.InvoiceResponse Invoice { get; set; }
|
||||
public int EventCode { get; set; }
|
||||
public string Name { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"Invoice {InvoiceId} new event: {Name} ({EventCode})";
|
||||
return $"Invoice {Invoice.Id} new event: {Name} ({EventCode})";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,21 +6,6 @@ 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)
|
||||
|
@ -29,33 +29,14 @@ using Microsoft.AspNetCore.Identity;
|
||||
using BTCPayServer.Models;
|
||||
using System.Security.Claims;
|
||||
using System.Globalization;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Data;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public static class Extensions
|
||||
{
|
||||
public static string Prettify(this TimeSpan timeSpan)
|
||||
{
|
||||
if (timeSpan.TotalMinutes < 1)
|
||||
{
|
||||
return $"{(int)timeSpan.TotalSeconds} second{Plural((int)timeSpan.TotalSeconds)}";
|
||||
}
|
||||
if (timeSpan.TotalHours < 1)
|
||||
{
|
||||
return $"{(int)timeSpan.TotalMinutes} minute{Plural((int)timeSpan.TotalMinutes)}";
|
||||
}
|
||||
if (timeSpan.Days < 1)
|
||||
{
|
||||
return $"{(int)timeSpan.TotalHours} hour{Plural((int)timeSpan.TotalHours)}";
|
||||
}
|
||||
return $"{(int)timeSpan.TotalDays} day{Plural((int)timeSpan.TotalDays)}";
|
||||
}
|
||||
|
||||
private static string Plural(int totalDays)
|
||||
{
|
||||
return totalDays > 1 ? "s" : string.Empty;
|
||||
}
|
||||
|
||||
public static string PrettyPrint(this TimeSpan expiration)
|
||||
{
|
||||
StringBuilder builder = new StringBuilder();
|
||||
@ -102,10 +83,13 @@ namespace BTCPayServer
|
||||
return activeProvider != "Microsoft.EntityFrameworkCore.Sqlite";
|
||||
}
|
||||
|
||||
public static bool IsCoinAverage(this string exchangeName)
|
||||
public static bool SupportDropForeignKey(this Microsoft.EntityFrameworkCore.Migrations.Migration migration, string activeProvider)
|
||||
{
|
||||
string[] coinAverages = new[] { "coinaverage", "bitcoinaverage" };
|
||||
return String.IsNullOrWhiteSpace(exchangeName) ? true : coinAverages.Contains(exchangeName, StringComparer.OrdinalIgnoreCase) ? true : false;
|
||||
return activeProvider != "Microsoft.EntityFrameworkCore.Sqlite";
|
||||
}
|
||||
public static bool SupportDropForeignKey(this DatabaseFacade facade)
|
||||
{
|
||||
return facade.ProviderName != "Microsoft.EntityFrameworkCore.Sqlite";
|
||||
}
|
||||
|
||||
public static async Task<Dictionary<uint256, TransactionResult>> GetTransactions(this BTCPayWallet client, uint256[] hashes, CancellationToken cts = default(CancellationToken))
|
||||
@ -124,6 +108,26 @@ namespace BTCPayServer
|
||||
return str + "/";
|
||||
}
|
||||
|
||||
public static void SetHeaderOnStarting(this HttpResponse resp, string name, string value)
|
||||
{
|
||||
if (resp.HasStarted)
|
||||
return;
|
||||
resp.OnStarting(() =>
|
||||
{
|
||||
SetHeader(resp, name, value);
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
}
|
||||
|
||||
public static void SetHeader(this HttpResponse resp, string name, string value)
|
||||
{
|
||||
var existing = resp.Headers[name].FirstOrDefault();
|
||||
if (existing != null && value == null)
|
||||
resp.Headers.Remove(name);
|
||||
else
|
||||
resp.Headers[name] = value;
|
||||
}
|
||||
|
||||
public static string GetAbsoluteRoot(this HttpRequest request)
|
||||
{
|
||||
return string.Concat(
|
||||
@ -133,6 +137,31 @@ namespace BTCPayServer
|
||||
request.PathBase.ToUriComponent());
|
||||
}
|
||||
|
||||
public static string GetCurrentUrl(this HttpRequest request)
|
||||
{
|
||||
return string.Concat(
|
||||
request.Scheme,
|
||||
"://",
|
||||
request.Host.ToUriComponent(),
|
||||
request.PathBase.ToUriComponent(),
|
||||
request.Path.ToUriComponent());
|
||||
}
|
||||
|
||||
public static string GetCurrentPath(this HttpRequest request)
|
||||
{
|
||||
return string.Concat(
|
||||
request.PathBase.ToUriComponent(),
|
||||
request.Path.ToUriComponent());
|
||||
}
|
||||
|
||||
public static string GetAbsoluteUri(this HttpRequest request, string redirectUrl)
|
||||
{
|
||||
bool isRelative =
|
||||
(redirectUrl.Length > 0 && redirectUrl[0] == '/')
|
||||
|| !new Uri(redirectUrl, UriKind.RelativeOrAbsolute).IsAbsoluteUri;
|
||||
return isRelative ? request.GetAbsoluteRoot() + redirectUrl : redirectUrl;
|
||||
}
|
||||
|
||||
public static IServiceCollection ConfigureBTCPayServer(this IServiceCollection services, IConfiguration conf)
|
||||
{
|
||||
services.Configure<BTCPayServerOptions>(o =>
|
||||
@ -142,12 +171,76 @@ namespace BTCPayServer
|
||||
return services;
|
||||
}
|
||||
|
||||
|
||||
public static BitIdentity GetBitIdentity(this Controller controller, bool throws = true)
|
||||
public static string GetSIN(this ClaimsPrincipal principal)
|
||||
{
|
||||
if (!(controller.User.Identity is BitIdentity))
|
||||
return throws ? throw new UnauthorizedAccessException("no-bitid") : (BitIdentity)null;
|
||||
return (BitIdentity)controller.User.Identity;
|
||||
return principal.Claims.Where(c => c.Type == Claims.SIN).Select(c => c.Value).FirstOrDefault();
|
||||
}
|
||||
|
||||
public static string GetStoreId(this ClaimsPrincipal principal)
|
||||
{
|
||||
return principal.Claims.Where(c => c.Type == Claims.OwnStore).Select(c => c.Value).FirstOrDefault();
|
||||
}
|
||||
|
||||
public static void SetIsBitpayAPI(this HttpContext ctx, bool value)
|
||||
{
|
||||
NBitcoin.Extensions.TryAdd(ctx.Items, "IsBitpayAPI", value);
|
||||
}
|
||||
|
||||
public static void AddRange<T>(this HashSet<T> hashSet, IEnumerable<T> items)
|
||||
{
|
||||
foreach (var item in items)
|
||||
{
|
||||
hashSet.Add(item);
|
||||
}
|
||||
}
|
||||
public static bool GetIsBitpayAPI(this HttpContext ctx)
|
||||
{
|
||||
return ctx.Items.TryGetValue("IsBitpayAPI", out object obj) &&
|
||||
obj is bool b && b;
|
||||
}
|
||||
|
||||
public static void SetBitpayAuth(this HttpContext ctx, (string Signature, String Id, String Authorization) value)
|
||||
{
|
||||
NBitcoin.Extensions.TryAdd(ctx.Items, "BitpayAuth", value);
|
||||
}
|
||||
|
||||
public static async Task<T> WithCancellation<T>(this Task<T> task, CancellationToken cancellationToken)
|
||||
{
|
||||
using (var delayCTS = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
|
||||
{
|
||||
var waiting = Task.Delay(-1, delayCTS.Token);
|
||||
var doing = task;
|
||||
await Task.WhenAny(waiting, doing);
|
||||
delayCTS.Cancel();
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
return await doing;
|
||||
}
|
||||
}
|
||||
public static async Task WithCancellation(this Task task, CancellationToken cancellationToken)
|
||||
{
|
||||
using (var delayCTS = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
|
||||
{
|
||||
var waiting = Task.Delay(-1, delayCTS.Token);
|
||||
var doing = task;
|
||||
await Task.WhenAny(waiting, doing);
|
||||
delayCTS.Cancel();
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
}
|
||||
}
|
||||
|
||||
public static (string Signature, String Id, String Authorization) GetBitpayAuth(this HttpContext ctx)
|
||||
{
|
||||
ctx.Items.TryGetValue("BitpayAuth", out object obj);
|
||||
return ((string Signature, String Id, String Authorization))obj;
|
||||
}
|
||||
|
||||
public static StoreData GetStoreData(this HttpContext ctx)
|
||||
{
|
||||
return ctx.Items.TryGet("BTCPAY.STOREDATA") as StoreData;
|
||||
}
|
||||
public static void SetStoreData(this HttpContext ctx, StoreData storeData)
|
||||
{
|
||||
ctx.Items["BTCPAY.STOREDATA"] = storeData;
|
||||
}
|
||||
|
||||
private static JsonSerializerSettings jsonSettings = new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() };
|
||||
@ -156,13 +249,5 @@ namespace BTCPayServer
|
||||
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 + "');");
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
106
BTCPayServer/Filters/ContentSecurityPolicyAttribute.cs
Normal file
106
BTCPayServer/Filters/ContentSecurityPolicyAttribute.cs
Normal file
@ -0,0 +1,106 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Security;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
|
||||
namespace BTCPayServer.Filters
|
||||
{
|
||||
public interface IContentSecurityPolicy : IFilterMetadata { }
|
||||
public class ContentSecurityPolicyAttribute : Attribute, IActionFilter, IContentSecurityPolicy
|
||||
{
|
||||
public void OnActionExecuted(ActionExecutedContext context)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public bool AutoSelf { get; set; } = true;
|
||||
public bool UnsafeInline { get; set; } = true;
|
||||
public bool FixWebsocket { get; set; } = true;
|
||||
public string FontSrc { get; set; } = null;
|
||||
public string ImgSrc { get; set; } = null;
|
||||
public string DefaultSrc { get; set; }
|
||||
public string StyleSrc { get; set; }
|
||||
public string ScriptSrc { get; set; }
|
||||
|
||||
public void OnActionExecuting(ActionExecutingContext context)
|
||||
{
|
||||
if (context.IsEffectivePolicy<IContentSecurityPolicy>(this))
|
||||
{
|
||||
var policies = context.HttpContext.RequestServices.GetService(typeof(ContentSecurityPolicies)) as ContentSecurityPolicies;
|
||||
if (policies == null)
|
||||
return;
|
||||
if (DefaultSrc != null)
|
||||
{
|
||||
policies.Add(new ConsentSecurityPolicy("default-src", DefaultSrc));
|
||||
}
|
||||
if (UnsafeInline)
|
||||
{
|
||||
policies.Add(new ConsentSecurityPolicy("script-src", "'unsafe-inline'"));
|
||||
}
|
||||
if (!string.IsNullOrEmpty(FontSrc))
|
||||
{
|
||||
policies.Add(new ConsentSecurityPolicy("font-src", FontSrc));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(ImgSrc))
|
||||
{
|
||||
policies.Add(new ConsentSecurityPolicy("img-src", ImgSrc));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(StyleSrc))
|
||||
{
|
||||
policies.Add(new ConsentSecurityPolicy("style-src", StyleSrc));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(ScriptSrc))
|
||||
{
|
||||
policies.Add(new ConsentSecurityPolicy("script-src", ScriptSrc));
|
||||
}
|
||||
|
||||
if (FixWebsocket && AutoSelf) // Self does not match wss:// and ws:// :(
|
||||
{
|
||||
var request = context.HttpContext.Request;
|
||||
|
||||
var url = string.Concat(
|
||||
request.Scheme.Equals("http", StringComparison.OrdinalIgnoreCase) ? "ws" : "wss",
|
||||
"://",
|
||||
request.Host.ToUriComponent(),
|
||||
request.PathBase.ToUriComponent());
|
||||
policies.Add(new ConsentSecurityPolicy("connect-src", url));
|
||||
}
|
||||
|
||||
context.HttpContext.Response.OnStarting(() =>
|
||||
{
|
||||
if (!policies.HasRules)
|
||||
return Task.CompletedTask;
|
||||
if (AutoSelf)
|
||||
{
|
||||
bool hasSelf = false;
|
||||
foreach (var group in policies.Rules.GroupBy(p => p.Name))
|
||||
{
|
||||
hasSelf = group.Any(g => g.Value.Contains("'self'", StringComparison.OrdinalIgnoreCase));
|
||||
if (!hasSelf && !group.Any(g => g.Value.Contains("'none'", StringComparison.OrdinalIgnoreCase) ||
|
||||
g.Value.Contains("*", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
policies.Add(new ConsentSecurityPolicy(group.Key, "'self'"));
|
||||
hasSelf = true;
|
||||
}
|
||||
if (hasSelf)
|
||||
{
|
||||
foreach (var authorized in policies.Authorized)
|
||||
{
|
||||
policies.Add(new ConsentSecurityPolicy(group.Key, authorized));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
context.HttpContext.Response.SetHeader("Content-Security-Policy", policies.ToString());
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -43,9 +43,7 @@ namespace BTCPayServer.Filters
|
||||
|
||||
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;
|
||||
return context.RouteContext.HttpContext.GetIsBitpayAPI() == IsBitpayAPI;
|
||||
}
|
||||
}
|
||||
|
||||
|
30
BTCPayServer/Filters/ReferrerPolicyAttribute.cs
Normal file
30
BTCPayServer/Filters/ReferrerPolicyAttribute.cs
Normal file
@ -0,0 +1,30 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
|
||||
namespace BTCPayServer.Filters
|
||||
{
|
||||
public interface IReferrerPolicy : IFilterMetadata { }
|
||||
public class ReferrerPolicyAttribute : Attribute, IActionFilter
|
||||
{
|
||||
public ReferrerPolicyAttribute(string value)
|
||||
{
|
||||
Value = value;
|
||||
}
|
||||
public string Value { get; set; }
|
||||
public void OnActionExecuted(ActionExecutedContext context)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public void OnActionExecuting(ActionExecutingContext context)
|
||||
{
|
||||
if (context.IsEffectivePolicy<ReferrerPolicyAttribute>(this))
|
||||
{
|
||||
context.HttpContext.Response.SetHeaderOnStarting("Referrer-Policy", Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
25
BTCPayServer/Filters/XContentTypeOptionsAttribute.cs
Normal file
25
BTCPayServer/Filters/XContentTypeOptionsAttribute.cs
Normal file
@ -0,0 +1,25 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
|
||||
namespace BTCPayServer.Filters
|
||||
{
|
||||
public class XContentTypeOptionsAttribute : Attribute, IActionFilter
|
||||
{
|
||||
public XContentTypeOptionsAttribute(string value)
|
||||
{
|
||||
Value = value;
|
||||
}
|
||||
public void OnActionExecuted(ActionExecutedContext context)
|
||||
{
|
||||
}
|
||||
|
||||
public string Value { get; set; }
|
||||
public void OnActionExecuting(ActionExecutingContext context)
|
||||
{
|
||||
context.HttpContext.Response.SetHeaderOnStarting("X-Content-Type-Options", Value);
|
||||
}
|
||||
}
|
||||
}
|
@ -23,11 +23,7 @@ namespace BTCPayServer.Filters
|
||||
|
||||
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;
|
||||
context.HttpContext.Response.SetHeaderOnStarting("X-Frame-Options", Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
23
BTCPayServer/Filters/XXSSProtectionAttribute.cs
Normal file
23
BTCPayServer/Filters/XXSSProtectionAttribute.cs
Normal file
@ -0,0 +1,23 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Microsoft.AspNetCore.Mvc.ViewFeatures;
|
||||
using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal;
|
||||
|
||||
namespace BTCPayServer.Filters
|
||||
{
|
||||
public class XXSSProtectionAttribute : Attribute, IActionFilter
|
||||
{
|
||||
public void OnActionExecuted(ActionExecutedContext context)
|
||||
{
|
||||
}
|
||||
|
||||
public void OnActionExecuting(ActionExecutingContext context)
|
||||
{
|
||||
context.HttpContext.Response.SetHeaderOnStarting("X-XSS-Protection", "1; mode=block");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
66
BTCPayServer/HostedServices/BaseAsyncService.cs
Normal file
66
BTCPayServer/HostedServices/BaseAsyncService.cs
Normal file
@ -0,0 +1,66 @@
|
||||
using System;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using BTCPayServer.Logging;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
namespace BTCPayServer.HostedServices
|
||||
{
|
||||
public abstract class BaseAsyncService : IHostedService
|
||||
{
|
||||
private CancellationTokenSource _Cts;
|
||||
protected Task[] _Tasks;
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_Cts = new CancellationTokenSource();
|
||||
_Tasks = InitializeTasks();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
internal abstract Task[] InitializeTasks();
|
||||
|
||||
protected CancellationToken Cancellation
|
||||
{
|
||||
get { return _Cts.Token; }
|
||||
}
|
||||
|
||||
protected async Task CreateLoopTask(Func<Task> act, [CallerMemberName]string caller = null)
|
||||
{
|
||||
await new SynchronizationContextRemover();
|
||||
while (!_Cts.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
await act();
|
||||
}
|
||||
catch (OperationCanceledException) when (_Cts.IsCancellationRequested)
|
||||
{
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logs.PayServer.LogWarning(ex, caller + " failed");
|
||||
try
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromMinutes(1), _Cts.Token);
|
||||
}
|
||||
catch (OperationCanceledException) when (_Cts.IsCancellationRequested) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_Cts.Cancel();
|
||||
return Task.WhenAll(_Tasks);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,76 @@
|
||||
using System;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Services;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using System.Threading;
|
||||
using BTCPayServer.Configuration;
|
||||
using BTCPayServer.Logging;
|
||||
using NBitcoin.DataEncoders;
|
||||
|
||||
namespace BTCPayServer.HostedServices
|
||||
{
|
||||
public class CheckConfigurationHostedService : IHostedService
|
||||
{
|
||||
private readonly BTCPayServerOptions _options;
|
||||
|
||||
public CheckConfigurationHostedService(BTCPayServerOptions options)
|
||||
{
|
||||
_options = options;
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
new Thread(() =>
|
||||
{
|
||||
if (_options.SSHSettings != null)
|
||||
{
|
||||
Logs.Configuration.LogInformation($"SSH settings detected, testing connection to {_options.SSHSettings.Username}@{_options.SSHSettings.Server} on port {_options.SSHSettings.Port} ...");
|
||||
var connection = new Renci.SshNet.SshClient(_options.SSHSettings.CreateConnectionInfo());
|
||||
connection.HostKeyReceived += (object sender, Renci.SshNet.Common.HostKeyEventArgs e) =>
|
||||
{
|
||||
e.CanTrust = true;
|
||||
if (!_options.IsTrustedFingerprint(e.FingerPrint))
|
||||
{
|
||||
Logs.Configuration.LogWarning($"SSH host fingerprint for {e.HostKey} is untrusted, start BTCPay with -sshtrustedfingerprints \"{Encoders.Hex.EncodeData(e.FingerPrint)}\"");
|
||||
}
|
||||
};
|
||||
try
|
||||
{
|
||||
connection.Connect();
|
||||
connection.Disconnect();
|
||||
Logs.Configuration.LogInformation($"SSH connection succeeded");
|
||||
}
|
||||
catch (Renci.SshNet.Common.SshAuthenticationException)
|
||||
{
|
||||
Logs.Configuration.LogWarning($"SSH invalid credentials");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var message = ex.Message;
|
||||
if (ex is AggregateException aggrEx && aggrEx.InnerException?.Message != null)
|
||||
{
|
||||
message = aggrEx.InnerException.Message;
|
||||
}
|
||||
Logs.Configuration.LogWarning($"SSH connection issue: {message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
connection.Dispose();
|
||||
}
|
||||
}
|
||||
})
|
||||
{ IsBackground = true }.Start();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
@ -11,52 +11,109 @@ using NBXplorer.Models;
|
||||
using System.Collections.Concurrent;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.Services;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using BTCPayServer.Security;
|
||||
|
||||
namespace BTCPayServer.HostedServices
|
||||
{
|
||||
public class CssThemeManager
|
||||
{
|
||||
public CssThemeManager(SettingsRepository settingsRepository)
|
||||
{
|
||||
Update(settingsRepository);
|
||||
}
|
||||
|
||||
private async void Update(SettingsRepository settingsRepository)
|
||||
{
|
||||
var data = (await settingsRepository.GetSettingAsync<ThemeSettings>()) ?? new ThemeSettings();
|
||||
Update(data);
|
||||
}
|
||||
|
||||
public void Update(ThemeSettings data)
|
||||
{
|
||||
UpdateBootstrap(data.BootstrapCssUri);
|
||||
UpdateCreativeStart(data.CreativeStartCssUri);
|
||||
}
|
||||
if (String.IsNullOrWhiteSpace(data.BootstrapCssUri))
|
||||
_bootstrapUri = "/vendor/bootstrap4/css/bootstrap.css?v=" + DateTime.Now.Ticks;
|
||||
else
|
||||
_bootstrapUri = data.BootstrapCssUri;
|
||||
|
||||
private string _bootstrapUri = "/vendor/bootstrap4/css/bootstrap.css?v=" + DateTime.Now.Ticks;
|
||||
|
||||
if (String.IsNullOrWhiteSpace(data.CreativeStartCssUri))
|
||||
_creativeStartUri = "/vendor/bootstrap4-creativestart/creative.css?v=" + DateTime.Now.Ticks;
|
||||
else
|
||||
_creativeStartUri = data.CreativeStartCssUri;
|
||||
}
|
||||
|
||||
private string _bootstrapUri;
|
||||
public string BootstrapUri
|
||||
{
|
||||
get { return _bootstrapUri; }
|
||||
}
|
||||
public void UpdateBootstrap(string newUri)
|
||||
{
|
||||
if (String.IsNullOrWhiteSpace(newUri))
|
||||
_bootstrapUri = "/vendor/bootstrap4/css/bootstrap.css?v="+ DateTime.Now.Ticks;
|
||||
else
|
||||
_bootstrapUri = newUri;
|
||||
}
|
||||
|
||||
private string _creativeStartUri = "/vendor/bootstrap4-creativestart/creative.css?v=" + DateTime.Now.Ticks;
|
||||
private string _creativeStartUri;
|
||||
public string CreativeStartUri
|
||||
{
|
||||
get { return _creativeStartUri; }
|
||||
}
|
||||
public void UpdateCreativeStart(string newUri)
|
||||
|
||||
public bool ShowRegister { get; set; }
|
||||
|
||||
internal void Update(PoliciesSettings data)
|
||||
{
|
||||
if (String.IsNullOrWhiteSpace(newUri))
|
||||
_creativeStartUri = "/vendor/bootstrap4-creativestart/creative.css?v=" + DateTime.Now.Ticks;
|
||||
else
|
||||
_creativeStartUri = newUri;
|
||||
ShowRegister = !data.LockSubscription;
|
||||
}
|
||||
}
|
||||
|
||||
public class ContentSecurityPolicyCssThemeManager : Attribute, IActionFilter, IOrderedFilter
|
||||
{
|
||||
public int Order => 1001;
|
||||
|
||||
public void OnActionExecuted(ActionExecutedContext context)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public void OnActionExecuting(ActionExecutingContext context)
|
||||
{
|
||||
var manager = context.HttpContext.RequestServices.GetService(typeof(CssThemeManager)) as CssThemeManager;
|
||||
var policies = context.HttpContext.RequestServices.GetService(typeof(ContentSecurityPolicies)) as ContentSecurityPolicies;
|
||||
if (manager != null && policies != null)
|
||||
{
|
||||
if(manager.CreativeStartUri != null && Uri.TryCreate(manager.CreativeStartUri, UriKind.Absolute, out var uri))
|
||||
{
|
||||
policies.Clear();
|
||||
}
|
||||
if (manager.BootstrapUri != null && Uri.TryCreate(manager.BootstrapUri, UriKind.Absolute, out uri))
|
||||
{
|
||||
policies.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class CssThemeManagerHostedService : BaseAsyncService
|
||||
{
|
||||
private SettingsRepository _SettingsRepository;
|
||||
private CssThemeManager _CssThemeManager;
|
||||
|
||||
public CssThemeManagerHostedService(SettingsRepository settingsRepository, CssThemeManager cssThemeManager)
|
||||
{
|
||||
_SettingsRepository = settingsRepository;
|
||||
_CssThemeManager = cssThemeManager;
|
||||
}
|
||||
|
||||
internal override Task[] InitializeTasks()
|
||||
{
|
||||
return new[]
|
||||
{
|
||||
CreateLoopTask(ListenForThemeChanges),
|
||||
CreateLoopTask(ListenForPoliciesChanges),
|
||||
};
|
||||
}
|
||||
|
||||
async Task ListenForPoliciesChanges()
|
||||
{
|
||||
await new SynchronizationContextRemover();
|
||||
var data = (await _SettingsRepository.GetSettingAsync<PoliciesSettings>()) ?? new PoliciesSettings();
|
||||
_CssThemeManager.Update(data);
|
||||
await _SettingsRepository.WaitSettingsChanged<PoliciesSettings>(Cancellation);
|
||||
}
|
||||
|
||||
async Task ListenForThemeChanges()
|
||||
{
|
||||
await new SynchronizationContextRemover();
|
||||
var data = (await _SettingsRepository.GetSettingAsync<ThemeSettings>()) ?? new ThemeSettings();
|
||||
_CssThemeManager.Update(data);
|
||||
|
||||
await _SettingsRepository.WaitSettingsChanged<ThemeSettings>(Cancellation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -198,7 +198,12 @@ namespace BTCPayServer.HostedServices
|
||||
PosData = dto.PosData,
|
||||
Price = dto.Price,
|
||||
Status = dto.Status,
|
||||
BuyerFields = invoice.RefundMail == null ? null : new Newtonsoft.Json.Linq.JObject() { new JProperty("buyerEmail", invoice.RefundMail) }
|
||||
BuyerFields = invoice.RefundMail == null ? null : new Newtonsoft.Json.Linq.JObject() { new JProperty("buyerEmail", invoice.RefundMail) },
|
||||
PaymentSubtotals = dto.PaymentSubtotals,
|
||||
PaymentTotals = dto.PaymentTotals,
|
||||
AmountPaid = dto.AmountPaid,
|
||||
ExchangeRates = dto.ExchangeRates,
|
||||
|
||||
};
|
||||
|
||||
// We keep backward compatibility with bitpay by passing BTC info to the notification
|
||||
@ -207,7 +212,7 @@ namespace BTCPayServer.HostedServices
|
||||
if (btcCryptoInfo != null)
|
||||
{
|
||||
#pragma warning disable CS0618
|
||||
notification.Rate = (double)dto.Rate;
|
||||
notification.Rate = dto.Rate;
|
||||
notification.Url = dto.Url;
|
||||
notification.BTCDue = dto.BTCDue;
|
||||
notification.BTCPaid = dto.BTCPaid;
|
||||
@ -304,8 +309,13 @@ namespace BTCPayServer.HostedServices
|
||||
{
|
||||
leases.Add(_EventAggregator.Subscribe<InvoiceEvent>(async e =>
|
||||
{
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, e.InvoiceId);
|
||||
await SaveEvent(invoice.Id, e);
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, e.Invoice.Id);
|
||||
if (invoice == null)
|
||||
return;
|
||||
List<Task> tasks = new List<Task>();
|
||||
|
||||
// Awaiting this later help make sure invoices should arrive in order
|
||||
tasks.Add(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)
|
||||
@ -315,20 +325,22 @@ namespace BTCPayServer.HostedServices
|
||||
e.Name == "invoice_failedToConfirm" ||
|
||||
e.Name == "invoice_markedInvalid" ||
|
||||
e.Name == "invoice_failedToConfirm" ||
|
||||
e.Name == "invoice_completed"
|
||||
e.Name == "invoice_completed" ||
|
||||
e.Name == "invoice_expiredPaidPartial"
|
||||
)
|
||||
await Notify(invoice);
|
||||
tasks.Add(Notify(invoice));
|
||||
}
|
||||
|
||||
if (e.Name == "invoice_confirmed")
|
||||
{
|
||||
await Notify(invoice);
|
||||
tasks.Add(Notify(invoice));
|
||||
}
|
||||
|
||||
if (invoice.ExtendedNotifications)
|
||||
{
|
||||
await Notify(invoice, e.EventCode, e.Name);
|
||||
tasks.Add(Notify(invoice, e.EventCode, e.Name));
|
||||
}
|
||||
await Task.WhenAll(tasks.ToArray());
|
||||
}));
|
||||
|
||||
|
||||
|
@ -66,8 +66,10 @@ namespace BTCPayServer.HostedServices
|
||||
context.MarkDirty();
|
||||
await _InvoiceRepository.UnaffectAddress(invoice.Id);
|
||||
|
||||
context.Events.Add(new InvoiceEvent(invoice, 1004, "invoice_expired"));
|
||||
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1004, "invoice_expired"));
|
||||
invoice.Status = "expired";
|
||||
if(invoice.ExceptionStatus == "paidPartial")
|
||||
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 2000, "invoice_expiredPaidPartial"));
|
||||
}
|
||||
|
||||
var payments = invoice.GetPayments().Where(p => p.Accounted).ToArray();
|
||||
@ -78,11 +80,11 @@ namespace BTCPayServer.HostedServices
|
||||
var network = _NetworkProvider.GetNetwork(paymentMethod.GetId().CryptoCode);
|
||||
if (invoice.Status == "new" || invoice.Status == "expired")
|
||||
{
|
||||
if (accounting.Paid >= accounting.TotalDue)
|
||||
if (accounting.Paid >= accounting.MinimumTotalDue)
|
||||
{
|
||||
if (invoice.Status == "new")
|
||||
{
|
||||
context.Events.Add(new InvoiceEvent(invoice, 1003, "invoice_paidInFull"));
|
||||
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1003, "invoice_paidInFull"));
|
||||
invoice.Status = "paid";
|
||||
invoice.ExceptionStatus = accounting.Paid > accounting.TotalDue ? "paidOver" : null;
|
||||
await _InvoiceRepository.UnaffectAddress(invoice.Id);
|
||||
@ -91,22 +93,22 @@ namespace BTCPayServer.HostedServices
|
||||
else if (invoice.Status == "expired" && invoice.ExceptionStatus != "paidLate")
|
||||
{
|
||||
invoice.ExceptionStatus = "paidLate";
|
||||
context.Events.Add(new InvoiceEvent(invoice, 1009, "invoice_paidAfterExpiration"));
|
||||
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1009, "invoice_paidAfterExpiration"));
|
||||
context.MarkDirty();
|
||||
}
|
||||
}
|
||||
|
||||
if (accounting.Paid < accounting.TotalDue && invoice.GetPayments().Count != 0 && invoice.ExceptionStatus != "paidPartial")
|
||||
if (accounting.Paid < accounting.MinimumTotalDue && invoice.GetPayments().Count != 0 && invoice.ExceptionStatus != "paidPartial")
|
||||
{
|
||||
invoice.ExceptionStatus = "paidPartial";
|
||||
context.MarkDirty();
|
||||
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")
|
||||
if (accounting.MinimumTotalDue <= accounting.Paid && accounting.Paid <= accounting.TotalDue && invoice.ExceptionStatus == "paidOver")
|
||||
{
|
||||
invoice.ExceptionStatus = null;
|
||||
context.MarkDirty();
|
||||
@ -118,7 +120,7 @@ namespace BTCPayServer.HostedServices
|
||||
context.MarkDirty();
|
||||
}
|
||||
|
||||
if (accounting.Paid < accounting.TotalDue)
|
||||
if (accounting.Paid < accounting.MinimumTotalDue)
|
||||
{
|
||||
invoice.Status = "new";
|
||||
invoice.ExceptionStatus = accounting.Paid == Money.Zero ? null : "paidPartial";
|
||||
@ -134,17 +136,17 @@ namespace BTCPayServer.HostedServices
|
||||
(invoice.MonitoringExpiration < DateTimeOffset.UtcNow)
|
||||
&&
|
||||
// And not enough amount confirmed
|
||||
(confirmedAccounting.Paid < accounting.TotalDue))
|
||||
(confirmedAccounting.Paid < accounting.MinimumTotalDue))
|
||||
{
|
||||
await _InvoiceRepository.UnaffectAddress(invoice.Id);
|
||||
context.Events.Add(new InvoiceEvent(invoice, 1013, "invoice_failedToConfirm"));
|
||||
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1013, "invoice_failedToConfirm"));
|
||||
invoice.Status = "invalid";
|
||||
context.MarkDirty();
|
||||
}
|
||||
else if (confirmedAccounting.Paid >= accounting.TotalDue)
|
||||
else if (confirmedAccounting.Paid >= accounting.MinimumTotalDue)
|
||||
{
|
||||
await _InvoiceRepository.UnaffectAddress(invoice.Id);
|
||||
context.Events.Add(new InvoiceEvent(invoice, 1005, "invoice_confirmed"));
|
||||
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1005, "invoice_confirmed"));
|
||||
invoice.Status = "confirmed";
|
||||
context.MarkDirty();
|
||||
}
|
||||
@ -153,9 +155,9 @@ namespace BTCPayServer.HostedServices
|
||||
if (invoice.Status == "confirmed")
|
||||
{
|
||||
var completedAccounting = paymentMethod.Calculate(p => p.GetCryptoPaymentData().PaymentCompleted(p, network));
|
||||
if (completedAccounting.Paid >= accounting.TotalDue)
|
||||
if (completedAccounting.Paid >= accounting.MinimumTotalDue)
|
||||
{
|
||||
context.Events.Add(new InvoiceEvent(invoice, 1006, "invoice_completed"));
|
||||
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1006, "invoice_completed"));
|
||||
invoice.Status = "complete";
|
||||
context.MarkDirty();
|
||||
}
|
||||
@ -247,13 +249,13 @@ namespace BTCPayServer.HostedServices
|
||||
{
|
||||
if (b.Name == "invoice_created")
|
||||
{
|
||||
Watch(b.InvoiceId);
|
||||
await Wait(b.InvoiceId);
|
||||
Watch(b.Invoice.Id);
|
||||
await Wait(b.Invoice.Id);
|
||||
}
|
||||
|
||||
if (b.Name == "invoice_receivedPayment")
|
||||
{
|
||||
Watch(b.InvoiceId);
|
||||
Watch(b.Invoice.Id);
|
||||
}
|
||||
}));
|
||||
return Task.CompletedTask;
|
||||
@ -289,7 +291,7 @@ namespace BTCPayServer.HostedServices
|
||||
if (updateContext.Dirty)
|
||||
{
|
||||
await _InvoiceRepository.UpdateInvoiceStatus(invoice.Id, invoice.Status, invoice.ExceptionStatus);
|
||||
updateContext.Events.Add(new InvoiceDataChangedEvent(invoice));
|
||||
updateContext.Events.Insert(0, new InvoiceDataChangedEvent(invoice));
|
||||
}
|
||||
|
||||
foreach (var evt in updateContext.Events)
|
||||
|
120
BTCPayServer/HostedServices/MigratorHostedService.cs
Normal file
120
BTCPayServer/HostedServices/MigratorHostedService.cs
Normal file
@ -0,0 +1,120 @@
|
||||
using System;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Logging;
|
||||
|
||||
namespace BTCPayServer.HostedServices
|
||||
{
|
||||
public class MigratorHostedService : BaseAsyncService
|
||||
{
|
||||
private ApplicationDbContextFactory _DBContextFactory;
|
||||
private StoreRepository _StoreRepository;
|
||||
private BTCPayNetworkProvider _NetworkProvider;
|
||||
private SettingsRepository _Settings;
|
||||
public MigratorHostedService(
|
||||
BTCPayNetworkProvider networkProvider,
|
||||
StoreRepository storeRepository,
|
||||
ApplicationDbContextFactory dbContextFactory,
|
||||
SettingsRepository settingsRepository)
|
||||
{
|
||||
_DBContextFactory = dbContextFactory;
|
||||
_StoreRepository = storeRepository;
|
||||
_NetworkProvider = networkProvider;
|
||||
_Settings = settingsRepository;
|
||||
}
|
||||
internal override Task[] InitializeTasks()
|
||||
{
|
||||
return new[]
|
||||
{
|
||||
Update()
|
||||
};
|
||||
}
|
||||
|
||||
private async Task Update()
|
||||
{
|
||||
try
|
||||
{
|
||||
var settings = (await _Settings.GetSettingAsync<MigrationSettings>()) ?? new MigrationSettings();
|
||||
if (!settings.DeprecatedLightningConnectionStringCheck)
|
||||
{
|
||||
await DeprecatedLightningConnectionStringCheck();
|
||||
settings.DeprecatedLightningConnectionStringCheck = true;
|
||||
await _Settings.UpdateSetting(settings);
|
||||
}
|
||||
if (!settings.UnreachableStoreCheck)
|
||||
{
|
||||
await UnreachableStoreCheck();
|
||||
settings.UnreachableStoreCheck = true;
|
||||
await _Settings.UpdateSetting(settings);
|
||||
}
|
||||
if (!settings.ConvertMultiplierToSpread)
|
||||
{
|
||||
await ConvertMultiplierToSpread();
|
||||
settings.ConvertMultiplierToSpread = true;
|
||||
await _Settings.UpdateSetting(settings);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logs.PayServer.LogError(ex, "Error on the MigratorHostedService");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ConvertMultiplierToSpread()
|
||||
{
|
||||
using (var ctx = _DBContextFactory.CreateContext())
|
||||
{
|
||||
foreach (var store in await ctx.Stores.ToArrayAsync())
|
||||
{
|
||||
var blob = store.GetStoreBlob();
|
||||
#pragma warning disable CS0612 // Type or member is obsolete
|
||||
decimal multiplier = 1.0m;
|
||||
if (blob.RateRules != null && blob.RateRules.Count != 0)
|
||||
{
|
||||
foreach (var rule in blob.RateRules)
|
||||
{
|
||||
multiplier = rule.Apply(null, multiplier);
|
||||
}
|
||||
}
|
||||
blob.RateRules = null;
|
||||
blob.Spread = Math.Min(1.0m, Math.Max(0m, -(multiplier - 1.0m)));
|
||||
store.SetStoreBlob(blob);
|
||||
#pragma warning restore CS0612 // Type or member is obsolete
|
||||
}
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private Task UnreachableStoreCheck()
|
||||
{
|
||||
return _StoreRepository.CleanUnreachableStores();
|
||||
}
|
||||
|
||||
private async Task DeprecatedLightningConnectionStringCheck()
|
||||
{
|
||||
using (var ctx = _DBContextFactory.CreateContext())
|
||||
{
|
||||
foreach (var store in await ctx.Stores.ToArrayAsync())
|
||||
{
|
||||
foreach (var method in store.GetSupportedPaymentMethods(_NetworkProvider).OfType<Payments.Lightning.LightningSupportedPaymentMethod>())
|
||||
{
|
||||
var lightning = method.GetLightningUrl();
|
||||
if (lightning.IsLegacy)
|
||||
{
|
||||
method.SetLightningUrl(lightning);
|
||||
store.SetSupportedPaymentMethod(method.PaymentId, method);
|
||||
}
|
||||
}
|
||||
}
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -192,7 +192,7 @@ namespace BTCPayServer.HostedServices
|
||||
{
|
||||
State = NBXplorerState.NotConnected;
|
||||
status = null;
|
||||
_Aggregator.Publish(new NBXplorerErrorEvent(_Network, error));
|
||||
Logs.PayServer.LogError($"{_Network.CryptoCode}: NBXplorer error `{error}`");
|
||||
}
|
||||
|
||||
_Dashboard.Publish(_Network, State, status, error);
|
||||
|
@ -14,93 +14,58 @@ using System.Text;
|
||||
|
||||
namespace BTCPayServer.HostedServices
|
||||
{
|
||||
public class RatesHostedService : IHostedService
|
||||
public class RatesHostedService : BaseAsyncService
|
||||
{
|
||||
private SettingsRepository _SettingsRepository;
|
||||
private IRateProviderFactory _RateProviderFactory;
|
||||
private CoinAverageSettings _coinAverageSettings;
|
||||
BTCPayRateProviderFactory _RateProviderFactory;
|
||||
public RatesHostedService(SettingsRepository repo,
|
||||
CoinAverageSettings coinAverageSettings,
|
||||
IRateProviderFactory rateProviderFactory)
|
||||
BTCPayRateProviderFactory rateProviderFactory,
|
||||
CoinAverageSettings coinAverageSettings)
|
||||
{
|
||||
this._SettingsRepository = repo;
|
||||
_RateProviderFactory = rateProviderFactory;
|
||||
_coinAverageSettings = coinAverageSettings;
|
||||
_RateProviderFactory = rateProviderFactory;
|
||||
}
|
||||
|
||||
|
||||
CancellationTokenSource _Cts = new CancellationTokenSource();
|
||||
|
||||
List<Task> _Tasks = new List<Task>();
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
internal override Task[] InitializeTasks()
|
||||
{
|
||||
_Tasks.Add(RefreshCoinAverageSupportedExchanges(_Cts.Token));
|
||||
_Tasks.Add(RefreshCoinAverageSettings(_Cts.Token));
|
||||
return Task.CompletedTask;
|
||||
return new[]
|
||||
{
|
||||
CreateLoopTask(RefreshCoinAverageSupportedExchanges),
|
||||
CreateLoopTask(RefreshCoinAverageSettings)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
async Task Timer(Func<Task> act, CancellationToken cancellation, [CallerMemberName]string caller = null)
|
||||
async Task RefreshCoinAverageSupportedExchanges()
|
||||
{
|
||||
await new SynchronizationContextRemover();
|
||||
while (!cancellation.IsCancellationRequested)
|
||||
var tickers = await new CoinAverageRateProvider() { Authenticator = _coinAverageSettings }.GetExchangeTickersAsync();
|
||||
var exchanges = new CoinAverageExchanges();
|
||||
foreach(var item in tickers
|
||||
.Exchanges
|
||||
.Select(c => new CoinAverageExchange(c.Name, c.DisplayName)))
|
||||
{
|
||||
try
|
||||
{
|
||||
await act();
|
||||
}
|
||||
catch (OperationCanceledException) when (cancellation.IsCancellationRequested)
|
||||
{
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logs.PayServer.LogWarning(ex, caller + " failed");
|
||||
try
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromMinutes(1), cancellation);
|
||||
}
|
||||
catch (OperationCanceledException) when (cancellation.IsCancellationRequested) { }
|
||||
}
|
||||
exchanges.Add(item);
|
||||
}
|
||||
}
|
||||
Task RefreshCoinAverageSupportedExchanges(CancellationToken cancellation)
|
||||
{
|
||||
return Timer(async () =>
|
||||
{
|
||||
await new SynchronizationContextRemover();
|
||||
var tickers = await new CoinAverageRateProvider("BTC") { Authenticator = _coinAverageSettings }.GetExchangeTickersAsync();
|
||||
_coinAverageSettings.AvailableExchanges = tickers
|
||||
.Exchanges
|
||||
.Select(c => (c.DisplayName, c.Name))
|
||||
.ToArray();
|
||||
await Task.Delay(TimeSpan.FromHours(5), cancellation);
|
||||
}, cancellation);
|
||||
_coinAverageSettings.AvailableExchanges = exchanges;
|
||||
await Task.Delay(TimeSpan.FromHours(5), Cancellation);
|
||||
}
|
||||
|
||||
Task RefreshCoinAverageSettings(CancellationToken cancellation)
|
||||
async Task RefreshCoinAverageSettings()
|
||||
{
|
||||
return Timer(async () =>
|
||||
await new SynchronizationContextRemover();
|
||||
var rates = (await _SettingsRepository.GetSettingAsync<RatesSetting>()) ?? new RatesSetting();
|
||||
_RateProviderFactory.CacheSpan = TimeSpan.FromMinutes(rates.CacheInMinutes);
|
||||
if (!string.IsNullOrWhiteSpace(rates.PrivateKey) && !string.IsNullOrWhiteSpace(rates.PublicKey))
|
||||
{
|
||||
await new SynchronizationContextRemover();
|
||||
var rates = (await _SettingsRepository.GetSettingAsync<RatesSetting>()) ?? new RatesSetting();
|
||||
_RateProviderFactory.CacheSpan = TimeSpan.FromMinutes(rates.CacheInMinutes);
|
||||
if (!string.IsNullOrWhiteSpace(rates.PrivateKey) && !string.IsNullOrWhiteSpace(rates.PublicKey))
|
||||
{
|
||||
_coinAverageSettings.KeyPair = (rates.PublicKey, rates.PrivateKey);
|
||||
}
|
||||
else
|
||||
{
|
||||
_coinAverageSettings.KeyPair = null;
|
||||
}
|
||||
await _SettingsRepository.WaitSettingsChanged<RatesSetting>(cancellation);
|
||||
}, cancellation);
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_Cts.Cancel();
|
||||
return Task.WhenAll(_Tasks.ToArray());
|
||||
_coinAverageSettings.KeyPair = (rates.PublicKey, rates.PrivateKey);
|
||||
}
|
||||
else
|
||||
{
|
||||
_coinAverageSettings.KeyPair = null;
|
||||
}
|
||||
await _SettingsRepository.WaitSettingsChanged<RatesSetting>(Cancellation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -12,7 +12,6 @@ using NBitcoin;
|
||||
using BTCPayServer.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.IO;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using NBXplorer;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
@ -38,55 +37,15 @@ using Microsoft.Extensions.Caching.Memory;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.HostedServices;
|
||||
using Meziantou.AspNetCore.BundleTagHelpers;
|
||||
using System.Security.Claims;
|
||||
using BTCPayServer.Security;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
|
||||
namespace BTCPayServer.Hosting
|
||||
{
|
||||
public static class BTCPayServerServices
|
||||
{
|
||||
public class OwnStoreAuthorizationRequirement : IAuthorizationRequirement
|
||||
{
|
||||
public OwnStoreAuthorizationRequirement()
|
||||
{
|
||||
}
|
||||
|
||||
public OwnStoreAuthorizationRequirement(string role)
|
||||
{
|
||||
Role = role;
|
||||
}
|
||||
|
||||
public string Role
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
}
|
||||
|
||||
public class OwnStoreHandler : AuthorizationHandler<OwnStoreAuthorizationRequirement>
|
||||
{
|
||||
StoreRepository _StoreRepository;
|
||||
UserManager<ApplicationUser> _UserManager;
|
||||
public OwnStoreHandler(StoreRepository storeRepository, UserManager<ApplicationUser> userManager)
|
||||
{
|
||||
_StoreRepository = storeRepository;
|
||||
_UserManager = userManager;
|
||||
}
|
||||
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, OwnStoreAuthorizationRequirement requirement)
|
||||
{
|
||||
object storeId = null;
|
||||
if (!((Microsoft.AspNetCore.Mvc.ActionContext)context.Resource).RouteData.Values.TryGetValue("storeId", out storeId))
|
||||
context.Succeed(requirement);
|
||||
else if (storeId != null)
|
||||
{
|
||||
var user = _UserManager.GetUserId(((Microsoft.AspNetCore.Mvc.ActionContext)context.Resource).HttpContext.User);
|
||||
if (user != null)
|
||||
{
|
||||
var store = await _StoreRepository.FindStore((string)storeId, user);
|
||||
if (store != null)
|
||||
if (requirement.Role == null || requirement.Role == store.Role)
|
||||
context.Succeed(requirement);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
public static IServiceCollection AddBTCPayServer(this IServiceCollection services)
|
||||
{
|
||||
services.AddDbContext<ApplicationDbContext>((provider, o) =>
|
||||
@ -110,7 +69,6 @@ namespace BTCPayServer.Hosting
|
||||
services.TryAddSingleton<TokenRepository>();
|
||||
services.TryAddSingleton<EventAggregator>();
|
||||
services.TryAddSingleton<CoinAverageSettings>();
|
||||
services.TryAddSingleton<ICoinAverageAuthenticator, CoinAverageSettingsAuthenticator>();
|
||||
services.TryAddSingleton<ApplicationDbContextFactory>(o =>
|
||||
{
|
||||
var opts = o.GetRequiredService<BTCPayServerOptions>();
|
||||
@ -136,9 +94,9 @@ namespace BTCPayServer.Hosting
|
||||
return opts.NetworkProvider;
|
||||
});
|
||||
|
||||
services.TryAddSingleton<LightningConfigurationProvider>();
|
||||
services.TryAddSingleton<LanguageService>();
|
||||
services.TryAddSingleton<NBXplorerDashboard>();
|
||||
services.TryAddSingleton<CssThemeManager>();
|
||||
services.TryAddSingleton<StoreRepository>();
|
||||
services.TryAddSingleton<BTCPayWalletProvider>();
|
||||
services.TryAddSingleton<CurrencyNameTable>();
|
||||
@ -148,9 +106,20 @@ namespace BTCPayServer.Hosting
|
||||
BlockTarget = 20
|
||||
});
|
||||
|
||||
services.AddSingleton<CssThemeManager>();
|
||||
services.Configure<MvcOptions>((o) => {
|
||||
o.Filters.Add(new ContentSecurityPolicyCssThemeManager());
|
||||
o.ModelMetadataDetailsProviders.Add(new SuppressChildValidationMetadataProvider(typeof(WalletId)));
|
||||
o.ModelMetadataDetailsProviders.Add(new SuppressChildValidationMetadataProvider(typeof(DerivationStrategyBase)));
|
||||
});
|
||||
services.AddSingleton<IHostedService, CssThemeManagerHostedService>();
|
||||
services.AddSingleton<IHostedService, MigratorHostedService>();
|
||||
|
||||
services.AddSingleton<Payments.IPaymentMethodHandler<DerivationStrategy>, Payments.Bitcoin.BitcoinLikePaymentHandler>();
|
||||
services.AddSingleton<IHostedService, Payments.Bitcoin.NBXplorerListener>();
|
||||
|
||||
services.AddSingleton<IHostedService, HostedServices.CheckConfigurationHostedService>();
|
||||
|
||||
services.AddSingleton<Payments.IPaymentMethodHandler<Payments.Lightning.LightningSupportedPaymentMethod>, Payments.Lightning.LightningLikePaymentHandler>();
|
||||
services.AddSingleton<IHostedService, Payments.Lightning.LightningListener>();
|
||||
|
||||
@ -158,6 +127,7 @@ namespace BTCPayServer.Hosting
|
||||
services.AddSingleton<IHostedService, InvoiceNotificationManager>();
|
||||
services.AddSingleton<IHostedService, InvoiceWatcher>();
|
||||
services.AddSingleton<IHostedService, RatesHostedService>();
|
||||
services.AddTransient<IConfigureOptions<MvcOptions>, BTCPayClaimsFilter>();
|
||||
|
||||
services.TryAddSingleton<ExplorerClientProvider>();
|
||||
services.TryAddSingleton<Bitpay>(o =>
|
||||
@ -167,30 +137,18 @@ namespace BTCPayServer.Hosting
|
||||
else
|
||||
return new Bitpay(new Key(), new Uri("https://test.bitpay.com/"));
|
||||
});
|
||||
services.TryAddSingleton<IRateProviderFactory, BTCPayRateProviderFactory>();
|
||||
services.TryAddSingleton<BTCPayRateProviderFactory>();
|
||||
|
||||
services.TryAddScoped<IHttpContextAccessor, HttpContextAccessor>();
|
||||
services.TryAddSingleton<IAuthorizationHandler, OwnStoreHandler>();
|
||||
services.AddTransient<AccessTokenController>();
|
||||
services.AddTransient<InvoiceController>();
|
||||
// Add application services.
|
||||
services.AddTransient<IEmailSender, EmailSender>();
|
||||
|
||||
services.AddAuthorization(o =>
|
||||
{
|
||||
o.AddPolicy(StorePolicies.CanAccessStores, builder =>
|
||||
{
|
||||
builder.AddRequirements(new OwnStoreAuthorizationRequirement());
|
||||
});
|
||||
|
||||
o.AddPolicy(StorePolicies.OwnStore, builder =>
|
||||
{
|
||||
builder.AddRequirements(new OwnStoreAuthorizationRequirement(StoreRoles.Owner));
|
||||
});
|
||||
});
|
||||
|
||||
// bundling
|
||||
|
||||
services.AddAuthorization(o => Policies.AddBTCPayPolicies(o));
|
||||
BitpayAuthentication.AddAuthentication(services);
|
||||
|
||||
services.AddBundles();
|
||||
services.AddTransient<BundleOptions>(provider =>
|
||||
{
|
||||
@ -201,6 +159,10 @@ namespace BTCPayServer.Hosting
|
||||
return bundle;
|
||||
});
|
||||
|
||||
services.AddCors(options=>
|
||||
{
|
||||
options.AddPolicy(CorsPolicies.All, p=>p.AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin());
|
||||
});
|
||||
return services;
|
||||
}
|
||||
|
||||
|
@ -6,37 +6,25 @@ using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using NBitcoin;
|
||||
using NBitcoin.Crypto;
|
||||
using NBitcoin.DataEncoders;
|
||||
using Microsoft.AspNetCore.Http.Internal;
|
||||
using System.IO;
|
||||
using BTCPayServer.Authentication;
|
||||
using System.Security.Principal;
|
||||
using NBitpayClient.Extensions;
|
||||
using BTCPayServer.Logging;
|
||||
using Newtonsoft.Json;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Configuration;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Routing;
|
||||
using Microsoft.AspNetCore.Http.Extensions;
|
||||
using BTCPayServer.Controllers;
|
||||
using System.Net.WebSockets;
|
||||
using BTCPayServer.Services.Stores;
|
||||
|
||||
namespace BTCPayServer.Hosting
|
||||
{
|
||||
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));
|
||||
}
|
||||
@ -45,42 +33,16 @@ namespace BTCPayServer.Hosting
|
||||
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();
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var bitpayAuth = GetBitpayAuth(httpContext, out bool isBitpayAuth);
|
||||
var isBitpayAPI = IsBitpayAPI(httpContext, isBitpayAuth);
|
||||
httpContext.SetIsBitpayAPI(isBitpayAPI);
|
||||
if (isBitpayAPI)
|
||||
{
|
||||
httpContext.SetBitpayAuth(bitpayAuth);
|
||||
}
|
||||
await _Next(httpContext);
|
||||
}
|
||||
catch (WebSocketException)
|
||||
@ -100,6 +62,56 @@ namespace BTCPayServer.Hosting
|
||||
}
|
||||
}
|
||||
|
||||
private static (string Signature, String Id, String Authorization) GetBitpayAuth(HttpContext httpContext, out bool hasBitpayAuth)
|
||||
{
|
||||
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();
|
||||
httpContext.Request.Headers.TryGetValue("Authorization", out values);
|
||||
var auth = values.FirstOrDefault();
|
||||
hasBitpayAuth = auth != null || (sig != null && id != null);
|
||||
return (sig, id, auth);
|
||||
}
|
||||
|
||||
private bool IsBitpayAPI(HttpContext httpContext, bool bitpayAuth)
|
||||
{
|
||||
if (!httpContext.Request.Path.HasValue)
|
||||
return false;
|
||||
|
||||
var isJson = (httpContext.Request.ContentType ?? string.Empty).StartsWith("application/json", StringComparison.OrdinalIgnoreCase);
|
||||
var path = httpContext.Request.Path.Value;
|
||||
if (
|
||||
bitpayAuth &&
|
||||
path == "/invoices" &&
|
||||
httpContext.Request.Method == "POST" &&
|
||||
isJson)
|
||||
return true;
|
||||
|
||||
if (
|
||||
bitpayAuth &&
|
||||
path == "/invoices" &&
|
||||
httpContext.Request.Method == "GET")
|
||||
return true;
|
||||
|
||||
if (
|
||||
path.StartsWith("/invoices/", StringComparison.OrdinalIgnoreCase) &&
|
||||
httpContext.Request.Method == "GET" &&
|
||||
(isJson || httpContext.Request.Query.ContainsKey("token")))
|
||||
return true;
|
||||
|
||||
if (path.StartsWith("/rates", StringComparison.OrdinalIgnoreCase) &&
|
||||
httpContext.Request.Method == "GET")
|
||||
return true;
|
||||
|
||||
if (
|
||||
path.Equals("/tokens", StringComparison.Ordinal) &&
|
||||
( httpContext.Request.Method == "GET" || httpContext.Request.Method == "POST"))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void RewriteHostIfNeeded(HttpContext httpContext)
|
||||
{
|
||||
string reverseProxyScheme = null;
|
||||
@ -132,7 +144,7 @@ namespace BTCPayServer.Hosting
|
||||
httpContext.Request.Scheme = reverseProxyScheme;
|
||||
}
|
||||
else
|
||||
{
|
||||
{
|
||||
httpContext.Request.Scheme = _Options.ExternalUrl.Scheme;
|
||||
}
|
||||
if (_Options.ExternalUrl.IsDefaultPort)
|
||||
|
@ -35,11 +35,11 @@ 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;
|
||||
using Meziantou.AspNetCore.BundleTagHelpers;
|
||||
using BTCPayServer.Security;
|
||||
|
||||
namespace BTCPayServer.Hosting
|
||||
{
|
||||
@ -80,8 +80,19 @@ namespace BTCPayServer.Hosting
|
||||
services.AddMvc(o =>
|
||||
{
|
||||
o.Filters.Add(new XFrameOptionsAttribute("DENY"));
|
||||
o.Filters.Add(new XContentTypeOptionsAttribute("nosniff"));
|
||||
o.Filters.Add(new XXSSProtectionAttribute());
|
||||
o.Filters.Add(new ReferrerPolicyAttribute("same-origin"));
|
||||
//o.Filters.Add(new ContentSecurityPolicyAttribute()
|
||||
//{
|
||||
// FontSrc = "'self' https://fonts.gstatic.com/",
|
||||
// ImgSrc = "'self' data:",
|
||||
// DefaultSrc = "'none'",
|
||||
// StyleSrc = "'self' 'unsafe-inline'",
|
||||
// ScriptSrc = "'self' 'unsafe-inline'"
|
||||
//});
|
||||
});
|
||||
|
||||
services.TryAddScoped<ContentSecurityPolicies>();
|
||||
services.Configure<IdentityOptions>(options =>
|
||||
{
|
||||
options.Password.RequireDigit = false;
|
||||
@ -104,10 +115,6 @@ namespace BTCPayServer.Hosting
|
||||
b.AllowAnyMethod().AllowAnyHeader().AllowAnyOrigin();
|
||||
});
|
||||
});
|
||||
services.Configure<IOptions<ApplicationInsightsServiceOptions>>(o =>
|
||||
{
|
||||
o.Value.DeveloperMode = _Env.IsDevelopment();
|
||||
});
|
||||
|
||||
// Needed to debug U2F for ledger support
|
||||
//services.Configure<KestrelServerOptions>(kestrel =>
|
||||
@ -146,12 +153,9 @@ namespace BTCPayServer.Hosting
|
||||
if (env.IsDevelopment())
|
||||
{
|
||||
app.UseDeveloperExceptionPage();
|
||||
app.UseBrowserLink();
|
||||
}
|
||||
|
||||
//App insight do not that by itself...
|
||||
loggerFactory.AddApplicationInsights(prov, LogLevel.Information);
|
||||
|
||||
app.UseCors();
|
||||
app.UsePayServer();
|
||||
app.UseStaticFiles();
|
||||
app.UseAuthentication();
|
||||
|
@ -1,13 +1,14 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Console;
|
||||
using Microsoft.Extensions.Logging.Console.Internal;
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions.Internal;
|
||||
using Microsoft.Extensions.Logging.Console;
|
||||
using Microsoft.Extensions.Logging.Console.Internal;
|
||||
|
||||
namespace BTCPayServer.Logging
|
||||
{
|
||||
@ -20,19 +21,18 @@ namespace BTCPayServer.Logging
|
||||
}
|
||||
public ILogger CreateLogger(string categoryName)
|
||||
{
|
||||
return new CustomConsoleLogger(categoryName, (a, b) => true, false, _Processor);
|
||||
return new CustomerConsoleLogger(categoryName, (a, b) => true, null, _Processor);
|
||||
}
|
||||
|
||||
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
|
||||
public class CustomerConsoleLogger : ILogger
|
||||
{
|
||||
private static readonly string _loglevelPadding = ": ";
|
||||
private static readonly string _messagePadding;
|
||||
@ -47,19 +47,33 @@ namespace BTCPayServer.Logging
|
||||
[ThreadStatic]
|
||||
private static StringBuilder _logBuilder;
|
||||
|
||||
static CustomConsoleLogger()
|
||||
static CustomerConsoleLogger()
|
||||
{
|
||||
var logLevelString = GetLogLevelString(LogLevel.Information);
|
||||
_messagePadding = new string(' ', logLevelString.Length + _loglevelPadding.Length);
|
||||
_newLineWithMessagePadding = Environment.NewLine + _messagePadding;
|
||||
}
|
||||
|
||||
public CustomConsoleLogger(string name, Func<string, LogLevel, bool> filter, bool includeScopes, ConsoleLoggerProcessor loggerProcessor)
|
||||
public CustomerConsoleLogger(string name, Func<string, LogLevel, bool> filter, bool includeScopes)
|
||||
: this(name, filter, includeScopes ? new LoggerExternalScopeProvider() : null, new ConsoleLoggerProcessor())
|
||||
{
|
||||
Name = name ?? throw new ArgumentNullException(nameof(name));
|
||||
Filter = filter ?? ((category, logLevel) => true);
|
||||
IncludeScopes = includeScopes;
|
||||
}
|
||||
|
||||
internal CustomerConsoleLogger(string name, Func<string, LogLevel, bool> filter, IExternalScopeProvider scopeProvider)
|
||||
: this(name, filter, scopeProvider, new ConsoleLoggerProcessor())
|
||||
{
|
||||
}
|
||||
|
||||
internal CustomerConsoleLogger(string name, Func<string, LogLevel, bool> filter, IExternalScopeProvider scopeProvider, ConsoleLoggerProcessor loggerProcessor)
|
||||
{
|
||||
if (name == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(name));
|
||||
}
|
||||
|
||||
Name = name;
|
||||
Filter = filter ?? ((category, logLevel) => true);
|
||||
ScopeProvider = scopeProvider;
|
||||
_queueProcessor = loggerProcessor;
|
||||
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
@ -80,7 +94,12 @@ namespace BTCPayServer.Logging
|
||||
}
|
||||
set
|
||||
{
|
||||
_queueProcessor.Console = value ?? throw new ArgumentNullException(nameof(value));
|
||||
if (value == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(value));
|
||||
}
|
||||
|
||||
_queueProcessor.Console = value;
|
||||
}
|
||||
}
|
||||
|
||||
@ -92,13 +111,13 @@ namespace BTCPayServer.Logging
|
||||
}
|
||||
set
|
||||
{
|
||||
_filter = value ?? throw new ArgumentNullException(nameof(value));
|
||||
}
|
||||
}
|
||||
if (value == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(value));
|
||||
}
|
||||
|
||||
public bool IncludeScopes
|
||||
{
|
||||
get; set;
|
||||
_filter = value;
|
||||
}
|
||||
}
|
||||
|
||||
public string Name
|
||||
@ -106,6 +125,16 @@ namespace BTCPayServer.Logging
|
||||
get;
|
||||
}
|
||||
|
||||
internal IExternalScopeProvider ScopeProvider
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public bool DisableColors
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
|
||||
{
|
||||
if (!IsEnabled(logLevel))
|
||||
@ -154,10 +183,7 @@ namespace BTCPayServer.Logging
|
||||
while (lenAfter++ < 18)
|
||||
logBuilder.Append(" ");
|
||||
// scope information
|
||||
if (IncludeScopes)
|
||||
{
|
||||
GetScopeInformation(logBuilder);
|
||||
}
|
||||
GetScopeInformation(logBuilder);
|
||||
|
||||
if (!string.IsNullOrEmpty(message))
|
||||
{
|
||||
@ -202,18 +228,15 @@ namespace BTCPayServer.Logging
|
||||
|
||||
public bool IsEnabled(LogLevel logLevel)
|
||||
{
|
||||
if (logLevel == LogLevel.None)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return Filter(Name, logLevel);
|
||||
}
|
||||
|
||||
public IDisposable BeginScope<TState>(TState state)
|
||||
{
|
||||
if (state == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(state));
|
||||
}
|
||||
|
||||
return ConsoleLogScope.Push(Name, state);
|
||||
}
|
||||
public IDisposable BeginScope<TState>(TState state) => ScopeProvider?.Push(state) ?? NullScope.Instance;
|
||||
|
||||
private static string GetLogLevelString(LogLevel logLevel)
|
||||
{
|
||||
@ -238,6 +261,11 @@ namespace BTCPayServer.Logging
|
||||
|
||||
private ConsoleColors GetLogLevelConsoleColors(LogLevel logLevel)
|
||||
{
|
||||
if (DisableColors)
|
||||
{
|
||||
return new ConsoleColors(null, null);
|
||||
}
|
||||
|
||||
// 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)
|
||||
@ -259,30 +287,25 @@ namespace BTCPayServer.Logging
|
||||
}
|
||||
}
|
||||
|
||||
private void GetScopeInformation(StringBuilder builder)
|
||||
private void GetScopeInformation(StringBuilder stringBuilder)
|
||||
{
|
||||
var current = ConsoleLogScope.Current;
|
||||
string scopeLog = string.Empty;
|
||||
var length = builder.Length;
|
||||
|
||||
while (current != null)
|
||||
var scopeProvider = ScopeProvider;
|
||||
if (scopeProvider != null)
|
||||
{
|
||||
if (length == builder.Length)
|
||||
{
|
||||
scopeLog = $"=> {current}";
|
||||
}
|
||||
else
|
||||
{
|
||||
scopeLog = $"=> {current} ";
|
||||
}
|
||||
var initialLength = stringBuilder.Length;
|
||||
|
||||
builder.Insert(length, scopeLog);
|
||||
current = current.Parent;
|
||||
}
|
||||
if (builder.Length > length)
|
||||
{
|
||||
builder.Insert(length, _messagePadding);
|
||||
builder.AppendLine();
|
||||
scopeProvider.ForEachScope((scope, state) =>
|
||||
{
|
||||
var (builder, length) = state;
|
||||
var first = length == builder.Length;
|
||||
builder.Append(first ? "=> " : " => ").Append(scope);
|
||||
}, (stringBuilder, initialLength));
|
||||
|
||||
if (stringBuilder.Length > initialLength)
|
||||
{
|
||||
stringBuilder.Insert(initialLength, _messagePadding);
|
||||
stringBuilder.AppendLine();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -333,9 +356,9 @@ namespace BTCPayServer.Logging
|
||||
// Start Console message queue processor
|
||||
_outputTask = Task.Factory.StartNew(
|
||||
ProcessLogQueue,
|
||||
this,
|
||||
default(CancellationToken),
|
||||
TaskCreationOptions.LongRunning, TaskScheduler.Default);
|
||||
state: this,
|
||||
cancellationToken: default(CancellationToken),
|
||||
creationOptions: TaskCreationOptions.LongRunning, scheduler: TaskScheduler.Default);
|
||||
}
|
||||
|
||||
public virtual void EnqueueMessage(LogMessageEntry message)
|
||||
|
37
BTCPayServer/Logging/InvoiceLog.cs
Normal file
37
BTCPayServer/Logging/InvoiceLog.cs
Normal file
@ -0,0 +1,37 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Logging
|
||||
{
|
||||
public class InvoiceLog
|
||||
{
|
||||
public DateTimeOffset Timestamp { get; set; }
|
||||
public string Log { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{Timestamp.UtcDateTime}: {Log}";
|
||||
}
|
||||
}
|
||||
public class InvoiceLogs
|
||||
{
|
||||
List<InvoiceLog> _InvoiceLogs = new List<InvoiceLog>();
|
||||
public void Write(string data)
|
||||
{
|
||||
lock (_InvoiceLogs)
|
||||
{
|
||||
_InvoiceLogs.Add(new InvoiceLog() { Timestamp = DateTimeOffset.UtcNow, Log = data });
|
||||
}
|
||||
}
|
||||
|
||||
public List<InvoiceLog> ToList()
|
||||
{
|
||||
lock (_InvoiceLogs)
|
||||
{
|
||||
return _InvoiceLogs.ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -15,9 +15,14 @@ namespace BTCPayServer.Logging
|
||||
}
|
||||
public static void Configure(ILoggerFactory factory)
|
||||
{
|
||||
Configuration = factory.CreateLogger("Configuration");
|
||||
PayServer = factory.CreateLogger("PayServer");
|
||||
Events = factory.CreateLogger("Events");
|
||||
if (factory == null)
|
||||
Configure(new FuncLoggerFactory(n => NullLogger.Instance));
|
||||
else
|
||||
{
|
||||
Configuration = factory.CreateLogger("Configuration");
|
||||
PayServer = factory.CreateLogger("PayServer");
|
||||
Events = factory.CreateLogger("Events");
|
||||
}
|
||||
}
|
||||
public static ILogger Configuration
|
||||
{
|
||||
|
@ -12,10 +12,10 @@ namespace BTCPayServer.Migrations
|
||||
name: "AspNetRoles",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<string>(type: "TEXT", nullable: false),
|
||||
ConcurrencyStamp = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Name = table.Column<string>(type: "TEXT", maxLength: 256, nullable: true),
|
||||
NormalizedName = table.Column<string>(type: "TEXT", maxLength: 256, nullable: true)
|
||||
Id = table.Column<string>(nullable: false),
|
||||
ConcurrencyStamp = table.Column<string>(nullable: true),
|
||||
Name = table.Column<string>(maxLength: 256, nullable: true),
|
||||
NormalizedName = table.Column<string>(maxLength: 256, nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
@ -26,21 +26,21 @@ namespace BTCPayServer.Migrations
|
||||
name: "AspNetUsers",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<string>(type: "TEXT", nullable: false),
|
||||
AccessFailedCount = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
ConcurrencyStamp = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Email = table.Column<string>(type: "TEXT", maxLength: 256, nullable: true),
|
||||
Id = table.Column<string>(nullable: false),
|
||||
AccessFailedCount = table.Column<int>(nullable: false),
|
||||
ConcurrencyStamp = table.Column<string>(nullable: true),
|
||||
Email = table.Column<string>(maxLength: 256, nullable: true),
|
||||
EmailConfirmed = table.Column<bool>(nullable: false),
|
||||
LockoutEnabled = table.Column<bool>(nullable: false),
|
||||
LockoutEnd = table.Column<DateTimeOffset>(nullable: true),
|
||||
NormalizedEmail = table.Column<string>(type: "TEXT", maxLength: 256, nullable: true),
|
||||
NormalizedUserName = table.Column<string>(type: "TEXT", maxLength: 256, nullable: true),
|
||||
PasswordHash = table.Column<string>(type: "TEXT", nullable: true),
|
||||
PhoneNumber = table.Column<string>(type: "TEXT", nullable: true),
|
||||
NormalizedEmail = table.Column<string>(maxLength: 256, nullable: true),
|
||||
NormalizedUserName = table.Column<string>(maxLength: 256, nullable: true),
|
||||
PasswordHash = table.Column<string>(nullable: true),
|
||||
PhoneNumber = table.Column<string>(nullable: true),
|
||||
PhoneNumberConfirmed = table.Column<bool>(nullable: false),
|
||||
SecurityStamp = table.Column<string>(type: "TEXT", nullable: true),
|
||||
SecurityStamp = table.Column<string>(nullable: true),
|
||||
TwoFactorEnabled = table.Column<bool>(nullable: false),
|
||||
UserName = table.Column<string>(type: "TEXT", maxLength: 256, nullable: true)
|
||||
UserName = table.Column<string>(maxLength: 256, nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
@ -51,12 +51,12 @@ namespace BTCPayServer.Migrations
|
||||
name: "Stores",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<string>(type: "TEXT", nullable: false),
|
||||
DerivationStrategy = table.Column<string>(type: "TEXT", nullable: true),
|
||||
SpeedPolicy = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
Id = table.Column<string>(nullable: false),
|
||||
DerivationStrategy = table.Column<string>(nullable: true),
|
||||
SpeedPolicy = table.Column<int>(nullable: false),
|
||||
StoreCertificate = table.Column<byte[]>(nullable: true),
|
||||
StoreName = table.Column<string>(type: "TEXT", nullable: true),
|
||||
StoreWebsite = table.Column<string>(type: "TEXT", nullable: true)
|
||||
StoreName = table.Column<string>(nullable: true),
|
||||
StoreWebsite = table.Column<string>(nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
@ -67,11 +67,11 @@ namespace BTCPayServer.Migrations
|
||||
name: "AspNetRoleClaims",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
Id = table.Column<int>(nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
ClaimType = table.Column<string>(type: "TEXT", nullable: true),
|
||||
ClaimValue = table.Column<string>(type: "TEXT", nullable: true),
|
||||
RoleId = table.Column<string>(type: "TEXT", nullable: false)
|
||||
ClaimType = table.Column<string>(nullable: true),
|
||||
ClaimValue = table.Column<string>(nullable: true),
|
||||
RoleId = table.Column<string>(nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
@ -88,11 +88,11 @@ namespace BTCPayServer.Migrations
|
||||
name: "AspNetUserClaims",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
Id = table.Column<int>(nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
ClaimType = table.Column<string>(type: "TEXT", nullable: true),
|
||||
ClaimValue = table.Column<string>(type: "TEXT", nullable: true),
|
||||
UserId = table.Column<string>(type: "TEXT", nullable: false)
|
||||
ClaimType = table.Column<string>(nullable: true),
|
||||
ClaimValue = table.Column<string>(nullable: true),
|
||||
UserId = table.Column<string>(nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
@ -109,10 +109,10 @@ namespace BTCPayServer.Migrations
|
||||
name: "AspNetUserLogins",
|
||||
columns: table => new
|
||||
{
|
||||
LoginProvider = table.Column<string>(type: "TEXT", nullable: false),
|
||||
ProviderKey = table.Column<string>(type: "TEXT", nullable: false),
|
||||
ProviderDisplayName = table.Column<string>(type: "TEXT", nullable: true),
|
||||
UserId = table.Column<string>(type: "TEXT", nullable: false)
|
||||
LoginProvider = table.Column<string>(nullable: false),
|
||||
ProviderKey = table.Column<string>(nullable: false),
|
||||
ProviderDisplayName = table.Column<string>(nullable: true),
|
||||
UserId = table.Column<string>(nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
@ -129,8 +129,8 @@ namespace BTCPayServer.Migrations
|
||||
name: "AspNetUserRoles",
|
||||
columns: table => new
|
||||
{
|
||||
UserId = table.Column<string>(type: "TEXT", nullable: false),
|
||||
RoleId = table.Column<string>(type: "TEXT", nullable: false)
|
||||
UserId = table.Column<string>(nullable: false),
|
||||
RoleId = table.Column<string>(nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
@ -153,10 +153,10 @@ namespace BTCPayServer.Migrations
|
||||
name: "AspNetUserTokens",
|
||||
columns: table => new
|
||||
{
|
||||
UserId = table.Column<string>(type: "TEXT", nullable: false),
|
||||
LoginProvider = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Name = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Value = table.Column<string>(type: "TEXT", nullable: true)
|
||||
UserId = table.Column<string>(nullable: false),
|
||||
LoginProvider = table.Column<string>(nullable: false),
|
||||
Name = table.Column<string>(nullable: false),
|
||||
Value = table.Column<string>(nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
@ -173,15 +173,15 @@ namespace BTCPayServer.Migrations
|
||||
name: "Invoices",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Id = table.Column<string>(nullable: false),
|
||||
Blob = table.Column<byte[]>(nullable: true),
|
||||
Created = table.Column<DateTimeOffset>(nullable: false),
|
||||
CustomerEmail = table.Column<string>(type: "TEXT", nullable: true),
|
||||
ExceptionStatus = table.Column<string>(type: "TEXT", nullable: true),
|
||||
ItemCode = table.Column<string>(type: "TEXT", nullable: true),
|
||||
OrderId = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Status = table.Column<string>(type: "TEXT", nullable: true),
|
||||
StoreDataId = table.Column<string>(type: "TEXT", nullable: true)
|
||||
CustomerEmail = table.Column<string>(nullable: true),
|
||||
ExceptionStatus = table.Column<string>(nullable: true),
|
||||
ItemCode = table.Column<string>(nullable: true),
|
||||
OrderId = table.Column<string>(nullable: true),
|
||||
Status = table.Column<string>(nullable: true),
|
||||
StoreDataId = table.Column<string>(nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
@ -198,9 +198,9 @@ namespace BTCPayServer.Migrations
|
||||
name: "UserStore",
|
||||
columns: table => new
|
||||
{
|
||||
ApplicationUserId = table.Column<string>(type: "TEXT", nullable: false),
|
||||
StoreDataId = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Role = table.Column<string>(type: "TEXT", nullable: true)
|
||||
ApplicationUserId = table.Column<string>(nullable: false),
|
||||
StoreDataId = table.Column<string>(nullable: false),
|
||||
Role = table.Column<string>(nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
@ -223,9 +223,9 @@ namespace BTCPayServer.Migrations
|
||||
name: "Payments",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Id = table.Column<string>(nullable: false),
|
||||
Blob = table.Column<byte[]>(nullable: true),
|
||||
InvoiceDataId = table.Column<string>(type: "TEXT", nullable: true)
|
||||
InvoiceDataId = table.Column<string>(nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
@ -242,9 +242,9 @@ namespace BTCPayServer.Migrations
|
||||
name: "RefundAddresses",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Id = table.Column<string>(nullable: false),
|
||||
Blob = table.Column<byte[]>(nullable: true),
|
||||
InvoiceDataId = table.Column<string>(type: "TEXT", nullable: true)
|
||||
InvoiceDataId = table.Column<string>(nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
|
@ -12,8 +12,8 @@ namespace BTCPayServer.Migrations
|
||||
name: "Settings",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Value = table.Column<string>(type: "TEXT", nullable: true)
|
||||
Id = table.Column<string>(nullable: false),
|
||||
Value = table.Column<string>(nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
|
@ -12,8 +12,8 @@ namespace BTCPayServer.Migrations
|
||||
name: "AddressInvoices",
|
||||
columns: table => new
|
||||
{
|
||||
Address = table.Column<string>(type: "TEXT", nullable: false),
|
||||
InvoiceDataId = table.Column<string>(type: "TEXT", nullable: true)
|
||||
Address = table.Column<string>(nullable: false),
|
||||
InvoiceDataId = table.Column<string>(nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
|
@ -12,13 +12,13 @@ namespace BTCPayServer.Migrations
|
||||
name: "PairedSINData",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Facade = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Label = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Name = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Id = table.Column<string>(nullable: false),
|
||||
Facade = table.Column<string>(nullable: true),
|
||||
Label = table.Column<string>(nullable: true),
|
||||
Name = table.Column<string>(nullable: true),
|
||||
PairingTime = table.Column<DateTimeOffset>(nullable: false),
|
||||
SIN = table.Column<string>(type: "TEXT", nullable: true),
|
||||
StoreDataId = table.Column<string>(type: "TEXT", nullable: true)
|
||||
SIN = table.Column<string>(nullable: true),
|
||||
StoreDataId = table.Column<string>(nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
@ -29,15 +29,15 @@ namespace BTCPayServer.Migrations
|
||||
name: "PairingCodes",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Id = table.Column<string>(nullable: false),
|
||||
DateCreated = table.Column<DateTime>(nullable: false),
|
||||
Expiration = table.Column<DateTimeOffset>(nullable: false),
|
||||
Facade = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Label = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Name = table.Column<string>(type: "TEXT", nullable: true),
|
||||
SIN = table.Column<string>(type: "TEXT", nullable: true),
|
||||
StoreDataId = table.Column<string>(type: "TEXT", nullable: true),
|
||||
TokenValue = table.Column<string>(type: "TEXT", nullable: true)
|
||||
Facade = table.Column<string>(nullable: true),
|
||||
Label = table.Column<string>(nullable: true),
|
||||
Name = table.Column<string>(nullable: true),
|
||||
SIN = table.Column<string>(nullable: true),
|
||||
StoreDataId = table.Column<string>(nullable: true),
|
||||
TokenValue = table.Column<string>(nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
|
@ -22,7 +22,7 @@ namespace BTCPayServer.Migrations
|
||||
name: "PendingInvoices",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<string>(type: "TEXT", nullable: false)
|
||||
Id = table.Column<string>(nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
|
@ -17,8 +17,8 @@ namespace BTCPayServer.Migrations
|
||||
name: "HistoricalAddressInvoices",
|
||||
columns: table => new
|
||||
{
|
||||
InvoiceDataId = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Address = table.Column<string>(type: "TEXT", nullable: false),
|
||||
InvoiceDataId = table.Column<string>(nullable: false),
|
||||
Address = table.Column<string>(nullable: false),
|
||||
Assigned = table.Column<DateTimeOffset>(nullable: false),
|
||||
UnAssigned = table.Column<DateTimeOffset>(nullable: true)
|
||||
},
|
||||
|
553
BTCPayServer/Migrations/20180429083930_legacyapikey.Designer.cs
generated
Normal file
553
BTCPayServer/Migrations/20180429083930_legacyapikey.Designer.cs
generated
Normal file
@ -0,0 +1,553 @@
|
||||
// <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("20180429083930_legacyapikey")]
|
||||
partial class legacyapikey
|
||||
{
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "2.0.2-rtm-10011");
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
|
||||
{
|
||||
b.Property<string>("Address")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<DateTimeOffset?>("CreatedTime");
|
||||
|
||||
b.Property<string>("InvoiceDataId");
|
||||
|
||||
b.HasKey("Address");
|
||||
|
||||
b.HasIndex("InvoiceDataId");
|
||||
|
||||
b.ToTable("AddressInvoices");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.APIKeyData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasMaxLength(50);
|
||||
|
||||
b.Property<string>("StoreId")
|
||||
.HasMaxLength(50);
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("StoreId");
|
||||
|
||||
b.ToTable("ApiKeys");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.AppData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<string>("AppType");
|
||||
|
||||
b.Property<DateTimeOffset>("Created");
|
||||
|
||||
b.Property<string>("Name");
|
||||
|
||||
b.Property<string>("Settings");
|
||||
|
||||
b.Property<string>("StoreDataId");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("StoreDataId");
|
||||
|
||||
b.ToTable("Apps");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b =>
|
||||
{
|
||||
b.Property<string>("InvoiceDataId");
|
||||
|
||||
b.Property<string>("Address");
|
||||
|
||||
b.Property<DateTimeOffset>("Assigned");
|
||||
|
||||
b.Property<string>("CryptoCode");
|
||||
|
||||
b.Property<DateTimeOffset?>("UnAssigned");
|
||||
|
||||
b.HasKey("InvoiceDataId", "Address");
|
||||
|
||||
b.ToTable("HistoricalAddressInvoices");
|
||||
});
|
||||
|
||||
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.InvoiceEventData", b =>
|
||||
{
|
||||
b.Property<string>("InvoiceDataId");
|
||||
|
||||
b.Property<string>("UniqueId");
|
||||
|
||||
b.Property<string>("Message");
|
||||
|
||||
b.Property<DateTimeOffset>("Timestamp");
|
||||
|
||||
b.HasKey("InvoiceDataId", "UniqueId");
|
||||
|
||||
b.ToTable("InvoiceEvents");
|
||||
});
|
||||
|
||||
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<bool>("Accounted");
|
||||
|
||||
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>("DefaultCrypto");
|
||||
|
||||
b.Property<string>("DerivationStrategies");
|
||||
|
||||
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("AddressInvoices")
|
||||
.HasForeignKey("InvoiceDataId");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.AppData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
|
||||
.WithMany("Apps")
|
||||
.HasForeignKey("StoreDataId");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData")
|
||||
.WithMany("HistoricalAddressInvoices")
|
||||
.HasForeignKey("InvoiceDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
|
||||
.WithMany()
|
||||
.HasForeignKey("StoreDataId");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.InvoiceEventData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData")
|
||||
.WithMany("Events")
|
||||
.HasForeignKey("InvoiceDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
35
BTCPayServer/Migrations/20180429083930_legacyapikey.cs
Normal file
35
BTCPayServer/Migrations/20180429083930_legacyapikey.cs
Normal file
@ -0,0 +1,35 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace BTCPayServer.Migrations
|
||||
{
|
||||
public partial class legacyapikey : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ApiKeys",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<string>(maxLength: 50, nullable: false),
|
||||
StoreId = table.Column<string>(maxLength: 50, nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ApiKeys", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ApiKeys_StoreId",
|
||||
table: "ApiKeys",
|
||||
column: "StoreId");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "ApiKeys");
|
||||
}
|
||||
}
|
||||
}
|
578
BTCPayServer/Migrations/20180719095626_CanDeleteStores.Designer.cs
generated
Normal file
578
BTCPayServer/Migrations/20180719095626_CanDeleteStores.Designer.cs
generated
Normal file
@ -0,0 +1,578 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using BTCPayServer.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
namespace BTCPayServer.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20180719095626_CanDeleteStores")]
|
||||
partial class CanDeleteStores
|
||||
{
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "2.1.0-rtm-30799");
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
|
||||
{
|
||||
b.Property<string>("Address")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<DateTimeOffset?>("CreatedTime");
|
||||
|
||||
b.Property<string>("InvoiceDataId");
|
||||
|
||||
b.HasKey("Address");
|
||||
|
||||
b.HasIndex("InvoiceDataId");
|
||||
|
||||
b.ToTable("AddressInvoices");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.APIKeyData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasMaxLength(50);
|
||||
|
||||
b.Property<string>("StoreId")
|
||||
.HasMaxLength(50);
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("StoreId");
|
||||
|
||||
b.ToTable("ApiKeys");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.AppData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<string>("AppType");
|
||||
|
||||
b.Property<DateTimeOffset>("Created");
|
||||
|
||||
b.Property<string>("Name");
|
||||
|
||||
b.Property<string>("Settings");
|
||||
|
||||
b.Property<string>("StoreDataId");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("StoreDataId");
|
||||
|
||||
b.ToTable("Apps");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b =>
|
||||
{
|
||||
b.Property<string>("InvoiceDataId");
|
||||
|
||||
b.Property<string>("Address");
|
||||
|
||||
b.Property<DateTimeOffset>("Assigned");
|
||||
|
||||
b.Property<string>("CryptoCode");
|
||||
|
||||
b.Property<DateTimeOffset?>("UnAssigned");
|
||||
|
||||
b.HasKey("InvoiceDataId", "Address");
|
||||
|
||||
b.ToTable("HistoricalAddressInvoices");
|
||||
});
|
||||
|
||||
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.InvoiceEventData", b =>
|
||||
{
|
||||
b.Property<string>("InvoiceDataId");
|
||||
|
||||
b.Property<string>("UniqueId");
|
||||
|
||||
b.Property<string>("Message");
|
||||
|
||||
b.Property<DateTimeOffset>("Timestamp");
|
||||
|
||||
b.HasKey("InvoiceDataId", "UniqueId");
|
||||
|
||||
b.ToTable("InvoiceEvents");
|
||||
});
|
||||
|
||||
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<bool>("Accounted");
|
||||
|
||||
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");
|
||||
|
||||
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>("DefaultCrypto");
|
||||
|
||||
b.Property<string>("DerivationStrategies");
|
||||
|
||||
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("AddressInvoices")
|
||||
.HasForeignKey("InvoiceDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.APIKeyData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
|
||||
.WithMany("APIKeys")
|
||||
.HasForeignKey("StoreId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.AppData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
|
||||
.WithMany("Apps")
|
||||
.HasForeignKey("StoreDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
||||
.WithMany("HistoricalAddressInvoices")
|
||||
.HasForeignKey("InvoiceDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
|
||||
.WithMany("Invoices")
|
||||
.HasForeignKey("StoreDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.InvoiceEventData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
||||
.WithMany("Events")
|
||||
.HasForeignKey("InvoiceDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PairedSINData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
|
||||
.WithMany("PairedSINs")
|
||||
.HasForeignKey("StoreDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PaymentData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
||||
.WithMany("Payments")
|
||||
.HasForeignKey("InvoiceDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
||||
.WithMany("PendingInvoices")
|
||||
.HasForeignKey("Id")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
||||
.WithMany("RefundAddresses")
|
||||
.HasForeignKey("InvoiceDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
172
BTCPayServer/Migrations/20180719095626_CanDeleteStores.cs
Normal file
172
BTCPayServer/Migrations/20180719095626_CanDeleteStores.cs
Normal file
@ -0,0 +1,172 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace BTCPayServer.Migrations
|
||||
{
|
||||
public partial class CanDeleteStores : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
if (this.SupportDropForeignKey(migrationBuilder.ActiveProvider))
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_AddressInvoices_Invoices_InvoiceDataId",
|
||||
table: "AddressInvoices");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Apps_Stores_StoreDataId",
|
||||
table: "Apps");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Invoices_Stores_StoreDataId",
|
||||
table: "Invoices");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Payments_Invoices_InvoiceDataId",
|
||||
table: "Payments");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_RefundAddresses_Invoices_InvoiceDataId",
|
||||
table: "RefundAddresses");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_AddressInvoices_Invoices_InvoiceDataId",
|
||||
table: "AddressInvoices",
|
||||
column: "InvoiceDataId",
|
||||
principalTable: "Invoices",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_ApiKeys_Stores_StoreId",
|
||||
table: "ApiKeys",
|
||||
column: "StoreId",
|
||||
principalTable: "Stores",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Apps_Stores_StoreDataId",
|
||||
table: "Apps",
|
||||
column: "StoreDataId",
|
||||
principalTable: "Stores",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Invoices_Stores_StoreDataId",
|
||||
table: "Invoices",
|
||||
column: "StoreDataId",
|
||||
principalTable: "Stores",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_PairedSINData_Stores_StoreDataId",
|
||||
table: "PairedSINData",
|
||||
column: "StoreDataId",
|
||||
principalTable: "Stores",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Payments_Invoices_InvoiceDataId",
|
||||
table: "Payments",
|
||||
column: "InvoiceDataId",
|
||||
principalTable: "Invoices",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_PendingInvoices_Invoices_Id",
|
||||
table: "PendingInvoices",
|
||||
column: "Id",
|
||||
principalTable: "Invoices",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_RefundAddresses_Invoices_InvoiceDataId",
|
||||
table: "RefundAddresses",
|
||||
column: "InvoiceDataId",
|
||||
principalTable: "Invoices",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_AddressInvoices_Invoices_InvoiceDataId",
|
||||
table: "AddressInvoices");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_ApiKeys_Stores_StoreId",
|
||||
table: "ApiKeys");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Apps_Stores_StoreDataId",
|
||||
table: "Apps");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Invoices_Stores_StoreDataId",
|
||||
table: "Invoices");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_PairedSINData_Stores_StoreDataId",
|
||||
table: "PairedSINData");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Payments_Invoices_InvoiceDataId",
|
||||
table: "Payments");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_PendingInvoices_Invoices_Id",
|
||||
table: "PendingInvoices");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_RefundAddresses_Invoices_InvoiceDataId",
|
||||
table: "RefundAddresses");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_AddressInvoices_Invoices_InvoiceDataId",
|
||||
table: "AddressInvoices",
|
||||
column: "InvoiceDataId",
|
||||
principalTable: "Invoices",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Apps_Stores_StoreDataId",
|
||||
table: "Apps",
|
||||
column: "StoreDataId",
|
||||
principalTable: "Stores",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Invoices_Stores_StoreDataId",
|
||||
table: "Invoices",
|
||||
column: "StoreDataId",
|
||||
principalTable: "Stores",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Payments_Invoices_InvoiceDataId",
|
||||
table: "Payments",
|
||||
column: "InvoiceDataId",
|
||||
principalTable: "Invoices",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_RefundAddresses_Invoices_InvoiceDataId",
|
||||
table: "RefundAddresses",
|
||||
column: "InvoiceDataId",
|
||||
principalTable: "Invoices",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,13 +1,9 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
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;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
namespace BTCPayServer.Migrations
|
||||
{
|
||||
@ -18,7 +14,7 @@ namespace BTCPayServer.Migrations
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "2.0.1-rtm-125");
|
||||
.HasAnnotation("ProductVersion", "2.1.0-rtm-30799");
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
|
||||
{
|
||||
@ -36,6 +32,22 @@ namespace BTCPayServer.Migrations
|
||||
b.ToTable("AddressInvoices");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.APIKeyData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasMaxLength(50);
|
||||
|
||||
b.Property<string>("StoreId")
|
||||
.HasMaxLength(50);
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("StoreId");
|
||||
|
||||
b.ToTable("ApiKeys");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.AppData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
@ -186,8 +198,7 @@ namespace BTCPayServer.Migrations
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
b.Property<string>("Id");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
@ -426,19 +437,29 @@ namespace BTCPayServer.Migrations
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
||||
.WithMany("AddressInvoices")
|
||||
.HasForeignKey("InvoiceDataId");
|
||||
.HasForeignKey("InvoiceDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.APIKeyData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
|
||||
.WithMany("APIKeys")
|
||||
.HasForeignKey("StoreId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.AppData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
|
||||
.WithMany("Apps")
|
||||
.HasForeignKey("StoreDataId");
|
||||
.HasForeignKey("StoreDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData")
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
||||
.WithMany("HistoricalAddressInvoices")
|
||||
.HasForeignKey("InvoiceDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
@ -447,30 +468,49 @@ namespace BTCPayServer.Migrations
|
||||
modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
|
||||
.WithMany()
|
||||
.HasForeignKey("StoreDataId");
|
||||
.WithMany("Invoices")
|
||||
.HasForeignKey("StoreDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.InvoiceEventData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData")
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
||||
.WithMany("Events")
|
||||
.HasForeignKey("InvoiceDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PairedSINData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
|
||||
.WithMany("PairedSINs")
|
||||
.HasForeignKey("StoreDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PaymentData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
||||
.WithMany("Payments")
|
||||
.HasForeignKey("InvoiceDataId");
|
||||
.HasForeignKey("InvoiceDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
||||
.WithMany("PendingInvoices")
|
||||
.HasForeignKey("Id")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
||||
.WithMany("RefundAddresses")
|
||||
.HasForeignKey("InvoiceDataId");
|
||||
.HasForeignKey("InvoiceDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.UserStore", b =>
|
||||
|
58
BTCPayServer/ModelBinders/DerivationSchemeModelBinder.cs
Normal file
58
BTCPayServer/ModelBinders/DerivationSchemeModelBinder.cs
Normal file
@ -0,0 +1,58 @@
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using NBitcoin;
|
||||
using System.Reflection;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc.Internal;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
|
||||
namespace BTCPayServer.ModelBinders
|
||||
{
|
||||
public class DerivationSchemeModelBinder : IModelBinder
|
||||
{
|
||||
public DerivationSchemeModelBinder()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
#region IModelBinder Members
|
||||
|
||||
public Task BindModelAsync(ModelBindingContext bindingContext)
|
||||
{
|
||||
if (!typeof(DerivationStrategyBase).GetTypeInfo().IsAssignableFrom(bindingContext.ModelType))
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
ValueProviderResult val = bindingContext.ValueProvider.GetValue(
|
||||
bindingContext.ModelName);
|
||||
if (val == null)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
string key = val.FirstValue as string;
|
||||
if (key == null)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
var networkProvider = (BTCPayNetworkProvider)bindingContext.HttpContext.RequestServices.GetService(typeof(BTCPayNetworkProvider));
|
||||
var cryptoCode = bindingContext.ValueProvider.GetValue("cryptoCode").FirstValue;
|
||||
var network = networkProvider.GetNetwork(cryptoCode ?? "BTC");
|
||||
try
|
||||
{
|
||||
var data = new DerivationStrategyFactory(network.NBitcoinNetwork).Parse(key);
|
||||
if (!bindingContext.ModelType.IsInstanceOfType(data))
|
||||
{
|
||||
throw new FormatException("Invalid destination type");
|
||||
}
|
||||
bindingContext.Result = ModelBindingResult.Success(data);
|
||||
}
|
||||
catch { throw new FormatException("Invalid derivation scheme"); }
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
39
BTCPayServer/ModelBinders/WalletIdModelBinder.cs
Normal file
39
BTCPayServer/ModelBinders/WalletIdModelBinder.cs
Normal file
@ -0,0 +1,39 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
|
||||
namespace BTCPayServer.ModelBinders
|
||||
{
|
||||
public class WalletIdModelBinder : IModelBinder
|
||||
{
|
||||
public Task BindModelAsync(ModelBindingContext bindingContext)
|
||||
{
|
||||
if (!typeof(WalletId).GetTypeInfo().IsAssignableFrom(bindingContext.ModelType))
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
ValueProviderResult val = bindingContext.ValueProvider.GetValue(
|
||||
bindingContext.ModelName);
|
||||
if (val == null)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
string key = val.FirstValue as string;
|
||||
if (key == null)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
if(WalletId.TryParse(key, out var walletId))
|
||||
{
|
||||
bindingContext.Result = ModelBindingResult.Success(walletId);
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
@ -17,5 +17,12 @@ namespace BTCPayServer.Models.AppViewModels
|
||||
[Required]
|
||||
[MaxLength(5000)]
|
||||
public string Template { get; set; }
|
||||
|
||||
[Display(Name = "User can input custom amount")]
|
||||
public bool ShowCustomAmount { get; set; }
|
||||
public string Example1 { get; internal set; }
|
||||
public string Example2 { get; internal set; }
|
||||
public string ExampleCallback { get; internal set; }
|
||||
public string InvoiceUrl { get; internal set; }
|
||||
}
|
||||
}
|
||||
|
@ -18,6 +18,8 @@ namespace BTCPayServer.Models.AppViewModels
|
||||
public ItemPrice Price { get; set; }
|
||||
public string Title { get; set; }
|
||||
}
|
||||
public bool ShowCustomAmount { get; set; }
|
||||
public string Step { get; set; }
|
||||
public string Title { get; set; }
|
||||
public Item[] Items { get; set; }
|
||||
}
|
||||
|
@ -19,5 +19,6 @@ namespace BTCPayServer.Models
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string ButtonClass { get; set; } = "btn-danger";
|
||||
}
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user